diff --git a/.github/workflows/chart-lint-publish.yml b/.github/workflows/chart-lint-publish.yml new file mode 100644 index 000000000..8d5f99011 --- /dev/null +++ b/.github/workflows/chart-lint-publish.yml @@ -0,0 +1,63 @@ +name: Validate / Publish helm charts + +on: + release: + types: [published] + pull_request: + types: [opened, reopened, synchronize] + paths: + - 'helm/**' + workflow_dispatch: + inputs: + IGNORE_CHARTS: + description: 'Provide list of charts to be ignored separated by pipe(|)' + required: false + default: '' + type: string + CHART_PUBLISH: + description: 'Chart publishing to gh-pages branch' + required: false + default: 'NO' + type: string + options: + - YES + - NO + INCLUDE_ALL_CHARTS: + description: 'Include all charts for Linting/Publishing (YES/NO)' + required: false + default: 'NO' + type: string + options: + - YES + - NO + push: + branches: + - '!release-branch' + - master + - 1.* + - 0.* + - develop + - MOSIP* + - release* + - INJI* + paths: + - 'helm/**' + +jobs: + chart-lint-publish: + uses: mosip/kattu/.github/workflows/chart-lint-publish.yml@master + with: + CHARTS_DIR: ./helm + CHARTS_URL: https://inji.github.io/helm + REPOSITORY: helm + BRANCH: gh-pages + INCLUDE_ALL_CHARTS: "${{ inputs.INCLUDE_ALL_CHARTS || 'NO' }}" + IGNORE_CHARTS: "${{ inputs.IGNORE_CHARTS ||'reporting|reporting-init|activemq-artemis' }}" + CHART_PUBLISH: "${{ inputs.CHART_PUBLISH || 'YES' }}" + LINTING_CHART_SCHEMA_YAML_URL: "https://raw.githubusercontent.com/mosip/kattu/master/.github/helm-lint-configs/chart-schema.yaml" + LINTING_LINTCONF_YAML_URL: "https://raw.githubusercontent.com/mosip/kattu/master/.github/helm-lint-configs/lintconf.yaml" + LINTING_CHART_TESTING_CONFIG_YAML_URL: "https://raw.githubusercontent.com/mosip/kattu/master/.github/helm-lint-configs/chart-testing-config.yaml" + LINTING_HEALTH_CHECK_SCHEMA_YAML_URL: "https://raw.githubusercontent.com/mosip/kattu/master/.github/helm-lint-configs/health-check-schema.yaml" + secrets: + TOKEN: ${{ secrets.ACTION_PAT }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} diff --git a/.github/workflows/clear-artifacts.yml b/.github/workflows/clear-artifacts.yml new file mode 100644 index 000000000..3beff6243 --- /dev/null +++ b/.github/workflows/clear-artifacts.yml @@ -0,0 +1,17 @@ +name: 'Delete old artifacts' +on: + schedule: + - cron: '0 * * * *' # Every hour + workflow_dispatch: + inputs: + message: + description: 'Message for manually triggering' + required: false + default: 'Triggered for Updates' + type: string +jobs: + delete-artifacts: + uses: mosip/kattu/.github/workflows/clear-artifacts.yml@master + secrets: + ACCESS_TOKEN: ${{ secrets.access_token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} diff --git a/.github/workflows/clear_artifacts.yml b/.github/workflows/clear_artifacts.yml deleted file mode 100644 index 85991ce3c..000000000 --- a/.github/workflows/clear_artifacts.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 'Delete old artifacts' -on: - schedule: - - cron: '0 * * * *' # every hour - -jobs: - delete-artifacts: - runs-on: ubuntu-latest - steps: - - name: Purge Artifacts - if: env.ACCESS_TOKEN != null - uses: kolpav/purge-artifacts-action@v1 - with: - token: ${{ secrets.access_token }} - expire-in: 2days # Setting this to 0 will delete all artifacts - env: - ACCESS_TOKEN: ${{ secrets.access_token }} diff --git a/.github/workflows/push-trigger.yml b/.github/workflows/push-trigger.yml new file mode 100644 index 000000000..a64c07566 --- /dev/null +++ b/.github/workflows/push-trigger.yml @@ -0,0 +1,191 @@ +name: Maven Package upon a push + +on: + release: + types: [published] + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: + inputs: + message: + description: 'Message for manually triggering' + required: false + default: 'Triggered for Updates' + type: string + push: + branches: + - '!release-branch' + - release* + - master + - 1.* + - develop* + - MOSIP* + - qa-develop + - inji*-* + +jobs: + build-mimoto: + uses: mosip/kattu/.github/workflows/maven-build.yml@master-java21 + with: + SERVICE_LOCATION: ./ + BUILD_ARTIFACT: mimoto + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} + + ## Recreate zips to provide support for docker unzip operation + recreate-zips: + needs: build-mimoto + runs-on: ubuntu-latest + env: + BUILD_ARTIFACT: mimoto + NEW_BUILD_ARTIFACT: mimoto-recreated + SERVICE_LOCATION: mimoto + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v4 + if: ${{ ( env.BUILD_ARTIFACT != 'false' ) }} + with: + name: ${{ env.BUILD_ARTIFACT }} + path: ../ + - name: recreate zip + run: | + cd ../ + + echo "LS ./" + ls . + echo "unzip command" + unzip -uj "${{ env.BUILD_ARTIFACT }}.zip" "target/*" -d "${{ env.SERVICE_LOCATION }}/target/" + + echo "find command" + cd ${{ env.SERVICE_LOCATION }} + find ../${{ env.SERVICE_LOCATION }} -path '*/target/*' -name '*.jar' -type f -executable -exec zip ${{ env.NEW_BUILD_ARTIFACT }}.zip {} + + + - name: Upload the springboot jars + if: ${{ !contains(github.ref, 'master') }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.NEW_BUILD_ARTIFACT }} + path: ${{ env.NEW_BUILD_ARTIFACT }}.zip + + sonar_analysis: + needs: build-mimoto + if: "${{ github.event_name != 'pull_request' }}" + uses: mosip/kattu/.github/workflows/maven-sonar-analysis.yml@dsd9685 + with: + SERVICE_LOCATION: ./ + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_KEY: ${{ secrets.ORG_KEY }} + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} + + build-dockers: + needs: recreate-zips + strategy: + matrix: + include: + - SERVICE_LOCATION: '../mimoto' + SERVICE_NAME: 'mimoto' + BUILD_ARTIFACT: 'mimoto-recreated' + SQUASH_LAYERS: '8' + fail-fast: false + uses: mosip/kattu/.github/workflows/docker-build.yml@master-java21 + name: ${{ matrix.SERVICE_NAME }} + with: + SERVICE_LOCATION: ${{ matrix.SERVICE_LOCATION }} + SERVICE_NAME: ${{ matrix.SERVICE_NAME }} + BUILD_ARTIFACT: ${{ matrix.BUILD_ARTIFACT }} + SQUASH_LAYERS: ${{ matrix.SQUASH_LAYERS }} + secrets: + DEV_NAMESPACE_DOCKER_HUB: ${{ secrets.DEV_NAMESPACE_DOCKER_HUB }} + ACTOR_DOCKER_HUB: ${{ secrets.ACTOR_DOCKER_HUB }} + RELEASE_DOCKER_HUB: ${{ secrets.RELEASE_DOCKER_HUB }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} + + build-maven-apitest-mimoto: + uses: mosip/kattu/.github/workflows/maven-build.yml@master-java21 + with: + SERVICE_LOCATION: ./api-test + BUILD_ARTIFACT: apitest-mimoto + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} + + build-apitest-mimoto-local: + needs: build-maven-apitest-mimoto + runs-on: ubuntu-latest + env: + NAMESPACE: ${{ secrets.dev_namespace_docker_hub }} + SERVICE_NAME: apitest-mimoto + SERVICE_LOCATION: api-test + BUILD_ARTIFACT: apitest-mimoto-local + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + - name: Setup the settings file for ossrh server + run: echo " ossrh ${{secrets.ossrh_user}} ${{secrets.ossrh_secret}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml + - name: Build Automationtests with Maven + run: | + cd ${{ env.SERVICE_LOCATION}} + mvn clean package -s $GITHUB_WORKSPACE/settings.xml + - name: Copy configuration files to target directory. + run: | + cp -r ${{ env.SERVICE_LOCATION}}/target/classes/config ${{ env.SERVICE_LOCATION}}/target/config + cp -r ${{ env.SERVICE_LOCATION}}/testNgXmlFiles ${{ env.SERVICE_LOCATION}}/target/testNgXmlFiles + - name: Ready the springboot artifacts + if: ${{ !contains(github.ref, 'master') }} + run: | + ## FIND JARS & COPY ONLY EXECUTABLE JARs STORED UNDER TARGET DIRECTORY + find ${{ env.SERVICE_LOCATION }} -path '*/target/*' -exec zip ${{ env.BUILD_ARTIFACT }}.zip {} + + - name: Upload the springboot jars + if: ${{ !contains(github.ref, 'master') }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BUILD_ARTIFACT }} + path: ${{ env.BUILD_ARTIFACT }}.zip + - uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,author,commit,workflow,job # selectable (default: repo,message) + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required + if: failure() # Pick up events even if the job fails or is canceled. + + build-dockers_apitest_mimoto: + needs: build-apitest-mimoto-local + strategy: + matrix: + include: + - SERVICE_LOCATION: 'api-test' + SERVICE_NAME: 'apitest-mimoto' + BUILD_ARTIFACT: 'apitest-mimoto-local' + ONLY_DOCKER: true + fail-fast: false + name: ${{ matrix.SERVICE_NAME }} + uses: mosip/kattu/.github/workflows/docker-build.yml@master-java21 + with: + SERVICE_LOCATION: ${{ matrix.SERVICE_LOCATION }} + SERVICE_NAME: ${{ matrix.SERVICE_NAME }} + BUILD_ARTIFACT: ${{ matrix.BUILD_ARTIFACT }} + ONLY_DOCKER: ${{ matrix.ONLY_DOCKER }} + secrets: + DEV_NAMESPACE_DOCKER_HUB: ${{ secrets.DEV_NAMESPACE_DOCKER_HUB }} + ACTOR_DOCKER_HUB: ${{ secrets.ACTOR_DOCKER_HUB }} + RELEASE_DOCKER_HUB: ${{ secrets.RELEASE_DOCKER_HUB }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_INJI_TEAM }} diff --git a/.github/workflows/push_trigger.yml b/.github/workflows/push_trigger.yml deleted file mode 100644 index 2434c9495..000000000 --- a/.github/workflows/push_trigger.yml +++ /dev/null @@ -1,249 +0,0 @@ - -name: Maven Package upon a push - -on: - push: - branches: - - master - - 1.* - - develop - - release* - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - ref: ${{ github.ref }} - java-version: 11 - server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup branch and env - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo "GPG_TTY=$(tty)" >> $GITHUB_ENV - - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo ${{ env.BRANCH_NAME }} - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven-${{ env.BRANCH_NAME }} - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install libxml2-utils - - name: Setup the settings file for ossrh server - run: echo " ossrh ${{secrets.ossrh_user}} ${{secrets.ossrh_secret}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml - - - name: Build with Maven - run: mvn -U -B package --file pom.xml -s $GITHUB_WORKSPACE/settings.xml - - - name: Ready the springboot artifacts - run: find -name '*.jar' -executable -type f -exec zip release.zip {} + - - - name: Upload the springboot jars - uses: actions/upload-artifact@v1 - with: - name: release - path: ./release.zip - - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,author,workflow # selectable (default: repo,message) - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} # required - if: failure() # Pick up events even if the job fails or is canceled. - - publish_to_nexus: - if: "!contains(github.ref, 'master')" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - ref: ${{ github.ref }} - java-version: 11 - server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup branch and env - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo "GPG_TTY=$(tty)" >> $GITHUB_ENV - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo ${{ env.BRANCH_NAME }} - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven-${{ env.BRANCH_NAME }} - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install libxml2-utils - - name: Setup the settings file for ossrh server - run: echo " ossrh ossrh ${{secrets.RELEASE_USER}} ${{secrets.RELEASE_TOKEN}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml - - - name: Build with Maven - run: mvn -B package --file pom.xml -s $GITHUB_WORKSPACE/settings.xml - - - name: Publish the maven package - run: | - mvn -B deploy -DaltDeploymentRepository=ossrh::default::${{ secrets.OSSRH_SNAPSHOT_URL }} -s $GITHUB_WORKSPACE/settings.xml -f pom.xml - env: - GITHUB_TOKEN: ${{secrets.RELEASE_TOKEN}} - GPG_TTY: $(tty) - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,author,workflow # selectable (default: repo,message) - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEVOPS_WEBHOOK }} # required - if: failure() # Pick up events even if the job fails or is canceled. - - - mimoto-docker: - needs: build - runs-on: ubuntu-latest - env: - NAMESPACE: ${{ secrets. dev_namespace_docker_hub }} - SERVICE_NAME: mimoto - - steps: - - uses: actions/checkout@v2 - - uses: actions/download-artifact@v1 - with: - name: release - path: ./ - - - name: Setup branch name - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo ${{ env.BRANCH_NAME }} - - - name: Get version info from pom - id: getPomVersion - uses: mavrosxristoforos/get-xml-info@1.0 - with: - xml-file: ./pom.xml - xpath: /*[local-name()="project"]/*[local-name()="version"] - - - name: Unzip and extract the id-repository-vid-service - run: unzip -uj "release.zip" "target/*" -d "./target" - - - name: Build image - run: | - docker build . --file Dockerfile --tag ${{ env.SERVICE_NAME }} - - - name: Log into registry - run: echo "${{ secrets.release_docker_hub }}" | docker login -u ${{ secrets.actor_docker_hub }} --password-stdin - - - name: Push image - run: | - IMAGE_ID=$NAMESPACE/$SERVICE_NAME - - # Change all uppercase to lowercase - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - echo "push version ${{steps.getPomVersion.outputs.info}}" - if [[ $BRANCH_NAME == 'master' ]]; then - VERSION=latest - else - VERSION=$BRANCH_NAME - fi - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $SERVICE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,author,workflow # selectable (default: repo,message) - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} # required - if: failure() # Pick up events even if the job fails or is canceled. - - - Sonar_Analysis: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - ref: ${{ github.ref }} - java-version: 11 - server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup branch and env - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo "GPG_TTY=$(tty)" >> $GITHUB_ENV - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo ${{ env.BRANCH_NAME }} - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven-${{ env.BRANCH_NAME }} - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install libxml2-utils - - name: Setup the settings file for ossrh server - run: echo " ossrh ${{secrets.ossrh_user}} ${{secrets.ossrh_secret}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml - - - name: Build with Maven - run: mvn -U -B package --file pom.xml -s $GITHUB_WORKSPACE/settings.xml - - - name: analyze with sonar - run: mvn -Dgpg.skip -B verify sonar:sonar -Dsonar.projectKey=mosip_${{ github.event.repository.name }} -Dsonar.organization=${{ secrets.ORG_KEY }} -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,author,workflow # selectable (default: repo,message) - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEVOPS_WEBHOOK }} # required - if: failure() # Pick up events even if the job fails or is canceled. diff --git a/.github/workflows/release_changes.yml b/.github/workflows/release_changes.yml deleted file mode 100644 index cc9c2956d..000000000 --- a/.github/workflows/release_changes.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Release/pre-release Preparation. - -on: - workflow_dispatch: - inputs: - message: - description: 'Triggered for release or pe-release' - required: false - default: 'Release Preparation' - releaseTags: - description: 'tag to update' - required: true - snapshotTags: - description: 'tag to be replaced' - required: true - base: - description: 'base branch for PR' - required: true -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup branch and env - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo "GPG_TTY=$(tty)" >> $GITHUB_ENV - - - name: Mannualy changing the pom versions - run: find . -type f -name "*pom.xml" -print0 | xargs -0 sed -i "s/${{ github.event.inputs.snapshotTags }}/${{ github.event.inputs.releaseTags }}/g" - - - name: Updating the Release URL in POM - run: | - cd .github/workflows - sed -i 's/OSSRH_SNAPSHOT_URL/RELEASE_URL/g' push_trigger.yml - - - name: Updating libs-snapshot-local to libs-release local for artifactory URL's. - run: find . -type f -name "*Dockerfile" -print0 | xargs -0 sed -i "s/libs-snapshot-local/libs-release-local/g" - - - name: removing -DskipTests - run: find . -type f -name "*push_trigger.yml" -print0 | xargs -0 sed -i "s/"-DskipTests"//g" - -# - name: removing --Dgpg.skip -# run: find . -type f -name "*push_trigger.yml" -print0 | xargs -0 sed -i "s/"-Dgpg.skip"//g" - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 - with: - token: ${{ secrets.ACTION_PAT }} - commit-message: Updated Pom versions for release changes - title: Release changes - body: Automated PR for ${{ github.event.inputs.releaseTags }} release. - branch: release-branch - delete-branch: true - base: ${{ github.event.inputs.base }} diff --git a/.github/workflows/release_trigger.yml b/.github/workflows/release_trigger.yml deleted file mode 100644 index dbc836db8..000000000 --- a/.github/workflows/release_trigger.yml +++ /dev/null @@ -1,168 +0,0 @@ -# This workflow will build and publish package using Maven, Docker when a release is published - -name: Release maven packages and docker upon a release - -on: - release: - types: [published] - - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - ref: ${{ github.ref }} - java-version: 11 - server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo "::set-env name=BRANCH_NAME::$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" - echo ${{ env.BRANCH_NAME }} - echo "::set-env name=GPG_TTY::$(tty)" - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven-${{ env.BRANCH_NAME }} - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install libxml2-utils - - - name: Setup the settings file for ossrh server - run: echo " ossrh ${{secrets.ossrh_user}} ${{secrets.ossrh_secret}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml - - - name: Build with Maven - run: | - mvn -B package --file pom.xml -s $GITHUB_WORKSPACE/settings.xml - - - name: Ready the springboot artifacts - run: find -name '*.jar' -executable -type f -exec zip release.zip {} + - - - name: Upload the springboot jars - uses: actions/upload-artifact@v1 - with: - name: release - path: ./release.zip - - publish_to_nexus: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - ref: ${{ github.ref }} - java-version: 11 - server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo "::set-env name=BRANCH_NAME::$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" - echo ${{ env.BRANCH_NAME }} - echo "::set-env name=GPG_TTY::$(tty)" - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - - uses: actions/cache@v1 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven-${{ env.BRANCH_NAME }} - - - name: Install xmllint - run: | - sudo apt-get update - sudo apt-get install libxml2-utils - - name: Setup the settings file for ossrh server - run: echo " ossrh ${{secrets.ossrh_user}} ${{secrets.ossrh_secret}} ossrh true gpg2 ${{secrets.gpg_secret}} allow-snapshots true snapshots-repo https://oss.sonatype.org/content/repositories/snapshots false true releases-repo https://oss.sonatype.org/service/local/staging/deploy/maven2 true false sonar . https://sonarcloud.io false " > $GITHUB_WORKSPACE/settings.xml - - - name: Build with Maven - run: | - mvn -B package --file pom.xml -s $GITHUB_WORKSPACE/settings.xml - - - name: Publish the maven package - run: | - chmod +x ./deploy.sh - ./deploy.sh . $GITHUB_WORKSPACE/settings.xml .* - env: - GPG_TTY: $(tty) - - name: Analyze with SonarCloud - run: | - mvn -B verify sonar:sonar -Dsonar.projectKey=${{ secrets.PROJECT_KEY }} -Dsonar.organization=${{ secrets.ORG_KEY }} -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${{ secrets.SONAR_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - docker-mimoto: - needs: build - - runs-on: ubuntu-latest - env: - NAMESPACE: ${{ secrets. dev_namespace_docker_hub }} - SERVICE_NAME: mimoto - SERVICE_LOCATION: mimoto - - steps: - - uses: actions/checkout@v2 - - uses: actions/download-artifact@v1 - with: - name: release - path: ./ - - - name: Setup branch name - run: | - # Strip git ref prefix from version - echo "::set-env name=BRANCH_NAME::$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" - echo ${{ env.BRANCH_NAME }} - - name: Get version info from pom - id: getPomVersion - uses: mavrosxristoforos/get-xml-info@1.0 - with: - xml-file: ./${{ env.SERVICE_LOCATION }}/pom.xml - xpath: /*[local-name()="project"]/*[local-name()="version"] - - - name: Unzip and extract the mimoto service - run: unzip -uj "release.zip" "${{ env.SERVICE_LOCATION }}/target/*" -d "./${{ env.SERVICE_LOCATION }}/target" - - - name: Build image - run: | - cd "./${{env.SERVICE_LOCATION}}" - docker build . --file Dockerfile --tag ${{ env.SERVICE_NAME }} - - name: Log into registry - run: echo "${{ secrets.release_docker_hub }}" | docker login -u ${{ secrets.actor_docker_hub }} --password-stdin - - - name: Push image - run: | - IMAGE_ID=$NAMESPACE/$SERVICE_NAME - - # Change all uppercase to lowercase - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - echo "push version ${{steps.getPomVersion.outputs.info}}" - VERSION=$BRANCH_NAME - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $SERVICE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index f30c7b5c8..000000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: SonarQube checks -on: - push: - branches: - - develop - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - name: Setup branch and env - run: | - # Strip git ref prefix from version - echo "BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,')" >> $GITHUB_ENV - echo "GPG_TTY=$(tty)" >> $GITHUB_ENV - - name: Setup branch and GPG public key - run: | - # Strip git ref prefix from version - echo ${{ env.BRANCH_NAME }} - echo ${{ env.GPG_TTY }} - sudo apt-get --yes install gnupg2 - gpg2 --import ./.github/keys/mosipgpgkey_pub.gpg - gpg2 --quiet --batch --passphrase=${{secrets.gpg_secret}} --allow-secret-key-import --import ./.github/keys/mosipgpgkey_sec.gpg - - name: Cache SonarQube packages - uses: actions/cache@v1 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v1 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: mvn -Dgpg.skip -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=mimoto diff --git a/.gitignore b/.gitignore index 87ba6d123..804af1755 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,5 @@ build/ ### VS Code ### .vscode/ *.code-workspace - -data \ No newline at end of file +.DS_Store +data diff --git a/.talismanrc b/.talismanrc new file mode 100644 index 000000000..30960d89b --- /dev/null +++ b/.talismanrc @@ -0,0 +1,255 @@ +fileignoreconfig: +- filename: certs/oidckeystore.p12 + checksum: d247acadb5fac098880011f0d0f8fa17223d78f67fa815525eff32616f5ff50c +- filename: src/main/resources/keystore.p12 + checksum: a4ff711aa52da2ca3ace8e660515991f71c3f76b5eee3598679f9282214013aa +- filename: src/main/java/io/mosip/mimoto/exception/BaseCheckedException.java + checksum: d6c31dadb603a3daa320c41a63cf94b983662749664c593cfac330234333a50f +- filename: src/main/java/io/mosip/mimoto/dto/ErrorDTO.java + checksum: f07ea78ad10b25b8c7ebb78be365cf87b03d7ee2681265de9617f1aa54a912e9 +- filename: src/main/java/io/mosip/mimoto/exception/BiometricException.java + checksum: e99050f4618224a32d378df776a177a1e0cf87089dfcc2ea542cb05c74c7d88b +- filename: src/test/java/io/mosip/mimoto/util/JoseUtilTest.java + checksum: 5c407e9011599fb6ff95820fff6a3a4b781c7b95875937c629893d1a8cb08fa0 +- filename: src/test/java/io/mosip/mimoto/service/CredentialShareServiceTest.java + checksum: ba02215bc13737afa162de5be9d9d76701259cf9764b61e8ee0d27a7c94ecb3d +- filename: Credits.md + checksum: 785ea952bde3ffa1daac349e6bb0cfa1ae1c74fdc7ea33eb40eeca20b23f537f +- filename: .github/workflows/sonarqube.yml + checksum: 06fea629c2f32b36e385b65d9ba5437920b06bdd7c379e4e487a10d0964e9ce4 +- filename: Dockerfile + checksum: 8245c8124920165b6d9f0696a17d7ae1e945bef0b0d79e8a325bfe3c8afac3f4 +- filename: docs/images/id_pass_logo.svg + checksum: 81b14da4f42f335cf6a3094489844962b92fc629c89691487558c71c3d44a915 +- filename: src/main/java/io/mosip/mimoto/controller/CredentialShareController.java + checksum: 666c22ca63adb8770de901e220f8efeb5ec1d3f63064a58dea33ba49b8e15872 +- filename: src/main/java/io/mosip/mimoto/service/CredentialPDFGeneratorService.java + checksum: 9203516612d01a036d7d304ddd448974ecd1cdda3cac21145490315a09facdc0 +- filename: src/test/java/io/mosip/mimoto/service/CredentialPDFGeneratorServiceTest.java + checksum: bc6b9e27699e1ec3bb024f38509ff9d9bc90edb8888168259f218caa5e829059 +- filename: src/main/java/io/mosip/mimoto/controller/IssuersController.java + checksum: 856d860de2562fa019ac23bd724b84e1d67b834883b332c12415e6714de9bb05 +- filename: helm/mimoto/templates/deployment.yaml + checksum: 6eee9c084a39881370a82d249c5f0ae42c99f752db93b59507f4e83b68e7ac26 +- filename: src/main/java/io/mosip/mimoto/constant/DocumentGeneratorExceptionCodeConstant.java + checksum: 329b8341fed7a8166fd1ba66af3d74810e4549b00b1d13e1216d097374c1ce52 +- filename: src/main/java/io/mosip/mimoto/dto/mimoto/AttestationStatement.java + checksum: 50a8c075791999d3ffc6f5df0a06b790a69d922688c4c7498d8a6d415c8f046d +- filename: src/main/java/io/mosip/mimoto/exception/ExceptionUtils.java + checksum: 078d1e13d2c6787718f2bfd8320380e3aa4921c498cd05c9e445d78f08384c20 +- filename: src/main/java/io/mosip/mimoto/util/WebSubSubscriptionHelper.java + checksum: c25969b732f7dd0fd49262c68983d85611cb78800b8080d9f63f7ed9eeb0251b +- filename: src/main/java/io/mosip/mimoto/util/Utilities.java + checksum: 52ac8d1e6fcf3f662a757eef2774357165a618c29d8e1fa6e97395bad3c24065 +- filename: src/test/java/io/mosip/mimoto/config/MimotoConfig.java + checksum: ba189011b924fe58a804281c88a190b9b969d21bc485e03206471f69be1a1b5d +- filename: src/main/java/io/mosip/mimoto/util/JsonUtil.java + checksum: cc7a9e1be3ffd68e809dbf70e75f57f1420d117eba0ea7a720eca1a43fbe7f79 +- filename: src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java + checksum: eddc1f3d44e8b8676c5c4ff6ed94ba9018f3a562d4e17908234704493ec6b35e +- filename: src/main/java/io/mosip/mimoto/service/impl/CredentialShareServiceImpl.java + checksum: 0200972fc4feb033ab7949fe5f1d02c5ac03caa6fdc5b8559115fb884fc38276 +- filename: src/main/java/io/mosip/mimoto/util/RestApiClient.java + checksum: 77b8afdefbb1f8ff56e140836e6713d150ac48f99c6778e43bc2854e5fcd4732 +- filename: src/main/java/io/mosip/mimoto/util/CryptoUtil.java + checksum: fc4b7bd41ca63b2060854e218eb50689f77162dc51c45e95f51ec0272bd3585a +- filename: src/main/java/io/mosip/mimoto/util/CryptoCoreUtil.java + checksum: 0ecab2033342fbbc7828c3eae63247c8c33f4faea3001582338819669cd9b904 +- filename: docs/postman-collections/Collab.postman_environment.json + checksum: 93662b305117d9ac73e9f8ace2bd8dc8b2de494908a8c69166d0c908cc80365c +- filename: docs/postman-collections/MIMOTO.postman_collection.json + checksum: 282da368540e45dcd9c5b6fcda47cee32d50a69ceb99e6ef69eea8dae08b45f6 +- filename: src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java + checksum: c4e28436cb04004d9cb4e1492a1d013eac076936a3fa3a99d35bfb137f27c37b +- filename: .github/workflows/push-trigger.yml + checksum: db107eeba72ab765725de47b071bcdc65a2addf6c178004032a8859aeaa4ebe0 +- filename: partner-onboarder/install.sh + checksum: 68931ea29cd2ab8bda88514a78e422946073bd4640ef4d3afdc7d7580b2d1cb5 +- filename: helm/mimoto/install.sh + checksum: b979d5180f2862a847455988b46408609edede52ddca7b3c0e229692d8aaa945 +- filename: helm/mimoto/values.yaml + checksum: 894215a6578dd6f27e22df1554ee87bfc7ef0f579d03c805f30d976546dfee35 +- filename: src/main/java/io/mosip/mimoto/util/JoseUtil.java + checksum: b6123f66cb7dbe208cb8d5f8b4b664d742c918608ec0ad22dfa3ab27ab9ec32e +- filename: docker-compose/docker-compose.yml + checksum: f62a54ea4d2e060f5712b2b8acdb2611b77997e600b0eaeb8b0062618f44176b +- filename: src/test/java/io/mosip/mimoto/util/WalletValidatorTest.java + checksum: 90dce7daa19f0eb9050194a3c77a88d449a83c337d9041bfdeb7bfded3bbd12c +- filename: src/main/java/io/mosip/mimoto/exception/ErrorConstants.java + checksum: 3766d9f0726f6f527aeca768ac173a6ea56f7df979e16e04a5a20d5ebe98eaf6 +- filename: src/test/java/io/mosip/mimoto/util/TestUtilities.java + checksum: 90d5310d9499acf98fd3d14230f70e8c378339ed46f384f02d170811a9b05dd5 +- filename: src/main/java/io/mosip/mimoto/controller/WalletCredentialsController.java + checksum: c378e601f657a82ef59da49c01e3b9d39619a2aec143c71b8a269ab1aeb815a1 +- filename: src/main/java/io/mosip/mimoto/service/WalletUnlockService.java + checksum: 4a67a6393ad6e70bb7c999c7e5dbf9187e765aaf2ff64f465d4344f58c05a00c +- filename: src/test/java/io/mosip/mimoto/service/WalletUnlockServiceTest.java + checksum: 55069981352accc5b709445ff1539ab1591bc41a2780f6502789d3e51b655c3a +- filename: src/main/java/io/mosip/mimoto/service/impl/WalletServiceImpl.java + checksum: c367393de6a1b05d37f33f8afa57d9d757df3cb4162e00d79b7cd094ce5cb9b8 +- filename: src/test/java/io/mosip/mimoto/service/WalletServiceTest.java + checksum: cae528908baf67427de0487bd54f003e6b4339ea76a74dc94e49d874952923c6 +- filename: src/main/java/io/mosip/mimoto/controller/WalletsController.java + checksum: b83d21b1fe50c50ec18c040c989ecc952a7b47c415abf2ea7286a4e72d7b3ab1 +- filename: src/test/java/io/mosip/mimoto/controller/WalletsControllerTest.java + checksum: c0227f0642513e7b642c7e27a4732e005e3064bf9cc5f9c31f07800f23a4d03d +- filename: src/main/java/io/mosip/mimoto/service/WalletLockManager.java + checksum: c03b3ce388f26e45daaa08f65d589783840b1d143e8a2a89affe7481b27fa430 +- filename: src/test/java/io/mosip/mimoto/service/WalletLockManagerTest.java + checksum: faa7a26762a6e296db2a2e494ec9e7c8a46427ebc2975677b77bb137e2319356 +- filename: src/test/java/io/mosip/mimoto/service/WalletLockStatusServiceTest.java + checksum: 39bafbb16762cf8bd218c1f2af5ed1426db834a91e6df62578da350b6cea0f23 +- filename: src/main/java/io/mosip/mimoto/service/WalletStatusService.java + checksum: 3858fd0ef51e9c4fe372e88471a84eff8a68523297e0e20c52f498b127d03459 +- filename: src/main/java/io/mosip/mimoto/util/WalletUtil.java + checksum: 08c3c2efda65f3d8eef7b80dfa4ba6dc54040f7ece28a4a97dc9c1dc30bcd133 +- filename: docs/WalletUnlockProcess.md + checksum: d172beb5aeef1037d70d2f83f034e6ec5e4ea69c6150ec6a25c9139c799b7879 +- filename: docs/api-documentation-openapi.json + checksum: 0394757e38ceaf2d16763076615a29914d4d98037464f89a7f2d91e2d512ad57 +- filename: src/main/java/io/mosip/mimoto/util/JwtGeneratorUtil.java + checksum: c8533dd38f79879119ba2895afca75d60e9704d5a0df4a0e79bc3b9292bd0d38 +- filename: src/main/java/io/mosip/mimoto/service/impl/CredentialRequestServiceImpl.java + checksum: 389b2afec7b591e4566badd8612877e828dde5ecfa36c7363610ed54d6b45cd9 +- filename: src/test/java/io/mosip/mimoto/service/CredentialRequestServiceTest.java + checksum: 49411e4e7f19c4bcbe42647efc4969cdcbd79d2058cc1d449ab53723338d7b2b +- filename: src/main/java/io/mosip/mimoto/constant/SigningAlgorithm.java + checksum: fcb462b4baec6db1aa48c16fad923dc7a20407aa072efeb3879c236e7a4d74fe +- filename: api-test/src/main/resources/config/IDRepo.properties + checksum: dfdffc817362f6c6067a5c2ce5db9c6759faebaf754590deea307bbb71e81381 +- filename: src/test/resources/application-test.properties + checksum: eff6c72dfdc2e0a2aa4e3ec9d520a1433189902467954d123bc562df095a43bd +- filename: src/main/resources/application-default.properties + checksum: 05110c9a04d4b8013c15d4c9294ebe589f00d1917be3e9b9f40996974c6cba3a +- filename: src/main/resources/application-local.properties + checksum: ebb1d0c711bd6c2fec344a1a50786dbe46f06916419198546077a975e67d3933 +- filename: docker-compose/config/mimoto-default.properties + checksum: 7fefcbd53ef6f34696c96be682dbb0d2d5535cea337d3ccb60f85a9cb9805cd1 +- filename: README.md + checksum: a22d46c4cf0c09d3a2a3665400118c2b919158ddbf6c20773955b2c7c72bb91f +- filename: docker-compose/README.md + checksum: f70385d1be6b7945df1afac0890bdce5ecceb63cdba08bdd3f760f40f843c630 +- filename: src/main/java/io/mosip/mimoto/service/impl/VcSdJwtCredentialFormatHandler.java + checksum: f44f44a691f7bbc2e34e22d2d48791b883b79794011d514865de2ebecd58a655 +- filename: src/test/java/io/mosip/mimoto/service/VcSdJwtCredentialFormatHandlerTest.java + checksum: 3c0a06bac77cde10b2d7d4b0c43ce010fe00a31b9aa97af78af9e87e8df8401d +- filename: db_scripts/inji_mimoto/ddl.sql + checksum: 51cd2efbe5220b995ae7cff7d70797b2c1bde0ce3325b70d62a31a1f4f41aa47 +- filename: db_scripts/inji_mimoto/ddl/mimoto-trusted-verifiers.sql + checksum: bbb73e09a9669a8bf78a30b1a72ffdcdffee3f3eb92c5f39ffb5f6ae37e748d2 +- filename: db_scripts/inji_mimoto/ddl/mimoto-verifiable_presentations.sql + checksum: 61bba5988812b323c27f3a8fbdd4b802e09efe42772cbd4263a1bff814c74886 +- filename: src/main/java/io/mosip/mimoto/controller/OpenID4VPController.java + checksum: eef302db396aa1d46d128ef7422d5aeb00c9173af32f61effdf31a0762ddff83 +- filename: src/main/java/io/mosip/mimoto/service/impl/VerifierServiceImpl.java + checksum: 04d143cb4cb10b7dd4d66344c412681a4888983e232f85e4a0e56237e5248325 +- filename: src/main/java/io/mosip/mimoto/service/impl/SessionManager.java + checksum: d2f22ee3ea32d86f510834512e25dba53888aa9b30127bb747049ea933646b55 +- filename: src/test/java/io/mosip/mimoto/controller/WalletPresentationsControllerTest.java + checksum: 9e8d06af154c14fa40f16193aaee9c905fa5b03c99cb901a8a0b28b0b09121ba +- filename: src/main/java/io/mosip/mimoto/controller/WalletPresentationsController.java + checksum: 9e8d06af154c14fa40f16193aaee9c905fa5b03c99cb901a8a0b28b0b09121ba +- filename: db_upgrade_script/inji_mimoto/sql/0.14.x_to_0.15.x_upgrade.sql + checksum: 32d62be953744f5307f5807f434c1f98e040f9d85b2760d26605a65adbbd162f +- filename: src/test/java/io/mosip/mimoto/service/VerifierServiceTest.java + checksum: f2f17ca6522b1b8f6018df19a0bea324edae5103936b538d3d675a1c4821e77a +- filename: db_upgrade_script/inji_mimoto/sql/0.14.x_to_0.15.x_rollback.sql + checksum: 21fdd239be77e4c8fa23cffe3c9799254a98536d433d74dde95add38ea288a8c + checksum: c7b264e1f134f3cfd83561143fc46e83168e664438f783109405b3ec7c753126 +- filename: src/main/java/io/mosip/mimoto/util/ClientValidationUtils.java + checksum: ef0cb7c9f0295ad7b5f25f4a73c998f55b2ed847922527929860b75eead7103c +- filename: src/main/java/io/mosip/mimoto/service/PresentationService.java + checksum: 9053ee654c595c0dd6087ab7284e57da20746734a4a02dca2c04d5fe42f061e6 +- filename: src/main/java/io/mosip/mimoto/service/impl/PresentationServiceImpl.java + checksum: 9053ee654c595c0dd6087ab7284e57da20746734a4a02dca2c04d5fe42f061e6 +- filename: src/main/java/io/mosip/mimoto/service/CredentialMatchingService.java + checksum: b2ceada5c81b9dadb6e7780604c68b7ae2d4272d2f67442fab0f5a45bd33727e +- filename: src/main/java/io/mosip/mimoto/service/impl/CredentialMatchingServiceImpl.java + checksum: deb4e42b6723163a610fe21a20bf8ed53e2acf858fcc19fc9ae8d35f41394bb0 +- filename: src/test/java/io/mosip/mimoto/service/CredentialMatchingServiceTest.java + checksum: f0064149fe98f4dc82195f23956bb6ae206e8b6555c8f53adb61982f26a5285e +- filename: src/test/java/io/mosip/mimoto/service/SessionManagerTest.java + checksum: f541358ce3b674a8865592b8c899c2dac484730b8790f9a99b6a79a5b7953c9a +- filename: src/main/java/io/mosip/mimoto/constant/OpenID4VPConstants.java + checksum: 81ca55276277c00a7121ecc70444202bd30a8a7f0c88b40799ab1106f5bbf0e5 +- filename: src/main/java/io/mosip/mimoto/service/KeyPairService.java + checksum: e2c4dbcdcf131b3026ba47cf8f7dddbd2cd71259888dad9160afd86b9aa7ea6d +- filename: src/main/java/io/mosip/mimoto/service/PresentationSubmissionService.java + checksum: f4cc9853d567931ae629ccf84568f148469e2853e477ea366e79585b9330a04d +- filename: src/main/java/io/mosip/mimoto/service/impl/KeyPairServiceImpl.java + checksum: f55561ba31c6710904b53502fde71564406e0f5102b8234f07e8789d7a313230 +- filename: src/main/java/io/mosip/mimoto/service/impl/PresentationSubmissionServiceImpl.java + checksum: 62fe8f55d40078eb88ca4bbf0f6b453f4be47a37e048796f08d94e9cd0aed1fb +- filename: src/test/java/io/mosip/mimoto/util/Base64UtilTest.java + checksum: 700663d179554cec53962c6ed12957411c5960c32bc0b8d799f79c1b92191c33 +- filename: src/test/java/io/mosip/mimoto/service/PresentationSubmissionServiceTest.java + checksum: 42154b3c9b9b392ad530d6edc5bb58bfa46a8f55ff744e50ab8ab1880a325bee +- filename: src/test/java/io/mosip/mimoto/service/KeyPairServiceTest.java + checksum: 3619f4af5a2da5319f6ee3e6c8bdf0491347987ea61e70a09abb58c355b67a6d +- filename: src/main/java/io/mosip/mimoto/service/impl/PresentationActionServiceImpl.java + checksum: 13f3da2b60a6319b1f48d5a98a726f5cad571865762626a06b432061097dafe3 +- filename: src/test/java/io/mosip/mimoto/service/PresentationActionServiceTest.java + checksum: f79257d43d522d4076e78fbe802c2e29184e8ff3a3347016578ffca56015a696 +- filename: db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.20.0_rollback.sql + checksum: cbb93a2eadd7e1e1ee2ddf8d34466d20a835f9a63b598e5c9d1a1b425106a1a9 +- filename: db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.20.0_upgrade.sql + checksum: fabe688963b37c6b479b289f9e988107dc76358a1ea57b9aeee150c02bc4c9a2 +- filename: docker-compose/mimoto_init.sql + checksum: 14ac202ae6b04f89b61708ea81bbc59a1819ad164800d0d016c1f6051e68a0d6 +- filename: src/main/java/io/mosip/mimoto/constant/SwaggerLiteralConstants.java + checksum: 34db30e6de4ea8e9eb939b584293284a5aef702d681116185467103669c0604a +- filename: src/test/java/io/mosip/mimoto/service/WalletPresentationServiceTest.java + checksum: 00dc10312bfc944ea19edc1656bb1152d3fa30de1bb1152b693d1e6695d1f0fb +- filename: src/main/java/io/mosip/mimoto/service/WalletPresentationService.java + checksum: f533f57817732d29d33338ee67fed3af74a6c3492d2539d0b36152ec6822d9a3 +- filename: src/main/java/io/mosip/mimoto/service/impl/WalletPresentationServiceImpl.java + checksum: 00c76b27e181836f7d94cb52567575d4dd10ce1bb519624e9e42a86cb73a02e3 +- filename: deploy/README.md + checksum: 1bef019be00c0eae8714ce8a71a4e33c68aa5e2bfe4a6d7f72eaad35b8dd64a9 +- filename: src/main/java/io/mosip/mimoto/service/impl/KeyPairRetrievalServiceImpl.java + checksum: 5245e749ca703d7bb854242465b40d97cba96a6ff3b6b93e867fbece062d0eb0 +- filename: src/test/java/io/mosip/mimoto/util/EncryptionDecryptionUtilTest.java + checksum: a5d347002074c6d97bbe5a95f6516cb09116854e5fdfc9eb274e510edf30125a +- filename: src/main/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandler.java + checksum: b194879e2a8d12952a7efa85cb019293c319280f1a3c3bfcb7adf81358b2d784 +- filename: src/test/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandlerTest.java + checksum: 74b1127faa64075bc7061217d8c364199fe4fef699f92cddf9e25db9560e8624 +- filename: src/main/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandler.java + checksum: 97b9c30b2297a1eb23818e53a6b49488d086a8f91e8982752c74b3fd7001bcf4 +- filename: src/main/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandler.java + checksum: 604f5ae8f9c4598eac79141bdcf0ce63cb73a691a28c73d7d860206149f489fb +- filename: src/test/java/io/mosip/mimoto/util/WalletUtilTest.java + checksum: 9df128c2fe0e625489acb99dfc3ddcb16b5afd484569d7b28f3fa1f221edefc8 +- filename: src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandler.java + checksum: 88414c683276784f85339f36f57a6d5195c247a0ff21af238e8c20d9448bee9f +- filename: src/test/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandlerTest.java + checksum: 6cc906f471b7191030392e474c52bd8a79f0f550c88a5b4c0c50aef5ff09c75d +- filename: src/main/java/io/mosip/mimoto/util/factory/ED25519AlgorithmHandler.java + checksum: b13332cccf2ad113fba71436831d764133082ecdcc343143486b230405317888 +- filename: src/test/java/io/mosip/mimoto/util/factory/ED25519AlgorithmHandlerTest.java + checksum: 7ac8b874b2c1f3d440bdfdb3de0fc4613cf3424924363642664559ad57b8f02f +- filename: src/test/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandlerTest.java + checksum: bf9140761defe9061b885c1ade3d120b4835df160ddf2fea0f790acb4801f221 +- filename: src/main/java/io/mosip/mimoto/util/SigningKeyUtil.java + checksum: b5eec9b76f972f685cf22002360d81dd55ea8a798e2d8b9a6f4fb382fa10b021 +- filename: src/test/java/io/mosip/mimoto/util/SigningKeyUtilTest.java + checksum: eeebabe81b6e2a60eece03f4505c9bd9b4a9f7059e1873b71c63dca5722847f2 +- filename: src/main/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandler.java + checksum: 6d9332df2edfc765ab45c8e985571bb5851c0da680afdc19becbc1e60362df20 +- filename: src/main/java/io/mosip/mimoto/controller/IssuersV2Controller.java + checksum: 132c81d2d4552789777632e7ff61c86ceb843c10a7177a5bcbd2eb1c196cb430 +- filename: src/main/java/io/mosip/mimoto/util/parser/Draft13WellknownParser.java + checksum: 15a9710874a5692a85182e8b30962122dd6cfff570a6e706a49938eadae267c4 +- filename: src/main/java/io/mosip/mimoto/util/parser/V1WellknownParser.java + checksum: efd151e6a65bc82de38b923fc495b4383f03e6b1641496ed7cd282ed0d2389ee +- filename: src/main/java/io/mosip/mimoto/service/impl/V1VCDownloadHandler.java + checksum: 621c5c9b0819d81ab69dae829862d6e740b9ff4da2567d9ea1ba6df3f8600dac +- filename: src/main/java/io/mosip/mimoto/service/V1CredentialRequestService.java + checksum: 7296598632d713db09024635937c8ac2b7f4a81bcc66ab236917e2a64c1989bf +- filename: src/test/java/io/mosip/mimoto/service/V1CredentialRequestServiceTest.java + checksum: 260b355b96c6618f7df5d80edca2c1398c1f3b9e54f6368fe3b850d4bd5829b4 +- filename: src/test/java/io/mosip/mimoto/service/V1VCDownloadHandlerTest.java + checksum: cbab428bf3c7cce688810eadd32025237c387110a1b4951498943f3c427fde27 +- filename: src/test/java/io/mosip/mimoto/util/parser/V1WellknownParserTest.java + checksum: 2aa44b42cb47e29a1f5e5ae4d7816ddc1cba8e593198fe92a7c6f0ae6ba881d9 +version: "" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 79e12716b..df50a2743 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,13 @@ -FROM openjdk:11 +FROM eclipse-temurin:21-jre + +ARG SOURCE +ARG COMMIT_HASH +ARG COMMIT_ID +ARG BUILD_TIME +LABEL source=${SOURCE} +LABEL commit_hash=${COMMIT_HASH} +LABEL commit_id=${COMMIT_ID} +LABEL build_time=${BUILD_TIME} # can be passed during Docker build as build time environment for github branch to pickup configuration from. ARG spring_config_label @@ -40,27 +49,24 @@ ARG container_user=mosip ARG container_user_group=mosip # can be passed during Docker build as build time environment for github branch to pickup configuration from. -ARG container_user_uid=1001 +ARG container_user_uid=1002 # can be passed during Docker build as build time environment for github branch to pickup configuration from. ARG container_user_gid=1001 # install packages and create user -RUN apt-get -y update \ -&& apt-get install -y unzip \ -&& groupadd -g ${container_user_gid} ${container_user_group} \ -&& useradd -u ${container_user_uid} -g ${container_user_group} -s /bin/sh -m ${container_user} - +RUN apt-get update -qq && \ + apt-get install -y -qq unzip wget adduser && \ + groupadd -g ${container_user_gid} ${container_user_group} && \ + useradd -u ${container_user_uid} -g ${container_user_group} -d /home/${container_user} -m -s /bin/sh ${container_user} && \ + apt-get clean && rm -rf /var/lib/apt/lists/* # set working directory for the user WORKDIR /home/${container_user} ENV work_dir=/home/${container_user} -ARG loader_path=${work_dir}/additional_jars/ - -RUN mkdir -p ${loader_path} ${work_dir}/data - -ENV loader_path_env=${loader_path} +# create directory - data under working directory to store JSON files in OTP flow credential download +RUN mkdir -p ${work_dir}/data # change volume to whichever storage directory you want to use for this container. VOLUME ${work_dir}/logs ${work_dir}/Glowroot @@ -81,11 +87,9 @@ CMD if [ "$is_glowroot_env" = "present" ]; then \ unzip glowroot.zip ; \ rm -rf glowroot.zip ; \ sed -i 's//mimoto/g' glowroot/glowroot.properties ; \ - wget -q --show-progress "${iam_adapter_url_env}" -O "${loader_path_env}"/kernel-auth-adapter.jar; \ - java -jar -javaagent:glowroot/glowroot.jar -Dloader.path="${loader_path_env}" -Dspring.cloud.config.label="${spring_config_label_env}" -Dspring.profiles.active="${active_profile_env}" -Dspring.cloud.config.uri="${spring_config_url_env}" mimoto.jar ; \ + java -jar -javaagent:glowroot/glowroot.jar -Dspring.cloud.config.label="${spring_config_label_env}" -Dspring.profiles.active="${active_profile_env}" -Dspring.cloud.config.uri="${spring_config_url_env}" mimoto.jar ; \ else \ - wget -q --show-progress "${iam_adapter_url_env}" -O "${loader_path_env}"/kernel-auth-adapter.jar; \ - java -jar -Dloader.path="${loader_path_env}" -Dspring.cloud.config.label="${spring_config_label_env}" -Dspring.profiles.active="${active_profile_env}" -Dspring.cloud.config.uri="${spring_config_url_env}" mimoto.jar ; \ + java -jar -Dspring.cloud.config.label="${spring_config_label_env}" -Dspring.profiles.active="${active_profile_env}" -Dspring.cloud.config.uri="${spring_config_url_env}" mimoto.jar ; \ fi #CMD ["java","-Dspring.cloud.config.label=${spring_config_label_env}","-Dspring.profiles.active=${active_profile_env}","-Dspring.cloud.config.uri=${spring_config_url_env}","-jar","-javaagent:/home/Glowroot/glowroot.jar","mimoto.jar"] diff --git a/LICENSE b/LICENSE index a612ad981..fa0086a95 100644 --- a/LICENSE +++ b/LICENSE @@ -370,4 +370,4 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/README.md b/README.md index 5bc96dda4..a8f70d45b 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,185 @@ +[![Maven Package upon a push](https://github.com/mosip/mimoto/actions/workflows/push-trigger.yml/badge.svg?branch=master)](https://github.com/mosip/mimoto/actions/workflows/push-trigger.yml) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mosip_mimoto&id=mosip_mimoto&metric=alert_status)](https://sonarcloud.io/project/overview?id=mosip_mimoto) + # mimoto -This is the mobile server backend supporting Inji. - -## Requirements -- Java SE 11 -- Maven - -## Build -```shell -mvn clean package -``` - -## Run -```shell -mvn spring-boot:run -Dspring.profiles.active=local -``` - -## Deploy - -### Docker compose stack folder structure: -- bin: service jar files -- conf: configuration files for docker services. -- data: using to store shared VC event and generated files. - -Follow the build section to compile and get the mimoto-*.jar file in the target folder. - -Copy jar files to bin folder. Websub (hub.jar) and safety net is optional, depends on the client app implementation. - -### Configuration - -conf/nginx/conf.d/mimoto.conf -```nginx -upstream mimoto-service { - server mimoto-service:8088; -} - -server { - listen 80; - listen [::]:80; - - access_log /var/log/nginx/mimoto-service.access.log main; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - location /v1/mimoto { - proxy_pass http://mimoto-service/v1/mimoto; - proxy_set_header Host $http_host; # required for docker client's sake - proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -docker-compose.yml -```yaml -version: "3.9" -services: - nginx: - image: nginx:alpine - volumes: - - ./conf/nginx/proxy_params:/etc/nginx/proxy_params - - ./conf/nginx/conf.d:/etc/nginx/conf.d - hostname: nginx - restart: always - ports: - - 80:80 - links: - - mimoto-service - - websub - - safetynet - depends_on: - - mimoto-service - - mimoto-service: - image: openjdk:11-jre-slim - volumes: - - ./bin:/opt/mimoto-service - - ./data:/data - hostname: mimoto-service - restart: always - command: > - bash -c "cd /data && java -jar \ - -Dpublic.url=https://YOUR_PUBLIC_URL_FOR_MIMOTO_SERVICE \ - -Dcredential.data.path=/data \ - -Dmosip.event.delay-millisecs=5000 \ - -Dwebsub-resubscription-delay-millisecs=300000 \ - -Dsafetynet.api.key=GOOGLE_API_KEY \ - /opt/mimoto-service/mimoto-*.jar" - expose: - - 8088 -``` - -Note: -- Replace public.url with your public accessible domain. For dev or local env ngrok is recommended. -- Google safetynet API key can be generate from Google Cloud Console. - -### Run with docker-compose -```shell -docker-compose up -d -``` - -## Update - -Copy and replace the new version of the jar files in the bin folder and restart the docker service. - -```shell -docker-compose restart mimoto-service -``` +## Overview +This repository contains source code for backend service of Inji Mobile and Inji Web. The modules exposes API endpoints. + + +## Build & run (for developers) +The project requires JDK 21, postgres and google client credentials + +### without docker-compose Build & install +1. Install pgadmin and update application-default.properties file with values + ```properties + spring.datasource.username= + spring.datasource.password= + ``` + +2. **Configuring Cache Providers:** + + By default, Mimoto uses Caffeine, a fast in-memory cache. This works well if you're running just one instance of Mimoto. + + But if you're running multiple Mimoto instances (like in Docker Swarm, Kubernetes, or a load-balanced setup), each instance has its own separate cache with Caffeine — they don’t talk to each other. + + In that case, switching to a shared cache like Redis is important. Redis lets all Mimoto instances share the same cached data, which helps keep things consistent and improves performance in distributed setups. + + For detailed setup instructions (including running Redis with Docker CLI and updating configuration), see the [Cache Providers Setup Guide](#cache-providers-setup-guide) section. + + +3. **Configuring Postgres Database:** + 1. **Ensure Postgres Service is Available and Connected** + - **Using Local Postgres Installation:** If you have Postgres installed locally, run the database initialization scripts: + ```bash + cd db_scripts/inji_mimoto + ./deploy.sh deploy.properties + ``` + * If needed, update the database connection properties in [application-local.properties](src/main/resources/application-local.properties) with your local Postgres credentials. + + - **Using Docker Compose:** If you don't have postgres setup in your local you can run Postgres along with Mimoto by using the `postgres` service in [docker-compose.yml](docker-compose/docker-compose.yml). + + - **Or, run Postgres using Docker while starting Mimoto through your IDE:** + * Use the following Docker command to start the Postgres service and expose it on the default port 5432. Make sure this port is accessible from your local machine. + ```bash + docker pull postgres:latest # Pull the Postgres image if not already available + docker run -d --name postgres \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=inji_mimoto \ + -p 5432:5432 \ + postgres:latest + ``` + * Start Mimoto normally, following the instructions mentioned in [Build & run (for developers) section](#build--run-for-developers). + 2. **Update the following properties** in + - [application-local.properties](src/main/resources/application-local.properties) *(when running through IDE)*, or + - [mimoto-default.properties](docker-compose/config/mimoto-default.properties) *(when running through Docker)*: + + **Look for properties starting with:** + - `spring.datasource.*` + - `spring.datasource.*` + - `mosip.mimoto.database.*` + 3. **Check data in postgres by using the following commands** + ```bash + docker exec -it postgres psql -U postgres -d inji_mimoto # connect to container + + \dt mimoto.* # to see all tables in mimoto schema + + SELECT * FROM mimoto.; # to see data in a table + ``` + +4. Refer to the [How to create Google Client Credentials](docker-compose/README.md#how-to-create-google-client-credentials) section to create + Google client credentials and update below properties in `application-local.properties`. + ``` + spring.security.oauth2.client.registration.google.client-id= + spring.security.oauth2.client.registration.google.client-secret= + ``` +5. Add identity providers as issuers in the `mimoto-issuers-config.json` file of [resources folder](src/main/resources/mimoto-issuers-config.json). For each provider, create a corresponding object with its issuer-specific configuration. Refer to the [Issuers Configuration](docker-compose/README.md#mimoto-issuers-configuration) section for details on how to structure this file and understand each field's purpose and what values need to be updated. + +6. Add or update the verifiers clientId, redirect and response Uris in `mimoto-trusted-verifiers.json` file of [resources folder](src/main/resources/mimoto-trusted-verifiers.json) for Verifiable credential Online Sharing. + +7. Keystore(oidckeystore.p12) Configuration: + In the root directory, create a certs folder and generate an OIDC client. Add the onboard client’s key to the oidckeystore.p12 file and place this file inside the certs folder. + Refer to the [official documentation](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/customization-overview/credential_providers) for guidance on how to create the **oidckeystore.p12** file and add the OIDC client key to it. + * The **oidckeystore.p12** file stores keys and certificates, each identified by an alias (e.g., mpartner-default-mimoto-insurance-oidc). Mimoto uses this alias to find the correct entry and access the corresponding private key during the authentication flow. + * Update the **client_alias** field in the [mimoto-issuers-config.json](src/main/resources/mimoto-issuers-config.json) file with this alias so that Mimoto can load the correct key from the keystore. + * Also, update the **client_id** field in the [same file](src/main/resources//mimoto-issuers-config.json) with the client_id used during the onboarding process. + * Set the `oidc_p12_password` environment variable in the Mimoto service configuration inside docker-compose.yml to match the password used for the **oidckeystore.p12** file. + * Update the following properties in applicatio-local.properties file + ```properties + mosip.oidc.p12.password= + mosip.kernel.keymanager.hsm.config-path= + mosip.kernel.keymanager.hsm.keystore-pass= + ``` + * Mimoto also uses this same keystore file (oidckeystore.p12) to store keys generated at service startup, which are essential for performing encryption and decryption operations through the KeyManager service. + +8. To configure any Mobile Wallet specific configurations refer to the [Inji Mobile Wallet Configuration](docker-compose/README.md#inji-mobile-wallet-configuration) section. + +9. Run the SQLs using /deploy.sh script. from [db_scripts folder](db_scripts/inji_mimoto) + ``` + ./deploy.sh deploy.properties + ``` + +10. Build the jar + ``` + mvn clean install -Dgpg.skip=true -Dmaven.javadoc.skip=true -DskipTests=true + ``` + +11. Run following command + ``` + mvn spring-boot:run -Dspring.profiles.active=local + ``` + +## Cache Providers Setup Guide + +To use Redis (or any other cache provider), the service must be **running** and **accessible to Mimoto**. Both services (cache provider and Mimoto) must be on the same Docker network. +This can be done by adding them to a shared network in your `docker-compose.yml` file, or by using the following commands if they are running separately. + +**Example: Using Redis as Cache Provider** + +1. **Ensure Redis Service is Available and Connected:** + + - **Using Docker Compose:** You can run Redis alongside Mimoto by adding the below lines in docker-compose.yml. Docker Compose ensures both services run on the same network automatically. + 1. Add the Redis service under the services section: + ```yaml + redis: + image: redis:alpine + container_name: 'redis' + ports: + - "6379:6379" + volumes: + - redis-data:/data + ``` + 2. Make Redis a dependency for the Mimoto service: + ```yaml + mimoto-service: + depends_on: + - redis + ``` + 3. Add Redis data volume in the volumes section: + ```yaml + volumes: + redis-data: + ``` + + - **Or, run Redis using Docker while starting Mimoto through your IDE:** + - Use the following Docker command to start the Redis service and expose it on the default port 6379. Make sure this port is accessible from your local machine. + ```bash + docker pull redis:alpine # Pull the Redis image if not already available + docker run -d --name redis -p 6379:6379 redis:alpine # Start a Redis container named 'redis' and expose it on port 6379 + ``` + - Start Mimoto normally, following the instructions mentioned in [Build & run (for developers) section](#build--run-for-developers). + +2. **Update the following properties** in + - [application-local.properties](src/main/resources/application-local.properties) *(when running through IDE)*, or + - [mimoto-default.properties](docker-compose/config/mimoto-default.properties) *(when running through Docker)*: + ```properties + spring.session.store-type=redis # Store HTTP sessions in Redis + spring.cache.type=redis # Store application data in Redis + ``` + +3. **Add and update the required Redis configurations** in + - [application-local.properties](src/main/resources/application-local.properties) or + - [mimoto-default.properties](docker-compose/config/mimoto-default.properties), similar to those in the [application-default.properties](src/main/resources/application-default.properties) file. + Look for properties starting with: + - `spring.data.redis.*` + - `spring.session.redis.*` + +4. **Check the cached data of the redis by running the following command:** + ```bash + docker exec -it redis redis-cli + ``` + +### with docker-compose +1. To simplify running mimoto in local for developers we have added [Docker Compose Setup](docker-compose/README.md). This docker-compose includes mimoto service and nginx service to server static data. +2. Follow the below steps to use custom build image in docker-compose +* Build the mimoto.jar + ```mvn clean install -Dgpg.skip=true -Dmaven.javadoc.skip=true -DskipTests=true``` +* Build docker image by running the below command in the directory where Dockerfile is present, use any image tag + ```docker build -t .``` +* Use newly built docker image in docker-compose file + +## [Deployment in K8 cluster](deploy/README.md) + ## Credits -Credits listed [here](/Credits.md) +Credits listed [here](/Credits.md) \ No newline at end of file diff --git a/api-test/.gitignore b/api-test/.gitignore new file mode 100644 index 000000000..defd05020 --- /dev/null +++ b/api-test/.gitignore @@ -0,0 +1,40 @@ +*.class +.mtj.tmp/ +*.war +*.ear +hs_err_pid* +#ignored files + +.springBeans +.metadata +.factorypath +.classpath +.project +.settings/ +bin/ +tmp/ + +logs/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.DS_Store +test.txt +.idea/ +.settings/ +.sonarlint/ +.recommenders/ +/.recommenders/ + +**/*.iml +*.log +src/logs/mosip-api-test.log +/target/ +target/ +test-output/ +testng-report/ +/reg +./reg diff --git a/api-test/Biometric Devices/Face/DeviceDiscovery.json b/api-test/Biometric Devices/Face/DeviceDiscovery.json new file mode 100644 index 000000000..b5ca41591 --- /dev/null +++ b/api-test/Biometric Devices/Face/DeviceDiscovery.json @@ -0,0 +1,17 @@ +{ + "deviceId":"3", + "deviceStatus":"Ready", + "certification":"L0", + "serviceVersion":"0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId":"", + "deviceCode": "b692b595-3523-face-99fc-bd76e35f190f", + "specVersion":[ + "0.9.5" + ], + "purpose":"", + "error":null +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Face/DeviceInfo.json b/api-test/Biometric Devices/Face/DeviceInfo.json new file mode 100644 index 000000000..e8e4d59ce --- /dev/null +++ b/api-test/Biometric Devices/Face/DeviceInfo.json @@ -0,0 +1,18 @@ +{ + "deviceStatus": "Ready", + "deviceId": "3", + "firmware": "MOSIP.FACE.1.0.0.0", + "certification": "L0", + "serviceVersion": "0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId": "", + "deviceCode": "b692b595-3523-face-99fc-bd76e35f190f", + "env": "Staging", + "purpose": "", + "specVersion": [ + "0.9.5" + ] +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Face/DigitalId.json b/api-test/Biometric Devices/Face/DigitalId.json new file mode 100644 index 000000000..a041adf73 --- /dev/null +++ b/api-test/Biometric Devices/Face/DigitalId.json @@ -0,0 +1,10 @@ +{ + "serialNo":"2345678901", + "make":"MOSIP", + "model":"FACE01", + "type":"Face", + "deviceSubType":"Full face", + "deviceProvider":"MOSIP", + "deviceProviderId":"MOSIP.PROXY.SBI", + "dateTime":"" +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Face/Keys/mosipface.p12 b/api-test/Biometric Devices/Face/Keys/mosipface.p12 new file mode 100644 index 000000000..94b2589ad Binary files /dev/null and b/api-test/Biometric Devices/Face/Keys/mosipface.p12 differ diff --git a/api-test/Biometric Devices/Face/Keys/mosipfaceftm.p12 b/api-test/Biometric Devices/Face/Keys/mosipfaceftm.p12 new file mode 100644 index 000000000..3a5cd3d00 Binary files /dev/null and b/api-test/Biometric Devices/Face/Keys/mosipfaceftm.p12 differ diff --git a/api-test/Biometric Devices/Face/Stream Image/0.jpeg b/api-test/Biometric Devices/Face/Stream Image/0.jpeg new file mode 100644 index 000000000..1fa782a7d Binary files /dev/null and b/api-test/Biometric Devices/Face/Stream Image/0.jpeg differ diff --git a/api-test/Biometric Devices/Finger/Single/DeviceDiscovery.json b/api-test/Biometric Devices/Finger/Single/DeviceDiscovery.json new file mode 100644 index 000000000..142a5aaed --- /dev/null +++ b/api-test/Biometric Devices/Finger/Single/DeviceDiscovery.json @@ -0,0 +1,17 @@ +{ + "deviceId":"4", + "deviceStatus":"Ready", + "certification":"L0", + "serviceVersion":"0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId":"", + "deviceCode": "b692b595-3523-slap-99fc-bd76e35f290f", + "specVersion":[ + "0.9.5" + ], + "purpose":"", + "error":null +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Single/DeviceInfo.json b/api-test/Biometric Devices/Finger/Single/DeviceInfo.json new file mode 100644 index 000000000..707064134 --- /dev/null +++ b/api-test/Biometric Devices/Finger/Single/DeviceInfo.json @@ -0,0 +1,18 @@ +{ + "deviceStatus": "Ready", + "deviceId": "4", + "firmware": "MOSIP.SINGLE.1.0.0.0", + "certification": "L0", + "serviceVersion": "0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId": "", + "deviceCode": "b692b595-3523-slap-99fc-bd76e35f290f", + "env": "Staging", + "purpose": "", + "specVersion": [ + "0.9.5" + ] +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Single/DigitalId.json b/api-test/Biometric Devices/Finger/Single/DigitalId.json new file mode 100644 index 000000000..477301245 --- /dev/null +++ b/api-test/Biometric Devices/Finger/Single/DigitalId.json @@ -0,0 +1,10 @@ +{ + "serialNo":"1234567990", + "make":"MOSIP", + "model":"SINGLE01", + "type":"Finger", + "deviceSubType":"Single", + "deviceProvider":"MOSIP", + "deviceProviderId":"MOSIP.PROXY.SBI", + "dateTime":"" +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingle.p12 b/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingle.p12 new file mode 100644 index 000000000..b1af929c6 Binary files /dev/null and b/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingle.p12 differ diff --git a/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingleftm.p12 b/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingleftm.p12 new file mode 100644 index 000000000..e92a0a26a Binary files /dev/null and b/api-test/Biometric Devices/Finger/Single/Keys/mosipfingersingleftm.p12 differ diff --git a/api-test/Biometric Devices/Finger/Slap/DeviceDiscovery.json b/api-test/Biometric Devices/Finger/Slap/DeviceDiscovery.json new file mode 100644 index 000000000..37664dc9d --- /dev/null +++ b/api-test/Biometric Devices/Finger/Slap/DeviceDiscovery.json @@ -0,0 +1,17 @@ +{ + "deviceId":"2", + "deviceStatus":"Ready", + "certification":"L0", + "serviceVersion":"0.9.5", + "deviceSubId":[ + "1", "2", "3" + ], + "callbackId":"", + "digitalId":"", + "deviceCode": "b692b595-3523-slap-99fc-bd76e35f190f", + "specVersion":[ + "0.9.5" + ], + "purpose":"", + "error":null +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Slap/DeviceInfo.json b/api-test/Biometric Devices/Finger/Slap/DeviceInfo.json new file mode 100644 index 000000000..7d9e3eb3a --- /dev/null +++ b/api-test/Biometric Devices/Finger/Slap/DeviceInfo.json @@ -0,0 +1,18 @@ +{ + "deviceStatus": "Ready", + "deviceId": "2", + "firmware": "MOSIP.SLAP.1.0.0.0", + "certification": "L0", + "serviceVersion": "0.9.5", + "deviceSubId":[ + "1", "2", "3" + ], + "callbackId":"", + "digitalId": "", + "deviceCode": "b692b595-3523-slap-99fc-bd76e35f190f", + "env": "Staging", + "purpose": "", + "specVersion": [ + "0.9.5" + ] +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Slap/DigitalId.json b/api-test/Biometric Devices/Finger/Slap/DigitalId.json new file mode 100644 index 000000000..759794b80 --- /dev/null +++ b/api-test/Biometric Devices/Finger/Slap/DigitalId.json @@ -0,0 +1,10 @@ +{ + "serialNo":"1234567890", + "make":"MOSIP", + "model":"SLAP01", + "type":"Finger", + "deviceSubType":"Slap", + "deviceProvider":"MOSIP", + "deviceProviderId":"MOSIP.PROXY.SBI", + "dateTime":"" +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslap.p12 b/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslap.p12 new file mode 100644 index 000000000..59e2d477b Binary files /dev/null and b/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslap.p12 differ diff --git a/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslapftm.p12 b/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslapftm.p12 new file mode 100644 index 000000000..0885eb8f4 Binary files /dev/null and b/api-test/Biometric Devices/Finger/Slap/Keys/mosipfingerslapftm.p12 differ diff --git a/api-test/Biometric Devices/Finger/Slap/Stream Image/1.jpeg b/api-test/Biometric Devices/Finger/Slap/Stream Image/1.jpeg new file mode 100644 index 000000000..b9d93bc29 Binary files /dev/null and b/api-test/Biometric Devices/Finger/Slap/Stream Image/1.jpeg differ diff --git a/api-test/Biometric Devices/Finger/Slap/Stream Image/2.jpeg b/api-test/Biometric Devices/Finger/Slap/Stream Image/2.jpeg new file mode 100644 index 000000000..faf47323f Binary files /dev/null and b/api-test/Biometric Devices/Finger/Slap/Stream Image/2.jpeg differ diff --git a/api-test/Biometric Devices/Finger/Slap/Stream Image/3.jpeg b/api-test/Biometric Devices/Finger/Slap/Stream Image/3.jpeg new file mode 100644 index 000000000..e2d0fb6c9 Binary files /dev/null and b/api-test/Biometric Devices/Finger/Slap/Stream Image/3.jpeg differ diff --git a/api-test/Biometric Devices/Iris/Double/DeviceDiscovery.json b/api-test/Biometric Devices/Iris/Double/DeviceDiscovery.json new file mode 100644 index 000000000..3b947d241 --- /dev/null +++ b/api-test/Biometric Devices/Iris/Double/DeviceDiscovery.json @@ -0,0 +1,17 @@ +{ + "deviceId":"1", + "deviceStatus":"Ready", + "certification":"L0", + "serviceVersion":"0.9.5", + "deviceSubId":[ + "1", "2", "3" + ], + "callbackId":"", + "digitalId":"", + "deviceCode": "b692b595-3523-iris-99fc-bd76e35f190f", + "specVersion":[ + "0.9.5" + ], + "purpose":"", + "error":null +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Double/DeviceInfo.json b/api-test/Biometric Devices/Iris/Double/DeviceInfo.json new file mode 100644 index 000000000..e7b08d1ef --- /dev/null +++ b/api-test/Biometric Devices/Iris/Double/DeviceInfo.json @@ -0,0 +1,18 @@ +{ + "deviceStatus": "Ready", + "deviceId": "1", + "firmware": "MOSIP.IRIS.1.0.0.0", + "certification": "L0", + "serviceVersion": "0.9.5", + "deviceSubId":[ + "1", "2", "3" + ], + "callbackId":"", + "digitalId": "", + "deviceCode": "b692b595-3523-iris-99fc-bd76e35f190f", + "env": "Staging", + "purpose": "", + "specVersion": [ + "0.9.5" + ] +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Double/DigitalId.json b/api-test/Biometric Devices/Iris/Double/DigitalId.json new file mode 100644 index 000000000..eb100965e --- /dev/null +++ b/api-test/Biometric Devices/Iris/Double/DigitalId.json @@ -0,0 +1,10 @@ +{ + "serialNo":"3456789012", + "make":"MOSIP", + "model":"IRIS01", + "type":"Iris", + "deviceSubType":"Double", + "deviceProvider":"MOSIP", + "deviceProviderId":"MOSIP.PROXY.SBI", + "dateTime":"" +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdouble.p12 b/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdouble.p12 new file mode 100644 index 000000000..6d0874494 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdouble.p12 differ diff --git a/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdoubleftm.p12 b/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdoubleftm.p12 new file mode 100644 index 000000000..3203fb8ef Binary files /dev/null and b/api-test/Biometric Devices/Iris/Double/Keys/mosipirisdoubleftm.p12 differ diff --git a/api-test/Biometric Devices/Iris/Double/Stream Image/1.jpeg b/api-test/Biometric Devices/Iris/Double/Stream Image/1.jpeg new file mode 100644 index 000000000..b4d108386 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Double/Stream Image/1.jpeg differ diff --git a/api-test/Biometric Devices/Iris/Double/Stream Image/2.jpeg b/api-test/Biometric Devices/Iris/Double/Stream Image/2.jpeg new file mode 100644 index 000000000..485712ef4 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Double/Stream Image/2.jpeg differ diff --git a/api-test/Biometric Devices/Iris/Double/Stream Image/3.jpeg b/api-test/Biometric Devices/Iris/Double/Stream Image/3.jpeg new file mode 100644 index 000000000..8691a9460 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Double/Stream Image/3.jpeg differ diff --git a/api-test/Biometric Devices/Iris/Single/DeviceDiscovery.json b/api-test/Biometric Devices/Iris/Single/DeviceDiscovery.json new file mode 100644 index 000000000..93510c8c4 --- /dev/null +++ b/api-test/Biometric Devices/Iris/Single/DeviceDiscovery.json @@ -0,0 +1,17 @@ +{ + "deviceId":"5", + "deviceStatus":"Ready", + "certification":"L0", + "serviceVersion":"0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId":"", + "deviceCode": "b692b595-3523-iris-99fc-bd76e35fff8", + "specVersion":[ + "0.9.5" + ], + "purpose":"", + "error":null +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Single/DeviceInfo.json b/api-test/Biometric Devices/Iris/Single/DeviceInfo.json new file mode 100644 index 000000000..0fecb5d66 --- /dev/null +++ b/api-test/Biometric Devices/Iris/Single/DeviceInfo.json @@ -0,0 +1,18 @@ +{ + "deviceStatus": "Ready", + "deviceId": "5", + "firmware": "MOSIP.IRIS.2.0.0.0", + "certification": "L0", + "serviceVersion": "0.9.5", + "deviceSubId":[ + "0" + ], + "callbackId":"", + "digitalId": "", + "deviceCode": "b692b595-3523-iris-99fc-bd76e35fff8", + "env": "Staging", + "purpose": "", + "specVersion": [ + "0.9.5" + ] +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Single/DigitalId.json b/api-test/Biometric Devices/Iris/Single/DigitalId.json new file mode 100644 index 000000000..b2d91c8d8 --- /dev/null +++ b/api-test/Biometric Devices/Iris/Single/DigitalId.json @@ -0,0 +1,10 @@ +{ + "serialNo":"3456789099", + "make":"MOSIP", + "model":"IRIS02", + "type":"Iris", + "deviceSubType":"Single", + "deviceProvider":"MOSIP", + "deviceProviderId":"MOSIP.PROXY.SBI", + "dateTime":"" +} \ No newline at end of file diff --git a/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingle.p12 b/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingle.p12 new file mode 100644 index 000000000..c3903f918 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingle.p12 differ diff --git a/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingleftm.p12 b/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingleftm.p12 new file mode 100644 index 000000000..0801aa158 Binary files /dev/null and b/api-test/Biometric Devices/Iris/Single/Keys/mosipirissingleftm.p12 differ diff --git a/api-test/Biometric Devices/mosiprootca.p12 b/api-test/Biometric Devices/mosiprootca.p12 new file mode 100644 index 000000000..3f1db48fb Binary files /dev/null and b/api-test/Biometric Devices/mosiprootca.p12 differ diff --git a/api-test/Biometric Devices/mosiprootcaftm.p12 b/api-test/Biometric Devices/mosiprootcaftm.p12 new file mode 100644 index 000000000..ab4c5929a Binary files /dev/null and b/api-test/Biometric Devices/mosiprootcaftm.p12 differ diff --git a/api-test/Biometric Devices/rootca.cer b/api-test/Biometric Devices/rootca.cer new file mode 100644 index 000000000..dfaf87e47 --- /dev/null +++ b/api-test/Biometric Devices/rootca.cer @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPzCCAiegAwIBAgIEYCFEgjANBgkqhkiG9w0BAQsFADBSMQswCQYDVQQGEwJJ +TjESMBAGA1UECAwJS2FybmF0YWthMQ4wDAYDVQQKDAVNb3NpcDEOMAwGA1UECwwF +TW9zaXAxDzANBgNVBAMMBlJvb3RDQTAeFw0yMTAyMDgxNDAyNDJaFw0zMTAyMDgx +NDAyNDJaMFIxCzAJBgNVBAYTAklOMRIwEAYDVQQIDAlLYXJuYXRha2ExDjAMBgNV +BAoMBU1vc2lwMQ4wDAYDVQQLDAVNb3NpcDEPMA0GA1UEAwwGUm9vdENBMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz9kaCs4Yrh2mMFx/wGV42Tn7vM9d +t2au3JOWu/fbeaNk8/TgNEA1GrVerqagcRDDo+6jBDbX7XO9iRHX1wmIcVkH7osa +cMKGfYzZbvkWjGFRCam10xLGyczSeDiIIj+H3eLXzTP0iZBcVyw40EMLWk7BHld+ +eFY1XS+EDPIv6snjIxWDEd+wND1tOvWgPsS7mYy8Mn9QRYZXH9KW8/nW0bf/Z74U +d89PtXl3WSSrrRZSQofGDwM5RPOEzrYcurZowwWLWmgAmvcsATxhOAiKe6e6tvbD +yXTC8+9CRv22zGTcasI5QOYzGaj6wMKr9h2WgAY1LJFBIkEjf1ykWGlTbQIDAQAB +ox0wGzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOC +AQEAzWge0dKje1/opIyiho0e6p6a/M/UD05bNLg7XD6uMqJAT9C1YTwq77OD0nrc +ZpMQoQ4zCFEbGoPvHWzW0Isc7Y3F40YI8bQdmEQtJuiqsCUQ8XW3b4YDTH6VM5go ++VZgoRucIRBwCAss1tMrxjT3lMWwuyA9DaOkryRW/m9QmlLoTWLQabi/pO+FS+oy +WYYxJOzvqC3xUTYHi+DzlLSWR08NE7gdHrD4aAHYLPIyzgVuToztB7PYmbuOnKmS +M9sHlKqgpN7/AgvTOFOEX5mi5BcPGKagryTfipwLVyWaQBDCcVNkAtoLTYPCTEAj +UhONf7GpbeehitYjHQoknr21EA== +-----END CERTIFICATE----- diff --git a/api-test/Biometric Devices/rootcaftm.cer b/api-test/Biometric Devices/rootcaftm.cer new file mode 100644 index 000000000..ac57e8ba9 --- /dev/null +++ b/api-test/Biometric Devices/rootcaftm.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIEYMHynDANBgkqhkiG9w0BAQsFADBvMQswCQYDVQQGEwJJ +TjESMBAGA1UECAwJS2FybmF0YWthMRIwEAYDVQQHDAlCYW5nYWxvcmUxETAPBgNV +BAoMCG1vc2lwLmlvMREwDwYDVQQLDAhtb3NpcC5pbzESMBAGA1UEAwwJUm9vdENB +RlRNMB4XDTIxMDYxMDExMDgxMloXDTIyMDYxMDExMDgxMlowbzELMAkGA1UEBhMC +SU4xEjAQBgNVBAgMCUthcm5hdGFrYTESMBAGA1UEBwwJQmFuZ2Fsb3JlMREwDwYD +VQQKDAhtb3NpcC5pbzERMA8GA1UECwwIbW9zaXAuaW8xEjAQBgNVBAMMCVJvb3RD +QUZUTTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3FfxvirIiPH6YF +X4GR1z7LOI8tiwvfqjAFba6p4vMHs0xNu0TWJ9GS3qlDglmWczm3hgHndWD3rIUQ +V4fMsQDapA0giFVMxushm8u9JYl2HXwtVgrXUeKEdVS3zpWf2qQCXK9vQAOHvhhu +UMWVzL4yxd1RwUGjuSjM/pZqa/Tq0aDfqFVZ/MckxSvieEKEYODtcxXA708SQ07I +aFDZ2gKTD4aYUlCed9fAois+ko06tzqS1kerMiORzZExtUY/hwx2UrLVUBFYTBqW +Wuw0N2VjsaifQAti0JJGYPCBqM2Q7OctIvccAjghHYxCXj+yNRtOm8ooNZTefMJr +fCY9OucCAwEAAaMdMBswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAEQJMIT958jm1e5sCctUcOoHLQVdM9f3NLKicCUCjVMNCDjg +DbkoL72rlktTDbzi4AizaBXwxQdB951SG4Vjbc5tSC3WzB7d8QMiLRwG14Q165Gw +VGuMMOpJn4jlMzJMhhP67vJ4hYI2Op4eDAPH3HCods/Ew4JJ/edwpJ80tLKiB0f6 +fnyPXxPUSMtkLCCpp2/zf1OkXmUT8dIgU27R9+/8x/ZphDVJcc00XrgM4VI1dX+W +8W8Ua6GlQSL83HIqVudnFAmCDF0dRX//ghUOeJwWEMgcm/cUNTZv6hZjmDpVeout +E1o6hXGXWRnaDU9/QGpi2BuiSgREOarAQf62sPI= +-----END CERTIFICATE----- diff --git a/api-test/Dockerfile b/api-test/Dockerfile new file mode 100644 index 000000000..a4e397b6d --- /dev/null +++ b/api-test/Dockerfile @@ -0,0 +1,60 @@ +FROM mosipid/openjdk-21-jre:21.0.4 + +ARG SOURCE +ARG COMMIT_HASH +ARG COMMIT_ID +ARG BUILD_TIME +LABEL source=${SOURCE} +LABEL commit_hash=${COMMIT_HASH} +LABEL commit_id=${COMMIT_ID} +LABEL build_time=${BUILD_TIME} + +# can be passed during Docker build as build time environment for github branch to pickup configuration from. +ARG container_user=inji + +# can be passed during Docker build as build time environment for github branch to pickup configuration from. +ARG container_user_group=inji + +# can be passed during Docker build as build time environment for github branch to pickup configuration from. +ARG container_user_uid=1001 + +# can be passed during Docker build as build time environment for github branch to pickup configuration from. +ARG container_user_gid=1001 + +ARG KUBECTL_VERSION=1.22.9 + +# set working directory for the user +WORKDIR /home/${container_user} + +ENV work_dir=/home/${container_user} + +# Combine all necessary files into a single COPY command +COPY ./api-test/target $work_dir/ +COPY application.properties $work_dir/ +COPY ["Biometric Devices", "$work_dir/Biometric Devices"] +COPY ["resource", "$work_dir/resource"] +COPY entrypoint.sh $work_dir + +# install packages and create user +RUN apt-get -y update \ +&& apt-get install -y unzip jq curl \ +&& groupadd -g ${container_user_gid} ${container_user_group} \ +&& useradd -u ${container_user_uid} -g ${container_user_group} -s /bin/bash -m ${container_user} \ +&& curl -LO "https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \ +&& mkdir -p /home/${container_user} \ +&& chmod +x kubectl $work_dir/entrypoint.sh \ +&& mv kubectl /usr/local/bin/ \ +&& chown -R ${container_user}:${container_user} /home/${container_user} /etc/ssl/certs/java/cacerts \ +&& chmod 644 /etc/ssl/certs/java/cacerts + +# select container user for all tasks +USER ${container_user_uid}:${container_user_gid} + +EXPOSE 8083 + +ENV MODULES= +ENV ENV_USER= +ENV ENV_ENDPOINT= +ENV ENV_TESTLEVEL=smokeAndRegression + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/api-test/README.md b/api-test/README.md new file mode 100644 index 000000000..30845dc62 --- /dev/null +++ b/api-test/README.md @@ -0,0 +1,184 @@ +# Mimoto API Test Rig + +## Overview + +The **Mimoto API Test Rig** is designed for the execution of module-wise automation API tests for the mimoto module. This test rig utilizes **Java REST Assured** and **TestNG** frameworks to automate testing of the mimoto module API functionalities. The key focus is to validate the Getting Issuers, Download VC, Binding OTP, Wallet OTP and related functionalities provided by the mimoto module. + +--- + +## Test Categories + +- **Smoke**: Contains only positive test scenarios for quick verification. +- **Regression**: Includes all test scenarios, covering both positive and negative cases. + +--- + +## Coverage + +This test rig covers only **external API endpoints** exposed by the mimoto module. + +--- + +## Pre-requisites + +Before running the automation tests, ensure the following software is installed on the machine: + +- **Java 21** ([download here](https://jdk.java.net/)) +- **Maven 3.9.6** or higher ([installation guide](https://maven.apache.org/install.html)) +- **Lombok** (Refer to [Lombok Project](https://projectlombok.org/)) +- **setting.xml** ([download here](https://github.com/mosip/mosip-functional-tests/blob/master/settings.xml)) +- **apitest-commons** library should be cloned and the JAR should be built. Refer to ([README](https://github.com/mosip/mosip-functional-tests/blob/release-1.3.0/apitest-commons/README.md)) + +### For Windows + +- **Git Bash 2.18.0** or higher +- Ensure the `settings.xml` file is present in the `.m2` folder. + +### For Linux + +- The `settings.xml` file should be present in two places: + - In the regular Maven configuration folder (`/conf`) + - Under `/usr/local/maven/conf/` + +--- + +## Access Test Automation Code + +You can access the test automation code using either of the following methods: + +### From Browser + +1. Clone or download the repository as a zip file from [GitHub](https://github.com/inji/mimoto). +2. Unzip the contents to your local machine. +3. Open a terminal (Linux) or command prompt (Windows) and continue with the following steps. + +### From Git Bash + +1. Copy the Git repository URL: `https://github.com/inji/mimoto` +2. Open **Git Bash** on your local machine. +3. Run the following command to clone the repository: + ```sh + git clone https://github.com/inji/mimoto + ``` + +--- + +## Build Test Automation Code + +Once the repository is cloned or downloaded, follow these steps to build and install the test automation code: + +1. Navigate to the project directory: + ```sh + cd api-test + ``` + +2. Build the project using Maven: + ```sh + mvn clean install -Dgpg.skip=true -Dmaven.gitcommitid.skip=true + ``` + +This will download the required dependencies and prepare the test suite for execution. + +--- + +## Execute Test Automation Suite + +You can execute the test automation code using either of the following methods: + +### Using Jar + +To execute the tests using Jar, use the following steps: + +1. Navigate to the `target` directory where the JAR file is generated: + ```sh + cd target/ + ``` + +2. Run the automation test suite JAR file: + ``` + java -jar -Dmodules=mimoto -Denv.user=api-internal. -Denv.endpoint= -Denv.testLevel=smokeAndRegression -jar apitest-mimoto-0.20.0-SNAPSHOT-jar-with-dependencies.jar + ``` + +# Using Eclipse IDE + +To execute the tests using Eclipse IDE, use the following steps: + +## 1. **Install Eclipse (Latest Version)** + - Download and install the latest version of Eclipse IDE from the [Eclipse Downloads](https://www.eclipse.org/downloads/). + +## 2. **Import the Maven Project** + + After Eclipse is installed, follow these steps to import the Maven project: + + - Open Eclipse IDE. + - Go to `File` > `Import`. + - In the **Import** wizard, select `Maven` > `Existing Maven Projects`, then click **Next**. + - Browse to the location where the `api-test` folder is saved (either from the cloned Git repository or downloaded zip). + - Select the folder, and Eclipse will automatically detect the Maven project. Click **Finish** to import the project. + +## 3. **Build the Project** + + - Right-click on the project in the **Project Explorer** and select `Maven` > `Update Project`. + - This will download the required dependencies as defined in the `pom.xml` and ensure everything is correctly set up. + +## 4. **Run the Tests** + + To execute the test automation suite, you need to configure the run parameters in Eclipse: + + - Go to `Run` > `Run Configurations`. + - In the **Run Configurations** window, create a new configuration for your tests: + - Right-click on **Java Application** and select **New**. + - In the **Main** tab, select the project by browsing the location where the `api-test` folder is saved, and select the **Main class** as `io.inji.testrig.apirig.mimoto.testrunner.InjiTestRunner`. + - In the **Arguments** tab, add the necessary **VM arguments**: + - **VM Arguments**: + ``` + -Dmodules=mimoto -Denv.user=api-internal. -Denv.endpoint= -Denv.testLevel=smokeAndRegression``` + +## 5. **Run the Configuration** + + - Once the configuration is set up, click **Run** to execute the test suite. + - The tests will run, and the results will be shown in the **Console** tab of Eclipse. + + **Note**: You can also run in **Debug Mode** to troubleshoot issues by setting breakpoints in your code and choosing `Debug` instead of `Run`. + +--- + +## 6. **View Test Results** + + - After the tests are executed, you can view the detailed results in the `api-test\testng-report` directory. + - Two reports will gets generated + - First report is for pre-requisite testcases + - Second report is for core testcases + +--- + +## Details of Arguments Used + +- **env.user**: Replace `` with the appropriate environment name (e.g., `dev`, `qa`, etc.). +- **env.endpoint**: The environment where the application under test is deployed. Replace `` with the correct base URL for the environment (e.g., `https://api-internal..mosip.net`). +- **env.testLevel**: Set this to `smoke` to run only smoke test cases, or `smokeAndRegression` to run both smoke and regression tests. +- **jar**: Specify the name of the JAR file to execute. The version will change according to the development code version. For example, the current version may look like `apitest-mimoto-0.20.0-SNAPSHOT-jar-with-dependencies.jar`. + +### Build and Run Info + +To run the tests for both **Smoke** and **Regression**: + +1. Ensure the correct environment and test level parameters are set. +2. Execute the tests as shown in the command above to validate mimoto API functionalities. + +### Configuration maps + +Update the below **config maps** while running the automation in rancher or in mimoto.propertes file for local run: + +1. google.client.id +2. google.client.secret +3. google.refresh.token + +Update the below **config maps** if required +1. testCasesToExecute=TC_Mimoto_GetAllIssuers_01 (Pass the unique Identifier of the testcase that has to be ran) + +--- + +## License + +This project is licensed under the terms of the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/) diff --git a/api-test/application.properties b/api-test/application.properties new file mode 100644 index 000000000..8e2d92cc1 --- /dev/null +++ b/api-test/application.properties @@ -0,0 +1,143 @@ +server.minport=4501 +server.maxport=4600 +server.serveripaddress=127.0.0.1 +cors.headers.allowed.methods="OPTIONS, RCAPTURE, CAPTURE, MOSIPDINFO, MOSIPDISC, STREAM, GET, POST" +cors.headers.allowed.origin="*" + +mosip.mock.sbi.allowed.admin.apis="admin/score, admin/delay, admin/status, admin/profile" +mosip.mock.sbi.quality.score=94 +mosip.mock.sbi.device.purpose.registration=Registration +mosip.mock.sbi.device.purpose.auth=Auth + +mosip.mock.sbi.biometric.type.biometricdevice=Biometric Device +mosip.mock.sbi.biometric.type.finger=Finger +mosip.mock.sbi.biometric.type.face=Face +mosip.mock.sbi.biometric.type.iris=Iris + +mosip.mock.sbi.biometric.subtype.finger.slap=Slap +mosip.mock.sbi.biometric.subtype.finger.single=Single +mosip.mock.sbi.biometric.subtype.finger.touchless=Touchless +mosip.mock.sbi.biometric.subtype.face=Full face +mosip.mock.sbi.biometric.subtype.iris.single=Single +mosip.mock.sbi.biometric.subtype.iris.double=Double + +mosip.mock.sbi.biometric.seed.finger=99 +mosip.mock.sbi.biometric.seed.face=3 +mosip.mock.sbi.biometric.seed.iris=999 + +mosip.mock.sbi.file.face.digitalid.json=/Biometric Devices/Face/DigitalId.json +mosip.mock.sbi.file.face.deviceinfo.json=/Biometric Devices/Face/DeviceInfo.json +mosip.mock.sbi.file.face.devicediscovery.json=/Biometric Devices/Face/DeviceDiscovery.json +mosip.mock.sbi.file.face.streamimage=/Biometric Devices/Face/Stream Image/0.jpeg +mosip.mock.sbi.file.face.keys.keystorefilename=/device-dsk-partner.p12 +mosip.mock.sbi.file.face.keys.keyalias=keyalias +mosip.mock.sbi.file.face.keys.keystorepwd=qwerty@123 +mosip.mock.sbi.file.face.keys.keystorefilename.ftm=/Biometric Devices/Face/Keys/mosipfaceftm.p12 +mosip.mock.sbi.file.face.keys.keyalias.ftm=mosipfaceftm +mosip.mock.sbi.file.face.keys.keystorepwd.ftm=mosipfaceftm +mosip.mock.sbi.file.face.keys.encryption=/Biometric Devices/Face/Keys/mosip-ida.cer + +mosip.mock.sbi.file.finger.slap.digitalid.json=/Biometric Devices/Finger/Slap/DigitalId.json +mosip.mock.sbi.file.finger.slap.deviceinfo.json=/Biometric Devices/Finger/Slap/DeviceInfo.json +mosip.mock.sbi.file.finger.slap.devicediscovery.json=/Biometric Devices/Finger/Slap/DeviceDiscovery.json +mosip.mock.sbi.file.finger.slap.streamimage.left=/Biometric Devices/Finger/Slap/Stream Image/1.jpeg +mosip.mock.sbi.file.finger.slap.streamimage.right=/Biometric Devices/Finger/Slap/Stream Image/2.jpeg +mosip.mock.sbi.file.finger.slap.streamimage.thumb=/Biometric Devices/Finger/Slap/Stream Image/3.jpeg +mosip.mock.sbi.file.finger.slap.keys.keystorefilename=/device-dsk-partner.p12 +mosip.mock.sbi.file.finger.slap.keys.keyalias=keyalias +mosip.mock.sbi.file.finger.slap.keys.keystorepwd=qwerty@123 +mosip.mock.sbi.file.finger.slap.keys.keystorefilename.ftm=/Biometric Devices/Finger/Slap/Keys/mosipfingerslapftm.p12 +mosip.mock.sbi.file.finger.slap.keys.keyalias.ftm=mosipfingerslapftm +mosip.mock.sbi.file.finger.slap.keys.keystorepwd.ftm=mosipfingerslapftm +mosip.mock.sbi.file.finger.slap.keys.encryption=/Biometric Devices/Finger/Slap/Keys/mosip-ida.cer + +mosip.mock.sbi.file.iris.double.digitalid.json=/Biometric Devices/Iris/Double/DigitalId.json +mosip.mock.sbi.file.iris.double.deviceinfo.json=/Biometric Devices/Iris/Double/DeviceInfo.json +mosip.mock.sbi.file.iris.double.devicediscovery.json=/Biometric Devices/Iris/Double/DeviceDiscovery.json +#mosip.mock.sbi.file.iris.double.streamimage.both=/Biometric Devices/Iris/Double/Stream Image/1.jpeg +#mosip.mock.sbi.file.iris.double.streamimage.left=/Biometric Devices/Iris/Double/Stream Image/2.jpeg +#mosip.mock.sbi.file.iris.double.streamimage.right=/Biometric Devices/Iris/Double/Stream Image/3.jpeg +mosip.mock.sbi.file.iris.double.keys.keystorefilename=/device-dsk-partner.p12 +mosip.mock.sbi.file.iris.double.keys.keyalias=keyalias +mosip.mock.sbi.file.iris.double.keys.keystorepwd=qwerty@123 +mosip.mock.sbi.file.iris.double.keys.keystorefilename.ftm=/Biometric Devices/Iris/Double/Keys/mosipirisdoubleftm.p12 +mosip.mock.sbi.file.iris.double.keys.keyalias.ftm=mosipirisdoubleftm +mosip.mock.sbi.file.iris.double.keys.keystorepwd.ftm=mosipirisdoubleftm +mosip.mock.sbi.file.iris.double.keys.encryption=/Biometric Devices/Iris/Double/Keys/mosip-ida.cer + +mosip.mock.sbi.folder.profile=/resource/Profile +mosip.mock.sbi.file.folder.default=/resource/Profile/Default + +mosip.kernel.crypto.sign-algorithm-name=RS256 + +mosip.auth.server.url=https://dev.mosip.net/v1/authmanager/authenticate/clientidsecretkey +mosip.auth.appid=regproc +mosip.auth.clientid=mosip-regproc-client +mosip.auth.secretkey=abc123 + +mosip.ida.server.url=https://extint1.mosip.net/idauthentication/v1/internal/getCertificate?applicationId=IDA&referenceId=IDA-FIR + +mds_ERROR_0_msg_en=Success + +mds_ERROR_100_msg_en=Device not registered +mds_ERROR_101_msg_en=Unable to detect a biometric object +mds_ERROR_102_msg_en=Technical error during extraction +mds_ERROR_103_msg_en=Device tamper detected +mds_ERROR_104_msg_en=Unable to connect to management server +mds_ERROR_105_msg_en=Image orientation error +mds_ERROR_106_msg_en=Device not found +mds_ERROR_107_msg_en=Device public key expired +mds_ERROR_108_msg_en=Domain public key missing +mds_ERROR_109_msg_en=Requested number of biometric (Finger/IRIS) not supported +mds_ERROR_110_msg_en=Device is not ready +mds_ERROR_111_msg_en=Device is busy +mds_ERROR_112_msg_en=Device Type MisMatch for given deviceId +mds_ERROR_113_msg_en=Count MisMatch for given deviceType +mds_ERROR_114_msg_en=Device Type can be only (Finger/Iris/Face) + +mds_ERROR_500_msg_en=Invalid URL +mds_ERROR_501_msg_en=Invalid Type Value in Device Discovery Request +mds_ERROR_502_msg_en=Biometric Type values must be Biometric Device, Finger, Face, Iris +mds_ERROR_503_msg_en=Devices are not Connected, please connect devices +mds_ERROR_504_msg_en=Device Status values must be Ready, Busy, Not Ready, Not Registered +mds_ERROR_505_msg_en=Quality Score can not be empty and greater than 100 or lesser than 0 +mds_ERROR_506_msg_en=Delay can not empty and lesser than 0 milliseconds +mds_ERROR_507_msg_en=Method array can contain values only MOSIPDISC, MOSIPDINFO, CAPTURE, STREAM, RCAPTURE + +mds_ERROR_551_msg_en=Profile Not Set, please check the profile value + +mds_ERROR_601_msg_en=Livestream request can not be done for Auth Devices +mds_ERROR_604_msg_en=Device Id value not present in Livestream request object +mds_ERROR_605_msg_en=Device is not connected or not Registered for the deviceId given in Livestream request object +mds_ERROR_606_msg_en=Device connected purpose should be for Registration only for Livestream +mds_ERROR_607_msg_en=Device Status is Not Ready or Busy +mds_ERROR_608_msg_en=Live Stream Image not present +mds_ERROR_609_msg_en=Live Stream timeout +mds_ERROR_610_msg_en=Live streaming unknown error + +mds_ERROR_700_msg_en=RCapture Failed because Live streaming was stopped +mds_ERROR_701_msg_en=RCapture Failed because Capture Timeout has happened +mds_ERROR_702_msg_en=RCapture Failed because Live streaming DeviceId and DeviceSubId different +mds_ERROR_703_msg_en=RCapture process already in progress, try after sometime +mds_ERROR_704_msg_en=Device Id value not present in RCapture request object +mds_ERROR_705_msg_en=Device is not connected or not Registered for the deviceId given in RCapture request object +mds_ERROR_706_msg_en=Device connected purpose should be for Registration only for RCapture +mds_ERROR_707_msg_en=BioMetric not present for the given bio subtype +mds_ERROR_708_msg_en=No BioMetric information present for the given Biotype and BioSubType Id +mds_ERROR_709_msg_en=RCapture request can not be done for Auth Devices +mds_ERROR_710_msg_en=RCapture unknown error + +mds_ERROR_800_msg_en=Auth Capture Failed +mds_ERROR_801_msg_en=Auth Capture Failed because Capture Timeout has happened +mds_ERROR_803_msg_en=Auth Capture process already in progress, try after sometime +mds_ERROR_804_msg_en=Device Id value not present in Auth Capture request object +mds_ERROR_805_msg_en=Device is not connected or not Registered for the deviceId given in Auth Capture request object +mds_ERROR_806_msg_en=Device connected purpose should be for Auth only for Auth Capture +mds_ERROR_809_msg_en=Auth Capture request can not be done for Registration Devices +mds_ERROR_810_msg_en=Auth Capture unknown error + +mds_ERROR_999_msg_en=Unknown Error + +mosip.mock.sbi.file.iris.double.streamimage.left=/Biometric Devices/Iris/Double/Stream Image/1.jpeg +mosip.mock.sbi.file.iris.double.streamimage.right=/Biometric Devices/Iris/Double/Stream Image/2.jpeg +mosip.mock.sbi.file.iris.double.streamimage.both=/Biometric Devices/Iris/Double/Stream Image/3.jpeg diff --git a/api-test/entrypoint.sh b/api-test/entrypoint.sh new file mode 100644 index 000000000..79e58e787 --- /dev/null +++ b/api-test/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +## Run automationtests +java -jar -Dmodules="$MODULES" -Denv.user="$ENV_USER" -Denv.endpoint="$ENV_ENDPOINT" -Denv.testLevel="$ENV_TESTLEVEL" apitest-mimoto-*-jar-with-dependencies.jar; diff --git a/api-test/metadata.xml b/api-test/metadata.xml new file mode 100644 index 000000000..ea66c92e6 --- /dev/null +++ b/api-test/metadata.xml @@ -0,0 +1,16 @@ + + 4.0.0 + io.mosip.testrig.apirig.automationtests + automationtests + jar + io.mosip.testrig.apirig.automationtests + http://maven.apache.org + + + io.mosip + mosip-parent + 1.0.10 + + + diff --git a/api-test/pom.xml b/api-test/pom.xml new file mode 100644 index 000000000..4d7c49bab --- /dev/null +++ b/api-test/pom.xml @@ -0,0 +1,280 @@ + + 4.0.0 + io.inji.mimoto + apitest-mimoto + jar + apitest-mimoto + Parent project of apitest-mimoto + https://github.com/inji/mimoto + 0.20.0-SNAPSHOT + + + + MPL 2.0 + https://www.mozilla.org/en-US/MPL/2.0/ + + + + + scm:git:git://github.com/inji/mimoto.git + scm:git:ssh://github.com:inji/mimoto.git + https://github.com/inji/mimoto + HEAD + + + + + Mosip + mosip.emailnotifier@gmail.com + io.inji + https://inji.io/ + + + + + + ossrh-central + MavenCentralRepository + https://central.sonatype.com/repository/maven-snapshots/ + default + + true + + + + + + UTF-8 + + + 21 + 21 + 3.8.0 + 3.0.2 + 3.1.0 + 3.2.0 + 1.5 + 3.2.4 + 3.0.0 + 2.2.1 + + 3.0.1 + apitest-mimoto-0.20.0-SNAPSHOT-jar-with-dependencies + + + + + io.mosip.testrig.apitest.commons + apitest-commons + 1.6.0 + + + + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + none + + + + maven-compiler-plugin + ${maven.compiler.version} + + ${maven.compiler.source} + ${maven.compiler.target} + -Dfile.encoding=UTF-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven.javadoc.version} + + none + + + + pl.project13.maven + git-commit-id-plugin + ${git.commit.id.plugin.version} + + + populate-git-commit-information + + revision + + + true + MM/dd/yyyy HH:mm:ss Z + 8 + true + + ${project.build.outputDirectory}/git.properties + + + + + ${project.basedir}/.git + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven.gpg.plugin.version} + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven.shade.plugin.version} + + + + shade + + + ${fileName} + + + + + + + io.inji.testrig.apirig.mimoto.testrunner.InjiTestRunner + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven.jar.plugin.version} + + + + true + true + + + ${project.name} + ${project.version} + ${user.name} + ${os.name} + ${maven.build.timestamp} + ${env.BUILD_NUMBER} + ${env.BUILD_ID} + ${env.BUILD_URL} + + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven.war.plugin.version} + + + + true + true + + + ${project.name} + ${project.version} + ${user.name} + ${os.name} + ${maven.build.timestamp} + ${env.BUILD_NUMBER} + ${env.BUILD_ID} + ${env.BUILD_URL} + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven.source.plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven.antrun.plugin.version} + + + make-jar-executable + package + + run + + + + + + + + + + + + + diff --git a/api-test/resource/Profile/Automatic/Auth/0001Face.iso b/api-test/resource/Profile/Automatic/Auth/0001Face.iso new file mode 100644 index 000000000..fbf94babc Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Face.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Index.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Index.iso new file mode 100644 index 000000000..ed8bce579 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Index.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Index.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Index.wsq.iso new file mode 100644 index 000000000..a14b92ba3 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Index.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Iris.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Iris.iso new file mode 100644 index 000000000..b36363b6c Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Iris.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Little.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Little.iso new file mode 100644 index 000000000..9f06705f6 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Little.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Little.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Little.wsq.iso new file mode 100644 index 000000000..664d6d120 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Little.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.iso new file mode 100644 index 000000000..3886e5a79 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.wsq.iso new file mode 100644 index 000000000..e0952a043 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Middle.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.iso new file mode 100644 index 000000000..e07667852 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.wsq.iso new file mode 100644 index 000000000..9c139f53a Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Ring.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.iso new file mode 100644 index 000000000..00d5ad5d4 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.wsq.iso new file mode 100644 index 000000000..722fcef74 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Left_Thumb.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Index.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Index.iso new file mode 100644 index 000000000..e45e8d152 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Index.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Index.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Index.wsq.iso new file mode 100644 index 000000000..2f082284b Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Index.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Iris.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Iris.iso new file mode 100644 index 000000000..552750cd6 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Iris.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Little.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Little.iso new file mode 100644 index 000000000..aa801c97d Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Little.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Little.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Little.wsq.iso new file mode 100644 index 000000000..c0fd3e8e1 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Little.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.iso new file mode 100644 index 000000000..e6771d026 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.wsq.iso new file mode 100644 index 000000000..3ee43b4ff Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Middle.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.iso new file mode 100644 index 000000000..ac8d2b629 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.wsq.iso new file mode 100644 index 000000000..5ac7ca429 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Ring.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.iso new file mode 100644 index 000000000..722a5cd9d Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.iso differ diff --git a/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.wsq.iso b/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.wsq.iso new file mode 100644 index 000000000..4f73143e4 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Auth/0001Right_Thumb.wsq.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Exception_Photo.iso b/api-test/resource/Profile/Automatic/Registration/0001Exception_Photo.iso new file mode 100644 index 000000000..80caced44 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Exception_Photo.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Face.iso b/api-test/resource/Profile/Automatic/Registration/0001Face.iso new file mode 100644 index 000000000..5e84939bd Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Face.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Index.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Index.iso new file mode 100644 index 000000000..46c4112fa Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Index.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Iris.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Iris.iso new file mode 100644 index 000000000..fedadc7df Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Iris.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Little.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Little.iso new file mode 100644 index 000000000..d2b268468 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Little.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Middle.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Middle.iso new file mode 100644 index 000000000..804de92ce Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Middle.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Ring.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Ring.iso new file mode 100644 index 000000000..b40104882 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Ring.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Left_Thumb.iso b/api-test/resource/Profile/Automatic/Registration/0001Left_Thumb.iso new file mode 100644 index 000000000..0574ba6c8 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Left_Thumb.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Index.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Index.iso new file mode 100644 index 000000000..bfa1808cc Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Index.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Iris.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Iris.iso new file mode 100644 index 000000000..72bb495bf Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Iris.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Little.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Little.iso new file mode 100644 index 000000000..6b3badf29 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Little.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Middle.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Middle.iso new file mode 100644 index 000000000..0fa225597 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Middle.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Ring.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Ring.iso new file mode 100644 index 000000000..9aa5dc523 Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Ring.iso differ diff --git a/api-test/resource/Profile/Automatic/Registration/0001Right_Thumb.iso b/api-test/resource/Profile/Automatic/Registration/0001Right_Thumb.iso new file mode 100644 index 000000000..0931ac4fd Binary files /dev/null and b/api-test/resource/Profile/Automatic/Registration/0001Right_Thumb.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Face.iso b/api-test/resource/Profile/Default/Auth/Face.iso new file mode 100644 index 000000000..fbf94babc Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Face.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Finger_UKNOWN.iso b/api-test/resource/Profile/Default/Auth/Finger_UKNOWN.iso new file mode 100644 index 000000000..4ee38f986 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Finger_UKNOWN.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Finger_UKNOWN_wsq.iso b/api-test/resource/Profile/Default/Auth/Finger_UKNOWN_wsq.iso new file mode 100644 index 000000000..cc5ed30a9 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Finger_UKNOWN_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Iris_UNKNOWN.iso b/api-test/resource/Profile/Default/Auth/Iris_UNKNOWN.iso new file mode 100644 index 000000000..1764a0e6a Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Iris_UNKNOWN.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Index.iso b/api-test/resource/Profile/Default/Auth/Left_Index.iso new file mode 100644 index 000000000..ed8bce579 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Index.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Index_wsq.iso b/api-test/resource/Profile/Default/Auth/Left_Index_wsq.iso new file mode 100644 index 000000000..a14b92ba3 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Index_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Iris.iso b/api-test/resource/Profile/Default/Auth/Left_Iris.iso new file mode 100644 index 000000000..8b3aafca1 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Iris.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Little.iso b/api-test/resource/Profile/Default/Auth/Left_Little.iso new file mode 100644 index 000000000..9f06705f6 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Little.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Little_wsq.iso b/api-test/resource/Profile/Default/Auth/Left_Little_wsq.iso new file mode 100644 index 000000000..664d6d120 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Little_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Middle.iso b/api-test/resource/Profile/Default/Auth/Left_Middle.iso new file mode 100644 index 000000000..3886e5a79 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Middle.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Middle_wsq.iso b/api-test/resource/Profile/Default/Auth/Left_Middle_wsq.iso new file mode 100644 index 000000000..e0952a043 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Middle_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Ring.iso b/api-test/resource/Profile/Default/Auth/Left_Ring.iso new file mode 100644 index 000000000..e07667852 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Ring.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Ring_wsq.iso b/api-test/resource/Profile/Default/Auth/Left_Ring_wsq.iso new file mode 100644 index 000000000..9c139f53a Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Ring_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Thumb.iso b/api-test/resource/Profile/Default/Auth/Left_Thumb.iso new file mode 100644 index 000000000..00d5ad5d4 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Thumb.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Left_Thumb_wsq.iso b/api-test/resource/Profile/Default/Auth/Left_Thumb_wsq.iso new file mode 100644 index 000000000..722fcef74 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Left_Thumb_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Index.iso b/api-test/resource/Profile/Default/Auth/Right_Index.iso new file mode 100644 index 000000000..e45e8d152 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Index.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Index_wsq.iso b/api-test/resource/Profile/Default/Auth/Right_Index_wsq.iso new file mode 100644 index 000000000..2f082284b Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Index_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Iris.iso b/api-test/resource/Profile/Default/Auth/Right_Iris.iso new file mode 100644 index 000000000..3f2d73ad1 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Iris.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Little.iso b/api-test/resource/Profile/Default/Auth/Right_Little.iso new file mode 100644 index 000000000..aa801c97d Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Little.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Little_wsq.iso b/api-test/resource/Profile/Default/Auth/Right_Little_wsq.iso new file mode 100644 index 000000000..c0fd3e8e1 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Little_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Middle.iso b/api-test/resource/Profile/Default/Auth/Right_Middle.iso new file mode 100644 index 000000000..e6771d026 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Middle.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Middle_wsq.iso b/api-test/resource/Profile/Default/Auth/Right_Middle_wsq.iso new file mode 100644 index 000000000..3ee43b4ff Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Middle_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Ring.iso b/api-test/resource/Profile/Default/Auth/Right_Ring.iso new file mode 100644 index 000000000..ac8d2b629 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Ring.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Ring_wsq.iso b/api-test/resource/Profile/Default/Auth/Right_Ring_wsq.iso new file mode 100644 index 000000000..5ac7ca429 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Ring_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Thumb.iso b/api-test/resource/Profile/Default/Auth/Right_Thumb.iso new file mode 100644 index 000000000..722a5cd9d Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Thumb.iso differ diff --git a/api-test/resource/Profile/Default/Auth/Right_Thumb_wsq.iso b/api-test/resource/Profile/Default/Auth/Right_Thumb_wsq.iso new file mode 100644 index 000000000..4f73143e4 Binary files /dev/null and b/api-test/resource/Profile/Default/Auth/Right_Thumb_wsq.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Exception_Photo.iso b/api-test/resource/Profile/Default/Registration/Exception_Photo.iso new file mode 100644 index 000000000..80caced44 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Exception_Photo.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Face.iso b/api-test/resource/Profile/Default/Registration/Face.iso new file mode 100644 index 000000000..5e84939bd Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Face.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Index.iso b/api-test/resource/Profile/Default/Registration/Left_Index.iso new file mode 100644 index 000000000..46c4112fa Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Index.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Iris.iso b/api-test/resource/Profile/Default/Registration/Left_Iris.iso new file mode 100644 index 000000000..fedadc7df Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Iris.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Little.iso b/api-test/resource/Profile/Default/Registration/Left_Little.iso new file mode 100644 index 000000000..d2b268468 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Little.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Middle.iso b/api-test/resource/Profile/Default/Registration/Left_Middle.iso new file mode 100644 index 000000000..804de92ce Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Middle.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Ring.iso b/api-test/resource/Profile/Default/Registration/Left_Ring.iso new file mode 100644 index 000000000..b40104882 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Ring.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Left_Thumb.iso b/api-test/resource/Profile/Default/Registration/Left_Thumb.iso new file mode 100644 index 000000000..0574ba6c8 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Left_Thumb.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Index.iso b/api-test/resource/Profile/Default/Registration/Right_Index.iso new file mode 100644 index 000000000..bfa1808cc Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Index.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Iris.iso b/api-test/resource/Profile/Default/Registration/Right_Iris.iso new file mode 100644 index 000000000..72bb495bf Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Iris.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Little.iso b/api-test/resource/Profile/Default/Registration/Right_Little.iso new file mode 100644 index 000000000..6b3badf29 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Little.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Middle.iso b/api-test/resource/Profile/Default/Registration/Right_Middle.iso new file mode 100644 index 000000000..0fa225597 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Middle.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Ring.iso b/api-test/resource/Profile/Default/Registration/Right_Ring.iso new file mode 100644 index 000000000..9aa5dc523 Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Ring.iso differ diff --git a/api-test/resource/Profile/Default/Registration/Right_Thumb.iso b/api-test/resource/Profile/Default/Registration/Right_Thumb.iso new file mode 100644 index 000000000..0931ac4fd Binary files /dev/null and b/api-test/resource/Profile/Default/Registration/Right_Thumb.iso differ diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testrunner/InjiTestRunner.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testrunner/InjiTestRunner.java new file mode 100644 index 000000000..094670f0d --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testrunner/InjiTestRunner.java @@ -0,0 +1,332 @@ +package io.inji.testrig.apirig.mimoto.testrunner; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.testng.TestNG; + +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dataprovider.BiometricDataProvider; +import io.mosip.testrig.apirig.dbaccess.DBManager; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.ExtractResource; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.testrunner.OTPListener; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthTestsUtil; +import io.mosip.testrig.apirig.utils.CertsUtil; +import io.mosip.testrig.apirig.utils.DependencyResolver; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.GlobalMethods; +import io.mosip.testrig.apirig.utils.JWKKeyUtil; +import io.mosip.testrig.apirig.utils.KernelAuthentication; +import io.mosip.testrig.apirig.utils.KeyCloakUserAndAPIKeyGeneration; +import io.mosip.testrig.apirig.utils.KeycloakUserManager; +import io.mosip.testrig.apirig.utils.MispPartnerAndLicenseKeyGeneration; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.PartnerRegistration; +import io.mosip.testrig.apirig.utils.SkipTestCaseHandler; + +/** + * Class to initiate mosip api test execution + * + * @author Vignesh + * + */ +public class InjiTestRunner { + private static final Logger LOGGER = Logger.getLogger(InjiTestRunner.class); + private static String cachedPath = null; + private static String generateDependency; + + public static String jarUrl = InjiTestRunner.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + public static List languageList = new ArrayList<>(); + public static boolean skipAll = false; + + /** + * C Main method to start mosip test execution + * + * @param arg + */ + public static void main(String[] arg) { + + try { + LOGGER.info("** ------------- API Test Rig Run Started --------------------------------------------- **"); + + BaseTestCase.setRunContext(getRunType(), jarUrl); + ExtractResource.removeOldMosipTestTestResource(); + if (getRunType().equalsIgnoreCase("JAR")) { + ExtractResource.extractCommonResourceFromJar(); + } else { + ExtractResource.copyCommonResources(); + } + AdminTestUtil.init(); + MimotoConfigManager.init(); + suiteSetup(getRunType()); + SkipTestCaseHandler.loadTestcaseToBeSkippedList("testCaseSkippedList.txt"); + GlobalMethods.setModuleNameAndReCompilePattern(MimotoConfigManager.getproperty("moduleNamePattern")); + GlobalMethods.reportCaptchaStatus(GlobalConstants.CAPTCHA_ENABLED, false); + setLogLevels(); + MimotoConfigManager.getEsignetBaseUrl(); + + HealthChecker healthcheck = new HealthChecker(); + healthcheck.setCurrentRunningModule(GlobalConstants.MIMOTO); + Thread trigger = new Thread(healthcheck); + trigger.start(); + + KeycloakUserManager.removeUser(); + KeycloakUserManager.createUsers(); + KeycloakUserManager.closeKeycloakInstance(); + AdminTestUtil.getRequiredField(); + + // Generate device certificates to be consumed by Mock-MDS + PartnerRegistration.deleteCertificates(); + PartnerRegistration.deviceGeneration(); + + // Generating biometric details with mock MDS + BiometricDataProvider.generateBiometricTestData("Registration"); + + String testCasesToExecuteString = MimotoConfigManager.getproperty("testCasesToExecute"); + generateDependency = MimotoConfigManager.getproperty("generateDependencyJson"); + + if (!"yes".equalsIgnoreCase(generateDependency)) { + if (testCasesToExecuteString != null && !testCasesToExecuteString.isBlank()) { + DependencyResolver.loadDependencies(BaseTestCase.getTestCaseInterDependencyPath()); + MimotoUtil.testCasesInRunScope = DependencyResolver.getDependencies(testCasesToExecuteString); + } + } + + startTestRunner(); + } catch (Exception e) { + LOGGER.error("Exception " + e.getMessage()); + } + + MimotoUtil.dbCleanUp(); + KeycloakUserManager.removeUser(); + KeycloakUserManager.closeKeycloakInstance(); + + OTPListener.bTerminate = true; + HealthChecker.bTerminate = true; + + if ("yes".equalsIgnoreCase(generateDependency)) { + LOGGER.info("Generating test case inter-dependencies"); + AdminTestUtil.generateTestCaseInterDependencies(BaseTestCase.getTestCaseInterDependencyPath()); + } + + System.exit(0); + + } + + public static void suiteSetup(String runType) { + if (MimotoConfigManager.IsDebugEnabled()) + LOGGER.setLevel(Level.ALL); + else + LOGGER.info("Test Framework for Mosip api Initialized"); + BaseTestCase.initialize(); + LOGGER.info("Done with BeforeSuite and test case setup! su TEST EXECUTION!\n\n"); + + if (!runType.equalsIgnoreCase("JAR")) { + AuthTestsUtil.removeOldMosipTempTestResource(); + } + AdminTestUtil.initiateMimotoTest(); + BaseTestCase.currentModule = BaseTestCase.runContext + GlobalConstants.MIMOTO; + BaseTestCase.certsForModule = BaseTestCase.runContext + GlobalConstants.MIMOTO; + MimotoUtil.dbCleanUp(); + BaseTestCase.otpListener = new OTPListener(); + BaseTestCase.otpListener.run(); + } + + private static void setLogLevels() { + AdminTestUtil.setLogLevel(); + OutputValidationUtil.setLogLevel(); + PartnerRegistration.setLogLevel(); + KeyCloakUserAndAPIKeyGeneration.setLogLevel(); + MispPartnerAndLicenseKeyGeneration.setLogLevel(); + JWKKeyUtil.setLogLevel(); + CertsUtil.setLogLevel(); + KernelAuthentication.setLogLevel(); + BaseTestCase.setLogLevel(); + MimotoUtil.setLogLevel(); + KeycloakUserManager.setLogLevel(); + DBManager.setLogLevel(); + BiometricDataProvider.setLogLevel(); + } + + /** + * The method to start mosip testng execution + * + * @throws IOException + */ + public static void startTestRunner() { + File homeDir = null; + String os = System.getProperty("os.name"); + LOGGER.info(os); + if (getRunType().contains("IDE") || os.toLowerCase().contains("windows")) { + homeDir = new File(System.getProperty("user.dir") + "/testNgXmlFiles"); + LOGGER.info("IDE :" + homeDir); + } else { + File dir = new File(System.getProperty("user.dir")); + homeDir = new File(dir.getParent() + "/inji/testNgXmlFiles"); + LOGGER.info("ELSE :" + homeDir); + } + File[] files = homeDir.listFiles(); + if (files != null) { + for (File file : files) { + TestNG runner = new TestNG(); + List suitefiles = new ArrayList<>(); + if (file.getName().toLowerCase().contains("mastertestsuite")) { + BaseTestCase.setReportName(GlobalConstants.MIMOTO); + suitefiles.add(file.getAbsolutePath()); + runner.setTestSuites(suitefiles); + System.getProperties().setProperty("testng.outpur.dir", "testng-report"); + runner.setOutputDirectory("testng-report"); + runner.run(); + } + } + } else { + LOGGER.error("No files found in directory: " + homeDir); + } + } + + public static String getGlobalResourcePath() { + if (cachedPath != null) { + return cachedPath; + } + + String path = null; + if (getRunType().equalsIgnoreCase("JAR")) { + path = new File(jarUrl).getParentFile().getAbsolutePath() + "/MosipTestResource/MosipTemporaryTestResource"; + } else if (getRunType().equalsIgnoreCase("IDE")) { + path = new File(InjiTestRunner.class.getClassLoader().getResource("").getPath()).getAbsolutePath() + + "/MosipTestResource/MosipTemporaryTestResource"; + if (path.contains(GlobalConstants.TESTCLASSES)) + path = path.replace(GlobalConstants.TESTCLASSES, "classes"); + } + + if (path != null) { + cachedPath = path; + return path; + } else { + return "Global Resource File Path Not Found"; + } + } + + public static String getResourcePath() { + return getGlobalResourcePath(); + } + + public static String generatePulicKey() { + String publicKey = null; + try { + KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); + keyGenerator.initialize(2048, BaseTestCase.secureRandom); + final KeyPair keypair = keyGenerator.generateKeyPair(); + publicKey = java.util.Base64.getEncoder().encodeToString(keypair.getPublic().getEncoded()); + } catch (NoSuchAlgorithmException e) { + LOGGER.error(e.getMessage()); + } + return publicKey; + } + + public static KeyPairGenerator keyPairGen = null; + + public static KeyPairGenerator getKeyPairGeneratorInstance() { + if (keyPairGen != null) + return keyPairGen; + try { + keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + + } catch (NoSuchAlgorithmException e) { + LOGGER.error(e.getMessage()); + } + + return keyPairGen; + } + + public static String generatePublicKeyForMimoto() { + + String vcString = ""; + try { + KeyPairGenerator keyPairGenerator = getKeyPairGeneratorInstance(); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + StringWriter stringWriter = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + vcString = stringWriter.toString(); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + vcString = vcString.replaceAll("\r\n", "\\\\n"); + } else { + vcString = vcString.replaceAll("\n", "\\\\n"); + } + } catch (Exception e) { + throw e; + } + } catch (Exception e) { + LOGGER.error(e.getMessage()); + } + return vcString; + } + + public static String generateJWKPublicKey() { + try { + KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); + keyGenerator.initialize(2048, BaseTestCase.secureRandom); + final KeyPair keypair = keyGenerator.generateKeyPair(); + RSAKey jwk = new RSAKey.Builder((RSAPublicKey) keypair.getPublic()).keyID("RSAKeyID") + .keyUse(KeyUse.SIGNATURE).privateKey(keypair.getPrivate()).build(); + + return jwk.toJSONString(); + } catch (NoSuchAlgorithmException e) { + LOGGER.error(e.getMessage()); + return null; + } + } + + public static Properties getproperty(String path) { + Properties prop = new Properties(); + FileInputStream inputStream = null; + try { + File file = new File(path); + inputStream = new FileInputStream(file); + prop.load(inputStream); + } catch (Exception e) { + LOGGER.error(GlobalConstants.EXCEPTION_STRING_2 + e.getMessage()); + } finally { + AdminTestUtil.closeInputStream(inputStream); + } + return prop; + } + + /** + * The method will return mode of application started either from jar or eclipse + * ide + * + * @return + */ + public static String getRunType() { + if (InjiTestRunner.class.getResource("InjiTestRunner.class").getPath().contains(".jar")) + return "JAR"; + else + return "IDE"; + } + +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/AddIdentity.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/AddIdentity.java new file mode 100644 index 000000000..3eb89c67d --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/AddIdentity.java @@ -0,0 +1,172 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.testrunner.JsonPrecondtion; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.KernelAuthentication; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.RestClient; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class AddIdentity extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(AddIdentity.class); + protected String testCaseName = ""; + public Response response = null; + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + + } + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + testCaseDTO.setInputTemplate(AdminTestUtil.modifySchemaGenerateHbs(testCaseDTO.isRegenerateHbs())); + String uin = JsonPrecondtion + .getValueFromJson( + RestClient.getRequestWithCookie(ApplnURI + "/v1/idgenerator/uin", MediaType.APPLICATION_JSON, + MediaType.APPLICATION_JSON, COOKIENAME, + new KernelAuthentication().getTokenByRole(testCaseDTO.getRole())).asString(), + "response.uin"); + + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss"); + Calendar cal = Calendar.getInstance(); + String timestampValue = dateFormatter.format(cal.getTime()); + String genRid = "27847" + generateRandomNumberString(10) + timestampValue; + + String jsonInput = testCaseDTO.getInput(); + + String inputJson = getJsonFromTemplate(jsonInput, testCaseDTO.getInputTemplate(), false); + + inputJson = inputJson.replace("$UIN$", uin); + inputJson = inputJson.replace("$RID$", genRid); + String phoneNumber = ""; + String email = testCaseName + "_" + BaseTestCase.runContext +"@mosip.net"; + if (inputJson.contains("$PHONENUMBERFORIDENTITY$")) { + if (!phoneSchemaRegex.isEmpty()) + try { + phoneNumber = genStringAsperRegex(phoneSchemaRegex); + } catch (Exception e) { + logger.error(e.getMessage()); + } + inputJson = replaceKeywordWithValue(inputJson, "$PHONENUMBERFORIDENTITY$", phoneNumber); + inputJson = replaceKeywordWithValue(inputJson, "$EMAILVALUE$", email); + } + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + response = postWithBodyAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, + testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + if (testCaseDTO.getTestCaseName().contains("_Pos")) { + writeAutoGeneratedId(testCaseDTO.getTestCaseName(), "UIN", uin); + writeAutoGeneratedId(testCaseDTO.getTestCaseName(), "RID", genRid); + writeAutoGeneratedId(testCaseDTO.getTestCaseName(), "EMAIL", email); + writeAutoGeneratedId(testCaseDTO.getTestCaseName(), "PHONE", phoneNumber); + } + if (!phoneNumber.isEmpty()) + writeAutoGeneratedId(testCaseDTO.getTestCaseName(), "PHONE", phoneNumber); + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } + + @AfterClass(alwaysRun = true) + public void waittime() { + + try { + logger.info( + "waiting for " + properties.getProperty("Delaytime") + " mili secs after UIN Generation In IDREPO"); + Thread.sleep(Long.parseLong(properties.getProperty("Delaytime"))); + + } catch (Exception e) { + logger.error("Exception : " + e.getMessage()); + Thread.currentThread().interrupt(); + } + + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/DeleteWithParam.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/DeleteWithParam.java new file mode 100644 index 000000000..729629f77 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/DeleteWithParam.java @@ -0,0 +1,155 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class DeleteWithParam extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(DeleteWithParam.class); + protected String testCaseName = ""; + public Response response = null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + String[] templateFields = testCaseDTO.getTemplateFields(); + + if (testCaseDTO.getTemplateFields() != null && templateFields.length > 0) { + ArrayList inputtestCases = AdminTestUtil.getInputTestCase(testCaseDTO); + ArrayList outputtestcase = AdminTestUtil.getOutputTestCase(testCaseDTO); + + for (int i = 0; i < languageList.size(); i++) { + response = deleteWithPathParamAndCookie(ApplnURI + testCaseDTO.getEndPoint(), + getJsonFromTemplate(inputtestCases.get(i).toString(), testCaseDTO.getInputTemplate()), + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(outputtestcase.get(i).toString(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + else { + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + if (testCaseName.contains("ESignet_")) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + + String tempUrl = ApplnURI; + + if (testCaseDTO.getEndPoint().startsWith("$SUNBIRDBASEURL$") && testCaseName.contains("SunBirdR")) { + + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + if (MimotoConfigManager.getSunbirdBaseURL() != null && !MimotoConfigManager.getSunbirdBaseURL().isBlank()) + tempUrl = MimotoConfigManager.getSunbirdBaseURL(); + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$SUNBIRDBASEURL$", "")); + } + + response = deleteWithPathParamAndCookie(tempUrl + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, + testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + + } else { + response = deleteWithPathParamAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, + testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + } + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParam.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParam.java new file mode 100644 index 000000000..90c129575 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParam.java @@ -0,0 +1,194 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class GetWithParam extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(GetWithParam.class); + protected String testCaseName = ""; + public Response response = null; + public boolean sendEsignetToken = false; + public boolean auditLogCheck = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + sendEsignetToken = context.getCurrentXmlTest().getLocalParameters().containsKey("sendEsignetToken"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + auditLogCheck = testCaseDTO.isAuditLogCheck(); + String[] templateFields = testCaseDTO.getTemplateFields(); + + if (testCaseDTO.getInputTemplate().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setInputTemplate(testCaseDTO.getInputTemplate().replace(GlobalConstants.$PRIMARYLANG$, + BaseTestCase.languageList.get(0))); + if (testCaseDTO.getOutputTemplate().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setOutputTemplate(testCaseDTO.getOutputTemplate().replace(GlobalConstants.$PRIMARYLANG$, + BaseTestCase.languageList.get(0))); + if (testCaseDTO.getInput().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setInput( + testCaseDTO.getInput().replace(GlobalConstants.$PRIMARYLANG$, BaseTestCase.languageList.get(0))); + if (testCaseDTO.getOutput().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setOutput( + testCaseDTO.getOutput().replace(GlobalConstants.$PRIMARYLANG$, BaseTestCase.languageList.get(0))); + + if (testCaseDTO.getTemplateFields() != null && templateFields.length > 0) { + ArrayList inputtestCases = AdminTestUtil.getInputTestCase(testCaseDTO); + ArrayList outputtestcase = AdminTestUtil.getOutputTestCase(testCaseDTO); + for (int i = 0; i < languageList.size(); i++) { + response = getWithPathParamAndCookie(ApplnURI + testCaseDTO.getEndPoint(), + getJsonFromTemplate(inputtestCases.get(i).toString(), testCaseDTO.getInputTemplate()), + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(outputtestcase.get(i).toString(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + else { + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + String outputJson = getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()); + outputJson = MimotoUtil.inputstringKeyWordHandeler(outputJson, testCaseName); + + if (testCaseName.contains("ESignet_")) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + + String tempUrl = ApplnURI; + + if (testCaseDTO.getEndPoint().startsWith("$SUNBIRDBASEURL$") && testCaseName.contains("SunBirdR")) { + + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + if (MimotoConfigManager.getSunbirdBaseURL() != null + && !MimotoConfigManager.getSunbirdBaseURL().isBlank()) + tempUrl = MimotoConfigManager.getSunbirdBaseURL(); + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$SUNBIRDBASEURL$", "")); + } + + response = getWithPathParamAndCookie(tempUrl + testCaseDTO.getEndPoint(), inputJson, auditLogCheck, + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), sendEsignetToken); + + } else { + response = getWithPathParamAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, auditLogCheck, + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), sendEsignetToken); + } + + Map> ouputValid = null; + if (testCaseName.contains("_StatusCode")) { + + OutputValidationDto customResponse = customStatusCodeResponse(String.valueOf(response.getStatusCode()), + testCaseDTO.getOutput()); + + ouputValid = new HashMap<>(); + ouputValid.put(GlobalConstants.EXPECTED_VS_ACTUAL, List.of(customResponse)); + } else { + ouputValid = OutputValidationUtil.doJsonOutputValidation(response.asString(), outputJson, testCaseDTO, + response.getStatusCode()); + } + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParamAndHeader.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParamAndHeader.java new file mode 100644 index 000000000..60b1fae8d --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithParamAndHeader.java @@ -0,0 +1,178 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.itextpdf.text.pdf.PdfReader; +import com.itextpdf.text.pdf.parser.PdfTextExtractor; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.GlobalMethods; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class GetWithParamAndHeader extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(GetWithParamAndHeader.class); + protected String testCaseName = ""; + public Response response = null; + public String pathParams = null; + public String headers = null; + public byte[] pdf = null; + public String pdfAsText = null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + headers = context.getCurrentXmlTest().getLocalParameters().get("headers"); + pathParams = context.getCurrentXmlTest().getLocalParameters().get("pathParams"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + String[] templateFields = testCaseDTO.getTemplateFields(); + + if (testCaseDTO.getTemplateFields() != null && templateFields.length > 0) { + ArrayList inputtestCases = AdminTestUtil.getInputTestCase(testCaseDTO); + ArrayList outputtestcase = AdminTestUtil.getOutputTestCase(testCaseDTO); + for (int i = 0; i < languageList.size(); i++) { + response = getWithPathParamsBodyHeadersAndCookie(ApplnURI + testCaseDTO.getEndPoint(), + getJsonFromTemplate(inputtestCases.get(i).toString(), testCaseDTO.getInputTemplate()), + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), pathParams, headers); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(outputtestcase.get(i).toString(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + else { + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + String outputJson = getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()); + outputJson = MimotoUtil.inputstringKeyWordHandeler(outputJson, testCaseName); + + response = getWithPathParamsBodyHeadersAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), pathParams, headers); + + String contentType = response.getHeader("Content-Type"); + if (contentType != null && contentType.contains("application/pdf")) { + pdf = response.asByteArray(); + + PdfReader pdfReader = null; + ByteArrayInputStream bIS = null; + + try { + bIS = new ByteArrayInputStream(pdf); + pdfReader = new PdfReader(bIS); + pdfAsText = PdfTextExtractor.getTextFromPage(pdfReader, 1); + } catch (IOException e) { + Reporter.log("Exception : " + e.getMessage()); + } finally { + AdminTestUtil.closeByteArrayInputStream(bIS); + AdminTestUtil.closePdfReader(pdfReader); + } + + if (pdf != null && (new String(pdf).contains("errors") || pdfAsText == null)) { + GlobalMethods.reportResponse(null, ApplnURI + testCaseDTO.getEndPoint(), + "Not able to download issuer credential"); + throw new AdminTestException("Not able to download issuer credential"); + } else { + GlobalMethods.reportResponse(null, ApplnURI + testCaseDTO.getEndPoint(), pdfAsText, true); + } + + } else { + Map> ouputValid = null; + ouputValid = OutputValidationUtil.doJsonOutputValidation(response.asString(), outputJson, testCaseDTO, + response.getStatusCode()); + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + + } + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithPathParamAndCookieForAutoGenId.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithPathParamAndCookieForAutoGenId.java new file mode 100644 index 000000000..9ec2b8093 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/GetWithPathParamAndCookieForAutoGenId.java @@ -0,0 +1,126 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class GetWithPathParamAndCookieForAutoGenId extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(GetWithPathParamAndCookieForAutoGenId.class); + protected String testCaseName = ""; + public String pathParams = null; + public String headers = null; + public String idKeyName = null; + public boolean auditLogCheck = false; + public boolean sendEsignetToken = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list. It reads the required parameters + * from the test context. * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + pathParams = context.getCurrentXmlTest().getLocalParameters().get("pathParams"); + headers = context.getCurrentXmlTest().getLocalParameters().get("headers"); + idKeyName = context.getCurrentXmlTest().getLocalParameters().get("idKeyName"); + sendEsignetToken = context.getCurrentXmlTest().getLocalParameters().containsKey("sendEsignetToken"); + + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for Get request execution with auto generated ID feature. + * * @param testCaseDTO + * + * @throws AuthenticationTestException + * @throws AdminTestException + * @throws SecurityXSSException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) + throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + auditLogCheck = testCaseDTO.isAuditLogCheck(); + + // Handle language variables in templates and inputs + if (testCaseDTO.getInputTemplate().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setInputTemplate(testCaseDTO.getInputTemplate().replace(GlobalConstants.$PRIMARYLANG$, + BaseTestCase.languageList.get(0))); + if (testCaseDTO.getOutputTemplate().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setOutputTemplate(testCaseDTO.getOutputTemplate().replace(GlobalConstants.$PRIMARYLANG$, + BaseTestCase.languageList.get(0))); + if (testCaseDTO.getInput().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setInput( + testCaseDTO.getInput().replace(GlobalConstants.$PRIMARYLANG$, BaseTestCase.languageList.get(0))); + if (testCaseDTO.getOutput().contains(GlobalConstants.$PRIMARYLANG$)) + testCaseDTO.setOutput( + testCaseDTO.getOutput().replace(GlobalConstants.$PRIMARYLANG$, BaseTestCase.languageList.get(0))); + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + Response response = getWithPathParamAndCookieForAutoGeneratedId(ApplnURI + testCaseDTO.getEndPoint(), inputJson, + auditLogCheck, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), this.idKeyName, + sendEsignetToken); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PatchWithPathParamsBodyAndCookie.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PatchWithPathParamsBodyAndCookie.java new file mode 100644 index 000000000..8139b25ce --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PatchWithPathParamsBodyAndCookie.java @@ -0,0 +1,91 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class PatchWithPathParamsBodyAndCookie extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PatchWithPathParamsBodyAndCookie.class); + protected String testCaseName = ""; + public String pathParams = null; + Response response = null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + @Override + public String getTestName() { + return testCaseName; + } + + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + pathParams = context.getCurrentXmlTest().getLocalParameters().get("pathParams"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) + throws AuthenticationTestException, AdminTestException, SecurityXSSException { + + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + response = patchWithPathParamsBodyAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, + testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), pathParams); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) { + throw new AdminTestException("Failed at output validation"); + } + } + + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithAutogenIdWithOtpGenerate.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithAutogenIdWithOtpGenerate.java new file mode 100644 index 000000000..bd8e4b099 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithAutogenIdWithOtpGenerate.java @@ -0,0 +1,232 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.NotificationListener; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class PostWithAutogenIdWithOtpGenerate extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PostWithAutogenIdWithOtpGenerate.class); + protected String testCaseName = ""; + public String idKeyName = null; + public Response response = null; + public boolean auditLogCheck = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + idKeyName = context.getCurrentXmlTest().getLocalParameters().get("idKeyName"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + * @throws InterruptedException + * @throws NumberFormatException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) + throws AuthenticationTestException, AdminTestException, NumberFormatException, InterruptedException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + JSONObject req = new JSONObject(testCaseDTO.getInput()); + + auditLogCheck = testCaseDTO.isAuditLogCheck(); + String otpRequest = null; + String sendOtpReqTemplate = null; + String sendOtpEndPoint = null; + if (req.has(GlobalConstants.SENDOTP)) { + otpRequest = req.get(GlobalConstants.SENDOTP).toString(); + req.remove(GlobalConstants.SENDOTP); + } + JSONObject otpReqJson = new JSONObject(otpRequest); + sendOtpReqTemplate = otpReqJson.getString("sendOtpReqTemplate"); + otpReqJson.remove("sendOtpReqTemplate"); + sendOtpEndPoint = otpReqJson.getString("sendOtpEndPoint"); + otpReqJson.remove("sendOtpEndPoint"); + + String inputStrJson = getJsonFromTemplate(otpReqJson.toString(), sendOtpReqTemplate); + + Response otpResponse = null; + int maxLoopCount = Integer.parseInt(properties.getProperty("uinGenMaxLoopCount")); + int currLoopCount = 0; + while (currLoopCount < maxLoopCount) { + String input = MimotoUtil.inputstringKeyWordHandeler(inputStrJson, testCaseName); + NotificationListener.markRequestStart(); + if (testCaseName.contains(GlobalConstants.ESIGNET_)) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + String tempUrl = MimotoConfigManager.getEsignetBaseUrl(); + otpResponse = postRequestWithCookieAuthHeaderAndXsrfToken(tempUrl + sendOtpEndPoint, input, COOKIENAME, + testCaseDTO.getTestCaseName()); + } else { + otpResponse = postWithBodyAndCookie(ApplnURI + sendOtpEndPoint, input, COOKIENAME, + GlobalConstants.RESIDENT, testCaseDTO.getTestCaseName()); + } + + if (otpResponse != null && otpResponse.asString().contains("IDA-MLC-018")) { + logger.info("waiting for: " + properties.getProperty("uinGenDelayTime") + + " as UIN not available in database"); + try { + Thread.sleep(Long.parseLong(properties.getProperty("uinGenDelayTime"))); + } catch (NumberFormatException | InterruptedException e) { + logger.error(e.getMessage()); + Thread.currentThread().interrupt(); + } + } else { + break; + } + + currLoopCount++; + } + + JSONObject res = new JSONObject(testCaseDTO.getOutput()); + String sendOtpResp = null; + String sendOtpResTemplate = null; + if (res.has(GlobalConstants.SENDOTPRESP)) { + sendOtpResp = res.get(GlobalConstants.SENDOTPRESP).toString(); + res.remove(GlobalConstants.SENDOTPRESP); + } + JSONObject sendOtpRespJson = new JSONObject(sendOtpResp); + sendOtpResTemplate = sendOtpRespJson.getString("sendOtpResTemplate"); + sendOtpRespJson.remove("sendOtpResTemplate"); + if (otpResponse != null) { + Map> ouputValidOtp = OutputValidationUtil.doJsonOutputValidation( + otpResponse.asString(), getJsonFromTemplate(sendOtpRespJson.toString(), sendOtpResTemplate), + testCaseDTO, otpResponse.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValidOtp)); + + if (!OutputValidationUtil.publishOutputResult(ouputValidOtp)) { + if (otpResponse.asString().contains("IDA-OTA-001")) { + throw new AdminTestException( + "Exceeded number of OTP requests in a given time, Increase otp.request.flooding.max-count"); + } + + else + throw new AdminTestException("Failed at otp output validation"); + } + + } else { + throw new AdminTestException("Invalid otp response"); + } + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + if (testCaseName.contains(GlobalConstants.ESIGNET_)) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + String tempUrl = MimotoConfigManager.getEsignetBaseUrl(); + inputJson = MimotoUtil.getOTPFromSMTP(inputJson, testCaseDTO); + response = postRequestWithCookieAuthHeaderAndXsrfTokenForAutoGenId(tempUrl + testCaseDTO.getEndPoint(), + inputJson, COOKIENAME, testCaseDTO.getTestCaseName(), idKeyName); + } else { + response = postWithBodyAndCookieForAutoGeneratedId(ApplnURI + testCaseDTO.getEndPoint(), inputJson, + auditLogCheck, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + } + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(res.toString(), testCaseDTO.getOutputTemplate()), testCaseDTO, + response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + NotificationListener.markRequestRemove(); + } + + @AfterClass(alwaysRun = true) + public void waittime() { + if (!testCaseName.contains(GlobalConstants._AUTHENTICATEUSER) + && MimotoUtil.isOTPEnabled().equals("true")) { + try { + long delayTime = Long.parseLong(properties.getProperty("Delaytime")); + logger.info("waiting for " + delayTime + " mili secs after VID Generation In RESIDENT SERVICES"); + Thread.sleep(delayTime); + } catch (Exception e) { + logger.error("Exception : " + e.getMessage()); + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithBodyWithOtpGenerate.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithBodyWithOtpGenerate.java new file mode 100644 index 000000000..1395cb23d --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithBodyWithOtpGenerate.java @@ -0,0 +1,178 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.NotificationListener; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class PostWithBodyWithOtpGenerate extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PostWithBodyWithOtpGenerate.class); + protected String testCaseName = ""; + public Response response = null; + public boolean sendEsignetToken = false; + public boolean auditLogCheck = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + sendEsignetToken = context.getCurrentXmlTest().getLocalParameters().containsKey("sendEsignetToken"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + auditLogCheck = testCaseDTO.isAuditLogCheck(); + String tempUrl = MimotoConfigManager.getEsignetBaseUrl(); + JSONObject req = new JSONObject(testCaseDTO.getInput()); + String otpRequest = null; + String sendOtpReqTemplate = null; + String sendOtpEndPoint = null; + if (req.has(GlobalConstants.SENDOTP)) { + otpRequest = req.get(GlobalConstants.SENDOTP).toString(); + req.remove(GlobalConstants.SENDOTP); + } + JSONObject otpReqJson = new JSONObject(otpRequest); + sendOtpReqTemplate = otpReqJson.getString("sendOtpReqTemplate"); + otpReqJson.remove("sendOtpReqTemplate"); + sendOtpEndPoint = otpReqJson.getString("sendOtpEndPoint"); + otpReqJson.remove("sendOtpEndPoint"); + Response otpResponse = null; + + String inputReqJson = getJsonFromTemplate(otpReqJson.toString(), sendOtpReqTemplate); + inputReqJson = MimotoUtil.inputstringKeyWordHandeler(inputReqJson, testCaseName); + + NotificationListener.markRequestStart(); + otpResponse = postWithBodyAndCookie(ApplnURI + sendOtpEndPoint, inputReqJson, COOKIENAME, + GlobalConstants.RESIDENT, testCaseDTO.getTestCaseName()); + + JSONObject res = new JSONObject(testCaseDTO.getOutput()); + String sendOtpResp = null, sendOtpResTemplate = null; + if (res.has(GlobalConstants.SENDOTPRESP)) { + sendOtpResp = res.get(GlobalConstants.SENDOTPRESP).toString(); + res.remove(GlobalConstants.SENDOTPRESP); + } + JSONObject sendOtpRespJson = new JSONObject(sendOtpResp); + sendOtpResTemplate = sendOtpRespJson.getString("sendOtpResTemplate"); + sendOtpRespJson.remove("sendOtpResTemplate"); + Map> ouputValidOtp = OutputValidationUtil.doJsonOutputValidation( + otpResponse.asString(), getJsonFromTemplate(sendOtpRespJson.toString(), sendOtpResTemplate), + testCaseDTO, otpResponse.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValidOtp)); + + if (!OutputValidationUtil.publishOutputResult(ouputValidOtp)) { + if (otpResponse.asString().contains("IDA-OTA-001")) + throw new AdminTestException( + "Exceeded number of OTP requests in a given time, Increase otp.request.flooding.max-count"); + else + throw new AdminTestException("Failed at otp output validation"); + } + + if (testCaseName.contains("_eotp")) { + try { + logger.info("waiting for " + properties.getProperty("expireOtpTime") + + " mili secs to test expire otp case in RESIDENT Service"); + Thread.sleep(Long.parseLong(properties.getProperty("expireOtpTime"))); + } catch (NumberFormatException | InterruptedException e) { + logger.error(e.getMessage()); + Thread.currentThread().interrupt(); + } + } + + String inputJson = getJsonFromTemplate(req.toString(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + response = postRequestWithCookieAndHeader(ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, + testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), sendEsignetToken); + Map> ouputValid = OutputValidationUtil + .doJsonOutputValidation(response.asString(), getJsonFromTemplate(res.toString(), + testCaseDTO.getOutputTemplate()), testCaseDTO, + response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + NotificationListener.markRequestRemove(); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithFormDataBodyForPdfDownload.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithFormDataBodyForPdfDownload.java new file mode 100644 index 000000000..93bbb8346 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithFormDataBodyForPdfDownload.java @@ -0,0 +1,134 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.itextpdf.text.pdf.PdfReader; +import com.itextpdf.text.pdf.parser.PdfTextExtractor; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.GlobalMethods; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class PostWithFormDataBodyForPdfDownload extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PostWithFormDataBodyForPdfDownload.class); + protected String testCaseName = ""; + public Response response = null; + public byte[] pdf=null; + public String pdfAsText =null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + logger.info("Started executing yml: "+ymlFile); + return getYmlTestData(ymlFile); + } + + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException(GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + pdf = postWithFormDataBodyForPdf(ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + PdfReader pdfReader = null; + ByteArrayInputStream bIS = null; + + try { + bIS = new ByteArrayInputStream(pdf); + pdfReader = new PdfReader(bIS); + pdfAsText = PdfTextExtractor.getTextFromPage(pdfReader, 1); + } catch (IOException e) { + Reporter.log("Exception : " + e.getMessage()); + } finally { + AdminTestUtil.closeByteArrayInputStream(bIS); + AdminTestUtil.closePdfReader(pdfReader); + } + + if (pdf != null && (new String(pdf).contains("errors") || pdfAsText == null)) { + GlobalMethods.reportResponse(null, ApplnURI + testCaseDTO.getEndPoint(), + "Not able to download issuer credential"); + if (!testCaseName.contains("_Neg")) { + throw new AdminTestException("Not able to download issuer credential"); + } + } else { + GlobalMethods.reportResponse(null, ApplnURI + testCaseDTO.getEndPoint(), pdfAsText); + } + + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsAndCookie.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsAndCookie.java new file mode 100644 index 000000000..6c999ff22 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsAndCookie.java @@ -0,0 +1,139 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class PostWithPathParamsAndCookie extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PostWithPathParamsAndCookie.class); + protected String testCaseName = ""; + public String pathParams = null; + Response response = null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + pathParams = context.getCurrentXmlTest().getLocalParameters().get("pathParams"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) + throws AuthenticationTestException, AdminTestException, SecurityXSSException { + + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS + ); + } + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + if (testCaseName.equals("Mimoto_UnlockWalletForTemporarilyLock_TemporaryLocker_IncorrectWalletPin_Neg")) { + for (int i = 0; i < MimotoConfigManager.getMaxFailedAttemptsAllowedPerCycle(); i++) { + response = postWithPathParamsBodyAndCookie( + ApplnURI + testCaseDTO.getEndPoint(), + inputJson, + COOKIENAME, + testCaseDTO.getRole(), + testCaseDTO.getTestCaseName(), + pathParams + ); + } + } else { + response = postWithPathParamsBodyAndCookie( + ApplnURI + testCaseDTO.getEndPoint(), + inputJson, + COOKIENAME, + testCaseDTO.getRole(), + testCaseDTO.getTestCaseName(), + pathParams + ); + } + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, + response.getStatusCode() + ); + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) { + throw new AdminTestException("Failed at output validation"); + } + } + + + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } + +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsHeadersAndCookieForAutoGenId.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsHeadersAndCookieForAutoGenId.java new file mode 100644 index 000000000..5e79994aa --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/PostWithPathParamsHeadersAndCookieForAutoGenId.java @@ -0,0 +1,135 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; +import org.json.JSONObject; + +public class PostWithPathParamsHeadersAndCookieForAutoGenId extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(PostWithPathParamsHeadersAndCookieForAutoGenId.class); + protected String testCaseName = ""; + public String pathParams = null; + public String headers = null; + public String idKeyName = null; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + pathParams = context.getCurrentXmlTest().getLocalParameters().get("pathParams"); + idKeyName = context.getCurrentXmlTest().getLocalParameters().get("idKeyName"); + headers = context.getCurrentXmlTest().getLocalParameters().get("headers"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + if (inputJson.contains("authorizationRequestUrl")) { + + try { + + JSONObject json = new JSONObject(inputJson); + if (json.has("authorizationRequestUrl")) { + String authUrl = json.getString("authorizationRequestUrl"); + + authUrl = authUrl.replace("&", "&").replace("%25", "%").replace("\\u0026", "&") + .replace("\\u003d", "="); + json.put("authorizationRequestUrl", authUrl); + inputJson = json.toString(); + } + } catch (Exception e) { + logger.warn("Warning: Unable to normalize authorizationRequestUrl"); + } + } + + Response response = postWithPathParamsBodyHeadersAndCookieForAutoGeneratedId( + ApplnURI + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, testCaseDTO.getRole(), + testCaseDTO.getTestCaseName(), pathParams, idKeyName, headers); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } + +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePost.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePost.java new file mode 100644 index 000000000..f8778373a --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePost.java @@ -0,0 +1,195 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.restassured.response.Response; + +public class SimplePost extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(SimplePost.class); + protected String testCaseName = ""; + public Response response = null; + public boolean sendEsignetToken = false; + public boolean auditLogCheck = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + * + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + sendEsignetToken = context.getCurrentXmlTest().getLocalParameters().containsKey("sendEsignetToken"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + * + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) throws AuthenticationTestException, AdminTestException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + auditLogCheck = testCaseDTO.isAuditLogCheck(); + String[] templateFields = testCaseDTO.getTemplateFields(); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + + String inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + if (testCaseDTO.getTemplateFields() != null && templateFields.length > 0) { + ArrayList inputtestCases = AdminTestUtil.getInputTestCase(testCaseDTO); + ArrayList outputtestcase = AdminTestUtil.getOutputTestCase(testCaseDTO); + for (int i = 0; i < languageList.size(); i++) { + response = postWithBodyAndCookie(ApplnURI + testCaseDTO.getEndPoint(), + getJsonFromTemplate(inputtestCases.get(i).toString(), testCaseDTO.getInputTemplate()), + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName()); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(outputtestcase.get(i).toString(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } + + else { + if (testCaseName.contains("ESignet_")) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + + String tempUrl = ApplnURI; + + if (testCaseDTO.getEndPoint().startsWith("$SUNBIRDBASEURL$") && testCaseName.contains("SunBirdR")) { + + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + if (MimotoConfigManager.getSunbirdBaseURL() != null && !MimotoConfigManager.getSunbirdBaseURL().isBlank()) + tempUrl = MimotoConfigManager.getSunbirdBaseURL(); + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$SUNBIRDBASEURL$", "")); + + response = postWithBodyAndCookie(tempUrl + testCaseDTO.getEndPoint(), inputJson, auditLogCheck, + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), sendEsignetToken); + + } else if (testCaseDTO.getEndPoint().startsWith("$ESIGNETMOCKBASEURL$") + && testCaseName.contains("SunBirdC")) { + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + String esignetSunBirdBaseURL = MimotoConfigManager.getEsignetSunBirdBaseURL(); + if (esignetSunBirdBaseURL != null && !esignetSunBirdBaseURL.isBlank()) + tempUrl = esignetSunBirdBaseURL; + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$ESIGNETMOCKBASEURL$", "")); + + response = postRequestWithCookieAuthHeaderAndXsrfToken(tempUrl + testCaseDTO.getEndPoint(), + inputJson, COOKIENAME, testCaseDTO.getTestCaseName()); + } + } else { + response = postWithBodyAndCookie(ApplnURI + testCaseDTO.getEndPoint(), inputJson, auditLogCheck, + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), sendEsignetToken); + } + Map> ouputValid = null; + if (testCaseName.contains("_StatusCode")) { + + OutputValidationDto customResponse = customStatusCodeResponse(String.valueOf(response.getStatusCode()), + testCaseDTO.getOutput()); + + ouputValid = new HashMap<>(); + ouputValid.put(GlobalConstants.EXPECTED_VS_ACTUAL, List.of(customResponse)); + } else { + ouputValid = OutputValidationUtil.doJsonOutputValidation(response.asString(), + getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), testCaseDTO, + response.getStatusCode()); + } + + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) { + if (response.asString().contains("IDA-OTA-001")) + throw new AdminTestException( + "Exceeded number of OTP requests in a given time, Increase otp.request.flooding.max-count"); + else + throw new AdminTestException("Failed at otp output validation"); + } + } + + } + + /** + * The method ser current test name to result + * + * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePostForAutoGenId.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePostForAutoGenId.java new file mode 100644 index 000000000..0d5b73482 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/testscripts/SimplePostForAutoGenId.java @@ -0,0 +1,238 @@ +package io.inji.testrig.apirig.mimoto.testscripts; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.testng.ITest; +import org.testng.ITestContext; +import org.testng.ITestResult; +import org.testng.Reporter; +import org.testng.SkipException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import io.inji.testrig.apirig.mimoto.utils.MimotoConfigManager; +import io.inji.testrig.apirig.mimoto.utils.MimotoUtil; +import io.mosip.testrig.apirig.dto.OutputValidationDto; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.BaseTestCase; +import io.mosip.testrig.apirig.testrunner.HealthChecker; +import io.mosip.testrig.apirig.utils.AdminTestException; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.OutputValidationUtil; +import io.mosip.testrig.apirig.utils.ReportUtil; +import io.mosip.testrig.apirig.utils.SecurityXSSException; +import io.mosip.testrig.apirig.utils.AuthenticationTestException; +import io.restassured.response.Response; + +public class SimplePostForAutoGenId extends MimotoUtil implements ITest { + private static final Logger logger = Logger.getLogger(SimplePostForAutoGenId.class); + protected String testCaseName = ""; + public String idKeyName = null; + public Response response = null; + public boolean sendEsignetToken = false; + public boolean auditLogCheck = false; + + @BeforeClass + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + /** + * get current testcaseName + */ + @Override + public String getTestName() { + return testCaseName; + } + + /** + * Data provider class provides test case list + *  + * @return object of data provider + */ + @DataProvider(name = "testcaselist") + public Object[] getTestCaseList(ITestContext context) { + String ymlFile = context.getCurrentXmlTest().getLocalParameters().get("ymlFile"); + sendEsignetToken = context.getCurrentXmlTest().getLocalParameters().containsKey("sendEsignetToken"); + idKeyName = context.getCurrentXmlTest().getLocalParameters().get("idKeyName"); + logger.info("Started executing yml: " + ymlFile); + return getYmlTestData(ymlFile); + } + + /** + * Test method for OTP Generation execution + *  + * @param objTestParameters + * @param testScenario + * @param testcaseName + * @throws AuthenticationTestException + * @throws AdminTestException + * @throws NoSuchAlgorithmException + */ + @Test(dataProvider = "testcaselist") + public void test(TestCaseDTO testCaseDTO) + throws AuthenticationTestException, AdminTestException, NoSuchAlgorithmException, SecurityXSSException { + testCaseName = testCaseDTO.getTestCaseName(); + AdminTestUtil.currentTestCaseName = testCaseDTO.getTestCaseName(); + if (HealthChecker.signalTerminateExecution) { + throw new SkipException( + GlobalConstants.TARGET_ENV_HEALTH_CHECK_FAILED + HealthChecker.healthCheckFailureMapS); + } + if (testCaseDTO.getTestCaseName().contains("VID") || testCaseDTO.getTestCaseName().contains("Vid")) { + if (!BaseTestCase.getSupportedIdTypesValueFromActuator().contains("VID") + && !BaseTestCase.getSupportedIdTypesValueFromActuator().contains("vid")) { + throw new SkipException(GlobalConstants.VID_FEATURE_NOT_SUPPORTED); + } + } + + testCaseDTO = MimotoUtil.isTestCaseValidForTheExecution(testCaseDTO); + testCaseDTO = MimotoUtil.changeContextURLByFlag(testCaseDTO); + String[] templateFields = testCaseDTO.getTemplateFields(); + String inputJson = ""; + + // FIX: Check if a template is present before calling the processor. + // If inputTemplate is empty, use the raw input JSON directly. + if (testCaseDTO.getInputTemplate() == null || testCaseDTO.getInputTemplate().isEmpty()) { + inputJson = testCaseDTO.getInput(); + } else { + inputJson = getJsonFromTemplate(testCaseDTO.getInput(), testCaseDTO.getInputTemplate()); + } + + inputJson = MimotoUtil.inputstringKeyWordHandeler(inputJson, testCaseName); + + + if (testCaseDTO.getTemplateFields() != null && templateFields.length > 0) { + ArrayList inputtestCases = AdminTestUtil.getInputTestCase(testCaseDTO); + ArrayList outputtestcase = AdminTestUtil.getOutputTestCase(testCaseDTO); + for (int i = 0; i < languageList.size(); i++) { + response = postWithBodyAndCookieForAutoGeneratedId(ApplnURI + testCaseDTO.getEndPoint(), + getJsonFromTemplate(inputtestCases.get(i).toString(), testCaseDTO.getInputTemplate()), + COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + + Map> ouputValid = OutputValidationUtil.doJsonOutputValidation( + response.asString(), + getJsonFromTemplate(outputtestcase.get(i).toString(), testCaseDTO.getOutputTemplate()), + testCaseDTO, response.getStatusCode()); + if (testCaseDTO.getTestCaseName().toLowerCase().contains("dynamic")) { + JSONObject json = new JSONObject(response.asString()); + idField = json.getJSONObject("response").get("id").toString(); + } + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + } else { + if (testCaseName.contains("ESignet_")) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.ESIGNET)) { + throw new SkipException("esignet is not deployed hence skipping the testcase"); + } + String tempUrl = MimotoConfigManager.getEsignetBaseUrl(); + if (testCaseDTO.getEndPoint().startsWith("$ESIGNETMOCKBASEURL$") + && testCaseName.contains("SunBirdC")) { + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + String esignetSunBirdBaseURL = MimotoConfigManager.getEsignetSunBirdBaseURL(); + if (esignetSunBirdBaseURL != null && !esignetSunBirdBaseURL.isBlank()) + tempUrl = esignetSunBirdBaseURL; + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$ESIGNETMOCKBASEURL$", "")); + } else if (testCaseDTO.getEndPoint().startsWith("$SUNBIRDBASEURL$") && testCaseName.contains("SunBirdR")) { + + if (MimotoConfigManager.isInServiceNotDeployedList("sunbirdrc")) + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + + if (MimotoConfigManager.getSunbirdBaseURL() != null && !MimotoConfigManager.getSunbirdBaseURL().isBlank()) + tempUrl = MimotoConfigManager.getSunbirdBaseURL(); + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$SUNBIRDBASEURL$", "")); + } + + if (testCaseName.contains("_AuthorizationCode_")) { + response = postRequestWithCookieAuthHeaderAndXsrfTokenForAutoGenId( + tempUrl + testCaseDTO.getEndPoint(), inputJson, COOKIENAME, testCaseDTO.getTestCaseName(), + idKeyName); + } else if (testCaseName.contains("SunBirdR_CreatePolicy_")) { + // Adding the while loop for try creating sunbird policy in registry for 3 times if gets failed + int currLoopCount = 0; + do { + response = postWithBodyAndBearerTokenForAutoGeneratedId(tempUrl + testCaseDTO.getEndPoint(), + inputJson, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + + if (response != null && !response.asString().contains("UNSUCCESSFUL")) { + break; + } + currLoopCount++; + } while (currLoopCount < 10); + } else { + response = postWithBodyAndBearerTokenForAutoGeneratedId(tempUrl + testCaseDTO.getEndPoint(), + inputJson, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + } + } else if (testCaseName.contains("InjiVerify_")) { + if (MimotoConfigManager.isInServiceNotDeployedList(GlobalConstants.INJIVERIFY)) { + throw new SkipException(GlobalConstants.SERVICE_NOT_DEPLOYED_MESSAGE); + } + String tempUrl = MimotoConfigManager.getInjiVerifyBaseURL(); + if (testCaseDTO.getEndPoint().startsWith("$INJIVERIFYBASEURL$")) { + + String injiVerifyBaseURL = MimotoConfigManager.getInjiVerifyBaseURL(); + if (injiVerifyBaseURL != null && !injiVerifyBaseURL.isBlank()) + tempUrl = injiVerifyBaseURL; + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("$INJIVERIFYBASEURL$", "")); + } + + // FIX: Pass an empty string ("") for the cookie name to suppress the Authorization token value + // while satisfying the non-null requirement of the utility method. + response = postWithBodyAndCookieForAutoGeneratedId(tempUrl + testCaseDTO.getEndPoint(), inputJson, + "", testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + } else if (testCaseName.contains("GoogleLoginToken_")) { + response = postWithBodyAndBearerTokenForAutoGeneratedId(ApplnURI + testCaseDTO.getEndPoint(), + inputJson, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName); + } + else { + response = postWithBodyAndCookieForAutoGeneratedId(ApplnURI + testCaseDTO.getEndPoint(), inputJson, + auditLogCheck, COOKIENAME, testCaseDTO.getRole(), testCaseDTO.getTestCaseName(), idKeyName, + sendEsignetToken); + } + + Map> ouputValid = null; + if (testCaseName.contains("_StatusCode")) { + + OutputValidationDto customResponse = customStatusCodeResponse(String.valueOf(response.getStatusCode()), + testCaseDTO.getOutput()); + + ouputValid = new HashMap<>(); + ouputValid.put(GlobalConstants.EXPECTED_VS_ACTUAL, List.of(customResponse)); + } else { + ouputValid = OutputValidationUtil.doJsonOutputValidation(response.asString(), + getJsonFromTemplate(testCaseDTO.getOutput(), testCaseDTO.getOutputTemplate()), testCaseDTO, + response.getStatusCode()); + } + Reporter.log(ReportUtil.getOutputValidationReport(ouputValid)); + if (!OutputValidationUtil.publishOutputResult(ouputValid)) + throw new AdminTestException("Failed at output validation"); + } + + } + + /** + * The method ser current test name to result + * * @param result + */ + @AfterMethod(alwaysRun = true) + public void setResultTestName(ITestResult result) { + result.setAttribute("TestCaseName", testCaseName); + } +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConfigManager.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConfigManager.java new file mode 100644 index 000000000..139c83e9f --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConfigManager.java @@ -0,0 +1,65 @@ +package io.inji.testrig.apirig.mimoto.utils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import io.inji.testrig.apirig.mimoto.testrunner.InjiTestRunner; +import io.mosip.testrig.apirig.utils.ConfigManager; + +public class MimotoConfigManager extends ConfigManager{ + private static final Logger LOGGER = Logger.getLogger(MimotoConfigManager.class); + + public static void init() { + Logger configManagerLogger = Logger.getLogger(ConfigManager.class); + configManagerLogger.setLevel(Level.WARN); + + Map moduleSpecificPropertiesMap = new HashMap<>(); + // Load scope specific properties + try { + String path = InjiTestRunner.getGlobalResourcePath() + "/config/mimoto.properties"; + Properties props = getproperties(path); + // Convert Properties to Map and add to moduleSpecificPropertiesMap + for (String key : props.stringPropertyNames()) { + moduleSpecificPropertiesMap.put(key, props.getProperty(key)); + } + } catch (Exception e) { + LOGGER.error(e.getMessage()); + } + // Add module specific properties as well. + init(moduleSpecificPropertiesMap); + } + + public static String getSunbirdBaseURL() { + return MimotoUtil.getValueFromMimotoActuator("overrides", "mosip.sunbird.url"); + } + + public static int getMaxFailedAttemptsAllowedPerCycle() { + return Integer.parseInt(MimotoUtil.getValueFromMimotoActuator("https://github.com/inji/inji-config/mimoto-default.properties", "wallet.passcode.maxFailedAttemptsAllowedPerCycle")); + } + + public static String getEsignetBaseUrl() { + String esignetBaseUrl = null; + if (getproperty("runPlugin").equals("mosipid")) { + esignetBaseUrl = "https://" + MimotoUtil.getValueFromMimotoActuator("overrides", getproperty("mosipid-identity-esignet-host")); + } else if (getproperty("runPlugin").equals("mockid")) { + esignetBaseUrl = "https://" + MimotoUtil.getValueFromMimotoActuator("overrides", getproperty("mock-identity-esignet-host")); + } + if(esignetBaseUrl != null) { + propertiesMap.put("eSignetbaseurl", esignetBaseUrl); + } + return esignetBaseUrl; + } + + public static String getEsignetSunBirdBaseURL() { + return "https://" + MimotoUtil.getValueFromMimotoActuator("overrides", getproperty("sunbirdrc-insurance-esignet-host")); + } + + public static String getInjiVerifyBaseURL() { + return "https://" + MimotoUtil.getValueFromMimotoActuator("overrides", "mosip.injiverify.host"); + } + +} diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConstants.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConstants.java new file mode 100644 index 000000000..a5cd28553 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoConstants.java @@ -0,0 +1,9 @@ +package io.inji.testrig.apirig.mimoto.utils; + +public class MimotoConstants { + + public static final String SUNBIRD_INSURANCE_AUTH_FACTOR_TYPE = "KBI"; + + public static final String SUNBIRD_INSURANCE_AUTH_FACTOR_TYPE_STRING = "sunbirdInsuranceAuthFactorType"; + +} \ No newline at end of file diff --git a/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoUtil.java b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoUtil.java new file mode 100644 index 000000000..538507b82 --- /dev/null +++ b/api-test/src/main/java/io/inji/testrig/apirig/mimoto/utils/MimotoUtil.java @@ -0,0 +1,444 @@ +package io.inji.testrig.apirig.mimoto.utils; + +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.testng.SkipException; + +import com.github.javafaker.Faker; + +import io.inji.testrig.apirig.mimoto.testrunner.InjiTestRunner; +import io.mosip.testrig.apirig.dataprovider.BiometricDataProvider; +import io.mosip.testrig.apirig.dbaccess.DBManager; +import io.mosip.testrig.apirig.dto.TestCaseDTO; +import io.mosip.testrig.apirig.testrunner.OTPListener; +import io.mosip.testrig.apirig.utils.AdminTestUtil; +import io.mosip.testrig.apirig.utils.ConfigManager; +import io.mosip.testrig.apirig.utils.GlobalConstants; +import io.mosip.testrig.apirig.utils.GlobalMethods; +import io.mosip.testrig.apirig.utils.NotificationListener; +import io.mosip.testrig.apirig.utils.RestClient; +import io.mosip.testrig.apirig.utils.SkipTestCaseHandler; +import io.restassured.response.Response; + +public class MimotoUtil extends AdminTestUtil { + + private static final Logger logger = Logger.getLogger(MimotoUtil.class); + private static String otpEnabled = "true"; + private static Faker faker = new Faker(); + private static String fullNameForSunBirdR = generateFullNameForSunBirdR(); + private static String dobForSunBirdR = generateDobForSunBirdR(); + private static String policyNumberForSunBirdR = generateRandomNumberString(9); + + public static List testCasesInRunScope = new ArrayList<>(); + + public static void setLogLevel() { + if (MimotoConfigManager.IsDebugEnabled()) + logger.setLevel(Level.ALL); + else + logger.setLevel(Level.ERROR); + } + + public static String isOTPEnabled() { + String value = getValueFromMimotoActuator("/mimoto-default.properties", "mosip.otp.download.enable").isBlank() + ? System.getenv("isOTPEnabled") + : getValueFromMimotoActuator("/mimoto-default.properties", "mosip.otp.download.enable"); + if (value != null && !(value.isBlank())) + otpEnabled = value; + logger.info("OTP Enabled value: " + otpEnabled); + return otpEnabled; + } + + public static TestCaseDTO changeContextURLByFlag(TestCaseDTO testCaseDTO) { + if (!(System.getenv("useOldContextURL") == null) && !(System.getenv("useOldContextURL").isBlank()) + && System.getenv("useOldContextURL").equalsIgnoreCase("true")) { + if (testCaseDTO.getEndPoint().contains("/v1/mimoto/")) { + testCaseDTO.setEndPoint(testCaseDTO.getEndPoint().replace("/v1/mimoto/", "/residentmobileapp/")); + } + if (testCaseDTO.getInput().contains("/v1/mimoto/")) { + testCaseDTO.setInput(testCaseDTO.getInput().replace("/v1/mimoto/", "/residentmobileapp/")); + } + } + + return testCaseDTO; + } + + public static boolean isValidJSONObject(String input) { + try { + new JSONObject(input); + return true; + } catch (JSONException e) { + return false; + } + } + + public static TestCaseDTO isTestCaseValidForTheExecution(TestCaseDTO testCaseDTO) { + String testCaseName = testCaseDTO.getTestCaseName(); + currentTestCaseName = testCaseName; + + int indexof = testCaseName.indexOf("_"); + String modifiedTestCaseName = testCaseName.substring(indexof + 1); + + addTestCaseDetailsToMap(modifiedTestCaseName, testCaseDTO.getUniqueIdentifier()); + + if (!testCasesInRunScope.isEmpty() + && testCasesInRunScope.contains(testCaseDTO.getUniqueIdentifier()) == false) { + throw new SkipException(GlobalConstants.NOT_IN_RUN_SCOPE_MESSAGE); + } + + // Handle extra workflow dependencies + if (testCaseDTO != null && testCaseDTO.getAdditionalDependencies() != null + && AdminTestUtil.generateDependency == true) { + addAdditionalDependencies(testCaseDTO); + } + + String endpoint = testCaseDTO.getEndPoint(); + String inputJson = testCaseDTO.getInput(); + + // When the captcha is enabled we cannot execute the test case as we can not generate the captcha token + if (isCaptchaEnabled() == true) { + GlobalMethods.reportCaptchaStatus(GlobalConstants.CAPTCHA_ENABLED, true); + throw new SkipException(GlobalConstants.CAPTCHA_ENABLED_MESSAGE); + } + + if (InjiTestRunner.skipAll == true) { + throw new SkipException(GlobalConstants.PRE_REQUISITE_FAILED_MESSAGE); + } + + if (isOTPEnabled().equals("false")) { + if (testCaseDTO.getEndPoint().contains(GlobalConstants.SEND_OTP_ENDPOINT) + || testCaseDTO.getInput().contains(GlobalConstants.SEND_OTP_ENDPOINT) + || testCaseName.startsWith(GlobalConstants.MIMOTO_CREDENTIAL_STATUS) + || (testCaseName.startsWith("Mimoto_Generate_") && endpoint.contains("/v1/mimoto/vid"))) { + throw new SkipException(GlobalConstants.OTP_FEATURE_NOT_SUPPORTED); + } + + if (inputJson.contains("_vid$")) { + inputJson = inputJson.replace("_vid$", "_VID$"); + testCaseDTO.setInput(inputJson); + } + } + if (isOTPEnabled().equals("true") && endpoint.contains("/idrepository/v1/vid")) { + throw new SkipException(GlobalConstants.FEATURE_NOT_SUPPORTED_MESSAGE); + } + + if (SkipTestCaseHandler.isTestCaseInSkippedList(testCaseName)) { + throw new SkipException(GlobalConstants.KNOWN_ISSUES); + } + return testCaseDTO; + } + + public static void dbCleanUp() { + DBManager.executeDBQueries(MimotoConfigManager.getKMDbUrl(), MimotoConfigManager.getKMDbUser(), + MimotoConfigManager.getKMDbPass(), MimotoConfigManager.getKMDbSchema(), + getGlobalResourcePath() + "/" + "config/keyManagerCertDataDeleteQueries.txt"); + DBManager.executeDBQueries(MimotoConfigManager.getIdaDbUrl(), MimotoConfigManager.getIdaDbUser(), + MimotoConfigManager.getPMSDbPass(), MimotoConfigManager.getIdaDbSchema(), + getGlobalResourcePath() + "/" + "config/idaCertDataDeleteQueries.txt"); + DBManager.executeDBQueries(MimotoConfigManager.getMASTERDbUrl(), MimotoConfigManager.getMasterDbUser(), + MimotoConfigManager.getMasterDbPass(), MimotoConfigManager.getMasterDbSchema(), + getGlobalResourcePath() + "/" + "config/masterDataCertDataDeleteQueries.txt"); + } + + public static String getOTPFromSMTP(String inputJson, TestCaseDTO testCaseDTO) { + String testCaseName = testCaseDTO.getTestCaseName(); + JSONObject request = new JSONObject(inputJson); + String emailId = null; + String otp = null; + + if (testCaseName.contains("ESignet_AuthenticateUser") && request.has(GlobalConstants.REQUEST)) { + if (request.getJSONObject(GlobalConstants.REQUEST).has(GlobalConstants.CHALLENGELIST)) { + if (request.getJSONObject(GlobalConstants.REQUEST).getJSONArray(GlobalConstants.CHALLENGELIST) + .length() > 0) { + if (request.getJSONObject(GlobalConstants.REQUEST).getJSONArray(GlobalConstants.CHALLENGELIST) + .getJSONObject(0).has(GlobalConstants.CHALLENGE)) { + if (request.getJSONObject(GlobalConstants.REQUEST).getJSONArray(GlobalConstants.CHALLENGELIST) + .getJSONObject(0).getString(GlobalConstants.CHALLENGE) + .endsWith(GlobalConstants.MAILINATOR_COM) + || request.getJSONObject(GlobalConstants.REQUEST) + .getJSONArray(GlobalConstants.CHALLENGELIST).getJSONObject(0) + .getString(GlobalConstants.CHALLENGE).endsWith(GlobalConstants.MOSIP_NET) + || request.getJSONObject(GlobalConstants.REQUEST) + .getJSONArray(GlobalConstants.CHALLENGELIST).getJSONObject(0) + .getString(GlobalConstants.CHALLENGE).endsWith(GlobalConstants.OTP_AS_PHONE)) { + emailId = request.getJSONObject(GlobalConstants.REQUEST) + .getJSONArray(GlobalConstants.CHALLENGELIST).getJSONObject(0) + .getString(GlobalConstants.CHALLENGE); + if (emailId.endsWith(GlobalConstants.OTP_AS_PHONE)) { + emailId = emailId.replace(GlobalConstants.OTP_AS_PHONE, ""); + emailId = removeLeadingPlusSigns(emailId); + } + logger.info(emailId); + otp = NotificationListener.getOtp(emailId); + request.getJSONObject(GlobalConstants.REQUEST).getJSONArray(GlobalConstants.CHALLENGELIST) + .getJSONObject(0).put(GlobalConstants.CHALLENGE, otp); + inputJson = request.toString(); + return inputJson; + } + } + } + } + } + + return inputJson; + } + + public static String inputstringKeyWordHandeler(String jsonString, String testCaseName) { + if (jsonString.contains("$ID:")) { + jsonString = replaceIdWithAutogeneratedId(jsonString, "$ID:"); + } + + if (jsonString.contains(GlobalConstants.TIMESTAMP)) { + jsonString = replaceKeywordValue(jsonString, GlobalConstants.TIMESTAMP, generateCurrentUTCTimeStamp()); + } + + if (jsonString.contains("$UNIQUENONCEVALUEFORESIGNET$")) { + jsonString = replaceKeywordValue(jsonString, "$UNIQUENONCEVALUEFORESIGNET$", + String.valueOf(Calendar.getInstance().getTimeInMillis())); + } + + if (jsonString.contains("$SUNBIRDINSURANCEAUTHFACTORTYPE$")) { + String authFactorType = MimotoConfigManager + .getproperty(MimotoConstants.SUNBIRD_INSURANCE_AUTH_FACTOR_TYPE_STRING); + + String valueToReplace = (authFactorType != null && !authFactorType.isBlank()) ? authFactorType + : MimotoConstants.SUNBIRD_INSURANCE_AUTH_FACTOR_TYPE; + + jsonString = replaceKeywordValue(jsonString, "$SUNBIRDINSURANCEAUTHFACTORTYPE$", valueToReplace); + + } + + if (jsonString.contains("$GOOGLE_IDT_TOKEN$")) { + jsonString = replaceKeywordValue(jsonString, "$GOOGLE_IDT_TOKEN$", getGoogleIdToken()); + } + + if (jsonString.contains("$POLICYNUMBERFORSUNBIRDRC$")) { + jsonString = replaceKeywordValue(jsonString, "$POLICYNUMBERFORSUNBIRDRC$", policyNumberForSunBirdR); + } + + if (jsonString.contains("$FULLNAMEFORSUNBIRDRC$")) { + jsonString = replaceKeywordValue(jsonString, "$FULLNAMEFORSUNBIRDRC$", fullNameForSunBirdR); + } + + if (jsonString.contains("$DOBFORSUNBIRDRC$")) { + jsonString = replaceKeywordValue(jsonString, "$DOBFORSUNBIRDRC$", dobForSunBirdR); + } + + if (jsonString.contains("$CHALLENGEVALUEFORSUNBIRDC$")) { + + HashMap mapForChallenge = new HashMap(); + mapForChallenge.put(GlobalConstants.FULLNAME, fullNameForSunBirdR); + mapForChallenge.put(GlobalConstants.DOB, dobForSunBirdR); + + String challenge = gson.toJson(mapForChallenge); + + String challengeValue = BiometricDataProvider.toBase64Url(challenge); + + jsonString = replaceKeywordValue(jsonString, "$CHALLENGEVALUEFORSUNBIRDC$", challengeValue); + } + + if (jsonString.contains("$PUBLICKEYFORBINDING$")) { + jsonString = replaceKeywordValue(jsonString, "$PUBLICKEYFORBINDING$", + generatePublicKeyForMimoto()); + } + + if (jsonString.contains("$INJIREDIRECTURI$")) { + jsonString = replaceKeywordValue(jsonString, "$INJIREDIRECTURI$", + ApplnURI.replace(GlobalConstants.API_INTERNAL, "injiweb") + "/redirect"); + } + if (jsonString.contains("$AUTHORIZATION_REQUEST_URL$")) { + jsonString = replaceKeywordValue(jsonString, "$AUTHORIZATION_REQUEST_URL$", + getAuthorizationRequestUrlMock()); + + } + if (jsonString.contains("$CLIENT_ID_INJI_VERIFY$")) { + jsonString = replaceKeywordValue(jsonString, "$CLIENT_ID_INJI_VERIFY$", getClientIdForInjiVerify()); + } + if (jsonString.contains("$GETCLIENTIDFORMOSIPIDFROMMIMOTOACTUATOR$")) { + String clientIdSection = MimotoConfigManager.getproperty("mimoto-oidc-mosipid-partner-clientid"); + jsonString = replaceKeywordWithValue(jsonString, "$GETCLIENTIDFORMOSIPIDFROMMIMOTOACTUATOR$", + getValueFromMimotoActuator("overrides", clientIdSection)); + } else if (jsonString.contains("$GETCLIENTIDFORINSURANCEFROMMIMOTOACTUATOR$")) { + String clientIdSection = MimotoConfigManager.getproperty("mimoto-oidc-sunbird-partner-clientid"); + jsonString = replaceKeywordWithValue(jsonString, "$GETCLIENTIDFORINSURANCEFROMMIMOTOACTUATOR$", + getValueFromMimotoActuator("overrides", clientIdSection)); + } + + return jsonString; + + } + + public static String replaceKeywordValue(String jsonString, String keyword, String value) { + if (value != null && !value.isEmpty()) + return jsonString.replace(keyword, value); + else { + if (keyword.contains("$ID:")) + throw new SkipException("Marking testcase as skipped as required field is empty " + keyword + + " please check the results of testcase: " + getTestCaseIDFromKeyword(keyword)); + else + throw new SkipException("Marking testcase as skipped as required field is empty " + keyword); + + } + } + + public static String generatePublicKeyForMimoto() { + + String vcString = ""; + try { + KeyPairGenerator keyPairGenerator = getKeyPairGeneratorInstance(); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + StringWriter stringWriter = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + vcString = stringWriter.toString(); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + vcString = vcString.replaceAll("\r\n", "\\\\n"); + } else { + vcString = vcString.replaceAll("\n", "\\\\n"); + } + } catch (Exception e) { + throw e; + } + } catch (Exception e) { + logger.error(e.getMessage()); + } + return vcString; + } + + public static String generateFullNameForSunBirdR() { + return faker.name().fullName(); + } + + public static String generateDobForSunBirdR() { + Faker faker = new Faker(); + LocalDate dob = faker.date().birthday().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return dob.format(formatter); + } + + public static JSONArray mimotoActuatorResponseArray = null; + + public static String getValueFromMimotoActuator(String section, String key) { + String url = ApplnURI + ConfigManager.getproperty("actuatorMimotoEndpoint"); + if (!(System.getenv("useOldContextURL") == null) && !(System.getenv("useOldContextURL").isBlank()) + && System.getenv("useOldContextURL").equalsIgnoreCase("true")) { + if (url.contains("/v1/mimoto/")) { + url = url.replace("/v1/mimoto/", "/residentmobileapp/"); + } + } + String actuatorCacheKey = url + section + key; + String value = actuatorValueCache.get(actuatorCacheKey); + if (value != null && !value.isEmpty()) + return value; + + try { + if (mimotoActuatorResponseArray == null) { + Response response = null; + JSONObject responseJson = null; + response = RestClient.getRequest(url, MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON); + + responseJson = new JSONObject(response.getBody().asString()); + mimotoActuatorResponseArray = responseJson.getJSONArray("propertySources"); + } + for (int i = 0, size = mimotoActuatorResponseArray.length(); i < size; i++) { + JSONObject eachJson = mimotoActuatorResponseArray.getJSONObject(i); + if (eachJson.get("name").toString().contains(section)) { + value = eachJson.getJSONObject(GlobalConstants.PROPERTIES).getJSONObject(key) + .get(GlobalConstants.VALUE).toString(); + if (ConfigManager.IsDebugEnabled()) + logger.info("Actuator: " + url + " key: " + key + " value: " + value); + break; + } + } + actuatorValueCache.put(actuatorCacheKey, value); + + return value; + } catch (Exception e) { + logger.error(GlobalConstants.EXCEPTION_STRING_2 + e); + logger.error("Unable to fetch the value from the actuator. URL = " + url + " section = " + section + " key " + + key); + return ""; + } + + } + + private static String getGoogleIdToken() { + String idToken = null; + + Map requestMap = new HashMap<>(); + requestMap.put("clientId", MimotoConfigManager.getproperty("google.client.id")); + requestMap.put("clientSecret", MimotoConfigManager.getproperty("google.client.secret")); + requestMap.put("refreshToken", MimotoConfigManager.getproperty("google.refresh.token")); + requestMap.put("grant_type", "refresh_token"); + String url = props.getProperty("googleIdToken"); + + Response response = RestClient.postRequestWithFormDataBody(url, requestMap); + + if (response.getStatusCode() != 200) { + String errorResponse = response.getBody().toString(); + throw new RuntimeException("Failed to get ID token. HTTP status code: " + response.getStatusCode() + + ", response body: " + errorResponse); + } + + JSONObject jsonObject = new JSONObject(response.getBody().asString()); + + if (jsonObject != null) { + idToken = jsonObject.get("id_token").toString(); + } + + if (idToken == null || idToken.isEmpty()) { + throw new RuntimeException("id_token not found in response: " + response); + } + + logger.info("Obtained id_token: " + idToken); // Debug log + return idToken; + + } + + private static String extractEnvironmentName() { + final String startMarker = "api-internal."; + final String endMarker = ".mosip.net"; + + int startIndex = ApplnURI.indexOf(startMarker); + int endIndex = ApplnURI.indexOf(endMarker); + + if (startIndex == -1 || endIndex == -1 || startIndex >= endIndex) { + throw new IllegalArgumentException( + "Failed to extract environment name from ApplnURI: " + ApplnURI + ); + } + + startIndex += startMarker.length(); + return ApplnURI.substring(startIndex, endIndex); + } + + private static String getClientIdForInjiVerify() { + String env_name = extractEnvironmentName(); + return "did:web:injiverify." + env_name + ".mosip.net:v1:verify"; + } + + private static String getAuthorizationRequestUrlMock() { + String env_name = extractEnvironmentName(); + return "openid4vp://authorize?client_id=" + getClientIdForInjiVerify() + + "&request_uri=" + "https://injiverify." + env_name + ".mosip.net/v1/verify/vp-request/"; + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/OtpGeneration/request.json b/api-test/src/main/resources/config/Authorization/OtpGeneration/request.json new file mode 100644 index 000000000..7ce05cc51 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/OtpGeneration/request.json @@ -0,0 +1,16 @@ +{ + "id": "string", + "metadata": {}, + "request": { + "appId": "prereg", + "context": "auth-otp", + "otpChannel": [ + "EMAIL" + ], + "templateVariables": {}, + "userId": "robin.hood@mailinator.com", + "useridtype": "USERID" + }, + "requesttime": "2018-12-10T06:12:52.994Z", + "version": "string" +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/OtpValidation/request.json b/api-test/src/main/resources/config/Authorization/OtpValidation/request.json new file mode 100644 index 000000000..800b94ad6 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/OtpValidation/request.json @@ -0,0 +1,11 @@ +{ + "id": "string", + "metadata": {}, + "request": { + "appId": "prereg", + "otp": "837439", + "userId": "9972388747" + }, + "requesttime": "2018-12-10T06:12:52.994Z", + "version": "string" +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/errorUIN.json b/api-test/src/main/resources/config/Authorization/errorUIN.json new file mode 100644 index 000000000..f54f8c7a8 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/errorUIN.json @@ -0,0 +1,17 @@ +{ + "transactionID": "$IGNORE$", + "version": "$IGNORE$", + "id": "$IGNORE$", + "errors": [ + { + "errorCode": "IDA-MLC-015", + "errorMessage": "$IGNORE$", + "actionMessage": "$IGNORE$" + } + ], + "responseTime": "$IGNORE$", + "response": { + "authStatus": false, + "authToken": "$IGNORE$" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/internalAuthRequest.json b/api-test/src/main/resources/config/Authorization/internalAuthRequest.json new file mode 100644 index 000000000..57aa8f2f6 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/internalAuthRequest.json @@ -0,0 +1,13 @@ +{ + "id": "string", + "version": "string", + "requesttime": "2022-01-13T06:07:20.554Z", + "metadata": {}, + "request": { + "userName": "110005", + "password": "mosip", + "appId": "admin", + "clientId": "mosip-admin-client", + "clientSecret": "xyz123" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/keycloakTokenGeneration.json b/api-test/src/main/resources/config/Authorization/keycloakTokenGeneration.json new file mode 100644 index 000000000..cb8ffb631 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/keycloakTokenGeneration.json @@ -0,0 +1,6 @@ +{ +"grant_type" : "password", +"client_id": "admin-cli", +"username": "keycloakusername", +"password": "keycloakpassword" +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/request.json b/api-test/src/main/resources/config/Authorization/request.json new file mode 100644 index 000000000..57831153b --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/request.json @@ -0,0 +1,11 @@ +{ + "id": "string", + "metadata": {}, + "request": { + "appId": "prereg", + "password": "prereguser", + "userName": "prereguser" + }, + "requesttime": "2019-04-10T10:00:00.000Z", + "version": "string" +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/requestIdentity.json b/api-test/src/main/resources/config/Authorization/requestIdentity.json new file mode 100644 index 000000000..4c6b94e24 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/requestIdentity.json @@ -0,0 +1,12 @@ +{ + "identity": { + "addressLine3": [ + { + "language": "languageValue", + "value": "valueOfAttribute" + } + ], + "IDSchemaVersion": "IDSchemaVersion", + "UIN": "$ID:AddIdentity_Positive_PRE_smoke_UIN$" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/Authorization/zoneMappingRequest.json b/api-test/src/main/resources/config/Authorization/zoneMappingRequest.json new file mode 100644 index 000000000..c7e5fbce5 --- /dev/null +++ b/api-test/src/main/resources/config/Authorization/zoneMappingRequest.json @@ -0,0 +1,12 @@ +{ + "id": "string", + "metadata": {}, + "request": { + "zoneCode": "CSB", + "userId": "110123", + "isActive": true, + "langCode": "eng" + }, + "requesttime": "2022-05-09T09:52:11.969Z", + "version": "string" +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/application.properties b/api-test/src/main/resources/config/application.properties new file mode 100644 index 000000000..305e4ef5d --- /dev/null +++ b/api-test/src/main/resources/config/application.properties @@ -0,0 +1,31 @@ +## End point(s) relative URLs +encryptionPath=v1/identity/encrypt?isInternal=false +internalEncryptionPath=v1/identity/encrypt?isInternal=true +encodePath=v1/identity/encode +decodePath=v1/identity/decode +decryptPath=/idauthentication/v1/internal/decrypt +encodeFilePath=v1/identity/encodeFile +decodeFilePath=v1/identity/decodeFile/?fileName=cbeff +validateSignaturePath=v1/identity/validateSign?signature=$signature$ +splitEncryptedData=v1/identity/splitEncryptedData +putPartnerRegistrationUrl=/v1/partnermanager/partners +uploadCACertificateUrl=/v1/partnermanager/partners/certificate/ca/upload +uploadIntermediateCertificateUrl=/v1/partnermanager/partners/certificate/ca/upload +uploadPartnerCertificateUrl=/v1/partnermanager/partners/certificate/upload +masterSchemaURL=/v1/masterdata/idschema/latest +googleIdToken=https://oauth2.googleapis.com/token + +#partner certificate refId, used for getting the partner certificate +uinGenMaxLoopCount=20 +uinGenDelayTime=10000 +Delaytime=90000 +expireOtpTime=180000 + +## As these will be based on regEx, move these out from proprties file +passwordForAddIdentity=12341234_Aa +passwordToReset=12341234_AaB + +## Need to revisit these propeties +XSRFTOKEN=7d01b2a8-b89d-41ad-9361-d7f6294021d1 +codeChallenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM +codeVerifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk \ No newline at end of file diff --git a/api-test/src/main/resources/config/healthCheckEndpoint.properties b/api-test/src/main/resources/config/healthCheckEndpoint.properties new file mode 100644 index 000000000..48f7e63a3 --- /dev/null +++ b/api-test/src/main/resources/config/healthCheckEndpoint.properties @@ -0,0 +1,44 @@ +###### = +regproc_masterdata=/v1/hotlist/actuator/health +masterdata=/v1/admin/actuator/health +auth_idrepo_resident_regproc_masterdata=/v1/auditmanager/actuator/health +auth_idrepo_resident_regproc_masterdata=/v1/authmanager/actuator/health +auth_idrepo_regproc=/biosdk-service/actuator/health +auth_mobileid=/v1/credentialservice/actuator/health +auth_mobileid=/v1/credentialrequest/actuator/health +auth_idrepo_regproc_partner=/v1/datashare/actuator/health +auth_mobileid_esignet=/idauthentication/v1/actuator/health +auth_mobileid_esignet_partner=/idauthentication/v1/internal/actuator/health +auth_mobileid_esignet=/idauthentication/v1/otp/actuator/health +idrepo_auth_mobileid_mimoto_esignet_resident_regproc=/idrepository/v1/identity/actuator/health +idrepo_regproc=/v1/idgenerator/actuator/health +idrepo_resident_regproc_masterdata=/v1/keymanager/actuator/health +auth_idrepo_masterdata_mobileid_mimoto_esignet_resident_partner_prereg_regproc=/v1/masterdata/actuator/health +auth_idrepo_resident_regproc=/v1/notifier/actuator/health +auth_mobileid=/v1/otpmanager/actuator/health +partner_auth_esignet_idrepo_resident_regproc=/v1/partnermanager/actuator/health +partner_auth_esignet_regproc=/v1/policymanager/actuator/health +prereg=/v1/pridgenerator/actuator/health +resident_regproc=/registrationprocessor/v1/packetreceiver/actuator/health +regproc_masterdata=/registrationprocessor/v1/registrationstatus/actuator/health +resident_auth_esignet_mobileid=/resident/v1/actuator/health +# TO DO idrepo_auth_mobileid_mimoto_esignet_resident=/v1/ridgenerator/actuator/health +regproc_masterdata=/v1/syncdata/actuator/health +idrepo_auth_mobileid_mimoto_esignet_resident_regproc=/idrepository/v1/actuator/health +auth_idrepo_resident_regproc=/hub/actuator/health +resident_esignet=/v1/esignet/actuator/health + +#The below actuators are not used for functional test rigs. +#regproc=/v1/identity/actuator/health +#regproc=/registrationprocessor/v1/registrationtransaction/actuator/health +#regproc=/registrationprocessor/v1/workflowmanager/actuator/health +#regproc=/registrationprocessor/v1/landingzone/actuator/health +#regproc=/registrationprocessor/v1/notification/actuator/health +#regproc=/registrationprocessor/v1/opencrvs-stage/actuator/health +#regproc=/registrationprocessor/v1/reprocessor/actuator/health +#regproc=/v1/print/actuator/health +#regproc=/registrationprocessor/v1/camelbridge/actuator/health +#regproc=/v1/packetcreator/actuator/health +#regproc=/commons/v1/packetmanager/actuator/health +#regproc=/v1/mock-abis-service/actuator/health +#regproc=/v1/mockmv/actuator/health \ No newline at end of file diff --git a/api-test/src/main/resources/config/idaCertDataDeleteQueries.txt b/api-test/src/main/resources/config/idaCertDataDeleteQueries.txt new file mode 100644 index 000000000..0b2c2e70b --- /dev/null +++ b/api-test/src/main/resources/config/idaCertDataDeleteQueries.txt @@ -0,0 +1,8 @@ +##### DB queries to be executed to tear down the data used and generated during the test rig run + +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_pid%' +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ekyc_pid%' +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_device_pid%' +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ftm_pid%' +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforautomationesi%' +delete from ida.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforesignet%' \ No newline at end of file diff --git a/api-test/src/main/resources/config/keyManagerCertDataDeleteQueries.txt b/api-test/src/main/resources/config/keyManagerCertDataDeleteQueries.txt new file mode 100644 index 000000000..a89dfaf9e --- /dev/null +++ b/api-test/src/main/resources/config/keyManagerCertDataDeleteQueries.txt @@ -0,0 +1,14 @@ +##### DB queries to be executed to tear down the data used and generated during the test rig run + +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_pid%' +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ekyc_pid%' +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_device_pid%' +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ftm_pid%' +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforautomationesi%' +delete from mosip_keymgr.keymgr.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforesignet%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=${currentModule}_pid%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ekyc_pid%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=${currentModule}_device_pid%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ftm_pid%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=partnernameforautomationesi%' +delete from mosip_keymgr.keymgr.partner_cert_store WHERE cert_subject LIKE '%O=partnernameforesignet%' \ No newline at end of file diff --git a/api-test/src/main/resources/config/lang-isocode-transid.csv b/api-test/src/main/resources/config/lang-isocode-transid.csv new file mode 100644 index 000000000..7a00fd36e --- /dev/null +++ b/api-test/src/main/resources/config/lang-isocode-transid.csv @@ -0,0 +1,550 @@ +ISO 639-2 Code,English Name,English-to-Language-TransID +aar,Afar,Any-ar +abk,Abkhazian, +ace,Achinese, +ach,Acoli, +ada,Adangme, +ady,Adyghe; Adygei, +afa,Afro-Asiatic languages, +afh,Afrihili, +afr,Afrikaans, +ain,Ainu, +aka,Akan, +akk,Akkadian, +alb (B),Albanian, +sqi (T),, +ale,Aleut, +alg,Algonquian languages, +alt,Southern Altai, +amh,Amharic, +ang,"English, Old (ca.450-1100)", +anp,Angika, +apa,Apache languages, +ara,Arabic,Any-Arab +arc,Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE), +arg,Aragonese, +arm (B),Armenian, +hye (T),, +arn,Mapudungun; Mapuche, +arp,Arapaho, +art,Artificial languages, +arw,Arawak, +asm,Assamese, +ast,Asturian; Bable; Leonese; Asturleonese, +ath,Athapascan languages, +aus,Australian languages, +ava,Avaric, +ave,Avestan, +awa,Awadhi, +aym,Aymara, +aze,Azerbaijani, +bad,Banda languages, +bai,Bamileke languages, +bak,Bashkir, +bal,Baluchi, +bam,Bambara, +ban,Balinese, +baq (B),Basque, +eus (T),, +bas,Basa, +bat,Baltic languages, +bej,Beja; Bedawiyet, +bel,Belarusian, +bem,Bemba, +ben,Bengali, +ber,Berber languages, +bho,Bhojpuri, +bih,Bihari languages, +bik,Bikol, +bin,Bini; Edo, +bis,Bislama, +bla,Siksika, +bnt,Bantu languages, +tib (B),Tibetan, +bod (T),, +bos,Bosnian, +bra,Braj, +bre,Breton, +btk,Batak languages, +bua,Buriat, +bug,Buginese, +bul,Bulgarian, +bur (B),Burmese, +mya (T),, +byn,Blin; Bilin, +cad,Caddo, +cai,Central American Indian languages, +car,Galibi Carib, +cat,Catalan; Valencian, +cau,Caucasian languages, +ceb,Cebuano, +cel,Celtic languages, +cze (B),Czech, +ces (T),, +cha,Chamorro, +chb,Chibcha, +che,Chechen, +chg,Chagatai, +chi (B),Chinese, +zho (T),, +chk,Chuukese, +chm,Mari, +chn,Chinook jargon, +cho,Choctaw, +chp,Chipewyan; Dene Suline, +chr,Cherokee, +chu,Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic, +chv,Chuvash, +chy,Cheyenne, +cmc,Chamic languages, +cnr,Montenegrin, +cop,Coptic, +cor,Cornish, +cos,Corsican, +cpe,"Creoles and pidgins, English based", +cpf,"Creoles and pidgins, French-based", +cpp,"Creoles and pidgins, Portuguese-based", +cre,Cree, +crh,Crimean Tatar; Crimean Turkish, +crp,Creoles and pidgins, +csb,Kashubian, +cus,Cushitic languages, +wel (B),Welsh, +cym (T),, +cze (B),Czech, +ces (T),, +dak,Dakota, +dan,Danish, +dar,Dargwa, +day,Land Dayak languages, +del,Delaware, +den,Slave (Athapascan), +ger (B),German, +deu (T),, +dgr,Dogrib, +din,Dinka, +div,Divehi; Dhivehi; Maldivian, +doi,Dogri, +dra,Dravidian languages, +dsb,Lower Sorbian, +dua,Duala, +dum,"Dutch, Middle (ca.1050-1350)", +dut (B),Dutch; Flemish, +nld (T),, +dyu,Dyula, +dzo,Dzongkha, +efi,Efik, +egy,Egyptian (Ancient), +eka,Ekajuk, +gre (B),"Greek, Modern (1453-)", +ell (T),, +elx,Elamite, +eng,English,eng, +enm,"English, Middle (1100-1500)", +epo,Esperanto, +est,Estonian, +baq (B),Basque, +eus (T),, +ewe,Ewe, +ewo,Ewondo, +fan,Fang,Any-fa +fao,Faroese, +per (B),Persian, +fas (T),, +fat,Fanti, +fij,Fijian, +fil,Filipino; Pilipino, +fin,Finnish, +fiu,Finno-Ugrian languages, +fon,Fon, +fre (B),French, +fra (T),, +fre (B),French, +fra (T),, +frm,"French, Middle (ca.1400-1600)", +fro,"French, Old (842-ca.1400)", +frr,Northern Frisian, +frs,Eastern Frisian, +fry,Western Frisian, +ful,Fulah, +fur,Friulian, +gaa,Ga, +gay,Gayo, +gba,Gbaya, +gem,Germanic languages, +geo (B),Georgian, +kat (T),, +ger (B),German, +deu (T),, +gez,Geez, +gil,Gilbertese, +gla,Gaelic; Scottish Gaelic, +gle,Irish, +glg,Galician, +glv,Manx, +gmh,"German, Middle High (ca.1050-1500)", +goh,"German, Old High (ca.750-1050)", +gon,Gondi, +gor,Gorontalo, +got,Gothic, +grb,Grebo, +grc,"Greek, Ancient (to 1453)", +gre (B),"Greek, Modern (1453-)", +ell (T),, +grn,Guarani, +gsw,Swiss German; Alemannic; Alsatian, +guj,Gujarati, +gwi,Gwich'in, +hai,Haida, +hat,Haitian; Haitian Creole, +hau,Hausa, +haw,Hawaiian, +heb,Hebrew,Any-Hebrew +her,Herero, +hil,Hiligaynon, +him,Himachali languages; Western Pahari languages, +hin,Hindi,Any-Devanagari +kan,Kannada,Any-Kannada +tam,Tamil,Any-Tamil +hit,Hittite, +hmn,Hmong; Mong, +hmo,Hiri Motu, +hrv,Croatian, +hsb,Upper Sorbian, +hun,Hungarian, +hup,Hupa, +arm (B),Armenian, +hye (T),, +iba,Iban, +ibo,Igbo, +ice (B),Icelandic, +isl (T),, +ido,Ido, +iii,Sichuan Yi; Nuosu, +ijo,Ijo languages, +iku,Inuktitut, +ile,Interlingue; Occidental, +ilo,Iloko, +ina,Interlingua (International Auxiliary Language Association), +inc,Indic languages, +ind,Indonesian, +ine,Indo-European languages, +inh,Ingush, +ipk,Inupiaq, +ira,Iranian languages, +iro,Iroquoian languages, +ice (B),Icelandic, +isl (T),, +ita,Italian, +jav,Javanese, +jbo,Lojban, +jpn,Japanese, +jpr,Judeo-Persian, +jrb,Judeo-Arabic, +kaa,Kara-Kalpak, +kab,Kabyle, +kac,Kachin; Jingpho, +kal,Kalaallisut; Greenlandic, +kam,Kamba, +kan,Kannada, +kar,Karen languages, +kas,Kashmiri, +geo (B),Georgian, +kat (T),, +kau,Kanuri, +kaw,Kawi, +kaz,Kazakh, +kbd,Kabardian, +kha,Khasi, +khi,Khoisan languages, +khm,Central Khmer, +kho,Khotanese; Sakan, +kik,Kikuyu; Gikuyu, +kin,Kinyarwanda, +kir,Kirghiz; Kyrgyz, +kmb,Kimbundu, +kok,Konkani, +kom,Komi, +kon,Kongo, +kor,Korean, +kos,Kosraean, +kpe,Kpelle, +krc,Karachay-Balkar, +krl,Karelian, +kro,Kru languages, +kru,Kurukh, +kua,Kuanyama; Kwanyama, +kum,Kumyk, +kur,Kurdish, +kut,Kutenai, +lad,Ladino, +lah,Lahnda, +lam,Lamba, +lao,Lao, +lat,Latin,Any-Latin +lav,Latvian, +lez,Lezghian, +lim,Limburgan; Limburger; Limburgish, +lin,Lingala, +lit,Lithuanian, +lol,Mongo, +loz,Lozi, +ltz,Luxembourgish; Letzeburgesch, +lua,Luba-Lulua, +lub,Luba-Katanga, +lug,Ganda, +lui,Luiseno, +lun,Lunda, +luo,Luo (Kenya and Tanzania), +lus,Lushai, +mac (B),Macedonian, +mkd (T),, +mad,Madurese, +mag,Magahi, +mah,Marshallese, +mai,Maithili, +mak,Makasar, +mal,Malayalam, +man,Mandingo, +mao (B),Maori, +mri (T),, +map,Austronesian languages, +mar,Marathi, +mas,Masai, +may (B),Malay, +msa (T),, +mdf,Moksha, +mdr,Mandar, +men,Mende, +mga,"Irish, Middle (900-1200)", +mic,Mi'kmaq; Micmac, +min,Minangkabau, +mis,Uncoded languages, +mac (B),Macedonian, +mkd (T),, +mkh,Mon-Khmer languages, +mlg,Malagasy, +mlt,Maltese, +mnc,Manchu, +mni,Manipuri, +mno,Manobo languages, +moh,Mohawk, +mon,Mongolian, +mos,Mossi, +mao (B),Maori, +mri (T),, +may (B),Malay, +msa (T),, +mul,Multiple languages, +mun,Munda languages, +mus,Creek, +mwl,Mirandese, +mwr,Marwari, +bur (B),Burmese, +mya (T),, +myn,Mayan languages, +myv,Erzya, +nah,Nahuatl languages, +nai,North American Indian languages, +nap,Neapolitan, +nau,Nauru, +nav,Navajo; Navaho, +nbl,"Ndebele, South; South Ndebele", +nde,"Ndebele, North; North Ndebele", +ndo,Ndonga, +nds,"Low German; Low Saxon; German, Low; Saxon, Low", +nep,Nepali, +new,Nepal Bhasa; Newari, +nia,Nias, +nic,Niger-Kordofanian languages, +niu,Niuean, +dut (B),Dutch; Flemish, +nld (T),, +nno,"Norwegian Nynorsk; Nynorsk, Norwegian", +nob,"Bokmål, Norwegian; Norwegian Bokmål", +nog,Nogai, +non,"Norse, Old", +nor,Norwegian, +nqo,N'Ko, +nso,Pedi; Sepedi; Northern Sotho, +nub,Nubian languages, +nwc,Classical Newari; Old Newari; Classical Nepal Bhasa, +nya,Chichewa; Chewa; Nyanja, +nym,Nyamwezi, +nyn,Nyankole, +nyo,Nyoro, +nzi,Nzima, +oci,Occitan (post 1500), +oji,Ojibwa, +ori,Oriya, +orm,Oromo, +osa,Osage, +oss,Ossetian; Ossetic, +ota,"Turkish, Ottoman (1500-1928)", +oto,Otomian languages, +paa,Papuan languages, +pag,Pangasinan, +pal,Pahlavi, +pam,Pampanga; Kapampangan, +pan,Panjabi; Punjabi, +pap,Papiamento, +pau,Palauan, +peo,"Persian, Old (ca.600-400 B.C.)", +per (B),Persian, +fas (T),, +phi,Philippine languages, +phn,Phoenician, +pli,Pali, +pol,Polish, +pon,Pohnpeian, +por,Portuguese, +pra,Prakrit languages, +pro,"Provençal, Old (to 1500);Occitan, Old (to 1500)", +pus,Pushto; Pashto, +qaa-qtz,Reserved for local use, +que,Quechua, +raj,Rajasthani, +rap,Rapanui, +rar,Rarotongan; Cook Islands Maori, +roa,Romance languages, +roh,Romansh, +rom,Romany, +rum (B),Romanian; Moldavian; Moldovan, +ron (T),, +rum (B),Romanian; Moldavian; Moldovan, +ron (T),, +run,Rundi, +rup,Aromanian; Arumanian; Macedo-Romanian, +rus,Russian, +sad,Sandawe, +sag,Sango, +sah,Yakut, +sai,South American Indian languages, +sal,Salishan languages, +sam,Samaritan Aramaic, +san,Sanskrit, +sas,Sasak, +sat,Santali, +scn,Sicilian, +sco,Scots, +sel,Selkup, +sem,Semitic languages, +sga,"Irish, Old (to 900)", +sgn,Sign Languages, +shn,Shan, +sid,Sidamo, +sin,Sinhala; Sinhalese, +sio,Siouan languages, +sit,Sino-Tibetan languages, +sla,Slavic languages, +slo (B),Slovak, +slk (T),, +slo (B),Slovak, +slk (T),, +slv,Slovenian, +sma,Southern Sami, +sme,Northern Sami, +smi,Sami languages, +smj,Lule Sami, +smn,Inari Sami, +smo,Samoan, +sms,Skolt Sami, +sna,Shona, +snd,Sindhi, +snk,Soninke, +sog,Sogdian, +som,Somali, +son,Songhai languages, +sot,"Sotho, Southern", +spa,Spanish; Castilian, +alb (B),Albanian, +sqi (T),, +srd,Sardinian, +srn,Sranan Tongo, +srp,Serbian, +srr,Serer, +ssa,Nilo-Saharan languages, +ssw,Swati, +suk,Sukuma, +sun,Sundanese, +sus,Susu, +sux,Sumerian, +swa,Swahili, +swe,Swedish, +syc,Classical Syriac, +syr,Syriac, +tah,Tahitian, +tai,Tai languages, +tam,Tamil, +tat,Tatar, +tel,Telugu, +tem,Timne, +ter,Tereno, +tet,Tetum, +tgk,Tajik, +tgl,Tagalog, +tha,Thai,Any-Thai +tib (B),Tibetan, +bod (T),, +tig,Tigre, +tir,Tigrinya, +tiv,Tiv, +tkl,Tokelau, +tlh,Klingon; tlhIngan-Hol, +tli,Tlingit, +tmh,Tamashek, +tog,Tonga (Nyasa), +ton,Tonga (Tonga Islands), +tpi,Tok Pisin, +tsi,Tsimshian, +tsn,Tswana, +tso,Tsonga, +tuk,Turkmen, +tum,Tumbuka, +tup,Tupi languages, +tur,Turkish, +tut,Altaic languages, +tvl,Tuvalu, +twi,Twi, +tyv,Tuvinian, +udm,Udmurt, +uga,Ugaritic, +uig,Uighur; Uyghur, +ukr,Ukrainian, +umb,Umbundu, +und,Undetermined, +urd,Urdu, +uzb,Uzbek, +vai,Vai, +ven,Venda, +vie,Vietnamese, +vol,Volapük, +vot,Votic, +wak,Wakashan languages, +wal,Wolaitta; Wolaytta, +war,Waray, +was,Washo, +wel (B),Welsh, +cym (T),, +wen,Sorbian languages, +wln,Walloon, +wol,Wolof, +xal,Kalmyk; Oirat, +xho,Xhosa, +yao,Yao, +yap,Yapese, +yid,Yiddish, +yor,Yoruba, +ypk,Yupik languages, +zap,Zapotec, +zbl,Blissymbols; Blissymbolics; Bliss, +zen,Zenaga, +zgh,Standard Moroccan Tamazight, +zha,Zhuang; Chuang, +chi (B),Chinese, +zho (T),, +znd,Zande languages, +zul,Zulu, +zun,Zuni, +zxx,No linguistic content; Not applicable, +zza,, diff --git a/api-test/src/main/resources/config/masterDataCertDataDeleteQueries.txt b/api-test/src/main/resources/config/masterDataCertDataDeleteQueries.txt new file mode 100644 index 000000000..81b0dc0d9 --- /dev/null +++ b/api-test/src/main/resources/config/masterDataCertDataDeleteQueries.txt @@ -0,0 +1,8 @@ +##### DB queries to be executed to tear down the data used and generated during the test rig run + +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_pid%' +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ekyc_pid%' +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_device_pid%' +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=${currentModule}_ftm_pid%' +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforautomationesi%' +delete from master.ca_cert_store WHERE cert_subject LIKE '%O=partnernameforesignet%' \ No newline at end of file diff --git a/api-test/src/main/resources/config/mimoto.properties b/api-test/src/main/resources/config/mimoto.properties new file mode 100644 index 000000000..0080d0376 --- /dev/null +++ b/api-test/src/main/resources/config/mimoto.properties @@ -0,0 +1,98 @@ +#---------------------------------- End point(s) relative URLs ----------------------------------# +actuatorMimotoEndpoint=/v1/mimoto/actuator/env +mimoto-oidc-mosipid-partner-clientid=mimoto.oidc.mosipid.partner.clientid +mimoto-oidc-sunbird-partner-clientid=mimoto.oidc.insurance.partner.clientid +sunbirdrc-insurance-esignet-host=sunbirdrc.insurance.esignet.host +mosipid-identity-esignet-host=mosipid.identity.esignet.host +mock-identity-esignet-host=mock.identity.esignet.host +runPlugin=mosipid + +# Uncomment the line below if the eSignet version is older than 1.5.1 for compatibility +#sunbirdInsuranceAuthFactorType=KBA + +#These properties to be added for google login +google.client.id = +google.client.secret = +google.refresh.token = + +#---------------------------------- Modifiable Properties ----------------------------------------------------------# + +#------------------------ Environment URLs and Database Connections ------------------------# + +# Keycloak URL. +keycloak-external-url = https://iam.released.mosip.net + +# PostgreSQL URLs for audit and partner databases. +audit_url = jdbc:postgresql://released.mosip.net:5432/mosip_audit +partner_url = jdbc:postgresql://released.mosip.net:5432/mosip_ida + +# Database server for connections. +db-server = released.mosip.net + + +#------------------------ secrets and passwords ------------------------# + +#------------------------ Keycloak Passwords ------------------------# +# Used for Keycloak authentication. +keycloak_Password = + +#------------------------ PostgreSQL Database Passwords ------------------------# +# Credentials for connecting to Postgres databases. +audit_password = +partner_password = +postgres-password = + +#-------- Client Secret Keys ----------# +# These keys are used for various services, make sure to update the values as required when running locally. + +mosip_partner_client_secret = +mosip_pms_client_secret = +mosip_resident_client_secret = +mosip_idrepo_client_secret = +mosip_reg_client_secret = +mosip_admin_client_secret = +mosip_hotlist_client_secret = +mosip_regproc_client_secret = +mpartner_default_mobile_secret = +mosip_testrig_client_secret = +AuthClientSecret = +mosip_crvs1_client_secret = + + +#-------- Generic Configuration ----------# + +# Enable or disable debugging mode (yes/no). +enableDebug = no + +# Mock Notification Channels (email/phone/email,phone). +mockNotificationChannel = email,phone + + +#------------------------ Mosip Components Base URLs ------------------------# +# Define base URLs for different components if required. +# Example: +# mosip_components_base_urls = auditmanager=api-internal.released.mosip.net;idrepository=api-internal.released.mosip.net;authmanager=api-internal.released.mosip.net;resident=api-internal.released.mosip.net;partnermanager=api-internal.released.mosip.net;idauthentication=api-internal.released.mosip.net;masterdata=api-internal.released.mosip.net;idgenerator=api-internal.released.mosip.net;policymanager=api-internal.released.mosip.net;preregistration=api-internal.released.mosip.net;keymanager=api-internal.released.mosip.net;mock-identity-system=api.released.mosip.net +# Feel free to add more components as needed. +mosip_components_base_urls = + +#------------------------ Module Name Pattern ------------------------# +# Define module name pattern if required. +# Example: +# moduleNamePattern = (mimoto|resident) +# Feel free to add more values as needed. +moduleNamePattern = + + +#------------------------ Uncomment for Local Run ------------------------# + +# Path to the authentication certificates (if running locally, uncomment the below line and keep the value empty). +# authCertsPath = + +# X-XSS-Protection: Controls the XSS (Cross-Site Scripting) filter in browsers. +# Values: (yes/no) +xssProtectionCheck = no + +testCasesToExecute= + +#Uncomment below line for generate dependency JSON. This works only in case of local run using IDE. +#generateDependencyJson = yes \ No newline at end of file diff --git a/api-test/src/main/resources/config/mispPolicy.json b/api-test/src/main/resources/config/mispPolicy.json new file mode 100644 index 000000000..a99d223fa --- /dev/null +++ b/api-test/src/main/resources/config/mispPolicy.json @@ -0,0 +1,7 @@ +{ + "trustBindedAuthVerificationToken": true, + "allowAuthRequestDelegation": true, + "allowKycRequestDelegation": true, + "allowKeyBindingDelegation": true, + "allowVciRequestDelegation": true +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/testCaseInterDependency.json b/api-test/src/main/resources/config/testCaseInterDependency.json new file mode 100644 index 000000000..801949414 --- /dev/null +++ b/api-test/src/main/resources/config/testCaseInterDependency.json @@ -0,0 +1,829 @@ +{ + "TC_Mimoto_UnlockWalletAfterDelete_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_DeleteWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_18": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_19": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_16": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_17": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_18": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_14": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_19": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_15": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_12": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_13": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_10": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_11": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_10": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_11": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_12": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_13": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_14": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_15": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_16": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_17": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_02": [ + "TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_02", + "TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_02" + ], + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01": [ + "TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_01", + "TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_09": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_07": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_08": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_07": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_08": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_04": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_09": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_WalletBinding_01": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_03": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_04": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_05": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_AuthorizationCode_01": [ + "TC_Mimoto_AuthenticateUser_01", + "TC_Mimoto_OAuthDetailsRequest_01" + ], + "TC_Mimoto_WalletBinding_06": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_UnlockWallet_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_UnlockWallet_02": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_38": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02", + "TC_Mimoto_CreateWallet_02" + ], + "TC_Mimoto_UnlockWallet_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_39": [ + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_36": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_37": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_34": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_35": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_32": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_33": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_30": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_31": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_40": [ + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_UnlockWallet_04": ["TC_Mimoto_CreateWallet_01"], + "TC_Mimoto_UnlockWallet_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_29": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_27": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_28": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_25": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_AuthenticateUserSunBirdC_01": [ + "TC_Mimoto_CreatePolicySunBirdR_01", + "TC_Mimoto_OAuthDetailsRequestSunBirdC_01" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_26": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_23": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_24": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_21": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_22": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_20": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02" + ], + "TC_Mimoto_GetPolicySunBirdR_01": ["TC_Mimoto_CreatePolicySunBirdR_01"], + "TC_Mimoto_CreateWallet_19": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AddTrustedVerifier_07": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_CreateWallet_16": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AddTrustedVerifier_06": [ + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_CreateWallet_15": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AddTrustedVerifier_05": [ + "TC_Mimoto_GoogleLoginToken_06", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_23" + ], + "TC_Mimoto_CreateWallet_18": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AddTrustedVerifier_04": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_CreateWallet_17": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AddTrustedVerifier_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_CreateWallet_12": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_11": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_14": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_13": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_10": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_BindingOtp_07": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_54": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_08": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_55": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_56": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_57": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_58": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_59": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_BindingOtp_09": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_50": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_51": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_01": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_52": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_53": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_GenerateVID_01": ["TC_Mimoto_AddIdentity_01"], + "TC_Mimoto_GenerateVID_06": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_GenerateVID_04": ["TC_Mimoto_AddIdentity_01"], + "TC_Mimoto_CreateWallet_23": ["TC_Mimoto_GoogleLoginToken_06"], + "TC_Mimoto_CreateWallet_22": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_21": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_GetUserProfile_01": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_20": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_BindingOtp_18": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_43": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_19": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_44": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_16": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_45": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_17": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_46": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_47": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_48": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_49": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_DeleteWallet_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteWallet_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_BindingOtp_10": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_AddTrustedVerifier_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteWallet_03": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_BindingOtp_11": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_AddTrustedVerifier_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_AuthorizationCodeForGoogleLogin_01": [ + "TC_Mimoto_OAuthDetailsRequestForGoogleLogin_01", + "TC_Mimoto_AuthenticateUserForGoogleLogin_01" + ], + "TC_Mimoto_DeleteWallet_04": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_AuthorizationCodeForGoogleLogin_02": [ + "TC_Mimoto_AuthenticateUserForGoogleLogin_02", + "TC_Mimoto_OAuthDetailsRequestForGoogleLogin_02" + ], + "TC_Mimoto_DeleteWallet_05": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_DeleteWallet_06": ["TC_Mimoto_CreateWallet_01"], + "TC_Mimoto_BindingOtp_14": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DeleteWallet_07": ["TC_Mimoto_CreateWallet_01"], + "TC_Mimoto_WalletBinding_40": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_BindingOtp_12": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_41": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_13": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_42": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_AuthLock_07": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_01": [ + "TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_01", + "TC_Mimoto_CreatePolicySunBirdR_01" + ], + "TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_02": [ + "TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_02", + "TC_Mimoto_CreatePolicySunBirdR_01" + ], + "TC_Mimoto_WalletBinding_32": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_InjiVerify_InjiVerifyVpRequest_01" + ], + "TC_Mimoto_RejectPresentation_04": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_33": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_04": [ + "TC_Mimoto_GoogleLoginToken_06", + "TC_Mimoto_CreateWallet_23", + "TC_InjiVerify_InjiVerifyVpRequest_01" + ], + "TC_Mimoto_RejectPresentation_05": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_34": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_05": [ + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_InjiVerify_InjiVerifyVpRequest_01" + ], + "TC_Mimoto_RejectPresentation_06": [ + "TC_Mimoto_GoogleLoginToken_06", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_23" + ], + "TC_Mimoto_WalletBinding_35": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_InjiVerify_InjiVerifyVpRequest_01" + ], + "TC_Mimoto_RejectPresentation_07": [ + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_36": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_37": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_RejectPresentation_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_38": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_InjiVerify_InjiVerifyVpRequest_01" + ], + "TC_Mimoto_RejectPresentation_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_WalletBinding_39": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_VerifiablePresentationAuthorization_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_RejectPresentation_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_WalletBinding_30": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_31": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_CreateWallet_09": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_08": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_07": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_06": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_WalletBinding_29": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_CreateWallet_01": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_03": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_CreateWallet_02": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_GetWallets_01": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_Logout_01": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_WalletBinding_21": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_22": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_23": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_24": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_UnlockWalletForTemporarilyLock_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_WalletBinding_25": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_26": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_27": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_28": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_WalletBinding_20": ["TC_Mimoto_AddIdentity_02"], + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_02" + ], + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01" + ], + "TC_Mimoto_AuthenticateUser_01": [ + "TC_Mimoto_AddIdentity_03", + "TC_Mimoto_OAuthDetailsRequest_01" + ], + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01" + ], + "TC_Mimoto_GetUserProfileAfterLogout_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_Logout_01" + ], + "TC_Mimoto_AuthenticateUserForGoogleLogin_01": [ + "TC_Mimoto_AddIdentity_03", + "TC_Mimoto_OAuthDetailsRequestForGoogleLogin_01" + ], + "TC_Mimoto_AuthenticateUserForGoogleLogin_02": [ + "TC_Mimoto_AddIdentity_03", + "TC_Mimoto_OAuthDetailsRequestForGoogleLogin_02" + ], + "TC_Mimoto_AuthorizationCodeSunBirdC_01": ["TC_Mimoto_OAuthDetailsRequestSunBirdC_01"], + "TC_Mimoto_DownloadIssuerCredential_02": ["TC_Mimoto_AuthorizationCode_01"], + "TC_Mimoto_BindingOtp_29": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_76": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_DownloadIssuerCredential_01": ["TC_Mimoto_AuthorizationCode_01"], + "TC_Mimoto_WalletBinding_77": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_27": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_78": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_28": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_DownloadIssuerCredential_06": ["TC_Mimoto_AuthorizationCodeSunBirdC_01"], + "TC_Mimoto_DownloadIssuerCredential_05": ["TC_Mimoto_AuthorizationCode_01"], + "TC_Mimoto_DownloadIssuerCredential_03": ["TC_Mimoto_AuthorizationCode_01"], + "TC_Mimoto_WalletBinding_70": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_71": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_25": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_72": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_26": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_73": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_74": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_24": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_75": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_65": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_66": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_67": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_68": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_69": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_32": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_BindingOtp_33": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_BindingOtp_30": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_BindingOtp_31": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_60": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_BindingOtp_36": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_61": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_WalletBinding_62": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_BindingOtp_34": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_63": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_04": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01" + ], + "TC_Mimoto_BindingOtp_35": ["TC_Mimoto_GenerateVID_06"], + "TC_Mimoto_WalletBinding_64": [ + "TC_Mimoto_AddIdentity_02", + "TC_Mimoto_GenerateVID_06" + ], + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_05": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01" + ], + "TC_Mimoto_DeletePolicySunBirdR_01": ["TC_Mimoto_CreatePolicySunBirdR_01"], + "TC_Mimoto_GetCredentialsForPresentation_10": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_09": [ + "TC_Mimoto_GoogleLoginToken_06", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_23" + ], + "TC_Mimoto_GetCredentialsForPresentation_08": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_02": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_07": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_GetCredentialsForPresentation_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_VerifiablePresentationAuthorization_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_05": [ + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_GetCredentialsForPresentation_04": [ + "TC_Mimoto_VerifiablePresentationAuthorization_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteCredentials_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteCredentials_02": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_02", + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteCredentials_01": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_02", + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_17": [ + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_FetchAllCredentials_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_16": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_DeleteCredentials_06": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01", + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_FetchAllCredentials_02": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_ViewCredential_15": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_DeleteCredentials_05": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_FetchAllCredentials_03": ["TC_Mimoto_UnlockWallet_01"], + "TC_Mimoto_ViewCredential_14": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_DeleteCredentials_04": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01", + "TC_Mimoto_GoogleLoginToken_01" + ], + "TC_Mimoto_FetchAllCredentials_04": ["TC_Mimoto_GoogleLoginToken_01"], + "TC_Mimoto_FetchAllCredentials_05": [ + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_FetchAllCredentials_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_02" + ], + "TC_Mimoto_FetchAllCredentials_07": [ + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_DeleteCredentials_08": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_18": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_13": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_12": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_11": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_10": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_06": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_05": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_04": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_03": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ], + "TC_Mimoto_ViewCredential_09": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_08": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_07": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_02": [ + "TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01", + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_AuthorizationCodeSunBirdC_01", + "TC_Mimoto_CreateWallet_01" + ], + "TC_Mimoto_ViewCredential_01": [ + "TC_Mimoto_GoogleLoginToken_01", + "TC_Mimoto_UnlockWallet_01", + "TC_Mimoto_CreateWallet_01", + "TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01" + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/config/valueMapping.properties b/api-test/src/main/resources/config/valueMapping.properties new file mode 100644 index 000000000..c9b1c8ebb --- /dev/null +++ b/api-test/src/main/resources/config/valueMapping.properties @@ -0,0 +1,35 @@ +# common properties +residenceStatus=NFR +fullName=TEST_FULLNAME +firstName=TEST_FIRSTNAME +dateOfBirth=1996/01/01 +gender=MLE +pobCountry=POB_COUNTRY +pobProvince=POB_PROVINCE +pobCity=POB_CITY +bloodType=BLOOD_TYPE +permanentCountry=PERMANENT_COUNTRY +permanentAddressLine1=PERMANENT_ADDRESS_LINE_1 +presentAddressLine1=PRESENT_ADDRESS_LINE_1 +modeOfClaim=MODE_OF_CLAIM +presentCountry=PRESENT_COUNTRY +addressLine1=TEST_ADDRESSLINE1 +addressLine2=TEST_ADDRESSLINE2 +addressLine3=TEST_ADDRESSLINE3 +postalCode=14022 +phone=8249742850 +email=test@mosip.net +region=TEST_REGION +fullName1=Lisa.GN +firstName1=Lisa.GN +province=TEST_PROVINCE +city=TEST_CITY +zone=TEST_ZONE +introducerName=TEST_INTRODUCERNAME +bloodGroup=O +Miss-TitleFromServer=MIS +Mr-TitleFromServer=MIR +Mrs-TitleFromServer=MRS +Miss-TitleFromServer.=MIS +addressCopy=TEST_ADDRESSLINE3 +proofOfAddress=TEST_ADDRESSLINE2 \ No newline at end of file diff --git a/api-test/src/main/resources/customize-emailable-report-template.html b/api-test/src/main/resources/customize-emailable-report-template.html new file mode 100644 index 000000000..cfeca4d6e --- /dev/null +++ b/api-test/src/main/resources/customize-emailable-report-template.html @@ -0,0 +1,61 @@ + + + + + + + + + +
+ mosip-logo +
+ + \ No newline at end of file diff --git a/api-test/src/main/resources/dbFiles/.xml b/api-test/src/main/resources/dbFiles/.xml new file mode 100644 index 000000000..ae236dcff --- /dev/null +++ b/api-test/src/main/resources/dbFiles/.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api-test/src/main/resources/dbFiles/PMS_delete_script.txt b/api-test/src/main/resources/dbFiles/PMS_delete_script.txt new file mode 100644 index 000000000..a0100b9d4 --- /dev/null +++ b/api-test/src/main/resources/dbFiles/PMS_delete_script.txt @@ -0,0 +1,28 @@ +delete from pms.partner_contact where partner_id='Tech-123'; +delete from partner_policy where part_id='Tech-123'; +delete from partner_policy where policy_id in (select id from auth_policy where name in ('mosip policy','mosip data share policy')); +delete from partner_policy_request where part_id='Tech-123'; +delete from pms.partner_policy where label='string'; +delete from partner_policy_request where policy_id in (select id from auth_policy where name in ('mosip policy','mosip data share policy')); +delete from partner_policy_bioextract where policy_id in (select id from auth_policy where name in ('mosip policy','mosip data share policy')); +delete from partner_policy_credential_type where part_id='MOVP'; +delete from pms.partner where id in ('Tech-123','MOVP','DPP','MISP','MISP2','FTP','111997','updatepolicy'); +delete from pms.auth_policy where name in('mosip policy','mosip policy2','mosip policy3','mosip data share policy','mosip data share policy2'); +delete from pms.policy_group where name in ('mosip policy group','mosip policy group2','update_policy_group'); +delete from pms.misp_license where cr_by='pm_testuser'; +delete from pms.misp_license where misp_id in ('MISP','MISP2'); +delete from pms.ftp_chip_detail where foundational_trust_provider_id='FTP'; +delete from pms.misp where name='mosip_misp'; +delete from pms.secure_biometric_interface where provider_id='Tech-123'; +delete from pms.device_detail where id='device-id-123'; +delete from pms.device_detail where make in ('abcde','abcdef'); +delete from mosip_keymgr.keymgr.ca_cert_store where cert_subject ='CN=mosiptest.org,O=MOSIPTEST,L=Bangalore,ST=Karantaka,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=Techno.com,O=Techno,L=Bangalore,ST=Karnataka,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=Techno,O=Techno,L=Bangalore,ST=Karnataka,C=IN'; +delete from mosip_keymgr.keymgr.ca_cert_store where cert_subject ='CN=apitest,OU=apitest,O=apitest,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.ca_cert_store where cert_subject ='CN=apitest2,OU=apitest2,O=apitest2,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=deviceprovider,OU=deviceprovider,O=deviceprovider,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=movp,OU=movp,O=movp,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=ftp,OU=ftp,O=ftp,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=misp,OU=misp,O=misp,L=BLR,ST=KAR,C=IN'; +delete from mosip_keymgr.keymgr.partner_cert_store where cert_subject ='CN=misp2,OU=misp2,O=misp2,L=BLR,ST=KAR,C=IN'; \ No newline at end of file diff --git a/api-test/src/main/resources/dbFiles/dbConfig.xml b/api-test/src/main/resources/dbFiles/dbConfig.xml new file mode 100644 index 000000000..e27fb8f56 --- /dev/null +++ b/api-test/src/main/resources/dbFiles/dbConfig.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api-test/src/main/resources/dbFiles/delete_Masterdata.sql b/api-test/src/main/resources/dbFiles/delete_Masterdata.sql new file mode 100644 index 000000000..8724f6b2e --- /dev/null +++ b/api-test/src/main/resources/dbFiles/delete_Masterdata.sql @@ -0,0 +1,88 @@ +delete from master.blocklisted_words where cr_by='dumbo6'; +delete from master.blocklisted_words where word='dumbooo'; +delete from master.blocklisted_words where cr_by='masterdata-220005'; +delete from master.machine_master where cr_by='masterdata-220005'; +delete from master.machine_master where cr_by='masterdata-220005'; +delete from master.machine_master where name in ('Mach-Test','Mach-Test2','Mach-Test updated'); +delete from master.machine_master mm WHERE mm.mspec_id IN(SELECT ms.id from master.machine_spec ms where ms.cr_by='masterdata-220005'); +delete FROM master.machine_spec WHERE mtyp_code IN (SELECT code FROM master.machine_type WHERE cr_by = 'masterdata-220005'); +delete from master.machine_spec where cr_by='masterdata-220005'; +delete from master.machine_type where cr_by='masterdata-220005'; +delete from master.gender where cr_by='masterdata-220005'; +delete from master.device_master where upd_by='masterdata-220005'; +delete from master.device_master where cr_by='masterdata-220005'; +delete from master.device_master where name='testDevicedummy'; +DELETE FROM master.device_master WHERE dspec_id IN(SELECT id FROM master.device_spec WHERE cr_by='masterdata-220005'); +delete from master.device_spec where cr_by='masterdata-220005'; +delete from master.device_type where cr_by='masterdata-220005'; +delete from master.loc_holiday where cr_by='masterdata-220005'; +delete from master.reg_center_type where cr_by='masterdata-220005'; +delete from master.registration_center where cr_by='masterdata-220005'; +delete from master.loc_holiday where cr_by='masterdata-220005'; +delete from master.reg_center_type where cr_by='masterdata-220005'; +delete from master.registration_center where cr_by='masterdata-220005'; +delete from master.device_type where cr_by='masterdata-220005'; +delete from master.doc_type where cr_by='masterdata-220005'; +delete from master.doc_category where cr_by='masterdata-220005'; +delete from master.location where cr_by='masterdata-220005'; +delete from master.template where cr_by='masterdata-220005'; +update master.template set is_active='true', is_deleted='false' where id='1101'; +delete from master.template where template_typ_code IN(select code from master.template_type where code='Test-info-Template-auto'); +delete from master.template_type where code='Test-info-Template-auto'; +update master.location set is_active='true', is_deleted='false' where code='10114'; +delete from master.location where code in('TST123','IND'); +delete from master.valid_document where cr_by='masterdata-220005'; +delete from master.user_detail where cr_by='masterdata-220005'; +delete from master.template_type where cr_by='masterdata-220005'; +delete from master.template_file_format where cr_by='masterdata-220005'; +delete from master.reason_list where cr_by='masterdata-220005'; +delete from master.reason_category where cr_by='masterdata-220005'; +delete from master.language where cr_by='masterdata-220005'; +delete from master.identity_schema where cr_by='masterdata-220005'; +delete from master.biometric_attribute where cr_by='masterdata-220005'; +delete from master.biometric_type where cr_by='masterdata-220005'; +delete from master.appl_form_type where cr_by='masterdata-220005'; +delete from master.id_type where cr_by='masterdata-220005'; +delete from master.dynamic_field where cr_by='masterdata-220005'; +delete from master.zone_user where usr_id='masterdata-220005'; +delete from master.blocklisted_words where word='dumbo6'; +delete from master.blocklisted_words where word='dumbo7'; +delete from master.machine_master where name in ('Mach-Test','Mach-Test2','Mach-Test updated'); +delete from master.machine_master where mac_address = '61-D3-FD-12-C9-ED'; +delete from master.machine_spec where name='HP'; +delete from master.machine_master where cr_by='masterdata-220005'; +delete from master.machine_type where code='Laptop2'; +delete from master.gender where code='Genderdummy'; +delete FROM master.device_master where name in ('testDevicedummy','testDevicedummy updated'); +delete from master.device_master where dspec_id='743'; +delete from master.device_spec where id='743'; +delete from master.device_type where code='GST3'; +delete from master.loc_holiday where holiday_name='AutoTest user Eng'; +delete from master.reg_center_type where code='ALT-3'; +delete FROM master.registration_center where name in ('Test123','HSR Center updated'); +delete from master.loc_holiday where holiday_name in ('AutoTest user Eng','AutoTest user'); +delete from master.reg_center_type where code in('ALT-3','ALT-5'); +delete from master.registration_center where id='10000'; +delete from master.device_type where code in ('GST3','GST4'); +delete from master.doc_type where code in ('TestDocType0010','TestDocType0020'); +delete from master.doc_category where code in ('DocTestCode123','DocTestCode321'); +delete from master.location where code='TST12'; +delete from master.template where id='445566777'; +delete from master.template where template_typ_code IN(select code from master.template_type where code='Test-info-Template-auto'); +update master.template set is_active='true', is_deleted='false' where id='1101'; +delete from master.template_type where code='Test-info-Template-auto'; +update master.location set is_active='true', is_deleted='false' where code='10114'; +delete from master.location where code in('TST123','IND'); +delete from master.valid_document where doctyp_Code='doc_auto_test'; +delete from master.user_detail where cr_by='110005'; +delete from master.template_type where code='Test-info-Template-auto'; +delete from master.template_file_format where code='Doc'; +delete from master.reason_list where code='TEST_LIST_CODE'; +delete from master.reason_category where code='TEST_CAT_CODE'; +delete from master.language where code='automationLang'; +delete from master.identity_schema where title='test-schema'; +delete from master.biometric_attribute where code='TST'; +delete from master.biometric_type where code='dumbo6'; +delete from master.appl_form_type where code='dumbo'; +delete from master.id_type where code='NEW'; +delete from master.dynamic_field where name in ('TestAutomationField','TestAPL'); diff --git a/api-test/src/main/resources/krb5.conf b/api-test/src/main/resources/krb5.conf new file mode 100644 index 000000000..734dbfcf8 --- /dev/null +++ b/api-test/src/main/resources/krb5.conf @@ -0,0 +1,29 @@ +# Configuration snippets may be placed in this directory as well +includedir /etc/krb5.conf.d/ + +[logging] + default = FILE:/var/log/krb5libs.log + kdc = FILE:/var/log/krb5kdc.log + admin_server = FILE:/var/log/kadmind.log + +[libdefaults] + kdc_timeout = 30s + udp_preference_limit = 1 + dns_lookup_realm = false + ticket_lifetime = 365d + renew_lifetime = 365d + forwardable = true + rdns = false + pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt + default_realm = NODE-MASTER.SOUTHINDIA.CLOUDAPP.AZURE.COM + #default_ccache_name = KEYRING:persistent:%{uid} + +[realms] + NODE-MASTER.SOUTHINDIA.CLOUDAPP.AZURE.COM = { + kdc = node-master.southindia.cloudapp.azure.com:51088 + admin_server = node-master.southindia.cloudapp.azure.com +} + +[domain_realm] + .node-master.southindia.cloudapp.azure.com = NODE-MASTER.SOUTHINDIA.CLOUDAPP.AZURE.COM + node-master.southindia.cloudapp.azure.com = NODE-MASTER.SOUTHINDIA.CLOUDAPP.AZURE.COM \ No newline at end of file diff --git a/api-test/src/main/resources/log4j.properties b/api-test/src/main/resources/log4j.properties new file mode 100644 index 000000000..93001dc68 --- /dev/null +++ b/api-test/src/main/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootLogger=INFO, Appender1,Appender2 +log4j.appender.Appender1=org.apache.log4j.ConsoleAppender +log4j.appender.Appender1.layout=org.apache.log4j.PatternLayout +log4j.appender.Appender1.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n +log4j.appender.Appender2=org.apache.log4j.FileAppender +log4j.appender.Appender2.File=src/logs/mosip-api-test.log +log4j.appender.Appender2.layout=org.apache.log4j.PatternLayout +log4j.appender.Appender2.layout.ConversionPattern=%-7p %d [%t] %c %x - %m%n diff --git a/api-test/src/main/resources/mimoto/AddIdentity/AddIdentity.yml b/api-test/src/main/resources/mimoto/AddIdentity/AddIdentity.yml new file mode 100644 index 000000000..ad48dd3bd --- /dev/null +++ b/api-test/src/main/resources/mimoto/AddIdentity/AddIdentity.yml @@ -0,0 +1,116 @@ +AddIdentity: + Mimoto_AddIdentity_withValidParameters_smoke_Pos: + endPoint: /idrepository/v1/identity/ + description: Validate that the system successfully creates a new identity in ID Repository using all valid input parameters + uniqueIdentifier: TC_Mimoto_AddIdentity_01 + role: idrepo + restMethod: post + inputTemplate: mimoto/AddIdentity/addIdentity_$LANGNUMBER$ + outputTemplate: mimoto/AddIdentity/addIdentityResult + input: '{ + "value": "$BIOVALUE$", + "id": "mosip.id.create", + "registrationId": "$RID$", + "biometricReferenceId": "23452353", + "UIN": "$UIN$", + "dateOfBirth": "1992/04/15", + "postalCode": "14022", + "email": "Mimoto_AddIdentity_withValidParameters_smoke_Pos@mosip.net", + "phone": "9876543210", + "referenceIdentityNumber": "6789545678878", + "version": "v1", + "introducerRID": "212124324784879", + "introducerUIN": "212124324784879", + "category": "individualBiometrics", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "status":"ACTIVATED" +}' + + Mimoto_AddIdentity_Binding_smoke_Pos: + endPoint: /idrepository/v1/identity/ + description: Validate that a new identity is successfully created using all valid parameters for Binding API workflows + uniqueIdentifier: TC_Mimoto_AddIdentity_02 + role: idrepo + restMethod: post + inputTemplate: mimoto/AddIdentity/addIdentity_$LANGNUMBER$ + outputTemplate: mimoto/AddIdentity/addIdentityResult + input: '{ + "value": "$BIOVALUE$", + "id": "mosip.id.create", + "registrationId": "$RID$", + "biometricReferenceId": "23452353", + "UIN": "$UIN$", + "dateOfBirth": "1992/04/15", + "postalCode": "14022", + "email": "Mimoto_AddIdentity_Binding_smoke_Pos@mosip.net", + "phone": "9876543210", + "referenceIdentityNumber": "6789545678878", + "version": "v1", + "introducerRID": "212124324784879", + "introducerUIN": "212124324784879", + "category": "individualBiometrics", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "status":"ACTIVATED" +}' + + Mimoto_AddIdentity_downloadIssuerCredential_smoke_Pos: + endPoint: /idrepository/v1/identity/ + description: Validate that a new identity with valid inputs can download the issuer credential + uniqueIdentifier: TC_Mimoto_AddIdentity_03 + role: idrepo + restMethod: post + inputTemplate: mimoto/AddIdentity/addIdentity_$LANGNUMBER$ + outputTemplate: mimoto/AddIdentity/addIdentityResult + input: '{ + "value": "$BIOVALUE$", + "id": "mosip.id.create", + "registrationId": "$RID$", + "biometricReferenceId": "23452353", + "UIN": "$UIN$", + "dateOfBirth": "1992/04/15", + "postalCode": "14022", + "email": "Mimoto_AddIdentity_downloadIssuerCredential_smoke_Pos@mosip.net", + "phone": "9876543210", + "referenceIdentityNumber": "6789545678878", + "version": "v1", + "introducerRID": "212124324784879", + "introducerUIN": "212124324784879", + "category": "individualBiometrics", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "status":"ACTIVATED" +}' + + Mimoto_AddIdentity_downloadIssuerCredentialWithGoogleLogin_smoke_Pos: + endPoint: /idrepository/v1/identity/ + description: Validate that creating a new identity allows the issuer credential download process to complete successfully + uniqueIdentifier: TC_Mimoto_AddIdentity_04 + role: idrepo + restMethod: post + inputTemplate: mimoto/AddIdentity/addIdentity_$LANGNUMBER$ + outputTemplate: mimoto/AddIdentity/addIdentityResult + input: '{ + "value": "$BIOVALUE$", + "id": "mosip.id.create", + "registrationId": "$RID$", + "biometricReferenceId": "23452353", + "UIN": "$UIN$", + "dateOfBirth": "1992/04/15", + "postalCode": "14022", + "email": "Mimoto_AddIdentity_downloadIssuerCredentialWithGoogleLogin_smoke_Pos@mosip.net", + "phone": "9876543210", + "referenceIdentityNumber": "6789545678878", + "version": "v1", + "introducerRID": "212124324784879", + "introducerUIN": "212124324784879", + "category": "individualBiometrics", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "status":"ACTIVATED" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AddIdentity/addIdentityResult.hbs b/api-test/src/main/resources/mimoto/AddIdentity/addIdentityResult.hbs new file mode 100644 index 000000000..ca45b742b --- /dev/null +++ b/api-test/src/main/resources/mimoto/AddIdentity/addIdentityResult.hbs @@ -0,0 +1,5 @@ +{ + "response": { + "status": "{{status}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AidGetIndividualId/AidGetIndividualId.yml b/api-test/src/main/resources/mimoto/AidGetIndividualId/AidGetIndividualId.yml new file mode 100644 index 000000000..8000d523c --- /dev/null +++ b/api-test/src/main/resources/mimoto/AidGetIndividualId/AidGetIndividualId.yml @@ -0,0 +1,147 @@ +AidGetIndividualId: + Mimoto_AidGetIndividualId_Getting_With_Valid_UIN_All: + endPoint: /v1/mimoto/aid/get-individual-id + description: Validate that the system successfully generates and returns the AID status when a valid AID is provided + uniqueIdentifier: TC_Mimoto_AidGetIndividualId_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AidGetIndividualId/aidGetIndividualId + outputTemplate: mimoto/AidGetIndividualId/aidGetIndividualIdResult + input: '{ + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "transactionID": "$TRANSACTIONID$", + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "sendOtp":{ + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "transactionID": "$TRANSACTIONID$", + "sendOtpReqTemplate": "mimoto/SendOTPUIN/individualIdOtp", + "sendOtpEndPoint": "/v1/mimoto/req/individualId/otp" + } +}' + output: '{ + "aidStatus": "PROCESSED", + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTPUIN/individualIdOtpResult" + } +}' + + Mimoto_AidGetIndividualId_Getting_With_Invalid_AID_Neg: + endPoint: /v1/mimoto/aid/get-individual-id + description: Validate that the system does not generate an individualID when an invalid AID is provided and returns the appropriate error response + uniqueIdentifier: TC_Mimoto_AidGetIndividualId_02 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AidGetIndividualId/aidGetIndividualId + outputTemplate: mimoto/error + input: '{ + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "transactionID": "$TRANSACTIONID$", + "aid": "$yt55g", + "sendOtp":{ + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "transactionID": "$TRANSACTIONID$", + "sendOtpReqTemplate": "mimoto/SendOTPUIN/individualIdOtp", + "sendOtpEndPoint": "/v1/mimoto/req/individualId/otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTPUIN/individualIdOtpResult" + } +}' + + Mimoto_AidGetIndividualId_Getting_With_Invalid_Otp_Neg: + endPoint: /v1/mimoto/aid/get-individual-id + description: Validate that the system does not generate an individualID when an invalid OTP is provided + uniqueIdentifier: TC_Mimoto_AidGetIndividualId_03 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AidGetIndividualId/aidGetIndividualId + outputTemplate: mimoto/error + input: '{ + "otp": "1234", + "transactionID": "$TRANSACTIONID$", + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "sendOtp":{ + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "transactionID": "$TRANSACTIONID$", + "sendOtpReqTemplate": "mimoto/SendOTPUIN/individualIdOtp", + "sendOtpEndPoint": "/v1/mimoto/req/individualId/otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTPUIN/individualIdOtpResult" + } +}' + + Mimoto_AidGetIndividualId_Getting_With_Valid_Otp_All: + endPoint: /v1/mimoto/aid/get-individual-id + description: Validate that the system successfully generates an individualID when a valid OTP is provided + uniqueIdentifier: TC_Mimoto_AidGetIndividualId_04 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AidGetIndividualId/aidGetIndividualId + outputTemplate: mimoto/AidGetIndividualId/aidGetIndividualIdResult + input: '{ + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "transactionID": "$TRANSACTIONID$", + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "sendOtp":{ + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "transactionID": "$TRANSACTIONID$", + "sendOtpReqTemplate": "mimoto/SendOTPUIN/individualIdOtp", + "sendOtpEndPoint": "/v1/mimoto/req/individualId/otp" + } +}' + output: '{ + "aidStatus": "PROCESSED", + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTPUIN/individualIdOtpResult" + } +}' + + Mimoto_AidGetIndividualId_Getting_With_Invalid_lenght_Of_AID_Neg: + endPoint: /v1/mimoto/aid/get-individual-id + description: Validate that the system does not generate an individualID when an invalid length AID is provided and returns the appropriate error response + uniqueIdentifier: TC_Mimoto_AidGetIndividualId_05 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AidGetIndividualId/aidGetIndividualId + outputTemplate: mimoto/error + input: '{ + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "transactionID": "$TRANSACTIONID$", + "aid": "2745538921628455389216202302041241544770230204124154278477", + "sendOtp":{ + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$", + "transactionID": "$TRANSACTIONID$", + "sendOtpReqTemplate": "mimoto/SendOTPUIN/individualIdOtp", + "sendOtpEndPoint": "/v1/mimoto/req/individualId/otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-457" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTPUIN/individualIdOtpResult" + } +}' diff --git a/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualId.hbs b/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualId.hbs new file mode 100644 index 000000000..0fda5feb3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualId.hbs @@ -0,0 +1,5 @@ +{ + "aid": "{{aid}}", + "otp": "{{otp}}", + "transactionID": "{{transactionID}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualIdResult.hbs b/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualIdResult.hbs new file mode 100644 index 000000000..eec0b1866 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AidGetIndividualId/aidGetIndividualIdResult.hbs @@ -0,0 +1,5 @@ +{ + "response": { + "aidStatus": "{{aidStatus}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.hbs b/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.hbs new file mode 100644 index 000000000..0229c1ef5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.hbs @@ -0,0 +1,12 @@ + { + "transactionID":"{{transactionID}}", + "individualId":"{{individualId}}", + "individualIdType":"{{individualIdType}}", + "otp":"{{otp}}", + "authType": [ + {{#each authType}} + "{{auth}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.yml b/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.yml new file mode 100644 index 000000000..12f6c62d7 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthLock/createAuthLock.yml @@ -0,0 +1,195 @@ +AuthLock: + Mimoto_AuthLock_Valid_UIN_All: + endPoint: /v1/mimoto/req/auth/lock + description: Validate that the system successfully locks the specified UIN when all parameters are valid + uniqueIdentifier: TC_Mimoto_AuthLock_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/AuthLock/createAuthLockResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthLock_InValid_Otp_All: + endPoint: /v1/mimoto/req/auth/lock + description: Verify that the system does not lock the UIN when an invalid OTP is provided + uniqueIdentifier: TC_Mimoto_AuthLock_02 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "111sss@@1", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthLock_with_invalidUIN_Valid_Otp_sid: + endPoint: /v1/mimoto/req/auth/lock + description: Validate that the system does not lock the UIN when an invalid UIN is provided + uniqueIdentifier: TC_Mimoto_AuthLock_03 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "263738@@#FDCVvb", + "individualIdType": "UIN", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthLock_Valid_VID_All: + endPoint: /v1/mimoto/req/auth/lock + description: Verify that the system successfully locks authentication for a VID when all parameters are valid + uniqueIdentifier: TC_Mimoto_AuthLock_04 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/AuthLock/createAuthLockResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthLock_with_invalidVID_Valid_Otp: + endPoint: /v1/mimoto/req/auth/lock + description: Verify that the system does not lock authentication when an invalid VID is provided + uniqueIdentifier: TC_Mimoto_AuthLock_05 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "26DBDB@73", + "individualIdType": "VID", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthLock_invalidVID_Valid_Otp: + endPoint: /v1/mimoto/req/auth/lock + description: Validate that the system does not lock the VID when an invalid VID is provided and returns the appropriate error message + uniqueIdentifier: TC_Mimoto_AuthLock_06 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthLock/createAuthLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "36363gvb", + "individualIdType": "VID", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' diff --git a/api-test/src/main/resources/mimoto/AuthLock/createAuthLockResult.hbs b/api-test/src/main/resources/mimoto/AuthLock/createAuthLockResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthLock/createAuthLockResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.hbs b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.hbs new file mode 100644 index 000000000..3607daaf7 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.hbs @@ -0,0 +1,13 @@ +{ + "transactionID":"{{transactionID}}", + "individualId":"{{individualId}}", + "individualIdType":"{{individualIdType}}", + "unlockForSeconds": "{{unlockForSeconds}}", + "otp":"{{otp}}", + "authType": [ + {{#each authType}} + "{{auth}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.yml b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.yml new file mode 100644 index 000000000..c2f2865f4 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLock.yml @@ -0,0 +1,131 @@ +AuthUnlock: + Mimoto_AuthUnLock_Valid_UIN_All: + endPoint: /v1/mimoto/req/auth/unlock + description: Verify that the system unlocks the UIN successfully when all parameters are valid + uniqueIdentifier: TC_Mimoto_AuthLock_07 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthUnLock/createAuthUnLock + outputTemplate: mimoto/AuthUnLock/createAuthUnLockResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "unlockForSeconds": "1", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthUnLock_Valid_UIN_With_VID_All: + endPoint: /v1/mimoto/req/auth/unlock + description: Validate that the system processes the unlock request and successfully unlocks the VID when all parameters are valid + uniqueIdentifier: TC_Mimoto_AuthLock_08 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthUnLock/createAuthUnLock + outputTemplate: mimoto/AuthUnLock/createAuthUnLockResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "unlockForSeconds": "1", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthUnLock_With_InValid_OTP_All: + endPoint: /v1/mimoto/req/auth/unlock + description: Validate that the system does not unlock the UIN when an invalid OTP is provided and returns the expected error code + uniqueIdentifier: TC_Mimoto_AuthLock_09 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthUnLock/createAuthUnLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "unlockForSeconds": "1", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "111@@@###hhdhdh111", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_AuthUnLock_InValid_OTP_With_VID_All: + endPoint: /v1/mimoto/req/auth/unlock + description: Validate that the system does not unlock the VID when an invalid OTP is provided + uniqueIdentifier: TC_Mimoto_AuthLock_10 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/AuthUnLock/createAuthUnLock + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "unlockForSeconds": "1", + "authType": [{auth: "bio-Iris"},{auth: "demo"},{auth: "bio-FACE"}], + "otp": "11111@@###1", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLockResult.hbs b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLockResult.hbs new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthUnLock/createAuthUnLockResult.hbs @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.hbs b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.hbs new file mode 100644 index 000000000..887633e11 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.hbs @@ -0,0 +1,15 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "challengeList" : [ + { + "authFactorType" : "{{authFactorType}}", + "challenge" : "{{challenge}}", + "format": "alpha-numeric" + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.yml b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.yml new file mode 100644 index 000000000..ee4269fc1 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUser.yml @@ -0,0 +1,33 @@ +AuthenticateUser: + Mimoto_ESignet_AuthenticateUserIDP_uin_Otp_Valid_Smoke: + endPoint: /v1/esignet/authorization/authenticate + description: Verify that the user is successfully authenticated for the credential download flow when all valid parameters are provided + uniqueIdentifier: TC_Mimoto_AuthenticateUser_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/AuthenticateUser/AuthenticateUser + outputTemplate: mimoto/AuthenticateUser/AuthenticateUserResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "authFactorType" : "OTP", + "challenge" : "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_EMAIL$", + "sendOtp":{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "otpChannels": [{channel: "email"},{channel: "phone"}], + "sendOtpReqTemplate": "mimoto/SendOtpEsignet/SendOtp", + "sendOtpEndPoint": "/v1/esignet/authorization/send-otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"esignet/SendOtp/SendOtpResult" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUserResult.hbs b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUserResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthenticateUser/AuthenticateUserResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.hbs b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.hbs new file mode 100644 index 000000000..491dca1ba --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.hbs @@ -0,0 +1,19 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "acceptedClaims": [ + {{#each acceptedClaims}} + "{{claim}}" + {{#unless @last}},{{/unless}} + {{/each}} + ], + "permittedAuthorizeScopes": [ + {{#each permittedAuthorizeScopes}} + "{{scope}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.yml b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.yml new file mode 100644 index 000000000..7f9e88772 --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCode.yml @@ -0,0 +1,19 @@ +AuthorizationCode: + Mimoto_ESignet_AuthorizationCode_uin_All_Valid_Smoke_sid: + endPoint: /v1/esignet/authorization/auth-code + description: Validate that the system successfully generates an authorization code to download credentials from the issuer + uniqueIdentifier: TC_Mimoto_AuthorizationCode_01 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_AuthenticateUser_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/AuthorizationCode/AuthorizationCode + outputTemplate: mimoto/AuthorizationCode/AuthorizationCodeResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid_transactionId$" +}' + output: '{ +}' diff --git a/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCodeResult.hbs b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCodeResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/AuthorizationCode/AuthorizationCodeResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.hbs b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.hbs new file mode 100644 index 000000000..0221d2be2 --- /dev/null +++ b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.hbs @@ -0,0 +1,12 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "individualId": "{{individualId}}", + "otpChannels": [ + {{#each otpChannels}} + "{{channel}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.yml b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.yml new file mode 100644 index 000000000..1b80b7308 --- /dev/null +++ b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtp.yml @@ -0,0 +1,778 @@ +BindingOtp: + Mimoto_BindingOtp_Valid_UIN_All_Smoke: + endPoint: /v1/mimoto/binding-otp + role: resident + description: Validate that the system successfully creates an OTP to bind an individualID to a valid UIN + uniqueIdentifier: TC_Mimoto_BindingOtp_01 + checkErrorsOnlyInResponse: true + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/BindingOtp/BindingOtpResult + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ +}' + + Mimoto_BindingOtp_Valid_vid_All_Smoke: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system generates an OTP for binding an individualID with a valid VID + uniqueIdentifier: TC_Mimoto_BindingOtp_02 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/BindingOtp/BindingOtpResult + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ +}' + + Mimoto_BindingOtp_UIN_Invalid_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when binding an individualID with an invalid UIN + uniqueIdentifier: TC_Mimoto_BindingOtp_03 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "@#DSDFD@#", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ] +}' + + Mimoto_BindingOtp_UIN_Missing_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate OTP generation fails if the individualID attribute is not provided + uniqueIdentifier: TC_Mimoto_BindingOtp_04 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$REMOVE$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Empty_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the individualID is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_05 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_identifier" + } + ] +}' + + Mimoto_BindingOtp_UIN_Null_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the individualID value is null + uniqueIdentifier: TC_Mimoto_BindingOtp_06 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "null", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ] +}' + + Mimoto_BindingOtp_UIN_SpaceSeparated_otpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value contains spaces + uniqueIdentifier: TC_Mimoto_BindingOtp_07 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "E M A I L"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Invalid_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when an invalid OTP channel is provided + uniqueIdentifier: TC_Mimoto_BindingOtp_08 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "sdsgsg"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Missing_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_09 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$" +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Empty_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_10 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: ""}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_UIN_Null_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is null + uniqueIdentifier: TC_Mimoto_BindingOtp_11 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "null"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Missing_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel is missing from the request + uniqueIdentifier: TC_Mimoto_BindingOtp_12 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$" +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_UIN_Empty_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that OTP generation fails when the OTP channel field is empty in the request + uniqueIdentifier: TC_Mimoto_BindingOtp_13 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": "" +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_UIN_Null_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Verify that the system prevents OTP generation when the OTP Channels field is the string null + uniqueIdentifier: TC_Mimoto_BindingOtp_14 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": "null" +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_UIN_Invalid_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is invalid + uniqueIdentifier: TC_Mimoto_AuthLock_07 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-08T06:35:53", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_UIN_Past_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is in the past + uniqueIdentifier: TC_Mimoto_BindingOtp_16 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-07T06:51:30.195Z", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_UIN_Future_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate OTP generation fails when the request time is in the future + uniqueIdentifier: TC_Mimoto_BindingOtp_17 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2033-11-07T06:51:30.195Z", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_UIN_Missing_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_18 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_UIN_Empty_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_19 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_vid_Invalid_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when an invalid VID is provided + uniqueIdentifier: TC_Mimoto_BindingOtp_20 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "@#DSDFD@#", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ] +}' + + Mimoto_BindingOtp_vid_Missing_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the VID is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_21 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$REMOVE$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Empty_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the VID is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_22 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_identifier" + } + ] +}' + + Mimoto_BindingOtp_vid_Null_individualId_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when an invalid VID is provided + uniqueIdentifier: TC_Mimoto_BindingOtp_23 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "null", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ] +}' + + Mimoto_BindingOtp_vid_SpaceSeparated_otpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value contains spaces + uniqueIdentifier: TC_Mimoto_BindingOtp_24 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "E M A I L"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Invalid_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when an invalid OTP channel is provided + uniqueIdentifier: TC_Mimoto_BindingOtp_25 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "sdsgsg"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Missing_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_26 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$" +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Empty_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_27 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: ""}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_vid_Null_OtpChannels_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is null + uniqueIdentifier: TC_Mimoto_BindingOtp_28 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "null"}] +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Missing_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Verify that the system prevents OTP generation when the OTP channel value is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_29 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$" +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-011" + } + ] +}' + + Mimoto_BindingOtp_vid_Empty_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_30 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": "" +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_vid_Null_OtpChannel_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the OTP channel value is null + uniqueIdentifier: TC_Mimoto_BindingOtp_31 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": "null" +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_otp_channel" + } + ] +}' + + Mimoto_BindingOtp_vid_Invalid_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is invalid + uniqueIdentifier: TC_Mimoto_BindingOtp_32 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-08T06:35:53", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_vid_Past_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is in the past + uniqueIdentifier: TC_Mimoto_BindingOtp_33 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-07T06:51:30.195Z", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_vid_Future_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is in the future + uniqueIdentifier: TC_Mimoto_BindingOtp_34 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "2033-11-07T06:51:30.195Z", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_vid_Missing_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate that the system does not generate an OTP when the request time is missing + uniqueIdentifier: TC_Mimoto_BindingOtp_35 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' + + Mimoto_BindingOtp_vid_Empty_RequestTime_Neg: + endPoint: /v1/mimoto/binding-otp + description: Validate OTP generation fails if the request time field is empty + uniqueIdentifier: TC_Mimoto_BindingOtp_36 + role: resident + restMethod: post + validityCheckRequired: true + inputTemplate: mimoto/BindingOtp/BindingOtp + outputTemplate: mimoto/error + input: '{ + "requestTime": "", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"}] +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ] +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/BindingOtp/BindingOtpResult.hbs b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtpResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/BindingOtp/BindingOtpResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CreateVID/CreateVid.yml b/api-test/src/main/resources/mimoto/CreateVID/CreateVid.yml new file mode 100644 index 000000000..42fcb57a6 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CreateVID/CreateVid.yml @@ -0,0 +1,57 @@ +CreateVID: + Mimoto_Generate_Perpetual_VID_Valid_Smoke_sid: + endPoint: /idrepository/v1/vid + description: Validate that the system successfully creates a perpetual VID when all input details are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_01 + role: idrepo + restMethod: post + inputTemplate: mimoto/CreateVID/createVid + outputTemplate: mimoto/CreateVID/createVidResult + input: '{ + "vidType": "Perpetual", + "UIN": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "version": "v1", + "id": "mosip.vid.create", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "vidStatus":"ACTIVE" +}' + + Mimoto_Generate_Temporary_VID_Valid_Smoke_sid: + endPoint: /idrepository/v1/vid + description: Validate that the system successfully creates a temporary VID when all provided details are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_04 + role: idrepo + restMethod: post + inputTemplate: mimoto/CreateVID/createVid + outputTemplate: mimoto/CreateVID/createVidResult + input: '{ + "vidType": "Perpetual", + "UIN": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "version": "v1", + "id": "mosip.vid.create", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "vidStatus":"ACTIVE" +}' + + Mimoto_Generate_Perpetual_VID_Binding_Valid_Smoke_sid: + endPoint: /idrepository/v1/vid + description: Validate that the system successfully creates a perpetual VID for binding when all provided details are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_06 + role: idrepo + restMethod: post + inputTemplate: mimoto/CreateVID/createVid + outputTemplate: mimoto/CreateVID/createVidResult + input: '{ + "vidType": "Perpetual", + "UIN": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "version": "v1", + "id": "mosip.vid.create", + "requesttime": "$TIMESTAMP$" +}' + output: '{ + "vidStatus":"ACTIVE" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CreateVID/createVid.hbs b/api-test/src/main/resources/mimoto/CreateVID/createVid.hbs new file mode 100644 index 000000000..b0dfb2cbf --- /dev/null +++ b/api-test/src/main/resources/mimoto/CreateVID/createVid.hbs @@ -0,0 +1,11 @@ + +{ + "id": "{{id}}", + "metadata": {}, + "request": { + "UIN": "{{UIN}}", + "vidType": "{{vidType}}" + }, + "requesttime": "{{requesttime}}", + "version": "{{version}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CreateVID/createVidResult.hbs b/api-test/src/main/resources/mimoto/CreateVID/createVidResult.hbs new file mode 100644 index 000000000..5e95428a4 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CreateVID/createVidResult.hbs @@ -0,0 +1,5 @@ +{ + "response": { + "vidStatus": "{{vidStatus}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/CredentialCheckStatus.yml b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/CredentialCheckStatus.yml new file mode 100644 index 000000000..c006f6165 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/CredentialCheckStatus.yml @@ -0,0 +1,48 @@ +CredentialsStatus: + Mimoto_CredentialsStatus_euin_All_Valid_Smoke: + endPoint: /v1/mimoto/credentialshare/request/status/{requestId} + description: Verifying that the credential status is retrieved successfully with a valid UIN + uniqueIdentifier: TC_Mimoto_CredentialsStatus_01 + role: resident + restMethod: get + inputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus + outputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult + input: '{ + "requestId": "$ID:RequestCredentials__All_Valid_Smoke_sid_requestId$" +}' + output: '{ + "statusCode": "ISSUED" +}' + Mimoto_CredentialsStatus_euin_With_VID_All_Valid_Smoke: + endPoint: /v1/mimoto/credentialshare/request/status/{requestId} + description: Verify that the credential status is successfully retrieved when a valid VID is provided + uniqueIdentifier: TC_Mimoto_CredentialsStatus_02 + role: resident + restMethod: get + inputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus + outputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult + input: '{ + "requestId": "$ID:RequestCredentials_All_Valid_Smoke_with_VID_sid_requestId$" +}' + output: '{ + "statusCode": "ISSUED" +}' + + Mimoto_CredentialsStatus_euin_With_Invalid_RequestId_Neg: + endPoint: /v1/mimoto/credentialshare/request/status/{requestId} + description: Verifying that the system returns the appropriate error response when retrieving credential status with an invalid request ID + uniqueIdentifier: TC_Mimoto_CreateVID_03 + role: resident + restMethod: get + inputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus + outputTemplate: mimoto/error + input: '{ + "requestId": "$er45" +}' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-29" + } + ] +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus.hbs new file mode 100644 index 000000000..2eb899690 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatus.hbs @@ -0,0 +1,3 @@ +{ + "requestId": "{{requestId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult.hbs new file mode 100644 index 000000000..5bd4b5fb0 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult.hbs @@ -0,0 +1,5 @@ +{ + "response": { + "statusCode": "{{statusCode}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/DownloadCredential.yml b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/DownloadCredential.yml new file mode 100644 index 000000000..5c2c1da39 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/DownloadCredential.yml @@ -0,0 +1,48 @@ +DownloadCredential: + Mimoto_DownloadCredential_All_Valid_Smoke: + endPoint: /v1/mimoto/credentialshare/download + description: Verifying that the credential is downloaded successfully with all valid parameters + uniqueIdentifier: TC_Mimoto_DownloadCredential_01 + role: resident + restMethod: post + inputTemplate: mimoto/CredentialIssuance/DownloadCredential/downloadCredential + outputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult + input: '{ + "requestId": "ccb87250-aaa7-404a-8d6c-c48dfd25c2fd", + "individualId": "9653041697" +}' + output: '{ + "statusCode": "printing" +}' + + Mimoto_DownloadCredential_Invalid_RequestId_Neg: + endPoint: /v1/mimoto/credentialshare/download + description: Validating the download of the credential with an invalid request ID + uniqueIdentifier: TC_Mimoto_DownloadCredential_02 + role: resident + restMethod: post + inputTemplate: mimoto/CredentialIssuance/DownloadCredential/downloadCredential + outputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult + input: '{ + "requestId": "hj$", + "individualId": "9653041697" +}' + output: '{ + "statusCode": "printing" +}' + + Mimoto_DownloadCredential_Invalid_IndividualId_Neg: + endPoint: /v1/mimoto/credentialshare/download + description: Verifying that the system returns an appropriate error when attempting to download the credential with an invalid Individual ID + uniqueIdentifier: TC_Mimoto_DownloadCredential_03 + role: resident + restMethod: post + inputTemplate: mimoto/CredentialIssuance/DownloadCredential/downloadCredential + outputTemplate: mimoto/CredentialIssuance/CredentialCheckStatus/credentialcheckstatusResult + input: '{ + "requestId": "ccb87250-aaa7-404a-8d6c-c48dfd25c2fd", + "individualId": "$ghg6" +}' + output: '{ + "statusCode": "printing" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/downloadCredential.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/downloadCredential.hbs new file mode 100644 index 000000000..0e1113d75 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadCredential/downloadCredential.hbs @@ -0,0 +1,4 @@ +{ + "individualId": "{{individualId}}", + "requestId": "{{requestId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/DownloadWithReqid.yml b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/DownloadWithReqid.yml new file mode 100644 index 000000000..aeb552ecb --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/DownloadWithReqid.yml @@ -0,0 +1,30 @@ +DownloadCredential: + Mimoto_CredentialsStatus_All_Valid_Smoke_euin: + endPoint: /v1/mimoto/credentialshare/download + description: Verify that the credential is downloaded successfully with all valid parameters + uniqueIdentifier: TC_Mimoto_DownloadCredential_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: get + inputTemplate: mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqid + outputTemplate: mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqidResult + input: '{ + "requestId": "$ID:RequestCredentials__All_Valid_Smoke_sid_requestId$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$" +}' + output: ' { + "Content-Type": "application/pdf" +}' + Mimoto_CredentialsStatus_StatusCode_InValid_Smoke_euin: + endPoint: /v1/mimoto/credentialshare/download + description: Verify that the system returns an appropriate error when checking credential status with an invalid StatusCode + uniqueIdentifier: TC_Mimoto_DownloadCredential_02 + role: resident + restMethod: get + inputTemplate: mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqid + outputTemplate: mimoto/error + input: '{ + "requestId": "$ID:RequestCredentials__All_Valid_Smoke_sid_requestId$", + "individualId": "965337484848041697" +}' + output: "404" diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqid.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqid.hbs new file mode 100644 index 000000000..0e1113d75 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqid.hbs @@ -0,0 +1,4 @@ +{ + "individualId": "{{individualId}}", + "requestId": "{{requestId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqidResult.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqidResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/DownloadWithReqid/downloadwithreqidResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredential.yml b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredential.yml new file mode 100644 index 000000000..1544b3c16 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredential.yml @@ -0,0 +1,191 @@ +RequestCredentials: + Mimoto_RequestCredentials__All_Valid_Smoke_sid: + endPoint: /v1/mimoto/credentialshare/request + description: Verifying that the credential is requested successfully with all valid parameters + uniqueIdentifier: TC_Mimoto_RequestCredentials_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/CredentialIssuance/RequestCredentials/ReqCredentialResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "user": "abc", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_RequestCredentials_All_Valid_Smoke_with_VID_sid: + endPoint: /v1/mimoto/credentialshare/request + description: Verifying that the credential is requested successfully with a valid VID + uniqueIdentifier: TC_Mimoto_RequestCredentials_02 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/CredentialIssuance/RequestCredentials/ReqCredentialResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "user": "abc", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_RequestCredentials_InValid_Otp_UIN_sid: + endPoint: /v1/mimoto/credentialshare/request + role: resident + description: Verifying that the system returns an appropriate error when requesting a UIN credential with an invalid OTP + uniqueIdentifier: TC_Mimoto_RequestCredentials_03 + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "user": "abc", + "otp": "4h4hnn", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_RequestCredentials_InValid_TransactionId_sid: + endPoint: /v1/mimoto/credentialshare/request + description: Verifying that the system returns an appropriate error when requesting a credential with an invalid transaction ID + uniqueIdentifier: TC_Mimoto_RequestCredentials_04 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/error + input: '{ + "transactionID": "4dhn@@##&%", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "user": "abc", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + + Mimoto_RequestCredentials_InValid_UIN_sid: + endPoint: /v1/mimoto/credentialshare/request + description: Verifying that the system returns an appropriate error when requesting a credential with an invalid UIN + uniqueIdentifier: TC_Mimoto_RequestCredentials_05 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ght56=", + "user": "abc", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + + Mimoto_RequestCredentials_Invalid_VID_sid_Neg: + endPoint: /v1/mimoto/credentialshare/request + description: Verifying that the system returns an appropriate error when requesting a credential with an invalid VID + uniqueIdentifier: TC_Mimoto_RequestCredentials_06 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/CredentialIssuance/RequestCredentials/reqCredential + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$hfg56", + "user": "abc", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Valid_Smoke_sid_vid$", + "individualIdType": "VID", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredentialResult.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredentialResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/ReqCredentialResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/reqCredential.hbs b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/reqCredential.hbs new file mode 100644 index 000000000..128553079 --- /dev/null +++ b/api-test/src/main/resources/mimoto/CredentialIssuance/RequestCredentials/reqCredential.hbs @@ -0,0 +1,6 @@ +{ + "individualId": "{{individualId}}", + "otp": "{{otp}}", + "transactionID": "{{transactionID}}", + "user": "{{user}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.hbs b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.hbs new file mode 100644 index 000000000..082575d78 --- /dev/null +++ b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.hbs @@ -0,0 +1,9 @@ +{ + "grant_type": "{{grant_type}}", + "code": "{{code}}", + "redirect_uri": "{{redirect_uri}}", + "code_verifier": "{{code_verifier}}", + "vcStorageExpiryLimitInTimes": 1, + "issuer": "{{issuer}}", + "credential": "{{credential}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.yml b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.yml new file mode 100644 index 000000000..f4e17a526 --- /dev/null +++ b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredential.yml @@ -0,0 +1,140 @@ +DownloadIssuerCredential: + Mimoto_DownloadIssuerCredential_IssuerMosip_All_Valid_Smoke: + endPoint: /v1/mimoto/credentials/download + description: Verify that the credential is downloaded successfully when all valid parameters are provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_01 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_uin_All_Valid_Smoke_sid_code$", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credential": "MOSIPVerifiableCredential" +}' + output: '{ +}' + + Mimoto_DownloadIssuerCredential_Invalid_Issuer_Neg: + endPoint: /v1/mimoto/credentials/download + description: Validate that the system does not download the credential when an invalid issuer is provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_02 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_uin_All_Valid_Smoke_sid_code$", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "MosipInvalid", + "credential": "MOSIPVerifiableCredential" +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + + Mimoto_DownloadIssuerCredential_Invalid_Credential_Neg: + endPoint: /v1/mimoto/credentials/download + description: Validate that the system does not download the credential when an invalid credential is provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_03 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_uin_All_Valid_Smoke_sid_code$", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credential": "MOSIPVerifiableCredentialInvalid" +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + + Mimoto_DownloadIssuerCredential_Invalid_AuthorizationCode_Neg: + endPoint: /v1/mimoto/credentials/download + description: Validate that the system does not download the credential when an invalid authorization code is provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_04 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "XjIsJgQAX3lrhMmXgRn-jerNMCqf0GdiBLx5MbMig", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credential": "MOSIPVerifiableCredential" +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + + + Mimoto_DownloadIssuerCredential_Reused_AuthorizationCode_Neg: + endPoint: /v1/mimoto/credentials/download + description: Validate that the system does not download the credential when an reused authorization code is provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_05 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_uin_All_Valid_Smoke_sid_code$", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credential": "MOSIPVerifiableCredential" +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + Mimoto_DownloadIssuerCredential_IssuerSunBird_All_Valid_Smoke: + endPoint: /v1/mimoto/credentials/download + description: Validate that the system successfully downloads the Sunbird credential when a valid authorization code is provided + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredential_06 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grant_type": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdC_All_Valid_Smoke_sid_code$", + "redirect_uri": "$INJIREDIRECTURI$", + "code_verifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credential": "InsuranceCredential" +}' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.hbs b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.hbs new file mode 100644 index 000000000..4540b9d97 --- /dev/null +++ b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.hbs @@ -0,0 +1,7 @@ +{ + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "otp": "{{otp}}", + "transactionID": "{{transactionID}}", + "vidType": "{{vidType}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.yml b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.yml new file mode 100644 index 000000000..14778af01 --- /dev/null +++ b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVID.yml @@ -0,0 +1,193 @@ +GenerateVID: + Mimoto_Generate_Perpetual_VID_Valid_Smoke_sid: + endPoint: /v1/mimoto/vid + description: Validate that the system successfully creates a perpetual VID when all input parameters are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/GenerateVID/createGenerateVIDResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "vidType": "Perpetual", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_Generate_Perpetual_VID_InValid_Otp_sid: + endPoint: /v1/mimoto/vid + description: Validate that the system does not create a perpetual VID when an invalid OTP is provided + uniqueIdentifier: TC_Mimoto_GenerateVID_02 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "vidType": "Perpetual", + "otp": "112637338", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + Mimoto_Generate_Perpetual_VID_with_InvalidUIN_Valid_Otp_sid: + endPoint: /v1/mimoto/vid + description: Verify that the system prevents creation of a perpetual VID when the UIN is invalid + uniqueIdentifier: TC_Mimoto_GenerateVID_03 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "253SDGSH@Y37", + "individualIdType": "UIN", + "vidType": "Perpetual", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-410" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + + mimoto_Generate_Temporary_VID_Valid_Smoke_sid: + endPoint: /v1/mimoto/vid + description: Validate that the system successfully creates a temporary VID when all request parameters are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_04 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/GenerateVID/createGenerateVIDResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "vidType": "Temporary", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + + Mimoto_Generate_Perpetual_VID_Invalid_TransactionId_Neg: + endPoint: /v1/mimoto/vid + description: Verify that the system prevents creation of a perpetual VID when the transaction ID is invalid + uniqueIdentifier: TC_Mimoto_GenerateVID_05 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/error + input: '{ + "transactionID": "$trc45", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "vidType": "Perpetual", + "otp": "$ID:AddIdentity_withValidParameters_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "errors": [ + { + "errorCode": "RES-SER-422" + } + ], + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' + + Mimoto_Generate_Perpetual_VID_Binding_Valid_Smoke_sid: + endPoint: /v1/mimoto/vid + description: Validate that the system successfully creates a perpetual VID for binding when the request parameters are valid + uniqueIdentifier: TC_Mimoto_GenerateVID_06 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/GenerateVID/createGenerateVID + outputTemplate: mimoto/GenerateVID/createGenerateVIDResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "individualIdType": "UIN", + "vidType": "Perpetual", + "otp": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "sendOtp":{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "individualIdType": "UIN", + "sendOtpReqTemplate": "mimoto/SendOTP/createSendOTP", + "sendOtpEndPoint": "/v1/mimoto/req/otp" + } + }' + output: '{ + "sendOtpResp":{ + "sendOtpResTemplate":"mimoto/SendOTP/createSendOTPResult" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVIDResult.hbs b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVIDResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/GenerateVID/createGenerateVIDResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.hbs b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.yml b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.yml new file mode 100644 index 000000000..f5bcfb840 --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuers.yml @@ -0,0 +1,47 @@ +GetAllIssuers: + Mimoto_GetAllIssuers_All_Valid_Smoke: + endPoint: /v1/mimoto/issuers + description: Validate that the system successfully retrieves issuer details when all input parameters are valid + uniqueIdentifier: TC_Mimoto_GetAllIssuers_01 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuers/GetAllIssuers + outputTemplate: mimoto/GetAllIssuers/GetAllIssuersResult + input: '{ +}' + output: '{ +}' + + Mimoto_GetAllIssuers_Specific_Issuer_All_Valid_Smoke: + endPoint: /v1/mimoto/issuers/Mosip + description: Validate that the system successfully retrieves the specific issuer details when all input parameters are valid + uniqueIdentifier: TC_Mimoto_GetAllIssuers_02 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuers/GetAllIssuers + outputTemplate: mimoto/GetAllIssuers/GetAllIssuersResult + input: '{ +}' + output: '{ +}' + + Mimoto_GetAllIssuers_Invalid_Issuer_Neg: + endPoint: /v1/mimoto/issuers/invalid + description: Validate that the system does not retrieve issuer details when an invalid issuer is provided + uniqueIdentifier: TC_Mimoto_GetAllIssuers_03 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuers/GetAllIssuers + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuersResult.hbs b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuersResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuers/GetAllIssuersResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.hbs b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.yml b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.yml new file mode 100644 index 000000000..b72310c8a --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2.yml @@ -0,0 +1,130 @@ +GetAllIssuersV2: + Mimoto_GetAllIssuersV2_All_Valid_Smoke: + endPoint: /v1/mimoto/v2/issuers + description: Validate that the system successfully retrieves issuer details when all input parameters are valid + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_01 + role: resident + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/responseCode + input: '{ +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_GetAllIssuersV2_SpecificIssuer_All_Valid_Smoke: + endPoint: /v1/mimoto/v2/issuers/Mosip + description: Validate that the system successfully retrieves the specific issuer details when all input parameters are valid + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_02 + role: resident + restMethod: get + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2Result + input: '{ +}' + output: '{ + "issuer_id": "Mosip" +}' + + Mimoto_GetAllIssuersV2_SpecificIssuer_InvalidIssuerID_Neg: + endPoint: /v1/mimoto/v2/issuers/invalid + description: Validate that the system does not retrieve issuer details when an invalid issuer is provided + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_03 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + Mimoto_GetAllIssuersV2_SpecificIssuer_Null_Issuer_Neg: + endPoint: /v1/mimoto/v2/issuers/Null + description: Validate that the system does not retrieve issuer details when an Null issuer is provided + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_04 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + Mimoto_GetAllIssuersV2_SpecificIssuer_SpecialCharacters_Issuer_Neg: + endPoint: /v1/mimoto/v2/issuers/@#$#@%# + description: Validate that the system does not retrieve issuer details when an Special Characters issuer is provided + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_05 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + Mimoto_GetAllIssuersV2_SpecificIssuer_caseSensitive_Issuer_Neg: + endPoint: /v1/mimoto/v2/issuers/mOSIP + description: Validate that the system does not retrieve issuer details when an issuer case does not match + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_06 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' + Mimoto_GetAllIssuersV2_AllIssuer_invalidURL_Neg: + endPoint: /v1/mimoto/v2/issuersInvalidURL + description: Validate that the system does not retrieve issuer details when an invalid URL is provided + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_07 + role: resident + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/responseCode + input: '{ +}' + output: '{ + "responseCode": "401" +}' + Mimoto_GetAllIssuersV2_AllIssuer_NullURL_Neg: + endPoint: /v1/mimoto/v2/null + description: Validate that the system does not retrieve issuer details when an null is provided in the URL + uniqueIdentifier: TC_Mimoto_GetAllIssuersV2_08 + role: resident + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/GetAllIssuersV2/GetAllIssuersV2 + outputTemplate: mimoto/responseCode + input: '{ +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2Result.hbs b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2Result.hbs new file mode 100644 index 000000000..1ecf56cfe --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetAllIssuersV2/GetAllIssuersV2Result.hbs @@ -0,0 +1,5 @@ +{ + "response": { + "issuer_id": "{{issuer_id}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.hbs b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.yml b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.yml new file mode 100644 index 000000000..07fbe08d3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfiguration.yml @@ -0,0 +1,32 @@ +GetIssuerConfiguration: + Mimoto_GetIssuerConfiguration_Mosip_All_Valid_Smoke: + endPoint: /v1/mimoto/issuers/Mosip/configuration + description: Verify that the system retrieves the verifiers list successfully when all parameters are valid + uniqueIdentifier: TC_Mimoto_GetIssuerConfiguration_01 + role: resident + restMethod: get + inputTemplate: mimoto/GetIssuerConfiguration/GetIssuerConfiguration + outputTemplate: mimoto/GetIssuerConfiguration/GetIssuerConfigurationResult + input: '{ +}' + output: '{ + "name": "MOSIPVerifiableCredential" +}' + Mimoto_GetIssuerConfiguration_With_Issuer_Removed_From_URL_Neg: + endPoint: /v1/mimoto/issuers/configuration + description: Validate that the system does not retrieve the verifiers list when an invalid URL is provided + uniqueIdentifier: TC_Mimoto_GetIssuerConfiguration_02 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetIssuerConfiguration/GetIssuerConfiguration + outputTemplate: mimoto/error + input: '{ +}' + output: '{ + "errors":[ + { + "errorCode":"RESIDENT-APP-035" + } + ] +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfigurationResult.hbs b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfigurationResult.hbs new file mode 100644 index 000000000..d57337fe0 --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerConfiguration/GetIssuerConfigurationResult.hbs @@ -0,0 +1,9 @@ +{ + "response": { + "credentials_supported": [ + { + "name": "{{name}}" + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.hbs b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.hbs new file mode 100644 index 000000000..71a599fbc --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.hbs @@ -0,0 +1,4 @@ +{ + "issuerId": "{{issuerId}}", + "credentialType": "{{credentialType}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.yml b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.yml new file mode 100644 index 000000000..a8e3158ad --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnown.yml @@ -0,0 +1,35 @@ +GetIssuerWellKnown: + Mimoto_GetIssuerWellKnown_All_Valid_Smoke: + endPoint: /v1/mimoto/issuers/{issuerId}/well-known-proxy + description: Verify that the system successfully generates the issuer well-known configuration with all valid parameters + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetIssuerWellKnown/GetIssuerWellKnown + outputTemplate: mimoto/GetIssuerWellKnown/GetIssuerWellKnownResult + input: '{ + "issuerId": "Mosip", + "credentialType": "$REMOVE$" +}' + output: '{ +}' + +# Mimoto_GetIssuerWellKnown_Invalid_Issuer_Neg: +# endPoint: /v1/mimoto/issuers/{issuerId}/well-known-proxy +# description: Verify that the system fails to generate the issuer well-known configuration with invalid parameters +# role: resident +# restMethod: get +# checkErrorsOnlyInResponse: true +# inputTemplate: mimoto/GetIssuerWellKnown/GetIssuerWellKnown +# outputTemplate: mimoto/error +# input: '{ +# "issuerId": "invalid", +# "credentialType": "$REMOVE$" +#}' +# output: '{ +# "errors":[ +# { +# "errorCode":"RESIDENT-APP-035" +# } +# ] +#}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnownResult.hbs b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnownResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetIssuerWellKnown/GetIssuerWellKnownResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.hbs b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.yml b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.yml new file mode 100644 index 000000000..d5d58b32f --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierList.yml @@ -0,0 +1,14 @@ +GetVerifierList: + Mimoto_GetVerifierList_All_Valid_Smoke: + endPoint: /v1/mimoto/verifiers + description: Verifying that the verifiers list is retrieved successfully with all valid parameters + uniqueIdentifier: TC_Mimoto_GetVerifierList_01 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/GetVerifierList/GetVerifierList + outputTemplate: mimoto/GetVerifierList/GetVerifierListResult + input: '{ +}' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierListResult.hbs b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierListResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/GetVerifierList/GetVerifierListResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.hbs new file mode 100644 index 000000000..d2939c083 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.hbs @@ -0,0 +1,3 @@ +{ + "cookie": "{{cookie}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.yml b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.yml new file mode 100644 index 000000000..a35f539f1 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken.yml @@ -0,0 +1,91 @@ +GoogleLoginToken: + Mimoto_GoogleLoginToken_all_Valid_Smoke: + endPoint: /v1/mimoto/auth/google/token-login + description: Verify and validate the session token generation during Google login + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_01 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$GOOGLE_IDT_TOKEN$" +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_GoogleLoginToken_Invalid_Provider_Neg: + endPoint: /v1/mimoto/auth/invalid/token-login + description: Verify the behavior when creating a session token for Google login with an invalid provider + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_02 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$GOOGLE_IDT_TOKEN$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_GoogleLoginToken_InvalidToken_Neg: + endPoint: /v1/mimoto/auth/google/token-login + description: Verify and validate that session token creation fails when Google login is attempted with an invalid IDT token + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_03 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/error2 + input: '{ + "cookie": "invalid" +}' + output: '{ + "errorCode": "invalid_token" +}' + + Mimoto_GoogleLoginToken_ExpiredToken_Neg: + endPoint: /v1/mimoto/auth/google/token-login + description: Verify and validate that session token creation fails when Google login is attempted with an expired token + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_04 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/error2 + input: '{ + "cookie": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFiYjc3NGJkODcyOWVhMzhlOWMyZmUwYzY0ZDJjYTk0OGJmNjZmMGYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxODg4MjgxNzM0OTktMHA0Zjk1Y2RxanM5aHR0bTEzMTBka2FlNmwzazNzam4uYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIxODg4MjgxNzM0OTktMHA0Zjk1Y2RxanM5aHR0bTEzMTBka2FlNmwzazNzam4uYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTAwODI5NzU4ODc2MDU2NjIyOTAiLCJlbWFpbCI6Im1vc2lwdGVzdGN5YmVycHduQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiZExSUmM1WVFZaW1TTXVjaFZWRXhpQSIsIm5hbWUiOiJNb3NpcCB0ZXN0IEN5YmVycHduIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0pZLWpRUE5UR3FLVHdiX3lOWHZ1c0FzSjVuSTBlcHhOczZWaWF2clVfd3hNMDdrZz1zOTYtYyIsImdpdmVuX25hbWUiOiJNb3NpcCB0ZXN0IiwiZmFtaWx5X25hbWUiOiJDeWJlcnB3biIsImlhdCI6MTc1MDc0NzExOSwiZXhwIjoxNzUwNzUwNzE5fQ.YGH3pgVvZoXyXS_eUPLmul5AXMdAdVaAoJRBJty_ZYBy7oKT7FGKwhJ9bLhXLiRG031noCaM-3EYv2N2oezvApuOGYM6xK5Mb6WdGzv-y0oR-00wniTTbuUEtgZGxPunZ2lfrI6WwkIq8JHhFh1_l4SnRGYfRqNA-vhLNMfcZsm0yG9NAt6TWgHEqy_JtMXSQTsvLfLijCLOqZCfQuTDqzk5EhtGaEMtppPN_au-Xc8fTDhY0nZtWTQ2262nqLXGSomika66dKH1SU50DtQEEdtz8Cbo_li5S4hSV1nDa7MzbIgiVqqN6fFalxRy4kvvvvaWNj7U46DsKjunINoNMQ" +}' + output: '{ + "errorCode": "invalid_token" +}' + + Mimoto_GoogleLoginToken_EmptyToken_Neg: + endPoint: /v1/mimoto/auth/google/token-login + description: Verify that the system fails to create a session token for Google login when the ID token is empty + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_05 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/error2 + input: '{ + "cookie": "" +}' + output: '{ + "errorCode": "invalid_request" +}' + Mimoto_GoogleLoginToken_all_ValidForLockedWallet_Smoke: + endPoint: /v1/mimoto/auth/google/token-login + description: Verify and validate the session token creation for Google login when the wallet is locked + uniqueIdentifier: TC_Mimoto_GoogleLoginToken_06 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginToken + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$GOOGLE_IDT_TOKEN$" +}' + output: '{ + "responseCode": "200" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginTokenResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginTokenResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/GoogleLoginToken/GoogleLoginTokenResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.hbs new file mode 100644 index 000000000..1c946c7fc --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.hbs @@ -0,0 +1,4 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.yml b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.yml new file mode 100644 index 000000000..97cac30c1 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/Logout.yml @@ -0,0 +1,51 @@ +Logout: + Mimoto_Logout_all_Valid_Smoke: + endPoint: /v1/mimoto/logout + description: Verify that the user is successfully logged out of the created session + uniqueIdentifier: TC_Mimoto_Logout_01 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Authorization/Logout/Logout + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_Logout_WithoutLogin_Neg: + endPoint: /v1/mimoto/logout + description: Verify that the logout request fails when a random cookie value is provided without creating a login session + uniqueIdentifier: TC_Mimoto_Logout_02 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Authorization/Logout/Logout + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "abcdef", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "404" +}' + + Mimoto_Logout_WithExpiredSession_Neg: + endPoint: /v1/mimoto/logout + description: Verify that the logout request fails when the login session has expired + uniqueIdentifier: TC_Mimoto_Logout_03 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Authorization/Logout/Logout + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "404" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/LogoutResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/LogoutResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Authorization/Logout/LogoutResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.hbs new file mode 100644 index 000000000..43ef2d127 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.hbs @@ -0,0 +1,15 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "challengeList" : [ + { + "authFactorType" : "{{authFactorType}}", + "challenge" : "{{challenge}}", + "format": "alpha-numeric" + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.yml new file mode 100644 index 000000000..0d982edc2 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser.yml @@ -0,0 +1,70 @@ +AuthenticateUserForGoogleLogin: + Mimoto_ESignet_AuthenticateUserIDPForGoogleLogin_uin_Otp_Valid_Smoke: + endPoint: /v1/esignet/authorization/authenticate + description: Verify that the user can authenticate successfully to download credentials with all valid parameters + uniqueIdentifier: TC_Mimoto_AuthenticateUserForGoogleLogin_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUserResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "authFactorType" : "OTP", + "challenge" : "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_EMAIL$", + "sendOtp":{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "otpChannels": [{channel: "email"},{channel: "phone"}], + "sendOtpReqTemplate": "mimoto/SendOtpEsignet/SendOtp", + "sendOtpEndPoint": "/v1/esignet/authorization/send-otp" + } + }' + output: '{ + "sendOtpResp":{ + "maskedMobile": "$IGNORE$", + "sendOtpResTemplate":"esignet/SendOtp/SendOtpResult", + "maskedEmail": "$IGNORE$" + } +}' + + Mimoto_ESignet_AuthenticateUserIDPForGoogleLogin_uin_Otp_Valid_ForNeg: + endPoint: /v1/esignet/authorization/authenticate + description: Verify that user authentication fails when attempting to download credentials with invalid parameters + uniqueIdentifier: TC_Mimoto_AuthenticateUserForGoogleLogin_02 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUser + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUserResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "authFactorType" : "OTP", + "challenge" : "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_EMAIL$", + "sendOtp":{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_transactionId$", + "individualId": "$ID:AddIdentity_downloadIssuerCredential_smoke_Pos_UIN$", + "otpChannels": [{channel: "email"},{channel: "phone"}], + "sendOtpReqTemplate": "mimoto/SendOtpEsignet/SendOtp", + "sendOtpEndPoint": "/v1/esignet/authorization/send-otp" + } + }' + output: '{ + "sendOtpResp":{ + "maskedMobile": "$IGNORE$", + "sendOtpResTemplate":"esignet/SendOtp/SendOtpResult", + "maskedEmail": "$IGNORE$" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUserResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUserResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthenticateUser/AuthenticateUserResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.hbs new file mode 100644 index 000000000..491dca1ba --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.hbs @@ -0,0 +1,19 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "acceptedClaims": [ + {{#each acceptedClaims}} + "{{claim}}" + {{#unless @last}},{{/unless}} + {{/each}} + ], + "permittedAuthorizeScopes": [ + {{#each permittedAuthorizeScopes}} + "{{scope}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.yml new file mode 100644 index 000000000..781ed1673 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode.yml @@ -0,0 +1,38 @@ +AuthorizationCodeForGoogleLogin: + Mimoto_ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_Smoke_sid: + endPoint: /v1/esignet/authorization/auth-code + description: Verify that an authorization code is successfully generated to download the credential from the issuer + uniqueIdentifier: TC_Mimoto_AuthorizationCodeForGoogleLogin_01 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_AuthenticateUserForGoogleLogin_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCodeResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid_transactionId$" +}' + output: '{ +}' + + Mimoto_ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg: + endPoint: /v1/esignet/authorization/auth-code + description: Verify that authorization code generation fails when invalid or negative conditions are applied while downloading the credential from the issuer + uniqueIdentifier: TC_Mimoto_AuthorizationCodeForGoogleLogin_02 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_AuthenticateUserForGoogleLogin_02 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCode + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCodeResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg_transactionId$" +}' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCodeResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCodeResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/AuthorizationCode/AuthorizationCodeResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.hbs new file mode 100644 index 000000000..06cd1f431 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "credentialId": "{{credentialId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.yml new file mode 100644 index 000000000..5922f88d9 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials.yml @@ -0,0 +1,147 @@ +DeleteCredentials: + Mimoto_DeleteCredentials_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that the system removes the specified credential from the wallet when requested + uniqueIdentifier: TC_Mimoto_DeleteCredentials_01 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadMultipleStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_DeleteCredentials_allReadyDeleted_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when the credential has already been removed from the wallet + uniqueIdentifier: TC_Mimoto_DeleteCredentials_02 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadMultipleStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "errorCode":"resource_not_found" +}' + + Mimoto_DeleteCredentials_Invalid_CredentialId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when an invalid credential ID is provided for the wallet + uniqueIdentifier: TC_Mimoto_DeleteCredentials_03 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "invalid" +}' + output: '{ + "errorCode":"resource_not_found" +}' + + Mimoto_DeleteCredentials_Invalid_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when an invalid wallet ID is provided + uniqueIdentifier: TC_Mimoto_DeleteCredentials_04 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalid", + "credentialId": "$ID:DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DeleteCredentials_EmptyOrSpace_CredentialId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when the credential ID is empty or contains only spaces + uniqueIdentifier: TC_Mimoto_DeleteCredentials_05 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": " " +}' + output: '{ + "errorCode":"resource_not_found" +}' + + Mimoto_DeleteCredentials_WithSession_Expired_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when the session has expired + uniqueIdentifier: TC_Mimoto_DeleteCredentials_07 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DeleteCredentials_WithSession_Expired_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that deleting a credential fails when the user is not logged in + uniqueIdentifier: TC_Mimoto_DeleteCredentials_08 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_DeleteCredentials_MultipleCredentialCreated_valid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId} + description: Verify that a specific credential can be successfully deleted from the wallet when multiple credentials exist + uniqueIdentifier: TC_Mimoto_DeleteCredentials_06 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentials + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$" +}' + output: '{ + "responseCode": "200" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentialsResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentialsResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DeleteCredentials/DeleteCredentialsResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.hbs new file mode 100644 index 000000000..c877ba377 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.hbs @@ -0,0 +1,11 @@ +{ + "grantType": "{{grantType}}", + "code": "{{code}}", + "redirectUri": "{{redirectUri}}", + "codeVerifier": "{{codeVerifier}}", + "issuer": "{{issuer}}", + "credentialConfigurationId": "{{credentialConfigurationId}}", + "walletId": "{{walletId}}", + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.yml new file mode 100644 index 000000000..f322f80c2 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential.yml @@ -0,0 +1,925 @@ +DownloadMosipIssuerCredentialWithGoogleLogin: + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the credential is downloaded successfully with all valid parameters using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "issuerDisplayName": "National Identity Department", + "credentialTypeDisplayName": "MOSIP National ID" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_DownloadSameVC_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the same credential again with all valid parameters using Google login fails + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_02 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"credential_download_error" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_With_Diff_Active_session_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential with all valid parameters using Google login fails when a different session is active + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_38 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_WithoutUnlock_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidWallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential with an invalid wallet ID using Google login fails + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_03 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "invalid-wallet-id", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInWallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential with a space value in the wallet ID using Google login fails + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_04 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": " ", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NullInWallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when a null value is provided in the wallet ID using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_05 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericValueInWallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when a numeric value is provided in the wallet ID using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_06 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "145672", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidGrantType_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when an invalid grant type is provided using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_07 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "invalid", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyGrantType_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when an empty grant type is provided using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_08 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInGrantType_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when a space value is provided in the grant type using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_09 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": " ", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingGrantType_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when the grant type is missing using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_10 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "$REMOVE$", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericGrantType_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading the credential fails when a numeric value is provided in the grant type using Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_11 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "5454453", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidCode_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential using an invalid code with Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_12 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "invalid", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyCode_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with an empty code using Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_13 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInCode_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential using a code with only spaces during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_14 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingCode_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential without providing a code during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_15 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$REMOVE$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericInCode_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential using a numeric-only code during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_16 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "7554432", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidRedirectUri_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with an invalid redirect URI during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_17 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "invalid", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyRedirectUri_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with an empty redirect URI during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_18 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInRedirectUri_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with a redirect URI containing only spaces during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_19 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": " ", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingRedirectUri_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential without providing a redirect URI during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_20 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$REMOVE$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericInRedirectUri_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with a numeric-only redirect URI during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_21 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "8654678", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidCodeVerifier_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with an invalid code verifier during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_22 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "invalid", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyCodeVerifier_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with an empty code verifier during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_23 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInCodeVerifier_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with a code verifier containing only spaces during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_24 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": " ", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingCodeVerifier_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential without providing a code verifier during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_25 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$REMOVE$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericInCodeVerifier_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential with a numeric-only code verifier during Google login fails and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_26 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "8665542", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidIssuer_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when an invalid issuer is provided even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_27 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "invalid", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyIssuer_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the issuer field is empty even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_28 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInIssuer_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the issuer contains spaces even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_29 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": " ", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingIssuer_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the issuer field is missing even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_30 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "$REMOVE$", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericInIssuer_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the issuer contains numeric values even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_31 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "86654556", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_InvalidCredential_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when an invalid credential is provided even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_32 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "invalid", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyCredential_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the credential field is empty even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_33 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_SpaceInCredential_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the credential contains spaces even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_34 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": " ", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_MissingCredential_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the credential field is missing even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_35 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "$REMOVE$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NumericInCredential_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the credential contains numeric values even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_36 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "655671754", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_EmptyRequestBody_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the request body is empty even with Google login + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_37 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "$REMOVE$", + "code": "$REMOVE$", + "redirectUri": "$REMOVE$", + "codeVerifier": "$REMOVE$", + "issuer": "$REMOVE$", + "credentialConfigurationId": "$REMOVE$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the user is not logged in with Google + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_39 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/responseCode + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "withoutLogin", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_WithSessionExpired_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that downloading a credential fails when the Google login session has expired + uniqueIdentifier: TC_Mimoto_DownloadIssuerCredentialWithGoogleLogin_40 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/responseCode + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_GoogleLogin_uin_All_Valid_sid_ForNeg_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "Mosip", + "credentialConfigurationId": "MOSIPVerifiableCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs new file mode 100644 index 000000000..757fa4a2e --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs @@ -0,0 +1,4 @@ +{ + "issuerDisplayName": "{{issuerDisplayName}}", + "credentialTypeDisplayName": "{{credentialTypeDisplayName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.hbs new file mode 100644 index 000000000..c3c22cf33 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "Accept-Language": "{{acceptLanguage}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.yml new file mode 100644 index 000000000..d74522707 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials.yml @@ -0,0 +1,126 @@ +FetchAllCredentials: + Mimoto_FetchAllCredentials_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the system retrieves the list of all credentials saved in the wallet + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_01 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "acceptLanguage": "en" +}' + output: '{ +}' + + Mimoto_FetchAllCredentials_Invalid_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when an invalid wallet ID is provided + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_02 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalid" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_FetchAllCredentials_Empty_WalletId_And_Empty_Accepted_claims_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when the wallet ID is empty + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_03 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_FetchAllCredentials_Space_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when the wallet ID contains spaces + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_04 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": " " +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_FetchAllCredentials_Invalid_Session_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when an invalid session is used + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_05 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "invalid", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_FetchAllCredentials_WithoutUnlock_Wallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when the wallet is not unlocked + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_06 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_WithoutUnlock_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_FetchAllCredentials_Expired_Session_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that retrieving the list of credentials fails when the session has expired + uniqueIdentifier: TC_Mimoto_FetchAllCredentials_07 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentials + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentialsResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentialsResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/FetchAllCredentials/FetchAllCredentialsResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.hbs new file mode 100644 index 000000000..6dec068b5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.hbs @@ -0,0 +1,17 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "clientId": "{{clientId}}", + "scope": "{{scope}}", + "responseType": "{{responseType}}", + "redirectUri": "{{redirectUri}}", + "display": "{{display}}", + "prompt": "{{prompt}}", + "acrValues": "{{acrValues}}", + "nonce" : "{{nonce}}", + "state" : "{{state}}", + "claimsLocales" : "{{claimsLocales}}", + "codeChallenge" : "{{codeChallenge}}", + "codeChallengeMethod" : "{{codeChallengeMethod}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.yml new file mode 100644 index 000000000..148bbd412 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest.yml @@ -0,0 +1,56 @@ +OAuthDetailsRequestForGoogleLogin: + Mimoto_ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_Smoke_sid: + endPoint: /v1/esignet/authorization/v2/oauth-details + description: Verify that the system successfully retrieves the authentication details for downloading the MOSIP identity credential when all valid parameters are provided + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequestForGoogleLogin_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequestResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORMOSIPIDFROMMIMOTOACTUATOR$", + "scope": "mosip_identity_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:generated-code mosip:idp:acr:linked-wallet mosip:idp:acr:biometrics", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' + + Mimoto_ESignet_OAuthDetailsRequest_GoogleLogin_all_Valid_sid_ForNeg: + endPoint: /v1/esignet/authorization/v2/oauth-details + description: Verify that creating OAuth details with invalid or missing parameters fails as expected + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequestForGoogleLogin_02 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequest + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequestResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORMOSIPIDFROMMIMOTOACTUATOR$", + "scope": "mosip_identity_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:generated-code mosip:idp:acr:linked-wallet mosip:idp:acr:biometrics", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.hbs new file mode 100644 index 000000000..a384c2855 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.hbs @@ -0,0 +1,8 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "credentialId": "{{credentialId}}", + "Accept-Language": "{{acceptLanguage}}", + "acceptHeader": "{{acceptHeader}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.yml new file mode 100644 index 000000000..10f6f9056 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential.yml @@ -0,0 +1,384 @@ +ViewCredential: + Mimoto_ViewCredential_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Validate that viewing a credential succeeds with a valid wallet ID and credential ID + uniqueIdentifier: TC_Mimoto_ViewCredential_01 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ +}' + + Mimoto_ViewCredential_MultipleCredential_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that another credential saved in the wallet can be viewed using its credential ID + uniqueIdentifier: TC_Mimoto_ViewCredential_02 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01,TC_Mimoto_AuthorizationCodeSunBirdC_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "$REMOVE$", + "acceptHeader": "application/pdf" +}' + output: '{ +}' + + Mimoto_ViewCredential_DownloadCredential_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=download + description: Verify that the credential saved in the wallet can be successfully downloaded using the credential ID + uniqueIdentifier: TC_Mimoto_ViewCredential_03 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ +}' + + Mimoto_ViewCredential_EmptyQueryParam_all_Valid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action= + description: Verify the behavior when viewing a credential using the credential ID while the action query parameter is empty + uniqueIdentifier: TC_Mimoto_ViewCredential_04 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ +}' + + Mimoto_ViewCredential_SpaceQueryParam_all_Valid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action= + description: Verify the behavior when viewing a credential using the credential ID while the action query parameter contains only spaces + uniqueIdentifier: TC_Mimoto_ViewCredential_05 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ +}' + + Mimoto_ViewCredential_InvalidQueryParam_all_Valid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=invalid + description: Verify the behavior when viewing a credential using the credential ID while the action query parameter is invalid + uniqueIdentifier: TC_Mimoto_ViewCredential_06 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "400" +}' + + Mimoto_ViewCredential_Invalid_CredentialId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that the system rejects the request to view a credential when an invalid credential ID is used + uniqueIdentifier: TC_Mimoto_ViewCredential_07 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "invalid", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "errorCode": "resource_not_found" +}' + + Mimoto_ViewCredential_Empty_CredentialId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the credential ID is empty + uniqueIdentifier: TC_Mimoto_ViewCredential_08 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "404" +}' + + Mimoto_ViewCredential_SpaceIn_CredentialId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the credential ID contains spaces + uniqueIdentifier: TC_Mimoto_ViewCredential_09 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": " ", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "400" +}' + + Mimoto_ViewCredential_Invalid_Wallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when an invalid wallet ID is provided + uniqueIdentifier: TC_Mimoto_ViewCredential_10 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalid", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_ViewCredential_Empty_Wallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the wallet ID is empty + uniqueIdentifier: TC_Mimoto_ViewCredential_11 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "404" +}' + + Mimoto_ViewCredential_SpaceIn_Wallet_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the wallet ID contains spaces + uniqueIdentifier: TC_Mimoto_ViewCredential_12 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": " ", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "400" +}' + + Mimoto_ViewCredential_Invalid_AcceptLanguage_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that the system rejects the request to view a credential when the Accept-Language header is invalid + uniqueIdentifier: TC_Mimoto_ViewCredential_13 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "oo", + "acceptHeader": "application/pdf" +}' + output: '{ + "errorCode": "credential_fetch_error" +}' + + Mimoto_ViewCredential_SpaceIn_AcceptLanguage_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the Accept-Language header contains spaces + uniqueIdentifier: TC_Mimoto_ViewCredential_14 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": " ", + "acceptHeader": "application/pdf" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_ViewCredential_Invalid_AcceptHeader_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential saved in the wallet with an invalid Accept header fails as expected + uniqueIdentifier: TC_Mimoto_ViewCredential_15 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "invalid" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_ViewCredential_Without_AcceptHeader_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the Accept header is not provided + uniqueIdentifier: TC_Mimoto_ViewCredential_16 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "$REMOVE$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_ViewCredential_SpaceIn_AcceptHeader_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that the system rejects the request to view a credential when the Accept header contains spaces + uniqueIdentifier: TC_Mimoto_ViewCredential_17 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": " " +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_ViewCredential_WithExpired_Session_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when an expired token is used + uniqueIdentifier: TC_Mimoto_ViewCredential_18 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialAfterLogout.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialAfterLogout.yml new file mode 100644 index 000000000..d1fa3a100 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialAfterLogout.yml @@ -0,0 +1,21 @@ +ViewCredentialAfterLogout: + Mimoto_ViewCredential_AfterLogout_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials/{credentialId}?action=inline + description: Verify that viewing a credential fails when the user has logged out of the wallet + uniqueIdentifier: TC_Mimoto_ViewCredential_19 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredential + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "credentialId": "$ID:DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_All_Valid_Smoke_sid_credentialId$", + "acceptLanguage": "en", + "acceptHeader": "application/pdf" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadMosipIssuerCredential/ViewCredential/ViewCredentialResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs new file mode 100644 index 000000000..d58a65555 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs @@ -0,0 +1,15 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "challengeList" : [ + { + "authFactorType" : "{{authFactorType}}", + "challenge" : "{{challenge}}", + "format": "{{format}}" + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml new file mode 100644 index 000000000..faccf7c1d --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml @@ -0,0 +1,46 @@ +AuthenticateUserSunBirdCForGoogleLogin: + Mimoto_ESignet_AuthenticateUserSunBirdCForGoogleLogin_SunBirdC_Valid_Smoke: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/authenticate + description: Verify successful user authentication for downloading the StayProtected credential through Google login + uniqueIdentifier: TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_01 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_CreatePolicySunBirdR_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_all_Valid_Smoke_sid_transactionId$", + "individualId": "$POLICYNUMBERFORSUNBIRDRC$", + "authFactorType" : "$SUNBIRDINSURANCEAUTHFACTORTYPE$", + "challenge" : "$CHALLENGEVALUEFORSUNBIRDC$", + "format": "base64url-encoded-json" + }' + output: '{ +}' + + Mimoto_ESignet_AuthenticateUserSunBirdCForGoogleLogin_SunBirdC_MultipleVC_Valid_Smoke: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/authenticate + description: Verify that the user is successfully authenticated to download the StayProtected credential using Google login + uniqueIdentifier: TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_02 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_CreatePolicySunBirdR_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_MultipleVC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_MultipleVC_all_Valid_Smoke_sid_transactionId$", + "individualId": "$POLICYNUMBERFORSUNBIRDRC$", + "authFactorType" : "$SUNBIRDINSURANCEAUTHFACTORTYPE$", + "challenge" : "$CHALLENGEVALUEFORSUNBIRDC$", + "format": "base64url-encoded-json" + }' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs new file mode 100644 index 000000000..491dca1ba --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs @@ -0,0 +1,19 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "acceptedClaims": [ + {{#each acceptedClaims}} + "{{claim}}" + {{#unless @last}},{{/unless}} + {{/each}} + ], + "permittedAuthorizeScopes": [ + {{#each permittedAuthorizeScopes}} + "{{scope}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml new file mode 100644 index 000000000..41bcce516 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml @@ -0,0 +1,40 @@ +AuthorizationCodeSunBirdCForGoogleLogin: + Mimoto_ESignet_AuthorizationCode_SunBirdCForGoogleLogin_SunBirdC_All_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/auth-code + description: Verify that an authorization code is successfully generated for downloading the Sunbird VC using Google login + uniqueIdentifier: TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_01 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_all_Valid_Smoke_sid_transactionId$", + "permittedAuthorizeScopes": [{scope: "sunbird_rc_insurance_vc_ldp"}] +}' + output: '{ +}' + + Mimoto_ESignet_AuthorizationCode_SunBirdCForGoogleLogin_MultipleVC_SunBirdC_All_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/auth-code + description: Verify that an authorization code is successfully generated for downloading multiple Sunbird VCs using Google login + uniqueIdentifier: TC_Mimoto_AuthorizationCodeSunBirdCForGoogleLogin_02 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_AuthenticateUserSunBirdCForGoogleLogin_02 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_MultipleVC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_MultipleVC_all_Valid_Smoke_sid_transactionId$", + "permittedAuthorizeScopes": [{scope: "sunbird_rc_insurance_vc_ldp"}] +}' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.hbs new file mode 100644 index 000000000..b358f636c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.hbs @@ -0,0 +1,12 @@ +{ + "grantType": "{{grantType}}", + "code": "{{code}}", + "redirectUri": "{{redirectUri}}", + "codeVerifier": "{{codeVerifier}}", + "issuer": "{{issuer}}", + "credentialConfigurationId": "{{credentialConfigurationId}}", + "walletId": "{{walletId}}", + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "Accept-Language": "{{acceptLanguage}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.yml new file mode 100644 index 000000000..8df777d44 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential.yml @@ -0,0 +1,127 @@ +DownloadStayProtectedIssuerCredentialWithGoogleLogin: + Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the Sunbird credential is successfully downloaded using a valid authorization code after Google login + uniqueIdentifier: TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_01 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdCForGoogleLogin_SunBirdC_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credentialConfigurationId": "InsuranceCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "acceptLanguage": "en" +}' + output: '{ + "issuerDisplayName": "StayProtected Insurance", + "credentialTypeDisplayName": "Health Insurance" +}' + + Mimoto_DownloadMultipleStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_All_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify the behavior when attempting to download multiple Sunbird credentials for a policy that has already been downloaded using a valid authorization code after Google login + uniqueIdentifier: TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_02 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredentialResult + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdCForGoogleLogin_MultipleVC_SunBirdC_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credentialConfigurationId": "InsuranceCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "acceptLanguage": "" +}' + output: '{ + "issuerDisplayName": "StayProtected Insurance", + "credentialTypeDisplayName": "Health Insurance" +}' + + Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_InvalidLocale_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the Sunbird credential download fails when an invalid locale is provided even with a valid authorization code after Google login + uniqueIdentifier: TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_03 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdCForGoogleLogin_SunBirdC_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credentialConfigurationId": "InsuranceCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "acceptLanguage": "oo" +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_SpaceInLocale_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the Sunbird credential download fails when the locale contains spaces even with a valid authorization code after Google login + uniqueIdentifier: TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_04 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdCForGoogleLogin_SunBirdC_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credentialConfigurationId": "InsuranceCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "acceptLanguage": " " +}' + output: '{ + "errorCode":"invalid_request" +}' + + Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_SunBirdC_NumericInLocale_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/credentials + description: Verify that the Sunbird credential download fails when a numeric value is provided in the locale even with a valid authorization code after Google login + uniqueIdentifier: TC_Mimoto_DownloadStayProtectedIssuerCredentialWithGoogleLogin_05 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredential + outputTemplate: mimoto/error2 + input: '{ + "grantType": "authorization_code", + "code": "$ID:ESignet_AuthorizationCode_SunBirdCForGoogleLogin_SunBirdC_All_Valid_Smoke_sid_code$", + "redirectUri": "$INJIREDIRECTURI$", + "codeVerifier": "$CODEVERIFIER$", + "issuer": "StayProtected", + "credentialConfigurationId": "InsuranceCredential", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "acceptLanguage": "14" +}' + output: '{ + "errorCode":"invalid_request" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs new file mode 100644 index 000000000..757fa4a2e --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/DownloadIssuerCredential/DownloadIssuerCredentialResult.hbs @@ -0,0 +1,4 @@ +{ + "issuerDisplayName": "{{issuerDisplayName}}", + "credentialTypeDisplayName": "{{credentialTypeDisplayName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs new file mode 100644 index 000000000..6dec068b5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs @@ -0,0 +1,17 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "clientId": "{{clientId}}", + "scope": "{{scope}}", + "responseType": "{{responseType}}", + "redirectUri": "{{redirectUri}}", + "display": "{{display}}", + "prompt": "{{prompt}}", + "acrValues": "{{acrValues}}", + "nonce" : "{{nonce}}", + "state" : "{{state}}", + "claimsLocales" : "{{claimsLocales}}", + "codeChallenge" : "{{codeChallenge}}", + "codeChallengeMethod" : "{{codeChallengeMethod}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml new file mode 100644 index 000000000..8858280dd --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml @@ -0,0 +1,56 @@ +OAuthDetailsRequestSunBirdCForGoogleLogin: + Mimoto_ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_all_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/v2/oauth-details + description: Verify that authentication details are successfully retrieved for downloading Sunbird credentials using Google login + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORINSURANCEFROMMIMOTOACTUATOR$", + "scope": "sunbird_rc_insurance_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:knowledge", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' + + Mimoto_ESignet_OAuthDetailsRequest_GoogleLogin_SunBirdC_MultipleVC_all_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/v2/oauth-details + description: Verify that authentication details are successfully retrieved for downloading multiple Sunbird credentials using Google login + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequestSunBirdCForGoogleLogin_02 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC + outputTemplate: mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORINSURANCEFROMMIMOTOACTUATOR$", + "scope": "sunbird_rc_insurance_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:knowledge", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/DownloadStayProtectedIssuerCredentialWithGoogleLogin/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.hbs new file mode 100644 index 000000000..68b63e951 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "verifierId": "{{verifierId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.yml new file mode 100644 index 000000000..10dd15aec --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier.yml @@ -0,0 +1,134 @@ +AddTrustedVerifier: + Mimoto_AddTrustedVerifier_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Validate adding a trusted verifier with valid data and verify that the operation succeeds + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_01 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifierResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ +}' + + Mimoto_AddTrustedVerifier_withEmpty_verifierId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier without a verifierId is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_02 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "verifierId": "" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_AddTrustedVerifier_withInvalid_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier using an invalid walletId is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_03 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalidWalletID", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_AddTrustedVerifier_withEmptyWalletID_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier without a walletId is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_04 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + + Mimoto_AddTrustedVerifier_withLocked_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier with a locked walletId is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_05 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_ValidForLockedWallet_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_ValidForLockedWallet_Smoke_sid_walletId$", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ + "errorCode": "wallet_locked" +}' + + Mimoto_AddTrustedVerifier_withInvalidSession_unAuthorized_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier using an invalid or expired session is not allowed and returns an appropriate authentication error + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_06 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_AddTrustedVerifier_all_Valid_duplicateVerifierID_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/trusted-verifiers + description: Verify that adding a trusted verifier with an already existing verifier ID is not allowed and returns an appropriate duplicate entry error + uniqueIdentifier: TC_Mimoto_AddTrustedVerifier_07 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifier + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "verifierId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_id$" +}' + output: '{ + "errorCode": "duplicate_verifier" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifierResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifierResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/AddTrustedVerifier/AddTrustedVerifierResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.hbs new file mode 100644 index 000000000..5099ba9d0 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "walletId": "{{walletId}}", + "presentationId": "{{presentationId}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.yml new file mode 100644 index 000000000..de47c65b4 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation.yml @@ -0,0 +1,183 @@ +GetCredentialsForPresentation: + Mimoto_GetCredentialsForPresentation_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Verify that the system successfully returns the credentials list when valid presentation ID and wallet ID are provided + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_01 + role: userDefinedCookie + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentationResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ +}' + + Mimoto_GetCredentialsForPresentation_invalidWalletID_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Verify that fetching the credentials list with an invalid wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_02 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "invalidID", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request" +}' + Mimoto_GetCredentialsForPresentation_invalidPresentationID_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Validate retrieval of the credentials list using an invalid presentation ID and verify that the operation fails + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_03 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "invalidPresentationID", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" +}' + + Mimoto_GetCredentialsForPresentation_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Verify that fetching the credentials list without a valid login session is not allowed and returns an appropriate authentication error + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_04 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_GetCredentialsForPresentation_WithExpiredSession_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Validate retrieval of the credentials list with an expired login session and verify that the operation fails + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_05 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_GetCredentialsForPresentation_emptyWalletID_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Verify that fetching the credentials list without providing a wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_06 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_GetCredentialsForPresentation_invalidWalletIdAndPresentationID_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Verify that fetching the credentials list with both invalid wallet ID and presentation ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_07 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "invalidWalletID", + "presentationId": "invalidPresentationId", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_GetCredentialsForPresentation_emptyStringWalletId_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Validate retrieval of the credentials list when the wallet ID is an empty string and verify that the operation fails + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_08 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": " ", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_GetCredentialsForPresentation_lockedWallet_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Validate retrieval of the credentials list using a locked wallet and verify that the operation fails + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_09 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_ValidForLockedWallet_Smoke_sessionCookie$", + "walletId": "$ID:CreateWallet_all_ValidForLockedWallet_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "wallet_locked" +}' + + Mimoto_GetCredentialsForPresentation_emptyPresentationID_Smoke_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId}/credentials + description: Validate retrieval of the credentials list when the presentation ID field is empty and verify that the operation fails + uniqueIdentifier: TC_Mimoto_GetCredentialsForPresentation_10 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": " ", + "cookieName": "SESSION" +}' + output: '{ + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentationResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentationResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/GetCredentialsForPresentation/GetCredentialsForPresentationResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.hbs new file mode 100644 index 000000000..0421e2f39 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.hbs @@ -0,0 +1,62 @@ +{ + "clientId": "{{clientId}}", + "presentationDefinition": { + "id": "c4822b58-7fb4-454e-b827-f8758fe27f9a", + "purpose": "Relying party is requesting your digital ID for the purpose of Self-Authentication", + "format": { + "ldp_vc": { + "proof_type": [ + "Ed25519Signature2020" + ] + } + }, + "input_descriptors": [ + { + "id": "id card credential", + "format": { + "ldp_vc": { + "proof_type": [ + "RsaSignature2018" + ] + } + }, + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "object", + "pattern": "MOSIPVerifiableCredential" + } + } + ] + } + }, + { + "id": "id card credential", + "format": { + "ldp_vc": { + "proof_type": [ + "Ed25519Signature2020" + ] + } + }, + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "object", + "pattern": "InsuranceCredential" + } + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.yml new file mode 100644 index 000000000..023458f48 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest.yml @@ -0,0 +1,15 @@ +InjiVerifyVpRequest: + InjiVerify_InjiVerifyVpRequest_all_Valid_Smoke_sid: + endPoint: $INJIVERIFYBASEURL$/v1/verify/vp-request + description: Verify the creation of a Verifiable Presentation (VP) request as a prerequisite for generating the Verifiable Presentation + uniqueIdentifier: TC_InjiVerify_InjiVerifyVpRequest_01 + restMethod: post + role: noauth + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequest + outputTemplate: mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequestResult + input: '{ + "clientId":"$CLIENT_ID_INJI_VERIFY$" + }' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequestResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequestResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/InjiVerifyVpRequest/InjiVerifyVpRequestResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.hbs new file mode 100644 index 000000000..1f2766b2d --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.hbs @@ -0,0 +1,8 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "presentationId": "{{presentationId}}", + "errorCode": "{{errorCode}}", + "errorMessage": "{{errorMessage}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.yml new file mode 100644 index 000000000..d66932f3c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation.yml @@ -0,0 +1,141 @@ +RejectPresentation: + Mimoto_RejectPresentation_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Validate that a presentation can be successfully rejected with user-denied authorization + uniqueIdentifier: TC_Mimoto_RejectPresentation_01 + role: userDefinedCookie + restMethod: patch + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentationResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ +}' + + Mimoto_RejectPresentation_invalidWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Validate that the request returns an invalid_request error when the wallet ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_02 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalidWalletID", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_emptyWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Validate that the request returns an invalid_request error when the wallet ID is empty + uniqueIdentifier: TC_Mimoto_RejectPresentation_03 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_invalidPresentationId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request returns an invalid_request error when the presentation ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_04 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "invalidPresentationId", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_emptyPresentationId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request returns an invalid_request error when the presentation ID is empty + uniqueIdentifier: TC_Mimoto_RejectPresentation_05 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_lockedWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request returns a wallet_locked error when the wallet ID is locked + uniqueIdentifier: TC_Mimoto_RejectPresentation_06 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_ValidForLockedWallet_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_ValidForLockedWallet_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "wallet_locked" +}' + + Mimoto_RejectPresentation_invalidSession_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request returns HTTP 401 when the session ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_07 + role: userDefinedCookie + checkOnlyStatusCodeInResponse: true + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentationResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentationResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentationResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.hbs new file mode 100644 index 000000000..1f2766b2d --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.hbs @@ -0,0 +1,8 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "presentationId": "{{presentationId}}", + "errorCode": "{{errorCode}}", + "errorMessage": "{{errorMessage}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.yml new file mode 100644 index 000000000..3943fb1ce --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentation.yml @@ -0,0 +1,141 @@ +RejectPresentation: + Mimoto_RejectPresentation_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Invalid Request error + uniqueIdentifier: TC_Mimoto_RejectPresentation_01 + role: userDefinedCookie + restMethod: patch + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentationResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ +}' + + Mimoto_RejectPresentation_invalidWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Invalid Request error when the wallet ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_02 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalidWalletID", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_emptyWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Invalid Request error when the wallet ID is empty + uniqueIdentifier: TC_Mimoto_RejectPresentation_03 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_invalidPresentationId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Invalid Request error when the presentation ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_04 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "invalidPresentationId", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_emptyPresentationId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Invalid Request error when the presentation ID is empty + uniqueIdentifier: TC_Mimoto_RejectPresentation_05 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_RejectPresentation_lockedWalletID_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with a Wallet Locked error when the wallet ID is locked + uniqueIdentifier: TC_Mimoto_RejectPresentation_06 + role: userDefinedCookie + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_WithoutUnlock_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "errorCode": "wallet_locked" +}' + + Mimoto_RejectPresentation_invalidSession_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations/{presentationId} + description: Verify that the request is rejected with an Access Denied error when the session ID is invalid + uniqueIdentifier: TC_Mimoto_RejectPresentation_07 + role: userDefinedCookie + checkOnlyStatusCodeInResponse: true + restMethod: patch + inputTemplate: mimoto/LoginFlow/Presentations/RejectPresentation/RejectPresentation + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "presentationId": "$ID:VerifiablePresentationAuthorization_all_Valid_Smoke_sid_presentationId$", + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +}' + output: '{ + "responseCode": "401" +}' diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentationResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentationResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/SubmitPresentation/SubmitPresentationResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.hbs new file mode 100644 index 000000000..638d5b618 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "authorizationRequestUrl": "{{authorizationRequestUrl}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.yml b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.yml new file mode 100644 index 000000000..689bf5acc --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization.yml @@ -0,0 +1,114 @@ +VerifiablePresentationAuthorization: + Mimoto_VerifiablePresentationAuthorization_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that a Verifiable Presentation is successfully created when valid data is provided + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_01 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorizationResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "authorizationRequestUrl": "$AUTHORIZATION_REQUEST_URL$$ID:InjiVerifyVpRequest_all_Valid_Smoke_sid_requestId$" +}' + output: '{ +}' + + Mimoto_VerifiablePresentationAuthorization_withInvalid_authorizationRequestUrl_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that the request to create a Verifiable Presentation is rejected or fails when the authorizationRequestUrl is invalid + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_02 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "authorizationRequestUrl": "invalidRequest" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_VerifiablePresentationAuthorization_withInvalid_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that the request to create a Verifiable Presentation is rejected when the wallet ID is invalid + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_03 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalidWalletID", + "authorizationRequestUrl": "$AUTHORIZATION_REQUEST_URL$$ID:InjiVerifyVpRequest_all_Valid_Smoke_sid_requestId$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_VerifiablePresentationAuthorization_withLocked_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that the request to create a Verifiable Presentation is rejected when the wallet ID is locked + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_04 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_ValidForLockedWallet_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_ValidForLockedWallet_Smoke_sid_walletId$", + "authorizationRequestUrl": "$AUTHORIZATION_REQUEST_URL$$ID:InjiVerifyVpRequest_all_Valid_Smoke_sid_requestId$" +}' + output: '{ + "errorCode": "wallet_locked" +}' + + Mimoto_VerifiablePresentationAuthorization_withInvalidSession_unAuthorized_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that the request to create a Verifiable Presentation is rejected when the session ID is invalid + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_05 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "authorizationRequestUrl": "$AUTHORIZATION_REQUEST_URL$$ID:InjiVerifyVpRequest_all_Valid_Smoke_sid_requestId$" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_VerifiablePresentationAuthorization_withNullWalletID_walletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/presentations + description: Verify that the request to create a Verifiable Presentation is rejected when the wallet ID is "null". + uniqueIdentifier: TC_Mimoto_VerifiablePresentationAuthorization_06 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_UnlockWallet_01 + inputTemplate: mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorization + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "null", + "authorizationRequestUrl": "$AUTHORIZATION_REQUEST_URL$$ID:InjiVerifyVpRequest_all_Valid_Smoke_sid_requestId$" +}' + output: '{ + "errorCode": "invalid_request" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorizationResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorizationResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Presentations/VerifiablePresentationAuthorization/VerifiablePresentationAuthorizationResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.hbs new file mode 100644 index 000000000..1c946c7fc --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.hbs @@ -0,0 +1,4 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.yml b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.yml new file mode 100644 index 000000000..80a919610 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile.yml @@ -0,0 +1,17 @@ +GetUserProfile: + Mimoto_GetUserProfile_all_Valid_Smoke: + endPoint: /v1/mimoto/users/me + description: Verify that the system successfully returns user profile details when requested with a valid login session + uniqueIdentifier: TC_Mimoto_GetUserProfile_01 + role: userDefinedCookie + restMethod: get + inputTemplate: mimoto/LoginFlow/Users/GetUserProfile/GetUserProfile + outputTemplate: mimoto/LoginFlow/Users/GetUserProfile/GetUserProfileResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "displayName": "Mosip test Cyberpwn", + "email": "mosiptestcyberpwn@gmail.com" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfileResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfileResult.hbs new file mode 100644 index 000000000..2155b167a --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfile/GetUserProfileResult.hbs @@ -0,0 +1,4 @@ +{ + "displayName": "{{displayName}}", + "email": "{{email}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.hbs new file mode 100644 index 000000000..1c946c7fc --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.hbs @@ -0,0 +1,4 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.yml b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.yml new file mode 100644 index 000000000..af9bb4af5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout.yml @@ -0,0 +1,18 @@ +GetUserProfileAfterLogout: + Mimoto_GetUserProfileAfterLogout_all_Valid_Smoke: + endPoint: /v1/mimoto/users/me + description: Verify that fetching user profile after logout returns unauthorized response 401 + uniqueIdentifier: TC_Mimoto_GetUserProfileAfterLogout_01 + role: userDefinedCookie + restMethod: get + additionalDependencies: TC_Mimoto_Logout_01 + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogout + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogoutResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogoutResult.hbs new file mode 100644 index 000000000..2155b167a --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Users/GetUserProfileAfterLogout/GetUserProfileAfterLogoutResult.hbs @@ -0,0 +1,4 @@ +{ + "displayName": "{{displayName}}", + "email": "{{email}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.hbs new file mode 100644 index 000000000..e8506e57c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.hbs @@ -0,0 +1,7 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletName": "{{walletName}}", + "walletPin": "{{walletPin}}", + "confirmWalletPin": "{{confirmWalletPin}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.yml new file mode 100644 index 000000000..5a29fab84 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet.yml @@ -0,0 +1,440 @@ +CreateWallet: + Mimoto_CreateWallet_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets + description: Verifying that a new wallet is created successfully after a successful login + uniqueIdentifier: TC_Mimoto_CreateWallet_01 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWallet", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "AutomationWallet" +}' + + Mimoto_CreateWallet_WithoutUnlock_all_Valid_Smoke_sid: + endPoint: /v1/mimoto/wallets + description: Verifying that a new wallet is created successfully for the same user after a successful login + uniqueIdentifier: TC_Mimoto_CreateWallet_02 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletWithoutUnlock", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "AutomationWalletWithoutUnlock" +}' + + Mimoto_CreateWallet_SameName_Valid: + endPoint: /v1/mimoto/wallets + description: Verifying that the system handles the creation of a new wallet with the same name for the same user after a successful login + uniqueIdentifier: TC_Mimoto_CreateWallet_03 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWallet", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "AutomationWallet" +}' + + Mimoto_CreateWallet_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets + description: Validating that wallet creation without login is not allowed + uniqueIdentifier: TC_Mimoto_CreateWallet_04 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg1", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_CreateWallet_SessionExpired_Neg: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the user session is expired + uniqueIdentifier: TC_Mimoto_CreateWallet_05 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg2", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_CreateWallet_InvalidWalletName_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with an invalid wallet name is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_06 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "********", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_EmptyWalletName_Neg: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the wallet name is empty + uniqueIdentifier: TC_Mimoto_CreateWallet_07 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_SpaceInWalletName_Neg: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the wallet name contains only spaces + uniqueIdentifier: TC_Mimoto_CreateWallet_08 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": " ", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_MissingWalletName_Valid: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the wallet name is missing + uniqueIdentifier: TC_Mimoto_CreateWallet_09 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "$REMOVE$", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_CreateWallet_MoreThan50Characters_WalletName_Neg: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the wallet name exceeds 50 characters + uniqueIdentifier: TC_Mimoto_CreateWallet_10 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_WalletNameWithSpaces_valid: + endPoint: /v1/mimoto/wallets + description: Verifying that the system prevents the creation of a new wallet when the wallet name exceeds 50 characters + uniqueIdentifier: TC_Mimoto_CreateWallet_11 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "Automation Wallet", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "Automation Wallet" +}' + + Mimoto_CreateWallet_WalletNameInSpecialCharacters_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with special characters in the wallet name is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_12 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "%$!*&^", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_WalletNameInNumbers_Valid: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with numbers in the wallet name is allowed and the wallet is created successfully + uniqueIdentifier: TC_Mimoto_CreateWallet_13 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "5675441", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "5675441" +}' + + Mimoto_CreateWallet_MoreThan6Digits_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with a PIN longer than 6 digits is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_14 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg3", + "walletPin": "12345678", + "confirmWalletPin": "12345678" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_Empty_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet without a wallet PIN is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_15 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "", + "confirmWalletPin": "" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_Space_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with spaces in the wallet PIN is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_16 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": " ", + "confirmWalletPin": " " +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_Missing_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet without providing a wallet PIN is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_17 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "$REMOVE$", + "confirmWalletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_invalid_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with an invalid wallet PIN is not allowed and returns an appropriate error. + uniqueIdentifier: TC_Mimoto_CreateWallet_18 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "12345", + "confirmWalletPin": "12345" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_StringValue_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with alphabetic characters in the wallet PIN is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_19 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "abcdef", + "confirmWalletPin": "abcdef" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_SpecialCharacters_WalletPin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet with special characters in the wallet PIN is not allowed and returns an appropriate error + uniqueIdentifier: TC_Mimoto_CreateWallet_20 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "^%$@*!", + "confirmWalletPin": "^%$@*!" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_WalletPin_ConfirmWalletPin_Mismatch_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet fails when the wallet PIN and confirm wallet PIN values mismatch, returning an appropriate error. + uniqueIdentifier: TC_Mimoto_CreateWallet_21 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWalletNeg4", + "walletPin": "123456", + "confirmWalletPin": "654321" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_Without_WalletPin_And_WalletName_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that creating a new wallet fails when the wallet PIN and wallet name are missing, returning appropriate error messages. + uniqueIdentifier: TC_Mimoto_CreateWallet_22 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "$REMOVE$", + "walletPin": "$REMOVE$", + "confirmWalletPin": "$REMOVE$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_CreateWallet_all_ValidForLockedWallet_Smoke_sid: + endPoint: /v1/mimoto/wallets + description: Validate wallet creation after a successful login when the previous wallet is in a locked state + uniqueIdentifier: TC_Mimoto_CreateWallet_23 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWallet + outputTemplate: mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_ValidForLockedWallet_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletName": "AutomationWallet", + "walletPin": "123456", + "confirmWalletPin": "123456" +}' + output: '{ + "walletName": "AutomationWallet" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult.hbs new file mode 100644 index 000000000..c6fe5ba21 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/CreateWallet/CreateWalletResult.hbs @@ -0,0 +1,3 @@ +{ + "walletName": "{{walletName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.hbs new file mode 100644 index 000000000..3ed69cbaa --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.hbs @@ -0,0 +1,5 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.yml new file mode 100644 index 000000000..d5b393c3f --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet.yml @@ -0,0 +1,122 @@ +UnlockWallet: + Mimoto_DeleteWallet_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that the user is able to delete an already created wallet successfully + uniqueIdentifier: TC_Mimoto_DeleteWallet_01 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "responseCode": "200" +}' + + Mimoto_DeleteWallet_AlreadyDeleted_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting an already deleted wallet is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DeleteWallet_02 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_DeleteWallet_Invalid_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting a wallet with an invalid wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DeleteWallet_03 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalid" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_DeleteWallet_Empty_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting a wallet without providing a wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DeleteWallet_04 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_DeleteWallet_Space_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting a wallet with spaces in the wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_DeleteWallet_05 + role: userDefinedCookie + restMethod: delete + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": " " +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_DeleteWallet_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting a wallet without a valid login session is not allowed and returns an appropriate authentication error + uniqueIdentifier: TC_Mimoto_DeleteWallet_06 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_DeleteWallet_ExpiredSession_Neg: + endPoint: /v1/mimoto/wallets/{walletId} + description: Verify that deleting a wallet with an expired session fails as expected + uniqueIdentifier: TC_Mimoto_DeleteWallet_07 + role: userDefinedCookie + restMethod: delete + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + output: '{ + "responseCode": "401" +}' diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWalletResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWalletResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/DeleteWallet/DeleteWalletResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.hbs new file mode 100644 index 000000000..1c946c7fc --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.hbs @@ -0,0 +1,4 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.yml new file mode 100644 index 000000000..f44051040 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWallets.yml @@ -0,0 +1,50 @@ +GetWallets: + Mimoto_GetWallets_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets + description: Verify that the system successfully fetches and displays the list of all wallets stored in the database + uniqueIdentifier: TC_Mimoto_GetWallets_01 + role: userDefinedCookie + restMethod: get + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/GetWallets/GetWallets + outputTemplate: mimoto/LoginFlow/Wallet/GetWallets/GetWalletsResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION" +}' + output: '{ +}' + + Mimoto_GetWallets_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that fetching the list of wallets without a valid login session is not allowed and returns an appropriate authentication error + uniqueIdentifier: TC_Mimoto_GetWallets_02 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/GetWallets/GetWallets + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_GetWallets_WithExpiredSession_Neg: + endPoint: /v1/mimoto/wallets + description: Verify that fetching the list of wallets with an expired session is not allowed and returns an appropriate session expiry or authentication error + uniqueIdentifier: TC_Mimoto_GetWallets_03 + role: userDefinedCookie + restMethod: get + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/GetWallets/GetWallets + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "OGYwNWI5YWQtZjg0MS00NmM2LWJiZTgtOGRmMWYwMGYzNzcy", + "cookieName": "SESSION" +}' + output: '{ + "responseCode": "401" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWalletsResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWalletsResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/GetWallets/GetWalletsResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.hbs new file mode 100644 index 000000000..59db2a6ff --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "walletPin": "{{walletPin}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.yml new file mode 100644 index 000000000..f42ed8fc3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet.yml @@ -0,0 +1,92 @@ +UnlockWallet: + Mimoto_UnlockWallet_all_Valid_Smoke: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that the created wallet can be unlocked successfully using valid credentials/PIN. + uniqueIdentifier: TC_Mimoto_UnlockWallet_01 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet + outputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWalletResult + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "123456" +}' + output: '{ + "walletName": "$ID:CreateWallet_all_Valid_Smoke_sid_walletName$", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$" +}' + + Mimoto_UnlockWallet_Invalid_WalletId_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that unlocking a wallet with an invalid wallet ID is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_UnlockWallet_02 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "invalid", + "walletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' + + Mimoto_UnlockWallet_Incorrect_WalletPin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that unlocking a wallet with an incorrect wallet PIN is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_UnlockWallet_03 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "876532" +}' + output: '{ + "errorCode": "invalid_pin" +}' + + Mimoto_UnlockWallet_WithoutLogin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Validate wallet unlocking without an active login session and verify that the operation fails + uniqueIdentifier: TC_Mimoto_UnlockWallet_04 + role: userDefinedCookie + restMethod: post + checkOnlyStatusCodeInResponse: true + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet + outputTemplate: mimoto/responseCode + input: '{ + "cookie": "withoutLogin", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "123456" +}' + output: '{ + "responseCode": "401" +}' + + Mimoto_UnlockWallet_Invalid_WalletPin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that unlocking a wallet with an invalid wallet PIN is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_UnlockWallet_06 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWallet + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "87653" +}' + output: '{ + "errorCode": "invalid_request" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWalletResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWalletResult.hbs new file mode 100644 index 000000000..8a296140c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWallet/UnlockWalletResult.hbs @@ -0,0 +1,4 @@ +{ + "walletName": "{{walletName}}", + "walletId": "{{walletId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.hbs new file mode 100644 index 000000000..59db2a6ff --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "walletPin": "{{walletPin}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.yml new file mode 100644 index 000000000..946e49988 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete.yml @@ -0,0 +1,19 @@ +UnlockWalletAfterDelete: + Mimoto_UnlockWalletAfterDelete_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that attempting to unlock a deleted wallet is not allowed and returns an appropriate error message + uniqueIdentifier: TC_Mimoto_UnlockWalletAfterDelete_01 + role: userDefinedCookie + restMethod: post + additionalDependencies: TC_Mimoto_DeleteWallet_01 + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDelete + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "123456" +}' + output: '{ + "errorCode": "invalid_request" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDeleteResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDeleteResult.hbs new file mode 100644 index 000000000..8a296140c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletAfterDelete/UnlockWalletAfterDeleteResult.hbs @@ -0,0 +1,4 @@ +{ + "walletName": "{{walletName}}", + "walletId": "{{walletId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.hbs new file mode 100644 index 000000000..59db2a6ff --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.hbs @@ -0,0 +1,6 @@ +{ + "cookie": "{{cookie}}", + "cookieName": "{{cookieName}}", + "walletId": "{{walletId}}", + "walletPin": "{{walletPin}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.yml b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.yml new file mode 100644 index 000000000..a87dc19f3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock.yml @@ -0,0 +1,18 @@ +UnlockWalletForTemporarilyLock: + Mimoto_UnlockWalletForTemporarilyLock_TemporaryLocker_IncorrectWalletPin_Neg: + endPoint: /v1/mimoto/wallets/{walletId}/unlock + description: Verify that the wallet becomes temporarily locked when incorrect PIN is entered repeatedly up to the allowed limit and that further unlock attempts fail until the lock expires or is reset. + uniqueIdentifier: TC_Mimoto_UnlockWalletForTemporarilyLock_01 + role: userDefinedCookie + restMethod: post + inputTemplate: mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLock + outputTemplate: mimoto/error2 + input: '{ + "cookie": "$ID:GoogleLoginToken_all_Valid_Smoke_sessionCookie$", + "cookieName": "SESSION", + "walletId": "$ID:CreateWallet_all_Valid_Smoke_sid_walletId$", + "walletPin": "876532" +}' + output: '{ + "errorCode": "temporarily_locked" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLockResult.hbs b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLockResult.hbs new file mode 100644 index 000000000..8a296140c --- /dev/null +++ b/api-test/src/main/resources/mimoto/LoginFlow/Wallet/UnlockWalletForTemporarilyLock/UnlockWalletForTemporarilyLockResult.hbs @@ -0,0 +1,4 @@ +{ + "walletName": "{{walletName}}", + "walletId": "{{walletId}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.hbs b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.hbs new file mode 100644 index 000000000..6dec068b5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.hbs @@ -0,0 +1,17 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "clientId": "{{clientId}}", + "scope": "{{scope}}", + "responseType": "{{responseType}}", + "redirectUri": "{{redirectUri}}", + "display": "{{display}}", + "prompt": "{{prompt}}", + "acrValues": "{{acrValues}}", + "nonce" : "{{nonce}}", + "state" : "{{state}}", + "claimsLocales" : "{{claimsLocales}}", + "codeChallenge" : "{{codeChallenge}}", + "codeChallengeMethod" : "{{codeChallengeMethod}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.yml b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.yml new file mode 100644 index 000000000..92b0f4758 --- /dev/null +++ b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequest.yml @@ -0,0 +1,28 @@ +OAuthDetailsRequest: + Mimoto_ESignet_OAuthDetailsRequest_all_Valid_Smoke_sid: + endPoint: /v1/esignet/authorization/v2/oauth-details + description: Verify that the system successfully retrieves the authentication details for downloading the Sunbird credential when all valid parameters are provided + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequest_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/OAuthDetailsRequest/OAuthDetailsRequest + outputTemplate: mimoto/OAuthDetailsRequest/OAuthDetailsRequestResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORMOSIPIDFROMMIMOTOACTUATOR$", + "scope": "mosip_identity_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:generated-code mosip:idp:acr:linked-wallet mosip:idp:acr:biometrics", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/OAuthDetailsRequest/OAuthDetailsRequestResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/SendOTP.yml b/api-test/src/main/resources/mimoto/SendOTP/SendOTP.yml new file mode 100644 index 000000000..dd89210de --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/SendOTP.yml @@ -0,0 +1,56 @@ +SendOTP: + Resident_SendOTP_Valid_UIN_All_Smoke: + endPoint: /resident/v1/req/otp + description: Verifying that the OTP request is created successfully for a resident with a valid UIN + uniqueIdentifier: TC_Mimoto_SendOTP_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "maskedEmail": "XXsXXeXXaXXoXXtXXn@gmail.com" +}' + + Resident_SendOTP_Invalid_UIN_Neg: + endPoint: /resident/v1/req/otp + description: Validating the resident OTP request creation with an invalid UIN + uniqueIdentifier: TC_Mimoto_SendOTP_02 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "34hgf$", + "individualIdType": "UIN" +}' + output: '{ + "maskedEmail": "XXsXXeXXaXXoXXtXXn@gmail.com" +}' + + Resident_SendOTP_Invalid_VID_Neg: + endPoint: /resident/v1/req/otp + description: Verifying that the system returns an appropriate error when creating an OTP request with an invalid VID + uniqueIdentifier: TC_Mimoto_SendOTP_03 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/error + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$45hgv", + "individualIdType": "VID" +}' + output: '{ + +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTP.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTP.hbs new file mode 100644 index 000000000..34cba88fd --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTP.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.otp.internal", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "metadata": {}, + "otpChannel": ["PHONE","EMAIL"], + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "version": "1.0" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTP2.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTP2.hbs new file mode 100644 index 000000000..a0a1e4fc3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTP2.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.otp.internal", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "metadata": {}, + "otpChannel": {{otpChannel}}, + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "version": "1.0" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmail.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmail.hbs new file mode 100644 index 000000000..ed98a4562 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmail.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.otp.internal", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "metadata": {}, + "otpChannel": ["EMAIL"], + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "version": "1.0" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmpty.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmpty.hbs new file mode 100644 index 000000000..ed641006d --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsEmpty.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.otp.internal", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "metadata": {}, + "otpChannel": [], + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "version": "1.0" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsPhone.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsPhone.hbs new file mode 100644 index 000000000..c0bfffa06 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPAsPhone.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.otp.internal", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "metadata": {}, + "otpChannel": ["PHONE"], + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "version": "1.0" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult.hbs new file mode 100644 index 000000000..c83688009 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult.hbs @@ -0,0 +1,14 @@ +{ + "id": "$IGNORE$", + "version": "$IGNORE$", + "responseTime": "$IGNORE$", + "transactionID": "$IGNORE$", + "metadata": "$IGNORE$", + "response": "$IGNORE$", + "errors": [ + { + "errorCode": "{{errorCode}}", + "errorMessage": "{{errorMessage}}" + } + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult2.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult2.hbs new file mode 100644 index 000000000..1216fda84 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPErrorResult2.hbs @@ -0,0 +1,14 @@ +{ + "id": "$IGNORE$", + "version": "$IGNORE$", + "responseTime": "$IGNORE$", + "transactionID": "$IGNORE$", + "metadata": "$IGNORE$", + "response": "$IGNORE$", + "errors": [ + { + "errorCode": "{{errorCode}}", + "message": "{{message}}" + } + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResult.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForEmail.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForEmail.hbs new file mode 100644 index 000000000..7fc3005ca --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForEmail.hbs @@ -0,0 +1,11 @@ +{ + "id": "mosip.identity.otp.internal", + "version": "1.0", + "responseTime": "$IGNORE$", + "transactionID": "$IGNORE$", + "metadata": "$IGNORE$", + "response": { + "maskedEmail": "{{maskedEmail}}" + }, + "errors": "$IGNORE$" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForPhone.hbs b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForPhone.hbs new file mode 100644 index 000000000..72659f98d --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTP/createSendOTPResultForPhone.hbs @@ -0,0 +1,11 @@ +{ + "id": "mosip.identity.otp.internal", + "version": "1.0", + "responseTime": "$IGNORE$", + "transactionID": "$IGNORE$", + "metadata": "$IGNORE$", + "response": { + "maskedMobile": "{{maskedMobile}}" + }, + "errors": "$IGNORE$" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTPUIN/createSendOTP.yml b/api-test/src/main/resources/mimoto/SendOTPUIN/createSendOTP.yml new file mode 100644 index 000000000..7bc307fca --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTPUIN/createSendOTP.yml @@ -0,0 +1,16 @@ +individualOTP: + Mimoto_individualOTP_All_Valid_Smoke: + endPoint: /v1/mimoto/req/individualId/otp + description: Validate OTP generation for an individual using valid parameters + uniqueIdentifier: TC_Mimoto_individualOTP_01 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: mimoto/SendOTPUIN/individualIdOtp + outputTemplate: mimoto/SendOTPUIN/individualIdOtpResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "aid": "$ID:AddIdentity_withValidParameters_smoke_Pos_RID$" + }' + output: '{ + }' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtp.hbs b/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtp.hbs new file mode 100644 index 000000000..b40d7bdb2 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtp.hbs @@ -0,0 +1,8 @@ +{ + "aid": "{{aid}}", + "otpChannel": [ + "EMAIL", + "PHONE" + ], + "transactionID": "{{transactionID}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtpResult.hbs b/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtpResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOTPUIN/individualIdOtpResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtp.hbs b/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtp.hbs new file mode 100644 index 000000000..aeaf81251 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtp.hbs @@ -0,0 +1,14 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "otpChannels": [ + {{#each otpChannels}} + "{{channel}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } + } \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtpResult.hbs b/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtpResult.hbs new file mode 100644 index 000000000..c823777f1 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOtpEsignet/SendOtpResult.hbs @@ -0,0 +1,6 @@ +{ + "response": { + "maskedEmail": "{{maskedEmail}}", + "maskedMobile": "{{maskedMobile}}" + } + } \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.hbs b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.hbs new file mode 100644 index 000000000..c468ffce3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.hbs @@ -0,0 +1,12 @@ +{ + "id": "mosip.identity.otp", + "version": "1.0", + "requestTime": "{{requestTime}}", + "transactionID": "{{transactionID}}", + "individualId": "{{individualId}}", + "individualIdType": "{{individualIdType}}", + "otpChannel": [ + "EMAIL", + "PHONE" + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.yml b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.yml new file mode 100644 index 000000000..85540c2f5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTP.yml @@ -0,0 +1,23 @@ +createSendOTP: + Resident_SendOTP_All_Valid_Smoke: + endPoint: /idauthentication/v1/otp/v4jO2rTUkDglaG7rCkWDnqIT3WpZaGEW9j3FMXwTA5xXKVaDpx/6001/34536061 + description: Verifying that the OTP is generated successfully with all valid parameters + uniqueIdentifier: TC_Mimoto_createSendOTP_01 + role: resident + restMethod: post + inputTemplate: resident/SendOtpForVid/createSendOTP + outputTemplate: resident/SendOtpForVid/createSendOTPResult + input: '{ + "transactionID": "1234567190", + "individualId": "5329487524103783", + "requestTime": "$TIMESTAMP$", + "individualIdType": "VID", + "otpChannel": [ + "EMAIL", + "PHONE" + ] + }' + output: '{ + "maskedMobile": "XXXXXX7143", + "maskedEmail": "XXkXXlXXhXXuXXr@technoforte.co.in" + }' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTPResult.hbs b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTPResult.hbs new file mode 100644 index 000000000..0668a51b5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SendOtpForVid/createSendOTPResult.hbs @@ -0,0 +1,12 @@ +{ + "id": "$IGNORE$", + "version": "$IGNORE$", + "responseTime": "$IGNORE$", + "transactionID": "$IGNORE$", + "metadata": "$IGNORE$", + "response": { + "maskedMobile": "{{maskedMobile}}", + "maskedEmail": "{{maskedEmail}}" + }, + "errors": "$IGNORE$" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs new file mode 100644 index 000000000..0e92bbf36 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.hbs @@ -0,0 +1,15 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "challengeList" : [ + { + "authFactorType" : "{{authFactorType}}", + "challenge" : "{{challenge}}", + "format": "{{format}}" + } + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml new file mode 100644 index 000000000..3f07c6250 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC.yml @@ -0,0 +1,23 @@ +AuthenticateUserSunBirdC: + Mimoto_ESignet_AuthenticateUser_SunBirdC_Valid_Smoke: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/authenticate + description: Verify that the user is successfully authenticated before downloading the Sunbird credential + uniqueIdentifier: TC_Mimoto_AuthenticateUserSunBirdC_01 + role: resident + restMethod: post + additionalDependencies: TC_Mimoto_CreatePolicySunBirdR_01 + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdC + outputTemplate: mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_SunBirdC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_SunBirdC_all_Valid_Smoke_sid_transactionId$", + "individualId": "$POLICYNUMBERFORSUNBIRDRC$", + "authFactorType" : "$SUNBIRDINSURANCEAUTHFACTORTYPE$", + "challenge" : "$CHALLENGEVALUEFORSUNBIRDC$", + "format": "base64url-encoded-json" + }' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthenticateUserSunBirdC/AuthenticateUserSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs new file mode 100644 index 000000000..491dca1ba --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.hbs @@ -0,0 +1,19 @@ +{ + "encodedHash": "{{encodedHash}}", + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "acceptedClaims": [ + {{#each acceptedClaims}} + "{{claim}}" + {{#unless @last}},{{/unless}} + {{/each}} + ], + "permittedAuthorizeScopes": [ + {{#each permittedAuthorizeScopes}} + "{{scope}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml new file mode 100644 index 000000000..d4b647c1e --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC.yml @@ -0,0 +1,19 @@ +AuthorizationCodeSunBirdC: + Mimoto_ESignet_AuthorizationCode_SunBirdC_All_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/auth-code + description: Validating the generation of the authorization code for downloading the Sunbird VC + uniqueIdentifier: TC_Mimoto_AuthorizationCodeSunBirdC_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdC + outputTemplate: mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult + input: '{ + "encodedHash": "$ID:ESignet_OAuthDetailsRequest_SunBirdC_all_Valid_Smoke_sid_encodedResp$", + "requestTime": "$TIMESTAMP$", + "transactionId": "$ID:ESignet_OAuthDetailsRequest_SunBirdC_all_Valid_Smoke_sid_transactionId$", + "permittedAuthorizeScopes": [{scope: "sunbird_rc_insurance_vc_ldp"}] +}' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/AuthorizationCodeSunBirdC/AuthorizationCodeSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs new file mode 100644 index 000000000..6dec068b5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.hbs @@ -0,0 +1,17 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "clientId": "{{clientId}}", + "scope": "{{scope}}", + "responseType": "{{responseType}}", + "redirectUri": "{{redirectUri}}", + "display": "{{display}}", + "prompt": "{{prompt}}", + "acrValues": "{{acrValues}}", + "nonce" : "{{nonce}}", + "state" : "{{state}}", + "claimsLocales" : "{{claimsLocales}}", + "codeChallenge" : "{{codeChallenge}}", + "codeChallengeMethod" : "{{codeChallengeMethod}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml new file mode 100644 index 000000000..d631dd9a5 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC.yml @@ -0,0 +1,28 @@ +OAuthDetailsRequestSunBirdC: + Mimoto_ESignet_OAuthDetailsRequest_SunBirdC_all_Valid_Smoke_sid: + endPoint: $ESIGNETMOCKBASEURL$/v1/esignet/authorization/v2/oauth-details + description: Verifying that the authentication details are retrieved successfully for downloading the Sunbird credentials + uniqueIdentifier: TC_Mimoto_OAuthDetailsRequestSunBirdC_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdC + outputTemplate: mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult + input: '{ + "requestTime": "$TIMESTAMP$", + "clientId": "$GETCLIENTIDFORINSURANCEFROMMIMOTOACTUATOR$", + "scope": "sunbird_rc_insurance_vc_ldp", + "responseType": "code", + "redirectUri": "$INJIREDIRECTURI$", + "display": "popup", + "prompt": "login", + "acrValues": "mosip:idp:acr:knowledge", + "nonce": "$UNIQUENONCEVALUEFORESIGNET$", + "state": "eree2311", + "claimsLocales": "en", + "codeChallenge": "$CODECHALLENGE$", + "codeChallengeMethod": "S256" +}' + output: '{ + +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdC/OAuthDetailsRequestSunBirdC/OAuthDetailsRequestSunBirdCResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.hbs b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.hbs new file mode 100644 index 000000000..fedc127da --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.hbs @@ -0,0 +1,16 @@ +{ + "policyNumber": "{{policyNumber}}", + "policyName": "{{policyName}}", + "policyExpiresOn": "{{policyExpiresOn}}", + "policyIssuedOn": "{{policyIssuedOn}}", + "fullName": "{{fullName}}", + "dob": "{{dob}}", + "benefits": [ + {{#each benefits}} + "{{benefits}}"{{#unless @last}},{{/unless}} + {{/each}} + ], + "gender": "{{gender}}", + "mobile": "{{mobile}}", + "email": "{{email}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.yml b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.yml new file mode 100644 index 000000000..964ff3271 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR.yml @@ -0,0 +1,26 @@ +CreatePolicySunBirdR: + Mimoto_ESignet_SunBirdR_CreatePolicy_NOAUTH_Valid_Smoke_Sid: + endPoint: $SUNBIRDBASEURL$/api/v1/Insurance + description: Verifying the creation of a new Sunbird policy with valid parameters + uniqueIdentifier: TC_Mimoto_CreatePolicySunBirdR_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdR + outputTemplate: mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdRResult + input: '{ + "policyNumber": "$POLICYNUMBERFORSUNBIRDRC$", + "policyName": "Start Insurance Gold Premium", + "policyExpiresOn": "2033-04-20T20:48:17.684Z", + "policyIssuedOn": "$TIMESTAMP$", + "fullName" : "$FULLNAMEFORSUNBIRDRC$", + "dob" : "$DOBFORSUNBIRDRC$", + "benefits": [{"benefits": "Critical Surgery"}, {"benefits": "Full body checkup"}], + "gender": "Male", + "mobile": "0123456789", + "email": "abhishek@gmail.com" + }' + output: '{ + "status": "SUCCESSFUL" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdRResult.hbs b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdRResult.hbs new file mode 100644 index 000000000..8efaf79fa --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/CreatePolicySunBirdR/CreatePolicySunBirdRResult.hbs @@ -0,0 +1,5 @@ +{ + "params": { + "status": "{{status}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.hbs b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.hbs new file mode 100644 index 000000000..4f7673ad4 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.hbs @@ -0,0 +1,3 @@ +{ + "insuranceid": "{{insuranceid}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.yml b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.yml new file mode 100644 index 000000000..bc7598059 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR.yml @@ -0,0 +1,17 @@ +DeletePolicySunBirdR: + Mimoto_ESignet_SunBirdR_DeletePolicy_NOAUTH_Valid_Smoke: + endPoint: $SUNBIRDBASEURL$/api/v1/Insurance/{insuranceid} + description: Validate the deletion of the Sunbird policy using valid parameters + uniqueIdentifier: TC_Mimoto_DeletePolicySunBirdR_01 + role: resident + restMethod: delete + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdR + outputTemplate: mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdRResult + input: '{ + "insuranceid": "$ID:ESignet_SunBirdR_CreatePolicy_NOAUTH_Valid_Smoke_Sid_osid$" + }' + output: '{ + "status": "SUCCESSFUL" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdRResult.hbs b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdRResult.hbs new file mode 100644 index 000000000..8efaf79fa --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/DeletePolicySunBirdR/DeletePolicySunBirdRResult.hbs @@ -0,0 +1,5 @@ +{ + "params": { + "status": "{{status}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.hbs b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.hbs new file mode 100644 index 000000000..4f7673ad4 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.hbs @@ -0,0 +1,3 @@ +{ + "insuranceid": "{{insuranceid}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.yml b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.yml new file mode 100644 index 000000000..b46641049 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR.yml @@ -0,0 +1,17 @@ +GetPolicySunBirdR: + Mimoto_ESignet_SunBirdR_GetPolicy_NOAUTH_Valid_Smoke: + endPoint: $SUNBIRDBASEURL$/api/v1/Insurance/{insuranceid} + description: Validating the retrieval of the Sunbird policy with valid parameters + uniqueIdentifier: TC_Mimoto_GetPolicySunBirdR_01 + role: resident + restMethod: get + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdR + outputTemplate: mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdRResult + input: '{ + "insuranceid": "$ID:ESignet_SunBirdR_CreatePolicy_NOAUTH_Valid_Smoke_Sid_osid$" + }' + output: '{ + "policyNumber": "$POLICYNUMBERFORSUNBIRDRC$" +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdRResult.hbs b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdRResult.hbs new file mode 100644 index 000000000..d1a64cf38 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/GetPolicySunBirdR/GetPolicySunBirdRResult.hbs @@ -0,0 +1,3 @@ +{ + "policyNumber": {{policyNumber}} +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.hbs b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.hbs new file mode 100644 index 000000000..38866c7ac --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.hbs @@ -0,0 +1,21 @@ +{ + "offset": {{offset}}, + "limit": {{limit}}, + "filters": { + {{#if fullName}} + "fullName": { + "eq": "{{fullName}}" + }, + {{/if}} + {{#if dob}} + "dob": { + "eq": "{{dob}}" + }, + {{/if}} + {{#if policyNumber}} + "policyNumber": { + "eq": "{{policyNumber}}" + } + {{/if}} + } +} diff --git a/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.yml b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.yml new file mode 100644 index 000000000..b85d958f9 --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR.yml @@ -0,0 +1,20 @@ +SearchPolicySunBirdR: + Mimoto_ESignet_SunBirdR_SearchPolicy_NOAUTH_Valid_Smoke: + endPoint: $SUNBIRDBASEURL$/api/v1/Insurance/search + description: Validating the search functionality for the Sunbird policy with valid parameters + uniqueIdentifier: TC_Mimoto_SearchPolicySunBirdR_01 + role: resident + restMethod: post + checkErrorsOnlyInResponse: true + validityCheckRequired: true + inputTemplate: mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdR + outputTemplate: mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdRResult + input: '{ + "offset": "0", + "limit": "5", + "fullName": "$FULLNAMEFORSUNBIRDRC$", + "dob": "$DOBFORSUNBIRDRC$", + "policyNumber": "$POLICYNUMBERFORSUNBIRDRC$" + }' + output: '{ +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdRResult.hbs b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdRResult.hbs new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/api-test/src/main/resources/mimoto/SunBirdR/SearchPolicySunBirdR/SearchPolicySunBirdRResult.hbs @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.hbs b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.hbs new file mode 100644 index 000000000..05f5d58a0 --- /dev/null +++ b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.hbs @@ -0,0 +1,10 @@ +{ + "id": "mosip.identity.auth.internal", + "version": "v1", + "requesttime": "{{requesttime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "otp": "{{otp}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.yml b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.yml new file mode 100644 index 000000000..426300ca3 --- /dev/null +++ b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTP.yml @@ -0,0 +1,206 @@ +SendOTP: + Resident_SendOTP_Valid_UIN_All_Smoke: + endPoint: /resident/v1/req/otp + description: Validate OTP generation using a valid UIN + uniqueIdentifier: TC_Mimoto_SendOTP_05 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + }' + Resident_SendOTP_Valid_UIN_Phone_Smoke: + endPoint: /resident/v1/req/otp + description: Verifying that the OTP is generated successfully when the OTP channel is set to phone + uniqueIdentifier: TC_Mimoto_SendOTP_06 + role: resident + checkErrorsOnlyInResponse: true + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP2 + outputTemplate: resident/SendOTP/createSendOTPResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "otpChannel": "[\"PHONE\"]", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + }' + Resident_SendOTP_Valid_UIN_Email_Smoke: + endPoint: /resident/v1/req/otp + description: Verifying that the OTP is generated successfully when the OTP channel is set to email + uniqueIdentifier: TC_Mimoto_SendOTP_07 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP2 + outputTemplate: resident/SendOTP/createSendOTPResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "otpChannel": "[\"EMAIL\"]", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + }' + Resident_SendOTP_RevokedVID_Invalid: + endPoint: /resident/v1/req/otp + description: Validate OTP generation using a revoked VID + uniqueIdentifier: TC_Mimoto_SendOTP_08 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:GenerateVID_All_Valid_Smoke_Perpetual_sid_vid$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-018" + }' + Resident_SendOTP_Valid_UIN_Invalid_empty_otpChannel: + endPoint: /resident/v1/req/otp + description: Validate OTP generation when the OTP channel field is empty + uniqueIdentifier: TC_Mimoto_SendOTP_09 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP2 + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "otpChannel": "[]", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-OTA-008" + }' + Resident_SendOTP_InValid_individualId: + endPoint: /resident/v1/req/otp + description: Validating the OTP generation with an invalid Individual ID + uniqueIdentifier: TC_Mimoto_SendOTP_10 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "asfvgaf", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-009" + }' + Resident_SendOTP_InValid_Missing_individualId: + endPoint: /resident/v1/req/otp + description: Validating the OTP generation with a missing Individual ID + uniqueIdentifier: TC_Mimoto_SendOTP_11 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "$TIMESTAMP$", + "individualId": "$REMOVE$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-009" + }' + Resident_SendOTP_InValid_Empty_transactionID: + endPoint: /resident/v1/req/otp + description: Validating the OTP generation with an empty transaction ID + uniqueIdentifier: TC_Mimoto_SendOTP_12 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-006" + }' + Resident_SendOTP_InValid_transactionID: + endPoint: /resident/v1/req/otp + description: Validate OTP generation using an invalid transaction ID + uniqueIdentifier: TC_Mimoto_SendOTP_13 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "asdveve", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-009" + }' + Resident_SendOTP_InValid_Missing_transactionID: + endPoint: /resident/v1/req/otp + description: Validating the OTP generation with a missing transaction ID + uniqueIdentifier: TC_Mimoto_SendOTP_14 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult2 + input: '{ + "transactionID": "$REMOVE$", + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "RES-SER-425" + }' + Resident_SendOTP_InValid_Empty_requestTime: + endPoint: /resident/v1/req/otp + description: Validating the OTP generation with an empty request time + uniqueIdentifier: TC_Mimoto_SendOTP_15 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "IDA-MLC-006" + }' + Resident_SendOTP_InValid_requestTime: + endPoint: /resident/v1/req/otp + description: Verifying that the system returns an appropriate error when generating an OTP with an invalid request time + uniqueIdentifier: TC_Mimoto_SendOTP_16 + role: resident + restMethod: post + inputTemplate: resident/SendOTP/createSendOTP + outputTemplate: resident/SendOTP/createSendOTPErrorResult2 + input: '{ + "transactionID": "$TRANSACTIONID$", + "requestTime": "asvfv", + "individualId": "$ID:AddIdentity_withValidParameters_smoke_Pos_UIN$", + "individualIdType": "UIN" +}' + output: '{ + "errorCode": "RES-SER-425" + }' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTPResult.hbs b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTPResult.hbs new file mode 100644 index 000000000..ab63e6191 --- /dev/null +++ b/api-test/src/main/resources/mimoto/ValidateOTP/ValidateOTPResult.hbs @@ -0,0 +1,4 @@ +{ + "id": "mosip.identity.auth.internal", + "version": "v1", +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.hbs b/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.hbs new file mode 100644 index 000000000..1a415bc51 --- /dev/null +++ b/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.hbs @@ -0,0 +1,16 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "individualId": "{{individualId}}", + "authFactorType": "{{authFactorType1}}", + "format": "{{format1}}", + "challengeList": [ + { + "authFactorType": "{{authFactorType}}", + "challenge": "{{challenge}}", + "format": "{{format}}" + } + ], + "publicKey": "{{publicKey}}" + } +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.yml b/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.yml new file mode 100644 index 000000000..faf8f4809 --- /dev/null +++ b/api-test/src/main/resources/mimoto/WalletBinding/WalletBinding.yml @@ -0,0 +1,3061 @@ +WalletBinding: + Mimoto_WalletBinding_uin_OTP_Valid_Smoke: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system generates an OTP when a valid UIN is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_01 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/WalletBinding/WalletBindingResult + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_OTP_Valid_Smoke: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system generates an OTP when a valid VID is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_02 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/WalletBinding/WalletBindingResult + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid request time is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_03 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "asdf$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_PastVal_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when a past request time is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_04 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-07T06:51:30.195Z", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_FutureVal_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when a future request time is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_05 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "2033-11-07T06:51:30.195Z", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the requestTime field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_06 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the requestTime field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_07 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$REMOVE$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid individualId is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_08 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + allowedErrorCodes: IDA-MLC-009,binding_auth_failed + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$tyhg", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_09 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_10 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + allowedErrorCodes: IDA-MLC-009,binding_auth_failed + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "null", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_11 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$REMOVE$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId contains spaces for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_12 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": " ", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid authFactorType is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_13 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "dfg5", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_14 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType field is missing for VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_15 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "$REMOVE$", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType value contains only spaces for VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_16 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": " ", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_17 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "null", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid challenge value is provided for VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_18 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "gd5$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_19 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$REMOVE$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_length" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_20 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_length" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge value contains spaces for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_21 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": " ", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_length" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_22 + role: resident + validityCheckRequired: true + allowedErrorCodes: + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "null", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid format value is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_23 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "GT$r5", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_24 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "$REMOVE$", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_25 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format value contains spaces for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_26 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": " ", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_27 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "null", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid authFactorType1 is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_28 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "hy$5", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_29 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "$REMOVE$", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_30 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 value contains spaces for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_31 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": " ", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_32 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "null", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid format1 value is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_33 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "hju$5", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 field is missing for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_34 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "$REMOVE$", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Empty_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 field is empty for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_35 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_SpaceVal_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 value contains spaces for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_36 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": " ", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_37 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "null", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Invalid_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid publicKey is provided for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_38 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "ghj$4", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Null_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the publicKey value is null for VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_39 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "null", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_uin_Missing_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the publicKey field is missing for VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_40 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$REMOVE$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:AddIdentity_Binding_smoke_Pos_UIN$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid requestTime is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_41 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "asdf$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_PastVal_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when a past requestTime is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_42 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "2022-11-07T06:51:30.195Z", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_FutureVal_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when a future requestTime is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_43 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "2033-11-07T06:51:30.195Z", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the requestTime field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_44 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_RequestTime_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that generating an OTP to bind a VID VC fails when requestTime is missing + uniqueIdentifier: TC_Mimoto_WalletBinding_45 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$REMOVE$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_request" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid individualId is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_46 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + allowedErrorCodes: IDA-MLC-009,binding_auth_failed + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$tyhg", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_47 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_48 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + allowedErrorCodes: IDA-MLC-009,binding_auth_failed + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "null", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "IDA-MLC-009" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_49 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$REMOVE$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_IndividualId_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the individualId contains spaces for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_50 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": " ", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_individual_id" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid authFactorType is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_51 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "dfg5", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType field is empty for VID-based VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_52 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_53 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "$REMOVE$", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType value contains only spaces for VID-based VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_54 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_auth_factor_type + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": " ", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_AuthFactorType_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_55 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "null", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid challenge value is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_56 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "gd5$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_57 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$REMOVE$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_58 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge value contains spaces for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_59 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": " ", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_Challenge_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the challenge value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_60 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "null", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid format value is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_61 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "GT$r5", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_62 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "$REMOVE$", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_63 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format value contains spaces for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_64 + role: resident + validityCheckRequired: true + allowedErrorCodes: invalid_challenge_format + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": " ", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_Formate_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_65 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "null", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid authFactorType1 is provided for VID-based VC binding. + uniqueIdentifier: TC_Mimoto_WalletBinding_66 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "hy$5", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_67 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "$REMOVE$", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_68 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 value contains spaces for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_69 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": " ", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_authFactorType1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the authFactorType1 value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_70 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "null", + "format1": "jwt", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid format1 value is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_71 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "hju$5", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_72 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "$REMOVE$", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Empty_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 field is empty for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_73 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_SpaceVal_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 value contains spaces for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_74 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": " ", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_format1_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the format1 value is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_75 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "null", + "publicKey": "$PUBLICKEYFORBINDING$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "invalid_auth_factor_type_or_challenge_format" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Invalid_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when an invalid publicKey is provided for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_76 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "ghj$4", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Null_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the publicKey is null for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_77 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "null", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + + } +}' + + Mimoto_WalletBinding_vid_Missing_publicKey_Neg: + endPoint: /v1/mimoto/wallet-binding + description: Verify that the system rejects the request and does not generate an OTP when the publicKey field is missing for VID-based VC binding + uniqueIdentifier: TC_Mimoto_WalletBinding_78 + role: resident + validityCheckRequired: true + restMethod: post + checkErrorsOnlyInResponse: true + inputTemplate: mimoto/WalletBinding/WalletBinding + outputTemplate: mimoto/error + input: '{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "authFactorType": "OTP", + "challenge": "$ID:AddIdentity_Binding_smoke_Pos_EMAIL$", + "format": "alpha-numeric", + "authFactorType1": "WLA", + "format1": "jwt", + "publicKey": "$REMOVE$", + "sendOtp":{ + "requestTime": "$TIMESTAMP$", + "individualId": "$ID:Generate_Perpetual_VID_Binding_Valid_Smoke_sid_vid$", + "otpChannels": [{channel: "EMAIL"},{channel: "PHONE"}], + "sendOtpReqTemplate": "mimoto/BindingOtp/BindingOtp", + "sendOtpEndPoint": "/v1/mimoto/binding-otp" + } +}' + output: '{ + "errors": [ + { + "errorCode": "RESIDENT-APP-028" + } + ], + "sendOtpResp": { + "sendOtpResTemplate":"mimoto/BindingOtp/BindingOtpResult" + } +}' \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingEmptyChallenge.hbs b/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingEmptyChallenge.hbs new file mode 100644 index 000000000..646bf0f46 --- /dev/null +++ b/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingEmptyChallenge.hbs @@ -0,0 +1,9 @@ +{ + "requestTime": "{{requestTime}}", + "request": { + "transactionId": "{{transactionId}}", + "individualId": "{{individualId}}", + "challengeList": "{{challengeList}}", + "publicKey": {{publicKey}} + } + } \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingResult.hbs b/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingResult.hbs new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/api-test/src/main/resources/mimoto/WalletBinding/WalletBindingResult.hbs @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/error.hbs b/api-test/src/main/resources/mimoto/error.hbs new file mode 100644 index 000000000..f4bcb6bee --- /dev/null +++ b/api-test/src/main/resources/mimoto/error.hbs @@ -0,0 +1,10 @@ +{ + "errors": [ + {{#each errors}} + { + "errorCode": "{{errorCode}}" + } + {{#unless @last}},{{/unless}} + {{/each}} + ] +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/error2.hbs b/api-test/src/main/resources/mimoto/error2.hbs new file mode 100644 index 000000000..bf46cc93a --- /dev/null +++ b/api-test/src/main/resources/mimoto/error2.hbs @@ -0,0 +1,3 @@ +{ + "errorCode": "{{errorCode}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/mimoto/responseCode.hbs b/api-test/src/main/resources/mimoto/responseCode.hbs new file mode 100644 index 000000000..f6db3ef22 --- /dev/null +++ b/api-test/src/main/resources/mimoto/responseCode.hbs @@ -0,0 +1,3 @@ +{ + "responseCode": "{{responseCode}}" +} \ No newline at end of file diff --git a/api-test/src/main/resources/spring.properties b/api-test/src/main/resources/spring.properties new file mode 100644 index 000000000..21fdcd3ec --- /dev/null +++ b/api-test/src/main/resources/spring.properties @@ -0,0 +1,329 @@ +#Please change only required values +logging.level.io.mosip.registrtaion=DEBUG + +hibernate.hbm2ddl.auto=none +hibernate.dialect=org.hibernate.dialect.DerbyTenSevenDialect +hibernate.show_sql=false +hibernate.format_sql=false +hibernate.connection.charSet=utf8 +hibernate.cache.use_second_level_cache=false +hibernate.cache.use_query_cache=false +hibernate.cache.use_structured_entries=false +hibernate.generate_statistics=false + +#otp validation time details +otp_validity_in_mins=2 + + +#Timeout Configuration +HTTP_API_READ_TIMEOUT = 60000 +HTTP_API_WRITE_TIMEOUT = 60000 + +#Biometric Device +PROVIDER_NAME = Mantra +WEBCAM_LIBRARY_NAME = sarxos + +#document scanner properties +DOCUMENT_SCANNER_DEPTH=10 +DOCUMENT_SCANNER_HOST=192.168.43.253 +DOCUMENT_SCANNER_PORT=6566 +DOCUMENT_SCANNER_TIMEOUT=5000 + +#Client ID and Secret Key +AUTH_CLIENT_ID=registration-client +AUTH_SECRET_KEY=c6f69439-f811-4415-8ebc-4c9e447c901e + +spring.cloud.config.uri=LOCAL +spring.application.name= +spring.profiles.active= +spring.cloud.config.label= + +#AUTH_SDK +mosip.registration.face.provider=com.identy.IdentyBioSDK +mosip.registration.iris.provider=com.identy.IdentyBioSDK +mosip.registration.finger.provider=com.identy.IdentyBioSDK + + +#MDM +#host has to be clarified whether nedd to be in config or hardcoded +mdm.host=127.0.0.1 +#portRangeFrom and portRangeTo are mandate,portRangeTo can be same are greater than then portRangeFrom +mdm.portRangeFrom=4500 +mdm.portRangeTo=4510 +mdm.contextPath= +mdm.hostProtocol=http + +mdm.deviceInfo.service.path=deviceInfo +mdm.deviceInfo.service.headers=Content-Type:APPLICATION/JSON +mdm.deviceInfo.service.httpmethod=GET +mdm.deviceInfo.service.responseType=java.lang.Object + +mdm.capture.service.path=capture +mdm.capture.service.headers=Content-Type:APPLICATION/JSON +mdm.capture.service.httpmethod=POST +mdm.deviceInfo.service.responseType=java.lang.Object + +mdm.deviceDiscovery.service.path=deviceDiscovery +mdm.deviceDiscovery.service.headers=Content-Type:APPLICATION/JSON +mdm.deviceDiscovery.service.httpmethod=POST +mdm.deviceDiscovery.service.responseType=java.lang.Object + +mosip.kernel.idobjectvalidator.file-storage-uri=LOCAL +mosip.kernel.idobjectvalidator.schema-name=mosip-identity-json-schema.json +mosip.kernel.idobjectvalidator.property-source=LOCAL +mosip.kernel.keymanager-service-sign-url= + +mosip.country.code=MOR + +#onboarded_user details +user_machine_mapping.service.url=https://qa.mosip.io/v1/masterdata/registrationmachineusermappings +user_machine_mapping.service.httpmethod=PUT +user_machine_mapping.service.requestType=java.lang.Object +user_machine_mapping.service.headers=Content-Type:APPLICATION/JSON +user_machine_mapping.service.authrequired=true +user_machine_mapping.service.signrequired=false +user_machine_mapping.service.authheader=Authorization:OAUTH +user_machine_mapping.service.requestsignrequired=true + +#packet_status details +packet_status.service.url=https://qa.mosip.io/registrationprocessor/v1/registrationstatus/search +packet_status.service.httpmethod=POST +packet_status.service.responseType=java.util.LinkedHashMap +packet_status.service.headers=Content-Type:APPLICATION/JSON +packet_status.service.authrequired=true +packet_status.service.signrequired=false +packet_status.service.authheader=Authorization:OAUTH +packet_status.service.requestsignrequired=true + +#sms +sms.service.url=https://qa.mosip.io/v1/smsnotifier/sms/send +sms.service.httpmethod=POST +sms.service.requestType=java.lang.Object +sms.service.headers=Content-Type:APPLICATION/JSON +sms.service.authrequired=true +sms.service.signrequired=false +sms.service.authheader=Authorization:OAUTH +sms.service.requestsignrequired=true + +#email +email.service.url=https://qa.mosip.io/v1/emailnotifier/email/send +email.service.httpmethod=POST +email.service.requestType=java.lang.Object +email.service.headers=Content-Type:multipart/form-data +email.service.authrequired=true +email.service.signrequired=false +email.service.authheader=Authorization:OAUTH +email.service.requestsignrequired=true + +#Upload +packet_upload.service.url=https://qa.mosip.io/registrationprocessor/v1/packetreceiver/registrationpackets +packet_upload.service.httpmethod=POST +packet_upload.service.requestType=java.lang.String +packet_upload.service.headers=Content-Type:multipart/form-data +packet_upload.service.authrequired=true +packet_upload.service.signrequired=false +packet_upload.service.authheader=Authorization:OAUTH +packet_upload.service.service.requestsignrequired=true + +#Packet Sync +packet_sync.service.url=https://qa.mosip.io/registrationprocessor/v1/registrationstatus/sync +packet_sync.service.httpmethod=POST +packet_sync.service.requestType=java.lang.Object +packet_sync.service.headers=Content-Type:APPLICATION/JSON,timestamp:timestamp,Center-Machine-RefId:centerId +packet_sync.service.authrequired=true +packet_sync.service.signrequired=false +packet_sync.service.authheader=Authorization:OAUTH +packet_sync.service.service.requestsignrequired=true + +#policy sync +policysync.service.url= https://qa.mosip.io/v1/syncdata/publickey/REGISTRATION +policysync.service.httpmethod=GET +policysync.service.responseType=java.util.LinkedHashMap +policysync.service.headers=Content-Type:APPLICATION/JSON +policysync.service.authrequired=true +policysync.service.signrequired=false +policysync.service.authheader=Authorization:OAUTH +policysync.service.service.requestsignrequired=true + +#Pre-Registration Get Pre-Reg Id's +get_pre_registration_Ids.service.url=https://qa.mosip.io/preregistration/v1/sync +get_pre_registration_Ids.service.httpmethod=POST +get_pre_registration_Ids.service.requestType=java.lang.Object +get_pre_registration_Ids.service.headers=Content-Type:APPLICATION/JSON +get_pre_registration_Ids.service.authrequired=true +get_pre_registration_Ids.service.signrequired=false +get_pre_registration_Ids.service.authheader=Authorization:OAUTH +get_pre_registration_Ids.service.requestsignrequired=true + +#Pre-Registration Get Pre-Reg packet +get_pre_registration.service.url=https://qa.mosip.io/preregistration/v1/sync/{pre_registration_id} +get_pre_registration.service.httpmethod=GET +get_pre_registration.service.responseType=java.lang.Object +get_pre_registration.service.headers=Content-Type:APPLICATION/JSON +get_pre_registration.service.authrequired=true +get_pre_registration.service.signrequired=false +get_pre_registration.service.authheader=Authorization:OAUTH +get_pre_registration.service.requestsignrequired=true + +#master_sync details +master_sync.service.url=https://qa.mosip.io/v1/syncdata/masterdata +master_sync.service.httpmethod=GET +master_sync.service.responseType=java.lang.Object +master_sync.service.headers=Content-Type:application/json;charset=UTF-8 +master_sync.service.authrequired=true +master_sync.service.signrequired=false +master_sync.service.authheader=Authorization:OAUTH +master_sync.service.requestsignrequired=true + +#Get Global-Config Details +get_registration_center_config.service.url=https://qa.mosip.io/v1/syncdata/configs +get_registration_center_config.service.httpmethod=GET +get_registration_center_config.service.responseType=java.util.LinkedHashMap +get_registration_center_config.service.headers=Content-Type:APPLICATION/JSON +get_registration_center_config.service.authrequired=true +get_registration_center_config.service.signrequired=false +get_registration_center_config.service.authheader=Authorization:OAUTH +get_registration_center_config.service.requestsignrequired=true + +#Send OTP +send_otp.service.url=https://qa.mosip.io/v1/authmanager/authenticate/sendotp +send_otp.service.httpmethod=POST +send_otp.service.requestType=java.lang.Object +send_otp.service.headers=Content-Type:APPLICATION/JSON +send_otp.service.authrequired=true +send_otp.service.signrequired=false +send_otp.service.authheader=Authorization:OAUTH +send_otp.service.requestsignrequired=false + +#Validate Authorization Token +validate_auth_token.service.url=https://qa.mosip.io/v1/authmanager/authorize/admin/validateToken +validate_auth_token.service.httpmethod=POST +validate_auth_token.service.requestType=java.lang.Object +validate_auth_token.service.headers=Content-Type:APPLICATION/JSON +validate_auth_token.service.authrequired=true +validate_auth_token.service.signrequired=false +validate_auth_token.service.authheader=Authorization:OAUTH +validate_auth_token.service.requestsignrequired=false + +#Authentication API +auth_by_password.service.url=https://qa.mosip.io/v1/authmanager/authenticate/useridPwd +auth_by_otp.service.url=https://qa.mosip.io/v1/authmanager/authenticate/useridOTP +auth_by_clientid_secretkey.service.url=https://qa.mosip.io/v1/authmanager/authenticate/clientidsecretkey + +#user details +user_details.service.url=https://qa.mosip.io/v1/syncdata/userdetails/{regid} +user_details.service.httpmethod=GET +user_details.service.responseType=java.lang.Object +user_details.service.headers=Content-Type:APPLICATION/JSON +user_details.service.authrequired=true +user_details.service.signrequired=false +user_details.service.authheader=Authorization:OAUTH +user_details.service.requestsignrequired=true + +#Invalidate Authorization Token +invalidate_auth_token.service.url=https://qa.mosip.io/v1/authmanager/authorize/invalidateToken +invalidate_auth_token.service.httpmethod=POST +invalidate_auth_token.service.requestType=java.lang.Object +invalidate_auth_token.service.headers=Content-Type:APPLICATION/JSON +invalidate_auth_token.service.authrequired=true +invalidate_auth_token.service.signrequired=false +invalidate_auth_token.service.authheader=Authorization:OAUTH +invalidate_auth_token.service.requestsignrequired=false + +#public_key details +public_key.service.url=https://qa.mosip.io/v1/keymanager/publickey/KERNEL +public_key.service.httpmethod=GET +public_key.service.responseType=java.lang.Object +public_key.service.headers=Content-Type:APPLICATION/JSON +public_key.service.authrequired=true +public_key.service.signrequired=false +public_key.service.authheader=Authorization:OAUTH +public_key.service.requestsignrequired=true + +#public_key details +public_key.service.url=https://qa.mosip.io/v1/keymanager/publickey/KERNEL +public_key.service.httpmethod=GET +public_key.service.responseType=java.lang.Object +public_key.service.headers=Content-Type:APPLICATION/JSON +public_key.service.authrequired=true +public_key.service.signrequired=false +public_key.service.authheader=Authorization:OAUTH +public_key.service.requestsignrequired=true + +#user_salt_details details +user_salt_details.service.url=https://qa.mosip.io/v1/authmanager/usersaltdetails/registrationclient +user_salt_details.service.httpmethod=GET +user_salt_details.service.responseType=java.lang.Object +user_salt_details.service.headers=Content-Type:APPLICATION/JSON +user_salt_details.service.authrequired=true +user_salt_details.service.signrequired=false +user_salt_details.service.authheader=Authorization:OAUTH +user_salt_details.service.requestsignrequired=true + +#SignatureResponseUrl's +mosip.kernel.signature.cryptomanager-encrypt-url=https://qa.mosip.io/v1/cryptomanager/private/encrypt +mosip.kernel.keymanager-service-publickey-url=https://qa.mosip.io/v1/keymanager/publickey/{applicationId} +auth.server.validate.url=https://qa.mosip.io/v1/authmanager/authorize/validateToken +auth.server.refreshToken.url=https://qa.mosip.io/v1/authmanager/authorize/refreshToken + +#master_sync details +center_remap_sync.service.url=https://qa.mosip.io/v1/syncdata/masterdata/{regcenterId} +center_remap_sync.service.httpmethod=GET +center_remap_sync.service.responseType=java.lang.Object +center_remap_sync.service.headers=Content-Type:application/json;charset=UTF-8 +center_remap_sync.service.authrequired=true +center_remap_sync.service.signrequired=false +center_remap_sync.service.authheader=Authorization:OAUTH +center_remap_sync.service.requestsignrequired=true + +#ida_key details +ida_key.service.url=https://qa.mosip.io/v1/keymanager/publickey/IDA +ida_key.service.httpmethod=GET +ida_key.service.responseType=java.util.LinkedHashMap +ida_key.service.headers=Content-Type:APPLICATION/JSON +ida_key.service.authrequired=true +ida_key.service.signrequired=false +ida_key.service.authheader=Authorization:OAUTH +ida_key.service.requestsignrequired=true + +#ida_auth details +ida_auth.service.url=https://qa.mosip.io/idauthentication/v1/internal/auth +ida_auth.service.httpmethod=POST +ida_auth.service.responseType=java.util.LinkedHashMap +ida_auth.service.headers=Content-Type:APPLICATION/JSON +ida_auth.service.authrequired=true +ida_auth.service.signrequired=false +ida_auth.service.authheader=Authorization:OAUTH +ida_auth.service.requestsignrequired=true + +#TPM Public Key Upload +tpm_public_key.service.url=https://qa.mosip.io/v1/syncdata/tpm/publickey +tpm_public_key.service.httpmethod=POST +tpm_public_key.service.responseType=java.lang.Object +tpm_public_key.service.headers=Content-Type:APPLICATION/JSON +tpm_public_key.service.authrequired=true +tpm_public_key.service.signrequired=false +tpm_public_key.service.authheader=Authorization:OAUTH +ida_auth.service.requestsignrequired=false + +#Main Properties +mosip.reg.client.url=https://devops.mosip.io/artifactory/libs-release/io/mosip/registration/registration-client/ +mosip.reg.logpath=../logs +mosip.reg.packetstorepath=../PacketStore +mosip.reg.healthcheck.url=https://qa.mosip.io/v1/authmanager/actuator/health +mosip.reg.rollback.path=../BackUp +mosip.reg.db.key=bW9zaXAxMjM0NQ\=\= +mosip.reg.cerpath=/cer//mosip_cer.cer +mosip.reg.xml.file.url=https://devops.mosip.io/artifactory/libs-release/io/mosip/registration/registration-client/maven-metadata.xml +mosip.reg.dbpath=db/reg +mosip.reg.client.tpm.availability=N + +#Cryptomanger +session_key.service.url=https://qa.mosip.io/v1/cryptomanager/encrypt +session_key.service.httpmethod=POST +session_key.service.requestType=java.lang.Object +session_key.service.headers=Content-Type:APPLICATION/JSON +session_key.service.authrequired=true +session_key.service.signrequired=false +session_key.service.authheader=Authorization:OAUTH +session_key.service.service.requestsignrequired=true diff --git a/api-test/src/main/resources/testCaseSkippedList.txt b/api-test/src/main/resources/testCaseSkippedList.txt new file mode 100644 index 000000000..df9d13d95 --- /dev/null +++ b/api-test/src/main/resources/testCaseSkippedList.txt @@ -0,0 +1,6 @@ +##### JIRA number;testcase +#MOSIP-12456------Mimoto_AddIdentity_Binding_smoke_Pos +INJIWEB-1619------Mimoto_DownloadIssuerCredentialWithGoogleLogin_IssuerMosip_NullInWallet_Neg +INJIWEB-1624------Mimoto_FetchAllCredentials_Empty_WalletId_And_Empty_Accepted_claims_Neg +INJIWEB-1666------Mimoto_BindingOtp_Valid_vid_All_Smoke +INJIWEB-1666------Mimoto_WalletBinding_vid_OTP_Valid_Smoke diff --git a/api-test/testNgXmlFiles/mimotoMasterTestSuite.xml b/api-test/testNgXmlFiles/mimotoMasterTestSuite.xml new file mode 100644 index 000000000..174bef942 --- /dev/null +++ b/api-test/testNgXmlFiles/mimotoMasterTestSuite.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/api-test/testNgXmlFiles/mimotoPrerequisiteSuite.xml b/api-test/testNgXmlFiles/mimotoPrerequisiteSuite.xml new file mode 100644 index 000000000..fcf4eb5f9 --- /dev/null +++ b/api-test/testNgXmlFiles/mimotoPrerequisiteSuite.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api-test/testNgXmlFiles/mimotoSuite.xml b/api-test/testNgXmlFiles/mimotoSuite.xml new file mode 100644 index 000000000..d4c52daaa --- /dev/null +++ b/api-test/testNgXmlFiles/mimotoSuite.xml @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/db_scripts/README.md b/db_scripts/README.md new file mode 100644 index 000000000..23fdf5e09 --- /dev/null +++ b/db_scripts/README.md @@ -0,0 +1,29 @@ +# Mimoto Database +Mimoto Database acts as a centralized data repository, ensuring that both Inji Mobile and Inji Web applications can access consistent and up-to-date information + +## Overview +This folder contains SQL scripts to create databases and tables in postgres. The table structure is described under the `/ddl/` folder. Default data populated in the tables can be found in the `/dml` folder. + +## Prerequisites +Before starting the installation, ensure the following: + +1. **Command line utilities**: + - `kubectl`: Kubernetes command-line tool. + - `helm`: Kubernetes package manager. + +2. **Helm Repositories**: + Add the required Helm repositories to your environment: + ```sh + helm repo add bitnami https://charts.bitnami.com/bitnami + helm repo add mosip https://mosip.github.io/mosip-helm +## Install in existing MOSIP K8 Cluster +These scripts are automatically run with below mentioned script in existing k8 cluster with Postgres installed. +### Install +* Set your kube_config file or kube_config variable on PC. +* Update `init_values.yaml` with db-common-password from the postgres namespace in the required field `dbUserPasswords.dbuserPassword` and ensure `databases.inji_mimoto` is enabled. + ``` + ./init_db.sh` + ``` + +## Install for developers +Developers may run the SQLs using `/deploy.sh` script. \ No newline at end of file diff --git a/db_scripts/copy_cm_func.sh b/db_scripts/copy_cm_func.sh new file mode 100755 index 000000000..691f1dbf5 --- /dev/null +++ b/db_scripts/copy_cm_func.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Copy configmap and secret from one namespace to another. +# ./copy_cm_func.sh [name] +# Parameters: +# resource: configmap|secret +# name: Optional new name of the configmap or secret in destination namespace. This may be needed if there is +# clash of names + +if [ $1 = "configmap" ] +then + RESOURCE=configmap +elif [ $1 = "secret" ] +then + RESOURCE=secret +else + echo "Incorrect resource $1. Exiting.." + exit 1 +fi + + +if [ $# -ge 5 ] +then + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $5 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | sed "s/name: $2/name: $5/g" | kubectl -n $4 create -f - +else + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $2 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | kubectl -n $4 create -f - +fi + + + diff --git a/db_scripts/init_db.sh b/db_scripts/init_db.sh new file mode 100755 index 000000000..410711b72 --- /dev/null +++ b/db_scripts/init_db.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Script to initialize mimoto DB. +## Usage: ./init_db.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +NS=injiweb +CHART_VERSION=0.0.1-develop + +helm repo add mosip https://mosip.github.io/mosip-helm +helm repo update + +while true; do + read -p "CAUTION: Do we already have Postgres installed? Also make sure the mimoto DB is backed up as the same will be overriden. Do you still want to continue?" yn + if [ $yn = "Y" ] + then + DB_USER_PASSWORD=$( kubectl -n postgres get secrets db-common-secrets -o jsonpath={.data.db-dbuser-password} | base64 -d ) + + kubectl create ns $NS + + echo Removing existing inji_mimoto DB installation + helm -n $NS delete postgres-init-mimoto + + echo Copy Postgres secrets + ./copy_cm_func.sh secret postgres-postgresql postgres $NS + + echo Delete existing DB common sets + kubectl -n $NS delete secret db-common-secrets + + echo Initializing DB + helm -n $NS install postgres-init-mimoto mosip/postgres-init -f init_values.yaml \ + --version $CHART_VERSION \ + --set dbUserPasswords.dbuserPassword="$DB_USER_PASSWORD" \ + --wait --wait-for-jobs + break + else + break + fi +done \ No newline at end of file diff --git a/db_scripts/init_values.yaml b/db_scripts/init_values.yaml new file mode 100644 index 000000000..8b65c8507 --- /dev/null +++ b/db_scripts/init_values.yaml @@ -0,0 +1,20 @@ +dbUserPasswords: + dbuserPassword: "" + +databases: + inji_mimoto: + enabled: true + scriptsDir: inji_mimoto # fixed - must match db_scripts/ folder in repo + dbName: inji_mimoto # customize e.g. inji_mimoto02 + dbUser: injimimotouser # customize e.g. injimimotouser02 + defaultDb: postgres + host: "postgres-postgresql" + port: 5432 + su: + user: postgres + secret: + name: postgres-postgresql + key: postgres-password + dml: 1 + repoUrl: https://github.com/mosip/mimoto.git + branch: develop \ No newline at end of file diff --git a/db_scripts/inji_mimoto/db.sql b/db_scripts/inji_mimoto/db.sql new file mode 100644 index 000000000..0e00567d7 --- /dev/null +++ b/db_scripts/inji_mimoto/db.sql @@ -0,0 +1,17 @@ +CREATE DATABASE :mosipdbname + ENCODING = 'UTF8' + LC_COLLATE = 'en_US.UTF-8' + LC_CTYPE = 'en_US.UTF-8' + TABLESPACE = pg_default + OWNER = postgres + TEMPLATE = template0; + +COMMENT ON DATABASE mosip_idp IS 'mimoto related data is stored in this database'; + +\c :mosipdbname postgres + +DROP SCHEMA IF EXISTS mimoto CASCADE; +CREATE SCHEMA mimoto; +ALTER SCHEMA mimoto OWNER TO postgres; +ALTER DATABASE :mosipdbname SET search_path TO mimoto,pg_catalog,public; + diff --git a/db_scripts/inji_mimoto/ddl.sql b/db_scripts/inji_mimoto/ddl.sql new file mode 100644 index 000000000..30e875775 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl.sql @@ -0,0 +1,12 @@ +\c :mosipdbname + +\ir ddl/mimoto-key_alias.sql +\ir ddl/mimoto-key_policy_def.sql +\ir ddl/mimoto-key_store.sql +\ir ddl/mimoto-ca_cert_store.sql +\ir ddl/mimoto-user_metadata.sql +\ir ddl/mimoto-wallet.sql +\ir ddl/mimoto-proof_signing_keys.sql +\ir ddl/mimoto-verifiable_credentials.sql +\ir ddl/mimoto-trusted-verifiers.sql +\ir ddl/mimoto-verifiable_presentations.sql diff --git a/db_scripts/inji_mimoto/ddl/mimoto-ca_cert_store.sql b/db_scripts/inji_mimoto/ddl/mimoto-ca_cert_store.sql new file mode 100644 index 000000000..2c9fd9520 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-ca_cert_store.sql @@ -0,0 +1,54 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : ca_cert_store +-- Purpose : Key Store table +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE ca_cert_store( + cert_id character varying(36) NOT NULL, + cert_subject character varying(500) NOT NULL, + cert_issuer character varying(500) NOT NULL, + issuer_id character varying(36) NOT NULL, + cert_not_before timestamp, + cert_not_after timestamp, + crl_uri character varying(120), + cert_data character varying, + cert_thumbprint character varying(100), + cert_serial_no character varying(50), + partner_domain character varying(36), + cr_by character varying(256), + cr_dtimes timestamp, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + ca_cert_type character varying(25), + CONSTRAINT pk_cacs_id PRIMARY KEY (cert_id), + CONSTRAINT cert_thumbprint_unique UNIQUE (cert_thumbprint,partner_domain) + +); +COMMENT ON TABLE ca_cert_store IS 'Certificate Authority Certificate Store: Store details of all the certificate provided by certificate authority which will be used by MOSIP'; +COMMENT ON COLUMN ca_cert_store.cert_id IS 'Certificate ID: Unique ID (UUID) will be generated and assigned to the uploaded CA/Sub-CA certificate'; +COMMENT ON COLUMN ca_cert_store.cert_subject IS 'Certificate Subject: Subject DN of the certificate'; +COMMENT ON COLUMN ca_cert_store.cert_issuer IS 'Certificate Issuer: Issuer DN of the certificate'; +COMMENT ON COLUMN ca_cert_store.issuer_id IS 'Issuer UUID of the certificate. (Issuer certificate should be available in the DB)'; +COMMENT ON COLUMN ca_cert_store.cert_not_before IS 'Certificate Start Date: Certificate Interval - Validity Start Date & Time'; +COMMENT ON COLUMN ca_cert_store.cert_not_after IS 'Certificate Validity end Date: Certificate Interval - Validity End Date & Time'; +COMMENT ON COLUMN ca_cert_store.crl_uri IS 'CRL URL: CRL URI of the issuer.'; +COMMENT ON COLUMN ca_cert_store.cert_data IS 'Certificate Data: PEM Encoded actual certificate data.'; +COMMENT ON COLUMN ca_cert_store.cert_thumbprint IS 'Certificate Thumb Print: SHA1 generated certificate thumbprint.'; +COMMENT ON COLUMN ca_cert_store.cert_serial_no IS 'Certificate Serial No: Serial Number of the certificate.'; +COMMENT ON COLUMN ca_cert_store.partner_domain IS 'Partner Domain : To add Partner Domain in CA/Sub-CA certificate chain'; +COMMENT ON COLUMN ca_cert_store.cr_by IS 'Created By : ID or name of the user who create / insert record'; +COMMENT ON COLUMN ca_cert_store.cr_dtimes IS 'Created DateTimestamp : Date and Timestamp when the record is created/inserted'; +COMMENT ON COLUMN ca_cert_store.upd_by IS 'Updated By : ID or name of the user who update the record with new values'; +COMMENT ON COLUMN ca_cert_store.upd_dtimes IS 'Updated DateTimestamp : Date and Timestamp when any of the fields in the record is updated with new values.'; +COMMENT ON COLUMN ca_cert_store.is_deleted IS 'IS_Deleted : Flag to mark whether the record is Soft deleted.'; +COMMENT ON COLUMN ca_cert_store.del_dtimes IS 'Deleted DateTimestamp : Date and Timestamp when the record is soft deleted with is_deleted=TRUE'; +COMMENT ON COLUMN ca_cert_store.ca_cert_type IS 'CA Certificate Type : Indicates if the certificate is a ROOT or INTERMEDIATE CA certificate'; diff --git a/db_scripts/inji_mimoto/ddl/mimoto-key_alias.sql b/db_scripts/inji_mimoto/ddl/mimoto-key_alias.sql new file mode 100644 index 000000000..83254481f --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-key_alias.sql @@ -0,0 +1,47 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : key_alias +-- Purpose : Key Alias table +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE key_alias( + id character varying(36) NOT NULL, + app_id character varying(36) NOT NULL, + ref_id character varying(128), + key_gen_dtimes timestamp, + key_expire_dtimes timestamp, + status_code character varying(36), + lang_code character varying(3), + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + cert_thumbprint character varying(100), + uni_ident character varying(50), + CONSTRAINT pk_keymals_id PRIMARY KEY (id), + CONSTRAINT uni_ident_const UNIQUE (uni_ident) +); + +COMMENT ON TABLE key_alias IS 'Contains key alias and metadata of all the keys used in MOSIP system.'; + +COMMENT ON COLUMN key_alias.id IS 'Unique identifier (UUID) used for referencing keys in key_store table and HSM'; +COMMENT ON COLUMN key_alias.app_id IS 'To reference a Module key'; +COMMENT ON COLUMN key_alias.ref_id IS 'To reference a Encryption key '; +COMMENT ON COLUMN key_alias.key_gen_dtimes IS 'Date and time when the key was generated.'; +COMMENT ON COLUMN key_alias.key_expire_dtimes IS 'Date and time when the key will be expired. This will be derived based on the configuration / policy defined in Key policy definition.'; +COMMENT ON COLUMN key_alias.status_code IS 'Status of the key, whether it is active or expired.'; +COMMENT ON COLUMN key_alias.lang_code IS 'For multilanguage implementation this attribute Refers master.language.code. The value of some of the attributes in current record is stored in this respective language. '; +COMMENT ON COLUMN key_alias.cr_by IS 'ID or name of the user who create / insert record'; +COMMENT ON COLUMN key_alias.cr_dtimes IS 'Date and Timestamp when the record is created/inserted'; +COMMENT ON COLUMN key_alias.upd_by IS 'ID or name of the user who update the record with new values'; +COMMENT ON COLUMN key_alias.upd_dtimes IS 'Date and Timestamp when any of the fields in the record is updated with new values.'; +COMMENT ON COLUMN key_alias.is_deleted IS 'Flag to mark whether the record is Soft deleted.'; +COMMENT ON COLUMN key_alias.del_dtimes IS 'Date and Timestamp when the record is soft deleted with is_deleted=TRUE'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/ddl/mimoto-key_policy_def.sql b/db_scripts/inji_mimoto/ddl/mimoto-key_policy_def.sql new file mode 100644 index 000000000..6fdd9d63a --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-key_policy_def.sql @@ -0,0 +1,36 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : key_policy_def +-- Purpose : Key Policy definition table +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE key_policy_def( + app_id character varying(36) NOT NULL, + key_validity_duration smallint, + is_active boolean NOT NULL, + pre_expire_days smallint, + access_allowed character varying(1024), + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + CONSTRAINT pk_keypdef_id PRIMARY KEY (app_id) +); +COMMENT ON TABLE key_policy_def IS 'Key Policy Defination: Policy related to encryption key management is defined here. For eg. Expiry duration of a key generated.'; +COMMENT ON COLUMN key_policy_def.app_id IS 'Application ID: Application id for which the key policy is defined'; +COMMENT ON COLUMN key_policy_def.key_validity_duration IS 'Key Validity Duration: Duration for which key is valid'; +COMMENT ON COLUMN key_policy_def.is_active IS 'IS_Active : Flag to mark whether the record is Active or In-active'; +COMMENT ON COLUMN key_policy_def.cr_by IS 'Created By : ID or name of the user who create / insert record'; +COMMENT ON COLUMN key_policy_def.cr_dtimes IS 'Created DateTimestamp : Date and Timestamp when the record is created/inserted'; +COMMENT ON COLUMN key_policy_def.upd_by IS 'Updated By : ID or name of the user who update the record with new values'; +COMMENT ON COLUMN key_policy_def.upd_dtimes IS 'Updated DateTimestamp : Date and Timestamp when any of the fields in the record is updated with new values.'; +COMMENT ON COLUMN key_policy_def.is_deleted IS 'IS_Deleted : Flag to mark whether the record is Soft deleted.'; +COMMENT ON COLUMN key_policy_def.del_dtimes IS 'Deleted DateTimestamp : Date and Timestamp when the record is soft deleted with is_deleted=TRUE'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/ddl/mimoto-key_store.sql b/db_scripts/inji_mimoto/ddl/mimoto-key_store.sql new file mode 100644 index 000000000..654ff760c --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-key_store.sql @@ -0,0 +1,37 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : key_store +-- Purpose : Key Store table +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE key_store( + id character varying(36) NOT NULL, + master_key character varying(36) NOT NULL, + private_key character varying(2500) NOT NULL, + certificate_data character varying NOT NULL, + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + CONSTRAINT pk_keystr_id PRIMARY KEY (id) +); + +COMMENT ON TABLE key_store IS 'Stores Encryption (Base) private keys along with certificates'; +COMMENT ON COLUMN key_store.id IS 'Unique identifier (UUID) for referencing keys'; +COMMENT ON COLUMN key_store.master_key IS 'UUID of the master key used to encrypt this key'; +COMMENT ON COLUMN key_store.private_key IS 'Encrypted private key'; +COMMENT ON COLUMN key_store.certificate_data IS 'X.509 encoded certificate data'; +COMMENT ON COLUMN key_store.cr_by IS 'ID or name of the user who create / insert record'; +COMMENT ON COLUMN key_store.cr_dtimes IS 'Date and Timestamp when the record is created/inserted'; +COMMENT ON COLUMN key_store.upd_by IS 'ID or name of the user who update the record with new values'; +COMMENT ON COLUMN key_store.upd_dtimes IS 'Date and Timestamp when any of the fields in the record is updated with new values.'; +COMMENT ON COLUMN key_store.is_deleted IS 'Flag to mark whether the record is Soft deleted.'; +COMMENT ON COLUMN key_store.del_dtimes IS 'Date and Timestamp when the record is soft deleted with is_deleted=TRUE'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/ddl/mimoto-proof_signing_keys.sql b/db_scripts/inji_mimoto/ddl/mimoto-proof_signing_keys.sql new file mode 100644 index 000000000..3ae51b065 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-proof_signing_keys.sql @@ -0,0 +1,30 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : proof_signing_key +-- Purpose : Stores wallet key-related information, including encrypted secret keys and metadata +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- 2025-03-27 User Initial table creation, referencing wallet for proof_signing_key encryption +-- ------------------------------------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS proof_signing_key ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referencing the wallet table + public_key TEXT NOT NULL, -- Public key for wallet + secret_key TEXT NOT NULL, -- Secret key, encrypted using proof_signing_key + key_metadata JSONB NOT NULL, -- Metadata about the public and private keys + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_wallet_id FOREIGN KEY (wallet_id) REFERENCES wallet (id) ON DELETE CASCADE +); + +COMMENT ON TABLE proof_signing_key IS 'Wallet Keys: Contains information about the wallet keys, including encrypted keys and metadata'; +COMMENT ON COLUMN proof_signing_key.id IS 'Primary Key: Unique identifier for the key'; +COMMENT ON COLUMN proof_signing_key.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table'; +COMMENT ON COLUMN proof_signing_key.public_key IS 'Public Key: The public key of the wallet'; +COMMENT ON COLUMN proof_signing_key.secret_key IS 'Secret Key: Encrypted using the proof_signing_key from wallet table'; +COMMENT ON COLUMN proof_signing_key.key_metadata IS 'Key Metadata: Contains additional information about the public and private keys'; +COMMENT ON COLUMN proof_signing_key.created_at IS 'Created At: The date and time when the key information was created'; +COMMENT ON COLUMN proof_signing_key.updated_at IS 'Updated At: The date and time when the key information was last updated'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/ddl/mimoto-trusted-verifiers.sql b/db_scripts/inji_mimoto/ddl/mimoto-trusted-verifiers.sql new file mode 100644 index 000000000..e7ecbc89e --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-trusted-verifiers.sql @@ -0,0 +1,22 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : trusted_verifiers +-- Purpose : Stores trusted verifiers associated with a user's wallet +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS trusted_verifiers ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + verifier_id character varying(255) NOT NULL, -- Stores the unique identifier (client_id) of the trusted verifier + created_at TIMESTAMP DEFAULT now() -- Timestamp of record creation (defaults to current time) +); + +COMMENT ON TABLE trusted_verifiers IS 'Trusted Verifiers: Contains information about verifiers trusted by a user''s wallet'; + +COMMENT ON COLUMN trusted_verifiers.id IS 'Primary Key: Unique identifier for the trusted verifier record'; +COMMENT ON COLUMN trusted_verifiers.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table, linking the verifier to a specific wallet'; +COMMENT ON COLUMN trusted_verifiers.verifier_id IS 'Verifier ID: Unique identifier (client_id) of the trusted verifier'; +COMMENT ON COLUMN trusted_verifiers.created_at IS 'Created At: The date and time when the trusted verifier record was created'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/ddl/mimoto-user_metadata.sql b/db_scripts/inji_mimoto/ddl/mimoto-user_metadata.sql new file mode 100644 index 000000000..334d39ed4 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-user_metadata.sql @@ -0,0 +1,33 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : user_metadata +-- Purpose : User Metadata table +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ + +CREATE TABLE IF NOT EXISTS user_metadata ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + provider_subject_id character varying(255) NOT NULL, -- Unique identifier for the provider subject + identity_provider character varying(255) NOT NULL, -- Unique identifier for the identity provider + display_name TEXT NOT NULL, -- Display name of the user + profile_picture_url TEXT, -- URL of the user's profile picture + phone_number TEXT, -- Phone number of the user + email TEXT NOT NULL, -- Email of the user (Required field) + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now() -- Timestamp of last update (defaults to current time) +); + +COMMENT ON TABLE user_metadata IS 'User Metadata: Contains details about the user such as identity provider, contact details, and display name'; + +COMMENT ON COLUMN user_metadata.id IS 'Primary Key: Unique identifier for the user metadata'; +COMMENT ON COLUMN user_metadata.provider_subject_id IS 'Provider Subject ID: Unique identifier for the subject assigned by the identity provider'; +COMMENT ON COLUMN user_metadata.identity_provider IS 'Identity Provider: The identity provider associated with the user'; +COMMENT ON COLUMN user_metadata.display_name IS 'Display Name: Name of the user'; +COMMENT ON COLUMN user_metadata.profile_picture_url IS 'Profile Picture URL: The URL link to the user''s profile picture'; +COMMENT ON COLUMN user_metadata.phone_number IS 'Phone Number: User''s phone number, if available'; +COMMENT ON COLUMN user_metadata.email IS 'Email: User''s email address'; +COMMENT ON COLUMN user_metadata.created_at IS 'Created At: The date and time when the metadata was created'; +COMMENT ON COLUMN user_metadata.updated_at IS 'Updated At: The date and time when the metadata was last updated'; diff --git a/db_scripts/inji_mimoto/ddl/mimoto-verifiable_credentials.sql b/db_scripts/inji_mimoto/ddl/mimoto-verifiable_credentials.sql new file mode 100644 index 000000000..7535addd6 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-verifiable_credentials.sql @@ -0,0 +1,27 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : verifiable_credentials +-- Purpose : Stores verifiable credentials related to the user, encrypted with wallet_key +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS verifiable_credentials ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + credential TEXT NOT NULL, -- Encrypted credential (using wallet_key for encryption/decryption) + credential_metadata JSONB NOT NULL, -- Metadata about the credential + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_wallet_id FOREIGN KEY (wallet_id) REFERENCES wallet (id) ON DELETE CASCADE +); + +COMMENT ON TABLE verifiable_credentials IS 'Verifiable Credentials: Contains user credentials, encrypted using wallet key'; +COMMENT ON COLUMN verifiable_credentials.id IS 'Primary Key: Unique identifier for the verifiable credential record'; +COMMENT ON COLUMN verifiable_credentials.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table, linked to the user''s wallet'; +COMMENT ON COLUMN verifiable_credentials.credential IS 'Credential: Encrypted credential using the wallet''s key'; +COMMENT ON COLUMN verifiable_credentials.credential_metadata IS 'Credential Metadata: Additional information about the credential (e.g., issuer, claims)'; +COMMENT ON COLUMN verifiable_credentials.created_at IS 'Created At: The date and time when the credential was created'; +COMMENT ON COLUMN verifiable_credentials.updated_at IS 'Updated At: The date and time when the credential was last updated'; diff --git a/db_scripts/inji_mimoto/ddl/mimoto-verifiable_presentations.sql b/db_scripts/inji_mimoto/ddl/mimoto-verifiable_presentations.sql new file mode 100644 index 000000000..24b20a5bd --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-verifiable_presentations.sql @@ -0,0 +1,33 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : verifiable_presentations +-- Purpose : Stores records of verifiable presentations submitted by a wallet to a verifier +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS verifiable_presentations ( + id character varying(36) PRIMARY KEY, -- Primary key for the presentation record (use presentationId) + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + auth_request JSONB NOT NULL, -- Verifier's authorization request payload (as JSON) + presentation_data JSONB NOT NULL, -- Additional metadata including shared credential ids + verifier_id character varying(255), -- Verifier identifier (e.g., client_id) + status character varying(32) NOT NULL, -- Submission status: in-progress/success/error + requested_at TIMESTAMP, -- Verifier request timestamp + created_at TIMESTAMP DEFAULT now(), -- Submission timestamp (defaults to current time) + consent BOOLEAN NOT NULL DEFAULT TRUE, -- User consent flag + + CONSTRAINT fk_vp_wallet_id FOREIGN KEY (wallet_id) REFERENCES wallet (id) ON DELETE CASCADE +); + +COMMENT ON TABLE verifiable_presentations IS 'Verifiable Presentations: Records of presentations shared with verifiers'; +COMMENT ON COLUMN verifiable_presentations.id IS 'Primary Key: Unique identifier for the presentation (presentationId)'; +COMMENT ON COLUMN verifiable_presentations.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table'; +COMMENT ON COLUMN verifiable_presentations.auth_request IS 'Authorization Request: The authorization request payload from the verifier'; +COMMENT ON COLUMN verifiable_presentations.presentation_data IS 'Presentation Data: Additional metadata including shared credential ids'; +COMMENT ON COLUMN verifiable_presentations.verifier_id IS 'Verifier Identifier: The verifier''s client_id or equivalent identifier'; +COMMENT ON COLUMN verifiable_presentations.status IS 'Status: in-progress/success/error'; +COMMENT ON COLUMN verifiable_presentations.requested_at IS 'Requested At: Timestamp when verifier requested the presentation'; +COMMENT ON COLUMN verifiable_presentations.created_at IS 'Created At: Timestamp when the submission record was created'; +COMMENT ON COLUMN verifiable_presentations.consent IS 'Consent: Indicates whether user consented to sharing'; diff --git a/db_scripts/inji_mimoto/ddl/mimoto-wallet.sql b/db_scripts/inji_mimoto/ddl/mimoto-wallet.sql new file mode 100644 index 000000000..f827bcd98 --- /dev/null +++ b/db_scripts/inji_mimoto/ddl/mimoto-wallet.sql @@ -0,0 +1,27 @@ +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : wallet +-- Purpose : Wallet Information table for user, encrypted with AES256-GCM, including key metadata +-- +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS wallet ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + user_id character varying(36) NOT NULL, -- Foreign key referencing user_metadata + wallet_key TEXT NOT NULL, -- Encrypted wallet key (retained here) + wallet_metadata JSONB NOT NULL, -- Metadata about the wallet, including encryption info + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user_metadata (id) ON DELETE CASCADE +); + +COMMENT ON TABLE wallet IS 'Wallet: Contains general information about the user''s wallet and metadata'; +COMMENT ON COLUMN wallet.id IS 'Primary Key: Unique identifier for the wallet'; +COMMENT ON COLUMN wallet.user_id IS 'User ID: Foreign key referring to the user_metadata table'; +COMMENT ON COLUMN wallet.wallet_key IS 'Wallet Key: Encrypted wallet key used to encrypt secret keys in wallet_keys'; +COMMENT ON COLUMN wallet.wallet_metadata IS 'Wallet Metadata: Contains information about the wallet, including encryption and PIN usage'; +COMMENT ON COLUMN wallet.created_at IS 'Created At: The date and time when the wallet was created'; +COMMENT ON COLUMN wallet.updated_at IS 'Updated At: The date and time when the wallet was last updated'; \ No newline at end of file diff --git a/db_scripts/inji_mimoto/deploy.properties b/db_scripts/inji_mimoto/deploy.properties new file mode 100644 index 000000000..ba7872363 --- /dev/null +++ b/db_scripts/inji_mimoto/deploy.properties @@ -0,0 +1,7 @@ +DB_SERVERIP= +DB_PORT=5432 +SU_USER=postgres +DEFAULT_DB_NAME=postgres +MOSIP_DB_NAME=inji_mimoto +DB_UNAME=mimotouser +DML_FLAG=1 \ No newline at end of file diff --git a/db_scripts/inji_mimoto/deploy.sh b/db_scripts/inji_mimoto/deploy.sh new file mode 100755 index 000000000..4965c903b --- /dev/null +++ b/db_scripts/inji_mimoto/deploy.sh @@ -0,0 +1,43 @@ + +## Properties file +set -e +properties_file="$1" +echo `date "+%m/%d/%Y %H:%M:%S"` ": $properties_file" +if [ -f "$properties_file" ] +then + echo `date "+%m/%d/%Y %H:%M:%S"` ": Property file \"$properties_file\" found." + while IFS='=' read -r key value || [ -n "$key" ] + do + key=$(echo $key | tr '.' '_') + eval ${key}=\${value} + done < "$properties_file" +else + echo `date "+%m/%d/%Y %H:%M:%S"` ": Property file not found, Pass property file name as argument." +fi + +## Terminate existing connections +echo "Terminating active connections" +CONN=$(PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -t -c "SELECT count(pg_terminate_backend(pg_stat_activity.pid)) FROM pg_stat_activity WHERE datname = '$MOSIP_DB_NAME' AND pid <> pg_backend_pid()";exit;) +echo "Terminated connections" + +## Drop db and role +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f drop_db.sql -v mosipdbname=$MOSIP_DB_NAME +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f drop_role.sql -v dbuname=$DB_UNAME + +## Create users +echo `date "+%m/%d/%Y %H:%M:%S"` ": Creating database users" | tee +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f role_dbuser.sql -v dbuserpwd=\'$DBUSER_PWD\' -v dbuname=$DB_UNAME + +## Create DB +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f db.sql -v mosipdbname=$MOSIP_DB_NAME +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f ddl.sql -v mosipdbname=$MOSIP_DB_NAME -v dbuname=$DB_UNAME + +## Grants +PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -f grants.sql -v mosipdbname=$MOSIP_DB_NAME -v dbuname=$DB_UNAME + +## Populate tables +if [ ${DML_FLAG} == 1 ] +then + echo `date "+%m/%d/%Y %H:%M:%S"` ": Deploying DML for ${MOSIP_DB_NAME} database" + PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -a -b -f dml.sql -v mosipdbname=$MOSIP_DB_NAME +fi \ No newline at end of file diff --git a/db_scripts/inji_mimoto/dml.sql b/db_scripts/inji_mimoto/dml.sql new file mode 100644 index 000000000..36e788cbf --- /dev/null +++ b/db_scripts/inji_mimoto/dml.sql @@ -0,0 +1,23 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- + +\c :mosipdbname + +\COPY mimoto.key_policy_def (APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) FROM './dml/mimoto-key_policy_def.csv' delimiter ',' HEADER csv; + + + + + + + + + + + + + + + diff --git a/db_scripts/inji_mimoto/dml/mimoto-key_policy_def.csv b/db_scripts/inji_mimoto/dml/mimoto-key_policy_def.csv new file mode 100644 index 000000000..88de66fa0 --- /dev/null +++ b/db_scripts/inji_mimoto/dml/mimoto-key_policy_def.csv @@ -0,0 +1,4 @@ +app_id,key_validity_duration,pre_expire_days,access_allowed,is_active,cr_by,cr_dtimes +ROOT,2920,1125,NA,TRUE,mosipadmin,now() +MIMOTO,1095,60,NA,TRUE,mosipadmin,now() +BASE,730,60,NA,TRUE,mosipadmin,now() diff --git a/db_scripts/inji_mimoto/drop_db.sql b/db_scripts/inji_mimoto/drop_db.sql new file mode 100644 index 000000000..cd0228d2b --- /dev/null +++ b/db_scripts/inji_mimoto/drop_db.sql @@ -0,0 +1,7 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- + +DROP DATABASE IF EXISTS :mosipdbname; + diff --git a/db_scripts/inji_mimoto/drop_role.sql b/db_scripts/inji_mimoto/drop_role.sql new file mode 100644 index 000000000..e58ac3cdd --- /dev/null +++ b/db_scripts/inji_mimoto/drop_role.sql @@ -0,0 +1,6 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- + +drop role if exists :dbuname; diff --git a/db_scripts/inji_mimoto/grants.sql b/db_scripts/inji_mimoto/grants.sql new file mode 100644 index 000000000..290a6a295 --- /dev/null +++ b/db_scripts/inji_mimoto/grants.sql @@ -0,0 +1,22 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- + +\c :mosipdbname + +GRANT CONNECT + ON DATABASE :mosipdbname + TO :dbuname; + +GRANT USAGE + ON SCHEMA mimoto + TO :dbuname; + +GRANT SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES + ON ALL TABLES IN SCHEMA mimoto + TO :dbuname; + +ALTER DEFAULT PRIVILEGES IN SCHEMA mimoto + GRANT SELECT,INSERT,UPDATE,DELETE,REFERENCES ON TABLES TO :dbuname; + diff --git a/db_scripts/inji_mimoto/role_dbuser.sql b/db_scripts/inji_mimoto/role_dbuser.sql new file mode 100644 index 000000000..40b21d05d --- /dev/null +++ b/db_scripts/inji_mimoto/role_dbuser.sql @@ -0,0 +1,9 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- + +CREATE ROLE :dbuname WITH + INHERIT + LOGIN + PASSWORD :dbuserpwd; diff --git a/db_upgrade_script/inji_mimoto/README.md b/db_upgrade_script/inji_mimoto/README.md new file mode 100644 index 000000000..883fe3868 --- /dev/null +++ b/db_upgrade_script/inji_mimoto/README.md @@ -0,0 +1,58 @@ +# Database Upgrade & Rollback Script + +This script automates database schema upgrades and rollbacks for the project. + +## Prerequisites + +- Bash shell (Linux/macOS) +- PostgreSQL client (`psql`) +- A properties file with required DB credentials and parameters + +## Usage + +```sh +./upgrade.sh +``` + +## Properties File Format + +The properties file should contain the following keys: + +``` +SU_USER= +SU_USER_PWD= +DB_SERVERIP= +DB_PORT= +DEFAULT_DB_NAME= +DB_NAME= +CURRENT_VERSION= +UPGRADE_VERSION= +ACTION= +PRIMARY_LANGUAGE_CODE= +``` + +## Actions + +- **Upgrade:** + Set `ACTION=upgrade` to apply the upgrade SQL script. +- **Rollback:** + Set `ACTION=rollback` to apply the rollback SQL script. + +## Script Behavior + +- Loads DB credentials and parameters from the properties file. +- Terminates active connections to the target database. +- Executes the appropriate SQL script from the `sql/` directory: + - Upgrade: `sql/_to__upgrade.sql` + - Rollback: `sql/_to__rollback.sql` + +## Example + +```sh +./upgrade.sh upgrade.properties +``` + +--- + +**Note:** +Ensure the required SQL scripts exist in the `sql/` directory before running the script. \ No newline at end of file diff --git a/db_upgrade_script/inji_mimoto/sql/0.18.0_to_0.18.1_rollback.sql b/db_upgrade_script/inji_mimoto/sql/0.18.0_to_0.18.1_rollback.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.18.0_to_0.18.1_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.18.0_to_0.18.1_upgrade.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.18.1_to_0.19.0_rollback.sql b/db_upgrade_script/inji_mimoto/sql/0.18.1_to_0.19.0_rollback.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.18.1_to_0.19.0_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.18.1_to_0.19.0_upgrade.sql new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/application.properties b/db_upgrade_script/inji_mimoto/sql/0.19.0_to_0.19.1_rollback.sql similarity index 100% rename from src/main/resources/application.properties rename to db_upgrade_script/inji_mimoto/sql/0.19.0_to_0.19.1_rollback.sql diff --git a/db_upgrade_script/inji_mimoto/sql/0.19.0_to_0.19.1_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.19.0_to_0.19.1_upgrade.sql new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/db_upgrade_script/inji_mimoto/sql/0.19.0_to_0.19.1_upgrade.sql @@ -0,0 +1 @@ + diff --git a/db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.19.2_rollback.sql b/db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.19.2_rollback.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.19.2_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.19.1_to_0.19.2_upgrade.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_rollback.sql b/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_rollback.sql new file mode 100644 index 000000000..e877ec237 --- /dev/null +++ b/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_rollback.sql @@ -0,0 +1,21 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : trusted_verifiers +-- Purpose : To drop the trusted_verifiers table that was created in 0.14.x_to_0.15.x_upgrade.sql to make it backward compatibility +-- +-- Create By : Bhargavi Puvvada, Durgesh Konga +-- Created Date : 11-sep-2025 +-- +-- Modified Date Modified By Comments / Remarks +-- 07-Oct-2025 Durgesh Konga Added verifiable_presentations table to make it backward compatibility +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------ + +-- Drop table for trusted_verifiers +DROP TABLE IF EXISTS trusted_verifiers; + +-- Drop table for verifiable_presentations +DROP TABLE IF EXISTS verifiable_presentations; \ No newline at end of file diff --git a/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_upgrade.sql new file mode 100644 index 000000000..c63b76ec9 --- /dev/null +++ b/db_upgrade_script/inji_mimoto/sql/0.19.2_to_0.20.0_upgrade.sql @@ -0,0 +1,56 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: inji_mimoto +-- Table Name : trusted_verifiers +-- Purpose : To store the verifiers details Which are trusted by the Wallet User +-- +-- Create By : Bhargavi Puvvada +-- Created Date : 09-Sep-2025 +-- +-- Modified Date Modified By Comments / Remarks +-- 07-Oct-2025 Durgesh Konga Added verifiable_presentations table to make it backward compatibility +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ + +--- Create table for trusted_verifiers + +CREATE TABLE IF NOT EXISTS trusted_verifiers ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + verifier_id character varying(255) NOT NULL, -- Stores the unique identifier (client_id) of the trusted verifier + created_at TIMESTAMP DEFAULT now() -- Timestamp of record creation (defaults to current time) + ); + +-- Add comments for documentation +COMMENT ON TABLE trusted_verifiers IS 'Trusted Verifiers: Contains information about verifiers trusted by a user''s wallet'; + +COMMENT ON COLUMN trusted_verifiers.id IS 'Primary Key: Unique identifier for the trusted verifier record'; +COMMENT ON COLUMN trusted_verifiers.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table, linking the verifier to a specific wallet'; +COMMENT ON COLUMN trusted_verifiers.verifier_id IS 'Verifier ID: Unique identifier (client_id) of the trusted verifier'; +COMMENT ON COLUMN trusted_verifiers.created_at IS 'Created At: The date and time when the trusted verifier record was created'; + +-- Create table for verifiable_presentations + +CREATE TABLE IF NOT EXISTS verifiable_presentations ( + id character varying(36) PRIMARY KEY, + wallet_id character varying(36) NOT NULL, + auth_request JSONB NOT NULL, + presentation_data JSONB NOT NULL, + verifier_id character varying(255), + status character varying(32) NOT NULL, + requested_at TIMESTAMP, + created_at TIMESTAMP DEFAULT now(), + consent BOOLEAN NOT NULL DEFAULT TRUE +); +COMMENT ON TABLE verifiable_presentations IS 'Verifiable Presentations: Records of presentations shared with verifiers'; +COMMENT ON COLUMN verifiable_presentations.id IS 'Primary Key: Unique identifier for the presentation (presentationId)'; +COMMENT ON COLUMN verifiable_presentations.wallet_id IS 'Wallet ID: Foreign key referring to the wallet table'; +COMMENT ON COLUMN verifiable_presentations.auth_request IS 'Authorization Request: The authorization request payload from the verifier'; +COMMENT ON COLUMN verifiable_presentations.presentation_data IS 'Presentation Data: Additional metadata including shared credential ids'; +COMMENT ON COLUMN verifiable_presentations.verifier_id IS 'Verifier Identifier: The verifier''s client_id or equivalent identifier'; +COMMENT ON COLUMN verifiable_presentations.status IS 'Status: in-progress/success/error'; +COMMENT ON COLUMN verifiable_presentations.requested_at IS 'Requested At: Timestamp when verifier requested the presentation'; +COMMENT ON COLUMN verifiable_presentations.created_at IS 'Created At: Timestamp when the submission record was created'; +COMMENT ON COLUMN verifiable_presentations.consent IS 'Consent: Indicates whether user consented to sharing'; diff --git a/db_upgrade_script/inji_mimoto/sql/0.20.0_to_0.21.0_rollback.sql b/db_upgrade_script/inji_mimoto/sql/0.20.0_to_0.21.0_rollback.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/sql/0.20.0_to_0.21.0_upgrade.sql b/db_upgrade_script/inji_mimoto/sql/0.20.0_to_0.21.0_upgrade.sql new file mode 100644 index 000000000..e69de29bb diff --git a/db_upgrade_script/inji_mimoto/upgrade.properties b/db_upgrade_script/inji_mimoto/upgrade.properties new file mode 100644 index 000000000..ba357784c --- /dev/null +++ b/db_upgrade_script/inji_mimoto/upgrade.properties @@ -0,0 +1,12 @@ +ACTION=upgrade +DB_NAME= +DB_SERVERIP= +DB_PORT= +SU_USER=postgres +SU_USER_PWD= +SYS_ADMIN_USER= +SYS_ADMIN_PWD= +DEFAULT_DB_NAME=postgres +DBUSER_PWD= +CURRENT_VERSION= +UPGRADE_VERSION= \ No newline at end of file diff --git a/db_upgrade_script/inji_mimoto/upgrade.sh b/db_upgrade_script/inji_mimoto/upgrade.sh new file mode 100644 index 000000000..b4f61444d --- /dev/null +++ b/db_upgrade_script/inji_mimoto/upgrade.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -e +properties_file="$1" +echo `date "+%m/%d/%Y %H:%M:%S"` ": $properties_file" +if [ -f "$properties_file" ] +then + echo `date "+%m/%d/%Y %H:%M:%S"` ": Property file \"$properties_file\" found." + while IFS='=' read -r key value + do + key=$(echo $key | tr '.' '_') + eval ${key}=\${value} + done < "$properties_file" +else + echo `date "+%m/%d/%Y %H:%M:%S"` ": Property file not found, Pass property file name as argument." +fi + +echo "Current version: $CURRENT_VERSION" +echo "UPGRADE version: $UPGRADE_VERSION" +echo "Action: $ACTION" + +# Terminate existing connections +echo "Terminating active connections" +CONN=$(PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -t -c "SELECT count(pg_terminate_backend(pg_stat_activity.pid)) FROM pg_stat_activity WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid()";exit;) +echo "Terminated connections" + +# Execute upgrade or rollback +if [ "$ACTION" == "upgrade" ]; then + echo "Upgrading database from $CURRENT_VERSION to $UPGRADE_VERSION" + UPGRADE_SCRIPT_FILE="sql/${CURRENT_VERSION}_to_${UPGRADE_VERSION}_upgrade.sql" + if [ -f "$UPGRADE_SCRIPT_FILE" ]; then + echo "Executing upgrade script $UPGRADE_SCRIPT_FILE" + PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -v primary_language_code=$PRIMARY_LANGUAGE_CODE -a -b -f $UPGRADE_SCRIPT_FILE + else + echo "Upgrade script not found, exiting." + exit 1 + fi +elif [ "$ACTION" == "rollback" ]; then + echo "Rolling back database for $CURRENT_VERSION to $UPGRADE_VERSION" + REVOKE_SCRIPT_FILE="sql/${CURRENT_VERSION}_to_${UPGRADE_VERSION}_rollback.sql" + if [ -f "$REVOKE_SCRIPT_FILE" ]; then + echo "Executing rollback script $REVOKE_SCRIPT_FILE" + PGPASSWORD=$SU_USER_PWD psql -v ON_ERROR_STOP=1 --username=$SU_USER --host=$DB_SERVERIP --port=$DB_PORT --dbname=$DEFAULT_DB_NAME -v primary_language_code=$PRIMARY_LANGUAGE_CODE -a -b -f $REVOKE_SCRIPT_FILE + else + echo "rollback script not found, exiting." + exit 1 + fi +else + echo "Unknown action: $ACTION, must be 'upgrade' or 'rollback'." + exit 1 +fi \ No newline at end of file diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 000000000..a78523c36 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,89 @@ +# Deployment + +## Pre-requisites +* Base infrastructure setup + * Tools and utilities to be installed locally [steps](https://docs.inji.io/readme/setup/deploy#tools-and-utilities) + * System Requirements: Hardware, network and certificate requirements [steps](https://docs.inji.io/readme/setup/deploy#system-requirements) + * Set up Wireguard Bastion Host [steps](https://docs.inji.io/readme/setup/deploy#wireguard) + * K8s Cluster setup [steps](https://docs.inji.io/readme/setup/deploy#k8-cluster-setup) + * NGINX setup and configuration [steps](https://docs.inji.io/readme/setup/deploy#nginx-for-inji-k8-cluster) + * K8s Cluster Configuration [steps](https://docs.inji.io/readme/setup/deploy#k8-cluster-configuration) +* inji-stack-config ConfigMap [steps](https://docs.inji.io/readme/setup/deploy#pre-requisites) +* Postgres installation [steps](https://github.com/mosip/mosip-infra/tree/v1.2.0.2/deployment/v3/external/postgres) + * Note: Before running the Postgres install script, update the `POSTGRES_HOST` value in `install.sh` with the correct PostgreSQL host. +* Config server secerts [steps](https://github.com/mosip/mosip-infra/tree/v1.2.0.2/deployment/v3/mosip/conf-secrets) +* Config server installation [steps](https://docs.inji.io/readme/setup/deploy#config-server-installation) +* Artifactory installation [steps](https://github.com/mosip/artifactory-ref-impl/tree/v1.3.0-beta.2/deploy) + +* Redis installation +``` +cd deploy/redis +./install.sh +``` +## Initialise pre-requisites +### [DB init](../db_scripts) +* Update values file for postgres init [here](../db_scripts/init_values.yaml). +```` + cd ../../db_scripts + ./init_db.sh +```` + +## [Install Onboarder](../partner-onboarder) +* Execute Onboarder install script +``` +cd ../partner-onboarder +./install.sh + ``` +* During the execution of the `install.sh` script, a prompt appears requesting information for the S3 bucket, including its name and URL. +* Once the job is completed, log in to S3 / NFS and check the reports. There should not be any failures. +* Note: If you are running the Onboarder in a separate INJI cluster, update the extraEnvVars section accordingly in [values.yaml](../partner-onboarder/values.yaml). + +## Install mimoto and datashare +* Execute mimoto install script +* Before installing Mimoto, please ensure that the database host and port are correctly configured in the [values.yaml](mimoto/values.yaml) file. +* **Datashare Deployment:** + * If you are using **Inji Web Wallet**, datashare will be deployed as part of the Mimoto installation. + * If you are using only **Inji Mobile Wallet**, datashare deployment is not required. +* Note: During the installation of Mimoto and Datashare, ensure that the active_profile_env parameter in the ConfigMap of the config-server-share (in the injiweb namespace) is set to: default,inji-default,standalone + +### To Deploy Mimoto WITHOUT Datashare +If you are using only Inji Mobile Wallet and do not need datashare, follow these steps: + +1. **Edit the install.sh script:** + - Open `deploy/mimoto/install.sh` + - Locate the datashare deployment section (around line 61-66) + - Comment out or remove the following lines: + ```bash + INJI_DATASHARE_HOST=$(kubectl get cm inji-stack-config -o jsonpath={.data.inji-datashare-host}) + echo "Installing datashare" + helm -n $NS install datashare-inji mosip/datashare \ + -f datashare-values.yaml \ + --version $DATASHARE_CHART_VERSION + ``` + +2. **Configure Mimoto for Mobile Wallet only:** + - Ensure that `qr_code_type` is set to `"EmbeddedVC"` in your `mimoto-issuers-config.json` file + - This embeds the entire Verifiable Credential in the QR code. + +3. **Run the modified install script:** +```bash +cd ../deploy/mimoto +./install.sh + ``` + +### To Deploy Mimoto WITH Datashare (Default) +If you are using Inji Web Wallet or need datashare for online verification: +``` +cd ../deploy/mimoto +./install.sh + ``` +* During the execution of the `install.sh` script, a prompt appears requesting information regarding the presence of a public domain and a valid SSL certificate on the server. +* If the server lacks a public domain and a valid SSL certificate, it is advisable to select the `n` option. Opting it will enable the `init-container` with an `emptyDir` volume and include it in the deployment process. +* The init-container will proceed to download the server's self-signed SSL certificate and mount it to the specified location within the container's Java keystore (i.e., `cacerts`) file. +* This particular functionality caters to scenarios where the script needs to be employed on a server utilizing self-signed SSL certificates. + +### For Onboarding new Issuer for VCI: + +- create a folder "certs" in the root and a file "oidckeystore.p12" inside certs and store the keys as different aliases for every issuers. for more details refer [here](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/customization-overview/credential_providers) + +## [mimoto apitestrig](mimoto-apitestrig) diff --git a/deploy/copy_cm_func.sh b/deploy/copy_cm_func.sh new file mode 100755 index 000000000..d05154a54 --- /dev/null +++ b/deploy/copy_cm_func.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Copy configmap and secret from one namespace to another. +# ./copy_cm_func.sh [name] +# Parameters: +# resource: configmap|secret +# name: Optional new name of the configmap or secret in destination namespace. This may be needed if there is +# clash of names + +if [ $1 = "configmap" ] +then + RESOURCE=configmap +elif [ $1 = "secret" ] +then + RESOURCE=secret +else + echo "Incorrect resource $1. Exiting.." + exit 1 +fi + + +if [ $# -ge 5 ] +then + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $5 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | sed "s/name: $2/name: $5/g" | kubectl -n $4 create -f - +else + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $2 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | kubectl -n $4 create -f - +fi \ No newline at end of file diff --git a/deploy/mimoto-apitestrig/README.md b/deploy/mimoto-apitestrig/README.md new file mode 100644 index 000000000..60ba3eaac --- /dev/null +++ b/deploy/mimoto-apitestrig/README.md @@ -0,0 +1,44 @@ +# APITESTRIG + +## Introduction +ApiTestRig will test the working of APIs of the MOSIP modules. + +## Install +* Review `values.yaml` and, Make sure to enable required modules for apitestrig operation. +* Install +```sh +./install.sh +``` +* During the execution of the `install.sh` script, a prompt appears requesting information regarding the presence of a public domain and a valid SSL certificate on the server. +* If the server lacks a public domain and a valid SSL certificate, it is advisable to select the `n` option. Opting it will enable the `init-container` with an `emptyDir` volume and include it in the deployment process. +* The init-container will proceed to download the server's self-signed SSL certificate and mount it to the specified location within the container's Java keystore (i.e., `cacerts`) file. +* This particular functionality caters to scenarios where the script needs to be employed on a server utilizing self-signed SSL certificates. + +## Uninstall +* To uninstall ApiTestRig, run `delete.sh` script. +```sh +./delete.sh +``` + +## Run apitestrig manually + +#### Rancher UI +* Run apitestrig manually via Rancher UI. + ![apitestrig-2.png](../../docs/images/apitestrig-2.png) +* There are two modes of apitestrig `smoke` & `smokeAndRegression`. +* By default, apitestrig will execute with `smokeAndRegression`.
+ If you want to run apitestrig with only `smoke`.
+ You have to update the `apitestrig` configmap and rerun the specific apitestrig job. + +#### CLI +* Download Kubernetes cluster `kubeconfig` file from `rancher dashboard` to your local. + ![apitestrig-1.png](../../docs/images/apitestrig-1.png) +* Install `kubectl` package to your local machine. +* Run apitestrig manually via CLI by creating a new job from an existing k8s cronjob. + ``` + kubectl --kubeconfig= -n apitestrig create job --from=cronjob/ + ``` + example: + ``` + kubectl --kubeconfig=/home/xxx/Downloads/qa4.config -n apitestrig create job --from=cronjob/cronjob-apitestrig-masterdata cronjob-apitestrig-masterdata + ``` \ No newline at end of file diff --git a/deploy/mimoto-apitestrig/delete.sh b/deploy/mimoto-apitestrig/delete.sh new file mode 100755 index 000000000..f458ae576 --- /dev/null +++ b/deploy/mimoto-apitestrig/delete.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Uninstalls apitestrig +## Usage: ./delete.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +function deleting_apitestrig() { + NS=injiweb + while true; do + read -p "Are you sure you want to delete apitestrig helm charts?(Y/n) " yn + if [ $yn = "Y" ] + then + helm -n $NS delete apitestrig + break + else + break + fi + done + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +deleting_apitestrig # calling function \ No newline at end of file diff --git a/deploy/mimoto-apitestrig/install.sh b/deploy/mimoto-apitestrig/install.sh new file mode 100755 index 000000000..46adb0f4c --- /dev/null +++ b/deploy/mimoto-apitestrig/install.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Installs apitestrig +## Usage: ./install.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +NS=injiweb +CHART_VERSION=0.0.1-develop + +echo Create $NS namespace +kubectl create ns $NS + +function installing_apitestrig() { + helm repo update + + echo Copy Configmaps + COPY_UTIL=../copy_cm_func.sh + $COPY_UTIL configmap inji-stack-config default $NS + $COPY_UTIL configmap keycloak-host keycloak $NS + $COPY_UTIL configmap artifactory-share artifactory $NS + $COPY_UTIL configmap config-server-share config-server $NS + + echo echo Copy Secrtes + $COPY_UTIL secret keycloak-client-secrets keycloak $NS + $COPY_UTIL secret s3 s3 $NS + $COPY_UTIL secret postgres-postgresql postgres $NS + + echo "Delete s3, db, & apitestrig configmap if exists" + kubectl -n $NS delete --ignore-not-found=true configmap s3 + kubectl -n $NS delete --ignore-not-found=true configmap db + kubectl -n $NS delete --ignore-not-found=true configmap apitestrig + + DB_HOST=$( kubectl -n default get cm inji-stack-config -o json |jq -r '.data."api-internal-host"' ) + API_INTERNAL_HOST=$( kubectl -n default get cm inji-stack-config -o json |jq -r '.data."api-internal-host"' ) + ENV_USER=$( kubectl -n default get cm inji-stack-config -o json |jq -r '.data."api-internal-host"' | awk -F '.' '/api-internal/{print $1"."$2}') + + read -p "Please enter the time(hr) to run the cronjob every day (time: 0-23) : " time + if [ -z "$time" ]; then + echo "ERROT: Time cannot be empty; EXITING;"; + exit 1; + fi + if ! [ $time -eq $time ] 2>/dev/null; then + echo "ERROR: Time $time is not a number; EXITING;"; + exit 1; + fi + if [ $time -gt 23 ] || [ $time -lt 0 ] ; then + echo "ERROR: Time should be in range ( 0-23 ); EXITING;"; + exit 1; + fi + + echo "Do you have public domain & valid SSL? (Y/n) " + echo "Y: if you have public domain & valid ssl certificate" + echo "n: If you don't have a public domain and a valid SSL certificate. Note: It is recommended to use this option only in development environments." + read -p "" flag + + if [ -z "$flag" ]; then + echo "'flag' was provided; EXITING;" + exit 1; + fi + ENABLE_INSECURE='' + if [ "$flag" = "n" ]; then + ENABLE_INSECURE='--set enable_insecure=true'; + fi + + read -p "Please provide the retention days to remove old reports ( Default: 3 )" reportExpirationInDays + + if [[ -z $reportExpirationInDays ]]; then + reportExpirationInDays=3 + fi + if ! [[ $reportExpirationInDays =~ ^[0-9]+$ ]]; then + echo "The variable \"reportExpirationInDays\" should contain only number; EXITING"; + exit 1; + fi + + read -p "Please provide slack webhook URL to notify server end issues on your slack channel : " slackWebhookUrl + + if [ -z $slackWebhookUrl ]; then + echo "slack webhook URL not provided; EXITING;" + exit 1; + fi + + valid_inputs=("yes" "no") + eSignetDeployed="" + + while [[ ! " ${valid_inputs[@]} " =~ " ${eSignetDeployed} " ]]; do + read -p "Is the eSignet service deployed? (yes/no): " eSignetDeployed + eSignetDeployed=${eSignetDeployed,,} # Convert input to lowercase + done + + if [[ $eSignetDeployed == "yes" ]]; then + echo "eSignet service is deployed. Proceeding with installation..." + else + echo "eSignet service is not deployed. hence will be skipping esignet related test-cases..." + fi + + echo Installing apitestrig + helm -n $NS install apitestrig mosip/apitestrig \ + --set crontime="0 $time * * *" \ + -f values.yaml \ + --version $CHART_VERSION \ + --set apitestrig.configmaps.s3.s3-host='http://minio.minio:9000' \ + --set apitestrig.configmaps.s3.s3-user-key='admin' \ + --set apitestrig.configmaps.s3.s3-region='' \ + --set apitestrig.configmaps.db.db-server="$DB_HOST" \ + --set apitestrig.configmaps.db.db-su-user="postgres" \ + --set apitestrig.configmaps.db.db-port="5432" \ + --set apitestrig.configmaps.apitestrig.ENV_USER="$ENV_USER" \ + --set apitestrig.configmaps.apitestrig.ENV_ENDPOINT="https://$API_INTERNAL_HOST" \ + --set apitestrig.configmaps.apitestrig.ENV_TESTLEVEL="smokeAndRegression" \ + --set apitestrig.configmaps.apitestrig.reportExpirationInDays="$reportExpirationInDays" \ + --set apitestrig.configmaps.apitestrig.slack-webhook-url="$slackWebhookUrl" \ + --set apitestrig.configmaps.apitestrig.eSignetDeployed="$eSignetDeployed" \ + --set apitestrig.configmaps.apitestrig.NS="$NS" \ + $ENABLE_INSECURE + + echo Installed apitestrig. + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +installing_apitestrig # calling function diff --git a/deploy/mimoto-apitestrig/values.yaml b/deploy/mimoto-apitestrig/values.yaml new file mode 100644 index 000000000..7e439f42e --- /dev/null +++ b/deploy/mimoto-apitestrig/values.yaml @@ -0,0 +1,109 @@ +commonLabels: + app.kubernetes.io/component: inji + +containerSecurityContext: + enabled: false + runAsUser: inji + runAsNonRoot: true + +modules: + mimoto: + enabled: true + image: + repository: injistack/apitest-mimoto + tag: develop + pullPolicy: Always + masterdata: + enabled: false + image: + repository: mosipqa/apitest-masterdata + tag: develop + pullPolicy: Always + prereg: + enabled: false + image: + repository: mosipqa/apitest-prereg + tag: develop + pullPolicy: Always + idrepo: + enabled: false + image: + repository: mosipqa/apitest-idrepo + tag: develop + pullPolicy: Always + partner: + enabled: false + image: + repository: mosipqa/apitest-partner + tag: develop + pullPolicy: Always + resident: + enabled: false + image: + repository: mosipqa/apitest-resident + tag: develop + pullPolicy: Always + auth: + enabled: false + image: + repository: mosipqa/apitest-auth + tag: develop + pullPolicy: Always + esignet: + enabled: false + image: + repository: mosipqa/apitest-esignet + tag: develop + pullPolicy: Always + pms: + enabled: false + image: + repository: mosipqa/apitest-pms + tag: develop + pullPolicy: Always + +apitestrig: + configmaps: + s3: + s3-host: 'http://minio.minio:9000' + s3-user-key: 'admin' + s3-region: '' + db: + db-port: '5432' + db-su-user: 'postgres' + db-server: 'api-internal.sandbox.xyz.net' + apitestrig: + ENV_USER: 'api-internal.sandbox' + ENV_ENDPOINT: 'https://api-internal.sandbox.xyz.net' + ENV_TESTLEVEL: 'smokeAndRegression' + authDemoServiceBaseURL: http://authdemo.authdemo + authDemoServicePort: 80 + eSignetDeployed: yes or no + authCertsPath: '/home/mosip/authcerts' + scripts: + fetch_docker_image_hash_ids.sh: | + #!/bin/bash + sleep 5 + export DOCKER_HASH_ID=$( kubectl get pod "$HOSTNAME" -n "$NS" -o jsonpath='{.status.containerStatuses[*].imageID}' | sed 's/ /\n/g' | grep -v 'istio' | sed 's/docker\-pullable\:\/\///g' ) + export DOCKER_IMAGE=$( kubectl get pod "$HOSTNAME" -n "$NS" -o jsonpath='{.status.containerStatuses[*].image}' | sed 's/ /\n/g' | grep -v 'istio' | sed 's/docker\-pullable\:\/\///g' ) + if [[ -z $DOCKER_HASH_ID ]]; then + echo "DOCKER_HASH_ID IS EMPTY;EXITING"; + exit 1; + fi + echo "DOCKER_HASH_ID ; $DOCKER_HASH_ID" + echo "DOCKER_IMAGE : $DOCKER_IMAGE" + kubectl get pods -A -o=jsonpath='{range .items[*]}{.metadata.namespace}{","}{.metadata.labels.app\.kubernetes\.io\/name}{","}{.status.containerStatuses[?(@.name!="istio-proxy")].image}{","}{.status.containerStatuses[?(@.name!="istio-proxy")].imageID}{","}{.metadata.creationTimestamp}{"\n"}' | sed 's/ /\n/g' | grep -vE 'istio*|longhorn*|cattle*|rancher|kube' | sed 's/docker\-pullable\:\/\///g' | sort -u | sed '/,,,/d' | awk -F ',' 'BEGIN {print "{ \"POD_NAME\": \"'$(echo $HOSTNAME)'\", \"DOCKER_IMAGE\": \"'$(echo $DOCKER_IMAGE)'\", \"DOCKER_HASH_ID\": \"'$(echo $DOCKER_HASH_ID)'\", \"k8s-cluster-image-list\": ["} {print "{"} {print "\"namespace\": \"" $1 "\","} {print "\"app_name\": \"" $2 "\","} {print "\"docker_image_name\": \"" $3 "\","} {print "\"docker_image_id\": \"" $4 "\","} {print "\"creation_timestamp\": \"" $5 "\"" } {print "},"} END {print "]}"}' | sed -z 's/},\n]/}\n]/g' | jq -r . | tee -a images-list.json + ## run entrypoint script + sleep 5 + cd /home/${container_user}/ + bash ./entrypoint.sh + secrets: + s3: + s3-user-secret: 'password' + apitestrig: + volumes: + configmaps: + scripts: + defaultMode: 0777 + volumeMounts: + mountPath: '/home/inji/scripts/' diff --git a/deploy/mimoto/README.md b/deploy/mimoto/README.md new file mode 100644 index 000000000..7f9789782 --- /dev/null +++ b/deploy/mimoto/README.md @@ -0,0 +1,21 @@ +# mimoto Application + +Helm chart for installing MOSIP mimoto Application. + +## TL;DR + +```console +$ helm repo add mosip https://mosip.github.io +$ helm repo add mosip https://inji.github.io +$ helm install my-release inji/mimoto +$ helm install my-release mosip/mimoto +``` +```console +./install.sh +``` +```console +./delete.sh +``` +```console +./restart.sh +``` \ No newline at end of file diff --git a/deploy/mimoto/datashare-values.yaml b/deploy/mimoto/datashare-values.yaml new file mode 100644 index 000000000..e401f2cf7 --- /dev/null +++ b/deploy/mimoto/datashare-values.yaml @@ -0,0 +1,13 @@ +# Override the envFrom field to exclude 'artifactory-share' +extraEnvVarsCM: + - inji-stack-config + - config-server-share + +istio: + enabled: false # Disables VirtualService creation if Istio is used + +virtualService: + enabled: false # Explicitly disable VirtualService if applicable + +service: + enabled: true # Ensure service is still enabled, if needed diff --git a/deploy/mimoto/delete.sh b/deploy/mimoto/delete.sh new file mode 100755 index 000000000..94375da5c --- /dev/null +++ b/deploy/mimoto/delete.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Uninstalls mimoto +function deleting_mimoto() { + NS=injiweb + while true; do + read -p "Are you sure you want to delete mimoto helm chart?(Y/n) " yn + if [ $yn = "Y" ] + then + helm -n $NS delete mimoto + helm -n $NS delete datashare-inji + break + else + break + fi + done + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +deleting_mimoto # calling function \ No newline at end of file diff --git a/deploy/mimoto/install.sh b/deploy/mimoto/install.sh new file mode 100755 index 000000000..b44221718 --- /dev/null +++ b/deploy/mimoto/install.sh @@ -0,0 +1,174 @@ +#!/bin/bash +# Installs mimoto service +## Usage: ./install.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +NS=injiweb +CHART_VERSION=0.0.1-develop +KEYGEN_CHART_VERSION=1.3.0-beta.2 +SOFTHSM_NS=softhsm +SOFTHSM_CHART_VERSION=1.3.0-beta.2 +DATASHARE_CHART_VERSION=1.3.0-beta.2 +##The value of INJI_DATASHARE_HOST is set to a fixed value: "datashare-inji.injiweb". +INJI_DATASHARE_HOST="datashare-inji.injiweb" + +DEFAULT_INJI_DATASHARE_HOST=$(kubectl get cm inji-stack-config -n config-server -o jsonpath={.data.inji-datashare-host}) +if [ -z "$DEFAULT_INJI_DATASHARE_HOST" ]; then + echo "Adding INJI_DATASHARE_HOST to config-server deployment" + kubectl patch configmap inji-stack-config -n config-server --type merge -p "{\"data\": {\"inji-datashare-host\": \"$INJI_DATASHARE_HOST\"}}" + kubectl patch configmap inji-stack-config -n default --type merge -p "{\"data\": {\"inji-datashare-host\": \"$INJI_DATASHARE_HOST\"}}" + kubectl -n config-server set env --keys=inji-datashare-host --from configmap/inji-stack-config deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + kubectl -n config-server get deploy -o name | xargs -n1 -t kubectl -n config-server rollout status +fi + +echo Create $NS namespace +kubectl create ns $NS + +function installing_mimoto() { + echo Istio label + kubectl label ns $NS istio-injection=enabled --overwrite + helm repo add mosip https://mosip.github.io/mosip-helm + helm repo add inji https://inji.github.io/helm + helm repo update + + echo Copy Configmaps + COPY_UTIL=../copy_cm_func.sh + $COPY_UTIL configmap inji-stack-config default $NS + $COPY_UTIL configmap artifactory-share artifactory $NS + $COPY_UTIL configmap config-server-share config-server $NS + $COPY_UTIL configmap redis-config redis $NS + + echo Copy Secrets + $COPY_UTIL secret redis redis $NS + $COPY_UTIL secret db-common-secrets postgres $NS + + echo "Do you have public domain & valid SSL? (Y/n) " + echo "Y: if you have public domain & valid ssl certificate" + echo "n: If you don't have a public domain and a valid SSL certificate. Note: It is recommended to use this option only in development environments." + read -p "" flag + + if [ -z "$flag" ]; then + echo "'flag' was not provided; EXITING;" + exit 1; + fi + ENABLE_INSECURE='' + if [ "$flag" = "n" ]; then + ENABLE_INSECURE='--set enable_insecure=true'; + fi + + INJI_DATASHARE_HOST=$(kubectl get cm inji-stack-config -o jsonpath={.data.inji-datashare-host}) + echo "Installing datashare" + helm -n $NS install datashare-inji mosip/datashare \ + -f datashare-values.yaml \ + --version $DATASHARE_CHART_VERSION + + echo "Do you want to use SoftHSM or .p12 keystore for key management?" + echo "1) SoftHSM" + echo "2) .p12 keystore" + echo "3) External HSM" + read -p "Enter 1, 2 or 3 [default: 2]: " keystore_choice + keystore_choice=${keystore_choice:-2} + + if [[ "$keystore_choice" == "1" ]]; then + echo Installing SoftHSM + helm -n $SOFTHSM_NS install softhsm-mimoto mosip/softhsm -f softhsm-values.yaml --version $SOFTHSM_CHART_VERSION --wait + + echo Copy SoftHSM configMap and Secret to $NS and config-server + $COPY_UTIL configmap softhsm-mimoto-share softhsm $NS + $COPY_UTIL secret softhsm-mimoto softhsm config-server + $COPY_UTIL secret softhsm-mimoto softhsm $NS + kubectl -n config-server set env --keys=security-pin --from secret/softhsm-mimoto deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_SOFTHSM_MIMOTO_ + + elif [[ "$keystore_choice" == "2" ]]; then + volume_size=200M + volume_mount_path='/home/mosip/encryption' + PVC_CLAIM_NAME='mimoto-keygen-keymanager' + MIMOTO_KEYGEN_HELM_ARGS='--set springConfigNameEnv="mimoto"' + MIMOTO_HELM_ARGS='' + + if kubectl -n $NS get pvc "$PVC_CLAIM_NAME" >/dev/null 2>&1; then + echo "PVC $PVC_CLAIM_NAME already exists. Skipping keygen job." + else + echo "Creating new PVC and running keygen job" + MIMOTO_KEYGEN_HELM_ARGS="--set persistence.enabled=true \ + --set volumePermissions.enabled=true \ + --set persistence.size=$volume_size \ + --set persistence.mountDir=\"$volume_mount_path\" \ + --set springConfigNameEnv='mimoto' \ + --set persistence.pvc_claim_name=\"$PVC_CLAIM_NAME\" \ + --set keysDir=\"$volume_mount_path\" \ + --set keyStore.fileName=\"encryptionkeystore.p12\" \ + --set skipIfFileExists=true" + + echo "MIMOTO KEYGEN HELM ARGS $MIMOTO_KEYGEN_HELM_ARGS" + echo "Running mimoto keygen" + helm -n $NS install mimoto-keygen mosip/keygen -f keygen-mimoto.yaml $MIMOTO_KEYGEN_HELM_ARGS --wait --wait-for-jobs --version $KEYGEN_CHART_VERSION + fi + + elif [[ "$keystore_choice" == "3" ]]; then + echo "Setting up External HSM configuration" + read -p "Please provide the URL where externalhsm client zip is located: " externalhsmclient + read -p "Please provide the host URL for externalhsm: " externalhsmhosturl + read -p "Please provide the password for the externalhsm: " externalhsmpassword + + kubectl create configmap softhsm-mimoto-share --from-literal=hsm_client_zip_url_env="$externalhsmclient" --from-literal=PKCS11_PROXY_SOCKET="$externalhsmhosturl" -n softhsm --dry-run=client -o yaml | kubectl apply -f - + kubectl create secret generic softhsm-mimoto --from-literal=security-pin="$externalhsmpassword" -n softhsm --dry-run=client -o yaml | kubectl apply -f - + $COPY_UTIL configmap softhsm-mimoto-share softhsm $NS + $COPY_UTIL secret softhsm-mimoto softhsm config-server + $COPY_UTIL secret softhsm-mimoto softhsm $NS + kubectl -n config-server set env --keys=security-pin --from secret/softhsm-mimoto deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_SOFTHSM_MIMOTO_ + else + echo "Invalid option. Please choose 1, 2, or 3." + exit 1 + fi + + echo "Copy secrets to config-server namespace" + ../copy_cm_func.sh secret mimoto-wallet-binding-partner-api-key injiweb config-server + ../copy_cm_func.sh secret mimoto-oidc-partner-clientid injiweb config-server + + echo Updating mimoto-oidc-keystore-password value + ../copy_cm_func.sh secret mimoto-oidc-keystore-password injiweb config-server + + kubectl -n config-server set env --keys=mimoto-wallet-binding-partner-api-key --from secret/mimoto-wallet-binding-partner-api-key deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + kubectl -n config-server set env --keys=mimoto-oidc-partner-clientid --from secret/mimoto-oidc-partner-clientid deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + kubectl -n config-server set env --keys=mimoto-oidc-keystore-password --from secret/mimoto-oidc-keystore-password deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + + kubectl -n config-server get deploy -o name | xargs -n1 -t kubectl -n config-server rollout status + + echo "Please proceed with generating the Google Client ID and Secret Key required for integration." + read -p "Please enter the Google Client ID: " clientId + + if [ -z "$clientId" ]; then + echo "'clientId' was not provided; EXITING;" + exit 1; + fi + read -p "Please enter the Google Secret Key: " secretKey + + if [ -z "$secretKey" ]; then + echo "'secretKey' was not provided; EXITING;" + exit 1; + fi + + + echo Installing mimoto + helm -n $NS install mimoto inji/mimoto --version $CHART_VERSION -f values.yaml $ENABLE_INSECURE \ + --set mimoto.secrets.google-client.INJI_INJIWEB_GOOGLE_CLIENT_ID="$clientId" \ + --set mimoto.secrets.google-client.INJI_INJIWEB_GOOGLE_CLIENT_SECRET="$secretKey" + + kubectl -n $NS get deploy -o name | xargs -n1 -t kubectl -n $NS rollout status + + echo Installed mimoto + + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +installing_mimoto # calling function \ No newline at end of file diff --git a/deploy/mimoto/keygen-mimoto.yaml b/deploy/mimoto/keygen-mimoto.yaml new file mode 100644 index 000000000..a64d8db36 --- /dev/null +++ b/deploy/mimoto/keygen-mimoto.yaml @@ -0,0 +1,10 @@ +extraEnvVars: + - name: DB_DBUSER_PASSWORD + valueFrom: + secretKeyRef: + name: db-common-secrets + key: db-dbuser-password + - name: INJI_MIMOTO_DATABASE_HOSTNAME + value: postgres-postresql.postgres + - name: INJI_MIMOTO_DATABASE_PORT + value: "5432" diff --git a/deploy/mimoto/restart.sh b/deploy/mimoto/restart.sh new file mode 100755 index 000000000..399e088cc --- /dev/null +++ b/deploy/mimoto/restart.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Restart the mimoto service +## Usage: ./restart.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +function Restarting_mimoto() { + NS=injiweb + kubectl -n $NS rollout restart deploy mimoto + + kubectl -n $NS get deploy -o name | xargs -n1 -t kubectl -n $NS rollout status + + echo Restarted mimoto service + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +Restarting_mimoto # calling function \ No newline at end of file diff --git a/deploy/mimoto/softhsm-values.yaml b/deploy/mimoto/softhsm-values.yaml new file mode 100644 index 000000000..581bbd139 --- /dev/null +++ b/deploy/mimoto/softhsm-values.yaml @@ -0,0 +1,7 @@ +resources: + limits: {} + # cpu: 250m + # memory: 1Gi + requests: + cpu: 100m + memory: 20Mi diff --git a/deploy/mimoto/values.yaml b/deploy/mimoto/values.yaml new file mode 100644 index 000000000..64a785c79 --- /dev/null +++ b/deploy/mimoto/values.yaml @@ -0,0 +1,25 @@ +extraEnvVars: + - name: REDIS_HOST + valueFrom: + configMapKeyRef: + name: redis-config + key: redis-host + - name: REDIS_PORT + valueFrom: + configMapKeyRef: + name: redis-config + key: redis-port + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: DB_DBUSER_PASSWORD + valueFrom: + secretKeyRef: + name: db-common-secrets + key: db-dbuser-password + - name: INJI_MIMOTO_DATABASE_HOSTNAME + value: postgres-postgresql.postgres + - name: INJI_MIMOTO_DATABASE_PORT + value: "5432" diff --git a/deploy/redis/README.md b/deploy/redis/README.md new file mode 100644 index 000000000..74c4649e5 --- /dev/null +++ b/deploy/redis/README.md @@ -0,0 +1,13 @@ +# Redis + +* Redis is an open-source, in-memory data store widely used by developers as a database, cache, streaming engine, and message broker. +* In Mimoto, Redis is utilized as a cache store to enhance performance and speed up data retrieval. + +## Install +``` +$ ./install.sh +``` +## Delete +``` +$ ./delete.sh +``` diff --git a/deploy/redis/delete.sh b/deploy/redis/delete.sh new file mode 100755 index 000000000..79199627d --- /dev/null +++ b/deploy/redis/delete.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Deleted Redis helm chart +## Usage: ./delete.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +function Deleting_redis() { + NS=redis + while true; do + read -p "Are you sure you want to delete redis helm chart? Y/n ?" yn + if [ $yn = "Y" ] + then + helm -n $NS delete redis + echo Deleted Redis services. + break + else + break + fi + done + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +Deleting_redis # calling function diff --git a/deploy/redis/install.sh b/deploy/redis/install.sh new file mode 100755 index 000000000..d20007c3d --- /dev/null +++ b/deploy/redis/install.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Installs redis helm chart +## Usage: ./install.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +function installing_redis() { + + while true; do + read -p "Do you want to install Redis in redis namespace? (y/n) :" ans + if [ "$ans" = "n" ] || [ "$ans" = "N" ]; then + read -p "Please confirm if redis-config is already present in redis namespace. (y/n):" response + if [ "$response" = "Y" ] || [ "$response" = "y" ]; then + echo "Skipping Redis installation as well as redis-config creation." + exit 1 + elif [ "$response" = "N" ] || [ "$response" = "n" ]; then + echo "Skipping Redis installation and continuing with redis-config creation." + kubectl apply -f redis-config.yaml + echo "redis-config configmap creation completed." + exit 1 + else + echo "Incorrect response. Please re-execute the redis installation script." + exit 1 + fi + elif [ "$ans" = "y" ] || [ "$ans" = "Y" ]; then + break + else + echo "Please provide a correct option (Y or N)" + fi + done + + NS=redis + CHART_VERSION=17.3.14 + + echo Create $NS namespace + kubectl create ns $NS || true + + echo Istio label + kubectl label ns $NS istio-injection=enabled --overwrite + + echo Updating helm repos + helm repo add bitnami https://charts.bitnami.com/bitnami + helm repo update + + echo Installing redis + helm -n $NS install redis bitnami/redis --wait --version $CHART_VERSION \ + --set image.repository=mosipint/redis --set image.tag=7.0.5-debian-11-r25 + + echo Installed redis service + + kubectl apply -f redis-config.yaml + + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +installing_redis # calling function diff --git a/deploy/redis/redis-config.yaml b/deploy/redis/redis-config.yaml new file mode 100644 index 000000000..c828fb86e --- /dev/null +++ b/deploy/redis/redis-config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: redis-config + namespace: redis + labels: + app: redis +data: + redis-host: "redis-master-0.redis-headless.redis.svc.cluster.local" + redis-port: "6379" diff --git a/docker-compose/README.md b/docker-compose/README.md new file mode 100644 index 000000000..2bee9adba --- /dev/null +++ b/docker-compose/README.md @@ -0,0 +1,208 @@ +## Overview + +This is the docker-compose setup to run mimoto which act as BFF for Inji mobile and backend for Inji web. This is not for production use. + +## What is in the docker-compose folder? + +1. certs folder holds the p12 file which is being created as part of OIDC client onboarding. +2. "config" folder holds the mimoto system properties file, issuer configuration and credential template. +3. "docker-compose.yml" file with mimoto setup. + + +## How to run this setup? +1. Refer to the [How to create Google Client Credentials](#how-to-create-google-client-credentials) section to create + Google client credentials and replace the below placeholders of Mimoto service in the `docker-compose.yml` file with the generated credentials: + ```yaml + environment: + - GOOGLE_OAUTH_CLIENT_ID= + - GOOGLE_OAUTH_CLIENT_SECRET= + ``` + +2. Add identity providers as issuers in the `mimoto-issuers-config.json` file of [docker-compose config folder](config/mimoto-issuers-config.json). For each provider, create a corresponding object with its issuer-specific configuration. Refer to the [Issuers Configuration](#mimoto-issuers-configuration) section for details on how to structure this file and understand each field's purpose and what values need to be updated. + +3. Add or update the verifiers clientId, redirect and response Uris in `mimoto-trusted-verifiers.json` file of [docker-compose config folder](config/mimoto-trusted-verifiers.json) for Verifiable credential Online Sharing. + +4. In the root directory, create a certs folder and generate an OIDC client. Add the onboard client’s key to the oidckeystore.p12 file and place this file inside the certs folder. + Refer to the [official documentation](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/customization-overview/credential_providers) for guidance on how to create the **oidckeystore.p12** file and add the OIDC client key to it. + * The **oidckeystore.p12** file stores keys and certificates, each identified by an alias (e.g., mpartner-default-mimoto-insurance-oidc). Mimoto uses this alias to find the correct entry and access the corresponding private key during the authentication flow. + * Update the **client_alias** field in the [mimoto-issuers-config.json](config/mimoto-issuers-config.json) file with this alias so that Mimoto can load the correct key from the keystore. + * Also, update the **client_id** field in the same file with the client_id used during the onboarding process. + * Set the `oidc_p12_password` environment variable in the Mimoto service configuration inside docker-compose.yml to match the password used for the **oidckeystore.p12** file. + * Mimoto also uses this same keystore file (oidckeystore.p12) to store keys generated at service startup, which are essential for performing encryption and decryption operations through the KeyManager service. + +5. To configure any Mobile Wallet specific configurations refer to the [Inji Mobile Wallet Configuration](#inji-mobile-wallet-configuration) section. + +6. Choose your setup for starting the services: + - **Starting all services via Docker Compose (including Mimoto):** + Run the following command to start the services + ```bash + docker-compose up + ``` + - **Running Mimoto in IDE and other services like `datashare service` via Docker Compose:** + 1. In `docker-compose.yml`, update the `DATASHARE_DOMAIN` environment variable for the `Datashare service` to `localhost:8097`. + 2. Ensure that the `mimoto-service` service (or) any other service is commented out in the `docker-compose.yml` file which you will be running locally. + 3. Then, start your dependent services by running the following command + ```bash + docker-compose up # Use this to start all the services defined in docker-compose.yml + # OR + docker-compose up datashare postgres other_service_name # To start specific services (replace with actual names from docker-compose.yml) + ``` + **Note:** Use the **-d** flag with docker-compose up to run the services in detached (background) mode. + +7. To stop all the services, navigate to docker-compose folder and run the following command + ```bash + docker-compose down + ``` + +8. To stop a specific service (e.g., mimoto-service) and remove its container and image, run the following commands + ```bash + docker-compose stop # To stop a specific service container + docker-compose rm # To remove a specific service container + docker rmi # To remove a specific service image + ``` + +9. **Removing the Docker Volume:** + - To remove the persistent data for a specific service within an application, you can delete its individual Docker volume. This is necessary in situations where you need to start fresh or when data has become corrupted. + - For example, if you update the oidckeystore.p12 file, the mimoto service might fail to start. This happens because the new .p12 file may not contain the keys that are stored in the database's key alias table. Since these keys are used in the encryption and decryption flow, their absence prevents the service from functioning correctly. To fix this, you must remove the postgres-data volume to clear the old, encrypted data, which allows the service to start correctly with a new dataset. + + - Use the following command to remove the volume: + ```Bash + docker volume rm # E.g., docker volume rm docker-compose_postgres-data + ``` + +10. Access Apis as + * http://localhost:8099/v1/mimoto/allProperties + * http://localhost:8099/v1/mimoto/issuers + * http://localhost:8099/v1/mimoto/issuers/StayProtected + * http://localhost:8099/v1/mimoto/issuers/StayProtected/well-known-proxy + +## How to create Google Client Credentials + +To enable Google OAuth2.0 authentication, follow these steps: + +1. **Go to the Google Cloud Console**: + - Visit [Google Cloud Console](https://console.cloud.google.com/). + +2. **Create a New Project**: + - If you don’t already have a project, create a new one by clicking on the project dropdown and selecting "New Project". + +3. **Enable the OAuth Consent Screen**: + - Navigate to "APIs & Services" > "OAuth consent screen". + - Select "External" for the user type and configure the required fields (e.g., app name, support email, etc.). + - Save the changes. +4. **Create OAuth 2.0 Credentials**: + - Navigate to "APIs & Services" > "Credentials". + - Click "Create Credentials" > "OAuth 2.0 Client IDs". + - Select "Web application" as the application type. + +5. **Configure Authorized JavaScript Origins**: + Depending on your environment, use the following values: + + - **Local or Docker**: + ``` + http://localhost:8099 + ``` + - **Deployed domain (e.g., collab.mosip.net)**: + ``` + https://collab.mosip.net + +6. **Configure Authorized Redirect URIs**: + - **Local or Docker**: + ``` + http://localhost:8099/v1/mimoto/oauth2/callback/google + ``` + - **Deployed domain (e.g., collab.mosip.net)**: + ``` + https://collab.mosip.net/v1/mimoto/oauth2/callback/google + ``` + +7. **Save and Retrieve Client Credentials**: + - After saving, you will receive a `Client ID` and `Client Secret`. + +## 📄Mimoto Issuers Configuration + +The mimoto-issuers-config.json file defines the list of Credential Issuers that support Verifiable Credential issuance using the OpenID4VCI protocol. Mimoto uses it to load Issuer metadata configuration like logo, client details and endpoints needed for initiating and completing credential issuance. + +### 🧩Structure and example + +```json +{ + "issuers": [ + { + "issuer_id": "StayProtected", + "credential_issuer": "StayProtected", + "display": [ + { + "name": "StayProtected Insurance", + "logo": { + "url": "https://raw.githubusercontent.com/tw-mosip/file-server/master/StayProtectedInsurance.png", + "alt_text": "a square logo of a Sunbird" + }, + "language": "en", + "title": "Download StayProtected Insurance Credentials", + "description": "Download insurance credential" + } + ], + "protocol": "OpenId4VCI", + "client_id": "esignet-sunbird-partner", + "client_alias": "esignet-sunbird-partner", + "wellknown_endpoint": "https://injicertify-insurance.collab.mosip.net/v1/certify/issuance/.well-known/openid-credential-issuer", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "authorization_audience": "https://esignet-insurance.collab.mosip.net/v1/esignet/oauth/v2/token", + "token_endpoint": "https://localhost:8099/v1/mimoto/get-token/StayProtected", + "proxy_token_endpoint": "https://esignet-insurance.collab.mosip.net/v1/esignet/oauth/v2/token", + "qr_code_type": "OnlineSharing", + "credential_issuer_host": "https://injicertify-insurance.collab.mosip.net", + "enabled": "true" + } + ] +} +``` + +### Issuer configuration field Descriptions + +| Field | Description | Value | Required | +| ------------------------ |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| `issuer_id` | Unique identifier for the Issuer | Use your own Issuer's name, e.g., `"HealthInsuranceIssuer"` | Mandatory | +| `credential_issuer` | Logical name (usually the same as `issuer_id`) | Same as above or a recognizable alias | Mandatory | +| `display[]` | A list of display configurations, one per supported language. Each object includes localized fields like `name`, `logo.url`, `logo.alt_text`, `language`, `title`, `description` | Provide user-facing display metadata such as Issuer name, logo, and descriptions. You can customize existing entries or add new ones for each supported language | Mandatory | +| `protocol` | Protocol used; must be `"OpenId4VCI"` | Do not change unless the protocol evolves | Mandatory | +| `client_id` | OAuth client ID registered with the Issuer during onboarding | Replace with the client ID provided when onboarding Issuer with the Issuer’s authorization server | Mandatory | +| `client_alias` | Internal alias for the Issuer (e.g., used to fetch secure credentials of Issuer from keystore file `oidckeystore.p12`) | Set this to the alias value provided while inserting an entry for the Issuer into oidckeystore.p12 file during Issuer onboarding | Mandatory | +| `wellknown_endpoint` | URL to the Issuer's `.well-known/openid-credential-issuer` endpoint as per OpenID4VCI spec | Replace with your Issuer’s actual metadata endpoint to discover important configuration details about the Credential Issuer | Mandatory | +| `redirect_uri` | OAuth2 redirect URI of your app where users are sent after authentication; must match one registered with the Issuer's authorization server | Replace with one of the URI's that were registered during Issuer onboarding on the authorization server | Mandatory | +| `authorization_audience` | Audience value for token requests | Usually the base URL of the Issuer’s token service | Mandatory | +| `token_endpoint` | Internal proxy token endpoint provided by Mimoto to forward requests to the real Auth server and it should be HTTPS URL | Use an internal/exposed domain or an ngrok URL if testing locally with the Inji mobile wallet | Mandatory | +| `proxy_token_endpoint` | Actual token endpoint of the Authorization server | Replace with the Auth server's token endpoint URL | Mandatory | +| `qr_code_type` | Type of QR code: `"OnlineSharing"` or `"EmbeddedVC"` | Use `"OnlineSharing"` to embed the OpenID4VP authorization request in a Verifiable Credential PDF QR code for verifiers to verify it. Use `"EmbeddedVC"` to embed the entire Verifiable Credential in the QR code (typically for offline use) | Optional | +| `credential_issuer_host` | Host/domain of the credential Issuer | Replace with your Issuer's domain | Mandatory | +| `enabled` | Enables or disables this Issuer configuration | Set to `"false"` to disable or hide this Issuer | Mandatory | + +### ✅ How to Add a New Issuer + +1. Add a new Issuer object in the mimoto-issuers-config.json file using the structure, field descriptions, and example provided above. +2. Ensure: + - All the configured endpoints for the Issuer and its Authorization server are reachable and functional. + - `redirect_uri` matches one of the URIs registered with the Issuer’s Authorization server during Issuer onboarding. + - `client_id` matches the one provided during Issuer onboarding with the Issuer's Authorization server. + - `client_alias` for each Issuer is correctly configured and available in the oidckeystore.p12 file. +3. For step-by-step guidance on how to add a new Issuer and generate the oidckeystore.p12 file, refer to the [Customization Overview](https://docs.inji.io/inji-wallet/inji-mobile/technical-overview/customization-overview/credential_providers) documentation. + +## Inji Mobile Wallet Configuration +To bind an Android or iOS wallet using the e-signet service via Mimoto, ensure the following property is updated in application-local.properties (if running Mimoto using IDE) or mimoto-default.properties (if running Mimoto using docker compose) to point to the appropriate e-signet instance running in your target environment +```properties + mosip.esignet.host= (E.g. https://esignet.env.mosip.net) +``` + +Note: +- Replace mosipbox.public.url, mosip.api.public.url with your public accessible domain. For dev or local env [ngrok](https://ngrok.com/docs/getting-started/) is recommended. + +### Deploying Mimoto Without Datashare + +If an entity (Ex: Country, Organization) uses only Mobile wallet, deploying datashare and its dependent services is not required. This also eliminates the need for pulling the datashare and minio images from Docker and running them. + +To deploy Mimoto without datashare: +1. Remove the datashare dependency from `depends_on` under `mimoto-service` in the `docker-compose.yml` file. +2. Update the services section by removing the `datashare` and `minio` services from the `docker-compose.yml` file. + +**Note:** When using only Inji Mobile Wallet, ensure that the `qr_code_type` field in the Mimoto Issuers Configuration (`mimoto-issuers-config.json`) is set to `"EmbeddedVC"` for all the issuers. diff --git a/docker-compose/config/credential-template.html b/docker-compose/config/credential-template.html new file mode 100644 index 000000000..48c96dfe2 --- /dev/null +++ b/docker-compose/config/credential-template.html @@ -0,0 +1,117 @@ + + + + + Identity Card + + + + + +
+
+ $titleName +
+
+
+ #if (!$face) +
+ Logo +
+ #else + Logo + #end +
+ Logo +
+
+ +
+ #foreach($entry in $rowProperties.entrySet()) ## Iterate over the outer map + #if($entry.value) ## Check if the value exists for this entry + #set($nestedMap = $entry.value) ## Get the nested value + #if($nestedMap && $nestedMap.size() > 0) ## Check if nestedMap is a Map with entries + #foreach($nestedEntry in $nestedMap.entrySet()) ## Iterate over the nested map + #if($nestedEntry.value) ## Ensure that there’s a value for this nested entry +
+
+ $nestedEntry.key +
+ #if($nestedEntry.value.startsWith("data:image/")) + Embedded Visual + #else +
+ $nestedEntry.value +
+ + #end +
+ #end + #end + #else +
+
+ $entry.key +
+ #if($entry.value.startsWith("data:image/")) + Embedded Visual + + #else +
+ $entry.value +
+ #end +
+ #end + #end + #end +
+
+
+ +#if($isSdJwtWithDisclosures) + #if($isMaskedOn) +
+ i +
+ Your card includes specific details marked by the issuer as selectively disclosable. Fields marked (X) are selectively disclosable and can be shared independently during credential presentation. Other fields not listed here can only be disclosed as part of the full credential, if requested by the verifier. +
+
+ #else +
+ i +
+ Your credential includes fields that can be shared selectively. These fields are not marked separately for you to identify visually, but you will see and choose them when presenting this credential to a verifier. +
+
+ #end +#end + +
+ QR Code +
+
+
+ Please note: + + #if ($credentialValidity != -1) + This credential is limited to a maximum of [$credentialValidity] verifications by authorized verifiers. Once the specified number of verifications has been reached, this credential will no longer be valid for further verification attempts. + #else + This credential can be used for verification by authorized verifiers without any limit on the number of verifications. It provides + continuous, secure access for credential validation. + #end + +
+
+ + diff --git a/docker-compose/config/data-share-inji-default.properties b/docker-compose/config/data-share-inji-default.properties new file mode 100644 index 000000000..ac669265a --- /dev/null +++ b/docker-compose/config/data-share-inji-default.properties @@ -0,0 +1,64 @@ +# Follow properites have their values assigned via 'overrides' environment variables of config server docker. +# DO NOT define these in any of the property files. They must be passed as env variables. Refer to config-server +# helm chart: +# keycloak.external.host +# keycloak.external.url +# keycloak.internal.host +# keycloak.internal.url +# mosip.datsha.client.secret +# s3.accesskey +# s3.region +# s3.secretkey + +mosip.data.share.service.id=mosip.data.share +mosip.data.share.service.version=1.0 + +CRYPTOMANAGER_ENCRYPT=${mosip.kernel.keymanager.url}/v1/keymanager/encrypt +KEYMANAGER_JWTSIGN=${mosip.kernel.keymanager.url}/v1/keymanager/jwtSign +PARTNER_POLICY=${mosip.pms.policymanager.url}/v1/policymanager/policies/{policyId}/partner/{partnerId} +KEYBASEDTOKENAPI=${mosip.kernel.authmanager.url}/v1/authmanager/authenticate/clientidsecretkey + + +data.share.application.id=PARTNER +mosip.data.share.datetime.pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z' +!-- if value is true then please set servlet path to / --! +mosip.data.share.urlshortner=false +data.share.token.request.appid=datsha +data.share.token.request.clientId=mosip-datsha-client +data.share.token.request.secretKey=${mosip.datsha.client.secret} +data.share.token.request.password= +data.share.token.request.username= +data.share.token.request.version=1.0 +data.share.token.request.id=io.mosip.datashare +data.share.token.request.issuerUrl=${keycloak.internal.url}/auth/realms/mosip +spring.servlet.multipart.max-file-size=14MB +mosip.data.share.protocol=http +mosip.data.share.includeCertificateHash=false +mosip.data.share.includeCertificate=false +mosip.data.share.includePayload=false +mosip.data.share.digest.algorithm=SHA256 +mosip.data.share.prependThumbprint=false +mosip.role.durian.postcreatepolicyidsubscriberid=CREATE_SHARE +auth.server.admin.allowed.audience=mosip-creser-client,mpartner-default-auth,mosip-regproc-client,mosip-reg-client,mosip-syncdata-client,mpartner-default-print,mosip-resident-client,opencrvs-partner,mosip-pms-client,mpartner-default-digitalcard,mosip-admin-client,mosip-abis-client,mpartner-default-mobile + +mosip.auth.filter_disable=false + +# Object store +object.store.s3.accesskey=minioadmin +object.store.s3.secretkey=minioadmin +object.store.s3.url=http://minio-service:9000 +object.store.s3.region=us-east-1 +object.store.s3.readlimit=10000000 + +#specific to Compliance Toolkit, to ABIS DataShare testcases +auth.handle.ctk.flow=true +mosip.api.internal.toolkit.url=https://${mosip.api.internal.host}/v1/toolkit +mosip.compliance.toolkit.saveDataShareToken.url=${mosip.api.internal.toolkit.url}/saveDataShareToken +mosip.compliance.toolkit.invalidateDataShareToken.url=${mosip.api.internal.toolkit.url}/invalidateDataShareToken +mosip.compliance.toolkit.invalidateDataShareToken.testCaseId=ABIS3031 +logging.level.org.springframework.web: DEBUG +#cache schedular +mosip.data.share.policy-cache.expiry-time-millisec=7200000 + +mosip.auth.adapter.impl.basepackage=io.mosip.kernel.auth.defaultadapter +mosip.pms.policymanager.url=http://pms-policy.pms diff --git a/docker-compose/config/data-share-standalone.properties b/docker-compose/config/data-share-standalone.properties new file mode 100644 index 000000000..25bb3c95e --- /dev/null +++ b/docker-compose/config/data-share-standalone.properties @@ -0,0 +1,15 @@ +# Enables the data-share application in standalone mode. +mosip.data.share.standalone.mode.enabled=true +# Defines the policy json which will be taken into consideration if +# "mosip.data.share.standalone.mode.enabled" is set as true. +# If we are using "encryptionType" as "Partner based" then subscriberId must be a valid subscriberId +# i.e. should exist in system. +mosip.data.share.static-policy.policy-json={"typeOfShare":"","transactionsAllowed":"250","shareDomain":"${DATASHARE_DOMAIN}","encryptionType":"NONE","source":"","validForInMinutes":"30"} +# Defines the policyId which will be taken into consideration if " +# mosip.data.share.standalone.mode.enabled" is set as true. +mosip.data.share.static-policy.policy-id=static-policyid +# Defines the subscriberId which will be taken into consideration if +# "mosip.data.share.standalone.mode.enabled" is set as true. +mosip.data.share.static-policy.subscriber-id=static-subscriberid +# Disables JWT signature computation while storing object in object store. +mosip.data.share.signature.disabled=true \ No newline at end of file diff --git a/docker-compose/config/mimoto-bootstrap.properties b/docker-compose/config/mimoto-bootstrap.properties new file mode 100644 index 000000000..120c9b23e --- /dev/null +++ b/docker-compose/config/mimoto-bootstrap.properties @@ -0,0 +1,31 @@ +spring.cloud.config.uri=http://nginx/ +spring.cloud.config.name=mimoto,inji +spring.application.name=mimoto + +#config.server.file.storage.uri=https://raw.githubusercontent.com/mosip/mosip-config/collab1/ +config.server.file.storage.uri=http://nginx/ + +management.endpoint.health.show-details=always +management.endpoints.web.exposure.include=info,health,refresh + +# The port number on which the Mimoto service will be running +server.port=8099 +# Base URL for all the APIs in Mimoto service +server.servlet.context-path=/v1/mimoto +# Sets the maximum allowed total size (in bytes) for all HTTP response headers generated by Tomcat, including the Location header for redirects +server.tomcat.max-http-response-header-size=65536 + +health.config.enabled=false + +openapi.info.title=${spring.application.name} +openapi.info.description=${spring.application.name} +openapi.info.version=1.0 +openapi.info.license.name=Mosip +openapi.info.license.url=https://docs.mosip.io/platform/license +openapi.service.servers[0].url=${mosip.api.public.url}${server.servlet.context-path} +openapi.service.servers[0].description=${spring.application.name} +openapi.group.name=${openapi.info.title} +openapi.group.paths[0]=/** +springdoc.swagger-ui.disable-swagger-default-url=true +springdoc.swagger-ui.tagsSorter=alpha +springdoc.swagger-ui.operationsSorter=alpha \ No newline at end of file diff --git a/docker-compose/config/mimoto-default.properties b/docker-compose/config/mimoto-default.properties new file mode 100644 index 000000000..c2e2a5733 --- /dev/null +++ b/docker-compose/config/mimoto-default.properties @@ -0,0 +1,460 @@ +mosip.service.end-points=/**/* + +# Inji default properties +mosip.inji.allowedAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedEkycAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedInternalAuthType=otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.faceSdkModelUrl=https://api.collab.mosip.net/inji +# maximum number of retry for downloading vc +mosip.inji.vcDownloadMaxRetry=10 +# pool interval in milliseconds +mosip.inji.vcDownloadPoolInterval=6000 +# validate binding audience url to be sent in token +mosip.inji.audience=ida-binding +# issuer to be sent in token +mosip.inji.issuer=residentapp +# warning screen domain name +mosip.inji.warningDomainName=${mosip.api.public.url} +#timeout for vc download api via OpenId4VCI flow in milliseconds +mosip.inji.openId4VCIDownloadVCTimeout=30000 +# inji documentation url +mosip.inji.aboutInjiUrl=https://docs.inji.io/inji-wallet/inji-mobile +# minimum storage space required for making audit entry in MB +mosip.inji.minStorageRequiredForAuditEntry=2 +# minimum storage space required for downloading / receiving vc in MB +mosip.inji.minStorageRequired=2 + +# MOSIP +# Defines the base URLs for various MOSIP services and Keycloak for internal and external communication +mosip.api.public.url=http://localhost:${server.port} +mosipbox.public.url=http://localhost:${server.port} +keycloak.internal.url=http://keycloak.keycloak +keycloak.external.url=https://iam.collab.mosip.net/ +mosip.kernel.authmanager.url=http://authmanager.kernel/ +mosip.kernel.masterdata.url=http://masterdata.kernel + +# Resident service Configurations used in downloading the Verifiable Credential of MosipOtp issuer +# Base URL for Resident service +mosip.resident.base.url=https://api.collab.mosip.net/resident/v1 +# This URL used for fetching the OTP from resident service prior to initiating the download flow on the Mobile Wallet +RESIDENT_OTP=${mosip.resident.base.url}/req/otp +# This URL used for fetching the OTP while getting the UIN/VID from AID(Application ID) +RESIDENT_INDIVIDUALID_OTP=${mosip.resident.base.url}/individualId/otp +# This URL is used to get the status about whether the UIN/VID is generated from AID or not +RESIDENT_AID_GET_INDIVIDUALID=${mosip.resident.base.url}/aid/status +# URL to request the Verifiable Credential download from the Resident service +RESIDENT_CREDENTIAL_REQUEST=${mosip.resident.base.url}/req/credential +# URL to check if the Verifiable Credential is downloaded yet or not +RESIDENT_CREDENTIAL_REQUEST_STATUS=${RESIDENT_CREDENTIAL_REQUEST}/status +# URL used to get the VID for the respective UIN +RESIDENT_VID=${mosip.resident.base.url}/vid +RESIDENT_AUTH_LOCK=${mosip.resident.base.url}/req/auth-lock +RESIDENT_AUTH_UNLOCK=${mosip.resident.base.url}/req/auth-unlock + +#Esignet service +# Base URL for e-Signet service +mosip.esignet.host=https://esignet.collab.mosip.net +# Endpoint for fetching the OTP in Wallet binding flow +BINDING_OTP=${mosip.esignet.host}/v1/esignet/binding/binding-otp +# Endpoint for binding the credentials to the Wallet +WALLET_BINDING=${mosip.esignet.host}/v1/esignet/binding/wallet-binding + +# Websub service configurations +# Dev sandbox +# Base URL for WebSub service +mosip.websub.url=http://websub.websub/ +# Hub URL for WebSub service +mosip.event.hubUrl=${mosip.websub.url}/hub/ +# WebSub hub URL used to subscribe to a specific topic +mosip.event.hub.subUrl=${mosip.event.hubUrl} +# WebSub hub URL used to publish the data to a specific topic +mosip.event.hub.pubUrl=${mosip.event.hubUrl} + +# Delayed WebSub subscription. Default is 5 seconds in ms. +mosip.event.delay-millisecs=5000 +# WebSub re-subscription workaround for losing subscribed topic when MOSIP WebSub update or restart. Default is 5 minutes in ms. +websub-resubscription-delay-millisecs=300000 + +# Callback URL for receiving the WebSub event notifications +mosip.event.callBackUrl=${mosip.api.public.url}/v1/mimoto/credentialshare/callback/notify +# Kafka topic to which the CREDENTIAL_ISSUED for specific partner id is published +mosip.event.topic=${mosip.partner.id}/CREDENTIAL_ISSUED +# Secret key used for validating the authenticity of the WebSub event notifications +mosip.event.secret=Kslk30SNF2AChs2 + +# MOSIP partner +# Unique identifier which is being used as the Issuer during MOSIP OTP issuer Verifiable Credential download requests +mosip.partner.id=mpartner-default-mobile +# Name of the PKCS#12 keystore file for cryptographic operations +mosip.partner.crypto.p12.filename=keystore.p12 +# Password for accessing the PKCS#12 keystore file +mosip.partner.crypto.p12.password=abc123 +# Alias used to access the secure credentials like private key of partner within the PKCS#12 keystore file +mosip.partner.crypto.p12.alias=partner +# Encryption key used for securing partner data +mosip.partner.encryption.key=123456 +# Property which indicates whether the data includes its thumbprint (a unique hash for integrity verification) or not +mosip.partner.prependThumbprint=true + +#-------------TOKEN GENERATION---------------- +#Token generation request id +token.request.id=io.mosip.registration.processor +#Token generation app id +token.request.appid=regproc +#Token generation username +token.request.username=registrationprocessor +#Token generation password +token.request.password=abc123 +#Token generation version +token.request.version=1.0 +#Token generation Client Id +token.request.clientId=mosip-regproc-client +#Token generation secret key +token.request.secretKey=abc123 +#Token generation issuer url +token.request.issuerUrl=${keycloak.internal.url}/auth/realms/mosip + +#Audit Service +AUDIT=${mosip.kernel.auditmanager.url}/v1/auditmanager/audits +AUDIT_URL=${mosip.kernel.auditmanager.url}/v1/auditmanager/audits +KEYBASEDTOKENAPI=${mosip.kernel.authmanager.url}/v1/authmanager/authenticate/clientidsecretkey + +#Master Data Services +# MASTER=http://kernel-masterdata-service/v1/masterdata +MASTER=${mosip.kernel.masterdata.url}/v1/masterdata +TEMPLATES=${MASTER}/templates + +#Packet receiver application version +mosip.print.application.version=1.0 +#Request Date Time format +mosip.print.datetime.pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + + +#-------------Printing Service-------------------- +mosip.print.service.id=mosip.print + +#Audit request id +mosip.print.audit.id=mosip.applicanttype.getApplicantType +mosip.country.code=MOR + +#Kernel Crypto signature +registration.processor.signature.isEnabled=true + +# Language Supported By Platform - ISO +# Defines the list of optional languages supported by the platform +mosip.optional-languages=fra,ara +# Defines the list of mandatory languages supported by the platform +mosip.mandatory-languages=eng +# mosip.primary-language=eng +# mosip.secondary-language=ara + + +#----------------------- CBEFF Util-------------------------------------------------- +# Cbeff URL where the files will be stored in git, change it accordingly in case of change of storage location. +# mosip.kernel.xsdstorage-uri=${spring.cloud.config.uri}/${spring.application.name}/${spring.profiles.active}/${spring.cloud.config.label}/ +mosip.kernel.xsdstorage-uri=https://raw.githubusercontent.com/mosip/mosip-config/develop/ +# Cbeff XSD file name in config server +mosip.kernel.xsdfile=mosip-cbeff.xsd + + +#----------------------------- Applicant Type -------------------------------------------------- +mosip.kernel.applicant.type.age.limit=5 + +#----------------------------- Static PIN -------------------------------------------------- +mosip.kernel.pin.length=6 + +#-----------------------------TOKEN-ID Properties--------------------------------- +#length of the token id +mosip.kernel.tokenid.length=36 + +#------------------------------------ Key-manager specific properties -------------------------------------------------- +# Crypto asymmetric algorithm name +mosip.kernel.crypto.asymmetric-algorithm-name=RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING +# Crypto symmetric algorithm name +mosip.kernel.crypto.symmetric-algorithm-name=AES/GCM/NoPadding +# Encrypted data and encrypted symmetric key separator +mosip.kernel.data-key-splitter=#KEY_SPLITTER# +# GCM tag length +mosip.kernel.crypto.gcm-tag-length=128 +# Hash algo name +mosip.kernel.crypto.hash-algorithm-name=PBKDF2WithHmacSHA512 +# Symmetric key length used in hash +mosip.kernel.crypto.hash-symmetric-key-length=256 +# No of iterations in hash +mosip.kernel.crypto.hash-iteration=10000 +# Keygenerator symmetric algorithm name +mosip.kernel.keygenerator.symmetric-algorithm-name=AES +# Symmetric algorithm key length +mosip.kernel.keygenerator.symmetric-key-length=256 +# Asymmetric algorithm key length +mosip.kernel.keygenerator.asymmetric-key-length=2048 +# Keygenerator asymmetric algorithm name +mosip.kernel.keygenerator.asymmetric-algorithm-name=RSA +# Specifies the file path to the PKCS#12 keystore containing cryptographic keys for the KeyManager service +mosip.kernel.keymanager.hsm.config-path=${SPRING_CONFIG_LOCATION}certs/oidckeystore.p12 +# Defines the type of the keystore used by the Key Manager as PKCS#12 +mosip.kernel.keymanager.hsm.keystore-type=PKCS12 +# Password to access the KeyManager PKCS#12 keystore file +mosip.kernel.keymanager.hsm.keystore-pass=${oidc_p12_password} + +# Default Certificate properties +mosip.kernel.keymanager.certificate.default.common-name=www.example.com +mosip.kernel.keymanager.certificate.default.organizational-unit=EXAMPLE-CENTER +mosip.kernel.keymanager.certificate.default.organization=IIITB +mosip.kernel.keymanager.certificate.default.location=BANGALORE +mosip.kernel.keymanager.certificate.default.state=KA +mosip.kernel.keymanager.certificate.default.country=IN + +# keymanager database properties +# Specifies the driver class name for the PostgreSQL database, required for the application to interact with the database +keymanager.persistence.jdbc.driver=org.postgresql.Driver +# Specifies the JDBC URL for connecting to the PostgreSQL database, required to establish a connection to the inji_mimoto database +keymanager_database_url=${SPRING_DATASOURCE_URL} +# Defines the password for accessing the PostgreSQL database, required for authentication +keymanager_database_password=${POSTGRES_PASSWORD} +# Defines the username for accessing the PostgreSQL database, required for authentication +keymanager_database_username= mimotouser +# Specifies the schema name used by the Keymanager in the PostgreSQL database +keymanager.persistence.jdbc.schema=mimoto + +# Hibernate properties +hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +hibernate.hbm2ddl.auto=none +hibernate.jdbc.lob.non_contextual_creation=true +hibernate.show_sql=false +hibernate.format_sql=false +hibernate.connection.charSet=utf8 +hibernate.cache.use_second_level_cache=false +hibernate.cache.use_query_cache=false +hibernate.cache.use_structured_entries=false +hibernate.generate_statistics=false +hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext + +# log level +logging.level.root=WARN +logging.level.io.mosip=INFO +# logging.level.io.mosip.kernel.auth.defaultadapter.filter=INFO +logging.level.io.mosip.kernel.auth.defaultadapter=DEBUG +logging.level.org.springframework.http.client=DEBUG +logging.level.io.mosip.mimoto=INFO +logging.level.reactor.netty.http.client=INFO +# tomcat access logs +server.tomcat.accesslog.enabled=true +server.tomcat.accesslog.directory=/dev +server.tomcat.accesslog.prefix=stdout +server.tomcat.accesslog.buffered=false +server.tomcat.accesslog.suffix= +server.tomcat.accesslog.file-date-format= +server.tomcat.accesslog.pattern={"@timestamp":"%{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}t","level":"ACCESS","level_value":70000,"traceId":"%{X-B3-TraceId}i","statusCode":%s,"req.requestURI":"%U","bytesSent":%b,"timeTaken":%T,"appName":"${spring.application.name}"} +server.tomcat.accesslog.className=io.mosip.kernel.core.logger.config.SleuthValve +registration.processor.unMaskedUin.length=5 + +IDSchema.Version=1.0 +registration.processor.identityjson=identity-mapping.json +registration.processor.demographic.identity=identity +CREATEDATASHARE=${mosip.injiweb.datashare.url}/v1/datashare/create +DECRYPTPINBASSED=${mosip.kernel.keymanager.url}/v1/keymanager/decryptWithPin + +#Auth Adapter rest template authentication configs +mosip.iam.adapter.appid=partner +mosip.iam.adapter.clientid=mpartner-default-mobile +mosip.iam.adapter.clientsecret=1234secret +auth.server.admin.issuer.uri=${keycloak.external.url}/auth/realms/ + +mosip.iam.adapter.issuerURL=${keycloak.internal.url}/auth/realms/mosip +mosip.authmanager.base-url=${mosip.kernel.authmanager.url}/v1/authmanager +mosip.authmanager.client-token-endpoint=${mosip.authmanager.base-url}/authenticate/clientidsecretkey +auth.server.admin.validate.url=${mosip.kernel.authmanager.url}/v1/authmanager/authorize/admin/validateToken + +# in minutes +mosip.iam.adapter.validate-expiry-check-rate=1440 +# in minutes +mosip.iam.adapter.renewal-before-expiry-interval=1440 + +#this should be false if you don?t use this restTemplate true if you do +mosip.iam.adapter.self-token-renewal-enable=true +mosip.auth.filter_disable=true +mosip.auth.adapter.impl.basepackage=io.mosip.kernel.auth.defaultadapter +mosip.kernel.auth.appids.realm.map={prereg:'mosip',ida:'mosip',registrationclient:'mosip',regproc:'mosip',partner:'mosip',resident:'mosip',admin:'mosip',crereq:'mosip',creser:'mosip',datsha:'mosip',idrepo:'mosip'} + +vercred.type.vid=VID +mosip.idp.partner.id=mpartner-default-mobile +mosip.idp.partner.encryption.key=Aci9jg28B8mO_LDfDXo3ZTp5_HKgEMun2tYyHCa1e8k +wallet.binding.partner.id=mpartner-default-mimotokeybinding +wallet.binding.partner.api.key=1234walletbindingkey +mosip.iam.adapter.disable-self-token-rest-template=true + +# Mosip notification otp channel config - defines the predefined list of otp channels allowed for sending the OTP to the user +mosip.notificationtype=SMS|EMAIL|PHONE + +# OpenID4VCI related configurations +# Specifies the file name which includes the list of Issuers Configurations, including their well-known endpoints and other details +mosip.openid.issuers=mimoto-issuers-config.json +# Fallback HTML template used to render the Verifiable Credential if no issuer-specific template is found +mosip.openid.htmlTemplate=credential-template.html +# Specifies the type of client assertion used for OIDC authentication with e-signet (e.g., JWT bearer) +mosip.oidc.client.assertion.type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer +# PKCS#12 keystore file name containing secured credentials (e.g., private key and client alias) for each onboarded issuer, used in the authentication flow with e-Signet +mosip.oidc.p12.filename=oidckeystore.p12 +# Password to access the PKCS#12 keystore file +mosip.oidc.p12.password=${oidc_p12_password} +# Relative path to the directory which contains PKCS#12 keystore file oidceystore.p12 +mosip.oidc.p12.path=certs/ + +# Inji Web Config +# Base URL for Inji Web application +mosip.inji.web.url=http://localhost:3004 +# Inji Web Redirect URL which is used to show the error message in case of failure in OpenID4VP flow +mosip.inji.web.redirect.url=${mosip.inji.web.url}/authorize +# Defines the maximum number of characters allowed to be encoded into a QR code. The value can be set to anything less than or equal to 4096. +# If the data size exceeds 4096 characters (equivalent to 4KB), the 'pixel pass' library will fail to generate the QR code. +mosip.inji.qr.data.size.limit=4096 +# Defines the height (in Pixels) of the generated QR code +mosip.inji.qr.code.height=400 +# Defines the width (in Pixels) of the generated QR code +mosip.inji.qr.code.width=400 + +# Specifies the URL to redirect the user after successful authentication using Identity Provider(e.g., Goggle), pointing to the passcode page in Inji Web +mosip.inji.web.authentication.success.redirect.url=${mosip.inji.web.url}/user/passcode +# Defines the List of Issuers that are limited to issue only single Verifiable Credential +mosip.inji.wallet.issuersWithSingleVcLimit=Mosip + +#User Wallet Config +# Regular expression for validating wallet PINs, ensuring it is exactly 6 digits. +mosip.inji.user.wallet.pin.validation.regex=^\\d{6}$ +# Regular expression for validating wallet names, allowing alphanumeric characters, spaces, underscores, hyphens, and periods, with a maximum length of 50 characters. +mosip.inji.user.wallet.name.validation.regex=^[A-Za-z0-9 _.-]{0,50}$ + +#OpenId4VP related Configuration +# Pattern for including the OpenID4VP Authorization request in the QR code data of the Verifiable Credential, with placeholders for including resource(DataShare URL) and presentation definition +mosip.inji.ovp.qrdata.pattern=INJI_OVP://http://localhost:${server.port}/v1/mimoto/authorize?response_type=vp_token&resource=%s&presentation_definition=%s +# Pattern for returning the response in redirect URL for successful Verifiable Presentation in OpenId4VP flow +mosip.inji.ovp.redirect.url.pattern=%s#vp_token=%s&presentation_submission=%s +# Pattern for returning the error in redirect URL in case of Verifiable Presentation failure in OpenId4VP flow +mosip.inji.ovp.error.redirect.url.pattern=%s?error=%s&error_description=%s +# File name containing trusted verifiers for OpenId4VP. It is used for validating the verifiers during the OpenID4VP process +mosip.openid.verifiers=mimoto-trusted-verifiers.json + +#DataShare Config +# Base URL for DataShare Service +mosip.data.share.url=http://datashare-service:8097 +# Endpoint for creating DataShare requests and storing downloaded Verifiable Credentials +mosip.data.share.create.url=${mosip.data.share.url}/v1/datashare/create/static-policyid/static-subscriberid +# URL pattern used to validate DataShare URL and retrieve Verifiable Credential from DataShare +mosip.data.share.get.url.pattern=${mosip.data.share.url}/v1/datashare/get/static-policyid/static-subscriberid/* +# Specifies the maximum number of retry attempts for creating a DataShare request to store Verifiable Credential, ensuring reliability in case of transient failures +mosip.data.share.create.retry.count=3 + +#Google OAuth2 configuration +# Specifies the client ID for Google OAuth2 authentication, required to identify the application during OAuth2 requests. +spring.security.oauth2.client.registration.google.client-id=${GOOGLE_OAUTH_CLIENT_ID} +# Specifies the client secret for Google OAuth2 authentication, used to authenticate the application securely +spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_OAUTH_CLIENT_SECRET} +# Defines the scopes requested during Google OAuth2 authentication, specifying the level of access required (e.g., profile and email) +spring.security.oauth2.client.registration.google.scope=profile,email +# Specifies the redirect URI for Google OAuth2 authentication callbacks, where Google sends the authorization response +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +# Defines the authorization grant type for Google OAuth2 authentication, indicating the flow used (e.g., authorization code) +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +# Defines the human-readable name that will be displayed for the Google login option +# For example, if it is set to "Login with Google" it will appear as "Login with Google" on a spring security generated login page +spring.security.oauth2.client.registration.google.client-name=Google +# Specifies the authorization endpoint URI for Google OAuth2, where users are redirected to authenticate +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth?prompt=select_account +# Specifies the token endpoint URI for Google OAuth2, used to exchange the authorization code for an access token +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +# Specifies the user information endpoint URI for Google OAuth2, used to fetch user details after authentication +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +# Specifies the URI for Google's JSON Web Key Set (JWK) used for token validation, ensuring secure communication +spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs +# Maps the 'sub' attribute from the Google UserInfo endpoint response to the Spring Security principal's name (username) +spring.security.oauth2.client.provider.google.userNameAttribute=sub +# Maps the 'name' attribute from the Google UserInfo endpoint response to the Spring Security principal's accessible name +spring.security.oauth2.client.provider.google.nameAttribute=name +# Maps the 'email' attribute from the Google UserInfo endpoint response to the Spring Security principal's email address +spring.security.oauth2.client.provider.google.emailAttribute=email +# Maps the 'picture' attribute from the Google UserInfo endpoint response to the Spring Security principal's profile picture URL +spring.security.oauth2.client.provider.google.pictureAttribute=picture +# Maps the 'phone_number' attribute from the Google UserInfo endpoint response to the Spring Security principal's phone number +spring.security.oauth2.client.provider.google.phoneNumberAttribute=phone_number + +#Database configuration +# Specifies the JDBC URL for connecting to the PostgreSQL database, required to establish a connection to the database. +spring.datasource.url=${SPRING_DATASOURCE_URL} +# Defines the username for accessing the PostgreSQL database, required for authentication. +spring.datasource.username=${POSTGRES_USER} +# Defines the password for accessing the PostgreSQL database, required for authentication. +spring.datasource.password=${POSTGRES_PASSWORD} +# Configures the Hibernate behavior for database schema management, setting it to 'none' prevents Hibernate from automatically creating, updating, or dropping the database schema on startup +spring.jpa.hibernate.ddl-auto=none +# Enables or disables the display of SQL statements in the logs, useful for debugging database queries +spring.jpa.show-sql=true +# Specifies the driver class name for the PostgreSQL database, required for the application to interact with the database. +spring.datasource.driver-class-name=org.postgresql.Driver + +# Http session cache configuration +# Specifies which session store type to be used for storing http sessions in User Login flow +spring.session.store-type=caffeine +# Specifies the duration of inactivity after which a user session in configured session store (Ex: redis) will expire, ensuring session cleanup. +server.servlet.session.timeout=30m + +# Configures the database dialect for Hibernate (PostgreSQL in this case). +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +# List of URLs that can be accessed without any user authentication by Identity Provider +mosip.security.ignore-auth-urls=/safetynet/**,/actuator/**,/swagger-ui/**,/v3/api-docs/**,\ + /allProperties,/credentials/**,/credentialshare/**,/binding-otp,/wallet-binding,/get-token/**,\ + /issuers,/issuers/**,/v2/issuers,/v2/issuers/**,/authorize,/req/otp,/vid,/req/auth/**,/req/individualId/otp,/aid/get-individual-id,\ + /verifiers, /auth/*/token-login + +# CSRF Security Configuration +# CSRF protection is always enabled by default. State-changing requests (POST, PUT, DELETE, PATCH) +# require a valid CSRF token to be sent in the X-XSRF-TOKEN header. +# List of URLs that are excluded from CSRF protection. These URLs can still require authentication. +# OAuth2 endpoints and public endpoints are typically excluded from CSRF protection. +mosip.security.csrf-ignore-urls=/oauth2/**,/logout,/safetynet/**,/credentials/**,/credentialshare/**,/binding-otp,/wallet-binding,\ + /get-token/**,/req/otp,/vid,/req/auth/**,/req/individualId/otp,/auth/*/token-login + +# Inji mobile wallet configuration +# This template defines the list of the fields of the downloaded verifiable credential will be included in the event files when generating different documents +credential.template=template.json +# Specifies the base directory path where the credentials which are downloaded in the Inji mobile wallet will be stored +credential.data.path=data +safetynet.api.key= +safetynet.api.url=https://www.googleapis.com/androidcheck/v1/attestations/verify?key=${safetynet.api.key} + +registration.processor.print.textfile=registration-processor-print-text-file.json + +# Application ID used for identifying and managing the Mimoto service keys in Key Manager service +mosip.inji.app.id=MIMOTO + +# Cache related configurations +# Specifies which cache type to use (e.g., caffeine, redis) +spring.cache.type=caffeine +# Cache expiry time in minutes for the issuer's well-known endpoint response. +cache.credential-issuer.wellknown.expiry-time-in-min=60 +# Cache expiry time in minutes for issuers configurations read from a config file. +cache.issuers-config.expiry-time-in-min=60 +# Cache expiry time in minutes for the authentication server's well-known endpoint response. +cache.credential-issuer.authserver-wellknown.expiry-time-in-min=60 +# Cache expiry time in minutes for the pre-registered trusted verifiers list. +cache.pre-registered-trusted-verifiers.expiry-time-in-min=60 +# Default cache expiry time in minutes for others cache types +cache.default.expiry-time-in-min=60 + +# Wallet Passcode Configuration +# Duration (in minutes) for which the wallet remains locked after exceeding failed attempts in a cycle. +# Must be a whole number; decimal/fractional values (e.g., 0.5) will throw an error during Mimoto launch +wallet.passcode.retryBlockedUntil=60 +# Maximum number of failed passcode attempts allowed in a single cycle (defaults to 1 if <=0) +wallet.passcode.maxFailedAttemptsAllowedPerCycle=5 +# Maximum number of lock cycles allowed before the wallet is permanently locked (defaults to 1 if <=0) +wallet.passcode.maxLockCyclesAllowed=3 + +# Signing Algorithms Priority Order Configuration +signing.algorithms.priority.order=ED25519,ES256K,ES256,RS256 + +# This config enables CORS for the application, allowing it to handle cross-origin requests +mosip.security.cors-enable=true +# Predefined list of cross origins which are allowed to access the application resources +mosip.security.origins=http://localhost:3004 \ No newline at end of file diff --git a/docker-compose/config/mimoto-issuers-config.json b/docker-compose/config/mimoto-issuers-config.json new file mode 100644 index 000000000..eb612f8f7 --- /dev/null +++ b/docker-compose/config/mimoto-issuers-config.json @@ -0,0 +1,31 @@ +{ + "issuers": [ + { + "issuer_id": "Mock", + "display": [ + { + "name": "Mock Identity", + "logo": { + "url": "https://inji.github.io/inji-config/logos/mosipid-logo.png", + "alt_text": "mosip-logo" + }, + "title": "Mock Identity", + "description": "Download Mock Identity Credential", + "language": "en" + } + ], + "protocol": "OpenId4VCI", + "client_id": "wallet-demo", + "client_alias": "wallet-demo-client", + "qr_code_type": "OnlineSharing", + "credential_issuer_host": "https://injicertify-mock.collab.mosip.net", + "wellknown_endpoint": "https://injicertify-mock.collab.mosip.net/v1/certify/issuance/.well-known/openid-credential-issuer", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "authorization_audience": "https://esignet-mock.collab.mosip.net/v1/esignet/oauth/v2/token", + "token_endpoint": "https://localhost:8099/v1/mimoto/get-token/Mock", + "proxy_token_endpoint": "https://esignet-mock.collab.mosip.net/v1/esignet/oauth/v2/token", + "credential_issuer": "Mock", + "enabled": "true" + } + ] +} \ No newline at end of file diff --git a/docker-compose/config/mimoto-trusted-verifiers.json b/docker-compose/config/mimoto-trusted-verifiers.json new file mode 100644 index 000000000..8c0576d90 --- /dev/null +++ b/docker-compose/config/mimoto-trusted-verifiers.json @@ -0,0 +1,16 @@ +{ + "verifiers": [ + { + "client_id": "https://injiverify.collab.mosip.net", + "redirect_uris": [ + "https://injiverify.collab.mosip.net/" + ], + "response_uris": [ + "https://injiverify.collab.mosip.net/v1/verify/vp-submission/vp-direct-post" + ], + "jwks_uri": "https://injiverify.collab.mosip.net/.well-known/jwks.json", + "allow_unsigned_request": false, + "spec_version": "draft23" + } + ] +} \ No newline at end of file diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml new file mode 100644 index 000000000..7ad24cc03 --- /dev/null +++ b/docker-compose/docker-compose.yml @@ -0,0 +1,82 @@ +version: '3.8' + +services: + + minio: + container_name: 'minio-service' + image: mosipint/minio:2022.2.7-debian-10-r0 + ports: + - "9001:9001" + - "9000:9000" + environment: + MINIO_ROOT_USER: minioadmin # Access Key + MINIO_ROOT_PASSWORD: minioadmin # Secret Key + + datashare: + container_name: 'datashare-service' + image: mosipid/data-share-service:1.3.0-beta.2 + ports: + - "8097:8097" + environment: + - active_profile_env=inji-default,standalone + - SPRING_CONFIG_NAME=data-share + - SPRING_CONFIG_LOCATION=/home/mosip/ + - DATASHARE_DOMAIN=datashare-service:8097 + volumes: + - ./config/data-share-inji-default.properties:/home/mosip/data-share-inji-default.properties + - ./config/data-share-standalone.properties:/home/mosip/data-share-standalone.properties + depends_on: + - minio + + nginx: + container_name: nginx + image: nginx:alpine + ports: + - '80:80' + volumes: + - ./config/mimoto-issuers-config.json:/config/server/mimoto-issuers-config.json + - ./config/mimoto-trusted-verifiers.json:/config/server/mimoto-trusted-verifiers.json + - ./config/credential-template.html:/config/server/credential-template.html + - ./nginx.conf:/etc/nginx/nginx.conf + + postgres: + image: 'postgres:latest' + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql + - ./mimoto_init.sql:/docker-entrypoint-initdb.d/mimoto_init.sql + + mimoto-service: + container_name: 'mimoto-service' + image: 'injistackdev/mimoto:develop' + user: root + ports: + - '8099:8099' + environment: + - container_user=mosip + - active_profile_env=default + - SPRING_CONFIG_NAME=mimoto + - SPRING_CONFIG_LOCATION=/home/mosip/ + - SPRING_CLOUD_BOOTSTRAP_LOCATION=/home/mosip/mimoto-bootstrap.properties + - oidc_p12_password=dummypassword + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/inji_mimoto + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - GOOGLE_OAUTH_CLIENT_ID=clientId + - GOOGLE_OAUTH_CLIENT_SECRET=clientSecret + volumes: + - ./config/mimoto-default.properties:/home/mosip/mimoto-default.properties + - ./config/mimoto-bootstrap.properties:/home/mosip/mimoto-bootstrap.properties + - ./config/mimoto-issuers-config.json:/home/mosip/mimoto-issuers-config.json + - ./config/mimoto-trusted-verifiers.json:/home/mosip/mimoto-trusted-verifiers.json + - ./certs/oidckeystore.p12:/home/mosip/certs/oidckeystore.p12 + depends_on: + - datashare + - postgres + +volumes: + postgres-data: diff --git a/docker-compose/mimoto_init.sql b/docker-compose/mimoto_init.sql new file mode 100644 index 000000000..082049966 --- /dev/null +++ b/docker-compose/mimoto_init.sql @@ -0,0 +1,159 @@ +CREATE DATABASE inji_mimoto + ENCODING = 'UTF8' + LC_COLLATE = 'en_US.UTF-8' + LC_CTYPE = 'en_US.UTF-8' + TABLESPACE = pg_default + OWNER = postgres + TEMPLATE = template0; + +COMMENT ON DATABASE inji_mimoto IS 'mimoto related data is stored in this database'; + +\c inji_mimoto postgres + +DROP SCHEMA IF EXISTS mimoto CASCADE; +CREATE SCHEMA mimoto; +ALTER SCHEMA mimoto OWNER TO postgres; +ALTER DATABASE inji_mimoto SET search_path TO mimoto,pg_catalog,public; + +--- keymanager specific DB changes --- +CREATE TABLE mimoto.key_alias( + id character varying(36) NOT NULL, + app_id character varying(36) NOT NULL, + ref_id character varying(128), + key_gen_dtimes timestamp, + key_expire_dtimes timestamp, + status_code character varying(36), + lang_code character varying(3), + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + cert_thumbprint character varying(100), + uni_ident character varying(50), + CONSTRAINT pk_keymals_id PRIMARY KEY (id), + CONSTRAINT uni_ident_const UNIQUE (uni_ident) +); + +CREATE TABLE mimoto.key_policy_def( + app_id character varying(36) NOT NULL, + key_validity_duration smallint, + is_active boolean NOT NULL, + pre_expire_days smallint, + access_allowed character varying(1024), + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + CONSTRAINT pk_keypdef_id PRIMARY KEY (app_id) +); + +CREATE TABLE mimoto.key_store( + id character varying(36) NOT NULL, + master_key character varying(36) NOT NULL, + private_key character varying(2500) NOT NULL, + certificate_data character varying NOT NULL, + cr_by character varying(256) NOT NULL, + cr_dtimes timestamp NOT NULL, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + CONSTRAINT pk_keystr_id PRIMARY KEY (id) +); + +CREATE TABLE mimoto.ca_cert_store( + cert_id character varying(36) NOT NULL, + cert_subject character varying(500) NOT NULL, + cert_issuer character varying(500) NOT NULL, + issuer_id character varying(36) NOT NULL, + cert_not_before timestamp, + cert_not_after timestamp, + crl_uri character varying(120), + cert_data character varying, + cert_thumbprint character varying(100), + cert_serial_no character varying(50), + partner_domain character varying(36), + cr_by character varying(256), + cr_dtimes timestamp, + upd_by character varying(256), + upd_dtimes timestamp, + is_deleted boolean DEFAULT FALSE, + del_dtimes timestamp, + ca_cert_type character varying(25), + CONSTRAINT pk_cacs_id PRIMARY KEY (cert_id), + CONSTRAINT cert_thumbprint_unique UNIQUE (cert_thumbprint,partner_domain) + +); + +CREATE TABLE IF NOT EXISTS user_metadata ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + provider_subject_id character varying(255) NOT NULL, -- Unique identifier for the provider subject + identity_provider character varying(255) NOT NULL, -- Unique identifier for the identity provider + display_name TEXT NOT NULL, -- Display name of the user + profile_picture_url TEXT, -- URL of the user's profile picture + phone_number TEXT, -- Phone number of the user + email TEXT NOT NULL, -- Email of the user (Required field) + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now() -- Timestamp of last update (defaults to current time) +); + +CREATE TABLE IF NOT EXISTS wallet ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + user_id character varying(36) NOT NULL, -- Foreign key referencing user_metadata + wallet_key TEXT NOT NULL, -- Encrypted wallet key (retained here) + wallet_metadata JSONB NOT NULL, -- Metadata about the wallet, including encryption info + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user_metadata (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS verifiable_credentials ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + credential TEXT NOT NULL, -- Encrypted credential (using wallet_key for encryption/decryption) + credential_metadata JSONB NOT NULL, -- Metadata about the credential + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_wallet_id FOREIGN KEY (wallet_id) REFERENCES wallet (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS proof_signing_key ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referencing the wallet table + public_key TEXT NOT NULL, -- Public key for wallet + secret_key TEXT NOT NULL, -- Secret key, encrypted using proof_signing_key + key_metadata JSONB NOT NULL, -- Metadata about the public and private keys + created_at TIMESTAMP DEFAULT now(), -- Timestamp of record creation (defaults to current time) + updated_at TIMESTAMP DEFAULT now(), -- Timestamp of last update (defaults to current time) + + CONSTRAINT fk_wallet_id FOREIGN KEY (wallet_id) REFERENCES wallet (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS trusted_verifiers ( + id character varying(36) PRIMARY KEY, -- Primary key for the table + wallet_id character varying(36) NOT NULL, -- Foreign key referring to the wallet table (wallet.id) + verifier_id character varying(255) NOT NULL, -- Stores the unique identifier (client_id) of the trusted verifier + created_at TIMESTAMP DEFAULT now() -- Timestamp of record creation (defaults to current time) +); + +CREATE TABLE IF NOT EXISTS verifiable_presentations ( + id character varying(36) PRIMARY KEY, + wallet_id character varying(36) NOT NULL, + auth_request JSONB NOT NULL, + presentation_data JSONB NOT NULL, + verifier_id character varying(255), + status character varying(32) NOT NULL, + requested_at TIMESTAMP, + created_at TIMESTAMP DEFAULT now(), + consent BOOLEAN NOT NULL DEFAULT TRUE +); + +INSERT INTO mimoto.key_policy_def(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('ROOT', 2920, 1125, 'NA', true, 'mosipadmin', now()); +INSERT INTO mimoto.key_policy_def(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('BASE', 1095, 60, 'NA', true, 'mosipadmin', now()); +INSERT INTO mimoto.key_policy_def(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('MIMOTO', 1095, 60, 'NA', true, 'mosipadmin', now()); diff --git a/docker-compose/nginx.conf b/docker-compose/nginx.conf new file mode 100644 index 000000000..03a3e1d7c --- /dev/null +++ b/docker-compose/nginx.conf @@ -0,0 +1,12 @@ +events { } + +http { + server { + listen 80; + + location / { + root /config/server; + autoindex on; + } + } +} diff --git a/docs/Claim169QRCodeSupport.md b/docs/Claim169QRCodeSupport.md new file mode 100644 index 000000000..f47e843d0 --- /dev/null +++ b/docs/Claim169QRCodeSupport.md @@ -0,0 +1,49 @@ +# Claim 169 QR Code Support + +## Overview +Mimoto now supports consumption of claim 169 QR data present in the Verifiable Credential (VC) issued by the issuer. This feature allows users to easily access and utilize the claim 169 data for various purposes, such as displaying it in the user interface or using it for verification. Mimoto now gives preference to claim 169 QR code present in the VC while displaying and generating PDF for VCs. + +## Pre-requisites +- VC should be valid and should contain claim 169 QR code data in the expected format. For e.g. for JSON-LD VC, claim 169 QR code data should be present in the `credentialSubject` section of the VC with the key `claim169`. + + ```json + "claim169": { + "identityQRCode": "NCF4:OCFBI%S:2EQ7NMC2558J6HW8VDQ0DW8V27MMSLYG%8WTGTEF9-8G7K6A5S+ORSWH4%F12802...", + "faceQRCode": "NCF4:OCFBI%S:2EQ7NMC2558J6HW8VDQ0DW8V27MMSLYG%8WTSJSJGTEF9-8G7K6A5S+ORSWH4%F12802..." + }, + ``` +- The claim 169 QR code data should be in the expected format as per the requirements of the application consuming it. Refer Claim 169 QR Code specification for further details : https://docs.mosip.io/1.2.0/readme/standards-and-specifications/mosip-standards/169-qr-code-specification +- Issuer configuration should have `qr_code_type` set as `EmbeddedVC` in [mimoto-issuers-config.json](../docker-compose/config/mimoto-issuers-config.json). + +## User flow : +**Note: The flow is applicable for both guest users and wallet users.** +1. User downloads the VC in guest mode or selects the VC from their wallet for PDF generation. +2. During PDF generation, Mimoto will check for the presence of claim 169 QR code data in the VC. +3. If claim 169 QR code data is present, Mimoto will extract the QR data in the first field encountered during iteration inside `claim169` object, generate QR code image and add it to the QR code placeholder in the generated PDF. +4. PDF is returned to the user. + +## Sequence Diagram +```mermaid +sequenceDiagram + participant User + participant InjiWeb as InjiWeb UI + participant Mimoto Service + participant VCSource as Database/Issuer + + User->>InjiWeb: Request to download VC / Generate PDF for VC in wallet + InjiWeb->> Mimoto Service: Download VC / Generate PDF request with VC identifier + Mimoto Service->>VCSource: Retrieve VC data + VCSource-->>Mimoto Service: Return VC + Mimoto Service->>Mimoto Service: Check for claim 169 QR code data in VC and issuer configuration + alt claim 169 QR code present and qr_code_type is EmbeddedVC in issuer configuration + Mimoto Service->>Mimoto Service: Extract claim 169 QR code data
and generate PDF with claim 169 QR code image + else not eligible for claim 169 QR code rendering + Mimoto Service->>Mimoto Service: Generate standard PDF with QR code based on VC data for EmbeddedVC
or data share url for OnlineSharing + end + Mimoto Service-->>InjiWeb: Return generated PDF + InjiWeb-->>User: Send PDF back to user +``` + +## References + +- [Claim 169 QR Code Specification](https://docs.mosip.io/1.2.0/readme/standards-and-specifications/mosip-standards/169-qr-code-specification) \ No newline at end of file diff --git a/docs/GoogleOauth2LoginIntegration.md b/docs/GoogleOauth2LoginIntegration.md new file mode 100644 index 000000000..d86a1db85 --- /dev/null +++ b/docs/GoogleOauth2LoginIntegration.md @@ -0,0 +1,158 @@ +# Google OAuth2 Login Integration Guide + +## Overview +This documentation explains the implementation of the login feature using Google OAuth2 login. It also covers the authentication mechanism for APIs in : + - `UsersController` + - `WalletController` + - `WalletCredentialsController`. + +--- + +## Prerequisites +Before you begin, ensure you have: +1. **A Google Developer Console account.** +2. **A project created in the Google Developer Console.** +3. **OAuth2 enabled for the project.** +4. **A Client ID and Client Secret generated.** +--- + +## Google OAuth2 Integration Steps + +1. **Configure OAuth2 Login**: + - Google client ID and secret in `application-default.properties`: + ``` + spring.security.oauth2.client.registration.google.client-id=${mosip.injiweb.google.client.id} + spring.security.oauth2.client.registration.google.client-secret=${mosip.injiweb.google.client.secret} + ``` + - Make sure value of `mosip.injiweb.google.client.id` and `mosip.injiweb.google.client.secret` are set in the environment. +2. **Authorized Host URL**: + - Register the base domain of your mimoto in Google Developer Console, e.g., `https://your-mimoto-domain`. + +3. **Redirect URI**: + - Under Authorized redirect URIs in Google Developer Console, add: + `https:///oauth2/callback/*` + Replace `` with your application's domain. + - Ensure the redirect URI matches the one configured in the Google Developer Console. +--- + +## Execution Flow + +### Phase 1: Login Initiation +1. **User Action**: The user clicks the "Continue with Google" button on the Inji Web landing page. +2. **Redirection**: Inji Web redirects the browser to Mimoto's authorization endpoint. +3. **IdP Handshake**: Mimoto's security filter chain handles the request and redirects the user to the IDP's consent screen. + +### Phase 2: Authorization & Token Exchange +1. **Callback**: After account selection, the IDP redirects back to Mimoto with an authorization code. +2. **Token Retrieval**: Spring Security internally exchanges this code for an `access_token` and fetches the user profile (Name, Email, Picture). + +### Phase 3: User Onboarding +After successful login, the system checks if the user already exists. If not, it creates a new internal identity and securely stores the user’s profile information. + +An internal `userId` is generated to uniquely identify the user within the system. This ensures the system is not tightly coupled to a specific identity provider and can support multiple providers in the future. + +1. **Metadata Check**: Mimoto checks the `user_metadata` table. +2. **Creation/Update**: If new, it generates a unique internal **UUID** (`userId`) and encrypts the PII (Name, Email, Picture). Else the existing record is used. +3. **Enrichment**: The internal `userId` is added to the security principal for downstream use. + +### Phase 4: Success/Failure Handling +1. **Success**: Mimoto saves the `userId` and user metadata into the **HTTPSession** and redirects the user to the passcode page. +2. **Failure**: Mimoto captures errors (timeouts, denied consent), encodes them, and redirects back to the UI with error parameters. +--- + +## Sequence Diagram +### Google Login Flow +```mermaid +sequenceDiagram + participant User + participant Browser + participant mimoto + participant GoogleOAuth2 + participant Redis + + User->>Browser: Navigate to Login Page + Browser->>mimoto: Request Login + mimoto->>GoogleOAuth2: Redirect to Google Login + GoogleOAuth2->>User: Prompt for Credentials + User->>GoogleOAuth2: Enter Credentials + GoogleOAuth2->>mimoto: Send Authorization Code + mimoto->>GoogleOAuth2: Exchange Code for Access Token + GoogleOAuth2->>mimoto: Return Access Token & User Info + mimoto->>Redis: Store Session ID and User Metadata + mimoto->>Browser: Set Session ID in Cookie / Response + Browser->>User: Redirect to Dashboard + ``` +### Authenticated API Access Flow +```mermaid + sequenceDiagram + participant User + participant Browser + participant mimoto + participant Redis + + User->>Browser: Request API + Browser->>mimoto: Send API Request with Session ID + mimoto->>Redis: Validate Session ID + Redis->>mimoto: Return User Metadata + mimoto->>Browser: Respond with Data +``` +--- +## Key Points +### Session-Based Authentication: +- On successful login, a session ID is generated and stored in Redis. +- All APIs in UsersController, WalletController, and WalletCredentialsController validate the session ID stored in Redis. + +### Integration Points in Application +### Component Responsibility + +| **Component** | **Responsibility** | +|--------------------------------|-------------------------------------------------------------------------------------| +| `Config` | Configures OAuth2 login, session management, and security settings. | +| `OAuth2AuthenticationSuccessHandler` | Handles successful authentication, stores user metadata in the session, and redirects to the dashboard. | +| `OAuth2AuthenticationFailureHandler` | Handles authentication failures, logs errors, and redirects to the login page with error details. | +| `CustomOAuth2UserService` | Retrieves and processes user information from the OAuth2 provider. | +| `TokenAuthController` | Provides API for token-based authentication and session creation. | +| `UsersController` | Manages user profile retrieval using session-based authentication. | +| `WalletsController` | Handles wallet creation, unlocking, and deletion using session-based authentication. | +| `WalletCredentialsController` | Manages credential download, retrieval, and deletion for wallets. | + +### Configuration & Switching Providers +If you want to change your IDP from Google to another provider, you need to update the following properties. + +**Important:** When switching, you should replace the word `google` in the property keys with your new provider's **Registration ID** (e.g., `okta`, `facebook`, or `keycloak`). Refer `application-default.properties` for the property keys. + +#### A. Client Registration (in `application-default.properties`) +These properties identify the application to the IDP: +* `spring.security.oauth2.client.registration.{registrationId}.client-id` – Client ID from your new provider. +* `spring.security.oauth2.client.registration.{registrationId}.client-secret` – Client secret from your new provider. +* `spring.security.oauth2.client.registration.{registrationId}.scope` – Scopes supported (e.g., `profile, email`). +* `spring.security.oauth2.client.registration.{registrationId}.client-name` – Human-readable name for the login button. + +#### B. Provider Endpoints (in `application-default.properties`) +These tell Mimoto where to send the user for authentication: +* `spring.security.oauth2.client.provider.{registrationId}.authorization-uri` – The provider's login URL. +* `spring.security.oauth2.client.provider.{registrationId}.token-uri` – The URL to exchange codes for tokens. +* `spring.security.oauth2.client.provider.{registrationId}.user-info-uri` – The URL to fetch user profile details. +* `spring.security.oauth2.client.provider.{registrationId}.jwk-set-uri` – The URI for the provider's public keys to validate tokens. + +#### C. Attribute Mappings (in `application-default.properties`) +These map the IDP's response fields to Inji’s internal metadata: +* `spring.security.oauth2.client.provider.{registrationId}.userNameAttribute` – The unique identifier (e.g., `sub`). +* `spring.security.oauth2.client.provider.{registrationId}.nameAttribute` – Field for the user's full name. +* `spring.security.oauth2.client.provider.{registrationId}.emailAttribute` – Field for the user's email. +* `spring.security.oauth2.client.provider.{registrationId}.pictureAttribute` – Field for the profile picture URL. + +#### D. Global Properties (in `application.properties`) +* `googleIdToken` – Update this URL to point to the new provider's token endpoint + + +### Errors +When login fails, Mimoto uses **302 Redirects** to pass error context back to Inji Web. +Direct requests to protected APIs made without a valid session will return a standard 401 Unauthorized response. + +| Scenario | HTTP Status | Description | +|--------------------|-------------|---------------------------------------------------------------------------------------| +| **Login Success** | 302 | Redirects to `${mosip.inji.web.authentication.success.redirect.url}/user/passcode`. | +| **Consent Denied** | 302 | Redirects to `${injiWebUrl}/?status=error&error_message=...`. | +| **IDP Timeout** | 302 | Redirects to `${injiWebUrl}/?status=error&error_message=...`. IDP servers unreachable. | +| **Unauthorized** | 401 | Returned if a user attempts to access protected APIs without an active session. | \ No newline at end of file diff --git a/docs/InjiCustomTemplateFeature.md b/docs/InjiCustomTemplateFeature.md new file mode 100644 index 000000000..14b4dbc25 --- /dev/null +++ b/docs/InjiCustomTemplateFeature.md @@ -0,0 +1,41 @@ +# Download Custom Credential Templates + +## Overview +This feature provides option to download a Verifiable Credential (VC) as a PDF using custom templates. +The templates is fetched from a configuration server. Code first tries to find a template using the pattern `issuerId-credentialType-template.html`. +If this specific template is not found, it will use `credential-template.html`. + +## Sequence Diagram + +```mermaid +sequenceDiagram +participant Client +participant CredentialsController +participant Utilities +participant ConfigServer + + Client->>+CredentialsController: POST /download + CredentialsController->>+Utilities: getCredentialSupportedTemplateString(issuerId, credentialType) + alt Template found in Config Server + Utilities->>+ConfigServer: Fetch template (issuerId-credentialType-template.html) + ConfigServer-->>-Utilities: Return custom template + else Template not found + Utilities->>+ConfigServer: Fetch default template (credential-template.html) + ConfigServer-->>-Utilities: Return default template + end + Utilities-->>-CredentialsController: Return template + CredentialsController->>CredentialsController: Generate PDF using template + CredentialsController-->>Client: Return PDF (200 OK) + CredentialsController->>+Utilities: handleExceptionWithErrorCode(exception, errorCode) + Utilities-->>-CredentialsController: Return error details + CredentialsController-->>-Client: ResponseEntity with error (400 Bad Request) +``` + +## Custom Template Samples + +Below are the links to the custom template samples that you can use: + +- [Stay Protected Insurance Credential Template](https://github.com/mosip/inji-config/blob/release-0.7.x/StayProtected-InsuranceCredential-template.html) +- [Mosip Verifiable Credential Template](https://github.com/mosip/inji-config/blob/release-0.7.x/Mosip-MosipVerifiableCredential-template.html) + +You can view and download the templates from the links above. Feel free to explore and customize them as per your requirements. \ No newline at end of file diff --git a/docs/InjiIssuerConfigurationFeature.md b/docs/InjiIssuerConfigurationFeature.md new file mode 100644 index 000000000..d40b48754 --- /dev/null +++ b/docs/InjiIssuerConfigurationFeature.md @@ -0,0 +1,74 @@ +# Issuer Configuration Feature + +## Overview + +This feature offers an endpoint to fetch the configuration of a credential issuer. +The configuration is retrieved from both the issuer's well-known endpoint and the authorization server's well-known +endpoint. To enhance performance and minimize the load on external services, the results are cached using Caffeine. +This endpoint is invoked in inji-web when an issuer is selected, allowing its configurations to be retrieved. + +## Sequence Diagram + +The following sequence diagram illustrates the flow of the `getIssuerConfiguration` method in the `IssuersController` class, +including interactions with the `IssuersService`, `IssuerConfigUtil`, `Utilities` and caching mechanisms. + +```mermaid +sequenceDiagram + participant Client + participant IssuersController + participant IssuersService + participant IssuerConfigUtil + participant Cache + participant Utilities + + Client->>+IssuersController: GET /issuers/{issuer-id}/configuration + IssuersController->>+IssuersService: getIssuerConfiguration(issuerId) + alt Cache hit + IssuersService->>+Cache: Check cache for issuer configuration (Caffeine) + Cache-->>-IssuersService: Return cached configuration + else Cache miss + IssuersService->>+IssuersService: getIssuerDetails(issuerId) + IssuersService->>+IssuerConfigUtil: getIssuerWellknown(issuerDetails.getCredential_issuer_host()) + IssuerConfigUtil-->>-IssuersService: CredentialIssuerWellKnownResponse + IssuersService->>+IssuerConfigUtil: getAuthServerWellknown(credentialIssuerWellKnownResponse.getAuthorizationServers().get(0)) + IssuerConfigUtil-->>-IssuersService: AuthorizationServerWellKnownResponse + IssuersService->>+Cache: Store configuration in cache (expiry: 60 min) + end + IssuersService-->>-IssuersController: Return issuer configuration + IssuersController->>+Utilities: handleExceptionWithErrorCode(exception, errorCode) + Utilities-->>-IssuersController: Return error details + IssuersController-->>-Client: ResponseEntity with configuration or error +``` + +## Configuration + +### Cache Configuration + +The caching mechanism used in this project is Caffeine. Cache timeout properties are defined in the +`application-local.properties` file for the local setup, and in the `mimoto-default.properties` file for the environment setup. + +#### Cache Timeout Properties + +- `cache.credential-issuer.wellknown.expiry-time-in-min`: Cache expiry time in minutes for the issuer's well-known endpoint response. +- `cache.issuers-config.expiry-time-in-min`: Cache expiry time in minutes for issuers configurations read from a config file. +- `cache.credential-issuer.authserver-wellknown.expiry-time-in-min`: Cache expiry time in minutes for the authentication server's well-known endpoint response. +- `cache.pre-registered-trusted-verifiers.expiry-time-in-min`: Cache expiry time in minutes for the pre-registered trusted verifiers list. +- `cache.default.expiry-time-in-min`: Default cache expiry time in minutes for other cache types. + +### Increasing Cache Time + +To increase the cache time, you can modify the properties in the `application-local.properties` or +`mimoto-default.properties` file. For example, to set the cache expiry time to 120 minutes, update the properties as follows: + +```properties +# Cache expiry time in minutes for the issuer's well-known endpoint response. +cache.credential-issuer.wellknown.expiry-time-in-min = 120 +# Cache expiry time in minutes for issuers configurations read from a config file. +cache.issuers-config.expiry-time-in-min = 120 +# Cache expiry time in minutes for the authentication server's well-known endpoint response. +cache.credential-issuer.authserver-wellknown.expiry-time-in-min = 120 +# Cache expiry time in minutes for the pre-registered trusted verifiers list. +cache.pre-registered-trusted-verifiers.expiry-time-in-min=60 +# Default cache expiry time in minutes for other cache types. +cache.default.expiry-time-in-min = 120 +``` \ No newline at end of file diff --git a/docs/IssuersV2EndpointMigration.md b/docs/IssuersV2EndpointMigration.md new file mode 100644 index 000000000..38bd7510a --- /dev/null +++ b/docs/IssuersV2EndpointMigration.md @@ -0,0 +1,185 @@ +# Issuers V2 API and mimoto-issuers-config + +This document describes the **Issuers V2** endpoints, how to configure **mimoto-issuers-config.json**, and how **both the old and new issuer schema** are supported. + +--- + +## V2 Endpoints + +The V2 issuer API provides a minimal, configuration-only view of issuers. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/v2/issuers` | List all onboarded issuers | +| GET | `/v2/issuers/{issuer-id}` | Get a single issuer by ID | + +### V2 features + +- **Configuration-only response** + Response data comes only from **mimoto-issuers-config.json**. No remote well-known calls are made. Fields returned include: `issuer_id`, `protocol`, `display`, `client_id`, `token_endpoint`, `client_alias`, `qr_code_type`, `enabled`, `credential_issuer_host`. + +- **Runtime token_endpoint generation** + - The application will compute and set `token_endpoint` at runtime for every issuer response (both V1 and V2). The computed value is: `${mosip.api.public.url}${server.servlet.context-path}/get-token/{issuerId}`. + - This computed `token_endpoint` will overwrite whatever value (if any) is present in the config file. In other words, `token_endpoint` in `mimoto-issuers-config.json` is optional and, if present, will be ignored in favor of the generated value. + +- **Same config, both APIs** + The same config file is used for: + - **V1** (`/issuers`, `/issuers/{issuer-id}`): returns an enriched response that may include well-known–derived fields (e.g. `wellknown_endpoint`, `authorization_audience`, `proxy_token_endpoint`). V1 may call each issuer’s well-known endpoint. Note: the `token_endpoint` in the V1 response will also be the generated value (overwritten at runtime). + - **V2** (`/v2/issuers`, `/v2/issuers/{issuer-id}`): returns only the issuer entries from config; no well-known fetch. `token_endpoint` is still present in the V2 response but is computed by the service at runtime. + +- **Error handling** + V2 returns `400` when the config is not accessible and `404` for an invalid `issuer-id` on the single-issuer endpoint. + +--- + +## Configuring mimoto-issuers-config.json + +### Location and property + +- **Property:** `mosip.openid.issuers` +- **Default (e.g. local):** `mimoto-issuers-config.json` (loaded from classpath, e.g. `src/main/resources/`). +- **Environment / Docker:** The same property can point to an external or mounted file path, depending on your deployment. + +Example in `application-local.properties`: + +```properties +mosip.openid.issuers=mimoto-issuers-config.json +``` + +### File structure + +The file must be valid JSON with a top-level key **`issuers`**, which is an array of issuer objects: + +```json +{ + "issuers": [ + { + "issuer_id": "MyIssuer", + "protocol": "OpenId4VCI", + "display": [ ... ], + "client_id": "...", + "token_endpoint": "https://...", + "client_alias": "...", + "qr_code_type": "OnlineSharing", + "enabled": "true", + "credential_issuer_host": "https://..." + } + ] +} +``` + +> Note: `token_endpoint` may be present in config for documentation but it is optional — the service will compute and overwrite it at runtime. + +### Required fields (all schemas) + +Every issuer entry must include: + +| Field | Type | Description | +|-------|------|-------------| +| `issuer_id` | string | Unique identifier of the issuer | +| `protocol` | string | `"OTP"` or `"OpenId4VCI"` | +| `display` | array | At least one display object with `name`, `logo` (with `url`, `alt_text`), `title`, `description`, `language` | +| `client_id` | string | OIDC client ID | +| `client_alias` | string | Client alias in keyStore | +| `qr_code_type` | string | `"OnlineSharing"`, `"EmbeddedVC"`, or `"None"` | +| `enabled` | string | `"true"` or `"false"` | +| `credential_issuer_host` | string | Valid URL of the credential issuer host | + +`token_endpoint` is optional in the issuer entry and will be generated by the application at runtime. + +Validation runs at startup (e.g. via `IssuersValidationConfig`). Invalid or missing required fields will cause startup failure with a clear message. + +--- + +## Old vs new schema (both supported) + +**Both the old and new issuer schema work** with the same application and config file. + +### New (minimal) schema + +Only the **required** fields above are needed. This is enough for **V2** and for **V1** to resolve well-known at runtime from `credential_issuer_host`: + +- `issuer_id`, `protocol`, `display`, `client_id`, `client_alias`, `qr_code_type`, `enabled`, `credential_issuer_host` (note that `token_endpoint` is optional and will be generated by the application at runtime) + +Example (minimal): + +```json +{ + "issuers": [ + { + "issuer_id": "StayProtected", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "StayProtected Insurance", + "logo": { "url": "https://...", "alt_text": "Logo" }, + "title": "Download StayProtected Insurance Credentials", + "description": "Download insurance credential", + "language": "en" + } + ], + "client_id": "mpartner-default-mimoto-insurance-oidc", + "token_endpoint": "https://api.example.com/v1/mimoto/get-token/StayProtected", + "client_alias": "mpartner-default-mimoto-insurance-oidc", + "qr_code_type": "OnlineSharing", + "enabled": "true", + "credential_issuer_host": "https://injicertify-insurance.example.com" + } + ] +} +``` + +> The runtime `token_endpoint` for the above issuer will be constructed as `${mosip.api.public.url}${server.servlet.context-path}/get-token/StayProtected` by the application and returned in both V1 and V2 responses. + +### Old (extended) schema + +You can still use the **extended** schema that includes optional fields previously used by the UI or tooling. These are **not** required for V2 or for V1; if present, they may be used or ignored depending on the endpoint: + +- `wellknown_endpoint` +- `redirect_uri` +- `authorization_audience` +- `proxy_token_endpoint` +- `credential_issuer` + +Example (extended): + +```json +{ + "issuers": [ + { + "issuer_id": "Mock", + "protocol": "OpenId4VCI", + "display": [ ... ], + "client_id": "wallet-demo", + "wellknown_endpoint": "https://.../.well-known/openid-credential-issuer", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "authorization_audience": "https://...", + "token_endpoint": "https://...", + "proxy_token_endpoint": "https://...", + "client_alias": "wallet-demo-client", + "qr_code_type": "OnlineSharing", + "enabled": "true", + "credential_issuer": "Mock", + "credential_issuer_host": "https://..." + } + ] +} +``` + +- **V2** ignores these extra fields and returns only the core issuer fields from config. the computed `token_endpoint` is included in the response. +- **V1** returns these fields as-is from the config (non-null when present), but its `token_endpoint` is also set/overwritten by the service at runtime. + +You can **mix** minimal and extended entries in the same `issuers` array; both schemas are valid. + +--- + +## Summary + +| Topic | Summary | +|-------|------------------------------------------------------------------------------------------------------------------------------------------| +| **V2 API** | `GET /v2/issuers` and `GET /v2/issuers/{issuer-id}`; config-only responses but `token_endpoint` is computed and injected at runtime. | +| **Config file** | Set `mosip.openid.issuers` (e.g. `mimoto-issuers-config.json`); structure is `{ "issuers": [ ... ] }`. | +| **Required fields** | `issuer_id`, `protocol`, `display`, `client_id`, `client_alias`, `qr_code_type`, `enabled`, `credential_issuer_host`.`token_endpoint` is optional in config and computed/injected at runtime. | +| **Schema compatibility** | Both **old (extended)** and **new (minimal)** issuer schemas work; extra fields are allowed and ignored by V2. | + +For OpenAPI details, see `docs/api-documentation-openapi.json` (paths `/v2/issuers` and `/v2/issuers/{issuer-id}`). For Postman, use the **Issuers** folder requests: **get Issuers List (V2)**, **get Issuer by ID (V2)**, and **Search Issuers List (V2)**. diff --git a/docs/OVP-1.0-Support.md b/docs/OVP-1.0-Support.md new file mode 100644 index 000000000..ca807bfee --- /dev/null +++ b/docs/OVP-1.0-Support.md @@ -0,0 +1,462 @@ +# OpenID4VP 1.0 support in Inji Web Wallet + +This document describes how **Inji Web Wallet clients and integrators** should use Mimoto’s presentation APIs when the verifier sends an **OpenID4VP 1.0 (DCQL)** authorization request. + +It focuses on **API contracts, response shapes, and UI behaviour** — not internal implementation details. + +For the older **Presentation Exchange (Draft-23)** flow, see [Appendix A](#appendix-a-draft-23-presentation-exchange). + +--- + +## Overview + +OVP 1.0 uses **DCQL** (Digital Credentials Query Language) instead of a flat `presentation_definition`. Mimoto exposes three session-authenticated endpoints under: + +```text +/wallets/{walletId}/presentations +``` + +| Step | Method | Path | Purpose | +|------|--------|------|---------| +| 1 | `POST` | `/wallets/{walletId}/presentations` | Parse verifier authorization request, create presentation session | +| 2 | `GET` | `/wallets/{walletId}/presentations/{presentationId}/credentials` | List wallet credentials that match the verifier’s DCQL query | +| 3 | `PATCH` | `/wallets/{walletId}/presentations/{presentationId}` | Submit selected credentials to the verifier, or reject the request | + +**Prerequisites** + +- Valid Mimoto **session** (user authenticated). +- Wallet **unlock key** present in session (`WALLET_KEY`) before steps 2 and 3. +- For state-changing calls (`PATCH`), include the **CSRF** token (`X-XSRF-TOKEN` header). + +**Important:** Always call **GET /credentials** before **PATCH** submit. Submit uses credential IDs cached in the presentation session from that GET response. Unknown IDs are rejected. + +--- + +## Detecting OVP 1.0 vs Draft-23 + +Mimoto selects the matching and submission path from the verifier authorization request: + +| Verifier request type | Mimoto behaviour | +|----------------------|------------------| +| DCQL (`AuthorizationDcqlRequest`) | **OVP 1.0** — use `queryGroups` / `credentialSets` | +| Presentation Exchange | **Draft-23** — use flat `availableCredentials` | + +**How the wallet UI knows which response shape to expect** + +| GET /credentials response | Meaning | +|---------------------------|---------| +| `queryGroups` is present | OVP 1.0 / DCQL | +| `availableCredentials` is present (no `queryGroups`) | Draft-23 | + +Do not send a mixed `selectedCredentials` array on submit — elements must be **all strings** (Draft-23) or **all objects** (DCQL). + +--- + +## Step 1 — Start presentation (`POST`) + +**Request** + +```json +{ + "authorizationRequestUrl": "openid4vp://authorize?client_id=..." +} +``` + +**Response (200)** + +```json +{ + "presentationId": "presentation-uuid", + "verifier": { + "clientId": "verifier-client-id", + "clientName": "Example Verifier", + "logoUri": "https://verifier.example.com/logo.png", + "trusted": true, + "preregisteredWithWallet": true + } +} +``` + +Store `presentationId` for the remaining steps. + +--- + +## Step 2 — Get matching credentials (`GET`) + +```http +GET /wallets/{walletId}/presentations/{presentationId}/credentials +``` + +### DCQL response model + +A DCQL response has **two layers**: + +```text +DCQL query (from verifier) +├── credentials[] ← Layer 1: one slot per CredentialQuery (→ queryGroups) +└── credential_sets[]? ← Layer 2: optional OR-grouping (→ credentialSets) +``` + +- **`queryGroups`** — one entry per `CredentialQuery.id`. Describes available credentials, formats, claims, and missing claims for that slot. +- **`credentialSets`** — optional grouping. When present, it defines which query slots are bundled into **options** the user must choose from. + +When `credentialSets` is **empty**, every query in `queryGroups` is an independent **mandatory** slot (DCQL default when the verifier omits `credential_sets`). + +### Example — two independent required queries + +Verifier needs national ID **and** driving license (no `credential_sets`): + +```json +{ + "queryGroups": [ + { + "queryId": "pid_query", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-uuid-111", + "credentialTypeDisplayName": "National ID Card", + "credentialTypeLogo": "https://issuer.example.com/logo.png", + "format": "ldp_vc", + "claims": ["$.name", "$.dateOfBirth"], + "sdClaims": ["$.email"] + } + ], + "missingClaims": [] + }, + { + "queryId": "mdl_query", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-uuid-222", + "credentialTypeDisplayName": "Driving License", + "format": "ldp_vc", + "claims": ["$.licenseNumber"], + "sdClaims": [] + } + ], + "missingClaims": [] + } + ], + "credentialSets": [] +} +``` + +### Example — `credential_sets` with options + +Verifier accepts **PAN OR Aadhaar OR (Voter ID + Driving License)**: + +```json +{ + "queryGroups": [ + { + "queryId": "pan", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-pan-111", + "credentialTypeDisplayName": "PAN Card", + "format": "ldp_vc", + "claims": ["$.name", "$.pan"], + "sdClaims": [] + } + ], + "missingClaims": [] + }, + { + "queryId": "aadhaar", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-aadh-222", + "credentialTypeDisplayName": "Aadhaar Card", + "format": "ldp_vc", + "claims": ["$.name", "$.dob"], + "sdClaims": ["$.address"] + } + ], + "missingClaims": [] + }, + { + "queryId": "voter_id", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-vid-333", + "credentialTypeDisplayName": "Voter ID", + "format": "ldp_vc", + "claims": ["$.name"], + "sdClaims": [] + } + ], + "missingClaims": [] + }, + { + "queryId": "dl", + "multiple": false, + "availableCredentials": [ + { + "credentialId": "vc-dl-444", + "credentialTypeDisplayName": "Driving License", + "format": "ldp_vc", + "claims": ["$.licenseNumber"], + "sdClaims": [] + } + ], + "missingClaims": [] + } + ], + "credentialSets": [ + { + "required": true, + "options": [ + ["pan"], + ["aadhaar"], + ["voter_id", "dl"] + ] + } + ] +} +``` + +### Field reference + +| Field | Description | +|-------|-------------| +| `queryGroups[].queryId` | `CredentialQuery.id` — **must** be sent back on submit | +| `queryGroups[].multiple` | `true` → user may select more than one credential for this query; `false` → at most one | +| `queryGroups[].availableCredentials` | Wallet credentials that satisfy this query | +| `queryGroups[].missingClaims` | JSON paths the query requires but no wallet credential satisfies | +| `credentialSets[].required` | `true` → user must satisfy **one** option in this section | +| `credentialSets[].options` | Each option is a list of `queryId`s that must **all** be submitted together (AND within option, OR between options) | +| `availableCredentials[].credentialId` | Wallet credential UUID — use in submit `selectedCredentialIds` | +| `availableCredentials[].claims` | Always-disclosed claim paths (shown pre-selected in UI) | +| `availableCredentials[].sdClaims` | Selectively disclosable paths — user must opt in (SD-JWT only) | + +### UI rendering guide + +```text +credentialSets is empty + → Render one slot per queryGroup; all slots are mandatory + → Enable submit only when every queryGroup has a selection + → Respect queryGroup.multiple for multi-select within a slot + +credentialSets is non-empty + → Render one section per credentialSet entry + → required=true → section is mandatory + → Within a section, render one option (tab/radio) per options[] entry + → Option ["voter_id","dl"] → user must fill BOTH slots in that option + → User picks exactly one option per required section +``` + +### Unsatisfiable requests + +If no credential matches a required query, `availableCredentials` is empty and `missingClaims` lists what is missing. The wallet may still let the user **reject** the verifier (see Step 3). + +--- + +## Step 3 — Submit or reject (`PATCH`) + +```http +PATCH /wallets/{walletId}/presentations/{presentationId} +``` + +### Submit — DCQL (`selectedCredentials` as objects) + +```json +{ + "selectedCredentials": [ + { + "queryId": "pid_query", + "selectedCredentialIds": ["vc-uuid-111"] + }, + { + "queryId": "mdl_query", + "selectedCredentialIds": ["vc-uuid-222"] + } + ] +} +``` + +**Rules enforced by Mimoto** + +- Every `selectedCredentialIds` entry must exist in the session cache from GET /credentials. +- `queryId` must match a credential query in the verifier’s DCQL request. +- When `multiple` is `false` for a query, at most **one** credential may be selected for that `queryId` (duplicate selections for the same query are merged and counted). +- When `credential_sets` has a **required** section, the selected `queryId`s must match **exactly one** option in that section. + +### Same credential across multiple queries + +One wallet credential may satisfy several DCQL queries. Submit one selection object per query, repeating the same `credentialId` where needed: + +```json +{ + "selectedCredentials": [ + { + "queryId": "age-proof", + "selectedCredentialIds": ["a3e92fcf-b107-46c9-8d68-f10cdbe8214b"] + }, + { + "queryId": "government-identity", + "selectedCredentialIds": ["a3e92fcf-b107-46c9-8d68-f10cdbe8214b"] + } + ] +} +``` + +### SD-JWT selective disclosure (`selectedSdClaims`) + +For `vc+sd-jwt` and `dc+sd-jwt` credentials, `selectedSdClaims` is a map of **credential ID → array of claim paths**: + +```json +{ + "selectedCredentials": [ + { + "queryId": "age-proof", + "selectedCredentialIds": ["a3e92fcf-b107-46c9-8d68-f10cdbe8214b"], + "selectedSdClaims": { + "a3e92fcf-b107-46c9-8d68-f10cdbe8214b": ["dateOfBirth"] + } + }, + { + "queryId": "government-identity", + "selectedCredentialIds": ["a3e92fcf-b107-46c9-8d68-f10cdbe8214b"], + "selectedSdClaims": { + "a3e92fcf-b107-46c9-8d68-f10cdbe8214b": ["region"] + } + } + ] +} +``` + +**SD-JWT behaviour** + +| Situation | What is shared | +|-----------|----------------| +| `selectedSdClaims` lists paths for a credential | Only those selectively disclosable claims | +| `selectedSdClaims` omitted or empty for an SD-JWT credential | **No** SD disclosures (credential JWT only) | +| Paths from multiple queries for the same credential | **Union** of all paths (deduplicated) | + +Paths may be sent as `dateOfBirth`, `$.dateOfBirth`, or `credentialSubject.dateOfBirth` — Mimoto normalizes them to match stored SD claim keys. + +You may also place `selectedSdClaims` at the **top level** of the submit body; Mimoto unions top-level and per-selection maps. + +### `claim_sets` and auto-resolved disclosures + +When the verifier’s DCQL query defines `claim_sets` on a credential query and the client does **not** send explicit `selectedSdClaims` for that credential, Mimoto may auto-resolve the first satisfiable claim set and include the corresponding SD-JWT paths on submit. + +When the user **does** send `selectedSdClaims`, those explicit paths take precedence for that credential/query combination. + +### Reject verifier + +```json +{ + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" +} +``` + +Do not include `selectedCredentials` or `selectedSdClaims` in a rejection request. + +### Submit / reject responses (HTTP 200) + +**Success** + +```json +{ + "status": "SUCCESS", + "message": "Presentation successfully submitted and shared with verifier", + "redirectUri": "https://verifier.example.com/callback?state=..." +} +``` + +**Share failed** + +```json +{ + "status": "ERROR", + "message": "Failed to share verifiable presentation with verifier", + "redirectUri": null +} +``` + +**Rejection sent** + +```json +{ + "status": "REJECTED_VERIFIER", + "message": "Verifier has been notified of the rejection", + "redirectUri": "https://verifier.example.com/error?error=access_denied" +} +``` + +`redirectUri` is the verifier’s callback URL. The wallet should redirect the user there when present. Mimoto is not the browser’s final destination after a successful share. + +--- + +## End-to-end flow (OVP 1.0) + +```text +Verifier QR / deep link + │ + ▼ +POST /presentations (authorizationRequestUrl) + │ → presentationId, verifier info + ▼ +GET /presentations/{id}/credentials + │ → queryGroups, credentialSets + │ → user selects credentials + SD claims + ▼ +PATCH /presentations/{id} + │ selectedCredentials: [{ queryId, selectedCredentialIds, selectedSdClaims? }] + ▼ +Redirect user to redirectUri (if returned) +``` + +--- + +## Common errors (400) + +| Cause | Typical message | +|-------|-----------------| +| Unknown credential ID on submit | `Selected credential not found in session: …` | +| Unknown `queryId` | `Unknown DCQL query id '…'` | +| Too many credentials for `multiple=false` | `DCQL query '…' has multiple=false but N credential(s) were selected` | +| Invalid `credential_sets` selection | `Credential selection must satisfy exactly one option in credential_set` | +| Mixed `selectedCredentials` array types | `selectedCredentials must be either all credential ID strings or all DCQL selection objects` | + +--- + +## Appendix A: Draft-23 (Presentation Exchange) + +When the verifier uses Presentation Exchange instead of DCQL: + +**GET /credentials** returns: + +```json +{ + "availableCredentials": [ { "credentialId": "...", "format": "ldp_vc", "claims": [], "sdClaims": [] } ], + "missingClaims": [] +} +``` + +**PATCH submit:** + +```json +{ + "selectedCredentials": ["cred-id-1", "cred-id-2"], + "selectedSdClaims": { + "cred-id-1": ["name", "dateOfBirth"] + } +} +``` + +`selectedCredentials` is a **flat array of credential ID strings**, not DCQL objects. + +--- + +## Related resources + +- OpenAPI / Swagger: Mimoto API docs ([`WalletPresentationsController`](../src/main/java/io/mosip/mimoto/controller/WalletPresentationsController.java)) +- Postman: [`postman-collections/MIMOTO.postman_collection.json`](./postman-collections/MIMOTO.postman_collection.json) diff --git a/docs/SVGRenderingIntegrationGuide.md b/docs/SVGRenderingIntegrationGuide.md new file mode 100644 index 000000000..18a310bff --- /dev/null +++ b/docs/SVGRenderingIntegrationGuide.md @@ -0,0 +1,64 @@ +# SVG rendering integration guide + +Mimoto service now supports rendering VCs using an issuer-controlled SVG template. It is W3C Verifiable Credential Rendering specification compliant and currently it is implemented for W3C JSON-LD VC 2.0. + +## Pre-requisites : +- The VC should be a valid W3C JSON-LD VC 2.0 credential. +- In the VC, renderMethod field should be present and renderSuite field in it should be set to `svg-mustache`. Along with it there should be information regarding the hosted template : + ```json + "renderMethod": { + "type": "TemplateRenderMethod", + "renderSuite": "svg-mustache", + "template": { + "id": "https://issuer-host.com/assets/templates/template.svg", + "mediaType": "image/svg+xml", + "digestMultibase": "zQmerWC85Wg6wFl9znFCwYxApG270iEu5h6JqWAPdhyxz2dR" + } + } + ``` + Reference : https://www.w3.org/TR/vc-render-method/ +- The template should be hosted on a publicly accessible URL and should be a valid SVG file. The placeholders in the template should be strictly compliant to JSON Pointer Algorithm as mentioned in the specification. + +## High-level user flow : +1. User downloads the VC either in guest mode or from their wallet. +2. Mimoto service checks for the presence of renderMethod field in the VC and if its renderSuite is `svg-mustache`. +3. inji-vc-renderer library is invoked, it fetches the template from the URL mentioned in template field and verifies the integrity of the template using the digestMultibase value. +4. The library then renders the VC using the fetched template and the data from the VC. The library replaces the placeholders in the template with the corresponding values from the VC. +5. The inji-vc-renderer library returns the rendered SVG to Mimoto service. Mimoto service then invokes the library again to convert the SVG to PDF, and returns the PDF to the user. + + ```mermaid + sequenceDiagram + participant User + participant Mimoto Service + participant inji-vc-renderer + participant Issuer + + User->>Mimoto Service: Download VC + Mimoto Service->>Mimoto Service: Check for VC format, renderMethod field and renderSuite value + alt eligible for SVG rendering + Mimoto Service->>inji-vc-renderer: Send VC data (and QR code data if OnlineSharing type) + inji-vc-renderer->>Issuer: Fetch template from URL + Issuer-->>inji-vc-renderer: Return template + inji-vc-renderer->inji-vc-renderer: Verify integrity of template using digestMultibase + inji-vc-renderer->>inji-vc-renderer: Replace placeholders with VC data + inji-vc-renderer-->>Mimoto Service: Return rendered SVG + Mimoto Service->>inji-vc-renderer: Convert SVG to PDF + inji-vc-renderer-->>Mimoto Service: Return PDF + Mimoto Service-->>User: Send PDF back to user + else not eligible for SVG rendering + Mimoto Service->>Mimoto Service: Handle using existing rendering logic (e.g., using custom HTML templates) + Mimoto Service-->>User: Send rendered credential PDF back to user + end + ``` + +More details about the SVG template rendering implementation can be found in the codebase of inji-vc-renderer library : https://github.com/inji/inji-vc-renderer/blob/master/kotlin/Readme.md + +## Error scenarios : +- If the template cannot be fetched from the URL, an error message is returned to the user indicating that the template could not be retrieved. +- If the integrity of the template cannot be verified using the digestMultibase value, an error message is returned to the user indicating that the template integrity verification failed. + +----------- + +## References : +- [W3C Verifiable Credential Rendering specification](https://www.w3.org/TR/vc-render-method/) +- [inji-vc-renderer Kotlin library](https://github.com/inji/inji-vc-renderer/blob/master/kotlin/Readme.md) diff --git a/docs/UserDataEncryptionWithPinBasedKey.md b/docs/UserDataEncryptionWithPinBasedKey.md new file mode 100644 index 000000000..5bd6c61aa --- /dev/null +++ b/docs/UserDataEncryptionWithPinBasedKey.md @@ -0,0 +1,174 @@ +## Securing User Data in the Mimoto Database with PIN-derived Key Encryption + + +## Overview of Encryption Requirement and Goals + +The encryption mechanism is designed to protect sensitive data stored in the database, such as wallet keys and user metadata. The primary goals are: + +- Ensuring confidentiality of sensitive data. +- Preventing unauthorized access to sensitive information. +- Supporting secure key management and retrieval processes. +- Enabling record-level encryption for granular security. + +## Encryption Algorithm and Mode Used + +- **Algorithm:** AES (Advanced Encryption Standard) +- **Key Size:** 256 bits +- **Mode:** AES-256 GCM + +## Key Management Strategy + +- **Key Generation:** The AES key is generated dynamically during wallet creation. +- **Key Encryption:** The AES key is encrypted using the wallet PIN provided by the user. +- **Key Storage:** The encrypted AES key is stored securely in the database. +- **Key Retrieval:** During wallet unlocking, the wallet PIN is used to decrypt the AES key. + +## Record-Level Encryption Design + +- Each sensitive record (e.g., user signing keys) is encrypted using a unique AES key for a user. +- The AES key itself is encrypted using the wallet PIN, ensuring that only the user with the correct PIN can access the data. +--- +# Wallet Key Lifecycle + +### 1. 🔧 Wallet Creation +The user submits a POST request with a PIN to create a new wallet. + +- The system generates a new 256-bit AES wallet key: +```java +SecretKey aesKey = KeyGenerationUtil.generateEncryptionKey("AES", 256); +``` +- A salt (32 bytes) and IV (12 bytes) are generated randomly. +- The user's PIN is used to derive a key using PBKDF2WithHmacSHA512. +- The AES key is encrypted using AES/GCM/NoPadding and stored as: + + ```Base64(salt + IV + ciphertext)``` +- The original PIN and raw AES key are never stored—only the AES key encrypted with a PIN-derived key is saved. + +### 2. 🔓 **Wallet Unlock (Session Start)** +When the user wants to access the wallet, they re-enter their PIN via an unlock API. + +- The system retrieves the encrypted wallet key. +- Extracts the salt and IV. +- Derives the decryption key from the entered PIN and stored salt. +- The ```decryptWithPin``` method from the ```keyManager``` library handles the decryption and returns the Base64-encoded AES wallet key. +- The Base64-encoded AES key is stored in the HTTP session. +### 3. 🔐 **In-Session Operations** +Once the AES wallet key is in session: +- It is Base64-decoded and used to encrypt or decrypt sensitive data like credentials or keys. +- The user does not need to re-enter the PIN during the session. +- Encryption and decryption continue using AES/GCM/NoPadding for both confidentiality and integrity. +### 4. 🔐 **Session Timeout Configuration** +The property +```properties +server.servlet.session.timeout=30m +``` +in ```application-default.properties``` sets the HTTP session timeout to 30 minutes. +This means that if a user is inactive for 30 minutes, their session will automatically expire, +requiring them to log in again. Adjust this value to control how long user sessions remain active after inactivity. + +### 5. 🔚 **Session End / Logout** + +- The Base64-encoded AES key is removed from the session. +- On the next login, in unlock wallet API, the user must provide their PIN again to decrypt the key. + +--- + +## Flow Diagram + +```mermaid +flowchart TD +%% Wallet Creation Flow +A[Wallet Creation] --> B[Gen AES Wallet Key] +B --> C[Gen Salt & IV] +C --> D[Derive Key
from PIN + Salt] +D --> E[Encrypt AES Key
using Derived Key] +E --> F[Store Encrypted Key
as Base64] +F --> G[DB] + + %% Wallet Unlock Subgraph + subgraph Unlock Flow + H[Wallet Unlock] --> I[Retrieve Encrypted Key
from DB] + I --> J[Extract Salt & IV] + J --> K[Derive Key
from Entered PIN] + K --> L[Receive Base64 AES Key
from decryptWithPin] + L --> M[Store Base64 AES Key
in Session] + end + + %% Session Operations + M --> N[Session Ops:
Base64 Decode Key → Encrypt/Decrypt Data] + N --> O[Session End / Logout] + O --> P[Remove Base64 AES Key
from Session] + P --> H + + %% Session Timeout Node + Q[Session Timeout] --> O + + %% Style classes + classDef stylePrimary fill:#ADD8E6,stroke:#000080,stroke-width:2px,color:#000000; + classDef styleSuccess fill:#90EE90,stroke:#006400,stroke-width:2px,color:#000000; + classDef styleWarning fill:#FFD700,stroke:#B8860B,stroke-width:2px,color:#000000; + + class A,B,C,D,E,F,G,H,I,J,K,L,M,N stylePrimary + class O,P,Q styleWarning +``` +This flow ensures that sensitive user data is securely encrypted and accessible only to the user who knows the PIN. + +## Sequence Diagram: Secure Data Access + +```mermaid +sequenceDiagram + actor User + participant UI as Inji Web UI + participant WC as WalletsController + participant WS as WalletService + participant DPS as DataProtectionService + participant DB as Database + participant KC as MOSIP Kernel
(Cryptomanager) + + Note over User, DB: Wallet Setup + User->>UI: Enter PIN + UI->>WC: POST /wallets + WC->>WS: createWallet + WS->>WS: Generate Wallet Key + WS->>WS: Protect Wallet Key using PIN (derive + encrypt) + WS->>DB: Store encrypted wallet data + WS-->>WC: Wallet Created + WC-->>UI: 200 OK + + Note over User, DB: Wallet Unlock + User->>UI: Enter PIN + UI->>WC: POST /wallets/{id}/unlock + WC->>WS: unlockWallet + WS->>DB: Fetch encrypted wallet key + WS->>WS: Decrypt Wallet Key using PIN + WS-->>WC: Wallet Key + WC->>WC: Store in session + WC-->>UI: 200 OK + + Note over User, DB: Credential Download + UI->>WC: Store Credential + WC->>DPS: encryptCredential + DPS->>DPS: AES-GCM encryption + DPS->>DB: Save encrypted credential + + Note over User, DB: PII Protection + UI->>WC: Save/Update Profile + WC->>DPS: encrypt + DPS->>KC: Request encryption + KC-->>DPS: Encrypted data + DPS->>DB: Save encrypted metadata +``` + +This high-level sequence diagram shows how the UI and backend interact during wallet creation, unlock, and secure data operations. + +## Errors + +Mimoto returns specific error codes if the encryption/decryption process fails: + +| Error Code | HTTP Status | Description | +|----------------------|-------------|--------------------------------------------------------------------------------------------| +| invalid_request | 400 | Validation failure: Missing Wallet ID or User ID or wallet key. invalid 6-digit PIN format. | +| invalid_pin | 400 | Incorrect PIN: Decryption of wallet_key failed; retries still available. | +| unauthorized | 401 | Session expired: User ID not found in session. | +| internal_server_error | 500 | Processing error during decryption or cryptomanager operation. | +| database_unavailable | 503 | Database unavailable: Unable to fetch or update required data. | \ No newline at end of file diff --git a/docs/VCFormatExtension.md b/docs/VCFormatExtension.md new file mode 100644 index 000000000..49e10dd87 --- /dev/null +++ b/docs/VCFormatExtension.md @@ -0,0 +1,113 @@ +# Adding Support for New VC Formats + +## Overview + +Mimoto uses a pluggable handler architecture to support multiple Verifiable Credential formats. Add new formats by implementing the `CredentialFormatHandler` interface without modifying core services. + +### Key Benefits +- ✅ **Zero Core Changes**: Add formats without touching existing services +- ✅ **Auto-Discovery**: Handlers are automatically registered via Spring +- ✅ **Format Agnostic**: Services work seamlessly across all formats + +## Architecture + +### Request Flow +```mermaid +sequenceDiagram + participant User as Client App + participant Mimoto as Mimoto BFF + participant Handler as Format Handler + participant Issuer as Credential Issuer + + + User->>Mimoto: Request Credential (format specified) + Mimoto->>Handler: Select handler by format + Handler->>Mimoto: Build Credential request for format + Mimoto->>Issuer: Request credential + Issuer-->>Mimoto: Return credential + Mimoto->>Handler: Extract claims & display properties + Handler-->>Mimoto: Processed credential data + Mimoto-->>User: Return formatted response +``` + +### Handler Architecture +```mermaid +classDiagram + class CredentialFormatHandler { + <> + +getSupportedFormat() String + +buildCredentialRequest() VCCredentialRequest + +extractCredentialClaims() Map + +loadDisplayPropertiesFromWellknown() LinkedHashMap + } + + class CredentialFormatHandlerFactory { + +getHandler(String format) CredentialFormatHandler + } + + class VcSdJwtCredentialFormatHandler { + +getSupportedFormat() String + +buildCredentialRequest() VCCredentialRequest + +extractCredentialClaims() Map + +loadDisplayPropertiesFromWellknown() LinkedHashMap + } + + class LdpVCCredentialFormatHandler { + +getSupportedFormat() String + +buildCredentialRequest() VCCredentialRequest + +extractCredentialClaims() Map + +loadDisplayPropertiesFromWellknown() LinkedHashMap + } + + CredentialFormatHandlerFactory --> CredentialFormatHandler : manages + CredentialFormatHandler <|.. VcSdJwtCredentialFormatHandler + CredentialFormatHandler <|.. LdpVCCredentialFormatHandler + ``` +## Implementation Guide + +### 1. Study Existing Handlers + +Before starting the implementation, check existing handlers implementation + +- **LDP VC Handler**: [`LdpVCCredentialFormatHandler.java`](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/service/impl/LdpVcCredentialFormatHandler.java) +- **Handler Interface**: [`CredentialFormatHandler.java`](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/service/CredentialFormatHandler.java) + +### 2. Implement New Handler + +Create a new handler class implementing `CredentialFormatHandler` (with a @Component qualifier name required for injection):: + +```java +@Component("vc+sd-jwt") +@Slf4j +public class SDJWTCredentialFormatHandler implements CredentialFormatHandler { + + @Override + public String getSupportedFormat() { + return "vc+sd-jwt"; // Your format identifier + } + + // Implement remaining methods following patterns from existing handlers + // See LdpVCCredentialFormatHandler for JSON-LD format reference +} +``` + +### 3. Handler Registration + +New handler will be auto-discovered by Spring. The factory automatically injects all handlers based on credential format: + +**Reference**: [`CredentialFormatHandlerFactory.java`](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/service/CredentialFormatHandlerFactory.java) + +### 4. Update Models (if needed) + +For format-specific fields, update: +- **Request Model**: [`VCCredentialRequest.java`](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequest.java) +- **Response Model**: [`VCCredentialResponse.java`](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java) + +### 5. Services Using Handlers + +These services automatically work with your new handler: + +| Service | Usage | Reference | +|---------|-------|-----------| +| `CredentialRequestService` | Requests credentials in your format | [Source](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/service/CredentialRequestService.java) | +| `CredentialPDFGeneratorService` | Generates PDFs from your format | [Source](https://github.com/mosip/mimoto/blob/release-0.19.x/src/main/java/io/mosip/mimoto/service/CredentialPDFGeneratorService.java) | diff --git a/docs/VCIssuanceV1SpecificationSupport.md b/docs/VCIssuanceV1SpecificationSupport.md new file mode 100644 index 000000000..c4aae5ca7 --- /dev/null +++ b/docs/VCIssuanceV1SpecificationSupport.md @@ -0,0 +1,192 @@ +# Support for OpenID4VCI Issuance spec 1.0 in INJI Web wallet + +This document outlines the changes expected in the INJI Web wallet to support the VCI Issuance spec 1.0, which is the latest version of the OpenID4VCI specification. The document also highlights the changes in the specification compared to draft 13 and the expected changes in mimoto, which is the reference implementation for OpenID4VCI specification. + +## Key Changes in OpenID4VCI specification 1.0 which impacts wallet implementation: +- `nonce_endpoint` - optional param is introduced in the well-known response. If the field is present in response, wallet has to fetch the credential from the nonce_endpoint during the proof creation and embed it in the proof. If the field is missing, c_nonce claim should not be added in proof JWT sent in the credential request. +- `credential_configurations_supported` -> `display` object is now moved to `credential_configurations_supported` -> `credential_metadata` -> `display` +- Credential request is now format-agnostic, with the following structure : + + ```json + { + "credential_configuration_id": "org.iso.18013.5.1.mDL", + "proofs": { + "jwt": [ + "" + ] + } + } + ``` +- Credential response structure is changed to support multiple credentials in response, with the following structure : + + ```json + { + "credentials": [ + { + "credential": "" + } + ] + } + ``` + +Guidelines for implementation in INJI Web wallet: +- We will be supporting both draft 13 and v1.0 specification +- The existing credential download code will be refactored to be made extensible to support future versions of the specification as well. +- The flow will be designed in such a way that the version detection logic will be implemented at the beginning of the flow and based on the version detected, the respective flow will be triggered. +- One assumption : since INJI Web currently supports sending only single proof object in the credential request, we expect only one credential will be returned in the credential response, even though the structure supports multiple credentials. + +### Changes expected in mimoto : + +1. Issuer configuration fetch logic change, since we want to support both draft 13 and v1.0 VCI specification + - Need to have version detection logic, should be checked in the following sequence in received well-known response : + - If `nonce_endpoint field` exists in the response and is not empty, version will be v1 + - if `credential_configurations_supported` -> `credential_metadata` object exists, version will be v1 + - if `credential_configurations_supported` -> `display`, version will be draft13 + - Default to v1 + - Well-known parser factory has to be introduced, to support parsing of both draft 13 and v1.0 specification + - The well-known response will be parsed using their separate DTOs and validated. + - The final values will be mapped to a common DTO, so that the rest of the flow remains unaffected by the changes in the specification. This will also help in supporting future versions of the specification with minimal changes in the code. +2. VC Download handler to be introduced for each version of the specification, to handle the respective credential request and response structure. The handler will be responsible for : + - Extracting nonce from the nonce_endpoint in case of v1.0 specification and embedding it in the proof JWT sent in credential request. The nonce for draft 13 will be extracted from token response and embedded in the proof JWT as c_nonce. + - Sending the credential request to the issuer + - Handling the credential response based on the respective specification + - Returning the credential to the caller in a standard format(same as draft 13 specification), so that the rest of the flow remains unaffected by the changes in the specification. +3. In both wallet and guest credential download flows, only the first credential from the credentials array will be extracted and used for subsequent operations. + +### Class structure : + +- Factory class to be introduced for version detection and parsing of well-known response based on the version detected. The factory class will be called in the IssuerConfigUtil, which is responsible for fetching and parsing the issuer configuration. +- The IssuerConfigUtil will return a common DTO, which will be used in the rest of the flow. This will help in minimizing the changes in the code and also support future versions of the specification with minimal changes. + +```text + ┌──────────────────────────────────────────────────────────────┐ + │ IssuersServiceImpl │ + │ (High-level orchestration) │ + └─────────────────────────┬────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ IssuerConfigUtil │ + │ ┌───────────────────────────────────────────────────┐ │ + │ │ getIssuerWellknown(url) │ │ + │ │ ├─> VCSpecVersionDetector (defaults to V1) │ │ + │ │ ├─> WellknownParserFactory │ │ + │ │ └─> Return CredentialIssuerWellKnownResponse │ │ + │ └───────────────────────────────────────────────────┘ │ + └─────────────────────────┬────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ + ┌─────────────────┐ ┌──────────────────┐ + │Draft13Wellknown │ │ WellknownParser │ + │ Parser │ │ (V1 - default) │ + └─────────────────┘ └──────────────────┘ +``` + +Interface for well-known response parser : + +```java +public interface WellknownResponseParser { + + /** + * Returns the specification version supported by this parser + */ + VCSpecificationVersion getSupportedVersion(); + + /** + * Parses the wellknown response JSON + */ + CredentialIssuerWellKnownResponse parse(String jsonResponse) throws IOException; + + /** + * Validates the parsed wellknown response + */ + void validate(CredentialIssuerWellKnownResponse response, Validator validator) + throws InvalidWellknownResponseException; +} +``` + + +- VC Download handler class structure : + +```text + + ┌──────────────────────────────────────────────────────────────┐ + │ VCDownloadHandlerFactory │ + │ getHandler(version) → returns handler │ + └─────────────────────────┬────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ + ┌──────────────────────┐ ┌──────────────────────┐ + │ Draft13VC │ │ V1VCDownloadHandler │ + │ DownloadHandler │ │ (V1 - default) │ + │ │ │ │ + |downloadCredential() │ │ downloadCredential() │ + │ • Get cNonce │ │ • Get cNonce │ + │ (from token response)│ │ (from nonce endpoint)│ + │ • Build proof │ │ • Build proof │ + │ • Build request │ │ • Build request │ + │ • POST endpoint │ │ • POST endpoint │ + │ • Parse response │ │ • Parse response │ + └────────┬─────────────┘ └──────────┬───────────┘ + │ │ + └────────────┬────────────────┘ + ▼ + ┌──────────────────────┐ + │ Common Response DTO │ + │ VCCredentialResponse │ + │ - Same as draft 13 │ + │ response with single│ + │ credential │ + │ (normalized array) │ + └──────────────────────┘ +``` + +Interface : + +```java + +public interface VCDownloadHandler { + + /** + * Returns the specification version supported by this handler + */ + VCSpecificationVersion getSupportedVersion(); + + /** + * Downloads credential from issuer and returns normalized response + * + * Responsibilities: + * 1. Retrieve nonce only when required by spec version/issuer metadata + * 2. Build proof JWT and include c_nonce only when nonce is available/required + * 3. Construct version-specific credential request + * 4. POST request to credential endpoint + * 5. Parse response and normalize to common DTO + * + * @param tokenResponse Token response from IdP + * @param credentialConfigurationId Credential configuration ID + * @param credentialConfig Credential configuration from wellknown + * @param wellKnownResponse Issuer wellknown response + * @param issuerDTO Issuer details + * @param walletId Wallet ID for proof binding + * @param base64Key Wallet key for proof signing + * @param isLoginFlow indicates if the user is logged in or not + * @return VCCredentialResponse with single credential in it + */ + VCCredentialResponse downloadCredential( + TokenResponseDTO tokenResponse, + String credentialConfigurationId, + CredentialsSupportedResponse credentialConfig, + CredentialIssuerWellKnownResponse wellKnownResponse, + IssuerDTO issuerDTO, + String walletId, + String base64Key, + boolean isLoginFlow + ) throws Exception; +} +``` + + +### References +[OpenID4VCI 1.0 specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) \ No newline at end of file diff --git a/docs/WalletUnlockProcess.md b/docs/WalletUnlockProcess.md new file mode 100644 index 000000000..8ded38b11 --- /dev/null +++ b/docs/WalletUnlockProcess.md @@ -0,0 +1,168 @@ +# Wallet Unlock Process with max limit on failed attempts + +## Overview + +The Wallet Unlock feature provides a secure mechanism to access a user's encrypted wallet using their PIN. This process includes sophisticated security measures such as temporary and permanent locking mechanisms to prevent brute force attacks, along with PIN-based decryption of wallet key. The unlock flow is triggered when a user needs to access their wallet after session expiry or initial login. + +## Key Features + +- PIN-based wallet unlocking +- Progressive security with temporary and permanent locking +- Configurable failed attempt limits and lock cycles +- Automatic lock expiry for temporary locks +- Session-based wallet key storage +- Last attempt warning before permanent lockout + +## Sequence Diagram + +The sequence diagram below illustrates the complete unlock process, including validation, PIN verification, and lock management steps. + +```mermaid +sequenceDiagram +actor Client +participant WalletsController +participant WalletService +participant WalletUnlockService +participant WalletLockService +participant WalletLockStatusUtils +participant WalletUtil +participant WalletRepository + + Client->>WalletsController: POST /wallets/{walletId}/unlock + activate WalletsController + + WalletsController->>WalletService: unlockWallet(walletId, pin, userId) + activate WalletService + + WalletService->>WalletService: validateUserId & validateWalletPin + WalletService->>WalletRepository: findByUserIdAndId(userId, walletId) + activate WalletRepository + WalletRepository-->>WalletService: Returns Wallet object + deactivate WalletRepository + + WalletService->>WalletUnlockService: handleUnlock(wallet, pin) + activate WalletUnlockService + + WalletUnlockService->>WalletLockService: resetTemporaryLockIfExpired(wallet) + activate WalletLockService + Note over WalletLockService: Checks if Wallet is temporarily locked before and that lock has expired (if retryBlockedUntil in milliseconds from Database > Current System time in milliseconds),
then it resets the failedAttemptCount and retryBlockedUntil to their defaults and sets the walletLockStatus = lock_expired + WalletLockService-->>WalletUnlockService: Returns updated Wallet + deactivate WalletLockService + + WalletUnlockService->>WalletUnlockService: throwExceptionIfWalletIsLocked(wallet) + + WalletUnlockService->>WalletLockStatusUtils: getErrorBasedOnWalletLockStatus(wallet) + activate WalletLockStatusUtils + WalletLockStatusUtils-->>WalletUnlockService: Returns ErrorDTO + deactivate WalletLockStatusUtils + + alt Wallet is locked Temporarily or Permanently + WalletUnlockService-->>WalletService: Throws WalletLockedException + else Wallet is not locked + WalletUnlockService->>WalletUtil: decryptWalletKey(walletKey, pin) + activate WalletUtil + alt Successful PIN Decryption + WalletUtil-->>WalletUnlockService: Returns decryptedWalletKey + + WalletUnlockService->>WalletLockService: resetLockState(wallet) + activate WalletLockService + Note over WalletLockService: Resets all lock counters,
retryBlockedUntil and status to default values + WalletLockService-->>WalletUnlockService: Returns updated Wallet + deactivate WalletLockService + + WalletUnlockService->>WalletRepository: save(wallet) + activate WalletRepository + deactivate WalletRepository + WalletUnlockService-->>WalletService: Returns decryptedWalletKey + else Invalid PIN + WalletUtil-->>WalletUnlockService: Throws InvalidRequestException + deactivate WalletUtil + WalletUnlockService->>WalletUnlockService: handleFailedUnlock(wallet) + + WalletUnlockService->>WalletLockService: enforceLockCyclePolicy(wallet) + activate WalletLockService + Note over WalletLockService:1. Increments failedAttemptCount &
2. if currentCycleCount = 0 (means this is the first failed attempt made in all cycles), it sets currentCycleCount = 1
3. If failedAttemptCount = maxFailedAttemptsAllowedPerCycle then increments currentCycleCount
4. If currentCycleCount > maxLockCyclesAllowed then sets walletLockStatus = permanently_locked & retryBlockedUntil = null
else sets walletLockStatus = temporarily_locked & and retryBlockedUntil = Current System time (in milliseconds) + retryBlockedUntil (in milliseconds) value from config properties file
5. If failedAttemptCount = maxFailedAttemptsAllowedPerCycle - 1 & currentCycleCount = maxLockCyclesAllowed then sets
walletLockStatus = last_attempt_before_lockout + WalletLockService-->>WalletUnlockService: Returns updated Wallet + deactivate WalletLockService + + WalletUnlockService->>WalletUnlockService: throwExceptionIfLastAttemptLeftForUnlock(wallet) + + WalletUnlockService->>WalletLockStatusUtils: getErrorBasedOnWalletLockStatus(wallet) + activate WalletLockStatusUtils + WalletLockStatusUtils-->>WalletUnlockService: Returns ErrorDTO + deactivate WalletLockStatusUtils + + alt if errorCode is last_attempt_before_lockout + WalletUnlockService-->>WalletService: Throws InvalidRequestException + else otherwise + WalletUnlockService->>WalletLockService: resetTemporaryLockIfExpired(wallet) + activate WalletLockService + Note over WalletLockService: Checks if Wallet is temporarily locked before and that lock has expired (if retryBlockedUntil in milliseconds from Database > Current System time in milliseconds),
then it resets the failedAttemptCount and retryBlockedUntil to their defaults and sets the walletLockStatus = lock_expired + WalletLockService-->>WalletUnlockService: Returns updated Wallet + deactivate WalletLockService + + WalletUnlockService->>WalletUnlockService: throwExceptionIfWalletIsLocked(wallet) + WalletUnlockService->>WalletLockStatusUtils: getErrorBasedOnWalletLockStatus(wallet) + activate WalletLockStatusUtils + WalletLockStatusUtils-->>WalletUnlockService: Returns ErrorDTO + deactivate WalletLockStatusUtils + alt Wallet is locked Temporarily or Permanently + WalletUnlockService-->>WalletRepository: save(wallet) + activate WalletRepository + deactivate WalletRepository + WalletUnlockService-->>WalletService: Throws WalletLockedException + else Wallet is not locked + WalletUnlockService-->>WalletRepository: save(wallet) + activate WalletRepository + deactivate WalletRepository + deactivate WalletUnlockService + end + end + end + end + + WalletService-->>WalletsController: Returns WalletResponseDto or throws Exception + deactivate WalletService + + alt Success Response + WalletsController->>WalletsController: Stores wallet_id & received decryptedWalletKey as wallet_key into Http session + WalletsController-->>Client: Returns 200 OK status with WalletResponseDto + else Error Response + WalletsController-->>Client: Returns Error status with ErrorDTO + end + deactivate WalletsController +``` +## Configuration + +Configurable properties that govern the entire Passcode flow within the Wallet unlock process are defined in the +`application-default.properties` file for the local setup, and in the `mimoto-default.properties` file for the environment setup. + +#### Passcode Control Properties +- `wallet.passcode.retryBlockedUntil`: Duration for which the Wallet remains in temporary lock state (in milliseconds) +- `wallet.passcode.maxFailedAttemptsAllowedPerCycle`: Maximum number of failed attempts allowed before triggering a temporary lock cycle +- `wallet.passcode.maxLockCyclesAllowed`: Maximum lock cycles allowed before permanent lockout + +```properties +# Duration (in minutes) for which the wallet remains locked after exceeding failed attempts in a cycle. +# Must be a whole number; decimal/fractional values (e.g., 0.5) will throw an error during Mimoto launch +wallet.passcode.retryBlockedUntil=60 +# Maximum number of failed passcode attempts allowed in a single cycle (defaults to 1 if <=0) +wallet.passcode.maxFailedAttemptsAllowedPerCycle=5 +# Maximum number of lock cycles allowed before the wallet is permanently locked (defaults to 1 if <=0) +wallet.passcode.maxLockCyclesAllowed=3 +``` + +## Errors + +Mimoto uses the following error codes to signal wallet state and failures to the UI: + +| Error Code | HTTP Status | Description | +|-------------------------------|-------------|------------------------------------------------------------------------------------------------------| +| `invalid_request` | 400 | Invalid input such as missing user ID, invalid wallet ID, or invalid PIN format | +| `invalid_pin` | 400 | Incorrect PIN, retry attempts are still available. | +| `last_attempt_before_lockout` | 400 | Indicates the final remaining retry attempt before the wallet transitions to a permanent lock state. | +| `unauthorized` | 401 | User is not authenticated or user ID is missing from the session. | +| `temporarily_locked` | 423 | Maximum attempts reached; wallet locked temporarily until cooldown period expires. | +| `permanently_locked` | 423 | Maximum lock cycles exceeded; wallet is permanently locked and cannot be unlocked using PIN. | +| `internal_server_error` | 500 | Unexpected server-side failure during wallet decryption or retrieval. | +| `database_unavailable` | 503 | Failure to connect to or interact with the database. | \ No newline at end of file diff --git a/docs/api-documentation-openapi.json b/docs/api-documentation-openapi.json new file mode 100644 index 000000000..957cc9fbe --- /dev/null +++ b/docs/api-documentation-openapi.json @@ -0,0 +1,5575 @@ +{ + "openapi": "3.0.1", + "x-stoplight": { + "id": "5bf5a1n68g4tq" + }, + "info": { + "title": "mimoto", + "description": "mimoto", + "license": { + "name": "Mosip", + "url": "https://docs.mosip.io/platform/license" + }, + "version": "1.0" + }, + "servers": [ + { + "url": "https://api.collab.mosip.net/v1/mimoto", + "description": "For Swagger" + } + ], + "paths": { + "/req/otp": { + "post": { + "operationId": "otpRequest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppOTPRequestDTO" + }, + "examples": { + "example-1": { + "value": { + "individualId": "1289358704", + "individualIdType": "UIN", + "otpChannel": [ + "EMAIL", + "PHONE" + ], + "transactionID": "1234567890" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "str": { + "type": "string", + "nullable": true + }, + "responsetime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metadata": { + "type": "object", + "nullable": true + }, + "response": { + "type": "object", + "properties": { + "maskedMobile": { + "type": "string" + }, + "maskedEmail": { + "type": "string" + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + } + } + }, + "nullable": true + } + }, + "x-examples": { + "Example 1": { + "id": "mosip.identity.otp.internal", + "version": "1.0", + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "maskedMobile": "XXXXXX6677", + "maskedEmail": "XXuXXrXX5XX9@gmail.com" + }, + "errors": null + } + } + }, + "examples": { + "example-1": { + "value": { + "id": "mosip.identity.otp.internal", + "version": "1.0", + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "maskedMobile": "XXXXXX6677", + "maskedEmail": "XXuXXrXX5XX9@gmail.com" + }, + "errors": null + } + } + } + } + } + } + }, + "description": "Request new otp", + "tags": [ + "Credential Download - OTP Flow" + ], + "summary": "Generate Credential - Request Otp" + } + }, + "/verifiers": { + "get": { + "operationId": "getAllTrustedVerifiers", + "tags": [ + "Verifiers" + ], + "description": "This API retrieves all trusted verifiers for Inji Wallet.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "verifiers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + }, + "response_uris": { + "type": "array", + "items": { + "type": "string" + } + }, + "allow_unsigned_request": { + "type": "boolean", + "description": "Indicates whether the verifier allows unsigned requests or not.", + "default": false + }, + "spec_version": { + "type": "string", + "enum": ["draft23", "v1"], + "description": "OpenID4VP specification version supported by the Verifier.", + "default": "v1" + } + } + } + } + } + }, + "examples": { + "Success response": { + "value": { + "verifiers": [ + { + "client_id": "https://injiverify.collab.mosip.net", + "redirect_uris": [ + "https://injiverify.collab.mosip.net/redirect" + ], + "response_uris": [ + "https://injiverify.collab.mosip.net/redirect" + ], + "allow_unsigned_request": false, + "spec_version": "v1" + } + ] + } + }, + "Failure response": { + "value": { + "id": null, + "version": "v1", + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "VERIFIER-API-26", + "errorMessage": "API not accessible failure" + } + ] + } + } + } + } + } + } + } + } + }, + "/get-token/{issuer}": { + "post": { + "tags": [ + "Credentials Download" + ], + "summary": "Retrieve accessToken for OIDC flow", + "description": "This endpoint allows you to retrieve the access token in exchange for the authorization code.", + "operationId": "getToken", + "parameters": [ + { + "name": "arg0", + "in": "query", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + { + "name": "issuer", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "str": { + "type": "string", + "nullable": true + }, + "responsetime": { + "type": "string", + "format": "date-time" + }, + "metadata": { + "type": "object", + "nullable": true + }, + "response": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "token_type": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "refresh_token": { + "type": "string", + "nullable": true + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + } + } + }, + "nullable": true + } + } + }, + "examples": { + "Success response": { + "value": { + "id": "mosip.mimoto.issuers", + "version": "v1", + "str": null, + "responsetime": "2024-04-25T05:56:55.890Z", + "metadata": null, + "response": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer", + "expires_in": 3600, + "scope": "mosip_identity_vc_ldp", + "refresh_token": "xyz123" + }, + "errors": [] + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "str": { + "type": "string", + "nullable": true + }, + "responsetime": { + "type": "string", + "format": "date-time" + }, + "metadata": { + "type": "object", + "nullable": true + }, + "response": { + "type": "object", + "nullable": true + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + } + } + } + } + } + }, + "examples": { + "Failure response": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-26", + "errorMessage": "Api not accessible failure" + } + ] + } + } + } + } + } + } + } + } + }, + "/credentialshare/request": { + "post": { + "operationId": "request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "transactionID": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "x-examples": { + "Example 1": { + "individualId": "2057135194", + "otp": "111111", + "transactionID": "1234567890", + "user": "mono" + } + } + }, + "examples": { + "example-1": { + "value": { + "individualId": "2057135194", + "otp": "111111", + "transactionID": "1234567890", + "user": "mono" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "str": { + "type": "string", + "nullable": true + }, + "responsetime": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metadata": { + "type": "object", + "nullable": true + }, + "response": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + } + } + }, + "nullable": true + } + }, + "x-examples": { + "Example 1": { + "id": "mosip.resident.vid", + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:08:14.846Z", + "metadata": null, + "response": { + "id": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257" + }, + "errors": null + } + } + }, + "examples": { + "Success response": { + "value": { + "id": "mosip.resident.vid", + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:08:14.846Z", + "metadata": null, + "response": { + "id": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257" + }, + "errors": null + } + }, + "Failure response": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RES-SER-422", + "errorMessage": "Input transactionID does not match transactionID of OTP Request" + } + ] + } + } + } + } + } + } + }, + "description": "Credential request", + "tags": [ + "Credential Download - OTP Flow" + ], + "summary": "Generate Credential - request new credential" + } + }, + "/credentialshare/request/status/{requestId}": { + "get": { + "operationId": "requestStatus", + "parameters": [ + { + "name": "requestId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "statusCode": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": {} + } + } + } + } + }, + "x-examples": { + "Example 1": { + "response": { + "id": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257", + "statusCode": "ISSUED" + }, + "errors": [] + } + } + }, + "examples": { + "success response": { + "value": { + "response": { + "id": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257", + "statusCode": "ISSUED" + }, + "errors": [] + } + } + } + } + } + } + }, + "description": "Credential request status", + "tags": [ + "Credential Download - OTP Flow" + ], + "summary": "Generate Credential - Check Status of Credential Share" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "requestId", + "in": "path", + "required": true, + "description": "The credential request id" + } + ] + }, + "/credentialshare/download": { + "post": { + "operationId": "download", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialDownloadResponseDTO" + }, + "examples": { + "success response": { + "value": { + "credential": { + "biometrics": { + "face": "" + }, + "province": [ + { + "language": "eng", + "value": "Kenitra" + }, + { + "language": "kan", + "value": "ಕೆನಿತ್ರಾ" + } + ], + "phone": "4567897654", + "city": [ + { + "language": "eng", + "value": "Kenitra" + }, + { + "language": "kan", + "value": "ಕೆನಿತ್ರಾ" + } + ], + "postalCode": "14022", + "fullName": [ + { + "language": "eng", + "value": "kimgḻbb" + }, + { + "language": "kan", + "value": "ಕಿಮ್ಗ್ೞ್ಬ್ಬ್" + } + ], + "region": [ + { + "language": "eng", + "value": "Rabat Sale Kenitra" + }, + { + "language": "kan", + "value": "ರಬತ್ ಸಲೆ ಕೆನಿತ್ರ" + } + ] + }, + "verifiableCredential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "http://localhost:8088/.well-known/mosip-context.json", + { + "sec": "https://w3id.org/security#" + } + ], + "credentialSubject": { + "biometrics": "", + "province": [ + { + "language": "eng", + "value": "Kenitra" + }, + { + "language": "kan", + "value": "ಕೆನಿತ್ರಾ" + } + ], + "city": [ + { + "language": "eng", + "value": "Kenitra" + }, + { + "language": "kan", + "value": "ಕೆನಿತ್ರಾ" + } + ], + "phone": "4567897654", + "postalCode": "14022", + "fullName": [ + { + "language": "eng", + "value": "kimgḻbb" + }, + { + "language": "kan", + "value": "ಕಿಮ್ಗ್ೞ್ಬ್ಬ್" + } + ], + "dateOfBirth": "1990/01/01", + "id": "http://credential.idrepo/credentials/2057135194", + "vcVer": "VC-V1", + "UIN": "2057135194", + "region": [ + { + "language": "eng", + "value": "Rabat Sale Kenitra" + }, + { + "language": "kan", + "value": "ರಬತ್ ಸಲೆ ಕೆನಿತ್ರ" + } + ], + "email": "santhosdss14@gmail.com" + }, + "id": "http://credential.idrepo/credentials/a3afcf8c-e5a6-4782-9ce5-84740ec2f332", + "issuanceDate": "2022-10-31T05:08:26.980Z", + "issuer": "http://localhost:8088/.well-known/controller.json", + "proof": { + "type": "RsaSignature2018", + "created": "2022-10-31T05:08:26Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "http://localhost:8088/.well-known/public-key.json", + "jws": "" + }, + "type": [ + "VerifiableCredential", + "MOSIPVerifiableCredential" + ] + } + } + } + } + } + } + } + }, + "description": "download VC", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "requestId": { + "type": "string" + } + }, + "x-examples": { + "Example 1": { + "individualId": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257" + } + } + }, + "examples": { + "example-1": { + "value": { + "individualId": "2057135194", + "requestId": "2bc3bb7f-6156-46d1-ae03-bf7b76e0c257" + } + } + } + } + } + }, + "tags": [ + "Credential Download - OTP Flow" + ], + "summary": "Generate Credential - Download Credential" + } + }, + "/credentialshare/callback/notify": { + "post": { + "operationId": "handleSubscribeEvent", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericResponseDTO" + } + } + } + } + }, + "description": "callback uri used by websub to notify", + "summary": "Callback API - Notify when credential is ready", + "tags": [ + "Credential Download - OTP Flow" + ], + "x-internal": true + } + }, + "/binding-otp": { + "post": { + "operationId": "bindingOtpGenerate", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "x-examples": { + "Example 1": { + "individualId": "2057135194", + "individualIdType": "UIN", + "otp": "111111", + "vidType": "PERPETUAL", + "transactionID": "1234567890" + } + }, + "properties": { + "requestTime": { + "type": "string" + }, + "request": { + "type": "object", + "properties": { + "individualId": { + "type": "string", + "description": "The individual id. UIN or VID." + }, + "otpChannels": { + "type": "array", + "description": "Otp channels using which user will be notified, for example - EMAIL, PHONE", + "items": {} + } + } + } + } + }, + "examples": { + "Wallet binding otp request example": { + "value": { + "requestTime": "{{$isoTimestamp}}", + "request": { + "individualId": "3816083254", + "otpChannels": [ + "EMAIL" + ] + } + } + } + } + }, + "application/xml": { + "schema": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "individualIdType": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "vidType": { + "type": "string" + }, + "transactionID": { + "type": "string" + } + }, + "x-examples": { + "Example 1": { + "individualId": "2057135194", + "individualIdType": "UIN", + "otp": "111111", + "vidType": "PERPETUAL", + "transactionID": "1234567890" + } + } + }, + "examples": { + "example-1": { + "value": "{\n \"individualId\": \"2057135194\",\n \"individualIdType\": \"UIN\",\n \"otp\": \"111111\",\n \"vidType\": \"PERPETUAL\",\n \"transactionID\": \"1234567890\"\n}" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "requestTime": { + "type": "string" + }, + "request": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "otpChannels": { + "type": "array", + "items": {} + } + } + } + } + } + } + }, + "required": true, + "description": "" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "x-examples": { + "Example 1": { + "id": null, + "version": null, + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "maskedEmail": "mock", + "maskedMobile": "mock" + }, + "errors": [] + } + }, + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "responsetime": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "response": { + "type": "object", + "properties": { + "maskedEmail": { + "type": "string", + "description": "The masked email of user" + }, + "maskedMobile": { + "type": "string", + "description": "the masked mobile of user." + } + } + } + } + }, + "examples": { + "Success response": { + "value": { + "id": null, + "version": null, + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "maskedEmail": "mono@mono.com", + "maskedMobile": "7897897890" + }, + "errors": [] + } + }, + "Failure response": { + "value": { + "id": "mosip.resident.vid", + "version": "v1", + "str": null, + "responsetime": "2022-10-31T04:57:10.792Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RES-SER-422", + "errorMessage": "Invalid UIN" + } + ] + } + } + } + } + } + } + }, + "summary": "Wallet Binding - Generate Otp", + "tags": [ + "Wallet Binding" + ], + "description": "This api is used to generate otp for wallet binding.\nThis api is used as a proxy to call [Identity Provider - Send Binding OTP Endpoint](https://mosip.stoplight.io/docs/identity-provider/a34e5371d4876-send-binding-otp-endpoint-draft) api internally." + } + }, + "/wallet-binding": { + "post": { + "operationId": "walletBinding", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "x-examples": { + "Example 1": { + "requestTime": "{{$isoTimestamp}}", + "request": { + "authFactorType": "WLA", + "format": "jwt", + "individualId": "3816083254", + "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAxo8hdrLl6nXyPSeSJu8xgmBBwVutacZS57rmIlHeKJfjYhKLT51v\n0k2IQqN2v6A3HOZqzKXh41p3RrxdSndkqqHwS0BLEVPNpKqYoEbe0tUICDpDSFaa\n7w87Z/OSGrw8bKpxmUT0xZ70+DGPhnWlMvPV5xenG1bVtf/vEq7cwQei1iNbNQRb\nqw/57e9LgmXdmbh/v5XOllT7rlUFmVFm4V4J3u6NH9iOPGlIKTplBlYyxlx6ayaR\n09Z6z4RJICG1MIipCv5qJwuzzXXpBja6gEG4xsN5K4W6b5GLt3IuzMMaQmReseRR\n46s53vBp7aqq1wrjs/CZ1SZqZ5ZG/nUchjTv+Nir0vPR9acvPB5j5qDgfdVz2sT/\nYL1HoeFPOIgcY1ydz+tUBm9F4AMCMZa7GbEmtAIWvxlQp6emo50/opdb51B3Teh3\neVQ+JqYYhOpuTEv0uHfhonXEsNfpGyttjR0FSAH2c6mI6UULi/PfLgcvKuqkhE+7\nlB0SFd852C993GqsY7qfxW1GvJpXYRvo6oRoFO8sj+St36MkmzJbm56AQn/9KWd5\nRLh13xrIUVRnW48y/Blchw/C68Ez1bmCyDeVe1WBPwel0zPl62jCtKawzCQiZcTL\n/ejmlKFHKQd980MwWN18OVakKU52CkO0NT+9ovgi0TRfLKvG6PQ1qGECAwEAAQ==\n-----END RSA PUBLIC KEY-----\n", + "challengeList": [ + { + "authFactorType": "OTP", + "challenge": "111111", + "format": "alpha-numeric" + } + ] + } + } + }, + "properties": { + "requestTime": { + "type": "string", + "description": "The request time" + }, + "request": { + "type": "object", + "properties": { + "authFactorType": { + "type": "string", + "description": "Auth factor type to be binded for the provided key." + }, + "format": { + "type": "string", + "description": "Format of the auth factor type supported in the wallet app. Right now Inji supports only jwt." + }, + "individualId": { + "type": "string", + "description": "The individual id. UIN or VID.\n" + }, + "publicKey": { + "type": "string", + "description": "The public key of the wallet app." + }, + "challengeList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "authFactorType": { + "type": "string", + "description": "Defines the type of auth challenge. It should be same as authfactor.type (oauth-details response)." + }, + "challenge": { + "type": "string", + "description": "Actual challenge as string. A jwt in case of inji." + }, + "format": { + "type": "string", + "description": "Format of the challenge provided." + } + } + } + } + } + } + } + }, + "examples": { + "Wallet binding otp request example": { + "value": { + "requestTime": "2023-01-17T05:38:41.869Z", + "request": { + "authFactorType": "WLA", + "format": "jwt", + "individualId": "3816083254", + "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAxo8hdrLl6nXyPSeSJu8xgmBBwVutacZS57rmIlHeKJfjYhKLT51v\n0k2IQqN2v6A3HOZqzKXh41p3RrxdSndkqqHwS0BLEVPNpKqYoEbe0tUICDpDSFaa\n7w87Z/OSGrw8bKpxmUT0xZ70+DGPhnWlMvPV5xenG1bVtf/vEq7cwQei1iNbNQRb\nqw/57e9LgmXdmbh/v5XOllT7rlUFmVFm4V4J3u6NH9iOPGlIKTplBlYyxlx6ayaR\n09Z6z4RJICG1MIipCv5qJwuzzXXpBja6gEG4xsN5K4W6b5GLt3IuzMMaQmReseRR\n46s53vBp7aqq1wrjs/CZ1SZqZ5ZG/nUchjTv+Nir0vPR9acvPB5j5qDgfdVz2sT/\nYL1HoeFPOIgcY1ydz+tUBm9F4AMCMZa7GbEmtAIWvxlQp6emo50/opdb51B3Teh3\neVQ+JqYYhOpuTEv0uHfhonXEsNfpGyttjR0FSAH2c6mI6UULi/PfLgcvKuqkhE+7\nlB0SFd852C993GqsY7qfxW1GvJpXYRvo6oRoFO8sj+St36MkmzJbm56AQn/9KWd5\nRLh13xrIUVRnW48y/Blchw/C68Ez1bmCyDeVe1WBPwel0zPl62jCtKawzCQiZcTL\n/ejmlKFHKQd980MwWN18OVakKU52CkO0NT+9ovgi0TRfLKvG6PQ1qGECAwEAAQ==\n-----END RSA PUBLIC KEY-----\n", + "challengeList": [ + { + "authFactorType": "OTP", + "challenge": "111111", + "format": "alpha-numeric" + } + ] + } + } + } + } + }, + "application/xml": { + "schema": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "individualIdType": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "vidType": { + "type": "string" + }, + "transactionID": { + "type": "string" + } + }, + "x-examples": { + "Example 1": { + "individualId": "2057135194", + "individualIdType": "UIN", + "otp": "111111", + "vidType": "PERPETUAL", + "transactionID": "1234567890" + } + } + }, + "examples": { + "example-1": { + "value": "{\n \"individualId\": \"2057135194\",\n \"individualIdType\": \"UIN\",\n \"otp\": \"111111\",\n \"vidType\": \"PERPETUAL\",\n \"transactionID\": \"1234567890\"\n}" + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "requestTime": { + "type": "string" + }, + "request": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "otpChannels": { + "type": "array", + "items": {} + } + } + } + } + } + } + }, + "required": true, + "description": "" + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "x-examples": { + "Example 1": { + "id": null, + "version": null, + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDqjCCApKgAwIBAgIGAYW+PCLUMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMT\nCE1vY2stSURBMB4XDTIzMDExNzA1MzgxMFoXDTIzMDEyNzA1MzgxMFowGTEXMBUG\nA1UEAxMOTW9jay11c2VyLW5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQDGjyF2suXqdfI9J5Im7zGCYEHBW61pxlLnuuYiUd4ol+NiEotPnW/STYhC\no3a/oDcc5mrMpeHjWndGvF1Kd2SqofBLQEsRU82kqpigRt7S1QgIOkNIVprvDztn\n85IavDxsqnGZRPTFnvT4MY+GdaUy89XnF6cbVtW1/+8SrtzBB6LWI1s1BFurD/nt\n70uCZd2ZuH+/lc6WVPuuVQWZUWbhXgne7o0f2I48aUgpOmUGVjLGXHprJpHT1nrP\nhEkgIbUwiKkK/monC7PNdekGNrqAQbjGw3krhbpvkYu3ci7MwxpCZF6x5FHjqzne\n8GntqqrXCuOz8JnVJmpnlkb+dRyGNO/42KvS89H1py88HmPmoOB91XPaxP9gvUeh\n4U84iBxjXJ3P61QGb0XgAwIxlrsZsSa0Aha/GVCnp6ajnT+il1vnUHdN6Hd5VD4m\nphiE6m5MS/S4d+GidcSw1+kbK22NHQVIAfZzqYjpRQuL898uBy8q6qSET7uUHRIV\n3znYL33caqxjup/FbUa8mldhG+jqhGgU7yyP5K3foySbMlubnoBCf/0pZ3lEuHXf\nGshRVGdbjzL8GVyHD8LrwTPVuYLIN5V7VYE/B6XTM+XraMK0prDMJCJlxMv96OaU\noUcpB33zQzBY3Xw5VqQpTnYKQ7Q1P72i+CLRNF8sq8bo9DWoYQIDAQABMA0GCSqG\nSIb3DQEBCwUAA4IBAQAmbgQcW22AnrP/GH5lzt+Fy6t20lxqpMDIc3rCO6dzXMCR\n9ocjwLj7E243jGFbhHzC8637qOC8b5bYLZL2ND9ZehSu0qdz/0U0Kwt81rj/lihk\nvhD6xgx8g966AMZcZ0iQncyTzbdEukq6jMAtYfWvAu41rL0H5qvIFvxLHGOoxOYV\n0XZslWTC8B8TB3hzQ+HHexxSZbLSbB+sHFShubOlqqxE/zMj2O+c2FGYSOKlbTXi\nTd4m1WY/eJ4+0XjvEAAMZqLSLtnF5U1POz2onnk0u/lzsRnpxiqJY2++1WmCnuAT\nwDIwNEeAPotfm6tM1TjWuwakYrbSY7Ru0YjaFC1S\n-----END CERTIFICATE-----\n", + "encryptedWalletBindingId": "qeCUuKEjRDUiDHfbYAsHv5L4RCiHe_KE7eKKjpw7jIo", + "expireDateTime": "2023-01-27T05:38:10.000Z", + "thumbprint": "rOe3ifBa0uIuKfArURKRKK5psE8YVdWxXbQoDXAvntk", + "keyId": "1673933890260" + }, + "errors": [] + } + }, + "properties": { + "version": { + "type": "string", + "nullable": true + }, + "id": { + "type": "string", + "nullable": true + }, + "responsetime": { + "type": "string", + "nullable": true + }, + "metadata": { + "type": "string", + "nullable": true + }, + "response": { + "type": "object", + "properties": { + "certificate": { + "type": "string", + "description": "Server certificate after successful binding. This certificate will be stored in wallet app." + }, + "encryptedWalletBindingId": { + "type": "string", + "description": "the wallet binding id in encrypted format. The wallet app will associate the requested credential with this id." + }, + "expireDateTime": { + "type": "string", + "description": "expiry date time of the binding." + }, + "thumbprint": { + "type": "string", + "description": "the certificate thumbprint." + }, + "keyId": { + "type": "string", + "description": "the certificate key id." + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "examples": { + "Success response": { + "value": { + "id": null, + "version": null, + "str": null, + "responsetime": null, + "metadata": null, + "response": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDqjCCApKgAwIBAgIGAYW+PCLUMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMT\nCE1vY2stSURBMB4XDTIzMDExNzA1MzgxMFoXDTIzMDEyNzA1MzgxMFowGTEXMBUG\nA1UEAxMOTW9jay11c2VyLW5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\nAoICAQDGjyF2suXqdfI9J5Im7zGCYEHBW61pxlLnuuYiUd4ol+NiEotPnW/STYhC\no3a/oDcc5mrMpeHjWndGvF1Kd2SqofBLQEsRU82kqpigRt7S1QgIOkNIVprvDztn\n85IavDxsqnGZRPTFnvT4MY+GdaUy89XnF6cbVtW1/+8SrtzBB6LWI1s1BFurD/nt\n70uCZd2ZuH+/lc6WVPuuVQWZUWbhXgne7o0f2I48aUgpOmUGVjLGXHprJpHT1nrP\nhEkgIbUwiKkK/monC7PNdekGNrqAQbjGw3krhbpvkYu3ci7MwxpCZF6x5FHjqzne\n8GntqqrXCuOz8JnVJmpnlkb+dRyGNO/42KvS89H1py88HmPmoOB91XPaxP9gvUeh\n4U84iBxjXJ3P61QGb0XgAwIxlrsZsSa0Aha/GVCnp6ajnT+il1vnUHdN6Hd5VD4m\nphiE6m5MS/S4d+GidcSw1+kbK22NHQVIAfZzqYjpRQuL898uBy8q6qSET7uUHRIV\n3znYL33caqxjup/FbUa8mldhG+jqhGgU7yyP5K3foySbMlubnoBCf/0pZ3lEuHXf\nGshRVGdbjzL8GVyHD8LrwTPVuYLIN5V7VYE/B6XTM+XraMK0prDMJCJlxMv96OaU\noUcpB33zQzBY3Xw5VqQpTnYKQ7Q1P72i+CLRNF8sq8bo9DWoYQIDAQABMA0GCSqG\nSIb3DQEBCwUAA4IBAQAmbgQcW22AnrP/GH5lzt+Fy6t20lxqpMDIc3rCO6dzXMCR\n9ocjwLj7E243jGFbhHzC8637qOC8b5bYLZL2ND9ZehSu0qdz/0U0Kwt81rj/lihk\nvhD6xgx8g966AMZcZ0iQncyTzbdEukq6jMAtYfWvAu41rL0H5qvIFvxLHGOoxOYV\n0XZslWTC8B8TB3hzQ+HHexxSZbLSbB+sHFShubOlqqxE/zMj2O+c2FGYSOKlbTXi\nTd4m1WY/eJ4+0XjvEAAMZqLSLtnF5U1POz2onnk0u/lzsRnpxiqJY2++1WmCnuAT\nwDIwNEeAPotfm6tM1TjWuwakYrbSY7Ru0YjaFC1S\n-----END CERTIFICATE-----\n", + "encryptedWalletBindingId": "qeCUuKEjRDUiDHfbYAsHv5L4RCiHe_KE7eKKjpw7jIo", + "expireDateTime": "2023-01-27T05:38:10.000Z", + "thumbprint": "rOe3ifBa0uIuKfArURKRKK5psE8YVdWxXbQoDXAvntk", + "keyId": "1673933890260" + }, + "errors": [] + } + } + } + } + } + } + }, + "summary": "Wallet Binding - Bind Credential with wallet", + "tags": [ + "Wallet Binding" + ], + "description": "This api is used to bind the credential with wallet.\nThis api is used as a proxy to call [Identity Provider - Wallet binding Endpoint](https://mosip.stoplight.io/docs/identity-provider/6e91115b16311-wallet-binding-endpoint) api internally." + } + }, + "/allProperties": { + "get": { + "operationId": "getAllProperties", + "responses": { + "200": { + "description": "Successfully retrieved all configurable properties for Inji.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "errors": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "examples": { + "example-1": { + "value": { + "response": { + "internal.auth.types.allowed": "otp,bio-Finger,bio-Iris,bio-Face", + "auth.types.allowed": "demo,otp,bio-Finger,bio-Iris,bio-Face", + "ekyc.auth.types.allowed": "demo,otp,bio-Finger,bio-Iris,bio-Face" + }, + "errors": [] + } + } + } + } + } + } + }, + "description": "Get all configurable properties for Inji.", + "summary": "Get all Inji app properties", + "tags": [ + "Inji Config" + ] + } + }, + "/issuers": { + "get": { + "operationId": "getAllIssuers", + "parameters": [ + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Trusted Issuers" + ], + "description": "This API provides data with search capability to populate the list of supported issuers in Inji Web, which is then displayed under the List of Issuers", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "issuers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "issuer_id": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "display": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "logo": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "alt_text": { + "type": "string" + } + } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "language": { + "type": "string" + } + } + } + }, + "client_id": { + "type": "string" + }, + "wellknown_endpoint": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "authorization_audience": { + "type": "string" + }, + "token_endpoint": { + "type": "string" + }, + "proxy_token_endpoint": { + "type": "string" + }, + "client_alias": { + "type": "string" + }, + "qr_code_type": { + "type": "string", + "enum": [ + "OnlineSharing", + "EmbeddedVC", + "None" + ] + }, + "enabled": { + "type": "string" + }, + "credential_issuer": { + "type": "string" + }, + "credential_issuer_host": { + "type": "string" + } + } + } + } + } + }, + "x-examples": { + "Example 1": { + "id": "mosip.resident.vid", + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:08:14.846Z", + "metadata": null, + "response": { + "issuers": [ + { + "issuer_id": "MOSIPInsurance", + "credential_issuer": "MOSIPInsurance", + "credential_issuer_host": "https://injicertify-insurance.collab.mosip.net", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "MOSIP Insurance", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "MOSIP-Logo" + }, + "title": "Download via LIC", + "description": "This API provides data to populate the list of supported issuers in Inji Web, which is then displayed under the List of Issuers section on the Home page of Inji Web.", + "language": "en" + } + ], + "client_id": "3yz7-j3xRzU3SODdoNgSGvO_cD8UijH3AIWRDAg1x-M", + "wellknown_endpoint": "http://localhost:8088/.well-known/openid-credential-issuer" + }, + { + "issuer_id": "MOSIPNationalID", + "credential_issuer": "MOSIPNationalID", + "credential_issuer_host": "https://injicertify-mosipid.released.mosip.net", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "MOSIP National ID", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "MOSIP-Logo" + }, + "title": "Download via LIC", + "description": "Enter your policy number to download your card.", + "language": "en" + } + ], + "client_id": "3yz7-j3xRzU3SODdoNgSGvO_cD8UijH3AIWRDAg1x-M", + "wellknown_endpoint": "http://localhost:8088/.well-known/openid-credential-issuer" + } + ] + }, + "errors": null + } + }, + "examples": { + "Success response": { + "value": { + "id": "mosip.resident.vid", + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:08:14.846Z", + "metadata": null, + "response": { + "issuers": [ + { + "issuer_id": "MOSIPInsurance", + "credential_issuer": "MOSIPInsurance", + "credential_issuer_host": "https://injicertify-insurance.collab.mosip.net", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "MOSIP Insurance", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "MOSIP-Logo" + }, + "title": "Download via LIC", + "description": "Enter your policy number to download your card.", + "language": "en" + } + ], + "client_id": "3yz7-j3xRzU3SODdoNgSGvO_cD8UijH3AIWRDAg1x-M", + ".well-known": "http://localhost:8088/.well-known/openid-credential-issuer" + }, + { + "issuer_id": "MOSIPNationalID", + "credential_issuer": "MOSIPNationalID", + "credential_issuer_host": "https://injicertify-mosipid.released.mosip.net", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "MOSIP National ID", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "MOSIP-Logo" + }, + "title": "Download via LIC", + "description": "Enter your policy number to download your card.", + "language": "en" + } + ], + "client_id": "3yz7-j3xRzU3SODdoNgSGvO_cD8UijH3AIWRDAg1x-M", + ".well-known": "http://localhost:8088/.well-known/openid-credential-issuer" + } + ] + } + } + }, + "Failure response": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-26", + "errorMessage": "Api not accessible failure" + } + ] + } + } + } + } + } + } + } + } + }, + "/issuers/{issuer-id}": { + "get": { + "operationId": "getIssuerConfig", + "parameters": [ + { + "name": "issuer-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Trusted Issuers" + ], + "description": "This API provides the complete configuration details for the specific issuers passed in the path variable", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "object", + "properties": { + "issuer_id": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "display": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "logo": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "alt_text": { + "type": "string" + } + } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "language": { + "type": "string" + } + } + } + }, + "client_id": { + "type": "string" + }, + "wellknown_endpoint": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "authorization_audience": { + "type": "string" + }, + "token_endpoint": { + "type": "string" + }, + "proxy_token_endpoint": { + "type": "string" + }, + "client_alias": { + "type": "string" + }, + "qr_code_type": { + "type": "string", + "enum": [ + "OnlineSharing", + "EmbeddedVC", + "None" + ] + }, + "enabled": { + "type": "boolean" + }, + "credential_issuer": { + "type": "string" + }, + "credential_issuer_host": { + "type": "string" + } + } + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "x-examples": { + "Example 1": { + "response": { + "issuer_id": "ESignet", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "mosip-logo" + }, + "title": "Download MOSIP Credentials", + "description": "Download credentials by providing UIN or VID", + "language": "en" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "شعار موسيب" + }, + "title": "قم بتنزيل بيانات اعتماد MOSIP", + "description": "توفير UIN أو VIDقم بتنزيل بيانات الاعتماد عن طريق", + "language": "ar" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "मोसिप लोगो" + }, + "title": "MOSIP क्रेडेंशियल डाउनलोड करेंं", + "description": "यूआईएन या वीआईडी प्रदान करके क्रेडेंशियल डाउनलोड करें", + "language": "hi" + } + ], + "client_id": "*****", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "scopes_supported": [ + "mosip_identity_vc_ldp" + ], + "authorization_endpoint": "https://esignet.collab.mosip.net/authorize", + "authorization_audience": "https://esignet.collab.mosip.net/v1/esignet/oauth/v2/token", + "token_endpoint": "https://api.collab.mosip.net/v1/mimoto/get-token/ESignet", + "proxy_token_endpoint": "https://esignet.collab.mosip.net/v1/esignet/oauth/v2/token", + "credential_endpoint": "https://esignet.collab.mosip.net/v1/esignet/vci/credential", + "credential_type": [ + "VerifiableCredential", + "MOSIPVerifiableCredential" + ], + "credential_audience": "https://esignet.collab.mosip.net", + "client_alias": "mpartner-default-mimotooidc", + "additional_headers": { + "Accept": "application/json" + }, + "wellknown_endpoint": "https://esignet.collab.mosip.net/.well-known/openid-credential-issuer" + }, + "errors": [] + }, + "errors": null + }, + "examples": { + "Success response": { + "value": { + "id": "mosip.mimoto.issuers", + "version": "v1", + "str": null, + "responsetime": "2024-04-25T05:56:55.890Z", + "metadata": null, + "response": { + "issuer_id": "mosip.mimoto.issuers", + "protocol": "OpenId4VCI", + "display": [ + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "mosip-logo" + }, + "title": "Download MOSIP Credentials", + "description": "Download credentials by providing UIN or VID", + "language": "en" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "شعار موسيب" + }, + "title": "قم بتنزيل بيانات اعتماد MOSIP", + "description": "توفير UIN أو VIDقم بتنزيل بيانات الاعتماد عن طريق", + "language": "ar" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "मोसिप लोगो" + }, + "title": "MOSIP क्रेडेंशियल डाउनलोड करेंं", + "description": "यूआईएन या वीआईडी प्रदान करके क्रेडेंशियल डाउनलोड करें", + "language": "hi" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "mosip ಲೋಗೋ" + }, + "title": "MOSIP ರುಜುವಾತುಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ", + "description": "UIN ಅಥವಾ VID ಒದಗಿಸುವ ಮೂಲಕ ರುಜುವಾತುಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ", + "language": "kn" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "mosip லோகோ" + }, + "title": "MOSIP சான்றுகளைப் பதிவிறக்கவும்", + "description": "UIN அல்லது VIDஐ வழங்குவதன் மூலம் நற்சான்றிதழ்களைப் பதிவிறக்கவும்", + "language": "ta" + }, + { + "name": "e-Signet", + "logo": { + "url": "https://api.collab.mosip.net/inji/mosip-logo.png", + "alt_text": "logo ng mosip" + }, + "title": "I-download ang Mga Kredensyal ng MOSIP", + "description": "Mag-download ng mga kredensyal sa pamamagitan ng pagbibigay ng UIN o VID", + "language": "fil" + } + ], + "client_id": "DEqVWfdKe9cQWikLdjak3vlDF0Pq7jtnwTcGdEXoT1I", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "authorization_audience": "https://esignet.collab.mosip.net/v1/esignet/oauth/v2/token", + "token_endpoint": "https://api.collab.mosip.net/v1/mimoto/get-token/ESignet", + "proxy_token_endpoint": "https://esignet.collab.mosip.net/v1/esignet/oauth/v2/token", + "credential_endpoint": "https://esignet.collab.mosip.net/v1/esignet/vci/credential", + "credential_type": [ + "VerifiableCredential", + "MOSIPVerifiableCredential" + ], + "credential_audience": "https://esignet.collab.mosip.net", + "client_alias": "mpartner-default-mimotooidc", + "additional_headers": { + "Accept": "application/json" + }, + "wellknown_endpoint": "https://esignet.collab.mosip.net/.well-known/openid-credential-issuer" + }, + "errors": [] + } + }, + "Failure response": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-26", + "errorMessage": "Api not accessible failure" + } + ] + } + } + } + } + } + } + } + } + }, + "/v2/issuers": { + "get": { + "operationId": "getAllIssuersV2", + "parameters": [], + "tags": [ + "Trusted Issuers V2" + ], + "summary": "Retrieve all onboarded issuers (V2)", + "description": "Retrieve all onboarded issuers in V2 response format. Returns minimal issuer data from configuration (issuer_id, client_id, credential_issuer_host, qr_code_type, token_endpoint, protocol, display, etc.) without fetching well-known endpoints.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string", "nullable": true }, + "version": { "type": "string", "nullable": true }, + "responsetime": { "type": "string", "format": "date-time", "nullable": true }, + "metadata": { "type": "object", "nullable": true }, + "response": { + "type": "object", + "properties": { + "issuers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IssuerV2DTO" + } + } + } + }, + "errors": { "type": "array", "items": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" } } }, "nullable": true } + } + } + } + } + }, + "400": { + "description": "Bad Request - API not accessible or configuration error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { "type": "object", "nullable": true }, + "errors": { "type": "array", "items": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" } } } } + } + } + } + } + } + } + } + }, + "/v2/issuers/{issuer-id}": { + "get": { + "operationId": "getIssuerByIdV2", + "parameters": [ + { + "name": "issuer-id", + "in": "path", + "required": true, + "schema": { "type": "string" }, + "description": "Unique identifier of the issuer" + } + ], + "tags": [ + "Trusted Issuers V2" + ], + "summary": "Retrieve specific issuer (V2)", + "description": "Retrieve a single issuer in V2 response format by issuer ID. Returns configuration-only data.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { "type": "string", "nullable": true }, + "version": { "type": "string", "nullable": true }, + "responsetime": { "type": "string", "format": "date-time", "nullable": true }, + "metadata": { "type": "object", "nullable": true }, + "response": { "$ref": "#/components/schemas/IssuerV2DTO" }, + "errors": { "type": "array", "items": { "type": "object" }, "nullable": true } + } + } + } + } + }, + "404": { + "description": "Not Found - Invalid issuer ID", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { "type": "object", "nullable": true }, + "errors": { "type": "array", "items": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" } } } } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { "type": "object", "nullable": true }, + "errors": { "type": "array" } + } + } + } + } + } + } + } + }, + "/issuers/{issuer-id}/well-known-proxy": { + "get": { + "operationId": "getCredentialWellknown", + "parameters": [ + { + "name": "issuer-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Trusted Issuers" + ], + "deprecated": true, + "description": "This API fetches and allows searching for Credential Types offered by the issuer, all sourced from the issuer's well-known configuration. Since version 0.16.0, this endpoint is deprecated and will be removed in a future release. Use issuers new endpoint issuers/{issuer-id}/configuration instead.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "credential_issuer": { + "type": "string" + }, + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "credential_endpoint": { + "type": "string" + }, + "credential_configurations_supported": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "format": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "doctype": { + "type": "string" + }, + "proof_types_supported": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "proof_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "claims": { + "type": "object" + }, + "credential_definition": { + "type": "object", + "properties": { + "type": { + "type": "array", + "items": { + "type": "string" + } + }, + "credentialSubject": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "display": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "locale": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "display": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "logo": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "alt_text": { + "type": "string" + } + } + }, + "background_image": { + "type": "object", + "properties": { + "uri": { + "type": "string" + } + } + }, + "background_color": { + "type": "string" + }, + "text_color": { + "type": "string" + } + } + } + }, + "order": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "examples": { + "Success response": { + "value": { + "credential_issuer": "https://injicertify-insurance.dev1.mosip.net", + "authorization_servers": [ + "https://esignet-insurance.dev1.mosip.net" + ], + "credential_endpoint": "https://injicertify-insurance.dev1.mosip.net/v1/certify/issuance/credential", + "credential_configurations_supported": { + "InsuranceCredential": { + "format": "ldp_vc", + "scope": "sunbird_rc_insurance_vc_ldp", + "order": [ + "fullName", + "policyName", + "policyExpiresOn", + "policyIssuedOn", + "policyNumber", + "mobile", + "dob", + "gender", + "benefits", + "email" + ], + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "PS256" + ] + } + }, + "credential_definition": { + "type": [ + "VerifiableCredential", + "InsuranceCredential" + ], + "credentialSubject": { + "fullName": { + "display": [ + { + "name": "Name", + "locale": "en" + } + ] + }, + "mobile": { + "display": [ + { + "name": "Phone Number", + "locale": "en" + } + ] + }, + "dob": { + "display": [ + { + "name": "Date of Birth", + "locale": "en" + } + ] + }, + "gender": { + "display": [ + { + "name": "Gender", + "locale": "en" + } + ] + }, + "benefits": { + "display": [ + { + "name": "Benefits", + "locale": "en" + } + ] + }, + "email": { + "display": [ + { + "name": "Email Id", + "locale": "en" + } + ] + }, + "policyIssuedOn": { + "display": [ + { + "name": "Policy Issued On", + "locale": "en" + } + ] + }, + "policyExpiresOn": { + "display": [ + { + "name": "Policy Expires On", + "locale": "en" + } + ] + }, + "policyName": { + "display": [ + { + "name": "Policy Name", + "locale": "en" + } + ] + }, + "policyNumber": { + "display": [ + { + "name": "Policy Number", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "Health Insurance", + "locale": "en", + "logo": { + "url": "https://raw.githubusercontent.com/tw-mosip/file-server/master/StayProtectedInsurance.png", + "alt_text": "a square logo of a Veridonia" + }, + "background_image": { + "uri": "https://api.dev1.mosip.net/inji/veridonia-logo.png" + }, + "background_color": "#FDFAF9", + "text_color": "#7C4616" + } + ] + }, + "LifeInsuranceCredential": { + "format": "ldp_vc", + "scope": "life_insurance_vc_ldp", + "order": [ + "fullName", + "policyName", + "policyExpiresOn", + "policyIssuedOn", + "policyNumber", + "mobile", + "dob", + "gender", + "benefits", + "email" + ], + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "credential_definition": { + "type": [ + "VerifiableCredential", + "LifeInsuranceCredential" + ], + "credentialSubject": { + "fullName": { + "display": [ + { + "name": "Name", + "locale": "en" + } + ] + }, + "mobile": { + "display": [ + { + "name": "Phone Number", + "locale": "en" + } + ] + }, + "dob": { + "display": [ + { + "name": "Date of Birth", + "locale": "en" + } + ] + }, + "gender": { + "display": [ + { + "name": "Gender", + "locale": "en" + } + ] + }, + "benefits": { + "display": [ + { + "name": "Benefits", + "locale": "en" + } + ] + }, + "email": { + "display": [ + { + "name": "Email Id", + "locale": "en" + } + ] + }, + "policyIssuedOn": { + "display": [ + { + "name": "Policy Issued On", + "locale": "en" + } + ] + }, + "policyExpiresOn": { + "display": [ + { + "name": "Policy Expires On", + "locale": "en" + } + ] + }, + "policyName": { + "display": [ + { + "name": "Policy Name", + "locale": "en" + } + ] + }, + "policyNumber": { + "display": [ + { + "name": "Policy Number", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "Life Insurance", + "locale": "en", + "logo": { + "url": "https://raw.githubusercontent.com/tw-mosip/file-server/master/StayProtectedInsurance.png", + "alt_text": "a square logo of a Veridonia" + }, + "background_image": { + "uri": "https://api.dev1.mosip.net/inji/veridonia-logo.png" + }, + "background_color": "#FDFAF9", + "text_color": "#7C4616" + } + ] + } + } + } + }, + "Failure response": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-026", + "errorMessage": "Api not accessible failure" + } + ] + } + }, + "Failure response 2": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-035", + "errorMessage": "Invalid issuer ID" + } + ] + } + } + } + } + } + } + }, + "summary": "Get Issuers Wellknown" + } + }, + "/issuers/{issuer-id}/configuration": { + "get": { + "operationId": "getCredentialIssuerConfiguration", + "parameters": [ + { + "name": "issuer-id", + "in": "path", + "required": true, + "description": "The unique identifier of the credential issuer.", + "schema": { + "type": "string" + } + } + ], + "tags": [ + "Trusted Issuers" + ], + "description": "Retrieve the required configuration details for a specific issuer and its authorization server from their respective well-known endpoints.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssuerConfigurationSuccessResponse" + }, + "examples": { + "SuccessResponse": { + "summary": "Issuer Configuration Response", + "value": { + "response": { + "credentials_supported": [ + { + "name": "InsuranceCredential", + "scope": "sunbird_rc_insurance_vc_ldp", + "display": [ + { + "name": "Health Insurance", + "locale": "en", + "logo": "https://raw.githubusercontent.com/tw-mosip/file-server/master/StayProtectedInsurance.png" + } + ] + } + ], + "authorization_endpoint": "https://esignet-insurance.collab.mosip.net/authorize", + "grant_types_supported": [ + "authorization_code" + ] + }, + "errors": [] + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssuerConfigurationErrorResponse" + }, + "examples": { + "FailureResponse1": { + "summary": "Api not accessible failure", + "value": { + "errors": [ + { + "errorCode": "RESIDENT-APP-026", + "errorMessage": "Api not accessible failure" + } + ], + "response": null + } + }, + "FailureResponse2": { + "summary": "Invalid issuer ID", + "value": { + "errors": [ + { + "errorCode": "RESIDENT-APP-035", + "errorMessage": "Invalid issuer ID" + } + ], + "response": null + } + }, + "FailureResponse3": { + "summary": "Invalid Authorization Server well-known endpoint", + "value": { + "errors": [ + { + "errorCode": "RESIDENT-APP-042", + "errorMessage": "Invalid Authorization Server well-known from server:\nAuthorization Server host url cannot be null" + } + ], + "response": null + } + } + } + } + } + } + } + } + }, + "/credentials/download": { + "post": { + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "grant_type": { + "type": "string", + "example": "authorization_code" + }, + "code": { + "type": "string", + "example": "GL9N1V-authorizationcodeyBkoaonjplms" + }, + "redirect_uri": { + "type": "string", + "example": "https://injiweb.collab.mosip.net/redirect" + }, + "code_verifier": { + "type": "string", + "example": "Sq6nZNToevcodeVerifierjXw70LCJ" + }, + "issuer": { + "type": "string", + "example": "Mosip" + }, + "credential": { + "type": "string", + "example": "MosipVerifiableCredential" + }, + "vcStorageExpiryLimitInTimes": { + "type": "string", + "example": "1" + }, + "locale": { + "type": "string", + "example": "en" + } + } + } + } + }, + "required": true + }, + "tags": [ + "Credentials Download" + ], + "description": "This API is responsible for generating PDFs for the received VC content. It fetches display properties from the well-known configuration of the issuer and incorporates them into the predefined template of the PDF file.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "BAD_REQUEST", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + }, + "examples": { + "Failure response 1": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-036", + "errorMessage": "Invalid Credential Type Id" + } + ] + } + }, + "Failure response 2": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-026", + "errorMessage": "Api not accessible failure" + } + ] + } + }, + "Failure response 3": { + "value": { + "id": null, + "version": "v1", + "str": null, + "responsetime": "2022-10-31T05:07:34.789Z", + "metadata": null, + "response": null, + "errors": [ + { + "errorCode": "RESIDENT-APP-028", + "errorMessage": "error occured while signing pdf" + } + ] + } + } + } + } + } + } + }, + "summary": "Download Credential As PDF", + "x-stoplight": { + "id": "42bjevdfa0stx" + } + } + }, + "/authorize": { + "get": { + "summary": "Authorize Verifiable Presentation", + "tags": [ + "Authorize the Verifiable Presentation" + ], + "responses": { + "302": { + "description": "Found\n\n**Success Case :**\n\n```json\n\n{redirectUri}#vp_token={base64_encoded_vpToken}&presentation_submission={url_encoded_presentation_submission}\n\n```\n**Error Case :** \n\n```json\n\n{redirectUri}?error={error_codes}&error_messages={error_messages}\n\n```\n\n\n**Possible Error Codes and Error Messages :**\n\n- **invalid_request** - \"Some incorrect parameters in the request\"\n- **unsupported_format** - \"No VC of this format is found\"\n- **resource_not_found** - \"The requested resource doesn’t exist.\"\n- **server_unavailable** - \"The server is not reachable right now.\"\n- **resource_expired** - \"The requested resource expired.\"\n- **resource_invalid** - \"The requested resource is invalid.\"\n- **request_timed_out** - \"We are unable to process your request right now\"\n- **uri_too_long** - \"Resource URI is too long to be handled\"\n- **invalid_client** - \"The requested client doesn’t match.\"\n- **invalid_redirect_uri** - \"The requested redirect URI doesn’t match.\"\n- **internal_server_error** - \"We are unable to process request now\"\n\n" + } + }, + "operationId": "get-v1-mimoto-authorize", + "x-stoplight": { + "id": "x4k8ruhin2f09" + }, + "parameters": [ + { + "schema": { + "type": "string", + "default": "vp_token" + }, + "in": "query", + "name": "response_type", + "description": "Response Type of the authorization" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "resource", + "description": "Credential Resource url" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "presentation_definition", + "description": "URL Encoded Value of the presentation Definition" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "client_id", + "description": "Client ID of the registered verifier" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "redirect_uri", + "description": "One of the Redirect uri of the registered verifiers" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "state", + "description": "Unique session identifier to prevent CSRF" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "nonce", + "description": "Cryptographic challenge used to bind the presentation to this request" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "response_uri", + "description": "Where to send the VP token" + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "response_mode", + "description": "Specifies how the Verifier will receive the Authorization Response from the Wallet" + } + ] + } + }, + "/users/me": { + "get": { + "tags": [ + "Users" + ], + "summary": "Retrieve user metadata", + "description": "First attempts to retrieve user metadata from the session cache. If not available, fetches from the database. This API is secured using session-based authentication.", + "operationId": "getUserProfile", + "security": [ + { + "SessionAuth": [] + } + ], + "responses": { + "200": { + "description": "User profile retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserMetadataDTO" + }, + "examples": { + "Success response from DB": { + "value": { + "displayName": "John Doe", + "profilePictureUrl": "https://example.com/profile.jpg", + "email": "john.doe@example.com" + } + }, + "Success response from cache": { + "value": { + "displayName": "John Doe", + "profilePictureUrl": "john.doe@example.com", + "email": "john.doe@example.com", + "walletId": "123e4567-e89b-12d3-a456-426614174000" + } + } + } + } + } + }, + "401": { + "description": "User data not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User data not available in DB" : { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User not found. Please check your credentials or login again" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + }, + "Decryption Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "Failed to process user data" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + } + }, + "/wallets/{walletId}/credentials": { + "post": { + "tags": [ + "Wallet Credentials" + ], + "summary": "Download and store a Verifiable Credential under a specific Wallet", + "description": "This API is secured using session-based authentication. Upon receiving a request, the session ID is extracted from the Cookie header and used to retrieve session details from Redis for authentication. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. It then retrieves the wallet key from the session and use it to decrypt the signing algorithm's secret key(which is used for signing the JWT in credential request) and encrypt the downloaded Verifiable Credential. If the process completes successfully, the credential is stored in the database and certain fields will be returned in the response. In case of any issues, an appropriate error response is returned.", + "operationId": "downloadCredential", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the user's wallet where the credential will be stored" + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + }, + { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string", + "default": "en" + }, + "description": "Locale is used to determine the language in which credential's issuer display details should be stored. Input should be a 2-letter code (e.g., 'en' for English, 'fr' for French). If not provided, defaults to 'en'." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifiableCredentialRequestDTO" + }, + "examples": { + "Default download": { + "value": { + "issuer": "Mosip", + "credentialConfigurationId": "MosipVerifiableCredential", + "code": "abc123", + "grant_type": "authorization_code", + "redirectUri": "https://example.com/redirect", + "codeVerifier": "xyz-secret-verifier" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Verifiable Credential downloaded and stored successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifiableCredentialResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "issuerDisplayName": "Mosip", + "issuerLogo": "https://example.com/logo.png", + "credentialTypeDisplayName": "National Identity Department Mosip", + "credentialTypeLogo": "https://example.com/credential-logo.png", + "credentialId": "1234567890" + } + } + } + } + } + }, + "400": { + "description": "Bad request - Wallet key is null / blank or Wallet ID is null / blank / mismatch with session Wallet ID or required params are missing / invalid or any invalid request data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Wallet ID is null or blank": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet ID cannot be blank" + } + }, + "Invalid Accept-Language header": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Locale must be a 2-letter code" + } + }, + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID not found in session": { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + }, + "Wallet key not found in session" : { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet key not found in session" + } + }, + "Invalid issuer": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "issuerId cannot be blank" + } + }, + "Invalid credentialConfigurationId": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "credentialConfigurationId cannot be blank" + } + }, + "Invalid code": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "code cannot be blank" + } + }, + "Invalid grantType": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "grantType cannot be blank" + } + }, + "Invalid redirectUri": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "redirectUri cannot be blank" + } + }, + "Invalid codeVerifier": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "codeVerifier cannot be blank" + } + }, + "Credential already exists for given Issuer and Credential Type in the wallet": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Duplicate credential for issuer and type" + } + } + } + } + } + }, + "500": { + "description": "Internal server error - along with below examples it will return this status code when error occurred while serializing the VC response or unable to store the response in data share or unable to encrypt the credential", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Exception occurred while fetching Issuer config": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Unable to fetch issuer configuration" + } + }, + "Failed to generate VC Credential request": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Unable to generate credential request" + } + }, + "Signature verification failed for downloaded Credential": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + }, + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable - along with below examples it will return this status code when any error occurred while fetching Issuer wellknown or Auth Server wellknown", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "API is not accessible": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Failed to download and store the credential" + } + }, + "Failed to download credential from Issuer": { + "value": { + "errorCode": "server_unavailable", + "errorMessage": "Unable to download credential from issuer" + } + }, + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + }, + "get": { + "tags": [ + "Wallet Credentials" + ], + "summary": "Fetch all credentials for the given wallet", + "description": "This API is secured using session-based authentication. When a request is made, the session ID is extracted from the Cookie header and used to fetch session details from Redis for authentication. It then retrieves the wallet key from the session and uses it to decrypt all stored Verifiable Credentials for the given wallet. If successful, it returns a list of credentials otherwise an appropriate error is returned.", + "operationId": "fetchAllCredentialsForWallet", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the user's wallet from where the credential will be fetched" + }, + { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language preference in which the user expects the Verifiable Credential to be rendered. Input should be a 2-letter code (e.g., 'en' for English, 'fr' for French). If not provided, defaults to 'en'." + } + ], + "responses": { + "200": { + "description": "Credentials retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerifiableCredentialResponseDTO" + } + }, + "examples": { + "Success response": { + "value": [ + { + "issuerDisplayName": "Mosip", + "issuerLogo": "https://example.com/logo.png", + "credentialTypeDisplayName": "National Identity Department Mosip", + "credentialTypeLogo": "https://example.com/credential-logo.png", + "credentialId": "1234567890" + } + ] + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid Wallet Id": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID is not available in session" : { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + }, + "Wallet key not found in session": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet key not found in session" + } + }, + "Invalid Accept-Language header": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Locale must be a 2-letter code" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + } + }, + "/wallets/{walletId}/credentials/{credentialId}": { + "get": { + "tags": [ + "Wallet Credentials" + ], + "summary": "Fetch a Verifiable Credential by ID to either preview it or download it based on the requirement", + "description": "This API is protected using session-based authentication. When a request is received, the session ID is extracted from the Cookie header and used to fetch session data from Redis for user authentication. Upon successful authentication, the wallet key is retrieved from the session and used to decrypt the credential data obtained from the database.\n\nThe API retrieves a specific Verifiable Credential based on the provided wallet ID and credential ID. Depending on the action query parameter (inline or download) received in the request, the Content-Disposition header in the response is adjusted to either display the credential in the browser or prompt a file download. On success, the API returns the credential as a PDF byte stream. In case of any errors, an appropriate error response is returned.", + "operationId": "fetchVerifiableCredentialById", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the user's wallet" + }, + { + "name": "credentialId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the Verifiable Credential to be retrieved" + }, + { + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "inline" + }, + "description": "Controls how the credential should be displayed. If it is sent as download, the response will include a header to download the file. For any other value or if not provided, it defaults to inline to preview the file in the browser." + }, + { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string", + "default": "en" + }, + "description": "Language preference in which the user expects the Verifiable Credential to be rendered. Input should be a 2-letter code (e.g., 'en' for English, 'fr' for French). If not provided, defaults to 'en'." + }, + { + "name": "Accept", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "application/pdf" + }, + "description": "The Accept header must be set to application/pdf to indicate that the response should be in PDF format. If not provided or set to a different value, the API will return a 400 Bad Request error." + } + ], + "responses": { + "200": { + "description": "Verifiable Credential fetched successfully", + "headers": { + "Content-Disposition": { + "description": "Indicates if the verifiable credential is displayed inline or downloaded", + "schema": { + "type": "string", + "example": "inline; filename=\"credential.pdf\"" + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + }, + "examples": { + "Success response": { + "value": "PDF content in Binary Format" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid Wallet Id": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID is not available in session" : { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + }, + "Wallet key not found in session": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet key not found in session" + } + }, + "Invalid Accept-Language header": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Locale must be a 2-letter code" + } + }, + "Invalid Accept header" : { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Accept header must be application/pdf" + } + } + } + } + } + }, + "404": { + "description": "Not Found - Credential not found for given Wallet ID and Credential ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "No credentials found for the given Credential ID and Wallet ID": { + "value": { + "errorCode": "resource_not_found", + "errorMessage": "The requested resource doesn’t exist." + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Failed to decrypt the Credential": { + "value": { + "errorCode": "credential_fetch_error", + "errorMessage": "Decryption failed" + } + }, + "Credential Type is invalid": { + "value": { + "errorCode": "credential_fetch_error", + "errorMessage": "Invalid credential type configuration" + } + }, + "Exception occurred while fetching Issuer config": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Unable to fetch issuer configuration" + } + }, + "Exception occurred when generating PDF": { + "value": { + "errorCode": "credential_download_error", + "errorMessage": "Failed to generate credential PDF" + } + }, + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Wallet Credentials" + ], + "summary": "Delete a credential from a wallet", + "description": "This endpoint allows you to delete a specific credential from a wallet. The API is secured using session-based authentication. The session ID is extracted from the Cookie header to authenticate the user. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. The wallet ID is validated against the session to ensure the user has access to the wallet. If the credential is successfully deleted, a 200 OK response is returned.", + "operationId": "deleteCredential", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the wallet containing the credential" + }, + { + "name": "credentialId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the credential to be deleted" + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "responses": { + "200": { + "description": "Credential successfully deleted" + }, + "400": { + "description": "Provided request is invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid Wallet Id": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID is not available in session" : { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + } + } + } + } + }, + "404": { + "description": "Credential not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Credential not found": { + "value": { + "errorCode": "resource_not_found", + "errorMessage": "The requested resource doesn’t exist." + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + } + } + } + }, + "/wallets": { + "post": { + "tags": [ + "Wallets" + ], + "summary": "Create a new wallet", + "description": "This API is secured using session-based authentication. The session ID is extracted from the Cookie header and used to retrieve session details from Redis for authentication. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. The user ID is then obtained from the session and along with the provided wallet name and PIN is used to create a new wallet in the database. If the wallet is created successfully, response including unique identifier of the wallet and and the provided name is returned otherwise an appropriate error response is provided.", + "operationId": "createWallet", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "requestBody": { + "required": true, + "description": "Wallet creation request containing the wallet name and wallet PIN", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWalletRequestDto" + }, + "examples": { + "Create wallet request": { + "value": { + "walletPin": "1234", + "confirmWalletPin": "1234", + "walletName": "My Personal Wallet" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Wallet created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "walletId": "123e4567-e89b-12d3-a456-426614174000", + "walletName": "My Personal Wallet" + } + } + } + } + } + }, + "400": { + "description": "Invalid wallet creation request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid User ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "User ID cannot be null or empty" + } + }, + "Invalid Wallet name": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet name must be alphanumeric with allowed special characters" + } + }, + "Invalid Wallet PIN": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "PIN must be numeric with 6 digits" + } + }, + "Entered Pin and Confirm Pin Mismatch": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Entered PIN and Confirm PIN do not match" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user creating a wallet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error while creating Wallet - error occurred while generating Wallet key or encrypting it with Wallet PIN", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + }, + "get": { + "tags": [ + "Wallets" + ], + "summary": "Retrieve all wallets for the user", + "description": "This API is secured using session-based authentication. Upon receiving a request, the session is first retrieved using the session ID extracted from the Cookie header to authenticate the user. Once authenticated, the user's ID is obtained from the session stored in Redis. Using this user ID, the API fetches all wallets associated with the user from the database and returns them. If an error occurs while retrieving the wallets, an appropriate error response is returned.", + "operationId": "getWallets", + "security": [ + { + "SessionAuth": [] + } + ], + "responses": { + "200": { + "description": "List of wallets retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WalletDetailsResponseDto" + } + }, + "examples": { + "Success response": { + "value": [ + { + "walletId": "123e4567-e89b-12d3-a456-426614174000", + "walletName": "My Personal Wallet", + "walletStatus": null + }, + { + "walletId": "223e4567-e89b-12d3-a456-426614174001", + "walletName": "Wallet Main", + "walletStatus": "temporarily_locked" + } + ] + } + } + } + } + }, + "400": { + "description": "Invalid Wallets fetching request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid User ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "User ID cannot be null or empty" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user fetching wallets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + } + }, + "/wallets/{walletId}": { + "delete": { + "tags": [ + "Wallets" + ], + "summary": "Delete a Wallet", + "description": "This endpoint allows you to delete a specific wallet. The API is secured using session-based authentication. The session ID is extracted from the Cookie header to authenticate the user. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. The user's ID is obtained from the session and used to validate ownership of the Wallet before deletion. If the Wallet is successfully deleted, a 200 OK response is returned; otherwise, an appropriate error response is returned.", + "operationId": "deleteWallet", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the Wallet to be deleted" + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "responses": { + "200": { + "description": "Wallet successfully deleted" + }, + "401": { + "description": "Unauthorized user deleting a Wallet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID not found in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "400": { + "description": "Invalid wallet deletion request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid User ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "User ID cannot be null or empty" + } + }, + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID not found in session": { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + }, + "Wallet not found in database": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet not found" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + } + } + } + }, + "/wallets/{walletId}/unlock": { + "post": { + "tags": [ + "Wallets" + ], + "summary": "Unlock an existing Wallet", + "description": "This API is secured using session-based authentication. Upon receiving a request, the session is first retrieved using the session ID extracted from the Cookie header to authenticate the user. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. Once authenticated, the user's ID is obtained from the session stored in Redis. Using this user ID along with the provided wallet ID and PIN, the corresponding wallet key is fetched and stored in the session. If successful then the API returns a response containing the wallet ID otherwise an appropriate error response is returned.", + "operationId": "unlockWallet", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the wallet to be unlocked", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnlockWalletRequestDto" + }, + "examples": { + "Unlock wallet request": { + "value": { + "walletPin": "1234" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Wallet unlocked successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletResponseDto" + }, + "examples": { + "Success response": { + "value": { + "walletId": "123e4567-e89b-12d3-a456-426614174000", + "walletName": "My Personal Wallet" + } + } + } + } + } + }, + "400": { + "description": "Invalid Wallet unlock request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Wallet not found": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Wallet not found" + } + }, + "Invalid User ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "User ID cannot be null or empty" + } + }, + "Invalid Wallet PIN": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "PIN must be numeric with 6 digits" + } + }, + "Incorrect Wallet PIN": { + "value": { + "errorCode": "invalid_pin", + "errorMessage": "Invalid PIN or wallet key provided" + } + }, + "Only one attempt left to unlock Wallet before Permanent Lockout": { + "value": { + "errorCode": "last_attempt_before_lockout", + "errorMessage": "Incorrect passcode. Last attempt remaining before your Wallet is permanently locked" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user unlocking wallet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "423": { + "description": "Wallet is locked temporarily or permanently", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Wallet is locked Temporarily": { + "value": { + "errorCode": "temporarily_locked", + "errorMessage": "You’ve reached the maximum number of attempts. Your wallet is now temporarily locked" + } + }, + "Wallet is locked Permanently": { + "value": { + "errorCode": "permanently_locked", + "errorMessage": "Your wallet has been permanently locked due to multiple failed attempts. Please click on forgot password to reset your wallet to continue" + } + } + } + } + } + }, + "500": { + "description": "Internal server error - any error occurred while decrypting the Wallet key with Wallet PIN or when fetching Wallet details from the database", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Database connection failure": { + "value": { + "errorCode": "database_unavailable", + "errorMessage": "Failed to connect to the database" + } + } + } + } + } + } + } + } + }, + "/wallets/{walletId}/presentations": { + "post": { + "tags": [ + "Wallet Presentations" + ], + "summary": "Processes Verifiable Presentation Authorization Request and provides details about the verifier and presentation.", + "description": "This API is secured using session-based authentication. Upon receiving a request, the session is first retrieved using the session ID extracted from the Cookie header to authenticate the user. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. Once authenticated, the API processes the received Verifiable Presentation Authorization Request from the Verifier for a specific wallet. It validates the session, verifies the authenticity of the request, and checks if the Verifier is pre-registered and trusted by the wallet. If all validations pass, the API returns a response containing the presentation details; otherwise, an appropriate error response is returned.", + "operationId": "processVPAuthorizationRequest", + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the Wallet." + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifiablePresentationAuthorizationRequest" + }, + "examples": { + "Verifier Verifiable Presentation Authorization Request": { + "value": { + "authorizationRequestUrl": "client_id=mock-client&presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully processed the Verifiable Presentation Authorization Request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifiablePresentationResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "presentationId": "123e4567-e89b-12d3-a456-426614174000", + "verifier": { + "id": "mock-client", + "name": "Requester name", + "logo": "https://api.collab.mosip.net/inji/verifier-logo.png", + "isTrusted": true, + "isPreregisteredWithWallet": true, + "redirectUri": "https://injiverify.collab.mosip.net/redirect" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request or missing required parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "response_type is missing in Authorization request": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Missing Input: response_type param is required" + } + }, + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID not found in session": { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user performing the Verifiable Presentation flow", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Failed to fetch pre-registered trusted verifiers": { + "value": { + "errorCode": "RESIDENT-APP-026", + "errorMessage": "Api not accessible failure" + } + }, + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + } + }, + "security": [ + { + "SessionAuth": [] + } + ] + } + }, + "/wallets/{walletId}/presentations/{presentationId}": { + "patch": { + "tags": [ + "Wallet Presentations" + ], + "summary": "Handles user submission or rejection of a presentation", + "description": "This API allows users to accept or reject a presentation request by selecting specific credentials. For SD-JWT credentials (vc+sd-jwt and dc+sd-jwt), the user may optionally include selectedSdClaims to selectively disclose the chosen claims. Only the explicitly selected disclosures are shared; when omitted (or empty) for an SD-JWT credential, none of its selectively-disclosable claims are shared. The API is secured using session-based authentication. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests. Upon acceptance, the system fetches the selected credentials, constructs and signs a VP token, shares it with the verifier, and stores the presentation record in the database.", + "operationId": "handlePresentationSubmission", + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the Wallet." + }, + { + "name": "presentationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the Presentation." + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { "$ref": "#/components/schemas/PresentationSubmissionRequestDTO" }, + { "$ref": "#/components/schemas/PresentationRejectionRequestDTO" } + ] + }, + "examples": { + "Presentation Submission Request": { + "value": { + "selectedCredentials": ["cred-123", "cred-456"] + } + }, + "Presentation Submission Request (SD-JWT with selective disclosure)": { + "value": { + "selectedCredentials": ["cred-123"], + "selectedSdClaims": { + "cred-123": ["name", "dob"] + } + } + }, + "Rejection payload": { + "value": { + "errorCode": "access_denied", + "errorMessage": "User denied authorization to share credentials" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully processed the presentation submission.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitPresentationResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "status": "success", + "redirectUri": "https://verifier.example.com/callback?state=af0ifjsldkj", + "message": "Presentation successfully submitted" + } + }, + "Response when successfully rejected": { + "value": { + "status": "success", + "redirectUri": "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee", + "message": "Presentation request rejected. An OpenID4VP error response has been sent to the verifier." + } + }, + "Response when an error occurs while submitting a rejection": { + "value": { + "status": "error", + "message": "Failed to submit Verifiable Presentation." + } + } + } + } + } + }, + "400": { + "description": "Invalid request or missing required parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Wallet ID not found in session": { + "value": { + "errorCode": "wallet_locked", + "errorMessage": "Wallet is locked" + } + }, + "Credential not found": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Credential not found: cred-123" + } + }, + "presentationId not found in session": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "presentationId not found in session" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user performing the presentation submission", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Failed to process presentation submission": { + "value": { + "errorCode": "wallet_vp_submission_error", + "errorMessage": "Failed to process presentation submission: Error message" + } + }, + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + }, + "Failed to reject verifier": { + "value": { + "errorCode": "error", + "errorMessage": "Unable to process reject verifier request" + } + } + } + } + } + } + }, + "security": [ + { + "SessionAuth": [] + } + ] + } + }, + "/wallets/{walletId}/presentations/{presentationId}/credentials": { + "get": { + "tags": [ + "Wallet Presentations" + ], + "summary": "Get matching credentials for a presentation request", + "description": "This API retrieves credentials from the wallet that match the presentation definition requirements. It returns available credentials that can satisfy the presentation request along with any missing claims that are required but not available.", + "operationId": "getMatchingCredentials", + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the Wallet." + }, + { + "name": "presentationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the Presentation." + } + ], + "responses": { + "200": { + "description": "Successfully retrieved matching credentials.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MatchingCredentialsResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "availableCredentials": [ + { + "credentialId": "cred-123", + "credentialTypeDisplayName": "W3C VC", + "credentialTypeLogo": "https://mosip.github.io/inji-config/logos/mosipid-logo.png", + "format": "ldp_vc" + } + ], + "missingClaims": ["$.type"] + } + } + } + } + } + }, + "400": { + "description": "Invalid request or missing required parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Presentation not found": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Presentation not found in session" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized user performing the Verifiable Presentation flow", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "User ID is not present in session": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + } + }, + "security": [ + { + "SessionAuth": [] + } + ] + } + }, + "/wallets/{walletId}/trusted-verifiers": { + "post": { + "tags": [ + "Wallet Trusted Verifiers" + ], + "summary": "Add a trusted verifier to a wallet", + "description": "Adds a trusted verifier for the specified wallet after validating the session and request. Requires an active session; the path walletId must match the wallet in session. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests.", + "operationId": "addTrustedVerifier", + "security": [ + { + "SessionAuth": [] + } + ], + "parameters": [ + { + "name": "walletId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The unique identifier of the wallet." + }, + { + "name": "X-XSRF-TOKEN", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "CSRF token obtained from the XSRF-TOKEN cookie set by the server on GET requests." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustedVerifierRequest" + }, + "examples": { + "Trusted Verifier Request": { + "value": { + "verifierId": "mock-client" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Trusted verifier created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrustedVerifierResponseDTO" + }, + "examples": { + "Success response": { + "value": { + "id": "2d598dfc-7218-cb7a7afd5a07" + } + } + } + } + } + }, + "400": { + "description": "Invalid request or missing required parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Missing verifierId": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Missing Input: verifierId is required" + } + }, + "Invalid Wallet ID": { + "value": { + "errorCode": "invalid_request", + "errorMessage": "Invalid Wallet ID. Session and request Wallet ID do not match" + } + }, + "Duplicate verifier": { + "value": { + "errorCode": "duplicate_verifier", + "errorMessage": "Verifier is already trusted for this wallet" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - session invalid or missing user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unauthorized": { + "value": { + "errorCode": "unauthorized", + "errorMessage": "User ID not found in session" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Unexpected Server Error": { + "value": { + "errorCode": "internal_server_error", + "errorMessage": "We are unable to process request now" + } + } + } + } + } + } + } + } + }, + "/auth/{provider}/token-login": { + "post": { + "tags": ["OAuth2 Authentication"], + "summary": "Login and create session using OAuth2 ID token", + "description": "This API accepts an OAuth2 ID token in the Authorization header and establishes a session by populating Spring Security context.\n\nFetch the ID token from a supported OAuth2 provider (such as Google or Microsoft) and provide it in the request as a Bearer token.", + "operationId": "loginWithOAuth2IdToken", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "google" + }, + "description": "The OAuth2 provider to use for login. Example values: 'google', 'microsoft', 'facebook'." + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "Bearer " + }, + "description": "The OAuth2 ID token that must be provided in the 'Authorization' header, prefixed with 'Bearer '." + } + ], + "responses": { + "200": { + "description": "Successfully logged in and session created", + "content": { + "application/json": { + "schema": { + "type": "string", + "description": "Success message indicating that the session has been created" + }, + "examples": { + "Success response": { + "value": "Session created." + } + } + } + } + }, + "400": { + "description": "Bad Request - Missing/invalid header or unsupported provider.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "UnsupportedProvider": { + "value": { + "errorCode": "INVALID_REQUEST", + "errorMessage": "Unsupported provider: provider123" + } + }, + "MissingToken": { + "value": { + "errorCode": "INVALID_REQUEST", + "errorMessage": "Bearer ID token required." + } + }, + "InvalidToken": { + "value": { + "errorCode": "invalid_token", + "errorMessage": "An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature" + } + }, + "ExpiredToken": { + "value": { + "errorCode": "invalid_token", + "errorMessage": "An error occurred while attempting to decode the Jwt: Jwt expired at 2025-06-10T12:00:00Z" + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "SessionAuth": { + "type": "apiKey", + "in": "cookie", + "name": "SESSION", + "description": "Session-based authentication using a session ID stored in the cookie. The client must send the 'SESSION' cookie (e.g., SESSION=) with each request. For state-changing requests (POST, PUT, DELETE, PATCH), a valid CSRF token must also be included in the X-XSRF-TOKEN header. The CSRF token can be obtained from the XSRF-TOKEN cookie set by the server on GET requests (e.g., by calling GET /issuers or GET /wallets)." + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "JWT-based authentication. Use a valid OAuth2 ID token (e.g., from Google or Microsoft) in the Authorization header as a Bearer token. Example: 'Authorization: Bearer eyJhbGciOi...'. This token will be used to authenticate the user and establish a session." + } + }, + "schemas": { + "UserMetadataDTO": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "description": "The display name of the user provided by the Identity Provider" + }, + "profile_picture_url": { + "type": "string", + "format": "uri", + "description": "The URL of the user's profile picture provided by the Identity Provider" + }, + "email": { + "type": "string", + "format": "email", + "description": "The email address of the user provided by the Identity Provider" + }, + "walletId": { + "type": "string", + "format": "email", + "description": "The unique identifier of the user's wallet. This is available if data is fetched from session else its null" + } + } + }, + "VerifiableCredentialRequestDTO": { + "type": "object", + "properties": { + "issuer": { + "type": "string", + "description": "The unique identifier of the issuer" + }, + "credentialConfigurationId": { + "type": "string", + "description": "The unique identifier of the credential type from the issuer well-known configuration" + }, + "code": { + "type": "string", + "description": "The authorization code received from the authorization server" + }, + "grantType": { + "type": "string", + "description": "The grant type for the authorization request" + }, + "redirectUri": { + "type": "string", + "description": "The redirect URI for the authorization request" + }, + "codeVerifier": { + "type": "string", + "description": "The code verifier used for PKCE (Proof Key for Code Exchange)" + } + }, + "required": [ + "issuer", + "credentialConfigurationId", + "code", + "grantType", + "redirectUri", + "codeVerifier" + ] + }, + "VerifiableCredentialResponseDTO": { + "type": "object", + "properties": { + "issuerDisplayName": { + "type": "string", + "description": "Name of the Issuer" + }, + "issuerLogo": { + "type": "string", + "description": "Logo of the Issuer" + }, + "credentialTypeDisplayName": { + "type": "string", + "description": "The name of the Credential Type issued by the Issuer" + }, + "credentialTypeLogo": { + "type": "string", + "description": "Logo of the Credential Type" + }, + "credentialId": { + "type": "string", + "description": "Unique Identifier of the Credential in the database" + } + } + }, + "CreateWalletRequestDto": { + "type": "object", + "properties": { + "walletPin": { + "type": "string", + "description": "PIN used to unlock the Wallet" + }, + "confirmWalletPin": { + "type": "string", + "description": "Re-entered PIN used to confirm the Wallet PIN" + }, + "walletName": { + "type": "string", + "description": "Name of the Wallet" + } + }, + "required": [ + "walletPin", + "confirmWalletPin" + ] + }, + "WalletResponseDTO": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "Unique identifier of the Wallet" + }, + "walletName": { + "type": "string", + "description": "Wallet name provided by the user" + } + } + }, + "UnlockWalletRequestDto": { + "type": "object", + "properties": { + "walletName": { + "type": "string", + "description": "Name of the Wallet" + }, + "walletPin": { + "type": "string", + "description": "PIN used to unlock the Wallet" + } + }, + "required": [ + "walletPin" + ] + }, + "WalletResponseDto": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "Unique identifier of the Wallet" + }, + "walletName" : { + "type": "string", + "description": "Name of the Wallet" + } + } + }, + "WalletDetailsResponseDto": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "Unique identifier of the Wallet" + }, + "walletName" : { + "type": "string", + "description": "Name of the Wallet" + }, + "walletStatus": { + "type": "string", + "description": "Wallet status indicating if it is locked or unlocked", + "enum": ["active", "temporarily_locked", "permanently_locked", "ready_for_unlock"], + "nullable": true + } + } + }, + "VerifiablePresentationAuthorizationRequest": { + "type": "object", + "properties": { + "authorizationRequestUrl": { + "type": "string", + "description": "The complete authorization request URL received from the Verifier" + } + }, + "required": [ + "authorizationRequestUrl" + ] + }, + "VerifiablePresentationResponseDTO": { + "type": "object", + "properties": { + "presentationId": { + "type": "string", + "description": "Unique identifier for the verifiable presentation request" + }, + "verifier": { + "$ref": "#/components/schemas/VerifiablePresentationVerifierDTO" + } + } + }, + "VerifiablePresentationVerifierDTO": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The client ID of the verifier" + }, + "name": { + "type": "string", + "description": "The name of the verifier" + }, + "logo": { + "type": "string", + "format": "uri", + "description": "The logo URI of the verifier" + }, + "isTrusted": { + "type": "boolean", + "description": "Indicates if the verifier is trusted by the wallet" + }, + "isPreregisteredWithWallet": { + "type": "boolean", + "description": "Indicates if the verifier is pre-registered with the wallet" + }, + "redirectUri": { + "type": "string", + "format": "uri", + "description": "The redirect URI provided in the authorization request to redirect the User back to Verifier" + } + } + }, + "PresentationSubmissionRequestDTO": { + "type": "object", + "properties": { + "selectedCredentials": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of selected credential IDs for presentation", + "example": ["cred-123", "cred-456"] + }, + "selectedSdClaims": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "Optional. Selected SD-JWT claim paths per credential ID for selective disclosure (only for SD-JWT credentials: vc+sd-jwt and dc+sd-jwt). Keyed by credential ID; values are the claim paths to disclose. Only the explicitly selected disclosures are shared; when omitted (or empty) for an SD-JWT credential, none of its selectively-disclosable claims are shared.", + "example": { + "cred-123": ["name", "dob"] + } + } + }, + "required": [ + "selectedCredentials" + ], + "additionalProperties": false + }, + "PresentationRejectionRequestDTO": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "description": "Error code for rejecting the verifier", + "example": "access_denied" + }, + "errorMessage": { + "type": "string", + "description": "Error message for rejecting the verifier", + "example": "User denied authorization to share credentials" + } + }, + "required": [ + "errorCode", + "errorMessage" + ], + "additionalProperties": false + }, + "IssuerConfigurationSuccessResponse": { + "type": "object", + "properties": { + "response": { + "type": "object", + "properties": { + "credentials_supported": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Credential" + } + }, + "authorization_endpoint": { + "type": "string", + "format": "uri" + }, + "grant_types_supported": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Error" + }, + "example": [] + } + } + }, + "IssuerConfigurationErrorResponse": { + "type": "object", + "properties": { + "response": { + "type": "object", + "nullable": true + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "IssuerV2DTO": { + "type": "object", + "description": "Minimal issuer representation (V2 API). Data from mimoto-issuers-config only.", + "properties": { + "issuer_id": { "type": "string", "description": "Unique identifier of the issuer" }, + "protocol": { "type": "string", "description": "Download flow protocol", "enum": ["OTP", "OpenId4VCI"] }, + "display": { + "type": "array", + "description": "Display properties of the issuer.", + "items": { + "type": "object", + "description": "Localized display entry for the issuer.", + "properties": { + "name": { "type": "string", "description": "Display name of the issuer." }, + "logo": { + "type": "object", + "description": "Display logo of the issuer.", + "properties": { + "url": { "type": "string", "description": "URL of the issuer logo image." }, + "alt_text": { "type": "string", "description": "Alternate text for the logo (accessibility)." } + } + }, + "title": { "type": "string", "description": "Display title of the issuer." }, + "description": { "type": "string", "description": "Short description of the issuer." }, + "language": { "type": "string", "description": "Language code for this display entry (e.g. en, fr)." } + } + } + }, + "client_id": { "type": "string", "description": "Onboarded Mimoto OIDC client ID" }, + "token_endpoint": { "type": "string", "description": "Token endpoint URL" }, + "client_alias": { "type": "string", "description": "Client alias in keyStore" }, + "qr_code_type": { "type": "string", "enum": ["OnlineSharing", "EmbeddedVC", "None"] }, + "enabled": { "type": "string", "description": "Whether issuer is enabled (\"true\", \"false\")" }, + "credential_issuer_host": { "type": "string", "description": "Credential issuer host URL" } + } + }, + "Credential": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "display": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Display" + } + } + } + }, + "Display": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "logo": { + "type": "string" + } + } + }, + "Error": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "description": "It represents the type or category of the error" + }, + "errorMessage": { + "type": "string", + "description": "A human-readable message providing more details about the error" + } + } + }, + "AppVIDGenerateRequestDTO": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "individualIdType": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "vidType": { + "type": "string" + }, + "transactionID": { + "type": "string" + } + } + }, + "AppOTPRequestDTO": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "individualIdType": { + "type": "string" + }, + "otpChannel": { + "type": "array", + "items": { + "type": "string" + } + }, + "transactionID": { + "type": "string" + } + } + }, + "AppCredentialRequestDTO": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "transactionID": { + "type": "string" + }, + "issuer": { + "type": "string" + }, + "credentialType": { + "type": "string" + }, + "user": { + "type": "string" + } + } + }, + "CredentialDownloadRequestDTO": { + "type": "object", + "properties": { + "individualId": { + "type": "string" + }, + "requestId": { + "type": "string" + } + } + }, + "CredentialDownloadResponseDTO": { + "type": "object", + "properties": { + "credential": { + "$ref": "#/components/schemas/JsonNode" + }, + "verifiableCredential": { + "$ref": "#/components/schemas/JsonNode" + } + } + }, + "JsonNode": { + "type": "object" + }, + "StringNode": { + "type": "string" + }, + "Event": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "transactionId": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/Type" + }, + "timestamp": { + "type": "string" + }, + "dataShareUri": { + "type": "string" + }, + "data": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + }, + "EventModel": { + "type": "object", + "properties": { + "publisher": { + "type": "string" + }, + "topic": { + "type": "string" + }, + "publishedOn": { + "type": "string" + }, + "event": { + "$ref": "#/components/schemas/Event" + } + } + }, + "Type": { + "type": "object", + "properties": { + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "GenericResponseDTO": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "AidStatusRequestDTO": { + "type": "object", + "properties": { + "aid": { + "type": "string" + }, + "otp": { + "type": "string" + }, + "transactionID": { + "type": "string" + } + } + }, + "MatchingCredentialsResponseDTO": { + "type": "object", + "properties": { + "availableCredentials": { + "type": "array", + "description": "List of credentials that match the presentation definition", + "items": { + "$ref": "#/components/schemas/AvailableCredentialDTO" + } + }, + "missingClaims": { + "type": "array", + "description": "List of claims that are required but not available in any credential", + "items": { + "type": "string" + } + } + } + }, + "AvailableCredentialDTO": { + "type": "object", + "properties": { + "credentialId": { + "type": "string", + "description": "Unique identifier of the credential" + }, + "credentialTypeDisplayName": { + "type": "string", + "description": "Display name of the credential type" + }, + "credentialTypeLogo": { + "type": "string", + "format": "uri", + "description": "Logo URL for the credential type" + }, + "format": { + "type": "string", + "description": "Format of the credential (e.g., ldp_vc, vc+sd-jwt, dc+sd-jwt)", + "example": "ldp_vc" + } + } + }, + "TrustedVerifierRequest": { + "type": "object", + "properties": { + "verifierId": { + "type": "string", + "description": "Unique client id of the verifier" + } + }, + "required": [ + "verifierId" + ] + }, + "TrustedVerifierResponseDTO": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique ID generated after creating database record for the verifier" + } + }, + "required": [ + "id" + ] + } + , + "SubmitPresentationResponseDTO": { + "type": "object", + "properties": { + "status": { "type": "string", "description": "Status of the presentation submission request (for example success, error)" }, + "message": { "type": "string", "description": "Human readable message describing the result for successful submission or rejected by user" }, + "redirectUri": { "type": "string", "description": "Redirect URI provided to the verifier", "nullable": true } + }, + "required": [ "status", "message" ] + } + } + }, + "x-internal": true +} \ No newline at end of file diff --git a/docs/images/apitestrig-1.png b/docs/images/apitestrig-1.png new file mode 100644 index 000000000..a7f36f6f0 Binary files /dev/null and b/docs/images/apitestrig-1.png differ diff --git a/docs/images/apitestrig-2.png b/docs/images/apitestrig-2.png new file mode 100644 index 000000000..c6e48e63d Binary files /dev/null and b/docs/images/apitestrig-2.png differ diff --git a/docs/postman-collections/Collab.postman_environment.json b/docs/postman-collections/Collab.postman_environment.json new file mode 100644 index 000000000..83de22baa --- /dev/null +++ b/docs/postman-collections/Collab.postman_environment.json @@ -0,0 +1,21 @@ +{ + "id": "8c93efeb-d403-4a20-aa23-d04afcfc0bd6", + "name": "Collab", + "values": [ + { + "key": "HOST", + "value": "https://api.collab.mosip.net/v1/mimoto", + "type": "default", + "enabled": true + }, + { + "key": "VC_ID", + "value": "9261578403", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-12-14T06:04:05.344Z", + "_postman_exported_using": "Postman/10.21.2-231211-1355" +} \ No newline at end of file diff --git a/docs/postman-collections/LocalDevelopment.postman_environment.json b/docs/postman-collections/LocalDevelopment.postman_environment.json new file mode 100644 index 000000000..7b4da5048 --- /dev/null +++ b/docs/postman-collections/LocalDevelopment.postman_environment.json @@ -0,0 +1,111 @@ +{ + "id": "384e12f7-b68a-40f9-b2ff-49905294ba0a", + "name": "LocalDevelopment", + "values": [ + { + "key": "walletPin", + "value": "123456", + "type": "default", + "enabled": true + }, + { + "key": "walletId", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "HOST", + "value": "http://localhost:8099/v1/mimoto", + "type": "default", + "enabled": true + }, + { + "key": "credentialId", + "value": "123445", + "type": "default", + "enabled": true + }, + { + "key": "clientId", + "value": "clientId", + "type": "secret", + "enabled": true + }, + { + "key": "clientSecret", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "credentialConfigurationId", + "value": "MockVerifiableCredential", + "type": "default", + "enabled": true + }, + { + "key": "issuer", + "value": "Mock", + "type": "default", + "enabled": true + }, + { + "key": "codeVerifier", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "code", + "value": "", + "type": "any", + "enabled": true + }, + { + "key": "authClientId", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "authRedirectUri", + "value": "http://localhost:3004/redirect", + "type": "default", + "enabled": true + }, + { + "key": "authServerUrl", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "clientIdForVcDownload", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "redirectionUrlForVcDownload", + "value": "http://localhost:3004/redirect", + "type": "default", + "enabled": true + }, + { + "key": "individual_id", + "value": "xxxxxxxxxx", + "type": "default", + "enabled": true + }, + { + "key": "authCode", + "value": "", + "type": "any", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2025-05-29T12:19:39.153Z", + "_postman_exported_using": "Postman/11.47.0" +} \ No newline at end of file diff --git a/docs/postman-collections/MIMOTO.postman_collection.json b/docs/postman-collections/MIMOTO.postman_collection.json new file mode 100644 index 000000000..cf84a84f1 --- /dev/null +++ b/docs/postman-collections/MIMOTO.postman_collection.json @@ -0,0 +1,1510 @@ +{ + "info": { + "_postman_id": "75c40eda-2aec-4fd7-a70b-ac2a40e8e04b", + "name": "MIMOTO", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "29153193" + }, + "item": [ + { + "name": "Issuers", + "item": [ + { + "name": "get Issuers List", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var token = pm.cookies.get('XSRF-TOKEN');", + "pm.environment.set('XSRF_TOKEN', token);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/issuers" + }, + "response": [] + }, + { + "name": "get Issuers Config", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/issuers/StayProtected" + }, + "response": [] + }, + { + "name": "get Issuers Wellknown", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/issuers/StayProtected/well-known-proxy" + }, + "response": [] + }, + { + "name": "get Issuers Configuration", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/issuers/StayProtected/configuration" + }, + "response": [] + }, + { + "name": "Search Issuers List", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/issuers?search=Sunbird", + "host": [ + "{{HOST}}" + ], + "path": [ + "issuers" + ], + "query": [ + { + "key": "search", + "value": "Sunbird" + } + ] + } + }, + "response": [] + }, + { + "name": "get Issuers List (V2)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var token = pm.cookies.get('XSRF-TOKEN');", + "pm.environment.set('XSRF_TOKEN', token);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/v2/issuers" + }, + "response": [] + }, + { + "name": "get Issuer by ID (V2)", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/v2/issuers/StayProtected" + }, + "response": [] + }, + { + "name": "Download Credential With GetToken", + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "grant_type", + "value": "authorization_code", + "type": "text" + }, + { + "key": "code", + "value": "GL9N1V-zW_OFgLSa2VBANv8qz6XOxh3yBkoaonjplms", + "type": "text" + }, + { + "key": "redirect_uri", + "value": "https://injiweb.dev1.mosip.net/redirect", + "type": "text" + }, + { + "key": "code_verifier", + "value": "Sq6nZNToevtx8LX-H2LI_~_Ds9vZGAImDKYjXw70LCJ", + "type": "text" + }, + { + "key": "issuer", + "value": "StayProtected", + "type": "text" + }, + { + "key": "credential", + "value": "InsuranceCredential", + "type": "text" + }, + { + "key": "vcStorageExpiryLimitInTimes", + "value": "1", + "type": "text" + }, + { + "key": "locale", + "value": "en", + "type": "text" + } + ] + }, + "url": "{{HOST}}/credentials/download" + }, + "response": [] + }, + { + "name": "Presentation Authorize", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/authorize?response_type=vp_token&resource=https://raw.githubusercontent.com/tw-mosip/verify-credential-js/main/VC/sunbird_qr.json&client_id=https://injiverify.dev2.mosip.net&redirect_uri=https://injiverify.dev2.mosip.net/redirect&presentation_definition=%7B%22id%22%3A%22vp%20token%20example%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22id%20card%20credential%22%2C%22format%22%3A%7B%22ldpVc%22%3A%7B%22proofTypes%22%3A%5B%22Ed25519Signature2020%22%5D%7D%7D%7D%5D%7D", + "host": [ + "{{HOST}}" + ], + "path": [ + "authorize" + ], + "query": [ + { + "key": "response_type", + "value": "vp_token" + }, + { + "key": "resource", + "value": "https://raw.githubusercontent.com/tw-mosip/verify-credential-js/main/VC/sunbird_qr.json" + }, + { + "key": "client_id", + "value": "https://injiverify.dev2.mosip.net" + }, + { + "key": "redirect_uri", + "value": "https://injiverify.dev2.mosip.net/redirect" + }, + { + "key": "presentation_definition", + "value": "%7B%22id%22%3A%22vp%20token%20example%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22id%20card%20credential%22%2C%22format%22%3A%7B%22ldpVc%22%3A%7B%22proofTypes%22%3A%5B%22Ed25519Signature2020%22%5D%7D%7D%7D%5D%7D" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Download VC - OTP", + "item": [ + { + "name": "Req OTP", + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"individualId\": \"{{VC_ID}}\",\n \"otpChannel\": [\n \"EMAIL\",\n \"PHONE\"\n ],\n \"transactionID\": \"1234567890\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/req/otp" + }, + "response": [] + }, + { + "name": "Credential Share Request", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"individualId\": \"{{VC_ID}}\",\n \"otp\": \"111111\",\n \"transactionID\": \"1234567890\",\n \"user\": \"mono\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/credentialshare/request" + }, + "response": [] + }, + { + "name": "Credential Share Status", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/credentialshare/request/status/c0fc00ae-6fc7-4e9f-b818-f51827582c2f" + }, + "response": [] + }, + { + "name": "Credential Share Download VC", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"individualId\": \"{{VC_ID}}\",\n \"requestId\": \"61cedc07-e036-46f5-8c75-97d852406373\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/credentialshare/download" + }, + "response": [] + } + ] + }, + { + "name": "Wallet Binding", + "item": [ + { + "name": "Binding OTP", + "request": { + "method": "POST", + "header": [ + { + "key": "accept", + "value": "*/*" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"individualId\": \"4536710485\",\n \"otpChannels\": [\n \"EMAIL\",\n \"PHONE\"\n ]\n }\n}" + }, + "url": "{{HOST}}/binding-otp" + }, + "response": [] + }, + { + "name": "Wallet Binding", + "request": { + "method": "POST", + "header": [ + { + "key": "accept", + "value": "*/*" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"individualId\": \"6148072053\",\n \"challengeList\": [\n {\n \"authFactorType\": \"OTP\",\n \"challenge\": \"111111\",\n \"format\": \"alpha-numeric\"\n }\n ],\n \"publicKey\": \"-----BEGIN RSA PUBLIC KEY-----\\nMIICCgKCAgEArk/N6Us8TkfH+r1UGCqTGWegJ3UzqHXmj49uE3l9ygIPrTB7Hee8\\ntiVnlNKcQR/7EipumAcNzOFzaysUUIX8rh/QSdlwYYXvC4OT9fLg5tmToYc5x3pN\\nO1k7qg9fnMb/Ksr4D95xUUxQjMLlbojBNWjLhvdP4da1gWUeAdfjIfyLjZGYiD8r\\nUqsVqOfmrec9Y0nLsIsUq735NVl2WIP9mMfjXuRId4MgEE3mN6x/Gj9Ov0AWfb48\\n22wLgybggTdIJZlCBAnPn/n+oiowqUdXADAB4CIaVH8hDJ1fz3Ag2RetriqwrcLF\\nVSK+PyVuLD261BhIeQrgyHdKC6qilVBOYIMqtXjazCdjGICNFoEa1nVP6mu85RN1\\ny8TTS+4ATUTm44++TFSNfsEEpagvd6O/BWlj6mJNGB9dmtEohowwR5fcSHgKbGtP\\ng5J49fKeSDapEln2A/xL7Ubk8KFzzgcQvPWttGaazs7tqdOE4iOMkI72XFbyvIVz\\nzwQEB7vOuRJRclP30SkcR81u694vCtXJ2t2pFUMQVJXat9ypg5BtiZMwBtAnc0Pz\\nDWdGCR5LIpJIaDISmPumHGAzeeRfREbsoVaCdi99vORoxBjuscpEOa5+MgYDKM3S\\nsJZWl2E2/7Kt9KCRVJoqeEkFFKq696zKVD3qbhbZJG3TjBcgYfbN2jMCAwEAAQ==\\n-----END RSA PUBLIC KEY-----\\n\",\n \"authFactorType\": \"WLA\",\n \"format\": \"jwt\"\n }\n}" + }, + "url": "{{HOST}}/wallet-binding" + }, + "response": [] + } + ] + }, + { + "name": "Verifiers", + "item": [ + { + "name": "Get Verifiers List", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/verifiers" + }, + "response": [] + } + ] + }, + { + "name": "Wallets", + "item": [ + { + "name": "Create Wallet", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Store the response body in a variable", + "let responseBody = pm.response.json();", + "", + "// Parse the response body to extract the walletId", + "let walletId = responseBody.walletId;", + "", + "pm.environment.set(\"walletId\", walletId);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"walletName\": \"default\",\n \"walletPin\": 123456,\n \"confirmWalletPin\": 123456\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/wallets" + }, + "response": [] + }, + { + "name": "Get Wallets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let responseBody = pm.response.json();", + "", + "// Parse the response body to extract the walletId", + "let walletId = responseBody[0].walletId;", + "", + "pm.environment.set(\"walletId\", walletId);", + "", + "// Extract and store CSRF token", + "var token = pm.cookies.get('XSRF-TOKEN');", + "pm.environment.set('XSRF_TOKEN', token);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/wallets" + }, + "response": [] + }, + { + "name": "Unlock Wallet", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"walletPin\": \"{{walletPin}}\"\n}" + }, + "url": "{{HOST}}/wallets/{{walletId}}/unlock" + }, + "response": [] + }, + { + "name": "Delete Wallet", + "request": { + "method": "DELETE", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "url": "{{HOST}}/wallets/" + }, + "response": [] + }, + { + "name": "Add Trusted Verifier", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "SESSION={{SESSION_TOKEN}}", + "type": "text" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"verifierId\": \"mock-client\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/trusted-verifiers", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "trusted-verifiers" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Wallet Presentations", + "item": [ + { + "name": "Create Presentation", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const presentationResponse = pm.response.json();", + "", + "pm.environment.set(\"presentationId\", presentationResponse.presentationId);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authorizationRequestUrl\": \"{{verifierAuthorizationRequestUrl}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/presentations", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "presentations" + ] + } + }, + "response": [] + }, + { + "name": "Get Matching Credentials", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const response = pm.response.json();", + "", + "if (Array.isArray(response.queryGroups)) {", + " const queryGroupsById = {};", + " (response.queryGroups || []).forEach(group => {", + " queryGroupsById[group.queryId] = group;", + " });", + "", + " const optionHasCredentials = option => option.every(qid => {", + " const group = queryGroupsById[qid];", + " return group && group.availableCredentials && group.availableCredentials.length > 0;", + " });", + "", + " const buildSelection = qid => ({", + " queryId: qid,", + " selectedCredentialIds: [queryGroupsById[qid].availableCredentials[0].credentialId]", + " });", + "", + " let selections = [];", + " const credentialSets = response.credentialSets || [];", + " const selectedQueryIds = new Set();", + " const optionIndex = parseInt(pm.collectionVariables.get(\"dcqlCredentialSetOptionIndex\") || \"0\", 10);", + "", + " credentialSets.forEach(set => {", + " if (!set.required) { return; }", + " const validOptions = (set.options || []).filter(optionHasCredentials);", + " const chosenOption = validOptions[optionIndex] || validOptions[0];", + " if (chosenOption) {", + " chosenOption.forEach(qid => selectedQueryIds.add(qid));", + " pm.collectionVariables.set(\"selectedDcqlCredentialSetOption\", JSON.stringify(chosenOption));", + " }", + " });", + "", + " selections = [...selectedQueryIds].map(buildSelection);", + "", + " if (selections.length === 0) {", + " selections = (response.queryGroups || [])", + " .filter(group => group.availableCredentials && group.availableCredentials.length > 0)", + " .map(group => ({", + " queryId: group.queryId,", + " selectedCredentialIds: [group.availableCredentials[0].credentialId]", + " }));", + " }", + "", + " pm.collectionVariables.set(\"submitPresentationDcqlBody\", JSON.stringify({", + " selectedCredentials: selections", + " }, null, 2));", + " pm.collectionVariables.set(\"isDcqlPresentation\", \"true\");", + "", + " if (selections.length > 0) {", + " pm.collectionVariables.set(\"credentialId\", selections[0].selectedCredentialIds[0]);", + " } else {", + " console.warn(\"No DCQL selections built. Check queryGroups.availableCredentials and missingClaims in the matching response.\");", + " }", + "} else {", + " const available = response.availableCredentials || [];", + " const credentialIds = available.map(c => c.credentialId).filter(Boolean);", + "", + " if (credentialIds.length > 0) {", + " pm.collectionVariables.set(\"credentialId\", credentialIds[0]);", + " pm.collectionVariables.set(\"credentialId1\", credentialIds[0]);", + " if (credentialIds.length > 1) {", + " pm.collectionVariables.set(\"credentialId2\", credentialIds[1]);", + " }", + " }", + "", + " const firstCredentialId = credentialIds.length > 0 ? credentialIds[0] : \"\";", + " pm.collectionVariables.set(\"submitPresentationDraft23Body\", JSON.stringify({", + " selectedCredentials: firstCredentialId ? [firstCredentialId] : []", + " }, null, 2));", + " pm.collectionVariables.set(\"isDcqlPresentation\", \"false\");", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/presentations/{{presentationId}}/credentials", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "presentations", + "{{presentationId}}", + "credentials" + ] + } + }, + "response": [] + }, + { + "name": "Submit Presentation (Draft-23)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const isDcql = pm.collectionVariables.get(\"isDcqlPresentation\") === \"true\";", + "if (isDcql) {", + " console.warn(\"This request is for Draft-23. Use Submit Presentation (OVP 1.0 / DCQL) for DCQL requests.\");", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{{submitPresentationDraft23Body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/presentations/{{presentationId}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "presentations", + "{{presentationId}}" + ] + }, + "description": "Presentation Exchange (Draft-23). Run Get Matching Credentials first — the first available credential is stored in submitPresentationDraft23Body." + }, + "response": [] + }, + { + "name": "Submit Presentation (OVP 1.0 / DCQL)", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const isDcql = pm.collectionVariables.get(\"isDcqlPresentation\") === \"true\";", + "if (!isDcql) {", + " console.warn(\"This request is for OVP 1.0 / DCQL. Use Submit Presentation (Draft-23) for Presentation Exchange requests.\");", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{{submitPresentationDcqlBody}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/presentations/{{presentationId}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "presentations", + "{{presentationId}}" + ] + }, + "description": "OVP 1.0 / DCQL. Run Get Matching Credentials first. When credentialSets is present, only one option is auto-selected (default: first valid option). Set collection variable dcqlCredentialSetOptionIndex to 0, 1, or 2 to pick a different option." + }, + "response": [] + }, + { + "name": "User Reject Verifier", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Cookie", + "value": "SESSION={{SESSION_TOKEN}}", + "type": "text" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"errorCode\": \"access_denied\",\n \"errorMessage\": \"User denied authorization to share credentials\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/presentations/{{presentationId}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "presentations", + "{{presentationId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "Get User Profile", + "request": { + "method": "GET", + "header": [ + { + "key": "Cookie", + "value": "SESSION={{SESSION_TOKEN}}", + "type": "text" + } + ], + "url": "{{HOST}}/users/me" + }, + "response": [] + } + ] + }, + { + "name": "Wallet Credentials", + "item": [ + { + "name": "authCode for VC download (pre-requisite)", + "item": [ + { + "name": "Get CSRF token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var token = pm.cookies.get(\"XSRF-TOKEN\")", + "pm.collectionVariables.set(\"csrf_token\", token);" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{authServerUrl}}/csrf/token" + }, + "response": [] + }, + { + "name": "Authorize / OAuthdetails request V2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var btoa = require('btoa');", + "", + "var token = pm.cookies.get(\"XSRF-TOKEN\")", + "pm.collectionVariables.set(\"csrf_token\", token);", + "", + "pm.test(\"Validate transactionId\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.response.transactionId).not.equals(null);", + " pm.collectionVariables.set(\"transaction_id\", jsonData.response.transactionId);", + "});", + "", + "pm.test(\"Validate auth factors\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.response.authFactors[0].name).to.eql(pm.environment.get(\"expected_amr\"));", + "});", + "", + "pm.test(\"set oauth-details-hash\", function () {", + " var jsonData = pm.response.json();", + " var sha256Hash = CryptoJS.SHA256(JSON.stringify(jsonData.response));", + " var base64Encoded = sha256Hash.toString(CryptoJS.enc.Base64);", + " // Remove padding characters", + " base64Encoded = base64Encoded.replace(/=+$/, '');", + " // Replace '+' with '-' and '/' with '_' to convert to base64 URL encoding", + " base64Encoded = base64Encoded.replace(/\\+/g, '-').replace(/\\//g, '_');", + " pm.collectionVariables.set(\"oauth_details_key\", jsonData.response.transactionId);", + " pm.collectionVariables.set(\"oauth_details_hash\", base64Encoded);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "eval(pm.globals.get('pmlib_code'))", + "", + "const pkce = pmlib.pkceChallenge();", + "pm.collectionVariables.set(\"codeChallenge\",pkce.code_challenge);", + "pm.collectionVariables.set(\"codeChallengeMethod\",pkce.code_challenge_method);", + "pm.environment.set(\"codeVerifier\",pkce.code_verifier);", + "", + "", + "// Generate a random nonce (UUID-style or random hex)", + "const nonce = Math.random().toString(36).substring(2) + Date.now().toString(36);", + "pm.collectionVariables.set(\"nonce\", nonce);", + "", + "", + "const generateRandomString = (length = 43, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~') => {", + " let randomString = '';", + " for (let i = 0; i < length; i++) {", + " const randomIndex = Math.floor(Math.random() * charset.length);", + " randomString += charset[randomIndex];", + " }", + " return randomString;", + "};", + "", + "const state = generateRandomString();", + "pm.collectionVariables.set(\"stateForDownload\", state);", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{csrf_token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"clientId\": \"{{clientIdForVcDownload}}\",\n \"scope\": \"mock_identity_vc_ldp\",\n \"responseType\": \"code\",\n \"redirectUri\": \"{{redirectionUrlForVcDownload}}\",\n \"display\": \"popup\",\n \"prompt\": \"login\",\n \"acrValues\": \"mosip:idp:acr:generated-code\",\n \"nonce\" : \"{{nonce}}\",\n \"state\" : \"{{stateForDownload}}\",\n \"claimsLocales\" : \"en\",\n \"codeChallenge\" : \"{{codeChallenge}}\",\n \"codeChallengeMethod\" : \"{{codeChallengeMethod}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{authServerUrl}}/authorization/v2/oauth-details" + }, + "response": [] + }, + { + "name": "Send OTP", + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{csrf_token}}", + "type": "text" + }, + { + "key": "oauth-details-key", + "value": "{{oauth_details_key}}", + "type": "text" + }, + { + "key": "oauth-details-hash", + "value": "{{oauth_details_hash}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"transactionId\": \"{{transaction_id}}\",\n \"individualId\": \"{{individual_id}}\",\n \"otpChannels\" : [\"email\",\"phone\"],\n \"captchaToken\" : \"dummy\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{authServerUrl}}/authorization/send-otp" + }, + "response": [] + }, + { + "name": "Authenticate User", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var token = pm.cookies.get(\"XSRF-TOKEN\")", + "pm.collectionVariables.set(\"csrf_token\", token);" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{csrf_token}}", + "type": "text" + }, + { + "key": "oauth-details-key", + "value": "{{oauth_details_key}}", + "type": "text" + }, + { + "key": "oauth-details-hash", + "value": "{{oauth_details_hash}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"transactionId\": \"{{transaction_id}}\",\n \"individualId\": \"{{individual_id}}\",\n \"challengeList\" : [\n {\n \"authFactorType\" : \"OTP\",\n \"challenge\" : \"111111\",\n \"format\" : \"alpha-numeric\"\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{authServerUrl}}/authorization/v3/authenticate" + }, + "response": [] + }, + { + "name": "Authorization Code", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var token = pm.cookies.get(\"XSRF-TOKEN\")", + "pm.collectionVariables.set(\"csrf_token\", token);", + "", + "var jsonData = pm.response.json();", + "pm.expect(jsonData.response.code).not.equals(null);", + "pm.environment.set(\"authCode\", jsonData.response.code);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "followRedirects": false + }, + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{csrf_token}}", + "type": "text" + }, + { + "key": "oauth-details-key", + "value": "{{oauth_details_key}}", + "type": "text" + }, + { + "key": "oauth-details-hash", + "value": "{{oauth_details_hash}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"transactionId\": \"{{transaction_id}}\",\n \"acceptedClaims\": [],\n \"permittedAuthorizeScopes\" : []\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{authServerUrl}}/authorization/auth-code" + }, + "response": [] + } + ] + }, + { + "name": "Save Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "pm.environment.set(\"credentialId\", response.credentialId);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept-Language", + "value": "en", + "type": "text" + }, + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"grantType\": \"authorization_code\",\n \"code\": \"{{authCode}}\",\n \"codeVerifier\": \"{{codeVerifier}}\",\n \"redirectUri\": \"http://localhost:3004/redirect\",\n \"credentialConfigurationId\": \"{{credentialConfigurationId}}\",\n \"issuer\": \"{{issuer}}\"\n}" + }, + "url": "{{HOST}}/wallets/{{walletId}}/credentials" + }, + "response": [] + }, + { + "name": "Fetch All Credentials", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept-Language", + "value": "en", + "type": "text" + } + ], + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/credentials", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "credentials" + ], + "query": [ + { + "key": "locale", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "View or Save Credential", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept-Language", + "value": "en", + "type": "text" + }, + { + "key": "Accept", + "value": "application/pdf", + "type": "text" + } + ], + "url": { + "raw": "{{HOST}}/wallets/{{walletId}}/credentials/{{credentialId}}?action=inline", + "host": [ + "{{HOST}}" + ], + "path": [ + "wallets", + "{{walletId}}", + "credentials", + "{{credentialId}}" + ], + "query": [ + { + "key": "action", + "value": "inline" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete Credential", + "request": { + "method": "DELETE", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "url": "{{HOST}}/wallets/{{walletId}}/credentials/{{credentialId}}" + }, + "response": [] + } + ] + }, + { + "name": "Authentication", + "item": [ + { + "name": "Token login", + "request": { + "auth": { + "type": "oauth2", + "oauth2": { + "clientSecret": "{{clientSecret}}", + "clientId": "{{clientId}}", + "accessTokenUrl": "https://oauth2.googleapis.com/token", + "scope": "profile email", + "redirect_uri": "http://localhost:8099/v1/mimoto/oauth2/callback/google", + "useBrowser": false, + "authUrl": "https://accounts.google.com/o/oauth2/auth?prompt=select_account", + "tokenName": "AuthToken", + "addTokenTo": "header" + } + }, + "method": "POST", + "header": [], + "url": "{{HOST}}/auth/google/token-login" + }, + "response": [] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "url": "{{HOST}}/logout" + }, + "response": [] + } + ] + }, + { + "name": "All properties", + "request": { + "method": "GET", + "header": [], + "url": "{{HOST}}/allProperties" + }, + "response": [] + }, + { + "name": "IndividualId Req OTP", + "request": { + "method": "POST", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{XSRF_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"mosip.identity.otp.internal\",\n \"aid\": \"10011100600000220231011051701\",\n \"metadata\": {},\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"version\": \"1.0\",\n \"otpChannel\": [\n \"EMAIL\",\n \"PHONE\"\n ],\n \"transactionID\": \"1234567890\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{HOST}}/req/individualId/otp" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "proof_jwt", + "value": "" + }, + { + "key": "codeChallenge", + "value": "" + }, + { + "key": "codeChallengeMethod", + "value": "" + }, + { + "key": "codeVerifier", + "value": "" + }, + { + "key": "code", + "value": "" + }, + { + "key": "authCode", + "value": "" + }, + { + "key": "nonce", + "value": "" + }, + { + "key": "stateForDownload", + "value": "" + }, + { + "key": "csrf_token", + "value": "" + }, + { + "key": "transaction_id", + "value": "" + }, + { + "key": "oauth_details_key", + "value": "" + }, + { + "key": "oauth_details_hash", + "value": "" + }, + { + "key": "XSRF_TOKEN", + "value": "" + }, + { + "key": "isDcqlPresentation", + "value": "" + }, + { + "key": "submitPresentationDraft23Body", + "value": "{\n \"selectedCredentials\": []\n}" + }, + { + "key": "submitPresentationDcqlBody", + "value": "{\n \"selectedCredentials\": []\n}" + }, + { + "key": "dcqlCredentialSetOptionIndex", + "value": "0" + }, + { + "key": "selectedDcqlCredentialSetOption", + "value": "" + }, + { + "key": "credentialId1", + "value": "" + }, + { + "key": "credentialId2", + "value": "" + } + ] +} \ No newline at end of file diff --git a/helm/mimoto/.gitignore b/helm/mimoto/.gitignore new file mode 100644 index 000000000..1654d0dc3 --- /dev/null +++ b/helm/mimoto/.gitignore @@ -0,0 +1,2 @@ +charts/ +Chart.lock \ No newline at end of file diff --git a/helm/mimoto/.helmignore b/helm/mimoto/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/helm/mimoto/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/helm/mimoto/Chart.yaml b/helm/mimoto/Chart.yaml new file mode 100644 index 000000000..a6dd02ffd --- /dev/null +++ b/helm/mimoto/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: mimoto +description: A Helm chart for INJI mimoto Application +type: application +version: 0.0.1-develop +appVersion: "" +dependencies: + - name: common + repository: https://charts.bitnami.com/bitnami + tags: + - bitnami-common + version: 1.x.x +home: https://inji.io +keywords: + - mosip + - mimoto +maintainers: + - email: info@inji.io + name: INJI diff --git a/helm/mimoto/templates/NOTES.txt b/helm/mimoto/templates/NOTES.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/helm/mimoto/templates/NOTES.txt @@ -0,0 +1 @@ + diff --git a/helm/mimoto/templates/_helpers.tpl b/helm/mimoto/templates/_helpers.tpl new file mode 100644 index 000000000..2c6d12dea --- /dev/null +++ b/helm/mimoto/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Return the proper image name +*/}} +{{- define "mimoto.image" -}} +{{ include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) }} +{{- end -}} + +{{/* +Return the proper image name (for the init container volume-permissions image) +*/}} +{{- define "mimoto.volumePermissions.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.volumePermissions.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Return the proper Docker Image Registry Secret Names +*/}} +{{- define "mimoto.imagePullSecrets" -}} +{{- include "common.images.pullSecrets" (dict "images" (list .Values.image .Values.volumePermissions.image) "global" .Values.global) -}} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "mimoto.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (printf "%s" (include "common.names.fullname" .)) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Compile all warnings into a single message. +*/}} +{{- define "mimoto.validateValues" -}} +{{- $messages := list -}} +{{- $messages := append $messages (include "mimoto.validateValues.foo" .) -}} +{{- $messages := append $messages (include "mimoto.validateValues.bar" .) -}} +{{- $messages := without $messages "" -}} +{{- $message := join "\n" $messages -}} + +{{- if $message -}} +{{- printf "\nVALUES VALIDATION:\n%s" $message -}} +{{- end -}} +{{- end -}} + +{{/* +Return podAnnotations +*/}} +{{- define "mimoto.podAnnotations" -}} +{{- if .Values.podAnnotations }} +{{ include "common.tplvalues.render" (dict "value" .Values.podAnnotations "context" $) }} +{{- end }} +{{- if and .Values.metrics.enabled .Values.metrics.podAnnotations }} +{{ include "common.tplvalues.render" (dict "value" .Values.metrics.podAnnotations "context" $) }} +{{- end }} +{{- end -}} + + diff --git a/helm/mimoto/templates/deployment.yaml b/helm/mimoto/templates/deployment.yaml new file mode 100644 index 000000000..21225ee56 --- /dev/null +++ b/helm/mimoto/templates/deployment.yaml @@ -0,0 +1,187 @@ +apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} +kind: Deployment +metadata: + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + name: {{ template "common.names.fullname" . }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + {{- if .Values.updateStrategy }} + strategy: {{- toYaml .Values.updateStrategy | nindent 4 }} + {{- end }} + selector: + matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- if or .Values.podAnnotations .Values.metrics.enabled }} + {{- include "mimoto.podAnnotations" . | nindent 8 }} + {{- end }} + labels: {{- include "common.labels.standard" . | nindent 8 }} + {{- if .Values.podLabels }} + {{- include "common.tplvalues.render" (dict "value" .Values.podLabels "context" $) | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ template "mimoto.serviceAccountName" . }} + {{- include "mimoto.imagePullSecrets" . | nindent 6 }} + {{- if .Values.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.affinity }} + affinity: {{- include "common.tplvalues.render" ( dict "value" .Values.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.podAffinityPreset "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.podAntiAffinityPreset "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.nodeAffinityPreset.type "key" .Values.nodeAffinityPreset.key "values" .Values.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.tolerations "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} + {{- if .Values.podSecurityContext.enabled }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- end }} + initContainers: + {{- if and .Values.volumePermissions.enabled .Values.extraVolumeMounts }} + - name: volume-permissions + image: "{{ .Values.volumePermissions.image.registry }}/{{ .Values.volumePermissions.image.repository }}:{{ .Values.volumePermissions.image.tag }}" + imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }} + command: + - /bin/bash + - -c + - | + {{- range .Values.extraVolumeMounts }} + mkdir -p {{ .mountPath }} + chown -R {{ $.Values.podSecurityContext.fsGroup }}:{{ $.Values.podSecurityContext.fsGroup }} {{ .mountPath }} + chmod -R 775 {{ .mountPath }} + {{- end }} + securityContext: + runAsUser: 0 + {{- if .Values.volumePermissions.resources }} + resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }} + {{- end }} + volumeMounts: + {{- toYaml .Values.extraVolumeMounts | nindent 12 }} + {{- end }} + {{- if .Values.enable_insecure }} + {{- include "common.tplvalues.render" (dict "value" .Values.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: {{ template "mimoto.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.lifecycleHooks }} + lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.containerSecurityContext.enabled }} + securityContext: + runAsUser: {{ .Values.containerSecurityContext.runAsUser }} + runAsNonRoot: {{ .Values.containerSecurityContext.runAsNonRoot }} + {{- with .Values.containerSecurityContext.capabilities }} + capabilities: + {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + {{- if .Values.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.args "context" $) | nindent 12 }} + {{- end }} + env: + - name: container_user + value: {{ .Values.containerSecurityContext.runAsUser | quote }} + - name: JDK_JAVA_OPTIONS + value: {{ .Values.additionalResources.javaOpts | quote }} + - name: KEYSTORE_P12_PATH + value: {{ .Values.volumes.secrets.mimotooidc.path | quote }} + {{- if .Values.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + envFrom: + {{- if .Values.extraEnvVarsCM }} + {{- range .Values.extraEnvVarsCM }} + - configMapRef: + name: {{ . | quote }} + {{- end }} + {{- end }} + {{- if .Values.extraEnvVarsSecret }} + {{- range .Values.extraEnvVarsSecret }} + - secretRef: + name: {{ . | quote }} + {{- end }} + {{- end }} + volumeMounts: + - name: landing-folder + mountPath: {{ .Values.persistence.mountDir }} + {{- if .Values.volumes.secrets }} + {{- range $secretName, $value := .Values.volumes.secrets }} + - name: {{ $secretName }} + mountPath: {{ $value.path }} + {{- end }} + {{- end }} + {{- if .Values.enable_insecure }} + - mountPath: /usr/local/openjdk-11/lib/security/cacerts + name: cacerts + subPath: cacerts + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: [] + {{- if .Values.resources }} + resources: {{- toYaml .Values.resources | nindent 12 }} + {{- end }} + {{- if .Values.startupProbe.enabled }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.startupProbe "enabled") "context" $) | nindent 12 }} + {{- else if .Values.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customStartupProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.livenessProbe "enabled") "context" $) | nindent 12 }} + {{- else if .Values.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customLivenessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.readinessProbe "enabled") "context" $) | nindent 12 }} + {{- else if .Values.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.customReadinessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.sidecars }} + {{- include "common.tplvalues.render" ( dict "value" .Values.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + - name: landing-folder + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "common.names.fullname" .) }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.volumes.secrets }} + {{- range $secretName, $value := .Values.volumes.secrets }} + - name: {{ $secretName }} + secret: + secretName: {{ $secretName }} + {{- end }} + {{- end }} + {{- if .Values.enable_insecure }} + - name: cacerts + emptyDir: {} + {{- end }} + {{- if .Values.extraVolumes }} + {{- toYaml .Values.extraVolumes | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/helm/mimoto/templates/extra-list.yaml b/helm/mimoto/templates/extra-list.yaml new file mode 100644 index 000000000..9ac65f9e1 --- /dev/null +++ b/helm/mimoto/templates/extra-list.yaml @@ -0,0 +1,4 @@ +{{- range .Values.extraDeploy }} +--- +{{ include "common.tplvalues.render" (dict "value" . "context" $) }} +{{- end }} diff --git a/helm/mimoto/templates/pvc.yaml b/helm/mimoto/templates/pvc.yaml new file mode 100644 index 000000000..6fb903db1 --- /dev/null +++ b/helm/mimoto/templates/pvc.yaml @@ -0,0 +1,60 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "common.names.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + "helm.sh/resource-policy": keep +spec: + accessModes: + {{- if not (empty .Values.persistence.accessModes) }} + {{- range .Values.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + {{- else }} + - {{ .Values.persistence.accessMode | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- include "common.storage.class" (dict "persistence" .Values.persistence "global" .Values.global) | nindent 2 }} + {{- if .Values.persistence.dataSource }} + dataSource: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.dataSource "context" $) | nindent 4 }} + {{- end }} + {{- end }} + {{- if .Values.extraVolumes }} + {{- range .Values.extraVolumes }} + {{- if and (eq .kind "persistentVolumeClaim") (eq .name "mimoto-encryption-volume") (not .persistentVolumeClaim.claimName) }} +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: mimoto-keygen-keymanager + namespace: {{ $.Release.Namespace | quote }} + labels: {{- include "common.labels.standard" $ | nindent 4 }} + {{- if $.Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" $.Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if $.Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" $.Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + "helm.sh/resource-policy": keep +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ $.Values.encryption.volume.size | default "1Gi" | quote }} + {{- include "common.storage.class" (dict "persistence" $.Values.persistence "global" $.Values.global) | nindent 2 }} + {{- end }} + {{- end }} + {{- end }} \ No newline at end of file diff --git a/helm/mimoto/templates/secrets.yaml b/helm/mimoto/templates/secrets.yaml new file mode 100644 index 000000000..667dba388 --- /dev/null +++ b/helm/mimoto/templates/secrets.yaml @@ -0,0 +1,24 @@ +{{- if .Values.mimoto.secrets }} +{{- range $secret_name, $secret_value := .Values.mimoto.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secret_name }} + namespace: {{ $.Release.Namespace }} + labels: + {{- include "common.labels.standard" $ | nindent 4 }} + {{- if $.Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" $.Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if $.Values.commonAnnotations }} + annotations: + {{- include "common.tplvalues.render" ( dict "value" $.Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: Opaque +data: + {{- range $key, $value := $secret_value }} + {{ $key }}: {{ $value | b64enc | quote }} + {{- end }} +--- +{{- end }} +{{- end }} diff --git a/helm/mimoto/templates/service-account.yaml b/helm/mimoto/templates/service-account.yaml new file mode 100644 index 000000000..ac200e1d6 --- /dev/null +++ b/helm/mimoto/templates/service-account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + name: {{ template "mimoto.serviceAccountName" . }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + namespace: {{ .Release.Namespace }} diff --git a/helm/mimoto/templates/service.yaml b/helm/mimoto/templates/service.yaml new file mode 100644 index 000000000..a0bd61a15 --- /dev/null +++ b/helm/mimoto/templates/service.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + name: {{ template "common.names.fullname" . }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if (or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort")) }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }} + {{- end }} + {{ if eq .Values.service.type "LoadBalancer" }} + loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges }} + {{ end }} + {{- if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP))) }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} + ports: + - port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.service.containerPort }} + selector: {{- include "common.labels.matchLabels" . | nindent 4 }} diff --git a/helm/mimoto/templates/servicemonitor.yaml b/helm/mimoto/templates/servicemonitor.yaml new file mode 100644 index 000000000..027c8ac04 --- /dev/null +++ b/helm/mimoto/templates/servicemonitor.yaml @@ -0,0 +1,36 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "common.names.fullname" . }} + {{- if .Values.metrics.serviceMonitor.namespace }} + namespace: {{ .Values.metrics.serviceMonitor.namespace }} + {{- else }} + namespace: {{ .Release.Namespace | quote }} + {{- end }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.metrics.serviceMonitor.additionalLabels }} + {{- include "common.tplvalues.render" (dict "value" .Values.metrics.serviceMonitor.additionalLabels "context" $) | nindent 4 }} + {{- end }} +spec: + endpoints: + - targetPort: {{ .Values.service.containerPort }} + path: {{ .Values.metrics.endpointPath }} + {{- if .Values.metrics.serviceMonitor.interval }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.honorLabels }} + honorLabels: {{ .Values.metrics.serviceMonitor.honorLabels }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.relabellings }} + metricRelabelings: {{- toYaml .Values.metrics.serviceMonitor.relabellings | nindent 6 }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace | quote }} + selector: + matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }} +{{- end }} diff --git a/helm/mimoto/templates/virtualservice.yaml b/helm/mimoto/templates/virtualservice.yaml new file mode 100644 index 000000000..cdeb45831 --- /dev/null +++ b/helm/mimoto/templates/virtualservice.yaml @@ -0,0 +1,36 @@ +{{- if .Values.istio.enabled }} +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: {{ template "common.names.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + hosts: + - "*" + gateways: + {{- include "common.tplvalues.render" ( dict "value" .Values.istio.gateways "context" $ ) | nindent 4 }} + http: + - match: + - uri: + prefix: {{ .Values.istio.prefix1 }} + - uri: + prefix: {{ .Values.istio.prefix2 }} + rewrite: + uri: {{ .Values.istio.rewrite }} + route: + - destination: + host: {{ template "common.names.fullname" . }} + port: + number: {{ .Values.service.port }} + headers: + request: + set: + x-forwarded-proto: https +{{- end }} diff --git a/helm/mimoto/values.yaml b/helm/mimoto/values.yaml new file mode 100644 index 000000000..37bcfb2b5 --- /dev/null +++ b/helm/mimoto/values.yaml @@ -0,0 +1,500 @@ +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +# global: +# imageRegistry: myRegistryName +# imagePullSecrets: +# - myRegistryKeySecretName +# storageClass: myStorageClass + +## Add labels to all the deployed resources +## +commonLabels: + app.kubernetes.io/component: inji + +## Add annotations to all the deployed resources +## +commonAnnotations: {} + +## Kubernetes Cluster Domain +## +clusterDomain: cluster.local + +## Extra objects to deploy (value evaluated as a template) +## +extraDeploy: [] + +## Number of nodes +## +replicaCount: 1 + +service: + type: ClusterIP + port: 80 + containerPort: 8099 + ## loadBalancerIP for the SuiteCRM Service (optional, cloud specific) + ## ref: http://kubernetes.io/docs/user-guide/services/#type-loadbalancer + ## + ## loadBalancerIP: + ## + ## nodePorts: + ## http: + ## https: + ## + + nodePorts: + http: "" + https: "" + ## Enable client source IP preservation + ## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + ## + externalTrafficPolicy: Cluster + +image: + registry: docker.io + repository: injistackdev/mimoto + tag: develop + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: Always + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + +## Configure extra options for liveness and readiness probes +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + +## TODO: enable probes once health urls are available + +startupProbe: + enabled: true + httpGet: + path: /v1/mimoto/actuator/health + port: 8099 + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + successThreshold: 1 + +livenessProbe: + enabled: true + httpGet: + path: /v1/mimoto/actuator/health + port: 8099 + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +readinessProbe: + enabled: true + httpGet: + path: /v1/mimoto/actuator/health + port: 8099 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +## +# existingConfigmap: + +## Command and args for running the container (set to default if not set). Use array form +## +command: [] +args: [] + +## Deployment pod host aliases +## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ +## +hostAliases: [] + +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 1000m + memory: 5000Mi + requests: + cpu: 200m + memory: 2000Mi + +additionalResources: + ## Specify any JAVA_OPTS string here. These typically will be specified in conjunction with above resources + ## Example: java_opts: "-Xms500M -Xmx500M" + javaOpts: "-Xms2250M -Xmx2250M" + +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container +## Mimoto container already runs as 'inji' user, so we may not need to enable this +containerSecurityContext: + enabled: false + runAsUser: 1001 + runAsNonRoot: true + +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod +## +podSecurityContext: + enabled: false + fsGroup: 1001 + +## Pod affinity preset +## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity +## Allowed values: soft, hard +## +podAffinityPreset: "" + +## Pod anti-affinity preset +## Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity +## Allowed values: soft, hard +## +podAntiAffinityPreset: soft + +## Node affinity preset +## Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity +## Allowed values: soft, hard +## +nodeAffinityPreset: + ## Node affinity type + ## Allowed values: soft, hard + ## + type: "" + ## Node label key to match + ## E.g. + ## key: "kubernetes.io/e2e-az-name" + ## + key: "" + ## Node label values to match + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + +## Affinity for pod assignment. Evaluated as a template. +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} + +## Node labels for pod assignment. Evaluated as a template. +## ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment. Evaluated as a template. +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Pod extra labels +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +## +podLabels: {} + +## Annotations for server pods. +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +## +podAnnotations: {} + +## pods' priority. +## ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/ +## +# priorityClassName: "" + +## lifecycleHooks for the container to automate configuration before or after startup. +## +lifecycleHooks: {} + +## Custom Liveness probes for +## +customLivenessProbe: {} + +## Custom Rediness probes +## +customReadinessProbe: {} + +## Update strategy - only really applicable for deployments with RWO PVs attached +## If replicas = 1, an update can get "stuck", as the previous pod remains attached to the +## PV, and the "incoming" pod can never start. Changing the strategy to "Recreate" will +## terminate the single previous pod, so that the new, incoming pod can attach to the PV +## +updateStrategy: + type: RollingUpdate + +## Additional environment variables to set +## Example: +extraEnvVars: [] +# - name: REDIS_HOST +# valueFrom: +# configMapKeyRef: +# name: redis-config +# key: redis-host +# - name: REDIS_PORT +# valueFrom: +# configMapKeyRef: +# name: redis-config +# key: redis-port +# - name: REDIS_PASSWORD +# valueFrom: +# secretKeyRef: +# name: redis +# key: redis-password +# - name: DB_DBUSER_PASSWORD +# valueFrom: +# secretKeyRef: +# name: db-common-secrets +# key: db-dbuser-password +# - name: INJI_MIMOTO_DATABASE_HOSTNAME +# value: postgres-postgresql.postgres + +## ConfigMap with extra environment variables that used +## +extraEnvVarsCM: + - inji-stack-config + - config-server-share + - artifactory-share + +## Secret with extra environment variables +## +extraEnvVarsSecret: + - google-client + +## Extra volumes to add to the deployment +## +extraVolumes: + - name: mimoto-encryption-volume + persistentVolumeClaim: + claimName: mimoto-keygen-keymanager + +## Extra volume mounts to add to the container +## +extraVolumeMounts: + - name: mimoto-encryption-volume + mountPath: /home/mosip/encryption/encryptionkeystore.p12 + subPath: encryptionkeystore.p12 + +## Add init containers to the pods. +## Example: +## initContainers: +## - name: your-image-name +## image: your-image +## imagePullPolicy: Always +# true # ports: +## - name: portname +## containerPort: 1234 +## +initContainers: + - command: + - /bin/bash + - -c + - if [ "$ENABLE_INSECURE" = "true" ]; then HOST=$( env | grep "mosip-api-internal-host" + |sed "s/mosip-api-internal-host=//g"); if [ -z "$HOST" ]; then echo "HOST + $HOST is empty; EXITING"; exit 1; fi; openssl s_client -servername "$HOST" + -connect "$HOST":443 > "$HOST.cer" 2>/dev/null & sleep 2 ; sed -i -ne '/-BEGIN + CERTIFICATE-/,/-END CERTIFICATE-/p' "$HOST.cer"; cat "$HOST.cer"; /usr/local/openjdk-11/bin/keytool + -delete -alias "$HOST" -keystore $JAVA_HOME/lib/security/cacerts -storepass + changeit; /usr/local/openjdk-11/bin/keytool -trustcacerts -keystore "$JAVA_HOME/lib/security/cacerts" + -storepass changeit -noprompt -importcert -alias "$HOST" -file "$HOST.cer" + ; if [ $? -gt 0 ]; then echo "Failed to add SSL certificate for host $host; + EXITING"; exit 1; fi; cp /usr/local/openjdk-11/lib/security/cacerts /cacerts; + fi + env: + - name: ENABLE_INSECURE + value: "true" + envFrom: + - configMapRef: + name: global + image: docker.io/openjdk:11-jre + imagePullPolicy: Always + name: cacerts + resources: {} + securityContext: + runAsUser: 0 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /cacerts + name: cacerts + +## Add sidecars to the pods. +## Example: +## sidecars: +## - name: your-image-name +## image: your-image +## imagePullPolicy: Always +## ports: +## - name: portname +## containerPort: 1234 +## +sidecars: {} + +persistence: + enabled: false + storageClass: nfs-csi + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack). + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + ## ReadWriteMany not supported by AWS gp2 + ## We use EFS storage class as that supports ReadWriteMany. Make sure you have installed the same as given + ## here: https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html + ## Packet landing zone requires ReadWriteMany + accessModes: + - ReadWriteMany + size: 1Gi + existingClaim: + # Dir where all incoming packets are stored + # Make sure this matches with what is given in the config properties + mountDir: /mnt/landing + labels: + purpose: landing-folder + reclaimPolicy: Retain + +## Init containers parameters: +## volumePermissions: Change the owner and group of the persistent volume mountpoint to runAsUser:fsGroup values from the securityContext section. +## +volumePermissions: + enabled: true + image: + registry: docker.io + repository: mosipint/os-shell + tag: "12-debian-12-r46" + pullPolicy: Always + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + pullSecrets: [] + ## - myRegistryKeySecretName + ## Init containers' resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: + ## We usually recommend not to specify default resources and to leave this as a conscious + ## choice for the user. This also increases chances charts run on environments with little + ## resources, such as Minikube. If you do want to specify resources, uncomment the following + ## lines, adjust them as necessary, and remove the curly braces after 'resources:'. + ## + limits: {} + ## cpu: 100m + ## memory: 128Mi + ## + requests: {} + ## cpu: 100m + ## memory: 128Mi + ## + +## Specifies whether RBAC resources should be created +## +rbac: + create: true + +## Specifies whether a ServiceAccount should be created +## +serviceAccount: + create: true + ## The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the fullname template + ## + name: mimoto + +## Prometheus Metrics +## +## TODO: Enable when prometheus url is available +metrics: + enabled: true + ## Prometheus pod annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: + prometheus.io/scrape: "true" + + endpointPath: + + ## Prometheus Service Monitor + ## ref: https://github.com/coreos/prometheus-operator + ## + serviceMonitor: + ## If the operator is installed in your cluster, set to true to create a Service Monitor Entry + ## + enabled: true + ## Specify the namespace in which the serviceMonitor resource will be created + ## + # namespace: "" + ## Specify the interval at which metrics should be scraped + ## + interval: 10s + ## Specify the timeout after which the scrape is ended + ## + # scrapeTimeout: 30s + ## Specify Metric Relabellings to add to the scrape endpoint + ## + # relabellings: + ## Specify honorLabels parameter to add the scrape endpoint + ## + honorLabels: false + ## Used to pass Labels that are used by the Prometheus installed in your cluster to select Service Monitors to work with + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec + ## + additionalLabels: {} + + ## Custom PrometheusRule to be defined + ## The value is evaluated as a template, so, for example, the value can depend on .Release or .Chart + ## ref: https://github.com/coreos/prometheus-operator#customresourcedefinitions + ## + prometheusRule: + enabled: false + additionalLabels: {} + namespace: '' + ## List of rules, used as template by Helm. + ## These are just examples rules inspired from https://awesome-prometheus-alerts.grep.to/rules.html + # rules: + # - alert: RabbitmqDown + # expr: rabbitmq_up{service="{{ template "rabbitmq.fullname" . }}"} == 0 + # for: 5m + # labels: + # severity: error + rules: [] + +## Requires public access. +istio: + enabled: true + gateways: + - inji-web-gateway + prefix1: /v1/mimoto + prefix2: /residentmobileapp + rewrite: /v1/mimoto + +enable_insecure: false + +volumes: + secrets: + mimotooidc: + key: oidckeystore.p12 + path: /home/mosip/certs + +mimoto: + secrets: + google-client: + INJI_INJIWEB_GOOGLE_CLIENT_ID: "your-app-google-client-id" + INJI_INJIWEB_GOOGLE_CLIENT_SECRET: "your-app-google-client-secret" diff --git a/partner-onboarder/README.md b/partner-onboarder/README.md new file mode 100644 index 000000000..1ef9f8d24 --- /dev/null +++ b/partner-onboarder/README.md @@ -0,0 +1,45 @@ +# Partner Onboarder + +## Overview +* Upload certificate for default partners. Refer [mosip-onboarding repo](https://github.com/mosip/mosip-onboarding). + +## Install +* Create a directory for onboarder on the NFS server at `/srv/nfs//onboarder/`: +``` +mkdir -p /srv/nfs/mosip//onboarder/ +``` +* Ensure the directory has 777 permissions: +``` +chmod 777 /srv/nfs/mosip//onboarder +``` +* Add the following entry to the /etc/exports file: +``` +/srv/nfs/mosip//onboarder *(ro,sync,no_root_squash,no_all_squash,insecure,subtree_check) +``` +* Set `values.yaml` to run onboarder for specific modules. +* run `./install.sh`. +``` +./install.sh +``` +# Troubleshootings + +* After completion of the job, a very detailed `html report` is prepared and stored in minio as part of onboarding bucket. + +* The user can go and view the same for more information or response messages. + +### Commonly found issues + + 1. KER-ATH-401: Authentication Failed + + Resolution: You need to provide correct secretkey for mosip-deployment-client. + + 2. Certificate dates are not valid + + Resolution: Check with admin regarding adding grace period in configuration. + + 3. Upload of certificate will not be allowed to update other domain certificate + + Resolution: This is expected when you try to upload `ida-cred` certificate twice. It should only run once and if you see this error while uploading a second time it can be ignored as the cert is already present. + + + diff --git a/partner-onboarder/copy_cm.sh b/partner-onboarder/copy_cm.sh new file mode 100755 index 000000000..d4a95fa07 --- /dev/null +++ b/partner-onboarder/copy_cm.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Copy configmaps from other namespaces +# DST_NS: Destination namespace + +COPY_UTIL=./copy_cm_func.sh +DST_NS=$( printenv NS ) + +$COPY_UTIL configmap inji-stack-config default $DST_NS +$COPY_UTIL configmap keycloak-env-vars keycloak $DST_NS || { echo "Ignore this error if Keycloak is deployed externally and its details have been added to values.yaml. If not, please update values.yaml as per the instructions in README.md."; true; } +$COPY_UTIL configmap keycloak-host keycloak $DST_NS || { echo "Ignore this error if Keycloak is deployed externally and its details have been added to values.yaml. If not, please update values.yaml as per the instructions in README.md."; true; } diff --git a/partner-onboarder/copy_cm_func.sh b/partner-onboarder/copy_cm_func.sh new file mode 100755 index 000000000..7b2259486 --- /dev/null +++ b/partner-onboarder/copy_cm_func.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Copy configmap and secret from one namespace to another. +# ./copy_cm_func.sh [name] +# Parameters: +# resource: configmap|secret +# name: Optional new name of the configmap or secret in destination namespace. This may be needed if there is +# clash of names + +if [ $1 = "configmap" ] +then + RESOURCE=configmap +elif [ $1 = "secret" ] +then + RESOURCE=secret +else + echo "Incorrect resource $1. Exiting.." + exit 1 +fi + + +if [ $# -ge 5 ] +then + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $5 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | sed "s/name: $2/name: $5/g" | kubectl -n $4 create -f - +else + kubectl -n $4 delete --ignore-not-found=true $RESOURCE $2 + kubectl -n $3 get $RESOURCE $2 -o yaml | sed "s/namespace: $3/namespace: $4/g" | kubectl -n $4 create -f - +fi + + + + + diff --git a/partner-onboarder/copy_secrets.sh b/partner-onboarder/copy_secrets.sh new file mode 100755 index 000000000..6c46332b3 --- /dev/null +++ b/partner-onboarder/copy_secrets.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copy secrets from other namespaces +# DST_NS: Destination namespace + +COPY_UTIL=./copy_cm_func.sh +DST_NS=injiwebDST_NS=$( printenv NS ) +#$COPY_UTIL secret s3 s3 $DST_NS +$COPY_UTIL secret keycloak keycloak $DST_NS || { echo "Ignore this error if Keycloak is deployed externally and its details have been added to values.yaml. If not, please update values.yaml as per the instructions in README.md."; true; } +$COPY_UTIL secret keycloak-client-secrets keycloak $DST_NS || { echo "Ignore this error if Keycloak is deployed externally and its details have been added to values.yaml. If not, please update values.yaml as per the instructions in README.md."; true; } diff --git a/partner-onboarder/delete.sh b/partner-onboarder/delete.sh new file mode 100755 index 000000000..30e2f5fd8 --- /dev/null +++ b/partner-onboarder/delete.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Uninstalls partner-onboarder helm +## Usage: ./delete.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + +function deleting_onboarder() { + NS=injiweb + while true; do + read -p "Are you sure you want to delete all partner-onboarder ?(Y/n) " yn + if [ $yn = "Y" ]; then + echo Deleting mimoto-partner-onboarder helm + helm -n $NS delete mimoto-partner-onboarder + break + fi + done + return 0 +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +deleting_onboarder # calling function diff --git a/partner-onboarder/install.sh b/partner-onboarder/install.sh new file mode 100644 index 000000000..033711df3 --- /dev/null +++ b/partner-onboarder/install.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# Onboards default partners +## Usage: ./install.sh [kubeconfig] + +if [ $# -ge 1 ] ; then + export KUBECONFIG=$1 +fi + + function installing_onboarder() { + + read -p "Is values.yaml for onboarder chart set correctly as part of pre-requisites? (Y/n) : " yn; + if [[ $yn = "Y" ]] || [[ $yn = "y" ]] ; then + NFS_OPTION='' + S3_OPTION='' + config_complete=false # flag to check if S3 or NFS is configured + while [ "$config_complete" = false ]; do + read -p "Do you have S3 details for storing Onboarder reports? (Y/n) : " ans + if [[ "$ans" == "y" || "$ans" == "Y" ]]; then + read -p "Please provide S3 host: " s3_host + if [[ -z $s3_host ]]; then + echo "S3 host not provided; EXITING;" + exit 1; + fi + read -p "Please provide S3 region: " s3_region + if [[ $s3_region == *[' !@#$%^&*()+']* ]]; then + echo "S3 region should not contain spaces or special characters; EXITING;" + exit 1; + fi + read -p "Please provide S3 bucket: " s3_bucket + if [[ $s3_bucket == *[' !@#$%^&*()+']* ]]; then + echo "S3 bucket should not contain spaces or special characters; EXITING;" + exit 1; + fi + read -p "Please provide S3 access key: " s3_user_key + if [[ -z $s3_user_key ]]; then + echo "S3 access key not provided; EXITING;" + exit 1; + fi + read -p "Please provide S3 secret key: " s3_secret_key + if [[ -z $s3_secret_key ]]; then + echo "S3 secret key not provided; EXITING;" + exit 1; + fi + S3_OPTION="--set onboarding.configmaps.s3.s3-host=$s3_host --set onboarding.configmaps.s3.s3-user-key=$s3_user_key --set onboarding.configmaps.s3.s3-region=$s3_region --set onboarding.configmaps.s3.s3-bucket-name=$s3_bucket --set onboarding.secrets.s3.s3-user-secret=$s3_secret_key" + push_reports_to_s3=true + config_complete=true + elif [[ "$ans" == "n" || "$ans" == "N" ]]; then + push_reports_to_s3=false + read -p "Since S3 details are not available, do you want to use NFS directory mount for storing reports? (y/n) : " answer + if [[ $answer == "Y" ]] || [[ $answer == "y" ]]; then + read -p "Please provide NFS Server IP: " nfs_server + if [[ -z $nfs_server ]]; then + echo "NFS server not provided; EXITING." + exit 1; + fi + read -p "Please provide NFS directory to store reports from NFS server (e.g. /srv/nfs//onboarder/), make sure permission is 777 for the folder: " nfs_path + if [[ -z $nfs_path ]]; then + echo "NFS Path not provided; EXITING." + exit 1; + fi + NFS_OPTION="--set onboarding.volumes.reports.nfs.server=$nfs_server --set onboarding.volumes.reports.nfs.path=$nfs_path" + config_complete=true + else + echo "Please rerun the script with either S3 or NFS server details." + exit 1; + fi + else + echo "Invalid input. Please respond with Y (yes) or N (no)." + fi + done + echo "Choose the namespace to run the onboarder in:" + read -p "Enter the custom namespace: " custom_ns + if [[ -z "$custom_ns" ]]; then + echo "No namespace provided; EXITING." + exit 1; + fi + echo "Do you have public domain & valid SSL? (Y/n) " + echo "Y: if you have public domain & valid ssl certificate" + echo "n: if you don't have a public domain and a valid SSL certificate. It will add an ssl certificate in onboarder docker. Only recommended in testing env." + read -p "" flag + + if [ -z "$flag" ]; then + echo "'flag' was not provided; EXITING;" + exit 1; + fi + ENABLE_INSECURE='' + if [ "$flag" = "n" ]; then + ENABLE_INSECURE='--set onboarding.configmaps.onboarding.ENABLE_INSECURE=true'; + fi + + export NS=$custom_ns + CHART_VERSION=0.0.1-develop + + echo Create $NS namespace + kubectl create ns $NS || true + + echo Istio label + kubectl label ns $NS istio-injection=disabled --overwrite + helm repo update + + echo Copy configmaps + kubectl -n $NS --ignore-not-found=true delete cm s3 + sed -i 's/\r$//' copy_cm.sh + ./copy_cm.sh + echo Copy secrets + sed -i 's/\r$//' copy_secrets.sh + ./copy_secrets.sh + + echo Onboarding Mimoto OIDC partner + helm -n $NS install mimoto-partner-onboarder mosip/partner-onboarder \ + $NFS_OPTION \ + $S3_OPTION \ + --set onboarding.variables.push_reports_to_s3=$push_reports_to_s3 \ + --set extraEnvVars[0].name=customnamespace,extraEnvVars[0].value=$NS \ + --set extraEnvVarsCM[0]=global \ + --set extraEnvVarsCM[1]=keycloak-env-vars \ + --set extraEnvVarsCM[2]=keycloak-host \ + $ENABLE_INSECURE \ + -f values.yaml \ + --version $CHART_VERSION \ + --wait --wait-for-jobs + + echo Updating mimoto-oidc-keystore-password value + kubectl -n $NS create secret generic mimoto-oidc-keystore-password --from-literal=mimoto-oidc-keystore-password='mosip123' --dry-run=client -o yaml | kubectl apply -f - + ./copy_cm_func.sh secret mimoto-oidc-keystore-password $NS config-server + + echo Updating Mimoto wallet binding partner api key and Mimoto OIDC Partner Client ID + ./copy_cm_func.sh secret mimoto-wallet-binding-partner-api-key $NS config-server + ./copy_cm_func.sh secret mimoto-oidc-partner-clientid $NS config-server + kubectl -n config-server set env --keys=mimoto-wallet-binding-partner-api-key --from secret/mimoto-wallet-binding-partner-api-key deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + kubectl -n config-server set env --keys=mimoto-oidc-partner-clientid --from secret/mimoto-oidc-partner-clientid deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + kubectl -n config-server set env --keys=mimoto-oidc-keystore-password --from secret/mimoto-oidc-keystore-password deployment/config-server --prefix=SPRING_CLOUD_CONFIG_SERVER_OVERRIDES_ + + kubectl -n config-server rollout restart deployment config-server + kubectl -n config-server rollout status deployment config-server + + echo Reports are moved to S3 under onboarder bucket + return 0 + fi +} + +# set commands for error handling. +set -e +set -o errexit ## set -e : exit the script if any statement returns a non-true return value +set -o nounset ## set -u : exit the script if you try to use an uninitialised variable +set -o errtrace # trace ERR through 'time command' and other functions +set -o pipefail # trace ERR through pipes +installing_onboarder # calling function \ No newline at end of file diff --git a/partner-onboarder/values.yaml b/partner-onboarder/values.yaml new file mode 100644 index 000000000..448d0409f --- /dev/null +++ b/partner-onboarder/values.yaml @@ -0,0 +1,65 @@ +#image: +# registry: docker.io +# repository: mosipdev/partner-onboarder +# tag: develop + +### If you are running Mimoto Onboarder in a separate inji cluster where PMS or Keycloak doesn't exist, +### then uncomment lines 8 to 25 and update the values accordingly. +#extraEnvVarsCM: [] +#extraEnvVarsSecret: [] +#extraEnvVars: +# - name: installation-domain +# value: dev.mosip.net +# - name: mosip-api-host +# value: api.dev.mosip.net + #### pms domain +# - name: mosip-api-internal-host +# value: api-internal.dev.mosip.net +# - name: keycloak-external-url +# value: https://iam.dev.mosip.net +# - name: KEYCLOAK_ADMIN_USER +# value: admin + ###keycloak admin password +# - name: admin-password +# value: 5wIOk3DgKW +# - name: mosip_pms_client_secret +# value: GnUMOiUSJm9oKjYE +# - name: mosip-esignet-host +# value: esignet.dev.mosip.net + +onboarding: + modules: + - name: mobileid + enabled: false + - name: mimoto-keybinding + enabled: true + - name: mimoto-oidc + enabled: true + +# configmaps: +# s3: +# s3-host: 'http://minio.minio:9000' +# s3-user-key: 'admin' +# s3-region: '' +# onboarder-namespace: +# ns_mimoto: mimoto +# ns_esignet: esignet +# ns_signup: signup +# secrets: +# s3: +# s3-user-secret: 'password' +# volumes: +# reports: +# name: onboarder-reports +# storageClass: nfs-client +# accessModes: +# - ReadWriteMany +# size: 10Mi +# existingClaim: +# # Dir where config and keys are written inside container +# mountDir: /home/mosip/reports +# nfs: +# path: "/srv/nfs/sandbox/onboarding" # Dir within the nfs server where config repo is cloned/maintained locally. +# server: "nfs-server" # Ip address of nfs server. +# variables: +# push_reports_to_s3: true diff --git a/performance-test/README.md b/performance-test/README.md new file mode 100644 index 000000000..c53c79c27 --- /dev/null +++ b/performance-test/README.md @@ -0,0 +1,138 @@ + +### Contains +* This folder contains performance Test script of below API endpoint categories. + 01. Create Identities in MOSIP Identity System (Setup) + 02. S03 User downloads MOSIP National ID credential (Preparation) + 03. S01 User accessing Inji Web portal landing page (Execution) + 04. S02 User choosing an issuer and landing on credential types screen (Execution) + 05. S03 User downloads MOSIP National ID credential (Execution) + +* Open source Tools used, + 1. [Apache JMeter](https://jmeter.apache.org/) + +### How to run performance scripts using Apache JMeter tool +* Download Apache JMeter from https://jmeter.apache.org/download_jmeter.cgi +* Download scripts for the required module. +* Start JMeter by running the jmeter.bat file for Windows or jmeter file for Unix. +* Validate the scripts for one user. +* Execute a dry run for 10 min. +* Execute performance run with various loads in order to achieve targeted NFR's. + +### Setup points for Execution + +* We need some jar files which needs to be added in lib folder of jmeter, PFA dependency links for your reference : + + * jmeter-plugins-manager-1.10.jar + * + + kg.apc + jmeter-plugins-manager + 1.10 + + + * jmeter-plugins-synthesis-2.2.jar + * + + kg.apc + jmeter-plugins-synthesis + 2.2 + + + +* eSignet-default properties: Update the value for the properties according to the execution setup. Perform the execution for eSignet api's with redis setup. So check for the redis setup accordingly. + + * Enabling redis cache of eSignet application. + *spring.cache.type=redis + *spring.redis.host=redis-master-0.redis-headless.redis.svc.cluster.local + *spring.redis.port=6379 + *management.health.redis.enabled=false + + * Increasing the expire time to 24 hours for field mosip.esignet.cache.expire-in-seconds + *'clientdetails' : 86400 + *'authcodegenerated': 86400 + *'authtokens': 86400 + +* id-authentication-default.properties: Update the value for the properties according to the execution setup. + *mosip.ida.kyc.token.expire.time.adjustment.seconds=86400 + +### Execution points for eSignet Authentication API's + +*InjiWeb_Test_Script.jmx + + * Create Identities in MOSIP Identity System (Setup) : This thread contains the authorization api's for regproc and idrepo from which the auth token will be generated. There is set of 4 api's generate RID, generate UIN, add identity and add VID. From here we will get the VID which can be further used as individual id. These 4 api's are present in the loop controller where we can define the number of samples for creating identities in which "addIdentitySetup" is used as a variable. + + * S03 User downloads MOSIP National ID credential (Preparation) : This thread generates authcode-token for the download of National ID department PDF. The number of testdata created is depended upon the TPS planned for test execution. The testdata created is non-reusable and expires after 24 hours. + + *Procedure to calculate number of authcodegenerated token values. + * Considering load of 100 TPS volume : As per the request page, 40% Load will be consumed by User downloads MOSIP National ID credential Scenario which is equivalent to 40 TPS. + * User downloads MOSIP National ID credential Scenario consists following 5 endpoints. + */issuers + */issuers/ + */issuers//credentialTypes + */get-token/ + */issuers//credential/download + * Each point url will consume 8 TPS ( 40 TPS / 5 requests). + * If we are executing test only for one hour then number of testdata will be as follows + * Total Number of testdata = TPS value X 3600(number of seconds in one hour) + = 8 X 3600 + = 28800 + * After calculating number of testdata. provide random number of threads in threadgroup (Assuming thread numbers equal to 4 for the sake of calculation), calculate number of iterations as follows + * Number of iterations = Total testdata required / Number of users + = 28800 / 4 + = 7200 + + * S01 User accessing Inji Web portal landing page (Execution) : + * S01 T01 Issuers : This thread executes issuer endpoint. + + * S02 User choosing an issuer and landing on credential types screen (Execution): + * S02 T01 Issuers : This thread executes issuer endpoint. + * S02 T02 Issuer Id : This thread executes eSignet issuer endpoint. + * S02 T03 Credential Types : This thread executes credential type endpoint. + + * S03 User downloads MOSIP National ID credential (Execution): + * S03 T01 Issuers : This thread executes issuer endpoint. + * S03 T02 Issuer Id : This thread executes eSignet issuer endpoint. + * S03 T03 Credential Types : This thread executes credential type endpoint. + * S03 T04 Get Token : This thread connects to eSignet /token endpoint with the authorization code. + * S03 T05 Download File : This thread downloads National ID department PDF file. + +### Downloading Plugin manager jar file for the purpose installing other JMeter specific plugins + +* Download JMeter plugin manager from below url links. + *https://jmeter-plugins.org/get/ + +* After downloading the jar file place it in below folder path. + *lib/ext + +* Please refer to following link to download JMeter jars. + https://mosip.atlassian.net/wiki/spaces/PT/pages/1227751491/Steps+to+set+up+the+local+system#PluginManager + +### Designing the workload model for performance test execution +* Calculation of number of users depending on Transactions per second (TPS) provided by client + +* Applying little's law + * Users = TPS * (SLA of transaction + think time + pacing) + * TPS --> Transaction per second. + +* For the realistic approach we can keep (Think time + Pacing) = 1 second for API testing + * Calculating number of users for 10 TPS + * Users= 10 X (SLA of transaction + 1) + = 10 X (1 + 1) + = 20 + +### Usage of Constant Throughput timer to control Hits/sec from JMeter +* In order to control hits/ minute in JMeter, it is better to use Timer called Constant Throughput Timer. + +* If we are performing load test with 10TPS as hits / sec in one thread group. Then we need to provide value hits / minute as in Constant Throughput Timer + * Value = 10 X 60 + = 600 + +* Dropdown option in Constant Throughput Timer + * Calculate Throughput based on as = All active threads in current thread group + * If we are performing load test with 10TPS as hits / sec in one thread group. Then we need to provide value hits / minute as in Constant Throughput Timer + Value = 10 X 60 + = 600 + + * Calculate Throughput based on as = this thread + * If we are performing scalability testing we need to calculate throughput for 10 TPS as + Value = (10 * 60 )/(Number of users) diff --git a/performance-test/scripts/InjiWeb_Test_Script.jmx b/performance-test/scripts/InjiWeb_Test_Script.jmx new file mode 100644 index 000000000..ecda1cd2c --- /dev/null +++ b/performance-test/scripts/InjiWeb_Test_Script.jmx @@ -0,0 +1,2124 @@ + + + + + + false + false + + + + + + + + + + protocol + https + = + + + serverIP + inji.cellbox1.mosip.net + = + + + serverPortNo + 443 + = + + + schemaVersion + 0 + = + + + serverIPEsignet + esignet.cellbox1.mosip.net + = + + + regProcClientId + mosip-regproc-client + = + + + regProcSecretKey + abc123 + = + + + regProcAppId + regproc + = + + + idRepoClientId + mosip-idrepo-client + = + + + idRepoSecretKey + abc123 + = + + + idRepoAppId + idrepo + = + + + numberOfIdentitiesPerThread + 50 + = + + + serverIPInternal + api-internal.cellbox1.mosip.net + = + + + rampUp + 30 + = + + + testDuration + 3600 + = + + + runTimeFilePath + ./runtime-files + = + + + issuerName + ESignet + = + + + + + + continue + + false + 1 + + 1 + 1 + false + 300 + + true + + + + + + false + import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone;//Set the format for the date-time +SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss"); +dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));// Get the current time in UTC +Calendar cal = Calendar.getInstance(); +String utcTime = dateFormat.format(cal.getTime()); +vars.put("utcTime", utcTime); + + + + true + + + + false + { + "id": "string", + "metadata": {}, + "request": { + "clientId": "${regProcClientId}", + "secretKey": "${regProcSecretKey}", + "appId": "${regProcAppId}" + }, + "requesttime": "${utcTime}Z", + "version": "1.0" +} + = + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + v1/authmanager/authenticate/clientidsecretkey + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + + + + + "message":"Clientid and Token combination had been validated successfully" + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + true + regprocAuthToken + set-cookie: Authorization=(.*?); + $1$ + authToken_not_found + 1 + + + + + true + + + + false + { + "id": "string", + "metadata": {}, + "request": { + "clientId": "${idRepoClientId}", + "secretKey": "${idRepoSecretKey}", + "appId": "${idRepoAppId}" + }, + "requesttime": "${utcTime}Z", + "version": "1.0" +} + = + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + v1/authmanager/authenticate/clientidsecretkey + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + + + + + "message":"Clientid and Token combination had been validated successfully" + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + true + idrepoAuthToken + set-cookie: Authorization=([\w.-]+); + $1$ + authToken_not_found + 1 + + + + + true + ${numberOfIdentitiesPerThread} + + + + ./support-files/center_machine_id_values.csv + + machineID,centerID + true + , + false + true + true + shareMode.all + + + + ./support-files/biometrics_data.txt + + biometrics + false + , + false + true + true + shareMode.all + + + + ./support-files/add_identity_request_details.csv + + firstName,dateOfBirth,gender + true + , + false + true + false + shareMode.all + + + + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + v1/ridgenerator/generate/rid/${centerID}/${machineID} + GET + true + false + true + false + + + + + + + + + Cookie + Authorization=${regprocAuthToken} + + + Content-Type + application/json + + + + + + + "response":{"rid":" + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + false + rid + {"rid":"(.*?)"}," + $1$ + rid not found + 1 + + + + + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + v1/idgenerator/uin + GET + true + false + true + false + + + + + + + + + Cookie + Authorization=${idrepoAuthToken} + + + Content-Type + application/json + + + + + + + response":{"uin":" + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + false + uin + {"uin":"(.*?)"}," + $1$ + uin not found + 1 + + + + + true + + + + false + { + "id": "mosip.id.create", + "request": { + "registrationId": "${rid}", + "identity": { + "IDSchemaVersion": 0.1, + "UIN": "${uin}", + + + "addressLine1": [{ + "language": "eng", + "value": "Line1" + }], + + "addressLine2": [{ + "language": "eng", + "value": "Line2" + }], + + "addressLine3": [{ + "language": "eng", + "value": "Line3" + }], + + "city": [{ + "language": "eng", + "value": "FR" + }], + + + "email": "abc@gmail${__Random(100,999999999999,)}.com", + + "phone": "8249742850", + "dateOfBirth": "${dateOfBirth}", + "postalCode": "14022", + + "fullName": [{ + "language": "eng", + "value": "FR" + }], + + "gender": [{ + "language": "eng", + "value": "${gender}" + }], + + + "province": [{ + "language": "eng", + "value": "FR" + }], + + + + "region": [{ + "language": "eng", + "value": "FR" + }], + + "zone": [{ + "language": "eng", + "value": "FR" + }], + + "proofOfAddress": { + "format": "pdf", + "type": "DOC015", + "value": "proofOfAddress" + }, + + + + "proofOfIdentity": { + "format": "pdf", + "type": "DOC015", + "value": "proofOfIdentity" + }, + + + + "individualBiometrics": { + "format": "cbeff", + "version": 1, + "value": "individualBiometrics_bio_CBEFF" + } + }, + "documents": [{ + "category": "proofOfAddress", + "value": "ZGF0YQ" + }, + { + "category": "individualBiometrics", + "value": "${biometrics}" + } + ] + }, + + + + + + + "requesttime": "${utcTime}Z", + "version": "v1.0" +} + = + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + idrepository/v1/identity/ + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + Cookie + Authorization=${idrepoAuthToken} + + + + + + + {"status":"ACTIVATED + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + ${__StringToFile(credential_password_auth.csv,${identifier}@phone\,${genPassword}\,${fullName}\n,true,)} + + + + groovy + + + true + def data=prev.getResponseDataAsString(); +log.info(data); + + + + + true + + + + false + { + "id": "mosip.vid.create", + "metadata": {}, + "request": { + "UIN":"${uin}", + "vidType": "Perpetual" + }, + "requesttime": "${utcTime}Z", + "version": "v1" +} + = + + + + ${serverIPInternal} + ${serverPortNo} + ${protocol} + + /idrepository/v1/vid + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + Cookie + Authorization=${idrepoAuthToken} + + + + + + + "vidStatus":"ACTIVE" + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + false + vid + "VID":"(.*?)" + $1$ + vid not found + 1 + + + + false + + + ${__StringToFile(${runTimeFilePath}/vid_testdata.txt,${vid}\n,true,)} + + + + + + + continue + + false + 1 + + 1 + 1 + false + 300 + + false + + + + ${runTimeFilePath}/vid_testdata.txt + + vId + true + , + false + true + true + shareMode.all + + + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers + GET + true + false + true + false + + + + + + + groovy + + + true + def starttime="${__time(,)}" +vars.put("starttime",starttime); + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName} + GET + true + false + true + false + + + + + + + false + clientId + client_id":"(.*?)" + $1$ + + 1 + true + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName}/credentialTypes + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + + + + ${serverIPEsignet} + + https + + v1/esignet/csrf/token + GET + true + false + true + false + + + + + + + false + loadCsrfToken + token":"(.*?)" + $1$ + + 1 + true + + + + + token + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + true + + + + false + {"requestTime":"${__timeShift(yyyy-MM-dd'T'HH:mm:ss.SSS'Z',,-PT5H30M,,)}","request":{"nonce":null,"state":"qTFFS9uGVpWnit0SwUGlQU62NRh06GA4wqtMfDpbwRI","clientId":"${clientId}","redirectUri":"https://${serverIP}/redirect","responseType":"code","scope":"mosip_identity_vc_ldp","acrValues":null,"claims":null,"claimsLocales":null,"display":null,"maxAge":null,"prompt":null,"uiLocales":"en","codeChallenge":"${codeChallenge}","codeChallengeMethod":"S256"}} + + + = + + + + ${serverIPEsignet} + + https + + /v1/esignet/authorization/v2/oauth-details + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + X-XSRF-TOKEN + ${loadCsrfToken} + + + Cookie + XSRF-TOKEN=${loadCsrfToken} + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + groovy + + + true + import org.apache.commons.codec.binary.Base64; +import java.security.SecureRandom; +import java.security.MessageDigest; + +SecureRandom secureRandom = new SecureRandom(); +byte[] codeVerifierBytes = new byte[32]; // 256 bits +secureRandom.nextBytes(codeVerifierBytes); + +String codeVerifier = Base64.encodeBase64URLSafeString(codeVerifierBytes); +vars.put("codeVerifier", codeVerifier); +log.info("codeVerifier: " + codeVerifier); + +MessageDigest digest = MessageDigest.getInstance("SHA-256"); +byte[] hash = digest.digest(codeVerifier.getBytes("UTF-8")); +String codeChallenge = Base64.encodeBase64URLSafeString(hash); +vars.put("codeChallenge", codeChallenge); +log.info("codeChallenge: " + codeChallenge); + + + + + false + tidUserInfoPrep + "transactionId":"(.*?)" + $1$ + tidUserInfoPrep not found + 1 + + + + javascript + + + true + // Import the required Java classes +var Base64 = Java.type('java.util.Base64'); +var StandardCharsets = Java.type('java.nio.charset.StandardCharsets'); + +// Retrieve the previous response value +var jsonResponse = prev.getResponseDataAsString(); + +// Print the previous response value +log.info("Previous Response: " + jsonResponse); + +// Parse the JSON response +var jsonObject = JSON.parse(jsonResponse); +var responseObjectString = JSON.stringify(jsonObject.response); + +log.info("Previous Response: " + responseObjectString); + +// Convert JSON to base64 URL-encoded SHA-256 hash +var sha256Hash = hashSHA256(responseObjectString); +var base64UrlEncodedHash = encodeBase64Url(sha256Hash); + +//Print the sha256 value +log.info("sha256Hash Value: " + sha256Hash); + +// Print the final hash value +log.info("Final Hash Value: " + base64UrlEncodedHash); + +// Set the new variable value in JMeter +vars.put("hashValue", base64UrlEncodedHash); + +// Function to compute the SHA-256 hash +function hashSHA256(value) { + var messageDigest = Java.type('java.security.MessageDigest').getInstance('SHA-256'); + var bytes = (value).getBytes(StandardCharsets.UTF_8); + var digest = messageDigest.digest(bytes); + return digest; +} + +// Function to base64 URL-encode the hash value +function encodeBase64Url(value) { + var base64 = Base64.getUrlEncoder().withoutPadding().encodeToString(value); + return base64; +} + + + + + true + + + + false + { + "requestTime": "${utcTime}Z", + "request": { + "transactionId": "${tidUserInfoPrep}", + "individualId": "${vId}", + "otpChannels": ["PHONE"], + "captchaToken" : "" + + } +} + + = + + + + ${serverIPEsignet} + ${serverPortNo} + ${protocol} + + /v1/esignet/authorization/send-otp + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + X-XSRF-TOKEN + ${loadCsrfToken} + + + Cookie + XSRF-TOKEN=${loadCsrfToken} + + + oauth-details-hash + ${hashValue} + + + oauth-details-key + ${tidUserInfoPrep} + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone;//Set the format for the date-time +SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss"); +dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));// Get the current time in UTC +Calendar cal = Calendar.getInstance(); +String utcTime = dateFormat.format(cal.getTime()); +vars.put("utcTime", utcTime); + + + + + + + true + + + + false + { + "requestTime": "${utcTime}Z", + "request": { + "transactionId": "${tidUserInfoPrep}", + "individualId": "${vId}", + "challengeList" : [ + { + "authFactorType" : "OTP", + "challenge" : "111111", + "format" : "alpha-numeric" + } + ] + } +} + = + + + + ${serverIPEsignet} + ${serverPortNo} + ${protocol} + + /v1/esignet/authorization/v3/authenticate + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + X-XSRF-TOKEN + ${loadCsrfToken} + + + Cookie + XSRF-TOKEN=${loadCsrfToken} + + + oauth-details-hash + ${hashValue} + + + oauth-details-key + ${tidUserInfoPrep} + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone;//Set the format for the date-time +SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss"); +dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));// Get the current time in UTC +Calendar cal = Calendar.getInstance(); +String utcTime = dateFormat.format(cal.getTime()); +vars.put("utcTime", utcTime); + + + + + + + true + + + + false + { + "requestTime": "${utcTime}Z", + "request": { + "transactionId": "${tidUserInfoPrep}", + "permittedAuthorizeScopes": [], + "acceptedClaims": [] + } +} + = + + + + ${serverIPEsignet} + ${serverPortNo} + ${protocol} + + /v1/esignet/authorization/auth-code + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + X-XSRF-TOKEN + ${loadCsrfToken} + + + Cookie + XSRF-TOKEN=${loadCsrfToken} + + + oauth-details-hash + ${hashValue} + + + oauth-details-key + ${tidUserInfoPrep} + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + false + codeTokenEndpoint + "code":"(.*?)" + $1$ + Code not found + 1 + + + + false + + + ${__StringToFile(${runTimeFilePath}/authcode_token.txt,${codeTokenEndpoint}\,${codeChallenge}\,${codeVerifier}\n,true,)} + + + + + + continue + + false + -1 + + 30 + ${rampUp} + true + ${testDuration} + + true + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + 2 + + throughput + 1800.0 + 0.0 + + + + + + continue + + false + -1 + + 30 + ${rampUp} + true + ${testDuration} + + true + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName} + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName}/credentialTypes + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + + throughput + 1800.0 + 0.0 + + 2 + + + + + continue + + false + -1 + + 40 + ${rampUp} + true + ${testDuration} + + false + + + + ${runTimeFilePath}/authcode_token.txt + + codeTokenEndpoint,codeChallenge,codeVerifier + true + , + false + false + true + shareMode.group + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName} + GET + true + false + true + false + + + + + + + false + clientId + client_id":"(.*?)" + $1$ + + 1 + true + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + true + + + + + + + ${serverIP} + + https + + v1/mimoto/issuers/${issuerName}/credentialTypes + GET + true + false + true + false + + + + + + + + "errors":[] + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + false + true + + + + + + + false + authorization_code + = + true + grant_type + + + false + ${codeTokenEndpoint} + = + true + code + + + false + https://${serverIP}/redirect + = + true + redirect_uri + + + false + ${codeVerifier} + = + true + code_verifier + + + false + ${clientId} + = + true + client_id + + + false + urn:ietf:params:oauth:client-assertion-type:jwt-bearer + = + true + client_assertion_type + + + + ${serverIP} + ${serverPortNo} + ${protocol} + + v1/mimoto/get-token/${issuerName} + POST + true + false + true + false + + + + + + + + + Content-Type + application/x-www-form-urlencoded + + + + + + + access_token + + + Assertion.response_data + false + 2 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + false + accessToken + "access_token":"(.*?)" + $1$ + access_token not found + 1 + + + + + + false + true + + + + + + + ${serverIP} + ${serverPortNo} + ${protocol} + + v1/mimoto/issuers/${issuerName}/credentials/MOSIPVerifiableCredential/download + GET + true + false + true + false + + + + + + + + + Content-Type + application/x-www-form-urlencoded + + + Bearer + ${accessToken} + + + + + + + PDF + + + Assertion.response_data + false + 16 + + + + + 200 + + + Assertion.response_code + false + 2 + + + + + + + throughput + 2400.0 + 0.0 + + 2 + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + .*Endpoint.* + + + + true + false + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + 500 + false + + .*Endpoint.* + + + false + true + + + + + diff --git a/performance-test/support-files/add_identity_request_details.csv b/performance-test/support-files/add_identity_request_details.csv new file mode 100644 index 000000000..27d3063d0 --- /dev/null +++ b/performance-test/support-files/add_identity_request_details.csv @@ -0,0 +1,10 @@ +firstName,dateOfBirth,gender +Sabya,1995\/05\/07,Male +Vishwa,1972\/03\/04,Male +Siva,1975\/05\/06,Male +Reeba,1982\/05\/08,Female +Manoj SP,1972\/05\/07,Male +Priya,1982\/05\/07,Female +Riya,1992\/05\/07,Female +Arun,1992\/03\/07,Male +Arvind,1993\/03\/06,Male diff --git a/performance-test/support-files/biometrics_data.txt b/performance-test/support-files/biometrics_data.txt new file mode 100644 index 000000000..099e07f32 --- /dev/null +++ b/performance-test/support-files/biometrics_data.txt @@ -0,0 +1 @@ +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI_Pgo8QklSIHhtbG5zPSJodHRwOi8vc3RhbmRhcmRzLmlzby5vcmcvaXNvLWllYy8xOTc4NS8tMy9lZC0yLyI-CiAgICA8QklSSW5mbz4KICAgICAgICA8SW50ZWdyaXR5PmZhbHNlPC9JbnRlZ3JpdHk-CiAgICA8L0JJUkluZm8-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT43PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MFo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-RmluZ2VyPC9UeXBlPgogICAgICAgICAgICA8U3VidHlwZT5MZWZ0IFJpbmdGaW5nZXI8L1N1YnR5cGU-CiAgICAgICAgICAgIDxMZXZlbD5SYXc8L0xldmVsPgogICAgICAgICAgICA8UHVycG9zZT5FbnJvbGw8L1B1cnBvc2U-CiAgICAgICAgICAgIDxRdWFsaXR5PgogICAgICAgICAgICAgICAgPEFsZ29yaXRobT4KICAgICAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPkhNQUM8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgICAgICA8VHlwZT5TSEEtMjU2PC9UeXBlPgogICAgICAgICAgICAgICAgPC9BbGdvcml0aG0-CiAgICAgICAgICAgICAgICA8U2NvcmU-NjQ8L1Njb3JlPgogICAgICAgICAgICA8L1F1YWxpdHk-CiAgICAgICAgPC9CREJJbmZvPgogICAgICAgIDxCREI-UmtsU0FEQXlNQUFBQVdEYkFBRUJBUUFCWU1zSDVnd2NEQjRQQVZrQUFBQUFBQUV4QUVBQUR3SUFRQUlBUUFJSkFBRUI5QUgwQWZRQjlBZ0ZIUUQ4QWM0QUFXQmFBQUFBREdwUUlDQU5Db2NLQUFBQUZHWjBlWEJxY0RJZ0FBQUFBR3B3TWlBQUFBQkhhbkF5YUFBQUFCWnBhR1J5QUFBQnpnQUFBUHdBQVFjSEFBQUFBQUFQWTI5c2NnRUFBQUFBQUJFQUFBQWFjbVZ6SUFBQUFCSnlaWE5qbWNvQUFabktBQUVBQUFBQUFBQnFjREpqLzAvL1VRQXBBQUFBQUFEOEFBQUJ6Z0FBQUFBQUFBQUFBQUFBL0FBQUFjNEFBQUFBQUFBQUFBQUJCd0VCLzJRQUl3QUJRM0psWVhSdmNqb2dTbUZ6VUdWeUlGWmxjbk5wYjI0Z01TNDVNREF1TWY5a0FCQUFBVVZ1WTBsRU9rZFNRbFF3TWY5U0FBd0FBZ0FKQUFVRUJBQUIvMXdBRTBCQVNFaFFTRWhRU0VoUVNFaFFTRWhRLzVBQUNnQUFBQUZmWWdBQi81UGZHeEFPelpINllyNnU5bisrbEtwZ3ZleUxHR3piZjlocHNZSGlHTlNCK3htTWVqV09yS2d3S2dXTDJTbXc2ejZ3SU5oZGdJQ0E4b2pleEZBQ1dON0tOLzBhM2tNS3Fnd3VLNENBOFFBMVBJQldRZkE1bTN3RmxrNzZTaTg4L0FlQUwxWmFNSnlhRmt3Q05wY0xCOVJxbnBNZGw3Qld3ZGJMdXZHdGpRcS9nTWRVQURnZ3FnSWNIZEwrSGxqeXpId1hzcVZ0MzM0ZW84eENxQ2E2SG9qb29ZdnZQeDdOZXIxNnA1azFCMnN6VmovblJoNXZkeTFIU3A0Mi9BSFZKZ0M5M1huWW1yS1cwY3NZVE9sRzBVWm4wMnVZWC9vSThTeWNRSzczNVhTaTJ5ZnhuTHJNNEV0NExGNHZtOUZuZFFad2szSkxCSmdnOStyTHN3WUNxdWNBaWNrcFArcFEyUWMxWmFZRlhoNnU4ZlZLUUFncVdoT0FYc1lBNE9Yd3dyOFM0bUpHdFZ1azBXTEZUSXJ3VGlGOUJXWjlPRkp0MlB3cXZqTGZtUENRUG0xYkRZenVlNThtU0VmdWk1bU9GYXVZQmNuMnkyQ3Nac3UxdkUvdVdRMmpVa05RUmpGZHRyOExwV3RVSDlhaWpISkVtQnZxMXdReGVIcDZoSEhqaHBUaGNhcXEzU0RicVF0MUZUVzQvZUx6U1ZGQ09qRnpBclFpYTA0VlcwR0lORVBlS3BBOFJpZEZKNStnTjh4MUhpU2VYYkRqdWlJV3BPYjZuWXlYbi9IM2Q4eHBvdzhaM2pkMEVpaFQ5dlZJbWRlbXRSYlF1b2RhdkxLTG10UnladDZTYlNwWVBBQ3pSL2RLQ2t0QTBIWSthL0wrWWIvOFV4WTV2UXlvZ1QycFlwY1B2OVpqQ1pOdFNiS0F1bW1EeG5zeUUyS0d2RHVBT00wVVRObCsvdXJPMFlCdE5teG9QMWsxbERpRlBmMS9yVkIwbjdnU2hncHhnSndtQ0o0VE5sMFI4c0s5VXEwRlFtWjBLTExCdkJremZuUWVUSnhIaFFrVUdLMjBZbkMxdjZRRWpjajVUSEdMcnZjd1Q5Qlh3STZ2Nllsc29KN1lxSUxZVGR6VDhja1I4TXNtNGkzZkhINE04dmNOWWRCOTJVVmI0NHVDb2RoREIyVTYxcmpZNll3SzlaczBhcksydG1XeVVvL0FCN2tsMWpTZFZobHpEcGxGdUc4dThNQnFQb0xXdU9Cd1d5T0o4WVFOWmZya1FEaGF5c1k0YTEycjVSTmgyakF3VWJ4UU16bEhYU3kwcm05cHdHMG9OeCtJaEdQRkRHZEJLeVU1YkpJZENWeCtNYnZicWRicGlQb2hKTHRDeVBjNHpSNDJZTlVYN29qQWN3VjVUcDJ5eWlyYldoQWJDV2FjeGJBYzl3bTQ1ZEI4d0ZNK3hmakFEa25kTk91SXh3eHRnM1llTzl0ZmtTWWlQLzE1UVJXRkU1NnB1cVBST1FvZ1dpcUcvcHRMOHc4YWpGb3BzOVAzTXp3L2ZlWlJ5WllLdytWZ3RiNTBVdkVEOFl6S055Zk80ZnhLbVZWd2I2L3pzUDZCOE5ia2tobEVCd09qNUpORkRYdklWakVZNFZhUXk4YjNqc3FtcDRGSFpCamlWZUlNTmNOd0RIUUQrR0RzZi9aSXhKeC9mSHB1OTFpdkFFT1h2UTVaQUhPYmM0MXFpZ0RudUpLOGNOQjVFQ3luT2pqRnBXdDFNQjhpM0FsdFFHZHpQQnViUmViNDhNVkZGNzgvY0ZlcDR6K2FYQlhaV0xDOXo0cW1kcFNnTXFJYjQwRmNtd3k3VDFuNHJEaHc4Y29OeDU0THNNRmhuQlA4aHQvS2Yvem8yTTVpRFI2TGlJRTBTeGo1bnVlYmRkUkplbWxRbE1DbGFGbDlvN3VyZWdRZTBzMC9ieGNzaitjUFFyNWQzRnoxQ0JtNk1GVmhoejV4V1NXV2UvOVNUVFpPNkx1NVZZWi9LVTA3Q05NRTErcWxaejN1YldQRWpjWWxTL0VXdkFLOGJGYmZuQ3VVYjdhbjZPZjF0OVVKYWlRSkVBbWV6aVREVUozRnE1YXZ1TVBTZm1SKy8ycWw4QkZZRTBlek5ObmZlTEpmTytySHFkT3lqeEIvUXE5V01WQVd0T1VjRm5HYkNHdDRsWlJJRHEvZmZ1WHVWdk0xcTVKL3hPdWt2YUxuaHdkOXhBMVcrL2x4MnNFL1JnTnVzV01tMjVPdmlON0o5Tm1MTDR1UzNiem91VU9XNDVwaUx0cGxidDJIS3JKTTF0ekh5eWR6ODJ2TVRvZ01qYW1KMDBSY3RabFV6RDJ0SmRJM1hEOEdueWdIMUNTaWhSdHVvUWNOSWVrenlma2htd0Nnc1dudjRjbEZ4NzMzbVBZUVZBMEtXRFlyeHE4aHg4ZkRNZ0lvQk15V0xmU3VuOFE5T3ZBdE13NmdDS0N3RGVuRzhUZUZVK3R3MGZPNGR2dSs1TG5MU2lvYk5lbTZ1bTdhYzNLaE4rMUpONVVXV2dxNFdQOHFBYnYzQVpjMFJOSFRYcWoxYWhWekZNbG1YWEVkU3daUEFQWFRFOFVNNTlDVFVOdE91VkJyY0RSZEEvYmpRc0hJM2VOczVKSzRHVHF2a1Vud0p2b1YxSGVYcFlHdjFaSkNsaUNCWVVJOXFydGcvU2NCVlhZVDZXY1h0STRWdVYzR3g0Um9aR0JrMk91SVg2azQrcDh0UVc5L29aV2JzSUJQelA0cWo4SHhCeENUWGJocFRzb25BbjEwK0VsTlAweDhLOG9TbEcvd0dyVjFIdVRzYW0xZnRqSjZoWWwyWnhvckxmQ1d5OU13SEZ1eDNSTjI5YjBCTTYvVEwrUUUxSWpmbjdGTUlGaGQ1Q21JckJHeFZlTFFCNDllUDRnMWYvQXQxelRpRlh4MXJlVVVFVk1UcTE2VkFlMlkxWEF1NEpBZHhUUjZWc0FYZEU2Z29SYlI5N3Z6SllIbm5oQlFoQWtHYm54UlhWaWxFN1J6bStwOWoxNlRQUW1aU2F5Z0xJc0FXdy85R0kvanp2TWhzV2F0QWxvZjhjY1JOUzdteHZwcVJYMS80UkJoM1BnTXlMTVpOVXlHdFh0SkJmVnFpMElJa25uUUt6KzZyN1ZNNDg1b0QrWWNkU2pzcUlvM0x0cjYwRkF4Uzdxa1c3MGpuajcxYjIvYTNibTA3cE9BeTk0QWdldzU3c1B0SnpQT2h3QVZMdW0xTkdrdkQwenkxN3lTL0ZZSXdlVkM2VU5HM2cvYlVyVDZMVUhZL3ZpaXNPRTB3clZYTnJCNC9yQVFxakRYYVZOZENMSFY5MlVQaE4wOEZxR2dYSUFtODltNmM3Y0EwdHdmaHVxR2xDamoxQU8vWWRkRTdYNEpCaHlKRHh0bnNQY1FwVnl5RkJuZGUvOWl6ZkwwWUpnNzFoK0dlcC9ScTBZVDZjWUhjZ3QzWjRzc3NiMUtzL1pMbVZjbnkxQm5QSDNjU2JQL0tUMUIyT1Z0SGkxZG91RkJBUDhJTlBuQ21jY3pIdWVoNldmcHNHSDdYNUlWUWVKdm9lemMwL2lzbEdGZ2loRFpTQ2gyS0diNkR6M1g4R0pIcE1oWWd6K1pkT2JvK25zZERVMnhZdm5qRjhhV0Nzbnhxc2diZG81WDV6Tzc4VjNDVDhjSGI4Y2pxYzgzdUg5TTRuL3dZZmd1UXBjaEQ4c1RDeFJ5MG12bGxEMXZGUFNrWVJaelJnSS9NTGthM2x2QitOczJ0QzZ4NXA3SEorWUlDbmlBNm10YWRpY08rVzBWcldRTE56ZmtCanhIV2MxUGpyeSs1d3dQc001OExCeEdxaFNiWmdybXRtS2c1VlpUNVhxNStMSzNQNjhDYms0Sk84RFBPQUZ3aTFWbEhYdnNMMkUycUNEd3VOYTRzMDZvTzdvSVlOd0ltL1dLN2c5SWxEcmtvMkVJMnFwbUpQQ2ZNRFNXTVI0c1poVjdrUkdVblA4N2NMWXQzdlFjaXZDYy9yRFR1a3BoMVhYcVlMcDZKTUdoamxSaGxjUlMwblNHSzlGSFduaklzeDZyZ0RRa2N3NjB4dzc1QkJOSTBRSU9OUnE3THdpRkM1QzVTWHN4NGltYmNPdFFWL2lBRHlMSkxOUnF3UG0vbHJxb0NMMFVvVnNyK01MUjl6cmJyV3FmVm1NbDZvMEpyVElqaDl0TGh3ZkREQWlpVEdzUUpVZVM1TCtIZERHN1I2MlRBWHJCUVVRbXNlbVEvZGJPMXNpOTJNU1V6eEV4bm5UaStjQkpEVGlwWURvUENkeGlqaHFEOGZ2R0lJS3kvTmJ5YjdnNndBUm9WS0xJNlJ5MzJFamxjdENKZDVNejU5RDRBa2hDa1RRS2tuSjQxdTBFMUE4WUxvTVlGU1NGWldJOFUxUjlRSG5waFFhdUNQS0w2MnduK01uRlZjK1RucDNGcWVGU0Y0WkJuSWlpaXp2NTlsbTBhcytGb0dnOFZ4anY5bTNkLzNzempmaVREUWcyNHlteWc2clFjNDhSeGZZUUFURjBZV2EwTUF4dmlkMndMZTlpT1lqUjdISysyeEhvemNaRU5PUmFXMlV0VjRGZVgwc3JPaWF0Y0NpSGROd0plY1BYL3MvaTFLNlJqdCtxZmtvUnhtYURvZWdmTnhlUmZXVmEvQUNIOG52bVNEY1lUN1hKL2xMWXBkTEtNbWVJYkhvWkdacE0wZE1FT2FCam9IdVdFeWxSMjUzS0kxeDNRSXJEZEhnK0VNMHphTUNLbFgybDVZbEp6SGlpejh1bzJWQTJpS0ZsYSsrZjI1dHVidWxOVVpzaWY3R2JvZXdVMkpZUk9yWUExTHVXOE4xSjlaVk5QNnpKTDZpeVBONG14dHl2ZC80WjBWOTcya2lQdFE4NHErWHNSMk9LUFBlK0MxVzliU0RsdklmSEw1YW1lbFUvTkZjOUlEQ2Nyc0NQZDlrRFdVL1ZyYVJSTDQ0eEM3aDNIaUlWTllwMVhkejBDRnIwUW9LWFFidmhuckV1cHBXb3NHOUhFdjVTbDZkemRjNklrUXFKTGtmMjVDNGNZV0N0VDgzbnJOc1JoZDIwWXNBS2N1TTE2U0E3bVo0UTRyZ3lIeXRIRlNES3VUbnpUNG9ucGFqR0JpR1JER0QvZEd3bG5HNDY4WjBURUxCcEd2VERXbWlrOW4xTGtVaSsyOU5yTFVVUmJZNHNoNERZbGIvd1dveGhTRFNXdXQwZ0ljajZyb1Z3TTl2UjVEZGNnV0xqNjhxZE9uZWhoNmJaOGFpdFlkYUR0NGRWRG1oc2JNWEQyNkhGSnVIL1ZDSENyMHZEZHBrbDhmR1JVYzJ5ZC9QcHl5QkVnRDRpZnIwd0FLL09odFJpR1I0aGFQZVROa0p1ZXNUeU1CWFlROVJLMlIzWlB0WU9aemxOMUVFQ0FtYnRDMlIveUY3MkUvcFVSVXdKUkJITEpFcEZnaUFrNGI4YnFjbWhZRnpSTVI0R091VE1xUHg3eE82RHNYM3cyY0t0WUlBd1huSmFrSVZCWmdudHlhcTFjYUlwWTRQVzVRZEpIcG42WjRXdnRrTDhmdkxhVUltbDdPQXVEKzJERnVHTCtWalh0L0pBaHJTTVNzMXN2eG5PWkU2ZWdSVkdlQTcyTFNtOVZkL1NNNElWQlIzbUJyRDE0c0cwTTd6SjBaWjY4elpySE1qL2NwMkRNTllUT0FHL1c2NldJay9rQkdkaDE3S29SL1hBNlEwMDRPbFREcEhNNUs4OTB5ZGVscm5BbkU0U00yQ1IrT1lJdkZrL3U1WFQzTkc3UGw1aklQeVR2ZDYrZ0NJYk12WmhjSVdHZTJhQlZSMjdSR0RVQkRXQWR1YnUrRUpmcElqeEs2d2FCZjk1NHhadTAyWENTZk9oQWJSU1hVZXZMc3dpMjlHQVh0d0x5NFhHZURjYW9nVzFCd2dpMENhTHlibWJFMGdLSGZyRTNTVUhFQVI3MnpKYXFqTTdnWEg3VFdQZ3V5QWRHUWEzbm5sYThEOFJtaXEvcHZMOHZ4Nmw3eG84TjJQbUQyZkVOZDlkTGJCS0hHYkZtd2JFaGlPc1p6YS9PWi9OMFFTWklqOU8wL0luR2twQjJYMWlDYzk2bHNoYnJwRlY3UG9Kc1RjME54aEhvaUxzYXFwMXNSdUp4SUpxek1rVmlQWjBCaG16OEx3K1ZNaWhaZDBsd0t1QUFDUVFNQ2ZxeXlyNWFMVUk2WjRZak9McWNiaktwL1RwaDZ6K1c1S2RjWXhSdU5RR3UrTzR6N0Q2d2dyc3ppakpUS2t4a3N2Ymk0aktwOWpnTzh2SVdRNWdDVVpOaXo0UW92RUxZN0dyQkh5dW5hbFBhNklPWXFRaXpWMzV5M1IvNDVJRHlUZ2RueG1BYnd2OUNFZjBKbGZXVHdDandDUEEwSVM0a2JtTDgzbnJ6S0c5L0MrSTJOQkR2MjF0NGovUXc1VE01ZWhNcHA5bTFkYUNyWlMzS05wMVNObVk1Sjc2bk5ZdUVHREpEblZSOW4xY095TWtCVTNiM0RqdGtEbGtsTEVtY1lKbnpoczZzaGJWK1A3YlF3TXdVcllDdkVHdDBIbXFsY2syMDVEd2lTektCT1NwMDRwQkY2MFRXK2szOXcrRnJvbTk4U2FTMEppaHpwM2ZIMDYvTC9TSXJ3NWRMOGhvNTcxZVNkS1d4emRLRzJPemx0TXczTDFJR0RPbGFueW9pUnVmWEpwc1hncVBhRmZyWEZ1VmJFYVNlVVg3UUNNb2xOTUpTd082OFV3MHVhMkZyZEIvWFpWZ3g2UjRITEtIZTE1UnNzdUd5ekEzQjlhOUVleGJNdWFydzJiRE04Sk1ETUtjMnVmaFducys5dDlnYXdQOURHc3MrbjFCVnRYMGtKbWZneDgzWjgybGUrS2dwUXppU24zaU9YamtTSVZDdmlFTnpQZXpMWDZoZk5DbUtVaU1wUzltMG45ZExRanpDT2pOTWRZeXBDOGMreWRTVndTRThpYVBTNGpJeWRJL1ZBbGUwOTV0Vm5DTHZIVmwrN1JFanFxM1Jvb1dIKzFHRm5hazcrcDNIL0d6VlF0UEU1aFBoNk5Vb2I5WHNxdnA3LzZXNW9PMzBNY241UDVLUy9qbENiNTllQTlCSnRqcnZwaU01VWJQSVk4cEFxclE3YTNQSjY2WDRTUUVBYTYyREhYS2xBQXRSczIwRG9FejF0QlF3NVI3aVYyWjYvMlNwSU45SEQzSWZIaEhZK1hMNG1DOEdsQXByKyt4VXFkcTdRUFhBR2pJWmJKdFR4a0M5MmUrUHByTm8vbE9VSysrSUNSVS9IT3lOMnEyOUZ3OVMrUnlFM3FCa3lhR21pV2U0cjZpOHl3WktwaGdwQ1duU1AzL1h0TVR4SUtXRVFJRGNyS0xYeEUvTExmUWs5TElQTXJSZzYzNlVBcHdSK1RIZTlKSkhZZUg0QWpXQ252QWNtZHRmWnQ5TXJsNFFxRVd3TjcrZXdqeE9SZXlFcjllRjhscEtaaWQ5NmZvZ3kwaWZybHlxTmtscndCUFFzbk13RnpEbldEVHd5TWlFVHovR3lzN0NxZ3gyWldFUVhFNkdubFdHMUcrL09sVzlZeVdUbFE4QkI1eHdUdXRURHljaVlrWmxIcjdvL1FyVHpZMWg0YkY2UVNydGFqTHNDallTSHhhTDJ4T0hXY25lamJpSmRBc1orY0c0MlBNbjBybzFWZVh3NXNEYWoxOHpMS0ZYUW1ZM0tSVGxtK2xYbyt6cDRCUEJYN3pVYjZ6LzNTUDZVVlV0dExSVkR1dkNaN0MyQkZTc2V1YktuK0Nia2lUU0xUMFZray9IOVBiYXRya1JnS3pncjdjT3J4TVU2eDgxb2dsRGoyZS9wWjE4NkdlZEQzeUhiZkcxaUdtdWpqcFFvTEZXOURxWGs4eld6aFFObFBLZDBhdDN6OHh3eFBKTVNkU0VWTHl0WkdodURsbzZKdW56U21CQW5mWjNzTjN6REhjQmZiOFlyS2x4S2xNL3cveHhjRmhaa1FCaXNSSG9KRDZxN2Y4ZGtNOGJKcjAwME93MngxbTIvWUdKUDBtWTN2S3JlQitLblcveS8zTDc5WTA0UW9ybVh1NDdtSURsM1ZRUFVpNVI5NGFNZEQzK2wvSEFYQk1hT2V5QWlvNkpHTG95R1dGdDRSOEZHL0krblFwUHh2NnlWQXdzWThFWlhHYVV1S3EzTy9iUG5nSHh1TkQyNkpOTkVEN3R0UmQyNktSTVNpUkxEZlV4WHJ6bHY5YzhJMHdHV3RUTEhwbGRXMXMzWjdHdldTcGpzbUV0RjZFZGN1Y2ZLdEJZUTd4NHNlMWhPSm1vWUY0eEVyK3k4aEFSWmVIQnVNdTJmUTRaNTZTMHEzbHg4MzE3UGxDVlcxUUkzSXFDZVMzQUFpVTFDaEJ0WXZpbldoQy94S3BwZXp6eG5YRTlBWkd5eWhoZHpsVFBrcWZWbWw4RVBVUitPMVc0cEZmOUVWeDBjNmhxSEVGZ0FCN29JYldSTWpKWndxYllJRGY2TG8rVy84aEVpOUQzU1dJVmx2SXdCTWdMOEI3TlI3ZG8rY2o3V05HeWRzL0lEdjEvalZJUTVVYzAwcVJLaUpZaEpjYVU1UFd1VGhWbVJFWmhYa0I3cDU1cUNOUXNycTZ3QmNPemFlcHNkWVhFODRsL3NqZnJiOUp2dGU4QnVDaW1NOWR2LzJyOUp0eVkrRWI0Mm4yVk1ncTlKOTVCTkVZWVcxYnNtTjdLNDZjSkp3MUcwNXFXOHBhSVN2NTBkZW51Ky95NVdCZitDbjA2ZmtTaXhwdng3b2J3KzVZTjVVNzYydGNGV2x2Z1BsUGdXNVhTMUlsaWFNZU1OaVVZUkJ5YllURGxDOUNvZS85RjYyUkE3aVB2akxEQ0ZiNEVBUGhUbitVSW5HRnkybzhwbzkxUkFTK1hEa0U1QlkvSFhyWFBaRmgwcHVYMTd2NHRjNldOc090VEhSRmp1MllyZGE1SFo1c0ozNTh3cjlVL2M5eHdkMHhnSlFlS3hnSnNuOHlSRlkweUNja0lnREhxV2JWSWtVU2g5TWFlQWhlYmN1Zkc2YlRudGFNekFGOWlqMmhTeWFpU3pGQk1GMFRMT1ZXMGdYVjJIWmM0WElxemJOWVErRHVmSjRQWTR5d3RGRkJUOVV2NThVK0M3aDR5TGYxTTVFWCtPT1h5cjRhTEs5WUM5aTU4aCtCM3d3ekY2U2FRdGxPalBaK2s1WHVxV3hvcm1PK3hMRzlWbXNySHRIdlczV2lpS3RDeWJGQlNKUFJDWWlkM1BaNjJydENkeE5qNUZTem5WSG85djBENCtzWDhJbEd2cXE0T2xMV3ROdFFwZHJIL3pETGlua0ZOMHRTajBSM0I2b0FKMm0yNUNrTkFMY2xIK1Vuam9tRmgwWGdnY2thVkpwYWt6aVZhTGVJcmNLbEpsZlZITHFaeTYza3AybER4cjRRUnhMejdNMmtmYmR1eVZ2QmJLSFhLTUc0K1psT01hYXdzTnI1YWl4dnJmTEZITWV1b2xJQXpUNHQ2ZVFPckFYWWozZnFDR1ZXUTV5dEsyTmFzemJMbXZtYTFoRjVTOXJ3SGthV2pSdU5WenFic2d5MCs3b2IvMmZTQkpzU25BUHFmSmhsbUZaMzUxcjBjdEJQSWRUOGR2TUo0eS9xNnlJSkdzdlQ4aVpDcXMreDJvU0tXNm5zM1VtVzUyODVwOG13aHNENTFtZC9SSXc1QUFzTkJCL2VwQTl6MWhEU05wNjhtL3JhN0MxQ3ZsR1lNNVVXaWpkb2tUYzA2Y1ZsaGljZnhROXM1ak1JRUNXWDFCdnY0eDR4OG1oVWYrbG1SaC9rUTY3WVlmMWNOOGRPREMxbk5aSjBZM3JpQ1MrakJ2M3gveU9KUGwzVUNzb3FoVzgyeGpaK01UVU1sSHh4WmNKS1N2ejF4bmMzQ1pIMm9vYitJQkVaQ0FjRlh6bGJYQ1JtRHl2REhnbE5kM0dvbjBRamNzL3gwYlhCR0hBYVUrU2poNERHa1hPcVducisycE5rSlhrZG9UTDhaWkp0Uit5bC9ZdXBhWXhtQ3UvbGtmRVV1all6ZXBNN2FrU1FoQkVldnFRVDlYK3JVL2ltWkRxUHZ4Qy9XakZmZmVERnVVTkZuNU9IN0UybEl5RXBZVjRBelpYQmFLckhYMTVLU0h6dHBDVFpFeVdIUWtYd2VjQnFQamR1T0o1SEZaMzdsazRoOHVubVZBTERrSndyYk5vNnRtQ3hyQ2FmK2FvTVQybUthajZuQ0ZYQXFhZHo0Y1BkVkhqeTlHZENaRmppMEJYUTQ4YnoyWDZ5N3hUOEpEbU9YNVZWV2VFcUdEZ2tnMW5lNTZua09RK1JZd29KOTNsclhsODdUeVhUd3E0NWJIUHQrTWtTaDdlbVhIQmxkbmtOb3FaV0hhc0tydGpsSDRLNi9MYWduZVlHdEpsRU5BcXhxZG8yVUFMNE5kdXAxM1pmdytubnMwWmdSUFc4L3pXbW9wWVg3aG5xQXBLZ2I2ZVgzanFFaVZWZ1FUZGhJSnBRMUN3NmgvUm1MKzhwQlo0Z3drVHBWM0p3aVdNY3RjYlpaQlRkc3cydHBhdjFrUjNzQU83aDAwNDMvQ1hwL3RwVkRaZDZtT0ZJcjVmb3QxK0FndThzREFHUWljTzgwWTB6Mk1aaEZwN1RVQ3VhM1JQL0Jobk43aHhJaERGcDBhQzZ0SFJqQ0tEeEdqV3N4NnM0Rk83NHdGd09lYmF4cVdudmZ6RlRUaTk0Z2VpK3ZDZHA1bmh4N0drRzNMQ2hRVUVTM1h2TW9TZldFSTlaT1VGZEVKUzE5VmZGTzBXeE5wVmdPdjhvWWJpUVNTTTZ4N0gvUUpuQjhtalQxLzlzZWhmS3dRUGk3WnpYRFJyU01LaDRTYU1yVzc2WW9rTjhNeHkwTEVZbjVOQ0FpNktGa0VvYlBDYUhPNkVZdlY2RWVUZW15dEdTbHdEVmZicklxck51VTdNWng1ZjUwOFUwUUVlOG8vU1RsdkJjT0RpenlnQXZKK0FVQTBoY3JmZjh0TWYwU2Q4b0RNQXhqemNOZm80aVY4dU1jeFZaSUY2OEpTVXhQa1liZ3JYOVZpa084Vmcyb09rMFJpZlRPUGcvUEVmQ05OS2w2R2p3dW9zTkpwQ0tMOUhOdkRpcWtObmhGc04yK1RoZ1Y0dG1KMGhuOUpQdVJ6RWg0andPaDRzL2F5WjBiZjlaWmlocUNYVDBkd21ObzZvZDRnSjNuZXhvMWVOTWE3VUo5MHhZVFN1M2dvNHZPS04rallMNkJMMUpTbHF0RlJVUGF5bXRuRklYTEh5TU41QUxsVjByY01hTjVCc2crVjRFYTlsRUltZVcyZ3ZBT3MxT1NTYzNwcDhjeUlZbHE3MEY3WnBVTGtQbTBQQnZ2N25FUjhHai8zczdScXUzc3NFMDJRVmZITXlMZlVIZmZ3WmVhTWZqNmoxWmlYZWNtdGRFNFd2ZkFnbmhPSXVWN2h0U0Y5UkZiVlozTnhIQzk4Nkk3MWhrSlBqTW9BNE9CaXZoSU8rRGxhTHNJbWYxWUVTTnE0VVZENVd5R1pwTEcrMXhXMjgveGhjR3oyZTh0U21vOHFWTDFMbkdNRkx3V013em1hMVpNQmd3SFY4SkNKekcrVEJ6Qzg3TTNTRVl6dlBqWUsreDBkQnhBWEFBSFc2VDdva0Z5U0NRWnZ5TWlmcWxqWmVmTHVTQWRCVmFBTVV4UFBvaTlPUmoxSnVjK3l2MXVaeEpzZTFFTEtnaDJQOUV4UHlweUIvbWdCYUpGOGpXSjdSWXRSKzA0SnBqVzVmdmxYZXhEb0tXMUtmZ2JPYnlrT3NlWUwzT0kxVEF5NnFiSHdZZXUwdDk4dzVrSmUxQmw4RzlHMW0yZWpsWTgwcDl4b1JRL0dTV2lUWSsrMWFBbGNLZ21jRStkOVNlNFhQS3h0ZEV4Q1BLRCtML0s5ZVNaaEgyNTd3NURoWWFTVVg2cFJrVnYxK2VuTkQwcEJQOTd3Q2JrS0hLRUV0NU8rYy9abWRHa29OcWFhUXc2UjFnWXU1WGc1WG83Y1VlUGJlQ0tEMEhHL0loVm1HSU11Njl5UXo3Z1pyV0lyYTNiVVlCNnlGTFhtQmxJUmhTU1BYRmdaTVArUHpkczdpUFphZ3dRRmpYN3IvZy9YN25XM2lVTVRUc3Jlb3hjdzgzbmorWUJQdmFuYVQ4clF1ZUxtQVdNclI1WFJ0SlgxbFRiZzd1dm9wZnVDRWh4Q2VPSG1RNWlJTTMyTUp6cVNUZEVJaEdMU25uMVNVYlBtOWQ2MFRSbTFTM0NhR2FuTTVOSVo0YUVUd3JxaDdrSG9NS1VGNHJqOWpoWmRvbVppTXYyYUk2VzNPUThzbW5VNmFPbWcraElRbnNBZ0V1QytTQ1liWWVrYS9wQWd2Y0xzQlpsMjMrTWs2enlQMG01N1dQWTdLZHBqT0JTcDJ1S1NrQkhjZ2w5Q1Q5YWxQYTdaYkJzejc0UURleFJHS09TZE8xbk9jQVZrQVJOSEIxdVlCSFBCKzBvS1lHeDVGTFI3N01IeEdwNXVzYTFXWm9xSUtRZkNGaXhUZmUySGgyVlBlempzd2JWR3NLMnl5bVNCcEFTdXRxV1RyTkI0MWdYeURlU1lwYUVDNUtKU29aU1k2VmJhK0FnSUNBNXZlR2JzalpYOEhwZ2RhcmVqelF1akIwcHg3dVpsNmpWTklvVlFVOVdjM3FwdlFpS3NNY0dSV1ltS3d1eis4RDhxSkdML0JML0dWVlNTYUlqTlRCdmhvNVFSdDltc01rdS9yZXN0cjBoRUlxNUF2WDRmOWt6aFkyNEZTUnZXVW9wemZMZXpveWhTOGM1UnhFYytpTXh4K29jRGxoemZZekl2eksrakNIT25nOHkzQnEyOUJkWGpjMXJzL2QrTXh5dktsc2YvcGF4dVFMV295Q0RMZGtjM3NRRml1dlgwQ3pZR3VHdTlDd1Zlbyt4NkFDNkxBYzFLMTFqMzVGdytFVUVtdE5sNFpwd0ZqNlZMc2VkeVMyRytrd0IxTUpnZUhLYWoxa3lMUUFpcFFnSFNwenM5YnJkejZIQ1lHdWdTcXJtU3I1MGFURVJHQzVFSFlub3BDTlFKUnZkM2VnQStDYk5ZNVBRUnpTbDBTRDFROW1YT0pWWno2Uk9mNEJqRnBnMkhwdHJtMGdDQVdMZEQyVkJrRVpvQkdzenhZK0tWMU43MXVzMHVab05Obzg4RzlnK1JkaElGekl5aHRmZDBvZWNJam1RYTNGU0RoekFTamVEdWpRK3FLa0pQdEgvcGZXUXJsUGJsdDhQMGlRRExXNDNZSXhtRENOS0tQdmhYWWFFVkdtK1hhZ0gvNHVxWEpRTkZ0QWdQU0xKOSthRnoveVZtbzN6NVRpbDZVaWtMSlRiT29QVFdOb2kyTTllb1BRcGpFUmZ6bVNFNlVCa01KWGU2Sk5lNUIwSlI4aVRqU1Q0SEFlNDk1M0VpMHNVc1dNTFRDVXZXRDg2Mmp6TFNoRGZLUDRRMWFJYlY0MStrTTRZMWVYSXV2NnJsa0FYT3lDWEdnVkVyTVpnOCtqaDZQaWdoaTE5QWxqS09SdHlZN016MzBDZSsyUUhvdlVyWG5tMHBydnBJME5HNnFpOGE4Y0hsZEpadWpzNVdtWU5VWUozVGpKbkpoeVV6U3BrNnJ0VitqUWdWU0RtSTIxNGpkVVNSdWRpOHk0dTB2bjJkWFRmbzZxRjRFU1F5MjhCUmZkNW0veEFLNCtlQmFsbHVaSEw5c3V4b1c1NkViaWR1OEFSa21zRTU0REF2VmRXdXlEUkZpZWdQY0NzanBqZE1BQS9VQTVWU0RhYXlsSHkrbWhaTzN5MkNZMjBXT2dlNDNZbUswVTNISDBrZDJPZER6SzA0TThqNVVUV1JXZVZaWTJ4T0RFUlk1UFcvTytBbVhvTFJma1ZkZ3ZVQWhpeE9qRDFkeURmb2dqT1VxZm4xWnE2dFBHTTN6SEpBNSsxT0QrZlRveUMvSHhUaG1DRmFLcmtwVTIrTHRja1dhY1NuNm83N3RBYk0yaHZBczI1Qzk4QWMvZ3o1MExzbEhEejEvdzhqaFljQlc4U1l0OER1dW1DOHZXeVE5R0ZOeDVqR05QeW5KOGJKbGNRQkRyMzJvcEt6NnRqRndjY295NHJPdlE1ckhibkIyRjg3U2ViVzVNSUxuZFFPekRNVmpZMEpGaFlKckNaekxFR2Q0Zm12ZWRrQ3dCTG9UaUZyVlRlZ3VUUmxJL0orblNqaCtvUmlrZG9RVDhMcWlHeFVDcGZSMkdYYlpHeVd4dWRZUERPM3hyVmg5Zm03YklKSU9EY3NkS1gwRzdZVXg2UTBrS25QTE1xWlpYdnJRLzJrUHVFeWcxTjIyZjU5MVhMQkQ5QXRjSlNTdGs4Y1RpemM3TGZuYU9NUGlYY280Nk5VYkZwTnJMUEthMUh5MFlhZXlEM1BlaXRSM2pEMkFwT09hQ3cySUw3bEZNTituVjlha09rMFlZQmVTMW1TOHE0eEVFQzNSNzRQUktzaWU5U3dWV0VENWY1a2x4dnRYOHYwdmZWb0QyVHViRW1Qd0l2cWN5enMrdzY5K3Q1ZkRNYWgxemxEOUR1aDF1SjZBUkI3RlIxRTMybDBseFl2Mkd0Yk9lVDFrODVLcGxSR2FseFRRWXpncVBsaW5RNjFuaUo5L0tZUWZnenV4ckNPSVN0aXhFSkZRcjk1eC9EaWplYUNXZWVXNjlFbjdmQ2NWUEJ0TGNHYk1odmRKOUx5UGNsRVJ1S3VkMGlGbTdzMjdGZm5ROERkN21IcVdyb2xsU2NRRXdJVGpxL1FBTEVsNVJvU2s0MDdJM2tmbGQ0UnpRdnljOXhhd0RSTzhLRHJVc2ZCVWtZdjl6VGJsNld0eDhNSzNQa21Sbm9oMTJJRU1GS1o3MmY2Vk9hOWJIbkNEbk5NazRsUkRCUWFMY0N2VnorMkgzN0JnQ09yTitvVGwwN1NmakxGUlFDQTN3TzBaVjVoemxiZkZZMlJYc2NJTU1FcmRXdllSVWkxSVZVNzZNR0FabzVEN21RcUFwOEt6MXNubzJCRWZjVmIxYmhjcmVZMFZlVkFFNk9odlNhOWVGbnZ2YXlBdHgzMWg0VzQ0OFpDb1JKKzUvOFV0Tjh4WXNhWWI3YkIwdWxqVEx6akpwc0NOT0s5VnoveGJWbVd2N1lBRHhLRlFzdGdZRHU1aktaL1U1aW0wSUFPVU50UjFOQk13YUpwYmw0OE1WYXBaVXhXWlU1Sm51M0ZDVU83NjYvTE84cmVnQi9wdG5zV3c0Y3FjSmNvcFEzNFlHWHVyTkRwTG9NTEpKVk03Ri9PREh5cm9ySjBFcGNPTEdSY2xjNmRIUDNFUURSeEFjempja3hrRlJMQUF1dXNYbEsyZ25tckFDTDdwNkJTM09nTjJCWUZFbG0wS1BuckZrd3c4Y3lLeWoyZlRmdHJDeDdVQlNpdERQaDY1M3pmT0VHS1Vack4yanJmd1hNdHBwMGdLYzB2bk1wSGpaTTVIYzlTUis1VUhXcUFCZ3psU3poUHBMOUkzMEFTRUd0OWpVU1dEa2F3azk0WHZKZkhzSGRURHF3b0ZJT25Jdmx2RzcwM1lBUy9DK0NmeGtCNUFkZ2lac284UXIzSXBRdEJrY1V5aDVSRDRHbm9lRjhnZWw4RFZwSmZHL29kcDZDREVLVTlld3pYUG5XN21Hb0FOR1R0ck1PVkZXV0ROVGVaa1U1eTZwQ2krZjZseUhaZWlqNmd5REtjWGsrKzV3c1FVSDZpOXhOWk5EZVpyendOYkl6WkhEeUp2NFgzc2hYNWtpc09xK1BEc1JwUU1CYmxzTkJVRjY2Ylg3bW93U09xZFdOSm5wOVNYTFhHMWZxSC91U2orOWJiV1prMFdmTUVtdFpDZUlUOVFyTGp2TjIwcWVkT3BUR01KM05mUTNjczBUYVRtYW1pMFZZYWM0ejE0a2tBc1g2WTFvU2NNdEFxYVExd1p1bXI2QnRkWThaNklKOUhDRENFZDNGbTZhajNyY0NxczZDRitMb2hwMmFkS3FXUlcxdm9oQ1NoaElqLzhSTCt3UlJFUFlpemdzdmJxZXR4WEtXTVNKYVdVa3BWanlEc0FMVURIUWRTb3BvTStGaGFFTkRnRkZ1SlZZUGtpOGtvVEFoSlY4QmY0MzhLbHVFNDJLQ2xpTnNUQTR6aHNnTTBYQmZRaFJpMmlha01jZEsrS1hIS3Z0cWJvUEVKejFnWUZaejVIdFo0WlR0bEFTRE0wNE80WnBjSE9uWVh6NHcva1JqM0xHWWN4RWowT2hGeVcyYm5HYlZWM1ZKdXFraFJJMnBPM2pVeUxFTTdyQnVlRVYvMjhkWkM5RVhsY2d3TytZa1NQMVBhd1JNaUVmM1V4Tnd6Tll1K3E1WlV5SkxsNVlYSElQZ1FmZVNobk10NEFLcnhyeXZaT3Y5aEdNek42Zm43KzZ5WUtFVHFLS3g5UWhXbDUxY0xXOEVITXJxSGc5NWZjSEpkTHUzdWRaZERzZCtFZndoQWo3MXpxSVdpYVZ5VmZhdVlxZ2Y5TTRyOE9jUDZPbldKUjBHbE10SlF2ZlJxcUM3QlBzdzdibFZ2TlNuTlBzNFdYdXdwaXQwT0R0alpNTmc1dmZQTEhWS0RzRWdvMmVMQ05GNXA0eXpwbUxBektuWWIraEp3VHozZUJXcjk3ellBY2lCTFNjbU90K2k5ZGlpV1Uwa0tHdnJMRkNjcTNVVDI5OHl1cU85ek1tWVliVVI0WjFqWEVmWmlaRHhGUDR6Qk45d1Vlb0lvV1RaQTF6S0lXaWpGaVB1eEMvZC9MMGZPRFJsUUhOazkzOVpQcjlsVnQ5aTZUNnlPbkg4UHRTZjRmWHZpeGM5ZnpMNzJLVVBpYzltazh0b1Z0Z3RWNUNVTVg3M3g4anhSV2I1LzAwS2xSb0s5S29sQ1B4YjNDMXJzK2xoN01DS0VQZWZBT1ZlNkl3SEk5dFZLdUZVVjBhenptWFZGMWFxK1d0azY1U0NwaHlKWGRpYVMwNEwreFZrcWNGQ2VocGxFcFRlemRiTjRjbm1SNE8zYUNwVmJvYlY5V3pnZzlpWFFXMlRpd1FuaTlGd2taUE5aTTZqUS90L2owZXNpS2VxcXhnSnREWjVvSmVqeHI0YUxvTTF2M1RQTjZISXFMeStndlB1Wk5BR3hUL0w4cXVPRDBxdXlOa3NNZGlTWisrS0N4MzdrS1V5R2RiODExc1FwbzR6cTZOeVJSaklUVUJUTDRqeHlLR2JUbjFkYnZoMmF0RU5vRmMwQVVqMGlMWlo5U05NVHhOdHJ2Tkxsci9KQ2xSbHhIbTJXWTUrWlZEQXhMazlic1VtM1A0TysrY0pkdG5DWnRicVhZYitQTk5oN05xUkI3dzVFeTR5L240R3lPa0paZytEQlNPdjlYR3E5ak5tYWd3VjVaUzZVM2c2bnZsSUYyMjhGVDB5eUZucHJ0cFpoYkNGTG9PT2U1ZmR4VkIvVWRjTkRQdllFdjRVbnhkT3ZLSXYveDJNMklEZjY4YXZySmozcTJxUThFbW1uc25aVEFGaXdGSmh0N3YyMkt6Nk1mdkprR3l6eEJzRWlvc25naWNsdW9ldzB5UElvLytoSWIxbG9nenlzaFcwd2VsLzBGU0ZHQUNDb2lPMHVqL0hBSFd5ZUhqUS9xVnRNUlU3UjJJZjI0NlZCdU0wM3ZuR3ZtQTFGd2Z1NDBLd1Y3UnhjdGtQUjVFaVF5NnFRMFZ5YWwzK0tjS3NtL1JiRE9PczAyT0NIU3pjM0JOOElNTXVVQkhKZ05LTVRJbkJuWXhPOElVL011S0EzbHpmVUJsb3BaeDAzdnNnZGU3WEtEb1BKSGlSdlR4SFZGYjRkdFRscFN2dFpHQTNLcmRCblA0MTJaekNtM1o2Z3AyczhNaFFkaUU4amlMbE1sQU9LbGJ4SlNaZWJ5aEhzd2k1UDdwVlpBSEs0M2hwSzUvZjZ2b1FLUEZaa2U1M1hUQjhPU3hUTEpyVXBCVTR0cG1NcHFNWG83MkZVWkp2cEhLNzBTaTVnN2szT29IKzE4ck9iWVBuT0FGMmhhRFN0Titnb1pBcGxYbm40bEh6MGRTVjdLTE1xNlAwalV2RlIwWWlLVmRNV2tZK3NqSzdVaTJGWnprUG9RQld2TkFQWUNJZGJQRUl0OVN1Y3ozSU1EQkJIemJ2Tm1wWWQrY2Q1OExJODg4Zkg5R2FoaTNJUnhia05KYitUTzZ0WjZVQ1hvR1RsVmFhUHNFUWh0SThva0N4QWNVeXpFUlhEYXg4TDNnTEtiNURlNndubi91S2JvTUpHM1dkS2dXVXNpUzVsdHduSE9OODZkbUpCTU5EdzZRZGQ4RUV3dVhqS3FJZDU5VUs2MVoxdEgzckpSWTkyb2s5SEtmNEVlOHgrWncrMDNjY2FkMHFjeXhkaUVLQVpxYm9kR3RocDhRMmFNbFQ0THpYVjd1RWkzd2M4UkFPSldneExzRldMV1FaRG9HQkJmZ3QwQ3dZZ0U2R1pvR09YaDhFdVJFT3ZyeFJTYWNCZTBPTmNKQ2VHQjI2c1QxUjY4TmxNdTJFZEVoZzZ5U3ExME0veXNYcHA0dmJobEFBQy9kck84Sm5ROUFINjdvQ2FyOEdncTlOSm1vaDdRaW52Y2ZqS1JFK01kN0xSNVRPVFM1KzB0bVo2S2dGR0toYlJCeFp2OFVxemhvVlF2c1dWazkxR2ZIWGRUQXJRWEhIWEkxcmdCYUU0c0pRRU02MXh4Mm1qY3dEdlhTQnIvaVdYeTVnTkFJeGd0WUZjY0YwSEMycXZqMmk5T2hlVXNOeW1YZFpXSFZBMldKclFPQ04vVWFzWS9lUXQwOU9lWXYxT0YrU2ZLdmNLM3o2eDh2dEhZamduS28xYXhHMmNVS1VsWHg2aHdSMGpaenl6NWFlbmxpQW9sTFltMkgzQWtMWFZFNWU0dVdkKzRKb21QZllyRkVzamRyYlRFZm1LaStXeWJGVHorTkI0Y2hVQUV5K0xFa0pRT3lMenFHMFMydTNyMCs3Rk1kSXZDTmw1UTY3NWlDcWo0ZlVPNzZrUnVBdzNaV0J1Y1c0ZkhNRjIvaEtpM1FwZ3pST3BGOUFiU0oyOWVtd2RMNTJ1WWJIdnlyQjQ1Z2xEd0x0L1RYTEc3NEdiVlpwZVM2Nys3dDJTQnhydEIwUDBhc0RSOUdLODFyNlhsdWRETnNadEY1S0FZWnROUVpyTk9vbno4M2EzTUEyeVNDUWlCb1BVVlFRQUFNUDJLam12ZFhnRjliZEFqZTRXbmxXQ1pETzUyTTNtQ0t5eW1hSzJTdEpCWmpxdTlPYWJKTFVRRmF0VThHN1REZ2hOdndsYzJuZzVBSXdsSEEwUktXWHZtallwcHY4Zks4TVA5ZE95cWxHMWpPcmY3ZWRNTWYxaWdZdUtMaU9IZExaQjNxZHpIRFNoWWZwRDc3dzFYZFBoOWVGSUw5MkRjckJ2M0JrMDNWdnY3V0FiVXVPSzVENWM4ZEFHMWRZS1pGeEQwNjAra1V0NHgzR0dCMUlmMUtBS05xcy9Vc0JHY3FoMXYwdWdTTE01SGx3N1duUW44ZXd5OW1IZnNaZEkrOTRZSlgwZ2lwNjBrcUNEMU9CcnNBT2pJeGVwRzlZNGFjcFlLdW1POWlMY2YyL0xRenJGakVXa3pBM3g3VmZ3MkNnMGtiOXZzLzJQN0ZEUnZSV29uMTN1dDJ3cjViK1ZBaU1BcnJNWEFsdGxDWWgzalNtZUhtWHRoOXpwM2I2N0ZCc2d5aHV6YS9Ua21PbjlsN29ELzlrb3FHMTF4dVJjalVlWk5GNG5LZDQ2OVNPTTVHeFc0Y1hiOTk5dnNoV1MvdGsrcmdLM1RleElBQkZjd0ZuRzZGMktyZVRQeFgyakZEL2dRTmV6RjBFY1Z2eEtrcmJXTEJyVm9sUkFibkQzeXRkeE9ibUg0RWdJRGo1TXBMbVhSS2c0OXM3VjZnT2xLblB5UWhkRm5COFgrU3gxRzh2Ui9OdzI2ZnZQY0ZBVVVOOVZVYnBtSzh5RVpZdjNTQUNBUEdvSzMxUFMvcmRUV3ZUclROM0w4VVc5Sks3N1BSdVNCS1YrSVN6R0kzN1RJZ01tTnZGWnJlMGZaYkRIaWFiamNuTWI5Vk1uL20zOHRORkFxL3Z4R2N0Q0tyajFPRkxRd3o1MmZGMFFWL3UwUWQzdVlEWFp5NTZ6TEtIOEpDOHIrQXNQeE1QUUwxVmc1TjJRc3ZrbzFIWUk0OVFWQVZacDJhTzFhWmVkZG9jenlIK0F3RTZDNW54UlRYUGhmd1ZTbmVwZzg0OWx1K1dxMjBRdzRCb2F0dERTNWQ1NG5lenhrU3FtQllHeG5iYWF1NzdzSjZ4VlRRYThKdGFEQitrTmR1ZlVUcmkzVE95elBOWkdNUCtOTmM3MU8rbGlqK0g3eVM5MmhxTEQ4TWlRbEFqajVhM0hIT3FZZ2I3blh1N2tOdG43NmRMZENLTEZJcEpVdXdQdWJOaEU4blVIU3pjRHFhMmpwakd0V2t6eW81QmhCanFoWjc2b2FFT29IekRXbmZMbloxT2VKcDBPcytQVExiWkNoZnAyTU1hZW9RSjRIQVgvOGFlbGw5MDdpeHI1SW5xZ1dLSTVzOFEzek53TzNnNWh0VWlRejNHOGQ4ZTlPNU52YTVDekNNQXVwL1NweXI5QTRTMUNCQnZJb3dtM3o5cFkrSk4rYkdkKzZLSmFIWnJBdmt5bjByL3RlM3cwVEJtRXRyOVYzSTljcldvRWQzL1g5aUZYdXVHd3c3blNjdFFWaFNZRE1RYzMzbFp5ZlR3VjVuUWxzVWRNSmV1TjEraTR5aEFzWmN2VUtPUGxadDljNEsrN1NMSStBemRBWmZlWHBLZWl5dkEyWHFscmxJWVdnTGV3cWtwOHkzVi9pcVEwZTZMNk55N2hBSnIxZHdzSjBkUmdUNndsWVJ1WndHRjFlRURHeDhmcU5nSGVDMmc0eGlGUkwxek0wRkpQWHB0Vm9pV2Y3RU9uKzR5RlFCc2ZGdDZpNm16MnlCWU1Dd2RmeHg3cTl4aHNEL2RPRUJEWFhubS96ZUR2Y0s4azFqM1JrSlZpUlRwTUM4b0plKy90cmVtWkRKY25SckZTSzNsR0sreDRhbnZmY25aL0l0NXJNN1NncTlvenJNZ0hsREZkUGFJT2RQTU9Hc2xUd2l3TWZ5WnlaZ1Y4VXdwQ05RVUoxck5JbFBSaTJCb2ZETnh5MFJWYzl1S0ZmMlAzWVZLMzViVEdlQWlINjd6bVZXNVFEWm9ydlBXenF4bUJOZmJiTDQzc3ZpZEhib1JFa01qNUhuVWRobzVFKzdYVVM2MHlZdkxORHE5RGZqTjYyVGhTMmp4ZWZ5YWx1UnpJWFlTRHpPaHdWeXpuZUhtd0J3L0QxR1E4VUpNa1QvSWhTY2l1U202SVJ1SlU5MWdCY2lNZkVFU1hiZ0tmWk9hY1ZBUTg4SytYRjRGYW04RGluS2JhdGE2cFZNMGtmK3lCd1h1a0JQWG1rS1gyK0I5Q3kwbit0dG1QR3ZDcWE4SHhsWjNoS3k0N2ZIUHBwNjhHa3pjdURVWi9EdUUvZFdNQ2VCQVBENnZPa0g3Umk4Qy9wWVN4bnlCN1dHOFpHamZaUTJvOVMxY0VLdXNzWGlGK0VmOG1leFdmcXJ0U3JIZGdLNmlyTE9nT25PaVhUcEpQaWNrRU5US2Fla2E0V2ZIdzREVlNDOVh3Vjd4eW1KaXI5N004a3UvWUF3MUtQNTFoM2tpZmZEbWQzRTB3cUNacXNGTk91d1M0emFJS3RtZkdocjVKdzV2OVdkcUxqWnF3bzEyWkFROWZrZGdhU3pWSWtlTVRGeXNuRWpqQUEyZ3lkZSt4dkVuOVg3UkRGUDVPclFGaGNVQWdNSlBIZW5iOUk1Qytab3IwdXFSdGNiNHBUSEhsYWdVUStsbXIwNDBoWVNiL3NMOHdVS2tTOUEzeUlWNUQ3ajUzRDNDYWNyT0RiekYvZmF6Y1U0R3EzL0d4SDRIMldPZ29tZ1ZwTnhmZ1RDa1Fpc0lnb0pMYXRKUjloS1FURUxlaUFzK1JjaHROaEFTMjJoRWdFdXpQWnlWNTBtVzhGVk05ZWlKRGt1L093YklweWc1RVI2SU5WYUh6emdpN2VnZ2RabFFnSHBtdDA5WVA4OGZEZ01GVmphOHNucElLck1ZbDQ1M3g2R3NFQUZJMCswYlFCSjdUengvVkZZTE9KSDJtTUVQVmZmY0Q2clA0QW5GSzBPQ2pOcEx0UDBiSElUb1Vndjd4aU5iRXQ3R0x1UG1ScjVYRXo3ZWFVWFR2VlBIZVh0Sk0vOTd4OERnNVlzSDVSOWNCa2VVeWsyY0R2dzlOSmlQN0pXazVIcXBmUGRFV1Q5eVEwUlRaRzdaZG9adDNhRitCOFpNSnVNb3hvOHdHNkdrVDVjRFNTdlluUmpqdzVSZFFYaWYzaTVLbDF0NVc5dktWRzRHczdzaTl4aFhZbTJyZ3pmZ2hDT1RieExYelFERHdXZktIK3F6TUhZTWpRejM1ZXVCTjYyWnU1SXIrTVdGeWZ4QUQrSTB0VGFxendCakdEakYyd0tmbHhGWWRyRkxFR3FKY0lsM1pqVHRnSjRwMktoRmFFWkV6N1BCQUJhM0kyQXluR0xPQ0ZPRDRjbGdrWm43UytDVlpCaUYrODE0MTcyT3JNWVVSRHN5SVl2Y1Y3YnYwdnpFSEExcFNtdnVmY2xjQTFBU1NZcjlSOUViSFE3MEs1WFRZaHk1bWJWVmQwMHIrem1yeFByeGx3WjlITXZOS1NQU2IwREVZYldLd3AreDRQT25OdFl3dUdMQm9HcGE5S2xHTWxYVjJVaSs2QXNydjdIUWlaRnlJakIyb08zcW1sclhWbHcrQ0oyRmdiL0J4b0NmakdEbkZvUFJwUEo5QVlFdnl2SGFBTTNUb1Y5RGZnNHZ1ZEE0cnB4VGJBZm50THJsU2dUbXVVYUt6QzVVNWg5YTRacnptK21ndC9jMXUyUDJEOTYvb0tWQ0xZZ256QTRhanFFZjBMOEZRUUtsamNuaFY3WDJqWnNiVUlBVzhSM2VBRktKMDZqekY2QjFicmFRZWRZRUd2Ylo2OWk4aVJIY0xBWFNzRzI0ZWhRcm91V1ZpRTlRamU4dStrN0I3N1c0U0JJSGpmbytnQ0ViQWFJdjlTelNmU2hTNVN1VmNYRHpLZmlRV1ViT1JmTjhLZ2E4Mlo0RlVkckwxVUxNM251aEQyVXptcjdxTkJPNThEbWJuTDRQU2hFT0d6Y0NVTE9CSXg0Q255ZFIraThtcVBDUjMxUkVRWVdITldWRjA0KzFUNC9sOVd4N1p0T0EvUXZxeGhMaUE4YmptdzFKUlFuWFV6Q1pVSFRHSE0yakdLY0JlUFVIb0Q1UjNMWTJBNHNPSk02YTZNdndPM0lWOHRXbVlkSGI3RzVJb004NVRrQmdOVWRBU3U2S0VLVjRYa3JVR2ptZGVTaGRmMWF0aHhmQ1BlY1lIK1prbE5jZ1JSdkdyZG84MndWUUJSa25menNwSUEyeGE5NXJOQW91VUxibGoxeFlvYTM2Q1RIMEl5NmV4aFI1Q0xXNlkvOVdpMzdkTGt2ZitLUnllUTVDS0ZqZmx1Y1ZxL0lMNjVrb0J5N1g3RzQzakNta3phUHBra2ozNTRxU1NXV1RteCt2dDVuY1BwbEF0ZmFnb1VwRm1hK1JPU1l6a0VSdUNJdWwvSnpHZE1OcGpDT2pLWlFFUno2d2JhV2tFeHUvSVZUdTF4WXlTMS9MQ29zdUZhcmlkUWprMXZIUmJCd2V1aWtSU295RnNqQlEzOWhkMEhIeDkrSkpXckx0STI5NTVRa1BOdzU2T0RINHBuYm1NQ0J5Uis0cHc5cDVGOFlnZytGRllYUlYzVjhlb3ZURWwrWGdzSUhnbDZpQ1JrVjg4NTFzdWUxRmU0WVpLYmU0eHc3Q3lZZ0NzYjRSNzZSTitOK09wYkM5TWs5NHVRS0JSUXN5Z2JnUm9VcllLYVlhNnh5WDJ4VDZMODZURDliZS8xaFRReFdobEhqbFhOUVAzamYvY3lyb3hkbzQ1MU5DMnF0VVA3N2pOMkZPVzVQdisybG0xcDdPMTk3Tkd5eGVBd2VhVmNZL0FLSlRyWS9uVnVZY2hzS3dpZEdGNkxCVEtJcDUwSWwvd2lXTmNTSzhCREdxR3JlUTcwckV0cjRDamJmcXVEaDc3YjRRbXdoajhWQ0VKTU8vMUVEcnpTU1ZqWmgvZnBwTWtIUXJOOTc5ekp5OUY3SVdWVEdKNk9SU01rTUlTSzRKcDk2L0lVRE0xVnVrNGhZSFd2Smc5bUxuKzhndm52aGt2aFVrZGtPNkpTNVdYY0c3RWtvU3lyRjIrVDZlb3ZqMXNIM3NtUEorK3VPejlRMmRuVFlDTStDdjVLZHAvbUlmN2dvVnBlc0V4NmNETFlIelhseUdHZmNYbk4wdjBjMCtmaWE4aXl0ZGhVQjkxVkx4TlI4cjkweU90MngvQmdtWXVHVmx4MUtFWEd1MGFEaVh5bklUYk5wT0xQOVBTKzkwWGp0NktLd1B3MnpBRFZ1Y1NoR2daem84VCtpekNsNE5hWjMzU2VpM0Rxei9yOS85YTBuMlluRmJCSDNZYVluSFF0dTgyM3psYzZqOFhzSUxEK0ErdlNwWURhWXFSRGpnejNlTzM3djZzYitLQWY3ME9yZmtYalZhWk51YjEzYWZvM2N0SjZmckYxMVdtTDAyaHVpUno4SlN0RzAwNldLVGE3WmZ0Rm41UzJtQ2N0eDNHem0wUk1TUGpIY0FGVTZsZzk3UzNwaGtvRUxXQjZTeGxHalphdW5ybGZJaTFFOWl1WGd5b1dwLzM1d3prUlZWS0JBTlVHM2VsOWlWR1dVSU9vYVhYcGJlUG1nZG1FbEwzRVlkTjRTbkh5SUdZS3RzWEVCa3QxaTFuUDlzUktMMlcza3NvUVFDSllZTVpoUTZ3TGhUYmNxWlFGeDRBUkVLK3dDQ1RWNC9QSU9RQTBKL1pLU2xrVHY1S0JNOE1qRTkxRTB3ZnpyTytYMEw2cnBmUVhrWkw5ajBvRE91Nkg5UkpLQ2VNSkhLK3FIdWhkb0NPZ0JDdTFFQVV2TUkwOWxEOFhiVEcvVXdxR3Mwb3BtLzFEc0RkOHVYQjVOWkVZS1VhejNPb0ZsUzg2M2NnUlFHV2h5RE5KNFd6ZS8xYlIyV0VScnNaalpMc1JUR3BUWXV4NmRzMnpmQXpuazVxL1MyQ0dZMThYcFdESThYUDJ6OHkxSFNteVAvVU1VWjdXNVJpSFQxbkR3WDVPYnFKTXVyZzVLRVpiMHYyaFR2YU81YzRvTlN5QWN5MkdRaHVOMG1xQ3NnYVZVK3hyR0hxQjRJVGh6dkNMMEY5MWlZRngyQ0Q3Tk9ZNGFOYXlUSklDZ1FXUHJHS2xPTGxPdGVVemJhVndFYzRsM0xqdDhlbnhveTBodW5tV1VOcGZwdFlFek5oTHdmWUNxYkpKUzJlMGtRUlZmODl3a1Vscm85NS9ZYS90NkI5Y3NOcEdld1lTWUttM2ttZkhvZExaMGtNVlRCMlZaY2NIU1pKbnA1OWx2c2lldlpKZ2M0YlJrVjVDWE9sU1B1QmFCaUVjelhoREd0THZ1bkJXL2I5a0xGWWdkWVl4cnVPYTI1Ym1IWTNWYTltZVcvMnY2QWJZcWFKQXdXM2ZyRlBsbWkzUnBud3o5bzEzNTBFUmhBODZhbmFUMkFkOHdmMCtTaW9kWjRNdWNyQk1iY09ZdjZRKysxMDBDN0NKZUNSaFNCRm1QNFlQcHJ6YVlTeVlhVzMvSFFIdlZlc1lOL01ib3RvTXBRSFQzRlNhbi93dVNPT2N2clNySkg4a2tadldhclljTnZZSG1HUEV1c20rdlFtMkpmSkJrc3JROUJQMVFvY0JBZTlRcFZITFJ3U0ZDbU9HbkFpNzZiTlRaRlBzTzVzNHpEMkIzZC9IeFJGcURObnhOZG5pT2c4aUhsc0sxNVlCaWx6cmU1bDVQUld2WnA4S3ZLZXFNNHZDVnR6Z0lGTlR2ejZsblBlT3loWWhXTVVzWFNNK1k2TVNaUWxxOG83b3BIOE1wWm9lYzhzT0VEVVdHV3hheFRaL0p6ZmY1TjJLS05adXhOZlQyMHVNdzFpQnVoQ2d0enVHZ3Bjc3pqYWswdFU2RitmdVQyZVJTNG1MUG00ZlZOVzltS1FMUStNSzVQSE1UQXhRMGpBQWoybWpnUjNOSXFoMFVZNEFEb2JNbDYwOTVqSU5LRHVLRjY4WmFPQ1VRTjVqWXVuY2R2SWwyWktTQmM4ODc1OVVJOElhWXNWUk5GSDJRemg5WVNjbWt1ZkFybGhRRVcxekNIeW1rWEFaQjk0WUlMK1owelNpL2dJbEpwOGdNcXRHaXZ4bUdUSCtIRHRxOVgyNVQ5OHBzbHR4NGZtNko5M3FyWlhJcnJhTDJoRXZiTTloSWkyWGErVlhacWhtMVVJVkdDdHMrOXpsRXplQmI4WXhqaENDVlJZZUdCaGZaYjJjMHBhb2d2aE5IMGIzRVB6OWcvMnFGaXZTRXdZdDY3aGNRdTBkRkdSYmlUSnljRUZYaml4Y3dZTHYzblEyeEE2SzlFdkkyRURobUkyZGxRajN0Zm1DSE5QdlQzY2p0R2RZVWtxNVVCYVBSZ29EbWh3K1p5VWRPRjF5WmhteHA4bWJnWi9WYkRCc21tS2ErMFdxZTF5NVMxNTFMekVJNHBrL1kzNkx0RWk5WE5ZWVdMT2tmaVBxTFkrRmg5VmVaaE9CdHFTOXo2WTdKOGc4RG5aT2piK3N1eWVsR2RJc3ZzQytuMXB6Qis2bmd3MG92blRXWi9XaXBMM05nb214b0EvdWdmRnFTRS81QnBVZ0QzenhlU3ZCUjB1T2NSWExBSHAzNkZHTTVSUUVsSkFFbXlLUmt3b2pnbVVXKzlUenJIMzZWWWhSb3AvcFBsaXQxWG5uR1VERGFaKzhxdTUvNDdtUS9jSnJVSFpyenh1T1JCUmJiODV6Wmx5QVVWZDdjVGdTNUJITXY0aWRCbVdCWm8xZFdTaW9kcm02VXE2RnJKa3FGTkRQeU5Zd1lkeDMyUmErT21jcFhuLzhrVEUzdHB1VjJEd2JhRmxwbGUxZmtCQy8yZFJhT1NSaUJZUFFMQ3ovVWZjaXpoUTlXUm5JS0s1czRjV3IvZ01ld2h1aFByZ2ZGYkdKY3NpUU1BNC82NTR4UzdXYWo0aHh0UzB6cUlJYk5VZjBUbWdZdWVSUVBhSGdxRk5RWlh5SzkvdHV6MWJWUm1WMS94cUxtNFFVYkh3eEtoSVR3TlduVVNJQXVvYlBaSmkxVlNMa1c0T25kSXJUWHFaSzBXMUtLaDArckZyZjVFVW00K1RvdnFuMmYwMmJuVVp0ZEoyVkxnU1R5eFdseUJHTjN0NE9SbW43bmVjMTF5VDI5bzZTWXp2c01MUVZGN0dOQXMvYzRWMElGV3ZEMkFIOVlSS1Z0dDlLME5qcGZLWXRlYXJ0NjhiVk9VZFpvV0JXMFNHdFN6Z2U5QjgyZCtEamoycHlqcUZwSDJxQTR2QnlEdGEvcGNPaW95R09uczR2Q1VPM2dITVpQZFJZZUo0a2k2ZHVuU0xSMWhLU2Y0SzBOYlo5SXZFU05wa3hEM0pGbEhXZEoxT1hFNFJWbEt1OGZvcTVENll3Um8zWXdiNitrckM5amtmV3dyUjM1ZXo3U1EvczlHQTlHOW5McGNaaFROSFh6T2RHY0UwNlJkM1hMcS9wRStSbHBMQmZzZkxrODZuK3UzMVlhTm9mNmZqVm83cGdGb0xxbGVXRHFxei9jaUlyMmp2RVNIcUw4WGUwU0FaMG81WkxtSjhzSjRtQjloMURwVlpmaUJnTy9YcG5KMVljeHc4NEIwcU9obUY4a05kVlRKMjBMOXk0aG4yTXZFd0VoSTliOVhOeVdXZG1jQmp0ZXhDWld3Z2t0bENRcnlYcklEUWlmaUFQN042dlplYlhRY0JZcmw0a1pDTEZVZjVzK0FjQkRMUng1eG84VG9scmgzKzBwaUhxV2svazdLbmNFZnU1Kyt6Unp5WWt5WVIxTHZMRHc1S2ZpMXQyWS9EZkY4RlBEdHdXci9CT1ArOWZ3d0owV3pkSk5acUdTenN5RTZKZFNvSEZNOXpIcWJQbERETHljZTh2UmlnU0RYd1pJbjBJekU0azkxVkFUYS9pRVNQcnp2SXlTcVAzMmhHMjQ5M3dTY2hsaWl2SWhIZTh0VFVUVndJazdoTkxBRTc1QTlBNGRJYnFCcGI4QnVMNzdGcFhrUkFJRXdWR3VVdXVEcDNHVjJNQ3Bob05aWC9QaXRib0ovVHhDVngwUWVpUXA4bHVKRzgvSU4wTlA0U0RBakw3dk1MMUt6YXpzVENxV2FvUEtycFRLeHErVzlSTDBVaDBsWWFaZ0FkWHhESFNqUDlMVkVIVTgvYTB2UktxdGEyTDYxcklUQ0VCL0FwVWwzd2JxbU1hRk9xdE9kRThrYnM0UkhDS3JVNmxHQnpCd1MvS2gxbzBFRm16L1JHQktsRjR1UlZ0RW03akxXQVRZYlQ0SUlRSkxwZkVqbmIrOWRGcUNuMVc1dXpEYSt1NFRaZmsvN3VBZk5GMXJwYTBWV2ZPcVRuamxIMEtDVW5UdVhuLzlpT1FQUGZvczVweC9qL1dpdzdrSC9BK0VtVFRIemxocnRTZkxncEZOK243ZklNSStMVFB3dkNhOXphbWlhYXZNWkZiU1JFM1E4dDZhSktyb2JQZmFnVnRUcTFyczBPdkhYTjAzN1NBS0Zsc1NWSTErRVllUnR0WXNJU1hYTVVIUUNzVDYyRzcxMlRNUEMxZ21SZjhxUUFhK0dlSncwRnptM0RrUnRVVEhZTm9DRy8wWU1TMmFhMzlOVjJtS1NyTC9JVHRmbXArY3pWNG1jNDZHN0IzSmlvanBlYWI4L3dMbUpkSVNiMUFZT0kyRG1XSHBPSnJ2Z01zYnJDeUxWeTEzWm9OaHlTbWtJaWZUUTk5NlBEY1B4SUdRT2ZWOXF3QVcwOStadExxSmZ1ZWZaQW85MG9aYngrMlkwTFZ2Wmdwc1pJb2lXZUs2OW9FdnlWMTg3NDVOdXBWcjFtNkh6bWgzdGtURzNTT0EzMGtGTVJNYTljc2xoZVNuK3YzSDBtS1o3OXFkSElDblNxempHelRjY05kYm1hL3FsdU1ZcFBZMml2VWRMYTREa1hlOElnRzd6M1dvcklzVmhpS25sUGFPeHhpc0N3anhua2U0V0ZTcVQycnQ2Vy9sdEp2QzNTcElnT1VETUpyWkhoUWRReVg0SGJ0VzhWTnVTdENrZmVjQkRjRkd1WFVCb1AwdVpDZzd2UEhXZFZiNGhSVW4zQnpKckFlV2xod0FqSnFIZGhVWlM4UlJpM2xuY2w2NUZlSjQ1NytBZ2JYSC9VZTJyL3VscDlQbXdMNDM3YjdVUU5VU25LTSt5UStMd29NMnp4dTlMKzVnMlEzakNmdzR4VmNVQ01pRm91QmdHQUMxNUR6U1NmQWtSUlR1ZW1oRWgvRGM1RlUwa3RoWTl1aHI5a0N2cWhOUTJFUDFpemxVZUhONzluRmlnckdwbjF3eHIvSldoa2pONnRHS2kyaG91d1FyTnVwWSt2cW9NZnJ2b0JtU2FVSWlIZHBna0ZjN3ZEQWJGdGhyRUlqUW5FVEN0OFhZc1dQYityNVdzaGJYbFgxZUoxV2pQeXJJcXI5bm95bmpHUUFTYTJuQTZyRkdtMnJLRWxwRWhBakN2Y2Job0cxQk55NmlzY0JUdC9QZnE3dGNjNVBFQlQ0bXBLaEpmMTJuZXZBRmUzUEx0eWhyYUc2NnN5NDZzdzNkVXJlVG9qalhFc2laeXUwbEF3aXFsREtjTDRQbXhwNnV6Ym1UWTU0Z2ZoeCtBSkU2bjdhMmFEU3Zoa0JrTjUybFF0OWxlN3ZqcHE3MHptN3JteUJ6SzA4SlRJa0ZoSEZXOW1LM0JKRWcwQ0hvOVd5Rk4wVTloY3hRdGMwLy9Pc05HMmFDc3Y4ZTU3S2hkV0JOenprVUtSV0t5dWhvRjRXQ3ZKTlp6NDlrVWZOZDFZcDNxVFZBTU1ucjdXd1F0MzdDL0ZUUjN5YjMrbEJiRHZaWUE2ZzhRMDEyV2xma2JEY0M2NGxaTmdmQU50QnFab1k4VU80U2JLcnFadGg0bVNjTys4azM5cmxSVnc2Nm1FN0tMdWhHZUZZM3ZXWjJvWnBnNG5mSy9nTE16ZVJ2cFloM1Z5c29OcitYRVdURCtNdm8ycWVhN3V5Tk5YVDJiR2ZhdmwxbGRNdnllcXFMRWxldW5lZy9rbkYySTRnelpmZlZJTHA1VzAzc3BNZlBTZ3Z5MHBzbHZ3R3MvSU5lUCtBYkhKeEM1VTkvdXF5alkxbVR3bTFocjRJVEkxaUttcDNrbjQrREZMQ3hOdWYyMHNMbGdGMFgzekE1TVpUQXhDT1NacklxZVNpOHVGR1Y2VFE3WTJvOTVndnBXMy9hdzhOSHl1Q29qSUFicDJ1U2lBbEZPMXRTdk9mNmcxejVpRVJROE1odzRRb1R2Y3R5dEE5RUprVHRuUXJ5MWQwb0U0ejJUZWpBWVZ2MWtNRUgycVRZUmc3WHcwY3lHM25FNytSYjd4czBCVlhkckJibTdqb29Za2c5V29HSE1JU0VSRkFNNmhpOURiYlZMVXJ5UlRmUWdaZHlyMWxIOFI5NUlSNjNobFRKWkc4OTBzMEtiRHo0TFNIY1R6RGN5dmxzMG02M1V2YXU1MFdQT2t6MDdDNzk4MEgvSU1mVjQxMCs4Qk5HUmhHcitWVlJ1eUNBWnExVmE1bEp1ZURBYkQxNUlUa01ZNzVTcllyVktOaUZCL0pqNkNGSzFLOXZtZnk1MGV3eHAyTE4rVUNNQ2VKaUdvdWpNYjBPcmhnZFRRa1BZNU1ScmwxK3dsWm9lSkdaY09BTVhQd041ZjBjR3psRWhZWjBNMkdUY1VQcjFBYUsrcGttWkJjTTFwOHNTZTh1ZGtkSUsyS0xwQ1c3dzNlWTFaVXgrM3dQcTRMK0dOODFJbkVIcXQ3cnEzNWRlYUNRUHZDN3VpeU5aRWlzK3pVamVkQ010d0FzNVN4LzZQSjdod2RBU1NId0l4SDZseFh0MTEzVWFBWXBKNkVvMDVXaUI0R0grRFlEZUd5SzUzZmpYbTlGV3RFbjVjK0c1anV0TVdaV05MUnl1aUwvSTRCazhCZm1KY0ltWkdOK0YxajUvVW1YcmxyeWlZZzR2eUllaDREQktGUmVTS2Q3dVhubVJsTTc5eVRJYUxvNHJlSC9ldkNaTEVPaHYrcElybkJjaXVwU0JTU25pMlhOY2N5SnNrN1pXSHJvNXJNWk5HN2hIRUVEcEo3SFB3SXZJdmxycllaMkdUb1FzaUxWYjBwNFlab2duU2xCelp4MGhQTk5sUno3cEk3L2Y3TnlLTWlsTkRZSDAvMURNL0tKb1B2MWdjTWZONVFTMXEyN3Y3aUFSMTVXM0JCVWRCQXFma0lZZGxZUEl6ZXhBRUQrb3hqcUozZ2ZhUkhoUmR2eGszRys4bUg4OHlnRzdoZFNuVkFFbEhQajBmS2NnR3lIVnZLU3laYVFxckRKVndYTkZ3SWEwZDNxWlp3c0xyZGJYL3dOZDVoSjhaZHF5clo5RWZlUXl4ZWFzNjNNUm5kMm15YnBRUnlvZ1pJais2R2pZdU1ubFJnVjRQRG1CZEhsQjVaTjlMTFhlQVJZb1k3bkorMnpRY21xNXUwUm14cm0wdFJPNDV1SmdPQzh1dHZ2SUpFVXZxMjYxZWk4ZmNvUDEvaGR5UDM5eDNoM1VNWVJHTUtsYlI0MzFPZDNnZGpsUmtPOEtVWG1aREhnNVBhTC9ZVmRCNVFXOHArQXl0RU1oSW8zTzZ1Q0dRY0o5RHVyRkMwVGVMazZTaFAwTG5COG5xbkhVMkVqWUx2eW93ekhZYXMxUXVpQUZSa2ptT0ZtTEEvUWF6UnNudks2MW5qNnVvRzg4bVljeU0xNnhINjFsNEllR1d4S0NrUHVJRFF4UUl6NFZlVVJKbmhmbzBEamJqTHRTM2EvZ3F5T05QYlJrblZvaS9SZm5hSThjbU10QWZra05wQTd4QjV3VnJDUVBSNTljbGVCdUFmSEpwVnpFYzI0SkRUR3IySHphd1FnQkRjcjBqdmxwakZxcktXeGEvOVN3KzlNTndzMFN6N3Y4U1ZHOWlPem1QVjJnNldLTENIUi9UYTlRL3VqdmF4ZmZMOVpENDh2bE1DT2c4YmorK2xLbCswY2FZZDhlcFlldlZPMTB1MXFhWCtMREhSRDZJRXQxTm1vNEsxR21hTEM1NUpKdWxMZTUvM0NmZnJIZTVrVHNRd0ZiWldyYmV4cWtIRXJYeVM2NmJwZ3Z2UWdnRHZTSnl6RDdaamdLSUdsNXovVFoxYkJ5NkRqclUxQXM5L0Evem1Yc0xxdkpDZjVOY2o3TnMyMkowMk01T1BSTEs3VzAwNUtBMGFZbE1nYVkwRkVDMENTNTFFVVpnWWNaU2F5cjBBbHdHVjN0eWpockpIZkRXcks3N0hoMU1GOVd0czJtTnp3MEEzMUZTdGpHQXZpNXdQSEsraVpCNlIwY09PeTlWYlVTNFVFS21UNm5iY2J0K2hFczdPbmpKaTJmQ3dVZTZtblZBNm56Q2c5dkwyQXlHeWdrRUxIY25NQWtleG5kTFF4dkx6V2JSYWpkODY3OEdHK0lMblZOcU5uZkV3b3VCbGxCU3ZYOW9uNVdRSWVUSlpIVXd4UmtPQkkxVCtoODdVZWErRUZTWjkwL0Q5N0lTWDhyRUxrSGlaa2NrcUQvVVkyNExEVHVmWitiOUhGcXZOWUJhdnp5djhWRWJqZEg0RVhGRk1XMk52UFRFd0wyRXBOQVRtZGlvSjhMZ2ZENEtGbUFHeUJnanNpSEhRbW1jQlk5dkZTUDdHeGl6MzFDUlV1RiszSkRvaEc4bWxzKzVQVWR0OXhQRHN0MVZvYWVYTEdacGs1VVZzU3FHcmZMTnpiYjlUSXF5d1JVQUlodmxRd3VzOVRpVlkxUjBOanJFQXk4dnJCTEVLZHk2OFh2aWZudnVhVlptOGs2aHYvY3dkK1lmY2wrQ0VEL0xENUYxUjRITitoS3pYTzZPSm5UUEdFSjU0SmljeDVLRnl6dzNoL3hVYm1qZWpqVERncVN5c3FBRXJwMFhZWUd6bS9IbXJ0OXJRZlphRjUyRSsyTWI2VGJjVngzL0xXOGtsdmhsRWxhRnRJaVZkVjF0Zjh1TngrQUI4cVFVRERlTnpoeE1HRnc4ckpHVGQ5K1Q4WXRtQytqSm50TkdLK0JFNzFBNmJPVGdudUtyWjUydEt6SkZTZDVtcnNsMk5zc0RpWkdjTmFnTXlrRUhHLzY3ZEhQYUgvYjJoNkhhZzl1MW1Mc1J3MzdiUDk1dEFtZUEvTDFkdUhRU21tK3hLMWtNUUhUb2w0MHZmaDk4UmVSUFBaeEZQTmdSMStLdStIc0dMWlZPYnpKT1AzbkQ0Mjh1WkRsWGlJUDRBYk5yS0NQSE1EQ3JVOFhqKzF2L2xaQU9oeWJyMkZ3L1FqYVcrdUc1Q25Gb2Y0blJwYzM5cTNydFhTWHgyYUtvNFd5Tm1HcC9MT3VjdCt4VEIrVlY3ZjlwbXVJc3ozNDU1Q1ptOVFKbVhiYnFxRFBtWHZEazdINVFoeEQwUUIwR05uZDFDMHgzTFYyVk5pQ3YzUkRzYjZWUUdsd3FnLzZiR3A3K3gwd1E5dEh5MjFlVEZYU2VvQ0FzeVNQRmdYUEx3SjJpQnZScTZJNXNCL3M0S0RiQnJLVTh3UzN0Z3UvY3Q3VURXWThtdmpzd0F5YTFjYitVMElzMHFoMEp5NURDSSt0OFRFY0Y2UXI0Vmg1VEZNdElnVkRlOWk4Ylo4eE9GTnRHczVSZ05XWTc5amVaeUlDamN4TUM2Z0lwdWJtdExZSjA4elZLdG5CVVBFcXNhaXltL21PSi9NTk1iT1pFT2k2Zk1UTHdlMGVIK0Jta3FPMXMxdzRONmtocHpnM3lIRzBHck1NQi83TzI4Wnc0T1pJc1p3TnU4QmVCeEVCeVJObnRyR3FYc1ZZTkNDZGp4ZUhteDAyZzZJVUhYVTg4SmxlTDIwZWgyT0haTjlqbTdLTDI0ZFV1UjZIL1k3K2RPWEV0dlY0WGMvZm1ZZlNXcjBEdXV6aHppYVdHbjdHRUFRTzFZd2dQSEU0dWIxaUVab1JqODZEVDM3c1htWVd2OFc3bmhiZFpLV2xhZjQxaE1JeGRrTzl5Yk1haE9DQ3A5cU8rOHlOeldnMDkySWVBaGZ4L1Q3VVNndkhQdktUb25DTkF1K1RNZnRMZ3pCeDZzbDJiNmR3czQ0akpIRmkzSlE5aUZPMVV0MFltTWY3bVZxTzFOOHBOc0poTXZQazBvR0t1aHBTS01NbG1BSllCR2x5bmhzVDFnVnBvK1RDZzFHekFjNlBkZ2hGV3NmUnBob28vMUVHZWFqWmQvVWVYenViQmVSWW9rSnExS1l1b0F5NHpicHNsMUlXVkJTSUdVaUY1SUtpVG1veE1CQUQyc1lqU2hxcVdHK2hGODJFdTdLZ1ZJM05UeExjeHpWMmVzVVh5Sm9kWVlPK1ZRd0tyTTlFak0wVFhOc0EwOFMyekwxYm5hK3VIRlZVWmFzWW1zWVVvR2Ntc1l6cmRrQW5VQ296WWllaXdNeklQck1FOXpTSmZnZUgrR3JxVi9BNWpjM0ZjenI3RmhaNjQyc3ZSbHIwTWJic2J6OHM0cFF3cXRrMzliUmV1NHMyeDl2a1BMZkVJTEhYVlJzeTlqME9hTTM2RlA1WlVxbm5RcmJmVG4vZXA5c0N3VENYckZPU3JrTlVnek5zSldLUmJpZDdGc2RvYjdQWm1ncEZZdGx1ZGNhR3A2alZFd0FlMjBhTlBKa3U1bFpVL3Nralc4R1hJWE1hRFh1M21reVNwdHVubWNNUWEySHJTeHNaR0hZbXR4RXZqM2UvWWVnc0owRXlzNHYrcmw2ajlka0hTUXJ6SDZMVkY2NTZTZkVzQUxFN0ROeWNWUjhRN1pSNnRjbkhtc3VNeWs2d0FvQnlpUjVsOW43T21nWHBqTmphNUJpc2RMZW9DeWRuRzIzQzF5MEw3Wm5JY2lNRjJBaTNzbWQ3c1BpOXlBTWxmTlNzNllXRUhOandNK240ejE5c0VYbHA2Z1cvWVd2cE5OcWYwOWRDelE4NGZBb3ZuNWxrdUhyQjJlSGxlWlp4MU9FTERoVnN0Q1FVSC80WG9BR1hEYlJqak1PM3dYYUFxbHJvWWQwMDVXTDVxbHNKRUdQR3hDeUZPMGYyTUtQNERZcnhRZDZrdldnTDc1YUR6eXJPaElwaGJUT3NHTGZTZW1TN0h2blBHdW5xOUpEQUE0N29uNW5QQy82OUJ1cmpBbWswNjFxNE9yV29zRTlacFhPcnFwWUtPbVh2UHVKZEJUK1p1c05UZWx5TDQ0UlNEMlFySlJGeWZuNzgybEpNR245SHJnZi9ITVVsbzlkR0UzY1ZVL3dWdStxTGdZNTlEM0J5VS9ZU05JMnBHRFV5eUZ3QWFsUmJLWWRhZ3hsTWdHem1zV1ZBelc1K05IRXliVFE0S3RoUGNvd0dUUkppMmZsUE04MVA5b0c2WWVDSThxcGdpSEx6WXo3RnJ1eWNnV1pKaVpHemtHMU8zN0VnMlI2UDRQd3paRExDeUprNlhIWjRUdE5VRy9NYUh3cGIxUmRIS2dpcGg3MnNwbzFzaTFybXZWd0l1WTRqM1RSOW41RVR6a0dIR2xTcGZQTDFEVzFzWjJ2VEc4bklVbWlxME5tQmJxaUk2UDRoRHpDR3NtY2wycE1aTlRnQjFXWmszdWE3WjhyUzJEN0xaeHJnS3BFbVhNOG9hS0x5OGdJbUIyYlRDa1dBVnA3Rzdsa1kwVXo3Tkx0YU5IeWx6YUV0VXA0Z2dXdnQ2Sjl6RCtBU0E5eEZnYzJFdkZhdkl3MWxxc0lKYWUzNTA0RzFUV1BvMmkwamVPWEI5djd3cjNSalZkSnJuNTRsa05iYUltdnJsU0N3TURGSjNMNzV0ekRqWW1Cay84STdJRW1nVGgvTVZNYndRbW0zbEZJblVHNW9PVWxtSjRyc2tLSUxkd3k5ZDlVazdibjVUZENRT0IwZkg1Q0tzbzRjT1RBWUZQMEFMUzhsN2s5Y2JjZXp6TlhZU0QxNXpiVHB6aEFvMURrbnlZN0lRamNpU284amNyTU90TTFhUjF0NSttODhRVm9PZTFwd0xtUFJyanZiQ1Q3M0NDRjJScExoSnNZa1FNeUFadW9xcmhjYm43ckdPbkxEcFRWcDdqZFVjSUR1Mkx0WVZROWFocWFjUzRpNm9oRU9WVDhhM1lxYXBFOE5rZlZWMjlyMkxWOEdDcjA3dXh2QTFjYUJqY1M0eEZqazJYZEROT0J0MmNJNVRRKzZpMmgvTkZDVmdCUVRQempQeUtBNkw3NlNVMkRiem9pa3lwOWttWUdmN1UzRGJFNFk4UiswMWRhNXp4aVBPVHkzNVQ2VTlTclRsVlJ5UWhkbU1paitUcW9RWHdicTFqVEpNTEtWMjVIaXMxdDkyd1p3bDJ2dlUxQmgvYUtUT29VdlZ2eDVaK0pkVHFYNmRodWdLNWdsMWxvbG1haTNlbjZCY08wUVMwdlZ1c1ZaQXViNVU4dnltUFd5MWh5azNja3h5Q3FhSW9XSVJOK3c4ZDk0V2tLR3NmS2RSL0xnQi8wemxkSmN1VmE0MDEzVFlGNDZKS1RvUEtna3BpQmNha3hXcDQ1YVpDNUY0RXorbTZ0dVVxT0Zza2tDVUY3YW53VkNCMWxSYlc0UTd4azEzOFJ5bE12Z1NXYkFLeUdvTm1keUpoZ3BDTWlpSkhEZmU1T1RKMDJRM1BFajMrTGhrbm1rbGh3VVpIN2swZWpZejFuaDA2dTJPb1c5SHBSTFBzOXVBcjljckhYY3dQVHJ1bi9XMnJsVk1jMDdySEt6WXk1bUkyZzlFcG94SmJxWmRmSytVOHZqRHZwR2xVMnhFQ2RrRHk1cVkyMjhMenRScmhQdVNzZDkrTUY2eTdDY3F1RjhLV0VidlV0VDk3dHBPVUM1elBSRG5lRk1pMXF1dDZRbGlPMHE1My9YdUZsbmRMVGFybG85YzlWS0hEeDBNRlFldy9GZDdQaWNQVGsrWjNHUUVhUGR4VjcxY2s2enFRTDhGQU9ZS1JaRk44dFBFY3BMSjBObExWWmRkaEJobUJrRUdwcmw3TVM2bWtNYmFCbkkrbnNtRFhjUGNpalFFOHNobjFSeTJHTDNwTE5BaWdUL2lDa3crdWRKL2kvdDE2SmRnZ1BjSERFbVB1TlZZUVcvZTBVd1hkaEUzSXFET3ZyMktUMzdOeFMzRUtHdjBuekZUbGdlcmoxNzVGSkZuNkUyUUJ0NEhGTEJjdVZ3Nk1mY3FNOFhTWlp0RTQvOHlyWCtGaXFiQ053U1ljaHJXVVNtK3AxOEJsOHVDNHZzaVBkL0w2VzE3MHphRUZMMC9iUm9qVE8vdEtweVlISXd6UW5qSnQ0YWFxTjh0RGswSVVSWXJ4VzcxeCtaRHVMYzZRUFl2VVZwTDRna2U5RFNzeDZOMUVrTWoxMWxsRmlTeWhJR0NKMnROQWtMOFJwNlBwNnpML003bG5UeEtvVk12TFZ4dm9JZ2t0NWY1dit1NnJUT3N0SEtMNHZUdFpYblZTdnhic1dSSkttSHVNelpuM3gzN1VlMXJSMjZhdzRPcVd1M0xDSFNUZ3RhWWI2YzRnbXBNakR5TUVpdlE3Z3B0Y3FTelJhc0x4aDhGVGFFeEo1alBIMmNtREFFRW92VTNBc0g4VjdaS2p2WkplNjdWWTdJNkxmZ1I1NS9GRmgvRzVtVVVZM3V1dUhQVjBZYmU1SU9PTHR3Rit2VU9jNjNJS0FyMzJ6L25iMWQzdWIyMWJTNWVZd3J2Ym5QeldqRElKenVWbStsVG1yVklsTzVONHhpMnJaMDI4MW1ydTlrV3l2ZC9EWXR4TWl4cEdlZ1FCTVhyek0zN2lNVjBsOFRWVWswQ3oraElSMmVMTlV2TlRwTWhzM2RxdVBuMWZPempLbGRhNEpyTko5elpQdHdrSWkvbk8vNmlEY0VFMktkUmsxUzlnc3BUVFFLZjI1eGR5ZVlOQnNFZDdxa2RlUlNYN0pKVXNZcUNIeEdqcThhM1VnM0hvUk9XT2M4QkxtVGU0M1lNbUZoSm1sOFg1bUdkVmxrK0o1Q2Y4UWpKTGJWK0twSmRPUUE4cFZFbURhVVJEWlhObVRYZTFhdXhvUXhoQ0UvdWo4Tkl0UlVQT2RCRi9RS3hvam9UVzM4WEZBVXNFNEZITE9LQmIzbExFQTBETThtVUcrT3ZzbG5QOGtORUp3NCtqb1FhSThRZGhsTGJ3dzEyeHMxNk14dkhqTVNiYVY4eUtzRmVNUnRnMGhMczFkWER0U0dxUUt1cUtLSlZSM3NjSmVWZ1d6NmFQbDY4N1lsbmkrMUZOMWFLME14YWhoSWw3akpYd3N6dWhyRkRHbmNoR0hzYUQyR3JqbTlqai9RMmtkeVhpQWs3ZG9xYk1uanowcWxhZEtuckdyRTJrM1NGRVYxWkwrWXY3ZnpiTlFIK0dKOUxZL1dwMHNVUkY3YU4xZEZiMjRqME9vcloyblYrQUFrT002TlNnRjBIVVBqclZrY1h0Qzc3aDREQ01BTkZQTDBROWtudzIwNlNONlowV0oveVZZa2dGWm40cHJYR1dNUWxUQk9HLzFLS0p2eFBhdDJpaWM2VlZKNDVVZFVLUXNmSnFoa0hKUmRySmVmK2plUDAvNW50UElTQ1F1TVFYcnVrSFBZcUJpTnB6L016RTlUZ28vRzJ3d0tKTXpUZmI5TE9WTmVIWE10VmhWbXlqcUpZUGQzQ1JvMCtKcGp1SmdqdXNYWVRKRU9DYlZNTnhKRUF5UGMyeHBYSks3UitUdU1mNzY5OXk5eGdkajdnM3h1MUNqMVhHdTlveGd5Q3ZNYUdwZW14SmtFS21oSDdxQ0NLay9iYmRLM3E2TUFjNmVpOGNWakpqRi9RaGJwMUM3Vks4b1VpVnpiV3dQR09MSkJ3ckpYeEt5bSs0aXorZUNMUWMyTTIydVVvVCsxQS9ybkpaN3pkdWloUUZOVldFa29udVlCUUR3cjhLQk9lVTllZ2ExSnBRMVpQTnkyY1JmdjdleS8xVStDUmVOZW1sdHE3bVBIbWJpUnM2cVNNMkZzUVdGcUtoQnVWbGN6QithNE56aVNrNVRybEpqc0V6RzN4T2FSV2VSNnY1ZWZodm0rbmVZeTJ1SnRJdXRmeUt3OUVDWDBCeWJFRk12NXFyS0xPMjUwbE9jTkl2S0I4Qjh2WGZXK0FtWHZFT3dOR29BbGM5QkxKdk5VZWw4NTRubE5VM0ttbStIM0RZMXJRcnB3cTlzZnJZMEk5NklpZFBGdW1aWkZZbnd5OG43NFFjdXpWSHJORk5ROUF4Q3h0a0J5YWdsY1VBZ1hHTHI5WEwxSHh5MWlmV01aSDcrS2UrcU1tREJhcE9IN2JTTG5SRm1uV2ptNFZCc1k2S0JKOWx6cS8zb3BMVm1xdkt1VlFIeDVvL2xCQjJ6SWpnWjRDaW44M0VuZ2tESjJTMm4vTUJhbWg5ejFWcjQyU1VJRU82ZnlaQ2doSGhqSkx3NTZsZllVcE1KcmVMOHI5b2dKUU5RYno2SFdHaUp5RTNOcFZhWWF3RFNKeVdwb0p3UXVUVWpneDdJN0JpQ3lmMzhPNkhDOFV1dStKQ0xwWnhlUGk4U3REa1JqSXBMSlc5b2drU25iUERsYjcycGJUV2s0RFdobnpWUW1VaDFJU1B3VUc5ekplS3d2WTF5eFRIdWMxK1ZSdEhtTjU1T29lLzFhOVBMMzdwanJ4UlYyQUJTeW5SRXBlSU8zdjcyRkZnS2xvS0I4L2t5cjBmU2lUZDVBeUZrMkZTYXUzUGxMdmY4T0lRTnBJaVZKbk4yVkl2TzZieEpLMEFabTBqNkxLOStHaXJ2cVU3UUtZWEMzMnBYUGwrekVidzRBZ21aZ2RrVkhCUWI3OTA0K2FXekRiY3duSEVsYUNEY2JieTNtdVI3MEh0bmFuREFTSnpJVFg3UzFSZ3p2bVcvQmVPaHArb24xKzlWSlQ2NFA1UGloRVRCWExCZ2lYbkdIOTFWeTJmd1JXZ1d1NG91RTg3NWh2VFVYa1RSV0pqKzRUMVRLanI2SXdLZzc1dmUxR0FPbHBnOWxQeEZySHFhQ0E3ZVZWTTdwVlNrNCtzWDYxemdDd0VwUTA1VzRQVUo3OWgrVGdwWG54WmdQMkNjV0hRRGFwREVTTU9qK2JiN0xRZ29RaEQ3NUpMYUVxbi9aUG9kRGFFM1k3RS9xcEszQ0dqTHJkeHh4c04zSUtZY0hNcHFvd0NuK2Nsd0xNMVUxdU9udHA5ZVNhWXFzK0xHVHhxN1RHRWoxL1htdjBRVmtLWW9raElneXAzeFM2amRPYlkyNFZOM3BsZTdoSzljTDZtUE5iNFhsRm1NT2dvYkJWSnJaUWVUNGNSZCtqcjdKbjRpUkFFVnlBbit4NXRVRUpIOXUzK0srUlhDZlBKbXQwZW0vd1ZQRGQ4YXZDU0M2Ym50U0J0ckRoQU9BU2NYMEJGeURDR0NTMkNuWWp0c0dQQURIQkdqR25MYkFqRTJBQ3djUVgwckFxRE5GczdacXZxa2w1SGV1Qm1xc0pZQTA2REhZQ2VWR2g0Kyt0T3duWTZoQ0EwdDRYWlBEd2dIa09kL2hxWTNNUjBUTFJWL0dkWE9UZlc1bndUb1BLMGtQU2pzd2F6SXc4a0o2Y2V3VC9JZDRjSUNpM3pHQm9FUk54ZENEblZaNXRxUnpYWUZia0dkbis5ZGhkWVU0Q2Q0YU9vK0tiZGtUVm9jbGExUmQ3N3Qzbnl5VTFYRnVUMFZnRnBXMlREbFY5TW9sZEFzaWxaSDB0UEZjam1UUHFzQ0U2NGRpakovVW9rZG9sUkNqZ2ZmNGlVR2dsUTVUN05rbTFlTE83K2FyVWVtTFBqVWJCdEVIb05Cd2dkWk1vMHFkdS9KQUJ6ekhGUVgyamVtRnFBTTlXa2ExQTRhWHJuUlU4b00rOHhacWF0RjZMb2lFQ0VVQTdVWTVNODJyd25UZksvemRza1lJSDRsUmJTc2FqQzJZVmN2ekRaYk1pMkJUbTFncCtweW5FVzdZcDRxa1ZaTFd0NmNKM0EyaXkvNjFJUWRibXdsQlNQRStpcGRrS0dzYlpKNytBWkN2R3lkM1liSU50U1FSNzRBd3AxOWhzOHdURkRFNlJNNXV1TFpJSlh1YVBrT1dYMUd0VmpOWityZlBRdDhJeG9rYXRFNU5pelVtTTIyRlU3RndwYzFTOWZqK292RU5RR3pUMnljdG9PWk5sQWJleXpVZ3VteWNBdHFnc2FXRXY1enpvcGl6ekpEUXord3VDbEdFaC80QUpCOUJhalkzNVJ0WmlUN1ZGb0JKeEd2ZDlYVndJY0FXRVJ1UzMrK1NtQklWWWZhNDFPamRtV05qSitQUDhhQ1gzbVNrYnpMeXVkYzVzZEZDbEtHRmQ3Y3A5MC9XWWNaVUpuTkYzcy80cWxhTUFhOHNsK2FxS3lNMndHa3M4MTlQTURkZDRwaWMraElrZUlEMFVrOGVJY0YxcisvbFBna2VIc3BwUkxQaEh5VzNaOVE3SzlXTzJVSGxLSmpTeGt6Ylh4NzR6K3ZLK2djRDB3ajVzOVpPaHlQcGlpa2dCWEpuMUJ0ZFJCSW12ZmRTcGpsL0V5MFFLeWZHT3RpRjFXYy9lS21MK29CQVA3bDZ5RFhFQlhtdU5rNmhvdi9FZnorZEkydUF5QVZLcnJLSU1GZDV4OHRZcUpyTWdFeDBIcDRYd2dERG8rYWNUb00yRlFFUU1PandmU2gwS082b0kwUDhwR0pocm9wTWVMaFdwS3piRklMQjAyVEdQWkRtTGNCeGRmQjBjZ043OU9qaDR1My9TMkQ3V3R4YktOS3g5dmZLaVlwMFE2N2QzOXE1NXp4azVSUzcyODJ2ZE9jMm1QS1U0MnFuVFhyb0UxNk9vOFdaWVhEdU1DR3NXbVlHa0d3QzROaE9kU0oxUVVINkVOZjVjWkR5ZWRTOGpRUHRHMjBQTXA1OGdmQjlud3N2YTJBcTQvVkFvZlY1czlhOHJ2aGtZKzF5S2puUkJiWWY5T2NIbktrSlAxc3V1NVJvWUVNT3FOVDAwblR5RG9hSzczWloxMlFWc0JMVERvWStwWWoxSTNHQkpMeWdOQUE4KzlRZHBMdWxKeGxnQmM2SHB1TTZZV3RWdHljd3VOb0c1WERqcnU5c0RLVnRCcXh2WTlvbEJNN0hFZk10SHg0Wm1tUXRZSjBOOWUwZC9JU1ZEdlB0bCsvQWlHSEtweWNCY3MveU9SU20zTVQ3TjVBYzA2ZkN3SkV0bjkwV1RKUERNcFp5UTBwWER1ZkZZcHRyM1BBaDlKVDRyYWwxeWNVL2ZxMEJZQVJMdUNySStuTWc0bHVJdWJtU1BnYjY3N1lYOVhUNmtobTVnOC9WSHQrMEk2RVk1enBZUVhqUGdaaUFWR3ZQYzRvbEtYMzVMMDU1TlQrUjRsbHZjL0Y2WkFWVGZDbDhVSGhYTWNxbk9FcFFZVEVlTzNwMXFvWkhBYTB0eEUyWE43eTJkRk83dXU2WnNrSnNhc1JBb3YrRFFHVWh5eVFGbENEbmpYMFdqZm1xZTk1YmJvV256eFJVeDk5N2FISkEvUTQ1WUZEYTczckIvRmRhc1JpVlVJWGJzUjJEQ2FqRkJVc2Q4ZkRZcmZmRU9VTXQ5ZDd1SVRGSzNuRGVOL1RyZ3F3dVhxTUJUWlpObUZ3U0RwWkxvMVY5TnQrVUg2cWVZekx1YndOUCtabmJBZTFTZzdIOVRiL1ZBdU5ua09hR3l2NVZrMncwZDJGNjVmQlQ5dTRmclAzeWh4SmYyU1QxSUMzWjBMV3VqbU9QUmN0aTNEdDdBL21BcE5ZUk9uWjJCemFvZGxKaVZGUjhLL2VUZFlNMjFtcTQ0VXBmZTdwRFpOelk0WTVmc3QzbktpTUpiQXZES0I0SW1UczFjY1FaTXhISmRTSEJRQldEek81aWNzbG9ORk9BQlR2VHdaUm5RSzNMb2RUWmc0TTdpRWE1QWh6b1FZMnNpemM4dTFhVExxbU4vMmxqVW5KblFGY3BKMU8rVEFHNm5IaU1Xc1NaTlNZekNvNlhWV1Bpd2Q5UTZneW1xQmg4U3hGUjU2QXNVa1FncWU2eE5KZFVDT0c4ellTN0hHL1h3STNOK3ZHMEx3RGZJRW5vUHN6SkR2UjVLZnlFSTJ5SlV3azByeEliclJmaEZsTjREYm9tZFFBOTkzS3I1N1JNNTI5dXd1NWF4Zk9CeTR2cFo5QjlKMzRORVY1bGU2RXN6OGtkeHZ1RzZDc0ErVWdGdFlzNCtQUkF1eitJRFNQaEI1NGFqUTVCaitiMU92b0hvS3VBcmI4eUh5ZEtTOXozd0o5Ylp2aVN3SDhTVmNrSW5lbXoxN25WYWQ3N2dDVlJyOEJsbVBCUk5ycUQraW5LdE9WZGtGTVc2TldGak9KeVFuUjI0VUFITW5LUFEzQnhxZ3M5V0dIRnlaYmkwZWt5d2Z6M3pxVG9jQ1ZCdmJtcldBVkw1NnRCcVR5VThZYlRpdCtZZVptVTNrR0ZYK2o5bUx5Y0VNQWJ4ZThxL2txU0k5YTYwWmN0R2VBcnRxZHliaWFyZjVMeFY5NUNldjkvci9hSmlOMyt3dnJZSEJjbkc4dXRrZ3YwOEtFVmF6OVR3ekRsMWh4b3Fvc0RMZkVMbi83eEFOUGtJU0VGaEpmQ3ZtQkhJdUUwaE93eHBzNG03M3A3d2ZUK09LSXVveWpNMHlZVG9aaDMvdnczeVFxRzY0WGRQaDhjTWZyK2dlNFhBc0l4STlLaXo1UG93OTUyWVIrUDhtYjhSVjJ4eSswdDlkenl0WnhsUDloYVRFK2MwYVcxdmFjeWRWMGEwWGNYTnRJVkU2dVlvb2lQRnFBdFp2bnhVaEtYU2tJZXArRitNL2NsOHo3OVZsTHZnYkFDSzNxT0NrSi9jNVpsV1NTMHdkRVhIcGFUVzVpOHZidmJyN2hrMDlKVm9Td0NWRVplVDVVSTdSNnZmZmszSFpTVmNqdUtvUkd5UVdPNGNDMGRtdWNSRHp6MmF4cll5QnNONkxUc3A0V3ArVjY0K280VFpSbkhkNTQrR2Z4bks1TUdLdEU5Yzd2bkd4cVpoOEJ2YmdCeUxKVGFZdUR3ZVJ4RjNMc2NRNGxMRmJDV2xRV0lMcjNGOUtjaksxdisySXUrNUV3NU93ZTNzZERnMEgwZ2swcnpyNDNLT2RTK212L2NOdFNEcGtwcHV2aE5zM3dFaUlRQ05qdUhUcjNObEJIT0tpL3NLMXZZM1pwajk4WURjNGcyVU5BS3czQnFKN0lZc0E3QzVDUUFpVi9BekVSMGFVcFRjUkhzVWpSeGtGMEMyNFZyckxtMDVhQTFPMTFVTk54V1NmS0RNU2xIY1d6QjlYZkpMR01qRjNxRU1jSjFHNWZUSXRMREtSZGk1L2Fvd1BIVHczcXBLU3RiWWRZY1RRLzVWSndlWldEOFpWeVp0ai9RZ0Rra1lOckhhVHIxQW1tSHFRRThNWGNQYW1ndTRaTlo5M1FEb2tBbW03RG8yN1g2RXhJb3Y2ajVJQzZIQ2NMY0oxUWdXY1pHSDJibVRIZS92M2lNS21Cc3Uyc2xUb0dUTGU3SnNiY283Zkl0Qys1ek95a3QyTGJIOVY5QWhBNlFFTEttUmk2L3I5bGRnRU5QbEc5aHZQMzVWNmhLdDBVMTMyV1pKendjbEdsWnIrMko1cHpDaDZ4aWNBTFVFUjdxU3VOUGhCSXZ3cXVsVGdHaVJTVWFSMHpqSVFtWC9qcVFaSEpNQUx3SFh5cE5VeXVWNkgxdTZPNnV3MW1pQkM1cUh6VHZsZnBiaHBlTmJ3TGRtcmNTN09CdTJmL2Z1WVY1KzRNOTFLZ1M4M3FZdFlNd1gwY01FUG1TTlRVRGl4UHdIblZUUU5CQmxVeXF3VVhQaVFWTEhscXZpdzR4RnlBaTFVTzg2cjQwWEdaaDlIQkdXUFFwSjdmOTBleHY5TzhoaU92K05xRXFaeUlUZmRDVGkrK1RZa0RVYkcyakRseTl4TEMxaVVXU2lGUEdwQzM0S01OL0ZYWHNyRkF2amtoTWh2d29nUFYvT01vUUhUaDRIeTI0cmlNSTE4YkNveDFuUW1ab0wwRUNqOWpMUkVLTy9QeG84cnZPWVF6Y3pKdXhSQlJwRnNRZVk0U09Sa3h1WGxQTHN5TFZpb3FwcG4rTUVMV1ZqUFBaTG4wMEtRSHRSSVlhei80cmo4MVBjWGUyMCtIRnZ0K0FZei9UY2ZzbHA2U2NqQm5CMVdwTTRjcVJwNGM2Z3lvS2tvcFpqdU8xRFRzQkVxdW1uQXZSRDlQYVZNS2JqUWdTSEdFMGRqQlF1Tk5IQy9yR0o2MlBqeE9tMzdqRkNYL2hjRHdJNEtHUDJwWW13bVFkK3N4L2xBY1pvQmZXQVBXVjRCajNLNEM3dlRqOUxtdFhQV0dOczl6YktoY2ZEQWlyVXRPWkxHQm9NUWdKMU1UNGlKMy9ET3g2d0VTaERwQVlrdkR4bGszRFBXVjFWLzFwYVc1ZGpvR084N3NWQk11aHhUYms2NTdSTXRuckZtcW5PQTFzd2Z3aDFocDlQQUpWTkFaK3RrWHVFc3UvenVFS3E2WlltamNQL0tDT3M2UTRpdXhseFpzUVZZbXJzTVdNcjBVWHNLaWVxakVLQ2FKQW0vdFVxVXRQTVpISDhUTkU4MTVRZGQ5eVZIdkJISm5BeW5GbW9BR0dwbjdqRGFUNzM2bzAyRXpkTlFXb01HMmdENVlwK09pY2wwb05taC9BbVQ4Zmw2YkY2OXJjd0NXRm00V0ZXZU5xbVJNWklVY3FtaHhlTWVWNS9MRUliVzlQR1dtb09tTkZYU2hYTzdKZEpJd2V4eC8rb1gvaFROVjI0bTFyQ3R2SGx6d1JySG9HeHNFN21wcE5ZTk8wV09XQW15NzVkL2FPaUZ6M05abUc0N3ZnYlNQTURLYjNqcktCQzUydEdjdnNwU0lLZ01nVmhlTmV0RUFGWG5HYm9KS0xRcmRzQTFXY2FwT2N1ckxRZmFVTnl6NWNVUUR3V1FFRHNZVUlnYWsxSkl0YjNYeXp5NHcvTld1cGtPU1BCZVo3dWp6OHhlY1h5alRPRHFidkdPTU50L0toTk9xeHpkbktYcUxiWFVVdFZnUWlwblVzZTU2Q0tvQUorQkIxcFJ4TVdHRFUva3hNT1NXS2lldlF2SmNxV3NtUDUyTk9iRlZWOG9KdWphb3UybU93K3lrQndCV1ZvYnVCUlA4Y1JQd2dBbUFjYUorZG9JMlNzUkovYWZZQzR5c1VMRmVXdzJYV2E3Y01nR3FXdm5FNkg0MStJNitRR2szbDRITVRZS2JveGtGSFFWRmVoMlVNSkxCWmFIQi9Dc3M1T0dRTDRkWnlHdHlkOFcrRUc0ZFp3UGpJWlRNbFBtajFhZEV2TThjZDR1ck9WY05VUHkvOUk4Z3BMMHRwd1cva0VNaUpBUmVmOHR0T2piQ0h4ZSszdzlENENiaVZteGt2QTFWUXpOZXV0U0R2b0VMTkhueXZ6b0s1YUdWck9Kd2JpVWxlQThqV21FTDNpTnVKYlJ1UTNxNVlPeWRkVHNqWUpYQ1IwYzI5bUZYdms2NFNEMm5FS3V0bXBZTXZ3WFNUSXVJTEtJekg0Q0FnSUNBZ0lDQTgzMi9RNzMyL1ZTWDIvYUQvdjliOC84SDJGZDl2MlMvNy9mSC9oNzcvTjl2MFN2OFBxWHI3ZnI1YjdQc3o5KzM3RDc5bjJoSDdmZjQvWjcvVGgvWjd6WDdmcGJyNnZySnYyZlh6NzlYMVZQNnZySmYxZTNiK3IyOWdOaUF0TTZRb1BxenRkQlB5ejB1T3QwWjZRVjVLM2szTmRkS0Q1ejRvc094dkMrYjRsaWVXbkR2cGJ2cCt5RzdmUVcvTXo4R1l1djY4aE92SjJJL1JYNkk2UkczZDIrdzVoaE45MUtnR0lxNTZISFErTU13d3RhNy9HR2pQTzloNWtEdkJ3N0pUSGxrMlpCejl4TVJBaDh6MGpyMTFuL1JVR0hJL3lVV0VCVjBvbVNYM1JWQVhrZnZkYjVGMDZoNkMxSmYrcHhvVEREbjJuaS9IYUZTWVdwRmJtSDRDbWF2ZElsOVN6TmxZR2hlVkFTdDQrY3ZEdHQwMTc2NDVYNkZ2cHpYekU3UDF1N3Y0Q0ZwaHRNVVhSa0FTRzBBMVNoa3N0ZTI1bjJPZ2ZTalhQZHpVNis1RHFxdkYyNXpBaFpFWktYamc2TjZWQjJZNVlrZWt4OTVBVnRWSERudTdDUDJmTjVzZnIxcTI2RkFaVHpMdmE1bUtiTHpFMzVsYzBWcnNJaGNqVEZoeVB0cFRPYmsrS1lpZlNram1uR0lCa0xzQTM5N0Mzek1SME80ckxSVEVodGNOdHU2VnZWOE8vaGRPTWdCQmFjY2t1d3RDQmx0STE1RTBRQitPZlhaTUZ6cVIzNCt0N3pvSG1IRDZUZWViOUxpcWZrd3RtVXY5R2U2Um90czliK2d5ZFlrSzdpR2VHNVE3Sk1nTFJhZWltYlRaZkVlKzdaeXdTc28rQ1F4ZkxSL003VmtEczNHQ0tSaUpRS0hvM3ppczdFM2tPcjVMWFhBRGtBVW56UHhCVktiUDNMWDIxZnlTNU4zdXJqMXhTZElJZWxZRXRnaEl6TnBwYnBQQ0VDdnFkekFoUjE3bGVzLzZpUW1CeXlneE5wRmt1d0hhUzA5dXVJU242U1p6cDlrZ1pqbjg1R2dkTUkxV0FUWURhblZWNDFrbktRTzVodzErVndMNTM2N21sTXQzQ09UdUpaOUFjekIzbjMyWXhKbVNTZXdyNW51V2JGVGtrcWUwRXpJNk4rQTUvSSswNmwrREMrNUFxTjU4WTI3eWVBeFJJTGhlU0krVzd6Rk5Rd2FaZVlQN0RlL0RIVjFlYldPMFQvUC80UzhEV1BwQmF1M2NTeDM3elN4Mk1SZnIxNVVrQkI2U0YzaGdIQ0pFVlhFaUVabDByRzQ4ZzhKRENUczllZnFRaWgzSzVVZ29kMDY4cnJZVHJvZURwSkQ0TEo4TGpTTzBpd0tpQjh3c2lsbDkyMElkZFNPaktTUEt0TGpMTjRsNGhpYTRBVC9lNS9VNW1YN3hSa1M5WXBicTlFcjdRcE8ydWpxUVV3cXNmUGxrYXBBUEU3WWRNWjJMNldubUl0dzRCY2hQTWU0b04xQS94ZGFWbjJIbjdCT3Q0R25OZTRVN0RXV094M0tiWFJua0RrTWFuRzRXVWN3bmxka1VMMFdsNzRNSUR3NTh0MHJqOHJLRVBMdzVmK0M3VHdxcXEyYVZYSmg5YWthN0FUUXVsU1hYN0tYTnZ0R1JORGFERnpPSmxjYStkUzAvMitZeWhtV0IyL3MzQ25pYnVET2FmSjE5eCtsVnp5QUdHcUZvUVFFNzRWcTlJaFh0eUJZcmpOcXRRR0xIaGg0eTJjTmZvUXJlMzYwVnhEcHg2amJrd0FobFpzOXRjVnFhaXh1OG1zSTBKZE40VldMMjlMYi9xUnFST1ZiZ20zMWdEamdBOHArWmx3eUpBbGR0ZVh0YjhPZStyZUgwNkZNQW4rWld0dlVRb0Rpa2cyRHhTSElpMFNxMFRaUFQvcURZK3FSNFhUcHlYWkN2WEllMjZJZ0tvMUVubHZaM0doZjlhdHdQZkt6RUdWeUMvWU12QWVZSDBwbTl1TjAxeFpkYUsvWWFqTGJOL2lvcnNWRE9aazJmNHd0cmliM3YvTytoWVMvdW52U0o4LzJQbmxPNnZLRXBmb1EySWE3Y2pCcFBDckxxTzJtbTZiOE9hczdqSncxeHVkSnlrdElNZU9mQXEvdzh6TCt5Rkd1YlF3UkRtUGJuSkxqbVI5UnJqTitydHJNcGhQdVZlNWVkUzhPNEZPN0tDQ0VLbVNUSWZhUk8xT1Rob2hBT0pGUGV1SE9wTGNDVTE5UjluQWJoQWQvT0w0YmptYkZyeHp0ZDYvYnkrUDRsNWhDbWlTY3lmdUJSeUxWK01lemR3MzJsSnBMR1BodzZraUx2dUhJNFZEZW93TEJqZnZYSTRmcTBiSXY1cFJrb2VNOEtDb2tsK3pnUkYvQ3dLdXZSNjl4U3g0NGpyWDlBWSthR0lKMURJMlZDOW4yU0tWTnl5V3NzSFhPdTVLQzQvczllRGt5TXVHejFnNE9Jc2h4NmttYzFqdlRoa2EwdmxLRC9ycVFNWnJPellQVXVhWTRxZC8wQmZPU2ZlOENwVzVoVkdNM3ArTVVPdVY3OGF6Rm1PTTJnU2VqcE0rZnhSeStWMnFCMERIdkwzWEFOcnNaKzk5ZFkrY0dwU1pVR3hUNE5kK2s1dk43blcyTVZjUWhZMUFNb1Q5Zk4wV1BZaVhDZStaU1dnRGthdnNWZWd2NEVZL0VoZTdkMmpqVjV3eVNKOFVwM296YThYS3ZKVnRSd3RjeXlSNkptS0RyTDFDcCtJK2d5aWJEM2MzMGJpZVlzNDExcytkNjF6ZTZxTHVwSStMbWZTdGl4bkZoNzQ4U3RxRVorTVdvNy9WQVZSMHErNGJOTGpFd0llMEJRWGt0L3J2NC9saWdkWjhob0Z2Qzd3cDh4SVZSUEhPaTUyNHR6WVBZcTUzenpiODhYTGxBcmdlbEd6aFhtUS9KampTclhKYmFCMWFIZyt2dWNYeGs4ak1tTUVOeVBWNFFnWFpCRHhZTWxsWW0vb0pBdkk5VmFNSU1jenNTN3puMExaMzh6VkY4RWtqY3F1MUsrenhDWStEaWZ6aDVnUGdYeC9ndk1hc2tuTXVURklxaWFYbkw3L2d1WUhib09kZml4MTAwNmZ5RmJ2dzlBZWJGUGpWeWlEMnZGYkl0NU9uS0pmeXZ3djdpci9CZ3Bmai9WT1BTSTViRGw3dEFuKzEvamJWR1JNTWV2cGovTW1mTUNUY2E3RmR5R0crSWJOciswUnFHWUdlYWRucHJGNk1ZdmI4QlQ1NUVydVJwQ093d1dqK1EveThCNFFyWUorWE93VkVSZm9RMm1ZMVEzYmg4bWJmYU1ScTNqMTVKNFJ4U092OHFXQ0o3SXBvRWFaZkloRG5raXUvT1ZjcGtqOFFUcGdONTFXRmJEeFM3TmNwN2dJQ3hTQTRTUnBTeW1NYm5tSEZvWno0VlgrRnlXcFl0eGlua3BBb3VGZ3QzTlBUMUtwYWhOaEk4eFBkbDc3RFZjSFBCZVBtaVBwaVVlTXFVeFlFYW1yWC9MVi9XOXo2a3p3aTMwdVIrbzZ6TEtWa1VFMDhBODhjanNyL290YW1OaXpkY0x5cnZDS0N3QTVpd2dpVTg3WmV4WkQreDI2TUFsNG02Mmd2SjhyMHRMTzBFRjg4RXZBdTNPVGM0ajhLSzNhaUdPM0VnNEFTRTJyU3dGNkl1dllyU2VSdjVrWC9SUHorc2Vtenhhc0x2cFptZFR3RTVSVURKaXVtY054bUJzdDg2T0FTT09hOGlpOWdwSEx6MU9hQXJKbHBQbVliellEUEhEZEZna09iWWhFeXFaVVBMSzhZdksxbjljZlhveWl4OHdKbTVQdXJtWGdxeUcvb3hwWG1QNlEreVViQnI0L0pwdS9GU3FTWkczMk16MmM2VzBSZkpvZGR4WjlXOXdaZ0ZxYklSZWI5V2kwZkEzVTlhcFgvQXVpakltUFpnMHI1MmxXS1QrRlhVT0xtWnRKOW5RL05DcStYY1RVV0ZYcS9MMDhpcnlCWXA1QzRzVGFUdTE2R2dUeGpEcjRPUy9pQU1sM080N1hIaDRDODhwU0luRUlZcXQrZ29FN0lNSlFEa1pvd3pmYUxsZGZBYWkxVElSSng4SzNyVkUyanZGcTJzMFNzZmpVNTJqUldsR2ovQzNIN25ZWUlRL3cybHFWYmVyTGNOMmpiZjhzaGdvQ2tDTlB5SGJxa1MraG54QVNmb0NOOVpOUFEzRTRUa0dzV09GS0RZVDZmV2NKNkl2eUhabUF2OUlXeFFZU2NIVmZJeEZydVQ5b3RZVFZlaGtzSDFrMllqeG85RC90eU0wSkpZOXlmNzVnTkZrWi9keUo0bmh2ZE1mbW9CMnM5aGU0WmVwZk9DcHlid2ltRFdFd0ZxMVkwbEhaMmt1c2ZhdjRzbTBTTDB4NDhDa1NHRXNmZG9xTVhJTTlJUkwvUWk3SVcveEJiQ0VOSSt4eTZWdjcydXF2Nll3SXJncVU5OGl4cGJveTBwZXhseW51SEZwMklORmtBcUVCTy9pU2lPYnhQemtrTjlwUkthN0VWbmU4VUFuMVpoRHlQK2I2UUdkc1BXdkVpYUlmcWkrVHBrUnlvM09vSXljQzArVVA4aWVoR2J6SjVzM3NLejBGeit1ZGNUNDYwTFZxZnVSMVZYTEJoODExL0NHb0Z3S2pzSmNzMmltVko0NUI5OWQ1dWc2SC9iclVSdUFibWU1NGFET2N3Mld6eTBvZGlYcXlzTzIrUDhXTXBVV0RPSWZKdmpOc1pPZVk4TTFBRjFib2xtOS9md00vbnFFQ0ZEWS82TU90YVo2MGRoS2x4dllrVk5RVTg1Ti9VWmNmVzVvZUdzYmRmUmVTYzhnajNlaWVBbElYVmZ0VVZQRkc2RTlxVTczLzUrWkhoZTk4bWw1dGtrQmVpUXN1dmdyaUN4c2RnZ0tYTkRWazZFdXNmdWJMZG9UQzg4NXc2aUZXQ0JaaVB3ZWwvZ0dqVHhHd1pRN29NaXI2a3NmY1F2VERGOXNSOFZ4OFdiQlkvejBkcW9ncEhkK0RWZzlSYjRYY0NCQmhyUEF3RGFoY3lxZ0pVM0NDQUNDOFR1OGVzSElqYnZ4aXdVTUl2VzNsUU9oRDJBbGdhMG40R1kvVFFaR3ZocGZuWHgwbGNiMTEramFrOU53ZGQzQzI4K3hFYUxydFBrRDRYMzRlVE02YWQ5T1lCZStBSDVXaEp4WnpzQ0Z2bktyVCtySzM4RCsrdkdrNXl5N01raVJTUmYrUlgrbG5OWi9KV0NXNnJwbWtEVzBzTXVISGVScmVsd2NxMFV4cVZIS1h3WlJESVBWZzhHQlE5eXRSd2NsQTA5Q1FyWGtTYjQwKzVhWGI0SXpvZ3F2NTE5YUxOUHh2amNCSzRQdzFYaGxmNjhQb2RUNFpEQTFYVWRPaE1tRmY0anBWTzlCenJqT2ZsS3QwQU9GeTFIMjZXM0lVc1VQTjFJQ0NoYjdwVllzeGUxR2MySHFXSjJNSGUyUC9oRnIzaHVBUkE0cWo1dllTQ0pORGFVQm5pZTg0RHpBT28za3FzK0VoRk84L2c3b2VXcmpaSU81anB5MndxYXU0MFg1V0kvN3B2cHZMS2wyQkNWbFRNSUJ6aGJ0dzNUc3JGeUpIUUcrTWdaSXlHNWhkcmwrMG83RGl6djdsVU1XVnJkbUR0NnJVUEZhb2JWNG9RRUQzc01IMXJRMVd6MUUrWWE2NkZEWXE3UWVPcU1PeUdKSlg5UE56SlNYVTBNT2IwSHF2Mi92cEpDblB0cTI4ZCtJZUw3ZUFCbjREZENZb0pEejd0YVNpTlJXVXYvUVRKWFhVdWRXaTY5VGw5MHpzZm9Ob1RqUWg4bWtBdStiNWtibHF5MkR3MXk0aTA4TWxwY1lOeDd0LzhiWStsS3cyR1JBd3hHem5rRTdQRG5Fell3d3FicUZOQVFyRllKQ0dwYUVuS00yeS8xbWdFd0s4akN0MklWZmdWc3A3UzVMK0pyUzg1Q2lwZ0xtNVpjREtWYXNIWVRVRExRU3ZnUXFZM2Jpd0V2UTlseFhzWnZWdnpSUTNwWXFQcjcxT01iQUpGSjBhWUQ3bVhzZWMwcTZWWTQ5Yk40eHd1ZGFqcWpCSnVtd3hLdCtnS2k1OEFOalU4b2pDUy9qL004elF4TUpvdi9NamVyYXdUL0RCR1ExVUU2c0tTdTc1NDk2aG8wRmc2NDRYbVhkUU1jOVhYOEtuVEZKdlhPMG81bVQyU3NzTGNkWXJTakZLckhLM3pST2xBTDRWblE2MFBZNmZMUjF1VDNSSXcwcDVYTTF5aUJxY09MR0o2U1hBRzdTOUhsM3BiR2RBZUF3YkdCWDVrb0F2UWNqY2YyTTMrakRxZG01czZDRHpxT21OcEczWXZDWkNtbHJLZmZvYi83eEFkK0kwSE51Smt6R2ZaY3Y0cFVCQjZ3YTdoMm9kdXdBaG12S0RoY3VscER6MUhpbjFkOE9UM0psNEVKN0x2aEwrUWdCeGlpNHhxeG1MZHMrc1d1UmJjSy94NXIwaEovczJabHcvTzZZL0VJaExLVVI0TXhtYXJjMDV4Tk1XYWphNlMzbUdhTXFwaExpZnZFczkvdmNnV2VESXI3SzFYUWIyVUw0ejhBelpuU2dvb1k3cDVTMGFoaUNwd010WVk3UUJKNm5RMFltYVhhM3NLRWVNRUp6VzAzcWROazFGYXQveW5iZ2RKcnZwVk5tUGZpR2VCUEQxbEoxay94dkJxdGIzYzQ1TG1Qa3B4Q2VISjJ3ZnAvVU5PK0dNMU5oak1DQlg2aGFCV2ozMlFzdUtqREc0R2Ftb0RVOXc1c3ZFK251VVk4d3pIWnZ3QWJvSExLdVRRL0FiZlZJVitlUlNiTW5xaFdZajBqZXlaMUhsRW9EMnhvWFFVaWtMZjFJOUFuYlcwMkZYR20xYlZkb1hKZ2ZKNWZ0TnRRbDFjM005K0RMMWxDYTVMNlVkUDZRTytabDRjbG4yczUxNFIvdFJHQ05QSm05QUVjMzVrQjV0OUMxblZmL3ZlaVY0Y1g3Y0s3SFV0TUF6NThnZHUxaFZaZldvZW9VM25qbHdmbmlEUHpCR0x0QUF4eXlDUDBuYmViSkdWcXlaL29qTWpzV3l3SXE3a2Jwa0ZVd2duemtuOXpzRlZpTGkzekpZNFhUMXQvRTFna2V6Z0x3c0QwalhYUkFjUkZnZkt1R2xpNkxNa1JaVG56OG56N29UY0JiWlJEWnM3OHdITktucmhRa0htd0YwekZvVDYvRkRZSE9mcVNEbThoNkZVUjQrTzNZTnF1cVYyVzNNZHBDVC9QbnJvaWhxL25NdmVnT3FNWjJKOXFvbHF6ZnoyTjYzNGtoSjVNTnRxa2FGdFhmZnllR0toV21TT1k2MlJhbGRzRnkrNWNYeGEvQ3VFMDQ0YXA2TTUxenVuQVdDYTRNaDgwb1dQeFV0cUowTEVXWXhJOUhMdEs2cytOLzNhODBNQTZVUWRBbUg0eFZaVnJEa1VTME9aV2g0QUNoK1dpbE9BRnFkTFhGdzVrNFlyc2trY25KRTNVYmt3UHNMTXF1cnFpNTFOaWNyQytXUldRRkV0OFdaUGlwVUxQWENQK2g1Z3EzZzlwS0pBZTFvbzM1UUJjMVFwb0RQWHBWSlNjU0VqOVgvWC9IZUo4b0Jkd0pYYk1xanVSNUdUSlZaWkxBb1NuVEtJZHZQckNvbjlxTFNmcmVFR0pmM2hoaG5PZ0NvN0lya0ZpNitjV2s3RFdSbnBWK0VDVE9HS2hVSjVrQ29teUMxTTIva2hwUnlxZDlmZEs3TkxWc0F6dTVoNnlFSExyeFhPNGxLNFgxS1Z4NGtNNWhiL1Z2UFJldUg2MUxzSjlOaG83S3VDUXdjOVZOeWZxZkNjYm1tcUozWUwrWWhwWlVKSWRqdk9hWEpRQkxyMHZ1OHplZ2NDZHN0VEUrM3kzU1ZnZlp2d0hpTHk0d3kycGh1M0NQdFFsaVVDM0tDMlpUMlhacmxDV0Z4M0dTS2pKNjZNZzV3cnVkQ2pnNTQ0djhmYVFibFUwZ2NOOUZocE0xT1I0V1l1d1lDaGozMW51eFVJdXFUZzhGdytQWXNPc0xYU2kyc09XNHdnWm1XQ1FTejJaaHhxQjJLQzVJYXhTZldLMVJHaVdpNWg0QmtDUEpIaVRVb1BCaEhCeGRvT29qNEpaUEZuK1g0bXpBT0dmL2VTRUF1RXNObHNFZ2w0b05aSTNlSm5nV3FZZ3AydVdJTDYrSGNWc0t6MllmSzRWNEJVUXprLzNEQzlWSDZSK3gvV2xPQzVHT05qRUFaRUV6SnpGMmkrRFBSRkNadHAwbzQwbmRBb01mVTlPZm5jVm1qNGc2NHIreW1mWm9NSlFMMHhKOHBGcXlhc1UyNnptYUpOSUZmUUhRWkE5WTNZVkUvbGZXZWkyOGo3Q2RNc3FDR0QxeFNtQnpUVEViZWhFenliUzUyN3Vja3kzTG1Uc093d29CajN3TmRxRUJOcGQzK1dDbXNndW03b0tiR1REM0l4VEtXTWlSMTYzRkxmNzNnMEFoRTEvQUtDV1lHTDloZkRGWWUreGtFV2xiRGZHeTNlRHNhUjNiYkFNeFl1UXBKdXptbXhvM3F3eTBtdGp1Q2RCRkltVUJoeHNnRUQ1ZEI0ZlZjT1c5ZjFjVUU3OEp5dFBiUVJrYUZjMWwxSHc1Um1tVmJPZEdpNUxtSzYvUmVNMlBTRHd3YnJ3NTU2VjRFb0RNWmZBVjQ4SGh3Y3ZxVjVSSFNUQVdVRmlUVjJSeXZOd1hzV0RzMnJFYVBaaWZWUFBIVUFocEZWclRVbEJMRDgwUnk2SU9uV1hDWFlDZ1VCVmhZdUkxcHVjcm9La1dtbzI3ZVNoZk5UUkUrdXpYd0NMVTBIUFBSZ3R6YkVTVmFON2RzbXU4ZjJVeFg1TVJZTC9hdTdBUkNad045RVFXQmJQdGFER2V2ci9ZYjBOeC9acis5NTRDd0cwWVQ3Q1pHZVF4WiszeHFDVTdrcGhDOEpubXBSQ3gyRnNBOEkvUllrbHNOOTYyMnZoZEVyTEFUZUdOc3FoaWhYNXlLMmVyY2hUTEoyQmVmWGZiM0dpMUZuaU1sM0gzMngyY1lHNm9LN1IyaVFxMXFyRXhnRzJOUTZ3bnlNWXVEQ1dWVytOeThsMG5NS2NQTzVETk82Sk1hcWx4eXFUTFUvK01mSG80Um5NbldnZ2h1YXBDMDNaVkEyN2tBMGcvUjFoS091R0FWL1FadDZ0MUthRFBnVnY2eW80WnZMYngyN3FTYjN6V3FXMUNBbHlKS0wrbUZJMHp3K0NEZ3hVeElaSUdEQnhlY2dMM0NRdUE0VE5qUUdoRDc1VWxoYkFob3UyOTJMWHlwSkZYR2FoL3c0czNENzJtLzU0ZXppSmxTemNrbkJCdVV5NlhRUUpKeGxVWURRZmFZN3ZhZmd4eVAzeDZzWXpxWkdOcEMzdUpmTE9tWW5NY3NEWFRodG9KaDRSYlhFS2x5Tkl1RGZkay9iVVNteEFuZjJJMGFyS3NtNEhQam9rcjMzcVVwZW11aGxrZzVxMGtzdkU5MkhZZnBPeVgveUVQelQyR2d2K2h6TGxRZCtCTFF1U2txZHVrTTlPeFFEejhsYmUvMXp6TFFqT1dnOHZMUWJOczB2WGJsSmx2T1dDQzdncXhIbWRXaEZROGVjQ2xTUHJVVHMyblh2d3NyVTc4YTdTUHo5cnhxRXprZ3lvZ3NyN25OUGdpNTZrWSswWHJJWkwyOWV0NXVETHAzd2VOODhTQjdoVXBIcUhab0owT3FzRXBFSkc2UnJmd3d3eGNSVzBwUFFQSThLTWsxUEplRE9KNG51Sk9UYXNpMGNmZ3JRSlArak11YTZabDR5cy9YMm5VNXhHRjlLS1lrQ01CbTdzWlpWbEY1ZFdoQnZsaUdXYVl0Mms3ZG5iOHVZVHlZS0pXSlptYzhxcFZQcTFWTkhWTGVUSm1OMFlSamtmTU9RMU1JZ2wxVS9DZnhoWDZPNXBlSFQ2NS9xWmhvdVBkRTdIV09RUm53bEtYSDVJdHR3UWsxVUZoR3ZOZkNiWmY5dmFpdTVKMjhselA2Z2RxNkNqY1FrWXhjUkNsRURiZVlWYU94Sm1EOVBrcVgzQjZMblM1bStYbldvdFg4Y3hUQ1JSckpCYTArMzJYZzZMUGsxNXlYWjNtR1YrTTVENCtwOVRHNTQzSkxQQi9td2lsS0dJdkhZRFdNOXN6M3VDcEI3QUpSUmJNck96enBuZ2xIRTAzbHNIampYS1BBcjJRTkl5MExWTG5BR3FmaWFqb3JqVFRNYVRCMjJNak5rWVh0NmdTaEd5dFpZQ3o0T2MrbzRUa0ZHNkNwQkpOMU96U1dVNTFPODk2aTA1bUVDUG1zbktoQnZUQUdkck5sdnlFY1c3Qk53UVp0WWFkYmYySnArT0RMNGVNeTZrZERVUUlmcElheVNIbUx5SHNQT0FndFhsdFlWdGZyUElVOFBNaDMrK21QTEhCeUkzd1plREhQcGVSMUh3UkdSWUt3OTRwbW11a0JwRkk2YUdkSUsrNHZUekVOSDdLV2FKTmdySGpuKy9OR2ZJSkMwS2pGaG1QZEdWY2NwNVdrekZkYUFxeUg4V00zcGZMUGZpd2hNN05GUGZqVDMyUFZUaWFxTkIySFJnWEpEWFMwdTJVMk83TnpHMXVLNnI1RmFKZ1cremZwd1dBaC9IV0s0ZUI3eXlDYi9VK2g4d3R1NUNWSCs0SDQ3QWw4eldXSXZQMFFhTU4zNWRGNmJRa3V1TGM2Rng3TXJqZDQrY3JReUM0N0krUHNhSktjTXkxelNFQld4Z1pwWlM0MWlURFRBTVRqTXNUYkVUMTI4a3QxNmNnY2swY2R4eWk1WlRRZ3dGZ1dFQW1Oc1VoOHA3TllXclZoL2ZFb1ZMOHA1Z0ZBYStOYllCUWY0emI3ZGRla3ZUbFR4bWlFVlcvcmRORUhIbDBDY2ZsaDFqK2tVb090NG12NlMvVC9Gd3g4TWZSQ0llYUovbFJPei9ScE1OSkRNZXBhMGEyKy9TS2pJKzNzZ1p5T3pEWndMSDkxNnMzU2ZxU20vaGg1Njh2WVBJdlBEV25xYWZsd0hyeDN5Nm5uTVJWdDF0bjN4dDZNMGhaUFN5V3BEYm5OSzg1aUFVc2VVR1pTbEpRTTV2ZFc4WE83THV1NUxsUzdHeU8yTjBpeVk5UjlIdEMzWmszSXI5Q1lQOWdSRkhoOWFKUVVTSDh3VmZGQk80UGZzVXRrQ1ljQy94YldZSTNBYS9DSnNWSmcydGh4dGdZaEJZYSt2ZDJUSmlvQUFPb3h2SHRTakU2Zld0R2FsZ2VlckxKVHlETmdLSGZ2Y0l5NG9xZWlhR29yNWhseUhDcjRYUkdJYmhNSkh6V2V1WnFvMzBqSlByYmpMdW81ZThJQ1dFa2lxYmV4Z3ozWnpQekJ5NVpHQWE3M1cxQndpbDZXbHl5aDZpcDloWVNWRHArcFVuZHhzRUhSMWpIdlpzZmpyd1hzMVY5M2oycDlXd2pSejBVdCtobDN5b2lWanFydnZNeklTKzQ0Wk1kb2xxUXBmdnltVjFoejRKN0llcTcrcDdQelA2OVcwcWVEb2E0VWJZNStJcms2dERNMnptMGZNQ0MxV1RzN2N2SHVVd09MWlUzUHlicTZ1T3p4M1oyTGdDbDZ3SC9MZGdjdm84aUswNXJHMHNLOFlZWmd3YkhhWk1FbElicTJLUVpNd2VnczFVdTZ5TjVJRHZTL0c2d05uZ2U1K0UxZ3Y4V3dpUzRqWVAyM3BrMThDZUE1OVkzSGlFL0pHWDlDcEhNcnRiOWd4RXpVWmF2SmRjV2lQQXFFdTlHL1p4dkhWZDBPU3dBeS9ReTAyTFJaUTFIbWRNb005Wnhpa2NBd0QwdDNBbFYycXgvVVp6SHRZWXEwOFdydVY5dUVCS2FTNXFYeE9pOTRYZHZkRDVJNU5oZmdCN2srT09GanRBelRyY3d3QlRUT2djU0Qyci9YYzZZclRiRnRNU0pGcU9BZ1V1RkVXK0tDNmhVbWNpUVZXckl5RlRHcUpXcjFTN1RiTmtYZERFV1AxdDF3ME5rS1lWS3N0bldUMTJ6Q1k5L2Exc2hFeUVvOUhkaG5pdkN2R0VvY2Y0VnRQNm1rNkJKL1FwbnhLRkVzWEtRMEM5ZkZHc0sxMnVlNUtVUE9GYyt3K1BaYks5c2VoNFBSVUF4ekozMll0QXBVVENuN1hDdDU1bDRnU1V1QlYzS01qY3BwUFg2ejZ2TnJ0R2FzZjNEQ3BTUDNRZ1lsZFRSakNsWWdsK3g0azN2cFV1YVU0QVJzd1orZWU2Y2hPUE9SekRlY2ZyZUNRNnJWcjRjUjNIU3oyNkVqaDJ0OCtDTmdoRWZMc1ViQ0d6WnV5NFgzL2toMFNheWFxNFU0U1VpVE5GTTBWVzJWbkNBZDRBVFhkWWwxb1ZwQWYzbGRMd3VQV1Nmb2x6OXZRM1hNMGt4WTN6Z2tjSy9GdThCaXh4Rzc2VTVDbGdWS2FoUG5MYUVyNzk5bFpGMUwrbytCSEhGWDdSZ0NnYldiNngvaWkyM1JmR0Fra3B0L1dWWjU1MjUrU1ZjenBLYWNRZHFDdlpiQUJCbEJkdkp2aysvazlGcE5VUmZ2NmxNNzlIemdYQ2tQaE5XNzFJbGlqUXRvcDF1VHFZMU4vVkFmaWNVUlUyNEo5WDhVY1o5OEM0M0Vqc2VSZjduQ3ZtVTF6cktNek5sL0x0Q2IxbXF5RkE3VVkvRWtZbjNORTYwbzJxY0I2eVV1OTF6aUpVcVlsS3p6RE8vUnFId0Jpem1XNHpBTkhKamZ2Z001WFoxenBJRlFTdU5xaEFQSTlhQWhhUUp0TVpyOXBEc3U0eWEzLzdrQzVnRktmbkkyOGt0YTdaSVhXVHpMazFIKzdjRjZYNUxHNDNONG1hcWJkdEpnRjV3ZTQ4bitiVS9vQXUvWENEY25RV1RRQng5bUpHTHA2ZHFzNFJGT1d2U3pRalRlaEEvMHBjUnJvYXpIcFRUb05uSzhwVWdGd21SL0syTndXRTg4WitISVN1VXNhM1o4eDB5S1p6empuOGo1OHlQdVhXenorZDltdUJ5YmxJVks4cHdKN3J5MFA1dSt0Vy9WSGNDR25aR1BLOUtEbFpYNWpKdFpMRWxFbGVoS2Fsa3hlRHNMZnd1N25QMWY0YlNOcXlNcGtQQnFYMFJmYXdyTmxCTCtZZGRZS1RuYUVQYVAyVk4zbTJTZ0l1bmNmY2g4dUxGQlh0cGRaS0pESVFINUFnai8xNzFWSTdUcGh4WUhBUG96a0FlZjdmR3FKaDFESmdXWXJZMVlCNkhvWTgyQUd0TFNROFpZejd0V2FjL25mVWdUSDlFZXJIUW9hdFE4a2ZqZDlEVW9jUXhibzRKYWZCR2Rubjk1QXFHUTBFUEkzTmVKNFpyREVJS2tyQmRkV1ZkdjhJL3Z2eTA3TTVZWC9zVkVFendIZDRZanNaRU11akdPaEF1MDdtcUpYQWdSV3ZaVkNUK1hlMHNLRWhmL1dEb3pRUkFPV3NuaFdwTFBoSDl2akViQ1N3ZDNQa09YK0NkK2Zxd1RLRml5cHEvQXNuZXIzNlBKeG5JYy9QU1dKME5aeFZGdGEreTFoZU04ZUlqM1pGT0dveGN0Ty9KdFMzTUUrR2JrUnU1bmRncUYzNmhGYzIydTBmUkhXRVFyRzFGMkJ1SkZlbWN3ZDhqallwVVJzMnBSbUdBZHA5UHlvK1p2NkkrZ1dReDJoZC9kRjNLUEM5bzdERTZ6QUd3c0pETXhMQ3E0Qk9DaEhEN3lHdG5mU0pKS1loL0xkdzRjSWluL1J1TjR2VzVCaWJUdTliVXQxRWUwc1ZGeEgwdzl2UHhheHNHUmhVQmFuRTU5NTZTeS91S3JzVGpwODR4SDZheDNWakpvNG5DeGU0V1E3YWVmVW52NnpSQkhCWnIrNXlVR0traEdzb1ZUTzZYNDFDOUNXazVCa21udUF4YWhkU2psdHRzQjhxZDVLUXVqRHZkUElGS25ob0U0OG1qWDkzWGpFSXROV3Jkam5zbytoVS9mN09VcE9XZWx2alBMalBHdVRlUVFoVGRqOEtRUnlmcnlYWDAzTk5lTkZLMGVWVkNGSFJuQ3p0TytPOVlhNFVyQnVseThiUi80WE1EamE2SjNHZXBJQUp1ZjkyZE82ekR3VXo5UVpxaVkzVWdqZGljMElPNTFGNUxpY0k1RERFNUxKSkI1WWI3Wk5rNG12NFFpbjBlKzUyYUtQUkowa2Ixb3BsMkxkODI3aGVqamM1T1MwaUVBUFhlVGI1Zk1xMXcrNkV2N2NKN1VEOHduTDZKT0lMbUp6cjAvdzJQUlY3VFRjcmx5RXM3ckkxbERqRVNTbFJkNHJ5cHI3VUVJWUV1NkVCdllLQU5zU1NpbFBvQ1ZKMktIVW1oSWw3aGNTT1l3SU03QkNodTF5aXBWckRqUldUYWcrNUZSUmxQWVBQQ09ibFdERHZMdmZ1YmUvMHNpVi8yQnRBL3hEYTVNeitLYm9jRGhBZHZxVE1WVUlEeEJrRW5XMHVoYStlVHpWcTFteGowTFdha2EyRDREVTU4SEgxaUVoWUJoYkRiUlR3c05yWExxVUhHK050SlRJOTZlc1hLcHVlSm5nNXRuYjV6R0UwbnZvRlgzNVU3TjZUeC8rQllzMFZNRjgzdGl0TjRvK3VHNzg1cElJQW5xd1lmK2hkWjR2VnN1Rm9zMjFQZHRzM0tQZXBUN2t6STBtaGU5WWF1OG11bWh1a2V2eU85b244TG94dENFQU5lcmcyTmlKVDY5QTJJUHBYWmpQaHpuYWxpSnVvSkhOd3NFd1ROS08wcjNxR0ljTWM5RzI0eVdZOGI1QVczbkljb0d5NGZZdGdkRTRra0lpMG12eUlIZWRrbWJBMy8wSDVvbXJDZTZtanNWQ01TbWNmd2E0M1ozTGYvMUhQU2xCODFibG5tdzk5ZDNqWW5xSXVvN1RTaHlpaW5LRzAxMnVxdC9uWTRrSFVTMHZyM0RpdXEwUUI0YmNMMWZhS3NoSm9RNkRNdVp1QU9qZDFWSnhQbmlETmtGQkpIbVptaXpNbVZsSlFKMnI0SWxsOU9tbnMwczBKZUVhNmNINDQyNVBoRjd4dmdPTmQ1a1gxQmxVY0VLd1R4eWdUQm1vRXFCOHdFWXJOcG9UTDZKK3VpaTdmWmZBMlgyQk54a25ETGtwK09WSWFtQzZXNlNucEc2Q1hJSDJGN0dSdVFaSDUwanJES29mU00xbzZmcy96SFRyK0xOVmpLeVVnZDk0UVorQUh0MFdqdU1uaWl5SDJLcXgxbkc3QWdSNU9FenUyZTRadFVPUEw0UGt4NG4xeUxsOGY0Z3ZxbGp2SmF4amNOWFdacW80QUk4QnFoUjFWNGtTOThYUCtlQnRoQnZGdnFGTUR0a2JSemJnNzFSOGpMM1lEOHU5QWRnZVdmcGp6ZW42QzRPbkg0Vm00MDZ0ajBNcW9CaW9UUEF2UFJSREYvVTBndG15QVd3eDJ2TXAvdHJQdWhPakNibDB2MEZueExwTW1KeFZQSDBwOGYwcnZtR0lOc1hQUFZBQkw0bWxLRFlvSEhVUmN3eVUwb1dHVElGdW1VbWNoVGNOZ2hOcDBLVTRxVjZiMVBTT3ZtbWsyVEx5N2hSREhQdU9VL0VKRmRwVjdwQlZwSmN3R0VRbW54RzFLZ09LV24vTGt4VldpQlJsTTZZMXhrbXNTVWVUQ09HSXdldkh4akdHbWtHWUdUUnVSK3Myei9wZjhRS0VKVm93OElqdy9oMlR3UXY4TFJjRks0ejVueGx0NXhYMGI5WVpjZjBrMHRuazhVZEozSGFlc1dhZjNCRmMxVitUcFgrYTBORFVCdmRhWGljWk01Vm1xY1B5NUdpTE1DQm00NllQUG9obFJXd0Q1VG02dEdMT1J5aEp1d2xaWkVFQkFORnNUNzBaQ1phRldQQmFMNG05WnZSK0lNMFZNVEZaT3B1MFVYL3I3cVoyNHNuSlQycUo4UlZYSlJTMC9OSmIyczVuMTJqUlRvOUtPUE40Znc2WS91RmlER09xVkhFalRVRGZvaGpxS2JVYUNNU2QrUWdsUHZPZEFFM2Z0UlNGSGRTZGxjVnJ0Ti9haVJnZlA1VkxTeVhtSVFkZEZxcVI2RGttSThDL2o5TFkwd1lpTTNkdjJVcXBqVndDYjVRREdLY0JyR3VycDgvaWVEcFNWdzcrU24rS0p6STJocGRUenpqbmZnbHo4UWdRTWZsRnc4cmVRYkZKcldiWkNEZzcxMUU3RG1Zd3BUK2lQTWNUSnowendiSkNCVnBFRHgvZFVwRnFDNFRIRzlvMzZTbFIxTmN1VU5LUmxyWVZPZFMyb2RMeW5LU0VnZXZWbWFoUVd4ZG1BamRERUJSaVhQajFjMHZTSTA1VWVyd3NQNWN3VXdkZ2JKR2xITC9ST3RxTjJCYnN0czVYTHpvNWdWUTRHY2R6ZkNNTHlpWkNMVDhGb1JESEJJNWtlbS9nTzI4ZGQzbW5tbU45OTdOc25rWFpKalhwNjhXNFF1N0F6YjdobVdFQ3NiT2lIbmgxZHB5YmNOOVJ3U3RkV0RJOXBOTGNod3dPQVRiSFZzYnBGcnNSSmdGeWs1MzYrV0xHWWNXUm5LWk9lSDJwVktLMFV0U3E2c1hoRmtXK09RcVNHQThpbHA4clo1Nk10VVVqZng1RjZPY1lPQU90TzBRQTJhN1NJWG9qQ1lCQk1LUmJ1ZXRPTTVwWE0rUDhOM0M2eE4wSXNKanRDbUdpK3hnRVZhdHVNN0QzR3gwSHZtaHRjNzlpYWJMN0FFc3RaMitEVXNQRkk4cXBtWVkrL0FHS1A2OFN0M3d6Tm8xN2x3MHJRMXpqbmZDendTV1c3TG4rQSs4Nk9qdmlOUFJyMW96bzd6OVlYY3pjK0hVaXJNbzBMNG9kRWo0YnROakhFMDJDV1ovVU5zZjdUMVByZUxmaTRtSDNFbkhmd01mZjcxS2JNWjRaUnFGdU5DZVZTUndTZFUySFArRDRTSzd6ZG5aUkZFOGphZEJ5ZWVaZFJxazZLRUhIZDNkQm9yQnRDOGRlLyswYnZFOW5PR0VLZGJZM0hxL2ZlSUNyYzNiUHY0Q2dRaWJOQkxZdXdUcGtiWHo2TkZOaVQ2RW5CRVhSczRYNVRrTmROYzd4TjJGTjhjaWdoYVJNSjI0MFlSY0dYS0lKWVc1bWJmMkM3ckV0VXNxaSs3cG1GOVp3NVRnM2tkNHlFS3A2QStrMW5DRC9PaEVqUTFrU2ZWdW9wNXl5bGtpRXc2aDVpSUdUTVQ0dVJJSVJTZmowUW1ueGF5d2MwT0Y5dzFMYlQ4MXNtWEhjcVV6OVRweGtMVlhpblVVQW1pdFYrbGwwMWRxVEhTSmp4MjBJc3N5UkVoU0FIU3VFWlNRN1l5Z25SdzBEdklraDZObHZadWkwZHFIZHZpSFdiNVdBWW03MGxTVXRzcmNtYlpIbTc0cUs3YXRKdjBTZHZZUHBlRThzTzByWm9qRWM4WDF5QU9WVU1GNTVBZDhXSndnOUdMQWYxV2JyTUIrTUhUOTBqdUFQYnMrbHM0dDdHeENyVVl1dW4rSUJ4R1VVOVpHak1DVlFvbVhwMFo4cHNhdXFkbHJEL09tOWJPd1Vub09XQmZOQ2dQTC9tTFJyc0tHakZEb2c2ak4zcU9MVnV0YXhwL0ducVo4UEFuWGpFTldjUXEvU01QM1U0RmgrRitzeGdNK2dWWUVkeGlsYit5MnhBeVRCM25TQlYzYStwc0pqbFFGZk1TQm5pRlN1cFVPNXlkOUlrSTlTWGNHS0N0ekxGOUplM2M1d1NzOFdUeUZPaWpqTG1rOUxDdXgxV3lhT2UvclUranNwK2V5bDdFNk5BL090cjBUbjNJeVN1SlRsSE9CV2dwdnQ5bm9xcDV6bWlYazVLc1VocXhWdnhJRm1DLzE0aWNIRkNQbkp5dno0N1h4Z3hUb1phVUJTUUdqUzV6VGxzVWRBUzNPc3lzY3QvVWRkUjZGdTE1OU5aeU4wYnlkM2Q5bVRHeEI1TEhBZmxXazdPQlhxK3NjYkZyYlRxb29Dd0xITGt5eUVWWHpDaXhYT0NTak9WYlpwTENsa3hOZUpuaDdHZGJsV3BqMloyZGlqQTJYSyt1WFZQa3pKM3U4RVFYa1dCa1NXMFIxeGoyYkJWM2g1NmUzdi9IMlhzY1BVWkN6RFZ6L0J6alk5YXVWbE84dHkyKzhqNHFBYVhyVmhZdUVjdkF5eGJaY0taZnV1ZUczYTd6akJEamcyWjRwMHlVclFmZDUwY1JReVg1K3NTaXNuRE9jZnZzU0Q4UVlRUWZOSmpvcTdza1d0U2pXaVdFUmdPcjM1MmUyUG9uazFGWk9yUzhjTit1d1hycWU5bjVIS1dsRW0rRHlPUXNLcVdxc2dNVC9HdW1FeURZTnJ5eHVCSnJCb1VzbWlUVXZqSXlnNFZWWVZLSDZKRUNiL1Y0OTRuVVM4WS9FVDNDZE1FZ0dtbkpTTmI4bUdham54U25yenR5MlpwdzNaTWNmems3dmJsMmtZTmtkZDNud1hlZ0hEREU3TzZ2cVVqZ295cTIzZWJ4anB4M2ZKVDFsUUNNaWtuK2pIZmRUSGVqcWdjQWUzY0ZPSUlpR2lpMHdYL2RWM05OeTRVcFBZTjFBVXZZdklIY0s2VnZPanlMSktzS2JBZmdVVkdiNGphOUhDQXJSNDNSSzVLNGJCbEh6Q3VTY3VvclFuREVaM1JiZTJzT0l5WXdDMmRGZkN3SUJyNTR1L3c1L0pMODU0QkVKRUJHUDhhbmJZeGsyejMwMm5mbDNTR1YvUGZTelp6SXlXYXVsdW1tMml2VEgzK1ozUlMwU0lHM2o5emk4SDBsbjI0RmVXMzNFc1dvcXRaa0NOS1kzeDRMTGZsdGlUeU01VU1TdlJWY0ZJQkFGdU5pWUwxOEF1cGU2RnVLbGRsbVhQWFlDUnpqR0syOXNRaWI1N1hVVnFzK3NGamVOZVVLS1Ewb0dsaVVvSUlCbnM1SzNodWdya3hXT2czY3JadFdxbDNqUEl0ZkRpbEQxRDVyS0xZU0FjRTc1SnFhR3V5VUFidnp3TEhqcHFpbXZXaTlsMENHOVZRWFBUR0E3VHF3L0ZNaTAwYW5udnlrUzdoYkZ6eVhBWFFocXdSUVU1Qk15emtJRFhQbUZrUi9mTlVEWHhyT1JaeFczMEhoOVhUcFNBRnhuRkNITCtmdWJiUWFoK3dOMVZNNzJ6cGNFRXBQVmFLdFhmTVRJOEJCbmE0S212bm1RQ0s1TW94VDBjSjYzcXF2WHROUzgxQUJna2dkdWhEVFFuNUdRWWJBTmhHNXBLaFZtMHFXL3JRbHdoRGdVTCtWOFlDNXR6M1VvN2oyTDBoK0J1M2xpMTBoSU9aWXVCUUpLZm84NHNIUENvOTVnd0NCUnBqbGFua0FDc2ZpMGNkckp3V2h6UnBWKzdBM1ZleVBoZExvRTc4Rjh5QUFyaE1pL016U04zcU5sT2VscnRzSndieDVmOWJvL0pQeW10bUF6V05mMWhra2JlSW1JSUFGcXdlZW5xZTJ5WnZVSFZSaUs5alNobGtWbDFXUFVUL2NrQmZkMXlFS0JzZVlvbTJWMnJhck5aODVwQkNpeG9XNmtaaitmc2QrODRIbDlGMENFWnJhL0dmeEdrbTdONVZDTmY5b1RDWlE0eVpCQkZ0dER0VzhQNkowNmdDcXZuOHFvY0dodGZ5RTlFMGNRa0lGVWk0TXRSdFNmdW8reThWK3hFQlcvcHlDaGlBaUZGRFhOa3I3QTdSNWQ1SzREMWZiVmtabnZJWWRTaDZ4V21DMDk0Zll4Skdkd0g0WjJYaGI0ZEk5M1RrVHJZa3hLL3FSZytFV1N5NXhkZUJOQXlpMm5GS1JTQWV2R0x6L01UaW4rVUNyQUNQbzRqWkRkTi8wWWtlN3k4d25heWdFS0dIeXFUeWpFUlVGRnFQYTR4Vm01RVdLcDJxQ2J0ZURCSlFhMzVxaEQvSnBKaGdOazQ4NndTQjR6Ym10Z1hjYW5rUEcyQzVVN1FWYjhkdGxjMTVqQUVNREt3TVdUMTU3T3V6T0U5c3M0VUdWR0dsS1hYMktHTXcvSHZGOVZ2dG1OV282Z1E2eldVZ1JvY1dzSnpaWHh6MGZWTHB1MUxOQUI4K2tYVENSeDkyVzBGT0dvRUdGM0ZQVmxSTUdEalZjLzYxUDVIUEV3VWxBS0xreisxVjN2OWd5T2NRU09qUnRjN2w0OFU1c2RvdWY1RHRoaEJMQ1owb2xqOFJDSFlnNDVGRUs1TVRMVmZJS3JWaW5SdmZWa216aWJEaE5CZkluYm9EWWlzQ1BGbkRVclcveHY0RTY0K0RyRFR4aGhRTWFpMFdMa29rQ08wUFBJeHNBSmNZTzJCYkJDS2VnNTVCcW9nRThWdUlRMDE3dDF6VHhSMitSclk0Z0ZDMjN2NVJFTi9tcVdqUGx2UFRyNWZveGxxbWF3U2FyNW5MQnJCcHVNN2VOalkvQ296MUdrTVp0TjU3SW8xMVhDZi9COTFOclZLWWwzMWtENTI2OHRuZHRyNEpscENtU2Nsdm1pWFF2ck1yNEtwem9lS2R2d3FpWDJjSHhNOU9SRnJZblJmdFoydmJRU1lKeHdxdmpoRzJFRWYvVk5vbDU5S01LdjkwNENTSzh3NkhVaWF4cVZPRi9QaWFHdzlYdXp5Kys2ZFRtcXd2YmQzTjNwTXlGRG9NVGg1ZzVaMTFqRGVNUFUrOXB6clVBRkpuUy9yNXFwR1lBMG9uMmxFOGEvQVQySGo0ZytIUjZtUmIvRUg5TUpqay93OC9NUUxzNlc4OFo1U1RjYnN5UnV3SkplTndHYzBPQkFKRitJVW53SW5ncHdXY3RZL01xZnpFUCtMR0lncDVaTTRaNWxTWHhGQk5lbEQwMHVzaGRqbjBYbG1WZlVXUTI4VmV5b3BtRVNiRm5Idk1PSlJ3N1g0Q1V1U0lGOTg3RzhqVTRubFNRbkYwU1NLREowVmlmRTc3cXNLdklwMDVxa09RTm5TYjUwTkRRR2taQ1pkWjBoMXhoRE83ZHZ0d0F4QzRlelZYRm1jUE5YOFZwTkNkMUkzRmQzOHFVQjJMckpBWTdnWnZ4YjE1MXJ6Z3kxMjRNV3JNSEpKblo5MEYwTXNxRmVQWkZ4am5FN2FhNElPVmdVM2dqVnpNY2ltT0lLc096N3VJYWlPSHdkNDJCaEZ1L3JtRTRpNjFrV0lQZEdDUGdudjNJL1JWQjFjL3lTTDh4OTUrQzY4V3lPckwzeGRWdUVHRk9yczVzZ0R4c0tyOFl4ODgrTFR4TWQ5SGNuRjlyTm9FdlNKV1Q0UnNSckRrNVNRbU5FeDNOZjlnTmhVdlZEVlBSNFA3L3kxeFZkYXhhb21adDFhNzlBTGtYUXFhT01aWlJ4MDJzcTRlemVIMkhOL3BWeCtwbU9FSlhMNVBmczNXRU41R2ZEdHowQ2RzaWZUMkZFR1hteE5COXZqUERiVDJIaHRldHI5bDRtQXhBVHhmd1k5Wk9LNVJTU1A5aVZIMVJRZC9lT0JSUEtrSk1FTDNYaFowL216ZTRwU3F5NXR1NTU0anNRcG9TUXFEc2ZVYzh1NmZXNVhZelZWNTdyZ3lQZFhwZFUzQ2UrVzB4MVF0bWhCVnVCaHNiWUtENTVQRDExRmExb09OSUdDelBkYTJkVHgxbUxramZhZFpjdlA0OTY1RjFoNnEyNVpSRHJkc3JrR0pTckFQWEswUVFzcmQ3R24zdDBxMW1mWi93VmxzTExmaHpESkcwZTNpdEh3TFZaaDlTNlNjQmErU2JndjRsL2pYN3lYQVhaSTNNTHhFUWdPL1hzNlgvRmRpUGZiRlFzTnBYL01DbnFGWkVrTTgvQ3lYVDRxL2w5SzdKL3N5VndCdFArRG8yQ3VUVFNQYmVtVzl0OFBwaHdNMDhvNWdmU1dvSmNQV0lLcWwxUXFxWmJxK3lDUTdsK2JsenNodGhoUVNzUmUvRHlWdHNNRlg1Zlhqd052NjluSHB6NFgxY2NSV05DOEg1S212SVNMdndtaEVXZkRmYVJtSmVpZVpzR2ovVHY5VVhNMHpDQlhzcU1nZFJHWkg2OWQxRlBrdHo5cFY1aFhEUTA5V0tUZUFZdEpieFc2NTJ4S3ZOWkdpS3FNMFBaRG94U0FVZ1Y4YlZoeW9yR2FvV1lkYVROWjNZeS9PTU44NFkvNWxjcVF5WjZ3SmxPMWdjNTlOYTNLVEhDbWQwYzNCRjNrUnRvYjhHSDJPTjBKSHp0K2lSSGdTWGc5V3Q3OFk4djBCSjh1WDAxWW03UDlmVE5aZWplMUdZTmpPTGxlMFIyUDh6dG9lVG1aOUxqOXRBb2QxZjlra3NsTmxoRkRCT2Q1VVdkK2lvdkk0bG9IeStDd1k3NXlmTFZOT3JYNHJGeWFJUCszOHFYRlAwQkpDYVF6UUdTTFdvN053cTI2OE1iWGpCQmJ1dnZMUnVpZ1MrVC92SEIrSk5UU1RMUUJ3ZzBBcm56RDMyd0NhWWlLWWdhaTlUakJZcDlzTVNpaUFRUFpqdEE1ak80eDR1ZWl3d0tqVUlWelRHRFVmKzB2cHZZTUhLaFlPajFQL1BJd2d6RTIxQ0pLaDQvUjQ3SjRJMEwvdjZMaG1ha3hvcmRYcEk2WDZXekR1NlpKVWhyVVJuTGxrMVNzaXQrME5CcFBtWkJNRi9aSHZtL25lZ2xENCtwYTNnWGs2d0EzcUR0VXRtZ3lQRkREeEw3TnVqazVQRXJaV09FK3YyRGpaUU1rek9PMU02N0JST3hBYk52OWhmMTJMYzFZR0g0VHU3Uy8reGlMTFBtVGNPUE9NdUlMS0VEcktTQVZ6aE5zNFpaMktqeVoxRlFkUzhiNTdWU3FheWk4SnNhUDdzTzFINWtWMWxaVk9Ia3lGMVRxRzZwdHQ5MHdxaHlYeWF6SjJ1djhuS1FRL1lLWmJoc0NQaUJiN0k5R1ZrZlpNa0dTejFBWGRaMVZoQ1JuUlg2QlE4NnRReDFoTU5mWjVGUi9uaEpaK3FodVVOWXJtNy85Nm44Yk5UUzZ3SnlSNlRoN2svZEdWRnYwS01xMUptVmp5MmZhVTZzWjZNMnRIekg1S0tGOXdoMnNRSkorVlNwT0dma0NpdHdKZS9VUGF5enBiZkFwZmxWb1E5ak5MdjRqMGFGdVBJUmlGd010ZVdna3R6WDdrWktLSU1Ea2M1ZzFwbzlWSWZMa0U1ZWxwKzZIR3dDOWJqUDhoOW93dmwxei9iVkVFVkppM1Y0SEZQa25wRWVRVEcxaENzQzl1QXZDYXlLOVVXajhWZVE0V0ZvazlpME15SVpWN0V0VS9scENhbC9zNGMrWStycXFuVjAxVWQ3b3FjNFZjdldvTDU0QnNTdy8zaGNkZ1hvcDJxVzNSRTRKYnhLNGlOWU5FdzZLZHUxUlphdml2VEowMSsxVXgrRUU2MG13bzJpRnd3RFZWbVNnQzRuSW1KMC9McVFUemtYbU81MklISE41NDRoajlrNG5rTHhnWEkzT3NQa3p5elkyWUtOOXNtMk5tR1dHS3JNSVRuUStHVjBSNFJMSlBtRVBrRkE2a3ltTDY1QTdSL2dBaHlRdWhxRnBpVVJkcnhYSTBrbXRKNE50MXdZV1YvaTgyVmRIRGJUZWRwaFRBM2FOelZ6VkNZTjV6Nk80WGdxRHIrVE1IcmdBZllWbGlkK3BCKzZRU0xzTERuaXBMZlRXMVkwbUI1aG1Mc0pTQnFCWlNaTDFCZnpjMm0zL1JsdkIwbGlnNmdwTVdENFlIV0cwTDFNMnBFYXVxN00rSXpaMC83dmkvSzBjQmNuR3lFZlF2Q3RFZnBPWlF6MG9lNVBMWFZrekdHTTJaVS9Lc3pnMHJlcXQwWkdTYlpaM0g5R2xpdDVpbnJHaTNhdGJJa2loUFNEYllMMFRiZ1RScjVRSkNyVmlvT1lpbjJNUGNncXpiQUtmeW9VQ3JEOEZDUkhUYjEvUkx2WHFaRXpRVXpucFE0S244NGMyaXdmQjJ6ZHNTNTNBS1RZZGZLcXh0RmxhQlBRK2RaRDNzajJpaUIzOFh1Qm9aWWNDMXM5bEFlZ3ZPZlNUaC8ybmZpMzN6WTBqVkVDOFNrNTVoUFo2VGpxZnJtQlRwZjU2TmdCZnFRcDVpbERyUHJlY1B5VThNVnJCeVpGdmRuU0N5ZUROVCtIWnZ1MzJlSStqNThDd2hRNjN1aVhyOGJkY3pmcGRkOTNDMjNYaWJzU2VOQmwrdkg0QjlqWHFKL25SdlRGc1JEWGFjSVRyZ1J2S2dmaU81NlZkaVRPaHJzOGlGSW0zQkpXUCtRQ1FJYnZ0ajIwV3FjMFl5bHUxTkVNUzVpUmE1Wnk3a3ZoQVhKVVRCQktBN3dCb3Y3RlhsL1JtOHBmRmxaNXNXUUJ2U2xFaitLWmtLVVhESDRDdWVKelhVcnhlTGFiNVVFMW5WYjVNRlR2Q3dmVmdQYUVzWUpGYU1kVkQvYUFML1kvNUxmWThFNGJJRWdNYnl5MFpMSERGMHRkOHRNanFZMjNic2l0ZWhkTEJ6d0pyUzZOOXNTZDkyQUo5d29hQ0JXaXg4YjU3bFQvUVR1K05wc0k2YVMxd1BIOHRqMlhDcHFlVGdLbkhvR2srNGprUCsvR0FEZWM4bmM2WDdJK3NnQWhFM2M4bitGUkVqZUtBWmd0MldHcm1XTTl6VEZYbDNicXdaMFQwUUVKT1RLZllUWFFMc3l3NVRFZjZrRVdZVUpSaVRBWml5WkdLVVVPb1VVZjhxQm5sMFBsc1BldHhLSHVmUmgwRnVhd084NGQzQ00xN3RpWUl3MDVOM2N6NEtMNW9peExwd044aDRob3JTUHdidFBNcGRLTjBFNlg1SUVtSU5udEVRZWtHMXYwM29lWkcxV3RVcmVoaHBGdFloNlk3VFoxMDZlS1M5RFh0TlIxb2Y3dnY5eENRMHNrSitsRVdvU0ltODBFVUJMT0JoVTB3ekJqd1dRNmtZT3RsMEI0ZGVYeGFmZEF2cFRVdGRlZ2IyaUhDdzNRVzFJSnVtanJEL1pNUFY3VU4wWTBnQzJtdmRUOXhkY1FrUzEzTm83WVp1REpENi9FUi9zTGlPRCsya1lIc1g0dDFEdmlJU0VML3B4ZnlPbmV1K1E0MmYvT3V1YzlLS3lHa2o5MlZZU3BjRjhQNHZXNHF0bHJsZjFjUWxnYUwzM3NsQWVXaEc5bG5xMWJaRi9CcklOeERWM29KL0t4NUVOUkxqNFJJWjNMc0RnZGFRb2VBWDZTMTh1bS93Yy9UYU81QlcwME85Rk9sUjhWUE1ZL0ZKNXVCVDBvNTVNVW9WQU5ndTJRanJoSWV3L21KUmd2REVpb29nUVBzaXNja09JQkMwRzljZitVbjFPai9YbFRITCt0dmhiZHByQkxQd3V0VGR1MWNsMmdnL2UvaFI4UmN4WkdXWTdYTlJNSWdveng3RmthWldRL3RUemRIajVCbWFhdjhOb0JwZ011UVZXOXhkb3h5VlRuV2JpM3pIc21ZOGpSMVhMUHA4aW5QbGlZck9jQ1FDemw1NThldDhyMXpTc3ZaSzNKa2p1YjY1cXdrMUNIbGF3Z01YdzRMdTFxODFrUzBuMDNackNhRFhzN0VpVzZSNG0vYkhMaHVyQkZSY1pnVDRGNlZneEdWcWRZYUVSeTFJMTg1UGtEaitZN2dBaFU3SUpvSjNYMWp5Ti9ybnF1d1R5QjNSRFF2dk5ENzJDNWhHRHdnOW5uUUxqZ25BNEMydXFYSTB0K0g5MjZYa3FZdEcwcC9iTGZHN0dxUVV2U29QQWZNcFpabVhhTUZyZldzNGRYTEh4Q3FtcU1DNU5xcUpuc0JtNmZ0Y3dnTkhLUG1RU2dBTGRiMnEyS1J5M2V4am90RUk4Q1pYaTRaVi9wYlpERGdBb2lVbmFsNHlmVE1QSHErcE1WNjNmT2JsQnlNdjczRSt5ZkwyMkw4bmdibHlQVzZ1WW01bVBRSmZqaTh2TXZzNnBNY1pFQWp3eUZ4NlA3RmEwVnNYWkUrdk1yaWhqV3JoeWJxZUl5VHJ2dURIdnl5MUhGTWtkdHlRbXVPcUhJWDRxN0hMOElFWkxtaU9yMW1XK0tSNWlYZFJGWFdIeXNiVTFyRWtjSFNCRU8yOVo1Ni9lRnhRRFdiRVQrOGN2YURwSWRDS0h3bys4dGNPcnNpRzhOdzcyWGxsWktnZGJEZUxFMDNraHNzdG0vaWZHSzJOcFJjTnRrYUx2R1RmUHozRTRSQ203OSs4UEs2RklpSERuTDQwL2dGRXBMQ3R4ekIwbDV6ZW1hQUJ4b1VLUStxd1N3cEJSa0pRSy9sUllvME1IbmdoeFR0eGl1dG9uMTdCNENEOGhBUkhSTk5vbEtlaEZKVjcvMUt1anFnQmFBTEJtN24zaWdOSW90ZEFodjN2NitTTVM0ZWN3L1A5aHBsNUozQzFSTkZnY1F5c0gwcXBQUWYrMmxzMm1BVENoYWZrNVVrTGxnOEdZN0c0NUtXV0d6QjZ2OFdvRWFoODBQUGhaakphamhYQ0ZQbVRPaVpMMzJZYUNVeVdYdmlZVTVmdFAvQUlEWjNiRENSNUZna2hCUU1tVk1vQXprc3Voc2o4ajdGL0MrZmprWUlFVWlTa1ZHODdRaTBwTWM1S2RyUzVCczJpbTMrdFE5anBaSGNTc3VqMGRHbkM2VUtla0c3SlVQRjVKQkN2MkwzTTdaMERqMlZHdmoreklXcHY2VXBhOFRXbnY2TlpLbG9HL3dla1pGd2dKQzdjVWczNld5N3B1R3NvaWRhNmpGTmZkM1dvS0pjM3VsY1VIMVA4RmNJU3RjYjJjcUdaWTZENkxhbzlreTNTWXJUdWNQaTN1ejFoRTJKaTdJRGtKRFdsZzY4ckNrWnNiYlUweFlSbzFKdFFFN1k3b3d4NHlzcXc1WXg1eFRpNVhzUW9lRFJ0QXU2UUR4MnBqYnJBN2k1TEhwTTBOQ1dyak41V2NxMldtSnF3VFdtSk9LanpHTkRMcEJGY2F5eEhwbUVoMStLdkdFR0hCQXFnRlRIV3p4ZE5OOW91SjJNZHdGRFNjbzFzZnpVREpIZkhpMGpCRk92YUJOQjJKR0dqcVJ0WGtIU3RrUkRQQzhxRjRlbHdjSFUyRjE2ckI5ZFJjY3ErdUxyczNhK0VHazcrYzk0MGw3NzNCZDliZG03WUMzVnRxc1VDWXpHcjhmUnJQbEQzVTc2eUYxM2hkVksvbzZWNlpUdklRNGVUSlZHMExJc05tMmcwWXlOMWRGb0s4S0IrSlgxUFFxdkhBWjlrOGFNUUJvSUV5QldYdHNGNitOdXdYUHVGdHhkZndUaHVJZTBDcGZScDFNY0xrNjZiNW1CYkdUaUxNNDFtM0VwWXhpRDNyellzZHpibmRBVTRjUkI4TEg1dzF6QkZ6M2k3R0o2OEJaams0bS9kMVh5dDlMNzVNRU91a3h4dW9HWFZUWHkxaHR4dUtDU2hBRzZLOWpWSnR4d0dwL1pERzVTc0JCZXdCTC96ZnZkSFYzZ2d1STIvV29HOXBrSjhVckxyYjFDc0NrTGlXYm56a2JsVUc3YkQrbXVXaXlnMXZvWSsrdTB2NU1VMFZPNW1YNk00OEpwWGRNd3djMjY3YWdKY0RLV2ZabENCUWpkajRZWE9qRitlRTk3ZFNxTW1wNURZSk5rNDRXRGd3Z0pvK0R1SW9GZGlvaW5XdUZOamdrdGtFU0FDeFVxd2VqaCsyUkt3Qk9GaGZWRjgyN0c2Q1B5UmNPcmhyQnRJVXp2em1sOGdBUzArMjlHMS9TWDc2MDVYcHdjYnRGcXBaZ0VsWlNVb3Q1M09PNnFXMzJaRVRuQ0FCdmt4RStWUmxrZkliZzRwWTV5eU9tQ1ZFenM2ODVDT0dhVGlhMFNUTTZLbFp0RktIb2p3WjFGcEprblBZMDJlV2djTUU4NmVzUUR6ck1HNFdQcVRBa2lLOFdSRmtjSnQ3c0lmZnNWN3djNHluRW96eUsxTlNibGoyams1Zm5DdG55RXhIeFN2NGZaRWkzRm44ZndEN09vbFhjeUQ2ZjliZHpDWE5QblBXREtqOEpxZFdnZnpYTExTZ1MwOUgyb0djM0N3cEJkQ3pCUVQ4Nk1Fc01QQWpaUzlhVzdZSmtPb0JmZ09nSHJCZEs2VTRFS2lnU0F0L1V6RGFJNkRmaXk3YVVKcVluNXNHbzhwMG1OdHNtMkxORlpCSWEvUDErYU9lSG5FZkJBdlZTV2VpWk5sVEE0SlRVQTNZUGcwQlZxenFWTytYb1VTaUw1K1h6ZXV6cHdmK3orZ0FPK3VwdXNIQ29MaTJWRmx3dU42NjNka2k0eFdQa0I2UG9SYSs5UlBjTnMxN0ZlTU9hcXhhYld1QVMva3hOeDMySE15Y3dhdDZOVEZQNmladTBwWXRqbnYyQm5ONnJwYXRiVDh1cWx1NmVXSDJkUno4bWNEd25FbWVteTg4TyszK1luSEZEME5lMHhVWDhkNEFhTTlPOGc0Y2E2eWpvWUpHc2YrUFpGeUNsSVNCQ05pTGcvdWs0SWF4WG5Wd2Y4alRZYlErUFVqd1JGOG5CdEd4bW5nNzdjRkhTa2F4VWk4SVEvMmFMUitHU05GMTdxdzlPeU9JUzRyY3UxTDAweXhrSTh0emlaQk0xbGMwQUw4bkpOUGJVK2FkbnNEeHBsOGZic0o5eG9TS25TSnplUUVXcTdIWTFjNTNPMkc0c2RFMFlTWm9tb0Qwam1oYVJCL1hzVEZLd3dxc2JCbG5NdTJ1MnNlVC9xbExld1d6azhWZzF0SjdnQjROOGMxaEdpaWFGUVZkQzQ4R08xWDByd29yZW9IcmVwSFRYV3EwbzdzMVlka0JueGJwRTJWZktveCtaOHhrWkFMNHR1QWFiOE1penZiVU1IWHRVNXV2dkI0cnRZcU1zRkxkNXBSQjJleHIzaTJqc3ZxSGp0M3czZDFiSVZOdExBZ3BGNlo1bkQrcml6ZzlacVF4M05uZ3FZbGdYVERXRUVMOENqRXRhVFBnVjRoR3V5UG5ya2lERUkzNWxCTUUrbHV2d2w0eGxSZzJxeldQRlZPc3c3aUJJdTJXcUYzNCsxeXZVZHJNS3FWRTdoQk91MGN2RVU5UzVaRjVUaGdrYlRJRDdHenFiRHhvVXM1eHBRei9ONklKSElVS1BobGpKUlI5RHgyaEhXemtwL0pEelZLb0Z5bEt3YTBqRWtxaE5UaVJWeTFFaTRSTGQvL0pjVytxYUNKb253Z2lmRnFGZEZQcW9lTnJXNTFiS1h2d0xMc2hpNGNhRXQvM0xBMWViSThCWisrQWxxSGJYZlI3SUJCWXdlaXdYRk02UjMraEt2M3A0blovMGIzOG52cTQ3M3FMUlo1UXdpYS9HbUZYeThxZVRqT1Nidlcvb245REE1N25ZK25kRERsbUFnQzQzTjcwYmtTNzA3S0JGc21rMlpjUlNZdkRUVUdyZkN5SFdYSUxDSEw3M3RuTnVNNjRiTmQ4d2wxSlg2Y0tQODdycTFOM1FHcG9CeVdtRHFvbEswMSsxYTNsVGh1Y2ZFVm5HeUw5NCtnOUsrc2NFTGE2TS9LQVBYL0pWcWtRYkpOelhISjFqTDk1eUJoRm51bEdXamVLYjBKejRKakNRenNES0lmY2x6Z1YzSHlsRVc2NVRyeldFNTdPRjlsYmFhbGthZzgwa0FLaHRMbWJFaTlITWN3NWVuSSthQlArTDBLR2h4dUd6OFZMakhVVktjTU5QOHRlaURFVmFFU2pLZXUxdU9pVDE2bEdVeE9XL2xVRDRGWm5DYld3V2RlcG1GbW1xTVJQZXFQL1d6a2FLdWhOUVFYaFhyNlBUazJrYzZEWGlhNGZxTmY3dGpPWjR5L0hiZG9jNGE2aldPTEdVaXlxb3lxNlNPR05NRmZvaFBaaWlrdjhoc3VNd29SRVUxZjJlMFBOVnV4d2Q5Wko2WisrRFVRNmZhWmh6bWFwTm1IdTQrNGpZQVJIZTY3N2NuSkZ5bnozZG5IRXBwekRNNFdaZExDb0F3NVMwN3hMUk5aRjNrOHVmT2lCTnB1M1FBYmRicDV2T1huZTd4REhybkhhQU5vNDdBMFFHT1pNc2t6cHB0bE5ObTIzYVYra0UxYisrR0VITGp5QnNqZzV5QzRrS21zVGZUZlZSNUluMGlaWjZYZHNBcVZhenhCSG9zUkZhVUdXeXlGUXJSRnFTNExuaEQwTk9vZkw5b2xiVTV6blRKK2R6VU1makZzbGhnQVpIZnNLVFhlUUVPR2RkRzRmZUdtbmYxMVltRVZsbFJnY3g2NmxNOHhmblJiUU9Eb3pJSkRHU1A0SW5lS3MvVDBMSkYvOEp1b2RjUDQ5K1hLMXMxd2lMQlNMbWl2am9aZnUzMCtzNFdRcE5iSzFCZmFMRVdqNXFqY3JxMnB0V0dBMWFGK1BjTUpzK3NJY1ZJVzRaMFpHaVBVdnJKRkRxUmVWaFZQYWFQUGh2V1lUdU1OeHRWenJTWmQvZExJZnczMC9mcjJ5Mkd3dEdtVXlEUnRscHdva01UNmN4RnF3SnFuOGZNZnI4c2lMdkhFdFJsTWRNbWt4cThPVGd2WGRNT0tETnlOVnNxMUdPYUpHV1JqRGJ0Vm9UcXoySFVwUGpvNWhHOGQ4VGNkVUNLajU3NW5MaG1lU2lnQnNBcHNyTEl6ZTJEVXRCOXpPVEdrRWp3THVWQ0o2OVpoaVg5MDczQmdXcllISU5jN0J2UndXK0R4RTRNbVF2b0JXMENFN3JJdGNCeFVtYlRJZGhaUjJVMW55UDhTMC9qWVNYQ2ZUbXZZcmdRMFlROXNla0ozOXpVTDNvQ0hSbklDdzQ1d3NjckIwN2szdHpFeUpBTDR6WHBQdFZpNUlWNXNPRmpJTkQ3MzZGcTBvV2lFcUNRL2RMYkJubEZ0cW8ybGd5NkhUdWUyVjlEYjZhNytQUXFLNWdhWnB5cm1pbmlvdEpWa3N1cUhaNzJWSXlpME9MRDBYbkdjT3NzMUQ5ZEJQNGhQRVdNZE1sU3lyall1TkZKSEFDU2JDM0QxcmdPbWJBK2k4cFpNRmRQYThxT2lwckpPenZNZVpORXF3R3A3TTVJaGxaT1hGVi85Ry85RE9lVzlacktVK3pBd1dHakhwS290TlN6Y3YxcmtibVRsNnJkT3p6WFUyejBzZnR4dERpN2ZxcmlyVUhuMXA3ZWxPakhhSGk3bXlmb3cyWmlpSWlYY0tab2h2WUF4NEFxYUJaSHhrZUtwK3RZMWtRUG41MC90Z01PNVR2VmE1MlpJR0xzUkRoTlR4dUFPQU9vb1k0a0NsdzBjRGNWMTVUYTlpMmhGam9NT3FTRE1sd0w4VnpiTlJzd1RpempSWU9xaXljZ2dIZjQrKzRvNHZnUGJxT3hkZWZ2N0ZZbXdZWFNDMEwrV2RDeGxVdGJFMXpEZGVnN1VXZnM3cE9KQ2RRak5FQmorZVdhZHdpcjhYYldyTGJsUmZQbkt3cmhpUDdzNjZmaktsK05CdlMvZ3lFQmJhMlVMYndTdGZUazk5emZ5ajR4MmV0RWJ5MkFKaWlDcHFGSWtoK2lMUUkrdllsNWxiVjUyTVZwbTRyS1NhenlFM1d4VVdyOTFMbXBwa3paQXpqT2drdHQxdEVJYUZ1UHJsVHhVN0FTM3RzRnlOakdZU3ZRRjR0K2ZNdkc1NjZWaUVqZ3Y2enVZU2o5Y3RUNkhFaDRLSU95djgyYlpkUDBYN1NNa3EyZFV1WGNTYURZZ2ZaTmRWeHZ1dHFzL0w2UFlrZWUzUVZXajdTRXBlanQ3OHc4V3pEcWFyQ1VCMHJzZ0NONDFCUzcrdUZwNGR6NzVLalFwWStBK2Jzd2NlbGQvRDRxZmhxUU9OOVUxRERMcUtuSVRpNExBK1VvMEhmWGRHNnBtdkZiZmtpckZIZlFXN291eDRpMHRydzVRQ3krMG9icEJDaS9sU0dTQkllQkZwN0VQRktNVXlDeUQybkVvMEMyUnpGeWQ5SEc4WWVQaFpTUlRIZW0zMmgrd21GWEJreUdGclVJVWtIRGt5dFdKODBxQWd2dll2ZitJWkZpM0dZb0dCNDNuS0NLU0E0ZGJlV3pVZmZGSzVjekxFVlRsWEF1T3BLdHZYQ0pOSU1UV0hwbXR6eVJXME80SVBxS3hDL3hJRkZ6aWNaZ0VtVHNlQzlDUkxnYjQxczMvWitDNm9xN1I1NXhzSE5kTWN0M05OVkVuY0V6V1BmSTdoV0ZwYjRGMjgxcDd6ekhIM1FIcytTSmJadEJBRjZxVjJTeGVESE9SVTJIZjlTL1lEMnpYODArSlhiOU4zQmNyQkhOY0pETFh4UjZ5REkxN1FJbjV1emhyNE9oK1JIOHVTRUtWTHZNWksvb3U3K1poeGlKUjJiUzljR3Y3L2tJZ0ZtVEpNVHpHWUl0RjhjbnBBZkp6Wno0Tk1WMytqLzZJSlF6blZoRy9PNHRBZDBjaElieVZhVzhlQjEvK2Y2YkE5VE51N0lyZnBRWm54UzZRNVdIRGJ4emNjaU9MeDliVGFYTDFtVVJOK25LVWh5OGlydmoxOTZJQy9VcHlkRjZZU3laZWV2ZmI0aHZyeGlYb3RvTTQrNVY0bnY5SEtjN3VVNVk4cnBwV3lQdE9UU2FlRkxjTzQ0TzAwb3NYQkJrVW5SRytQUzBSK3d4UTdMTnhvUk9PVUhhRE9hZDNyK3dQY2cxM215T1VOYk9CUGRJclh4Szl5dHNGeDNpWTFGeURSMnJKenR6M1NhaGZIRmZ4dmJHMDdyTGlEcHN6RTBQUndtdERrc0lJYzVHYU5DcUhkdjU5OGEyWDZXNStMa0VQVnA3QjhMV245aFphU2ZIL1NJZEthdUkxKzY1V2R4L1dKeStyVFBSeEJaQjZDNC9FdlBSSUFQTFhlNGNSb0dsU3FhSi9pSlY1NEpucmVmaGhwUnY2Z0xkd29MVEJ2ZDBsUnc2TnYzUEZoMjFDdWtKZjBIcnZnODBGak5icE9TbmJuWVoyT2NrenVpVlZydEkxb3Q3UWlBdEl4aSsrUjNpY3pQYWxHNTlqTG5zeFFZR29DV3pZYTJuZUZFdkVBWUdQYmJaU2J2UHN3dVNBdWpidEZkalU5QnN0MDAxR0QwZE5iejN1b1ZPRkVDaVNvOGw5RGEzdDJCOWR4WDhKOHBaSXpKRDZ4WVJJaGxVcnlkVzYyYXVEMEtHZ1pCQldRV2ExRFVPbExmQ2grNmhzZkhqNnFyKzk1MGQraEEyYldYQ0toaWdQeDhZL1pER2hBTUY4NzBPQ2l6eUJXR29LU0dHaDRFUFJQRnFSU3FZZHVCOTdsWnBTQ2F6RENEOXBRRkRnaVhqR2E2RzBTYW1LU3F3RGtzZHgxUWJFMWp1T21scG5INThFNWFLOVFhaDZGdVdRWS9jNHpsSklMemZFc0ZJTzN1U2hNVUJPTWhVM01QdDhoMS9MQVBsRlpkaWN1dlFMdzdJekdORG9rc0dMREFPVXMwbWlMcTZWSGZGT2JGWkRGY3NxK0ZjaUNFRmY2ancveDFsSlV6UGUvVDBheUtXTmZxL0IxNWxYVG5mZzFRcXNOSkxtOHhQcThKdWE0akZIMnp6SGJCaEI0dGNEMUtKMjZnU0d1MEVVMDRndFRtVERWL0dCOWRIR0hzRVZUQlpoUE5MZnI1dGQvME44cmUyUmtQMDF4TGc1N2h2T1hBZXBrclpWRDJIUlcxdW94ZlAxSG5NN09ISUlKN1o1VWc1Rmw2aEs5WHpSTVd0VnBscmc3b0gwMGQ5cHQyQjZQSkhGN0tVeVZ6bFovYTRqNzlKZ3dhVkJRVmhkd1lOdDk0bFovZFQxSm02ajFncVhjK0p5NHA0aDZhWndFOFcva1lxbnBEMDAwcFZBMzFHeVJ6VDJMTjYzQS95TWQrWDd3OFBjUXJNYzhkTjFXenBPNWhMUGpENXJBbTJhSHlQUjZEWlM1Z1F3b1hmeVd6UnRnSDBoKzMzWmNOenlkLzhidWh0S014L3FZVlVMVUFIOTZIbEZTK0xiSWdnRlVXYk1SQkxLbk0xbGhGSVQ2TUEvSmVlVE5tU0U4ai9DTXQ2QlRpV3U0MEt0b1YrZUg5d2hMcE5Td2ZZWEo1dVF4NXNFWVp5cGVRWFBqR2dYb2NORnU1NVU1R1ZLTnJUcTJVQXVLNTYrMUhKRGEraHptY2tCWWFsY1Z3ckJjY3NQL2JMM2pleXU1MmYxUTNMd1l0dElWSEhJa0VXSzFLcnE2blpxVndFMDVPNFcyS1pJNnQrTSt0dG5CUW1kWksvb3kvektNakV2L3RpNFVEUzlqUXBuakRJb3FjV1MvRHAvMXBlUDFsSnhna2p2RVVTSTVidXpTeEYwRDExMGY5NnB6QzV3WUVkVjJJM3ZOK2I1dTR3UTNjUDRta1dNd2ZCSy8yYzhUZnlNQ0pvM21QemQ0Y0pibm1xUkNwV010R0krQ25jNi9CZ3lnNjdmVHZDRUNmNW5KL01xMXMxbTJxNXIrQjQ2QVI3TmRiWGM3MlpJdEp3cmVyL2JxYnpjbHp3cGVzSUFVMGlLRzFWeVBDMmZxYjZKSDd3SC9GdzdJaVNxU3YydThiaHpyWExoUGpyWDluQW56MUxLbjNEL0Q0Z2ViWTVwMFdWdEE4RDJhNC9mNWdscUZ5RkdUTms3T01BY1l1WHVsYWcrVDJzS1JvQ0F3ekk0RU8xR1BoZnUwNEMyS2xEanN5MTkyVHRQVnhmd0Q3ZlJWcXc4ZkhZWjJNWm9qcjFjL1FhRFhCaDROOHpoMitDL2dTUW9MYWdJTnVTTjVBWkRiVmdxS244dkxxdzNBcFRUTDdsMGM5LzJDYUppV2pmbGNEdmNiZ2VIVTA2VkFqWER3SXV2T0RVTFJUZHNyY1cxWExubjBXdmRpUDVON1Q5QjhVR1BGd3o0MG0xTWk0WjBXMnp3K1RMckp5NHBpTDZObGhGZ2hBZ21ob0pFU056c25DelpTdXljaG1GNlY2ekplOUNGK2FXMzM1S1lHZHFxVGdJMHJlWkoycXhUMXdWRHBoYStnbnlNd3I0TU9XR2FZM0JvdUlZUXFDYkp4RG9CSjJ0cjFVSW9hMnFEZUpaSU9kL3hjRHRhdDh5VEU4MkxQUXJ2Ykg0TGxOQWRWMHgvdWU2SVRMTlJWMzA0TFAwME5UYzFpck81NVFudk9XdnRXVWdnTE9rdWRPMDI3MGMzRjdBTERXR2RUMTFCZUJyTHJsL0lTVTE1TjNnUFVxOWNWaWRTNkhxK3NhZFVxdjl5VWxnejVOZ09KSVZQQ1hnaE5GaVIxa2ZYckl4TnZ5UnJJRU10Q3ZyY3JnVE92TDJsZW1VTGJkVERwc09EckkvSmZzS2ZKcEdhY1hFSStyMTlDaW5mYldxa0h5aXVMVFNsek0zN3dwQWpHUnFNWkR0Z0NETGVBN1JBUUI0eEUzTVN5c1FrU0wzeDVjaFp5YkFhOWRWN2IxZGtwNGZYdktyQW5FQ2JJb3Z0T3dpNEtRV1p0eG5xYVMvVzFYUkJPZUFrY3VkUDRNem9MRjNaK3lkNjdFSk1jSllBN1pMcG91M2NsZWZtQUVTMndtVnQzNDlEM3UrTEI4VnJUUXNqajRzUmVMRkVtVTE4V2tXcjhmOUJNMG5IR1A4eE9UK05mYmkwcGxwZUpCRXZkWkltYmZucjhyMzMrZDA2UHdMQXZncmJHWXVjWTNLejlUYnp3Yk9USTlib2w4RU9HbXc2NFlEd2tWczFucnZ5U0xiZ0FNdHFINUpQWXlteEx6TS9namFHczJ1d1N5NE1aaXFnUE5lczgxWGJWeWVSdlBKU3RqMVNPM21OWDlaNXJhUEFCR2hLWmJ3RlhFcTA2VnJQTnNmRVM0cDczZmhyZ203SDdtOXlXL2h4d0FWeWx6TGJBbTR3Qzg5UUxub1VJVnpoYlp3Qk1Ra1RIalNLZ1JWUEtyblNVUklNeFAyYnkrcHdtY0tXVFF0dWF4cDlOc3NiWmFMMFd5TitIdk1lZHduL1p3bXpVWUt5ZGJiUG5ZeEVTTjJCWXZUald4TWtCUDhnQ3FSVFFTdXdUbkU5WUZpOGJFcFEzRElZenJZR3lCLzBNOHN4UENKYTB6a3hud01wWXhtNjFKazVLcWF2THF4YkNrbjhHQjJlNkhJcngyZVc3V2hoZ2tlcEJ6VGlySlM5S2VLWWY3UTJTckZTZFh6aW5rVEM3VGdQdkxEL1NGUGRseGNrMkxURnFoZkVCNGVxOTJQdncxRW83dzl2NHF2TUZxeDY1TktMSHV3Z2ZUUDBmdlRtMCtHYUl1czlETEwwQmpKd2R6TTB0cUQwMGtsUE5CempHcG9DaTFRd0xka2xVWHRpRG8yM3dPaU9HNUo3eHFxbzk3anRma1kyUjFhaVRkWjd0WWhLYVlQQXlqbnVETmtRRmtFdG1IUWluSmJrNnBDS0ExTTM0SnMyenBHSzJEcnVnWEFkaVdlYXNRRDZnVHpYRGRQWFR3NzJnRjF2Uk50bWl1YUZkWlFkTjBPYy9zQ1NYNFUvRm5pUFBrd3hOenhlYmJodWhXeWkxSmRObC9lNzJNY3h2YTRqcE5qNVZZRkpRSDdPWkV1VXZ2aUR4akdLQzZlMkJpOVh0bHNaalkxSUtxMFNDM0ZwaUJhTjNnSnFPT1IrUlJGYmNROERHL3VVSitheGpyTTczVXRvOWFGRTJyNWc1Nkt4MStDNDRuTVY2cWU3ZzRNTWJwckp4TDR1Q24xdExGa2czUGpaQnBCNnlBV3FqWDByelBoMWQxSk9PQ0tYb3c0SVU0aUlRL00zM211WEhvejhFVndld01GeUhOVEF0NTRhd2xVejZ4R0MwNjBHUlhiSDEzT2puQ3VvbnVOYXRDVzduSlhxbUFlalczaWs4TTNjOUpiK3dlb1QwSklSMUxrQ3MzYlJ4eXBGSVZXNTJVajQ3clBBalpDaFVJaitMWUhaaDRSWkRaZFJDTmNLN25iVlp4NzgvbUM5QWI1OXhXaExxOVZicWV0VHFFdnEwclBXTVJneTR2QXJlN1I4Y2s2QkRSK0owTlQydnkySDNMYmZNWVd4Y212MlRYUTRDQ3I0YWdxRU9FMVBGaW8vQ1pqSjJpWkF0YTBvVE81UFJ5RTk1SnJ0UENZdUg5QlBEaWJXeG03WDEvOG5wTHhWZC9WM2JCamw3VEN2R2VIWDFtaWhKNndGOXl5bSttcU92MGlMcVM2OTM1WFpMYzJ3Ui9VWWExM1dSZHBoVm93OUh1M2g3Y1hubFVtNXB2bVY0NHRMNnN3OHR6endWNm4yZlEvTEY5bnovUDhKSnZYeEdCdzlqWk1HeGF3M2IxZUtHb1JkZTZIOEhPUmxlaFZVaEsrNHY4VnBZbXJYMXlvZ2lONGJocDgrZ2VkeVlXY1dZMGVzS2JQc2JpeDdUM2FyYy92SzhFZEZPK1FPZGRHMGxra2lQMzFFVk5Oek9oMnBPQUttNkV5QmhzLzVnc0k3c0hkVFIxY0l0SkdjN2pkUXVWR0g2aCtPRGJFdFZsWWphSUFhSDNLc0FnODE3cTlSVU5zdzhMSmxaMzY2VTFGRFBYcm8vWGJrbHZnYTF1RFZqb2RjVnBhUTlsYnlPem1MeDlLcmlaQnh4dTk3U2ptMmFiUGdHdjlJSm1PalUyOWdxTEpmbTVtaENpV3FmU2NPSldyVVE3WnUycmxndTJkc3l3Tk9TSVR3SHRYTVh0a1BYS1pJNEpBcU5veVJCUVdqVjBPWVFtVFNwemg0Sys2d0NFMjBZSFlRZUlrYWdVWE5oV2g3bG1wcUFDWmc4VUlneTYxa2UwYzROUk5JTGR1c1MwS1hrYnVvR21LUWs2bUZRUDkvUy84K1YxQmlmOXZwVVd4aktMODYyenNmR08vL2VKbERpTitEVkNxMmtaemRiUHZOTmxYRndxdmVxdnRtM21RQUxxSXpXMnR2bHNZSWEwVW5oUlpiemp4aXNMUm9sOHhzVnBUWUZsc25GbFhmQ2VsdUtMQ2tVb2lOSFM0MDdsTlliN1Z1L0tBbVRyWXZyNnNQSVhRQ0hpK0NZUVRjeU1aUXpHdW53QllEVGNNc1FheEVFSC9WM1R5cTZsUmovakhEQU5KMk5yWnp6QitzWVo3RzFNcnRJUmp0MXNRZmY3c0hxL1hrMEp1SGNUbW4yLzVVMTdCRktrdGV0ZjJ5Y1EwbHNOME0vWEJxQVB4and5OXBId2VMblJCN1hKYWJOY2lSeGRvREFiSVhSdG5Yd0VYSkpjZk1uSVNJaGt2cGZXWW45bzVpTEhWVXQ0czlqcEJJNXYwMng0VmJ2RTQxcHFPY3RaMUZhbGptc2VSLzI2cmxBbmpwNTNxNXlCaDFrQVlBYXNFL3ZsMzNjbHIycnNDSzZ6Y1F5ZmgvTE0vVmZoaVQ5UzViemV4Q2ZodUNBNDU1Mk1OZmZpVXdXOWpvOTdhcEZjU1QrdllGbGswRW9Gc0liSmp2RUpid21YU3NRWG95RXJoZzBWNTh2Y2pFUTRWL1JiMkhRR1AwNjluMlV3ZHc4TGpnSE5iUjlDbGdaY0VUdDNEK2RHN3RIRHRLLzFsaU5xeW8zdjlIU1ZxMkRnU0JTU0xwcGQrQnduQS9WczE4RWxmNUZleDVrYVNDMGF5anI1RDJiYjlzWTdUZm56bjlWSGY2WHkvU2REcnVCUjN2aENYZUU0NU1FeThDY1lGZ1NhRnhqNXE4UGZDSzlYcGFUUmNuQnd6M3pQNENGUnZTTys0ZEoyR054SDFaenh1SGNPNEhYL2ZpQnhtMHVldFh1d2sxRDlPamhnYjIwK05KaVpwbFRYeXJ6NTg4UTh2akJvaTFiOFFSZCtobUliZTkyeUJhaHhtbC96bklmSDA1VzVFZ1ZKWERPYjZUUTdROUliMnpmUWlNZmZVQmRMelBtL0dKYVVzQmsyUVFoQU41RGpKUThmWFV3R3BoUGhiUkdLVzIwYlJHL2krdGE1YWN5ZnBJZUJWRXdPMDcrRVZDc29pMGRuMVp1Rng5Z0lqMTlneWdUTEpsOUM2cGVHMlJuc1pzKzY1OHJ6Vm1nQ2dTRmNTZjBlTlgyblAyR1FOV3kzb0owbW1YZTBzS1NJYXZobnNrUmtwd3VzRkJDV2hZbGREd05RSWE3dlV3NWg1THVvZVU2dVQvT1VVMzFRQXZFYXI0bGIvaFdXVnYxcnlOK1pmSWVsR2lXV1dxZWE0dmVTV1lXUDhXL0U0YzJFS0I1Y0E4aHQ1WHFIM2NrQ2hEdnFOcEQ2S1Y3bC8zZm10STlEL0dCbDF4SVczMUNFSzZGdmhPSDg5UTYvRFNndVJ6cHczN2RXM1JqODZpYTdKakF6S1cweVA4SUQ0VGFXR3J4REhTWWd2eVZCMkhsT2ljb1NCNUFqYkh0amk3b3M5cU9MWUpTRFB3dG45cXdKTllaNzVDMnNSSHZzNUY4RGp4YnJZNHdra0JsWGNsMm5BS2gxOGJaQ1RyWDRMQ2xWUXRQK3JkSVlhQnMrd0tsSXFITHNGVll3TUV2TDJ5M1lYU0g0dndUcm1QQzRCMWNFR1NTSnlnV2RxeEZQbUo3enY3TTUvS0R5S3dlZ1ljOTJxYUl6RWlMekx2dXV1RTZCOFBqN3dDWlo0UUhyUUVDcHNIQTVQa29Va3p1ek9SODQxdjNkR0pVRXowQllxYVN1VFJQSk1WTXh3Wmc3enZpNG5SWlIvU05RMWRncFN3RjQ1QkdxU2pWekQ5dzVKT0pxSkZGTzg2dGVxMnFBUmMycUJIbXEydit6M1AxeVVQSG9rNTdNL0IraFNtMXkwby9OS0ZQa05iYlBrRVkyT05Odjd0SjI0KzhZZktZQjJ1S1JBU2o2ejBCQ3JOWWwrOFhPeS9tZzBKUWNGWGhUZm9NeU13QVA1RUtSNU81QVBxejVCVXRNbmRjTG1hN2sraExHRHM5Wm1jZWtFVGFqdkRMa3E5K2FkTHIzZ3JxeEY4MWNzcDlpMkQ5R2JlNW94QXdqREc5dXpsUmwyNkl3dXpYM28yclRKTUQ2MCtBK2JsTWtUckxpcGRMeVp3NzM5SjBtcjV6R2NzUVNsM0lwNFdueFlwdVdtSHNDaWVKekJPQjNob211MjZjQzdtK1lqQmxKblA3SVoyUmM2bTZSbFZTMzdYNTJBWi8wUmdZd1RHWlRBaEFzLy9SQ0xGb3VQQ2F1dUw3WXpocUZ6Y2JpQVA5VVNPYW4vZERpQmZNcEZ5RmxQSnh3aXJCWVppbnhLOERTMGp5cFNvRnZ2dVFOTWo2czR5eUxjSElZcEd1Z0Z2ZGlJcm9GZGExM09KZ0Z0WGVobFp6QjduS2pmbmF6RThJdFIvemNDNU9sMWpraVR2Z1Z1YzJPK0o3dlFkekFCYTIwdVh4M1piYU5LWXpLREVjejNhWWpTODNuVkhYUzhXb2lHam0rSEszNDAvZnVuVzNaKzFTNE9rV0lLbm1EczVZQ2ZpYWl5SDFqVm1POUVOejB5SXdPRldkdVd6V0RDazhjNENkVTdyS2o5WDJuYitqQWdKRHJaanlHbmY0MFdBSVNEdG1DbE5GeU9ZN09mOGc1TFozNDdhU3FFZDgzREcwZ1g4QmM3WnFDYUF3RlVvWGpFSm5ndEhoNk04UmI3MUlvaWwyU0dqcWxGMnV5dXVrbEprajBqS0FBdHJVcHE4cUJveG9WUVJ2USsyZ3pFWjlESUNPZXVGZkhrWXNkK1k2K3F4d2RVNXNXcVlta2V4LzN5VVhoc2trbEpYM0xjY0RFakZoZXRIV0tyL1pXc2Njam1ZK3c1NlBPZEdGdUhzYkdTZWN5TC9QL3ZMMFVMMWpRVFgvaCtXM3k2cnJqejRHNDlFblZ0SWdEUkJza1dXd2NOaTgzY05CVzVYaFhtR2ltTVh5WStkb010NlFmcFZCQjNNVnN0aE9CZlE0NmliV0p3WDV1WjJCcEJEZXlJZzBPWWVQeU8rY01Ka1dLdWlHWWlLMW41WFQwblQ4SW0ydGhjeUV1VjZjN2NYVXNteEpSWFg3YXppSGV5aDBGOW9KRmJOUExVZTV5bkw3bHRqV0RLMzJqSDZ5bmtGKzBPbmdsMnVQWURSbDZ5Z3BWTkFkNitKMTl0MEovTE1pNG1PMTloZHc2cTkwazBxU3BaKy9TeGxNMFE5VzY1MTJtRlhZRUZnWUNxYlRzSy8wYlBxa0hCQlBtVSs4ckRjWllaVlZiY2dHWjFyZGZId2FxYXd3V0VmeU81SUw0WVNtNXZLVmtpUXI3WnJUZmRaNTFYTndFSDlxdDZ0cEZhYzFOYjZUcHlJV1M4dXJuSDNNMzNFNHdoeWxzM0g5dVhhbkJVczJLaGhZYjFjeHdnZnIvWG8yR2swdWgwSGQyNkx1cWlYVHREcTM1LzVBRlI5OE1EVGFrUFVpVXhBZjZVaU5IVU16Si80WVpZOE1PS0tKMnY3Witqa3FDaFBqM2ZCSzdMUjRLYWNVdGFCZkhJSjdSYit5NmUrQzVKVHdvZkI1eVhlbkNSU01pTzZ2ek1DNVFIa3NXZWxYa09PTmQyeFFmVExXVHNxRkpGWFpLeDE2QXFocktWcml5Yyt2U2lyK0dhN1R3NXVMaGlVZDZqT3ZNeXBhK01CSWM5M0tvRkQyK2RpVlF3ZmFmVFRZMXcvUk82dTBiWitrTHpPSnFTTktUODhPcHd5VmpnWlcwVGVRYUNqM042Q2NueTBqUk1EcVhRWGxXU0l3SkNRS0NjUWFpMnlNM3ZoNTdObk1KSDQzMjJ3RlV5eWIzVCtKaTlZNWJxUWh4cGRUK1YxWGh2ekoxU0FoNmRzdXFLL3VHUGRSdVRaMGkxL3pHT3V1ZE1wdXFJSFBGaEV2anpJbHNUYkx0ZTJqOFk1VkgyR1hKRlpYTjhWWkxCbTA2OHBhRjVINzNTSHd3V1VQUEw1SVJ1aWZrSmhyWjJRRksybTlzRlllZ1FSMW5yR2dic2RUNllldE5hQ2VWZUdTMWN3MjMrOFhYMUR2SXVsTkFaVExnVmRybWRmQnp1azBKN1VURUtkaEk1azhFWXViVFdvV1V3dXg1WVEzejRkQzhkVDczbHE4anFwOWNNU2lFTzgrN2oxNGFFelpiN2xEZVkweXBLR2xqcG1pSmRuS0Y5bUllSW9MS215UHZBVnBDSFlZbE5jZWlrUkl0dTJZYnc1bXpVU0tjMm40b01GMzQ5N2JyWlI2L1NOR0tNT0IxSVM5RlA1VS9iNWJRNVJrOWp4bVdDODh4TnF3VHVlVmZXYS9TdWxnWm0yaW5CMUEzMjNVYzZUanB1UTZocnA2YzludGpkVEREdkZ1WXJCUjR5NlRuWkY4RVhuTE4rUk11YTdrWFhIaGpIbVVkU2N5eW13cFZ5bjRzUlFkNjJwSXRIdFRpL1B3M1VlUjdYWWNqdzh5SWdBekFlbVlITWh3d3FQSlh5MnI1MmVpbmpxWmZQNklPVVRmaENTQkxzeHNVZzdTdVJnTW8yaGR4RmhCR0xxbFBrTHgvQk9oME5UUjNWbC92STZzUGNwQ1V4VDFMTktYTUpTak9DNStFQWdQNk1BT1llbUlxeW1BazUxTlVJRlptbTdRaS9kdHh4Z3MyVThFZ1Q2d1YxMWp2Ymw3S0xXRGFYblI0OUgwenJoQ1YrNWFFekJ1UVo5M1VqdHpnQ3J6QVpvSVJMcHpKZzJpUVZ5cUM3Mkw3VDVsYndvOTRXRjQrQ1R4YTZZOStGcmJGVzBDbUVPTDF2RnhXVXNXR2JTanRHL013MXV3ckU1TUp2eTNhcUZmZ3RhcG9ydXplRnZZSkJoVTZIT2F4RUw2UlAvQjVObFliUXQ4RVIxU0Z0MTNIdmVVQkp4SUlzYnJlTDBkNTUzYTZYck1ydmI2YkQxcUdlWVN3Z2J1UHQ3N1hsb3l4Tm1vWGFucFZTVW1RV2xFa1hsb1dZVGQyV3ZsSGJDTUYxbWdwT3o2SDRBemw0WDRIa1BzR2trOG8yM25zOURKTHZNQkZ4dFNIQ1V5d1E0N3hPTGxkejJEam4xSkdGNjRKbFZwOHk1dHNlQkJXNXg0cE53OSsrWGVYYjRUU0psMXZPY3FKSWllUTJMS0dHZjNqZXVrbmZkR2wzQldBa1pHLzFYaTVHeEdKc1RZZnFGVGVIRzFsNDI3UklYWXNCWEZac2Q0aEJpd1F3RTZOQ2NqajdYY3FEK2pWNkZZNnA2WDJtYmRJdGRBcVk0MEJRcjhkUVQrMU02RFFNU3lnY3pZVEo3SVVwN1pUaXBPNklkQjdXcHRnUFFmazgxYWpINW4waGRUdXVYZ01pbVY3Z2c0Z1FkTWtLQWpDUTF0L1lLbFNCSjlUN1BzWjBmN0oyOFVlV2pMN21hSXdWdWVVUm4xWHlIdW9nYWJHdis5SVNEQnFnaldKQWJ2ZXJKemo4cjh3d014OWs2RkQwWmtaRW82bkJoNFpKUldDVUcydTJYOXFBN2RJeHQ5Mm5UWkwrZTdNd05sRnRuME4yd0U1dmJsNktDTzJFVXMzV2FLSWlHaW5TQ2Roa3k1S0RYdG12VDNRTHZDczM0dDFyM0NOVk15alNGRDE5MVBaQUdJaTBYdUhsTUd3enFTTmtZRjRTM0NNRVRVTjM1Z2xjdVdwU0pkREVzcXRUM2ROcUJnbjgrT3dFWWM2OW5qSTZPbG95N2ZhSkV0VWtWcEZrQnp0S1BLZUFhZU56OEdrei9hZXpEL3hJYmFPK0dKajZxc2g0cGUvVDJicDBtb3VVSUg3ZlhDa0dOdjZ6eGdsMFBaRlVsRHN3YWlGUytUc09aQzRhbWt3S2FITHBjQncxeC9Ncmg2ODNQc2VzQ3Y1TWw5ZFFZRHVwamJpbDZCOGRhcUJLWGI3SmVqRjZOS1prbjVoem82cmVISW14Umd5akRNVUw2QmFyNmtaZHI3K1ZZVVl6WngxZk5ZdUZybGl2VWp4dWc4K2g5eUtBVWNyUllYMXl3VzBpbzZMQjdteFJSZy8wZjdubnNjQzJkM0tFdnVFRXA4dXhOd2g2d2N1K0ZlM0wwS1E5cXNKUU82NElrUVFxU1VBTHdsT3hMdGVUeVVzY0VjdTUycEF4WndjZ0RrNVpyVG9WNHJ6TTNMVEo3aXE4Znd5LzZBTVNsSGtLaDhCYkRVM3k4bVZ1czZjRTRSQ3BzZFZZVG4vQUR0ZFhzbEo1TDVvT0R1VjFSdGliVDhSYzQ2YXZWbVBHQmgxRzVjckFlbTRqc3FJbnZ1eEt3bU56TTJSQ3VDbmxPK0JqUGRZai9VNndIek5KakRKelRHbUhtV25uTlZySkJxOXhkd3k5Q0NBRjlkazVKbFRlWkZwNmoraTB1T1RVbDJqdmJ2RzdYSE40cVhsRHNFRkh4bjY3VXlOZUYzUVFUWnU3L0g3WWloSUJzaE5wdHI5Rm81UFV3cTZJV0E5dmgwWTNSOTVOMDNadUxWdFhrTnZEditPd0xkTWc2NGp2NUtjRWxhVFdvK2tkVU1IWkd6NlkyRmxScDVWWFVqZHBhSmY5RFB5QVJxN3dNZzJBODVNRHNVZ2JtdmNHRk1tQkRQOWZKSGhqcUI3VkpqTy9NQ1FPTlBlOGhsQnRYL0lKcFRXOGhCRFJyOHlBRUtqSUcxZm9OT3JLZm5MYTFaRHltd3lMWDdwVlV4V0RaVE42RytweGRXUHlVK2dkTkN1aEpxbkppOG8yYXVrTk1lbUVPMVQxNGtiL1VRcGNrUmkxbFd1anNvaUNXcVA5TVc0amUwblpWNnhoZ0JlNkJTanU0ZW51c0g5Q1FZWlhLZ0JUVVVQZmM0TWRSdDlmOWRqaWdxVDdWUjBKR3hXVWNLcUYxZG9mL2VtUCtaZ21ibzVrUE5id29SZnpQcUIrbzczZERiU3JxTDJRU2ZwMFBFaE1EMkxmVzdOOFlIT283a2x1blZ5VlZHYzdLZnE4N2h4MlZBb1JpNW5qd3VCWE1KQkZoL0lydlh0T2pkUklWTThick9FSTVYNnI4S2lPQTRWK3NjUFA3d0toNHBPalE0YURTbWRDYUtHeUNnM2xsWm9aVVM5eUp3NHhkeWxoZXA0THNPL3p6cGpkVlJuK0lIbGR4R0JRdzB4MXpTMGdVeHVyY1pjTXFMMW54WXlYK3BaWDdkZmE0clg0c3IzZ0tTS1VTRHNoK0NxSlNxbHN4bGRjZmhTSlJNWUJjdUtZa3BVeThkbm9KZERLVDV1QXNLV0lmaEdhdnVxTXhGckYyd25ySUl0TzRDL1ZPMEtQSUpWRkI1VE1ITmlrQWFXZmd5bWxXaFVMd3JERXIra3g3YXhaZ1pCTVdzcHN5bUh2ejRnRHYvZkZJTGgzTGE0TlhwZ04xZ1dGMExwSWVXVWpvMElOVXJMTHRUbVpMU2wyanpXRUdUR0xTTVpJSjlPUW9FcXFmTjlab21tRjFQQVdFUGV2WDVIKzFiSTIzV3hacXNDY2Y3UXpTSENNZmdpNGZvcG5tRGIrd3NxVTJOT1pCdS9lUGdXMWxlTm1mYWRnK3FwK3VKUlo3WjQ0blpnRWhpQm12TjYzd1FiaE9wbGQzZHpTbGxCZDlkeFlUNlVHRktrbmNLR2hBWEJOVEZCWlJFcEhieWdRaWR5bGF2d05NaWRBdHF6U3JiNWJLRit3UG5YNVZ0Y2E0cGkrMksyZmRlbWpBUjR0dEM5K0V4K3A0aDFSV2FEc2tmbyt0YVFpOC9RK1pPbCtGdU5IbkRmMjlLM09iRlpqaTNQeEM1a3B0RnVkcytOWEg1WTU4bnJJN0JTMnFaeGR0M2NoR3FCcW93NXo4MDc4VDFHclpRbVl1VXd1NFU1Q1BrbVF2Vnk1UGtmY1NUOVJLaWZEK08wTGl2NzBTZW9pSXpJSE5qYmFvNWlwUlAvUnNGZjVQMzk1M2cxY3BWNStmMlRjQnlJcUFZc3RDM2hpdVFmbkd5TElRdHFiYXIzQ3ZYRC8rbXJEQnduZVg3UmtCTmt3L0IzejM1djJNc1E0TDNkaHpHQ0NRM1ZpeG1hM0VvWXk3dmVZdmdNRGVqNXRBdkUzMXhyclBKTG9kUlJQYUZoWVFzWUVqUml6Q3N6U3BaSkpQcFlQb29uRjFmUE54eWdBZURQeU4rQlY3bFh6dE1YVy92WW5DMHcveGhvZ3NlL3UwVE51QkdONXgwUytRbHRtcEtyUFVyWUNoZWFpbGw4TzlEMW1BWjUzTlcwa25va1ZQaUg2N24raXdIYnN1T1g3d0xTN3ZLNTVNUDdxNytUVXQ0V1JOUGpxUmI1bk05WlI5NzlYdlFjcjF4eGNyblR0YmpKTzRiRGYyRTVuZzBveGFPcmQ2VzlyQzJvOEQyd1BLMXFYRHU2UGN1dVhBd0UxeU5JUzVVU0pvSjlqSTZYVG5LSmNqVnNZMVpTK2UrTVEvNGpvdHNkdkFlT3FvUWYxUmFEa3ZnTndDUTc0RUZzY21XaC9SK1g4NWJnR1I2WitocWpGTGNRaUh0YklvRlZRdEt0ay9ESCszV0J3dEE5QWlxTC9sK09mbHRSZ0Y2SGdxU09tVjVwOG1rNkxuV2kvWWl2NUFoZ0dxSHM4ZmlhcXVuNmFGU0lINzI4dzRDczlFL0ZiK1R1Qzl1ME5Ldk91WnU3aFZtSk9lRFB6eDgzZEpnd3pTZXdsaFVnVitHLzNvMlQvRU1UcVZzNVZZK2RHVmN3cXFnZlRqMFNOUVZpZGpqYnltQUY0TjhjNWNHb2J1ck92WjArUnZveGE0RURLdCtBNEh4dXBWbGw0S1dwYndkMmdWR3Y0R3l6OGRBZnQvUVNOYWgvSE5zV29UelFrVDVQVWVQNzEyaTkxa2I1VDkrV2YvSWNGV0JsUFhUTFViU2FTU1NPZVB6YjZOQ1BxWFUzWEZPRXBjSnNlNzlaOVFPS1BYbU9kcm02eEc2cUx1TnZJeU1pVnJYRkFNOUdWbTM3VjJpczFXdytXNStIWlErc1pXM2FERmpDT0o2ZUpBNXRoTGg4bmJUZC9JRzJDWE9rTXltakhabmlvK09qMW9hVGRPNEorZGR5Rks2MCtwQm5ESFJpWExNcjlyWVE0OUVGN1J4Y1NwWURaTDFqWTBxdWkzc3YxUVV3WnlKREhGYlB1N1BpK1BhSnN5NDRJdEN2SDdsYlJnVlpBWWRyeC9PcGl0L05tQzlLL0xpQmpvekxCSnZpV0lRYmFxQUpRNWl0KzJjdVQrc3hVQjYyWXphQW10YVNpZFBmZ1c5eHQrZmJqWUhzRnhaMG50eGYzbjd5a283bUhmZmFKMm9WRUZxckRvNTY5NlpyRkkxTXZOZGNRdHBYbHplWWRJT2FQVk0wTjJ0NFZkMXJ5NzgxbjZqVGRHOW51eDhsSEM1RnpSTWhQK3c5MTNHOUYwWWNEU0cxUEEvQ21jTW4rTGtyYW1SMDRlV2NCMk1HdFhabnZHSVFPa1ZSMnFtSTh6QTRSZlY4UkluWTNCTHF3UTcrVFp4NkI0eWNqM3pOME1KVzRlS2k1M09WdWNndU1TNGdOY1RRWjZFRVVrcld3SlltR2F3SWhNaWFZZTBNaXZxaXJkV0ZCSml2Y1NWY2ZsZW1CVldudUVSa2w5UHFiY05WWWpRYjhDR0lJQjJRVVFFYUI0elVtNysrZENjUFlTcVJpM1l2cTdvczlXQjgrYVUyQ0h5L0ovNmFFTVd1ZGF1VUtFcXI0TGZwejBCVEg0dkJNU0N1NzQ3QXEyb1VvL2oxTmJaVElFdFhER2lubTVmQWE4UVpML2NoenZXNlNOMGdPTDlhWGtxdWtNV0t2VE1QWDEwajR0UzhORmVSeVpSRHo4T1BkWldrTmlBWVMrYWtQSjVER2NFMk1raGk0dVVCc1pvaVVjczFLd1ZScTZBdWFyeGZhMnFjMXV4SW80QWMxLzRGZzhmcmE2djIyOU1iZjJDcm1qRFFCcXRlRERvNnpIMTN0cnd1bFlrZ3ErU3oybFNKelFWdmZMVlF2Z01tNjA2MHlVWnlrRmprZFhJclM1SXNvZFROY2Q1bENnc0Zobit4SmxMSjV4VEVYeE8wNzlCMUFXRk1Xa0J3QWh3dDlvUEFaZUhRUW5qSmhWblJ6SFpnTmdpcll3SkhTSU9wS05wUStYdEJXZGF1Z3B0K2thWTBNbkkwMGlka3k5cnBjd0VaQVRRWkxjZHpKVHhTd09EaXhIeTZUTUhzQUhzTXQvVWJHZkE4RWtVbWR3a3g5cllOS2tJR0VEQmlIbHA4Qkp1L0puN0xCUlhMcUlTbk5uUjdWSHQzbGNwVWt6TlFGc0cxQkI5NlZodFVINFM2TytYeHZSWWRpVmhSWUNHYm9NTkdPdE82dXZsbmgyK1JBMnVCT3dNOFNUaXRKb3lodmhycnVRcmNDZjIzcXBGSzd6QklMcStvNlluL3R6bGtsUmF5c1RwTCtYNWE5RjlQNWdtSGV6RGt6bnZ5SVdRNGFSaDNHWmhzY1V6RDZlM0tkcGhLT1NnUWhiN28yWWh1YlZwL3FHcUNmalpQYXpsNzZwUXRZTExSNUdsbmUvdUo0YWsrKys3VUlra3pUejgydUtLbE1qaitnY3U1RVFPUnRtWllTV05Wa2lnUzJsckl1eHUxM0p1YW4vSm9XQzJWRWVaemU2SVBIQ2E2SUlaa214U2hWSC9vdUhVS0FXMG5VR1VSVzFNblM4eU9PSW9lOFlUSWZJSjVRU2FJaFdqSXhqOEhGVlVOb1MxZ3dzU1o5dzJySTBKUFJ5VTZLaGxHNm1YZSt0Znp5NWVjV0IrazlCa0xDYmI5TGIwMnVCU3c2OXRwUGZhVXU5MWhRd2dheHVUQ096U2h6TThsdFF4bTZQU29FNTlQNVBEdnkwZ3EwQWV0VEJCWjFZMGRSbEwxdlpXamtNc0gzWmJ3UGdiMHBOdjJPM1RJdmtkRnpDaTh5UDUyUXV0bUlxakJONDc1ZUFMMjN4OGhYSFB3dVg3MUpZajdkY0F6U0FpMC83ckFwdERHaVVQekM0YmluM0ZlUmlpTnM3RkxiTUZaZzc1QUlWSmVaVzc2UFh0Ymo1QzdlbmlwTysyRFVPNVhFSXJVWGxTK0RHSEJ3RUFYNjNkaWtzbjlsWEp1cks5aTMvYmxMVnJURVNDR2V4bGZOWmZteE01MnZUeW9va3QxZnQ2eUxqNnFWNUZnY0NYZVpsWjBsSisyS1MwcFV4Z1ltcWIyNUI5WmhYVmNHRGVkSjZxZ0psSjJncDkza1EwU1UzUUo1YzQwM1NqbXYvRld6TFNtbXI3dTkwTGhpN3hTMHVPZkhNd1QxWlNlQk53blpjWHZoYTFVV01kMmVzQy9DOUVlRHI1UmNlV3dySlFyU2hPU0pBZFU3NlZYOHlha0IxaFo5TjVicnN0cDFPdUxsZGdmeVlVT256eXNoQ0ViMGZaY1NrK3ZRZzlITk1qRHJobjZvQ0xNNUF0aU9nQ29tWGZhSXpyU1ExQjhyVXVuU1RKV0NpY0pPL0sxMjNMYkNnakZRdi9yZ21IQTAycFNZU0FhcWNrbUVhQXdWRDBWVUFKZ3J3RmRDZWxISmJUZVkvQ205WWFYblM4ZEZ6dTN1dU96UkpEQ01ZVzlrb1B5ckJCZ0R6TEFoZUxXRlFPYTljb3Z3cWt5SU1QNnR6VXJBclZlRTVLNURrblZHNDJsd21EYjBTRnFBNDQ4TjAzTmJOVU1EZGZRTXZ5WWt5OFVhVU5XTHRhdjk3REllTGl5TDJqT2NqQzlBT2Y3RU5rME4wczE1dUFuRkwySitscjZCWjk2cktESEJjd0RhSlhVc3FhU1IwZDBRS3c1N1k4QU1OYWpmdlhDMlQwYVNYQ3pYMm5iSko1VkVhQVpMWW5QaHFnNE1yeDRndFJXSWdaMDN0eElnWGU1YkJHTnR3eGpKdU40S1hkRmROd2ttS0w2eXVwSjhMRERuVlN1a3RnY1lGaVBqL1lNRENxR1ZseTJ2VmZ3NmU3UjBtbWxmdTBJN202RlFWUDJucGkwNXFCOSt2RUc2ekxhVDllbTd0M0dMK2VsVHBqZXFETCtGdmtwdVd1MUtZTnNoT3owZzJLSi9ZNWlTWEpGWnkyWWkrd1pBMVBMcEN5UnFwVDB3UGRReElkN1F5L1E4ZW1aOUlKVGRzRzZ5bHoxWXJpZnJ2ZTd3eFRqUmFsT3pvVjNDdXFMREJDWWViRFVOaXY1UW5Nam5BU2FzL1Vac05QUXZFdVg2YmZJVXJYZ0ZiV25FZzcyTWZzM0tZS1F2S0czUEVCVGJEeU5UcUxWcytmS2syeE5DS2RhcTlNcGVXWjZiYkNqblNWc2dHeXVPZXFhdURTa0hqbWxhbUhXYzhvRkF2bTVnRnVZZHY3MUhPQUx0RW90NjVJNHgrZXltSEtLVGRZTVViUzhKRncrb2h3WFdVNWNudUVkNm9nMEZ4SGpRYlpGQ00zb0VMT3hUZDFTRS9zalpXaHhQdXlwSU94SGhSQVFXdjZxWW1oZVY4TEtQc1pkOVo4QUoxTXE5bmg0ZEV1OERoZGRrTWJRb3RxeHVUUm9KV0c4NXZJMjFNVFN3Q09vN1B0MGROQ2NtdmJYUUxRajEzNzJhSEtXUGRNU0xReFAxaXFBdzlZdENiT2VOek5XR2JBOEpPWWRMQ2pqM0pPWVhLcU4xeUlGNytMMS9DOEVsTk02T0ZzSnZzYmM5VHBES3daVzdkQ1J3OUhFK1FTVElxakZmM21MdTlaK05PZlRObElUWStIK2ZJY2RCcjhlRlVUVlFMaEZxMTE0Zm9yVlVadU9DdXh3RkhDb1M1RzBvSXZ6aEpla3RLVjUwRllLQk4zLzVBVHJpczZLcVBETjRqYXFxWVZUNDNtSitqRDg4dUROZGx0VmluY24xMzRyUG9vQ1lzWnoyOHpoK1A4ZkQ2Q3ZtTk5zTmpBSE16UUJ0c05NVUo4dGxRZnhlVnVoMEFOajJOc2xjek9TejdWcFZnOERZSm85alFZdzFZNGcrS2E5K0phdUJSeHVNVlAzSnlNalZyOFAweG1XOVJETkFDQlZvWkFKblE0M0xBNmtSelh0NGNOM0orTENFVEFyZVM1T3lQVDJvendFTnMwYjgwdGNTZnQ3Wkx3bVM5elVKMXhFdWdiRmRmSUxRYXl5N1lrV0l5dm1PWExtSWRYeFpCczIwZTI2a1FsUHFzL3pScW0wMG1IdDZNeWRaOUh1LytOZncvTkt0UlFCM29FOWU2K2RkQUV1SmJLZFdWdERwMzhVaVlITWtZTW8yc1ZncDlHVVgzSFFNL2hYLzNjZy9CcjI3UndkWlE3Y1ZvRkNPQnpnTTI5MlZsM3NmcFlKb0loamF0SGc2c2cxeTFTaGNORDhtdkRRbmR6cXZNQ2xsNURuVjJpVlFVUWs4S0ZpOUVwNDJ5aE9NdUttOTNrckNqVktEUlNla0lrWTg4elNjLzNuOXRCaDNEcjJuZ205VlZCN1RHNm5TUUR6Z29TUlZtVk5LdE4rY2wxZnFCT1g4TDFNVEdOcU1zN2JObncwY2xHSzA1Z2ZCTTk4MzRnUGNTZm5qNEhqS1VMV2JTblk4TVlKNytmaFNXcFNjZXQ2Q2VVaGticmFyRDM3cVNNQzVlUXBsdE9ubEUxeVlrdk51OC9DcXlFMVZORitWbzgzRnAwaE5DWHRiVnZzYnc4SFZORFVwR0duUDh4ekYxZ1lUV1JsZkpTZk01YnYyTnRRT0JSWjlYMUZzcEdJcVVxNXM2THBucG9FYThlTE9ocWtXUkRmak9ibjhHQ3dBeW5oK1RJcmNkUUtBU2JUOXNXRU52SWFuM1RDSVViMXgxWGd0b01FVkEzU0N2SVNLZUZ3bU9HbFdpMVY4NEJ5NEJhQUkyVTU4YmdjbGNza05RVTRycUpFM0ZHUDIzUjBNMnMrSnhlQS9xQjEzc3dlNWRZbVU5bWgzQWsxQnEzczZjUFR2L0kyTXJtZGpzT1B6MVpOb2UxR2ViS2xRM1NFcVJGL1MzdE1mM2g5YiswRGw1a0FWd2cyUVBjTG1JTUdRMEhndlZ2VVlYU2RHQTJYN3ZSTHU0Zi95UitCd2t2eUdjcnRNUjcwVXlsK0pDSXBsbGN5b1FvSlZGUG9JT1Fkc0wwSmRnaU9iWUs2Q1lKT0lPYStqUFZRQXVneXkwalh0YjlKbmQvRlNQTlpRbXZicjB4ak5tRElONm1RazlXTGV3YTJGOVVaeFRqODF0b0FCRVVmd3prbUhVWGx1amQrb1lleUxMT3NlOXpIczVlR3ZWczlQNzNOMEQ1VkdxS2pJTk1uUDBsMmxFRkh2ZG96MlRYVGlFVlZVanVMWE8rRmtlaGh2QXR3RDFib3RnR0xlSTZENkxHdXNwQ2hkaTdiSGlUSDRDM0lldXZCaEFFQ1lOOXp2SllSNXVibkVnUGNVNmVMNEdNY3IwRThHY2hXYWVRbkxxTTJUUTNsZFJlOEZva2l1Z2ZyWCt4MWtiaXVhRWVHYi9GVmpPZkprd2FCcHE2U3NUSXE0VDBMY3ZJdGFmTS8zenRXT2pRZm9VSzAxNENiQTBUQlRZdlAzVkRQQjM4bXUwNUFRSGZ0M2NtQmU3M1JlenM4Y0pscFJCaEU4Qm1hY1Z1bzhmaTloQkdLNmFqYUpiUTFjT3ZjSzZqOTJVVit3cFFLYjRkQnluRFRBSnBVNVRwQlVSZzFTeVpDeTdWWi9iTGkyMHc4K1dydlFuQTlNbVZ6emR5VExxWkcyWUlLT2lBTHVOdnhTUXNWekRubjN1TGZPa0JhYnJtdUVCSWRDMGtCT0hmY3ZJQ3VycDd6MUNjTDBSTE91STdkNUlIUXI3eEtUbVcvT1ZocUJYMVpGWmNKdUwxSTl6bnlEZ1NPa24rOVVIc3JQOXp6d1daNFJxQUdtRGJ4Q3RHRzZ4WXJDdlFFMWQxK3VZYU1KQ0xpRXlUWlNxSHIzdzJrUnViaHdpN2FTd1dVQ2VJaFdBUHJEWnMvdXZoZXhxc2lONXpVbFBpYWd3T0hKenh0cmJKZ3B6NmFjWjMzK2E0S1lSeS9hNGVLNDJyWFJwUHRQTzVaK0F5MUFBYkI1dExTcVBmNE9vMk9nRnJPVWt2Qld5VVpLN2xpUEdINm4waElpY29MUFR1K1BuSjRDNmNmc1owRjlHdTJSN3N5dEt5eldFUTd4Ti93QlFoT3BHNlFBMXNQbStEejgyYVluTng4TThoSVhsNCt6Q3RWNXZ1ODhSRWdCelpaK2ZUeldHTWM3UllWeWtOc1JqVHFXeFI2UCtLVVAzWHlFL25EQ2U2ZWtYUTg4NmJiTGlQZTlUZWZxdDQ2TVUwcmszNkJRcm8zcHlyTjRCZUpaTGx4blNYYWYzWXpaT2g2R3BiMThtZlBQY1lBN24weS9sZjNIbGpBZjhCWXNLOS9kK1NOODFGelN0K1l0RkV3MnNyUHVNZXVITXQrSGRvdktsaTRobTFGeWd1L3I3NWMyQWN2TE9OT1hNL0tvaW9SYjB2dXFDREV5MXVtUnhzSmZpOU95NEVBd1hNZ2ZuYXhqa1FiMFdmejB5WFBub0tVSDArSUdPekd4ZUxITFloT3REa0pRTXEvMkZRSjRRdVBuRzBndTZIU2p2SGNKYW5lck1mREpVYlAxbjU0YkhsZ0V5U2E5ZE54bVV6TVdWMC96U3grMy9ESFVpczBzcmhNYjV2NUxIU3Erdmw4UXVhbVFQeDBrdnhYS3ByOXlwa1Z5VThWUmNwZmpBVnpCM0FNa3kzNGM5K1NrNEErVmtUNlBEYlhDSklVdkgzUkduSFB4amRqdDBJdzNvbHg2dUVOS2tsZmdNdU9OSkV2SXltRGhFcWx0SnZwQjJ1bWZzVVl5Kzh0K2Jva3hXOElQSnRSRllCdXNLMjVORktIcE5xdXRhVlF6dFFSWm5idEVhenQwYVoxbUZZMjZXNlQxQVovaytobi8ycEZjN2lrU2VzK2Zoc2dCZVhHa0ZkQVFlOEN6QnUwZnlIWXBXSWFaWXJYVlNJczN5V2lNY0dIZ0I4YVhEeU9EVlc5VTJ2YVhvY0RJa1FzUEpVeUlNU1lmWHZtd1JaUDc1Qm1Xd1hNdDA3dmlGM29xT3FPNlA3U1ZGKzFLOFU1eWNQaXAxN2ZZdW5VSHA0Z2RPNSt5RGpEZjJUcW9MRUpIWTZ2UFRZWnNOanBRR1FHTXZYTnJma0V6NmxTSnBFUGtiN1RNV1dPWFdVamIrbHhjSTF1MHJCRXFYMmE1M0kwNkcvMVdwOXN5YThtVWlyNDgwQW1iV1VUTEhqWFpOSXNMNXpMWkg3M3V3bHJsS1lMMEQ0L3RTWVhHa0psbWRZMnZ1N1d5SFl4T1dMSmt2dzFHWm5lZi9iOUJNbk1VUWlNYkQ4djdkSFNNZHVMYTM5QkdjT0tocWNwdHpUWXNjQW9XMlRtMWQza3pXN29lMW82NzM4TTh2SnBBOTMybmwzelpkZHEyVGUrM1VGaEU2R0xndGNNUnQxL1I4NEJrcDErWXQ0UlBFM0Jibml0L0t2aGVkLzJwTWNzMnlmSFN3NGNNQlNyK0dLREJCcGV2SStqTzVyeXN4TFZEckRjZ3lxTHpwTGJ5S3RCOStzcE0rK1NMY0F2RUZ6QVp3VHNNNU41QVhld1RIeG1yMXM4MWFHVHZ5bDh6Snpna1pqQlZ0NmRtWkJlMUY3d0JGcUI2aTI1cmZ3Q3g4Z056czV5Ry9FYUw4UlBVZ05GSi9Jbit0MWlVUHordENza3lKdFl6TzNvQUdJSlRXdDBZN3dJVUEzMGFoYVY5TS9zY1IwZkNSZEdOZ2JxV2xSdVlLT01nS1BQQ2hucVJUQkRUdXpVb2FWQldHTWtzcXI1bHJzU1MzSVc1K3JZYU5zTUdZdHZrUHU4MnIzY3E0WUREck1yWU4vSStJU0gxVUVudlZDQitzSWZ2NzRZU1pXbnlVdkw3b1ZOSjNWV2xBMlBCa01BeHM3cVJWNjJXTEZlaGpqazhWcFdYMHhnWWY5UTBsWE00OU55WlRtY3VTcko1bUwvejd4Z3N4cTAxdWYySVpxLzNEZkpidlpBeDlGbFNFSHlwQVlYd2dZSDJ4a1hmNVF6cDJQb205UytET0k1WHdsc0hXYVZzd1h4a3NhZVUvaURpVmt4bmRWVHNiWUdleE5vMG1yRFJyVmFWMzRCQkNxWGcrMlRxeC8xNEt6TmtsTmk1M2VJeHVhZ09UWnZleS9aVGpOZFFwNVZTeURMMU80RVR5TDNPcHc4citJWi9BNE9aNGJxdytpc3VDNzJBTUZBRmgvQkZ5ZCtVMk90K2tWV1E1ekJ6VnVnMEs5MkNMa0JzazBEbkQzWWFSeTJaclpwUm9QRTBMWjhvc1YvaUVZTmR6VytFV1BvalJReWRiOU41SXk5VGl1UnF2Nk5PNEtWazFnUm1TT2s4V3JnMWZ1eEdadVhidzl3UHpVbkZmRGtjRXg5dktxZzRuQkNUSW5TWEozZkE0ZjB3c3VsQTMvMW50a3hUYU1CTWtoWndLTmdtRlhCMTg5Z3JpZmNxNGMwZDlMenpNYjZ0dmlwNEhoRk9sM3h5RHRVT3NZL3plMlRqZDRtT0dKWmVtNktyZFZYSFhFMlRmbEYvem9EQW0vbU5tS0lnUGU3YzRzMnovZmRCK25DdWxxY2tGTDhNSUg2aHVONWh0Q1l0TlFTZzZqZE4rNS9FTGFzd2JOelo4aEYwbTZTdjVWMENkcTR1cUlUUWg0Z2dENjV6VkhFQU1JMW9XcExoZmkvOE1LOEZLbXBMdDdaMldiWFRVSVpYMGwydXA0anJ2TDRXYjZDZEMvS1VkTnJsRGtmS1N3NXVGY1ZkcUpuYWFJdTFTUjRTbzVXWEM4SERTWC9UVVlCOUw4cU85WHJKK1pvaFR3RGZiREZOaUFtdXVaVEIzV29ud2V6WkpNOHFnbm1qTG94Q1FDV29nOW1ZL0hSUFYvbzM5VTFKakNMYWZNTWJyeFRvY2NuQ2U4Nngzbml5dDA5Ti9oNjZYZWRtRDJsWTRJRHlkUjcxNDRnc1cyQ2daWHoxSlJUZjRTdGZ3ZHRwMkt0WFBwdmp6UWlFNUhQS0RVOXY2TTdJTFV1QUtxMWd4NmZBZGxnU3JSc1JFMHF6WjAvWDdzQlpaSUtpOG96TzE5K1ZmK1ZXRTd1c2IrS3lOam1BOGVJZGZVV1ZQc2dreHhsTU5MSjVxZmJFYlN1NUxYeE5RWjVEVGh4OG1ndTJTbGw4MVRaaHF2dzlQOGVLUjBqa04vUzdSdmgxN1BGNThvUk9qSHFOdEpZcDVmM3JvQU1IeVk2bG5lQ3ZZZXJBYjY4MjlEcTFlZVJzUEJJWFZJbTVaZmV4ZGJzRWpLUzNScVovZWozL2xCbzNCanZCeDR2WlB3WnJsdCtDRFRKblNpWExzVFlEbHVNL3ZrZjE1L2VxZkZIa1cyMVlMVDRzYW4zWnBkSFM1RHBzZklVTS9OV21kWWRKY1gyRng4b01ob1h0VHdHWDFLcUxNUEtQN1BETkdxaUxvWkV2MjFDK0VpRnh6aUVPQk9KZjMyT05QbmdiL01FcE1LakRFZ1hUbzNDZTBzV2lxNjVWQlRtUjY0YlB4SElHb2g5ZnYxYWQ4bGFmWkdqNHJWaGZjQkdiRVgwQzYxQ1NIdE1XbGxrTTRvOHhPS3NPQ0MwUEZVaG9YOEFwV2NpSjdoNjhPMURQamMwbWI3NVBEQnVZa2JFak1RMllvMXNqNjdOdU54TTh5NnJ5ai96eTBnT0hVMXdkN0lEY3dIdjduSUVqN0U1eGQwTzdhMm9nV3ZRRU9kamJPMDMvR25OdUpUTThJMVl5S05vcWw5T0tVQnBnUVp5OHpUaDM2aWZZZDJSRGlKRWZXKzUybnNWTXBLTkJkcllOaWRYMFgzTXpKSmxwNHVjdjJvWWRhTkZiVHNIM2w1WGxleWVEMmxyK2NCcFBIQW1VL1BVOFdTQWNET3QvZ1IzZDBhS3pFRmZIRC90c3htL2ZTSk5KdzB3bnNtWjEwU2lROWlHbG5BZklXUjBsaEZDR20rRjhuV0JoTEljZEZWMTJaZllGUWdjY2grTlNaYnVpM3lKODl2eHVaV0tXcnd3NkEvaE1vU1dsVzRHdzFOeXhFeXVKQWp1OG9TU1Vzd0hKTmxyRkFPMG1tcHg2Nmd1K1o2cDR4N3JxOTRPcUQ2Sll6SURhSVhQc2pDUDhyTjNqOE5kSkIrY0JYWjlDdlViNDZ3VEFSM1I0UVdqYjI1QUlNeTdtd0RjZEUxcDkrcmVKbjFDY0FMYk55N1B4eGk5cVJSVnBSSTNyTFRLR0VtLzlrUTdSckNWb0dmOWhKa1d1bVhNTDloTDB2bnhwTTkzbmliMlRPV3BUL1U5ZUcrUXh0ZHdOVm5NSHA3bzhrMmR0WnBsbFoxWkJTb294ZzhUSDZKZXFST2s2NGpNZzN0RDFZYVNrbXRZQ2RuaVVycGRtaStQRmk3dUpWR2dVRzZybTNYZ2xNT3ZJYmJzZ3VadmhSSnNRa1BONjlWc3VQRms3akdLanMxWlNhUnJkOThtcDFPbzJ6M0FzQjZxbW1tTCsyaGx2WGFFcXVKSWhpOXgwS0ZBMm5mdHRXL2o3QnJnN3R3MG43bUZqMm1mVE4yNzFtaUtUSCtIU0pvRkxMdnd6WXRqTEdDaXhuWTZZOFdma2VQYzZ2SEw1VUZFRkVXR1pCcHZCWE0zeXBlMkJrSVRwRlRJSFkyRjBPbnFqZ2Z6YnVOQXhIQ05OV3JkRDA1aHhOa2xGMitkZEc3cE1CYllIYThqZUdZU25sZGVPRnRqOG4xYmhidVJVYmRtckROM2VWQnoxcmpPeDZsaU51a21lYXJRN1RKeGNvUjB4dXVaSVNNUk8rK1N1SFRYYktnU1VVYUo2OUFEQThoOWFTOHg5NXVtQkl5UklRKzBJVkZHWnpyY3plMjU0Q2VqMVRYMjJKMU9rSGR5Q0szQktYZ2lSM0dqaGVYdlppdDFmOVZOcUtYUjJvcWMyRExva0ZqUXZoVjlKVFl4aUQyckZOSUp5eWdWMVBFSjFLenNBbURZcFVSUzE3ZmlPV3M0VllJb0VIVUwxbS93amRHbTFZL3hzL0hEZCtoOUVSbi92OXlsQjc1Zm01SUFwaDViRlZyWWluTkoveGQwMHgxUWVCUk5PSkwvR1RqVkpicmNJR2lWaVA1ZlpQYTVraWNPUW1XNEVLNncxUC96YjFMbDc5dlIyQ0VSdVZxTHQ0cnJOSzFNQmxRUUgwUm9VUHpUOWN6eWlHalQ3ZERxbTN5d2xSQlV0SFphdDdzWWFSTlJNRTJyZ1Z3V2pTeFJtRFlMVTJkanJ1QlMzUGlCeGE4ZlNTdmlDY2hMRWhkNGxZa3d1NFpQVkhxRnVqQVZzdmNlZXFNdDZmNysybUducHI3T0pTRTNmR2FrUkRTVGViRGIrazg1VHcyeUtWeFFocnM3eWdIeWtadEpSdEZyOFBEUFd6Y1pscXMveStIV0xiNVpCRjZoeXREd3lacWZXVVV0Z1dJamdTN0VpL0NDM29Fb3BYWGgwbDU4MElyM1BZcTNhY2Q1MkpxeGV3eXBuZVNBaVU1bisrbkpYRThFWlpKNklIUXRGSjF5cG5SdzNabmJIY3VVazVXeXRFOHQyN2xKV201bGFnMStIQStGR3l0UjMzb1NPMVN5UTVhY2dOdTFjaDlzaGJ5SnpnOHhtZ3VrSjRoTCtsRW1Ua25GbEZZS0VCSjF0MitqaThlMy9McU9sZkhvOVRRODArdUFlL2ZIcGl4aVpHZlMza291bngzMWdHeWZaYnpsOGQ5dGE1NVNjd3pWQlJ4RmNPRFNWbk8yV1NYeE9xUTJnTnIxWFFZemVSRE05aGY1S1UwWHVyNjVIdms1blF6WlFlWHV3dmRsb3B1M1l0YldZRldvYU5hUSsrbkFrYnVtMWdFOERWUWUvQXRTM2F2emRTSEdCUUh6UUZhYzc4UFkxWDdHc0hCT240d1hFbFVyUG85MzQyN01LQnNPaUhBZkFTUTVOYlJWS2l4U0lNc2wvM003dGhjQTlxRnE4SExUMVNQN2l2YWhLbXFicGl4eE5HM043NGRVWGgwRDdvZWdqS2dqNWcwbkVtT1BOOXhXNkhLQys4bnBBWmMyU01VSSs4NU5aeWpWeXNQajdNUVE5dW1nRFh2Z0NsZVVuclhMN3dCblBOZ3ZGNkJJVXBJbUZ6NGxFWi9mOUdySE9HSy84NlZldlV4bFIrVkI0aFRzQkplRFRweXVLazlRL3QwaG03djBNWlN1bHgwWERuRzBTQm51RXUrWEllTmlkRFJXV0cxU016N1JkSFYyUW90TXpYYkFqUEdRU2dpNjJ1YmVnblpYbXArYW9aRVIvRU9JRThZL2VVWWp4dE5HWHJVNURsakJZQkJocTFlRDVRamlrWWpOZHRFdFgvWFlaY0NoRHJ6MFVIV1ZEOWhFdE8vZVpvY3o2dFZ2OFppSjRUR29yZzdUWHhaTWQrYWVJamFuQ3ZGRDBSTU54eXlRSlhRZU84dktNVmJkdzBmRXpQRWk4NElxT3RpRUlMTmNZemN5OGhqUlFEakRPSXpKelFnYzlYejFTMnR5WnBSOW1TNkdibkdHQmwvVk5PTitYRmhFSFpFVzJJTXozbk1DTE9ybmdqb0RYOFg3Ty9DZkhTVHhwampJRFNxeSsrLzBvcnU3dW5JbUtmTmlWK3BQWE5xSGU2eXF1bWFOcXBjaDJhUlVyN3g5M2VkZmdDMm9XejVYaHVwN09QMnQ3b1BPU1kzVVVzdEJaUy80Q1RJeGJGTmR0V1VNRmppYmhOQUlEMDI0eEw0cEFaWWJPMXQxSkYwTHNLUmw4dGJQRldSdEhuQ1JFUmF6bzVNTEkzaTNNQ0JGNVNoN1ZCZDJ6N2ZXY3l2RzFQUFZ5MUx3OU51QUJzcDlMNmxiMnRPTExaTkUrczFBKzBXUTBGSURmZXgyK0ROQWpjS1VGSjVJT2I1UUdqUzRJcE1Uc0FHQTVvcmFpdUVlNHkxS3JaM3J5bHJQQkNNK1FvaG1sV3NPVjh3WkNlOFgzV1Q4UFNxQ0VEb3dxWEl1NTNObFZ1aE0rQnExUWVMTzI5R3FzSE5VWndEZkpuNWx6R2FLZFhVU3dNaUlCVTRYeWtOVTVoUkhRS0J4M2VXSlEyYTFxZlg1YXRKYTd0ZjhlRGpwcnR3VnJLT3FkNjZMZnpYdXBLT2U1aklrTW9lQWdCZ1RvZkRSRGlVTXBVQUhNdmtXN0JRaFZhbEh2RnpSVUI0OGhtOUNXVUIxSnIyUWI2M1REVUgyWUhyeFd3TEl1NTh6SnhmanpnVUN4Q2dZQk03ZWxjT2M2MlFhQ1djWG9CcEdPR0FScS9iVHB6eHZUOWs3MkgwU243YnZ5ZWt2aWJrUXE4U1V5U1hqamltWm9XMW5EM1RLU3pzRlFPZ0JoNWh4d3FtZkYrU1hybmJoUXFhamUvWTMyMGRpSUpaVUJyT0tWOGR0Y3FnWUJHUklOTUhSSWZyYUt3UEtQOCtLSFp3V1Z5OVBuQ2lOcFJZa0VZTm1zaUF0SDl2VTZDRzFjamdxalFxL010Mlg1VEttamU1OTB3dHhnT3pFVll5VlVKaGUzSjltNUE3TXNsK1VoTmRrMDc4UFg5VFB3aEpPaGxBV21ZOGI3ZWlIMmR0aDRxMS9xRXJRc2FFZXNGWkpBU042cmRLcnRmbWtiNFhsMDNVWHpTZlhSdmk1NXZZTzFoNk1wUjJSTDgxRm4xQkdiTVpxRkZVdmNrd21va25SN0dZWldObSt6QjU1N3JGUDFEUVZsZ0ZDbFlRNHhLaTNlUGlBMDRodDNqcVhkd1VzNmlGWnVKbGtOZG00WitFRGZ0T0lNVkFKVW03aXdTdkdLcVhXWk9HR0FjSVg5bm1NdmFWcThhRSs1SUN3ZEpJVnpGdVdHZzE4akZOQ3BSNG51WjZkaXdFQlIrd0lNOXJkenBYemJZZVdZVXFoSzMvTGQ3NWRocnFuR2dFejFCM01hNWw0VmV1ajNQeENzMkhWcUdvQzFlU21vMGI5b01LeWJXdzdtWmg0Ukg0ZnE2bk5LRXh2ZFR4QVp4VjJEbFpQWTh2bkhZL0pEV0l4RmNqL0owY3pCejgyVld3eG05REtyeW4wb1BwR250Ulh5NHd6ejV3R3hmdzRldlBCUktrL2RFQmpGK1N0RzZWMFYrZk9PWnlJeHhxWFYrSHhyQ1BVY0ppQ2Q4OURjRVVtblM2YWRrMzFqNzhXL3JRWXRBZDF6Z3RzQkpLWHBkMTV1QzQ3Ulh5SzJyWTU1MnE0VTFwVCtid1o5ZG9id3REeXBndE5QWHBQcjZSR0VJSFZiYm9taHprVjEveExVOURrWWdsdk5NdFdpZ1J1VHUxeVY1TnZIT1Vtenc2UER6UHNDalJlSmdaeTFyZ25EMFd1ZWZCSmc1Unc3TllyVjBlWnpRZWJPWHVPZFlMejFZOU95aUNkVEpMOU9pV216ZCsxeDNvcHJsRXYxbjU2RVVsWE5IcnhJSFphNW1FYlVzaVhHUmhkUzA5ZUNka2RqeHBMb1hyVlZ5UGpvL3BaRTFrQXc3TWhqVHFZUkVrZUhkYkQ1NXNzNkxpSXViWWJqeVF5WTAvaklzdmwrc3lIYXNVNGdwYXpxYi9vSGxJRUlMK0g4WFdHQUZOM1h6L2E5c0MxRkJpcTZxVzdnUTRqMlVETnVnejA4c2kxSXVFZ1VhVkR4ckljMURXQVNEVGNDblhFdXdwVG1aZjBSY3hiTVhld1ZHV3YzWVJEL1UrVy91WEpGM1M1R2pLZDNyN2ZpeTFVQ0N6NHEvTHNkaUNZT3RrK1FKNFErbHZVNEZRcVhYeFFjemZUbUVXYUE1VFlvMmQ2dWl4WktPLzB0aWtHNmkvb05ZbnNZcmhMRTd6NElrUDVxSWhRWU0wek1SQSszZ3Z1V3pHS3pwVHlhRFVPV1BYMmljQ1VTMHZock55NDBqekF0MlQzajR5aVBSUDBBNDF5a3U1bE9ycXQ5ZG4rNGNhbnJXY0RkL2FFN016YjVTY3ZzMkRKRGhmUVlvdWNHcXB4UnZrUXYwN2MyY1ZCNHUxRzNSV1l6elNBbHdSSFN0WWthemRqY3VKdUtwcFpRTEc4QTNDR0Vnay9CVmx0UGJVTys5TWpFYTRJZE1NSTZscmFFL2RjOU1NNVV6NnhTUjJuUVErdzBCWlFEWUJnUGF6cDNPeXZET1N4cyswVXdhMkt2eThWalB3eVQ5TlFNd3VWd3d2Z1RYTWY2aFIxSmw1UVdMU2EySWhHM0FKd3ZsMEVFbkZleTlyYXVleXhjZUtXZHQ1QlRVWjdIN2tsb05TL1lkTHpUUXVyTnNsclI2eWJaY2p0QVVtRDBXV0hwTmJJNEV2MmsvelQ4UzB5QSsxNGRTdWhFcnpsM1VTSTN0U0Myb0VINzZITnVWdlcvcXNxOXo4dE5wOWtwUFRPYnZKR2JxOVJPWGx1SUVocWFobStpeDg2VDY3NFJiMFpPUW1zNk5wa2ZYamZRQnRxbTdDUWhPejZ5dmpRNzdBdUx4SWJVYW5GV0VSSjBobVhEKzFqMktSVGpWby9FUVBFaEpOdFJNN1RmenVQMG81UnMyVlRRNG9QV21WWE1GQVA5QllBc25xai83Ym9pSTIxQVJkYkdwWXRRTXEvQ0RqWHc1ZEF3ZFR3ekZyRHpoME5Fb2VHanVIVWVBckpyZFp6YllmajJXRjFHbjE5UHdlL0h4YmVVTm00SlZERlVHeUtESDFJZk9FZlJGdkxVNnJCSzFCWHY3U2ZGb01ZWFJrWkRjejdKcFJGVmJ1VTZmWGhKY0hHWjhhSWxPTnJJZVhTVjhTSWVZYmNWWmVNZXlHWk5mUjVEbGI1OWFkdTdTclNKS3ZiQmVUQ1hzdlRwMTJpTjFqZUM3V2RzTmNiWHVkUWtaOEhLMG9kVVp1L0tyNEZqT3YyZUFGdkJiaXUrQTZZMlpkSEtXbTdOTGQvVkcxMXRtUGdnVS9zMUVUNGYyTTFOK3l5elprUlp2TERmaVQwSjBTWnh0MkMvRENicSt3Q2N5bTE1elR4VlZqdXR4c0RWOFY4UGdzdDhJZ1JDTEEwMnpsSm12ZEwzL2hnbmtXS29SNEd4dEMxcityQnhBQXIyc2lrblhRRHFQdzNzWWluS2hEZFVENmxlU0owMHFqV1kyWHpiallhVHhUbUQwQTBoVGlNZVEzWkpqZFZ2Q2FLTWJNRUNIZkF0MzFTeTAwVVhSbmJqQ3FZMGowNDArQWZ1Tk92c1hhREVicXR6RUVSQkowSjF3Vm1taFE1VTJzc3RKc2p1ckZaVGtvbGJNdFk4RjZiNHd6VWtKYktHYWRIazU3cnkzN1R0Zm4vYUhzaFNBS3NZekpZUElZMzJkdXNBY29ubXlJcGttNzhFWXhiOXhSOTkyZW9NRDFZRHJPMWozeVdnNElDVzdYSUdHb1l4SUo4SjJ2USsxazhmTEI2K25PVWdXRlZhU3VVRG9VSXU0a3RGR3hUeUwvd00vb3NqY25HMk81aUQ5NWlmZ1hzcS9KemtwZkFjY1ZRc2wreCs3a096SGtjaFoyUWlDazJxMld5YUkyUDh6UlMyQVN3YVBoalZjd3NGODg0dzR4Y2FNemcwTW4vZTJuL3hYUkU1SFM3aHdWZWhkcjRzV0dNT0RSbks2TjVUNW9nRjVuNFNQcGxoZ1BsU3VPR2RMK1JRdDhoZ0dRZG5wUGpYRGk1TEhOUGNJN2RsMUI0TXM0UFRUdm9YMER4OUtLRVFtTitoYUZDN1ZIYjNESlBLMWU4aHVYRmgwSnVUcDAwVFFlQnlIWko5ZzQ1S3MwZjVmczZIZUNFUnA5OXlVSjV2SWxmMEJFanZTMkZ1dWdkNTBYbzFKbjQvV2hiaUlYSmU4d2JpMzg1TnRqaU44OFYwWVRGcnloeVg0bGwzMEhsMk85YlVXUFlQZU45dzFFQmd1UGd3S0hIMGpjVjdWWjJpdG9hc1FvUEdGb2l0VzRyN1dwRTBaZWtsMGQ0aHM4Wjd6c1ZHdk1uQXJEdm83NXNqUExGcFp4ckgyZWZWTUpRTEU2MzlFbUM3YnpGdGVMNVFwWlQ1K1pua0cxNGk1QlAwQ1Q1NmFZc1U5a1AzR0FUSkFBYUxJVkM1V2lCcTdUM0cwUkpPbXEyT1EzR3puVUtMbWtXSDE2MyswOHhkdzg4ckNrazE4U3V0YjlVNHdRdXJ2S1dLdndBc1FDY2NvNDhEcnROM2xRU3JTNytwSGs0ZzBFRjJMTGoxTS8wWW5BZ0JhWGNmanI3U3JldDdoZ2s2ZnRqbUY5T3BLT0VycmIrd2FHK21kbmE1R1lROGJMTzFzSGtLajk1eHlXdm9Fc0IwUWZXaytkZERGTVAzRHBDVTBUVm9XU1U2blpVbGFmNmdIeTVQRExLRmc3OE9JUWFackJsUEFRODlDQTJZTDdlKzNzL3dib3NCcnk2Q1pyRTBWZ1ZZWldNZHQ1cXg5bVRDNDVOWFFLQUhIbHN5bDJpQWVLc0diL2Jpay9zRUVlZ29rMlJZS2tJVGpCdE9pdUd1cDR3amwxSys5WmdmbEV6WjdYa2w4bnp2OU8yZWQ1NGxjd3YzSzZVRVQ3MllSWGhrdE9LQjJrOVJIMVRDdjBPakVDSXY1UHFVbndNN3ZkUUZKRGxlazB3VXRuQ0NOdWs3QTJiOTdOWmhTU2pmZzhEeUw3M2JwRGV2OGZOeEhhY3NUTHUvWG13UmdvUXllSWRvU2cvQVByRlo0RkZWOGRJT2NxZk9EZmhKT21uY0xuTEcycUFvR005b21nVFlLKzE4SVdxUFY3ejNKeWlYM2Q0bnZDeTlsU1JrTWRDdSttYnVRa0xYNnkwMWhvTVUvbjR1WnlNN05jcHM2bEZaOWlZbytCZnE5aWwvVlZLczRZZ055TVFxK1lQNitOUlZrMGwraW9jRVREa3Z5dlJOZ0hCR251OU9jeVV6MzhSeEFuTE9qYkM0WDl6KzNZRzJQV1RsSmxKeEdyWFQxSWdzb2ptZVBDRndsY1JSVFdncWFnRTNTOU1tMGkxTzJtVVVqYUVwYVNZajI1NTlDTFN2OVFhdlRWYmVQcmJkajZqWEVLc3RVWnp1Y3JqSm1BOXQ2S0RTNFBMWUpZSFRITWt6clpncHZ2SnhuaENHbnl2QlJNVWwrZVNTOXFidDJZd0o4dHpFVGpqK0JsdmdvVCsyOHdRY3Rldm4xRk9BbVBFWFh3dlltM0tYSHl4cDhUR1ZnR2h4UFNLZk14NWNST3dJNTRuSlNYTW90dEhLTWx2VG0xU1dEdkxRa0VOQnJwUDJoZ0VmWVBMWmNJWlZDVmRVS2dLZ0xzbWVDdU1lSFlNVGZvN2FvcWhvQzJXZTN3d2lnZXdNTEp0Q1dYOHFhdzU4ZHluV3IrTlcwckhrSEIyZzBCd3JxNHZ3Wk5IVHJXa0d0Zk1DSDhrU1FwRXdpaFp4VnEzT0lSL3RRY21LTmNvL3VKYkc2Ym5IdkhpSUNPN2VhZm1zK21pcEE1K2lQalJaVkpjN3ZFZkhTZFJuOUNkSWdQZVRiaGhyMUsrQ3lGK2VreWU5M2ZSK2V0VjBBSS85T3YxazdnSHN2U3l2aG9rNmFjVFJYM2I1YkRJQlRPN1RLUys5cm5IUXNhYVBSNjVjSk9qdzNSWlo3VzBBTEUrQUNkcWxSS0lYUk10dGZ0Q05tQWRqZUQvNWMrYWFuMTBkOWplQXFaOE5WeS84eUxjUWZnQlF5UEV5M2duTmFWVk1mU3hjOENGcXlaM1JGSHlNVXk1UElQV0tPQjlVeVZIczhqK3llMkwrZ0dmclRod2Rpall3cUtkRTErMDZsbFFCRnJiQ2ZqVE85STVsQTF0NUFacVlGbENUQ2dNSSsycjFBN3ovdy8yVmRxZkY3M2RZZ3R0eUl6S2w1M3RtV2hRWkRSRXNJMnNHWFEyUDU2ZnYvWXFuektxT0MwR0lqdkFJYzU5YXc2endNQjBjN0tuT2pnd2M3K0IxUDFvcytvOG9NWDVqSmtHMWh4YmZZc1dNRjFzK01GRkduNU8yOSswcHcxZzBhbkpMdjNuUW0vVDFrbzQ0QWRicUw3eERLYzRZUUhlRldWNkVDdTBVd3BKTFRzTTRWM0pZVG8zQWNoTWhncnB4b3lsd1lLR1hmMGtTVTF4NGJEd1FVRzVxaFA3Zk1jNkFsdWZGTDFUYllhclZNSDAzZEtwQXZabDgweEpjT1I2b0Z2cXVkVEYxaExHNVc1S2RnV05mSVBpNEVwZTB6V0s0YjhRV0dGL2xHbDBqUTJpZXZ6SWtzcGhSM0ZOZFdpc1o3eUNVSEVMWHNaOHl4cXp0THpzemF4dW4xT1I4dnJXUVcwM3dQLzNmQjdwT28zK3lwdFFEdlFFNnIvajBRdXJiS2VTZTJqREJQdUYxREJYYVlEWlpUTEp6R0ZSOUI5Y3FqY0w4RklJSFNaR29vQVh2QWFTem5NMjVmQzFnY0s3aGxFYm1Jb1IzdXRDejJOOXFGemNSV0VrTnlid2dLSExzMmxhR2RkWnJhS2ZFdGJQRUVjREVVMkJrLzVCNEgyVjg5SU9vUFduMy9UK1dPdkVFT0dycDM2azQ4Zno2elpyWnJNZk4rZ3lub0dSbzNoQW1mK3BYbnh4a0Z2bjZhSDRhNzVVejNpMk5ycG9heW1BR0VuRVRrRlgxM1l4U1N1YjJPVzFmdlFyZ0ZDQjlIbjk3MUs0WnRPQXd6VVNubWtQRVdqZG0yNWVvOHg1OHhJek5wVzNpWGoraW4rVlBVUzVYajRLRUpyWCthcVZ3S015Y21yMjVPMVU1OUtEbzBMdzFTdTAzeVh6bldYN04xR2VqcUIwRmx6WjBCMWZ4QTB5ZVlmOTFTUFgvYmpNN1hOMWdMalVqWWF4eWpNWEhzbGlJNXdHak52eDVJMWdSZXRjVmxLMEtaeUVFbUozeFgrcUtiaWROUXZrdW4rU0l5c1B5c0hoSUhIWTlFZmhUaFN1OFlRR1RxNFhSZ01DL28rUmZESnVMTGZtdHJFOEFIVHE1enlFcTNYd3dqdy82TzB0bHVKSE5lUWpWeGhlSUl1U2p4MVlzbmJXZ09ScTcxODA1YStpeEZraEJ1S2ZiL1JreWtZMzlNcGdXSE5mU1NTelg2ZkRxSVlQSzlwQ2ZBRC9zMU96WG9FNkMycjhady9rdG5WcUJ1UW5OOXl1eUx1Slg0Q051dzNtcmhXRUcrbWtDWk1pZUMyTDhvc0dNWktBNGZDZFF3YVNxZDdKSk9tZncySmNMbG5QSEw5dTVCUmFoM3I2YS8xeDRRbzdnSk1Qc3A4VGFEL1YxL3d0b01OMjNDT3VJUVFadkxEUzVTWXFnWTY3UUpTMzVRQ28zbUpOdENWVGRSNDQzVHpBZnhFcm5oc2xKc0NQa3NuTUx4SjNoU1RWZG0wMkJZbG9BaVc5bC9KUTc3aGlBaVprMks0NUUvRXdycGRFaTlqY1Arak5SSkUzckYwd016ZFJ6QmpSZStPY0xOMUFXdFFQaU9hNXQyelVWUnNQTHVrSUJONzNnZERieUE3WFNrTnVleUhjM0JzZ0wza3NLOFczVVNqcDVMMFRHNThMcEEzS1VLYm05S2FmTUROR0txWWRDZnY1OHZselVCN0RWRlRJdmtGOCtkUldmalAvZWNueU9laFRRalVkNVJXT1M4eGNmNHArMmFBdDF1MFpEeTE0ZVBOWHNjYWZGVnFBekVSYnV5bjBWTS9oYnJNNmhPVGE0b1QwZk9RM3VHN2xxYXJvYzJmR1oxN2xIYUhEbjdLSXlZeDl5ZlJoQjFVbzY1QnFhb0QxcTV1K0lhZzZKQjdZb2pBV21veis5TUVKTENYMFBCN29sd1FkTTNRUHVGVnZiRmRXN2ZvVElpcEdUckI0c0UrQzcxQ2x6MXhlUmpLVnFvUTA3NnRlcmFZVkNBQi9ZcFl6QTE1dHRkMk53cWRBSmUvaXF3cngyeUlncTNnOEMvbVM2bjZhVGtCY2dMODR0bDlHby96UzQybTg3emo2bGpDM3MyQTNtdU01djZaMmNoWmp0VFBiMVppRVVLM2RoUlRhSmFaRkhPYk1ucm9QMWJaQU1jSUpqNkhmWDBxR3lLTzhtUlpGNFlRbElzVjlYVUpzSjNsNnNaK0Z2dE1maE1uSUdRNU9TNTlPVHZMUkxjbi8yYVU4MVo4QUk3ZDUybG9jaHRkem5QYkRIZExGc05zU1k3Tldka29vWi8yTG1lMjI2RWdFNHA3UWhsdnZ2U3ByT2tFbE51MU9acXo3eTNSa3lrVTg1RkNZRm5ybHgreVU0WHFyOGVnQlNxOHpySDlYQVV1R3JVcjN1VlJNWDJjMnNNZTkzVGlVSEljREFtNFlBSS9wYTArZTJHeFc4T0dHRG90N0l2ZXh6eHZOdkcwTmorTElmS0xPZ09iT2xmVjBGR0JZZlIzQzJEVXk4dlRaMlRQSVdxUG5uaHJTVk1Lak9JYVhLclhJS2pld2hHOVVXRUpNSUJPYUpCbDFYTWZSNXkvTS9ZTjY1VHJVNm1uSjRiM0tTc1FrK0tSZ2hhYVBWaUp3YzczRG5mb040M1dka2U3UjlibGY3UTBKZE41OGtoZ1c2WG1hOGUwb1NGZk5ZRy9uaC9SOVNZbGVvZzNVL3IvZTY1Y1RaNVhzOWlad3RHM2l2UExIN0wzMml0S2F1Sk5xWDBGK0t0dDdEVVE3ZkhoSGs3WExMdlRnSFNGUE9sRGRQUVFZVG9QbFdlZlJxVUEzb2V2SWZpM1BheVltV0JpTzBBd3J3dzJaSEhHQk1OdGl3eGV4RTJXYVhuVGZ4WEo5R1VGUG5kWkw0OWpEOXBXSXdJWjE1MFVIaU52YWd0WmpQL0ZHam1xZEc0Q2xLYW1OVHkrNlIxd2lEM09BL3VSdTJTUmNkdlVBWXZhUWtyTDZFeVZJM21uOEhxWm9qN1JZMWE1N1FVcmN0cXpnNVltRXI1KzRJcUtCTmZMOGhaNExMTDc5ZjcyMmU0WEd1OHdkWTI0YnpKZVFLOXpFR1ZpTzJPbG9VelRyVFV4eUM5RG9wUS9ORkVzd1hLeVNrSnBPL0RvYmlVS2pEQUVaamJWMGxydmp3KzJsdWpDL2s1S2VTTjFoTGprbG94UFVDcEkraEtaTCt4UmV6cUFhMkRXd2pHd3NrR0ZreUFydXZ2YU4rcTRFL21iMG1YdG01dHVEajFoNmkyWjYrbUJHRUZGdXFqZE5XVEJUeHhxcVpRbEdEY0d2T09qMGpuaWpOT3hMWE16V1VpUjdBbWhqcDgwQU53U3JsSEI0L3VINzFRQ0pnYjFXYTlmVjBXdWlDOEsvOFNUeTBkVVZ1RFJ4bVp5aTNxTk1pV1VvcG5xYWVqUG15emdOMlY5YktJOW9zL0JRMzVQcGxDNDFDQS96WHRqR290aEdQd1ByOEJnajkwOEdkQXUzcEs0WmhwWnlwRWMwQkJtdCt6YjMySFlIZmR4QzNBR1c4d3NFaUtwZlRyVVl6WnZ2ZTg1dUYxUlI1dWNGcXIySjZadXgrWVNxMEtaV1hiV0FzQmlDcnBQUWxnd3NKOFlhdDdvOUF2bm5jblRyTms1SXF5SzJpQWhYZkhWcmxuU0xUdS9xWXFxQjhrSC8yTWVIdGJ0OG5vWUdSaDlzSmlDUGwxdFBGMTZEelNYQ2xMWkdzdVBTWEp0ejdSMjJiN0tHUll2eXdzcTZaNFdvR3RaSTB0dXBLRU1CcUROZXZZaVdXQ1l0KzZIZ1c3UjRJNGlhSzF0dkRuTWFqaXZ0Z1Nrc3NFSnVIbGNXRi9DVlc1OTJ0d0pEL3F4UE5ya2RZdzNIYUhoaGN4WlRxVmZOdzg5ckpTdmxyenJINUlGUHM1SWQ0VHAxYS82elFzQUlHNDBuLzVCb0hCbDFlbkhBUEJ3R3NlQUorRkQzbGY3K216dUFnSy9DbG50QVpJeHo1Yi8wcHp1QkxZWW9uVUVJRXdVUGxLTHJ3VjVDNTZKVDNLQm9sUkdoUDhOSVR1UVhqaUZYNVBoSXdYS0REcnVaZWxFR3VZSUJ4ZjR4SGR4SDAvaHZ4NUI0UjRaMTUwVXRmSlMxZnFXd01Fd24yc0ZDVnB4YTQ0NngxUEUvcGh5RzVIV21Vc3V1cFd4R3diQmgrMDNEaHB6cnh0amxXdTdkalZhMGdJM0VqcGVOTHhveDhyeFdZekhnaXNFd0wrWGlvdGVSRjFySDFUQnUxcEFXNXhXMXFGcGUxWDQrbWlTT1ZSRGs5dFhCZ3d5VHh3L0kwTm9TWnFIbUl1SHh0ekVESVdwYU9YZUFIN1pkbGdVQjBJRzB2R2FWa3hJU3RpbzgvUkQ2L05WaXIyM09rVEpxSXlQbVJnYzdOTStSVExxV2htYzlxMXJYSkJUaGN2MVBucFVLQlpsYWhCNjRRV3FGY2hGSk9Xd0RCb2tXeW5pYmhjZW5nRXJrczUyY0wrUGkzVnNCdVExM3ZsejI5UW5YU1h2M1JWOTlCeDdmeHIrVlROWjJCS2d6WW9wNDczdDRnR3Zqb09hTHpONHB1Q3h1V092YUJDQkc2L1dWalBuZ3ZJM3hxeHlMYTY1bjBlcGFJajcwMUh6TG1BVUkvWXRnQ25zckswazZjL2xienBjeTV4Ym5lSHQ3NTBGRnpSdk5ZUVVBak5YSjlqS3NjQjl4bG1nbkNKZDNJeE9IMGs1dWN2b1FZRURIMzc4SytXMUQxQmM0aisySkpLaEZxNmZZNlVURENIUGIxNDJoZDBxczV4QXBhemZXYTJPRGNKN2ZJNUxYSUFUWTVSQVcweER3MTVQMTBrNXEvc3dYRmFlUHBIblRrbklCZml0Vjd3eDdxTU5jdzd5V1VXbElxQlowRkU5bllDNXdZTEorVkwxclN2K0ZLZG45NmpJaHdhWi9Gb01lWHFUNm1pOUFMSkFZTmFHNlduLzNUZk11MDR6N1FHTnF3WXU5dTdKa2xIZnpoaUhQdGdxZXNkbUYvTnZTOUtkK0JRS1NIdVRmRHUwa0lXSm4rTFBUSjZ2L2RNZWVxRTNXNFFJVjA0WFFLQUoxcWFYNTJ2Z2p4Qm5WK1BzK1Q1UVgwbEpySi9DVHBTL0FYVU9RazJLN1BKa25tUzdNSkdFOXd5TFYyRC9GUUEydFQ2dDlMamp3djlNVXZWcXZBNTh4MjhkS0QyVys3U3JEMi9VTitMTGJQbGwvbHIrY3RGVmdzSllVb0RjM1NvVS9kM2VVeTRxNkU2NkIyQ1ZObVRoWDY5TWlub0dlcFdUOXgwMWxHQmp6TlJDSUZSL29tZXcxMStQai9JeW1WWnpOU0ZsSTRjbjJxYVVNbFhqQlVhQ3RnMkk5amtIWndMNjE2SUs4M3VNQXRGR2tRd05Ob3hQK09jUyt3R2ZVUzVLWUNPRWJhRVNvUGc2VE1nMldQUTNwVDhaTXl5QUs1b3pZV1ZRN2ZwYUs0YnprZmVEV1B4TFBZcE9zdnhka05VYXdpcWxSZmxQVko3a05LZnJlVVZTVkRCSkRqaHRsL24xV0pYZ1gzVXBvOUhkWUlXbU9aeUFKcm9Pb000Z2wzNzhJcThYMjJScTNMdkRqMDR0WlJaRGtqTTN4Zk5jZ0tjT2NVY3dCaTQrMCtWWnhhZnJTbFVaK1R4TkdMZVNqRXRBV05IUTBMWkxuUWg5ZjgxMXZWckorcVhKMzBrTTNTTmNsRDk3bEtrc2FidThQS1hCUkNMZ0NSdVlOeU51dGNLM3FNUStOL3hpRlpQZk5kTkRueG1OaU5haE1mRzZmVkorQkZHWEIzQVFnMnhxQklPQXZ0REk1SmE3UVpzOVJENUFHU1FPME5JR3BNYzJRTVY5MDM3bE1XUXMxSURRQzVMTlJQbFlBdDdpU3FSRk5OdjhZWHNOYTBQT1c5QXh5NkFWcjk3aVpKVHdRVG9mQU53cHRaVzQ0eldjeEthbEtVdkN5bHNxdDY3bXpBL3A1dWlBZjg0b0hRNmkyOXFXejVVYUR2VHptRll2cDFvM2tEOHUwOWgvV29yVVB0UzZrVVRwOE05REVJaVV1YXhYN1NxYk85VVhCQlpQcG5rMkdaT0E1bUNDdEI3UGEvWGF2Y2FwYllnWjZ2VzJRcE9nekx0b0ZjUUwybE5QMC9tbUJLNnhMVzA1OURwazgwUFpCdms2TmZVeFVkemlRc0EwWkZlZTE1RVg3bjVFdjNFQkt1N1dpYnZobEdiaGhQb3ozMGZVdzFiSndwR29McmkrMlpacTM5c0R3eldESVNTOWw3R2dmU0tBSkVta2IxeHc4S2dCMXJxdG9kNldYYW1pdWx4ZzZvM25JcmdXUTV5MWUwNndjVitnbnVwTTMrTXhRd3JYVWFGUmhsQ0pCOWZhTno0ZXhjOUFZZm9OeGRMOUhxcWVQTGZDWUpOSGNDNHN1eitzR0t2bnF3ckxLc2tWRTNaV1hERjAySFhrb0xCeDlYRWZKSnBCeko2aDBNYlljQnQ5ZmRuMXNLaUxyWUV4V2g5d3dsajh5NGFDY2dxL1dLTnArTGNtSTM3YWttbG5XTk51RTFRKzNCd3FJYlRsZTU0MW0xa3lzSENxcndUSlZxZERzTFFiejlpazRYZEMzVlNZS2dUalVrdVJjY3drS3dQU2xGbGtnbm5aOGJ2bk5mNzlCajdXMSsyM2M5UFhYamtDV0VuR0ZxNXlvd1RqRU5zV0NSNFpkYnBnZnpMOVdSYTZnS3B4VGtlNVN2dE9lV0dlaVVpZGM1MldXNWFHYmNSNThBcWs3M28rc28ySnpReWQ4VDJmdHFSd3J4UjAyZHVEZ05DZXNtWitNWkpDN2ZodWlDTnVUczBqcmJMZSs3aGZ0ZmVYOEx6eG00UEdGNVFna3p3bnkzOUNoNnNRMlRhQit2cHcxUmIyTmRHTC92eURnNzJ4TzhsME9YcXVjdGczQ3VBSnZQbm55cll1eTI4Ry9KS1dkcmdOZTgzSHNrYmEzamwyUlgyby9PM2JrZDJxdTluL1dwN3ozdzF3NXBHWGdUQmhmU1h0bnFMNWthdmMySzgzSGhpRExGSDBBNmgwUjVYNm0xem1GYmZrQTlvVXdPUEt1aFBKSzArV0xqTXA5NlUyV1ArWHU4eEZ3VGNQRWlpdEMyWWxqSW1RTEI0VjlaWkQzWG0zc3JDS1dzVmtkUUQzVTd1aGRPMGZjVnI4cnJVeFplNDRSdm1TeFl3YkVKR1A0SDlpUzVMQkplSnU0VGJhMHdaM01iWStXTCtzRVcyT1lqODFMM3AwdC82SEJORmRkNEtBNzN6YjVOSll6VWU5V0RtM05vTHcyUkxtUGFud1hqYkl4TkRwdnN2bEsvdWRHbGVpZVBocklNVjQyUjVIVVJOVFRESUVGTUN2akVBUk5Rb250MjZHdVJRcGR6NXRlbTJENHFLdlVVNVBwWFo1WWtvUDRtN2doMjVQdW5malBXbmtxK1NOYmlJUHBuWDdpVnl2bGZSTTJPN2dDZ3VKYWlJRUpvMXgwNVB0TDUrUEVJQ21rTmovZjdvdTlyd3diTFN1ZlJUQldMVkI0UC9aeUw0bk56cWVacXNVSkFhdE5Ma1RqUVBUTzZreUtlSTQ4WDNNZkZDU3J0dWVRSUtxUGtzcldEREttU2RYUzVNajc0NEx2MGt1T3c1TWh0TzVyWVMyTFdBeVhnWWcwd1FIYUZaaVlHZnlMeVlxMUV6N1F1UkFsSS9sVktUcXM1cDM0SHgyWG9OOG4zdWlBREdkNU10bHNwTncrWUgzTUFVdVJyUGh0bWdWTVhkV0d4YjdPSHlTWkhRSDVhYTlha3R2NmpodUZHN3JxalcrSm9lU2tnd0plYi9mdmR1WW0yakFHYWppNjI5dmc5dDdoZzJDZkhnTFdoMXRTOGU2YzUyckNCbzdOa0tnbDJub1BIbTlXM0RJUWJYai9ta0tKWFRlRUN1a3dhYzhJZm50ZzJYODFHamx3VThHcVErdHE1OFArL1AvbGd5eTZ0dzRZa0RjaURMSkpuV3F2dmM2MUg0Y3hTb0xEaTAyTnBScEtBTklDVjNYcmhUV2Zjb0I2ck43dVNjelU3d0hidW43cVk2VVJpNnNhVTNDR1JUMUVESExacmdpQWE2dnJ6L0RGTWZZTW92WlhJU2M4VTZPWjI2bGlxTjFwTzYzRDFBODh0dXcvdlAyaDZYZldoOVZGNWR6cjdNclBNdWFrYWxaSEdEWFZQMDRzNjVHSG8zY1MzbXZOdTRUR2pZNXQ1bEswVFBuSXJjMCtCcEFrY2NaYlY5QlZJU1EzSUNDYUVVS0xJaFNXTis4YkExaTZZeU1sMW9XRXhNNS9GNjhlSDBGTDR4STBsWUZ2OUovTEU0cGFBeEVoZHFSZXJxZEVvZk1yQU9HeEY3WktMaHkrMVM0VmUxNkdTMS9DU2w1VlFTR3RWeFc3REdtVDJpRHlkQVZsSjhVNnJCMXJLMnhINGo1Vm5tWGZhK3l6elVRT1NSbUl2L1RWRUpvWHJYek9LdDZFcXFyTFp5NjFvRnZvME5YVUVudUFMa21hc2dPNlI1MlhZTWU1RGN0Z1BsSXBKSkFxZkY2cFZRSUVpM1drSEljdG45anAwVGJkaktQdkJDRXBwdWpkRGVuclA4QUI1QjFBdmN0Qm9vRlY2NmZiUkFOaTl1ZjlHY3FBb3E4L1NGRExPenJkRjM3Y3B6eXRXUHhaN2hVSGtQcGRkdytCemJBWmhTeXJ1TEhyOHBhZytIWEM0TWZGdXgxcFU3V0dVWmpBK0wxaSt3V2VkL3dXcGZOUk56T3d3Rzkrc1p0ejRGQjc5aktZWk1yd3VEbkJSeEVhc01VRnRQRE1kVW0yTGtjM0Q5MS9vTEorUU1OZldwUWt1cW9YY1AveDlrR3c0a0dRbFlnMWZ0dUVHY2x5N3dabFRRbTUwQ05oVm01Myt6Wk05QzdQcWFYWngwNmlyZmlTbmNETlRDRjQ5akRHRFlZVGNRWUh4cmR1TzJ5QW9BMWFCZk1lZnY0TlVTa3JrSjNYbkFsN1JXdDR0bGFRcURqY2duYk9Cck5qSDhWQlhrM0tJeXdBSEZwc2I4Z1k5cENxQy9OMHZURFVkZ3lZS1Fic1lmQ1E4N0F6NHQwREFEU01JNHFrWGoyQkZENzN4c01tRGtRaGFaMkVTcHhqSGdmMUF5Q0EvOU1UR1ppa0lPRjdvS1pmUENETDlQV09pNEJkU3lzcFNmZE5rSkhJREt6SWdSTEdQdGZadTlLQjRBUmo2NVNUYUlqUkhLTE1Ja05KL2VSZkFJaXlPSTh2Mk83Z1NVRDNmQ3R6dGs0amZEZ2t2a0w4Qmw5UWtvSnNLbEhOVmpnOGE1K2tMOTRQcForQis0Z3UweHFjeVFNVm1Vc3ZXMUZSdndQOEJYOUQ0STFZaDViL050WDBIRmZ0QUt0Und3MHlWRUFTcFpaeEtkc0JYQ200dFBPQkVLYnBQM2l4dUpGaDJrQ3BSRmxsajVpQzhiZHFab0Yrb3NzUWVjTUxPNk9oN2lBcGlHSEs3cS9YVkpXVHVDZW1mSjRtZmEvWU5FSE16ekFPK1AyYWhYcE9nRTNSdnZ2cExlZzJNRERyK1JVOGkveENuTU52MUFIWnBvT2wxWnlockovMmNMS1NUdU5FQTRHRCtXRitod1lLOFJPUXZIeThjNVg3cTRyejNwWE0wSmlCRjJxTGFBakpMTGI3enowK05iREcvUWpKQ1h6ZGxnZ0xnalo4L3FGSVpmUFRwTnVJQVZEaU42RlgreTBwYTBmVTdwa2dhVzlyVjJOYUdtak9wZDJZZDlTdU1BZE5MS2xBbWlwM0RVSG9mcWIyMGk2Ry9zQ1l2Y05YS2M3ajNuYmJOOVo1bGY0Z0U1NThYUjFHUFB5REF3N3BtdTRTMHhScmhvejZuUmZncndhT3FPdnhtOFFFVjFlVnJSSHlVK09JOG8wV0pDZllOWk9SS1FoK3htdjBKbkhNNWdHZzJydHg0cUpscnZEMmMwOGc2V25aQWwzWTUvZXVqMzlrN1dNTG1iT0Mrb0tLUlRlRE9OS1BiZndReEdNbjdoSC91ckEvVkdCRGFLSWRHWUQrdkw3dW1sZ21PbldnM2VPd2dUL0lNdXM5REVoaW9jRncvN0Y5a3dyaWFFK2ZEdEJSMmRQQm5Uek9OWXhkRWt3Zm1HS0QwSEpRaFFSVW5HWjZ2SUlyV3NBdUZ0dncxK1NvMmRCRW1kd2EwQTcvd0V4L0ZkMi9YOUhVaXJXWlozaHhkTEVlcndvS3Y0d2RzbmZab2hmdGtvWkptU3VlaXVHVlRjYllWejBoYkcxTUJSeFpXMzZ1b2hDSXlGenJ6K1lBZlBCa0dPRjluZm1KVUZQc2JBNWw1d0dGdVcwbjFEUS81M1JpK0x5VnBIYURvY1craUp2SGQ1a2xhSE5meDNvdlFCN3RnZ1didWUwWm8reFA5NFVqdDk0bGFIT3R4Z1FmU1JJUy9kaDBTNTZyNS9FSkZIU1NjNGN4TVVnZktFOUVvc1J1YVB3ZlFZN21uNmo3dFB5TnNQVEdUbHJNQ1h6R1pWOWhtL013SGY4QmZHd043bTY3UmoxM3FoSGFZZWpvQ1JUclFab05mRDlBbHZTS1U1RllGYzNnejVaOFEveTV2OXJxWkhnSmpMUFdkeXg4L01Ca2RLUEFsNWJiRis2cmZIS2FKS0VwS3E4S3dCMkVSTGlSTTVpa2N2OHJtREVpRlBUTVlwOWZvdGJkOGIrbTU0MnlReWNkeTZXU0hrc2xlekhVSjljT0NjWjBTNS9xWjdpSkdFWFRXbmdheFJ2cTFxdDFPZDVwTUpKU0pPdmljeXQxM2xQWVFYZG9hazUxTHdHM2YzMFAyVlQvaHdsZ2RuUVVXVkdmWHFYTGZvNk1HdWVFUTFDekZCeUVxVzAzNkoxeW1aRnEreWdzUklYRnJTRXdTckpCZHJOS3piVTlKaEo3eVZ6NmIvWFdBOGxsOFl4SUxQaUZ4SklMUlBGZGI3WGw0NWNscHJkZmh1V2FSZFozQ28zZ0dPTmFxSndOSTdSaEZ4S3Y2YVdCZjhkR2JWaGJqZ1B0cTFrS0FkTGZYMVhWZmhHcjA2bHFyK0didTJPQlFVTmN1WjdpQVl6ZldURWkyU2QxcjNxWXNLRFFYRnpXUS9pd09PZldhRlNyQUM5V0k0MDl0Tkdsb3o0ODR0ZmlhSnNRSWlTc1pqS3dzQ1NxZFpvNEl6ZXd2RjBteTFQejhFRFl2QXR4U2FCTjhwQVhacXBITlBjTURTZW1iYlZrNFNCd1lyM0hhTkRGZkZOa1FNNmZuVHNRQ2FQRDNpTitJZy9rZ1hpdGw1ZG9HWlZ5L2Zza00xTTVsWk9KeFF5ZXhvUnBSNFNDa3E1UW1EVHZLZi9jaE1OOWtmS3AxRVlmZUY5RUhoY2h5OXdQTlJyNGwzbStLR1NUTHE4aW90dFV3Ny9OaE5PSGNoS2ttUkFxWjd3QTNlNUdhYXdtaU11ZEZsdkRGNjVBZ0hNMElVSEp0M0NWbnJ4ZS9ZOGoweUJjd0dRbFpCckdnRE9wYm9ta0pZVFovMzZ3T0dXYXNaOHFkSDl3UXZxSEtCT0REcVFPa3ZNS3JpWUlCRnU0K081Y3hiRzJmUUQ4ZHA4QzdYVk50VEY2M0pnbzd1alMyNWlCRjdlSjdOYlQ4NG5uczJIQlRWM2hrZ2Z5cFhHNlYvVlNldkQwVWQ2d0U0SU5Rc0RaZkpqSnlGbGFKbDNrMitGelhLOVFTSFdHSmQrVXpJK2ZMQ0dYa2NOWnNpNXRqcTNyMElxYnQ5bTVPb05GQXRKZ3hwZnVRMC95VDdpU1J1RlFjK05PY1pKVnNxYlJiWENUbFJhNCt6bnUyN3JOUzI5R1E0RFl5VmE0Z2F2NWk0UThYdFUycyszREh6bVVWcmdZckhhSGlUVmJYYVY3dFFjNzhlMXBvU0ZSejdXN1phM3ZjbE5xZXFqbDJML1p1Sm1MOTJJZUtxb2ZtZHhYLzVmem5xZm5YRElKbFFIZEMrUkFOYnZhVjFlaVIwdkM1OHZBWVhzNEZIT1pFN1ppVXdZbVpDWmloWTdjWkgrdXMxbWlyMm1vUXk0dWdYSDRLNVdXU1I2ays5YnJFWStsZ2FNU05sdzJXSTFUSFViMS9WUnRnZWdBRjZGTWZ4T2JWeWJTOWFZM3hVcmNrV0JxUmRXU0w5aSt0cW45LzBOMmJHL1lNWTNvS29oaUx5YTJTcXFhbGNpVmdXYXEyd2NFOFdIc3dQOWlKSzFNaUtsaW43R3RqMGRTYlBHK2tMdVNQT1VkM0hObEU1Q1VWMjQvRHhPVDlyWUZXUEIxZnhnUEpvajdDbSttUTZ5dFNhRFZsZFlLL3hlQndsbVVJS3RBMEdCTHZLUkdkc2VwM2lncFNEbHlxS3Fpc2taaDJrY1BTME9nKzJFc2JqYU1XR3VTeFl1amtERGxUbXJnMDd4WGVuL3liaDFUY2N0YXhDdzNWUklqMXdxMkRlYng5MmJNYjZyODlzOTVNZ3hBYWJ1NllJT3lmNGtDeGlGbWN5c0JFMWNhY2hNZ1FsWnY5NDhsMnJwRU95Rk4xMTJpU09EWjhQNnpHb0hBbWdMYzN3SVA4ZVhUMDQ2bjdYbDIzUzRsK1NUb1N5U2lEdFRNb2xRWG1qVExlUHBobnNBMFZ6aDBZcjEwNCtMNHdIM2k5bVlQd2dvcmloak1PMFZnSnlRcHZmbFdGZmM2b0hWeGw3MTVVczJlNktPbWN6VUsvWjU1QW55TEw2RGU5NkNaeERpRE5kcnNDUUJIUU4xSWJJOWFReDMxbG1jVURQUHQ4L1JQUjVyNXFvUzdCNllwbS9XVmttU25vZ1BoWXJGSlRKaTA4bm1CTm40RVBJVWYzWlR0MWY0TnhoMDFEREJ2bkp2MHRXa1ZYNXBJdkRvTURlVTBzVExyL3MvMm1tNmYwVHJ4cDBQRWNKK0xUTHpxT0xreFlhcEdMdW9GY3d5N0JheGtGUzdYSUduNktRcDROcGswSnIzNGgzRG9Dc2VPajhuaDdiS2ErMExRQWlvbFp3RDNZbGFBRU9XRmtrcnZROW9yRm5KaXBmUm5TL2QxenllL05UZFRNS3lnK2xhTnNzTmpnTGZ3VENuMUpTeUNpeEtRbXFuVS9CNFErdVdseElUYzdPbHpMSGhqYWYvWEtWU2tqcG0yMDg1a0huK3dLOXp2QUdka3I4dkVvdkZCSmtzbFdGVkZXY0NVQWRMUmFLL2wwL2ZZT1g4Z2N2aXRKVkVRT0c0bi9DWEJtTmpqaVQvMlYvRk92QXZRcUtEVHU0RFJHT05BQTNKenNvTGdlMGJocDNDcEJZN0ZnWGpuTkl0ME10VitkVk96R2NVSjF6SU1sdDFQOHJnOVJaQW42ay9XanRDc1VOVDRIUkhvUXE3YUhqTXMwUzRxdTVXVjgvSU5aQ2tyaGdMdWtDSkxlS1pRajR6NHNwaDdJMlZHRVVvTXN3b1o5allkMERtY21LU05HOXMvcEl6N3NqamdSd2JjWVZVdHpNUGFNNmtOYUdiV1k5SjAwQis1eGxWdWQzVmtUTGtXRTdqbERPMHBqSiszYVBTbEdhaXF4ZlgrRXhBbkVOa0dpSW8xQ2lYeHJ1U0liR2FiZGpHTlo3QU9jWFcyVnNsbUFwZ2w0MnFrTHhzRzRPV203R0lkZjdrNlpEZVdibFdGdUl5TU15V3B3UHhzdUJqTDUyYmNybmthaGFrT2U3Q1cydXM3WGRHVzNOaUVzOVhYbm5yemhxem9LS0ZtK1RBTEFPbjBVK3o4bjBoWGN2M0JxVSt4RmduUW14Rk45eG5pbjBsR2RxNmpxRmhCZDRySEI0MWFnMCt6cEdhSGNnalcvOGhvZkZCS2U2SG9COFErbUVTK3drNEN6NEMrZUJDTnBjQ1JtdDlxOVlkeHFpVnZlUDIzWVo3RlZZMEtyMWNxTkhma3dLelJOYVg0UkdTL0JOb3loUVo0SzA2cllzbnJFZU5tQUlYQ1d0ZmFCT3loNnFLOERYY1NzQWxtcWt6aXpOUTFuNGJENGhyandkUDNkMi9MRjZWbHZXTTNJdHRjRmQxdkhVa1NMUzl0TkdQTkhqQzJaajNRMXpTRkloK1dJRndoM0tqVVR6eVBQVmZFK0t0cnVxdGhySmo2TkI2Q0FEcUcxdUY3dWtHQTgzS1kzODhoVFAwYmwycnZNT3dMQ2JSdXl0MlptY3Y3QW9CUVdvU0FpaWZXOEphREN4WURIZXZabCtUOGVYbDNOdkhNaUJqMURaaGZhZk11NDEzeHpMeTQ5Z3J0T01IdlRwbEQrclJXbCtrc3NWK1g0MjBxRGxqYzh2dlZ0ZlNrT1RCMEN0ME9aV0NBdHNzV0dBbWYyYy9IbXFQYmFVeUVmSGhiUk5zd3BRNTVsb2NCTmUzczRuVmcyZFdoVE9oOHZDZFJtMHF4NHcxTTlsSGZlc09WOEJmTkR1M1czUTUweUc3WGorYkFJQmdCdmNFcVVINDZ6cmE2Q3BNa3Y0K2kvSHdHMDVFNGhsNGhTK3NtdE5kdFJicHdFRFIzdTZlUG9oNUNFZUJGT1l1MnVnaFNJSjg0bjJkckl6K0FlUG5HTE1PWUJmMHhBRmpOc1g2UGZDbS85Ry9VWnBPd1d5MTkvRWdBUk5ib25YaVJaN2UvbnIzcnBFQjRZbVI2dTc2cFRJdGV0MTZvZTlmTmNnVWlpVWJYVnRwR2tJUUFKSE5DSnJqSGJxOEFPUERGZkJuLzJoNWpmaEU0VDhJVGhHZ0FPL29kaGxDQUZvK0dpVzhUNElzNlFmbEFmTGxpd3RHQXZLK3FEc05vV3ZuT2RnajVTSHRFdHhsTjNXb0VkZlNydWJ1ZW9Najc2UW9jVThKRHVTVE5ZWnU1Z2J6b1RHUTlpNVZyTzFYMHBxTE9HaG9vOExKSE11cG9KSGFZYnRYRWprZW5qZE1PbGN4OTBncUh3UEh5Qm1vWDR5bUhSZXBuZ0Z0NUk2L01xbEZKTmVYSzBYVS93Y1BwMXd6Q0FGQkNCVmJRZldSR25PZWhzSGF6SFFYSE5HK21jSUd6NmltOVI1eWdpMzBWQlRMYXg3UUxRYUZmOVhySzdnT2w2RFRkMkoxRUk5N2dxN0x1T1NyVzRySlJCM2V0cjQycGpldDMzaDk2RGNLZlMweWxwWVRmR3dZT2JidU0wMVZqZlhjbkxKZU8yT09VSEFUcERaN2c2TU5zeENnaXdPdjlRTHo0TnJTTzlhR0E4d2JkbTJVa1VvVUNlUWpVeWpqb3phbTI5RVpDQllteWNJcUNYZWJNcXNGcWhZZmhCSG9mMXdacjJPV0hCRDd6Q3daZ01URUluVVlOSURVTDlrQ0pvWUF2cTF3akhjR2dNVTlJVXFBTVdPdWxRRXlxc21SQW9oU0loREJtS1NKUm91NlFxY1Y3djBzNDBWRDNpR2J6eVZwL0VXdTRad3lpR2VWWlhxTjNqSzZodVFyN21ZRkE1S091OHdSZjR0UmRwYVhxMUxpQzJBc2ZWYUdtemt2Tlp6Z1cyWG1JMXV4V2RUTTRhMEh5ZFlJK2U5ck93RTd5ZFhlTDlRb3pralhtRllqQlJnNHdqTi9qZHVJWnBLL29HQkJkN3V1SDUydURqN2g0bTgyb2R5OGJyZWNodjRvWENaeFVZWHhObEo3dFg0cHR3RE5hVTNJQktiNWgxY01NS3EwUE1wY3JPc0gvbzNPY1p0L1RGMzY4YzhCMGlZYXgxS3NyQ0R5YXBMelRpcEszb01EbnlpOUFmT0ovYUx4TmJLblhKT0tBSHQxekh1aVh3V0VPTC9iYUd6b25QYWE3dnU1SkNBUkhxaUROLytGb25aUld1TE5KYUUrTkVHY3dyZHYvc2ZxZWZGcWFGQ3Q0L200cUFySTU1a0owZkh5R003SzFEN1RYRTJqdjdWMkgzL1pBK0RGRThDUlpZK1VPSW9RakN3N3poSnhsTzNMUE5LVERDTXM0YklVeFFSZlN5Q1pTbnEyMG5aUisyUWI3VWJpRXpnbkQvUjgzYW03bFphUzZnS0MvaXFlZlpnSnJ6WHl5OUtteEVNKzRMMFA4TDQ1K0lhaUp1OXRiaGFTOHBsNWdSTnp3NU9rYldoTmJaWDZwZlJLanp6UDB1Y1ljT3luUjh0L2EzY2wzTUh0a0NVUm00NmRXUHV0aVZDdHo2RXZHOHhZVFhVN0ozeUR4ZTIrT0xDNXd6ODErck5taEtwdDNLUEdnbTlNTVhNSnpmbVNwY01qS1h5dk5VdmlxelJOOFhEYlRnSXYwTHB0ajJQVC9YTXRiWVorTEZVTFdmR1VyL3pNcHFKSDYzQ05Ta1NQazY2NUpCd2xwOXR5ajlkOEpZS1hhaWY3NlBybmNJYVY5cENvaWhOUzBEbERRSVMxbllIVDJtQzJ2eFNNZjgwemVEOGYvbmRlczF4QURIRUNzZmlqOGtZT1NvK3A4ZFFjOHhzNDBTTXo2M1Zza00xakVkYkdjczBSbFlzbVRWeHFSUmtuR3JzVTVxZVdYNnF4ZUVqY3cxcG1LMk1lRmRrTzY3ZHZxK25samRQbysxMTczVnUxZy9QcnRvQm5wMTVFK3k1L1VlNmxpMGxSL0FYZGp1a3FxakRod0w4eUl6SVYxWkRSRnF0UzEzNWM5SnB6SWFQRE82aEJoQk16UnowaG9JMVhabjBrVUk5eUhpOWlQNG1jWkU0elZ6OE8vdWdJcVN6cVBKQWRTQWI4LzVwV0dDUHliNGY0S1dxYU9tMjBTS2w1bmFZWUNPMUR4VW0vS0Fyak1oZGk2cUVYZnYyTWJxTUE1Y1dNdHpRR2FnYnNZTmhwSEFkVHY3TUZEK0dPQnZIa3pnZHpOOVUwek56bVpUVU83TEs3MEJnc0NNQmNuZ0FKU09vWnFUaFk1STk3c0FwNXN5UUVsTHhSMWFqOXVVbXNjTU5qaVJJUnp3Y1dtdE9UREdBbGJaMy9oSERBOEVZMEdheUo5RFQ1SU1sL2ltam9oK0gwS2Z4L25YMVFOQ3E2aTNicnFVYmVyUENteE9CNkFidTQycnZQMmszOEpZWTlZUFpYSnV3MXp2Q0NIQ0V3TkZ0eUVKeTl6SklNSlVHVWNvV1RrcndSVFo2Q0VpRWJ6MlQvMjMyYTBrK2hoSkc5akhoa2RLWnVDanNrdkM1cGxTU0RuMVlHWFRjSkhneExkWHcyZDZOY1BuVlVZV3EvMm9pMGNYNCszWnVUZFZObmZybGw4a2JtRVlpTndqK3pGRENUVWNuVFBiZU9PbUx5ZWxsMDFFdktnSy96a3R3MWdXSGJRUlNoNm1TTVR2L0VMcHRaYTNwL3FmdE15c1BEdC9tak1kRkR2YTI3M050dnlNZit0NjNxM2I3VUx3ZzAxL0pobVV5VlhtQVJWU0w4bDFBUlpjTFlpSHFueWJXTzVNaWc1WnBkaitRTFRzbHMxdmdrY0tzLzB3OS9uQ3lSaUE1eGVqOFJPM3F4Tm5FVktkMDV2NUUwZEgrZ09GSjlQcG0wU0MybjJSZ1hESnhacWZBR3Z2WUZOOVVCbVJBTVZXbUFXOWNZeXh5ak15L2FCUHdqUEo5ODBYV1NaTTlTdU5HR2QweFduWFhHTXg0UEZhZXBrbTFmNEVXSG1iZEZJWmhMSzFwMklFeXYzTmhmV0oybWQxc3NTcWVNT2JjVjN1d0NkQ01qcjBVek0zMnJRRDJzU0xNZGNGeVJnV0FLbkN5MGdQNEpDbEZ1Y1FYZy9KbWlUL0s0UlBiQkFXMzlIVlhvYVVnQ3hFZjVtVk1zNzFoeWJQQzJDMTNoR2VwbktJMjllN2lvTWZCaGFMSlhKRVk5RE83MXVpT2tpWXpEN1ZIdk51aXo1SC9ORFBhZENCR2ZtUW5PMi9FU0dsVXFONXQ3VDBrZ3hCK3pDRm9McWVpT05yc3NKZHZ2Mnd4Q2VJNEw3M1ltMkkxVVV3WERCTXpidTZCV2ZCbkxtU2hQazhmR1hSV2N1dm5KdGdTcVVFMnpHTU5DMU9KL08wUlhrYnJRNElzQnI3b3IrQ2xUUU9oZm81K3B2Rkg1TStKa0oyaGxrbzMvSzF4TWVjUEthaTZSRHhKQWFaYlpyeUY1S0QvZE1GVzlPWm8rSmhKamVmYTIxc1FGUGpBb2FTWnh5WmRWWlB4QWxRUmpPRlJ0OEdUdjhEQjQ3WnJuYXpEbTJ5UjVaTVRrSWtkSXc0ZU5zYzg3YTBHVldnVVl2QnEyNG5uMTVLeTJVNlFrb3dxYlBqMVdVMXg5YlZQNDc0WkhqeXFHM3BkSUJTUGVselhtUUtRTVc4TSsyak5aZ1lrS1MycG9oa2tIbm5qVVRmUW56YmMwSlFHbFZCY1htRW8zTmo5bGlaTml1NzNVbTJXeE8zMjBBSnViL1ErY1hiV1RKekdhM3N4b0szeU9HYmM5aElnMHduMlBpOUdrVVlXWHY5aG4rR3A1Tlp0QTVrZHFDTXF2NDF4TzNFZFR4S2tlZ2MxejBNYjFFenlvNGJ2L3cvMnQvc3FwTEZOYVVUQVBQdURYa2JWYVNBUEhoY0JGeVJwSWw1NzdHREhsT2dMNEx2bXkrdzFKdVBVSmtHdUVscHE5dlVoR2k3VjNxVktpTVNuSzVQUWdYQkVySGUwR01NelNoWmtPWFpWNWZLVzZSMWR0cHVkdzFneWhQN0Y5NytKUEdyY0htbmRtVlo0R1NXSGdPUEV6WFplQjU0NW1MbStjUk81MlNSeHhIWGJJTDA4VXFoSGk3NjErbXZ6VTQrNzJWYzAvcDVCNHVnZ2hIaklmQ0tpTWlBVFdPdWZMTmV4Tjg0U2M2UDR0K2RFdmh5eTVoZGJaU0lTL0wza0V4ZUZadE9KMmtnVVk3aUZWMjdFa0diTi91MlRieHZCaUlZQjVwRnpuYyttcitPNFZKdkE3QjAwbjVib0tNMDArbUhYNDRLRlBJd1kwalFEUUlUYjhqQ082d1pDS0NOWkdPazBCSVVCbTNhM2hCdHFXS3VRS2J4UHdTbGZORTBtajhiOXo3UU9kYUw1ei83c296ZnVhT3hWN0xkRDZJL3RXbzkwemVzYnBBWkw2Q2ovQ01FSlUvSWFLb2wyN2FJNG5sSGtjbE9SYjZCTFNrV0psMkgwamJvU1dDd0h6VXBmOGtzcFBzZ3A3VFdRY2RVY1JIUXdsOFdZYlpQRzVEUnp2SVJEWHMzbklmdGZzRjBFcWt2K3dtOVhQWFd5ZzFON1ZrYjdiV2VXN1VUKzFFOEJ5UkxzMmlzRG9zeXAyUnEvRmxYaTdsaHZPRUdCenZpM1dSM3ArbVdhUlpONlJIUVFaZDVnQzc0bEx6MEtRSjRIeG1FL3JSV2MweWZvWDkrTVlxbXZYMGEwRGdFWDdacEgybUprbnkwa3k5OTF3K2VXQmlHdXhEUnEzTWFlVnhHeE0wKzRVZCszVkRYclJGZnVOVEIrdCtIVWxlNXdGWGxleUE4MUwwZW1ONzVMNjZvcEVCNlNqMkJBTXY5aEhEMGFaYXVLazJPbHcwR0YxbmRZbHJxZ2JEZmpMSWovRXNOWTFmaW5tb3E5aHI2ZGt2cGJXZGJXY1hLU0dwQS9ZVUtDeEFPWnUxaDhnWmpwQ1VJcWJEbW9iWElUM1lGcFo0enNncmtDOWtHT0hZNEVwR2dRZTBpa29IdE5XRGV5ekNIWlBWNFhsYUFCTHdEZFE0N0tscXVHYmlZUm1lMUM4Z3dvRFp2dGZnYjlINUtrQVVLR0JxZFJmSHBvVVBkbCtERWpWQTVzek51dHFlaE1qZS8zemtEM21GRGZvYTRMallPbXpGNmdzd3RlQ1JUamVOdWdKbWhiOTJ3NUJuSkZMTld6MXpSTlN0ZzhwRlNWakdHNFN4dWJOcDAwdDNXZU5kSnNUUzlpd3hrSUwyN2U1OTRWT3RwaGxTREFnS3ZHVXovSVpiRXUzcFh6NWdkeGRsNHdUWjAvQXFiVnpRbXMyeXp4bGJZNXZQdDhZOWpMa0kyaTZRNlUvMHhPc1cwKzg4b2hObmlpZXkwZ0YxTG9LSmdBM2lLUTFzS04xR3pubkJIN2FTSXp6UForbGJRaldsdUxtb0lZVndwYUcwSmVBMGc2SUV3V1VVanpaTnlRemFmWDhpZ2dPUnoxS1RGUk9LVzdtZ2hNd3Y5WVlpcldHYS8wb0hiVmFyQVloeWk3UVI4WTdTaWlIRHAwa2F5NU5LSXM2SC92aG10MU9wV252VS9kbjUyWC9ucUEyWUxmZlVmOTBHa1U2ZHRjK0VyZlVNMWtCdXVlZWRLOTBrYlF0QmpyWjZoOVVYRlpsSCtCUXIrUWtGV244QlptRnhmZzJFL1QwSE1SQi9idDdqSXl5bEJRQ1lsYVo5aXVkazV5d2tqd2JZT040WGxsNUc4RXhobU9uL0NvbHg1L2JuOURhR3lvWXpnOUd6Tmw0V243OGRWY3VDYnAyYjlMeWZFb2k4QUZTSFJCMGY5YzNQcVhmWjNSTUsvQW9lWUxGK1JTcFVCMC9DOXlCVG1rMFlBdHVRZHFmZEFZZmdIKzg0RXhWMUZLWEZONHRLRWhjTmZjSndUaWpIN29lMVVwME9vdE5QUXFTWmttZ2YwQVR3SUg3QlVnUGpWS05JbTFsdG92SEZsRVB2TVEzMnNMUnp5QkEwOVAyZTE2QXR2UmxTVGdKaDFiVUZhMmVITXlKeEEzSjFFNlo2eENrTW1rQVVYTVFRQUo4MzNxbTlNZVFKa2xUYUt5RGpBY0hUemRxcThEZWQrbGlsL2ZRYUR0N1pQMStUV0RPSnd6SDNQV2FWY3pSUXZWczBQYTJTYnhnSXoxOG51ZXc5WndDd3JQZjVZWm5QY2ExaGV0UFEvRVVYa0JFZDAwc3lPdHp2UGszMERoODNmYmRnZTMxeVVQNkpVbENQRXhEWkhTMmF1MG81U1MwRFVNT1pSNGxVa0daUVpFSjFvNTZ3WXErY2ZoWDE4ZnRvZzU5UGtad21ZNFdCWThOSTd1TmhOekl6VWIzbGtwTnRKdjVtTndOU0NBK3UyMFJNRExob3gwNk50WGt1UHZPeUFvTGZFTTdocGhyYldGSTlVemQ3ZVVIVThObEtzd21PSWxXZEdObS9HUFZjS3BzV0Nkek4rdU9Lem5SRmFmVW9UOGlDU040MnJHT0xHTDJrRWpDS1F3ZmRtNmhrVWIySzFHajFlQ2VzNmpxWjYyd1lUenkzVFlIQU1TV0REQ3BrYlNuTVdkdFhYVkpwTWx6ZVptTkhVZXhJU0JXMkVCWDdkUy9JdE5CT2N4S2ZJWGVZQlduTEh1aTduMXVnZS85VkxqdllsS09HSjkxWHJPUng1aUw1UFJza05USTBqV28zTXptdDA5V2ZBZU9wT0ZoZTREdjdNckJ3NG8ySDlWRWsyeXFGMStOc0NjbUtCL3kycEVoWndLc3cyMjZHamR2b1FBdnRPVEo2QVFHWkpVSnJkdmdRTVNzRDdpRi9mUDQ2Z2g2NWpQNGw2VzBGaUZNWkVaK0FkQmNXeExtYkpRMnlUelZtRXlwWnlKQWhGWi9VbDUva1c5ZFRBV08rYTJvUDVMRGNjSURMRndmZ3RQa0h3T0xLWVdVNHFpNStyRjIyUHAraklKZTlLOUdRQ205eEhLd1VLRU1QbHJqWU1KTTFhc0xOSkFkbHBTMm11eUR0RlpZcTRIZW9raExHTGFqK2xveFhqajdDajI3SE91TzRvY2VFZmp5b0tpeG1mRzZtSytLLzErOUxJeFhaK01Mdk9iZ1J6elJ6MjZvR3VSNWtGaGJEelVSM1FseDBUdXVJVnUrbWlYeDV2U05CRG9Pa1ZLdXhlNkNVcjFtODlGTTQvS1dPL0RqRUp0MlcwYmRGdE4wOHhWVEYxdDZtQkpFdjhxMFYyM3g3cktpck9rdkNnRFcrRnV1Tlloa1lFVkE4QnNTVFBWNjc0eEhwek5FdnJ5VWV6R2V5OTh6N0VrUGFvZkM5c20rZ3ZxL0pDaldiRVZGaWpIOGpOdkhsVlRkY2xHaW5wYjBpYUJOZ05FajBTY2dmOUxCeUNPc2ZhQkI2TGxPbldKMkxMeWRYd2J0UzNpaFlZMXg0ek80dDZNWnhSWHovUytyVUdQZmE0SFo5cHZPUGRXcGwvVVRGT2VFeGFlVE9lcUg5SitUQjFxWWdlblNPUmJweEFrQS9wN2dDS2QwNi9tT1BuYkZOUjZpQWhoWTVESUY4WVViREpZZHVoSjZlS1ZzYitFMWx1UVlLdHg3ejZEZUFPNCt5WCtkYnZjS3FpOEFZNlF6Snl4N25pbWduNCtDZnlIUVMrYlcyemNRS1ZnYkRwTDJhNzdOdkdyUldXQzJ5Rzc4bkN2N2R4OHl4alRRUWJqbVp2bXg5aisyZ1YyU2dRNWQ0MzhtSUZoOHljaXhWODFiOXZ3N1JVN3F4UUNFUjRZMkNBbWhZUHpRZGs2V1ducGtyUWRwSDZPcHVhYUhnUCtOL1dMdk5vOXpCYzZVWHNuWEJUbWJ6eEMrcW5XVStDdkJNNVVOdndnck44NTgvSkttejV2VUdnLzE3bE8zSVJ5d01lMEJ2UEdoMHJoTG9PSnBqZzR2ZVFPdmFkYXZ1Zk5QT1RROUdyRURZdHhDeXViSnljQ0dTbWVDZHludG9BSDQyZVZIZVhmYjR5a2VsWDBGSzYxTDNFTWRFMDM0RHo2cDB4cnlPSE5aazNWYTk5WkdwV2hLRXJ2UEhUYkpiNmdGTHJCRlBmNkdRUVY2bTQ1MVdpMEg1ejVaWXVtSGZtQ3hLQ0NvVjNVb2M3VExuanZBeDkwcUx2TE1UWFBIYisxYUhBMHZsMGxBaTRJR2hlT3ZMcjFTZllCSjIvZXlPTHB4V1RmNUlWY3NIOEpTb1I4RjF2eDg1bjc5Q3NXTkhoVnUySXMxeno0cmpLZHJvN0hObDZaVlpHK1prbGRSOS9lR1JQb0EwV2Jic3ZCdnA5bXdtRHZ1VmRBcWdNamdFbG1NWmg1QnNLV3hKdUVoUW52Qnk5Z0I5QjE5Y3RVc3FCbFhBMi9BcnB6T29YT1JQZ0FGZWFCWU1hc2lXazlLNnRKZ2RPdDJ4SkxyTmpNT0kybTNsL2UwbThnbXRSdFRvN1NORmI3RTRQVWFiWXhmR0trWTFiOXhkYmo0T0srT29TYnBzKzc0QjE0M2pnK3I5ektnZnBCRVI5di9tM2dTWVNwU0FTYm5iMGRubGNUSHN1QTJZZzV3K00wWE5MU2syZHdZUERZOEdSMjVzZmRQcEgzb0RPZ2F2SmxEQkZJYTZHWEFJWEs0VnVtanE2L3U2bHR6ZU4rYUtRa3hNVk9wWHRkclgvdWtBTkNLS2hWbG5PdFpsTjB1b1ZnMXhiMlU0QUZkaXEvU3o1MFVjY1BORFUwc1grREZEQ0RmN1Zpd2d2TnVub0k3T3AzQjdHaGtsbjZORFBSNE9zV1ovYWZXRTh0REJjTUVHeFdWVGQxQUM3SWJWY2c3eWdSWGtCcEVTclRybjZERVBlRmNjRU1nTHJTQ2NET1hHaGs5aEVGUE9kMlJBS3BJem1sb05VWkc5UW96b0hxTHZBdFQ1Y2ZIRzZTUWtoK1J1MElyU0V0R3ZsQVJ5WERTVlZaZURzam5yS1FXY3VSNnQ5V1VDU3JXeFdFN3NGU3ZXNk9JcUlEMXBUTk5wT25jcWIyOXd6YWhhVm1lOGlTM0toL3l3M2tSS3V3YW5aYzJlem1ST0JYcXltdDFZQnpBWnpMTnJrRDZJeDhlajBhc0I3Z2VQaE9wTDBEajUrUm1jWGJMT1grMzl4OHNpb1NmMXN0MG9pNFNnMlUwZDE1Z1c5c09ncS94R3hXb1hMbWR5c2FURm1kQU9vZ0dueXZscXJvckhMeTIvTndSc3ozUSsxeFZTa2tiUVpleTU4dWszK3JZK1RkL0pKZUdtaG1aMlZzTk1YZzFpZWdlYmg4NnY4ZXJOMm5GYmNLQUliTFN5cnpDcWNIOEdiRXY1elkxT1RmZkVmTTQ3Q2RTTWRiSkQzMGRwWEFaRThLZzY2cFhQd3ExdzlHWjZLWFE3WmN3VW5wbWFtNk9ONzlxN3ZvZmpXaEtyczZUZE5iWFBBdmNmV3hoRVJvWHpwNDNDeVJBeEJnZ0ZaMXgzdENjby9YUFFFV0ltZy9qZ3NoM1IyQlB3eEFZQ3drYytCcTQyVDB6cXdISk9pSFdWbndRSjZZYzE0SXdpdWlsUWhZcnZwVHFkNllGa3k4SFhkVHI4cHozRE90ZWE4a25xY1pEYzN5QzBBWkxlZDZ3RDUyMGd5bVpTaUMzS3c0YWdBY3owRFRTMjA4ZzRjSm9SUTZ5TTBBczhmdTVma20xRERVSWFqL0NSZjNBT2QveWJiZTNacnZKTUh4ek5IcVlQMHkwOW84bERXRWl5TkZsU2luMXVGckdKSk5ZVHNaaHNNNFg3Z1orMTZQbFZwMlRjQUZvRENBVXZYb0VTQ2IvRGVQdG9kb3AyY1Z3M3dyai9RMFo0ekhKL3BoN3RQVjBKNFU3UEsrRkd5Q2k0NFJySlQ0QUxKemduTW4vTDBwN0ErS2NRYTZBc1hKM0VFTkhvR0txVVY0cjA3dGVwc09xOUMrczhZVEgrbCtUSGtpekpNZ2gyaTVUV1gwMTlpYitCTU1YdDhPM0drYlAzV25nM3ZFRnBmREsraE9nRFRQZkxOVnJHRmx1dGlQQWJTM0w4NnBjenRXbElHdktySEwzK21jNlY3U3RCc1RjM2ZvaVQwdDhrQ0Eya2kxQXRxL1Z4bFhudzlGWTR5S3g4L3RMMUFvaEs2bUYwOC9hNVdnbUlONkZMRWRrQzJ2ejJaMVlqNERJY2x1TURIUmxoL2VXb1ozSHNwSmEvOXk5cWEzM1BJWW1sVXBQYm5sM2RsdC9BTlp2WjE2OURUZjFUUWFFem9BTCsyS1lRaUVnWWxlL0ZVRG03UXNPYjdoRGFjRWh2YUdFRFpQdWZzOVF6MUFFeUZNY0hwaTVQdFRxaXRaZUN1UzdNTmZsOWRJbmFLOGE2QmROS3R3aG9RMDlHM2tUK2pnSTJlNi90UnBUOU1tYXZKMTZXRDd6dHVVYm9nR0RGblRXblUvVWVmb0k3K2cvWGludlYya0RDaDBWUS9CbXZsQmE3VHkyZmNHaWZIVkhXU1ZCTzEyYUVrUUw2MUJwZEgweEprL1FJcjVqb3RiT0k4QnJ5VXduS1Y3U3NqTVBWcTVTZThldWJQZlJKalZOTXBPbm40MDVLMUxpVWdCODlNeERaNmFWSXkyMjBVa2RIb211L045QjFWUS9lLzdBekF6N2VHQld6cW9KbjlNUC81R3BueUY0UDhGOVgvNENBaVlIVGg3Rm9HYm80dDlmckFLMnU1THZkd0dXRk1EN1hRV3Z5QjBTVDZqQm9seWNUVUhPZG1lQzM0ZkRaZVJMUnh0YWIrdEJhUWloY24zUDhiSVd4d3NGVlM1WDBhZ0pHVWN3RlU5clBnMDZEV1QzemFUQzhMbDJrQWU3Z3k3eVB6ZmlCcEtCNE9URUk1cURVemhrV295SGtnbG9ZbnF1YSszUmwwaTVtYmZlQUovUDUvdHlHbTBsa1FySnZZcWpiUTl0TjBFbWxzdjBQejc1b2ZRMER2WXdTalc0VFRFWElFNm9hYjFlazhXYmV0WGdwMm9PamlSQk4wdm1hWGt0US9TVm9oc2NxVVVXNUFoRDBrc0xZYjlUR3JLTE84ZzNyUmNIbDFNQU1kcmdHN2dJYURCYUJCdGpUUGxyYlNEc1ZhcXN1N29SdTdldWRyYXhyRTdyUDVRU0RRYTE5aXAvSUk1K1J2dkdSSlBsWnB2azRlM0dPRnJXdDhRT3pxSzNvSjFUUDlSVWptaFludm1WNjJIODZFSTg0TU9QRXRNKzErZVV1WnFIRjF4UWd1d0FFeFdRZDBjOWM1RnJqWWJIcXZWV2JTaHNBRXpsSS9OM3BHYmFLZ2M4QVN1aHZFL3hTdlEvcmdPbUVGZHM2RWVZeWVnY1hNd2JaQ0YrellIRDNwNTB0WHp4cFFrQkpjQm5CcFR2N3N1T3dxWVNsSkRuQ3A1OFNRdGlNdVdiSUdFZFpXaU1aNHU4N2xUSXJrYTdMYm5Ib0xKT3F4VkI2SU01WjcxMVMzVmNoUndBUmp0MU05aG1KMlZJSlY3NDErRk1PY1NhSTFyUmlUMDRBTndCQ0NacEs0V0MwaGMzWTRweDh2ZzVKQ2wvWnFsU0g5VEdtR2xMR2haWmV2elpEZXJvcFhvM1REbkcvalRlRVRwVVlCMzBVWi9lejFUS01jTnkrK1F1OEZTK3NrVm9GcnFaYjhad0RGc1pqVGxNV1N3bXJMODBxbHpkK3ZnMlhITm8wTUVBS2czOTg2Q3Y1dVp4b2JzeTIvZWprMFp2RElEWVMzOS9vK2xFRUNhejNWNHpaeUdKT1A1YWl5VkVlMTNCS3ZlMWN1dW1rU2ZuV29VUzdnY3FMdVdtd0tGSzcxTG1IcGxxcyt2M3FrMjBKZGNSRXZNa0ZuaGZJU1F4SjIzYWYxak5FeTBjMjZLSUFZQXZWWUtvT0lpbmFMUnJqZkw5MURrV28xcWUwMEkyam9TWklvclBwbVgweDVJWlY5Qm1nNkFvV0orUTZWMEdHRFA2OVYxRnpZTzhaZzRUU3NYa3NMeW8raURtVnAxRk1PY3VlazNyQ0daNGY5NG40Qm1maUd1V2pnSVRIQmhZNTgwMmk4WlFrcHErS3U0NGpNOFduWWlLWmJlZmpGMjdaS3JLbE9PUFJHK1lTU1hTRjlFQ2N5cnFRS2dFYnlGNVQyeVFJczdNVUhiMG9lbGxGQ1dXanc5RmNUME50SElGbjRzY1c4cGNQOCtGVkhPcnhsby9FU1dUaDN5NSthbGphQlNyeHpBRWVUSFJBeko3bWUxYjhDbzMxSnZMTExmODJDRUJIdEwvcTM1SWQ3WlVsY1ZhU21KMXRrTVlJdnp0QjFhelpwd3RpcVlwcGhTMXRENlVobWtFS05vVXhXYVJMUGtXQjBKd2hIaVZ5MmphZmRaMEdCUVBWODY2eXpYZE1UdFd2c2srNWhJNHF6RkhjVWVJWU5JUmNMMUVtSVJQMWxFQmFuZ1l2b3VjVk5aOTNqUnM1TlVpcTNZV3hwaFdiU0JCQmU4cGpTUldpRkZLd3N0WXpFWVFxSlZpYUFGdUZKQXljaHJORE1lcnIxWG43Mzl2QWRsVzJHRmg0YklLRG1DQkFGU1B6YUFZSnhhNjRCbE54QUN3NllvbVFqZGFFSytOMHFyNW0zeFd1QUZYYUlQeGxZMHlrT2UwWW11cXdkMytBeHBXb1dtWHF3V3djNHBzYU9SVFN0YVlsdmpxS2l6YU9LQTYrSUswYi9OdWFDLzhNa0NkVHlEL3RjNlVwcWVubGxvUHZ6cyt5TTk3ek5NZEFXSFVVYVJjNUROSFhQWEVqSDdITUhWQXNtRGpQc0ErNDkrRWswa3VwS2o1TVB2b1I2TXpqMGd4WnNYOTdsWjlPazFtaEoxYW85KzNjUitycmtuL2R5cnpnWG5qUE1QVTh0Y3h0ZDFtUkJmSzFxenZJOG5uVkdyZVh6NjVObVRDQ1MwRDhkcHdrOEhwWC9UT2Y1Ym12eEMveC9UNVBiai9SS2xVYnpZMFQ2RnU0YTFWTFNJbXlZVTNKK2dDNWVvY1pKV08vZEJRTkJRRU41VHVpcDViazhidW9XbkN6WG1oQUYxc3lJMXo1d3hYd2dmQ1Baa3U0KzFoeThyZFNta0l4aTJIWXVnUWQyS3lRaXF6R09DRlFVRVhWNmN6L09mMTNDNDNJRzNLcDQzMjlPREx3OWJPZkVqbGRUeVNuRlkrU1dBSWxyUml0WXoxMVkvTzV2Zm0zRXFaNHBtekZYaU1mVzRoRUJzRksvejFidDNLY3RmV3ZMY21TKytTeUs4VThlVGZ5N1dPL0p0NVhXWUV6RDE5SDQ4cmVub091SnIyNndjcGwrMHNXbncwUlA0MW9tMDdBM0NhZ1pRSUlnN1NQSklBWkFIS1MwSjJSVmpnbm5MQ2NNQ3ZrSmtLa21RTDBvNFdaZDhIcG1LaTVmVFdHam5tb0xOdnVnREZaeFpGWEZWTUtuaDcxVHFFNHNGS1JBZXFaNWoxcm5EYlNHMWV0dkIxRUVHMDVsT2FqVVJZeTgzVmJVQW1WelM5ZjVmYzVUTUJWQnJrZ2hNc2ladXR6cVZTbzNrdDQ5TkE4cE82ckpZVTVpYm02aitFVVdBU1N4YUtGV0I0bmJYRDJiR0pzdE9ZYjJTUTNZR25ZZ3dVNTkrOEdhcUx2a0MxcVBMWjNaY2MvQWlCbXdubldKTTNqWW1PQ1RhaEdJWmZvbmt3VmlJNit1OUdsQkVGYXc0U29NeWJwekdOL25MNDgrenRoMXNXbWhHektyWGROeC9FdEFVMFIwd1N3eDVBYjV4ZUdQdDd2YXFnR01FbGc0VTIvaUc2SzQwRnlLdjlQZC82NmloMS9sekhqZ0dKbUtZWVYvcGRwcHBpN3d0NzVHSi90RnY3VXMvMEUxR1pZcHJtVFQ0dkMxMU42bnBzYWtJOHF4OUFmV1dMSmJ5TkRiUjlYb3JHZDh2Ynd6TnFia1FFVHRuTjBaamtzNGs4bTAveFNhK25zKzA4NHE2V092T1V6M0ttc0ttRjh5TUdneHFxZCtoVDVyMndldEhDZm9QSGZtNTFtd1hEV0JManBkdGNMNnJiRzN1WnJxSzBxenpCUEV2cnlyZU00TFdiREtpUzluTXpxdTRpVVNrWUpkcy94dm01TlFJaTBCN1Q3NVZzT0hpM09RWVpqMURkM3NIOS9NdDh0a0o3VWZiUUtDVEtWR3lZYjBjK0VldEkyYTFDSVFxWFlGbDVpWXp0cUJKVXZYcEk0YWNBV3BwemVNbjlzWk83VUxIZHB1NXBnQ1BOelB0aTR3OHcrNnB4MlVmZkZCSGFpY29MS2NjeHFvMXFuT2hUbVdkamw2YTRJSHpFakxzVmNuenk5ZDFiWFRabTVGUjNPL3BISkhxc2xpR2FKVUw3Q2lraWtjajVOdmRjZXpiL3ptWGpqMFE1WUxWTmI1Yk4rNnRWUHRwS3llUGVEYWt3a1J3cXU1dkU3RGFUOTZCb25UK2svQmsrM21vQVlCWUhCRnRzYU5sbmYzQXEvVHdDRjFhaU1YaVJMcG1PVC9JajAwVzNoQ0ZBbnBPWmh1cUN0enNEc2ZIOXpYU1B2MTNEWExjQ25QUnNjZjR3VjR1RTFUaENGOExHKzl1NGFRMmdTSG9ZSEhYdVg5eklCdThsV05aS0hiY1R2MVFoTWhtZFFkS090V0twL05oUzRVTWFRSHovUm1aSDhkN000a0VmM0VMeStRUGUvOVFMOFdKMnNGNWdIZTZRQmtmWUc0Mkl5WTUrazlUdmZ5N2ZPemNsMkZWZmQrS244MFlFZ0M1QTN3S1lQai9QbXllektBa3NYV2dyZTE3cndSbnNuU1dvbGRWTFFaVVRpYlBOMG95K2U4MXBmTlJSa2FUbHVJVE1ZV3g2bi9rcjFzeEh1dTRFWTNqNXhvcDQ5ZjlYVVlaWWFCbnJmOVQ0NmQyY0lRM3QyZmxsZEJmY3lnRUljdUFIVkdxTlJOQmFwZlEwOVpBcll4VlFhbktRQzBiM3hCSmJ6dkczdVZaUlU4c29YR1lxRXh2MU84Vis0ZS9zYSt6WitkVjltbUtDbDZwZmZhTDIyQWh3WGtPV0pzY0h4WnZKVzJyTDVQSzh3MUJBVUNoK21oMlovTGZvTTdrL3Q3M2haRWtIYW00ampOaW13Zkl2WHVRRGczZUVPczNyaDVRd3p1eW1MRnRUdFE1ZEFabklMTE9VamVwRytnbkhRTngrMCt4dnRsbjFrVi9UK01SdGxSUStKNVR2RktzUFYrZ1YxSkhieXJJdm5zR2g1Tlg4Si9YaFNEVHNFWGs4WWVpS3drU1dKRkFmdnFwUmkzQ1pUaHZLSkt4b2h4NmdkNE1yRUd3bUxSN2taUzJpRXdXR29VYVljQXMyWjFMOXJRbW9kMUI2bGgxNmFJSTdGZC9SUFFCNEx1R2xkT0p5WGlSdkJnRFFMbHdVY0laREtnOXVwNkJmMHdGczA5ZXZ6b0o5TFdvUTZkOXQzNUJHeDQ4alFYL1kwYktjMnlEUGFya3dMVzFUcWJnYlVWRmRtQzdjeldWd1JldXZjUXdzVlh2Vmw1VkhwZ1NLUzZISDBEMjBQczI0dFhnQUVYNmxhL2ptL3NDbW5OZ20yRmhSb2NmVy8yclAwY3FNSnVoc3EvS0pmRGFGSVVzdkdyNjd3VFAxK0hBeUhENU1wdjZkYnozMUg1dzhVUHRSRDJ4b0VnM2RVRVIrcmYvWVZ3aU14c09WNkJNUS94NXBycWhBb3pzeUpDYW43THpqNFZ0RTdIelI0SVE1Rk1SYWpsVFpaVXh1MlVNY0RUcE9VdUJUUUZoMDVhY3owekNIbldHRWZDb2g2QkJadEFFN0cyUU85ck9CTkluRG9GL1RIU21qUXhLcnZZQTdsM2llWHNIUERlYkhWWGxLTEYvcmJ6VndqNEp3aCttN3hBck1BS3RlQVRUVTBKT2ZDaEl6Q041aXpnU2RGWXc3OE5uYVROeGlCTFp4L3ZoZy9jaGpTMkk1aFJ1dE80WEV2WThHeG9FOVVtV2VqTzRDZUZjTzBPRjJsTG1FQ2QxMmNCL0hCbzkrYmVrbk93RC9Db0ZrSlZIamNvN2FFQkxvWDV3Z3cvOUZ0d1hEeFJzc2lhdHpLOEJRVXk2RGNkcGZQRmFuRUtJVGdxUWFvVWdtLzIzNEwyVVQzMVZpb3ZqVFJuK3hZMHp2Z3JJR3pvcFJhdnhILzkxRU5CY1FvK3Z4dDlZRjY3TlF4bzJuTUVmUmlWRGZmUmdxMW9yZXQwWkxTY1FuNmExVldPYTZBeEI1MHRsTHB0KzRDSHZHQnI2cTBhckVaZ3A5NzlHK2t2cG9EUDg0R2MxZXdtbitCUlg1UGtjbGp6ZzRRWWY2dHBYYms2Qkw4NThtUk1xSkJZWHlXU0FyYkpvN3dycTBlOFZOWGZFL2hHUE81aHdCa3BYbUNRaEpBNUUvTnpBMS9aZWRoT0luOGt0TGJkMHQxT0diUGQ3UDRzN3J0cEo5dlpSNS9ORlk5S3ZZeXVNNm1SWm9QeUt6OTFiN0hpTjR3ZXJPdFBlL21pdE5lYWhwQWhYcVFuWk1zREZwc0pFNFpYVGU5RGpRalo3bHhJbm4xdUs4L21RZmRMaHBlNVNTNjU4dkpucE84NFJINnlSOXA4aFA0M2VKMDJ4OUlvUmpTZ21jRUVDMjlyTnJzejF0VitWbEhsK0lySkZwdjRrbXlwTTNoWEpXbEF6ZlpRb1lBRjd2NTBKbjg3bE03eVViTkg5ajdHcS9XZlNnSFBPekJvazRzaFhkK21OdExIYWdrUE5YQ2hlRVJSVmJ6Wjdad2twL1hqTzE0TllSQm5SQ2tWaGpmUlM1RXpkOWUzWVJEMFYvRU9QdU1BZmJDMEgwRlh2V3BZMlJ1d1Nqb08yTzg5Snh5bWdwMjdRUk1KakxLck5EeG10NHV1VXJkVFYyc1llUzhja0IwM3ZobllwOSsvYUxNOGVoaE45TFpHVUszQlV3Y2ZnbE41Q0ZkcUxYWktLYnBwelJhZ0tNWHlSbUtCRk54U3JBZzVBNUtoNU91M2Y0N2tCVkRCWkRndEdBOGk5dUpGTkpQWkxXZXJwbHp2T1JTSW5VTXJScW5wRHg4NDFiZjlsa092YzBxWUx6eWQ0cWIrc1IvcHBkSGs0TGJGRkI5V2lhaE4wYkpmVkFtWDJzR283UW1UNXNIYnFnWnNBTkVxYjdPVmtUb2I0S0k2UVUzcUNUVG1zUEU1UE9mOWRvMW1zcWtjSjZyZkgwdVBSVDVwTGVVcWVkamwxZ21OY3lwam5CRldJcHNZK3VjaXVaZzZObTE1c1hhVXVFTFVxUktEcmxyeHJIZW9tZCtPOTRGbldIVGZBL21lSVE5Z2YzSVVSZXpsVFR5ekY3L3Q3a3dmOHg2RmFKanVFNHlkVEZXa3o5MXpBSExzc09CRTFaNWIvVUxqc2d1RFlKNC85YW9yUFQxUEFmN1V6T0JpTlpBeEZnZ1h1K1h6cjdqcG1MTWpvVHF0ZGNSRS9GRUMwSktKZUJoYmx6cStiN0RVdnFTY2pLZ1pGd2FRTVExRDdWN0RFM0E0cUl6a0haWDZkU1k4YnZHd2NaVjlOZTJxRFZDMVZMSEt0YkJWdWV3NFZsYTdtSE5iR3lWNmYwYzEzbXRVZE1HeDNWQ3MxeVlqd1ozYWdlcTFSK3JlWEN6T3pMNVpaZlBYam00ZEJkU05Lc0liakhFT0xjTEZ1WHBtZkx4ZzdSTVBJZUNxZkhlTWlEZE9Gbjh6UjIyRUZEeUI2QUJMcHdPOW1yNVhTd09Mb0FOa3VrQ1hYUTNWQTkxK2JmL25lcUpqZS9pTGN2SDF1Wm0yaUt1d3NwTTBBaFZZZmhrVzF4ank0YVc1S2tqcEJuWFFqWitjSUZFVWJ3R1JZMzhML0NiczJhbHBQdjJ1TjJ5bVlzTGNpRDRJZ1ZPREZqTUhUWkw4UGVHNms1TC96UnB5bVlrNE83eXF5cDUrM3pjdnlnTWVJeVp4VGtxVzVPOWV1c093Q2VMdE9DUm5EU0FpZlRtQktoUW9HdCsvMjVYMG1FcTJzR050dWZZMnRsVmtGc0hvRFdTWFlWUGZkTnBUNjEwU2pmZHYvZEZlVXhXbTFaeHRLaXZjVnE1M0hnSVhQRVBubGRVQlZBa2hPYU5PWTlkR1FxUmhlaEdEcWNzVDJnbHpHZXRYZVlBQTV2VjJ4a3Q3V0JkQkp3ZTBJT3pBSTgrNGQyUStaM0JVZlBHN2VVSk12U05BRnUxU1owUkZOWUlmbkhyY0ZCeXQ1WXNPNEV6d2dTSVlnOFBJdVJPSVltMEgxL282Nmd1eVp0SVJlYzN1MkxuSEVtZXREb1NZdlBZZnJLdDBPMndEZkxxeVBUUG95d2U5Q29jdzVZMFNHWVNnUVdxSGVJVzBYMk5oa09BemVqZDNrVzBMRWg1bE1UeUZmVWwvSml6OG5pdW9VWjFNRDlZK0F5SnVwcHVndis3UlJQSlExQ3JOZE1ueXQ0eVE1Qi9XWTFTZlR0UUhGajVjZ3Z5STg5TkVLaTlOSTA4VXRGdzZRNEZkcWZZdEV4emNFZE5lcXFSUmhzd2dOS0dWcVgyYXdSc0ZnQkxQY1ZzUXhxSWp2QVhwUGVPVXVZcENLNkc0alZjQy9zRjg3WWtjUXZZQ1l2MHFOODI0UHIvMVBTcmhvNmgvUEtycmZpSEtBTyt6RHJDVnpCYThCblpnMVY1cjVwZTJBNFY1NitKTXI3OHRlTWpZcGE0YUVEWmRqamdNaXp0VVhIL0huckFoSWljMm9uUFBtVHRLQm9vWHFLdlVnRncrWFZsWCtqeGo4VFI5bmlWcVVValVQcWN6cUd6S0o1NmQ2Sy9DV0R6M3NoRjFxYUF4YWhPaldtdHkyNE1mYjNDanhJNW1aYktDZ3BlWnI2dG5ibkE2Slg5UjNMbnZnRDJ2Q0x2QnhFNVJmUmZYR0x0U2NRMjdBdEN0QkpSQ2Z3aFhtUmdjL01mUHJWZ2FCT0UyeFlLS2tNZXlwSGx1bitzMmRLL1FtRElvTmdISzFBMUdRY1c3VGFIVndZZWRWRnJDME9oanBGa0xpUU1kUEdpZXNhVGxpTTVUL0VJUmVDQ3VBRDQxc2k5NTBSYVRMTnJqSnRFU0UrRm43UG5YRGhxd0ZJNlVXUnRzaGZZY1hTZE1TYnAwNzBpRUNFSGZ2UFNGVmV6WFUzZXR3dUVzTjNLSWVlbE5HNjBjZElBdElEUVdwaytKbFA4N0d6Lzd3cmZyeENiMUN5TmxyemFXNkhJemYvbm50NVZhRWtXa0dMcHViQXdNTmxZb2FXaE9reVcxdTl2Rjk4VXZMc3MxSkwrVWZDUlFaMm53MVFaYm95ZHo5aGlJMWZRN0lmdTM2TTJJbkkvTi9xcVg3cFdTNkNRbFFpRmtEQmxBeGQ5OEJEL3lFQzh0UTVGTy9EaDJ1bk9pZlNUeU8rdUxWZkRNUG9rdS9UYStBb3ozZ2ZnalFVTkI4NysvWGM0VHRWTXZEcTZQamRDcHJHUE44S3NuRitwaHdSMmdjWW9jM1Q3V1lIL3BKc1lsS2RMWWViUDNZN0pQYUlZeGE5Qlp1RGpaZWxZdGZMYk5uRVhuWjVnU24vRU9wbHozOFFGK0dOZWFsZDU4RHBJMTNFQSttczhkSmJEZW5jZVk5NzJ2TGhIblFaUWtDengzVnBuakNEWEJrOTgreW5EQ005cnltR09RbkE0TEp4cjQwRzJ0RWtjRyt6Z1ZMeHZBczZWSXN5Z2VmMGNvb3Q0RDhkRGNDV3V4YWEzK0czbnlUcE03d0psZFZDSWdFSzFzWUVEcy8xQzdNV3NPT2hCWnpPQXNQL1VVcjJQSDJmbzljVU5ZN0d2N1RBN2JhN0k4dk91YXdpMktEMVd3RXFYcTVpYUduNWdUbU1sVG9WeG1xd2FobXJOZnA1blQ4c2Q0NkJIN1Fsb2UwNW9qZG11c21KOUV5UXVKVmpkTW0zSll6UG4xK1cxREZaT0NRaTY0WkNNUTl0bkdOVEk3cjBKZlBvNHNqU0pPT1pvU0hzOEFxZDNoVktXUmhibitDd2RmWXRNWE53Zm1zU1k3RjRTaWkwOFpibTc0WFpld3JoQStLdzRzV2xhVk1COFI4SFY0QmR0ZHVyNEdWZFdjOFZSUmY4cUlNbzJGRW5VVHpPUDNoajBUUzNoY3JmSFI2N2xPV1Jvc0w2WHR5Tm1ZMlFzaFNZNFRSMHBzNUhIZ1JYaHcySUxYQWZtVUFqOGtVTU53MTRJVEFqY0NwazlXOUdDT0ZYVWJ6VG00aVV0NXJBaHZ0NElva08zdzJzaWdGYlNUWWVuSmU1OXV1dlNISEQxY1lROEF4ZSt0c1dxdlpCMjhjUjNZSnc5VlJOa0pnNlhuaWk1dHZUTzc1NENvcDFwVXRhUGR4S0UvY0xkMFY4bDI4V2ZCUElkRStPc1ZkNjdoTEJkMlhJelg0Mno4RUxwcFZtOU1FZllhYzhCRnExQWhRRC82dENRY1I0M2F2d2RlYytqSDRYT2Z1ak5vVCtoWU53d2Q2L1hLSHQ5YjV1OHVWaDJsMVJjUlhNT3NHVlBXYWNSSVUxcm4zWnNzcDZydUFITXZBRUNsV0c1aU0wM3I5Mk9RNGFFM2dIVjVRdlpwMUJXTDh0OUgwcjBzQkdieVc2S0QrK1Y0WUkzRGtISzZmWit2dmJsK0J5M3RpaGV0eXVwNHlSZ0dmQUtaTENHelNMVXA3bDZBbGJscU9uUTBOSkdIb1ZLNlpnL0t6ZUJ0TXNhK1cxeU84K3pFdHdSNkVBc3hJdWVkVXpzM2JSRVZaMXpOcnJFekpIQ2Q2WVhTZDZHVFVtK3VzNmtkY1A3cjhoR283UnU0eWs5NWdWa3kxYzVYZml0MmpmMzJqTk04NEI3dm5lYngwVzBjOGZHcUZPeTJ4ZExIZmJDRXk5a2NGR2x4SXJpNGFvK1p4eExhL0VGNkRBV2lVbmh6SUlMMnN1RjA3VUUrUFF0eWpvNTUxbTBnZGpwU0hkQ05zOTQrZnhlM2VnRkd1RllwMUVORVVnSjRHelU4eGtZZGdyT1ZkVDJEQiswb09icERFRjVpUWRXcjZGNWtTNXVocXVkV2lLNUlMTkhMM3hESENISUVPUDhFZnhRWkN3TGluZmdXNFZqUDdzckorTTBzb0hmQUh3M2tCSjRkRlZjZTBmYWw2RTRBa2lDREd6RHVZZnpYbHBObXk3cjBPaFdLZm9CTHRXUXhxVmliaXZOTDA1Z3dIakNsaHhXT3EydnF1Qk9nNFBNMTdLSGpzQzdWVnpPTjh4SzRmVzZNMDd6cVZOUVZPQlhIbDFkbHJCTlNVcytIVS9vZTZlQ0NLS1RVbEd0SmtmWW1QdEVxL0Z4UDJsTmRrVHhZcUY3YnQ0cTc2cjNJNWREb2NqdTVTMjhqTlZNNitIclB4N0VyajRlM25DdGVuSjc0ZWh2TUNPZkFRbkxZN0NiditSKzBTMlNQd0RqMGRUd1RMMG1ZSm5zeXdEaXloZUlqaGhDK0xWL3AvV1I3K2hlaDZ4THZrb0tUbE93bFRUS0ZJSDRoUnYwSDR3dXBlVHV6a2lGVDEwTU9EUkEwZ3JscGJEQ1pZMVdIOExYZHZYU1d4bVBvcnViMDhwL3BXaWRVZkFta25JUER2ZUcxclJ0blc1bmZLOWFTR0ZtbFdMR0RSNk1DQ2ZyMSttS29PWG81dmZSYWRHTEFLL0h5YnFDMjZ1a2Q3T2daYkNPbnU2L2ZTZkFjSWVja3REYlBzMi9teDZHVnBxb3FDOWZmV1hWVEdXK2hpam9HTzdpSWlSaXh6Z3JxbWRDakh6cDgzOFUwbjRsSGZ6L0hvZy9SZ01mcTdEbEZ2TzQvdGN3dXo4ZW5CbytvMEJvdHNNNXVqVzhjVUgxWTl0SHRoZU43NlVBdUFVcFNoVFBDN2Q0bTNwMEFPK1hIZnlYUmpyTFJHNTZXN2NqR09CVG9jUXN2cStVUm9Na284bXcwQ0tZQmxrWVRIamlJMjVpeE1xYXFrTkxPZmE3Rk82UXNENnBrM2NXQmJhT1h2UzJNdmRZVmVmK3BFT3lrTnZXclRDSUNCaDF6VFhjMmE4aDkrdUMzTXJjbFVuSC9IMlAxWUhSL2IwaHl2RklrUG1OMlZKWFNvcllNUFVRM0R6eHFGdTkzbmxVSGJKOXZNU0R2RGtZWVhQMUpEZnRvWWt3cDllUnh1SUo3SDd0VmJCUEREMjhWeERSTzBteFgrclFKLzZrVnhpQTVFU0h0Z2hJRkpnTzdpMmZ5Z1R3K1hRemRHeHZkNll5eHVJUG9hM1VXSThVbHdUcVVaRDNnUUlSUXNFUUwycm5oWGx3WVY0bWtyWTBZU3V6d25xQjhLblNoSTk2RExmYmlybFAxSXRoMFJPOEJqbExJd0pnbExpTmJhV05oNXlwL0dpTWMwc2lYWjZ0RitXY0NhN0R0WVpVRTNtZSs3aDZmT0JOVlFkNnIxZ3kvWWEzWGs3Y0VSeVZ4N0kyMnZaNWFLSWxldC9va0NyTWJ2NVN0bGgvRjBlaDc5MzNKMDBsbk5MRWo0ajNLQXV1YXoyOEd6NXc2SHVPVVhJbkhaK001WUgyMCtrZko2ajFVVnpYejhsQklIeUpXZkRzbkN1YWs5cDAreWVNSUh1NzFnOExHbk9QWlptTTg1aTFvM3JUVGE3QitreDdYWnNwUmQ5eE94azY3RE42MzdxSlVKQVNoemtUWkRtNTEwb3pSZVRjdGl2eTJHcm5rSk1xdGw1bmRpdjd6bnhOcWVTdFAxa0tnNndKYWdhTmp6amQ0QVdhTGl3ZUFlZnhwTmtESWJ6TXp3T2RNZyt0R29rZzV4cGp2b3NaZzQzbm9JYkI2YndrS3JYQVJxNm0xMEZhM0dSb2N5aFRPS29hMCtValdwSGczM0hJZ2RVaEU1SGFQaXhSUDJkL2xicEdMM0VSQ3hNRWlkMllEUXVJWVpoR1JOMjNQSUZrQ2hoY3ZIZ3B4ZzRlaU9iTEJrZ3RMU3UzdCs3STRmcXlyNDFPREkzODhpWDJLZ2hHVGhnRTNyMEk1MmxMamU5K0ZuU2NTWm0xSzRuSlBOZkQ0R3VyR2RaeGhSeXRCTVNCV2ZYL3k5YXF2UEpYSXNNVjRjOTh3TVZ6MzhJTEwwZ3NyY1lYWnppaEdFNXZBWFJvcE1XN2d5MXpoWllrVytRT2EzUmNFd2ltc0I2dmxLTFFRcVVEcmZ1TTBpSTd5VzhIZTB2aDB0RDBQV3llVWdITkpBMVQ4VmsvV2wrQ3RQb3M1ZjBKY3FudENESEpDc2h4MGcvU3V3QStUaGtlMnl3dlBLcGVRSFNXaTg2eGFzV0xHUFo5aXRSS0V1TFB5dE84NjVBZzJCYzFrcllqcHZvZ2VlK1NyWkNtVWI4T2lWQVFaa1lrUEEyWjcwRDh5Q3NHQ2M0RzRuNVFJdUZhTmI4UVdLQXFSNHh4WDFQQVQyNXVHYUE1NmxFV1JsZDQxQzcyamw1cTlCUkVUcGVROGdSTktjWTN6ck1zcE9tc09RRm1ybDhtZlh6eWNNQWU4QkhMVmNuY1F6clRITHRzTElVeHFya2QxVnpKVDF4NUJJVHF5VVZmNDBvZXRMd1hFQkltMDl1ajVHSmQzUEU5YTIxT1A4Z21BRDNOMTE2YjA4MElqeXNyWC9CZy83bzJmU29Ia0RIcjZJdC9RMnA3b1BJV0dWNWQ5WDZFTVpSdmFldUdsdXhUOEl4U09xNWRvK0dFNFAweElNSDdwR1J1b0R1eUZjZStNMXJUYTNDTFMzdnBPb0ErUzhwV0paa0t2QjI5S3M4YmdoQkgrc3J4Q3c5T1h2MERwdEFobkJUYmdESUYySkpzeVNaQ2NGcmVSbFZOOVVLcUp2Z2FKdTRWR25IemIvQlNKK2dLT3U2dnhxWmpCWkdsSitHRTVaVnZmVUtBVGtpL0wwQk5MaTZCbnk2eW1JRnNjN0owQ0gzUzFmOGZJTE9CdlFuWE1NMWtadklvRzNuSHhtS1FCUVVQODJBazZaRWhmYWtkTjRpbmsydUU1Y2lCWjZtWWFCS09udEd3QWg1UzU5V0thVlZ0VS9tZGYxaDNjTnlSZDMxN01jdXNQRjFsWDFIMFRjVkV6aldLUi84NUt0RXBOREhHVE5BbTQ5MlMwdDY1WmhtVk9oMjkyclE4THlOUnh2aWZ5TlB2TUd0S24yS3hqM1g1N0dMcVhEQXA4clRzVUFRZGg5N1p5MUFVMVZ0SDFJNGN4VVl1MEhYem5iRFBQVDhPTDBzUlNUbTBNcVp3QzF0RzM5MFRrR2NXckZUM0llS0dBQ2JQRWpwcjRMcWdKZm9oTW1CZ1BXeDJIZmtXRUtSOWp6Q1J6cjZwWkZGdjZCaFBWV0hsQTFTcG52V2hEejZ3eUVWc2JIREdnMzBVR3BYOWFOSjBpRGFtVk9sU01heHVsY2Z1ZHRjdTR6c3I3N0xRdzdHb051MnJnSjUzYTZ3QlU4VzlVckpLTVBlRENRQi9LbHMxWlBEd2h3OWJLMWM2SG52TUF2c0VGUWRlNlVUb1BVRkNmUDBIa3BRYUROY1NFYkYwU3dzcFdXRXhUUGNIVERmN1hXN2R1ckNrVm1qOERuNEVyaURZT1lxSmFibGgvckJ6eW0zVlExZ2V3dnFMS1VQOGhvUVQzUW1jODFWK2EwY3d2Z1o1Y1NTU29oaUZhdnBvazRGYjFRT3J3NmtPeVNKQVJNWXZqYktubWN5TjZMR3pHcGhkWGRDbnFoVllmUmR4ekg2NXZrR2pyR3VGVXRhL2NSL0NaS2Vkb2QyT0l0RDloTTJyZ1Z0a1dUaVpuY1FsaU9ndkZPR1didHVEcjhwSktrYTA1ZERFVWowWFFYUUd0Wi9nQjJxenVjNXhnS1FkamVsYlA3YTZEMFF6aS94ZlVVbThJRUp3ZDQzZ0dTaUZwM0ZXcGxmeTVuTU1kbnY2VkdCUDNEalNqd0lqbENITDQvRFhTZkxrMXVzWS9KZ0xWUDY3Ukx0dFpHMUJpT2xIY1Rxejc1ckZjZjVEQWF5Zjd6eHVzZmllVTVtSzh3MWRtNWRSMUF4VVAydnZ4Zm5XdHBSeXNJR3lEc0dIN3kxczl5VlJXOUFaOTZORHhPZ3JnbFVTbDhQS01YNXQrbkUyeEQyUjJ1QitxbXRJdDNHVWNEUWxyK09uR1ZiSWtOYklST0kwR2Zyemk5RjJ1RzRQdHBIT0RaNFJKd2k4SHAveWg3RzRwb1kvaUVjMi9YTTVyUXNrYVR0V3JFd2ZoTkJETG1BL0JmdGdyWSsva0xORmR3bUJzNkNnWWFDM2kxUjVqaTVkZUV5SFVWMGp4T2FQdjdPQlBXVUFmZVU2dEhFS1FwZ3RJNzVtZXVCZnZPdmNwMWxFaEZNcENacXNhakoza042dlI1TWIzZG9ZVGdHZzIxcXBnT09KMm5Fd3REUG53dFhOUkFSdlc5VmJrRGlPN25PTVFKNHQ5U2ZTa0dVQmQrRjZ4OFhGTjhwWGFCM2t0RUI1elppdlRFS0FKZnNHeFY1SWVYOVdvK2NlNVlUeHJFUHJzQWZrVEgzUW8xNFFpSnRRS1dEeGt0Rml3QkVoNndmcERwOVRDbWZwQTVKbE1ONEVaZEwyaDZ4RElqd1cybHlSK01OKzRkL1pUNHl5V3ZXci8wRTZRQzMxWFBUMHQzNGREQWVLZGVaY2EyYnNQWHE1eWFSSUhScDR3c25vbTFiWHlHSC8zbGxjTStHNlRPMWpVMmwzcHQyaktIT1RkSlBkWjd4MEZMclhMcmN5eHd5MlJXSnpHMGZWbEJaZVNZaXl2V3Mrb3J2NWxFaXBlb0YrU09pZ0x3V1FNZ3YvZlZITTNCWTFKOUZrOWg3UXd3VkZ5elZOK2w3UloxVlhCMmV2OGY3U0xJdUs2WVFkeEoyMWJZYmFFdXp0VlIxTGFmdCtSQm9DUHVkZ2l4M2xwMlhHQjdYa2pqV3JkQWZkZUJ2WnVSSHBNWXR6Unc5UUY1eGpIbTdRekRyTjhOa0xDYi9KaXFDajZEcWdkOVE3bmNiNTNEMjlBWE94OVNwTEVVZVdjS3dxdG5oL2lxQ2hiczVlbVdkc3JZY1ErSjEyM2Eydy94aGpCNlI1elBDb0tjVi9rNjQ2dng0U0UxQWRVVjZrZ1hlYURiY0ErMERWeDdrdENPazhpSkhHMTdkc09CaXRsVDRxS2tPY2ZRdEZUTVo4d2FqcTgxY1paWDBaWE9GV0luZ1lMUkViZ2ZsL05ZTmN6a05lL3Y2N1A1UXg5aUJkV0I3anZiN1U2bXpZNTNZRm02TXpOa3gwT21Rd0ZrTkZVR0ZJSjNLb0Q2RzZMbEs5VVNERWswMUx0Y1h4VWpPck5WcVJ5NzZ0eFpSbW5ia1Iyc2dZVTNmNG5DaVgrOGlqVE9SMXdzQm44YVY3UktKWVJINURiRXk4Zm9wNk04NzV3RTFYQU5RRWlaZS95bk9HK0Q2elRLSnVCZHluTDMvNEl5Q1VyNmF1czl3d1RXVElXNUZEcCs1MFAxeUFpd0tnZnpacExFcE5BU1FsbDU1U0lSY1c4UFJUdG5SZWZYLzEzUDEzZWYveERGVkI3OHJ0Nk40SmJhcGN5aGpFaFZYeWJWWjRoMktxZDVRRExIY2ZKejlpY0k1aXNjamJQdWJZanI3L1BXMnFMMmwrRzRIVUlSU3pIS1VuTWdKOW1nd2dEeklUaXVDOXkvQ3BPY0J0ZGhEMVJEU2lPeTY3Q3JDdEFSYzE0TW90OTRpZTVnZnZFOGNZUWliNXZ5cUtGRkJqa3pFSkNBaHdXVVFpYi9ZcE5KUUpxd3llbHIrSDBKcDNuaEZ3SkVnL2lBK2JCYUl5d2tYbkJmVlVrTGkvSUV2ZE80TllRNFc1REM3RVdDNzExVmJnQ1VLSFNlRXFveUVVZTJDSTZPZWp0STQvMTF0UGx1bTJ1elJSb3RId01nN1dMUmd3R3FhZ0FzLzRZWStMUmlEaDhqWjc0NVRkcDFpSGJPY0N0ZURRdE1JSUE4NkxBckxPeGJ4b1VSNDEvMFRQT2JRejJyaDhMR1g0ZkJLVXNJaXFJT2lYU0VQTVdjYUhsUUJ3Tlp5bURYYU5IS1dVNTFYa2p4TlZNTFhtaUxLVUFzbXdYdmVuNTlOL0E4WCt1WHdTZWRsclh6NFpVOE0yUTJYMmtHa2hZUTUwT0tuQ3k2YWpPUXB1bXBjZWRuY3Z3OURNRHJDL3NUOU5SNFJEMTRvU21FOGlZTnA3QjJEZVBtckQ1a0hWL2VHQzhjWmlxOUtML0hzM2svSHZIKzdoNW5yWHNPenFNZUZ2QXAvemdZSHExRUhlMkErYjkxV2E4OFZ6Tkl4UTE1VHl5TkdIV3ZFaWpJWVIzd0Ywa1ZURFhyWFZXU0tCVC9aTkNjY01jS0xkazFlRTR3V0hTNW1aS1lVYnFWcVZrdXZYbm95dGhXSlNuWGQ5aWh4Q0dRWVFGaW9abWxVMWxBaFd5L0psY0VCZ3krWlhKVm5zaVZLaXB0VUtWZU85L01RV1RUL1VVSEEyQ2x4cU82NXZQNlRGT20yb0xFMTJBTTl4QzZpRUFtTTF0aGlvdjA4VjBtZTZKVTNKM3dOL3UyR3Z1YWx2cGRlNGI3bi9yM1hjWGRZRjA1UVd6MjZUQzl4OENHRithbFEzMVoraUdSM3F5bU1Td0N0cWRvak5KY25sR2ttQ05IWGVsdzI1dkFKVmFodzRkdEp6Zk51TFVZcmhCbGp4UmVmbHBTMkxLWk1aZnpwbkFzaG1oSEVRS0c0UFBLYnZaZFdUYkhlaDRmc3lOemxOUFdXd21GTHNEdi8zRUN6Sm9aZjBzdTFqS1NhYnV0b080anFFbHJaQXhWcCsxcGhFMERWUG9veDhLRUxRTW1aQjJHVzBjbjg1ODdLNFNZbnlpTXBkSHdpUTA2VVRvcnQzb2J4UmE0cGh2emZIaGd2bGRDSU9zZllRWTJ4MVlVK2w4RlYzc3Roa2RtKzVtamVwTDJJczlhTVRyenJLanhzbW10UkpDQ1hrNnl2K2k0Z1ZyQVFYZFBCVE0remFkLzFKR1Y1c0xZaVdlS2pxM0p6LzB1aytCYnFnMzQvbXI5MnREcGE3WXpLdmdRb1NCb1hvbG04eTJqc251S3NodThFMWlqWVI5L0RGMTRoNThkVnpPTjBFeEZkdXJHK1Q2NDRqSmdMMFlYK3lkSUNzMmNoL0F0OTF3S1JVOUNQbGRsWW05c0cvR2ZJYThZTVBqYWJPRW11SEJUcytyVzZmcmhIdWt6dE5IZ1dHZnFEbmFONjhtODl4blpTMXNIYWU5NForSExYQS83N0YxVTFsR1lDK1l4ZEtacTBQTklVSmM1SjZMMlE3TWlBREhwMTJkbXhmV0J5VHAwYlBnMzUxajIyVytrd0pOZGRrQXgrV1N2ZUFWeVlocTRHUExlNlBYL0Q5K0tIbkN0Ykh5bE9BZDY5OEYzWm9ZRksvTmJORmo1SlRKbE5Hc3hWWkVWUG5QU3NzOHE5cVdpZ1FVcUpxNVBGYWJjbXBxdThKR0pDc3hmUlZCV0dZMUhMdEVTRERZeTM5WFFuZmdsY25BdEI1KzZaWEZFRkJHUk5qUWtST0NoQnFTWk9lMHAxNjlqVkJmK2xra2tnWjNHZkdkYmczQmZVb3QrMWVtOFc4SWt0dk1mZFJwOTVTWkM4L2pOdzBpTmhwTWJDV0pJSUlCNDRjSDJvRHVFbnR5N2JQaFpsV0hiZ2ovMHZ4Vm5ySlE2djIzS25LeE9SeTFzZC9sdThvZm1BeDR1L3NjcjE5MTRqb1lFVlcwaXRESmRlUzFPb3k4K1o4M0dDZVdIdGNleEk2K2N5ckRIc1ltekZlUy9GV3FyOUU0UGlkZ1NkKzVGUmdpZlU2Q1A0UG5OWFp2eFJ3Yms2cDdNNFBsRXV4RWNSRGtyRWlnR0dwOWF0aXk0S1JJSlVTdHFISzM1YitKayt5SVJWZG9KVWRUREk2S0JhVmgvbmovdWtwSXE5WVh6cGFiTG1RdUFPOVlqTTc0TSt6R3VlUk5ESkZ2bmt5WWxPWGJzbVhmNllwSE0yRk5FVnkwQVhwYWthTGE5UHpDZlJTUFlwQnBDTTV4VnE5R2FIMkdsM0ZzT0ZOM2xybiszcnREZlFoc2JxMzNpb1o2MW5hTnI4Y0JuaEJFTUdXS1JrWkZ6SnczbXBNSGlUZTlnTHhGTmtsb1M0SlpOaDlYSU9WaHJjQXMzYkhpelN3bmJqWFJvcUlEeHlBRDFHT3NkbitSU3FVTnlObFlpbGJiSWJDdGZCbk1uRkNwVjJlN1dSNU9WRmhyZXdCUGk0V2VsOTFudVZWY2F3MVpMbjhVRXpheWIwV1VucktIRU5DcjBXcFJ2LzMxemM3N2pROG5uUnJoTkZKRmtsdVZVUUl1L24vclRDL3dKTzFiSWt2WDBHY0d5SkNWVXgwaGwxbHB6aFFNZGJZc2lVbGsxVjE3QzZDT3RQeWswcENqZzAvRytNUXlYVGZYNW5xMFluejF3bzVuZVp0WkJ5MW9IcVV0Rmc5VS9tdFl0R0dTekROWWpNZVpWSko0V1ovcy83anFNMFlsTnpMamppMytyWWYzRHB5MHMydEJXRWZONXZWSFN4THJPVk5NV1NaT2x2Z09zQUNrTW04K2ExdjZPeGFRM0J2OWRrcFhRUmFjVjhnNlo0Nk1CMzcvTWl3YXY1bnZMQVl3MW1OV3BXdEJOTm92YXo1TE4rZFJGSW5TNFJxUDV6V0htN0NoU3dLT3g3dzJSYVM5eEFrbHlmZzUxMkYyQjdBNlk1amhTQ3BGQXcxakwrRnBQUHRQbnVQVTFCRnhkSUlrWkhZQ0NCY01ONDkyWmNrSUVUVlpHRnorSnRnVDUyNEpVSWM5M0Jxdktnb0xOeldkTHg1OEYrNHNldzlKUVUrdTVyeldVVSt0V1EyYWlKVldQNjQxU3ZmRFZFV2VPNXRkVG9VLy9Qb2NDd2FnbTIyMnJWWE9Db2Q1SCtCdVRoSE13d05tdXhlT1IwSXFZNGVodFF3a0VaV2hoVzlVV250WU5kb05zQmROSjErWTVQdzRCYUZBM2hOK1BxV0FFdEhWaXNIckdkZnJPQUY5eXkwUlV2TjhKeTg4MUoxWi8rZCtNMG42YnoyVmltSjBsbHQ0cDZQNFJ2WHlEZG9TVWFRNmpTc3dmdHU0WDRLN3pFb3hBa1dzZ2F6ZXl1TzFxa3BjdFNsK0FUTHNRakRpWmRJRnptZW5tTk14UmFKaFBpMmtndGxvSjFXc3krdDh1b2IvNDlmVVVXUi9sZ3FSSStkRlpTNEVxdnppQVpueWZiVlU5VlhyNkxXVU9wY3R3ZXl0K0Fsd3ZsS1phVTY2Q1RRWm1ZRDhNTmJtOWpTVnVaVWRiKzFnV1hlS1ZmRHRqS0dDSUxFajNBcVlYMDM0ZktlSElLQ04wcTA4d1ZmOGR0OGNOcDNhL3NwanR3cUxaM3U4WWJBbWNOZ1hoaEhwT3NNbFh3REowWUhxSFliLy9VQ0ZQWmJIYS95OEZyRnoyWWNPRHU5c2hXc0VUQ2RxTnE2bHYzZXgvdmRReDZPNGlOZVNocU8rNjd2ODMrYVo4OEdZNXpVcHpwQXlxKzZZV0hyaTRhdkNsV3NlMlJvUkk5UnJRaWlHM1I3UllueE85OXVxMjIyQS9KMG5vZ0k0Sk5vTHphOGxIUE1ndU1WRWdUb0t5OVB4cXI5Mjg3YVVYRC95UGJYUlBUeEdjQjZqRE50L3JyVitoU2t3MklXMWJUelhsaTlOblR0ODBLQzh1cGRUaGI0emdKaDBJa2FVcXY0WUZRVnBJY1h1bjZ4dHc3T01UOTR2TWE1alRYTTZneFBKYXJSdVBIWHBucS9wRjNzcUNwYS8zbytOaEd2Z1lVVVBtcW9jNDdCYjJoNXhmL3UzYi9DYWYzRFBxZDVhaTBCQzdoUENWRXZCaGM0c2NyVmc1SjRXaUduUjhHVWNoN0RTTUZrR2ZwTXcyOVcvbzBhMzNOdjlacThYbmR0bWsrWEVvOXliT0hvZ0I5ZHdHN212bWlReHJKUk52ODlndmNqZ095eEdmdGRPUEJ6Mmo1MWM4TkJQSXJtam5URE9XcHZHbXg5RWZZZFFrc2Q2RUZHSEFKWm1VdHkvbEVOVENjcUlYZEEyY24va0dBZkdTYk8wSHJGZnU2R3pXS1oxRHhOOTJxV0ExODVqZy90d0RvRE9reGNBRGJkbVA4dFh0eEFyR1lkV1NpYnd0YWRGRERwM1VkeDhmc2k1aS9oaDRtdjd6dUR5NmI4L2Q4NmxpbDliOHhUSmpoWWtPR2c0Y0tYUHVzdDhmVVhhWmpudU9xVC8rcXM3SkNMdm5mN2FWd0lIcHlENkpVZytCZ3JqUlV5TTUzMXYvMlFBQkFCb0FRQUFCTVFCQUFBOEJDVEVDQUFBQUFBRDhBYzVBQUFJQUNRSUJBUW9DQUFNQUdWUm9hWE1nYVhNZ2IyWWdSbWx1WjJWeUlDZzVLUT09PC9CREI-CiAgICA8L0JJUj4KICAgIDxCSVI-CiAgICAgICAgPFZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvVmVyc2lvbj4KICAgICAgICA8Q0JFRkZWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L0NCRUZGVmVyc2lvbj4KICAgICAgICA8QklSSW5mbz4KICAgICAgICAgICAgPEludGVncml0eT5mYWxzZTwvSW50ZWdyaXR5PgogICAgICAgIDwvQklSSW5mbz4KICAgICAgICA8QkRCSW5mbz4KICAgICAgICAgICAgPEZvcm1hdD4KICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-TW9zaXA8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgIDxUeXBlPjc8L1R5cGU-CiAgICAgICAgICAgIDwvRm9ybWF0PgogICAgICAgICAgICA8Q3JlYXRpb25EYXRlPjIwMjAtMDEtMDdUMDI6Mzg6NTIuNjcxWjwvQ3JlYXRpb25EYXRlPgogICAgICAgICAgICA8VHlwZT5GaW5nZXI8L1R5cGU-CiAgICAgICAgICAgIDxTdWJ0eXBlPkxlZnQgTWlkZGxlRmluZ2VyPC9TdWJ0eXBlPgogICAgICAgICAgICA8TGV2ZWw-UmF3PC9MZXZlbD4KICAgICAgICAgICAgPFB1cnBvc2U-RW5yb2xsPC9QdXJwb3NlPgogICAgICAgICAgICA8UXVhbGl0eT4KICAgICAgICAgICAgICAgIDxBbGdvcml0aG0-CiAgICAgICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5ITUFDPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICAgICAgPFR5cGU-U0hBLTI1NjwvVHlwZT4KICAgICAgICAgICAgICAgIDwvQWxnb3JpdGhtPgogICAgICAgICAgICAgICAgPFNjb3JlPjY0PC9TY29yZT4KICAgICAgICAgICAgPC9RdWFsaXR5PgogICAgICAgIDwvQkRCSW5mbz4KICAgICAgICA8QkRCPlJrbFNBREF5TUFBQUFYNUVBQUVCQVFBQmZqUUg1Z3djREI0UUFQRUFBQUFBQUFFNUFFQUFEd0lBUUFJQVFBSUlBQUVCOUFIMEFmUUI5QWdGSFFFZkFiSUFBWDNEQUFBQURHcFFJQ0FOQ29jS0FBQUFGR1owZVhCcWNESWdBQUFBQUdwd01pQUFBQUJIYW5BeWFBQUFBQlpwYUdSeUFBQUJzZ0FBQVI4QUFRY0hBQUFBQUFBUFkyOXNjZ0VBQUFBQUFCRUFBQUFhY21WeklBQUFBQkp5WlhOam1jb0FBWm5LQUFFQUFBQUFBQUJxY0RKai8wLy9VUUFwQUFBQUFBRWZBQUFCc2dBQUFBQUFBQUFBQUFBQkh3QUFBYklBQUFBQUFBQUFBQUFCQndFQi8yUUFJd0FCUTNKbFlYUnZjam9nU21GelVHVnlJRlpsY25OcGIyNGdNUzQ1TURBdU1mOWtBQkFBQVVWdVkwbEVPa2RTUWxRd01mOVNBQXdBQWdBSkFBVUVCQUFCLzF3QUUwQkFTRWhRU0VoUVNFaFFTRWhRU0VoUS81QUFDZ0FBQUFGOHl3QUIvNVBmR3dBVGZON1VUZWdzTForL0RQakRqTXp0bGtmeWtvT1Zza09PNnFhR1JlSWpaZDFJc2xJZ0s5RENqTzVpQUFlRzg3MkE1SUE2RVJkLzdTcEdmaW1BZ05TQUFaZC9qOW1pekJDaDRnQjVEMVoyQ3VheEtzemJZdFdXRGx0amJ6aXFNOC84Q0lBOW4zVXBSb0hKYWg3eEYyUTlJQWYwdGV3RlV2dVdQQVNGODd2QU13TVJhZWgveFVSbGdFT1NvWWhacGtNaklqMWkxbFNLVTNZYzFEUHcwT1BFcGRoTVU5Z2Y5UzE2V29HeVk2aUE3ZHhnTjlEVHdRMzRQd25yUmh0SXNVNklJVVB5aUEybEdtR0E4VjFIbElEczJrWmJhZklnVDJqTUhTQmp6M01Tdm14cVIxMW55ZUE4SkhrZy8wSXZhMWZMTWFzak5KQm1mWEwrTURlSWxWZjFOc3JTck5BMnJzdE5teHRMWlVaNUkwSmFaYXFsRTMwVnNlV1ppekd5ZDM2Z3NnQThMUFFKSFhmWWRxMUR4VDFDeEtON3g0L0RnQ1VvQzZ0amlDelRTWmoyTXFVc0lkZjc2TDM0dmRMWWlDODN2elA4Wm44WnA4aDQyVTZWOHFxQVFRV3FaZWgvS2wySzhVNlhKYnltc05yQnJVZFJ6Y2xKc1kwbWJqekhBU3dheC9adGlJVG54MmJpSHM2NzRUc0NseGlhWlBZVUtzZk5HNDhqdW5zeFNWeWlmTWFNV3BOQUpVWS9MK3NXeXJ4NjZ0U3VlM2dDNnRMbFBKRzJ4UkxsdWl1OThYWHZOV1owVmZPekFXZXhoLzhBbjEzNkhUZHZlSklDbGNYY0EycHdEOUE4bld4Rk1McmlGeWlDRE9lQlNJUE9pN2RuVlpOQnYwNTdqNENtU09BK0pmdTlBUXBQZk03VDY1RnZBS0w0ZzRHYUMvNG5uYUwrcTNISmxDQjFMaFlzVFVtZTdUTEFVN3gyZm05ZGhtaXFzcjVtdHZscjJMNkpKaldmMGNrR1ZYTWhlODJER010U240NG9jdUcvZWZBa25hUHJ1OUJ6bUR4MXkyVE5abDVFcVZQYUwvbHZHaGRwWW5RenExYXRHSnB6cHFSSHQwUldoZjlWTnhqVUNXZXpBK285enNLNVUxcGRUSzYwcjNJL0ttdkM4djhIZU9oSmhQQlVYWUZFa21SdVRKNHZsUmdwY3JMSFZsM0N4czdjTWRlZy9nZXpqaFJjVmZPZGx6UFd0Vm1ZaW9KZVFSbzRQOTBCSHpORG9MTmhwVW5XQytuVWpENmFTTjFUcC9pVHBoSmVCNWpidHY4SERWMnVFbXlnSjJHVHo5RWVlWG82dnFGUjR0UEpjbThQMlQ3ZzJ0bDZMdWNRVUlwV0xOY29URmpuWGRyUmxuYk81aTZsTVMxVUs3eURicDZNREt6ZkpHZHdYYkdWdUpkYVUvOWViSUt6Qkh5Z3pVVTVqbjB6MGwyVHpzckljMnQxdUxxOXQ4UTBCQkJzNmMzazNuczdqRnVCQkxpakwzL2FpVlAxNU52WFUxaDQ1eVlJNUkxS05yUTNtUTFIckorZTZPNUZJOHEyUW1uVFFnUUtGVnk1NzRGTVVYbHo1V2dZMEJRUFRjVG4xaHliWTZXRGFYNTFrbmZqK3hNQzJrOVNTdFZCRmlHM0NzVStPeVpyVFVGT1JpaUczbDI1eE54d1JyRGhqWEZ5VnE2T2x6eGRrdDNYZTAzc0ZkNGQvdUg5aFZWTzg2UGNXdkwyQTduaVRnQy94U0tndUFETmtveW02dTZvYTZyY09KUWVRSG10VWs5TTcxamxTY0JhalhpQXJNUUNrMzQ0ckN1bG9Uejg2TmZPaVAwcXFGRkI3bkY1bS80M00xZWdvR1JzdFBWMFV4WndSVEp6elNSVS8xTmJoUkE4N0Q1Q3BrQlluL2xHalR5OXNPRG5vdFJGV3ZNL1k0OEgrU05UdXQ4UVA0YnM0N1dnb3dJL1EvVEd3UHJGanprSlpPYkY2QWRCQUgzc0xFdGZ0MHlqMDVPazFHemQxZDdKeTRHYXlDK3VIc25LRWJzUVJaMklvR0Q2MVgwamljUE92ZHRiSHNEeGNJVHJoQTZBTXNId0tFVnBXb3FVRmtrOTlvZmhhbm9hd2JKQVV6Y085MTlkKys3YkxUVEdEQjZqbmhKNDFYTzMwYzc3dWZRNzZSNlAwR1JLN2kzM3JBKzY3VUNYN25VWjhKN0VBem1PNnd0T2JRcDd0R1JXY3BHeGNCKzZWRHRlRit0eE9GZGlXaDFCcDhWRFM0SmVRaDNxcG5raVNSdWcyUTNBTUhOR21iTXgxdEdSUUVRaFJWS280dlYvWFJwN1JRVUJmd09CRkdMdTZaOVh3MG9SMmlrYUJVQ05RTnpNaHNOcFl2dlZkci9BblFsVjloRzdoNjJETUJFTGFNMTdiNVlLckFCTFVGL0NTMW1FMHJONjczMksvekJ1M1Foazg1UVM1Vk1hL0pJK21TbzJjM2RTbjc5NStjS1lWdDNBYjUzMjdrdVArbVk0WjB0c3lDZDBEZjdiUGdxdCtJQ2x0SDV0bmpSRzNJSnBzTitaeGh1OWFoUnF6OWlMZlMxMWhXN05ONkZoRDVacWkvYVZ3MUhDOWFhNXEza1Z6UHlmMGZhdEk3MU5lVXdWMitnL1ZpNlhYRmZpbmh6b3BmNUhuK281MDBFZ2hXcFR2T3o4eEdLdXUvS2ozL1FPQ1Q1RW1sSzJ6V0p6MFduNEhvTCtwTU1xbXg4emh4b0ZycU9JVEFkVGRnejlGbTJlcXRJZjV0TUN3L1dGMHpqUzBwak1xbElKZTdsVkF2ZjZrNS90SDArWlBGbjZJMm9NQVErak14ZEYrYUNYcTFYYTBjc2ZHQnh5UzNIYnVjTXpsQWZFK0JXVG56Mnh4bFlUdHdvVnRxeEFveFVBcVlUUlp0Qk1JWlNTbVhNN21vVk9VaGlFMlhtV05MNlpCR2w4UElHb0wydm56N0YrSG9qQXczYk5UUTY1c0psazdWZUI4c2RJSjc2VVFqanppMlJjc080S1gzUzM2TG52Sis5SWh1QUQzSnFtS3JEdUlNOWFGUTVmYXVkMEpXMjFuOHdZYjNVSnJIUHc1djAyVGsyUmFZL3NsZmcvOS9VM01PRWN3aXZmR2NucWowczNiZ1FOczRKR0JmdVk0R0p2WWpLWHVxbDh6OXZ3SVNOMDZzQkk0WDFoSlUzUWhuNXJoTlE5UVozVTlDeXdOU2dGYmVFcXZEVTJzOEFPVWdsMjFScm05SEY3TzJ4ODVhbFZyRUE2VnRVZmN1TWhZdmljQU0vMVNwc3B1L2tUazRXc0JvekVSSVZxL09Ta0lSWUVoR01TVUlpd3FncHk5ZldHL0hPV0NObnlra1U4TXFFUWdieHBxV211QTJ1TUEwYkFtTU04S0ZDNmp5Q2VlcG5QZ04zcGNOSkNvbi9rT0Z0T3FUSUZuSjVBM0FnNHBqSzRMZFRkQzdFODd5UmhvNHpRaFJqNWdiZzhPM1o3UVkyeS96MWVGVmU0NmFoVU1WcTdpYmZsWmh4NnBPNjdWd1lPNHgzUEg5bFdUL05MOVZsTlM4Qno4M2JHWE9sVlpibzNrK0E0Rklxenc3aUExamh2RWo0YTNGc0JVQWdoTTg5M2RzMk0vMXpENWorY1NHeHNvZHprSkIxWXU1cW1ydGJ5TGEreE13V3VVQUE1QzV5dTdTRHVSWDV1c1AxZTk2S1UvanpOMEZhM2pEakxyUXlYdWRNRDNHa3ptZGVIaG53VnYxMjVPU0R4M1VmVTVRTFlVemd0VlFybDFaS2ptdHNuVmp2WXdkV2t1bHFJZFlUWWRVZThiZ0h3VU04RDIyVG0rbkx5M0NvUU0yWkYrYjU5YTN5SFc1Wmxab0p3SVhyWHJhSG02R0FFMENnQWlDTlRKVmpMUiswODlocXkzQ3NEeFlrK0dLZ3NJNU4yeThzTEF0YW5lemdSQ3FhaXhKTjkzZDJZYm1OMFFtRENhUGlsb1hxRGJjYmhzTXczNVJHcHNRTjZJd3VjRHIvTFpOMm4yNjJya21tWFlqMTQzUFZwaUFsUisrandNUkdlTHIxdEl4T0ViY3JHLzNvUlFsREhwcEpoN1VrNXFXVE1CeERnak9nUVUwWTBPVDhnbk5ianhOdTkyUGZNZEVPa2VNUitodEVrN05ORUttL21tYy9tZ0dSRXRwWlQ1bEtQU0JDbkIxK0pDTks5dko4cmpKTkxsTm5ibXcvMGVMbHE1SW1hT21LQlI0SnMvUk1RME9malRqenoxMncvT25ZajJGVklsWW9seFJyaDUzL2o0SGdBQ0hjL1ZCT2tUem5rcU9uY3A5bGU5NzFPRzJlci9renBLWml5N2lidkpncmh4VVNsRWpGTmg0ak51YTF3RnZrdlZHQU9Ja29aN2ZNYzlkRVdpenR1TkQybVhudFVnUW9JSU1GcmZ3eGQ2RDFWS05aMDltNTRBR0RpWklXYWdGN1dJSkNSM0QzaHB2MEJKek9xL3Q4a1RvZjNJeHhJdWtZajVQMkxyWG9NTTJRb2xrL1NJeGpjSW1EZGZncUdkMEIrcjI5U2NPOGpxVUR3VGlyVUJKamVPWUtUMzRyTkF4K1FDaGYvQVZ4cXNSakk0cUFtYkJpWWQ5OWJpVlNwNmJQSE9EQlFSeUtjM2pCNkRvY015TUN3ZjFndmh1SlZ3bXhzcEc2VWxDNzVTQU5Tak1qWGZLS2huNGdvRzRIQnNrbG80RXJJMGJPdnBIOFFyMFh6d3lmWHhORzBPdENxTGNxZUplQndlZ2laTU9mL1Q2eGZzU0VpRUlld3RYTFkycmlkUnJVOU5UUWNoaEsxdzRKZ2JkZTVSQnp2WWw3aFhQVFowR3JKRmFtMmFVaUVjZFJMc0VpSkRIU05YMWc1STNEUjljYzB2K3BibHBVbjgwekg1S0h4OGxvRENyMEk1UnYwL3BoQzhQOHRhUVpQTk50d1I3ai9MTVh0YXd2VU0xQ1VQbnBacDM0WldJeFJGZnFzV1J4TkRjTGZxRGZ1YklZNXZSWmRjV3kvYUJ6Rndla2hJTVJ1RUQ4L1hFWjB2ZzNaNVJScXRpNzF4b3E3VU5NS1g4MHl4OG94dGs1M1VjdXJJcElWbTNaRzQrb2hSeWQ3WGpSZU0xMFpzTHRFd2ZjYkZ2NGZmOVpmSUs0cnBFMHZMdFN0VE9GNHF6YWVjcG9QNmJMK0ZPR2JrSm9GMnJsWDFwNWwzL0pSV1o0SEdmTzV2VE9RTkNvZUt6ci9MdlluNnlHWUI0endObS82U2tUM2VBVDhEdXNYL1RlZmF5bEpNcFdMR0kxMlJFZWw0dkhtOTgzYlVudFFaMGZDdGp6YTZ3QVN4ZEFhdHJFVXUrSldKdk1pTkNZNXZPUWZLMUorb1lGV0RTMXNlVEw2ckhva2p4RWpqcit4Zi9QQy8wL0E0OTN2TEJxWTFlbmVvaVZrUFNjaHNRN1pCN3h4Y01LQURGZjFBWlFpaTkxaXJnWmdseDgwazdkOCtQbjM5cWFhUGxCM3JZZWtiNGNUOVFicnNJc2pVeGxydkFkNDMwYWRkc245b01RZHlCdUhJSENCZkxwbld3STdBTDFwcWVSbFVPV05xbk5CakpnUTFYbVlGdEhVUU8yc2JqK2RWWUcwSTZOelZFdlR1cDJqRVlZTG9nQkhlS3ZWeVhJWjM4ZnB5REVmVEVuVmw5VzhFMzRBUXBHOWlzVDFSR05IYzZVVDBWd29Nb2tlakp6dkJLNGpIK0ZjQmdCU3JsZkpKekdjTU9MQkJZaDJSWkdHMEduRWNwTXVHTERualorMEhUbXhORG9WQW5hN0pWWDdzMjZDUUw2RTk4MFAyS2hNSGhDOXkxOTdQY21KVUJlUFhncWFRdDZMVVlpVDRkOE5jc2dkT2tuZDhHcC9JWDFidEc2L2xIVldNTCtwaEJNTnFWNjdIVUZUUUZZTUFrbHluUVROajgzeHU5NkI1VHh3cVNXOE1jS1BRb3lrWjRKVVZIdXNWRTJlcDFNa3hCVWFlM0h5YkNXb2RlaWtDZXpIa1A2VUYvcDF1NHd1WklOT0JValEyWkUvOGhTS3BVRllRZFlLYmE1MEVvaVcrWFpxSVhvWXFmd3JNS0RuWDhtWTV6eWlWV3dOa2lIb215U2NGU3ovSUNUV0cya2ZTbWdXSk4vb0hjdFhiMkdwQUdLdVM1Y0R1ME9zVzFSRlZKbHZVZ1lEZkhESzNCVDFwcXJNWFhjNjkwUEFHVXB1U2JyWWdlcm5DcndBUlVTMnFQYW9vTWtpL1dIaTVWVHdPaW5QQ3MvSk5pYlZjVDNyTDhOSjBSV25mbjJiTkROWE91S0pkSDhicGFUT09WcDlVbGdTY2I1TWN0Ly9BZ3dVdUlWWWVCKzBzSVh5N3htTktSNUpEcllsVHJoZ051T0JZOWk0OTZ6Wi80VkY5TDZRSVcrM0tjOXdWVi8raUtiSFcrNzBqdkR3VE54cmUxazJtb3dEdU5vc0ttUUpBSFlDZC9aUHhCYnhhOXZrOFdueFpPUCtCa2F0SXl4bTFFdUVwVW5UbnExRWlqUW02aHdVckhGSjZoZi9GOXRITDY3bWdwWVd0dXNyZTc0Q2hSaUUwdjBLWWZTcFI5UlgwSDA4dUtvZzI3VXdBSFEwYitCNFNYaUQ5ZDNveVp3Z3RTNlhKUHNScUhVNTRQY2xMbnJSRU9BUEJpbDVEZVZNU1Z2bUllWEdPUWRqVUxnUzJzWFJmSW9YNThFc0g1cUk3OElqVWNib2FyTVRnUEkxYWZ3dzdvTUh4MHFMWmZseXZzYU5STFVhb3cwY2NtKzlWaDJvMVJmS0RxUzlXdG1VS1hLSjNMMTcveXNzSmd6UCt5NEQ4OW9ZK2psbmJQS2hJb3Vlakoxb1NlS2hvYWJCWHlBSWVmWmQ5ZzFFTGpjcUdlNEN6WjBKSTJaYktSTTJDdG5RQ0Z4RmorY3pUVEw4UHBjeWtGQTFKZ0l3NnB3QVRzbi9lVUJ0K3FHWjJWeXJBZHhRems2SFlDVFEwODVPQkcvbmhmZzlUcU90U2NQRmVkUEJTV3MvNE05bVBkcmVFcjdFZ2M2SCswamNQUEQ0SXJTRE03djhYZTc3RVZWWncyaHJnMm5FUWJtaW9nbGFtUGdPTllpMFltU2UvbmR0T0RMQ01nZjF3TTZuNVNzUkQzZmhORVhYYTRaNllkMC90VmFtYTNvWk9PWWZkcDZRT012K2MxYVlsRzNtT0I0dkY2Uzh2RXE0UEFoNUtWWm44MmlzWXBEZkVnZzBkMG1iQllIa3FXbmhha3FVL0ZNN2tWaVhMdUJ4UjI4a0hwY1EwQWVUVlMwUk0rQ2lleHlnd0lPczErVUJrQWw4dDd0Y3dXb1FVK0trWTJtVkxBRDNrM0xLNXliMHc3bDFmRjZ0alM1Sm9XQlJQMlY0c293b01ZdzQ5QXdsTXNpY3FBZVZtUis3Wmc2NDZ0V0lhNmFwS3NXUDRSeFZnWklRMHpJRUgrYlZlZU9nTzEzSk1KUTVqT2lNMjRReU1aTzNlc2lVZmp1eTF4WXBzbFVpVnZ0cXp1MEVwMXFrMHdpNDZJOUxqTkFxbDgyTlMzMGUrTGhNYzl4SjdzdnBFVzY5MmFHUVRWUyt2Tm5sMHczSG85ckpzbVFPOEUzeGt2c0NIQnE0Mzh1cUo4Q1lWZnJEQm1zRStMbkZzd0NMQkhJTWswWXpGbG9wMW5tNC9SeS91OEJMaTlvd2tVT1BXS3dMUXNLZkpUODBRNEtQYnFvVnhBekpnd21rc1FxVnJiRzVnVmJIQlVlVjdNSDVUdHdsZmtYUlYrZWtmczJuSzhpeS9ISktGaTI4Y2xPZktjYUZLU1ZEQTBnbXl1Sk9zS1VQcHZDUnF2TUREZmRzeUF6dlIrVjIrd21VYlpEWkdpb2x6UlFZVnZNYkt2dnp4OVdTSTRjLzBObEFSdnFOZ0NNQnV2OXJlRDB5amx3RzAwMENwcmpXbXM5bXNtLzhHL2t5Y2NoSkE1enVsdTRjOU5VcXBtU09aOGtVMWJ4Z2pMRzl3OUtBYitRLzJZbkwzdWF0bTNNR1VWNWdnVWNRblVpQ3lGeUtkTjZleHhhdzltU1phTFdnam03ajQvZ0RZUU11UHNpL0VWQWc3M2tTcGxqSEFuL0FBS3hIcWVmb2pHN3orYTRzanlmRVo5a3A1cWdXQmtiek1BRmFXR29ZV3EwL0xqNjRNYU5rYWw1T0MrMXRmbHgvTzJHMFJvaEdqQnJ5NVVYUFdmTEhJc3ZuRXY2MStnWm5JbHN3dURSK2tpaTB2dkZuQS93UEJxbjJ6S0ZPazVmN1dNbjN5VnJROHFlY0huRUIvWUE1bFdZWnpNc3dsUzJzU2M4Z2d2ekNYc3NQOGtHcWozSzhCdmFCNWx1amJNN2hTeVhDbW85WmpHRnczWjJZZGNycGNEUTJVQURMamU0MDI4M0pRQUM1d2o3eXR2Y2QxK2RhcUxncUJhSm01bkpoM280d2lqL1p6cDBnYldjWjZiT1R0RHVyVkJKSmRubjBTQ2tQOW81YVc2ZzNlTlVzNzVzdFJ0aDBwOSttMnhZNE1Kc00wV3BJbmpaZXNzVGFVdU81c1dQaTdpRTVvc3Jzcmt2NHZqdjMvdW9ndVA0VFhsQTZrbE8ybzB2NVQ4MDVCQVVkYjdPVklNaDRrNUVXWklKMFNaTjk0NVJHVU5UeWFaS1FBY21EcDRUK0hNd1RaeXBlN0ZTUmkzTVIwNTJ0azVWRlFuVnUwdXJYalVuTC9kTEJZaHZkMkt5RW12Y0Z6ZUIrWW1SYlJ4ejIwZjNwOTFBUmw0eUJBQjBkZ0hHcjBZRDFNQy9PbWFqWnp5WjEzU01JNFRuK1YxMzZjbWJac25MTXpHNlgwVk1KSjVMdkNxaHNWVkl2eDR0R0pIeklONG5xdk8wUVRGd25xR1FNQnY5UWVqeVFHMy91TUZiOUl4ZVp5Y0Zab0J2SCtHZTFsejRnV1ZSVDNXalRVY2ppOCtYZ3VFY0h2ckx1VFc1YzlPODlEUkxlSitJaElMQmt2dE5TclE3bnpHWVdVWXdMTTNnZlhqSkdRUVRWb2VIc3F3bUJTUEtjaSt5RE9Kc1Q5aEtQMUhWZkFTcHhmVnR4dUlqbFNxZDdFK2x1R2thc3RxeE0zcHRqWmZ6aC9wSCsyRWlQOHlHS0lnbzA3K1RReEgzdHRtTUFWYUV4N0FVT1UwQXp1M2tpS1UveXh2b0Z4NXFUU3lNOWF5OFRscS9FdE5idU1lQ3M4aDFRb05rZW50RXF3YmM5dk42Tld6dUxyeEZENHYzZmluWDFtNTl6bzJOaTZ1cHBnOEpwcjY2UmMvNDlVUlJQUmpNaWZWVXlpaVo2Wkw3RElacVdYYk5pK2RoV20yVTlkY2cwcndRSnZqb3duL1hOaFE0dkNJZGt3aHVOSWpJUUVmVmlwSkNnU3dMc1N0d0RKdUJvTllvUE1ERFozSnIrc3Q1RC9PRmRTYXZnc0pKUlE0QURDdkx6V1FXMkVwOXVGMHBZamh2ckxHcWFYdHh2OTZZQllSWEZqc01ocWFXWk1VZEJUU1p2Y1dYdDQ0UjlFdnZjYThDQ21yME1STUxrbVpwazJRYWRuTjVlSTNOKzRRNE9HODAxSWZJbXFiNzFjZzFYcEtFdXR5Y1prVWJLU1ZNNk96RTBpK3BRTDBtajhGcUdIMHV6RW1oaU9FYXh4TWJlR1poRXdxYVlxdytYeVQvWWg1dThXblFWV3Z0TDRrNDdnb1ZLUTZIUVBvNGM0aWx5ejU0MHBRZCtYb3FwN0NFTHdQTVc3Ny9MUHUxaEYzYWYxaFN2MllIOVZBRDEvdnlISWUvT24yZnMwdDk5R1JUdklka3NxNFF1NzV4LzJsbDY2L2lZaGFNNEFkaE9DdC9QNGIwUHREbDF3dHY4ZzgvVVBsT3dQRm00U0pJc1NOUzVtWUI4RzM1VzZrZ0wrcFM5WGFNVUNVY1VFN1RwU0dwWGZMcnNhQ2xkRy9mUDlYakVCRE51M3dFQnZvQnllVDdhemhzUCtvcW14V2dGWWtIWWtLbUZNNG45TmR5UFV1WDN3VkFtcGcwSmFaczZVeDhxYjg5SjZrRDVzS0tKT245dFExRXBHakpIMVRKTlpTb0dQelhKZExuRUZTR2NTRG5WTnB1eThmUTJnUVFseFBjc2NiajhWbDU1emtIWlllUHdWTjA1WnQ5SkFVcWtYdXZySHN6d01kWjFib002U1MrZGsxd2tvUG1xbTNvUk50WXZTV29STmNhQzBCSzZidENqYmtZSkYvd1UydFZadGQ4NnU2SGRGZ1lrSzAwNHZBMUlkRW0zMTlHMjVkb1Erayt6RFo4WWZNbStyam1IREs2MUh6VFNneDNTNkdrakxZL1lhU1JQbDNjMVVEUjM4Vm9KMUJOd3NJZWEyRGJ1WjRWd1YzNjQvc3d2L0N6dXVFQ3pMMExucnVzOWxmcGZ4VlJ3VDJoQmwwRUdWZVNJREp1YlZEYkIrUHBjY2xqYlFSOHFYemFmdGVlZG5SbUhJSDZqcnpWSVNDdEkzNFJUOEllbUxWRFVFV2prZXBRQmNzVVNRN3lnVHBNS1dzbWNkbVBydjNvak9MZWY2Uit0SHFwVFNvU3VqRFRIZ3EyQ3BZY04wS1ZFUDdIdGFOMjI3Y1hJNHNWUFZRM2JhLzg1YmtRT2Nucm1HQjdMM3VxVjRZeDVyZDdOTUtEcm93TWdMTDZSU1VOYTFiSmsxU2kvMnBRc0dNOXVrMkNTbWZla0xIVGFBWTNJaFUrV2RQUm9QYTlqNS85eUxXdEpBQy9SMVNzUzN5K0EvZXh6UUJCaWhIMUc2Zkwza092TnA0UWJuRzZROFdiL0NHK2NSNTFnREthNVVXbnZTbjc4OUVCVERFQlBMa01BOEd4MVY2UWpjZklESmRSZnlUR3dIMXQ0NXFBRTRJZ085MER0bTdxK3BGUDNvazhiR003MkNtS0ttcVF5emhFdXV5YnZ5NDFiVVUvS3VkYjB3c0RBbm0zQXZlNFJDVUpiK3o0cTJFaVErSzMxdGdMT01SQWpsaEJURzAvYlF4eEJ0M2t1ZnQwSUFabnpTZEZzVU9NcXRmRkROSXZrVDc3ZGlVWnNKallYQlR6MWh2czNxeitCbDFwZUdOckR0SHZsL0grNStHaEdJMUd0OGJlNDhOWkFhSit3b1JXcE9FQllBVmtJK25YRExVUGpTVUkvc2s2VVM4TklmcS9iTlN2SmIxSGRYSHFvSmtDUXptcGc0MHF4dG9ET3RIelVoZG40QzVsVDQrMEZIZ3NUNGlNVUhSOFZFZENwRVNtSHlhSHdCR1pNUjRQYkVYTXVac24yTkEycFZHLzRHYmZMdzg4ZE0wQlhNQm11U0NjM3JpOUdJL3p0MEpqbkRVSDJscDlGSnFzNWVmblBqdk80WEJvRkRiMi9IT0J2R0QwWS93U0VXcno2L2xwbzZzczNRQkc1ckJWYWJnNXdMVzh5WktaYXlwZUxaK3VuR1VZVU9YRDFQWWpENXdVMk55dVM3bFRqd092b0NUOHBFOGZuVFc4VW8wUE5mRFBvU3kwQVJpWmpPVi8zN1BDNitrWG4wbzFtM1RtWUZPV2ZXYzROWUQzc3lvMVlrazU4SSt5S002TFVVRHIycmJwRE9NWElZK1NQR0JMMGVxbWIraVNqYTNwQWhXMG1icll1ZlN5Y0NWdnM1V0VqQlNRQTEvTEY3ampTUmFmQ0tiTkVpOHNyWFgzQ1VSS0lxeUhtSWptb3Mwdk8rY0licUNvTFFjKzlYMUZsZHpXQ0lJVGFZSEtaQ3hBdEV2b01OdWQrZmJzR0kramgySDY5MFNneVlkcWdJQU5HT2dxRXB0Z3V6cDNxUWJrK3paWnRTTjlVU2x1V0hUNzg3QUZEU0dXNzRyZ2NHWGlQcWI4bWdQVjlKaUttVndwKzJaWnhBZm5EbFhPNUpFRDVkYUs2WmxBVnNpVXZzaE05eDdCZ3RzZ1JGUlFVN1hrc2Nadlk3dDFvQ0RJK1NHNEpPUnpRdzZxWWhFMkI0ZXoyT1FaQ1lBMGdGczZLOEU2M1QzQWdveDN4V2k1cmJ0NlBRd2tpZEUzY0s5cjV5YmpHSnVxM01Tb2hOZWVqMlVtNWJHRmkvUW9wSDRKV21Vemk4OEFkUkl4aURLMmp5RisyNlJGZWhLNXZSNEZWbUhrbHhzR3ZURmR0WEFNU085b0tpbzBSYWVWYytaa1pmRmdDelN6dDBxNTNVQ0hPRjljWlVNdTQrckhZc1l1NEZTUDdrbitqYmF5ZzhzS3RMQnloa28wa3Y2cHhJZ2pEN1V0Z3NFT0sxVy9HV1lwbVRKeHRpa3JJbGtLUTRTSjlzbWhxMjZESXNLZjRzWkd4L2ZGMEhReUZ4TG1nb0M2Q2NycGpwZTNPMldZbk83WkZCdll1T3hRUTdkNTYrNkxCc0FtYnZWck1CamxQaWFSMERsclU5MUw2UnYwZUNqUGNtMEp2ZTl0dFVuMUdoTU14Q2VwRXJyZjN6d0gvWWRVV25HRUtCN0l1Um0yZklHVExJRG1KNjcxc2NPbkd3V2RnL3dpUlFIYkdYWkluMk96RmI5TG8rbmxHRFZaYXg4MTM4bW5MaXRHY1l3TnlWR2JzTm50b21FVFI2a1lOWHZ5cEFBaHdjU2FUNkZEWVBCTlcrMXZGRDlLbXloMktGV3A3YVJYcTV3VmhmUEdKZkZNRFo3S2hPOWNtQndwZ2w3SnpHb3Rkb0hwQnY3Z3NESUk0UjE2d3lSQzhURFk0ZTFjN1lMRi9FcWZGZk40RG1aRjAvM0hQcUQ2SnNLY3BDVmEwTUNWSHZ3eGRhUDBKaGdZYVI1RGxDQm5lenFlc0tYVzFHd3VycVQ3a2hmcHJ3NUQxMldqbW9FRklmM255d2o4Nm8yNUw2R1ZlQnQ0QUVBVk15d1k2ajNZME81NG5ISk01RmJoY3NBVXBKUkdUVThKRGIvaU1yVDFWKzg4S2s4NXg1amlTMU5qOUdpaVIzTTdBMzNuNG5VRkl2akpCMURYY3ozZHNwbTNsdXVVWjlsNVNiNGxhT2w2RXpUUElubHBiUWtZVm9UcnJUY1lyVmtpOGRVamV0R2ttYUludVRtRmR4eVQwaFZrNWY5bzRvWS84eGVlNlBEakQvUWhqeGNQQlUrY0ppMWNoZmtCaGNqYkRkQjJmd3AvUVdjVk4wNy9tdFZGWEhyK0tzUVNyVmM0ZFhOOXpSMitOZkZjK2xnTEluTW00VkdyejFvZUlmTFpXZ1VnSVI4TkF6YTVYWlBWNDQvTjNCd2dlSTNtMFBRVURicWxtSFl6ZTA5SVlURG5UVTZmQzVtd0hzRyszNi9SKzdnRkQ0dEo2ZjF0YmF3T1NSUWJQbFl3amVCSnYxVHdvOFdrT0tHODRnRjFrRTloeThraDd0NFNaYlViWkZmTG5jNjYwdGd1NTQyczNseGJaOUt6eUFsc1dPMzhrOVQzNHFrRDlXcEFxYU5KYWRibU96czNJYUFKYUJQYk16a05Gb0RibjQ2NmdUMVllZEdMcStXTy9BVU9UeTlDTmhOaUUrbUp4MkljT29FQmdjTWlkRkh3UjZvY0NGdDdvazlxRXZmSTBSRTdWbjVxRFhxbnFocUkvcXhWOG1KVUtBTVBvM0VEbTB1QnJYaXFsMFF6MWNpbjNJdzU4UVlrVlc2ZEsyUjgyK3J0U3UvT3NZRnNRM2w3ZkpQQVpBWm5yUjRUbGNrNzFzVWo1VXZEMGtJNDNRNFc1bVJURDJna2kyL3FuaElDQWdJRG0rclFBNUxGVVhzNXY2R0lNeTROYzAyZ0lEVlA4TEJKVE03M1lDMk8zUnNETzIxbFg4bzUrZ0tYUlMxU1J6SG1SREVMaG0ra3cvZkw4K3BpVW8yUEhQN2RaU3RQbldSbTBxSG1BSEZSRDZpWlNCRFBIeXQ3K0lZcVBoKzlyWkF0aUhlOVlidEhEdURNcHB1YWMxUkxBa25vYzEwTE1ZVzdlMG9NT045YlVXOVJtQ3NwU1dZckIvSDFBTUFQMExOK2h5ZDB4NDA5andlVlNFNDVMT0U4ZUJ4NnV6eXM5SVVTeDdkRVlLZE5kZWZqbEdicVdidjI5UkxmbnJNUHFyM1NmaURIQXJkTTVScHVnK04raVVpeVQ3VGpoUFV1Y1hsRmFRRE5kcHFCdkNRcXJsNHg2WGhwNzlrVkpYeXNBSGdxdGljNjhhYXFneEl4M3BDWkpVV1NVOGJCQUwyVFlEZ1IvM3BIYllIQXRTaHZGSEtWYXc1U3RTaDdMblhjTWVZZkIyYkVoSHJOckFYWEdxbzk1UGdDdER5UzdaWlo0TVNpeWlPVHdrOHFJTXUzQ1dtMGNpRmxLSisvSXlYZHJ3Q0MzTjBDeUZTVTlBdXBpaUZ6QzdIWUI4dTBUbC9pVFoyZ1pKc0JZNEtrZitiRkhHTVdSWCs5Mzg2UUF6WWdqNmhLVXkrM1FnUFlDZFlHd1QycEE4c0t4cHJyTFJWMmxrQmhZcEgvbUw3bWt4L1dvYStMcHF4enMrbDE5SCs3M0VVeGVwY0pXTUJMRFVlVHFnUzhjTkIrS1hxNzViMDZvb05ldTE0THp4d29hWVhxQzlrTWZVMTBLL01YZWJIeFczUVo3UTFtSnhuazBsamlRUWhGOG92dnZJRGpLU3lGUWVCUHRBNUQrN2pGZUpCTVI4NnM1SkQ5c2swY1RUczZPcjI4Sm5DSXhjNUU4RHQ1d0J6RkprUzBCQWY5RXNxZ3JyMmFPeFhWa01KaW5JVEQ3bjRibFRreWRwdHdFSDZvTTJYcWRjNHMweFB3RVc2eWlaTitSMkZFZ0tQaC9oOE84OS9pSkVxYUZXWFM1ay81ZlIxcUM1b2drSGYxTDQyOVVTS1RsbzFIRDl6bkZ0OXBVSHVsbWw3eE9WLzZFaG5ReU5iWkZLU2JGSXdiOUdTU29IODJYbExXUVlTMG9WNXpuRkN2ZmZteklPQVdhdXdocms5NHIxMDR0UXdxeGx3QzFvMGpLZjJ1NWR6TWpsU3ZYd0FZUEdTNHJSTXJMWms5elVNeWhNcEkvMlJTZW51SlZrWk9XekoySTNralFTTVhqbmdVRE5sRk9xbHBzOTRaT3krTWs3OEFUTWplUjdzeUJqelBuNzFUOHAzMnlrUG5rT0JjeTYrTDBrc2xPOE9VaW5jajI5ckJhRmplVmszVlBGL3I4T3ljc2ZlSXo0YjVEMFFWZnE4Ky94b0ljd2ZFY3pnSEhKcmRVNXUyRndCbE9WRG9JOUxTa3VoWmFyKzhvQ3lpUnp1eFpJZGlETzJTSlV2c2MrS0pkQTZTUnpZaDFxRUFuOTVoYml5NjVQOU5qSndVR21XQlA0aExaUGM4WGZkT0hmK29MQ0VMdWdRNXl0ZEVFZTh3UjZjU2JXT3FuUUVTUTFaeFhYcW56T0pnSzZMU1B2SGxaVVk1Q0dVc3E0dWgvS08rVUJkcFZ6Q01rRUowVGFxRHhDRXp4Y2xiTU5sK0I3WU5kUVlKbjVIZTUzbkZOVUd1dXkyTUNueENSaSsvL0pHdkRVRld4T25hN3I5c1IzYk5HaU9Tei94RE4zSnF3NnEzdlprZ1MwNEY0SVJ5dDFzV3F5eXpWcURmQjYxQlNxaWUwSGVmK0RQMnRhY1ZvOE4vRkRYcHhLR0VKcFNMS2pvU0I4SnVVNWJ2VDYzc3NqY1JSZTF0ZzU1dUtCbGNTUjUrMXEvOXo2NmF4TnlUcTdjeSt0TTFORFBtYWhqRDI5TVJseE83bVN1cERvc1dKQUI2S1I5TFoxNC9CbkUwK250MmMwcTFBMjZPZG0rVU1RblduejM2Q2hwTnJOMmd0UWROeExLMkR2dGJkYmh4aHpsTkkxdGhraldKZ1lIWkhVdjhZL052cTlIY09jQzZpaXV5UXM3SVNVekU4bk5qT3RpdmZXNnE3TTJmK1lXSXAzUFRjbk9RajB6OVVuM0ZGcWhwK0gyRDI1eURYbnR0QXVmMmgxRDVZZ2lRaWw0MDM3a01MYWZGeEk4M1k5RmNoeGlaWjlMTTBhaTc5bjhwZ1EzbGtkYUVuR0xrb2d1N3kyUkVzWCtRck1lUW1nVUl4K2c0YnZONHZjTzAvRUxvZ1dPTThpUU4wWWl4bWJrYk5kTzVjL3k1VGdkVG9YcVdMbm9PRWtxV2R6ZG5penJPOE9FY1Uwa1R0bW5sVDQrY1pNeFNPanI2S2pTNXd0WkdVUGhOZEY0eTRmQzJRWDBLQis4YVZSWG1aZ2VvYmRnU3hjeE5pQXJTWmM0eHB1OGIwRzh2Tm9iSWc3UUlIdjRZa3ZqbVo5ZWk3L3dZcFpvTko1TzJBWFlCVHBKcHlmUElRTUpaanVaQXp3OWp5STBUdWpoTWptV0N6VEJIRGh4NGtmV3daLzFmcVlRbEkrak9NT0lCUUdCYjhITmk4czRQL2JuUzdVTjFld0lycmplN2dtMm12amhHMWxLaUIwbExwZDA4SGlqZWorRG9XZFpZbitWcjZrcU5CbmVEMGZNQUZ5RmRJWndZUE1XUkdXcEFQRnE5RCtFVWgxdloydGM4UzVJazI2ZC9Sems0UlRPa3N3N2pNU2ZDNHJiNHVYa1lwTG94SzVIMllXd3hTL1hsWHowblRGaWFXa2NQcy9kWGF0SzByWGRhNjA4ZWVvVzZrZEE5ZUVyWjR5R25NSFpvbVBVNk1hYmIwVlhCaXl0bGZHT2oxK25NZm5QcXBuRmp0TjdDcnpKMnhNYkFiN0Y5S0VCTHkwaUZ4Y2hiZEZ1QUhaelcrZDJ6bGZ6S0FIL0U2UWF3bFR3ZkpKMFVoRDZuRm5ZejJ5aEtTVTdPL0FCakhZaTNwMGtvZTdGTitFeWN5cVB1aUljSTZOd3N1QlBmaXNRQzRBd3lFWEE0eGNGM3RGdUQ0RzNZZ0R1NzRmWmtqR0FOUzdrRkxGQ0E4dnVPL0FKZ3hjR0pLV3F3NUUwZ0psVDhmQWg2MkhoNzNMRDBkWXVEcVZkdTZsQTA4N082UE9MMzZTcHlDYWVaS0lKWGpvZFF0a2F3b1U0VkxrWGVrU2hKWTV0OE5naWdlSnVWVzNrVUNyMmRoamNreHE5Z0lFR3Bja05pYzhVbEFPWmJoaFhkS0ZjeGo5emxxTVNaK1RHenRnNHJQV3YyUkNjbG9iQkN4R0hGTzRWRzhudTd2eHIyYmwveXRpY3BpREYwTm5MSFJndFRZSkc0Vk00ZStVVlRRbjB2WHBMQ2JSTnlvVWJSU1BRQWZEWUJSWDk4RHp4enZJd0cvR1pXZGNuS2crRmwwV0w0QW4wWjRWNzlFV2ljd2JSQ1pBanVKUkZ4L2ExQnEvUkFFQjhsYzhHNDlnN2NuMFFNRW9ybGwyTFRzY0NaOFRSU3R0WUtST2NJQWdkbUJqTCtXUWJXRWZTVndVbFIyaVZFN2d3TDFiM0ZsbkdpRUtqTWcyTG1rR1Z4UzlzUGNabG1LeC9nNG4rSGhWSDFvRXVxR3JrRmRSSksrcnMySGdvYXJEbWJBR0ZMYVRTaHVrUWc4Z2FPY0F2ZlRBdm5yZ0k5R3RRdVdleTBySUNwdkpBYmVSRjZlMDk0MkdFVVF0MEFoQm9YQlh6cHd3emFJME5IT1IwaU1neGFUaUU2ZUt4ZGQrNXJVU2x0S3FHRTVZckMwTnlHMG9FNHpBT2xtdDY2cE93OHNPMEFsWC9ScU1lTjdzV2ZMTllQZU5FeFM0QVRUNXZjRExiQ2xpL212YlVLTVVDSVY4OVdpcmlUNTUycStBRmJSNUExcVVyczJRYW5zMTlpZ0RsOVNpeERJemF6SjIrV05pdGFNcCsvQTVxR3U0dFdabE9WMEZvZm9OanE4dFlmRnFuM2d5R2dwbGZlRy9GZTArWTlTajBPNjlJVWF2UG9ESkhJc0dEYmpEc2hSY0JRRVB3SkdBTnZSVlh6bFVXSnJqLzNYRnhXcDZQWGxlNjhqVGVPV3hDME5RRmhhTkVqWGhRZlJLVXd3WXVGcHlUYzFzWCtMMnlMOWVmNzFzblNNNjZ2WWNzMUVRTTRYVlNDSlNwSWRQbGJkeG93VjBNeC90emw0NjVFcnpvK21XWU9sUE5kWW1NS2h6VjNJTEhmbWovVGZiMVVDS1ltamxaY2l5OWxZbWVmeWl2alhTdHNPTWh0SDJCVXR5VEh1TDh2SS9XV0QvV2VYMlV3UDJHSjlqTUQ2MHY5V2xFK3NxL3R1RS80YWVmZnFmUDRXdUhoZ2pzSmZxd2pKck0rdEdoYUIvenU3ZEFoRHRoZW5vUWl2MzVJdXVZcXRmUFMybVJBdmthSkJnbmZma2dpZ3d2bG10MTJGWXMzTHpIczBFNkdMcFEzSHVrQWFQMnN5WU5NOVJ0cjhVaTB3UEFTaFE0L1dQZFNweHRiREZJT212Wkc1NlVYMnpvNmhWd0puR1VOMFBIanVIS1RoeS85dDhqRWxoRzV1T2NtWFJNOWRnSG9CRTJKTmo3QmNrOXk2Y2dLUEtPYi9Vdm9MUFhiMkRWczdSL3YxekxoWWJDNGJKWmM1WGZQbzFsQVNrUDV5ZEhXQzRrc3BZckM1c3BhRVFGeXNnSXBnVzU3K2Q1MWoxWGNUYlNybE9XSm5LYVBVUHJhNm9aV3ZkalJPREpjbTh1OTNXQytuOFF1RnptMDhmby9vcWF3WithTlRvZDMzWDlzdHQrYjRYKzk2dFhEaGdySWpobzNGR21udEFNVzVXVTIyck5lYko2SG03OHFkQm40MkhJbDFEQnVZdTllM1JOTkpWaGUwdmFFNGFTSVVFc05WV2ZwMTl0cE4wek5ybFJxaEY2R3pBOVpFUTBrQXMvaU03TEdkaUxIc3hjVElTM2IvSVRKYWp0eVR5VUJYcXVqRVprUjB1UWVsYWpscXhDd1NXaXJDeC9ueWpBK0lNNHZoZXQrUSt0S1hZdTVCSE12WWxLZ2hqRncvRFJJOXluWndqNWk2WTQ2Z1QyNklabVNHcVhleEgxMXlTNkx6QlJDNDJBaVRkUEN6WElnYUhYTDRjVkhEZzl3TlRpd1ZZLzFKQUlGaDdiTmtrdkkvUDlTQUlKNkpKWHBZS1daaTdXQlJzVmt6R0l6NEZQTVA4M1ZRYVQybXVnSjhjc2drM0s1NlUvVE00NmF0U2xvVk03YkE1dW1QTURzTEIySzZBaDF6Qy9RaDlsQUt2b1dBQk4xOEtKTTBWUTY3MjR0eUlNeEFnUGNQREthdHZ4Z1RRSEZpQkpnajk2M1h2MUlGc3lsQzlMT1UrMFNBeERQOUNsLzA0WkRHeW9UWXNQSHREZWpKNWVQaVd4QS9EQkd5VEJad05qQlpXSXFwSTFkRFpJRDdYaXlSZGQ4MU1sLzdkNHpxS2E0SDNxN2orMm9reDUrb201OEtpSTUxM0Fldm5xb201ZWliUFk0dlNVSEN4YnJ0czRDZ2NKNzExS0FkSXRWaVZIUGVEbXE2SU5uaThjYnZaU0RuOG9VSTg2bGx6K2RINDdZeHYvcmZuT2dCUFBtazZLUTUxZ2p3RU9LY1phNUl0VTNWZnJrOGZKYzBMQ2pCV2t5M0hFZ0hLTFlZdk5hWUdxOFJhbVpoRGFRSjVpN2tSS2ZveFIyRHk5S0p4Sm9EMHp5b25Bb2JlMGlVamF6T25LeE9LNzI0M2NqNDFTR3ZHUkV0djIveFF0YTNMcHFFMWNPYkUvNnhlcXh0emJid2x1MGJsK1cwRmJqNmlRUzRnUkNFdFNjVVJwK1YwbVRpelNCaEUrQlZTSTFWY2NLN0lBNjFZME9UbWZoeE9KUEk4Vm9jQXVhNmkvZWtXdE5mb2o1RlNnVlQ3d3g1T1hINWczYnYveEJGamFRQlV3eXI1SmEreW9WelduMmdNNXk5YlBHMW5QWWRVQXo5WHNuMGZLZnNQcXcyaDFQSlMzaEdwbkhoSXBsTks3emNqM1RWVUV2elFCY1FxSFhMQ2dBNDE3empTTVRzVHpzU0JibHpKRU5ISVFiVGwxU1FRelR3NFBsUnQ5MW1RZFVvNjJqdFREZUdFZkNnckVtbEIvSHBEc2ppbkt3aGJsL3lkd3paZ1htaHZmc0UvblN1SEtBcGVYTUtnRDNaSU4raWtuYmtQWmtFcTE5Qk9UMzV2bHRDcjlqK0RDcnFYYTF2a3RCaTFic254MmV6OWh2enZ4eHRvUWQ5OElwTWFJMzVJZlQwQUtYbDZYYkpOVXg5UFhvMGo1MzFDOHFWMjVBa2h6MklEOGdBcnE3YUhZVFkzcTFwd3Z5NWhkREZneS9YTzVwZGhzTnQ5UjNhcEZkOXVqa0t1UHJKQmVxaDMzSEROZW10bzhTdDgvU2Y1VWE5eHdONWRGZFJlMnQ3clNGbnJ2a1RYa1dCVlhNV3A1Qm8zMnhFa0hwRWJYeW1kcW9HVU9GWTNJdE15Qzhydm84SnplMksyQ1A4dXRmTkxaMThVUExMOWgzVXo0TW1PQVIvUENsLzZWemVqclVycnRtb0IvOEtRa1d3Qld1YVRRNWx6Y1FpQ0FwNkYrdENYbk1sdXhocHNBd0hiSXZqcjRDVXltalM5eWNvcHJIZFM5YmpZVmVVTWI2S1V2S3ZQM0hDRXFTSmZrZjh3Z1cydjdQYk9zeVVmUXJNcW53dkFlNENyTVJqY3RLNWxuUmhEK1VXbm4zUlE5V1cySkFhOUxYdWVzOHpITTFVaWJCMkJWM3VRNVMrZ2g2RDdGK3pzVnM3SXVhSDg2UHZTK3lxT0pJZzZOb0VUZGYzY3dFWVlTVWIweXBpTHo0MEpYYjNYQUZVMFNHYnBEQ25xUkRkbjFGVS9ya0k4Q04rOFhsZjJhbklPMW1BOFZTc1FibkJrdjZ0cTRMV0RISXB1dVBxNXJEN0RLYkVrbU1yYkNJa1R6bmEvMWJxN0g1K0tYOEd5dTlERDhneHJHbHBUZUhsSjhoYTJSeDBvY0lXV2tQTlE2UmFVcFVxN2QwM2hrRXJCS0c4R2h3RitmOWpLek80bUIrUG4xUCt2clVkeFp1RENCWGNSMlVIWjcydy9TdExNY2x5SFc2clBVMlp3OWJ3QlM3R0ROOGtvTDRUY2hJVHh5em5FMVRKdGlGWDdSajZJZmEyTFNJY1JBRUVuVkZsL3kyZXlSUGVzcU5OOEZFenRwQW9SSkhhU3F5VTkvRHc3bzM5M3luVGhsYUZQdktEc1YvWmpwOGdOSkNSZlF6bW9VVEFxYi84SHRrS01YVlhLQm83ak0xa2wra28xUlgzYTZiU0kxNnVoNVdOTWdwai9JMzdVam03TTlQdFhCdVNCdGNYa3FhV1FxbUQxS25ab3hwOGQ1Z0F2WDRUMVFHa254TTVFWGRGQU9FMEEzOW41bjVjOUVhTUJyMFRvYWZiNUZCVmx4eVNpTjhxQU95VXAwVUYydWVaTkFLMUZNRmhjRFN2d1dqem9hQnhDeVQ0YU5KVndTM0tWS0xFdEpWN0tRRHU3ODRPby9SaEk5Ly9mQWQ0dGZlUEJ5aTZNajdPYlcxNUkrV0xOK25iWStIRytMdkFvRW4yZmRjRlNJczQvMUg3MUphS1REbGVZMER0STM0d1grcS9OTzJlQ29uMGR1M2pRdDk2NW9HNXpTUVBSN1JZRDZqQUkvQ1ovSnYxWDkxSUlJbDZCOWJpN3Q1RGpzSjZrQ1pWQjA1aisrc1VSc3FrVFJxYWcyWnZFY0JWT2lDMGZVN1BIVys4SEJ3TzlYWFpzVFVpN3JieWErMkZ0cFRuLzM1MWNsZWFVYXI5MWl6RERJRW8vWGkvQlNNbXMvWEJJamRMeHlyV0g5UHJVc0pFc1ZzU0wvTEU2T3R1STQrdXVEMWIvQUtoMWhad2lWTm5xMnk2ZVJYVCt5SEFaVmxpN3dWcjZlSmEweG85Z1NacjhiK0FIOFJJa3Y1WVRTK3hCTzJKN3BWL0VZNUkwK3lnNEsybkdaOVc4enUxT0UvQS9Ea1diWjFHRkg1LzZqa3lwcHNrVzVmaUZyMG1mVnlaMFF4SCs4VXJVYnk4dHNlN3VkT3NoSGdkVHFMbHdyOVA0SGM0MEJCWkdwNmdpYjUyUERYcW1KU2hYa1ZTdDdpMDBEUDMwTnFEUGJkSEdvNDlaUTZmdjVOTVE0YndwTUV6OWFPQWhWaUxTbXdDV0VRUG00TGdUek9BVnQxSzM0dHlwQ054NFBXYjJ2WkNqdXJhNG52N090TVBGdUtFM1E2Mmg4T3VwdzJlTm1sUG9NYTF3ckQ3WUFCL25OUnhjcHZLczVMWHVEYnFSRmtLSmVTdTJMbFc2djh3OVY0a2VVSjMrZkdtRXQ2MGFBT0d6S1kvZm5NL0RsZU0yMmNVRDlpbmI4YUFvUGtZV1NBS0dqUGM2Q0hVQVYyR3E3ZUxYblFIRXp2MW05RktuQWk3eTUyYXFyK09pTFZ5SHI3d01SMjNscUVia0ZTeFkzVkNROEYwb2UzaThlelBDNzNmSnk2a0Y5Zk5OYlZxYmJoWjFuTTRaVmtHMTNCWFlZdVc2emJ5YUNxeU5xaHlHZWozUVlFcitEVkRvNWlUVWZpWXBhTUJQOVowdG9sNHBjNk5OZlAwWkVzbTllRllHWDVNWTFEQWxQRjRWbHpiYzBZN1RzbEx3QmswbUZUd1hSVFBLeTNYeHBMWmc0WklXTXo5dTYrcThmRExXVzMyRFBHNzdzYzJJemMxVzZqaHREdkdTWTh0bkdySWNZSktmVmFQbUxESzVJNEJYNTdpYVJqSmU2cDRQdlNVdzBwTlg1K2d4c0dMMmh3aHJaMWZRYXlDeGlYUS9PdG5FQ1V4TTBxSkQxa2poYVhEYmRTUUhNRTVOZlNpSmNjR2NGOVNBUjg3dHdSRzh0Sm9CQUI2WVFGRFRRVUpnUWZ5cUgycnQwcU4vZUtNQnJ0MDFhNFFJSDBwcjNvRmdlZzgxVndiWlE0NEJPZEhaL3dSVG1zYjFOM2plWGdhQ0dyUlMvSEw0cEpXdmovVzhEQk5FWTU4azdJS0M0SWxld1pPV1BQY3J1RmRYNDNRcERnN21xQ0dYSkZad1ptdnFaaUZ4d01YUFMrSVdtTFl0bGVHSEZWU1laVUIzNEl0TVo5REwrNy9MZGQ0S0ROalNkNTNjM1JDNElxd0xuOEswMXlxdnhab0xqYkNzenowQW1BUWxRbVVIMndnQ2VHUzM0WmpiRG9tUHhzNHNkeUlidFAvTTNXZ2F5L3BWUlNud1NTeWs5bUtvaXFkaGxhNlhPc2hyemMyMVREeTFldVUvMzBPaHlwQkhDUTZyWnZLRVg0cEtWMDkycFV5bkZadldObUNKZjgzWnNZckl4NTVBNlhkWnV6NGNDaFBTd0hlOGNxbVRlWFBWQmVSSTVaTXFHTXVuUktQV2xlbUE5bUQrM3FESHozbm5yVDFSZGFVbmNrVTBBbktnbGowNGIrakJZWUdWNDBLWkorczQvZEsxNytZRDA3N1hBcGpCRnhZMlM0WkRkZm0zOXYwRkJ6NTd3N0NrRVV6eStVRzNqMU9KaE5FL2kya1pqM0ZsQ2U1VU12MkFVSi81c0dKbnoyN3FUakp2d1ZzVU4vMzJnQnB0dUxqeFhTMVVVbFNTajMxYVhCbVQvMjZhMUc2MWRuUm92c1pRUnFlTWlQMVZuZWhKZGlkS3U5c0kyRlBGRktDVElld24yZC9rM3o3OVJ3YURhMDBJVXdyUU5KbmQ2dHZtZk92eENHZ3M4NzYrYWo5QnNlZzNtZ1ZxQ3Vwb3d6cEN0OGJuRmdUTWs0NEpicW8zQVdwams5NlcwcU5sQjMxZCt4YW1oU3B3V041Mk54ZXlySnllVnA4YVlCeG5EVkczZkE5UVNoVjloTW8xRHpwS3JURjQ4VEZtMEN2a2FuYXlISkZ2VThBLzNLalF3cEEzSXloU2hNUStLZVAzN0p2S09Rb2hQcmxhKzBQNi93S3pEUWUwanpEWTBjVVJxQVBlZms0WC9kWXRnb2VtemVrK0xxVEdob1UzRkRkODQydzYySGg1Y3dTMmlOdzV4RUw2UXdVODFwMi8ycTA1RWJJOTliaTd5dHVyYlp1VzJjS0tuUWxqU01tZUMxT0tyR29VdVhTRWQydTZQZHhTTHpWOURjV0dWK0xnd2Z1aFYzTXBySk9mTHFJODhtbGtjZE9MaHowZWdSUW9XYVhvYUZnbFJMWE5JaDdCMDFVYVcveTZ1eDNvN2pEOGpDdG85RFRuZkE4aFdvR3ByTiswYVVqV0F5cG5YVXRJWGRwbUVhanc5MkptQlBYNXliRElxOXBHZ2VBK3VTc0MxQllpQmkwTkZBSnQ4bmF2NTVKMlNVUnJvRytGdHRXYWRuQzJlMWowbzQyQ0pndEYrVnB5SkZ4OGlTRHgwNmM4VFpkSElpYlRLRTBXWXp6ampOSjBNRDRiWmtHbGZKVlN3czhUanp6ZXdmMkNaZXFXT3JxaDNzKzdEWjdCRWl4VlloeDkxWTNGR0dvODJZN09DbU5nYUZ3eU1Wd1Q4WDFtRjZqWTBua0pCWFdaVFN0cEdBMGJJdUZYZnczSmZsSWdKODlJNXFTdHpOTzBPMTV0TUxkZDlXZDlWRW11MFR3WmppNzgxRE95WkN2bG1STmNjcFowN0wxOElnY29lSlNSZ3hZcFlpZDJzNnFhNTBYQSt2K2l1YVVTQ1duZ0syOGlmQU00ekhrOVIrZDRzd2U0QnpCUWs1NkNBaEdRdDRjQjZhVi9mNloxVWtUUm9JTGV3NlhDbFJpY0YxSDJTRnhpaDl2SCs2M0dDcDZySGx5REhDZU40bFY1alNnUC8zblg1cDdXUncrTTk5MzhiWGtyMVNDZ3F1Qitxa2FZQ2wzdHNUdjRQMk93YittcDVSajNpMStmOEhVSVRRVG5MbG9WWHFEZWozRFIrMmw2TU1LNWorWnJ6WkN3SnVTYThxa1NHTlNaWlAwTHJSaEJndkxIdnR3cldVNmZBVDlSRFNuUXh3MXhJVmhTUXVSYlNJb0hHc1RGTTBFekpGZ1FRd0NLU0NPTEtUaHBEam9ZekpHQ1NUTWJsb2tsenk5THhndUxXeGRYTEpjVEJrTWJxSFA4ZzdTeXY4TGxyRzBtbUVjMjUwd1lOUmMrekxNdTIrTUpVREx2dVB6TkF3T1JnN0JDVnlMTnRLTzVXUit6d2RnZVNkU1Bja201VThIelN5eXVDTEluTlhpY1IvK0V6NmtYTGhwemREMVFCK2dBYk9YcGxCc2JKcGVTUExDNjhIeTQ4M00xZkR5Y0c2OXE2QUlHWXlaUUhRandkdE5Zak5aRGZ2RkdaUlRoZnNobFpDaFZ2YTA1Q1hWUWtXVkhjb2lrM1pIMnlhSHJQMkIvREVaUjBXNER1aCticHVqTTZnbEpxbklKVjlNZDZkdGZ2UnpiQWZ1UkF1Y1hiTWVCN0V5UWRYMlVDSkpBbnYwdll1dndnbVpvdE1RK1VORUNlc01adjJDWDNDYWZyTVRveVBiTHhxc01UcFdnc0thWEJhS3N2TmdiemhlUzVpVHBKSXdXNlczTE0vWWpxN25pMXY3cjNMNlgwR1lDWUIvNTZwaUZ5b2ZhRXl4cnd3eTFlREI2MEo2dEhmaWJZQlkvZ0w5azFoZFM2dW5RcndBM0JxTDBJNTJRRCtKRG5QQUh1Mk1hcFVBa2RsS3RJR2JtZlFxQzN4dFMvbS9JamNPVHBjY3g2a3RpeU9lQVJZRzJDRjVKUThlQmtQbVpOdm9BMmtZNmk3bVJYMUVqeVhTWEZqby9xTCtqRDdyem8wY2x6MDljQVNBdUlsNGdPWGxTaGFCbUZoRXNRcTVFcWppaFpxQ09yUitQWXpQRTBWOHVWRmw0TERGYmgrQVVIZmt1bElpRXg5QlZQdUwyTm9ScVhoYWJWNWRQMnBQcThQYUhwUTNzRm5IcS9xWU1hQjF2U1drUGVEbnpwMERhVWxnSXc1OWRiT3FKK1k1L3lNd3UzL1JMNGJpR2k3UHZuK1JNeWd4MytkMXV0VURvRGFKZU1ucW51U0puSVVjNmkxLzZOT1ZOWXAvWFF5eFZWRTZJMlZJbUlnWVJhd1ZMSHJtMmZwSU4zZkg0QUk2QSszKzZ4NmduNkFNTEZlTnNON1V1ZVFHZEJHOE9vemVaNFR2UmdmeFYvMkxJVXU3RzR4OHl6Q1ZVa2dkZ1VPV3poNXZXN1RHeXpBWTE3NzA1VzVtMDBZMWMvZFRMdFBwNW5yVDEyQmo5M0RwM3VuUjBuSmhXaWw3OGpCTWVuMlU4MFA1VCsrOTRxM3ErOVdDZVZYSnc3RXVOZjlDUTcrdzdHMnBJMU9NcWlBSUhwZEJVOVpyQlEzRkM4OWs4OVlMRmxOQkVkSWtMajc3VThOSnh4QWNKR3ZhSXlmVTJRSzJnNk1BTENqQnZuK2poYlM3RmlkRHArbGZiUXdJUENmaVBZU29yZVdnc3dHNEN2R2hTQ0p3NDh0L1JtRFJPU0hsSGE5M0pLMTgwUTkzTCtiZW1KVU51MUpOMkF6RldmNWJENFl0cll4aEJraGE3Rm8wRXRFSkt0N3Fqcm1NbWcrWW52d3FkM0Q0bFdjTzI3eUpkemNXN3Z0eHJVeEJxaFh6cklIdVo1YlhqaU5nTWJFRlZGVmxNaTFpRGo3ZE1WMEdLYkwrQnQ4VXVFSS9kNy9iMW8zUDdPbFo1MEcxZWowYXpEZ3hrV25WM1grNHFueVk1Tld5blhrM21jdTgwMjlkVlMzRFkrOWY0T1lEOUVzTXdSbjc3TkZiMVNSS2NIdEZXeVBYMURYL0FTU05vMlN1N29YYVdCdkI5TC90UmhXdm42WGQ1anAyQ1Q3YkZSdzFpY05jSVdmeWFsbWZVOWY5T1NMNTlsSkhJcm5NSHEyTjVjMU9RcCtOeTBoMmRJSTB2NHhlMEZwZlBIc1VFN0pvZmV1cmxLTG5SWktZZjlUMkcrUDU0YklncTgwbWQ0QkYwdUdZVUE4S2Z2ODdNWG5yZFV6MEora25GUU56RXNNekpCVENuZFhLaUwzbUFMTVg3WHZCZzdUVFNvMnhHeTVoTzF1ZGhINlJLcTRYRC9OcEVlTFlRVkdMRmpoL016bEFaK1BwZ2JzaDJEVXdOSW9GTHhhL1hCUlU5ZG9wUDU3d084QVhsQ2hVTjRUQTRwVXVSRzhRZm43TlNQdWUweGhuVlpPc0V1N0x2ZVpCL0txOWFFUjV1UktzeXMvTlFnOStSR2RabXdEdHlRWE9BRTFaOXZWRkU4Z25lKzNiRGppb2k2bW12RFRqMFpDblUrenhadEovMWlKbU16S1pYYm1SdmUzeW1YdklFdld3RGlMakw3VXdwbGtsczl6TUhRbFJDTFgzYzJxYk16V2J0T3pRNzF3ZlRzWE5hWEhmR1BBRkZCZ200Z2hwZXdjV2pjc2FxL0grZzhGK3ZpaXR4YTlBSjRCR1hrdWJDZlY0dlZoYmk3VmFnWHBSWUVpV3R6aFZKQTlySHVZU1VlMnBIMGNVZ0dhc3BTZDNVNGJDQVlzMkdwb0FXcFcwYzlHQWZ6YW4vandsUVQ4TmpDczExWmhpUndPN3J4SS9QNzJhRlFXd2luY1hJVzB2eG4wSkxCbytCY0c4SC91MWllSnl3ckV1RHU2bXBkMmpUTmU2QWhjelU1ZGE3d2U1T0xSdUdkUnZ2UjV6S0krWERRVU52aFhRN1ZGQ1RkcXoyd09EQ0FRSUlwL29HbFVyd1ZRKzl2ZTIrSDVOK1hNQ0tjcVhHWXVuOEdWWCs1anlIVkQrKzdlK2R5YjdkSUpROTJPM2hGT0dKZGJINzloMERBcWVVcjhJVEtaNkpqNktYVU5ONlN5UHNuNTJPb2c1bEh6dkM5RVBwOS9RKzhrUG02cHFTcHhhUEd0R1lhTDdNMEpBb3BvSFI2QmhoRWlLd1NjRnVMcTY4UDJmZjNzNitabUlXcEdpUUpEdHpJMHYvU0JUNnVMM1pTbXVtSkplVmhvRU1VeVJYS3BUQUhmenY1NS9vQXNEMFhzR212YUdKT0xLa2RzWERiWmZPWXBuR0FHdEYzMWtMUDhPUkdiQmJvOXFhdjNVNXVsbWZhOGliWGRKdmE2NHlmaFNqdG85Y1RPSlRvNUR6L3FZWFBVelJxWDQvZUlkejlHa3p5ZEVPbjBmbTRxNWhWY3pGT2N2Zm1lUUY3TFJtYzdlSDhReENralF4Nk9YdmpNbENRVGdKdXBKejR0WHA4VUZOb0NCYnVVeElOanh5N1h3MWJzVXpJeTNNcUlHWGtFcGlLNmZsa1dpQVRiWWRVb1Q2alU0TFlUcGpXd2FianpwS0dFK2o3TlVDWWFpOTM2c0JrYkJmejVJRFU5VUUzZUdaN040NFNOVUlYeFUrb01WZkUwZHMxSmZWakxtdytpMkR1aDdGYnV2NHJSdmpDSXVqTVFVb3R2TGg3QXZ6WllHR2NucVpWeStwazdZdTlVK0hnbUxyc0Zkdmx3eGcvdU5zS0k5NG1KaHYyOVpHa0xUdXRyWGsyYktoMGkwUnpCSmZyVzR0VXRBSnFkNWo3cnoyc3h1andVek83SjZRTE5oWm8vSGpRdUZoQUgyQnhsUGRJakhaL04rRlBIb2dmN1M4K0xUL1loay9wTERweGhaWUQ3QXJPYUdBdzludDJmWjAzSzlrU0FtQkQxRDF1dHF1SGg0NWtzWWZSK21CL3N1eFJXN2N1bGV4VjhDeUFXblY2VnB2alhNRWF6WGQ2NmUxNk1xZlRpQ3ZSUlVMUHBrbndONXRLU0E4VjZabnlCMGEzdUVEUHNJUXhkYlI2ZG9sdkhPRi9mU2JLQjBVUHV6eHQxbDdTUkRNOWpIbFdaQk1RMGhRQVhCWDBLRlFaczBMRGltMW14K2tnR0Nnc0lwUTh4cGM2VHZ3NHB2YjRFNVE5dklSQ0ZRNUNmbTR2MklHR2NsbzUrb1hubzhJMjZkWW9aTTRtVXU0dlV1blR0bXJNNEZMbW1PejM0dlZGNUpHOFUrVi9zcGM4SUxyUlJtazJDdVIvNEoyTHBUMU5ia2lyUUlCZWgzYWY1NGJRRkJuNVc5Z0pDaFkrTGlJbWdNS1J2dUR2M3crK1kvVE41alFBcWQ5aCtGTHNaejZ5NE1lWm5vaFhiZDBhNW4zMldNb3ZlVkh5WG5KTnZEb3VYZm91ZjJUK1JyMkp1RHQ2TGIxOS8wNEhHc2ZjQUFMODVoY3BqTGRWTmhSYTNXMlNhWjliam84aWFXU2lpYU54MnVnMGFNWnFGeE9RMEczTURweGRSblJmSU4xNG54RmowdUorWmpNRWw4Y28rVTJiVUE1bGxUT1pOa2lXRXV4MmVoTndQUys0ejlFYjhsZEN2YWJMQ2FlcmVOUkJCb1ljaEt6WmZGREtSb2lvNzY1cTV4eWNBbjA4R0pqUk8xeC84ZGFkRyt3dXBBb2tMVTNZd1RDS1pNakNHekpJSUdqOEFoR0NDTURseHhxUXl5azhuZTBFRWxlRkNxK0JYU3BzWHdoODd1MS9ydHNEVXFnV1V1S1NYenJsV3JkOTYzMS9xcVVucjlKaTBkeHNSVWxMY3lrNUJ2ZkIwa2RqRjBra2lNOEFmejF3Z21mTCtNSElndGNPazR1V1JubS9PdzRNSm1lVGtDTFJiSThWVWZWT1EyZDlPZEx0MGdZMXVKSEVNOUlDK2pwR3hERTM0aGYwaHdHbVcvWjR1YjRqNWJBMG9KakpXTklQbCtqMmo2VXBTZVNHbUZiQlorNnV4VWNxYTdKQm5lUmJQTzVSTWdZRWhGTGFzOWdaWkJsbit3VlVUdGJFRU1CVi91dVpZMHVLQ3RkZjNoWFVHeFgveDhZVm5SREQ1N2lqNFlSVTNKbFhWemVFd2J5bm41NXVuRHZ2Y1BzVkw1UXBaMWlZMXQwQmpqTzIzRnFxdy9nNmRVelNMdUc0RndkM216bDdwR0VINGkyNXZaNkI0NjlaeEdRSnNiQzdrTXA3cDlKcWpmNGNWazhBTmFsQ2RvZGFwcDV1L2gvSHZoUXVPQnNLMGZ1am84TnB3cEpvZ1U4dkdtQ0Y2cnNUMUxOSjkrUFNQOElSWTdTMnkxckhweXZlYzhrQnRNTzFyNmozYTVISmF5c0xqL2ZDVzIrVXlVbjBaRTFsUFQvbjBnL1FDSDVmZDQ3cHppYTV4bEUxNzBuek5ydFo0ZjFrbnBsU2c4WXBka2RKbkhxeUhZUlI1VUI4MkhMRFBZN2hUWjB0cDBDN2E4RHRKRm9mb1lRMCszZmhDRlpSWTMwTVRWeG5ZYXRtZ2gra2JyclZkV21adko3QWJOeHF2NWx4ckl0Vm85azZWbllEWHdGbCtTc1J5RWo2OUUwTG1hY2s3emFYVmZrNEVnWTFoTnJCMWYvd0NZbm5RdzZ1anpmVFY5NHVpeDlXUmY1M2Qxc0JFNW9qcWNNSi9xeWZVaGpLMUdiT29wdXJKdmhLNG5kNm1CVEQxTjdVdWgram11MFlobCtndHJsRHl0Q3Zkb0JXamRqbUFzU2x0cWlFanNaQld0OVRHLzJSRzlhQ25xdVB4aHc0WXBxWE5NZm03TlZ5dk82UTR4cjhDd0FuWXR4RnRidGd5TDhxQlJ1TnkrbjBxSXBDdjZidVJDRkxMaHhaT2IxUERHUlB4SXdXeWVENTdkNEtZQzN3eHlsMG5ycS9KcUU0MkdBY3dkTVBpZ0o2VDkwMW1qTVNlYW9QdUZUdjNZWCtNMXBKdnBjU3RWMXpjVVFDQytjVVkrV1FPTkxUNWREZjNMNTgxYmtMVGJKMlJDbFF2TkU5MWlvREhPbkN1dDhiUzZINGdKT0VkeUxpNi9nUUxhdU5POWgzYWpGUk4xVTQ0UlJVc21SSmEwODJMTHd3c3JkU08veTVPY0tCN2JRT3lOcGZLUHNVQlZUVE92dEF4MTVwaE9vRVhHeUNndk51K2lCNnlHeTdZczZWUW1wUGRLRDhjeGY2QmU0aUZtOWNWRmN6MHQwaGtTRVp3ZU96UVVuTFVoQTBqbGtud3hqQVprbGVvWm14VGUyTUgxVkl0U1VacExScnY1YmYydEY1VmNIVHo2UDZsQ2ZsMHM4Q2xZTjdrK2RPVGUxOGhseG1ib1lMNkRaeU43L3hNNGNlenh4Y3V6M1IyblFlRk5wMThVRmVyaEhKTW05N1lqRHEwaWY1N1Yvd2ZxaVZnaWdPTHV4OTZVL2FCNFA1aTE4RytKZldaclFqL1doRTZEa2podzhGdHRINmJKMHorQlNLY05ZYkFTZ1pjcHR4ZFMwMjN6dUZFUk5EcHkyWmZYTm1zVVZZc0FIakFVTWU3U25BcDg4UVUwYkRkaGhkRFZJNHJrWlZGV2ZEVldnZFY3dTI0NnlrVzVUSUJZVlRYWnJ1aEFaRCtWdThNRGhpS0R3Z1JUeGdYTGpjR2pLYjlNRXlmK05CNFY5aWsxSGw4VWNZTjhmbG52dTROM3JHL3U1bVpySnMxdTBoTVVlK1loc2hmTFNlWDN4Y01UZmFOMDBNS2oxMnU4aVRkY0ZlN1NBNDY1ZXo4aGNkYnBkSEx5aWNPaEdLSk4wdUQwVkNzaXVhYWdKa3RKTVFkSHJxeW5QK3g5Wmx5R0dyUVBJeW16ZHk0ZGdrZXplWU9EdWMya3ZYYlIxUUhtZ2VvamZ3Qm1JNUI4STM4cS9JVVU1RXJ1VVdiYmU3TDdmc3ZqQ3RCNkJpTVdTaTc3OHoyYmluSkdZY3FjdWd0aWhvV2JQeTdLSU50K2VWYWZkb0U4cXBIMzBmWkZEcDBRcXhuMWpDUFVLcmlpOGpNdVE4dURVejBieDJOdHVDeHY4ZGl1RFJsWDdWb1VPd0FNbTBROUdmSHBzeTZ5Qy9WSWpNSi91c1E2eGFoRWNTMW4yU3RvOGRrYUV5N05IdFBlS0drN1kyYmFjNEZkcytIUjlCTTU0WVpuZ2RTcWxJYVRIdVFwU3p5ZDRDckVoSkpxSkhhbjV3VGNwZElvZ0IwV2ZxbjJVbWQzRjdpQXJEblJFLzZNWmg1YlAxVVJ1OTJERlRHc1RmRU9YNGNyOFRqb05Tc1dGSkljUTdtOXIzY00rUEV0QjZ5MEUxTVJjK0pjc0d5OEJUSUN6a0REdmRtZ1ZXUTkzNjFrQm1Cd0RyRnRmTzlHSFJ6elR3emFQSzB1YXBnQVpVV2RUdTZwT094MGFPVThSYWh0WHhROFN6UktRUVNuYU1LZVUrTXQzRytDYVh5MzZWb3I0bE05eEVWWm5mTER1aU95RG4zSmZEc0hkWHVvTFlLeTduamNzU1IyZnlTRGNaVU92USt3cWRGODBSV1BneC9VUnZRMDhKSk9hdDNpVXlpS05USXRqaU1SeXNMK2Z4bFJBTzRpc2JaVW1TVkc5T3RPTS85a2FhZTliTDdwTjBiTGYvSkpMamhkemVnN1RSaVNuREd2U2VmSXc1T2tGSy9vTU9uR2lDeDQzcW5rTVJLZ0c4cGI4ZXVFK3dvby9ncWFTc0xVVGErY29RSlB6cUtzOTNvZDZNTzAyNndiWlV2TFhXbHE0QUJQZDBmT2pFUXdrYWZtSTA5Q3o0S2lXNDhBSURhOFpQNDBVUDkzd3JIWWZ5U1VlYTNHSkppWEFJNmdDd3Y4TUhGUUhzeXZLSjJSc29iYVFjK3hWdDJ6L0dxakpXVzFROGUySm9CbVRyd3M1UmJnOHJoT2pIa0R4V2JPamlWb0dDSitaYjlLKy91amhRNGdRWUIzWkxRMmJUK1dJdUJxOHlmVURKeVl0cDZGQkMzUW5nVFdnQU9rNnBtaHFqYVpSeTliTGJiUEw5U1FocjdqYlZEZFR1bGRUU2c5cVhzeHJHL1VwZy9WQ2pXelVuV3d3YXN1THlETnVyT3QzV05ySUY1M1EzT2k2NnJuSFVqcmYyMVE3UGNXOC9RZGc2eWtrVDhwb0VWYTBIZGRUdnQyZVZPZDdaU1BGVWx5VXZyK2NrM3VPTUg0TFdHNWwyRTdVamhtb3BVME5BMXVoT1prN0hubXZ4bHdSbTlNd1dEa2JjU0RBT3ZaaVNIZmcwcFNLY0hLeElGd2lkZ2hFWjVEa1dCOU80d3FsTDNlakZ5TGNNL3FqMitHSzVkK29XaEJGWUdwYXlaQ1dpcW1KbktRQkNBMXJoS2ROc1Fub2lPQVRHR2tQdjk0K2dlc3ZMM2JId2hNM3JJc3JaeXpGd2k1RG95WEYzU0RUSktXRGptR0hzYXFUM2s3dkNzSm4vSGRZZm1yeG14SC9Gc0hSRjBJVG40N3ZtM0Z5V0I3RTVrMXV4MXFibmpMU0x1QTQwOXFQR0kxNnNxSmRBZWdrMU13QklRSHlGb0RFMGlBY0hJRk9tc0grSklkdHE2dWtmUTdza1JCNFBuZHVsQUdIWktTQWF0akloYkZ2R29xeGUyMnhINlNVMWNmZThXY2Vac2RzaFhscTJQbnp2UXBGbExCRUdYdnJSalFOMjR6blRjWnhSQ243elByNjlseFdUUEpRaFNkbDNTNDdBdzJTdmhWbzAwK3hQT3g2MUczNUxmazlBaWdUR01IQzJuV1JtTDdESnpTbmR0cG9NKzZlMkxFUUhrbzBTUzhYTEp4VkVKb2VEWW4wT0htakdLSzNrdGtIRitHclJXQWdmNHZDbE1HQ09OaWpMM1R1ZXExaGptQWR6WlJ6cFlaeVpiODk3NGFRa05DNWI3cVJvRWM0b2puSktCblBaMVl0ODd0ZGJrb1BYUm5XaEcxRWtsWGc4MHl1L2ZNL05TbHlIY2owdzVXQWZvelU4K2NTZHduSFNNUURDQmJqMGRwc2VDUXpzYmFBTXRINWZXelo4YWNVM2xSWDZUUWljUE1uZFczUFRLWHBvTWdJYk83Znl2d2FCU0x1OTVjZkhWQWovdHptSVd2YTJKd1dYK082Sm9Bd2VJeExBZTVxOGJqYnc0eGNkdWdVZzByMFdYamRGWVFra1RQYi9jc0FHZ1p6NHVsS09Pb21vRXhBS2QvVFd1OVI4aFg0VVQ4N2tzb2VDMkFtMVVnSGZqOE00N3JXNUZEY0hMaHFqaUdydTBlOGVuQmpRTWdtOWJaYmFtYVNrRkZDMXBIais3S3ZNcHpNS1QraUV1YUZiQzd5eS9IY0NzL0QvVjRHcHUyT3FMcWtYbTR6aVJWQ3BNTFNoRVpyeklESTlVcHB4RDc5TjY3MkxKWjBURWlUZmJNYnpKRmNWTDhWRnlhVHczOTVKbFV1Y1htaTA3TnF5OVI4RzQ1ZTBwMHA2a1JZYzdJRExxSjM1OWxzcTY4U1l6aDRsbW51N2p0bFJkdCtFdTF1VWZsYVo2by9ydmE2YlRiNTBWT1JqVGtMWStCUThVSENSSEdRUDRvTDNiY3FyeG9vTEdQWFRsY2l3SXA5RXNvd3FxYUM2SHJIWjJCRXY0RGZTZ2pMNnhEZEduQ2pjWDF2eS9hRFFvSThhejYvUmJpWWpsWE9HTDVXaTR3aGwxdk4yYUErUHdqNlBqWFRZSXhyUDZIRnBxYkRob2RhM3dGQWJqOE00ZEVxakRmVzM4ckdhRitkZFN5T2FrKzJxcjdMdWYrWFE2N0taK3pOWVZnUHRUa1BKdElrdGg0ZkNqL2JVWTJqV2UrNmNpbGF1c0JkQXFES2pkMFp6eW1kRG92cjhLUUIySXlYSEQ3WE1JWlI5OW9kRlI3TFpZRWJJejRxanlGTjNRSTNvblB4SlFsa0pxSXdacG9OeStkYkhCQ0tTbWNNVU05eHpjR3kxS3ZWS0M1ditEMmQ1Q2VkRE4wbFZIV2xYM0daNGE1VmJJR1FibHdyM0ZEV3J4RG83MDB5OUNCM3pyUFRQeCs1S0JVNGJVbmJsUWY1dnBBb0ptd0dtck1QN21aeTdMRnFyRUw4c1h6ZmhGOVR0QnY0V2NubjNBN0dJVVE5cHVNRFVWLy8xdFRpRzMzbEF3bmZTVGl6UFRubUFMcEw5VzBia2lWc2d6YlB2TUhabU84d2k3ZnhKM2t3eVhZRzNlTWhUcnROUFpwSG9lUisxMmM5YVRiTVBKd3NBOFRjVms0RnQ3TUZIRDNoWHpDZVFnK20za1p0VzNRUGQza1dndmlzd2lMdmEwak1hOENVUTlqMmRFRUtUOC9MWm9WclFWeXA0N0R1SDE2T1g0MGZiQWVYYW5GTDFTUFpPUEpCS0hkNWFnNlpHM0NjZHROMVZLaDdrSWFUTVNvaDB3QVkzUWMwTU1zazdBdlVEcDgzbjMvWDNOS25nUFRoYVFldGtHRGNPekphMHFiUHNpUXdiaDY5V3RmL0luM0NpVEdSWmwzZndLYW1nOC9aRGRFQk1lUDM2em81ZGc2OWxlSFZ6U2ZLMVpTYi80T1dlanJPYjVMd1NtZXoyUW9UMmNGVHN0VW1yYmthdUY5RVQ2ODUzT0FqRzA5dVJiUzhNU0k2ZVN2d1dtcDVwKzJPbkc4UmsxdlowZnF6QVQ5RjRPdXBlRmF4cGk5bERTa0ptU1VqTkRKOXJmSnRDOXdLV1l4ZVNreEV4eCs0ajllZVpXTkw5d1VqdmdlZHBMaXQ0TndCRFVyMjBFOS9lMm9pVW9VWm5VNzBnNEhNdEFRRHFsRFI2QXN5cjZybm5Ra05odVBoTFlwSC82M1VFaTR3OFRGUHUrQVZabG0rdXFweFJzaXJCTlgzWFRNTE9hL1pVMkRLV2dYbkZaeEl5R3I2SjQ3YU5SRFQwdkR0ZVk3S1lxaFZrdzh2eCs4S2VITUIzcDd5cDkwandXanFnNjJ2NVd3eEg5TUluOGxpbkNhUEU4bTRJS3VDSE1acTUzbFI4RWV6Qmt0VWR0QlhpSTNUTXkvUVZBVjl5UjVvUzQ1MWVEUDRNVERYeWVJRGMxNTlQdUVia2E0aHBOTVRLb3lpRnY4dnhUbWRzdVB0ZUxGdlNYeXd2NXpWL3V0cjNjaGdRSjlnK29SbGpGVjlxdlA1bHRmaElrMngzNk5QNk1yUFg3enkwQ1RKaFdXMHo2RnM4anBXK1RpVGU2cWFHVU9WVC8wNENRc0F3cGtTU0FYNDl6TnROMFlVd016bTl4bUpEZDMvcjBreWdmeUZkakJOTlVla3FQam1HUEhFQ2E0aFdaYnlQTzJPTWp4eDNDN0ZNZC9sT29nKzcwcjI0UGRQSktvZWZrSm1iSWY5Mm11d2JjdktVMDlxOWlzby9lSFJRcGZ1OUlxVndFNkZWVjdCeWJnQVU3ZWpURjRXbGh2K1dBTHh5ZGd0M25xaHNidElZNCtEWTJNdlljN3FveVpqOW5wcS9MUmRod3F0clEyREhFRVBxUmdERTl6cnZYNDRvUTlwYkFsQ0V6SVNEemdXL2ttMXpyYkJpYW9pK3FMUUhKaVF0UTZrZkd4ZWFPclJVREZwcVVWdWtkcXl1WTN6V1lvUDdYeXpGZ2RRUiswV2JnVXhrZGpQMHpwWEdQbEd6dmZtais0L0ZmVjRtNWNtd0M0OXFNeGErbFpYNDJEMjE1TStaVFpzS0JBMjBkQUZIWXVWMVEzUEZ5cTZDM0JWaGhLT0RmTzNEb01mY1dWSTlrN0loZitNanlwSUhpWFk4d0t5ZnJLZCtjM00yU1piNEpEY3BFQkxyQmV4eTlFSVNweHo3STdEWHZvYlBEelNWVVEwSXQrcmRqeEpKSGUxelNSd0xIN09WWk9MZ2FIaWpyTWtlY01ZYzZ2bkhBcTU1eC9MZ2s5NzJVQWtoVnorbFJMT3plbDdodDAwZW5nUVcwanp0Njh5RU11ZGNsK1I4bHczM09taU1XMGlsSUcxRXJJTnNJUW12bjRDREdrNkdOR3k3dFA5K0F3aWVyN3B2VlNGeld6NUZBa0RVQlpOQ1E1eThVaTZKUHZjNGJvR0dMWGhybm5QZHp3SXdrYnlzblJOMnZna29BZWtERXo4UENkRjJTVUoyaGc1ZitBVEk4eWZBRnYzYWlMK0xpU29hRCtnaVVyV3JGVVdVNjRpaDlwamV5aVlIS2hnaWZwVDFSdFRESjR3bjBaSFRJQzM1QVd0eDVLb0krVmtzaVU1RHVudHJKdWtGTlQ3d1M4cDFhY29aemlkYWgyNVQ4SzRKbzN3VXhaMFprWnpYUXpoTmM4bTBhSFJSY0h1Y1B5YnZUQk1pbWZRQ1RIZngra09NVWsyQllaelVXWmkvbUtKWHBIRXB6V2wrVnZNaUZ2TERERC9tT3JqMVBjelI1WER0ZnRoM3h0eTdzbU1nWEQ5aWFVN21CUGpYYlE0aCtjUXdKZi9RUGlzak9iVVoydXlaNi9UU0R2bFJ0LzZ2S0NJQ3R6ZmlCTXFRNmtKK3dxamhuTENvSXdqa3VYdEwzTXg4QnZTSVdXeWVwOFdrZzRUdjltUjhwaE04bkNOWWhnWERiZlJFMGhaSVpZS2F1UGdlMlNNTzd5bVZBOFRoUVBPdDRwM3RqVlI1SjVIOUQxczAvU2xRTCt1ZzdFVUduRkNld0Rhc2o4eXlYbEx3WDNPNSsrcXJDU0s5alRsLzRhTEttdG4zQW9uVkh4QTl2Z25VeElWT2RqWkZNZjF2c2p4SUhKMysvZlBQWVZSVDZ6TS9Bc1lLRll5N3RFZXJ6V1BXOXpOVFVFWW42WHJUM2l0RTlkMEFkY0p4dW5HRkZSOGxGZ0NLYkZBYjlYTnlqcDVCa2xMZkdSQlVwOFYyeW85b3dhcWx3QzJhQmRjVHNJanJXTFVzdmFrOUx0YzFwc3FzOE8rY0s5YVZMcVc0OHc0T21Da0NOdDFxc3NPK0J1bWhlcW1lTW1SL2puVnhjdXF4a3oySWdoRk1uUW1OSmJtTlpuVFdyQmF5YWwzWHp0KzdhWUVHcHgrTFFUVzJkTm1GZDhOUVZYd0QwTWZUbURwYm1Xc2VKWndBMHhHRTFYODRLVzRWcGMzYmFKbkVlR2djditsWHBGZzZSMnFGS0pMQlR1bzdoTEJhUWR0TXNZemxraVU1QkJWN01sTHlMeTE0VndONWdhbW41UnBTWWRud1dGdk5YdGpQQmgyOXR6SXNtZGxZTHpFNDYwTzhCbmxHdmRsYU5uT3BKVUtLTUxTT2JaUjlsRnZFTUxZVXB6WTVnbFc4V3g5V2lYQUR1aFhFbittNVJOcklaRDNBVkF2aWVaWlFJVXhOWGduNVdTQ2ZDbDdXL3poNHRlRFZ1Q2NiREVqQWJZQ2Y4eU8rNGR1VnRrb2hsTk9XNU5JeWZnQzJPZGUrQzFpSXZnY0p2QlRiZ0hPMDF4L2pJejhROFN3alpnQ1hRUDZoMnlHRjRLN1lOWDI3aCt4c2ZUZGc2OTJ4ZENrbnRrcnEvM1VtVlY2RWNXMk1OQWhVZEcyRDhNZVZhNUwyVGVsdFphL3lXN05yQ3JmVGx3VWtDOHE0YjhzSXczeHMxN3FNd2l1aktIT25rRHFNdGhRT2V5RGRWVDNLQk95UTBZUXhLa2pRalcwOUVraFVBb0NMcFNuejBoRGtFVmVQODgxU1pWMGZHMXVKckNXb2ZZSkpvbkIxd0lsUzN0L2VZUGhsTmduQVRrelBWL05sNnRYREpSanRFNGIwckgzQUk3eGVUcHBwaHZuMFVJWmJpTDB2ZWJXVlFrSW5adGxFMHdPdGFmWUkraGMvY0ZwcGF1SjFPVTlFUnM0bTZPNUl5VUdzSEdnRmhyS1gvRGoxYytKZWVwQkxJOEdWYmxjQnVmZ2NQYXh2WW9aWFVMbTkraFQxUXVxaXo3Sk9LUUEvQXZZSk0yYytGQ1ZaN0p2NXhrc2pqMkpOMmRpa1VKNWZJY3hRZm1pNnRzMHdMTGlucFdwZndidkZGRUpjU3B2enBoSmtqQXdreGUva1U4a3FYZ2RieEVDRmdQajhjbGJOdTBUTE0wVHZQN3RTVWYxTTN1cUNUSXdkNjk4c1l5dzlxdFJqaHNyWVV1OVBqdzNHR2FVU0tOOTkwQ2cvaWRYQzhpUTNXWnZlM290ZVIzeGJmTWduMzc3bzVJNGpWNVZCVFVoUmgvUFlMbEcybWU4VTgwZ1AwTno2dWxmN3NIbU0zT1ZWQUE3M0todDBYeFQxTjFEN3hKa3NoWW9jeU9IdVNidVg1Q2xsUmhJMS8xM2NXb3BNNHFsYzlCaXl1V2dRbkszZ3VOcE9tUTRtSHJWTWRyTVJnVDdOc2dRSzhnallDcjdTc0ZKeWpoTEhpM09XejJqSUhhRTFTbUNEaVFycSs0eE13S20xd0tQQ05iVy9vRzJEOFVDSit6SnVBTG9kTHRwRG5oRmRPTWxKRktvVHJKQVNkdUZVUHpUNHVlcFI2ZEp6NjVzeTBKbFZLa3h6QnVwemw5TUdUbGZ3czNqMU5uSDhJYmpzaW5XNjhhdlQ4bXMyMTBmU0E2bGFFZjFDSjNENjR5SFh4a2t2d0swRTNvSmdRZzAxbU1YWjVxbUlNSmYzaE5lajRUUHhjSWhXZUN4Vi9NdFc3bTNNNnB1enFqYjJUT01wbll5WkxEa2lLY2hidFpNMjZVWGFObEV6emVObnZzTzFZUEIvcTZWSVFWbUpMYjUzRUNxRmRncUZWVzNUamxKSzREeTBUZkZOMnlLT1hnNFo1dytmZGN3NldYM1FqN0x5am9BcGlIdHY5Y3c5MzRoNzUzaFhFd1V6TldPNmpFSGF2U3JDMElTejd3c054alA3R1F2aGl1cmZrUHNIaHVqNmtPL0lWdmI2SUlZWnkyYS9DZUZIWnhuUEtndWxqWVNUZmRrU1U5UEdoSDdyVCtZcnJ5V2d6N0gwalE0dW5oSmNzM0VLaTYya0g4RkhpSmlZcG9qSWJtSGJjMnNSMCtPVUtmRFBXSVAra3N3eURGcVBiQXVlSUg2UVpZSy9HbGowc0VmbVIvZXVNN3NycmN4RUU4RFhjeHNDUUk5RmpobDUvOGNRRXljTEduTWI1Rk85bndaT2d2WTRRbWRlYXZVQlBLbFVDUlhWd0RYZjdrWUE3N1lyRFE1djl3N2VoVEdOMmNraWM3MTlqeW1CR1ViR2xjMDE0cHFrUjRoV2dnZmlHWVk3aVMzV0FYcU1wUERJZnkzb016cW9kclh1dEg2U0RiRkQ3NDVJb3lkd2xPcWhYc21Ka2hyRHhNcGI1a1V1N0Y1ZTdEaHZ1cWVaTVRoT1V3SnJKT2ZzMWtGSmg5QXQ1S3Q1d1lKUitaZy9TTFNpeEVWUUdnOTVQVE1kaERsWFdkK3N5eUtpbWJpK0E1SEJRVjh3YmVXOUY0MHFrM29oQmlIMVFUS0ZVaGJidmFTU2M0ckxYRnMwSDlxZ0c2elFSeTJYalhSOVI3TkJPbUxldWdOSU1lVnNkWldTZmVlUlV2bnVIcGd5T0k4TVlrNWZ5b2tlVmY3M29ybThhZngxd3o5S0hGSjc3VG9BTEhoN0VrV2tlWUlWOXF3QVcxQ0wrNWp3VllKR1hRV0xBUFVRSDhlVzdhTE1kckNqUDVUVkMxYkJpaUhLY1k5SWg1MzRWLzVtbm4vVTVET3A4THV4VmdyaWFPSGJ5aC93TGVVSkg4N2VBTElMeFdrWHI4L1ZPb3NFY2g5S3JtbmdUa2k5ZzcxQWtvdkVzdTdQMWJnaHNwdlo1bEJ4S3VIUVJLM0wyNnhaZ2UzaXNTSnJsWmpLbkhQNTNwSDRjcWJsV1dpNXE0SGhkT2YyUzB1MnVkdWhVM3hPeE5PMkhRQ1UrTkdxZlZtRTJUajh1TGJwN2Z5TGcwYzV4TTlGRXVqZnM3UUVkYVJUak5hQjZjZC9NekUwR24xRlpYOVBIdUcweFNUN2hScFhxbzExRGZsNkNrQm1LRWpsYzZNeVJxWXhSeUJuTGhJbHBjUTZMa2RMaENDd2JwNkxId01kUmNJNUo1SGVCME02VVBua0xrNDNLbG1ZZnVXQU9UbXprSy9rWFNGYjhKRC9PaGV1REQ1WlBreHhrOUI0NnNibFJYYk5RS0M0YmR2MlhXSkdrcEJ5VWFoNGhlUjZIVW9TN05wVjlGaCt2bUg4WUZPZFlQbXBTTjFHUE1Pc2xBbHdwMmZXczIxcEw1enFIUXpneHVPbnJrVWpoMXNZTzlyMFlHVUtZZElES3hPS1RUVm9vNnZIMm1oT0h0Z0NFbVhMSHlNaE51S3BTMzRRU2M4c1hwaGw2V3Yvb0h5VW43SVl2akQvYmFvdm5QTlRsV0pOK0tEZ3ByZ2Q4RVc4aVVDS1MzN2pmcERwcjUxUWMreEt5RmxOV2tCU1g4dXgyL28zQWtzWTNHOXl6cEhSVk8rTjIwTGJYRys2ODZSUzF1NDBScXJQZzczZ2JCa0hlZlBNRndmclVLdlVMOW9jMS9ZWHp6d2JVL09UeElyYUtCSkRzUGhodm9lem01dFFKaUJVVlBMUXJpS3pjZEpwQWt1Tkd5VzdBMHBQOUtmT2dId0pLR2xxK1BWZ3kzVEFTUHRUQWpjZ0xWaGRIQ3AycEtuSlpTdGdVWHJNQllZYTM5WnlEMXpIUWtLUUo2Z3dSSElldWxGbjZERW9TUUtiM0JXWnBwV0NncWMxZmNhaENrMEZWWTBrRHViNTgyQkVYUHZKTHd2dzRnbE0zUXNiYkR2SVZvMlRUVWFlZFJ6SHcyVEhpZnZRWUxydm1wZU9UVnNGYzRYcGFYSlh4NmZqOUNTU1lzcWRRNzF5aGRPSGQ0b1pGZllKZndmNHNVMysrWTIrZVdwdGhTeTVaUUxMK1BYQ0dNVGhBMm9BZHdReW8zdDRqVk41YmpuejhqUjUxZ0V0VWJ2UTltazdMSFd6dnRVLzgrZHB0b3NRMmNiV3FTOVBVMGxtV0Eya3RReWg2VmZWdTlXM00zUWdETEtNQXozQStQMkZ1am5MN3N3NjB1dnQ0R0JuV2FGSnpnZlByNXdpbk5NVk13ekQ1UGQwZXE1RTZHbUxHWGNHWEE0SUQvYTdmQUpmU0R6cUxVUktET09scVhWUmNob3VkWktYU0pNb2ZqRDkzYkxUVVdmZTVUcDdkc25DRmlYYkEzbWJ2emx4VkVEUm4wQndDZDBNTXU3WThHa3Jzek5STDR5cVR4RFBoWW14Q3BYRlRxOHhNZDhGY3dPSnZneEprNUc5eTVIM2tlbDY4UUNNY1ljWFhQUnlhTHA4cnFYK1IzZFc0RExSeENpVkF0alFrSVEwODdmenBCZnZQOXBBVS9rNzk5MlJUK3Y5T0NIMWFXYXQ4REdwOGo5SURPVFc4Z0V2SGc4RTRHbkgrY3lDeGFFOC9nTjE1cTQxcktpNzZPM2psRDZQMWpUaGljRW5UK3l0Z1l4OGRqWjA3M1dTYXVLN2t0OFR0TEJNTktvR013QWVxV3lkSHg4aUZVaXhvZFFaZ3VmOWpBMU1WS295L3Q5QUJpZzc4YjhLcFZ3YjdETlVqU3h1T0RMZ0dOalA4Tnl5dzNmeVRiQzI0K0lwNXVmUlQrQ2Znb2hYSVRWNXJzQWhvbndQWUpkZFlhcGVMNVptM3Vodyt6ZnhYTUY0eE1MbDhXcmhWM1YzWitFR3RWRDNOeVZ0SUhCa0ZJcXNBS3ZmUWs1aENDVk9XUDRTT3AzMzl0dVo0bVYyMGJieG8vOXFHRVpYbGN5VWJuOFRoN0JJdnZEaUFjejd2ZDZPOE1vU2NlNjZOdVQ0WHY2cUQ1QnBseWdzY3p6T0l6RkhzdnJkQXNKbTMvSVpKUDRWUXpmcTVMQzByM21QOHQreHVLSmtTNEpES051Z0JrSy9wK3RXbktiVGMwMnkvOFJPRDRsYU5CSElPd2J0a1BDaHdFbFRYM3EvU3RkdWtyMDdVQ0RkVmoyZTVxeGJBQkZESmNoVW1UQzJzcGdmd2I0UDlkVVNTam9vMWdkT0JVM3k3czA2bjhyMXl3WnB5MVVaWXpoYU0vcTVmMlhhU3hidzU3MnhNTmYxTjVvZy84RGpmZGpJODRyTC96SFpCODdoWjhYR2hUVThCL2QvVzVoNjZXRzhmaVlJNndlWGtncTBvWVVWamYvMyt3V0MrRTgycXlnVWwxQTVnZmlKVVdKbldNaktTSmhiY05PcUNoSUhkQlB5K0l5UVNGdWkyclJWalQwYzFGcitxRGhjVW9wQXlGK3hBaEJITVRlSDhwak5XWCt2cDN6eWpmM3dqSFpCNGdXZnNURFpMM2pGN2pubmEwOTVrd203RThUTTQ2Q2JmNDJ2RVZDcUdWKzNieU9GeVpWQ0FiSHpDVjdUSyt3QmVscGx4ai9zNFZycGpNM0VsaTVnV3JnaklTMHhDTEh6QVl3NzNDbDdOK3FYbDhyOEJlbkpXbXprc09YSkRjbGEzeU9TOVZIcVNFTjhGSm9sdC9mMmJLM09QSGt2NEg1VVpEMFRidnloaXE2T1p6bk9XYzUweDZvbVF2WHJqaXg5czdyRXhNcDEvcG1tYmxEbnM3aHAxWi8zRkovbDNmbk4xcFJSUWVrUFB6YnVIQVFDRGp1dXV1eG94R0J6L3BvZUd1R3FhWW1ST1FlQXZwZ2h1ZngzOHBQSzlFV3Y3T3dWVTR6U2ZsQ0JJVGRuWitldS9KNWNNMU9LSWxteE0zanFKTjk1TCtpTEJjcVQ5bkJaaDZUYkdRVk8xTVdzNFNuai9RTGJlY0hHYmpJZUR3WWhRdjB3dEEydmo1UkJ3MTZoK1NBYUlSc3RQVHlqTlFPaEtrc3VXQWNGdjR4eGdocElEVHk5UGJQRXBPZ0FOK1FDK0ZvTEZpR1FPUlk0S21nb2ZiSFNydVluL0FOZ0FmWEgrcjJvdmViWmZicUFZeC96VXhaMGwxczdLY3pldVhRMzR4bnIxdDNBU0lBK0I0SXNqaWcvUGUrd2pGRVlMbWVFWDNUNndYY0RHcVBnVTc5M2sweUx4TnRCRGgyMFNhcUZDNHIrN2taVGhpY3N3QmV6QXAxOGMxYXpNRU5LQWF0QkRsZnJKRkZ4NzN0Y1owUHBSdjNKYUw0ckM0bU00Yi8zK1VtbkpwMTdUTFY3clM0V1ZQSUZRejFDaTQxVDVySzlDYkRIMmdrWUhJSnFtOExkWDhJcmFjNjlLTGpVME41ODBLUWtGdW1DUlIyZURsZWJnNk5ad29VaVh5ZUZzUG1lbVJiUkw0RlMvS3UwVWlxbzNqWjZvdTYvUDNyWUp0STZBVW5BTU1pRE9xRmIwQkFQaGViY3BZRGt3dENvL1BFdWo5Tng5SmZ4MHlteTVpWWlXRW1xb04xdjhDbm9yM0lOWGRIbVk2aVdCYzJGVDYxSzNRTzU3bnNKcnVQU3FsN3lJaUZYVStlYkxaNnJMa25TeFJ5SDZ3TEwrbGd3c0NLbGRhZ2dBVWRRYlREa3BXaXBubytiRDZ3Yy9XcXBGeGU2bUI3RTFabjBDY09NNDlneGh5azlMaVRuQ1Z5SDhKREgrYTJ3VzR5SzhaSHBuaDZ0Nmo1M3ljRFdtU0JTWmtBU280OXZ5UTFVQ2NMY1NBQUk2WTlVY1IyQVJ3b083VkNLNlhkYk9pV3I3REpKV1Y3SVVISTVpeEQ2MVpaVEdqNHFHZVh5S2ltZGIrL0NWY3ZqdlNEUGoyMVV2TmozLzBETm1ieFUyMnJxYnQ2blhUdFV6REZDWWVGMDdVZWUxUmtIQXhGcXFPOThRclFEcE93RFR2b1FWdXZDNnRpN1NEMExQVjIwN3ZxZDdKcnVOYWxKa3ZZMUJxRFRmekJReXNjaVU1U05WYVNucS9aU0UvRHVKaHJkTXpKNE13eG5tWnV6UEZXL3FBRm1zbG42bElZV3FKVHV3c0dZUm1pMStkVHpLMFpVcFh5Z3FrdGpreVM1QWY3M2hzNG9paE8ybG10bjMyeFo2RWxXUmZKOHByZW5EczVvRldMSFdlbTNsV1RtM2FhWE5wNGwzSE9ITi9rUTJWWkdEV3N4UWJ5eGhpeEZqYUE0c2YrclFGL2piOU5CSW5nQTN0QlZDUzZTeVVkVDhjTHNGeHB0VjhJSzFBMERxWEdqOFBuOHdHNkJJSkc1ZHhMRXh1SU84MUw5c1FMVkRzL2hoRmdVeE1BNEd6UTVuZWFXRGg4VW5HeWxoQUNpaWxmUG0zaHdaVUNndnpFVllkeXZ4dGw1cWhZY0VnVlM3WG1PUFFBVmN4ZzV4MHRDaVM5dGtmTVRiaHZxZXRSeVFRQVZQa3FjNGMvRjdCT0ROZU5jYkorZDlrTE5VSFM5cFZoa2xybHJXb3FYT0hZSHgrakErbXRkSU9KM0hEOG53QTdWbENUM2R2SlhFZnBLZE1hTUpuR092OHNxNHVzbmpOektkbmhRV1BycjU0eVdENkl0aUo0c0VrSVkxeHdtVmtkVnFOVWtMZW1YSXNUd0lPMitRZFpOYTJlNkVQL0dxSkhzS3JrMVVnS2tITVJBWllvbVhTTnBUejRsQ2ZpSXd0cUJXYWllaGVnV1I5TE9GaG9CL1FNU3FrS0VUNUxCS0ExYTZwWlRzOTNFT0VIUmNyZE96aGVUTkVSdUFpcUVlTTdXY1BtdnY0cytGRjZobzlwSFdRci9WbDF2TzUwcjhndHIyVnEvbXNxdkRjSXQ0c3o5M1MzaFVtM2R6WjdWSWJudFZadGpva2MvWVdyYi9uNFRoYWhoUHE4djJ2d2srVCs2eUhJUU92R0FpaEN2OGNBNGZuY2hXcjNTNGU0ZVhxb3NuSkNQWGtqYVFEdVZsZnp5dTNvbldpZVhFOUNUV0pWSGM5ckt0RnFNWWl6ekhtY2hMb2s3NlJyUEprRm92bUcvT1ZHZ2NLNmJkWlE0aFl3ZUZvenZ0MllDWXB3YjJCMVg2emh2M1dOMmI3dDhNS0ZGS2dzK2V0UG1MV09NR0hxRUI0dGx1cXVyb3V4dE4wV29pekF0TytOVmw4ZHI3VXlPekVQcVZHaWxjeDNEbWZPUHdkcFVUckplMmxXcmdIempmUmxDR2xNNGpPMHVxb0ttcktUMzdhMm5ob0ltWm9DN2NuVjdyNWVCLzkrL05IdXU4WEdGZE9TcFJFU2VkQm0xUTZweG9hN2E2dEV1Q29tRkJtdEMxQ1dXWExSOVBRcVBBS20rbGxnMXRHTXBYclozbkViTGp0MmF6b2owMWxEMTR2OXpSNEpHQTdtaTc0cCtVSFJEUEpzbEZvT3lDV0pDOVU0ejNGcGdWeUlZREFieEN1djZaQ3Q5a3Z1T0oxYVhVRU1DK1lhMjZlT0JEQTlCd3ZSNVMrUEhaZzFVVFNES3FldURSOUJzdFE5REJISnhFN0x5SHRRL2tzckpicVhhUnVFR2FFTUF5Y1I3UkVOVzAvYzVHMElYSCt2Vkc3T2dyc0hlT0hDZUtycURCaEhUVGQ1RUtoWUNFejZLZjE0WTNQYmJ5SDd5a1JTa1ljVUYxMTFOYWpxaDVQdkFMcnJ6ZW5tVm9pWUgwQzB0cTdHNk1tUU5qOTVOL28rckM1ZDA0eXZkREJYcTI2aERuYW1QOGhTbTRlOWRXUEo3OW9lU2psK1pOM005RGFWREk4dEFRLzk4WjVPeVNFTk1xcUlXdm9zdWYvTThSeG55NmZMQytuTENlOU1FTUlMc24yVzhScUo0RlhCSm1IdGJIWHVkUDk0THdQMHVhSTZhUzBzcnRRQ0EzUUNKeEdFcmk5NWRaZ2ovcjZveUZCbkJtS0RURlI3b1FQZHJyOWpWUEF6TzlzZCtmWmdCYzlidldRajJLRDN6TklNalEyVmhUa1JtRUY2djBqbU1aV1piZER1dVRBWnVPUU82bk43bHc4VHRqMExyVENDTEcrMGx1MGRlY0pNRnlBbHpIM2FBazFXOVVQbEI1eGNlcGV5VEhPakFnbDhtWkJDRlpnVXJTVG1WY0tTQ1lrWXI3SzFyZVVza1d0YXZvdlAzbGdTRTE2SGRHeGl6VzI3cThZLzNjTnRHUG1HK2E1RkJJdG91YnVQeC9yME01cVQyV2trNS92QlMvSTUyRkRHSmJ0RXBmZjN6VytWWkZDZ0VEMW9EZEhrTVVlOVFqVDZBWHEwNWJGbFhLRTlaM3AvZ1pYNlRBZ0FuM29aTDJsMFA1ckQxQmQ0YmR3U2VNQ3NXNFkwK3ZscVFwbi9aNS9XWjdoQWozRDM2RWMyUURnWENTdE1Gem11VHU5WnN5M2EvcC9PbVNFUDA2TG9ESVlNVHBnaTZxQ0hycXV5RnVxM3hOaWczRzZnRlUzZ0srTTlEUk1uWXRWNWh0b1JONTZEekRkVzM5OWUydThOelpBVFA5WjVaSEROOHFrZFY2b2FZWGJXYlZ1b0V6OE1CM01RWmUrSkVRa0JXdUNPUmZsMHlRb2ZCQzd6aHpDQ2VpUUN6d0VJM3JGUU5UYlBManB2MHBySm5QYVJyOG1vRUJMbHRVK2NtTUpxeXRMZGpSaUNKL3c0SWlBL1g5WHJzUDZxK0lrWm1HQVdMN0s0S01UdjJSbzR1bUZhODRaazhlb2IveUN5Qjl1dkdvazAxTW5qUTNLK1JETEFjNkd0Q0JISkl0Wnp1bFJJd0RXd1hOVXh4Vit4RkVrZllYNDVXY0ZuV0xzY3dxVVRlN2lEWitsQzNiRlZHWUxvYllXblZQc0RFNk1USVJtdTBwRHl5R2dkcEc3QmNNbnFBTTdLbEQ4TjdQeDZITFRlRjg4WHpGeEppcGthbHN4Qkl4bWg3OGxWRVRjdlp1TzEzaTNNZTFDVjh5MDZ3b2s2RENCaFpkS3JKREFEZUdtaUgxME9EYml0Q2M2bkxxZjZGYjNBSUhWK2Fvc2hsTnFOdzdubmJmVG4rVmprd0svSURzZlliY1lCd0hFR0pVNnpnWnVTQU11TmEzSTJuRDhuUEVrZ3o3UHgwS1E5d3NFdDNUZzFDbUpINmZLRUFuODhPSk16R1E1dkV3WkhwcHFWOFEycDA2UXBUNXR3eEFBZnZtL2ZxUkp3WjhDRmxta3dQTG9RSWFzdFNEUWZ6cU0wSjNWQ2Y1bWxNUFZyRmR4RDJwVWdDeVdJVVE5UGVuSzRHL29kczJydVl0N3Y1RmFuWUpsc1hLR0s0d3d1UDhqLzJxaGpiT0creW1sTVlLbmxyNmUvV0NrQTQ2SWVTcjZXZ204dE53Q1J6ckZINUZicHFudWE3N3plRU5FSExodGJHdThqUXBJREdrRUdMNlJOQUhvQ3VGcnpCY2REMHBTYmltM0JBUnp5VjBXNHdIRG9kdXZtNERsN3NEVGNnWTNUSjVLZEx2U28wRXBBbG12TDhteDNhK1hRYUhRRUZhaVJKdUlRVHpvZjdLTDU3dTIyQ2RoZ0xkcENzcDF3M2FtVmh1bm1wYndHUTg0ZVVBWXJZdWROK3NlMmV2aGxwZkVITXFTMXRWTWRuQnJCdFUwTjdxM2k0bEFHVkN3WHpUMG5EK0N4YU93bzlIUEwweHEyWVYrc2RoM3RoZE9WTkVyMjBUR081WEtzSDhjK1JDeHBTQ01QNkt3cTI1WklSYmVNZ0tDNDNpOU9kUVVaODdwNEg3d09SL1hhczBUWWp6RVRvNTg0SGoxaURHbDJnVkJJU0RwZHlGaUtrTzllMGw4bjJ4Z0s2N2g0UUVKemwvNDFjWExvQVNOSCtLMEduYVFaeVJhODFRY0ZJWXdTUTZoMkZLZHZxUHMwTE83dStpRjl3UkhaZldOVmVVWG50Qld1Q3JENzg0NUtxKzZYK01YKzFWL2Vic2NONWpYNjdCcWh5OG1lbEovVUJwK09vVG9xUVpxUXVVVlJ1b3pHTlAvT0hNbUhrSHkxK1R0dFhzWVhyWElvR2tSOXFVQk1NV1hjRDMxcmY5MGNNUTVlekRIZkxzM2RHc2JZZ0UweWxMdDdUYU10TmhRYktlUGRWMXl5Z1VHdDF5ZFBvaXZHSm9NSjhPTG9GZnZVWHBicCs4cFZoZEhhdHNZUkhQNWN0ZTJhZFN6RkhITVR4Q0pBejROR0ZjaE5RNGJpbUhDM0R6aVdLbytKbWpPMlIyZ043RTdSdEthWEhveVBLS3FScDhVVWhrMG9xNVM5N3BHWm0ycHRKeDVET29TYXlMTUNOTkZDWTc1UGNOSURWUnFaYlJQRjBvRnVTdm95T0ltZXRvOXRTVkZLQ2QwaCtiZDlSM2VkaUYxZUFiOXdtRXRRb1pYSW1qdlpsaVJ2NElIWGt1aFcvclpIRGRtVmdwMDViQVdMNW5DK2VwOFlldWprTmdac0JteUFVRTVlMFdBTGpVT2VxOXpSVlExeEM3WnJ4Z0c0RkRvWkU3cFhCNlVieUJJZ1BLSUU5dEpVQzlKbTFtQTIveFpkS2M4T3lxcTIxRWJFeEhPbDlYODdiM0IzdVI3Ni85UGtRZ2lpVGNxMFZISTNsWXlTbHNvMFdhcHJlZjllb0dDcFV6MTdGbFJNVkEyekhiUXJLNExjSXhjcjA3Q0ZEYUduMlBOMGFyVmFKck84N3FBSEN5OHhVK0ZhMC9Ld0pYQy9rbm1Sekd0c05nbWN2QWI3dytHL2M2ZTd6YTdKdDd2Q0xOUGVrRGZLNE8yS21hMm1hK3FwWmZ5d1ozcjViSW52WkEralFJNWxUSjNQQjY2SXl6M2VDaG8vM1NPU0pLOUVwOUxpQ202RzRETWF0N1NITFBxV1dPWXdvM3lGNzFETHNnbVhUQWd1SmFIeHV3THVOQll2WW1HTllMODY5clk4WnQyR1FwK291VHdaNVJuaklsWFBYYUVoc0p4b1N0eXgva3U2UERhcXAxbWVxZjNpR21GOFNOSzUwbUFrRk1JTnZYcGdtL2tlbjlJeTRvMVI4d3p1L21xZ0ltUWJOanY5alc2UTRIL0ZCOG9ZVjNkd2c3SGFoZzRqR2dUYzVOTEpSTEFlcWFwV20rcTRUUllaQWlPb3lDWjZ6djRnY1g3SW90MGxaTXBKdTRZamN3a1FEbkVKQ3paQnlXNGY3UElObDQ3SkM3MkZBQXk2RzNJQ3NBRFR6ZVJpZXhEb0R3RDlaYjd2bUdPOHVydWEzYXBldTVoL0ppeEM3akdSS25wUHpqMUdyVkp5QzM2L0dNOXhya3JQTGRVbFlwSFFrM2RveWpjbEthYXd2RGtGbEFCYTFyM1pxL1QxUnZKZGlNbk11V1V0eXdHWi9iMmRacVVySUJUUXJINU9Vd25BYlY1TXA4VUVjWXRYMDNqd3lLS2RMaUNhM1JqVTBMaGVDYW9PTHhES2FScVphMTZKTlBDSHFKZWloYS9jOERmbkxUT2VKVDJmWFdaSnl1dnJ5UUl1YW5tejRrMDd4VHY0eXJXYUtNSXNoaVkvclJ4clo4TTNTT3haay9jN0tWQ3l2SnNhbGszQ3dQd1BBdStWMHVRYk4vNmhqbXFwYThmWjc1RllJTmU3SDNZWnVWdDcwWTNXT20xTS9BM2J6OGlTcExRaFdxZFlRTUdlRkFnbTNNUkVxY1pCZVdLWlpWN3RIcWwwNFoxak12dTJwVkVCRXBkSjUxTXArWjFIYzJ0aklEZnd6YktxMXdQWnRiNjYzUEVKeWVFd1pubHlMT05MM1ZlYTdUTk82aFVpMnE0N3huR3BZUXM2cTAyWUUvenhGOXUxbEVrMzJYcDF0Uk1WTTh5UHFITG5uWDVNaHYyRjdnZmFVK0U5VnlscFlYbUJEYVo1ckhqQzROTVI1ZWE5VlBrTTZHQlJxemNVKzM1ZTlONGJJbG05dzRVYmxDZXhSSVdjdEEzVzNZcU9YU1NwaTMrOXhwNlJIdFBGNW00ZTB3cHIyUklMWlkrZERoalh3Q1Uwek5NM1ZXSVNKa1o3alRBejJyYWxkLzdKZFlnbUdubDk4KzY2UHRka1lOOG53d0FOUW8xbHZRL3JNMEkxUzBrdUJYV1krNjByTlZ5SUx6NGc2MWpZZG1yVTJtMmpEaG5Xa2FBZ3MrVTZ6N2FncEdtZHdBaGQyMHNMb2dLRTE5VythdnZrWm9mVENOYTVyV1RpY3p6WWdmZkdLejZYV2FGelpsRm9JNHVHRFE0bThFemZtaVprbnNVeTI4THpUazBEWDJqUnJ3dFZIdzdsZDQ2UVFNY3Q1RXpDa0JXR1EvOFRJeE1QSmZOOFVkMlRmd25HZjljYzVTa1FySlhxTjRURU5EZ0tmREtTZWVPN1BUWHhZWk5Fa2c0ZmpDWUovdXpmaGZkQ0VRdTdIaU16OG9FNE5mZWtGa09YNFd4cmljSXpIVWxDSmduL0VWY1JTUGxxRmVNSitQVzdSYjQ3SkRTSzJ5a3lnMGtjbFlWQXd5NzZkdW53S2Jrek5PK2hIUGV6dzBPRFFFWHl6eDVQdjZFRTBvWlRLUnEvMm5CeSszQzk5SldlS0M0eitWb3hscExtdm8wdE0yQWdJQ0FnSUNBZ1BQOFBwcTc3ZnNjdCsyN2Y0ZmNEL3YrNnAvWjFSOTl2MldmNy91SDkrM29pdnQ5TW0rMzA4cjZpWG0rMzZZYitIMmt1L2JmVjl2MmU5OW4ybTM3ZXFIdnQrMUd2cyszbDM3ZWsvOUQwOS80ZW94UG5XY1A3Zm9mL3pmckhOK25aLzdmc0Z2MmZZLy9WZExmKzM2MUMrbjlpTHYxYjMvdDlGQytyMFo3NTBiMDhOVXFxTERvMTNwK05RNzlIVm9OM0dxR1FBc1FRR0tMMnhaODFyeHhOUFlDOEhmY1NtTDZTYlFoZDBBd0pQeElqM1RPc0tmbzZQZFBtRkJhakIvTjdkdzRTKzZyd3dGc1lWZ1pvaE5mNVZ2WEdpcDBWKzRvcDN6T3hmYnQ1amFncjBENjFyQllOK01lUkFuNGN1UncxVG5uYkRoWWx4cllDRG9sajloV1NaTGYvSG9zcE5lcW9zY2FqTTFWNG9DVGwxK1pJc3NQb0tneHVKMmZIaVlaSWRnRmllRStyeUxFMmRsbFVZVkV0dWdkcXorTGozTmFpenllZnk5MTcxSHA4QjNDaEMxblhFRS95bE9ObFo5bWpzMXlOY2wrR0lDellBUDJ2cFVhdklrdSt5UjllbTQ1bTlVdXhZcXZTYStjNjFNNHA4N0VONFpDWnVSYkZtd0JUSWsydm5VSkVvQkMxQXFWYTZYVkFyMjRSTFFuVXI4Wllpd2x6aXNZQmg1aTZPK3R2ZEJNby9ROXh6WXcreGx1TDQrWC9pVEVUd3Z5WjVlbXA0cWNIcDlkcWlaaUhHU1lOZ2x0bDg4ZTA3NEIwY0tOcCtvOVM4dVFQNFpydzgzQm9SKy8velN0cHdTLzEyc3RmTDY5SDNZTm1EYTg4a1IweDA4VEFocTR1SFZJNFN1Tmh1Z1FZYkFSUXVkc2VHWnNHRTJyTkxoOFZXV3ZkOENOWTFhNktEejZ3N2pibFBQTmVzYmJXNXZaY0cvSkswMUlzSFdlVVN3Mng3ZkVmdEd0SzlIMEl1dVkvWVlTQnVHSnhCOE1QT21DRmFUUk5KY0d2UlpoVmdLZWJUZTRQY3FyOFIzdWoxUU5URDdtMjROM21FcEJaTHJ5WEM0UjI1WnZPWWk1d2tQVjVUcXJtRTZSSlBkNllEdHFZSmRkQzhYUG5ld2c2elkxbEpLRnpzaHVrditCUFNnRnZrVTdNL2FCZllhL3lHSVg4Y0F5ZEhFYnR3QitDd1FUQzFoajRVdUR5K2REKzZWb1FFVzQwb0FVZUh3S1dHTGxxZ0FQZkh3VGxrVHhuOGJvcWV5dzN3dDZpN2dFYUNSc0xNeTkyYWxBcUs3cm5uR0w2eTM3OFdzNnlFRmc2ZU9RaU5uTHplOHVqM3F1ZkI0MVlWNkU4ZWxaQVJxY1pxNitsNGtXTWlUcEtiaExFclV3L3EwZm9Sa01TQlRIbkhtVFYzbWs5bHFGQUdWQUxZcGY5LzdhbGprc1J6TTVEVDZUSUdEcisvVGRLeDRpVWFPYUF3VXlnMWZER1pXUnh5QVA1L3FENkVOZ1VFS0NXNlZiVnpVSm5OOVNidk4vQWtjNERoTWZnR1Jnb2xucnc1ZUp3SXZKcm1tZ2hKL3dpRG5LNXo3M2JtaHdQcmNmZWw1Vjh4QzUrVldneGYxK003c09kay93clF6d2ZjeDRINnRVV2gxa09BNHVVNS9odGhMYTdFQjUwNTVHZmdkaWhCb2FxUFpFbHBvbFFQaTRDdHlvT0tpVHBDRUJGYUlpbk1qOHpSaWRaZjlCK2hIcXdSTWhoeHA0cUUwRnRDR2hBQmxBSlBtWjZLYThkVXpMdGNlN2J1STAxWmw5bmxENkwyRXErR1crOXZtanpaK3laQWhnZmVPVnkwTk1JZ3RlQ3hsMTBFR1BQZWpWVXU1bHpqSXFPdGd0cFJvSzArWGlMeDZRbVZyOHhlNDdsTndIYk4wZ1JRYTNHR0I1MjllcjlLcHIxVzk3dVY1N1JqbFRvaXZYQ05pUUxWWFpOQXIwbkxoM3QyaDl3ZGpIQlJvU0lGelY2TFNjYmY3MWZOa296eFg1OGU3enE4eTBSa3ZQV0lCTkVOcE5DUjU1dXh5K0FNd2VwSEZJTHFvYks5RW5LWmh3a1k2K2xQeWZrYXVzWk82UTN0RmRqWmZ2QUZMbVhPMnpnNWpESDVaeEFuSXFRaWxaY1RSN3RsT2V4OXBodXJ0RVhyNDZuT0phdFE5NWZOVmVuM1BqOExMSG5ZZ2htL1dFL1BZZlF3WXE0ZEZTYjh6NlR3RTJ1N2o4bW5kZm9PL2ZmMmFzSjVTeFVCRjg1b2NRVkw5MVVVRG9lV0FFNTlXc0JORjZBSjJLNzU3eGdNMXdqWGlVemFiMGFaNzZRbG9PUVpSdWN0Y1QzcnpJbkM0N2J6aVNrN3VLWW5GRllselpSNXMxWThkQjc5U2F6Ui9sUy9raHFVbkY5WTkyL0pYQU9CTWpkZE5ndDZnRGVlNndoMVFFaWtmUUN1bEs0bzQvWWJkbSt3TDYxWUF2M2tzeklrZ0pvNVkvL0xZQU05NzdxSEd4NkI2UGFIWlIxLzZFZ1NjRDdOTmg3eEljcFo1dW1sUTZQOXNrakgxa29GRDhXNEpEZzErSWdCRUlYU1hBRXNkRjBtYWJFdW0xTGVJWlJRdEFibWgraFlCaG1lZ2pmcTBHbnNmWVp3NGxZUTU0b3htUHdZK0xKUHNkU2xBcFVZMG5ZaGRuT0sxSlEwRElha3htK3BmMlk0NFlHT2p6WXVvRFFIV0lWenFXWmhiSkRQd2JMREt3NjFKVllPQm9acFF3Q1VUdE9VbDc3K2p4aVlsNnhIck14WHAwU2dTcy8yNEVJb2FOVTlwVElBRDBhRVQremRGTEtRT1hKNXQxNTdTam5COURBL0tNYlJjWlVVYWRlWC9YZ0cwSEJKYlpuTGVWc0g1T25jencwc3FhNmxTVGg5aWFoYmJ4VkJCQ1YxVG4zNGk0bytsaXhTZWsybWIwc2RzUExiQVVrWlpKTDNZVEZtSE03Q2R3VVZEczlnbHF2dXhvamVKL0R5UkplOWw5MWN3N1BjTVlwK0t4UUhqb3cyWDJRWllKTjBScCt2NDJNOFphaW1Edkl0UktsR3E2RzZXYUZtcmpXRzhNWkh1aTdSVE5vRGNaU2dhWGpFVmJ0TFlxY1J5dkJobnd2a2E5NTdGTW5ucCtlZnBvVVROZU5WMnpFVXhnNmlEMHpRdG5jQ2x3NWlqWUlndE9YYytSVVNCbWxVOWl5ZEYycXROZW50VWFWQ1NrNDNpdzRFMG53bzdKQWtMSkkwVk5TU2NPeE40ZTh6VnVUUjdNZnduZXFMcWQzN0owNktNMGZUM0dYL3Q3T2JrNEVEOTNRdHNDK3A4NHFKelJXelZOVUhIcUh1Z2hZV0JMTXo0RGMvd0w0N3Q2Ukcxb05WWURSNXIxTndQTUdmNExOWm8vL0NSVWpGZVVCVVhUTXZZcTNPYlBkamUzRFFtcGVZOU45UFZFMHpUOStONHcxejNabFE0bnJXWHd1NDNyTU11VllRUkpwMUNZSXRaZFNzWVRiZmZ5UjZqR1VVMFhSZWxyYW81V2UzTTl1TCtzOGdwNS90U3hqR2l2OE5XOE9DSWliMjYwVzZjbjJpYUVCbUk3OXBEOVhtb3FyckM4TGFVeVdlZmYwSzZ2VUFMUFk5YzlXVlRoNVcwVGF3b01aTEVhaS9rTnI4WlBPalhBMEJiNGpUakR5NW1hOFF6OFVPR2F0R0Mxdlg0Q1krS25IS1d2WmFhb056MDNIaGxQL21jbmc4RWp5TjdERDB6YVJOY1lCTnZMOEs2eG9Wdm9jOFo2SG1GcExwR0FrUFlyTm1xTTcxa1dURUNqRG1nSVY2aVRnSWl3cGcyVkhyZEw1RlJ0My9QMGdpTlR4dDdwNzRPTGpYQ0ZjeWF3c1QyUDJCRkVxdkIxZGxUZ3pJVXdFUWpMUVhJT1NZMTEvUlRFcGJ4c3V5SGRDZ2xwZ1pYa3J0V0xKRGVUVnVpSmdpOFRodHNzZjRnc0xIMEV4dmQrbWRiZ0tiWjVyclc1UUo1c1BKNVZ0N25wYktFU09VditLQTFqODhaSjl1VUFsMHgzYndGVFMxekdyTE84c3ZqTFQ1VVZ2bmRrQkZLclFVc3RKbUhyRE1UdUpqdWpmRDhuTmZ0QUZYRWFJaDh3Y2JIQUNwWXdvMmQrdXBSRGtoSGFEcDczVlFxM3d1ZFpZQlNLNERCSUJGMks2ZXJnL2NGcjBXSFBDL0JlcGxldWNNTmloVm9nSWlKckYxd05zRDhVSGNmVzFqZEYxTjhDdXNiNzBDQ2pzYWcwaVZsQWRsYlJqa0wzWDhPWko4b3BxVDlJOFR1bUorL05CNmdVMjlUTS9RcHBMQ0tHNitnNUJIMDlIWE1mNnR3QXVOVUV2a1NrTk96d1k5UjVudjlCcURVa3RNZzErT1hHTjVROWFiQVhXNFJEaFcrdzdZVkxUTFUvT2FNY1Q0K1hwclZTcUx5cWJMNGtuWGlBbWRjdEE4YXIySHhuS01nUmtkUWNGSlAxZWFTWHhZNWhrVGZJSmtqcHJlOXMzazY0enhEU0RuVlRvSkRDTHNoZlZjclZIVE4xYnd2UFhLZTFYdmd5eXdFN0E2QVBGaWRDeDQ5am9QQk9ueXU2SVNycVcyUlhDbnFEWHhsWXJ4VlgrRURRa0xHdkQ0eVJaTVNlT3VvYjZCUStiSm9pTlppMlJ3T0tlNlJadWtCNVZqdm5zMk9nb1M3Rm9ITlp0bkFyOHRLZ0hQOHdHakFpRXJMeUc2U2JDZEd4RXlmbTN1NXhxdUZ1Y2JVRUxhTkZNdURNRmFnUVVqTWpnWTlBcXpLOHdPN21BSExxUm1uVkl3b2JzdU5QVGZCSjdOU3NkL3JRUkF5N3pLSjUvcHg2cHNHdlFWbGdlYkdFQUltOERPcVJaRVJaMnhFYXVhSEt4R1hja3NTN3hmb1g0TGhwZy9uQjZLUXZuOVpnV2VJN1V3N0xqeVNqR2VEYkRaMGtaV050UFdFNENUOUZ3ZEJHcStYL0VHZlRNZ0R4Ujdla3p3Wjl3anVpczFiYjhWSnRFWkMxR3psaU1oZWN1UzZDTm1xeVM1SEthTnU0VTNIa0t1dzgwQWZOMFJZNHR0Z0xpdFFyWGpPaGV0N0lUcTc1Q0VnOWFXNytvNVFxanJCcHdpbWplcU9Dcyt4cTdPWDlCMHRiZWxheTdsd1JJNjBueGovTHRiN3NvWFJvSEJwVGtuZE12R3JlNkphZmVRNk9WV2dhWVI1Y2Erb2pXUDYyOGFrb1FMa1JVejFXamI3akZESUxERzIvMWhVSGxmVkJScUlGaThSMHlqaFJsQW03bStPVW9qSVAxSnl0dlpsNGZCa3JrQ2NyU244MjJUaEZraXN2aHBiK2VFNXNhd3hwdWs5T1ozeWpGNDNKTWJhc3BNemUxeVc3Y1FubHIrTmZHN1FaQi9maVVhTHFPU3I2SFZhMXEvd3ZEQlNGbFVhbjAxc0FsWjdBRXZ1bng1d3RqZ2M3clZ3eXVGdTA5UXNIaG5vcHJuajEyL2tpMEVUSXBkWTNtNWxtQVo4aTVObUJqNnRGNUlXVTBjSENldUlBYXBCem5WNDFTOURzemVldE42VlhEeHdqODR6QUdJNUtSVzBremRCaFhGQmw1QzloNU5JNjAwRTdNTS9sSzl0RmxkQlVqMm8zUDJZd1JsRlhXQ3pyajAwdnFla0tEUEorZUFKYkRYcU9CWmRYMlFrM2VNWklaTVJJaHJOK2UyS2hDYXBaSEdVbW9DNmFnQzNrU3J3WGNTWENOSFF0eHNZb0loZTR0UWF5b3R2UnVYWTlrb3lZbTJCb1BodUVKY1pMZTl3S09BSi9uSjNweTBGczI1dnZldEF3SG04REV0VGxNYkN5cytnM1MrZVZwOEdPY0lGb1NGNGFCUjdzWXVoQVN4RzJPaTFDY0RhazE2Y1RLenI4OUUzZ3FmZ2R0bXk5MCsrTmxvaU1wbFIzd1pVc1lQOHN5N3Zoc1QzYlZZWkFKMzNKaVRUY0NMNzNuSmlzck1TdGc0U251ODZ4OEdIT3lzYjZWM0lCbmZsSW1uQzE0OFpYNGdDUGF0TmswT3BySjJCeldRQTVwS0NORktmcHA5djVuNytIV2FoZzNzV1ltZE5yalJVQmFlYVBZSktxRXhHVFZMSmRBenFDdFNuK0U3N1JXSWprVDN5UjhVNjcxNUpYS1I2cUZRRis4UUppNXhrMGs3Ny9hMkhqcTl5N1ZmN0FqbE9jOEZIb3ZUekJpNDhFcXFsQjYxditqRkZRZFkwNitjNCtoWVEzVDFCSWRRcUltaXZnV0hYWUZhbzRzMXk1cStRTUg3NFZ2N2EzYUg2emxrcmR2QzB6UDJXeUZGN1VsSSs4Y0huRDVIUmZLZ3Fra2hZT2hNNGl0ajJKQU1EZkgrOTBXUElRWHJUOXBpVlFCajlqZXFHQ1ltMVRvSlduNWhmcjJWN0YybVpkNFZqMUhxT3FPTFlsaDUxN25qWWFwRU1PTUYrenFqOE1QRHhqSUw3SDBLcUJNcldxVVhYaGM4SFRuSnp0Y1pua3RkZXV0aFNXZkg5ZkRITlc3RklXTTdodkxpOHY3THZBaWR2K2hmTFRTZGk0TnNzZDZ4RklBZEJ5c1dMQ0xub3R2NmZpYXVUL2lSVk9OU0FSTjlwNlRXR3BKcm9KYWsvamsvVHR4ODdmbmFBSXN2M3hYT3hrUXNmY3RBckk2aUtxSDg4Z28vcEJENEJoSEY5OFJlK1doZ3ZHVUR6OGo1a0ZiTTVaWTFPUktXUktOVWpYdnlkYjVMOWJMcGRaVTVjMzROS1NqeUYySnJCM254MVpCRXM5cnNoZzVsK3pFQkZpRE04MTQ5amY3TnppaDhxTndndkkzcVFiSkZMRDBlQ2tyZUsrV2t6QW5SMG40ZzVUcWhrQ0xTK2dmcjQ4Z1F6cG1sTGhqUzJ6Njd0SktKYTZmK1crSkIyODhyK2YrSFpWRGxzeUNtNkYrS25MOExWa2VvV1k1YXFJanFHeHVzbDIwWGdINHlCczljZFlpNldDNUdxSlFaVFRVZXBndy9MR3h3anE0VEZQMzVucE5TbkdtdVE4YkpyeFJkWjBmbEhMQU93djllK0pvSU1NOW1qWHdFTEtWQTdDZkJoTXJMM1hBYTQ0dkw3RklpS095VkF4SklJelpoOFVRZGtJTlRQSm5iRmFBUTl3cnk1T3pjRUdlQTErNyt0L2E1Q1dsL3FuRGFvMzZwR0FLc3R0aC9NdkNSWXVVUG5BcWlSbG5SalF4NFNnRytwVThEa0xFQldWMHEvdDRpV0d6K3haaTFvQjdPUlluZm4zSjR3bVh4aHdwczVVTnNuYStYb0phek42WDBKOGF5MUpZbFhiVXFmY3ZUcllrWWNULzFIZkZHaitKWlRNdzZwSE5pem1hTEtLN0NXNGltTXphN2xhTktKQkFIUmh0SkxCK1BkZWpXNmc5MnpzRlhJOWZzY0gvQkZuTDdkL3FNM0tzclBPNjQyZ0xpUU8rN3lTT2JNN09uNHU4VDRmWENEMUEvYVZzS2tSOU5SUkRMelpNRitsNTdOb2xnSlIvdEw0dlhLVTlmaVlGaWl6TjhQb3BkYnRNTzAvSTRYMUJqbHlRQUxPTjdCZmxGWlNZOWp6c0lBdE5jN1hNRk1idHNaM3ljeHFEYmdZUDdoamIxSW5vK0p1WTdZb1U1REhxREI2ZjJzbjhNeHFRYytyR3Z0Q3NqelBQd3I0bjN4dTJCd2tHS0MxMkhqc0tUa1NEREQ3TGRobDFzVGFRM3NTMDVINXExYTZJTDFEbjgxblNDTTk3V1NURS9UNkgremUzYXV0eUNIMmZkNVN0U2ZUSmJNV0ZmbjhkcmRhekZUQnUzWDlGeEozRGhrTElWeG1UZ1lpcUJLd3ZuVmRYSXdBSCtJbXBnK203ZEl6YVRYMDlSeVI4OXNhRERBYzkvV2kvYmJpbVMzTG1iaGxtV0pvU291WmhZTUgzZFlMTVNmRTByemtEZktuQXVGUDhMdVhUcWpVTVZYbUx0TzlKdEp6UHdLSDI4bXBTOVRmUy9lVHN5Y2RnRzVKUlBJaG9OMHg1WHJwZFh1cXczUVh2d3FSZTgwaVJET1BBSWxOTWx2MUZDd0dSV2xwaGRhdy9yK0ZzaEc5RE10dGVPRHJ3eHdkaXVZVWxUbldpbVYvQ2FIVEpiblFCYWJKMEJUSUZuY0l0bHppL2E3Qm4reDJmRnZ1bnZWTTUrcXJTbUNvNzRVZnAvTVhOdXVmYytsbHR6TFdOZFhqUlZkdnRwUkN5K2xEdTlZV1JQOXJ4T3I1YXMzekgrd0kxWEtiWXdjckEvODI4R3kvL1BqaUF5Z1JQZjhzTEZmVGV2UUNJN2hJaEhwWW41R1dudWR1VzZMek9DNGQwNk9uMVlVVUZVM0hyOVNOM0tHMTdvaDVNclZ4N2lkQkcwWGZLcXZzc3ZkY0g0UDVZQW52YUluS1lOb0luRTUxK3Jia3NBZDBOUllvcnNYZ1JGQ1ZYT1FiajFKczlhTkxQVXFTOGkwY0ZVUGt5blRZR2Evbk9vRzlCZDB5em82Tnora3NCb3UxMHdKYUlmQUlyN0w4cjROaXpZanRxdWhPcHFMWWlkUlR0c1hBUUNkT2NqK2tCVVFsQWI2Tk90bHd2Y2RmL0JXSmlzRmZmdVNjVFdmbUgwMlF6WXJWNkgvZG9OOFpleHN1YzVYYVJXSnZYZDd2UzluQkl1RWRZbThtQkw2TUpOVnduZE8zV3dPekhOUmlPa2cvbUNOOXVveDFETFo5OTIxeUlPUWZiZTBOYVJidVRnd2ZjcFFPSlpHbnVOSjNuVDVCbzh3MG5CVXBOMGthM1NwcGZDWGNVOWZHT3FvanlqRTVTZWJENVN5UDRQenprTGtkaUxjTGx4eVhKbURtSVN0VGxuZkcrV2lYYzZINThSaFlqckc3V2U4U0lPRGZwSWsrZS9Mbkh3SW5DYjYvUVJkWmJYUmw4eG8yTjFlQVJaekpoVUplMWJYSjY3OXF3SGhpSGJjbmZqU3pLVkRsenVSaDVaZXVyOTBWT1lLcDF0bTZsdlA2QWdyMWtOK1hvSXV4aWdENlVnaTJzL0FDbG96Szc1Vk5ESjNEdDlEUFlqRE1ndnJyaEN2VlFXUXdHUGd3dC82L0pGdHdjbnI4VDJVOWhFVC9HMzFLQThHLzhkeVZ0VlZWNE9JL0hQbVV0aGpPTHM0VktYTldPNTFCUzJSV2VkTEd1cVU4S1lSWmFTaWc1TmpqWWtudE9TcDF0TkhaOU5SV3dJajBjNmtPL0p2ZW9HMXNhUlJ1RWE1Y1ljSUUzQWhsZWNxcElRR3dkWUw4SXVHd1Z1Y0xNUXVqbmMwY0hGUHE3dVRMTmUxRlh3VGhuTUg1VGFRdWpVTXhGYzc2OTdkS1ZFbUJQM1J3QlQ0WHJ3U25vajcrMUdjcWJ4VjU3dkN5cTl3cWNtRlZpdGsrUWtXZDhlSUhoTk5XQnRQeWYvTGdSek8yWjVKYi9uTjBDUEI2aGtHanBXamRQWndNTEFZQ2lQTExzSTIvcFZIRUdJcldqdzdtbGtBY3hhSTZ1VTFCWml2dUQ3a0NKRnJKWkMyWDZaR2RKM0htUk15SmhEN3dkVDVVdHpJVHBmYUFqcmc0ZEtPRnhuVHJMZnRvRFFBUjdjTUEwemdJdDZIVWNXcXZQd0pPbUY5d3FYVkZCcVhtd01pOFE3Tm54NVNIQnZJbFVtOW9WZm1LQTJMNXFEZis3RXYvZWthTEordnhaV1BtcHRZTXMxNWhHUkxlenloRmJXQU9ocENGTXlJYU5LOFBRQkNadjErQnYxVERhWFBmM0tkY1gxell2eXE4eDFhTUo0VXpIZGZZWFZaei95WXR3NG4zeC82MDNvaTAyeWRQMjdPODlRT3ZJWlY4TUJVQ01jczNxVTN5NTY5ZUsyU0djN202M1lIa04rTWxoeXR2ZGVSL0QvdUVxZnIwaGl2Y0NYMXdLaEVJR1NaWTMza1MrMC8yTWxqSnpObDZpeXJKMjEvZmtHdEZyZHZKckYrbm80ekU4U3plTzhXK2VIYzZmK0dmaWhGcU9DYStoekV6eENRbjlDMDZzNnFyMDJ3NCs3ZlkwVzBwVDBHTFVyeDdYZ3FCK2h2SG92dU5adC93U3lyUnRZSlA0S0VMV2ErUkJWTitEWWdaeHJLWEJidm5LV3VlcnFaSHpiRVUyVlNXQXN5N0xCSllCcTN5UElXSGZ0QzNRYnNQZmJqMlhzQ0YrbjMrQ282S0ZvMGJJY3NDaC9RLzRlUlUwb0pJMG1CVys2alR0SWR4STBTUnR6VkxaZEdwaXYyM3JyNTNXQUlhNUpIcFBtMmxLRWx6dTZaVlBuMHc0NHNPeDVBQXZQaWdKZ3AzN085N1RkOEZSY093T3F2WmZ5WG5vd0hKcWpFR3dyU0RVQWVkeVA1ZmFwZGEvb1drVEJJbWVIRFhlUy9DbFpFbzBSZVFYS25FbEdLVlZkTXdxTnlybVY5eFFaczJkZ1FtNlpoOWRwN0tmNWZWTmJUUEViY1lsaFdhVjNFWWE1TkxFbm5nbmdBb1U3eTZxbEszQ2lSZnpCdm0xZmpVQzRhb25XSHFyUDFDbVZTSEg5eUpVK1NQYXc5QVdPTDY2YUd0bkdqbEgyNVVnQjVpQnN3cnZhblROYlFLVU53UUdlQWcxWTZvREh5Mk1VZUJaN3ByY3NKYkZqVHBlQzNrSDVTeTQ4cjRjZk5XNHpNWkxEN0UwWEgwTmNxNGFjLzBCN2hyRFZOTzVvME5iYVd5RFhMVjczQ1hkNG5rRlhhZmtRb2FLWEIrOHVkckpHTkg0SDcyL1YwL29oVnBvb3pnM3lCWGhvRlE1dXA5WXVqWExHdHpZZkw4dWt1bFZERHJrOGVaQzFZRTJha0d6czdzYmNzMEQ4OWFCWXRYYjlPOVJtSW91bDJTajRaWTMrM0FHcVg3ZlpTczdnY0NNVjdnSE9YRGJaMjNFS21Namw1N1VqRmNSQTg4NncxZk1ZMGlBNkI2aWF0U0JmOFl4SlFWaWh4QWFsVDg0ZXhHSVROK1ZLdGUyOFB6VVRVRURPbmZZME5jN1RWWlFpVjlpemdNcHk5bmgzYURET0pFb0VyM08yRXgwRGVhVFMrdW40RW1sdVFVWk9tNTZjbG1ZM0NHeWU1ck43V3pLNXRKZHAxMm80RjMrQU96bCs2VVh4VFZHdjdOenllSmxPRkVrY0ovQTVFSXpWYnJNN3Z1SFk2L0J5WUFjcWpSWk1vcnJLNEcxOXRyQmRUTUczbnNrbWZUUnpGRmRrMCtkbDNEektLU1pyZzhQNXVHdVZJQVp4cVl2YmgwdXpUUm9GVmNVSTFzbVZId0s0VTFwSDAzK01YZWoyNitNZXRTcFQwOUZFYWgrZUJNUXdYbVVSU3l5dHJMN3RGdHFLTmQrTDVBa3cyQUN1NGVDT0Mrb0pseFVWMnlYZEdaZ1N2Qi9VaXJ2azhta1pQejhUTXJHcFBpbG5NRUxDZk9lSmdSUTBXQ2R6enFRbmViMXZwWW56UWxQaHYxa0ZXbUdsKzY1Nkp5MmlackxTVHdCZnI4Zks5eFc3QWRMY0dIS3RIVzh5YWVuc2RSQ3QzNm9vTlNwMFNzUEdLZDZaVDVNaXFkeDczVXpKMFBRNGVMNFQ4UEFZSjdhb1h5WjFxdkJZMk83eENka3VQYjNxdW9nMUNWL21oV3V5bW5Lc05FZmVJelFqeSt2c0psRlQ5STFRRlRZK0pza3dVVDJKaUMyVTN2d1RPSHgxMWN4dDd0akhIcnZydEFaYlVWa0F6eUxybWJOS0ExcjFPM3pGMWlzWWxJTEF4THdkMjZzL1h0ZllVSDMyVjNqNUY5QmR1c0ZtV3RlY0RQU2VUS3AzL1dTRnZXZUN3NU9iZEoydU5IZXJ4NllRYll3Ty9XYTd6WHFHS1Y2VGNnRisxbXhtTDJ5S2FjbTBES1pBaGpyc0dBZ2ErV3BpZGV4aGRzd2hBbys0dDlPKzFGcy9qZ2Q3b0xmUFgyVjQvNWY5MXhWbnVSYWlzaFAvTklUclo1TndTMVdWNGp3OEdmMXRndFhPQVR5VlhtVWxEeUxDRFZlbTRDelZ5SkdwUDNxRE9CY3h0N0lFQTM3ZW9GeUZKSTZqNHBVMGZtQVZ1WTVwaEhUNTJQcVNyQTdoUEZjY00zeHpCdFN2NzUrU0dRMUdBbUh0TFlSZEtIdUJHY3dlWGpXOXlDeU5sWHo1Y01pdWdETnhDN3FrVk51d21JbzN1WUtYUi8rSkhVUkZOVk1YU1BrNHZYWmE3bFZkQWwreDV2VElVenpXQ2FTYjNIWXhLdG8vbzJvM3NDQW83OGZyVDB5KzUxb1NjQmF2NTk3NjdDcVJyaTNZT0Z2U2hMcHJGOGpoMEF0aE1STXl5cUZHVE45amQ3Y21RTUVtSGxwMEtHQzk5OU9ydW1JY2tuL1NWWGFJU0liQlRiZ2pxdUdVZnhWT2hqbTZ5bno4dlI2cHRMdFowMy80TndlYmVreDk4cVhkSWlIL09udEdySXBuK0Evcmw4cEdkSHkxTGtGQitWSlhnYUk4N3hVU29od3lhUUU2b3I2OGhzd3RoMWd6VHo1aVM5d1MwM0NiQ3NwTkViSHpWSmtMeG1ia1U4SHdETUJRSDRKU2NBbTlmamFBbXh2QXpGTThBb1dScStPV1drM2FrNnhGTWlONGluVVl1OTJ3ZU1EaHg4akRZb1didUZoR3lKeHdCUE9QdnNJQWRROHZ1NHVYclB1bUpxcUxRZmtobEtKc3UrTDh4Q0tVQ1NtUFhqZkIzRkpTclRzMzFTdWV6bGFlVXRiNmF4Uy96L1lmTElsRW5UN0lTWVc4Zm9MRmJOVnYxZnVLZkEzQjh4bTM2ZXJUMG5pb3NxWFZ4SG5nUUowZzBhWk4wYkloeEZmUTB2VTN2TjJuRWtXRGsvMVdRZU8vTkI2aFQ2RFpiS3Zybmllak53Vi9xMDRrSlhlNWZjVGpZK3ViMzRuQ2FyYXVPQ1dDMmN0SUt2MFFuV3RKdWt5S1ZvYmtkT1lwMWZpM0NVN2wydHY0bDFZYTVRVlB1a0tPTnhnNVVxbklOT0poZDlsSWJlVktYOVp2TDdWczBRVk9FdXFCZ0ZMNWptNG50QURNRTBxVlBxb25ETm5pVlRMM3RrRC84SE9hM2VMRFJyUW5wNldnaUNNb0RsYU5uTW14dkFJMjJtNFc0TVd1cEljTEJpK3o2eEZWajJqRDJzaGttQS9aZDJsbEdZemg0WGpKck5vQkFHY2psa2lmRXdOQmtCM0lLVXJPbUVaOUk1REZ5SWVkb1RuSHhUK0tqdVNNaXduam5ReFllekRueE9BSStSdEZldU1PYW5VZDdOMDBEbFg3d3BJMnBDYVhyeG9mUHpPZHh1QXNIcjM4aGNObHhwTXk2L1NuL3BvenZiaHA2NXR5YTFTMGNuMXdGWjdmNjFDNE93bi8zb1p0VE8yQnNUTkR4dzY0WUtqbTJNN1pBRkRvbFdpQmRmanlHSnZjaGkxM2RQUmh4Z1haYWthYTNxQjgzYnJDS2hCNit3K0VkKzNFbkxXU0loVG5DT3NwcmVJSWJYYXpJY2NPaUp0U3pzaENGT1EwQ3FhTXJHeUYrQWV6TythbWVSc2g0UDF2OHZrOFBWVjNtcFJVNmJjcE1EMlhvZ2JFSklQamphbUQ0ZTJuZkd6NStvTWYwL2REb0ZIZEZuKzE1SzdjVG5sOXZKOTN6cTVUaG00aG1NakZpMFBmUnJjNjR2SWFOWnlSa0p6VGxMMW5DWlJkUkl0aWcyTVA4Y2hzSnM4dXVDODdXSnZ3SXlSWDZ3TlhJakhhdlBzeDZ5Yk96cTVUblduWHo1akpaK0VYa00waE1ReVdKclROZWcwSFJuQ2wxeldBNkEzZHZvMTBWUWJRMGlQYUpwaU51OVozb0RPMHd5MnVZdURncERmUjQxM0MrbWpZVWJLY2NhakVVQ0NPazZRaisrZ1VSTHRZb0N3MC9mQzJiYVg4bHRWbTF4ZnBVeVF2Y2hHcjhmc1dxYUs4eFdtbC9hbytDaTZuSlJtcGxxaSt1czhVRnhLbzlRbDBrSmdEcWt4RDhxbzFlNVRDQStNcExQV25GUUxESFJpbCsvaVNuTUZBRGVvWE9VdGYzUGFMQVVhT1lOdUdXVUhjQ3FqQU5yWkRyRCtLRHRRQW9uNFpRRFlrUTFBd0VGRk9oWTR5aXcwci92Uk1qRVBlTlMrS3RXay9wdkVRQ1VEc0dIT1Zoc3djdFcxdTU5UTd3bDd0ZHAwa2NNa0RhQ1dkZ0xKWDN0L3JCcG1VTElFeDlPaFRTYkd0Z1E2cjFKRzdoQXVvcGNRdExFRGhJZmxBU2luMklOTUxHUkh6b0xwV01qWFAydXNMdjNMSy9Xb211RFd4RWp6eGgrS1doZmVaVVYwR2ZOY21RZHZrQTc0b0NzQ2ZmQStjbTJuS1V3ME1uWEMxY0ptYlBncFhwbkR2dXRBeERwU0JHV0NCT2NScllsOVNkK25HWWZvSU9zT3Q5dUk3MkFWMG9nNVl2MnJUTVNMUUdXSWFxblMyQTZYWGpSU0tweFkxWDZWU2twVnZ2L1NBWmVIUXVCMTRvREE4Snl6VjVYTU9NUFVxZ0N1Q1lveDVEb2tIL1RUeEJiaGJrSmk2ekNsNmZvb3FETnFoQmNreVNvbWVRVE5OVjlxR2hrOUVVUXd6L1VGODZ4OW1Pd3c1dzhEYnBzeUpremVhVlhKUmRndW1sVFNLbGxwcXJObHZrSGMzZUtpWmVlNXNnMHp2YnA5a0VYUlpLVjExdzZvY0NNVGJlM1RQWExUN1FpSUtXZnZUWTU3Vnp3WC9GMEs4WlUxaW5JcDNodW5WQ3dZdlUzSzBZaERpZC9qS2xRM3BocXR2aTRpUHNERUlsZFNkQTNWSWxiMURCbDZSRW5PNHEzZFhRanRDUm42eUNhL1JjQkxTeXQ3T0gvRXpwZktzTkdsWTNuUGpqN1MyMFdHajFEV3hTRk9oNWJtS2t0ME5oY0Z0SnQ4TFJtS0RJNUhkYlZ6Q2VqYzRDL1hMUk5ZbGlJc3hKZEkvWXkydVE1cDlpa21KcFFhQ2o3bkxnUGFraFlXSEZGYnVyYWVVWlc0Uk4rcHVOM3hJSTZRMjJtdk1SNi9mRG1ReVBYNWh1WGdYYXhJWHdtaTRpbjNzUkEySXZzNlpyQ1QyOEljbDhYcnFHaC9MdjF1OFZsWWpORmxBZnRQbW5MNlNGNFNWSmp5cHpYMkR4SldvVzV5QThrUWVIVFVab3VINXR4aDFRYmV4VWpuaXI4YkJLZGlubkNWZ0VNbEpKaURRc1ZJZ3pIdkhZSG9nc0kvREZJTkc4VWthWFFXMG9nTVdTRkRFQVp2cFBKQVRTSTVXUFAwa0V4R1NwTzVJalVGdEszZEY4dnI5cHRUa3NkVnlHOG11dzd0TWUvM1BvZkFvRmJhMjExMnlyVC9xR0JQcnZaMDhDSU9OMW1DeHU2b3lKQzFucXRlcVcwSlg5c1R1RVl3R1NqQU5PT01XcEl5QmQ1eklDb3FFU2Z6WDk4bE1EUWlzaFlGRXllQ2VkNDIwRmsyVFpWUnhvdXlncWlrTHhPUEdKSHFlL0xjVlc0ZVhVbnJVUnVhN21mU0QzUVBhK0EzWlIwU2lzTEZGdzRObmlPTFBVTkRHSEcrWUk3VGZlc09Pbk5tcDFDd1pkeHpVM2RvYmlOcUl4Ny95VkEyTUJXZTN1VnZ5MXZqL2MrajVpdkxaVU5EQXBKN1RmcUFzT1phVWZOa2FGbGp0dFhvVkxNTWdRYnpJMHUySGUwaE9aUTRXMXFWa0ZWejBxeVRqbXNXUlVwSDBYZnZuODVPSEhWVmZlWmdTRm5PSDVGbit6dlZkYjF2ZzdublM1ZERWZlliWjhybXljUmlHczhzSEFxRzI0Zm1qRGxiMlVSZVdRN0Fna2RaeE5GZ3hUYnRmZ0YzdXkwUVdHSEVuWEtGQ3hsOFRhTEthTHJtYzUxWHFiS291c3lnK2E5dVozL2RPaEdJbFRyS0llSWJWT0l1aUpZd0w4N243MjM4cXh4VWhLTE5NRncxWVBPclZKcGx6YkFDTHdMSkN4RWRjb3hTY25FVjlCWFVCKzYxcHFIaTB6TTc1U1pHNVZ1YlpLWWlVR1RvbkI0SGtnbFA3VFd4aVVWRm1ySnJBalNYaXN4a3BJZUxDaUc3a05ua0ozY0dRR2JudHZ4MVlKZy9VYTZrRkJqQmJlcVdoOGJWMmFSaXBrMWFpditFK2FjMnRxK0t4ZGw1dnU3L3hOWWFwM00yc1dJZk9xRFBjaEF3NzNuSlJORzV2aVZoWExTZE5ZWUJ2UFg2Zm01N3BiUjIxbWtxMWRaMXNLZk5xYnRyT0ZjQjgvaDExSDFNdWFaTzdLR0ljM0xwajRQV2pZQWdLRTVCTHo1SmVvVUtxYVYvcExyUWdPc3lYT1QvTzV1dkMwT1VHcEdROHV3V21FNVZrQndwOFRqclJKL1VCMEd5alhXd1Z4UDcvc3NwYWl5NzA4SnZJQ0dVMFN1M202R0FPU1RxZVJabVptVUI0WjFTdFplR1Z5UzZCN2F2enhKRWhESHBrZVNEb05VNGhBTmcxcE9EUmEwdDB5c3c2TE5nVHUvWjhDU2dna0dpeHlEOW5kMzlRVkdFRHE5bWNQZ2RsMkpSOFlwbDU3ZGR0NWphYnphVHhsUWRNTVdiWUR0cWd4YmV2SStubkp0Um0yLzBPeFplSFptMk1QN3FYL29iOVRKNDA3OXZjQ0RBNUpLblNRQzBlcDdpcG9BUFNCb1dzMWxYRFZ2MkluYnJHbnpJeU5lSmd5OGlVRk1yUjZINnpBMzhMNGYzMytxSjFqb0JHdFRNRlVHcGdXaHI3ejN0Lzc1bDhIbXE1bzdkMGpMNDhyc3dDL2dvaXhhbEdsN1Y2NzZSdG15NUZQeittdy9PRXpIN1FIUkU1K0J3Q3dWVVk5TlZxVEFGcXk0cWlmVFZIdEJGSUliZUtHeDJEMmQwSFlZNmdPb1NrdUE4cmJoLzA1UnVOaXU0Nm9talNwYVZQVUM1VXZPMTBLUGpZL0JxcU91UDRTbVhEVXlkVVFDV3h5KzZyTEFsdUhxWVdaSnIvREY3aEN2Skx2N3JpKytnNzUwdEJXUjZKMEs2YURNd3o4T2g3V2hGNmIrQjlvdHBOME5vQ2h2Mk5lVEpVbXlhbGFqVDRqS3Ayc3V5SXRRMmNORXhJYUk5cGRLMW81aXo4SmFWN09hK1ZyR1FZNzdYdlU2cTdySkJ2ZktNL1p3Q1RvVXJPQ25IL215c1BNYmJ5eHdXTXhscUJuOCtQcmlCMkFZYkpxVmRPV0hHcy9NZURmMHY1VWwvVXhDQy9JRTB1bkllNkk3UklOM05WUmtiUFpGODNrK2oyanRxUkU1M3dQQ21NdFljbzczM01vVWh6UktwV01wYVVPcFIrUXZqRk9hM3M5bDhnZ2pBYTQ3alU1ZnZRbWUrMHNBKzVIN0dOamdpYkNzazZIQVVaTmhIQUZPdkdDaTlPeUNHWHA5R0J2cTRYdENJZDVuQjRwVDZFZlFmY1VIQXRjaENVUkpYcnRXWjdpcWp6ZnkwNWtPbkJMb1ZCbUxGOXJnVXRoWjU0VWcvSTBmaFJCekQ0dUNIczdxOFlmaXlVNjBvOXB4RFdVR0hOZkREcGxydHNhNk5nZlpGMkQ1RWhJb28wcWNJaUJScVFxRUhEdTZjbUE1MFpKcEZuVml3TFVSTUxxQWF6VStua1Zya3BGUUJFYzVIN0NrVVRWMlZOTXFHYlpReEVpOGlDRS93RDhZQUhUelZIRGlhQUJzYlg3YnFZdEFyYTQ4Nmo4bGlKT0Jlc015MnpOdE1MNUZXWWcrei9DVmNUakVKaU9DS0d6cTAzdzRKeXBCY1UxZ1IzNlQ1NUFmUzNic25hdmhIUFVyY29ZRGxFSks0QXkzSGFkYUdwWmlyVHJFYlFiU2krQlczQjhjUE04TWtONmhsQVVXWmp1MkdjNFc2cHpKLzFKK1dReUM3K3JSSkpRRXVLZ3MvUlRlQXZGbjZwbzFFcGN5TDVzUm1SMzRNUlZZL3o2bjZraWxUZlhqeTlwMDR1UDlVMldmVVJTMEg2Tk1wK0Fqa09GdUEvVlhHeGJrUmh1TFdrdjZyUGR5dXg3R0RwMnJ2bHpleFNJTHRIclN3R05LQ1lNc1I2QnV5aithNnd2L0wxQktVZjlsT1VNOXdGY1JsKzg4UUNRaFRTRGhndHBxdlFZaVczbmJlMmhyb1lDaHpGL3Z1Y0JJOHEyMzNuQ085T0FHbUJqclFPZ3VzbmdYY2JycXA2SGp1N2daQjRybzd2UlU2eHdWd29NUk5JWWx2a2UvaWZ6cTFKZlEwa3hLSTdXblJVVjFnejNoZkFORG1oblpDdGRUK3luTHIyRTBwbUtRcllGR0tHU3ZQNExFbkdUY05VbndvaDFOSUgzbXJKN1RRb1kzbHhBZFVNbHhaaEFFR1FIZVRrbE12dTBrd01wM3pjOHRmazVHdXgxN0ZacFZhL2thWlZzTGo1b2JrWHpMWk5Dcmg5YmNKNnhpeEk0cTgzdUgvbDFJaU95YzFhWFFyQlFWWTlmOXdWWk9wTnpqbWNXeVdZRXN3bG9ZNHV1ZmlkOUEvUFppbTBYcFRvY1JibVJLTWlKcjdYb3pjNlZZNjRQMnpOK3JRSndkc0NhRlZHRGpOVVNsenh3WjhlVDU3MVplN2RFZXRGTHR5TXNwM2s2YUZqMFlSYWI4NXlseGVGNUVTQmtqQ3VIRTZJZmo0dlBGSWNOT0phWWZqbkNkSWtMcWNndjN4azJEcmlpMTk4L3pmV1FyakJid0NpZUl6dHFWUFdDRTdsamV5OEFoSVZVN292cnZkakVZNGYwVVVtOWdPSjg5K044VXhJWWZpRXdiZFJLVUlyMXQySENGQ2o1SEZHY1BocXppZnV5U1pjZ2JhWC9ldis3TUpiSS9ZQ1hGUEFubGxSaUJ1U0trWVk2Tk5SQ2t2a282SE04QU41OUl0V0FTckxVSjFVYU9ZM0N1R1pzc0g4WFI3M2xBL0k0dnJISFJab2p4ZlR4ZXFKc1RHMGYxYlpkNUlzbzNyVElQMjlLbmNJYUNPTEduVzJYaXA5Vzl2QktoRm56am1jOTEwdGxUN3YwOXFHZ0kvR3hNMWRwUzJxSkVyS3VUbzV1dXBoeU9jTXpSbVBVVXRKbEI2VTVPRWtOK0dJQmlCeVBJVzQ2ZTNmVjl5T0RRMVZyVmtxRnFaQlpwWXd4eTlKQ0hQWGgrZmhoaE5TV2lZWnQ1YXVUT0FFUHBvanYrc09YS3VQOTF5b3gxT0pRTzBUNmJKTGhVY1pOZWYxUHA4MG10K3c1K0EyVWZvcm9XVGowbWJkaVB0OWFsSGtiT2hENGhOM3BOQ0o4WEFyUlM2S0ZKUzArR1ArNXdVaTBVL0p1WEtybGdhaWFjUklOMFJzaFo3S1U0TS9MZTNZYjRrT1pDdVFkd1hxRGh1emNpVkFDYUpYOUdlSVNubWRySVcyOVN2SHgvdTBML04vWXI1UEI4UkVYZjR4Tk5hUUx4UW0ra2lvWUVYdlZkRis3cW9pNThvcjc5TXZaS01aYmdEMk5NYmUrdXVEVlc5V3BYRTcrVWFlWWlIV2dLdW91QXhSY0ZxMFJZdVJWcGVsVEJxUFJoeCtidnFTZ0syMUFZeTRRcExDR1R2YTRUakhiYVFUUndFS2JFb1lEMjdiUU45MWcvNGl4d3FwZW9YM0F1V2NtbEFFUnBwcDcrU2hxVXRDRUlTbTREdEtNV1NMQUZDcDJmM3A3UlU4MGVrWXhSREhyc01DQlBGQWVyWmJiUnZtdnNkenZpbVF4cFcwc0RENGR2WkZzeXQyREJmbXlpVURRV0Eyam1xaG9pVU52MU15dVQ3T281VG1SaUtJZVl0RGF6c1gwUHBRK0hmYW5FY1B4eURoQ3pqaW44Q0p1dE12cHF2OWl3QVBHUmNaVXVZTHhheFRZVmZ2ZlBlNW9mdDkvNFZSbWM0MnV2RGVZYitrNjg0UXljWG4yMlo5MjBoQmw0SHFyUXA5NWpYaXJoYWxiRk14TVA5WHE5bXd0WndVTVpCdkpGTWZlMnhpUDQrMk5ReGFYcGl2ZWRHMzhSTVdUVHRZWHpFN0c2UlBjNWc0RU52N1VYbnExNjZDdll4L05ZcDBzdTNubDZzQ3ZsRTJXU3V4TVcweXhIMzlaMU5RUlZFNzdEcDI3U1VRRWo1c3FWVGpzeklBTmpROXVDN2VXQ2RGUHErdVZ5YTRjSWdRS2FyNFovRS9NSkhlTU5HR0xpUWNiNk5jdkwyNmZXTVJDZWo3UzN2WllRdk1FTHQxTHhXMytTWHZRZHpXWUE1TVBiNHgyU1EzV0tlRW4zdmxSazFXUEQ3bUltNFhrcjRrbnh5ZURyNGZIY3ZVNnZRZGsvbzIzSnFzcXFlRWRucjhGVFUxMXh2bFZGMFFJOUx1TG1pbVNDWkpvQ0xtaWFVK0YxdmoveUFNOE9ncENCTDZPT2hMa1NlZ1V0Q2ErbXk5Z2wrVnorOVBmd3lZb2diRVdSbEtlVC9QMjN1Qk5sOWxmNTV5MXVVc3JlOEFxQVdCai9KNmtzWnJVeWxWSGVnTndHbXlmbzNJZlBmdTBnQzE4cnptdVUrck9LbWJ4ZXVnYWNRNHIzb2hxdmVQOU9pT1JHQWlmMWVlWjdFeFZ4T0NjU0VWUmRWSW02WnJtY3V6d1l3TWp6dXZlY3VxTWw4UUkxdzJuY0lKcDBSTTZFdDd6N05Dd00walorMWlxdTh4ejdPS2Z5ODV4VzJBWUtOWGFGcUJRRzBEQ25tclBwSlBSM2JiTXlkUHpFZ21DSkpoSG9ScmFMcVM4UnBoYmc0bzE3THczN0VONUE1b2MraUdOeDVFRzBBdmdTM0ovNGVRd0R2Y3hPMkkzaXl1MGtpeUZ6NUk4RVJjZzRIR0wwRU9YaytDN3RkU3N2d21JTkNzZUVaaHRWMlFhSzRHODBvRVF6U09jWGIrZXU1SmZqS2V2YlFOY3U2UzIybW9vOEtWaGR6QVVscTI4dlY5OXJBMWM3N1cvaGhGK3lKdzVXZzg0dXlLS3dpZGtLSVRjYzd1NkxFVXFZQzNVdXlPN1l2Kzg3bnhUb01wSDlqY3FXNTlVU1RjeGRhYjFOK21NbUFIVS9qNG1rWDAzU2dzcUR0Y0lHeXpYY0UyVUhvQ1FLRGNWbDVVM1ZDR2NtdnFLUk43VTE1Q2ZrTVlJSjhwVUZJRXdiZ1h4aHV2YmV6bjRtMCtWVHEyeDNRU1ptekxOc2tMbUIvSzV0ckJCTUR4MmhDeWFlNmtGK0JTamRNOWUrOXhoYmJIRTIwQW5oRlJVZVpXLyt2bDEram5YT0JzcVpvL3BKV0xBSkI4bVVxaEZ0b3pQNVlzbnFCUGZWNnBkZXd6RVRaTHBkeTdWOGNHSXoxNHBtU0RRY2NrWlVqTkVHa2Z0MWlhckwwb3RxWTJBc2c2eDBrem5UcEE4OVNDbzFCMUVJM2M1Y0tLZzN3blJlanJNR0c3ZDh6SFBraWw4WFBIS0F1dHhwdW81UEZKbG1VdVN3RGwrbWtmS1Rva0ZoMXBFRmc3ZHJLLzBkRG9rYVg1RGsrZmJlSzhydGp5cnFmNWl0SDd2Z1NZZmhCcEV6eVhkaWlCY2FGWEpaYlIyWnBMekdlRzBoUWc1MW1kNTMzNVlmVVc0SVJVSklxTmVndGRhamZHMnhKTWRtN3hkbEg1dFI3ZDhnWFVtQjluQmlldmsyNWJ6K1pCcis0QndHUm9iZmtobTl2ckpQRmRPRVJPbVZVdEF1c0c1YVVJMFRyWkNnZCtzb1UyeTVib2lCeG9xVDAyK0szZ3pTcmZXaElGb2ZJVmpMbXRYbDFWc04yalYySlRTa1BQVWVPbFVCSWEvUVByTW5VLzhFeWNxdFNCc1NqN3prcG1jbko1RGNXc1VTS2g1NkorMXlSU3FXenlDclBZZG5QVllSVlZQSThlUkVmTkhqL1JtWFJzUmVoaWZKVi85dzJyTDZmWnVPYjdnNFpUemZJbHluRHBKWk9IdXg1NE5LNnlDTlBGUUZXYlBSUzBWQ3d4WmwrNXozQlNJT3dRVDM4WWluQTZidE00aFkyNjUvQi9zR014dW85K0VjUjlJdDJPL0lzSllYSWNLdU1qK3VKM3dHYzQ2SC8xR0ZqVlJQZU4rTCszS2ZmSjQzTDRkS0NiYjJMdkZxblBvNldNQnlwVWJtV1ltS3hDeFp2S3Eyd3BKQndZZEltUWZIZU51d0RSa0VaMHF0Y09samNqUUNsWGkrMXVKZWRqcGgrYzhXZlpsdUI2Q1doSzdPTU1Fb2V6VHZSZFp3ZTVYa3d4ZStmcmNSaVlRVWtXWFpwQklwcSsrbHBiNGoybXJhVkI1NTliTmUvVmgvYVY5NVJsTmNpVVZzVlN4ZDl6ZzRlT0FYQmxvN2FyaUp3dUo4ZHdJb1lnaWNFZ0FTUEdrYlFyZHNjNUM5UFVHYUs4Snkzc25ldldtd1I3djhxSnRLK3B1bEVyODRoT04zWWs4WjhuVjNFaUJZTVVKZTd1K0QrdTRYb05KOW93TVNqZFJJOUplMXB2cmdWRnBlSCt5U3cwazhOMU5JVEltWVAyaVFRblJ0NTRDci9KSDJFanNUTEUvWlRpUllIT3dUOXVkaHgwUDQ1ZHExV2hRb3IwUDhlTTZKMDNtRVcwSVo0NlVjQmZQVm5IbS9oVmtnL0hoRVZHcVEyVEN1Z003UW1hK3VkQlYyT2l1MkdVQ3VWcnhjc1J2NnRab3VRWjlheUEwR3ZoWUJFQ1NmdlVjUDI1TUttMU5idmU1ZGNzVm5IdUhjTTFHc00wQUVoUitpRUlTZnl0R0JZZGc2b2ZuL2R2ei9NYXBtcTlCSEJqUnB6eWhSSU9vZEVNRFFRL2ZBR09XZllmRFptU0ljVXN5aGdidjhTNFpXQkF0c1RCQnF5Y0l2UFIyNjZ2R3BJa3JIdUUyOXNGZk1pdDkxTWM2YmQrbUFDWjVRdmtOOHhLd2hiUGNOc05DZ3lrTGtneGpTbThQVHhTWEc1YTFwK2RsUmxiMHQ3VUcwTGdldlViNEkreXVJTjZ1bURoTVVtUlZ4clZmL1ZWNlo0UWE2UENLWVdzbHNPcnZneVBpdkhoNFN2cVhvNG9zenRTQlkyUUNscXVIL0RwazlTQVE1WjZMUTdnUmZocFNjaGZDZFVDdWNJNGgxWFJIUDA2NjBLNWt2YUI3N1JTWGNiU3RVWEFvTlVKNUpCWU1XMiswUnJwcGVMejdTdXQ2UHhJbWNRWDlFVGQ2SUhuZmNnMm04QVR1UEVObXRodzdveURvbEl3OTRyS3R6NndxU0JBaklZemFtbGZScnV5ZnI3WGJ0M1lIVTlEZjN0Qm1jczdCUllGYnpqTGt5TVZZTDVsSDAxUGFxaFdZRXl1YUdCMmllZWZxelVYK24wOE5mR0hXaEYxbHprdlNzaENodEpCTWhld1JFWWY3TFpCcjcrN1B4NzZyRDNsSUVEMmVuREhIOWRpY215RERoRGxQZ0FudWQvYVNpL0Q4N3FwNGEwWkFoMlk2REJpQ2xOSUNtZWNlYW1FWFczVy90dU1jNEx4VERYbHBRc3RlUEUrODhKMElCRmlYaVlhMVN4NERDeU5JUUEyai9ibU9LeUN6UzNzKzdCTGxodkVLTE9YNE5oUEp4QWtrdEExMmFJRUtmUUpHZDFDM2w1ZjdDQkFES2VPNUZhWWVxbFpWak93bUFFWGUvNU5SOXkzNmxLVFkrK053cUVTY2xLL1k1V3N1QmRBWHF1R2Z4cHVPT0RHaUNROHU5eS9rRHV6RExQT2wrbFBhTldlZU9uR0NzcVdBOFdiblFETzZDMng5MGZqZTFTMU91eVNKNERsSVpWaDZZY3hXeURsT3d3Nmt4Q1p4WVNBd09oVVU4aFJuRnA3RzdqeXlSQ3RoM3NTRnU4bFA1ZklDNXJnZVpnbkVhc2dneUlaVVRscFI4dC9PbHpOc3RLZVNMenI5SWpXbnN3V1RBL3VrQkZnaVBDZGxxb20veHJmNktGdm54WTlFTmpZYmUxenhodWlZUWd2TUh2U29FR25MRHJrVkNNMTZCeWlkbkZBbFBia01jWGpHdnpXcFBBS3lTVWhZVGQyUXVwZWt2SGFSazR1MllhMlNVcU4xN2FUWjUrMGRiREhFQUV6WmhTZ1J1QW9pZDhyaEZYS2RhM3MxSkdWbWpXclpRMlBwZFNQWnBueHdJL2VJbzExcjdpTkZyRndmQ094em9IVTJlSXFmNE9mZWZDWkZZMHBqMnh0U21XUEpETkRHRnJ3dVpVNC8xNmU4V29OL1llMUJzNnZIblVhdHhDUXM5WDk2dnRHTm5PaEdFKzd5T21tTm9XbEFRbGNwZHRZSHNRNm1TeWdaTnh5b2ZPdjI2SjB0S01MWXFKRUlUNVNlMU90TldSd2NMQTJpMWhIUUU1TTJjalhuYlV5b2xOMUp4clE1MVdOeUdwT1JkRmRmSEl1aTN5NUh1MThva1NVak9heFlKVTNDNG1ac1oybFJyRG95U0VmMTJDeVo3eld3NHpnelJpeUl1dW1nV2tyODlzZXN5SDJFQXR4VnNraHNablVmdnhNb3Z5S2VrOUNZS0hFTFZ5TXVuWUtOUDV5NVFUVjFDaEdIUlVCMFVsZW1LRURMSENZYXorUnpmSmoycEhxT1BWZFB3cDY4YXNac2RrTVJuZjBRckJvOTZ5SStzck55cHU2OG1ZNDRZL3lCSk9SK1RxY1JmbXNxTllYeUlqelFkQXFTZm5HWUNKQzBrWEdlYkVBcFRQY1JxaHo1TDB0M3ZpM2NVclg2NkdxUVlXSU9sUFFPRkZQd0xnVll6Z20yK2VveVpPck1CdkNlYlQ3Z2p2OVJlMXRlYzZhbS93UG4rYUtXL3h0bzQwMDRrR0pZM0JsbXdtM2lGSnl5RWVKN3YrbldZZWtrL3U3OEFqcGdMUFQ1NTFqaC82VG94NjdUQ3ZkTEw1ZGRlOUNBeldzWWZpRmIybHhZUTd2UitSLzVtNWdlcGt0WU11NkVqRTlRbmUwaVVwRCtaWk5aT0duTm5PeVNMWXYyS1FmS2xjbHBmSzhCM2l4WHU3M3NoWllwb21TUVgrMUxhSG1SSUVWcVJIeHc0ZVFjd0wrQjJmclVkVnNrRnFvY2g5dVRRWGViQWdvRExEQ1J1TndIQVNLQUxmR1R3MmdmTmFLWHhPMVMyVkxSdGV0cWgvVEVaeklXZEhLc1hkY0ZoK0F0eGEwMjV5T1NVcUY1dGk1N2I0S04raGY1MW1VS29RSEJlZmI2ZVFrVG8yOGZNbGtDaTQ0bEdxMDF0T1p0czV6YW5yVXVQcllGOFRFRDJBZjk1R0FicTcveW1RQ0JzTytCK1dMTFVyaERsa1EvZXBZZVgrUVdMMGNMdWVPTHR4ekZoNi9TdVhMWTFMSzF1U0pnQmc0emc3a0p5bnk5Yk82YXRpTC9QMjB2eGxhRWYvZjcwY3Bqb1l3L1pwN0VYL3Q5NlNWWjRiL3FVTWNzL1lvZWdGL0hsaHRGM2hUbHhpUWxUbzdoWDRPRkJPQnJIOE1JS0swVjNEOG1NdDdQMElWZGJJd0UrWXBYOHFnVG5hblo2TS9nN2QrOCtxWkxhUDdQdXZkUTlrNkRKU0hENGFnYStFK2xBeHlZVm01NUdSSTl5VUJHc0ZYVGE3Tm5mNTlFendPLzl4dnBUbmpObWp3cThDN203Nmh2L1kzdm9mS1NhMDdvTk55V1liSC90cWlpWnJaYVNzd1phemhYMHh4TC9HRmtYcFFQYnJxb1FOZEk4bHZJWG5MejZxQ3hIakxKRG1tWXlCMEhtc0dPcUpDT1ZFT2dGK1U5SDNyQ2JWcFBWZW4xYk9TUWdGTThKMmcremZHU3NmdDcrMDRlUW1HbTZEd1hZeXkzcTZYbnJuN2o3T21XUjlUQ2s1a3FhQkRuYk9nR3FZNHoyUmVQMW5GNDkyVVR0c2RVaWt3MVJ4TFVvcWhhT3M2NXEzM1lzK2gzY3RsbG9XVmczZ1hmYkdWUHladGVHNUFVbWhVVWgvdU9BYy9FNGhvaXBsa3NuQTRkVUJDaDBzK0Nhd203OHNwWTF6S3ZnRHQ0VTA2SEtteEdqUmRFcWlhZDgzekNyRy9xcHNEcE01KzhldWZEZTNtTWd2YXBUNmtGUnN5VDRGS3FxdHVBRngxVHNZOUxzK2dlTytyQ2RZcFViRklyeVZTdjhVMlFkdjlwUjBjQm5ncDhnb3N5UTd1WlkrckFRQzgvT3lRQW41NmV0S1NMMWR2bmJ3VVc5eG5wcEppeStINTNVRnhESUpCWWRYVFMzRW4rbXFTNTcvd0w5bHZoQmdFY0FQU09tV3Y1Rzk4V3daLzMzSHpqbWU3anZOVjVXZWcvQ2o2QUVMMXR5Z3AvRVN0aW91TWtndytaZ08rdVp1MXFoR0duVUM4WW5CVmVsay9HdkMycTJNZGxTYlBWUTNCWDBWOE9qUnh6ZHR4S3NUMEdsUGNoa3VEWE1IakxlYnlLWmdQdU9wUVByZVRCbU5ud1NVSjUwKytFbXBFSkFSOWZJSlpaMk15MnR1ZU0zM0MzNklNcHVGeFJaaDFuNDlCWTNYVllSaFZsYlJyTUNsTkhtS0RUTDZubktKZDRIVi9GWityYmplNVE2dk1OR0hCK0NQVUlIblFIb0Rlb20vaXNldDBmOWxFbVB4SXVjSVpnZXNaejJad1M2VTZJdi8zL09KN1lpY2VIc3hob0xqeUVXT2RHdkowVkI4YVZmamJJM0pZYWxXbUVRUHZwa1FSdWg3S2kyVEtQUFBic1hSeklvMitBSTNiU3hKVkk0MUtyRHZiS2RMeTlrb0cyeWM2R25nV1NlN20wVjlDVVBnY3doS3lsMGQ2K2Z2cHltbUpPTTZSd0VoS0tyUm5PcUlDUXRrdjArTVpYZTZrbDBUTHJvM1kwMGxwNk5RbXJLcVZGdjlWZXdEVEE3NW1KcldZc1F4TFluZ2ZLQXZzUkI2N3VyaGtvTXF0REJHSzhyQ0Z3blhyeVVZdVd2Q3pWY3JEazNBTEJJaUlCaVpmRzJhSkRBb0lSdW9Sd2RVdnFsZ2lxNStxbDVWZ2VtTERYRFIrUFNTK0Z0cFk3eTlERktLMGdtbkZSN0FoS25FN21oQkV5RjBRQUs0Q2FUMVJ5WWhFcXVjY3N0UnFXbHY0TUNaQVVpeUxpWllOckE4WGtuMGw0OHk1bWhoOGpIMlhRWlVrcG9oQUlkOVo4N2lkS0VJSVEwUkhjeDZnakZ5UE1PQ0p2ZXg4anFPV2J4UU9yRTFtVko4eDNVZ3Y0YUg2ZU5uUDRMamdRWnNhQ1FCLzNUa3prNXRFSUY5OFRUYmlZRmswUUd1S3kxUmR4aGdJQzl0Q3RJb3F0Y3JIaWhhMmdJcjdaMlFXRFZheHpYVXFNMXhzTnVKYVBvakdrK3RvSkJQRXRRTGtMa09mR0ZKNTI3WENwMzIrd0kyOEVXaE1QZ1g2OXdVd0x3ZnRzWWpQSGpwdzUwS082Ujh1Wk90WlB2WDBNenV2eERWUkRKYnhwN2F4UnE3TzBRalNFTnhxOXkweERlaThQcWJKUDVpYzQwVlgrbWg0Vkkzdmlzc1RjWExuRzNZbFY4a2xIQ0IrcEVDbDBRd3FzMEpqUi84MHN6bXhjOVBXaWlhbUhteS96ZDcxTnpaZXRRV3dPT085QkJLVm5VYmdSUnhRVndnc2N2Y0JER0V2WGkzazRGT1hDRXdOQ04wU0lpZnBLWTZUUFV4VmRjdndndWd5TTFVUUlMcEpSL1BLWWdHOVQyaTRJNWhpQnZEbzNvTStZKy9kYTZ4TGJ6dk04b0psdmhUeldKM002SXBxbDRMT3NlSmNKZDdZZUs4ZjFpcWlTMU1VQW50ZTVPckZpQmF5SUpzNUVHeUdoekZtaXJwRVUvNEI0SFRHYlo4R2ZDaDU4TUN6cGFoVHNCZmpjRVZheXdnYzRYUVFkWGJIWlJLVjdhdUx6bDVBOVBoMldZV3BRaVd3blM4U24rcFlUU2lYMXRzSlJheTg1a3RqWFNGVGxuU2dscDltNGZSQXBhYnU5VzlFUFZLWk9zbE9SZnlGYnlqRndoVmZhZVJmakJIWTd5eUNxMW9QNGx1WmhHcWpkcmk0Q0ZxNTRCVmNrQWpoOXJDU2libVNLdi95SXR5YW1Xa3h4ditQOThWNm9wREhxVnhwOVYxTXVlZHhvWm1IWlk5RTQrUkdBYkNLa3F0YnBidys1ZUVMZ2x0MjVzajVZY0czNWExWi91dTdyanlsTXVldWg5Znd2ODJiczE1eVd6dkhSUHpoMjdlTUxGdWp2ZWlGUm4zQUZ0aWFSQ0gwYkZ0N0VGVlBWUzJjRTRkMUdZQjBaZUV5Qm12aGUzREU1TFlycEpBU2ZMV0VwYW9CbGRJaTV4ZFB3YWxsMTJTNW1lbzV1UnVpZ05reVl1U2JEQjh0QTEzL2V6NTNPRGM5RlBGUDZhVVljc0Z3RkFFb050YndaMDNyeHlvMmpHWVc4cFpWeHQxWDV0a1RTRmRrWWhCTjJLVTdXSWpGSWNyUUl5dlliMFQ1U0tDb1REWm9OclI2aUVyWW55YmNxV0ttdE5oenNHS3cxM1l0YjZqL2FUY3RKU2F1NGs5SFBVazUvZmxPT2RNSXltcStWeFpaUmQrTEZlUDZpK3Q1RVlZS2tmYURmKzdPdWs2TzlRZFhqVlo2NThUaFo0VzFic2paZlQzbzFrMG1BOE1WeUpyMmMxN1N2SEFMOG56M3lZbkxVZ3doemZzR09iYUEraEJHSDAwYTc1dG5GbUJKOU1wYTNjYXZlYjdXRkFLSkRnWHlwTzhJZjNVekh3RE83dUpzaU5IajZMRUI0QlNRMUh4RGVKUXFld0s1NGpBUTdjN1dxM3Y0M0grdTdQaURBR0Z6THNMK3NIeW0xODA1cTdLdkJIcHFVU2FxMEZ1U1k5TlFSTEhqUERqOVpaQWpVdTQvdUhlR3kyaGsrdnkyNVkzeVA5c1NiNzRPZzIrMUljSHoyTzNrMkQ3N0pTR0owbUV6V1RBZndJRjdjNUswdzkwQi8vZ01wYnV5SXplL2JzYWtUbUI5VVkwTUwvQXlBWktWQlRZMGNucmtPaTVBSWNkZlV5b0h6SHBsdTRaM2hlZW1qWTRsODNwak80S043T1RkeTZFbjI4M3dibU5aNm1Nd0NHaGc0eHBzTldDa3h0UGxhU3Z2RXVTSXpnVEs0S2kzQ3JuZSs5UlpFd3RUVHpWckVLaGlac3RkSTQ3K3FpNCtad3FQYjR4SWNSYUwyMVoxV0NHOE9NeUJ5amp0MGJLQjJUTVFzcDRtaXFhM09hU2NWVTgzV1BBVmRHbGU2MS9FVlI5d2RoUWJFK0JvWUhQSTE3SHVKQ2JTbEtPYmF4TFR4aWFvRkVLZ0I5TElXUnQvSE9iZkJPWEpwbnk1Y0tGdkpiaDA3alJmOHVuam05aC9zaWtKNHNrZ1VnTHU0enF3ajFTTjZxOHVZNEtKRUluNkZ6RVFnQzhDQ3BFYnVTUndldFVmRzBmR2prNmt1UGUwQzhxVHBLL1E3UmlkdndPY1F5eDF0V3JWaWpya3hNTzhQZnNVdEVsdUQ3VjdUUzUxNExQcDdzYmZaWWdycjY3Vjg5UnN2ZWN1UHdKbUVRZ0tLa2RFQXVSRFlqbUM1WGtmYnI3M0xTNTA5U2FGNWdPeWNtY01vb1hNcnZCQnR0eU1Zd0VVaE9QNUxZd2s0WXZ0QVd4RWp0NUVlYjFLU2JTeGY2WXRBMW1zWG9KVGhFVzBqNW5OMzRwTy95eEhoSHorUEpwNWx2T3FqbnNQaERBWDhwWVk5WUQzYWNOSVFDTHVNOUxVMEViQlB3WHc0UjlHMHF4TkV2MTZDclRHTHkvanp2NVdKVXhTVkZoSUhMaE1xb3AxbDNpZmx1cDkvRTF5dDNBRXJYRU1jRXArNWgvZ0d3a3ZVNmlocHhCc2R3cWNpek1KdSt1cExoQ0RlL2pUREpsaUNKa0dNTjJWbHZvYlV1WDRFM1dsSzJDZnA3N0E0bUJNYWZKTnM4TnRmaHNYckFqMUxGenZHZlJZMGtmSXhFRXNNU0YrNDA0U3NicUlZTUFsRjJBTDJaY25pMzd0b2RkQ1kvMFJxd3hLOHNTRWdwUGROY3pWSFIvbWFycytrekhyWXUzakxraUtWK091MklKc011RlZWZWliSWI0dFFmK3diTTBNRFIxV2JNc1g5a3JDM1VVOE90aGxQQlF3ZEZVM21xT1VBeXpnY0hUa0J0L2djTzNkTzh2UkkwczlnT29nVjl6SzNmdFJpMnRNUGdaNDJPS1FmaFo3c0NYN2s2bnVvaVNTRVFFa1lZSmVnL1V0eDQwbkoyelNSMldkejJ6RzBIQ1N0bmdJZmthSStaQU8vdDRaSldSL0gyUHlkallJVjd2WDlWZ1VoUEZ6bXNlQnhXY3JhelNnaUJQOHZ6VnJWc0R3N2VnMHNOSVBkNFlpK09mUDkyNnE5MzdMMFdEUStYQXVpZ3FrbmpwTS9XT2ZRd2pxQTJaNXdXM1RlWUNCVEl1K1U5YU1HTDdWMlRqQ0p3Z2VXQmlCeGEzZTI4MjZJd3hsQ0VVT0lNa3lVYk83VGovUDhUZmpsVlcxcTJSalJjV3k5bFhCVTVTcnJLK21QbGpVcU45ZjFNUnpLQjZRa2VRL3ZYQkFpdVhpbHpGb2l1TExyV2l0b1hLY2p0c213RVNmWDdjdGVBT3JOT0RuTXliS1pWWFIwbUgreS9uUHZZYm9tOGxWVUlIWldURkFBR1ljR1YwYWRFeVV6bGVMb3BodG1POTRYVW1GdEJwZTFzUmo1Y25BaDBQWndKUCtBRkV1T2U3SjNFZGkxU1dpcXBIdzVMN2hDd242Y2VTQUlLOU4rcWdMdTNOdXB6U0d4ZkE3eUhlWUo5R28wK0IvMnBNUG9zemc3MkRkM0t2VUJQVGdiMi9hMlF2SWd3eWlObzB1T0VVTjdPRGpOWlBSK1hVclBLZXpXZXFIY1lEUHJ2K3d4cGFqeDgrbDFWS1V5Uk1MQ1kzcVZXNy9LeXZEVERRamNTZ3NJdjdSVG0veHRxTnNmK0V0UldkenhxamVKZkhDQjJQWjdveldhcHpWeXhtM3pFbVRZUGhSTmtBWG4rdWV2L0VWaUdFdDB2VGRrYXhpODN1UGdCU2VaQ0h5a2g0VUN6N3V2VVhkVXBvbEVSUTRjVTVseWNLRFAydzBwU1poS1crNEJmRzdXZHUvM3laNkVrZDQ4dWYrN3owYm9ETmF4VmxGR2pJbE84cDhGOFM2cnhJOCtsVmdmZU1wVk1jd1lvZ2F0QXVnaDdQRmxUMXpxaHRZMXVpekM0dG9TbzAvck9lbXN5b09USlNBSFhKcVgyYU9ZcFBSNlFWcDdVQXpRWmV4OXc2TFh1aFJqRld4VnIySmUxUVZpMWZuL1RlUks3aHBscG8xNXhocXAyRXJwQ2FHLzRqQ0hEcU5XdW5xUDdLOHhSeXVZekJSei9OU1pvU2h3NWo5UysxRUI2RVdCSUU4RFRvSytxMEFudk1jZGJnT0c4UFpFWWNlOWVHYWMyWVU4RXlyV0ZpTzF0bmcrZ1lMNHFoZWNvNzNUQ1JCTFl6Yy9yKzBIeHN3R29lem44c2g2c29LWll6ZjAyV0xZeVdoQW9BRGtGL3JQTmgwV1JaUGhyTDJ6VEtNbTJveExIM3ZQTkVWQW5lOHZJWWRQaHU5Rkx5Vi9jQ3lKakxlSXNvMG5SV3hublRMTDBmRFRRVjVBMk9JVkFlWUtqUWYzOTdMWC9IdFc5bnhzTnVzcGhKYlkrY1E4eFZua2pMS1lkRFMzYkxnOHZUTXJEcVNJZnFKeEkrNkpSSjM5MklTektITmlvRHpYQWVDRFhVNFM1YzRkYVlreGtZNkY0VHNLbjJFMjVPNVZBanFTZ2VtMlNvVEdidWQ5bGUyL3RGY0VCb0NteXRjK2dDUjEvWk91eHpPejRtWlFHUjg5WEpPaGE5WXkrNHZXemRPaUU3Z2RNa0JnRExPa1ZHV013UExCbWdvNmJHaUZHRWo0U1hoS1Z5Q1B4OHdzRlNVVm9IUVI2bFVyaE5RaHM4RThYMm9JTUZWRDUwVVEvNG1zODY1emZoejEzQWRBV2RtZGh3OHpUaGl0RlRqcFlBbEZoRWZjREdjdzBVQmpxT1h4Kyt6a0xtdFBTSmVySno2TEZyckNkajByVVl5UWZGS0RkT3JaNEdUMlJ4OG1SUGhVbmlsMTlvclU0Vk1UNklSVVNkUEhLUmVyQy93eFZDTTVSdDVHZ1NPbUxTRmpHNWx2RXdhSXE5VVZLSm50bHNONTQ1K1V4N0ZXcjBoTTVxL2Fyc3NENnNaYzZzZTNzZ0paeHRQUmcxWnJ4dEUzc0RKeEtkSUZlcnVWWnVobGxTeWVLMkFOWFZueXBjK2xESUhlNG92Rlk3VDd0eGI2MWZzL1pUVnFCK3RVelhxK1NjUlhYL1IwekUyNWdQQTducjI2clRYVFUyOVRNRHNBU0h2SDZwZy8vUkhGTlAxZTRMOWlzS2FnSzF2ZjEwNklUQ0xPNjBHSjU3MUcwcTc0WWZUNDM3ZTJaMUpxTlFsRWlzc1hUSFhkU3QvWVhUQTRFanBTS1NVYXZZNlBVNkloWkR1bUZjRWFuMVJXMzFaSDZ6VnRNN051R2w2dnFqSjdTVVh3SHBaV1RlS1hZZDRBazZ1bkUxTDRkckZPOXpJNW5sZkRBVGRYOVRpOWd3ekdWSjAwdG41TTc3OVVTVUtURzhicFQ3bGYrcm05OTk0L2FlN3BXc3NUTmlsN2FzalFmZjIydGFkcXZYcGlPWkxpUzU2R2xycTQ2M1crcFQ0YzY5MTZ0R2EwT2NkL2loamxuak9Jblc2SkUxdVg0ZEdYY1l0bkNFRDROcjVzbzIzcUZUZmFFOG5yK0V2akpNSUNmMERTY09yaGtRWkxPZXhqNWRjSzN2MDErYzB3NHJhRDdpQmZRN2hLY21QUXhIUjE2c1RjSnIzSVdYMFpTV1FPbmlJUS9vcEt6Y3NkaDVhanJEZE8zRXBlMWhiMnFlY3lSYmdCRXNleE1XZzJYZVBoZ1dHdlBCZ2pMS2xGU3BoMGdqSFJLZjZtL0djeFRyRnQ0dmVPUnl6N0xFTkl3Z2lad1FRM2M3cldJKzJ6aEhINm4vc1QxcEp2TXZSZWtNdkFLTW1NRCtUMEt6VHUwa1hIdy80RGtNU2pQTjBIRGhuT3BHUTlkR0ZGTWhtSmJxMTEyeUJWRWQ3WTAwcldrK2RUaHc5SFJmcm1XSFYvOVV4VFBjM04yZ3BONHlseHhCa0czVkJKMXVLUFVpdUtaK1BOTi9xTWp5RXEvQzhqbm1BRUVZMFhuNzc0bmJaMFRzWkgxdjZtMk5uYkFvQ1ptcnY5UmRHelBXbWZndUl3RjErM1psanZXeWk2NVUxM25NbndIekdkbzdEK3VBWVRyUHN2cHhuK3pNSy9wUUNXVnFqSmJ4SFRNTVpkalRQemg1TmdzMSt0cFJjR1k5NWk4Ym5XeE9nYWtWVERBTTRqbEFJM095WlFYUE1tcWNaUVpZWnQ4ZjNBejRtcnJVMVMwTnM1UFZoc0ZWUWF4eFRlY2UrNXBOa0M2dFlKWWJIVk1rWWM0a1FmN1IwazJpc2ErTzJGKzB5L3B3c2pqejFwN1VKeDQ1NDlMMVV4b3ZWU3BnOXYxSGxEK1pUYWJSMS9yMnVQb29LUzRDWHBYTTJlVk5LU0g5VXJpdWZJdVZ1cUhSY1B2RHpNaUEvZmN3Wi9BUEsydXBNczdaWnpHU2cyQTJlQnZudlNhY3NsamE4VkhQa2hQa0VteFFJY0tQSnplNzhGTE13dEFxbDNQSGl0ZjI0M0lRcndOcTd0ZER0dTFONkpFTzJtZ01OZnRZYVdFcWc4N0xXaFNXaWRGTWo4SWQ3M1oxZDdVOU1PTTk3UDBqNEVmSFFPNHlZSVFqczJGRitOaGI1TjAvVlVZZTVKVVRYdlJXZTJwaU5WNE9uV3FYcVpSNFltQ1k5Smxpa3UyUG40RS9vVW5GNlp1SU03NHJzaFVKSXFKQzhWcXVKTHVEZEJSMWMvQVZ0Rk9kZkVhTUJKalNaQ2lMWU9nL29WYkVISE43eWRRQkF2b29FTUQyUU44endWMzV3TnFsQThtTzB3NXZaNktndER3SzMzL1dVVFlVZlNwVTFmRk5HaXdJYmZKeEtlWGV4VFh5RC9WL0c1Z21KS3lpb0p6Z2liSFZRK0hkeEZZQTdNUGZzYWs2SmE0VnRJZ3RJVUZFdUVPdzRTdnQ3Y3o5eDFIeDM4aFozbXNSZERCdkplWWx1OXFrUkxSMEl1bERUUzVzNm8zM29SQUtEai9CK1VxUExOeEM5WGg5SU9QbG5wSnpYSlJpekRrck9Rd3orOW4zaGlmUGJISWJmWnJ6S0VaYk9hRUpCdHpHS2VoUTM5ZDJiNGdaWHpTQmlocnM0ZTVWTXBmTUxpbiswaU1qbTdEb084b2xEUFY0aEpjY3pGZDJqTlByRUNjT0lEVk9RZjAwME92R20zNWYrUktJMlJPS1M5bTZ2WVpPejMwSUVvdjdTRGY1U2EvbGNZZnJIeTYzeWduYUh5SEtqMndwNmlhOWhScldDc0xuV3N4ckxpTmhKZytNaHI1aDdDTHg2R3VKWkM2UXloYWYrVzl2eWlGVGEzRlhvMTV5cjl4NC9LOGpja3FRL0k2NDdyUi9vYnFpVTlvRWdWWkJDc0NIR2tkRnlKVG9qTnRUek5neElTS3NjMVViMExLV2R6M3hldzJzWk1aRUJiNzhmZEJOK3NLNG1STTA1a25xOG5DUnE3Z2ZOcWR0dk1Gc1lEMW9GSm4rZDM2eUhQUkRUV3lVNHMvTTVncG1QMjlpdFliVmtrVTBReXQyK1EvSnBwRnM5UGJ1eEpQVWp4TnVia1oyeGhFVDlsZkc1dXAraWpFVGU2ZUpNVWtvTEhSQmNtc2RtKzRqK1FjbFVXeXFGQUp3TVpVWWNCdktkQ2IvaEYwRWxoMUxzbmRUcFZrd2VhV29vZTVrMEhUam1SVlFZV1VMWmRUZUlBbWxrKzNaQlNMVXdyekpIeEtLN2d0ZGVNdHJxdVJjOTA1YmZhUUlxMDNCMWNRdmRsaUVTRTU5bTYraDlyalZUVmdXZ2pYTmVqRDFNNnVGU0RVVWl2clFwamJIVVZXQkhkdlYxYzg2djVZdmMrcTlDbmVxN1NVY1BhLzkxYlNpUVN3RTVOQ0U0UUlWbjJ2aHZiU3pQOUQydncvcFkwV2hVYjc2a3M2WmhWOXVEaXlSUjVWekYxUDBvMldRN3BWMmRxbEtMTHFZNjgxM0h5T3NFdG1QRHRaQWRIODUxU0Y4QXo2V25HUkVHbTVSQXlNeTNqWkZISXF4R2h6NTdoRHZuQmxVY3hGekJQR0RXcXEyVEZ6M1JSMFBIczRiYTRxckVKdkhFSktyN0J2OUNLbFdrT3dWR2JsU1p2R1d3QldqMDZHOGhWVXFZRlZ1Mnc3N3pmb2I3aThqR2RhdVlYOE9iU3loamk0ckdoTmhMVjY0T3FsOC84cnlHZWhabXNhbTA4b3FZMVN3UXRYWmxIVXVoNTRWQW14ZFpOdWFCUjlFYzMvUGI4WWtXbEUyWjRScFBnajJxaTl5czl3bVBlUm1KamsyZXVJa2VLbUxBQUdZckpYZzNGUy9yQ3lCL1ZYQkJjSFpWUWpySWxPTUExeFNyTzZJMFJBdUd1YlJEbkRUY3FlR05PaGY4VktselRMSHJEYjRTYk9NQzdsM0w0WlMwM3k2Z3ZBb09MZnA0djR0MnhOV0I3YStrMGhKUitYNG4zT014cXRPbFpLVWZRdU8zQmZVRDJBblR0a2lYbllQdFJ1bnNOVFVjTGJZd0dVb2ZzM2tRaW5Xa2VwNERSVGt5Q2dZaUNBSUR4NFUyV2kyWVVyOWFGdG5XWWRTWHF6RHdaeUZCNW53SmdObVk0NFJ2NGdFOHFvTXNuVjlPSklNTzBseFNEalF4enNtRWlHZXRFQU9Uckt0SVE2eTRJRVhoQ25WY1hmYWt0eXdTNTZNdHRjMnpDVlFmOE9XVHRQWWRhMFFOeUNjMklNRGdicnRTM1hwWFdRcCtlRE5DSCtGd3dlemhKL1o3UnhHR0VQRnZOWkx2TjNEcGQ2MXd1NjlwSU5IZFJMU01mUHZGbUhNbU9TZTBaMHRadkNHQ2l5WDRvQk5mNUpkeGJjRURkY2hwNG1MY0FpSXI2ak1iSXNZTjhucWk4aGRFOGkyY1plWTNGMzNtTDdRMkpRbmcvclFwM2cvUHgxKzZpMjI4SUVDZzhxenorVGJSVS9uelJYTFZSYjRoWHcvd3ljdEJNVkRIWFhqdk0yZHlRZXpuSmpWaExTbzBCY1BFbXJ0bWFsTVoxeTNCZjZrWFNIaHVtZ29kZisrSzJGa29KR29sY2VnVVpTLzNHcmdWcmNvd3o0U3BScWM0TXh2alJPenRrUEhFKzBMUFRUbHFITFp3Tlh4YnpGUkU5cG02dWtWdEZaM054ay9qM0JJOElHSmcrSDBjWDBCblE4SVQ2WERZOVRJZW9NVkhVWWc1d1BVaTlZTWZVWXVtSDFNZGxHSUcvSUdOTUliSHUrbGQzQWswNGlDMDZEQndNMnVJcmxBWU9CYnVNUTBNMWtuN3ZRaGxxK2xjZitVVDZyTWtxOWliNTM5WEd1cGs0aUhxRG1KZlVGMFkvb1VzaGxRaU80MzFDaTJKSGkwaWpPdW9TK3piMmNuMWxCdTRudDBPeTRJRTAwNTkraGZ5YTB1VFdJb1I5TU5yT2ZScTVyNGNEUlc5K3l0ZW9zZWhDT3BtaXcxbWtOT2lTeHNISkgzWUNTOGRoV2UvVUUxb1JHSEFFQjlUZ05XcE1jSG9yK290Z0N1SkpSK2JVNzk2S3I5ZTNHWDEwZW5rRFk0eGxJaS9LT3F1LzYzNHVPR2FmZ2Nud1A0ak13U1FpN3JjTWxCMU54RXdVM3hIdmI3NVV6KzNtQTBTM1gyeTBtaStxSmI1cXVpaTk1dWhEMFE3YmpyZktiL3lRbXRtSkJxdjZVVkIyTjk0V3NjUmdJdjhHaFpoRjhTRjY3WHA2a2xWSWxnMW1Fb2N6bE54YmxDeWhWaCtqczl3QkVhb0ExRzlHb0VFYm9EMXRkaHlubnRQZ1RRYVlPbUEvcFRUMFlOaGFtQUtVMlBPRGF4UlFHQ2FrTWhIMUd4bzhxNWxhT0l0d2kzYmY3L2hZR3FYZzcveUsyN1grMGo5ekpDMVNpc0MvYjQ2OFExcEFmNGxDQVNMTGFtaGZaYVA0enBoMkthUFQ4Vjhzd3B3OWdFR0Z0ZWRjamV1YlVaeXVCdXA1MmJ4TnliWlpqb3dyb1IxN3pNZEMxYkhsejBPOWdGZXIrS3M0L2NyVGQ2aFhMMTNDdkhFSnNXaVVsbVNIWUZiSTRlRUQ3c1VRaWgzVlQ2bWxCOHZMVUQvQVFJQndhRVQydUphdW4vcTEvbzhyYlByVjVmb2I0eldIM2VubllSbzd3U0xNN25zNWZ4NWptNGViU3FBTEtaeW1yQTJ4ZWdrZWoycHRtbzFaQjZFbTEvZE1uVFNBeUE1N2VhNEpXY094N2E1QVloNGRHalVISmg1Sm9DZ0krODN4NDZBTUZwRWQ0UWtKNmorVWcyRDFyY1RTSU1rRi9RUnRId2JyZWUwUUdyRC9UOFpwRHpnZnY5bVk1WmNXc0pNVnBpZ1IzbVVRT1JKb3pNSDlnZjQzaSsrSFZCT3VZeUpIVzFlSEFFWXBzRDVlV3ZISG1SWUFtY3paS0NQMW9ra1VpZER4SlQ1UHl6YzRZVlFGam14R2FNUWEyMHRJQzg0TGl0WkZIU3VwUXdGM3UvaWdXbkg3dEQ5R25zUGNpU2E3SE14NFNjL2dtMHE5MGhoUVlyODNmOTJLTG0xakNUUEVVTzQ2MGRSZTEydldDdWV1aDJlVHVUc0V6RVJnRGJUUGJSYzIwRmwwL21XVU1OdUNXdFlwTDgzTXY5OC9Pd0tpNVk4Q2xrQTFhNmVocGlXQzN1cDdVUStuZDh6SXF0ckh1emxHUllEdkdDRDdEVmI5by96Z0hNMW52MmJzb0JkbURSOUhSSEdONzR0QkM5YWg3NWEwTFZQVEFYR1pZUjZDYWI2ZVpzYTd6TWI2UWtmaHhPbTFOT3UzTkVoM29qYzNmRzZaQzhMVk9VUDFnUWRjQWp1ZkhJOXBkWlBTMlpacnJmdVZyVy9ZalRhWFFKbjBiVzRqRmI1OGR3YXNRTVh2L0M2ZkZvWTlQMTNId3E3RElyYmFCc3huQlRFQ2FFNmVFRXI3bmxKNVFRUWhnOTN6ZmREZzQ0a0xrWjFRaTQyY1BiSWluYTJFdm9pdmNETHlZZ2FoWG5LRlA1aFdGaWE5NUJmSFhNNkV6aGdDckc5WCtBbTJucTFzUzBWMnp3Q09QS2tHdWpzbXppbkQwRWNRbzJGei9TTTlQak02ZS9FK1dEQWxHZ2RLRS95RmowckMyU3I0QVFJSjV6WlE1S29LTnh5S050L1liNnRlYVYrM3MxVE1qaFZQRk0wcXdLQ0dVY0FwaWF3ZGV3YTh6MEJxUjZnd3lwUHVwc2RJZDViZ21TMUEvTHBYRENYcGpSRnZIZzdKdHBFLzF4SStZRmY0VFdXd1VBOU50S2dqYU0zWmRkRFVOTDB6clhZT1loeEtXMXVNdnZvTGFmWVZwSWx1Rm1uVGNaWldXZkMvU25NMzNTQ01Cb0d0L0NyUGFhMnhxRXNqaDRnTkFJUTlHaFp2YXZxZkpRVm1vT3B6SDlvUVh0dmtMdDZOWjNsMTV3VHVDN3dwV3Yrdkl1bG9rdmo4N3RUZTdTWW53Zi9OMHVEbUN1Zzl5MTlVQ1hselVzanhBcDltRzMzamQ4RzlxMWx6bW1OKzd2TUUxUmMrNEpBVzJzbUIzL09FT2pYa2luNGVjanprSkFrNnVsT2ZIZEZnKzNqWHdVMEY4NXNQd2pibFJNWGI2MHRITUV4UDJzamFhczhwc0xQOTJjd0NaVE12MmVBWGxYUGE4S1BhaU54YzJQa1dBcmszYys1Y2xoMTVXMndod1B5eCtBVVM5aGxnUDdxeGJuazNUaVBVTG8xdGNoQlZvamFtVFowMnR0dnlVQmtNb3JSaVI5S3NxQmpYR3NrRllaMDdGbEo5cTdsUjVyVGZacXB6Zmt0YXdXKzFmMm1pYWZYODBMdWJidnpSVHRWOGF0QVFvdkk1QURBdmZ3bXluTkI0enVVSnIxOERLS1lDTC94Y1Q0aWd2dURZbkVuRVhZZDhKNzB4b3pRMi9yL3hOR21QaFpnUzRQOFJsaDd1NmIyaWNFenNGc2ZTaDZXc2k5c1loM0VoVVRyR2kxLzFRRTIvRDJOT3QxSldqY2VHZk10eFBaeTlBWjB4WTdxYzI0VUhWOWhrQUh1WUF1K2ZxTUdDNWc4TWprOHo2TytRWU5mYjJ0UVJIOHpZM1VNaWsxWlQybHJSMnhvV1plOHJ4TE9kUXZYRllIV0xGSDdWeW92QmRUaUltbFZOTFNHTHFzTTBlS2hGM2UzNyt4ZXBUd2dNUVpYRU5Ib3UwQlk2c3V4TUdpamR3NkJsVnJjbHZMc2ttMEJjY0lNMi9NQ3RJN0lVVHFuS2Z0cWNOQlhjTG1XbVJVRk41N21vUVYvd3h2cE5UWkovNVpkUUc1eWxFUENRQzExMzRQdXVOTndRNU8yRS9FSXI3cG1uVjRabHkvZUR6NzM3aGt2dUVxZFgyM3FnK21YemoxOTdkWGJ4b2ZlUzNxSE5IU2crZVNsVThIR2ZlNFB0akk2V0RBa0dZZFFJODR4UVBhS0NSTHcvdUduTGMwS3dxanVUdm9pVlpDZjkxWmNubGVyRitMcURDSGttWHM0SW5DNzlzaGd0Y0RmWTRBZngzSCthN2dNNXBadkltK1VJcENPTWM0Q2VlMmNteHpwQ0xIbWJSRHFqMkxTTWQ3NUg1aWNHUmxXcGo2TU9INU1tM3NHTkRqQVBPOFJYZjRoMmRrTXVxbDJYcXFWV1BNVERNbVIyNjV0dXJ3Vjl1WWtVN20zTjY2UFg0RGxkTUlCdXNiemZrdHZPam5xVVNQOStjVWhxdlR0TzRxQUJScFI4Y3VLWmZNTlZuMkQrOWtMMHZldnFieENFMTR1dGp0aEx2M1QrWVpIdHdmSUxOWVVLN2J0dnVSNmxUTHlGK24vaEpjSmJuYjZkTWdJSTdxcDFxZVdLZGN3djJJRUdIckM5U2o3MDJjRDFsRXZLMmU3aEJvMDVjYVkwRFR6aUFZNVBCSnVpOHowUUFKOE9YRk1LaGdJeXdIclBWTGNoK045YWU2b1pUOHhTa0twUWN6NE9VZHhqTXJJUW14bkZaeGxvTnFzc1FUNXZFRGhVTWY4bUpmZTg3c2orSy8wN0NoWHRLM2dDbmhCdWFpamdPYlJocG9mM0Qzc21lVG1YVmpsdGxiTmVCajBhR2N0Rkg5VUowRThGNTBIc0xvZHl5bVlLVWMxT2hIM01GY05qUmpaWWZzT0NGWmJiNDEwNVltZ2tJVDltTyt0bm5OQmY0Sjh0eUhDbUpKYzd6cWk0VlZsdnc5YW92Tjk3R2d1U3IwQzh0aXZiZ1BTZldqZ2Y1cEU5QXJmM2tEb0pDdCtSQnJ4amU5OGt2bVQxSGxkY3cxMCtkZHFJNTRRVjhURnhaYndSU1cyQlRjYll0S0EvNjZmcHhKMmNKejBYUXcrUWVGVnpmUlcxQXV0NXhxQURHcWd5MEZuZmZiOUtzcllpODJJZFg0Nm9USTBjVUdPb2NURlpaenROZG1DcHoycWs2QlVVSmpwb09UUHd4ZDNsdGxvVHVVSmtsaFZ6ckpzbGZWb3RkUFBaNUFHQnA2RGx1bE1QQ1MrUmNhdlNES1dTaVpDaGtLREtOMkVJMkFwV2hTSFpGUkV4a0VGeU1QeWhyV1Y2dGVxOUZFV01qSzNYVGVFaFhpbHlaN3BxMVYza0ZrRlJ5STNBYVlUN3RCeVdwc1NrWnVjM1hJczFVM0k3THRPbGluWVhqbGdIVzZGaVk0bWRmQmZDRWZoTXpPYmVrU29iNkFLTk1SVFh0YlpOQlZiKzN2aC9VejAzdFBwVWhCaXJqcFVxNGlKL2VjTmRHZ0dzTzZqUXF0QldsMFV4R0xNTjJ0NnRab1pjeUNZVTZEbVNIL1Bsa2V5VGJGeGJac1lrblN4WXJPcER1MWtoZU9XOHQ0OVhkY1AzQ25HbkZpb3BUODY2dkFVODJ0eEUzc2N3RU5oQ2pFeHlYTExXMUtaMFZMV1o5RHVMK0c5bDAwQnZ6TDl0UFA0Z252cmlnRnBjc0dEMC94eWtmQksrUXMrdE51b3dudzRBQU83SUlld29CVXh6L0tLbVMvV0Z4RzQ1YmQzZzEybkRsZXY4eEdGSVF5K1hpNFFRanIrVU04NE1rRkRLa3pXUmc2aTZUYXFmTk9waUowWitGUnU1N001dGE5eXBEcE03Y1dUVnA4eTVEeDUzSWg3dG9XRjJDbXVKckRKczd4K2RsUDNHbDk5MDlFU0RrNlJnaGxWUkdVeGl6RElyNHphdWI5cTVQdDZuV0t5VXVmdDFtZEJZNnEzVlZoUE96OXdGS1dmek4rMndKd1B1SlN0bWl2QTI2MzZYV013VFl4REJIYWprVUVVY1FrNDd4c2UydThZWm9vaDY0eS9iN3dPZGV2RFVqbVkwcXYxZVdJOVhnYVFPdHM3ZGRKa25VeG9yanRnakFUNHMwVTlKYnR0U0ZJTFFlLzFZdDNxVnZZQVZucExBYW5DQ1ZWYks4OFVuMitmdE1YVGUvVTJRT2pyRHNLWDRyT1l6RE9PQXJTb3hLVmJEVmxoOHgwd2F3Z0lXR1RsU1BDbExGN25oM24vOUZ1THpGdUxWay8zdFBsalB2bCtxeGpMOEp0UEpQUmgrakMrZ0twNUdSdWVOMzJhRklXRXBta2sxbll2aW0wbVQzQ1ExRDVtUHowNUZueUQ1a3Bzb0dYdjkrQU5FbDZZWGFSUGdiYW04TDh6VW9ySElaeVBnb2lUUFZhQmpqR2xQS2ZFditWQ2ZFeElXbGZ3aWNSZ2ZwM3VscUQwZlFNTkdqL2syRGxyZUFWZ3pFRTl4TmNjMVdzVCt3dzYxNytPa3k0MDFDMCtNUHd3TWduclVtY2JxcnBlYkY3VTRST2MyWkFtbkZqVWI2M0IvUnRuQ2Y5VFU4WXBqNVg5dTlsajcvWlc2SjkveWJVMkhmeSsxd3QyOXJOMEZja0VJREorKzJpb0dheHlGc0hEWTdWZTVEajlEMTh5Zmo4L1NXYS84c3gwbVJMMnlaRHZ4VDI2elo0cG0vTUFzcmJLdktDajBYYkRFRDRiNmcydzd1Zzl2QW9udjc5ZXhIdUFZRHlEMU5mcytDaXlpNWMwRVgzcnEreE1VckpGem5hc0NpelRCcDA4U0svMGtsd25aVDZsaDhKVmZmYWpRdDR0MUlTc0pLaG5jWjlrVzc0S2x1SVRERXNXbHRUdGNlTVVxV3NxNUM4amFERUFOZlRkWXBxWlZKdlZNdDBNNmVzNzRDdkQwWFBKQ0E1SG82WlVkeFJVVGF2RzNPc0lncFNidEZZN2NlVEtVNWVqNlgyQ1gyRlNXM1RCY2FRTkVsMFhnQ0RVMUtvWFdROEoyeTdKcGxLa2crWjhhTUx1b3p0K2NoZDBEMkZNY2IwdWtFY3NPcjhHRzRxaGJYV01CQ0FET1dNSHpzNHFpRzhUWE52Y3FJbmtocXZvc0pjZm9sb2FlSTdPR1RiOThoN3lRQWFCNlVwUE94YWpJTTRLZGtNZUF3S3dzTDNxNE16MDNwWGk1czRiSE9GOGlRVXhwZ2ErUFYzN1h0R05ja2VyN0RUMWZVODlGR3lZa3N1eGlhTG9pZUhTS01hRDhXVEFuUmFxWjVDQ3VuM0l0dnhQY2UrZExMWUp4Rkp0WHVoYU5tY2ducEY1VmxVRUhkUXRTRVBicFlJb3EwQzBETlF2aFRHRXpoV0gycEQ1VWlZR2NGSnFMYWRrK2FIbk54czlwZWlnWFkwSEVGUU9aODNuZ3g5Y2lzdUxwUFhyNVNhd0c5TE1OL1BrSFZRdDUwdHIvaWI3aVl3VEFyY3BBV1hPTmF2S2ZueUg2QWg1T2lRa0NKeVJyaFIvWXFTYW9rcDVITnVZb3FBeXE2aCtQeHhzYmxsZUx5ZVZTdE1WSEJxb1NPbkFSS0g2c2hSMERBbmIvTWpsRTVLTi9LbEI0eXR3bmtTMkVsOExRUEhKZythejBwTVJ6ZlY2WXdnUHdSejlGRzFTRWRTZHUwQTJxSUdUUm9keXJiVk8wNFBPM0YvZjBuTmxaV3QwMFQzSkJtVDZha0VUMWxZWVduUnZlZGlxdFl3RWFaajc0WXpISUtjM1RiUG1ETUQvdXRpR1BuUkxLOFVaNllDVmkwL216d0V5QmhNQjBOb3VjUjAxYWNnOENLa0pGNVFWTFpnVEp1ci84QmhIYTBWckZEc3NndDUzc0FJdVc4RHJhR0VySkcxN3BpRFc2YzBwRVBYd2VZWDVWcVUyY2xsQStRdGNUU1I2RVorek9SV2FUTjJoTXh6SjlMR1hnb3pKUldlNkxGazRrelA1Q3pmbkZBNkJ4ZkRlZ3Y1MzRoU0d0OG1WQ0Z5Q3kzOW5KQ3hGamtmTy9kd3BsL1R6SnpBdkhhVHFJUTFCNVFaREFNV09qMTFtRUNkS1FqcCtNdEtSU0p1amtmRnh0MXVqUXdzVTZBWTh4YmY4V3JtcFJTQk5UWW1RZERBNGQrbTZGRDYvNzBQcSt3eWNaNXg5Sm9McE0zaGRvcjBsa1QzQmJhZE5VblVGMGtwNUhFMFh4L2h1Q1l6SnBrU0trcW1FbHpZdWZyUlRLQ0szWUhDamZhZW94Uk5PcnhtUXdJb2hLUzJmc3ZXMjFYOGc5eWo1RHFlRHhrMjhocFQ4MzQ1S3VNcHNTa3V5aU1pSnhSaXJKMU1jNVJkUnpBS1dzZkxLOTNjaHRyekI5OHB2a09lS2luclNvM3BWblNoRDBoVTVmK1BNcDV2dTRTMEU5Z1VEbDEwbEhITVlkQjhjSnlvQS9qTUFwMG9oam1mVW93WkQ2MERmei9SODlaMzlaTFdEKy9hc0t0VHBnOWNhVHJTbjdsbUlyMDBMZ2VMNUl4MmllMGtzd1loSld4Z2tMSG5BT2IwSHNaQUxEQ1BIYnVIZzVXUXJFbG4vRFMyTmczeko0ekVZdVMySUpaMFAwNlI5VmlqeVQ5Qmgxbysxb0d3Q0hyQ0QzOWo0eUJZZlhteE9WTk5tS3laZmp5N3JiTnJKd0xTbW14d1dEN291NnhaZDV6Q1RlSXI5aXExUVBmVDR3OW5vMWJObVBuM0dNU0RFQk9aRUtaclorc1hodVk5ZFYwanRRMWVVcHhoY0p0ZnpBc29CSHVncWhPNkxuWGNoZG9FeW13Y0hVQnhJV0tzdlBOMDV2ZVdsM3Z0ZVJTOVd3Mlp5NGMzeThZUXVkVDhiR1I4VStsck8xdkVyektuYURUL2pNbWRuRis0a1prZTh6Sy8welorOHdSSmhNNXhjS250dVFaSW4xczdEOHU3ZjlyZlFzTUY1K2ZSc2RsYmR6WXNjL2liY3hsUGRvU1lkWGtyTy9SWk4rUUVNSUJWc01tZEFtbDRpdXp4Q3dkL2F6QnVzM2IyMFVwUi91UlMwUW5aLzQ1N2NsZUV4a2FWa1g5aTFKOWl4WFVraEFWRGJuNk9hOExHSkFvQzMya3R1OVZGdEhZbEV6L0xtTmhVTTNVbW5NVzZSS1c0ZFJhTHkzNXhJSU9jb1JmNWdQYjd2amJ6UUcyalVDQVJUcnp0ZVpQanNjbmp5ZGJITWdWMmJpc2FrNDNXcFBuTkhtYk84VEViTmdBUkxXZTVrSG5kbEtCZkwzWDVETS9WVnl2WkJyMUhxZGhVcjVCOXZMMHdyRHd5WExFWUwzR0tXd0NMWFJDa0NGZmFuNUl1TXg1NTY5QzFrTVEyb0ZqaGl1QTlRa2J2dVB5OXlkY0c1elo2UVVkVmcxR3liaFFaNHk3QWhpRGZLZGNnbWlNdWFaR01QUEZKcDUwS3JnanFIQVhLSGl5QVgzZUVDS004TkNIQ0tyKzdLa1liMHdMWDNUczc2Q2I4bWkwN2F3Nkc0Z293UEdIaUVCV2lvaFlxa1VnRWM4dkN4VDFhMmFaZHc2cXIrTGs0dnJ6OE9XOE1OcDJuZ21pQWVqVkdST0FIN1g3cWl3UFlJVmw2cXJnc0p0MnQ0MVJnS3A2V0YxWFp6eDZlcTdhUTQ1c3FsQzZ5TWhUZWtQREhMS1d1cUxiUzczMGZCQ1hnVGRsNmxPckUxN204VEhzaldxS3NuNzR0S2o3NVpWSHdWMUg1NUJWaCttWDZuRTgzV3d1VDg5S3djemsyYVNXcHNDNHdnNDlYVlVaR09BQ0RKN1FUelpXaUJYSDBlRnZ2aVIrUUxMRHZJU2NjS0t6MXA0dGtnamJ2YnNiVXc3MlV4Ymc0b2lwdXFhUHZXR25oQ1gwa21lR0ZtdjNycWtQaXptTno2WTVWK1psblp5ZFBQWWtPYlFQZkZEVll2WUM2SVRhNS9FWStBbzBscEV0STQ2ZEk0ZWdRRWNiTWtwZ0daeFRLSURLZ2p1d2lqL3RPNnNGbnZtUUFhRmZuNGhkZXZTcWpwOTN5c3d3VVdoekZxSUtoZ04zWktZUFZrZHVZWkJ0cmdOQlpvdjJvV2J5UFF3aHVjcWRPTDBPOG5WRlN3Z3ArYWFSdjlQL1d1cXoxQUtHQ09HSTgxMWhiWHFpbmJ0M1R5eVd2QlQzTGNmZUtvMnN3Tjh6UVViUFFTa09UYkJDb3FxWTZoZXVEVk9tQ1pBSDBMU1VSeXVSUjUwTDZmZTVCMTgwdS9mRm9MNzhIMnNYUk55UFo0cnE1L0RJK1BrTUdKNEhkM2tLdGVKRzd5NjFJZmZuZnN4UlZFSituc0ZCY3RuUHNqckdaRmdIZHkzU0RxMWcxbmNOWXQzcHBaLzJsNnl3WElTT0gzRVJrMU5LcnZTd2FWV096Tk1BQlB4SVZLR25yeVFIY1AyQXRKNkJIb25xWDB6eiswTmxPdTMreUFSajdrQzFURjd4TkpHMHlzNCtVMTFDK0ZweXJkVjg2cHJ4blEwQnMxU01Oc3lnMmhjcDVYZkl6RzNRRmxmalhHQzY2UDUydk5oMm9aRFhQNU5oVEw2QW1IUjIyR2dhbjNLYVl5RTE0L3NPU04vUVdjbjRuN0krWUl5TkJTVitoTHh0VTdBZmxLTVhGa3ErcW5BNVRuUHkyVE11eWZia3kycEdsZDNjUzJONDNKUVZRSExSMjNwTWRTZmo4NTQyNFdLcmlOWW1LU3g5Y3gwS1dvWkZzTGZ5elpLMGZmU0h4MDNlUkU4eUVzZVFGTGRGWWxFdmZ1dXR1clpZR2J2YlhEVWdqRmFXQm5aSEI3NHVKRERySFVpNEZFRkEzeWM3VEFOeVZub2Qvd2prNWdIcWExc00rQjVzNnRJem5sUzlxb3VYMmlxMUZNakpWdlp1Y1JnZjVvZ2ZOUlpZREQwS2IzNmJzd3VrTzYrVzUrYXIvdFFUSjAwdzNDdU1DTmwxUnQ2b0RqdkNaejJRakR3NVFvMHZBNnVRSlR0TnVKNkhVRm5ubjcvRVZqNm5na3lJQk9Eb0hEbXVuWnlDd1BCNFBrWkFUUDFqWXVRanRBcnJVcWc4dmxuOVh2TTlVM3RoSG82UE02UlpPSFVSMG5iSGNQQUtJaXArMmN6K0VOOUJXTzBqbGJuK2NsbkJBdDlZRXh3cXN4NmYzeWppVmoxU2VITE1XbUNKOXpVL2FEOVc5alZRTEZ3c2ZISXphQ0s0Vy9FSnd6Rll3REVjcHh1RjBpSEpsUUNubXgrTHdvY1BrWjdteHorelYySlJ5QnU4VDc3aU1nNGJDUWw5T0U2aTg5SktRMlMyNWxiNVNyT3FibkFTTWhDaHphc1JiRkN1cnBUbXlSWUN1TVpTSGZScnpHd1BkTnpYTDAvdE5VNmRPTXRqQm1zR3JIQ2w0L0xwN0VrV09zeDUyQ0hkRWgwRll4c3NDOTNQdGJZRVRZR3EzeWlZd0hPTi9DMEtpTkNHR1pqbENVb0lXSURLbzBLVlFWZEhZTmduMC9LYmdJcmgxaURGRUFwQmdZODNTRVZDZGtGK3JPcGYwSE5GTmtxa0NLb0Erb1plSWxaeUkvd0Q2dldIbXlUbW45Z3ZZK0V1UTNEUkJ6MU8vaWZGNVQ4SkpiVlE1bm5RNENFMDNaeUJ0QjRMcU5JRzlDT3gremN3Mm1IbTk1T1M4RGlJUFg0ODZ0UnUvaG5KRHVUemxaRGUycjdXZ0xNbTFQTHFRbzlSa21CdFFiaWY1QlpvY2JIS3VVVlRyK0ZTSTZpTndBTTVzNjdGbHNPQ3Vmd3BuMWxRa1Z1YThjRk9obnFPZktqYThNd1FHeTROTDBGMUNTWS8yTUFPMk1DSmF4YnpMc2h5TmtXY3Z2YlhWSVp6OCtabncxdCtKc0Q4YWlFWitmMS91Y3pNemt0VGo1OTY3cGdkczd6VUkxSTYvaXQwVVRzT0lnNnNGM3lINUNQSjRnZ0RpQzM0dDJxQUZZQWdRVExBMzhHUnpQOEV0SEcxSS9pOXc1MVc5TXU2NlNMbHV4M0t5M0tYMFJzQW5NVkdITEhpeDZGYnlRajBDVWdYcWpvYnVFUUh6UzJlYnN5OVNWUkorQVJxQUFSVnM3YmZXYjcvQnV3cEtJYm8wQ2V0SXJiYTlhV1h6dUhpeGRSQUlzM0xXSVd3SUtZSWcrcS9OMHdRMXpBaUxRZjJST1VYQnNBWE15ZHJpc3RTdlhHaUgxdWVTODB0dHFkcGlMYyswaFN0L0ZNalNpbStaM3VFZG9aL2VIbXplTGxzVk0vUzZqRncxMU5PSlptZzQvSHpPclRPVjE5NlBMK1h0YWNCNkhYb1c0T1NBaW83REZyMzVuSnNrWDBrQVB2WG1idTRTSVc0THBsR2NkdXhhTlJGMUx6ZHljcTVRaTR6a2RjTGp0UjhVTXZDK01xRGY0ZjloWUFhTXZ5ZExqRytwU2o2VEg1SGlIeDZpTkNYdUliWjVrTmRDSTN6OW1kYTNrNmJIRW80amJtR05aZHQzdVMyci9jRUc3NWlFRHQ5d2NxOHFHdmQxY3dnaWhtZk9DbEdQSW5FNWdtS3VkamtjV1h5ZFhxd0ROd3I1MVUxSkYyV01TVExESHplZ0Qwbm93TlR0V1dpTWZiaG1RVDFiL2lMVWcvZGc4ZTBJZnRMdEF6OTBEbzNFUFlYYkJsUFl4aWZjSE43dzhodUQwaHZ4eGtjNml4Tm1Kajg4RlpMdXY0SFNLMHprVTloNDlvbjd2aktudmJMdk1pa082Uk1xMjJkbjdadnN2SWprOFozZU01OU1BQ3hZdDk5RWRCL0p5eVdobVRVMitxRUN3SS9haU9Jc0pqbEdpNUdCMnVPNDN0MDloY3pLSWVqaFhUbit2UmJxUXI5NmxLeWZ1b1BQZm9Ub1UwSmZsek9sZnN1SW5VZ0t3cHNxdThVbTNRdUllZVd3OTlKNDgxN3dUTEcyVzIxcmh4MWlSRURTd1VnVVUyU1lGSy9KM05PaHp0SFc0bEFvREhCejJKU2hoMW51QjVrQ3hFUVpLYUhJSzZ6VHRhRFllRFFyRVczYVk0UjBkR2ZzRlppVGY5VDlPemtaR1kvSCs2NXp6S1c3ZzRadGVJVkFGT1Q4bnZyWTNuNVJTRHBZazZob3FJUWJoZEhwTVNzTXBxb3hVMVh5STd3OVgzSmZkTUtwdkdSU1JmNnphbkNuL0NUNXRxZy8xVVA0Z01FM1IrMG1VUC9nbGdqdXB2ZzdnRmRHbUtVdUZKK3Q0VkxXZ3BFWVZnNUlFUTNrNVhDRlRvSVRRYmJQaVVGL3p2bDZrK1Buby9XMktqK1Q2czJGRDhuY0I0THlXKzJGeGhuMnpmaUxIVjFTNmxaZFhaS0tPaWpncHdNQWh5Q0RPVU1jNGFZRm5oOHNjYTdKbjVKVkQwTXNlS3lRcEJwTWh5VkpzSWZrSWNUVXBCd21rakxMbm1zZlUrcy8xdFExZm1YNHlUeHpyeGZYUFZZNnJJelkzWWRhMDFGVzBqNk5GMmZneVhZQ29WUUxlUHZUMUJPVStiVmpBUldmNlZEYTdGaVVWZGs2ZDdFajJ1dTIyMlNVc3V3NE4xSnc4cU9VeWVKaVBmUmx3Q2hEeDJQOGViemhtUkZaREJwa3V3alRHMHN2eXpKdjFxbGtKWWxoajN2cEEyb1BaMW1NZVdHeVBuRnU2dWhkRTBLQjNqdEE1eHJjOUNrc3kzeUJmVFFuTVNLVmkxaEZUM1o3Y1dhWGtVNTc2elRNNUJXVmNJaGJxalBVMU54R0crSzVXNm9Ea0VSMHRsMU4yRFl1UmxqUjA1c1RVYVdjZGpKTUdjaFZwRUN5UE55VVR1dDJ6cGtlRHk5NENOVHFhczBOUXkyaDBvNzVPcHNyWmpGTXBnUlczZ09vKy9QdWg1M2Jacm1rVjlWQmpaK0NNZVlXK0p3NGdXQWJYaU9hOEVSRHBSRnZWMTRMU1JCUXg1bCtEVXJBTWE5Q1FKV3dnWC9RbGNHZ2d2aVBDRjFCa01ESG1lbGU1NklWWlZaYzdYcURRTDZoU0JUTE5WTG5lWmxjcmlhb2lDWDVBTjBhS21Xbkh4bXJtbjh5TU9yaHhVSUgvMmZmVzUxczlzbk4yaHRwT1FlY3BNSnAvYWhJMVNHdGlMUEZYMnhpcVBiWVhTdHNpZHpGZVUrUTZmaCtIaWo1YklLRGxYbklKa0lQTW1pQWVkL3N3cXZCL1cwQ0lsczdNV2F4aDMrTnVwcURNSVp2V3BobUs4cHlucHlKNHB4VS8xRUxtOGtLUVFGMXF6VWJ2MWV1K292aUV1VjBQM0R3RCs2YVJrTERtOXJqSEFqaDVWbndVM0t0TmN4MVVSd1B1b2ZsQ2M5RmtVaWFwdHFXUyt1MGsrZDN6MEdlcWpOYUZCdjArMUVxaDEyU2lkYzhKOFY1ek5qZThHTjFLZUFGWm5udmdXdFRUSndmVVpUMWNPVWs2eGEwRzAxZGlQRURFK2RBZ3NDcUFZOVRUMFNnc0U3Wk02ZSs5Z1JidENSY2ozRW5WODhNVVRBSnR0QmFpQk5mK1MxVGlDcEsxMnkwY0VibVB5bDZzSEh5OFJKUDlXUlBpc2YvaTlqcXRXd09HeGdwaGFUN1UrTFJiaWR6ZC9qOGRJcjE1MkFtZGNiKy85VER5Q0YvMGExTmdNOVRvbVBVam5ISk54ZStDYU1mbG5NNjJ6ZEUvWHhvaGZ0UWp5Y09WcG9Hd3RNYzY1ZUJYU1VkRVN2a2VoaVBZbDhXODdYRFlVeit4Si9jS3ViVEFrZXZoODZaRGNtOHJVbkNRTVQ5UHgvRnZYRE9LUUhRc0YzWlRaZ0FGVEtzVWJya2tsNXAwZXFRSXRoSWFUdzc4cXd1RlYvRXlZRGQ3UTRZQVhnR0N4MWxMSlVSL0IyY3UrcGN5OVZJQTRRaERWdHF1ZzZJb1A0dDk1V05PZHg0ZkhIZUJ2OU1WRGQrVWM1QkpEWTB0NU1QdEQ2U1N6RGcxeEkrdm1zQVdlZS9ucU44M1JGZDVVOVVJTHMwMitXL2ZuRkJnWkxPUGJWc0JqUXI4MVdtZEpvdjBoTE1jM1NxMlNZOHRNdzhrZ0xHcXU2bmRGVTM3SytIVzNETTVuNmtvRDk1dlp2QVJJYmVyajZFNUllL3hUQTk0c1JqSVFLQWRyeUdpNDZnV244YS91UUgyN2l1YzRCMEdOb1BEWXJvTjVjcUJJWG0wVVNFNms0R1BpU0hMeG50ZHFDVFY5Z01oQi83WnNTeTZ1UGJsQWNnS2VwK0l4bDIvY2RNUzdzeVVkOURHSEtSaFpRMkkyQVpWaVFoeFZSa1YxSUZKK3pvdExYL1NaVDMyOGhVa3J6RUxvNlpLOGlRTUFBWTBCVm9ES0o3NGFoSTFXRFVVUXUvWVF0d3BsTXdadmlwQTZEZlM2S3YwQS9CZGVMS0xDSTduRXd3RFVlRXNUSnpHSzlOa2xUZGpUd1FqR3lSdzBRQXN3SmhSbW0vYVVWOFJYTDltczROWU5seDd3ZmgrZks2Z09OY0FZdXBtQzZFRVhrd2FnbkZaSGJkeDBtclRUNzFyaXFHZHBhODMvK24vZ1JlU3puWTJVL1c3a2FXN2RXQTBtRm9pdVBBZzFDUVl1Tyt0WkR1YXE0RHRyQUgyclQ0ZG1YSHZYTnVvdTNMV1RiMW9rcXI0VG0wZWJCTGplQkx6cXNlY3F5RjZ1VmJJVzJSZEJkeGRrei9WTnUvTHFDM3NzS0FHU0RGVjB6dnVjVW5DbE80VmdXS1RHZk4wc2RaWmsyeDVyS2pVM2NYZ2NCMFp5UVBKVTFyWitxcDB2NWpUYTRmWDcySUNuamRhWDVPVFBxek9aYzVFdHBsN1c1N2lCVDR0Ry9FOTQ1aFJjS1pSMlBjd1Z3ZzQyK2o0R1pjcEVzT1VRRTkyeVJVTjBuTmg5NWRzdlI3UXdRWWpFY0F3TmRBTjljdFYwbUlwOG01b3E5a3J4aGJsYmtiaU5ZT0Q2M1dQb1ZpMEMzU3NncTE0VENRRVRTci9CTmJkcFdTZjhzSEcyOXcxcTdyb041aG1JVEEydXpKZVR0d0d0N1BDUXVtbVFlSEVjVmtjR0JQbGN1WlRxWEZsNFdKMlRWQ1FVVnEwWFFydWdUakk2MDhtN1Bhd2d1MXZLS1pEQkRtZnlnMDM2OFNvaGZCUEVnNnBBcHZnZDhXenF4Vy9tZ0xNVmRlNmIxbTRaYXBnSFI4TEVQeVV1MytYWU44dVdHam5XVThXa2N4WFIrYjdCTWxBalowUjF2dXo5d3VnWlNyRmtSdDNETUtqZ2JtcjJxMXB2YkRwQ0tHbW5wQ2ZkWThkb01Hd3hYN0dkU0lyWlNRMGt0eFlFaWE1Q1lxbnBsSzlmaXcwM1Jld0s3MUFqT1F5KzdDdkhNWWFGVjFlYlJyRTVReVlxVG5iZTgvS0RLUjJWVGI5aDY0RFJQQXBuM3hsWGRHVXlYQU1NdFVweW12cWUyS3lrTEpmd2NHcllXMVdudFdXMGVSQ2hOckhlR1B4UnJ1VmVsak4vMFVZUWNvbHFKd0p4emJuMWdMZW5MVWZMVVhpSVM1dElCalpqNGNqMlFDY09ZRkkwNlU1NkNlWGlLVDZuazZ2ZTZBR1Y1Yk1SSEJORURjZm83aG1ZMEphZ2tqMmxsTU1Wb3djU28zV3JzMmk4RTJGL3VmL1RQVnRiK3RnN3hEcHJEUFVWbWJkZE5vY2w1VU5aYlRZMFJ4dzRtbVlJSDg2QWJzUHhMdkFUNVVIS3FtVzBvbFBYckNhQmF6MHNYSEdXZzJmWVpTUmlqcFY5VDhLejl2OUtqVUtQSXdTSHZlaHpYb00xeU40am0ya2RudVV3Nyt2OC9VU2FkRko3UXdkK3JvNTQ0ejNNRWJ4OHVLaGEvL0p3NDNWbzdKWm9GNUVBSmNNYmFWczVvWlNaajVpbUdTTld6S1N1MmtIOVhyZzFOVENaMEx1VjMzTU12SEQrYWYyanlncVltMnhkUEJuZ3NQeUtod21DN1Rab1Q3ZXR0eDh5L1RvcjNZWHFZNFBQSnBMZ0Y2TXNEVUFjc01zbXhYZU94Y0FqcitEVTcveU9UQ2ZLekNMN0p6VnZuTmFycmh5MUpWWkt4NVFRUlhLWndIWTVlOXNVQmlRdTBnU1pBNmcyR29DaVV4Nm0vTkN1RUsyb204WDk1UnBxeUZmYWxPQm5wK2h2dTRPbGtxZXdrV2l0RGR4OXRTOWxLRDdvQkN4T0xpck5YVDRuT3RvZUFYN2RnYkE0SmtzdnRGM01pTE9SOVFNQXM4ZEdoQU4rcFJWOTZYR1hCUk8yUTZLb1VwTVJ2QVF1WndVbEVzVTBpT3BtVXdQTnErS0ZvMEVsc2t4NXYzalE0aXV0c2tMb2JUVjVFaGFCUkxUVkxFMHBERDdaUlB3T2JxUEFjaDNHMGErSGhlbWNhcW82Z0kvYm90UWU5S1BoaVpORXBIM2NTTHFkZ0hwLzlTWU4zWERVeWJEWXN0OS9SV01OL1BkMm1QN05CNUdTS2hRZzJtdFl5SHRETmcxMUpwYy9PZ2dHNWdRdFg2N3JTQlNtM2pKYnREUnpiaTQ0S0pQM1d2eFJVcElFVUJCbkRqZTRHbnR3UGRCOTFGSC9jbm9CSkZUVnBOSHFyU0o3WkFUZS9PY05ka0Q4T2pQTjhsNS9KakFqeWkwNWRVamVnNHVhZDU2blROM1JwYWRNT3NhaWprQStxalRHbE9Wa1N4RWtpblRyMnhNOE1ycGJRN2xLdzJKbmkwR3hzck50MXY4TkpBamUzYnZCOHFyWW1lQVc2VUpCUFZnN3RFNGdwTk1nTnFYTnBaR0FtTVpKQkNMTS9rVGNJZGN0Zk0xYnVDYzBZcFJUbjROYzNKSWdMcWgrdHlSNE9sSGp1RTZLSmVaOEkrWlJsekxmOFlvM2dldGUxakFOb0tFRU13Z2QyVDF2aldsdEZQOVBlUjNuMW9jM3ZmclNPbks0USsrODJoWWdlMFdMc2xtMGc0eFJUa1liUEFIUHp2R1VwVTd0LzhQOUR2UHk4ZDZwUUJ2d25qeU9oWGE2TVdkUFZpSS8yR2R4d2RodkVJUFoyMFI1UEhmSjE3MmNWUDJ2NDdaOThWVGVGcTNqZmtwc25jWndZbHI4dVlzeEpLdzRZME9UL0pMa1dQTTNFN0kramVBTlp4d2d4RzdoQm9iL2NMVzVHbTZlUEI4dTVJUjNwMEtHUjZKSE4rcDlVSjlNZmtSMnRFaTdodGU2OFk3UE9ieWZjQ2FjVEpENzZWZlh1TEsvWWhVMm5tSVF1SDVNem51YmhRbEhWQm1RZkFTZW1XM2R6b1FkbHdjZURmUnMrSjFkOGJ2ZFlpdUVObEtFYmZpZElHeDBZZjUzUmx3NVRxRVNydzY4b1gyNmxwTzNxQWpheE5sdSt6OEN3QS9mdzFXZ21QZEFsMTdCcndWNzJtbHpieGJxaG4wYWE3MmRMSTMyZXBrcFRHZnRmY0IvT1ZSNE5IMnpHSklud0ZVSnVtQkFsdElxSkV5TEYxL3g3UmQvVGhSOTRCL3U2RWZ5Ym14WXdyZjl1S0ZBOEkvRzlsaUtQeTdZU3Brd3ZjM3NOSjh6YjRZcXdtNUMxSUhIMmhPT2RKRDMrTjdDWUs5MUF1c2lJSXJhcGVPK1JST2NDWVVSbi9iWkZRVEloRDFBRnkzM0FrRnpSejY1aUF1UXdVbmw3WWt1UGlmVkZ3bm9zU0d6NXRLS1B2emFVT24rb3Baek1EUExFZElhNkxuRklOQWRzc0xxKzJvbXJpUHJzejlBUjVueVFoVStORTNzWVUwZW9tUEFpOUs0VnNLUzBvMVB2Ukw2RHl1RlZKOGp5YjNGdjhFUStXbERLZk5vTHMrUUxlelR0ejI5OVljemYxejFiWmdHd1UwbzlyK0tjdGlPLzZZRDFPQXk2b09zS0RaU2tGVHJtU1pWbjQyN2hhQ1VIWUZpZ2FIUEIvUW9aRzBvTU42UW9Za05QTXhNbUhXMU53bEhUUkRNaDRsOC9BS0ErWE5ZVTkvS0tpUmFtQy95V0syNE1YYWZlTmF4OWNVT1M2YXkxNWc2Z0xQVTdNYlVvZ3dyNUxtdU1hcCs4c0c2cmYzV1lFZ1ovR1RLTGZqTER2Q1VHa2d5eDFMbGovUHBwWVprdVc4QkY3R2ZzcWxvZW5LaUZjWjFKY3FnMGdOU2pRQnowUW9UQ2J2YlJyNU9aNVpyVjJxWmVwSExOT2d5d0pScmtCNUJXcWlqN3RBNVJ5QWZtQ05wNUlVelVZbjY1S2dIcUFXbktva2M4RkdvWnh3OFhtOTJzKzBHb2pLRERTVFNaYk1uUHd2OUMyamxMZ0hxdUEyWnR6Z0NNOG1nTTNVY1BHalZFNndaSG5jSTFhcGx1VzFqdWJubEo0dWRiSDhWSXRzUENxWHQ2MURtTVNiR3VLZkxjYTBKUXV6V014V0dHdzNMWllGUXBHZ3F5Q3A0OWxuTFZHcG5rQmJoTGNGbzYrd0Y1ZlVKU1RieWZmTVJCOGxRemlKRGxLUWhuYS93ZmJKU1BENzZMOGFqS1d6NEJhWndiQUV1ZVhmbUFNV2Y0M3E2aGdtZUZaY1dQUk1qckwzbUs0QmlZZTQ3R3k5M21MeWZFZlM5S3ZjSnZTbFV4Ny9MQVY0Mk4rbUI5cW16ZGJwVWRVUWtvZlhwTzMyTjYvM3oyZFQ4bXFsWlZhOVlrMUFhNHpMbElZeklkczU4TFduNy9Ca1RlNG16YXB1TjJ3MWY4SkpvTnhEd0cyVVJxZlVma1ROZklBdHFPRlJUUzZ5UUJPclJMSzVSV3BBRzVkU2R6cHdxVXdjelkzRDhPZ3BTNndDOFFDNVRNVHZzeG9ycGpuZWp2UzIza2dBekxlMDJnN0p5akh6YUIrSjRxZmNVdHB5TWVRY2xadXJocXpobHg1cTNtNXgzMkJDdWNFUlJ4cHNHSEI3OXZnTENUd282aHV1MmcxVW8zWG9kUk5Fd1p0RjhmazNOeGlMWDVkZTVBUkZIMExmckFsRGxJd3AzUmNEMGxkM1hoYU9Id0sxYnp5SWdUZFJVSUxDV3RMbEh6Qnk0K09tRW9wbTVLM0dNeVJFV0RQVmxzNkV6OXllcVhNSS9NVXYxcmJwTWVGalAvSjU3emlJbi9KemlxaEZsM0NNeWJJY2ZTRFdCNG15ellVbDFycGswcjI0b2xEcXpYbUJPZ3BQMUtHcTd3eTJiYlBoZ3Vvd0hnWm0xaUdTbjF6b05NK0h1c04wbmZtaUtla1hHTGMyNmk2bU9tc0Uxa0ttTHNMZjhORnRiRnlIMUx0TythMUVZcE9NaXlDNWZoKzNOd3VYWUh3R3VZZnVJVkFlSEZ5N3hyQUFrRWF4RERXbHU3c3RSclNkMnp2a0o1V0s1NzRNdE1qU1czS2RNd1RSVFFGaEs2dTJpaldpcGx3Si9jU3FOdWZqQ0NuWG1PNDZ3U2t5MmtibnNRU0RGT3d4ZzA3NmR6WWxNTDZUbkN2bGNHcTV6SjMwT0g1VldGYVI1RHFuUUlKck9WbzBvMUh2dHpwdHoxbHlXZlVNbW1FS2pRdnMrajBaa09scDFWL0ZsVEl5NHNucmlhWUFpcis0NFIzSnkwaExoN0tabC80NzRyZVMwNFhuc001VEl6VDEzdUdFWVB1NHN3eVRUNXNRTFpHWC90eitRTkRiRWVXTzNtbTZhSU5vQkw5c3UyWWh3VG8rdFBCTndjMFNqZjJ1MzI0N1FHSzBnL1VhakVKZkZzQjlSTWs1QjhMK1ptOWZKVjJCdjhDbitBWEF2bXZrNXg1R085aVlUcVpqbHZhY1ZUZEdMVFdPMmZ5UW9XK3BST2dybzFzMFB0TGFwazVaS0xub1g2ZjdGMVl2SEI4SUpvNjhUMnRBVXdpaHhiT3crMHh1QlpEUVlkaHdiNzhDQnhEQi9vYnVWME1hY0JEenVFbUVXMVdqMk9vK2J0VkE2cjVKczEzZU03WUVCNHVGZmRiZHMxVk9sWDVkZ0dYcUtCV2dNRlVNdzZCTlNGS2dQY3EvVVdmQksxT3NXcHIzOThubDVwZU9yK0YwZGNZV0w3RXovTUVtckpkS2tsS1VSUVFiakc2bHF2L0l0ajNwU1JoWVpkT05xMmJDSVI0Z0lxU2k2VSsybHEvaHpZazZFMTZGTzI0TmZMY1Y0SUhGcjlscXhoTDRzNlA0eVFCc0JlOCtjRTdUOVRsOTJia0JzN3VNdUZ1dGVtelBBZzR1YnBGVlRCYmlzMEZFM3VRMis5bUJ3M0o4RG5ZYzNtL3FWV2llcmpBM28zcHJ1dFhZTENhQTFSYnJJUjFkMGFtYlVGWGV2ZCtPWUZNV0FQblFPMTB2UGVtMVZHKzFDYytmWWRMWkpmNUY0MnU4U1BOa3NTNnZuMUhyMmhVeFluMThSSFNBUERLcFFZOVk3NUhRT2VxcjVFd0hDQ09JODVneFVlclAwM0hzbndRV0ZUWEt1OUFsdHJNYzNaanJjOFMvaEtrSXVIT2ZDdnBNdTdhQkdpWUpOQkNzaEVONXVxS1FpcGE4WWFzblZML1FrM2NXSGtuN2JYckQ3THQrNFhUa0RMSktzZkxFZzJlbk9JQUJRbGZkMlpMbEVmektvZTdZOHJ2M09Na2duSThlSEJ1SDAvT1B5SEU4ajVBTTJCcUJDZXRPNkdaZ1k4aGg5QmVZWUtZeldoMnh5bkVvMkptTWNlS2hBeTkvTjJoQm5GUDczempKL1RXS2VKMlpBUzdIVTRBY0tzNWtWTDNNODFKMnNSTEVrenRUM3RkK2ZBalhWc0prQWFocGJFb1F6Zks0QTNQWE5xdzRxUUJwQ25ZWWNzVmt0SlpnVXFndmRrUzRVRldwR2dHaGJmUk5QQThJSkNGRVJDMGdvWk5nbHZ0M1JIN1pwSzVrVmgvU3FvQmRoZjlQMjc3cHovRnVxa0lCUDEwNU9aYWxiTHM1cnFyR05zMjRZLy9pYnM4RXRpZ2k2ZXl4amdsZDl6LzBCZTN0Z2RYejQ5TEJKQ1JQNi9semdaSFlrRm9jOFo0a094MnNhUVBjUmRVZjRTZVgzKzBsbTVSVHZSQ2EreVVWZnJpNUZuOGZSR3BualFkNGZGaGlKUGorOTRONVRyc3hEa3liSDJFZ2M2SFMxOGVoc1ZPYW9zNkM1MDJKcEc2YnJmem9lajQvV3k4YXROeTFwRmJSN0lHckRqc3Z4L1JzWWhlUS9NQWNOcEFBeG1qbGppUjFOZmlZWUc4STYvSXJDNXVjT2JISTgvdTdWVjVpMG5Pb2pQRmtNZmhhSjJwVWFOUlQyQ1Y4dHQxelh0anA0a1hmZUFoYTlvYTRnbU1QUEs4T3RsMllQd2tNc1hQaDQvcW9VSFp4dWZjcmQwUHR0dTEwU3NSVGpOMVQ4M1FNS0VoMXNoOGJybWFzY1lkcTFRbTBSaVVSK29PVkxxb3F0RkxnbmNNQks1djVGaktpWkoxMmhlT1BPcm9QWTg2MGk3UG9lTXRMRlljWmk4OTBYU3A0c0J3OWdHRUVINFRpWXJFMEdHL09JZm4wdXN0cUdYdFhtWXFZZzJ5UDg0eElJKzNRdVJKNUhXQ3V5UUl6blR6T3FFTXFPTmJSYjFNQWdPY3dIOWtwVjdSelo1UGxCZURnM1F5RitUTjIrM2F1bUo1MUZsOU1zQkhxaDNaUWlJVlJyYnlXMDlUaDcvRFJDSzlua2RaZzIwVnJleVZQZDRicDVQZWpSQnhVdEpNZzE3WXNKK3RsMnhpWXczL1dWNGNhbkxESnZMakF3bUVOWE9RTktwczNUUzZlQVhVaGtBNTdEanhtWG9CRFBsdER3VE56TkhnV0FET0NXeGpZaDMwNC9wWDFOMThJLzdSVVhtN2l1bHR1RFZCam81UHBsUmlOOGNOTFFkL0RkeDFyejZWOVRtYkZIRmhJR3hpRC9FNFNaSTlOaW9Ka2VGSmFaK3VZTkM3Uk10UWgxN290QlhzUWJsQThwYW01T1BzNWhmcUl5UFhEcUNTSGdycHhwRnlPQUNxRHVmTlR5cFhYUVFlVFhzTTQ0cGJ0Q21WTklTVk15L1o4S2F3RkZ6d0d0dDdUZDhNMTZsbkVOei9SNEkwTXd4dVdzZk1hSm41RVd4aTNZTXhYYTY2VEJPMENlNzVVWEcwbjBoSTdxQUhJUXV6S1ZnTnp3WjVMQ21Yb2pQVkNPaDVBb3VBQkwwR0l0Q2lmVnI0dVRRa0JqS2JqeG03YWZLdjhOWTZua1dpZ2hKekRHQmpobU9xSjJHNkw3UkpKa3FuSHRGaVZ3Ni8wNXB6aFE4SVQ4aEdJMVN1UDZtdWVlVVNwandJTXd3QWpzRFhSWlhhRjBDRWlqS0tRYzgvSHFpa1V3SysyQlpIb1djUGd6UnorN2M4UjVxSFR4UW1ZRlN0b3Q0U3NDYktBTjZvMDN0UU5teXJCNG8xSjZQdjRsN0htblNwLzUxaWNPUHZlbVVyRC8wdDRsbFBFemNRdWZxeDlWWkpHM25pcDRoV3JZdmE5SFpURVhEeDlMUE9QZVhzVy94MnY2aFJLdlF6SHpZZkpvMTU5TUZvN3VLZVdKdWZsZm5OclNqYUxlZlllZm1QSkk1c3VvMFh0S0U2VlhIQVczSjRpVVAzTTBJZ2RJcm4wNGhaUnhYMGswZjVHTE51T3JJdDhaU3JKUFlyWGs3RlluT3A2UXp1U1Z3WWZ4OHZVSVV2cHRoaWZXcTFuY0FhNkpCZVlaQ2dqMFhWd3VXU3lJUktkTVR4WVcrOUk3bmhheTRaTjJiSExpR3dHYWZjeC9yazNaTE9uL09QM05tak1Wbld5RlZUa1BXNUJTVk1Ub2RYTWxCRUkrSDcwNnAxLzVvM2RQODJYRGFOa3MvSElQV3pCVHZFUURka3FGTEovUkZpNEpiWmF2c2lLdnpaVVdVYUVyVHJhaHFhTm1NRVRwNmxRaUVMQmVhbzFzN2ZKWUhSNmNlMWNIQVYyNFF3OXY3L09qOTBQSWE1amJqajRhVHB2eWgrK2RYeUJ1aUEwaU1WVjU4RUI4TFhhMXpiKzZzVUtSUUYyYnV2ektZeUNnWHE3cE9TeUtyN0dXZWNqbWhsdjZyZUh3SXhuTzcvc0pGa3FCMU14UHh4bDQ4bkFYbEtsbXNET0Z5d0R0VElxdnlMa1FnREZhS0kxektrSkVnMTlvOEdoRStqZVJFc1BsM1RkbEhNTTI4NC9tYVlsaS91NE1xK0YwUE5KUXdQMTZBQXpBRU5vdUdOSEg4amt1cTZ6NjJkSTNQWVlNYzJrSExnYTJ2SkFnK1l3bWVMWU5WYy96UThZdlVGZUNZK0JaYWs1VVUwNnpiTXpNQ25TNE9CaE1CWFFINWUxeFJFbDVhbHB5c2lxM1A3Zlg2N0Q2ZmFDd21PVVRMM2lqYkVOY3pFbzFod1NvL1dXTjFMZVV2aXZsaGx5c3EwSVU2SSt6OGI1bUNXWmpKeVN0Tkx5NE43OFZBSGVyWjFJRmJmajErMW00akQ1dTFGYUN1a3BvWXBJa3VQWmExVjQrdFVFVTVQRjlPU1hLUVZUT3piZHQ0R3BrU2txS3dRTGhQZjV5RlVuY2V3RVRoa2s1NUROdUJXVHoxcW4yK2lZdmdibzB3RTZxYzRCT00xby9ra3Yyc0VUZWtCRlYwVWlpV1dGYzkwcGVhVE1vaWoxOEQyZ25wcThDdnlvSStKYk1RbnJicEhyWjdCQlVhVFNhekZub2g4Y3dsWHpOcU5EcDJvQ2ZpYmNMSlpKZllsWFNIcDdscXV1cDh6WHpialJaNDJQclBwMm9yM0JqL1JnLzVKUW9IRXJPdVE1VGtGYkxqaHFBeGF0N25aNXhUcUdINzFNSDNlUm9OcTYxOWpMSEhEZ2kvOHRXRi91NGVLcXV0a0dCbGRFeTN5ekNUbmE5YTlrb1ZvL3NaTHNvYmlYcmxTcjNTNjNCWGRVZ0RyU0pPSjZvUTVoT2RCbmRSZFZGck1WRzBmUkhTM0xSdm16MWJVVk1wQ0ZJMFg5eDdBaXRibnFiT24zbDZWcyt2b2plVmdZZGU0cGxoZEJMOWZyeDBpeW9vdFUvcHNnZ0JMeGEwS1ZTc0lIS1MvTE9EM3J6MFN3L2FxSjhxVStCdmlwcUF5VFJXVWttcWNtTFdpZEY4WVYveVBEYWVoWUwvT1JtNllXa1Q4OThWTTI5N283aDdjWG8vcjlxTEVQRUt1Zy9GMXpka2VVNWlDaVk5RmJJSmtMUEhNVCtMbVp0eHh0MCtMTE5SdStIdkt4TjVNVXUxdkVpZTJTbVJkdlEwYjMvUlVSNGdqWjRHdWVvUlJucUZ6VVBvRDBJSDhyaVMvTWs3cVdtSDlFNGE3VnAxUVdPekY5WFA0bGNuYmV6TkxpNU5aeUQ1MEVETm8vd0RqRHRFL2IvZFY5WG9wWTZleUpaYVB1R0xuUy9KT3dXWDh1N2hiWW9Kb1FJRVZnNUZoZ2o1QVdBK0NGN1ZJMmFCLzNubEVQVXBzRGdLVUt4dkVWOEFpSjFoazF3S2crdWRpSXlueVRXYUV3eWp5ZWIyczI2bUtINGZxeVpiT0ZWRVNmU1ppemVlREdBSHFaUk9kRlBDSFhLVlVOWmtvMTROQUd2N3VpU1Vsd0s1SURURkxYSTN4N3NUSkJuMnRDNWoxYUloNzhtR3ArUFNzcFpBMmlhalZuRUhvaWkrU2JJMVdIYTliTzlIeU9TcjB3QjZUazN6VFZaM0RkK3hqdU1pVVFaR3ZLQlpMQWVFeWtpd3NUTm1oUUI5cmIzcHdkOXh1ajR3bmh6VE94QW9wUUhrc3FsRGtkWUtFYmluVnFMZ0JRekRNMU1SUkJWdk80Q3pPeVAxOXVHT0hNUGdpWXYvQStjUlNpcHZzeG84WThVS3lIVG44WnFSb1JVVWw5clZvMTVoVWVBeXFpd2N0aXp6ZEpzYkdoNUpuZHgzMlpJbURWQjNUR0lsYTkyS0hLK01WdjhJNElHS3BvNWtLRWhkMDNrUkNqRU05czE5VGZ5Ui9NZGxBYnZiSkI5MGJEK2g5ZXAxSzA3Ry9yL0tybzB5dytBWEpjMmlSWm5uL29Cd3BvQkdxNDFrODBOMGpJVGZMN1JpQnh3MUU5aHN5bkpZa290MTdjeWtsclM3Z3pXQ3pub2pLajM5TGZvaXppQnRHZ0xzSkk3cEZpS1JHajBQTFBFWDRzWkJKSUFMTHlEdVVHMFlkMi9UUUZpYUNyL0xScVlEelFLYTgvd2wyT0FOWjViMGI4dHIyZnVkRkIwUTEzR3ZHM2t6MkU1c3JPN284OUhuRXJKazcxcDRlSERxYTlFbmcvZHNaSUhwVTRYcnNzOVFXaU81R0JPK1lYc0NYemJWb3hUTVAwSGRJZGVVem9tQnRaSXc4OEtyL0F5Z3VJZnFPWUJ4bmNGcHFFbkpVN0JtNmVpQUp6MnJvc1hvc2lIdGlvc0NZSGltMXRiTzgvT2ZEV2FMeUF2MFhtdGg5cFNRN2I5NlJqRXgwTjlOMGw4L1ZwT01LZzVSd2FFMEpOUTkvdDVOL3lRVVlGZC8xSmlQcG5hMy94RmpPUFdheStPZjRlQ2lMVkY5ZUttYS9uTXdrejdmNG90S1FuV3o0ZFFwY3hRbWJ5cGh4aGUvVXpEL1QzWnY3SGg1YmRpMXZqYmVab1B2VDh0L284b0l2eTBqLzJUdm5HNDd1T0Ztbm9ITEpzK3FOYlJIY3M5My9uLzlCcDAwWC9BVU1oSGZraUx4dktLajBsMEhBaXVCbWozRmxxcVJVSVQ1YzdwL3ZDTGJwaUN6YmUzemFicXo5WCtYclhkWEpCZnZpRFBOWkZ5ZkJNNEdtVWZTT2JIYkYyb2EwaDY3VGRybTZudGtjaVZOTmJKck5uTVZONXo5cmU2NTdyQTdzUFFEbExZTHpkQ2FOSkVWM3VhK0l0R2IxVlF0SkdleVZXaWh1M3hacFZ4QWpURDRLbmtLcDlaU0M5MlNjdVFuQ3dYTTl5VUNkT2R5U3hkR2ZmY2VtMkM0TnduV3U1Q3EyNTJiY0tNOUNUSktaVVhGalVGSk5qWFducEorYkhlcDE3aUJlQkFQSWxaM3Roa0VUMmxia2RoMnFEZDdSYmduOTl2d01CZGozSFdJTVNmTkhiN2hEUThGQVREVWhTak1PZkxBV2lkSXVyQ1cyb2ZJTGlwNTFFcDFRck5YN0h3VUtLbVlYMjZmaHVxWGNnTWNkd2RMaUdXY25YWlE4azRhSlpzQlpPOEhWVHFYTGRIdGZLeGRKVmdIZDVRTmNha05hKzVIVDdNdDhNTFZjZE9xYVhUY3R2dlZVaGNvUVJ4VHVSNFQxU1JadWllSnoyajc0Y29UUDZWQUpINGcwNlk3Q1FHd0ZVK3lHWUd3elA5bVg3Nzd0V2dmcmJ6NEptK0FTMjN3T05pamZCRzV3dUxTdnhvQ2lNTlZuR1ZIWlNTcjdBVHZyYnlVNGRnVGJUaDRsWFQwOTdWM3BFVVF2UkJzaGYzK29TSENXVUlFUHNGV0JBSXNuUXRGT0pZVnhoYkxzZCt4SVFrMm1KckFiS1JvZmpoRDJhcnBOWmRPaERnMFZrdDkwbmtRTCt3VlllWHducUVOY21NM3p5TkFjR0RMaG1GaGwxWTU4NVdxWFRqRHN6WVFpUHQ0TzNQUWJXaTI2Q2l2bW5PVmNmNGY0L1FzRzZsd2UvY1JVVDZXa3hwVEs0S28wQzhlR1M0Q2l0SEZNRGpSc05rK0pTMHJVOEE2NmtHTFRWK2xZR3JrNU1nRGp5WTcxUW1jczZ1SGJkUU5ua0Q5dElJanpaUjlTclUzNVMyQjBhbC9mZzZxNnF6TmdqUHdWSDdmdVl1eW5iUjdEQlpkQXFMREJLYjcrUHNWQVMyb3RQU3E2UlJhVE9YdlJLWUgyekRDVDcrSS9uRklaRThQUUVQelFJbGp3cUU0ZnJhbWoxUENFWjR3TjZNb0x3ZEFkTHViRWp1aUpialVwLzloMmNvRStJNGJRZnZtYm96TUY2TUgySzVEcHAwaGxvUWo3c2NKVjNXZVIyK0swT1ZhZEhNL2EydjcyYnY0SGdSUGhrZFR1QklnNHhpNkJJc2R2UjBML2FPV0xxNk9XbWJjbnM2WXR4ZUdxbkNGa0hUakdPUlplSUhBUlVzZkZPVTdqSlUxenJuN3JXaFVHa2ZITlcyN3Z1UEduaU12VFF0S2NHdVVXMENhTDBITG15QlpjcTdEV2J1bVprWFhGT3hCaGlUZmo2UzZqNkpGY2tXaTNpMjJQNlFOT09UaVV6MjFzbmlMaXhlOStGbWZtdGU3d0t2RXowNDQvbXNzSERqc1cwVkF5UEZzNkh2ZU5XK1h1TGNvVU1tWmlqLzkvMko2TEFZMFdPZm1iUExCallSTnZaTU9oYmYrZ2MvdmlJV2ZwOW4xa3M1aFN5dk1FSzlkcmFhRWtYRWh5clZFTmRmemQvclgwL0syejBIMTlwaXBuVkl0SlpDQ0h1ZER3ZnZ1dXNaa0JqL21PeHBJNVBBNFNNRU5WOTMxQ2RaUyttL0ZBSXgzN1ZZOE82YWxwQnRFSS9Ob2RNZmxiN045Yy9DQnJrT0ZYOWYyTEN6dXJRTU5LSlM0VFNJcGQyc1ZsRXZJTjFHTnZTR3pmVzllVi96aUtRcjVQaFJLeFU5RG4zaGF1blorditBelBaV2JlZ3NyS20xNytUNGtrQkk2S3RjalJQcjk3VGhrUlBZVVdGWTVFR2hiZXFGVXcrN0RUZVBRUTcwdlBZTVNjYU9CQXZxWTNETDdlRmI1Y0ZzbHoxYzg2ZXZESlJZU2p1d3loWm5wYXgvYndvVzRKejNEUEYwVE9XS21iSHo5ejBRYjFGaDcxRWlneERLbUFCMFd3SlRMZXExcCtOTVdUM2ptRHNGL2VKOXZubjJkbS80aTRidVNOb1V3bmJneGgwNE5CcU0xdksyK2VlY0NGRng1d0trN0hSMThmVjJrN1BQNCtEd2ZHWnFEL1VrRTBvYllyWjVzWEZGQ1dBZ0ZvKytTaHM1bXJKWHY2dmtqRGlScFVncWFLTlhkZzFVd1NEeGo3NjdCd01vcnowdEsxVUtGRWxFUG5ISXoyQUxraTNyUC9PR3VheUlxenAzc3RYWEE1czFreVpMcmdoRkhzVzlwL2NSZnU4VXptamNYSktaSm5TVTBiZnRzL0NmT1VQNmgwbDQxYThpVjRra1JReVRQd1Q2OTQvWFI0MDFscTVNZkxVZFpHcmRTbElEQkQ1Mk05cFkzd2k0M0FnV3QweGE4SUkybVpJQUYzWXRPQmlOdDJGVGU5OWdyL3ZhTnJWcnBBMkxneWcyT0U1MFhydytSektJWndVa3RIU0VEa1V6OTVSUHhkb0hpR1NoV3RoSXVZem0wVDZzQ2YwWmZRWjI0MTdINzJENE5xbXhodUZtNGQwa1licTFXVmhyNUFTNlgxMEVXdm9SUFpIbHJ1aXZ4Tlk2Ullucm1TbEJMM1orVkxwZHNRTjAwK3BSa2xvRXhBd0NrazZHVTIxM0dEV2lVc1pQd0J4OE5YcFJsbkZqZE4rNlYvb2E3d3pFY3cwZ1ZGQmhOWVdQNTVacUNDTWtNL09XT01OQy9Wbko5K1kxWE1jclBoNmlYYkdnNWdVVU9hWlVZM0dRdk83LzRkVXYyRGNmTk1NY1BTZXN1c1ErRGpRYlhRTmlqM2gvRmQvbitTQ0F3RU9JNTZ0YlpReDB4ZjIrdG9VcTNieHhING41QWowZXhKWHM2d2Y3dXRoRWs0VWdubEIrc2t2cVBqalB1WEp3V0tRWlo5aEZ5RDRtNXVzUHdoUFBaa1Yyclk5QmprbW5Ta3ErT1Vrd2JhbnNjV2dMWG91b0xLM25rSitVUEEzOVVEYWFuZklMVklqeEtldXI5TExaNnFseGdNNytDMm1uZFE3MHozd0Z5ZXV5dGhWcHdHRGZ6azJEck9xSXlYK3ViTUkyaEdpREFUdnkyY0FSbEtlR3hZb2ZWLzBiZURVbC9MQUFiMStxTWVvY3N4N09UcEhKekpGNjNRV1hiVTBWSk1CTE9nQUI5bkZURFZMTFJoMW9vR2VPVkZPcW14dUNUcThsRDNwdEVHTDRQeXVGbTNhaWZPYlkyVGlLT1licStXRDUrRW5jZ1ZEc3FvRnk0dFA0ZlpTQXg0UFpYZllJOGVrbGxoeE15bkVmQndPczA3RUlJWm5pTExJMFJ2Rm5pckswcWYrSVZRYit3SjNJMEVqZ2ZSNk81K2pFRThFT3RZMURLT3cxZm1ubFlna3BBb3NHeXViVC9VY2N6aWRvYjc2ZEpwY2J1d2Nsdys4TFhNQXhZTGNUVW9iSzNPTUNPdzZMYk8zSTlpQVRHNElhc1FOZ1ltcFdkalNkbmxQWHd5aGIvY3VKQVd5cmdiZWhVZHpjRHhvdUx5VVM0blEvMmwwcm5nS1hUVU43ZG8weGZhWFV2eHZERnB3SjROMzJaMm91RGNOWG15L0F4Z3NBcmdxRVJVWkM4RysxUXR0WVJZazFwWDZPVW9Ga3JjMlpRWTZsY2NPeUhiejN4YTlnSVhGa3NSeFU0Nm1ETWpnZ0R0R096b0xlL1NIS1NFNWJFT21rdWNlTlJDVVNLUkVoLytJbGQvQ24vTE84VWRMNWV6QWw3VWN3dE9iN3dtWFVDUk0ybGJMbW9rVlhCK2wwV3FRZHI2Q0NUT1p4SU5xQ1Yrb0czVXpHT3JrY2hrb3oyUlpseDdhSEc3RCs2c2pjWFJZa2hjckNHZk9pano4OTZhRkU1aDEraWl0clF6anlMQ2lBWGd3YnNTVUh4UUp4cTFra2dZZmU2VmgyRlFTbjkwQ3d5ZU9ZTU1vRGdNUGxURGNKTTFVbTlYMll0eGtTRkdXaTgwOEpMS2ZSZkcvbmFlOFA4djBUUEkvSHlzL1pGUHV4aS9OMjA2NjdrbXpCSkNKVC9jaXJCWGVCSXM0SlhrWjlJK21BUFAybTcycHpwQzVscDgzYUJCOTBpb1JDTUowV3RSUk41RFlHYmsxTDk0T2IrNzNSWkhhc3MwOXdkM2IyWm56ZDRIUlU4V1dQYjIyTXk1Mzk3cmxpL2tiRDBQNlRwKzNXRUxHc3hUcEFTRVZldzEwUlJIU05NZTVUWEozVUFJaDlCSms3a085UFArb1VpVWNNQTBla1grS1JaRC9qcVFIWGE4N3V1OGNOc2pkYzUxNUlGb0VvWlBFZHIrdjdGM1A5NGZsc2kxOTBPb0NjNXQ4aFZtaWJRYmlrT3VVOW5rSVpDVDIwUnBMdFIxUVBGeUNGMDE0L2FyVi9IdlNIUmJid3dJWThQaXpCSnRmOXdBaHVhVERMWGZzNnhWYVFoeElUUTQwVlhKcFJFd01GSTlCNmdZYXYrQWw2NUtUM2dDVnRRSUEyRWhhamdWcjZWNFR6ZWhpZzdNVnlGV0ZoZWlaQk1MQzZqdG9LTnFIUTUwMG9wZE9reE1VZGZwM3k2bkhvNGpCS1NwRjB5cWFtUXRSMnJ1SlZ2Z0EyaEtZZjkwemFwTENjVGduSzZDK1FFWFlCOVJ1c1pSakJ2RkhqZWdrTFpMZndLeUNiOW1CTGNkelBMUjZsVHI1aFR5NlJwRnE5WkllbFR4SG5TOGdqWWgyNXpnVWdBeGw5M1prZk0reU1RY3E0aXNhNDFWREY2Z2lpZWh2c2tKMFRRMXEyaFJVbkV0VTlNYjB1RmNCd2NWNUdIeXd1RlM1UzNBVHhrQWQvdFRwN3hnWHMxVHBSQmdrQ1dRdUR0TnQzWnFJaWxvaStjZHlxYUxwTE5XdXh3cTJUU2c2RDhLUjJqSmNxOW9VK0pYUVFub0l5eTU3VDZRUW1VR09sMzdkWHkxYk9oTEo5TVhFZ0tsdjFlMFBCUnRTbVMvSVRPTy85dFVoc3hxajBZMUtOWWNVQlh2RDd6V1VEbEQ2YkpFZlp2bjd1Nkx1clUzcTFqK2QvMDJFcFpYalg1Ym9zTUt4R3dKQmd6QUR4T2pWR3FWVUx0MlgrWE85eUlzMmxoMzlpMjhCRmFZWEVPay9mSmIzOURjT3B2WWwrWThPanlCRVhYOFFsRUZqeDV3dytuTVVOcFVQeEh4dzc0L1NjMzJ5S3BMQW4zY1hRWTJWdG9mZlk1aFQxb0tVZEdFSWZVRTNKWnNORVY4aERaUmtRNFRoR1VTQWw1SzUyRmhmT0JtaUY3Q24zRVAwNEpLSDZ6RXZ5NUpRQzhBN29sWmJ4bzJlZVdUK3M2a1FPV1l1bkJIc0V6WEx0SEJ6WWVpbFNmYlgxWXNMbms2TlQ2OFRYa0h3Sm95NXhjVlNTR21EMDRBcmd5U3luK0ZVdWMvQ3RQcGRiN2ZTeTdsS3c5YmY4cWxFWi9QTWVPTjhDZkk1TFBySUlPOFd0cFAwQ09nb2hWYkIrZGRsTjJ6TXZkZTdqYjhPSFRDbFBhT1d5T2VrTEJVTU5zQXBVSjZvdlpGRlU0ZE4vaGYrQTMxRnlseXp4d1VSeFN4Qkw5M256dGlXbWNGYUpXamtGWXR2Z2MzT1JyNXFKK1VVVUZMQUorcFgvY2MwVmR5ZGVwT2dGQXNqNUhTbUZlVW5WWHd6ZE54Rm5yeS9GMi94Y1l3ZDBNY0c3aVVVUUFqU0VFYVJNamd0NVYzUGQ0OGowb1gyYXk3Szd2bWxvcm5HTnR1a01Kb2xyUXNzVm1LMmhpczYrcHpmYXQrUEljT3VIUndpUDF6ZDVnVis5c3Z4QUJxSnF1SmlyL2RTSEY2RnJPa0dxdFlFZUlwMUhCU0YyZU9rT2pFVGVqQkhhQjFoT2tRdHJRRk1XdnhLSXo2MmNPOUQ0QXVOKyt2VFdnengxNHhFdVBvd2U1a2kvczNSVVdJWXVjWnY2S0hST3F5dzcxVldmbDVpUUwwdHZvcGQxa29obWM3YWc4QW9XL0kzdmJFQmR3OFZkKzlqaVFPaG1IRmROc2ROVGtKTTdUeVFONVl5OXVDSUdTdWNMaGd2QVpQUFhkaFdOaTZmckRBbWFTSXM3SXBGN3VML0ZzczdjRDBnWkRtMjRRUEFlS3NabGVPM2lyZ0FJNGJNbzVseGM3dm1kbkdabEFYKy91c3I4ZU94eEc2LzBzM013WDlKWjNTUnVaVm1yWk0rVksxdVBURmtKWUJTM2ZLSitoMVZnR2V0QnF3ei9SNy9FL3pRcjRvSGRRSDQvNU1Mb1FnTi9NcDIyVUQ2c0tpSWY1eU9jYko3R2wwbVRDcnhYZkZsbDBTbDZoa2hudmMrZnBaZDZJaW96RE9pV0x0bzZMZUNyZEJ4OEFVL0ZrcnV2NnovTC9TY1B2VjdxdFFaUXVMZnBwSFU0ZXZRUHRXdnRnYWxHdzFmWndCSkFzdzM4QTF1eGVaWTVYbWxuTncreklZZWFiSzgyVnpSQnRPcWZDdW5mb2x2aDFyRng5cHM3QWhZaDdFcTNuUnc4WDBtN1BhdHFhRHJHejl3b3hmL0tMcklTQnczUEtCQVk0NmJUdXUyUzFOb3hPTmV0VktUSG9XRUFtUHlxcTJmMmJOUDhoTUw0blhmU09sY0FHUFNhTVJGUzkxRWo2UFh3RWJvdkx5Z09IanlraVZTTS9SQTF2L2NwWSsyUlZhNjMxMGlvaVNvMVpNU1I5MHVGMnRmNlJCUERCTnFHUXJ1bVNzM1poS0YzZjJIVFdkNThWd1IyTjRPNHpPQXFPSkZBUXNTUWRWeXNMRzRMRG0wYkJaLzlSOVJNN1MvU21nVGJTQlJLTVF5WWY2WEl6K0Z5dWp0aEprdlpmWGUrOTd4SSswRnJ4dHFXMC9jcTRjSUpnU1NGSlZHRDFHZm0vV3l2KzhOOXRSNUhCZ1RNQ2YrMDBkM2hhYytxampXRW1ZV3lIeGs5UHFQczhiTDVYUGUrZWtJQVVsSHRPR2JJQ09UMW5tbHFvMmdmNytkakhpUHd6Q3BDUkM3TGVYa3oza1RZWHpTb3F5c1BTNEp1ekhBM2ZxaEFJRzJWTWV3VWQ2TTVKQ3ZFeTNyWkNwdXYwOXB0T2xPb3k4RHYycWg4R2pWMktpdmk2S1RGQzBwVFM3YTNCbGNEY1BjL0Y3U2RJbDR1VUxTMTNWZ2xKc1hXTlRVMExTN1dwQ2l5MG1uSkk5dy8weUdJcXZMMFBlakRmQ2RoSVkwaVM4NkxlN2I3NURrV0JuRFdFV29vUzJmZ3NWQ3J4aHk3Umx3NGV5OHBZQnlaUzAxdndibHNHRW9QdkdPcTVKSmNpRUNZajFFVHN6b1pxcjQvZm5pVFBCc3JYd29tOFlnZXFmR25QUFA3L2YxQm1RaklWcENtak1DczVBaXFxV3dhbmxKVnMvemhBWFlzVTQ4WnFyZGhCVEw2Q1ZHWURSRk9iT3J1WlJqZ1BoakVyMTFyQ2J3L2p2aEt2TG5sZUtqVFRqMXp3N29XZk4xM1BFRXpTRGwvaElhbzFrcDJDTEpBNXhKeW9rN21qZnp6cUJXMEhBNGN2d0VkUkdTUVNoM09kUG9sMDEwZlRKRFE5RzRnL2poRUxreFVJQWpWS0F6bmcySUM2dG9vTVRqSFJaaW5ZajQ5dGM4dHFqU2tlTlA4QWNZK2M4UzM3dGswV09qY1c0eitxMGxxNVE1MzJubGhCRlVKMTNVZHJaKzl3SlVoNjh1ZjBMZDZCWEF4czh6cWozaHIwWUsxZXMyc1lrQytYemszK3FISmF5TXdRNjJYdFhPOHlHaGZoUHZqeTI3clp3Y2ZEVWNmbGoxeDhwb3dSQ0IwZ3VSQ2FoL21aWVpIanRmdmxtM3ZDMUd3R0dxbFNvVGZLeVErRGpnblozeXYrc09YNEtYanBQMlpXM1l5eHJLUnV0Wmlja1ZGQXlVNGlzbkJGSHdiTmdxYmVjWmh3WUZXT2RnQVI1aVp1NXlLa3hKYkF2c0RyWk9DMXRNTDhSNGJvTGYwVnRKcVNNZTNJRUJod0pPaGl1Znp2OVd5UmJZbHNYUTdoOTJHR3gvdEkrL3BGMzN5Sm1GUHNUTk9SRW01WFZDL2dpbVJDZVlyL1hua3dWRFdSTUdRQzE2a01XN0ZSY2FPZDh5bzJQTkMyRU4vOWdWdGtZU3lNZis5ekdXTHk3OE11M3N4cVFUQzIveXpRNXFwUjdIelB5VER0cDlhRFNKK0MySU5ZZE1zcnZVdmZqWG15U0phWU02UXFySEF6UE1hcXk4dnducml2QVVzNnhtSkVDMGtoaTlubTIrdXZ1UHVaTHMyK0F0T0tjbVFxV095TjVJMVVyQWZXQmZFS2U4VldNb0lHTkNkSzN4ajNGUWVCN3Vyc0YyWElTTVBHZzdMYk5mbGRoME5ZL0F0VVBHSVduczlpS3diT1BiQnh6WTAyTk1kK3d6U2c2bWo2eXhQbVNxUUFjanFhSktyY09kamxkbVRORHdQd3YyeGtPcDlva1MrNGpXS1F5MVd1dGt5ZHJXKzlMbVlaUWcydllETUg5U2tPQkY5WW9qUTFWcW00R1JtcmJXSjVKR3hqUUhIanV1cDJuWWVKOFJITGdLRTV0bVpkdTdHbStuVEd3cXN5ZzJkeHBEaDYwYzYrdVZ1UVpQU2o1aThyNXM3aGxnU2JvdXJBdXRUWUhadnlQU0pTQ1JIQkcwK0Y5d1JUZXR3bzk5OFNhNkt0Z3A2Sk8xMnpwMmlwVEY0T3ZiNk4xaWxNNXVKbGpKVHVCL0psUFd1NXZENVdYelVaNFdEMGpNb1VMa0MzbUhtbjM0SFNqVnk5ZEI0S2JDYkhYSTJ4VUlXUDVKZ3oxV0ZQanl2NFpuQ3l0QXdDNlVmRTVZcS9ZWXBrTGxWZTVFdVovSWQ0dGh1a3d0eXUwUlVXREp0NDBtUURWNmpaUyt0Qlc2UGtwQlFuK29oYWhxSmJkT2lMUUpGUlV2RkNEVkpBM2V3TGFYN2FPS2lXMEg1WnVaMDYvVGZ3azI5QnlnL3dYTkN4Wi8xUXptK1ZacjYvN0Q4K29UUXAwem1IcTJBWlFyMzk5OCs2NldacVVsc0hIenFucG1YN2grMnJFb0IySmRNM3BzN2x1MFc1Y01TcUtiNCtsSDlidUovY1pwNWxURjRXVlJsSm5rbHloamhuaFUwWHZkbjdkeUZicnJFYWVIVU5EK2lwMWRkRm5hQzFTeGVSVDBsSmdGeWVNbGM5QkRpWGd5bUlqRUFkZTE0RVJRbVluOEZ0NUtkL29zUm5JZzRTU0E4L1hsNnM4c05HaXdiU3ExYVNLWUJOSVlNTlJhQ1ArUkJBdE5pSXpRVjlmZm4wRExrclhvY0xkZTFvZkV6eFhPa3lDU2NMVEhDajFsRWRwdHFiL0lJbGlNc2YyUnN0VHZmM3N0eXB3eWExd0h0Ny94K0RYakZJbXVHWHFNdVpaZnN1N1VOQVBoT2NoVXdod01LSm9XMGwwaHgyTS9wdVcrb3dXVmZ5cFB5OWpjRCtLcXRjekZzRzlTV2lrNVpQbkdPdzR1QTEwZ1V0NDg4YVJhQmtiSG05WHpPZ0xqVTdiT2x1YVl5UEhMeVUyTTkrV1JxVlVBSnJTNVNIVnJvMWhkbGc2ZGdmWGxGZkFlZjM1cXQxRWwvTkVGWXVGZEtWUGtzbGFPZmdDRnB3Q05yS0RHSGdyL2hMTUMwQ3JLaWZJaTc0QThpUENsdFZvd2VlWi9Ld1cxRTA2RnE0b3V6QTMyRjN2ZDM4MzAxRjJYeG93TmF6ODg5Vkl3WHY1STlBc1dDTE1TYXRNcUVDcno3dXQxUjU1K1ludjRpU2tVR0dENlpBUWZGSTVPaEZIQnZmSzNOVEk5QjlIMzhka0NiVWFTMm5oeFV3Z2huWENsb1VRSHIxVDFaWUVJWklTQkU0SE5KdFJHc014Q3VUOTlMWjl5MUcrQll4MWMyR1JHUElEL2xtWWJ5QVlEbXlCY003RUpEaWd4SlVoYUFBTm1PMFdTRFJaK3haWnQvK3h6UlNJNlU0Njg1L3kxZEF2dlNyL0xxc2ZPSS9zOXVRYk1YQXhSSFZWYVNTY1VaK1N2TXhST1pra1NTa0wyajF4WFhOaW9VUHYvVStvMjIwYkV0amlkV1Q5dFp1VG5EMnRTdk1iN0dibnJNVkNOaUJnK3JiTWJwVURYREZFampudmh6SVhKWmEvcm1VZjVEeXhid1lrNk9wclJxQ28zajFOWFBkOGpjWDVSMk5kWGtkSEhrbm9kb1N5aWlqYTdDOVRWSTdPeDd1dU9rbXQrNEo2OGlEMm1VK2oxdEZ0Z1FySWlaVjYyOFkrZmwzYlpjUDYzMWNGbFVtYUIrRnIzaHdMOWFENXZzdFhZS3c2NmFMbEFBZlVtM2xKeW01TWVxc0ZKV1pOR1Q3RldaczFOMFpTRDRJQWxDdmZHbzZ6QzZ3c3A1M3FiM1lSRUJNK3hzUXVIZ1d0OWY1d1JyUXcrcnF5MFVyM0E5TWVHMUlsNkZOallWK3VUNWt3aENSalYrc3U4UWZCS0I2eVVuS0p5czdMNmtZYjgrOUN2ZHUzazJuSm5lN1BVcnY2ZHhlVlNON0JOUUpEY2d5Zis0QWo3azEzZE54SlY1aWUrZTZlQklhR2hmSDF2M0dRdmZxODZWYXp3NmVMV1pkdCtnZUMxSU1teVJQT2luN2F2aWUzbWVtdSszTjhXZHlQVlg0SkJ3YS9CMG5lWFZEcUxGeGw2Z21BWnVnSlVXTC91RDZCWHFDWGVTZHpKOHA2ejRkb2lDdGxNRjBIZjJiM1hWdm9KQ2tJZUM2dWVCVlRPaWk2MlVCNHd0a00yN0NMZVFNeHNDZTVlcjFjdU1RQTRHQktmeU84RkZsV1RrZnFOenMvT1gxT2ZQUFJVclB3VWF2dVdNeDRDV3RBZEoyYTIyNE5EUlQvSndWWFE0SEg5azVqR0grazFUNDFtaEFQVnlLK1hSYXE3Y3l1eDdpTjQrMmV0VlpwZ0FCMlFNR1JET0k5K3p1MHpHRmsrZDZ0Rk5neW5xM1E3VlI3RjA2MUJuUFY3aVgwSGFzekRxa2ZNTkEzNnA0a2xxUHlsbjhaczdqSXgzakJrRlRtZmpEdFBETkt3KzU4R3J2ZWV3T0w0V0l2U1dFc0t0L2RJZ2tPSGhEVGUzeEtlZmI4RFVqUGdHRnBFOGxsVWZUUFpDNXFpaUxoT0VuRForT282RU5OZXZSUmE1ZzhjcE8rZXNlTENjNWNZa0JwV0UxSWRBeTBXdEpDWU9yT2gzYVhTd1JHSjNQOHRSeFlaV0FZUiszM1pGNkpyUC9RT0I5VFVBV1l2bW54b2NrdHNKR3lwMFlJWnZIYjlXalhOcklkdHBOY3pwUEpGVEV2eUtObmFJY3pldCtoMWQ4c01Ub2VXRDc4cGxCZ0xueEdVYTIzSHA5YmlDK2w0WGw1YXVXcjVQNUgxalN2MVdyaFlwa3BvU2NSRUQ5SzZaSlplVUtZSHRIODBUbWZWc0VRaHlzS21uUGVCZFZ1dmVpOHQrcDZwdzZJcS9hVWVZb2IzN2svUU1Oa1VVQ2pyQ3JpbXZnUWxYVDNZMnkyWkNnYk5EN0l3MHpYUklKdW9IdnpwT2J5akM5UVZkWUFWNnZVTUxybytYZERySHZPTy9LcmQ0MUN6eUVBVkNjUmU4cjNmY1NINFdSWDRjWmVWeUUvUTRVV2RyTU1IdzVDTWhsVXNudDlZWWVoWXN1alVmRkdMOWJ2SjZxL3pkQ1NXbGpna0M3NmpJMmY1WmpFbTU2dG56MnM3dzkvZXdwaHZQc0FUWVZhSUUzODlValFBZjFZajdFWE5FOTNVNDcwNlFET0o1dlVMZXRTckFtNlkycjI1NG8vR1BxdDhhc2tab1NRdUVIU2Q4OGxwWEZZWExrMVJTbXJRWFkzTktDRzFEUk0wd0MzaVpKTExERW84MzgxM0pUNnJXbkMzTUd2Z0JtbDJLejZUU3dkMXY0cUF2SWZXVEhCV2dtaVdyWEJjOGQ4UElZd3p5a2R0Q3pjMUdFUTdhWFNEbmZ4cFdERXZNY1hjb2lPVG5FcjFHNi9idkVhVVdoclByekVFZUdjTllxY1ZiaHl4L1UxRnBVVnpSSVl5QzI3MnRocW1ieDZnQ2lHQWxmNmJoQkdrRnAyOERDbXlIQWQ2a1BDZWNSRWVqTXRIT3pORHB0VThNWDFLZHg5amZKTDJjWHR1eEZoZ0Z1UmNHT0Z0TjJabTNqKzYyaDRLQ1RDMjRmSUlFRGdDZDNzRGd5a29FbTl6SVByU1V5ajlCMU56SGMzMkQwV3A1aEsxMFhHSjRiZGNKWEpSREJHeFQzcFBtNW1aWjk4RkFWNmI1MmZSYWlSQmk3cjhUbng5WEdLTFl1aUptTmRJNkw0VkFNRjVoMTZZZ1R0Zlc2eTJhM1JhV3VST1dmeHFxcEVCZDR4bXIwN2xyWjZQd0lyTXd0T0tXMVFxQ1VyVUJqb3UxMjE5OVZ1L0RzYzg5am9GRVUwZ1JENE1PRDNucmh6VHhUdWFtU3dQWS9hdUtvWTJJVzJzOUt5SlZkNXFHMmppN3VRZkt5TkNIWTBoV1RTWit5K09LV0tLdFYvTGlZSHJlcXJySDg0Ri9PSTNnakZ5eGMrMzVOVmNpeVNBUzFDa24rYnNIWVI2YzZxTVBiR3JxYXY0SXNYM21kZGJ5RFBDUi9oeUdMbzB3VHBIbUFVdVhmVXJ2ME05SDZyRXJBdHkwL0VlQXVKNElRVmpWRFlHTDBaMWR4cHExb2hjWU1iODdEdi9sZjR0ZTIyRnA2NnRvM2RsTGJIY2N0M1VPZVlubGhZYTNLWW11WVVuaGw3VE9RMzgzaklGd3RRWHpXejJuTmJvN3FsZ05PZlprYUcyN0FnY2tIT3Qvck9hOFJMdlpaSWJwb2RuTXZJc2RQcnArMDR3RG1zQXE1YXR0ai9JYjc3TkNuaTNyNmtZM0kzVGhPQ0pXSFBYS1NlemMyZ0JwbjE0a0QzUTRRZlBCOHVpbEFVRWF3U0htR0JvN2JUb2s1WXVxSmhJLzk4ZHRqSW9hb3pRVjNFNmhXdFJCdHdqZGN2RlkwRWFlVXBSZThwdWlER002QjY0bFZSUHE2Q21NeDdaa1g3M25GM1NuTFkxN05XMmpoUUNpdENDanhTc3l1SnRIaS9SMllCVzBNME5QTWx0dzk2aEdaOWRiYXNzenhZWDNDM0k3dlFjNWwzNTNuVDVRdUV3TGJQVEpkeDhvcmdlamY0MVExVzFUSjVyRzhvdEJKZzRqcmEzL1ZsWTN1anJwODVJOThpVWkxQWxZbXY5TEVSRjRrNXNFYk5aaE5uSitVajBPV1lrYWk5eE9lTUlvUk5FOG5pN3lDWHVzcUxzeEE4LzFiSi83NE50Q2RoQnhMWTdVeWkvTWRUU0gwdmZ4UEVaK1FQU3BpQ0hWcE1VTUY5M2dhYm83azU1TkkwN1BOaGlqcmtmVTNzcmc0c2FLZk1QWkFobGdVdWErMTNxdXpBZXhIVmlMTXFJZHNqSTQ3ckRZSElXd2tVUEJJV2Rab3VhQlV5VGx5V2IxZEZBWEFLa1kvMDloYXpESFZkRHVlbStWVnNRQnVyMW5MUWpGejBlK292bTlEallFdjhXY2VMYVhNRFpTMGRWRTIzOEE0djUyazJPVTluaWcvZWp6UUNaNFZPZTRhck8xaGxsS2Z6NHNjY1Z5NjhjUmh5MzlkRWxEZXNwYUhQZndlY2VXWDVKa1QvTko5d2ZWd1praWRFdWhEaGh5NTREMTV3aW1jTGFzaDJhRHVweUVqQnliZk5EOWN1VGtZeGZ6ZmIxbmljZTRLS1Q4ZVJ0VXppVFFzRjJYM3ZSeTEveU9HM2RvcEwvckRleFNYMEhQYUNXS2pVOUpKUXM0dnBsOUJ1Kzg0d3N1VkFTYzlOT3p0OEU5L3JlS0RreVVjd2dYTnNmdTVSbmplZk5KaHY0ckVRMldyNElHdWtyMUxLU3VyR2t0eSs3MVNnNmtsdUJmalp0R2Eva2cxekxNc3VaR093Nlk5V3krUlVDTmRlYTluVVJMbERBanhpTTZIa2h0UHVDOEJRQ3ErNjBHYmJrdGpCcnFpbkN2RUpveDJOSkVHY3I5OXdyNnpuWnRlUGVRZFZtTTNYeHY0ZWY3WGR5dkRNKzBxSC9NTzlIUmJsVzFOTHJRM0c3NHhFVzBuRnFRNThGY1U3VE1KYkVRNjdaZ1dac0lRek8zb3NFYkovc2hjR3hCc3F2bXVCWUsvM3RxVWt3NkNTUDVrS2JtMHFpT1IzYU1uR0ZoK244dGc4bTNYQ0N0bWd5WndnRDhhSVpNY3N5b1YzUi9hb2pDMi92eXphdXBKVUduNWZrdG9HcWFUbEtGNjd1OFZQd2xZb09DMkx0Nm1mcnJJL3k0NG1uKzdtTXB2YWdITTllbEdSUGJTbGtMbnF1eXh4UzIvdHA0M3cza2xwNlBYTUlhVGJWbm5COE9UT1l6cjBoQTFlSUlqZzJ5Tk5rQmplSk1ySHc4dUNUemZGU2tNTmY4WHJtTU9XeWVpSFN6T0VGRWxCSlZ4ZG5UVDc2ZzJyMU40VS90cU9LZ3NTUklDSjArSVBLOVJYZ1JxSFkzdnpnUU5DYmtjaUtITzh1UjhVWW1RNmgwVTNsYkMxL1Q2czJ0UlZhc2EveWdyU1lHdGlRNnJQREFFVEdZeEZ3WWtIUFdKZXhUcktVVEY2d0s4ZEtwcTQrQm9MakpnWGFUMDNIMnMvbHJ2TmdGcGc1MzJ1VSswZExyL1VKaWhrZUlYVHcvQjVUdEN0ZnhGZHdYcklTelNsd3dtTS94MnN4b01TS0JCTjFZenhHR1A3b2M0akYwdUpMNVlVWWM5NGJPMDdhK3JLWW0zclpIcGFxd3dqV3F3MTNpekhZbHExa0poS1dhQzRSbVRBM0U2bHAycW80WEUxNlo1aUhaRWFiSWpiL2FhWFdUQXk1TkNRVVdRSS94ejRFSC9WbWttSVg2QlNmbUpEaE1OMDRIYnJRMzhrcFVrMVBncWZTMmpJNUpMczhKb1BzUXlTcGlqdGZ1K3g5S3UyNW5JS2N2ZlN5eENVdklHM3F6WkRKanA0TzBrNXFiM0N6RFVPaGMzcVVpVXhiY3hjU0dvemFyejNVQUJ0b3hRcXZ4OC9IRVQ0cWFERE54UWRuMXdHa0Fnck5INWtaZTlOa1JxbzNRaGVKVFY0N2lPRWQ0TDdLQmN4aldXTDJYT2YwOVBIRGJXdWw4MDdkSlpTOGVUeFIvc3ZSSWZxcG0zOUJ6QVkwNVdja3VuRFJYUUIzaStSVHVnMTVoYXhzalNwOFoxcHRlWkc4VjY5eUtQTnVROFplYXVjRllBL2ZzTUxwZ2ZBNUxWcEV6QnZCbzZRUkJweWdZUFpHWGN5M2NuMGRpVjRXcmlZNGw4UXlSUFR3YlhwMFVKZXpnOGFiRkU2NmtTbXhyK2ZHRDVSQlgwY0dKYXRuN0Q1cGIvVm9PSXJyTjBWRzE2R3lzdHBzSU5pWW0wV1dCY09NMW43aUE2c3NLWkRQcEJHUXNBenZXVEgwYWtsOVpXV28vQUF0L2xHekF2OWpoQlF3T0hYdG5FR3VNbDFKTTJ0UUVkMVpYRHRjVFhSUE9xS244RVVBM0FmbjJXUXNPbHZDcGpaUkkvK3RhaEg3TW9WSHN0REhtU3FoRERoMitFRXNHQm10NmNaWFluMjUyVGxyZXRJL1JuWVlRNURyTFYyQ0FkOHF2STVLcGMxU3Y1cXFEYWFqOU8wWnJvOGtnR3FzZnhqSk5KUDZzRHlPZnZGQ2hCWmZOaW9DTzJWZmxBVTNoZHNyN05ITWJrQ3d1T2JTQTlqUG1GYTFTOWlRaXlKOVNDS3lua2lIVkFGRFlNaW50TjVEKytlaUtEN3lTbU91ZU9iKzZBQUJPdExheDVHSk1BcEkxUFJCTjVFcUN2OUdLUHZscXc0TWoxaEZIWUJZcjdHY1ZVZGhWa0wrUk9XcFpZYlhIdm4vWkhjMUhnRHk2QmxPQU1sSk9xMEl5bmQxY0FNN0g0clg5Rit4Lys3TzVhc2JjUGVSdll0Z200VUxCZGsyMjBVd09vVWYyYmsraVh1WlNxajNMS2tpUDc2SGh5RmhRNFNRTjlmaE9ScGdFUkkrMzVnTHU1eGtLSjFXOG4xb1AyNnZWdExja0VsVzR3ZTFxTFZVTzBCaWUyM0cvWGlyb0d3a2l0RDJ4cEhYUWRtWS9HdzQrZGNpM3JpSzhaR0M0Y2pwQ1JZUVIzZHkvSDcxYjMrU3llUFl6cEUvcDI5aGZKbjcvd3psc2VxMWc0ckZlMGp2REVSRjRsU1lOR29KeDAxbURRRStGc09FS2x2MXlZbTdxdzU1R0NZTUV1STZmZmJCUzNvLzRrN3o3cHlSVjU5M0MrVTAxVzdoa3FVMkJaUXlUM1JuWGM2NXd2d1hMSHBFRTU3VVJMcTByZEE0aFIyUzczVHBNcFpTbE1aTC9CaUYxMFYyMXZDaVd0TmJFOG13Y09KNlZHczdEbnZXK1ZSYnk5bTlXdTJ0QXAxRTVXWi8zOHQzYWpNQzRIVE55RkRGWjd1blVYek9WZEJRblZGUWxldzJhT0hCL2lad2pLdmtmc05iTUFNOHFLd3NwQkM5bUZSNmJFNEVGMzd4RWdYTkNyMnprT2t6WFVxU2x6a1JtV3FKcW5UU0JyQTNMaGhkVkpuWEZhK2RROUVUb0xRNmdZbnM5MzgySUlCaUdDZUl0Um9PL2gveTFZN1QzL1RXck9BbnlSY3ZuT01tOVpqVHI1bkZPYWs4R1BiU1MzanNrMDlualN2SFM4Q2NJNHcxbnRkdmg2cm5ITmZsK3Nka1VNQS9DQUZscmdEWEhZZWtUZGV4RFZkcmgzajN6SGhuWk84R0dSRnd3TFVEWjFrYjZzQWwxWnBvUzN5NGxmbTlLM0VLcE5tRDJBZHhqMHAvUWtwaGozY0RpRXZSRjdHZWs5UXBjbXZ1bDBLS2hoSEZ6YTNyNFQrY0s3M3ZrakVHYkFUMmlJcVRJV09PUGtSNmdMTzJrTWpWTjdOR3hubXo1aXdRNXgrbmxiQnNvSnFlTlU2UFh6NEJjMXY2Z1BrZkNxemNVZ1A0WkNuMDlsL3pLNXNybmh2ZWIrcm05b09najZqUXZncGczUmxXTlk3UEI5NUFIOTRpbHZyZ25FMWpFeXdmUEFCcVMwK2Z6N1JWcUJsYWJRSm1hUy9NWndrd2pJOUxhOWpSWGo1QlNmVXRVWkhzbXp1MVFPbjhBWmlqVGlVQm92TDcvajRNNmNXY0Z2QkNBV001Q2hybEkycmlqV3VOSUdPc1JuTldvZ3NRNEgvcGtwTG9QM0ZHY2UrSFlTV2hvVWRBekJzRUE2MVA4ZUtPSXlqdTdDL1AwM1p1Q2lXdndDSW1xanMwQXRjelJzV1dnalBmbXNWQTNJemdzaUNHeG9naWpoRXgxeTJpcXM2NitCVzczSGJuaVhGK0tyWi81ZVdROWpZb2RMMXZRQ0RVR0NJU09lZDNIQXNBMlNaYU42eTdVN2k5RC9YNllNRWE2UHdENnpjazdVZCt4SXI0REljeDJkQUpnSTU4d3hxQk90Slk4MnVyV0lnTzQ2cWRyZFFmQVBVbVFtdmUwdERIVFhQaEpUMkMxQllyZExlblJFUFllcDRudXBWUXVqNjZxTkRQaSt6cTZxdnNKanFOcThwZlVpMkVCNG40N050SEpsRDR6bVdyMUpSYmUxRkoxZnVlTmV1aTZRSjRrd1ZBd3NBZGw5UmFQek45eWszY2hudWtiT2RUWm05ZXlxQ0JSUWJJM05LeitiemJrRHNvcVFydDhMSHRTSHN2MmVaQVhIUVhMbUdXWU93MWlaN2xxZDk1MnBhTVFVTXc5YWpSNmhYc3VPZ0U5U1F1VW53Wk13blUvWnA0RmFpNVc2YnRzdFN3bU9HRTJtLzFBSjdyZkZjWDdZTk10ZHRNc1RWeGhSQ3BxekVKZ2tkZE1zSytzdElvTjJHTEh5b3RJTnRYWUwydks1YVQxSzQyN0pxNHY2bXVZU1lmenJ5NEtzaUxTOEx3UmpJRDU3LzEzcTZkMEZmSXlZaWdGMkFPbEIwNStkWHZ1MmROQk1ydnNTZG1WbkdOMjZMaGdHcUI4ZDJwb2hVcnQ3eGI1YTlxSDZGZUtydlNYZ1ZtNmlDakRPdDROVXNyQWwxT2JqVGgzNlQ0alIramJOWFVvdEsxd29zb2JyRTlLeXJhVEw0QVFYZDExT3p6c3E3dTh4WTBaem15MnpERWJnNjgvSUgyZm1ObVNPTmpGWTlHVEU4bVRMK3lKRCs5aTJxbnpBU20zeUsrN3FkY1JOd3g4OU9tNmc4UGpDaW9HcStYM1p1ZU9XYkozL0M5WVA1Y1ZJQkZFeUcwQWV1RW5DRFUyQksyTmJhK21XaVFOcWxJQzN3U1l2N3VzOEFMb1p6bDdHUVI3bkxyamlVTmxQcndGTmJLSTgyWFI5R214WjdYQ25VcWhTSzkwb0FVb3RaM3RQcWxOQ1Y3dTA2eFNYdTdBL2F5WmsxbW5nN1JoUTR5dG5BamQrQmx3Vy9nU1ZxVkJJN2plZ2tlaEw2M1h4d2JpdmN6NEFZSmtWVlRNOVFnZkRYZS9hcDFNNm9jZ0VUdjBKd01RenZ4K25Pd3pmVUNINTdxdDBsR1ErdmU0MzBUN0V2S0JKZ0RiditmQ2JRdE9wQjhUMzkxUHl0TEhYbHlWcjZ0b0JGUk5aK25USXltaWxody9mekJ6Qmt5ZFBXaTZmdWVENzV1ZzdDb0ZocWRML1diZmJHTStJQzc3bTZORmNMUnQ0c1VSSUtwTkZpZlJYRmcydjB5dmtTRXU1RG1DVWIzUEsyVWsvZk1FdFdXUllmU2tXaG05SXJNODdmWFZscjF3SjZ1T3VGZ1U5ZnZSekovMDNIZEFJSm9QNTRCZkVoQUg4UXI3anNyOVl2VkVLRE5BbXNYL3grUTZQVHRGS1RNUnFMeXdxY2dIMEkrNHpKalhEZ01HQkxjWG8zeHNOYjgxRGIvcmNvRjN3OGR3UmlqbGF3ZVovVDBKMGRUQ3NZUXZqM2p3OS9mRkJURmxha0RUbWwvNzlPSW5lTURUSitWWFpRUThDczU5R2hNR25uVnRIMjZqYU0rQlF5M0NHVGFQcnhDTG1IZHpxNkhkblEwQkRpcmdPazlHaDByclg3aHJ2NkxtcGFNTUpjb0xYR1JHQk1WT3FjajhocjlZekw4bnVmVDAwamNuT0w1RGwrWTNueXI1UEszYTJkaS80RUN0bjRYbFJFTDBiQjlIK0RuL3VCM25zT204YmorUmc0b0preEtRWHpEWDlEWWxaSzRpOTBlNGwyT28vZGhrclp0NStyRlFheTRVZzU5NmZaREZ2UnRHZkV3Sm5VYlNrTll2bDd0RzM0cUt3d3FicEZSdmdkZEhZN0JwN0xxOWR3SXVjczEyZDBjUCtWNE0yQW1ONVJ5VXcrVDcxODhwNHE0QUlTZHVWa3Z6REI5dUVNTEJlTGJHbGFHQ3d3NEQwMjYxcm9hL2t6d0laTEpUM1pJYzRHNEErUVVmMmw0cVRXY1I2NlBQbEM0MFhwS0tHZllpMHl4Y2lUaEFKbHJZbmM5b1picFEvNXhUb3hmOTJwK0xaU2hKQng0ZE9pMnQ1ZnNxQ1lPQnQzTVd0RmhLcG9sL2tUR2hZR0VHY0Q3R2wwRTl4WUdGTWt4RWF4d0VpbVFrOVZYcjVnT2tGMk9adWgwTW4yVHU4NTZvL1dYeUNjVUlZWEliOHVHY2N0Tkl0ZWVvSFczRGd6MG5xbUNCbEIwUC9VSWYzUlRxbTNHTlFoVFRZekV5NXVWQVlyTmJYTHoxNVUrUGZ0cTJYb3RaOHR3dUxpUVQ1aUE0UEhYeTZBQnY2R2VhMlRKM2pVSTd5Nmk5VWhobUNocWhieVdla0RldVB3OXcxRDhxaGJNZVRCRDM4QTBSaWxoelVlcXI5QkRZTytvU3VkeUFxTUhEUWg3UmdoQ1Azd2N5SFpVWXY2V092MS81Y0h5WC9YY2RIWlpLQkxrUHBBODYyWDJ0aGJ5c0NlWWhhZlc1VEZoSUIwRXc0MTlnUXpzVHk2VGdlVSt5cHg1ek9iNlNzK0xRUlNwNWEvMXpTQkhvSHpuWWlZbVJhcVljamtCWU90ejdwQ0xNUzBJSEUvdDJGVEtyVm5mU3JPRmtBM2IxeEYveW5sZDZPUldxUUlWK2Uxam9OQWpDVDA5RHlMSzJTaDBlb2ZhTzhiQ3ZjcDZIUGhqRVV6KzBIS1AzS3BsT0FQcS9RdDFNNlZ0TVpzNWFrWkFxMFZiWEdsaFp2dVR2VzFaM1NGWlp0MWtPMnpaNnhHeUxZblNMMUpPdE1KR05tYlczZDRwWUxCZlFjcXQ3R25aUmNSU0F0aTc3aDVQRGs4ejlzV0JEVWFISUdCZmJQR3VndHZtZFF2dVRHZGx0OVdIVkZOVFgrdU1PRks2VGlhNTl4TDRWYUFueHdKdXNJcG9ybGd1WWRMelBRZjY1WUoyOUdEaDkwSHZyb3hRaXpSSjMyallqZUkvdStLaWNxaHZKcUdyQkN2YXlFZzBkWTl3WTh4RzlqT3dOSk5TYm9aeXBDcEpWVUEyYjRKc2tTRVg1dEJlRTFta3dmaUg4dzBEZGU5OEFUQUtPRk9qWWhEUnVzSWtFR1BrNFBYNnFONmpvZy9jWEI2NnBGbDdsd0pCZ0FIcU95SDdPb0tvbGJFYVJmNE5qblRKZ21ZTDhZTmczRXZjMTdiTWpGUnV1aTgzSjJkK2F1QVRqZGo5OGFNeFFyYmgrWS92dno3NkVwdURRWmw1K2NvN1NKY21BeUFJSTBQeG85R2pEWjdDN0lpbVZWL2kwVkVnSVVwMjhuUlBEM1BmZisyYit0VmU1akRYU3dXQ2FMS0NXV3JFTGUzQUhROHQ5dklqZEpqMTU4LzE4b0tvZkZObFMyWFZ5Qm1aN3RtcXZSemVSM25xSTFtUkZrS0ZLSXdvelIrSmpwWWpKVnM0Q29Oa045Tmk5ckF2UjRUcjNaVFk3aVIrNVMrQ3g3NFNhWEI2RElmdkV6akJLamYwOW1jOE9qbmU0ZWtzaUVodTVmZmNNWjZDdjQyUDZ6eTBZM0lTQ0xicWNzRW1WZmtUMUxMMnhmTjFUV0tER21SMEJ5UUpNdkVNVXpmK0EzbXNtdjNSalBXR21Xbm9raytLalNQenN3Z1pUK3BmYy96eGJnSko3dFJ1WjBRelp6WmtMRWhyaEEwZXErd1gzVHY3VmJXWXUrcWNRbnd3d1BhRGV0TnQxTFZLeE42L1B6VU94dVlpYzFkSHNaU1p5N3IzOG1pWWVIV1lpUDlnU1ZTTkp4MXVYcUkybnBqMFAxQWdFZkJiUWcvbTJvdFdQV2lnU0I2WlFrSVdPaFVnNGZoQk55dVd2NU0yOHAreHp6VGViemw4a0U4QXhCRHpLeXYwaldHSStiWExQclVvSVc0VUNRWkQ5N3hwaHdUdktpMHo2bEp2amRTemxOd3NjZVJlbzdFSWJGcTBxNTljTWlxWE9pcXNWQml4QnNVdWU5ZmFpZE1oRENvdGdQcG9xYU4rTnZicXY4UkhKREpmcFErS2w1SWpZcDJuSDZyUHhMY2l1VWx1bEtxWjBZaExyTEVoMHJETWNnNnkwQVp6TXRQbFZjOVFZcE1keVhlOHRtcnFJRThpT2ZjV2Z1UzQ2ZWVwbXFjVGNySG02b3RsMktaZVc4ak80czRTZjM3LzJ4d0pITGo1eWFnSjREUGJNQVNacmFWMzk0aFIwVTJPY2grVGN6QlEwa1dqU3h3UXQxSmY1andiL3JkRDVqWUwycE8wR0JOV0RYbnV0TjdhcnQ5TGtxU3FtRFA0Nm9weHJISzZha081YVQyVUlmeXkxWjgxZE95K1J0c0RCTFdXejd5eFJheWZxcnd0c01PVk1hQmduWjgrVjVGYzROQVZVYUcxYXVzRngrZDIrNFBvN1hLR3V5QVlUWHpIVXVxODB1T1JKTGpSZkdzUWJWeXVXdlpFZUVuT1Mya0VnbTZ3NXIwOUViYmI2djRGUFJxK3RWWTJkN0t6ejFoa0ZIbW1heGNNMXRteXVNaEZBSm43VnYxVEJGMlRHVmh5dkI2elozKzdIT3N5V1lwZUM4WllTT1pGRC9EWFZIOTJaVWEyR015WHpHUU5NNnZkdlBLN1dNSTBWMlJiaTVPUDZCcWx4SldsejZ4K3dza1JDTVFSUzJrb3RjWWZqRnZOQXp3ODFuL0FaQW9FK3Z1ZVpqd2JiZk13R1RiL3dkbFNPOEZEQVRObXFkdlNLUWtjVVZiakhuQW1IVFBmOS9UazZRbWluOGY4QkEvMlpRWXdNVEJuQTZtOG9FclNDMERRYzJPMmtuODV1T0ovTkNXWXppcVVFNE9PNWtURkNTenVsNGlaSXZnYU5IeW4rbmg2MWNNbCtIT2lMQUp5VWozVDh1em9RWUNzUTA5YWJoeFREZkxqRGFCQlNCMkNVNnljM0FUY2dONWEyRmpBRWdtUEppRmVCRlhwTmxTYkczSHVoQktVOEx4VDBBUFBlOWN4VFUwMEhDNmpPcm8vVGg1S3l0SUFpTVF5dWFFa1prOTRWeTcwWjMzK21oYzVGVzRvTjhSOENJQm9ZYnI5RlZuS1pldlVvaVAzRmYrSlV2V2VRMXA5ZFBRYXNjTkI5Q1lrcXhKdlFpb0xXZDkzUW1hR2cvRCtyYmxaanZUdHlPOW5wYkZQOWVpTEFybFgzSUZJOVB0aHp3bklZc0E2aHVjT3oyOG4vTmJ5Q0RKdjFwRks2eHQ5eFpaUDZKZ0tQL0Y4NUJSZlFyY2xpY3hUVmRMSE9hb2xJY1VBT2NRSjB2WHA5YWlOUEc2RmJ2a2tLakdHMUQ4SlE5ZmZoQnRDRzZEVlRReUR2dExVVWhpSFdIandMNDVOak5tTkVxTlFLM0RyV0FZb3RvNUdTMEtCalN4a3JCQXdpd2RYUDExZGU2WU9EWVZWWllQc3ExV0VlSVBHWlJpbFo0OHlHUkZQRkNBN1I1ekZUcjU3TlZZWGVMK21NWFBFb2dVakl0ZlNNTUxlNjczblpBWk91WTl4ak1ZeFhpZXY4di9uQ3NGVjVraC91dkhIR1FmRjEyL2hheXgvcmdNL0pTTHZmVS9FVENvUjZzb29OYjFOanFCZFd6eXpPWTFyQzZGaHh4bENWR2diaU9WK2J3Myt1MExEdkEyNzMzc3lDb21PcFVUZ3JNMjBYVFNJNWhHWDlHdUpER2thdnpqNkd3UVVzQXlJY0tRTHJ1WHRYSzR4VW5BSEQvTHU0RDl1N21CV2NBNjV0UmhXeGE1M1lKZ2ZxbklCalFWWjRqVkgvWDRNYXp3bmQzQWJXRnVQa1B3dW1nbFVaK2dMNFEyR1pHOGNmVEtBZFF6V0pIMkM3SGNXOHErUWFHSzIzeVA3QnRTQVkrWWwxd3lYYmVRYW1IR3dKK1k2Tll4SDVVc1ZnMW9NSXppK0RVcFpHNU5LK1pPZ296RGFJU0NaVHg5SWFlTjdoTERqT0ozUGFhb0dhQnlIcGhQc0R0QjBGR05TQTZOc2p3THBoemhGOXU3eW01d0UzOEwzYS9ZcXE4NHNyZkFuYUx0MGFEeWIyQ3l4aHZab0pXMmpUbVp4ZVJsam9jMExQdWdJNjBlKy9EcHBSK1Nvc1pVMXUvalpmVGpzRHM2NXprQ0F1K1NuL0VSa3ZTYjFvbXYxRnA4RTBsUEkrWG5KL2htTytCU0ZMaHJQUjh2UDBDb3AwRGJhU3JCKyt1ekJOSjE2VmdVRnRkZXZKNmoxTlA1Y0o1a2hJdE5RR3E1SldHemtDUENwSjd1b25VcmdXenVKQjBzS3F2S3hQSmM2VFg4RFpDU2l0M0pwblptOUZmZFYwRUFOaVc2QUJKOTM2MkU1ZGRKZjN5K0UzaTJTZFIzQWliWnJreDJrVW9ZU21pcXdWTERaS2ZvdTJaaktuNE91K3pqaUY4Z0R1Wk55L2NwQUhIRGpoL1UzUGk4TmlWUm95cDY0UXFZOXh4RmNOMEN0NS9Sb0NpMXBrR2JlZVZrSnVsdm9ObUxXT0thbENzRktEUGJrMkRYKzFIODFFZ0JLVk1UQ2pzRHBHRmpiRXREWng1eE9QVEhJaGFHazY4aXFKZlV3Y3BMbXhuVzhrTGUzdjhEL0Y5dm1aNkQyTmdSSkwrMjZUSEdTNndCbTkrdmVwdHdlOEFhYkN3dUUraHFYSXJKNVRrTXU2MHVrb1FWNXZmSDQzaHZnS0RIQmljY2ZkVm1rYzhpa3hFajErTkxLZXVWME1uUUpQdGd5QmFUem1YQUNFc2ZoSTJEUnp4ZFJJWE5SQnNTYzA2OTZ1V1p4c1I5VU5Wb1daV3BBSFhLZmtXRnNPNXlubnlNbUNRWENHL3ROUXJhUXQ4Tk9VU29RYktwZ3paR0c0cktnQ0xCbzhXMXBkSnBIeWphclZmWDU4eVY3S1orVmxLZUloYXJLdit5YkJYZ0FHNHJ3eGxuYkQ1aVdadkg2QWFTa3dhakZsU2NhMmdiSnFSaExnV2RzNU1ySEF2eFhjM3ZyeWovaEhOVEVjUmIzWDdEMWc3bXVqb2R2UXFFTThrQisvNk95ZXU4MmFFWlU2UVF4MFNNcDZMTDlDU0NTdCtGRXhvT1ptTHgvYlh6WjZ1YWpPU1VjMGFVV1pSd2tlTzZmVTU0Q0FLRTdTQXhYaXJzUHd4cTAzeDdjckRyd0M4R0tBaXJOaGZ5dkkxTHBHRWthakRCOE1QL3BxdWoxQThFT1hoc0NjK3BiTDFzamNHUGxtcUdTaFlNZ1pma1FZTmJmTWVDOEJRbTRrUE41VVpJQ1UxN2xJamhvQ0VxNnNSQnV4OTBQNFZDeTZ2V0lqVjBjR0tLZ2lOMVEyeTVhSlZtTWRuR3NQek5rVXQ5VFB1ZDlDc3hiN0UrcmpMVjNFY1NOeDI1RWRIU3U4M0t3cEU5cC8yM3lCQkFSUlNtckxSUUNGdSs2Y0tOdWNQZUpySVV3TU02V0p3eWpTSWxpaU5RTFRLMmxqOE8xRzlrZUJ0TTdtdjZ5NEVyeDk2NUpRQzZFVmdrUnFjWThYNm9CTHNQQmdIZnBUaG5nTUY2SzFIdDRiMVRXNnJEUnhDem9PQjNlY0R3MTBpSTlmMmR4Q2o4cm1uWEhmVzVvVE00U2FNNWt6c1FXbFhzaTlEWXZGaDRrYkhUR1ozdkNiRjVGVTdOT3dVUitaWjlZWWxtdmtsTzdYV21iWUdoSXF0MVlUSGdEa0J0dExxZDE2QTRpQVI1Z3ZWN29ZRmVGY1dUSU55ajU2cFdJbGFSbVNIdmdhQUZ1ZnVyekhmcDkzV2VoMm1meEtGTWgrRExkRUdwUGVGMlhZSVVvUjBWVlgwQWF5Rm9TaCs1b2lQQ29NZFh5cjNaWStDSTNqeGhTMmNWaU5GdUV1ZE9MY2RSMWtaMGZsUDFzZXp4ZmJoMmFkbWpBMDVlNm92ZndobmZIOXEzRWZESThMdjFGbE9BeitZZFZwUGFlNVdZUUZUL2YwYlMzR0N1WkdBZ1ByaXcyK3hwYlNVTjRSRVdBUmtpZ1YrRmZSQkovZWlLVEVTdm41d2RNRUt6cWdlR1paUzl4OVM3OW9YUkhBblRLQUpYN1VXZ0tteUdhS1ZpTTZKRFFISVl1RGMrUk5rZjh5c3pNdXJnN1VPSlNSSlZFYTJ6cTh2SHdrb0tIcWhLSGZCUU45SFNWbFBmRDlha1RQa0RLNlRyK0wxQkJBS1E4akRTNC9waXZBVCs5WFpCd1lSa2RLTEFzcTRwaDRKY0tkZis4aHZ0RERRWGt6MmlFRHhZTWR1V3JiK0Q2OUJCMEJoYng4MW5iR29IajBVQnpQSVRQRVJpR0VQU296eXJKS0xweTk0VS8xR3dXWjBzVVVxWjRYV3I2cG5WUFhVNXRYRWpFQVRNTk9PUmtCOEJrMzJUSGFtUlNoUWxrOGNPQTltTkNrOFhzVzdPcWo3V3hQYXdSTDhNVnk4WkVWU2FuMGRPL05IeGZ6WjNxM21HK1JiN2cvekUxRVpTSFpBRjlvQTN3MmtGbnpXQ1BXVGY4N2RGN29mZVA4MnozUUQyQjdSbjZRcmo4a3FTcTJVK1BYWFpHbWdscitjSjBsY3psM0t2QWcrYThETVNDMkhSRzg2eGpqYTRyOTUva29zZzZLMXNMREZUMW5MTUhhMDhlUW9rd0kxdFVTYkUvV3hBaUhZV2Z4YUFsT2JZUGZaMys2a2hCNndRcVZkQmQzeTZZcDF3aWYxdUZYK3o0dStSdW5wWDl4eWdudGZnYkgwOVgrR0xVR0E4aTlXaDFwVVpsUHJENGhKMnRKMmVnMTZ6bkdJaXhXTklMY0kya0ZpRFhxQm94NWY2eklXdUl5MnVrNFF5MGhWZDNEZWk5UzB3UE13dGRqNDJzNUwvM1EvdlJBK2tTUHBkaFloWlc2MWF3akZiVEZWMXdoNm5aRFoyQ1F1bmNSdlQ1VTNZQlBZOW04MlFtNVVSenVSVDE1UGN2RGd1TjI4OUxHT3BySFArRDF5dkh1NldTYXpkdVhiVnRTTC90dEROU0lzdHQyMVdMbHkwaWZtS2E1RDc3R0h6SzR1TnduTkRLd1A4M2plbVN1Q2xETGZwdjkyREhWd1dTb1pOTjJnMWNSWVBSeXhIRWg0dzlnVzh2T3RVME5EdmlSUVBNMkZZSzVUYTJ2VjczYXA5SkRCcjhTZXNuVlFrL3lUeDRGK3RNV3g1dFI3Ty83dzBST0FvVllvYWlpTk9oaWx0QkhWRk96a3FmcjlkQVNiSGNqQ0prVGIrNDZ0ZjU1SkYyMVFIbDdwNW4zNktMdVVyUkplcE5mUXYxaFZ2SWdVQ1R0Y25BdW5nZUNiRktzVXJKeUdyWVZYZWFNSERZT2ZzS21hWSt4a3dNZnJsbzJCYldJbGd6dm9PaWpKS3Nac29oVWVXTFFoeWJiSXRtT3FSek1ITzdGNWJRVjRycnBqbzUwN0JqMWJ2UHA4VHVSL0xIL3hGWnZqNkoyb0taMVZuc2g2WE4wVmVLbnc4eThlWnZRZUVJQXF1Nm5WRjA5MFJkdVF0WitzVlExNDlwRXZkMlBuNDJWR3dNcmhocEovaEtjUGpueDZWQTFxcU9HcjRiNTR0RUhFZnFpcHNqc3IxRU5DanUzc1NNUWNwQ21neE5yWWpXczZDVEF6cHpxMUdwRjB0Z3BneG05QytoMVZlVVV3QnZjQUZnbzJsa1hmc3Z5MzhjQklkcEtnRHkvQzA2QkhIQnpIbkpuQnhocWkzTEdoMm1XYnBpN0x2QXJVcHJ4cmpKUXhhZWtSRHBZcEE1TWxqWDhETVlVTHpxV0xCak00elBNUzFkVXhJajdwK0cxQ3ppTXhlcXl4bmtTZHNZaEZrV1JLTnV4OEdTWGMrY1Z3aVo4aGV2Yk1lM3pFSG1ETFdndTRxajNRbFI3VnN0OENoNlYxZzBqeXcwR3BDRWpOcHpNWFRjaXRlN0RmeFg5VXM0ZVpGT3BJTGdBeEQ5NVdXMm00dVAwZm1rS3Q5WVpJa2o1M0ozQkNiL1lXR0plaTdXZHl1em1sbWVHTnE3Ukl5RDBrSjk2TklCMGhRQW9uUkN4ejk4UjFTZEFHVHFUUDZVSjJKSyt6R3B6NTlja3U3UU9hbEdlV0pIUE5HbUg4Sjd1Ti9vTThwNFM3YUJVK2d5REd3VWdHSUxlYUpOVERiN1REN0JyMk95UjA3cFBhd0ZuUnhUSjZjWmRMOHA5Vk5zUkw2V0MrOURNL0RuWEFtc3ArZTF4TVIzM29reGJSN1o3em9TQk0vMld2S3NjblJBVTlLVjczVjNjL3lMTVcxejFkY0VhejYwRXdmU1YwU1dMTWdHM084a0FSOFMvOElzSWc2eEFCM050dStvaWI1Z3k5SHdETlZnODJrYkdueXIwbWw5THo3SCt5TDZucjEzSEdOQko0TGVyeHh0K0taVnVTdUlyblZuRitaMjFKNldPdnJydFZVR2FsclJ5aVRoWW1aYWZjaUpQRlNjams0c3BDc0NiOGhwLzY4N3ZWOS9xUkZ3ZDNoYzhXQVdrSHgrajg4dHl3dVUxajQ0ZmtPOVo5dUI5UDg0dG1HU0lMTnNReUpLZlNqbTJueU5LRmo4NjQwaGF5MDlzRXpYNm4zUFVKT1FXSnFPVWxYWVpYbzlDSFNLcWIzL3FpVjF3anQrZlFhMjRtVnZzdWtDZXh3TnVCdlZzcDFoa0tjajBSL3liUGdIeGo1RHdOaXYxVm5sWkVaZ0JWbzRYVERyWXdJYzFnaGRFRzBsekxWMllFUnZiK1hNVENWRHNYazd4QXBPYitRcE50M2k4dEdXVUxhVVk2Y0JvaGRpSG9KSm5TTTQ0Ti9OeS95dzdaTldZQUpNNWNtQXRJenpOSWxMY0lyQ1BnNU91ZGdlUXhoR2k0NFlNOTN2QUNuOWJDY3h0OTMzZ0t1bHdGQnRER3hmcFFTS3E2SldSeUNpUkduWkRubmhIVmhjUXo0L1krbEd5ZEtYb3N6QzcvZDlFSkxxUnNLU1djbVNJa1pMOU9nelhOZ0wwRTJXaXBRc2VKMDFwK0tEWWtlOExudTIzSXozNi9RcjVaM2tJYW41Qm5yWGppM0hXTjVKK3dzYkl4V3M5bmxXamNqaEpHcFhGcnZnMnB5Q2hwNStPb2E5UllrdTBwMG9ucHhRQVpScmw0QlpoYmRRYU5EekZHeXQyTks5d3p3UStGeGhOQ01NazI5R3hHVnlhaldubjB1OVJqQzJBZzhpcFdjMExyajFSaDI0OGFSWVJFbHJRR2dROW9LQ2xqZWowK29Zb0ZrVHBKWTNYemZQOUk1bTVSalNyZ1ZDb1lIcDBZTWdoT2lLQ2xKNGdwanNIYS92VFBYNmNGaXFpOGt1RFNydGZjenNlVXloSE9OR3lXSCt5ZHZ1WDMrMjJhanJYelJITEFZOVBMZlFyblN1VXhCa3UydVlCNm5vV3VjWDVJbFBTcURuc0M5NmlaQm9Gb0w0YlRram5rbTNlSld1NEY0MjgweE9HVExPNWRUeXl3dmJmWDk1eTJhc3FjU0R3UklVbEV0TnFiN0Jqd2RzNUdvWHBEZ2kxV2JIYjk3RjhFY3JzNXhGUWtPTjZSNkUwOFB4SVZYRkI1ay9JeVk1WmQ4TG1BYWVwWGltMytNbExYMDNlQXlsbUNOS0dQaGgwSlJSVUpBSUk5dnR2ay9uY013b0d0a1VCRGRYQWpPdEo5Y2Zuc2hHenVHTnBzQ2d5aU1YcGc3LzBkT1JhaEZWZzFnVml5aTRkMTc1K3grY0xxVG0wZFhxRlN2bVhWNjh4a2xEUWZlNDlVbHZWbGs0RnAxN3ZZNVBMYjdTQ1lzcnZuMnF0VXJxVWYxU3BCYmpVa0FNeElOeTFNTTJyejNFeGFFTGY5TnVXQXFhcXVVV1BEYjl3d2I0dTMzc21rN3U4dFNnQnhPUGN3RDRHRGF1TS9CZSs3RytNUkRoZy9WM0ZkZ2t1dlNwY0NMcE9jd1pjS3FtNHBiZ1RVazdBSUVaY2RVR2VLaXpQNG9UbVpQRjlyM2RhMWNiOXI3S0VldE1oY0ZBZ3diN2NVZFhNbjMvbXZOUkJZOVh0VlUxY1g1YmF3dGFjcGJ6VWlEVTRZSWtBQi9WMkdwSlpYU2RyRU5xOTNjdjVxS25udDhLZ2tJMDFNcXNuTG9DbTlOblROb280cEhJUDFXb29BVGp0RWJva1dhUHMwa014K0JWZmF6WnJuais3ZjZ2WG5xTmlXUHJyWHVxNjRjSGZjQzMwTzQ3aUhyUVNPMFJDTzdXZlZIbUpNRmpnNHBoK2V0UXQ5Qk12UUd5c0NUcWdTckQvcUdnUERCNldSa1B4VzV4ZUl4eWNWK000SkVxQ2xKK3NmSHZIbWIvbXlETVc3SkZMR1MyQTFtQmZ5VENtWERqZmZGS3ppS1p6dlF5eWxMMzdtNTQ0L0Z4eE9UdDlja0cwUVNRSEljQWtQeDFaTDBkYU9lVzRrUUdjR1FDaVVpMzNENzNtbWhUTEdTcU1vOXYwMHFuWlZNLzY2ZmwxUkZncGp0aHpaRW1mRmpXaDJONGoxWk5JandGWGRjRTJDN1VyelBpM2gxODRUTjV2TFFmZExEZEJxZjJBTjZwK1Z1ZG1CSUpONFZMU3hldFgxdnoySnJjS01weGVJTVh6SXplUkRDZng5ei9pbEtsM0l1Zk8rZ21hN1BFTHpmektZSWxET3ErUStpWUhKN2VJNSsxQzFFdSs2Wlh4bjhUWktuOENnWS9MZU9rOVB3eTR1N1RNZFh3VDJyVDVDOGxUMHNoSytxTm1MTXBvbEFPd0FrSVZBYysweHJUZHQvZUFZSTR2UzNNMXNId1N5L2xSQXpqQ1NoNVBBQzlQejM5QkVlS3RweitZNzBUcEJjVnJMWTdmQmkyclpmRmNMUm1tVHpzZWdBVTJWR3dpZ0YrQ1lUNThjaUhPaC9nV1QrTjUrNXhyemxDU2dhYkxDVWVoTENJZi9HQ0lzM2ErMDlOeWo2MzVDQ0M4NzdPTWpKNlhUM0NNdTZQaHk3SjRZeHh4WkFHTlltemFNN09UdDQ1UUJvYXVMb2J6aTQwckJMRjZRbEVSTXVkaHZ3OUgwM3NSMjBTaVRLczlvMXpyKytpZnA5eVBjeUFRLzU0R3JQRkg2SitZcVFLVHBYZTJGT3grSVRBRzJYeURNNUJ2S0hqSm1zWlV3T2o3U3c4bHRYSCtxN2VvWlhoZDNVT043UXlEZVhveUdHWlRMbTJzR2tQeEFyV00vTXl6dHpQZE44SEN6WW1JR1ZZTGZZWE1MU2hJMFh2cTF6bnNkUkIyMWVvNkVtU1lMY1VTNGFDanhkRDRtNDM2b201dWpiam5USXhDV2ZEYllyVWc3M3lsK0ZKdGxhcm9hSlBObFZiMEtYUVlLdW9CZkZEMDNmTmNlL3JHSUo3ZVZLTVFtYUJLK0FVTzdmWitRVzlNWnIyWW9jTVNneGIxT3htNi9Bay9teVFWNWtQQ29CbG03YXRiekFMZktINjZEZUxuakZyeHZCOEpoMExCY2F5WDEvMGc5bVNVaGI2RGpzVXBWMkFPMStWelNBVFNNVmQvOHg0RUtkcE5TV0FkM2cydm96cmJyUUJmQWxValY3WVIwK0wyWnNtQ0FYamY5UjJEMytTdkxmOU15QkdqdkFVNFR0Z1plTDd0UVpldVJOQ1diL3pEdjBzQ0d4SDRjZVVUL04vTzQ2cHM2OGxUVW56QzMzbmwzRHNmYmlPVW5PL3lmV3N3TFVLSVBFc1BKR3BjRndTem8vMkowd3NTcHlqS1psZ0ZOeVQyUE5oZU8wRWZqamVaOHE2Y2o2OFM2UURRU1NNbmpBZEZiMHdWeE9KTHJFUjlrSUpNUjloSW8yOUVnSkxSVXIvNTJjVzlwWmxIVzZ0ZHVwUnB2NHZKbUFISzRDNUlrUUh2aG4zcjVsY3V1UUREUWtHMEFkRml0Sys5Vkt3Qy9FSTRNbCtSbitmc2YrMXhWK0ZKNlBoVmgxNndpRXFOc04vT3Q1MGFlTU9BZmt1L3liYnlIZW4yLzJ6YUlCYmRFeXd5TEhlSVZnUzFWbW05aUhmVklRVDVFdnhocWI2MFRRNitLWVM4UWR6S215WEtDNHBGSlZrNWFkNmpnS05XazRtbGdCbVNJcDlOV294S04za1VXdEtUVGE3S3MxT3pNUnliMmFCT1IwN3JFSm9nRmhzazFwTzRNQVJKQmlDWUI0VWZFWlRXaHFhZVFGcEYxdXg5K1NLWktwcUxhY0VwSEEyU2VpVS9OT1ppNEhiVzRkYUJyc3dGOG1iRnBLaTF4b1ljVVpxVXJSVU4xMy9tcnNaZTZzQ0I2MHFDVWtCVTN0WVlUa0xKaVBHMVpoa2oyQ2RPK3MrMGpTTHRURVVRR3NyZUhvVDJRbG9yVkx6YmFQUlhYM0RiNG5TUENwQnM3cklId2JQOWpTZ3g2bW9uR3R4NmMyYTRSL3hCMVlycnpNdktialVOSlpBdTlVb2gyazRLTWxUcitDQ1pxdElMbUFWUXRUekZQYnN0RlZHdkNDZlJEYmNYRDlYVi9XWC9WRXowMkdqY2Z1NUpJc24vNHRnR0xObTdwSnhxeUptZ1BuUUhJRkl2R3JZMHBVc21ScjdSUkRvRllSZkxVR2xvY2JUdU14Q2pFWTFtM2hEZXI0R05uakNoV3dIQ0thaEo4a2tibjZ2SDVJUWxucmZQaEdWL0g2Q1pvMHBaMXowU0VqcTdFaFJuMjR5dDYxM0U2QzVTdm9qWkxPUi9TeWpYS1Nha0xCUWEyMmUzMzhuOE5Ea1pkdGJIZmZRMHd5V3RlTUs4dnNENjNNMTRJTW8xYXdGZWlteElMNWVUL0ZNVjBpNmdlVmtidXY3R24xSGd1eENESk5YZGpGeStmVnY3NVBPVWlGWWZsME85amdiT1ZCb3ZSejZGU2hBT21qcG5FTXRXSWhiNEEwYlhVa3ZFY1lUY1NOTmJGOUdCUUliTzhtdjhjZmVSL1Fpa0ZtRHZEaGYzVkJZU1FPQ1ppTDdXYkJzS1VIZjFLUWFzYTRMdjlNNHdCVjJ3RnJOdVR2dDNaK0lRRXhGaDlhYWpVaElDNGhNMzhRN2liZTRTcFhBbUUyOXVFTGRvbkZqczc4V01wZy82bTdpZkhVUGJzODlkMkxoK2hJdkpvdkRBd0k5bDhlNlNZbVUzMkZNUjYvakdQMU5xVDBOckdyNk9aTldlT0ZjWWJvdWw2WGE4SytLbmtBbUl4bTlaemtYTVNORHE2TFZLcXBiUmxzcThiWXRLbjc3SDRqZ09EQk5PTUJDWG1ickh2dVY2aUhValVkSlhVZE5Qck51RFdCcUYrNXczRGpocElIRHQrMk1sNVIxREVnRHlka2xIWG5nL2JxT1JXSHpqYmtmS3kvVXFNRWh6ODBHTFg1OVRDQWZJMXhpTi9Tb2txdUQydGNZR3A3aFRPbThaeVpkK2lHU2pIQ0UzNk0zbWhPM3d5OXY4UkVFL3JybVUwRm9iaFlZMXRTM2lsT0xDcFZVTi9MQUQ3d3FLYTB6blVFejFKMnlSTmFGQjdRMmI4dkM4aEhIS3p1MnJxNE5VcVMxV1kvRTZiUllBcWx6VlFqcUlZdGUrd0Q5azhkb2tFcWtERnRoWmEzZnJ3Z1dHZTR4ZGJSVHYxYzIwU1dZV3FVNFIzU1FLRVpsQ3dOcVpvVGZKVDhuQVVQbE9uYURqRnlOdGpiclR1QTFsQ1dCelBFSDhMNURKYmJnYlVYektXTGV5VmtmQThaWVQyVFpTL1Rwa1YwSGdDMkl5MjRTMXZIVzZTNktaV0x5VTk1UkQ4R1gwQllRMkRPaGxDbURoSFRnanl4NmVOdTg2eGVkRnhYVG5LQlpvcGRUUkJ6YWZLdkRIeE43aEdyWVBsMGVyZjh0MFVwUzZpWW9SZmNDaEdJbHI3djFLd0p4QjJIMHBBc2NxTXVGWmNYRE54Rkg2Y0l0TXJDTjZCcVgwMklMeXczOS9jbGRLR083WGhXOWNlZWFVbTE1VEtZbUEzakxwbGF3Rmt0WlB3ejFCaWRNU0d4OUl1VnRNaXdvbXg5ZE04a0VnMmN2bU01QzZYdFZJRnNMNmdDMlJhblBENUlqRldxYUVDWXJlbm1yaFp3dTZxb0pxNkpkZDdYdW42R3JuR2k4U0czNHV1dHlMV0lseERaODNCblV6RXpTbzFkZUhXVTBVUUhUWDQyL2VJakNDSmNuNWZLNzZtaUJyVHkrR1BxWHRwTWwrcXpKM3M2OWUvVVhZT2hSSVZqRlJLNVdqVXd4KzN1V2lrWGVrWmQzU203MUJSQ2tWL0FNZ0VodFQwaWg1dHB3dFVSU3FmMkhiNlNsckhsVUZ2WjRsays5RmcwMUFDWnZ5Y2paNXVySk5GWmUwOUtXejJ5Ym11bm5pRi9QOTBjTnM5Z1U1WmN2NXlVYVIvMVZQd1F1Q3ZhVHhlTFVXKzBxRUlNd1dDNGlPR0w2MWJUT0FQNzREbUEwcVJ1TlRVTEJRaXNkOEFOaGRoOXN5VFpUa2ZCWnJKakRaRmQ5bGF1ZnpkbTdMTmdscDViZGxLRU1GSnBEQkMzazhUZzNkTUNvT3BMdzFHcDA2YjgzSVNJQ1M5ZUN6eE1SZU43WXYzYVR0REZUSWJXSWlXMjBqNmJ5VnlkNEtSdE5rVXQwb2Y1VllueTYyTjlUd0RsOEJkWmwvcGpUVytRVGhYdGdVTnlKZ2hXdno5a3NNOFUxWFlyNWliOEJleVpVTEZTMVBJTnlIMnd2OVk4Ry9CdmIvYk1PYWtCSE0rVnE1aWFMeTNiZW16cEg3Z3lLdFUvbXJ1MldydE5waXpjdll0ZHNtaHpoZDV5QXFGbVhveGxQQnQyaXM4U1dtR1R0NUh3by9XVEJJUEdJR21ySjB3L3M4Q1pRbGxFRFVRV0k5WFMzUE5vbENlMVoyNEp1RUdZYktDQVA2TjBSQkx5TTl2OEhSSXh0SGhVNzd0cjJRWkxjT2Z5amptRkxVZk9Xelk1eWpxSGtRZzBucHQwUm9MSzhrRlgxRXRzQ3JKUUdnbmdmRnV4YWRDeU9jblk3UHkvekk1dmxjYndTR01yYU5vQ1ZXTmE5K1NhUjZtV0RNeUE5UC9mbVp0bGRUTEZ1MlJTVzllY1FLL29PeHRQa2RPSmh0RW5xaGdlTk9jZFhQQ3E5OG91dm9FNDRCdTdDTjR6b3BYR2lpaGpuK0U4OUhNNGJMcDA2SjYwZlpxVnFlNVIyZWVpbWF1UGNXVVA2ajlhbHN5UWw1MTdDK0MzcjNycWhsR2plQkxiVkw4aEhSZThjUUpKUHJJVmE2YTRQODA4TkJTY2xJWGlpeVBGaFFXc3JNRmRLVXE4YUJYSnJRMUNYenBIODN0dm9IRnBvMUcxKzNOT2UySGtpK1N4c3djTEJUVHIwMmwxUGJNRERoOUlraXhrM05tWUZBQ1RUQnl0SFI2Y2tQTU0xQVltTERxZEtEK2E2THd5aHBGSy9QTENHK3VXNkRXWmg1c0JCWEZaZ3BqK2svdTVkTTF2cWEvd3Z5cXVRSTFZMWlzTXBjRUM4RC9LMDYzdCt3YWJnbmpFZ3ZMdW9SalY2N09YeU9nRlJTWkM5NFVsbnR0dmlKYllBaXVTZ01DaWRwbW5BOXp1TzdmTXhZUmFhM25ZRDhFb0VFZTNzNUdXL3M3bmlzS3oxMExpY2g3ZTdJM0lrY0MydHRQYWVUNDBiYVZDaG5tSE5VZFBLRjlsZjY1SjhmcC96YkZRajcrZUFSSVlvcmdYWVc4VjlwMUkrZ0pIU0xkK0pQTVFKZWNjNWlNNHNGeWJUWHMvR2hDeFdYdXpKd3ltK3ZlSFlQc205M1dTZnpiVmpUeFR4ejJpTzRwb0p1cEFNekRZK1Nac08waTRJd2ljQ0g4SXEyVFZmdzNwc05yZDZnOERzLy9XYUNnMnFxeVJyS2REd1p0aU1ObzZla2JrR1B2ZWduYXpUa0JUYWRnc04wVllBOFdpNmhZTkQ0WDVmN1hVL21YdHRFTVF4eExaaHo1SlBDSTJrS2dJRlBZYjYwV2pIMDRaTTZ4Q0dMdVBqRTZVU0tDbGJBYmZIenZFc0xVckx0MDlIWGw3RUZpTmxCSm9ydVZMZmN0bnNDbWdYaTB6dFpHR2lBNHE4MSsya1BlZXV5NDVWRWtrUktMc3JZOEFjYTBtcm1NSllJK0EyVy9EY0tIU2lCQ0ZicWsxOXNqcUNQV2ptTmhlNUZ6NEEwcmM0MGpwUXl6ZzREVzVTUERudzYyYlZKc2ZyNFJyWWZNRXBKcDI0bW9uQWpObG05OWJGS1hkUHdjd0pRdDNJS3lZdGhTbXpGODYzeHJDSTFVaUtxTnFYN2tlOFljVGxVN080c0NNdGhpR0IzL0VzbFRVdUJYQldLNkdjazFobVJwT3VtTVF2Rk1kTmV3UXNNVFkyWDNOOUdOc2I2Y0VucE50ZFVvNjBPNUV6YjFGaTBHTFMyMXRSSVRpRXlvTmRqNWJ4WGt2L0o2amJvS0pMVWw0L2xYRkFuaS9BUEtvYWxBeUlEbTlMeDd1QytBcExVYU53U0RzOThXVTAxNXZnQ1M4aGRsOE9CNEYvV3NqTDZHNlZXOEVtRUw0M21MbDdEVGNOWVFaOE5vSlpJeTcvbTJuQVFod1VjZWYzb2IwS3dmdGdPdU9FcDlFQ1FrMENJSkhDSDd1UGFuMFdLTFVYSittbnZwOHpXL0Y1Tm9NNFYrdVZGMXZpMXAwYUxuUHlaMlZNY2hWcEJ4ZEVET0lPV1Jlb0lMeEpiZk5tSWlvYlpqWGxsWjlxWDNPdEtqb3JZWDB6VTlkUVJ1MmFaa2FNaFR1RzZOaHJQOVJTOXZEeHlBWG9XMlBJUFpGSmNycDR6V2t1dTN4QUJxcWV0UEZ1ZWcrRWp4UEZLVFNRNjdhVHRsY0l6Z1B5S3F1QlRYOFBPU2R3eWVmeWhzdHdNTStkQlhlOXRCK1l0YlB2UE1CV09HU0ZKWDlWek80OFFSVUNERFF3R1lUbGtuV3haeVhvaTRnVVZwUXprSXBybTFOdjh6dGRGYmUzM01PaEZIaU5FaDFwMjhZNzNxeDhLYU5pZ1JtY2RkL1pWckFxZGtVWkNIQlloUVkvOUFWT3BPcEZLYTR1TVZXUmw3aFUxbE8vOThRc2I3Ulk2eHowZlNvMHpzTldaMElxUWt6ZGg2RXYwZHZva3RiVUlPWWFsYm0zN0xVblQxUDBSZG9wamgwZjdBd28vM29aWTNUaUNHTkp3RHlGTEZwZGk3U3h0MzFsUWtoSDBxaGx6SHIySDBQUWp0dEZFaTBXcTlLaW9ET1BKWTFJUjVUSjBkSjU0NDhUUTdOam5Wa043SVhRZzBEU0V1Q21kODNLRUxNQ2hFUTFxSnJXL0NCWkRoTFhJQlJLakNxbkkzVkQ5R0Y3cG41cXNoU3hBWFJhQmMraURwOU1sYlJiUnExa1ZneU8yNzJOMEhpNml1Y1lvMVpGd3NnTlNNYlpQNmZ0SVpTTytEdGx1V2E0b3pYL1AwTVd5TEFzbDVJWmNoT1JNYWtUanUzejhCdWZPL2hTMlFUUExVYi9oSFM5UWVLM0xsS1VMeW1FZEg2cE5BdlU1MitIeHZtU1VYZkFiM2grOTJUeE5VL0hsdFF5NkZuTkJQbi85Sjc1Y1NqbTRROS9VOWppcGtTUTVnMERENGhHZG5HdjI5T3FRUVk3eis0UVN3alJpZnNONUY1c1dHcnVsQnd5N00raHdsQ2xlY0lLTWF1SDl5UGsrUWZQQkV1NUJXNHNpNWdPVTRWc0tRMXV1YytBeUNjazUxQkd6c0RxNmZPbDdoYmJjV2xSOWg1c0U0U0FPZTFiTm8zN0lQa0hnYzFMK1pjTzZ2dCsyYVgxeEIrRjlYUE1OOEdPekMxeHlDb21DRWU2OTJUMklIdVlsU0F4QWpQMVVSeUFMNC9HQzJzTDgrUHAvQm5udTlhaUdWV2p6WFJFR0FwKzZGTjhEQ3hBb3UvTG0xcjRCemcza0c1U2dYcGdKQmU0d1hLM1NuRFdKaE4yV25Nc0NsMEFKc3lMeVgwUmJualA1dk5IdEVuQ2ZJTGRibkhLQTBhQi9GS29FcWV0NlZBWXduRUFRUFV6ZTQwV0k2SWRFcnJoNXRleXZDQnd1R2p3YzYxZzJOTDBnVnpXSG9GU09YSTlhV3lCMFljNmFuYjZUWGF6d2pGeEVrSGdKZVdVQVlLYmhzT1lMMFZ6dDZqYzZOTFIwYW5KYVRueEUrT0Q1WU4rNDJhQUd4WFdVQkx4Wk5uODZiQ1ZjVFF6L0FZWnhsUi9wTGMxbEZvemNGYnk0QTVQVDV4VFg1TlU2WFFNajhlMVIvVGI3YlAya3lXYUVpNy9BRHh6aVU5UW82ZFYzMzRaUW0vekdvcjdmSURFRmFiMEJuNFR1YTI3KzJYdWpUckdxSFBnWlBUaTRZMDdsR1R5UXNFeGk5cXRpUUxhUCtMdmxMSzlKR3ltUjZ0SUNQTE93MGd6SEFieUNTbWJnZjVmTnNhRE50b2c0QjdoM2hjVjhLdHFWN2R3NXczSXFjOTJoQk5FUDBPcVFpZEFWbTdkSWZPSG1zRmpHaWZseUdLUXdLVFhsdXU0YzRBd0RBR2QyTm05SXpGakN5Z0VQREpiaXRPaWpxN0JRUDBVYXRCWVI2b25NMnZZNUw3bzA0Z093MVo2Y3RUVkhrSDZ0N2ZGeGxCZm51d0xMY1llZTZLYWVJZ2dybW4rYzljMGszZ05IckRKV0dmYnpXTXJ2V3l2WnBpNXU3UjB2TVVaeWt2VUh2dEp2NG9TZSt2MXFCMWl6QTRTSHYyQXpHOXM4bFVrYnByZEJyUGpxUnI2QW5mZHpmb2NyK3pLRDYvSERQUTBDaHdzZm1rQzA2WmtKT2VzQzlnWEVLaFRRU3RWWmJiS3ZGcUhjNm9mdzZ4Z0dYWHRocVUycGZiMlRiTWJLb0lPNGp2TUU3TStCN3EreFh2YVg4OHBSN0NQRUx1cE5oTldFa05BaUZlckh5ZkpEcWZBQzRZUG84cFFMeDhhR09jczJRc2ZDSmR0WTN6ZEFjVTA0c0FPb093QWxKcGFxSFVSdFhuNjZwR0VObS9HQ0lTTmFnTWxXdzc3QjRSL1dOLzFCYVFtSUl1K1NyNEYreGFwaHFKMW1pNVcvdERMVVZQZTk2NXdxMEd0TXpOcFI5TTNZK0JjYUhUd0N5Tm5XM3dqUFhLV05Oa0l4dnhxVkVKaW81V25MVmhxV25mRUJnYVFsQXJIUHllQXZ1MnJKS3B2elVBQWp0ZE9CTXpOTHk3K29uZW0yT0pjTEhXOUFsbnVKZVp2R0s5Mlp6R2toQzBENE5Ca0htY0hXKzdFbFZrMHZPN09aNmZubXBFYzBiUjEzMWc4TTc4K2dBYTBaQ3dyLzZpeDJpMTBKV0EyNkFWOGVaODhvT21FYnVsR0ZXaWdHcWhIMkNCS0prR2dhQzV1WW56YTczSFB5UEZQYjNyWXpZTlE4Q3FNWWRyY3NDWXFxRGZTUkkzV0dHUmFXa0E1d2FsbXd0NDNWZ0ZrUVZLallZdVYwangzR0U3OHdXa1EzMElsWGZZUFBwRGlIZ2RVa0lsazJ4aldzbTVTZUZtVmsvRTd4VG5BNFVEVnFacWpZZXlheEdaMUd2dXlXMVJGdTJkdk9xSWQ0ZFpiTUV2QjFnR2txWUtyL2sxemIyTWJkZHl2d05VTWMyeXNReVJ1VzRpQll6UHBsMWQ4ZnJlSWZYRURGY05QYW5DMERrMVZzZWUxakdua0xqRWJFTU1DNDZMRFg4RkFveU9Fb1FKejVnWnV1R0N5cjgxRG5hMlcrN0Z6Zmp5Uy9EWlVubk12ODYrSE1hckc4MU96Ym9EL25uOXRRWmtsbGNVNWVpcytTRGJZRURYYjNRRjFuYlMydmJPdW9zdURQVGRoSVJGQld0SndNSmdGKzlJZ0lTVWg5WE53aWNjOUhHNk9MUFl0NUtNM1RmSHd2SkhneHdtM01EUzlRYjBZeWxYcWtrZ3BGM2d3MUE1ekhBaXVEY0Q1WVVFanJnUzB6N2U5bERWRmkxMUxiVUhZSnp3Q3BXbUVPMERKejJtZjRwd0owVi9nYmVpUk5YQmlDc3JDSWJuSnprVzBkUFk5blJKNXNHUmlDa2h2dHc5eVE2RHVobjhmbUNxZ0hoK3Z2RUhtSW56cGY2REdnVm9yWHlrSE5aTzdJSzBZbUFPWGhMWDV6YnVPTkYwdUh4cC93OGxPa3FpVUV4TkxTa1NYOWVnaDdzODAzbUorNElla3Y4bEpmTUZtcjZLRXFhWjBXYkp6dzdTZU1RdWJSZDFQa3JzKzQvRWU4WjJGdjVvaDlnczEvdE1ocVloMmNYeXVJcDRPWGt3WkZsR0lCaTFoeHV1aFBER0lMOUV0OXZpK0gwbDU4N0ZmVmVkZ1dHK0ZKREZMM1ZPZ25wZWs3WlkrSXVPY2w2dXBwRTZDTHA4MzdsQXh4c1hJOTdENTFsRWhrdjk2YmpBZno0aG9HM0haTFpUNnRjdUNDRVZRL2kzdElqYk1aQ3RIL0NyK0syUytETTlzMWFmS0dsNmFzZ3JzQzZlQVRiMGVuakJYeGVuZWpsbjV1a3h6VDRrOWVlVUhwVDRmd3hhWXRoRS85OHhuMDZCRXZKLzJpRGUveHRiRG85aU1iZzBzei9SSHVRSUd2d2ErblVFUk9yZWpCSmU5V0NIOWZTNm4vYUdWZTIyRjROU2tyZ0xYM1FsYURhcHA2Y3o1TWhJTGs2ZE9EOVdoZHZ2SXpkeWRBY3BHbjE0VUFvdTFBWHlzNXU4TUIvYlYycnFVck54cHVuRXlWNS9Yd3MwYWluYmJjdWE0UGVac25yVk11UDZ5Q0V2TGIyRk9tQ205NnNzUkhnSjZzanVaZG1kdndaMWpBenNiczFCYkFJaSszdld2OVFDdllyVkQ1QWlua1dtUzNpS2xLUVhsQWV0eXdRRm9QUjM2dWdmTXJSZ1dpVnFaNUNObjZsdzB6WFlpUVZCS3NIQjJTWjNJa2YyYjRIOUFGYjdyanBaSStJR0YxMVdGU2MxOUw2dHFEK3dEenpFWERBK0Nma3dLMlVUWUtBZkpaSW9GMGtaQWdvb2ZjMGZqS3loS0NaNTNrZVpxV3kyMUtzNHVvaTBEQlhPREpjNFlkVzl4UGVQQzNCaGVHaGE5L1hXYnk1aVVkaWNTdVZPY0RHdDY5RVBUYytMSFZWcldzRGN6T0ZwalY4TGU5YzlqVTk4bzBCZzZRSW1paStVdzZ4aW9IYXV6MUJmajJRL1A2SFJZTmxZa05MK2F3cGdzd3Y3SjV2MEgvcnY2dUtETDNhTnhVWU1QUFFMSERRaTR6cWs1b0Q0aDg3cHZUV2VGc2dPV2h4bjhxTFVucDg0emUzKzdocDRKWFFsOGtMSzYvQysvaGx2Wk1LV0t0dkl1V3RHTTU0dmxvRXY1TFpLN1JaS1Nod2VyL3ZxQXBxNUUrenkvelRUclc2TDJtdWE3d3FzeVgxUENpNXpEQzZ0TVpCOVFpY2VvYmN3QzZmMzZKT2ExZGk0ZlpGYjhQNmlPRlBXMzM5cUU4bTZxK2RqaVphYXdyWWZZSFM0NmJYL0ZqTGY3SGZaSllySEZiL0ovallVY0xwZnlsWFBoS3lMdUVIMGJPaXVQSzdVT3Y2cDRMNHZGeVpYSExma1c0QzBhdEpjWmJKdlluTGFaTitLS0h5bzE4TWQrZk9HTXJkeE4xdUJNR3VYZ0J4b092bjF3TVUzdElrRFRCUk8zN0sxS0c3bU42VDJOREk1REtUeGNmTGRrNFBPM0d4NHlkcWRjUTFLREZoRis5MFVINzRTYS9IcjhSY1h2cjg2YlR6dGxmN0dhK2YySTN5cFdNSmhhdnEzS1dOWVNGamkyZ1QyU3VXOXZ2d3NOZE5zdmlWczdIajVlS1B0bEc2RUtHMThqbGxXaFhhM0lsZDlxcHd4bzl5N01vcE0wVjFydmxYQUk4ZENLM3d5ZGQ2dDhCaE1pVTdzcFR3NVJZUzF6MmU2QVNLOXJzK1BEMmRIcDViL3Bpdm5nNXFvL014TFpZc09lNElwNkhiM3VtNWRJdkxad0lhSUxadHU0aWMxeThJNlBNSmgzbVMwL1hjMkJ4UnY0TXhBY1ZJNDc4Snp5cytqcTlMNE1LcFM0enZKS3E5R2ZkaS9PUlM5T1hQdHFFdENKSXl6dEY5bG00Snc1aEJMcUhRWUM5NStKZUhHVHRtMVc3eWtGY2poQ0Vuc2pKWTgybm1ONitaYW9vWDRTbkcxMlQ4VWFuV1lpbjVHT0ZOMGJlTnB5NVRLc1F5QWM1UWs4QUlsWUJzR2lMdG16SDc3NWxXTTBBeTZTYXRwV2FONHd5ZlhKeU1ONHM5RDgxc3NwaXIvV3Q5QTZnSU55ay9KaVJTU2FKaEd0SmdOQVdBSGo1d0ZiVUNoT0RiV3pSUlJzNzRmamhCbE9yMDdwNVpjbVBRdXVFVE1GTjJSN3U5azVENk1OQXJ6dFV4QVo2WEpKQkh1RjZWUmplZjc3ZS8zaUZLU2RSRUN5MEs4NjZSbFVERTU5ZnFZUDREdmxCMFFMRE80UEpGYnBJQXFKVjhjMXpPUDAyYkExQ1JYTm9vQTVyeFlFblhpYVZTMVhpbGVydmtORGQzTXd0SU5pck5JNG9WaG5nTWVqZ0JOeTVaUkQrVXRrN3FQSlk0by94YnFWYnVucFNVRmhZM090Q0d4TjF5T3NNRUF2U3lSTEJJVHYxNW5CcDZPNEc4NThoTVNHbTFSVWhWcXpDbmNDTE9ZQ3U3d2EzbUpvWS91R25rWTdzYmdPbktDUS9vTEs5SXA4dCtzclV1VitLblFORTl1VTRFRTY1Nyt3NlR6ak9pSUthQ0FDZ2VOaHllWncxTWlLS2NBTnlvWUpUVUEvSS94dmU0cmU0ME5CeVBCSXhQbFZhaHFWMStXK3VEeWNzQW9WSWw0WXRUQmlDSWxYQlhNOWdpZHVhMjNXN2dFekNCQkRRVkxaWndwWHE0UXJQbHpMbHhUMFV5ZE9hZEI1QzRYU2JpNWVsL3pLY3VYcGEyTlNFeHlId2I0bnArSmxqL0JaSkZNMjVadzQ0U0V1RFlRMDFReitEL1M0L1dyZWVxNlBLQ0l6dEZ2K3BUa09sa3N1aS9ad2RpaUJadzlOS3lLSWZpZjJzM21iVkE0QjJ2c2NmQVE0eUZnNXdmdjUvajhyc2hRaUJSNTd1bGttbm5KOTN2VzE4bmtTMGpyZlhqWmtJVGlFT2lzVVhYU3YwWVRHbzgyL1JpbU9YZFNiOEJTRzhDcndZVTlLZGhrQVdkMGZkTDlzN2pZY3FmTk5CZFBORmREZ1JzeS9RMEFXQXhLWmNtWGhDbGU3b2oxSU5yZjd6THcyNldIVnAxdWxLeGRuOWFWMURHd2JOMzlQZnZQRUtVM1lmZlROMFNSWkJ1V0x0eUJPNVpPUTVmQzdWUWVCUklqUTJac0N5U2FabDBaUVlVeExwZTZac1d2QVdqd2tKempaRFhBOW5XejZBcFFnS0twL2ZobE80cVE1R0J3K2llRUdQMkVEZWtVVURTdTA5WXhjZ0drOWZ1K0RaZUluaVE1VWt6L0xLaFhrL3VCd0ZHZVhMOXVpd2tNVzJsRSt5Mk1kYzFNK1RhdVpRblpjNlpQclhubHZmUTBxVkZpTzNSeUxJTzRZYTJsYWNOUW1BNmlDTXJTV2F4aWdsOEtVZ0x6eWJoTXRtR2x3RUtKM2pNV3NtNldkejBVR0tPOHFLZjFtNEsrLzRQQ3VoQWxSbWJ2Y0xlN2dTamZhT3FZb1FzQUpHVURFUWl3Z1Q3MWUrZ2pmS3ROcElOM1BIcU1BazM4SGszS21yY3FxNS9rL29JeG41Y0k2aWh4RnEzYkZBZEpCMlM0VzJiU2RTeXNCZHhkUXg4VlFVM0xvVzBTbTErdEdKNWtjRU5yTlJzQVVxelUrRWxLNFJxRllpWGhTQnhhUVE4WW9VcmpyalhhNjA0Zy9mdVNuT0NHT2N0MTJqZlhObERIOFdnV3NjZWd0WW5ZS2tTNkxjcWkyWG1MQzNDZ29OWitxV3dkdDFpRGJYdW1zMzM0V3JyZGJBeVF1VDhEQkx1YStCSjhqd0ovZ2NFZXBsRytpNUNrNitGTFlTSGdDYkMveFNHcWtYRTlwQ1VkUGJGUjM1aExDbkNDTlJMYWZBRTl5a2VNN1pOYmo2bmQ0QVNXOU5XUFV2ZlVYelJCbEdObDFESVV0dlJKelIzTzFtZjJqVkJRNmpvNXpBL25OdEtpZklVUS9SOU14Q1JROHpQR1lFb2h4VWZWejh1RHBQcGxLZFpVVERldWJDS21vcGtVbU5saUNOUFpEWisvdjVGR003V2JTMlU5VkJCUkY5WUtHS0sxay8xa2ZZV2ZJTlJpSkZoYlNxOWxMSzhqdDZ3QnRZUi95OVhTWDB3bW5GRytvRzkwNFFmSnBWaEFCaGVWSmhaQzdHbTJza1E3WlJ1U1doajhkM2tuRFZpei9GajRPbzRpM25ZaXlFWTRnUG1ZMmlRN2JPVUlnby9HNERITG51K0NRYTBwdjNJYUxIUlYrZ3FyUjJMbG9XakxUbTBsV0d0Vy92NFk5NndQR0s1V1dEMFpxalJwMDZtbElranVtdmhoZkxIanZPaUl0c2VneXVWcjF4RHFYait6L0JId2hCUnB3RGF4YkkyQTdCdlAxQnIwQWNTbXJhZ1VLWlpPY0c3T2wxUDBFM201a1R3c2ZGSTRac3VVZTFZczUxblJ0cVNPVU9lNFhvU2hmM2hjS25FR3ZDdm14UFRQdGxkNGRaK2RDWDU4MlRoQUJnekpTWEtIYndQTStCU053Y0pTUjREMlJldFl6M2d6Vkh3M0dub3E4MUo1b2g2eUgvaVF5T0ZHQ1lqWHIwR1ZoVytoYmdiNzVwWUI5Wm1XZlY0Z3R2SlFFeEhoMWVuY2t5VUhPUzY5L0lxSlExTmlOKzA4K3BxdGVmdzlNWDd3K05RZ2pMMTBVODhHYVh0OU1uUGIvZWdSckVnY1JRckxFaWxjdldTN09FNjhVSlRFTXBjSXBDaFZTT290eFhDc09CMktaTDVOQ0xRZmk1NUpVYmxPNUZ5dlZuS0NMRklvL0syZkZPcnJMWExodlJkdG5oSHZhUExIRndYNnJ2MWZSd3RuZmxneEZlSVpBMW9jdkU3bm15ckRldE5zVzZRMTFWdXd0Y2U2U3pyRmNGekhRaU1DQWVRUTFIUnFieHhiQmVGUmV5eTF5RmdRWDg3SmZOS0poNXROZWNNYm0zZ3kzSkhKeGlNcHFaZ0crelhVRHJIQ3Z4eDdhcmxOT2FJZzhzZGw3NFdnMnBRclFuZGg2Q3lqR3NZdFNPdGZOMkVJQWlnL2N1MWxSb2pZMGhtSVFiUHJJZ0xLQzN6UUw3ZjBrRWpmeWZwd1cxL0ZBU0t6UlVCVC9uNTloWEhlOWR0cFF4QWtTMzRScHhqUS80K09hVm1IUWZqdG9WcE12S1JoUHBBbk9hdVpZdy9LT2xWcHZtUWVMNHhhcWlSVy8rQjdIK0phQkFEYmwxbmpJRUlZQ0pCL05IdW9hWDR5cUlPek9taXBXWWJwNnNKWVA4bWg3VEFpdXdqTDFYbm9RN3piN1BZQUpTOUNaMm5COWMreTVZQk9xVE9JZjZkMjNoQUhYbmZnYkVwd3ZBdnBUcEdrTFFNUWl3Y1VEQ1R5a3hIZk5uZk1CYWdNL2Q2YTZJSWxGUE1Wc0t5U2NzaHVNOStXeFQxakx6b013a3JvUGFUL2U0TVdsY3dOcTJ1UE1tODFaM1BrNGdjN0h5UFluQURscmd0NVYvcllhY3ZMWmtVQmFSeE0xMnNpQXRVaGo2SHJJei9NR2s5TGZqaXFITzl1dEU3THQwS1J3NVl2T0FiRWVVOFBwdm1YK1hScml1TW9ZdXkrUlBHbEI4dmNLWDlxTUkyMGxqeHBiWW95aFdaeVJ3a3NUNGFGMWVwQkV2MWVDOWtabXB5d2pOdGZ1NWkzNVJIeEcxenhRWElkbS9ZV21EVmF3T2lCMzkrbk5LM2kxQllrMTVkUDRvOTk2eUlzL0N5QXlxYUVGTUErMjBOZWJwNit0Mk9xdU9KY2Fwc1gycU9yTFp2ME15RDhabytmTnBXTTR4c0k4UE9wUDJsVHZUV29CU2VXNytxbWZ1Y1dkeXZsY1NOYU4veVdjUkQwSUdaY2NFNlhuNVNnbTVmaFlzamlpQ2xTYXppQ1dtaXZFNlJRV3hWRzgvQ1R3MXhVc2duN0VTaExIUmRXcmVqdldVVjdUTEh5bFBSUFpSTCtMNVJRSTloTVpHVlVDelBneGRKOGRNK3VzWDQxRkdrNE5SeXkzTnR5dE1oL3V2MzAwQUM4WXNEZG9WaGkrM0tBb3lrS3VweW4yQTR4ejM5dExXS3NjQTRlMUN5QjVnQ2VsSVc2Zm9aak1LZWNJNjNVWk13YjBYc0h1UGlKWmpXeUU0aXQrWmpjVkMyR09ZQ1liVlc3akhXWFVES1ViVTZzazBsS2luckdDYUlyNzI0c2RpZ3dPeWVzS25DNG5hdnFHVllGWU9GM0RGVUhrQXg0UnlrVW50WnF1RS94VXh3L0ttT3ZhN3pHUnR1UG5mbnljcUFQaDdzSGVVTWVraS9zRUFGWTFZaGhMdnV2dHVkbjFSTW56b2NGK3RZSjJaZ2JCb2RGbHkzWi94L0xOaDR0WDlHYWpjZEVPQ05JTnRXTFNXODZoUzRIRE1Bb002ZjlvT0pzdVFEeXhDaVNXcEgxTG83UDlUSkExa21ncHpaRXU4YXhWQTBBVUdXZXp0ZUlFeUFKOFlvQVNFZ0N6TGszaEwvTXpnUDNnZXoxWHZhbWcvakU2aUoyZ1l0T3FxQkxuTlVDeTVYWU5BUHBpejkvd0xsUVZsNXFGU0NDenJydDZJNXFscExrTTZacFQ5WTBvUTRncThEalh3R0pwblk3VzV1bzVJV0tLcitxZ3ltUlg5UjcxNzN6RmUwM3ZERm4vRXJES3pTN1hYSWthWkxmS0d3bGwrNHlGUTFON01VV1BOYnZDYnZhbTZRM2wzemt6MVpQbEtWSGFGVTRHQ1RRaFhOYnBMY0V6T0Q3RnJJbFdzblcvaWJ1eW1qQnBaUU1mTEJsQis2YkRQVFphalJRdUY4cnBid21OM0Jaei9pekdHYy9BVHliSlFCZ2g2UkxUMm1jSGhqY1d1U3pkT2ZieXl2UVJweXdPZzRPMENBdXh4ckRMR0VCZTJ4bHA1WkVBaWxQckV5aUdzZUNORzMwTERlRGdEbkNJNnRIeUVzdkFLMkFtenRhaVlzWTJBUk54Ri9acndYS3FiNVB0bitlSFZycFdLN3JRU1lKc1pseU00SjluTjR4VTVjNy9NcGxUcFpud0VrRWVCTGpNMXZQNS9TTGczOFE5S2lJd3V5dnQ2dzBTcm9IdEJRTnVyS3V0VkFmeUZVZForNU1XSi9YQUIvMEZsZEkyZzRrZ0V2VXRsVVQzOFVReGVxdHVRaHJuNDF6RnBQeDF6bkx4RDNOMHExbElGVkdzbnkveUNFR2M3dWVIZFhZSGZxcU1GQ2dscHlJREFEOURpbHVzeHZpNS9aTnM3L1VnZkV4SEZveHJKeVl3NHZGRVFGdXc0UGJkTXpVWkUxeTZKT2RyZHlwWXdYOXE3QldGb1RWYWhWSno4UTRIN2JJWldTS3JYZERQdzhxVW01NklZNDNUcm53WnU5VmFLWmptTGR4STNmZDBvaHFIY2pid1VxajdEMkQ3ckUzVmUyaU80aWVta3pOQ0l2Tlo5RUdVek9mQk42akowa1J3OXdwdTlDcmVQcDN6a3NacDVEYmR1dUlkRGVUei93cTVXY2R0WDR4VWQ5YmxGUWpyT2c0b1JiRVFxakhBdzJkZ1VXUkgyaTREQXVpU2V1aHAzNE1HVEtoTVRYT0tFSHRFZ3IrQ1NydEJqb3krcUJpZXZzTkxaaVdtQWVVUmEwayt0eVN0NndnTUluV1hiZG4zRXZURDhzaEhZUTYyeWYyLzljdG5qc29UNzA5dVl0QnJOYjFBdVFQZ2JlOTlRK2JrOEI5MWErS09LZFZZQ2p2a21MYzUrYkdmY3FlbDgwNWIvUFlLdWpyTzVYcmN3Vnc3MzZiYXNTcWNzTy9nOGNuNVNBcEs0M0oyeFVtclF4Mlg2L2NMbkdhZUdTMEdDSzJ6V3hhQUJWd0N4Q2xQWVVsVmZZMlQzTHd4c1lSejJscm9wUThDN045SEE5dVZGMFJtbjRIbjZ0RjFBMFAwS0U1a2sxcHpPUUtCcXkrb2pnVGkyQ3cyWUFVaTgva2ZnbGpoeDMyT1gzbFN6TlRTcGJDWXo0VzR3Q1dndThXZll0ZkFudTdvR2I3dW1jYnlnbkJQZ1Z4cCtINU9zQkViYnFuMkZQdTlidnljZXNFWEwzNUZZY0N1a3VoU1plTEpLRFQvaHVCanJveStjRjZQeitSTzU4dUFJV25hUE5YRlhvcm55Ri9LdHFzVkJ3REJibjhIR2tIcVpaZHlsUTNCS1AyUzYxK1VPSUNpclhKNFpxcHZqbnJxbHIvQTN2UGlUUm1OMjRLZG1vd0tyYTZBeCtTUVRyUGNKMzZkeTdDQkR2RmY5b3hhWm91Z0RaRUFWUksrdjFYYjdUbUhtZlFtMVkwZnl3WlBROEoxSEVLYjhIVlZCelZ4dXllMXdzRTErSkxBOGxQekQxNjM1VWdkZGZkMlN6RUhmNWs1VUdIS2JaYjRlS3ZlME5WQVZtV25VR0FzMUZmVFZvOWlOZFMzOUcxd0NHUlN3VjdFY0dQdzJuYlpObGtObWRzdlhqVDlLWTNxeTFJSHViK0JpRysyNkhqdytON2s0RVJGTzhEZEx4TXQ5bG0yTnpNSTVIc1RkYk1zdW1sNDVES1pMOTA1elBQWG1WSWRKUW9UNEcwbUFmanAraGo4MFRqQmlZVUpoSWhWMzVmbFl1azZPdXFNMUpOYmVadkt6dklka2RQblFaYkFRcG9WSkg2VkZOdXBLbFZnWkFXaTExU2R6bXEvVFBVV1dWYlQvYUxNZ0dPRk9KSXI1SWVNb0VjZ0hnTXJOcWVkL01BaU0yZEJTOEFLZndubjNlR2xFaWN5ZnQxL0JmbE9FZEoxQkh6VVJidmZWZVRrWk14Zkh5cW1NT0lqNnBBMVBvOS9TdkdLWnlHMmQvbGRsN0hRZ2xOQTZVd0J0MXFBNGVqL0dTZnZhUnU3RTlYMFllendCSDF5VU5sR3BsWlQzbEZycmZhbDIzdEtOd0NXUmNxYzR2S2RCZTNtbTh4czU2Y2gzbWNWT1pBKzgwZndoaWhmTlcxOGJndGt6cmdVellIRzRBeDlxbmxka1I1NWNSUDN6SkxTTXdWdFNOaTlocUxpelRnZ0dFdGVMY29ucnpmeE4xQk41V0dqNWw3TVFpVFZNdFR6YmFYbkJ6WExTV0RBUnQrSjJ0cEVneGpNZ0lpdXJlSHRZc2RWN2h2bDFqcjZNb1NueVNpRWNSUmd4aStkTHZtYk5LV3BKcnJzZ2N2R2pnS2RPZVptRTJGb3YvRWZuSWVPTTYzM2ZTUUMyYktBemU3THJmSXo0M3E2YTZFTlR5NFNPYWdqbzFGTlVkaHJpMDRtTlhDME10NHlReno2NHNHZFJxSit6SnUrRVUvdmpZWVJlWmtLVHI3bDVqc2ZMR21UNlVWbHpKRTQ0STMrZndpeXBXMG45bmxIc013UTMyeTQvZnFNT1ZNSDlra0xMZVBmSEJQVHFTaGdrczQ5WUtDVnZGSUxaYjlKREZYZVlXTjRIL0ZlOUtuZnNtSlZlNmRMbkdYb3k1VVMzaVhtTzhVeTFlSjhLT29BUGVEejR0b1M3dmFKNmdadWhsREpKRXoyZ2pBQzBoYnpjMHArMlVtMmpFTStrbklSV1pqK01WSlp6R2lxMmxySDRVeUhOaE1oaDd5eFlUYi9jeUd0eVVZQTIxTVF0OU1oMkNwOVVsaXlDMlE5ZlZLUzhZZkthYTQxdHc3WUJXaEhqNXNMT0lRdndNNnZXbVh3ZVNQMUNwMno4b2M3MXc4MTFncHNmMUJzSkl6UnlCR2NmYmVoRVkrTGVNeWFZSzR6Q2pPUG8wcktyM0ljTXkyaG1ldENIUnNGUVNuSTY3R3UrK3hZZDFQYllOT0hRM1FOaWRrK01MQlBGdzFTSW02RnA1di84WURCMEpwMG1UVTNmWTh3Zk9QZnZGTWN3S3Q2b3dBbGFLaGVXQ2xwN2VZRDFYY09wYTJXbE52R0VWL3hxRXMrKzZLOFlYSWdZeUQwb0IwUUxEalEzVkZqWE0rUFRxQ0RWL3ZtanJmbWtsYjgxRk83U1BKSHRWd3UrbWt2Q04vdFdiOU5lL1h1QklUN2VIck9PbjNzK1BnVXFWRDRteWRZb3MzcDdZK2xSSlAzdXJxTHR5WENXUWd0QUloZDBtQ3ZRczdaeHVMK1c3MFFyenZZdGlMWVN4T0JoVWlwNnRWSWJuaE9rUlZKYmlGTFU3UkhJMkExTFFETUhlRkpXbmJhcTdPU2ZPNm1EcTBHZGpnZ1ZUTTNlb0ZGM2sxOUcvWURESHFXc2EyWW5IZzNIdWJ4aDlGM0tJSXBTd0JQalB1NG9mR0lTNUxCSy8yakJjTlA3UHd5YlJucUJZUm9vaHFRUDNMY1JXTkc1bUZaWFVKNDY2djNxOUNzbnlHRlRDQ2hKL0dnWDBvSmZtM1RCUzFWbHhCU0VJTE5uSXZVUCtOVjNPZldFNEZCTkhsUG1sS0FMaU5aU0ZJVndEeUxUeUtjczY1QTVSOGQrQ3kvVkNjaTNIR0xzQndXSmVZM0ppMk1GRWNNWTdUdGcxc3UzRDRBTUhsaU40L0YyOTJwdjVpTEhHRDFXTmFxM0hEdmNjMFU2RjVGSFJockUwb2RmSCtZck9xWFM1RTFvWkFNVmQycmx1RFBIUERZcjNEL1FnMWNnRE4xRUFqb01nUmdob0h4aTdNc0ZueUxUUmpQQjZNVUZWc1liWTJuK0tLbXBBMGJDRW5Fakcwc3g1NnNNdFFPeWtlN0Y2VHV1c21HVFo5VjFncytXY3lVSmxUcGdUbkg5ZktFZnd6TEJPVUZTV3RsT0llTXZxVmJIMTA0MXdPRVhCbzdRUS8xclVNbWFHL1poMi8zdVBiNE9xQWIxcEpNWXp0bXFseWpqakt2Q0Rxd2gvYVYrNUdnMDVXQmJNZzdHbTJFbnFueTJHUjk4blJCbjY4eTFqQU9NUjY0bkQxd2h1TFJvK3BWRXV6TXI0K21TS0RjOVpyNnpjN1ZYU2tiWHhSaXEzeU04ZVplYUFsSXh6cktqd2toWURjY2VCY3dCbUpVUWVuK1h2VlFnSjRJVTd6a3FJN1luRDBMcFk5UWFZcHQwdHlhVW9pUzlkQzRSVmRodnN4ckEzN3dEenB2dDlwdkFXTUpjTmh6OTF5anZvSy9leVJiRzJlSkdBR3FFbTF3bnd6Sk9TMC9OZ2pyeDQ0M0c0Z3NUZnNzYnpUSzlObGJEYzVFdUpPNnBrbzVHK2wvbGZOTjdSTnYyc01qYXRtNE92UjlRT3QrYzUvTTNwQk16L2NoRGJVbVpEb2VkN01hRytrMUhIWkhjNW1yWXN4THdscmtuSW05MlBLMWk5dm56SDg1UnBCVDRmVjJic3FrM0JCSHlaYWtkNFZzTU44aktkL1dhc0RNLzVSb1c0dEhieElzY0RSblh4M0pwWlZTb1h2MXVzOFJ4RW5EdjU0VHpZTzNrR2hRQXBqTXd1WDlzdU9maFlMQk05V2huUUlUUlNnQVV6OFl4aG00d0FKNk5kNTFTSzhGaVJpenFKcFdjYWZmSHF4Uk5QcmEvY1d6QWgrTmVLVEs5eWdnSDQySkdzcmNNTnNiSFZFVmlONlFGQURnL3ZhaHJ3bWtTV1htQ3NKZnh6QldaQXF1a3FmYUtUN25hYkVMem51TmRsNGpUbEY4SFp6cWlQUUl0bm1UOHB5UjVYOWZ1Q3dpUDZ2L0VHVy80SG5sVFJWcUVJU3k5RGMvYzUrZnJQUVRjQVNJSUJaTTNEWFFGd2JWS0cyaEN4Z3RuOFNKSC9lTXZnY0wrNENxSlJrMnJzWlVqTUFZTTJZM2xLZGpZSkdDRVluaEFtY3EyTmN5enFXc05BMUxlTFNZNE50VFg1R09GOVJFL29Pd3FSN3dZT2VmMkdDYUVUSWFzRVpMMStKd3ZmaUh5VUJTQlNlYUZ4ZlN5Y29ZVExJVTRaVlduNGJENFU2ckwxTnpDL2dYZ0VuR3hhMUg4bjhZNld2bGg3TWpGMzd4U1RjNHNZTE02NURJUlA0OUFBMWVVVUhYa1NNRG0yQTdjN04wUW56eEtNdjY3bk1WMU9YNm1kN29UOXZKL0xHUUpNd3pXV2ZHUzFHYXhHZkNhQzU1TTE1S0ZGZ2lNcFlEOGEwRjZjbzhIdytnQU1wMjVYWTRVUG5xRUdrTXdHSDluQlBxT3NnaUFoOVkzTkR0ZE9rWkFVN29CTFM2MzBBdy9vd0NBN1lneGNIYTI1VWdwYjE3YkZYWC8zbkR0S2ExeGl1Vml4OXdTT1Fxd0N0bnVGNTJ2bEdoNTNURENDMm5yRE1pcFBuSmZaUjdKdG93WGZPZUFRc3llUFNNWXdNWkcrd0YxWWtycVdkYnV3b0lKaFlzWVl2QVRpWFc3dktBb0Q0N2lKb1NIQzFIRzVSa3Ewc2NuYVgvMHdtY3Zsbm9kNWRlZHJxSGIwT3RYV2NaS1drWkZqS2YzYWh5d3gyeUhwQU01L3BsdEE1MDQ4bFNEd0JwOXFPZndKL3pCbU94K0RLeHowcmJhd1dtZEZsWW95WkhscXhBTC9LS3NFbmFrWEw4b0dIdEp1RTJGdGM2REFLNlhTV1FuVmZxQXE5QmRsRnFGSFowcFV5TkdUaVY0dEVNUkdwZ2I1c1VGcjNDcUdkblJLNGdYbGxvQWhUZjhBendlYXFBT3M1SzhPOE9TdE9ISzhqbE5ubWIxa2VGU2x0Sk5ONHYzUGNYVTlpS3ZyVzlKV3JBMGhiK1hLY04wS05aQ0ZDdU8wRkE2bXdzRVE2M1NkMTZzSGpRVGRYNVdzSHJiU0FxdVFiUXIzVnFHVm8wbXE4Q29EandHbUs3QjI5OWhMZFdkWGs2N0FLME51ZVNJbnM1cERoY3YyQmFaOTdDNXVDOURUU0NQdVNYTXh1c3lQTTJyTms2bHNIcEJVZlEyYVM2d1k3MDFGcmtsWXN2RnpoSlZVTmpwVHhwM0REeTRzS1R3dUFqOTlXWFB5MUJtOFp6SHpXdk5aNi9LMHVMY2VUcWZZMkVUbmM0YndONWRCWTZRMGxlUUsrT0lVczZLcExRVzJlU0NhelV5S1NnVmZWMHpBdXNNc2tHM3IvRGYralVqN0E4Qk1pb2xNWTM4SzQ1T1N5RnIxVG1QV3ltd1pseFhpcm1HV1ZjWlZzb2JRZGVPSzVKZkRXNGdzQklNYXJvV0RJdWhreGlWenRNUFdnSlljdTdxcHR4REhWZjlod1VNaHloTzZJRWVML2paS2NMdzgvMzZka1hHdmN4dkxIRUFGWmJkZHRlZHJHZ3pBd3l5d3dEWWtyUVFhUE13a0t6SmtFWkYvNkwxcnBnOVNmd01OUDRCUENKMmxoVjBLM2YyRHFJT0k1NG1LcTVxSXNWRlRpMVRTUHV3ZE1PUzVIWHEvelIrYkVEbXIveWk1MGJnNmFFSW5TQTd3MncxNG5jTVlXV2dXZENucjNydUdWdmRpTDdSelRHT3lvOUVpWHl6V1R1RERzSVpVWWFmSCttV0oxMWp1T3pQTXUvNERFN2dnS3pHbmNFN3lFemhGTVllS2JTeDBjU05VSUhZN09aLzhtTGEvTzFzaWtSd2RRUWRpcHptNHlxbTRJTSs2MkdYNmx1aysxK1JidVdFditTMTdMN2paU2loVkdRdm5QZUVnQmVyazR6N2hzUGZUc1Eya2czUCtzaXEzZFFpR1ZuNng1TE1SaDBoRXRhbVdhZFRLcDk2ckFid1dKemhaYjlvSkFWQ09FQTdHejhjdjA4SkRkU01TUnJNY0hjUDdzcjJYUDdzWnQvczl3VjVRQlJDQlc1dUlFTVpJU2xIYTRyaW51L3QvTVR4V1ZzWHVhVUd0M3BMTTl6RG82V2dXTEVuY0tGOGk5bWM0YWpnWlZPNmpoaExzKzlOS0N5VXZvNzBhSWd1MU9IclVQMFpTRU5oMlZrbkZ3SFRmTWg1MTRTdC9mMWxjRTUyNDFLNGYvM3MrTVFBbkpRNks2cGV2NVJ6dklOalRsUnpXSmFyZGdhSVdXTUY0MUg1NEdkZ1p4bGpBT1hkVkt0VGxkVjd2MnFoaUkxem9uNTVWOUMwYk1EbVFvcy83VW9odU0yUUlQR21FT0tIcnd2cnV4UU9Ua3p2bE92VFVGNkFVVkRnUTlYSTRiWUgvS284ZUdDcndEam96WkhVZm1Kemsvc3NTUnNRQkdXK3Ivd1E0b0FubVloQms0QTVKUHNEL1Y1ZDFmTzdiWWpHajg2amtJQzR3RG9vOEl4OC9MTjNzcXAxbE9oYzNQbUs3Z0xoNkNrcjdDTGw3ekFYcXJxZktOTE9tbTBNTWc1bkQ0eHh4K0l6bmdvUjRoaFNSTXdRNHVsRE1TY2ROUFM5OEd1R2pWcC9NYnVNU09NRmJESUxmMHozZFdLbVpUV015d3dxM1ZkUmR5Y2IyVGovbW5UalVVbU5ZQ0VmK2E1czAreXc1Nmc0MzVvaU1EZ1pnTUdmNjRVYTAvUTgwdE9QVUFKSWdlNmd0aHV1UW9DMitSamdiWENJMGFUYS8zekFOeTVkdE5HNHBXTTErT2Q0cGNHZE92cElVVmRsekdNVEhGcmVNblVXaXdRcHN0MEhYS09TTUNQNGlsZDkyaHY0L3IxNk9HR095c2ZXN2dPSzFwVTNuNUI5TnMxL0trYkM3eENIU0MzL2ZsbnkyZmZSVThQa0tleEQ0Q2I3WnAyQTBuMVphbDRYRE1kbTRmMU92RGVzUFBTdmZzUXZpUkVTNWdWeVVhaVV3Z01lUzVWWDVkRFBPSmhvQ3VXMFU5ZmJlaTRLK2ZIUnAwbWcwRjZKMW9XeVptdUtpY3FLWjF0MlZFYzBDbDhYSmVkUnZ3ck9RVWt5ZVkrdSsxNWpqeklSQmR4cmozM3hoSWZnd2pVL3BSZVB2VWViNXBQT0dadXF4REp4dVMxTjc1emJOcnFyL2FVK05lTlZOUU5DZGVxRVJXNjA3Kzl4cVpIMnB1a25YZ3dxNm1UUHJoaTBSMmNTVHJOZjk4c2xsLzE2bTRhUUlSMzJDREFuMDhlSUUyVG1BbHNTSDVhTXRFM0FETEN4YlloTHVSakVuT3V2eXJ0ZWxjeEZGUjdINFlHYTQxZEQrSTJrTXQvWG4xb2xvWjhpL01ZRitqWXNDVlRpb0tudHNYdUhrWHlNOGszWmVrckxZNlVmS0RaUmQvQzV6bEppczNESVplb09xaXBxNHI4cE13VFhDL3k5R3FiS0prOXlrTXptN21BQ09IRzVxWDVLMTdPMkRRRGVpMGMvK2RiUUNLTnVXbnRBRXZRb3oxYTdXNE1rR1Z1V0NhMXVrV2FpRS9pSDdwd3Z4QVlMTFVJQTlHQkxoM0pvRDNGUU5qN3JWOHlOSU9BbTZKaUpYNFloVUJZR2lsaXJzMWI0c0ZzOHAvYkRTR2ZjQXFDWDMyQmZFOFQxclpnRnVQaEdoRXVQVXN6dFFZOEFISG5ZQUp2RWNKMzFjcnhmUkRTNWFEVnJQQ2txUlJyc0lxMFVaNnlraVdlMmluekQ4TEZmVFhET1JHeiszOTJoUDJYRVRkajBPOEZ0NEcxY2hXaW5HWVdKMXQ5NUIvY24vYmhnZjAzL3dqVnptOGVVVFJxdnpDSmN4dVVPQ2crd0Z0VVA2emRRb0NVTDJOek1MYUxBMWt0d0lvTS9mVFlGOWd6NFc0eEpGWCtiaC8yWU9sWjhEU3lsMFVXRG9QU1JKY0d2bms2MjJ4VUE0QnJJK2hmdzhDS2MzU1h3ZUJYTkVXQVhtUHBqaWkvdGRmUlB1MVRSN2tDeHhEL2lrdk1CSFB0S3o3NGwrbFhDMHJqenZGLzBJKzdNaEQrdDZxWGhTVFZndUxmOWJUaG9WRjhaem5SY3RXRG9JU2hCdGlQOUFNdWFoWCtBVnhPU3hBRERXbGJFTFpxNi8vdzJabmx1SjltbVl0Y0RYOWorR2dVZEx5RURId29zSWQrWGVXT1dNWHVBY2pwYWRQY0xEWnhBMHpkUFIvVENXUmFBZk5TN1AvdlJCa3loaThtNEdwaVN3WHUxcC9aWmdhZXlJdjdpRUhyUzlyclZBeXdqYjVuWjhrQktJUUxVMzJmOHliS0gyQVlPcTNOOXQ0b3UyQVFCQ0dMTkl6NFc1Y1JiMkR6ODNQWnRPRjNWOEVWRkZrVE9hMXNJMnNLUnBXUm1GczV6cVhqZUJscnQrSEhka1FzZTJpUGgwa3JRcXpFcTJFVlV0dGl6V1cxbnNZc1RtcFVMWUpmdldDK0ZYaWsrWGsxVm5rbGREWmpoS1RRb203OXhESSt3SWZGaTJ5Uy9tblhJczQwRmhwTndmaE1uZWJoYXdsTXY0azM5QVAybHYyNVNEakpvNGE3QnJqaGQ2MERoUVlicGlhU2lkZ1RMZHBWNmZic21XOFd3ZzVXVFBYQUphUi8wc3NNRU8wbWlOcXZGVDd6K1MvaW44c1BtdlA1cjQ3dk9PMXV4UTBHSFZrSlByeSt0aUs1UFJoWk5XZmNyRyt5N3RaU1ZRRE9XOE9CTVJzaGZlUkEyQ08rT1Z2S3I0ZGZndkR6N013ekJkeVhMa3ZTUmlmUzhwN3dOUXp0L0dwSGlvT0VFUkpDd2w3ZGFNMXdSN2NucWxNbUFtZ1Rnb2dxSVVBL2xGNkg5b3lGODZMdW5LbUUxaUpMa0dISFRHWit3T05hN01LRWRJREJzeHRja0pEZFlIcTBNZ0JydkVCUVVHeTFvOHBEbWVuaXM4TWlzbTMxYXZzV2N0RUEvS1JiZXlUelMrREZORDl5aXloTno3VEMvcGNhOUFwY2lXSDVuaE8wQklmcVhzRGMzbXlmNEdCL0ZtSlorL0I1ZEZXVEFlcDFmVDRwNFBSMGhpRHF6c1lDNXV3REVLVmlZQ1lDUS9nQm51M2p0enpab29jWVpQNXZNOFArL1NGRHZRMG1ZVUptWU14N0xZbkhSNm1abHlTQlJZWW92Rmp2ZjhuMURiakUwbWI2T0UvdDQyOVVFYTNLK2lFcXp3TkV0SEhIMjVzOXpTaUcrSG5PVnpaK1R4VDR0VWxsRVlVZzIxbnhzeDZjbjlLdDkwTzVFc2p3WTVvUFBxODFxVFFyNVJvYUtlT0g4a1dETWwrT1A1azJtVU1RSGMydEtZVFFDNXEyZkRlT2hDUHhFdzdCMnY4cURlMnBNc3B4ajV3d3dQOUNIdER0QWdPMFhKUDV1clZoT09CNTJSeC9kSDZEajluRkdkZDNCWmFKaVQrVHgycDdpUzczNHlNMGxzekdWQ0FtNGxIdEthUHBKWWlyaGNoYnJmWXMyWU1sc1pxeDNPUFJEdmg0Q2JGQ29HeTN4QkRoRkQ4d2RKYkpCMGVYdzFnbVA0cm40L3Q0MUwwZzB2UG1pbTRUNmVEblJ5RWlTT1A5RE9pZGxDdDZtamFseS9uZC9vbXJaM1FKTDRubmw5WFB1VjlXK2Y3Nmw1dWNxcWNxbjc4U0J2U1JodkxqeW5yU3VidjJUa0VkdmdnTlpIeFdMM1BuOVhjYXFET0F0UVNYVmd0VURwRG1lTVRJOE9ERExVM2ZTOGtKbEMxZEFtUVVlWUJhcWZTM3RDWi9DRHZudXExeHc1aHkvU0t0L2FaKzd1NDlQcDFwVXhDMFYybkxaYXRGSm9wZk9BcXVWbnpqTWVSVlUvR21oQWZ1cGtvcWQvMW1SZ0F0WmZmNFo0Y2M2dFdjN0NwRndLSHFvdzJ2WDhJVjB0OC83THprUC9IYVB5UjlFRzNmMDU0anZZS3dwYWczTHM4dU9jNnZKYUpxd3pOSWI5bE9zT0lXZXN3aVJIMlpjUExnZWhUbk02OWRMZE5NVTlvQzRaZFExTW9oVnBncmZqSE1sSm1kN3pDSVc0a1JuSVVGclg2VE9vUnA1L2t2V2tWVytmRUNRckZrRENPUyszS1YzSjJOZzVlV0E1QmJ0TzA1ODhIZDNyQjFLcEdha2tKMDY5aXAwaTFERGFNc3MvM3lEdHE5ZHl5czIxWDRiTlZRQnBTMlZicjkxOHNDUWNZK2FjY3V6ZEYwSFhtc0FIcjc4TGxEUHpKNmQrcHdENEtueG5WUGsvMk9TSUx1RU5FcC9uaUt4L09DeUxLK0RwdDZrbDZPbjd0eGZPdm5Bc3duMS9RS0JZRzBWeVphWXBFaDVlUkJXUDdMbFRFaU5YTkdjcGo3RE56SGlaWlhRNWk4WGRQT1NWRnBTRG5tcGZJMVh6VmJPQklEMGV4YUtjRTJML0RPU3NwL3dOeTl3Sy9lZ3BZa0x3cGI2d2xPVWdqVmxGcUlDQWcreStjMnhnMmZsYzB4bngwRi93UTR0Z3ZLc1ZNalpJYk9qb3N3NmxkR1gvR3RKdmZodkhLWXMvS0g2RG4vQzFkSDB6Y3VOaFdtMHFJZERkU25JaUdBdjByZU5OTTREcUNmQmdyU2FZci9BbUlHZmo5dmhGOUM4WXpPdzhsM09ZNkRuVmVXRlRpZi9aQUFFQUdnQkFBQUU1QUVBQUR3RUlPUUlBQUFBQUFSOEJza0FBQWdBSkFnRUJDZ0lBQXdBWlZHaHBjeUJwY3lCdlppQkdhVzVuWlhJZ0tEZ3A8L0JEQj4KICAgIDwvQklSPgogICAgPEJJUj4KICAgICAgICA8VmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9WZXJzaW9uPgogICAgICAgIDxDQkVGRlZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvQ0JFRkZWZXJzaW9uPgogICAgICAgIDxCSVJJbmZvPgogICAgICAgICAgICA8SW50ZWdyaXR5PmZhbHNlPC9JbnRlZ3JpdHk-CiAgICAgICAgPC9CSVJJbmZvPgogICAgICAgIDxCREJJbmZvPgogICAgICAgICAgICA8Rm9ybWF0PgogICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5Nb3NpcDwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgPFR5cGU-NzwvVHlwZT4KICAgICAgICAgICAgPC9Gb3JtYXQ-CiAgICAgICAgICAgIDxDcmVhdGlvbkRhdGU-MjAyMC0wMS0wN1QwMjozODo1Mi42NzFaPC9DcmVhdGlvbkRhdGU-CiAgICAgICAgICAgIDxUeXBlPkZpbmdlcjwvVHlwZT4KICAgICAgICAgICAgPFN1YnR5cGU-TGVmdCBJbmRleEZpbmdlcjwvU3VidHlwZT4KICAgICAgICAgICAgPExldmVsPlJhdzwvTGV2ZWw-CiAgICAgICAgICAgIDxQdXJwb3NlPkVucm9sbDwvUHVycG9zZT4KICAgICAgICAgICAgPFF1YWxpdHk-CiAgICAgICAgICAgICAgICA8QWxnb3JpdGhtPgogICAgICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-SE1BQzwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgICAgIDxUeXBlPlNIQS0yNTY8L1R5cGU-CiAgICAgICAgICAgICAgICA8L0FsZ29yaXRobT4KICAgICAgICAgICAgICAgIDxTY29yZT42MDwvU2NvcmU-CiAgICAgICAgICAgIDwvUXVhbGl0eT4KICAgICAgICA8L0JEQkluZm8-CiAgICAgICAgPEJEQj5Sa2xTQURBeU1BQUFBWUhiQUFFQkFRQUJnY3NINWd3Y0RCNFBBOUFBQUFBQUFBRTVBRUFBRHdJQVFBSUFRQUlIQUFFQjlBSDBBZlFCOUFnRkhRRVlBY0FBQVlGYUFBQUFER3BRSUNBTkNvY0tBQUFBRkdaMGVYQnFjRElnQUFBQUFHcHdNaUFBQUFCSGFuQXlhQUFBQUJacGFHUnlBQUFCd0FBQUFSZ0FBUWNIQUFBQUFBQVBZMjlzY2dFQUFBQUFBQkVBQUFBYWNtVnpJQUFBQUJKeVpYTmptY29BQVpuS0FBRUFBQUFBQUFCcWNESmovMC8vVVFBcEFBQUFBQUVZQUFBQndBQUFBQUFBQUFBQUFBQUJHQUFBQWNBQUFBQUFBQUFBQUFBQkJ3RUIvMlFBSXdBQlEzSmxZWFJ2Y2pvZ1NtRnpVR1Z5SUZabGNuTnBiMjRnTVM0NU1EQXVNZjlrQUJBQUFVVnVZMGxFT2tkU1FsUXdNZjlTQUF3QUFnQUpBQVVFQkFBQi8xd0FFMEJBU0VoUVNFaFFTRWhRU0VoUVNFaFEvNUFBQ2dBQUFBR0FZZ0FCLzVQZkV2QVVPNjhJZE5rMmtDMVduUW5pOS9Ga09aQ1NzTVJBK011MUxYWHAwNVhFSysvaUtaSkpHTXZ4L0k5SktOUkZLWURVQU0zWDdiV2x0a0VuZ09JQXVaRTVhcGs2cnIzTGlPdDhtRTZtR2d0Y1hDcUE4U0FqNkg3UVRLK3ZGWW5yemY4bHJKREh1UG44Q0FBTE5xSTFBSm5pRzRMVWlqQnI2THRubHNOVXBPdCtPWGRIREx3ZWJnLy9BOFpzWndCRGtwcVRJRDI1M3lSaExCeG1KcStvcFNicGxCVFFnTTFiYThia2dOVXBwOTFQcWE4NCs1Q3ZBNmVWbTdtRzlhaDkvYTBoNThhRnVVQzdURThFV3FMQ3V0RXZQY2NkaHFXZjhwNG5xRUNsREx2eFR6ZnlCemRyWC9TZFdVbmUzVG1IeGsxSDdNb1l6ZHhROVpOV2VJR1QzeGNDemhrUzVlZmhjNkdYZmFrd2orUGhZUmUyNWs5b2haenhnTXllbzhVSVdTbDNLcE00czFpM3JxYTZzRXBJS2RQa1ZGTythSWR5eUlzY1JINVM2ZDMxby9nYm5HeXV1MTNhcWhOTmdRTDhabDhacDh5Q0pDeHJsM293TnBwYTdEQllJV2FTcXVmZ0x0WjFJUkFlSmp0M0V2aXNrNnNlL0VUTGpkSlErTmgrZ1ZIRVZRSUZVYVpHSW9oYjZkUzVUekVaQnIzMFgyaElRSW1wcTYwd1JEcXBPWFgvTU9FT1NjNHYzdGJUWXJIbWIwYWpGNlQ3am9jbUpwcXpKOGtpSHNDTXcvdUxRWnlyK3NUK0FrenJ2T0J4VnY4TnRBMWcvMzUzTU5yR3VySmZSZTBzUzdCRWcrZkhqNkNhTnBqUW52WUp6RjRKZW1GOElWNHF6czZUVUM4VjU3bjlodC8wN2N0RHZ6YUthdUNaSVA5NjZwdHFHQzhuVml6NGFPSVg5WFdjTTg2ckYxU2d5MjR2aTA4ZVZyd1JmclhQem1OaXQzRHprajRkR3FqN2lQRjZVV2NVR3E5ZDVaVDJlUnVBOEQ3NVM1dFlWL3JpUkhrNUV3dG0wdkp6cTk1MlcrV2xJQmdUUlB4Z3RjQXpmOVJxaVRURW0xOGtyQmNYcTR4UWFyQ1ZCVEZDV1BVa2l5RG5yZmNvWmsrRDgzR0dPV3A4bGtYTC8yTUVIUnlvdVpjd3UzUmw2L0NvUmxKNDlOc1ZTTGRadkRQWmdjbFNHcVJPU2I4Nm9rY0Q0dGs3V05ISENIZXVsOEtZdmZINUJXeDg0QU9OdzZXWDFwVnllQ1FzN0JhZnNiVXA1cU1GSndTNkZSWXlia2xKRnZ4ZUhONC9ISm84T3VaVGorcTNuTjI4SFJCZkRiNUhHNlUvVklKRml3bWxmcUpMNmRZcFFZa1ZnTmJTeXN5d3pBdDI4cmF5NHZ1emtYb2w1ZndDdlN6TmNjNTFOWFE2M0R3UkFxU3FjdjJ2L3BqTGZWb0RwaGdzbFJoSzA0b0hLeEhmZ1ZDZVJwdWNQdmE2U2s0Tm5IdnpMclJZM3MxaFc0VkFleGlwNDJNR0I4eGxER2VveEtwbFo4dmhYSjJWY29aQW5wTzR0U3dFZlpMcEtiUXJabUR0K1BnclFyTjlpV25HVVFldEVrdEhYMFQ3QUNtZ0JvYWg4ekJUMGs5YU1TZk5yK0U0MWhTRDUyeUQ5MUpvbjYyeTNEQWFXVWZWeXJJeVVsQnJ0ZjNkMm1xNGZaL3dLejNqYkttTnhLRGFXSUJOdTFhcld2Y1BldzBrTHpQZDRHMHlmV1V4NVdCMVlQeW9WOHlOZlFwUVVtN3NPejg1ajdXZ2w2SjdYU1JQNk9IWTZ3cjlKL0luckpab0Uxb0Q2TDQ0Qm9TRllUemJ3R1hFNTNVTEt0MVpUM3pjaStqdVVDbis0NjhJaXNYbEhyUW5CTnk1OExCeHB3YmV3aVJDazNaR2ZWTzh4OG01bnBPOHVhTUhPc1YybmV6cjgwejFyVDlDazBZRkNTWWdpaUVTK0dOc21EUmV6QWtpVUc5bFNQUWJ6V0U1bUU2cmpLM0FWSjFMMnZiVU92T3puSXV0YjNoSWtLU0Z4MUh1TE4zUGV1K24vV2RVVEpUdjA0elVqK0ZpODRkVHFaSjh1OFpDS3ViZmgvZi9LRC9MQVpUeWhtajR2RU9JU29qOTU2WUsvZ0J3UDBJM05XVzA4QU80VjUySDRMdWliQ3g4LzFYTytlWjYvM1hOdGd0ZlRDS0R4SDhMd3BqODFBdHpvbUFuREZKbTYwcDhJeitWUlI1eVBJc1lPaWNHNGx1UVVSU1dUZ1M5aDdHekVpekVranovQ0d4S1RIcGtyM1NneXo4amFlQ0M2aFVuenl6MUtoL0g5Ri9KVGN1RTlsSjgremJySmNQZzNSZGFsWXlHTjJUNUp1Yjl5ODBMMkQ2d0xuVUwxbnFjb2R3OW1kZk1LM2N1NHNnNDlsS0RtZkhrc29rQ01nMUp4Z0RjQ1NzcktzSGpGSnk0cE9QM1dtQTVHdFFrRDNOTzd6VzJ5cWJ6L0ZhSExQZE9WUkJTMzViMzNkbm5OR1NLZ1YveGl4OVJpTTZnRDNWZFZyek9vNWxSVmNXMjFSVHNhQ2p1ZEhFVnhZaFZlR2s1N0g5R0lRQ3d1ZXJZMCtvYXNra081dkJJZDgvbmQzSTRuQjMvUnVEMktub25nV0MxcXRwUjJjUjJ2ZUdpUlJxZSt4eVhFcjJCU1BzVDB0Wk5zbkcvQU1Ka0JheGJxSkdhbktmbWh0aTh0dGVVZmtiUHQzUlVzTW9GTVRNekxsdVhFZFExYndheDhXTTNPd3pKcmVkL0pPNXRsM21wTDNmOXZjQi9tYjdtK0ZUcE91Vm05eDlpVTVjYkJjb240SG1aaWJTY2RROGcvU0hWbnNsL0dNeHhRMXhvdG04UEF6aWlXcWxEdDNtNG5ocis0dXBuRnRWbjBLemJhZ3ZrMEJReU1UMTJIc0RHVGl6UDJMZXI2QnRwVnlacVh3NTMyMVhqU0hjRU11SGE3NmpiRWpUR2lJT01XdUNIZUkvbmJKcE5jQWJKSEJLSEI0OHZVd1BuK2pPV2tTdVBlRDdXS3ROc0Nzcjh5eFZKUk9rUEtWYnI0VUNROXJ1VUlMWjFpSC80TzlWWlZuZkRrWUxCeW9Rc2orRk1XS2ZBakttNGdpV3p4b1dCYUZUWlRVVDJqODlmbkpQdk1xRi9VZEpkeE85alpDWE5YOHNBd0ZvRFNySzV3MzhiL3l0YU9tRVp3dlhteDVRT1MyUHVSNEdLa0IycHdnb0w2Y1BQMnhZT0c1bkJsVFE3Smx5SS9zVDBIcEVLVm0yV2FKTXB4M041ZWF4c3h2YWFpU3VBeTlUbDlEaWNIYUtndWJvcTY3TDdQRUJwZkI1Y3VaSDdhdHFrZU1aNUhHQ3NMSG1WSTQvREpQV3FMd1hTcTBTWDJHeGpvbmFKRWN4bXVraWptT1hCN29NOElFV2dUM0h2ZGs1MGFiNXBmUVhDUEpwTWYxUURWa2JSOTJvRm1LSDY3TmFLd09kWHdnSDVKcFZUdnBkdUkxZmtkTFU5amY4N2lEWnFUK1dMMUduZ3JCTmxIVk1SemJEUG9NTFFRYzlRZ0tGZmdPZFQ1NmZvNDlXNW9EdUc2S2RBTVhrRzQ3YmxXdUZienR0bnRzdHBZTy9aUnBLby8wN3lxY3F4cXZybkhRWkVaREJKYUdKdmJJV0U1UlpMZE0xeVR1YVpnWHN5MGJrSnlTMlNzcExzUUZic2JlYWNtNTdpbDdjQjEyeElKNVZ5Wm1qQVlickRMTWU5UFd4bnpKN0QvajVTc1p4Z2RXemJEaW5jamdEWlFZYk5rbGNsQVBYckEzK2pieHZWZlRXZXpibHRndmJYVkVPUVNDdElZUk0vNVlLTUlGN3VqVmdaN0Jrd29aK2tObk11REVTdXk5SkY1TUtNdmduaXFFM216dmRvRnlhUjRPclhWczdKTXRWWWZBN0JnN1FIOWpHR1FmS1JHSUFsZndrd0dKcjIwWkFLQm5jTUJCTUtGb3dhMDdxSzlVQXNhMGgyQ2xQdlBQZVVXNHNuNnZydkFOVWdoZk9XMFJPZXpoYlNwUkN6RlRSNFlUMjF1OU5vZ2JRaFdQUS83ZnBiZjhaQ3dBMHZ1SzV3K0lmTjZ4Y3JRa3k1YkxHVlRPcnVRNktkUGRLM2N1dlhiRzhDWnBUVWdjL2tqcFZ6Q2xXWndDZUF2eUxyL1NvRVBHSUFKWDduUGFnU0hOYnkzZ0JNcXVOWmRiME9WRGdGdngvUGJKMi8vbkxrbDhSUjNobTFPQjRRSlovRUNiY3BoSVRPeG9XK1lsajlXSDhmREUvYkJvQzcrOTlxdzhvU3p6ekFBY3NEUGQvd29RbVBQOVhDREozQzdIZmdNcCtiK21iK1hvNk9BUFlWb3diT3dzSUsveWJpbmhHcW9Cc1hsR25VUnBjSVNQbkRBTlFSeXpuU3F6VEhNRHNHdFdaWERkVGpBUXIyRU1GUTRPbDZnNjQydDcvK0pRWmtMUmlOTGRJVnVTVjJFNWlqeWo4b2lpMWZDWHJNV1M0WnI2ck1zV2Y4MTQzc0Q0UytJaXhFSklobVVMb1Jpd1RLcGhCa1NtUVFoY1ljN3A4a2ttNExYTGdzVWlxWmtNL1JCMVZid0U1UkhpM3NiOFZXL1ZOQVNBdkRqdFRJWktRbVFaTXVJL2hNUXp2enppSjhnWEdrZ2xhWkJJeHNRS1VubUh2c0ovYk1kb1A1YXQvcHVDd3M3NjNwKy9YOHE4UDd5MlBocHFOellHT0lWU2pTemJlY3VuT1h2WFowMmpJbWw1R2tsYTBYQUJpMVlncitSZVhPWTJxY3BvOGxMSURWbEpianhKVmp6OXVydTZjREtrRk9FK2s2VEF0Q08zaEVVek9NM0ZscEJ0bE1zaFlxMitnUVVyeHZSbHpqK0Vyb1N4NDRNaUppWG5uckhzM2xSemVaaExnZlpBRWtFSTBKc2ZBc093dFprSEdPM0tUaWhyQm5PQTNnL2RIVGZnVWUvTWZrUkVqVW5iUkNUQ1V4UzdEL1NJcTR4QVBvN2lDT0s2QzdFUndnbjV1K240K0syWTRrdGZXRHdEUk9XZnZoTE5kTUZ1UzRTcjhIMzczZElpUE80T3k4bW9WS09POFEvaVA2cGh5dkJocSt5Z2p1ZnRReE5vYXh2clVpbW5UeDh0aVRCdmVtVS9qVFcvOVhRM28wbzhRSis2TVo5c3E2L2FxRExaOXZTNy9RTCtvMG94aWsyS1loK3dwSk9FMTQ1Z2dXbWZGSjYwS0pWVkUwQUh2REpJeHR5RmRJMmdoYUJNcnBHVVgvWEttQXZiZUw3TnNyWmxwejU0bUpWNmswR2pDZGVZRkJIQ2ZRSUpid1ZLa1U2L2VLSW9RWVJoajBwWHJzcm1qdTBlM0FUVWNPZmFKenJDS2FYTHE2WkVpR2ZOcm1CdVZISGgrQzZvYTBXQ1dJa3loclV2aEpKNXJYakN6VDJhV0tRVDZoc2V1aWV2U1pKSUtuUWVwMXlWNWlIZEVpK1N3NmdJQ2RtQVFWZG44VmRuMGNGOFBIMUp5R3AzTGNmTzZBVlN1cWl6b2lhdzhUaXplenZIUUZkN0JtT1U3dG1ETHVYRkZGVjFFa1NiYUZMOEFaMFlNVFJYaCswaEZLU2JSK0NRbmwweU5FQU1lSGpoL2ZIcWhwdGdVREREOWxmT0hqNW1DQ0ZSSWNqWFd6VHpML1ozL082cUx4MEYrTi91TmRNSjI2TFdZK0t0emxpRFVkb2N0ZnpNVG92cVpWVTAxalMyeWNTYzFmZXlrQUd6Z0tybnYrSDN3eU9JMmxGNVZOSVdIcjFtNk1SbjQrOGNJekRZSFA0WXNOWUhMZ3pqZE1rNk9sckZHT1FsTHdFNTVOdVpYanl5ZkhEOTQ5SjlYVTdJUDRqS3RHeTNPVHZ4TWF5eWNUZ2YwSnNEVDRWUUtQWjZwMVV6c3FUYmNuZkFjV3d0NERXRmZTSWNoOWlGcCtreGxERis5UUtFY3J5YXRjZlg3M3JpUUU5NHNuY0MzUGk1M3R1b3p3OWtqbDVPT1h0SHRQa09xRW1XRWdWQ0R4R1o1c2lPQlFhWWNxS3c5N0toV2FBZGYxVVRzL0YySXVKUUJXNndNYUs5SHFMRXRoSnduUHg3dEZjaW5KRkJyTnlnMFIrbzBscVRZSEttSERPQkhPZG9rc2Fnd2lPblRuTFA5QmZNTzJOOC85Q2puMEtRL1d4Y0E0YTB0QzNuR1F4VmgzQWcweTlpT1lZVUpVMzB4YkUxaFBJT0hiRUE5NStMenhwbDZjS0ZBTldmNUtPbmdERTJGWmFhbHNkM0VFeHF1Z2tQbHZCV2JpdTFPcE9VTm1lQnBDRFluRnZEUDVJNlBTV3A3cnlYM29SUFdvS2NudXoyQzdUcGhadUNrcVk0VXB2alRpdzV4NnAwY2F5SkJKL1hCYVBCWng2ZFZGaThtUzAycUlzQjkvRzVWTnE1c3lkNldjOUZ4WUdvbnlvTk9VRTJLeHgzaGQ3QVV1Wm5hY0JDRDY5amM5aEVSSWs1b2lPWE9LMklXdHR5Z3lhT1UwRzFoTEQ3YzdieVdRdUFGMkVESWJDU3dPRTY0VzMyL0FLQjNVRU1DLzFQS1c1YU9rOTk4b1QrRUxjbGE4Z1dhTC95anBxb29FejVuZUU1R251emF1d2NOcjFkTmhOYzRnNkRCVXlXbHVFTGYvTHpVeVRKVFppZHA0dUdnUnNVc3N4QWxIc3NEVUZPUmgyMzgwSnBkL2FZWVlQNEEzSzBrTHBIU3lwOTQ5cGN1RU85QUVXejdna2ZHVUxqc1g5Yzg1QlhsRVdTaEJONHczNU1WWlZhYUlFNWFSTWlPZ3YzSk85QnJ6c3NPeFJSRC9QYzJmVzlieThVaXhmQ0JmMk16Z3dEdHp2YUhTR0hxM3NFVkl6UWRSclVKMXFaeTM2U2luQTlKb25IS2VEaDlZYlNYNkdxMzM1RjBCYXpNbUJnb3FIZ3dQakkzbisyR1pIVVpORkdjUmVSTk9BazVTdk1sZjVlQmVWNTR0SERWSkI3RGRwbXBMNXgrUmRDQzZWQ3Vla1lValU4VDJKOWsybnJVRlAzYVRLNkZYRXgvQTRscURyYVFlM1BkSG9YL25kanVvdit0ODQzUDl5Wmo2Vy81VUdGZURvTHgzQ0pvdWhUeUtLQWhhZHhLME1uUDVSazFIRVJkK2hPa0JNOFBpVmJZWXBnd1JwSUZRTC9SamYvbC9NVlhzT08zeW1HVm5BN3N2S2xMYUx3VEJSZTB4MWtyczhZNklEYU1UOUNmY2pyN3U3NUdsbmtld05Od2NlVzZ3Z3poU3Q1MVJZK09rYUtjbmhQWVJLeGVlOStqMk9oRjBKUHFKUUlzQ2ZoYUYxR0lmRWpQbDlBMHIvWHNnanBDYkl2eFg0OHRaTEEyZStMVTB1bEcxQWlQT0UvYll6MmJDeStZcGdxUmdKejNGd3hob0FaNWlvazd6UWVFYjZUWlFlM2lGNis4QlF6ZGZqQzlPbEZyR2w3ZDB1VEhKQ1ZxbGFhVk5Ea0doZGg1RGowTHc3d2xBOGw5bks5R1AyT0Q1UGMxbWRVQkh3MmFYd1UvekdSVmdnNDJnbHRsOHBvOXcxWGV5WXlhSGFSemhqMisvMndOL1R4bDF2TWtjMFRWdUt5YnZ3ejlYZW0xMCtDRTBLUVBERUh4c0pqZ00xdml2QUV5Y1R5Yy9TLzhOTG5VTzdVcHd0QW0xaVY3Wm5TMWw5Unl3RUV3cVU1dFB2Um5YNjJMVzBRUnBKR09Bb3g1bTRFRkV6N21Id1ZtaEgyZXZVb2VOc2hzV2ttNW52a01kSGVZekxyRjR4N3paVDdid1ZTck9QdXYweWcrdW0yWVQxWXBJa1BTVGhWd0NhaFEyYmxTVnNLZXR2NlQrVlVJSXlFcmZBSStlcDdIN0xaNkl6TlNaZ3hoVUZiOVZldGtxRlRySENnYlZuTzJhTUIrdkdsL1JsNzM5djl3TVlNNTVkcm84RSt1YUtKR1RRRkdzOEtTMUhIZHVXaE1tcEU4cVBwc29KMlQzWjkvVzdVTmd1eWQzclVyVEJPaW1ISnd5Vms1MVozR1NudkFtd3Nra040Q3hlZHQ4SFNNZUt0cmh0L08zYnRPOGhlMksvNFFlZ0JlcndOR01wZGpFb1NlV0lYQnZQZVBKS0lqUUQxQXZuRUFuUStaMGFMQVhSd1pJeVBxbE9kK3BlL2dnNExjRWVMcGNKNjhTR0NTYTlqbFhjdzFOaXg1TXdTT01Md0tEUzhaczIxVmJzbWpMNEJ5czZoVFZkK2c2UDFNcHBTV3hhUUw1SnBsZzlOSVI3YmhLVmtWdUtzTVhJekcwVDhkUEJMYVBvRG13ZjB5cDgvR0FBWEdCWTJncHMyQnB0STJtSVFuRTdpK1F2aU1wbXNzQXFtWmtDR0Rmd0dKVnAyZWEwRnJJaFFBUFVRM3Z0eEQ2Z0ltZXpJY2VjbERVdkc4QitFWmtrN0ExVDluNjBpZWhDN2VFMEltby8zaU02cldtMUJlekJGZlh6d2RyUlBrNlFlb1BucjZGMlNyZDI5N00vcy9pYzRNSDJGVHM4eGF3L0w1cFFkaExiNlNHUFE0cVpLbGpsOWZKOW1ETTlsQXNBMHZRMElIT0RZY0t1SXM0dndycXZiUlhyaWZ1OUhJdUUrR1gyeXlqOVVQSmhValVhOE9ORTJNNmxyamtGcjNBSDdVWng1b2NPSEpVdGVCaWh2a1VPZHREbEdMbDdscVMxRVlPdlZBeC9GWjA3RDI2bkJaY1l6aVZ3RWlqb1FSdHJqNVVOSm1CZVVGZXVHdmltMnFLSklYWllReXJUMzB2ZnNwQStHdTRPa1huRmlCYUMwTnVqK3lteit4Y0U4VDBLYmQ0dGlGcnh2ZmhMdEIxSHI5bDAzTHJIUUxvazdCRFlGQ3hRQnRPUm5UeUFqZGQzT2w2ZXo5YVhtc3FDaGdGeFJuWHBkQWZDV3hRVzM2bmdkWHBjTnM2azMrWGRwOGE3d25Ydi9tcFNrL29kaUN2WEtiUUFFYndNQllUd3Y1b3Y2N2I0UWY0b25vMnRKeDkyRVUwaDlPcnR6YjRQZCt2L2o5R05WSklsL2RUSm13OWRUeUJXdzdOdGRuYzQ3VXhMbVVpbWpCb21jQWp1Y1ZhYjdQRWtzN0JFUktBZ3lmMm1XL2h0endNKzlIN1Bsc1FTZW5QQkYzUW5PV3FRQmZLSWFibEJxN085d040MFREQjJmRytFMnlISDgyMWI0bnIvVEdWSC94aFpSWmErNHYxMGV4dktvYnZieXlnc1FPbmRhSWI4NTUwRXRLdVZiQi9pRFhKcFBLZlc1WmRJb0ZYa3paZDdYZGdnNmRadHpDazgwTitJMHNUQUl0TXo0Y09PQkFNYkF2bDFWaXo4RHBXOCsvNDFHUnFKeXR2L1BXRVg4bEtkZUVFVjhLNTJTZ21ZTDBzenc5L0R0OFpFQ1pXdExmaVhiQzROb0FldFdad2xUQ2IraHVHVml0VlN3LzcyTTdPNnRrdVlGOUtldnFjNHdxRk1UR0pheDhKRWhIUFd6RlNXb2kxQ1JYSEw1WFhRa3U1QkxXT1pralkzZGowbHV1amFiZlo3enJDNW1LUUlraHJEMW9yWmo2M3J6ZUJwK0ovT29vdXZUM25SMVd1T2hucEh6MFg3ZWJLbHNvb2NGalprUWZqRWRzanRQbk1NTkdrYXpzbGFqSEZFSmc1YmM5WXg4dTVlMXhUWEF3M1ZLWU5MWG9MZldSSUVxeThjUEdOallYTkFxS2xCTWhmMTBtVWtJb1dsVXJMWmh1SU8yN3FJYXBMUVN5eWxwOTFPUEtpOW9IckhOQm5BemN1bTdzZ3lsZmN1RlZrVU5kN2g0V2N1STVhVkdBRlVFS3ZSSU12cUdTNlpadlJkQTMwa3pCRjM1bThHKzdWNWhBRjlrN3ZKb2hDTE1FVXVSSXBiaFN6SHU4V0FWNlQ4M3M3S2pBTWVtZUh1am1CZUtLWXZzcGxDdC9JUTQyZzlQbjFZUGROaTNQVFNYTGRRZXFSdU5UVVFwUHhXcjN0YnhZMW91NGNUZzltV0FHNHp0Wkp1YlR5dVZ3a01FTm8vd1FVUmZ6QWdaS3lrMHJyeEhEZjQrK0lLVGRDMDJMN0dmWkYyYUJ5Vkd4Tm5MczZoODNhdFMrRjJUZHJzd0lBZkRkeGZEQTFBWWtubGF5M0tEOWhhZ3RMTFg3d2tuZnFSR1NSbm42cTZVNWhrV2FXK0U5ME5vQVJ6NUtzMEJvM1FVaFc2MjVpVzBOQ2NmVUtIcWI3TFVSSnRYZlRRTUtheG1mK0VDWDZBdDM2NnREYTJHR3NCUy9nc1BuVkE4RkVSeExQQTN6aXI3R3NkcVRkUXVudzhncUZhdWhoUWhRa2RpVmd2MUNGN25QQnVFSTQySWRpa2tFSFNvU28rRENoODNJZXhOMVlyZGhXQWVBTWNZWFZyRlJmRjBLSHh1ZzQ2RFU4Vno5Wmw5K3dOc0dVenkxbk9XcHhUUmZTTlF3ZXRKY3AwV1FUbTR5WXpjVkdZbTZLdGhtYVMxNWQ2Ym9LcWQ3WEs3WGt1YVFsOVQzbHg4Y3pMMWFQRG9RSnlkVXhNRDRNSGhsb1YxWm5UT21WMW5zZTE0WFZiayt3Mnl1VUZxeldmZndNeXFwbTJOVm1BV0ZMakRHN3AvdU5lakQ5L1k4SEZIdW1nN0dEUUg5NUZYTzB5amFSTGlvVlBDNEViYTNHR0dqOGl4b3dmWGg5RWhxS1cvZWgrb0s0UWJWblhjYU5JdHRZZWZWRlZnVFJTV3Evend4YjQ3VlpWdXc2cDVoSHBqMUJSMlIyYnk5U3pyaVBWeXZjbytqNUdmOEc3bjNibGhXN3FjM0l0Si9Dc1dHTkpqbHdlZ0ZLbWlwdVh2bTBDYjVmYW9qYUVUUXBqcVkxTlpGVnJ0Nm4xVnFuOTJ4ZlppTVlmeWNSYWVPbk55c0FxRTI2L1NYcHhaY2FTRDlhdVVYcExBNS9MRXlzanFzSXZZSlRjV2FLbHFGSyswd0RQVjJabnJid2VwZGhRMWdCTVI1dGI4NXA1OXE0OVNYRXIzTWxvMzVWU3lBendKYkhlMEJvU3RRTUIyRW1jZnRUUTE3N1ZoK3Y0Tk1Qd0taVWU0N3JTSlNTTFhPdGwwTnd0azN1dTVGdGM1UWNxRkZTdWFIRUNLOFRWb2JlWHRiK04rcWpMM1dib3BKRDAzZHhZeWo0RVNtcVVlS3dKVXN6KzVrQ3p5RHNBM3lFYkpLdTg2bW5DZ0JYbFFrQ2o2M1RHMEdkVjgzSS9aL3lZS3VERUlabXVZQ2dkM01tMkh4WmFMYmNmRmg3YlNNZzZ3alJzWG1qYnVJdmlMWWdsVGhJQm85SjAwQkhrUlZ5M1Jmb25YUWYzSjFWbWc4Y3NWaUljdWdrbUtuUFVLMVhZN1hyUkpHYVByRGxwY2hDaTFxTGpZUERlMmtTb2VQOUcvTkFLT2ZpamtVUjRPUC9iTjFmVUx6a3QzdEFleHE1YVBpbitPSG1HcjJJMFpUQ1Q0TnNJcXVmTFl0TTZuWTZQNnpRTWVDYm1INDRnVzNaazBnYm5MbXpqY3k2WlUzZmswaUlXRTJnNnV6UHNhbEtUdHp5QzZtK2tNVHJQZHpqV0ovUTJibkdSQ1pVaVA3WmdVMU5rUStpU0phYitkRG9FWVFieFFwL1pzU0ltM3YwbHhLWTg2MytpNzV2Y0dDZ0lqRUJxSGNvRFF6UGd1bzhMTWNHVEUzY2YzZDRvaXF1dzI2S1lpc1VmV3hOZFY2dDEwdFNMcXdteFgrMXJXK1U3azExdkhrbFBJNE5jTWgrSkxXNGFZdUp3QWhIMXYxMmtkSVU3c1hERm5mTEZJMVU2dTlGYjNMaG43VHY0Mm1Mc0t1V3BXRXQwd3hCeW1qek9WSlZXUmx1OWtHZzlxWjkydGNLbWluTFkvUTJiTUVOR0tTTDhCbEk2bHBDV2x3QzBPS3h2VFE2eTVpdHlIL21iZXg5Yk5Bb29KMkJvaURnQzF3WllaQ2NleTNPYytiYVViR1Y3eTdIRU9NTFQwNitReDVWVFIwNUhXSkZpRm5DSTlEQ3U4MGtTL243UW1RbVhNNTBVZDJhVy9uVUdjU2svd295OWNvU2lEQzhDSUxIVGQ3NWNudGcxbjduTXM2UW12WjVvUEszYlpPbEVkd1RqQWZaRE1aWDMvZjFMR0kzR1dEMmhoNVQzZXJicEhoc2FBbzErWUlsUjQyR3lGb2NVZWV1TURjWk9lVkRMNW4vWTZTb2xDcFo1N2lMUHV3U1liQm1pWWcwcVJCOXVUREU0SG0rYUVxc3NBVGlBZExqaFdaYVpPa09pdHpDbmJvMUg5SVhhMlYyR2UyZ1NNMmpicExsWkVEK0JFaHg0MGVEQzZ4RUJ0YWtSRzcwM3gyNGxxVEozc1g3dVFtd0NLTDhVNGNlaFJ0ckRoYlduTkFXTzE0a2lsRlg4TVkxV1Jzdk1ENWtXU2ZidlZMak1YOXFKelUyelZVMnFHNk9xMGdKeG1PeEFlZ3E2YVZjdHhrUWp4MVd2RnFFOGdoWk9iZmZ4MVFtMTMrRmMrYWdFaFoxQ0hpbytRbmozbnZ6MzBXUUhmSHhzaVVNUlBNWC91N3owM014WlNLcnZCVGxJZ1M0OFJpdkJqR1Y2TnlaUVd3Qmt3aVhRV3dTYU9vOFV3U1BTSDkybDloeFJSZEJ0bnNZSWRLc2lXOVVsakZxZ1IrVjN6elZiTnIyR1JFbWkzWlBmT1FtbWFla1o5ejhLS2EwM0dXbjdZQzJ4Q3N3TWhybmtvMmJUUnBMMlA2U2h1TFhQU2poMDduNzBXbDJscXF6RWN2YmpwNGQxSUN1b2xnS2I1SWs1eEF0TmJRbFFJSVlxbm9FMnR5MmIraHhNRnB1RGN6RFhkZG9uamxaKzJjTkhhS1F4cGJOT094WjhaZC9BRjVlOTMvdDZFdzNMd0hWd1ljWGJzS2NmdlVMUUtidmYxOEVlWG55SlA1eEcwNHA4UzNiSkdSQ0tNRUNmeXIvU3c3WnhWUkVSMFRPSkRuTGkyWTVzTysvTHRBL0VlMk5nMkFLazgrUHVDWmxyZkFJbmgzVkI4ZjBaR2UxVVBrbWZHY2NsbmE5M2hqazhUWDZ3R21idTI0YTRFb2FsaHdiYUNDZEdHQWhGaGF2eHBsb0sxeG9sMWdKbDdpbktURzByeHF1dzVFbU1nTFZIdGpmRzFaZXRuOXJUc1R4Z2tzZXYrb1U2OXF3dEJ6RUVNd2EybVZsZmtOOExQOGx6clowMWl3Zks5OUxVb0JQazJQcFZqdGhmMGF4ZVVML2JSY1lReGdhOGZqaVRBZVJEcU9HK1NMUlFaNVJ5MXZRaEFKTGNBUncyei9DYXdJdjljQjI0SitUSWRDWVZMVE96WTYvb3RzOHFQN24xRlJyY2dPOFlnUjRQUkd3Mm5SQWZCaWhxTUt1Q29aN0tWcE05VnRMRVBubnVaK3dvREdTd1dZdG82Rnowc3JqVTc0WTRRN2MvUU9xdkpNWElybGd3TXN3UGFuNVlmc0pzakRNVnhvWm85UlI3SDJqRmF6RjVHeXdqVDJ5NHMwSnRObEFyZGJZTXFZZ1VLcHpPT0dBZ0lDQTV2cGtBT0RranhQWkV2WGlZUXZqREhxTDVqRVEraTBicTh6KzRRQjk5YnZDTFFUUmpuTjNJQ0gvYW9CVld1ZVpZSU5vZ3F1MGc2UmxjcUJTR0tleWJ5OVN2Ny94cUhIcEc4c2NlRjdrcEI0dlJKdzY4b3N0Um15aVZiL0NscExPYm1OVUVMd3htdUpQRlEraXh5eTFqelFtNmxnQWFBUWxwdkNZeG13RmxsVUIxU2t3a2FLRmZybWg5TUJ2cVFwd3FoYWdwb3pwZitWMlhvSGR5WnU0WjU5bjI5cEFKSHRDWEdZV1RLQjNDRDgrMkEvY1NKbmdMeDRRYUI1NklHVHVzakhiQ0pvSkMwNW1STXB6eEZ3OTJZSitBMkplMEV1ZlFwSmhOYjZVQmNNTUhwYXZiTHFackVNZHhkVURaWjVuSEJQTUVoSGdXSTFXWHVXeENGRGxHc0pHMEVSYkFBb1l2T2hFNk8vUEZZbnBRQVlPd1kwMWNwUk83RC81RVdpbEIyNlJrK3hrSWZraGM3V3V0OGM1dWdvV0lkWXZDaUJ6czhndmJLT3RWbURYVDc1Qkw1MnAxSVd4bzNDTVFnZFhwQkpmSDFybVhOUEJNVEg2bjRNcEhXeVNMc2J0ZXZLMTdxQUxpQTg0ZjZmSGM0UEhNVDYydzVBVzFadGNlcXlGRlpNSVhyMGRwdkJGRlp2ZDU1amN1alBoeXlSWEFlTDQ5bkMrNkNpcTNMZFJtOGRLeHhnSmVhTzZCbWNQZDdjQ1dTbzlBSkpYejBnazE4SkpwMEd0R0FWNGJXQmRKNnoxdVRITVVua3U1Uy9CU2tIVmZUeGZlc3RoN3VsL1Jwa28wNENobktPNnNySU12MlFqcHFUM0ZVZERTYVVkZE5wM1NCRnQveFJIeGZPdmlMczBRSCtkUFJ3UWxST09lRDFmNjFjT3E3OXIzWE1KeS9JK1JDNjBIZmVlajA3a1dIb1lxZHN0N0Q2Sk4wT2MvcDRsWVYwT3NsOTB5T25OblpjWGFjR3p4djluNjl6RXN3VjV1SFFJQXUxdlFvMUN5TnVXaUF0SkJTZmxaV2crcTlMNTZzeHpEbHlkdURaNkdsaVZVVC9FMk9IU3Fld0FwNldQajZyVTlYQnUxQjhMUWlja1Q3T2ZEMWhPRnA0Mm4wcGVjcG0vczZXTGlZV0ZQamxSWjl1ZDFNUVE1QnU4RlF4QnpocTF5ZW1rN0hQeTFKT2ZhODJabm96NVpsWDJrSTVvYk9hZk5KdzBpVUoxQXNnZWMwVHlEbkNvQ2t0Smk2YktkQjBMSWhyRWhCaGc0Lzl0SmxMcXNsWEt5KzFnVXVabUFnaDFxZ3loVkJycWkvQWYzaVBNeFlXeURPRTdqckV1b3BHNVBPR1QvcFNvdGxCZjh1dlczRlF4V3E5eVVEWG5PWUZjMzhjRTFqS0xsU1Ryc205TlJiUWtQQUNNd3RCbzhKc3pOOGJDVDc1bjBadytPbmpOMnJ4MHRwWkpNYitjT1JhQXdvM0NVTWtqWGxuOW9SaFFFYzZwL3duU05wc3Y5cktpbHNBVzkxYjVjZnplZ2tvL3NVMVpzSnZmQ21QbnBqWDVBY0I1dG9kNC9RUFNvdmMvcVBublZyU1k1Ukc2ZTIvTEVUZEx6cWVOR3FNMjJPZDdYclJRU1BCRm1CNWZGdHoralJ0ZDVEd2RXY0pUYk9TQUc2aFBNSjVYd3A3Z0l3czd4L00zMzA4dmpGaFFFRWViM1E2c2ZWR1BTTzkyeE4xYkpPcUlVNGcyZHhIdHVsaFl4ZzRpS0VORWNpZEI4bzUvUFpPTVI0ZElUU3prb3pRemN3MHRXTG9ha3A5eDhVUGVuN1duTzZYTUltdG1CM3IxZnFhdnhTOFBpVmN0NlhlMUl3aXlCUXpRUi9DUTdJNnVjallVU2N2c3FGdUd5aHlCNWhJdGNvbHh5NU9oZy85anJzODcyNHVGU04rV3BzYUFsL0p0RGdJVXlqYWtTTWhaYXhSUVN5a3RUeHgwOXhkVjJkOW1rVUlnb0xTRnNnWVV5bnNxa2ZWMmxEL0FzSDFPdnhxQnhTbTZmamxFaVQwMFg4VjVoS2FEdHk2Z2RvWTVZSHYycE9qWUdXMm9VbThvanpuQytFYTdVMkdzRVNXbTA5OEtvbFFLUElrTHEyajlaRnRIM1BPcHhub0paQWExTndzUHYyOFAveDBmVURlVTBSQnZGOWJ4VjJLWDRjclZwaEV1dklESlhYVVBZekRqL2pUODNWWDB0YkEyZUdabGRGaEQ5ZExMWS9QV2JucmZXK2FBbzhlZVdObTJZRE1DalpxUnllYzlxV3FtQkx3UWpYeXZsVnROa1BsR3NROUtWelJDQnlIMWxrTEhxak03M3lVR3Y4bDZCSUprVG5KRW5KU3N4a1RMbTlSdVMzMDZ0OG5iakozNE5MQkJmTHltekNZUXBoYTdkeVFuZ0RRcUV1QmF6b1AxVVBsbHB3N3pUYXNzUWJGV2h2SFI0WGN3cGdlenkwVGJUYmFTVUJNOXc3K1pqV1V5Tyt6cmN2b01RWGFPdDN4MnVGd3hJTmVlRGFiYzFqbDNwQTlONm1kSHFkNUdxUVV3dTZNNUd6UGNTb29tUzBFVGV2ellaUkEyOTRTZmF5dktNbWllTTR3SE44aWJPWWZJT2lKcjlFdUFQS1Q0clFFQ1JsKy9CZ1B1OWc0QjBQUmdQeUhVNHlsUlg3aGd6eXZaOUhlbjh3d2djK3dwczlqVUFEV2tGcGg3MUdNT2tTMG1zcFdIcUxnbzk5YnhDd2pCNVdqdG4zbUxvQjhwS1pQanZyMzh6MEhCV2hVQWRsa2ppdEcycmVEd3kwTUZib0xLc0I1cHhEUGpXSkFDMFZOMkZzV1IxNnhHREJsQk03WE91aGJZOUI1aWhwU1dyNit6VWk1bFkyZjVhWEdvdVZ0eEtlWkcwejBtZGlvN0NzbFhiZ3JqOUlsV242N0Izc3c1NVptR0RGZmZWaXZKOWEveWF0WlhzL0I2MVB3ZDFFVUM2bnYxN2ZXNlNLWmkwdndJZ2UzNloxbWx0M3JXNEZKRGYxZlk0V1g4WU9BeFRrczNjZEFmK0E3dDVRLzE0MkpKazRWaEh4aWJKZUg5czVUQTVIbmUyM20zTlltWHh6NUI3RHB2eFA2QTRoV1pRQ0NqYXVuMDRxM3ZmMGYrcVJneTdRVVZURUs1NUJjaElaYUxiS2ZZWEE5azI0enNTT2tHNjc4d0EyK3p1VjF3ZE9rckJZMEdVSEt5OXdXb3JpM1NlcjJZSlo3RlFYcm9HTzl4amJjMXk4dE5zemg5ZzZGR29ieW9ObmU3SnU0QlNkbFVzNTY2MWtTakc3MHF0dG42SmcyeEpTdXJjeDBPVXhSdU1uVTBLQysvSUM2OVgvcElxaFQ3VENPQW04TUt4dDJpaUZjVThNTHlXeGJaMHBRaEJlZlpvM2JUVEJPVENhOUFpWE40L3dnM21HTWp2Z2hacVl3TERQS1djcitCRHRhZXgxTE5zR0EvWDNWbzlOdGhNdjVGMHZTYXVUVnNkVlBjM2JQWnpkZWU3YjNFZTRGcTVrUW9ET3JaNlpGT1RpRy9DSFl1UjJlMnJLYTM4YTM4K0t2eGEvcmgzamNNaHVya1JCdFk1RVl1NWVnUS93dUpWSVRrNXpGQkxsYnJJR21ZTXRmMWltV2ZxUGpmSHMyY1ptK2Z1bEJZeUV4ME5pWEtBTktvc1lKUUI3L3ZzQ1pvdVhyTHZqaHdwc0duZXVqeGw1OGowMkJnWlRpZ256U2ZqcGJHWUNiS0J6c0pnN05jaEp5bHpyMS94VEhmcmlOajJvd0NEWGlLN0dWMzVYM3YxMlhTRHNIakxWZ2g5b29hT1RNR21iajN5RFlHVk1ZT1plNWpRMU9URkovNElhK2k1R1N5RS92bXFGZEZCV3JyQU92bVdSSEQvWndyZG5PMnpFY0JYL0dFbXBMQzRiV1ZoTUUxZmZSME94WkNXM3FPQTJIK3hTb1Noc2Z5QlFqU0phREFsUVErbzRZVHBrK2dPM29Ia1RwN2VpUWRKdW8wTlFVOXh6THJwdzJ5RDJyZVdGVUtPRWhUd2RNQmpReDdpL3RLcytMU2lkTElBYXZDclF5VVI4SWdUK0gzZ29QZldZY2wycmxKQWIzYmhnRG44bnF2a25vOVZsdHpTKzZkbVJkRmxCZUF2c3BhaXpLWDg4RlhjOVkxcld6Y2ptUWJnUmZ3S2dEM0xPcHR0MEg5ejZUajlsOXZmV01jZHNaanhEQ0FXSURFTHJwRFREMjA0d0lEWXFyR0lCb1gzcTFmbXZmVU5rME96UWU4K2wyWkppOWwvZDEzNm5Qc2ZSS0JSMmoxVzlzdy93QklRYk9OcEFIR2xqZG9HVHZ6VGRUMFNjTGlYM1JzY0dOSzZ2MWpnU09EOVo2MzVmalBiVFJQN2MxMnhaVHduVktzSjVVdGwrR09WMytaMkZBYURjMWpOQ1dtenQyRkZucloyUHYwYVZTVHRvVmx5S0h2b2RUL1h6WHdmcVc4RUxXYjJTcWJpSVFJZ1Azc0VvN1hWU0JoWnNZZXd1cUxxZ3VVNEFOR1MzTnFmZFZoMzFxY3VVTmJVRTdGcGI4RnVZSkFvSjQ1TDZJL2tpWDR1OTFJK0N1VW5PWFdhK3ViY3cvVC9hZkVadGpudHBCV1gvc0xmWG9oeWpDZ1dkOXEwMzVQUFN1dGV6WGNjaGMya1cybTUraW1FU1NnbVl5ZWNUVmJEdTNCZVIxNkNjRjdNL2ZaRTNpc1ExckJkK2pEV2RQaUVML3NRdzM5Zmx0RTFQMk1KZnJhUDFxRS9yYVAyTUQvVm5SOWllVDZpKysyMHV2dnZ2N2RYUGZmZlFERzB5Q2k5MmNmMXh4cGZweHNYL0RSTU5lNlhVU2wrcS9waG9HQVh3cXgwNVhmNS9ieHMzTUlBMVlmMzRFOTIwWXYreXNySUZmblpWNGsyQndFc0tNbXFpNDBvSVdxQnljeTlLNzJyZmtDOGtlTkkrTTFnakIzdkxKc0lKN0NkUEpuem40YTVzbmtGdXExbkZoLzdhbEp1cG1HVFlSSjVWaDdBVHVFQUFsNzhNd25DMVkzQngvSk9ReHMzcVpGVGdMQkxmQXBkOUdNaVNQZElRckxzZzZBRVZJTHVlcDNQS2tGd0p6QXFmRk9oczRvdVBvQ3hwdnhGbUgvQjF1K1hiV0FpRHNNdXhLZnlIdkxGNE8xUng0aHg2dVlyTDFRbUNPSjRFNW5IVU1UNVRqUWdqSks4eFhLait1elVTV0NlaDRXY3k2YkVBYnVHRWRqbmNZNDQ4ZU5EYWE0OThEeHhGTWRlN1pzOThvb1BUa0ZJSDY2MzFUR0VPc2VHQXkyZzNIMlBkNWJvWTEzUHI5akZHS2FPUE9QQjlnWXRPaHpCV01kWm9HUk9RWE0yMVphL3N6VzNLRDVIOHZJUlE1RHlCUTFTeEFUSHlGa0FYMjZLbnovRWhiUzY3ZFVoN0tnRi94NVp0V3paVkJubnpoSURNVWZYcSs3UWhYaDZabFJrR3pRdnRaMFRFeHVNSjdGK2NlTkZOaUMvYnVXbzkzOU1LdC8wd2pGOFh5Um9RREt1TXcxemkwVy9PbHNpaktyeGY4OUFiY3dZMkdQQk5XNXNKbU52TEhpTGRUTGlnV3dRdVBxRDhWeEJOeitlT0xzTWFldUJkZ3k4VzJLbDBLRmZQUXV0S0Fob1JwUkFqMStldkRTWVBLTU0zazRvTkFmajJCcGozc1VEdVNhdmdHNjNEMDkydDl5c05zaVFxZXMzUTYzc0IzR0VzTlZUTXdpUUhMaE9ZVmZwM1dwVGo3bi9zZWg3NlpTeXNXbkRLMmxuOGN1dm13TjUvNXFOQ0U1ZDN0ZUgzNjZOdmd4NllSNHVJei9HRHIraC8ycFBZaVRSMHZBejB6K0FCSEMzRVhVTSs4dVRuZ3hEWkVsWVNUeFJrYjNNY2dPbVlnZU5wTjhSN1NvNWlDczl2dDFxRnZYaW9ZSjV3ZzRTbk5aMDNtS0NSWXRIaTZHa250K09iZE92dGRNRDVWZ28xbWtiUmNNdjhLWHBDOG56d2U0ZGJZdklYU1FaSjIrTERDc200L3VpQ011NktoODhiMzlvZ1lkdmY2V3I5Ung3MjFBTm1KTTFyZVcxOVg2M2g3ckp1d2J0cWk0dCtnRHpIUk1CQXRSN0g5V3VEaGIyUGpQNkE5QUFvd3Z4cmY4NjJiaGhzSS9Yc2wyOGRrVGU4cCsvWXNNMnZvSGh1NlJUWUFGNzYyb0lWSkI5M3E4UXUrK0NEMGw2SkRIVnp6ZDl1YkIyTG1tV1B6WWNEYUI3UnVJT3MreUdMSWZQQnV1MElZV3Fna2lhQ1U3U29hclZLeUppSmZZQjd4K053Ym1Uc25RV0FZQVZpRU5LVnFRU1M2TWxLSTlmRmUyN3pKR3grL2pEV0dIYitrVFpzWDNXSWluRmUxV0E0TlNkeTZRZWs5Q2p3UFY5ZEFDVkdWQ25wQjBtNThwY0FZK0RoNVJmOGliUHZqdkgwSEwyZExMS2hsTTFxV2greTRlUlpRSWVhbmZTVFIwbVV3bXVkcTl5NzFScTlLTWxaajVMa3NyUWgzV2FmRitpSFllcHZ6dUsrRFVxckRvb0ZXWUFvaDJVa0NxOExkWjlNdi9VNElHa0R2Y1JVcUtYVG1lYWtLYnU4SjgyOExUQUJJbmZ0aWtNeTNJSW5yOXNJQWUxWFY0ZDBPWC9BYmt3RXZRUVlFOUIyYmVvWVp4Q2lWTHR4cHU3cFN5Tk1GTVNkdDQvUm8vb2NYTTJRRm9WL1BxVjBUL0JXaFdXNFN1b1NTd3Jkcy9JbklObHlKbFJoZ1RQemk1c3ZJNGhFNnpSNlh2UjhXSEpIT0wvaHg1VTNFVXJiNC9ZZy9LUzdxLzA5QnBNQ1NvTit2WmZiWUpzZHMrVkRJVjBRajczUFlaMzAxWGVRdGN4MVdpcHdPOHo5Uk5vaGhjdU9PbCtWcTRHOHg1L2VlU1FRYWpvSkVHNmE2OG54dUdjOExWdFFLTHJGRTVZeGthZXB2S1hvWm9iaW8xRk9zM3Q5Vk1Wdy9maXZZbmdkL2toSE9hZytsSElrcEkxaS9tMGJraUZNSWN6ZWVsaXhUd05MYzlSZ3U5aDdIZFMxa1BrdWFnUWF1cnlmOFppSkhMcERMQVhqZWRNamMzWHJsMEYreHJIRW0rU09TWDR3WnZpTEEva3Avd0U5UXFXOWxTOWVhZVBDczlMdytEWklpeTdVRjM0cTAxRDQ1OFA1L1ZrZXl3anJtZmltVWNrNXNlRjJLSkFMMFlvemU1UVdkUCtiUWNjSzZCd2JzS3FwSjZpVUZBQk9DU3RQMXliOUk3cGp4TDAyeExhclFPbzQrYVFjTVJ2alNOMDFaRURiRkFHMUhrUlppK2RUbDliUUhyb2w2UlFUbk1IM2xoUEE0NzZuUnAzZjd0Umc4OFZJQ0dpWXpzeThPOUplZUJPeHhTS0VYNE43T0V4ZFlkd0JMOUNoZnRlL0J1dHVnZ3ZCR2dxV1E2SnA2UmEyYTFBbXp0QjM3bDlpYm1DWUUwa1VwSU1OT29OcVJMS3pvUkV4MlhnMUhjd1NUZUpPaUEveFMraWVCK2gySVMranphV0kzWnZqQXNYbzEzR2JRRHBpN1Yvd2ZKQ3dJL1czOGNOdEVTdWFFV2MvaUh2UHBnY055RnBUN1hmQXlhM3FOY2RHRlorZm5TWitpNk5rSWhlV2FmcHFiMnBpSi83eXBSRWxBdnBta0x2dkpEcXRQSzRLa3ZqZFR6LzF1UHIwSTN0TlBYcFVlK2V1SWZ4M2V4TFQxSklpN3Z0dzdQbHJlT1hwRk1hNkhKN2ZpbWpDeUlqcisyYkw0M0J5ZVNjTHVhY2FMbnJSWkFIc2Y4emd6bWxnWUVVRHU5bmlFdzFpUDRtWjZyWW84UGxuVVBIWUcvRTg4K3ZvbDY1TUtkR1BOMHVVREdkMWtHN2F0M3lncDV0N2Noa1Y5TVhYV1pLVDBUZ2JyL0ZJdW9HNVZVV0RHb05tWnBQd3V5eFRpeERKK3o1Q3dwNnNKS1VUeXJ1RlFUbEZVOUJBcnBaL29BcjNTd3lEWWF5bFp4citnazdDSG1BNml3RmQ4dXVNS3pIeFFrZkh0dk5RSElZZEtLVnBmU2QwcGk4cUo1eUdlbVJVVm1YdkxUZzErZHZsYjdCeE0rSDlIb0tVcmlSZUxVSXdsZ0hiVHJ2dE9yeXNXdXZIMmNsK0ZaV1drV1ZDRzhseWEweWRoMklsK2Q3eGRqcDVXMy9VVlNValV1bDhnNXMzS2NvS3dzMjd4bTBnUElEY01ERXBSM1dNUmFKR0ZRbzZDaXgxT3JJcnUzM241dzZ2ek9jdUpFUm11MHEvVWFrRzE4L2x5bXlNQXAyT0lPMHZIVWYvcVdUTEdMNy9yRGIyMW1Sd2VPNjlDc0xzTVdhNzlaMFFBeUduaU9laVJXajFlcHV0Qmg5U05sTlZwblhUb0lWWHJHc0FMQ1ZITGw1NmJmd2Y2K0xMbUlQdW41YXBLR1BPTXgxS0h2eURiYVdaSkQ4bmMwWm1UQkNzd0xCYjVwQWY0VS9pZWJucnZVekp6TjZtSnJxTk9PNTJGRUVyUGsya2hHQm0wQXYwSWhrRjZFa21Ndit2bmRFSlo4dnhKWDNBUmtKazR1MXhPWXlmOHRRb3NJa0pOWXhTZlhxM29iOWFZOXYwb3dOR0FNdHBXejBmdzl5Y0c3Yk1GL0JBV1JIWlZyWGdDY01FWFZ2TU1reVVmQW51R2NZVmE2VVlYbHdlekdsTHJSaVZwK3J2aXlhZUNHMjU0eUR6WnEveStkZThXOU9PSEh0c1Y4Y0dpNmR4Mjd3QldEUmtvRU41TFFjWnMxelBEalNQcDRCdmJKcFVvMUdSVGNUZVhYTGs0NUliV3lwMWZlL2tVRlliemhDRWNKQXNkbXJVS3JpRE9xOEJHMTkza1pQa0ZmVHBxM0dRckxzU0JwTGJVT1FEbVUrMG1lakh5MXg5K29FNC8rRWQyRXdtb3FBSkd1RE9yMGJWTERUc0IvQ3gwdW5OSFhObGRIeG5obThOa2Y0MDJCQ3ZFekU2MWpiRHpOSFR3UmViMUkyajRJMUFrcTA4ZGNEN0xyY2d2NEIva09udmxJWWNWcHUzMU1jb2dwRmNqMnBXUjB4SlI3MzM4cnZKQ0lvdVZIUHNta0U5ZWxxbFlRdytKRng1THZMMEN5UU5KZFA3bmV5N3g1cERqVXp3dmtWVFFNZCtIM1Zub1V0Q0s0Yk0zZXlSdmw5bUNxNUxZUGV0QUNBcWhqNUxyQlNtNzQvZmtoUmx1NkFneDJJOEFBTldpSXBkY3dsT2hiOXRkeTlYSldBM1pUVDNOSkowcXBGVVZwVEx0K00zTE9GaUlha1loWks2bnlUVWhyM1pJajA0ME9ickptNWo0UzRCRFlRbmpxWFBxbEc1bGFyeE10WVAyWXVaQmRoQmhlWWRsNk1WS01WeXlZd3FNK0h5VGQxVjlscjFGTjVVWklzWWI3djJXenFRQXR4L2tYUTlzUzgzQmZYRHdkMWQyM29XWmRud3A1YmlKZGt6a25GNXNUOUhtQkh3WlRjR1F4d1N0aURxOGEycExkOGRHeUkvTEMrTDBSMnB5WEV1THRDZURxc3gyejRJZXhjYzVpOGJYRWo5OGdWMnFxVXVWWmRuZWtmK1M0dXV1eEFoMHNORy9QWUJMeGZZMmRUcHg1VldJWjVuTkZOTitvWHppSFBzWmJ2d05ndnJySWxCZWtHQmpjWWF6VEh5U0k4VVNudE5RZm1tbm9jWTRPdjh6QmQvSkQrcDk3NENHQWRmSnFzaHlSZ2RkMWh3WlhIaFp3RWdNK1NhK3BJcExDdEg0bkhuOTJLbW1CakhTclgwRjdOdmRvTVRxdmZGZ1FxeFNyVDBzWTVHWEFrNm1YaGZIUnZFdTlMblNzeWt5RlFadmpyVFVBdGVTSzFnYXNLVS9ZNmozeTRrWDFVS2ZoSzdDcTN6MW8ySUREcnVLYnZmUFl6WWVBN3JKQmZwTGVvOVdFNG4vNFV5MGdUR2F4UXAxSjZiWG9vS0Iwc1lqdWRCRWJlNWlqNkFERG9lcm9pNkxkZ29TRyt5OG5mQUJuYnR6ZzBRZnhSa2x0YXhSNDlOK0ROeW9ndjQ0YTZEMmNRZGhLbElRUUZhcEtENml4THRYb2ZGZmw1M2tFZjgrNW5VOU04R1ErQWlqS0dpN0NYY1BhTG1GbENuMlB4MVFOUTE1a29LUm9CL0E1Q3lyM3VJMFdQQWUwQ1dwaTRVQXBkZGJCbDVaTUtzUk5HVFAxQXY4SjZkOXpnRkQySjk4eFJpUHFJWk1IakpqZVMzTEpsMFlGNTRXSE9PYStUeitqTEwxTHRaRmR2c2MrRDZSaS80S3F4VklHQUI2TWJZRFRVbGpnU2J1d2pxVHFBN3Q5ZUhDbzROTlgzVWdBNkYzN1JobjdOSHZ6VjBMZzhXbGZ6bHFlc0ZvekpMd0ZRWVF5eUY0SjNuWndON2t2S1dEVGYvcndWSmJlWFpJNCt4MUErZWRKRUJQZTVHTldsbFZOb2dZL2lDMzJhQU1malI2S2gxQytFSlQzZWJRajZpdGYvQVpvR3NrZk5SdlZLTGQrZk5uYjRYb2FzNzhRbTRGTktKUWtPNi95UDc5Z3FDRjVoaEV3OSs4eE5idUdWbmNrSlNpbWJNdGZiaWNTWUJVRkdudytNQmNWazFvbTdLc2dEaDZLT01LT1hFNSt0OHNYLzNPMEZnbEcxbXBjOHIrQjdsUS9Nemg1Uisrc1prSXRGNmtqUFQrZHdtczhjU2hkV2xGeU5rRWlzZUdScmlIbEM4ekRsekNIblNEZm1Gbk5MaWVnR2t1anVTWm9iN0pvWTd4K3F3Yi9hUkRnZVNEQVcxWVpveFcxTEUvZWoyTm9ReG1UNXpUSElGY1ZyeVlHOW05azR5QXh4NUsvYlgyS3Jzak01V3BZdjBCSTI0RnJsWVprR2xUbHpYYU5QVTlmY3ZrOWNFTVRSWEptSCs1THRMaFk4UUllNmFGY3NvalZGZ25DdEdJRk5rNEZkVzFzd1VieE5vUUlWQ3cxU056OTUydysyVk1qb3kwRzVJUHF2TmlSNHlQL2Y0Q1JkYkt3OEtISDRManJBejlZT0tpVDloa3BFR2xRUXVmM2o3SjcyS2VSYTQrZ1c3T1RKTEJIQnhwSVZGWkYyK0lCZUhjKzNwWlA5VHBmYUY4QzVheHpDNVVaLzFlR2xDZTMrTTdrc01hdVFiekNvRUtDcXNpKzFnSjhoMng1VHAzNFVVV1hNdGVEbnJtTHVJaVVnV2xjZmZVUVA5K3E0c04reGxZajZicE1RcXE4Q1hoYzVZU2twdDIyTm02Y1UxbldsaEFPS0JraTVOTm9sN1JZNkNTVXRuV281UjRjNlpLU0NPbXVqZkVIemxWUytTVXFwWkVDK1FiNGoybUVwaTJ0V0ZDSXZnaWtmaDM1eXNmNlRYYjZDNlZqR2oxTExVZDdyRW5MYjZES2ZsM2NEbjJXalRHNGpDVWdDQnJMR1BKWEJjWTUrMGl5SHE3YUJ6TFVUODdEbXFuMFgyQW1Nak9Nby83cksvekhrNWRieUhZZ2EwRXNlRUl0WWdXa053Q09JOUIzZi9VcmFBVDFDVXhwMjBQNmk0TmNCQUNLcFdPUGNOeGVXOUNDc1BvZU1UTWxhS1IxTEJ0YzltZW04bUpNL3N4OEVrU3YrdGdFUDlkcy9ta2VIdTAzQzlKKzJyNmlySGhrV0JBaHQ2WmF5SU1GVXV6MDgraTVWRENZeVZoTVNVcGp4SFZZQUo2YUFGTVpjWU93eU14djNNZy9hVS9TbXo0cWx3S3A1YWxndmJDTkJqYmxDS0tuY0JLL1BDUHNlTDVsbjVFb3R5SnFZbUJHakR4cEp5TStGbE92M0RiWDJyY2NIeG9PZVhRVlZQTWJIb2E3SXB4M1ZpcW1nWUhMT2lFZ2h1UW13bjZ3bHA2bVU2bWRWdjE2cTRDYTB5R1dIL0pGTFhWSzJRWW0yNThzdUppRk9LWHIzVDI1eVU4dVpNSjVtbWh1QkN3MExjUFZBM1p4ODB4Vmc5OHJXeFlQT1dLRWlXL0NWY2dHbVB0VEExeTAxeGszcGxNTU9HOXdWT2xJNm1LempDYVFDVGxaMjFlWnZmUlpCcVVFNGRFb1JuRmxEaFZiWjNjZmdybE5MV0VVSVFFUjg0Szl2YlpqM2NPWi95L01YSXRnTVpIV0R0NjVYSmhFb29PNTFQeUt6WmEvaEIrSlJ1NFNEM3FlbDVZdEg4QXp3RVNDaW9nUFlQbXRDVU9naWs3VWNWVmd2YXd0cStaTW00M2hXS3F6KzRRUExQOHFVVG1oZjJRNmtmODA0c0VoZ1BXakdOc2lBMzFaY1hKNEhPT29tTC9hRmNZMnJYeFY0WDJHMWxNL1RNYmJwSGNNTTF3QkxpVzNhOG5rOWh1bFRDYW1KS3JzeXVTRzE3MWRNT3JNK2JPRlRZd1FDY2hsdFBRR3E4ZjUwOEtwanE2R3JRdENrbjg5QldHOHBtZlBBRW1xQkZtRTVkM2U4b3BJSGo5OXBYamFxdlNKd1QyT3d4ZHR2MG1FdmFSYlkwUGcvdEk1Q3lRL0xQSTE1SmUyczVkNHpYYi9kNkpXT2VCVVJ0V2tBck1FVDBWLzdhR3lnaWVBbkx5WUora2l1aVZVUzhxNFdHS1lGRWRzL2N0TkdTZDdKS3lRL1FZd0ZGQ01sejlYL05OdWs0WnRYSUpsR29RWVQwc0ZWQTRNTy9odjhwV1pQb0I5MG5LVnRQS0plMGxUd0ZVVWE2RzY3WkRMU3NNeGJMa1NMQkl4dEk0ZExKZVZPbVVGdEU1MjZGWG4zU0c5ejNmNkN4ZEFUU1NITGRiY1lkaE85SGhoMXU1alo5QkJwYlZlMFQ4ZzBDNzNnaWxacHU0YkkvMk1waU9wSzMraUdWckZIRnYxVm1xaDJoZlBTTGpyQWhaZ2dMR3lDWUtJTXdKejNkWUNQWWpDZVJIaW84Mll3UmZtNUZuT3IzTUdJWjlwUmwvekl1dHNuZEZ0YWs0bkNQdHlFbXNjaFdLQy9sbDFGbjBaRWxhb3dWcVJBQi9WMXJYUkM4Tk9kU0UrTGlnSWJSSnI0L0xZU21ET01hYmtOY0EwZGVCbjBJY0Q0aEN6UzZ3bGtFbm9tSnNBSk9pZjZhUUh4RDJRaXNBbGEvNDB5UFlDVHNkRVV0SEtWTG8vSW9lcUVtWHVBSldpMmU2U2Nlb1c5SHlGV3g3c1RYYWk4WW9yNU84QTVzcGgwWGxtZml3K0dpeFErV28ydkJiZWVNSlpsYVVpOVRzSHdESEx1YmpaL0FOcS9yTE9aMlhVSnpQTkEwZjkwb0R2TVBqaEFRNGxvZGpMNW5VR0FHZ3QyUmlWTkp0VVZQSVFJT0FWNG1sd0M1V0lxeWZMYUFQOWR4b3krVUM1bEhMbHlUKy9SRW54WXlBYm5aa1hwazh2dkQ5UUQrRkZiRDQ2TFZpdE1xb2pRVmFTYUp1SDNKVitEb3BQeWZGVWRBMXZXblIydGdhY0VGRjNMaVZHL1hWeHU3Y3hLaXcvTFFJRHlJSWNkeWdhS01MYUNpdXVUYkFJclNLSE9kYzZXZ1piSTNOaVJPRnJucjUydDdTcElsSEFvcHBmSVQyREl2RHZKVUpHeTZWRTIyWHlSVzlVSGVwRUk3b1dpeWs5N2FBMytlMkdFT2svUVRxRUxsNUZyRndBbHVqeWZMbVNGeU9BZHpNTlM5MC85NWh3VFJ6RWgxbHhUL3h4ZUpQb3c5WkI1MGo0QjN6aUhzSmlJRGFvVThLZjQ0YlRTc0p2bGdyS1FaQnJIMlB3MHNWNkV2UHdWR3JCZzMzTmZKZ2h1aWplSGo4VVZuMEFnZjJQVEpsMDczbW5TNGtXYjVDKy9zQ2Rxd0V6dFZYaDVIbHFkVUdwbDhmSWNhb2RnRHM5N2ZDZ0R0bFBoMGxIRzZSSmdCaVdSV0Z1RnFHZTBBa2NjZDRDem10d1Ztemh1a1dwdEZ5clBvRTJEVmRHdVJLbWlGY2VOVVhOMTI0UzhxS2lHSDFiSkJvcUlJOFQxYUpVbnNDbWdnamFkOHlhZkYrMHBHUmV6VUgwY202OFBlK011NHVzVnNEbG5GYmF1VE5rWEl6MkhDdDcyaWpqWUpmRmh6emo3ZFV0aEFnTEUva2RKMU5uK1diUEdOajZBMnN1UFg3SW1iRnBKVWVoQzNDek5tSlpNbWYvV1J6blV4cTI1bmNxRzFJejQrcmhndkhUcU1ydzJZN201TVc0UnpHdGZpODI2UlowOWRGMU9NaUdlb0ZxRjNjZFpMd2NsR0dTV0lUL0dsb01JRnJKOTMvSzVJRC9Xc3NBNFFoa3FoQnV1dk1sV3ljRXhwNU0rRkNBSThzZTFaQ3h2RW5WZm8ySFMxYndQS1lNM3F2T2JWUnBSd3Jta29rNUR3RXhsUWFRRWR6dEp6ZTVCZDhwNVcwaVhLTWc4WWZwZ0FNSFMxYzZSVXJndWt3MEg1SnJKRFF3Y1VOMWhuOTN0cmdzb2hKdTEwdTNnYXNkUkdSTHNTU20rdWNtalNNdGhTT2FSTEh2MWd2dTJXOGVpVTFwOUZGaUpLckovdk1SOEthYlVWZmg0MUF4RlpEVWd4ZGs5cGhPbkNvVE4wZ3hPQTNLOFN0VUprLzBTL3Mwc0VldHVkQlBDdzBlTkNLbkdLcHBhTlVYejUzWHk5K3VqNi9QZkNlREpucWVUUVRrV2VBNjEvaEE1M1g1akVZZmtBMHpSMm1Cb1ZvUWlMQmR6aXpaOTdHTVdrNnhrdWZsWVBUTUdiSngyd1NtbFllTXRsU0NIc2JvT1ltcXpHTHlPMTlnZFJ3NVBXREZrZ2QyQjlkS1ZiMitPWFBpaXlhRDRVV2hIdWM1SVh4OUFKTWkxWklZMnozZnEvYmZLbVo4ZlFwWmNqRk11MkdtWjVaUXhWM1UwWS92RHF2UjhmYVVOdElGUTFGYVFlRGRpSzlMM3hNWTROa2xvNnRpekUxMURFNXhvRnZnMUlnVUpBRUptSDZMb0ROZ210MS8wQURXYmYydlovSDBjaEtzWTBzZlRwMGdpcHRPK3lTMnlDcHFLd0xGVy85MUQ0Wi9uNUZqNHFVSUJpNmxvMko1UlU3YkpwaWZpSFlocm1JLzZDQ1RHSW9ZZGdGVk1rRVJOZHloNFVTM2Q4Ni9LV0NHMDNDemYvcFE3UU9adnNqWUdwSE52L1VwRm95bHJjV1paREtlZDdsbWpndkpQNlZUYWZ3NVZ0a1JsN1JEcEwyTFhCZ3BtOGs1QTB2Nk9VanJqYWVHcFJmd0FnM29NKzJWdVh6VnJxbkFXYUUyUjBQZ3QwZXpzQWlLU2FDditLL2VhVzhkaXJCRmZSSnJzb2JKN1VYNGoyOURtdzVQdDMrMTFsckVuNkYrcDA5RWxpY01tSkt1WmtXdzJyZzBZeGJOQklqWUQyV3BvZ0R1Y1FTM1p1VW1NUmd6QkRSbFF2SE1WdkllRitaMHFWRXVTTldNYjJrbllZcjRJa3lIRm9ielpTUGQyZFhyaGdoa05DUGxCYTcyMUl0cGlNaGZBd04rR25SdnBzZ1YwSmdWUkYrNVhNTHA2eThxZzVpL3h2TW94MFlodlVWODdWTXl4QmtZc3NRUXRPdTNCOWg3UGM2ZzRnN1lnZjV3OEx4T2Q2Z1h4dlM4enBXbkZwbXY0VERLbXVCS2l3ZzFzM2FzNzcxWStIb09mdzZrb1FTVWVSYkVWdklPWHNQeFllNS9nR2JSeHkxcXp2clFBOHhVU0lUVGxhNCtlWkUyaGZLTjNHK0Q0Z28zaVQzMVh0ZGxmbHJKUkxnVjdyaURXQU5UU1p2eSsvQXE2bkVsZUFrVXNUMU5Idzh6T2o5VGNNdzFodVRIU3diNm1IV3ppOE0vZ0VtQ3Q4alZmL1NSelZXSEQzQmJLQWp5VGJRTGpNSllGbmRkVzBYMEFrTGhUVTRMWmxWUktYZHRmd0xFVEVxaC9KS1BySW4xMk44N3dVMzNjVUFGTUh5UHN3cVVrMDBlZStNNUQ5cjVkQTY4YzNIZE1OZmY3OExtbTJLenB4WVFJSi9XKzBqVmREak4yZHV2dlEwR1AySkZmMUtYby9wS3dqS29zSWw2NzUrY2dlVjMxOVRtWUVhTW9NQmsxZUk3bjBHd1Y2c3NXWkI2SEhrM3V1WkxyN243VUFlbWFjNlAwZzFmQmxleHlDdkg0SytReUx0SUpVK0xsandXR0JqT1BvaElybmJTUlpGZWphRTViTWhMczI5OGxSZGdOaHh3cTZod29qbXVFakFoYUQzb1NrdERWQW9PS1k1ZDkrYVBxNVVwL0pmTEtGY0xTeXNDY05HMlJMd29naHNTeFc2aUhHc2dObVlIY0VRTFl4THJSZU14WU0rbVRQZk4yazhEU2RSSUxSM3ZCWFBGNHFZZEczTGFlLzh0b0VPbVhKd0ZGQjBNTDAwcHNWanlUdlYvOHk1Q051WFZlQnJRNjNhelprT2lQMkY1dFloWVZPZUU4QzVvcGJNdHAxSzlXVE4zbUF3RUgyaWF1NlZnQTR0R3Q5R2JyaElpSHZwbGlIeEd2aXJESWhKV0MrdUlOWnFORlMxNVA0dGVib01KcS9LMmJNa09IUWROU0hoaG95YjlUbHdldDJVeXRWbkd0eWtnNDdOR2NwMEpNSWx3Si9GRnFGOEpCYkdDcG9VdFVsMTZSOVEyem1WMWZUVWMwOCtKd3dtNm9zTFovemNXRnVuWlNRaDNEcTZSZ2RqUWlnSHFYQ2pldnNyVThRWjljWTJGWmtsK1g4R0VTRGUvTktDVE02MXhpZU9rVytxZitMbktUTk16NWxGK1E5bHdGMUw0VXFibFBacnRtaWs5cTlVZHpTa2M4R1hRRnRaUjd6UzBDd2tYbTU5S0wwcG1lVTNWQjdjbENvM01VbWN1OGpKUkZxcU4vZGZ2YnZQRnpFVk1WZUNIdDl5c0ttSGRvalprbVFhV1JuSWluYmorYUs5ZFR5TUFSNEg5Nno1MTRPdXd1bHJWUGJuS0F2TzdLRDU0ZXJkUUpEekluU0tZMjhwVTZNdTVJNC8zbk1VSU9wcURONjBDN3JHb2xzeU1zQlFlMEtDMmh6U1pGSHJLN05VdVlvVUZ1Z1JoWkxaSThDWG02cm1OSk10S1FqZHZkOHNDVU9QT1pVY0MyTVk3OVNqTGNxT2F6NFY3eWg1TnpFeEFvRDBZZUZQdmlZWXVCNjIzdEZWYVN0MkJ0WTRaalhpamNKMGFuZlYrQVpMeXM1ckEyM216Q1ZDTmN5V2hiQ1BxVU93bzJGcW5ZUlhtWEJZZG9zSEtLZGRrckZYL0dqMG1SSHBCNWpkR2pmeSszdVRScW9TTnZTbXh1cEVLUDltMTkwNVFUcWJUNmxxQkRwS3k4WTdzTFY4eDB3SEVpQUJsTTVYR2VOeUlWc0xlcW9RZU9ZOUFheG1mdnQ5Sm1NVFR4MlBpSGVzemdSc0VwOTNqR0VRYjFhMjZscUlxOFA4SkIwUmd2MmpYdFd1aGFyVmlRaE1CQ1Z3ZFJPZ3NpTTVLQ21GbFRIT1RZaVU5eVp0Y01SbGdESVQ3eFlpaU4zYkFUSDVyMkpZdjNpWlpGNG9aN3duSEtIdm1ZV1hTU3VhS1NDTExFaGNybk96OXZpaHZVRFFrUVUzNEtULzQvMFczSFZzRCtHNG1tbkZrejFwL2QvZy9oTUlXSzhzWWowR3Fma0F5MXNFUUY2enFNV3ljV2RQVkN4aWxsbS8rSWFQTmhMOGtSbTFLL2xkTnRtVHh1Q0EwcUIzS0VKQ1dEU0lBeit2UHhqMUZITzZmNTQyTUlodWxWRUdoSEhVSEd6UGd5VjBHMzBxdjNGZUlxVU1FTGR5eXd6UURqV2F0cXFEbzQzRmJwai9JNmJLVk9QeHQxQ0p5VEF5MVB1WFdMNzdLeXVLWlFjYU9nTWhEYWFTL212T1gyVi9peEZDUWFLcmUrM0Q1RVN6VFFaRWZIbzFXZjhZR3lQYU5KVW1jOWt2bmtpOFAzMFUwdzlWOVVMOThzUHR0T0x2TkFaaTkwNy8yTW1yc2ZVZDZtZUJhMm15U0VaQUYrQ1FMTk96ZEFUc09vdDBDdWpnSTFkZWVzRGpwa0Y5RnRrMmhRZ1JxYnVkeEZPdWZsUWtteC9rRHRJbDk3dUo0bXJ1bkhLR1lxQXNSWnRSY3JjTWxxNVVQbnZwcjVHTmUwQmlCVTg5M2JqSXFUQzdXUWtBcWZiSmM5WTFrcHRPZkZ4ZjVVcUhUSjJBUVpqazRQaU95a2JYcmltbm1KN0dqbHl1OE1vN0pScVh0SG1kTTVva0c3bDlUbmY1UGN6UlYrNUprODl0azhQenp0SUMwbHVqdEhHZS9FOEtPbEZpTkQzR1Z3djg5RWJvQ2hZR2hRdFMxYmoxYWdiRjdpUXRPZHpUbHRRRWcvbklMbHhYZUlFeVN6SGNsek5DeUM5b3M1LzZQMWJsOXBDT2hlSVhZbGNoZW5IWVY1L0ZFN3M4TWVpcUw4a0JySkI5M05BMXNGS3BBaDNzTjhzaWlxNlF5Tk0zTzBoU0RzRUxOS0tjeUN4TXVydWMrcStBSXhhc1p1WVg4SnBYNTluTXJxNDczUlhxdFN0VGptR1E1VWhmekRzNWJTSzVVNTdYWXRxYW5sV3ZmZmhCT0h1ZVJwNXVVOWh0M2FEN0twM1VwWEk1SnB3OXJBRFlMeEhBbitRS2htOCtIZVp3UFQwMm14ZnFiQUUyNktZMlk5OVI2M2pET3lzU1BPSjZHOGlXMDR6VHYxcVJHKzBiREdpZnRQOTlMaHhod2xLVE80Z3Z4NmViTXplanhFT1NpdlVERTNpdzBiQkJYaTNiMi9JZkd2cjhmYlZjSGcvWUZUTUU2SUF1RnJmaytvRHN1QVRTM2UvQ2JDSk1ZeTBpOXEzdTVOTmViUVpUYi9LKzV6VTZkZGtCWnpNWHozMVR6bUsyNC8wbktsMklxd2tFWFF2eXlwbmpoQzEwRVlCTENNYjZyeUVUdEV0aDhjZEVLVW91cjYyRnhKbE1xZGtFWGplZnRiYUlBSVJrSUtldXRQSmtuUHk1NnlRRGxjc3c3dTFmNzdRNUVIeEtGU3FIUW1TSTF5bmhpc1pqWXFTZTNMdzBtTVRpbUYzbWo1VDdhRkp1dUtuQVRVTm1oT1J0VktZUmIraXpTem5ZV1JrR1JGWEg3bnZ2YmZKUFc3b25UMzlxQmx5bFpkTmcySmtPYXc1cGs2OXQ2a1JxV1lYVzlnMHY1WVExTnRxY0VnMlZKRVd6NzE1ajZ4eERaMCtvN1RtWENWRllnUmhqL0tBSE1kejBhZGxBTW9VZzluaUcxWlEzMWx1SkRrRGxuSXdwYzNnSWxjQllQR3o3K1E2ZGc0WVBJc3Bzd3hDRG0vRjBhY3JMSDcrTTlUMlhub0RCWG9zNmdsakZSZnlGVVV1Y05tNzNiUjNEays4TTd3NEhsYndBRzZmNGdyVE5keGs4NlQzMXJWNVpabVJLZy9yWnJBTSttc3cydnVXYzNrQnhzaGVOdXlzNU9xaS9ENmJkeWpWWmYzeHY2VGRybVpKYmtnY05yRnZWZXJQbXZkMVlvbzRLVDdIdHhOSUhzbVpHWkxZdGJweGdTZ0dISnk2THk5a3lvQW9MR1hvRm1Iejl1Yi96eDRyK0dsRDBPRE9RaXBreGRhMDlrc0NmYzkreDF3U001Y01zNmZJdzVHMGZiUjFiRlJLMS9iU3FxN0dOSjJKdHFzMkIydjA0emd1U3VsSTlFbytnMHNQZFpLeHVoWlhZN3QrWU16b2tYZ3NhWlJEUFZ6SXAwNndiSllYL29lRVdUTXhlY084ZmxKdWl6Nm5LbnQ5SzZ6THgzT2dlYUhud3JLekZ4UHlhbEo5ZUFJSDRFM0ZYbGFtVHdrbDR2a1ZjZ3AvNGV3dHpRUlo4N2ZuWlp0MlozT2lROStQZlRIVEpvS0QrZmRETzByUTgrOGFVTy9ES1pmN1B5SENnbnNLbFVlRHN5SFVadFNMVTU5bXU5cXdhMjY5OFd3NWRRMkFvZStVb2cyYWFVL3VwV0lIUnJyZnNCSmRzS0U3cWZLTDA5V2tpaFAvQ1hRWmMyNHJTNndJUE5wcGhwTkp0MmhoU1UrTlV4aWRvNDlZWGh0ZHkyYkUxTW9vWHFCVzFiRzBTODlnNGx1eXlSRVB2YXMrZmJEMlZicTlrVVRKTlhSZkx2OEpKdDJRUzVqaS8vRVd2SDhmZXFvMVMrY0ZmTE1zZ01TNHpabWhoQVI1NmNZMGFJQ2dZQzluNGNsbXJOQytVSjA1eHBZN0M2NUlkclBaMWhSaWFXMzA0YkZnT05VNTJtS1R4QnhmWk5mVWRsRGxYVU5yaVdMekh5anEwWlBydW90aTMxQ2toMTNvby9vSy9RdUY0a0IrUDNpMmZxSVdybHJWb3Y0QkpoRDBxK0x0OUR2TkgyNDJJeHBQRlhST283VGxubWNlTFdDd2paQVBFUUlDMExCM0w5UklFT29oVVMzN3JWdFhhZjN3a2YwV3lHU1l5QnFmNEpMNTJqbHhQQmlDUjJSWjJuaU1aTDFRckhoVVpHQW5vTHhVREsxbFBNMEd5MFpJdUc4bVpia3ZOUUJQNnNzL0xUUzY3cFBIR2lOTU9hVGo0eFZadEVTQUx5OGN2ZUg5VnRWbjY2d3ZQQmpwU1hpMWdCa3pzM2lJVkxrSnRFU2JGVGhKNU4zOVhRc0dORG1DQWh3bFk2QzVjWXRhVnRaYWEyak9mZDFnRFVYdWJkWlpIbTFkMmxUOVl1ZWxqZ1JLSXphK1VJQ3FLMXl1c2hsT0kwMEpyVmhBdUJtWVJLSXVFMVYvNmRlYTY2WlVHR1Zad25pcVl6K2VEUitzRHl3VnFBYmtYTGhBempEalFGN0ROS3ZFd1MrWXN4TldDRy9ldklCaGVQaVpISHJDV1FPdzg4eE9DS092RzgzTExsUktySnR5S1l0ZkZmRGpMVnFVM0VyeU5lWEQwTlJncjNzS3lJM0liNTBLVko3SExHSEJsVVNzVU9NY3I0RG5ZbS9NcS9sTG1iUjR6WFVQTDNleDAwZWQ3cDAySjJNOGVFZXEvY21wNGJ3M1NIcUlnQWl1WE54QldnMzYzbkpXd3dkOHFRUmNqTW1aSlQvY2J0OVhqQWxYTTlhYlBhUlh6Z3NzKzdqeTdkKzk0UUpPNTlHRVVWYUxIMnNrSFVhNzRsd1RMNnliRGRVRlpBMDYxalBpeDFMUTI4c1RNK2NROFBZZWkzMGZIMisvRVFyL0k0U1VQU0pMSkZRazFrekJqSDUxNWtacFVFVXJ0bTRZdjd2Vit1MTkrVDhaS1NpWDdFdUttVis1OU96ZnpLMG0wSHNOcmZpM0ZhOWFVdkxFUG53M29zSzN4OWxJUEE1aVZoam9ISXI1LzNIQ0dUWGdzbDlrL0FoT241enJpd3RldURjdlhwZ0IyZElha0ZGT3MyTHIzUjlzc093cTY3dUx6cmd0SFNwWnFwWk85UTJEOFRSWU9ZN0huY1k4TDVXdFNNc1crTnhpZGRMSUpRdDExYnhKRThrZ1hCVGd3ajB0VTdLRzJhYVJUOTY4YTJvcmJMM2hXTXh2OFhtaFB5ZlZUSDE4cnV1QjlwcTUvOEpBczZ1WXBYUW9iYUIxOGhCZnJvODhsS3lpUVJ1ckRKcDZ2UGpjMzZjMUhVNERCMDdNZW5GZlpLazNCUDRPdGM5VU5wYlNvNzRNcnZ6MmlXakJuaDZEa0F0TFlKNjh4OEVUbURVZFBha3F4cnN1Q3RablR3KzhqZzhNTWx5Q0F1QlloeFRqZ1VNV2daYSsvaEI4MHRhZm5DMGFsQWFhNVNCci90bGlML0FweWxGbE10d0duMFdHSDczN3U0R1MrK09zS2dpZTB5TU50M0FwZmxRcDBJVDIyM2crTnZxOTl4NXhTVTZWR1lkUWk5WlFUYy9yVERkNEZNZW1CUDAvVXFnMG1wUGtteDBNejZOajhETjFUUmF0ejgrbmxORzZwZEJjMlVBUDhMU2x3bFBwNlBLYVo5Y0pkaWFQSDFLbno5MmdOd2hUekkxempkd2Y5cU5rMndtaDFPMVJ3RTQ5ZVZuVGFYZFJ4bmZhd1haU0tNcHdLM04zeVkzZ3hZamRleUhLQnJYUTk2bWRRUGFYbVN1WmtXRDFLckY2V3pTT3JYL1lTMm9JblpXZWcwVEJ4ZW5kS28xWDkvSHJUR0pUQWE1RnF1L05UTklUYXhRU2ZmVmNEaC8wTWdWR1dzZFhWRTNBV1lzYlVjQWQrWjJ3MkdhL1N5ZzM1bVA1aG9NZDFjb0s0Rm4rRS9qd3ZidEUzalUwSGdqUytnZFZhRXhPN05JdzFUdm5QNE8vay9TSWwxbm1RdWlpdEMvMkYvaytzMG5ib3dYcWljbmpsMHBvK0puSTQyM1RpdDFHQjVnZUhnTDVqU1g1eEEyKzhnQzV1QkdYY2Q4eEQxT3VwVlVFNmNsZmtLQXR4QWRMRDN6c1Nxazgwb3VhbzR5VTR2TkovbXZNbGVWOWllSThMYVBPcEltcFRJVUZ0UE14YmJuOHV6cnRKTW41RFNNcVJJSDJodi9yMkdhb3pwL3N3RXZrZVJlT3ZQTVJnayt4SG5mK1FXMXNMUWFCL1V3ZkpPbVV3MEVmN0pUcURvUUc3M0VoYTlBVVFXbHdHVG92Y2xXVE5FdVc1Y3psK2o1RTIzVTJtYUxUNmF0bjF3dEZtdW5RWnR6eGpyZnlCLzl4TE8xSUQ0UGlUWm0rc1R3UWhtbGtrbmJqN3k4K2hmSm5ZUm84b2VpL01HKzNyWVEwdlZsMW41VGo1VWpMMlZjeWtVazNsK1J0UW5RcjlYellEcHNiMmUyMlJGV3lJNWRURTR0VkRJK0RCbExCWDFhTjdUSVR4dTFFY001VlRzeGFSb3BDREg2WWRkbFFVTzlkZ0JVRTlvVjNySGJlYzNOWjlxN0VPeVBqZ1FwSWFGWlRzV09TZlFucmZwakdQanpGK3JxWlBQbzJFN3JUU1Z0UUhFWUcwWk9HdUd5OFZta0d5WDdka2hSZ0VUNytsL2Z2Ynh4KzVtQUYxSmtqSHN3b3pkcUtmczlqd2xMYWlCdnRBUmJpa2szbDhFTi8yYUJ2UEdEa3RyTnZvalYrTkgwQmQ0WUNqMTJ4alZGcEZva25uaTF2UlpzcDlSNG5PU2haZmRnb29nc2R0S0VVYVB0ak5pcFdMeEEwa21SUjd0V1VOeTJKYW9ORjRWcS95Q2hzNW8yTk1UYUVSUjZ0bElaVmticlY4Q0xtdm52b3VsNm5kVGlEdEdScEdveDBqOFJOYVByR3B6VmIwSVc0d29xb1JmdE5TMXNNOXI4aVc0WjRjVG16aEZkZXUyblcyRHJJS2F2Z0ZkR2xCUTNqdjVYdER3SWhCWkhXMUk4SjlyaXhoeld1SEtNMXJmRXY1TkExNTMvZUJIdWYyZ0QzNm5sMzFtRjk2WVA3dW9LNnFhQUFYODVFbzJURmgzOXFpK3FYcGxERVB6bG9VWDNvMkdyNDk1VjN5K2orTnFFa0JhOWx0bWxRd2piSWZ4ZnVwOXlyelEzUWg4bjEzbHU0WHZSa0VRNENFNWdQOVljbklsNS9VRUtQTS9KSk5YbDRUMjcxZXA3ZW1BSlI0WkdWajM1L0xEU0tLRWt5VnR3aVhXQmZsVlhWckxLeTBQVi9nNlVoanFmOElqU2U4OHJXYU1lSHhkOTBUQk1yV1VZWWhTVFNaVENhcTVxZHJsNHJFdHZTRCszMDMxOFlBTXhBeW5NcEdJQTlmQ1JDZXR3NzN4ZnhlRmd3cVRqaHJFRFNZdmdtc0lwb0JPRHMxZnQ0R2h1b3BDSURKckh2dnlZczZCRThXdDMxZDFGTHM0b3RHL3BwT2hubzk4eHNFY0FsUGkxT3ZCWFJPVEtud3krb0x5dFVHK2dFSG5NZEQ4blVSL1FIMC9kZUlUdFBQeGlMU0U1azI5OVJQMXhWdEsrdVJtZ1grUHFXV3pNbFRLZ3pEY2hVeHRnRDVNWk4yQUpkenpaTzAvVkhIUlV6eEZ2bEgvRVJTUzdwQ0tydjhGMzJnbzQ3N1dDc0hWY0ZKWlRIYWJrcDFqekhibWNIL202NURQMnFrUFJxM2lSOWdRZFB2ZWNuaDdlaDNxak4wei92Nk4yb0RRbzI1ckYyTHY0dUk0WER1clBaYWFBZFRqUUdoUnczdnhvK3dCcGJUc1l3eVkvQXVoTEFzcUxQZ3kzM0J6a1RNWW9zOWJPcjIveGJ6ekFlS1pnT2lHWkRBbVlYQkxKQ1h4eTdHK3JhQjg0dlFJTmJPdDJlSFF3M1F3bS9oOFNkaVcvMXI5eXJVcWROemZ1dDdER05JczlSN2Y1QWRmQUh4UGkwRTZxdE1wRE5lclhuSVFXZHY4RkFUT2dxM29SZ0VPWW5tV3FiWG5POFhuZ1lFeklDSVFpZGJRVmwxUnBBQlltc2t3OUd0M1gvUTVpMXJyS21XQ2VPZE94bVMrK0x5ZzYzc0gyQlJwYVlwSVpocnh0K3Qrd05Zc2p4bHZtdjhrUHdqQlNvTmdQSEtwaWJXUG5rR2ZYMytsM0g4WS9Ma0toVnVjZkpFWEk1UVdQbmJsalJKN3ZIM2dWTmZVb3p0S2xIU1F4NGJqSHJWbFRaa3lTc1pJNUFvRHhRVzF0SFByclA4TTlVZjJPcC8wcVRDVVFIUWJTQkRFRjlzcjBibVI0MzNFYzV2Nzl1TnBPeDdtV1Fwa2lxZFJlTTllbnpXWnQra25iRmFuSWk5cWcvaVlXTit5bjFwY3pUMThGUDQrUkt4Y1ZncjdMWnlrN01ZY3ZkUzJSZUZWdCtOZHFkelRDSGVSbXlIdXBvZVBzbXBNQkpqUS9OY1lUdExJZnVMR1FxTkFCTDRwWFVBVUVQZTMxU25XY0E4cjZKOGIrK0xUQ0U5eTY1bktQeDRDdzB3SElVemFGdEhLYlNlUVE0NzFqRlFRVmZJRmZUamNIei8yLzErSWx0ak5yTjhGVWRGaWIwNE8rcno3Y05FdHp4aTkvZkQxR2tOZ01pZUtabmxSOG1yaFdlRlBDQzJJM2pKR1VjeGVZdGxuSWo5anVBUFp0SVdzaFR1SGRjdCt4RG9qTGxpY2JIbTBsb2xURjYxREJSaTlnWmxIMHc5Qk4yRHY3bERLc3BHeGN5UzFGWlVIY3ZzMld5T1dsNlNTRWN6bVI4aFVSVnYzZmpFTUc1cE5aVUVVVXMrcko5Tksvb1FPczFwZHZ6ZFFnSUw3dEtibXlhMHdBRmZOck83MTdQUFAzbVpWMklYaDk5ZjV2ZVNVeTZxVml3bXdaUW00TExBQkR4b3hrcVFWYnAwakZDK0hRdTB3aTB0QWRpbTJ1akFnTXNoeWFMR2taaldaLzFDVTEyeEZ6NUJmcjNjcUhTeks3UGdUUmFnbnpleWFzTmcvVm9pRTAxMEt2WEUxSE80RGtaT1BEc1N0ekp5RWRXaVVGUUxvZWxzZlJJbkVTWXFZa0lHRnEzRS8zMXc4RGpQeksxNEtVbUhqYVVYS1FEWTkvM3JIQ0JZK2JtNG9vaFU4d0pPRnVwU2lORTlVVEN4aDhmdnoyTDlpN0xpRm41eFB6dTJCeVlzTVhPa25reWU0TjNUQ2FwYWxrUnd6Vzduc3dVL1YvZ05GZXdXZ211ZStIYjdJYmh0VHVHUDNPN05zbUNUUkhVaWE0WUE1cnMwWm4zaHF1d1pIWnJaTjYwcU9BNEJobUJNMW5PVTJkWUFsQ0VZQVpzVWNaRVZpcXJ5YTYvYlJnY2xuS0tBLzMvTkl5WUhERFUzWWhkZEhRSndaQ05WZ3A1OEU3N3F2bzVtOW52eXFRMWE4bHJNb2pHRW56SkoxVmtCK09nbWdmK214ek5kaXk1QUZqR0llY3IraHRNZkR5UmpUVjhwcFJzTkthZlFSTk1aNDIvSzh5OXRqMU83NFd4NXYxQWFnQ0FSWVVVTC9iRzdrTVJSeUFrZlpZMHFTMENCYUtjbjhZRjcxNnIzZHU5QitZdkNWbWhBelJhWGJVd2Z3NGtGRllDUVdqQWlPcC9HdnEzU3EvdkZiVDN6K2hNZzZqN0lPekY3L0JYa3IxSm5heXRWWUVvK2hjaGtEa1ZObVk2Qm9BT2JwZmFSUUNqRGJnTzNDaUdJQzNqMS9RT3RhNjFwRGk4WHBEQ0grVWtueDNzVXIyRGRkcHV3c294bHFuUjlmMUxWdWJoQzFQSW81dUFIc1M2MmY4VDlDNzA2NVBCbWNzaEpaZkpjVEZockVPRFQ2MWNEQlc3Y0poc21iRVhBbzhYanBaQjVBUEoyUDBtVVF1OGM1N2sybXgvU2gySmVaY3I5WXBNWVBRN0QvQXFla0luOGd2ejg5WlNDNm9LZGx4cUhGQVBUWDY0eEVFOU1Ja0tZU1VObHMvV3VZTGxyVStTbXNISkhGMnZWRnVCYUNoVTJUT2lQUnk0bW41UWlCQWVBWWdFVkhUZUdHd2Z4V2lBTTFoNnFHOEplcU43M21WYjkybG5LVitENm4xUStiek1hZmhDZ2NqMXAyN1pTb3BJN3BVajhrUkxSU0pwb0VmYVpGTDdmeTBNYWtQM1BHbTRNdWxqZGVjZHpsMTZEc3lsMDVTNjJwYkxoR2RXZUVWVHNlYngrR3FsaHdBbHdhK0NTRnZEelhtZk84SmZOTXhPOStIWnJsc25VU2w5VFgrYmpQRUpOQkpNSVpTcE9iNFhXa3ZmV0ZCWThNMlVxdlZHclpxTER3OUthd3hCOFhubmFjVEZOMzZCUUIxWFM3QjliUXNqeEtDSzF2a21UbFlIYUppYmRCU0x6d0dJYnFWaThONUc5cFZVOG03a28xMzZUMS9RZjhibTluU0NSMm1Obldya2s0QmF5VlMwQjE0VG95OU1TcTVVQWFTVytNcCtUZEpsQ0c1ZlBUbG1QZWx1cUVwRU5hRjJRaWZDTkhwb2hxVERPK1N4NHZYTWVVY0FyVG5wUjJzcUs4dlhKVnllbUhDT0FGSWI3MHdZaDNQZk5YY1c4cUxRMUs1MEExTzYzUjVPcEUwaUVPbW9ndVpwQUFVQmgvenpkSURPTnNBWGt1WXlZbjdISTlQeFQyMnE5WjFoam4xTlkyVUMzdFp0NGNONXZ2UlNDcjNBMUkzaWo4ZGZ3Mi9JUVJxaFN6Q0l2NXV6dGlMa1hDdm4rZk9idFROY29yOXA3V2FDeGZnUlNTZDZXVUlER2lLeVZOeTJWbmRrZElFTDNxODhGcmZrSlN0SUFlNHRJSlRSUnNSVHNPYlVGSUwvMmNhL2RGeS9CQzJBWEMzR0xuZVZaSlJGZnZ0Nk1XMGV3Z1ZlYTF6Q2ZHaUYrWmxUeE1pQ2JCVStZc3Q0YmRsTWVsdlc4WWNnWDRUcjhtQStiRzJQZHIzQjE1bWJtbmlRb3pjbWhoc2toa1BCT2t2OXdhSEp4OXpXUUdQQmtReVg2QWlGTVpCbVl1Z1kreU9ZcXA1anZ2c2tPVXpYQ0tCS2NwU0c0MUdIRCtieGxYRDJxY21EdWRsOXNqQlBZa0dFY0QwR0s0NEdSQ2oyUHJ5czliWUhDdnBQY04wVjNSMzVWQXp6NzU2S2ZVakdZMnJYOENzRktvdEt6anJUZkJVQkZ5dERBdzUrTWtuM0lQNEprcnl2ZUZ1QVR5ZmN1NUE2Yzl4TW5iYjlQTHM1OUpRS0YrZVN5NzRwY3pKa2NPS1BVNHlmOEhQaEdnVVc1MTErYVBxZWtnZW1FOWo2dnNrc3ZQR3BDVG5CTzJWdjhESnJXSnQrNnZqNjVrSlhhS2RFODVtYitzU20wcmc1dGtrR1prK1FRWDN0MHZKTm1hRzB4bURhR1JNV2dxaEUzbGczY0xFMVNtNjZMOEJIMUxMZUMzV1pjeXNCd2hDNGRsc044akpGYlZyb1M5VDZtSENRcTBhREd3dTFtQUE2emFxV0pnSHkzak42UFYxeHBLemRaTFY3cHZNQ2JFTTMwQ0U0L3kyQzFBN1ZzVFJ6ZGdDU00rN3Y0L2pJdzFPaVVad2xrMU9vWEY4MUxFVElWL2plLy9mNmptUG5BNGJWVjU5eU9mRnJuYUkwdFFrclNqYi9MUG0zZ3VZN3kxNEt1VE9USnVBZmlPeUplVnplSjJDNUY1STVYRW5BZE9YTFdoM3U0aVFENFBqWVVYYmJjMHduMG5QS2ZtRktFTkdYVW9HMGxKSy8xNThJUTZOWEFUd0EvaG05R0tnSjlrWjZwbGZrRXVZWnhDWW1tdW1DK1NVWXlhd3YzNWxJMjNzOU40QmNpbTRjQTJXYnJQNi82N0dWTGNuZXdFUU9JNklxM2RhNWpnWHphQ3A5bDFNRkI2UHNIWHBZNlM5ZzNpV25CNGZRK0pIaHJvS1RQamdMMWRHWUtKL25WU0xxNWx6QTFiWk1vVVdEZ1dtOXQrVUNEajlNSXZjQVI0N05hMmprbUpVNTVnNDljOFVvSmhxOHV6bUdvZHlIdWFWL1ROMEd1RDNkVU0rNnd4MEpyTnBTNitIUFp5VHNvaDBCYTJielZteWVMbWw0RXA0V1VCNmszTmVjUGtFc2lud1dYdnpobm44YTh0aG40SFRLU0tjTnpuVFBOM0VmeXIzVFdzSm9rZ2x2TnFLNDgxK3ZDTEFZWDZ1VXlGV0FKUTVvKytacUxvOTZPd24yTmZPUGFXNG9BQ3g3ZUs5d3RpS0tEL1AwY210anFnSlZybG5oYTZvVVBYbHdhQUpqZExTRTlqVUtlWDFDNlk2RS9vYnZrTm9iM2pzWU1aaEUwRFQ3dlVtTUVrdGNzVmRjSXFUb3pFUkFud0h2bUlhd1BVbE8vMmpNYk5rZjlPOGhSSjdFbEwxQTg4amVsa1c0SDM1NktQTkREaS8yaC9HRlZGQUEyWFdsWHkzLzN6TWxWUitwaWl5WVVpVWVEVGVHUCsvSFZmZklCaVh0dHphZ2RCa1lSWGNBZ1RPWXMyeFo5Ty9tQnhlWGZmMXREREE5WDJqQnFPbmRET1FEZzJBeld5NHpFUHVQOWhWTTQ0cEJzUnIyTTFuYUJ5UWdGODZCeTN5NjdvakVKTHpjRlY5RzN0WEFKc2VDU0xJLzZSbnpoalBtd2J5b0V2aWhXb3F6MjIraWhBeldsUXdWU2Z4dXN2WE1rajl4MFFHeG1wUTRHZEFINlRzZGtsbW16a3dhT3BIcFRuV0hVMVhSbUxIVG1kdGFUV1B5VUc2L0tSVDRjWXQ1YTI1aHpud05KdzgwYUJIcUl5dXNnai8wYWpOY3k3L3lZY0daUDlLWE1DVDlQMGkwMDAwUjZtSWxLQVpvbG4wVzM4Y3dJZzZ1NVpOT0N1U3FsZGlURlRhbG5JZHFWUU5XNEFvZkFqbnVwelFxRC9kRTJpdmJzalpqZ1R0NnZIUWc3WWZTZFltT3N1THl3SjFPclF2T2doQnJ3UTBRK1QzT2I5NjhtSmtXcjJ1OSs2a3FsTFgwL2ZDLzJZTnhwT0QwSmlkaEpmRUI2ZVV4L3gya2hzNUhvaml3SjBCTkh4OGlUUWZDQVZzc1YrWkd1S3g5eXZBUGJTUFpTbE5NWVFYR3J0Z0pTcUFJcGxaZ0NoR0s2MUFIR0F6WldpdzVWZ2xORXdZOUpUeU1TQUN5Y1g5alg1NlVzQU5nWks3L2VidlhpYVdHV2dWVU1sV0JLcUg0SnlJOE40TjZxUWNiR01LcFZOVmpxN0J4c2oycjZHOUU3QW1kZVkwWVJZK0FCMGk2ekVBQm1OWmRwUUVCNzlPRzR6U0toNFVSUGl4ZXVla0ZCb0dwc0YrQkpXbjd1ems4REowT3cydHJQRDNXL3VHVU90aldsSUx4OGtBc3VqZTVzenRtK1RxQnZHWDl0VzBPZ3FHVGwyMzVXNHNuL3Y4UEU0UXBUQzhPZ1lBOG8rcEMwM3BuUVVOdEFVZmdaRVlkdHExUnFBdHZSVjlXdldHNjdFNUIyVlJMMjdSc2ZFTWEvT0pVaWdwU1RrcmNrRG9USzJYZ0FWUkFtSXlYODRzaWVYSDB2Mnk1U0loUWlUbDVhN0gzclRQZHlBYnFwZGluOG9sbThSa21GS2NnajJzZk1mM3JOcWpSeUlnYWYwNUVHSjgzNExoWEdIWVVTeCt0NGtDdjV0bkZYU2JIcnZ3Z3RNMVRCS1p5czlVamFsSW9yeG41cHRjQlBOU0xqR2l0MEZ0cFd1eXdrdDdDWCtMclQ2ems4bjM4NDBJWjIzTVkxYTFiVkczVWZUaWhyMlljMC96UDdSVVpmSEFzOXU4anhza044LzZEOEl4eUhIT2c1cVJWQjVGbUJhdHJJRGY0NnVadG9FSWxRTGJpWnpZajh6YUplZmpqQnFSdkR3aW9TY1REY2FsUlhRNEhJS1diR21WREU4ZXcwbktSR1Z2MzlweGlQbk94anJBZU5HUU9wZTVLL3p2TEpSVmphNUExanRad0M0citZVUR3Vll2Q3R6endEeUFrYU9PWGtWb2ZGamQ4cW1yQjBrblZHekNoMEJMb3BtQ0RHQTNhS2pyWndtQ0VZYlJTcHIzd1MrV0tJbFc1Ymh1ZE9jTUZqS0laN1RqWU5ZNURFRzBoUWEyQWhnRG43RlRkeEh1YW43amNYdzNYem5lVkxlTTdEcW5VRnIvUURnbWJiU2NiSDRXZ2ppTGFkTXBhSE50V1p5d2h6OC94NDRBdk40blFkdGNaWTJZZk5ldkNCbmQzTDl6T2NiTkZQVW9Zc2Y4YXU3cHBvT0NGMzRNcHc5OEg2aXF3UVpOeXBGYWxPMU5DTC9QTXZCMzdhU1RNSHd4MHFLMTUyZjcyV1VGVjBRaXJrUy9qOU43UG1PSHEwcHdvZG1aUTFGc3o3TDlFUnBRVUlEdFd5VE1zSHQ5a1pNTXVjbXhPK0JURU5hRVdyUWZyMDhiVUF1WXNzWFFWK0M5QnFneC9pT0l2NnI5VFFiV25ac1dhbmRHYXNHS0dvWTNFZVFGNVU1aU9ZT1hDdmhNcFRONHJJNVpjbHVFVUNWcU02anRoUGJ2SGdQNWZEUXRmZHJOSGl4ZUR4RDEwdkVjNmJUZXFKUmxCcDlUdjIwRnJheWNtL2FzZHhHbWlnUGJ3MGljd05VY08vZWo4NFp4Z0xYY2ZoL3JRY3A3L1plWGtjNWhIMUc2K1loWHZVaHcyRjZ1dm1nNzVRSDgzenord2NTUmVGemtub2pzRGZ3VHdEZ2E5ZEgrK3hBRWdpWHpRL1lZQ1dGbVZpODkxZUxkNGd1MlJEbXlFWmZsV2pBWDkxY0d1QnlTT010VVNIa3ZoUC9aVFRhYzY1M05qalR5dy84dzZlelYwamU1M3g3ZEVuZnZrT3dPR3pSOWtuUEFFeGM3ajAxVW1LcWp1dEt5cm1RdEhtSjl0WEZWMXZQWndERkdjZlJHQ0tsUHdmT3pGQllzdk9zOUsxanV2bmU1ZjRMV04vRTBGMkM0RzQvU3k5K0g0eUFtS3pmbUVEdEdEOHlpQTh3K0VIVjVVTENNMlljOHQ0NlQxVXBDNThiK2xFN3EyQnpEUVc4U0ZWQnJzbkNPWEpZR1RjWHNxaSt2ZlN5M0liZW5nUlZwOWliQStsbWl6WnhEU2R4RHlYYjFpSUdaZm1vOGN5MkVwRWMwbHh3d1JVZEFoak96MjhSZ1packdHQ0ZXeXd1Q2JJVlNRUzJVMnhFUGJ1YjZtOHlzc3dPbHR0K2o3cDF6Mjk4M3JyUmU2VndFTGpWMkNrSUkxRCszVU5EZHdpZzkvUXdFZ1FtdmI4UTM5Vmp1MllrbTYrdkdwNXh4TUg3K3MwV0RSZTByMGpGTk04YUExNEx0L053WWtkM1BuNWJNaGRzODBUZ1B3T1o1R0RWUmZ0WDVRM0NkdzB5ZUJ5aFJHT2ZFOEFQL2pKY1BuOS9YM3p4Z2g4eFFjdnFUUktZdDZvSnRzNnVZblo4bTRGa1BMdlJjc3JIaVNPMFlCOWJna2VOSHlZTTUrWE12ejc3bllFYVEyc241NEY2MWhuamNTSGVWOUFDZTl6ZjZGQnR4dTkrVHR0djZrYldNMi9HMmtFRkc5Wkg1dTJzR0JpeTVEOVhPMlUxM3p5SGQvMzY4Uzl0OXd1djRUOUNtN29iSFlYUTF4dUpXTzRYb0Rwc0tEcFBFb0taeFBCamtxU05hUTl4aDVKOEIvUUo3MHJpbFlCSXZKWVZrN0pOTnJJMWtXU3hGakdFTjBvTmhQcTd6R0JzWUxEUThZOGU3MTNGcEhXNUhSZ1d4bXU5U0ZoblIyZ2JOYnNJQVpFRkN5dEQrNjhzQmlGVVo0WVVCVGRtQU1VRDNaQUZQQWJacERobmpNS3puUlZnWXlSK29yT2VqYXo2L1A5TWZkYjBmd1BnSHc3Y3VORHNSNVI4Zjg4OFZ0N3pQTDhybjY0eGZ0cnA3b3RTaXk1Q1pxSE8wSHpGUGNDdENiRHF3SW1SRzRibnJNVytqTzNoOWhyakdGTnJwMnllbjFyQjVDcEF1NjM5UERqUnpyMHNOc2p4Ny9OUmpJbTNlZ2ZvOUdlOUFmYXpVMmNQbTZITkpYVTJoWXJUOG0rYWJ2SWdtcjBDR1ZUb25INSt6dEFxR0ZIZjRoNUE1d2t5NDVrNmF4MWRoU2Y4emc3RVpSWUdKdnR3MEt6NmxqQUJ3NFdCTUxIcWhtNGcveVVRVHJoU1grRXVPeW5WNGVMaVJBRVkxUDl2RUdBbjcrdUl5TExwVXBTTlpXWTcraHNCUGJKR2RqZFZKMzRLZDJmaGFjQWlhbnVPVWlXY2pXN3FNRWpwK2xvTW9Yb0IyMkY5RXcweVBoSlBDS0RtNUtSMUcrV0ViZngzMVM1S2Z5dVBPRHhZSWNrQmM4a01CUHU5T2k3Mm9JZVBDOUI2NUMxMFVnR2t5dkJyYU1oUFpCQkxRMTlRNlNvYm9Ba08vUUU0a2NlelNhakFUNWZLUUlYSW5OWmoydnpDVTg4Y3F6TUY3MWl2L3RwS0VCN0hyOUVLb2ZmNWJQM2pmVDN1cktKMk0wQVY1TktGQjVEN2h3dy9HWU15OG9xR1UzVStFZHk4Z0hoNWp5VGJIZm1qdmt2dEdvWXpDMXUxVTEwZW13VGZocUVDYzZsaFZoL1FISTFhMytCK2dnd1pMTnBFMEZvWFV3S0ZxdGVCQ2pRZ1ptVVBwOWw2Yyt3a2hnSHVka2c5L3hPc0gxb3Q3SHdCUmY3RnpqWlExLzZYWW9EdmxRV0JtRnRTZjhreUViYkJRMk1VQmNMY1ozYWlNclJoakg3R2NTR0w1VlBmbGdWekx3OGNHbDZYRzdIaUh1NWI2WGxnMS92YkQzdEFPeGV1S3BLb1BZbW9MaTRmRUlmYWFOMGJBc3QvMVkyYlR4NmJGdmJkZjFnYlBSSFBSa1pON0RheDFhUXRSRUR6UC8xSjZhYWhJVnVHMFRZbmpjRDRzWVIvMjlKcThwbW84Z1F2YkdKWGo5L2RLZ29UR3RNWDY2QnRnbDJabmVRbTIxWTcvVm1zdC93RGl4OFlLdTROT25UZDIvWG9yUmFyUit2NXp2M0VpVTZTNFZYZDN6VlBiVEM4MDNwMW9MaTlFUmRKZkQzdDVlbTV2WVBUVWIzUXJocEdTQk4ycEp3Qi9Tem9aQWU1WjhtdUZwK0N6U0grVll1Q0R6bXZyK2wyUlBueHFRNnJtMTczdEtvOTVNb24xcmcyZUNJY0o2S0g1ODhjRzF1RWhDRGdLSmdRQml0WnFtd0Z5NzZNY1lyOUtleE1xTG1Nd2k0L0w3aHRaTWpXOUlMUDZreXVKN2hwdlZBK3RldTNUUUhtTEFFUTV2a3hWSDI5eVJabTBlaURwUjdjRFl5ZnJ0UFIvTFpqbWp3UGUyR0ZnbEREOUxTUUcwaGx0QVM5c0dGSlB3OW5DSStXRmJEYjlROEFuNnR3TDVGU1k0SFRuRWtWVnNCaHV0MlcxOXZvMFdjM2NNTHo2TXJWTjVaK2hKR0o1N1lqNDVBSUoxdWxHaHMybHJEN2d2SEpsYWZNZVdzN3ZCdStGVm5zWEZ6K1NHcDRYYjFlbmJBNndRZjNha0dDVVJCcDhSV1RSekUzZ3Z6TmV2dnVxTnBxWmVIbCtCeEk5Z0tiSUlJODRNMW9vOHF1YnlhWEJSQVdGY2xJeE9kNkNaVnQxVnA4WktCUWZ3OTR1T1IxT0lsNTBKZmFYYStNN1Q3c2I3YkVWWDdrYnA0clV2OGhYcGhnY1FkakZqUGVLRGN4OE9MVUtQbUN5QU0zZnBKR2ZqUEtzNno3a3oxM2hrOGdvdGVNUjZuL0NtL1FWNFpURmR4RGhYL2cyK2JJQ0NsaHNMRTJrdDc5ZmtGUDFJTzVha0M1ZnBwaUVoQkVDaEZJeEJmaW9EeDh6VlpLSWpHWjlhY3R0bm44YzNSOXpaelJNdzl6WnJpWThaUXZTWXF1dFJZSzMyTThHSHlnUllyR0I3U3NING1PNVVzeExndWRURGlDY3ArT20vZUQ5WnFwVW41K0xPTDlYUDI3RnJieE11VndoTlZzSDVTL1pCUUhPZkhweEd0eE43dDdWOTJwNDZsQVVIQTN4MWN3bGN1MkpnMFI0ZWVKUXgyOXdTNHpjS2UrL3oxV3I3L2MySHdZdld0dE4zVk5pbFhyQWVyWHh1QXdzQy9zeXRLOXFBbjRySE5GUVVzbkRnOFRudUVKaUl1eU45ZDcrcEV0cHFXTWlKbHhHa1Y2RTkxSkpiOTlINWF0Tno3Z0pZKzFHMk1nT0JEUUhTK2VrRlU4OFpIM0dkRm1ncy9BL1o5NUdSTE02SFcxYzl1ZjNGTmZmeFRaNkJhOGZPMEhCRkpuMjdaOGFGdFlMYUZZVEsxSUJMekh3VHdDYjlaaG1pZ0VTQ3M1bzBjQXJBVHRXNUNoV2RtSm85UlJPTmx5TmlBRncyZDVaNWpSclFRVzk2MWh6cmovZllQZjhUbThOYVorVWtsd3k4a2Rzem5mcXVZZFNSeW9ueE5aR2M0cVZGdndWNElKOXdLN3A5Nm5tWUM0UXdrbzlpdVRETUhzZ3ZKS3VaMnkyUjBOdFFmamJKWVB4TytkeGFDMzRSVUk1eW8yS1R6VGUxMWtEaytxN08zMXJxVnRiaERRaFVPbTBkb0hPUEFKM0hIQVVDWFpFQ1laMURwVE9mN3dESWlqRWVRTW45R2FTZjRWN3FjSVVkM2xJR3lWU2FpdUx3WGh0M0VOT1ZXd2tXRm9XWnd3NkxIU3IxaTJPdE9jUklPemN4QUg3R2VsWmY4RG9FcGo3OGtDOEZCOHhnL2oyU3B1SjJzNENTcDgzZFBKenNkNXVsMW1yOUZHRjhFZm9DWUVscVJ1anRGSWJCb1Z4MTV6Q3ptc2p4ODVKSlFQYW4vUkcwYzZKWVdzcVlsVGIwNTZOYjJid2hwRU83UWFEb1J6UVBTYVZ0QXlKVG1LaGphN05mTmlSdURVemROTnJQWGc3ejU5UVlXY0JkbU9XcElWSGdWM0xpbCtGSUtiMTdmaGhERkczclBUKzR5SVlkNWhMK0tDd2R2QnhNRHBKdGc5K1JtK1dxejBNeEFYaG4xeXlJcFdjb0l4YXZRZHNmbVJycXNZTW82MHpDS2dJZW9oQ1dZMmp1eUJUN0NjUmpVa3kvR1k0Vy82RWhsWkQxVjZRemtqQVRRMElZZzdFc3BoS3Y4TVBuY0pBK0hTbDNmaE1MQlpMRWFoOVBrK0NCU0tzQlZ1am96M3lrZlpoN0YwS1JxaW8ySVNUN3I5eWdOSjZjZjFhb1F2WWV0SnhPeDlLZzBLRWdRamFEVEhrcWZGRE9XQW5YdjFsMk5XcWIzb1BzZnRqZkZneVdFdnZFS251U2QzRU9XZUhaQjJTNzhlRWFYOEZpalZON2tjSzhQYXpTT0w4Y0Ixb0g2ekpXYmhXYnRhRFdEaVpYYUxwV2lGcWVvWmJ2TzlWdWdvcy9XZ0JKQnA5c3F5c1J4S0E5R1JuNlVQWUZFM0VJK05meTJ0QkFHck8zSWVqaytrWjRtYU9FY3RtUzNFM2syVTIxNkxXbVh4ZFd2Qks1ajNYazFvSlRzZnlpcGlhbWUxdFR0c0hDSXpOeE9kZmJ1bXBKck13MXlYZUJPQzFDVCtiemFOZlR2QURRZnN6ZkhrTmdzRDF0S1BpUzRvalJJNUNBS1RvNmR1N0liMENSSEdkRisyRXF3djgyZGtLTnF2eFFhR0tYUjMwOERIV0txenZrdFFzblZvS1pxblUzTmR0U05XNUZJYmNvYnpPVnlqSlBZOXZudnFhM01XZWRtNmdNS0x6eGp4REpPbmd0WGVRRHoyekovOUZUa3ZoNlkvbTJZcVUzMENWMllONW02SlVKeTNlR21MMUI4MFovY2N2NWlIZUxWeEVybEFuWDBxa3g2S1hpaFVNRHoyc2hNcWNLQTZvVDFyOWxGbjVoeDE1TUcrYmtyYnN0dzJyeGdrbVB2cEtKbmFydHRObGVKaDhWVjYydjVTWWNpbE1qeXZzSVhITGFZQU5jU2p5NlU0bmlEbDRDZ2hNMitWSmt5YWEvRFZTVDdVNGVweHV0ZlFNY1RsUDBYR1JIVjdYYklMSm96ck5MRDFLeGh0WWpEbzJSajNiVDZqdUVGNnJySmxyRmpqMmhsMDh5SEdCcmdwOVl1eldLbGZ2WG9JNGJITHZhQmJYdkV1QU1wMWdRUjYyOE9hN0ErSGxxNEV4dG9MMTB5L3NIRTR4Vlh0amUwRkhOZDhlemx5UDJqVXhxQWJHOVBlR3NOMnE1YnNTek84Tk4wSkdJU0RsblhGaVQzWDVLR09GMUZiaW9hZVo3emh2eC93TjZCYWR6aWgvcEZ0NXkrYXVoOENvWUZodzBhajU0VnltQUJnS2dXNzVHYVp3cjZHWTdjR05SdEhwSmMrZDNTRXluUE5yZ2RpTjRnVWxjQktvT2xlZkFjalpBYkNlZmhTUm5UU2lZSlMxMVdpaG1rcmlOM3dTNEw3bDJ2MlpyVUJzVmhyOWYwNUx5V0h5WVA0ZUlMK0VqNkJ2N2haQWtnS3hrdUlMZGdvVjR1U2ZNd1l6ekxmT0trS1VJWUZZNS9hM2x3ejQzczM2S09NeTlFRmN5WjlzUmlHU1hHelJMK002VWdUMWIwWFRweU5VZ1NRVWlXR04wN2FRSnZPZStGVjNSdnQ2dkpzU3NZOGlWUXhnQVJNN0NZbkF6V0ZGclRTaGtSOW5ESU8vSFZheC9idms1OUExRU1pR2liTi9FNWRnc1BnVllRQlhOeGlCNlZ4ZWppcjl4R2lZNXRUZ0N3UG9vTTAzWUpRSmlKRnZEKzVHNko0d1VtNjcxeXRRSGVYaHg1T1dzR1pZOTB0ZkhEdjI4UFJzeUJheWE2dUFHR1prQzFYZEJGWkJ0Zm85T2tCY0tEbDJ1cnNZZWlURXNIeXIzNWlJM3V2ZkRDUWEyZ1J1bTVQTEZqTEZ5K3BPYllhVW9qY2lRYzBFZVNldmNoTndjRlNkWTJWNFZNU0RsK056Zk1td1Nic05HSjZhSnNiYVBPbnpoMkxRQU9VRzhuTjN0L202SWNPS2FsUjdRRUdxZGIvYS81UXVQME9mWXBTeER6NE5GU2FHY0dJaTVTRUtuMFZLdXBCMVdRMlBMdDBVbVgyZHlYelZBQlRwUENGRjduOFdrMzArc3BiaEZqWFBrZlFSZm5zaXBkVFpPdStiQmYwdEZXVExLTENyUStjM0VQUVNPT1Q5d2ZNUkd1clJmbVNtZWxXaWxhS2YvWEtOeEFpVFRVbHhERGpNcFVaWVlkbFhPNTc5V2NTZjN6RGlFQW5OOUgrb3JHTGxJSW1DdXlja0ZaUkNyNW8xTW5OMFNuc0xqcEhKQnluU0lwcGk1dUJZRmZMN1JEWElJQUN3c25Tbm5pYTQ3d2tMZ1puaHdJS29IOEpFTzVZN0V4bDNOdSszcDdNYVJ6aUY2SWl6cnh4R29BVVVFb0NQbmNsVTI3K0MwQmRCWHhRd2VjYSthQytXcFVSMmhGbFNqNCtUdGQwSkJnU0E1K0FnSUNBZ0lDQTRuenNiNndBa29xcjJMSktnVTlyVForaWhoQ3ZQYnUxbWFNSTZlYk1GR0dtVUFSS0Vnek0reHNqZzN2MTRyREZVL2tyT2RZQTRsajdJeHdHNkRFQ05yQmxkUFcvS2ZEL0tGTU0vakRIUUovSTFreDh1NXJTSkErMG1nSmszWU9IeUVUYnhKeHdLVzI1aVhlb0Nja1h3eVRWeHUyOE9SRWltUkFhNGFBZHBUOVkzMi9UQVgyL1R6YStxeWZzMit2Ny91VFAyZEVIM2UwdS9mODIxLzM5UGw5dnEzL3Y5ck9mVlJmUDhQcDhQNGZXaWI5dHJYMmZaby92KzNoL2IwUWY4UHNyZjMvY1c3OXZVbC9EMW83N1BjZS9iVFBEK3o2Smo5djBxbS9WWFA3UHNSUDJmWTIvcTNoLzIvWEMvcyt3UzM2ZlJiKzMwNzc2ZnNGL3FvYlhQZ2sxcnM5bG5hTmZ3am1iU29MOWNQU3NxOWtHRjBzaU10aEREdlpZb2x5MDFXd2JZVDZkRnB4U1JMMUJVdTVGdVBLRUZoR0lkaCtrTFJBUmxXMkRjTGh1M1Axd01yZ00wWk1XaGxuYWw1bWFjSE5ZWFNld1p4cUYydjQ0NzFyVXZ1OU1nbm5tWUF3bVNCcmszOENIaTB1bU02ajVHaGRrSG94eCtZMHFFT1AyL052SGdFOXVuSGFYazRjd2ZJT2UranQ2aEpsWUZ0anRwcDcyYzczNzdLNHU5Yk5RcE90aGRsVmhhVXpNQnBTYWFuQng0aFlOZ1k4SWd5NEhETm1mbzhNeXRkZS9lb25NUnA3RytkYjFYaFh6OTllU3pIcE00aWVXRzhUQ0dLWmZ0eVA0eG0rblZ6MVhkSUFvUURPYWNpb2JzR2NVTlpPMDZPZGtDWGpab09Zd2ZSY2Fxb0k0MnowTGlyVFhSODlQQjRpQlBjQkFmVTUzTDE4bkFsSkRjT2VveXRtZkRCaGVkWlcvc3pqaHNkTnNSbmNuWHJZZmlQOHN4MVRTMnZVZFFFQzFKYTY2b2NObGNwUXZDRnV1aHRXYWJLSVJoUXRVL2kyOEJUZlMyVUViSEhxWFVKWWRDdTQ3TUZIbDEvbnljUXF6Ukc2U0c5V250bm1yWlZpOUg3S1JrTVdTdlpPdTFXM1M4MDU4VEJKT1F3Z2lyZk9CVzFaekQ4dkZKQy92QzdUVVpTc3gyMzlwek9xWDF3eDQ2RGY0YmNvS0h6SnJSMStDdG0xZ0wzNEpZN0VhVTJMd29IR1UxdDhNNC9ubkJnbk04Zmg3U1Fldm1rejhVT290Vi83NXZ1MGpqWFNiUkdDSy9lMnRMY2t1LzB6MWZkelhjc0R0N2htSU9IZ3JnTlMyczZudGtKM1RucHBvbVIxaG42R0lSR2FVQnc5eHA5aFBTU3pTbEFKM09GOUhMd3Ixc0RicU5OdGh3OEgrUDhNWnhXd1VwNWZzdHpxZUpQNlZuMTQvSWt6bTBFUzZjZFdJMVRQY3kya1l0U0NxdCtoZlF4UXRGU0JEeTJaeHdQTVVjSTBTMi9LNUZlVWd6Wnh5NEVqeFU0bjdyUEtBZVhFSTM2Y3dHOGFqOG9iODByMTBhb3F3SmtuOW92b0JLd011dlZOK0tmOWh3Smx4VDNjMThqVDhCU0YrcGxJWkhXbVdlUGpQNkpMdjdvRmtqbTJZU2xlSjh6T0pvVTNNSTNEWWhXTmxWaFFUYVc4S1NwVE1WMmdEVmJheUZENVF0Ri91ZWpxbU1HbHZKZ1p5U1Qzc1pLdjJ4ZXprRDdzUDQxakpTcHNuL3IvL2Uzd2w0bnBRTzV5Q3Jsa1lsNTFQcDlDbWE5S21sdU40OWdMeUJ6bGpYTm5iRjIzTWVRM0FaVzRPdm5QbERtNlBoOU5pamUvYVFPRnNUZDlibUZpWm1ERDBKaWVGN3pFbUZERjJqVHkyeWMyRCs4NVhDMksyZlVMems1OS8xRjUwTTVsMmhLR04zWEkra2g0Q2NDSGFTTGdqayt4QXVzNU5PcERTNEVpK0NKUzhzTURWc0Z2WC9Eb0RkK1BkaG1IeEU4Y1RQdmNQUU44QnZ4ZFFnU2NBYWtET1ZzcHM5QTdDbXhJQWRSUEc3MU9LWUhBUHZpTG9Hd3BPN2dSTkNPS0dQZ0hpbHIxaVNpWTBlS25HZHBoUldhdjNyejViNXV2aXEwSlVJdVdqc1pldDByWldtTEh1TVp6bDJycTk5VzlXVTNLRGRlek5ycnh4MTJUeWQrYlUrRXdsbjVCVjdEK3h4NVdnRWwyMTV2eWsxTDVLSEh3YzVBZklnd2FFVzNrM3JOaXhqNWtXUk5XU2I1azVqeE54NkNBdlZvMmF1b3BhK1cyU3BoTHVhMzNOaDBLZERNR1RMNkZoaGFVUnZsWHhQcTV2UkF6MURJUUNJQ2twWkZQWXQzTUEwR3RMZURqRUkxenAxZXk2bnIyQXVxZkFzbkVleVVFK0dkSHFLM29vSUJQUHJESzVmak5ueHpsc1VQdkQxS3h0ZlhBd1A4VUdtWVM0WUk4MW9BVE9kbWx6TTh0T0pzejhOYUhOMi90MGI2aFhEVDhVK0VQdEQrSWVqdCthS3RXSVNTeVBxdEN1OXJlQTlYMjRvR0laZ0pGajh1cUxlQzBHQ1Z2ZW5HTHVKMHNjQ2pwRUJkekxHTHJLb0FmZ1pRbm1Zb2UvVjBVZjZNM09QUTlNd0xGcWZzMVR2Q010d2psbHQ0OFp3WnRrSmFrcjRqU0duMUdoaGJzVmVCenMzc1dvcldrcytWUndCS0JFeVFvM21jcG5zNzF4L2ZLcXdXQjBjS0ptL2wxck1jcFZnMHZ2NXJEOU5xbjM0eXJsZ3VyZ295KzZqL0VJamQyazdoQUhDckd1TEVrV1hlOVVIVWNiYmZ6MlZnLzdJMGRKMWFOZkpqNGJPc0Z2Mis4VmQ4Nml1WlIrQ1RmMlpyTXN3U1RURS9SZ2pXQ28vdFlscFdJL1VVaERlcUtCVXFpeGt3YTB4Qy9lMVpOL3B4Z0NwYlVpRTNQdDJLTXpwd3ZTcG9RMEdKaVRoYTBlS083UldGVDlzSkxWYWZMWGZTbUdyTG05R01KV1JWR2pNRDNaY2VPRGtDdDZDM043VW9oODBVRS9sU0lUUk91ck4rbGwyWDd2VWhMaHZSSUNIb1Bmbko2Zk5DNDJOV3lYbTVadk45cUh0S1pVT0Z3RzBNOTVWbDdORlV0VkpDWGxmSHZpd1NGNkFlSXZKaEtoRnJzUzJYbXMyTGVGN2Nya1hUZ05qbVpxdU9lNE00STAydnI5bG12Q1NxTlE1Nmc0YStEOEhaK0xwcStKdUpOYkRTSlNEWjhQNENrdzRaRFpQU1NNdExWdG5qdWxtOGRCMzVNZ1kvSUVnOXluS0l6aWFCL2xBeXhkZDh5cWc2TE9uOUxISnVNYUkzb3MvMWpuWFdldmtOQlR4UTJTNUtLUFh3VkdyU3Y4dDFFTmREUnp4c0ppRjB0aklhMHlXZEtMSFhOWTUwUFUyOFRLd3RNelhNK1lTYWJJYlpMTlBlSVlRV1dMOUZ3QjE1aFBBZkg2dVZjRWk4U3M3dzJsUE5rMFd3Z3A4M3RjSWtTZitJZGl3VEN2ME1zR1hUN0pNRzFYMVFiaGN5Z2FDZlhjQ3pyTWQ5TStDU212MzRhY285Z3FaY3VRdEhORVdzVkZjK09vTmVFZWdBYklEaVA3NEVWL05Cbi9mWmRTUHZ0MnA2SXVoSXR1dk9aVk9PUmVRZUJuSmJTYUVCbGJOK3oxUTJjQXMzWUEwN3d5M0NOdStiNVhCQktUMGIvWk9LL0VQQ2kwcUZHdHNkMzRXM3IwTnZRc2lyaFRLbVlRSDVYbVQ2NUpVZzlWWkt0R29HN1BNTW5iL3VZMzBucW9ZUUd3TVZQY0FlN0k0WWdOdHVSQUdSR01uUW9qWTBjaXd3Um5EYXdhUWZiUGF6SlFHWitTU2dpMGx2WElGbkZsc0kyQ1dIWVlxZUV0bHdCeWxCME1iREl4K0w0MUY5bmkrMFJXRmNnYUpaUUMvVzF1ak80UU1teWNXQU5rcXFNSWw3M2lXcE1yL29EdjkxYUNxTmtkeUo3S3B3bjlXWmxQT3lBZlZkdk9NdTVHVzFDS2kwdUVNWE11ZTd0QlJhaUpFdXpTQWprWkt3WGVQVlFXWnpwTU91SzVIaG5VaUc2enpzVURyS3ZnZGdJNTBjZ3R2UW5aTkpwYzNuQnlXTlRIb1dyY2YwNThpbUJrcGpwL3A5YUtaUVYvSFNqRkJSV3k5UjJZVkdLR1NUblNqUXFwUGZkWGFzU2RRc29SeEN5dWc2L0F6eGhCSjJRVzR0Nk4yTG9RbncxaENLWVg0M09wRUJ2cmpuU1NLNW1LTHRFdDZoWTgvUEpWK1EvMkU4dzE2NTRrU0FldFFnMms0VnJrUjYzcVBCaTNIK3I4aGxuUTY5ZFRFM1BQc2xwU2IrVGIwRHlGVXY5T1FFWDI5ZUI2NEhOa0NPQVk3SFVJVUVFZVF4ZjV1eWZvVlg3cEs0SUtDSTdXSCtibGh3TVQ0TVYvckFCa2VUWUtzNUErS1k2SnRUdmkyUDZkTHY3TEJadTVqcWlVMjU3SWhEVGU1L3hSZ0dsV3JMSS9CbEhmUStIa0dtSE5PU0pzWXVqbUJEZjdTeGI5UW1PeHlYNGxjbzNqbVpZbXJMZDFSMDdyQkxIZ2R2VUd4Z0g0VmdiTDg3R0dMUXZNa1U5K0lRUTdZYVFJd3J4V1FneHh3SXRDTlFXc29TejNmdmFPM0hiNWx2NnhNZnRSOW43VDJSWTVnNEFPYmp0ZmRiWnV5aVU3MUJJalBvWVFNcVI5VndzOHFYSzdUOXhjZHhZRGdvaFB2dGs5c0VTeUc0Y0RiaEdsZ0p5TVNjTU1seE1ZekVIdll0L3RhZ2dWVGcxY2NkUDJBUHpwZ1Q3KysvVDBhYk0xb0lwSERIQWlXSzdqYy9YWFlIajhVL3J2VEE4MExSZDJITlczSjdPUld6NzZYZFpsZk9DcmlaQ0tSb0JsallpbGtVYVU5K3FtYnRuaDNxQVFUaWtTSFBycWpXRmVuRnpCVGlwMTZrd0pqeUdCQVJmcys3eXdlL2w5MWFHaTNZdVdVZWNaSTNpN1pLNzJzSmdlNWNLbDNvYW4yZU1LVGhXUC96d291clNwWGc0Uzl4L3VNQ1F0Wm1EdGJyb0JYamVaUkFxWEh6dmJUOVdMMU51VDhPVTF4blJrZTFvZ0kyMkVTNFVVbnhQUUtIcEZtR2JnVnNwUi9JQVNna282REIrTU5oQmNTYXVvWDU2VVBsM0plRFBjVFU2UlB1aW01ejhBdFNKMEpQRm5YK0hxUDlnYjBkbzFMSlcrR1p2RXBDS3VuZnkvbDJpZDF3a1c2YU8vMzdvRlJPcGw4ZGphNk9IeVB4WnEwdmpWNVQ4NGVBcVlqL3ZpeERqSnN1RDV0TU14bFo3TTNFZnNucjNJZkxuemxzTFdxWFV1MWJyMlVqTWprNFpJQWFvN2gvQmY1ZTZrb2hkME1pdU55c08zZW9yKzhqL2hVbi9DYUFUTHJnK2xhREdrMXFwRXhibFFERlRCZW90TUtOcVd5RFZlQVNVUFlOSEdycjE5WENOYWdpMDg0WW1IMVVqQ3huSEpjb1U3TlhGYTduNmJtVDVzVFZERjhHZ21pLzhzVno2anNBQTdvbGpidDJGY3BSSW1jREtmeTNMWnlYMVh3cXVxdjliNHJUUkE1Mm45cWVCNDdSeFoxWURiRC9abEdCaldFUElqSVBTbkRDL0F0SElaMnRXM0c2TDNwWUZlcVNTTmk1Z3FrSS9qZDNTTGM0ZW9nZ3ROMWRBT1pmeEhOb2tWMHRmR3ZDQWdTVXdMVzZIaVpxdW5nVTFwOExZTFpDdGpPaVRkZ09LK2VBK1FYZk43aXJsUm44Y1k3VTQzQnNCYTlQMVJPOTBVb2lBUk1zWTRic20xK3BFTjRrOGF5VEZicEFUMzBtaDl3YVFYNmdEV3dsU1JSdXdXTUFielJONXQvWklaNDNaRlJNUllJcWRGRUxuWUpSWEtWb0dhQ0wwNnRrQmpzNGNubE5hSVZFcXF4andRQ1dVUUw5aW01NUYreEx0U1Rld2VkdGxSKzhVd1ZzYTRCTGg3ZEhVczJtc3R2a0Y1Q0UrbUVhTkpRYTNtZmJNejJmTlhGZXBMeTIzN2hpWTRCaUtRNDR0MWhxbzh6UUwxZlpRZ21tcGNLUWF1MTM3cDhScFdkVEY4aWlCL2czYU5IcmdMc2FDYXVOMGRZT01lRHdNOERHeGNpUUl0VGtsSVZwYkRkWG1EZWdpVElGQWZVODVNT2h0RHdLV2JuSGR2NWgvQWNOQy9ZZUVvd3NPdllteXNYU2R2SW1yREtsank1L0tSalMxMWY4OS9aVDFibUFYdUgxendmUlJxNHJJOFNPSTBlUEZqQWs5L2VDdC94WHZrZTRvVGkwcnFkUll0MzhmbTdzeXdrbEdmdWErMlp6c0ptTHhsREs5b3RwRU9OcWNZS1dxYVAydEZMTnVHN3hEUVUvYzFIbUZCZTBIQmZ2WTFsTXVDa3pBODNhMUljbS9veGMxWTd0emhiRTltS3lzZXg0dFUrV1ZkMDlnWmkyRlVnRVUxSWJzN1FzVWR5ZXd6REVvcUpDNjU3b0dna0JiNzJLWE1IU1hrYWVXRkZ3cTRZVWJ4WVVDMjlSbFZ1dXNXeTZoL24rTVo1anFqOThaWEsrdmF3SExHM25Oa3ZzdlFQRHRkUzhuTkRsNDBrSWNKOUNaZGcrODJzU0FOWmsvSUdXaW42c3FBNHhsRXRBYmFsQkFYc01xN2tEdDA2ekppY1QvSFNoNGZhUWtRTEJLY1JJNnVJVUFGbDA1SGpLNDVwSlk1aUxXREVUd0FLZzlnckNsVnkxMXQ4QTgycEwyczRKZDVjYmdMeHdvMTV0MWpOeUM4UXJFb0NYWnZRYVZ0cDVxaUhmNzRsZkhxaXltdjJ3QjRkbUFJRXJPaGtxQzh2VzRVSmRIUjAzQ1FQSUphSjAzSzZ3L25kTW8xQzZsSGZabmJJaHNXei9tVUVWOFpKNGl0VVc3dG1kLzRlWjlYQXFzUCsrQTRHQjUwdHJSM2FtZ0FGVFNpN2F4UWJDM3g2RWpwdzlQTlB1K2dOQ2Evajl2S1U1SEt2NStRMVdwWHQ1dFFHK0dSNmg1cVFpL1JQNGY4QnpuYlAxSDdNZHBGTVI2Q3BGTUN6U3JoMDFPQm9ra3krS1BkNXlKZ1E2RnFxUEV4aWtxbVlIaDFONVpueWpmZE81Uk92ZWZnTmwweXh5eTljZmI1UDV1LzBBdDFzbTllQzNHeUJuY1JnNm41cVVtb3R5eFlseld1UTN6cGljMHZEOFpqa0lTeFN0ZVNaemFMZlc5SFFKdjZsbm9oVnpVV0Uvd1NzWlE0eS9qZ1NQM0dyQTBsTnJqRWpFYUNoTzEwTno5TUcyQzhsL0Y0UDhuUnlJaUVremZZVEVnMk5zZXBqaVQxU0J6SlJFNXRIeXlzOXlqUitDUiszenNVcjlueVhLdXQ2Y1VKaytzYTJCMmwzcUpWVkUwWmtUb0VjZ0pjWGhBcTlvSG1ramczQmc0VTlMaWRDZjBUMHVVcTBPNFgzdWJwYzR5MzZSWlVLOEZEd0tMeUhudElwL2lJai9sWUZWZk9wZXdmTDZRaFlVdnhiYnZTaTFQc1VVbm9FcVNEcThlR1BJUE9QTUhQSnJoMEJKRHVDMWlVVWZlbDdzcDNpZmxFbVFmek1qNmYxTitTSWhuU00rZUJ5NFJGaU1KWW9SMkJZM0NPR2ZlZk42MmlkTnFtOTB3MXlOSUY4aERxRk1GUmVvMUJYdk9BYVdwd2FzdUdEcWJ2NllpTzRrTnpteGJlWm5kZDlEOURReW5sZFRQQWFLTFc5VDFrcHBsOE1PMWNPcGNlZ3RaWlpjM0pvcFA0dURSU1hNb2pQbmpqempWOFdtVUFmeWtCTFdJeEl3ZkUxOHYrOTlwK0E4cGxOdzFVamQ3Tnh5MHhCK0RHRGkvYm5nQmtzVmJGbEd1QUcvaCtHNVZNYkhVSkIwY0F0TnBUT2JRSHZKY2VOczIxTmFMZ1FsbU10K2NEQVJSb2YreXVOTzU0NS84bkpFQlRrczR5T3J0TXhoeEdlYk54R01VK2FlN2NIZXZ2dkRYT21Pd2pVKzBXWlZqQ0hPUzNFM0hjYk5yQmhva2hNU1NxSWhiU0lFaTFWRnJVUWVJS3JvQ1daeHZmaU43T2xCNVJueTgwRCswSnVxa1liQ2pWeEpuS3plVXFGalNuWWRmVXVaU1o0YjFsVDBOamFwWGE1Y3dIZkZXZlhQbUk5WWNBMUZhcDRLQUZUOExyaGt1MUg1YnM3V3ZGdXlaVFpSSUNyUzJNRHp0bnN2eHNXNm9sa0REUVA1ei9GUGtNTlN3WXlTN0g2NVdrMStsWWVJd2xSMjdtZk5zQThyS3oyMm5iUkJXQWNWc3p1UkJOWlo1Tk91RnEvWGNuYVdJbmdJS1VGdW1QbXJsUEJFSHNWODUzRlBOKzU3cFpMdFJ0VW5DMU1RUDg4QWtidlAwUHNYQ0xxd1FxRzFsZ2oxK0hJSlpKdk5CaDU3RmhYbGNXVmgwMTNibmkvamtmZFFSWTlFZEo0YkRxVzk0WXlQcTM3emdCb1F2ZGVYSmVvWklIckZyTWxUYWxpV01EbGowTWVGeDJHRzgxM3ZJU1JHRytXbi9nY003NHRXMXk5NjNaK28zOVZ6Uzh6R1dvWXpjbjRLdXNEa1B5bXJ0Szd5SkQwUUEzNkNuZHArcGZtMmM3b3Bmbzg1clZ0bHp6NWN3LzNDSjBIbi9GUGE3enNxbU5MMWF4cEVZb1RHU3pCYU8rYU9aSVc0Zi9EanhjN0ZZd2VLTW41V21teUc3dDg1VTNSQjc1Z2g2SnM3WGs4aC84SkdtY0Z6eHNPRHRNNG50dHEzMEpZTE1zbHFjZEpzaVBQRytRVHRCbEJqTVVoeW9Gb1BQUVBkNHpuUUR0VXhISE9oSmFFNCtUODN3TU8xTUdzaGxqL2oxNHpiT3ZhRlJ4VWN2NWZBZnhVL2dKeFVySkRtVzQ4Q2pNaVNKQkw0QmU3YnBtMnVZU3JEME04d01rdXhTY3NPWmNVZDR0TXBnUFRyTTJaNlFoNytETmE4eDgvalVuYmwydDIvQXl5WmxSeGx4NmI4SlJjSnRId2RORjNNV2JBTDBnZmkxL3E1L04wUVpJZ2wwYkRuSmYwckE3NUNUNGc5UlBUbCsxYnZtSzVTdEE3dFpDRHQ5ZUszQ3hXS0VBSzVWQlBuOWdMckMxeTNheG0yZVNRQlRjYTYxUUZHemg4V1hnQmxUOEQrNlBhVjcwbUZjZithb0xpbW12TXNFUnpjR2U5d2hYbEhyUXB6NUhhOXJ4UUtNOVR0bUpGZWJTNlNzQnRaSzBvcFQ2WlhkT0gyZ2pjQ1hjSHNnZ2w5c3Z4bFhUR1ZuWGdQdGZKK1RDVnpaRFZUbVkwUzYvZ2lYQVBGaktjSjZzY3FlKzM1ZjJ3dXdUQnQ2c2JjSzVSb1hRV3Y0NU5qRDBibXdtcDVhS3lXMVAybmdpNy9wY3pXREM1UDJQNWtwNVZnbFlSMklGekJINXpSajVsY0x5K0JPNC9YTkJ6Vk85ZmVMTittb2lXY1M0QlNzODZDS3BqQTFCVnMwYXNkOUNleE5zdE0yWVFGREt5c1FITmY2Ry8wN0tyY1Y4MU5NQXdqVGRSNFhhOFNFWVdBdXRHM09wSkhzMmhqMUpYTFV5OE5vRlByTUI2UFJMY2FuWitZRUZsTUhhYVZUMHl2R1FYamVxQUVIMUV2RmsvSU11WGp0U1BuQ2g5MW4zQ2tBQndHSzZib2UvQ2dDaGRBVXF5S2xLV2NsSncvZ1hiREo1VkEzcWNzMlJEUEsyb0YxdThJRWZKdWhnUDk4V0tUaXBRWGs1dHo4WEdBa0lPLzhiYml4aVFmd05SSjJyeng3aE1JVytUZm1zUU5NQ1VJcytEbTkvTlJ0R0xNa2dFU2VxSG5BY2hxOXpVR1dvQnY1TFNCV3V3QVF6MmlTc1ZaaFcrTHNRM3BIanZPNUVCVHV3MHpyTlBHa1pmb284eTF5M2xPdXZkb0RNZzg4RVVpSUFWd25NSUFVbCtOUFFMcUNFWUtsb2I1REpTRmFvWDdDL0cybTJrUmNWSTBIZG9hZFdtS0hGZlRsWU11UEhoUUp1cThLb0JpcTlPUk9rNTMrc3dEQ1NERnU3RmoyWXFoNVJEUVJpWjk4aENZcUxPWkVxUFhYM0RsSGVaR3VKN0ozc0FVcDBucmc3YU9DYWhlZkx1QkdhWENrMjR4N3hCWlZLVDd4NGZ4ZUI5OGNoWEkxKy9Ta3RHdDh1Q0x1TDVFaENZNTh6Z1hTZkszZEFUSUZEY2NGNnU5eG1HV1BtRk5wNGVSOGZ4NEdPQmhpOWdUUklWWm9wdFdUVGpIdEpFNHB2M2lhU2FOek5NcUxLWXFuei9hYjNxUG1IZVUwY3YwV1hxNWR1cUg0UHRoc3EzM2pSVjlQRjRrQ3RGMFJPUU9CYnczcGFTZjhRMjhmRUhRbTVnN2JEbjJCV2Y1TjhPQjFBNnl0REZqL2JTSFJDVVBKOUV3S2hRM1ZPcGtBeDN1aXRuOXgrZnpLYWhMZVY0VEp0NFdvTG83dytUenliL1lMT1BreFN2VE5QMkh6MEl5SnJiNDF3cHV0U0JQbTRqMWE3WnhlRDZNWS9GTVhFYTV4SDJoRVdFREdKbmdyU0EyZE5jdnlNNDlKaWVVYWpUNHlLYVl2ektmeVNLbDBHdHh2d1pTYWMzSFFRYWZKbld5c3NKUXJHYTVudGRsRFE0TGVaczlZTUpGVkt4enhBKzBzSW5BL0VqTWdlR1Bvak9VdCtYeUcwcHZ5NG80eXNweWVVN2V6SmVVT09QSmk1dk83RGRTRmJxNkU3R0NrVC9heHVmRVR0eDdKTlAwUkc1TDl2T0plL2UrL0VnUWoweG01bnNZYVljZXFtN09YbHBkcGN2MThOaHYyNUt0UFNRYkFzbHhCRWNvcEpxV20vbkhKMTkvbnFSU0V3S1dsVjlvaDFBelhXdit1VDVCTG00OHlGdW9ZNjRUczJQUkxhRFVEK1NzRXdWUTBLOHRGRTVPaVRFS0NLeVo2UFFtQ1FPNE92L2FCMFNRYWl2Y2g3OXlhU0UvM21KT0l6YkEyR1dPZEw1b0dzTkxzQ0dzR09HNnBjNFVQUCtydXkwUEhPNm9HSlFDWnNmdlh2NklEaUNBMG8yQ1JZQVVnbzYxU1dBRk0vdVRydTF2c2FxVmxTOG5VVmkrRkpJVCtmR1g3aHhJYzRkb3gvTE1nVmFPdEZBUlBRVjJqTC9hZGtBMmwzblFyYU9teVR1d1BLZkU4eXhOQ0VYeStrUHBQN1VoWlllYlJNZC92bmJBZHpoOGdsUlNyMFhpcVJKYm1STStTNWs5WllobDltUm5lTDZxaHFSMy9DK2w5NU1DZkQvaUlmTXQrU0pkOTRGLzkwUWh4ejlpZGJpNDhGcjc2MlNMNnNlQ3V1bllVaCtPRXRta0FNREpBU3RxR2tIcFVyN3phS09Tc1gzWW1ERlAzNHJEQnR3dUoxTnhBZzZoZVR4MkV3d3gwYlBsSmx0SHpVdm9wL0c1QlNuVkt2OHUwdk9sOGF5U09BQ3BBS0VteFUrUkdNVDNxY1BYc29LZCsya2szN21tMlpqN2JFUzVuQ29PT0hNNE93VmxIWHVWWFEyYUNlczNWcXV5dUhreUNrZ0FFcFU0K2VKQjM4WXJLNE9OaG4xY1pjeEVpalVLVURITE1iQkRLc09rV3FPTFd1WXVUM0REakpQRlN0NytVemZ5Y0hPS1lTbmsxZ0VWYS9wbXY5L2hpWlFuS1hVUFhJWm9zci9WSklLUEtWNmROOVpReGgxNEtDaUROQnhGc1Rma09CbnlyWHdTTXlOVTBwMHlrWVc1RUU0OVNoOC85VlhJUGJrMmU3TlBWQ1FwbXBVK1VyK3JBNm1BMXlpK2ZrNklPUlNRcVZHZ3RpTUZsbkRsS05IU0FrNDlSd284ejZUNHVrVEd0WFp2R1dUT2h6dGgrMzVRbG5DdUp3ZUxNdnJJMGQxY2drblBITWE0OUw1OUFJT3JhSU9FRWFyRGFheGFxRDFPeGdISVNoSTJPZjBmeDFoVWdkSUdNMUZmSWFwT3hNdS9jdXprZzR4aERzY2ZMbTlZSThhN2JDYmhlbHRZNzFIZ05DYy9DbEdVVFNMVE9GTlNMbFdjUzhZd0dETG1sSGFhTFNKaWdZak5TT2JFakVJWFdTNjAzMHl6N3pFalUxQldTUk1SbW1GSHZLRW4vT29WdzI0TWxKT1ZHSkpVcTBPV1JMbkhRL1BiTGNnTzNqS3h1NXVLUlFuSWJsQTFaZjI4YVRYbnFzNGtyODA1YnlXZ082U25Ed05pVWlHMWh6cDFaNmFxc2tjUFIvdjliSTNDdlZ2S3ZuNFdRZGovMFRwZHRpNWZLYTdPeFJmMUdQMTQ0SHl6Nmlpa1hvWlkwcEkwTCt3ZG4vTVhuY1FrUFlhdVoxZVZHVkZ5TmJpajBJWnByQ2UzcTBLeWxVSERXN3BDOWRYR0pEQkhtajQ5NXhWMGhHWFl6czhWUlJlRWpPbURNRzAvMGpXSGhYSThvZ2J5SnNDeTFJQjNTYm9XN0NweW02ZXo0aTNIMHlxWE9ISnVUWVdnaE9EY0JlWU52TVBISGQ3R3FTQ2UvSUd3M3JuVkN6T3RBcjJEYmhHY3llU0w5OCsrdDZTYm5VazZsTFN3ZS9DL3FucjJDWWVxR0h4RFRjU2c3TUhZZ3ZRcmJxUkJvQ3ZBZWxCTUVXNHl0c1cwVVRyd2M3U2VKc2VPNndoVituYzFGV3NtNC81aUdDZXlnNHA3NE9haEdtTDhKbjZ5ZUhTLzJKWGlLT2N5OTB3aTBtTzNUUEZhOXpLUXJVbFMrRlAzUlBRYlloYWZQVHFOekZxbG04Rzd3d1VvWE5ja1o5RE42NWtVWlVvMWxsdjNvL1ZMVThHR2FJSW1ONTRCa3FCM2ZNZ2dsMXNTU3Z0YlhkelBmVkxNNCs0M0NVa1RUc0RWUDNwUFVCa2R1L3doeE5UUG52VStLdmp3V1o4aG5Tc3BFTitFc2FwVHhwYjJnODBXTm5la1QzMmFwZnphRzZ5Q2g3RmFiSFVQK0NjOWd1SzYyelVuQUpBaEVZaU5KWlB3Njc3a1RhQjMyakhDVzBUVEgzczRwWGlNVEdPR0M1M00zYUM2U29FL25SZ0FJMjJvRHIyZWhiMWJueVVNd05TNXM3cjduUVdobXpERDBoQTkwT0ZPazVGbkVaaXRJVkY1L2RrR2FTb1JMSWl5OGdhTVZtbUFnRk1aNXgreFBEVmxMQlN2cmtYdHJFT2t5N3dKTXNkVWhOVG81NStwS3hlWXVOYVNNL1AzMjFGK3hEdWc0NC95cmE4R0k0ZWNDbVZldE9UUHlDYzZjTFphdjk2M3JGbXpDTnVaMC9jWUNzYXliN2pHOWNDenhTT0o3djk3K1pla3BmOGVxcW5ZWEJsdkgxYjBVaWMzR2lXcXBsamlkYTZNWkttdElvN2o5cDBPQzBuZnEvZFhNUUR6dVltMkVRdWpXSy8zMzZscXBhM00rbThtVFUvWVVzZzExSDlnWXpVZC9vWDJjb2RiY2RXRkpxeGVpSnNVcWRNb284NGVLSUUwSWRucG1GMmk4R1NiaCtLNHF3MmVxdTRCWSs0dkpSY0FRcGF3WEhTMG5uTmcyRGU5NzczZHdhSEtjL0s4a0hqSVYwdDdiclg2K3VScjFOZUUrRUJ6T0pvMUFwVkRscjlJdUtyTThuT1plVFNXaXQvVGVhMFhDTU5uODlsWEpXc0piNWxMNnViZEZpN2o3SzNXNW0yTFBaVC9QUXcrUCtJRXJuM1JYRkZkTmRhcWc0QmpzeGY0YTcxTWlrUWNoVDF1L1I3NHVuVXNpNzQ3WXB4U1E2bGN0QU5KbndPWno3Q1c5S3NadS9PcFJ6aTVLZFAxMElub3BwcUNuaW9xRW9KM2pOaCtRbmt5aVdYbWNUcy92ZFRveGcvNEFWc21VUFRZcWVSSXJhVGowM2hRdllQZ1kzdEhadEtsSG5WWVZrZFFnV0NhTTdXRjFLVGZmclR4bU83TVVmaGcwZEpLbmx0cDF2VlcyZ2dDSnlkSTI3WWNycGtGRi9zVDVoTGxJeGExeVBuVVN0YzhUdlROazY3a05ZVEhYcjVnV2x2VU5YTDBEWE91TEJlUFRSUEh6T0RLL1hsdEZPRWlSYmtQTGFxWUlPQVI3MUxTVDEzQWRmME45elNmRll1cmg0b2s2NlNpS2tab21NMnZpU2VzTzVWbm04TkM4aytHSS8xanlUYndpcUV2R0xuQkFOaGR5RGtWbm9QR2d0UlRKaVk3cUlZWjVNeXRqdFUxa0djRUJKTWRkcnJveFZEdExvcGlWRlNEdm1tU0hwdHlYZEFGUWpxZEZoRnFPNkovWmJscEpBRGpHQnI2VENDc3R5bEc5VDZhekViajArNGhMUlpwY0hiYUxrQWlzV2xOejM1THhOL3BmSmR4bDMzTVFwelNHVHp1RlBHWkp3T2Y4dHBsbzZ3M2VtOTFYSXZ6N2JHMFFGMnFqRXBOQ3hxbXY3RXFUR3lPZmdUZktUNmVsL0wwTUdOVjRtY3N5bHpVMFdTNWU5emRSaWhPTFM2b2Vnc0c0eGx0ZHlHMUtFb1M0VWJLRHduR21pc2hoM3JxeGxlQUx0Tkx2emQySXFiQnBETUU1T0Vkd1p3aTBvcHB5NzNXcTdSTmFFM1NIRkVTQURJTG9BZSsreE42bkV6VVdseHBFeDRTd1BZMTk5eWswSTdKUm15NHByWTBJUEJxVjEveDBVUUZPOEtCc0ZxbEhrczZmbEFNOWl3eTZvd1IyVzBncE9WSGdXeThycXBUcGsySEU5SkVpTTFpcFMyVkdRQnVXT2pmcnYzMmd4MkVJWkxGT2ZOdW9IcFNXbnVWTTRSMStEVXEzUGVHdFFDaVo0MkJqTGNtT3dHNzB3TVdRR2swR0tUWHJlMXdoK3hkTERoNUNSN1hxaitINlpSdkpFN1dWK29xVElvSVZJQjd3V2I0cUlDL0xxMDlLRWZ0eUFxTEZMOUlVT0hkMjFrVDV1ckc1Y2JDMWg5c1dnVFgzNGtDRXN5ZnpuNVl6aS9KVDhmaHUrdGE5Qy9wQ2JqdVVNM0l0RjNFeHFmZExRTm94WGRZd2xIY2cxV2E1cWlkd1lsS0p1YloxRnY0dWJHWlBpZzcxV04xRllzNy9hNm5rVTFqUEtKRjZoU3Y4alJFL0d2bzFMTUlnNEgwSzBwaFVhV3JpeUlJK2t6SU1JWlo1eGJVaC96Zm5EczNkU0xPQ2tXN2VKTTZ4TzZhQUR3cU9DRndkR1ZEQk41bXBmekR2OUt3ZEM1UHM4a01lZTB1MmE1b05jTXZWcjNxYWdtdzlrWVdRQnJGRGozcFk1UlJ4R2IrbklsSWtqZVlud3ZMbEFsRVZjaTA5bmpocGVoWEtJa2hsbmZwbHo5Q0FmUTdNTEU4TVZGYVpDR1B4VHoxWmpGR2JZNVRzNmJVcS9mVlhrUzZwb093Nzd0aDNOTGJsRW1XTzJMcWFBYTdlYWJsNE9PQUg2YkNEOGw4YWVJbkVLdjNnL2VzY1d3NmRPV2cwMTByQ1dpWjVYdEpyUEY1c0lvY2s5NkIzanBLRlFETGhQV2taMUpyamVZZ3dFODFPTlI1Z1pETDdRdVliMXpabXRoSGNNL09xakRXOXZzZmlNMTJOcm1PRXBlbXovVjBudWJ5clA0cDNhYTVHUmViOXQrcEYraU9RVkx1MmtQYWg3TnRMQjk1aFU4Nnd5Uk93a2h1eko1aWJDMDE4TXM0ZkcxaWFRR1g4aUUyckhkTEVLTWR0ZnpOV1RPQnVtdGlvSFRFcFpsY1pPbGdJdFNBaWl6d1RBdGpMZWdzdmRrdEtGTkQwckNIR21CQkFyQ1B0VEZoYlp6dTFUTTVIUFRoVnpEVmdvVEl4WGxITlFnejJENWNQbm5IMFY3Yk9idjlnZG1DTmhnVnp2V0laSjdlT1JwVE13eFB1WlZSeHlrYXJISEd2U3hSNCtzZE10QkhBakpJNWNHQlI1RmxhQ2QydjdNMFVEZ0JJQnUvaUN1SG5NVWRER3NuWEJYMTBFalB1ZHNseGcyc0lMSWJUVElVenNTdnlHa0tzOGV0TEVjVWlVWnNkN2hSY2ZvUThQNVltZENvMjJ6Tzl0VGtZOCtoNlB5ZTlNZXBmZTl5R2wzTS9kT2hoTGVERlkwMFJXTFEvYVNhZ1ZRL0RNWldnekRFOTc5T0ZiRXQ0MnhXTkdJSS9FQWNBeUVWR1J0YS9PR2RkNkJrcktIbVpjNyt2T1FDQ1N2bHA0YVhrN3FnVWFLVUoyWXE3TGl4cHV6blFXTXl6QzJtL0RkSWc3L01xNXRJejRjVFNadE9CU0FKWnBHZk41THB2VjVQUXhSWGVoS3EweHRsU0VFOGlMUmtMU1ZZRGMyME5jNkg0ZnV2SDdudkpTdzdBOEMwYVcyZHRDbHpnM2F3Ym9GdVdMd1FLNzRnQTdpblBINmh1Z1lFcUhFcUt0S2lwb3dOb1BRTU0zZzMrRk5aTjhoM0w1enJzb2hUSHpIWlhvVlZsLzMzeGhsQ0FMMHB4dzFab3JabEp5Z2JaTC9DWHFrL1ZwcnZ3WmlJeHlzV09PSXkzU2liT1B2S1JUellhTGlyblBKa0RWQ3cvamZ2YW5CdDAycWp0dEVnYzdqK3BvUEFpTDFxTWZqL1hjb2pCMnFySUMyNjFKU3FhSnA4TDZiQUptTUtMVGhQcXkreWNHZVhqWlAyeHZrUDFwTk1CMFpKYVNudHpRREt3MUcwNkRYSHdRT3A5d0Q0UlVtaGpDUm9xT2FtRVc1Y0ZiTDZUM29pdTVtMHowWk9hZFJOU3hWd3VRcmxZYlRzckN1TUdBRVY5VnVsZHh6ZEFuMzlCMkppb1U1Tkc0eElZRTNzYzRvYzFmZ1VQRFFEb3d6WU56RzgvNkFNdFVtQzh6T2F4RENRKyswdjJwZG9ZbFppRDFvbFhDWnlRUHkvM2RnUkhhbnBaUWxLdFl1djVRbTNyR2dFUEhBM2xUekVxbUlCT0lybEZRM0tkVEg1djlMY0V5QURhSzRTL28yMWk0N2gyMHJKdXI0djVUanh2djVTMlZzMkNZTlcxUEh3UnpERjZKT0l4YlFXU3UyWGJ3bS9sU09DTHpHUVkzNWJxWmk1MmtUdThvbVNKRGF1ZDZEcjh5MUFIUks5KzJYblY5bWJoSGJZek9VYWt2ZGgvR0FIZmxtYTZRZVZkc25kNVJkWTRtMlhaRkpCbmgzdWIydXJYOGkydC9TM1NBRUJ1UXhzdHJmcnFqV2lWMGs3cTR1TEZEWG9tb3FvcDd3NUVpV1d6N1BmcFUyQzJDRGhvWmVsTHZqMXBsZGorL0VxbDdJNVE3U1B0Rmw0akRzbCtaRnNvcHI0NUw3TXZDKzRNMmZWMkc3RXAwNzJORGk2SjNsRDdobTdvL29DM1p5a3pXelBDcml2OVcxeWlDRUdlN3N6akRMQlZCM1l4dXI3U2lURXdBSTB6YkthQllhQnRlT0t2WXlsanQ5MFNBUGt1aUhNYVNsMTRmU3ZOc1Zzbnd0bEhSb25BUm5Wc28vTGVVVFFaR1FzTFJHOXZnUmo0bE9Zd0xnaHd0VmZpWWZVMkp3c1gyZzVCK2xpTkM3WGRiUTVoc201OFdsQnBHejhyRDFlaFUwSXVTM1ZjMU42YThRdmF1M0xzcDY0TE5UZWVBOHZXWDE2YTcvQ2VNV0grUkJOdGdiQTZ3ZGxKTlc0RTE1Slhoekw3WFJ6Ym1HM0s5YzYwaWhuZTVnSlRTMm9xcGx1dXRoN3VuTjl6NFlydUN2MmE3NC9IZjFxcHFDalVCTVZjbG0wM0RUTWVVNnBWYzd3WFJRdlBNTEVIU3gyYThsSk1TbW1aNjJNdDNHNVFuTzVMT1U0TUFNZkd3TUIvTjZlbmFRMGM0Y1dVWUJFNEp5bTduWlp1bVhPQWdBM1NUeDZlRGdVblhXWXlIU1Q3L013VFRHKzFrZlpIVVBVWGpnRmJURWdBVTVLbEVKSDhBVlZZbG9wVDhRKzVPd0RyUWlFRGlHZDM1bEpJV3NteS9sSHBzeUxaTG5pcjByekdVdnBpdUt2Q1htRjNnZUtZSXdsK3haajVIalNmcklvUDFjaXhEQm4veGQ0OXN1MHBsb3M1L2pUR1NNaS9Jekk4VG5JTmFIbTR4K0gvdFYrRHdUUVEvckhCcnVyV3dRdW9oOXo3RmNIMVc3UU1YejB3UUNBRTlHYi9POC9Ld2U3VTIySzJOc3dVWnRCQmNaVzVLeTlsM01qZ3ZHYVBuYkJGM3dOcXBuOEtqck1CM3pSNVdSTkVCc0c0MVJQZXBjVE96UCs3S2pQMWNDVm1ZcXJmOTk1UlBOS1RMRnhyaTBuTE1wRFlxSXBENG1Ha1lzbnJWckh2NElkd0diRFp0Wnk4amJuSGxkdjljRTJCTlhQV29tM1kyemhtdmtuWjZ2RnU3K0taSGFOS2VuYVg0cDBRdlpQUk9XdjhJWDVOZmwzcmZpa0NVYjViS1FjbWxtWjkxenhQQWg2bjViaDJFdUZWaTFjandybXVXUnlmRmgzZm9zM1RFUnh2MEdUSjhxMUxKSThYRmU1dDJ1MGUrZnc1VTBFb1lESlpRcUVKcDBlU0E3UWR5NE5CRG1wUTBFUDNGSi9NSzR2WEIyRUFnT0tMYWt5Vnh1bXFxSVdSSDNVY1lYek1KYVVKc2hJdzZEdCtmMHlkWU1acFRPRGxXeGsyTWdSZU9SeU90N1dsYXlhc2FwaVdhckJPMjBPdFJ5eE9EOTg4RG9DaXJzYjFGUk11dTFwcFpnQzh0N0syb0VQTjVPYVJrTzBXaUNQRnh1Szl3eHdZQ2kzNGdJWnQ0Rk9kaUtwUFlZVWhIMkxUSldrdlRMaVZ0UE1iVjJ3Y2NSU3RIRmkyRVk5TTgzaG00ODQydWd3K3lLTDlIdEh0YzBLL3hST3RBcnlKTHJtVVdnUTB4S1dZeTBJQStjaG41YS9jUUR6cmVPVUpJQTdiR1F6b3RaclZ6bTcvdGorUnNVU3NVVWluWUZPZDVPWkhCeHZiWmc2UWdjcGdwdFU4TUhzazRLekNBOVRpM0lhOC9JQzR0RmFwZGNLeTNqZ0U3U2thWWovOHBRamRTaSsvdHkxcGZsb3U4TEI2YjlHb1FKa0hjbWtkczFCTUN2a2twSURSbU85MnNQVXVnUFpsWjczM01JQ29xcmZIakNNTmpmQ21UWi9VNG9keTRvK2pLT01SeXdXYXRwTVNzY0xXeXVGTjBLV0lVc1BiMlZsTENFOVZBbFVWaTBuOXRDYmsrSkZBcHVUd2tpRW9KQ3VqVzV2TndiTkN3dUZOMURvakF6dnkyVjNTeTZHaklOTnJBYmRTYlF1N2FYeG5LUjA1RDdHZVZXVDZRaytxQ0pWV01Mam80Zzl4T21TbGhiWnFDT21oem5pUVA3S1J4YTFBUjArK2kwdStEWjVxbjYwUTBXTkNlc3k5K1ZPeHJNM1phTkc0UE1oVWdaenhRYmZpMno1Z0txYnhLVFFkaXZRTVF2UUUrNzhkMTRobW5IRzRTRFFjS1k1bFpHeTFLbTJWeU8wU0RockVvZjdYaEJnU1ZjZDdqdDd4TXJvSzg1VHVBS3lneGhvUE1VZUM5aDB3Q2F2S2FTTnRRU3BoVUJGWmtqZUVpNGcyWkwyaXR2WG9RSnUwMFZseFE1VmVVcmVXS2dTM09jSTE0bDJ6VG1ZZ1BvaTh6VGNPRlYreHAzYnRqNHlidmoyTS9pdDJtN29sdjFsQlZBYTJWVXBoY2xXN1RGUVFjQ0Njbmg3ZkZNVko2Z2JzbG5VNEFydEtFcG1Jb00yd01oYjUwVlFnYThnU3k0M2E5UTZNSGFZdXMxTjJObGZNMWd2TzBWOHBpbGdmVlNnUVpZTkJ6T0dRUVlxVXMrL0ZmZkwwUFNlN1FUaVB5UDBwUnVsN0N1MFFYU2ZmQWhpRWI2elN3UlBoRnhMR3crOHFSdWFhZVNJVUJtTEkycC9yeHR6a0tJaC9qTkkxL09wbmxSa3Q4NERZSWhXZ3JwUTdKdHdhd2JWMDNiRkJpTXYxbytWQi93MEQwWjM5dVovOG0wYkxFK0h4dDZCZ0pXNk5TQzA1ZTVsUGFGcWUwdnQ3d3p0QWVQays1YVdYaWFoZUtIbFl3Nkt6WS9sejRjbEpnNEJHVEhVczJveWN6dGRWWTN3c3hzdC9YWjB5M0J0ME56bXhycVBmZTNvOG9ZUXN2Qm0zNTZjR3ZMN1JKcGcyakdrTHBxVGluRmVBMGc0ZE1ObDhYMjJVSDhyTWJBZXJzZDRMVFZ2bXhMaFVTNnZSbGI4bVJXeWIvUnFybDZPUnhCQ1dZUnRtU096cFJwY0I2ZjlXbyt0Ny9DR1UxTVNkczNnMXF5VXlzQlRpK2t0K2s5N2dTc3FnSGJzRitYYzVBOVpubGdtMFNtUmIxWEl5MDhBSDFsUkgwekt0N0d1RCtoRnZOaVRveTA4WncxUEpsdEZRaEJIemNpQ0dCVjJaZ0YvMlVZVlhYL1hBVVM4YURaZVRET2UzdkdMVmYrcGlsUEF2aXlrM3liK1pNR0JGUndGUWx5eTUzcytFWVd0UGFxaTAra09oMXdWWWgwYmh3UmE4Zm90L3N4UzRreTZJL1VmYS9sUENKS0pUSFFLY3hCM3Vnc3FnZDZQR1BoTytIUzd4cCtJRDZIcmpJUmx0b2dnOHU3N2ZpVnNlQ2xMTytCcUN3RmxVODJ5YmxxL0lGOEdxWVEzVWthQS9vVGhDVTZoTTJ4UWdvRXlTd0xLMUgrUzRKcGVMZjZqZkNLMnNFU0lxN3A1ZVBlVVA5dzNJL0gzaU41bXpJUlpvb0gycTVTbEF2MC9BdlUrc0R0bmJTOEhkYVFEMFI1NjZlc3ZmWGhxRUloTUplTWlBNjkwdFlkc1dCa2xvc3E0ckNyVXoxWHhWQlcreFdUTWI0bXpCaWppcTZwMG9lelg2ZkJUK0o0QVBwOThnYnNJQ1NqbXJkaUtvYVZZaFVnWG9xTkdHdXVkUDlqL1FmU1pPVGxLSDBCbGhnQzViWWZ5RzMvaVhQUWIvd2hqcXJva1dXRjNLeDY0Y1hUcHVGVGtyTnlhRlhxelArRmd3SVV0Y3VhTzJMMks1K3Zta3BONzFUd2VSbHlwZ01VQ2x6aFZtM3RKa3ZOQ1k4TXhDUGszN1BPWXF3N1NCZmVObVRNRHdYUTl6NW0zTmZ1MERiU0MzRGQ0aTBGVU1TbVNuMHNsYnNKZ3g2NG9LQ2NvU0lRbGhEWHNSR1hPTXZPSlFZREhHNEYrMXk1RGd2bUUyNkpmcWxOak9oWUEzdnkvRktwNHE2aVJpeXJvVXUyeFo3WkpxNWxES055TEFJa1M1RlRuSUt6NXJyUFRYVlkyalh5TWVwNUQvWGN3MGV1WEY4My9rZkJRdmZsK3Z4YWoybnlLV0xuTlI3NHhYeFMwd1QyK2F3OUhjLy8xcE1WRk1oUzI0aWF4dEY2Q3F2VitYNkxSbTBrbjU5VGVYR25qZy9JRDNTSHVONFFpdFFtK0xzTjUzdTdvcUxPYXdoS2xQaDcyc0E0bVVOR0VHdmtvTFhBeUYveFd6UTJyUjA3SHBJaTlUd3JTRkxQbHRzK2pSTHA0WjVibkxnRUhBc1huVStsQ1ZUNVFOQnhYKzdiQjZBWnNiR2NtbEhQVzUyajVFaW80N1VCOTZVOHdqQWNRcWtNakV3QXpEV3RqNEdGSmVEOTdrMEZ3NUVzTENvRFE3TXRpL1pkZ0hjRlVWejZoWTlNQ21laTlNUFZXMjFDbW1Ja0c0SFF3YkFHaXRVRURld2dTR0tRek9lWmpEQ1kxbDllblFyNGlqV0tIb010WmJkbHV0WkZnbnI4TTFCRkkzWXlqNytxVzBHcFJxM3FHVHg2QzhqSVdmSGEyKy9UWGd0SktLbXNvQTdlT1ZFNXROWExwVXhjUkQraDhvWXZjZmVOUmlySXFNalF4MTlzVENVQXpDWGFPTFRPd3FrRThKMi9DNmpDTEhoM2QwbVp6MS9mWkNMMk0wTzF0RjhTbUJBNnJwZVUxNERsYWxRZEpDd2E1QlZGWnJRREJkeXVYWlhjQ0p2S3ZMc0tKUEttcTFIUlBuMi9kR0xsK2w0cU9MdFVMZzVnZnQwQ0VnZ2NOWnAwWEFjSDl1MWhLNEZrRFkxUTNBZEZSN2V5c1J4cFVSNUNoMGJTMEtkN1NQNHpDZUZub1ZYSHJreHVGazhib0hlakVYQ0NobzYrK3BzTTM4bnVuNExUTGpTQ2dkS0lpSlBsWTV1dkZZNFUrM3JwOE95UzI3WVF4M083U2ZxT0pOMWxHN01OQktLV3NhTjJDc2MyRzN5YmJkeFhNQ3doRzFmV203TzN5MlJHYkZ4SktNK2hpaUIvM3BYVUZ1TDZIS1I0TEJDem5sVTRaQ2ZyTkZsdVNzdUcvcVNLRG0vRlN2Z1g2d1ZuWFpvOUN3ZVhXcmNrdXl6d2tVcFZEbXBlWHVlSTFIWDZWbWtrOHRSN3lZanVVK0llbWNmQVR0ekVVQVdzSWtUdUNHOStFeDZhVUlrZjA4c3pEckowNDVsQzdWZ0YwWG54ZDJFREk0ektpbGRoVTBGaEd0MkVUWlRBdmJvYXZKNVMyMXNPQ0FrV2tLSlFFTndXOHNIZDIwSDMySnRJM014K3hSK1hvSjh3d3JmMlZ1MXVTZ1IyWVdYM1ZhUmlmdkNBMUZHRGdUdWtoT2tDcU12cDNJWWE3WmJBaW1vL25zOGduTWJpVkVjZVM2VUU3dzRsWjJiR0hCTUZ3ZjNBQ3psbjNha2hRd24zTSttdXdROEQvVXFqbERaMm5nVDN5cHVFWCt4NlUvQnlieFlDSWMwWUdlUFZNYmtoYUpwWUM4cFJieTVySGdaM3hrc0VlNmpHd3FtOEhmWkptU2FWdUpDKzZLaHdvT3NJTGhNM2w1bE1lT2UxQnpUTmwxUENUN25UMk50aFVETmEyamtEa3RXZ3JFU2JESWwvWTNMNjFkdjdUR0lWVEhYWEpVS0swa21HbE5HZ1ZUODNFMkQrZ3ZvV1NjMUFudkVkWEJQMWpnLzc4emd6cW1lc2M4SjVyS0lIYUdmK2lTSjBRQjBNUkpKMjVJR3U3OU9tME5YQjFod0ZuZXRLUGVPZDdvUGllWTdmMnF2Q0kyQkJrV1V4SFR0OGx0a2syeThLQ0o2UE0rYW9qSm1lakJYcmlxdzMzOFNSTVlibVd3UjVtTUpqcFpuWW4rNFJzb1hsTExDL3g3TllQTnZ2QWZNSFFXVXdHakVJUDFHc3gzc1VVY2x4YjQ0Yy95SFk1Y3V4UVEvY0p3eTNuTG42SEJhK0NYZ2JaTnk0SThZRWJxSlUzWnpmV21RamNqZnFtcXRBT1dNdDU1WTZqSjNFSTIxbS91TnVUTmJMeTV0MDJSMkQ0U0VOY2VsYlN4TENCRGRxTWZjVHkxNW9ROGdWcHJsaFg4OVhSdnBKSFNIK1dkcGpMNmQxcTR3V2ttaEtvdnpYdWcxK1ZoemRHR2JjZ09xd3RpeTd3MzV0eWRDQmcyZlNENkx2YzMzSC8wbWc4emlqT2NISzNWSnpLZFlnYmRXWlpzOWdjSktLZkR5ZFRuUDROc1h6Ym9XK0JJaTVRWHJUaHdFMThIdGlhVHZXdUhXeUdOUGVGWjZEOCtMaE05a2dmOXdpZmNsUjl1YzI2S050YzlzekZIeC9McTNvY0oyc1RzRGliT3NqMDBUSzY2dHZ3U1NoeTZpcWcwb3hBOGh5NGlYZGMxeHpUcUR6V0g2UHByMWt4UmU4N2ticFdCQzRIS0QwTm5ESEZhMWRmOGtuclNPVXpLbS9lZUYyY2wreFJLbW1kWHFwSHVqMmlNd0c1d2VVTU44UXRHUEhWKzNrR1JjVCt6VGI1MHdDN0ZzU3FFNm9jTjdBQ1VJc0Q5UmJLZSthTE5mcXVoT0xWWkIvRy9wOENLVUFNNmxaKzNZUGRRVFc0TDBhbXhkUndSczM5OS9LT0FrRjhWOGpaTzQyQm1pdzREbGs0Q3RWZTNVNVZPRXU5dXIwYjZpaitKRWU1YWZERUhaS0d0SFVWdDQwVGs1ZWJTRjVZWUNPTm91SFdvM3paNFNQdmMvb2FhZzE1a1loMVMrUXMwcnNGMU5VMlZYc0dDRnJ0WjUrcUw2Y01FYU0wTjlEZ1BUdG42Vjg2NXJ5N2FrWGFFSjlwKyswdE41S1UxOHlrVEZhNCtmT0E3VTU3OXZjVitBcXMrcXltb3daS3dudzVIOVo1YTIzVUw0elA5MmhtUjBWRHlSaGMrdFkyWmkxS0l6czdPMVNWTEJOc0dDdVlUYjNZbUFKSjkrQU9CMTB0MnlnWDZOOEhJQjZUa0RSNnh2cjJCdmYrZVl3djlUelcxVytpQXV0TGoyZldlZGRDTko2anJWZ0V5YjBrcVVJbXVlRHRYbXpPUFBibkIwTHpNdzlEQmt5WGkxYkh5QmJhckJrWnBaSUhSWUxab3NiTnJPdzNyRWhmOExGU3FobEkvejNhR1o5NkdjOWRQVlBkSXpMZFJBeXMyZks5SmZ4aEJKTmljL0lkV216d1k1WEQzKzBvMFhHQVZhZkZZVkdrcXBQTVdjOXBnNStaU2NzamxLV3Y1UjNzQ0lyMzlVSHZTRGNBeEtHdGxxdHRiamNscDRQSU8wUkxwOUtkaDRYckxqQ1FoNEYzZlREU21mb0hyQ1FMTllyOVFHaVFHU0hLeEI4RWE0MG9keVFxZHJmTkJiaURDT1p0VUx0Qng2a2M5MmhNVjVyUWlCOUp6WlI0YXMyWlA4OE1yb3M4QkpnNis1MmEwS2hxb2dLUExjZlZJQWordVJnTXdQTmtia3pDMjREYW9ZYVNzUmtZS2g4Sms3a1RwMEszcmcvcURKcStFL2xxY0NodU1FRjZQTnpVWE5ZNEdrUUxXYUZGSDZVVzIza29DOFJaM1RRWkV5cnpiVGRTYkI3cnZiNlEvOGNLR05kYlJwUnA3dHVHanhncHFDMS9EOCtGYnJPWUlVWm9yNFlsNUdjR00xYTZQYWc2Qk9YUDhzV25IN1JDWEl0TnN5eFVQcGdrcmphVUQ5ckIyQmp6WE1UYUZqWTBVNEIzT3dzcEdhMDMzQndxaHdaYjlNQnNzMFdPOXoyN3JKTjNUQVpxdUVwazJ1YThVMDBvcUFkR204VWJNcGhtKzBGenl4emVXU1hudU50cUxVUnNKWHJBdEd1Z3pZZkM2d212Ylk1S3l1RGhSOWZvY2YrZW02dVI4dVZGcHJ0bFRJVDdYaUJ6Z2lybHUxZ2pmRkhEcWs3WEhvS2g4VlJtQkxteGZ1UkJxU0ZvY0FZRzN5R0lXcFAwK2hGa2tyV2RCb21CRWNsV3JUVkJHUzFic0QzN1RYSnFadE54RGFzTU1NdTMyWndzLzdvd1dIb1VFWmErTkUxaFd1bVRoU290V0VmTUtjd05qcGw1eVozMDR4WVQrdDFPenZXMnV2ODVaZFFtRHhSWlpPUWl3VEdCNGYvcmZCcjlsclZ2bGp0NFowWUxJVkI0T2dPc3NxUlkrYktYZ3ZaMHVFaUZVc0JmWktuL1ZtVkx0b3dKS0RNMXBZdjN0ZUloK2JDWnZWTk9MZ0gvMkNWd0VKM1lJYmNPMFdJRUlDdVJqa3VoQXpMUmlBdzFCcGE0TGJCMXpWdzllV1ZmdDFwSmRCWnUxaXdIYlhDNy9oQ3FFeW5IK3grZElFNGNONWI5M2d5UTBjckR2QzI0cDJwS2YwWU9OSklkNFRLNzNhWW9mT3dLTWxoOXBoL1puKy9mbVIwWkM0ampkZEZValhXUmxMRUY0ellSS0c1RVFpTmN6Zlh0Z2tmdmttaUdYZGN5akRqM1RGNzdoWGMvTXNBclZVY1ljbU1SWXJxR084aXFGdy9wUndONnplSXJZMERyRHZlWE41TkZUbXlxM0NXcU9SL3lJY1ZkcUs0V2FsNW41bWFTTGx3L2Rva1ZFRkxWK3FLVXRPem1PMzZ1b2xoei83aUNBdXpoVDV3Mi9zd0IzaithYVpCY2tHcloxdkh4T0hGNTZIYnEyL0tOdFRJTjBzV3Q5MGZkdlJJcVdjcWNRb0VXZ2lFcTJZb3NXZ0hYSmM3UEJuUXRWQ2E3OENtM3F6Y0ZmNUhScmF3UVdvaHVBTTdKWHhXUFo2dVl6T0cvOTYxSEhPenJFbkcvNDRxVzNyZEExUnJSTytHOUV6YlhEb3VYSHdoekFJYjM2VndnZTAwRExCeDFaTkJ5SXl5NUNIZzdwcC9CYVpmSVBwNlNYb2Fzd3g5S1ZxdUw4a0l6RjJ2TTI5RTdaQXBoTUpraGtkVkR2Sm1RbndtQ2NIL3pDSFBtYWx6NFU1QTVIOThJb0NscmRVRkMwRFJiNmsxREc0YVVtaUVPMDJRMHlDNldnN2dFTTdwcDZjV29GM3lrT3YxZmpDZFRpaSszOE9KVFRjL1pYc09qRUNITlNDNGVnOXN5MURuVG9WMFNKd0VvNmJNeTNQc1pkSlJSMm0rQ3hueG1vMzFIOU5xc09qaWp2RWtFSjFTUXppMklHbnBDcmk1Y0diUTFVSXlteXdMOEZzV3QyaEdUeVdJSjA0RlZuM3l2akJVWE9JQXBxdklVd3ozQ2hxdjF3UklZd1Bjc3NaR1l6VzNid25XVG9ZT1pCbFUzVXVKbXYzMURxTWl4cU41aWpKTC9FSGJiMEtNczVHYmFUS0VtVnVtQ0pSNUpuUTN5MW1VOXRJQW9VaDdmUE9EQk1QMHQ0cVp0cUQveXpHSDNlYUVVOUJXUjFCT3BSVzVsc2JXZGNUcCs0TXVPdVh4Vkd1ZGt1V05SU1BVRFpPVUlnTzIrTTRDUmJOWFBTakhtQnRhVGZmS2xIcGluVXhqQUVybndjcWtXMTBncGljZmttM3YyNXpNeWxwSDkvbWF5UmZWbmtqNFF6NXpqaHdqTXoxZ1piSmtMenVFMk5Oa0g2MWlCZkloR1NLL21LWE1relBkYzVQaUcxbFZkL3R6NWZPK2Nma21qcUZzQzdYYVpUVmh3a2hhRXFDMk5LNHBsTURQV1RBTldvZnBwOC9sVTNrbFlUcStXNzdyNU9NRDlBOG45VitnbEJCYzhnN2ZTRkF4U29wN2lIWllheWdQdkVVTXhZclVTZG5jK0IyV3pRMXpmRzI5cUZnQVYyQThoOVZVaWxSUU1yWXY5bzR2YUZObEpQKy9NNmdqaFllWDNsQU1veFA2M1Y5K0NDb3g4MEk1WVJ6VDMxbmw0empQYkNzVklZMHVWMnprbFh5bGhiSkVUMGl6TGthS1JkRmYyS1BpTHdYaHJhcG9vd2p1S1ZjSzNtQ2hyK1A3U1NpUHh3TTBCc0srV2g3eVRpNmIzc2kzQTd1bVRWZ1hYOFJaNHQ5Z0dlODJWS1Vlc1Mvb2pnZHhkMnljMVl0T0s1TXBKdUh6NVlhcnZ3amF1UHlkRzAyTHJneXQ3My9aYzFJWVMxTkh1eDluMzBNRDdLMVhROTYwY29lU253WUo0ck5RbXhyNlVWNkt1SzVPK2hzOTFOKytza3VzdlRhaE9kZnlxWFFjUlVRMjZDeWpDeE1HQ0g3VnJwRmVtZzN0bkR6RlRVSE1WSW9uVUdzU3JGTUoxdzFwSGtqY25SRTYranV0eWRWbW0zUW8wSFh1WVkyazlWdGJkRGhBNXRhZXp4MDk5b0ZqKzAya0Y0V3NSTENKeThxSThzUHBzMFpkRzRDOUp4SlFRd3ArVGVxbk84NHA5Q3FpYldsUmFwOGMxMGpJUEJvSmxMWFBBOHY1cVdmcHlnek9VOFpGU2RBY2VBUFREcDMwK3MySmVhMkcxR3ZrVDh0THNNalFLUjRmWm5ybVdPTWlmc1RnOEtxYnBFMytpWDdLNzJ1Z05pdlBaTEx2RHZ3OUVoU3N1enpyM0ZMZFBFemNQZmVtdUZyQUNZcUY4LzQvMWMwd1RaUDc3a3pPcHR2amU3WkZwU3M0NThtOG1XSzB5cW1xeXBKSGFRV2NHNjBOWDhleE5VVHNQbU5ZTjB5TTRUdDQzMWRoalRVQUI4Zk5VbWVCSkxiZERGdEw3V1M1cFhBS09DTEN4bUM5elRQWE4zTlBibzIzb3NTUE5LK3pSaDdzcTZjRW85UW9ranNRaEZBRWYxT25tdFMwb3Z3YXQ2RGdwazZJT3N6U2JsbUlCczQzN1gxbGhpQWZMWGN5WUNTMG1NYWNQOEJPOHZSRXBQY3Bmb2tzRG5pWTZ4a1FKTWdLVWNlOWcyc2xSc24wcThkcCszN2I5SU5Mck84bys4NVZqelZtRjVoN25vRWhzSnpEbjltUFdjVzZlZFozMWMzSytXZHV1SzNUQWV0ZTJ5OHdqdHlxNzJKcTVyMFMwYzJETllIdDR3UjY4M2lmZmFHdmx0eTJ1ejBnVXpFWWtxVU9NY1RjMVNuOXBaTGVrQlFJdWlEUWJIeTg1Uk9pSWJsNGFFMG1MdkJ5RFdVK3ozQ0t1RHEvbVlWM2lST2ZKSjdpRlgwYzZyQlBNbFFLZ2ZHbXdYMEFyazlZaG5Ib0p1TU9LaWI1Um9PdnNiTEw2a1YzUUcvOXR4Q1MzR0loTE45dVZmTmZ2Wm1iM2Y0UEIxQ2pwSmhkWFUyL3VvYzdWc0ZuNkJWR3I2NEFhcGdoK2JCTC9GR0tFM0lLSzdONEE1U1hQZVBkUWxJY3hyUHJ2UXhheHJZWmFDSXkyb3RMbjV6QUk1TlBNV2RKTWd3elZLUVV5V0ZFUmtQWnBQL0daRUw1b2FkNit0a1h2NURzQ09rdmNBN0I2ZHRmcUNTZGJOVy9LemFTdVhiK2ZkZnJUcUkxbm5kUDVJZXo0emE3WDdFZnpubUVmVG5qUExDU0xsTTlHWnEzUmo5Zm5zNnhqNFR4VmF2dnljaW1GbjgxTWtlcHVXMWxTV2ZnV01iYm56TGczZnMydjRiVGxHdG9sWUJ3RVBOZm9UVzN1UDdnTE1FeHhBWW9KREhrcHZJU01rYU8rTHQrS1NNVnEyOXhUQTJJczF1YjB5ZDJCb3RsVzJLQmh5VWI5S0IzUUl2dFZwWjhUQ2ZPZks1TkkwSW5HaGJ2Y1lodUMzSXo2U3h5VDZqeEp5eWhaWHFuTzJGZnY5RjhaRkRtVVNKSXpKT3FIUWZHRGNPcHZJc3h6bE00cGNPMk9yTmtYQS9ieW9GbXdEZEg0UmVad0x0WkNtZ0VFLzdjc29IU3c4blFWbnYzS0EzakQwZnlPek5PTGY4aXJwbVByMnR6RFJ6bVRBVFcyanlxZG5nSm10aW5FM2ZYdW0yODVRc2ZrdXN1c2VFRHd0WjJpQ0RaNFd5U2k5amswdHZyeWhKbW1TT0lxMXlGdUFPYUxOVGUxNU05SWFPNDAzdDJmeHVPWUY5VHB6dVRsdndINU1uc1JrZ1ZEa2hvVWd6N1dZMG9hZC9nUFg4RFNwTWxKdlJDenAvak43Szd3ZFNhT1hkY3h0NlBDK2tNaVJoeVlJei96ck5zK3Uva3FOTWVSSGYzK1lqSTNTUlZYT050N3loK0tIUCtXdHhqdmg1d1hXcTg1TmF4Y3hyQzYrMHRYN1QvZGlva3U0ODlHSzJodXFUUWpxaHRTVmZyZ1J3K1ZNUHRVWVRBMEMvQ3Q0cW5wQ0NvMDZreUl5OGt2ZllCajRNYUdsV0hzbXllNERqenpKQk1BRnVNNmFLdDBzVVYzdURpWCtGOWdOU3NDd0w4dTFFWVl0S1BkOTNPaDBhcjcwYnVTbTBBZ3JxSDlsUmpWaXl6aVc2L3pjR3pERXQzT214azZpdnR0aG5DdUkxaExvaVVTRy92bTBvVDlGcUxWMXFaTFFUM09FeGwwd2VvdENtYVA3VUhIUTIrRnQvdnc4TU5kZENWalhVelZiVm96WmFXdGEzbUFBTW1BZGR5TzQ0VURLWEJBQklPQWNEalBVVm5uc05BMWNTSnBNWW5uQitlelFLdjdSdGt5MGtqM1dVQ3gzTmZPcmM5emFEVUhlMWNSOFhEZmFCZW9Ga25JWGVFZzY5eHNmNlBJWU56OXh4QlVmSUNldjhhNjJRTFJESVhQdmN5UzdWSHlOMWE0M0RPU0lSdTQ5OUpJbk1wdmFQM0kyYlNwT1ZTNEhkNDlYSENMTnhqVnh2WTV2NU0xMCtkcVM5aENidUdEVnpsT3ZWUXdYbHdubG0zR2I1K28zS0xxN3JSdVo3enVVblY0RXZxMkJDQ09lOFhiVXk3eXJIY0doVythQzduZ0llMThsQ1JuRHVydXBIVnE0V245bzRkendiV09KUnBGUzdxMDE1UDZzRlBVMTdEYzh3WmEwczdZQjJ5UUp1K1o4TFpHUkFpYTl0TS92bHBvTkRQYWJsQXFNMWNkN3pIUHJJaUdHSXFYckRQcjduQ0QvVXE5Y0ZoTFNLK0NXMUd0YTZNK3dPbUp4Vks2NHBBTXlnZ2RuU2hUbUhrTjV3TlIvT3V1andUSXdzUE9mT2VhNDlWRXZacFNQSTBsc3lSdmNRanc5S2VKcVQzR3R4eFBPN3E0YTRjREczTE1QS1NObEVicytVbVd6ZVFaeG5ldEQzemk5K1BtNTI3dlc2QXAxMkFUdWJQSzJvZ0gyalpuaGhzT0d1VW44UkxUVG51MUw0S2YrQlYvODlmNFA4Q2Y4bU5mOW1hYXpMRWZsY0tFczJsMUxVU1lHL1dwS250TXlEbHZJZHBqaklaZTVoejFVekhDS2RWckYxcGVpQUd6aGJWM0hpRFlMeEY3NllqMTE5cWh1T2tNS0hWUldBbE40Ly9yYW9kZmVtQnpnRHJXdE5DYXR4ZmJuVndLRHJQL0tZeTkzcEZoU2FYWmQ1SlVESEFwWThjR1hwSWJ6Z08wVnVNcVRRUmR3KzJxbHF2Vlk3eXZ1NEE1elF4UTNuRVM3TmdRM1laRXdxNURlbEpEZnFuVGlMb0NQR0dPOFJxM2JxMUhYOHkvVkdpRGpwOFo1bDJja29iaW9Fem43OGh1L3ViU0JQekRyajhGSlVaNjNYakRsSEkwVHBuQklDUEVYeXppMGZZRFJvUDRSVGp5MTk0ZUNjc1R6bWc0VTBZNG5oSTZkb1Q3cjQwUmV0SFBTSEF2TTFod1VXMlhvaFNvbk54QndlRzVWTXJ4Y0Fqa1hyUUNLM2p6M3VtdDFRSy9XZ0tNcmVTZjVPbGF4QU9kK0hGNTJZdWJBL0xkU3ZwNmhLbFlhVzUyY2VmMXovZC9zbk10bGNEQUpiU2tHWEZXZkQwaVBWNE1tS0tEbEc5cEtDUXdmSUNOZG1FM0VQK1FBMjMvMG9LRWxHQnRtYU1VanJ2dEFEQm5oVXptbUV1U011dHVVaEkwS2lVT3pBMnBmcmdkb2g1ZHc4NXNlQXM0Zzk5T0JaVy9VdGtQVVIyNUcxTFpyQmtxMzJxdnQxWlEwZVFNdjF6YWtBTmRrQStTRGtIQmVveWJWdVdDbDFwTDZNeWw2Y1dBSE9uNzNIbTk1QWMxVUMyMjlTOWJ3bDZteGgyWGNaYldLMHc4NmJVaFNaSCtJajRQY0JvTUJldE9kWUVFZnhiVktoN3M3bS9vWmZ5SGdNbXpkbURCa3F6cldsZXFBZ1Bhb21hbU13T0hnSkRTRHlZRjlOSWhEblRDVWhlK2E0bEFua2NhQlQxMFJWT0poZGsvdy84QTVtOGkwWjY2Q3VOWkNjbUlLUEpOUUdVTUREUkgycFM0QVk2dkdzM25KUXp1MTVrU3Y1UkVxeGRuNWpiM1NubFk0aUdlWXpIbnVjVjhmYVRVYVg4Z0RyV001VUI1YmZBSHEwTjNZNndrMENoeFRQN1RhdjNPY2JsemFYTjhDL3RCT1ZRMDl3eGFKUGZqcWEyMVFxQmtsM3ZEejd5YVdiUWJYU0szY29uNEdjTUlmRkJacDlFQS9RYXROVFFqREgrRSswV0JIRmQ2ZFRUY2o3elBQQ2NKWkJWbGE0WHA0K0U2ZndINmQxY3NFM0NoUEdEZWtyc2gwUzVxb1d5Y2xMbGVqWXQxV1Byd21uYUJJUDFaT2wrL2Y5ZEZXbEtpZHJqUDJWS0RzOUUvUWw1SkpIWFZnNXd5amJLd0k0amFjNE55a25LZmdkZlA4SndpU29MZFNWa29SUlZvMnlxZmJFL05BL0ZJSDRVMnA3dGRHMVcyRGxkRGMwU1lZS0JWTng0bnpjbFJaV09GY0cvZGQ4Ymw0WWxka0k2ZTNaZy9ISVR2VGc0alRRNzZBS0xSYVdKVTlJOVozMFZEM25BSW9RQy90RExEMGxtcFpTS2ZsNXZzbTdreGsrL1lLR1lFR2FRUURrdVpCSUI1NGVBNVBWSUR0bVYyU20rTnZ2TVNVTnJaUGhUQmlibnF0N29TcWR3aG9TbTZEWVBMSVNyK2RBOTAya0RnUjJPM2ZDdkowa0ZxVWM0eUhkSTN0RVJvcFpkY3RYMUozVWhlZE14a00vd042TzRueUp5eUNDYzVwbU5MSGR3QUg0Q2lZVkovSDZXbnhVTkUvdVpoVDlzaW0xWEVKdWVlcllkWW5iTjhOaTlmN01udlRFbFJHQVBRUDQ1UDNjUG8vV3k4eC9aM2luOFNHWGo4NXlyK0pHb0dHWkFIQTQ0YlgrdzMxN21rbEhwMUdVSzk2ZlMrbEF2TDhjUEhrdVZONVFhNnpKRzB0RHJHUVdQdjIzV1FlaVZhQmFYSGJBNTJ1aWIrdlpNcjRKZUtweHVBV2dGeUwvZnh0WFhtYjg4MWdJTS9RMDdZTUJXQUtpVEthalZ2YUZmYlo0Ym5UelNsTXplZDZRSEJLWGNuZ2h2dUlXZXhOcWZkUEFlY3pMYno0bGlOT2ZkRHNrM0RJeDJVRE45aGZyTDB3TU5UaFAzV3c4bFRycXFVSVIvZDh6UHFTUXhkQnZRYk1neHlXNmdnM00waVNXYWdScnQzbmRxRUt3eGM0L1BIdi9WWU9ITUtwanAvY2RBTEtqdXlFMGVpK2hGbk9raDhkZGtWSEdtNTBod0Y3QWl6YS80V1hBekhJL3hPRi85S2pDY0poVTdpS3JLZHhiNXZqZHJzWlJVNm1SMytPaEpTVUV4bW5pSkRPQjRCbkJ5czlxTUNkNUdHdDhpb1NwOGJBUVRuMVZCZ1M0M2t3WDc2RmVoRWx1a3ZNWlRTNDZpRUlUTkl0OFl2Zyt2YmlZMDgrZENYamhtUDltbks4Mks5MlVLS3RhUis2OFBZNlJxQTRsb0FUZStmVlVlZkc5M1V1L2g2bTFKNHBvY2xGN3NOWHI4VWVjb1JyZUNzQ2o0U0w3a0pvWG1NUGZFZWprNlppeFBWMG43c0d5Z2IzZTFoU3A5L0NMdHlPU2l0T0d0WkVCTENrMXJPUVFGK0VvRmNma05hbnYrajQ1SDd2c0hoRkZZM21EMW1iM0xibjA5ZFpFWjNLeUJvWWVwOG5MQTBWTTl3VlB6akpHdWM4MkhuNmVxeG5MMmRJUmxoZkx0RENWN3ZEeHlRanZvVmFXWUxXOU42dHdXYW45ditDKzRPYjk3Ni80cWFYTkVhVUFmb2YxcXh4MEV2NVVjOTd5SWpFZlk3YXA1WVB6bUU2SXZaSkhES3JNQWNHNS9QWHJiRHJFNHQwUXBuLzhpSDZWSWp3bkY2aVFDRkQ2dGg1djZkVEFWZmFLZGErTnpEWWU4L0RUQzc4NWtoMUZsN1JqNmViU3VFMkJCZXFlWFNIdlNmSms5bUxlNlpya1hRcExsK0N3ckRRRUJLQjZ3MmlWdjljTFJTSjlnT0RnMlV6cWRWdWhMOUhReWlXSVZwVDBLYnBYbmJtemlPdjlEOS9ldjhGM3NkUDE2UmlTbWxFaklzWWJ5VkxvNXpoNDVtU0h4MytWcHRrSTRwMjZtY3FtdTF2azZKUTJQUlBqQXlnT2xmM2lhN0FUWXdLb1BGQnFYd2wxWVBKaDVnakdTMXRIbklKNUk1Y1BoRTQwanJSQWloeFdQblNzb21DNjJ3OEtxYXJNUE9VL2xFWU9GY1F2dFZSRWliREdzd2JuY3o4Q0sySld3OGhjWFdoZWxRKzF6OGl2cWFGVVZBQkxlKzBlWjl2VTBaNVBSbm45OWRzSlVUbUEzRHVadEZuMTBibllDWHQvZ2xSTy94MGJJVjA2ejVQTmhxZ2FhcE5YbzFGei9HWUR3V3ZLUTdtcTg0bitMd2ZMZUlTMWJtelF2MFpSSDNlT2JVWVkrRjdrRXp0MUk0MnZvaEtLb1k0WHFzekdYTWZZSzF4TWhTYWs5S0J5VXlPckdBdlVXVUJmb1pUTzJ0RVlsb0RJZCtaUnlYSlNyWjJ0R0Y5RVdPSDFQdnRLcVhrQmJkYWdOR0VyN0FlMUs3aloza01BcklqUFRnS0FqdnBRdWp2UEkxbEV2ZStJa2phd0FwU3ZNc0ZJY0tNOTg2NERyTFBVdUg1SUNOZkFPSXpFb3k4dHRvWnRJUHlnVkhnMXN3OXBXeElINFoyL0h5eVd3WVN5ZG0wNW8zVDBNcE54OFRvQ3IzZ0lTU0htenJsNW54Y1RZSk1WSDQxbTBDS1V5bTFsVjdudi9mZ1J3K3ZZTGRuQVBJWDhkZ2JFcmFEa21peWNTWUFCL2Z2UGZlWUY4YnkxWXVKNFlmOTdEU1AvZ3hwdGtVdENHTUhRbmFLOFZhOXk4c1d5aTZQTnYxbm5IMHNqOEtsY0FXMlpNZWlBSXFtK2orUGc3dFEvTWVzNUxaZEhMM3drYUdMQldLbVk2dzZ6NklQMzZUUFppTHRGd09MSnFaRUlpZVpGYWkvdkdKM3VFVFdqOGZ4MUl4Q2g3RHRrWmRaYmZHNDZDOENjQzRkbTR5VjBqd0JZd2xBU3dlVVhNd0RlNEtmRTZ4R2xoSitYSGI0ajlzQ29PclRHNzRNRGFxK1lEMSt0QTdqblFVNWQzc293MHpsd0RiT1Y0NHZkbXpiMStDeXNLeGd4cjVrK1ZUQTVoekM2bG1IQmxXd2xPRmVwSXBwdm5nODkxc0JNR0VHVE55TFFFSnNSZ3M1NkhIUDRVbXczUTJmRW1kVjhld1lVZGRpSDZTZzRpQi9pS00rYUdWL0NkRDQ4VFhERlUvaU1EanAxc08wakVFSTFpVFZwNnJ5Z3lvNUlvSDRibUpoNTFUYzJEdVE3UnptLzErWkwvcjdGR2ZYdlpLZTNvcWd1UHZRQjBCSTk3d2RPT2E4QVEvT1E5b2dCL0FKNC9icEd6OTVMRHlUWW5tM3NybURrbTdpVmRUSnhhKzhDemZSRGVGdWRvOHc0bEMyQnVHdGVOM0lIaHMvYmh4MmtJc21LbGtBRm5KRHV5aTZwbHlkaWl0ME4rVkczN0JXS214aWlmNkhtYmZnODVqdVBZb3JJUUxsRlU2WmdZekMwQ0VZaHNBbGV2S0JPVVRHdW0zYktqTStUNUdXanU0eEh6enkyN3lXd1k0cW40WTRCQkJ5NEQ3bFBlaFdOK1czU29raWhRMXNRcVNjVGZFNjM3elhYeCtHdlVnQlFzN1IrdEUwSHN4dXdBUGg0eXBNZlo0dG5STWtwWWJkeVZYR2trNXZyQ3AxU3ZkRFQwTGtGdHVxcGRQT2NRbnJTQUtxeFpNSHF4NDFqTTZVWWVHS05tNUxRM3J6dkFPbTZ6YTRUdlJRNE5KakhOeHl0WUxldGVobnZObmZIZWFOb0Y2WEkvMVVIYXV6dzBqR3NGdTdEWTdjTmVPVVJlTmJVTHB4cnhLZEpBQ1NBb1FJVkxvMzB1dzNmbHBZOVlWVzIzNGRQL05CTFk3clBDWWswa1RXSjU5ZEYvRWRqYUVMa3NyaFh6T1lxTVdvdnI2d3MzbE9TTndsUmxDYktFUHJwOCtKQU14ZlFsa1Nxa3BmeWVqVWdxRnBsWWMybkZ6eEcxN2NFVzI2MEkwMEw5bGZHU2lwVmsya2lFL0VFZ3d5d3lFUnZlSzBGVW01SWhOVDNDVzkvejZQbVc0WUZ2QkxkRVowMEM3OEJ4K2pjR2pQeWdZRnlBenlYNldKMXRjSWptQUxHa0M4anlmNzBVRU41OXluYklpN0FmOHdLNkREc2JsaHZ5ZjlHeUltUmhZL0tON0w4elg4OGVNclp6eXc1YlhKdTFkcGpuYnNvcWhFd2xIdTVSSEVNZDZsaG10R1BzMFlFNENCVDRaQVJHYWV3Z01KOUw1RS9laFQ1UE1adjZKQnhXWjdUNWRicEdSSG5LTUVYSElaTGRDazh3eWRtUGZ5d0N4V3lPVklzQWxPSi9tRzdRR0l0clZMSzdhQzgwOTBodDdDQ2tyelY2TVNCa1IyMlBkSks5SGt1bGgzOEZPbHZTMVgwSnd3bjR2UlNmU3FKM3VITG96SzZ5b29FNEN1Y2JYdEhXdTJmcDdDaXJtSU5vcmlGblRkenc5OVZXSXYyY2xDQTlnWUFWTVkrakJFdTVCaWhuMkZKR0VXK3dHcGI4MjJZZEdYcFdhcVU5QmVxdG5XUEU3ejR4MWhtbVE2RG9sVHRZUElzaVdPRldYZlpyaFQwSGZGK1VJdTh5elh1NDlYWEFNV3FST3NlOVBEMGV3MzVETVJQU2JzdTUxSjBWOUVrMEpMK3RqWndtY2poTWJQbE1jaExqYTJRdEIxaEliVk44ZWxoWTFPcEY1SFBwZ0xkZk5ITTNEV3laRGtJMEo2U2Q3Q1NtY2JEWUpPRE9IY3FibmJLQWtrTzJaOXVSTGN4cm42UzBlRGF0ZGVaNkJ4SnArRlRxR2ZWMi92K1BrWDhjZUM1R3N3VFZ4eWx3aHpBN1hEOG1iay9nb0FjWEkzT3ZGQTdxUHRwOFBMZHdEbk1mSTdrYmNKQ1hFS0xwYis5RDNoRmNub3IrWnpHRlhzZFp5bklPZUxReE1aQnc4T1JHc1BDV1ByNTllOHg2UzRGa1AxS0l3MVp1akozajJ3WEwvQnZrVm43L2Q1bGhBc3VBY1lSZm5zTnMySlV3MVBrSTVMWit4S3JNSlEzRXJzZnluQW5NMllMV0RyUEkyOFFMRTJFb0IxMW5tSWlwQzNEb1Q3eE95cU1VYklja1d5dXVQeWxKZGVGVHRCRWtWYml1d1JVQkJXL043RlNHbGUxMG1zUXUzZXI3ejRGd09SeDNQdFVDL2FKOVAxNHk2d0NIcGVTVDhtNmJVbWxvaXdaNGpLV1lIRW9HUzJSYWRZSXdnQVgxUlNLWWtUSGpiNC9PTmxpR25EWDlIWEFUR3NKcHp5YUFyM25vNmR4RVVMQlB2NjFsY21QeWprSno5WUFsaXFBMEh4NHprSTd3TDB0Tkp0RDlnTmppMGk1azdWajlIUUc0NmdrSi9HT0J4WDVoR2hNcE5EQjBsbUZ3VEpCeFpoQnJWQ2FWUTcyNlo3bDhPbEJ3QlBjd3RHL3BEelZoVXcvcWswaStRREZmVXZ2WnE5YXNETG9lVXBJbEx3OFMydFFWa1FZZWc3ZGpOaFhwVldqQVRyS3ZmOUZTeWtIMnpqTmYwS0d0anQ2RWNpSXQ5djJPYWVVa0xHUkVZZjJRY2dRZkZ0Q2YyNDlrTDMzM0Q0elh1TklPUmVtb0tTSkRxbmFIb0ovVm8rUC96MVdrUnlGRVZtTHhPVU5WTHA1U0ZZdlpoSXRzUGNOVGpyeVFnTnBjaXM4U0hSV3J6c3lOaUhqWU82WGtSemlyb0JDVi9UMVk2a3VWRXBUYkpqM2VUZFVFOHUxcytqQ0lLcENneEoydStRYWIzVk4yT0xjblpmRy9XSURZRGRWRi9LU0N6MlgvRkd2dG1PQTdPTUR2SEphWkh5L1pwSlUvajhsUnF3bHc0TVlPcDRBamNINUJnbElxL3RLRU1WSk1nWldsc21BQVl5RDBnS044dVN5RVVMUW5BWjA2dDkrZmNGME5jRHBaamVqWDV3djZReW96SDgzTnVTYmxNRmJjYmtYTUNuc2l4VUc3ZmJzaFdqakpvQlk0UlliRy81NllrUHltYVhQeU8rMUdJcXpIdnlFVm1BQ1FjY0xFNVJIendBb21GRkNJVWpuM292MFZFZ2x5T2xzWGEzQXJJY29Rbk40UWxYSXRxd1AwVnN4TTZ4Z3ZHR29IWDVlazNMTFVSZWExMUZZTlhyeUROSUNuY3BMTGhTc2E3ek41RWNWanNpdzBpc1d1Wk1BVU5aUmYvbGJJbUxmNEpnTE1yZmF4UWdaSlREMVh1bnBRVFM5U3VpN0crS2p4bmdIMlhxWmRRM0pjcVhzaDhQZzFIM2ZqdDVhTVk2bVV3eXQwN29xM0tscE5zbW1ZMFZZMWZiZHpaMVVCRytleGxLZWx2VytaejNuRHd6cXBTUkswQWJHQ3g3RE82ek5TMUlOVllHVEJoSklScXora0N4T0F3b3R3bXJBMXJIVDY0aVMwamtseURYRkxCQUpLeG5XYVFnMUp3Uk9jT2Q1eEYwdmhWQ0ZqVjEyVXRua04wNUZ4R3ZZUUJPRHZ2eGZ4cGxuN05VUXlQUVc2V2xITWxNZzlXUmIxMnB0TkN1YUVEZGZWTTV1NC9kdlJiaXhTaUk1eUJyS1lPUXg2OVVZNkU5d0NkK3pnQ3JuYi9zQzF3dkppVUp4cExRc1JYbkNJa0xZOTZNNlhIQXEvQldWdEo5MUFVL202Mit4QUNVSkZkdFV6VVlaSVJzSVBld3c3M2ZPbmRIellaVVNIcW1KeFhJUSs2YUpWSG9mT0RjTnlZdjVVaVMvdU5LK2lJWHVtcUkyQTNncUtxMm5IRjd2QlBvcEVOM052bmYvTkhEQTBjREZ0U2RTYmdxSlBZQVJKYXlsZ1dqQlBCNlA2NzlKVWtpVjNYU3E3UEZWNXdtWmJwR2hFWkRJcDgwaW5hT0t1am5XN1A5MHlpUU5xYlByTTdDNVN3Ums1eXp6SG9MWDlWSEVDUExMKytCVG0zYTdKdlVlM1BqcEpxNVdMOUtDcG9BRWZ1OWhiaE9wdlFnRk0rdWJvMkczd0p6aExPTlhjSkl2TzBoTEYwYmRxQ2c5ZUZqWTd3YWdGY05TdlN1WnZKaUEwU0I0dEd2bHZrNkt5RDZTTnpLT1c5Z0JaM1pGZ3E1V2liL2tXamtPSGV1K0lhVjVsSWxBeTZidmdrdi9lOE5LWXJEMnFESlFBbXlkbkUzUzlVZno1VDRiMXVJNWsrWjRwM05mc3ZuSElVZnp3U05TOGhoSVdVRFpqdXQ3VVllcWxiRXhTVWhPSTZSR08reWZWVFFUZjNiRWQ0d0poQ0czckJNM3FsSzQvUWU4ZzByV1VhdHRDa0xXT1pTWnpNelhGclJ1eGJGMEVpSWJCdWdORTBzS0pVa3RqVWtQbUk3bUdLUVUzam1yUytabWFkQUdORkxZMStVcnVONjVnV0E1RFV4TEluckxPRHMzS09uM0gwMjk5eTQ1MGhvY05kUG13ZWF2RXRCZmRKa3hXNlhxSlkzWi9oWGdQZE5sRmpwd2tDR2VyNXk1L3hnVEIwR1FTalptdUJpTWhIa3VrTm14UldoM3gxMlVHZ1F0UllQeVRuZ1BQZVErZ3djNWRjd3hsZy8xc05UdGp6eDV1ZUN1SmlJTFErZW8zMFc2cnFyaGlYR0psWGhRV3p5dHZPZ3NEa0xaY1BERE9uOWtTTk9KWVJOZUhPdHl0TmV2MG5ZRmV0NGpIc0dRN24zL0N4b2cwNEFqNFd3MDAzVnNKNk9xTmY2dkEwTmtNVVRrMkQwTzIrOHAwMkYrNGlndDVOK3dQaFQ4VXdCU1RiYVpmOFNjMEZxekYwOVQ1YkFUY3o4c0FUcTNuVmlyT0R4SzJHUDZWUjYwMlNkVkNncWRYRkl6R2lHUUEvdlFHZlZFbFE0cVd4S0M1RTQvOFlBQ2pkZ0pxZW85WE9uVjJqOGdXZ3Z6WnA5akxkQWIrMDlsUkhsMjd1WW1CNzZoWE4xNXRVUElHd0VuY1FrY0xydWFUeGFnb3o2RkM2cU0rZko1UnlGN3VuQy9Ic0xrQmYwRXRZZ1EzNHhLSVpoM0hQSUtTTzRzRTZJcXp5MGtXa0IyUmJpNEczVElpUGg1NS8wbEFpNG91K3ByMkFYaWt0TkNSOHErS1hFWlhrZnZVZFU3K2ZVb0pvcmg0UGFMV09WZHJTS3ovWGdUZ21XbWs3NzNJMERNUWdMYTZ5dlVHQkM2WDRGL0dvSXZ3S3hxbndFZEF6QnVmYjZQUHBjY0o4bExGV2I0NmMxRWpOWXpJakUwTTVxM0RQRjYweXQ2VmUwS3V6YkFtYldiWGdlZDluNUZRZ3laNXNicDc0dngxamVBVTJlbmJCOGRQSG1FSVdPNDM0T0VjVnE3Q2FaT3E5OCtBanM4ZVhUODF6UzRaTFJ0MWM4N0J2UHNJb1Zvd3h3OVhidDdnVElEbFNzRkMySEZ1OHFoQVY3SUpqMG9nZy9tQStDejRJdnFXdmVsNnRCTi9pWXVwcmo2bzViUGFQSStEUHlUdWJnTEVoYzV4d3dJZTVCL1AxV0N1UWptVlR1Vm9NWTU2RzIxRjBYVU53UkVjKy9MUlcrVHliZHBOVTA2aE50WDBVRFU3bDJ1Y2JYVjl1eEM2d29ZSmVPNnpqc0NoN21WN09teVB5TlJjVGRtK21DOU8zRE5rT04reFhuUllrZFVRVXFMZ1FpNEpSMUFMMXE4RVdsWUYzY3gzRisrOTIyQUtsb1lCOTlOQUhvQ21zVXNxanFOVVBGb2pkZk4zL2VFZjdURWJsNjgzYTB0NDZqVXhIYi9LMFhVamFzSXNQcEJKenY0bDExbmxDay9xUVBxM0lVa2RFcmxlSHZsc2lrc21pZDgyWGtZUzA2WWFGL1d0MFJBT0FLdkFwZmxnN2UzOW54REgvTFdzSWx2VGkwWWtEbGVXUnlVSVFHeHdUUWNsc0Fvc2h6M05UbTBGM0tLeWkvOFFRYjFSOEkxd1VPanA4d29ONnFlOTlNNis0cFVKRWpCZ0huUVR2Q09wQmgwNXlwbEIyckw5TXJEdnNOa2dhZHJmY0ZubWVqbkt2YllBdGZoU3Azb2JtWm40cjBKOUxLbzJ0L0FFZFVORHpmSVZkMXY2SURjeERsdFI3bnlJeCtQSk1sUWpJNGh3aGdFalI2SFNWYnM0MlFXbU91RllkWDFsbkhySHVValllbWV6ZzhQOGp0eHhid0RaOVc4Wnd6cXBiZHVaUktIT3dPQzNZYUNLVVlXOEhscDdCNXVTTVEzM3F2eDY1T3FHMVRHdVRYQ21USVRPcmFYSHVRVWFEMHBBRnFZTENzOVZrOWsvQ1M5elhyT1pLZVFmTVB2Wm1CNGFXWEdjTTZ4bUxLckloNmVSYXpBOVkrdGZSV2tvd2VCMldZSHIvc3k1OSsrd29jVkoxNEczUURiUzNWM2VsaysvY2xGQ090aURaMGJiQ1psd0t0aWYxcWJ4dnRUTnRBNEhvY1VuOWcrRllzUHlkcDk4czRUL1VBL1hsajBtOXNlcTJuSzJpbWdGeE80cnNIWHNQaStzUloyeXRiTGlnWVZrMEZmcnovTnBHenRjSDIydTM4U1BMdFE2WkViZ1JKeFFRMGxVbHF4czFOTjk4ODRxV2liemNZcnBTV2FFeDRmNXljRkQ4N09wUWc1RWtQL0VnZTA0REk3SFgyYzRud0R4WThNUENyWFpEZHdNRXZLS0RDQTNXVnVnSWYxN2l3Ym1VRFJFak9IekEvRUhFayt3YVBSZ08xT1kreDNqaTg5Z0lwMUlLR0xoMlhYYU9YMWg2dWlWOUlGT1NCQXJYc3VVbjJYcmEvcGlQOUtmRHBEeFhmcFlTTGhkcHhnQUZTWlQyOGc0Rm82U0Z1TTNlT0c0Sm9CTTZWZUVyRnFBWjRldXRoWjVUQnZ0MC9sSlRRSUY5RFRDN1lTNjVQcnhpRXR1UHVXTUhHcXlPb2ozK1M1YnIwVzZKcFd5bE1mNFNsdUVlZkl2MUM0VE9DdmJtZGVqay8rZStmUTY4dC9MM2pBdzZQSFVBaXhGQ2JhY2lPZjBUQzhtY2xmSEdtUEhQOWNLeHV1ckh4Yyt5QmlETUl6R2JiYzhnUzFBQnNtMmlkRml0S1grRFc1YmIwWE5YL29BT3VIM2hBeUc5OWsvRkVaME53WWpzYytJenV3SjJIZkNsRitUNEUyWXVaYXkyNkVsc1J2b1BMeXp1dFdmYnUzOWJpQWJXV0dvSmxWSUNmeW1NQWxUNWdaQkNoeGM4dWpsZEVIUGZBQ2xVQzNRU3NUMm02SEhGYnI2VUsvbk5wM2VsZHFIL1JOUW5mMk9IQkJZU01HNXdyYjR4RVdKb2hpcC93TVpNWXd0a1lIdHo1VDdRZzJESTVpN1gwQ2h0elBvZDdycURNV3J6OWhRdkZvZExQT1FSR28ydjc3VUgrcXNiWmJOQ2w1RUdZS2xON2p6NWZxd2wxOU05ck9kNU9jU2hZSHNZNzVCaWVoZENzWC9pZTJiR0EvRG9PcUx2MndrS21TRzh1NXhMUDJPOVEybkdDZmxVbkJrdWNPWjltWFk0ekJjcEpOU3pJTWlsUU9yMmlrWnpHQ0RxS1pDNmRwVklMSDJKd1ZSWTdjZnhTekkvZmpxSzNubmNwd3JydFZsb3V0QXVMVnZCMWJJK3JyRFRZZDExVVNISjE1cXdSOVJVa2Y5RVVIbURjemQwK1ZFVjYvc29ZbVhyMENBalZibEd4VlZjVzV3OVl0a24xRTFOYXFQWkc2ZUxRcVFJNTdOY1FNdzh0Q0krQUUvZWRVUHFmRmxHZ0hSK3libEdPS0dubUtBdGhza2hKWlRvaEQrZG9KSTdFcU5TRFo4RVk3M2YyMFd1eGVzUExvVjJOdzFqWDBRcGlDZTdBM0xIR1NxS0E1MXdMdzhuSHU4M2ZqTkNUWUhXM0hsZnVkdUNmYjVXKzdtVFdScmN5aEs2b3V5UnBxTUVyUFJDclJIZ3F4VEpWWEV5Q0xmc01zaGY5Ynp0eVJxenJzcG5CemJtcHZoTE1qNll5WDQ1b2pVQ2hkUTRlblVNbFpxVmNJdWpZQWpVQ2h0NTdHNDRJUTBGUTBoOVB5Z0pmb1IreWhSQVNxTnY2VUZuUlExSmYvTjBSNWFyRlZNQ2c3THE4WXN3Nk9uZ1lYYW5Sd0VJelpkQTJ3UEZPcVJGL3NXSjVVaTAyRnNLMzFxbTlhRElXWDFKc3BsNEJpMjFudjdhTzd0NlVqUlBqWEdaYldRS1BVYmZlVlppdTdLcVo1YmNXUEh0OUNRVlZtQmZlWjc3VHgrc2Jvalo4OWxVdld2RlNYZ085ZXZURDl4UGVrL1piTHdrb3Q3WTZUQ3JYUXpKV3F0aElaUi92MklrUWRERjFUQ041RmxWMFlNZS8vY0g0a0dLejlyM2tTRzR1VE83Q2RSTGRFMTBQNTh1UTVBWHk1cmlIbmU3aDF6WDVDUksyRjlSNEt2b3FHaE03UWErMDNsbHFFQXBadytxN3A1L0NMNHVIS0cwUEhrZGxLMit0VzRSelBqSXBXVnI4ZWhQY08reWNnU2ZDMWx1bEhuTDlac25nVlMzcE1JaXFFbE4vN25qeklrdGtZTll1dzF2NTdPTVdTRmFGR29oVEcvU09DTEcwUEFTTGR6WUZGa05WeVhTa21iQkNTbURlZ1FCSXZLZUpWSG50TUgzTU94VDR4VEJWWk9BNzVzdkJrTHlxTnJuUmdnYjNHVGdMVGNDYlZYRDRxUWxib2FmWkNBamUxR3pRMzlrb3MxVitwQWV6Nk9maHVseFVteWlweG03VTNZc2NEbXB3MjdES1V1TG1HU05Kc2lLT01qWEpvRE5ESVhkVVVPQnBlNTczbUxxV1VpN0QxMW5ZQmx2SVlidU1KZFB5UXZTMFFzeGhtK2JYd1NsdVFTajNXL2dDdzRhMTRkMVBvQktZWVgvY0E3NkRoM3FUa3Z2aHZyemRIc1luUmYxby91RU5NQmhZQWkrb09KeTkzYklrbkZURGtoOHRKMWVvZENJSTBkemxmMS9JTUNYZDVLRkdjSTUrYW9DZHFTK1lhQlRxcU9IK2o0aUhoZE1xcFBDeC85UThobHkxWnNZWm13VU8vek8yRzM2MTRRK09IM1V2KzZ2TVZ6SzFuSnRUZVJSNWhNeFFZTkgxYjF2eURmR3E1T3Z1N0E0SmlXZXBFUW1kNElWaHhQcGpxRzVsT1VyS2Iya0EraFVIR1g0MGpFbVQrbXc3SmdOOUVyc3BRdmxyTGpJT0crVUpVdDVsclBlTk9tWjFNajlmcXh4SHpDQmx1N3FqNnpPTFNCaEQvUFFXd2NzejUvSk1VNGdsS2M3YnFWU21QMVZlMnVRaVNXeHJOWVcvTXJsVlhkOERROExYMDMzcmFlanhIUW5zOFJoNDZ4eUx6aGlMS2trekZrOGV1ZjZ1UE9jYWdHQTZrMnJrdExPNCtGdUkzOHl6dW85TUM4djh6TE50b1ZXKzZCbWFXYUpSZlVIOHVYSWZ0citMQ1JDSzZBN2oyQ1VLSDZqR0dyZzFvaEh6bC9aUWwzREErT1UvMTJpNWtIdUdrN1hkVWRVejBGVU5vcTF6aTRRMGUyU3p4K0RsRmxVL1ZmdDVaUjArYkVYTldjM0g0VHBiUGQrdFM1K3ZFcFpqMExNRUYvM3YyVkFTQmhTdG9walltUEhaMzllaU40UjZCM1p3WlBDVDdYa3krQzlPMVRnck10T01aK0V5K0lnZEkwOEVaT1UrbDR5Z2E5V3dUVmxvMzJiZHU3OWdQeHFyMG54RnNnUTlwSkdRS3BjMWVIVCt1dFU2b2VJZG5zYjF1dWVSVVc0WFI3blczZ2tyc09rcGVhbDQrQy9tbVJBaTdUTkJNbTQwZWppeCt1S3JtTE5sTXRET3hQWnZocVJSYmY3cnVKM1c2RzdmT1lEbFE0eXlJQ1NaKzBxZzl0aTZPdFJvNmFBdWJDS3UwcHQ4a3J2b3pBUUR0bHJvSXpLaEEva245RzhJdmh5U2RsNldGTGhtVzVYOURXNlVLbk43S1BPZExkaXFUUlFiTGJqTWlrZFJQR25tNzYwR1V5VEliMjVjcU5qU0lqZTBIOWIzNTVkQ1ZrNzNQK0xtNFRrYVF0dU02M2xkUkRHNjQ1ZDJSWjVBYTRTN2MwRFlWQ1I4V0N6a2dtNFhjaEtvN1BZUlhLUis4dHNqVVp3SlpSbzNFUmNGVy9sQndzOEt6RWcwdlFpSjk2aTBLbDMwL3dwcnhIbnV6cDc4MEora2VneGhPQmN3enpJeU1IVG9EalFvS1BlQ0V4czdEOWRmdVI1bjBHdXU1VU55Z290dDl3TnJTWUkyZ3pIbGN6U0xNRnhtKy8yd3JxdFkrWFYrSWNXRHZ3WDQvazB3ZjJTckt2ejBRVC9NSE12aG5OS2xQQ20wdFpsWXV4QVd5dzBxdnNkMHRPdXBDMUtiUUF5MTdmbjBFaW1PYlU5QmZTSUZlbldIQjJyVlNuT0F6OVZzc3FKeTJsWkdvYk9EenhyeXh6NGhSTHJ5akRuOEZSSTNjQWxUM2cxVktwK3dPZGRqMEcvdENpNkxiT1A2ZlBvRDVKNXpRNjZBUHF1aHdudHJTc2NqdFpjd21LSEJENGR3VTZTUURKQXBOMmNUdCt1aDRDVzAvUEdLRDdSdFhhZWU2MEh1WlozYnRRK1YwYlBEVjl1R2gyTXliNFp5WXhIZ2ZLbFZNMnZYZ0VkQzBiZ3QzQ2pTdTR4RDdZbnEyZFI1V25HbXpGZXVsakdwWVNsK2RoazdjTzhjTGM5azNjbWxsanpkZ1M3YU41TWtqUkRDUnlGampaenFlTk5lbDY3Z1Fybk9jc29BdkZrQ2ZRZVpTRW5wZ0h3QXNNeEFxV1IvdDhVdHJ2MW5WZlo3eDEyS2l0MU5VaTM2VXJycGg5WW5YVVk0c0x2RVZCNUtQZ2JDN0Vwb1FERVlrTVV5T1BKQXJLeGthUERDeG1ONUEwZGtXbzBuVU0zbTBGRU10aEJEQmMrcHJ0TWN4UDd3eFVURXFDRktCWmozcmFkcnZOWEZNR3l6bllWZE5qb0NUamhPMlEybmxRbmJvbTFmYnp0dUt0eWJEblBPcGswRFZPTjNBTmFXVHZraUgyMzEyTUlFREQzRm8xSDdkOFRHSXhFMlZGQkNsUWpCZDFkOUROcDZFYS9nOExqL0pJZ3ZUOXJSYlh2TzZsOVByOHNaeXZ5K1N0QjB4bWFvNnBaQjdXcE5XVTYzNXk2RWxwVGZLNDF2ellWbFZlWVlFYVVpU0graFlOMjd3WDVrUWpTaWttODhBM2FsQzFFYjMyTFd4SHJRbnk4WmVXdmQzeFZzcVIvU2d4YUR3QXlJZjR0RWthMVdKdWZBR0lYcVV5VnozZXo1UnlLdER2RFpoVmhNSXFWK2xRSUpwZWdmSTc2SkI3VkZLbWYwL2xyUTdzUnhRUVN5Tk8yOGtRc0tiRUhpeTR1TXArdjlvQ0o5ckJDbU80UDRsRzhidzNEdnF1Z0oxUHZHbHgxT2NTQnlBWEEzT29MT2N5N2paNGFidXlya2p0ZEVDUjBaRllrUFpTY1p5RjVlQmgrTUQrSUZIQnlxSktZZmxENnhJdW5RYUVGKzFMS3hBUFRHZDRic3hGdDhMdGZlbDhKYlZEdEd3cnJsdnlVTE1xN1dyTkV1WlE4MUlEcnQ0bXNqZkx4TEZXYjBxeXZLTThra1JZYjlMWjBBUUpLd2pWRk5hN0ttM1E1V3lHMnpka2tnRVY5MWduNmlpRXJvZVpsaHJCZ0d4WVIwdWNrZWVnd25MUUxRdGQ5SHBhRFIzYUZCMEhmZWo3Yy9HaFp2dmxzbkcrUHJDR01pdm1xL0oxOUJNT2g0SXlnQkJ4MzRXNEZleUJhL1dGMWNBcjUySHk1aEE3aFQxd0F5VjRvbGVSUGpRcENjaGM2TGJFY21UQWordjR3Q2d4bGlaYkJVMkpvQnNwc0x0R2p1VFZaa2FuZlgrRFhSQ1Z2SHlOcmJZM0FrellOajNWbzhvSDBWM3l1RzA5RW5ibjcxeTVtNzdlK04xaFhla3ZUM2dqNzNhQ01zMWErTGNYdVd4d0xMSkZrK3FGOHpvdTV2WFJJQ1dwS0FISVpvUHBsMlBvMFJyMmEvYWhVQk5EeHk0NjdValR2bElnejdNdUlsOXZYNDJpdWFCS3FneVdVenBlN3FicHlFM2lvQkpSUHcvYWl5STBWbXJWM0d4enZDQVVXUWtaTGRqT05JUmZEK2NLbDBCdk90dVFjdFRhcnYwOUlzbDF2MU5RRzgvRWErbm9WRmx3OXBwOStYTUNoWmE5QUM3ejQ2SWJIR3d1NjZUdVM3eXptaFJvZTQvMVY4RFJ6M0VycjdZN2FqbnpEdUt1WVFoRll3aWNyZi9HNHBFY09TK3JjZUR4bDhaRzhkZkZwT0dvL1lhQjIxUTFmSDdoM0JtbHpjaXNOdUlxQUFVNXJwdldVdW9ZcGM5cGVMa1hMSUhUZjF0bjFiQy9ic2Z3WW50TnNOc2ovSW9odGVPMktrTHQyNDg3cWUrSmJCZWNRQ2JjSkIxVURGK1FpdE5NUlhyY3M1NEY2UFV5Mys3YllEc1Ewd2krVXJGaHkvS1kxSEdqV2I4bmt5RFZrN2pBRjRxQzVOcGJyVzdvMTdwWERkRkkwNGR3R0FBS2d1NldYdlVhUiswTElOWlVMLzhhWFZ4RjhzeEZOTGZIV2lVRjM2cVlWd1dabmpZanNPYkYxUTF6THB5Y3lQRFg5SDlnczBxSWc1K2dzVmNjdVhocGJLRExNbGFrSHVGMUZKV3Axd3N5djFnYmlnV0lvOHRwNmNMTkduK2dDTzlCWlVHUm1XUGEwaUwwbXhodDdScnRsTUhiU1gzSzJSUHdTSzRHZDFtclFEWEVjMTE2aUFoMjB3ZGFoN0lYRmhPZS9DOUdlLzYydDRVaTRVemJYTGIyckY5K1lEYkx0U2o0OGR5Rlkwck9WaW1IQ3NUTWk3ajhKaHN0ZkpEVndhTDlDU01ndjl4bmZXeGg5ZVdHditKT3I2SXF6TTArZTNEaU1QdkZzSHRQT0pYS3NIYzZ3RmU4QTNYMEsyK3FLYjhYL214YVlBQkloRkwreVJIbnpZV2xrQVI5MHVHVDNpMHZzTi85M1JoMGlmeDZ3T1VMNGNNci9PS0dEL3Z3TUdGUll0NFB6NEh0THkxTkNlbFhkb29hYVRvejdZWkZsdWl4VmdLemZjL2w4TFEyVmJ3RWFwQmd4eWMwVnRjSGE3Z2xpOUcyNTQ2R0FNUXd0R25qYk9DZ3ZVSlN3UWdWc3NvSnVoZjZFSEx6QjhNWmNBa0RKbGZsV2NnUHVPV2YzTFJzQ1BBTk03V0thRlJWczhzRXpBV3FCWWJxWWRjM0xONVBKaGpTOUJYbVpkR3h3RElOYXF4eDJBYWZOWXVNU2xrbllkMVpFa2lwL1cvU3VQM0NYSHFCd1A4bDhyZFJUUWFFTFZ5VVBIUjh3UWxXd3JueVQxaVltREUrT0ZkMW1ZVG53ZGlIbUMxL2xMQTU5MFBkUnhyUjFFbUpNZUhJMVJtZmVPOXpKYkd2WkpDc2R0emVOYmVHV3JBOW9FQ2VlalZuUEdyNWhUMER6WE5kVjh2YkhGaXNYWksxV3k3ZUxxUkxjWkliMlZNVEFnQkFvL2g1S29mMmlaUmVTelJWN0w3TU9Ta1g0ZVp5Y1JWY2llQUdnTWdNZWg4dUdIVjhhb2l5R25IZnlSTkJaUk1uNlhMdjRsUjNNMjVBV21zWEQ5YXVyc29mbFR3aUtLaFdTY0VwaHZUdVBSSlhJdUFvU1J0ZGZ0SmJvcitncmJOaEFoY2VseVpYVExZQm1idDlzWWZERW8yYnMxWkk4c00zQTZhZjZFQzB5UVQ0K3FhM0J3VDhIM0x5Q1dwMXpldEFRMjhxK3JFcFdLZG14VkVtM1h0QWErWUlyazQyREY4ZWZnNEtTSVFZTVlxWVlNdnl0N3V5SUgxWk1lL1hKWVk4SjhGbHdCQUdQMVNROTR5VUxWNE5acndLWFFBYUJsZkVlZWV6TjhzeTBBNmFrbS9rMzJmcDl5T01aT0dPbGxUOUNVR3krcWlPUDZZSnFTMi9SbkhneHljOWhEbXR3LzN1Tmk0dGh1dThsZTFLQ1ZDaU1mY3pDTGxXeHl2c01YVlpscWM0VEt0M3liTFdta1ZxaDErZXBzS0FTL3o2MU1YaVZwem9QRkZ2cWQzdGh2aENpNk82dExNd2ZRYlFvMHdjeWtNcGN2Z2pNaGNrNWMvWVV6QUNkdXdhWUp1NFVKMVhtKzdPVy9Yd01kLzhGb1grOFN5SWNkTjQ3bDJFeGdRbHp4dVBTOVFPOHJuc2lRT1ZlaFlRdFpvTjhxOXBvdTZqenVIREtJekk3cTRISHNhNkFQUUxoalBWRUx3NzZkQjQ4cFdNaVB1bzE0eGVvd0JhSjc2czljZlE3ajh6STJNZzZpRjJmRC9BY0RJdWcrU1k1ZFBTVXBxOFFLYVI3R3E2V3dmVzREUVhoTHN3cnlveFl4eVdkdWU4VFFKZUNsR0JRR051K0d1NFV4SDFwL21LK3FCK2c2VnZkbHdyZGR4bUt2amZVTXUySlBWR0FYMEJJd3dFNXdyTDRBOHdCelVxYU4xaktNRjFOemlEdjA4TGhZc0MreUtGanRDQnhIKzJvVDA3OWMrL3FDZVFWWEtscFZaU0IrZkU4cDFpbGo4TUlWbTEwV3NWQzRlY0ZBQVhoVWRCeTEyREdvY0RuZVB1ZC95bGZsRkxPcXFMSStRaFhzZkhjKzNRN0pOM0pXN3lLVmtCalpwWG5Za3hrQURnVWhaNm9MaS9qNnhNT0VKTWgxaHl2NXlWUHlnb1dOZE9uME4wMGI2K1gyV29Sb1lNT3A1dnVPQzl3Sm1BZTFDekxlQjlpZUk5YS84Y3hzSjhHZy8yVUtZQ1lHbE84RDY4MWpWeW42elhiSCt6MGVCSVhBMmFaWlJHcDE2ZEZEcmJsSjNzWis3ekc0bk45YzFubVBuUUZCL2FMNEE0bTV6Z3VUaDRiZTFPNHc1czMvMENoRHZQVkZmMGdpeUlxUThYQ3U4NmhnOW5DelVYMExZS2pvZXp6YStzVnFXTGVXZWtNWVY5ZDdTT2xRQ3FuL1dqRk9ER09TcXVrZVBzWTMxVW1pa2I4ZTdzbWZRdDE4QWx3OG5oMEh0eUExbWZmbmZIemxkYXFreGFhZVhxNm1yUURPME1zbHVkVHppVFhUdSthUCtVdlVIeWhqQUZrK2xvM3BtVXRWalVLaGN1WHdFQjgzQy9JckpMVDFPYTM2dGdaNmhtWFJFbzVxMy9NT2RzT2VOSlpKZXpCbXJRakFDS1FHVWxxUXRvM3pIVmtxOWpvMlZXUUZybmFWMk55VGRCVytzeHJKSXRzd2RsK3NCZEp2M1loaElhdkJ3a0lDUWpGd25mcnN5bmh0U1prbWJRQTVYaXBWb3V6bVpsS29IV3paU3BkNnkyL0oxcGs4M0EvVlVHcVBTM0N0UXFiT1RzcUhjRWkwc0tCK0pidEdCTXVPMkhpVko4a3JSMnRoUlZZQ1NkamhrbHRoK1BIRzVJODB1aHFUNmZRMXJvUjh3UjF2WkpUT0xsb094VjA0bFhnZTZGMWtVaHd5K2VIWllBOElpRFZCR3g1SDVsYTRJbW12VkFVNnU5RllRZVM4R0JUQk05Mlk1a1cwelRoRTBxNHdsb1NaM1lPTThyYkRJK0Q2dkg4VUFUSVJQd0lIdklTSGYwazB5N2FWVUtiSVJNbU9SOUEyY01mUCtqdk5UeVh2M1JPQ3hEeExJZ2hOdEpDb2pzaUExaW5ZSStXazB1b2thOHIwaXRYWW53QVJ5VmpINEhOVFNXWG8vZE9PWFhKOG9CbHhRY2J3UjFGTnZGbEI4VDgzbzlQOENlR3JyeEx1cHdad1VuS3l5dWJoWFc5Vldoa3NnMHZwUkUrbFdtekhSQ0x5SmUybjJwdTRpYno2a044eU9ITDQvUk9TWjJqZk9ETndWc3JZRk0zbkROYVZHamJVbGhFSVRFYzdBVXMwK3dYUksvWkxubE9COXZrLy9XcjVENEk3a2paQWErWkhHQVlEUS8zODdVZ0Y3SXlXUDN2QkY4eUVJVnB1WEN6Z3QyZmpyejNvVnc1VlVEZWFmMEptNm5DRE5FaElSSmk4MHllNzBtMmZJVTVwY215OU1Ia05tSXpLVjBUNkk5cEdUS3NlV3hTTytzWHJkLzBqcXMzTXd5aU8reUdhd3dhb0xaWEZxQXpRRTV6Z3NtZXdMYXdzRWhYNVZQMFlHT00rUlpjNFIrY3RQeUZxL2RGOTR5VnpKbFJRcldMTFpLTFhKMzVHUjZhN2RuQys1cWZJeitEM0lFMkUzN1hkcFJrV3VqNThNNkNsT2doazd6clhMaWpXTkQ1ZXhEc0l4TjlNVGR1UFN5NEljVm4zQ1g3R2wzNlZpNUhNSTZ0alVRVVkxT0lLNWhGSUZ0ZThGYmdSVDgvd2ZIZjhIVUxrUVRCbFc1UVNsc2NjR1I4Uy9hQmRRQ1VueTJEZDB5TWtzN1gvOCtKWWhscXdoa0NmOHQ5L213WDc5MUlUdHAvUWZ1ZTdIbUFEMmUwZGxKSnQ2SlR3aDVlclFtMEhpN1BCU2piWHJZWXVXZWtHR09EejMxMUZ0WUJYdEM2eFZ5OTVZaU1uRlFXWkVyKy9YY1FiRmZoWnRYSmZ3R1BneVBGVXM0NFcyVEtmSktJRmJVMGdYTDJzWTJIWWJhYU5jMU5RY3VYUE96dnZDb3h5cVNUU2hPazVabXNYWWVvbzRmd05qVWR1UndlVktGcE5tYWVPRE5PNjlzcXlXQ3lsUkYzTVhPdFVYNkh1c2Z1amt5ekRINys3Vy95V0MvMy9LeTRpanE2TDhsdEI1a28rMmcwaVVLSjZMWUVhVlZQZGpQUnREMFlnb2M3NUJwbm8vT3JzYlJyZFJaazA5K0lIQk5mRnZUVDFoTWMyNUU3T09zMlJPTnVscTc1NEJ3Z2Z6WWM5dHk0OE5ETWZJd0ZOSlAvTFN4enp2bzg4ZHBFTHhqZ01KQUpPa1JxRDcrT0FnMGN6QUJmM0l1RUh0YTFvVkMrTHdZNmZyR1pqdmJEaTB3UzlMSmI4eXovWkluNVF5cFRPTXJNMVh0NVdmVHhIckw1VEczdnJMZ0J2SktsUjdyaG9GMlp4dWc0U2V3ZzBtNWVBSTdKU1h6ZmE5aUZkYkNRaVU5OUxCQUE3QmRLUXg5NFRqWjZtcHlCcUhOMmNBWDdyODQ0MjR1Zlk1eEJ0KzMxQTNWc0VRWlAxZ1RUUjhDUTJPMitNUEVWSVRsSnpQNm9WUm9VdVArWkxtUUtjRG1UNVF6VkNXK1dqdzFHQnJaOUxmWHZ6RFJYdlJMY05lMXFmUzNuUDFDL3JWdUtXL2pBVGpGMlNuV1RLOEY0T2FlbXBGbnlBWnBCWDE3SlVjMmIzeE14Wmo5ZGI0QmorcFZYWlFCdEd5cisydHVCSEdVTzdmblFzR1RtRnBKT2ExTURKSFF5Qlp4aFNUd3kzQ1FwRGZmNmw4aWJkNmJWdDlXZ085TmFTOWNZSzIrQS83YWtXMmxybDNSMDY4ZVJYZ3FiWGd6VzFpMythdUZJN1ZUWVR0a2gxTlFiVW4xUzliOEM2M1k1WG4xS1dHUUJwRDJESnlmWEZVbXV1WDJaNHZIQXFuOGZMazcrVzBpNFFBMjlUbGxhOHlsMkY4N1piT1gwTjFmRWl1aDRQQUN3TjZUdUxUYnpuTW1YSWR1Z3pPWTU0RFcwSUcrQitYODhtRzhwdE9nVkFxMlkxVGF6QUNpK3hmU1FML05hK0R2M1hoQi9vdHRJMlVsYUVubEp3clkzVUV4ZWJNbGthak9TZ1VWZlZoeGl2SmNhamZIakdtZHh6Mm9URWM2SU0yaWx5MDZMUWtLT0R6SVdIQ0RVdTczUmw0RXBBaGRXQ3dCbktOb1Zkb0lQQnU2Mk54ZDd2TWVwekFuQjdUZmJBR3I0Rlh5ZHVDQlpXc0RqMzJhWlA1WmV6cHFpV2ZWV3Vybkh2dWxxM2hRTEFxM3lmanhFUkJkQzVoaHkyMWo2ZzNKMjBQencxTVE1ZTk4d3RZUlY5U1FZWEFyeGRKY0l2dVppb0kxaGFIemNTRWwrZGx1bHFacjNhQ3pzMTl0QjZJMnRIN1prQ0ZZNDNrVUwzL1hGZUMxK2k4RXlpdlRPRFlCeVV6RU9zbUNUVHdNRmhzSHpCcExqVkhSUlI3L1NPbkJ0bW5QL0hobkF6UGhRYXJhdHhBVjNxdmF1TEI4K0YyazhhTkRQclhodEFyaXBoQjdFbTBiRUxIeTZsQ2x0czExRnNzODhMNitHMWdYbnZtM2Zia0V6d2tSdjlmOVdFQVlUL0JBcHdCK0M2d3Joa1ZpNXdRdzZISFFJOFhQOXNiTGlGY01sN2QyUUlOT213dWNsSUY2c1hUWXRwUlJwUTIzN0dycWJrWTBWZit3WFRpWVZ0b3NNMkRidklOU1Jjejkzb3dVc2pnMURCNVhvYmNIRTBPbGZXVGtyZUg4VXAwcWxpL3E0V0ptUEpMLzNkS3krR2dLeGVMSDQ4MkxkRVdxdFAvZHY4NjBVNUYveFRKMWx0RU0vcC9YRWRHWGMrbDduWDByaTM0TlhiWEhORU83bmpwMWJiS2Y3K3RWNW4rYVBEUFU5VDEzaStqbkg3cWxSZlBGOTZsMTJzSnJwVGRkLzhoVG5Ici9TZk1zRGRtVXNMcjdxL3hwbWNUSzlBd01JeDZ1NzloYXd1MnptN1loK1RSbDZUU2hOSmdhaDlIQ0RkL0pNWE9zWU9MMWRKVTVnMS9LS3AxYk5uc0puck1rdEVYTXhxaVc0cVM4R250S1hpdXRJYWorUE4zS3diNVV1dzRpc2htRjFOZTVkbWE3QWl1YW9NMDhSLzZBL05qZ2txaEp4TlZqMVlNTTBRSzZMTDhvdUFFYWRNRWZQdEpoWWRkWlg5VnhnczVIM2RyQ2dlbDBuUXhDV0FNclpZWWNhR01tbEIyMHhBNGpRRlhBQkM3aWVRN3VhUTUya0VGUjBxVHJ3cW1ocERMQlNCOEZ3cGxtaEpOM2Z1dlROclhsRWU1MjVvVEhQQ0todVFlR2tVRzJEWW4zUGlOQTZnTWhuVHhIZy85TzJqRm90SzR3aTdGN0hUbTA2OXdqVnFJTTUrZUNyMW90dFlEcXArN1VQT05qSTU3eFR3Y3d3S0VocGY0SHM4NDdodDB0QkZEV3o2bE16d1NtU0xURHhIMkQ2UGdweVNLemJDOXdIUzJ0bUF1WXFJdWFrWktMb2VFZC9KN28weW45OTVxM0RzQ3lVY2FLSUlBUjdveDJqdUdNTE1MZkQwT1FQSnp3NmMraHVWekxPWWJsQ0xlVlIxRDdQWktvbGtVVmFFdjN1Um9WeHdHaE5SMnEzcFhSei9RK2tId1hLTFFYbXBGczZpOU9zYS9veStmdkhQbk1IL3BmYzZveFExUDVoNHBCSGhHQlY2U0tWaUE5VlJGcmp0VTRXZ2UyZXYvVng2cDBUTElZcE5BR2NTcU9NbDdITCszL0g0dXI1NFYzWWVZb3RnRkh2ZWZ6ZEFBN2F4dXMwaFJPWkVJMXlYMkN2ZDRvWHU1bzRYaW54Yk9QNTBJR3JHdG1xZGxQaUhUTVdqTDlJK0JuOVdrTUtaWUUyQmlOSDFqQzNPNEFFTGR5QVc2YTA0WXRqNll2elc5TGhHNGplcGNRWnFScytDOHFhK05BK3hUUWxxWStyTHNXQnFua2paV3JQMjhUYzJ1VEN1VFBaRytwazZ5UjJxRlVSMExpY1JTclI1LzN1dGVXR3JPcC90ME9zYXU0VTVJYzAvcGxWY05seFY5WXpXZHJNRjJ0TUVGK1dBeXU0Z0xrRGZQeUUwUVpiUnV4V2V2QlNvTHRWQ29GRDNqczBVWFN4L2c5NzdpQTNXM3doVUwzQ1dtSTljcFZCOGVqWllyVkhRaWdiUnFsTDFMVm5LZTA2YVJ0VnphZDNFU1dMRTNCc3F0T3V1WnBOM1JsRUVZTlNzYnJsSGNoSjNKUE9EcG45Rk9Hb2V2M3NETXVGbzRxUkJIcjhPVW5UVXAxL3M1T0RlYlg3V1BaSW1qU2NKamVnVllnV2NyTnZDblFIN2lLUlRHR21lMVhudjd4Nk13aUJnZE9TV0Rrdld3Ti9wQkNxMDdVR2lwSWxQNkp4ZU9Pc2JLbzJ6cUtOVUd4MElWN2F6VVltYWRWbXloOHJZeDc3ZjN5bGtRY2JiTXlveld2Ni9zRTY4ejYvUU5XWnVzbGlGYmRIbXNGaDFXVGhKeGlSRjA2d1lDMHQ0OUpMcnV2akJoRFduWHJrNGVDTnRIMmwvY1BNMm82SXkyWU9FNzRzRkhqVEZtRDVYRFFzNTJZWkNVSFdJLzVWZHZXWWZzNzE4ZHpDUFhRZFRVMzgwUGdJcXpmSGJ2Q3hZNHp2OEVDc2djd3NqMy9zZHFUbSthNUNoUkZHT0tlcFVZdlhBNjZkQTJFS2dYTldVZXpuYmtNYzFEdWVwZDdQeHBhSGg4cHcxNkxGNXNhcmVZamFHclI2YnJ6UHdwd3BXancrOThkd3FsV09BbDRZV0prZHJkSGIzeCtabEh5UjErclF6N2NMaXZCOCtmRmkrU2dDU1hvZlRsSnl0dGJvOEVrbUdvai9Rb2dkclkwM3NMK2hEWWN3a0dVKy9CditjRUNQWjFucmpVR2I2d2dxZlVSaXF3Q3hyem4vYUY0cXB1U3dqbXI2ai9TaEVDaDc4V0laWksrVnlBTDRKa1FkM3FWWnQ3dmZGRCtWdG9YMGFxdGxwRVhJb3VQL2hta3VyZkhZZ1U4OW5zM2dueXdnZjdUTDJtUkl3OXJuQkpZMU5aRmlpU1VhOW1hOXd0VEQyV09Zck55Q0NMR1IzV3RTVklSY1NmK244QjBRMzA2bnBRdk5nOVVjSWs5WXJUMEdGVVFJV1pualNwcFZreEw4N09aMTFseU9pa3l6RFdlcDREcnlWTnlyZDE4MUx0OHJ2UU1tamRlVGdvQkgramdTWWFrcWdPVDBCcmhTNE1JMUMzQVBYVnpsazhaTUVnRGg3WEVFOEg4U2Q0RGhBTlcvUFFpVDB3ODcvMzMxdlA3R0ZwZVljZWhxMmYyNEdMc2hxYkFTekVMVjQ0UVNNOEJNcVNmWWsyV2FWY2VKM1lRSllNc1FNcEt6Yks2Q2ZQcGZveWpVdmtsNG05SWZxanZBOGtmOTJWSW5DTTIwMjFvcXNLVHNDa0xmcnlBd2hERnRIK3hyUE1ibjBneHdVUGRKYVNhZEFSOWFzbHlsOEZ6WGxwbmFhRysyZFE4SXlpcWM2WGFoUncxMHJra3diUEowcFFXTk5LaHM0amFqNjN5QXB1cDkxcjR6K3pTSHNwTFBMektwdTNkUDZDd2VEWUVyRjdpUDRObTNZT1RuNFBhcU5aVEoyUzVnNk1zelhGTEZXQTVxSUs0Z2dQenExUHVQdGl0dVU0d3BHMVRoOFFpVEFPeDhXNmZVUEFERlN3V3ZwSTBJSFhSYnVtSldsRWFJd1BPV1E2T1huNHpVZThzeFMvL0VpcExxbDlMVEZRc3Jyak9wWXQ3ZXZ0ditqd01XVnNHQVZ5aEFKOUlmbFBOUU9OK1I4STZWa25FcFk1ZFh6NzRnTW00eVhiRHNwZlpNM1NwNUU0WHV4RzBBREJGZFVxdVNBaS94eG9lSXoybDJmanJpOVRST05TYWVXN3Q2R0RsYzBwRDl0NkJhaG5EMmtINmdLWW8yVzlnMVN1SjRXNXRVNWtnNXF0N055ZEpzVk56WVNhcmdSMHQ2NG5sbW9nVDRab2duN2h6MkEvSHFMUUp0cnlDT0FEeGplOXFQaWxaSWErcDJGNHplMnFpanpHTFFCbWFvNzVIV0JDZmowQzV4ZXlISHlacUdmcjlXc0hXN01wSDVMWllSNmlia1VOSGxHejF3NVVlQndadU00bHp2YTlWTElaNTNsek1XWWdMbVRocmppWXZOcVUrTStWNDFjMVFYczVjcmp0NitrbzZRMCtudlFyWDRhS1R6UVF5Y0hkMEdRWS9ieGRCZkVGZ3Y3UFB3OUVGcFY2QUppeWxTTkRsNWpLWithY1JhWDhCUElJbG1LSVVoMlBTUUdESjE5NFgxdWVNWmk0bmJiL0dKY0tEYWlnNisvNHlNUFl4MWQ5K0dHMk5LY0VwNTF1WEkxSTA3S25LZlorT05IdzIyMW03OWdaaGl1ZVBYSkxvT3A3RkN5UTdqK1cwTjBMV05TbmRhUlB6anUwVFdiUjczY2d1WGQvZ205cmdrOGVyU0F5VHVsMEQvYk0venhlZmFjWTBYM2g3eCtsZ0ZpVXNzWkd6enN1YkJ3K0J1eXlMKzQyVDZRbkV2U0ExeDhDUDhmNUoxemRtUjREcWgyTisvclc3THVtL0pvRmtpK0w1SER6eXBsY1g3aGN3QXFBS2orTlNka2NpdWhsaWw4WUkwWHNoNlRmb3JpTVdZQnJQMDZhZzhKTzlvanBxekJpbk1oUGdhbHFyV1pneXNObWNyeVJpOE1zRHJIK2tKNTJwWS9pcEFoeXgrUlI2SWc5RXJhVnVuc2t2ckhKMzNHYklqT0dRb1ExYkxnMUF1SlFEUmU0OTBRbGlLejZjZ0o3TkkreWxyREdOcm1CWFhzd2tHbFErRmROOEcybnJwNElDK3FnWSttZGJjOGI3b29ndDNWbVFJQzJpUTZhV0k3ODB1K1RpbHdpaldsRG1MbEZ3Qk5jbDdlTlptQjFQOW1UOEwxbDRTUFF0YVlSMWJ1MHFUMTEzVmsvSFYySCszaTVuRUx1OEVlUUFtbHBUcWlYZDlzU3pNUU5hVzZyOEFlNGNBamVuckpjc3Q0TUxXZXpLdVBqUmoxODg5WUQzTlp1Y2NMQUdlQ3ZGRHlJMDZteUUzeUNRUUMrVHpyNGhucnlZczNRVHRmOVA5VkoxRHhlVHBJbnh5MEsvdDVQeVZ0UDM4M2tZclMzRTJadXdIRlVMYzkyUXRTSHlBZzNNTTF2c1Q4V3NaTTJYRkpNcHEyQ2ZrUHhKWmZJcEdIVXVkTG43Yld0Tlh4VlNPOVBoWFZ0SjRJRU5NeEZoTEpvaU80THZ5a3c0RlZ3WmJsY2pDK0lxeGNZcGV5NlRvMTFEQVBDYUwxVzBjQ0xNYVlQTnphbFMzT2VJcGxPNHJnOW1QNHhSMFk0UTNqcjNseUJOUkl2SGcvajcvOUErajZsV2tkMFV3TG1JWE0weUVjMXZPVjN4VGRVWXQwSDVrNzMvcFlNNmszK0liSFRva3B0MWpYc3ZXSVVJN1cyUVZlMm9KS1duL2JYQW9keFFTVnUvZUl5bzVRZFVuZGJ1Q2hjeE5waHJuaVcxSXk5WGY3aTYzdFVPSXRYUllGU1lITThSdHpTdVNUdG1PNjVpWXlnK2dxNHVxN0VsYzBGeHQrckdqa1NVakVrRWlXVEpXZzMvVmtPVjU4NWZZK24zOVd1d3JtS3MzRDhIVFFGK2tZenFlVWZCV2ZjbFZXdEhxMjZZdEpLdnBVdDBreDcyTnNPVUNHSThBTnU5Q1BPZmR0SW1HYks0UEJLNkp4bFpwdXBKMlY1Z1FyUTBtVXRseTVWN0srOVlZT1YrU04zeThIWUFpS3pTdWNJZ0lQOHB3M2luTlU3cndVVHBUdzZMMlN2d2U0VXJFRS9YUWtSQ1NnTEw2bmRSSVpxLzJ6aGREajZZcDFTbkVYR2tBSEMwMUREdnE2ZVhPV2VMK2dCZjBrdzhLVjI1eGhiK0FZT0Y2Ky90Rlc1aHRhdE94aEI2UGZoNVNKMkU3UnZkT2xWT1ZlaHk2dktsK3JrZGZTVmx0WngrZnhMTkVQRUtGdFpPTjliME45ZnlBZDVwbmlmaWc0TDJjWkpMMTQ0UVYxZmw3Tlo2ZnVTN0ZHTHNvLzN1eDV6UUw4NDE1OWFJT2t3UUJwRm80bjJYK0ZxRXQrN2YzekxZSThSdXROWTNYSDJuRkNENnR4TXBIeVFEWGdMVmhKdkRKbFM3Wk1mUzFpenZ5NUQ1TW5UMzlvbWlDNUYxc016VEFhOVJTVkRnbzJaZGhzZkZ1cmNPekRqcE5mSk9nUEZvLzg1OGZHWGZMMjR4bXh1ZENMSy9UQzg3TDJWTlRUT0p1QlBZcHNMM2xPL0JkWXBWYkszb3pDZGxvTVVobEphNVdWcGJSTFRvQ29mNGUwTUMveng4QjB6Z0NQWWNaWHZ2ZFQ3V0hKMUxveHZWUGwzWEJJNDBybjR3bTR1Y2pBUGJDUmdPdWZ5Nkt0YU9nTmhtS21CK2V3QU1ETi9yTmdEd25EcmJRcnY1NFpaL1M0WHlMdnJSUDhQN1pQbzJDY1liVzVIdG81elRxbEZQSXR1K1hxVFBiTmhqcXF6VlZXL2FjTWtiTmFYeXBJRnl6Yy9WaGRGTnI4L0MvNzZsRFBHMVlsZ295ZUZCUkpxQVlXNDIwVmpaRlZVV2gzbDAvUUFvT29xLzJ6YitiNVNMckliYUFwOXVVV09XTkc4QzZZSnBqRFlDelZHZXZUem14UkNVb3kyOExjc0crN2RoWC9vSnplc216Vzg4bzJRTlJCSlhtZmxuUkdjYmcrSElmMTBlSzg0ZS9vSktVMkNLR01NTmlHSVFTM3REdVRKT2tRRTB6QlFTejM3RXZkcmhGWnFJNTVQc0VJdHkvRW5Id1RJR2Y1WU41Qm05ZEViUXg2KzIrUHd4a2cxa3pHQ1hUQVZMWDhuU0JUdUJsNEZsUHN0ODZzcDRtd0dPYzU4UGltMkg0R083VFNaaC92bDNWOTVNeHFnT1RnYTRRSTJNQ2cwelZLSHVaM0lIaWNDU2hvbXVqRE9vYlZDeDFVS053dGZhYnB3NGdhY1NYckROTGpzakNOa1ljL0VST2trTnJ3YnVDYW4wZEQzNUdNWHVIUUZsMCs0SDBONC9QanNhMzlyTkNOcHAzU29kSHhMa1ZoZ3VyYnFqVStEVThSTGxkcFRaRmJDQ0dla0RUUytrUnpReDhjZEQ1Qllzcm82K01pV3pvQkRxTjFGZDV1WnZxWXRlQ3VYS1RsNTNTdVV3UmlscmIzNkNtd3pYOWhvUHN6RGRVUVRxOEpNVEo5QlRmQzZ2U1VZWmx3QXlpUEhZanpHVmMwK0RCMTVaK3E2Vk9nMk5UV0VRUGkvN0JLWEtFNy9GMU1yYlE5cmxRdjQrZWkxcEt0NjZ0Q05oYVFUT0srZzVLajdsSGFjRVdYMmNZMkNLWSs0MmQycGZxNThaQUdHL3UvNW1EVE5xcDNldzdpY2FHQit2dHFyUG1CR0ZiMnF5Z09tbElMdDhoMGZGVlFhWFRhZW9TMUhoZ3Zuc2NCWHpaZ2Z1VWpQbkRyS1oyV1AyRS83TDY5bHVCYkxEaWVPcEJRanZOMkVUVWtnYm9oeUttUVA5dkVwalRUZXJXM2g5SWFOMklxd0J6MDFNbWtEVFowNkc0UWVPbXNwU2J6c0wraXVPTWluT1AzTWRMam9lV0ZiUXp4NVM2YzFWemZVaGdYNU9QTlN4ZkkrUS9ld1FnaW5lYUdBVG90QVZaczlwSkxGRnI5RHNMRHFYdUlWZWg4cXgrQnd1T1hQUU9oeDlHSVA5OHJSSVJqV3pOdzNSRXRpRURQMEV0Sy9ZMVRvSlp5bk5UdFdmZDZGT3l5ekJsY3FCNWNHSnhtbVdrV0hRbWhpWENTVmwyTnVMVVdlVEtWeDRxaUxhV0VDU0NxOHQ1ZCtEWWc3anhrUUo5VDJ3NWE2eHR2bGlHMUszbmhRVUhaVkJGNC9HbHMvUjdXa3dXdm9QYjFtdU5GM1cwY3lrcUs1a2J5YTcyQ1ZaeEJERDVjeTYyazNmZ3RHZXpvS3hnazU5N1ZobjEvclJ1Q2pNOVNpK2tSVkNxNnExTEptWXlJSjNpRGxuR2VSWHp4MHdZQlJibnF3TnRBTG5HSUt3a2NiTlgrczd3MjIrQVdGeVdndkJpMml2VTNndXJxS3pHVVhrZ3E5enlnN0MvMGtJQ3UxK2NadnAzMUFlcnlzUVRsSzhEd0VvL0g1K0x3bnN3MUxoVlJTT0dnZDRYOTRvenNQbXVOQ0xwNjhDaWtKdm5hajQ5bHRMcitNb1JLVjRnV0RPdDRQd0phMktnZXdnY3JEVEJPV1ZUVDhmTVRERlpDdXY0eXRLdCtMdU12cUFLLzBjS1dBb25hNjg0R04rdmd2WEluYm1vbnQxQWw3b1dnNmU0UFVqcEJwbktnWFRPNWQ0TFJ5c0xLWkp0cTQ5VDdHb2hFa0k0ZVBjTnNXcG5iSjc2YjFIZnFTNW1ESHVKTGJXd3dqN2oycEdXeGlmdUoyVEJnb3h3Ulh0ZERzWDFudUdlbk9DZFlBenhuZk9uRlRVaE9SNVVSalFyMDlibTRGbDhxaTd0ZkZEQTVnbkppVzBmME5VL1dtc2xnUXM1eTB0Ym10VUxHaUFQWHFUVUJPN3QwMHhNYXFmV1JUWEJ0UWc3NjA1Y2h1V211U2U3WEpONzc2OTg0dTMrcUxCNEI0YjRVYzcva2pNRWsrVWhsUnlFQTRWcmxuVkg5Zk9oeXVlMVlWdExiWC96NFpETkhyR3VpdVAvTU14Vkh4aE5HUUpZN2YxWjRETktqNitlNE1GVC9zQWtxcGZzQnRWd0ZsOElLVDJJUWF5OTNVakRjaFhWNG1tMytIRkMwbTUyUzFraVhJWkFGRUlQOE1MTUF5VXJxYlNxTWRvZU5VZ212YTFLU0Y2djJtV0p0YVhnQTJkcm03azhhcE1yL1ZMUkNSRUF6RGJIUjVRQ1JTZHdLcFp6L3hsNVRINHNZSXQyOTZ5L3ZwYUovTjU3RUw0LzRwRmY0YWtzbTFvRDgvL2pneUV3R3lYUDNmNkNzT3h5WTB6MXhrNXpVdXZzb2UvQndqV2dnZG9WR0ErYmNVSnZxRkZMZ2xIclQvUDFTc0lETkN1ZWQ2Y0R1Z0xyRFFzVXVWcmlVWExMcnlGc21YMWFCeE9VdndDd3UrOFNVWlpzQk1GNENnNk9RM0M4ZGdVR3RXbURpMURldjFXRDhNL3pmeFJyd2MycmY5MzhZeEhsRS9FdnBPZkQ5QUgyRjZQNVREamNsV2lOTjFCb2tyMG1MSE40aHdvcVkzbG5Kek9hNGxDcVArNnVudWQ4VUIzbk5hOW52RTZwYjZReWdMU3RMZ2dhRk1JWEkwcmdGdFpkbzdRZnQvTVF0Rm9ud2QyKy91ZEFwaThuQ0pubDdsMEVYTHkwWlBUOGl2Mm4yN1hnc1JJQTE4WTFSUFFKU2FtN2hadnByemNudHdkekRsNDB2RDNDazBQeDY1QlFUYTZUUElOaUljVGMyenpwTGVCWStXWTJmYTZmb3BIVGw4YUF5UUM1VWhEMWptTk04TW85R1JQVzZSM3hibDJiZWRiWjZIUnVhc3VwU0tnRVM1NGdvdmY3Y0FrSlBqbVA2YWxpKzhDS0hJY1BoWCt3QkU3YXRpejdRSDFxRUxrU2VueFpoR0JpUTZHWXE1WllVbU0ra2kyOENGaG4wV2xPa2swdWRkdnFiZlZDZWowYk44T1duWnJiYS90bndhZEJKMVhFM0FVM3FVRTZXSzVTQ0doRDFDSUVMckdLclRjYU5sWUI0NjFjZUE1Wk84Rzl3dFlIOU5ub0pDejFlckdWWVNVYWJ2MUh5RFBxd2NhZ1RHMmRneStOV1VtY0czYllkNFVWSUlRQlNhc2pudncyblNteUhxUGY0TEdwUjRpQ0tZRnVIdEk5Yk1aUW82VDFReW5yakRHZHRlNFhabHNmSmNDNThKelJtUnplYm4wQjVrZmV1WVErQmY1dHdDc1pNNjhVNzJWOG1GMzJ4WG1uSi8zTzRHcGZmckZWWlJZSUhtb1M3Q0U5VXFoR09lUXByQ3B6WHVXNmZ1OW83aFlzblpRb0JsUFJVbVp5NzlsKzN5Qi9ZT1ZmZkJ1MnVsbTM4cjVmeGo0NW96UGhXZVJneGZJZW05VUcycEVnUU5BSS85RWNxdDNxc0R6TytvcTR6MGxENkF2UVlBN1JZN1Q5V2UrQXhFZllrQnk2MnkxTW1iMzdlMFlWNTRlRk1lM3hLQm9UOG5SQjhXdHJQbDZpTVhxUE5EZkkrTUt1V01CTXNGdG5JSnQ5clBoRWtiUEowYXBBQUpkb3h0akh5NnF4c1pSSC8vYnJCTGlYYkpDbTJTelVZeitGNERta1NNcE1icEdpbTVLd1VRbE83QzROeE9GS3ZQOXJqK1FSRFpPNGl1T1A3ZFBNY1M3cndaY2tEWWlJTHN0aHRuRE1pRkErZ1dHRnBJM0wwWmpOZjFEVWd1OGZlQWFZMTVCcmo4NE10Wmk1d2RJalNsNHZPcDRXZmgzVXZSVm8xNXk5UWsrQkRhRW5qWDQ4SlFwMTZaRVcvR2RpZGtQMXMxbzFaWmp4OHZWNElSNVZocnhMU3dQOEs4a1IzNHNYcUtqZzR4NENxajUrbW9kYnJQMCtrNlpNRDVUMDArZVV1Q2luSFF1Yjd6TW9FWmtIUUxQd2R6blJWUVBuZ1ZoaE5KSnFScVJSRmhLWkNzclJqWVRsMGJJZm83U0JjOHJDVGhSM3NqWnNDQzJPNC9abTJKcXlCWjRoV21ucDJrSTVvdllYYXY5ekZVbWxOMnBwbHVHR3RNWE14NDFhMkhKcThxeVZtdDZxZm8zcVlTcG0wN0xSNk5GNXF4bSt1VHZmUzNZVndnb2M3eXdVQ3l6SW1zNTlaYlpNUE9idXUrNGpKcldQY2dXNHoyeVdlMlB5UHByZkJhR0lPaVlyUWRyZDBUT2piRCttOHM5TWJnNm9NdjEwTEs1SGFUTFRMbDVUR3BpekpNa3FnWm82VUJxRlhDSGJ0Q29mVTM1VkIrcGZqeEErNzhIRURYeHFPTnBWUGlOOHZBTmUxTldtY2JwNUpVZkt3VkpyeG9DTjlxS2toWjFTb0VkdTBPeHRjQUtPUUVpU0dmRFFlNnFQeFNYZ1JDVTZuU3hzL29hLzF4SHI4Sml5ZnlkUzRJTTlKa0tOODM1dk1QZGZyaUtIRDRoNHBuanVBMDFsQk11Mms0eVpxd1QvemVKTnlndWpwMlpMRjN4TVUvaVh5UjJmVzg3K3ArNi9wZjRtRU8rMVhuZlhDR2hyeUFmUTNYeEJEd2NoQTJZQ0lNVDRQM0ZoaGZJdW54RFJKUDl6ZDR1ZVpDbjh5YzdqVjdONi9nVUZwRjRsTHZkQXJtS3ZIK2RyL09DRE8vbEdmTGVtQ1dnTmxMOStieGxXMVBNTGJKMTBEclpJM21ocW4wZkFyekZqNmNTei92aDI5ZWh5WDFNczEzRk5OcENzODVlODNHaHArTWRpdmF4UUtQMHpmeGt6SWNmaUJ5azJSUXhSRGw1VGFWcUhRQXVRaTZ6MmFDbkczV25YN1JIWnRvc2FiRXhYaWtJd01qY0dHWGlORXFFRjFwWGxJZFRhQXgzdFlkMjArR0Q3U2JoQUEvQkpOUTlGdXFCdW1nOEo2Y1psRzBLUGtBMzlEaDNxcjVESHdGaHlkbVg5cEUzVWlTWERnN3hoZWFXZXpyQkYwOWFzOWREbGQvdDdEZkVFSWtsVjVpVytyWFJ0QXNCSmJySFpsUDVuSTdFd1B2aG1QT1RYRW5uWElwb1ViZEVoYTR4dmJRMWZWenQrOFZkRDd5SWptVmI3VmpOL2dtdEdTeUF2ejcyeXZHSDRyT1RobHBldjZkbzZjQVRMejNYVWRLWGVsRjg2ZW5GcEhMZUlmZ1FqdUxuSlE3ZGFaeHZiZFArRTkwUmIrRTI2aENyUE11MlFHOTZuT21NMEtzdXBWZ3NFZFZnMlFoYkZxZ1R1TUp3TGxCd3JBcG5VYmVLNmR1K3ZNZzVpbkNGNHpWa0dWTml3QjJKbXdHdnJQWThZTVU1U3Q5alVnR0hwUkplZmMxUFRYQjh1c3dtOU53bC9pTG1JVFJ4VFpFd2ppZnphYnFSNkR2Unk3eVZSU2tkbTB2OUxrUFpKelpSV0hEcVU4eXBqMU1pM1dybVVpeG42ejREYVFLMHJVMkw4M1NLYi83aXE1aG0rZ0Y3WXZTQjhaN3dCbXZ3VEEwK1pVYzZsckJIUE5jSkorNE5renRvcmtvVUg1OHB1UTJSUTgvNldYS2VnRUltdHZzS3ZVYkdWTFJjeDVmVTNqUXJoUUs3N0s1R0F6Z056MUFHeHI3czRyWldscjlWNkJYbkw1MU52MTFaeEg1S2RIZlgyU1RpWXFaWm1FK09SYzkyTld2TVVWVWovVU9KY1h6b0ZQSXRCdnAyZGxMUDd4VHBkMnRHWm1iSUtPTEV3bU1nRjFTZmZBV2Zacnk3a2xmbjFqMTRDcGtRRjhTQ2x5ZForV2NsaE9wN2R6K0FRbmkwNWVvVU90ZmJOVGZxYUtqekU1Q2V5TnN1eWFkbGc1bDZHcHQzWDVpNUpDQjZWbjhWb3JOaCtoQU1KS1dsUE1YYmQxVDVnWFE5bXFha1VlaUQrbGExUUxRaHlXV0paMVhUb2ZNTnRjRDQ0UHYxaFZkZEljaktCb044ZThleGRBc3A3ZE9kUlQ4NU40dlVubENmY0cyaml4TTR3TUU0dS9keHFaRWlyWWxKNFkyWlcyQ3pSV2VjazMzWDZPNDQ2eW4yTnhwVzNRbnc5NG95QTZ0TGhuWWk3OHNEVFpPRDJTanVxVnRpY05LVnRKWGQ5b0lmRkJBcEhWUE5Kd2swZ3Q4aXFrTGlNcytIcE9TdzlsWmVNZzQ2YkZrdWZRbFB1R3hGejNFejVWTkZsN3pJWkZ3ckZtd2h0NkxaQUl1QkZqaTc3dDRXbFNrQ0xDSDRzN3Q0RkZrb3d1cTk5SXVzSWM4TkpSUy9oRGx5ZFJ2czM3NjNBR0VXNEFKZDFwZzR6S2xDdnJ6RU13TTJoUW5wUzlvVGc5VllldTVFUHQ3QTVhVk1EK3dvTERYR0hFVWZTN2hoZ3JXMDV1YTZlRFZBeEJva1VOWDhQR0RCby9UVVBwWG9nM2FiNjhNS0MwQUhLd01DOEVsNm1ENW5RT2F1ODBpbjFHMVhnNWpqVmhFcWRDeHM4T2wwSnEwYkZoNE5IZ1RiWTBzc3ZpZTJHMnFQLzhvNTlJWGR1QlQxMVROYlhpdE0wRkZNN1dUTzFpUTRUa1VaMml3M0FrV0lieW1BZFhQUm5CT1pwci9heVN6U0JlN2lVa2dpNU5pODRlS0EyaHp4WXRYUVRUZFU4WWpGZnRiRmdCaXNVcEJuOEFHRThqM0hxM2t0V2hJYktzeFNHaEZsQ1g4ZlA1a3lBd0ljd3lzQWhGbytwSU92d05TYjhXaVNTeWRzWGdaVEUvZ215VVg0Vzg2eXU3N0hFcXJHWmMxYUdveDFGVndiNXlybEgwSG5GM0RLVkJMS09BdklYR3pGWnQ5VTNYMWR6S1VzYUU0UjlrUjR3eGdoeGNiT0c5NDVxalNKVXZSNXVtc1VHTmVCK3JUNFc4V2laNGUxeEl5MzVma0VBRWZtQnNla3NEdTBENG1yaVJ0WUVpVmgrczVveDB2aW1nSm5WQ0hlZ1ZUdEcrMWJBUE5tbnd0Q2pPVU9tMzg4MmM1M204dU5Ma29RWXVvY3dNanJLRkx6amp3bnEzeVpZdjlvMFlTTzdWTU10Slh2OVFDOFFUc3N1VHUvMm1MbWVUMzYvVXJmSVMyVURIRnBQN0dwVUhtZHBWZ2N6QjIzUld1MTZjS0ZJUG1DMyt2K3BsVjAySmJvZmRENStOc3dvZW1OTTNzU3E3c1pOZlVSQWlJZzRvb3lLajYrSHVmd0tVZzRXUXF3MG1vL2lDd09udWFKM0RyZUZtdnVPalBqZnVUY0dyRjZKNDZ3M1VpWGVlVFhMV1BPYTJYaloxTCtPdzNXMXBhOXdHUGovZE52WWJtaG5kbkJ5ZXhXS2lRbDZuRFMvQUkyaFpmOURMSnhIRU5wMmEwZmVZejJRVkFlS1NDK1gzd3VRa0UxMUh0OXhoWGpNMUI5aERjRHd2SUNGT1pJMCs0SGppTUhRUXoySm9GRVhCR1NvRzhqelZyQlRkdGdQaVkybTRjd3RkY3p5VW14d09QM29saHFEdzRwSjFKdmRVMEdCM0JRWnVrU0VBWHAyZlQ0djdPbUt0OUFHZ0V0SFFVT1VWSHlpT1JEZ0pvRTd0KytLd2VodHoxNXE1SWxqdkdtbkVDVzlkampXMEpxbjFLVFhHc0FQMFN2eFg0ekthTVRtY1dPYmR6T0JtTUY5VGFNcGJUOUdMV2l3SHNJU2c0eEVXUlFtUXlKWXlGTGZveEFUSFRUQUNvRVgyRGRGeHI0NGZDc2JtRG1idTI4YWhkd1BZcHVUWkdBRGoyd25iYTI5QUI0aXRibCtHNTdwdWIxc1F6cW5ucVJDK2NRb1lmOGZwY3NONDdyeENWYmhEV0dGd2FmS1RydkVqRGF4RUNWYXVHenY0SWlzT3lqR3d6N3ErNTBtbmJLMHhXY1Y2UHE0TjNaSGF1Z0NYTmpUcDZBSjI2WmdjaHdrL2R6SWdLbkZIb1B1QStQemhIT2FwQ3JzV2dKL2pSOUpYVS9NeWczcndwNG15bHpjOTJqaFRnWVM0RmZHVmFXcDQ4VHdwZVhFNzgzbEZ3RHRXdVJ6YTMrOC9TeFVZWHNGRGRTb2FwTmp2M1J1Sk42WnJ5RFF5d3ltbDR0anc4UXZPVStVK000cHY4MWRhOTFESHZLZDIxa1c2UmZCOEF5SmxsdHlqWG0xTXhDRUsxWjJwMzAyVVN4aUkwQUg1NGp5OUFFSkM0aXMzcHJWZ25vMzZtK3E1dUpFRjhub0o0cTBFdVM5MGZ5L0p6aWp6THk0U1RsT0RpYjd2UnRXSkRTMUUzNU5BRFpuUkR2RmdjZWdaY2lCTG53M1pmMWZyN3AyUC9IWnpyM1o1VHJWOXNHTHU5VmRPd1prdzc2anlOeGwzZVpQSk9neittN0M2aTNaQ3ViVU84N1JhN2R1Z2w4N1R0TE5LZFdnQm9IY1BSRjFqOGRjU2ltbm85dVFsd1ZDNS9vN2VhbzVzZnhibEtUMjdOSUxUS1UxeDVQM1dLeFUwRjJ3WlI1YW95UU5oRUk0T1NMTGQ0U0RxeXoxeWtYZng1bXYxMSsyVmF2Mk0xenZXTW1rdVdTU21mU2hPNXppbjVTOVMxRmN0eDdOMEZoSkdiczdVa1FKaURKa1dMNWxmcGFzQ05aMGtiSjRGY0RxT1JmQkcyYWR5THdyVnp3L0VneFFIT1hxTFBSSnBTMnorSkk3OS9ZVzBOZHRybmFvSDFFNjd5MnZhZGJsY2tjUmsrYTFCSU4xeG5qeUNUNEs5VC9xUUU3U1VvRFFSNnhqT0hFMWpVQUtFK2Y0SGlzSEwyTiswSUJGYlNsYWszKzlBQ21NY2JneDdsaS9ENk50UDh0K1Z5QmNWMkh5U0xTN01CT3piS3BwZjRqOVY0QlRFM1FBUmhSeEVCVkMyTy9SWGs4ZWttUzBkUHRTS3dtc0ZLdjhMVWJLaUx6cDRtU0YzWURFNHNtQUsxK0U4c2hrS2tqanJhTU1FaXJLUXFERVF3V2JVMDJGSW5TTEI0eW02M01VUW03Z1d0bFIzeHYxOTBkeTMvYW41MXVwZGp0eXJkWXppclhjK1NadG1Nbmhaa21PQnFwQ1lSbmZzcSsvVlpWU3I5ZTVQQitqZ3lkUzU5NTdEbXQyWDZLaG8zMklLa0tPb0o3UTVrWS9ZTHVyYms1LzNNc0Y2Wm1lNGhLRit6YVNHeE1nUkJoNFFEZEFVMlg5V1Z4eUVORXVyK2RWcWErdTFxdGQ0UFFXdVk1MGhleXRNbHZBUzJHOUpQdGo5cFBMQTZHSllscTMyOG8wMWtlaFJXWU1RbVdPV0svcGpoZmJRaVdrMno3d1RsazR4bjNPVjFCZm0xbE81c1VSZFlyaThzNWNOOGVnZ2VENG55eUJ1czM3cU9JcVRJbWZqTUI3VWcrQjc5V1JDSVdPMk9kaDVoYXM5K0pDcVR3ZmZ1c20xbnlhT1lhbkp2WVhTY3NsbHdSNHFId0E3ZXB1YWF3NHRISHhXSkFXa0lIQm9mdjk5UjFCRGNSb3gvRi92anBrQ1hmOGdHTm9paDJ6dklsUDh0TzJNdGxsYUZDT1FNaXpKUENsN0pmZG9mSVpxM1hmZG5pSHBTRjlHaC9CeDJteVRjUmpuZU4ydldub1VFZU03c2IzQTUwaGVCTWxXNStXQ1B0VUd1aVJjR3hBc0NXMHk3eEY4eWVCeWNKZ205c3NCeWxuV1ZLZlVjN2dVOExTb1dXaTFqd0tVdGhjL2JLSVlsUlc2U21PZ1hyWjVNM3dFZERjY3dNS3pDZkxTcnY0MkJGcTc0Z1p5N2J6aStZajEzaEV0QWJoQlFJUDNwc0NBV2F0ckhLTXpXTzJ3NWt0cEViUjZYSzZsNEdpUjRNYzk1NnBpUUViTmY0ZlNsc045dzlFQXBGSGFQd3V5eHNMUUhrd05BdkMycnNkVGVZV0h1NEQrYkk3ZHhRY2VjcC9iQVpsMHhQd1VsYVVjUU5OaFZOMXhOMkZmeWZWdUFNTjRBSVMzTXZ5b3o2dmc2MVYvYnF1alVlVlVhbTZBNko4dTZGU2FXMUY2YVFvNWdlZlZwZTNRbzUyM1NDbG1ObVViQms5QWlObmtUdm8rWXczMDh2WmlXN2t5ZDdDcFErQ1U5RnBhQ3BzOW9XMG1abGdXTUFrN3VSS3ZQNlo1NFFZeEs5YmkrUno0SCtrZkZvOC9Dc2M3TGdkSWNSUjdiSUZ3U2xueUZFazE2MEdoNC9nZVdtcEt5RXVKbjVNNCtkVzJ2ZUdhZFBIWUxKZWhOdmF1R0I0Z1RLWmRPUWFERU95WjVjOVRjTTAzT1UvOVAxdVV5SnBrVWpMaDNpMGxabE1aemRVVlNaQjFQOVkyUUl4UU1yNGowMHRjdEJxMHliWSswSEg1YmtQSTRQYS9BcXZDa1hkY1RtSTg3VkhFTW1xRWoxQXhhNytiSVRRbnZQVnNSVWE3VkgvZnhDcUZ6RlNDVFVKVUZmb1dqOVYyajdxTzN4YUl5N2Y4OGQ3QTRPSlJBTEU5ZWcwYy8vTTdBNVgvNStwNEZZU0NFRFhtV25oWVZzZjVhRWFPcHBrNnhUZXR2WmM3aElQTlN0R05HbXdHSjRVNUluVjBWcTRzK3VoeDJYTlBPaHk0ZXJQRXBqNWYvUmJpMS9ISnpsREJDaW9aLzZNays2dUNFaUxoQkd5QjFpbWoyYm5oVGRrQUZ5K3FDRnlDTmpJcXBOa3RFeUQrVXkyZENveG1za0Z2dGozZmVpNmpLamNqa0JleVBGWUlmM0dFMitQOG9PempyVGwvbkcxaXcvMmZneDhEVVpEcllHbWpmZTlwaXZ1TWxGWmhYdlZJOHBxc1UxU0hiUWhJeEd0WlJubG5YOE0zUEhSSnVQZUJkWFV2MFo2eGwxOHBhbzM5R3h2OWhwZU90bWZ6OE9VVWZsWDRyOVprVG1sYVhQMnlVUHJ0dTdiY3d4SVIxNnNieUVVSm80eXlNZncwYnJsWXpoTkRLNiszNXY0V2cwa0dEdGRkL2tzOWVZRmlXcEpXeXByR1ltYnp6ZHlUbVkyMEtXWEU0WVMwUWUwZ0Y0NG1pYVNOeWNDZWhZdGFjbzkzR1FvTEJjMGl6SitqV0tIU1hEZXR0SWVFWXljd05WVEJhaE14NjE1MWRvRm9id1FndmlneG5CdmpkZXZTV2NHR2k3ajVPWW12NFFVRURSRG5jbDlMM2lIb0hPbzc2RU5WYmRxaW1BcXZRWHpvcHdHUnc1TGZ0MDlOT3dFYmFSMUZRbytnNllwWnJ3djF1RElKc0M2VjZGZTZhWnZ6ME1jZGhZQ0Q4c3h5US94b3JjNnljbUYrVHBvQVFVa0lwaXpOR1Y0eWl6QXZNdktiTFlBeVpDMmt0M29SQkVkdzYrSDIwalpiK01iU1lsMUtaK0o1M2FWU3lqUE1CNkZCTUNwRUNnTlFXb3dWSkVaZ1k3LzkvNUpzQ3JXS3VMZnlxWU0rOXhZa2poL1NyRCsrUWh2azFOejIzZGNiNmFVLzd4cTZxYWNyRjZwQmsvYk5vSXRqYlJud1RWdGFkaHhRc3YzdWJPQmtnZTFweTM4VzNvMGRZaTlJNDFxMnppazJTbkliTlA3alJZSklMUVZTdVMwMXdPSUVzM0Jralo1VExHeFZwR3Raa0Q5UGFMZDdiajlrbFNXVlByQ01DSDRQR1RtR210N3kxS0oyV2RxQkdhdmVRRVhFNjZ2T0N0QXlXZVZtRkUzcU9DdzdITnhhNDg0VERhZDJ1Vzd2eWVsWWc4cDVjMmRTVzdzMHBpYVVVMThGalA2Q2NsUHdWM28rSEcva3htMUJJbUU2T2loQVdVVFZHRXBGVzZuL1FiSDRBN1J4Qlk0RGxKN1MraHBMakpnQjV2NkVvMzlnMFhWenJzcU9Oc1RhcThEU21kRVg3cHhidzhIVStkdk9mUERvbDdKU3ZkM0hPK3ZpT1B6QjZZM2tBeDBTQXVWOGJOeGFSS1ErNHBESjdubzhOS0I5VCtINmk5UWpPUEZtY081ZmZGS2hRUXR4SDIyekxXTHFWa01mbXZLL2RzblNOc3RqMnltbXplZ1NvWWpWdWJZSW9SSHFidmxHelNac0VIRnFkbm1HYkwvQy92dEduM3E0TzNYOVhEL25FWXBjeUEyNHNocTFrQzRzWDh5ZW55dEE0aWlmRHRwN2Y0a0Vzem8xay9KTDNDS3BnQWhwSGVNVVNibmU0YnlkTHhBZGx6UTZ1bmRLYmtTSjEwR3BEdHdXV1NvQndWOEVUdERSaS93a3BGSzRyMW5pNHFXREEyUkE5UlVUbDhqbkFrTnZqTy9WaTYwd2NxcTUyRkUxdlFtWFVOd2VpMVpITWZNdUw4TjFacGZDY09wQ0d5M3M1b20vSGVsMmo5bGlyV0tHRmg2bEZXdVBCeVNzS1lEdHZVeENkOGE1QVk2T0NRaFB1cTNDV2k3SmR3dDBvdC9WWFR5WE54QnEwS092ZG12dm5SWWVzS2w1a2hYQ3RlUDlBRkxaNndKT0tydjVsKzJ3SUpPcVBGY2hFekFVUlcyNUg4a1dmU3RKanpFV09XTlJDVGUwRXpNMHBxSUZkbW15UmpRSHR4YUVvQzZtb2twQ0t6Q3pQRDlBazRkbVZNQmt0TS9hUENrcmJpNDJJVjZHUVRSWWtIOUNxblFKbWtSVkJnNEF6NWdCR0JEOUIwNGJKSmtjL3JsRDdMVTVjbXpsVXpnTDh0Uk5vZUlPZ1k0T2NZTEp0TUlzZ3E3b0pDR1JRT0hSOTk5RWVLYldXZVJ4cXcwNGdLS0laNU40Nmt4NFlUc2xaZnVsdXN6TGZURkVoM2pEZS9xeHhPazdvcjJTYTlqVEV1aEpZZ1IyVkdOWmNnMjR2RjhyRFVxZEgyWGZyWGdrbENoVG1VekdBQVdRU1BOZjJHUE1SQ3dNMUZGeDJQRHFHT1A5K0NISkQ5dEIxM0NUTXlRNU5CYUlybXVVREFaRVo4RER2ZDhGbyttTUlObFZDeFViMXRKRVRrcGl5R25hTmpwZ2FjREZ2RjZVSjhjaXljT3BrSnZoRVA2b1dRUU1qYmVab3daa25jenBrL3hCOW9kTXdsdVdRNEdxSjN1NEdPeUJvMnA4M0xrYTBQMFliOXVHUFEwQlU3RVlGRzNoRFlQVThEc2xydkI3SUt6VERmN3ZtSUpJbWYwdWtIRURGeVovSXZUVmkrZXIxWnFCUXZxSDh1ZnEzTUNnaVQ1dWVsaTdzeDRqdVBtZzFiNHJITlQ1dk9td1BlL0pTbXRkZVNEWm5kK0ttT3BNSWxNTHl0OFlTemQ0UW1nQTJNZTVYRVJWeTVBNFRsazVZcFpLeHp0OTRxRDFRTGRMakNzL1B4RU5ZUWtPSFFCNVkvanVSc05rR25rQ0pUTDRtYmM1Nk5VUE42ZUhrR003STc3eW9GMndKSnR0UTNmdXZZdmliUHVDbGJuQnBJUGVYNmJHQldSd1JWWHdvWTFwMXU1TEk5cE1kSDNsZUZMeEFzZzlVTWNkdEJjVm15ZTlNT014Q2c5Wk5aRGFaTTQ2dVZFK2d3VU9WZXhuRm9VcG9Ya01JNklJaXpWTUZ0RmRJaDY5Q090MlZ4SnpBejZHcFZvYkowWVFSbkZVTlpOcHRtdnZoVXVuNFVrV0VYWkxzSjdFZFV3WVdGMHpHL3dQWkkzSHppaXhqODZkbTBnV2I2ZGdwaWpLaFozT0xTY0lSNmRvYS9Pbm03b0NQUHlpejE5UzFLNkthSlNGNjBLbTlTcm9aelJkNmh5ZzN3M0xlZUp2V1VBS0Jmd1IxZnZIUlVyZW1laU42b3lSbVo2WGxPZSt0YVQ5cS9JSlNhQnZhRGFMaS91RWFXaUxWNHJGTzJlMi9TSlJpTlM0SWx1ZFdZTVBsd0lpVytWZ082R1Y4TXFacjZKZC9NdHYzdnpYa1VSWFZVakRFdkhoa1JMSGIyYlJFWWU2SWJveWVXcVJ2Ui9mMitsa2k0ZExRR1pWTVhobEc4c3grNTN6N2t6U1ZaUmxtditIalMrNEswZ3JWcnBKOENGTkNGWnVlYVV6RnJoSElKWUZxMVpLeFQwekwxZjAyMmZxTW80azlweEk2ZFk5U1BlcUhYZnd4SEZRL1lKaTYySE9Rd1hlVmE0ejdUUVdFeHVIREhtYlErNEdid2kzaUZwVUpweEVYUmdqSkhwM3J3UmliWDhtaEY3QVFMVmJCZ2ZaVFNkY3FxMlJDSUpZWFZnTEJxRFI1bnJkajliMmt3WlRSaDdtRHdKZklOT1JPTGo1bFpJQVFxajRNdVlQanJJRTRxazdVQmNBbkNXWHRuTEI4bnVaZTNDenpwMG1HYThxOHBuMXhGOHdHcXJNYmxwb3lnM21NMGdRZFo0TlM3d0cyMVNMU3NoK0hGNHpTS3dXTjNHVFZHR0ZEUVZBdVZ1OFpMVmgzZnNNcnBNcy9aMXR6T3VPMUphNm1XNWxpNGxBSjFFT0hqNzZYa1dzSWFvTHFuQk16V093R05tMzdaVHZZRjQzeVYrNm5MOHdtMjNGclNNNjhsQWFSS1RDa1VnUDd4RmVQb3NGYy95dnZ6Nldra2JUU3EycXZ0WCt0MmJ2dGhnQ0JwR0J1NndmUWZCc0Q2QXA1MDRnTDRPcnJLNGM5RDRML09sanQrV2trZ1UrWFdldHZsbmt5eFBnL21UVkZtck1CbUtmUERKTGZuMXRBaG1RMzYzNGNocUNtajJWUDZ3cmdWaWVpRnVUNGhHS2Q4MldKTHFJVk9zMjdLNFJsWk5URnNvdFZseEVrQVhNbnpzcE1LZloraFgvSnBoZy9icE4xemsrNFMzQWVXUkJoanVMR1gydzA2OGdJdDMveVorRmQxYlJaM2M1VjVlS29PdlpZNFlXaGRRaEhoNXhydDk5VWJiM2ZRWm5MUkpuVCtSSGNGSW1mZTA5VlVCSDBQT21BelFYK1NFUzUrVWxxM1BGVVI2WlVJUkM2MFp6enByQlRZQmZNVVNzQWtDK2hPNWhxVllZQ0VMeWpnZUhDbWRIWXh1azhHbmIxT0dGSHBFMWtuVWVWNjlUOUExd1RnZ3h0OWVWOWZVcER5WmlQaVJNcjNERXAxZFhjVDJsVlJRWlFUMEJMUVJPUUkvVnNOaXZqcUlDdytKM21aaW5MYUlieXNvcUpCM1RZTE9FbHlrNWN3emJLQnZqYU1aTUdBYnIvTUdEeEg5N1NjNTZTbnZtUDlHZ1BVRjdMQTVZWXc1SUVVT1I1SmM5ZTdMTURsMjRTcGNwUURXcjhDdGRTbS9KRFJLNk5OcmhVV0FxRzFMQkF0SXlnOVdxRWZOTW55RG9aOWpWR05ZWjFmK3VBMVV2WDA5MU8xMW1ha256YitvNDR1cCtER2JiVFhoczlqczlzT2JwNEc5bE9rWHZ2bEZBS1lzNTF4MTFFbHhVVVNZS2ovek42eXZMY2JXS3JpTW9LMUs1eUVYTVJqN3hJQXYwY1B0eGJSL2RJVnRnRWhEcDY0K1VER2pseWZlYzFpTzhVNUhyaTQyNU5kc2RaaXZkRjhzdDJBMVRJMDFJQ201ZmtFZW1oZmRZemR0QTNqclIreGt0ZGFxbTZ5a2FsVU5oMVQ4U3ZUVURDRWFOUk1uV3FzK3licXlsYVVEcWxrSFJnOFVyNmsyRlJSV05LZWcvVmpyR3dLbzVKQnBNb0Y4cmF4aFU3U1hSSHFaTVRyTFdVR2RoNGdQaVZtQ2xxdGhhaUxKb05tbTdnM25wOThhZm0vTFpMNFFkL3JxL0orbkt3OFM5d20zREN0cERxd2szVlRWajVHdEs0MU5QWFRWVTJ2UHNhelVQdUVlU1M5UkVRbWh5Ny93MEI3d1lwRGNmZkt1QnFIc1I0dlhncUE1RUxTM2drTEIyNnBPY3hweHpEQkZONTVDZHp1WUwrS2tCQUY4NjQ4M0kvaWQ1RWJUYjdWUzlkaHVTRi9MOUY0Nm5GVVNzUlgrcnZMMnBqZXFXc1JrODhuZVVpSmlzL015MloxYnZmRkliOWVUeWVONEU0L1FUNnp5c2xVWUpPYmU0U2lRaWV1ZHpFNVlGa2EvWkJmMysyUnlpQXlYN3UwUVJDdytYNSt5OFZlWGxuZ2lnSUN6ZEE4L3d0KzBTK0kvaUVVTG9wNXlPeitJNnlPQzdjUEVoR0NSZ0s2RHhQbEUrYkl4aEFJcGVCV3RMRTFLZElHaEQ1ckxRQ3diQ0x4UVYrbFozc0hSVkpQcmF5SURQRGVXZzgzUURmaE5uMmgxMkpCL0dWS1NpY2xCd0dqa1J2S2ZvYXRraVZyQzFMN2xEdFZ5WjJFYnhsdGlmTytXV0E1SCt0T3RQeHQ0dll1ajZUVEtZL05pZ3QvTk5kaG9nbmtOcm84SGxaYjJtRmF4MllSNmYyNUxES2svY3pQdldTaXN3em1jV0hjU3A2enJsN0pXTzNwUVE4VEVhUUN0bUpVTWNiN3hjMmU3dVgrNlFUUmVOaUo0a21ISzgxNDdMMUFFNktraUk0aHBaNWVZclJoWjJxeGxqUXIzWW1nNFEvODhOdUEyNVpqM0h5MXFBeWxaSDgzVzcyMUZPbnhUZXhmTWcvdUN0VXozZ0c3ak1Fc0xCZ3N2MG1yK0hCSXVqSVpPalhpaTRkN1BKcDJhYUdsSm10eU1GQlg2dGpoWHA0SEFjWXArUUFrM0o2MEQreTFBNk9ZUXFkMElibTdlL3FZNG5pYUZmeUdrSjNyMmJ5WjRwRzRDK0ZLS3lKTmVhREJ4clJsSlBUSlFhQm1HcGt6MXJUbXQ0cmNvSCtHckoycG14Z1JrKytvOWxvQ2hFTVdTaHc5MXNvNmNRRmw2RHJZbUNjUFVNZml3Um9IblhpaUNmWDduSEZML29vL2FkSFgrNnp4dHBhMFQ5UVVpYktEeUdPRmlyaEFtc0ZDWnNNbXY5blhrRlZlOU0xVFk2NmMybE1CWW9EUFVTaGIxcTU4Q0ZVZ3FDZW10VDJFbmdBS3Q5MHVqUDlYQms4T3ZPbSttYndQSWNyTXVxWEhpUGxKNmNoRWFCTXFUaEJnN3ArS21pQzE0dFVzNUIybmlwVzMxTE1JbTVkTFN0ZVQwdW1RSHczNytuSmxRWnFvSWJUTnZpNVpoSk9HSjVOU3Y4YXBjeVBjTDlML3I0TU5DeFlHQnArUlNOalpnR0pkWWR2OTFNY1VIUTFLRUdBNFdmTEtKN2l4MUhObXJ6VkhINkEvMFFXcTQwSGZjYm5BZm1NWVIrcGxuZ2Vnb0wrOXNkdk54VnRzMEZjditaRkViWmdDM1VXdWV1blVPb2d6NHBPQURjeXZIN3dPK0hrQzErVTRVK2gyekd3T3N6QXl6OXFKb2wxZUx5UEVRVFFFdUo0alIxUWltWEV0ek12Y3RKUzB2ZWtMU0xtTFBpcDJkZTRiOHZrQUhlYUFjaFFSbVVia24rRTFjdGI0cm13cVlkZ3p1RDZHRVZocXY4eExBNXB6UTIzQVJSMlZoUzEvdFBzQ3VXdnVCQ1BWMVJrdngyR0FkN0ZuT3hLcW9VaXBJRC9CcjJtVmdqQStTU1FISjFVQyt5UVFwQzdrRkpoeDFwbW51YU9UN3JBU1NVdGE4MDZFOEl2a3ZjWlMzemFRMDRqRG5nWEg2S1M1MmtObFo3OW9PNElKVEdUbkE5aVZjODQ2VVJtYXN5SytNcWVYVGowZHFsaHhyQTRyREZjTHVtM05HZUZsNTN2am9ZOGxzb0Z6ZC82aTl2S1ZDU3hJSS9QTmhqYWU1NXBHMW15RlNqVXF0enhSMEdIbVBlRGZuRFFON25ERlpUQStPeks1V2dkakd4akVCWjAvTktlTG1CNW1WNklFOU9neGprWSswODRKZVRTYVU0Wkh1akZRRTdNWFI5L1IvY0dIMHozajBNMTVHQWw0YWNSMHpuZmd6VEFtOGNlL3N0eUd3b0dXOGpob1lYMzVndEYybEpjdk44b2Z0eHlsK2pXV2JlSHNxRjBrL1JNU3RhNEl5aWp3MWJFZk0xNVNtQ2ZPTGxsN1pLTVNhTExtTWVXSGpjZzlkcDRPVVd3bDVmclB3ek43OW52TEVsTXgveitYTWRtWVVyeUhRWU94WFBpZTJ0VU9CSjN1SWtFMlNaOCt1R2Y1YXd6c1hjSWdXaHkxWTN5cFlVWUV3OExPcXB0T2ltcE1mb3REeHBYMHNhWFMyVjBFVWc5WEl3RGJ5VnQ1ejZsdDR6MzE5ZHRIbTJybjRrWEhjQXhqZUFaMHE2aEpDbm5nNkIya21KdE5LNlRSampuYnNCK2x6cy9iV0dxV1F4a0RJeTBaYm5zSnd2cFVoSUVlbmpVbmxZUURVZncyUmpGRVBYNEtGSXRZaGRCa0ZSc3JIYnBMcGtFb0k3eC9LbktzcjVqQlpQMk9qQjNBbDh3RE1vUU5XV09OR01jRE5wNFNXNWpQUkxoMnNFUlE3cGZzb05xNm0zaW1ZMHVkVVNuWWdIWGlkdWVpRlM3NGdsVytGNkJMV0hOZFZaZUN1OGJvM0RocDhrYXVwOXJZV1d2WkNyTEV6aUlydmgzOXE4VTFhQjBEUWdNWTBQZlNSUHZsS0I3eFptVzFWTkhpWk5ZWDFxSGFvR0M4ZGtPcGFRQXo1RnkyVVRXQU1Uc20vemxZNlljTEU1eC9WbWI5bGdCMEFleGwxL1hNZm1meXRPL1ZGNjAyc1U2NmEyelNuRUtrK3JObk10ZzhYeVFzaFAreFJKNk8vQWJmNnNZUUVUbWRmODlwRG1ZM0VmdUhUVnBHKzAvMkw4dWhkVzg4bXFIZmovQW9TY1pqa1NRRHQzSnZTMWlIQjBzNGZrNWl0RnIxbDFKQXlEZ1Q1ZHNxQ0x6MFh6MDhBRitSTno4aTJhNVQyZzQ4cWdna3I2bkt3bDhhREZOeEhiQndMRW9RZDgxcFlSRDlqYmJnMk5KVU8vc2ZqdTR1Tk9pc0JJTWdLTHpXeDNHejVDY3JHNjZuN0FLWXErKzF5WmhHQkc0V1d6NER2dHdYaU95QVFkKys0VVgzRS8zVEEyazkweVpsdG1ZdFV6ZjZ4YmRoWGtEeTRZR3VZV0Y1VjV5VlkzNWhmMUlhcmt4OTYvdGZqN0RETytlYlVZYzIyUzFNYjNBWTNDTFdSeURyaUdjQXpIWllPaWxFVkszQit2VXBicnVqUEJCWmhBS1JXWXhTdWFFRklpeG1HVnZ0WE42WWtnRkNCRVRMUnVlL2s0SDk1OEN4aU9NZnd4UTBlV2FIQUx4bUNUa1BwcUlBNzJDK2RZM0gwVkZGVW9FaW02M2c1blJ1bldGdDREMGNka054VWZubzdmbDNlQW5SY0hIQk5NVm52UWFibUFkYldxemptaUlBY2YxRzR2d2RGTUtHUXNQOVhVUFVrbUJBTGxZSmc5SC9IMlNGdVZFMjlDZmVWazA3OG41bXhvMGJkZmFyOG5vRWx2YlA3SzBnSjdGNzNMOUVDQmxCTUZIaG5haUJFd3h6M1lqWUtkODhQYWwzQVRoZVRRVG1aUCtnQmxtNmJGVE5TaHZtY1VwK283SXNmZElWYXYzc1RudjYydmZpWHpqSUJuUkJSanJsZzRsVlVWbTdNdlJDVXFvQmFtUjFtUEVFemdXd2hkVnNhNlFUWDdpek5xL3BUMytkeVpWR21BUEdQbDlJNStwVGZOQUo5eDRicFErNEdDZWpFZERNMTd3WURqQlAwWUxxckpSejltU2x6Y3poQm1wS0xMUE1JenpLQ0tPMFFWQ0JXSUcxRGVpTldhc1RFUTFDYlRVTk1IWUhVZ0Zzc0JFdEh3clVoSjNEaCtqT3V2UTFLV3c0QlJ0ZlY4cmM4SEJMak0zMXRMU3RNYlhNL21TRDNsbmQ3RDhSaFBQbFRQOUtWVXQ2d0piM3VUWkJKckZ0ZFBJd1VRd3hhSlFDUGdqcEpHRXc2RW9sWmY3eW0yRk5NdEZFMnR0UE84RzF0cUdabnpZL0RoZC9GQUNrT1M3N3VUOThlQ0VFMGEvTFN1aTZMd29WdUNtSFlHc3BLTXNEeURwOTZuTnVERU1GbTJXU2hrbEp4dnEweDZVa0VPcTFPZGErRzlJSXJYKzY5VEtaajlkTmxVWHM4OElsS0xCRWlmeTdreU5kRXRxSUpqOHZ1dEJpN0hKNFpNdDdraW1qWHpZeGpMSkd3ay9YVTUvTHd2Tk8wYlNQNmtFSTVXL0JvU2NJSWp2cForZ3k5MTJDUlRVdmJpSTlsc1dpVHoweFAyc05VelV3MjNPc1Y0WEFJVnBQNXRxdnY5Uk1vRWdZUDhKYVNLeDRPa1d3UkpSd3dYbEpTVXRBYWhycmtGelZTTGlPUmh0emMvZEpUVE5pUk8zMjIyLzVKdkFHeS9sbzAwZlBXeVBNOVFyQ2VnbXhwVVRyWFlmWE9LY1FTajM1TzRUajdYaHVQMy9hOHQ3Skg3OXZXK3JyWnJtN01ndGlKQXkyQ2p6cU1XZ3VTalNVVythdDhMRzZPTzBIZ20xL3hmZ0VBOHk0SS9wS0FGSFh0bXlwYkZpVkFIdDlSeW8rZFJ0ZnJjaWc1cUg4OGxzcllBRGJOeW51N3ZwMzQ0dTdITGx6bWROVGdaVkJ5SVhwdFJIS1pmcjVEblhEcFV3NkNOYmpMRXBrc0o5dWs3R2ZtVi9PUmxiRitvUGw2dDdXSjA0ZnZNZHFCUkFqaEdHZmNoeXJZdW9TYUdiTXZacjE1aWFTNGxSdndrekJtcUZ5Sk13REF1WDVzUmZyRWt3MmFNbGlGdUJUYjFhR1lqeHM3aWhHajdZbGgwdngxY2J1ckg0NXBrWFQyaWxDM1JKekovMkhQYTlldWtNQ3dkSjhpckxJbzhlQVRjWXlwVEI5S1dpVGF5VmcveVROcDBia0JOSmhBS3RsWWFSTFlBWGNZbEJoMEFVNHUxMjJua0JSTkUrSU9pTThqR3V0N3doUFpKclF1NmNwYTV0eW5LN3JPVFRJNldzRUJVaHFMOVJ1cHJBbEdhQm1aTmtBRGliUkRhUjZaazR4YzA0b3RNdmYvNnc3cnc2eDRaUDNiVHVSQW1kZm4yN0VlemI3S2Z0SnRlQkNWRi9xcGRHS1BHNGpkYS9CNnJaMlZBZjlPSFR5QXBEb3UzcFM5VWNiZXVkZjFUc1diZFlzaGhsa2pOSWVHekxXK1RxR2hmaEtNSWdmSjNqdDk4VE5UbUFaTDFrZ01tbWx5RWdXUDZ0SFJNOHBXMFd3Q3FMNnErSklFalVZckdJbDFBbVEwL1NjV2NiM0pOVU9nci9JYU9waW5RaHJTMVU3N3pkL3RiWXErYVRsTkZSVFhNNEduQURqbkVLUmhaSEFEZ3FRR3BubTh3NjJjb0o1bENCSmZlQ01mZjJodXh5UGtPN0NRVnZSVmJYWTNoZ0s0QlpxVVV4V0RNMmNYRXdONjhQc1hGSFArNktpb0lieHBudzR0ZWkwMktqb3VpdVVYc2c2T0lNRXlkTzVndlpoSitUV3RhUFpnOXJhUFgvQnBCMEhjVVMxa2E5YUd5MXVSK0Y2Qmc5VkJDS0RUcmJLMFcwenNCWlZkSmVKNUhQYUtzdTI5ZGpkSGtqNkZzTnpsSVM2UUF1Tjk4dTI3UnlEMm1EYkg2dmo0V1VlZ3U0SjJZREkyNWVvK09DNUJ5VHB4dytHeVJvSHFnNnpjTzlsZUNUK2pWeHdnV1lBS3hQSG56OFRUK2FiU1IwaDlETTZWbFJPUllZSWNVSXZuMnRTdkR0c0NNb1Y5a0Z4akl4RmRvdVNHMjVBQ3NLMUxZMElVYldiSlFCWjRUR2lnV2FjUnlEWTk4WHdRSjUxL2tUVDZvRnJzK2RYZ3pXRTRkWjl1NGM4d0pXRm12dFg5NzFnRkNtZGlWQ2ZUUDlYRDREb1ZsR09FTittcXUyV0l0Z1J3emFLUU42Q1ZqelI1RUxPZGhNTkxBaUFKV1NtbTFCTGlUakpqRUNyN0RHaEQ3cFN3Rko4NFlTelZwWnh6ZUxWOXd0bUh3ZDhBUzF2M1Z4bEtIVU1lWm1jQ2s5VWtIMXFQSS94ZS85cW4yd2R3ekZyY0QraWlGRFVHMGhyQkV2bVRYOWxldXhienZ1NHFEb3dCaFcyUG9QZks3L2ZIcUlzSWY4M3Nya1Jqbk1wVWxReHhaZzNXelkyZUFaTWdrRmMrZmZxbXFLVnpES0VGb09rNXNqN240Y3JUOFJaTzJWQnVnTm5DQml6L1JsZndGTTZJcG1lcDk3S1BOalFqSnYveDNpZDhlNkMrL3QvcUwrTzd5algyQUVjRlF1MVZpRmY3cXltWkh4TGJNdTJTSDFIZ0VtNm9jaE1NSWZERWdBcEd1OVVNUm1EdWxINCs4TXZwblpTS1VBc3Z3OWhHKytuSEVMMCs4RjRjZ3NnL1RqbkxJdXVWRnhxOGJVek42eDNvQitmUzRnd2hva21uM2lJdEFhdFBiRlU2dTBBQjhNVmV0SjV0QkxaaHRQL0szemlnWlJEY0ZsTXNVVFIxUlFZZExhQXBvazl4VmtZTUorQ05Kak5IMW1wdktUT2s3eXNjbTJxcmIvUlJ1bkx2OHl6TGRWRFRQbTY5ZEdJZmlWSFZFSWZGaTJLMGJkRnZkeTF6R3BsN1VNM0Y3a2RoY0tZUFNjRVZXSG95Wk54c2N4V1cydHB2am0vekgwTmhtYS9MZFRSY1hMVWpTTk5DWFRaNnNSbldwZjVaKzBibi9aeEVaS1hhUzEyeithb0d4anI2N3J1SmpjWXkrZ3E3Nk1PUHBVM2xlWXB6Rmo1b0ZCWXFaSjErZjJPbGNDS3JyZjJja0duZGF2dml0QnZFTmQ5YllqN2NuUmRlOVpiSTNWWk5oN2RVOTMxVWlDYUtTVmd4dndrcWpoMUtqSnBDYkJWc2tMSU5qdEVNQ3ZubkRVNmlsdjlleC9TRTFTOERHOWl4azNPbVJXaXhmUUpVdDRteDRhdjlpYlVUMGFnZklPNUZ2ZTNhNXVMNU9helZsc3NGZUdXRWJmdTVLSEVLRXpJQ3M2Nk4yRzBHU29pSStIV0NQeEFUcWZ0VVd2aG1hM1VMVHAzMDI4SmsrbFdqYXpXbEJOUmJXSTJmVU9sY3g1eFg2M2JxaEpZd3J4ZjZKN2lhQlo5L3E5WXBINEl1VVgybXExeE10dy9lN251Z3QyZ1JCd2dlR0JHSjVDWHRSTDRlcDNFTkw5MnpvRWZuVC9NVnRuZUtyMGRza3JnbXNuQThLZEUvMUJVNlpOeVVhdUJJMUs1QlhTUjh4L1BXWWRhMWFBSGdwMXJveDF2OE5wSkRldlBKVXJHRGtqQmt6STVpaHJPVUg3Uk1YeG1qbGR5SW5YcmhDS284TXdwdHE0NFhUVnhzdC9aRHVWYlc1eCtFdVdLeVA5aFhkRmNZVkk1eHZuUk1OeG5UTG12Y3hpNkxaVTM1VC9Rd3pmaVZWd0tJOVVreUswZWpHYVkzcjFiR2ZiNG8xUW1XMUp4Wk4xL29VeXh6SkRDQUJxVy9maFVxMG1XNWIyc2JJZFBQVy94K0hRTVNzN3FLYlZkc3FZYUJkRjVFTzUvUXYrR2VTZ1J5ZGlLOVZwdStGV3VCaDcrNnZqdWFMQnozdUhFSDlwOEpKZkVMYVAzNWRXdHNlTVBCejk2RW5hM2lQcXQvaDdpQnJsY1BmZDV2bHZIYVErYXVNdmcxYmhrd0R2YU9nZEcxdGxpMUFBTmM4cUZGTFVpdXpTeDZrc2pvUkdqWkhiQkN4b1RoK2l3RHRsNkt5QzFDMFU3Vm11cnJicHdMT0JrbnhUQmFiVWdQRUwvZkRNbmZYMDBDekxobUtjMjEwR2p1NUthaVdKcnZuSnBpbk44VUlZUmVkN00vZW5Pb3U0Q1Uvb2J4a0llOStWWHM5SjI0bCs5NGQ0VGJNUzFaNmY3NlFBZ1hKL2QzQkIrR1BMSThZeXh5enVGTEdYbGowWGRqZkd4dzdQa2NWaHlzenVwR01TMkkzaGRmMGpCTTAzMzFkYTJTSGtZeUZsTFE2N2VqU0F6N3NCMXhHQ21RZ0tod1lhSXEvckZiUThKWm9nR2tIZHN3bTR0dTM3UEFtT2ovRjI0WDZTalZBK0ZDM3BLa05Tczc0cG4xNUMyWFh3MHBmNThrSVNrOHR2azhIMytiYmtoNXZyWnE2NWlPUG8yS3lKYmg2VGNucjR2MFBrY2pqYzUxeklaTTFOVmRHd3oyUlc5M3VKdXc0M1RDMlhxOXJCTU1NdUhVSlVFNENFNHFjWXdlcTJUWXdCU1ZOVHNzMnhOdHlHTVAyVXpGcHZVY0xQSUM3ZCt3OGR1MDFPcTVKNjEzem9nSEFWSlB5Y3ZPR0lqYnNraUh4RDJoL2FBNUhoQis0M3lFM3pZbm16SDY5ZEJldi9FLzVMRUQ4dWVCcFpsVmVtZmo2Q0NWZU1qT1lpUUVyVE1DSkZqRzdYSXZTaHRhdDV5czVFV0wzTjlsQlFyOTlKd3B0R0JLUzc0Q3Zubng5ODFrMHBDY2ZkdHNYL05xek11SThZZ0QwcmIzanhwa3JsRU5OTEc0bGNNeDdYSGVzSm4vTURhQXA2MG5jd0V3Tlp1a1AraUlJRjJxVjZXaHo5THA0RkE3RkkxOVhPZzk2Qjh5WTFGZFo5OVRRcndTUUF6V2RRY3dzcTE4ZzRhWVBJaVludEdyVWR0Y3ZZL3QzVzdPQnRNUUVYaWVnU0pFQVJsN0FZUnpPSTBrSEJrUzZLOGdyOVNhVHA0Y0ZOZllGM24vTEVYWVhUYno5QkZjTlFBMUdFTHdmQkQvUDFCOHVoWHNpVDNDellBN0xwNndwTFRtRHpXVmMycHZrdHJPUlZLTE4zYnlMYkR4MmlzVXQ1a3VoaDhwWFhDK2I3OWZkZitBcEI2VHZ4ZC9JRWw0TVY5WnRNSCtPNjNHTVpPLzg0eVY3WERDS3ZLdjZCZkRjRmZZQ2pFWS9mRlR4ems2a3JrazYwVXVZam5IZmNnK2Q1Z3VjeHVpY0NNODhyOVpOa0Y3NTU5TG5SejBzZ2NCRVVtcnh2ZHliaHRYbHJMb1VSN29pa2hPT0NoZHVHOUxaeG1rU01TR2paa2h2MXh6Q3c0ZVRmWXF1cWgzRUl4ZlF3VmZSN0JrK09WM3pLMVYvSXgwZW5SVUtJYVBHZkpPNWVKRUpxM1VsZ0kvUi9QdVFoaCs0MzlOcmM5bHZrWDJuWWtFRytleTdTTTdhNFA0MjJhVkdyVWo5Yk9YVWkvUG5QMitxNGgyL2ZYR1c2M3MraVk2a3BzTjJWVXZKbjVKU1g1OU9FdHFmNlNpd29nLzhTbjVHd2hWTllLQWM1dEJneStSQkcrREt4TExZcHIyV0RFQnl5NG1DTjlZSWpYM3JEV3JXWDZFSU1NS1FFS09wdUpneDc5K2d0cjhkZjRtV3ArTFFtRVVXeUQ2aTdHNVJtT001Q3ZJdFcwT2h0K0JGQzBhVjhDR3ZCVjgwczd6MDF6cTlDMlordU1VeGlub2JDSnVpSUNQYVdtbWhqdGs2QUNlNVBkL3BxYloxVmRGU0hVNXFPV3QxSnJESFBvRjZTcG9leXdUTUg4UmQxSGxNamFLV1FwZkJnS0pBeFJ2TlpUanFTckMrbll4azVBME1FdXppVDROMTZFQ0Q2Qjdqd3FkQ3M2SkNva25YZVA1dzBUOXg2dml3a0JuK0lsQXUwNGg2dnl6NmhxOFNsWHNHWUdXT1o4aUtnOGh4Y2hzWmJFWUtHbDZZUlNWT1NFVE40TGg2Nm43Zmhka25pNkFmcmx4ZFVzMUIzOU9HMElDZ3BZdjljRUxGQkpFUVlzd0NxVzZ1TytNNGdrWk56SkJSdmltT1hQNnBNbVdCVTErMTJKc0cvdmUxOExxRjkvVmlrTCsrSzhnWlQ0U0gyQnZQL2JWc2ZCVzkrKzEyeERnNHhZdXJTYmlXQW1GMGVwTWZpZm1qNjNORW5tNE5CMVdzMTVXRWtyRjdOVCtTc2JZaWtkWGJKeXhmbmxRL1g4cnJKYlA5MEIxZlI1bVY1ZmdzMDNIN1BYZW9pQ0FuSXhkTFNxSU9TUUlZTW1zS3ZDazJ2eTBNSVVncXZvZDh2bHQ3M2I5TlMwcjdNT0FaS0p1MmpBNjdqQlZKVGljYlJGZTlVOHVKU05jaENKSU1MMm10SzgyM1RwSEh6MmdWQlJBWWRkN1oyNE9vWGdaWXRmcHF3ZG9TcVIwMWFYYlltMnY0cllCRmx1L2lEczg4cDBkdllUMHUyRWI2ZmhYTm1FL0xCcmtKcllFVFFTSTR4MlIyWXlRUWVzNWU0L2x4b0FFbEFpQmpETjFNU0NUeEtkVWg3b3lCaWdWZG1jdmtNTG1xa3B2NEFQZ3Y2TWZYWkR4dEZlWE9Bb1JhZ1NsR1dubnhPWDlqWUI2RE9LK0J2U0pSL3p0MTkyLzFmc0k4akY4dVhaTC9kOFVVam0zVkpnMzNlVDZ3dENTUE15Nm9LbWY2T0NuR2VmYityRzZqZ25ickIxVk5EbWNsMFF4R2xwVVBoTXZlMU4zY242NDVpMGUrRUtma3Y1blI0Vk11L0tYV3luSVVaS05GWDUzR2VyWmtsRXlURStLY3ZWdXpnVFhnNUNTdXY5UFBONFRTRXhNYWRaaU40alVTNTFsUlFxM25lcm52UzkzYnJ2YmtUTjliOUdEeFpSYVd6VUlFR2E3RkU1ZUM0U1F2c0lxMjcwTGQxY0oxT2VBa1I2UGZURWpqM0JPL0dzei91WXowdGF4YXdzbjlCcVROaHNLZDlZOU1JMHM1RVFmdDVESzlaY1N3TkNGYnlzaEs0UVRvN1hPTk9CeHhDem9DVFVEakRjNS9JSklDaVN4enVtQXdkOXBxeHVsRnJpL0tHZm9DT3lUb3NVT3YrclVPNXIvOU95aUd3Q2h5OENpMzVsNVNBS3UrS1h4ZXVQR0F5b0lrQ2tJVGpDZ292K2F5cS8yL3JZK3N4M0RzdGs1cXMwVGRHY2FUaWZOQWZEWUMxTnpkaGluekpLYW03RG51V28vYS82d0RXODhEM2V5NjV5N1BBaWQxLy9wbkJtaUZ5MFR2WCtTTUlsV01vTUVTUUV1ZmZtRGswNGFKWGJZYmNsWW9BZzFlNStUYXNEc2pzYS9EQTZaZlNyWExWQTc5NkIrOXF0aGVlMi9xbmdDQ3BYTS9yMC9KK3E1WFMvR3QwRmVEbzk3V1VsazE2YWFuK0gzQnY0WFY2RHk5cG9JLzFMT3hNUmFOVktNbzdNZ3p3UzgzVjlkT216M3I3N0JhZzVDOUR1bk8wUUd0dmlxNVE5WGc4amZ4TlJWYkl4RFRCNTcyT1dlRGErT1VTeFBoTTQ2ODJ3azFoaEM0TzRsODI4aUIxbmNWUzJaeHlVRXdYK2w0YnI3ZmtWVERoTVVrTVFubVVJdm9QQ3FRSU5xaVJPV3I4TEZsdUlLMjVaODBXeVVXaVRKblFTZHFNOFRIMCtHb2JoM2lLaDlMVXpJR3hWcmRQb1RkY09UVUJPSXpvS0dWRnptVHVsTjJ2aEw0WDY4YUZjSlFheU5jR0ZQaDY2WW5ncjRSNDRmd20yRUlVYzBzUTVjb05VaHBibWN0Sy9Vb2ZFWXdxL2lvMFhUdnkwSmd0dzYwYTJiUWRjZlVMUjhqZ1p2bWlmZEpkYTZYamhndFJ4aDFzUVM2SGwrNVRFWkt3M2NHV2luWHgyUG5rV2VhUHErSWRickR6dElzVmxQUlZSd2p6Q0taZ2J4cGdjY3pRbnFSTWlnc3ZoRlFtVW52ZG9mT1M3UThubzd6SFExV1RDNjVDQkFEUVE5VW41aTlkVUtwNjF6Qlc4OThuSUZPbG9MMXZ5UWxzU1NOTVpKM3Z3dUdmYU5jd2Z2cmI2ZTF5N3k0alNFSWhOajRTK2N2eXlvMTYya0JhY3FiTWVOQ1RydllxNWhwbml6eXkvR0tORGZHL1VBZWpHNVcvcVhMeDA5eDJsZE9HL3p0NllrVGtNay9iSGFXUmVIdTJzUTlCMm1ZbXhxeGhQOVZXK1NLZXIwU3BHSDFVazZQenQ2SXV4SGJTcWxra3A3ckI4dHduM21SWnkrc3lMLzhtcWpRWW5lMU5xN0ZrL251TStLRmhscDdKNk1nT2ticWVrQXl3YkxoK1F3bkNIa3REMWtIdUxRMXBMcDd5Qm50ZnBTU2Q4S1BPK0E3aUJhVWszV0JzV2UydmlCOHJ3NzlvdjdBTHkyR1YraUNWVE1SeGs2WklSTk1aTzBVZmx2SkdMdmhWcHViUGNXY29PTWF2bmxFM2s3SlNsYVpOMEZCak1Yb0xQaGtpNXF0ZU9xamxuWEFmOEt1VXhpMysvYnlkQ1JSNzIrZDUwQjFPYjd6eGZvcmx6aHdJVWsySjg3VlFPM2ExRk96YjJMSEFCR2JQb0ZaNTdzZS9CRTR1UmhGYUhMc0Y4Uy85L1ZNRUN3eGF1REdxR2NFcXBtcEx3NUdDbjE0bUUrUkRQRUk2V0lyYlNMcHcycmxRODFqeHJicVp0TXVoZk43VEhiZHAzSmt2dGplNTl6MXFKcFlHeG5CcDJiZGgweGwxc0FKUWt6RVRJRGh2TEsxN2UvNU9ySVVUV2htN3pYV3lHKzNGM2pYWkN2TElQWm5pamtQYkI0aXkzNVpuUHpvUHZ3aU1WcUMzWkFxcjNoWkYrMFl4TVJ0RDVzOXRqWXlvck40ZCt0Ylh0RGNvNnlKUEo4eTk2VVBmUk1wblpYTmwyVW11cHBiVU82cHhmRk55UnhhQ05VSGJFbVZUZ20vZkhLdlNMTW85cnhVdUYwTHJoU3pqbnAvQmVWS2FvZlpjNHB4MVJvamxYUDRyU0lBWURFQWZmalFZMkxGVkpwRzZHQkhrOUpzZUhZbXpOMTJWcFVEdEN3ZHZGVnRnUkxRWVNRZmdRV1hzWUtwNG5sMG1URTFTSHhvTzA4YTV2dGxlNXc1bjNzdTVtbGs0cU9WcU9TTk9NSGZETTh0dnVoREZETHMrOVcrZ1B3dGZXSmpCUWxwL3ZBblFDazBRcTBPLy9mVXZBY29YNUpDLzJFTS80VFZweTRLSUMzN2VIdDkvbkZVKzdpbExGdm5mQjYzNE94M3k5dnRmd0lPSERIRlFHSnpodUJwcVAwVENEZW42ZEo2MmFVSUJ4enhzOUtlckU1SWUxVVFQUEl4OThMMTFNRERoK3VNYzJhWWlvSjdEOGFiQzFSOE9HVTVYREhmd0QxNVlZamM3OE9Wc3IyVlZLUTlNTlRlVDZacGFsb3hXc3MvVHh0RnE1WDNhbkZ5dlRaa0NWL3RqbE1LdkYrQW9Lc21NWHV2aWY2MkFZbGxBUUpNbGdLMnVjd0d2SWNMUFBWcmxtaExtY1pOSmVQRmhHb0RGRGQ1Y0wwczhxall5dDFKZE1PRyt4dS8ybmdoaGlNWjlyUnpPN2trSyswWUdLQ0ZDRFkwNHVKN29lOWxvSE9RajAyNUk5N2dpQjMvTld1WElyRk81S09SMndsbWFIalpBREo4Zk9mdERGRUtiSmFPZFlyQ1Z4eCt3TjBmUU9qYTAzOEVzV2dIOW96d1VTUHRKZ1MvQXVNVmxvOGpCYmJaaFk0aUtScmxPL0Q1QzRoY0hMdk9CUExNK0tEQUxsUGQ3akZ3aDEyWWhBUi9OMWFVYXhTZkVOYUIzeXVWbG5BN1ZvbUpTZVBaUWtHS2syMW56TEFIdDFjTDFZQURFVG5hZm95ZllnL1YzOEhGWG10bFBKSFpjZ0Rkdi9tZ2pzM0l6N2w5cHBSQU10RUtXK3lsQmpSZmExMDV6U2d2aHZPVUdYOVozakxOcnF1WFhRTERGWG1CU2VUVCt6QVdRL2FsRVBMTldPU3ZWTDJPOE9FRm0ydTdxTGpRZ2ZLZE5ua1F1ejF3SVBxdit5MkRHVFB5eFpxOTlodUNaSGUzKzUySUYwVzJrU1E1bGtpZSs5MnlvandiVjBUVmZKczNIL1cyQ05OczJvaG81REVlUy93eFhiaTZiajBMYVVNa0w1SW4wZVFSUWkvVXkwYmE4WURqOW94SjI0N2RIOG9rN0ZqMHZvSE0ydVUxeHVoR1MyaHd2K21ycnJYNHJPa1VhcmFoWU5teHlleHNGc0xCenNPUktEM0phOElIdkxlTk1pdXl5SkVOUHVBSlVQWDZDeEIxQ0V6Vjl3Q2hwUVBINFpZaWZDRmhMRjFJUXQ4NlpQeDYyZkdpMUVkMHI3dDBuZnNWeUMxKzJ3bGVtUE0vYUlrNGdoamx0VEtPRkhmVkkybmNiU2RBUWVDMVRPOXZxNnBUUDZvSjR6WEJkS2picTRzczkzbzVRaXNQTDBaeTVDVnN4b3lNaUIzV0lRb29UUk1xckZRSTFoaXlIb3RzMkZ6eXk4NFk3V3dZTFg5ek1JVHJZbjdLTWpwQWk5QkNGYThkZFZhOE1WQnNtOEcxOVlBZmtlQ0o1RC9OUkNFOEFsRVhITWtNZWk5TVdEVTdZSWp0UnQ0Q0ZTYy9hTnhjRmpsYmh2K1pRaEZ0S2p6ZG9LK3ZDRjgybEM2NnNoeHBFK3R1VkoxWkhtUDUwN1pta01sQ0NhaUlQcU1DcWNJWlVXUnhnVkxxVi9CUktLdEc2Mnk0Y2tqdnBNNkRIWWF5eS9RNGJUbUpFOGFpQ0RCa3NHaXJ4QmJRSjJQZXRYR1ZTWTBaSnozWW5GWmhiYndDaW01eVMvdXJGcnFEYm51d0pDRWF1Yk13Zk8vWWc1VDNYYythN3d3S2RuZURjTEc3UWFTK0hCWlZ5ZExUN0pVY2JQUnF1ZnBQR2xKTER2czUrY1lzVVBDVDdBWUlaa21NTXNyYVB1aVFoMVBJVHlJd0JDNTVXQU5ZREI3anRxa2huWFVTWDUvc3Q4UU1pK3NJZmxEZkVtYjFpQWhXZ1RlNE8xckFQZ01CNFJtR1VtR3plVWhRVlVxZEl3RWRCdVUvekVSbWEyWk9KVmhQNWFjd3RvMUtwY0VweWdyOTFOeTgrOXhtUytHSElmWHEzWnBFQUwvdWtkUnYrS2YxSWdNcXdETzhpdXRHSnNvRXFNSHFOUXJHNThkeGlyRmRXN0h4dWcrL1BNVkpRb3NVbmh3YWdYVVJKZC84aTRFYk1xcDluWFBoZGJyZ1NOeUdycVErKzlpbXBzMXUvTGdqcDRSUVJlRXZacWFGWnczLzJJVkxuSW11V2x3azhWci9RKzNaaTdrbnJrMnZNaXg2Tk9CNjR1Y215QzJTTU51b0NmZmdUWXpTaGovRWNBVW1BTVRmaGxNS1phOVdMeC90MmNNWGdROTA2dlNKcGFqb2tqSGc0cE4veU8ramhlZHh1WGZ6eS91bHdaMHdOcm1vcGt3SGlHUWFyZTJ5MmhCTkNpQjZVYmR5dlNOaUZLcXkybXZmZk5OSEFLTmcwd3ZaOUhMbGZZd1p3TUJQWlUycHUvM050cE9pbUYvaVg5cEQ3MUxSTlV1ZFVCY2c1ZDFTRnpHdUJWMVNsZVozRHc5UE01WngwUUdIak5xd0VTdTVBVm9jTzVYaitsRWxBQUtaajhaSEdGZ3VSSXhMVkhzZXlweVpRMC85NFAvRGtvTHloWGhkb2puOHYrMTBMNkNwQVViRkpvd1N6ZGRCWVRQZ1YvT1VyTWQxT0VHVmxIdlV2d0RGU2M1Uk9iSmlvTU01dkhEYWlhbDdaUlZTT3c2R1cwb2ZiYjJTTUNFZEkyOHRYM0lNUnZveGp4UVhKRndzcGRTL291STdxakdUZjVnVGtaTHVNaTJRSXYzZHdXSXlwQ05LMEFPS0p2WXNZYkVYaDl6Tm04aEhLWTlqTUR5VzNFclRONFhiRGFpKzYxbFZTdmNDM3EwYnM2NXF1NXE3TXJuU0toaEg2VUdOVW1yTitJTWtwYlltZjlGWWpVcytKSjM4Q2t0dnJiT1BPbjRpMUIvU1lvcElsdk8wMjRSU0NkTFdlakpZMm1NZTFBbGlNNFlvcGphY3FyOUZhb3hQL0tzT205N01ScUdaSGJYenlSZEhXS3Y5ZWgwK2ZDcS9UcDAyR3FwS2Y5MklFK25UQjhFYVA4anFRdlVSWjdDcWI4a2Vtd2VIcnI5WEhxWEJ3ODVITXl5K2dxd1VocXUyUzNJOHVMM1l3cVpneXRxc1F1YVlvOURPeXhaK29XLzZXQmlwVHM3eHhpTnExL0NIYzVGOXFBOXp5QWtNSWl0bGdUelg1dVBjZ01PWlpiZ01pa0w0Nm83aEVTWEIvMkdkZmROV3hWUzF2MXQxU2gyWUlVSTJPOHBaakJIVDAxclBrTDdTeE5UYUxBcTVzdXhtV0xlc3JOZXBtb1crcWUvUmJHU1BOQmtobG5CcExpTHN5emJLVER5UkZRUEtLM0xIN0RZVmJhZlJQRXFqL1ExakFYMG1TQk95cEFhUXZ2VXNyYjNoWXNUaFhEQWtKQnJ6dHBmM2tTaVAyeXVFVVB1dDNybkc1M3pUTGR4SW9IeC93ZFZlMlNpOE04VjZUcGl6WG0rNmVRUHdBcjFvbitGZkVuZ0hkVldFMUJybEhqZ0dFeUlsaVFGSlB4YUVSTzkyMzd0eEc3WDZJNWZsVUlubnR6WWRxenJ4bEhrZ0ZMeWQ2QWh3amp0bGd0TEJGKzNlM1V3YlRDNjlvMExvU0RTdWhsK0VWQ213M2ZlZnBqckFrcU1RNTc2bFRvMWZ2WWlnUUdjVDUzd3NueFVNck5VWDA1eGtONG5FSU81TFB2TDdWS0k5NzBiRFlRQVZjMDdNUTdQc0ZqS2VmKzVkR2toUGdsWnd3MGVHalQyYnU5YTluY2x0TDIrZkRlNUhpWTNiTHhuUTNmS1d2UDhFUzdiS2pEOXJuNTZsRUFGbG5VTHNPQndlOEtSZndLb2Q5UFhKYW5FOEtMM2V4RXFYNFU2WlZQVi9URzY5ck9hUEVBV1NxKzlJcnNLdXJEenlqZ01LTmI3L0VGV2o3N3RVK1dkTkFJQnJQSHYxNW9LWHphNTk5M0VBUmtEcnZCcTUzdnV1Rk9HelhjS2lEb0tLK3JRMHVZNFBza0ZkTzJBMUtFZGg1RUFrU0duaUYraDNjMkVTRElDbDZUNGZ4ckROM3JWVnBjUVFsb1NraDBES2t5QjRNODVXWVFPelp1ZzZYOUUwN2ZidkwwZVJBcHNoc1Q3U0FtSm5XU1FtZHZ5a3kreEdzNXU2eFQrQWwza2tSbS94VWtCQVNJZmh2Rkk2VmxwRkw2YzA0ZlByYzdpOG9FS3VxVlF0Y2h3U1B6SmFzRnBWWXY0L2l6Z2JFQjNBV3VmMmlGcGMrQng1Uk9takt2Q3UyblZkdUJPMmR3QmlXWTBVMXd1T05PQVNaaTZ3OW1INGVGL2NRVlBGeDlIM21meUJQdnZhd2xuenorSHpHbXU1eTNwU01MdDU0M1FYbnFhQkFoeHJUUExXcFgwUGwvSEU2czQyTzVNdFluYVlOd1pFTEJCREkzSUZ6cEwzaUNORldlWGcyMlgrSnFnSTdMcWVlQThvSWpBMjJ4cnBJQkhJZnlSMjRLTnNVNjhtU0hYQUFZS0xvb2pQYnFtMHk5VjBsN3l0Z3QvQUVMdDRjYmk0a2Y0d25pY1dscDVVN1NmSkdYdGVXRDJKWVVRNEdzVXdtbFVWTEVhN2tnendrcnJBSjA0M0dJOWQ0RmFwUTdZb1grNDA3RWlYaDh2RUdhWHZjUk9vbjI5ZXZPanZITHNjY1A0bDdvdkppRVRRYnhhNHg4SUZ2YTMxV3FNelZYa3VuVHl6MUpiTHZDcE1Zd3N4VHlXemQ2bnFkbjJ1Y000aitRYzBtQ0FUZTVuL2ZFM3ZNVko4emdvdEUrclZzeW9nQlJWOGRWTisvU2s5ZCswYWcvWGxOcW5Fd3lWa1h0NHRSRExqa0psZmVmajBzWnBpMnVENHFoeVRxOUVXQ1JrTytYeW9lQ1RBZXZ5MkxuR2EzcW9sN1dHZC9yVXVDbDByeFVSK25jeUp5S2VVc3VZY0p1MmJkYzQwV05XNG91M0VwSitBOXg0U3NQMWk2WWpUY2NYRFhQL0I0OTdyaXJaY0pQVzlvV1pLald3aC9QUERTenI1ajNCSGx6TUFnRVhxbTcyTE9sbjR6VStBY3RCTDh6bUhhcnVCbWhiTWFSWHdlSTR6dWFBYXpENmFUclNJeGl4RmdZYUVaU2VZNG9NT3RCejMzRFprZ1pGZnl3OVZFUGFTdzRtNWs0SEw3Tzg4RXNtSWRGdW1ZMVJCWUNsZk9jOTRwTHY0VjFSSjVZTTNaVmZXUE5qYUVFL2x4ODZQSlBCU0JCQytKMGdZNmFseFZYOXpRWkRnZWd0TG13T0IwcUJhNkl1L3V4TisvRW9FTFFTL1Z6OVBqU1dDbTJ1WENqYzg5SWI0eHRDcjlBcG5wZ2lzVlZCRS96K0Z0N2JuZjRFNno3b0xuRXRWWW9Qa25YclUvb1FBM21HOTFwZW5ranA0clZmMlpGRWp1WDIyN2U3VEh3TzFNL1Rhc2pvQml5YkdZUkJ4Ym5IdXZxZ3NoVTJwTDA2b3ZZYVlicVc2OWYxMUU3Vi9kcVQ4QmQrcExpaytId3VNK3NmYldvSGxMUTlCMlNMR0pNUUNLSjJPYjVTcENEN3RWMjRNUTFBc0tlQ29DZGQ3c2xCRENTajF0bGp2Sk92eXAxUGV2bk5DcFpoUWxtQWVXT3B1a1l6bnVQbVpPOWgwS3dMd3p6QVJSL0FWRzJNaXRRQlFIR1N4bFFCVGlKdHkrSTZXL2pYY25jNHZaeVJ0SElpWTJmTElseXJDRldCOFlha2xjNm8vNDFYZkdQb0tJRDJIOEdSaXdVMEpxY0JXYUthanE3bGNwUFl6c2pqVUhLZnVDbkI0NlZURnZLdUd6OXRNTmxOekFJckdTNnFDNGJLUkF4LzZPMHNDYkJsS1BtbDZiZnhVZHZ3TEVsRlV4c0hYV1ppQ1pEKzgxOGtPb04wbDZkSjB1ZTJTQzNPRyt4K3ZuVXJ5RzZVMTRlWmdlaE84Y1dadk1FMThXREs2YjdDVSt0QnBzYUZ6VGcrSUpFWFYxVVZHMmhMUjVYWFFzMXlZVmZDdUpPVmpZYUsxb1dNcWlJNmhDZTkxVFNaaE9Od1ZSbVR1WmpLZ1RpUWVZSEYwRkNLcnQrUmMxM09BVU5DelhvSlpkOGVWaG1FdVA1TWYyUm9oVUIreXlaaCttWXRrSkh3MTBoR3gvNi9Dc3dTTEk1MUNtSE14bnhicU5XdjhGdzVadUNOc2MwY1ZmTUxMYmR3Y09VSE1kR1cxZGk0YTk5b3M4ZmZsV1cwcUYvVWhtcElob0NBNDU4VGxVWnBWWU1Mb1VPcmNqWmhYOVIwMFlUWk11eEFFbThReEJPVk9iZkNaVVNtN3RnQ1pYbVhkb2ROWXB3TnFVZHd1Lys1T3dETmZqNzN0bXJXcnVQZHBvVjVkN21Od2s0RkRzM3RidHZpOTYrSnhSTW1xTXFJQ0kveUczR0pnMEd0KzMwM1NadndManpXa0JpcVFrSVdTYmhQYmhhcUVSOHd1V3FyWGI3Umo4RWliVTVkbjArZFpHSDBhak55bkltU0FhTnlReDF2a0JZMW9Id3k3WkY5RGd2clphdFJtOXRBSXNrSG9EMkN1em1odllpVEpyQlczL0RPUkJYeWpkaXQ1THZObWxuU2VZRTU2VjIwQ3VkVXhRaDFpbkg2TWZsbGdwbHlKZDJYb0dFakp3cy8waHpnWERmZ21YQ3AyUU5MdlhNdDEzM1lOL0JVTlovY0pYNUZUdjNRRytld2Q4cmVhQjZOc3puZDNzVEhlL0d0d1YzaVRFcjZRc3o2N3FPMnl3V3ZrOHNDL3Baa0FyZkZBZGRDaUFaay9mTTlEYkYzV2hRd0NQQVV5R1NuMVhxWHZ4Q1lndEp1VlNFOG9kSlJjZE92UXlXTkFtWGE3NXpkVHJFcXp6dUZpVXpQdGZXRTlZMGtmMUdGcXlydXFpYm9OTHl0TjJ0Uld4QXV4clU4aURuQThNMmNJYVRsekdvWUlBalpZN1lVcHlSZHk5Y3pZM1BPaWVmbm9OdDlXMkg1L3BTMUs2K3RVQzZyUmYyby9vNjZmdmRZbHp0a2JsbEFyVUtxRmNNOUpEQjQza1ErL2xvS3pINDVTU3g3aHptcDZ6NjAyUjF0d0YvMWN4Z04vNDFXeXp3aDVYenVFWlJ4UEtlaHN6L2NuZW85Ym52RU5UcXRXUnlRbWlKa2RaVGgxTHJ3OEZkSnpKZHB5R1NleEpLa0EwSVJJL3lQOFFVWndqQXp2ODRGNnUyaGtrS251NGcxbktmam5vUkVsR3VRR2pDU05DdXVENVQvSTNtWC9SNXF1T3M2RHJUam9PUnBFWkN3NzRIK0JnclQ1Mlp2aWNaeXRreldsdVFGQ2c1aGdBcEVzRFJLeitna3F5TTR3ZXlVamJrSkdoTnNLb2ZBcmhoaWIxb0lTUklBU0N1QmFTRDFPN0VEWFZrTFNxc2VjdzRuYkVzRG5ZdGZEYTZDbzBOTlJxNk9xQktMVEU5S2RFdWhUbU5tMWp3WDFLVU5sanRVMW5vM09vd3dMMXRpNGkvR2pHK0xQUjc4b0g3VjRqeFpJK3cyd1oxelVQNjFER2g2QkJKMS9FT21QcXVoaFNDY1lXY0dBTjZSWHZGVzg0UzNqS0pjVHFoT3lwV0Q5c0dBZjhNK0NZVUllVkxBQnpwblczeXhPNXpQOWxsSk1DanBlTS9SK1RRei93MmNJY0R1bHIveG4rMHNKQnJraG8wZm9Xam9CdVhwYll3NFVNT2l3dTlZRkYyd0lPVlVGdHY5ZlE4M1pYV0gyNmVsNnBaaElsNDc3T2VFbWsza1U1NGpMQ0QxZG81dDduYjZEOFVFc21Sd3pNc3NicHIzcjE2TFNjY0RNTzJ4TFB3THlWaXg1bHNpclBobnBvSldFeUpkUEJNTzdpSU1wRjJFa3A1QXNGVjJwMjJ6TGk5bU9jeVdMSG90YzAvdHEwdFlRS3dKVHQzUy9qblkwd3F4c1hab1NFT1R0Qnd2ODQvM1RDMXdIaElKSmlnV0RrRUNQOHdzOXlqcXp2eVlRSU82R2o0QXZDdmU4VjdSbDBVOG5FZm1uRUNRc3VjSmpyQzNURDhpektJUGdPZkZ5bHdWdllWWU8wcTBoeGJ2eEl2OWx1YnVraWo1Y294RitsOXFjNlkxbk5WcUpQL0NQblpmeEtaZEc0MURUT2s1d2hOVHl3KzdmZzhZV0VXUFcvUlBWdXRZdUNDNGVoYzJlNmVwM3VVbnU3Yi9tY1VuMk1uK2JmZ0lVTmVIdEJ2ZDZNQXBPT00xak1tUWs5U2N1eDh5d2sycldPR1NtazBNZ0creDBvUjRPTEwvNzc2SjAxRWxGeU5WZWQvdkZqazVvM3REbUU2cS9zWFZGVDJmaWNXUUZxWjAyQjBpenlzZHgySVBRRFBtUjMyb2JOeUdxRWFhQzJjd3BwSm90QmRoLzE2VTBVd1pzWlpLWnJjSzhmTTJZN3lFbDM3TzEzcUsyc0lzak1WdFpiOW5lZE1leW9wSlBHVlB1QkFwSWN0MDZpYnhDeml5MFA2QURwYVdPVFVhaE53QU1KWGhPRUkxNkowQ2xYc1N1aTV0dnV3UlJKVmFHQytmZlZTeEZHUDN1WVYyZG12eG8yWXZnTEc3TWJsU1NNNC9telUrUWRjcEU3a3U0V0JoUlRpczZhYjdtYUg3dFVIS3hCak5YYitIOEgvMk01VUtGSnJHeU54ZDhUaUtRT3VMakxNTk5kU0hEYnYxdGRHWmVwTTA0UVExb2V5TjBaU0ZzWnZxTFRiNTBxR1ZLQ1J4OTk4cEFFb1M4akxRMkV2aFhKVDN2RmFwVnNjSW84cFR6bGsrQVRUOXAxVnBBZzFpUk4zVkdkQlB2TWcvVlBxRlFHcWpkNjNvZnVmY1VsbEd2Qk11QzRKMjZUd0lKUmdSY2xCU2FwZFdGSThSbzVVYjVPT2tIRmtHZ1FPYTBZdWVKZ2J6QXd5M2Z0U3Vlc3FHSG4zQURtQkEzOHVHY2tCYkNQK3hjbnMyRW5sUU5zdWkrVkhJNHpPU3R4UkRINnNjc3pzeVpha2M0aUp3aGJOYWQ5dndSNi8zQ0p0dDZPRmpDMEVSQlFZMGVjTy9MR0tYR1dJYzFHRVBNM2xCVlhoNXlyVzRpSTBTMitwWWQ3bDBLaUErVEZENTl3M1psVnNnell2UEs1Rm5vdmw3M1hkaWxRbE9KQUQ4VWpUTTdsV3UxOEd6a0xMeTZMUG4zYjZZdEs3b2dnV0RhSkVOOVBPS1lkK2tZSU1DRSszVnhEbnB5RndMQ3pHRjRPRWJVRVFSSUg1dTRqNDI2alV4WFJSaUNjOHVwWkdRNi96bFdsd1IwaEVEdUFDWm8xMW1WanMyaGt6b1gvVjRsZWlYcU1Jczd4OW1jd2dpcjJwdVY2UzByc0NuYTk2MTltcnpBN2JtMXU3azdzTXg3Z0ZuTEppV0RoZTBtTGx3c2N0YWFYcWxsdFg3SVM4S2J6N1JTenpzdGx4SExrMWEwN25kaHlFMk55SlJqSVRWdDE1dUJ4U0dSNjNqdFppQnFrTUFnUm0wbzhTclhaNTRDeDJTNHd0eGVQS3RCTUVPT2dXUXdjL2pjNkNXZFRKbHkranorNXRRUlZTVDJJSXBTenVDcFd0aXZJTVlwc1lxUDZEVW5QdHd4VHczdXhCTW9CTUE3ODZRbUJrSFdNQzlBMVFreTBuM1I3RlhOUmxlVkNCRjdtUlBtbS91bzZqYlU3Sk5XZVNpMWdYa050dHBqVkd1cW05T3lRY1ErZS9PaGJZcjZNQzRyYjdyVnczK1EzQUNnSW5PaEtub05FblJvaVV5cCsvMGU5YmNIVHgwdVVEZTAyZENBdTlCSUYya3NqVXRzR1AvZXYxZE1BblNPcXAwa2x2NWJOalE0WjE1VGhTNy9yMkozQkluNVEzOEs5bTBPNjV6SG1FZG5YeHpOTGtoQXVTWDU1dVB1RzFZczRiQ2ZEa3FhQUs3QUtEbm9qTHVsYTE5OFRzYjhCSE9UK0lEaFJoaDdtTm1lcWtMTlcweldjR3BVeldZeUNiSmF3ZmhSUXV2Q29qOXpFelhSd1NMUGNXSG5KQzYyUjJTQXhzd3VEdjlPLzZKOVhQYVREU0hiUkRlZUpJSVg0ejczY0R3NUQ3UnJ4K0diMEpjTmVQVjRaZFZZM2dSd0tzM3JUbjdMczJkdEVJWkw2UXNsZUs5WEM1TkRrbGpIQjNLRzJGOW12MFNGMllVUEdSeHY4eWovRi9kM00rRHVrL3VWaHJQRDF0dWNnVGFobFRXWlNYSmlST0V2WTVyaXY4c1BsWHdaQ29tMERZNGhwNDNSeTczcCtIMFhBdnRRYjdTVG8yRVhQWjJicERjb25OTWdxR21DN2JRYlY1dysrZUhleUZXd0NjWU5Ga1BVWGFUWTFxYlBJc1MxQzJML1ZjOENrUkJYb3VYNGVZbDVwWFNOengvMVkzZG9JWlUrbUd6SGhmR3NlNVd0a3E4K3BMM2F2bHJoMXBVTEpWendrVVlvZitOSlBDbXFNMnllREgvR2cvWXdGaWFTWjBQeUpMYm0vOFB0dXJ6ZGZVV2h6Tkk3VExCL1FFTUUyM1AzcENMMTJkSUd0SW5iUERmalkxbUZzMTk3ODVLMFdxdkZER0NJOTF6YXl3YllEVDRaWmNRYzJ3dGloYUdRTThIRnpOMGZiaGRtNXlNZit3TStNbnhhbUxPQ2tlTkJXbEZvUmw0cXY2T1NPUEp1cXlBQ01TQXprSi9BZllGK3hYZjV6RFpid3l1K0EzSXN2S2JPZlJldnViZGN1UWRNRTBpTVAxUVhaUFJrRnRzZEJvcXRrUHNYajR1dUZzamZJbVlCbDhTUFVzTlFyZVBFOThKTS8vR0F6WDJyNnYxS0JhNFVQTy93UGhUQlFPSXNZdnpzRWxkV0I1STVFdWs4R21EempHSm9iN2hOVnd6N1FMZnBiSUtKTStzTUpJMWh3bzljODVDTnVpeGFVRlZzRWVHNDBVVUhJQXFuZ29qamxocXlhKzdvMmd0dHVVaGhYOU5oRkFSLzJnakRhNkd2WkhMYURxbnJDWkx1bldOL25mQW9vazlUSkZ2N3JHMjZRc1h3bndJejJ3cTV2dFcyT0MwTDU4NUcwd2Q0VVo5bEliSlRya0VFVi90d2dpd2tpOCtlY1VNK0x0VHlZQndiWlM0ZDlLUFdZRnY4ZTA5WUZkV2VYTXh3TEVZaGdnTXRwcG9RbEtzVWR3YWE5V3VZcmgyS2dLUUtIK2VPSWE5UzJPVmtCcC94SDRKeURLZkVLRW9TUmZOZDZwa1Bvd0hsTGx2ZDRoRjhFbzZNTGhrVnZ4cFlvSVNwUWkzN3Flc3d3QlhjNEFMd0NUMVp5QjQ0b1dZeTNxREYyRFhWdzhKY2RYdXNqd0FnZHRyZ3ljU2YxYktIU1htdm1JVzc2TVp6RXg3azBFTGVPRHBML01tVnRZMFRTMG1pczREcTNyOFllZ0x2TE9JS2oxMU16SDMwTkE3djB1eUQrMGJ4WXFOcW9SQmtwQTdIbWx1bnNtSE1NN1ZwU2JmWXN5eEV0MGsraytOMDlSck1IRWJoVFU1cjlqaHhiYVNNMFltZVJhK2pmQTRSQkY4RWVLS3BtTUllUUQrT2JHZHAxNlFtTDNFZmxHWnlsbnVZd3BqVjc4YTM1SlJVK1RkUyt3K09lOTQvMCtJQ0IvaThQdml2bW80TklFODhtNHFQWE1MR3FaQnhTVFBvbjVMTk91M3Npbkwvdk5JL3FMVndUTWFaUm5EbmZYQmViVDB4UXlxQ040NE4vTkZWbHRVL3d5UUwremtocGlZRWZMMk8rUWZ3N01vSWpGbjlyMWFUaTdWZ0ZBd3lKSXdHUFVybUNOL0tQZVFxQXRpQ3cxSllSdVRLcVk2M0dGbEk4SU5wWlVKM2toSmZLeE5yTFcwajB1bjV4VUVmOUdLcXE1NzhoTURjcExMSStKL3Jzck0xSFJJdHdIVjFwM2gySDkwaUFhb3FNMHhHWjd0NW1QaUpCV1VUeVA1UzdYYlhqL1dHNHlIT24za2tHZkorYkRhTW1jYVE3QWRmaUoxN2dXTUYxRHFaUVA3RE1TN3NjYVdFWnhSZC9CTm9Ua085SUhDcC9ZZWFsSUFlWFV1bUpMWjZ0OXNrQjVRR2FUWXJXV0NzazRvdnB0VHZXSUhacWY5dUM5NWFPaXdiN0NJcmgyVUVUcXRnUlk3VkN4UGFURUN3U3JuVWN0ZXpwY2IvbGg5NWMzNnhCNW9IS0V5TmNxSEU2N0Q0cU9EVkpiZ2d0ZHhtUi9Wd3FoV3JTZ00rNWFFbjFuaXVicjRldXdqMXFTWEtHVWhWVjdtQ0pGUkxaY0UvTjhXOXI4ZkcvNjNxRUZTa3IrZ2pQdzJUWkxGNVM1N1RYZ3RyQWwvdDZncXdVR3lKTmc1SVZNa3c1UDlBQUxrWDBLaXFFdk4vTDJRcWFMSUJCeXlUd0NMYXRQV24wOUMrZTdFL0JIMTJacUJHZ3hKc3g2SE50WFVoSUxWUzNqaVBkQjlSZm1GQ0IrWEZ2bTAxMDhua1YycTAyUUFZaW01R0YxOEVhK1FpVEpqQXhPb0VFRVdLb1VIYTRKdk4zMlNlTWJGemhjdVFwQ0tpUXl3MlIybnRTbld2SFhqd2hPa0t0SWtuTENPTG9pT0tabHlRb1RJRWZVMDRzMnRjTEhLTTJoQnJacTJpSG55cXNnL0RCV2UyT2Q3Mldia2JVd0p1TGwvV2N1ZzZyTDlzVFlzV1Z6em9zR0tkakhVaWFpaEpTcVNGTjV0aHJNWkNNMW1LbU9qMG80Q09MbkRSekpQa20xdFlmajh0LyszbUZOcmxRQkNBZnozTTJONWJKSU9IR3QwaUhTbCt6bVFwNS9KdUdPRFJCSUZuSU9VVVRyVytURmVlcko4S3REUTdYQjBxYmlMNllOWmJ0MFRxbW83TnZ0VFVDMGMvT1FsbGZEK2F4am05TEVkUzA4ZWxDankvUmtWYVRLRFpWZDVNaXBwd09YOVc4OWNubHBCUnpFTm9rcjA1NGRsSTNDTmtOWkFwdURtZDJVMkUyajBncFZqbDlWdmRnbytpK3lDSkJham5CU2VLUHhudUFaTUdkUkh6aTBHYWUvdzN1YzdEcWtXTDFhVng4dUFWZWhTbEF5MWxSQnZZR0orUDhQbndXUUZUSWVoazErT01KaWRabjZkYVg0K3I5Q2RjMi94QUdJSDh2cFV1d082blZZU3pBWVFyWWhwQUEydjhjVG45Qi9ha1lhTDhrcjhTZTVySmZsRy9SVjk3MXVibklnTmp3NWZ4aUYwR1NrU2VwVHpNQnlWZ00vc29jVmZ0RmU5SmNvaG1KV0ttNkQrS2dvQVQ3TG1lRnBhYnNQbTVtYytWRjN5dk4ydUdIc3VjTW8zNFFTRHpNb2F4U0IyUGJCUk9tREE5UVE1a3B2QUZCMTJxOUYydG5VMzMrRFFBcVBjOXIrdHVTM1NhOTBVTjUzUFhTbndTbnNScFdOMG9kS3k2ZFFOSnNRYXRkVXFiUmlsbnllS0dWN1RDNC95NXlORDZiNnp5d2xicTcybEhZTFc0V1BYUEtzWFU1eFZDU2JtV0E1NmtJaVhIYlUrMmVQVG5CZVBrSXF2amF4QXpnTHlnN0JwQVUrUnNzcGxCK3lEdFVNM3d3cnU5UjZQbDI5U29aWnZrZFlXNUtQdzIzR2FHM2ppZkJaenNTOE56dTZqRUZLeitzK0xkN3lmQzRsL2RtNDZsTkVmYTVDdklxMWVtOVJRR0VYQlA3Uk1McXV1UVlNZDJJeC9kVjVRMEp6T2VRc2toVXFqQjBNNXc1dTZzK2RQWG9lSTNOaE1qOVYxQ2tHZ2E5Y0diOEVMZWRSQTlhMmpWKzY0QWl6Ni96M3REV1NRSXRiVHVMcnFXNjdUVzhscWpBaE1od0g3dFVaeGRPZmtNb2JabzFyL0NuMXdQOE5ISFhQSFRPendPY0g1Y3lYYmNnVnplcmtkRzFkZmZ3QUxrWWxpNW0yOHpSRzAvd3ZJOVpWU3ZZTFkyajFDYUxtM05BQTU2L0MvcDZsQlk1UGM5Ni9Ld1pqS0VOVkRFUDV0Y1lUYWJsZDQwWjlZbFhCamNaTFQ0UU5tdE5rZ1ZGMkNZTEc3WlBQMzMzRk9FZHlMc1NyVzhES25JK3Q1WFZoSUliOVBRbk9wT2lncVNNaU93eE5vZUZiNEFLRWIrRkZYK0NaTGpkTHQxTitkUHVrcGkvQUxVWWFnbEZFTXR3blV3TXIzLzdJaXNqOHpGSG9nendlY2N3TDNOYVVCMUtrOUM0TklnY3Z5dGdaK2duekJLdHNrSVBHdFNGTmR1VDRrUFZTaFZSYjViNUo3alRvUHRuMk5FUlhOUW5VM0ttQ3hvUzA3WDhMdk5SM0d1cUJpelEwdUZxVGNZUTVsV1Q3VmZYd3NNaWRJcEJWV3FoaDBmeUZvVVlkOFhLRlJNcE54YnEvUFVOT3NuaHU0dTRBdTFLVi9PaGxqSkViSDhzVHg4ZXRMdEVGMnhKNXlkZE9qN3ZKSWh0T3I1Z0tYdWs4RTBzTFdNcmtWa2gzM0VGSURnWVM0SlZ0aW9FYUEvNWJZM2VxMy9rbC9TRkh5Sm1Ic0lqNGxLbWhORjk0ZWp1MVBSZXQ1YWI5ZENROUFUeCtXWWVBRVUzR1pKemQwMmlMUFZ0bW5mMGJhVnU3ZjVEaUp2bUc2aDlEN05ScFBKQnRQaWtPRkdFdktSUTZpRkk2QkhIdHZzcjBpZ3c3MzJOV0J4TEpmOXFKVmVmWXk2bUZmdEhKejZMREY4NVJIdXZlQVJhL1paMENaWFdjMlcycEd0TFRXMWV3STVzN1puMTFPWjdNR3kzUGk4Q0hzUldtZTcwWVBlUnlpbFFnSnhkbjNudk0reFJBbWc0aTNIQzVoYjkrRitCR1NYUFplSXpzZEJtN0xTc3QzUWxiVWVRdTUrY3R5ZUtSVEJ0U1B4eUtud1F1Q0VwQmlwcE5rcTlUWmwzOCtZdWk5amVoZTgwVGVSYmZKaUFsemZubEk5cWxYTjlxOSs0WUlUbzg2aExUeHBLRkdZM0RqVm5YcDVoTTZYaDlpaUdGU09IOEhPZ1R6VjRpTEljd2VBelMxZEY4ZUFYWXR5Z3pHWC9DN3dmbjlUYThGQ1FHWWFVcXFoSEp1cmlCRWt6K3piVFo2Y3BJSGE4NlZHN2V5L3pHSGYvRTRBSkhmcVBXaEpydklKV0swbmNaWnY5NmJNbDVJWVpFSi9XN3BMT2tkT2dvbnpLMVFsWk1qQ0dzWnZRWGZZTmt5VFQ0NzBHWEQxdTFaeDVxRkQ2NG1RR1pCLzM0UGl3TkZybVVIZGNxUklGcHdsaml1c01VaFFtWklnTTZ5Q01lZlM0ZGNjbVBEUFhYS0cyUzZmSVAvdC9aRnI5MmRuc1cxUGtNTUpjc3cwS1BlSGwvelZlV1RDeHc2WFZHc3IwNUdKNk5BOFpVL0RLdUZKcEdNay82bmNGSWFneDh5bE8xRGtUNkcrUmFsZXovMVZET1A4Q0tIZlkzOWNJWmowVWZpTEMwOHBUK01NbG5WTHVCMEF4b0JYTURPMnVPMzAwZDd2QytwaHNjMnBNNlRaaXpwOTMzeUlrN2FrbmQ5cmNVU1poMVJ4cTRIRWFzNlBDU2FMakdLWG1TQmRvVmttWVVEenllUFVjaGFyaDltRXBkT3dNd1lVMHZiS1JFMHdJb1dNS2hnczI0QkVKTStvU0luWktSdVhUR2paeElSQU9SSVNYYjl1WGJ1WWp6MytVdTJGRWhRbVhhTThrVk5XVTd5NmxKRGxDLzFKUUFhSHFMRUlGdE9hRk4yNFh3a2lycFN4ZlcvTDdMZWFYNldjSk5pOTZqUVVHWVcyS0M1QXBsMm8vV25CUkx1YzFPeXp5YUg3eW5ZUnJ6UEsyUE5QOVRuOGlHU0dKeGJXeEJlYWZVdjQveXhuMy80RmNpRHhpemlxbzJvZDR1Y1c4Nml5NlhIb0UwRGdOdkNLYXVBdHR2N1lVRml1bGRDcnhIZTNtUkgzL0phZURBdWtDUFRPQlE5VXBlV3loWnpUN2E1SXRCajFxR0FMUngxckRxL3g2MHdNd1NyQUYzd1NSS2FlRlJPTDZCcU05N3RoNnpKYytpRVZwRDZIM2gvRGk2aUN5bnFmZ3Q3dy9ZWnNiWSsxb002QWpwNEMyWTl0TjBkRENjNUFHUlgwdDFZVWRRQ1hqWHA0MlZwM1pneHBRR2tBUXhrUU51MjU2QnZQRDU3ZzhVSzhXQWxxMFZHZ3c5OGlvbGdXRDIvNG5SSUJFVzU5Y3NhNHRxWmRpV0dKMC9yb2lzbGhwRlRuNVZTekFZYTVwU3lTWU9kZnV5TmQ3WjhSUXRSNkxtcDkxQW9QdlQzeUFCQ3Q4UzVHQm5SSGcvcFIwd3NRTW5XV3NBS1B1RCtVWC9IcUx2d0xQRFI5bkRMTzYyUDVKMjU2VmQxYlZCakdtaWVhcmsya2RtVmZtUDVjNXIrblBSeWltd0tRVllhWURNMGZUS2N5c1NwdHlyUkJqN2NibnhLNDBlby9FMW8vZzVWbkNTL2YxbkN0ZEh0UllTT1AxYU94aVZvaXNJNlhFc1gySjhvWHFEQUorNGh5czRxRzVMZ01QM0cydUY2Uk5SbEZXYjIvOXo2UWxzdG1YTUhwekdlQkVjMmxTT3B1N0M1L2tPWmxuK1J2OXQrVmdqTkFtaXhxTytjRllhSWw1TXBxcC93cHhaTW8xbndNTVdTWW80a09FRldaaFBWL0tMVDVRU2svU09qQ3dIei9jTm9nekd2d3VPK0JjY25QVzcycUF4azE1UUVqbStpNlh6QkxKZlpUcmFmdW1mYXowVnk3RzJocjJpeVdCdnh2SktxZE5nRC83M2xlbUJoNDBXd0NJellESFh6M0ptZHlTRnpBUnFOZmd4MzB6Vk9sVlpOaUJLTkVkTE5hVmZYYTZzaGxiUDVyNFZ6aDBuT1c4SmJsbVpXV0JOQVJsRVE2YzlvOW12TFhYcHJQUDlYbklDT3NOREhFVGZVMzhxSGxvRlVDc1pCWnVLTWJkaXQ4Zk91KzVSTkN1bEZjRkQvOHUva3IrQ3IzQmNROGY2M2tHVmQ1eVdyaTBFV1Bjam8vN012ejFnL3g0bCswNGJzMGVMZVp0QnkyUFhBYTNYY1ordXQwS3hybkFhbEVtQVFsRmlYKzNneld0UGVjY0xreUsyM0YzMG9xYlJLOTkwSWlsVlN6RW9CNmFPMmVEOW5Ha3A0MXdqTytPSEU5Q2hxT3dtZWlSVUZFNnBBQk1hcDJmbjFuTmdnQ21PazRLd2tldW9nTU54SUJwdUQ1T21reVJTYUI2Mk1oSk84ZTU2ejJUWWo1TC92V2hhS3g0Y1VVbDVQQ1k2dXQ2SVR5Qytkek9oQk5PNWlGdlVlWCthTURQZi9LT204bWZyTTRROGF6THJvTnQ4NFV6ZHlDdG9PYlVGNXhZbStGOUxBTXFzeXEzcE9wb3ljMTl4UUxROGE3bTBTb3RxWDE3SGdsOEk0byt0UmJINE82R2JrQlV3ZGtkQytPQm5sR21GMWphMlFOUThRRUt0OG1tZkt2Z05HQTBiek5MRE5GV3orRjBnZmVQbUM0Q05sdzlXalhVVTh5SERXakVjbjdxeXlUSm0xSzh1TUttbWVPeHZPdTR2MkR0YkcrWkcrYWY0WDk5Ull2TWh0THMyb2QyaHVLSzc1czVRbk4xMzMzR3RibktmR0FmUnNQL0REUHJtMWw4M0tTdHJ4RDZrNWZOQWxicXB2WnA4U0pZNFg5MmpWMU9FSWRON2pGL01lQTNrRWdHNXJ4SjE1d0g5OWtBMUFRY1RCVDNmOEU1TTRZNjNJMSs1TktGK1hhUGp5dWVzYTlFU0xSY2RnNytWT1pYK01FMnlDdFVaQ1lsdkpmWjlHRjlwV2Y2SXVlT0JtTFVJdEFwSXhYbHJTM00zd0ZzTitpWE1NV3dycUpyWVVzN1B4NEFOWVlSNmZJVmRZVDlBNlYzZjFrYjFaREpyV2FxRWZFaEpQT2o4c0ZwWEUyT1gyR2ZlQVJ4dG9uZU1MNzhtQVk4ZVBYc0lzMTR4aFdzMGNuK2JlWGpCdVV1SWkzVDRnelpZMWVDamZkZXBFZFp4YmFUdFNrRzJBQlh6WDEzaTJBNnZMZ3BCU3dLNU5kSkZIVlgzcnJUcWVCQlB1dWx2WjU5VEtMSDRmbG4zK0lNZW1pY0tZUmJwKzVyOFMxKzliK3ZTK1piNHM0dkFSUTJWVGdHWWNYSDQ3Z3lWT0FMZ2JIK3BwUWhPYnJzTVNTb3A3dnVBVnljYVZwbHN1c0N0YWthQ283Z1VVYzJGK0gvRUsxTmRZVlUvZTIyUE9xNmhGZXlvaFZETUJyZFJvaXdiUm9RWDV1WEMvRkRodXp6ZjhhNnBJWXpHMklTWFhiNE81Qkc1Q2ZEYjZmV2xzSkRaa1g0NDJzc2xNd2F6WVo3MzYwaVpqajZlMFNrOGFuRGhUYlo4dzJoN1dFSjVTWWR2UEVmdi9MeEFvR2pMa2NCcjdwaVpTNTRRRnl2d1l6N0tJQWkvZXljbEplWDRsRGVZZ3FLR2YwVGVDVHRaM291cG5HSVkzRWdRRDRYMWNES0g1dDVwZVhYZFZ1d3FDekIvemMzejVIYWQvc0I4NnVQRm8xZlpsWmNpeXlCTWdzaDRGUkNBekdYNjhmbXlueVVka2xSdjUvZWlHbGZNN2dLcFlBSy9VYjdHUnRsdmRESlJ3blVwc2w3bmRjNHlOQ2ZYUHpuYnlaRjZWYTFuTkJmY2lrYzlrZEVESENYckppaEVnYmdwOTZtY2FOS1loVVozanp1N05hQWpjMytUdVQxRWk3cEhJNDRqY1dGdUg2alJobmppZFRXeEMxcG16N3RKMEhtSWhiWThXcUhROWxZays2d2pVamZEZEt3c1E3VzUvOGd3cVRZR2FJVXRQaXVCQ1NjSEs3MTNpazRFRFBSa2w2YThYMHFPYzZ4YnFybjV3OTdzOS9ma0xpSUpGdXo3QWliYnNUMTFjVllnTXVaZnpyMmhKVG5LUy9QWElzSWJEYlhVcCtlNzgzWk1mYVMzV0NJdzZDVXlNUTdWOUdzYlZQK2EyR3RlWmJUWmw0cDdoZ0NCTFEvSTh0cjFybmJYWjRibkxTcDFVUHVBUlVwb242dXduM3plRmtFN21mNHRTdDhrSTRscVl6VVBveXpPZWNpYmIzTFB5KzViSUEzdHppd3NwelJqSG9DaXBUaGx5RXcwelArWkl6bERtK0ZBWTVpVmJReTY0NUhxYkFXajJYZTlGUlZDQkUxZmd1TDBYbDY3NzRPQ09sVDJDdHZwL2pqZE8yRVhXRml1dkhKRHhhai91djcvQkpFdUtBbGNhNC9OamhBdEtvNlFjbG4vYUh4dEswLy9QVTg5dk12OVlUdFNZVnBDS0pteXRYSjJYOXZGODBMV2NCMUMzM3I4UFpxeDhxRXV4WXQyTXJOc3VPcHE3a2FIdTVTQnBRdldoZHBkTDhOS0M0NlIybzA5aEJqRWNab2N6Z3paY1l5RHZtT2hPeVdrZVQwM2VrZWlBdjdtenZDMkVCbFYxMW8wQVhoMXpVNTJGcWFQaVIxWTU1M25VenhEUURNMmhxZFNnVTk3NkVuUmxCbUUrbmVpNmcwT3NjMjlGYmphQjVGRkc2QlVpbEQ1RGhyK2JnU3lJNzZsNE9rN0x2cjk5SytnbzJIQkFZVTBtMHE0ekY5c1hBaExjR1VDUlFRZEJqNEt1MXN6Z2p0azE0VUFPdDdGaFlYY01PZ1BKVWl2K2Q0VERJZm1GOXVBWHpYUmNabklLYm81ejkyRlY3V2d3OXB2OVE5TDZrY0R4NkZsZXh5N0drWm1CM2RXbkJrS0pEbXFEZGZZdVE4MkhiUTNzdnJJbmlETjFqSkNsSEFKa0R6ZUQxcmFOWTVkQXhFcTBSeWRiY3kwMFA3NlZmZUpkOHJtSjhqUkhKWjhFenNLdTBqdDFESTVmMWJYWkVtSUlRaWVkbi9TdXNaaHBBRHMxd2NCaHdUYTc5M1FYMEJ1UFd0R29hcmx0Q05KU1NnOVhMV3VEZ1pHVGp3UnM1cDVPemN1WEU2bUxPSlA5ZzJFWmc4RXE2ekt6U0JXU3pENEhONnZwMFR2b0E1TlBETWp0b1Vlb3k5eGdFV2owYUtzL0NCaEFrc3YxV0ZsU0hGUERINVNrWW5GdExoY3BXTnFpSzByVE5QU2dhRnZPUEgyK28rbEIraHRJd2lVd000NFpYblo5djNWbXBJYjVVT0Rjak01Y0dNR2hrNThNWDdDeHZiNHRHaHByQVZBS0l2OGlvUUlTQlI1enFZOW9rSnlxSWZPZGNya0NPbTRXUytWeStPMWFlQnEzZ3hsMFNFWlp6MEtIczBwMktJQUlTbkNiSUNWeUo3QnBCWEo2Z21RM3UxQ29odTdQdWRhRXFGc09icUhLaG1KaHVTaGxZbTRwM3NEYXNpZmJ0ZTBJQTUyK3ltNVRSbElCTEx1OTBDaEswbStGYUgzcEg0NWVKZVJmTHIzTzcvVmpFeDJRQjNIVFBuQjRXdEVEeHlEL242ak4xMUhqcURNTUlkR2NKemVJcEwxcDB1Z3grbDB6ZG9HRVNmZmZNbzUwZkZHQjFxb3grdTdQbUVzR3pGTHJKbGdLMXJySE5PdEp1ckt0K01ZNEt5a3l6NlhkY09SQWpxYXZ5ZEV1T3RZWEwvRG5vbXU0WXgwMkMyd2NXRHByeVFrUG9hellVZk9rby9GeFZEeWlpK1VnTFpYOVF2bENDaXpEU24veXprUkcxUDE1R3RZOUZJRjhqaVd0Vk1jeTRhYlozZVRGaTB4S3BpSzB5TkJZRy90cVJWMVl4ODU2bFp2NnFGam1TdWRBMmMwUThJb1FxT1RSM3lQUThJL2Raa2Uzbzk2Yjl5VEhHc2RLTFc5OW5sNDRRNFM1UU0rV0dwdmRsOTFMQXROWm9DWWVtbG9mZEwvVWlmVW1WSENwb3ZmQlhRa1BIZWNRUVBrcnFTV0VjMHlKbDVuMk1HSDJybkdqZG9GREVRb3RWL05nc3BXaSs3bkw1MEtKMkNaSmY0TWZWeXMxdFZZRkEwbms5aDZNSHB2L0M0Y2VNL1lsTzM4bFp3T2RhbUJDalZDMlFzUExtb1I4Qnl5YzN6amNKbGpXMkdzc1lRQ1UvclNrMzNnN0pCSzluczNZdERJMkJHMzZ5bFVxa1I5S1hqbGFKaHA1aUxqVHJMeGpCMTNBYThSemRUVWVGMmVWcXlON1ZyZWc4MzBpdU5UMS9hMUo1ZDVZL0VNa3lselJCSy9ac1B3MzVTZ2hFNkpTZ3I1bnFaODVVeUdPdzVvNlZCYjBENUtjaFpsOGJJWFlyL2FCRjg1bFpKaC92TDhEN3hXUGcvRUJ1MTFKZXdyRldFQ29aRzM5VlpMbEx3N1BlSG5qNEZ2K1ZScXBjKy9qcHRGOGVZb0FINzYvWHVTdGRZY1pQaEFPWDFWbEZwRVNhZXdRek9qWkpabVVNc3NwZ1Arb1hGRVNadUl3c0piTmRhVDdCTWNqWWpqYkludnlQSmllYW5DeTJBU0poaHlRcjJZZjlZaE5zWkk4WE1yZGJWTHVzUmY0QXI5WElOcGZ4dTZoeDNIeVNRYTFjSU81aWJSVk0zanNTbVJiV1pHcXlvTGNiaFJOaWxLSUE5NVVUM0ZPeTJvaWtQaEpUZ2ZZdHNoeUI5c1dsSW1nOTVlSTk5WmFxb0hvR2wxeU9aaVcxZ3N3cW42blF5ZUZmcUNMWVFSVUJBQWV2REVydnZncTRucWZmY3ZIeTFYaGpwRGxWQk13WmdKa01LQlBwejR6SWNOemV4ZlI2YzYzZisyeEIrRlIxNEZBekFBVlowREV1b0J4STY5REl5cG16YkxPRDBCUHJnRkw2MlNIWjhFQVd2eDFDR2YyQkE4MTZuY1N5ei9HdkRpZzcyVmxZU3R5UVJNUWlySlR3dXdNMklEL2lqbC85bDU2OS9FRytmNmhnUXFTMVJFb1YyNzZzYlhmUUZ5Zno2dXhkL0p3VzRpSDloY2FyZDFiK0tOajdQWEY0SmZ6MWZ1NFNCbm8yQi9EVE1YTjIzQzJvdXptaHBTcmEwRmdvS1NIZDVwcWtaaGxKd21HMFpTY211QS92Q243cmtRM1Jwc1U3Ukl6aTRmU3ZSZlpvcmhkWHZ1UERVclNvRW9JT3YrRVpmczllSzAxeGpBZ0VUU1dXaDNERVREYnR5eGljYU02WXdlMEtMZXcrZDRXNFMxOU9RWld1Qkk2V2Z6US80RDd2S0dYdVVEMGZzMHNPayt6Y3ZUa1ByaUpsOWpjeUVKN1pJbVZobmJVaG1Ocy92WG9ITUNyNmUxU0lFblNaTFRaZE1aekgyc25iMmpRZDFXd202aWR2aXJaejFhYW9ZdHRFWVdRaG9EUFl1Y1JDUjdsZ2owajl1MUMwWDM2ZFpyVmo5ZDRxbDdaQ3NvUTVZUmprRG9PbGY2WjJCRkk4cnd1eHdOMk5hcE5MN0VPM3lNTk9ET3BMaWV2bGI0RFdDTCs1dDRJNEprbEc3eURyRzQ2aHNsRDhrSmZkOXJhRS91WUxmazFSeUc0UFh5cGEvdVFQWHVPRC9GWGtFWjA4RmFpS2xTL2VaSnVxMytVdEtKM054WmdBZGluNEFFQ3FaaDV3M3hNZUU2ZHlWeTEzOGw2aE1JSkZmU1NIVzZJbW5IZVY2Yno4OWdoSWg4ckxIb2xvYXJBeHdKaEtpaFJ6TkZnRWw5MzBabnRnZEg4SlRPM3RPZ2lYeFZkOXQ2eHdqMGtKMzR1QU5td2p2RGVjZWhrOFRHR1hxSXVPdDVhWXp0Ync3Q0drMlhKa3VnK3I1eVNtTzYycG5JY0lOWWRYV1ZJRW5oN3p0ckoxbGJybnVUYSsyVlBxWnorVjJLaVNBY2dxRFAyV1ZwVDBJMlo0QXZWNGsySVJMMnVvSHVleCtyc1NKQzFaQ3dIcmRSQUF1b09uSGM0N1paYnNQUHdHWXFZZG82MkVSWEt4aFEvRTZIYmdlMlpUMENBdWV2MHpjclR0b3F3TEFnekZ3Y25NMkEvUk5weEpORmQ3RWQyMUp4RW15azQwOWExOEErOWRBTS84SUhXVU9oa25qdENrOC9wZ0VYVDNWNUc5ZWhaMnd3ajlLcGxzM1o4MFZLNHFjdEkwY0ZWUDVxNkcrVDBsa0Q4VjRVTkd3WWxtenA2M0pEdUxoMWRaY2Q4WVFMWDdvMXl5ZjhtdzBabEdPbnJ6UFRpdUVvMWo0THpGTFQ5a1BnV0JXOUg3a0xtZkZDb1kvZmxXeU93K3p3VzZEaWtZbk9nQi9Pc2ZmMXY0Tm9vN1QxYzhFcTFxZWJEMW9BbTVidE90UTlsWDJERE9jY2lZNHd2YTdXWTN3UTMvWjFydzBSZVl3WExBNFV5YXlrUHQvMzZ4WVRBUk9Vak5wc2IraTZpdll3OGw5cm5IeUdFdlQ4dG9jb0h0QmgwTUdhWnRTNC9KSWdIUmloY1liU2ZDaHdSemZGd1R2RkFSbWl4YTFMRkxva3JrTldiMCs3Yk1ZNzk3NGVFMEhEMGgrd2NSSnJlWGMwdzYyMTJiUW9Zamc0Q1lyQjFnQk5KbXpKanFuQkZINDVBSTIzMzU3TlU4OFRqMVZXWnFpdE50VHBTMCtZNEttZkxoV0Nqb3VGQWdzcG1FZ2ZnMENoK2pScUpHN3FscHBiQXRTSGpVaXlmMmN2Y2RrYWNLQnVlK00xNndZVi9uSnRzcGlubjVRZFFPdzkyZWpjdGJzN1ZWcTM3ckdkYkhkTWxyQVVSRlFtanpTSEFoMDlZYXltWGE4amQxSElYTG03bnVaT0p0KyttSVl4dmw0blVHbDZMRzdua2JZbzRWUGwwOUJicFFtN3piTUdybVVoTUV6NE5pT29jQ0JNWC9sa3FOQkhvcGx6SlNETFNSZVQ2dHJDUkpxV3R3Zy9oTDArcXB5bCs2OTE1MW0zY1BiNWR1bTkxMms4Q25JMUV3WHdqQXFwL283NHJRTDhWVm5uNmN3YWtqcTVuN3dkelZRRThOZVp2ditOTXRBSVRSR0YxR3BRb0l4UCtvalp3aXB4WXJCQmFQR0pRVnlzUy9BRmF2ZUYyaGxUdERpVUV6Y3RxcXdjWE9TcUgrUEYyaGFUQ3JEM1A2OFB1bitrMVVWL25CN01YN29VT0tINSs1UXBicnFXZStBZThOWGZNQVlyck1SM3F1THNVYklBV3dOT1JEdmFkRW9hVWlieUNYc2tDUnAvZnlEOXJDRnNlOWsxOEVaZHNqdzNza1VTODl1WFA2aVMzTkY0T3hWNE0wekhTUDBPK2tLSHV4cXlzUDlmMXZFb0tMSExndUlRRWowZmtDYlBVYlE2QWR0NnNGL3JRd056dGkzdm5qY0tIelloQVdCM2ZrZDZsR3ZJaU5zWlAwRmpzK2crVCtQaGx6MnVJMkNkcVZmT3RFbENTM0k3bzRiWlpMMFg1bTJSMVh6S2U4VlpOODh2dFJPcWhrQWdEWmFmRWJwTDdLTmJEbTd6b0NaMHc0Vy8zYzUrODBnRXNTUDdXT0lMd0c5Q1hxQ2xEQXlBU281ZDFlT0grcXJBa1lyQ1pwRlBpTlN1UjBlS2daeDByN1Z3Mms3SG1LOW4yRHA3NWdydUFWK3VlSzlNcHo0ZGM2RFFTRUJaQy9lNkdzS2ZjV0NVczlOVE50RkcxWlZBUkVYRXhoRTdzNGFTbWJZZW1kYzVaV1ArcU9rNFdRKyt0ZVhCcDljTnE3TllkOGVjR1hJN0V6NUxCNmlTUzBuZnZDNFdzV2d2RWhmMnU5RTRXWjByd3FCTEVjNEwvbWhSaDREcEF6OGovTmJzYVlaYkVzQ2NyaTMwdU9SRWU4bTUraWhseDlmZXAwVTl6MU1FZFJHaFFKMUcwendGNlhCMy9lRFExN1Bnb3FDcnFCSjNsczhLM2p3VTdsSnB4VmsrZnFKVVAxY3JLNSthMkxEQ244dUpjc3M3YytKNWM4Wmo5VWlUL0Z3RG1YcC93RWtoN1JaRTVsREtZQWh0L0ZKMWh1QXZJUXR3Y3lIKzRINVlNQzhvYkxheDY1VExUYnNiMGJjRzc4bUljbkxDOEJKZjZ4YVRwOFVmeVNXQ01pdkhNNTB2TFVvaTZOako4elE0MW1ORnA2RWN2ZUorcnJFMU83eDI3QVEzV1hZNTBqdkEwcmc1OUpaRjgvUGhDR2xFbG5mYUl3SnlvTHFUWFQzSWhyVHo0SW1hZ09CejRxeElZaDRvWVdDdHR6RE90SXZNRkxZdW0vTHNUR0Fhc1Q4V3FEVXFSWkkvVFNYSG5hbmNnMHJOS0UvV2QrL0JMVFdpOXd4djFMZzVlUUxueGNMT1hIcklOWVc4eWtLZDZVdi9SZUZoQkFkdHVyVzlwV1ZqVFFHb1BGNDNEcWdyMWxjanJjMWFNRWt2YzU4VGZjdVk4UDZzc1BzbUNrZG11WFNJUzA3V1B2dXRQVUE2L1ZaTDN1NGUrRW1tc09ucmN5UlUxTUtkSlRXTnI3RG5GQmZJaVJWS2VZTk9Db2RtRXI5b3dtRmRRK1psNU1uU2JSOVNMeFpPUUg5VmQvUHJhSEJMaDRNUDFaK05kckN0ZTlHWHNQOHBEWWgyT0FNWUwzcUpTaGtCS1Z5dU02WUptay9mMyt6MmtVOHhWM295Q2tEU1JmMk9zZmkrQ1ByeFpQV3ZIaVFkREJRNEVTUWs0c3M1WHFBQzVtZ0xacjZaNWVWbGtXaHVjVVgrNVNzQzFYL0FVMGliTStGUGQ1RlM5VGV3N3lHZE4yVldCS2xUcmlhSXdJWHJ2ellkVWpmbjJiUGROOTk0K0tZNWl3dFRpKzNiV01ERVEvZXZZQzBUazljTm1neHNMZ1VSRGJOWkx3OEVpMVBBa21GSGIzMTJqZXkwSXBMTTc1dElTNU5MQ1VSZ2xUcnRvZWRSSDQ0b24vWHlpMExiU0dUTU5ybXRRTE1FRmRSdDVMbEt2bGZiTGUrOElac21QS1NHQmFNODhGcUIwdWliNy9WOTNjYXNOeHdZR1IxR2lQNEh0b1YxUkRGbmhYOVlJVEpNSVRXbC9CZ1doeXpIUHlZeEp6UjNmT2wyRXB5Umg4blFDZkVPWVNRTS9pRjZzblptTHprVmlPZUtRa25OQVRPODl3T0NrNjBCQWJjYnY2VmVPVVNaWGxJbzBGM1hlc2FPcFZPV1lBVmNpL0JuTWd1ZGNjOEl2cU10QlFadHovQlN4R1ROc1hNVDJlMHFYdTQ3VExBdzNOVXZYM2QzS05ucGRYR25WSWVOZXk3ZERFU2lWb1NvcUZlam1BdjR3NTcrSEJaZnlLQlgwK1o1WE4rTWthNi9FN3YrYTlzNmxPUUVHelo5TFRydzFrOXc3RWlqc3dZMWFYcGtDcU00Y1FaclFicVBtNWNXQTdKZDVESkZocDZERmd6b0JaN0hnN2E3VkVleGxxZHNqYk9VL0t4dGR4dnlHUWlPSFliQ1hmaWxiY1U4M1JncG5UVHJKRll0cnMrV1hnOXBEVjVjempGNFhRMGdPNktVUDlzaHFPL0ZoOUtKdmJYcWFZSDNWZ1pYVjlrSXY4NGZaVXpLTEhUMUVmZ3NRM2tRTnl1NW1hREgwUEJYdURmSndYR0N5SnhtVi9ZMUZFWUtub2psbi93WVRnZzBWM0NUWWZ4cnVBSW1kV3l5NkdIR1NGek93ak1xK2RVTXBLRThTSHp2cS9ocG9nK0xPc3dLblEvcVRoSlVxcWxkemhUcEttTm1nRTZLRjNJaXFJbWt4UlRLSTNCbE83cTlrRE1nd05KZ09VTHV0RlprdVNmTjR1OUQrZ2l1WURieklUWmZIV2VXK09rRkJnNUltdzliMW5rSWU4M2l4MmxKbU1mUWsxVFNWUjJzSWRNYnpUWjZ1ZHJSaGhDZHdFbXBVb1FWQXROa3RkTk42RFduVmRIZ0dzUUR2ZE9ULzIrRERzSXFhSHVBUGc1aDFJRkhLRlFpNG9OZzJBNU9XaTh6K2xiak05U2kvUzNLTi95RzJ3L1Y3cGRsdjYxeG5GWGVZUlVXVWdvbkJCMUVYV3JwQytYZldVTzc2dllxTE9Jdm9nQ0lWQzA5elNZVHF0V3dGQk1XemVCT29hUjM2K1JSbUY3bVN6OEh6ekhLdThTWTN6b080SDdyZSt1Y3Y2QWNlQitlalpYbFQ4ZE44SXJaYlJ4ZHU0OXhuRmtQUkZZRHpTU1FsUGkxc0o1b1djSndQZ2xzQkdiWklBeXYxK01wUEFtbDQ5R0hydjhGMDI1NjFoRHFySDVGaVIwTnhmWTMyMWlMUmJ4bHdVVW5pcjU1RzdpTkNqaDRTa1VRd05LanQ0VktFUzRwMWZTK0lITFhiMktZaFlETWxpb3B1SXhxb1dlRFRmQ0lmS2lIRFIrOXB2Q01mSzErblNzb1VKK3RPQjg1K2RXVk5GUVJUMHovdVNablR6UXlMeVA2VllnbC92dHJjeXVXYndBdEtTeFovcXJOZmRRVXZGWjIwQStveWNhUGFOTGtYR3lOVDNjMSs1MVFwZVRlalYrYXV6ZVpwNk5XRzZDaFA4Z2NNb0FxTkJ3QnlzNXoxL0JJZytNYXd0bUVTK05qQVJnUHZtTWtCd1JSTndMaDUycGQ1MFZjWTdIWGdYWGQ3WjY1UE9ESmNyajJMMCtKUDhhbHhSNjNuU2tnQUdnZHpjSFVvbHFBT09rR29WNDkvZis2OW83L3FnQVRZM0x4L3hvZkY0eGR4dUtmVWlqWSt3elM4d204bndQWW1oNUhFdTQzeG80dE43aEk0NHRvZnQ0NjQ3L2MvYkYxSVVnUUdLVUZpYTcxdndEVk5XT1YyVW1pM0FzMVpYeTFMeWFpS0cvNE1MamI3YXRmRGwvTElzM3dSd0l3Z2RheGxjWVBPZTFEd0VMZitpQTZKNGowWW12OVRzVDhrZGZGOHQvTGJ5ZUtwaDRuQTlBSzBkYnBVTkc3bnJZYTFBYzhsVEY3MDBWNVJhcjdlaC82dnBCRVM5djA2clIydlJPUHRZSkdxWUdRYkxWLzNBd2U4VUNETHhDVkNBazNiU29KbDVXMjRaOHBXV01ubDhVbG1GbnpXV3VOSHEzVUhnQkd5emR1L01NLzJrTHNHVHpQUXJ5TTB0UXZiTEd5d2ZyUGtLdFlSd004NTNFdisvNGNseTN1dXdtZXJkeVhTREtqeit6RzdHdnhmays0MmwyOVFaazNBZDRHSndlejBKaHk2dU94c0FFbDlXc2JXdE9ZdGgwUFZEWUNCS0orbXJoTTJNZWhHNTE4MFpvNlNjTmJsUEdjZWJiYTExNjQ2K1REcS91RU1rWE9JZTNFNVlDRzVON1lpTmJ4WGQ3MzljUS9vQzFlQzQvcURwNTVmSWp5aU1EdEpDdWJIMVA4SlYwV21TVkluOFYwRFdPY2pkYldycUxJNWFEWTFqYk1xaFpQNy9lMldJcGxubjdqNEJQcnJTZXdSMkxoa3gwMjBmNG1Zd1VneC9DYkxnVjY2RmN3eWpzRHlrdWEvbHVNZXNSWVl6dlAwN2lUc3hCSkp2d1VVQS9ZVmdRZFhxcHcvQ3RCZnU0Q3Y5ZmVmZVQvTHpCcFJ5UGZ1M011alpoUnMrSjFWZzBTc21sWmpZOGZZSEh5MzJ0YVlwMUdLMFpaWDdBeldpQm9TVXg0TjlRa1BPbEhrTjFOV3p2d2luQmdrMmdvUEN2MVZLNU41cDA1QnQxYUFFZzFPRGtJQitwakxXaURlQnMvMDU5NGI5ZE9aK3JnZzUvR3ZBNjJiMCtSOGN1UzhTQnZSeWlOa0Zqb2VsZjRGam1ORnUyYWZoVnBnWkEyQ2ZNek5LT29wck5TZVhxRkNjQjQwb28rQkpvZGFnU0p6Ym1Jb0dkMnNvdTY2ODlqSlRiWEY3MXIzUUI0SUZqSkxiMjN0N3AwWkY2S0xxSnh0VEdNRFRwY280UjZ5QmZ3TU96ZTJ0NGszdWI4TTRJS01PakVYdDdJZGxieDBNYjVxREFib2o0bVpMKzhjVHh4YTl4RjF1V3hidjNjUFBwSk5YR3UxTDY5VnNBdzY0WFZ3OWRiYnhFeXYwa1hUTTBXUFVoMmwrd3lJNVFIY1MyckgrajZYVnBDdmgrbVdCdWtTTTRMbEljWC80SUk1SHJWTnRHczgySHlDUjdOQUloOGpZUWwyMHJFNXN5OWMxQ0ZTbzNkdytaQmJsc0QrSGxmNXdNdTFCYXFWeDMxb2NySnR3OFlMNzJIbXFNYUR0WHg2NndoQm03Ymp3WjRESjVTcXh5S0Yza3BOVEM2aVpNWFVCUWtvRStKcFBnRXpYYkpjWjkzWVhaTlUxTlBQOUJoVzFMdkFzYkpYVTdDbXRHNVZ4L3B2c3BESzNucnZ5OWhSZ2RkYy9CQnRVazJBRlpHV1RzTkhMR0Y4SWpDYTVtRlNXa1F0SnJ2ZEpaSHVuSVFjdXc0Umt6dVF0bFVtRmZtc0J1QXI2aElwVDVaSm93dHJjajIvdmIvdG1hbHRJSkg3NXc2aDBvRUlWeG83RUJWRHcrLzFMdWtHbGkzcldESndKeDlVZk9mRENXMkdaWFEvWVlqNjBwRmpqRkNzSkRXSnRhc2ZzQWR5WnhMVm1VUmhSYTRGNXJseTJoWlVrNkdQUDZOai9JM2FrRGhSS0FBMUlxK1pjcHpuM3F5a1RrL3Z3Qlo4b3FpUUt1eXpoY1B2QXl6eitIckxlZDR2UnRUVE9zODNtVUwydzh2L3A0VjhTOWJVVkRYU0RmdS80eDhYNitDYU5WSXkzdXVGdmtRL3BEMWZGSmlvWERPay94b0pvTlhJWkVONGlFSFJ2OWFQY2dNWk9UN1pueTQvSXlhaUxJZTRoWG1iYytVbll2bHFmNGpBd2M2cXFOOHMxU2dMVDNyQmxKYkw3dmxiOGZvQXRFbSs4UkVlVHNIYXFyZmtDb1ZVWWN0RHhoUGxMYmZwK2VNNVpibmdWMHJwbCs5a3pacmJnK0REWmo4ckFrNnJKbUN5UHBxYXNVTDJGbzZVWUJBZWFBaE5IVytiVG5oWEkxS2x2SUpHRWpBOVZzTXNWbXEvWDlxTTJTdjJmUGUwRWEwUDhBaDdtd0VtNXBlbU5ER1hLU0Q5anJkY1A0ZlZpV1AzV1QySHFQM3ozZUVQWDF4R2grUVFWSWZTd0RoU2pnanBJcjIxbkZtcWVNaFF2azhNdWJWN0Fuam1SSWlNTVZwbEovMnk1SWtGblVPclphL0I0UzhpL0Y5Z05IWVNFMmVDL0dmazUwSGl1TWZOZ0tTdlpKS0Q5QTd3SHFOV3dtWStiVE5sdXk2MVNMSGRVS0JqejJ4V1Vqd1IvYnZXNFUvVGI1UzMwNW4wQXhSY1J0LzFnWmdzcVZobzZ3U1oyZ21aK01COHIzcm8xNGZRUEFqUlZzbVNPTUNUbEZucTVmclBrbGZFQzR0WFNWcm5pYnpRUHRPVnBTcjh1T1c0TXdTY0hEdTJCNE5yRDcyaEh2cnVpZFFIZlA0SkFNOGcwY1dHcURNekZXSlpuc3A3QlFmQmIrTmcwQ2dheGUvV3BiM0UvSGFCNWQzMGprUnNkeU9ySEdwWWFtVmN4SnFoTjBsbllRcG1udXFxcFZQWGpNc2VodktHZUpJQ04vNHllUStYY1JKYUx0R3h3Z3FueENqQlRRcUt1Z05VSytVckVqcUJZSUFuSzcva3ByeVBtekhxNUxSampzZ2ZadGhCK0MyV2xGVDhQdnJVa2p3ajFrVUVYK2VnTVdnVmo4bnk4Z0pyR09KV3ZyUkVHbzhkQlAvemFjVkt6bmNhQ3NZdjgzZldiR2pBT3FEVEUzQ3ZpM0JLak4vYlRXK0FxRWorbDJMSWpYMDRLMXc4UjRuU2xXMEx3RHVJYjlmaW9id1BEZEpGdSt2MTVzRHFSdEcwRmN5b1hqQ3Y0L3pYRE5aeVl1WVpsQjJEQkZZS3lCcW9mUjNQem83WjlQUU9NT2ZHbjA1aElNcHF2Q3J0OWpObXJIWHhBQlIwQTFZamdWNkRoUy9PbkJvMGZrREh1ZUxlR2pPUnNyUW5jK2tJZG1JYmd6ampIRjlsYmt5VWpxNHNpVWFyWUNuWU52dkdMYkRmRk1ZeUlPcUJNYmN1eEJvSStUc2RiNlJqa0hvRDVzWkovRXdKckViRWs1WnkrbDR0Z3BGalFjS0VqS0V3U0ovb01jejNDbEtMZGtDWjZnZGlLRWQ0cjN4QVBLVjNLZnROY1BjQkI0Z0F3a2F6cnFXdkVoZ0FGcGpNNVlHU05CV0dXUUQxVndIQ1RYVkJiYjcwcm42a1V5aEpENEtmNklQZm9RN1EyTk9jNW16dU53VGdDb01xWkp4OVBNOUVHcGdTUEY5MFBEWDZSOFl4NWUyRFVCb0xPakNXNU40cmpMOC90a2ZwM0ZWQ0JCQnlhdWdzQTNseFdhMHV2WmsySWJVWWIvTUVGdGFEbElZMTdvZTdOWlBweUJjV25aL3hwWCs2VFBub0hPRXdHcmdSVGVHc0N1S3MvZVdpVm1zRkZSd2x0WHU4SXhHcmVubTRxRmtLaEJqYlFrY254MlBUZDArQkF6VjZzS01kRWRCN0lDcVBLeU5iaS94M3ozazduT0dHc29xb01oaUcwMkFBemU1bzVSb0Y4UWZVamFaRk96ZEYwWklzUUE0c01IdkVoUmlWckE0N2gyY0o1V1pucGJtSDA2TGxzT1kvRmcrVU1ES0ZtTURDQ3BSTkk4d3RkajczNWtFUzBBbkRDVll5ZmRNeURQRTBVSWlxeHZzL25kdkt6ZFBza2tOd2k2WE1laGs2MWVsKy9ZRlM4TENaTWR0WkNUSlQvNWFkWTR6eE9PZ1phWE44Q0xNdkhuWVdoYW9vNThEYnl1TCtQU0ZBTUw1Nm5NbTl0Nmg0UUNYTjFsTXRyVkxheng3b3dyaWkySmVKUVI5UDdHaWlORnNDVGsrdTg1RU80N0dPd2xzMmNoQmRRaXVzekNoTmtjZXJ6RW1pUEhnQUM2OUkyNFhRcGdseGJ2dFhCZW45VHpVVWI3THc2b1BwWUx1bTJYcGtOWlczYStpUDFEUEVER3pSRFRyVndLaW5uV3FwYkpsdjJZMWZ4ZFV5WGcrZmVEeEt4Vm9JSXprNUFtL2h3ZnlUTlVybEVCUGNncWNCNzdXUkwrNlN0VVdqM20rRmt5VmxnUFo5QkZIRTBBNEd0cmtlVUlsM0t6VnJhQXdwVmxHb0psMlUvT0RqNVJIZUNKUnhzZmtjcm15UDZFY3BLQjVneDB1SWNRQU56NXF0cXB6ek9jVHRiUGE4SjJjTnRKVnFIbmFYbWZtMTE2WEc1aHpjalUxZGw0eVBVTVRickIvcDJZNXpHODVPRDB1cEFLckFrcitVanJ5Z1Y3WXVRUUQ0bUxTb1NXZVdFaGxodk5pcDI3ZURvSTVITW92eWtqL1hXeHVxV2E4U1F5RlJkYkJkeis4T2hidjNEMXk3Ty9SYkJRcEU4YXlRMFJnS040aUpYVFVmdFk1YUppaFN1N1VtTk1vVkxmemhFOFBzRDZlK3BmUWd6TGM5TnIwNGFhUXlmYitheHNhMWY2LzBXUzN3eWt0UVZHM1lpRGUwam9uYjBETU4zVnNYZEZjQ29GaGlsNVAyZEJ5eHZJL0ZBL3IzS3I2OVhwTmhvT2JIWWk0OTdqWFJvWVN1ZEJZTzBWUDhyQjRIb2tiS0NHR0hxOVQ5Zkk0VDdiNTU0emtkTFVUZjhsb21ubUNzUXlYT3F4Q3pVQ0JOT3VDb0lBNE1PRnMwZ2VlK0dhV09sQmNzUWwrcCtZN2x4NGtQa2kxUVB6NjVVMTFBTlhvR2Rsa2l3SGgwUEhHUEhQZXhSRWhlNlp2TzRQTkJGQkFqZW5HL3NBcFhKbVhYSmIyNHZoWkd1V2QyRTdVU0IwODk1Yk4yZlNsVzQxSkdNYTEvS2lWMGpNUmVuK0xhRnNrTElrNFhNczJibEc1ZXBQVkhobXVsWi9zVER4SUZuYjFlQUoyNTBmKy9zVWtiNndyWDh5MGxWOTAwV3Z6SEZJTlRqeGIwN3ZPZVppdlFmQWdUM2djZGJOaTA3ZFNDNjl0aW1TYWlMTm5GZWZacWZqUE9CdUVBWE5qU3pHaGtwaEV4UDMrVWtjbWE0eEx3UHFvVi9IMVZtUlR0eXl3SDZzNjkrVmtybllQYUtqMXNMQWhIbmhtb3duRnF4ZGZhLzV1MmxERkNEMno4WFgwdFY0NmhZeDBSMndBZUdrTVV6QVpLclRhaGloSkVsT2dSYVY4Rm4weEgyZWlGT3locGFSYUtMMDExVE9LNXZuaEFyY0txM2lZaVZLREwrai9lbmYxcEdma3U0N01hODhHUUF2Mk5KQlc3SytTWlAzNkYvLzJRQUJBQm9BUUFBQk9RQkFBQThCQnprQ0FBQUFBQUVZQWNCQUFBSUFDUUlCQVFvQ0FBTUFHVlJvYVhNZ2FYTWdiMllnUm1sdVoyVnlJQ2czS1E9PTwvQkRCPgogICAgPC9CSVI-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT43PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MVo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-RmluZ2VyPC9UeXBlPgogICAgICAgICAgICA8U3VidHlwZT5MZWZ0IExpdHRsZUZpbmdlcjwvU3VidHlwZT4KICAgICAgICAgICAgPExldmVsPlJhdzwvTGV2ZWw-CiAgICAgICAgICAgIDxQdXJwb3NlPkVucm9sbDwvUHVycG9zZT4KICAgICAgICAgICAgPFF1YWxpdHk-CiAgICAgICAgICAgICAgICA8QWxnb3JpdGhtPgogICAgICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-SE1BQzwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgICAgIDxUeXBlPlNIQS0yNTY8L1R5cGU-CiAgICAgICAgICAgICAgICA8L0FsZ29yaXRobT4KICAgICAgICAgICAgICAgIDxTY29yZT42ODwvU2NvcmU-CiAgICAgICAgICAgIDwvUXVhbGl0eT4KICAgICAgICA8L0JEQkluZm8-CiAgICAgICAgPEJEQj5Sa2xTQURBeU1BQUFBVWZwQUFFQkFRQUJSOWtINWd3Y0RCNFFBZ1FBQUFBQUFBRTdBRUFBRHdJQVFBSUFRQUlLQUFFQjlBSDBBZlFCOUFnRkhRRURBYmtBQVVkbkFBQUFER3BRSUNBTkNvY0tBQUFBRkdaMGVYQnFjRElnQUFBQUFHcHdNaUFBQUFCSGFuQXlhQUFBQUJacGFHUnlBQUFCdVFBQUFRTUFBUWNIQUFBQUFBQVBZMjlzY2dFQUFBQUFBQkVBQUFBYWNtVnpJQUFBQUJKeVpYTmptY29BQVpuS0FBRUFBQUFBQUFCcWNESmovMC8vVVFBcEFBQUFBQUVEQUFBQnVRQUFBQUFBQUFBQUFBQUJBd0FBQWJrQUFBQUFBQUFBQUFBQkJ3RUIvMlFBSXdBQlEzSmxZWFJ2Y2pvZ1NtRnpVR1Z5SUZabGNuTnBiMjRnTVM0NU1EQXVNZjlrQUJBQUFVVnVZMGxFT2tkU1FsUXdNZjlTQUF3QUFnQUpBQVVFQkFBQi8xd0FFMEJBU0VoUVNFaFFTRWhRU0VoUVNFaFEvNUFBQ2dBQUFBRkdid0FCLzVQZkczQVVONHo5RTlsc0hEbXZXMGJGemNhbmVoV3dQVUp6aDNSeExodVBBejJsckpVRnRZeHNmNnlmOHJtT2RSOWFsSTVieXY0aDFaYTlnSUNBOHBneWJJdlJsUTdjS0VGcTZkbDNVWndhdkxKZWdQRVE1VmdHeDJZQ1I0SlhhSE9vUXRVWDYrdUEvQWhBeUtMVEljblovMjZ3OC9oMjFYZHhQT3dNSmNWaGJYOTlEQlVyM1pBWnl2emZ4MVFBT2VlbmVUUE9BOFBURXFxZjFQbU9GMkpuRi91QW9WMEs4QnBUQUxhUmVQOGhJS0g2MWJvMElrWGxZeHZnenViUVV1NWtwSnhnMTgrVUVpb0laZGRBK21nQkh5U2c2T1VwanVPTkdKQ0c3M3hlcmdjUWt1STdNREorL2Q4NzZwSGpzb0RFNGp4SW9zT0cxKytaNDJ3NVdaeHhtZWQ1ZmgzUHoxT1dDRDdtU3JMWko3VzVpeTRLajVjVWdGUmw0bEVBend3QnV3S1NQWSszOGk3K2xjaU1hOU9jRFl3cHFlU3lkQ1VZZ0FQOFpaOFp0OHlBQXh1MncyYXlnWjgxeXFKRzRKaTk0TmxZWGp3ektRRktTaWxuK3FjOFB5ak5xS2o0SWFnQ2xPMGFWdU9MeEdCQTNPQlg2a3VCLzRmSUlrNmhNYzNXUmNlb1ZFNUpIMlZSYmJERzNCYTFkaVQreWNOYmZ5VkRaaE04RG43QUpmZmVzSk41WkVHL1BRRTVURTlkY29oRkw2RVBZZTVwYTlkeENWdTlVZFBxOWNPQjFSVTJRSEFJeGtibmcwOHR2em1MT1BQcFhGUFZycXVBZ2ZjM0JaaTk1VmgveVZPMUlKMlZPVkVWVGVabmhnZlI0NFBHNkxINWZyekdFVzFOcS9ZTFhRWEw5UEJqR2kvb2Z6Q1RCUUZLVGNsMjFXeUE4cnhib0g3N1ovRWFJR1hiZmVzR0VmUjJFTEtDVjZkRDlTbUFPZUFZd0FScnNwSGpneS9DWU1vUTZqWXZoaVNxQkVVb0dCci9HR1RhU3BLMmR3YUllb1FMUk8rRFJzZVpvSE9lNkdCMThmODRSSE5OR0tWQnpDck0wV2xwZzFWNTdkS2FaMkNqV0ttbGE4bVhDYlhOWUVnTkUwRmlQakZnTmM4UGg2UUkrbU1JVkRmSUI1YnA1ZTNqZDdNS0VNNFp4SGp0V2U0aThscEZVSklITVp4dzRUd3Bpd0RIc0tMZUloL1NiMkdCbVdqV2N6eVdFMVZtYnNxNEorWGpMcVNFR1JkTWd6NTBqUDhwMTF0T0NRWWpBeFRSTC81U0dXTG5TZ0piV2FVSHJYOWR6T1lWY1BOTGZTZ1UxUldJK2JpanZESmxNU21Dd0JCWTJWNVNvUXNLZVRkN1FGbnA1ak1sWjZ0OWU3U0ZYNDI0MFAxNEdiZ1pSNjJlaGVpclphaEVpQVdqay9jaUM4Q3Z6alVJYTZiNStuZmp2em0xQndiVFM5cFRlTTVVd25HamNhOXAyZ0pvUVJuQ1pRY0loWFN4b1o2aiszemdRZnIrZmVGdzZkYmRZUUZYV1EyZElDcCt4N3V4UlY5Mk9rOVRUcU5Rc2h1QXhHSWNNeEtVbXViTGEyODBqTXdkRGtkaStYTDN5QjVLUmZDQjB3YkZCRm9BUWlsMlV1cEVkY2k3YjlQSDhoNFZtc0VCRmNJeHV3bnREM2MrRDAvbWFhYmdkOThUQ05qUHVnbjQ2aVFSdEJjMTlrbnJsQ000VlB6bmw4NkEvUWg0dzlZS0VCanZYS3JQTkp5ODRwRDhOeWxob21jUGFDdGd4WTJPeFo3M3lDbGdMckgrSTk4MWRMTkkxSk5UM3pDbys4QnlYeU9OUmxROU5hRnpiMEV6MlBpUFNCMi9vN2FoQk1QdUxMWTUwS2dWdG5YR0xRN2RXUVI4ZG1MaE0rS2tEUVBLdE5tNGdOd2FPVHFWOTBXOEFJTzdabGlSaDF3bTVZK09WbDR2SDQ0M0ZaYmJyckNyWlEzVGdYWDdIUnVtTGdJbnE0QVVYcmtnTWdYd3h0QnZmMldHNXMzSHE3R1R5bUtobnNXSmlMRHk5NFBWYXZzYWo2QTFQd01xQmNYczJsZGVYOFRTM25XOHJrbDV4TXVvR3RXbmppeUtOQVB5K1RrUFhBT1pBUEM3ZS9yQUUwbFRDOHo1dlZIQU5lZWZFcitSRWd2OGpka1FHdWs0cncrREs2c1VNL2lRbDh1bmtubDUrbHIrQzdVeXV4NFJ2a2VwM1lFOHBJY2lVeE1vb3pzTFJzd01UUitMOE5zU0x0cHJBVzZ4T0ZlaDhyVVdneXFiaFQ1UE5QcUR6YTB3TVJuaGJRdHU0bTNqbHJPTnhOMmp5ZEY5VjQya2ExUXZjaFRPRDRUUXVxY2d4Uko0My8ybE1Td0xQbzFPZ1JkZEZXNndXajgyalBtVUFOMzBleDlDaThBUHNRY0RmMjBSQU1OVWNQaFI4VFpHMHdrMzNJMGgvd3l2Wko2ZXphU2JtOWtRWUFwYi9NbFdLeHVGT3hDTTJpUEtYMHVSdXpWeWc1TjZFcHJ2Z3laN2JCNjIxZDZqZk5YVGNvaU8yc28yZEVmMVQ2UHlBSURuNUhRQ1lUTGZyQkdOSTB6c1FoNXZ5aVl4SkxMWlQ3cHZUcGswOG5xdG5CY1M3eEt5aXpFdG9LSFYweHZETGl6eERtVWgxcGJhb29mOVBQWlhPMnRQemlodDZRTzhyV3dtUzBJTTgySFRDR2k2b0d1bDVWeXlMSGxSQ0IzRENWdXA5dGpiQkJjR1pJRGxjUXVjeVVTdHZtWTlTOWJOdG5KQk5UVnlmcW1ScG9yamVrZU5IbjdIVjk5MVdOMDBSVVVDQTZ2ZDgwL3hpYytKZUJXNDZKcHhuZkFFcks3amZ6Z0hmemlxOW96WC9GQkhIYzZ3TEo1WUJDKzFzSVFNUU15Ukt0T2M2M01SS1cwajRDdTVwSHN3cXdFdEg1V1BOVXhHaUtNT0R5ejFOVytRQzArcjN4Qy9QR0NSVW9YSzJ0dng1NTltbDlua2VLWDRjNTlRdmxwS3lOSUVzTlF2U0E1TW5ZU0pHUVU2NC91bE03WFRJdTZKRXF3b1p0N0N6UWR1a21pNzJKK3NzSHdVS1Jnd2d1eGlpcmRveTlmM1RBREhjRld6MnB6ejU1bnlNNzVsQ2NVcVU1dmk1RW1QOXRTVXBGZjgzYW5qM2tjTWREYzN0MTM5Wjdpdkc3U0dqbjVGZlpVencxUk9TbE9lR3ZRNncwbURaczcxcDFXRlZTaFFHUmxpSGgvTHd3bjIxOFhTeXM4a0NvUHlwcVpzNjlSUVNYUlRYSVQwa0dmd2tMazdyWEo1VDhWY25pRGxpVUh0YUZ4eUQ1RFU2c2xUd2VVdWs4NUc3aXR3MSthd1JvdCtqMldqc3ZZRDNRd2RXZHE4QmxKd0c5cVNxWGdKOG9CNm14WGpXS2dKS1dQdDBiMHNjZ3Rid2hud0ova09hWlF6SlV5YUVBaGVhYWVERzM3SGtpSHlqT3VBRWxPWlNXaHhmVWNMdUEvSCt3d3VPeHptcmo5djFGVzFYNXZqOElTZTUvSmlzWEw3aWdYelhIVjF4Rm9NTGVlMWFtYjJ5NHBQaGtuSUprZVQwSkhyNmVlZWFnMzJzdVRiSUVqR1RYRWc3eVJUZ05Xa1hsZWg3SkY2NVQ5RTNmd005TTZ5aDRiUExIRm1JVjRDa1BMU3hIWG9NbXVzeVlGTnMxemxzQ09ERHMxMWtjMnVjbkxSL3hBSGorc1dFVEFLVi9kZ25PdDFqZVhNWERoLzdab2RPeVNDTXBXVnBhM2dZbDc0clMxelBIQ3laTWtMa0FkTXhJQ252R3k1ZmxBcFV5MmVWc3lJYjgvdnEydTd1RnU3dy9KbDdyd21LbERLTlN0YmdDdmlFamFMeFZJR2JOdmhwMGJtSGlTS08xWGJ3eDVodEdmdSt0Tmg0NkZITXR4b1gxOTVLbHIxd3ExUEYxdHFTS2ppVS8ydm1kZEpzd3pST2s4YlpwN3R2bVBiYVBIZTR5ejVnZ2hURWY3WXp5eWZxcHptbDBkZnlxS3RXSHYzOFMraUtNRkladHNNWG1RdkJEaXVJYjFXdS9YRjBUd3JNemVZSmR1NDR5TnBpdGh4NXRMV0lZSDM2ajIzcVRkQmo3ajJtQ2N3TVZsS1B6L2FTanVmMmd6T0ZUeE01YU1odm01ckNzbENPdFNicU9ndlc5cERZZExreXNSZEdLNWplZDYveEFuWnRKd0R4T3Z3ZVZ2bVhWTFZYQSsrUGZnWVZnUEVDdzJWaGNTTGptNnBnSWI1OTRYTTZFYUpXbmJoSFFBd3pCMlFYbTRzbkI5S0hKa3VmRUhOM3RqTFZwOWEwSGpVa3RQZ0l2bE9xYXI2OGNRWDBZK01IN1R6VU9WcTRuelo0cFp3bmg1TlZKU3U1VE9pZWhZS0l6eG1MTk50TnNYMWhOUkMzUndTVndpSzhxek0zaGxRNkk4TC9CNFpsM2tjbVJ5Qm45NVRINkY5aUpsTkIzS1cxclNCRlJDT0owaDZHcjhKYjlTejkxckRSTThjNFRGYlF5M0psSHM2U094OTI4c1JLM2ptRFZzMXRvSDR3L1BOciszRGJ0bzZKK2ZwQzI5UC9ienlEWlh4d2Npem9zMmNyaTRkRzB2aG1uTWV0djNCOERDZmZ5RHkxbHFuUURVZlpxanNUTkZxcDJHRFZmT1VEbXhRajBVUFJZRkxmekZ1TmEyZWZiRHorcGFvVW9nNzdydCs1WUh0ekE1d2tQR0VaTks0dTI1TEZnM0lWTWNpRm1PSlowd3ZjVE8yTUtLS1d0dGdmNDNtTzVaeDBXVnRaM0VyTC9WQzdRMHJGdy9rbGNSNnl4QVZWbHNkR01OOUxsT2hEQ2FERCtmcnVycTZ5eVNqN1BhcVZ3ZGdsbnp5RUZZUEZsR0I0bjRkTVgyZUgyWi9SUVdVWW9vTmxucUVGNHRpL080dzRIdHRWeXBXVEVEVmdSaVM1SDl0Y1ZkcDMyOXlpYVdkM2JCRXkvdTR6WEd1czBmaG5PTXZSSTJtVGx0TDJlM1daUXBNbzZSeGJsTVBiSEZZNFZoS2xvLytSMGhHUzJsU2txeFNSSUtsaTBVbkUvMG53WmdManlTc0NDK0ZUYWkwY25FNDExY25aK01HVVpuYlQ2REwvTlA1Q2JKbm5BRWlTYjFDbzl0RGg3MnZ2YXIvYXlFb2tMWFQ3Q09kUFVnZHVFY2hWRk1JTUxSblhzaVgyV3gyaExVWmdPVWQ4bWY2SGhPakZOeXJ3dUZEQ3JJNC9WOXYyNGJBRlpobUxpSlRuNURpMVhoRmkyemt0RnM3cXR4ZTJNbjN1MjRlVmlBc3M2S0djc1dka3hsd2Vkb2pWcFhlTjN4anV0WnBpSTAzdmFzTStVSzJvUUxNa3RKNFBKZ2VPc2FxT2xhOVBTakhjWkpuRkNOVWpFb3JlZFMwNTVHT3R1bm9EUU42bVhuV1ppcHp0NXBKb243bTU1eWVTekxuOVoyQTl6UnR6UERENUdMaGZnZVMvR2Y0Z0NldHI3OXdQY3FlWVlNRlJURkJiVDA3WUNFRDl6L3VTWjR5dG4raUpjY2ZBWXJJQVB1anpYS0RUTmxYSFlzUUZMS09scTlDS0oyZlFrMXhVcjQ3ZXFseEhlSEFFU3RueXUxOW5ybVVTZDhpQTYyU1BZVE53dXBKUCtjVzJPczYreWRLMVlxeUJ2VkRUR1E0SklidGNUM2JIZVB1VmtYai9Dc0xVVXgrMzFUTzNxaWs3d2F5K2hwbkRPZmVqUFM2RGE0cUI2OHJrNzM3NWRrcjUrbklwdE9LVHhVc2svMG83ZlNrMzlaTklBRVJnMTVWbkxoN1NhMGtLcEZ4TGx2WEtaVTFvWDZTd29mTmxnK1A4SEJDQ09nYU1DWDFQbjBKblYwLzZtWlpzZ1prazc5a0JVamdVUG40bnQ5MzdKbFRkeEVYc0dNUnk5VlBFdjFFUHJ5QWRSYnJEbmY5MUg1OE1ZQWtKSUpTNjBibEJDb2FxTThQRWNtTjVMQ1hWK3pGWDVVcFJxdzdianFEWG9YT0hqNUc5WTFyRDRIUWl3Mlg5ckNkK2wwbzBvTzhtOGU1b2xqQ1d1WTM5TlArWXMrTExlejhTV0xJdFhsYzArTk5rNngyME5jZzk4VWxFM00rQ3RQQXk1OUV6bk1YWkNnOHpWKzRlWno1Q3hwaWlVMVJRRnN0OG1ialpscnJEbExjWTNxVjlKa3lWenZNV1N6eGZ5SFNmTEVhS2pzRmM3a0dORWppWk9Lcy96MVV4Y0hQN3Rodm0vZUQwT040UlB2QnpnNGhRR05VT3huYUZjRjUvSkVLUXJiNUxFcFpGaHZnNlBxRXRMUTFCOGc5eHNDc1d3UWc5d1ROYWFBbXBKZnM3RGFpajIrSjNKSi80TVJWNlR5UFlVUjVYT0dPeFV3UHhiUHJnb254QmZBMDlWd200ZHh1Qkk1OHVhNk5OUkxkNmxabThSSnY4ekpRZkZCdExGZ3ZmaXg0MzVIUE1XdmYzU016VnNyeEMyNW5hVjEyc0hxNmx5UVpBNTlTd255SDB3RGtQWXdOM3dLM2lDQmVSVzhSYjFDNTJQb1ZObElROStGWmI5Z0RLVmx6aFN1RkU3NU9DRVV5VmRPTGg2bkE3S3QyTXlIQ2Z5UFU1Q2xHblFRMVBJQW00VHBTczBaR29YU0RlQkxtd1FrZitOeDcxNEJYY29rRUZXZ3ZoVTZEN05VSzdwR2FrT3pncTZyck80VFN0dVJUbDIydDJwQnUvMmxpUmMwRkNQODk0MDVRTWtMR1hYMjNEVzN6eVplak90bVJiVnh1Nmo0ZmZlZ0xhcVhINXFLT2xacUt4SXNCZm50RU5GRkwxazRuQjdjdU1taE1UUXNtMWpsZy9RdUZtby9YQW52RjV6dUlINkhURUpjUnlBNXBZN2pDREVhWHU1WU9zejE5Qm1hUVVpRDFmVm01eWdRWTA2dnQ3d0dQV2VDUyt1Rll4cnVJeTNJYXpNSURWY2NJdzk0V2t6eDlUS0IvbUtQc0xtaGxhend5NVhXMExjVU1FT20ydks0SXlCN2N6TUNQOGtVM1JVSEExbGtibFBGV2tpOXVkUWRaYnljVGZvMGFoZHFYQjUwYzdES01TZU9ZRWFOMXNrdlJScW1DT1dQemRmam9BUlF1UHZoVlhYVXFHVCs5dFNBT05QTlVXNE9MWW9Vd1hKdlJWanI2YXlLUEkvbU4yZ0hPNGlRRFdkLzlsSkhDRVRLLzhXd1g2ZzJIOS9PZnBDU2tJVWl1cndsTTlsNmFqOUZ1OTN5d0thVm1RM2Z0NUxJVjB1Z0hiZ1RwMWEwVU5iOTRtakNWUFl6UmRPcGhGbE1uakdJSmIzM0xMcGloTDhVQlgxbUx5OTZBbW96RzRSZVpsMlhrZUdXVStVbmxkVzhMRm8vWmptUjhQNzFucWVHK0xrQmcwd08wMllNdjh2K0NiOVhZWXh0U1NhRmNqKzYwYUpwN25xSGJ0dTliNmFlV3NoL3JCQzhpWllmdEhSMUVzb3JjK05KV2tsRlRwTUZJc2JGMm5iMkZoVDdOTFppTEhQOUVZZ0V5Vk1Eby94dkhzVkt2TkdhZU1Ec043OXdza0hkZTBlL2lzeUlPQjFiUTJMVG1XMHZnalFTMHpKN0czRWVjSWlYUkFRZTJzWWtpM0VjNGdrNit1OU9MdCtTTzM4WEphRzltNTFnMlNEMWNGakZKSUltWjQra01OTzRMc200QlN5QlJLSy9jK0Z6S29mb3dNeGNNQzc0aXdyaEdJSkRRbW16RWx6bXo3T3VvdEo2S3JUeWhFWk8reG9wRGFUZ1I2c1VkQVltN0ZscmZPRFlSZ0dtdkRDOWtNcE90N29SbzloamlxU05kUHFaa0NoQmRiOS82VmtGK2FvdFlEZ2I0b2ZHRW1WVTVMdnVXVkdYaHNDOXlPejBuRVBFQmtIYWZPNERwei80YkZsM2VSR1hzRmJaSXRyUks0NkRnUUkwb2ZOZm83ZjNCeG1hcEsxdmdsQSt2ZlhlV1FBL1VkRGJvcVR0U0hmTlkvTE9panU3RGRna3pSU1Qxc2dXdE5xelc2Sys3SUtLZkxwNzFFMUpYUDh6VWw4VHQxN2JYVmEvOGJLbU9xcTdYLzRFeXBaVm5CbVNSNm5tbkM3SzU5TVNma0ZIK3k1eUpJRzRTYjAxdnpmbjJKMU9mdWVud3F5VjVKK2JOcU1IWHlLWTRObU9MSFhOc0pqRHFzLzhxZXRwQXZpdk0xVCtMdms2VjV4Q0M0SVc2Zk93UnA1MC84QUwrT1F2R3ZJdHJQSEQ2SnZvZGVRNStjaTBHdHRTblAxTnRCYko2a2Rhb2RSZmZWVEE1UDgzZmgzQ3U2MkZuMDhLQkFmc1lpaTM5WDl4QitXdGsvTzNVZmtPcE5aTjRjdXhQbitmNlhOcnBYQVNDZDlRM2lwWWFwSHBzekNWVUhSaWtkTGQ1QWNNdHRyaXJiWEFubzMxbk5rK25ReHpJRld0MlFKbkExc3VSOWpreFo3eHV1TXlTZFEyNHEwV3ZXZFpHS2NZanhQRTcxd0NjcGt1VjVJTE50S2RWRWdXaVAreCszM1lKVVczQjFsc0VncFBEQXZjZklObzcrMVJOcGk5TjBXRG9nR0lLOTlFOUt0UmdDOE9ReWFXdDVBS2xBODJzVFByNXl1R25wV1FaQm81T3FVblhlbVJmSXNqQzFUSmx0ZkVxVTcvVXhaUnNYMU5EVWFNZDRaMDJNZHI5WXEzd3d6NzNWbzJlNWp2bmNnY09aa3NrQ3NLLzV4VWpBcmRSa2xXL09idXgxRVBpQXU5Wi95Y25paFVQWnN1SGU1WlJTYVZlNUNVZTlaVk5JTldiZ3VXRUNGeFlCcHFyMjhOdGhDQjFYMVROMDlUS1M2c2ExOHhSSkVFZXdjam1lQWl0SGlBdUZyZUEwak1jdHFtSUc4YzRrdlhNWlFJc2NUcWliUitpc2JVQ01SdGRFMmpDZiszZ01MdUFnV1ZVY1dVOFV3U0pDV2VFcGNzT2xRS2tNcDBrdzMxYkJOWHEwUE9RY2lEQ1VTWS9aakMvbklQRmcrcGw3REFYUk9lektxTVYvUWRLdzVWTjRPajJ4V2xRV2NtTHVLeUFEVUh1bHhSem01SVFZQzZuVnczRDIvR3djbFRJVjgrSlExSVBWQjJmTnphdExrREgvTzlRRGlQSmhBYXg3YzNibHhzcG5TdldUTWowUnByOTVsVzhValZzODEyc1FBLy9YcFpqUGVXZWRZSlRIcUZ1Z3FzT1l5WXJJY0dBaWs0VU0wNGtGQWttRWFMQ1ZFWHl4TzVxRldXN3pIRmYyeXhHa3RsM3lFOS9PV3ZGdU9RSnI4UlFqWE92bHN4ejVDTDJuOUFUWUpkZlFjejlLdjlNazR4dm9vNFJrUEtQV3dIeHVVb0pEK2JVamk1VGM3a0FwczIyQ1ZuWEtEamxQZ3dBV2swbXFLUmxsTEhkalp3QU1lTlpYOW8yZnAydTFlWCs0V053aTVyUXZQajR3cGF0aXRFOExBb1BMb09POEt1Y3pqMnlsb2dDMlVqS3ZmcGx5U2lGbnVsKzZmam1IRGhlVWpic2ZDL3lTam1CQkpVNVM5ME5vU0pZdzZUQ2ZBZUxXU1ZjZnhLeU9nMmVOUGI1RlJOVUp2UTR2MEgraTdmdEc4S0xZaG55akU4cWd5aGpqcmVCMnR3Wnc0aHB5ZTNIbVp5OHBzNUlrT00wV01qS2NpamdnNjZFMTgrWE9wdjd0U2crcDFsTi94RG9qYmtia0JPaVg4OGVMQjZFUjRoMWZkNXVlWElQQWFCWDRPNlEzQ3drM1BSMlBBU1F1d2p1ZGNaUkdaS2k0cDNnQ2F6dHNVZnpqLzdKREpFbGhWR0NRTlVZbm1aQXVDNnUxTEpqRmRGL0ljRmpxcXJRK2h1cFVwUW9FamQrczRKU3ByNE1sTnoyMTlad0gwVE0yN2RzN2xCcjVMejR4V3I1aXc3aGNReFJDMnNkZFRjZStCSC9ia2svTEtWRUNuSkhKcVlzc2FoRnJ2emM2L2FRNS92NG9YOG8ybmxuNCtpRG1tQXYrOXRDellIc1FwUkhaVTg3N1JFK0hzQU9RbGlLWnFDVHJyTENkRjArN3NhRVVlcE5ubTdFajRCM2lOQ1g4OWNOL1BUZGcwS3dPQzBjQWtndTFubU5jY0xhRUxJbFRtRzJFVW0xdmtjUDVubk1iVGlYdHM1eVhNWTFHRWJiaHJZR3VPYzBFM1dIeENMMFQ5NlVSM2MyZ0xNRG80Y3Y3Q2t1MW4xdFNPNEQ3dWV3aHhZTjZuUVRieFUzNXNqVjNKU0NqODgvck9ZMXRBYi9mNnRMR2NyS2hBRXBBeE5PVlZyYkNBQ3krakhXUG54OGNkVUgzd2trdFJOYkJNYzNJK3MvVG4rRFVSVkQyYm1uelcrT29WZE9kdzlTUFE4dGx3Y2JFMkE1bDFqVndjL1NDbGduY1hYTGhCNS82cnh1ZTAyb2cvYlZ0YWdzeGt4QURnOHdYcEJReEVadjMreGN3dmw0L3lMSGtmN1VhNlpDYlRaQS9aRzN2V094VlpFcElDZzd6SkZIdjR6bHV1RmlXZXNNdDRmVHFwZDhtTzhWaXVPU0pqcVRNOThOaVpXT3pKY0dSNTAxc0dxTllqQnZuS0pOemt0bzdDTURJWDVOeDFoVGJJZDl6UVNuTDZlZDlGY096WnZyZXZMNlBGMkZ3UHJWeGZnTWpETjM1MjNZWlBOT2VpNlppalcvL1JqeC9zWFVOZ0I1OTh1Z3BwY0ozVVVQQnZReVZRUGxHYmJXQVp5OFh2eDNubndDM2RBbXMrazJSMTVGNWs1UmprTWFwRHNNSXZhaHRwZGRLRDBoL3lvZGg3V3RlOWQ4N0RsTHYxZjdGMmI2MTVOMnA2d0VzUjB1ME5NdTVKR2t6NUdoRGU2M0lOVVB3M1V6dU9xcjNqY245c3NmMG5qbUQya3BNb2FlMUdVaHc5ZktZNk4vWVQ5MzY0OWROaTNodVZXTllwK3hGbW5weXdIbmZPWnptRzBuVkF1U2Y5MTV5VHY2bGt1WG02OFB3aGVlOUN0REhOLzRUaWZTQVhtQmRxMXdzYjdYcXIzNmVYL1J5bXlQS0tXMlpNVmJPTUs2TkFVZ3lLVTFzRHhLL0xTMDU5SjFQcENRcy91SFZiZEkvMDBreXRXcDc1Ny9kcGhDVGlQenRCcFNlbWkrT1lBQmdtT1NUVTg5TTR3b2EwYzM5b2NhZUdEV3pGd3dPeTlMb0xTR2QxZDhGRnJVRU5rOEYya3ZqMExUQnltbXlCSk5YMUZBYjJJMmowQVlxVWtKem9BNnFvMVhkajVWdURRb1J4b21PeWJpYThLdjNmakk4RDlyb3Z0WG9jMkJqRm1FWTZvN0dreVZPek5md0llVTdhRDZvbWhzSk03ZXhsaTdtdERkU3ZuY3FQdHFqZUNPZC9rSWhhcWVFVnIxQk9taWQ4d2lRVmh4UmV4SmdQMmFrQWtNSm0yMXRnTk0vSzJyREpDVWR1R0FqN016Nkx4QTNlTjU1NVJrWFlmRGtVY1JQdDlQMTNJK2ZRMmkyTXFlQ0p5SXFvVVlrVm43S3FEOWNKdlZ0WWtZRHh2MEk2cVhVV1dhd0VPUjErMWg5cmlmdmVSQlkvb3JNS2xvQTVFWVhXN0hHYm5raTJualh4a2YydTdmTXpPVU11NGR6Nm8vckNZS09GVDgzVDU1bWxlQjFaS08zSFlhK1Z3d2x2TldaTUdRdThtOHJyMEF5bjc4eGh0Q1IxWXR0UUNxdy9OU2o1ZHM4ZjJrang0MForVDNzZ2trU2N2UEt2Vml4MkFWVm0wbEJlbWh1SnNpQUZyYjYxVVF1UE9OWHlHa0ZWUFVWbFB2dmF0WE1YbVBIR3l4Tkd1NldZd0NBM2pMRWFPMTBiT2lpd2Z4aXZscDRDZEVOUy81RXEvSXN3UkpKRlFjZGltMlJKbzh0OUZyMHRaZXRwcEJYNHVEWXh3UGJGK3V0RXV5QUxHd2VYRXR0M0VmY2dUSHN1OTNuOUdQVEs4TitGaHh0TWM0Y1ptbGhSMW5NMi9sMUFoR1A0Q0FnSURtOVdidlFKdjBWK0Y2NDZZVnJlb3ZFdVR6Mlp6bDFqOFcwbGZHTVlUUW5BWTBpS25semVKUXp0cWJHMXpyTjExcGpVNEtzK080c2tTMEx5UmNTTCtWSWxPcmkyeGxvNjFOTzMraHZNdjVxeVhiSm1WdCtIKyt6SXJpUnRaamhRSVNhTlZ1Y3ZVMHo5S3dZK21MYmNGQ0dxc1J1NVdRWFdyTkZHdlA1L3MyR2x5bEdVc3o5TGM1d3RiUW9aLzJOUHYxbWpLa3p0d050dVVZemV5ZzFScHdVcERSU1hTS0xVZTlxamFsZzlta01xeWVnT1lUeVdPSXFkVFhINkdPOXlrYjI2ZFhMd2UyYWZrUWora01hSGMzVjJRZlBreFJJaEhZTlc5MllrZ1ZHNmoxa0wvektYWGVTalRXVGFhT0tRaG1aY3BTSDFOZzZaK0tscnNMNTRQRmlkVlprUEVBa0JvRUxEdGNyNWRNVFUybnZSb1JLRnBsUmpPYzZyV2x2SUtMV0NMbWxOaHRFMzZmYnpJZFBpb0xjRlo2ajYzdjN1UXd0ZnQxODQ5djJNUU9KT3l0aGtZVTZROE1IYTIxUzgxNTJSYWIzanZhcUZibG90aUpMU3lrSXNYQVRyUVJPeWtUTjRGS1FKb0crczJDeTZuWlBjNnRWT2FIckREY08wcXRGN005YzFwNTFETGRRbjUxWW5Yc1BvR1JOU3ZMWnkxaWhtdnp3U29SU1A0MHlmR1Qxd1plYUR1VnFlK2l3RmdTSldTNnhDNk9rK1hCMjdwazhIcEw3dzNKWUxhaHJwQjhzd0tEc2ppN2pjNDBsSTJhZEZZaG41SUpCZU5CcFZWUHQ0UFQzV1plUjF2a3hMY1U1NVNiMmpIMWU5cENhTjVETkN4dDBaWEdlaGg0R216ZUxHbEdubVpHa2c5MzEvemhjWlBPV3BqWGhLTUdDL1pLcWc3K3o2Y3AxZGwvV3Vpa0QxTVFITGlBU1JsWHVPUFRsTUd2Rjl1cm1pMG5EaTArc0hBUC8wUHJrTk5aU1N1bjZTQThBREpzYXM3VjVkcWNFU1NYYmJWTXZmOHJ2UTNlNnRDN1VoZC8zVmhCNVV2SW5tK1Y4SzFhTEg0dGh3WVpFekJrakVSeC9SSmhCVDRSYjdUbFJ6RmhUSDh0cmN4T2dPK3RMTU80eFhxa0hFbDBTTE8wN3AyRy96cG9XbTVPc1dNdllwV2xuMks2cHdOT3pmOWdYZDFXL0U5d1k0amh3a0dSajhoKzJSdHh5aEtSMDJqK2tweHo2VTRkdHNnMysrTng0Q0w0UncyT3dDMlBtVGM5YjZmVmY4eW1nVFhOWEJHZi9LNVhHVFdYQ3NjZW1odDFaUTAzaGttTVJFNjl6U0xZOWVwTlE4YzBSZnREVHNtMW8wTStIZjhLaS9wMENBWS9CMUtKM0pZcnJkc1lyanlsdGM3bmxhL3U5cXNwZjhXU0E4TzAycjRmV2lxUXg0eVNqVjRJTzB6YUhmMHVqZ0E5NlZ5Wk1WRjUybGNGVU9IcFVmREthQVVkcE1ZaUN1bm9mcGxxR1JqR1JnTHphcTBVempwSFRhTjljZXE2OVVxeU5DTU5Wa0xwUDRBY1o5K1NvNDAvUHZHWnQ5eHp2Z2o0L0U0bFNxNWNvUTZGWEd5QU9scXJ2R1N4dWJDalNGUHRPZy9qREVDR2FOajFackltczhuU3J4NzNqMmZ5VEwvOVJ5QUNDTVpXQklNanVjd0VUeWJYaXZLUm0ySUFFTkh6dGlWenFiN09MSnBlQzlsaHpPL28wMWQ3c0lrRVJYZThPaVR3QmZTZjZNc0FyMzcwd0JvaUVybVltMWZJY0Y0UDJmRnF6dFpyc3BUckN3OGIvbjNVUWZnUFM0N28wc2twcjNNM2tMSTV0VkhjZXZpK3plWGl0bjRjaUFrU1ViMjNrOEFqMllBMnNYb3lmZk5QZndrMVYyeHA5VEsva1ZRbTg4TjV4U2VDdnkxVmtZNFoySUtzS29WeFZTYlMxWUs4ZUFlT1k2WUZVdTY2Rmxua0lRU2Y1eWNkRW9kSjlBZHpkWWQyTVBnL256TTFFZm1FTlg0MjdhSzgwT0pMT0l3QllJWG9QWTBGUWZpbkJya2pvblY2aU5MSVZzYmVrU29NbllzS1YrSVM2S1lhaXptYmg3azhOTnhLTEJ5ODdzQ0NES1p3Z0lEbjhoNW4xQUlnamUvbkNTSjNlZm4wYmhYWXo5Wmp2bkRWdHQ3akh1bFhFSytoVnVSZVdaZmh6TmgwM3oxVm5LMGt6TDgvSktVSnd5MC90ZmFXQjVFSnZZQy9mODFSQ0hnNVU4Y2JwVWlURjdhSDlrMjZ2NGFWK0htNUcxMUZ6TzhZMVhtTXhmYy9aRnlab1RLZmlWZEZCU3lkZUtpSE81OHlDZnVWWXArc2RPc0FTUjlpdUF6ckVTd2dnRlZlL0I0Y3h0N3pmelhjYVlkWXRhN01qLzhyNlppcjVGRXJ0ZTdGZVBncXJUam1ZU1N3QmUxeWFzSEcxdFdoM3dNQkJsMzlQR2RiYXZ1ZXBhYnF1NmpiZ1JzNUlmQURCRm9kRnNFc3Awa1dkYkR0T2RpNTB3cG1JbXZWZDcxNDJZYlBVYmVlMk41Qzc0YU5sU0ZMT0VGZlVVVUpQRk5Gank5RUxNeCtlVXRUejJCZDJrQzhEM0pTR2VrTHBBZUYvMWR3TnhHdDN4dnY1OWM1RmQwbXI5aDF2T29qdTYrNHVBRUJJUDJ1N0s1V0VoZlZ2VCtWVXhqZHdKTHFMbG9ZV3piMGNYMER5NHFmNzBWRzhEcjliMG9UZWZyS0FXK1JSOUp3WEFCKzNDc05SVWVUTFZPdWIxdXduektaSFRzdmJPRmh4T0hPQ1RXS2JNOWttbmFscUREYmxWUWU3SFpISzkrU0tXZHpIL2ZoM0FhUEFEekdCRm0wRnRWdnE1bVZzTXNZT25xN1VWTUpuSWd1NDVBVm1VZ3lOWVA0T3d6cFY3SHU5bUVSSk5Zb0NvZWxhUVZ2Y0tTVExPaE02VUU2RXFDL1IwQ1dFdlJ1M2l6SXRjelhUK25CVUI3TmZtZEJYUElaMjJ2WUpuT3RwUWxmNmlwcmN0SVBlN2xrR29uWitIZWxMVU11UXE0TFp6RzJCZXB6aWRqVGIzME5WTHMzNHpPYXBmVTVRQU1Jbnh5RFhSUDl1LzlHcjBsTHlxNER2QzhwYTd1Rkk1U2thMGZxczlSdnVWZ1EySjUzM2M5RjNOdnY2TmtJVmpCTzVvdU4xNVpPeUk0TG1RcVloMWZ2ZVl4V2cxN2Y3eEExRHl1VXJ2cDNiVythOHBPTjM2aXQ0STFXNjJIQzVidmVSQzZGVGdWeEVDd1hqMTRHSEJLTGNBYVJsejEzMG94anZGeVdXazBvOVg0eHJzRkVxMGF5VHRNWW9NeHpwY1NzdFFiZzU0WlN3REtycDR5Zm1wQVZmTWFXZWFRV1J2dkwwY09mM1l4RmY2TnJwTUhHNlU1TTVaZzl2d04yeklPbzRrRW41SjNzZDZNTWxJM1R3V2Fla1lhZWtXdUpnTTNVZHUxZk16MFhrNmYxQmZodXgyVVFpRHBqc1FleUU0NEgvMjBrVmt2MTJBS2tQZllFS2NwdlRwaXg2OGRtMmxaRU43ZmZaaGxBOCs5U2lrSnNjWThhMFVFSDA3TitadVZGS21vRlBHK0x6dnF1ekE2ZlFzOEtINkM3cklSaGl3ZWN6OWVUR2lWT0gxRkRqMmI1UUVOMUszcjBvTkJybVMrM3NwYmQxSWw0cStNeGl1MmN5SU1UaCtrcEhPVU1OWW5BVjMvT3NtYnJpaDFlckxzUEQ1VEY0NzVqTlFXbnpMZ1dyYThrVkt4eUZIREhOaCtDRU96bVRqaHl4L2RhTnBKZ3RKdjJVZzZKU1RYZDhXZ1FRL3VSODBFSm1KcmVSN3cwdkphWHZ1NDlOSUxjMGpTakkyQ2JkaEljdU5VNXFuL3ZYTHBnUTY4TmVudytjQlN2RDZGbGg1Wnl2am9odVNYb0xBVGdsU25seUJNM3cwclpoQ3F4ajU4TEZraHNWYUluKzAraUdLN0NodXpDMHUyV21zTWc0ZTRqbjBlSEtXc05pMFp0RS83b2NLRTZ0cXVlKy9qOE1QejFJNUxRVWFnelFhOS9IM2g1WXhsQ01kWHFjTjcwMk1TTzV1TEVneFU1cVlBckhEVjBudDZDZzBteWVBRzAxL3A5bmFFSnJZUDJrRFJjd2QzMnA3TldOMW05bnRURVdraTJjYitpU3dxQWNvSUlaR1NJNnEyYk1mVzZ2bXJjanExVGtTK0wyOUFJbkN4NnlLRWpHSnRKRm02SVE1dkc2d1YxeEg0VjdNT0ZzNGZneHpnSmkxTFNGelNyWDVKYTllRkJ2bGlkL2hVRnRQdWRwN3o4YWZ1M2lzOWRWNElZUnQrMnE0NjU0RkdLNlhmY1ZYRHZocC9oL09FTDFLZGVZQjRUNm1uaGJTZjFUbEdoSUk3M1FpdElyMlZwcWdJd1pSU2pPcmRuVTcwYzd5dWgxbzVNS0F6V2t3eWQzNU9lUW0rTC9XVm0vZVAvTFBJUU9pZldWM3o2aWI3S0hXRDRTZnYyK045cEovRDZaQ0Q0UkdJb3ZraWd0RXE5V01aOG9NcldBUjFyWEhKcW9td3NlQjRVRmVoVGVnQkkxTE5kd0JrMUs2MWYvSmZIclVjSit3akR3dVpqVnpkMTZqanN1NjBNRUhyeWx6YUpSMGFZOFM0eDROQjVRUTdhNDU4MnlUMU4rZjFDRk5lSCtpeFNhdnl1VnBlWDcrVmU0TkpMTjZYZGthSmdveTgrRW56MnBBZWdGcEZUVFFUOTU1akxIMi9qUGRiS2JtTFVQekVHcUt0eUZoTEw4WjRvd3gyZFhIcXBhcC9TYXhCQkNyS0VrMkczTWdyMEpDZFdIbVJZRVd3R1V2SWFJRmx2bHdEd01qR29oSjNlelhvazdaZ0N5aS9VQ1hiUklVN0oxbWw5TGo5NHFKbllZK08ra0xBZXl4OTYvTE12OXVpVVY4cWJhS0h4K0VKT2s4T0lBb2lickxiY01zNXZ2T1ZINGRzTnMycWpYYi9LamUrVHk0TEtDZ09rdHVyS2ZqZ00yT1NVREd3RUhueGd0MzlEd0NxNm5RRnR4KythQXJzeTVaZ2VhU1Z0T0UyYklxaVpGY1oyRkdUbUdEc3Z0bm1UTzJpR0Y2d24vM1NrRTFPQUpnVUI3aUthTFNGOThiTURtQ2pZOTVWNGcya0NhVVF3QTJ3WlowNzBkOXBTNlpheHd4QXcrUzJOd0dNejJvYXBFdnhWR3RUb1FHWnF5ek8rUmdvMld6L1l2a3pDMkNTbnZlaEVjU2ZXa1hDZDBGOUxTTHRSQ1lqanB4VHdOcld4Y3Jya3pQZEVCNUoyZnE1Z0VISWNROFhZV2FFWjlaNUtmOTRXaDk2SVpvR251enhKRSs3WitkY3JzR09mOUxWbTRjSlh6ZEdnOU83ZnQ0VlBKTkVXOGpzanFWOHFNbERiUEljSHVMMnhjR25pc2lhUTRwbWsxUUs0b1BnSEYzRkNnRnB3Ui9sUGtwR1pKMzJkSE1BMGVha2pmRm1PaWxyd0tTVjU0ellzMC9GUDI0RXlSUDlUY09aNHQrVVdRNmNtTG8rY0xqSjZNSnBlMjg3YzM0VEpXaXA1RVFtWFBZQUFQamF4TE9sYml4bmUrRHpQSWlTY2IxWllVUnBFbU41Unh6VU50N1pFMzlEWHhTZTBXanJEbHBkNkZKWmRNZjVnZUVnSXJ5NWtEcDBzUHd4Mmx6ZWpZWkgzSk5acTFLS3V6VjQ1SmpsUktncVJoV294Kyt4citKODdycFA4OElPdHJhSW5MeWU4R0pSSk1aSldnUmVGTE5XS2FJQlVRVGpCMUI4Z25NZ0JqNkx0WEwvTVdCN0FzdngyQ2R5VHBCcU5oTlpqWDlMdG82c2FHL0ZPMHdLZytRelhlb3NxTmQxRGIvOWFIMkxHMkZiVzJZK1doRDgzVitPZ3hPUVpVUFphKy84OFZGaVpBVHljODVicVhpVGhIMmRtdDFsT1Vkc2FmSnNoMU1HbUpmT1RHRCtWekg0OUNNK0RVeWF4bDVLWEdWbG1SOXJYcStuamtTRlRnbG1qL2VDRGpSVzN4U2t0NDVmQ2ZUYmMrcnRvWC9DUmNuWkhDRTlVbVEyajdDWTZEM1B1dFhGd3JSSGQrTVdxalFBVjl3N1RwNXpjZktPSDVpeFp6NG9pbFBIY0gzMVdIdHYwelJiL2ZRNW1zSGcxMEZxTzdpbEV0UkNiUGFVN3liZ1krdmxtMGdsWXVWY1JNc040cVVjdGpFa0c1bjFFQi9BL2w1dWF0Y2tIRUJNbUVKWUJWMDBZVDZBRzVtWHphL1RMYTRrWExYWU9IaTRSSThOc3NwQW4xZElraFAxbGVtRW05d21laDBlNUFmNGpUUGYyQm1lalQ0Q3ZvdVdVbE15UlRrRGRoN1NSTm5sKzJMaEJ6bUQ4dE1PNThTVjZKYWhXdVhqNGgrNlUwc0s0RG1WYnhrTFBzamxIRGhRQkc0d1hRQkxjRGpGa21PY1BuUHliTitDQXQzejBGcVlmWGg2SWpDSFVraU9hMTgrcmxyRkt3c0RpOWtacXpRejYzQXB6VU40RHRlQ0R3ZExIcFFZcDVOSDF6T0sxSlZaZDZmbW5kSmQzcThNT3FVWWRsQkpRZUlsSlBRL1hLR2ZIUGdSa0FheUJhN054djRienZHQUJwcXlSZWdiYXVDUGJEdXhYdGQ2STQ4bzh6OHpHYVVIVUFTTmdYdjFhOWZWOGZ4SzR2SGFWcDJZUHlXZFduSW9CejE5TWIwUC9VU0llbFZPL3Fjb1paSmFTVXAya1crZU52Y084bDBnTEdtVjZONlEvRDQzV0h4RjhuMzI2VWZnTjcvN1RRR1A0ZFcvNjB6UnJwaTVGYVFXanhNL0wvTHFPUjJmT0NPWUlrSTdWMjMzY2JOZzdhSHVCbGcyYkVJbGdub3NhckNBQ3RCMVdzODRST3Q2RWxnSCtZN1dJNHNLcCs2WVRGL0t3U3RlRU8waCs3bEtNbWRuRlJoUUtvZGxLRVljNlY3NlRucDdDK0Rjb2E1Y2VFdlM0a2VyY2xvSVczSmlSaEZOeXdKbVZNeCtqZzB0ajhmRHREUWNrL0E0S2ZPcUQrNUVLVU1paVBpQUQzeUpjZDJvZXZDVmNvd0J1TER5K0d1blY4OE1tZUtqSWt4d3JIMTREVzlzMTY1USsxN0ZmZzdXdnRMcVdMU09YS2c4amNIdlM5Q28wZHJ2UjdIWUZGUHk5REI2Q3VwRitYRkdRUERQc0IyMEMwK2t6dEdJVDViYkJ6R0RhMGJTUXNTNE1KNmc3eDVrZlZ6eTNZYWhGa3RyRmtKdUpyRHltNGlkOUNLcDNQWndkM08wdXRndXdmbDVqWXlYZHlxWFdTSW5heWJjamIrNXNaTW9uSmRiV1F0eGJ6eUVTWncwMXQxVXdjZnkrQ2VJa2hzY0tCck1EbElBeVJod3EwVVVTNmdoc2ZhNmE0UHowYU9DNVp1MkNodVNGbDk1RzVsMS9WYTZrN0lIT1FnVUNxYjNpMzR0M2FhZE5WMmhYSlJyWWR6NFh2ajVxQTdtTlJkR3Z2M1pJc3dzU0M1MUt0SE1SdEEyLzh5SkxEMTRiZHlKQWFlZnkwaWpYTUltK1pvTzR6Q3cyUkorZUd4S3IxUmRiMHBob2ZZaFlnWS9CNTZPeXpwcTExbHB6M2orRUs2VGlsNURPR3VKVGxUNEtML0oyMEZKUnlDdmpPZXhWMWUyV0NBQmR6M3NMS1FxWVBaeGJDeE14UjhnckRDajMyZ0ExTVd5S1h3V2RwUVV0OUwvNnh3NGsvTXFacUxSeHEvVkxtN0VEWXY0bjIvSDVUdGJ3YlJ3bldWbjBkd2lpVnB2L0w1bjlHMS9va21yeU9USVpSSmZ4bCtLbHB6blZYTHdhYWF6V2ZYa3FueEwxWVZqRytHR3U5LzZBK201dWdyRVBQQ21CejBaZHdRRVN4WEJKOFNNc3BvQWYyRXdjQ0xYaThZNyszNnVxcG15bUJqQmtabkVKUFB2U1RwWGIwRlhUU21pNzZ2bTBnQ1lPQnNCdS80Uk9YVnNZbWNiR1RTVkxuL1NaOE1BM0hHNkNodkk3Zml2WitSWVlYOUFMVEkvUUYwaXJrVDBFL3pqWVJEekp0S3l1a3RWcjg4ZmUxSmZ6cXFjLzF2V3VubFJFUk9aV2ZvZGZHcDVybXcxOWZ3WWY2OEIwNEMyWG0rd2xiU09oWXV0Y24xK2Jmdy8xbzJsamM2WVgyU1RaSXZEcGhqenZxTzZHZGlQU05YdG5sK090Y2lPUCs0WW5RK3Q3ckEweWhhNm04V2FhWG1BVFVkdmgrSFkvRldjdHBuMlpON010REdyYmlLa09vTGpadFFPdXZhd3l6Lzg5Nm9xY0pNWmZpempabEkyVTJMRityTC9yWHFvYVBIc3lJR1duN0hZRGc4RHBOQVlUWG1kYUdZZk1XTlh1L3JWUkljUWp2eUdnZ2JxL2kreTBPOVdUYnM4c2dzV3Fvc3Nod3lpWkxSeXJQZ3F0cUVvV0drRjVoMktzYm9wUDR2YWZXOXdIaGg4RkpDaGFybVFxL3JxOXhEV0FvcW02Y0VUc3puejBwY21Sai84dUJWeFI5U25HM1I2YW5yYkZteVA3OXlEdnF5eHVHTUMyT0x4RHEybEoyUVdwK1NwN1hWMDBKZUZ6T0JteWZMQkkrWUw2S0VOaElwVFFiZFh4OFFFTUlpVzZMbk5wMWt6VllMWDFNamlEc2tMTm14WnJXWlFxQ1F0UTEyNGUwbVV5WEttRCtQdmo1ZktkQTZvc0gwUThEN3dWdCt6aXU2ZTNFZHlNWTI2dTA0dTVkaVpGT2g5c2pUUy9IYUt1cGNubmdSQ3Z4dEpHL3luVS9hUG5xNnlOSFZjK3VWLzRiTzRGU2xoemhpcnpLRUZqNFVDLytwMzlGNXlmNldTZFI1a2hEdHRYbG9QamZCZjNMd0ZmWVJrL0lQOGdzU0gzb3BmRzczL2RnS1h2cHpCU3lvWktvd3RjcVpFZEdlclJxTnZJYW9FLzZnK2IweS9xbXA5WkhwRCtma1c3ZkRKWm1UVTZKejBzMmY1VzR3bjdPYVpEVGlHU01qTEx2Sk1hZTZqVGUyU0ZhcUNSL3p3TFRVNUtkc2h2TnpiWGxMQ0hGOWsyMVhidTJYMmxCYThqK2VHeFNGMU5qSFJyUEE0bTJIQ2ZSbVo5bzJjd295UmFvM2E3T0lpK3BDRDBsREsyYzkvTkxJZlZYbVVGNlh6eG1hTjVsa0V5emJaOHgzcGZ4MXJWSHZldTBXL21zK09LMEV4VFFEYW94YkwvRFhsVHNnSDBzU2owRDdxL0Z2eTNRYXcrdGxLdzVXV0FYTkNRTEMyRWhvNmM2b3pVY2FXRk02R3JFTzZVd2ZhZmpIaFluK3ZJb2FIS05LVWRDOWRZZnVON1F5b1JMT3JoY1JvajhGZ1NNazJtOXBqL2VYS2tYeWZkTnhiUmdTYVFtSDJXVTBSdW80akVUSEZ2YndRTkdjajFQRHdEM1VrdUYxdnZUMk1ycmEyNUVPSGJZemhQZ1pZRkQrNUlabU81Tk1PeVozZlYvTSt3WVVibmU5SzViNDRJR3FCclJjTHNRb1FLaGRrdTV3SEFpenFOeGhLNnpJZk82d0srNzAyYmlPajM4VFBzOHNpMm5kcG4wOHo0S0xmS0xMTHdtNm9GN01BUi9iS0NGamc2QUdjc3hGVjJ1amtKMlZ1RXVZUkRiYXAvUmtRTnIvZkpqb3BUcCtEVEVYV2pNMHAvVHIvN2ZmRFBUTmkzNFRLS25HUmN4QU1rbWtRSmxvRHNMRktzN1ZmVjJjTGNXZ0xZYllSQmxZZDB4c3BWaWwvbUhzSGhPWGZuMWVnRHRITkI2T2g4aGV6c0FqU0VLU0VyeUExTlFjenJDYm1hU3M5U0pmWVczU2tHelF6NHJSSkxoMkRDYUNrc3g4YmIxc2JtbzlFN1h2aFc3aHJ3S2RWbmJCd0JNS284NzFFNWZ0d1htcE1xNGRpVmRaSEEyekZJa3AxdkJZMEZ0TjVOWENoUWlkY1Bja0ZYWGhtdmhWdyt5N3V6M3lQNExNUU1xREkzejltajBiS3ZJdFFKVVpvM0RzaUIrdTVxSnpHNTd3c3pIR3JpR0ltV2VINVc0bThmNFgwSDZCMEtZbWFNeWx1ZnpjTUMvbUtzRDhjV2xKb1RISXd4elpONFRhUDU5dHNaSHRGZ2F0TjhYdDVpZUV3M2NDUnpJZHdwcUpUT0lMejYxRlNaL1A5S2RyWnFyWi9vRVppU3dKMXdBMUJ1eE4yRWZvOVI4M3ZjU2Y1czhnbzRZTGpWbXBqWVlKaDhJc1Z2UWQrb2VGcHoyenhLN3I5aGpTVUhhcXFEazhkcEZqaUxubHBsQzl3VWtIUTI0clN2VGtpOS9HUDNkcUx2YkFVUjNhclVHUzBaTU1iZ3ErSFlUSUJKTnVZd3NzUi9EN1gvRzcwNDc2TStqQlNONC80Z2lvcnhWUUtJaVBVNVlyMkE5UUxqYkwzNnpJSDlUcGdHM25NMDFiMUxxZEY0RXU4WTREZVpTUDBXTk5YTGpreGw1dmZVNklNVmNseXFiby9zdFF4aVRnNkQ2QktjUFA2VFdpek5lWFk0UG1Ia0MzL2xpT3ozWHA1RmtLNzdSOVFhNk1yUkJpSHNSdzN5TGkzQVBZdlAyZ0JVeW9iRmFkOGtzK0doOFF6MngyK2dybk4yK0JzMEJTR2J5V3A1TEdQOTRiQjZVQm5EUENsTmY1WG0xUmNLeENxTzVZYjJVNkRpTXhkRHU0OUsyQXFGTkpCTDNTR096a3UzeERVT0d0bXpxMTNrbmd1YVk0RVBudXFhcXd6ZUt1a0lBSldlYlBMNUI0cTNtdFJTa1pzUk1hQXFUaEpUY3N2c3hyMTE2ZFJ6L1U4Zyt2SmxSN0x5ZEthNldEdVNNOXJoNWEzV3M2bXRaZ3NXdjB0SlQweENscE5idnpNV2FHZlJLUFRUamZOU1FTdnQ4dHFBa29raWRYUDJrblkvbHh2Q3Q2R2Zkenp0OUUvUGxqVCtRVm5ETW96ZnhNL0tTU2dlMDdwZnErU1pBR0JBUjFoWUJ5Qk11R2N2RHp4VDA3NXBoUS83cXRVaWZlVE9HTXBXVitBZE5xczZ1REt1YnRkUytTL1puODZsM1JXVXdmUlhZWFZ5NWsvcUhHOWF5WUtWbEVEckw3SzVUeEl3L3p6bUE5elNJbENyVG9tK0puMlJpUzNIdTFneXl5SUMzMVU2UlFBWDI0cG5keDdFaU0zaTJVRWxKVFpUenMwWS9uekp0R2ViM3ZvaUN6d0ZxTlZXUWhTa0NQT2ZDNXd5ZC9xY3BoajJkK0dodkxBTGVmQmNnMGdKVVFrbVc0WGVBVlZlcDhqTWxmSFB2WVU0ZS9VNjhxNmpiZnJKL0NVNk5heHp4R0tnemovWWpWbHM3VmhWVm1hQVluVjIrb1FPQ1JFUHJLaktLdURWZ2NLdStKK08wa25rMkZnTytrbHA2VVdnTEIzaWl4UU1ZTVVUa0l3Uld2amZMNUpkVWc2V0V0cGlPVEl2MGVIYm1tcEtab0RrLzFDNmMwa3JITTZTcmdzZklLUXZaNXdsUGszV3dwRTBWMzAraEhLRkQ5WUYvSjFYVTdHRlpCYlNiTFZiekxTT1NxaVoxTXlSWTdiaUQ1QUtFbEU2c3dIa3dHOEV3R2JSTWhqQnZ5RjJKcHM2TFd0cm1vQUJDbGE0U3hUMSs1ZVBxQ0h1bUc2andpMEIwMHNlOE5ZWjc1c2RTWXlIZ01BWVB0SGRUUDRiMFltNzNFQzgyajNPZm81NEZOU3kyTzFWYWRNN2JxYzZJeTFUWFJRdU5WNm1VcWl5djZ3S3N4ZDVSaCtlQVJpRWpkNUFvUkNEY1dvZkpOVGlZQnZUMnFITnBNd0IzZ2l1UTlUdHRvUVVaL0ttS2JrR2VETFJKL1RrdDNqMTJIUnNzK3Q3WUxXTC95NHh1RElzRmwvcllMcFhKREFQQUpZc1c5UTZidXNiWDQvS1gzSldvcGtXbFV5a2VDQmZIeHBmeVZ3d3oxU054czRDT2hEbFRLNmgxSUJoVVJFZ2IwZkxPbHJ4dDg0bkF5Z3p5ZmZqR0JuU0JUWmZiWE42RU5OalUrb2xDbmRtOEx2S2Via0lhMUc4R3dKQWdBSHcrUS8zUVIxazZPSXpRUzdzcUN1SVU4K2wwWGFLQjcvS2dGc2lDT3JzMnpXektyaGtGemhubUs5SVprSzE3eEcxdHJWZnpPcjlZak9CZTgzd1R3SndRT2RET0orMW1KaHQ1S2k3STRaRnVUd01qYjlxMFRvcW0xMzBtTWlkUnY4YjNYbFBWV1Z1SXBaQVNvdjAzVFNxL2M2UkxtUk0wdk5YZWcrQkdQSUtabWxvTDRTdlRXUUVtSGFwNHB3bmRuKy9XOWd4c1R5MXRzeVY1NE9wazNTU2JIQ3VXckdpbytHaElNRTFHVjdRR1hCZEhJR2dxOTRoSlpJWTJpS2tsUzRoU25pSzJ2WnlOcEFGbHJqU0JXZnE5ZzA3ZUtYdWtUOVp5U01pdS9rRHVCQ2poV0RTSEp4QjFJSjN6N05vbmo3Nmd6Y3lXbVM4YXNRZTZLellPZUxESkM1QVVhKytvUUhnK2lCV0tCb0pWRFd6WnFGSnJ4Z2FHSjI5a2tOd2VJWVFVTWhoUGFwdU5HSkpWNDN4dEFaY3l5ejV2ZEpCL3BDVFhJMklkS1pjM1FWdG9KRUZ0SkpMK3NmQTJTWk0yL0lNTkRKZ0ZhamtTWXNmbkFiTlFJeHMrODl1QXRKUVNKV1dFMDNVVkxoN2hLUWhiRHBqMlRUQngwK3k4RXVJZjFrSThqamhkSWw0MFpqMVVSWTUzMjhlTk91UFRoNEtKWlV0ZU5oYjhVem9CZ29PWGNNN0dJOWJNNDdtMU9yVnNuOTY5SVRnTUlMQWZodW9iZFdTY242eGR6RDFPSGZob0JKMlozNHlTWS9peXZIVlFaS0VqenR4YVlVdHFJaEh5Z29BODh5Vy9EOVpHaXZsMDJrZWN6OU4xSC90NndvOENtQ2UwVXg3TVl2NW5ScmlsdHNhcnZoWFA5aXdaL2lSaDk1QXRPajRZNjl4ZXJGWE5aTjlVdUU4VEQvUnBaRllXc0t3cXk4cHp1Z3NyaWVFQlpXTmh3RDB3RFE2TUhyT3N0M1Yxd2phbjkyZzJSUVJjdHY0SXlsbkhZSC9hRnJZNk9iZTdvY0dtMjFIYmYyQVhjSnZrRE0rU2k1bkZxM3BZbXpzcXI4Q2N2cDU2NHB0ajhRTkg3cGl0S1Q3NTgrMzhpakQwOGtCeCtnVHZnK1plU2o3dm0xUGZsZTJ3TURKZlBWYVE4M1JQWWI0SXhrWmRrMWZSazVKaXEwVmcrZzNydHJJR2pxcS9YWVFCZU54Wks3WE1PK2NPYjVjSVRudWkzWHV6OUEwdUtWTkkzODJTekVUdmRMeEo2MVJLVVQ0aWxZemRWUitGaGZGak9hS0lVUERpNjBsbWVCcGJ3Rk8zbHBMbFIyZTFWY1JNemNiOElWK1BsNVV1ZlhBaFcrSW82eTM3SExKQVBFb2d4cHZ0MEFWc1FpYkZ3VHE3QTJJN2pzMXZMVmRrS2Rtd0JrMjVGem9zYk1KYk1LcFUzTHJ2eFJMZmpWM2gvRkc3VFRFTXNReW5JcVJRdEJsdHBsSERVUTNkbTVLNHdoK3BxZ0pGV3FsNWZzOStSZ1VFS0duMFZhMU9GVzFvWjhmMGhXRmVkNzkyVGJUdDViZWp4amc5ejRoeGgrVkwvRVpYdHZ3QXl2a1htTld3dzNMNXhHV3BTTHlaT2c1OEJmY0RDUld1cG1KUE4vckFZSE5yNDhqdzZDUFp0TlJ4aDhBTTB2Mzg4ajBZUVdVcURpb2tuYnRNdG9wV1pVeWNpOXl0a1c2cUdwaXhkeDVEU2p4QUptdk1HSnJxM080K2hNOHRra1cxSXZoME1yWkFWSTUwT2NVQUx0b0JKa1FvVGtNeHFyZlAzNVRGMVJsNG96NS9sN0dKOWY1cHhOVlZXdVJLaUdTVkpTdGRKa3lxN1VEbkJwUytiK3czekdBVTNCRWlheDdLMVpLRVY3MHV3d0xKeThWQTlPUTZ1dGxaclJ3WEdJcGZSZTN5VmVJSEJjVmtIVmVwQnhIL3BaTHRUeHRhbUM5TThZUFQ4cng1Tkc2MURlVTBCdzdmam15VDYzUjVrdHQ2STR6Tk1MWmdPSHZsWVk5UTdhOS9Cekc5QktOWTJUV1g2M1ZEd0lZTGIvVFQrMDB2N1VVZEpiMHlYQi8vVXVSeElTQ3FYWklsV0l1RUxFZzdpck5PcWRUUGJETUtYS21nbDBXOHUwL3FqQTBMaWJ5UlJhNm4zM1VBaTBYYTRueHJuUElZd2dJN2FadWF0UkhqOGtxMXF0WW00ZnVXV1hDWHlOekNGamtITlpNRzBPNmdjM2R4S3RCK01SRDdRelJvT2pKOUtOckxzN0d4SUVydDBKQ05kbHlBVWkzYlQzZ2RudWFJdzM1eU5DUWd3YXNqdGlwWk0xWkNXbEliOE1lMVFTaTJ6T1hockh2OXVCREh5S3VEV3NpRGV3OU9ONEMwTlJJQ2NCR1NYK0hid2srUW8yOW9sS2VIN3gzYWpCT3pGSWZ5L1Y2M2x3S3RxNkQxUElrc1pMK0orak9HaGZCTlJlQmdnVlcvRkRLYkpxUXhVazdQaHExL3k4Zk9zV3J3amY1eTdoSmtYbzRMQW41bWlTOTBsOU1rbDZCbklBUHNIOU5wVUg1UThxVVFvdTB0OWdQaUZqVHlxNTd6aTY0TlBKR1ZiLzRCTk9laVFZaFNxWWpJNXd0S2xSUHhQM3NkYkpWNHNucUJCc3lmUEJDd21uUHp2amtacWc5UkNFUmFCdk9GbzFjTmF4T0FTV2wxajFGb25VWTk2N0o3em5GMGtTL1I3RUVNdng2c1JGajBnbkNUQWFRNkFpVm1pWFV0VXNBcUVkM2tnejRzUFI2SWpVZnJOMnl3bWlEN0tMaHN2Mkp4Mm1nQmRvZHJCVXBMZjg5ZklnMXBaN1p0T0VpbGJsUVRmQVg2aTlpbk5tMlQwRzQ4UEJUOEJjVzRzK2lrTHArTUJ4NC9BcXpXNFRrbzNPdWdhRmsrVGpxWDBwanRxR3JVRUUvdHZsdFRWVlVDUlppejllZS9FNWgrdkdHMTZJSFVFRUI0UWhnYU41aHJ2THVSK2F5a3ZzQnQrOEg1d0hrd0dsVXJab2YxMWdpdm5kWUFFYVNlV1NoNXU1Nnp6U0dLU295RnNjV2hYcHNDcDcvSDF0U2pOakZHWWNXS3Npdm50QTFqMzFyTEliSlFxcGRqWUwwNloxU3ZpeWRJKzhhZHpidlVnR2krMVlOcnB5NXRKN0NzOEFiNEdWcndsdHFQNXpPS0tHZldJNUhXTnRZRnN4R1RaQzNnNlJnZHIrSmVlRDJnNmc4eGFkQ2RQOHkvSHoyTlJXY1NrRnlWZ1BRMWpkRDJJLzlhYk5OanowRmlWQkUwTncvTCsxb0FlWnVYSG1xR3dlU1QwdXpCUXplb0N3VmREWTVRR011QlRjQXJENWx5VlZ1UWZqZGhLL3draksyYTI2U1BMeWI4Q1pMZVA1ejVYaWNxQ0ZyeUQzbktiMG91MTQyWTFQeTBBVVQ2WEF3V1VscWFuZ0lpRytQZW41T1FNNWdaVG00ZERCbDY3ejBJR093WXlXcHdXV3E2ZVNsMGFvZzQ4Y2g1Mk85QmliT2dwbmVWTU93Z0pYQUZOSEJLbWF2aFJzeWM4b3EzWThXZGNIUHQ2OGZSWW1aNHpoWjZHbGhtTFdLSXArNGY2SWVrL3VCSWRCYkhxSWJLa3NIcFFtL25MTVo4SE85bmdyN0VaTThEWjZnaUFKSjcxVGY3RGtEQkxDYkxXSGpYdWRoZGF1bTJ5ZlJVU2RFeHpJUWU4UUNjamZRRkFjOHF3OTFUSFhacVp2eUluNngzWmRmdkkydUZDcDJiUVRCSVZ1MDdsdTJZU0FLb0FyY0F4R1A3RU5DaDZscndSbmRqKzN1b3pMOVQ3VjdOZDRWSTJINmpVMHh3cEtoNlgrYWdOelRpbHdPWEtEZDBIeVlJQTV3dkliNUxPek9jayt3cmZ6WU5FQmwxYWQrUmJaa1hVMWp2ejNKVHEwSVJ1dEtRMTdmNEswMEQzZWppNHdpTlVwcjFrMmh2YVdhQkFkMnB6VGJLckkrN1BTN3hyK1o3UGd6cDJ5QnM5anJtOE0rL29hbGxrdkViNldXWEJVSjFlZTZGS044Z1B3bG9LaWc0T3lVenFxYmpOWEdyamFSR2ZtZmk3ZEdNeWY5NkVCYXRaY1greVE4bmpwOVJZemo5bmVlTEN1QjVJbWJ0bi9FZDBnbXhFVzg1elMvN1RrdENoNWwwQXl3U1pJS084VlpER1JFUU9TNng1WDdnbUlIWllNWmEwSnF5c05WV0E1WGo4SlBzYzV5MEgrd3RYNjZUdnluY3pzZGV2dVZERmsrdFBvcHI4Szd6NmNkd1JiOU9Ldmc0Q3BFaXRNR0FWU3d2TTVhbWdsaEpnZTdRdmVHRzNTZmgzMHpBcWhXV1F1bkNXcDVnZURGZGE2RUdtdUFIZG5LNThxZ2ZWcUJMWURrY2srNU5TMUpKQ3BocDhZMEdjUUVPbGh2WU4vTHo1R0ZBS1lPQ3NlRmNzbng2anQ2OUIwSXpKblVBWDVIZzR1RTZsWHpydXB1Y2xGWG1HRnVIWTBDZnRJTVAzQ1VMa3lkTG04bDF2M0oxVUNFdlIwMThhSklHZys0L3hHM1ZtWW1ERWtjTFdvQ0NCTUdZZDNaVmlpQUZJK0wveDdiRjBnWGdLeUkycm9DTnVpM3pZcFd1ZUIwb01rRkE4TmFZWmxpTnpXMzcrZE9qUnlGVHJsQUQyYnplTm5FNzFnRS94YkQycC9zZVlES0ZnTE1Ea2FKOTZUM2NCQ0dWeVovcXlmNVIxenpHZ2FTU0YxSU1NU2JVR01jK3pNUVBWa0V4MFJIeVVHMlltSDJzd1RNZmJtbmZ2dkZjMnhoRTA3eWtPQ3RrV2hpQm5GSVBpVVVCNW8wdjVCVEQ5M3dtTS8zVGEwYUtKcThEbitFQ0FUekgyekhma25aKzN6ck9hWG9XM2hqaVpVWUM2UXA4ak9rNUptM2JNUmIzUTcwWG96S0VmRGVmUGV3ZzhSVUN6TFZmTXhoZFppUkdvSlcxRU1GcHhORzMvYTcwQmUvTGgxL3dlb3RxNDN0bEYvZjRqUmpsb1E1SkpmWHFSWDhheWRyZmtoZzE1ZWdrQmxaeU00cmNaSkxjMmNibnhUeXdJTkNqa0hjVkZ2YTV5eHhJbVptcXBqSy9FKzVpbFBZaitvZk5Oazd2Tk95ai9uU1BtQVVPbHk4dGF1RGJ5dEFXUXNNU3VRNGxtNGpJUUlmTmh4b0VHTFlLd01UMFgycWk4a2drZUZxUG5YTHhoRlFvb250Q3hleU9zRVR2cFNJNE9MeGtselY5VzhhNEpuMnZ1Ni82ZzVTNHoycXRTNzRaL1ExOHVpdW42YzFLTGtwb2hyV1dYWnNqd1hnWFhHcFVJdkZNWnowQlNoOVZKRHRyNkVHSkdJSStsOVYrMzVEUXpTdElRbVVsdFZybGtHRnFKNC8vcDJFdWxFU1VnVlpDK0lpbmdTT1JaZGs1U1ZJemE3a2hMbEIxWUpNaGFaMUY1THFxMEYzZmhPOFZ6SzFYWXN6bUVkSjhZNlJXalExMDBFZlZ3U0JIRmtKUzlnM3JSdm5BajJiMjlyU09LM0k0eWNOSGVuQWowbXFLVDczRkExN1EybDB6ZFpJd2daQ0ErNDVqc2E2ZzdUemZGSE1Zc0hKdThkaDdkemd4cHpCUis3YlhWZGsyOUFZL0lQVVJGZXlPTkQweFpaTWx0VllOR1p4Yk9CZXNFUHB2ZmVUN3Jna3R3KzVnamp0Zkp4TlRsN2lKaWZXVTRwVENiZ2dmY0M0cW9oV0FYOUNleFgwWU4zbUFVK1NJdDJPa2NLREkzSGpSbnVPTWdaTWg2eDIyUEdvQmtIaVgzaGtvaUFkNlg4aTV6WVRDTGFXdndWMWhsNjM3TFJkdnRqSHVYKyt3Mkx3UDlUekRORFBYRFpmOHdWOTBLUjA5VUY3b2FZN2dzaGY5MzdkSnVPMnI0UENRL3h0S3BjL1NyKzVHNDIwcEFzQW83T0RkQmRDOWhIWGhWRWU5U3lHTWVwb2lqUE5hRlRrQTdPaUV6RzJOb09xNUE3WHpyb0FHbXhUait0N25qOElKdERoT2h2dmFiSHVQd1Q3aythL1pleFVMZTFlRWRPNzFjeldDRTROS2RpUVVjTm5NSjVEdDhtNit1MzV1UmRKcDJZUDF2UlZZT0JVcGpBUVFDa2tHc0xYUmFQNTdHNlNmY1dtSEVpREZjS0RuOXpvZlVvdHB2SDVtU2E0Z1B0akhneTNhakd3WGwyS0t3b0xIbTRCTFJJL0VtcEl6ankvYW4yZkticHJHb1BhMi84dytHMFBWZ1BXdjNraVN4UTB0NHBMMEpnZU05anU3bzRoZnMzRC8wc243TkQrU3kzdUdvQm9adGlZWlBmdFBPRGZabTJwQ2pLQ3ZhaEtTTWxJOUdtMTltQ3NNQVpNdCtOaFVZZWhxYWRGUkY4OUhvV0tHeXNqcmNjV3oyMFZvdWFJdGpsOFdEK3o3ZTNReEx5SVhwM2NiNFFBa1k1WkdtZk94d3FiL1RaN0VrNnFxQVRpb1l2cjkvaGpTd3djcjZtNmgyOXYrenloK2ZCTEhqLzhDOVJJalBIK3pBdDJWc2pabW41bTJjYlZPQlJTTUVUVldLbkRhN0IzaGNKWDF5dXRLTUJrTk9GN3dMVFZVTitxd3NjS20rVlY4U25xWG1LTDM3OW81Y1hRT0Y5NzFWSUp2OVRlR29tL2xWcXpQNk4xVWVJK0dyUm1XL3RaOEJUeDlZOWVML0JTdk1CUlJoNXVJdk5UTFNUTTg1aHBzZ0dkNEM3Tm5lNVlQLzYxbXJpOTdOVnZLMWUvVXFUdXJNdlVRemZZZ09GcGJ3TGFqdDBYMkdGN2dJNTRySUYraVBOdHlqQlcyRDlQQ2ZUSEJSN3E0L0tpVysrNHpjZDBJSUR3SFhCaHpKVE1tcnNPQ29qVGNVcVRZVWxuZGV5YVdVbUt1bFdRb2Z5Qk1HRko5SzZUbUpkZTkvK0FUSG90MVZ4U2hVcDRuRmtOK3NBMnZ0NjhheEpNa1BYbk9JM0lUeGhGT2RQaVFLUGpCaEg1d0R5bkxnN2N2dVRaMHJyUG9NTkZHcGg5VHlhakpWQ29nZ3RadjJXN3F5MjhSMEFhemhYaHJmbGh2aVBzeEdqV2ZYU0xBU1hzbDNVNkZmbTA0NGVKc0wwZDFROG50ZzZKNWpkWFB2RFpxMG9jZEo5L1VOM0FpeHNjcnVidzMyRkFMV0pZUDNudE5BR09tZGxmdlc0YmZ0cWl3aUJtRXJsT09wS09VQXNJTndBNGMzZERuRldKQzhXNkJYdmtBV3JXb0JITW1yU1o3V1FZVDU5cW5BcE12Sm9lYm96ZUJ5L1ZVRVgwcmg4MlFYNHgydEU1R0UrTkE4WHh3UFdnNHE3SjFzZnl0Zk92S0l5ZXd0UzZiVitrNXNvMGdTV0paaTV1WjZ1bUs3NWtLOFJJUTVJU0c5SzlYKzJxZVQvam5QenpyczNjWUtnOG4ySUl6MHplRndWSU9PcFVaUDl5cEFqTEZFbTVOQ3lUSXczckphQlNISC8yU0dRbTZsOStoRlp1b2JHTnpGYStzV2NLdG1jL1ZhN3Z2WFVUdnhYeDJ1a0pVMm1mRWRlWCtzVHdCdWZGWmRxQ0w5K0Y1QW83a1h4cWI2Ymsya3cyLzFjUlFaVlhJaDJ4d3hEam5iUm1Ua05FQ2NMTEtrd09vaEM4UEd5eE91ZGlRZkVwR3lhNG1aeG5NWi9JZTZmc0dKeHk4RDI3OFVYSE9ab2RDSzNpbUd1eExUWXZJODB1eXNmNGJhcnBiR1BqU3lDeGxscFZWdzdlYmpoNmk1MFp3SlJFM3lnZ01zL1hiV1Z3ZVdybXMxcXlyNURJSTVkTWk4WnF6ZHRtTzJEamQ4UFEvV0ErNDBXWDNoQ1hYeG1ZdUlUWTZESmVaL28wNjRWZEc2dTRtRkdxSGpsZzgvRFFKZm0xbkF3a2NCMWxZS1ZpZUFkRWNTdVJYd1NDQ29IODhvOGdUTndOL0ZQUnF4bXhUOTgyaU5BNWJvZmlnbmdoTHZlbDBGMDBjZFVEUGZBSGtKNEVBU2JvV0FYMkN4VnRkWlJya0JVNWdqcGkySmtWbjdzUE5kYk1FRkdzd0N5T3VXWTYxS3hiMEVQaVpLdmg1bmY0QzlKSHNlOHJjbndYeGNkQVBPb0E2OER3UmZCVUJ6bHd0N0dyUXg5RUhWMGNrcnhmVDc5bTBoVkJxZXRjNVluRGg1VzRGOHA1RkYvMHI1eEpOajBudXFEcU5EWjBCRDFmMGp1MEFsMkdjMGJxQzlqZTBTZ25rek9kQ0d3cUpWV0FhS2Zrc0NXZDFIT3pJVU9xTDNKSGN4a1hyeDdoRVR3cXJUSCsvZ3JLdCtwaHhJN2lESTJCdFRiU2ViekJpRmoxV1hXTEZWMHJkendEbzlvVEZrVmtpN3FpU2ttK3RrL0M4YWNXazkwd1BWSEJLVTdkRHc4WXVCQ0JBL2JpYUx4UDE1VkEvcncxSTZ0aEZ1eTJDeU50TkpzYTNWWklrdC9sb3NIVmM3dmN1bWxDSTBScUJEYStVSXRPa01oaWVVMTcvM1RSOFJBYjhXWStuNmRkZlhjazFraldtZmpDVkJYblpXQzMrZDUvSERyWE05c29mZFhQSzNXWDd3WXp0bVNWMnNTa2lvYmtyNndWVUhMdU9OSys2VEpFWFZVYzJaanpuQlYxZTJjTHNHMWxsalYxdFRRMGMvWS9JSGc2a2t5VDZOdEswSC9qTTh1bW1WcE04bU1IUmtIYzJabnBLNlhFWk1ldnpXTnE0T25RUEkydmNDRFlEQVRZdFVNam1OWGw5cjIvN0NiQzhUanZBSlVacTQwcm5zQnFRdDNzMDErVVhlajdEUm5qbCt4bjFFOGt5NU5Za0V5OUNLdWg4VDNmQ2JqUFduUHJ2ZEFJU1N2OG5DdjNHcWRWMmNhTkpCL21XYlJRNTVIbG83TDlyaWQ2UDU0VEpMUFN5K3dydUdpVmJHZURCbE0wSDg5OHhQYW9GNkdjVjRGWUpjeGw1Q0RrUUhuMVlveW4xYkZhTGRjNnhwUUpaS2tZZy9oTCtIcy9pbFFFYXU3V2lCRXkzaGNuZWJGckJjZHA3UnVEWlZEUHFLZzFFamkvL0hkT0tsYW0rV0ZWMmVhZXdiTkI2WVpvWjlPdDA1eVFGK0xXTU0wMVFGTVUvQ3dINVJjT2hWR2d4OXl2dkpiWllKTU9HN3dhV1RFdklqTWpQcXFZeVM5T1lWbUoweEJ5T2VaUCt5TFhPN1ZhOHNwSTR1TW0zTm1CdytIVXovWDYxQVFBZmxyMkhabkJSblcwVFZRaHdZSDdRcVZ6d0tya0JNWm02d25DRy9idU4xb2dJendROW0rSXQvNDBmekp5aVlSazNJSWU5SVYzWVpqV0wrc29vb0svVWNycUR0dDhtdEUydzFoaGJpTjNqWHQzV0RVMExCQTBqUWZSTE96WlpnTDVYaWlPanhRdHpjT1VQVkpxNzlZZDhMVmFEdmNySjIvZkozSmRSa2NRWDhUZjI5bUpVZjV2ZEJOYUVCWFZTcUtTYmlZWjN3aytIbzUyMmdvNFg5TjdEMkN6WGlxSElLK1BMYXgxODdjQUtGeDk5S3VjNlJEMjFkQ2hFbW42WXlvblUvOVROU2tzV2hmYVJZTm5keWVhL2VYcnR0bjhvbjRrY0VJNGFnbnhiY1ZYNEZkS0VlTzZ2cUdFZDVmdWIyMkdjcHpuaHYxWm1CRjJoZ3d0UFRObEFEY3AxcEhyQlB5ZXhVY3hIYUQvZGUvZTEvSVNpc1RYRCtNK0xGcE5kYktSUlBuSlhqSmp3REQ2N2FmOThmTTlGZGEyZlI2c2ZQRWtrM3RQZ1UzVEZ3eGpsRmZPc3JUckhSVFlGVktyMW1kOHlmUlg2dmJjc0RSYmRjRDM1YUVFSlMydzlabGdXb212TmdwakpKWWFqZmZ2MHpCcXAycGVsT1NIQUIreDE0cTR6Z2IwMFZ1Y21kRXRyQUp6TC82LzlRbWtIa1JSMVY5dzVQbjFsZFVOUGFyejcrV0lMYkdBRkpuc3JESUI5bThrbzNlVDNTaEFtZjVMd2RjZjB2TTI2bGpnT0JtTnA5Y09IRVM3VGViZEtodU5KWTh4eEw4b2tWSzF0Umt6NFE2WmNjYWx1eUJ2cnhHUHQrSTdEU0o1amorK3I1eVlvSHNYeFpxMC9wZkJjMlYzNngrQlphTGRLQ0VrY2hFVDdXL2VJWDFTbG1jbkUrYXB0NUxFYTRhR1BnbzlsQXJEZTl0Q29TcG9sVDBBWHJRVTRXZFNRdm8vNmlycm9TbWlkek1qWkF4Qnk0dG16YkVTM01zSTVZNmUrdGlzK1A4Y2wzQVcxWi81NEVSY2ZkSFRwZnY1c1k2L1MxR1pTZVZseGh3YllJQ01KbUdRWENjbWw4aXRvdTQ5VnlRYjE5b2MvRE8raFFUNWVnODZuL3FlczVMVkhtMDhtdUpTQ291azEwT1JJRDdET2dmZ3FERDNXNVllcjhXMmkvcDdpMnlOUG5vekk0S3hQU1lkamhXSFB3VUxMeFZXWHlqS2h1WXVrR0IweEpkc1VVcVh4YkEvQnB5b0hmckpxaytpN0JzVWl3T0pwODBUMmlUeDdBM293T2JlNXE2dWhRWFFnc3RzVkpCME5tMlh0K2JQTlowdGdYaDc3aW1nMkxTQTZnbTZQMjlGeFVuRWxLQjNaTGwreElWTTVTNUJFZDh2WlRVZWlKSjIvMHgyc0c5V2Rrc0V5TDVaaEc2N1ZvcUdKd3hxOW1nOTNVMFNNWGtmakkxVk1qeXM3dXR2UEt2d1dCVlI2MXhQUnNUb3hMNVNKdjlRN1BoMnlvSzVwcEoxWHpwcUNwZmEwbVREZ0g4QjNTNzR4MVg3ZzdqNlRwRTZ1RU82cjIwTXJsZ2ZEdVFpVG5DaEgrSHlQYkkyM0dHVkMvQ202eEo3bVFLZ0MxWW9vVnVGYmtJdi9MZW9GbHMwT3ZLbDE1bi9EL0JBaEtPT0JSWElJaENrOEpOV21iUVIyQmVJcGF5SkhhRGw2UHcyZ3Z0YnV4YitjdDRJanVSMFhWUXZ0L3gxV1h2YjJrcDVQcHV0bVJ0WmsyaERBOHRsQ3oyZTJqNHNOR2paUjlaT1lZY2FuOWtyWEZaZHlZcTVHTE03WnF3WHo3UlBZRTdsU2JiKzZBUkVIdFZIR3pFWHo0eDFaam5laXdKVHBLSHhwQ1Q4NWVEN0xSLzFjZlpwTEhHWStZaUcxQzk2amc0N3NxZ21rc3lPZHE3NFpDOGZGVjhNSkFhbnc4UHlUOUJoUW5YWU5IMmVlbVB5am52M3FWRWxncnFsUTdNWFppdlpIQlhiNnloWHRkdkJsVzd1bGtNS0VMbFAxTzFtdjFvL09MYkJUN01kQTBYMk51cmE3amNGU2EwOG5WVEprK3ZudXpsdmdsVklzYWRTMHlLUVdaNGZNZTRYSnpxWnVoTnQrNHltQVZBbkl0Uk81U3dMck5XYzdZSjFmdVA1b1hRUGpSVmxGQkhMRHYwbU9zZDVKektqMTlBQW9UcnpxR3JNSmlBdkVtL3BQdkt0WlNyc1k0dXI0bkU1eDJuNGxlUnl1Y1VrbmVvQmp1YnYzVHBBY0RrS0NSTDR5YTRJdStwWFgxRjRZOVB6aTU2NzRjQmZ5UkRCZU1IQ3BOWVRsUldPb2FBWElGcE5HQlBDcnY3QnpVSWRadDB6UGdYWmFVeFhMTTdQQXBKVXcyUW9aa2JtNWtTcDBYY2JCMUZUK2JkL1lRUUdDUVdCZUtQMkpwcThDb1lheUpFVytWd3FKczZyanl4OWdIWW5YYlVKeG9INVIzU0E2SUpnZVAzSng2MVhoWGxiUFY5NnVHbWZkaDBLQnl5QkZ0MkNEMTJMRURIWWo4L2xpZzV0Zk1IaENBamNNYWNob0pqaXVhRFkySVZsblFXZHVDbmxyMmpmcnQzSVl1TGNtRTNQRHFBRndpVHoxQmt6d05RbDd3WFRQdWI3aWtqc3JxdVFmOUg4OUJIUHFmOUdFSk5Kc0FpVEZZajRTMllYUjlQVW13MDFwQ1pOWE42OXdQZitUa3R5RmhKQm80YjBQVm94THpURnJvdFQxR3pVN3l4M2pEOVpSSng2TTVBY3JXVnFWWkJJUHYwWUFxRG5kb2daRmtGTGdLV3lkM1BRMkJEU2ttUk81am9KK2svVFlBTy93OVh1VFllNmJYSW1vbGllZkxDQk9SenB4eXlvV3lZd1Uwby9aLzdPbWgySEV4SVFyRGtXc2htL1VPTDlnK2hmelRSc2JwNTlIbDNPaExraEdqNW5FRnRFSFhXcUJHNVdhVzc1MmRGQno4UWw2WFZJUFZLY29MYUxNTXIwV0dQb21yVStPRE1sU24xOE9ubE9keWZhUmZtV2tlTEJwZEZhcGpHVFZ5M2U3azNGMkdiM1dSdVlTbWVMeGRYM2g5ai9jcWJlS1c5bHpma3RLVHBXdDZqbFlCZ1M5YnBCWGg3cWNBSU9vVmhwbjFEYS9QdnkvYXVjM0NGMEZ1MEluM0VDdlRCelRZMmwyUnZaWWs4cGw2VnFaclV0R2djRDhVL2xtTytsVDgxcDB4V20vS0dOeWI2THVpK2RHZDFaaVZkNGtQdHJoUkNZckpkUGVGVnVaMDR3TFFsVlhrMTZzTHNyK1ZnL29DckdZLytCVFR0bkJvRURKNXV2WWZ0SGw5VnBhZGwvem9UamlUdmYvUVpUcnV4TGlxNnhqVmpnTzZuSFJxemttSGhBYktZRlc3SGtxY3A3WVZQM2d6ckNySi9PQWt2K1FuRTBVYmpwZFFxcUd1bHJiM2dhT2JXM1RxZWJFNVNKTEVESHczanNDbVdOdFNQQVhXVDhtZGlIWjdyOXF1NE5VL0ZRTnFUTEoxOW9LdGVsL3kxM2RkMGRZakQyRms0YTlmVE1KZVhhazlwYk8wSXVwV1NGSG83U0c1SVV2VmVBWnRPR2RvNXMrVTIzWWJSKzRZOUI0d3NSSzIraDJjRTg0QmducnZQMDh4RkgreDdlVUtFS0paZHhDcjdNckVwZ2xHTUE0eU8wWU95NGo1RjFjS1RpM2I2eTFQU3hqanB2OWkrVDBlakhuR3FlbFFMMW51ZU50MnlmdmFYc3NTbVprNEpvZkd3WStNTEcxejQvTHhyejBMMGd6MlNrd21hUm8wVS96RWVzdkxneU5nWUhCMUdaUU92RW5McXVUb0xYT0FVV3ZqaUxLMGp0TnZTMzkzdmNSY3J0OEowUjZ3UEE0bFFrMzBZVmZQQnZSOWZQeWFIanpmRGgvbS9YbS93dTZMWEdDSk16QThZN0E1VmVXakk3c2N5d0M4MGJSREpPSnpMMTVaaU02Q1QrK3BzNWhucFI5bXc0cFBmWEtwVFJQNTc0a2wyU2M0OThHaUo5c2w4dmdBL1owK2ZiODlOYlhBbkhOZS9KbmZGUVZOaUlSREY1ODFJc2RjMTYvWFRDOFhvV0pmOGRUMU1vV0M4OGh0OWhzZ0h6dnM3K0Y2Z0tvOGU3ZUlwS1VoUmZUUlJnUml5VzhzSk54cG1YVTYyWnJGdjJ4YjJWdVNaSU9nVFJVbmEya21xcFZRZGREbS9DS3poV0MrMWF0V1Q0RjNRSWxQT0hvR0padDR3UzJIUUpjK3ZJbVZRKzE3allGNE0xdmhNOFNUdTd1Z3hmUFFjYWRMUDhrQmtKNUhFeEpWeGtad1NUUDdobm1ldFBQbktlaVJ5RGRudi9IZGM2QUpWZXFFc2pZNFJxT1p6S0FLajJqY2E5YjB4b3locEtwRXE1emhEaUM5bTRJSitFQk14dHdrVXk3cmR3RzhBUFFZWFpmVFgyR3F4R2s0cEJ0Zm1IaGNTMXAva3E5cUZuWXA3MlpWMk1LSW5Mekp3MmlRVTJXWkQ2dVF5ZzlXREROVTFLc1pkMHFhTyt3djh3U0VHSGhWUnVrWldXZnN2diszNEFlbkRzWldLYmQvWWdRMEVFTGhFV2MycUNiWm4wTGhlLys0UUNYL1FEMU5sdERUZXhuc1UySXpnMEsxQjFWSmpmQVpzYnJFWjJZQnMvdGdNcjlyaUZMd2h4NTQvK0xWNHpDODhZYWF6eEh6WTZQalY4RDBZdzArV0xwUEJXYmFMbWVnYXoxRWVIZ3ViYkhpNDAvWGdiakR0dUhBSFpmaG9nQU43OWpTd1RZNnNITytBcE92cUtDT2c2TGd0NXJmTloyOGptUmJJVTJxbTNod0pIai9WaEdOL3FWMFNVbTRFeEN3ZkVLSWgzTGttelBDVmxiMmJFRnBnc3NPdG9EUVU5cU1CWnFrdmFtN2JVS3hmL2RSak0yZ0xUeVhPK2w2OXYvenBLUXQrT25GK1BLU0NwcUNWZi9xMzBPRkJVOTJWZEI1a2JwcVpwalpZS0E4Q1NLY2JXWVRxNE1xaGh0YVNJMCt3Q0ZoWUVJbklWRWV4RHhQTnk5TFdJMEExZVBlSy9oSm1IblVtS3JIV05WVmFRWkJWTlg3c3h3VnNEL2VZNEM0Mm5TWi9rRW1qNE5XT1lJZzJVMVRXZHMrYXZ2VG5Tb011WkpRbU4vM1Y3MzRxdVdvWTZtVXIrOVlNWWdNK0gxNmhlMEtQSWpObU83TDBwT0VjTWRXWm9jSnRNMUc3SXNWa1BGMCtkZjFkak1yZnd5MXVINENEUXZpTkFKWmE2UVo4U1YrYzhnV0xNME1VZFRXZ2hRYzhNckRsOE5ZV2lGQ3VxNzEyMWF3OGJaWmdGdldTZXpmeFQ5aHhpc25KblFzWHVKQm1TRnNsL0VZb0N2ZG11K3NkSHNhSHVnNy9CWFpHWG1wRXNuUUtnUTFtd2h0bHM3TUZIRllMOUhkWVluS1RocStyeCtpRFl6NFNSeU5RVWVZYkdNbENKS0ljdDByaGhWaEJJNW1DaVBzd0N4Zjk3YXkydWI2TzlCMVJzbVh3U1FYK1JCMUZ4d1NONXp2YWRWQUVESnhzSnVZZWh2dXJUSzIva0YweHQra0ZJbVh2UHQvNlFMaFdUNDUwSGxGemV3VjFJVEdDQlkwMVIvRW0vWGFpclBrR2dVZnBtdFpwWWZDdldxbzhrSDd2Ym5tQTdsTVdUZTZjaXA4VE9HQzFWVVpFNGtwdWtDbm1EU05JYlRnbkdSS0M1VVNXNlJMM0NQV25sRFd5cTJuRlNrUFkzTGJGKzU2dzlWMGV2TkRiZkRYRjIvMkoyT2crdGU5MDlIQ3JJUzdreHFPWVc5S2VDWmptZWdsMFBqVnE0Z044UjZpYnI3L3Vqdnd5M0RCSlZMVVFyeitxZWFucHZDRTNJNFdOc3pzSFl4WlZjZDdZc1hQTUc1RjY4b0pjSU0wcWo1S0o4TXZjVGU4bG53UVpiVTlOdkxpTlh4b3JKbGxaQXBxaVFCeHYyKzFvVEZwOWtMUEFOMGNTZ0lLREJCZmd1L3hiY21FUTR3S1d6QU9qMi9Zb1JtYzhTVVppLzR0WG03ZW1OMWpINGV2bGVmLzVpcFRMU0w1Y0M2Yk9qRk8zeXBMSGNqaStBOEVjTDhSZ1ExMk9aMjlEWElHM2htcDNWcjF4QlB3WUZPdGY2NWpVelcycjFSc0pMTzV1ME9ubVNKVXhnajhxZ1drUndXNWI3enNsWGJoazllWE1Kb3VyT1BRU04xZ3hTaTJGSFBkcDViS1FoWDJkVmZUaGxkaExOSjdWUEJIUDJpUityeHljZXN4cGZpcFo4WDFWNmJUSEZVSzUyNHJ2YjJXQ2xhK2p3WDlEYjFHM0NsNWFYZXpSNTFqSUwzYU9QVGFPZFZmOS9SVjhPMHcxS3JLSElIVDIyM2IwK2JxcGFWNHpJSVJiS3pUOWpzOGo1ZnB3ejRaZlh0bGFYUjFGODBFdTUwT3hZcU1ocW9rOFQvanJWZ0tveTVrMWhqakwza3Z6aHpCZThBVk11NHgyVm9kQm9sYzFZVzlETzgySURXNUlHSkJLd2tQQTZmMDBLQldBS2I5UGlwZDltYkV2aFZvVy80b0JFWGlEYTNKTkkzOTgwSDFvaVgxbXEyclFIUXJCYmZaWVMvTVdiNk53cGhuTHo5Qld1TEpBVFhiV3ZDeEtXZmRrVDVWYUFPOHVWRVZtQkYxQlJJYmRBTk5wN2Flc2Z6UnpuT2J1RkJGYUsxRWlzNTM0TlFhRDI2WlExZ05jNmI4K3ZlaXpGWTMxckNyTGRacWxwZHA5bWFKRUN0UnlrU3ZZRlM1SHZLT1RzNkFLQjBsRGtTeDhwMkd2M1kvWlhHUzJaZEtqTkp1S3VnajRQYlNNdXJBMDBYVG50Zk1yRG5Lc2dJNGpyZnd3aGdnMEpZeG10bkFFUjFoUFR6U1pUVUkzMmQ0MGw4T0d6c3FRaVpFcDNNL3hydXNQRkNzMHFUdzZMSmdyRXU1cW9mbUhSTTFBNk9WWkZ6MmdNYVBFekdIZ3NSV0I5NkJ1ZEQ4TGhrK1FuemI5UzhoMGZyQWkzN3NRaWtEcm0wWjBpSWJmalpNQmkvaDF5YlZhMjJOYklZSzA1SW5pU1BCU2hmWi9KMUZyYU8xMy9YU3BhSXFrNTBYZWY4YTBHWHZxQ2tjbW1VcTVmUm9LSzRudXh1dzEzTktRZUFnWlNtQjh6VWNSWGRTa1FHYllJWmtkTzVhNUVBeGtwV0FnRTFHQzkxWHdlTk8yRVdYS0tUSGpkY0JJV092d2paMnZQU2FVVkF5V2tpaUdab0xvQUR2b3ozbjRXTm1FaVdEMkw3SktmSXZIRDB6NWtJZ2JhalhHYm0xeXpzQmY3WWwvNEV2MnpuZ3VGcUZTWjIrbmtsTXN3Wnk3bVB0aklXN1pJSENtQVVqRTNhUkRzNVgvRnFJZ1laRjFZZXBaVmtBc0VweElNR1JLZURZSWV6VWZCQWx1ZkNMWFdsZ3c4eThQZi93SzRZL2dmdC9sODJsZUtad2ZOMUZQTlVpUlJPSVFuYWpJZVlmcFFSSjhUL3ROZTJucUVjaEY1aVhLdjZHcTB3RFRpeUhZSTZBRUZGa0dnMldOaWRjUXlNUHBNdVZYSzEzcENwWDZ0WXYxQXZOcmtmVjRwMXhhNTNQQ2prbVpkU29GeUdUYU1jbnRCVmtHS2hrNURjODBuWVlERWRUbHlUZnRxK2Z3b3FIZ1FhRXlaUFE3M0VGay8wWmF2c0NOTU4wM25PU1VlZng1bzI2SGtTYnllM29EQ2JIdm9Va3NISUI3U3VjWnVGWktjclNTeDNKV2VBckF5Mkk4K3ZUMFBMdVNMVTdiYm82RmtneHd4WjlyTFZoYmkrb1ZQVTZPdU56cm1ITGI5TkkxUnd5VVVhcmhPalBjeFF5SkthQ3piV2dHdlZGR05KN0RicDhkMGo2Y1paZURqQjM0SDVtSjI4bVA3U0FKeEozYXQwR3NRRVJCZUMvMTZjTGg1VWhpc2lZU1NiVmp5ZFNtTUFtNFlZbjdVSktzL0tLeFVTMDVIV2pkemt0RG5nbzN0ZkRSRFh3bEwyZmorbXhwbi9CbnlxcGdYZTE3NkNTWDVhbS9uT0FyVWQ0aXpEQlRVaURuU2Q1d0N4R1dLU1htVjU3T2x6dWROeTkvSWc3L3Q4TXUvTGh2ZWdoSE42TisxblB4bUcxUmpHOFRIVUF3a0lHTmZNNW1XT0tsWCtON1BLall3bHVkTGdmeGxMTThWYnp4STBDWko4aWU2TFY4ZEZMT244OE8vVDkxN25JN2d6U3ZVNzVkQUo4Z0RkOXVoMTlDWG1BR2d5L2RoTGlKTktXS1F1ZllLek91UmVvZmxuU1R6aXh1eGxHdGlXRmQ2NCt1NGxkNk9VV2o2d1FyY3BncFBCVDVJUi9qVTJja1RBOHBQYXBiYWVHZVZsVUxjV3dOMWpDaDVucHFONFdJR3dabWYzRGZzOFVCNHFPMnhsV3FlWUl1OGEyUUoyZDNXdzdKYldaOWNta2xMUUxiQVdETEkrcjVIZjlSQk5IT2h0NFplSkkrVUl1MDl4UnR2NmJJT0I5L2pIUkdOVHlhcDdXZVBBOE1zMVRTem9Dck8wUW43ZzBxQ3IwRWI4M1lrR3lxK0pJTkg4TjZnZzgyamZqUDNJdnpTUHltbHpXSEl1RVVMMU13d3VsRmowakw3NHZOK1hxbkY2OWllUHBFS2dUQVE1MUh6RVRoNHZvUWxMc3hvYXhpSXhRMGFQWlJVbDJGNFNMNFd3eFNhc296RjhpSENaNkx5TjBZNklNU01IckYvaEk2QXdRdG50NnJwTDJJLzRHeXJNVkRvMzRiNnZ0OHdjc0l1eUUvVFFTbnVOd0U1UWNjbEtYNmR3SjlFNldIanFGUWVwZzl5U1NhanZEUTh1WHRFb0haUktRMGMwNWNuL3Z4bzRjUFZ4eDJaOUExeW9GM0lRZDRFeUtpTXFmSUtzZGdPbmlTVjlvOEwzOGF6MG5qMFVRZ05aS2lScTZtT2tEeXVxMW1OQ3EzQ2x5K1BYdnU4WGhJUksxb3VMR2IyZGRwTEUxanVUL3Azb1VMRWNDZklUb01oTHBxRVhOY0x6VytCK1V2aDIwUUhhV0g5ajIxM3RiTzFBQXA1WU9vcFpUVUFvaVNvU2hFSkIzVnNWTHRHaUd3OVludmcxQ3N5NnZoRytuWllma3FRdEVTYWN1ZmUvdk56aVNnd3hwSjRjQWZ5VzhrcGJBVkltRGdqY1kvSkx0dXorbysxbW5iTkRxeStTY2xzWlNOUEZmSEJKRkl2eGtvTFVaMDlUckRqZExzb0lTTCtxbjUzL1J1bnNTOXM4aHo4UG91SWozcExlRXFlRC8wcWZsSWt0Ny9VcW53ZEd5dGhFVmhzeGVyVFZHWkNyWjBDa3VGdERHYTBkUDZMWFlGYTdNbEVZVUlMdUdLUWYycmN0b0laMHN0RkpHUkluQWZVUjJOMW9PeDBZaVZ0NXdibldObHJHU1o5QmJEUHZOUm5iUFFqTExyMDYwcStMOHg2VDlUZSsyUFZWc1JMZHc0cjM0TkVHYVVGRGFCRFpFL3BNUnBBMXQyODNQZ2JOdWJTMC84SjdpTEl5VmRYVVhMYm05NDNVMWtwUFpOREhLN0tPc0RGd0xTVkx6S01FSUE0N3ZlejNDdFZneldNaVNhN2RlTUY0aXgwaHpjaklGbVI0UHRLR2IxeGlhN0lCSTFFMFliQVVkU2NMTmtDQWx0NGxueVptQ2NzbG1BdkdqMVJ3ejBQMjdtekFnb25lR2Jvb2h1RkRqRlVHNFZGTFNxYk1zeFE1bzFmRGhmdnl4WHloYWlwZUFsU1paT1JicElZWE4xczZ0ZXFKY2kwZnU3aTlZUGJMOG1zcFEyaXZ0eUNyYTkrelZENWVPc1VCaEc2S0FyTHpzdjk1bEtPYWhadks2bkVuQ1NhTFNxSG12WkJNUzgvc3crWWRMQVVWbkZZRVFBSmxXR3JmcDF3QlQwYytINlZmZnlUNW5sQndkUDZSQ0ptaVVMdUNLNVFnakhXdWorZWx2V1R3QUcra2VqM3RmVnRuSWlOMUViQW1lTGpJOUppK203Ui9hYWhsenJndVJkNGJ6bHdXTklqZHFTb3NUaTR3Wk80ViswZGNOUVVyM0sxMG5IOXdqMURBci9qVnpYKzVjUGF5OG56cDFMOVl2bjZMb1Jpd2hFVUY1eXBsYi9HR3QyR3prYWJ2VmhBZlVuZHk0bUc3VXlkaExQU252MnVlK2xvcUZHeW12dE5vYTVCd1ZVdk9hMUVkaTJZZCtGYkRmQWhqOVJhSUNITjcrTEplR0Q0YXJ1dUJiT3lYM3pSSjg3RkJsOU9HeXB3VDlaRzBYQkpjbTJBYjY4ekJacTJnR0xkenM4QjNDY3BVcW41QllyTlZHMU5nOE5lL2ovVEs0bDhabW51MWdWRjljbHBCdnpzN3hNbXBYY3hIcmJwSHNEcTFzRGl2VFQrZXJpNndmR2RLMkpoQkpwL1JaRmxSWGlBNVRGUGtpSDI4SERmK0kreHNwbkJUa3NiSDhLdWhHV1ByYmlOSXhUbkFtQ05XWXNjdG8rM1dsNzQxdzY1TWZrTnByOHpoZGY3NjVrUHZSMWx6UUxJVXZCQkxsWmM2TXRXQkphTE1ESDh1NWxZYm9RWkVxTzZPVG5oT3NVeUJydU1MY2pkR2lxL3VLQ3ZWSGlRbVNuQ0l0MFZKSzZueVFHRmhBTkduamc5dWdUZ3NhVXVGSUw3V0RVbk1qaC9IcHc0eklDQWdJQ0FnSUNBOHYyL1JVZnQrbTIxOVFWKzM3Yzc5bjNOWDdTci9oOW54ZmI5ampnZWkzMjlnTjl2Y1E1ZnQrbEkvYjlXRG42Zy8yMzdaUDh6N1VyOVNoMzJmWlQzMi9ZZDRIcGY4TzBDKzN1cGNOOVgwQUY5WDBmR3ZuRDMxZlh6ZnMrd3EvVVRmN2ZyUjc2dnEwMTlYVjNmVjJRQTlBQldvZGVMRFc2OHdURUlYMnhZVjc2dWtsbUdnUWJOK3JTOGlKL0xwWmhoZVBaTFk2dVZHWG12VlZiRmg3dzdPNVU2bi9FWVZuMlRQRUxTQ1hPSkxuN2l4dllxdGtZRUZMT3NkNnVab1Mwd09mbDNnZFJCTFc1MUo5SFUwaGVMb3AzZ3hudGtmRXB0bmdpVnVneWkrMUkrdXVRTyt4aUV3NDloZjdOekxaOEJaUDc4NWxMY090d2FvWk5IOC92eDhjL09qampOZXBUU1lqT3o3TTIvbFVoTlJhQlVsUFp5VWJFVllVd0FiTFVMMGt6N2s2bVJVVzkxQWVvMjkybU02SVNHM0N1S3V0ckhhSDVCZXAxWExSZks2NURjVVdPRVVYVUtIdVVSOUlobDRTRUdzVFFDdlRjV0dETzNpajRaeHo1NW1mMGJNTW5lWWxNaFRqeGtPQWpydzNVdVk1M1Q3bzNXcWFQNnpWd20vWGdXaVZQTzJDc2ljWm1TSXc0eWRoVTFmeTNsdDVTRWVwWDhMVHBLSzhNWEs2ZzU3VUxQeXlyUHFCcTRQUUpYa21LdHJZRVE4WXF5bCtqUXBTN2IyV0l3S2J0ci8weG94SHBvZ1k3RnpKOGVGWjhwYm1IaUdBVENZMGliSkxwaC8zdStuN3FyVkdwU3FOclFSdmlUbWEySVpMUDg2YlZUeTdHazBsRzlGQWJKc29xeHJ2OGpLZmNjSXpBTlAwMGx3TTZ4aFJveWJ6R200b1I1T3JIbDVtdlc5MjVvdEJZbnFLVVhhWHZmYXdvSmRYeXo4NS9TT3JFb09WNlhpMm5ETXYwSk45eTFkUUtaczRYdm5sbHV5TVFHbnVTN0d4T2g0djhFRUZlVnltWmM4c3J5dXdISVc1SHhnaFU4bStxSENHN3pmdWRwd21ZVjlZMFErZDlham85T1JNQklybzl4blV3eXAzbHlrQnZwZ3pNWGpBVkhORGlCYS9wTGJKUlM0dCtpUDlGVWI1a241QWFNTG01dnAxODFDR0J0ZjR0VTN0RWtpSWl4WmlDT0l6WTBacW9nSzRDYXhuVGYvZmNoOC9UYmtYTHNUNnVhMnQ4MUR4L1ZSUzdYcHJrWUZMRGpzKzRHdUwvSGE5SzZ0cG1qcDExemhDSGttTFRSempIZStMMnlQM1U1MWhDelladi9mSUNHT0RFdWlOTk9jY2lXRTlnZjBIV0VZTDR1UUhuM2E4V2FzVUF4MDVFS2hmN1BnTjA4Q2FCLzNCZHlsZzF0VW1LNWpIWnRDTnlPeTBiSFRNT1NTaGJnSWJKTUFJL2s2aXRTUmxKeUJZMUhsQ1VnUzQ3QmlyZHd6MlpnQnRYaEY2NzlIcUc1V21MNnZtMEVpd3ZWNkwzbXVVdi9Xdm9WMTljRnQwb3RObDhTSU5KbjJBTmRFWVdMRVlzUzdQajdXc1BqdEhqR2RUbmNEWUt2T0dWS0xuV1NoUTNOV3hJVFJlNEVqYXNFMytPS3BpZHQ5eUdVYzF6Qzc0LzFYdjhBNHlMMFJ6cmRBSmFOSFZFbk9BOWZ1dEdGOHdTcGpjMHdpVm1GdHFxd3Y3SG81Y2VGRjRJTllrUmpSTEpobDNJc3UrMDlCSkhja3dEbmlsaHZNdFZ0bWJxTklsamhHazhUZGwrNVZNSEFIaVcyZTBPNnN6eE1tSm5wMGROOW4weWxRVG52Q3VXbit6RFAybmdwUTJxMk55elJxZHF3UFFET09VdDUxZDdnUGc3dGQ1bHNJSEIvK2puVnppbC84Z0hFcnRTclhJTVZBK1UzTHk3V01nN0YzajNLa2FCWCthREdNY2x2V0h5cUpNNlVVK0t4Wi9KTWpNT2k4T3JXdTBMVENiMTJ3S3hLajZPZm1RU21TZFFwRmp1YzdPY3AyblBkUDFwaWVEdTRUMWRKZTh2Z0F5M1BFSHlHY0N3TXg0UUtBWnF6SkovZDFHVXo5TnVtV0RBN2o5UHBWSit6Wk5VbXJxWTF2K05VdHRrdFlFZ1RUZGJadnRzRHB0OG4zdmtnMDBMRkpMa0Z2Ukx0c3RoR2RJMGw3SXFaRFZNM1dZa1VPVG05cDFabVhtQTdrY0ZmanJtb1dVcm9QL01NejErbmw3K1JjWWRnMC9XNWdQY2w0bzN1cGx5aGNhQ1ZrTFZBTnVzazZWM3hLek1WY1dOclI1WHBjODZBMTBodU5zZldmdzBLRlZxQUZwalFzVUI3RFlySE9jY1RWWldKRmZhbENRTVloQzBaU0tEZ2ZFVis5U0RGN0NlTTJtTE5xZ1NpdlZ3cEIzbmdjVGlnS1IrZ2paT05OejRRdEhOSCtMRlBhRU1TQzQ4c0lYSVRKV2p5SVVlMkNCRnFMZjcrQm9ROWo3Qjg1RkEvZHhvTlR4b1l1dWJEQ0pxckdNTmdLMnpmc2NzVUExT3MwVjUzd01HVCtOUnZ2NHBsQjQvc3FOd0FYcWdTQlBWZGNqZURudWw1a3hUYVhDOWJiVDMvVXE3RlBVVUJCdGh4SkdPcVFMaWFGUlBhaHZPc1NFcEtMVUhsSWxQT1ZQbWZxZExaUXhjNkl6VzlwdTVEWFRpQzhZQkVUTU1XR1JLK0J5cXkrdlU0QTJkU0VhaUVXd2RqREZ3ZnhUOURwZWxGaUw4M0hNNUFNRG1LRkQ1Y2VjWVFjTmIzdzdSNVczOWhETlNqU21JamJqUVBpVHdaQUZRNmIvRGhoNXc3bUl3MlkrNTVkbjRMU2JhZkZxZFQ2Mlp0RjBDNHVIdlRlcHpQUnRqNFVpSmNWcnNMam1ndzJRYk03NXNvSW9SNFpOcktlZkw4R0ZKQVcyb3pWeDM4K1JJMzdsVmdNNldjTEgvbkhqODQwVVgyU3VEUEZrVEh0N2p3cjJheFUvMDFWblpseFdvRGU4KzVEMEE4UndLc2RQNGY1NEUyM2taK0xGMWNrT2UyNTFJZWFVYTF5b2tvOGVWUlp4MG1rSUVZejJzNWVoQjQwSG9oSEE4emlDNGNVZmJwMkJYVEpCTkRLYVBpVmk2OXNHZkQxcktuQ1c5clR6cUFQVGJFekF3Yll6SjRmMU1rNm1MMDBJQnZoSmFCMUZJQXFpazBNMnFqY2RydGNHeUdLSnhtUkdSRzJNR0RLamZqTEZnWVA0ZVgwN0FtK21PNFVUMXo4MSt0aFJBd3ArMmM4QTFFaW1uOTdQWUVRa1dUL09VV3BwWnpNQ2JHL2MxRmZhMHZhczZYUzdiRDI3SWNRd2lMVldROVZHdFNxWmRUTVlFSDlINVBRZnEvRVJ6Qlo3YVhaOUpuN0pjMTBJOEtzdVg5RTRMUTFkaWpsNG83eXNMTWFMOUtqYVRnWnZWM1daNVFzTkZxUzZIb3c0NDlVMDRmZWZuVWxWREFUTmI3eGtLcFJZUC9SYnUyNzFsTWEyRzErNHhjUkh5WTMrRDZidGxSMThmVEZiOWJvd1djYS9hbWlmVEJlWEJiQ2NDL3pDUUpVLzZ5Ym9peGhGcTZBUWlhTEJxN1E2NEJ3KzFOczZDU0FJRWp0U3lRN1EyK2FnOE1CeDFJMVhuUENDamxTRWYvUExIOXFNYmlGVi9JWDRKY3RLK3dIMVhpcStUdDFnR3o0U01kUnRoMVpzRnBQdFF2UjlJT1F5bWJ3c0hJN3pramNtZDA2QWNrNldITzRmc3RjRW9vQUNQT0JEc2RKVlpLYzhGM3QzbUhRUlZ3RldLRnBmcjZuVmNQNDNsRzA2ZUh3QTlpSENnTFNSRGo2SmE0QWFzOXpScDF0Tm1VcmVQakFzcmpXRkRmcGEwOU14UzJYS1BqK1lZWFlOTkNXVm1RaDdCUk9FdldscU1ObHArTXVONEM2YkZZYklXR2FGMjRxWXJ2ZURnVEp2dGluaVJtQ0N6WFV4WFlwYXZJTzQxUXIrekJsdys4MjhWaTV4L3BsYmhWeEZ2a3ZVNmswS2RtN2h3WVJzRlZ0S3BlNFlXVTlKMUVicTZZdVhwQlRnNUdaV2taWk5TeWxiQy9PMG42SjEyR3V4Tkwyc3A4N24zdWZHVnVQVThBTkhlUWFhSzFjKzkyZ00xWTc4MW9oZjBCaThvS3RpNS9CWFBqbWlES3d1U0RxclVxVU5mUkRtRU1NblczY0YwN2FrbWVzNHQ0V1QvaGFSQ0tRWnFqV3BXM0R1Ly9PV2tBQ3pHUHhydkRpZlp5YjEzcUFHS0ZvVWNVT3hYWHlEcUNZN1JSN1pZZjZrRVAwdnlRZmxQdDhoYTJKa2N5Rzl1QnVOWkd2d3A3ckRpeTg3S0pKR0RGdjRldHFhbGlDV3lKeHVUN0sySG4vQUlucjdwb3hUU0JCY1c5SlE5dHp2WlRvZXg5bUs4dVhaOTU2TmZRNG5iZjNkRHZUNCtydUpaMjFKUjNmdmUyMWRSQUt0aEEweGhkMnRmdVYxY1Q4QWs4Z3hRUmFEZE5XVCsxaUw2WTcyaGYzU3dWd241UXhIK01ja0h0RkkvcWtYSGxIYzJrbm1KeEZ0bkt0Tms3NmF2a1hyckJzMEw3d28wdE9ON2dQVGRMZU9pSnJ2UGx2RWllV2thRk1VVzNsa21veW1RVC9saG1NeURnYndHYU0xaVJyZGxHM1E2dTRaVnM0ak93NXB3QmZSdXZZRnJlTkEvYm43cFU5Q3p4SE5YZkFzejU2MnBLUlNHekNiOGlVN3JZa2NOQUtUSXNHWnRveTlVRHJNWFhRZ2RRSEt6VG9nUUdYT1BYamovd2szSXhFK0lGdkFnYU5JcENqT3R0ZVJlOUhsVkJMcUhWT0R0cm13N2lWRlFhK2t4Tnl4dVE5T3dPTW1IU2VKc1d5OW05RFh4MmNvejB3MG9iMkp2WEtnTkd2UzRnd3pvQVNJbTBsMWtFQ0c0Y25GUU1LQkxBYmtwaXhWV21DQzBSYkEzYzg3ZTllWnZxTzVxOUlTYitXclF3TW42VEZmdkVlejl4aTdNazBIQ2QxS2dkT3pmVWZzd1krcDJlRnZaajZIZzVJVzE1SWN5Y3RLTzNkaW9VOUlQeG94ZUJuWXZ4cVNlUi9NZytNVGh6U09qeUJObmxaV0s2RmZHWFRMWEZCbjVUenltMXlVWmdkdjY3NEU5a09zWEc1a3BQWHc2a2RMVVMvTmNtcm1idHJoZWVzTmh5Y0h1M0FJYVVsUUtNOENHQm1ZVE8zci9lNG9iVXZTUUxFM09Ha0YySEFtbnB5QkZNbFp2WVFxbFA4ZVRyOVMrU0pSVHcxOGZHdVp5V0hSNmJkOWF4T1hraWkwYXNnREk3WDZWMWlZWmZaWVJjcW9wM2tHditxL0tSWW9ScXVKdjRkbkVPUkNqbTRtTWRGMlR2MHNUZkRIS2xoSldEbjFEaEhSWkw0dTlrMDdxSFpJaWxrOWZ3ZDhqcGdyT1MwRVhDcFdEVXFDR0NQV2o4MUxObXFkcTh4QTRCZEpIbkxoaElXV3ZueUZ4VFFNOVNSY0c4TlNVeFlWcnNBQmxUM1I4a1V2azBSYlpxSUFTYU1lakt4WEREUkZBMFhpZTBBWVRJak15bU9LYVMxNGxvaTZpdkNoVHBqNVVjYlRTdW85MFJnc3MxTCtGK1B1TVllVU1MLzFtS3ZoOEZJcGxlalkvRTRkaFlROEZtMDhMSXRIZzJxZmVKVU5SRVpaM0ZCTW53WmRKMmpDeWQ2b2U3V0pTNzh4cm9CK2RkUG5aNUcxd1I5VGhpWG5zRm9uK3pqTUdCTlJYTnEvb2grVTZnbTBxbG9TVXc5Z3pqa09NN3dHdTlJUithWEpOZHZxRks1WFFpQWwxTWpEckpBcWF6R2VYTnJNYmRtMWtZZUVaRVNMb3M3aGRTQ0NNZmdWM1cxYjcxbitCTjZkNlNMaTNnVEQwa2ZwOXdVbmZqTExUMG5Jd3E1ZTJlWVdMdGZoVEZDYklzb2ZoUURZczFSTkVwQzdReGw5NDhwazF3bjdTRGF6VEJuVjFnS3c5VGtvNnYzR1pwMEJ1TURtMmN2VEw0NFZRSUZXUUpmR0g2V0tsM1FrNDVWWW1tMkN0WWY1V0RXaGdzdC9kajA4VnRldnlad2JLVkZNOXltUnN4RXB5WXVoTk5wMm0yRTJ5TTJrT01ldGsydVlINTJRb0xIMEZ3bHFJM3A0T1ljUGk0NE5KZ0d4ZTV2L01zRGRkMGVOSHN1UURCQ2RHK2hsQ0FRSXVibWgzSUtRajJUV2J5SlNkdjF5cWJ0bnFiWGxHcmxUNnQ0bXUrZWxMRGNrMlAwUDg5TUg4VDhnQ0JteUVFN0xBc1FMaUtSTzE4T2NTR3ptQWhQU3ROODNHVGp1TnRseEVSNkk4MytTUlBDVFlsckZHTzBJaW9BRGtrS1h1UlFxcmluMENhR0dUdUpWbzNHSXM3RU5Uc082V0VPbjIxNDd2eThjUEZoZGZYSzNnaVY1NksxR3RTQVRwbWdQRU9ZaXkyL3QwT1JvNWVaYkl6TVJ6THJ6WEpHdDVoQ0NET0pyQjVROWhQZGtCU3pWc0VSOU1LbWZjODQybWtoL3djcXZRbmJtbDBvYVZ1NHJjTUQ0S04vaS80TXYyVHMvbmxMMkljSlF5UHpRNEs1NTNybXc2enhpMGVmenl0WTduSm4xSkp1UW82UW5WYmk4ZFpYcGEzL1g2RDNVR0dLK1lPWDR2YlQ5aS92NjlGbldHMHkvSXIrdUROMnM5QzJBMFh5SHF3R3ljdmZVRDRRM29UdkVuUmFzQWthUXhUYlNVL0FGZ1JtWkpsa1Iyd1FaOWtGNE9MVzNkRFBJNHJaOHBuTlJYTFFFVmZmVUVCSlFqb0hVZG5vTk1kbTV2Y09xYUZtNmpRR3RxRzVQcEsvWnpXQXVwVk56bzYxSmtiZFhrR3QrNyt0aFBHYVRmbkhmRjVsSnlWdm9xdW9vWHJZZlZvcWhSQi9WK0VOcEswTDU2WUplRWNOR0lQOHpybmJ6Si81bTRFaWZ0aytwNlRLMHoweEUyeU1wMyt4ditFM05RQlFQeFAzT3oreklrTHFYaWhsemwwZ1FmYXdRRVhISjg1TTcvYlVUZUpOYkVWZE9pYW50Q2ZpcEZ3U3FEWnNrQ3owa3VSUnlmK3BWQko1TEFnZmFSd0dGcjdPRDBQUzFReFJnRVl1VEVnaGtCci9HSUMxTzVZeTlVcnVMdGRlSEMycy9jSkFXaDBnSFFKWlUrVWVIVWl6TWxJNkxER3FRSkJ0ckNzR0t0YURUTFh4bkZweWk4RFpvbU1HQnJRY3pkS1pkMXJhT0dmRmFvczhONjF2a0pvUlJWQ0xDTmw2SXdlQWtCdDZQOFh3ZTZGRW1sVXppRWZNSmNaVUowNG41bWdJWC9Mb1FSTHNtNm5zd0duNzR1S00vUlE4R2Z5VmJERUFCZUJzV2xoZ1draUhodzkvYlNUU2lOcTEyYmM2VjBTRTl2L0I5UlJuVnRjd1ltdHdXOXh0MWJNcEVWOUJNMDc2ZFpkakRhNGYxOW1TdXdBRDBxeUVaTy92dXpQSURoV3lRRUswS2VhZXZhUnlnMVpwRFlSb0U3aVpBN2lja3JsbkJabWh3WHZvZXNLVDBid1NRWGZMdnhuNU9yL2V5SWNoQ3BIS2YyQWNZbHJjOG1QZ2FnOVM2ajQxaUlhQmtMS2lOeTVtSUJ5NzJYWkFmQTduOU1qc1lybTBSYnEwMHg2aEJabm4rQ1ZYbHZsNmNORHZwOTd1dTkyUzArRXZFcXBRc3RaV0Y4T1E5YnpBT3pXQnV0eDdHbWg2OTMyaUpDdlh5SXV6QjJPZUZlL0VXR0V1OHR4cTU0YTY0MmxwZ0VoUGtuSlZSNTFXOUFMRkpuWjBlQklqZEg1WjF1UXhqMEtUS3RJNi9nQlBNdytYdk1aMFlMTjFLTjYySTh5bG9oUDJZVzd0UGxPL3k1SVQ5TVQrcXlLb3hGdFNkejFjcjB5UWNielpyRWZDSy9Yc2JqUVNsb0tnWXJJK29DZlVWc3U0S0VzdnRzQTF1VDZIODI4REJPSW44R29MK2ZqNkNjY2pyVHE4RUR1UXNJUVdMQm5zR1FWUDFNTXpBbXBOK1FoamRRaC9xM3YvcWNqbndWaHhHOHp3LzhRVkpna3hHUFNROEFKdEFoQWJlMDlZZk5ycTliWnRsanA3dTFqM001aURWaXBrOXdHUk5FVVZxNEc5Z2hycWx3Y2RXd3VyVi8zUDJzZnVETTlRRmcvb0ZWVytodlRaMGNvaTZOMGxkVW0wRmhibjh6TUl3K0JsSExHcHEzWFZwd3hZTWQ5d1FBNGJYOUlpeEhzam5ZU2syd1VMOWVTbEtYaFdUaXFWM2wwWENBSjhTbEtWOWFoRmNmbTlUZE1mekp1RUdyeWc5NnpXbDk5OHdpV1E4NW1LZmZxeXE5SGFHYnVzcTZTVzdNWkJuVEFtYThWS1ZNMGliY1lycXBvSHBPb2dmWlA5THE0cFliTnZFdE5SS3VnKzJlS0lEY05LbjJDQ05wWG12bGszR3FZSmVQN21CZkxNZ2FjRzFtU1lCenh2bUJzSm92b0ZGb1Z1Y3J0V0ZnbFJGTllPUzRVM1BNMWJrVElweVJEa1hQcDJwNE4xOXNtUnhIOUJkZ3JBYnpIYU5tRktyZEptNHhuVFdPeGtRVnZDK0U0OHQrZzBFbkd2UWVQSnluYWFBWlk1S2F5dEFlcmJzMWRBTldnQm9PcmVkNVhRZWQrbEFzdW0rV01BZXgrK1EzSUlnd0NxUUxMK3V3cVF4bEFaR3VqYVN2eUI2ZFNldlNEeklRblhnRDBSVk5aZEdlTzhlM0RZVTBsamo2SzNZRTltK1EvVkwxeHUyTGRwWTQ0MFF5V281N3Z6REwyS2VkdlRPLzBPT29zWENHcVh6QnRDaUNhQVpZc0Z1cStMSDJuL3pnd0FIMjdXWmt2ZXFyWEVaRnhuZDA1dUI4V3JFdk1KTEJwbkdiVC96MTJSRlJnTG5sd3lXczhpaVNsNm1oTE9zQ1JKS25QU2JIVDhJTU84WmN1b1JvQXNRVGxCaWlzOFVQZFZ3Rm9JYUxZV09idC9sQkxaeW5SRWo3cEhvSllWNldTMktacVFKSnRFL3BsYTB4L3czUnhJQyt1YVRUVkl5YjhacnlvOVZMMEtiR3NvNzc2cVlINWVvMmpUWi9tQWdHVW1TUTJQMjFiZjNoQzZTVXk4VytpTlpwZ0VFajN1dFJKVUNKWXRPcXBjY25MVzhPWEdzWm04MWVOS2owMzdOTjVhNTlvYm1iUzN5RUFhS0VlRFhPbis1Vkxwek1adjJ1L214WEhoQ3hXZVRmY2ZtOVJDRlhrVWR6Wm92NXJ1OUtlMWFCR2pnNkVuZzd3ajI3NnBaQkZJOXAvZXpoZkNicXMxNm82RWRYekRrdGhhN29TdlRtZXhobldJbktjYmNKTUs5T2tZeWptS3dPM0l6enVkM0I0WHh3T0Z5eHlSbFhHS1hiVU9La0liZXdEYU8yWEliUnNVaU12OGJ3MEcwL3M2ZGRpai9rbmVzS0lHblJEdExRWit6a0lQZU1ubysxUnV0U2o2aXVSOHQwTEtwN0hMTERaNXU3U2Yrb09UVmFJVGJ3bjEwYU1ueWtJU3JPWnVWZkc4dEVCRTNHaHpoSGtsWXNMcDR1QUpTY1Bsc1oycTYrM2c2Y2FOTFBwK0dIVlpqUWNZU04zSkZlVkphTFlHTlNRTWNydmpidnZYOEZ6TDU5cDNWdXF1cHZwdC82V3JwWmNBWHcrNjNnbEY2Yi9NQjc3ZjFhMUJ4TFdCb1lYRXhHckZOdU1SdWFxRU9QbGhYcTJWTWRzWU41Tk9tS0kvTEVYYmxLM2FIazU1L1poN1M0Z2ZWYkxFWDBnckpzMkE4aEhaZVFhTWFlY0JibGFKRENiT0lUVll3cXppRkNuNTlXeDQzU2g2ZmtGcDJLMC8xMG4rSHI0SUNiMk82ejVoRTgzV3ZhaEl6RWdVNnB1OFF0c2Nwc0FsMzYzTG0zemIwOUdJaFhHd1lySnpnUUtrWWVJT0FlbHpCdFU2bUtPMHVRTEg3TENxNFEwOCtZNWVsdzBadWljbUUxTTVLQ3g2dzBDQVRwR1ZFZVg4dE8vRHVvOFJBVTZOY1F2M1dacUhNa1gwWTJVOHZvL2JJdjVqYmJDK2QwbXh3RnlmTlZIMWdSdTl5Y3hLZjgxNXBEcUd1WWs0RmQyTTd2RmtDNU5XN09ydHM1UUhsMmdab1hqYUFOOUlJMTBESW5vUDdqcE03T2tjNElGditMeEVwNVZRZDMrTXhGWXRSekI0eitVWWhTMzdia3lJVXUxVjh4Zlp3SWxFSTFCT3dTTzZiWHhZRjlYUlpnbFhTV0dUUTNvelFtNmRaaWZlUWJjRnd0MlhER3VXbUQ5RlRvbmttQ1NIeXBaOFRaNlVIK1JHNm5FQ0VaNVRBU2ZhUUNCblBhZmcwWk40bzFNdllROUpva2UySDloWnR3TXRhNS9Zb21EQUxZZ3hEOFhFeDRZSlIwRy91bWN3VFUydXVvZ3JxdkkrbDB3K1JPTk9UZWlzUnZNZTFnWVRWKzRtSkNhdzdsN2NtV3VVRFAxaFpEcmdEdmM2SW5vaFdjVUQrWmE3c01WVVJLVVUyclBEckwyazVSbHIrb09PMndKN092TUt3cVNsRkVOYmxaeEdiMGJEWDFVRTBEV1RzeWdGTGtyd0hOQjQ4MHpXanJYVVQvcWMwTnd4M0tIOWlldldwODFXcFNPWE5SdjJTODN4aC9XMFVOd3hzcHl1aEZuZXZEZ3pXb3UyelpmRkNCOGd4cDlwdmE2RkVtd2VReENwcXVYV0tmNXVIbXBhUzRpa0lKQjA1bERVUmQ0c0RNMTBDQ2NrUVRKM1gxNzVLeFE1RXFDQVBvZFYrOHpaaXVUQ1YvNk5mR3F5ZWZqZkhqeW1RWGszUkdvelZ5L3REeXJpa2V5cFhoS0tDOG94eVBGRDVLUnB3Sm1oTTNVUktVY1Q2R3YwK0lpKzZKY2tOblJMRWo5QlQ5c3J4VnpFSDZyNTcxQkd6UENTWC80M3JYQ3Fldk5JM2NiL2UySkFDRW9SVDFibVdsWlJyL2hXdHNmZXZzVFdFTG5nNnVKTzRETk1OcitlRzJIR1lOM1g1dURwSEdjb3FUcXlZbWtMZ3dObTRtakkzKzFYZW94dHdJak9XSGdxZ1ArNnRXeEJ2N1krREVxcWRTalJqVXBrT0FjcG8zNTRBVFZmRjlFVjBDRUUrbUQ5dS9Bb0RtcGQwMzhOVm9xQ25sMEpjVmJjMHRrZ0h5OE0yZzFpNDdFMTBpQ1VhWnE3S2QzMU5xcXNqaUFwc25weEkzUGU5cEtUNVJGMHMxV1pkb0hsWlFTcHZ2NEJQdnAvdXo2bnVLNms3TTNTU2gyMUJZNUkvOGdMRCtDRFFCMFhUYlBTMmFnNElnTE55eXpac0NXY1VjbC9aMmFyRGV0N0ZFTXlIQWprd25WSFdySmVLd0hTQU5kQjY0L2x4VGJQS24vQWg1Zm9ZaFgvRkhZemo4L1VJV0pBR3FBOTRReWsxbVVuNmdZelN0UkJKd0RKS2E0dnhTZVZ0U2grUkdPdVJseGUrZ256MlpTRGU4VWc1TmdOckI4c3lSS0dCQndYclJJNERxLzg5T1ROTkhGVkVqYzdZZ2U1VlFnTmJ6ZnJ6TU14dFRUcVV4dHlsaFNyTjdWbXg2TVk0YmVVcEtzVlQwRitIZzgwelkvRUpjalVrWldFSm4zOGczd21lcVVlZm9UT05Da1FPYklsUGlPTjhFVlJUWHJ5VGdPZzg5NTI5N0w4WlQyaHJzZjI5N0R1TXBweng1T2YzSzVOR2FyUlNYa0FNR3NnOWY1NlhYN0NlcjFpbzUzY0g3Rll0OHpsSlJmUEQ0M3FQNzZpNmcwRnJUWGlqUjZRMnY0ZjZoV0RoYXFsRGZjQW9hZzdkeEIvTzVtYm1xUWlCVmRXUFdaUSt5SHZ0aVVzYkplZm96cUJLN1BsNTBvZWRxUndlS2dqYVBRUmh5ZmRJakpRZlVPcENuL1BGT21TVlF5dE5YYXFGS0FFUlVnZ2FjQm5GSnR4YUxiUzFIdlY4RG04U3lzRGNwVnIwaTF3UklLeVFKUTlpVHZHYThRR0R6eGQ5N3JlZk93UDc1bjYrdXNuMDNEYXpDaHBzU0VHKzZRQjRRUGpqZ3pHL2NQeFB3ZUgwMVA1bDZ0OFkxc0xBSGhVTTM5dzhMWm53ZTNCQWlmdmFXSXdIVTlEY0tSRkN5SmdzeEFTTTVvMWY5M3FzN1Q1RUVDMkV2ckdZdk9jbWEvQjF4czZFSEQxM3VlbTgyRW9UT0h4L0hrcW1LdjlCSUxyZXByRHVoUlY1VjIwWGJiK1dRaStsMllVdk1kb2RTRzV6bUNoRGNtTWtGY29mNXZlUVZJZXVyaTE1OTYzTkxkS0M0Q29hMUNWWUo5MmVjNkVXUTd5VlVNblhKaWwvS0o0a3NySU1WZWh4ci9mM2lKMC9DMkpvTXQ2aWJRanQvVVRvWWJSTXNtd0QwMDk1VnB4eWV5QmVDM1NabkhGelZZbWZWaGFkanVvZ1FEOUJ2cUZTY21tVlJIYkI4QytTK3UzTngzZmVUMXhPWDF3NXZuVVJHT1kvVEo4QXd4RHVTS1hXZVJkdThxRzNjRWVTWDhOU1RTTU96NGhNSFlCcWxRb3c3N2hTL0gwdHgwNGF0VmdmZkFKK05QeHBVa25jZWh3SnAxcTBLaytXQmwxak53eG8vTDBHOThqRVgyTlNnZE9WWEZlNWVxNDNjbUx0bmZRY3gzV2JKRTJZNjdra2xxWG9PZEhlKzBoU3BWRmFIdC8zOXRxUXRLZDVVN2Ztb0ZKNEhEQzNZUEJNbGhhdnliVnVwMURQUW1pdnV6b3RUSis3VklGOWNIK2ZMN2tTVDRTVXdtWnZ2emxkL1o4a3RDdmFQOE9odDNuWkt2UGgwZXYwbkxwd1RsZkRaOFRONEcrMjdrNlNtRk9Fb0lCVE45TUJDSU1hQm1EV3VqM1NIMmtLa0R0ZGVZeS9mb0NOWXZSNmNaL0hGWHU2cFZZVTJYbnpVVC9xbmlMdFVOTW5hWXM1eXp1cVYzbHFkME9mWXNlZC9jYklKOTlNKytRWi9zZjV2eHZ6TXVaQVp3cjJxT0FwWnlnR1lmaXBJdWg2OWFRSlFFTUJnVjFHdFpFUFd4c3pzaU1ESXVlaVNCUXhjTkJrUGlaRkovbzhVSStuem9uY0F2MzJpNEIxT09xaGFmMXhoZlh2Szg4R1c4aUt6VkZlRzIrdzNIb2FwNzcwMHRCM3E3TkdZSll5VGVjQjg1bnlvRzRkblFDSThNZThEZ1hIMVhZTDN2cnUxQXZsSHlwamtYV1ZJZXFkMzRlTHVRUFZIa3lUQjRobldrNy8zalhXbjQ1ZlloRGVuREdzeXc4VmhhSVlDZGlTRWdJVU4rclI0cWtzbjVZMTNFZkFzbExvaGFLZWFHbFA4Y0YrWUU5NjZDVGZ1QmFDenJCZ0NtWnNuWEdlYmpNVVdwSmd3UCtVV2w0OXFhNFNJN3dvQTlyNVhvZWFaTC9GMTU3MHNkaUxNUFdGQnVWSEJONUt0Zzd2emRwZDFPS3ZrQmY2N0tIQlphVFpmcTRZbXZ3SU9KeExaM2FzcktaeUxiSU1OZnA4TkIwOEdHQ2xmaE9yOXF6RnMzQ3dLZ2QxSGY0cmVEVExZRzFvUkVpc29tQ3lnV2k4TzgwY0NvSEI0VUdFNjcyTnpscjFlbGxUd3kxT3N5WFRGR0xwZjN0OTd1bzBDMmgrbkNudWoxaUJJVXhtVnZYNFlsNVFWUXlqR2ErdmZwbk9wTzJsd0ZuaHhhUVh5SUVqaDNvUE8zOWQ1bGJEMmQweHRlWWV6VzI5R29QcGtFMmJnemR3MzRzTHhNTXdUUjEvVitvb3Q0djhhNVN2OCtPRXZ2K2hxUWQ1OXBvYzB2L3puR09xN053MDc2bGwxZDNKT0hadGVmdlVBQlZmVDJidzMwenhBUzltYURGSHNuZVlxd2lKeXJoZ0NwK0NoK0g3SDVWcGcrUDB4Q1FkVExEczdlakU1OHFFN1d6VldoT3UxZFlNNjdxbi90b29haWtLWTBSTW9HY0hhemhibUNldThBbEJUQm93QkxZWHVITDFna24ybzBCeXdKNnl0dFNsamRDcFNJbS9ZZWhaL01ZVmdTekFOdzI0MWFoYzB0QXpYU01BUHlhRkFSMUxLamRuc28zdE9QcFh1SVl0UmR1WGFOejh3VjhRS2N6ampQTlhYRHcxYnlpT1pYc3haS2pPZWdTQkkvLzN0cVZKalZsWDF1b2FhYzZzNzN2NU5PanVPblBGK2dZRk1VS1FWOXBaWnpnT2F1b3VrQVd0ckRMaXBsWUhINytEK3N4K2V1ejNpRFR3RGFMZUFzZ0R3SkdMZ1ZMNENETFo1eENZYTdySVUySEF2bytTRHMvaEVVUTQ4YXBuQll6bHVjMkI3SzZETHVWNEpnYTN6dDRmMFFsTjBLaE4vQ3FHNEhlOXYyY2duVmwyc3NwWHpFMTF6aVBsOFpqbmNvWXlzMzRiQ016SFB2Y0FzTm5jSEtuVFlOSlBPaW9TcHZLNkhxWFV4NGVQVUsxQmErZ3QzRnZoK00wV0lHaEhZdWt1SHo3cFFsaDV1RTJidUE4SkcyM2Q5eGZENzRxS3FVS01sOUZCK0c4SjJRT1lxLzJYR0NPd01RbFd2NXBaa1RDTThwWUs0TTFxZldwYXhRTjZYQzlVdTFEUDRMcU0zaDNnRzJob2VZYThlUitQM3RiMERTd1lKcXF6ZzBZM2VqUi9qU3N0azVaWFhyMFdPTWNySE8rRElBUllMUE45ckU3Q0F3Q1FtaFlCYlJwZzBwaFU5bnpuYU81Qld5enZpUmdkRnZWazJNWWxrQVhwb2EzbFRoL1lZVkRSU1J0b2hMWnVBbUpCMmlMOEsvZkhEMCtpeHFNbTBpNVhRdGNLbEorUFNkZDBkQUtVcFhlM3o4L3pYcDZrZnJQWkpHS3grMnZ0b2x5MFljb05mQ3k2ZEJteFVTcElxL3BTS2FVQklQVjB0bFE1RkVJb3ZMMUhER1NKc21NNTRodGtudDdpZnBiamJjUTV3U1ZPaU4raS9FaTlLbmRvSER4NGxYcjZDbUc2Skord1NhTDM2SlFBc3V4Wnk4SDlXZUdVb2U4Y245UVp5YWMxdlRMcktEMGtaZG5pdFRxd1dzdmU0MXFaMnpwT0xFd3FNMEFJU2NXQzhZeWdyT3dSYkJlOGFGQ0ZhS21RZmRvcWJuVHQ0cVpoeTFOczRxUEtLV1FrVDNjRHA2bDZQWmg4SlRmV2p4ZkVTYnFQMW5zMlAyQmlOT1B4OUd2VVRvSXM0ek9VYUVRR2xDa2o0TWZiVmxESk4xMXQvTjZXeUttMzFTVjhJUkV3TlRtZVBjQXphUTBJUml2Y21ycUxiR29XajdTbDNoWnJ1NytmYXBwaWpyV3Y2aGlXNFlaVTB6ZFBoTlpvbVplNSt5bmEvMGVQdXNYdkIrLzA1bXhSaEJWR0tMZWVzbk9hVGxMd1Z5ZmlHYnBlU2dzQnAwRHNWMmw4TTVxS2tsUHo4RzNDNGRRK0UxV201WkNVUVA1RldDblNNQmFaeUQzRUwzc0p6eGx1L3BGVVpYSHVPck00azhzMUhCeHo5eXVKR1I0M2J5ODZTNzBoNnpKMi9oNUVjTVAwcjgrM2srdWJnU0NUcDM4cmpCSGozdHA2K056UjVERFVlMjVOVEFiQXRzaGZBeGpHRDlFTHkyV1VJMFEvVTl4S2VxbGFXZXd1L29XNnhhR3JlVzNDUGJkYjc3RUhKTzBrcnJKeGJlRzVBTnpGa2NWVGR4bzhOZTVNMjI3UGF4c2xBRlJ4eDZVVk1CdHhvbURmVldmNUpqWTlwRnVTd3FPVlBWdlNwbWVNcGYwYVFGNlRDSWdUanBUVlNLZVU2TWxKU1NlTkNhYTNYeWVMZE1rMzZjVTNvS3dERG9GWVdWMzNlTlQzMGNMa3FTQmZCZFArT1JZWXdBKzVvcnlvbWZsbzNjY0FqdXltRXU3a1JPMk5LWThZWnlHZXR5YTFPSkJlVUhIeTRHaFZ4Q2d1eTczZFp3UDVkR29lTzB6UUZCT0QwNGlFR1JmNisxdFo2RDJWUVc0cXl0M3ViRllNeGEyNytnNVhvWi9wa2hUNU13NWR4aTVpMmxRNlhydFRmRmd1blVvVitnMmovNXdDa1JyY3VzSkNXbGcxUHhMMDVsTGNGNU93OEhEL0JZU2xoNlVESkpPSjk0M0NOUlR4MjBWaHdiQVlydkJhbEg4UW12WmphOUh2a1M3cUsrU1JWK3NiNVF1L05TdTB3RldjM2pTblFTK29iQXlJTkc4SGd2TnhuWTFjSCtSekl4ZlFrbXhzMkVaN0cwNVBOc2dsVUJuWXBUMkZYL1U4bzdvRE1vSUVzdVBIenBVRituRmt0aGdmNUw4bVo5QUlHT3o1MkRNbmpmK2lQQ3FZY1RYQ0xOWTM2VmtYRHFCMWR2aW51cnFTZEJzbjlobWJxdzNIa0hQMU54MGhqTnpyMWYwSmYzcUVDRUgzMWxDdGlsNjJRSHl6WTFMTnJjTW5GbFZ0RTZsa3NnblhUL1pYMzAxZlBFVHRrbzBOd1YxU3hpV2hpL0liRVNSRTRIWWJDbTh6VStuNzlqb2loa3BGekFwTktrYjdtaWMvb3htM1MyZ05GYm1XclhsU1FoL0o3NTFGbEUwWDk5SW1mSUlsSjFrUjZPa202dkZOaDllRkJZL2g2WmJZYkovZWlXdGJmbTdJU295MzdCTWhua0tRL3B3WmNNaHdxTGIyTTV5WWtKZnNvaCtnU0pmMlNkUjhGMGpxSTVyK0ZaQWlCVG5CdHZTMFFhY1dkK0o4cmZzYVUrdlNESUNIbS9xVk5RSi9BRWVHNnUxYnQ3WEYzemoyRk5hQ1p3dUR2MDVLYTJkUTJ1b1FCUndXb2dmZ0hFdkQyU0I1aWdYL1NlQmV4cW5YUnR0SGNudVpYNElHYTFrS0EzZGpDM0NTcVJDYjExbkpLWjRRRERSQzd5OGRWeUViU2FRTmVQWUlYV01JR2Z5STZxUFIxOWF2dXlRSno4ZGdhd2h6WFpwdU1VVU1RdDlQNTZkWU5RaVhQMG9TZHQyN2ZTWThpcHBJTzB0VURGWmtLK3FGdk0zcWgyTGpnb3R6T0daYUp3Tkc4V2E2a1luZkUzL3NaTis5YlNRMUVwRzc5cVVUQ24rc2J6RXNsUngrYWFaWkhiZ0x5NU5RUDl0UTJtNHFaZnZmZ05xaDJ4Ykc1Z01QdlVZQUtCaWZ5SXE2OGhHeVVYU3RQblA5Slo5UWNFTlJTU3l1cFJrOW5RZnBKdWdZbXB2bTU1MVFVbTRXWjh1bDRKRWtkRjVDU3g4RGY1eU43RjBLSkl0alJxRXdOd0F6enhrRlE3cEVHY3VXTjB2Vzc4MUdTbERYK2E2NSt4YnJGdTU5Nm9jOUxzRCtDYWJFVEI1SkNNVjdLcklBZHprcGFOYU96QzJhODU3WFRwZys4OEhWYjZpNStxb1dTR2dOZFM5RlVRcjRRTysyU2JHTlpERDcxVHpvR0o1NlZ4emN2UGJOUWRBSDRmUVBHbkUzMUE5QWxjNWk3TStwTHVJUDNyVkJ1Z2lMRnNMNHlKMkJzQVZ3ZHE4aGVEMVl5T1NmYVB2alpCRThOb0luc2JPa0Q3WUJDWXhhZEJkS0NicFpSaEZYK3dpUjhQNlhHSTZQdjdTWWpxMSt3U1p5SlQ0QzdDNzN1eHl1WEVrNW54VllUM0IrdUxQQmFoOFpFS1h1NVFvdVc2bGd0a2NhTC9Ockh2c0c3T3JaYWZYTEdSeE9YWE9jRDYwTkFQczdKeUc1R0VTRXFJYUU4YjduNjJmU0lxWEVEVnJTUVVRUTJqejZkb2xsbDMzZlhvaHRlNDBrTktIaFF3VnZnWkQwUGU5YVJmV2VzOENnWmVLbFZPbndBMWh3WFRxZllRbGR3Qmd6UFV0S3J6RmhIMzAyeUFQQkdVeVdWdndzWVNYU2l6RHZEVzZybjBJdGIydHVOZnl4WWMvZXE2THdEbVVYWkY0UkJhRU92YWVUYmpuNUh6bFhqTUcwNnIzckNEUkRFbzA4aWwxNVpuS2g4TXFBVVVjUWZQL1lQOUpBNTQ3Y3BaeS82UjNsRC9NOG96UjdMdnRYelZjdm1rbHhHNXVteWYwS1JibzlOc0VrdmNwaHE5U1FMcFBaam5oUFRrbGlZQUdjcmk1cDQya2M3b0lrbGc2YVNoQXQ2bUNSeG5XNm5aS1hIV2F0WWFQdklnYmFDUHhGV0RvcnlGZWNvbndQRFVkRFkxbXdKRGl0MUNHeEJnL3lIa25maXQrYWlCMjVpZlA1UXdpSkpVelVJL2hLNEJYSnRRN3FSUVdhRVh2WmFhREVXQU1hQ2xIS1p0Zm55SkFuaWk0NkR4UHlqZFhHZmg0WHVQblRVMGw0QWF2dDk1Tm1OZmVVeXZseXNiVm9hY2VFNnlSL1RZWDUycStDczFqTDlKaDNiNXhwMkt3OHppVUZQVlpOVXkwMEZtalY1ZFpudkU5RVkwL0UrbGhUMGp4Y3FPb3BLL2g5bUxhTWFGOGsxbWM2dkE0dnQwS0YxRUNVcE1jdTZyZGdrMi9SdFl1VjJvblZuWlg1TG1Ha3o1ZzRYeUR2dm05UW9iMFRhMFNzdWo3bERrWEFSdmg5NzN5eFFRTEJ6eVVZKzBML3RON3NucWt6MXF0TUZROThpRlJzUlc0QStyNndidTZjSFJlUWNlVTZxZTRySFRQbG5HaEg1blBkWWpraUFERVl6a0xReWhCN2hxK2dCTHFYODV4SkxFWHA3Vlh3RmdhNmNNa0E2cEphRUduOG1FOWVIYmRNYWg4YXVWenJsUXVzdnZEcHpTVHpQV1BwYk1CamNuYXczTTY3OUNVcGd0RUo3REFOYkVPNFV3RkM3RmZrb3FiUzh2U1ZiWGJVaUJja056R2JLQnZJWlRxelVsdEp3cXo0RGhVZ0l6RTYyYzlGNTlsNU9mT2ZmWlNqdU1mdVRnQWxwRnYybEFiZ1cyLzFyendrSldkL2lZbUQrU0VuOGVnT1BITmtIYnlmNDRWQ20yZ29sTmo2blRXWVB1ZDZZTWlteml0akNrUW55L3FXNlNiTnVCaSs5cW96dlpCZW9ES2I3SlNEN2dTaHpaaWxwTlhvOVgrSm4xZHFqVlBpYXZvM0lnaGNTaTU1dXQ3YWlraDZncUFkSk5OUXdCRXV5WDFIaWZSczVralRzL2h6WGpLN1p3ZDZubmVBUjFtNFpJWlA3andNQ1NIQVpTU3QwamxQSGE1Vy9URS9BSVFMTWlZcUppSHpjam1NTlRoWkxqSDVlaTFxNzVBaTZPYVpWQ0N1OUlBanFRWW1xS2ExL2FxdjhvZzRmS2k3ZVBKZW1UWGRyc29WQStFa2o0VktjNGFiMko2dzZJMkZxWWo3VW0rek9BQWE3NW1zbnRwN3JzTnhLZDVuUmhMZ29ORFdFcm9lWi9SQWhiWjFieDZqUTRoU2tPTXU2dVd1b3pGYnRaanU3cTNTVmM3U0NTWE9YelkvRGpISUxTNm9ydFhqU0ZsZnVnSWJBM3RqWEJ0cklsb0czdUNDVGUva2hiUGRJbWl2MEdXN2VxdGF4Y2Q1cTR6dWdhS2l3TmJnaW4wVFViU0huR2tBNHJ3UG1PRWMySGxmMlNQaHY0SUJNaVZaOWg3bVNqclUyOHc0aFVtOUJiMktjbnVIUlNGdEthek1TVGN3RmtTVEZ3ekVJRG5iSWJBSWlCR3VNclQ1TmJDREhGUWRDVVI0WlFLQW11eHN3eHBXY1NJdW5seS9rTUJPL0lON3JrUnR4STJXaTlVWW1kdEcwczAvU2VXbHRpT1pOUnpROXFZa2gxQndIWnlVY1BHZmpwbUhyYTRyU2JvM0JSdlFKTjFIRFRXN0I1YjcxdnpMbDN3N2gyN3NTQ0lMdG56K3JZUTFUeDVHeEtLYVBBd291Wm5Pd3NDVTduQU5LL2gxR0FrUnR6Um9SVWFUSzhmcUxMWjhYa0lMaHBmVm1WMitJRXhPN0xDaDN5ZzdrMXhqQzNaTUJudjRVZTg5UDRoclYrNjVNY1BwVXFGc0FVdTY3K082SXdkcjlaYnVtRENOaFRvWFZMelBMRCtCb0tud1V0SkQ1b1ZOUjc0QXpxU0I2M0tCWkZHcHgzYVUwM0JpN1lYblpxdEhkMU1QUTNMd0w0RmptWkJhYTlNdWI3ZWZyeTZWMWdLK0tHTVkyNG9pd2JDbTNqdlV6SzJIV0FwUHo5ME5od05qRW04MTNRRW5UdkIrNm9kWHFrdkR5dGNDWEpMb1l6TnBTaHBveXl2clJ6RW01aThheU55SUJqVk52UnFvTWd2ZlAvVzB4eGFKbVJ0NEh3Ukl2ZkJTbHFtd2NzUWQxRkl6Njd2SVVrOTFvWXVCeWlXNUMxUW9KQ0VhNTNyc3J2NG5FR2txb3BHVGIydnBVQXZxY0VVZVRsdDVPQTJwalhxQ3F6c05aWXFEVnNQSmJEci80eEx5d0k4cHo5aG54M3gvTHBsRVZsUW10NzNHYVpianowWks3dHNaL2F0T1ZTWTFzUE53TkZzc1N4eXJnQjRJOUk4K1J0TVRNTWxCNUNHN2lZUndNa0ZVK210anM5UDZrdmgrckdCeCtUd3NoMHRUWlVVRjFUNjRoSXdqRC9BcU5aRzZkL09QbUcxc1V4K25JRXR0V09JSklXbTBZMVl4N0pqVWZqRkp0K01SZ2xNc3VIWk5kenN6ZzhaU3QvYkI2RzVyUFhHN3QvNHE3ZUFPaEYyNzhKNHpGc2ZMTGFKSkFicGprMFpNaHBySzZoVHhwR1Jqb3h2U1o4c05USW12bkwvQXNOSU8yaURkdmVYTXFUTi90UmIzb2c2VG1zb05HWFZuZUk5d09wUmErRUtwVFBWMkh4YUoyV2RNMCtaTjlRZDA0SUNoQ3N6ZEZFSitWN0hkLzRQalRWQ1pwVHhBSFhsUUtvSXU1bXFhYjRKRmZ2UHZWV1RjN2J3a2lEVHhRcVNTazhZQzN4Zmt6Q3lOMU4walpwVW5oV0FaY1pxSE9qOEFHd2Ezcmo0SG1hbk9xTmlvY29CTEMxRGZNRmxTWTBRVkZHZEx6Z0pCYmNra3hadXY1TE1VZkpJRlBXSURhZTBsdDJYZDBFMzZiMy93ZEh2NzdpZnJOekhVUjVuQ0pRM2k4UExTMWNaNTRVbDhqa2RoKzlLRkxFekZkRkRHZFl3QWFCelBnY0NCeFprSVRMTjAya0JUL280Tk9NMnhSMU5lREFNeFNOWDlVMHUyYVNwZVY1TTdIK01YZU9ocXZaYjhQQ0NVbUI4d2krZXZLQ2poOTNUUGRDSlQ1NXRjNVlTOVZmUXVqbGVaQTV6U3IwUzNUS3BkSEo5VEhybVBqdXZXUVZNUzY4OWlMNDVVSUJxTmdsb3FnT1pRN2MzUlN4VHVuV1FpNm52RHA4YUwwSktndjhBNkdqVUdKUDdoYWhKeGp2dHRPQ3ZZN0oxdUlId2txTTBwbkVVYU84bTd2VVorWDg2Y21sM3hHa25jZzVxSVhXc1VPMUI2YlhKQmplRVJ6RWR3N1c0aW81bnRvbmVWbVVodThVdW5xR3J5a0tHdnpHcW5qVkUvOVczRHNGM2RHMzBKbHVqYkgvWDRBTGE1WGZrVy9OT2xoNzBRSlhvQWllb3RRL2xvZmo2dkF0S0RUMFV3UXMycko4MXJrNmo0bTFGSDdUMm92ZmhFb0t3NU9paGlNMncxTk9MTE1URWduWjlyVkRTZnc4NU52RHluQjlQTHR2eFp3UUNPbTk0dnl3QmVzVThzaUpSTlBHazcxNzE0cDBla1FvT3FoU0tndnlxMmI2cUJzS2w3c2R5V2Vyb3RjQUxUWmI4T3I5a2FVOUNFbUszRmJnZElldU5CbHdEa1QvWWNHVUFvcW8zUjR6YVEveEJ2Wk9xNmR1M01wMGttZXpRNkY1QlErUHBGOEgzcVFZTE5oTVVzY21BTkNpbHJhaTZlcGRvMmY1MWpDOG96dDNGUnB4YkVKUmg4dzltZjNRWmxIczF1RGJnS0ZtYU12SS9LSE5HTGNiQUpvZndvS0Z2THFNbjArMm1CbUJrdHBWczdOTW5Lck1ldGtHU2h1TkQzMWdwOXN5THBiRmM5RVE0N0dpek1tcGdCTUlZdUZkR2ZOWXZ6M2wyQ0Z2cmN4Ync3UGtDNlg0L3hKa0dKUjZ6WnNBb0k1QkprdTFMSGFnY0s1bUMyVUwveHJzSEhueVJEbTMzbTBvNFJ6T01QbUdSTnRPUjRDQ1RvMmc3YUpuZlRLeERnU0hKcUw3aWp5VExwWGZVL2VEU1V4WEc3V0R2RUtoZjZFUjBJbHZQeE0vMEpTaGtCZFA5L1o3OHRBYnVRb0lMVFd4bFRSQkd1QjRERC9QNUlrKzNpQm56TUl2K0RXdDlzK1pJcXlJQW9nKzRqK1l6SjNwZ1VycFdTQU5aNEhjbHVYMzVhVXJrdFJOMHdsdkwwcm9STTZKYnVsYWJqSmxDMkFndVBQN2FSYlM4bXhNbWxRWmhGMVNsWTd0Nk1UaXVkRXNpYjlxN2VBZFR2cnFQNGhsTVp1WVhDSUhPamlFMnVSZGczd2txVEd1ZGVxbkxGNmxqZ3FYWDNXMGhKZ1U2bUVibjlxejRPZXBZMENham5WUlpzL1A4eFdQZE5hTDc1UXRnSWhRRytJQ0NZcWZxakpEMkp2T2JLekhsQWtjamppVnVEdWxjU1FtejAraTdnRFVtZkxSRXZMUk9wVU5MS2VRazNlajFmbjRBcGxVcjFydlFJOEZ1bWZyMDBOek50Z21zZ09TZ01OV2dlcTJmZmdoRFpHc2xSeWl6K3daUmIxazR0cEVlaTFtb25icVFwaVc3QzQ4bGRMeEJtdkZVSWRUdVVtQk02REJZREhlZ1IrVFp6NG9yb3BpNVZqYWpIUm5LL1hnSDlqM0ZGSCs5WnZIMHJidGZxVVgrYTAxVEtzNW8wT2F3WTlpMFFXL3dKK2V6ZWJVSzZjc2plYWk4OXF0YmU5RGpTY1E1d2IzaWMzL1BET2IrNC9mNThHOXVSQjNsLzMyYjZHSXVTOUZjSVdHQWtkTEVhQ293Q2FPR1psUFZWVVMvbnAvZU5KMnFUOWlydXVVdWpaOTdONHJWYUowRUpnU2lkYktaTGs4bDYzQXV2M0tvZjFHb2pzbFkvUG16c1dGVE55Z2JRNkdvMGplVDFiRUM4bmEwSTRyZnc2cHVGY2ZLSEkxclJKd2RLUEVHa2drbVFrNE44aURoTzFiZHdyYzZTVHkyRTh5MUJ4M2RrVVgrS0xldENyUEkzRlF5bkNjWUdQMEtnUEhydXN4Z3B1djBwam45aWFFK1ZXejRuN1FzRmUyOGhOM2hQR1BSU3REUmp5L3p5eklYclkvd1RjS0hYOXEvbkVTRTBBM0pYUmdyUVdLTzN3bFJHSmVQWXRXRENCS0dlL3BUd0s2R2F1NG44TnRqTU9TSFpIdXBCeWEyVHRQN0pJdUlVUTROY2FCVFJqU0hnRFlGVC9ZK210TXFxclVPZkFhRzd1aElsNU1wVHZNaFkzSWxJYkhmNytPSFVWbjl4cjhtUitmRCs2MnhZdWhHOXM2cHlXUDBSblQ1VXpiY2o0NWp3dUxtb0VSNVQvbktBQ0RDK2FxRnExbGo5SXlpZ2FZbDUxVGlVa0NKb0g2Vk4yTVFzWldWbm5ERnR3RDB0ZDZBK1htQktpOGo2OHBLRDFCd2xCWXRrY3Q4UThqZ3RPWGxFdDVJcUdjdFQwSmEvOHliSi9VMlZYV3pNM1hPS3d1Kzh1N3NJZjlWcVNnSWx4bHNKcFp0N0JsZDFUT3lOT25WYTduOUZYVVpjWXRwSlppcGFJMFNXeFRWMHNiclkwaythMFV3eERzUEJUbWtIMU4xVDJSYklReVBPdlF6aHMwT0xtbHFjdXJEV2pKVUNTWW5TSWdHOXRhRFcvVzI2UnZVK0cxZyt1Q3orWjdtcnRWTUlUYk9LeFVnd0Y4d3laQ2tsdE5sbnAwNWpIT2NNUTA1Ti81UldGSW54ZWZnd3FtbzlYNVB1WUZnbTJwSjQvSjhkTFlwdmkvQU9NUlZvMU54VlZnTWZoS1ppRUpKbVFJVGNXUE1IUHZXOWlaejM5QXFQbmF1cFF0Y3NZMUdqWnhjVjBKb1V0dXZJeDZyNUdBMmw4dWJaQ2gvWHlvdERwUE16cFFNYXpTQlN6NXRmMUxlOWVrTmJEVXhMQnJQOTJqMFRBRXc1VW9HWkFYRGlKbEhYeDJKOFQ2VzZaM1ROaW1tMHdZWXBvM2s1S0JNWm4rZGIwMS94TXZJOUkzY1ZWOVZqVlBmMnQ3alE2SkpVcEhUSFdaRXU1SkRhZ0Y0N096bWl0SFBYYkhJeDAyTnI2Z1RmeC84VWYyR1pPTG1aNVBpOFlBUmlSVkxNTThQb25pa3JYa1JQaW8vM1I1bHkycjAxY1VVVDdVdG1vRXRnRjlKd2pQMWZsaUlPaitENkk1VjlrLzZqeDZBcjVyNStsS3lIdkovMElmSzBiWk9saDNFL2U2d05BaVI4Q1ZpNTFEL3hnR3krS2VLNHhkV1g0dXpjMTJ1dXM0WFgvVVZRVEI2K1MvR2EvWlZ1dzdnUzRucEhuWDE0M1p3NTM3OFhRUm0zNTNhbTRVdldFcHRTMDZPVTBnZDFEWUFTaWFEWWsxbTMwWjdIZStPWEZKcjMxUDVFeXQwaVBkd2UxaHY0bkl2WFF6NnhZN1c0dndudE9PMkZkMUIzbTU0UFh5Nk1WWVVjVjdhSHZERm1laU5uRGgrekhxanZCK3Nza24wa0ZTNlFrZkVSSVFxQ2RyWW5QUUZudjZHamdSTjlnM2hLZ1YzeW9nL3pJei93ZjRsWjhZUUxCWUZpdEh0YU1RMmZRQ044cDhFdUgyeURpK21ZWjlJVnc1cTdjL1lHd0N4SHp1Q1RUKzJrSUVLMUdvbTNWNWZMbmdqUDJqNmk4bnYxemNHZk5rTUdPYmErcmQvRDlMZ29NK0NFVGZXMmhHbkZFS3daKzZPL3B5alBrWU5Teko0QlQ1V1MrTTYyTjE0a0lybGs4b1oxTWprTE9LVWRuc1kzT01VcDRjVy9Ua2c5Q0E2QVlObUZaWmsvb2xXS093OFRDbDU0NHZFeVlOQTNweHRRcU44WUxjL3Y0T0NrdStlNUc1a3JGdFFtSDJQWkFpN2NnTkhNZUhGK2t4bnlMQ1VMTWR1NXNwajV4cmw3c1BocElzODRLZ1VzYmdaTk9HSVd0WHdMUVNxaTc2Z0NBKzBEdHpSeTBBakpEREV4MUxhdE83OEtlSEtvZGVOWU1ScU5HcmlOL0tKMjJ4cFEyQ2g5ZFBnbklKb3VSMEh4a1ZFa2l3UVFrTXk0OThJb09LNEwzOXdKaWRVNkhlWnhVRi9xN04yNFlvaFZNS0dTRnZzajArYURIK3JSMjdUc3ZKcjZwK3JvczJCZmFjdmN3em1vajk4bHY1d3hRWTJEOTZVaTVaYVdmcHR2NmxNRi9vZGpYNTdCeUV5UmRBbU9tMHFyaHZuWEtyMnFPWlc0Z2p5QnZLeWN2ODluT0FJdGZSUGpBVXVFODdlc2JqNUZZL0t4b1JDVmRzUnk2YkpVRFZjbVJMZ3VMd2pmd0JzVnNodEN0T3NHUnpmeDk4a2lRbFhQSWFsTWVWVWx1cE1IQ09yWXd4M25WTDBwRUtHTldJMDAzMlZLN0hrWWdDR0ZUNzhtdTY3RUlDdFdHaDdPajlVM0RuSGY4Q0kwd3ZVVzJaVi9hbFQ5VWlzaXpQSjVsWlR4Y3ROSTJBZ0hkSUljYURBUHJ3ZzgzeVBNeENxbldseUk5ZTlKdms0Y0prWHNSUzRpOVFxbjE1T2Vja0ltbTZ5c2h4S29jUXBwNk43QjgyZWtNczh5NVdZeFhBQk50SzBqNW1RTXhnR3VTNWlnckx5T0IrWStXZjg5RlBHSG9kYkRQVUJjbVRtUEZySWNMRUZuY2Fmb0NtWnVZRGdtajV1Umo5NU45c3hvYldWUmRDSVFtR1BDUEVoUk9hTitzY1ZmcldrbXV3OUEvamF3Y0VWd1NzbXhNdzNiQlV6SGowOFhUdGNZcno3dHc0QWRRYXkxNW5CVG5ueXJNWlVVSVpERjhsMHo0dGk5WktvdnRmUG5Pb011M0ZYS0dmTTNYRC8yclJXajRtd3l2NEx4SUVKd2NNN0luOFJKNjN0dVhqSDZ1S1p3OFNhY3NPUUlCRHRRQ1FPblh1QzR3a1MvNk1ZK1BTTzdzd25NUHhZbWltN2pHcHJxZHBJY1NETmhTOFdpcEM2enUrRHRCbzBHdGhJMkRRKzIrdzlqSmp3Z1hDUmdCc2hCRWZ0OXNadTVEWFBSUXFLZFlMUklmMklXRkhxbFh2dVRuVWlFTjNoVmRpUEljN3hYN1p4K1lXdjhiN0svOUZqYkljRDJpTERqWkZhbnd4UUkzYlVuYzY3R0ZlYk5SUnRwUUh0dnRIWEhkaXRLVm83RmtWSzlmVWNwMEZKaExjMkVPNDIzOS9ndmFYVFN4YVE3M1F5SHlzTzlhSUNuSkZmRTVodTFZeStBblpXTGlGbEJYODBldWZBU3RLaE96RGNEQWMva1JPM1c4OWZobnhmbFB6eGtzbUxOejYzMGNMNTJzc0d2Wko4QVlmdXJFU0J2SktpMWJXM010bVROUnIvdFJwcktnUmREU3ZQS1BJV29FMFBqa3NWTE9paFl5Wkc1VkZ2dUhDeDNJck80LzYzTWdJY2hIUXBPVTE3cXVWVXVLY3FteHJ5WGFCUTRFMS9JUWVma0ZqbTBpdjJwcnZJVGpwZ1ZGS1NrTzUvVE5qbGZvMjVQZzh3T0FvanRpZm9PMnBPaWkxRFd2dm5UeVBCeXV0bmRSeTZiSzFxRUJCV3U0M0FFTHE0dE5KaHhrc3k2R3ZPbmNkTDN1aWtYMW93aTZsUWxGMWZLYzRsWmJUOGlXcHN4aDRiWGk2Q2dzcUk4eEFWa2NuZmtrMlREQVZDS3ZTZng3M252QWFOZE93RXFDd0xzYUhaMTM2VVh2N2JZV2U0OGtRcjExRGc3VUlvc1lCNGNIb1djMUREanlqWE9uckxRZk9pM1hFSmZpUDl6cTFDQ2d2ajNiMEpLcWRoMmxmZS8yWFVYbGNMNEJKQ1FLTTRjWkhPV1AyK2tjanFVU2lTWEIwT05nRU13QjVTbHdGUlBhR0lPUjZHdkFyQkpjWDZYOTFTODFBcmhrQWhtZFpNU1B4NnJhdVVrVEozVHBvemw0MEpMWE81R2tOY1E1YTZQWVBiYnBuRTV4bXFBVHAyeTlhQ3hKQnZ1cWJneEc2UU9RZHptNERIZ3c4clhpVENVMHVBWUh5bmhndkhIZkMveWdvTHRtSVYyNTBKalcvQUMzTFJiakErU3lZRU9RK1lJcXNKYmtmYWpqbHlzNlh1bDFLYXJJcHJlZGdVOFZoRjNLSlNhMU1iNzJ6TnlIUDNMeFE3ajN3dWJOMkFOdXNSSlRlL1JCMTc2dkJSOU5mZ2VuN0FWTm5OQ0trVXAreE1ZblQ3ZlIzWllDeG5VMnNwdTFmZTZZc0lHUG5aeEc0OFlRRVVuYlhRU0NOZzdONmtNWWxsay8vTUJFTWFYZDdLM0g3VzhtUEpHdXNob3NnM3F6MnI2c2dZT3liaG1Fak1BTnRUVUtkUzN3bFpLRitBZEtySENYZHBZUkFQV2x2amt6VVFtNDZLN2xCNUtUelAzVnR4RUc5MThrbStNbVJxajVJdExNQ0ZKT01GQlp6QjFpOW9nVGNDbkJyeU51T25yTEVKZzQ4MVhIdEpRd1doajBmdTZPak1wWmVaZFJlVG9CTDZoRGUwdHhWOWc5OHdVamFCY3phaDR3WFRta0N2N2VQVzg1RXNZY00zQXltTUhtbE1RMUZKMFhld2x1ZlozM0Jmc0EyREF3VmUrRjhpQ3JTbjR3K0VBTjB3eFg0THNiQ2RsbXRvSks1TEREczJuaWw2K3VTOGxia3hDbWdyTkR0NVM2T1BrS1FPak4vcWtPOHdsMzBTYTNxZ1RSSk9TTlRTUG94aDFWdEhjWnh0aTBFVkpjNEFEeUxKUWdxTXFqbzFoemoyTURZTWJZSXU0N1BHQUttME5xU2ZTYTlETkwzTjNDWGVzbFFsWnZvUU9XODlOTUJsZnpZZTg4Q1hQRnYrMDk4NFYrTTFyQWhRNGpVSkVUamgvS2I3T2pjTG53TkdieHRXWlVqYVBqOHkxSW9KN1poNTE1cGZ3dXJzL1VWNTVNbHRONHpwcjZIeHZYR3VPbnBXMlVDQnFnMkNiYXNIVmg4Rm13aGpjQ000RldGWThyL09UUGhtY0JVbVhqN1lkc2RHNXp0S0lPKzFtVEZqVXNhREhpUjBHaFdLV2U2NERmMWVZM1N1SS9KdUJ6bFEvVnJBVmY5ajBvN0JrN1E3NHVEb2QxS0VoWEJrK0hzZzRMYXFTNHdGL1RUSGY4dWNIWHBkQmQzZThSVXREYlRiR2orT2U0S1BQaXNoS25yVHJQeUR2MXRVVzNiZUNyL0VOV2kzVkFYZlFsb3h3alhDSTAweVZPRy9Zb0dadU1SRjRTaU9MWkJQWEkwdHVVQWZwR1Y0VWVVSlVpSWNGVWVOcDR4Y2lGYTZURCtmcnhEOHFvY0o0TjJJTDM2T3BMNTlPM1RTeXV0aDgzU1k4Wkw1azBBd3RsWXFnK2pjRUdRQ0h3UjlscTh0cFlmT3ZOUVpaOVZwcktYeGlDVDR0YmdjR09sZFlWa20wYisrZWtYbjJjRk9lOHM1RnlwL0JoQjJONGxiSFoyUkRXZUpnQ080VDZnSjNlbk5sOXUrZWpxeGw0ZUhNaHk4NXlTeFcrTjBrUHNDN0MxTGppaFBrVzE4bkRnTUpWNEZFWVcvYTY0T3RSZ2Q3cW9RTzloSE9sRFBtd0s0blQrcnJFTitYbUdTREw1UzRQSjMveUNCMWxYYzBWbHE3WnVmM1VSQ2kyRjZQOEJjRmtRMy9wei9pUHhGNHk1THFEOTgweFhtMGNqUHVNS2FaMm9rMHhvckpoSlpGNGlRUW12bnJGUTd3TU9aaEtiM05zSGpDVUZtVTEyQ3oyTXYwdFAxQ1IyR2d0OFUyTkdZenp5MWIvckRiZm91aDJwYmV3QlpSV1pkVDAycHhTc0Q0RHdET2VndXgwU1BMSkxxU0pjU1p4dG9QQzNuYzJMZ0cwa3dXS0s1NGVlL0U4UkFMNEpqYWoyZmNoeUpmakkycmxpTW02QWlTeWxZMUNNVnRFRVRWRXFIS1RaM1BUMFVVeWFOVmpuakY0eDRZUEF0aVdwK0pNT1BSQzJiM0JYN2M4NWVDcFNJVzlkMzBhb2VtSlpaaGY5SDRkOVZPTHVsQ2V2ZGc2NHVYR3RqNzNHY2N5NXBEcVVFSitWZ2tHZXpNTk1oaHFMbXRBVUxpZlA5eFJmT3JMZG14Rm5JTUo3TktTNmpkelF2R1lWeGxpbHAyNGsyNlROTk40bUVFeFJOdjRtck5LeUc1SlJyMmY5SXpjdXRyZDlGQU1uaUlEc0MwVnU4VzBqbGdOUnhmRTk0eURMZE8wV2JCNnd2RTVabnd1akxIN1duMEpjSmM2UHNCRFZVZmYreFJZcjgyR0xsclZzYytFaFcwTU82ZFR2bWgydll1SDhnVnlFbE1qMmoxbFpnV1ZFZThKNXZUZTVYZTZzQUpsb20vQkQrVUMwR3Z1OVJ4eUVBNWtCb1k0R0JEQlNRZGN2VmlGVE5CU3BQUXo0QXMyMWdvQWJYWmxnYWg0bGtlSkpuMzVKZmJuQWowT29WaHNtRC9qZTk0QnI1QVFkaWQ2MUpaekVaRllZbmJST3Nla0s1RTYreUxiQXdaVkdxRTcrMHlKSjM2WVhKZEloSkgxZnBFdjRwL0FiVEV6R2txK29mcW9YcGExOGhuaDVHL1NManBRMmFOR2xTVDlOZzFGMEpFK1lTd3RCbnhXMGhOSkhkemIwbXFhSFdZRjBWZGhUdDZ5cmplRlJvRUlyaHljSmUxWkEvcFlZeDQvbFVHSW1LNnFIaGlCVUY4WXg4SDNwdnErcE5vN3lDTXN3cUdTbWVPYXFiMlh1dWZmdjhVbnRpcDRLWnNYZ0VtUFF3NWdReGlKLzlMeG1IaDNHMFpYSUJxYkVJZXg1OG51MzRXbTZnTnRjaFMvbXNHUW1wSk8rUFlVUHFDNjF5N1laZmdFb3AvQzFCb3BTZzBkQytKRVJtZ29mV2hqMSt3ckN4Kzd2WDZBUWpvSjJQWDdWOTQwVUNjeHNkUTBYbVZkdDFsdjJDU0E0OU9KR1piM2ZHeEtJQ3Rkdk5MUXdaZXJaZmhNVDlXc2w2NlNCdXNJNjRKbkJtaXpQcjN6V0RYNXZPM3g0MHowQWxpbVdwQ0ZQMTZKY2dPRmRvWmZUYTZGbk5tb054eUdZMzZiV1JSeEQ3aTh5bVI3NFJCQTZ4K2FGdlhaSTExdy9DWjRKVWdsZU04aDBiNHpMTnRvOHoyOGZuTnFWYUtxK1NrUGM2cUp2RHNTYUVJZ2xDTzdTZ1B6MHpZakFrZE92bTdhVGFMLzAyQmdxYm54TmJTODZidUcvVlkzamt6UnJ5TWtMWlFralhUcnF3NlVOak94WksvOWh6dGFUb3NhK25Xa3dDVjduV1FXNXJXWGtyRUdWcjYreG5Da3hxTkk1QmtZS2Y5U2kyY2dxTGRZM1ZOLzltcldmcUUxQWxubDdmcis1SmxFNUU1UWdnM1JRTmY0UDZVVUp1ZnRtbkpXdU9ST21paDFISHBQdVE1VjFuN05ud3NyM2VxL29YM2Joa292eExta21xVXcrM2s4N3BLNUlpakZ2ZXV4Tm5CODIxQVdkYkdnRWR1TXJCTXNuczc2MjVvZ05jUGs2QUx0SnVBUG5NMFNJTWdyZ0RidEkyNDB2VkgxT1JmMGk3Umh0YW1ZUmt0NVpNaTdXbGgxb2ZoWCtXT2JmbG1xOFRiT2M1dElDYnVYSDdIakF0YkNzYy83cXA3YmYwdURURFB5b2JNZFQvZFovc0tQQlU2OG5aeXQ0RnYyam1sNGNlcFpOTnIybnA1aGhaRU4reXlvWkhNaEwvck9ZSTNpM1dTR3dRNFZsdkYxOW0veW8zc3F0OU95N0dOdEErWHhMOWc4U0cxN01IbGZRNTdrV294Wkx1bmM0TGpaNW9WRjZDeXUwZXNxa3M1TjF1RzRTVFNNcHJGZS9wbFMyN3ExNTFHMVJjbENtNndzdHd0U2ZMTGQ3WEFPSDJVV2N5eW9UYkZoUVRUa1llaWRIaWIrekFEckdyM05PRE05VTdrbmZQZmZjY3IwNlFRTmgwOVFGb3NTM1JrT3lqUjRUSmppQWxVK1gwQjIyNm5vMFdqS1RidGxwUlBYSi9LMU9qTTN1RTF0M1RlbnV1b2pwRDEvVGh5ZEJnN3MwbjQreTlJZXhpdy95V2Jvdk1ZZFkzV0NjQVYvQ1cxb3pyNk9pc0xTd2pzU2pNN2UvMDJSZkkvdEl0QTNWZndRNUtDM3N6UW5Zam1PQWc4TnozYTQrUnVxWlNweFM0SGhabXcxUWRCZysrMFdhNW80QmJwVTBVUFpSbk5PdVNuM3ZjbkZodys0UFBVYzFKY2prNXdIN0xjRzYzaTVxQ2hkcU9Bc0lqczgxc2NEaWJnMHoyR3hpRVRBY1dJbzRPZWVZOWdYSW9MblBKUEdwVGFhZGVlcmZ5Q3gzY2JMeWdRajNQL1RnUk1UMTRHdC90c292SFhFVEtueWhhQXdCdkQ4OElpSU5GZVo0MjlzSGxiaEN3cU02U0srSHlGOU5rQnlkQVJxWjRJbmlQS2JUZ3U2K3lZeGthU1o4ZW9jTXlsTUNZYTF3aCtoclpmRXJsU3ZwR1BvejV6QVVNa3BGQjRDT3BtVkdsRC9sRlYxelB0cWNKTFVFc0N4OThCVHl0bVFzeGxNMzJqNjl1VU43QzJZbzdZUmpzUU1CY0Myb3Z4S3RIeGFoKzhXSXQ0MTdqc2tLalBKR2paZE1NbHJyeXlGZUhvRUx5V0pabWVtUFR1NUdOUTF6RVltck11QVNsWGFPY3pSM0xVR3o2YUdkbXBTcVdzdngyWEhCcG83T2M3YktwWFo5dmVsd05FdEpwNHpRMTYwWTMxL3dhWVVCa1BJa0pjV1BEeldxVERiL0NWeGNxZExTRVkvMFBlZllGRGFTNjNyRFQwdUhXTXQ2bjd2c3hnV1dpQjBORWMyRGs1emEzai9STXhha3FVSjFSQjc0M3hhSWM2dVMzRkttWklVZ3hRcFdFZTZKd3VSZllhZW5xblFNdTNKTkt0dE14UlovSERSc3hkSkZpMU1OVFNvSExaVVBPVmFZQ2RSMk1oNGozZVNENW5MTE9neXJQNlUyemYzNXg3amoxNU5PNGFXZm5wOTkxbjdzNlJqck5SZzZxN1hwTHFqN3Q3ajBSa3pMb0tYb1AyTnd1ZFh2SHVyMmRrWXRHTWEyWndCbFZSUHZFOWNwMnlvWStOUnJRVjV5NFdCbXZsSnNNcHdaMU1GZmwyMkVFdVEza3g2TWZySnVhSmFBTFl3Uk8wc1FZdXZsTkRveUN3SytLek9hb09NbDRCWjZyMzVWZ0hxTUhTM2tsWkhpVmhjT1EyMS9WenV2TEVFdGtyK2o1NzFqOUVSMlJOdG1aNDArcXRXSHQ0YytNTVU0TXA5dFFscUxwUVNnZUtTV2x6YVdxOXdGSDlaUE52YWk1RndUbEVmUlBkL2ZvMUN3WUdhajI5SGRHdTVrNHorc1hjTmlJS2d4L1BDUDMraGI3VTRzeWJGOHRNODNIeFlrTnN2d2ZMSzFxbWN6aldDa2x2c0dvUEovZ3VzZTNReThyOFZ1SzY5UVdMSFlQNlRYWlpOUC9kaER5OE9MM3hsZ2VqZk4rQ2xYMHZBZndMZGx5dGR5K3lQNjhCbXVjNm45V2lWSFdRb3ZQdExOZUEyUk1SQnJITWpQd1hIUElNd25QeXpPdFplT1FxbDVFNzVZMGM4SC9XVW5wK3F2NE1MVjhnTTVGQkJ1aXJGUVYraDNmemhYV1VtL24yeUdWNFdRa25wQVNydUd1SStQSlYrL2ZyY1lBbXFWSEVKQUFqYUExUm14NUxjTXcxYmQ4aWFBd2VMdng2c1NSdXNTWkpCQllrYlkrZU5vcHZBTk1Qc05DbnV0c1diOGVSeFE0NTZEMjA3M0luQXFLSWNqWXRqdmk4V0oxTlJGTEIwQlkvcllDMUMzcm5nNVpQWUVNS1ZPTk9GbkNXZnAzNHppZW90YzUyVVhDcktKQ1psZFJsLzdna0xSS2s4Rk9zKzZ2Wld6cXV2UWh2OXNqV1VtUFhSSXBOdE9BQ2pQMEFoL29LOXFhK1J4Z3JWekMwbnpkVEtwOEpPVm1JejRTZEJGMHNjeERGUXJOVzRLcS9jTFh6aU9RbXMxSTF2TnlWTUlhVnFIbVI4Qk0rNTlZMVNqWjVKTm1Ba2Y5aUd5T2ZpZ3dGNEc2V1B6MWd6Q3FvWEtEVlVSNUorNmQ2RVc1N1BlK3BCTjdjUHlzOFZJcGRjWWVjcmRSOTVzTEFSK3Y4RVhVZHNKV084Q21uNlVoNkw2T01WL3lmSm0xampDUm44NzFRdjlDb3B5UUZNeVdwcktaSTYrRXlrU3J0TldETEVZSExYTTRsZlhoYm0yMmpVVWVSa2xoYmdPTEcrN1B5bWV3VElaTU9CVWF6dXByT1drZ0FmZkdjcURWMTlzcUd3M2dMMnl1VWF2WHgySmNLMVpoeGZWZHZyUjRWeFlWaHVnV0I1dEdjQjQ1ZUE5SWRIbmpteFhYb0t4S2Z3dTNMQi81RU9uaFlHbmxrWnhmNkZBVEp4ZHduRXlIVzBnd284YU5TOENjcHRuVGRKWVdJNnFhYzFCdG95b3VNNFJJK2tCdUI5Vms5SUdvVFRxdE9jc2dxZXRnZ3FCaXlJMVNscGgvY095OVB0NVdLNkpwTkdKczVrK1FaQlJQWVlpakxXYld3U3Z2THQvaDdsZm4zNEtrS3hUMFJQZWtSRVFqRlY1Rmp6KzhKY3RZNUovakRhQmRlcjhXQVZLS3A0bHAvVEpQeCsxWHk5K3YxSExlUW1NZ0lNaTk2WGFOTWRiZi9xbHhUZFB3YlA4N1lXOTFsaFFVU0J1WEJiZ0lpaVpBV0NlSFExYlFvWTQwaE5naXZLVlBybDhWNG9Vb2o5cXFNVWRabWM0SG9YaXk5WFZFeUFSTGNKemZodVpYNFEzdkJBdjRoZ0l2ZXZSaEdCZERQcENoejRKbEg0Wngyam5oMHdqY3ZkOHVmYXY1eWpyKy9XMFdiaFVVaXlrcVM1bm1XdlM0QkI1VjJnd2hWaDFJYjZhaWZNMFhndzF0YWh2SkJZcjAwdllZdFhuVGJKdS9LQnJTbXVIQU9KS05FcEh0RW5mdmhKZ3NvV1hhK2dhNUxRQndvbk1MSU9mNWtsN25vUnRQUldTN2puZWt4S3J2L09sNXB1UHpKSU9ZUUtUdHh4QjFXaDVNN3VuRkdrQkh2eVF2K3Y2TWxTZ1RkblBORThYZWROREgrREVYc3YwZG02MEhCdWd4ZEowbHJaeXdQQUVwWTlPZEtDMTRJMnNFcTlweklTUi93TEJrZkgrbjZVTHJaVlJSRHVaOHpEQVNVcHF1TmtBcjNpTDYyUDhZbDE1ZTdrdDNqVWkzR1BwUEFadCtiQVlqbklBc1Q0YTJqTTBCUkxNbGxmWll3cUVoNzFWT1VoNTA3ZEIrTTVhdUw4RDJYRk5iN0NEZk5xQktTV0tLTjhSNkJ5YjJ2bkYvcUNsdVA5VEZTbnVhQkVsODBIZUU2MHM0ejR0d1dqeDF3SGFGMklpTk01QTVMaXhsSUxPMWo3Q3VWYkI1TEJXRTBCb0ZCMFc5cDZDN01RRE5CdnE4V3hONTVxT08vajArZnRsZDBQTXFqaVlsUzJMcTYxUGJNWnVUV1lpdXE2RkVXc3IvQk0raUZ1L1ZNc2o4M1JWZ0VvREJ4bXU0L3greEpHaStLZTVVM2YxYXp2RjlNQTJVOXFzRTBrSG12VnZtRlhrK21FemhxRHplM1U2ZHZiR0hDTVVmQWdiaUI2dGxGNmc0NjEwVXN2bDd5QStwanNPSCtFNTB4NnlRdFJuSFd0UkFFaEdNUERCREUvV3BRbzMyOFc4dlMrQ3h3Z2t6ZDkvS25iSUFacFUrMmg3dVZVaEVkRXdqTDlPUzZ6alJTWkExZkZwZ3hYamE4YlJvQkVEYTRqT3VheWZBOE51bWdsN09JNGlYc0VXbkN1UnM3UlRrb0JZOHVoeTNvL0xwS3lRWEIycytPZnRYWlJoYmtkbFFrdWFnTGFMM1FPT1hIRDMvMXdlZ3VwS3RsUmZoSGxyV3ZuMzdmb1lnL01zVjZ2WXB2RDBVUjZJZFhNQlVWVzhJcDhJdThNeFlFWHBuK0ZlakJwc1JrNElrckVwcUUxZXAvQTNzc1JyZWlkNEtudnUwcjlYa0RKbHZlLy9KdTlTMmRYNUR3amhjc281SSs3U3o5dmdlazlwWExrdmNzK3lTZEtOSG9ySGtmZlRyV0g4QWJ4Ymo0RnJTZjVzQXJKbjF3OElOeUtYdVh1OGo3OFNFTW5PWUtzQWpKanJxOGZLcXRRUWZDMmQ1SU45ZUJWOUxxdlNmWERrZmxqWGhDY00wS28zQ3pMS01makcwUmdGTm9PTkMrZGxKUTgrQzl0NmxFVkR3UXZpa01pQzNDcXdMcDQzdk1tNmliRkdLVE16R0dERk50ZUVBWHo2SWhGMDJmNW1ORmhjV2xVVXNNQjlLT290WWE2TTdrck1NWEpBYjBpcmMzN3dIYVFWYjhhUzJkS1M4bURtQ081UlByT3Jha0VJSElqcENITUtSRmxUMHd6T0dXZU55emtwOHJHZnBpNzdUaEZsTUtOQ05kWUNQeXJYbXlGRVZTck5MVW53bk1PaVpvUEUzTTZQcElnRHNNamxHbkFDUFJ1UkVQOG8wN2wvOEJNcnYyL2JOc3lVZk83Y1dlUnVaOGl6aHNCRytqbVU1UzY0clBzTFdnTXN2eUdTa21tV0VzeDQxR3pUNERpaUc4SVlsbWVLWWt3SWZFemYxQzM4Vzd6a0RhRlJmbTdWc0RmclE4NWNhS3k0dGJZbXl5WlJ6NjV2OGYzRDhjcjVSSWJSWnJRNTYwaUtzNVJDSm12U08wQnh0SnMwUmZ5cDRYQk5mYkl0dVBsdG5xaHV4aEFlenkzeElieXRDVkJjeG4wVmVkdWtQRURhNnBoTGpibEhuS05aRDNkMFpRSHZJWjNVS2MraHE1R2c4TDVqN0N1TU9FUFc2OFRub2o5LzEzRkxwUllXdzhPWWJ0K3BzVGJ1UVBlbVNnblFHNFM4N2FtcWpEdjFCSUNkSURYMkJyUXRpaGNwNDJQTmhlREhSM09yM1dONFRCNnZ4RHh3QURrZkUyR1hnUEgrQkoySFZPb2RTK3ZDcFFXdG5xTHltbHFHb1BUZTB1b0dmck5xeVRPcnllbDhWTW9DVmo2T3NZcmE2QUF6Z213WDlrYTVlaTJvZkd6Y2ZKczJEajU5dmRMM2IzRzd2NlczbW9zM1hMSVNXOGxGOXVWYXNpS01oTGJzV0VtYnNjTGJLaW5ZNi9QdHA5WXozeVNIYnlTS0JiSnQ4bWpaTzhCcnRBYzFqZWI4ckJKM0tlUEJlSmJqNjF6UjRWa2F4LzQwTnJEUm1LVHdxTU5TWVhZazJnWnJwRlp0TlcrZ0krZEJWbVVObWU5S2RuKzdMUVNOaWVGOG1rYW9IUlV1eFIwZWRFUmdqSUFiQWoxdHhkTzh1ajlVcWRwbUVHa2JyQXJzdnJIbWloRE1GNXpjRzFOM0FiUzJ2QWdmUmloTjh4OFhIUUVEbDRMTXVVY3NId2RrVXRzYklDQ0dBa3l6bVdocVV3OXU1czliREIyZUhkSVV5TnpidzhFSHViekcvVlpQVktaRnNKelV6d2NNZ0ltQ3FzY0lLTGIwV21OWU1Dc1RYRVJxLzFrNG5YMGFVcS9rMEdrR1BHaGxuZ3RLSldETTg3TW1YMi9XVnE3NzZ4WGZHcGZXYzNqSElkZUcyM1U0SEN6ZFZCWXlzdWVJVTY2ZnRWZFRJQTd3RUF5V1BsUFk2TUwxQmxKU1dtbWVFVFdJZzdsdXFIVFM4bU85ZFVITldJbnludEZNOW9tUEFjR3hqOXpsNUc5NHp2ZkIxMkN4c2swSlMrcm9DUnJyREI4SE5mbkRZSWZWTXd6Q2RVYjJNN3ErTXpTYTJuRnhxMExNNWNXaXlocFZwclZNU1RGN1BYcUpjaGFCajM4MGpDVWViK3lMdGFWZnVOTUJ3dUhmYVpoYzdLcExXUEErNW9UWm1rWW92cmlJTWhRcG1rOWpFcmpsNEJpSVp3NXFnelpVdTNqcjFpVlBDc0hDcTJSbGNnTnNoMGFwMUI0Y0VZNTU5bFRSU3hmd3JLdUtLWTVTUHpud3lkZXdHakVxUDR5MTdaenVOWTllMjFQUXRlT2ZmUDBTajIwSnpMRUdKWUhYZEJlR1NVTy9PWVhyZXBZSks5VVdWYnQ1bkJROEs3NzlxN1h6d3dUK1JubjhlZy9LTXdGUDNjTGNlOTdEZ05xOExyV1hwVGJmNnI2bjBlN0N3K3NuSU42STUxUWhHSGNhbnAxZFE0aHNjNjdjUDdmbkszSTRKTXpsTFllMXJUdzNFdzd6VEdEdU16WE1NZy9xWjFsSWFTcEY2SVR6Yy9jRjgrQjdBaE9waG9YYnhRSGFiZkRkS084UWJxTTRsT1BnUU5tc1JoQUlUNHJ4WExvWmFNK1V3aTd2bEdScWdxa0V0bHZMTGxHUHpTSGFadEg1VEZJV3FIdGtobnlybGRGZ2JnbjYxamZyZHRPa2pUcUpXOUVSTDdwWVdLU2NRSTRNSW43VHBIUTBDYW4wd252a2hjOXNxMUI0T0M0bWtIcFM1TkN1eURkUHNBQ0k0aCtTNXVIMWk4eCtRc3NuaWJUY3FWc0JzY2JhYURuenhQd0NYcTZiVHpBNEdRSU91RXpJR3JFL2xCemprcE1ybnAxclJUcVJGNWJQV3ZiTUNoWXY0TUpIeDhTeHhuc0ZxUjJGNXpDQkVERk5kNTE4bVpzcHhsSlZZY1dyNTZ4TGFVRXlndE9md0FiQVhHcFRDRjRUR2lNOGNJUDVGSmdzVGJBaUhMRzFIZjZzQlNsY2dYa21FK0Vub1FyZEFlOTRzV3dxTUJSRVRRUTdrTVAvVGVwVHpra2RuakkrNWkrWnNpaFFwRitkVXBLZTMrK3ZMVy82cTY3cXdRWWI2b2grTHFneHZqUERQZDFyS1p5ZjZCZk9rT3ZLVjRaenhxeTJPTVl3RWhVZXEyMkxkTVYxeHZhNVJyNG1Wd1NpdzE0enEveFRlMTJlZnE5TDhtM3dvTVNLZG5pRDlWbkQzVXozRUNVQldURnN4eTBha3RwYzRNdmdIU0ZGTXJjNjJVbyswdXVCY0E0Z1NJQVZqRXZxUnhnd0ZxZ2lKZnpMeEdVbTdWYmhRWWd0VDMrbnRUV1JRZ3JpclVtQWtrK0lQMm1oYzZwbmhZbUVjdVNBN3BZMUNTQ0dzTklRR3YyQ1luUUpKVDNWeVFFMXhoNTZnMkNtS0FCMWRxTnM5YlJDdHRIQ0h3a0pjcm9ZbG5sNGF2VEJuK1llWDN3aFBTTTdZQmNDaEpxUmlCZzdHZ0hEU01iOUpOZ2o2ay9lM0VWQUQ2OFZKZ2JsZzNuZ204YTFGS2lZL2ZVTGFBTHZYWUlTOWdkOXlCVGtqTzhKWmttSDVQaVVUWW1zWEptWEFQVzRYMWRRM0Q2cFlXc3lnZSt4c0hPYmoyR2VBMEkvd3R4dnUrYWc2TzN6cGk5WGpzRnhOaUtrWDZhL1FBaFZQYURDM3dZNmJpV3JmTW56M0lZYXNuSkZzVHFuUnBLd0JvWUVlVG9kbmxEN0dXb2M4N1VzOXNFT1lLbkRrYytRdzgxbXZqQVArZExlSmhOek0vbDY5akVEbjQySjB2SExlNVVwZDU4cjB1UFI2b2Q2b2tVcFdpTGtuclZBdjQ1L0RhSTB5RUFZMHRFTndpOWpqVVhaU3MxSTIxN1huc1VNdzM1VG1ReEVJODZFQVNncHZUQ296bjBSSm1yeVBDazRlTWRNMlJpL0RBVkxkaG5IcTZmTEltOVZPMmhaV0tDRVhvc0RsUmhCQld1R2c1ZmlXVVNvZFJ0Y1dBZWh0VG1ER3dCOHpEY2hEQ0V5YXNPVVJucktxNG5uaHdVencxdUJBY3l5ODlvQnF1YUxwbjhCWnR1Ymh2QXpVVGl4YmtvWlJrQStweDFqWFRpQUtDZHFlUmFwMlFENXJtaEJhVnlqUy9hWmtFMklzaERnc09IblYvSDBWdXYrajIwS0RSQko5VHRGRldvZ2VqMFFuMWdiT1V2SWIyeEdkaWlQVjFNY2JoVjhETGNiY3FndHlUbUNCVk1CVlhuMkNxMmQwalRlQW83MVE4OEJMY3hESGFRZkRyR1RLOE9sMWZrejNvUGozcytobnVnVUVDL3ZHb2J3OEZRdGtFMkZaNzMrVTBBcHB0aERoTU1RMi9mVEI5c0lYQ0RqeDJnVjJOL2hsWG9naS9VZVozSGJhbUw3aU5ZUzEydEdWTnFDSWY4dkdBNDA2cWdlQXF5cnFzdkRIRUI2dWI3dUNqU0V1K3k1Vlc4ejhvQXE1M1NWVmFJbWlVbFN1ei96Mlc5YWxmZEtnZzdtMi9hUG53Z0F3Y2RteG1XL1VXclRXYWxTN0EvY2lSdFlDRElYeVhJQTA3bEhZdHhPTHJRcmlVWEFUcU80T2xqdjZWeXpSQ2xzZzlOZFdTa0pEeUtkOWIyWkI1S1BLVis0Tys1NzFCNkZROXFmWDd4dFVZY0hJemZGMTFUbUpoL0JWSUk0NjRSU01ZWFhjOXdjNFhiaUMyUHpjSFk1Z00xcUtSSU5kUkVJTFYvUlVqdnRvbThzemlPYTVGMkJiamJUMjNGRDJJbDZMOVFKVmZ2aXJVL1VBL3V2cm9VTU01S25NbDcyTW5MYjN6alV1THZPMFh4elVKeHRuZy81ekJ6WnowSmg0ODg0WFlmM3FFYldyQTBYNzY0UjdSbFdWT0drZnBxZkJyOWZiVk5wVHNrN3B2dHQzTC95VDZDTWhIcFdiMDkwVUV6VnZ0Qk5zZmpqYUpGMnRqVUdjUkpHaWUrbkV5ODNoamk2L0I2SC95aVBTdHlZNVlXSEJuMFlYbFdXdEZvQ2JtcXJKOUQ0V2wvaHdiZmtBcTQ0L3Jhdi96MHRRZWYzbC9Sc05iVEpzV2ZUd0dXbWp0ZE1Rc0l2OFkrbG5Bb3k2YkRScml4RHNleW1BMnpsd1k4REppSzJqWWtBRFlNT3V4dHFpczdPMWcvSCtDUDcxcTI4ZlIwTU11RXRBd1RodnM0Z1NJVmQ3SlZ4enR5U1VSNFpVUlBXaWN1RmpJaFBES1ZCMmdzMFpuZVFxdkVOOW12YmpEd2FxdjF2UitWMVByUEMrZlNPUDZ6bWd6d0xqL01uV0R2UTdKUHBVcU1HOGlialk4VGlib0RtLzJiQmlhdUN5ZTMzNjlPNWROQ1Z5b1BveW8xa1hhN2RuRFZTU0JDV2xZSlc3OWpwNE5hNEhqWjB4M0VOaWFrY29rYUlrbUZNQURXWTlqL2lwK2J2MmpEcmRiVE4vSUVGVzdNTEY5NGROVmtkaUZFQ1VkK1pmU2pWeis1MHFDenFOYjYrSktwcTdqWU1Eelhmdjh0cjdXZmdiSE0wcWNWck9xbWpVYUwyK3JwVW1DK3BlSEVsVXJNRXFBV3oycWRnaWIrZzBxZWFmVXRVaWVEaWlNWGl1UDlyMFp6enJxdlk5YXRoN1EzZjB1eUFVZlZFRFFRTEllQ3RkV1hVWjBXTVpCLzVDa08yd1dlQmZWK3JiQjdyV3hQaHVGZDJDcHB2cktKem00NHgxd3pHNjg0VWxsSHREcm1tV3VXU3NhWVJZd20zdHBTZGJpbldaZUU0S0JENnZxU2hJZ0lCeDNzOG01Z3h6OW5YaFFRTDNRZXo4K0orZklyam8xTGJyRWEyL0hnNi9wZ2l5azZOdUpTNFJMQm9xQlNaR2FOQjBFVVdoWWFyQXJOUDNVZTkvQ1VtdjRFdFJDWVV1Q0EyR0FNL3ZPMERGNDVQbHEwT0hiNHBtdDNSU2VUQ1VlaDB3bGZCVzV5OVU1SG85Zk1FQWUxZkUzSS84dHFzZUh6YmNOZEp5WFIyZVQySUE5NE1iVU56a2ZBSVJqRmo0Y0ZjanVQeE12eENOZ05uRXJ1V2RTUC9jakVpSDZLVGdXSXRRcGM4T29ubXl3aDB6QUVCSHZuT2NMWWM2SThGYWF1dXl4MEh3YUZUWUxkSVBMOG1PUGM5V2RtaWtPWHNLK25udlZVRlltbXhNRDhobzQyV1hLRmVOanp4Y1duWEpkbGFJeVpoaVhteFlJY0E2SS9WOU5pMXI2ZitKL0VtVWxBOW1RemE2a2l5eUI1QjhaRGFZSmxWQ240SlZDOFoyV0Z1eE5tZ1ZQV0hFblR5aGpucEhUWGFab2MzQzdSajlENGI4YVZiNlFFcThJQi96NkhZSVZXNGRQVzNuU2dxam5SMjhGWWpMT3MraUVtRXNKZ2tsMmVOQ1BhcTdrbzNnTUNFeEwxeitqVDZUU3FhZW1HRXpUQStha1hPakNhWHdXMjQwOXJyaUlza3lCcUExN3d6Q2phRFVlSkNtdHYrSVJKdG9ITGJzbHhreGJzUWo1NElNeVZyTjh6Tm9NZHB4eC9ydjl6RStkeUNLTkY5cmhpbmlzNWFscE13MDN5aWpXcGRsa0hCQXczUlp3b0lqUWNJYkJMdFRzTW1ya1VpL3ZGV1JGK2NEVjhzTkN6K09WS052b0lzUkNzN1NERm9NSDM1UlFlU25QMTUrWHp3eFJOZ1BUQ1h1WnVyQWNyc1diK2hpVnEwd3ZGcGx2RmI2dlFwRHN3eU81eTRoa1ZWRlBTSkFzMGJ6Y2ZKMEJUS1oxNDFqVjd1Q3ZCM0svWFFoUzRVZEpXL0xYS2J0aCtZcVczdlVJRHlnU1N1L3FId1pQZit1dENTSDl3YTJOSTJMZnZ0a2RRLzVOalBCRXkyeUdtK3ZXdlRDMUpxVURtaWJENUh4TjdZZml3U0txdE1lMlo3YnFPWmMvWFArOUVqMnBBaTErVmpoZlVBb0NKWCs4cnRxdVpVZlRQbjF2ZWFkSGx3M3FXZ1VOVlhLeVo4S1lFKzhyNU45WTl6S1JTcE9Ua0VmVmJlTG9FS1lUSGk0NzBIZ2pIb2hjTzhOUE03ZlZHT0xkZy9XWUhyeVpTNHpKRUxVbWkyN2xKY1FaSGRzOUVqZmNLL0szU1VtZTZXcGhMUTZEUmdSSUZTdTI0WU1YS1JnaTJrR3M0YlBFSVkxZENNTjRvQ0hYSVQwMEZoYXdjaGVhd1VQQlJEVFpSNGhlVHgzRTJQZDZWQ2FyOWIrbE9oZ2NqRVN4U0d2cVZta3Ftei9TUXVTRWc4dTRqbEMrb0FGSzdzUHpVYU81WGlGUHRrdEZLUDZDZ0JGeVdIUXdwTlFBMFo5VDhoQ3VVbDFxSmZKSzA1WW9DcGxGSFc0REpiVmR2RHZyNHQxT2tLSUxyNFI5c0hlSzE0TW5rMjZQR0svdTB6ZUtNVm4vai9JcGhobXBxVzVDR2trZXo2dVZrN1h3YkQyQnRGSjJKUmJXK2ZNZzBUOENibHFNQTJDYUhGL0J6OUNRaFd4QlR2OUtPVjd0T2NveThURHZteGZTcGFyOVUyVU9RbFJZQzNFZUlFOEVwWHM0NDVuZzdUd0NvZW92R1N6b1VMeFJRaWhWUW9LS1hROWpWMlM2Wk9HQWI4d0s5c2hFb28vd240eCt6N3o0cXNFalpkT1ZHbEFxanphM2VINWY1S3pETUFTSThDSzhKekFsRGRJeTNrR0Q0d3VqNHVQMHhwK203WDREcXkwbHBiZmNIcm5VelYrT0I0ZitYaTJPVk90S0tpejBxaUxjTXlSbjdCcFJYM2RKYlhSN1FvWm9ieHNiZVJsS09TSDhWQnNHQUc5YzRJT0hxQ05McHJzV01qYllYanF6ZmcxU0IzV29LUVdIZmZ1WW1QOG1JQnEzUGpPYjNnMnFXVEpDeWROWnM1S2NCWC9TV2Rna3hnZ2EyMUY5bmRPS01LdGRucnc5T3BXdGdvUUZIL2UwVktveTlTa01wcWNnYUpmdnptYmJraVRBMUt3UjlFSDdTMXRrNmNRZ2RreVZycXZJQzR5bWN0TnFFcWZxeGlHOFh4VkxxUm5SazJKb0hIODNEczR2U3lhSG5BblJCK210VXEzM1dMRkdFL0tZWWZQbE03djRWS1FlNFB1UmxWTjk4UGsvYy96cU1BcmVYNERRYmtrRDdmOUhEZS9NNm4vWUJiOFQ1aWM4Q3J1YnMyWTljMDMrU1gzak5tS2JUT0VuTDFpT3pSVXhXNzZVZGxXZElHcFpjVjhvdzdwVzEvL1c5Q2pteG9XT3lxaWxSaTZIKzhsNS95d1I0RzlCbnpzbC8zWDBMc2VZdXZldWJNRUFBaEpFTmxFSDB3SGFQYmhOZzhtc0tyV2VKMXE5T2NZMXA5eFZuQWdKcVo2QjZUZWY3U2diVUdpTHRHNmwydGdiYk8wRW1PVG9uL0c5RHVCR1AxcXdnZ3psMzUyL0oweHcyM2gvZ2ZqUllndmN0M0I2bDlJcXdEMVFlV2hVSXh0blZ6N3FWTGlTclY2N0xrbGVwQkJHTWFFNHlzd3p1VktzNndxd3M1TjNhVHIwbjErdVA4UCtpOGpHOExxbFBCdmR4cXdNOCtScG1ZZk1Hc0cyNlgxTEF2eG9razVGUjhaZVlJM1NtM0dyem0vVFM0anhCcU1WUWhxNnFNRXljdHBLc09nN0ZMY3FJRU9pamdjQjU2VCtZWS9vcUxtRDVsaGlBUFhpWlFRc1IyNWhIU2JPcDR6TFdqWm1kYmRDa1BETThiY2JJcFZlbnJyUE5vZit1UDB2ZWdQSEQ4Zjd0OUdBRVNScTQvNllOOHNnMlNnVU1VbXFFaWpXN3BFY08yTWg0NERoMG5hWG81U1dZNkY4eHBVN1VFYVNTWWI5Q1JDNGpUWGJ5VFJVUlZ1WGp1N0lXMnBFSnBjMldkb1hCSGJ4SjJldW5iWDdWUDdVcE5FcDd4ZUViQUthaXFjYWJQRFE0N3Nnd0ZxaHlNMVZwdW1FZVRzYjVDMXFSKzhNNDRNMHExZmlPQUJCOGVTZHZ2UWg0U2hoK0FLWCt5aTJOS0tDREs0Ny9XdzBpQkIzQTZvWXZxWWNQY1Z3d055bzA4NTd3QWIzbGdZa09HYVcrTTh5cjVtbTB5cjAxTkxIZFplWHlYZnBhaTRUY2RiZG0xSW1MVzlNbDdtSzI0NVlSUi9CR2xKbjliMWlPeCtWejU1ZjR0eFJ1c3RhWXF6c3dzTnYwQkxyREhyNzIyQm11TXVHZ0xwVXRyS050WVN5aGFsWlNuRVliRWppRVhjMlRRMWFpRFVHcjVLTmVsdER3ekdERUUzQ3hpU3JjTTB6T0hKUWlhb2o0b1FSd0ZGVjVBOFJwR2VGTXZqc0x1QXJLNnIvQ1NnNDAzcmZoR0NjV0pLdk00ekFqN1dpelVhNy8xYkc3bVRuVit4ODhyVW1OcVlCN3h3SHUvbEJoSGJ1RHAzTGdjZVI0dG1kRW1jMzFycmRVSGJMWXZoVnFyYjFSdWVNYUljOCtTRUt6emFOTzVHeVVaRGRrMWdwVkxhTWRNQlZGem10UDB4dTR2NlVCNkdZQS83d1IyWTNnSjRBZ2VCa2RPWGVxYlFKNEQyYXJqWE04ampBV3RZQ0FuQ01sU29BelNBK0ZnY01pOVhSTVQyMitDUUcrU2doRHBSTEhQV0hWWjAyRDZEczltUFYxaWllUDZJRncyalpiYlVIei9MeXJKWFhNaWZDYktobm5BY01haEZTb3dwMmRCZzBkR0lMZWtiTUJDSEVBSGg3RnVVYXowRUp2c1h1RW5INllIZXY4eTl6bE9kRmtGQXB5U1NPbnRwRVNaZmVGRUlIcmRjcU9wemxvYW9nT0w4QzNBeEZuMWdDTVJVOGFoM1hJZk41M0JNTmp6M3hUWXZ2UGdpWE5oeWM3WVFOWHZZSW5XakR2b0pRb1FyY1NkQUtZcFdqeWdmYU9OaWpBQVJmb0wzVW1JVE9zTUFzZTdyakFyY3FGZTJaMUpEdGVPcGIrcGMzdU51YjdGWkRodUozTWlKUjRUUklLanJWbWdKa3hQdUdXMVoyL1VNNnNaaDF0WGc2MmVsNkJiRUxYQzFOblZ5bUFoK0hhQlhnOWxmVVZNalRxeWNEb1BESHlFM2xtZFJkU2lEUWNxVnMvRUI5d29kTTZ2V1AwZ3Uwei9jUUZVek5DZjhabTlZZDZLOHdRcXRuWDVIbDgyRitVVlRSUnBrWHpFcVdHNG9RZUFJdXFNSVMrNlBCMTN2OWFvMzlqU0Zvb0ZORzd0NnY1Mk05QXFaQWJEUGlRVnYxLzdYV3VBcGhIdjFjSHJhTGJoQ2liNUxReVlpbENvbTZjK1F0UHpOWE9Pd0wyRS9pblNyMFFacnlSQXJrTHZXU0VBaExLNHVEQ2dQSVRKcFczRkZnZnVwWkFxU2Jna1JLUDJnSklaVSszdUpPUmljdm5IZkthdGQ5dmd6Zmd0ZWRIVkVvRmg4ZkhXaFIxQUJ0SS9xcWUvcEJNRExJd1dFT0FhTmdQTTZRdmlaWjBPa2NEUHR3eTlkcHhtZUxHc1Voemg2UjRMWkI2WTkvOE43bkJlMFJGTUFqTk9LQ0NZRE5nTmFrWDVnQTYyeTdWcXo3VkRjQUx4bWI1ekJoN2NweHV1ai9wN1o0TERmdVRDOFN2aUsvY1ludzV0SDNaanBvRFg4bVdUZEFlbU1PNjFZN2k1ZDNtRkdlOHQ5a1B4a1FFQnVGMWVxbjJxbEc3aEVyRDZlYVJwS1VUYk02czlXbmxyZUtWS1UveWZsSEUwTEVhdDZiZko2K1pBY1FWRWtIMkxUbHRKSTBXb25WTTIxcFpETzBaaW9zSHJNeXcvSnBqS2wvcE9BTERuYTZKWFZzU0NCcndoZHJWZEFFTFV4NStEaXBSVE9wRDlTcGVkaDNsZWM4SStHcWZhcUFiZmhIWnBsWjFIRTRBODAwTGdYN2JXblhTbHVIOXRROVVJWk9rNGZBdTlqdlE2OS9Rc1FTY1dQRUV5TGNGejAzeDVEUHZaeVlHR2pOZHBISytIcnE3cWpwNE5ONW1CcHovTEpOTUtxMXNWSStBTEZNbGc5cG55aEdSRmZLSjdGS05aRjhYOUJDRzVCNnVLUkRFSXNRRGRkTDU4YXhjSGdjVnhxa0hBNk1iN3V5RHJiMnB1bFRaeVRLOTA5ME9MUXJndXhJYW9qNzYweER2dkhicDNhZEU2T2dNWGlqY3R1UFM2UmNCSm1FT1hMMWNMY0NockVCaFRSMnpCZXNaaTdtNFc0UFYvZm8rMUM3dFZuVmY4WVNxUHJONGVrRWZVRzZuNVRUcVgwVlFRalMxVkFYamZWek1DS1gremRpYktJWFoxbXhQdUVxcG53RlNxVmd6b0pQdFNrNjZYdTdjUkZoMnJOcTFFbDJreGFxQ0oweWdlQjQ5SDcxN1kvcDg5eUY4Wjk3RisrdExwT3M1cU1ORWhRS3d2cUtZM0F1Q1NmNGEzMnNHanFvMkwyaFA5VHN2WUVteHJQa21iZTgvQ24za1FBTENjRzdPaVUvc0F2SjR6Q1lvTmJpRW9hTFFQVGtTSG1IWlh0ay8xYW9GYmF4S2R1WmwyalIwdExNWm00T2dpZythVE9PaVB0UGNoMk9OYlVLSURxZHdlcWJOZENySzZwc1kxOVZ1aVM5d1p1aUtZYkEzL1dvT2tuOEhXTzQ3dUtIeGhhWEc1TjVNaExpNHcrTVlyNTl4NVFLTXhuODNBQUJCWWorMFRHR3laRG9tOTlFVGlyamRVTDZuUFJ4aHVDenlMeUZucHJWN3ZINjR1aW9uUkJyRDdtYnVFMG9tVXJkY2JzWFZ6cUtiei9TRHB1NXJTY3pEc2dLeVFmWEdsK1Z1M040ekNDR1NBNzE4MnFpVjBmcnNnL2hYdmNUaHVRNGxIcTNLZGVGcnBJNUpOTWR4ZHMyYWx3ckNqdUN0Uk9YM1pvWGxVbG9QcTZSbHlWRjEwUjd5TytNemxGL3V0T0NJK2lzQkpiOUZwdjVWZEFLWG9Gd3M4Sm1NQmxJeVFvV0FpNTFyeU9PaHBjTGJLUFZrNWU5OW14NUxoNi9NejhpVlYveURGMEt1Q3dwZVJ6RTQ5MDh6K2U0Qy8zcmVudUVEOVlUM1A5S1FmbGVMUHNNQXVSUHRUcHBqSXJwYTdpV2xJSy9JY2RpOG5iekczbFJMT0hFS0xPV0ppVlFNdGJFei9FbGdsMEZWMjRKUHE0Ym5Fa0Z6dk1LaHRQdTFkWTg4c2xPbGoyUUJ1cDhnKzc1ZmowK1JlbmZUek8vWmI3V2VSbHZNTjVOMzdyNmJ1T0dsOGJmSGpZMDhyNS82TCtoUENZRE9yWkFVaFJSNTd0N1lOamlZa252Q2l2RVkxVVh0TTN3Mmk0Rks5RjQxczAxaHBTSzRmNlNjTWl3N3lEVzJoNC91RkJpNW1nTVFxWmY4SmdjdG55UEo4UU4rUWNEdnRaZmFaTzJIWDAydXlPcjRXdXdNb0hocWFRMTFFTUJla2Z6RDJFd3VxbVRiMnpBNnVzdVJyZFZ2bU1jLy9GUmpzNWZCdzl2Z2JDYUozMGw4UUY5Tnk0R3FQN1hIS3VhbXdIQjV2MXVWQ2w0VkphQkdLbkczRjRyUDJLc1Q5eDFiZHJiL0RDNDM0UmxhWWU3RERCa3kvQWdSL205VWZuTGhoYmM3R3dzaWVlR0MxaC9ISG96VnZKbk1XWU1lc3VkTTR6VkVqMmcyLzh2bnFLdm1OVzIvMG5NWGhYMjZLTTZPeDhJeTVGMnBSRURWZDVxZlhWOEpwSGkvVzIzUkoyekI4UlFEN093ajBLeG9OSUoycDFjY3FsRnJsbzRFNk5aNU1pbnlERWlqSjJ6RktBZjg0NDBjdlNLSGZLY2lIRTlVS1pCWUdBTGVzY0tuNDJqVjVqS2MvNk5OWnNJYWFRQkRpVXpkUUYxZmdVUHhNcEhVWExDaWtES3FtWmFnRlZKUFZjQmFUMEhtaG1vaURNWnNXU0srYmhJZDNvejRqOWg1SGZvSUNnckliZzVwTjh6aERodWVsSUUxalJGTjBONVRNY3dRVEhMM25neENuYWFvOVNhUXNYRnl6QjRmN2dMQzBydTlsaTg1ck9PdGxQcEt3bzZvUVFiR3MrWWo0YWR2TEoyaG9zQ2lpN2ltYS96VElQeTYweEtHZUhOWkQ5M0daRkM4UGowYmIydUpZMFJiT0pxc1VIWjNRakcvV1ZiQk4ydjhLVzNTaG1oOWRYZUt4VVFuQWx0Z1IzUDR0YnQzbDdqc1VFc3pwUU50aWRuRE53VkN6ekVUYVdON1pPQ1VsbEpSK0ovaVZOVytDNDF1Smx6MndEQ1BvUTFWZEczRmdXNENKdG9tNnZCaUF5U01vSmhvYU1NMThxSmE0TStpU0gya0gxVlgwd2pkclZWKzQwUTVodmFwaEt0M2phMVdMc0NkU21MaEk0Vm4rZkVEdzZnSG5BMVNhUWJlc0czdG5tb2RUeXRZeW5iVzRTa21Kd21KU2JTYXp5cUo0WTAzbEN0d0dXa1hTODkrR2kyUmRXaUpXdkl2MmxVMDNteEVGVTZXU21LUUZydTkyLzBaRlRxQ21oQUxMNzZ0dGZjQVpPZ2g3Y3JOYXVnajhtSnBHV3E0L29aai9OTk1uVzNpa0pXNHlTN2E5eU9rNDA0ZDFWd3RRTXFrQXo1aEY5d0ZvYUY5Z0Q0OENKWlYxZzg0UERDdGNJVzlwQWZPd3R3NGVMcStEeHBkN1ZHaE4yWjlzSlhHdmlhOFF6RjF2UnphanZySFRmN0N4Qm1JdFhjaHlUK0pqZFgzb3BubTFhR3c0ZmxyWkl1REFsRlhLSU5HUko5Tm9XRFk3YUdyQUhlNWZ3ckVxbVV2VFRNYWJZeFpDNHFWcnIwMm9QM1hrZElaNm5uclZrZEZuZnhsS1FXNFVxNktKOGs4ckdwM0dCMXUvZkU2UHdSaVFDT04xNTk2ck9meDg1K0s5Y2p5K2h2MEtkbTdJWVgveG9EeXp5NzJ4T1A2MmJUcldsY3dJb0oxMHN4RXpIeFBqSkVpMldwOSs0OU5BYnplRTVqb3Jzd2NZczQxNCtFSytwT2ZBeHR1R0lkaU5OUXozR01Gcm1xdlgvZzVCUHZaYmlOK0c5SFFuWko2cEN6UGxldGZqdmlRWFcwUDVBTzh6c1h6ajBlMVlFek9mQWxEYUNnc001bkdYWW9xVi9RSkoyUGxFTitpc0FnSUE0eVkwcFc3SnZzekx3WGxGd2kvTmtnVnllVVJjVnFPS24yY2NWelljYmtFYXoyTWNLOWw2MUVaSjc2V2tuZDIzVmxzeExMVFEza0NUWXI5S1lBMWhTdldsK0JUMk8vZStuY3JwKzZDQUFEeXlsYWs0UktqcTRLRExHVm80eTUxU1pwMW92djRWQzdnZXVmOWtsUGZoMjFmVEl5Q1dSaHRidDV2RUFaYk9NbjV1bnhITTZGZ0FSeVRCRGlMSkhNNGh2NXB1eUdHZmJkSkEvV0FXemlieXkrNTRXNTJUVlFib2VFdFk3bUsxM3V5Y0JWM0dFWURIZmZZcHc0VzJIT2hROGZmZVo0WldlV0pvUTdMVis1VVl1U0g4NVJtN3JwVTQ2aysyT0RvVjR3a2hoVmRNTDA4ZDArV1hqUzRlNzVITU5zV0NQNnVLUnVSWmxNMytuQytTTU92Qk0wM3oxWG56c1U2RVk4MjJnakNSQm9Bay9oSFN1UUNnZ200Qkd1Y01XamJzRE9YYlB2VzYvRnQ0Um5oQVdIbjFFQjZHRzJET1IyYUE2UmdLeFk2NnE3WlAzY2ZzY0xwTDhJWXBIdjY0ZitEM2g1MGlDNVBoMklhUmZrYmVOUVova21xaVAwQTBzSmQrNHJkU2lMclJXVitEdW5jbGhWb3paWnRTWnlFVElvK2FKazQrRTYyM1d3UGE1VXo2eVIyeHlTM1RjbFlha1lBVnBVTzdMODg1cXZsa0ZQeTJ1aG4xY3k1dThZSDRkTWhDbUltSlJpdjhGeUdzb2swSWJvc1Z4K2RXSFJOc091dkVjbWZJSnFIckYwaXZEMEVyUktwdkNNWVh6bE0zUjZ6U1ZoMFZuRmo0UUxJS3pNVElYR3BjUDRLSkdrcFpGTTJMOURncGRabW41WUU4OStFa21ZZFA0TCs5N0V3d3YrM1RsNmI4NnEwbGkrSFB6eTZpcEtFV0pqdS8vcVcxZ3ZpSTJKaWc4WGg5MkZlMlY4SW8wM2tQTFpITkMyQ1EyK1A1M3BScW5zK1Bmc0tvdzRvd2dnVXJNWlBHY1g3TlRTUms5dXBYRVNGLzNGdENkOEFMbnZZSlhURnQ4bVJkWjZ0OWJJNXNLQ0l1RVFRL3FFNzhkSjZsWUhrNEswQ1lWS0pxVGJma0JTa2ZRN2x4dVVZY0RYK0JJSk51MWtPOEhNbjBrTW1wNm5ZNjIyZlNwUkd4M0RRY2xCMnphdDV0czJEb3JFelVmV3h3NjNJL29qczVYQ0ttNWp2MzJLQWFxVWEvTU9DY0FuMU1qMCs1TmhoQ2hsWC9DUWlUblZyWDF3RmZmcEI4SElmbTBjWDUxaGJQdXNvb1gxVjdnYTZ6RzRYdW9ZdnMrMHhIQVFqeDdEWVJENW0vUzJFNXVqUDlrNkRPTFZYemp3Unpvazl6bjBlWWVFeXBDUEFqTWdXM20yeUFGMTA3VmNBcWNEaWFBRDFWU05oQ0tMdjA0TWN2cWxCa05Gck5QWCtjZkhidzB6QTZwbE0yR08xcTRpY05GbGlHM3UwZ09mNFV0ek5xZ3VzUUtHK1BTZ3NjZFRUb0dyZXpOcVA1dFpxRnVhZzNKSXJyOFVPZ01VajZVOS8vWWUwK2YzL2hBUXVqRVUrT0pNUDFCMmNsMHNvM1pxZVNjckNoMkg1VFlKOVdVQklSOUtram9STGFQMm1JdGJiSy80QVNsMVB5UElPbStEczVXSUlnL3p0ekVQbDVHMG1FdW5tQXNKdUZmR1dQRENhNEpZeXpoU0NaNFcxTVNJT0JvQUF4RHRiRUdKTlVVN0FQSndzUDlseFUrUlRzZUlpTWI5OWxVQkRWdG5DdGFkNDBrSGdDOTAyY2JaSU5NY1BEUWg5NzhBZnZFY2VBcU1OTm5VZFl1cmFkeDBWRzZTWDRZMStmQ29PMlhTVkhmMU9uRDdMOXhPSm85MTJOdlBscENYUkcxTWovVHJDZ0FKRUNobFhDQ0w2ZjNrRHBlQ3dqVUZtaE5JVkhPQ1pZV2MwOWNENG81WGhOT09GdGpRb0N3M201aTJkbWJ3cXVaaDd5aWxZU2E2Q2lSblFkQzhYSXZQMm81MFVpbnlsZHR4Z0VxUXk5dHRKUlZjbE5aTlNSTzBWU2lFQnlXb3F3WUxjUDVxSzYycDFNc1ZUbkErNG5vZnBWdmRzc0RmSitMZ2c5aHZINUwyUEtEL3RmZWZUdURGUllqdVlpMWpMZVZKaElHUjRUOW51MEMzY2JJUUt3cEN6dCtEZU5FNlEyakFQclRmUG1ONmsremdVS2JEY3VadGlyaHF4Y1I1T0NGdzI5d01xYTJtVXFINlhvOCtpYnYxT0R0L1lKYUN0UVMzVWhMN09DNGYyTDIyTFVaR2JZKzl0NnZnUVUyY0dHUU5uaFZLSG9GWkd1QmJGZ09uSi80RDVISjlzdG9EaytuQTA5QUxsSk9LOWJlVDF1V2FmS2ZOOEtHbXJUdFJNMG56NnJTb3pBUjJOTmhmN3RoQkZvYTlrcTMwQ0g0YnlibFl5YUxJN2N0TURrQlk4dmRFdGxXb3Zwa0ZYVmthemF5d1BXUTJVVnZ1aW51Q0FiL1I4bWwvUzRydzJJTlJrVG9oZTVqTkVsaXo5UDUyWDF3dEFTbkFrZ1lXK3Q3WjlYL3lMcVhnU2RiWHdHdFVnbmtWczlzNmIrSlpGU1cyRm9KazFQOHdoQ1BLLzFzVG4zVkxJMWNtQjlleXZCaXBZMjNNYjUrbWViY0c0UzRQTUtScTQvaUNrUXZ4UDM0UDhlUFk5R3lrczNFRHpkZGx0MWNaT3M1cHp3ODNnNDd1YUFjeTBhYUNoMnY3NkVQWDZSRTdBUTJuRTFOeldiVEQyZk1vUHRhdU5GOGc0TDR5b2tGdjRpQzlUSmlkM09tNjZtdXpTYjc2Z3ZEYVZVOU1VQVFyYjh0QjZLaVJNUUNLZ21ULzc5SUlmSFgvTGpLeWU4bVk3ajZzSGg5R0VIMnlEY3N6RVlTczBFczUyWGxGR2ptM0hIMUJpSm0zVk40Z3RjNnBWSXRJT0V0aXhMNkw2N0RGYkdkbnU0WHdRMWJwN3YwRHpuTk9JSEZiV2o0djlBOFpQdXZCS1pOUnNBVzlVMkVBN0pHWE1ZcTRVc0kwUi9OZmZnSzF4N0tDZ2ppQSsrOXd4OUdSQ0RYY1NkWDZ2R0ZlbGRhR2lyOGt2WTc5ODk0dDc5YlE3aVJzaWhXVVduT1p1M0JxN2xtN2xDdHJoWnB0SzZoRUFZekFKK1FpSjJRT0FockhNNzlKb2JKV2lXZ2VmN0RnT0lZUmhQL25rTUE1cmdJeTVsdG1SWEZYdGtpejZRdzBPWVVkV0F0NXplLzFEUllaVDd1cllSYVUxeE9XUUFkSjhXYzRpSzV4aUpMM244aGNReW5XZFM0UFBUUm9JbE92OGRkdWE1elZaanJMcm1wNGlYVlh2WUY1bmxuTVhXUVdMYmY0aHV5Z2hqZC85RExQc1Zlb2JwcE5kZEp2WHFMYUh4eEpnNEpjSTFCTFYzcEtrWWpWYzRNeU50Q0t6UjlHa1JIbHE5QTQvZVg1NVhsNjUwS1cxaEJsOTBnbXRQN3RLdE03ZUpLbUo5VEpidjBuNkQ3bEg1U0UyVWpRNkhvK0ZvQjE1TzZQQ0RQYXlycXAzYkJ6aHgrVFV6QXY4MVNuamtDNEg3MjIwakg1SWFRSG5nbGJMRGRxd3Z2WUtFbHhPWVoxL1VoeElFOGF1VVBQblFiaHIxeDRlNnhsSGNrb2RIOGc2QU1LeGw3YnoyYkVJMGxXMTRDcTlGb3dBazFQYVJta3dvNmlITFF5SWxmNTNFeW9TRFVyQmV4UmRYL3ZMYTFDdGtZQTdKcnZwT3VrS0x2U2Q2TERUT1pCQlNqYnhkQkd1QktuQ0l2cVplV2VXNWpsWUVYbjAvR0syaHRjTTNpNjV4VGIySWFCVlFlYTZVTTVRdkxMWVhvc1QwdmZ6N2UreTVRZWxFbisyT3lCTnVxSTJKSUZKbUFjK29Dazk5SU1tZXlXb0I2WUZFSzFPSW9sZ1ZJNE9qSHlZNVhkbkdFSmZGY3gydk9EaEFWU2d2T1ZEZUROT1hxb0hweWlSSjZOYjdPZnNZTEh4YTFib25EaG9acW84dTZGWjNiVCtMb28zaWFpZmN1VytMeTdsaytkQTd5bXZoaGt5SmZRdXRKcmcrb0xZUFRNcmpHY2s1VVZSWjg2UkszMlhrVnFNcW9aRk9nakw0YmoxeEM5d0pCdGowMndFMlpsNFVSR3E3WmRtUlBZNmRKS3NLNGtnQTEyY1FqUDlBbjhlRXZvamxJN29qOWFPakhXZjVTNUt3Q0pPM1E1RThzcXRmNWJ0a3JLQU1hakk2eGw0NmNXWWdNYmZzWU5yNXJibVhNUHZQQmV6bkN2OW80VmFOR3NDd2tjWlIvbW9NNWJMZEpiRHYwbVUzdmUwcWpwbzhRVElZamgyVmQ2ajJCWHp4SytCMlZTZXQyZXlmRTR3bDNjRHBsM3FxbjJWRDVJaGw1S2FISlJGdzlBWEMzNTVFWVFLQk1GZERyOGdWWnRPak1GK05PeWhOZGlVaXRBcGJqdUU0eDFTc3FUM3lFL3dyVmRINXcvV1lvUUtleFc2UUpBRWJUSEVhSEluNjFmR0hJMkcxZW96cmhibWZHRUZuMS84TDhIZVNJRmQxWG5JME5wbEhhZkFZR1E3WlF6dVdQUUMzbTIxcTRSVmNtb1pwRGlxR3N6c2J2SmVkUjN1Y3JMMnJ2bFVTcFhqYXVoMFJtelkrTnlkNmZJMHBmdUN0RzNZNmdUN0dZVTZjcDRZcmExYUNXQzZIOEwzZStMOTI5VUhFd3NlZTFEVDdtdjZqd2FGakxXTURDMURFdmdTcFBVTjNoMldDYlJVQU1GR2YxVFNYYmxaOWxmOUFydk1SbUNyaWhBdktPRnVkZ1NCQXYyYjZXS05YQ2tieUZ4ZUJ2RjZHOC9KWUpqM0l4M0pMSkZuU1lXVHkzZVZDbk1ZYjdKcm5neFc1d2NPYjE4bDk2dUVmMThkaUxPaGhUUlZIZG0rMWFWanBGMXdyZFgxb2pnNHh4RmxBQkhqL2VTc3pkdkNUbGd0Zk1zR3FHbmJTV2FVNkVwTDd1YmVKRUlvTUhQMnpPVnA2V3NMRUNrcUpEWWN5ZllyVG1wSy96VXp4VTc1VHZIUXFUc0hxVlR4NXkxNmp4STd5cit6dkk3WVN1WHdhZ0IzVVdOUm0vSTB5aDBpWW9RVDRLaXVQb2FnTEpRcVZtM0lMUnB0ZFFoRXhOSVpLZDl3aCt0V09rbGhkZERDRlFJeDU1VWcvUGdsSkNOVXROMWxNVWI2U21kNkd2L2g1Y0xGdlpqMDUwei9jMU9MaWJzaDVoK1phSDFONjI5dnBmS1d0eDNFVld3SE5rMEZ0RUJyV09PNTdmZW03VTlzWHB3TG5qcFpJSEhRdE50azhVV09WbGVMQmlCOEZ2eDQ0VFRIcXdEUTBuYWpLMDZxYkkyUHMwZS9nSmhvRTJUbE94czV3VGZncFRLT29zL2dsTFp4dGJkUjFMU3prUnMvUmRqOFdMNUNRbnpUblRLQUZFT3ZIWWpnVWZQRFo1SmhZTEI1aTlJQkNObVY2cXRqZUE0M294RVlHTXROUVNyUXFPSllTUUVZYlZWQVVYR2hLT25URFAycm8zSTV4Y0VkbGtHZ0JYQ1NUN24rUVVINTNhbGpydXR5bm5uMzV0UUJIZ3JJTW44VUdMeWZvS29TZGJFelNMeFMyUUgyRXZwQUZ6YTlNNWJ4RVJQY1N5bHpVNVJWL3R5TWFZYjRnRmRQdkRPQTFRSTBMSzRSVUxRVzlzTGgwemtJdjBrNjR1TWc4aTFFRy8zWGxQd0dYeG8yTVc2bjJTdFVkeGxXR1VDbzZreWhzb2ZaY3RGbk5NRk5UQjJHSXNralVBOXlhVEJzNXYwanZrRnIyTlBtMi9JVmJlZ01CNmQvOGNWNGtObVhlT1hpK0RGL3U2Q2NMbzltUitFQWJyRjhZeHZNcDBaVHRIVWJJVUkxb0ltMFhOTExRejIwUnNmbnFoZnRDYzVFZzQ0NktBaW4xTFRPS0c3ME5MNDlyT09QK2hqNjhrZEw1QTJIajBNT2RSN1BSRzBiY0xIWHZWekJvVWJzUm4xcWN2R3IxaWMzVit5eGc0V3picFp3U0JiTjNrQmIyY21TUGlFdjZOcUduZzRpd0RLSjEvaWpKNWxLc1ZBUVhORHV4aFp0Tkc5MFBIQjBFaFJQTlZjeTZ5a3lUT0tZSzBXYjlLRXFheW5RcnRrYzVTdkd5eGNOdkZFZ3VKNmNnSk9RZDY3L2gyaFY1YW44M1M0WDcrTWNaYkdDMG0wbkZUSUswSDFBNXNHMkxiL3JWVnZPVW9vdHBWUmp3T0I1OWZ0WDVieXdYeldBQzVUR1JIdVhCRy9UM21FNXJHSE84eVBhYitUN09tR3A4ejI5MjE4Z0FTTXpzZ0tzUEFlYzF3TzJvVWt4MGQyZU5ZUWZJNHN3eDlIREpCSHRwL2trbDZ2MFR4S3krdEk5b0wwaHZ1ZC9kVElvU3pPczdqNXI2eU5neUVTVGpVYVovSFUzMjhyNWxRVmdxYTRodytUVGtVdWNxVEp0QU01M1RMdHgxcjhlTWU0cHBUWEZFdThuZmVmOG1xNlU1L0V5dWJzdXBhY3BrNUtoTjdCUEdjd0pIYU5HTXBWSnI3VTVDcXNiaFFhcTB5UnB6WWI2dmhLQ1pKQnkwUUFneTgwRDhOQkpWbUc1SUh3L2JQWUFSMncxaGsvMHhsVHI5SnN3bklYQjk1dGU2bGk5RXYvakx5YlU5KzFPcEFCSC9CeGJmckV0MVJWN3dGT3I1cUsveHZ4MkFtT0ZwMEVidjcvdVNoYjIrSkJLMUUyakx6RzRQRHZzK3NEZXdudEtYc3IvUHByUFdlQTRPRWtyek04eXI5K0YvSjlMSTZRNysxVnErUkJ2WkFWanYrV2ZqWFIwNU9pcVVnZHlxbFdoN1VSN1RKUDB0aDZoUHZFblpZVFhCRFBDUUdndTRtaHRBVFBRSDJkSWN4MW5xc3h0M2QwT0x5L0xIY0g2NmJuVC9nUGNBZXVEaDhuUHNUemFGQ1FoNmg2WXZFM09SYkRMcHJQK0QyNlRjWmxuRnRCSGk0MGNzZGJjY0JVeDBYVWEzOHdQdW96cmp6RTR1Z0lXMTBhQlRxS0c5Zk5mNHhBSGR2azVUYkhCb291WjBHY1V5Sjk1MytNTVZ4NjFmTy8xSVlwV2g4djlYN0kxUzlTNE5URXY5VDErV0h6N2NTaTdMamJNWnJ3LzhpTVR2L1ZzekQyWm9aT3FpYnBidnpjQSttTXBSMmZIWmJOV055YU9peno3MEMySVJIT1pvTUYzS3lkSEdPU0dMbHdqcWR1a2NSOVRGV1VsVHFYcUxibjBjZ3RSZmNmSnF0WEZEWGV5dFp2aHBkUi9pOHJvb1Z4RW1UVC9tTzFUbVNKVS9HM2lVNE1COVRlZDQ3UXNQTHlYcll3TUlxQ2tWaGY1STZDdExQQ3llRXlpWmtudkJrdzlSR3gxbFI5c3Q5VXp5NExVSVFKeVoyZFZvTFNMNjAzTkM1M3h6bWgzaUJSNnl4WWZTS213ZmkxREdIQUxUZlRvblNCbTRWeHZpNHpwalpKaXRLTHJpYytpY2ZXMGx6Qjh2NnpaVnpvUHUyNWNHTzlacjlKSTFOamNnanZPalcyNEduWlNQeTAweVZvcnRSeXZZK2p6aitRZldldmx4Yjg3bUFna1pRT0YrZ25vRXNPak0wVFpSRjlOeFNVaXc1bHJEdjN2UlVQZ2xyem96TE4wZDVTaXRkbUlIS0lGL0VPWGxXdGhLcUVHNllCQVhweldCZUFMUWx6SE1VSFkxdWdERFNzeS9FRmMwTVErVXJkZkloOWNQMDVEYjJHOTVVUm8rSlhHNEpsOU1UU2dkUDR6WVBJMkRhWkhQbG1ZWHprakFHODJhaE1uMXdRRVhVaXcydE1LMVJQVk5RVStsV3V0aklYWDhDcU1aMHhVRWsrREw5MGNmQWpuN1pLU1Q5S0hOZWFCaWJoaCtUcjByNTBCRUthdkd0K01aVHRQZnpTWWFRa3VHZGQwR2Rienh6T0dvVGMrTVdZOVF6VlBFMHovQllidGJOYUVMdXdXdmhjZFlUZnlNWVkvelBHU09WTGp3d2oyUHZKQzU2M1hrdTBFWW03bk5oUVUxTGtyNnNCODNHNEdXL3FRKzZ6VDNsbmR0bCtYUkVETGVKVGpsUWdnNXQ1MXAwM3YvTkZPeWNjV0s3b2MyZHB1c3dER0w5bkhqbkdrSXBKOU5zcllZbXU5RnhlcGZBRnRML2NKbW0yRk1kUjdLSEJDNDhvVkFWNXVOWXFYdWJMaG1QRW9vQkhNRmMrWWtmazBCYlhlTmlObmRCdERXb082NHcvVGUrV0l5T1JVNzJUSFNPNEw1QklHdjZ5WU9wcitpR3g3bjNOVDF0czk3VElOVTUyWFJMRktxVDBzTDhXcjNKVktLa3NXQWRTanZidWlWT2l6VU1QM3VCYXhZNGFnYXBzYjd0eUloOVUvVHpwMzJ4Rzg0U1gwNG5iSlpZNGo5ck03TG1iNDJOK093Q3A1ZUNsN2xNbTFDdlM4ZjhQdm9hdkJ0VFNKcnAxKzcrSFN5bWdQS1Jid2RvcCtGSDlFV0k2bSswWDMvS0xTVVhYVEg0N1RtbHlsTExKQ2FFUVpMSXBuL2dPTzNKK1BhcEpMNWtYKzdjakxWTW5lR202WmJvVGtRQUZDU3JBUVdWNFMrU1ZFaE5Kbk1tVVk3UWZ1eFNLYmpyVVlMWVFYeE9HVld2bUh0dlM5ZEtJZ2drY3BLR0hLZWZJd2NpUTMwOHZSRWhuRDVnU0YwZkhYOC9UMWFkK2QySjdpK3VnbE41dFJleVFZOTJOVzFsU1BaNnhqNE01WHk4UGg5NXI0UHVQb1ZsbW5GMjhVaTVqblFqU0twWlFLMi9nK2kybDR6MGNML0VUV1lnTFZXQW1Idk5iUVR5dFoyQkh2ZGNDTnBZQzdiZWIyOUpFS1JDTzJ6OFd4Z2JDWWpoZGZNUUU0R0dFVkliNDE2eElDbmNCT25QeWxyVE91QkM5ZHNrdmRIeUU3TDFxandKalZhMkQ5enhZTmRZZXlreDR1am9oYW4rd1VCcGZNVkhOZmk2UmdPbEVFSjl2a25hWDVhZlMrY0FlL21wWkpHd0NwQjdWWGZhUnVIKzZPcEdrYjZNYk96b2FIMlRoOC9LUWxwS3czS0NPb3NHUW4xMEZ2aHZ1QVJFOVBXd1A5Wjlqa21LUWZCbUFwMnlOYTM3NERPVzd4RTB3SUkzdmNTNmJkNGUwVE13emltVE1iVDZUVUVZamc3YWpobC9tV0FlUXovZEs4VW1xSURxZTYvOUgrUFlpbFY2ZTJVeUJqeHJVYkFxalJyU3R5eWFYd0xRdDBPbmxtMzNmZ2Q0Ull2VTNndjlOejF5cDd0QWNYSXAxR2lPUStOOTA0Vkp3L1k4a2hFeEZYVjdEWXlIV1pkRGExUmY3dmZzTFBLekRjZXF6bEZLMVdINFd2aSsvWXFaZjV3Nmg4KzRINWM4VnY2bXV1K1pUYlUwdzF4SnJUSmtoSkc4RmxoTFJmajJjbGFUMnhPdG5xTnNadkx2N3l1WS84Q1hzQU9yYjRwbUhxSHNqZ0lyS3RxRlI1eTY2aW5pOHNlOHRuNHJSc2ZxNW1ORDVPVmhyYnJwbmlweFR6bjV3K0hsU2RMU0hhWGpZTnRnVlNOMFBmWm5tc2t5TURUVnU5TE1ZMmtxTEl3c2doZ3cxMThTc1dNSFpVWVZiUmlWRjFDNkQ2a1kxbXkzeDdteGRocFlQM3FKbVFqU3ZTZkU3ak5QZnVLRGI4MnhBM25DNXk4SVdQbzQ3QkJFdmtCOHlwcDBWY1pSdElaUjc2b0RaUzdxRFcxUFlpa01wM3hYaWZSYzloWVNzeitaTk5lT2NhWTU1eUdtSDAxbVN6V3YwbmNTWkVranRhTHF6ak1yd0NtZlhHa1EyK0hpTGVPTWhkSGZ3R3BxRnJhM3lkdVVyU2dWMmhWZ25nSjhGaU0wQiszWGpVTm41Q1BFSThZcitkYU5DS1h0c3FzaXF4MUw2UjRZc2txM1NiN2M3amVXU1gxSFhkeHZ4dlZDVGxrTm5UTnBqK0ZhSktoMGJPR3dqaktWMXBCOE1CYzEvc1owUnFzTHFKdDVsN1drSXh0MWtEeGVWQU9tYjFUcTFXa2hIbUVPUDdZQVIreW5ZYzlpaXdYdGZseDJBU2FhZUx1SU0xOEd3SWNrVk5ob0RJa3VnNlhEVGJvY0VhZkZDbkl2TkRKbFY4SksvS0p1anowV3IyWXNEN3ZybHVxSytGN3UyTU42QXgyeTZsWU5TVzNnblpwYVFNZkFOaVlmRk1xSjM1WUQxTGswSVBSRThFTmtsMUlrMEFuVk1mQ0tjNmYraXMxc0t1RVN1d2luUmdhU3JmR2RLRGQxVDFkelg3N1E2K3FyY1d2Z0NjY3d3NytiaURHZFdZcmtvNGw4MDBjZ1pvQy9VaHM2T0huYXB2WGM1QTNRcTF3eVgvMUE4WWRzK2d1eXR0SitGcHU0S01vdFhJYzVjYzNSK2doSDJNMXNydzJZQmRlUzNCMXFYemFseTRNa0wwblFlclJyYWNyOW5LYTdtQUd2QWxiU3Fsc1lvWXh5dkF6cVRVN2YxODNWbVV5a3B1UGVvdVA4S2RhSS9nTGd5bUVJV2JDV2IycDhKQ3NwMHdTeE5iUVBFMk1xM2c1VG41ZlQyZnpaTzlUbE03NHNPQXl2N0NNVjU4aXIyaVpyYlFPRXJ6djJSRUJMa1dhWWdtRytuOUpRaFlaVEhSL0puN1Z2R3RwbkF3MDhuUkowd0Y5SXRNUGZVVVZMeXFORWJJYjhKdTFiVE05cXVPL1k3enEzamRMT1BHTGVqdmMwWkRVeGxuTFpvU1dsWWJEbW5xMXlSREhLb2d5NGJpeTJCaFVDSDQ1Z0Y1aUxwd3pxdEtZYUZzSHp2SjkxSlVaWElNL2dGS3VXRkFqL3pzMlF1bjZYTGNRbXdaSzhIWnlUYzhrTnZ1Wmh1ZFpsYy8zMU9zWVBBT2hMbUY0dVEzeTFXTXZYVXZ4V2lPN3AzSDZWQlVRcUplb1FCSDZ2UzRoZjYraElnMElDbVFIMm5nVmxobG1YbU5OZXQ2dHRMcW5PTFgxVnBnaXZLVnh2aGtoalMyNVNrOXhVNk9NblVZU3VpSWhudFJydXBEamdRWnl3M21yaHRKclJuNXB6TENob3pSOEpUMnEreDVTenVaUllnLzJoUDlCNEs2MFVPVHFvY1d4NWllOWJhZE41WnlzVkZsUXVVUzNXMVpEV1B6RC9ZcS8yRENhRnBKQ29jOE9wMExZTHFodXNMMzQrVk5ZS0tVbGJRek13R3pRUFRta0txaGRxMmlLUG53OHV1Q2l5NDZ5Z0NTOGlWVWEwT293UEVodzNKczJuR0R1NWZHdGJSWkRvU1ZESjlXclZwV0gxZHVUS3U5VmNwK2lzbkw2V2dzTWkwaHV5dUJ1eDBoOEc5d2tDUTFwTzVZK2VKS3Z4dmVKU1B5SXlZSHNuMlhOcTVtRlBuVVFHZDQxakVyczhvQ0x0TFFpVEtuTkE3RW5wSlFNSmN3S3FieWV5Z0NqWjFFOWc2enZ1eTV1Q25SSTVOcG5acjdGTExhOTZYdUdmTzE3SHNoTHlCSjVkOWlSaHl5YVplckJFYldzZ1lYcUd1c1czUVVlc1Q2UXM4UVJSTk40bHB0MHZucDNJckk5anM0Y0NsKzQrZ3lMY2s0cktnSXBCMENMNTcxTnI1emI3VERSYmNkWjZnMExtY2RrWTN3bXBrNGVCeW04LzFOWHdZL1p3dVhKS3RBb3Nid2lMTWt1TGJzSnNVMEx5YWo3a2J0bU5TNlc3aWM0ZFdMK2l4UllWbGtQTzFQMTdrTDRNQU56SzZYb0Z1a1JnRStDK0tnUExxcnFBZmdIV2JsM2twYWZ3eXIzMm1GbkI3VkhVNHlmUkRQZHVoMXUzQVV6WmtkbkZvWlFYS3Fla00ybHI4eW9Lb3VUNXBxdmgyQ256MHFMSFJxVkdRYkMwazdRZDJBSlJSZ3pveUZkTWErQU5KUHFJOUlBY3Z5ejhLWU85ZTgrWEg2MUdONTN5enFuOWdiUDFMQTErVjhBZUpLa2tRbS9LeXZzUzFJbE96VW9nWXdTeTVaWTJPYmdPS3FRc043dGJ4NU5ad1hhdTZLN1BvL0R2M0J6dzV3T2VsNkFSZEpjQ1ZhUzJYRVMwOHUycHBxVVFPSlh2Y3RnTk94YXB4eU5pM2NPWUtRREYrR0hBeE1rL3dRMWpwTEhseldJUndSR1hkUjVnUzVKYm1ZSHQ0dWc5T0tkUk1iYXFjMFBrdVhYME9UODRUbGs5R3FzY3dsSkJwUERiWm9GQlRyaXB2d2hJNmVKZnZZWnk1LzJqUmFKc1JmNzl4cHM1UG9uNHFJNHR4RFplQWxxYzFHT0dhSC9ZN21sc0ZKTHl0WG4yTmZ5QUtIRUxNY08vaDh5SjAxOXJGT2Urbng3ZGtQNnBuQ2ZJbGhzS2cyQWdqd25EWXB2bE1lVDlIa3duMmY3SGkyQUsyYlFKdkhqMUJ2QXUraWYwblQ3K2ROdFpJM1k5T1NuRHZoSVNFMHkxWDAyMm96SVNIQkdyVmI1elBuN0NiUHFtOGsvcEFlTnBqMjdKL2d3MkRUdHJOTXM4a3BlTEpHVlRHNm92bUI2VFNHL1ZVMFdiUnY2QXQ1V3h2WXpKdE9PYVFzdEVDNXBBSVF2M2RPVHRmQU5HMXFBZUMxRjlyYjVuWCtQejNyQnAzYy9DZkZjclNnWjJQSGFjSE9NOWtueks4U09xcWxQRkNvSmN1S3IyWkRMbDBCQzhqL3NCYVVuMTdZbWRPRCtHVUR5NGt1ZDRiVTIvTXNRY0l2eE5uQVM0aW1pTmNpd2w5TzhlQ1RlUlpHWi9WRTg0WHh4ZkQxQkMrNDMrQWYxK2xuUk1xVjBtOWhkeDY4TnFOWU9udyt0R3pvK3ZQdW9hMTRPN2tqY2UxZWFFd2ZsdVhOaGEzMUdIR0Njbk9DbVllZDdwSWQ4ZDdSRSszQ3R0cGk0UkJWcFo3b0F4T1lsS1lCUGJKUFp1NWVEdHFSMThqN1B4ZHVBK3l6ZWhrSXdCTzFpWVJVQXE3VnhrWC9ITUJDd3crMmdzM25URm14V1NQM2RaazZGRU1vRzZpSDRSM0tGL1V0QWk2RVVqRVVGTjhIYjRaalh5RmY2L056VktibkVtd0thbXJ6OGRjdjlPenI5ckZnMG82QjRHalBTRGZTVEpxdHBPa1NNYWtYNE5JeHo1c2Q0dFZKdzBvSis3cXBUdHE4T2ZyLzFUa0wvTlBrQmJLTkdzbEpSd252U0ZNbDFpeFBDMm9QZzFVRGtzaHMxTTVCSlpNbDJqUlp3bU9WNEpMV3ZFaUJBL2NuMDJJUHpLNGw1ZUJKWGF0MDZDcHMrcFhMdW5YcTlLbzVIRFRkK1BVM2dJc3Q2KzJxUXVZQTdVODYyc0h5NEp4ZnR0NWRFTDh6ZlpLam9YMVVtQ2k2UFRBYjhNZG83UHcvaE5QbDVqWnYyTEc5Nm5HeUltRkpTaForMWM0c1Q2TTdNaldqRHpnVHlyMnRjakFMODhrQzJFTlRDSnVNUjl5ZUlXOUt3RExTUlRYYmdoY2N5N2NVTkl3SHlHZEo0U1pmQ2wxbFJKZ1J1dnVuc1FOUjRSUzVUMkVodmp6TW5xZTNrVWtsY0F2N0xCNXByai9IZXBoSm94OEtNa1JRbXNlU2wzQjZZUENyU1puaHNwM215OVN3U1F0MEgzS1hPdEpLU0dkdDFlQ2VoZUg4UkczWTIveTRKaXhJb2VDcWN4RUcvNDVob29oUG1FOFVoR1l1UmJIRWxLbXA2cEs5eHNqZk9GNGdabDRKSmJ6YzNnSUNxdGpEMEY3dy9TSGJwVndIYTgxMWxDKzUyYVZxalJ3UVRYVEF2eHc0STNSbEtQUlplUnp3VGhJajd0Nkh1S2ZEbTdVaTczeWxQWFVZNitMak5GM2JRckxFMmttMlZLUXc4NFByUmlRN0k3WEh5cXNUWW5NdG5ZSDQzYVJFMDJrbXI1b3RFR05vb1luV01aUVRISWNiME9ZTU9qY1JUMzI3TDNwNlhwc1h6NmVTdzlmOXU3N256Qk12L09raFcvZ0F0VTFBTjZ3WmgyVkY4R09UZ3hCbW1qVjZoSzE1T0l4QTZ2ZkZXRzBLSGxLdmpBQW5VRXd0ZDFwV1lXRGM5K3RrTTlHMjJEQzd5emZsVXFKVFNJVWNhSjNtcTZ1ZXEydHUzSmJ3ZXo0eEI0ZWRRdlFGZ1J1K2w1VTk2Y2RtM1Jhb0dyQTZZVk5lK3VEaGJ0cUk1bklXRS8zay9HUGdPTmpoVjlZZW9wYkEzWEl5T0VtZXRwVU9ZQkdDcUR4VDZzdWhtWXJRcjVwTlFnV2VMNXJvNi9qQyt1alJJWDRaZENSTlpvWHV2OGtVVDRYNW5QVU1DamVYb2pzYVBzaEdMVWxXZWtuTm9pWkhXZGtOZ2lFVm50TkZSek5sOXJlQ3llUlVZWTRDQTZhTC9ZVEE0MEs4eDd3aXhFK0FLcGlmSHRqQU1uN2pCL0xyMGFxZ1pzV1lYajk4SVFLUFo4cUhKRTd1RGFNRTJGWFVRRFp1K0czbkdlMFJBaUdlT0hlbXZaRktQTzFUcVNiSmUvRjVsbkE4Y1RETE0xSFVyTGJHc2loSUtEUFRPeTM4cE9EcmdNVFNaKzJra2lmcjQ4dFZBeFNWbmFUT2dwYmloS0czOUppMmxLNThJUFY1LzBpNXRRQzZQbi9rVXlXVnJwTEluVW5IbkJucWhvMGhRaWc0TTFJdHJzSXpmcDZGenhTWHRDYXBDY1BCMTBJN3hRSldVb3BZR3haUnF5bzhFRXFmcWZJZWpFbDUxcTh5SE9saU1uUEZwOC9ZSkVMVG5ILzJ6c2FkNTA0TFoyTEcwTkljR3p1Qm01anlYTWJqMFdubEZtaytGQkVMN0Q3QmhLWHVlVWdBN0NBWEt0YXp1ekF2QXpSQlNCRmVvUE9GbWovOFE1bmErWFUwY0NsVTlOMi81ekhsWmlkN2wyMG9uc05ETnRNQXlBanBEZnpxMG9iYm9INy9pZ1RIMDBBdHRuYnJYTHpSSjVhS2xnL3QzUllPNUZjWEpPNVpIOERXVWYvOFByTFF4TkVqQ1ZnelRUMURNVFV1aGV6bXJIVkQ3L1dYaVlQR2E2SGZWZ0p2WlM3S1A5d2JaR1lFZS95RC9MYWZNTlZ5akRaOURZNmdFS2lYWVhjNUxaUXBJMU5XWVY0cElJa2xOVDRtZHdFNG8yNHNmaHBEeFNMekIySnVPU203dk51a1RhSzRVM3ZiVGNNMys3c2RVckRPUklNZ253TUVGVVFmelNadXFFUVFBTm5teGZCYmJrVHdXM0h1YmJ5SnZSUEJtNkNtb0w1R1ZsOERzSFhvV29raWIzcVNUSE9rOEljWlVnVjhkZEQ2aS9URVJGSzlnalRHMThMOGRqdSs0eDgxVkdpY05Kb0tGcXY5ekNSTUdVRU9WVkNaa0FwdTBrOEovVG82Sm5BbzI4UXoxclpCd2s3R0xuRUVEcEZEQS94ckx4NEVjS1pGN29ZSlhCTGN3NFpJRVAxNW5icVBnZm9ueHE2ZGZsZm5OQktrZkJMZWNoa1d3cWZFK3dVZnltbW1WS1BLRVV2c3NyZU5QdVdGUXZxODh3Y1ROQnlCdGM0M0ViZWFOYi9OUlpya2Z0elRBbHgxc05QdllsaU1qNWowTDk3VkVHNitVZTF4YUtWYVZLblV6NzkzVTBocFpmRVFBWVpKS1hlb0dSbkxNWWtvMDNWVGtnRU5zVkoyWkw5WHlpd0dUS2JYSkZieEJqdGVmcFVSUE9WSkU4bi9wNnBQZmhqeTdWWjFDSU1CYjFtald4RUIyWVgreVB4L29qQWY0Sk4xcS9rZ1h0R2xRbWtDZW9STm1oUlFEWmlGcUJsZDUzMkEzaDdWUGUybURaa0tXSmxlV2d1WEg1bDgzNFZJcXkyRXJTUElkZ1Zva1B0aEdxalJZejRnYzlPVVp1cjR2NWk4NFViRUREVU9vRm5lVW53Y1dld1RMVDU4VUJIZ1dPY1NLK2pKZWhpYWc2aHJTSXZxdG1OVjduUm5TL2lOcWl4Sk1JdWtpR01VVTFyMkhFYlVFSVBpdDhFM1VlVVdxRE1OY0hSYjBDWE9PdGRZUFhNYmtZZHY2d2IwWUx0clU3dDR2R1dMUFcyWVhobWR0YkhtQWw0S3dRYUZaaGxsdnc2OHovVG5VN25DRzFUdGtDQzdna0FCVVBCRTZJODBmbkRrM3kyLzk4c2oxc3dqTUR1Z1VjR2NFeVVxeTdBZ1JLcythcEpiNHE4MXNySFNGd0JXZElYdlhDVTZtNkVub0JvSFM3NjBIT1dsTlNnbXNpSzl0OGJsMVpWdlRQM3RZSkhXNW9ndk90eFk2aCtkRFlPajQ1WkFNYXY1RFd2WWlIMEZwblhRR3dZUGt4ZU9KSERLdG03OGhXMTFrMDVKbDBtK242RGoyT3BjWEQ3aExBOHVZMEk4MjVIOVJ0Qnk4NEVCMVplZExab0hVQVF3elBLakw5NiswbU5ncmJCcW16VDVqOXdCTDJPWlhiNlFGRFcxSHZReGRxTno1ZG9HZ0ZuRGFJdys1VGV4REFGWWxSdG1Qc3BaampSWXhCWFVNRUFReEZpcFpTRVYwMEE5TUZRaGJJdTNxNS9ITmxNUVpWOUZHaTltcS9hZm9UMldLY25TWEZ6aEpmbmlZWmI1Snpkc3cxSnFEWTVoNTZpSnoya2VhaEQvUTdTZVVDclJCUHB6a1BMTU93L3hMOGdvNWdoWTlkampGa0ROZEJiSjNMN1p4Q2JyenNZS2UzMXM3Z1QvNW1Iay9sSU9hejR1ZEFZY0U5Rkk5TjF0d1JUSjlXTWdXMlMyZkNzZUxqbTNlZGxEeWdFR1UxTi92QTJscG1sbWFON0J6d0toYjRmQncwNW9RUkY1Y3h3RjAweXRMMEdpblJQTjFJcXBBVkdoa1BpZ2J0VEJpeTh6RWJwcXQ1N2V6cUZtZFVXTEh2aVJ0NDlOdnc2OFNQalZTL3pjY2dvcHZUU05Ca1JKOUJiaHRFZGpDNUd3cGkxOVVBV2RjUEl3SjVqSGpNZnR1VWVHMFV5bFVBV3hFdy9HK21uMTREUmZCUTRZNG9PQkhvVndFdkN3TmpPVnZVRXJUOStZaElsaUIvRWxKMi9iZjlBcEcreVFMUUMyTE4wME1FeE5Xa3JtWjBYSjVPODFoSHdkMDJkTGhTNGx6QUtCdWJDMS9kYitaTXpyUDNXL3IxN3VWd2lpL2UrS01Kb2JublN3OTA0WDYvWGt4VURuNVVieFRVZ3hWYTI5Q0ZlaFhiNitpT2RxWnJQd21UbW94WkQrSGMwN3BCblBJczhRMjd0MUFrSWlORTNSSndhY2MrbkNkak1PZWNxL0tGZHYzSTVNbDBtUGhQUUZyU1d5SW1kTXhSclZWSlZhQlJ4Vlc2Slk1M244eXdJZmVhRk40RVo1UWpTN1p3bStzSThKY3NGSm9BbTFCb0kzUDg0Z1F4ZGhiU3ZHT3cydjNCMWMzUE55U0srVFR5VW5BUGpKdHpZTjBtcFBZUmRQT0lEZHp4MVN3VSsvUTZnUUpaTTA0aU1wS25ncThEUGd2U3FicWQxRm9hdnRoWXVUYUk5d2tqSE5MRUlQdWhtdjdBcjhyaCszdkxOSFlUWmtWRmRuY2luSHhoaUZWYzdzQ1BZNko0TGFLRmJxOEw0OWY5M1pRd3BwSWh2NnF6SUl0MG94a0pkTzZ1bThCYk1DVXpRcDUvY0g0c3pGRDRUUjMyOEtXR1RkR1Ayd3N3MVNuSXNiMHErUUtreWdEOGI5bVlwZmZQRzUxUjd1NGZha1BjUjNLZyt6Z1ZOc2hoTW16VUVDL3gvNCs1L1lZdXBzR2owMnFtVVo2MnlsUkJ6am00eFpBZnU3UUxBR3hheHpZdzVwNW94bks3Z2lKREcydC9VTWsyemg3Sk55a3lOTGc0NGZrYUlJd21waFZzM3hBUG02cVNOL2RtZlN5S0krVWVqUDlBZVQ1RisrandKL0EvaHJsc0hSK3pNbGdyemRMQ1NRSjVYSFdHZkhNTzVWckdXc2pWQzhVRk5nazRmZ2tvSXpxU3gxS3oxOFRkd3Z5WXhTZStUZWw0LzZsK1JYTFp5bVB0eVlWbUxmaUFKOU52djR5QkhSN1VKU2FZREQxaUNUWFUvdHBvcWlMTXRqTVltQk5tc29hTlc0RWtxaGMzVlp3MnVuemJhVjZjRmdscVVDcU9IMDZ0bWZ2RW1hbG4weDJVUzlTU1M1UENDemhyelkxMXFWdml0T1YrSkhld1NlM21LbDN1SjJ0eGc2emt6V1hrU1lSWFZJelNCMU81TXBWZmpQZFFsOExWdVZ5R3ViZUI0dXZ2TFhCUi9zYVkzWFlmNDFnUUtrVkQzQTVsZldVNldYZTlMUVVLUEFnNVYxZExDWGZYSE5ZNWlaNE1tM3JDdHVkMXR4eHdPNHV6Q0pVb2VGTHRNaDlFQSsrZUVaQ0N3OVNEem9DakhpWlllOGZzc0dLRWc5MVFuZkd4aFdGN3FaVWdTZDN6N3lDVUJWa1BYL0V3aGNYRGFPN0VUbDV2eEliUXBaRm1yQUJxOVYvMWxNT0tlZC9MRWtsc3VRR0xKZ004c2F5VzQvRytrYkQ1SzczYi9Ka0thREVRcE4zZVRIaVBZaHJiNHYxNEdSUE11N3F1WW1ReE1JSEpEeHNvbzV6b0x0WHdrQnM5TFhqK2IrZWZ5a1JVc2ZuMCtVYitKQUlEeW9pM0cvbXhCSHZUOGhrN0h4RUlyU1I1UzBFUm5rR3RGYnJTWHlkVjlIbjNXdUxDcHhQVFpCeCtRVVU0QW9CdnJsakt2ek5vQXRzNGNvYXhQMDhsNEljbEpjZW94ejkxQit1dDliRHBBUUoyNDI2K1dzOUplb0U4ZmY3QzkxbndRbHpaa0w3a0REQlBBTEJRMlEwZ2pNSXhsS1ArT2JFOU9rNW0zZE1mWnZ3ZmRhZ1VTV1RSNitGazNhMlZ3Q2RCRXhhTEpWNk1Sa1BPNnBhTVJDUUQ2UEhaRHdLMXVnZHhBTGFlNzJHb0VMT3paamR2akd2YjZnMVU3dHhxK2E2Nk1iaTBsWWE3RmlIQXNSRjM0WmFGL0lNYTE2TGZpaUNhODhSbnhNZEtiNTZobzNwSXk4c0hUalZDRGlYMlh6WGJ1NnN4MWZsb0c0cGt4dk1YVGZwMXNCQ3psQ1g0ampLZndNaHJqV3crTXBCU2YxaFpVMTBIZU1JbHBZeHRNclJNY0NzSTMxd0RvcmpIMitTY0JNeXU0Vi9oRUMwZEg3SmpESCtxSmZMd3NzbHpkMjdBVEFrNE0yYnkyc3hJU3NxZk9mT2hUMUJSdzdCUDhNS3RoMW13bzZFUDFTMFJWMXNFY2JaeThlVDE4RWExVVRLb2hlQ0VIZVp4bE1uR0dUYmlKaXJ4OEw3c3EybXk1cTlDMGtTdEcycHA5a2ZZQmxFRGpDMzBNbUFTUHR0N0pPL0MzejlxSHRWck5KZVVRYlR6S3ZyVzVPb2VReEZ0WXYzeXRBNW9JN0VnWmNWa0RpYW54cGt3TStRSlBRTHBpTlprRUpUWnYyZ3JqZVVHWC9iRTlUaXpEaXJ3dUpKbFdLVGk1aGo5Z0g2YmlxakQ5S0w0TWFuN3M5alRjNDIyekNaMnZTTGwrNG4xNGIzMy9oMUdNOTBHMlRPU0kwVXJKSFZRaHBPYVZhMHNUNnZITkplQ3ZvV1duVlBwdXVZYndqbEY2U2I3c0JLYUVERTkxbXdsMlAyV1ZOWjN5V2Z4MDZZZk1XaC92SHJrYmlpcmRibGFnU0N0dUxMMHlvVUs1QTFsNUlpQVBtaG5hTnZ0RG10VlJieXlOWExIVXJsZzlUZnl6ZmxiZFJzeXZFM29OVTJ3c1l3dDAxbnJ5VHVlbTBQQzgrUGlXemVFNXJnRFVuV29mVS9mWWVTVHB2czViMXdSL1dvWjlwOWx5NlNlSkZwUUM3TVMxSFhuQmgrQ0pYOXdaK0d2UnU4Zzh3dFU2R29peTRITEdIaTUvUmJEK3BaQzBhZmdlTHk0Qll5L3R1RWZ3UUpWRHlVWGFhLzl6a1pKR2NhcnFtSTQ5TFptZFlVNlpwUU9jeGxlbzl6VjQ0SkV0anFUOEducHBIM240SmRmOVRGRStjeXBkL01QRk9LUzZ6TkFiZUwwNlo2TEdrOU9KL3dtSFRPVS85QVVYMlA1aHlqdHVsV2FreTlsOHJ5N0ZJZWxyMFVSV082RDFXTnBQcm5Nd3g5T25lak9IUC9LK0R1aXNWZkZMRG1JTjJIQmtZV2lOWFVoYzZEcW5wSHFhZGtaVE1WV3RpTUk4aDJRZzByZkZQamdjdHBYV3FVeVNFVmcrTC9hL2NWS0ZyTjREbHphTkdrdElQRE1JcWdibFF2dTU2NDhmOWFyWTcya3dUOGo5di9XVWMweU50ZUU0a2t4akdMLzFiYkcvRVlaaEluNU9keGpUR2NpcmViNC8xcVROLzVZVGRTQkkzRks4ZHRIUk9hL0g5eHhDdnRQWHB0a29BUFlYVWNGZ1pEcmdkQUFtajZqZHFiVy9kQzh0ZmZmbGtUWlVoQmNGWm84MnRVQ04rZnN6a1lFNFRDTXp2UFFTN21RNUI3YjF6U2FSRWw2M2dHZXZ3L1hyVnU4eFlYT3J3dTRXdi9PQ2N0Y2R2ZUM4ME1jOGxMMU9yUHE0RmdKVWgrRmJIejdoNkVjTVExaFg2Z1liajY1dno2cGk5Z3Iya3pYM1MrUklZOXNrNFlXYURxZHJqdE52b2JCbnhhZ0NObW1uRDRYeDRsTklJbmlDUU01dUJjK0xwWnc1aEZ5N3EwaG5DdmgyeVpwQ3Y1a0NPSDdWaXFySkY0ZDluY3Uvb1hzRUhZU1BCQTg3MlRWRWpveFJ1VWVqcHZGQXNjMGhCVEpjejNITmdLNnVKOTljSFFyRDJPV2xZTERranBFemM0d1NjdFRJZ2RnMUVoUVFlc1g0MmhJQm9yL0RtRHNMU29WZ1E4cDI2YUovWUMvMittNWt0QXpUTWg5bFo5cDJ5RytpcGpQNmJZSGFQNEd2OUtpSXcvaXc0SlF1NGZJcGZkSG91WHhZYzNJaUhCWmZOWnlQaEJHdWtHOU1hNVhKVHlwTGg4eGg3RFVkd09kaURjeEx3a1FVN3FXSnFqTDFRVTZkWnZTM1ZXQ3NDNkpRNGVmY2xmdkNGTi8ySWhQQUdTcDlFczNoNWRISDNYRmFnMEZHUzEzRTU0SC9wOExwRDgxcXRPUSsxbkVPRGs3Y2RsalQ2aStHSFV6bnFuZnI0dS9TYStGRkhueUZndTB6TGFYQ1NnVTZMOXMvOXZzM0RNR2Q4bE1Dc0ViTW1uOXhMOTdRNGNhNXZ6emIreEFwaXpWVTdGcENudnRZMkduVFNGUDYwa1l2Rk1meCtJSlJ3V1k3eVNWM3MrSHY1R3NYQmhMNkhoVHJKZXFVdGJmR25mT1UyV2VUemtSbG1aNW8yYVg2TkZHN09EUkd4dmxlSm5GUjErWFJ6Smt1bDlaLzh6UTdNdURzMUZkSFVPd3FQcXhjNVo1ajZIM2NYNHZIdTV1d0VpSWgwUW9wUmVjNmFEVjUyN0Y1ekJzRUE5MEcxM3piWGdOMkNnK2d1SythYnMycjhxbFVpRXhPUVVGQ3ZGZ3hjWWNLTXg3VEdaZWJDZGhvL3VwRDgyNU94TTBOK2p6N1pQSmVMZnJIL0U4R1Q2UkRVWVduK1hNUURlczZQVlpqN3Q2dmpQUWNiRWhTSVBFZHVlSFI0bG14WFFQME8zWTloYmV5STcyQ0V6L0xQT084a1krWGh4V0VRZmRLRkI3R0k2c1RvVzRxTHFGTHJaTklOMCtUUitJYWoxZkxLM3d1b2RLOUphRi93MFczekkxczR0aElKUzBIRm1kVFFzNC81K0ZlMzVvTDhBSWlsSVdwSGJrMk9reE5KQUpkRU5weGlFOVlwaHVNblkwYjNaWHlkZnhhc1ZRbjhXL0Q2aWFUN1ZiTlBkR0FnL2taNUJlaS93ZGE2N1FLY01ISEpXams2TVBpN2Eyb0NWUmtBV2xzci9KdjVvZWwyTnM1R3Nhc0pJZU8zT1d1dFlMM3lWYnowdzFiTE1IUjRUK1gzbXNOL2ZreWNEb1p3WWZRVkRJYXJIVTZ1dGV6U29tZERjZEhSbUxxN1BnSDIremtPVVdWaVFQcVJUbFhqODF5cnVvL2ljSDFnNnFWdDlEc1c3TS93RUZDSWZCQjJQNGZ2VWU3YWNwS3ZwaHJ1MmJhT1poaWNqOW5CWUtRUTl1VGx6Z01CdmJXUnFUdUQ3bGZKK0FJYnNKNEF5cjh6OG14dE9EMjc0bVg3aE9qUDdmRTJta2x0ZHhzME1uUitjMzdPUXdmcHJuRmZGb2duZi8xTzcvUTFpM0s4VXdWbHBzUDhVbXYzcVhBZTZFRnRNSG1wUEtDVWVWWEhveVVZMEd6cnZXTkpqUk9CZVpXNUFFdjUyQjlWdlpSOGtFaGhrVXhTRkRkTGlVRGhrcVZPcU5acGdWdXFEREhnbSt5aE1hMjl0dFlIaktUZ0hNY2ttd0x0ZzU0UkR2LzhHQTNIOVBKZk8rRVBFYkt2M3BsTEZQNlY2UUFNMjVRcWIvMFdVTVY5TFR6VmVoUlduOG4xZUpQMEFkcUpEYXJTM0RjUmd6bGErRnkrVncvTlhNZjlUTmdSS0pyUVJxdVl1cWVGY084WmhLT05oQmtwdW9Nd2lOaC9BSzJSQkNObzJmMjFWbmdhUHFEQk02NGVMWGtGemhLREdyZ0VLSDA0V1Y1Y3R1aHp4S29WRTdqQnZOQU0zQ1N2dnZ3My9SK3FScys2VkpGcE93OGZXRXE1aTg1MWo3b0tLWGtFcURHQnU2ZENLVUd5QTUvQVd6QTdjbWMvV0dMV2NSUzNiS2ZsdFY5MmhNc1EvWkJTRlZadk1mT2dZNndmU3hVa3FOcG9ML2kvdWJrdERUejljOVQ2bmQ0aEdzQnp3dkxPRXJMWnB1OWtqT3JPcFVlK01GRzJsTCtBcmliWFd6bmpxZzBYRjQwUHlQZTBwaFk0d0ZHVW9YNy9GN3N3a1QwL0tHc2N4cHp6d0hJR3F6Skh1a1VKeWlzOEpuRU5FbllLNDBldE5QTU5MbkprakZSWmlCNG10aEIxNnJiYS9oTTdBYWwxalkvTXZNdnVTbFNIVFNkRVRiYU9kTWRXdzhkd2FhQ3J1cmg0cVFaNzNYN0VzL3gxMWJ0YVBuMFdNa2dZWmdYQTA5M1U4VUhUMk5qU2xJQStOcXl6UmpjZk9rUGJlWm8ybi8wNXBPTjVWNGlJWGNXTUgwSmNFUmZDTURHejRaa2tWVHZROHpZTnhqMEZneXIzbHVURllkMEd3NThQRllKYWl5cm5ONnBrWVhvVURzQy94WHJ1MWhYanlsdW1kWnM3b1ZPWTBEZWhXeWdua0p3UHFZcFdLQ3RZTnp5eEZySEtYRXFXOWdqZ3p2cnkwdHg0eEtteHJrYWF1NE1iUnRqN29GRkp5SWNRMWVjZ3I4ZHNtWEZZZlNGcks1OTYrOExaZWc5L2pWT0hBVGZyZDF5N3puRU5QNUNGWTdXVEd6RUdFUWxUL2crS3dYaHVKa3dWMWsyZ2M5eGdxay9SRldmek55TzhHbVlVQ1BTNXZwOTRrcGpTY05rM3hrYjExQlFVL2V6V0pyOUVHdEpDdHMvdkpqUHBWR0UxaGVERWlBZnE1elVVL3k3eHdRcXk3cXEvdjFjVFJ6eFF0SnhaeW13ZVBDUVIwMHAwUGI5NmZyK05XTS93aHFzMGkwZGZOSVd6S2NnUS80WjlCWTVIbTFUK3RvZUI5dWdtdDh6OFd2TGRSZXFZcHJ0dWs0T2hoTUo1aFVteThIN3FHa0ZkeER4clgwcDUzKzBieTBlSjFCTUlzMDExNG1NYkUzdGNxc0NTWVlwNUVDQ0IzY0tMemZoaU1qa1RWMlBDenV0czVsVE9INkt5ZzNibnF1cVB4ZEgyWnFaNHk3UUtuOUlYbjE3eFZpTHR4RTZ3TWZYMFVQQUlIQ3c3M0huTnRuY1M3UFZxdzRkOEdnY21rTExhL0JRVDdQazU2OGhQZ0l0dGF0RTI4MmU0ZmwxT2tyNmdGVW9kR21yNTltcHJRbEp6TGhJTlladURZa2RpbFZqdW53SnVPVFR1WWFaalU2NGF3dHRiMDlLZmVxdGJjREsrdGdQUmJSQUNsaUwxWm5DemcrUlh6Z1hsenZFbTlWWXNPMWxGYVdsbjJXWXUyNFBJZzZhVk1ZTGdtSnNmbHp4NTB1Yll4dXBWSmtqWDl4SExESks5QmZkT29vYnYxMk1RMUhsOGpodndBU0xzRGVGS2JDY2UyTDZnbzJVeUcyM3pWeXRNeDAyVUhBeXdMdXVreGZCRk9PdWlxQS8zV093TkZGY3gvdDhPZVlnM2FOTGgrWXpvV25sZ2haRkZ5RHZKWEo5U040Qy9TVnQycThPZUhJelFVUm02ZUNVTC9TUVl1blVLV1E3MDZhRkxONVlNMGIwRVA4YXE5Nm45ZjRNYWhFblh5b0FzeTZvYVl3bW9zVVUrT2RaYWgvWnRXR2pNblBpelZWVmV4Y3p0QVB4VC9GTjFVRmUvSVZNaGYrR1NiTGdQTnRUbWxmUlZKNVR6SjZPRGQrZ21EQ0ZaYTlEYXRqS0YxTmlDOEtxTFNvZ2VKNkRyMG9YSWN0V3FXK1BsemFVaFA5c05MbDFSNkROOTcrN3NFSzJ0S3g5VENqS0Y4Q2ZlRHpma0tnejY0eCs0em1DQnIxbW1YK1VwZFZSSVVLVGRnbUk4Q2J2TW1oVncyVWJYaFQvS2NMQkxFRE1vK0ZTVDcyOTQ5NUJlNEdVaVpzUGdlTU5WekcrRXlPZEd6ZE5iRU4vcUlRbmt0ZGQySWY1QzJtc2NndmxvcHFrcGtBUnVRNWwybStFbFk5QkttSGU4TmFCMVdlUTAzSlZnMTdSa0o3TW5peUlJcUYwc3c3Mmd4YzJsL3JVTDFuUlEzZW10NVBNZkx3cHJiMld2ZXlUckxVeHp5OXBLMnZZN1FKVXQxRHp0TDRVUkpFeTl2VFBUYlpjTlNLc3lEekx1R2FJYTlJSFBtZjdiU244MHVyS3F1S0F3UWgrMVcwWTUwczFraUFhOVZyRzB2VFVoWWh3R0FnV3paU2hVd1FFWGdJSWxKS1BPbVZWeEdoNzdmRTdxemFsKzB5dzFWaGpUd2x6eDAzZ2kvZm50c2pEbHo1Qm5yRWFBdUc5cGxRa0RlYjJYbmwyT0k4eTg5d2prcGgxZDN3a2hZZVpsOVhGMHU4UmNkYVMrUHJRekkyUmt5Zjg2ZSswZDZLK1d0RXpRZ2ZnL3BHOXA0Tm00akw2RjZwZzZzUW54S3E3MDZBVHp4N2ZRUnNIekRwSzN4MnAwWXBJNTVMeUt3OWF3UzNhSG8rOU1yMmRMNkpNL3YzclRpTDJjRkRuQlduMnVxVnczN3BEMEtmNElkdmlqYWt2RmxDdzBYaUJFdEZvZHhFSGF4ZzBSS1Y0TmxUNVBzenJ4aXZYR2UrSC9PV010eEtwVExpWWIwV0o1N2RPOGtOdlBRbE1uRmtsTzJiRFlmQWppZUFKbW45MVN1aU1weXVueVpCanl4cDFoOElvdmVpTWkxbVBPWW93MHFGQjFPb3RNRzIrejZibmg2V0p5cXdhSXR0TnpaMFQreTYzRWovdTIzczlRRnpNdTRHS0RtV3dFelAxZE92Slc3S0RwaGVVNXZMZkpjaEU3THNJdEF6TTF4QWZGVVRRZFVMeXQxSWtrcGs4U25KZ0dxbkJuM0J1TXljeFRNMW5vbFZmN2NWbnNwUnBNTEtEMXFzR204ZUp4RFJPZjVkWSt5QjlKQ29zc3Q4dUVxcVhIaHYzTWdHN291UDJuNDBBeXBHYWdpNzZTRXhVclpoOEtvU2pqRVFEc2VuMTNrd3hKNzNaMG9ob2xXSnIzMEszTlhnalVYMUxZUHM3N2Y3WU9jNXNwZUZ2cko5dHRLYnJQSFhxVmRkYTM3WUNsTEt0NVA0VVZDTzRsdmtQUUpFcHdvOG5FQWh2OHAzN0R4MUxucThtclV0NjN4YlJ5Y2g4dlZzYU52SERFUHYvNFBhd1JvZEZkMjgzTlNYdTFROVh6WVNlb3JpcENCdnp1UG5WOUFOTVlrZUd6Z3JodnNDY29jL3l0ZU1uWFJCcWJYczY5ejVzcUFMUEVodkpXUFhKNUcvMGM3Y1B5alVVS2ZBeVVHR1diZFk4TzZFais4OGp1WTZhR2RWeW41VTRYbW0xZjZMWVhtcHRBNDFtQjdhajhISEExSEo5OWR6YW95S01nR01tbkg1OUEyUlg1WjhvQWF4YVR5Q2ZkbmR3U3pEV2NVOFdvRjFZMDJ2bmVjSFA4YXY3VVVNUUFkRXZuTjVxNVBmcUU4K21GT1JBMkdibmpFNWE1NHphZEhFUXF0cGxscXB6dy91WHBKdGVKV0FRUzJJU2JBV2tScHViM1EzUVZEYkRYNkVBaDRVTi80cGF0RnRtRDhTTnRrNTU2bVR5a0tKMjI1YnhpbmcxNEZkN3JnNk42MGVLbGZOMVhhZUcvU0dLR0o5NERVR3d2b2txcDFiQ1NpNC9Zd0dxRDRMc05UOFVvTGdpWXlQQzN2OG9PQnBocHlaMDJRNlIvZ2dJRCtVb3JKcjMyUE1ZYVpTZjJuQkZDMFBhdEkrRFM0UXBXTHgwcElMOU9JYzdJM3VJVm00RE00UGR4RHJtOWc2UWd0MG02LzJlZGQ1cWJ1aEREOU5vakM4cmpocWFqWlY0dFlhSDhKZWdCb08zRjZDRHlkWEh2c0JmSEoyV2oyUkZlcnlaQ0g3eVBobFdHejhqVEVHd21RR1MwTGRyOFdtRmVHYU5Dc21zN0t0T0szdmprL0xscmJPV0hlMkRTNjBETHpIZkZVYm5JUmNwRUF6VGJ0RGxiZzhlMlpQSDlMTTJ2V29iWmVPZkwyc3hGYThJSjJyR1dBR2FVUXkyTVBJcUJsdm5PbS94N2xqODZoQjU5LzI2WHIwRUswamZ0SHMvcy9QR3g0akVxaUU3cGoxY0FwYVdzR3piMjFOZ2ZkSkVvd1FUSWh5OU9qNVdEV3pvNWNIZUNoSkF2WHluMlhDOGtjc0RUTWJIWUJuNmltU3Z5c3YvWFdpZkc4eC80L1hOcTlJdUxkQ1M0MVIvcVJlaTYvRkZDUTdqL0V2YUFsNFBtVXFEdVUvMTVDd3BiUDFtYlNpTUltdkkyZERBdis2TnQ0dXFzR2R6Y2J1MVJtemlucWZvdE82RXVIalZPeENrQVh0MkZrMWlHZmJRS0llVGsrKzFia1hNRlRwOFdrQWt0dXB2OGJvNXdRZmpxVWtJYjYwL2JvQ0k0WndIZzlid0dieVpLTEFRR0o5N0V1L0t3aGdTbFNidTQwUzJUUnpyNUVzV1pUN2lpR0VIQlpsVzZPUVpqcGVSVUJYTS95RTkxcGlDMnQrSXBZT2pDSVZEaFUrOGlzcXpKZDR5ZXhXNzBXQUc4RzZTMFBvVlBiMHZzWVF3K0NkR1lRb2g3b001c3VsSlZ4aDBEcGx6M3pDY0tkZUZodEU0Mk9zN1FaQkpjbExNOGRUOFdnUVZYbGh2NzRKTlBXUnZrNlpNbW8zREhQakpnSTBMcnlSaDlkREdkcDhwOHRvSk0zbUZBZ2xqRnN3dExHNk1kY3pOMm83bnJQRWo3bHkzczZLaUs5d3cvbmRGVHFVQmNLbTROdE0rZEhRMzZlT0ZFcWMvRm51UExPbUF3emtwYVBQZzJ2QUsxZURyNzRnajJJZm9ZWndkclJVdFErbVFqUjI2dFdiVnBIbC9La25vTXNRSDdQNXhTcTNlVkxnYTNDSFZjck5kWlRhN2lrL2V1bFhVazNDUi9WZ1RxRDZZaFBhZTN5S1N1ZktxL1dxN3F5Z04yUC96WW0rdXNQTklzaGFVWTRvRnBHVW96bUdDY25mUjRQNVBzeFFEZ1dBVGpHN1g1Vm5PMHNWUVVCUGZDTEM2UGgvMmRDRGhKbkM1YWxKbllaOTN5SHVzTDBRR0piRUFLcGFQVnQ2WVNIYW9MWE4rZllyY3lzbjN2V0MzQ3VZRENheC9qTVRHMTA3WUZ6Zlk2b0tMUVdIUERqWktXeEZrbGJQSlhkT2VwVE12V3R0aEEwNSs3RTFHUk5lN0lxRithSTVtTzVnQ0VCWXgyYktyR2RWbmlzcmJ0N0lwZTlRbGpvWjIyQmRoY0oyL3RTUVJhYjZ0dG5YekU2VVVrVTVYKzM3NTVYbVVzcVcxWUFNMnZXR1A3dVNhbThFZDd4SVZkVFVCTlJwMDAyNUdkcytoWGZKamRQZzRsejZGb0hNZTdoQTgxWjlSTVZ1T2pJU25ZVWtlcWwwaUUvbEIvR1VuK0Y0aHNaRlJmbjVXQjdHZzU2TFBiVzZMdDBRMWdkRzBVbHNWekxJQXg1RkwzNFVESUd6MHRQQVptL2lxVVkrY3g3UHM0b1d4QVYyTTRYQkdWc0RsZjVTNU5pL3pURUQ4MGp4cm1Ia1hjbXU4ZC9vdjgvaW9qWEdWdjNFYVlKcjNIOFFobkdLeHZSa1pIT2dHYThuUzc4NkJVb2pYMzJaNDRDcmg4OVhTZlRGc2FDRXhRd29NRFlzVUo1dFhxdlNDR05ISlFyVDdUMmVCbDIrb1hxdTl6M1N3NmVIcC9ONGVSZVdmQURxZVlwc0F1cmRxaWxiOW13NDRGbXpmdG9YYUtybXhhUms2ZWhpZVZNU2lDamhhTEszNzYwWm5CR3NrS3ZjNGU1b3dGNTdQU29BTGU4YTQ3NW9HZFNZK2hoaXIwTlJRaEJoTXZEWFUxM1NBMEVESmo0Y2xFZ0hWc0ZsT2lCNThUWU9GbFVUeDVQK04yUVZQNHB6dXF2TDdxRjNJbmF1U284dXBSLzl3M2hDWml5MlptLzJzUGVRK1p2YVBJa2FJajF6dUhSVDI0WnRTbDdSTm9yVHdkbHM1ZUlYRjg3bXpkdzJ0Q3RCOExoNnE1RzdNci9laGpadlNuZ1oxd2VmVU95SmpHanEyWFYyQXozelh5eWx3WG8wcjhuYUNWTkNTd3Q0TEtGN1N0NUlJMGV0N1ZHTVo0ajY1WWsvS3A4KzdkZVQ0eld4VHZiV2VhKzNUcllmYkZ1UVg1ZXVKRjE5TThBN09pT0hwWFBNY2txbmJDMXdJNVQvWkY1YmhrYkFhWDlsaThTZk9IVTBSS1hNVnRoQXRBUzZMTFhXcEQ3TzN0Q005ZlFNUjYraVA5VDkrNFJGZWNSWjVqMHlHNjBWQkhXYzJCSVBzZEdlTjMvSk9aUUpubnlVbzI0UVBxOGlRaHlsL3RUdjhsLzZKNDVDWmlwY1R6ZGFWS0Rlem56Y3JjekJuTDljTzBiV2hmc01sSFFoQi9maUFHazNnVm5VQ2JwT2cwZ2VKZ3FrdUFaa0FReU5CTUE3MjdveVhqSFdkaEZLRzB2Wnp5SGhQdVFXUjBHRkVDUU1zNHpRQmt3dWdWVTc5cmNzOVZaYkZGNXB3T01vRTFPR0JRZEM4ZFdZNktQYTM0LzRCaFpIRzVmUndRM3QxamJUMlNEa091Szh1djFrZmt3akNNTGxPUE1wdWsyS2cyVkZFSC9YMWEvbXpQRGdZZ01KNWJGS0sxYW13SXdzT21PUTdBTXRyMWFLRGVzSWJkekZlOHZrQ2ZYamNvRSt6NUNOQzhyZXhtZXl1ME12Rms0ODFtT3kzazU4czRTOVBCc3lWc0duRGpKbGZZSHpPbVIxM2t1TTVZbnJRMWtVMHdrZWF4VzlTRG0xdEw0TWU3OGIrR3IxUzJVeExqZDdCQ1AvdkpJK290bmxHM0tIUGxDaEFYU2xsMElaVWJudXFUd1FaVWRWZE5yVEcvZUFVcU9mNENoVCs1dWMwb2xTVzhRazNHY0lRRXhwVHZmaTJmUXR3bU8reUg2ZVR3V1JEa3BSVXNxTzFLTm5XRzNMU2hqa3RMZkZDRktpMk9laTdicU9PeHdELzdwcCs2SjNLMGJXL2M1OXd0Wnd6WllrdldDeDh5ZUpmNVNxcVJMUFNRZFVRbWF0d2VNSTVXN1RkV1ovT1JpVE5LVUtaVGtneE9iNzdudGJtcEVIbzRjYVc1Q25BekFCeGdDNVBRdWxhNXJNZXE0aVg4Uk9DZ2RsNFlYSW4yZWltelRNdlBPcEtZTlJTRTM5bjI4UGxERDcrSlArUUt0Z0NzdCtmbzQ4Vm9XcU84QmUxQ25PbStRaFMwUEZNenh5dHpXNFdjMG1rbmt2cmdCcnJWOXBUS1BBTDBZLzFNUHVYMDYvRHlhbXdVTzVNQnE4SS9XRldKTXhpWXdxREhSUzJSaXhDTjh3U2Z1WUxiR3EyajFmYUY3Y0xVajBDUU9aWHVTT0t6aGFYc2YvUWVmQTBSaUNDQ25XWG5iSWpQcVozUWlZckpMSkFZY1NtTDNEaHJMZm5uZU5FTWt4MnlrajJIaFhpanQxRzBIWW8zajFJMk9Qdk1RMDBsZzRNTktNMzl6YnVvdStFUjNuMWxUU2QrUVNWTUJ1c3NyTnhvQ0RMTE1oVUxlM3VBUmdSaUlqMk12THRBajdJek4zRDI5ZCtJZUNNM1NQT09NOEw1cTdzSC9McTk5OXh6V0MvZjRFMi9zREs4eTJIY1g5SDQvMU1OQTVXRE56c1QzYUNuYlZrMDZFUFJWUFc1WnN0SFdZamhGUjRKRjVuMGhvZVJpZ3pUdU1jZUFRNE5EeVpTVy8xLzJWU0ZSL2ZiUVlxaFNHMlNaUHV3c2VCZmNYUHhUSGxmYWhlckk5SU8rV3RQVzcrZk5BTmxEbDE2RnJyeFhFNmhmNjV6QVlGMkVBTmViWmY2ZWpjcE1SWUkrck5qMk00dFlnaDVSZ2NVSE45OWNLMWxibno4eUJ5eE96VnVzZG9lL0tDakkweXVLZGcrOTVYd2FTbDJkZWFoeVVlUDhlelZJYTNGYm0vY2tMeEwrN0JoRkRrTjNvR0FNeDd6VElad2VrRFZZb1p0d0taeFE1Zmdpek45MHRMc01FUERWZmVjUlVCM2hGRW96OFFVQVhGeVFjdEVOc09hTXNYYkNLVWpNVmNvS3QvZm9sR0xlaVoyL2RhMUxHOGllQ0drb0p2NVYzdUV0ZkV6ZkJWZWlidElxcEZWYXlKdm1JK0ZQb1dYVjJPc0hWVVlxN0NaY1RVL2lMZ3RNQVh2WllDWmFKWXVodEZsWDJGWGFqUEhIN1F5bDg2VHlRZWZzSmdibWd6Z3RnSUQ4S2lpdi9pQkJ3MXRhTkZkSGxpQ2xlNThKUTlDZ3cwTEk2TDNtNVF0dkgzSFNaSGU4bnFmYTlXSS9xZHJsRXoreUVPRXhiR3BNVTZZSE1wNDlYMjcwV3VsQXlBamZQVzArK2U4d2VGSTVpU0l4K3ZWcmU0alFJVmtNdkYwNmZMemlUUFBzQzNEYk93R2dIMzVrT0E0eWZkMUhMZS8vRnN0aVJob09CUlY3S0d6K0tRK1pIK1A2SlBaeXBDOThVUFI5dmNub2tSL3NIamh6V1pXWTJZM0ZBbjF1R3h5aGMydUkxTUhuWTJNci9mT3dWTjhoL0RMMTRqUktRa2xXdHl1Wjc5eXV6Zmpndmxrb0dLeDJNOUlocmVHZjlRVUNRME5VRi9aSmlnR25ucHcwdE0rTzdmZHd2eVU0emR5dHFmdTRjdG9DcGVUVTFoYVRXaWVsRjFsTWtBTXFlZDVYSG1CRlo3WVgvUTdZdU94TmM2UjlJLzhYZmwwbkF5VTI1QmVkMXJtV2lwSkVnMHV2Ri9SVTZQaFcxR01LNTEwQi9VWGJFUEdTL2xleU42eVduR0xDQUtiN2d6cXJkaHlwUTlHVXI0RE9BKzRETE9WNkNYVzgvQjhmSCttVW9OT0JLNlQ5ay9WSVRmSkNudDVaNXEwRTdja0l1bEpIWG1IU1pEeXFiemJ2SW1TOUJuV1JkRE0rVkg2dElwdTRqeTN0K0JLUXdSRFdBU3pUVTZsb2t3S1lWbHhyWjRhRGxmVEZub2lROFU5eGtUbkozY0QvS01GbXFWWmJQRXByLzFrUGxVZUh6eTFIS244VDVxRVVMb2ZQbkY3a3E0UjZwWXpIMnoyYnZMZlRSTHZZWEthWG12OEp3WDdXL1lqb2NrNmZTeGtyemRXbUF5b0RIdXJBb0pBcnQ5Q0dvQmpHYTFVdGUxeDNLdGpKVy9BdHI1NUs4M2FXYythUkE1RjFqL2VpVmMvbFlzbTN1SXNVUUhLQmhBcEphQ0JpMkszL0thWUo1UVRUUy93ZjdOQVhKTVEvMUdYcU1keXZaODRtcU5JRXNoVkJwWjE3dzAxbWdDZ3JnWXNQWTBORlM0WGpzTmF3OExnOGJtMW1sS1gzZ2NTdEZDWUdTcHgzdGtqZFdGZWFQdGVlaE9GVXlya3hnZ2lBd0hwWkdYVmNIVW9rRVNsSmQ0dmhINTY2eS9veGx4bGEwWktOZTdoN1gxa0dCTWc3RC9FYTM1d3ZKdEtzYTFTUzU1b1VWUGZLQjdNSEl4alB4NDRic2xoZ2VGRzJDSHkyam1QanRIMUcyZEpGbmxjS3FYRnk2Z2FzSlhJSGJhZFhIc1NsUU5ycHZwRVBpYUkyTGxKNy93b0lDSUdBeXZCMWx1NlZNUGVCTWsvdWFuRFJFaERPYksvVnEwUFZDYys2TldQbGU3R1pGT1I4R0Q1eC9FbjlaS2RUWUJTU0RPTUVLemsxdWVseEJ2V3JHQ0xnWUN0QzNwVGwycDJkMnJ6b0x6U1JSMlc2cWZmaXpqTUF6NkJqWkgxOTJaWEhwWFE2MGVHUGVJSVI3QjFKeVU5akorem5CRDY1NWFDSGIzcmFVd3hzYlN2VEdPNEswSHFtM1d0ckJpbGNGZ3B2SzhaY29Md09ESnFXNFp3b0Q2V1VPdUtEOE4rT1dZbDg2UVdLbE5xbC9xOVlwbWhUcDNhOUM5M1I3clhucFplNnFhc1AvcnNWcHRMSzUwUWJKVW5RZWFtZ0htLzBiR0c4aCszUzZvVFF3SXVlYVJjSGwrK2hBOWI2TGNkdTJqaDdCUEtKWE9QMElqaTY1SnNXby82aENVc3R1YXo5OStuL1NrdTJDbTdCcEpWVTZoK3p2OXNjTGJDcVVPdTg2U2NHcnRpeERLSDBuL2NucVFSU3hIdythaURGYk9CdVBLZXBzRU9xK09NYWZpU0I0aEJMbkNCc2hvU1g4VkJpMzNwcHZyQ1VYdVpSZjZTM2oyWFpmRHpIeVBZWXNTU2FBUE5pTFNaUkdLaURVaXFlQUNKSzJSQ2J5TzUyQld1ZzJLZExTbzJKOUJpejV0bW5QaWNmR2hsSmt3UkJwUHVDMU1XWGIxanZhMkxTK24yb0dFZnpaR0xNaU4zaHdURUMyU2dzQXFzOXZMSFVnMHJWbnhPdmNsaEpJMjdxQ20rSFZTTE5nS1dDWEROL2hoenpJS3BmTWgyL1V1VlZhdFFSeWRKc3lRbWJoaTdUTG4zdHFqZVpjc1kvaEJYRlpHNmp4SUNxOUs5R0dDeFFLUDh3OVd2R0ZVTyt2K2xoRHl5SUVveGJTZWRmR05kMDJqelB5WUJUeFdvZjVrVklLSDl1Qi85YlIxZEJnR3puZmJ5bm1TS0lsSWVIZTBnTEg1YnlvMkFLaVhPaFNBZFFFSDdyYmc2V20xNUFBK0ZJK2RXbkVEZFB0MVU5QWcxaHhCTGFCUXJLcEdlNDdCRjg3OWpadzdHY1I3TE8zaDBDMVRoV21pcUZsTW1kK0RwdVhRbWFSWGpNSjlqTHZramdESGpvVTNNYmlwU2I5VkM0SkpBWUJMcE82NkhzWVpXN3NjOVhsMWozdWJXRTgxSkd6NUt5emYzK1BxemthbmFZSGNUTzVxS0hnWmx0d2NHTWM3RkJJQ3NucTBQY3ZyOTU4K2UxZFR6V0twZmFoeUlzWlVJVXFoZ3NXQ1lqRGJYVlFHYWxybTJCSFZ6OXJrKzlvRTNUNkozcWJwaHVBQ0luRk5JSjNNY1EvTmJmYjhkQ3ZMVnY0WmFrVmN6d1ExZzhLbFBzc0dhMGp6VHVxSk15ejJYWnRoWUJQalV6VmRvVlpPYVc1VUFXMDN1Z3hxcnc5Yll2djZoR203ZFBHRlFjREhyUjV3bEZEbmlDUG5hT1lxQldLSUNPWkEwT0w5Y0JkL2lPOVkwQVZZemFqYzlxT21YR1ZjVmd5cUVOZ0JIZENka0Y4bU80TGFUaEdWa3NSdkpNLzJ4Nnp2cWRreWNrU0ZnWkg4bXRVZWZwRHZWcU11UnY1VmJtc044VjZNeTRXKzNBWGp4cGZjSlNJYlR3Njc0TVNZdWRRbDY5d1REL0p1TUhMWkhmQm9nQmYvSndoZmh1dS9GNC9jRmRSNUJGT0J3V3ZCNXZQcFNEZU1XYWhTdHlGa1BXay9UNitPc0Z6a3djejY4bHlKKzVXTkpFbHowdjRJUk15aGFmZkhTcEl1YjI5dFY1aUtJSmFDME1yYXVobll6eWUwVTJwYkppYUE3NllqVzJGZHBkcXVyQlRxcXdLL2RENStMYzdaV0lqN0dGS3JFMytBdVpNVGh2K1VQUUp5dTcxZ3JjNzNKQS9wbHU0NkIreTJKb1dIRVpwRC93aUNJS1Y2bHJGVXBEQzUvUWRrODRyNzMvY3BWOUxZMmsxcUxvNmhqemNtMnQ4djhkWGhoUlpIN2NoNFBSZ2FKWGxpbEVZUlFrczNUZ1JmQjMzckYxNG0wcllrUTlTLzVqZlo4SDI1SzR4Ukt0OWc1NlgrNFJpTGVQRm9mRHhMOStrZFRFcG94VjQ4MlV5S2FkMzd6Wjl4MmE3OHkwUWtQWlFsTXFXdjBWc0g5eGwvOFU4NFJOdTNxdmtxM2xmT0g2VFNhY2IvSzM5UzRZK291Wm56bFFZRUxheXFjQlB3THlSejg4OFMydWpXVEUya3VVVHU1a0NGSmczWDRFSWo0NGI2SFF1b01KV0l3YkxacEUrTUFUbVQrenRlRkNUT0I5WUI1ZWFKYWh4am1DWEI1VDZBMnhjbGZ2UE5abkgzdUVtOU5vNHZvbFhBQXJ6dUlzOHdEeDE2Nm80MktGZW5NbC9lU29yaHpBYU1qQWprU1ZKbVFmd2lhdXhXS2JNV3dFVDJiQXl0KzhxcmxJVEtxZGsyQ3hEYTVsdmhBRmYyZHY5alZ5UnAxQU85cmVmKzZHaEtHYkpYa2NndXRsRVMrMWdld010blE5bHNzV1dlNzcvMGlza1I0djJPTVVwUEhDbzRheXNOSS8xdHRwaXFxY3MvdHM2TVN5d3V6REhILzdaeWVlMlVteUg3dDhqMWhScVVVY3M5MmQ3YXRBVE5rS20wY29oamRCaDl4ZWpvVTh6THk1RnZlWlNXeHZ1U3pqQ0dOODRkVDJZQitWUFZBNzcwSnVqeERZeElnR3ZKZkVnVXkzZDEvdjZ4UXpLSmtEOWtXSi9HZjRuU3p2ei96eG5TdHM2ZlpCL1JJYlVCbkZ4MDRYcUZoRE5PTWwyb2hRUDJTTG96TlZyZTQ0RVZ1Ris4ZGxTQmJIaU8ybkNCRnRkTVFNeDVMRU5oYjJhSUx2dmlNbTZ3ZTBaZTU2bXRhUGlPbTQ0VTVPYWpQbytLQ1IydFNrVFhzWHZpKzh1TGJSMllydVlIaG1xSVZiK2dGeWJ0ckFqTHdoTCtpejRLNGtkM3Y1aGxjLzkrbWpaazNFQXFTdElmQjJDdTYxc0dLejFqazFRQXZ6NUhqbDlwQUQwZDl3dUhtN3FMcUllQlQxUzJiZlN4STZyNVlpS3I1azJwQ2VoZHFoSUx4RVd4QjlZcktneG93cC9OODBNWmg2L3dWTUppd2NxY2d0ZDVDL1Vuam1CNVpjVkp0bGVQa0EzRGlLVm5UVElMZ2VCaFlkVE9jVU1DSmVnZHhIVEQvVnU5RnlkNEhFb2JLTVlDZi9aR1FQdmV0aEhCQU5xUUV0UzN1a3hZRXJjM3IxVGtRWUp6bUFzdkN3VlFmK2lvYVdaUGhRWHM2eVVocENtUDA5Y1k3cVkza09GaUp6TTlHeCsxMFNIVEpUUXNYOThNQS9UTDNxaDVtOXVhK25rdGxGL2hNK3VNNThWalBSaitaYzRTNjgrVEZ0RjhiclZuQTJWc3BVM2d1LzJuK2tMeDZ2UktkY1JVcEorMnBOd2dFUDdPdHJUaTNRRXI3U3J5SFpFcGFKRzJlYjNsRmJDVVE2ekIvWTE5MUt3djh2QTQvU1o5ZUhvOExDdGIzRGY4T0htUTJnbEtTOUJSbkU2a2FGVFpmdUw1Q3VLV1NXZDhEMWZlRG1rTWdMMzV1TEx0NEIvS25Bd3hOakRRYWlGMnBkUlRNNFVsekVJcWJGYk5NREFHNFRLSzkrOWQxZG1TY0lWWXVNRnRkczJuL2IxdDNaYUdNL2k2aUJueHdqeG1mNDB3SjdoNDlFWEJSQnA4bmVSNmFhQStqY0djc2hudVpnUUZwSzEvRDZOb0xPT3RwaUl2MW9yWUtUK0phQnpWUTNFa2hoUHQ1cG1DTVJ1TFoweVNMTjN3dThacXpDR1ozV3JXNGpleWFUeXVuREhJc0twbmxMM1dKcnJJeVFzemtpRVlEckVhQzlVNVI1ZDV4Zk96dm9PNG54ckRSbUdpT0YvSEpGM29tN2FJYXdUWExTNDRwemVVT1JCd3I1RlRURHpKeVdZRnVwQ0pqSWo2ckVRMlllR2FSNVIwSzlkT1NrREsrUnBod1cxckEwODhUWUNNL2FzTkplVTgvTE9DWG0wZWZiV3dYUHNhRGc4WTFtWW45RVVYQVY4RkhpY2R5cVFNNXp1aUZtR09IQ29pZkNaQ1NHM2FybmprSCtxUUxjbUh1WGl4U1dtWXNHZi9GZjhxdmZtL081cnRjUU55WGJpZGtPM1FSUGpPOEl2NXZ4QWR6UU5PQWNwbHcwQzd0TWJYd3pKS1N2Sk5RcWpJWGc0YU9XaTBjbzNpYkRoMjByc2c0dEZpTjQ2RlVHTFgrdlIrUW10N2p3VHpLdlc1bUd6SytGaXQxTUtDeG1jNWZSUVpQV0JjMC9jbXJ5UWhmZTJJMWdDR3ZUL1BLU2FiZ2F4ZjREMERrTkNKbS9WT3NOUkMxUlNyUlBCY0dmb0RmcVlsWWg1REQxQWhuWTI0LzBwQUNzVUFiUlpBeEVBVzFXTlh6SXNrMGNyTzVQUDRYN0JHWGdtOFQrWHZmN1lHMDRWakE0Y1pHc2JsY1VkSUJBaE0wWFdidFBaemJRK3llMmtyb3pIZFE2bDVLVFdHSUwrblpNRWR0WkJlWmgvRUFId2hKU0JzUlRCY0g3WURrRlcrbEw5M01JM0tpZFFVQmdhRVF2V0pqNitDU0NvMlV6MGRVVW9KZVhNdk1vSzlDc1dyY0FGMklhaVEyUm83OHNZMTQ2VE1Pa21SS1RUcXU5Yk1SS1d0RE52OFVRaVMxa2pXcHZCcm5UemVnaEJKUTl2NWl4OUJmekJ4ZkpHQ3h5Sy85am5aZTN1RVNZalVhTUFEL05ONlVxcUJLSFg4VE1VUGJKL3pDRFpyOUtZbjlDbGE1RlJGdHY5RFlEV25nSG9kTVQ5ZGN5TXMxbG1FZkdacTNhekM4SnhIdXU0Q0NRYlZqZ0pybDZtTzlxdG9Rd2NsQTh5Uk1YUnhzODdPditCSWFpSjJ3M0pFTEFVRGJRb3FxN0JXNnpIWm5seHBZSWtDOG45QW5iNldOOEtIbkhUOXgyVG4zNjcrc1NLSjFxU3JKSUl5b3o0MFdSQ1g2WGVQaG82Q29wVlZYMExkdlZiZDRwTG5FL3BzZ2l1ajJOMWVENHFDYlU3SkRoaHZVYkRKcFhLU0o5M3gzdENBT09xeHpVRHYrQzREejU0bzMrV3hoZ2I3dE1qUnphbDduUE5DQmlCSXVhNGprb096Q09TRVZCTWxIcGxsdVJSR1ZPK0dGQ2p5NlFDejh0K0dVQ3hKajdpQUt0ZG03SHFyc2EwSXorekVsamNNNys5WXU5WmNyN1dGM09TQmVXeFFwMXJvVzM0YmNlbUw3dlgxb25ZRjFjeVNyVSt4d0FqaE1zNEtJTTZzczhreHo2WlpIYTBFbEQyaHFYY05HNEd0UWQyZUNWZkErUEdVaXp6TFA4WlE1MDZ2dG14NmMrM3FuRVpaWFozYjM2QzVzamVrTWx5WVNFVTZWVDFmNzljU0ZNUG5aQ01FNk1zWmd4YWNoTEZpckZuWFZIMEsrYmtNcy91NzU4bWJPSjZwZTJvWTBIQ2RLbEphbXZUcTJjWHgrUndLTXorZFIvbFlQMjNhRUkxbi9iZWpJM20vekd4eTN4SzJ1RVdWMCtmc3oyWDJYaWlmTDNWWkJMSmk1STNLVkRrT0I5ZGE3b0M4Q1M3QnZyYzlSbVlXUWc3bDZ4VHp6WXVvK2QxTkhlNXNMdUVWSTh6V29ONDh5U0JDSFdaTi8zelFkdUdxWW9EVEN3Rit1bE9GK3MzMW8wbU4zL0ZBVkU1a3dvOTc4M2tMNW5ZT0hqWWFVd1VGZ3BqdEhxdGRXa1hNck9DTktkS1dERnp6djV2ZVd2NkEvYXBuUER5WUhMOVZTZjlEYVVRejNCaUs3elVVUWcwOVRJVWdLQ1RVNTAxVkMwbjlicFJmbS91ZlhMYTc1T2F4L0lGUWRIalV0WEFvUXAzaFJOcHBGOHFsc1JNWEc4a3A4QWNOUk5WMGNsb3lsTi9MRWhLc1JtNGs5TG9xcDRlYnE2WnFQT0NpNkNRZEdKTHJsZmhFVjNTc1FCdE9Cdk5FZ1pWTGtyT29XS0prcUVndDFMOFNDaDJ5STlqTFNxM1JSTUNqbzhsVTJxdGovcUpqenJpQWI5VGRIa2NoMTBPV1dIVy8xYWdreHVRV0dNcldHYURqcURvYjd3bGdFMlo1c2tlK3VTeHQ1NDRyUlIvcEVjT2tRWHNLaklSN1U1Z3kxT1FteVZGNGlxelhnczBSNUhXK1RPTGRyVTlrREFGdFR6MDBGYVlMWHhNakV5dW9IcE90T2d4elYza25NQnhoaGpFeVdKRy9DOTJia3VscFZJd0FjODBxRUt0OEkwa2ZvZUVlMEdOcHJtREUwRGtGTWI1cDE3Y0tQcmNmUE9qRW4zMTZnMzZ2Zy9WK0RpMWNlWEZXcTJubFpwQ1NvVUNDaElqNHFuSWo0Zlo1elJuS1BGUHN0K3VaUTJDQmI3cEJ3MHNHR0FVT2w0YkxLWjJuVUhSbHk4SktsVzlvZkRDM2pyMkZjaHNlQWdRTDkveVJxSEVyMHR4OGkwR2hQZXU1UXdGbjlEV25wbU1uZGliS3RxS3hVdkprWEdRVW9sd3hra0JyNUNPVzlROWx1UGx2c2hrWFdKSGRKcnk1R3Z6VUQ2TXU1dk92d1NDOENDODdpSTlZK1hQNHVvSWtJWWdvYXA1WXhQRm1xS1dSUFN1N05vRk8vV2Y0Q2lYM0RjUXNjTDJYVXZWQmZjNUJFS2ozSG14K2VOUVloTGNpK05wSUJIMU9nSXhuaGthdlRYWWlQNkwrOGh4Zkt0Zi9LWFZqM3lobWtYYVpkc3ptZDh4bzBqSUE0RWtRN3pycUhLY0VyRjRMQ0t4S0Y0YXBFZjVYanJ0S0prcU1ZWUlSRGdldWRkZURjL0ErSzFtQ3RpSy9zdjQ4VVpYWGdlVEVaYmJrS3k4UkFtcUowZTZheDRsb2J0c3BqYVJqYkpmQ1RVZ0xVbi9WNkxhdDBKUHdzR3lsWDJrcmxEMUxZbERLREkybnROQUZxL25kNzZyb3N3TVoycHVvME16RmlETEZ3aHhIc2F5UXRGckpkZjhkWDZXemI0V20yZHFyVDFaMG1rOXJiT1lGb01IY3F2SExuYWVUckMwNktaWURzQlNrcEhNeFdVS2JxOUY4OVNpT2NwaE9YWFQ3bUtTSkJUdXl5ZVp0ekJTRU5nbk45SURnYWZXQ2E1QkREL0FaUVdSVUlXRUVwZElsUjBBTzNhbUZvQnF6RkZtNU1FOE5IZTdSSXZldEg2S1ZBWlZTcXdOYnplaTlMSkU1eFJkaE0vRVBXN3pTVDMzeTExV3VSN01xTXJNNTh5eEUxOWhqQ2syUmlueXpPNzgrR0FLWGhtV0FBVDl0djU1aWNhUDRBYlVJTHlGQVJuQWppNjViVnNqdkkyb3RVTzBOcVRNeG8zbUtXczNXdGFTR2daK0JGR2pKMDlmMFlZOG04Y2Z5TFZSOExrQnRpeEF5L3FXQUdSS3lBeFREU3JDV0ZTY0ZvUUUwNDd3RXdGVndiblprTFdDdnc5ZkNTQ1B5Z2tYU3I1blVZYVBDaDl6cHkvZllGR2dNWVFZNHNTS3JOVGQzU1BSQmZmaW1kV2dnejlyeDBmRitNRTVBMTJmK3VOSE81SWRVUDNrOFZrWGNjczcxbGpqVHdQS2lnQzA3QWxJT3hOb3VGakZiTWd4VU8yTmNiYkxiMlMvQ2ZGemZDeHBTenZvY0J2OFdJVHllWG9WZGlpcEtnRVZkcTZKelVDZVhqRHlqcDkxWjVpVVhRaC9wY2ZQQWJVQkl4SGlOek9oaW5HQ21UajY5ZDNnRmFEZXVmTHpuY1dEQXF5bHd2TkV4Y0hqdFVuUlo0T0tyaVRVSnh0bzlLNWF3ZTlWSmQwYU1hamgrUktRS1liZkQvVmtoVHg5ell3K2RTN0VXdk0vMW5hV0g4eVZHUTlGZlZvWTN6TWxJSkFOeEFwZjVYZXJMRHp4TDNhSU13NE15WlpXWEtzL2NpaTFBUnVsS2hIVFMvZE15MURPMVhGcWVqcU11U0k2QzFvZ0ozOEFGTEwrd2JkRzgvd0Y3Ry83cHFYREhoZDVkV21tSmQxeFNKVVYrSGV3Ni9XRklCK3JPSHFpdG0zZjJwWDVWc0FlUUUvMWRTLzhoK0JEOTBYRWVQR1dDUnVSYXU2UUp1YWZiYk1WQkN6Y1BWdkExT3YwL0hLZXFUMitobUNEWTBtT2YwR1ZNMmh4bnNNZU9kZEx4eDl4V01oRFRGUDZoakNDbDVxbEVGUndieVpxRVp6dktGS0xzKzdybURod1BIbjYwRkxPVjBrVHczZXVNUGdTN0pjakxMcFRNVENIbllBMW5FVkFuZThSY0ZpQ1FzSGxVVjZBdlBxQ21RMzJjNnVsWGh6R3FleVk3dEdtV0ZCYWdkUjFhTlA3aGtBWWp0UDMxeXRwd3gxZVozOXp2YVNpeWRRSEJWdWxjY2tuN1ZXTXAvSFRjWWRReHBKaUNZRFZxbXhOYkR0b05RL04wWXBVdVpjRSszL042eElXM20vRGRock4zSUFYZkxZUVg1T2VrQlpjc2VmUENNSWtNWWNKalZxU0VlR3ZHRUVWMi9HWDdFMitCa2c1ZE1iQ3luRDdXaVFHSE9LUVJHZm1MOTZyanlBYWxMMFpVbDBVbUVWU1QxanNWYzFrd1VseS9nUSsyMkFIeENtU1BCOXFqNkJjZzFIWTY0bC9kaFVMQ1krVlRoaGt0bVFINkpQY1RJeUtiRHp6TFUybU9PS2Y4U3ZwZHJIMGRQZmwvdk5ITVpjMmFqZVNwVkhXWk16SWljVkR6Q2I2eHlQV25USFFxNTd2T3Z4RUMrM3ZzTmJjYVNUS1ZNMzhONFVNSm50TmNtdkRUSWVMZTZQSWNGNGE4NmQwYktrM3Q1d0xwMXRmWHM3SjBDU0VkNEdDbEtDSVorRis1aVg1RG9iT2hYcWJTN3JyYmdLSTF6d3JPVmZqSGw2dEhHNkNMQWNmN2ljUHVrMDRTWTFtUXJvNnJoQ1RIVW1jV0YxUzdDWTBUNHp0b2ZHVFRQVVFPUVJPODRsVkRlb2ovNEhKTXY4U1NaVFIxYVVMZTZwM2sxSGd6UmExSndwWVl6RUk4dVc2N1lnYitsZ3Y5bHRqWFlXcjRWRUNUTTlNZXg5eFJWT3AvNkxHU3luR1BFcFFqbFVrNWh4Nk5icjB0SmRQY2VmWkhheFRDUG05Y0NIZFVldiswYllDRHQrdzdnVVlxZmRVV3AvbERFRk1WRU1JbWRLRlIxN054by9Bak5jcTdGNzJhSTRGRWEwWllDbDJrWEtBS2xvaGdIQzRVbWV5Rlc5bFZqTUVWY3RjK2pyNFByeU9WZkIyWmdmQ3hIQXo2QlpueUVHV1YrUU51Mk9nYUlYTUdyV2JmQmlYcWhqLzBUMyt3THRqeGxMd3FmMXhsa1BLZDFTVUdpMXFGQ0FXZGpCZWVTSDB4WXNZaGwzTDE2WSszd2llVWMyUGEzNUVHZXM1YksyNUxUMDVnL1NKUHIwNnFkU0k2ek5FWU9XSGp4L0J0UkxUZ1NGVHN4QzloSlpDeDBWVzAwU2h4eittbDllUXZTMzRNZE84YkpLWmlzbGFKakM0KzFpWmFqbVhRVlU1QTBSeTF3bytXbldrZ0dsNHRwSmZOSGdwdzFsaTlwR1lSckorOWNDK1ZrYWhCQXU1UStiY0dHRlQvS3BrV3dsVFVMZy9naG5VQ09ISVgyaFdvZnpMTkdFZHZqY1F2MnprZ1ptUldlc1oyak4xYjVrcDFCN1c1MlM4bDZHY2dOckNxUUdHOFJlQXBxVlIxN2pjTTU1U1hTYVk4TjRiUnpFZSttSG5TNGtxWHdCdXJ0RlpyYXBnUUNzajZxSTExZTlqbUJTZkpmMzAxek91VFhqM0NyMDlTYjFZZ1A5ODNPNzh1NHhVSXhnMmNTdFJZWlRzN2VvYTlUVXJGVkhwQWZBL25BNC95NGJLQ2RyTlhLMXhKZFBOMXVqVE1TQVBmVCtTQW9PTkIxNGtFMGp5cjNPQlpESWdTMFNtRHpuR2k2cW01Y01CVnRab3RPYm5RNWtmalJ4dkh5V1lLbUQ1MzZYQVNWUGRtTzd5a0c4YkI5amRnV0RSTVZwaTRob1NJUUtLTVkzajlJbmQxc3ZDTHNITmZwM08wTW5JeHF1RXlWa3Q0UTNremtZWERyUWtTdE5kNEgwYTNOYXRHUGdheUx5NXVrM3AvYVhnWkRnaVUwaFRiUnd5OFBBR1EreGR5d0ZRc0tTYURvMythS1d0MkhhSkFEbm9XWnRIdkZFdnB5T2o0QmFkUXZUVkhnMkdESjlSMTlmVzdJZW9scHpCQlgyaEJ4RzY5a0w5dXF3R1FMSFV2M2E4WUVNVkptdFVOS1YydUJ5eGI4a0FSMG8zVVVlc1BtZUU0aEsxM1RqeHNvOUdremVtelo2VkVOWHhDTlZoazBIOUlCaXp6elJYNm9vOHZTOGc1SW9odzJmUkIxKzFHUzUrdmUwOUhIZjF5QXM3bFpJSkpKVXdmRG5lZzFvZWVWaGQzWndzMUlqa3N4c1dVTk51dmpyOHFHNXllT054UG55QnJJQXFWZGloNnlwWVd2UTljZzVPZjVxVmYzWUhWRjhxa3Y1c09JNGVmbnlGVGRXY0FlM1ZzdVMxM2xxNE5qSjBxanNnUmMrVGQxT3BnZFdSK095NUpLMC8xRUt4QnJweUxqQnlrZzg0NVI0WU1PcER0SzJ0WXlXNTloTmlzazNoQ2ZQS0E3MjRuakhaelMxWlUxYXhxK0JWdzVtdmhiYVB5ZlZtMmpTMkduTWo2SEQveFQ4dkFqUmRWMDRydTUyVlVmRFdRRFVmbXlNM1Mzd21BTHVDNVpIT0NkZW1vODU5VUFBNnQzUzlWRy9Fa3RhbXZPK3BaTUNCRU5WUjh0eDVxWWJCbkVVaUxDWmZkZHlGMXFNaE1VekFlZFk2ZVNKanVTRm56ZldHNUdpU2hQOUpuaGpOc0pTR1BiM1pQZlpBYkJFUks4YTc4TUxESXFiR0tyVDBiWDN1TW1uSW16ZWlKa2NNNVo2NXUvNGVHZTEzcVVmbjFNM1JSd2dGNWRoaks1SHZxZGxEelZIaEdpdXVkeTNpS0w1YUpoN2MvdmplSWJ2SFZ3Qmpyd2F0dGxvMGF4ZUowb1lNcUFKL1lzYlV4R3dBUk85aVpDUC9MMWM5eWJZdUpiOU5sSEVyczNWcVZPY2dKdmVqckJPbmRTbXVFcG83V3lMTVNNVG5nNEphbHlyU2pQRVh1bURFTyttb2VmWlZ1eXMwbEZBQmllOC96RWJMRGh3UlQzdG5CYU5mbFUwYzQ2WDQza3REbUxxZzZkcGNTTG9iTGIwME53czNFWHR1K2p0U095d2tXVk9WUERXTkgyd20vVXVwSm1JUFFMSktoQ1pmL0pUbWtlOVUzNlIyeVp4SFowU29iMHZNZ0NSV2UvbjNiVFZPNENmQUVZN05Ba3Fzemd0RnVqTS9vT0Jab1k1N2RUZFBHRmkwSXZWcE9PZFhQQWd2ODNORnFsU25XYS8zUkxUOFBVR2I4b1dvWkJGdXVmd0pBTE5RdXZPVHdlMHVUU2IzYmJwNFFQeERycGFGVzNUbFRoajBtekNWN0tJWG1oVy9JYkdZOGgxaENoRm0xeEo1U1BIcVZ0U3VLcmRXOVUzTVE2MVh1Y1ovR3Uzc2cvNEQ5WnZJUHBYZnJCbzVjZWxhcEs0NUtteXNwb2htS3M0WEI5YzE2TFgzaUp0RG00c1ppY2Urc2hGL3l5WUdaVm5teW4yTDlBSno5blVLQW9BMnZJZ1dSSXh1bTUwQ0pGUnVzS0tVajloSkw0V05YV3U3VVJUSEVyR0JYTmhRK2c1c0x0YUk0TDVKM0dhYmtHU21RdEZSdExnQTc2YTR5eU1hamZUV20xako5S3FYeWpWcEs2eWF2N2p0Z29tVlhpdTZPUm1TMVNFWXBVNmJnZGtiOHBlczhPcDhWaFJaekxMTjd2aEt6ZnFuUllVUWZOVFBKVDMzTTdOc1pZUFZZMUR2RU9aK0o1TU1CemFhbHZ1SGNma2cwWStHaFNnWWh6d0xUQUpqd043K2N6aElZb3VyaUloUEJUMW0rbU1nSGdVNHBoWUVDcHhrT0NueE1TLzIrSHpreGY0MFNuQ3RjT2pOaVFpUDR1TktEMEM5SW9DM0dCRUtkWFlBQmZxdThLVkFqWDIxZEVJblBIYXNhWjFrZnhacndTMWg2TnRhVnV0czB3Mmphcy9CVHFtQjZQN0x2eXZ6c09LWFRydlhEdVJIdzZvSDk3K1hPRG9NV0RpVk0zM3BXRkxrZ1k4dXpaL3FoSXc3ZGFSMXJiSHREcTdvSnZ2OU0waEh3WGtuRWVPM1crQmEyUS95SVJIa2wvdFJQTTJjNm9UUmpwN1RaT3RiU3JJTXBqQjdFNVpOL1JzRktJMVBha2VZWmpqcTZ3c3lWQWJLZXVUQ2t6T0RIRS9wYVIvNzlmWkZhMDN2aDdMUzlVMi95Nkk1cGl0eW55ai9MRnVIZjZrdnZPTnpwWUd4UEs1QklsRUtoejVQZ2w0NEJQMElxeDV4TmZKdkM1aTYyZ2FNWk50aVo4emVJRWx2SVFpZ0hoRWtWOWVCbzVjU3dUdldKd25IQ0Z3Uitrbi9meWdaK1U4QWprSGZiQm1CbTJIZ3U0VTJaYWFZQzZ2YTFiQXAxcHE3SllJUURYS1lwQ0U2d2JYTE9pajVqdWVOVG9WUE5ESUJsMXY1RmNPanJOalpkemIyMGZuN0FEVFZ3VE9uRzI2UDlJRjVmWUJidFM5TllBZ3lSdUd0emhySjF0SWhocTVYamlWU25CTnY3eDVxSHh4c043U0JsMklIZ3RFa1BxMWJDY3dzN0IxUktCLzEreVVzY1lRMHc1M2JoUW1ZSk5DaTBTeXFjdHo2SmZIZ3hqS3FudENuZWVJNXp6ZDhZZmJLUVFZY2NoOHNTWWNtVTB4SHl2RTF6d3QrMGJtQVBmYjE2b2x2RiswQy94UmpzWjZHaFZRU0JMNEsvdk9hOWE0MWZSS2x3eHZqYkxxS2hUSXpxN3ZwaWNDcUdxSmxhK1lrQnlmUG9vaW1tMjRYR2NOeW9rSzlsMDN1dUJjZzFoclZzTUJZK0RxYXBTaUVhRFR2M3paWFhoZ2I3RDVUUzZFbXViRURabVJ6MEgzKzA3RnM2UmZ5QXd5RHVSVmlzNGFUU0dXWEgzR0QyMjMrQWZWbW1EMCs2RUxHdXMvekpZdTdhNnpCWlN4SUhLSVFUdk53SzlqTXpPRjgweERML3hIVUhnOVY1YUQ1NnJGVDlWSUhGY0FqNjhSZ1R4cTFlc2FTMERIcUppbFNrbnBwRlIycmoxSnM3UXVjQzVzNzErNko4NVBlUUxFdFFGRXg2MlJraWs3NWFpYWZkVG9HTkF1L1o5bnFPaWd0NHowMFh1S0V1bGwwMUU0VUlBWWtoZm5BNmE4RmsxZGxocmNvL2grckl2VFh3OUN3cm1Yc1RqRHRDeGFGVzI2TjR2L1pQK0k4YkNENjdWQ092VGNmMjBvOGtMU3hsOFNNVDcxUFVGTHR4SWNtSitsQUxBRE5mSjFMa0hMR1ZlbldCY1ExcExmRWpwZUFPSjVXekh5WEZydGhpRFdmcnEvUldOb3NWWHVjY1E1TmJIRUc3WkFnRGs2dE5oS0NZcHo5L2hDTG9ScmVKRFNNN09wbU1jTDhhNC9MR2FUaENITnlPSmFDSk9NbFdOUGFGTzhDb2wrMDJvSW5wdUFmZENRWFlQS0tWREZFVU53eVM0VHAySWtoQzVHaVFtcWdMUUYvb3V0Y01xbmxWT2k5WWNNQXdqdlpiRklDNVpnTktkOWxiRkhlNE1lVXgrRzg2WkpqczNOUDRsNlRYZE8wUVFsakFRamNZTnZRN3djMU5LTVE0SS9XNlpEdGNaQjZ0ZSsrRUVaZTFucG1nd010azNhOGF0alUzdmR5am96N00vSkhYbDlrdlQ4SUxyYndhSVkySjV4SXZDb3VnTGl2ZzZjSXA4bzVUTWI0Q2tNdmpxSThDR0UvVlZYM2xQbWk4MkptQlRhc2FOVFl3UE5tNVdMOTZHT3dIendqZ3dBOUs5ZFhSOGhmN01zbDVUUWxBdmtmSlFrcnZVbjhVZzlUa3RGOUl0aW5UaTJsb3htdW1TemV5LzFFOVEzK1VyWHd5OHIzQTFHRC9pVkRBVS9Jck1iZGRYNTN2c04xaElQVWRiN3FEVkZHSGtNUkd3b25Ba1pocGxnOGZNUHF2b0EwTXYrQWxLcU1POVJidnBFeWpzV2hqNnh6NTZmWDlnbDhGN0ZMM0VRU3pBcmhaRDdDM25EcEFldktUUTBQR3IwQkFNS2l0cys0UUQwdVI3ZlRmV1hHSGY4NkFENldZSGEwUUtQMGM0M1NqdnhzVXRGUTdxQUJvbjF6QTdFQlBaS2ROeFd6UkdUYmFWQ1BqeXhsNXJISVc4bG9qNWo2NFZxUzE4dEp2emhBMDhJSDZ0UENKTlJBTlpaV1BYOEE0RFRueFBDb29tay9uRWJ6aExKWWRQb2prQmhrN1JNNXBOVzdLSUdhdERodzRzRUpxYy9DNGE0QmQ2YTRzcGZ1NWVtajBOUE4rNlBKTTFydllIT2R6WFpubjcyQTNTcjRGbjVhN21xdDZOQW1uSEpEcDMvY0k4cHl1YkJXZExNV2ZYSHFvRkZoSnBlTjJkdlBXYnd1ejVIU0UzMWNHOEVTRjljVzgxRVdTMU1xMlFSTkRBdEpIV1hRNFdtSWJldDBYU3FNTjdYY1RUSmZzNnVJenpvOXFGRWY5TlpZK1MwdHhjVXdMQUthZFpMMktONE9UYjJiT3AwZE1EOExPQVphU1FGZ1BlOUdyb2kwZllUSEdOVndrSXZqUlRWZG5hTkdSeWY3K3hRQ3g2VnRGMGx5Z0g3SnNXdGY2YWtocFRYbFVZb1l6d2Z3STVSRDhjV1RkUjd2V001WWVrVmZEdktCRXZqOVhYcmVhT2hNalE1dXZDZVc1Y3VzMmNNOUFGZTRkaEZqc0dodFZiSTAyYnZuYkh6V1JOcDcybjNKMjM5elNYcnU0cWxOTDVjVndZSDZ6K0hueTAwUlRVblc3Y2tDZjZzUEl1cjB4SXZpQWI3dWQ5ZElKOGRtTEVrdmRMaTdqSlA3TEZaNXJBcXFpNUZrNW5nZERqMW1pTVpHWG8vWWdZdEphKytiZElzNjZsYUNCQlFJY3d2MTV2Nzlna3RUN1lmK1NoSDZveWFocEVGZDM0cVpPVy9sYmdmKzZ6Sm8xY3lOdFRQOTNhUkdhamxGUzhodzRDanc5VnV4UldnbytKT2F0TERIanlmSlhlZjVsMXNzaU8yUWp0UDhzL0xFNWZuTm1TcHJxWXlqdDFzQUNiQWk0NE9PZDllanV1bGdJY2lPcGFERjZDdURsSDFsaUJFbXlOUVlVZm9Wb0t1c0RaZGw0Y2lBYlBtcytSdnd1TmZxZHYyNjQ3ZHphc3RyRVdHMjZWMHloY1FQbXJXM0g1QmRJU3NlRjh5YUJEQi9Fc0tTR0RCcDM4SHhpYWpSOHJOUEZNYy9pbUpBdmRCRjJBZEJmenowK05sZUlZaElPN3VqckpwT2w5MXFnTitSNjM5enpUTDJ2SzJ6K2RSV1ZGTFVaTWVvWVJZYkE4YUUzYUtkV3JodWxsNS9xUHRyKzNGdm4zK1FEYzRoVDZWbkNwSHBzaUwrdDA5QXUyeFBzbWEydklDazlRTis3c3hGTHMrS2ZmUWdWTEdHRG1RcDA1UVJLSllKNXkraVBPUVZENG1tZkVSNWtwZE1UbS9tNEgydFBrajRZUDJyZGVvV3FZZ2gxZHcrNFQ0U2t5YUdISTNrWFM5M3dlc1QrWFppbFJqQVBTYlo0cTNFNmVXUVJ4U0dabi9TYlFwai9wVmJMc28zbGZmVTFwSmwxYUFTZEZudnlLdS94cGZDYUQwWHVDaHRSVVpzb2ppN3hFV0JTZ1FQR2N3U3pLdTRidHM0LzVhYmhyY3p1YWJSRld3V0VJVjM5b0lqVkNhYUgvMTUzN2kySi8xallDMHg5c1NGWlRDVVMxYW01V29WRmZmaklvSDFGSk5Lc3RMK3F2OGRFdDNXVDhubzh3VFBJWFVlVCt3elRXZXQwSjB1akNLSVgybTEyc2FXdHM5MmtlUkF5TGxDVFpKcXczTmZlWlp4QlpjM3lMQ252OHhvZ3JkWWN6dWRkRXNad1loS1J0ZE0vcGtremljUzVaeGF5T3VIakYvcGhLeVA4dkxUdGsrYk45LzFNNm50L2wrdTR1aGlvV3NGbGZWa0xpblBkZm54NFB3a1FWWnBUVjVNL2JqMVhQdFczckJVamRIOUd3dDlya0w3SE1vUWowU0Y4YUNsVTU4dG11TUFtc05BRTZRcTViS3gyQk4vMjhob3ZaemJXL2lNYTQzenRtakFuVVg0bm5OaFIxU3RDeWN0OFRwZ29INzNFZHQ1dlZDVnhnb1VXenF0Sk1USkFPZEF3YzU5N3hJTWYxR3FLNmprMTFtajhXWFY5TW5IUWIya1J0dUk5eTd0dUNLZEZTcmp6SkttUFJBNnZBeDFJU2RwSGNDU0RldjF6SXRVOHBNcXFodVQ5b3J5ME9PbnUxU0RZUDhsVWlQNnBQU1JMcHp4S1ZROVB2NEJZbnB5ZHVOTHZXRkVXbkdFVVpPNEg4RzFuQUpUT3djRVJObC9TeFY3R0kxWGkxVU5Tc0hlaExPV2RER3dCK1lEbk9xQXFJY1FtK2pITFRWbGw1b1ZZbkxTU29NWktvTUJxbmxibHp0QmN4YURMVlMxczhCbStLcHhrcllLMlhqM05xTmpibXVXRG5YWXY4T0FCYktNVTRBWG5qV3kyeEd4TEVnOXNxU1dtY1JRM0ZTTndFeitEZVNtOEE5cjA5aE8rdnNJeWFCRjliUWJHRDBKWTZSRkJFMS95WjI2OCtJNW42ZGxRQ2F2eHR0ejBsaCt1ZUFlazFaWTlsK0xGVVoxYlU0ODRhVWdaamFrNndtcHl5Nm5zRy9yNWt0d3JCaDk3eW81WnFXWVZjRUIreUR4SHoxd3c2cW9lbFRXbU8wdmxDZVB2N2V1Q2RHZE9uVHZPczJjQ0xubyszTWNaZEtGU1lleHJGc0l3Q05Rd3VTVkZaQWpiRlRxMURlaXJFZHJlSFM1cGZRY096c0sweUhmQjlIRVZCdXErb1Erd1ZsdjZDYzd1bG43cTZDck5lRnNiLzlkNkF0TFpZb1dqeFVXZE1QVUtyd0t0SW14cVVGWkNyaHcxSkticUZaeExmMnI4aHZubnpzZU9HR1dKdTE1eW1LMTEvT3NWRXNaTWExaGsyTHdzV2FtZXhvWUdPa3VIbU02RDlvS3hCd3BCRGQ5d05XY0VETEJneU5LbmdFb0FrSEM0Rm5CY0xnQkxKQVM2QUtqVUlKYmxha3Z3Tlp6ZUl6MUNiTW9EcVBicHk1U3QvL0IvMHpYOFNLZmlac21TemQveTNoZzRVOGlQNlJYVmVGdm1vUS9PS2g1OVNVRnR0aVVOSWllanBNNjA5NnlsRFlQbXlnVDdkaEgxbTlpLzdTWHZSUHdqZ3FoNE43QVBWc2RKLys2YStmQjRYa1ZSK3Q5V0V3N1J2VkxkRW5mRlIxZERZSHJDdHlRYWlhMGxjR0xHVGEvejlxTGdTVjY3QVZJTVdJTC9KTm9rRGc5Wkg0WS9oV3RQeTFUTURRbkN5TXcvbTY0cytYQnl2SDAxZktUdk1aczdiMUNKcUE2djA3cThiUFh3UDR0cUIrRmhxU29yYy9TcEpjRUp6bFloQWtUbmdkU01scDB1aHBZVnJ2MFc1LzA4emcrYWhKWTkwOGlFMGdaVWNVT28wWGpKcEg3N3hhSmJzdEV6dFZlQk1oN0ozU1ZtK3hUcFUxQWE3YUQ3YnVabGpMQ3V1T2w2SWpyVW5vRlprbXBXbTRWWWRyT1B4Nk1BVGRxZEpQNzZMNG42NnhSZG1XSWNMR0N1b2ptZWs5cXBoMGYrbHJlRDJxTjluYXgyenpGTDF6S29wR1p1dTMyZGkzZ3hsYzNaUmd3aGJMZkxoWS9qREh1a2V1RExITUR4NWVUYlVkeXd1bXQ0Rm9raE4rOGFwVTJSRGk3d1YvMEFkejRKNjluMmRtdUs0Y3owZjJNSVN3T1orN2IwN00vQ2taL1ZWa2Z4amFvRUl1Q3hDT3V5SXJFbEV1YWxuUkw3OFRLZys2Z3kyOEJ3QTZMUDZPSGRyMVdZRzEyK2RWWWtFU0JHdEdoQjVoNFl2bm5nWENycUdnU0FYM2pvNXZwUFdONGl0WlQ3aFp2NGRSQ1BVdkkxNmtPMXVhNnFCb1VGWFBIMzQrRlk3Uk52R2NMSFNPSWtMWkh6UXZCd0JLNG40cnkzR1pNWWFCVlRtNExLVi81dzJzbFI5bDQxNzNCbHBtK0xEbkFON0ZXZWcxWEgyNS94NEt3ajYrTFA4T0ppZEFtMGQrVXdIRWI1czBocGZzcERVaVYwaTBDcU1MaFFLT2RrNnNrSDI1YjdnYlBkME5Ham5DMGliZGRzSXVMN2k3cTdDbFVkVWpFNnNJdlczaStEcXBRTlRZVEVycS9vdkVwbWhsRHFMVjdhUlMrd2dNUzVvc3hrbm0wenljRVRwYkRxVWNqKzVzeERzMTVJbkYxQ2FYTFp1ZXJaNy9hRUMzblJkRHgzenVvWnJNbzIxZTBmOVlMK1FyV3pCUmlKK0U0aURrQWJObzlpdy9oZlFlczhtN1Q2WWxvTWE4em5QQ0toMVI4WTh5NnV3UDFvc1A5aWZwSjZZcWZSMTRtTWtsR0k5RzdUUjU2MEk2djY5R1lGL29NTC95WXcxc285anhBeU9EZzFDZmhEaDZBNmt5OHdJb2FBV3JsSkZzSk5GQUp0ZHRDcjVJbWRVOVNpY1N0VzBXZWVFT2dwaEpCUUhweUFvZVhaQ1F4Q3RySWlXQlU1WTFJRjhyeXpVZy95WWZOdmlPM0YxNkk5dCtVUndybWx4Lys2OGprVjVHanU3cWxJd0tKODdZRzZVK0NpbEs4ajZ5TldidGRpZkplNnF1Z0RMRXVuM3BteHFnbzNaNGdQWGVpS2UwU3FIRy9GWnNQN0NlUGtUemhkZ3NlM1AvUm1zcm8vSnpYMm4yS044ZmdSYzZlSHgwQXU2ZkNOUjBsc0FWWEoyOG4rbXkwVVpPRTNweWptTWN6OXUzeHJJVHZnQ0p5VnJwRVptMDBhNmxQRFo3bFNna3J1bmUzcFhFdk1SbTdIdEFqK0FHNngzZVlBbllMWUlnSkxHMHROb094RjM4ZXIxYUgySjBERnBMS2hNaTNZamhyQlJGUTdmcFZxZDZ6TThOMVFXNUFxeXhkQUcrR2pZV2ZWeTJudlFwaWh0Y0dzeEV4ck8wNTlQNDlsb0s0WW9IM2lPRi9zS0RlYitEVEtUd0hmdTVGcXEyTDdLZnhpVmJzK043ZVlNOVNVZ29rdnkrZmRQOVR3N3ppSGxFQ0Z2SWpUZE1YTFdXaUM3UlBwNnFlUTJwbmxyREoxdlQ1dVFTWFc2TE9VZHE3bm1pTDVjUHErdHZ4YUt4bG5MWEY0UTkreUZ2eHB2dG9sVG1qeU5xcFpnRDNTWGFsQTg0REZPQ2h3N20wcE14a3UydHNvSzB4TjdPNU1sVUIzejBxeVRqYXF0KzRKOUgwbWNPVzhqSmdEWGhVaUthcVNpQzRCazNNZ0RLNVNTWVNQVGM4V2cvdDJKOFdHU2UyS1N1K3hKTDBiZGZQR2J1UUo2YWxoVnYxQUZTTHkzb0VnZ0lzL3Y1U0hzVmNub2NacDNFRDUrK00zL0pTd0ZvUk81M0VJYkZqVDc3VFBqb0RyNWlRLzFzQ2o4dUpSdjVmbHFGZUo5ekEvbzE5QlhKTGh4T0kvNmw5aXlZb0J4UzM5Y0tpUGlsVjN6c0xPdkh3VGJEdnVYMmkrODBXbG90RnhXVU9QZjIxMEFXeUxkYnhGUHlBb0hNY1ZIYVRqVHFyUTkxamhtNDBZZW4rSTJ0OWNwY25OZ1RTU0dlN2ZtY0dlS3pNcjlERkZ5RFJSdFVzWTRLUHNpUVdRbU1GaUdQYW45a05USnR0WkpBWnYzNHBuZUVCNVBnbUVCVkVEaklTN01mMzFNWWFkTml3QWZwSTZxZ0pSUlRrc09xWDFvSDNtR1Z3SDhrT2x0TDNXYWJObWRUam43UDNWQVBzQ2pJVUxmdXB4dEhQV2YvMlFBQkFCb0FRQUFCT3dCQUFBOEJDanNDQUFBQUFBRURBYmxBQUFJQUNRSUJBUW9DQUFNQUdsUm9hWE1nYVhNZ2IyWWdSbWx1WjJWeUlDZ3hNQ2s9PC9CREI-CiAgICA8L0JJUj4KICAgIDxCSVI-CiAgICAgICAgPFZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvVmVyc2lvbj4KICAgICAgICA8Q0JFRkZWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L0NCRUZGVmVyc2lvbj4KICAgICAgICA8QklSSW5mbz4KICAgICAgICAgICAgPEludGVncml0eT5mYWxzZTwvSW50ZWdyaXR5PgogICAgICAgIDwvQklSSW5mbz4KICAgICAgICA8QkRCSW5mbz4KICAgICAgICAgICAgPEZvcm1hdD4KICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-TW9zaXA8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgIDxUeXBlPjc8L1R5cGU-CiAgICAgICAgICAgIDwvRm9ybWF0PgogICAgICAgICAgICA8Q3JlYXRpb25EYXRlPjIwMjAtMDEtMDdUMDI6Mzg6NTIuNjcxWjwvQ3JlYXRpb25EYXRlPgogICAgICAgICAgICA8VHlwZT5GaW5nZXI8L1R5cGU-CiAgICAgICAgICAgIDxTdWJ0eXBlPlJpZ2h0IEluZGV4RmluZ2VyPC9TdWJ0eXBlPgogICAgICAgICAgICA8TGV2ZWw-UmF3PC9MZXZlbD4KICAgICAgICAgICAgPFB1cnBvc2U-RW5yb2xsPC9QdXJwb3NlPgogICAgICAgICAgICA8UXVhbGl0eT4KICAgICAgICAgICAgICAgIDxBbGdvcml0aG0-CiAgICAgICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5ITUFDPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICAgICAgPFR5cGU-U0hBLTI1NjwvVHlwZT4KICAgICAgICAgICAgICAgIDwvQWxnb3JpdGhtPgogICAgICAgICAgICAgICAgPFNjb3JlPjcyPC9TY29yZT4KICAgICAgICAgICAgPC9RdWFsaXR5PgogICAgICAgIDwvQkRCSW5mbz4KICAgICAgICA8QkRCPlJrbFNBREF5TUFBQUFXN1dBQUVCQVFBQmJzWUg1Z3djRENBcEFVY0FBQUFBQUFFM0FFQUFEd0lBUUFJQVFBSUNBQUVCOUFIMEFmUUI5QWdGSFFFMEFaMEFBVzVWQUFBQURHcFFJQ0FOQ29jS0FBQUFGR1owZVhCcWNESWdBQUFBQUdwd01pQUFBQUJIYW5BeWFBQUFBQlpwYUdSeUFBQUJuUUFBQVRRQUFRY0hBQUFBQUFBUFkyOXNjZ0VBQUFBQUFCRUFBQUFhY21WeklBQUFBQkp5WlhOam1jb0FBWm5LQUFFQUFBQUFBQUJxY0RKai8wLy9VUUFwQUFBQUFBRTBBQUFCblFBQUFBQUFBQUFBQUFBQk5BQUFBWjBBQUFBQUFBQUFBQUFCQndFQi8yUUFJd0FCUTNKbFlYUnZjam9nU21GelVHVnlJRlpsY25OcGIyNGdNUzQ1TURBdU1mOWtBQkFBQVVWdVkwbEVPa2RTUWxRd01mOVNBQXdBQWdBSkFBVUVCQUFCLzF3QUUwQkFTRWhRU0VoUVNFaFFTRWhRU0VoUS81QUFDZ0FBQUFGdFhRQUIvNVBmRzZBVHppTEU4bkJvL1o3cURGOEpubUVua3ZsTVpzK280N3RLK29hdG5OdDdQSjBWQWMranNkSU5jQnRHbjgvT3BFNWtlUkFUMHc0MVF0OExnSUNBOHBCd05EanN1bzhZRGROV2VoVXJpVnlkN3k2QThTRFpLWlBYOUZyalJVbmMxalp6OGlOdDJodUEvQWhBT0Y5NUpHNW9Samtnd0tHM2lTbGtIemVwMHBGRGFjTExCSGEzRjFueWRYRi96cWNUQUhMU05zZjJmYkYwQU1pbUJFMlNYYjk3MHNFblhBbFlTbi8xTGpvVlFKK2U1TU92MC8yZ2NEQmUya3JGdGoyR0Z1UnhkM2JpUXkzK3RyV25pME9tRzcyNXhEZ3ZXSW5tTzZZQTdJdmdwWlk1bkV4L3RUbDR5V2JtU2FCWm5SRHVjektpb1VvVzhkbEFQdnZZczFrTTVKU2ZKN2FsRnhOcW1LUnV3SkhWNERHTWxRQ280RFJYN0JLbklZS09lalBxNkJzY3RWaGd0dHVKdmxvWG5wekxsTFlTaUg0SkxmczI3K2ROdTJlTkF2R0R6TUpBMVJoUFZrNXdQOWlUL2xHK3JkS3EvRzMrTTErUWVuOG8vUklsZUdIWUU5cnlucXdOb1BvSzVXVXB1c0lUcW01MlpLRUZWeVowYjFmenZjNkM1M3VRMXdaeGsvOFhhaWpnT2c4M2I1K2J0c2Yyc2djUzNwZUY3YVZIdXdPS0FKN3dYZ3RDekVQei9EWG5ydFVxdEJwaFFXRTJHMU9KeHM4QmRUcWxtcXByYTdvNkgwbk96Zjc5REZlSU1QajI5OS9QSk1ZMVdNWmNGNWhXY1RpcEhzenpjNXRzczZudW9hZVJHU3pEMjkrcGxuYzdSWFpWZjRPZTNGVjk3c1BxSXArQXpxaWRwTWhsMDc3cFlxMHVvclpQWXpPdHZ4elVSRW03anFGeHkrQy9yZjV2K0FXeXRRd1kvQ29qa05INHZhSEQ0QTNzUXl0T2tSei9YZWFScDFtdWdHQ0RmeWdIZllUWWx1Y0d3RitjYXNmVUVuNktsMnorajUxbE9yN0hIWkJZTHNuVkJ4c0I2c0hxcDdsVVFqMmtlNkZWVzJldFE5cjgya0lBTXdsQllhcXVKMnhETDd2bzBXSkYvUGdTMWNZNUo1TnRBWHVrZGNIazg1V2Q3QndwVnBEYm5naGs1Smx0aTJsVUVjQlZDRzZCVy9WUHY2UjR0Tmh3aEMwNDVXUmc5eUppdjNFdGJNTWhXK1RwTUkranRrNEpDNWpuYms1eU56YUpNY20xR1J0SGoyMUhPUmFKbUtQaDNXdXo2clVUQnhLZC9OS2ZlV2R2TkY2ZElPek1EQlgvZGQxSlAyeVZhRmJuZUs3MFh0MnVoSVR4VitGU2ErWXY0UHI4ZXMzb0tiZDlRTElrck56dEtjYVZVbUxmN01udmNmenZZL0tUeW1ZVXBnZkwvTmtyTnFZcVkzTitOSTNmY1Z4N0E2NnZ3ejNoSjhETHBUODNCU0xzTm45NEcvVEtNY0c0NTZTVXRIazRpN005T2I3eVBzT2ZvRzI1R3hLV1ZMdmJ5UFNjVVNDV1NJa1dMdjI0UWlyaG1hdmwyVEF4bnpSQ3k4MDZFZ2Era2paRzV6RDhnemFaVXZiZHdaN0dlZ0R5eVlld3JZejBleVRZU1N4bm81cVRqTjBua1VWSUpLblYySjdmQXRBdGp3TjVmUWRENUF6THlxdm5pSHdpa2lSU0hsRGhaZjZmeDREOHlJZktrWDBxZ05RbkY5aG5ON01keU1aa1dmcFNCN2FrOTF6eStoNldhOHBucUdBVWdPZEVaOWw4VTd5TE1wSEo4OTZqdXVyNDB0N0VjUFJYZldYNStlckVWYVd4T3kvalBaZ2xDakRPL0Jadk01K3ViWDFZYlV2Q0g4WUJ5ZjBLenY1Q2hMejJJNjA0WWVnS2FJRWtBZXg3aG5XZ2pSdExnZVpOcDFvZzZ0QW5xOEN1T210dUFPTVNUN1dEQWxvdnNPWXZiblhDdzNydEtmQ3ROWUZDdlI1c3IrOVVFM0x2RWVXVjJOY0w3Q01QRDc2ZDd1YmZxTXdnRHNob0VqUlgyL29VSk9kQ2ZHTC9XSCswMkRYUHVmeFdwdkhXdDlUckcvSnFoZmhnUERSNDFTTm5tblc2aThuM1FMTUNHbEZ5VDQwTGdQeDc2WUFNVDd5NTZPRDM5aldOSXZsZkNiM2tzc3JCcng4bFM1dVdyVEJwK1VpVjV0NHdxWlU3ejZhWUJMY0NudlFmb3A2QjdJT3VucFZtQ3FSNmJSNk8zN1lVVXpuWlcvdWhhQ2VKaE55TjNHMURSdHRmUE1oNkxpWWd1TlRwUVpzT3BiL2JDaTlCKzRHLy9tVlpUMEpVdGdGcDlQeS9tcGhYMnZLWUhmd2daUUhRVnZwV2JweHRRWEw2UHA3R285a0NKZ3NFSlhJQmZoQWFWSDVCMGQxWXVvSWFKOWFEQ1RkRW5MNXk5MCs1MFlDaXB1cU1KVTEwV0lKK1ZWUHB1S0VseC9STUtLSkdvWm5JS2FsMGlGWUlGRXljYkg1TUNYd1RXWkNxNjhLb2VoNTBLcWpQdjA5RGRIejNUM1hyZWhKVmJnb242SzBDOUhpbmdtVFd3QVpyV3pwVVVFQUFGWWJKSzVQek1SeXoyUElSd3FkN2R4d3JMYWk2SGhkaVhCc3JRQlAwaGJPUkhMQ2lpT2Y2d3p1MnBRcXBsUGJPamNaa3p2OWhLd0NNWWNuajA3K0J3REI0bm9wRjYxQ2t6UEhvMU9XWkhKT2lKVHA2RFFjSUZlS3NaMUd6ZXJWbmZIaWd6SjF2L2ZaeXg5aG90N2dyMGl3TDgxcFprRVd0a241aGNwRy9icVk3NXBkcGoyZjgyUHhQblNjZDFRbytqUlBYOU4rcHQzV2RDQzNjZTEyT2M2eTdsVXJ3NnVYSVF0VnozVmdkVDI5TzBDbGgydS9Sc2xaVHNTWmh5KzNFell5eFlySm1DQWNxRHM3bVRzamZJcFNtUitCeTE0UEEyM2lJK2dGa1AvQkVtWlo0REdGU1RrUTZYN0ZISnorTjFQN0dBNjVIejBLVUMwaFNYRzRrZTBiQTdXcW5vRENVQUZjMTJJV281NDhnUU56MUV0MU5nbkdNanJIaXM2QW12L1ZiQTllamhSNHVMbkczZmlndDFkU0ExbnJVRjNNVGQ5NnFQUm9hS2w3ODEvT2pmWjZmWjg1anEyc1RoYnJQMEVGQU9iaVlWSTBFSDNHS1BrNG80VWprTzVKaktyelNVUDlsUHpoa2FGZHhOSUZZQjYvSjI5RDZJc0pmS3QyUElVQW5JMW51dW5uZXlGQlhlZnl1NjhDZjdISk8wSFVNbDVIMFFqaEVOTmFhc24zSlNDOTBKS3Bqc2FVTXVVS2FyMmRUOVlwajVyMjlQRmdWQnVXYnBTNVdRd0VkU21lRFVKMFp3M1lyMU1NUEFUcndWRi9KS3h4clk5WGFRWUJiZTlrdlNQMHlwemgrSUdyZ2w2UEp5N2dBZTBJVWVjR1FyRk8rZlY5aDFmNnZiZjMzTEdydUlmTHFJcGlRV3JoQTRNdWI5c2I3UTN1bTdkMXJzTjFZMkRwYUlXNXk2WFRkMTB5MXpQZStIMCtIQktFMWVPeGRzcndUajg0MjB0MVNXcEVhSEVtcnFhTXhyc1Jjb3hMa1NPTktoK1lJSUUrWlFiNlZUUm1FYmg1Yks2dVQ1bjZUUmYyRWd0UVV2VjhqSEpsdllqbmRKR0lWYWZzV2NkcjFva2VvZ2c0aElJZVNzSG4ydU5DSm0vcFBoL2JVUFhRTnNBQXdzYi9WSWQ3eTBFMjNKNzFqSHZXckxKQXRYUDlWS3VwTjExZ2F2VnNROU5IOUxaYXcyVDJ1U2RRMnVKSHJ3QUN2YllMUGZpR2VDTjNFZDZneE1menBsZ2dBdWtjMVlkUGh3ekFmMk1aazhZTSs4N2drWDVMTC9sd2hocnUydXBjWHkyMUNtRm1PN1lSbTdRZlFtUkMxYit0ekFaNzBXcktTOFlHYkVWYWozSWRFckIwUU8zU01SL1QrYmRDbjlqZ3JBZEN6YlJxR2VEUVZ5OUJkejg0RUpaL0wyQjN2Rkl3bm95YU1FU3gxV0ZyallXb3V0NjRsQlpxOW5kazFMVTFuWVMrNi9qUkxJOThVczFsWUtuUzBGamNER2x4UEZXRkpGS3BkSjR5VkowSjRQSVMzMWdDdUF3eDh5RzA2QjFQUVFxbHVxT3BHRzBRZCt3bXZzb1pST3JwTElKSjBaZ1JybkMrVk11WVBZYlNxRHJBQVROOG5hNHJEQTJJRlR5Y3BiZDBhTG5sYk5aYTI0WE5PT24ramVGVE82ZGkzV0liYWhJNzdDNzRONzU5bjdhRStpUTRsQXlkNHc4Wi90OHpQeTNFT0RNTmkva3VNdTRYSHdFZ3JtNzFSdHFjSW9hZi9TQ0VkL2FGQ3NFc09NUHRxMmdOQ2lveTB3RlFkUzJGZUczMDA5WlBiTTEzV3BxTzk0MUEwRExqNlMvbU90RmF6TllXVnd5OXNBNmRjbWpseE84VTZ3R1IyZmRkVENsVDQwT3RlUm1iVlpVaGFQSmxUMEtYRlJVRzNSOUF5MUkrWERSYWpneWN2SnNmNnRvRVhha3FTNEhwYzd0T1AyRnhZNU55K2JqRVpyTTU4aUtXYlNoNmtKbmExbmZpT2ZzZFJwTC9tek5mRXNyVisyUU5NQ2o4Yk1Ia2FXK1J4d2pjSk9HTjBNaWlzWVdLK2Q2S1d4T1M4MWw3VG1QZnc1RlFWT1hBWGRHYjNlRnRNTFZyMEpVcEtsbzN4UFFOdU1RaEs2Y1ZPaE5FRmpWbi9MS0xHNFNER3E1bGFXS1RJVC85bG5HenF4QTd3M3RCMk44Y00yY0laU3NlUlVjYjBjQkp4Z25Ma0MydUxSL2RxQ2hJdzdIdGc1MXpSNFh6Z1NRS3YwVzcrL1A2ZGRJd1Y4eXZrQlBLWkdFbUFPTmVRd210WnVjNW5xbHRtakxGZGpSQVZLS2FqUnZzOUNubXpUQlllU2Q4ZGUwYmtVU1VPMmFpSGtCQXBQVy9SVDA0U1lEa3FmMmtScGRGYXZwK005YU5DWmxhSFFJTDhYWlZPbTlGV1p4ZTkvdGZaYnIrUExybWcxcnZuQ1A4cWJrOVJ3M0MzSnRkYU1tSW4wNnRPOWJSbURuY2NEVHYrczVyOGd2T0NsUU10QzhIcHBIMVBCdTh6WGtVTUFGd2poRkNFaFBib2VHTkNWek5LUHQ0b2ZSUkdURFpjRW1BNFFMOHdUcWZtcFFyMGo1QUFqMnpEZVdDUEM5eTNRa3ArKzhmVVFzeS9SeFBKMm8xU055d1lWY3E4WTExdTYwNDZ3d3dVZlF3WjR4a2dmN0FRaXBKT1h2MDlYK0ZMRFRoVmpMUjlyQ0wxN2pYY2Q0Q2M4WGJGbG5ia3E2Z2ZCY3JVRE9PSmwzUXY4aUNHNVF0c1JBS2pOU3NVREQ1L1kvMEdpNzhjSmhYOUNBSUZTd2xHeHN4bGpmMllRMDlSVzloSWlhcWpuU1BXc1N6ZHZXNzIvc0hZdnQzNEI5bjZBZHBKczEyK0dwL3ZzYmx6aTFFOUdScURxeFV6cHV4ZkoyOEYvZk9uSFNuRGZoTDZtWDF1RW1MeWRxdEFJN2dvWXN2b09QZktvZU1kcXA0LzR0TXVUTkZzYTJNTmJybHl2Wk5nbWVrTnpjWDM5UnVDdWhReFpsOFU2OGVMZGJmNy9vUzJXa3dEZzdiNXlteklBNmNRaFZrSlp4bG5xOHVFVEJlWXZibzZzeWNzVkloc1ZnSVFxVVAyTldwSlg1dWFPcXhsalEyeC9wdHZSYnNQK0QxQUx2QlZSSmhzWS85RXVTbzd4Zm1hVEVSVkEvMlN1RTVvcmdORTRjd0tXeG44M2tYN2ZRQWkrek5Gc2cvM09aNys2MjNTT0wyN2gxRDVWQmNvU2ViMjN5bHNCT1hVSE40UHM4MmttTk90Rmpmd1FoYXlYS1orRUNlME9yT21YR3lUcnVuTS8yelR3cDg3a3BvWnZqODg4T29sRHVST20yQVRyRUlIR3NhcGZhNzh2M0NLZU1XNTYwdVhpMVA1YmR4NjdGRkhtK1Rjbm1JRkFvckluSWhHYVFMTDdubUpldjQ1eHpMQkN5QVdqaXJhNVZPckxTaURDYXo5WTFOeFdiOTExbm12RnYwcVAvU3FIOVpZUUtkUUkxN1padS9LSmFKeUlXMzVNdmdrcW83REh3QUw0MXNOZWF2d2IvcHk4T0lBY2JjU3lRN3dPR1NyYmcvNnBJcWpTUVU5YVdiZjBIRHFBbFMvQTV5VW9FSEJzK1hBVElaMGZQOEc0bndhNy80QWUxSlpPaGxFMU1XUmIrc3VEaGxqSGNyVXdMekRXUGdwb0pPYVN3QlhwS1lUU2JUZkJTQm00b1RrZ1JTcnpyeW0xak9MeUlvdUU5bTZIcXl3MUJ2KzRlckFMbC9IQ25POWhVYXJ5TXdaZkc4KzJRMnlZYUVoRTVoaVNwWW93ZlFEOFZqbmxIT1RyQkUwTTZnT1MxcW9KSGtaT1ZZYTJZZURmOXBiaEpUdFp4UTZJZTJlR1g1dEVPN0R5OXZEd3VJZHR2YnZMUVZMREd1K3htZVVNaVh2ZTRCaUptbURKN1BUdGZGMDNIeW5vclNoWHpLc0NFd1VGUWFldEUzM2Z0a3Y5THdobERPb2JRSnNCMVBrbm5GU2thM0RxaTJuTDFhcERZS3lhNmdQaU9RTUdVdU82eFJxWmNTbkNuem0wRUhYME0vOUtCMTBPSmZtcCtnR1FZSlErUGVHTEE1UW9JRVUxMUhqNUtSeURaREJEVzdSUldnemVsRlF0aDBoS3dTcjQrT0syNHk5ZGpjZUNRaWhGa1JUWlMybk91VG5ybXRlOXo1NHRQMm1CTjdLVlhQUUQ0V2YyWCtvaWVqeHhqL2lwQm1Paml2Mkl4NVBVa1d6MEpBSXlvN1dJbUdIbmpkTUtSZG5lampmZjRnVUxtOEZnUzBqKzlZaUlnQXFTbFpkTEMySWxkWXIyL1ZMSkh3bW9FMnF6a3hWS0tSdzJNQ1RXcElLMUprb2pLc1RmZ281bTladElzR1o3bHlDSHEreXErOFdMSURnUmp5R3NWa0ZXbmpuTDMrNmNWeWVWVzg4WmZhUjdMcTB0ZlpYVndLMTRqcjZrajFDUnB3T0s2TUhVb1VFVW42N1pyeUU4QnFTU2dKLzZvL2hmOXc5YmRQSHF2cjF3MStFdFFVYVJnNHJzSTk5R09UZmNJN0FtNWhTemlXeXdQcEhxSDBhbldRNlRaUytHWm9tdzJ5aDZCdHkxd3RPNGxyL2QyNm9ZNDY3d2V0Q2syNWZ6ZHZReU5TZllCOTRiZTJ5c3F1eHZ6cTRaU1NGMGliL0tiaXk2cTU5aUtPaWg3VnlUcXN3UkQvdHBGU0JEdElvbk1ZVFM1SExGbnJMLzJHejBRd0ZWZ2w5WVdBVzcxZ25zR0dFQlJvN0dzQ2RXK083Y3oxWENrd0J2L0o5VXNmWis2bkZpWHovTjlkNjhmWTJjZS90dkN2UnBXdnZrRGZkZ0tMRmxoaFhDa0JYZ1JLOXEyeDZqaXlXTXJFY1N0UTVKeElpbnVqbTJ2Vm04VnNOYzNFUUVOMERtcE42WnMyN3VVZjFnL2NRR2NoYjQ3ZmZmNnBHTWJLME9mclJlbjlpbWtBK25XNkFQL282UXUvNFBRVXlzUmNIakROcVNRSENpbDMyc3lQQUF2YklsRzNQdngvVWVxQ2RRVXljZ1N2UDVCWlFJMGZlRUJWaS9JTlIrZ0doWlRTaDY4dHU1ZGdlZXRhYkhzSVhVSlpRbmNXUVV5Y09lcTNtcms1REJPc0hVblBPckNhbmIzdDd6QkNUR2NTZzJwamJYVFllK2FSUnRlVkhxWklOZm9hUFhBRlFHNXdRc2VKL1dHVXcvam85bHdVd1VydHlzSjVFeWRIaDRyUXlxVjd5NDJGTDNRcDBWSzhrcjNncDZ0YmtCZW5KQXZFZHU1bit1T2htY3ZPcE1rR3F1bWdXQmhOSWJqcVpKUHZ1aWUxdVVJY0cwU1RTYzlVZEMvdlN2SkNnTnd2V1h1UWtmR2NqU1hmQTJlMUlyNWJlaW8yVElPcFhjVXVaMmRSSXhDNDg2MUk5R3BrMUE4MnpOWXBrSDV0SjM4LzNlY0lhUXRtTnNjSzBUemZEOHJYQkwvSjhTbU9JUEFQcjZ3V0FGbzFTcTF3K3doa21qUFMvdXNiQ2xsVXZHcGYwQXlWdU1Ldys4eENNM3BBemhDRkxmR1BISlRTWEJOaWFkcmdpNDV2L2Q5SHRkbFlwT1pKdkhkd0dmKzZ4N0VFUm13d3BiL1ZsR3hLM3BsdU1INDUrUFpZYm80ZmppenRRcUoxVFVPQ3FWZXVBUEgyM1NWaFkvd0MxcEJoSjJYeWhTOCtHZnI1bGZHVnM3bUk5Z3NZTVdzNVpJYWlCSis4S0NERVJCSnYxQkpRWEJUNGhNL1R1M0Q0aEpndUVHYktTTW1WYzJ0R3Z3bm5RY25KQ2FqM3Y1MWY0YzFKcHk4SEp1eEQ4VjlEL0cvbW1sQ3ZOS3I0U2ZtSmtNYlJkK1Z0VEI3ZDRnZnlRUXgwV0VjcXRqS1dVcFhLWDBwcnNFQ0x4Ri9QdS9aQ2NYMEpjbFRVZVdtc0xMd20wd2UzOUl5YklmakFqNnRMMHBIL3pyNytSK1JPZEdHd21DTEN3angxQnkrU0czNVU1RGdmNU40ZnhBUW1UMFIwTUlFaldFWHVhY0lVWjJzNDRkTDR3Vk9KYlNqVE4zNm5mNXlkdEVzMytYbUErVmJMREhYaXVzZGZrTFozTnVCaTBSZm92a3I2QUhBa1dFYmFEcStteG40SWExTU5MM3JDWTM1OTJXTW5yZFQ5T01sRHc3b21TZ0VHRjZoSjR6WHZHVVNlU2FEdVo2TEMvY0pVWTFUNDRsYUtNd2hUZWR5UGkwaEtnT0k1V0lhdkZMenQ0RkcvenJRMms1OUxVTllQRmliWG0rdFI0OU56SWlYZzJJejdhUnhzRW1zWHVXd2srYjloam1YMU9MZ2o5NzJPbDZPTVlIRXFyWTdZcmU1Y2Q5bnV1N09kNFlSNUJMR2JuL2sxYTBvZ29ZUDZqakUwOUlJYU5zdm1nd3lmM201TTd5RElEdmx5MmkrYXl5Y1VQYm9tYmtWRnJ0WkxkLzBCazJRMThLSzU3eno5SWNNandQODZIbmszT2lFbDJLdm43ZFJwWGVHbzlEZFRZanVoR3JHMTFydHFEREREaHJLUkJVUDRXbVhwQWZPczJ3bUcvdU45Ni9SajZOK1UzRi9Cdmk5cEVHM3JaSzZTRCsza1NQMURrMVFGM2FTVjZOTGNOVnhGTlB6d3cyUGppSGsyYWJ4RTB3VHlIaVZkV1NOZnd1ZFVJMTJoV1B2NEFYMktoNjBnVFB6ZTFQWlBJNUlNaDFYVzMrT2J6WHNIV3NyZjFCM2FoZlA4bWRzRC9ONzlOZ1lUdDNBMzFDN0Nua0NsWTgvOFlXUllYUStQM3kzUlp4N2llWXBtbGlnSUhMb1hGL3lRNlBqR2U3cjRIK29sSDgyK25IOWtRMG9DUVhVTURuUHczSm5BMXFXZFh5ZHphR2lVUDI1V0VxN3lYNkd6N2N1Vld6QW1lWWJreDF0REdmMnJmajRacVdlbW02SzE5N3QrMXl0RWJvNzcvYWErTGVRRC9TUzMrQ1RHZzk1c3JSTytIVkZWTHZzY0hSVDQ4RVo2T25XSFZMbC91bFJXdVVxVjloVFpMQm8vNDE0SVl0d1Y1cHd4NFFMU2xYd0NwR0IxVyt0MXFTc3JiVzE5SzM0VHE2dU1CbHhhVTk5Vm5KNWRxSWNFc1lHd0JKb2dJL1V2amdHalZDSFArVGtLNzlNWStnZmk4SWJndGJVN3dtNnFUY3NDQ214dVZPWEQzcGxvcDhISHkzQVIxeW9aZWh0Y2RIdEhqZDJVT3JHQWZ4N2VOSytmN3V0eHFPdHFCTU1PZTYvVDNtSU0xRDQzak5QODVSY3lLLzE2cjk1ZXR1NUlBNzVORVlialUwbXJ6VU9VQTkxNlJRUTB3bk5GS3RhUUo2TmN6SFRycEpYTW82cWduendzaGJpY3VCM2lsZTltUXRyZ0FWS20rb1BlRTY2QUNSK1VidUlaWUFiVkc3YmRWR1RhVTh1bE5KVy91ZWZPanRQUVBuQWlmRFY0bGpaZmNBTEhPaEo2L2FyTWNIemVuRWVYYkl1RzdwL3dsU2ZsUEs4emREZENnbC9MTC9HWjAzeUVKOGFkek8rSG1sa1lLMC9sMzFXeUhOQ25KUUwzNngzbXZDRXRWMHVvbVk5Yi9PYVZPNURVYUM3SFBkWi9HUnV5M2R4c1lKZlVxMElsTExwMW1uNnVoc0ZGS1l1Vm8wNkpPblNxV1RMbXQ5L2cvK3Y1Q1JlNWNDR0R2eXNxZmlYdzlkb0ZsVVNpemljaFZNZjhTcjNkWUl1L2JOTWtsT3Fzb1g1UzFlS0YzQlM1OFMvUnZvK3kyTWk4YnV0bWJFdXVHSEhoVVVNaTJ0S3JsRHlBUExVTE5rcmxid1FwKzJRMWZmcENBNTEwQ25kWkhGRUNDR1JualBjQmFsVjYzbkI4Ull6YWkyb2RPeS9XcFE1YlZuYVZsTk1NUGdRR0tCN1FYVnFGeDkyMUN6UDVwckwvR2NubzlxT28xb0VSVU1xOXRMSkt4TDdnQWVBSUZVSy9lUUtDZURIVVFGUVdHM0J1WGVxQWVsTXM5NytnQmFib1RwamRyelJBRlh1NWlpcVduVjYwUHBnc1hxNFBtWnhVL3ZBSUtUSEdoa3FLMXRXS0hzVXM5TG9aVWFmYjQ4ekp6VFJFUkFVYTNsTWEzb3YyQjMrbWd5bithb0lZTEZYYkcrS3Z3bG9RTThFZ05wN3RFclA4ZWRGT29XcnVpTkxxcEdwdUVmWlNwb3NML1YwU2lHWk16eE9QT2NFNGlvNHp4aWlxbGhXOEFwSnp3M1hiT3JsTjVyUFlUeUNseVZoakQ1ZE1aUVNiT0l0TStCclArMkl0OGlteW85aW15ZzZWYy9ZSUt3bFZZNFg5Yy9IbXhoWUxmWDY3MTBYcHNUVFhGa1Z2bDN2cGZxMDFSNVRYcnpjaUZjRGF1ZWJ5VE9DSXBTazhmYTRjVmpWOUxldlpWQlFvbmx1WTRqTVlwNFNReW80SDRXS3YrRWFhMEpuYzU2eEFQV2tuUmFtb0RUSWVVdGo1V2srL2xyNUJGNC8wZTdkK21iRzZyOGV1RVVaZnZicmFSZXlUSTRoZHduY0w1SVF5M3c0ZnhhRkVjUXZhV3BYcXVIZG1haGFiVUF2OXBDbDhMVjBaVzVoeEROVHphYVpJZlhIMzlleUZYT0N5ekplK0FJeUdMeWQwK2d3bXh5MU80aVphQjhRZ3lEbVRKSXJzMm14S0xnMFgwNVN3WW9GY2tYeHJHMkMyYVFpb1pWc2kzbDF6MlcrY1pZZjhtWXBtOGVUTHhaSjhvUXg5b0ltdHdUV3NtWm5uMURWWHQ4UjVjTGhabHhoQzN0VEFkWWxNNGp5Y1BaVHpJeU5PcE9pQ1BRU0RUTzhzQXVHSFI2ZWhQbWdlSmhNN05NQTYvMHU1Q2RhTTBUMUhxK015Q2Y2WGpjMlMvK0VUU3lDOXVXUFdza3hJc0llaW0zZ2VGZzViYXgwNEptenU4VGt1QlRkT2NubFZkT3FIVE5QOHdPN3NsNFk5NVZJWXdCOTRhRGthL3FGanZ2aXFJSDdUM3BaU2h3YUx6bDgyNE1Wc2I0UmRkYUJKdlZZK0FHa2RLZmdlcWhRZjdic01SeEd0TjR5WGZLTzZPZEJlNFlsR01USnladkF6VnJqNWhObndKYnQ5bzV3cEVGMDhWM2JNeC8xTmtBb0RmdEFvQmpDUnRIN2FXYitTL056TWxOZkhqSFA2K0hPNE8wUGJJSVg0a2RZWGpWaGdseUxhczA3ckZCc3M5M0JCYTIyK3E2ekF1VGtVMXM4UTg4dTN6WDhwbVJSK2UyUEZ5NTJXZ1BUNDl3VlhwU0tYNjBHa2N1RW8vdFYyZXhHa2dYb1hFN3gzU0I2SjNnNGRRTlRzQWlmN2J1T2FCRWRPaTJabGdxMXByc2dNNi9DbWpkbXZPRWVSdXlDdVpQN0Y0T1F6aWpCSHg3Y3ZmYnNpcEsvOHBnbWdaMHdJbmZvU2JabGx3Tno5RGlIWWRjSm1xY2RjSmpGUUI3U01OZmFqRGtsYnl6dURJc1lLUDI4YmNwcW9zVVBzTWsrWDVDWTlheTYzdzdOcFlhdDBOT3J1M2R0SVNkMC9tN3prTXlPWjArckhoYWg0RE5Lb1VlaGVlVGRDN1ZOOEVYa1ZvcDUvVU9YVitZeldOYjhLWlFENzhHV3d3SVRNanBOVG5iM01YeGhLb2NxYTNVS25uOHltRFFROFBkbUNpN0FWK3FvNW01MDcvSVYvbmJzVnFoWkxiek0rTWhlanZqejdENU04eDcvNnplc1BRa0ZlbHVqYjV5OFJmQU41V3EzeVROemxBRllXYk1RY0pwd2xVMkUzOTBYUVJ6aXhKbkNPalZRT0I1QjFEZDBGUGFKWTVIN0J4cnpaSC9VbElFRVpGOVhtenppKzRRbTN6YXUzUjVSL2lkcHpna3VJb2dWZW4vUjNVTUZWaTFUNlYwT250T2JWNHJyTnRRaVgzdGdmMUUvUUNKNXA1czB1Y1JQaU9KZFlzZFV2MytBYWJIUzhFS3NwUmFmSi9VRnVnOWxDUUNNRXZCcXgrNXZaWHdFQmZuL2JuS0RaN1ZWMlpYeUlPb1l6eTRoUzhuOFlqREJNYkdvWEsyVGhFY1BOaDNwcGtvemN3bjRoNUcvYWdScFZPdG85WnJXVGgyMEdXZkR1V0hNdnBPWWNHWEFmU3lVU1BNYzYrbTYzSG93aEk0bmoxNFdsR1Y2WDFDT29wTmxrQlNSQVRBb1NrT0hQUmZXL0d5NkQzejJuNHlkaVdEcmxWZ3dzRzJoMGZDeTBvMzRYRnNOTXRkTzZ4YjZqVkFPdlNGWkZXYTRoQXhVVDIwaFMzMUhkZERhNHBkdW5lNDl4amVlQnVDZDRUZ3ZudEo0NmF4cEx0ekx3N2tXcysxUnBBNnVsVklwQWY4eGxNTU1BWHlObDNnV05pR3V6RUFLWUxzM01OcENrZmMzaUhRMnIweU1KdnR2aFpkSERiWWlHYjNONnJvWm1YcjdSNW9YbFhyb3RmbjRkYWJ2bzVpMStqZVNodDdtV0h3bk1kYVltTFMyT1ZDV1Y4d3p4dXJTSUxLL2ZPSnQ2aVFQOGRUTU15bkhnWGJON1ViR1BzYzNRZjAyWjQ5OHZFZmpjUjI0YzArOTNwWFBwL2dJQ0FnT2IwZmJyYU9iMzhBTnRJd2ZoMlhpOTExL3pXNlZWRGtldlFDTEEzZmU1UlROdzF4L2JaYmdpeDNHRVNqUjEycUt5TW1ZU2g2TEZjaEc5SzAwVWZjZXhrOUhwVXdqSEI1VDJhT3grZnBrYm1zdVVyS1BNY3pSNlNSVW9DVGFWcGtIZEV3dC91OElCQW9vZmk2WEl3U0dPdW9mV0xqRFY3UXMxZVVwZzJwcE1LNHVXQXJBZ0tQUStuZHo4Ykx1WFdHVkdFWE1XZXNteTI5ODVhOXB5Y0ZZSysrc2tHMllzd0xhL1NKUjhyZml4Z3FHOUlQMnZjaCt2QTBpN2J0Ty8vUjdXVFNBUnlzd0hrazJPMWJqMW9TWUdpcUsyekw3R3Z5ZW5hMFRFSmt1NmZQVWlMOEE3WFlTMko2N3N1clp5aXZTSGEzZmlMZWJwcnQ0K053d0hrVzI0ekRBQVVOWWdZb0lFTkRTZE1aSDBrY2Rla0F2NDRMUjJYYTRHOTBQWEdUeEVaamtVUnRvUEEzaGtveVh0Uk1zZkV6cW51eStadjJVamdNellZTHZ6M3FRSHk1UHFSR0haRjhaenNVQUVDM2V2UHFTd01wTEs4NmNzWDMwUGh0eklVZkM4TS90bWh2cHgxYXhjRzQwaEUwN1pEQmlOUXl5SXhHWUdOcEVJbzNSemIybFY1N0V2Wm14TFRsZ2cwbGJpejNGWEpYeEdDZGhtV2dFY2p1a0NMUEFsVElGV3h5dVJRc2ZORUJ0SGtjY2FCay9oSnhWcFNzUVlkNDdOWXJtMlMzckZzalFHMTRuWUhQckJyL3JLdmlWZjN1RG5uRStYQUpKaVpHVWV5aWZqU3h4VkNKMVVBaUtldm9Yc05LR3U5RHF0VHQ4TWFqOTJMSGRGNE9RMEUvem01aDVMd2I4T0VkTzdOWGhPMFhwTUQxTHZuRDUzMk5Pak1sRUU3MXd4U1I4STROZ3pQU1dVQVdXZk5aUnF3S09GMldSWGd6dXVpU1dUbTl5Ly9MVnlkTDFCaitSV1EzM2ZlOE9RdDdod1F4eElPSUQzajlhV2RXczNaQ1I3Rkl6MGFwaUFjbGhZcWZEOWhNTmUzS2h6WHZFbFFQdGI3MjM2L2plbkEvVTFXZmc5TUZsK2R1RWRJUmpBdUM1Z2NUTVRhZTJrSkFWektXOTFSZTFHY2xaMm5IV1ltVTVibStVa3NNUXAyYWVuMW5JeEM4RDc5dFBvL0xubnlSMWFoTktSdkhrYmxrVDRVQk5QbUp4bHMxbEZPa3VjL3c3c0JraEdzei82ZUFFZUxuSm5vS2NGU1ZveU9na2hJbHkrMUxISG1seklBRGhEK3dadnAwTVRFeE9nUlRXTCtkcDJlcjhuM3JncHRnMUw5OFpldFNLSHducEJVSjdOTUEvY1FhNkRqM041aWxQWUV4NXlvZ1RPQk9mSjIraUpnb2xUN1c0UzFDOVpyK3Nuc1hMUy9OdG1DaHJwQ1dsSGFOWGgxL1ZibmhJOUVRVy9KMnh3cXR0dnJ1SnZoeGZhRC9tblVIUndtdUF4R3AxMWZqZ0tUMFYrdElWRExGeVZzVktoWTg0OVZ4VVlweXdxdTJLSll4RWlFSmpaNjRSRGxscEtWYm1sK084bWo3TWJnODdvT213bzBzdXp0NjhlVnR0dWl3ekphbnp0dS9RdXhuaWdadHhoeVVxSGdhK2RQV1ZrUmsyeUhFSS81eHQ1TWQwZXdNanNkZmhXZFZ2OTdYUlVwcEN5M3lYRStBWkNJZlEzQmNwcll1WXNnV2hCRk5UVy8zbmkybmVDdll1UkcvRGh2L2s4aGlJOUU2QXl0eDFhM1RBeE9TemNJV3lJeTI4S0dZRzNIcW5rY3NIaGE0b1NuNWd4ZE00RnZpam1IRkhWdzFFT1RHcTBiNHRwdDlJN3RpakhXejVyRlFobGRBcTlvR3lMc29pQWVNNFMvZUlrcVN5a2FRMjRTZjZhcTRDR29mL2haMGNwckdiKzVTUE9zSGN2VGpHSXhTa0tBMHNLakN4SHNodDg3MXlHbEUwb3pwWXJuWkEzSHY1cklWNVhLTWpZRDRXWHltRUFUM0U1TzRTbDRmU1c4VHFWS0hEU0RnNGhBaWlXRjlOR2RrZkNRNENIVkt4K016WGRKSXQ5azIxQVVMZm5XRDJ3dUV6TUs0OFJ4VWVORnA0cSsyUDRFZ1M4Q0JnNy9UQllwblVQOVdQbXZjcy9EQlI5bGl1Mi9PeGJhM0xFK2MrcGc0VVlkS1RpWEVnSzFLbnZRTEtQOVl5OE5UK29XS2xWRjZoTHhZZWx1YUsxSE5XNkRUaHdKQkdhaXFuNFRXU29CdE9WYzVxWFZNMkFlRjRuS3kzQk5yTG1LZG1yS3BMaUJhSElVRDdKUzFUUTlGWnJFMXF3SFpDSmtrRHdMU2dJZDA5bXhjdDlKR2RPbDNJNVI3VUJSc08yTHRDK3R1dGpKNytnOW90M01FKzFEa3Bid0JIUlBraTQ0UVZPdFB5YmlreHhoVXZacndXWkhvVWNqeWpDZC9CWDNqRU1zYXpKYzEwWVR5QjluQmptN0tPZWRCeVh0c2pSUFlPRTFFdmZ1N25saTJVd2RhaUhNN0dCYmZ4THltd1NOdVdWS0dXZk0wcXo4MG9LLzFrMmdrNkpSdVdQYkJCSXVHUnNZUUhPNGdUU0pPL1kvMnRVQlQ3dDd2Tk9nbiswdjNkZG4yQmxmendLMUFEYVd4eWFRZWM3TXV0VW1pZjFORWlGci9rVk5CaURoZGZxUGwyZTE3NGRrZjAyRm9DV0pvbUJlL1UwRGN1OHByQmZxMVVzV0tGaWZiNXRRZERqdHdrNmZGdW13eHBjeE42czQ2THJmZ2RXaStFejdOOWhVVDMxcDFQQVJjWFhRUTFCZzVta0FsSVE4VVlreTNock1rZERpdGVObW96UVRtZDN3QnZNUmhOODI2dUk1bXM5SFZDbE14djNzTXZkSTYvc0QxY3BxVDlHTlhWTzYyT2FJdzY2aVJ0dThIK3JjdUtocG9yRVRyV0I2ZnBMRFBFQnJZakl2am1RYURGZjArbnArcmxodk9ZQ1BTeCsvQjlOMDdtY21UL09PT3orREJ0YVYzY1o2aTJTc1NRU3NTaWNvaWNtZzIxUHN1bUcvU3NQUG9xWVE2YXVyUFVMTCsvNEhsVjIxSUdUOHlrSzByWE4zRHBHUCsrR3A3OGF1UXpRN0tqNXBzeVFRM1BnT1o2bWgybno2d2lNV0F6UzdYTjU5Rk53aE1ZMzBnQXQ3c0JRRGk2bUJUdHlLejBOU2VacVp5YXBBZytmUVhoZys4cUJZaEJPME5qd3d5LzJCNUlpTkQ0UENlT3BybjZERkM1VzNOdGRhOEpUMkdoSU1UNndUZk9OTEFnbUQvMGxtTTFpblgxaWh6RENwR2hGMUJyT1F2MzQ2ZHJMNGQ1M2l2M3pTOWlEM21Hb0djOEJmR3ZuOHdpZ2JnWFAvT0RMa0htT290dWVZZ21PMWZsZXRVMzJxWGRhWlRsZHY0NDI5K2xLMFl4UFpJZ3UrQWQ3aFFZamo4MFpKeE9DMERDR2lXU1BVbGJtRklrUEowcytmd3hYZk96V3ZzVkkvTU9JaFFiWDFkT2d6L2NlbnM2dnhZbGpJVFdrNXk2VlpvanovZFM5NmlCOVZlUkMrb3VWcFR0M2JYdmdTUFNkY21Ib0JUZ283RnF0VURwTThTdWNoV1JQUCtld240RjdPdFkyRmxtdjNMMmN6TGUxcGdtbEViMXcySjlSQUwwWmdsckpvaGwvQWJxWCtIUkxlT09IUkZ5UVpQbVgyUFQvMExSRWxHeVhwZTFnaWhTTGk4blB5RUJpUk5uUkU1cXFSYjdwUmppTnQyd2E2aGFIdmVDVHV6VkNFaXhsQk5aUFRpaHM3VW1yTGxOdVc2aWZqWS9UMFY4LzVLbCt2RFhjeWxSN2NxSGVESHlDK3UvZW1jT2RER0Q5UENIUnZTTUZ3ejZJcmhBMW10WHhmaVc3UXJMY0RDeVZnWnJ3R0Z3QitPdENuKzlSajRSUkFiR3VydUlvZE5SOWMrc3UvMDRORmNDblBTOGNKaXJub1VtaExGVkd0cC84enplenZnOXlZZ1BrN1pjR2NXNW5WUDFCcE02b2ZBMVEySGkyY0ErYmV0WEpGRWVoN2ZId3dmdlYyclM0dDFwNzBEeGZFUUtFQWY1TnBqRWpSeGZ0d25mQWhUbEpEajlCamRDakFRUDVCV1BNZlVRUk14WWtNNlVXcmV6LzV3TXJ6dm9wTWJVeS9CVDhCaURJZWFoMU1XZTZGZjRwV0lOelU5aGtHRndkYmZMVnpzOE83ZjFua2RFSUZXZUZ6US9RclYrb3pNNkx3QlRIMjc2M0V2N3lmelNablNHaTFKYkIwT2xXWWVoTkJTVDJGOHZhVGxaNzNJVks2dEhEQ1hnajdzOG03UVJTNWhOUlFqY0VJRUh5M013ZzRVTjQ2c2ptdHZsNkYzbkZTNExGUmw0MjNmeUpOcHB1VGE5V1dTR2F3d0ZXQ2doMFJ0WVg0aWg0ZGZXUFpZeEVpaEpWTFpUSGQxWGJ3L1dWYy9XZTc3S0hyNno2K3RZTjllanY3TkJuOWVobjIyb24zYWR2dTBmSDc5SGhRbmp5QnpFYWMvRTlxZkFJRldpR3UxQ0RwNldreVdvRHlFRWlZNzRhVis1d1BZVm9lR2hWY05STUlKOStrWkxNdTQ2bCtEbnFnT085Qjk2Unh3Zng1UlVQRzVUbjNnb002Vng4YXF6SzRGVjdHSklsaEhPSDkrbWlTbEZBODFxNzl0SVhjVXM5Yk9scXdlWUV1R0owMkt6NVUwTTlRbXlvRDZDM3AxZGFIKzlQWXNUODhIZ3JnSTFvVGIvNmtJTk1MZzMxV3VMS0gvcHhmVlJ6YzdtRmdyYU9HM0lNNk9ScVhIMVJhdGlHZU5oK0ZYNWNhbElzdnJQQjdQNS92K0NTcHdDc011RjVsUkpLcVpkSXQwdHFzZHVKcHlOMDBFV2gyN3dwT3BEUDdWOWZYNlkrODI2VVZPY1BSdi9SQlRCZnYzWFQ2a3FGMlQ3ODVhempjYitlMUVMV2ZuVlZiYkNwQnJSYkJseXo2dFJibTVCaVNkYkhYZ3JPUW1kei9RbXJ2cVNGTGN5N2RZTkpSQ3FxWnBnQXkzNnMvbVBReFZIN05WVEJ0eVZIcDloeTdmMEh4ZCtMc2ZYeHE5S1AwcmR3RU9xVVR3YjNzSUhYdlBHQnNBbzI3aEFKWFZYR0ZiTmZqRzVEY0JwV051NDFZVy9wb21CWkQzdUY0UzVoeXNoWEN5bkhjK29sL3FoL3ZxNFYvMXpFdnV5UjUrc2JoUXliczRMZkl3V24zb1hESWMzUHVlbHZhQ2Y1a1lyMklkMVJiQU9zMmM5aEozSHNsNjArS3pZaVNDcVgzLzFwNHJGMmhVT1dNbXl0ZHpPNHdOZ2hrV1lsdFlDU0sxL29YOXgyS3ZMSmZOeVVGZjQ5dXhPOWl0ekFJTTR2ZzlBS3ovejlWemk4dFlCRnNiY0grUitRcHRCMlQzOC9OTWVhTHB0UG5uOFU3VVZnYWRmc056MnJpa09IT2ltZWFkNndpUm9KV2dIY3pPQ29tbC9DeTRaci9NTmdQR290MVpuSGJNWi82QjFEeE5ULzU2dWpoNjZHLzFnT0lrT0FVcGsvYTRvNUpDckpXNm9UWE53L1VwR1BaV0ZtRy96dVVXck9IUEEwczJVNkJDNE9rWlo5TFhqUEJic045S1ozNml6S1VBVXVhOUdESU9aeXN6eStYV2JJZCtrZmtWeDVLdk5lNjFBN1dtVWdJQVZwVUoxeElPc1M5aVUyeWRnQUdMeDNCcCtaMXlFNldsOVBlQ2QzdDRmSnJmVXhlNElKRzFJNHN0a0JVd2loZGZibk0wMyt3NmMrbTZaTFF4VEFhakYzQXZsSTZLdGNnR1RrclVYSXBXNkpKdXVrTHQxM2tZekE2L21IWUN1UGNaU2ZLcC96YW0vZDNsaUc2YVFkWFJUdUJjSjFQQTc2ZHpTaHdyeno4aTBmdEk0cjRDSkZBOXZGcHNWeWN0Ulh4ODNZalh2VlNiRnpQb2VTSFA0aUdZYnhuQVlXM0lQb3ptb3M4U2h6N3h2QXZTaXdVNjArYWl0WXp3MHVTV1Zaem15YWNhM3R0VlZLSkFsWXMxUjJEMUY1NnlHZ1AvVnJ2M3p0UGQzcjZSRWhNalNvZzZSbWFrTmZsTUpFbmw2ODlzZUMwbE5NcVFkRlZaNXlNdFc5TjlBTThmcWJJL0U2cm4yMGw3L21NaXFiZG9uMXpOK1o1WEh0blhmWnIrRDF2d2NWS3ROaUVsRjI2Z1c0RXovb0tTTVdjWGI2dlcrekp0aUpkZXdKVStnRHJybXRVYjYvSTNZQ0l0czEvbE9rTG1icWpiS1g0Ynd1MVdkMVlZb1hiMWV5M0poVDNOUUYzRm4zNW5rOVkvQnQ4Z3MwajdYVGIxZ3NHVStUT2dEK1ZWcGRCWlJSak1iN3BjT290S2dYdXFMenZZc0NIUkNPN21oc0o4YkZnd21TY1R4MGY1eHEyVEdYZStTTFhvbmZXdkxGOTRjMFhWaEpNUFhLdWZEaEZtWVFkbHU0WWJqYWpaMG1HTTJ0QlhGbU1VcGpLL1pQUS96MmVlZ3l0REFtd1NpSXpaY1FnYm16Mk9hTnJrem10QzB0ZlA2NFA1aTgrdHZtcmRyVVdiVmx3ZHhUMm9qL3E4YXk4YStKcjJsN0RuYmw2TnROUHV2WDFuS1JyNzR4bG1PeWdhVE8rWERMMHlHdVF2T280aTZhdHR2aE03QjdadmN4WEhzKzhWMk1QQ205S3NVRHZjamgyQUR3TG9LQTRYSkpsZWp4cGIvOU8vM1F1dmZTSzhraTRrVVplSGduVFNOTzlmejVocG4vMzAxU0JndzVGc2NNWmhuQkE3SzRTRENuQ2N4WGlNM09XbWJkWVJRWWR1YndCUStRVTVtTmM0UTlMNXZ6SmhBQ25sa055R2k5Q1cxcm9veFhtZS9WRXpFYWxmM3R4NnJpVjlHdy9iVWtoc1VLeDFEMnBNNTNwS1RKYVdzYWRicUhTVHhVc2R0UFBIQVpyUXpKckdNdjVrZEJVeGd1RlpZV0Y3V3NWaXVvaXc1U3duQW9sanRpL0xQRmxoYk1pd1ZxTXFtaG5aMXBzeXNUQUw1SjdvTk1JdlpsUnFULzIvVndreDAxR3loTGFBWUs2eFhsb1E5eHQ1MnRmYy95UWZyYnFkK1BOZHBGQlFOYlFvclA0VXltRVg4VndCWC82U0hVVC9lemhkQUZYQ0RZSEhNazVpY0ZYcjFCL21nZ3NtQzJlTjlVTEhXNFYyWHlFb0pRUmc5c1Fyb1RxcnE5TE0vV3IvVHMvUVR6VmdaTU9YYU4zSFhkcTZzcCs1VndCd0s4RHhLZ0YraHRBQWJyYzltOWxRQTNtbXdhbldMY1ovVzdsV0h1WkxiUkFlQlNGekdJSTBORWNhSVpWU3lCU3JZOGVLbUxkY2d4L0lwMUk1UStRb01LejcwRWM4UENWQ1FzVUhLUVh2UHVFSkNxeG04VGQ2RGFaUW82TnFObzNiSURlVlFtREJPOXJKQjBQWkFXV2ZTb21SZXVyanJ6dUx2ZjdOTm5vZWgwd21wQktFT0lXeUVQNzBNb3V2MVRpMDhOUk10Q2ZEQ2xpSWpDOUVhMWR5L2ZFZEJwQVhGQ0FxNnl5U0hTUUcwenRFd3NpZjZsdFJiNkgyKzRyWTdyUjhkT01EWWRsdFdDdWd4aHdGQVJLb211RlZzSVQyUW10aytmYWhpU05tZTl5S0NOeVhwdkUvUDZ4ZWVKRm5nU3FuVDhQd3B4eVNqTXorWU5kTFA5b09rM3ZHWkxzUjg3NEpaNEU4dzN2QW1pZTZsczFaL1FqY3NISnZIWlU4Tjg4ZEJBdSs1NnpRZ0hKcDhmT2Z3U0l4dkpGVEFhdkxLeHZHMkE1WEJJdlZsWUtIUlhINm9UYXhtZ1NXM2xOZUdHMmlaanJOM0R5NXlSNFd3UGJTYXJ0ZzBLam9ZZmF1RFVYQlZuVHJRNDdPUjB5ejRjMVh2MEVkcFVBa3phZE9Tc3NnT3ZacERwSXZ5Q3ZyN1hZeWJYTDV3eW42MXRxVitKamt6UmVHR3g4VS9LYWtHcWo1Wi9MdVk2SXpPTUNSbmlwSXdiU1drSXZPNnc0NExURDkxeHlRQnRIbGkzaXo0QUxSR1BhbXVzUWtxdHZTVkdaNjZwNUNxRTNtcERwb1d0YlNyZTB2eUFjVmhBbkt4T3FrdWxJSzJianBrakxYUFdlMUJrck5kVzVVMmFoODJYRWtaWjB3VTA4ZCtuZEt4aGM2U1lEenB0MTU0ZlFJRjBsUm1zRE9tZzF4bjVKMHVYRVliTmw0cHBHemtycUZ4OVNDTElDSUZ4SFFxdHFiekdwNGNmUktnOW1kMEVrRjVTVDVRK3Fld0IyN1JjZkpFWXVKckJZSVhObFNsd3VZemFneHYwVGxHUVlVTy9LSWVXc1dZRUxIMExha0EzRFRESjh4MDNpdnk1NEJRNTIzR0FUMndrWUVUMWhOVzdVU2RLT2NOckc5dWRxL0NvVjRNZEF5R2tRRmpsVW1wdUs4ZThwaEoybGtrNFkwNGg3dHN4OHJGYjVKOU1PU0RwZGF3WVZ0UGtKOWphQ1hDbUJMaEdKYll2NGFxVkRpVlNRR08rVDN2QUlVL0VxSWxEa1VrVzhkenM0T2xvbWh3cVZ3WUJJN29XWnhpYzBOcUgxSWMrckJEYm9yWnA5S0hZZHQrcjU5cWpwU2JvTnNET21RTzZRYzNYVXNwYXBYTWJHWHh3M2hzY0hTYzFDbmV5Rm5xekpKaXRwWkZReFFTNVFPdGg0YVU5QmE5cXRYM2t2UkVySEFhamRMSUV4emF2bTJJM2t2MDJTb1U4VTBLV1hBUkJWVWtKa0xrT2djelI1QjR5ams0SGZpN2RZY1djNUd5N0JDeHpJOXBjaDNUcExYUmRuVEVDUWphMG1lUnZjSFYzdVN5MDFsR2U5a1E3d0dGdDdyZjVpNHIzWldrS2NBOE1xMmZac3JnODRqYjBQWXhDdVh2aGx2Ky9wcUFiTzdNT3l5R21nSk5WYWppMkh0czR0OFBxb0Z3OHhNd1llRS84MHRrcmdQWTJndWdTemYrVVZxYVZlMzg0bVBPRFk0TE9PL1hwalNXWGF5MHNsK055WjV4M1oxVHdudU5pL3VOb1oxVkVpUVNyM3BzZCtSdDZmTjRFaXQwb3Jkc0E5UlpPbnpSUm4vTks1aEZ2ODFBZzBGN2J3ZnBuYUEwT1N1bTJuVy9PMmVyMGFMSDlpcmRzWGU1QU5lVmp4Q1llZzdyUE9MUm4yYStveDg2YmdRQ0dOVkZtRmxIbWxrNkcyUnhYYmU3eUFhNUd4eE1VK3lBUEJURmtEcEdyS3ZuanlRVy9vZVZTMHlZZUtlVHNubmRQc3ptUE5RamliNFJxbWlHbXZ0R3hLbW9DUmM4MTZpRnlJRVA5Q2ZUUTcwdjdJUEM2d1dnTk9LVG82a1gzTk92ZXpON0ZwdjhzTk1OR3JMZTAvZDR5bW83WjVjSy9QMlh5M1BIcFVqREp4NGZMdnU0S214bmNEeVdwL3pUTFhJRzF3ZnY5bXFMZ0JHYUJ2SVpxM29Jd0VMRjJZMDgyMkdjRmwwcDVzb1NvbElYcGFPUUlWSGRWcS9aKzNhbmdjTnprQW85bzBGVVRtRnp2WktidFU3bkZSbklSUWM3SUlLUWZ3S3oxL3psekNKbHhSSEVOS011Q1JmNjZyQWVpRzBYNmpXSG5kTVFzWEtFb1o2YkdpWGpsS3Z4OXdQSWk5eU1MNklNZ0ZGcDdDZHQwTjRiUi84NUg2dXdCME9wYzFZaS9PemZ3a05GRUN6QlpaaHJUNWFnaVJ3cUFkeTMzcUovdnFuNGtxaEJja3FHSU1KcDQwUGVIM1E1RUxjN3ZuK3BTTzQrd2RnWmE4U0F4ZHdqMnBaOHZTeUQ2bE0wUXZKNng1dFoxcGE0SCtlT0tTUUF3T3BnRHRhWld2WkVzQnYrTmlkRm5jOFFJQlZkcGtqVS9Md1BkMHBlQmkvRDJYeStrUHJqUEJpa2FBQm5LRzhKNHdMR21YWjdSTHpJL3MvVmgySkVwQmwxTnBzb3cvelIzTEpzWW8xRXgvNHBuc0hnSHVCSEp4YzVyZmNGdWhWaTZwZXFKTEtWMG1YZkdYRnZOTENSSERYRDVXM29XT1F2dys5YjFkMGdjNnova1NyZlV6dG5uSTNTV0ZnRFFyU0tzMVdGR1ViQ245NmVYQmlpS3pqTjhGdmFqbXgvcDAzNjZrY0lYczZ4a205Q3I0VklnQTdHTXNsK3NhVHo4L3kyWUhxNTRha0d1TllWakZlY2dUUE16WG10aXlkUm1QY3hZOWpiWmVORFFJTitjUkJRb1pqR09UZ2hZMDlSRGZ0elo3bklmbkU5UTZ5RzJ1TXRkU3FLNVZySEdMZk9PZDJpSFFIRGxLR3N2UnVuc1c4S3hZd2FiZjlSdVkxZE1FQWdXUmp5OFVheC9pV3ZFaU9waWtXRnIrTGZZZ1IwbXBXRFpocndTeU1EVlNWQ0w1NGpNSjVXTFVieXVOandublU2U0p2VGJjYUpVR21ReSthT05lbG9pcmkrUHVoaG1GTUFaU1VZaHE3L1o1alIxZDdUZ2hvdTRDbHhuTStRQUZ1Y2ZvMnBBRFlDVU4zSXhGZ0k4a1BDTVdacFNKYVRiSTQ5Z3pDTTJ1Q3p4cHBjcUxkVzkzOEFKNlNuWWR0UUFkUGVST1YzaUtBRFpKTTl6NmF2cGhJNUZCZlhFNFVpcDZuelVyMkpXaFp5cXEvUE8zNWFxclNob0MzUGVIQzJpZnVNQlFBU0pqdjRnVzhjRS9udnRVdGVRSjg4ek5IMGtNNllpeDhHdUZ3RXA5ZmdZRE9vK01RREJRVGFKSTlNei9XRnZmOFlGLzVHNEgvQnhqQmxFOUZGRXNQTHVodyswbUFLREJPV2ZqZFJPWVN4OEJLMzdRWDZJVCtCTEZJTnJCaDhrcHJBTktXTDBUMEJpWjRYcHRILzA0S1E4U0xiRXY3THJFZm0vWjVvaks3VE1wU3FKeG5xeWcvclkwRERhbjhBbXFZWTVqcmphcjRvWG5MWUZ5V2p3bkZORk5PTjFuOEpXQk00emk0L2ptTy9zZ2dKWmJmRlVSV3lOQXU1ZGExWEdPZ1NIV0pROWIxek5vR25TNlN3b3dOcVl3NC9Kc3Y4RmMrK3A4eHR4VitvSzVVd1VPWkFFa3ZCZ0tXallhTlBxckQ1Tzc2cnlCZ0c5aXFxT25HWTJwL3ErM1pEN1ovQi9ZbFY2VFY0dmtTN04yRTI3WndNQkF4cFZFZllMNy8wclBhbEdCRFd0Qk1YbFFJMWtuWHNvSEtFOGQycmg0U0N4RXI5MTJVT1V1bGRnbXNxVnh6R3JHOW4ranhGcER0c2JOSXNoQmEvalZhaTk2eG1nRWZ6QTRHMkk1OUZHRUl0N2pUTmZXOElaRWRydlhFenBEQ2FkQ0lMSzdERGFGb0pOdytVNlo4VjMvVk1rUklzUWZXcU9qR2I5U0J0L1BFcy9qQTYvVmhGM2xPQjlUbG5raGl0K0l4UXd6TitBOTkraHF6eHFPK01NOFhZU21VR1ljdDBCbGozcmRZTnNmRE01THp5cnFCeWlyU0hpSHl2U2FpamhQQjVWTVovS0cyeWVkR01OUExLbVk1UThVV29wdjZXZ1NFNm5VT1JJeFZMMDJPZUo5Q1RVcDVybWtwVWJVbHJ5elBrZlY1WnZTV25WOGh0MU1ERnFWR3lNU1R3WjNTWWY5YXdEWXBGVHpOOUlYMUhCUFJRM04xV3lXK0tNNlhsMDB5aWxNWGVCUUxDVU83RHdUc2NiNXNyM2xRYWtudXg3YUhkbEhWQVVxd0dYVVRLdXJuTGdVSk40aDlPSVdOcEVCYy9CVHVqS3FtUWpsMTl5R0hpZXl1eWdhYXJ1TDlJaUM5QUlOeXVRempDK0VESkJ6RVA0ODAzTzZ3WjMrUm0vbWY1V1crNWw2RWVvL0huY24xSG5hUzRQY2t0aTkwSjdyVGVSSldPbWxZVnNwdjhUVnlEMkRzai9IZ1crWkFhaFRGQVNLanBZS3NjQVBMd21BUmNMVWQvUExnR0FRRFl5YkVac1IxdDE5QnprTGJZV3pRUkEzNDUxaUoyWnE5TG9pZkRBdHFQazc0SzdCdGhDVDJyZzFMWS85Yzh0SjZaRmt5UnQwNWdmdFkxWU1KVks2Mjh4RUxxR2k1TWFYcHJnMVFGTHlSaWpSUnF2cEJtSk5GSkpEWEpmU2pmVHYrNmhzTGcvN0RZM3gzM1RxNFUvMXBMS2NpdEx5eWdlakh1THo5eTZ3YkdKNUkxMFk4REF6WDNtYzBXYStsLzdtNUtTaXZaZzI0eDEzQndRS0JIdFU5NnFoeDg5TjBSSjdScWkzT0g3MnVqc01qUXB5Qi91Q3dmVGk0TlNUNFdzS3NrVHI1TVhvZXZRcXM4NGdIdm9aUDdUVmdxN0EzVG1BVVdaSWVrVGNPWHpYdXEwb2NQaGs4Y3FsZWZRV1dkS01KZkZ3K1Y3bmxtVm1LbVI4aEExR2o5bWFSVFdPTkVyZyt1b0tONVRzUUR6WkdkSGg3cXIzNEFUenlBbzZtNHZieUdMTks4WGtFZkdKY09ScVNwZ1R4WHloTGxKQVNHZ2EzNzVLVFZEVnBWekQxUm9OazZEaGZwbFNtZlZob3M3aEJVYVQ1MjZEaW1odjNhLzVzRzBJUExNRDNTVTYzTzNFT0xEa1BrQWtoWk94VFFxQUt2bS9NVDA2ZzIzQTBnRTlpT1o1bzlMUkRrUmkrSkJIMzdhVmFoTFNBajdWVVA4NjNLaTJsVC9xbVE5bzF6K0JRRjZNWEg2em96QW1UU0QwYkJVYktoNWxEaThJR3dBd1RDUWluVm9ZOTlxOEdTcG5oUnI4QkFZaElwRkk2MjhrMHRGV28zMGREZVBFZjR0bERqelg1TVhHNyt4blJ6ZGRYU3hMd3JXRmErYlplZnozUEE4RzNJUFFMdDl5RXNNMjRkNktwdVk1WWFUSDBuU2JrZnVjZjdTeDBzMzBuKzlxK2hKK0ZqM2NWRDRWOWRGNDM5UW1wR3lXTVhrTi90YlY0ckdIelZpKzF2Q0NuK0h6dzcxbGM3ZUZFWTdSbDhMMVVZRDdmTVg4MGFlYUwyakl2L1pnaUNGMW5NRmMzRm9IbzlEQTFkOW9jY0txazdHZUtRbnhMMHgrSU5HandnUlgwSlA5R1IxbG1oaUV4RDhqdXZoREVtU3EyRFBlVXliMnkySXhpWWQxZWhIV0hCQlczWUpXQ0lwbXFNYXExQlFoQktBRmkwaFExY2YwSnRaRWp6amFyY0lwL0hYb0NaM3BVaGg4ejI1czhWVkNaT3N5RmpZemJycFpHMkdrb1Z4R1NuTVYvSXhIb1ZFV2xST2xHNll4VTArVy8zYTdyMFlpOHVtbGVJZXBITXR4VDA3b1RlS01GUEZsM0hSUWUyN2I1YmhYWElNN2I5ZkJWQXVYRGQ5WTdDVHlPV2hJYm5kSGtBU0FUOVBlRWU5aVZoYTl1MzFKOXFsU245Yit4MDlUNWk5a3JoOGluWWROdHFUcmQ5KzJ6MG9nNis3c204dTUvQmpLSTVob2hpeVhWMyttTFNuTWl4RmhmaE9abXBSaGVMVG1zeDl1U0RXV252QW56ZUtLR1BXeUJYb2tZVzd6ZU9CUjZneHZhcVN6akNpVC9VOUN6RTE0WjhrczZQWW5ocVVyQ1RhS0FtbEVlMEkvZ2o0cHUwY2d6dE9sZmRubGZ1WXlsWUJHaVlUK2dKc0ZQUWQ2ZUcwSGFYY1lyNVFlU0tjNGhoRmNEMzI0WXkrdDVoQjg4UVROREFSb1NNck5pUGJVRmpsclVPMllXTkpaYTJvR2FSczdYUVpQK0xkaDhmVVQ0cVNKQmVtR1lpVEdHck1vS2lUaXNheFJ3a0VyMmxaTlE0QXdrdFF6bXI0c1NhTWk2KzJZUXN1T21tY1EvdnlOQXlaOUxLQ1NjYXBUUkcwRzRiZGlobXF6N09vdm9xc0FGRFFJSTgzL0JwVHhsWWprRmNZZHhSWTBtdVI2NFMzQTZDandrRC8xeUU0Y21nZG5vNDV2dnR3Q2ROeC9OeG1jZmNWYmkvNTVCTVRzS3VLejExT1UrZ0RQL25yZWx2b0tMT0IzZGZxOWNSdFhSdXlVbmZpcEhNbVJQbi9KOVIvSlQvSDlhS3NuMzhWMmFzVmlaaEExS3VHNzg5N1I4bElhVGtLait4eGhrL0I3eTZ1dGFWS3EzNFA3ZnlidHFQWkhRbkVjUzFldnJkbXlUREp5Qmc1YXZ0dksvdjNHN3NSMUR5Um44VERTTTREUVIwdGpFbW5OWmxtVUFvSkNrbDBXd1h1azVzcnlBZ3Rxb1B2TFZTdkIwUFh0WVA0aHRmbUtSQWpjZU43T1NScXYzR0Uxa3VTdkN2TzJVdmQyWW0rZVBVQm9LY0Mva3psdlVNNHQreFd1NW5kVHIzWUZFb1NkeENjdFhZR1BMQUgvR3dZNFJ4U3QvUDhsb0UwRkJyZG5tOGlaZ1lzOTJLRGptbmd5SjNwd1RtR1pqaEhXVGVOWTNZNlhzaUhxMm4xSFZuN0RVdFRxQ3U0dDdlOXRXSGl2K0V4ZmVDZmtHRDFvZ1NTKzRiL25Lek9uVWtmNkorbkhGUENFT1V1OWxndWZzUjJNQjZvN1orUlZ4bjI3Wjc3cE9uQzVXdkJ1eWZNUFhNQlVSSnBPakg5cjZRR0VqaEhkTWg5OXNuSk1WdzRkc2ZQN3FybVFSRGVOQ1BVS3dNZkFRa1RJdFhveGR2MWt1TGJCVW1QalR6NTByQjFrYVJLRkR6R1Y5TE5sdjNRVzZNUE9GbnlKSkNGQkpVVDhTNE9rQlE4SGVjdkgycjB2YXpSclJKanBjcVZCRnZ5enFkRVRTVFVGZkRuWHorZWVIY3pSTzg0dmdTQVNTa2h4NDN5Tm84bDAyWXZzdkhjNFZjSzZERmE4dmNpVWVTUzcxa1dzR2tVYm5Vd3VnT1c4QzdpVXJOR2tiZldDK0RKdVJLY2QwQkZGVzVMblNzYmU1TERZOW5oclhtMlEwbHQ5WWgzaVpqbkUyVHRYU0p4S3BtQVAvVXZOcXNwSXErUmYwaFduTHBFZXVqczkyWStlVlFrNC90ZlY4WlprYzdESGdONTRYWkZDaXl4OGNUYlZib2p0UU9kRE5kZUVKeUhkM3hZa0laZmhCU0dVczgwYm1nWUV2NW9uL3pDc01WMnp2WUFWLzJlWkk2V2FmUkRoWEJrNWVjdlNBVVB6alhxRGRmMWJrWjRxblpaNUpWVGFIRGZEa0tOM3FWb3R5YnNIdFZIQXo5aUVvSUlCQUVaeEJtZmEzOFpLaWliL1Z3aTNJSjdpckxpWXV0bllNNnQrazBFU09CbWR4ZldPOW1DNHNVT0huK1FmTnlvTXRIeTBaYW02NkFOR3NsU1JoNGsrbzkyWjF1WUxwM0U0UjVRK3ZtbWtjYjhvb08vS1NaaWVTZUZIYXZ6UmVHa3h1K2hwNm5LZ0hCZnc2T0ljVkJvM0ZSUzdYSmlGQVY3dTR1WURWY2VSYzVsb1Z0c0lBRDNIalBJd3p5QVcwMjVBemhWSXlOUlRPamJxQ3VsTWZkOWRSR1I4TDd6NXpLSVdtYVhlZlNjTjNlWEpBZ1JmWnR6MmcwTTVaQVBZVllEYTBPVDEwK09HK0pPZHRsWDNjMFVvNVBGUkNhcytOTTYzUTVubzFFS3JLRlcvN0FyUWRSRW9zUGRrVjQwK21XZkRFZytWUnlxSzVSV3NMalZIWm9OTmJ4dEFOR2RDSzJ3MVR5YXcwMTF4eWhFc2o5SHZRaE8zYTRWNEdiMkZoaHYybkhCd0FiVmRVV1cyNWhNS2kzUXdRdFpUNFVZWG1aYlhWN2tJN0VkRnIvM3V4YnExZ1BvV3J3S095eTBEVUh5djUyeVhHdzRlc0dqQ2RBY2ZuM1kwTVgyTHoxaklzUlJZZ21SRlVHSXdUa2Yrd1lsamRFeDBqSlIwUStVV092Nkp6bGFxdEZyeG9xOEVJZE1lT29neWpCdmphckhOYWtkQnZtUUZwY2xRdjM1ZTI4OE42anV3WitEalNOeWFid2tqcncyVlNRN3lod3ppL3FtSjh0dkMvem4yY0Z2a29WU2huY0pZUkJlczNnNnB2NndUSnNZY25BTGYyNFkvN0FjSHNmMUFrcEM4c3VxRG5hbC9oeEFJR1hXTWNUMnFpZUdmNm4ybnd6MG1hd283OXNUSDRyUjFKaVdBblpWVyt2Q0pxcDIwYy9Fc25Hd2hlSWZJRUJqNjN0U1dQWWNsTE5RSEZpM213RFZMSWlYWVdXSTVJQU1aRHVVNmpTaURtQUwzTTFmeWh3R0QxMzhQZktoOUw5R2xmSlZlV3pzbGxWS0c0MUpUWktCb0d6UnpCV2t6NTVUcnZFcUNURzFVR3pvY1hKV1VteDg2MGlXVE1tZjR2Wjd4VHBrZFdpT25DOWU3Rm9uYkVFTTZDSDV2THZSV1lpdFcwWWRRdG1NM3hvZEpnUlY5QU5vbzN6azEvMHlzUlFTVGtvMHFEWWZuUTNIS1NvekIwQUtXTVQ3SU5wUW1OZytqbUw1Z0R3c1J4cGxvdkRjYzFxcjVJdSt1QWFTdTh3VktHYTlpOTZhZGNsellpN2ZibzJhbzFwd0pDK3V2UXFyNnRUcExNdS9hcEp0VmpDOTljRjAzckl1Umd3VmMvemhjK0hFcHlqZUtMME5wbEVVbkJud0NPbkRZMUVGVWVlNk5YamtIb1FsN0FLT1oyT1ZqVkd5TlpwOHJxVG9Cb3J0b2tobmgyZDF2M1g4OUNyR1J2TXdsUkVDTWxqeTR0Skh6V25jN0pkYWVxaHJqOE9oQUduRVJYLzhGc0Q5YWZXRjdHSDV4M3hMSkZTUHdlU2NVYTVPdFBWY1B2WDdoN1NGZG5QTGpUMnNpRURMa2ZzaXlvZVZsQWJEdFdEcS80VnVES0RZNWVHSmsxQ1NRcDUyRXRSeDcrM3VaZHF5ak9yQ1dzZzVYMXFEb3hTUE9vK2tkYTRxZHVnZlFUcWZtWWErQ3U0TUxpL013LzBPYkZvWTFpN0NwU1IwSWVXcEFvWldUUDAxK1dJQzVNSHN6ZHBnNGRsOU9nbHNkWWsyemlRTTVqc1FSY2c1dk5NOFZGUW9FMFBMMWNSeU9IQ1NlTThzT2g1UEx1d0wxTzkwMFhGNi9LdjFNMDM0RTdIZ0hQZWdpSmdLMHMzTFo2bVE0bU05a0lhTWloK0lWZWs5RUo0L3gzaGoyWnp1U0E1Y2w4M1JwQ3F0SXpnK281T1FnUVpORFNRVFBUeVZDS0UwVnA1ckt2UFhtTENYajBpbFFhWklBcVhXY2c5YldhclExVk9XUDBpRnZqTkZqeUJVQm5penVnTVhWMUIxbmtHOWhhUWg0enhXMlVOU1did2p4cXIvUnVyMlBTd3QvcWlycXlXZTNxbTM2VGJVTjhYZUl0MUUrWFRHc3NmZ2laZm9FT3BydFo3dlZsTG45dVJiTGNuNklWT2dLc1BSSGNicGMvejRGcG9sVUlsNURGaXVsVXFWSkFLNkw2K0sweVBIUzZxbkNrcWRQZ0ZON2VJSDdaUmdWaDljcmZlczBBTUNPOGJsZ0FwbEV6SkVXNXgwbXlMbFQ4c3ZXNW5VYVVsM1NFVU5iZytndmN5ZTNXSEQrcGtReHRJOS9HQVdRYk16a1pWeStCTis1UkNtRld3QjBNRDZEdHNZVHRXVVN1UjNFMmJvSCs3blkyY1RsZlNYTUFZVE5OWjFIVEZIRmNwbkJsSHo4K09CNkl2amhDcERPUlhDUHpoNVZoUEtmQytEa0NVcFVtY2sycWRlVjNrR3lGaVR5dmhNbjFmdVd3cDNEVW4yZXlDZll2MDVTYUhSRXk5MmpNWVc0Ylo5MDZjWlFHVkFCMU1sNGErZGdEMTFXWThUWElaUkhEaHl5OFhSdUJhUDNzY2hSL3hxR3hEbFZWbHlIbmhsdnRJd0h3aUpNZ0MwZVVudy9ZbStFdVpSbUZvNGFiUGdkVEswR1QycXlodmlHT3RxbHF2NlJCRUE1dnJDYkRBbFBHNkxpWHNhUmQwQWlqMmt5VUM5UjI3YkVsZCtweHFVVUd5NlZUSEVzeE85QzU0blhtSHM5Z0xKbkxINWpIdTZ6a0NRVnppcGF5NVpzbHVTbWR6K0xWdnJQemUwOFp6Rmw1UWZEZ0xQVHdtMWFWQTVhWkdVQnJBMzVkaG9UZUtxZlpraU14elBaa1FXQ0piSFdIQ09ZNGF3NmJNdC90UUdSOWxnVUhVVThVbUt2aTlUYkVobEY3WTY0K0J6ZGFkV3M1ZVFJTlNxMFRyNC9GWkt6bzhVb1Qydk5oYUdhV3J2bGpmQ2NjZVR4MXZIVkw4NER6ZE8zc0poRnp5c0ZuY0V0SHo0cmY3RVNaUThyWGZIVUJyWmcxcjcybDJ4VU1QUTJsWkJTU0lWVTg2ZG9DNUpFYVNNeXNrSi9BK3B5VUlHQW9FYks2M0lHTHlpUlFvS3crUzFhZzA2K3Y1V2VJbUNlTzZPdlFvMFpwbHpRMFlUdVhqN29NQUxyaGRxV1NQRXI2bUg1WlVuZWszYWJMWXN2S3BleDMrcTExTHJHSjBSQWltQkRJZEVEdHprWGJnUEY0bktOazlnQXhGcXlqa0hsc0w2YkpNbUpudXpMY29BN3hOQVppUjRra2VpdjkvRkpZNG9HUG1acGtZaW1jSld4cmpIQmxhTXQ0TkxaQU10NEI2N0l2bnJ3bWM3akRqcFhHbzQwWnZ5eHVZTUV4ZHQralBqcFhpbWUydGE4YkNTaTFvUHpsZlloZmFHbjR5TkFwUENVc1NnMkxZaldHblUza0V1cS9TUURobWVDQWxlNHZiT24rY0RVU1lJdFVnSmVNamxrMm1YcmF3WDd1Y1VUYlBzOUlrbU9YZGVUWFNGU21EZ0REMnFoTmdGUFFlNldYUTZaYjJwYTlJdzVpQ0g2V2grMDhNVW0yb3RmSVlidVpGUHVzZkdTMzg4UVhOUTJGL3JCdlM3Sm4zYk1uUlprVWFFc01PL1VFMWRqbHNFMmFnaXJ0dDJaSXZBeHJkakpOMlYza3BML05Bb3NDOG1zdDVxUmtCaHdVN0xXWU9EQzh3aStMNm1uUThpOG50c2RpY21Ya2NVTWI0TUR4SjFQMGtVRGdUVGFWeldsR1NzYk9YenBNbWJ4SVhlZjNyVjI4dHprU28xTUVLY0ZqN25BNXQvWjhvK2tWRFV0L2ZqRTVjNjNhUi9RZWNva0IzVkFWeUdpWndKTGFCVjdhTngrYjZ5bmVtL2NjK05IMG1wejExQThGbUcrRld2VUhQdktFdSt5a0RlcEZtM3ZrSE16OS9UR2dNcVlwNWgzV21wUGF6MzdqV281RFkzbUZUckM3SzU5QnJHYm4yWnBaTi9kLzBnMGR3UmgvMkp5R0Z2K3JjZldEOTVCd0J4NFlnN0hma0dXZHo4bUZVdDZ2K25tWUJoZVRyY3VKZVI4dVh6NTJQVWh2YVlOSi9BekU4cUdWeThoK2lWY0ZTc0hReE9Zb040VzlpT09mdkIxdmR1RWg3MnJoK3o0d3BEN2xleWl6RWVWS3JKMnRYdXA1aktvUnRZMzEzVndpaWZ1WEsyL2hYNnBsTjBKM2tuUm1Ic3AxRmpKZGoraURKTlQ5K0lORHN4ZjJpdjdoN1JJUGYxdWFBMWl5SFo1OGkxSmdSamQwWm1vRUI1L0RkaGtVSjU0dGJCejlDS3E2Uis0QytoNzgrdmlRcDhta1NnVU1zdWlVVnVqU3NpeGR6UkIzVXE2WlVUT1NmTmtaZElUWUFvNlZDWDFTeDg3aGxNdURJWWcvVVpqYWI5RGFGa1pIeVNsMzMwN2RXeTVPSWdMdmI1cm1pWWs4cTF3d0N0SWRGY1dza0VpNE1DT2dCam5mZFhwOXFvTjBvSktQQlVETHJrYjZ1bFVidm9xVXlNWmRnZVRhYUpMbXhiMllFNFZnMTdvYzVuZE54LzBpV0l2MS9RMElVa3UrZGtGRUh0b0c0YkpxWVhKNG95SjR2SnMrOUkwQm42SDlLdEpGSGFHWVdOZFRsdDRMcG1UcWs4RzhtS1BsaERUeE91cmkxcHNrZzZ6SjhDZVdFNmNjMVFWMlRTODBsMHBUWXNEVlU3U0t4MUdPb0dqdE1WYldiZ0RSWmd6Q1VOMkdja3lnZzFIZGUrb3MrNXJrOUN1dVJqU1BVMFYrbkZ0UG5wQ2MyeGRDVis3NEhOZHlpdnJTektDSTVqeDNPQ1E5dnpsSjVQOFBTWTlibk8zWUtPdFlNbGRMTmpUaUxHU0ljWElKckNZL0Z3T3B6VlRWeFFTbjBybllkYUExNkMvOXlYVXYvOEdoZmtMbVZmOVpITWF3WEVYeTNFTWdKUEsxUnhJYThLeUdIdzJwenI1c0ZwUzFDNm9hM3BnQ1F6WVcxbE5lNXJLWWhLSnBSdVk3TVA4OVV3WEFBK25TNWF2ZEFZTkREK2x6RkFIb2lOV0g1aDNrNnpaK29NVWZsenlTYk93UkNOUkRKVzVrNnp5V0pUU0RVK0NHQ3hBa3ZpMjJnZjFSc0NmbEM4cWVoWHV5MEpqZGRhRjRrYktoRzhxdnEvZ09HYXhJUUpYbnJ1QTNSVXNZU05sM09XWktRVzEycHkwbDNMNmdmdjZQbDNHWUp6ZUNMVSt4bkh1RlNIaU9LNThFVmJoMlJXNXhhdzgxVXgyUXM4N2ozQTc5VDdDb09vUDkvNWZUZmgzUTluVzRvNmtMeG13L1ovYnRuTlBPYTZkOG14RUN2YnVhVWIwWWw1SEJUYkw2RWF5akJLd2JkQnlFaGNRL0hWZ0YvQ1NSb2NGZnpKTStHUVJKcnp2T3ZXaHpWak0rYzdIV1lZb2l3ZmZxbHJJS2c2aituL05weWVhbEZSZTFLeWQ5Zk9hbmlqSjhvZDdNRzZYVm9CR1RDbnQvYTVNaU00NmE3M0tqTlN5L1VOT0hRaDltWWlDUFVPZXI2UjVieXZQYncwUXJRUzRSR2wxOXNQd0EvWlR4U0puMGcydW8zY1ZuVkV4VUxPNHJiK0s4a1pOViszaGJkbG1jYVJuajlEcU1xeFovRUN5N0NsOG9OQitSQUpkOU0zM0ZOMGxTMGN3K1piRUIySzRENzJqbDZESlo1UjdTWlNzZU13YUhsbG51cTRQQWZTclEwclRGTTd5SW00VkZWcW9wMjZGZGh6Q0dzMmp4YkNsN1hoOWIzN0ZSY2VmTWY4YVdRVHlNTU1aVzVCOU54NGF3bFZKTGJkVllIUFhmYUdQL2VjMXVjTloyazl1RHlrUzN0cU9TOWdVaEl2MTM5UlVQaEovQ0NHZ2J4V25lYlZwVzZjL2htNlNVS2RkTEszdnBhQThjNUJMU2wrREEyOExHNHNldzVtOHRSVFExczRMNVlWWlRYd2hqK0NXK25yUnMvaENrWnMrN3hISkNWeVBYY3VTTGZwdHBNSUtDNHVGMVJQVzA5eEJxZ0xtQUZ3aDA2bkR4ZVhrcmFwZUhzMVFiRk5IQTd0WXV6L1pxN3pwUWc3N3ZYMjZMaURLYi9SUjN6UEJyd0pVV0ZEb3E5S3BURlZFQWd3WjhFUkgzSjMxd1ZFZ2l2YlRwV3ExZWtHeUtNOS9zTlJGL3l2c1JXR3ZIdkUvalozczM0N0ttRUR4RWVUTVFZUzZMRzhXWkRrdGRPQVRHWEovRTNxUWhXQWlENTJVc215eEtDSVhTOFhPdEVrWWp0S3R0RWhkYWU2cmVuaXNxbi9pMC9LZTNQeG9ZK1YvcWVsL1hrOW9qMmFKdDBnN3JEblloN2kzWVZobE10UHUycjlOUFR2Qmg2VWoyN2ZYa0FjODdmZzNzSzZwWjZpN1RreUl5SWgvY3ZPNVFNSENOZmVWWHI0d1JaS0sxNTk5aXF2K1NZMDNWSkRpdEg5Ui9PWW1JNlMweUtQZlJpSXZTVDVJN2lvTEZOTnZFLzJaaU4zWDZ4MUtiblkxN3BOemV1R2NUb2dLeTRFdGIzNm1KTXNZeHZqY0FFNmlIaHN5Z3BWclVkVnlxWkUzOS93MEdiSWZ3bkxIQ2dzbUZCbE14VGtEUzRkaGEzM0NDbUtNcDB1em1xZGxBdkVqeWdNcGRMYkxsVFZTNTFrVXNoczRpWWozREdtTThnaHc5NXNSdWJFa3ZETXdPQjVFYzB5blJ1Skl0dm05UzVPODBXclVTTmJHSTBVRFZ5SlNORS9GWlJKMnJXcWFDUmJ5OW00a1FjRUNTL3l4UUQxTithaXZFR1BuNTJzK1dhdnFXdHI1TDJYMnFQN0xQS0VJb21TOVYxdW91ZDZGT3lHa01HY0hiZk5WcHJaTDk3RGVvMVJ0R3NrYWFSSzFoenQ3MC8wNXRVM2dFTXltRjVXQXlzdGtmM09HZ0ZGcUtKek1CMHB5eWRQQVZsR0M4dzJoYnJiRTlVSGxjM1dCV2lXMnUwblJzY3l1bzcrVytWR2ZHd28xUmlPUGJ4bzBQMG1oUm9oU29PNlpvbzJRUTZvUnRPRmFLMzJqMnZhV0Z5eEhBallCeFJXa003YndjcEJpbjdiZHk3aTRxUXZTZVJUZUowSjdIOEoyWVBXdEFhVHBFVEE5RlY5blVxbVp1UW51M004bDdIR3ZKV3N3WGdJbWFnZjRTWUdRNXI2SXVXUE0xTkNjTk9DSmJRRFR2OGdUNTNWRTNRSVdHRGZ0VlVhRnNjaTBwa29LSXE5N2JlVXBZeHhHK1RjZjc1azBFUENQbi92My9YeVh1SG1ENlYwOGsyZG1aWDRhbW9UL0dyWGJnd2hBVUh5elUwbEtkbi9HcGZMUFczcHhDb29yenlSUVFKdUxUQ2dSWWRlYmhBY3RaMHUzV0sweUJETE1EMm92aVhWb2xHYlFGZ0FvM3dseUVtaWJjakpxQTl3cmJReWVRa2ptVXk1VFh1T2RxaklSV3hGSDFraS8rZmNxZ0tPaFllZFdVRFNKanFxVDYxTnhmWldsNURqUXlhOGlOMGZWVnl1cldCRmp0cGV5MGw3dHIycnhVQXgxRGlaNjIwUEtDOHRnNjFCVG1Cd0R5YWhFT2h3UDR6Y0tkVGRoVDlGQVNla2dDMEorcUlwMnpLeTZ1bGd3bHpCVzBSMVdTcmZQNFNmc0J2ZGt2Z09YVUd3SEo5MUMwd2dpRldrZW5Fd1pobFcySVB2Z3RjRGY0Y3MrSnBSSjJXQzVGQ2NyRjZVUUYyMVFEbWFhciszaktkMmlVWnZjc0Z4L2c5UDFSRHNVU3E2ZTYwZlphMHFSOGdqTmhZTm1CQTlCNUhLMG9YdU5tM1h5dTNaNE1qc1BucTFKT1pwR2czUVZWSnpYTnBSRzBaYU5BUEZJMUNmdTNNQ0Z4TVdTVGFQSS9FK3JFZ211WHNmU0pzek9HbEdIelBqR3pGU1FEdCtzbHMxNU9PR1hvZG1xcy9VSEo3ZE1CU2dKZFgyZW1MbWJSNG9KTjBHT3IzeG9zdVBvZE56bUt1eFBoV2FxNDc3MUZsSGcrbHRQU0RVL3RETTBTcHBrSEh1UG91NkZNZFFYNW40U0RQVVpaV2VIWHN6bGx6QWNrM0YwY2I1ajhHak83WmZGL3ExL01oNytBcHNvNnlJNXArVkFqUEFURjJESmNkYjV0Kzd5N1NQTk80U0pDUG5DQmcxNFRrUVhVVE5Lcnk3THhNQ1RiZUY5SFY1OThVNDJ0OGRraU1keHJncXl3UmZ5U01zdWpFZ0dOWm9yWjRUQ2V5Y0ZBKzZkNVF4WHpiOHliU3UyY01CekFXRUlPTUsvNC83a2FXeHBoNTk4eGY5MmpjK2FsQnBnbDhTaXFqS0JxZXdxd0VaRS9ucThucmh5QllSaGRJUnFrZTFOOVI1bDc3NGY5S01YZjBaam0xNEZDdkVQTmFqU0NPaDhnL2ZjbHFhZHFKY1dJb3Zya1VKN1hMQjQ2d0IzcVIreXNPK1k0M21lMjJGNDRHd2FoQnZTYWppWlBEWE93ZGl4N3MyN085RlovMzNEcng2S0ZsZUIxcWhPMUVXNThzSXc3NFB1RVd4NTFuZ2ZYeUNHQ1NIU0duNlVhWHduOGdabGNOc2xjQUdHZW5hQUpvSHg1OWtMeG5jNDdaUnVNQTRJTitVeUVjMXlUN1FPY3hxKzZOSXNmblRzLzFXTEswWUpIYVdzUzdheFBVRndYVCs2MCtwMGJDWlVJYUJBV2h5RVRjTWthQkNDMStHK3dvTHRJSXRJZmtSK2M0bmRyeEc5WUJ6SFpIQTFJWitxMmRhdDFGa1dHOFdrMXR2bTRMV09PY21XZWloS1d2MW94N1pRaHFnWTUwN21uY1JrNEo4NGd2RkViTEhoZFFFbVNYNlA1RmZjVU9JeEVDYllWNFh3dVJPVG5zMDdwdFZ4SXI3UkpQcTRrUTB3OG1OekZ4b1BTOGZlKzVabUhaamhIdkVxYk9XN3VTdG0vQmdGVFRSUXgvN3NkWEJUWWdlclpFVzRRejhLdWZLV0tmRnlUR1h1cU5pT0UwN2hJYUhNWXhvelJsR2lvZEpnVS94ZjUraHpVcGV5eE1BRkEraFZKUTU3ak5LdE5VZ1NJemduV0xPaFNLRVBic2F0YTBoUDBQYktKeXRmWU5GMHBocWx4d0owaThnb1lmR3BnNlRzeGRHR0t2WVBJMVhtTXkwYkRjQTVuTkxMeEQxc0VHbENMNDZ0SWZWajh2MkpNWkwyc0F3VklRUXprWEkxTDBsaHdyTlppeVdHNVBGWU5jVnJtN1h4aXhBWnVha1kwMUpQYjhLMmZLWnlrUnNQU05icTNDK0FIdVpJWElENXkycUpySnVkcVdqQWVYekZnRUtrZnA4Yy9uOUJ2VWxJKzBEVWxqM0FiYm5ROHpJdU9SMzRhK2Foam1QTVZTdXhIMEdMeXRjUkNodkErNkJKR05HV29WNFVtTVNqUTdZcWNlbkJKWFZUdTQ2T0ZrYlJaOTIrMEhSY1RjZSthS0VBSkkwSy9ycDFXczNvWCtpRTdKdm1ZTEZIRVRyQ2NTNGM2M3JkUXlUaHV0V3FEWGsvNFBnT05iSW1NWkRvL3ErM0JoK2RZb3czaU5PZFM5NXQ4V05IOWtXVlBtSytyR0xBVVUzZnNUR050OHgwOXM2ZURpNzQ4Mjc1RWN1ZzBKaTJXa2ZtMHdvdy9zNW4zOHd3eitvQ0tmRzBWc1ErOGp2VnNYQ2JiNll5aXhGK0UyaG9BZ005NUNoWk5FSU9mcnBrY2t1aFpTYThZTU1NMTd0aWlGVHJYQVJYMjFSb3g4aUNqSTFUaU5xaHYySEVRelpqY05jWEV3MEcvaHFmS2FsTGJxU0ZCY0NBakhocW1IUTFmclRVZkMwNExFbjNLZlRjdjRobUFuTzBLT0dMaWlqdjhNQWJISlMwK1loU1REM05KWllOc1dhTjBmTVJtZUIyQWU4NlFPc2hITFVDTldzN000eUdzUDl4MlptQmVxeGxoaWMvSk83V2liS2Y4TjhmYTN6dkZqQm9JNEZuUTl5QytLSHhCNTQzNndpNS9EcTlFbkM4M2ZwU0FBMndET0R1RUxZTDR1a1RsNGkvWUcyWXl1eU02aDB1cllTVUtGcDV4djJSRXFCWFp1ZmtIZFhGMytWejY0anZhUHdtekhoczlRZURoSWl4b21idHpQZTI2NUp2SGVWcmFTNFk5cUZVQndDbGtqNU9FbWxhek5yKzhUa1lPczFKUTJYTnZxNzg2MUt6T3Y5R1ByeGQwWEt5R1JvdTJ4K2l3NzJYcTBzUmhhY2RtaEhpbWNoQXhDTjc5Rmo3dHdmNWt4OGxKUkJKeDFZbU5vbHV0ckcrY0EvcVlPT1BwbDJVaFdiWnIzZUh3SW1qbVJHejdQeXFibkZCVUVCcE45UXptWVh3U3Q2WFhZQkx2TGJsVmZ5U3EzdnVuNmJqdkJyVUxONVMvT2N5Wk9NTlp2ZTMvMC9lZ3JLN0pVYVZmcHZHWlRqcEhOMFc0NWpmVzdoVEpCbmt1Mk5YRzhlRGxvSlBHVVdmS2dxWitpV0xQVi9YKytEcnJJSytVTDRUQ3c2c1NJNXV5cDBSY0JXWVdjQUlJREFwTHFkeUdET20wcFR3bWd0bmZnYlpZaUJ3T0lTUmNjTlRGVGVEM0t3bDNXOWRYTCt3UFNaOUdrU0FvWUJNQ1lpTlRGWVdVdFFtZVVIYXEwNzdSbkJCeU56endXTWNic0QwSnZMREt5c2FSR2IzNXRCUXpFQ2FMZG45bkR4U3MwZ3NXc1VTWDhLUUFYbGE5c1ZEb0x0QXp3VllWcDgwb3UrdTFObHc5Vk9Hc2ZvQ09Zb3c1azNHbkVtRVU0Tk11RjJVV09YK05mOTVYcUUxRmlVRTZBZTMreHBVU0FWbjJCRHAxRUdJV09XaS9QTjNhcmNsY1JZUkdZZkYrUzBiQ2c0N2tncWs3enpvM0hPL2ZNNjlEbXZ3WEZhMmlKNTNrcTFwUU5LYmlVdCtpbFV2ZXFPK1JHZ3RGM2xKK0V5S1FZMG1qKzRZRHRDWWo0empGMGwrNHFNZHgxVVVieVR3SE5SaEhSeThnd2xqNXJ1RGRKNkcvc0thMTY1Zk1ZbW1NTXhNR0xJTVRleGsvaC8rV2ZhdHJMV3Zrc1lQWVN4bytPclZMdGQzVFBwcEJHd0hFb1J6ME4rRXpNWmtDZkdmV2NRKzdlWis5ZFJLeFAza0ZkZDlxbGNaRlZHbDVFNFIvVThFZzQxMEFoSlNLVCtZSWpQWFFYN0liV2FMcmdPRVZoNzlOYmd1MWsyNEkrMWNVemFsL1F4SXVMQmd3czU0YVNJN3ltc2pkVTdxcUVjalVMMmM0aDUwWlZreDBOdGtIVURTVVltMkZjcFJZM2pjSEx6bk5QTHFvY0pQbWV3NkQ5blRnWGs4NG9ReXR5SDhFcCtHNVZ1b25hZStnVTB5ZjRzSDI2bk9hN1RhVlZKZlpjcmFsODE5MkpaOWxMVndkRjlHNVlhZUM4WVBubjFCM3V0QWZuK0M5WmY0Mnh5dVF2VFc3RTZiSTBYSGRvQ3JBWlJRTDd2aUdsYXdIMnU4T00vbXVwa3owV3pXQWw2clVrZWhwczAxZGJmVVdybnJWTHRNVTFIWWlGRWJHUDFNejhXMDBwQUxUZlhtSDZzakljRlJzaVJLM3FVRmxONGlpUUlrVktvUk5WcXVEbXpQNzl5N1d0L2llK015WWttWEpsdUg0Z3FOK2ZNR2lLdDI5WjJQVG04K0lRQnBRd2k1bk9JZXV1blE0dDJoaHN0YUwrcUduc1NEMjdGVmtMTTY2YmkvMzgreWFLOTRaMmhUUStaRWhuazFld1A0WTNrYzFJUUJVbXNZSXZvUzJ5UzZUWU42bkFJWmloL3FUb0l1ekF0OGpFVE0xY3QrM2hEWm5KU1ZUanhySnI4dFVranNaUzkzTzlFL0xYMmViTm5oeXdXMVVKdHhTYnhFNUFrQVhMQ0VYVkgzbU5KYnp4K2VmTkxYWi9YNXMxVjdpTWhlQUlVamp2YXlOT21nckt0aHpEU0xYdTlVTUhQRDlOcUNlVUpJYy9FVm5qR05QeTBrOWlxZXc5cDRwVFVhU1dHcTdaK0FtQlZhMnFkM2dwZTNrMU9PRXI5TEw3TlNkNGExemNXMC8wdmdEZ0JhLzFGdU9EK21JMmY1MFdLWWdtZ2F6L3ltNG1KTVVxNDVMMnQ5dCtQS1ZhQ0Y1UXBWeFBlV2J3elB6YS9ibzVGU0xuYkhqU0FMUDl3U1g4OGg1WmNERTRaSG5jRUE5bmRxSTdWemNNM2ExcTA5UHBORmVvQjludUlaUGV1Z25IeVVWT0M3MnBiR3pzYVI0TjRBeUhjZGxUYk5ZRzIrZWxtWUNKcW81YlltYW9wT3RDb2tPYzg3MU5va2RwcWp1ZzZDY2g2dzhEZEVjN3FjV0Z6M3ZCNkxIMDNpbEdnSHI4Z3RpazR4Mk1PZ0Z5L3ozYi85dndEdjNlZGxremR5NE1oWWdGazlZcGZTQ21YSzdFY0szTmZ0bFBYRzg1YWZNT25BUDc3UGZXdVBXRlpqM055VVFuK1UrTVVnWElKb1U3OVVWMkdQYzRiTWNxaTdKWk90QmdQbXJ5SCtJUDJCL3ZERVpvNkJCNk8rYjBtUzQxdHVWOGluYURwRmY4bjh5OVovTFpBSWtWN2tNc3pJN1AzbFhvUG5wVFRmK0NVaUxMNld5d1N1TmN6VUxYdlZIV0NYb2J4SU9wdEtXTkgxZXlCZ0hNd2VCTTlhWnl3RGp5aHp0YWJuZlIreHgvSExuVkd4cnhTSVdQYTFjZ1RpWHFPZDhyOUNkUnJFaGdrKytxZVlnVitkQzVveDdkaXBKVFNKQXlBSWsrUXloMVo1QURuSklubDY0QjREVUtKSFVHL2loODNPTFkvbk8yVTI1YUhXZzBiMCtOTDREb0VKMjRNd0o1Q0JQd2g4bEQvUXVQelA0TG1QdEJwRWVJenFoZWpUdDFBamZLRmpvQ1JkbWhpQzNINCtBbE1RMkhkUVZyVEJqREJGcmQvWFd2czZHL2J3V1B6aWVOeGpOWktwS1ZkZUpKQUVWL25hNHBYNXlEK0dzRHp3RG1QK1VmWlRDb0tkZlJhN0R3V25FNDdSbXdiejdmUFl6aGZYU05vTndRd0RVN0NrRDZiNjh2OHJpYUE3cEdSUWYwTFFsOTlpRWVKUzRENk5Eb1d4QUJQQ3lrdEtNMlozeW1od3NWYURlWWt2d1pMeHNMeXdBQkYvKzJuK3Z0VnFRYXVuNEl1MUtHSGVmR2xnT1BSRlZ1bS80QVFzL2FwT0FnMFZMZFhacDJydzR4SE1teDlNeXZTWEhtMURRWXg4WWcrRnVQOE1lNFloTTR5SzlicGFUTWZwV3MzNjBVdHVscHhIT2dTL3grR3RIWkdnTWJqSXQ5Z0tPWnNkZjk2Y2s2N3VWYjVleWZHamd2SU9VdDgwanNhSFdERnJFQXpsQXdXa2NsYWNkRklLZUxHR1JGdUlKV1lxNGFwYWQvb2RFb1k4TVpRSEdycitLT3o2K01hMUI5b3lWNW0zaDNua3BvVDI0UkM3ZUlVSHBITzBpR2h1bzFPYzhNcUloYkVEOEFJbVNoL3p6NjFPMHU3U1ZXbmxXbXdnaEJ4N2J2eVBRZGl4L2txWDBlTGdvZnorTnJCbVZoVk0yZFpLN2VUQWx1T0xxZWt2cU5UZ0VMUi9wTnJNL3hhb2xMaWZ3MGFQRkRZMDBaZThyaDVYaldmTFhGZGFvNkdEbEoybjVsRmxpb3JrQUo4U0cvM1FubCt6Y0JlOTBDWnFlMUFIMVIxNDIxbEt4YVNHZW8yTUo4WDgrZWlxRWsxNDUwemk5UnlnWlAxSi9rV3NGZGtNRS9FTzBUV2JyODlSZ3BrbVlzRDd0ZWV5NXczSjlTeDZtOVdmdWN3V1Q1M0w0NVJUWnpQS3QrOS9SZ0FLM1dLeHZhV1p5RXU5M1g0ZXF1aUtCYm11dGY3KzQydVRYdGE4dUdMN1FvbVV4WXFRMDVxcTg1YnY1S2xJbS9WNTB4SzI0THlFaEptR2o0M2VTNWdSb1lrQWZFbXp3eDkvMGpUT0ZCT3VvZHJxUnNMT1U3TURPcER6Z29CdHU2QzRVZ2tYM2pHMzIvS3RGdkpka091RlNqZGdhSVNYVXNNUU8zbnZ3ODhwdzUyMTh3dUFqWThOdE44MmxYUTF1Qjh3YWtQZlhnN2VJdDJVSjhmK2JZOUY2a2FxZmVUL2Y5YWprTDZTU1dDWFAvQjFKNUpNQkQzNDJNOFhjTG1SeXloNHU4dGppZVJidC9QSkhVMEZqSngwbzJySnBOKzJGdWJERGFrL01RV2w3MTVWRHNuc0txanNTTStob0VIZHJlR0ExK2hCZ1lrMmsyUFZYRzhwSFVDejQzcHhaR3VjaTMzaU9HMng4NzFwQ252SGI3TGpwbUZRQkN2V1h5Y0xaREJJMTB5S2s2b09iL2pqUXkwT3ptM3RWVDlwNGJFc3VEV3JCN2VQQkhYUjVJa0VtenJrRjR6RlhuTENXOXVDMW85ZmxsczJ6QlJsTDNwMmlzNDU5Nk5YdjdKdWtxN3o3WDBleTdOSjlpRGtXaXNSWFpZUUtDOG15SmtzZFU0N01EVE5MT0I4VE9EMHlvZzR5eFFkMkN1clhsVUE2ajRQREtIZ29wZVgyWHppTXp5aVQxT1Nib2lKWmZQUFA0RlhxOTg1TXFyZElOajVQZzY4TmlWRENVcmpzRitBQ29yNk5rNTRGYzcxcDEvL0FOT3F5R1Y2cVlGS0I4Nk1BbE9ITk9zUnRMZGt2dll5aE5XZGQrcGY2ejVReWVESDZXR2Y2d3Jka2dNaWlLRG54cjFFRDM3ajdqL0RuNlZIaUxVTythY3psSUp1MWpYaU05QnY2Q0IzenZQUFdqbGtVZDlBNnplUXBlNVlHMUh5R252WnE4Sy9UWGcwN1RNOUhxbEU0TjR6emRxdHdhZHl3dlJMeGlua2ZseEJkZFBvQjlDazhwdENKN3pzUjhhZEliR3JJWjZpM1ZOUklIaG81cm1maGdXVVMrVzg5S2pPNEROWHZNM2FxTU1nU3JxOHBKUDByUEgvSk85SDQyMEtCZ3lBMGNkSWJkTU5yWWt2bTZiVnJJUzY4d1AxTGN6NWx2U3A5djhHUVc3eExOQnh4eVovRzZBRVVrdVc3VDFuZG1kMXB0V2RQejZteTdIVlZIR0YvMlZOcVpTbWRUcXpyTkRxMkVIYnUwZ2VEdytJM0xzdkFHeWJxUzBkY0pzZENnMit4TXk1UGtreGJCL01Zd3Vldk9sUFRNOFQ0RGpQWm5DZmk0SEdCbTNiSDJjWUhRZHR2RzBmNWpXMERmREcrWkVJZDdVRC9vTCs1eUowKytib3hES2JMN1RNbm9la0crV0FkS0lWWUxrQjNaK3JUeUJmYnBscjJycnBxTW1kM0RvdkpMeGJMSnEvL1BCZERMeDZaOFVDSG44S1RLWTY2dkRvenlvd3JzZldEZWh3SE1qOTFmSmdJSGJYMFhKNHgwa2QvV0pjd2djQXdCZ29GMjRYbzJBc01NL0U3VEhQVTh3TU1BaWNwQVJvamd3WGxiU2M2UzAxVlVCUjU4L2NQQmVSUXFoUGhqczFkMi9MU0RqT1ltTm9PVEMybWJONk9YTWNxVmRXdUFiTWs5enVveVl1QmNkM3pmZFcwbzNtYWNUTnJoT0JSR3ErRjJPenB0L1lMWERCaUl1OTVzVHhOK3p3UVl4YmtIVk1kdDhuekh2OTViMW4vSHdRTEVMd2psWU5NSkF0WkQ0Q0JKYmpsMFpvTk5nZW5LUmZ3Y1dxM3hvRUxUMGVSYUpJWTYzdW9CZFE5SW8zUER0ODl3ekdvNVUvYjlOQy9ya1BpSFdaMysxRXVmb2tybWRSU2xXTHpJaVEwV2cwbktGcTZEb1VaSEI1dG9NLytPcldvODFqMzdnczk3V0l4WGdzQkV4cXVFZ0ZkMk5MWEJIZ1h0eDExT1ZWejVGRGFpVExnMkxtVDcvS2ZWVHdVNVV5S0lKc09vaHl4U0ZnSXVXMkxVR0xudElJQmdoWmo1WWRjckluS201SFVsbDBUbFpKbTVNZjBrd0hUR3Fyenpub1FMeVV6bnpGTk1GREdHQnpNbmxaWVdmb1FsRjhRcmMrWXFYSlhQMjJaNEgxYWExL2ErVlpMSUVZWEdDaVlYTit4eUFmWHFJZzZBREx1aUE2Z2FncFVMOWdQdEtLVnBqb2E1cmIvY3RvTUVUb3BIME5maHU5L29zbHZPUkkybCtmWFBNSUVwenFDajhieUhFN0VKYTBaYlNwekdSSVEraXJidzUyMTgwbnRZWnFpOS9ZcVBMMisyU0VrcFhqRitnaHZNVXlCOVV4d1p2dzRWcDJWTWhiUFA5eWRESFZ5Q0VsWjdTNTJPSDlxN0lWYVBYZkprUk1aQmxjT3N5NlNVUDg5TVBNTlRJVlFZN0ROM01KYmN4OUV1TER2TEZqZTdheHZsL1lMc3g3dWRlcXNYTk1HWFcveTNXc1doTzR0TWNCL2Y4dThRT3VUcXIrVEZxY2FoYXU2NnUzR1JDak5na3EyNUVQQ3ByZWxjaWFtQUV1Ulg5MFNuc0dUUCtGN0MyUG5YWHZrcTdxTG1ycmZpay94MERKL3hxenVZUktxbWpTZm5TRk9GSDJrU1BNQ3ZZOERyeXlYTW1pcnMxMy91V0I1WHhxYXh6dHV3eURjMy9aTitKRzRYNFR2b2tla1lZaVY5NGs5K3NYWUtHWFBjZ05jVEp1Y3R2WE9Eam45dUdNOVZKa0hNSUxLeEFqbzAxZ1RUbXNEWmxlWmpMb1h2SjIxTHEzbGx3ZWJUbWxaWjh5YUNxNG1RY3RUaVFwazRJcmxaSXBvQ3FmdWF2TkZ0dWN0TWNZcGlybUpicHlrOWZOdWdkK2RHT2lNVnRQVHpLM0FCKzQ5RUNDcDg0M2ZkNkxIei8wOUp4cVlJZHlmVVlXMENPZGF5M2ZaKzVxMVpabzIxQXp2N1AvTDdyTHljZGpvMW0wd3plbkpNN21sYUp0NUJldFRVb3Jya3E4U242aFhwa1c0YVEvWjkycWMxTG5MRE02RHB2eVk5aVJudWNMcDhZZUJmWTNDMzFmb2lIbGRkRzVDL093c3dKMU9HcWxxbWFpWFUxclVNdU5NcTliVS91dFIxSXVyTlVQWExUdHQzWXU0VXFxb1NOQXZGY3MxQUdjYlMraUtpOHo1UTYyVk9QdUs2L1VPVWlvK2tlbEowcFUwbVczSm4rWWZWaEJTQ0x2S2RtMjZqZC80eGk3SWdMTXM2b0NaRmJWWGxwTzBvdDYxK3lrWEhZeHpsVnlxYWFlRnlDVzlNdXZJY2oyOFFjRm1GbG9yb0drWHBSQTVab1oxM01PbUdKNktIT3NNUWduT1BkZGc2c29JNnlJekZ0eXlQS3NON2drOGlKQmVWZ0hkcXd2ZWVzWlh0a05FSkN3Rk5vcThSQy9kRFJpM0hqaEdubEJtSjdKMkw5bmYxTEp2eHBSWXRJdHZSREFQd2ovV0R5eGdKT2o4aWxweTVYZ0NpRHhqWUFjYlEyYkphckdTVldjUDFhbmE5c3F1dWZtYzhaTGZMZ08rbWorKytYclJKaytscU5xS1FmYThuMzQvVEpEZFd3ck1iMExFN084c1BYZTB1WDYvMkc5VjMrY1VBTi9aK0hnUjJjckVxVnh6Z3Y0dWY5ZTF5T3dLTW12azdKUzBCUmNjdHpmQVp3Z2Ivek9JbHl4akZHME5DK29lMG9YWnN3dWkyRVoxVXdnQ3VHK1dYVFdHZUNYdVJIdVVoRERYRE1Falp2RWJRdHREU0NwS1k1TzQ5NDYvYXRlc0xGS1pIenh2ODZ2dHdIaURnZTBHZ1pLRHNORmtrd2pITnRBQlBGRzN6bm55UlduMnlrUjkvNGs2NWxORWR4SFYxT1NRQXkrV2FyL0NsZXQ0Y0FEc0FGTHMvOEJxTWtpRmM1ZGhJcThFUzd2aVA4bEZGVFNVcHhHMzhTZXV1dEFmVk54R3FDTEVHRzNjOHdub1JCdFRybGUzV3hiUW1LSnljMitGY3l6SzFjbC9PanhCcndqU0o2YS80RUF2cHZCYnVsQ0xTalVETUJNaU1haEZXWmw0OVl4b25OSThrcWpxTWJiN1VtSEcwRDJsQjd5TGVlWmZZUVpSMFNXSXlUeldkazdRNEloaU1lN1VLazd6QWN6cTcyOEx6OGhyc2VhS25sVmxvanlxK3R2cHQ5UmwwMzcyU1hUYko0RWpCWXRWOUJDa2NGRVRqeHNORE5zQzNGcHFra1RQVEttNmtKay9zOFBJOG9wQmRZRlcwNUpFOE9MNEthaEFtR25hMGQyNkNwdENCeFFBdm8vVE9HUE4zbEdGMjZGOXBHVzlidEtsZTZ1S0hJRVFLNG5LYk94ZWRkcUk2RVRBbmJEUTl0K1RPZ1RJQ0Q0UGZDMUpwRnppSVBQM0Nta3BnK2xUWTlURkdXbld0Zm1UcnVYTDFBamdVN3dvQ09pMHVKRjc2blNmQjVEb0lLMnNJVkF5cG14d3dsSkswWTZQdkRkYUlaU29LdWZWOTgydGk0NnEzZkFnMkZZZXRtN2NWRW9ROXJtMmpHUE03cGVJRUlNaVZKeXlWUFpmK1RWbjRDa3lBNUVDZE1RRG5vay9OSGY0dGZCMzZnd0ZOMGYxRXAyYmNNYWplbUN6SzlvN254dWdTZGJEMWtKVFhodWYzNUJHZW1pWFg0WjlxblJscHA0K3JOUzFJQ1lzZlpaRWpXODg4aWF6aWVjSGFpcEw4T0taazJVbWI0djgwU1hyQkZWNjY2REgya2NEb1l4ZWZKZE1oVXp1MTdXWUZmZ2swRllaWi9jSkxCOS93bTRjazhSVExhS09ReUNsMWpsOXNmOUpPUzNTQ1l5VytOYUZSZXFVMzJQaFNvRXpRZmpPcmYyTURJb2RLcDhwM1dFeW1UQVB1Mnd6YU05Rk5zcjROYUJmU3pBcDY3RWNjSU4vdnh2RnhoakVGUkJkbUNkaldKM2xQNjluOUlJRVhoVStHNjR1bFlZUlEyL0lDUFAzb1RQdERsRy91SFRKZUxmcU5XTzRFVWpGQmhna3p1SVFnM2JKanROakZ6SGF6bUQ2Sk42Q2ZLWitubjRIMStRRVhIeDllQ2F6anM4ZWFndEtPRnpYejBEbGZid3p5NWJMNjNMTjRQZjRCUE45ZjRVWXJ2cUJPWXMzbVNrVklPejlFbk9uSnFnT1cvQUdwS2ZWejY1WHphL210TUFHTUt6b01PbXZPcktCemNxelJSS3VOcGFnbE1IQ216bTdZbFo3RWlOWXlFT1RMYTkrUi9GWldDeUhYQ1o1U012V0JJcHhkTHErS0x5dEV4elo5OE16NTBpSjVQZHRVaUZIUFIrQmZjYTlEN2czZ1lXQW9ZaE11VnRQK0grTFZqT0hzL3dWVkhTbjR0SVNyeW9hU0J6MnBDcUN6MU11S0R3R1dSTFVnUEtldUtOeXV4Z09zMTY0TGpZYUxnRWlCY3lPL0wzV1ZQeElGSlNXeFlyMnBRTDVOeFlTVi9TZytvVTFrUU5lMzBNMlFLZE8wMldvTlYvT1F4ZDRwVlMwTWdLZTkreXNROTVueDdobkVGV2x2b3dYcFZTTzRsL1ZjTXhHSjI4MDU5aUtKdW96YXJEYU1EUFhnTnZoMzh0cjhoY2c2dEVia0FHUDdDK0crbnI5YlJKNS85ajRrRjJJYjFJdGhQWEZra21vT21zb2w5SGFON3I1WTV3eUJxZXlseUpIcVVZK1UwL0JSa3pMMmdpNUxka3pjTW1waGJoVmZ2YjdlTmNnS3ZqcW5KbjBId3UreFFiVHNIUWxPNFpOclhKZUtHRTM0MTdNdGptb25QR1BlL3JNTVh6RW9HRE0rSVJGODArY2Robm8yY0dMdXdLUUJSTk95VXU3MW1sbU5Jd3M2ZHdUTDZ2eDlvd2pKZmFGcEVhL3p5aSsxNFNhU2JVUXRnOUdIYXJRN0NabU91VjFwNklraU8yYUM5VVUzMVpMdkRxSE5DVjI3UkgyYVNURVlSZ3pUclE2WkVla0plaDk2NkVXR01IUWczcTlEbjQvT2JjUDJ0Q28yU01HTmJqcy9MdGhSNEpJTXhzTjJkNmxaRVpkUTlIdFJkcU9EWFp5SE03a3RXd0hUZHNVR2w2dGpNK1RNVmJMVlVoYnUrbFhsZTVHUTQvakN2WWxRN0hYNGx2bGZ1RVEwMDdIVjhkZ09KWU9CLzhjUkoxc2YxbSt3RFdqZTdFZXVCWlh5aDQvcE1KaVQxVWNQMHQySXBob1dpVjc5SnNCaWFkejNna2ZkVDFLR1BwNktRUTZJU2NzelRMTlJHdk9KWFQ5S1lIOStPNVUyODZSWVF5Y2FhTFZhRXhjbzJEWTRRTkJKbWJDa1c1djk0UFRSc3ZpVW9yd1Y3eTFtQjd3WG5qVGNTSVNSV3VETUQ4WU81dlNzUzJNK1BkRnVOY3FlZ0FpMDg1SkVoazFOSjIyQlRJVG41M3N5bUt6cFE3T2F0bVA4TTNDNDViblFMcVBMb0pvRnprTU8vNjBNRnR4c2t3WlN6dzlMRkFQbHJmT0RPeGJJR2N6aE1CRzVLd2tONTM0dDlranlTa3J6d0FLN2t5Tm16Z2VhMHh4ZlFQenVzNUswVGRoc291cTVCeUxJZHJKK2dnUDRub3Q2dGV3L0tDNmFlRmJuM1JtQ1h1dXpHRlY2QWVuOFBLZkp2Yzd5S2wrVU9UWGVpajZmM0VsMVpGNVd5M2dpSkt2UXhoaGVoUWlUREZNWjNtYnExb1Uzd1EvVUMwV1dzcXMvOTdkZ3VBZ0lDQWdJQ0E0VDc2UGdDbEpONFJBNHBxN1d3Y0pxcTR1OXF1eG9jd1dxazdsSVk4VG10UGM2MlEwZjFIcDE0Uit0QVc4QXdGRjdjZTM5ZWxsaVNSV2wydlVtaEZmRnEzMFZkV3B1czVaTXBUY25wTmNya0pwODNrNEY4alV2akdaZXlRQjJ6Z3pPWDZ0WEtWdEFIKzdqbTB6Ulg0UVN5ZGRLNHFjWUg5M3pyK0hpejgvTHQyVHZDUW55N2VhRlJmUnh1QnYzZVBFaUo3S0cyYnJDaDNiMUdObVFtcXJIdXB6cmh4Kzltdk04dXZQS0JyR2luaVIyQUJXKzU1bVYyQ0xYdkwrZU9uTXJRL2wwN2F5MkVLY3AzSldSbUJJWnZWbGVxWm5yVzZCRUJoazZwbHl4SFUzQ2Y3VXQ0NVREMTl1Sk9NVjc4N0Q1bkV5TjZ5OGNxRGN0dHVObnNGbEFhUitBK2ZSSytoTUpUL1dEclBtTHdzNlpud0JLNmdBenBnME9xMVZJVGNlVkJick45dnRUWDIvWWlmOE9zbSszN1JINjdWZitIcUVkOXYySS83L3R1UCtIYjVmYjBuMzJkZk1mT2c1L2g3c1g4UHRaTiszcksvaDlsamZaOWp4KzMwbzkrMzdNcjluMmhHL1oyNC90NkdmMmRaZjdDdmgvWjY5bjdmclFkK3JxSC9OK3RTL1o5ZzMrcjBVZStyNnVqOW4xM1cvVjEvWDFiMVgwK212OUo4NTFWQ2RXYmVjc1Y2RThyaUE3ZGVBbzZlcXFNTnozVDRIRjJ6SktuRXk5RmxJN1RCTkt1SXZlYVV1T2E5TDVaLzZNVEc5QS9sVlFXeHI2VXZWYkljekhISXNMei9SREZBbnVva3BpUjE0aENDK0t4M2hEWTRRdXM0bVVuNVRzU2xSRVdGNVo5aTFTejcwdThnbC9XYXBaU0gzc3dRajFsS1JUTjRkRCtXUEZWd1I4c0xucGw3bTBQaUgzNUViVTh6cytDVzd6TnI0ZUd3U25mNUtpVTdqUmt4anZkUmFyc3h3bU9pQzc1N01nMzVNTXM5aUhQNmZRUkptNFV3Q01YWEtIUGI2VHJIcUVBM2w3RG1LUHZLUmRROHM1N210VVorZ09YT28vUncvRFNkS0tsdFFJK3BxS0poNC9BdktzWHJveXVuRHAyd0RmRDB0M0tySGVza05wZEg0ak93UnRMYnBTbU1ESTIwZDB2MXRxc3EvbEdNRTRaRE5xcUcxY1pIVTU4dHBNOTZ0bkxOMWtTeFJxQmVJYm85WGlWa1c1N2R0Y2haRHplZzgyYi9XbGZZajI2S1lmdVg0dXFBckVDb1Q4Y1kvZ1kzQzNuT1E2YVUwTVpiZkVhb2V6c2NRWjlSajlVZ0REVHluOEJiOHdOL1hqcG9iaDVoOFBLRmdEQmFSZlJRZXBHMkFsYzJjNTNtaGJHNE9USDV3SkEwVDZZeEl2V3JONXlxaTliZXduTDRWdjRKYmpSRGFNck5FMzZIN3J0N0VUOFkvU1IvdEJRam1Td0lEcHRiaXV5V0I5ZHVTVTFiREVDOHRycUJ1ZWVQTDEzZ29kbEdHdmlYNmZQWlZXRzlaZjhpNVdKTFdBWHIwZHBwNkJMVDBBQ29JdDhPWUVJMGROWTg3YUEveklDaWFTVVlTMmozM3pDRmhtaTh5TkI2eXgrZU5TVHMvMW9HVU1WaHNsRUlCSytQV0U1dmxjd1d4bzJNMERWMDZ0NEdVcEhTaFF6UHhHanVpZGkxYW5nQUpUSVJnVVpsODlacUxzd2JxRjE1SjhtM2dVZW5OR00xNGNxMGd5eFQzV29PU3dYMDFQOVdXZys3cnhwNStCN1dKcG9mdWFKaXpoNUNmUGhzU1I3UTlzRHlJSDliZmxqemp4L3pTa0g5a2YzOGtvcTBsTmNGdkdpZzBFckFNNkY2VzYyZzg0RklScTJhVndtaFFzNlc0cEYyL21DeDBTZ2EzUzVLc2VXa1FTNjNxTFhYWkhyak9CRXBpWVFBeGtuYWV6a2hSeW5FMU0zcEtDUzVDWk5YblQzVnlZMWxZN3Z4aHpEcXRCSTFBVVNhVnNUa25kNU84VndyNmtzMERsdmJwNDE2YXlUVzlzU09TbXBSOE0xREo3QjJuZ2hlZWJWb2RodlN2QVdvM1Ruc1VwejNueEQvUTNwU1BXU0xMd1Z3MUt6czNVM0hJMkhsbENyajUvMWc5R0pJV1lkUWZCaUdlZ1pCVEZFc3FISy9NSjBGbzduOVJna0tXcmZXcW81VXA2UzdCTFVHc0E1WS9SZEdVRGJWT2tZRW5GZGJuL1ZTSnpJa3g1WHpKZkUxdTVyWmFWc3FiZ2FJYzNaVEg2OGVHdnpidmc2ZDdhbGdreWJiQVdzQ0ttTDc1UU4wS3NBK29ZTy9wMEpFbThxVFBVNWdZQ0xSZmhrcGlHaEtUUW1TdGExQm4vS29sNmpYb25wcUsvTmZzcXdlRFJEVmdFM2FlOFA0YkZ1TkJTWU9UUHBOdkxhUnpQMjNob2RPVEowZkhubDV3bVd6RXdzYk53SGc5UGcxZXdXM0FydDJEb3dZKzB2cThjSzlWR0NHcjZ2ajJlTXp1RHNWRTlyTmR1dEMrQUxrSzhLNks4cFN0MXVjazVUc3NjZFRzTW9YMEdtSjZIc2xHUUozV1NLZlJwNVFRV1MzT3BYNXNhSzZaR2pmRVkvczd2bENHSWgxK2Uzd01XRXluNUticGlVYXBOMi9hR1U2WEJOVmVoeFBUcytqRUhaL01QOFZqZTlFdldSNXhPNlBxR2ZmZU0rMVE4cE9KclNBTkNpbWw2VVFacHRRZFQ3ZFN0a1JUY0pZU2FwMWZkNFg5OGdZd0V1ekZ4WUlVcjhoSkJtTjR4d0xkeDJJaFJjbU9uTDg4RDQ3cUFkRzlsSTRIN1J0Skx4eElDSUgrVnk2blRyYTExL1dxSHJMWXVQRVdLZ0JiWjMyZkpzYVhQVDRreTF6d3ordkNCTy8ySUp4SkZ6YTB6ZnRtRzNQcnQ1Vm5IRXNINXVRWjJmWERNV09GdjkvVk5ZR25sYVpDM04wQXEyZi8zNDJFdzhSUEp6UHMzZVRoRGpneUVvMURpd0JXdENBMEFQRDZiRjlhS2xZZkZmcVNvUURQVkNqYjRWeno3ZHdyWVNObjBYbVBvSjZ1em80SHNwY0RFSVlwN245OU45RVpnVmtjR0docXNQOGJsTEdaR25pL1MzSVlUVUNrK1NQZmd4eS9IQ1lPalovR1RJZExLclJUVmNXZUxMc2FEdlB6dDZiU0tzQmZzaEZlSXlKalVVRzRObVAySDNUSFRHejk2aHBxaVlrZDIxdWFLZGZ5SGt3cFRRTGxxYUZFVFhRc1RaYnZYeHZSL1dqelk5b0t1S1NyQUd2YllCNEtPaHhqM1YzSkh0TjRJencxWDFnOEsrbms1bXpiUTRrOG83bDZtbkE1R0pheEFYcnVndTQ1VFF4T2lzRkU0bWFUUloyb09oR3Z3bjg2SGt3NENIditxZCtWc0NnRE43MEJ6d1Q2TXhSazZOaGtoS0xvYzNESVE2QUxURmFVRXNzL3pZamVZL2VnYmhzd01GSFB2V1ZsaFNpZVBtb2kySm8xWXJzL3lsYjNMNEdyVnliTWlMTmptZVZjemFUamVMR2kyNFV6cSs1SFV0SFBZM2RvYmFWbGpxT204elAyaitkQ3h2NFdNanV1TnZzbjNvY0lUV3VKUnNJTXdtYUUvY2dva2cxMTlMS3c5cVpBZDlod29IeUk4N3FhanIxRGI2ZnhFTWYwVWtwK2VDUkllWWswcjBHVTdQcW1rcldNSVk5N1VCRDlBSG0wbWNuZEFJejQvY3V6NFBCK3lmTXlEaWtiaS9mZ3NuYjAzTy9oclorMmRySHkrK2FyV1hUT1dWcDRvanY1ZkFGREh2Q2JaVFVETFZMLzBVZUVPcTZ3UUlhZVdNdnVrVXlXM0YyK3dHd1lPa2Y5NkFjRnNOOUovZG9TMnc1Y1NHODQ4S0JBMDcwbDJMcDJpSVZZeFkzN1ZHcHdhU2x3NnpmUlFKRnpYcGhoWkJ3OE9PZnJXQnRDajQ1VXdSSjNTTUoySWEvTldiQzU5TXFyM0pTam5jdytsUS9sN3g4a1R5Skx4dkQxU1k2RU9HMjFsVXNVUWlraVR4V3AxamRsS0l6SS9ZZElFUlQvcTlJamxuUU1uUXNpZFNtS0QzYWRDaFdUM21UeWFhQ1dyZjJmUmRvSkZuY3JnZHU5UnhMbkphTWVNcDJZVkVzWGZkbTlQQkRzaDdKT1RSOVdsOEFKemNQeSs0RnlZczI2VjdSWnkwRFYzNHFlTUhab1J4dktuT1VlMkNLUEs1enFFd0lzZVltS1FVVjJaRkVrT3dPN1hFT1FyVDZXaU5VMVU5U1JYM3M0a0drUGExaTRVSEpMT3BqYU1xZGVKYnhIZm9aY3gwSzlTL0NMUDZiV1hOYlQ3VEZ5MC96UzV0S0xpRlk0YTFOVHhNQXc0dnFRM0lWWVQzRkJGaU16Qi9UcVVKNElpVWFOQmtCb3JDd1ZQRWt6Zktyd0pvc2lwYnpweXo3RVJ3dXRtYXJWTGpoSkM3Z3AySW9uTGdSdHNqbFFZZUNLV3dqazBhNlBJRGUzVTZxQTZNV1RRZ2kxV3RscEIyaWJ5UmExRlYySE44TkhPTW9LMk5SQ0N4cGdpQncyUk5mNTF6YWhnaFk5UDlQT3d3SksyVGFwSU9FS1gySWVNck1DRFIwZW94M2o1S0thekVyMTFRNFBMMUpMYkNNektMOVIzKzZiMU56L3hqekREdTBTVENiUVpBTDE5NjZvb29Pd2lXeVR3bUtjNmpoRGYzVjJJN3ZVRnFvbTl4ZExQcmpNWGRrVW1zdDY4WDRuNVFIbmxmS1dwRFFlVlF6eFJvVFFxUWpIdTd1WG1xYkNmRXRzc0FvSVBjTXdzMmxMckk4ZUF1dUUyTWh4ZU1mUXlTazQ2dFJPTnUwM2Z1VTJjVTJTbTZ2NDBPUnhtanZGbG5WaDV0blBwV01vcDd4UWNWWGNWZjJ2Ynd4ZWovWTlZbytmbHNtZm5BWm1CTjk4UTQ3cDdpTmJnc3dhQ2FhQzBYYXdRSE5DZVpQUVFjTWZHdlkxRlJVaXh6TmliTExQU2Y1ZmZsWllNaGZrWW53UGIxbTVEenBEeVVtclF3bEpvZzR1aTBjdk45dU1JdzNNQVVSei9DVWUwc3QvMjZ3TlhRaVJaNEloSUVGd3I0ZkZKamFqa0RvQXJLd3c5NG1RN3FnL3lpbHFQQWVTeERnd3hhYnZOK2pqYUU4S0VJSFlTMGQ1dkI3VEg5OFpIK1N1L05mV3BGYjFtTENIZHdPdlhBQ29FV2poK0hodTVPd1E4Vi8zRDF5cnZLc2lqMG5QTnZvSGlRYTFNK1c4NzdBTTRBRnAyL2lMVXpHWjlVMmk2MUJWYXhCK2dhekhIeXpnb0xnall4M0lWTk8rNVhpM2owVGFyS0NvVTVENGE1UDdOWVBKZm9Udm4xQ3ppemZMdGNuOFJneGd6ZHErYS9QWVlQT3lCZjY0RjlNeG9nSFlEdUxJT2xSa2lZTTFSdityN0ZLekxrZGtTeURHbDRRd2ZmMkh3a08zbkh3ejNnOEhVQ3FqcVVqdkg4eTg0MisxNExKZW0xSmhhVVl3M0I1R2Q2d0NnNWZJbTl0VCtzMGZ6WmJYeGM2ajZSUUx6TW13RXJiNUtKb0hWczJodjNMNDREWmxMeVhLQlhXRmZ0M3paZUVKcGdMVmxTMzJ6dnYvcnFJNE1NMEhNMkNFTnV6SmZuSE9nK0xpWFJmQlM3MkYrUVYxREFFTkRaSFpXQmk4RndUY0JXSDN0MkdVaHY1NTVlcHlEZm4rWmxDK2dVbTB1dWQ0dzdmekZ5T3dKOEdKNno0RTdtSGNVNlhYZlJibkJTRjVmSGJUeGxnWnFhQk85Y0ZleDBndmNsNE9lb21ETldGNG5GaWZZbzJJUzBBaHpONmhNWWVmOXRMUzRhRUFNSmFVRFl4ZjRUdlZIc2s4V2ZhRGZwOW81blJ1OTl6WFkyalZ5UytPS1h4cUNEVVVuZDR1cVNCMTZqYXY2elUzTWJ1NUpna1czNFRLMHFhYXk1V1d5QWk1aU83WHVva0F3T1prV3g2WjJqd0xUeXZmVCtibnRiSnVqV3RvZGk4S1VVcTlNdys4MUN6SEIzMldqYks1UG5PK3JOVTNrNWgzSzZEVlJSeDFPWmRNVDhwOWZHSUl1MDU3eHUrT0cvaDY1RlBRa1hSTnBrbGRUaUVXMjhLSEJ6UitEeGlUYjczY0FYcEluQUZwZU1qaVZQam0xRW5yQzlnQm9OdWtzWmlBRWQwdGxTeDZHOHR2ZTNieDFUZFVRQlo5VXlzbVZIak14bkl0UlJkNy84S2hrbnpoWUNpU3pxZ0FtR204MGdUaXl4RVE3OHpmcUdxYTFnWjIrNkF1MFJqM200SWhsVlZ6MlU0TytjaTd4M2Z4NmZ5TjkrK0VtcFdZQTYxOUJ3NitJWC9aaVRYL0xYWDg1NVhwME5lTWVYdGYzdlJYVlMwd0FHUkt2M3pGWnZRa0wrak4rc2IyQ2FiZHlDeGI2TGIzUVZ3ZjZER3UwUkxUNSt4bkhOMzFpVjMyZUFEa1A0V3RtY1dDdWVhWW9Fc3BLS055MkxMN3E1R3ZXcGtFK2JicGxHQUJielp2bVdzNCtqNWxVWFF2SGVTZEFZWHZUeUI3NkhuTnFxZU53cGVGZW04NUxuYUFkalJUK1NINFhiYmRjL0ZnbVpVOTlVQUgrUzJtclREUU42cnI2WEZXN3g4bGdFU1pkWWt3dFd1WUhUQ1JWSGFSNFZpL3dQUUVYbkd5YnVhMng3S3Y5VGs5TG9PZTI3NWlUWHkyT2NpV3dzSXZQTS8zZkpFVVcvT0ZKU2orcG1saUxnQ0FURUJONEtESjVOTjN1UW50YzJuYlRIMXN4RUFkcTh1aW94b01yTDF3ZFdVdWF0L0FwS0JXdmR1dnJ6THV4YzRVZGhXbmhMZGdLSmpnWW5LbWs2T3lKWGVmNGwyaDFyektMSHk2cDJqY1c4dDhWWE8zR2RtU2YwUGdKbWVsa1NDQ1UxbzBtUlpSVm5zWWVZODRCb29wZnhiaEozaTB3Y0pRS0dlTEtVTzVhbjhSVEZRS3dESlVoL1p5aDlCRDQ2Y0M4dnZpQ2FIWmVuelpPTUxDY3pxelhOMW5ITGpUbUtSYzR2aUtYeWp4UGhpWGFyeTNMakk4MWwwUTRSTTQ3TVcrU3pvYVBuZldJKzY1aWsyUUlhNmRLbWFETUlkSVdHaU02TlllbktlYU5NMXNhbzZhYTQyK2JKRmcyeWFEL3owd1puL2JSczVUdWlvUWJBS25LdlVmUXB4cERCRGk5TkhrS3dQQk1WMHVmZkVralhjWXV3KzBFOWJ5dGhGNzFsOURrYnloOW1aOFhLclN3ckRyYlR6Ylltbk1ZeEZrQWxmK2Nva2hGZmhtc3JuSGx5c3VueHJxMlNOU01ad1B1UkVrVjYyWFhvNEF0RnVMVE5BTEphcnJ1SVNDdC9nMEk0ekNvemxtTjBIMEJDMDAySE5WRFh1ZStlMFoyeUJJdTdleUVtc2UyMVZiN3JkODJENkx3WlFGdVBDemE2Z3VSQ0NxRWp1cVVoK09xNjlGUHNmQXRCeDM1S1A4V2FnRzUrd0twbGtQTzB2S0MrdGZuOHlpdVlXVHEycllVUlZOQ1RhbDRtMFJQbkJMUnFncy84Z3kwZXlVUHR0V05NUkQ5Z2lTUmYvUkU5TUthcm14UmREZUdnNWcyTFdBQ29nVnFQZExyV3NUQ3NjVGQ3YnhCU24xeHBPZnVDQnl5QnRjTzhRUWEwTmcwU210QWh1Tk85cHpkRXBKb0pJWmxjWTc3UEtjL203WURONzVrNTl1aXhGbmtDa21wSEpDaUc3NnVwU0ZPaVB6WmpReE9LTWwxTUIxTzN1SEJBb3o1OXFKTkwwSFdNM2U0VWpIalNTVVhIVWZicFdPSTlaVjNBaHNITEk2a3dLRk5aZ2xzSzNnS1VRYWVFcW5XUE5ISUJvK1d6RXdOckZIQ0hHS0lONVBVbm12Y0k3YnFPaExncVlxSjlRUmRwNmlERENmY3QrNmQ2V3I2VVN3Vko0bW5Xb09Kd29rYU9vaHRTN3AzWXJ3YWswM2dDakhOazdPdEJOUTh4amNxYTNkQ2lOc0dhU2dkMlBrSXFrR2cxazd6bHd3czVDU0srVmovbmVCWWIrR2tYN084TkNwWHJ3b0FtRzFQODV5VjRIeHpCS0wyTHFyc3lBRUM1bVA2UTZ2QWh3QXlLQmZxbjhaME9KV2crdUNZMW9WZmV1NlRiSmd0SnBYU1BMRkxrOVhtTEtSL29SQ1YwT0NxZmwwSWtUSlRSS1RrejdVeWNMdndHcVFTcVBPWnM0RlpmV1F0N0Zhbjk3VloxSE9STTIyTGFQbUFoa1hVaUt0QUIzMDlxTk9jTEFUUTZCOTlaS2tlaktWYTd2UXVBTnNPSzZzWGgxTENVbEYwam53NG91NnZiT1NTZ2lrYkk3eGg5TlpFbXNROFROajRoQ255UnpwZnVlT2JVQlVOeHBNamowRVEzd2l5M29DSk1DMzJkdnZwMlpxdklBcklhVUUzRWxhUEtBbDB6U1lIdGdGa3ltQTZPcmJWazRIL21ZeWh1MlcyZldIVWQ4V0Z3SVdHQmdxNGlYaEpHUUJWZ1hFbU9ubkU5cmFDS3gzQkZMdWxtY1k5K0hHQVdISDMrRk9ndzk3VkFGMXZSTUZYZGpMTWl0d1NacnNHdlZvZVhkYS93dFFpNUgzQU1YYm9US2l6ZzNGd25UZ3FOYXBqZC9KRTZ1NFJYZHV6OTk5SFhraG50VUF5VHlXZURyMzBjV1NXSkMranN5Y2ZaSThHblE0aGI1M2w0TTZSMyt3VTcvWXpYaHlLU3lXaGFCOWwzUmJnMXozaXFqamhWRHBDaEg3cFIrMTFlV1k4QzhhZkJhOG9Sd1dubnk5SFFVVlZFalY5VC9ENDFLL1kyS1ZNZmZGWEdhNVdmRnJpTUtUTHduaVAzUlI0a1pKOVN1SVVFZFdIOUdxUnhLTXN6OFFXNkpiWFRTSU5KcVRVN1M2VnFyMUpORlFwSHdLVGZ5eFFEanM2RWVQSExrM29hUWpFZk5iMVFPN05uK3lyS1U2aEpOb0Z0QTgzd2hOaXJicXBWbm9WUjFkY1hETFNxTEVoSEFYVXRXRkNjYUFMRk44UlpLVlozOEpmbmJOOGttODZIYlo5R1AzdWVzNlpNZGhSOTRzZldjcGFFRU41a2VianB0SGZXdVB5dk9meVFXMmFqaG1MOEl6WXoyWEVsYUtlWHBoMEM5SW1kRTdiMUNRNjlmQ3RTZk1jb1RCdElwNFlxWmJEUk9OK05rVnZxWCtxbzFEQ1hhbkt0dm83YmVsWDhSMWpjdzlvcVBSdCtqWmorQVlwUnRGSmRGVm96V3ptZzJkaDJnN2lNYVF4blJKUGROQW1sN0hSWG5yTkZ0UHlGRGNQY0dZOEVzcnFVdVh5MUZxM2pVZWk4MmZBdTMvMEZUeHJEMGlnVk5CcTlYR2kzcC9BeVplUjZUT2ZOTzQrenJGUVROVThuTFlyYjFhcEZvcmk3WUNMcUhSNUpoNCtQVE1aYUl6bHJkczZKK3phSGxwdHhmOXN0dm1TSVVjWEs3VlAzTzhZT3ZrMXl2R2s5ZStjN1NGNURZR2F2Y1VMdmpwVjdCTlFyVmU0WFNUK0ljUzVyalBCSEZSK29MYWpjN2ZaYkoxS3NlazFLQUpRbzJLc1VjaWY5TkVRcEQ1YUtsdUZ0dmg2NW5MQTI4S1pzeWZXZFZjVjZ1dFFCS2luYS83aHFOdWg3L1ppbUN0YzFSYm1CYXk0Y0o2V3lLWE9ycUU2bEpnY3QvOUVCaVhXWStNblMzeWE4cVpmajlxSERXS3dJbmZNbWlDU2lMcFFWSG1sTHNPbGtuMGtuK2MyUitxdDFnVktENWhXZjdlejNHbGlYZ2NZM1lCS1ZyekVBNEFpUTdhbEtaZ3RCOXdFam9vaHJhTW9UYU8xWVhwck5RZTA1THZIZGNUY1pOekk5bG9sZWdKbFNGSzlLSmF1azBVd1JyWW5zUnJmNUVJZ0U1bC9WazcxV0p1UVNMTVF4RWRBckVhMVpQajVCS21IMkRpT2hoZVczTUpGSkFWQkloNjMrRlQ2SjFydkRPcGU2Rzl6M1c5NEt0d3p3OGIwZUcwVm1Hc3hDdEp4aVpZeGZxcStTd000YVQ0OHNrcUloQjhLWHFTWFJJV2N3SnozYVJRMEJZVHIxanBPTm9DWEJEYjF1YmJocFl6WDJYQzYxUUhRa0M1K3pKMUhmSzkvTEo2TlE0Y1pwVUJNWjhkVm52cGNnc0hrOFJGcnF0ODB5dERJblVsWm9odXpPUG5lTklybEx5L0QvTUV1NGxGRFBLWmJkdG5oZWdxRUJveFhOMU5pTjQyaHU5WHJ1OUNpQTVwQ0VGTFAxL3RHbzU2cGY5Sll3OGEwbEVQNDF4KzErc2JSV25IZy9tT2lrVkJDdEYwTktWL3VnMkFvTHZrQ0M5eVdqOTN6bXgybHNiMU9MbzhjSmlYOEFFb0V3NTVlK1ovaXhNY1FhdGg2cW9hY2xGT01BRDNrbFR0cG00R0tCQ2ltTnUxd0RaTHBBdFVCclUvRlh1emI5a3dHNXF2SWswV09NSi9CSWwwc0pNaWw5TzByb1J5cXBFRWl0eU9CcFpmOXk0UTg3MEZzZEgwVnlqQXZrZG45MkZ1TUVuZGhNVlRUeXBWTC8wbHV4S0d0eGxMUTQ4U3lGRXRsaXh5T3AyNUxqU2NYMTlHN3Q3bDI2SkxrMGFsUXN2ZHAzcmVGbnphaHZSaFJFbjlaNDVTQ0drVllMZzR6cVd5cDZjVjAxc2pSRUx6Q3JTeG5OSFh4aGxpL3c5enBReEc5WnVPMW04OTM3ZkZvUHBQcXNraXdCZUZ6ZTNVSUJBNjEzT1dKS0RYWDNIYWVZcDNEbURuazBKSlllVXlPb0NmM2NSZDR5RW5BY0NBeWYxNVNlUTc2dWRHOWdNSFA0WFdQR01NVnZBNjdaM1NTUEE3V1VhVzRoSlI2QmlPbnMwU0hEWTFnVzZqMytBQUc0YTNESEJSZXU2d2o5U0VYWDE1N1J4RVcxUEQ4OHFrUGsya2xmcmRWaHZ4Q2VWU1NjVVlnWEhSMEl3WTY3WjhOaTFxRDRTQXR3d094L0dTSlIvQTY1S3ZTZ0Q3RHR5cnVpenBDcGpYcmE4c2hMa2JXVGo3NjZtU3JZQkVCd3hndG54SHF1VmVNcWVBNmsxcCtrVnBZMUJWNTBXQ3NwaEs1eXM2enVLZzMya2YxK0MrWVNOVXFjZWZ5QTVuZUNTYlVRVDNtYnFHMTVNVkVPbDQ1a2l3UWtuRVU1YVh1aVY1alBrb3c3YkhBdkdKdDhiNDg3QVJ3QU9VRC9GV2NSQnQ1VlhLODRpby9aUEc1S2IvT21DdVZsODVpNU1lOWJ3RnQ5WmxsdHpRZHVhU0NmODhVcXFIUlp3SjIrL0YzcnZzOUVuQ2kweERPMkNUTUVBSXVzWEZscllBN1hhZzBEUUpybU1sUVg2aG5ibXZUVmZNNDZud3VIUHpYZEJPZmFzUVdseDJRcnduSkdoOVp0cWJyaDJTdFl3cHR2dHdLcjk5elVlVkd4SzZUd2phZWZ5Umg0ZjhCbUtVbHVVZ0gyQXJDc2FKazZWVlFJL1pjMEpUWThDLzNpNDNIYlVHaHhiMCtkWCtyN0JNVnZvRHJVbXZ4ZDBTemVhejQ0WWsyNHc0L3pSWnRSYUxUVVBpWkFOc0JHZTNCL2hIY1NHeUNsZ1pXZ0RyMEMvcEJDaGppOXhoTnRCTDJiQUFaWUY2WVQ0Z1NhVHZIcVJQeThXdmhzWXJVNUZ0cGRTWVZwemVMOEdkSGpqL0tKRlR2UlZkKzUwUjNVZmg3VFFBYTBaY2RucjR4ellPSUwxQkdheTE2MnQ0U3pUWVlxcTRTemxGVXk2RzhxQXpVWlozUTJUYlE2L0xvTVY5cGJnUXVqeHp2RmVGYTFYbVI0VWFpTUs1Vm55cDNYNTEzcmJmcHNPUkhaSGJzVkNsZUo2b3dVWXNaMVErdysxd0JVRzRSTGhQZXV4Q0lhL2lxMEZtZXdDek5zREdxM2s3d2UxVWlyb0syb2VrY0lESFpBTDB5Z3kweDlhYjBNVnJ2SkpCYjBWZVpmblh3eVNGTmFUbERaQkY0ZmFhbUEvODZVcktmeTdGMklub0ZpWGE1VXdKNURUSEFGUU1CZkU5YlZMVjd0Rm9YOExOWHVhdW92QTZHVTBveG1VdDkrbGtObTk5NGpJSjZBNXBBN3hWQzl6M0hDN1dKbmtxZHdVQ3VaVXVCNWpScTN6dndiVEliNHIya2t0TXVjTndIb3hGeGZKOWtyZWo1N05icFMvUkdENlpKTEJuWms1cmdOTUdnV0ZnSU9EZ014aWM2WTkvcFNYejVrTHFzZEhBZEpHYVRpQWVGNXg1dDNqeHZyRVZ5YjZQNEh5cGFqaEtsTGtBdCtlOXJ5OUZELzh3RFZ5Q2tIdHcwbklMY004UkFDOUJjT3hHV29sR1Yvczk3SDE3b0xuUjJJY2VpK256UnVLTE9iU2ExM0NlL2oyak0xS1BJVG5Dc2ZqRDN3MTlqekV6WFk2c0tTVjhlKzV3MzdGaFR4L3VUZmVnODhJeVYxbEpScDVCeWJLVnE5TWFhVjg5VGQzMi9IN1Z0dDdKRFd3VDNPZCtGTitneGFLN2hqMmhzWlhwa0tWWmtTbjlIb2lTNnZDV0VOSklrNHN0ZjBreTBURkc3RDBJbDNibGVYZGNMVGxuLzZjSHhkazU4eEpPYUtuZHlTbklXWGsxU3krOENlUFQ2SUN0eEVjZXZ4d2NMMXFsbjVDQWVzdzllOWk0dTZKVk5YdTdTR1gvZW5LN3o1REFiYS9ZTjJ6a1ViTXM0ZldsK0p6VVhXS0dTOGk4QzFDYk45V1BmZW5LUGMxUDQ4WmxWMjRnN1NoWWJHUU9oWWRmZzcvQ1laVklVVVFCK0g3OG92Vll5VkMxQkFYWllWQ1R5Z290NUREUXZxZ21SdkliV2UzcWZrdnRPaU5JSEpRU0t1SSs0TFpmU1lYQ0lzR3dFb1VKWGl5NXF5U3Bua2NTaE5QeUhqZ0IvYmZDQzdaQWI0VmI5ZTdMVEtYd3M5b1F0alFoRGsxNDJtdnNITVFhN1c4M25MdWhUanhBLzAyZ2VzMWZkOGFIV1AyQ2tTeE5rQnA4amJ6Q3ZDMnZFNXNGYUhvZFRZSE1tN2c5UXcrU0hzVVgyWWsyRDFCUnNpK1NHcmQyRzE1SjFPMzc2ZFZvZWdYSEhZUjdZNnlkZHpwVklkSVVBcmlTL3ZHMmJlMzhxUXlNbWdWRDNlTlRPQUJiZ1A3d0VPZFpaVUpieXZ2dXZYU0ZmYm40SThwQXF1MVFIeFhKZUtIUmYxTFJiNTJXa2RYV0M0STk4NEZrK0JzbnY5TnF6amg0S05CS3UvcjBCMFY2SnUvVnFISWdWSzcxZVkvYmxsR2JVMVh3K3NYTE1VTWFlZUJRN205NkRsV2JIY3RjZ3RGZ2ZPV2dCb3M4N0RZUytFV25hNnh3NUFUMHlnUVh5d3lSK3FqTFFZelFIczZtdlRINnpFR0s4OVF5aEVsMFNxU3J0V2hOYlJpTmNxcytEY2x4UFFsd3M1cHpjVjRVM1ZHV042eUhHMnd6bFpJTmR6NG1IR3ArQm9sUUdSemV6NE0rUnhHUysvS2FkSUxUN0syMmR4OUxhTFdmRFdBUjZGbjFiT3BEcm9rZUU2N1htVHRTdnh6VTZRSXdVZXlsMUoyUjVYSk9WNkZDQ3B1cXdTcG4wYnRDNmNrMjYyVHR0WFRxNVh1MC9xaEZNdmh3eU81N0c4ejJGS2dPekhCQmZ6Y2FNR3J6R0FnbDAzMWxLU0xiWjFkQ0xMNzdhVlBFbm1aWi9MNGJCZVZWRFg2djlpVUl0RTkzT1JtbUN3WnB2NDdzR0lmMlI1QjFmbWVQdjB0R1p6RTRuQWUrazJmWFh2elpLdVBZWGZjNXdnbkpySFlLdDFaL2l6U1JKZldtWDhhMVRkMDlDMW55MGh5QWgrWk5NTllVeGIwNFp0UWhYQjI1cnRXTFlVYThwelREcFhSNGFFZWFYZE44RzJHYllndmJUTThuWGNQU1pON3FFdlFGdlc3TlRHb0hnK3BPcTRPRXQ2MWNzOUlvam1rdXIwNTRQZ2lGRElMZktMRDczQjVaOUdWQ1FuTjlOVUpsSnY1cU9iMStiVXkxNGVBekViUmJrR3lLRmNFRDB1Y2RhaEcrck5RWWc2N05rMEEvM0pMbFJoUFByaDdaSnZ3NHE5YXdiRVhHK0t3T0QwWExuQlc3M0dwNFdSYUp6enkvMS9QQVhqdmZ1NmNOT3piUDRNQ2lZaTZ0djZKWkdWalUvM3FnQWJOWFkyUHBTRm9PR0ZQMjZZK0VhRTdiamR3L3U1M29LM1BKTDlKbGx3QnFFR2QxZ3dVWlhCN00zZkpYSTlibDFxdVVZQU9mM1JtWEh4c2doQVNMZGRhQncvckNhaUMxNXV5UFBFRlZEZVNuWVdOK1lkbzkvYkFuYitZeUcweDVqd2RXWHFJbkNrSldsZFBBZld3bS9zQzRRaytSY2tLUDc4RVRmTlgybEZpaGxta1ZsMzlKWmM5YUpkSGNvT2FhY2VDci91TGJNU2NYdjl4czJRek5YS2lzcEs2ZGpIVDVVakhGWjZqQW1QVXdWNWRncXZLSjZmTVpUNmhQT3dBa1NleVZvSUI0SVBLWlphc2hocFk0VXJBamY5U1p6ZWgwcEFXc1JPcXcxNmtLRWJpWVQ5bzV3ek9HNnpwTmI5dGoxUzJ2R2FNNTBoMC94TEM5TUlOb0xIczZFbWF5aDk3bmRrc1dTdkVaWTJReXUwRnorZW9nY0Fpbmpaa3NqbCs1elVCSHBQRFdVSmJhSnY0dzl4R3ROT3U1bWdRZUExcG9Ha2xYTWNpVkFaL1Uyd1dEd09taWRjUkV0dVU1SkJzVUFOMTBQaER1Y0VlcHl1Yjc5aTY3VllXU1ZKUTMrTTNGbi9DN3dScTIyZzFYRFlXR0grdW5zT29zVjV4N05xSnkyeEZLRWtRSStNQUMwRzlZRmN1N2JZellIRjc2NHV2WUt6VUl3T3Y5alI4K1A2ZWx6TE5OUlFQWkZ6SWlxRm5JNnRibFY2VDZJdHNEak9OSWd5VkNqNXFkZlh1Z2s4UzhVeDBKTDZjc2x5TzdTL1pvVnhYZXFDRko4eGJpS3h6dE4wNnBjSEpBNEFWN01mcVdJcmc4dlRuT3V0Y1NwMWJqYUYxakcrNUNRMW1YMVJWWTBsZERubkh4RlhjczNBRnlESE81SS9raEhFS0ZrQ0R1c3JLS3hXSzdiYm4xb21zY2ZuTjBMUjhnVmpOTnVEMW5MbUoyM3Z3aUd1QU94Y1UrTCtVN1dKU0d3NE16UDhUdWh1eDhhamFVaGxyZHl6RWplQlNZMFZUbXNXNHhNNWc1R0V6M2E0elg2R1pwUDl0aGthUndFbEZsdHBRdjNZR3BvYnF0Q3Nxem1VM0dqNXpDcDc2c0xRNm1oTVpXQ0l5SlI0TTY5b0RlczlzcHZKZXZTdktJTS9BL1NETExTU09MVjhKSytLOFp5bW05cHlvZDhCQm50SkxaVmUwMHNQay9EQjdBNDFJSk9oT0NDcEgrZXp2dmErWmFVb3VaSkhxTS9IZE83VXlybGZLaVJpUG5Hc2IxZVhjQVZkRGI0WWVYNDlMRTNLZDZRZGtwNVRMdnBZQVFHT25rTnFIOUxUSTdiMXNmZTZCbit6VC9KdDdjSC9lTlJSTjJxZzA5TklmSTZwZW1wTE16V0RhTUtEaEdGbUh6LzRxbEl3VU5VUy9xSFNhREErNzNwVUNCMUhsM01STm04TkNOOFN0Q0RYR2tsZTBZdUJrdDVjYlljd0krcXd5TUlkajBGUVIvdDFTa2N4U2ZqMHlsWjZzZjFkWTBsRTZXS20rRWF5UHdqY09QUDZsakNNbUlnNWlZZFo2eGVTcHhzelhNWUF4cTZtbTlNcm9Nb21HbzVSZTZJVmIrZHdPVjhkUHo2UktESFhRS3B6NmVMaGpSTEdUai9LTy9DZllHTUU3LzJOLzZpejJmQW5Yb3kxaHlCMmhjYmsvRFdraHVMaDIwN1dmQ051TWtwcXB5TFI5c0hMQ0hWcWwxRGxYcEVQSTNLS0UyWmlTc3Y4Zk9IbjA3a0pKWjVyRzV0VTVEd3lLWTVZU3VaM1NST2psanBxclg3NnltcGp1eDdScmhYT0hoNk91U3Z4VjZ1aERoSERGM21OSlVKbG1JaGZqTlZ5NE1zM1hRbzlqbVdNcXJzNWtmNXZBR2s2MWFoa21FNCt0SnZRbE5obUFVcTluSTlLa3lNdklWZ2d3RXJGZ0Fqd0J5VGh5N2QyUzJ3RFBLYnNqR29nZnQwMTdhV1dLU3N5eTUxbDhRUWloZ3Vtb0FHdlFhb1FraWtjQnkzUWw1alFrVDJwd1ZnUndManJQZDlSOUNJY1BTUDVwZ3BVNmpzSVlRM25NYzExMjc2K2xhOGpHd3ZVb1BWMDA3OThjQ29MdDFxSWhUcURGTGFNZEY0WmpSczhpamFpdEpNZGNjMWZGREdtanJzbVNFUDlVWEtJQUlFTXlKVHhVSm9nNUhIZThadmI2WXdJWGR3aHNIbnMrMkdNK3FqVFBGU3c1NEtaY3l6NTdQdFlSZWphRVRtUmU3alRBWTJ4Z1dDeUpJZy9mMnNicEQzSkNRYVlZbTVsZW11b1QyVkNnclpTY1pLclBrTVZlUlFQejYzVWxPbCt5eGsyY1BKT1ZKL05VTnRYWU5XVVNmaC9xVktXRmxIWWtTY2ptNXQrbndPKytlcjJxNUZtM1g0enBRcmsySTBOZmh0c2VMa29PNnBPSHVhYm5iRlE1UTlxQWZ0cXNPLzdOYnhRMVVzdWJIVEZjblh4aE44UERXSTdyRzFpSlhpa3dBeUkrWjNoY1g0Q0RqdlQ3a0FYTytZVEIxdU55U09lY3Z3MUVvZjJxSkFWQXV5OHhEc1RWSkxMaVdvcUdkVm9GcDRHR3JWTFhGd0tHYk9EY29ZaHkvYlVPcGNsZ2ZCUXRKZnVxd241TDJ4NENDQkRwdUt5alhSVGNmc1QwTGlySUVhTmMvMGxITlllN0I3SFlQUlVpaXl5R1QzMTMyZ0R2dDRTRmEyVkEvd044blN1UUNDNjRZVjkybDMyS0ZkMjRZRWxDTk1CamR6SmN6M3FSR2lqSlV2WXZxTm93UkJQYXdidHJxUlY2S0dRSHM3VjQ2aHByQ2lSTGFYY2VLb0Uwci9qYnJVMy9VbEpmWlRuQSsydmpZQjkrZHJsVy9tRWoycVora0lwQmNwSXRvRis1SGhTOGoweGVHa3hUTllqU3B6U1EvU3FkSENoblVRa2NWMWRGTnpmNHNNWW9Pa1RZN1I1MnpYZ2ZWQ2NUYk04K3hBU2U4VVlFcTJyRGtiNjg0QlJ4OHlTY1RBbFBYdTU4NnRhOUtjanRmczFNR3JtcW92Rm1wRjdQV2lnditTcXZqQ29IbkVtWGhEa2lHVTFpb3NiOXd4WFhPMVpxendWUjVBWmFVU2F3dG52bVozMWJLOVBlN1RzUm1RUmtwSC80cHRlemlxT0NQeTdWcmRFWUtkVHdTTEZnT0h2RExqZ2x3SkhXcUxpV3J4ZmxFRW9Ccm5QQzhFKzhQZ2k0Ri9TQlBaek1wMm04ZnRiaVJkb3AyMjlMNnVlTDNiZDUyUFV6N1Yrc05lU045NmtnMWw5U3k5Mnd5LzhmWTNPdGxsU3k1T1Z5ZFhJdGUyNHBnUldabVA4U0RCekFUM205WjVCZzRQV01CUFNKSkF4UE9JR0UyQ2c4ZGE3UUtjdXpVNEZnd2ppdi9nWXVML1BpQzZVT2NDMUt2TTVUYW9tb0RyYU1Dd3dGWGlPalZlTGtHdlIzNm9Id2FmUDdDKzRJSmxnU1plMmlyTmU4VVROeTU4ZHRqS3lMQnNabjNzc1NySVJqbVIzbm52NmQ1MVViL3FuUzYzQnJsYm9vV2JXOWVhTG85d25Wa1dXcklPREtRdEJMaDJmTmFqWENEYktRR05YYmpNcVJadHRiQ3pTaWxzTzFPdElsdGFlWitMZmZla2RTMUhZL1p2ZE42Z245REF4djQzM3piNTVDUVFEZkluQU1rRVZwazRMR3V2Q0NuNVpVREpZUThySEhEZjNIV3hKUGdOTmUxN2QwelJTR3VOME9ENURMSTYrQlFzVkN2WGZjMWVEZ0ZDTG5oVWltWDdLeHAvOENncTZMRzNBNTc1ekd6ZFFSU29GVHljb2hlaHhEU09NVWVPWUt0SWlIRVJOaVJuNkVDMnR5NldIWFRBTXN6cE5LdWx2TGRwaDZrVXJsRzFtWHI1YUNSQlZXNnZQaElOdTJialFuTjBnZzZhME1xTEh1UW5YbEV5eE9PWHpzdWEzZ2I2V2s5VXc4VThGQjN2OVFNeHpOdkRUMGpPY2lncHZPS0d4aG1XcXpZdWQ0TDdYd2xDVVFTYlhMY3BZbGNRQ2d0TWEyanVreVhaWlJnTHQ1NnRnK0pTSmNFcldqN1hCaGtIRENyalB2TUhmK3Ivcjh5TXpzWXMvam5kTnMrR1lIUnJPV1NkOVF0bzdBK0VpZFc1eDF4d1VQYWZudzd4ZnNuY3RydkF6UFptNWdHUmxRMTJBOWpuemFPaVNPU09ISElZTVBWYjloTnppaUo4ZmFtWXJ5cnB3TkFHcElZM2RmUTdMWHA5bTRqdllCMjZFbXo2aEZzQWdtMWp3R05xbk1ySUZlcm5hNUh6eE9EN0hYVE9uWW5mMUs0NzFDUTJWUU82N2FFN29vMlk5blFITmhSMUVMRTd2UloxTStiTnNCd0JoL2JyMTZPc3plVFJOVVNpVjFOUHgxbU1vSnFVMFJhR0NNTEE5NkhVVXFoQ3ViaUdOd3NvOWUvZFN6YWI3VjNHY0htQUpIMXFxd0Fmd3dNRE5ScVRZOWJ1cTU2RU5rdHNWODM1c1M3aTNaeU1OaGtWSlV5ZC8zMitPZG1SNy9MQmNsZTN2VkkxS2w3OEtIa2xOem5Cbk10VmZUUXdYT2FWTFBVQVdJakZ0STBHd2xtb003Q0RDWFVHNkU0Um5UTG5tRkF3N0pobnZwb24vTXYzTXJldW10UWIvdmlJVjF1Q3IzNldQR1ZydGVRa2p4SGRBeUJ1NkZpSnZxK0lheWlRcUdBQlZwVHJobk1vYUFmWjVoMUVFSnE0akNINkJHMldrU05LdTFiZXR1cC9KM3dqVlN6R0J1aEk4NTg2dE5OMGtzd2hRSkxmTjRuZU9saSsvRnNrbjF2Z1ZkOTg1WlpiRmQ2a1BiWFc3TWFSRU5pNnJiVSt4aVpkZXFPMlBBeGQ0eHNoSUJmak9IYnI5L2g1RVFtcm16ekFzQWhHa3A5S21WTkQ5dklJRlNjY2J5UXBxbnpwWnlvT1VOckpiQ2NqZUJ2enM1eHlySXZwUHF4dFlKcFpCQXlqWVU4UHRTUlB0QkUwT0lhT3lSbjJ0czFRdmJFd3lzNmw5aCtQQ010czZBd3ZJMmwyeG5EQ1lLZkpIaXFLUzRXTzVFTzk3QWtlRVVvNWxVRUZjV2pJU0YrNS9xRGNFUXVPdkt5T0ljdVJaNUF5V1ZYRjRBYSs1NXRoT0RoaGF5Q0dDdnlSV25PSm5OYVNGeGdsR2p5MlZFZXgyeW5TcXh4eDRDc2xtazFFNk1QVFdlcUpZWEc1QnlmVklFWkxLMjdXQzU5b2I1Unl1cS9mand2UHlFdk1WTlp5QVNFNDhjUWs3Yzl6dXdsNlFXcWVYMEx1c2VDMThiOXo1UXdSQlpDcWJMMFdoTE5JOGVGdDg4K1R3a1pvcS94bnRla091V3N5TzY5V21uNjRVQkVadGJZTXJwVHI5cmN6QVVXbm5TOHZvRm9GWkdMbDJnOSt6L2hmc1kyVVQzQUlUaWtxRTE1NjlkUjM0VzNmNTNXRTB3VWdJUnVaVVpVVTFGMzZ4aWlURThzdzhNOXZVMEdhcDZMK0VnY3drbjZsRTlXMUg2K1BySzBTNTlPc3lUN05KWG43cFVoRjRtQnNIMmlicExLVXRFWkQwbERJU2s1ZWwxV2F4QXlScEhaUml0elpzcjR1VE5WUWZXNVBQazFkd0RjcnRmZW14ZFllazdaKzBYakVZbldzajRRZ21lNy9ybmlvT1NNRHVUVmRrMTRCVy9MTStIQ1dOaTA5YzlJNEJ0bmRvMldqa2NWY3krclA3aWhYME5sd1Y1UHBLNTNBMlZGRTE1OWtLWkh0ZUhjOGpYYVoxVFhqWHd2VitEeHp6YzA4cWhqQkt3RW5rcVZ4U1JoOCtYeUw0S3pWNCtnUHc3RmdaMEs5ckRsYndGSi8yeXBwUVVkUkhGRGp1RnJ2clRrTUN4ME10MFhob1oweHFWdDY1bzhOYUhsdHgyQTVIRjZKNWxKdHR3QWVEZ2llemxySXJnSERVTjNoSkYyMFRrSzlJSU9xMTd3eVFNZC9VZFF5QnBsSVNrU0R0ZlVZQk55cU9IMFVXZjJ6anphOFI4aG5hN3lzSWdQZXNGdlRrOFFEVW16V1BSckl4YktwVGw0Y2RudWlOb3Nla2VnZGNYbEFOakh5RHo0MmJYa1N0V2dkSjJMSzU1eXArYVlabnN1bDJGd3UrbEhFOUVxNUxCNEJJNzhqYU1nR2x3SDZFR1NWZzBKOXIyb1RxLy9PNWtwRzdtTHVpVUFENEYxUFljVFpWTHI3VjJVRFo4ZGZVS1B2TUlNamRBeUdaRkhWbllJWVlEY2d1ZnU1dmdFbURySHBISEZ1ankrWTFWamZrWUVSRmZiaSt1RWdjVkUra2ZESEZGZjRqekJPc1pTdmh6RGFONks1N0l3SWZORlZZRGxyTE9wSEhGek5HVnRsL1dUU2JYZW5ic2o3MG9YdTVtYS9sMlc5WU9wRHpOSERnYStqbi9YMXAyNU5Iem5iQ3pVWUdBTTE5ZzJ0T1hWMWlpanhPaHcvWWQ0emhRZ2QydDdLUVBNNTBSbVE0bGNTSDFHbFNIanRrVnBLdm84V2lFM0RPYUYzcHl0N0EwK1A1Nkk1UmhaZXVROEtaWHNUaTlJeWRhUXZsZXN2WTcrT0o1Q1RXcXpIdEFNME5DSUl4WGxxREhpQlZzbHRZWG5mUm9ybHdQRm5ZbjhpZlllOVNYRmRlUGc2bU9NRlBVdUxsaVFmTnFkeU1FUGp0bGNPMndOYkhzTWF1L094UDN2WEMvQ2tudFZoc0FzaUd2NUhnR0p1bitON1V5ZktvUUZkelFKa1g0cHNweWthQ0FWM2RtME9CWG9WQXNHOG9xUlBKMFVmUzhjMTBUOFB6a3VoQ2JWbCtuSnZ3eGFMMGUycXdVR1ZFM0xGaElvRnBCcXQ0dkx3ZmNSMjRDczhuV1oybDRiNndjK09UUDVIWmp4c3diMW9ReHJMd1FpNmRpUkJhQ2ZXMVBZWVZ2QkVJQUVYV20rczRMTmRpaHRFWnRlZWVsVEZFN0xvWXllVEVza1dvT1ViOHVnZkx1VklrRVFnWms0QVZJTThQKytSaUZyUklEM3JXRm56S1lUVXgwZWR6bFVaSjI3cEtmbEliNmxjd0hSOWRSK1ZHL3gxS1ErZk13b2x1cnk1dTA1MmoyV1h6V04xWmQ0ZGRtSzNvQ1I5MzNTY0V5clJKZnNteHBDdjY4cndrSFJEdHprZ1NYbDc1YVU0ZXAza0wxMzBFSjlLVnVWZXg4djFiUUtINFZ6Vnl0eFpvWnFmR3B4eWk1Qmp6RFZIQUFWNUVUdjNSMnRGSmk1VmQwV1NXSnZQWGtBQmYrTXB6MjBGVDhwa29UbytQZkN3dlhTWVU4b1NweDgrUFVBVHNITWVqbDdETlEwVThCME16KzVxV0RrdmNwM0ZrWElWSXRySEJBcHRRcnFuWHR4RWhtL1BDd2ZDUjlrNEx1NlZ0ekxaTmU1UnA2MlF3WUNGc29HcGxNQUpoajJwd0JzMXh0cTZmUG4yU2UvQ3RDRU00MW9xOU5Qd2Yzc3dLK1ZxTmQybWtLekNQZWhrQTliVE1hOFJCK0VmaDhaMWR6QXU1cnVDOHJpZDZ0T09XTjJqSTlNV0E4RHIxckJ1Y0FkY0l6ekdsV08vbGxPRmdEWXFxMEg0KzJ2dk03bmZ5ZHhObkhLazMwU3hxQkNzemxYUmtIYnhyQS9zcS92SkU1VisxUHZNeExMT3BYZmV6L1hXekhIVHluWi9aMjJJN0d2ckhQcmhBWTJweDhzN3RkOUI3SHJmS0pCQ09YQk9FU05Ec1hXc04zOGJtcmlzNWhpL05ZcWg5bUdiTytzSmIrYzhFZnh6RjI2YmxrS2F6ZDJHOFFjL3dSQ3dhbnFJQnJRYnJvd0liWjBwWTZWQlVERmJ4cW4rS3lKWlRKYnRaNXhUajhvK0RmYnBKY1VBMjZIeFFCcTV6anVtUXFhK0Q1d2NLVlJrcC9zNXF6R011TVIrUDJqUTFQNGg4REJSaTJ0S3RNc2dsL2RHVXBjdytDcHJuS2R3cWRxcFMvWXdLeU5XSTVtVGZxQTd4d3R6Z1hpYmxKRVI0b09ZTHBpSi9mTzYzUjFzMzZEcHZMRmcwbmNpSUd5MjU5STV1dzM0b2FNbzlQcG1RdkRvY2Q0STVkNzB5OHdJVFJxT0FHTWdoTkZWWUJwdUlUWXg4ODRWMGdUTGxJbVFqK1JnWkdhNm42Q1JURnEzZS8rOVhFakJXTmFZVUExVnZwNzdGeXdYVHZBTkRteXpYNHBPcHAyU21CMUhXbFU1V0R4THNlRUlJdFNKTmNiNDNNUjVENytoby94aHNaalU5K0hDYm9oVE9MWS9abEV2TFFjM2s4R0JMRFY1M2Vtb25sZXNNNGxwZU5OQ1VranBrTjdFY3JTejBXcDMxY1ZreE5CQU1aUm9aMzcvOVcwaEJRRnhxemtIKzgwcnIrdzRyM0tKdEdQQzlkS0lMMk1tblZPVUk1V2Jac2VRdlA0RVR6OVcwSmtSaU5qSDRvSEdVek9lQjc1ZVo1UjQ0SFlCRFVDalZMenpiaEw1clV1Mk1MS09iM0hPcUFtcUtXbS9udmtweWphNUFVOGFIQWJGMjgyWnJVN1hHN3F3RDE5bWg0S2RlMlRRY1F6NWk4VCs3Y2dEQ0t5dVN2NzZ5NncyMTAybkRSTyt3UWVDUk1CR1EzUkNqWUE3b2VlT3FwUUdDMlBUYmxjSndyVU9tVmxBb3psSEdrSDNwUFhQZ1lKajhUSUxRSUtYUEdOVFdkQXMzbWh1aEw5S2dDcUhhOWhuTlFmejFGQnZzMCtia1ZZSCszZVNkMThRY0lEQlVyN3hmTXdEejlCOUxDbVBReEpSRVl2UFVlQ3MvUk9LSDQ2OFJRdEpxSFR6NlVUR2lXNXdnekxkT2VwVzJTMkVCd1h3ekVxRGpIUzE4Q2ZtS3M1cjU1R21lMkVRN1R4eVJLOXdyRFdSdUNwVUhaZmxyVDJLdFRzWWh1ejB0OHM5dEFHclhxZjQvOW45a2xqMzlHUUhDUEkzOTRHelY3WnZSeXNhVW0zbTlMR3BiNHVDZGM0NndCdmpVUW5wZnE5eDEyZEFHUmJYOVBGSkgyUGo2L2FFdmU0eVVaVmcrSHA4UXR0eVBnOVRPdFY5TlBiUjRFdzc5angzNlduTUJWalFjRG40SW9CRmJINHRNN3RNZHAzTlROWmtscUt5ZVFxc2FGZGU4RzgxWEZNRWNwT3lRczFYQWJ2Ny9wcktQdWFyUi8rSEtnREtteWxiZ3Y5NTNZUWh2Z2pXQTBsWW9BcHFLZTkvTWxKbGY2S1RxeWROSWRCSkhOVzl5dzk0WWs5MUZLWHIyRlZ2eWNHam9QS3hiNzlKM0ZMMVVycnkrTFpkTXBhK2ZOWjRsSytMMmZTOXFZNm9BMEEzU0dmcWtvb2ZYZ2FXYkZoNzZSQm1NdnNPd2pWS1Bja2F4eTBmWFo4VWpWUDV1RDdPN2JFWWcxS0dwTEYxNnpQRGpNeWpSTlR5Y3Nma3ZDWElJNHBSa2pFOEVsNTZzd0FqbU9Ob0hBdmdTajZFWlh4NDNlMTRJSlh6S3pSM3FpU0N1eWx6VitDSVM4WnNtVkdDWUtrVmlXYTZyMU5aTUVOS1BzaGlmU2RCQlZ4WktkeG9MUzRYZXFPZ2hyZjZoTlRNWDhsK0doVk9OUGdhY2dRRnlQcWpjZHlHRmZWTDJzT1ZVS0R6Z1h2Yzc1MGluWDFOaDBRaEJMS3lyMGliUXBHZHpoTXNoRklTZ3RIQ1p4Mmp2Nm1ZRnRzV2xRVXp6ZVJLOFp5WTk4WThhdW4rVzdETWdKM2pnSUo2dnFRdUZSY2pqVWlzWVgxcEkrNGppWi9jTnk2Y1BZTzJBb0l3UjVUcXBkbDh3MEhJQVZ6V0lLWTBTcTFsay90YWlveDdPZzBhNVRJOWNUVkJQaHhrd1hkdlN6OE5VNjlTRmxWYWlVYWN0Q091eXh0bFV1RmVDc1NGWFJuMW44MmxVR2J4dk4yWmM3ZnJ0cnNQclg3S0UzTzdTSStZdS9zTnlFWmFnMDJGVU9rR2NBaHRsaXhDK20yOUdCSWNlWHdJMThpTGFTTGtsOEpQa2dYdTAxQWR5V0ZlTWN5a21iSzhkKzJoTTErQWZjY0RuK3Y2VDFxdnRiL0FoZzdYM2FkMUE1RS9SNlBaMjNDSEhTUkw5S1NjQ1VHWEYva0RIdFB3eWFkWU12dWhVSXFKU2pNeHpNZ1ZqUStxdGh2eXFRdzNQOFpPWDRkNUpkdjB1ek1LcUVkeHdGMUkwRThLUWdSQ1BVQ2YydGlzZUhjbW9zbVFhWURKWFZpRE85Q1lGYW9Ma2h5ZWdlRW1lYldvRlpZTXUzOVNLY0NITUw2dHNuakhBNmpvQlVqTjIyb3dXa2RjcVlwZTlqYytCMVNjb1JCM3A0VS9PK25pMVNtZkpSSmpnN1NZQ01oaW9tdnJVaXovVUcvek9Va0tKZVdUWUFPck1Pa1Z0MzgweVJaSlQvaFJ1UFg1UnhNOFpjR2V0YVhvLzc5cFNFUmFNeUdIcWRvWEYwanc4MXJxVG83UkV3UGtnNTNUOGNhQXdzeEk0N1NKQ3hSYXdDMmJSSzBmWFMxOUdLc04yRU1uamJtVTJuaGRKNW91Q0NOM0N6bHQ4M0U5SHJIZ0tNVDhIR2k0L2dMbmwxYm81RWttblAvMW1MaGY3SjRTRXc2WXBvNU02V2hyNmJpc3gwTGlFTTFHYVJWRU9SSG9pNC9zUnBGRHdDRUtUMUw3MmxzODNUUkF1RWRVeUdYWmU0RlQyajJYdDR5R01GK2lSYmFaZFBMNzV6eEZIYUlPVjAxYjlPRDJCb3duOVQ5YWVZdkhhNnNsOWdzN1ZDbU4vYlk3WEkwL3JnMm9lKzNLdUVYRTRGeWxjaVlISndwOG0vcnlWS2JNU0hlMmhDOTkrb3R5QlJOR2pJZktCMzdzOFdwaVA3WmZFK0FNZUo3TEJKMFIvanE0c1QvMVM3VzhKakhtcGJubVFrMFRKZzNaVTBFaU1jZTRRcThWNHovYldhV09NNmJnVUg5aEMxNlh4YUh3R1FvaEhkNVhRa3FZL0pqR1dZUGlNblVFTEFjV0oyL25oUHJGbEs4R2hFVjdFYlBjVDQwbi82OXNwVFFmN1lGalpkWVZ3TXlBTXBtT2pUc3R0VUlBSjFHVTQ5b3psS2JRelNxU3Jsb2hJWUZPWHNUenhQdzFOWVdFcVdWeFBLcVNpSS9EYy83SFdpMHd2ZGtkZURSUnBOZG1RYlF5UllKZWlaQWM4M2x6aWQ4bFZka3R0dVRweHI5Nk9YYTU1RUZGWG9XMTZET2RIT29YNE9IZGxsTVlRRUNkK2JwaFJXdkFwbjdmTnJTbWRKZFpBRENxVzRFYURmbi8yQ0g3R1V0RXZ2aFY3OFNXclVpa3ZKTFBUTkZaUTJ6eWhPSWttKzFTTEV3UldIQUpxdFFLTUJUcTN6Rms3M3pWVXR5L1VwOUZhK3dpQmY3Tm5VWDE3UmJDWUJiTnp4TDdYY2xRZkFlUVNLYklTWlFyT0hvcFc1b25QR256Q2dRVmZROVZEQ2R1UjJ6RkYxU3MwTS9OSk1TMFh1ZDhvSm4xQVB0U2Z1eXhOWXlBRnBKazV0OEpCeVhOWGMvTU1jaFBQRzh2U3Q2VzJSYmJhYjhIR0tOUFQxbUhPaUs5M2ZVRXJTMXRaSXMzYjV2K0VJVDdwbFRjZDEzVVBTakI2czlha0JCdHp1bXgreEJaMnhlSnl1RDh6SjhCV2pHVGhqbjVzNHlMMmJrSWpFRGM4OU1uVnZHRlBBTEhlcGRMQ25jdWdGR1grOHdMZ2FVNVhhejhwVGxLTFhHSjU3c2ZlbG0zV3B1L0Nlc1luVkpmNzc4MVZGdXlwbTNaampRT0k0VCtVN0k2SHVOMFErTVJ2NXZteDV2LzEwV213ME1pM2Z6ZWRLeVVpS0pKdVVFdkJ3dllLTWV0dVUrc055UndBN0d4MUNTSmZDNlF1OHRBMGdEcUE0dExmbUN0aWhOV3dtNC9ZemZDdTZqQU50QlFSN1duRmJoSGp4QXJzKy9ZVVhZbEhFUUx6NEUxeTVsbUxhblpiRkE0cEtMa3FIVndTV3BsYmhRbmpQZTM3ZytxeWFCUks1Q1QrL01VVG0xRmR0YmN2alE4V29xSk1jdGtNeEljd1V1bmxkRXhIUXo2eTVzcytxRHVJSU53dTJnU3ZTM2RJRFVUUmpxY05Md2JIZUxMbFgyS04wbkZHaTlvVXlNelpZMVZ3QUd1NG4xLzhHRExEWHJTS1JFR1drajZxd2srUGl3dDBaWUQrQjI5M0o4bnBuQTZ0V2NRSmt3YjZ1UVM4cHRXNTdJeFNFRG9ycmZyNmFqWjVtUVg4OUdoNDhBelNzNkVpZjFnOTQ4VXpNczc2OVltZ25EZm05WFUwZVlBNTVJcnJ3SFVZODJSWnp5VzdUeCtvYVUwOTNrSkNQMEF1eThPTTUrRE02REZyRFhaR1JRL0RQVW9rVzc5bkpHVmhWTU91Q2plaEMwWGlhci9BYUhUQ3M4ZlBRSWFhQThsdmo0Q0MveFVvR2Z4UnRFNkhmd1ZXMmptUHNiL1A5ajVYWnROaytSQkdTaUJCQm9XYnVTVTNSKzZjUXY1bkZRc2F1cE5yQUV1Sm9YNXhvMzRNWitpL3JLUktwb3BVT25wZmUvZHZxcytLSmFNNTQrSktMZ3F2Vkl1aVlCNVBFREtRZEFUbTFoeWxpWE5jaXBDb1FHaElncDFHcE84SU0yMVB0aCswVzZ1c2ZDYmY1NjMvcDV2UXpVU2k4MDV2Vy9qTUF2ekZrMnpBSGRCcTVUb1RPdEV0REFrZnpwbldRR1kvZEpsdEM2cGd0VHo3NDNwRitiMWZTdmx0T1Q5d0ZnK3BmcHVkOFE5WDZSc3dSb0xoaWJRTlB2d2FXei94RWVjQ05RQ3d4OVQ1SkQ1Ylk4VFhRYisvb0dGd3BVZ1VaV2JKaFBlYURBNmhudmkvRWovTjU0L2MrQmVaRmwvOXRlQ1FRM0hFZ2NNTkdMeHRPMVRsY0swM3FpOENOWllXUUxhSG1FS1ltNk9EaEJmSEt4R0NtRTBwQ2o3UTdTMW42ZCtHZkkvNlNFalYwa0FqdUtOMUlYdC8vS2VWNnJ5dWdmVUhGT2RINWQvSGl1L005SER6TUN4SzRWQTBPbHBQWGVjRUlQUlh5V0dBSmtMRFFXc3lwcCtSb1VZeGl3Z0xjS084NEdlRzdtcVdJcmI4dDJQdE8yYjNpeFFYQTZ4WkpYU2hhSG9CdFFSd3plcFVlTXBYSS9RT1ZJUzQvN2srYVFEVW5pQkJaMGM0WVo5VENVb3cwNjFHTTRFUkZZbVNNSW83YmZqUHViZUFSMWtRdWJCZlVIL2k5RWN1dFkxN2VibW5KSkFiRWREcGFRQTJ3RFZTMGwrdXloN3AvdHlFMExiNS85T01CemFFOWZ1dWhVV05hZEFBdkQyanlpRjlBNU1IWU9JcU1TUUwrSHRoS3FlSUVqN0lDNTZhM0dYUlRtYk9pNmIvZW5zd0xmampndkpKTTJuaWQyMG1MeWtWeUxGdUFpWWJ6MU1PQXNabmZMK08waTNJUWcwUlErQ1U1M3NtblNrZG9IWGFzU2t4REJMbS9TWEdWNzBNN21HdWRjY25RRWVuM09SRjU5c3oxd0JXNG1WbkliWVhhYm5UTVJhV1ovV282bmhPOG43NTZpeUVuR1NuKzMzOGE1c1g5U2dPSVBDay9VWVpOSXVxQUJ6RmY2OWlTdG5iOGVrdVVzQjhPNDlFK2xGQ2Z5cDhlNE1pRDUrcGFkKzU0dDFYNk1Rb1NxNit4MDFMY2ZTVkNVZlZ5eEVXY0sxckFZNDI3MU5nZmoyOXFlZm1NMXpBb0I2NGdveFpIamNaWnlkbWxVWVBiMjFYU2kxYW5wQzdEWFRDTmpvbUttUk0wNWZOUkFlRlkzSERhemZUSlhXdE8xVVExdUYrNjY0ZDhEcmFCQyswc0N2a2J1N3p5ekR4VGIzMHBxb05qVEI0ODZXRjZzL2tPN2FiUWNENVByN3JXajRoZ2RkUGxGRGowZDFpNFltQ3NBREpMdU5nZVdDbEdxNnkzSVFNcHhUUE1uWFFZV0lYc3YrSXkyOCsvYkJqQXF6VE9DUmNjck4zaEZla3prV3ZzbDFmbytZK05MNWFBTC9BeXJJRnBVZXZmaktrOTVXVXZ0ZFp6d3JudDVqRTR5MWRHUHZQUzdKSEREMGZ2TS9jdW5qT21vSVp0NXJ6NVdleThQRSt4Q2IvUW1jUlZJRjJkQlBUVU8vSWtTVmVxalJEQTgxZU84MFFmcDNiWS9hdGE2cXZRV1Uzd3Q1OG13bXVZZm5jYVBYamptNk15dWNzOWlkc3VzaitpZVBta2FlL1pqRFQrbzhtNjZndy9wbW15RUxEWHpST0ZJUkx5TEtueHpjcE4rRGRyWDRkN3pRSk5xYmUzclU4aUFMbUFrUUJ0ZUZVdHlkRkNzY1F1cUJuUXZrT0xXbjlocXRxZkdzRWhWd05XVHl4UVpOUGVlVUNDam96cWkxRURKK1VTTml3aXI0TXVHakdUb25BZ3JZcDdldFJoRHVmM3RUL3FTSHo3UGdweEU1RzdybnIyOHJGUFBER1kxeHpSd081NGthaEF5UnlGMkRJWEdQWWo4d2I2UCtxNUxoZXNBellpYkFuUGpFUXYvVDJ3RFVOL3VtbVFoemV6RnJNRXpBRG1UYjlmbys2aU9BOTd4b2FzblM4YXh4aFE5QzU0TnJDb0Q4SVpxaGtyWDhMTGl0alY5TmtZL2xYMm9DWmJrQVRpeDJLMXB1UlRCNGVzNDJhK0swU05SQkYwZFhKMkV3UUJlelVpMEhLRng1eG9yQUZPNDlFOS8xeUJRc3pFelQzdVRVSTQrOHhXdVFOWVpUeXFycmNKUmhHaGt2Vjd2VXFMWHZvOStSb1JORlM4QnVWY0lxME5kWDF6dmJYY3ZKVGZtMEYrRjVlODZUdnpsNml4bzdCVThLZWREQmZxczZLVlVFMmhQeW16TVQ4SVpycmxDMnoxbG9sb1UzejNyZy96NTBHVENFM1B4bU1YY1A5LzB2VDJOZjQ2ajVTQytBR00vWDdab2xwWEpqYkRLOTZGQ3BFT2hOR2FMOUduYWI3UXNGenBLWjZzU2M3dXhTOVlGZ1FEcUFuZTgvbmlyNkUxM3FVZStBUnc2Mzloem9ZRjAyeXh5M0hLSWpSdG55dzE3RHUxcERMRHB3OU42bXFGV0NXS04rWGx0MWpaelFFdjJTbEhacVI0bWxKekc2N055Z3h6THBVUlRTWURPWmppZU5wdmNERFlpRnR2M0xWTFBpMUVMdWxGVHVmaFBJZHF1dkx4aStaR1NET090OHhMZTdML0NxQ29HZmRRN0xXOFJ2SEZuZnhSOWlHV2pWQVROVTYwZHZtendjMWxvQ3VEZUsvTWM4cnY4QzFnVTJOdGk2TmFuSC9WRTRoc3gwQVRUMFJwSExyS00yZ3M1S1FmT0taYVhwQjJOR3N6YnBjME9xU0lvbng0bXRPRTg0cEJ6T2VQSC92VnNYcnpVM3lFVGRER2t0S3VmQjU5T0hkb3hSL3ZUYnRUQVQ4YTBTaXkxY3E1aXUxNHhzaURsZjFhWEdZc05JdUVNdmtJWjFRMTBEcHdRRDdzLzNJMy8xMW5rczFXcmlTOXVxVEg0cmI0dHNIWktDQ1B1Y0p5NnFzOTNsazdkNHdNZzBBT2JrUnpmTjJvQVVrNVJkQUJXTHE2eXBDb2hlWHNZWVk2M2NEQnFIM0JJZFVsSmx0NzI4eElBeXFmbmQ3dkNXUHY2RWJsOStvRGFlZ1lmKzdlTVE5Rm41dHNHMGkyb2ZGb0U3R3hNcFJLUFVIcE05djB6NnQyME1aUnN1ckg0Q1NJNmlPeTNmOE1DVE8zcEcxVVI2R000NFpYaWxtVlhuU2JSOFAzc2d1bmdIczNWRjNQVnN4WXMyRFpWUnZsKzk0cmR6QWlneEJybWR2d0N1K0FpMWhTRUpQV05raU9BT3BVd1FpWGVpa0VsWG1rYTZBemVzMXAyczV3NUY3QUN5bGhpS1VjU2RKK1IvWVlzcUtZZlhOaGJPTGxqL0EvKzZMRjl5RFhDdzZSVmFoL2dhRi9NNlRhUTd3U2hOQmgxTUg4YWV6cGdpb3hwcm5mbVNWRDdWZU5USlZXSU9tRzVCT0JLK1dDanEzOWF1YzZ1TjdZK2trQkp2dnVQdk9KMFFqU0lyWm93akt6aVdQaE1yY3BYLytBdXpib0hVdmxJQjZDeExJVzdFYnh2ZEljbHhQSjhNTUdzdDc1VTFTRm5lSG4wOXBucmF2c1hzT3FDS1BSMnE2bk5UZlh0aXJoQmhudjlyUVM1SXNXWVZBNm5pZmNLaitGWUtCcXk0bC8vT2NwZzdzMEtyUGVaU0w4TkRHZG15QmVvdGRKLzBFNTdiUXZiVi8wU2lEeVFKQUhOZUoyMXhTckxzdTBPOHlMbWE0dWE0ckhPTjEyc3NNM2JjSHJyRjRTKzVtdVQwTzRYWUhobUMxemlmb3lzNS9SQnJjZW5WcVV3d3ZESHpTcDN1TWlqdElCZHE2RHJQY0l1TjBKR0FLTFZEZGNQKzF5UUxiRC9PWk9zTENkU0pjQitnWTZXTlFkSDBoYUVESk5DSjg4dUN5b052NStPOEMrSStNblZpNXB1T294dGl5VEN1QW1PTk5zRFlXakpBWGlYQ2lTVXhXVHI3VE5hZ3poZGpuaVltYjV0cVNpc2RKNnZVTlhGYStJcnhUOU1PYWg4eC9hZzc5ci9abUF5cDNjWUVtcWhxeFZEcUQ1M2l5S1pyanB3N3pUVFdMUXlITlR1RGozOFUvSC9JaXZTN1FEbURKVHBLaFdnOVZVL1Z2Wno0a0ljdW1QSTNvd0s3STRMNmdsNGh3V0Nobk9SZDhsKytEWDFwRDR3Y3Q2a28wWW9hTGJiK25mK0IwOTZHZkJHcUMzZDlxYlBxU3dXWncyT09qdzI4K2ZIbTkyVnBuOFBOdXFsb25xVThVT2p6UHh0cERlRS94dW85TzRHT3Y2VzRBV2grV2thZWF3M1A1MVpyeEczeXJBNno4cGY2VitnVEErWDl3YS9kRlAyL0pOWXkvcmRJQ2VMSXRZc2pZZFpBQSs0SWxPbTFMYUUrTEFpak5NWGliQk5jbm5Sc3djQ3FISjRVbUVOQlBhNDNIM3Qwa2JmVkF4dGI2RU8wRmV3dGhPWnhpSmo5TDk5RkZvT0VxK3ZJY2x1NmR0YWlOdVlYbUl4NkxUNERzaGlONWVLQ3ZYSCtEK0MxOXVmSHk2QlFZRWFFbEQzZnk3eGg5MmgrdEVBVjd5VHdhZTJJZHVVaTh6dm1GMUFYR1dhNHZGeGl5N0NHRXNHaVJobjlIemlYV0ExS3NUK3JadGJaUE5ydmZiWEZTb3RzUXJ6SlMxcWhKaVpGL0REalVxSVBGTWlDMmxpSkFQYWxBVzRwdVoyYmlBbW5iZjIzNDg3VUc2eUUzT2dBZjh5N01WbHBoOWRKYjhMQTZ3ZW9ZZzhneWpuc2h1empNMzJtUWUxT2Z0azRabDVCZFNqTC9LRDEzeWZtc0NVQzA4Z3ppODkrNkFTeE8xVS9FeW1RSThzdnorWDB0RGpsOEdxR0RFRjdaMkVheGJObnpCeU1BemxlUkgvd1lTbFE4YzNkQ01UbE5SWWk2NTNibTUwd2d3Sm9wUml3WEh3N212U1VLdVRDaW9waTl5U0Vwakc0YmpPNjhNbG90eEx1MjI4Z0ZtY1VBRVZzTHIxTXk0bVhpTTVDc0FXOGFXOUM3UW5IRUs0aGFNY2F1K1ROY0Vtb3pxcUVHei9pdEFmQkI4M2Q0VnQzVkhCRkZnV0JpYmhVMGlsRm01OEZ5ajllRnR0Vnd6S0Ixb010QzA0UWdhK016aU9jbFY1aWxSVFphb2w1dzExSXFPeWpLNEo0TFNodUdNRGJYQnlXNmR0c2JCZ3hwelgrYStuM2V3L3FPUVZJZnFROVQ1bHNhVFhwTFlRWXZoLzFtVUszVGRuTlVSMTBNVTVVd1g4UFFQUnBlU3Q2dTAyQ0NxekFLUWdYTzNXMWRxNXdtWWI3YnFCSG1lNU93LzZRdmVZelVlcFdWdlYySyt0bTVHYlIyTjJqT1FXbUpPQk1KS0xGYzBCVEpjYXRxa3dCU3ZkY2ZuN2dtY0xZd3ZqejNxV0g5ZW9wcmMvR2lZVnZBVjYvYlpjR2JQajdpcWI1ZDJKRE5aTk4yWTc5bjJ1QmtZdVl2MDU1TUFoRmIxYTdaSHYxbzFBaGZoR0ljV2hkbWlZeFMxdy9ReWJYd3RCVnRPKzgvcVVXcmdKbEUyODJXVGFtMmU1SzZDZzBZbmIxMGwzb1ZubTgvdmx0SHVMMUM5Q3hreUl0akYzUGE3ODhYYlFTVGNTUEh3Qi9HSXMyK05aTGlnaUtoVTdxUzZXQW53b0o0WVVuNVlEZ01kSnhqbksyemFncXE5QUpzdndTb1htN2NXYll1b3FRSVdKWS9rZmtybmM2OXNJWG44dlJ2ZDlLTkl1NGdONVphUXlFaTBSMmR4algxdlJacUFXc1JqWUhlQXdNWGhINjdWQjk4ZkgrOW85eTZXUDM4c000WDdjZ3AzS1REZ0d6M202QjBWTEF3Q2NneEM2eFZsdUpwMW01dVFxYmppT3c5cEhJSncwK2duQ1lVZHJQVC9kWUI3dHpxV1gya2J4Y2VaU2FPaGdJTGZSQ1QxNFM3OThjQktmeTRoNEVra05BaGZVTURhYUdtbHpUTkpIRFVwWGVRUEc5T1dzdGZMdElqOXJUZ0ZhemxrSm1pUGNEK0Z1ME5yR0hyaDBjNERUVkhMQ3pCVHQxeDNGbllkcnBVRTlZZWJhdjZmbXFpcFg3RWFlM3U4ZTdLbEdjV3Q0czk4alR0aHpZYUhCaXJnWk0xUG9YNjJXK00yaXUwR0ZWbkVlbnh2dzFBVU03eDhXYnJhTW1pM2V0dWNEVThubWVHcVBlTjgxZk1QR0d0K256aUtKZ1ZNcFVCdmN3TmtpaVhNOWZYU3Nwa3hBclVWZlAzN0lxWmRkZ0trTkJza21FSFZDb05va3BhbjREUDhES3lUMS84Uno4VjljTjZTb2lhbU5TS2tWcGs4Z1B3N2dKSllscllpQTlreWswWVZwQVFNNzJKelduTUYrQ0ZLaXdqb0xYcTlKQkpIZVRmdHE0QXM3YnhpaW1uL0hKbVYwQzA0NnZtS2I5VWl1Y2VJYU5Ddm53SHlvSHR2QVEzNFBRbTBnNDhLVkJzaDhMbHpFSG4zMDJLVkxVMFpaQWhFaWZXQy9uSnNSMDZ2NDltU05Na3hIdXU5L0RqYmVYWC9iQW5CK3NORFZ5UFNwOHRheUtKaWtkcHpMaDY4WGpNcnlNdkM5Y1JVUHF2cGozclhrekk5U3RBSVdKRmxLNWdiMXBITDBrQW9mcFJidmx4L0s0TjczRkJwL1R1RkdFTlNydko1NXBMZEtrZE9FZVkvd0lzZGV3N2hRbHk0cFFwNTh3TGpBR1FFaGs2Vko1NGVHcG5lSDhLV0F4SHFDNHdXNktwbkdJMjR6emdWOUhwenNkc0h5VVNwODVZRFYyWldBSzVqY1ZGdXhpeWJLRmlvaC82QWtTYXNyTmVLVVhDQ01xY1hWWE1vOWdjVUh6NEt4eDhaM3FxaVZxQWMzVy9MODB2Qi9JaXJ3UzIzQ1JqVzhwNGo5eXhJWFNuVlVLZUNGaDVOaGlaeTJqZkNwaGptTXRRd2V2ejJrRkJJRnp1MFd5Q2ZWaENmMkpIaDJoSEU1d3ZoYzdnbHFhWHpPd3J4WlZtSmpVMUViWXNpWFF2VW5DeElKcXVVU1R5UFpPR2toMHVrbDVxZUhLZUpDUDhwN3ZjeFN6VXI0ZW5vT0hoRlFFRk9WaUN2dVBGaXMydUg0Ri9VcHE0Q1NpWFZ0TVVZdGpwbS8zcUJVbFYwaXFLMmR5K3FFZVhTV2Z6c2JHdmVBU3RaSG8yek4vdFpTZDlHTkNMTXluNW1VQkNabEtZei9XVlJVOFA3T1B2dGZBRjI2dTlHMVpkd2ZYNzkrV1EvNTBROUFnTzVkSG5lamdhNXRPck9DQm5XOUFuYVpFVFpJdnAvbUpPMGJaQi9yalNGaWgwSE9aeDljdXBqd1h0TndCbVBadVQzR1pjSjFFYmdWN2R0YUtjc24xWXAxT1JyNnNhZUdSM3NCcFhRVmZlVkJDVG50K2prOHFuNFRKK2FBS3c4UmlUSFlxc0E2Q25nM3BwSzJaODQ3VmNkOEdrKzAyVk80QlV0aDYxOFpkMGVMVW90VTM0enVCOGZQOVc3amZmTEorM0pGNWtzNFNDNVZGbGVUKzJGNmppQmVqajh4KzhSbFN5OVVSejVqWmJUSDBDbHB6OVMwWGU5OGg4NUJ1ZjBTN1RaQTltZUNtYzFhZ0pSSFp0QVQydWJuUjFJRmRmaEZ0MVpMaU9lSVMzMk1jTk5YelV2YVBWMHlRRDUzVVVLL295eG9HdW4rbkRSVm5nVVM1WnJvc2tyZ3RkSElnOWtFdkVLcWI5TldTSWcrazdLc3BzZUNUVXh6a2h2WmhuNDUvakdJV3hQaWZLOENoYXliUml1NmN6ZndLR2kvSktBTVhUbm9ZTVA3eWNIL1BSdE50djFRYzdKa25McG40czVMVEd5RTkyNUpGY0NzMGlRWllhUEFoaWdCRDI3K016czBKVkk0ZnJSMEpLekJLQXNYY0grd21vaWxtRVgwL1lBdmlsRDBOYlJ0L0dnN0ZHMFdlMHZXdUxtSlcwRnJDanVxNGozNVQvMUh1czlxRFlWdDVRQVF0R0g0WG94cUNabldBY2I4ZEVud0JhZXAxaHA4NUp1eTdvVjRYL1RZNXYzN01wWXJyc0NtejJaZ0VJYWFqTnFhMlZRYytqNDc5T05XNEJNUkJOL3l4L2V6QkcvNWUvL1ZOeU9vNG5oL1Y3c2NENHltNEtmVEh1RjNTZ1ZmUy9ZaVpWbWc4dkRKY2Z1NDRlYWVyV0hJYnl1Z0FRcTluamh0ODRwZnFoc0M2YlJLRVBqOEdieTB6Ly9Hck1uck1nZlBGU2ZQTTl0U2JFWVNGU1I1Yk9ibXZkako4OTg0bjJ4cjI3OXZlVnVvRzQza0RxcU5tU3ZuTlFwTnVzUk4wSjVYdTFwOSswb0JqdCtYemVycVA4RExOeGNIbGdVa0NtTFRPRWQ4ZmwyMjZWZzd6bFRQN0Nqc3VldkYvZmh2UmtTcHk4dlFSbVQwSGswOHdCeUEyTkR4d2ZoZHAvSnQxNDN1NFdqSzNkZi93cHZTRVVZSWZZZFMwcHM1QWxERXAwaVRBQVdpNVhFeHdWOS9RS3RLb0FZTVZ6MDhHYlZJbWZ3amxjeFZnSDlESisvQjMxSzB1RjdNdDBVWHBmOGNNTHg3RWVzTjZyTVZJSHVtWXRGK2pqZ0tmOHBydFRGdnp5c1plejkrK0dsMnk3TEJGV2J0V0NwdDNMK09nNWxJZ3ZRWTAwQWZoV2pEbGhDV1VoM3lZandoeDdaaHBETmVBSnJhbGpwOW9aRjUwZGl0bHVYeFpsNGh6SndiN0dZdlNtbFhhMmZKS2JVby91c0xiNkxSdEdiY2RrbXpud0cvdHJHcEZSVStPTGplUUFSVWpOeGhwTG5SckZWSG1tZFdFb0F1K0NLR3BKSEFmellYQWh4bHdWSzlNV29nU1hiMHJDQVBBMVl4ak1Rekd4VUZaNllUbkVrSzF5SDAxY1NGVHhNVHdKZERDWGhQYzJsTjIybVpZcllVZ1pjSGcwYzRjYWU2WXlSdlZ1UHlpOTVVcllDOTFnN3lYZjZvMXFpMnVqaHNHTkpMU3FzSTNlUHA0a08rMDZJWXorWktlN29rRUhjNUdlQW55Ty80OXRKdlNIa2drQ094V1VQY08vUUpaRHlKSnlsRmhIVUlmSEFMYmVFMnhXdnBTRDhDelNwSEpiSVRwZGZ6RnI1QWNxNWhJamkxeGhkSDFSZnl5MzFXeE5CR28xcWVQZHl2Q2VNaisvOFBtSHNaQ3cwVTBrRnk1TThWRS9vVUpYWjB4aGw2MVE4N0dEajhmM2MyeHkzcjFzWnkvWkJ2TG5NcUN0OXZvekhxVW9CcDZCTnUvbGR2V3p3MWwzdTV2YXBiNWlTNmFnU0RqcmFxOXhyTEp6Wjc1WW83SUJBdU8zb2huRFlwWlJjOXVPMnh4bnRqZE5rTktFbzZVWFRKa05paUxIeHZmMWdMcXllZ3VTZWd1ZXYrQXQyc1RydjJ0N0lTQWFKaTRwUXlONkFOT1ZTUVRRajNNaEpTWVF6a0tiNFRMTlBVTE9CaFgyVWpmMFgyU2JPcHpGOHFyU2dqbVFnY3lEeitpNnRrT056bXZRL1NUNnh2SkdHMHdYZGlDMFU1WHV5WnZHT2d3aE8rMlQ2SW9hYWNKOHF1MzhUWE9pTHZ1b284SlNIUnROTGRueWFXNmFZSTNObHg4WU9XVGdTcllLb2VXTTdRUGtObFVIbTZtM2hvRXV4UkN5WVlWdEoyQlBmWlE0UEp1OUJxTnFmRGVTQVZobEhBSGFucmttZlFUN0d0SHpENHFRRFRZcThFWnEzWnVHclVaRG5nQTRKbEQzYWNUUExNcFdkSm13Y1dBcHVON1RHb0tHQ0dRNnZWZUxTVzVFZEZLUExHTnpscUtoenk2Tk1GTlJ5ejRkQlVyRHh0MEduWDc5V1VBZEhlUllqUC9GTmFydGlodVpQVU1zcnhmd3NFWERvMjc1WFF4V0ljSm0vWHlFUEV4VFkxeGhVNXZUUnlQSmFXVHBPSXVzY2x4UlhMM25GaHpiTzJYYXRIM3hTT0xPdWFlWkZ4SVBidi9tYXFPaGlxK1hmRVVMYTlNVmZjUW1xTGxQSFdBRHFnR052YnJtaDZYaS90NFVIdlZKY3pKNGFOQXA1RXVpNERQcElRUU5sZEFIL0VEZHBvbC9kRUFsb3NQZHdnTG1GallSdWJwaTRYbEdYZFJraG1oL052ODV3WHR0U1BaYW9SemxnY3EyTlhPOU9nY05DMXJtbjZLajRrdWE3Zlg4elhIbjN0SE84VjhGaTdySG5udWpHZmk5aGUvWXdONUpuUjJhUlZUaUl3Rmk4KzZpQ1pKUEl1SWUzbkZzSXRzY0hSNmZiNyt3bzZsMTFyNG1Mdi9aeDdPalBRMWRTM3lGUzVKQkYyMkZxVWxUNjc5VkEzQktFSER1dWExQVVlN290RlZxd21adng0a0dJZkNhc2dUVVVDOFlqQWhhY1o0NWp4Z3Z0UEw3MVFpTmdJeVlsY3VIbzhoZzArK1UyamJwb1pWbUQwVlFqcDcweDJpVGtWT0ZnaTkrT2gwWFFjWFlJalU0N1BMR0dNdGpKRGxFK2phbzhRcTVDTVdGbmo1dDhheHMxZmlOTmZJMlhlbTN1SG1BK25EVHduZWNmN09ONExGM3ZDL2NldmtWRmZEeVprRldnZDk5VW1BcFBvSWJpd1lIeTVHTGpndS9jRmp0Y2hyYTZxaXVGdFY0bDVOcDdnZXJpTmw5MTA2SCtYQ1pqM3lVa1ArNHdMVUNnd3AzeVFaR3NHOGVZWkZaT25KTjJyZ0dvV25hNDNUZEFPUUtjbE5iUi92M1RMVUFzNkgxTEtscjVYYi9TQUQyK05mVEc1cDNGZlBjdjNHdFpybHJmd3VkWURkL1A0Q1NGK0pyWlBVRHMya05BMzhrb0NiUGlFYjg1WUlkbHZYcFlLNUUrVmpvNy95OWRNWWkwYWh2VjVMb3FLY1JsRW4rWk1RYnl5dkF4U2VJeGRDUUtMcENQbWg2dklENWpsT293ZS9HMXliNm5RU29STUt6cFhtV0lNOTZLek40eFRhMmF2MWsxeVVoT3BUc0wyYzczRkhtQnQ0U3RJb0xqNitkeUF5MDIvM3dWY1ljQWZDdm1aSWE0enFmZzk4L3FwTjd4aDcyTkNaZ2VnUTBsa1NQSjlXbTlqK2dQbE81T1RJTFZJMzFqaHVCTmNHdWtWMSsrbWtOMUpIcWhRZDcrUitlN0M4dHd6WnNKNEMwZEdsMnc4aTN1aUtlVjR3WUZVcUU2bFlWZDVTWCtzcURVV0t3Wk1YVzVQS20wQlNUMXpENW1rNzdtRnhSM0FqQzFuNUpPVm1VUEMwZTFYL3FaM2RIZ1Bta0NaRXA0eDFBeE1ueHBZT09iQWxZZVk3aWJHOWxSNVdhL1ZXNzJNcHhVYUlxaFRiaHY0U1Y3S1lwUDVpaXIxL2dUWUIxUVJqcXg3MkNCZGZpMTlxd3J2T0FpZ0ZKVTRwZXowcm1KYlJZN01NSUpndUlxekErcE9JS0VQOXlXdnB2aGlsUkVzYm5DcFRCZFRxZ2ZIRmk2RVp0NVJidXNrelFyaEp2cUJITGwzYWdxL1J0akljMmlyOGlkbVpaMTM0VDVtNEdZVFYrdmxlbStvRkVsMTJoNHJvMUxpTDFPNjdQcTVvVkRZdFFRUWJQcGVLWi9xV2I5aEJDSkNrdUlzQkQ5MDBtTUNqT3dXVUROVzZjUlJ3T1JaRC9XRjV3NjV4OEhUS3hPUXAycDU1SEhkMDViMkxjbjlBejlDL0pRNTl2RUtYWWM3OGhUUkhUcUFscER3eitNZGEzdStqRktOUUNNZDNuV05UcUxRVk1vNFVTT1BkdFM3UGR3dlNnTTJuc1ZXNDZlMDFzNTNBL29YRTJ2ajlIcmZxRE52SWlSRDUrTjZZcVpQNHJpZ056bi9OaXI2WllZRU5zSUxweFhFYmpZTml6RXBEcytWWERldWd4R2l5aC9iR3p1SDlNbWx6d25mZEdmbFR4bE96eVFnMGpTZURoVzQzdUluSVZkenpNT3M0Z2NXRDJQWldCcDIyOWRVTm9WTFJwU0laVjBITzRLbnBKQkF6UXBXblBBb2ZOZUxPOXVXTVdIYVlpQ1ZVTlZyU3luT2swS2RmUURJSlBIWnNWSEJhZXFDb0k4YWt2RWF4bTBNUmxsSjBPVnhOVUNDbUpqZ3BQd3BwVVdyVmVURHdkRXpQOU1GMkZPZmVrTTA4ZllidzNXTTkzZkxVMkdxaGVoV1FSQjhJOXVDbjdIYWFlOUhrTVpBZURENDNyMjdRcjZ2M1RSVVRBd1FkekUxRFlHUmNRa1RYUEVOcHZwOFYwY2FnYUY5dWk0QngzdEM2VFJ0RUFpN0h1b0RabXdlK1UrTVQzdFdqT1VxS1V3YUFtRWNTZG41RHdGVlViUnNqUjVidVlXYkdiMGxmb1BpVUM4WEVjaml1cmpJaTdMZDc5RS9tRDAydDRldE9BMHUxdGU5MXR4SVBXY2M5Uk5HQkNTbXJDYlFBT241andzUnRoSHBqNjlOVm9rM3NPcTFKT2hqdVNlSjFRNHE0L1pmNkpKS3QrTWkxL1hLdlBzZFlHVkRGWmtlRnJZcVl0bDVGZ0VsU09MYnBlbzBzMmo0RkRwQTlmZDhwNnI1NHBMOWpad3E5ZzhEWnNmeVdTMHI0em1KVm1zU1NKVyttQkZQRHJkeXhyeFNXUTEzaGh0RXNzbTV0QTViRjB6MVdrSUJ3L2ZzL3h2NE03d3NLOFpxVG42RDIvbzdjWkNqUFEzOGltTE9BbkVSMC9qZlBwRzE2TU9sdytkOWw0cXp6aExBeHFHZWFPTTlITTlhaGg4RTdyanBTYTBaVlk1TUl2dEg4SllDLzl5aXp6NE92U29Ob1FlWVJSZEcvQlJhTUFpeVdxL3lqb2tSRXVMWnZjcnhyUWN3SXFYYmVMMDhMTXBlYjlabmYrSGJpTnZ6OTB1UWN3ZElWaVdrNnN4dzNGY3BaUWUrandQNWVsRGVHL3h3NUxzWFU4Ykk3QTQxb2hpSFIyY1RYOUpUTnZvR3hMRDlGejM4UGZqQ1Q1TVAvVTFzL1dTaUEwektLTU8vVlB4RlB3Z1RsN1g4SEFrdnpibmxlS2lrbGY3VEt3TzY3by85RS83TVFsNGMyWldXVkRDY01kekN2OVJEcUFHcjIxaUlSQllsTGxPYVRndERzVE95dTdKR2EvcUJmSGFDalhVMjZDWHVRNHkwQ1IzZS84ZkNYVG40VU5KYUI0RnUyR3pFUFdZMFJGN3F0MkNLb1poNWtrSWpnU1FCcGxBcGp3S3UwUnNBRjBMZ3hZMm5VZU5ZOVdzQ1ljZWRrWkxlQXA3NS81d1B4NUZibVA2aWVEUFY1eVBoMzJZdzMyMDRoc0RuZUU5VTlNRmhsMTM0alVWRVIreHE5L0FZemNBMVlEK0Nmb010TUpCTExJclN0dkU4K2dpK0M5TlFUOSsxYUJ0S1pWMk1UWndVU21NcXl4Q0gvaXhsck5qZDJzM2NkOGFLcUNZa2Y2b2k5cTBGUU9lUDVpMnROT3pLendVS1dUYkVsUXdqVzB2QldDUnZvOUYrSlU4ZUpmMzZUUE8zeTdzZ0FKVGFQVloxUjJZcUMrWlAySG4zamFobzdRdHJYZEo0WmtqcTdFenVVN094OTZCamQ1L0JIUy9ZQmM1bFFHUTlHdCtxZEtvRGtkVDdrRDcrU014WituWnkvUjI0aUEvTlVzTUtXQnpvR1R3YjJFdlZIV1BLVUZiekdjMjNmN3pLR1M2emlnQVp0QTRuclRmSHF0WnFXZlU5NGUwaVA5dGhoS1lrazhIWlhnL3RNbjZzMmFPOExxSVdxaGpJUm5kMHNyZXppSWVjUnhpb3NsQ2FBdENtcVc2SUFtaGc1bnc3TE9MTHlRK2V1MjY4ZVJiVGJNaVdteEtZOU1FNWZEVUh5NytWVXZ0OElKRkYzRFEwOUo1aFN2Vk81ZU9pTFM4RjhRNU9jTnFHYWJCUTN3SXlwL3p6T1FZVEx2allqK3hJU3p4SlN3a0FHRERHUnFvWmJDcnBoMVgyMFhEZ0d0c21oN1lHbStKVHd3MzJrSjlxRk1zNUU0ZGRiTGlLUGtFbXNXbnIxbFdRSnhWR2h2Ujg0R2JwN1BqUnd1NGFGb2JhZm43UUpPa2J2eVp3UDd6ZUErYXVuUisxZjlZUGdBeFRXSE5KbzRidG1GemVlTzYzNmx3bGhGeWxiVktLcWY5RFJWUzAyZVEvTVFBMnBUc1F6TnZxZU56NHcxL1RaUW9VV3dnNk8xcnkzc1FsRGxRaFVrczgwYy8rZ3NGR21RVGFoUXRrU1doL2VEWmhXWUZVYVBkdDFFUTBNK1UwSkRicTVaelk1dFJsdzI5d0xUYjJvQlRTeG4rS3dMZDVTTklSRnlua01BVzl6Rko1ck5GZjY1MDhmOGR1N09QTitsNDY2dVFRWUthd1VzOXpwM0Y3TWpjc3puN3lITTRnbWVwSXRVZHl6V3dmdVZhcnFYL0lBZ3I5d2VyNWRKdW85blg3ckZKU2hyejZDNzN4V2diYzFlbGlJYzBqb2o1RjBYV09VQnlWS2lJWjhTRm1XbFpyR3ZZZmZBTC9QdXhNaVpXYjN1bHFwcDVEb3AvbXE5STFGdU9HVTBuNElEc250VTczME0wRVRWcXAvcEdHQlNza2p5bDFRK3Vta3ltNWNyVDVSckhCWDdIa3hGeEZQQm4xN2t5VDFKK253TXpQNFZ2YXpsNkYwcUptbThWOVRlaXFzMzhMb2Rjdyt4cWYxeld2b3BjZ0RsUmN3by9mS0FPTytWSFkxbXJtYy81SnlWMG1SRVRRWms0aVZjTk1ETTBhaGJWVVRnQ2RnczBtMmxZOFh6bkNPMmFGSzRoWEhPRVcyVHNFWlpvUDA0Q0ZNVzNjRUpUbnR0cFlHK1NHWDFQTXpqUjZxMmVaZzVaenp1b2xJOGdvMU9CVnhZNHJHSngvemE1MnR3ZndhVkdVODZUVC9Obll4RlFMYkR5N3BJcVhpTXkwMnhTdVVUb09vTGxEbmR2L0haQXR5TFg2R21IVUpxREFNTldVeTUzUGZFRmFVeFJMLzE5a0E0YXBUQ3cwSUlmRjRMN3diRDZSaEZ0TzRhK2VYNHF3TXNucVRPTTJBY3FETFVycml0YlpBbUZ3VTZoZU96OUNzWjZyNnhjVytCdzV4bkx4ZFkvN0JQSndpTXB4bExYaHAwN096QVRiSDduVzVZNU1sUmVwSzBOY1hBTnZIbkhlQldRWUduRkdYeGlTVGJROHdWK1JBb2tuL2h3RkZBZHdZZkxoVnZLOW5xa1cvRVR3Vll0Y1h3NXM0QXp1YTMwaHRNTGdobThWSmFZQkNacUhNZmJnZUxQbFhqN3NoUVpNbzBEeHdjcGVnaFF1dGV2MGIxUmp4ckhvWU9QY1NPL1JHRlBrMHlGSXhESmRJaHR0ZjN2QzFYRFpXaFdnb3BBR2xrbW90NUIrV3BnR0MrMzlzWGI4S2FvOXJ2V01FQUQwOTNGb0FsQmpyTGhVWWhOdEFYbm5ueGZqY1greURzcE1XRHE1ck5mejNSOEppbDIwZFZnRXBkUGdYOWVkYWRYL1lyYnVscXMxT1FxN1NTcUlBVTNSMXlSZVlISVhLbXVQUFd5SThpd2V5eFY3TG9kY2Qrd3Y1V2dVeG1jZXhYZmJYSTVLT1BtYmN1ZlJPM2FIMG1uMSt0NEoxc1NnMlMwOFBDanNkNXd2bCtGL0NnUmRZcyt3dVUvR2VXZEdXYnNMcktKSmlEc1BpWGxTeSsraXBtS2graDJJbFRMck9vbkR6MW11MDZBWndvZHRLWXQ3NngrUzdsbjBQS01yb1Z3M1RCQUYvUDNibkoyQkJUSnppcCtPOVFpQmxpd05vcWJWT0N1aDh1dHdDdm9FVjRqZDUwQk9XdlFUOWFGNmFHVWh0L3JHRVI4aGJCQ2ZxU1I4VzRWL3lzcXFhdTBmcDBVUmhLKzU1TWZDd0V5WVp4SDJKSVZMSndPNGN1Vm9IeWRZSE5qOUNrVUd0SHpKWGZsZ3ZmRE1wRTVBUm8wbExaRWtDSUpyNWlMVjRGMnRHMS9HRmJKalMyOWdFYXd1RFJPV3ZyMEp2a0drdkpJZmtmcWtEU1pyME04dXNLTS9zRHZGSSt3dEpFOUNORnBMekZ2ZTJKTm1PTVEyMVl1YjduMzRkYUQ4YzNHTmhQbW9xT0dLc2pZdW5pZm5RcTRTZCt0MkI4Q1VuSWZuTXZLWEFqYUwvYnU3ZzRRbGxWci9OWVVXeTdOSTh4NStLQUIxRFo4ZXFEQUh2dytkdHBudjNvY3V0RE1nYlM2N3B5VjY0cWQra0N5R0w4T0FhRmtKdUgxVzdLNE1qbVdWVWRSQmNod3dYQ2JBK0dWRmlnZmVZdm5mN2c0Z0k5dWVJZmcwMXVRVy9ZemlIU1ZYVEY2ZXEyc2RES2wrZmplcWtabTgvdnJCM0F4YnNrL3hRa25ieWhLWGtkRGsxQ3czLzM3VTRmRmM0Uy95RVBndHJIdmZMckpnRFVBeWEwWlJpNkJlZTdsektUOUlvQlVINlFqakc1LzBKRk05eVp2S1M2Slpsd01sazV4R1BDeUM1UFA3MTQwaGg5eFpobGkvSmVhR1VJeUg3RkZFV2ppNVdWMjBsM1RVZWovVEx3eFhVS1RNMlVVeDgyVHAzcTk2ZGpXeHVyQnp6ckVzSlVoRVJFY3lFbVljalNKQXRzbjUyd3RzeGo5T1J4QjI4cXFXdFg3S1ZlZk01WVgyRnRQT3lDUXBQcWxqRlVObXp0QzhkWVJGV01qTm1OZ2I5TTdHaGxkWWgvOWYwSmFYeDEyaE1ScDlBTFNJSWl4TDQ0bkVnK244UmtFOFE5MHpYczBoRmFVdGpQai90elZrV3pUWUdtNUF0aGphWDYvNlZVSlZTZ2J2a2NtdHNrSjlFdFZQOFY5YytKTFFEZHRtVWZNRnZQc1FLdlZmZW82ZktqeElwVXJ0eURodjZ5ZGFjdWkxV3lLM0pnRzVTUlB0U1pjd21mVFllL0c1ZkttbWJ6amx5d2RTRGJTRGlFNmVpRkRYK3gvTHhiTGI2S1NqMVNFY0dWWlFvSzh3RTZMZi9Na2RmamFZWVR5eW9VUkorTmhBa1U5TGtnbHNBWWVBekVIeWVWK29YeDUvMWNNQkZQczE4QWNLL3pPZGorRVp3TWdtQzNHVWFOeVFKN3A4ZS9lcE9VNU5USjBwNHByZDk1U3ZJUmlaVGtjQTNrbU5RVUxEaGpQTTRReEdUcjNlL2M1by9BNFJ3MnlRYStYRzVCaVZjMGFoOUo1SEtSUlZHaXhPNVNhT21JZFJ5QS9oUXlULzAzYWFxT2FSOVZIQjhnbE9LQ2FqOVF6RjY0VEhFVWxVOTJmZVdxTkJzVCtkODNaSjJZSnUxU3UxRU5rbm5qU2lXcTNGa3BaUjE0bGlKYnN4eGlydVlJemNpVEQxWElqUEh0MG9FS2N0ejgvd2sybVJrc1hpczJCSXBmMUYxSGtwR2dhMXcwWENPS2txSk9nUWF6aWZjdmpqYW1DenhpS2hpSEFmWDBxaitSTkc3YzVjMG94bXlKTm9pMDFCVDJRRDNJOTFHdE1ZVzZocnZzVi96VTl3QXBtS2NxaDE1R1dXczJKV0xqK3VUTTV5N011NlNFTGlCajhtSEIwMFFwNlYyeUtub1VKK25RelhNNEd6RjBnYjNMS1UzR1dhT05tdVp1MlB6M2pUUmNNMVdhcTgwTHM5dlY2OWhNalpjbjE2eitVRDhLeW5rVzN5ZTlON3ZMajZialNTRFZEVGlHQ3duQkVlWlJjQVphSVRiUVJibHBMSUd6TG1SWklPb0J1dnNoWGM0YnFwTVc4NnZocG9KVnh6Z3h0VWpPWXBLbVBoOXJZd0ZrdEtaWWNTdURKYXJYc2JlcnpUaUhHd0tDVkJZT0RyM0VNRVlreTFRL2owc2F1R2l6NzRPVXYwbC84Q3hBMWVLNmxjRkJNVzJYT21wTndMbGYwSndDa1hkT010Vk8zN29QVHYyVWJnU0taMElGNDdhRi9JVmhmRmdoNFhqd08rTnlxaXFwSG9saHAyL0RFNTY0N0ppazVTRkx2b1NnTUZXdkFqcXVrdWtud2xDZ04rUEl2aXd4UzdxbnlDTzgzSHp3K1hEcG5vOGx5RWhlc2xtdHdhcXl1aHdhL2RqWnM0OG1sdFJScFNkUEViaGMxTWlvbktFOFFjWWpYWTFCdk1paTRaMk45Sy9kNHpoVTUrTnVIWFF0YmZNR0ZiMGU3d3VzNXZkaVBLZGo3clJyYTRnZ2t3UHdLQW9za1dBeGZRN2I3bllRUmJMTmR4VWhZSHBKQ1BsdHJFWTgxYUR1cFhjZkZHYWJDemVPNllQS3ZXN0dFVFZQVkJRM2hDYlZ0RkdDUGE0cWwwSTlveHpCVXUwSHFXellFYVRLZjIrZTNNMHJEMFVsYU5oNmljSEhXbUJzbkZaS3hnd3hSZTgxemZ1aTFkVk8yYXZJcGd5M1NzV0kvT3BhTkNCaEU1TytWVHdkZVdRakYwcitiQWtMeTliUkFJaGQzK3VHdGJQaGJFb05peHo2OXNsam8zcFlvemo3R0UzNW51c2cyRVkrODJ3d2ZCVERXWThCSklLbTA4NU1MMHBsd3paNkFtRjk2L3FRWkgzWkZHQ2ZQV3B5eGx6WlFqRTlTa2dqVG1rbDV1My9ya2FwSkV0Z3Jkdm9FRnJHcVp5RXRiM2RoYU9rZDNielBmekR3ZUFsdE40emhZRm80RTJMSXlSS1VaKzhRUnlrY05pM20zdHFab2paUXZxRU5VUGl5dEdnTE9jdGt1MzBUQ2hENlpKc3dmZEJaa1JqS2tkSDBIK1FuckJVbDZrRERIa2RDa3ZIbld0c3kzL2ZPdkljWVVsb3diQWJkM05IcDRPRmRUb01KTk1wN3NSYmFvOVlJUVQ0ZWFORWd4bVd0TjdtblNEQThuQ0FldnNnOVA3eWRkVDNUWUhYYTdub24zRm5CYjc5eXA0bUFmbmJid243QzVKSGE4Rm0wL2xmQjJDR0dxSW95SytrRDBTdnZ0b0FYNDU4aFEzSTNyNDJ0K3RqRzkwZU9DUGk5anNIdEh1aWhTWXY4WUhmMTdnQ2pXY3lodk9aL09UVE1abUovNEc2dkdkTnZSTDhCOWdveFF5a1YvVXdNNWxoSmhrR0pxWGFYcWZ1OXBvbTFKR0w1N09HbkRxR2d3YVhPTE0wR1BoYnBMSmZsM1lPRUVMSGVzbW8vZXBwaXNsU21SdCtqMmphMWlpWURQWkQ1L0pYUisrcVlCM2pPODJ5NXF5Z1dhZjhxQjRYZWVlRzk5c3FiQ0d6WkpOTmo5WHB5dUpMVjMyMDVFQmR0dzRXSWNRSzlIVjlJSzhBY1pZVE5oSmJRVmJrRk5mTHNqZUwyTFRDR0cvb1RQTTBWbzJVa25CK3UzNnJjRUorMUhJL2hCUWJUVXcvRHF4ZVJ6YkxEcG8yQ05maTc3YnByNjRCbm5rVUdQdFZOaW9aWGRsaWFYUGhrY2h2MVVXQzFQb3NCRmhML0FoOW4vMGwzSVZpNm40cEcrRjh4bVpOTW00VC9jc1RJR1JSTDBCL3Z1LzdJL0NIcjJKWEZydmdKWGdPNTFJMXBXc0tCVjJIaU1SRXhYSlNHMnlnSmhMYWkzM3lON2VoL2VPanVveEt6cTBocUh0bmxzUEQzU0pFVE5QdDFZczljVU5MREZTdjVOeTMxVFFlSmN0Zm82bitmYVBBTUtEM25OTm03eVpYd3doZ2xPbW9Say9xWGI1R2tnSlA1Z1o1NVRZRmZwek9ETVpCZW9hMDB3MDB1WU4raDJUb0ZCOC9DK08rZTZVWGs2QTM5ZnJ3WjJUb281ekpjdW51ZGZWQ3lzclpCVjhzYnVrd2ozR2ZKWUtoT1gvRk9BRFdocGJScm9wUy81MFdkWGE3RUZjV25iMm5uTVFpdlpJRmIyUWNYWjNIOFJON2tsdWR6QzR0QW5mam1BdEFOOVpUVDRDdVJSK1NsamhtakxMcVFyKzdvNU9pbUxSTDZreFlrQWJYekQ4anBzRG1oU3F2MGQ1M2VtODRXOXBheWYrQjdBWDBvVUZnMHVxb0M2SHhReTN6RE5WdWhubGt2NEZRbDMwcTFsYmdITjNPSTZTV1pCUlNnY2k2YkEvZDJmcUVMN1dHaGJmMFhZeFFCUXJIMjdoUWtTa1hrbkNHVXNhS1RTYWl3TFhWNVl4TENyaVNCNHdrSXdBYTluZVl5MkZpWXp4T3lZcmhqbk8yWEh4M1Rid2ZpUVUzdUIrdGZXV3JPWkxsWmFabGdrZkRaSGUvL1V4MHJiKzZxNU5DQ2F3Y0JZcEYyTnhmVlZiV21SUGxWck1MRlZHbE9XdFNYTUtyTUUzUVU1WHRSSER3Ri9FODlINUJHYXJLa0NzVEs5RFNxSkdheDgxVjIxTUxkVGwzdmpzVGlqak10SG95aW8rMHlGVGV2aHBFSHVScWJseFJ3bi9taC8xZ1lzRFBXN09UZENWT0dmang2aTRLclNRZ0czSjFtSy9qQStwYmtMbUc2WkJZWWltNUJLSHNENnhQTUdyUmJLd0JQNHRna0lNR3JZY2RIK3ZGbi9wSWVjV2pPVWtjeHJ0NUxucXhqU3kxN1pOR2NvdkkxQVNEa0dQWXdwYXV2UkFodmp0ejB6cGJzeDJvRUVVS2lRRVZkRDQrSlVUV1Q1SHVyZFF0NTIyV0VrT3JVRDFBRGcrYmM0cU5sZmxMRTlnZnFROEhxT09lK1hlZFR0UlZvN2F5REtLbittN2VSTUtIWXFSL21aaXhmekxVNjhMQ3BPT3VMaTY4ZzYvUVNzdElUdms2VFpTQUdCVldrMVFMN09QOUV3SVJucW55ZFlWMEhqS3VMMHcrWDB2Vllkc3FwaURLcmNPdWEwRkZXQ2RvalpIaWRabnZzNGducXRsZ09XSUY2d0pYN2xkc05SbUZ6SXErNXZjQi84U0hCTkp0RFQvNjA5bFhZYk5wRTFPVDlCMTFRbjdBUUxtNWNKZzJhTHZZUjlqZVllTDFseW54WkhWSmlOVG9hdWV1azBZT3dCVjhub3F4aGVld1I3dWIybzdTTWpyU01hR2ZZVVpiUG5LSko0eFREelF5ZkdYTnNPTFhkSGU4Q1ZaK281ekxETmxUL2IwdUl4b0t3aDl2VmRLR3huNHF4ZDlpdTRTRlBvSHdLZjlnOHVDK3NCOEFiT1Y3NnE2cWJ5OVdwYmNyMFJvaVZaQ094Y2pZZ082VlhvbTZrRkIrbW13YVAvY29xenhFRnM2VGh4a0pSR1F5bFVBWWJRRXZ4L253TWN5b3psZVdIR004aHV4blBaMEwxc3puNmpOblJ5WlpRYU11THZBck9GRGNJdVBaY1Nub2hycWw5K3p3cDZhakxJTWR4bkJDcU1Rc0JPbnhOZ2Z6dmFCZ3djZkwxNFJ2aUEzTVFqRXUzUXA4M0ZyM3ZDRHZqODRJRHYzYUxDaFgxTWUxZE8raGhsODNkeWVaZFNJMEZma24wSkttNkxtL3ozcG1ndU1vNFoyUVFYTjJRaGtyaGVFRG1mcWZ6Wm51OEk0bjlPQUlNSGc1UTNzaDRtaFdUM0xGT0hRN0c4UlhtT1VCeko0SFN6L1VwWlZTeVp6Rko0bWlpbDBadkQ4S0J6RHBBemN0R2kvZW93Nk9RNDJqTzhGTGtzNWlhTjkzTE94bHdsczY0MXN5WGdTS2RheXFDU3dVWlNSVEdISmVwbjg0dGpZYm1ZL3I5Q0o2QlR0aDBOMGVGS3VFOVN6Mlc1YjYzWkRaNEUrUnh5WWlWd09TUGxuSnlpajJVZ2wxYWt3MzZFZjk0LzFKaXFLd1dUUG15c2grVDU5ZUFyc1FGN2x1UVdqRnBPb1FXbHcvVWl6MEgrT2ZPZGRiWFBDM3FHN05wV1d5eUpUaTYvaVhwTlBheHNJQWxPMG9tckUyQWRvSEFvL1JCMDVXMlNkUHVYdHJUR3hOelNwaWQ0Q2NGRXhER1ZFL1ZkSFp1S1J4VU4zZWtaYzRYS1d1VkVNSGZSNExqSC9LY3R3TVV3UDgvVTZiMWloV3lmMWgweVBvdXgwSVBaRjBVUFdrWVlMc2wxSlU1cFdpa0d5NUlJekw2OHpBOWpZYlhNTDY3czJ6TG9FN3Z2U2U3UXdYRzB3TlVreVRBV0hqa2Urek44NlRQR1pkZ3haRm9VZ3BYaVNBeHdHRTZaTGFNQWVwb1hEVE9zNU53WjR2QWpRZWtRT2I1NGpxOXcrRXl3Ymd3a0VpaVBxM1Zmb1MwWkNnQXJmc3BCbWtpYTlxSkJzdlRzS0lWcndSakx3N0xLWVhmNEE4VXF5dHFCQlROL0dZelBGVnZ3clE4Zk1qSWZhRFo4ek00a1NsakFIL3FrVUgxak1vU3F1cjczd1ZoSSsrZlYreER2alg0cmFhNEhpMzd1bGxTT2dnZUhJMXFweUxCNENuMFd2OFpnT1NiUkxSRXM0L0E0V0FqN3F5M053cUdSZ05ocGRUOHVZbStKRVgwSmY1ejFpTkhaVVMySGZmeXRQMjQ4VGdKeDRML1Q4U1lKL3ZuR2FZNVRRRkN2TDhKWjdmMzhWeEFhcFR6dVQvaW8xQnM4N1ZZZ2pZU1FldDRZbFpZY1NKSHVaWFJKVlhQalJNYmtRSXJpZFN6N1pKQjFEWGR3VVN6Q1RRNG1SSForV0pMVkV1R1Q0NHZDdWtPUmJFRngxKytPdUpLOEQyek0xMG42Sk9FMlJmVGtFSkw3Q2djU3AxTmIzeXExUEdRL2U5WnFFem4ySko2bjFZLzVCQkZRQzBHbW04WWF1bnkzQlVsblNQcjFLbTVEN2ZnTW5vWUwvdzZjQ2VHUFNHUklxUVhmS2x3Y2JhbGJVT2lZRVhxb2JOQkpoZm9WNGkvNXRGWEcwZm5ycWdUMm5SQ0tGcFgxNi9rYUFpeXZBYVJaSVcveVZBaXJxdU1FLzFtc2phS2pkTFc3WjNqRHRROVZBZTJOSDkxVmtwSUllNUxaa2x6NE1qeUV2Mk1vWGJmSER1VTUwZUMwc0xVeVRhWVMvY2NDS01DSlJJckVOSEE2THpmeG1pVTNBbTVmZ1YzY0F5bGdtNEh2bHdseWw2dDBHMmVpak1OajVYOW1MdW9XRUFpQXQreE1UM2k5MXhvazJnS3RoQ2N6dUdTWDBhbnRXMkc1SVNtZEMvNGxkc1NpZ2U1NUxBNDBON2ZwWHFxQy83ZzFiRi9PMFhCMVN6b0NPV1I4RzE1NXhaQnBaWG5EZkJVelVmYzBpZVlTbFQ2NTJ1VkNDd0U3cmNMSUlEc21Ed1JzMW5uOFk3cTljbDdmemMvdEh3M1NHeGFSbk5HWFlZOGxEWFB4SHYxN05ydFc4bDZzTkJjK0lCOTFIYXVkY3hQTEkrNks3RW9YTDM5ZUhMbzk5akREbTEySjhGa0JrU3NjTzVrSUh5Ni92bHFqd0YzemFFUHo1T1NENDdKZTZCMVY3NnJ1QUdSeXMvVS9mVmxPNTRETUlRaWJzV1l0UEJxUnd4OVVaeWlZb0ZEdDNxaFg1YTJERkY4eUZPdnJRcEcwb004bWViUnBCWHFVMnVVS2lvT1B5akFGbjZWSUJqVmtnTHdpd21aMjBMNmxzWHR2aldHSzdvNDhNRTkyY3VheVloR25sa2QxS2gwWTVDKzhJUDFVc2FNaE0xNURuM0NQNDZ2cWZYaWFsSjNScXp2d3dXS1d5QzJvam9KL2ZiMHFRbDdsK3VuOW1vK3V2WmFrcjVBdi9WVzZQdUhucXNCYzZRSWtVYjJpUjZ3V1N4azBZTWhtWDBQejRmWTdsME0vQkRMdXdnWFE0NVJTTCtLMnFsaHNsc0tRNWFJdlBud2ZORTBPWHgvNXUwWUF6dzZOSE1TMER2YVBFbXJ6UmNEZTZMT09SQU9WMjFrLzBXQjZCNzJzZXlVaHFGOXBvWEo4bElycW5ibS9CQzR3SDNZUC9iazhyU1U4eTVIM1pRTGFCQVpaRlBJTjhORFYzNG9RNDQvb2FsaWJUNUdiSGEwT2pjWFBtSmNxcktyU1I0YjNUb3JjN3hlNjlndzc3d0grejQ0UEMvc2RlRTN4Mm1XdGYwOTJveDd2dnc2R2pyTnA1Tkp6TXdDbWIyZisyc3ZLTjdwYTVUNHlJaTlxWkY3Y1JWS1BXU0hvODdaMU9JRCs0NnFoODZuTmJBVWMwcEFFYnNPNmNFRTR2UDBrRkZWYXIxSmR5ZU9WRmZ6KzZmSFRGODlZNTVxdTJ6eldtZ2xFalp2RVpHUk5Bc3paR3dYZW9CM3Flb1NjNHhtM3Z1aFJYUElkVjI3akViSTh0dlVZRW1yUGpLOS9zbDJJaDZRZ2o3NXE2RjJ2UDlyZEg3ZzFNU3JCdDVYZWlXSURCUE94M2tUNlVlYTlDWEs1MFFPYm5lMnNDMitoNVF0cEwxWGhaS25IVFJjaWRjNVRMZEU0N0NOdk9kK3c2OTZxcThnYmxrNUVjeXgxRU5yOWVadTQ0VTh4SDBBSDNLckJPb20vMlBpRW9xLzBQRXlzVzNoRHhsZVNTaDM5aGJUWDNIRkljSnFNSnU3cHNZWWkzMzBEZXFvYXZvaVRvOXdvOGJKR25obDR2amdaMkIvWU0zcjdYZzdiZVY1emJpb2x3ZWxLb3BXcEliVXlmMmx1UElxNDhKOGJSei9EYy9YY2xYZ2MxcGJFRjA4S1Q4d0tUSzc1cGNsbTZEWHZ4UHBJcld5VEh1RU4zaUdCSWRsMWtPcVMwbzJ2WFhBaHVYK1RFSk1CT09YUFE0RTZxVlFaNnl1cXRVZVRRZW1PdmUxeFAzQVFKeTJQL2NsL1EzZ2hpR0ZNaDJwdGsrZnJLSU56TG96SVpISC9YVXFVYmZaTkRWd1psZEx4Y21zOERzWUxDa1Q1YlRJUWw4dlZzUFpKUmpHdU1jQ2ZFV1JoWWM3dks5Syt1MWZYVGd4REZwbWhBWDBoaU1hSU0yZzlNYm90bUhxK0IwRzdjOFFieW1TZzA3SjlEYXpydTJFZGFsZmpGZTh6UG15K0FtcUFCVEc1TEhZZWthV0krdHBCWVl4ZWJWUGgwU3luZU55ODB2TWZVSHNtbjd2ZEdmQVVvOGN5MVpkcnJ3OFZCaThYS1F5a1R6aUVmTjMxRUl1MUkwcWFoN0ZKL0NhRUVPMWc2WkVGMmNPM281MFp6TjdaNDM5OEUybHM2RlQ3bjdwRkQvK2RwYlZvcm5lNmJrdTY5SkIyb2ErbDNXNDVvOWJUSDU4U1BValdqcXc5bmhlWE5paVRCVFdEaFpNMFdjYWhUc2llMERiWmhyOUE2eXZlUTFLeUY4MUdVektFK2VKUGdhdkovdU5tdlhwUXFxcWpwYXdqR2wzaU81V04yUytHR1Ricm81Zk9mT042NHA4K09DZ09mUmRNRWltYWVFVHZ4Q3NkeDkyNTVNSVcvUjdZcjlESWFlODdxeFhac3Z1N0VCc3lQWUdYaGQxbk81TXJxVk84NjE2cDlTbk5POFlGcS96UGJhYXA3bmZuSXNVZGUwWE85dGJ6cTFpUHhncXRoRm44WWxOQU4xbmRXYXl6enlCeHhPU3BlYWFYVE9JNUZlakFnU1lHaXNPUTVxYjFtVXg2RmdHWU81Z3R6QXloQkNMZ0VOaGczZm55MEpScTZoSWw2djFvQzJuUFNGR1Yvb3lmbXFQU3ZLOC9RQjJNeUFaMmhjdmc2aVZHZUoxWjMvVnFtNWpFdno0M3pnR3ZUQmFRc1dhQmdRNjBMNVBRUUJvSHFjZTdBdTJISEhtU3BDTWtDWHNIM2h1UHBnaWdpVlV0Ulhub3ZiUks2VDJRaWx6R0toelNFUlh0dXorMlJoUFFoZ1M5VS8yWTc5NlJiUkZKcGhvMUZ1VGdBMTFqYy9NYVlVc3hZa2Z0RllpTW5Yb3hMcC9rZmxObE9RVW04ZUVyK1BpUjZQUWJoV084QnEvQUtYdEE3VCtHNmpqZ1lTUm1mbEJhVU8xUHBwYlZJZXdVZTR5cUpUUlZjUXFaZVVFUnVwcmgranBvVmg2TkdNWnMzRVFqNWZLTTNmUkpSYU5QeDk5OTdpbEozN0F2M2FEdE45amdYejYvZ3kxcStLM1NFa3FxTlE4SldmeER0VG9vdWRXRDV5elNKRVI1L1FRbS9HOTZ1VDA1cFZ3ZVlUaWJpL0M1aUlucFc4T21WZlZCRlV3OWpUR2FmSVZ0SldEakdUZEZrL0JIdHQxbEdUN3lPeUY4Sk5tckRZdktCMGtGNlRVRXdqSE11TXE4a2FOU1E2Wi9rWndtVCtyUXlBTUpBeE1UZ2dJbE0xNENQUVlCbkVoa3M4TzdWbEpSZGZtakdqS01lOE1HRXlDaHZCYVlwcUlhWjZBU0tUMFlyUGNReFgyS1ZoNTRrWHpIcFg4NXFlRXhSeXROV1hiZmh6MWovMXVGVzV1WEowOElEdnhyZ3E0cnl6a2k0WUx3Q0xVMVloVVl0bTBMRlNuWHNEYmdKWm1wazZpSENjRWVuVksrM3JmWnFQYTQxeVZRdTJuRWlLTWhBdll4Z2VhczhsVTJHV25NL2tRSGx5OGFPWmRBenN4VEVwVVl3ZHRHUDd5YWlDYXZOcld3a2lMQTNyd1lBR2Z5eFJ2ZitKNzJ6d1dROTkzT2tUbTdKeW5NTE8zS3ZDbEcxOTZvN0R3RzNuQTNmMWhxTVZzQ3JvWTdSNGdyQVVmbCthSlNLcVN1cXdqcGNSNkVFdUF1d1lHS2w3UGZ3LzJGanlyZUVGeXE0MDV0T3ZlSEtsbksrc2ROY0tObHFOMm5GTXdQTklvMDB6V2ZFU2kyam5RQzdGSFVEVEtBa1pFOTZKL1NCTVFhTFc2WEFDMlY1WXRqSjFxSFFhV2NBOXphcXBLa29VVldreks2L0F0eWRJRE5hRGZlNmVPa3VNY1lKTC9OM2R6UHF1bDN3QXFIY3FCNC9XajZiYnpIL2p3ZDRuL1ZrbmJCY3FIZkVVcHdSVGJ1ZjNudWhGRVFybkt5OHFZbUxkckViN1JnWlo0d1l1Rm1MdllrRVVFUG9wSHk5WmpiUnJJU0JYOWtzdTBTeHI1Zm9UaEJYODVPaTVmVjlLSWpvSTFqVERnOXZrNHBodXFUUGpvY0JBcUZOaXdLNTZ1K2tudDZmQjdzZXZVc2d1V0lQajdaSjh2WnZNajVZbGlPZUV5SVVDSWw1QjdoZjJrUzRzU1A3dGNaS0NPU0VCaHpvTElOK3ZaZ1pCQ3ozcGJWVnJjM1VjSkUySEFScHlvelpOS1hseHJnbnU4VjJWYWNodTZWYU5pUnd2MWdXdjRIT1BCYTZDcUFMUks2RHRiQVI5UlZVazRZb0dOckx2Q2JKeGJqMHp2bDR4cXE0ZUJFaDlzRUlJUHVyVlFXZHJjcW45Qm8rYmk0MzMyUkpFWmhqRmxZSXM4NGxlblo1eWlHWU9Mejd6VU1RWkFpak5tb040WUp2MTRXZ2hxTXRuVmgvaGZnT3FGM3lVUkhRdDZyY3VrbHpFR2NwUFNWamdpM29ha1o0MnFyc2Q4MlZXTmJoVDk0OWdlZnExK1J1MWJkUmtXeDBaTDZSUWVDYTRKUmJQNUtSTVVSSGMxdXhFQnFnaS9GSGxUbXlnaXY5Mnk4ejEyTlVyanR4VUtGeTlGL0VIOXhUWmZPZVVtZmdWT3NmU1R1WGpmT3Fkb1EwSkJZd0F4MElJUGRSMS8xU0NzWk95bWFjcTNGOHJLY2R3SXZnSFJ1aE80cGxuc0Zud0pqTzU4eDJ2VldrQzJ4YlBYTUQxRmZyalBtSTdYVk5uL0c4dUdhcWVjT0ZKTkhhaDJaLzZiTW42M2NheTBsejhiYWFjUU9md3Fjb1V1YVpWeFBHM21XZkNMMmFvWk54ckJmSDU4TkllaGxiUk9lcUJJdTJEV0hPU3ZoUUxXVnhDL0VTNmpVdXlIL3dWcTY0ckNJaXF2OU13VldpUVVnOCs1V2ZYTHEvaHRkSlJwalFZRm9DaklhR1FCTDZkQU9jNTg0bVp6ZHA2d0h1ZG1EZFFSSzdnYU1iWTU4UWxaQlhzejRwWDZFakFvU2g0UkpCbjEvbk9SUGQ2ODdUNExGdzZyaXhqYjBGVXUvZXZ6N0hoaFlGNE9uOHRZWmZvUUVoTTIvQXJJL3U0TEk5UWE1b1pwMk1NUkkwYXZTY20va0V1Vmxzcm9adlI1VjFIaEozVVg0cXNLMmRXdUNtT0dlUEVsNFRpZjY2dlBRUUhyNXRvVHNTVjV5QkprV0RwZXEzRlIrK1NBc3RxYjA0RUU4dy9FNGNjOWV4YjVlUWVQUzJGeVdSZ242anJ6ZFo2d2FEMS9lNG92NVRUbmlDTUN4RUM0cXlwUmRIeG9wWnRvM0FOam1UbnBtTTZwQzJpeEZ1MWtmUjBTYW1qU25FaHhyUDlLS0hXeWdETDVCK0lyREt0dzBsc0N1anU1cEdLaGxaMnQybEZZcmdCZkVmcmZoZE0vMFlMVlhWamk3SThDL3U5czdBbnY1N0hSYy95ZWJNMldhYm16MG1TdWFWK2Y3ZW50M2hhNHc0a3RpWDFWZGpCejJLem9CZDA5VXBEbkNYVGJnSEE0VmFHSHpRalVYcEYxR050SVZINndxckFSa290SXcyY2IrdUNqdHA1cHNRb2VraDFkYS9mbWt3RDdLd2VyOS9wNmRRRE9vaU83V3g1d2cvc2Rva1BxZDBncEcxSlV2VVV5KzF4Vlc1NHFmVkVNZ1U1QXI1ci9iNWNzMUpPdkJPTENMaVRiZDExYXFnS0dCYUFWVE5QUVd5bzJhTUtNS0doVFIzQm05T1VjQ0FxNFZSOVZheGRCM1MyZEZCY1MyMUZubis4OUkzZjBXbytkVCtrRnFmdHBpc0RXRGxKL09TbHE3UEVuNUI0ZTBHSVlhT1JFcVZ3QmZ5SUJKMCtmMUNhOGkwVENLR0lTY2NrbWlXTWlVNXNuYndnUGRhQjhyanI0ck4xa3RydVhsWmpQMEpYdlRUcXlodFYwcnhBM2RQdFhENTRMbllGeXkzVG5reFZHTTRDN0NIZlQ4SENQMEpFWmVFaGpzSGZNbFdnVmg0TU5XQkd0SjhGNk0vUG9kQzBTT1ZnYkxpd25qNVg2dkNtUFhiVDFEeVorVTBtY1c4dkRDK3ZuYzhiRlQwOG1CVDJOWkl1cGxzZkVoYjc2SzVVMWp0cVF4MXFWTE55QXRFa2ZpbHBQMHRmQ0hKenRGbmF5ZFZQWVNjNCtVV25GaEY5UkE4VmNGNEJyRHJ0Vk14Qk4ybkRxZ1I5UG04RjJnWHM1ejNjMDF3blJaam9GUmo3TWlXQUlSeUN0L0tOaHJmcldPR1Vwd3JCeFJGMFI5Zll4Q2Y5bThzdktvbGVvUXhITU0vaTlGR0pQRFFHQUlQMGJ6anV3K2RnK1k2aXpjbHRIUlQ3Tm0yWkdlOVU1ZlZTOXBMNnFiUDkvRHdGbUFGaHo3MncwejlZcWMreitiM25OejhFV21KaEZyejRQb1ZSekhxakFRMkczRk9Jem13ZnQzYUFuOWptVnJmRmZKOFZIMlk4dXMzeFhxdlhsVzBZZm1oMnlCd0srcEUzdlp1cWhtT09qZXM3Z290Y2t3ek1UZVYrZnZPMVV5YlhUTlF3K1h2RENNVzg4L0xTWlNsZmNCeWIvT2xGbitXbW96UnZEd3VyVlJOcU9DZzRTWk9nZTdnNTRsM3NpUkx5Wkl6bVVTNjBlbTFiOHBYOTAyV21zQjBUTlgxY3MwOFMxWDZWWkladGFwRy9IdDlGUEdWdC9COFFETjVTSTlaSjY1YmtMbk9KSmFyamIyeXc3YklrOWo5V2VTZExGOWcxTGpwMHlxamtpdEJZc0wxL202Y2tIbzZiekI4UWRUVGQ3OXhHNVRuMU93MzRNQlJQVEJMRVd2b2hhSmFnUlFYMjJ2NjVEc2tHZ0J2SVVOQ3FzNWthOXpvaEVua2ZZS3RoMzJ6SXFicWlUbVg1bXE2bForeUVNNE80M2c0WjNiTWN4NThudG5iNW9lQUkvUkIyWjZZaTJYTmN3eFJDejIyNDNGMDhKVDhmRVNnNGkrQnJTY3VEZEFjUkdpNGVIRE9OV2FaT1huZjZrdk5hK2U1eTQxUWVpakY0VFp5L1B5VjhCNXVNeDlvM09uRURKbWlhb1AzbG0xNkszU3lYM1Q4ODYyUXZSdmtFRkFIbnpHNFh5OGNnalZ2d1RhR04zamtLaHJVZGNsemRTZkZUZ2E0MHlMSzdwb0JRcVZlQUVEdEtZNFZIWG1qbXNtekQzLzFQc1FPS3ZScUNIR1U4SGVuTmdJOVdpUlFpbTF0RktoQllUUEFZL3NYdGU0eEs4N2hmNDFDREltTGtqbmZBbDRiTTh5SVJpc015MVNpZDdlVnZnYmNtOWJ1SnJWZE1IU3pLc2tFTjl1S2xEYU9SVWdwNDB2em1QRnhRZllIVUpnRUVnNzMyd1VTbzdmWDR5c3dRY3RXYWhUTkR2Zm9INkRIcmpjdDhmMWhuSmJzRVBsZXBZVTRNOGJpVlZuV3NwS2tETzJvUlFFejU0L2pXcktiRlF4eEFXdUVMNUZIMXQ1TTBWTSswSFZ2RVVnZ3JQN2M4bHNLdVdWbnBzVXJFY0NUSzNkajZKamRCUDg5WHdxakNhbGdvaERVSFA3ZjgyUGtyRHQ2Qk5mcFFENHNHT3pWN3V1RTFMTEJGTCtIWVd4U3dQNkg2L1ZuZEJpbVhyaU9nN2pFOEpINnRhWXRWZ3NjcEc0NHpWdUhsSk9acjBVVGl3a1E4SDVzZ0JweTlobmxvYVRUWU9RQUxZU3IyU2ozVElJZGJMWUZLa0NsNFg1c0MrRW85VS9vVVJkSWp4TzY1Q1VXRmdEMTQ0S3hxSnBZTHNKekxLMnZidnVuL0ttTEorZEJ3UDZzZWp6R05wdWcxSEprK0FQTHk5WVA5SEwzOVRJQ0dDNUJBS1ZUVUhHcDUySkpXNDFRcHBKMjRTOFdBOTc2S0RXWEM3TC9rS3oxUHduenhJMUt4ZGlkVi9EZ0lRT01Ld2pLVDA5U25vcERDN0ZuWEFuNW9DMm5oTjhYZlVvNzEveFY4eDZMSW4rbTFiQnNDK09BRWhVQTJ2b0svOXdjUTRBK3Y5SjFaaGU1OXZmaDRsdEttaEFTZklnN0YxZDRHZzByOTN1RkR3K0xWUGMzTkFwL0lpZFZ6RzFYTzlZaUtubW5QQTJBVHl1UVkvZ3JYQjEyZTRMUVpUZHI3eUYzN3EvZUJrWjBSVEYzeTdCUHdrQ2U4bFBMUVNjM01ydFpmTjIrMzBDOXR0REtmdTczK2FFTUVXUUh3blFPUW1QTUNXazdBRmpmTUVYV0Q0Q0U1WVVqVFZQSmlWV2pWakkzcC9wTWkvTG9YWDJyYVAzNjVqdTV6YjlDV3BROUFTVU5Ob0JuL3A5dGdhc3VBMFc3V3Z6WE5VTmZaRmdrajdjS2Nta3BISndxWGEyeTFsdWxNSjIwV2hENmJyRXdPVVhCZW5YRHAzNy9oLzRJRnpHYzJuL0d6d2V4K3hmZUQrRTc0SmNUd0ZGaTUza2pvd3l3eFFuZHNkN3d6K21EOStVb05rM3ZhUStycm16Z2VicjdMdlovdUdCb0d3dDZ1OGdNUStqMm1KaW81SlUzVVM0bU9OTzJKbThyZThSdm50aTYvTzJQOFlQV3lCT01QdENEWTAzdC9NSGNqUHZkakRjSlBzcGNLbkhRZjZZdkRPWEJsRnJXckRVcWJtcHFTcnVRNTM4dEkxQmhzaHdNNWNaK3RuZDh6b2E0bXM2OUVyL2hpMS8xNXdHbWxrZ2pMRTQ4OUthYkY3MkhUemZQVU5SdzBNeHZFbjFoMkE3OWNNeEMzUDZwamxhVm9wc2JTYXpYWnovMUhJcEc2ZDF2SFR6UzVNYXJEbWZhYm5lMHAzWGQ3Rm1WSSthNnJFUkV5YzQ4cDNwU2V3YU9NSWdPaDE0UVRxWUFSb3VtN3c5Tm9XL0RhVDlaMWdsbzY2V1NSV2REYkhCaDUwTTdpcUdCMm9NbHJOMytkbzFhM2J4dzRhYzJFOEgzY0RuTWpGWjdRVVhVOXU2RWM0RCs5a01YSU9wRjJjYytoOFM1TkFReXJZTkFLbXRxV0E2My9oeUM2cFdoSk16NWV6YldML1dKUnJ3QUQ1RTF6VU1LQ1FEbjZVdXRjTGRCVERUbk1MUktMdE93NUxUN2lDUHNoNWl5dWF1Zzl3d1hSRGRtWW9aZVZ4K0owNTEyYkpLWUMrMmg2R3Mxa0hJOGxiOCt6aUorbGNuem8rOEhOcWg5NnkxMklNQkkzNEpYbTFTVlFLL1hlQktjcTZHeW1wQUhhY0lsOExjNnEydUdtSG9WZ09NTlJWdUFDalhhaTE2RmprRGU0QlBmOWhMSUJleFBLamduVC9HNDVnY0hRVkRVZFQ1UzNRNnBXNkd3NHFpaWNSSHNTdWVuQjc0cE9vVDJBVXRKZFpsRWloYnJ3ajd4QWtKRkFDemdxb1JyRnhlMkNRVlkzVmZCRzlzdDV4UDltV2FOVE42MCtKV0V5d0NDbzhrWUZyTXUyQzY0azI3VDdVME1ReDJTdWZ4MEtXOHowVi8yT1VwMHllNzNBMUlYZHJ2dFBVbWlBWXNoeFhaaFZyQkJIR0dlTjZLMUZMWlk0QnNLSnUxTkVXaDJtOFBMUFcrVER2eGVHSVdBUFovaDZzUStYTlAyNjFIT3FUTFc2Q1ZwMUcxSnZGZkJ5NlErQ0JnSGw4SzFRRXRReEVUejQwT01HeWZHR1BVM0oyNVYxb2h6bjNyMHBCZkFiYURHRGF0c2RGc0NzSGZCd2VSaDlpMk55T2s2N21NV0xrUWhmVSsxOXdhdXNHMVBkTThvODRjTzl6Q0tMV0s1SVUvYWpIdHdJUTVlZHFTQy9JSUtxWHlHRDlFRkJ3N1RGTi9FZDJDRGNmUHFyNE9BbWNBZklYYUE2VkpsS3B1cXlvT2F3TjNweUN1N0sveEIvWTJqZkdRZ0RoSUg5bkhrR3R2dDJuUXV1QVJYV1Q2OXB2SkNzc2FreHRsZ253R3o3bDRzL01waS9zamRaa29nRTFIUEJYTlM4TE1MUlh2R0lOM3M1WFUzWjNDMDJIbmwySHgzbzFubi82ZFQ3WU1qMlYxVVlSSUZNajlFUTZ3RTNaejViYlY4ZjNYSGJ6M2RwRHdLaTVlaXNyU1FoTHFHMGl0MkoxcytrU0dtNGFvQ2M1WURLQ0YvUHJlMThQN3JaTDNqSlo0dDdnMVE0ZjdhdTR0V3J5OTlIdnIyN0twSkFEeWJjMHBsb0cwQzBJZjhPRTV1OW9vSWJ1T1JkMUlVbGh0QmltcVNUbXBZTVM2UjR0dDRQYmo4S0V1L1lEOG1rNUtlMWdjNnBURjNRQ1ZRSUswVFZZeG1EVUVGamxsL0lodDNIRTJlUzQ1WE9rQlUyRU5USkd5UytVRUwwWlhzTlhGSDlyaklKWmYvNmc2cTF3L1ppdS9sOTRqZEdUaXgvV1FFVGxvVTFlM0E1TlRnYmFqbkkrOXJXMWRLOHVzTmVzaWVSdjArTHZINWlQU0tyNFc4RmxFd3ovZjRRVDRJaWhlOFlGVjZQRDcyRGt5ZnR1cjBxWjhVOVE0L2tmUHR1SnBRNWRjTjNlOStZVDN6RmM1bFdqYXBMMW1uTVJ2SWZ0anptYzFGZ3piWEtndmRqeXhyaWdaVG52MVM5L1drVCtaL1JwdDJWMURxTXlxTEx4a3czY0pMK3JGbjdwc0Z1bzhIczIzTjRTQjZlL1l0MVIrTHlsN3RsRFJ0Qzh1K1hTSUI1b0VHQzFLSHFGc1JZVHVpUnlEd21tZWNsakwzOFhaUE5ZUlprMi9jdE9LRzAyOWRSQlVEc0tXRjN0NzZ5SzVxY2JlamlSTVVPUlo2dGlmOXFZSWxxK3MzWGdTdGY1SHJyVTBTY05QMXM1U2FFMWhHcjc2K1RZSXZXSkVRMjBDZzY1VGVuKzJGZmdiOXRudGJYOU5PZUVROVcwSFdLbVFNMWM5VW45Z1JJc2hPOGxHNE5tcmVVbkVkWlYxNWxCTFRHSlRFd3RQYmliUXVuOVQ5ME9ZMDVwb0FxVnV2SFg4WmEvUWVmS2JqaW9WOU5DRGk5c29CT0pNVGUvZjZIZjM1SXpIVXJ5em01cHI5MXNReDEzamUyYS9wV1M4SFNxemZpeG5jOTN5RVU1dC9OTDhETWVGb01nQTByamE2clZjL1ZWWDgvbjl5S2VDdzFzNTBiZlNqYXJLNVZXcGNleXlYYkpDZzZIK3VrbHBPdHY3QVVob2g1YVNBWE1mRVRZZHY1clAzcXNJc2REc0NNMFJkOGpPSnlJcHJOZGpWdXpSeDFxS1g4VDZtS0VXTGRKQjBxRGJnRklYY2JhaEljdElKZ29tc05HYVlOU0UvSCtSbTFpNkIzdUt4NldyK00yVERjelZvZis4b0dKdmVqT244SlYvUXFsMjBTYjYyUnhuYUJhTGErQ1REK29HSFFBZDNxbFpHbEIrTmRRejZlZEFEaXcyd2hjNTJjYzFwWWpkaUxNZkxFZkNOS3J5N2srUU5DQndSbmxyWkppdkUrSmVvS2FoVWJZY2toWTlINjMzWkM5bjA5Q3NXUEcwOHUyMzhXY2xHM2lhM0NNUnNTZm1lTE4rd3hONFA4MHJ3Yk03cmVPM0Zha1o1NGtMQm54V09vY2pFTnJsWG43cmNjSjBYTlhUcWZiOXVWTGtLY2crbUROZzVmeXZPZk9QcjcvSm0xSSt3REsxd2ZqYmtyOWl3MzZtdGxJbTU5NFFCYlAzRHBoRkNDOHhqelA1RVFuL00xRnNyVUUzN0VsU0NTVnhSbWpIRGo3L2kxaE5TczJmbWlndWJaSmJpV0FoVEd1OE9QV1czTDJjdlIzWGFHNmRDZTdtMXhCMDZnWTJTQkY2ZTVwOFdyNVMzMXpieWNObElrYTloZWxNaWczUTF5V2p1Yit3VTFrY25UTzFhU0lNMUdXVjNIQjJCbDNRWEtqUXhDQUVBUlpvL1BvQWh4dWV5ZnJLd3hWQ0dZdUQ5MFJ6U3VScEE2TGJxUEl3ZHJESnpiRkUxd3p6enV2UEQrUjRlc2ZZSjMyRnJ2SlpOK25DcHhyM2NKRFlSbUYzeTRuR2p2YXVUSVZlQWxvWmwrcTVNUHlEbjRib0kvVllIUnpJbzhDNlI3Q0FQV3U1Smt6cXNEb0EvQlloWSsrbHJja1YzL1FNbURYd2FxZ0lJT3hVbVJLYUlHR0M5QVU2UHFCMG1RS293N3VjVTNkRUlzZTF3UDBqOFM0bmJGTk5vbFhUWjZtaVBuY1p3ZlQyRURRK1NrMy9DNGRaTlVzYjFFWm9oU25sOFRpdzdoOXQ0YUZrUjVySGsydDJnUEVvUnlKeEhUR01TamxDTGhUNys3S2RERFluemQyQldaeDFBVkYyUmJwY2Fqc2NlUEFVOGx1TUdFNEN0NUJ6NURma0xmWlZBZ0I1VmZVZHF2QVlNamNsL2NXMzJMSjZlcE5oWHFaQmhsZlFBdmZFWTlDWEtzYXNqanhwaVNDY0tFclJ0Q3ZVdERvRFd4WlYvS1UwVDdwbW51S3MxWWhWb2NTZXRRZHNERHRHRFhMNnZxSGIzOEovMWJOdWt5WnVtQWtWbHcycFFGc3Z4R2lKdEpZTzdzL0lqbjRhNXVscGFMblcxdVR6SXk4enllNVZiSThUMTlvbEZnN2Y4M2Q0cHNMQjRwYlBxU2ZCejdGUmQzb1hpUjU4U2VoSDI0T2E5N3phaVZ3eldYc0ovSmVvVlZJcHpDWmE3T3lxN1E3TmhxTU1nUFp3VVM0bmdZNSt3RUUyMG9ZWmR4aER5YWJQK1dpWExhQTQ5QzNXbDI2aG5za3BoY2ZJTWR1bGZnQ2JIbW5KdTN2Z2c0b09nVmd1cXVtcGdzTFdyVGlEcU5JUlpsV0ZGek1QVlNQOVVIYytGdzB4TXZRLzFGY0ZmWWJnOXcyTldqSzJoM1BVb0VwZ2dKR1kvZloxaGYvTW9kR3krN1Uzb09ZT0JHSkxXemw2K2NrTzEyeW16czF4cCtqUE9SYjhDRVNZdUd2bW96K010WVZOTkhZM1hjd2kyZndkSVNYaGFyVjZCOXJjQ1d4bmRDeEJQSmZXRDhDY21wYlBCaVlQZk1nYlJZL0ZIUUE5Vk5uS1ZKNlVBVms1TnhlZ3NpVEJENUpPaGdKN2QrYzBaRDNGUXZlN1FWWjhvWThMNkpRczR4N3RtK3V4M0RzYjI2Q0xrWEtocWF0bUh2bG9zSkpMVlRRSnhVYnFZVk93dVhGSGx1b0NmTzRocGF3LzFzQmlTTHUvY2EzZjJzMWlMNDBFMXFBSmZOSTRGWFpCZ0JDOFNvdklaUCtNeXdWTVJQT2dLNk5TS3B5RjhxTFRvc1FnUmt0c0lxS2tjY2tDRkhaUndKMkhENDJjdnpQSjQwZHIwR1FtRjhBZmVweWFjdDIybnBYN0g4OExXMVgrZHpFYUJqUUhubzRocjRpOW1vMmpUUjRERVZVSjFtT2ZUL3NQSitXYjE1eFJQTE9zeGlVSVNsaS95Yjg5OUJpdVU1VlhRbmZNZmJBWmZSbEpwNnI4Y2pQVG1aWDBLRTVMdEUyMnFZeURvMk5Rem0wNDdoMitpMUJPUFBVWmg1cWFVMnJWV2xEekFUanBCV2NnRFk5bnFEWTgvdlhnOG9pSzVzZW55UTJCYk5GR1J3eitMR1ExZjdKMFFpMFlrRTFLU1VnTmN1MC9lM0ZFR29OR1lmYTlvTk1XYnJVMVFiZEl4K3lXcStIVWZ5Nnpzd1UzUitZQjFUUGdxZ2tXbjFPZU9RaWJHRmJmTUtWMEFrRnE1SnFrVnlwUHVJMjdUdnlVOUtkTjc1c2V5ZUVBdU5GV0I2c1Z4VHhDRm9CaVlyZ3pUckgrVW1sdCtLbi9DeHFCUHphZHR4QXA0NURVU1NWajhDWjV5amN3dVRidEFhd3RLSzVVRy84Y2lIb3A4K01ITWRVaURrajNIQVZPNWxYQXN5TEpQMTRnNTVSNndIRUJwZ3I0ZGZ1eHU3VEE0blE3QkFhenZ6MEVoSkIxUkhSMHEzalJnZktQWkQ2T2YwaTFZWVZFQUc1K2VKZTFpVDUwemo5VFpzYUFDNXVvWWs0UHJvc0ZrcFFUbmJJLzJ6OVkyVERlc1VKMEVXbDR5UXlseU80RWJVWE9yK0xrZ01Vc1NpWjkrV1hCVGpnVk8wT0c1MnczcEdWU01qejY5WkNGRFlXNHZQOGpyRVplVHRvYlVBUithMGxURFhoMERETlZCdGFYbWVnenRBUWRRSVJhOVpkUVlxS3BXVXZqRkNqYzFybUlQNVBqckhrMTlHSjYvZTk1RXNGRk82aVhCMytET1ZaeVZSQm44NkhIWVlmUWNaUTc4NTVkL0ZSVWU2TU83R3F0T0pxUnQxS21NODByQ0dwdnFtYmNJcWt3QWlJYUVKT0h5NUU0dTVoT2JFN1FMUUpuYUtCVnVtbENhcld5UHlSbXRIVW05VWM5bzhaVWJ2eG9CaEJpRmF6OXhkT2tidFJrT3dGZFg0UndRTEJVWUhod05lQ05ycXNWbFo4WUI0YytQWFlJVUk2VldrL0dDYWpyUlduNFM1T0ZaYUZlQkxaVUpObzUvQm8zQUxkdi95MEFPRzhUdys5dGVrdjB1WjdGbE1zeVRkTjI5Y3hkRWNPd3dUelA0UzM3TjRnWEFnSWpINWpMcDZBN29PV3JubXVXV1VHeDBlVFJ2UzNnUlJDTytrOGZ1Z0U5dG5TMC9sYWlNNUNOMVY2T2NZdUh3NEV3TE9UUHlSbHBjZ2ZBcmVWYjR6YXMyN3lEa1VaN3NHZGYxL2ZJR0lDeG9BQTh2TGR0WUZBLzNwR29yOEJhMXZPNlNGbXBkbWIrRjVpTGU1UkMrY3pDTVM2bmdIV1R4bmNJT2dBNVA0L3JzVHJaZXg5eFoyVnJuSlFibTlwWFR0U0REdDVFamhoSnJoTGRrajBDTktpc2RQU090RnFJK3JleUNNK2ZrSlJkVHlFSXJsOUtZR0F2Nm5VRlVNTGtwY0FYK1h6TTd1WEdvL1lXYmYweUsyczVmVzZiL1JuRG91NFRPRHZOOHA2d095N3MzSlRCSmtFYWd2VHNLZlFndnZCVEkyYkNtV1d6d2M1RlNtT1VkTkx0OU9MUEhhb0ExZ3JhQ2dpUzVuSkZ2OWE5OVRzcmdjWVpFblhBVDBuRGJMajZxOTRuVldtS3pLSVVQY2czUGNJejR2WXJKQmFVOHcrdm03TE9BLzIvUGRHdCs0andxWmVwa1JmaGJPNlhwaXRMZ3NRWUlhNGYrT0lveWh3R1ZVd2l2Y3J0NGt6d3RPaTlYZ0RObkwzckR0ZDUwTWRvaElIaEpBWFpaUGUzc0NEMmltcFZMcmVpdXhYS3I1b2lZT1ErYWxUNkRuRnNOeGJtREF2b05wNi93RWlpZEJTTVozaUZCODdnaHh0LzArU1V6TWFmMDFGWFlGU2xVYkZPbWtvdjExSktQcHRCR1JQU09ublRpUWRieVY3YUZ1Z1dpcW9VSE80bFI5QjdJRW1oL3NvQ3E3dmpHYTJEdENZQklQNlh6YmppUUJKOEVmK0x0cTlWUDkrRDlpYzVtQSs5VjcvSTZrVFVaNndEZ3R3N2wxRW5jekIwc3Y5TlpSbEJGbEtRd3R0TVhmMkJmOTlveHphVjYzYVhvYTJibDYwbGtSU2loOVBkUEd2MUdLcXB5ckhrYWhkamt0b3NBVnd6dGxwcXU3WTRsOVh3OEdvTElpT3pvaVVYdlpJS01nZmNrajBmeE1pS0JFL2gydzErSm1GR0lKWkUzZlBZbXVCdVF3TzVDakZtaHhKbFRKZFBFY2srYURBODNjWEd4YXZYa3NoZ3FobWZsQjBjL3pudGRROVlMU3h3WkhsSmhQVzhNZ1hZNzFTUmNRV2tTK1ZZSEY1UG5TcGswa3FxWUt3UnVJU0xLdGJjNGFtMmVqa0ZncXJkSEJ3ZSs2N3drREZjQmxBbmg5aXFBZHJJZHJIRFljRnViNWkvaElqSXNvVGc2VnFTSU5oYUxpb24rcEZPZFYwTWVJT3ZremdyZHc2YXlFSFdQYkRtdXZxWG4zdzJGYW1nYWVDZjkrcnY4YkVLNG9qUkV4eE8vT2FubTlZZkVWQnBZZTV6TXA2d084d2hSYjltM3J6MGRoSWJrK21saEhGc1kvUnJwNDBYdGtZNVZvN1FJWnNZSGJWZXBFL0tvcGtOMFYrazVSMmJnWFM2RTdkME5ZM0NEUlEzeW9QSUVwY1pPLzMvMlFybUlZek5RcHI2L3NZSWNlRElEOGVWc1ZGZHRGakdMdWdMRGxOR2VlUVdQSTQ3bEZiN3NuVnZNWDQyN05ESGZHb0FhbnVjbUhFSWVxenQzcjdFR2Y0UDBYT09DWEN1cGFONjZ0bHJuSE9oMmRkaDhTM1lscFdURkI2ME44bFBXZTFaK2UwUnl3OHV1bE5VMDl1OUp2R2t3SzJldnZBdlpweXA3OEp0djVHMUpPdUtnWmJsV0VQT3VJL1NSWEt6QlZjUDB1OFBvOFFORHVjQ25TUGt2eWlvV2xGQUZiSmNoWnZBTVphQko2aFMxMnE3NWxSbXY4MW82MENoNlJVZFZjQTlwREIyTlEyNVJnVXowMStiaEZMdnNSN01sYVdSbnVLSVYvbzZaUXZHb2N1UFhaMkV3VXJ5SUxpc1FtQWFydWFCOS9XVkY5T21OaGhPTU90dzdmZlk5Sng5YmxpUnh1KzkrNmMyWFJiSVpQTzZ3V1dTUGVEUld5TzgvK0ozZHY4UmQ3S1JWQVZDdGtUOVNhUW41NzdDUVpmKzFMNEVCbW5IZTNFMzI1M0JNa1hDVXJLam9ZN0JBRmgySmZEd1lMQnVTNXNKQTVNc204QUJuWmxURFBGV0pWRGFXamJZUlovaUhueCt6L1RzNThiaE1OYmI1L0VXYWxValZWWGV2NS8weWlYcHk1bnJNMUZsM2tOTm9PS0l5NHlYS290cmVCc21mVFB0MG9YRkFIakpNbDFDNlo5dnF6cDR4RUJCNzhlcStnbTF6WCs3OXRpdHpzM2RMMThSZjlaQkFQWkpMWDhHd2pKbVpHWVp6cHRCenNnTDBZT1BDN3pJWEJuaXpPdXRWWXQ5ejJ6YnRpVUNoWVpDTXpNd0Z5aWhKRGVGQzlZMkNzVm1MR1gwWXhGRzliRzN3WnRGN0lQWUZPVjJ4TnFZTEVLVy9UZWVtYVQyb0tGNVdkbjZ0U0VIZHVlV1lyS3U1V1ZxR3Z2NHF1VFgrK2xkcjFsbFBSaytNSW5GVFNXYk9LTXowWCsvMUFPeDZWMmJ4TWRCaUExVGtCbVFWVVFCOEFXZTdVcVhWQ01CajNFNFliZVZBMElBendESDcyOWRKTXZYK0M5UTBpUXVKR1Z2VTh0N2FwdkUyY1ZLUmJjbHNVOXBKQXpkOXZXV3A0dVdIdm1IT083c2d1OUZHb1V0RHRma3dYWWk3bEZrazNkWVRqWVo4dUh5d0xuZFRwdStmb0VTQm5WV3ZhbFlidURNbjBVM2k5NWtBemJjdzlmWCtIeTlEZFpTNlNCL3lTa21hNU5xWUM4VDZPaE5uYW0wTlNDUm5wZ2h4eStQVUpQUUg3Nk5Dc0pTOVdaYTc3eGRaWlpPQnNMRExBTGduWm5mbHNvUXpYYnZOSCtGdVZXZnRTNmxCM1BKd2ViSGV1QTRyRjdXNS84dDhqdVJneG82TjBIbloydm4rU1JaY0xaWE9MNUtkNFBMRGZ0RHFGeVlSRXpVbnMwTEM2SnJxSkw3UWxsZzRuanpiRG8rQmlrRUg3NG9iT3p6TGN5cVdmL2dERXhaajhTTTNocDBZN2dyL2F3UExNV3pGNVFCa3ExQXEvbno2TnlEc1BKNnBGbmlVNmFRK3JLU1Jpb2RyNTFXZ1g1Q0xkTHpBa2dZaDcxOS9kS29kejVYL3pDazA5ZTNUbWhLUWQwV3dweW5EK3NnY2hSeEV5SElRVUVRVEg0UHpJdWZoZ0F0MzFLaDdKbUZzWTlzL2tGRGdHL3ZRV1E3RUU2cDlTdkpsNWtNU0JrZVpCbUQ3Vkt1QjNWSTh6b1JTbHQvMHJRa0llSFlIWnM1ODZENUtDU2k1REkxMUVDNFZPQmtiSzJOQ2lUM1dTd1R0bWg1cXNJTWxQZ0RvU2k5czZkNVZzSjRQTHUwRkVaU3ZEaHJZMVhjbWxDRHVvYTlpSkk4M09WYUdsV0YwSkRTeFlCYWlqbFFYaU52ZjJRWWN1NEV1VzdUMUZEN2VaYW1pN3BzdnNnZkhKZnZ5L3l2VlNId0laWlI3SVU2ZzgzOWltQnd0MmhubWlFdnMydFZUQ1I2TmhSN0phdk9EUThwc3M2MGpiRUEveERWejYreTB3RnV5YnAveGlnYnBpblV0bDhuVWlmN0pnaHV6Wkd1aUIyREFxYWpKTFhXWk5oL2tab1BnMnR5bDZrY1pjd2k2dnAveFlQSWFEUjY3Qjdqd0YxU3BVcG5DRXdOS00rKy84Y3NzSU5kRWRGUGxybVpzbmg2NE1TZFVrWk5LZkNPS3lJUS9HbGpYaDJRVmRINmM1bFNYT05ZQzJoQmQ4OWlyTml5OElGZTNrMmVnNlFreEhEZ201Mm5PcXVnT3EyNlRYMDk5K2UyWWdmbnVCcDBhbCtIMVhQQVlITzRQNWI1TWhPSlNZV2puaFJnU3paOWN3bmIzQWtPTTdNT1g1Q1MwYld6UTFQVTF0YTFRVlljUGR4R0x1UDN2akprNVVZcjZsL0lObzZmcys1OUZOR09Gek9PcHNCeTBsRG9tVkJTVi9qZmhsa2xSY3ZyNEI2ZUNPWDd3VFVkNkxDZml4clBOVnVSRzlOVG9MNlVBaE5xMnZJdzhsK2o0Ky91OFhOSlh0a1JNQ0tmR3pyeitDWmF4YmF1MXd4clJpVVZ3Q3VQeWpMdnprdHFsOXdjZjdVSzJhb2hhT0hteG1aQW1tekM3TllUYXJ0aGpobzJFR3Z0cmdqN0tmQSttTHlueVN5UHA3OHhZN1U3UzA2Y2t1UUNwMlgrOGxWOFBFSkpmTG5rNERQUGlYUm1nU2dWTlpoUnFLUFgraVp0N1hkcUs0N280QzlLeDhOTU1NUHJ4UUUwN21TKzB5WjNVdFFkSGpSQ1lrVVl6MlhsR2RORzVkMVd0Z2dKOGFBTDZySUVVaGYrQVcrT1JZcUppVi9lWDNEc2d3a3BDazhvZ1N5YUJ2SUd0TXFQSFZwNHhBWkJQZkorSVN0M2ptZVUyL1ZOeFBEaHVuUnBkUEhmZHdlTm5pSDJpZzAwOG9JSXpyeGpodGJQdUhwK1V1ZExPdDEvSEsxMm05QktKZzNRdElPRU1IYU9GUzdLVHZWVGNmeVZETXdYNmpYekZiTzBKcGJpWFhuVk1SaURhSHlsTit2NlNBRmFIOEpkajNOa2Fla1IxczRCVDI3K01ML1JYOEtqeDRNZzRucWRlSmllSHNjd0pGZ0F2QkVUWXBLVWFHUHRFem9WOHpmMjdyMW91NzRZRjJUQncwK3dWWHlvbCsrUFllbUViaTB6Ry96eTJ3UlBGYmZWdmpkWVRKRUVKRzhyMzdIeWFxbTQvUHlHZlN3UmRveFBnc2ZwMDhRajVhUXdJNXhXOE1lajNhRUJoayt6V1hYVmpZUDdRTkdUbm03S281MmEvaWZlZ2xQb1hQY0xwaVdZYU9qU0R4S1VidnJqemN1U1pYZ3JJNU50WElZNm45eU4xWWpUajRobkIxMEFZTHFiakdmQVptVVVQMjBlRXNiODd5b2Uxb1c4ZVYyZk5xaTZDWUsvck1HdDFNU1FVRUM2cTVOWllWUXE1WmNGUTF1d2twQ1dpVTV5MXNxcUorYlRsREFjUzBFamhkcTNSTGhXdU1CaUhBb3Jqa2ZIK28wMU84a1VWeFBLalV0RHNwcTArcVFXOG8rTzRMYzREME5wc21hbUpkUHc5S09sUldabTA3ODRIRHRPY0JBN3BFY1hVSkpqWjNGT0hoZXJEQTUreEQreVFMR0F4eWNtYmlvem9KcStaZ29JeFVtMzVveW5XUjNYMjdYM3NMZnZjNjdlem1aVE5nOTB0NkJBeG9hdFZ1NzR2bUxYWEtnU3BjUDdkS2NmMkRvaFgwZml4a2dSalR5OVRIaC9PN0NCbVZZSGRDdHNHQ2J2NXJoK1pmancvSnhTL2IwSmcvZXI1WWdDMTNRZEtUNlhBWTNFYmhLTThwVFMrUjFnUWVQeTBNYm1XTjVyc3JFdHpYbmRMeGtlcTNEdXhaRE9ObUF4OFdSRVJsb1lpZ2tXYjA2cDFPNWgrTS93ZklOSW1TRWhoSkV1S0ZibWxZVTBReFVvY0VvMTQxbk9ZTmh6UWViSjRORUZBVGhHTWI2ZDl1QWRyTmRoaWk5cVF5Qk9qMWdCY2NyQTJxcGdKZVAvWXI0N0hONEtoMDRQK2VZV0pIZ3U3ckVqM0tHWDVRV3ZUNEk1SW03WVYzRitod1Urdm5YWS81dE9KNTdLc1pSTCtqWHpDeXRhdXEvekZ3M2RrZHhNNU81NjY1bFhPUnl5M0VaNnRYejhza3I4NUpkbE4xWmJvRU4rNThJbU42ekl4NDY0L3dzVlhCeHFxejIzUnUvZlBLT3JIQjRPWFAvRlVRdUFpOXYwcGdlYjhQOSs3TTlmZjN1WXpmdjdJQTBwcmlyWHJzWm1Kc0JoS3pwbCtubFNncmExZ1ZKdjZRRTBGUktIaDNRU013TlNVRXlKU3FyczFsa0lwcnVrRDhnK0g5T28zbGRvY3pSOXZsOUhlOUlsbGxOMEY4dU9MdzFsQ0ljNmRadWovWUpXcC8yYUV4VFJvazZxM1QxUnQyMUlscXBVNTNUbU5OckJHbndjUDJBYzVKMnFKMEFsQTFYR2V0b0JwcXV4dXh0WXRkUC9DMmk5ZDlWdk1ZbkxBZlpyNzZENzFwSW1mdUNEVjArdUVJaHFkcEV1YUFHUDZ3ai9WRitPSmhucjFkQjFGbE9NbXlWZDNSamxOMGF1M2pZM1VWejB4cW5ZNDN2UUc3L2YrV2NNSThzU3VPL3A3ajR5aW13TXRnOUR4QTRmMENWZVNtQWRZKzNLc3dSV1NwaElFSGZvNFBhc29jU2RWdFd6c2dRZ2VZeGRIekZKM2FjRkZXK2VlUFBGeUxKVS9ZVjlBWXE5YWlCeFR1RndSa0NiYmw1TUdJRGxYdVRPU2taem1id0gxZWU4TlBBcnd4TC9wZ2ExN3JsZ2JGQ3grWlBpOHdMa2NFSGozN25ibUZVVHNMOEpyMFAyQjBhRlB4YnNIUTFsa3ROOU16cW5ZV2RnT2t5WGRqVjZkZVFVS0pPUmJrR3hCdHpQckxPdFc1T3BBU2tYT0lPR3RmRzV6UmIxbzV0cXFuRmxvR3ZvbU1mZVIySWRtQ1lWTmM2VXY0SjRISzc1dmRwN2xPSmNlRHptK3RoSHI1RC9mSWp4RStva25TU2NBYXJlZ3QrUWNwL1ZrVGQ2OEJMRkF4MG9JZmlZbTg3aWEwTTBMdGc1OEUxck54Q09PT0drUjd6SzY4Z2g5V3BMQ0l5YS9MZEtlNm1yVjBYbzVwSTJnOUplaDFEUHlhUWdwTXFTT1VPSGM2WlV0aDdmNnN6TldEUEE3TzU1WkNuSFdWT3IyNFIzWlh1WHROeDRna2lmWW1VMHo0QU05TnNiTzNsWFFyb2RseEFKNy9vdXpOV1BPK2xuM0F6QU14b2pxKzdpOWNVTTF4NW4zT2hIRm1WTVdiQ3hadjk2Qnpza1JSV2wzYnVaQ0M3eGhDbVJDL0U5NUNzNVlJcE5ITzl6a2gxa3EydTF1akZ3cklzK0hLZDd3eUlIeGVaVzFTVTNneHc1QTFGY3NtbUppR3RTdDhBWGh6K2R6dFJRbDV4NUxUQzVnWG84YnZTNGpiUTR6d2JJQlE0YzdEcTBOTmo5RzhHV3dqck1FMEhocWhFaHE1cXRJYmYyaTJDVkxWc09xMHIxQ1MzSThlemc3Tk1MMkFReDJicjFveEZKK2o4bGNKL0FNYkFwK2lhREZTVm0yTFUxOWZvNUFLSTk2TmlROWdqSitZTTQxZFd1UnprY3krdkIrQVZJcmxMTkp2cXBTdXdDdHZ1MXVrOXpVTTlES3VZaXRQeVFnYW9IVm04b2xsTk9RcWg2c3IzVjU2Z2dzL1lSZnRmaThId2lyYjRhc3ZqR2tiV2dveTZrS2xUekc2YU9ROFlmSGN2ZDBBZlk2emExNEZFRzBMTmQwRnkrYStSUXpQK08yVzdiV1dOc21oZTRKSlNQY08zdkZxZjUrbGRqbUxCc1ltWmpmNE1CNU9VVmhXVDlnWmNTemFxUVJqbUFiNk1VMEp1VHpsY3NUbXd4N1Azc2RpemIxYzErV3ZqM045eGUrL0M4dzlDa01MSDhQODRyUlBZN0RleUhsZlVndW1ZRi8xYmluNFQvelQraGVkazFTY0gyRDUwQ3N0aXRESXJ6c0pWY0ZpVGhxZi9WU1ZmSHRKSkJPOHphdHpUbU1EM0RwTGNmYjU0WjdhVFFpY0VqeW5oU2NJd3JDbWRVeXRZZG90czNkeHBRZnFDbHFzY3lRMXJCUU5iRW1lSkJ5RDh0S1VKM2RoalJsVHZvQm1tWUc4SVhSWERHZllMZjhtdHp5Y1dqOVAvTm5hSHgxemRudmxSc0t2Z0M2dVhXWHJuQmR0VGlsYlprWHhXL254NHg1ZkNvb1JMNlkzYmIydWJSY1pPZ2VlRmRVZS9WdjBmNkVTaUNacVpudnRDdFBmME1mR1phT1k5NTRYdmNmN0lNK1hwandpMmdna2FLeE1SMXhiMXpiWG5iTFh1Rm56bUQ0Z3lOVk14dk0rTnloM0lyN3VXV0kxRk5JSjJBdHprb0hQNzA1VUJTQlVrTDRJNGJNc1dTV2VuZTNIbGtWRjlxSXAzUVphQTEyeTFtbjFwbWRhQnFPbHU4NG5WbzIvVnpVY1ZCaDFsSDhhMWoxSGFOTDVFTGFnZE1CMy9LbFhHYks1MDJzTlRkcWZabjY4STJpZm56UzI3Y0xzcWxBMW0vMXo3Q2JiUk10Mldkb3N3c3JMSFBDd0JUVVZYVHB2THdOMlhndW42OG10ZmFDMWpiR0hUb0NQRkpaRlNaUy9ENkpWYjVCSkkvcnZhMUNsVzVhU1hla0M1Z21sSEQzaisrTHFST2lrNlRRSHFyd2s1enpWNk1OV3VsajNta24vMGJCRXZyekhKTjRsVlVDT3ljZTVNRlNnT0pUanIxb21ScURlYzJpYzJ0UWd6VEduUkl2SWQ0V0RtK1pWaU5OeHdTRjh2NDRaN2xwb2pvUnZSb0tNcnVISnorMFNkUXRhK1JPTGF3MkFPbEEzV0xQOXNyamtqbCtsSDZqL09IYWw1R3k1M2RDeEE5azVyTEJ0cG85U3U3MHZuUEYwQ0IrRWdXbEdKQlErQm9EMXJVZ0ZxekNpdFdhT1dWTnhUd0RBQWJ3M0x5dTVkWWZwNStnRFc2OGVJNng1MHdiUW4vdHNKZldvem1EeE55VUgxejJXZUpBVHdxSm5yRmI2OUFaT0R5K1pQOGtuQjFNYnJaUFRvUTF4TEpNdFIyUnNOTGlwSm9pNlA0Z0QrRG1FTC8wZXBVS05xcHUyY2tuUmtBWmh1V0RjYjlTTm1jdXFQME11UjBOQ1FhbjJpQ0FNV0tOVEQrdGR6eE9PZ3dhZHlYWVNESlhjajE4RlE4UXE3Rm5yazZ3dmFLTkd6Z1FvY2VBREczR0dCRFFIRTJycFU4dm9lOTE3T3lteVhJcXNOcnZSN2EzWEo4aGhtbnR2NzNRTlp0T1hTVmdXbGNZYUg3enoySTJLazFCNk10T3JNZzdZOW5lMlFGYllQWU5oZGxnSGtyczlOZGQyKzFOaWJkL2wvV25ZSmdoRVowV2VublAyaEh4RWFPcWg3STd4WWdONXdMVWpLTWtTSGc0dkJILzhiZ1dkR0VqTVovU0F2Tm1Oa1FZU1M1cUIxZDB1VVo4TXllZS9HSHFYclpjVjBlU2RaamxaZzExclFycmNkZkpLaUQyTi9HaVMxWUtLQUdjbzVNdjZLSHpYMEpUWEl3WkNLN3JyMzB2NnlpaERyeXoyZVZVVnZXTnhPQW53dWxNbVozMWFuOGN3YllkTEc4RDhlMWd4NUtGU2FqdUVpK2Iyc2tDZVZtaC9yR1Joc3lNWFNQWWtLemFDSzBkSGV4L1o5aUhScjFLYU9XL3EyeWdaUENaQnVHa3dmZ3FWQ2VrOGlzTmNTMFZsT0I1QitIdjJtUnoveDZNbWU4RDBwUTBINkJXZFFHcTVoOEdicWhQRHVaMzlYU01sTklKaXlEQzdVM3M2OGkxd204VFQzb25HY2cwZXNkY0xHVldjL2tuRFM1eHh0SzNJblFsNU1RNzBSbFVKdW1wN2FvZFQzNnB5Zk1EZjJxV2VWZTlRcWtycTRzNzRPV0VJN1BSMVhpTTBoeXhuUmRhSHFaQzkrcVl4cmVtc280eEF1aU04bHBhSXBxQURPRU1zOWdYOVVIU0tRQkMzQUdJaVhIMHd3NXQwellnNkx1amZ6WjhnczJBM20yRWpGcnV3dFdiSzNCdkowL240THpVTDhlTWNJTWp0ZGVUVnM4Nkx5Y2YxUlUyV0Zmd0daZTFOdmNEMlZlTjVIQmQ5ZExtRzY2L1Uvd2tKMVJQclIrU29peDBLaVJWTlJYRW1UVnRYUDQwcGFvUDdXTHJYYWhOYldlb1ZZeXg2dTc3cTZsQ3pmVWlBVVRUNnJHdUh3OTN4T3hkRDRTSzVSWGdrSmt4RUlVWldNdlNNWVZXdEFBSXNoT1NsUVJDcFVmWHpaeVpkL0RjNWdBTnBCdnpISTM4aytYeTJVWlV4aUdqcXFDS2FaT01nK3RnMUxJUE5NSlI1SU0yYWxWYjh2YVpZUGVObC8veWlzRVJrVUt6MWxURlZMUkNSUDQyTWVXL0hCb0Q0SnlLUWdzYndoZXVTK2pmVG5aMmJzQUJyVDN0U09EUkpxNVhhNk5ZUEIwMENYVldSNlc3NFR3dlRyaE5JQ29DYm44bkpwWVp2dlNBeWkydWdneUJEbVVBeTQ5SW9YZ096SkM1RU5HNS9Md2RNeDVqcVpUVlIrZmJGWlFNdVJMcXQ3Y1JuWkFBNnE1MU8rR2hTaDRGM0o4bW50bkpHK3ZRNjVNem5jM25lbmNYRGtNeWFLZml4ZjJSSjBWK2hwSHMrTGVLSkJ6a2dPMENEWHhydXdRU0lGblp6UmxiZTZBVUVvcVNBT3djVUFadjJFeWtCclNFSjFTbU44aWZNa2NPMEZuSTNheDBoaTZhem9rRHFjL0NWMkRMUC9RSkE3Y2V5MmJQMDg5ZjBRaGRxdjRWRTJZNmRMRDRRd1ZiNCtuZGVwY3puNEZNbE9OOG9QL3hKQlZDa2JSdHQ4d1h2bnVRZXdIamVvVFI2QVRYRzdEUDlPQ1dOVklDM3JkcUM2TE84czdpK1NBRnRZc2tiUHd5RWtoQ0NwODBiWmk0WmdBdjFXV0hITUdqa1o5bzRlU3lMOEtXb1Y0Z0cxelVRMUk1MmhBSG9YZy9lbWFhY0Z2bk1DdHI4aEdsQi9ZTmhqcExTdmRjSmcwOENJN0RoUUZPeFhEb2h6TjNNMElQdVZLMGdhR3VGSmtuVDRjQjB0Qk9oc0g0NDBhRzVqZ2oxbC9Vd0c1VHNaOEh6cXJxUGFoNC9lanJCcisra2FjWk5xR1pkakZ6OTBBUXFNUzVCY201SWtkWDdMK0F4YkpLRnZDQ05sK3F0Zmhna05pa2VhZXdCNVRpVXdHR1F4OE40aElBejZBaGFOU1JJZzlUcElsTWVLcUJ3TlViQTd3dmR2bGoyWEpHY0hSRnJCeUUrZjVKWk5wYzFQZkJYQS9KSnJ3YVdSWXAvYnF0bHozdk1VYjI3UHFXNlFyVElneHY0dkFnWUpQR2Q0TmRZYTlLRzJPc0JQU29kWmZCZ01ad205VlJyTW43U1VPMk96c0lyaU5iY01lSmtQTi9IUGxIMGNKdEJGZ3NPUkVzeHIvVUxEaEhCbEZkWjdKNzRFMkk5RWlrQUF1SGJWZ29KSWtKVDMzUUhIbjNpcFcwYy9UNnJ0OU5pOFdCK0xLL05zdW1TbEJqUTJLUDJnaDJpK1BwZTNna0FqdXdSeGtEUUlQQ05hUWFQalZ2bS85QXJ0L3J6amg1MGR6VytSV0VyOVluVkh0bkFhL1ZtZEhXaXV6UVMrQ0JSMXdudENvNEl5SFhxbnFWYkhTekYvNjNKc2U0QWR2eG1ZeUpKTUxRUlpYcXpOY2laYVN6SzFUNzAyZit4VG1nQzNvKzNFTHJRZVE3OEhzUkg1VU9hWHJGdDc4N3l2aEFCQktBTHptSm9yNFl1cGpLVzlxay9xQkZqSFlwZGdjaXhWTlFmZFVneWpFY3RiUzVZRVJkQ2hRMm5waVZoclF6Y3JSOEZ6cStTUG0remJoZktCRnlDRHErQTg0b21jOHR0VHFZRk1CODRMa0lxcUtGUmtibG9pd1hvdjJOUDdmNDRCYlVZQ0VuVmxmdjZlQ3E1bXcyTEF6bXE0eEc3Zzh3WE9UdG1HbW55NDdHcGh0VzV6QmVUU3JOWUJ5aW9qZmNmdmtmSkRESlZueC9YTWhneGhCcXhEUmtFVWg2WjI5UWc0d1pCUGkvMDdtTEdoTldnanJ4YWxNZEtsRFRFSTQvMnZFek5vZVMrUDNCQnkvV2R4Q1hQcSsxZEY2M0huYTlJL1Q1K2ZoTjB0Sm44bzkvSTBBZ0Y4NFVDTXJPVG5uQU5vS05nVkNLeHVHU1BPUFdwcGl0VVlab0swaWY4clFlK2RYZ214cmcvK3JJUFFLSWF5RVBCeHQ5YkN4NjlyZ09CaE0wMTR0c3VxUmczZEJhaHc1dkxid3B6WW9oSzI0WHAvSHdEbWQ0ZGh3UEhUT2xvTS94YlhkTGhYVDFJaFVNcjhoMWhnRFByQU13R3U0RGxZOWJmN0E2djBQb1BvSkhuWTJibXlkSUFJbGFxbTNQNXVHVk45V0pJdC9Nbkd4NzZuc0piSm1YbDMvWFJBNUVEaWVhVktnMHZMZlpKbVh3L1AxVUMzVFlaNHRreWU5bThNR1NaY0VjT1MwN0dWOVA0Vnc3QW5VNHo2dFFXTWp6L1JQWDdCVkhiU3c5N1IwOTlZbzVqVzcrZEZ0T3hTbVNTeW5jOEpwSjl3dXZUS1FhYXpHMFM0MkNjenFkRmtDdk9BMzBUcldFcEVhZ0Z6aHhiNW5KMTMwclkrcjhmWGhxZWkxRzdRc1R3ek1UUXJKNjA4ZFplUWkvYjhYTmc5RmlzQk9Lc2JDWGpsSE9kVDFySFlua09wUngvWW1vN1R5bGtzN3AvSjJ6VDJucHVtNXBIRE1ja29lQ040S1ZhaDk0RTJhajlvRkNvUDA1OFk1OUxDTWRicS9ETTRFb1FwaHlhMHhuTGhDVit3OVdBY2N1MUxiNGlVN1BMOFJXQTcxMTVYd3B3Q1NHbHN0eGFRckFidDF2Q3ZzY25ZTXhmZkJjVytLT1F3WlZlcThLZFAyMWY4aEhiUkJCK1JNaFdYaDNQem56ejVWOGs5aXJwZ3lrbVVEL3p4OUwrUklYZ1QwcS8vVVRaY2R0MDhBTERLVHI0bnhTeTNVZHFZR29tRXM0bHJoWURqazJjcmdVaVMwREM0eElhZ1JDeUYzLzlNdWtoZzVMaUhuelZVbjFMRmdWdmxkeWNVYXVqMU9yNVJ0VFhPQldsczg3SXd6ZGppYlhuQTlzd2VLQmRRR0tsQ3JjKzlTdnVXQ2h4SUhmUUhNVWN5WEx3UFR6T09maTAvanBDTDV3NTZremsyeFVNRkhLRzU0WGhGbE1GdDNvS0NFUVpHSjhnZ0VGVlBpZEV3ZUVoczVqMWFPZFdCNnZwazRvUmhvalJnUzVCR0xEa1JOYktRNnNlQXFVN0JkVUdqL0sxNlducjNiM082b2JRTW1DcGZNcmhWeklvWUR5VTZHSlprMjNWNjZpNUJZTHlSSlo4S2Y5cXRLRVgvZkVrY2VCYjlOa2FKWjdSbzZ4cGRuTGVYWkhyWHZJNjUrSWlESXFhSDh1TjZaSUUySUVlQUFJelN4MWV2OEx4VVJPRVFJWnBUU1ZJYWFud2k4c202V3lHaU1rblVtS1ZLZmhDaDR2OEdmQW90a0FidVVVKytXM1pxa0F0MVlvb0RMQ3VlcGJzV2oyNndCY0FUVFBaSERMNWMveG5DbEluVzhjdlNYcmYrOThndXpTWWovZEtLZTlvbXR6bkFVOWt2VCtkYUdIbEhxdzVYUXgxZm9BcjdvVG5ISmczTkdhUEVURU93TEk2Q3lFMlQrRDlkUVJHSHgxZldUT2MvZGZpQmNCQWNtUXVDWGNNMi8zT0V6UmpQcXVOa0VlaXNFVnBRREtUOHJRREh2Q01mK3pJNUk3M3FNc0hpMDNCcnJVYVI5dWRxQkxBVFBtc2xBTWlrcHQwMzR2SFpnRHMxTm01TDl1SjcyT0tDYWtsRHl1NFMydGxYQXI5aFJFVHlLSS9JcG4ybGZ4bjZ3cjdVSGFid0FZUDM5VjR3dnVYbXhGQ1Z1bGI5MVhVdnoyVmxLZzNjUnBua2pLOVpJbW11UWUzZXdsZkdXZWFOa2NFOVVxUWsxS3ZxVksvZmMvM045WnY0bStOd3pVWG1DUEV1bUpkeUtVU2VaOGNMUmlNa241T0lvMkhjWkVYY3FVM1I2TVJZb3d1WGg2QjNXYUl2Ty9uWVM5S1NEb2paMUQxNVVTK3ppdlRaRHc3UFErc3JOckVyalZxMXVLMDZzZENIVzFuNmFuZjZSVk5YbTRaQ2FxT0FsbmtHU1NpdVB5b0t1WUlTVGFSOWF5K2RSNlJvM0tiNHlWRklJbmc0VnczWDk5ZjFRRDJWRG1kR2dZR2Yya1N0YjlMZy9Dd3pYdW1RSzlIa0Zac1hTVUgvTVUxSzJ2QjFRSlFiZDN2aVJ5aytTT3pRT3FZRGZ3cDRnemlGeW1tVFMvcTArUFViRTZONzBEVkk2U0s0QmgwdVBKZlZ3bkhMdWJ5K0lwdTBqdFdJZ0lsaUJGOU0rcnBncS9QQnZPN1BXRHFjNVlVWm5YcG5IZEp5N3Z5SDRlSFBBOFdoUTgwUjdyd3lBNHZlUm5ZTkhCcU9nWWdDbE9QQnBHSEVvZU4yWUs3MHh5VkdVaEh4dUJRc1EvZHpZQzFzVjZQSlo1a0ozS2orQ2kzVk55aWFZZlVLNU1ML05tbjh0NXZndEg0R2V6Q29PNEJXcVVLeFFZOVI0eHAxMXpTK1hoQTdRZkpVR1BGVFNaWVhJQWxYQjNzcGlQbk9yMFc0UGRIRzNzNmtvOXE1RW9rbXc0aStUcU92TUQ1SmJWR1dtS2hyNjNKYXM5SW5ZaHUvRlh4b0hXQ3AzdGlmKzl5cEtNZXUvMHhreTYzTHBCNnN1SW8vMVg2T3JwYWE1VHRMUGQ1d0dWcEN6bnR5S0FVdzBjeEV3aVQ5aXVwMHFhOFFMOVlNZFpZdm82WG9wRHVNMm9LSjlRVGlIRU9uRFhWdEplT0RPNi90aEtyclJwL2RDWDdEQzE0RVNHZW0xOTk5Vnp5ZjRqWlUwOW0rejVLVSt2N2tQTklkSjI4U1o0dWZtMnZxVXduZlpBZ0FVTWJxRFU2QklNeTcrN240ZlBKaVBZbE5tYWRSYVh1YjgvclBQK05mTWFGbXpmemFRbWdmb2pCR0tIa3NsY0M0b3dBTkxJU1R4N210dnRpOG9Tc1h2NWthUy9pSkREbEZ3SGtGUEZGRllrY1ErdytYWUlZQUk2WXdURjg0My9OWUNoVy85RDZkTkRHaE4ybTN6eDJjVm9jMllLZFhvU1NQRWUvOFU3ZG1wZ3hZMDhqQkgwMlBuN1REYkw4MGNXQnlxSWhxUXRzcWt4bDRuOGlyWWIydVJxNlRPU0FVOWVzMldLNmoyRGYxTVI3clJRM21DTW5xSG95VVcwaWxJek9mT3pOY0tjYU5pUUF0Qy9BZXZqMUxQK0Z6M29KdWQ3V0w3UjB4di9hYWVGR0J5V0E2K1N6bWZCVklSam1sdGJzYVlibUFEcE5VVVNTQ254RUtjVFB3OHRybThjY2ZIWmd0MzlzYUV1WjYzb3JwUjhXQjA4QUdpcjJyVE9BM3ZWV3R2Ry9wOWRXYUdpUVZqUWtGQ2o5WmJJYUhOOUF4NnlHT3pkdEova3A4RUE3Zm9LK3lmbFhGdS9PTmZCZHBmVUJMa3JiUU5hQjljanlBMXNRdXd4SUZtaVp0TGV0aTVQUW16b0Z2ekZObVg3TTZIQVoyalBkVGIrbnV3YkYvQUZCL25OUHRZOEFqOFJYM1MwdC9xM0piRVNNT0ZBU05RcUlqcFNUSGdLUEdPalRYcis2bGxqQkFBbVQwVnBwWVZHQTkvMFNGYlFYRlRQQ2dScmpMczhFU0VWbWpDRkd1SjQ3dzMwUzdFT3YxenpaM0tSWUdtWUw0Q3VsU1M1cVlBSUJGNlJjU25VUDJnNlpMRVAySmZkQStCZUtZdU5TNEJiNHAvc0dlVWFhZjIrZ1JTSUkwamo3dGhuMXdjVU9LV1cvREVtYWlueGRzenV5NEw3M0p2WnhUM0lqaEJSVW5OVXdiL3BTaGJqVTV6ckxDUk5LeWtVK0JCYWt3SzE4UWttVjk3VzlGck83OGpFVDJ4aUhjZFNDV0pTc1NFU0N5OUN0SVZJSHNJZHlOYUZ4WVJkckp0akxxR3JpL3gwdTdXZ0tyQWxHUXhqWGoyR0x5ZGM5YW5CRllTZm0waEJXbHBDelJ4dElmNURocE5OWmEyQlZsNE1tK1ErZzlVQ1FZV296S0NCWDNsU2VQWTlEZzRTZEdzR01GWUZ1Z2J5a3ArR0pGSXB4TjdZKys1a2RvMXZpeUdGR0NpZVk2Z25GTFN5eVZ6YWZsamF5S2NjTHRQY1dTaE9NaC9iQWh2MmQwKzJ4djl4QkhKSkM2SXBVVEJ0aTVJTUJGc0lkS2dHL2M1ZTh2ZFdUbEx2WDRDdHVNOE9nd0Y4VjByak5nSzJONVdZd3c3SmNSZmpPRmNlVFZ1R2dTNUNLU1dQU051RHRvUTJZMVdQSzNIVlYwdXRUeWllVlQyVUVhRmhqajJSa2w0aWV5dkVLWm1QQ1N2UDUwK1JmRFRFdW1xY3hnM081cDRZcEJTVFhTaTQ4NC9uUzhsbE9obWFSaFdFZGx6VDBnYS9wZG5ITFBhUlFKckc0RW1KNnFSN3g4TFAzN1pFUmlqRlU1N1llMWxXMnRHZlhRVG5UbjZPM0Z1cXYzQkU2K3hlM3BNVW4rdTFMb2prN0VZUlI1TVFzdWRkZUk4cTRxaDNpTlF6RlV6NVhRMmo1T1NQTzB1UjRNL0FUdUp0R2JVb3I3UjRhSzNuSXIrRHpzVjQ4R2RxaUsvbjQvUGxHd3lZMzJUTmR5QTFjMnM3UER1eld3YlozZ2tQc013TytTT0NIYVZHaGUrbVdNelpoS2FQcUp0a25zY1lVeGo0bkhhalJzVXRxc0VuQkZDQlhrckxJN1NwQmpiNk1pbXpSVUJLNkdhYk1FQTFybnl3emp1VENPbEV0dGpsZWlvSDd6dXNXcFdTK292NjFQMFhWRUJqeWNSdlUyRUtXYnpLNUo3UWhPRm16TFg3NW1Ga0toSzJ1S2dvMmE0WUVaZ3F4ZG9CeTFtcElPV1VpR043cWE4dFc5RnlhU256K3dHMHp6bDliTjR1Ym42aytlY01aeEJyaFhQOHRKbWp2RXNrZmVsWWZFOGxPaGhTaDlZbzQvaE9kTEtIRzlrS1lEQlY5QzdmSU9UTE43QklGNDI2YVZ4OVZnSjNlbUNzSFNNY0NzUzRHRUpPTGFnSEhzbVp4MXFpWEdmRDlUNk1SVCtqUFdGNTRTeW1kZjVlK016Q3ZQRGV3YW1uUjVCakczN1pPNG5oazdBL0cyNjNiWWxxdXBSMURxMW4ySUNvRUtzeE43cVlnd3hTRkMwa09iMWxNYmV5Mjc3ZzRoamEyMWlTaTc0ZHljUkxGMkZnb0twWlA4Wkh4elowYnFwa0xYMHk1cXNEMHFLeUlDSjhQNkJ4bGxuQkJNTmRuVUhhUUJKYlAyMEd5anpIbytwS1ZrNHF2TU14TldiMXZFK1UydCs3ajk2V3czUWtKMjlXK2VNMnFDNEZydTc3YjdNbEloaTdpQnUyOCtRVEFWTnhsMFdscHFqTktLNVBsTkZaSmRyQ2lEWTJ1SjgxQ0YzMjJkdFp0Wkh1R05Gd3Y4VVg4K3N3ZTByQytMeTdhSDFmTGFXNlZjd0ZwZmZUVlo4aFIzR2MvMDhWTWZUUkJkVk5BM2J2VnhYLzkvQVF5eU9kRGV5MjJWZDlPczcrelhuaGlKL0xib1Z6SFF4NVJUS2pvSDlxRk9ubE9ZNStXL0RPSFdiUDRocUc5VWJrQ2Y4YjhOeUxHMGoxVWhzRDh6OWNjN0p4WldoSXJicmY4Z212Y2pyLzhrbDVuTHhjMmZHR2l3SnQxSDJ5WW12Q2tjaG9IR240WnVDNTNDNlh1cGpsOVk0bEpaRi9oaHp0VFY4Q0pIRGUzQS9qUjlOamZBRlAzVlZ5Q0NLdmN6YmpWTFpZc0ZsTVl3SVZLVVIyWlhwYVZoQmNncURqOFZWcnFmZ2xDV1FQdnpyRmtKcG91alBzSDhCVU83K2hCVi8xMlFVaHJ5ZjlGYlZhbXN3V3FsSlNnamM4Z3E1TC8rZVN4NS9kam1iR0hSMmZ5b1FHR2Nnc1hXSVdrYjBmWThyWmlWOWE1dmpmUG1CRlFESE4zZFZEa1p2bnU4dG9JMlhwR0MrdmpyTGVhaHgzZ1FxME5BMGNBVHBWNGkvbUFUL3cvYjUxWHZGVEN0N2p4TGwvOFBwSnVwaVQxN2Q1RVdWWEIwRyt3Z3A4QWFNcHZKSnM4N1RmR1dzRWdGYVExRFZMTGRib09mb0l0U2EzUWEzOUFvTy8xVVdJb0lYU2dOUTEwQWFpcm9QL094bE9vd0hGeHkvRU5NOG5EZ3dpUk9RQUdhQ0p1L3ZtSWFNc1lVUCs1V28xd1dES0UzdE1OcW02aUFqYURuZzdCOEw3SmpDNStKRmI5MnBtd0lpamJ4WEdkQTNVMkFEdkE1a3BQZnhsWloyc3FIbkdqZTQ3MStzTDhlU0RvVHJ0eG1RNVlHZEoxMEQ5WXhRblJHSlUzVjFQbmx6RHpkU29ORkZUYm9XQzl5NGg1Uk5OKytlb2M5RWZab0FWR09OVzZOeVBuMTRmSzF5VFpDbkVOY1MwbXpkTmF6ek5ydkVsWTRYYWk1VWMzcDF4TXhCR0RIT0Q3ZmVEdVU4UlNUcTk4Y1pKSjVXdnVpWXpUNCs0UzNzdWM0bFpGUGhiN04zYVR4TkJkaWRKYnJtdU1EWG80OFcxS2dybllSNnV5dUxWR2J5dmZoR1ZGcVVzdDJ6d0l4REw3U1pQcll1NEZOS25jbmVVMGdiVlNIcldOR2xOMHNialVlVHdPNjU5eGo1dnVpR0N0UVFLMFJpemg2eXRTN0RYOFBmQ2Q4anJwT2Uxd0R2OEF0cmkwNVdaS01HNDlyaWR3OWo3bzJWdkw1UGxIZkk1a0w0SnZybkxZS3dPaktBM1p0cm1jMXg3TU1hRmNMT1lFcmRNTXUzSzJEYk1rRDFjbTk4UEdqVlJGVDQxRDZpUjc4ckQwaUNudHNVd21jSGthR21FOG9nT0xqUzFkeStqUHRCd0dubFZva1B4U1ZrbVJJbEw3NVVOM3hkYmtMQTdjcWVxeHVBa3h3cUN0L3B3Z2I1cEdQYy8rQWdZVzc2NmdjeFFCRVBhTHZtTm5zYmwxbTg1TkVLblIwd09vZGw5V1N2NWJQTmVINWZqT2NwN1RQYktxSlVwdy9uOHY3Q1JERDdYVGVGYzJqeTdpWHVjN0dnc1JFYjYwNWtXSEhPc3V3UGx4alpaYVY5VFJSOXRtR3plbm0xQzBPQjBqL0FoK3Z3MU1VNm96SnNJZ2FwekVrNHlQeEpFZHM3Y2JFOURiaklvUldLY3pseWIwcElhWFBNT1lXVk9sSHNlQUVUS3NOYmg2ekZCV05sVDE1eHpwc1g5aGpwb0Vmb3dKNjZRV1NIRndJcEsxb1FpajExZk9HbGRKWWJIUG9GKzhmY0FjSXpiTUZicUZ2NzB5SFVqNW5wQUEyWUQ1WWViMWk1SGkwRGxzTHZMc3ZBZ1Q5NmJVZlBpcXB4YlI0YzV4cERxc01TNkNhZm9qRjlUMUFVenUvc3U2cTI1ZWVHbU50UmdPdXZrTEl4QmJZaldvNEJ1N0M2OXU4aytvNUJBNkhoMFQxTkpzdHVzS2MwWmdyaGQzaG05NERxSnpONXV5VHVjcCtERUVwVFFrVm9KMzA4UThvUytrc2N4a2hEUHJjZi9IV0JKSVhYb3UvSXV4QVoyL3djNGNUcGVhazJPNklhSFZrK051UzZJUmI5ZjcxYnl5TGVMY0o2bk9xbEZrWDhjRnFTRUhZVzBpTDFLRWN0T0tNR0hPMzJlbUErOElneG1CQmhZNVRpM0UxR3RBZUpiakwvZ0F1VjM1MFFhVlhud0poQ2N2QXA2QnNIV1grd3ZHL3hmaUo2WHptUHJRaGNuRnJtN2Q2KzE5bElTMWxwd2F6WUh5dUgrTXNpQzFOUHFFa09UTlh6WmtJSW9QUDk2dllEWU1MeDNIbUVVY1hwSFczRG1YWmZ3ZmpzV1JoQzRkbnllak15dFVaME1iZ2FmRUtQdGlzT1ZnUFdyRGJjRlJTVklocmN0QTlZTGpmcnhKMGhiTWpVcXp2TU02Y0dwY0prQWFUNGdMT2s2NWZPbU43Qm4yS1NEcWFFZ0ZJb3IzVndrYmtpREhNTk5UTVVQVkc1Qm9aWno4UzFFbXFzSWNURG93cjA2bmNUUTBLTHdsSmRkWDIreHA4c1lCRzJBZGsxcTc3S1JjaVIxeVVrd01kelkyK0F3QURPN0hhMlVDY2toNzJQcHArbUtGWmd2OUFERXYvRGlJbEJ5VFdycnZGQ0NPVUxab0JMLzd6TjBLSDdGRmNWR3llcERqNERMcS9SMWVqbnl1MXV4QXNGcUJydVM4SXB4emRONyt1M3RVWlA1QmUzSUZtUnM0ZjkvTGRJMVIrMGdPSWJzdERSRm1WSlJKSi9qZ3g3dFJYa3FGTEdNK1VnUndsRWZjVjVua1dzMGJaeWJQazZYUkdpYjZ6MmlEa0tUeVIzRjhZTnd2L1ZJSXB6aEhpOW5zWUFTNVhzRnB0OWczSjJOalZMSkhSMUJ2YjFlVFJVMFJUbDVQcXdnZDR5dmo1UTVmR1dtaTNhYU9ZaTNLRFM0anNZQzZWb0lsaVh2NGI4VjFTZU8xVjFRRW9HNlNtNzlyZjJGNndNczhSZHF0aktDWmd5MTdXeE4zV1FrM0NaYndpMFA2ZXVZcEVxRkVtTG5iNG9xTENmM0VqeVR5eVhKdkpadXdFZmhGZC95VCtFN0RnUVkvcCtTUXFaU2lSY1p5d3BkK1dRRTNpVWVnOUo0VmxGSEYxclR2OGdpbTQrTWJ6MzBQYnVtd1N5RVRWcFYwSXQ0WDcrZHdkOXN2dE5SejBRbzhUajJidnFMdXBaWXUrcktYM0JRbXhwdFFuUkVObTI1MDJWcS9yYi9UNHJXN0pxdE0vYVZmR09Vbkt2VitaWjVwaEY1NEN1RjFjOGx4UFczSXFVMldlS0psVXMxZ2l4OWpEd0JKM0JlbXVCUFVRR3pBanpFMmdhOTZGYVJNc2VCTzdTSWFKcit0SnN3cnBtczBRdGdPZHZPU0tMK013U3dNbm5BV0pzb3VLcTlmeDBDUXowRThrTTFRa0RidFNtWjZlMTZhUFhRMEFCb0dkZUp0VXIydnhneDFmbTZEY1F0UzUvMlBxY29NUWVXVWcrbmRwY0R2QmdQM2ZGcHp1MkFPQThONG9zVC91TmJuYU02bnZmbXI2aEpadjRkc1QycHprdU9sbmdma0IzZ2RlUm02enkzby9YcUppcWJzUXk4WVkvU1l3YmVRSXI2aVI3eXQ0TFRyeHJCWG55R0Rib1dCd3VnNVppUXlncWtRMWp3cGZPYnRteG45VVF2YW1NeitZbXQ0cFJob0VGZUlKeWRWTUpCc2NYOVZiM21MWUwzMDlwWGV0UlRjdlc4TEorYmZUckhhZ1Q5SDlTM1hsVlpoZmRydER6Y2xUMlFEQm5Gb2p5RlEzM3BuRnJaZE80L2RkMmgvZVRvWFIrK29CdkFIOEpDTzhlSVhtMWJHcVppczBBc2JIYjByTDVCeWNPNUNCbC9ncHFGOE9YWHdkQ2lEeGE3dWIwRW9TYStDMVI1U3k2Z3FkS3c4QUZLaFhHb3VFVFc3TUNTZUZhR1FRdzU0SFFTZjZpT3drdzVBRkNsMEpXN3VuQk5IelRjNzkyMDhsNW1obFNMam1wT1VyRTlESFl2ZVgySnBxN3JvcURUVVZUWCtxZUwyLzh2NS92SXRJQ1dyMW1sN0YwNGs4VXlSMXp3d0s3UTBLSFZRYXJjbUdGaTVCVm5mTWIvRVZpQkZGcXQxWlQyZVYrK2U1TlBjeVNJa2dQM09OckxYeFJ4c3FybTlaWUM5V1Y3Z1liL0FUd2tzM281SmJzbW50czVvNWJkS21PeFRpaWRCdG5GUWdXN3VTcHVSZE51TllYYW1ZdlIrRTh4WGgyUXFRQXVRNWZsZTEzSHVDM2JXcHErZDVtNWVpQ1BSTFZmZVpVQlpud1RFTHRaajFNNlQ5ZUJsaWcvVTNDWXRocUpMd1NUZUhzekNBMThXNlgwbVh3V1M5YkozdWZtN3g2Sm5LUDVJV2l6SnlCRDRKRWpCbjlyQjZTVVpMTnRQdDB0ZmtaWEI0YThERUpzcmRncjJBc3AvNzBJUnlDekZoOG9XcTd5NWdSUlFIVDRHcmt4Z1F5bXdiRjMxK3hpYVJTYlVwalQ1QTZZUkFycnlKQUZLWGRaY1RFMk5sUHhIRjQ2c2pKdmU0Sm1icVgxTE44K1RBdDR5NlFna1pqOXMvL3daeGU3MWFEaVRsc3Y5OVJrK1B2NkxRcnF0cHNBR2tMR1dsTVh4Tzc1MytPRU50d1lwd3hUNlh1eEpuQVBoZ0I4QTVOdy9tTWxXOS94RTF3dnViWmZTYU9jRGJHdC9uYUlLY3VkaEVqNWxMZ0hjMjJNZkRobTQ4Y2paS24vWVllNDdsL2RST0JrWHd6TElRUHVQbTl0M2U3MmZZVmVjalM3d3NzTWR6VkNzSXNULzVPOTdST1YxL3JkQmVVNGFxZG10TzR3b0NPZThZbkJaTjdYVUQ5TTZsaTE3MU9PcGRYcHp2RC93ZG5pemdyekY1MFBrbU82eFJEUkxzYlBacEdyUk11T0lCaWlHMWtNaUp4US9Rc1JJTXpqVFVvR2xvL09yRlU5TmFwYXhwNEFrckpDVUtqV245QVIyQzFJVnhXRjB3VVNYQTVEZWVCb0UyREF0WHhoK3FnNVA4S1VCSitjT1VWLzlsUDl3bCsycFN4M0ZwWWZwdkxvTmdUSFFBSGdsNU5XbzRtb2ZIVzBTRmkrM0ZDL2R0L21HVlhiSS9UQ0MxbHR0bWhYMm9oV2pHRWkzNC9MV3FXWE1zWDh4aGpicThDWmFodzdZMitZVzVGdmcwU2NHRDBCT3hvNzdGRDF3MWxwQ0RYNE9MYk1aQzRJNldOdUhLblFZNnZIN2plRzJOcnBWcEFZWUJyajVvYzd2cEw4TXB3U2pGYmY3Rm1uOFpwSjBwTWRyN2ozMmF1bXloS1YvM0lLRDI4RUhjV2Nrank1K3R3Y0ZjNkRtZUtMZVpoS0pVNE5JVzVNNHlhc2VHL05QWDNPcTdQbm5USFFiWlRMaDEzS09ReUx1SURWMk0rOXVRa0RtTG1nWithem4zODZkYXB4bUZ2RGZMWXhxbDh1WmUvblJuWU9HMk1PaHBGblRybU45UDVob0IybC9FZmxlT2FUT0JWNms0ekZrR2c3ZkhtczN2OEpIb21DU1QyalNudTg3SnAzTnhldmpRRys3YWxvdEpqcDZvemczU2lPdWw3SDIrRDJwUWJ6RVFlNVpwdG5ZdHNBSTBiaWtybTFURGRYRWZ0TXlhU0RENVRWZklLNENBOTJMeHpCRzlsa2ttSGVuMzUzV29sb3FCeVNGdUcrZHFWZkZENzJlVHVJZWVaWStoK2p3ZVBrK3ExT0RDWlVyZk9FNE94ajBZWmx5MzZVZlFndy93VnVEeVRjQ0ZRM280V0VJODVMeHpxcTUwUEtlb1EyN0s5aEpnR1dsbDNjSDFEbWJaY205U0wzSlV0bXVONCtCMk5HV2VWQzdhRHhrZVlkb2FZcmdlQ0F0RW5XRHFScFFMaW9TK3Z1YmpkczBQV1RvbnhXTHB0b2EzQUpoamQ1TWl1QnBxaFpLQU1qNjl6cmY0WkVqQW0zbHM4ajBrdERDTHF6MXJEd2VOeDF0OHRlazY3ZFRhVXRhQlhQSldEN0VUYmE3Y1NGU1hLd0pTUklzQXkzSmdFR3dQc1FIN0ZnaVplRDMybVpEME9POW1mTXkxRU5ja0NaQTRVQUM5TEVVdEllYzJtQ2QySGcvL1MyQ05XYVhBcVVtY3N1UHdIeDBNdzFmeUt4N29MWk9YVzZXTDY1QjBXci9GVTZ0ZWo2YlBHMnkzRklHQlI2S1NhWkRmOUtmbm0vT3B3d1BRektaZVQvMm9qcnNpT1lXOUR4djR3VTA5ZUtBSTdBMG4velZ5dUxpekpnL2JNdnRsbTRiUFYxZzZBRVpIRGQ4RWkyNm1uRDBmRG80eDMrd1N1Um44c1UwaUdsK2RGaDZubHpmNDMwZlVwdmFaMisrQTdiaGFwZGNTV3h2cWZtWERHRzNuK1VjZDBBZ2l1TmhEcCttNk40dlB5V0txV0ova1VuQ0VrTmR4V3A1bEVSVk03d1MyNllpWHptNG1JWkZ3S0pvNDVFaGhOakZJc2JodTdiRFUyaXd5RklZUG84Z09SMS9OUDhFRFpJelpSUWpIYTJuRFVtL2kxWHVqNzFPb2x6cW0wcWVGY3VaNmZVbFcra0k4VnJEdFU2aHRoQXNKOHVGL2Z1TmRkbGJ0aytBMk9pK0pIaG52QVJIUnRUUFRicHNsYlczY0NuNFAvb2djWkxlaEhXV2FqSW1jU1RpR3JmdEdmekh4OHhkMnBpWjJUQWFSVFVlNWV2RC9FejVYL0NUejhSbU5mZEZiRzAvOXJrUEp2SmJXc1hlK0pMQlBRdTBMQWx6QTVBMWV2aXVwVzNOQ1RyNUVKMU5uQ1BOeEhDTXl0UmN6Z2RxWXNxOXZuVWtqVWltdDNEaVdkNUxOdXg4RXVpMkc2SWYwRFdiNG1uSUZDcW4yU0dBZWUxZEIzR0NFTHBNUllHQisyK0RoT2xkb2xOSHdYVXkvNzNYOHJRWm0vU2wweUdZS05paHh1em4yUlYwbXQvQmh1NWJCc1M0aE5iVGRXV3h3WE15U25ja3p2dDhRbnFhalpBMVdQeEYzbmhRN1JLS01VQTY1Q2dGT0NZKzdoWXdrK2lQbEZIckNjTkd3QVBLanU1YjdkVEN6eWdXeUpzMmpVNTFwbWRCa014T3puYlRNRjlSQzBqNnVFNVB4OGlPcjVxR0dmZXhNTXg1eVNMejdOOUFzOHFlTmFvN3RraFg0QVlmZHZnY0ZleFExYUdxOTMvSGVDcTFIOVRkdENRVDBQbkN5Umx4eUZoTGRIbzFmVXJzVTBXRFBBbldSUHR2MnhhY0FmVVdYV2NZN3ZGYjlUWW9XZVU0dWJzeUhtRUh2Vk81Szg0ZUJ0UWtHa3BpTW5nV3lSclNnREJ6c3FYK0JNajBhMC9sYjBlNExneENqZ3Z6YlUvem9Zc3FoS1VhSGtVMjFLV3dLK2ZDUUIrTXB3bjdkcThYaVh6RlBLRUFPQXVsSDY2bXR1eCtoZ2s0b0ZsRURPWWs5b3FWTUtLYmx0Q25yU1NnbUx2aDhFVC9VaEZTR0ViUE55MlRub0NDeHZld2t1VUVscERjZWdpbCtKK2ZnWGRyYVBZVGVCemMvSmRaZkNwM2JDdTFmVVdSMUQyQjh0RmtvdTdmckI3RUgrWDA2eFYvakxTNEJwQUpjY1ErVTFvSk1GRXRNYjlSdlZNY0ZBWVQ0bjJTYjRPVEg0UityRGRocVBMTk5xTkNsQzUzdDhiYnE4TSs3RkJxUUlnSHNwMTVWNE43WCtxS3pVRlRSUlZhOHBKSEZuWHh6NnNRVnBBZ3pvNnh2WGw1eWhTbndWNDRJbDg5QXBFMHdUcHl0RktjaVZaamFHQTdDb2VZakNIL2xWdGpjckM2WUhpRXlVZmFqVGpEMTJ6aDhnaHJWSjBSRjFqZE01SGp0ZUlhMTh4RzFkQ1ZXZ082NXBZSlVuQ214NW4xaXlLaTVnajU5NVYyRnN3eXVpMis0QmdBMUU5TnhaRk1EeXFFckt5b0JUZ2p1OGpiWThCbDBHT0dRcHdBVnpSNXYzN2JjS2FvN0gveHFiaDVYOGpsaExGK3lIbkQycmVOL3QySDlEVHMzT0Y4TFN2VU4ydG9iL01kcWhpeHMydjBremNlQW9UL1U4MXZtbWg2ZjNMKzhKZDdZZlVncmF5M2U2b0ZGSnBNMysxVWVkY0dkQzh6ZWJOSXd5U0RnZzVMb250blZhRk0xS2FyZG1wa3luc2t4eEtrQmx2VDJjdmk5SW53L0llZDBzUEIvSjkzOGxxczlqRGV0MDVpWi9DMS9pbTBON0lscmVkaXZsVnQ2a0hxemdBbEVTSXV4MTFRRmEyNFZvZW52ZHF0NVhFZndkV1JkYTJUZTBjTnNvOVN1ajBabnZvaXNyZENoOSt6bVJNWnNBK1FuZzNzYVJFRWRZdHpseVdYSHpYTERrVDFEeURjdXJoRzFyRGs1b0F6dFpZWE5QenFWY1BCenV3RlU2akNpTHd5NlFKN25YQ29hSEhNRmN0TFZpQWRtaWpDcXBhdzdFOFRiYlU0NjBnTkZmVTdtTTgxWU10OXlhR1ZFeUlXUWtUeWNxMUpiQTNrUVpMK1ZONnhuc0lKdHVGSjR2RnNJM2xDN1dMZWZ6U0h3cFpPb01PRHFqQ0JOMXRjNEUySHExbC90MW5NS0xSRXZLZnR6aGpnWmRyNHgvTFliRGJxL0ZvUStmUDNTQTZjZGJKTmhDcWl3K2JYS1VBMFBxODhHdkNnSUJ3aXNxbkJuZ2JtNDVtcEVROG1UdzhXeHJtL0pacTgzektaTERQQnovanlaUUtkSG9tTXhnRGJua2pGTlE5UnVUSHlzVklDcVFuYkh4MHpLRFpUSVNFNTQxRkRZOHFYbksyc0NIWEtvd0lWMEFqSHAzdThsTWlqcFl0b0FTZHMxem05MmdxQjVqeWVjMzczUmlnRittVFRnMmdzbkpOcUZya2t3YzBoazlIWFFGdVprdlFjNWhSNWNpMVNKM1VCTW9Zbzl6N0lLWlhycmFKaHJhUGhvWW5mRlR6bU5RV25FQTJYY2N4NEdUMTE2WTFPcFpvTnNnQnRTVlVqSmxDR3V3WWZ5NEFVMXRuWlhFd21SSGprWDhmS1ROSWRkMUhnQkw1Y2hPNVpYdFVTam92cnFGdFhxeHJUUmhMQW9QZVVlRnYwYTE3eVdjTVU1Yy9XL0wrelBlclRJZUJjQ0dkRC9UY3ZNVFBkV1ZKeFdLQ05CaVNUOHgyY1NkUS9pdGZUL1hINDBIRVBkRUF3TE5GMTNOT1NKb1BkMHFvbkhRMjlLVnEwdFQvakFER3BTbkVWbFo2ckhjYVd6ZVEwL0RqOENlbEdRL0lweDNjcSs4dEhaQW8xU2w1SXN5d0hwTnJacW56bWF4dktCOUlsbGFtaHVpWDZKbi9ETnIyQ3k5cE1oNnYxVXdlMi9FUk9ldGxSNUcxakRud3g3TkR4S0xxOGprdTNZUkVFZzQ5YWxiT0JVclB3cU53WEwveUNuZE1oSlJoRnlwQ0ZJdGN1eFFHMmN6eGszRGVydS9JWHBrYWNNWHhnNnBpd3l3OHdiYlg1cnNEanA0QTlIMno2NVo3U0ZQWUgrWXJYOHJ2bVk1WkVNMGQ0Q0tOSHV6WGdzTURqWTdUR2hjNlM1WVFVbWt1QmMvbTI5ZUM0VUdic0R4LzdFWUFkZTV1anNQSnRoSzF4ZkVwb3haaXJHUC9aU1p5TVVGZXU0N3pzWTVnTGhkR0FVSXdXckM5NFo5R3J6dnhqcmxzTWhBRWc5dHNLUElPcjY3N0VWY0NLaHRocndmWDhMMUtpSVI5Ny9VbEZpWTltTEVxV1Baa012WjdvTHB0NFNsVU1aMEJHVVN3cjhwZmVURWVnRVI0K0dWdjkyQ2JvUXN4ZjVLSEFpNXkzcG15NVdaU2JmNjVidFcxVVFyZ1FwbENNaEdkNW9uNEp0dUFkRjhBNkE5RFc5V3RmbEpobGdDdWZzU2xZeWduRXVxQ3prNENPSk13QzlyT1FLbXRYbTJzSEVoZ0ovRklFM3d0MW5RWmtreG9UZzkrZUlUQkVsZjVDUk52a2d2L2JldGdrWWxWZlBJSE91aFBnRGdLKytpemduSExTY2M5YnpTRGpzTWF3OXdKTkxVNWcycUI1LzBqS204elY4NU4xRW1lRFhjUkdQWStnL3E3dDRYa2VObk1GTHZSVjEwU3RYdS9aL000NVg2d0t2SUNEMGhLZ1hld3BsRGtPWDIvTFkxZW5jaWN1VUFScktMNlp1WlY5ZlRCNXI1MldLdTZMZTBqMjgzT2xHRFRZbGpxbHM1T0FnNyt4SUw1SkdwRFJnOEZnMi90Q0duV0FzZlZZTHB4bFBsMGxyMlVwV2xSbUJOYWFRT1hQWng1ejVZVE94V1NiZTVZbktTWHAzaGdVcC9kN2l4aUFXRkkyUjFZZ3JqU0N3VURXWTNGbXZOZHg3SGdQMm1ILzNUSHJDdGxUNVpEYXZSNzNEZzlFUGxKSU9aNzJLTThaSEVOU0xkK3VsNW9QVi9LNlpxaCsxaHBsZFU1SVJLOG9oZ3pWa1hJMk9zWTIveC9va2NhKzZxckVVam1nV2luREZVV2V5Qng1eEQxWGJ5Sjc0ZHhpOHdHRmxGYnMyb3VIRXRPVkcrMG01WVpOWDQxbWlMWkRYWFdiN0ROT1drY3lGcjhXVm53cVZpcGg3VC9xYUNYOUV6Z0lvN2lZMlVqTVdSQTA1TWMxMVFyeDhTK3ArUjJuWWVvOEJhVmx0Z2xDWkhaelRzZ0duUnF5K29aY0NsaHR3WWU1dms3c2gxWk5aZW03ZnpnUUtwZ3M1OWlpaWFJZ1dsNDlma2VYTjFpUDFDb05NY09ldEtwU2ZVNFdZZ083aHVzY2dJdjlGV09sU1g4WTdwWllSZWxHZnFSVGFXd3FpTGNidlRlcmFjR3oreUlsR1Rydy9yd1ZBaG1CdkIrUmpDVE1QR2tpOHhEcnZHMDlIUmJDNGZ4eVRBZWQ3OE0wYjNYMzJ3dm9XY05YQVNSNkxOUzJOUGVhUXhwN3dyamt6QzVUSVdmWlpuNDlBeDROandyRGVLdjhWdzI2OElER0h5Mjl2S3owMmk1WEMycE9rRFRnS3U1YTZaOHQ0K0NuN2ZWV2h6cGt1WDBqNExIaFJiNVJxZG1zbENMK1hWcWRyc2xma3VOaHBYZ0NCcTdoWjJYS0RPeFBSVlhlNys4MzI5b3dwcEtZREo5NXRHLzV5UjU3WGVKTjNvQTkxTzJXMHNRT0I1Q09KZE9DVGlCMEdtKzhBSFZmY25qTHozbFJlK3ZYSWF6dVpwYnk0ck1ZYURiSWJBQ1pQVndwdllEMjl4ZHlGTzBJUDlqVlZvaXpzdHhZTkZON0dUZEdyZlF2OW11Q2RZcGROenVOeGtCejdtRHRvWStLa2R3Qm53WUhJQ3ZwK3hPbytmYlBqeE9lZkNTZG1ORG9GQ0M2anVmN21hcHYvaFYzWWlFUmxtRUpsZFhpZ0hCUTBzNWNJcjJXQ0xMNzF6Rkh1ZmRHejFYRjZLc2toK1R6VXBCUVR4YXNpYWZjNHZRVWJ6U2plSjI5ZE5jbjQ2MFVId1ZMc1hwM2NROTYwVlNXei9PRjVaMVpRdFdlQU1ST1dMTjlvUXliTFJRM244RitrY1RkZTk5dnpKbEVCTHEwQ296VXRtU0NxTEdGSUc0MWphS1pGYlVVajlpekJPS0ZNSzlMeU0zeXcyNkd4NE15RWlpRkUxRlhodGF4NzRVNENKa2FPYlJ4UFQ2VHhOOG9STUxrTTJOVVJpcElsUWhXSENvbHBqT1pXeitKQnE2dkV3a2h1QzhOb1ZqNVJpYmN2YlpTM0FnYkd0ZkVydTV3YngzeUpKRXlzeXVrK1BNSUl5RkV5WGpnQ1BVT2tiS2JPUUhhbi9mZEF2dVp3TGlUdXRQUTJYaVRBdnRNWGlFeEpDdDVlMVZRdmpvbVVoL0hrQUNERjU5dXZtUXRBZlZSYXltTE9kdy96Q243cm1ibnhWb2Zoc0EvRTgzZ2psR3RYelRDVVlLejRtY0JMWG9jOU9JWG5XOCs0Z1NDYlpnTERHM3duU3lSRmZFOHNNOFVZenZVb2VDeTFFY1NPRGpOVURSVFNUU0hiaWlHa3ZtTm94bVM1SFBOV3JZSTRUZUhFTmhFWmFlUjM0eHFudFUydmF0a0JHSDI4SWsxSnA1KzE4SkVES0tDR2w4RVJLdHVtR1BsdXE1RkpDT3NKaTE2QVVBZWtoY3Roa1pKQ3lsRXNnZmVlNmlTUjNMam1Va1ErMzJhZHQxbk1nZCtxOHRwM25ORnZ6Q05LUW9XWCtibTV3QjFxUzZGd1dOSHZMZ05BWnlYTEE5K1JOVElVTzFydE4rNjMxN2FGU0xwMStsTHRreWNVSk5SSTFUUVdsMGMwbXBVSGZvNDVrcWt1elBIdENwdzZmc1hCYkI1dCtXVFNEMWZubXRrOFRjV0hqRm0vcnZGbzJrc2JJVEdiVGhlbXNReDZibWxKL1c5K3c3U3ZoVHgrQ3JZL2lXcldtSW5ZdVc2RlEvRjFOUkV5ZzVRaGUzaEhOTlk4MEttNXFCSlVFeUxEZ0dHTlZ2UVpULzhaVGg3NDdQNlpiUWFGWHJ0MURSTWR2ejFrVWpsSXhKbFJNY3M1Skliemx3UzBxclpGb1VuYkVpUHRpMHFHaHZ3YWhZUjRvY1JTbWo4SUZLalFtaTlvUDNpTENkQjVsOHRBR2orTkZyZUNoTHhWMUovbW9qOExRTjNYUFREaU50TitiZUFpakhzMUp5ei9EKytlaUgvSEdsQkpZREo2QWZRYXJqVGphcll3KzlFM0I1bXI1NnV4c2ZkV09KZmZiSW8yeXp6VVQ5OWxWcDFsVmpSWFN1YlR1ZThlL3pBbzFLdTVoQ2VnZDVlQW5XcW5xUVR3U21FRjk4TlU4Z0JrUTlqWXI0UTZkMXRNNXEzYUQ4elJibjBndDZRcFRZZTc1Mi9qb1IvZmFVaE5PZ0E0RTlZdWNLQit0TkdpNC8yMmFScTh4TjVwL3JXWlBzMW1JcU5xRnFSb1RmYXZBSDc0dVlIQnlGWHdzMiswS09hcmNidzdwZ05nR25JM0dZR1Q5ejV0dzRuM2JISlRTMDdmd3IxUU9KQU5KV2R6UWhWUFVpRHQ4M21WODJ6NnJPLzA0YjRRZElkV3MyUDllMkZNRjIzVS96WW9zZGRoaEpDOE5KeHMzN2JPOE1SQXdnYlNVK3J1MHJ5VHNIUGpMdjQ4UWpmSjN4L1dLcXFzdC9YSWoyNFYraEJ6TXZXL2F5d1haVk1keVl4ZUpET1doS2d2TTljL09MLzM5S1lPazZmdjVaWmJPcGs2NXU1M2YvbmZIUDdtclBUU0IrY2FrbmxVUWx4ckJSSW02bytmWE9uRVlyNWdrQUc5d29hOE5tTmcwTStHd2NGOGVuM2NHRm5LQjFkN29QY3F5V21pR252YkErMDVWUWpyU0FSbThHZ1dCZGc2YzBzSHRLOEUrL1A3N0t2SHFuZ3hnYjRWTVRhL3JNK3lhS3FkTHJUcUN4U3ZYcWZuU1J0OGxsM0FNRkQ3Q3huYVpJMFovVHFnYkFjcDVqUzA3SlJhcjlWdDljbGNYZnRBOFhGQktsdnd0RDZEeXdtdFVNc2kzYUthWmt6MWhkSkp1ME8zTzdTV2pZSmxZNTBjVSs5MXZRM3NRdG5SZXZXWlc0QjdKUkM3WVZ0bThVelErdXNodWtkRlNkQjFZU0E3bkNZdjlOUkhPY3FNdDU4cGx3a1ZaK3JhMDNFa2JUaXkxREJnanJiL01uOUo0UzljZFFMcXVRcnNyWTdoNFZFMzVvbEJBaUIvN3BTend3c0hyajAzZExQQWdrYm1pVllJeE85VCt5eGViaUN3VzhkZlJDWU5zcCtMMS90M2RscVZONzlMWmdGZWtFeXZyMFJpVWpKcUVNcUxtNDJYWlFtSXJ4R1VFZDl1NG5yNGRrWnJsOVVvQzhkbFg5VUF2SmpreHNiTFlkdjVZNzU4enZ1aTNHUmhnQnplZEhvVnFaYkVHQnFKOWJGUUhsdlBLUzdFY1FOa2hWT2hOL1orRkw2OUVYUDJQZTZUbldzZi93MEV1aUk5RXhReXIzTGgxWlJPQ2lVLzZrQndKODhWTmdhZlRLcHhVN1kvbXIydUZ1NDFxREhHcExMZUtCa1NkeUlEUm5MclRldkx4dC82d09yVS9IOEY1TGN1dU5tL3ZRNzBlMkpZWUFXN1pyWEJOME5STExDcnV3VzlXbEQ2SDBTbUhZMmRRUXNPc0FmNHEzV0dPV3VYVkN1WjBtdlRIN0JNNjE1VnptU3ZNTDlOeGF1NnBnSHJFOHNUOWdXcnRiTm5XeWRhYTZxN01uQm45dDdPd0Vtc0VUa0NOU3VTbDlFbGE3VmVsNUFuZjh5NkhyUzlVclR1VTU1VlQ3WnFFU2EvRHhScHFqT0F1VFN4bFJMVDN2MTNVdW5hYm5GMUMvWUIyRjZCSXpEazFJejk4OENsc3o3c2tTWUNYL3lkam43SG4xNFYxYjJoZWVaYjQxc1NXR2pNbDZmdnhJV2ExZWtjdngxK2Q4anNPUjN3S1VwcHdzR29oT0RTRGdZc1k4dWhFVnYzMGlqM0ptV0ZvT1ZnS2xmbTVvZ1QzTlZ4OEdPVkpPWXRVTHV1a0V3djVLMExBM2N0UWFPWmw2NlpwQnFkSXFabkZVOTB3QkIvdC95b3ZucTJXV3oyUlJEVlJ0MnFEQlByOXRuQ0tUL29RcWR4bzZ5ckFEajBMVzAxZnZSSG8zQlVOSlBLSXZCU2xiOUY1eHN3MkoraFhvWlByUWp0MGdSWGRBTENVVzFtMnhQTlhmclZXZnNNbkVTaEZXS1QxNWd2ZjJjZlpucmo5QXo2N1BCM010MGx4UW5GTUFlRFk2dHAvUlpGOFpMOWdOeU9JUXhQaVVGZUlJUWFtMGJpU2hmOFppdFhhZFhzRnhmOFIyQlpaWkhLaXAwWVgxb0ZSTFg1aTE1UFV5c1VkbmFyVTJqS0hEb0FLTzltUEdHWU1PUDhCRmo2b0Z4UngrTVVHaVlham1tYy9WVG5hSXpNTHpuTjQ4RFVSK016b3RlbDJFUk5TQ1pNU2h6V0lITUREREFzbnd1bXNaajlaSEFnNndnK2J6QXdsWGtwZjNNZDNONGt5WEFRNUFqQ09IeXdnVG9XTWJScjhJeTJMS0JraHRKNkZZc0c3THljdHhMd0Q2dUdaLzJBeWNTdVBIcjJxeUpBdWY3KzNWenVOdmpEa0h2UWYwYnl1S3F4MCtSSElxLzg2WDJ0eExUU0dNbG4xNFJqcWZXS1g4YjhiZG53S1dabXFsOHk2T2pQRXlzazU1bytBZnpkNDNEbTRyaWZRTGc2RzhiSDcreE9FT3V4eS9wblQ2VEVZUGpCZlhhNDJVOVVTV3M4bzc2R0xKM3RhbmY5ZlNJd0FYWXlzWTZaNnZ6RjVXZ1hMZUs3SndJRGhvY2NReGhIdXVtM2dmRzdodXFNWDZlejlwSDlBbTJTc1NOaFhzMkdMSERpQnVuUG01K3phSGFLSXRiUmVYN09GNEZpL3V1MUM1ZnArZm9Sd2dmQmNpSytWYlpOKzM5aE1lTWhOYVZqRkswVnpyL0Ria0NOdjE4amVsdWx0NlhHZm12RHlNUzVkUXZpL1ZyTUU5N21iM2RXZGdtaVB1YnFKR0lmTWRKRUZQdnVyMWpESVhyWnRWMkpJN2FuM0FBWk9QSWNQODVqRFpDZXMyWUhYYmFtM2hsdSt6dUpMdHdsa1UvT3BxTmNFRWNKWXRBRGhEVks3NWZtNzMrQVhQT1hyTjNIRWljaWNIWVhZeko2MWYxZ3BDZUFMTWJDMmM2amlmQzlTZ1FXNnEzL1ZRNWxUOUVtOGNRV3AyQ1lxcGtLTFpBaHlmSU1hS01FTTFpNUs2ZFFLd21wQ2xORmdpVTdEelZ2UTE4cnRpRmFTOWUwVElTZHM4a1IwazJRZk1XQUo0NHd5SlBHQStZaUhHY25TbUE2SjYzL2ZnM3hLNTlrSjlsOU5vTFUxM2o4eEtNL2N5QU9YQ3pJY1B1ZzFWOEtOaFRaZmg0WnJOSlJRdU85enR4ejBDZERkMWUweDVVbWZKYjJ3angzdXRUWll2TjZLdlVvTU9nUlJhTmRWTVJiY0pZTjVJS1R2V2ZtZ2JaOExSSW1PU1hGWkwzZzcwU0RlRUdIeE4xVmtNandUeWhIeHBGUUJGV0Zjb2hMNEt0Y2QwQlRlb3RSN3h5dWFWbjhzUU1uMC9ZR1AxWmdNV1ZiWXordHRJUEpsOVBjZG5BNWkrQm5OdXBQams2MFpjSEFqRUhzYjRRMm9VMXBkMmVrN0w4QTFxald4bFNneHpwZVdGUHYrSjByS0lDcUk3RUNBaU4rQ3lMSmRCL1FOdnFUYm5kenhPQ05LQUJCcW5mNThmY2dqS3lkM0JUbVQrT1RYWnZPOUVDS202VlNwN1JvSnVPOTN1Q2s1UDlBblltbnNodk1samtST3NHc1pEK1duMm5BVk5JTlBaUjBON3FYM2lHeFZybDlTaFlRYlc5K29KK1NnOXdjeDlwcDNPcFErbjhWSVF0RmdlZGFHekdsc3RJcEg1Ry9RYjNEQ1VjOTJQWWhCYUYyUkh5M2hpSGdrUlV1NStXb2hlYXFLdTlaK1d0NURJTEp1UWpTVVlSY1hyRExTY0t3VDdJdFlKM3dtM0l0WEJFeEVUZW84MWtLOWRZYzV2bllVUlBra2V3WXowOGNqVHFxd24yMy9OcnJZMlR5bldWcUREa1hqTldrMm5FKzZIdkZGTElQNUJ5aU9IKzVCVHdSL1F2MEV2Z0pRUGdFREt5d0dFLyswM0Q3NnF1N0VLNVoydTV0WTI0WU5UOTVnUDJMK0d4OWJmdytYWUF4aHlPVmhCM2krZk9LcEtKVzY2YmlVRlduemhxemNtMXpjRy84S1NmdzAzdGs0N3dwaG5IMHQzc292MWUrQ2IxWEJRaFg2Q3FWM3Y5OHBrajJpZHBmTm53eE1sZ0RTMTdwS1c4K0ZOVWNBWFc2aGVhbmNHTXlMVmM2RFJ3WTZ6dHBHWHpCRFFRTlQxb25QVzRHcnhYb1V2T0JyQ000TTNvNENpbEQ2aEJKMkVPQVNHbUEzR2FqUHA4ZnFrOElFOXZZcWt5VUt3bVJZd1JzbHpya2FsemFtNDhQbkFlVDBleUV5UmtKNTg0UW90Mk1yODN4aG1LWVVNMTUxdjdZYXV0dVlYWFh4eUttWk5IeDVaOTNld0hiZUVEZjU3bW9LYm54Y0V0OVJaWlgyZkZoeTRwekxaM3pDblFtV1VUSERQV2x0QitjYTN6b1hFM3EvcXNGRGU5VXF3ZE9ZUkZ4Sm5NMHNFTXpqcFIyS0RwQWRwZkVQSkRSbVVBV3Vkc2JDZTY5SktBMmFuc2Z2eW5Md0ZhNzd1L2YxemU4T0FVSGRNb3k3a0QyUmRDYUROSHVmbWt4OWI3MXpqYldUejRQL3o1WWRWTmgzM2hZUHFyL1dPVU02a1dEQWl1OWVBeHJkKzhiT3B2UGIxcVA0a0d1NmZ0RW51Q3JwUlFHcTVhaXE1MDJQZDl4bHhiVXo5ZkVIQTRVS1hROEhQRmV3VmZ2WEtIQlRwbndkc1AyMFd0UXdGRTRydEhucHYwamxHeXZpNmh2OGVFY2Z4NFhUVUtHYmtkbVBWOG80cGd3RnE0cnpWUVptSFRjSU81SHkxdHRJR3E1OStGbVRhTXduQ01CU2NCSU9BNWxYRk4rdnlmdG5HUXhLZm45eXRsNFdSMzgvb2Y4TExFc1JYM1g2L0laSldaNHhMMEFNSUZSK1JpSEx4NVRHeVo3TDh2S05QazNIY1dYVVAzTXVlVXpOaTNmY1VYYkNEck9uYm1WVGJubGhuay9Hek1GU2RLMGFMUVpLaDZlQlhTcGY0R0x2ejBheGZHbWVvWXNhMHdETDhPNWkyYmUrVC90aTJMY05jeFhwWjVvbVlvOTJpWnFkWG9qK25yZnlTVlZVa3o5T1cwRC8wcFRuRjBnZktnekNmY3M0RGplaHRpVjNKbGxoaGtVNjRLTTJGQ2RWU29iZk5qY3k3NTFLcTh2QmJKWThYSG5GSnZjVVFDSVkvekY3bEJPbmlxY1Rnem1iVjZTZlMwb2tjMkh4TE55WU1YMlkrdnlLL3VWWER6bDJVUjdjVllzdHNjd2F2RXhiSkIwdWhkT2VyRmlHZ1BjWlc2YWhFaGg4b0hsRUMyWnNCTmNvT3ZTSG5xa3Z1UU5xMnovR2dScHdCT2N0Yk0vSnJIKzZXL2ttMkd6dUFTNEI5bS8vVDkrME9lNUNwbERJZkRyd3o5NXFJVXVaYnk0aVo3d24rOGk0dDJSNDdQN2R1aU9jdXFkN1NaTDVCNXIrZ1pocVNWNXA1VjA3SFlWTmZmaldiM1ArY0x5S2Q3UmMvazdOeFdOam5mVnNEamxKYXpUWVZrVE1KN1ZFdnIwUDJiSnpXSy9zcENWTGhRZ1oxdDdnRmI3MVJzNy8zQi9EcEtxcnhielRvanNJRDhqTWRVZTJ5d0tCMytRY0g3VHM1bXhSLzlYZFhhSm1JT083OTZvNFNYZUhqUk5pSjlib0o5NlI1K2NSNGY2ODJ6TDhXb1hNbExkd1pEa0FLN3pqcTRXL3ZER09yQm9xcjhYYWxpenFyVkErM0U4dDR1MHlicm1oNmNGUjF5ZkNjdnNnUllteHpnVXBGSE5rYUJDQXRmMno2MkxZeWxTN1NQdGxPdEtRM0xlRktFOUJRaHNjZ3Q0ZXBZNklRdDhDK2NWYTZUS21OZHk0dCtaWFgrK2hnMyt4dEQvV2tHenVKUWZvMFJ4akd5aUpYR3JFTk95SGo0dEZSY3dEbU43a1VRVWprWDltamhIZ1FjQVIxUDAvdllUaDZhYWRZdWUwanA0WkxPSDFBbDZYYWJ0OXFWZWlONFdpYVlkeUZzdHpERVlvVjZ3TkpQRWNUbFFPY3VYb2VzYjJxUGkxYmxSMDVSandPcFgvdzNoRjBIUk8yT3FsdzU4WmJDODdiVUdRSjZRMG9EZ3I0eFJYZHJ6dTJ0TEVBKzEwTHRLWDY3QUtuamhyR3RFSzdqMnBxSHJBcjZ1ZXR3Q1RVdHAvRnlna0FLcmErb0E5VkRnV2x6Z1V6NXhQejBSdzgvR3luSXQ0MTJxbGNqVUNON2IzL2hqOGloeURuRnVtUlpiUnQrZThRa0tDVjVlcW91dHo4QjM1UWZLQzFsNFA3eGdYYUdZWCtxb00rWGk5NHo1S1FNZGtsL3pVWHFwcWtSMmpnYVJpWFdmTGFyb01VK1NrV2ppZFVWdHl6TVZYeDlEbGtaWXBLRklYK3pURHJuR1A0dnJpcmk1UmpJQ0RNMkdzSVkyQXR6N0pDTFZ0RU0za21JdFVBKzZ2NzluTnNxQXk0OHhaM0pyaWxjdWYvNHVrSzYwSUZKZWt5MUh1cVl3aEgvYS9PdlE0YmxlS29LZWU0citmMnNsRkJ3czlxYzZmb25GZG9aNzRlSVNYNFRaQ1BrUG9LNElQb3BHYi9HVzdXM1ZQYmgyb3B6QUpjcElWdjRrc1ljZkhZOXhuRXlBamV1YmJiMlNNTFhSR2V3Y2N4aFl5MUFNK2h4dkRmQTJFWFhNbFBmckgwRU4xN1BQbFN0K0xyU2NLTlNKRHY5eW5EMmllTUczQ3ZVM2ZuRytwTDk2djJUVVNoOER1SnpMY1o3MFZQMzB4VzlGM0RGWW9waXExTjVSa2ZObzZOL2lWYlVVY2VwUkV4SVpMS3hXdFVTVVJWSE5HMXRsUGZGVWxaQ2RXcGZKdHUvYm85d2QyanpxTjdQeGRPbkFtK1VzZWxHYUhqNUlKZ0xLM3hZL0VjaTBjSGRVN0oxRmZFbzFvVjlPTEJEK25hUjA5V2c1eGRnN1l6TmJabGd4emdaclhiT3ZXeGk5SDVSSTVwWi9xOU1xOC85bWZYd2VUeEo3dlBTdUE0WDV0aG1ZaEpZNzJIUjMva2NPZUpZZVpRT1ZJbVk3N1RKODloNU1RZlVSeTNCRGJQdGN2b1ROQjg5SG8wdDk3UGo1Nk9MSTJ5WjRFVk9mb241L1VNM1NzbDFqd3ZqWjRITzlSZnN3OWlhamN3WFhpb3RvMFNHR3paSUZodHNrVmo3dFJ0N2FDa25nYWcrVUVzRmRFd2I2ejFLWGZscjUvai9LQTVISm9Vb1dGY0JYd3ZWZGNMS1RWdEg0QW9lbm1NUmFISTBCMjRWT1BJNTJKczB2RjBsUERMT0lnQ0IvVmN3SWppbzZ0UzlmVUJVczVOaWFqWks2VnBuWTFWN1J6VnErY3ZKS1FGSGZpalFncHZjNWxNM0RjKzVUd2E2Q3ZwM090Ylc5aDVZTVhXVExwa2pqaFduYzFUZmwwWGgvVEx6WEV4UU0xc2cwTXJoMGxudGxpSVhqMGZkVEJJMzk2QUNydXhPU2dsSlRVOGVtRjAvcytmZXJPMWNhKzFiVU1BNDNKVXR3WWRRQzEzMjc2bzJxSkdFTE8zTHR4V0lET2FDeXpHc0M2aHlKTnplVTI4L0JXVld6NDU0VVY5ZXhkaUpjbG5YUFpSdWdhY0FuN1lQUG85LzhOTkdaN1pZcXNiNmxEMEZBN3krTExSN1pxem9Pby9pYmxROExDVmczcWkxcThScmFwVmhXc205YTlxUTc0MGtoSFM1ZmxRUlNMVzVRMW1yRXU0MjY5U0xHWjdKL0UwQ1BJTFQwM1k4MDdSazhORkdpY0VFdE12Zis2SlByMnlhVUFvaUk5QjhnckEycGgzYUMvV3d2d2xqemxjaGZoWEo1WFZPc3M5UkZ5QUNGUEx5UVB2N29FZ0dZRGs5ODdzaUd5eDlBRU4xUmJZRVJJNkpPS1RrN2FyTWpxdWIzeEJGV3ArTE1XdzZuZ2tNWkJmbzl0RUFEaVhJOWY2d2hyb1oweXJSM0dhR1JiakR4Vm9ZTCt5dlZIczkxSXkvZGtvZGJVT0tpRjRpcWRYc0pxZUpLUHBZYldLWUQ2NHEra1ZlblhhUVNGanRtOStLMFBVeCtiQjBrMzBFbHYzalBpUjBFZ2o5WGI1RC9PL2dOeFljODdEcmJRUDZRZmFkRGZZL0JySHM1QlFPY0JNSGZ1eXE3U1ZYY3lSdTROZmJCeGV6RDhaczlwamNZMVpHRm1CODMyNlQrWUxvaW9xcUlmRVVXNlgrS3JsUFdzSXd3UjZscUR6U0EwQnFKdVA1S2Z5VE16UG52bWs3Tld1bldtWlZFNloraXI4d3hDN2g3c0RlRFlLRGRvTm4yeGxXSkVvL3hldXVsQ3N3Z0N4OUt0SjEzdG13YlBJZjVxSjR1NDBrQUVRZFFxS2prOFYwQlEvWEcrNjJJTldMOVpDNGx4Q2U5TlE3UGwzb1RhYm5DU2V1UWMreHU1OXZHMVNIWHduaWE0akFRZ2J4QWwvUlgxcEtYUk4xKy9PQjRxd3c4cHlrdDJhUDUwTTBWTjBqMm5Lc2lROHJac3gyaUV0aWVoQjYyQjE0dEVLZjA4VFNwTVhKNHlhUUhMV21pUXBrN0xmRktnOUEwZDNaam44R1NqWEczN3BtYzQ2VnNPM1MrYWJySFRGVk9WblRLV2pMWmdTOVp0c3l4cm9DKzl4VXJSdWdhUWN3TzNYRjBpNEpzN0tlcVNVSHFaRGV3RU5rSXoyYjJtbk5KdVl2OGhGbVliWWdUS3lRbTQzRm12MDhBbUtGWGN3WWFWNy9uN1JmNTArdUh4RzZlS05YcDVsY2R2OWFYTmc5eDhib1p0KzdvMlZ1ZUEybytrQmtiVmhSNElnRlZuUS9uT0NhbG9sanUya2tiMzFHSHJ0N3ZtQk5HekdGQWZlbHNCSjd3VHlMOGpjNWpEUHFubWJyQnJGYVUxWXlGc0Yyck42MnhuU3JJT2NQTHArbDgzaWd4T3MzOUpuOTBUb1U5a1ZqK2dUVlpwRG1tNTV0QWgwSXdWSGFxNlpkbjgzMHhENVlVQjN6TUxiY0dhNDgvRVdJK1hHWmtsTU45eFhxNjFrUWJmM2syKzJMK2cvdVBhZENiVTZHTExuNTRiRUNPbWd2QUt5RVRDK0FPNTh4TEg3L1hJbWNkRVFTRElTU0V2alNrUE1MbEJzRk82NmI2Zm15REpNakNreUVCWjE4Z29wbU9ibWpCeUNxU0hiSy9yVmJNTE9ZTzV4V2pkU3RSZXpnUUxad043c2lvYk9vQ2NtQUMrblI2eUVtV01vOFJqMmVoNWVjUk5ORnVQeVI1SzhlRm1mc05aSWdqeXprQkEzbGJZQWc4ci81R3FCMnVlN3lYZU5Eb3dMREpUS3AyK3lYcjdPeTZLaUZ2Zko0bTFKM1BIMFY5SFBNckdqd3gxS2Ura04vMFN1QUQ4VHpxR0ZtcHp0UUdVWnZkczE3Um5EMzU5cTl5RnoxS1VSR1d2d3VjY2lxSUluZnZySnVER3pJb05Tb1IrUytFeGtLdFAvU3g2SEROYy9oL3V2TEJqdjlZazRZQkhyamdSMWdwbmlkTGU1NnFianAxbURvbXQ5bmtTMUdkOTl6NEhLMkZoYkVqMmpwWUhCcTRSMFVJLzA4cTdZOE40dWFDTllUMktVeHdmaEh1VjU4MG5qVmtEclZlNndIVHpmQk9JbTlqNkd1SnN6eURWTUd3Uk1mOE03aytFSG5MYWJhM2NEcndlaTgxTGw4RDRQTVhVUjZhc01YWGZXdDdmS1hpUnVaSGd2YTQxNU9aaDN3TnFYQnIxMzg5WEdZTno4NThDNEh3UTR6S1paOHRXRDRNQjgyeFJkQnQ3RkZkQ2JVZlhDbGpFbWNWWkhVSUROUlVnWGYvQTN4TGhDaW5XYnFDSENjSEYxVlFwRVFnb2JNb0UvbGdHZUV5UytvaW1scm9oNWZ0MVc1VWNJRWRDWlB0Rk1HZzBCcGNaUXh0bkRMZVFFUG9WUnRuemE2K1ZFLzBuVnZ6azdJekRFUllRRUZTUS9wc2N1THZVMlFidzlPSS9RbWJDUDRsZkFQVHRndW1aOXVjZ21tZ1VYbUdzRE1oQjZERkRKSnVIUmc0UzVYTk94RzVQNlBVbjJBSThQbDZya094VWxwakJLWVRsb1AxdktSSEZxVFZoUjUwdmhuUTRNajRnWHcwalZ6Slk5bmY3dUlIcHNoU1daRGFrNkY4QjF5VktkdGVxbVFBV1ZacUFhQXlGdFJlazhZaWtjMlUwNG5EbVNMUGNmei9JdGZ4dk9qMktUR3A2akdsT1Vlc2praW42MVdpVFVyc1ZDdDlwS3VUeFBvclN4amF3MWVESFJPd3ROK3E2UzJWSTRONlBkdWFRMllxV2RwNDJTejh2cmNPUHJHNTVVb2lOb28zVUJTcVFYdGEwdy85SnNZeHozeVZ4U3p4Y0hXYXZhNVR6Y3BvN29MUkpjMFdHYzRaR0RKWlpSa0wwY09MaUFhUHhVN05TRjFPakxSSGxZQ0ZJemEvZTdlMk5RaStnL2dVeUQwdFRaSi9nbzF1amEyVG5EaWRnUlhGR2c0emltR3M0WXV2bXVuVDBlRThaV3dhVUt6bjhJVmJ3MldxUkgrb3lMc3dEdVFEanlFMjZINnBQTW5EanQ4cng1WURPc25hWlFXL1kvcDRKUVo3eVpNVmhjbnJvZWwxQXdITU5YUGorclZpUWlYKzhuRzdBSmcyb1lxcGp5b0I1a0VxOVRzcHZFeVQ4K3NMZnQ4SDc0SzFDbDBwaWdMdG9vYm9XemRocmJiczI2RVgwZmp3V01LcHN5NUdITlhwcWpnWi9SeE5vZzE3U202aFUrczdNTjBEZGMwR3pNOENJLzRlUzlYeGs4d3cxblhJNm1hNk9IbCtCTTAzRmIydHpyaHNZODJvVkZQNENCeFBCUDhhOHdwNmVDVzcweFRRNXZzcGdpemdZbldTSTNQMzR6OUZpbStRZHEvUmovaGJZRUxTNEN5Nk9aSDRDbEhpMjlyUkVBbFQ0LzdnZmhsRFBhWktNWHNDUXREa1E0citzb1Y2NTk1K05LeENVTFROMEtOYVNxVEIzUnIvY2toZTJUNHJudkZNZ0M2cHZhbFh5T3BxVUhNdXd5UDcvMDNIS0JFZndvb0hrdnU5S2JCd1V4b3ZSMkxBc1F2RmM1SXFienZlZ1lWdUlSMGZ3cXV2czRwclkvRnlYK0ZyazB6eEY5SG54MjFVbUcxSjh6WDJlbEZSdjJjSDgwdzlPZlhsc3RFczY0OTRNMmhhOEg5MnpyRzBKNXhKYWVMcGZDc1dNb28vdW9HWEgyK2JpaDVOSVo4QWZEVVdIa2JPeGxuQjdad0ZSVjV3RmpPKzduaUs1K2tFUFpobFdUcDN1eU5UQW5PT3FzQmxMK0lwMFFJbEtRMjJjUWVSem5FMVI1ckR2emd1QmlNckoyZFZTeG5TZHZyQUVOY2YyVmlZNldyZXkyTmpSdGlwdWg1L3NKOGxxMG5lVkdqRmk4VnlVZTE1UUlsY2s2b1ZrNi9XUGk3ZUwyaFVYdWkvSnZyVE8zYW9ZTFVLb3VVb2ttc3dsQjR6VStTU0c4b1Z5TjVGNzBxa0V3UGVNTk0rT3NaaDZtNXR0d0xjSmR1UldONHJPbzF1amxTK1ovREVQVU1ZRWZpOW50QWtUNDdUakhtY1hTeFpWR3dqVjgvUGg5KzF3dC9wdHdGU3FZS0NVMk5YRHF1Qk1iSHplL0VVRjRvNGFtME9jYUdianlnS3d2NGVPWW0vTXVLdG1yMlpDcEt1QUUrZDFYaEcySk14K2RBckZkTXFKVkFMc3VWRk8vc3FUbW4rRU9ZVnR0aWVyV0h5ellXRXAweEJ3ak03UU9SeTk0NnBZcGt3Rk8zRjRneFlHYzFQbDgvRGxxOVRrMEJCSjVoeFRSaVVsQjZ1K1Jtc0FxRTlFdmxCYzVIamRYTjhxcVYxUG9kcE1GUi9uK0V1WVVZdEV6VjJGM3VkY2p4TWlCTXhJOFhRa0dGWDlDYnk1dkttQTY4TWYxeGRQbUk0c3QxL1M3aTZqYUw2N2RubFQwMGZxUktpMFpHWE9HaEErK3JjS2I0dXh1OG1RcUNLYmM2K2lpRVcrMHFtMmNlMklsVDRLaVNDUkNYVTlyWEFxTEZrWEZDbDl0ZjVFY2NHcy9odFpDTW16UExnWVdYQ2RyZS8wdjRwM25WSzVqYU9LV2VHZmpxZzJINWdCT1VsV0VBM3Q1eWFsWW8rdE5oUFJtOW1LeUNmR1VJK3F2eC9uQUZEZ2JJVkxOL3V0NWtmN055MHU3eEpLL2lybW9qbVBmdzVRU1RCYXMxdGtxK0lTQ3pwaVF0b2I5QWkxYkh3SUExRzdpTHFXbDIwNE9kVHRONnkrNUN6T3RRcW9GMnZyRXJ6NUVsSEUvSno1V0lHSEJhbXhUNXB6RkxIYUFRaUtrMUwrNFlZMUpYNXZqa0txSkk2TmUwY2R0cW9lYndhSDBpeGUvRXpmRTlEWnZteWNVcHVoWlg0Nk84RldiRWRKNzhWWXcrNzV1Y2NsVUoxa0JqaVloVlk4eEo3YXJvUGtvcUFzeXJRV3JJL3RycG9YSjJ5SlpmaGRvemg0ZHVVTEJqVmNaUXRqMGJNakRjOXdRU0g2cWtWMHJ5SkgvYm04cWNod25zWFlpNDlnMjBoY0hHRkZFYUJ1MjRiM29xM2NkZWlwZGV0QUd3bnFPSjFwU3FybWRnOHJQUTBOYWRlZHpXR2xOVE9TYzJNc29lVzU4TGwyUjQ2SWZONk9NRm9HZW9HRjcycHpKVDlGRDQwWXRXbHFGdVBHTDBtenBRMklzaitWOGRqdjBCWnhld0gzRXB5VDhWbzBuVjFrUmFZaFEvTk5BUDRodmhlZ1ZkQzBKa2hKTE5FR3BSSzdCTEN0aldDaG5ybEFGNjZoQ3d0TlIrREc1SmVKMEZOdVNGcEo1TmI4dUR0Mk9ZYmFqaG9Qclpteis5a0lHNWVJd0J3VzQ0V1ZQWTl3WFFyNXJobXZOWVhDcC9nc2I1MUkraFlqbjRWc1pLOXU4ZElSdjZEc2hraEpyVUVIL0JUS1JRbW5WSlFJbWJvTzdqZUJySFpwOGx6Q0VldnlFMzFTYnhrcU5DUTZ2NjI3S1FySFBLZ2tta1N4aGo4YWRuTzEzcU5XNG13bEp0K0lOdDZBT2NlWmZOYnJYNnpNK0p4ZUtTZDVaMlQzS0M2VE9MMnJCNFBuclFHWG5FYjh6VTByUlNzNURRWFRVUWM0QzE4Yy9OeFVnbzRNVWZHY0N4VmM5WU9FdVBHMEZ4MUxsNURnb2dncjFOZ2N2ZEttT0E1Q1hJU1JCZU9taXFnM3BPK204bVZLUEg0eTRUekkrS2hFQW5OL0x1ZDkvNUNUbjVUbDdDNGpaNGQzS3U1OTlESUZ4NTZGbGUyR1h6RXczQ3U3YUFsck5yenZhMXorb0JGZDRkaFN5algvM1lDVlZkRTdoUWhqdGlMeG4rMlpyM0FRRGNLcDkzM2FhT25HRDVzd1RscW9SajB3YU1BNE1CK1RYTTVRSlJNZzU5d1ZzUVFJa1hKRGh6WlRjTDRkQmUvM2t1WkV3NlpRaFNtR0FEWTRaVzBZQjIrS1NubjlybEdoUHI3a0tJak1leDh2aHhnd3FXQ044RGxSeTZWVVNtR3J2ekNnaC9Oam9sMDZiNjJ6M2hGYlZUZ1ZnWjlBb05jb293OFlCT09nZFRJZktUbWxKTXRTekVqTXp6amRGakgvb1RENWVza0IxTnZhUGZEVy9hbVdWVTB0emJwRmMvSFo2emlJVE1JWkI2d05xZW82S2NweFh5blBwbjVWTU5lMjY1SkZjQzk3U0VVNXpDd3ErUmlDY0xZQnhZd0hFN3YwUlBqRm9wMnFheitzdVFoWWpPd3kzQzlJWmJBVlFLTE84ZW1LeHN5QXM3V25aY1VVTjJNdXRTZzg3Q2F2VS8vT004ZFh4ek84L2pmVzZmdUhYdGhqTGZSaVZNZnhtMWJVZ0hmT1Y3bTJTeGlGbVpFUFRvNE9iV1czdk9JVFhnZmJ5MkRmQmFLbE9vSVRXUmtGQmdJNW1CY2c5RE5La1FGTG9RMFVjMUZEK1JYM0dLU1k3MzJMcjZUditpU3dZZnUvaUI2ZnRlTzlqdVVxODd6eFZBWVlNZlYrWmdPbHF3R3dTNDZXZ1BCa1ZBcVM4RmQyRjJYL3o4Z09yd3VxdXkrT1FXL3BPTUlLTjI1MFQ3OEVsS2pqRDNlVmZMcWxYbGJBYTZudDdDL3BGcFNqYXJTWENGSjRMQndnSVUrdm00cWVYN21aQTdhQUNYL1FoZGJaRGZYZDJCYlZ4VmhFbitCT1c3QXBvcWlOa3Z4d2tvUnl1NStNbHlyaVpRVWZza3FLVm92MzhRNmtpY0tUZ0Q2blFjY05JcVdCMDFsUW1idGdlbERVZkhGTndrcGxGUmRYcGdIa3BqNkhjZHdDT3lrMFh5b05qUDRlcUwzaks0S0VoRGpQNzdzRHl3b2crNENLWGU1bTdiZjhaRjEwcHRPSXM4d21hWDY5Y29qeXdVVGZrUlJQN1RydXRwblgzSXVZOUdsZDkrWDd6YVpjekFudEhqL0hzRVBCblVGdm5XYk5wbWxVOUNQbTFPV1E4Nk43TXBsWjRCOElzWVZiTDNub0wvbGk0czl5OExIaXZSQUtTclNrTjN4QU9SWlhmSHhZYUFEaGpiV2l4bTlycGNmMmR4dlIyY3hzSHE5TlFNQVBNTnpOVjh0WXRQMC9jS28xdjlkNWgwa1VLQ3IxeGpzN05EVGhzdkpESUFMU0VlY09MOTlDSUdmZklKS0xvYi9EVTY1SmJBUGdYb1hTWG54ZjJZSVQzVDZyakI2cXpJdU40OE5tTjUzbVgya0JlTmJTQ25uTlVpV3VjTVYyQzQrUUE4bzBJdjNkOTQrRFNaUDZ2THFkU2lGdDljSjFsYkQzaHo5K0ZSSkdaV0JpVUsxV0owRFRLc2NDcUhWV002M3FmSFZxUndCMGR6Zm5hRUVPUlBwekYyRUJYL0hzNFpuYmZWN2FNVS91YVFMa1UxL010SEZGamNuY01ET1F5ZzZ4R3Q2ZXVDWE1uVGVpZ01RdnFtSFVrZjNLYWVPWk5MakFneDR4dTJNT1dwRzJYVTgrZWh5dk9vNEYrTWdHMVJWZWRHOENRbzlWVXlHS3dCVTZ4cFVHTHh2Z2s3ZitjRUJ4Y0tZY2NINWJ2cnNkNmNhaEQ5cUlYRzUzK3ZDWDlFaUljUXVkd1prdis0ZUVLc0NtTHhHWDlLamc0eGdzc0l0Nm4yMGUxMEQ3WENLaUpVK3ZLcm1RMkROUDRSTmRmVmYzQ1QxZi9XTTh6dXVWaGZiV3gxWHQ5eTdrUS9Pc09JNkppcGh4RTBQRWJyMWlDcklIWVhRRVRieWIzOEVnVlU2TFNzMW1aT3g3VFAwOGcwQXhlUmN5UjA3dHRMdHNzZEl3cmU3dmRlQ1grMTJBY245Y211UXNEVkE4dTd1TUJBV28xY0x4V29nQWd3eDZRdlNwYm1yRUFkWkhNNVJtS0U2NUpJQmdLZ2pjaU0vaUhPOEU1eitGRDFXQmFuamUyVU1WNkRtcDdpdUxqZ0l3emRFVmlzOGJPSFNyVnBESmx3a3pXbnpJOE1EWUlVODkvTlE4V2NqNk9QeUtCY0VFQWgxeUl6cjZ2cml0UndUV2pOeFJaWi9KTXlyR1NyMHZNbnU4Y1JkOEo5RmlVc1FZNjhWN3lPRXY0M1NreUl5MWRWRmNLOEdZaERYUGt3SDV3b0JaTUl3VlF0RUJKZ1M0Mms2aHdtTlhGSlMvVDM5MVMvWGdiUWVVOTNqdkFVS3FTOHk3bG93alpqMHMvUmpLUzh3aDgrazVQeXZEVGVLVGw5dCs0NTg0b3Y4YnpKa3daZDVhZVl4Y1R1WDd1bFBHWmVuYUFHekhtT3Zkd0JyOGxWR2h4dEdtNzI4ZEFjUTh1TGZ6YlVjdjFRcktGOUYwQzBDTURqb0VXYVI0TWg4V1Rld29RdTNkSEdlelZMYUV2UDNWRm1kbEJLQ0oxQVNyVkZaUXFQYTdXVGRkaU5YUVVqb2NiN2syQzJuR1llUWFQNnlzSUFPSkdYTmJFQU1STER0dHJXcFg5WjkzNFowUTNjNW1zaEtDZ0ZpM0xvUnpNT2N5V1FBdlBkaldVd0FkcHZPOGpvSkhDc2hxT2F6Zk1zU1hOVldaZ2JjSEdaNjVqZFZ2WnFkSEhreFoxWFBzWnllb1g5WGtGZlJQd2UvbW1VSFducmJ4Z1RQU2pxNmEybkFhUnVKVXIzUmR5Y3B1aDF0ZHRYUk9mNzFkUDJCb2VJa2JJMk5KaTZKbDQyVkFmaU56ZU5wRUlYV2RPTVB2OVowVlBGK1diSVFicEJ4ZzhMM3JuNFd0bFVuRXlYTkJ0a0lNVzlNWjhRck1iTVNCamhyek03UlZWQVg3MXhaMWd3QmxLWWtaemFNRjkwVzl0aU90OWtmRkFpZkVxbmxHVXZHYzJLeFR1SGdvbXZDaTc1NmViT3pSdXhxVy9kY0diVnN0Qm5uVU5MS2pRd2JnVW1sZ0dZK1VYVWM2cnpvQWt0Rkk2OEtWSUhHbG1CTVlOQWlLRVUxUzMwY2ZZa2FiVXhkL0hwWWFEc011ZURuWktVVytoN09OVVEzbFZzR3JxbnNqb1FpQjRxemhqM1d0NU85WlBBWUc2NXZLOHh5T2FRT0JTKzhFWHJvMlltbWpxbGdUMjJKNHZKZEJvMitQMEM1SFNYTnVMWHV5dHMwL1NGM1ZFZDBJTU1HM0hVOWFpemxLZndjb0xBalJ0S3lwVUpxdXB1Q0Q5RWJkekloaHVxRnhhMzd5bWdWNDlYNHlqVUE1WmcveENHVUROUkV2WHJoUUduRFppTGxNdndnNUZvOHgyVjhiSFA0a2xQZGJoRUhFZVN6T29OR2djNTdQVEJPVE9UL1JxN3YzeXFUWlZZcmRJQ0NCcWNkMWFGNjF5LytORU8veXRkakNDU09weHR2T1hBWFhVZEhXZVNTSDlzSEFZbHpWY2FpcEJleVVhTkRRanRLY0xFczh3L3VGOThJVzQ3bmticWk1bGZYcXdlNml5TW5DTTQ4aE9XemYxdzBqVjYwbnZJd1YwU0FrWURtYTZUOEhNSktaYkljUHZsQnZyTUw4WldtRXI1TzFkWURFVUkyMS9saHFPSlBZYmNyZmd0REl5UDN1MEs1Yi9tTUVIa1REUUkxQ1UxRUhXR0s2U0U4OXBNYWtiVnhGWHlsUWJUaFdQbnZCNDZLVG0yOTVLRWhyTW9kZ1NmZ2YwRVVkQnUzNWdMQkVNdzJETUxTaUU5OGd1bnBsd0FaNDVJYlBhdW5ZblJrY2RvblRTQmRkTHRmaDBZREgza2tKQ1BOMUE3U21SMG9QNWFIajJKSW9NaVRLVGgvOTM2d0NVaXFmb2dlcnlML1lhN3JzKytkakRBTmNqcnZIL2lYZGRjeldTUXZvYmE4d3VQcEd1dGRLRmpTZHZzYmk3NUhZZzJ4VkEyMlN5Rk1YVUlIS0dBcDh6dzd3L0thOU83Z1JieFpyeFlnaWg4NXdOY2NZbjNZb0FIVldnSy9QbmZhaEdscjlwOUwzUlp6b0JsaFR4WUxiK1V5TUNCQWRDMk5PcUllOVpPb1JzbTlXalNLNE9vOXYvVHVtRUJTK0FnaksvVzhJMlNLbXhJOFRwbEVWcE5JV0JqU3VjNWJJSE0rQ0E2c1FzbDVxRW9pcVBZOW43WEEvSW1UWUxaVlZQYzNsb3ZRcUhWOXV5eVNWRktFZERVbTduYzl4b1RMWHRzNGZNV2RWbHI3SkVNZXZwOW9ad01oYzBWbWNKV09HL0tVUjBXWE9qWWVNS0RWSEljZnhmQjNncXNWeWhCTkg5WjJud21McmhQdU9uK1FvaHljc09PY0RuUjIzbVFLNXRBQUlYcTA3WVpHNzJ2VUkyTEUzN2NXRXM5Q2xEZm55d1Y4TXc4anBOZDNZVEJldEYwdC9PcWpLeXFNSXh5eCtkSGcyRTh0TXJyZXRVVlJ1WmM5Q2VNOS9KVDhNZG1SUFJJM2grMThTNXdRcGNIUFlVYVI5QjNNRGh0Zkh2b2xVazFpVy9ydVhvQ1ErT08ySHJTNnRVVFc2bUd6alZIbkY5YWw5UUs4U2hOQU1nNTBwRlA0NlJWcDVPdUM2WGhGYzB6QW83TXpsNjBZS1pVaFp0bjlsbVo5MEwxemliRkdSQUhaRG9ud2kvWHl4aVV5RUwydHdOeUZ4Uy9qTEhLOS9JN0xPTlVZbHNiZmdjanpFM0JYYU4zcDZGMlBYdFZrK3FrR05MM3RBbWdMdXUxeks4TSs4Z1RWWTVwZ1g0SE81bUphSUtzMGRGZ2I0aXR1dG03N1NYd1JPekZlcXYxSENpd2t4NDMxUnovcVYrVlo5QmU0NDFrNkMreDkwcktaZ093S1BoTTNVdTlONTNhY0hZamdvYnlKdFFtUk5oVXBNNjhacjF1UzNHV3UwKzhVVHE1RTBZelN2RjZGcklVOW1xTkpJZm0xMVUzVy9Rd0dyZGtKV3h4NnpibWJHTGdyb1BHWFpsZHpVUndzNEJjZGZJdFBJMWtBYkxReE43cjNlTlF2a3dTdE1kaFFnOHpQb240cS9jQ0JKMFl1c2l3M3NLTmFtSTg1SkQ1NlRRQTBtWXJkRjNxV1g0SnNOUnBCMnBER1VReDhIZlZscEwrcWFDalFXWThKMWo0NGs2cWhpK21LMkFMWHMzSktqZldoa0VlOVc5d3ozdTkyU3dUVjFTZjZkWStBWEs4Vi9CM1VBWEtwSUMrV3l5M0o0MVZmVlRVaXRNN3hWR05pUVExcUNGR0pPMlIrSUxDb1UwdU5pZVgvQ2lTQmdSL0IycWYzTFR3Q3VtVTVUZWdaeTZGT1hvZjc2RXZiZG95T3IyS2E1ZEg5aHpaQmQ0OW55N0xJdWc5eVBHUmFvQTRtRVQ5TExDSzFCSnFzNHptNFcwdzFYd3JrOWgxdHo1UmN4bElrWXV4VzIzRHR1bGM0TGtQdGUzbVEwT1BRTE83b05ieDdRVEtxTHBRa1QrUHJtNk5temNpcWlvZ0dZV21lbXJxV1BSZVpDL3pvSyt5anVvRDl4MjB0VDc3VU9RRmhLQTI1ZUhrWUp4TnE0ck5SQ1NXcldkb0NXSXZXUE9MbDRkSEJSQTRROEliVUlpTmowV3VKRmNMU1BSNW1adlJrN3RxaE5zaEp6S0oyc0V0bUlWN3Uzd29aU25hek9LTVpTVGkwTUxRSXMwZEpBSjJwckZnYkV4eWdYQ3VVUjlLZXJIdytXNVlGUUIvOHIwRjBVMUhYeUk5enNRQ3l3bi8vMlFBQkFCb0FRQUFCTndCQUFBOEJBamNDQUFBQUFBRTBBWjFBQUFJQUNRSUJBUW9DQUFNQUdWUm9hWE1nYVhNZ2IyWWdSbWx1WjJWeUlDZ3lLUT09PC9CREI-CiAgICA8L0JJUj4KICAgIDxCSVI-CiAgICAgICAgPFZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvVmVyc2lvbj4KICAgICAgICA8Q0JFRkZWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L0NCRUZGVmVyc2lvbj4KICAgICAgICA8QklSSW5mbz4KICAgICAgICAgICAgPEludGVncml0eT5mYWxzZTwvSW50ZWdyaXR5PgogICAgICAgIDwvQklSSW5mbz4KICAgICAgICA8QkRCSW5mbz4KICAgICAgICAgICAgPEZvcm1hdD4KICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-TW9zaXA8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgIDxUeXBlPjc8L1R5cGU-CiAgICAgICAgICAgIDwvRm9ybWF0PgogICAgICAgICAgICA8Q3JlYXRpb25EYXRlPjIwMjAtMDEtMDdUMDI6Mzg6NTIuNjcxWjwvQ3JlYXRpb25EYXRlPgogICAgICAgICAgICA8VHlwZT5GaW5nZXI8L1R5cGU-CiAgICAgICAgICAgIDxTdWJ0eXBlPlJpZ2h0IFJpbmdGaW5nZXI8L1N1YnR5cGU-CiAgICAgICAgICAgIDxMZXZlbD5SYXc8L0xldmVsPgogICAgICAgICAgICA8UHVycG9zZT5FbnJvbGw8L1B1cnBvc2U-CiAgICAgICAgICAgIDxRdWFsaXR5PgogICAgICAgICAgICAgICAgPEFsZ29yaXRobT4KICAgICAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPkhNQUM8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgICAgICA8VHlwZT5TSEEtMjU2PC9UeXBlPgogICAgICAgICAgICAgICAgPC9BbGdvcml0aG0-CiAgICAgICAgICAgICAgICA8U2NvcmU-NjQ8L1Njb3JlPgogICAgICAgICAgICA8L1F1YWxpdHk-CiAgICAgICAgPC9CREJJbmZvPgogICAgICAgIDxCREI-UmtsU0FEQXlNQUFBQVlqZUFBRUJBUUFCaU00SDVnd2NEQ0FyQTdjQUFBQUFBQUVqQUVBQUR3SUFRQUlBUUFJRUFBRUI5QUgwQWZRQjlBZ0ZIUUVSQWZnQUFZaGRBQUFBREdwUUlDQU5Db2NLQUFBQUZHWjBlWEJxY0RJZ0FBQUFBR3B3TWlBQUFBQkhhbkF5YUFBQUFCWnBhR1J5QUFBQitBQUFBUkVBQVFjSEFBQUFBQUFQWTI5c2NnRUFBQUFBQUJFQUFBQWFjbVZ6SUFBQUFCSnlaWE5qbWNvQUFabktBQUVBQUFBQUFBQnFjREpqLzAvL1VRQXBBQUFBQUFFUkFBQUIrQUFBQUFBQUFBQUFBQUFCRVFBQUFmZ0FBQUFBQUFBQUFBQUJCd0VCLzJRQUl3QUJRM0psWVhSdmNqb2dTbUZ6VUdWeUlGWmxjbk5wYjI0Z01TNDVNREF1TWY5a0FCQUFBVVZ1WTBsRU9rZFNRbFF3TWY5U0FBd0FBZ0FKQUFVRUJBQUIvMXdBRTBCQVNFaFFTRWhRU0VoUVNFaFFTRWhRLzVBQUNnQUFBQUdIWlFBQi81UGZFeUFVTFBjTUJlVWZGTWhxbU56VHNad3NDdVpoNVN3bEN1T0xpdlN3LzBHR2RmVFk2NlNScHdCTHd6aHFqQXZabG01Q0lOYUFlZGtQOHIzdytza0pzMDc0Rm9DQThVQVJFaG1sbit0YU9GQkwyMkdDaWFFb0lWaHBCb0R4VUNlcGg3dU9LMXBXbnQrZ1g5VVdlNGc0M3FkcGZZRDhDUUJFTk5iQTU2R0oyOVFhRVUzYzhUU29Rd3p5eEM5eGVSS1VZSGRlYlFZN0tycnBYREhIVjRrQUpCcnEwcjJiOEpBSVdaNy9EN1VMdGxpR1NCQWVzZXlDa2k5enVPaFdnRkVGZTd6R24zd0d5TlFqZWpaRUhsN1AvWElqdjF0NzIrY0hONnRBUnNwVXNsenhyYk0waVhaVUxOYXFRTFBZOFhtNml2bVcyYVluNnVFc2RFVWZkMnNUY2pvUTFrVnN0S1JuRDRUaU9LOEpnRDhEOW5ueGJ2cFVNWlk0S1RTRzZWRWgwZmF6MjVaV1NNTEdPVzVxSTlCL25lY3NlQUdvbzJFWnU5S2wySkc0WDgwQVV4NXlleGRWdGR4bFFCOVpoOGlQRHJDclhjeVlKRys1alBLL1ZqRDZPS2RCZzdNWTFPcUEvR2NmR2UvTVRGMXJJN21CSUlhVWY2aFh2MEtBOForcXhRRjMvai9JRnhEWEtJOERZUVhQSERUdnZQS3RUUEhpdTRoc0EraVFtclJHOFkvL0dnSi92ZFhIaG5tcHd4dzBkdkpmV2cyd0hVVlZjMlM0YjJOZWwzNTY2eGlEUFRucjJtUGw1SGw4SmZGUmVueDlLdWJnZWVneVZsWmh0ZUZhTGl4OGo3dVoyc3pUaGJ5ZElmVWdLeXl4N1RCTEVyUWNQSkJXT0wxNHNUMmtucm10bmNIMHJ1OTc3TjFDZW4rUm1DTGlURWYyS2hBMGZQTEVOQjg0bzlsLzFtWnVZK1Jkd0xpSVM5SEhWck9tYUx4MjB4c0dFeEFlQ3M1SjF3dTJqaTJRcVB5Nk1HUjluYmZ1dUU0a3RvQ1d4TmNkYkZaWHRTSDJic3poZ2xBc3JwbU9UY1VsR1lXZzZNNGsyVXdyam13WHVSWE5RajRTYld0R3ZDQ3BnTWQyRGxENDRDT1prMDdNUU92cEpVNlZ0YVJBZ3ZHbXoxVVV6SzFUVXlFRXRFTkwzTDBzRHhmOHJjWHBjaXFXLzFqSm5BU2ZXZHZuNjlwRk1Ka3NmaG84VnNpajhkMUZic3JraGZTN3hrVUh3SnZXRDBEMW15blhIRkZJUk9keE5GbUJleDJwenZFK1UvMFlJZjlsdWlGQ29VMFdpaENWS1dZUFpManJ0cWg2MUpFdmQ5RzNVMTdQUXZSWS96djlzc0JwLzJKNFVJMWk5QXNPVi9Pc0VXMlVZMlJ6bkJZaGtTcnZEZ3FYSVd4UFRvNi9GOTBBUVkzbGt3S1Q2ZUV2bEw3UU5FT1BvU3VTNEg5bW1IMWN2SFNoOXk2Z2hqN1I1SkdKcnF1L0JEYTUxNERYRTNyZ00waml2Tlk0UXMxSzk3SzFpVllqNUhGV0FTRmI3SzRUNnVUUlhHOE5qY1JuakRZUVdHdVZDZno2SCtwUURFUU5sZE1zUENpK2krSVhCeFhwVEZCODc5MnVJKzNldFdhUGJnM09QSEtlcFoyNTZWSDJxZnhYY1kwTXJxN0ZWSC9MN1d4WXN5b0h2NkRKQnJDQzZ6b0xxdnhRQnJSL3BMaFMyZnFIeXBkdkx5aEhuRDc4SXNqcmF5Wkx5djlVcHVKT2M5M3UwTW5kTjNBOUlPNmV6ZndTVGU4cjF4Vng4bkxFSXJoRW9mQ3hzb1RkdXhDOHU1R3hMNjhvQi9vTXMzZTMyd1dLTTZDUG10ZmdDQXRtVnhkVW9CLzc1YUwwcFo1ZFJnK3hsRERDaVFCU2txRG9OU2dBMTkrbi9LbXpMK1FYbjBXckYrVEFLSWsvYVRuN2hMeWwyc2JBSUY2SjErd2JvMHpmSjgvOHlTZk1sUHpwb05mREtVQVhoSzdIa0Y0VEZSbXhsQWQwRjhrdE5ubVVqMGpJbWthK3lCT041ckVoUFEyZDhMYS9QbzFHSFVCZDc1aUhmQ0E5NFE1dkIraldFUy9IQUg0OWdrc1VZejM1UFRxUVR4UFFIcklVZ2hlWm43ZHZlNHlFMlhSMThyVDhmRVpnY3B4NTlic2JWRFhwUlNKSUFGTDcyNkxRMnlDVlByRFdLREFLdmZyMXhWMkdiWDNJR3RmdDJ3Tnkzam9Jd1A4YmZYZ2hucld5bzFlSTBTeFFDM2FWakFQNGw3dE40Nlg0RW1od3Y2OFk1bTRpVXpha2VxUEMrUDhaZFc0U0pCMGJxRHlEM05xb3NDK2ZZOUlCeWNYUFJLSHJJMk55MWZSaGNPd0Z1WXNML3VUZ2xLK1RTTWFvRGZCdFlHSUp0YjFyQ2wvNmwxVFE5bkVoR1pwQy9KTlNJd2lPTVpLV3ZwOS9BR1k2b3pOSFo1V0pZQ1JtVHYxeExJWTZBRXpiNHpGbzJCOEhFZmV3NGFJOTRHU09mNWhaWmhrVWlySklMMmdpc0FCMmIyOWRWeGhQQTBxTHU0NlNWcVVSTk9obUZJMnZvM2dtUFlURlNtSGNTUndYdVl5dTV3QWkzZDduS1o2Y1FUNjQ2SElKa1BJM09zWkRmYWQ3WkFLZDcyZ3N0TVVxRUY4OWNGM0NCdzEvVGgwbGhUdEZxdG56eFlzRmpaQjdKT2tSbkJNZTFNUUQwVDUxc2RIOEhSMVYxbVRuVVRxejlYaGlRV0Izd0xtWnc5UUFlOVhlbUNFSllnMGI4NE5wOTRseUdEQzBJRzdPcVVHdGFseE1HL214WHZqbjZoVVRyVjVvWGdvdk9rOUZUa0tEZEVPZnlHemJ1dHcwNVRBUlVKbmduTmNYZDZQNkVnNHB3NkNsNDNiKzNMaFpLWnhSZno2Q2drSnBhQlYrSldBTTAvWHZDZzBzdE80UitmYm90NGUvL0NGU1o1UUxXVVBWR2o5YTN2Y21SQk1LcnhEdnBxUWRmNStPN3RTbTdIODl5OWtHenduUzBxVWQ2aDNTM3hiZDIyckpGWXpNS1QyQmVCQXBabCtQR3dxcVVTN012Vk0waEdwbHdqT2JLaHhmeXhDaGNrcGVqd3lMank1cy9HTGJUdGJDRjJxdm1PZVRZcXRsYmdpWVpZbkZzaFlsb2kyUWx3YVk3ZWlXTjRxUzF5ZVN2SlVldWgwNzBuNUFZbUdlYVRtOG1EQ3g1T2lLd1ByQzBMN2pEcVN6UHpSeXVjS1Q3cjJTNDRVNld1bEdnWlF2OG9ERDljTlFWMDI0aUR3ZElSdHp1UlZlNUdrSGdFNGRRdUxVenlaVXh6SEZJdHNBOGtYbEZVTTJNREhjYTRIekNycVdwaHo3U2F5TFR6Uk43L2dhQnV6ako0UmhMRzEva2NUOU1tY0lMQ21CZUZNeEtMek5qTDhlQTZEL2NtN1dGSlZpK1V5ZlpqVTF5M1lVcXhUMUsvZVBVK0UxTFZWbHU3VnAzZWxEVWViamI5TDhXSDJXQk1BSVNCT0RMOXVUZWRFWVlzVGVYMGViRS92QklSUXQ3OHZYd0pSZ1JlbytUYzl6YkYzL2RveFV2VUpJNXBPb3ZablY2bkZ0OXBYeEtrZmxEWndGSUllSTlBUEJyNlE1c2QvdElBcFh1cXRpQ1dVNWZXdUtFdjROS2puRnRSZ285akFMUDRwR0YzUUkrVXV1TkE0RUloQzJGSWV1eDkrNjRnTTZwZW9nVU45Mmw5R0FBK1F6dWo2TE55MmhjdmtmSTFMa1lOTk5MaFdUS01xTU9qRENIR0QwYlNycE9JbzhVeE1kYzdvd2l3YkE0bVF0M0NtSy8xd1cvdUFiRG8rWkxSSEliaktYUmk4bEIrU3htMUNteTNrSXEvUlJ3WmJIaVZkNzM2aldldjdMT2VSaGNnOHQyYWRrc2xnUWp4YXN4by9WNUNoa2hzVXJJby9vcVBIYmN5TjZEb2ZWWUg2NmlXdHRVZ1F1OFNWL2lkSzlHelV4N1l0Mk5OSHoyc1lsNDUxcjVZUzZKekZzTnFHcmduaUs4U1ViZVNwaG9XeFJCVDJiV2tlSDZaZ2pKUElncWRxRnFJWkluUytjc05pR1dXVEhXYTBYQko3cW0vRVRROFNpbFZ0V3gxNWFLTFg2TzNVWUNVdzg0Q2I3S0lLYi9QSThxaUlTR2MyRTFlRFpGZ2k1VlhlUkxxdURESEkzZWpCWnZrRGhDZHVJWGhaSzY5ckhMaTA1ZXhXZEhwRjByRzNjKzZpSTA4b2tuZGNpTzhHSnRZRFlZbkRZaVpHbjBDOUo5a0thVzVsV0FMSDBNMDNmRXM0TVluL1VmWWVNQVRrSkVkSmhWb2RaY0dJRmRFSFJKU05uak84VC9yV05EOHFUUHFpQWJ6QnR6R2ZjRHdxR3hQeThnK3JVaTMzT1hjVlBzdXQ2NkRWWlRHM2VkVGYwU0ordmF1RFpVNzAycjlwNXBnZms3cFYwVnlXZFJWSFh0V3ZYYUlkdmIwV1NZdzFmS0lWT3lKM3FjUWxVMXoyS3dsRENCZ3VDTlh0RDFsT0NIaEVaOHpYQlMwL0EzT1M5eVZyZ3F0RVR5QU5hM3N6WS92Y3pYcE9XaFpTajh5SU1MbE9hRzQ4eXcyV0NSVkdXc3hYYTJwYis4QXZHcDBWYjdGQlN5UWp3ZlE3SHJnTkdvdXM2WG5BZWJmcVpkd3JwUnZuM2IvYWJObXYvWkNNMnVDZlhyemxMUVpYQ3NaM3N5anVXSklQeUN4UGJma3k2eEJiNVZoYWUzSGkxcDUvbVc0NGlQdmxIMzIvTDFib2ZNQ1VkNlFCY3ZWeVphdmtZRFJXd1RLbldoUnlscU0yb2p1S0p1TnV3UjlVbFhkZXdKSHpubmhHb2NtbXlCNDhkTEN0Wm9COFd4bCtDV3BWMmIvbGMxQjQ0cnNleWdzMnpyclVBVnhSOWsyLzdkejFTRUhia3Y0SlFvNHVwMlJzNnl1d3pDemFwNTZHVkZCZFV2VE5MS0M2eUdIQVhnV29vTC9vR1o5eGJvUTJDWGFsaUdoNHYrR3pDa3U5RU8xVjJ6a1JMT1h2a3lENUw1SEhjVm4zc0JIVHREN0Z3UjBrbXczdlZTVVRBWldxazlWdzhYUFN4WU05N1g5SzBNNlhBSUlrZk9hdzZvbXM1eldsZ2FMOXRBdDhqb0JTR3BRMWdzWGp5YWlPdjJ2YUl2bjdRbXFyajRJRm5GSWlmOURxUXZlNnZHOHNhdWRNUWcyYnJISVVnSWhXaU5ZSHlrVkJLbnh0T0lMelJKN2ZPMmI2OHExK0FCalp4alpVdU1QbVgvcFRZa211Nmc3TUhiSWdiRUZNN3NYMHdsZG1lYmlSMWVpRHVuODNtRGNDdjY5N005R3BQOUh6RlVvR2pqRDZUSWFKMGRTdHJmOTVpVFZjNVlrOGRtS29RMUlPbkI1eXExTVlwaVZmMDNjb2d5amtKUHhWMjZYOHJiWlplVS96Wm1hVGM5NGwwVWo0dUd1dDZJUzQyZHcwUm0wSi9uV2c0aFo4VUxVWXdFNkQ1SE9YSEdtQWRZVjYwQWpHdlI0RnNML2Rsckg2bWhyZFRPQWEzR2hibHFIV2RpcXhsVzVGakhlbjc4Rmsyb043S0ZDRk9jdWM2L25BWTM3eVFUKzZQN3FIazNobk1PaVFOemNzQVN0ZWovcEZTcVd6ZDFTYk9JNGg1Wjl5YzRhOEwrT29Gb2ptQXpiTE9Vb2tFVUkxQmp3UVVsQm5vR29wQU8vYWpXMiszdEZoUEUwbmRsSDJWYjVJUENzaXlWdU4yNWhrbkQzWldzaEFscTJGUE1vY2IxSkU3YmFlTEd0K1VtTDBHeUVXY2dnT2pPclpVeno2dnlwTUJ3YUFzQ2pVMXZlNTUvblNoRXhtdlFZblVrcG9ybEF0WWIxSFZqQ3c2TXRISGR3a3Ryd1VzZ2U1MXZERGkwNmxLUWRIMFZ4bVdacjVKS2lmSUlEUEJJcjdML0d1SGc0MFpjMHBqbFhFSmN6WFA2NnpodUxKZkF4anNPQ0VhYTRRdGZmV2FqSXRiTFcwaUxEMThhOFFEMWRHdmNyTlQ5ck9VR3c1amRGbTgvdXNid203K055SFhEOTB2ZVVZVWNTVU9wdjJVWXRwc0U3SlIwMm9iajg2TWhGd0lvL3ZoaDMyNDFFK1V0cUpjbGd3YTlJcERxYmUyV0FLMTNraXVHQWdQMzlmSHMwWGd0M2cycDQxdGRQaXJweXJrSnliQk9RTFZzMytFSExmN1RJNEp4S1NPUFJZZnlja3JXeUNmbS9icERGemxTN2E4WEFjMHd6OVpqWDQ1Tjd5U0VnSVdLUkJYQXEwMWUvWUd0MC85WW9yTGVlempKQnRTQ3Brd2xqOWVCRzNuZnc3b0IxN3RPUFFpaUhLaExSMS80UmVnRTZJdWtFMkJTelY3cEw3MVRvV2JqTS9PeG1nT3VXclY3NzAzcE04VDNiNkRZQ3Frd0VSbTFxaWwzVGxDY1BjTFhSeU81ai95Nmkvc1Q2OURHdTN2M1FyekR4WDRDd0c2TzNoNjI2MTNJSTVDeFNuemFQOHFCZkxwNEhwaHgyTDVDbituKzdTeDBVLy9idUZIeEFJQkE1QlBSRGVtV3J6d0lnYzJQSjQvUlk5UC8zMnN2eGtCbzF4RVNGS1liV3FkRzFxc3EySVhVZFAzTnBNQXp1bzN0ZWZncEVvL2FXaXhyL2J6Tk5HdFJQSDdteTJsZTE1alJLTDErRG8wVXNQTEg1ZWRnTVcwNUJOOHpNYXMzeW9UaTBOVm96VzE3WStCVGRFMC9VZEVEZDIvM055Y3dnTWwzKzA3WGJ1MXNSOFZtS1JqcWpHMUxsNzRLUW9HVVlQa0ZvQ2tWSDlaTDFlVnZldjhvWnQvMkZ6eXZaOFFmOW94bkRLc0MvSVhtelBTS0lycTc4eGcxbFcvUFhPU2IwdzhkRytOa2VBcXl1TWdEKzlGUi9OU2RPVVN4UEtVUG1LZ1VPVXlrTkI2TXlJVFQyeHVXOEVDZG80WmMyVjc0WGJkcDFsZGUxYmpHellrSGJhZ1lxTUpCYnZQZzhhc2x1Z2Q4RnNxblV4cTdveDNGK0lRcXY1VkpSTno2RVBqS1RIQUlhc01YZnN6U2pySzE5cWxGcnNpaFFRQUl6bzJPS0p1Z0hRTTN2ejlOWWdJdWFNeFZNbFNaR0xhcGk5YzkvS0tuclg5Q1hIMEp4ZldXMkJSdEdHcndsSEdTd29keW8vTmMzMEFDdFV0L05jakNIMThCREo1eStCTmdNdGREZnhoYlF2cFBseS9NbW02RE1mdnRleDBySjJpZHlaM2NZRy9YMG1zaUtmWVhaOW9ENldwQTkvZmV0cDcwYkw3K1hrUVVVQXRBYjNHMlFFeWc1NW9TaUVYdVdsWnNUdEF5LzdYUTJGQ3JETDhTWjUwbC95WkxaUjJDMGFMVHJNRmYxSU5pZ1NsTy9GRVkrU3BaYkN3N3hDQjBHTWxBVGFCOXRldmhKS3hRWTc3eVRjbW50VU91eFlWRzhQT24xcXgyWHBxZi9SNitRNEJ3NTE4anFlVExkWTNpb1dOZHR1MTlSbW5mRGc4eFkyWDhZOXcwdEIyQ0JWaVgwb2ZSbXljbXlDVmRScXhEQ08yNTlhYUFQRjBUTGk1L3dQYldjMXZscm94WWRUdUVrSG9YVEEwVWdLNy9wTFZPLzZsUGMydGxpTnpwS3UxY2czZTF3MkJCVXk4TTdjVUk1M0V2cHVEdER0U3dKVVhaaTVya0pzYmk5cTZseEVqSjdVSXNQcTVlWU0rNXJCU2VOM3gzVWI5RDFkZjU3a3F0SFdaa1lNeWVadzlDSGdMVSt5WXhEc01vZENLOG1qN1F1S1ltS0hQNTZCMEE2cjcxZGl5a3Irb0grcWY4bnhTUXFYNEZTKzV2QjJvR056V0NhZjhSVkZNcnd5OVIyMTgwdTRqUHJqSVdIRHVoQzhyZlllZG1nNzNyV3A4OXI0VEdoeVUvM0NPUUhVamdtdGMvM1FJVklTK3ZzQ3FmU2c2WEJLOElBQnpVUG85RXk4R1c1a3lwcDd4NnR4dTB3SzdNdXBoMWlLV3N4YmNTZFVkNjRMT3U0ZzhMNXF1R1lPQU94MWVtWGVEcExMM09JYnZ5WmVhMEhtTmI0Sy9ldUtEUzNzODVEQnJ5cE53M0tpZkVidE1tTUh5QlRhU0RlajRqNExYU0xRVG1KZ2tDcTAvSFMweHFLMGJlY3Q2aGtzQjMvV2NUbzhZMngvcmprSEE2dXgyOTQxTHBpMnRicXZQdEwyYW5UY0wyc2ZPL1NEOWdFMjY2Z0xyajBlTmpxZzFWdng0OUZVdHBRc3ptRm5RbVdOU0syVWpXZ3JhTjYyK1E0WENsMmpadTQ4UXQ5VnZaN1RDUytGM2RTcEVBekdndVVWRGw4T0hNTmNuckc5cHJUZ1ZiMWRKZDh6YmlXZlltdEdpMHhvYmFmZ1VVVm9WUnBvdDdURXduam5RN3dGNGhzL21pOTBaVktrWnNsbTJPbzJ6U1NoaFpwZDE3N1lsdGJQK0J2Q0RZdmFqTG1iV1h5UnhQaFBDQi95WGwySWJyeCsrekllMkw0bDU5OStFcmF1cjVtckFIZTNUWFYzY1BickYxZnVUSURIdzRvelZOR09RS3JvNUlGU05WaS9HQnhyWDZpYThBNXpuMjJkVU1QNklhY3k4ZG85K1VCckoxMzRoK0t2ZTBhd2NydE1VWVlkbURORGMrcEpveTBSNk1MTnI1VTJYS2k0dG1IU3lsZFhtcUpKa21xdllyZ0tqUEkvVW16d1B0Ry91cEFocnBJUFhzRDZmNUdaNXB2SXZFSUJaclRZMjVNcWlaOGVlNEtTWjhDV2lXdDQwMm9jdmlRc3JaMFhOUWU5MEFCM2FNdExmZ004c0o5NmI5WmREQ2ttd0ErVjF0VDhhUTFYWkFhVVFtUWw4ODI1VjJqTUwwQWZSVkNjTXZSTFZpK2x1MjEwd2lwQ2VLcUJDaEpQYVVpS2xZeEtSNytXTWRqSHp1eGNoVEtoZWpxN29rMTU0c2JlM21yVGsreEFEY3NoTDZJNnF5dFdqYVJ0d2RpaG5PS3ptVFZ6enVJNUhtemN0dURpVitvQkprbUZlMEtQVWtmQXVWOVlBVHdDK3RscU1BY2J1OCtxZFRzeHpYTisrYlhvNHY5WkdZQ2FQeUc4Nm01S3pkek0xdHRIcUxUOTd5b0xicEhlUTd5a3Y3ZWVaRGhoVkhiSGdqY2hMS085em43eU5rUWlPdkZ1OXdTSWg0TVQ1RU5rU0VjZUw0cEJPOWxnKzhnRnQ0VnVKWTh3QXd1SkhXUms1SnpDbFNQWmxXTzlBWjRndVYzSm01Zkl2YWtTdkhUV3hldGtWbldLSm1PT295YTZpVXVYNXhtYWFoVkJSTEQ1R3o5eWRhaGFoandsbVJnSnl1OXRKZWdWUHFvbThRWUF0aDJLK2Q0U0h3TWtubitvNlY5eDhWblFDMk9QejdGOURENHpKMmRNQjVUeTdwRVNEMDM1UVdPNnZZTmkzS251eis4VDBvNHJKUDFJakZmbGFucTFTY0dhWERrRjRKTjREbC83Z1JiZW1oRGlDSnY4MUNDSzVhSG9GUWY2OG9pRGZhaklvQ3R1b0RhU0huL25HUlUxM29XNE1xZDkvamJ1OVhJOEJoYmJzdE5mT1hiQmxzb0RjSmNGZTFPMDRSZnZQeHVmZlpMZ1JMZE9OTU1jZlF2R25BbjJGMzZGNm5ISmI5NXV0Z2NnRStzaDlNVEJnRUhvZDRkSVB3VlZ0OWpUeC9xMmlOWVdyNmpGNWlKYWlhdHJZb1FycmNybnVyODJ6SGo5UUpmdS9Fb053MWdqRWNld1djSU5JcUpqZzlieXRNZC9wVnhHNUNuMnJSUnJEVHRCSTBTWHphQW1EZUxHMlZ1RFRzejE4NldiSGRVR2xXNG9meUErbkUwNmlsQ2VYTFJHbllraFRHOUpLcjZPZ1AvYmFwWWNtallnT2FFZnJGSHJRYjFORlUrOUF2RlNOeXdTSUNvVzRHQyttTHY1UU4xbFA0MjhONVk1bVFKUXFhaUo3UmU2SThPU01WU283MUExVXhqaitvdjhnZ1VtS2RQNnZFeDNkRk1NdVl5cHhXMUFZQno1SGVId2hOaVFmMHR0SUhLS2ZOSkxpckN4dkRqdzNEWnRGTW5pWVJ6T0o5NUw4MWhBc0VaREdNZ1RZQ1d0eXRKZ3JMZHJIZ0JzY3ovQ0NmVk16eG9VU1F2ekRQb000SDVTdWVSVjBoUGI4ZWU4bzJobjA0VVBLTE9Oc0pHdEpHTW1jcldPRXZkTzBtQVFVREdIYkNndWVZTXFQWHFIN25iRFA3c3hoMHcrU0V2RVVNT0c1MDVGSzdLUmVSNWhlaE9BSEFsUnBtR3ZVd1MyUGk4T21yaUNIN3hJTTNkMjQwOUdGemxYRmtpaDB3M2RqUW1QT0Vma3J4ajBJMFZzZWd2bllYek9sc200UHJyeW14SUJZSWFvbVI5NS9pRGZQWGt0NXhxeHFOeTZSL1dVRTFKV2NIN21OOWJ2V3pkeWp4OC9QODZsbkIzbm5kRFljaGlZVC9wWmhaQmJRL1lLTTM1T1UzUkdJNVFDNmY1MFZkMFVOSTBybjkzRkhJRTROdU5FY3k3bFZzcVk5dlk5WTdyaDBhOXZOZjNGaFNpWWlGQ0ZaMlVqVlFUZFd1ckRmSkwwL1oxUjFvbEJXcVJEOU1IZEJ5VXdjWE8zdHUxSENwWU1XQmd4U2tmRXM0Wk1SSmg5T3lNeVdCVHRWVHlYaERpbm9qWU5nenpTc2xBQkhQWmJIQ0JEcEVFaWxuMHJveEppR1E5NVphelp3alNBZEdVL3lSQU1kdS9zL2dZSFlGcVdEbmRES1IrUUVhaGFDUVRvaEt1NTNWN0hwMmtmdkhSU3NQam5QTkFBWlREWVlOSnRISXBNaUVqWjVoN3hhL0ZvL1o2dWNnSnVOc0tSdEQwUXBQRE5abGNyL1huZGMvZ21qcFN3Vi9NR2o4VkJFTVpYb0NLSllnTDQxd2JGM2FKN0VHK2hmUGowaUlyeXpSZDFlVjhRMmsybFRWeUYwY2hGWkV3YnptWHNtMHViWi9RRjkrU1JKZlkxQmt0clRGKzl5dmtmS3pESE9vcFNsNFZVM2ZWbzZsbVZudXNQdGRyRmNnN3NWa3hIeWMvU3NMbzJxUElCZm5HWnpnWHMvSTRwV0YraWVNWmFIVUhFZUJwd2xiZEl1cXorNHFDWU1aR2N3OHFWeXRFbzFmT1RQZ1hWelc1NHJoeWd5RVl2WWhMbVlIU1lxNi9QRFhpZGtDQy9TRHRCbHVvYzR1dEh0R3YxSmFaYnhjVTlKejZSMUUrRzg3UnNhTUZreFhJUFg3L01CbTF0YW10c2FxVG9zMnBMOWttT2pIQXV3L0pIL1Fqc0pmRmRWWDJnYnV2WFdkbEZWclRpUGswSkVlQWtHYnlHTm1YMm5YMm4rZ0Zvb1hhL2FhVnNTQmxmL0paZlptdXVPemRrbGxBSDVpYisrNXcyWnQ0cVNWRHMveWRoa1lVOEVmVGl4aFNqaFJyVVhjcHB0bVROMFBkbWJmQTE4RG11cTBkS2l3enM4TG1wL3k2WGNicnJqZTY4eHFybFBuRTJaRW5jMndsRS9MUUErc1EwcDlVZXhQN2lrSUF0SmtYRHJyTjE3dnljbnQrZEFSY053VWQvenFTVXZtWmpnMDBLQjQzNEJCS1J4NVE2anpTTnBobG9ja2p3bDJyUDNCYlhRZWxRcmljUFpOd3d5Zno5L1dIN1M5dzAxQllIckk3Ym16M1VPUGVjWUpyeXRTT2R2RTJjYlo4aTJ2VjROYjIzT2c0eVZMTGM4VHVGSXhaeGJ5MWpGK2E1NnJzdVpIRlFxUGs1Smw2bWlyWXU5ZVM3YXdJayt3TG53YW4raWI1dzFzUFFaL3FQSFdSdk16bk0wUHVoa3ZuK3laWW4yc3dZaVJRMGJMcUtUUlBZRGVVdzEyMjRPOFg3MEJHTkJMVGhxN043Tjk2cVp1SXBTa2F1UjJTRmJPNitEZ2t3N2hhZXI2QW93bzdhYkFDMUlmeUhBZ1hNMFJtdW9QREw5S0QrSlZab00vbUQyQnZ0SlptelEyOGI0eThmejdYV2ZrRXZMa2gxb3JCMVM0ZWQ4ZXhpVjFPenErc1JJcUhCRml3MEdqZ2tiaTV0OUY3d3BEZmZ4VWVHRGt1VnVaaXBXTGh6Z3A1L09yejlCNmpKYXJiQU1sa0lZZnduWWdnWXdOSldpMjNoWFJ5ZzZlRUs1VjhqZFFIR2VzaE5TRkg1azJMQ1FlQ2pwRm5kQWc4YVBwdkVKaW1lMDR0SElIamdCaVlITnhPZXFQRlMvWDFLSnJqOTc0aWhHM21Ec0V3RitVbnFKdHFsTStLTHF1bjZabmVjU2ExeEJ5dUJHRlBOUllFTHhpdlRvbzdEVkY4cTBTYnFUMXlFaFhMd1JqLzZuNTliOXdBYjJzWEh4ZWhsR003Vk1QZUhmcGZVUjgvU2NJSk10OCt6NmRPWjBaamtOOW9YN0c3amNnaGlEV28veGIweWNZUnFNOTMzQXBYYVVkbUxQcWIzeURBbktFMCtQRngyTjJoYk9KTTNNc0hZRXlkUEVXZ2tjLzF5ZCt0NHZjQW5TNjgyZXJId2Y2RUEveGUvSW1KZFNSVjZaZVNRK0RoemtiRDlEdU80K0VaNDByK1RLZHFldVRzMENhd1A3NmhybDBiYWk5bm5WcWs3UU9ZZmJ3Y1RlVHIvWXUwVjZrQkUzbEhyRnM0K2ZGeG0rT2VFNURtbStTcXBIdFVxZEEyU3hIbjlYWVBJS2hNYW00L21ZcGIyc3VUTHVvTWtGcXBKb3R4TjZlME5xYlQyNGZKRnVLeVVpUkU0eWFzMHpJcFVrUGtNUUVpbVhZeFRIUCtZYUxGcGo3b2J2TFpMVnBUNlFaY1NsUkgyNEtDUUg3Y3dPcWU1SVBCa2tzZjhQL1RLdjQyZm9YMS9NcHN0SUZjMGI5MkpOZytPNVZ3ZTB0QlBpd0YwV0hweWcwdk1HWnVpcjBvckV3b1VHUWkxaG5PU3ltYm1hZ1hDSjlOMlB0UDYwWWdsbXMrZmFORlhacnlkMGdWRDlNeUtlSklDejB6TXY1RmhhUXIyTjdzcFN0ZDRyS1F3cEdscCtkLzMxT1crMXdzb3h6ZEZHMlFwK3M5OHlUbTF5ZTlmS2xjdmVwWjVTaDlmR1JsS2NVamdwY0VDZlZzTmxNZTJBUmZGU0JQVWg4SkZ5c2xJUmF2bmdaa3FkV2F1SHRnOHZIcFdybTAzSjBYcVhZeUx3MTQzRmJkcDdNSENmSUE5SGMrUU11SXZaeFRUNmFrWW5wRUJSeGZIdHZFZXZkZUI4QldJL3pZK0Z2TDRRSnFGNjBIVW45ZkxXQSthb203R1JoVHR5VHByd1A4c0xBNTZ4dktyaXRGajdUYVlpSCtzelVwcW1PcmxkYVo2REZaVStWZzREUDcxRjAvSEI1bVMvdytuMDhEY1V0ejI4eUJZbTJFNXZKbUtkbXgzRU1DcE50UVlvMnJzczIyV1lZdEFhd016eDBjRFZ1Z20vdFUxSmx4SC9ia0lUMTl6TTR2bEhra1FwcXlESUUxWHZCK1B6ZjlyV3VoQ2J0TnVkNUpmcm5RNUFzQjNGSlNON3BmaUxjWitidnQ1eUxJaTQxZUN4L2hIMGVWSkwrTTdXMC9hT0JBbHBpY3JsQytSNS9aYlBhdTdWKzBSbmlXSEJrU2V4N3pDWkFvbktoZ1RMbVhsUmZpNmdPMUZFZG9mVE9oVG5qNUFqMXN6SExFdjRIU1RwS2VoWGpmRWRGZ2kzRXorNjg5bDF5TWdIV3lGaGgvQkxWaFVsdzJHZ2tRRlNsQ05LbVBmOW9NMUhEZjRjajdKWFFCdVRTaVIrQjBBUTgzTFJPVGs2aThRRWRkL2JBVXFDREJzOHJqZ1hsNnlOOEtQN0JQaG9HVUNhVDl1UnhLNU5xZGVoQk0xaFJ6VEt5MHlCeHZ6czNDRWZHK2srWHVJNHNXeGREalZBVDNIV1FybExwTGZiNDFlZmdESEswNmxQWDVmOFJIc0tqMzRhdlZ5K0JjV2swNVJxWGJXS240Q0FnSURubjFRQTBkcDhiZ1VoeGxTeUFHNHBMb3JNOHRPSlQ3K0l6OVNsQXhFSGFRWk1mMElWVFNhOGNKanJvb0tuUDFZc2tsbVNjcmdVdEpwc1ZwUTVTV2FvV080bnFvV1lFK2ZzWFJzaEJpQ1dPT0JrODJrSm1hSzBHTVozUDNiRTJQVFAxQzIvRDNJNCsxcVBOT2hNTnExOFFxc3FDcy9hZHp0emZwdzNGd1NBSzhjMHdXRHhUbE84Mld1NThUdmpYc2lpdjJmVXVhQjNJcUJ0akVvcjZqRTNqZDlGUGtHdmR3WmN0Tm5ISGozK0RBSkRoOERFemJFUGh3YUV2VVdrYUNaTXlvaW5tVWN5Wkd2WG55U09rSHBURUZpK2lzY0tHQ0dRdmd0djdTMDVEWXlVbVBPVFVZWGxVVnRNUFE4QVBLekxDMGdPdllIbzlFdDROUGZiYXo3MDJkazNZWno5YnNwVkVqbXkyOVRvTDRLSHJ1Zm0zZElKK2NlWkNpL2ZLSEZWdC84R1crQUZPekJ0YVdYQTJHYiszWlFJRTd1QzNIYUp2b3AvNE9PcWk5a2xxcEpCRXR4enVCU1k1eUxCVEJYelB2MXpobm5vQnUxQWhVNGU1ZmtBbWgwQk9jNFppa3dSTkZMeEhsZTNjdUZEc0NJUXpxcWVnWCtYcXpFTzlXNUVadytjREUvZ05rM3FGclpnY0h4dCt4RzFwdmdRZThlejQzWEV3MkZkWlkxVlpRaGMyS2owSm1TUU5KY1Y5WDV0Z1BnQXY0cFVQdW40NkhFQUJXT2p3ZTFJRGRoYjExN2NoUHg4MEhVYnBWTTRHUlFqOGY5S3JENGNVM2xjZGFTSzRubzR5a1pNRnp1eVFqWHRLTTdVWk01d2tJRkZxU1hWTSsxMVFoa2x0eWJXOEdDQTVONFRrNVg5U3RuWmhNMjN2ZWF2aHpYTXdGeWtkNzlYanBEeGNNeE9sVjA0SEdNUktQNUJCUDRzczVrTHorNUJZM2ZISkxlOEl5VnJOQ1pXc21HZUgwMy9YVFpPU1M0Sno5cURpS3ArRHpWd2VGQ3JkTURLVkZWOGZ0UWptQnBBUFY4UnovSmZoOHBVYnMvTFBoeGpxNU5aMXFkVXpVMmdvVHkxZDFlc3pFSjBZWlZPR1BzeXFHZW4rY2hvWnpsbVNWWTNJMXpuMDd6VFk1UXZFdWVYNEx0QXVaS1F1cFBzaExoWFlxeHRyMDhSbEpDT0Z1ZHg0MWcxMEVJOThsRmw3Vy8xU1UrMmhRWGZLOGl0STZnSGxTb0tRVGpoZ1E3Njh1WHVVMmhJYXhsUmg0ZHNxVzBrK01Ld0t5SU8zbDZWMTJRaGpEVXhKWmh1S0N2cVVYMFgyNTBYdm05N0p6L3lrYkNzYjFIVkpTQnk0a3pPZzZYOGRMKzYwQ29odXlPMFlqazBweFdBbFh6RE5QRS9jYlBQb2NIeU83SjJiaFYrTUpTYjVmb1doczcxZ3pRbHRxKzF1cW9FVTZYRE1MMllXRk8yS0VEN1laZGorTDlNREt5LzZGYnFlcVZjcmtBNGZpL0hQaW5FR0d1TTBER1EwamR1QzloYURZSHN0d2ljSzhocHozdXFvSFZXUDVvZzdMVzU3VGxnYWtvUDZJeVNqV011a3hmK3Z6M2NWczJiVnR2dGFTVXplRjNmSFhxMHNSYVNNTWp4VHNLdVNJT044VUtsMjBzYzB4RnJrNW56NlYwbENSTmVSNnBPV3lvcS9ZQUdDbUpsc21hZUZWaFhjR2dwaG1xblY1UFRCMVBkSGVUcHNuYnNtZkNqcVlIOFlvU3dod2ZNbGVTVUhXZzJYTWpIV2JaS281R003L0MrVEpzU2FscVNGd2FEZ0hGVjZlUlh1ZDBzM1hOS3ZOUFp0QzVPQi9GczBIOUVZMlpEZ29BZUFnWlZVTGdpTktJYzNFenRYNWVmSTliUTVEV0hPMDBkVmxFaE1IWFVRcDJKSmhGaGc3ZDl2M2UwUlRoRENpUU11Q1pUY1pGQWZHSGdZaGRyd0UxRnEzNk5wOTIwVU5NTkZmU1dNUzVBQUpoRFlDU2dRWUxhVzFzM2RiZHNJWXd6dWU5SFJnb0M1dzYxcVVUdE9tb3VrYlc3K2VFbDdzSDdWR0dhOTNoOUg5Tjk0ZlVZTS9FT2p3cHlyZUhBRlFoS1pISFAzc3FoSDIvVXlDMm8vZnhsYndPelR3K2JrMTIrYysrSVZObFBldjZmZHdjV2dxZllQWWdWMEFySTR6MW96Q29HWTloMVVHT2wvNEpLeXg4Qy9tV3laK1JMUmQ2UGZMTEMwcmNMbE02RkFWcUVlM1g0ZFdkcm9xaHBNQWdYZWlaSVoxTzBESUk0WjZmVDdKb1haVkNCbUxRbGhUWW1abE9JMnliSDRhRGptbGVKZEJmMGgwM252aVFWcWUydExyaTNuTlEyZnIrY1VDZnNYL3lNaU90dHFCSk0wWTRpWEV6MjZxKzJoTVh4MDRpbDBDVmw3TFpZWVp3Nk5TWlp5clVJVGtscTA3MGdBSnNjV204VFZrWW04VXZLSmEvenRCVTl2ZXgvNjlkK094WTAxaGcxbExjaHF3NVRqbEdyRUJxV3NHbjVRdVVPTDFSZDVYR2RZSnJ4NmRVV3NwVDB0L3RtYnkxZ3pwaE81TkdrdzBDcjBNeDVzVm5wK2toVDlBUW52MWkrSXp0bkxzS2x6eUZjYWpha0FnbkhncFlocXhlSGFodXorSEpKQnlsNVRYd2l4czdpSGxJbDk2aVlYbVhlakFONlB4SmpicWczZU11b0RVakkyT3IxV1N5RmRqWk9Ta1VoREJHSUZ6M1k1ZzZvYnJiY0N3Yk5OcmhuNzFzakZOWHdWYjFZMnk4ZWVsRzZFTFIvT3RSZ0d6OFNLeTg5N0l1dEVOTksvbXFtWWNPWW10WU5ZUHE0NzVZa0xIK1ZNSjl4VmlCMTJ2VnoyVVBkU0QrYW9qenBTM0tLZjhpYS9JMTdVTFY2OTVIcUdZZjFFRFlrSHJXQnh6S3R5SFluMGlaNDRVTUd5cm9GYzQ1RWNKUnRNQkY4M0duTlkxc0tXc1JOUm9NaTk4cnI5em9sK0ViTThtNWNodGpFMTBrd1RJallZbUdsTTZ3LzlWS3JqWnhlcERKcXNrTmJXMURtRnI1ek9pNkdHR0s0V1pKYzdnNkZ4MmZGTFh4WTFBMllkZGNnZWFoYjMxdWVOUDNNT2M0eGRFMjFOMmw0WHg1YmVhb2E5ZnJrK2VHWnh0OXNWZWluL0JUQ0s2M0cwZTRzaUJhUXZkcDNpNk9kNWVjQTJadnZnWHJ3MmdpejA2N2xqd3N1ajhmeWRTcmxiZTFlZzZDTkxsdG1IUTBnQlJpYTVUQnRpdXlEb1RzS2plV0x0UmY5eDBvNVJDeWJ6ZU9RK0FmdHB4WUEvMXpTL1JGVnZpeVJaY2ttbTNRaVpVSE4xR0xqaW5Jb0FWMlg2TWlnMGVkR1h5YjRWRU1VUG1rSTBsVjZDNy9BWk5EbEVrUGJCblBzTE9zR09pblhFT3VqN2hhQ3BLeUNHRHR2cURtWXB0a1YzNGVmeCtKakx2OTBRa0t2bzZrM1lCSmNBUFNMd1YzOWc5WGhld2JDVXZ5eEtIMGFNbGo1SHprSjl4UEZxTmV3TWRCVWVJc21FSkhQMzc1ZzREVkFOR2JTMjcvdVl1ODR1VWFsdC8wLzA2RUlnOUs2eWR3cCtDeGpWUWtRNU00c21Oai9LcE5TclVGdjZzN2p3YmVUcmdSdTU3ZXhsNHpCUFJFajdmY1lWYkZQamU1Yk5IT3J1Ym9IRXZxZDJ1aU1ocm0zaW12aGVnT1FaWE53K0FwWXBIUUMrN1hoNjdGZmRUZ043NVRlZ1Y3YXV0bmpxQTZLSjlxM0I4NFBtUjZjNkE5cTBpNFlCc0RPU0NjbjNLa0g0UlFOWTQ2UU5pbzdlQW5lSHVHQTd2NElKSjlhOWNvN3RDMno1dkxEblA5SlN0QkFqQXFMZGpzR3JSMTJhSGdpR0xkQmhJTHp3dmhUaEhoNDZUM1FBOFMyOGdlSXhZb1JQdGNDeEF0ZGRKeGpTbHhRMzJWdDVvMVV3NHlTYXFBTjJ5WTIwRWFwb0FVQ3BHd0lDWWNuY3pCb04zaVowN2g3WUx3dm1xTU1JRWNwU1EweDg5Mk4xeDJoQnE2bXgySTFIN1VhckRZSExGREMxLzlrYUwyalFnNDllU1RPVTNyUlpTTE5tSWNLWWlRd25FL0NzQ0Zra0tnazU5MTV6b0VkQnlreHA1NzFsakFEVThnNVhMYy8vcEdFblNPY0Rpb2F2bWlXdHJIRXJJdHJuc216Y0FqdEMrZ3RydXUzT20vL0plcTQ5Y2poWG5UaCt0T0dLc09ldWNTUkFVZ2UwRUdqK2ZxWEh1dlQ5K3AxWVZqMGd6aEpTNzZCbmhZcktpeHhuemVOaVpvdWtPalY2NUZ5L3dHZmFyb2ZUTHNnRmQxMWh5VGdpOTIybUl5RUxvS2syNXhDanNLU3hadWRpMVNJV3hhVlBuVGF1RVBLbUsvT3JFQkpYM25oSXhkR3BLTEw5WkdsaS85eUJKY1gxc2tkQ0M1TEl4UEk0NmpUY1NhM0ZqbERJMjNlbTY0cGs5dFhKM0hRcnNwMlhGbnpxeGJpaThPS2FwTVZWZTZqRkN4U0M4Zmc2NUpBeVBvMXlBUDhiVWpZKzNiL0NUWEIrd2FsUkVqR29jMHk4UXV6cDJ5VW9MdlJOUnJDcjBwcmF5YkJXTWJScmJSWXd0ekoxYm5vRGIzN0pkNmVNRUFsMlJjbGQzZmdKc3Q5MWpNbVJoS0ZOR1pkTXNWcloxMlZkVlN0aDRBTHA0d3p4ZCtFMnV6U0k1RC9BT29GS3lHUWdvSGdPVmh5YitoVGJ0aFRvS2V2c05QWE1KZ004VjcraGIrbXoxLzFsRjVyRTlnRHgyZ2dCU3ViRWtESDgrVGlyelNEN3dPMm9hTk5CV2NwQmo4OE5reVRoNzUxUFNKNWZLbDBhaXhjcXEzMmdNZDlEVnlkRDJOM1FxNXhWMnpRZ2dXTEFpdkcrNmIzbmlXZWdpa0swRi8rUkpMcGRGa0w1VmpsYXV3RHdEYXoyR2NlZlhFa085bjU1N1ZwbXl3V3AxeXVqamdrL0hOa3M0NzllRVhveFBGQVBaYVRncDRJdmRaWDBtQ2NpRVZUbFpUNFJzREJIb3VkT3I3bnAvbTVtNFdYc2tGeFRuZG9NbmFWNUkzOVpTbEh6b2ZzMlUvOEZyNnlxRyswMCtzc08rdGsrMjArbjFGSDM3ZEsrMWRBbVhvaXdCbUZBbHNNb1JsVmh2VktaV2tibUdXR0gwSW9uYVJoSDFFTjY2aEpZUEdPbTFKNzRRUTBYbmRnMXBoNGdMYXdtR2Y1VjNJUmNyM3hvWHFRelVwNC96KzlJT0Y3YnJGUzZOS0pKb3RlVzBXNjRGT0l5YWpCaWNxNEd2VUpxcTlXT1JwRkhRSnlaY3o3cUhHd0hzMEExK2JoVWZvcHBoaU1EVllldkpJY2dHZytGM2o0dVZPc2JaNnJiaER5Sm5hRE95K0p3dURTRXRQL0h5bGRmZzg0VmNjZVE2R0NMWkl6bmhPUGJVTEtnWStXOW1IbU10TTNFMHpyVVEweC9ySjBGK2JVMUl3UzZ3MXBFSWlEc05jNm9VY1UxUjB6S3JHZThYNisvbkxJejltcklhR1IxclA4eE1HOXVuazgyMVJWZUtPWkgzUmRGVGpUVlpiY0NKUnFsc3Qrb3ZOOFQwRU0vajM2d1dJM2U1TGNTWmRaU1haM1QyenFWSERGc2JXV2lBOTdwWHhhZGViZ3lkQVdGTHcrdkZTaldhN1VibTFaeFZpUzZkSlNFR1JOVkRiMWhZL0tCMFgrbnVYUmNveEZwOENucGtYUzFOdzNGb2FRM29adVpWdHo1ZXV4Y29Kei9TWmMvZHJLT0Q2Y1YzajZuY0doRkppTVJzMlQ1S3p2dE5RbzIwZTdkL2VuSm1IZ1FCNlkydmtIbEIxRFYvcWVZTFpJQzBJTDYvc0dLZG52bnZ1VFdIaVEyRUN6VkJaQkZ3VGU3UGs5VGhlZ3hZNFlNQ0w1SFFpei95T3hHTUU3TkR6S2xadlo3dW5rWDVnMVlwNTB5emQvVTJmWlF0T21vV0dFaklVSUhqRGlSaFo1bVpqYUQ3VGc5cDhYd2x4V3cyR3ZJT1N6YS95anFtWFpQUmFtbWIzZzZRWC9XeUpTV2FlWjVwQmlIZ04wTXdDeXlsMjVFY3hGZ1hsKzRHeUs3RGNrNVgwUW5yelhwYU9zb1JNb2JhNWNvQ2JneVdyaDE1RjZIcU5vSTJmNmxNYUM4OCtrZWw2cDdTd08zMTFwelRoaytrZ0xYQjBMbkliUUd5VUxIZG05dys0VmNvRXhWTlIvRm5QaFljdHQ2N0c5ck1oNnVORUV3ZVNNSlgxeFg4Wi9DK1JXdlZ6cHVDekc1SkJ1ZTlOaWs2Sjdyd3hLZVFPdTl6c2JDZVY2d2MwOTN2eWtOMHZHUzRmL0xSeGduMjhIYmJWMzRSenFyVGVHYnA5dzIwVmo0bnlaSkRIWkJpa21IZFhjalEvYW5OQXlCWkVLVUZ1VFpyZWZScTlZMXJpTXZDZkQ2OE01L0RKZlpoM1ZRRlFGbzdLSTArQlRSUnJKV0VuMm9jV2p4ZE5MMlFGdVZnK2NEdm9vWS9DdndTK0lBMEtxRDdsOHRldDRKcjZoOHFDODlncDNvdHRnaWhQUzJlVUtYWWhRU090dVREaEJNbjZVWWZMYXlZY1BPZ2FDL2sxSlRrMW5OeXhpWkRLNTBjeXI4V3AxTDVvU0ZCVllpVFFTM2ZiS3hwL1VvemNzWHBlWWxHWHRMVGdhcVcveHBZeDIyQWxnMnlGeVQzZFhjaitOMVZwVnRoM2Q5OUFxajkwaWpWS2dacWFXUE9vVmZyeUFnQjNWcGhvRUZESjVFejZoeG5veXdpMjhMZWhKeWVtY2JLVHcvTXdCN0xPUmwwMUhHN3ZKNG9YVXQyZExVejdab2wvWXhZbHl1UzlWMWt4bFpRL2lZbDdNNHJpTkRRSXc1NVRyV0IxZjZ0dGsxdGw1bkFTVjlsdHgrU2RkRVRPMVJYMndzL0ljaG94VldyYTdjRW1WNW9la3N3Z1IrOVdWNjM5VTNqZlJiMnlTWTJPNy9vTC9PbFk5N3hLbURHSVpNZWhLczNqbjFLV08vWjMyVHZXWmJPUjRHanRneGh4bUV3WENsTGloS3hKaEVSdzJVMFFVcmZyR2drcGhwbkdGWFU3czhCVUJnZXlhbHVyUlBhTEVxQjZIdHk2SWxYaGZ1ZXZmMzM1bVo4YnFTTVRLNzg2SkU3d05KS3o2aXhRWWgzQzczWEh6ME53ZE5xTFhoNGVuZE5UeUtCLzhnQ1VBL3JJbTZUVnhpUE9ySURtZXIvaGVEdFBrbUpxOFI3N0V1NGN4aCsxWkFoVmQrVG1NTVQ1WDFkTXpaQTFMOTBhWWtTRUNxWUZOR2ZrMlo1Zlp3Wk82VlJPUmRMUHNNOUtlNUxKRDhEaTBscFdpUkxuY2RsSzFUY1FkUklIVjFLYzhhZHBGZktLam5PZVMvRWIxWHdIdXJVU2RDRWg2bzl2UU9rS0hqSEFEN0NUbHRBVm05STZTczZlYVBsbjgvQ29KdjVNTGNtMUk2YTFPbm8wbHRoNFVDWVdGeXpWVEZBZGxTV1l2N1pRMVF1aFBmSlM1cE1EY1hkc05sWVRvVi95MVZKc2hmaThnNEpDWlNXbXFEZnYyKzZEQWlmWXhiZytvNEFTSUlvcGUyVGVqT2ZxaG5vNk1wbTZLZEUvQTVsbytRalNURjJkSExwaVhmbFNWRlJia0FpWDdOdUxRa1pEYnVtb3crTnA5aE82OEx6Z0RoRm91SlQ2elcrb0pRNXdsTlhqMCthYnpreE9VMHZibVZjTVd3VDk3eFlOSHdRSFFLM1F5SGJBdEh2ZzZYQzFPemlONkNaNStwQXZRNC84d3R0K0tPOW9xMjRPckpLY0xYSkpzRGRyVEczQ29HZ1FTaFNEY0NTWUh4eE9ETFFiRFJObEpsaUNwNnRKZVFGNUlvSHF3WWZubmoyUTYrNk1sUHRneDdTSWs5RmFCYUZodmkyTXplbnN1N2RsanhxUDhKbnZMRzVMSWJVclVDUHJYa0VhMTJXRS9VYXh1SHlqSDBMeHJCVkR5ZWN2UWVncmtjSnR2S01XWUhOZWl6NTZ4ZWdQZ3J3OGhJNFEyVjQ2SGt3eVV4d3pqNm41N0VGMUMvVEFzY3g1RjhhVnovaWl6eFZMYXYzU3hnS3U3dEdmTEF6b3FKbFNKQmRvZGZma1plTElYeWpEZXBZL2MzQ0dwNVFCRlN0NS9pVUhxbnl0ckJPS2hFYmdPci9kRFEzNUZRcDlKNnBuWXpGSWgrdDcyaFZHSElJeXRIazg4V2xDOEhnMmcveUZvdFFvcVpMMjB6U0JRQit6TkgvaWVDc3FJVHQ1amdTUTlNaVlJaGRHRXdjMjRHQzIyWDRVQkJ3VW5sbldNa0xUY3ZIUVRIVGZmT2wwRHRZSzN2QUFjSVFEYStYeG85MUxkMGtPVy96Rlppa054bFRFSVRVN1djL0tybHo5VHRMOExScHY5Y3IzOUhqTTBtLzh5MVF2TXAxVnVybkxnY3lsais5em11Wm13ZUNQdk55OFNlMDFFd0ZNdTc1bkFLNjIrZ2VScEVxZHAvTlRSTDQ3V2pQVHRoenNwZVJtT0R0U00zZDdiZ1RxTGpLUWM1WGsvNUdIdWRTUmlaM2VPL3habE10VXNrWDdpbmZEOEQrQ25ZVzJOU09FQTlsMVFVcjFUbWJXZGh1b0FXVEpvTjZkUFpNTU9xZ292ZDZFdG1pZ2VDV1lLY0c3bnFXbmtLWTZoZlRNYjZPTWFFakRyR1Z3T2FvZXJBV1NwZ0N0Z3JxUXpHYXpmaGhXd3JKYTdtTGNIZHUwYUZ0dDVLNjdHUVJqdEJxS0JveG1BeVNBZ3kxN2Z6ZFFqYzhXdFRRNkQyOHU1RWVOdzlDYXlJMTFxN3daaWI1eFdyZHF2SEpNcGV0d1NyRS8yWkh6YXJBQ2JPWTF2eUtjalJjb3RtZ0pNU0x0eTdWZzM4Q3VkOFJQV3loK01sdlAvQWl0b0YrOFR2S0wxZnM2K0pSVTJkQVhWaThUR1FtUWpaS1J4cFVBc2M1cEhNOEppWm16VkNNbkkxRUZMclZUK1ZCQ01idkF1RkRNckk2cUV1WStwSWVqVXFhN2tNQmMweHVWZzBwVnU5YnUxQ1Q0RWhVWVlWMk9MNGZ0M1BXejlpT0RadkNOQk11YjZHNzhVVDlJajRtZXhNVzR1dW5DNHZqZnprMVdyQ2d0NkpXL3RuajV4cE9BSWJMUThhVS84Mjd3ak5zSmZaMU5aRzFGUUpGdXpxTG02aDNUek1relZsNXFGVUhEMkFWSTlSNWxuV2hxcVVZWXlTRm5XNGN2c3NhUnFkWmNsK0hFMjEvUDJ0UkhqTFhtUjdndHhUSXpGcHRUMUkxVjF3Ky9DZ1NlM0xOSGpTNnFZTlVWbEx6M0RNRU42OUt6aW5GK3J3RVpzMGxLTUdOdThXRlZQb1FDMDNLUzNWS3dTN2pQdXpraU0vZGp2L01XRzl6QjBpWnlXNk1PdXF4SXliZ1dTNjdsazVyZUtjRnNLTk9jOGZudkgxQkU0bVR1Vk9uRWlMd1p6ZEpYK3JGYlFpRGpwUWN2SlN1aXRwQnNRTER2T1cvejdqdDQ4RC8xdHROc09NbTlEdmRiV203bHh0aHgycC9iMGd1UHkzOEJoVjhoZ3p0YXY3eFFmOGVPVHJJbWJaQ0ltQnB1bWxGallsRlkrTkYyK3lza1djdG5XT2h0TTBUcHAyMy9JNlpWZGxINmM2L1RLN3dhaGZxRW51dnZYRWdZaVlBK3FnVjJnT09rQ0dNTGJiYlpraDZSNEpyK1lXZ0JYWE9yTFAyRENsbmdPRUN4UHVjNXpCRTN3alBYQ3pGd2YrVzRrQlg5ckduQlFrd0VtOWtITEUrcnorcU9SdnR2RXF5RW1PZjF5RUFseU9nVUU5R2pSdFlXbUw0VU5QSUV0aFUva0kxMGk4WHRZM1NSSWFGQjVtWWR3ZTF6WDZQOUdrKzJ3T2R3NUpVZ1Arako0UlNSZmI3clhzYk1RbW5lZXdIKzZBWGZHQ3BMeVYwamF5dm9FdW5aOEkzTEdiZjlSUVZ2Slo0ZkN2eWwvL20vRDJ1RWxuWlgvUUVLV0VsTDlDWWU5dnhpeERuOHZaUkk1bUVMOUs3YkJ0UVdVWjg3a0N4QVpISmNveEFnbC9oWDhyeUF0ZFM2ejZ5VFdkcTFrZG5lUmlKNWpCdGxJalorcnV2TTFEN29OQ1NWTzFGWFVtVmNSQVlHUmEzSEY0Y1lTMThIMy9rMjFzN3VNRnByOVlsNktianpiZHo1cUJrcjZXRkJtTWVTN2c4WS93RU5IWUVqUFVvK2JYZlpXRHZzcVBZbEZXc1kxbEVncVREWkNpRExnSHFaOHhEV1FoTW5ib2RMZDhud3h3dU10VmNTVlp3V1REdEFWazY1ZkJLaWJoMWpvUHBWZkJnM1Z0cE9wZThjT0VqMlBhRHRFaEl2ZVdXREs0cWxmaWlTOEIrSXlHajJzdEdMVDVBSXlrQm5aVmYwZTJyVGpuM2FHVWdlV1ZFVUZMUnRiUjd3MitqYWtBUHUzblRSeGVWWlVxbTJwU0NiYVhtWEtHc1FOZm51OTZ0QytxNDcxMEdlQUhOQmF4cWFkTkIzRC9XMVlnUjRiQVp0WWJUWHFwVCtwRHViK3c2VzdPdStSeEZWSm83bjlJcjUrM1pIbDRuM1RSWGRid1Bkc091NGlnempyNExqcXBPTXd4MnVpYlgxTDRUWUxObkN0bEZUSU05Z2NHZEU5RERCQ2FjUXJBTXFWWXp4SHpFaSs2VnE3UnNMYS96SU1xcWkwa1ZQZkJsekJRZlNKR2wwOGd4UmxQeU80dW13empTd3lmNTVFR2QyenZZWm1wd2RKd2tMM2pEVUpwaWFMZCtiakJ5NG85WHRTTlh6WXpTWkJ0RzRsNXpvUWVLUFNhL2tBemVoSmdNN0tOOVNoSnhKQW1BRmhzZGNrUkdiYWVESWZpb2VoRUlFK3MrMFBoQmtyejFZV1JaYWtBYzdMMjZoTGNMTjkyaC82aXI1UDE0bG5FWHhGUDMxakVkektlaHNkK2pMbEI2N25RVlhNd21nM3M4TE05L2xsejBoRzJ2WjlDbFFHbFBFOWRMTjR3d0VmRS80Q3YxRUVBalhiQnRqUzRldklCQW8rQ1JmWEdubnNrWGtPQjdLR1pYZHV0WVd0eElla2pqeHFsQkd4aTFnMm8wZktNVFVrSmxJc015dUZ3cWJnSk84eVc1M2x4THF3alIzTElib3FZVG8rR0cxUFpGTDd2NDlNKzFhdzkyR2RhMG9nQmdtVGpwTENGcTBJODQzRXF1TzdRNzdUbVJsQWhrdktmUTNZaXhNcDd0UW1oVGZKenduODBsQ2pDalY2QkdrNFZBNkFleXhrWlF5aFhsbjd6eTdpTXFoRW9ZckRPOUhVQmZDRG1ZbUNUOUZzSEFMZE5SczhvQ0FCcy9HNzBoZk1WWEIyYXpHeFdkOXdwZzhLczdsY3Mxa2xCVHhFbzliVVZUMUhDek5aOG5WV1pIbGpzQVZXbEJlRThLdWo4T25rMVVKNkFzZ1o3bmlTUWdkMmtNMmJFK1BuaS94UTdKUUtKMDNnNm1jOGtadnUxbkpuSzVKRFdIRlc0QnNKb3dBTi95cys1K2ZQWi9pUnBVdmJNZEFqekYrL1p1U2REQlpsTFFVWEFKM1dENFJjYUozbG04enpVdjk4VENjRGdsU1NQd3AvZkVpRjZSbjRPL1FKVFEydVkzTWVBRWs3b1FRSzBDRnhXQ1dibDFRK2FRaUQzQzNoQysyenJyL3gxUnN4ejd6cHRWYm5aK3NzTHMxUEpya2ZRNXFmWVpEWWFkK3RrZlFTRDh5WVVyRDVSQ1JPNW9VUUZVRXlXWEV5Ny9PN0k1MU4xRUkwb2xrZ3YvTk5JMFZnUzVpeG9kd3J3SEVraUhZRmphS0Z3ejRTblFnTDJlVEdPeldMS2RqdDJxd2UwcWdCRExNWU5FQm56aXp0TUFhby9IUEZjdE9OMGdwQnEwa3JwRE1vVEM2d1QzT0t1amtXUWhEVDRuSDJRM3I0VzZhS1ZXNkR1cHI2N1ZkTWd5WVU0eXFWbklrN1NjamVxV2twVEhpa1ZMOFdTaXY2cG90bk1qMExiTDhGb09MQ0dldTBBdGVJSEJtckh3cjkxRnB3dTI1SW1vTXpBdmdYNDF0VFhwL3pFSDc2Rnp6NnF2TVZNTWFmVXBPdFVXZEpvTGRadnE3Rk1mYnpHZDQ3RTR6Zk15ZFNoRE41Uk5GVlF0ZWY3bndYSWRzWVpYdFVhWGdNSm1vaHd6aW5UWnpZZHh4WUtZK1dyVW9ISTFFRDdKUlJSbStEM3paU0l0UUZDWkxVWTVmVXYvUm5oVUhTb1hRa2t6YlVpTDU4UExSbnVrZmZpWXJhRE1vTU1NSFlLc2hiU1lNd0Q0bkZCVkpNbFFBOHI5czFSUTBDQWs1Rkw0YW5QV2ZBRittVkw0bFNMdzlwdC9ZODE2cEtOVENGOFlueU9heWZEVHRtVktpOGxrVFFKeVhZOHVoa2lHVHhXZ2YzdGhRbVAwWkt6ekxvTDJGa3paeC9iT1E4d2JpSHJaWHoyWVIrSkM3eHJiRDloS0NuRUt5SCtWTnBBUnFCQk4wYmZzWE5LNEFjQnJIaFMvTnlnM1czL1dIRXNZeVFZd3Zvd0F0ZHd1SFNkS1paOU9jRDRHS09icWFRNGhCNnFjUmdNTmk4REtST0VwZ1o0bmFSRVpuWWtTY1dLTitlRHBwWHJleXJPZDVKc1F6VzlOYVpqZWZjKzI1cHVsRGZMMUZDbW1CVC9NcUtLckl0NzRTcGNOT0Q2RTdvQUplYlkzRDZPU1ZCUVhVQ0oyZlJyV1k4N3NBQW1MYnhMd01SZ283djgwb3pUTVorOWdpMWFuVXJNRStZRzJFY3RaVE9PMU1lQmh5TFN3V0Vjc3NvejVhaFdRRjBtdTJvSFBXOTFSa0R3MVREVjNrS0N1R2lMaUdDV3BrZXhJVTRsNG40cjFkcW5DY3hlY1pVbE9ObW82eXlXdGloUDBTRmRibnFlRDcyc2ViRUh0M01CdFhQOTJqSngzVWk4ZmpybmV2TXBNNFBicmI1dGVLK1J3R0lxU0NrWFhaaUp1SkpBWlgvRFJwVlN0akpESytGaVYzUStMNGk0ZE0xR0VjWUxNeE00ZERORWpGeStFK0FXaEdPb0tiaXByaU1XQ2l4QWZWRHYzdkN6SmVYMXN0OEhwdjlkWWVCaGJXUmdSa0k5OGlvU1psakgzOXF6MUpEbXFRNzhCbHAzRHNPOFZwR0VHb2ZydWE2OGRwY2czV3ZpcjUwT1dlNFBXVGVHaFo1bjJlMGNZYU15YTgvTDJwQ1pma2ZkbEFKUVFUTlZIWkdha3pCMTB6QUxOY0RwUlFjdzZjaGhDNlMzaWx5ekQ2d2VIK01MbFJlMlhpMXBBTzYxWDZncEUxV2NjeUdzZjhlUC9RUmZlUlNBcVRXL3RrZHpXckQ3NXB0TmpBTDR3TEs0SkJ6cENTYXE2NnRzNVlpelhaVkZRZHVjUlIyNUxOc1g5bEsvcXJtcHNiSENBNmwydUE1bDFqUkp3SXZmbWU4WC9RbWcxOHAwYnAzQ3ptQ285cS9PMGVDak5JV2ljdXFQMy9LaFlYTStBRWJTcWI1UDdGL3VaVGhFOVZHR3k3REs3NS93c2oyMXRHMHFkS3ZwOHdPT3MxaUF1QjR5dGNlTXdHZUV5MlU0VWl4ZUJSZE4rWHJ1Nk9IeFpIdytQS2dIVGkzMkdLWExZekhjT0VYejdadFh5Y3RNcG5VWVd5RE5rcHpIWEM5enIrK3FpMUZqc2p4dHAxU1B5a2lqQlhta1dyUVhqVkowUmJFMjY1Wk1pMkpiQ2ZnVmtQa0RLendwZEFhNk81eUE5YU9oY1U2c2J6MDFRcU1zNVpkZmhuZkEwblRMUDJuV0tVU0tHQms3RGRrYnRVancvbUQ2c1FxMllBSTBESjR6L0Y0YXBwZHZ1RFJTNjU3REU4Vy9qdzB0T3ZaM0poYzJWUSs4b0E5L2V3aTRuc0FHNTNoZnZXZG5GSnVmbjIvU2M0bHZkWjVhR01CY3pzR1B6OGxIdDNJK0VMemlqK2hmOUd5WHVvQnJXQlVIYTAyQmx5VmsxcEMwSnJybFBya3JIa0wzeWxXbUtwVkx2NnpXMDlNaU1VaWJ5bHpmd3pYTGZaamh6dlBYdEZDeDhsUkc1eU85d0ZoZWl0cFhacnhGRTBGaE5rMmxMZEp0dStTN1JjWlpMTGlBQkxNWG84Zm92VWNNTDhjQjkwSG5CSDkrSnhrWGZIR2N6WVBZa3ZJNlB6VXU5SmlrT044dlRBWnlBNWM5YjYvZkhFT240WDhyZURnYWIyR0c1WGFCVVFlcnphci9teERUb09xVzdEZ0x2TE1xYXNnYnJaYktXY3I2SHh0NnJrY2Npb2I1V25ERWR0UkJaWklyT29xZ290L1k3UXdVZDV1ODAzSHplOWVSQ3IxNHY0V2twZHBpYXdqOFkvaVBuQXZaVVpZS1lFcXgySHcvWE9aQy9RL0M5MklBcysyMjlhTytuWVhsZEZIQmlCbG5pN0hWbjB1Nk5SVVVEOVlRSG5yMnlTV3huSnYwRWNDckNZWVByYnN4SWZZNU83S3B0bk50TTZIZWs5SFRROUpnaUpCODFXTGtnWEhCT1hJcTAxMmNkNTBmN2VTenRFajRnZ21tTlB1K0g5YmhqUk55MXphbUJLQU85UmJrQTdyanNFTkFrOVhpeWtCU1VLc0JmRUl0ZnpnSjBDcXVNb2JuM0ZtZjF1V2VQZW4xYk1Md0hydDB0RDMzcDMwcEkyTGp2R3Z5Q1RsMldzcGdBbFFyTExtS0lNYlFsQjFiZWhoZzJ2c0VDNkNpSE9kekxmazBwNEJjRmU0ZzBDUG4ySkZ6K3J3emZvM3RaWEZNclRjc05xaGx1ZUl0QU1kbkVLeEdDOERrTmJEZjl5NERtWVBYNnJqZENEaVdETFJrTlNtam93R2wzNFZEeVVJU1gxYkhVT3pTNlZpN1NxdktzaVJVQVBuKzN0c2dsTlJuRm94Qm8rQUNJbnNwR0xyd0hNdUpOVkJhMDR5WHlvdk4wV1cvZndsZXNFQmZwT1BsWXgva3JpMXN0S2d1QXNONTloQnJZaWtsZzJWNFREbE1GTXlWczJXUkRCNmUrZG9CS1M5V3FFeU5wbm1tUStWcmU3UmNjK0l1OFdoWUJTMTNDTEZGTEl1SnVDWHR3cHZub3pqa2ZtSkI1K0pwbncyRDVxV3drUUdOUitYSVdkaFA5V2R4RlVvK3hzM3hlcHM3OU9DVFhKdmFxdGtUdm43Si82N3hRNmROd0NXZURIaUp6eFBtSTJsNEVyYXRHRk95eDQ3Z2Y5ZU1xTUxiWXdnNE1xOEIrM282ZlV4T1grSGxHYkp6RXJObUNFdjg1Q0NFbmlaRHd3b3pCaUhwS2o2Q1VXLzBIemJnYmR1OXZYbFJUZVNqTWdwd3lYaXdZTXdiN2tzZnJBNkQ1ZVlWQlE0cWVNbElHbDhJOTROdk9ZNSs0MzBOR001UzY0VzhyT3RrT2NjRmFvSWhvenVJTkY3RHJZdWhqcWhrMXB0N1VuMDgvTXBFaE1Kc2tmdTcyOVlRRk1MMGxTOE1FSGpreHpJYlB3cW52c3Vid09qbVFRUmdMaDNRc3dtRWdBUzBaOWR6ajFrYnAzeExnbmx6TEZqZitiL0RNUGJyby9JNHl6NlJkZU1xakk4NlRqK1gvcEtlQml5ZTdHQzJjZzJVYVhPeEpJSGxnY2pZRTI2M1lJanZ4YjkxM1JVYXhaYUR2MEYydjd5aGdDMkRVcmd1bExNcGY5ZG1JZzBtdmFnd0NEc3k3aXBKZkVLa29HcWN2N3pUelVKQnMxZ2FwTng3akZURU5oanJjZzVnQkJId3NlRzAvTHlkb0JTdTAwTENlczlXZmtxbzdaN3owZFRuZ2Q3OFJoMUZmZGZXS2JaQ0Ixc1M1WE9XR0xZQXZWYTBIbGYwZFMvaFBPUmRNbFdaZjdtZnB2MzZuUVRCUmdDQ1RKT1N1UmQzQVNQTEdXWkVaZW5YMWhTMGRPa3YzTFZERnlnYXU3OXM1a3l5cE5lOFltRVRoQzVlSVRoVnJSckE4b0t5M3VWZlA1ZitxOGVUd1k5clRFcnlwUGZCb3I4MC91SWExeExxQ0RxdXNRMFNuTWlKdXI4dStOTWhMV3k1MXdRdUt5R3BIVUlrNzR6bW1FZ0V1YVh1WUphMkt4K1dxbzdVMG55T2pXdmdJdUJZRGVHYlRvRVAwSnBmN2ROUml2MFpXbkZ3dXhUTHB6UWVBK3FMbnNxcjhEazQrdU84anBGUmc2dWl5T2tiVitaUGtqN1I5b1lqMnI0U2dINUNDU3RWcFlFZEZiL1hBWWNkMHg0eEJJYTVLTVAzdzRQSW4yaW16b3FNQTBncnZBOC9kSE4xWWlQT3Y3ZVRSeHo3Z3BET25oM042NFJtNERoYjFNc24yZktqaEZiWjV2N3h6SlphVDl6RlV4WEpXVDFkVU4wOFRlUmJJWnI2Z1JPV2NvWURqUGU3TkJEZkhWWWtrZ0JlUXJpcUEzVkx4Y2R6d3pLTEtaL1U2Q1BiSCthRllrZzJoZ1QwRnk1cVlVSHJGMHphVlU3c1VMU29PSEk0bnJMVjlHTFRlZy9rTjFDMzhrUGtBaGRBaERCSUpqeVMrajJKVHNqZ05GakhoSHIrSkRFbExPMVNHdlQ4NWloWS9zeng3OFkzNVQ5dlgxQ2huVUlEcmhTUVFJZjdONU9WVDhWVHVaQW4wMklrYXZnWXJBSVlvS3YvYkVzc09rY1BMV2Y0WnpwUmJZT1dPVFZWank0dE9PVHJLb3Znb1hwVnB6SzdnRkhYWm1vTld2K2svOFNGRzB1UVg5djNwKzkzWHNKWTB2OG1yTFJPMWhDcWdjSmhuMGE4WGZNRXhNNGYvb2NrbDdlTThjaERGMXFjTTRISzVuMGxVT0JFa2t2Sk5OUnh0eFV4TVZPeTJ5K0hyd21ZR0VQVWxwazRlRERvOTZ6dVA0NEl0bmFYdkV4Q2lOYVlhNEdlOEZNSlFza0FmTy9ZTkMvMWxHRVVqNmJYa3hjd21uakxZUWppTDVhVlBwS2VkQ0pnMzJKYnpxaTB3OU0ya0RZaUdlQ1c3cXo2R0ZnYS9JYmNWOGxhU1NxbEpOMDE0cDVjNGJsYk05cDlFOWpEbk5Hakp2dWFVRGpBcVNhcUYzUFZQbnk4am00aUV3WE5zdDVVVkZKMDF3Tjl0QUViNFAvWE8xQXN6MHhTZnVwd0M5ZldjN0M2c0VTRk9CQW9ReHZZVlV5YTRTbkljMDIxb0xNMHprZEszM0lQenVkNHp0S1hlbEErcG0wajY2MVR4d01yYVVsREM1N0twZFdUUVNHVTc3eWpiNGFUaTYxNUJpcjBSVDJDSkZWaS9aUXRHVDVnVzNiSnM5d25uRDFDVDFySUdIZDE2YzBxS1MvOTQreG1wM3E3VTVpWUxCVnhJQzFqRWw4UWlxMXQwSEo3TkppbzlydUlEOGZsUHJOWlVvZDFDZHFPaEtXd2xhWUNPWXc0Wnp5ZGFlR0R6VmM1NkczOXV2cUtuNG5RdVlXQ3ZkOTBhSVJOdUFtVnpLTWtrZENnU3lJTy9UendMOXpmWVVYVlNiay9lRWhjaGZsOTJZYUVXS2JiWTc1VXg5ZFV5T1NBa1psZm9CREtMZUZVM3ZWV1F4UVBLdDFIVDhCNHNHOXNtOUo1WmFVMGF3TnN2OFcvZG9tRHR5Mzd0RmtqR3JKRklmRmZ5NDlkRVFaUmhHdDc1T0xYQmRCQmN0RmZCM09PR3lubjg4ZEpPZmk4aUZLaVNnNDRERm5EaVNMdXNNMThkRkNObk9nYnl4MUIrSUZRbmR5bndONWd4WDU0bFlxYmkzRDk0eG90bUxuWksvOFFyVGVoMlp4elE1VUZWL21lck5sME9GcmR1MnE3SGVETEpNWDh5ckdqWG9aeXRmS3dmeGxlM1U5U2JFTW5pa29YM3lIaElqMmsvd1BTenNRcGI5VlRkYXhYM1UwcnQ0U2Q4N3hGYjBUQUZFanI0ek9oWnpmUURQZHN1bmhoTzVJZnlLSDNoVzgvMHpDYkRwRzRRQ1NnT3RGMi8rR3RKd2RRT20vQVh4WGI4MGVhVXBxeHlaNzgxcEx6cnAwL1NwV2lJWWNSNktCU2ttcFNPTHFBcmhpdDNlanRxSWJ2YTQ3QVU2dlNqOXVldU04Rk4wQVM4UWxha1VtTk85K2xKdWNPa3p1M2NwSTc3MzNtbHNtTVdVbnVmNUNaWXN3cmhTQ0x5TWJPUnlKaFErVkhld2l3bDh0MDY1bzlDNldjWGZhWmZmdXVLTkRkYnY5K1RHVCtueFVqazRKYTRMeWZEYU94OXRQZU9lSzQ2Z01OMzBJY2M0TVJFWXdpRkRRelJVdDlJVzg3eHBwWkNwRncveGVKU3o5L1RsR0E2MUw3MWxNZW15UElKNHR6emFEWWx1RXZJWHlsMEpURC9MakMvZHJiemdvb25KRkkvOXpHSnIydktITGNtVUFCbXlPNmQvUitYL0NLWVh0cUl0TkpMV050dW1zWjVKdmJab0hFdDI1TGgrcXcwa082eUhkdXFaNUx6cWpNSWV6bVRCYUZoZEJkNXkyT2F0TE9GQ2xoSVJHMHFhNDNIYUFqMnFSK3l0RzRwanVBTDByNWZXTEVZV20zYUpTQ1JWTkZvQXJnb0NpWkVGRXpCS0VhV1dLZndDWHhSQjFsM3VsbEJIcVJrbFBzb3dxSUx5TlVMek5rTlNYd3FYNzhLa3Rsc1Z0STZFZEVyTFVHNDdhckkrZmk1bzcyRnlETkJvNVBEMkthditCWjgyYTlMdG1ybU5YbXFHMTA3VFBkRHAyQkVCbmUzdTVLbzlQdjNLcVBLYkVPT0NOU1VBN05kb1dXQ1ZwY0lHYitqdTVQaXNvT0w4YXJzYzBJSWRjMmtydmoyTlpQVGtHbTJDUmRVUWNQTTVhR0FvaE9nYlRXZ3ZPenZCU0ZFeHk2VGlXSU45ZlJVbnFKdzMwdElIWFFaa3lEVHh5M1ltSzc3U1dVMzdyODhQUE9oWVBNUjA0cXNSQmsrQUJ0WHB2QStuNGM5YU1oWEQvT1M3NDVUaUhwa1gyUmUyNGhPa2ZYTTBrMHVjLzhJdXR0RGVHNm1SVGY4QlBIcmRhenRTT3FSL1pudk02QWxFZDBjaUtUY3dHSWx5d2tmd0ZUSG5ITFB4ZUVndkZ5QmFHelRBMHRCU0Iva3RsNE5kb0lnQWlDRjhaV1Y1T0FkalRCM1E1ZXJQaDNjTlVld3lhcFAycU9LOGdOTmQrZzRlRWt1WW9IaCtNaE5zdkVEQ3duZFFuTWZpenowMi9mVDQyaldQaThydnZFeGNkMmRBbkphNUQxL2lneHZQOGR6TDUrdGNxSytTS2U0VE9EK3ZTUWdBZFhQcDhMYWJrd29BendVc0s3SDVMWGd3Q1Q4S3FCZVQxcUpxc21yS21ERURnN1REYXBNZ1hVNTBCVEFtZlBnSlNaSjAxKzRtZlo0c1ZGVDFXVnJqeXcvZ0VGZFNLMnovRjhJc05DcHAxN3d0WlllVEtQdlZZUm1KS0lNbDI5T0lBcHNqZGNwWW95eGYrbmdDMDRibFBhTUlnU3ZmRVNBMjJUcHJoY3VmNWdnMy9PRXMvdEhzUWNBQzBuMWdqVzR2L0xsbUJ3VmM3K3Q2bXNsVVFHcncrS0xtRHcyVy9lQjUxUXZyU3pwVUdkK0o3Wm1ES00yaVROc1hRVE15TUNWRWh0VGJLZmZLb3Y1UGxBYmRVQXdxdGZIR3U1R3ZiblNuYmlNMGx6azdTSjhZWTFQMzRKL3NsbWVJdkM0MVhlSjVZMko2YTY1V2NCdFV3RzZibmJaZktSdlF3SnpVYkdFQ1liZHN0VFNPYmRKaE5FaExDZEFKbi8yVjJ4ZkxxMTBMRHJGR2hMQi9PVk0wQ2UzMm44VnRoVS90dVArWkNhYWh4UEtLNVJiZHltVE4vOWQzVDgrS1NSdy9qRS9BWXl6UjZpUTE5dm1OZXB5ZWJKQWtjU3N3WFhjWFhkc1Q0RHhtTHQrZ1Q1UWpOd3ZvekpPa1Q1eTBFbC9VVkpITkFzTnpVYnFoRDJNck9FZ0tlY2RjMktPMnJ0amdDa2ZzQnl2NVlVS25PZFZmdVk5MmhWR1AvTUhrcklxRXc0UGdoT2NvYkhzZ2I3OHFhejhSN2M5eUdrakxVVWR1SHUvYXAxM0ZrY3NKOHlkQ1FEZUw5U0YxY1FCVjMxUDBUZHlaNXdNWHd2R0x3TEdRNVlndGNLQWlmZkVFOEtJRW5QeWNhdFJLcldnOVEySEY1RlBOeE1wbDZBNGxvbXR4SmFaUGpyV0hVdTVmMTljcVBIQWxNbjdiK24wSllTd1gzUzc5V3lwQ2pybUQ4Y0FYWm9SR3dHSWFEK1hrV3hXV0NyQ3U3dDkvRHpMMWRLRHpZZlMvZTlsdGdyRkk1eDNhcnphTHVoVlAvWGs1SWJ6S3ZDN1dHU1hPYndiUEJ6VW0rWEQzUll4TzhQQXZBZUxyQkRHd0FRS3VIZnF3ZFowZVlYSkNUZG85Nzl5SHVVM3pVNFhhZExNUE1IMEcvS0czaU1mR3ZUQ1FENmxJVGovdFcvQWMvcFRKRFpBOEk1TEdCYlIvNWRFMnVPMVl5a0E5QmRVQTIzZjFvQ1FoQ0tIYWlERlVLdG55R0p1T1V2dTdaR3NYVDNoQVM0M2ZFcTJqU1hhaVpjNTl2SFlDSmRrMDdaUnZLRlFFTjYwR2JXQnhabjNpYWRaQWRwaUxRWHUrRmJtQlRBdENlVUJYTzI1bGQyWlB0NjVma1c5anZBYURhRklTMS9NR0NGSFBHT0Z2eGt1c3hpNWhGalpWUW1jd3dnTkJrdnh1UUl0am9vV1dJT1Q2MnlLUFRJLzhGNERwTDlNL0sxTy9RTVAwaFhHSCtjRmc5RmoydHNXUkRSVGNDZVE1MGdrc1FlU0d5R3prZ1BFTGVpMWtJQVBLUVlkUHM2KzJvaHcxaVJUNmNOZ0dRaWZjWDJKbVV4SW5iaWRzQnZEQXJoa2tlSVJ2dTl3ZjNSODJ0bzBWQ2ZvNUxnS040UmtqaUtOQ01lTkozd1BEWEwyR0UyRDMvTnZsbTVoRkdQTzFRSm5iUWtpcmdMMy85QzJuT0lsTlUwWTI1RVJaNlFqZDh4bTVTK3g1N1lwSDdJeG9tR1hBVnN6UDZKRW5EUkRRRmxOS0Vqei9lVnBJQ0pDcC9PQUJqeVRPeVpaU2FaZlpoSngvQmJUbEdyakNBTzJjOStPeEwyb0NRamhlWW92bnp1eCtIUXBrWWxnUFJselRzRHg5eVhhSXFpekwxNi9UUjNQNlNNRjdyQXpkQ0NXTXRRTWNqSXM0ME9nbTNveVpIOG9NUHU2NEw1VmdMU3UrQ1BMZ2t5UUFqSGVjaEhsUTNCcmwwUkx2MHdjSjZ2SFlCQkxBWExHMFczT3NDNy9ZcVB1b0kySElGZkhrUlNtMmM3bDVQRGRROGRNYUV4ZmMwSzA1N0F4eGdEM05MZUxmM290OUZCeFlnYWdjQ1BwWTluU2tQQnNDNXJxMWJaVHc1dk4vV0wxZDR3RWJlZ1dHTTAwbTQzQnlUZUNJVlZnRmNtVVpNZ3ltNzlsdXp0dFQ3VkxFVzFiTjZJNjRmRU1IdjRXcG9qQ2t2TmRmckV0ZGE2bkxjbzVOWk5WK21GRjlDYnhwTDJpUXpGNWxuVWVmUWNRT2ZXc3gzSkRkMFZhM3VRTHFkUmVKeCt2V1Z6SHJab0o4MGtsVzczb0FHZG9Cdld2MSt4ZGw0WEU0RWNCYWMwZUZ0QUw4c0l1R1A2eGhWWTJkVExyZzFJR3BUY0hNb0I5TG1NTkh0VGY5UEpWMnhIeUpUUDl5TzV1eVl3bjdSSDFxdFVxczlGSXVGOTBEZDZYaTkrenEwNUhVcC9qNVlZalllRWkwaTJramFOS0FkSlExeVpReEpKaC90WnAxdEd0NnZaQTVIRVQ4d3ZhVFpiSU8zN0NYNW8vYW5hdkpkaDVBSkQ3TFloLzVqUUZLbG9XQ3JEMnUzYis0ZDdkd2ZaU0ppZFY4WDdOTmIrcm5hQlpMTnJtN29xUUVPSGFLdlFheENaTDlnRUloUE9OVmZEenAvWnIwMVZzQnFiN1BDblRvTzlGeWI2aEdPRFhIeVpNTnJONkpaUURxMVdqK2dlQXhEejJDekhVV203dzkwUnB3b2JsWUVPMkdWbGRIR29rS040S1loeTZWMTE0UFRPdFRkbEJFWFVkVllTZnkzbEVKSWVzVk52NHg1cFY3RG9BOXAyNlc3MzVqcktMNzNxbjA5dTYvNDllaVFMc1M1dXcxMlBhZSs1SE1rdjJRRGpRY1l5NzNpeGFwakQ5ZGVJTlk4TXpFOE5iTlFKaU5PcnBkQTdaaWc0UldaY2t5T0NjQk5rNjlwWWpacUpwSFI0b2E0OU1wRExHUGtqZ1JlSm9YOWV0UXBqTzBuU2ZwNGpFbmxqVnlSMTlrdFRMWEV5WXpubERwTjBScGJXMlJ2SUhCR3JVUHpZTU9maHVqeklualkwT0hwSGpUQmF5YmJOeUFiWHJBeWxVcU9Cd3pCaG9KTFlOWXBtalBrdkt4OVRqY0l5QVo0alBHQkJSNnJBNGxCN09oTlE1dkJpZ0xCbVRWdmxXVmt3YVRmNHJKeXlDd0pRUVAzazZYeU9qZnVCZjE4clk4U0ZMMmxHMEg3LzFCb2R0dGNIbHZTYmRqU0pBd2tTWXFTQVUzMU1tQk92Q0NpQS9TMWt0NFptMXlWanZOOFJQOWdKK0pLcnY5NEtpMDh3RXNpV1kxOVNXN1cvQ3QvUTJCbFNOaFZvV0pJU0xuVHZmMmpGN2p0MVNhVlZuaXNRRldjazRIa0M4U2tDanRxSlpvZ3VnNXVkbm92bHJ2cW4xRFhrN0gra2tSc2lycXN2KytmYXlia3hiQUVONFVQZnYySWpMRXRwOXFxZDZnbTRlSnR0RGRrdmJEMzkwUmE2cFFlNXNVZnd0anF1SmxaanpxaDhjRHkvQmRhTEtNa1YwcEFyWDltK3U0MFIvOEJjcHJxVUQ0Y2NaZ2ZOc2hLUUY2UTFwbWw4UjRUS01xWEk4WTBENGpKcTZERmgwQ3dxVjNpWU54VmpFUDJuLzIyRnJSQlNNRVYvMlpMY3BVdkUwMFBaTWtHNUEzNDNsU0JkMzFwT0I4enJxdzRBZjQ2bGpBRzZXUzRYelNCN1pKWXIwZXZ2bVNKbFMyK3ZmcmI0bWdFN1lFazFobEdKYXZLcVQ1WnFNdDhFODNubzhWTU94cm95ODF2azl0Z2g2VS9KVkhlZjhnMExHWXQyYXpCbGxXZ00vNWJjQitrRUhNT0tlQ3I2OWd1aW9qS2hKbEo0N28xQmVlWHgvTWUrY2trdDdxRXR6aVlZcGxZSXJYbGVxL0lnOTY4ZEpnTk5Jb1g3aU1aM2tYaDNhVzVTbUlXZ29QTDhENmx6U3g0ejFLMG95b3dQOGlWUmtPZkNpck13UXFvV2ZvWjNJd3hIYzlaNTBxa0pHT05ySjduQWx3cnRTZWF2dGh2cEV5NGdDS2ZIeXZyNGlQOS9PYmtheGptWDkrdGowekZNTDdzWWZNRWpjQno0bmRhcEptSm1xV1prMXg3M0c1MVpiWlZtOWVDMVQvYjVlbEt2RFZrN2NYY1ZrVjlBWmZHSFZBV3BsNnBub3NtcTIwbmNCTWk5Mk01SCtxZ3g4dEVNcDhWVDZ3aDJmMWFDbXA5NzlYVE1Tdkt5aFB4bytzNGdVaHMxKzJwTnhEL1JoWG04emNJdjNaZTQwSkZjSDB0M2NHRWJUSTRGc3ZJRU82dCtQOFFmeW9TUHNjU2lHRkNVMjEvelM4b3UyYkZSOWlidXRuV094M3VoNS92N3FzSGZ5TkxsODk3d2kzRW1Nc2pHZjhBbm42L0s4U3VJaENFT3FQZ1ByY09xc3p1RFJmZjdjc3VLWVJzNDVsT2dVeWVOeWVnR0licHNRT2VUNDk1bjVzUEFhQ2RLQURuK0tsRDZRWXJLNVRKQjB2NEFhM3o3WDJkRlpVNjNab3VTUGhvN0h1dVk2UlZ6QktPQy9mSFpUUERTeW43a1FzT2IwMUNrWGUrUlM1ZzhYQ3NqbS82aGtEaDdCRnFCczRkRys0alpNN0EzWFVLbk5VL25KMHlRV1BSdnZVNkhuT0dNYkRJT3ZaOEg5S1R4THJkRitaaXhra1BySHJSaTAwMVBnQzFJbzNLWW1lL21hcUN1TEs1aytWMWR6TXhaWU9UN3dIbDdzVVRwTzFhdW9mNkhqMlllNGVDQVMyQ0xDR0VHVkRFdnVtU3hQNnlIYTJpdkFyYStMeVpPLzg1cVJ4cUhNNm05Zld1T3V1bEgxUVM0QUwvQ2V0bnFENWFNYlhJWlgwZXEvQkx2R2d5NVFTd0hOa0JTT3NqRXVkZXhXTTB2MW5GbG1hRlBHaFNpRWZTUUtjVmpGUTIwOW12NVRqREVGNkJFWlJsSjlNVktTdWxMVFBOTllseEN6NVcyR1AyNFRHZDNRa1V2N2M0VGZYR3UxWWFLSlVWMUhpa01vb0gxNnFwVXN5aUlwK0JxWndhbW9kakwxSGJGNjk2QmJqUDJUNndlYlNEUzZJWmVsR09KY096ZmJwZ2NQblNkYUZJa0xHVnBvRjFaeWtRcGRQdnFIUnVmTm9QQjZzYi9OVGRaUGswUXJhdDZ0aWg2bjc4UXhWZUYybXRFYUdsK1lBdkFOZ0ZiNjVsRStjSHJYMUgxbEdXWHp1SnRjT1hiN21EMHk5OGp3OWJiOEVzQ05PcExvWGVKNFBoaWJ1bkVVUmZVVjN5WWNKN0tqMGZlRjMvS2pISVJKa2JvdXFRK2FEM1YxR3dTenpZWTNFTE1VQ0haTENVREU4R2hoaWx1Uy8zd2g2VEg4M08rTVRYUGZJSG85M2lBUkQxRVMwNGJUOExYQmdRN1V1VzNjckJxb2twc2haSzdNMXRoTjVkeHFNeUl1bURvQ2Q5V1YrbS8xTy9HQ2RuWHhOYURyVGhIeG90VURSaVo4dlpndk5pVjBLbVQ4TEY3LzFJM2VSWXoyVFd6SzJQekxOcUVXZitnTmp6aDd5UlpMR1VKcTNEVldHbFBqcm80cGZ5d3BJL2pFZ3F6UGxDMHFuaUxjRHlkSWJmQ3d1NUVsTUpnb0hDWnAwZkI3OE9xdUdXNnNmRkgzZmNEV082WlJyamYrMGF0bkNXcG1SN3VOYkIzQnZMSldxMExFNGdzdTdROXNzQjZRWkl5VUxDWDZnbEdFZVpXNVJtNzhma3pMSlRCeENBVTBhV1IzaDhNcVJXRUtLR2Q4SCszSGJ0L0V6VUFvM1RRdHdDSWw0ZE0zNDUvVGxCWDFzN0pScC9RSUNDRG5UdHAyNGxUbzdmQS9vREc3dkxETjlsK2ZjdkFhSGllcGtUT20yeUdyZTBxcUdlYzhaMjlqeDgyTzIyWDhrSXA4a3l1c29zSk9RUEx1cEd0aU11aTdTZnpXSndxYThBMFNLZllQbWl0VUdoWFA1VFRjYVVDRXp2eXB6VWVNY1pUUFdJZnd6RGd6NExIQ0Z6T00weFNZQnZwVkNvVHRPRXIrYy8vSDhRT1BEMW1CL1d6TkczbkVlQTNZN1J4UHc4c3JsTGMzL1B0UmUxTXNvSk1XZmpkbnFndTd3RkFOZXB5YkFneEpzUVkzVHNmQnRZNmdvRTlmMmNMQnI4T1dOTFpJU0hGMVM1TGRJeFpMUFlKaHprN2QwazljTW0xdWxURm5icmpyMmtpa2VQVDVSRlV6VHpGRlpVcmh4eVNQaklWMkhqbVZkZVRtdDgvMVQ5ZTZ6Mm5tSFd6Zng5RC85MzNkSDNUd2J0bHk1K2oxQ1RnblIyTldQMHZJVjdCYzFVN0J1Z0x1a1diVzg4TEFQSzFhUzBLL0R5WSsreHlLZFIzUlV1Qml4MXNsTzFmUlNBdGxua00rM0tHMFNqR3lDV21rVkxCcDEwWnR5OUhMQ1o3aFcvdXU5QkM0KzV1RUE3RVFaeE5zbDQ5WWE1U2xjWVQwcHBDZUN5enZzTTlLKzFITmE4bFdVSERDSG1xM0FtbXFvN1gwZElWbGkzUTg5Qi9uSnVVKytWb3U4dnZ4bTI0bS9jM1k5UHNPWW1OL0Qyc2xpd2hLanhLcS8xUjRGK2V1cmovM1ovTFlESHdFZ09ucTZIRGllSlNnVy95bWNHYU50dlB6MmtTU1VHQ1IwclRGZVdhRkFYQXhtSHNLcFdxbmVKVllyckpGRjc5N2lWendVeHVhQTVVejBiYUxURk9qYUlhNEZMNHJlbHl1VWMxb21EVW1oc1lkemdHbVQwUnlLNE5KWHQxRTVtZ09laWJUd2RiQ0lsRE5DSW5uRzJQWk5ZSUU0bktFd3l0aWVabHhZYnBBbVpyaHhUZ2ZTallnU3NoWE1BSmVFdnltMm1wTjNlTzlyL0xsOVpRRTJPS1lmeHM3c0UxK213aDFPaFJaeEF5VnhOZHV3NEIwZW1KT0dLV0RBVnlTdUZBcGUvZERlWlVKNjE4bHBISDBqam1vazJHTXRiN09wNlU5MnFDUll2Y0pkUUttdENDK2VkeUwvYzQ5QzVaWU92dWIyclYySGlxL0xNNlBtV2dkdUxHbVJJTmZWVHo4dVExcFJLNHptVkc2cmI1V0t4ZlM0bnAwSHJ5Qmg3V092YlF3d0lGcGs0Ty9HdjlDejhwb1dRRmZLVE04STVKWkdFdnJGdkdIOTA0RGJUcVI2UVJ5dGZNV2hwdk5abjlGVnNqNTQvSUdBYVdCVExkMDdjTExSNVpqam5LaFZEd0tTMnZMcXV1Q24vU21rdm1QNnJ2TGV4TytCSE5EV0NaNElsdXhuVjErc0lPN1VDcG0rcVNpakN4Y2RDYTFYRUZFR0pKbzI1QzhMTk5zU3R3OGg1RFo5bXY3aWNGT3ZvZGVMR0dPR095RGJNUDF5cFE4TWtqVXZGUktZV0NhandSeFZsaG13dHpLeDdxam1PdDBTMVJqaFJzUWp2R2pZa3dlQTIxS0Y3c3NtMUZnYktKeHJKV2lVczlqM20ySHcyd0d4OUMyVGNDbE8vUnZRTm9iTDVudSt0bFRvaUluaXh5WGdWMlMydDFtamRSSjhVNjVxMWNBbUFkblJZZVhkVHZJcXhESnhzMGczRTlXeGpMNmlkSytsMmQ4YzJodlpUSkxLTFpOYzVyNWZnUGZlbUxhUER5bkFGMTlHMnRPWFlOdWJPeFN6R0l2b0hQTHM3elFlamh1ZngvSDRKMVA0RVlLNE9HOHhQRkVrdkcvbEM0UHRBV1U2S2hRSlROaWI1ZTNHTktZTWY2d2orNGNITjJuQjRiYThENnlpamp3T1NTRWlGYlh6YmFGZzRxbDljYkdUMmFGOXN5Yjhaa1hpRkxYTzRqMjN4YVhnT0JyeFBxSDlsVGpldVpnNEppNGc0Vm1rTGZsdHgzK1Q1SCs1SlJXMmRSU1FuQm91M2ZibUFzU1JWN0lubUliTXN2SVlLczFTZUllOUNRZWVwdit2a0FTWUZvV2s5N29NRWxESzRLbUVteHpCRzBJanJFS2VzWVB6czROWHBqQnh0M08ra0F5Tlgvd2tzSlZSMlhTdHlYUTRDM0VQWnV0Z2FjL0NML1RNQWhhWDVnQXhTVmNYcm1mZXZvSFNLTVRKdE9mSHgrcHRXdmlKWjhXYUFjMFNTa29QdFRTMjlzTFJ0Z1U4RVYrcW5EWlFibk5iTmFrTjVRaG1nS0h1SitEbkwzK3RZbHhiTVo1OEpGVVZyVVVtRDFMc0lPV2lHOGwrVDM4L0xHb1ZsZXBCSUVTckpldjd0c1V2TWFTSWJnRWZPVlh3TEI5WjRQQXQyMzZwMDBaV0w4d2JScklJU3MraUFiSnF0a0NzMi95aWIxUnNTZy9sY1BTUnphdjlXMXJLeUgwbzVrdFdyYUtiaUxiU1NUZmc3TTRDb0xDRGszejBXc1U2MkhUd2NHOFdqTXk5RWUrZEJiR1FkRXJ1WTlCZkN1Tm9kM3JWUm93bXZubDlhazQxTlMvVGhLRk44UEpSSzNWTUlrK05HZlBLazRnaDBvc0xNb3pPVlhNeEM1R0NLdXRBWHBobms4SnVpWjdDUDBBSHpDTjBtUlNMeVdLMkQvcXBWb01sUU1qSFZOWHR2NTRjZEpKQzA4Si9JemRwcUVob2ZXaE56U3hIQWdLOWpyYm5Bby85d3g4QUhzRmMwWk1YL1lET3RCamRDV0VmYWc5ZWZyVDZtd2xFckw4TjhuVmNuclJMWjB3SlVvbEtzWmhIR1ZlZ1VBa29DUTNvQ2RuMCtYSGRSK1FvSURNL1hmdGIyRDJsTm9nTFJkb0lHUUkyMk5CcVhxR2o0bm1heGU2Q2lrWmlwVWlCODNNMFhJaFFhVm9PeFhoRTdwS3Y1b0VtQTJYY3g5NFlKYnFMSG1Wc3BjRjRQQW54cEJsZTBKV3N4OVViUUp3NFRWb0pwdWtkL1RwdlM4MnVHWEFOWUJKc3BRL2FPMjNCc24yaktFMmU0N3kyT0I5ZG9QeVlWWndnaVZTdmZKMm42Zmw0RlpveGpzQTRlb1pyRGdQb2FQSUdwb0lJWnBtcnFkS3BONUVKZFhWeUYvTjhkZitKTkJrdGdNSUthRXdnN0xhRlJkMHdJbGV6VE9lRTUyclFRQ01jTlh0NTZtNUxYeDVJUWZqa0MrMVFuNmdmOUVqbW9MUEtCQWdsQkFLKzNwbmxTMnRQbnBJOGpSL2hxNm8rM01vcjhDaXlaWGJ0c05kaGVpZkR4L3JuS0dpb0JZRmdZbXFnRWNIRmZyTXR4elN3Z2VFRFozd0VuMHFEMnF4R0RXK3BLbEZnV3IrMVpoT2s5c1VxTDBHWGVMeXI1anpjL2ZEVlZmVnVGUkYzS2pycjdLUGJGMTRYNU4zWU5XZHRVWXE5eE9CcFhhemlSNnZrZGZoSFZ5SWdPaXg5QWpnTW1GN2V3RkJHZmhjMzhsVDBrZGpVcUsyQm5qdERqZUNheXl0bFdTVXFFTWVZTUl3Mk5xM0FKbENEVUZDNmdVSktsN045MGpqQ2FiQnp4dGdxMzZnT0hUemVmek10eHpHckRXV05sS2Jvc1R5UHcxK2pmUFQxTndPUSs0bjA1K3lmdUU2OU8vRWJmNXkvVEFBbFNPSGZmYXpUeTZteFJ5K2RJZHRxc1ZJa3ZWV1poYkRzSnlpbjEwZzN0bWlKaDRqMUlaaElUWlNRQk0zQmF0YmJuMGR5SDA1YlpOdnJxQVAxNEFpTndSZ3VjbzZKbmpqY1l3OWxZc1NaS3dEcVVzOWxPRFlvNC9TZ2FzYUNEVmVqYm1aK3UxNGVEZ05pb1BwaXg2Qkt4NC91eG5yOVJkdnBNQTdZUDdpOHZxVzhNaDFEdXJtN1hMdjJtNnRTY2hONXZZOHdsNDVNUms1NnE2Qk1hdjQ1d05EWlMvMi9rRFVpWVkya0Q4b2g3eVpVU2tobTY0ODZoYWJXUXVPWC9ZNVVxaXRQckt4RDJDMm53WnlCY2ZsS25aNkxOL210RUprUjRpSHppQ1JtekJieTFva2ZmNDdOZk9yR21pbG9xQzVzRmRlQnRMaGJqOHBIT1k1MUtrTlFvY0JjcElHR2NERVFMOUtxbERrOHlPVkVIUUdWb2E4K2xYMTJQTjk0R0gyRnRBMVBFZFB3SFVGZ3U4a1U3S2lCcEtCdUFlYUJPN2lWSHFBckEweXEwaDZ6MU5MK24vdmVzVW85U3V2TmorcmoyaVJ3TFdUV2NBaE1YZFBSU0htUnA1Zmk1VXlnelpvMkFCTjdYQ3JGWVJieXllK2FpbWJXcmJ3YSszeENiUEZMNmZFS1dGbHV4VTlzOUE0eS9ZcVBnL1JuMFlKQU9PQXlkSDhoRDI0aWxkLzR2c28wVzFINng3U0l4YUNBUUZVRVVmSWdTRGpvK0lVdkNVcWRvSlU5eFhJdG9TS0pxanJ1WWJ6L0hRc1BjdVRpVk9uNjFsNWhDbjBxTUVuSjhVSG5JSFNyT0txY1d2Nzh1S2t3TjN4S25qL3ovUUlJMmRVdlMxY3NKQjVXMnlsRnh3cTRoa0Q2eVAraGJVckJrSTlXekdBVUFEMVdKWkZtbWZwK0RIUU5QZU56VEdmWC9IYnBrTHVZRG1QS2FsTUZ2eFFMeUZTK29oalh5ZmFkbklzbm5ZNS95NzJkQUZHa0hvYnFjU0ZxcDJGRDZibVFOSEN2dUNrZXJLSUY3VFJnUi9pb0VJUTczRDB0azJpOXB4M0djMEVCb0dxamRPbXN4cFpSNXdMSnZvU1RSTzUzUnB6NEdaVGREbGRoSTFlZWpRTStlQzlYQnBkS1JHZ2tCMElja1VDcDhVYTFqK1ZGV0VEMW1IUkdnUjIrcVRmWnF4b3g1bm5ncE1mMVJMSTBIWFpRRXJkZEhPRFk1bS9RQ3VCT0ttU3Yza05CZWRRYURXMWRMVFZVVnNweHV0NXJhdzRXaXh6WS9VMFZwVUY1Q0UzblVVbmI1d1VqRUNqNTdQajIzY21GdUVBaHZYQXhHQjVRc0ZhWEhzQStROTFSNlpzRUZVVlJpdWh5YlVjNWlJcHp5UHVCZ0U3WExtUGdkbzBYc3BTQ2FJLzlJV1VnUzMvbml2MjZDYlJ5UWw2RExQOTM1QjZrblh5dFI0bk92cTVjTnM1bFdJMXJib29SVC9TWTZKK3ZWeUlDRS9QWUlmdVZqamFrSnk4ZFZXUHhXUEtRZGJTS1pxTmtjMUhqcE1DL2xYMXNWNlZWMDE1SGk0S1hLMWJ4Z2VaQ3daSkpFLzM2cFo0WVgvRWxwVTNjU1E0a1QxTGlDSjNvRUpJY1VyaXl2YUJpdE52a1hpUTdLNWxpeDdnL0dITCtTNzRIOEhwbVZVeWJVcElPbit2amdIWG9Eemhtckw4Z0JLZ3Z4c05pVjE3aUtpdzZTUSt4aWRpZlRaMDJYK2toaVZhMGd1RVVYa0pFTDU0QW1FYVhXcTI4amFIeC9taWVzNXR0UFB3YllDeHRRdjNpOTY3cjNiT2dUOUFEUDUxTTlMc2NMQS9NZGd0SW40TzZtODl5Ri9pSGJvLzFKMlBHZDdiRVVZZ0hhSk9EcEpLYjlYWEE4bXJKeFRhZ0tIR1hzU0FxMlloYlV2QmpGdmcvcGhHR1JuRVllTlA4V2RCdzU2MDRvaXVnYk5tVGllYWhVYkV6NUVUcHFLUk1QVW5iTmYwOVA4b2h6dFBSRnkzZllpUHBob0IvYmdsRk5oZmNwV09ocUl3Nm1BcjRyVU1IL0FyRFRIL3FleHpqQVJ4dDdveHY4ak42bHM4NEdha1lVMjhnSVBjWjZaRVNFWFQwQUNWckVSb1YwT00vbU1SU3NnRGpwL1BTb2ptM1U0bXE5d0pJVWlPYnRhcUZGaUJHbm1GQXJnRVhvajVnUmZaVG91M0dJMUlpU1B1SldYeHhnVnBXMkw1ODZwejl5aC85UnhlTWJKVkc4cW1CMWlkSXFYRnVKdHhMMHBwcVNDKzYxVXZKNjRpeHFLN29BYm1DTndubWZVVUxFNDV6Z2VvbWQ5RU5jVTZRUTBDRU1OQldMQ1U5cW16NmpsRCtydzQwRVVZczVqUENoMW9icllZdTVCTW1ydktYclU4VjgrcDkrdEo2VGtuTjdFcVRhMjdrckY5VS9jTjNPSzFYTmY1eUxQblF1OUFZQlJITUplWnRhV0xzajdCYzdTR0xDcnJ2L2Q1ZEluUkZpWXE4RnB3MHh5dG9mU1M3SW1KTVBFOFh6MWVHY2pONVBtU2EwUlcvOTZNWnluUjh2WG5rN0dubTJlSWVYYWlEWS9NOEFiZWlZUnZxY1FyN3dYQStIbDNtMXBhbjFla3JmNFJ4WE5oUU15OUZrWVVpY3FSRUl0eW53bkU1ak8xMXBxMll6TkZFalNpOVlycCttRFREejA1ZnBLZXg1dEMrV3g1L21xdzFqcWs5TFhsMS9maEpyQjJhSmtIc09iRGptcEQ3d3ZLd2ZieWxkRWtvRlpUQUdQRVl2Y3pjb0l1aTZuU3NNVW5scDRwSWpPYXViaCsyMm9GQk43RkxNb2N2OEtBZjZlVUxGelBKc0xVMHpCUEVTemJDNmY4U1Q2cFMzSmo0SExsTHo5OEJnTVhUelRjejBKSjJOZmI5UXVna2pLRUR0ODF0RnZqRVhRQnR6VWt4bmZUeVE0c0o4Y3RqQzhTcnZDNDNURDl5Tms4U3hOSlJDeHpUNDdBTkxJZWkrVFFVVFJybXZocjdWR3g5bXJvMWVCUnBDbXd5cWNOTG93Z2NBVStFMUdpTklpNE1YcENkSlJGQnV5cFhXRlh1OEZTYUZSLzVtYURUTlJwSWZVcjlQcjFhTzhOZFlud3VLOU9sZVBYRTNkcW8yWVdKWmtBZkcrM21aMFRYaUhUMzBXcGpWc1hGWWJhSm0rZ2V5ZnNhVWNsOWdLVVRlRGhnby9mVDNObXUyK0VsVEVrelUyaGhvU2RnN0EyaUM1UWN6ZkROM1NCK1Uza002Y2YxeEZqeDlsSnhETitwN1haNFZ5K2djRGV6czNXWnYrZzJVa1ZjbTMxd0ovdjhpYnNRRkVCNDR5M1NFVzVoaThIa2tlU2Y3akZmUG5zdnlubXV1WktDbzRBZGZQOGZHZ0RxU0hsWityR09sdXNEcDlTS1JhemdGQmJxOHgwL2hLQWozZ2ZMTmZEOE11Y2lmZjNpNVE3UVcwYmtKNG1LRHUyN0tCNWJDRWdYSVVpU09rR05lT0k1aHF5MDFJRUxOUjA4NHAwSkV4UjgxSTkybDNnY243VUZiN3M4bWF4ckFuaDhWWnI1RjltVnhSTm1zNER0MStJaS8rMlEwMDFoeHBHR1hrOXJnM2lSaEs1N2UveUx1cVpyNEZtZTFEaDJaUjRxZ3RVMklyTzlKN0J4c2F0czlBT0xkS21uQXMwWnJSbGgvVFlaK3FyS2tCV01ia2VDZUp4N1JqR1loYWY1WVdiS0taVGJwK09BbVM3b2FEWlNuYVFSTmd0bkgvQTFtcVo4SDBWUHc0aURna3RxWjRrMlBDWS9pMmNONkRINmhXSHVrWFdIL0tZd3QxVXYwdFVPbVhQZGdodUJyM3FpWDZXUGwvRmRXdTNqdm9DSTR4WGNDTUVRNkYxbkh5bXlVSE5JSzJ1akNHdDhlYUJQd0lCNEtqZjlUOWF1ZHdxQ2VBalpndjdhQVBOTTZpUW1RVU85WDBTc2E4L011VTZVN08va0pxTlZMSmUzY0Z2anpRSVBUY0VTTWpYOFNyeUdPQzJnL1BGbTJ6clBLMzJHTXBZaTZLVmJXQkF2Vk00RlpEbFB4OENON0lsTk9KTVFHR3V2VFhGUVkxc2dkdUMveE9YR3dLWkUwNmxqR0ZMbS9zbnR4UmxrVlNpUVpwcnVkdlMxalZVeWpXTkNTalloYVFVSjEyTm1IWWVLSHJTWXlsekpadEFyZTNOam1kOVJsRjgxeGErc2VRLzhoMkxLNFFPODFEQXlVQ3I3cGRQeFFWWDdYbnU1S1VOQXJwVWxtd2J6QU9GSS9od2tKb3BYMDgrWGVOcG16eVNORE1VWjA0QkVBSSttYmFjZFFhbWR0RXJTZFJnaGwrUzNSNGgrSzU5T3Z6OXVMNlljRlBXL3hlLzlIZkxPZmc5Zk96a3VZVDRITGVHQi9mdzd4dVJCZVg0djMyT1dlV1ZKSjhTdUNFTHJhcC9ma3lkTWNLUHhqcExKcG1uNFlzNlYrN0owUlUzd3FHSVhSNU1McGxCUE9mMXQ2NHdUeUVES0Z2VEl1dk42ZThHc3VJcG5RaHowUmFKcFdaaTgycmNvRGNHdU5RWXc3dlRySkdiK0dGK0wrZ2IvcFhOTGh1YkZGR2NJV2dHUWJobENwTGsxVU9kYU1GZnJ2ZVZjbjJaMXg0Tk5YN1dSZFhTZ2xsMzN0ekdRUm8rREFldEV0aHZ6L1IwbnFOcjJ4Mjh4d2tsRTNXYlpBZk9oUktwVHJCeXE2SDBCc0phcGtIZytMdFN1bkVuNlBaajRicnVvYVB5bUlYVTBKSmpVenVHNWRsRlQxV0pFOUNpQlI4QzlpYjVYbHhrTGtERWd0WFREL1lvcnE4aUtqRUk4WkNDVXo4MVF4N3FaSktKU2swcFhCVjROeGdDZDY0a2EyVEFJazFTMzhuWll5bnFsRVpVeFQrNCtaUjlFQ3RMc0RkTFVHVjdrdHR5cndURkE2MFZudm5idDBYWE9kK2F6Q051VTZtU1VBdStWMWI3ZGF6MDJkQTdJd3dqZVJ3TSt6eURYK2FUT093M0hlWSs1NWFKR3RZaUNPUUNUTHlkcWVTSHp0aDFucjNqN244L2pFbCtQeG1GcVA5bGpNWUp6QTZPTGtkZ1BvKzJwUk9WKzlHVEhqTkgwSEJud0M2VFkvZWtkbGs3cFNteUZTVEI1ZmZnV2hjWWNtVVlPQWhqVWgwUmUrbG0vSEszT0xiNCtPYnh3cXhXSEFMSkVKRGdaVDM3NElkenU5bHBwdXB1djBOWkc3cHNLT0U5ekZHU1VxTWdmN0JyckZ5b1AyRExlZG9CRjhtWlpmRStwRHNDdHRoR2wvdzdseVljUnVndVJ4RTVoZUNjOVdPMlBXVkczc0YwcjluVmtDRVQycDZYQzQybEsrVldqOTdkNXdjYVJBZ1cyNlhTRnByZ2NNUTUyVVkrR2w5UTJXTXo0Zzg5YWVKdGo2TnIrOCt6ajMwUEhwYkZYZDZRWmh4UnE5YlJZV3ZySUEyMGsxL1k0cisxSWNnVUl6UHFNcHgvUC9BSUprNWhwcEN1UnB2cHplTEY0R1dFSUIvMHd0WGNpSU04c1FLamZ6TDRHcWh5RXZjTXVid3FFZzRnRURIblp4NlNVVkt1YUhGUThtK3k4YWlxenVjZ3BNZGFzeGhYUGdjSHZaRWhDRHUyUllXZUI0NHhEcjl0TlVCMjdybHRpRHdjUnZ6ZERhZWZwaDJpMnowMWIzbVprUENBRlRWVk5GdkFUSVFualZpR0YzeFVETGZFclVSRVh2UkYwU2hrRllMVDVYanJBditINDNVRkg5U1dCUDZaVEt1UVUzRWdjVXN0VGE5elBWUlVyeWIveGtnYlhOL21XS0ZoMlNqL3V1RDRIcC93cHFyNVNCVnRBenQvY21pWEQwd2VjcCtUaFJENkR4R1VoeXp1cGM5TDQ3cSsxVFhiNG9Rb1lwb0JNRTR2OWlmTVlwc3JXaTFCdVRGVGtDamRUWFZ5U0U0SVZ2N3FkRjI1MkFTZGcrS0hyb2pqWENwRzJ3NmxPSWRYWlRLL05PdHBZc0JkRW9FdHAveWxURGI4MFBPT2xhOUhPbWQvdEkyaW54UGRPUDU5OFZNTjZSbzIrOStQNTI3NjhPU1FhSlJqa1FtVlNZVExNTWV2WEI2TGpLd3BOcWFOT1VHLzIxZjN3cXpqNW1HNzBJajBkNUg5TzUxWExBVmdxRDhsQnVwbkV0ZDZZREpTK1FxUHBwL3NWb0lZSm9IdmlGd01LSHpLNUxZWUNjK1V3RitjRTdFalFROE1LbXE2RFVkeXRrVnFmOXZxbU9PRStMb3RKRGNmR1JJYXh1ZmVSeUpvYzVWMTI3RWZpZTNpem4ydlN1eFF6OWlpVXRFczBsRHRGRVd5ZDBmbHNsSEkrWjQwOEpWOUtuUWZYNTY1eFVxRitNdHpLb3dqc1NwaFlOYmFWVENJYWhweVN5SW1MdzVudzN0NVBCOFdTZXNKVmZKVW5nRnVQdytGMloxMm94L2NYUmNLQUQvRFg5QVdvR2lNL3FrbUo3OUhKNHVDTWJ1dEN1bU5XMmVlYzRpcU9DUEVOOXpIOElvKzZLeUQ1SkFrR2JTYldaRGR2TEk3NWZ3RnlCZUNXb2sxVnVnUlNsdHErcnl0ZXQveHI1azJjTnlZcHhhTVpYL000c3VtRFA0bUdMUVJCMGhlQTF1Q2dFWjJtb2tXSmpnY1p4SHdrNmVWZlQ4QzFVN0l2RUJLRWoxVjAwQnBBa2N4MlgvQ3MvZTdUTEFQeUdJRk5vV3lvVXo5Yis3ZTZZUzM1YmZtRm9TOXBBNDRCVzJJeCtMT2kyTzA5ZHVDOVpmSVl2Zkg5UUgzU2xTanZYcFlKWWRzRzJ4ekp5WkZ1TGNHNWNodk1udGNSUlhEV3lHNlN3QmpzVkdMVFR2QTcrK216T3RmNnlXY3pkT2I5cDRQUWF6YVo2SGNQbG5JMm9sczcrMTcvbTB3ZDFyUDVFMFduOEhtNW5mb3kwdXJNei8zaDRBRFdxTk9hN0YxNXVOZVhUSmdiaXVXbk5YVHg0YnkxRE1PZkhOZEtRNHhubHUwYkIrM0FINis0dmQ3dmhyVWU3WnJOSXAzcUM1TDhOdkMvSGlrdWJ0R2s5N3JuZVBDc0Fvd3dJdjVhK0N6c1NZRWpqdTdMSEFVNUp6U1dQQUJWb2JwOUtOa2NYdytxT3ZENzd0YjUvcTZNdHlLYXVtYVpVcm5paGh2SHh5STZOU2dqTHBpQXM1MEZDUHJvVW9yVWdRaUs0SFNDb0pxYUFsMXg2QVE0TWFSSGhVNzJzRFN4djNhRGwwZy9NSElIM3Ixc0cvVU1aMkFmb1FWQTRYdWxoOTZxcGxFdk03UnlzdDEwVVJ3YWdEcytsZ0xmTUJ3TDBLUjNkSlRzQ0RCSXY5UzlaNEdNa2g2OG9SbGovYmgvSSt5RGx5ZXlCdVhtbXNreG9CNzl2S2pPU085MFVKTkV1ZzVyVWlpSldvbHVMeHVKM0piT2MrWEZYeEJ5WGx6KzFxWHZFZEl6c0V6QVl1RVErbDFCQ0FmY0tOZWt3dTNkVys1YzJWRDRhT3p4eHRCQ3dPTUtQZnIyT2lXYTJXd0VXMXJBRXgzaFJtbHhpSndwTlJ2U0JZTHdIYlZkNWpYeEppbk1PT204L0ZNQmdrUVV0NFNTaGV2a0JXTCtMVFVmZERtZGw1dlpxcE4xcVZ3TlRaSjdWbWVwRitKREVSNHlXYk5jT3gyaFVpMVUzaUhTa05yZ1psZzJIaDhHcTZiL1RiRkIrSllZQTl3N3Fta0pYeHdTcVdya21TYkcvYnJNRmJxU1k2V2xub2JnanpWSGZ2RkN5NHJOUTllVzJmSDh4UDhmYVRsZENjSXhEYnUvMmFMV0R2bnkyYjdNcHNYbWh0SGdGcUIxUXBOM3JhRGovVlhTbkMyZWE3bHlIc1dKVzF2WmxScll0THl3QWIyQjYyRHF6djU5SG9hMlZJZzErOEZqbFdObHFqcFMvSWdwVXptNktOcUgyb1l1SGkvZDFBcTd3eXBNdU54cGdLZ2g5WnZNNFR0aWdtWkt2aVlseW4rWVhtNVBjYURkdVJPU1pzL0h6VDlKSE5icmxQRU1YZ3paalBlSHpVUEg0YnBvTkMyMjNsWW1oRysyckFaYnJXWkJ1SW1HeW5xeC9lZDlPdnNHU1FnTU9idlBnSDJ4VWdVeUw2b1VkTHVJUnVVcFBxUUkzVjlSNXJmVzR1RlJWeDd6Zmtzb2ZKMU4rT3ZFQUg1dlRjaERqQTlOa3AvVDAvZTlwUDJCYVBjK2p2amZUdzdrVU5TbE1RdklZMG9BMDVlOU9WdUk0cnR1ZElVY1NKWFhwQkNmQ2pDeVpIRHdYMm9McWFZQzd4d0EzOXI4bUYxOXZFcHd4WmxNUUdqS0lDbzM2ODNxVkhHTW8rRUE2ZndYcGcxU1doQ2xjdW5qMjJCd1NHdnBBV2pPZFordktqSEkvdEpBUE9QN3J2Z3I0Wk4vWlg1WHR2NUJPc2hWbDRBUHNENTNxMWQwbExtWWl6cXlxMkpyQXBpZEg4enk0MFdrYXBSdnY0TStuRlNIUUxMaWpHVVNQNkRTRnZ5YlJLS2Q4WjFqMCt0eGRmY0NsZGRqTzcrem5LS0VVZkd2UW8wVDJOeHR6OVNyZXQvMmNmS1BlMXlUY2xYWCtUeUlHZklEYm5GYkRlbjM0Q2szRldORTQ3dm5OWUVwMDBWYkNhQUUrYnpINlI1YkZtR0JXSTR0bTBCUDBEK3MrMnB2REprbmRIa2Z3UGhZc0RSOVhSZmNJbWExM3B1eEhUMWFhWlNEdEFIWlFCcDJsNDZGK05TWXZHeFJhQjU1VUFYdlZqc1JZUkpuSEJEaXZsblliYzBnWVdEd1FOcG1oTmI5MlRIdVQzUnloSFR2a255d0N1WStTUGIyNTNvRmRYL1RYNldzMzJSSzhFaVZ1YXI5ZmdJQ0FnSUNBZ0lEemZiOVlKL0Q2TkRGOGhQOC83a3UrMzdITDg2M2Z0K3pKL2I5V0JyNmk3OXYxMGY3ZnNKdjIzRHovQjlWcmZiOUlYajg1ZSt6N1hxKzM3RzM4N0x2dCt6a3Z0K3RoMGZJdy9oOWNSZmI5aUgrMjcrSDl2MDhYN2Zmd3lmQ24rejdGajl2MWQzNTFYKzM2MDYrbjlORnI1MVg3ZnFNYjZ2cXBQMVdPeURZejlFbzFKRElOQ0pVOXdLS3FGUlZxQlN4NWVkdEJoYzhMZTV3ajNhOSsvR2xOR3VIdWFFVGkyeEh5MHdmdUF4NVE5N2tBdHFKRU5Dd0ZQb3ZLWTZWUS9ibnN2RjFvdzVsRUg0QWlSeTc4bGsvdmJGWFRjT1J4WVVHbXBIejhnbDR6OTNGTXdLaEx3UFZ3WndhOFpyVlU5dk5WQ0lEcjRRSDB2YjZNamRvb0IrWUY1djBkOE5qbWtCaU1rV0dZR3ZSaUZtRGZEeENNOWV0NURjeWgxWnM2V0NjNDNRRUxMeFNxRExRUGgzcHUrQWxYOUFlYnVDbjZld3BBOGJma2Npek53d3Y4Ui9tTGFrbUwyd3JVelRBbU9aQ242TU4ySDc5MnM5eDlXMk1yUUlkSFJOUnpYVk5yTHl5bmtDZXZVZkVuckI2S1RZd0hCa003U2JUMmREdGY3K3NhVjlSMkh3OHZuUDhzL3RvSkJFaEVmYkFkSzVBTWJMUnB6NzM1bzhDQVpaUzROU3h0b09zWUZFQnFoZ2RpTTVmdk9JTTdQMTQ0bEpxRGtQd0RxYWlBUmlITmJVR3J5K21qVi9mYnZCU05CL2RscUpGbXUrRDltR2Y3L3Z2dFpmam5UaDkwY0I5MzhKelhNcjMyS0RhS29XRjZacUl1RzBPVzBkcy9xZDhKWDh0SDlCQ3VPa1BPN0RwRDFlU1cxaFovVmlITjNQQ3pJMGROTGRLVzc3ckl5c1BsK08xNWl6V0JIRXlaL2t5d2pVWUV3WVhnc2dDMDVCNTQ2MHpLUVh2dUZ1aExzSVd6Rmd5bldiVHE5VWJPWWU3QjlxVy9IVzYyZGc4RFZ1ODRsaFZ1c25SVXlWMVlsRGU4SVJIZGxSenBTdGRMN0tZdjV5dk8zamNMdEhhK0NzSDJHUkg3Mk9EaEgzbHA1Qm5qaHFYbk5jb09ZU2Z3N3Bsa0lMT2dmeHYwN3RuOVEzdGVGWURYb3crRUZvd3NqbnBGYmdSSnpFQmdnVlF6dlA3R2F1QUxBcVBSOGdjKzhVYXJYejMyL2t0YllGbjlWUEJhenlvaUpjeHQ2NEZUbTUwL3N3NkZ2OXJqQkYvVFdNcDUxaEkvL2tUbWJmRndLcVZHV1hENzdtbFVSWU9TN1N6R2MxQTB0OTJzc1VhRGptdThlQVRXZEhPcCs1Y2tmWXdWOWJEV01UVjlnWHQvN2d6THlqRDkrdS9HUElIWVdhVVBSRlBYc1p2UW41WmJjd2cwbEdwVE5YS0pKMkRLcGJneWFsZWRGUXhxRHVtbml2eUw5akgvRGlBZnpZRXdqVThyN1JkeDJleEdVYnllZ2JPaVg4T2diUUlUV2FCbTNNbjcwQTVzSy9wNzZ0MEF4bDJwb1A2dDY5a3htSkRmb2Y4azM3ZnJUbTRoS1JwZ1NCaGx2TkRLTVlOWmFUZzdmOGlKYnVxaWNQcHRMaWRGQUo3cC9qTm5abVBsL3JLUU0wL2hFTTFrbTFQWVRvWUtyc3Q0OVNDYWc3K1dzckMwd1I1TEg0WEk0TVh1YUw2cXBuRUNuQU83MTBNUzVoWEdkeG4rNDB4VmV4MUVhOVNNc3NONXFIY240MG54aDdoaTJHbFYrVG82dUdKaUkyTnh6R1Q3VWpUY3JuWllxOVNjS1ZiNWcreEdJazhNU0daSGZSeTQvVlowZGI1YXZPaGROM2F4dnNyNXJmZ0U1TnJDVVE4WUFqUDJGYU92SFFTQmZIaVN4M29wMWZPT1g4UEpjSDlaZFFoT3BjeFF2V2ZzV3V0SThrTEl3RWx0UzViaDNJN1Y0alBJYVM5Uk5qb0o4bVBGWWtpdGIzMEhYeGN2OUVjZ0NaVXBkdXdORzFzOW1qbnJyL3F1UHp2ZGxYWkZTZHJRdmQ3SFZtTG5VbUVsR0RrVk1FWjBBSmwxZlR3ZENqaTIyUW16Y0l4T3AzQzhBaVhOOTdkR2JzclBYWWY0cW1BUU9melJ3dllqT1lKN0FGcmIxY2FiNE16cEd6VlIvUzgvM3J3TkRjZFZrUWszcDJGWEpDWUZEcEJxUXBoVGJ3TWt0NzRuUkxoL2hUSHJuOFZxN3lRUEdLYlZoOEROZi9xdjRpQ29yenNXS2JQT2d4bTFpRDIvTFlvTVl5dDdIc3RSREwvOWlpT1FzbjJrSkFvbU9OWXhIYjZ3VkF6N09CVmw2UzRZUUZTVC9rT24zcnl1QUdkT2lJakhoS2ExMkR4YlRHQ3I4TVdaRW5MdklyVndOeFkzbC9ybFJGam4vblVPdkoyRU5sTFprYzU1OHVEM2gvYXc2L2x4YnYxTUhkOEVLQ0pZS0J4Q2R5TTBoSks4dnF6SC9OdlFKMnVkem5NSW1saWNILzZFb01NeklxTGNZMUZuSm9CY3hKc0d5d3JMeW1BblN4OGI3UEVzYVVBdXdXa0hsdVV1aTdVRFIwK05RMnk1NGdoS1lYRHA1TXFYQ1BYUGVLblRqK2NjMldiNW1zdVM5RHdkOTJ4b3F5bi9KYW02NEsrT3NXY2RPbU1TWWhNSjNXUlpSdnFtcFFHcFhVMy9NbVg5cFcxcmVzY1lObFJuTGNsaTZWNjlXeUZKVy83Zkh0SU9sYmZGY3BHMW5sUE5kZkdYZnkrSS9mNFppSHBmOGFBdXdKbXVYVXdFRDR6ZXZKMlNZVitucnZpS3RDTE44MzY3cEFQd093VkE2MmFsVjdHWDFjNFI0WHBjbUNydlhlM3N2VGg5L0FNcVdwUGdpMVpUbzlyZ2RmYnFOeVZkWWs0UzUraVNYbkxPZjNjUGFwVTkrNHBQdjBrekIrcHdDTkpKekErMUorZDd3NWt2d2szY3dSbHh1YXlpK3oyWlF5QUt3RUZ2T3prZllreVo1OXFOTFdyMEw5RmZoVmxQNnFDOGM5WHY5OU14N0k4SDB0Mkh5TDJJdU5KYUNaaWQvUk0vbmZpVGhpaCs0ZnFQbnBXcElDYk5VK2dMa1Q0R1lFK3oyc2hGMFBQa2JuOW1RTndqNk1TTkhkcEkyWkhOOTNhcE9JdEZuMTVYdzk0ZlMzeGpkNTY0bFZvZkpIOFJKMzVjOW5RK01aS3Y2K2VSR2pvUmo2NGc1TVRrUEpoNHFPL2RMZjFIdzVSYmRIQUh6MU9TQXRialdaOHJYaXdmT1UxU1c0RnROTHlyVjd4N29tZytwd3R6b2d6S0hnZFplTVI1anpHcTEvQ3JTQ3hDVFJaa1F5TU5MeHAwU0VSdnBCd2FUbldVTEJPeVltSW85dFZKNG5KUlFFUkFPelNGMTdGWTBHUVVLVDZacllvNFJSdzhqYk9KWklpV3AyZVl0VGxoRjZaczU0WTVONmhMM05FbmNTTFJNcTlOa2lBK1pQRHJqL05qL25ESVZLNm9kNzVwYThNTjYyNFl3UzI2Zjk3NHhBUU1BWWpTbkowUFNGMDJTMmpyU1dlYzAyWUN4aGJkV0tVRkp5b3Jzd1VlT2lRc0MxQUpqQ240SHVnQUYxbDhEQ0VHWDE4OHo4a2JGTWYvZ3B0RVo1TTRnbkZrR2xoTkhUQ0JhNGlSQlZ0WjhQek4veGVEcEtBSHcyWXFGaEdSM3IvczdocGwxaHllSnlYQkpmb0lTWE1JNGpFNVFNcE5xR3pQZ3dBaHcyMFJwSHk4OUxtZE9SODlXbGNQRmRPZEwreFgwc0NhS3duanBUREJWZTdzOXd0cHRJNStaTjlRVHdaNW9zVjhmdHArMURMR0dQK0dYT2dpMkRUUFdCSVpLbC9mNi9FUEprZVNyOTdyNUUyeEpFeXNGNUhtWjJlMGIxRXRUM1hTWVlxZ29LNVhFY1F2OHlvajBrZzZOckRxMWt2ZHBOL05MQS9WVGdadjB1L2dSbmJLcXA5dHZjcUJDcVJPYU5xMkxDaUIvS2ZLVVJ4ZEE2ZGdBU05ES3c5RTNUVnVIa1pvSjBCeXJVZk5TUXBnalpIcTFSU09jcHplWWlQTVFIbXd0MVJnaU50c2VncGJaUE5wb3FVWmxxdjZvQ2J5VE9VZEpqL21MYnNNZ0Y3Rysxbmsvdys1SzZ5c1c1VXBiWVd5UDEwZldRMkV5YklnUWZpTC8waTFhV2VBV2V3V3Y0VDhUYjNvN2N0MnFtRFFXVDZKVHF1MEVQOFNEcktFMXNEbXV2U2IvRDdUYW9LYXF6eVBLQ1o1bkVqNHN4Tlh0OUtKUnRHT1d6QXNkOWJMTUxRZWF2RTJ1UVk2eGpFQ3ppellzakdodndkMHBXelo2Q1dhYmxxT01VWUxKV2JHbkFrYXQ1dGZoaHYxR3JYQVZNSHROVzNaWjFRenQ0M1hXVG8zR2p6YTFFSXJZVVFNcDBSOWJzTUgwVjIrN2pYOEdjYlZUSXNZL0k4Tnd5NWhlZURLTnFaNTQvbkpDaDFwYkIvVUkvR01ycmVTS3pnUG15MnBsWkJTUjgrL24wOE0zK3AvdEp0eDBqUUVvU0xDanordTkyTGEvUDh4MW5ERGZpb2c2OFVqZnNQYXVOaGh1a0FGNTM5eE1WVk9IVDNMTEhWNUs0bzFySW5xMTM3c0tNN3ZDZ2NRdmY5bjNydVZPSm9NNjIwRUhNWkxEdUlKQ0RCc1IrYTZXd0NzVWN3MjJTdnlUMExWZlV6bmFLb0tIbmZXbTVjTjArUC9FY3JwYkZ2Z2NWb3RKQ1R4cDlsc0ovVkVUZFZOWFhYcHZuYTlraTJEUFNaTHVHN04zcE9uOTA4V1pKcWhTaGQzSS9NQUVTenF1YmJUZi9CTStCRjNNTjVzazM4UHVHSDF6SFdTbWkyZjNwOWNBY0w2cDRNK1NhYU5yeUpkdm9oWm9ZK2JpczdDbWJuYjV5aXIxR2JxRjBPelFaQ0NGYUIxdEtveHRDKzRxVTU2NFJ6YU84QTdIMVhuaGl5VUtwOGhUTUY2UlRpQ0diN0lUVTY3b2lCcDBJZGxJeVZQTi9heEV0VmV3dDZ0QTE0Q0tBejl4TVc2emZ6MTJvYXR4RWVPUGZRbitHZU9acGlrRHI0TVVKOHZUeDk5UC9nMHVibEp4aWZMZkM2NENmMCtOY3AzWmNlNTB5QzE3UG1jaG01c2dsNUFBb2lUZTJFeHppdE80cTZFQWEwWXpPUkxyaWZkQS9xMzNWVGp5WWkvVStXdFNDZGt3Ym1XR0NZMTA4TDhyQ1ZaVTlZTmdYbzN5MkxtL1VIbkZIVlI1QXVpQ1V2MVdWbmMyalNoNVpxV0hldlFwTGlBSXNqaTJwTVhmZnExWFZqWXNLY1VQUHFPRFk3YVJteHFuRkZWUjM5M1pvSi9jbmdYWm9oa1ZxZUhkRW5yN3F4THV2N3FGaU5aYXh3YjBzMkpXSlU2YXRzYlpRQmlrYUprS1pqSzR6Um9xUzY3dTE1Wnp1TEpNYmNOamZsNDIvdk4wVklQNlBQVUJzTzA2TlRKTVNPMzQ2MjN3M3ZHSlVONzluK2pRUTM0N3k3NVgvcXNpSWtWQnRVbEp4czF3RnprM0ZOMVdMaXlhUlplN0JrL0hValhpdHhPWGE4QzRjTUUzMlpNUk5XZEhrMFNmR0JXUElrZmlDS0lka2ZVRmJwemNrU2xSMUhtUUl2L0N5cFdBSEFjT3ppMWVCQXBvVGdGQ0pRZzAyZ2cwRlZScU9RekN5U3BxSmdLcEVBSHBOdnVEc2hGcVNWY3I0RkRzdjlyL3Zob2NIMExFbVlnSWlzYm4vT3NOZktGQUFDZ2NtYlRwZCtobDdFdlVta1VpSTBDcGRkY0xQZjlEdEl5ZE5CRjg2UlVILzBmSkJmMUE3UUhOVDJZUDI4dmtpckxDNi9oK0hlS1U5WXgxckU4WVhGRUYrbGFjbFFIVTBVUXR2d01XOVJEZUxkT1BaTVlmaDlPRHk0eG5DdkdicjY1bjN1VE1IeEZycThhSWNMeDNvdXl5aGwzTFpJSGRHNk8xRTR6amxra0FzT1NBKzdNUGhyN3ZVV1VhYkpLRlZqZXJZT05DUUREK1NWNjNOa1FUOWV4S1M2STdOWEFWYkVBSDNnUHRWTExFRFRPZHllNnRyekdBbFdIM05wRjZnTDRySm1sUWpmWmxXTGZLeThReURBRHBmOFppT1k5WjBnTDF4N1NZSDkxcUFPZ1ZuQysvT3YrMWNNQmF4dFRHbzFmVDFuVzJaYWdHdEJoTE9HVmlSMkJmdWw3cmJNNzdwNkE3MlZOZHBSeW1yYVA5aDJHUHhSRVlyTHVtcmVBeEo5QlVoMjNNcjNKMGFWQk9EZUpSY1UxYXQ4NldyS0RMeVFSWjdaZ3hMTDNwMW5zTE9vWXZDemRvQmhHbFJiWHdKUVZDUHlxTVhxK0hhOCtSVUw2WWJEdFVwQmJERHdVcUVZSTFrcy9kZFY5MTBBN29GRktKdXZZM0JLWEVBLzFRV2ljR0grQjJKZjhoS2NDTm5mVDJ6OExDZkgxRHZFZzdqNFlKV29LV00vakZqUHdWeDRUMHQzaEdPYXhBcXpLL2M0MlF2bURXVlVRTHA1WTBZY2drWWc1TFg1cDV4R1lkbGlXM21KL3UyUDVxbys4aTlpa0xMSmsycGdieFRMMEF1d25XRE1rKzZLWlowb0xXNlZzdld1RWlmYnJLaFcvRm5BRmlYSXl6djk2bkU5cXlxelZvUkl3SDgxREFEaG85MXNwRnVuQzFrMzludHdidTM3NkgxUzZWampXcEdid3RYMllFTkp6ZEJNRno1VXZEUGFsajBzZDhVMWxSNzRDL3VMeEdkY1F0NEs5RlQ1TGxnZ2ZuaVg2V0tnMG5QTER2UzhRMFdnTnRzRHZRdFMrS2lEMjI2UTg4TmZETHk1aHhNeDRwSWZWd252bjVoVjZDS1AzYkN6YXBRLzE1R0pPRXBCR3MvdWF0dzZXTDcyamJhaFQ4dFhocTJvK29yMWdTZnpFdlRyODIxaTBNdDBUYXBuRlpxM0l0YURTUUxEMC8yMlRkbVNUcm1QVkRRK3lyMnBLZkxLRFFZd2lidDE3eFBPZ29aR0V6N29jTTVZZGQ5Q0hwZ0h3SDM5UnQ4NnU1bm96OTEyWDdhaGt5VGM2bUVBVzIxUGVLcWtVNTlIVmVBMHNYM0ZFQmJoSnlJd0oySkpmUUs1aDQycFVuUzhNaVBFSk9yUUJLbnZ5OEhhRjRDaGpXOUlXRXpORDI4RHJEUmR2eHN4YmNUc1lCSnNVbXpmVmY2eC9uWWhIUGZiVlFJUi9ZcWFPSTVLaG1UM0cwUmFpSllxek8vOEs3S21MVVBlWFZJLytVUDdPUGVhNk9YajNNdXQvR1JreUZkM0Y5TzBBTDc3SHl5SWJpWW9nZENvd2lIdm1pYmRKWVhUZWFXZ0F0SDVZZ29wQzZHYklMTGovVE5UVnNDbjhvandoS0o0SnBGVDJyN0VNTmI1N1FReWhNS1VZK29Oa2Zuc0FxRURFN1NFdEsvcXljenZYK1p5aGtPelJ6ZUtXMHk1YVB6MDE2UjMwVTB1WXl3V292QkZ6SzE2Sk5NcU1HVzdEbXF6a0V2ME9hNS9wU3BJZzhRTUFYeDZscC9pd0Z2Mm9HVEUySmk4V2VEQTRLTzNvL0FOd0V4Nm04dG9lTGVONmFnSHEvZXV5a2tZeVI4b3UwclJXZjhpb00wZXU2MFUxcU02U2t3aFBUcll4VjNESisrWWY5TXIraDdaWmV1alRNWU1oYXl2bFNRSU9aMkJFU2JDTWtvMUM0a0FXZjdZRElRRzBvUE9MTXRjU1NsYkZLUlVQYzYva2t0ZjJheStyRGVkZE9ZbnBCanQrczBjMjlLNDZERXozVlFCRk1TektaaHBEalV4MTdZVFN0MExSNk1UcnlCUzkyMXVFc0hYeFc0YmYvVzhrQjZmUkYvcThTc2psZ2NLWE5tQzQyNHhnRjVPMUVxL0J6aG5EZFdHOTd6R0VRVDh0T3VoTHMrWEN0Q0lJV05Jd1VPeWtzdk4yOXRlaVpsVjVrZCtDR3BUTzFhM2FydHVCamluQWlEeFgxaVEwbDB0dE0vV2J1aFdHc2RrQ3FMZGxkamt6SVJVL3c2cndHdGNEblYwQy9DNTQvTFdyekYwQTNRTXFNYzBuR1kwVWQzQlA3REdRakY2T1hXeFZWREF5YU1HRFdrRTNNcXNZWHZZd1NHVDRWMmhXOHQ2aVR0UFU5OXlEdEdvOXVYd0dXVmpUazg3SkVuaDFDNGJkZ2JVTUxtd2VGQjJobGRvNTFFSU5zK3Nia25GTHFOMW9RcFJsSk52VlNJTzdIWm1GWEdXdVZTelpMQTNXUDIyWWhlVTNDMGlsaUozUE5oaWN6dndFTWNUKzRzUmhNQzc2c0VzL1RZSDVWcFVwRVVpUnZmWWZraWFaWGp0d2dYV3F6VDZtYnIxNFJ6UEd1RTdTc2svazJyTUNkaGozRGxBNDFaM2pCMFF0M1lyTlZJdWgwN2g0NHZvWDNUWG9Tb1NjWEZYTXhqRFZwMnFmZmxzR2Jib3Y2WXNldnZWY25BcnhYQTFqcHdMZzMxeGtXbmhMT0xKejlsR1M2WUpzaGx5SHhSOTNINlNWRDFiYng1TGlOZDVwMnppL2diWmN5bFhKcitOM1Y5UU8xU0krVjkvaFJHUWczZFl2a0ZCU1gzR1dha0YxTGViRzFvMXRra05nemhIQTU0SVNmWU9LeVhGTkFLcldxZXFEWjNSem91ZnJjK1FQbXh5NGwyMzBaemhoM3Vnd3dxVThueXowMlJVdW9wVjkzYzlxWXlIZU1DeGUyaWlVdWozaHUyd2I5cW9icnRkSUZzUm01aXBSc2RZS0d3d1htZUg3KzZPSEJ4V3FKVHFDR3lyOXQ4VWNURXNmOW5pdGs2eUtiY242aUpqWldyTnlzYktrRkFCM3FObWFiY0ZXb05sTEV3WUxFUm5Hd0oxMk1PaG5VbjVYTjlBODBEU2MzTlZNWjRKbkhFTkgyR0owZE1JVVpDQng3aGhLUjlUS1ljaDAvc1NPUGJsNHYyd1kxajJrUG1xOXRoMmVNNUxYZzVzaE0zWmkzQWJZY3UycHd4NHlwZ1daYyttNXgzNmlBUlhDcnczbWo3aHNMTDJub3ZjcWJZS1pjaG12S1dsd1dZN3JIMHUrWURMb2JPYkZFLzBqTGVXbythMitzTWVrNDFBcVZXT0I1MHhqelJsVG9SMFh0YnJMMHp1SHhYelNwc0FWb0tNeEpJVHM1MDY1Q3JJUERIeW4rRG5zM1NkN2tKUGRJL2dmV1NmMWFEZUVGUm1WUVhCRDNYQ3VFQ3Y0Ny9WQ3MzZUhxeE1ob3Q0QUhlTUpTTVFUOHNxRmprb3J3bEczY21aU1FVYnhCazJJUVFHWHpNSkxjNHMvRWJ5TlZLWFBCb2VEVERnbjFlVVVKVEE1T3E4K2NoZjBqZExuTHh4SGRtSTRJa3lQUDVwQ1UzN2htUk5reG0zNHJ0K1BZdExFMCtScHhiamxtM1FQQndNa1k4NHJzWVhWZGdISFR5QVh4cm1aWHhiN0JERXF3S1pCOVBLQld1MWlqZExDa3ZwNWozSHFOelRjMmd5Q01uV1ZSOUc1ZlNXNEJtdjNuSG1TYmdEZFgwS3Zlc0hIRlVveWJUTys5NTdOZEJwakJvem5DUUJKOUxLeFp0ak9rMUpJTGNtMkxXUE4vYlpqUzRCYjdpSVgveExHOWtIaWlpcEtQRWY0Nzd0YW1FdVVpblpNeGZGV013SjExeDBRMzBUUVcvZEZlU2Q4cmpmRWFQYytldTRZSFRwcUlwMmMzSG9HT29YT1NlYWQzc1c1K2oySXNjYXNhSXJhaGN5TjBHbjBDUzVHK1NsdjFKN1hpVkNmcE1jcUd1ZXExS252b0lSYWgzNlIxdmxpVDRDdjNNSDI3SXFqZi9MRDU5b1JBRGxGZ1dRM21tSlEzVGtvcktPaEROZUh3V0hiQTR4VkVaRThWT2p4OXI3YXJ0czluc3lIZEdlWTlQa3Z5bExHVjV2Y2Z4TEkyMDZnT1hCTGs5em9pUGRwQ0c5aHIrQ1NLeDl2ZE5FbGxIR05GNVRGUlZTL1JBQzIvMDVuaUdUM0FZN1JCRUw5Mzk1Y1lWaGtXckZyRVVwazRkdWNoanNjZE1KU2FvUjhvNmMxWHNUaVV0NGNodmRrdlYyWGlteTNlSzhQL0wyOFJycCs3NnphekdTQmRiZzRBUmlIQUpyVVpCNkNTWVJYQllOMVMvRTJneFVLSlJkSzJTTUE4N1VBc1FaSnEwd0VpRThzeGlnaWtzNSt3QjZLUlRBYnZjTktIQllCWSt3UTJmYkI5dmtsMXZYNHBEamFpRkVGQUlBSngreGsyNUx3N1I3cjNyWHJZR0lXZGluYitVSnNlMFZ2b3AxazJhd0NtbXFsa25RUWpUMUxmS0VsZURwenZsRDZteDFBaHp6NStMa1VHb3BBb2h1WTJOcUVTbitEdTFQWHJ4VDEvWXVMb2ZmN2hLdExuZmhEZ2hjQ0RhOGE3RktUUmhrVXhxZFBuS28vMXRzRm9EUWJIclJtMS9ya2d3cnk2ekFZaVdqZmdCRnRFaXVsL3hKbjA1SDBPWC9yN0NGTXZ0bFg0ZGYzdFNnM2xLazRlY09RVzI0RDRhVTRFa090NXBPTDFXaDcwQU5JZVA0ZnpvOWhFK0RkYVpYVG9MZEhQSVdCY1VNSVJOQU9HbzdIN2FEUzJGNjh2NVNBdk8vcEY1ZGNPdytoQTVzL3RJdS9IQm9Wb0JlWERzNGxjQ1VucVgrY2NlTGpOdUkrS3VXLzJXYUEyZ0pzZEtpRnNqVHN4K0kxMEE5ZFJ4ekwwaCtCSGFxY3lMZjdFUkdTNFJvTVM1cEJyeW15TEFZWXVkKytOUDAxTytHZUxFVXFiSmcvd25OYnVROEJrVERDc01tNllYVzluYnIzSEV3eHc3NjcvMXpTK3M2RUZVYVVnWGJwM2RTTVRpOTVqRS9GSDd3SC9RMXp3cTlyeWRMN21Ed01LUE9SSjF3c1Bwa09TK2FJWHB1OU1VcERsSUY5UmVxekRuazk3N05YUXphdUNKV05QQ3FtbXYzZTRlTnBjTlM0dmtOUkhoM2VGN3pReHlXRE1xM0JmaFo1WEVjeDMwRjVmKzFCKzh2SVN4MFkxWFRGSEZwbXFqYXo0dlgrTisrZHAzY3BWdkFQMi9RdHBtaWhHTGtWK1JaTXZQeFBkbktORkJqWHlxSnJtV0xyN1Qwd09kS1l3bEtTVTVlbUNRcWdTSmVRSW41UTh6T3hCWFJhanhYZTdlOUxEUTVlOGpqUGl2S1AzV1NYRVNlNXlSNDZNT0o2bWZGdmI2NktSZEU1T3pjcmZUOWZLWXhLOWZWdzhhMkFXUklFK2I1WnZ4QlpIRVlVS3owdWlTdndFTHVicDE1L0NyRzZtZlJEek1NZUx6ZDhxNVZpWmFlTXNCRmNUTzdUMmg5OTI0TXZPdng4anVOZWJVNjZEeVhnUzdmWkY0UDg2cEVaY2VWRGt6YXhURmJmRjQ0TmhSQWhWdytSaG1KMUErejEwSGxpN09vS0RweWNaWVY4M0hDNDBuVUZPZGFJcHBtRTJxM2NobkEweHVjY1pCd0hxQkQvV2I1ZlR1N005SWtvMTRtajhvZDVSUmE2Q2o1TWxRSXR2K0poVlZxRHFORGo4b3JRM1lVYjVTUzhIMWgvRStwM3kxY0I1TGhIQjRSL2Y1TDR3UDMydUtzVW1uUVRnb0J2ZysrSTRqV3NPdWx5dnhjRjFuRDNaSWUzbTM2c1ZMMnZNS2JOL2J5WEo0TmdZZGJwdjZYVmxYbDE4VmdLdUVVa2tuRUFkbWROOUNQUzlOMjBlU09mS0N6NkpIN3JVa0pya1JjdEZ2aWpReGtoejdkNHpWV1hwTVlrTEllM3hsK2Y3emtKbGJqczBPVE1iR0JJdllMdkg1eUlKczhscEJpWEtkb2VHZmRBQVJ4SVNtWFRwbXFXYm5GT2RBd1ZSQ0U0TDFhNFFWZk1ERElXVjhXK1AzMi9rSkJhWVNvNXl1WXBEOUExMnpsV25UL1lzanBDSVJ6bTZFWUd5Smo3WWVJTThpQm1YMmJqZFBmeVlnRFFsOWhtVWFjM0VQeGp2WlQ3VFRFRHJWWloyZ3pkaWwvVDVzbkJjY2tiQktCRWIzYWdkdzI5TGwvVVdaMEN4WmRMWlNaNnRMall6WERZeWczOEM3T2d5MHhtVElKTlp0ak8xTmQ2UWJMSnF2dUhkaUFGem03L1FiaG5oUUJrTnJnRXZRTVVkYnNvKzlaakpKSTUxQUh4cUk3UGhQN3BMZE1QeHFOaHpwdEZOZmx1RW1rcEFaL2FINGFLTjBzeUEvOEI3Z2FwOUtzZ3llVzJnNHFMUThmQmRyU0ZzVUxOdXJJdmNoSWtkTnZuMHFlZGJNb0JuaTlxRXdjT053dzYrV3BOd0NhQUhOR09nSkRINkxmZWlpcW5zdjVGQXZzbWRUU29uWSt0TWtHNEo2M2JaVXoxSEE2Q1BOSDFrUEhHNXN2elBoRnZIN0NJS1ZydW9ZakVUTFZuWDJkRU4yVzBkUGIvZm0wcEtSQk5hTS9tN2IzRUpVRVJkMTBWMm5MS3NGeWZUME9WWlNPNDlSNzltRVRWaEVUalJ6Y1FkSVdSdVRtTmk0TmpOYjV2QmhkZTJmWXNMb0V2OXhrTjFsTDNsY1FaR2hFOWN4d0VDc2MzNU1SeEhuWDU2a2ZiR2ZoTHlCQWJrL1JaSW9QSHFmTU5Ya1dENXJPSHN0V1RxM2NZUm9yeUZaL2FEbFhVcjJJeHA4aEJYSGRXdUZ5NzQ1NjV1K0YvRGxzaVpxTmR5NE81SnpEZFdJdGY3VDZCNGFzaXZ2OHllR2lQdGpuYnY5dVVlMUUyTDRjY0Z0VTF0dmRyeE1MSkVaOFhKNmlVV0hDYnhmUXc5dDNTS1dIb1diV1l1bGZWQ0NKeUhhSGplSGR4QWdkUC9GZm42OW5BbThZRFp4K3JQWWVrU3VPZWxJMTVWWFJ3NWhvQ0kyb2VpYjhiQjlwWDZYL1BqOWRKTWJudldTTGtRdDhEREdwaHhrcyt3YjNOeG8yT3NHTVJ1Tjc3QThHMm52K0FNVnU2ODlTMy9FdTRDbmUzN0d2WWN6WDE3VmEzdndxSlRJcWNzdzZYVGtRZnp0Sms5ZnpjRy9DZE1id2tXdmVESTI5c2tJelByVFFDVmQ3T2RGbm0zYkxYd20vSnNscCt1Tkp4b09NQjJhak4zQVJhNHQvYW1aeXk1RytNL1o0a0FCSE1XSmQ5T1ZHLy95bStQWWxFbTN6Q0FyaGovZHgzeEpoMXlRck5WUTErY0dCbzh4WkVKQTlRSkZraERwOXFXMmwyNnBDMDBrc0t2bnVpd3BOTmdnVjVxNzBtREp4MmltMUFXUGVrU1ZNci90MHNsdmxHQTN3Z2o1b1JWS0NIeHNySTFmcWtKd0lmczhJUy9tUFM5NHNVcTRmNURoSWZBRVR3cDhqb2JLRm9rRkJNUXhjZndwVzh4c1VzM1M3ZStkMHJVV1oyQ3h5aHlkeVo0MU9IVGJjcHIvMEx3L0ZXRUZKTG5zQVduUFcrMXFCT0tUaEt0SFhXNzZGYXZnYnJQcTM1WUtrUjVyRm5lcG4zN0NqZStnb1NBcVdOMmh5aElNNVNqb2tSblRXTVJYN1hFc2FtbE5UU1laTEJ0THlBYnpBVFd0WExicFB4NVJFZEc2amxOSk1tZE5DRFJTR0tvMVVCdjY5V2pzdEVPUktKMWZGWVhSaVlNM1NwZWxzN3JROHRDdEsxQ2FTblhxQzR5SG8zeTZJNm1wZW84QjJXcGtRNVpmcVR1YmdnYmg4WVdRcmp6eG4rUGoyRkRDNVFIa2tGbHc2ZGVzZk5xZGZ5VEJ0R0VFTlkyWTUvZjFPMXk1T0wyWU9QWFpNU24zTExBc2lObTR6RU1oR1MzOEp0aUhKZ25vNW1TeUhtNEJJa29oMU9vREF3ZlVMTktsZFd2cnJlMjc4ZmREM2U0R3lHWlRycXBFblFreEEzSmFqNWVrR2wwWGlmVVdCRnVsNFFJY2p3YlpIM1g5ZlJqdHFKYVFpNmJCTHhBUXRVa2l4M21waDBsSEZxTXhlN0xaNlZZd0hFTDFIaVhGOFJQdEY5SXZ3dlNmWnJ4NFpDQkZENXRMV1Q0WWh0ZGhNc3pjSzVWZXVkSjN3RldrMnd2MktNdHFVaEE3N0VOcGtiRWhnWUt5SXVWTDRadlR6N2lCSVBoRGdwVzIwYUlSVGpDc0ROSWlacDB0VWc2QVVqUkVYMUdZNU0zVlFaNUhTUnV2ZDJzL0pPSmZ3bjdmSW05VXZMV2hNR2RBWWFsbUxBSWd4cW03My9IY1piV0NacS9kN0YxdGk3WmVxenNya2tWK3JnajV2Mm5MODc3UE5RVVp3bTN4dDhKMW1pWWlxSEpJVzRRdE5JcTA0TVdPOUZWOWZyNEhFMFF6ZTd2U0NiajdZNHozb3ZqNHBabkQyUlVFY2FpWU55ZXhZUXcyd0taMG54c2t5OTNZNTlZTElERWJDV2R6eXFjcVdaVjRic0luV1pjU1RuTTEra2tUb1dCYk1ndzNoU2kvbHFYeVpVQktkb1cyMkdTSjFCNWo1QU5SdGkya0pNNlliVWxBbU9wdkdoTXNNT0VwUitLcWhEWWpPQzhUV01tOS9PT1VEYnZidzBmTDdGRHN1NzRTVzhlYXFad0QxMHJKZnV3Y0JmWEd6WUZOUUZaY0hmYnFoeDM0enltM3ZKYVRTdTFuZEc3dVRYMHdUNXVBTWtXRDN3Zlhsb0FROXo4anpWa1ZiaFVGS1BXc3NnWGVyQndUNDRjMUlmWmQ2N2h2N0g1ckYwaTNSU0RkcENMRldPSHZTOStuQTIzalQ2c0dBMkFnTTFJQVd3NkVzWXlEbVladHpuOXJXK3l3aXViMW53bFB5RlA1M3Z5SjhsSXdFdS8wSWcxb1BFeGdxblU1OUwzTEpOSmRoNnl2TGF3NU5UU3NQN0dVeFJPMkc5ejhOZlJEdlB2VXM2clRSVisrNDVpK1l4aUlHWUpVOGNpeEl4ZVF3SjBOQ1J3cGEveXAwdytwekcwZ2ZGbnZwRzduY2JwRWJPNi9NOEJsaCt2cjJUSHROek5zMnBBNjBDUTZxaHZoTDBxZTNWS0VmVFpNODNsd2JpWE9mMGptcEpYcU5YNDZ6VFZvRTJGbTdjUEgrdDFqV2ttTDJSWEIzTWtkdWtVeks3Y09na1p6OE1xZVQwSVN3K3RjNno5UHRibzV3VXVYbGJwZ3hxaHFaV0pjNHF6YWI4bmpVRzFDNUNiZ1dWRCtobHNVVlpRaVVEWjlRWHlKbUE5SmlMZzNWMEtuNGY2WVdESGZEWE5EdnlSbVB4UXgvZ21qU2pHc2RNb0VSMFM1OENxS01FTExyandnNHhHTzVGU2Q4ck04ank1RDlpWDJnYWZoOG1yNDVIRGJZc2xHZEY2d3UxMS9oRTJLRDBDbzRmbTdCMis5Q3dtc1daMXg2UlFmT21LYzZzOG5CQ1dxeWRPb2VIRFU0cCtRWERBVDVwTVBVOU1JRFJlVlVxT0JGS2o0WHRpRGJYQ0VGc05rV0NUc0FDbFRjTUQra0crUWkra3h3Mnl3bEplOGVQZ1NpdS8wdVZqaENJUElSNGFwbGR6bDJ1dENJT1BiYjREUUd6NndBZHVQa0FzbC9pSjJmUXlFZ2lFR05CYm5FWk9tZHVDVkppeHNEWlRtZkQ4bnVjTUpkNGg5b3VoaEtBZlFYMks2TCtWVTdsQktCYkE4TjF5dTZ3WlJuZm50VWw0aE9jc0NGOTlCUllWdG9NTjY3dFRUWVg0ZkpNUTFNbVhnbmI4NXB4ZnlwMHRrRGJRQTZDdUc5N0ZlZXkzWWRMRlBhUDFBNzJXWEE5bitVLzQzZnZoaDRXNWlkM29iSUppaFNBMUFvUFA2Wk9Qd3B4RGQzSUhlL2c4ZzUwSWxXOWsyZ3ByTjQ0VCtjREp2bkJENlhQNEx2SEJ3NEhPNlVHcnc5U3R3dmFYdVVmTkdTNk9nemZ1NTRjVVhNazZRQitBKzRrbFBTVS8wQ1dWYmNxWm9xc0FWQ0hOeFlRQ0d5c2tTY3hGRTNUbTNqT3pGWVNueWlZSTBza1oyb1E3bHEvZ3YyV09MZ0VVNzY0dzlCMXR4aDJnSTdhSDQ2Und4S1E2c0pBVzI2aERnZzF4YjRMenNqeENYMXVrdGZDeVVSdmMwTEw4UnE4OG5RZ21FNnc0YzU5Z1V0c1ROWkRQWGZiUHZqdU4zTi9FOTNrbWUzSEp1Rm9GNFRRWmdwNjMzOUdxbTNtdkN1aXdUb2hTM0NBOUxtUW1vS1RKdzVDQ1E4Zit6Tm5pYlRvZmQ4dDhHZGJzNEwxL3kwNHdadTJ6SGNCQWN0dzBYU0lsaU1BOVJiTm82b25ocDllM2gwZEN5NlJQOSt1WFhtYXROck9qN0kvVGZMVldnaG5lcmJYdVN0UjlndU1ReWttUWJnRkF1ZmJhZjFiUVhjWkFEckQxOWpOZFVJMTREWms3MGx1TDVvWkxPajFTQ1NtTEt0S3AzUGM1VThiVXlQNUswNTRaaEFSbjhPQ1ZmQzJsSjBYWHZQS1hESXhZZUlMbzU5SVJ6aXRwMjY0K2lnOE1VN2NqVmFZVWYyN05oRG55SlhpYWNDQzJrSUphYSt4Q2p1dFpUeXRwMGVob2FYbGhpTDFEUHpLWmgzSS90UmZ5aDVtQkw3dzd5NzdReXN4SFlyL0Q5N1o2UlRUQ29EZ0hZR2RVM3dLYmI0VERVcUY1Nis4NzA1eEVGSldxcGNjUzZ3R05yVVJnSHVDanlvVjFXMmtHK2psbGZMZXllZFFsbDM3UmFsQkg0Mzc4VWd3eDZYUmRldTMxa1hsSFQ0VXB5R0ovMjRrVVNHT011S1IyUTNlOUdmOHNySXEwN2lZSEg0VTlZUFh0a0Z5YnkrTi9IQXJXdHpYeFZwc1I2VVc2TW5GNHJSdnJaSTBXbUtjRlBxRUdWaW8yNG9QVitnS1UyY2hTSnAyRUlvdkZXRTUxM2haVTIvaU1vaFlCbEhqRDFnRCtyeDlvbjNYSlUyVnBMYTJmK3pTUjhnbW5VM2xTblFIUElXMWFnVnEyTENPdnlRNlI0TkpGWERoL3hEWWpnVjlsL1BxODczc3NRVXQrOUJSWWtXYmpJYmJuWE5qVTl2eW1TVVZzeVQvRHk2NmVVcjlBRDBTb0RZVkpVVzJLYzJadml5WlhVWm5YV1BHSXNmbnNoK3JYaDUyYnp2M0NFNjlDTkJDSW52T01zQzhjUmZ6eU90M1EyQ2d0Qm93R2d3QmVFYXJwUXRPTWo3SE9qRkMzeXo2Sk1TUHJDaUFwaDlNVXdCWXdrbDNGemx1V3hQaUxyd1ZYUVFmUm9vVWEzWkM1cjZpSXpOWWxlZnN3SkRHaUlNMW01ejJNdmJYRVdieWE2RXRBVi9ET2dKa21ZNk9QbFJsbVZOUENBeFJnRnRFelVoTGQydS8waEM1eGh6WVZ0WFV2ZTNFbU95NE1WOElXR09sRXgwS1luMjY5NmtuUVg4K2ZJV3VUUlZmaVQvQ3RXZDEyUW1USE5Cd0svdzVkdlIrWmV5WjZTWHBRbGlQaTkrRFFJQlM4Ry9iK1YwYVlZZW5tRytQNEhEN2hOZ3lZOEZ0WmZLb3ppeU5CazhjbnJzNDdXNWNBRnhheUZrMlZzcEJwV1lSVSthRWljTWJDNE1hcVNleG9YT2RBd2x4dGZqWVNkTVFxbndZS000L0tlbWFDVHBSWkgyWGdCNFpBOFVLb1d6bE9icEVoQUMySlpldjBRRUxKc2NkWGZSbndBcVNPYW8vSEYzVXNGam1XTlJqa05XemQ2dU9ETGRxTzYxeTJYSTFndVI1QjhoeWNBc3JhWVlBdWkySWdHQUNoaVp2U094NmtLekNNSXhTUXdFb0YvRXYrSEpOMXF3NXdpMzZ3bW1lWVhUeVF2MHduODBzMFdNYlp2K0ZFNnhyTHRUbmpaNHhVMnJGdUlzOS9SRlhFUTZXYWJiZk1Qak9MZ1c3UlFTdDI3WTY3dHJBRHFVREJyM2ZzZnJKa2N6bWZmdVJPcTkxd0JLVXVQTThoc0FNeGZxUVZkN0pNaDlpZGx0VFRtdGlXOGhNMGtyZ1JDdnhPdjN4UW9xa2IvaEF0ekNOdDVnM1ZHdGVqSmNuYlZHeUJjdm1tVEQyV1RKT3F5b1NLQjl5VGl2Y2Q5ck1aOXRsWWhjcWpzOXBrUnFPTUx3L1dLWklUWCtHeTZWVGo5WTVhL1NpUndaSEM5ellVdjBmWWpzZDkvb3FaWlF3T05FeUtzMTc5clUyM3pNZmcrVEtQem9jMlZuNDVOSndyMlFHUUpabm5QbUJIdUFSZjBlTXR0UmNFbkhkT0VpMW9YM3BFdFUrMGhhMmliRW9tSzJyN0h5UnVmY0w0MmRLbURUaDY2cjlwbWs5ME1idEpjVkx3NlI4WHR0cHFhMzl3SCtER3hET28va1hpNTlZQzVMMGdtQ3Bja1FGMUFNV2R0Y0VRZHp0am5tQmhPMUFUc3ZPTVoyNzIyN25OK3VnSFk1ejE0ZnQ3NXoxdFlrdXJJc1FqbXh2VjJIdHpiekVJSmxXdTZOdG9QUUZMRmhtZE5lRDc5UUtaMGE1ekNFcDA0WEMzZ0VPR25XMEQveGpoaFYwV0NiaE95MTNhRlpmampjQ3ZFaWtBbEJGV1oyWE9yM3NNRkZESEZtbEUvbVFNN2k4VTFrb242TThNOTBRMGZYdlEwRE9KejZkTkQ2WXBtWVJ0N0NXUlNWSmFoK1ovaTJvaUN1L21vSTkzZVBxT1c2dzZsUCtBakMvUW15TTA2WFFuZU9QbVFTdFFjOEdxTWJSd21PeUlLSk5NNmZzbnNCd0FjOU0xNlNVTFNWRnR2SzJkdGVTaDVEbmtSaXdKRDlaNjJBMG5WN21kUDNhT1BURkhWQWMwajl6YXJxUkpFRnpKRDdoK1ZmaUs0RCs4cnRkUGNrcHlWbWJiTFQ5YWNuVitHOFdsNXhtNkUwTmtWeTZJTHhhUzRKbUx5ank1TFpZSk42c1dYODF2Z3hnYUZqOHdrZzU2RHRyM0JlaC9IZnpSVGZBczU4eWp4dkVIYWNNTjMvODZ0SWpCMHROZWVadDFxN1krT2xGbFV4bC9NZi9PSHVuL1lMSnQzeUMwNzlZYW5EUnlGWWdsdnVQK3VCQ3Q1OHlPNnU0WldRaDhzcUpFMEhjaUZIdFR1Qzk2anA4TzR0YTR1bU55dUJMamcwTkF3QVpjZ3lqbEVUc0NZemU0Q0JMbTFzdVJObkY5eURFQkNkdXZJdEE2RzZEemI2R2hzZmliZ0x0WXAvemwvcVhtUXJmQUh2NnB4ZzNHOHZtYXFkQ0pSdDRnY1JrVmF5UGpJSmtQS2d3Uzh6enlLQUd4WHNORm1vRUxWd0xjQ3I2YUpIdkVZNTJqaVlvWis4azl6YlVqRGVFQ3o0ZkMvdkhIQXRBeWFMTVlzN0lHbDZqQll0ZDZUbGs3eTdscitwTi93OWRsVE5OdFJmWjc0OUJqd2dBUmd1bk0yN3BZUDlUS0k5VlZWc3dISVRaWTVNc0VLYmNIdGNqSW5PNng0djZETG53Mmh2eGFjRFExRldBUnJUMG9qcjVNR0VvYUt6MFA4SXd4WkdIQzdWZHZZTDBHVnF1c3FLcFdET2E2cUZjaVl2cHNnMVk3VG96YVNmZmYvWVdxK1R2b3ZKRFJXWWMxek1WcHlZbjM5QWZzNTcvS0xWcTVsVHFESVRvKzRoNElKSkhyRWExeUxmaHF2dzNNYWdNaWZoOWU0SVRldnc2OWNxM0Q2UE9MelN0MHU5cmZaL0tjMlRiMjYvZzFOMzhjcFVWUU1aZEhvL1FLalJtZk1ZNC9ucTBNRlQrU0g1VUdzOHpMUHVrT21UaHZnMVZQTHcyWFNFV0dMUVpyT2RIYlZzZWVwcm1VZEtTL3pLS0hOejB5Y3Yxa0dWUzNneEN1a1p2OVRVUlJQS0RXSGpLNGFxYlM5REVUc2w0UFA4bkMrUDZRY2thUENGME1qNld0TnpRdDM1WGIvalRIRVN2ano3c0RHWGY2V01YZExRRUxNdC9Rd2xUOUc1dGZoWjk2SldPK2Judk5OUTFER29LZjZwRllOWGZDRGR5cnIzS2NyQ1pRVWlGK0lWNS9PYi9PSGxMdjBjZmhldG9BeVBtOHlGeklGUFcyQXpkbDE4ZlV6VzVrZCttYUZKWURKSEpTd0ZRZWw3aERpcHlxMjg0YmE3eWw0Nk55NWI5ZDVDOGRmQ1pnYjhxNVgxNUhyWTFOOStvQ2JUdXhEdkJnelo4UU9kNmU5QWpvSExWdHdTUDRmOVExaVNCY3FtWXJJM083S293ekcwK0g1ZXVsSXE3djNuU0hoMWVwYzBVMFVBOWw1b3k1RGFoWDdCYlJZUURtUTY1aC9VUUszUGROTUN0ejI3MytwZGt3bmEwTXgvd3R4OW9hSjFQR29NeEluSGRYR3lCMlVrMytLdjVsMWdaYWY0OWVRc1BENGFIME5WYU11eDd0RmNFcVR2bUFBdExqdERlTVJvdzdJd3RsWUZXSUtENUdvNmdKamF0bWRTRVlwV0oxRHE5ekJoRHNZMTFmVW93Umw4QzZMdm0zeDNCWm53TlBTSUhQUy9Jb2xtdGNnaFU1VmRDZWQ0Wjd5dVFrQ25KcWJCQXVJd01oV1J0L1pNa0JtWDVDVk9naWtiT2FBbWFnZWxscUJVdVp3VzVNQWFyclFqTHRTQWRSVGwxam9zL09iV2VVL0ZwZWQvOWd2Z1pobmlzcWt3YnpsZ1hGY2RBV1lBeCtiS1dvbWo5SEkzUFROZTExYzYvRWp6cU1VWHpPTVlxWHlRZWxWdWRjMzhpeUtOa09wTFZMM1lTcTMrNy9scHgzaTdqVVRYZ1JDWEhjTXM0dk56dFBzSTlzRG5xSWpaS1pIM25hM0tEU084Qm0yUk9JNXZ6aGJnZUlhVDlrZE5ndjdTME5TQTZab2xmcUg5NjF5LzB0L3p4OHZxMGIySHJ2T3JsWXFjajdPRFk0cnNLUVVya1MrUXRXYTJRSmhkcFdlWDA0dWlUWW1OYStJaHU4T1plMkdVL3Vsa0E0eks3enc5MmdKU3kwMjZMSVpvcUpaVWZTVGV5Y3lqelpOMjFtNCtZTnNub3BmNlVCNnc3Rk1xUkxoa0VQdC9zaDFrQ21OK3VnU1psd3dvUmRFMitwVnFOZ0RMdi9lay9GMDg4TGpWQ0xzc3duT1E2U0JPdDU4aHlVakdwbHVlRTNNMUVsZU9CWUU0ZlRxcm5UVnA1aVBGeGxEU3RWMjJtaDJoSVk5RmlSSWxvZFl2M2E3VERqZ2VGYlAxVlZ1dlg5SE1RdkRJNVU4ZWVOUWkzNGlNd1VmOHUvMHdnYVQ3b1pyY3ZIbXVsQ05ybTFkRWVHUXhwVjFPMUhZTko2VGFMN0JpS1BUblBtNW5NekxiV3RBZmVvWm8xRXlhZU5lM1BsY24zb0xlclJlUExWbzFKaDQvVFpYM3hESWhPdjIzQTgwcVpVMDlLTS96K00vNzI4TzdsT3NtR3IwSlF1Ky9zYlBnM1pLcXl5dWlWam5qaE5uWFBDcW0xVUZkbjE2WUF0S252UllxVlFLSk1DdlRPWXFNeWdmVW53TlVzaXN5STJiNGRCUWd1cjZ4WG1HbTRrQ2VodlhqeWlvc2lBUmVVdnZEQ0pkc01UeTVjaytXa24ydVRhcGRsWU1CQVVlRkd6c0ZkejJoSjJiZVEvZzAwZWsrdFRsV2JHM0N5R2VDL1p2RlBXdjkzRGNMbmtSL3pvc0xvb2lkR3EwcmhBd1ZwbThnbTNoZmh6VURFNzRSVzRWa1dsMlUzRyszUU5lMEdySlRFVnlFVEhuRmhJMitRRzF4UDdqOHFkdW5FS1YzcFJGZVZZVnlHNFUwd2w1di9YMUVPbGpLOEtWcVpQQTFIWmkwbWdqYlpJUlFUdHY5OGdhdSs4TGplNisvRi9zcHorTTMzTWdDLzhnK3UrQkxuSmZQUGhSNk9RWkN6bytJSEd1STNURmx6REpMMUpHcmU4SmlTWW9lemczaXhiZFRHSkUrWk03UkJYVkNSd253UmE4LzN1b3dVSnk2RlhBdkxqK0UzYWQ4RWc3ZjJaeXd3RG9VbkJ6UExhNUVOMHEvemtLdjRwUVoremhtZG9TSTlTQTFNdVNaL0J1eHpmcWhaL3Y2TjlkelptdzMzRHczVWgxd08zNC9aNWs2WHE1NWNibEZWVFlNdm1xQWYyU0hpNXFmZDdGZmtEczY0RWFJQmU5TlduOU1aZGY3RmdLY3lRYzNKSGtuRnFEUjd6QlRPMnBIbFRzMzJ5WFQyOUZiZWFQWjJSRGovRUNTZXdwTmJUYWVuQUhyMS93dWRVSzFKM1lDMXZxVzY4ZE54cU4weDEzZmthVXE3THkvUVRLRkZoa1g2cm5VSzdEOTBQa2xwYmZFWTlRTjVBNTVRYk1jaXlvTFpmbUlBSnNkY2hTVjkrSXEzU2ZVdytaRmJkd2VOUGYyRUVPcXErcmNSSklxZ3FtbjY3WTNLWlF5MWZvRHhLQkpsbFhWWGpucE5tWFlqQy9sMktYTm9yQ1AxSEU2U1hlZzFIYVpYRHFOZ00zQWp6dVJPSDNTM05xRVFzREdDVHV2Vlk1akpSZ2wwVWJIaEx5eE9pVFNFaTY1OEplakxlTGZ5QWU2WTZCK3NUTk5ZQ1ZKT01mbVBxR2cyTitOc2RxTHJPTERraVBobEpvbkY3Y1Nxa1BQRTVhK3RXQS9CdmdmYjlJdGxva2xRWTdKTVZWd25IUTBjUFNoZlk4R3o4bHpqMkZnazZ3bFBLVzg1Vjk2R2FqcGx4bkovckQwTlVFZk8rL0lmNkZCcXNBZnhNT1hGWm1acCtwS3lvV1Y5RjZKMTQ4V1Y2U2pXR1g2Vnh4WVFxK0VRc0xQbDJWMGNGdkorK2QyRmE3WG5VaVFPeGlJT3A5aGVQOGVOUytFWlgzS1JPU0p4blJ3bGd4VGRIMWYxNHlPYUF6RDFPUVIrYnJqTC9vMEQ1bU9KQndmTnlRNVpTMzlvV0RhcytBUzhacmtrSVlnWVpHUHlMMGF4T0RENnVYZHhWTTlJWUhQc2lpaTZ6SFJhYTk5NWdqdXBKeEgvSGE4M01ob01UWTdScFpaVEN3NGxDQklCQnJKWW9GNnRWVzROL1dETVlOak54QXA5SVJaR0FKUUZ6Z0NRcnhVckFQSnBwZWxnT05hSUVhN29WVU5JQmtlYmE1NWtEaUJXbWJZR21QaE45cithZjRsbGZ2RGxmNVh3dDZOOHJLTmg5YjViRkRuczhGc203N05zbEI2dCt4RzVkQjVzeFE4RW9JKzVxZ1FqM1lwaTRqMG0vSmwvQklqaDEzRlN1enM0YmY4M0hDdUVyalp0VWxiWENCZlVaV21OQTFhQ0hscmRsSGN3WHUwOCthTjhwcUlGelVScUpycDVySktmak1WYTI0RVoyNnYxSjNOa1V6VHM5TFh0dm9pUzhSTk0vdDh6UHRSRmd3YkpDMWJLWmRtWkNCL1k1U1dFaFpMUGZMZFkxVHlrM2NkeENLZWRkUHZxZGc5MDZWb3NkT2gvMFowcGVSL3JMa3hTL2FSaGpZMTc5b3Ywa2s3a0UvQVAvQ0pYc1cyWnpES2NoM1dEck8vS29kU0t2STFFeFZhblBualR0aThqZDRyeW8xSEZVczJLN2pNT2dUVDEvTU9XWm8vbVBCeFQyVnpoaSt1d0RRUGsyd0tuK0tZQTRCSFhFU1VRQXpraVQxV0tIVW1RMTdENHIrYndZWVVOcWVZNk1xVFlXYWhoNlA4V3FKaThyOStUQTdIMWF2VUNHSHoyM3o5SnEvYThZOHVwK0FHNUkzckIwN0l0dTdKUjYvZ0R6dStaTDBlV0p3QjJuZUhBRG8wb2JWQ3BRNkZ0N09TWSt6UzltK1YrVDM3bW1CdVFSTExSVGZlNFIvQlVKbWdxa2dvWmdOK3BFV3R2QmdncHBUN1IyTVJYMEN1OUszb0JXNDFDalAzMzhmWlJvaEFLY2JubmJJWEVBVmNpbzBGdmlySENKc1ZLNTZNU0UrSHJ3Uk1PcG5ndzMxS2ErZHowdFR0OVQxaFRjRytVdFIrV2NZWWVadW5uSi9WSVUzUUdRM1hnQlY4dEliaE9LQXFZZForVlg2U0Z0cHNsUG10YjhaMW12NHRJS3JPYm5zcFVJajQ1SWtxQ3V0U0doL2lJcC9LbkVZeWE4S1NrN29BZ25pZFV6SmJHVi9qOGtRRWNxWWNwWGZXWm1IVEpMZ2g5R3orWm1NR0owQ3B2VkJBY1lXbmlqcit2WXdZUCt3MXYrKzJZOFA1SjV3YXFoMDBIV3JRbHdQRlRKVnFhd1FlZEFObUxRZjd0azkxTDVielZxNjRvMXhqdjVPcnF6MWlIVHhpQWUvRVo3RFNaVHRldmdYSWFXcHZCS0tSeWNQM0hTeS9jUjdHcjhrbTlRaXllNkMrYlZDcTM4Qis1ekxPYjZpOGdNK3JYWkdDRnc0MkVNTTZLRnRicFZnV29uRGVxclpaeU9aeTZXYWZaUyswSy9rZXdWN3BpV3hudzN5MEhTbVFtOFA3enEwVnM3cUpjT0J4Lzg4VWdUTnJwbE9QeWZaUXJFd3EwaFZ4L25UdUU1eUd2ZTc4SFd0aFhrRWlSeHhKQ1hDS1gvcXZvV3p4eVNYZEVQMWl5WWJHa2JtbmJibE8rWDl6N0poNFZPOWJJSlVUcHJWOTJab2Qrdi9Uc2hsQmZhREpncEVyVm16N2RKVXlhVTNuK0pXNmxpS2RWWGlOVG1iMWZkdDZYQ3liZTYyWi9MNkxVQlhPcURQR01SVk15bkw4YzVWT0tOOFhzQkFqQTI2MHAyeEhQOHJzSlU0QlQxc3RVL0VTTVlYdUZxdXFpYWtUT3Vab2ZqSytUbWlMekdMZFVCcUZaOEZIYzJGZldqc1BNR2NTbWFWNzhYK3pKNG9yTDB1MnhFdkZIUGxzWjVhbFpjaW9KTGZyWjA5SlMvUTExZFpjdkVTbUVxd2FnMDRVOGc5clRodjlCUUNER1BhQmVDR1NPd0c5RWN6SGVocmVPZUdHWVE1bG9iL3lqV3JPWS9uT2wyMUg3cnBkd05FZzQ2Y1hpc0pVbGdkSlpBWEhlcEZMSUw4bnpLRTNGY3grQTh0ODdFZzNzVzdVcmFlalRTOWtjQW05YVliL2xlTUJqNE5nQXRXK1pEbzMzS29mdGRUcEpuUWtUaXpBKzRPNHBPMUJYVjNpZENXRHg3YkdvaGhMV3Y0OWovenNtQXVVWmdiOWN5VkozV0M5UDEyVURsaVJtSEJwaEtZM1Zoa1VLK0htWE0xMitLdEY4NytPWDc3VFBXdU5RTFZYWkNvWWtnUGlKVEZnRE95ZmY4ejVGNUtFVUdkMXdEMHYxZkhEK3lYWWxvU0tCellHUGNQS3NTQ2hiakFQN1RTa2ZmMzlOSStWalZzaWZGQkVvQ1ZIT2lBQ0c2REZOOGIyRlFkMUd6V1phdUJnRTgxRHVFYjExamtTbHFnQlVZMGhuekxTQjh2VmxUQTRmZjQ1U0NtUXNtU0JlUUNVY0ZCaVhpWmV6bGdINjVzNzhFbHY1S2g2cHY5S3ljTUNacFlESS9yZXQwN0N6ekxuWS9GODRmT1puVzJGVWNkSG1PcWhxR1pVczZrWUxObjFKaTVmU3hoWjRpSG93RjJjTDgzZFk1R3BGWG5ySS9GUDF5M0NxL2QwdU56MWRJZ1FFczBxdEZrWjk2Wk9XWWZzNTE2UExzVjcwYk5nYWVOSVB6ZytsQkMvemVMTFFZWWFsWVZJQ1piUmxSQTlqZk5IQUZqekVDZkVRTXlYNVh2NWMzSWx1MmhvaHNZQjJMbHlSa3czVURMcHZuUnpRckUyZFVmRW9DcVFpcUV2c0M5UjdYaUhNaVRMM0tOanNNRjlCY1dJVlNnL2ZKS0x4UTg2RTdCbnBHVm4rY1ZNRXpMZ0xHZjg2cms4Y0RhdVNkR3N0c0F1ei9WcG41U2Zqdi84UUZqK2FqaU9icXZpZkRkUTNPZUdYa1JBTGhOZTRRSkdMa1V1SmNOeDF0S2Erb1hXem1TaUVOaUJoQUs3TFBlYnMwaExvSGdmUXdKdUloazJmb2tVQUE2eHdVaCtIT3JGdDd2Y3JFUElmNEJtaXNkNzhTdkRRWTN1T0lqamxLNlh5TUdxN3V6K2lqQTB4YWNWeDZHTFczZnJKSVZRQWZvWExnUDJ5bTllSXZDci9hVG5SQ3BqTW1LNGQ3VkhDMHV6K0dBc1l1SElLaVY2ME05d0hla2VGMFVhRk1HZ3BzNlNrUkg4UURJallyMmljN1o1c1lvUkRiQUpmZDROaEd4ZzZzbTc3Mk1sZlQrdEh5eEtMTjB6SjAyb0cwSEh6UjF6WnJkaVEvMFVjNUlkeWlvbURwZjA5M0IrUmRSdzBUdHNOenRyT2p3d2hHeTEzNDFvblVQa2x0TFRPQXlYTEt0L3IyVFJtYnVraHRGWFBCM0NiazBZUlhlWWUycW10UTBpQ1N0OFZ3RkdwQ3FwbUNyTlVXaWh0WGdncWtKMnVDNmQwOFdmUzR2UWxaSDRySnBOb3Y4NE8rWStSTnZJbFFkSjBCTk14b1dUdGt0S1EvbS80bUwyNFQ2STZSWnVPTTBYTTY1RmhSaXdDdXRVeUhZbVU4bkVmSFM3a1JkSk5tUENBU0d6UUd1WTlVbm1ZWVpqV3cySVFnY2FmQ3RITytiOHZzNjNiVDNKWlQ3SXVaVmJJMG5aTUo0QlFmVVVyY3V5eVU4eWw1TUVTOUdwQ2RTdXk3Y09XUnIzelpRTzQweVl1WFdEeWVJRkI1SnlJWVlBNjdQM2NLQ28rcVF6VVhuby83eklNTU1vdzdrZW1GYm5aM2xEWlk4bEZ6OGlPUEkySGVkUElPNVRlV24rSmwvN1RHN2dwOTR2bElMUmNMUkI3QTIyWTM0dWZKM2NOZzgyZ0dzOUYwTXMxMlVvZmlNcG9nanZ2d0F2bjRoQ0RyN1Q3OUlaemlxMGtZRitkUHRQVnRVZ3dFU3dBSFIwY2JJMER1M1VzUFBYeTFGdzNIMHBDNU40U2U0ZU5KYkhzVTFJMEFrazJnM29WZm4rUkZZemVPSWpYMEh3aU5SQmpDSThGUWtZZ0ZJUC9aWjdDMDdjU1VFVjBOajhicFJrL1Z4VGVzRTcrR0JwNmQwRy81WHU0R3hNa3FtSU9UcnRNVmlLSmFnOTdpZEVoUldzS1BJL0k2UVVQb0dnQWZRZXVVVkkwQXlaTGpEL2xkc0t2NUNVQmc5UlJQc2RLNk1Nb1l3aFNHaWJndTljdWhZK2FKcXB1MEtMc0YzSWFRWlE5NG4wczY0VmRtVWQ3dmRYSHB3cG0zY0dueVhGeWFaaHE1QUw2QmRiRmp1UzIvMkJGOEVNVXd6aW5wOW83aFdOdE1mNHlCNmZudjlqSnQ0d3hZS0p1REc1WnN1Rk03SXlvSDgvVTNhWm5uNWUrbVdKM2dqTk1Rdk92V0lmWVMwcXJSQlZVNWx1YUMwZFc5NFlRVlNuYVZHVU5VY2VJckRpUURQSUlRRGxzcFk3YnVBcXdaWk5iTnNDMnhFQjVrM0dlZEhzczZqekNnT1dlajFIbmdoQ1JlV1dvYzZpYkpyV010TW1FMDVuL1pHNzgwU0NsYjZIZWd3aVBSSmJMYlpaMCswMXFJOHhFS0E3VU1nZE96Q3I0eTArZVk4aEJmRmhZWUphOWJxWVMyd2RoZkVFVjZDWlV2Vyt3SGIwc05CYlQybFBRY1BPQ3hWUHFFKzg2d3RRNElTOXZybmdadzdiYkExLzZmVEVpMFQ5elI4NkhWcDkwd3F1d2p0cmJZOWRxZ1MwSFkyM0l1dFJLUFB2RG9leWtZSlNMQ29wVVpoWlJhRExMajhoTlV6bHFUMkcvWnNxWDN5ZEZDQ3lxbG1heVBWWjRlWGx3RHdPVFUvT3JJN0lsSWFSaFRPZDJTVGhDRTQwRGs5RDVrdlcvMnB0MHYwWXhLYlF3cUJMc1JxeW8xelpjemNaV2phNUNxYWNjUUZ6N3F1eERuTGFZL1owSEJwVngxSnlVYlJFUkljK0tvYzRxanAycUNuWTRGWUhoaUVPTHZZUVBtT1QrZGJ1QzJDcG5Wd2JDQ3ZLVWh1RjB4MWZRbm44ek5yek1GM09rY2xickd1OGRwYUZpRzdhTUk1bmVPUWdmTnZVOFIyYllDaVpJci83VURId3ZkQys2VXFPWDRha3pIaWpYekVWNFFWZVgrTEJJcnhKZzBqOG8xZnFjRFgzQTF6Qkp5U1llRjR3VWlJSWFhNDNXVy9HbnM5cVNMRENVYmxyZG9RVjNNOXJlSzlBV3dYbjhlK3B5OEVXYVFoTUxUaVFieXY5SDhLd0ZsU2FSRVhNdjlmcXVMUDk1OUtDME5TTkhiOVQ3djA1V3RxMUtNZWhaZWdpcGR1eHU5eGtpYkVQOUtsK3hVbUlpWXlyTlVyWTVqbHlKdFBTMWR3VDc5MEhydjgvblM4bDVWWjZxVnRHMnJCLzBxdHVRWjd6OHFlV2NLelhDRjRObXBnNEN1K1dzSU1ZYy9pYkVkK0NHbUVHQklpZ0k4Z1V5L3JxWFY0dkVnaEJQaTI1eUZWb1ZTdFVLdFcybFZFZFNUaDROSS85R2czM2NWRFBxWmxqZUdtYWtRU1RNMXBqdFFValZYdXZCUmFjRitVMEpyemtodzR4QjlzaWZZUWNXeG0wOXViWXc3SVVwVEk0aDNHZmhtNkRoc2w0YmFyUlUxRm5oQUtDZjNpSkIrWFFPZTR4YVVnVTRGT1VaU0JUTzVsb29zeUdQOTNQdlNaTVAraEhZUTh0aTNSdkhmWVVqVDFNZDM5NDJvZTRIdXNpTDcxRE03dG84dWVSRjFJci9CQXpIRkJNOHc5VzJTeVFUemQyc1JwTml1ZlFIeGhycW5aMGxPWTl2NnZBd00zWW5jUUd5V3ZJTkRmOXBEQVpqUFZuNThOU0Q0NVVBUU1MVmNHc3NaNDllQU1YelpaZGwwY0xVeW5MdXJqQ0F0TlpEWWRpbHU4TS9OQVFkSjJXZmllTFY2S3k5Z3NUaXQ3bGNRS0RFeUhFOGlXNnkxbjYzaUlhODFucXJwM2dhc1ViOEFJZDJma1hHWkNUWUg2MGo0ZFVaRnFia1ZFMmd0azZoOWkyc2NvOEpJUkhJNVc1a3lxaEVqUmZuZEg2ZEJRU2F3MzYxQTJ0VGd4VG00WjhOdy9wVldVdHU1VHh4VkZPZ21UZWJLQmZkZEFKY0JXelkrZEd2NXhRSlRtVUUwc2xIQUpQRXVaRGM5QnpSVXh2bjRiR3gzblN0TW9BSTUyWVdzQ3hCRzFjS1FiaGtQWWtTSFN3ODdHcTRwbktOZFRwUGZqKzhHbkNPVUtnd3JGSXMzS2NqM1RWTmtKbnhFVFg2a0xoaDNpcmJtdnZZeWJwdHdRM0ZxMUZ1cXRiZWFlZ2Q0ZGFESWo4NHhlVUg5R1NsRXV4S1pvWm10eDhMT3N0YjFZMDZFdG41SDNFRTJZR1BKV1gyQXRMcVFjNWJHenJnV1lwQ3BNd1U1K2d4azBYZk5TWDVTbWwyTmxlQWZjQ3AwZGwyZytFVnA4dGhIbHZXbVA4dFBqSjJIY2xhME9CcW01dTlIcUgwcDhNem5vODRsNzBlZW01QThxUzRVYTRjWmxiMFk1TFBLd1RCb3Z5VmUrUWo3MFdmTlF1SXVBMjhFSzJQZ0JJbjVURzlFUTk4WTFscXY5OHpMVFUwNEsvQ3NHdVVmVk1tajZibmhEVTVuby9Od2crbmRKSngwc0ZMS1NEL05QU1FvaStodW1MRnVqYUIreEVJWTlDa2ZXMDAvZWROSHQ3WklvNUd3c2dOMjVKMUhPTVYvaDBzbzZ6UVM5VjFVSFVPdzkvZmlLWE5Wd3VoUmhxRlZHeUx0RDh6NFdDVHhjbHJ3U1lNMU96b2p6cTA3SGs1eVcrc3IyRUgyRDYyWW4zeHloWXVNTHIyTjlEOTJQdUkyb29jMzhkSVZtTnBybkNTb1c2VitpZTNZbTY4bTB0UUdoblNnbW03T1ZZN09Fc1RWSGx0WmFJWm83cHdRV2t0cWJvUGxuYUR3Z3Z1R0ZCb0pWMmVZb1hQOHRiQWswT1FKNFFLRzFVZE5NcHI2ZHhRbTErQmdGTnZQdGIxSm9keUFkdjRITU9TbGRYRW81dS9OTmllNWlZYjFWQlFibEE1QTFWN2xoZUQ3bXBuZVdxOTB2cFRydHBDYUU2MFE1NFEzcFZPTFRVU0FrR2hOYzVDVlpFREZwWjNVSG5NNTB1U0QyWDErdklJQ3YyU2N5ZUx4c1RTNTZkVGJBcmxHdk5heWhCL2tpNXc1WGtaWXBaTWdDWDY1NmVpL2Rhb01hV0wyTmVaUDNCWk1vMUlYVWxPNVAzb1RSOEMraDNSTmN1WnR4SVVKVW9lT3J3aXdtZlZKWGtFeEFBaWRFNU8rNUF1MHJyQ250R2pFY0hZYmovT01ETDFRYnUrV2FYVWZKbUsrTzhxRWZMb0NQZ1pFaXoxTDNDQk1sZWZ6Q2VPYmVTbGkzN3E2ZWsrQXZYcWZseElOWWVmOHV3M25UUjZDRXI2SCtsTGU1N2JsZXNxK2p6M0FkbGVvNlBCcVVQNU5LRVp0RjJKTkY4cUZxcHlXWVhacWtuaHlIVVJTOW5PMEE0c0FGTjVhWThOYXRMMWZDS01OMEdjZ2JxRnpuNW5uWnlZT1psdmZ5RFVGUmplandEaytVQUhPTEdTZDRXWk03d3FHN2RwTmxVbE1kQ2RMN2xYWi9MSGVuWitMN29WbUhWRGJ2eTEyZW02VkhsWTFvZHNDRlRDeExMTlhGR01HS3oyYWxlV1A2UlUyZkVnYnJRM0RKSEZHNjZQQnRuQkhmQ2o3M3ZJOHQ4OFJNV1E0YTJhU1JoS00rZng5WGRsRHpXQXRNbmY5SGNpVkpleGdiTFNUK24zR0dFVnQ3V0FNdGRrN3hUOGNFUGRxRDl0SjdGYkgyNDZEeEl2V2VCVjRVeU94T2FuSUwybmNPano1cVNTcnQ2NGJUdE1sVEtHMjJHU3duT2VKNVpNMlJDeXA3dmY2QUhzU3VHc0NrL3pINUhGUWlnNW9CS1dKajY2N0dPdW4yVy9RRGpyeUoyRmZnaVJNOEFpTFJ0TlE3WkJZbHM0UXROUjFyTXFBZENGc2M0TG5MTk9xRlcxZGVwRU5GZ2xDZW5nTVI2MlFpUStMQ3ZaQXcwUUNQcERwcS80UGVQc0llMUhkc25sbzBKcTlITEcwbFRMT0M5SE1pOUhiNGRmdkRlSVZiUmpUVUFKa3dTeFNtUEJqKzRYMW9GczRqRUpKdnQ1RHptbG1JajVWSW9velFuK0RpZGpkaURiR2dZRjZIUitPSkdKWTJ6Q1JmQ3YxTlBjU3EvQXRpSUZkd0hQRWp1ZDV6aGYvVUJCU3VTZmp4OEZxYTdQT2FkWlpjR3RXMW9SRUtYUzFseXVZSGVWRWt5MkQydjdkcmNTK1ZDbHZjdnNNNnZVRUxJUjNaKzhRNTVCdFA1UCtuWlNnL1EyMENWRVJrZFkzQStHbktZNWtBWEFaZm1ML0ZxSzNKUGNBdGZ5QkswVGE0MDlXRlRlZGFBdHI1OTZ5cC9oK3o3NDVCVHp2dlRhWUl2eVlTd1pMbHVwa2pCMU0wUEdKRUp0RkdrNklYOFNpY2tMZmlYQ2I0QkRvakhlVktzakE5dTB5bzk3d3hzek5zWkNaNFd3eTBLeFAvU3ZycFNwSTc3NTV4OVMvREpOY05NLzJsY1Z3QUhOQUNZR0lHM1MrWlpWSm84Ym9QRVZ3eUVsSVV0QXhuODdGV29nUDhObGNzTGFLbThsaERLb2RlMndjQ083dW5LS0o1TVBRZWpKNUplaUdkTmswM3RrL21DeS9USXRpMi9jb1QrN0piY2hwL05udU5ucG9NOEdzZkxydlBBbk1JU2g3aVRnamVybm00dE9EdkgvRDBrdkdXWWRWUWFuRG1vMkFhaXF2bHg0ZlphU1ZXVkwxODZxRUdqdXIvekdENkpLQlUrWVRXelA3Vmh3eUJlbVpZTTM3Qy9IVnY3WktML29MNjRLd2JzY2hqWjlvblJENHBXK0RjN3BHaTRNSWc5VVN2azB1Z282bEltOTY1Ukdnako1bCs1SGV5OWpMSlJSdXBKS1ZtdUxMVWZub1lTR3crbFV2RU1ReCtkVDF3emc4Q2VzVEw4NjF4YmVNcS9NeE1qUGJIS1hOdXdEb2V3OWEreVg0dUJSdlc3bEhGcmkxaFFKUW5Ra0lXeElBZHRjbmtyRGV3WmliTldLbW9mclFiTmV6d09PRkpmQjh4ZTA5amRUNitlQnNpV1Y0MytCZTVYL01raDdDR0lJUXQva3Z6K0VwSGhWS2IzcDQ4c2pic1ZBcFZBbWtiLzZTN0V0NVRmZTdoN25wZkxaR24rM1M5T0JhWnhYWjhvMXY5SW1TK1NWQy9jWnh0R0RPMVN4OHhXazVJOUpiS24yZEZqY1JkNkhGc21wRnZpMnd1bit0c0NWSStGMUg3OEdYQW9ScDlVU3dIL1hXU0w1S1dXNnJxWENGMmVHZEFPMXM0d05FYTR3RHh3T3IwRDNGWlQzZytoU2p1eWZjZU9WYUgzZUV3S1czendvZE82TjU0Sk1GaXp2ekhxN2FMM1RZY1BoQ2dsZG0vQThCU2RyNjFRd0Vtc0swZnBINGtDb29VYmZsZUh2ZGZMTFkzbmVxYWFsWVBoajdOMmRad1FyZTVBT1g5dHMrMk1JN0I1YVZ2SXBiTFRKOEEyVTVMWjFqcHFWbkhQeFRVbkJ5QVF6SzRzYWthOWxZQmRlNlFGTnFmM01qcWR0SHlWQjBKbUwvUWVjeFA5SkZKYXZocHNhbHNSc1NJVWVFanlVNUQ3aW5lUXFGY2FKclUrdmZnamJ5L0x5R0tzdFFRNUdpTWlSZXZUUDNHd2xmUi9xU3NaWVJzMkFzOEdrVkYyUk1kQzRNbzRLeGxDczEvYklTTWRWQUEzTm1hUFdUOU40eTFuMkJGK0dMcHYya0Q1bVc5SUpFQkp0dFJXRlR2Tktabmg0VWlVNEZkenl1K3p5MUFGY29HYnE0QTNzQk5qODNnTGhEV2hZTVNxRHZTdXBFeUdJR3VvU210WVA4b05GKzZtOEJHdDByaWttckUwTGdBc2tTMFFYQlJWbDhpUnRPVDRnZ1lmaFR0SjBVMmRSME44UG1INm51NUJ1STVPNCtjRnBRVFBOZlhKWGZJSU1lR0paT3JNa3N1R0N2bVBEWDhhckUvYi9Dc2NRTUhLTnNBc2Fxdjk1RGNBbWhRZERJYVM4TVd6cG5RSG56bG84MFphTmhRb1ZxY3ZmbmFkVEFxNW9pK0RGQmJadmxZbm9QMENhcVYwRDNvdUg1V1hsSHpKMVpSRUg3TUpJQjA4LzEvc1FPOXRMRUhDUnN0Z3pMd2JQcXRYN05adlNRME9sMUxFdkdsVFZhUHZUL0VMT3lNdVp3SXdqd01Cd2o5Qi9JZmNkaWNZN0NxQ3Q0YjNrNXpubDVGTUsvUFF5MktIN2xudGRWVWQxNmRZcDBDNkgzMkx2d0NFRXhzcFgxWmh6dnpPSExzRDZ4cXg3ZEx3S2d2ODd3b1BEeHVKL3kyUlJCSTJ2TXdsNklTcnQ3VFBwTVFJeGMwbjBHQ21RUTJacjBobXRFdEljczB5S3FmNCtXM3llU1hFOStPcXYvM0FmTllHS0VVOHVCVVpLM0toa1hCSk9kNVRFbXY3OXBkS3JhTFV1TURtWTNIS2R4eHhEb01YRHFzY1JkLzRFd0NSY3BrREZuRTEwdGpmbS9pZE84Sm9HSmVkU2JWRC9LNVViM1Q1c2hubXhFdjgxZStTaDhGMHhoK1dDS2RrUithRUxxa1dnTGMvR081ZEpkcU10YzRGT21lZnE5RGR4MTVSN3VrcC9FOGxweDdzczgzR0VBa05MR0NTOC9hSEVnbkt4T2ZGZUQzKzliV0NqbDV2Rm5uWmlGSDQrQ1JpY2tJTDZVTEM5UFpnVzlVWm5RLzNRR2Q5czF6UndTQVBPTlpQQytCU0YxNjY0RU5QM3VRTlBpakF6UER4cm5iMFd6bm9tR1hITVdiL21DVUFybTY2Um8xUVV4T21iR1RHMXBWZjhGQ2p0Tm1WY1pvZS9hOXhIMm9rQkozOGhJdTNhcVJhakRSNzl1dnBwRTl6OG5MTURKbWlGUVBnRUhVT0taZzljcWltUk9zQjVaeENFNkxrcTdmaWFKaW9naVhWM1ZybWVuNkFuMVlnUEc3QVp0bUtDai9zT0FYNVViVWZqbTY0ajArS0pWM0I3QkV3dDUyYVBSQmJUc2g4ODJQOTFLemNyWWlJOU5ZejJFSlpQbXJDYzFScUxGVnRoNDhkTVZ6eENHb0pYOHkwd21ycDJDTGlmR1JQUnBCYmtsdGx0WHFOUnlOVTl2TEJLZFpkQXkvU2hvQkViRHJMRkN4eFpPczc5SGgwZWhYYW9HMUNIWFBlYVVTK1V1RG9WVGtLTEozcU9KdnJFWEFxQlE1cEx3TEliQ2w3MnVnYWVkYWNmM2ZibjJJWXFFclRwTnlzOUR1UU9jUXlFZVQzeExRMURrYmRkRnUrWUQ3enREejBVb2UvVERreFllQmRibWdrdEpKVXphblNHbHhZT3JpVWFhb0g5aDRzcXhjZHB0cER1c3VnRVVJMEN5UjFacmk5ZUhnYmxONHdkZW5XRWllMThCQTd4Tko0aGwwalhlWEJLQ3I2YkFVbWlFNk5BT2FlZG5ndng3MElmdjB3Wlh2UmVRWHZsdEJUNW8zZ3dWMVYxWEp4ZlZnd21KNXJ0WDUwVkc3RWEyMkFaWGpZdjMwR1FKanhyNXVueVdIWkdKVlU5TVNRbm5FNVB6SkFXS1hSU0EwaHdQZFgrWmxiNU0vWmpiV2xkemdvZ3lvRnFya09zQ1piZDgvNTYzNjJDdGJFNTJLem42MUJxMFpacTJoZXNSdWgwK1NRNE5mT2ZwTTYxOEpWTW5hNHlzaTdPa3JuVkYxWitnc0RReVZ1ZVEyRGVXRVdnYXl1ZzYzd21NVkxiWmxsU0JrSTJIZjJocUJLRVRKc3RNSDNnVGJyV1N5MHM2V3kxdW9PTVlDQ3dSUXJCQXhoWGRROEVuSVhSbHlCeHVZbTkxZzVSRkRLS1FOMzVLWmxZSXNKVmVOcFNHVmtUNWNWV2IzL2pjaEs1bmUvUHhwSVhPNlNzRjRjaFBleEZZakdjQmp0QU5VbHdONll1bDRDc2VnMnhoNkFGWXhpekN3RHRSa2xGTWlRekhoTEtUUkp1T212UFY3Y3MxMWhZakxXUDR3WHdCQjhXYnQ4eStHSVVueHRPeUJXT3daM1BoSXUzNzdPbU8vSjJKSXBZM0JnaHFYQm1zdkdscHFoZ2lTL1ByZHpBYTM5QnJDZThiamxyb0FqT2JETVJGQ0piaUdXRUZ5QmhDK3BuUUo5V0RraU5YMGpiL1U4ZTgzREdRYnFNLzNHZ1l5NjJOWlM1VnQzeDE4OUNJOUFUYzFXbUU3cHphUmlqL3Z0Mm13cENmN0RNTko2OEl4NFFVRUd4dXFvelZDc2FBZyt5TjJPTk1UdWNuMkFKR2lVdnBjRVo0RnlPZ01XNk4yTkgvQlpueFA5WHE3VnFWaU5WWkFKcjBoclFRTTU0SXRTY3RrWFJ3c2JQRkVzb25RcmhmVVIwdGwwRWlMU0N6YVRVWldZUnFOSktRbmpwWlg5UWRZV28wS3dNdmhpbEprRllIZmxDenFWNmErV25jdFN6YkZubG5IbzE2SklhOHc2cjRFcDNITm1zTHF1QlU0NVgrRUZCS0hJQXUwZjBpY29kTW5GeFUvOTJHZFNERDhyU2FoR216NnV1TkJCdWFUems0cFFSM1hEcjhvd2ljNG52Z1VtM1FxaFc1MjZHTjgvYTFDL2g3M1BQZERMSytlKzU0TngxU1g3RGVTZzhjcUlHVmJaWE1EMWhOdXBBU3RwSWJGWDkwL0V6bDUvSHlBYnBRV1lMQTRqTkNyRXc0YzBIQlp1MUVGQmNrZGwwZ1VtUHU1LzU0L1FBa2J5N0VkUFNzK2IvTXIrWUE0U1drT3lsTzBONHlLeUtPbGFnZVB2SThhOTJQSkNQb2RLdGRSRklpY0c0R1Q2N2c4UGh1VEpmK3JzME44QzNlWXJwK3daaVJzbURza0RWVkFPdU5kakFIWDB2UU0weTBIVmlWSjQxZU9neVRTTzhzaEdWVENuY3FtQkZScm5pZWxTemNtSnpUQkxQZHNJOWpNU3hyangwS2dWd2hrZE05V2U3NmxLQk53TW1CeDlXU2x5L1VtYXM3NjJuTG8rcUtXSWFHZUFCUEFXTHNkODc4dEN5OTVuOGgzNVdRWEpCbkZaQ3dQbkRUZzJmNjRkaFkwSGVHOWtXbjkxU3NYNEswVkRBbEJTR2JleWd2VXRvVlVVZDFSQ2JwVnZXTkhTaDYzTmp3TzJQdGdveUdqMTFvVmJPR01HMVVITEhPSFMwZDE5RmtuYlU2MEg4UWRtNFhONFJycVV3aXM1Q3U5NXZQM2RIbEthRWVHK2xBQkpJTWxnMGdIY0l6Z3FUQmJjeFRKd3hGdDJ1d2dmdWx0Z09UQ3o5bHBuRllYMG1hdDJoUVlUbXg2T0NyK25KZ3R2ZGpzMURsOGJBUEdtUjE0OG44Rkg5SEhCN2d1bjcxbll5VmlYanZBeXhKMGJaY29HOEpubi9YVnlyVnh6V1dQazVwU0hUWERYamNoLythY050SEpxSFFlKzBlajl4dXBJY3pRNVovZUpkRVM3eGlKS0gxL0hocHJCMFNGY21aeThoT3pxR0R0NjczSnVzNHNDOXRQQ1N2Ri9ja0p0MnZFTml1bFcwUCtwdEF1MVN2NkRZdWk4c21VZFlvdjNjZzBTZmRDSXNRZUpZdk92OUNxcGNueElSYVdiWjhEZzVBcWxzUzY5MFQ0cW5nMDhkNlhrOWliMC9OR0hrR1Z3VU5CSnU2K0dmbmMwdnFkTXdQRVhVeHp0em5NUjNjb3VQYUFyREVIeUpRVEU1YjZJbTRXaExqSmFWMUpBUWYrNFhna09YK2hYT0doRTExY1FtdE5TNHJNZk84a2NDU3IxUDNCL1VCcUlhUWp0elRsWlVNazRXZTloZGRGMXlhNGVocUZ1RzFVdXl1a0l3U0RwWk0vS3dyRE5KcmNra0xYYkl2Nm4yMmZ3NXdUSWhtd2VKTVdXak9vWUFTZ2xOSitPK3hJdU4wVE9mQXc2Y2dSNW5zSytVd3dSbUtTOGxNZmdOSmMwZG53bk03K3ZzUVpaeFYxQ3VYMnl5YW1ERGlySnArSFRjdElIOHBaZ3BrZkpTV0FuSHZ3U3ZLU0hrYkxLaGtOOFVVZGN2dWgyblgwWE16UVFac1NkSFFVYy93N0YvZ2xGRUdXSHdGM0ZYeHc3MFc5Z29pVWdoM050L2lGSGYyOWJRNSt1Sm82S3FxQ1AwWTdRTjg1NDA3cGs3blg5bC9YUEFQaXVOc3J4TmVtcGt3THFxUEp3MXpYYVpYcDF5ejVMbDlZeE50Qjg4MCt0RjNaekMvRmp3WjRSWTdEOCtoQlcyQWxWSGpBeWFGVkpueFBWOU42c3hXa3BXYUp6a3JXb1Bjd25hdTNrc3pTcHk0MDR6cHFLeldYTzBOdEFSc2FzcDA0T2Z0UTRtaDA3S2cvWS8zVmJiNUwyN05WaXI2Z0lUdkU5dk9CYmZFSzNBcFBVWFRsbjRlTUFrMmNsR2xJY1JLdUE1YjBJZkdPUC9SM0xTWVhvZ1ExZlQ4bnphRUdSNDgvOGlka1Q5NG1wK1oxT2VyRkdqQ1dWMUxQYXZHMHlQaXhEK0F5cDlSUWlmMTNxcTZobGFNS3pZMlRzOWpiNkhDQlVhWmJMVzdXcHVqNWhHemU1c2k3OGc0Uk12SVlPdVRaczZ0VEE1TDI3V3p6Ym5wL25ucHlOVzBic2J5N2l6UE5WaTFqUnZWZEQxVXZkbjhUbXdXU1lud3B6ZkloLzdhUEVrcjBrcURSSnJTWXNKM2I2M1B3a2xWekowT2RlWEdCTEkvcHQyajI2Y3VVZG5DZTZpR0hlLzZad3Z0MU8vRWxmUDJ1VWlDMS9XbnhtdTRoME16RStXNDFWMW1rMDFaQmc2R3UzNWpwRndWS3JJZ1Z3L1lJR0hsNUJscHRlNWEvYnVmN3VaZjRpcmNTNVMvdm1zTE9pRkJuM1V6eGMrVElrUVNPUDlOY0U4RzNuTHVLZHRWUlFZYUJLWnJoQTNCT1dPZnc5eS94RjRXMUJxRnhZQjl6Z1ZVeW51d3hkRHMrT2NkQlI0VjEwdTNEdUc4L2g4SzFBTTM5V1NnRTdoSWp4NktrSTAwbE9NanZMN1RFT2ZzSUhESWlJZGtTaUVsMWdkRVNweWs4bEVLbStxSmVCYWEwNHU1VU13RG5uYWVYVktQRzFXbVlPanBKeWRad1Q2QVIvUFRUT2M3eE5kTXJiSlNjbHdiWXN6RklxK3Z1bmRXVWNoa1N6VzdqcCtNbERMRDNrMjlsSFh6N3RHWVFvMEtLc1ZOVGVzdllyd3NqT09FeklVVEpJdmlPMjhja29OaDNJdjcxa3Y1dGUxRUViNjFiS3lsMzYrUjZVZlVHT3ZLY1BnOS9QeGdlaWljbC9zdWZCeHlQVkVTbUN6RnhDUmtlRFoxL0Nhem55WEhmOXE0TWVudURrcW1BNU1RaUZteTVyalFKazJrcExleEhhTEl0c0ZReE16NXZURU0xQVR2aUFrbkdFSmltV0JHL0U2cUlTVEE4cUNwK05zQUw3SUZpSWtVNXJRNEVlVC8yWFdjQzFCdnZJUHFydFZWN3lNaDN1cXhvVi9ZamEwSk1SQnVRR2E5S1o4cHhaRG1qa1pubmlDZkc5QjNybE9ISXMxcEFGaWxyRWcweDZTM1dpREZZODFDcHZJMTI3NDBVWW9wYXZtWmg3NDN3MUFGenRWN2pzU0prRExEVTdGTk8zeHhaWjBSTjZ3ODhxMmlsdlJjOW11ODdtOU8vOUlhS0VVN1d2UXFBUXArTkU0T0hPZGxmU2grUVZKQm4rY1crUW1HOWlodkFnYlo5b2xCcXhqSWUycWwvTFhySVJOdTRua25tdW1FTExaTUd0aG9QSmFSNkhLdmUrL0lQSlNDc2V6eEQwcHNvbzJjT1BsNmxVNUhsby9oeWxyazFnU1QwR0JSbitqb0FLSTFhaVErTzFaRHphN21aK1R6ZnAvZUtBQWcrRW16UnZCM083TGJPZ0hEbDQ3cHZoTVJJblQ4NEJ6VTJhVFpFV2pMWDQ3UlVmUldPdEU4Y1pwN05hd1lRUVRKT05WVEt6Y3ZJWVU2RTRubkVML09IMmlBejA3RkZWRmkxYnlCQkpveG1GcERkOHZxZDJueVk0NXhMWEN5UmZQZU00akl5c3N2cUovdDV0ZlJyV0VKZEphQUtHd0F0N1BYdUxqdkFWTHY2VUpLTnAzOHhrd24wdys0bW1BQW51Ly9NMm5NMFRweU51TnE5c20xNDRCa1lOKzlvTkpHMXRjOGVTNFg4Q2c3V2IxK0tzaWlidXNSemM1azFXUlU5Q2R2YTFldEhiYVYyOS9IMDEzQUw0MngyOUVvR1U2NWZoeWNLR3FOUGk1VGNTTzJNWEF1QmVwdWZRZTRIVHZqWVhpYXdPK093cDFWSXpIcHpuTk9HRytyQ3RvVFNjajNiTExib0UrTEdQZWs4U1ZuVVdWSmdyaXhiVHpTYm0rQXlIT1BHMHdiV29QU1A4N0d2ZWlIK3ZJK2NOdnU2clZFa2srSjJqSHd2VTJUSTRUb204SzFRRUFpUkEyZFFOZkZ5QlZpMGNhY0FEUUIxQmVveGVVUjRNd1hqYnQyRjBKQ1g4R2ptYnRQeDBFOXhHK21ZNlVHZGRZSzFzTnIzUHJuOTUwUTNFU2w0aldqcU5YbG54VWVTS0szQm9kdEZacTE5TDBDQXlkVVVXMDl5SzdNSW1RK3RSU0lWM3lkNUxJY0pZTWRvYjhvTTFQY0Q4TStwSXgvZmM3QXVQcG5mbnZkMWtRai81dUthYmJjTFNVcUdubXNNT3pqeW5Ga0JaekhoSEVSNnNKV0FjTGRWRWdSSGMwanErUHNFaENQdDJ2VXdkeVA0OVZnTENJc2p2RzF5TkF1cVdTeHk2R0kvOGVXOGxCVXB5UGNIUlFJcXlZcjZzcmdVQ0RqeS9LdHpZRlA4MEFNRkZXb1dTVzBITUJuZ0ZSeFByNmFkSUVEUWxHc0wvK2F3ODQ1cnQ3WFovTGlxWFZYUWt2VHQ3dVY1VEJPMUJ3clpXWEJobEU4ZEpnaXh1NW1VMERYWSt6clhyMlladzVZVm9NTGlFM05XblNHMTJlM214OU8vUUg2NUdIUmY3VHp6WW52dHgxeU0xU1cvdVQ4bmo4RUZKanN1NnloSlR6SEkzb21CclFqcjhQemxpR1dDQXVhVzJvTWpHZnp2bDhJbDdaTEFqTkwzZ3hiWUVoSy9uc2VZTWcyZ2QwMmR3ejA2R0YrN1JCSlJWM1VCUjJvN2tsWDJqaTA4eXNrNkNueUZqNEt6MWgrSWxJS3lDN0JmV25FUmQyUDAyY0YyZE5KNmJFTEhyeHRVb3Q3TVRkNFJZSVhnNGNXUmlUMzk1Ym5nYnpZZnY4WnpkZWR3MExLYXRtL2oyQkkrL3k3TzRaS1NDSkt0dFplZndmbTM1MERWd0w1QUJtcnFWdjh2enp1T3VmSWpSNXlURm9nV1Y4OHRVdytsYzBsdlpSRnBGTnBuekd3UFMwZ052eG8xVE9Kd1ZRei9PVzYyV2Y4ajhUbFRjdzRqR1Y3c0R2d2V5MjdNWDFobzRmdGkzME00dENQT0IrV1NsT3FoVHhDd1dtT2xVazZvQVpVQVBsc2tEeHh4Z3YyZUhFR3g0b05sTWZSd2VRdlVRRDJtejY3aVFIeE9oVUd4QjZ4SFpFb2RqSERldHM2UnhoSThJaEYxS3dSajFnTzNBZms5S0FmcHN1ajE4M1YwenRrSEQxekgvU1dLYzcvM2daZStyTEZOMmdoaGxoL2IvUEtiK21abGswM0tjUHB6Y1FxWWlONTlYSFlaUE14Q2RTWDVoV0loRHZIbUd4UlhBcDhNQ0t5bDZxd1Q4bEQ1eWdSbzkveEM0bkh4eFJtK3pEc3lEdG1pK0djaGR3NWNSQWY2NjlUMGJmVHYxMU85ZkVkUWhBRzB0RGJvT1VXUjRxdVhEdGZDVGxhNXZ6Z0RrRnAybFdEMWlJNnRzUW5FbnNFcExhaXFtcEh0eWVqTnB0ajRqUWJHNXBaZ2tnUkNFcGozaEZiS0h4UGVubWl2MzFVdmlEbklGcjdOTjhoeWpkQyt3TzZXbGkxcGpkdXY2TFc4M3paSkZBdXVtY0s3TlVtTVNRUDNlWlZ5Q1V0SHJFdDlkNUlCSmY0eWpGaUE3ZTZtdnB5UnBSbCtMYW5BY0hLTWp0RXl2Q1BBVEh2UmtxcllmRnNra3RVYmVtUzd2aE5TOXBxZy9yMEpxMUNSZ1hialRmV3ltdXJNSHZldEhNbjFYWm9EclZQNGRWYllVNXdGSExXMmhGM2JzQi9GTFg1V2NnTXZzZnpDRmZVaVhoN2d0YVJ0TlpaV01iNTkyeTVCMXpsRmY4STlWVVNCSWpFcDZCejBFTmpxSWMyN0lKdmtOZklLR25yaGR1VTFOcVdHd1JLUDNTR3djMWRYTlRRekZpOHRzSnRLTnNvR2pNVXFzM3Ewb29hZ2RtdUlIVFdBNjVPbWxMQWRFK0E3TjhLN3FsVlB0eE1tWmIzVE4xc0hQSU1DS1hndy9iTnhOZzFCTGxzditHT3BFKzhzUlFaaWE4czdqZmR0SkpubU1aNlFweGlqSTdsM2I0QUhFaGJiRTBRWDBpVXhIYWZuNDUwVUlxT0xFYTd4eTd1M3o2VG5XaEUrYUhWZXRRVS9nMG1OKzMzZTlqdi9DUnpBUEczbVVGOWJ0a0Q2aHB1KzQxdVd4YkYvcGNSajR0NXgxdjVvbkUyeVJHc1JGWFhZZko5MVo5dlg0Ly9SWk9lcEtydWN0NlY4MVJUa1B4eS8zNGFLcXBsVVAzT1Y5UnFzYXpUYW9hZGNtZ0RvaDFEZUt5dEdPanlwSGhNS0Y2SGt5NDNUdVR1cGdMUXR0VnFtRU1Pd2VhR3ZxcEdCalBWS00rQXJndDNWN1RWU3VhY1JRbnh0ZHdmQm9tMjBkeFVwcnJHZml5Uk5ZZEIrTFVRSXQxNmgxQ0l2NzdUaFBudkpwQzhDQUlaV3ZveEZ0aGJIWkRFVEV2YUlEcEQ2b0JpaUtjT29ZU0RwbmJhemY3c1NZcGQ2MlVJaHYzazh1VVAycW8xOFhLdXBKdCtDNXdQMXM0MU4yVFRmOXVyWDFZVGFOSjFJNWRNellKNEJlUnQwY2xkQnV0MTM1dkVxWlFHZ1ZxdU1OQWdUU1RtUlJlM1puZ1k4ZWJXN25PcFg2emJtaUtXK1dIQWhLVTZxYXVnZFQ3a295VEJvWjRLZlpHSnZONnhabUtmbE9XakVOZ2ZWRU9iU1JMeldWUUpjQTF2QlZnaEM0c254K2l2NGNzdzRPSmlKYWhOME9YU1VrUUUwYzd5Z0dXU2dqWVNDTW5EUHJiMTk2R0dTd0M5c0Z5dXRMMDIwc1F4c0trSnM4Z2JoMlNXcVZqWnY0V0FtdEZ1b0JNQ2Q2eGNaMFZaT1RDRlNXVnZWRmVkR3I4cUtCR3JRT1lIWXhlVDc5bUE3YlRNdTZ2SWZNYVloYkJCWWF6RkhSWERsODBiVjVvc2d2ejhTVitpMHpNODJ2RUhJcE9BaUY1Q3VNZ2ZyWnhUTm9iLzRCc3lCZVZKSG1LREVqcFFaNkZBNFdiRFhVUUFzTzg3bjAvNGg5ajVPSzRlUXNBMlFac09BZjhnd3d5blRPUWN1RGxieVREb21WdEVnL3VuQTQ4WCt6ZW8wVmpzQnl1ZjBTWnRDUksxcDVRYjAyRUROek5QZFFndVp3elk3Nm5yVE9TdnEvbUY1V2dIcG9jZmpOMU9DdmdRYXVJeDJ0THI2Z1NQOG02N054cjFJR2ZGNTUvK0tDM0lCL2drSmQxbTVqUFh5SWIxWjB1aGtNUjhkeXFReWg5VXN1SjF6OFhTeE1EbWVkZytXdFhCR0pWMitwZGdlbDkwTWdIWXlLTnh5aHFBbkQvUjdialZEVHZ4K3prQ29BdENqYlR5ZEpGallnellmU1BFUVNhVzVkRnpZbkYvU0NSRXk0VlhOVEJZVWZGNTJyZW1wMjZqYVZkZ2lwWk5KVEdKOE91WUlNOXZlc2VraUoxQnMxV1lBMUgxSDZva1puMkN0UjJ1bWdoMzFiakF1N0hvTmY4MDNTWW1ILzZPRVFMMG9tb0tRb0xPeHJIZ0VoMjRNY0h4Y0p4eFI3eUJoZXRnNHBSTEJ1OGQrYVdNQkUxQ1RoeFkvTmwydUx5SFdxQ3QwWEE2dzczWDVUeUV2OWdKOE4yZysyOVNITmZlaEI2T1lPcWx1YW5wd2thYWFOR0xmbDZQb0ZKWGhWOUxLVG02Q2syZ1RrZ0gzZ0M1Y0duRGVtNFhFR2t6STl0YzlQenlPalJTRG1mZ0d4N1B1ZldCeEJ0dlc4MkJ1ZmwrMHNwTDRnelRKU3ZrcGNUOU12Q3FjbGdleW9CVm43L0xZYkNGRExKWDZ3U01LdXZObWNPRE9MVkxzUXE2akZPbkQ1a1Q3Rlo5TlpOOGJySXZibHMzU1A1QXN6Zko3cHdnSjZpL2dBSm1zcmY3ZGxLWmNFKzBOby9pMDVwQ2R2Qk9QWFVzNzJubTNyaFI5Z2xNRlFoMitqU2pnQm9tWGNLWWdlMnpzYzc4bS9aM2dWRWFWdUM5YnpvbTRzLzZQdUFwaG5sYy9wanZkeDh5aTJERUh5UWVORlVVRXVjTlFpNmV5dkVnVlFyeXI0V1FDeWNzQnhGYlg0V000N2pGVHJBRGpnL0MxWHY2VzBGTjhHNE5ORVFYNEVJSEd5aWR1UDBDSm1wVFliMzlUbGx2SnptSTFFelVScFVheUNNM1VhNEdqeDBqbnd4QzBaSDkyalZHd203YmNHRm5LbjNiQ1BkSUY0TzdKeHp5dWwxV2ZXMHZlNXQxb1JPRE1DS2dxTlY3empyUy9QZzRHMTNEN1M1T2xhQkJoNXhrRElEcDRSVG9yRmxiUnAvSXBmS3hGRE95WXp0czgvdmN2RCtnc2M5RUFCSlJGK0tRWTJOd1FrZS9KMEdRREtMVDVaOVc4UHprY3d0cnN3dmRESlR1c01DTVZWbzNjcDltemNydFJtZmFiZjhVNEplTituNDdEelA1a1hYL2sxYXNkKzJJR0gxWURPa0FQR3Qzb3ZaRjBKYnJWK1RPeUE3VGVHeE90ekp0K1FXSXNFdS93Qk16YzFKTzAvNitiZXVUTnRBVU5PVlNpWSsvb2ZlK2RIZlIrS2RxTTROZmJodFF3clNteWtYUXg4KytrSCs3amoyMWZXSC9XUW83bFlSbTNQYzR2T1RSU216TExkVEJxTU1NY21UbmdiZFhTZSszelU2L3hNa2ZQUnNkTHk0WUFzalZ1WDhOdWhPWjJKS29pRzR4VjZqbkdLT3BGbXhnUjI2ZEhleTdyeUR2Skk1Zy9Td3NvVElVRkdOTDY4TkcxVkVSYXF5OEU4d0pBc0t0dkp1ekM0dW1RNkdYblVBaGEzdXNjeEV2cndpSmdOejRIeFpJQnRFNTJBRVpCR0J0YitPY1FoMkRLVDZMWTk3dVJuVEVPOEpiczJzaGMxQ3djQXZ0bFZQY2lNSWF6SSsxanExbHQ2Y2lmdUVldFVpbmJhL05YZ3daR1ZzLzFmb0g3ckZvS0R3eU1RRGdxT0ZEMk9pUHZGR0QrYXh0Nzgvc09sNE5TWTdqZnlQTkU1VDd3ZFhWSEZHZFBDa0hUbTNKNlVjcFZST293M3lLbjFDZDRUWUFGR3pFaU5pTW5OS2t0VG9VM3cvcXpud3FtaTVVaEkxS1FLQzBGZlQwN096RlVkbkF6cnhkcVdOZUs0dlZIVXptU09zUXV2NjFsaTArWjhlcEFXc3VaL1NaU1hVVWR5dnZaT3hqamowVkduWFZsQ3E4MFNlbkxPTTh5dU81OXNZd080d2laWW91aVRlZ0lZbTdiTis4Y3NISW4ycVJmZTFQODNVNjExTTUzK2VSbEEyeFVQOTJaaG0vMmEzV1JVV1ppRU5GSHNjVnluNEppNnVPWEpFcE9EaU9MekVoVFd2dy9rN2NwYkxIUFlXbDV1Wi9ibGUreExFVk82bDdBUlVDSDY1cDJnOVNha3NHM3RRTTAzWHAzdG9LSFFlUTk4NWh6ZVJWTG9Wck0vTThhOHNuazkzMmloeWY2UWhzV0RhOXhzRFpyajdObHNwSWp1U1VzbFZWcjFDN01kMFFTaitXTW5ML3lnOTV4Wmd5SFFhbGZEK2ltSlhybHVEUDY4V1ZkRnA0Q0ROTDdWZk9WRG1NNEMyL0hoTDQwV1gvRTBvNDZDQ200cHU3c2VKblJadGlBTU9Ma1hJTUQ1L2pxT2pSY2I2WFZCRHQ1U1dVK1lvUXdKYTR2czk4UTdUbWxQRC9WYVR4UVhQTlBKdSttMnNob2ZDTzVIYlZxZEVOUGkxRXlRQ1kxcktZSjlSZ3VGM1VWbjdlYXNDNFoyenlsdkVaR0pVSHVTenNMdU9GL3JYUExXUFd0QjR5cGFCNkx5VU5pZHV1Ujc5MTBhQWhxdDAzUitJWkJzMUhHUEtIUzlLeEhMQ0R2cDY4L0NjQzdXVXZzYmpwUXpwL2hBODhuMENhSTJJektlWnl1RHZOcm9hU2ErZmtISS9XN2N4elF4dFV3STU5YU11MnhQeW9mOWoxRnp4clJnZ0lBWTZRbTB5NWFXU1FGNDBVUG55Z2R3ajQvU3dyZGtqcVBoL3VEbk03dDZlRU9SbkRkbi9DVTBUQ0U4RFZKbTV0K1RzaUEwTmhuZVJjTmM1WDFoTW1HS25pdlh3bUlXQVdNZk1FczZVa28xTDBiV3loWUoxNHlycmpvbGRSVlh5TG5VVFBpeHdrUzFTVmxNV0hvd2tVTnFrUFU4Ymh6VzN0VDQvY3d3YVk5dEkwdHZORVhXMitTWkdHeGRZd0Y3TjN1ZElQcFhjczBWclp2R2lUaS83UlBVd2hGcmRRdm5rZ3JtQytUUkhoN2k5eVpXOTl3aDlSZ1FuOEF3NEY4UmJNNWtvd0J0VW5NaGYxYkpjQ2hTNjFrR1BDZUZOU2FGSG9SSVBFUlJ3c0NxTmlpQ1lGOW1DbW9sQzg2THVVN3VOc09tR1k1OFozaU5OajN5UzRydGlCMU92dVNrMC92VDViTmsyYnhWVytMMHBZWldaMUM2L1NhZ0RFNWJnSnFEVlJhYmNtMEVHaTVqRlV3TmNRNlREQ1pKVDc3L3BudFMwb0JEczkrd3EwbUJTeENYOVgrQzhNdDcvZXgzd1RxMlBHUjAzdXlqdmMvNmV6a0lYR0gwMUlhRDR2V2JRci9iQzhhd3VMQTdRVG5VcXBjaWFOWVRvdnhDOGZYdCtsWFdlVllkR2JiVlcvNEtmSFpHK2dJL3Z3aDJ4bmNwN2Rxby96aVBVekVNT3VoeXBZOUNwMElSNkpMalVETW9KdHdKZTBGa1ZHV3hiM010NFdPL1pDQXZSc21HdDk5a0tJdVB0cnUzTU9abmQzQVUzcVJTb3JoVlV0Y05nL0s1MVVxSWgyZjI4QTk1d05Tb05lRUZIZ20zNXZ1Vjg1UStUejEzR0JRbm9sUVU2dDFubHRxMHU1S085YWR4TnA1Lzg1Z1NMZ2FBLzdnS1BnckRCdjNvK2k1a1U5RmU0MGdLWGlycEozZ1NkMHBjTm5MTUp2ZmQzdXlqUmtRQjNWUGJ5SmtyWVpaVU81OG9EVGZuU1g2VDJBVE9pam9JTWNyMXRMdjh1NmVEcFhNbGhoM2FwQVE1QmF2RjZGRmw3bnBXRDZVQkRCemYyYXhxZnRRTGdHWS8ycWk3aDdFUmZjWm5CZkxZU3phL2JjZXVPcmt0b0tONTFhODYxa2VZQlRvczJUY1hHdVdWNEtoMEtYUW1HVG4ydWp3ajlwSXRYWjg2RUp0c1N0aWNGenh5MXRKY244cDUwQTZHYVNSYW9ZTklOdEJ4Z0kwdE00TFpNc2RiWi9xb3h3YXJIbVhQai8wb1loTU5ESEltQ2VHL1QycEQ2MUJVbENXYVdmYjc4aW93eVAxYVdaNlZXbWtlb1B0VEdtb1I0MW1WNGJnT01oU2JVRlkrZkdDbUFYVEVuSzRic0FEUXN6N2Y2TDZ5SUNmYjFDUUJra0pKd21LTnJ2bkdTblpJRUUwNTRhTEo2OXN4dElEL24xZlFkQWg3SUJKZmlvWWZCUjlER21sc0dCYktTaDNTQ2g2WUVvb3lmOTlYWUNDTW9oeDlwanRTWXMzSkI2S3hLSWlvUTgxWm4ybXFjQ1puc25YamJjR0ZPdWRXcC9KOUMzZ0l5TjVmbW5zMWNsemhUUlU1MHhGdVVnbzdUeVc4UDRFb2l2OXhlL1gwZFRGZE52WDJHL0JXbUo2NnE2WkV2YnJKR1ZyWkkxcnAyNHh3Q3R0Ri9MbHZ3VkJkdUtTV0ZDSzNJUUMrL2ZRU3pzd2FITURuRjRvcllmVFpBM1BUZTRtTG8wVWluN0lFV0JoODYwRE5kKzVtR2M2WXNGbllaaG5iT25TV2VYUEZDbEVvUzFBTVc1K0ZKTWdSbmt5d2padkhWVmlUWDhuaHdpanVZd0R5bjdQeW5kVUVDUWFpL0FjY3cvb2w5OC9zeHA4bGZQQ291MkdGS2JpVVFQZVRhUVA4bU1kTHdjYXVlaGJ1Tng3RlJPRkRQNVp4NmlGWmZ4bkIvUDZNMTFBYkMxNDdEeHhGL3JmbFQ5Ylk3eFp3VWE3MVVNNXgxSnZ3ZTFkT3BMU25pUTRZSjNrSW5VUXNwQ0hVQm84bGNXdThNbTRMZU1lMk44OFZNU1B2Qy9PYk1oaFpwUjdIMnBqM280UTZGUGVPbEdWelpJelFSYzNNaDBOM3Rkdy9rdXhRQjBGRG5OZUlIQTZ3czFmVVIxZU9LWmR5QXh3MENxTFNIb0YrejJjbFU1UlJ1OFYzMUdOdEhGQWNmdUZ0YUF3TFdYeWVlZHQ3UmE5ZnVST05hMFZ2Y1NRSUc3bi9QMDNxWitUMDUwelUyazBLQnY4SXBJRndTWHg0Q2tVbU53TmZ3QlBIbmJMckkrNzVWQTR3WDZYcFBiVTFjQWcrUGUvNTE2VHRwZnI1Y3hlOWlmZkFVaG90bjdWQTNPcVhpdGk2TXJXVE54ZWNPNlhVUWRBa29sWEpBSlNBTnhYMnNiUDgwZVZsa0lIZERESmxVdGg2KzhKdVhuTXJsM2ZnZnhOWUdnU1diMDkxYk1NUDRwMVN6WHBNaEVpVy9mZTdQS0JvMEpaczJNUHlBdzRkU1dFYWVzQ2lMd2MyWG9FNS9CdG5MMmpFUXJuTDhzNXBrdmk0K3V6QlJEcXZDNG1ZSmgyeDI5b2FQRzlobDREbThqVlBsaWtuOUNPMzI0TU90ME1aUTNlWjZpcC85SHJFWFNxL0MvZmNJT1U0Y2ROUFhHdWVxbVJzS2pkdjFzM2JkTzFZWFFqcmcxUjZDb3k1V2tlM2Fuc1o5bWlBRHU1Y214MWU4TWliTHorOTBBOW8xbSt1ZUpTNGpnQWd6blRBQkpqOTlaZGRJb2F2Z002UkYxblJxZC9CVVNMWFVVK0c5UGc3SUJ3ZzBVbmxXdDZOZHlJNmdqVHgrYWlNM3p1UHFvVll5cEs5SjcwYmdhL3Z3NmNCUURoekFnaVFNOVV1RU1wN3Mzd0xCRWlPeFAxVFgyUWNtNklYNXBudHAyZmF1MEtvcWVPY1VsSG8vWXRtMEZNcFlJSURGL29jOHptUjE5NVg4Mk1IWEtCMnoxWEEzUzVCZGVpQWdJNG5UODBkQk0yTnVwY3B5OUhOT2Z5LzdDUTlDanF1ZE1yUjZmcW4rbklld2EydTNWWnZqbUF2cUJkVlJOT3RabUk2bGpBMmljd2F1SXhBZWlFNDJDYjdSTWpXdE1RR0JUUDN4dE8yamdyT0RpU1E3aDZQMFpZTmJvSlhnbFo4Y2hWK3oveTkybUJOQkNycGxqUVYzVGhwZjJ2RDJLQk5UamgweGgrWnoxSFdYZnNEdU1GaEJ6RHBrS21IZ2hGemVPb1JBRk4vbDFJR0V2SExPYW5RUE8rd29yV2R5NFVYRGtUelpOdGtpOUtmblNoNjhqZWJHTHZlTzl1K3pQK0wxUDd1djk5OVlzUXFKbEZYTWFsY0pmdzROeE9YT0FRdk9jYjFjcHBEMVFhOHM1T3AxMHdUbjZsdmpWY1YveW9XT3B3c3BqVDZ0UmxtbTRsUDFjbzErcURXV09kRU5QekpnMFhmaXZrUUhybG9TQ1I0MmY4TzdSWTRRa01oNmJBaktvVFEraVVBRmtxb3Zvc0JkRDJrZmF5QzJ3TWZPSGNJWnM5WWNKSE9Ib2E4T1RwZlgrZUFpUE9EZFZ4QjZKRFRHUFNyaXZmT1JjdUt2M3dUNzdBZDNxeTl2bytabGpvcWVKNTE4NHZ5cFR6SVE2dXRZTCtvRm1wWnRpN3VZMVM2Q1VLMmVyOHN3ME9DYWF0TE5FVXBzREQyZy8xSmUrZlNOdHl0eWhsVndweWpGVTByVFNVZ0Y5a2ZuL0NQNnVFclNvQTR5SGxaVlpIeWJPQS9QYmw0Vkt0SGpVcW95aGxyQmMzbWhBNHVSandJT0RHLzlFZnVFTzd0OXRUb1VnT0hZODEzVEI2b0F2NDVQaUhlbWU5L0FUOVIxelAxN3g5VVpzZEpxNlBKQnQrYVpXNnpnc1IvNVpWNXRQL1dWK3ZJZzBOTTIxZ0dnb01URlFYZGFJQmJ6MmZJNkxsY0gvdjBGUzZGODBNM1Q2Y2dQQ2NON0RTcFhWMjM1Z1Q0Z0tTNHdwdFU3S0dWVzFRRnZDQ2pZZWcxYVBrSzFwa0c1SnpSb3V1WWR1aWtRZzFzUWxlSDVsVkdoMVQ2bytWWVZVVDRIcm05RTFNYUxYajlweHYxZEdHaEs4OTRMUk1aVm9vdzBTeEdDdVNNaGhvN1RST3puUmNGT2FBRFQ5RzNwTE1oc1JGeml6c20vQkxHTnkxbHRoeklEbmRSTFY2TmZTajRKYlZLc2RkZDM4L1ZDL0hQVkpaTklBcUM1SGh0U1V0Q2pWSnlISno4elBBTnVkWDNWU2FQcmp4ZjhuU0VVNHJISm9TVjZuTlBaSDFvZ2MwTVBGbmVreVFlVmhic2FicGw3c0VqR2FUdWZyUHM4UEVGVzFQc2N1OVJGR1NzaE1JR1pjRXdRMVlMcENCUVByN1hUME9vZE1ZTFZZek5XdmtFd3RHM0UzbmZncDFRNG1zS0src2I5aHc5UUJPUGNGYnhEUWpzUGRVRitFVnpaUUpiRWxYeGZudGNMaklSSllKMkl2WFNMZWdVWXRnNmtETVBqRkpzalZzUERibVlzL3hkSUcra1BmNWU3Tk83QjNoaUZObXhoSkh3KzAyMUE4U0x5aEoxTEtXSWlTVjM1L0s2VGxRUStyT2MwcDgxcXpjTCt2ZUw5WmZsYVNLeW1yMW5IeEZoZTFHaHA4Q2FVNFg3dTEzS2UzdC9yK1UxQmM5NzlETFIrRjNROS91SGM2VHZ1bld3UXNXZUtEMkt2QXMzVW82ZUpVcndOSTFBUXhBVy82L3lFNHRXT1gwMW5hcm1iSXFjSGU3YzY2dGh0TWloV3Z6a1djNytGUThIVjdHNGw3K1RGbXVQSHRCYTVBQmt3QW5ac1p0S2Y3NEtPUnpvSEgzWlFnZjJqSGVPaXQxMUNzbTJOUzBVMlZjbUlVSmJJaEJZdGQ2S3Y4ZDdOeDRUeUNvakwrcWFIWGpId3JIeU9MeDErdFA0RWxXbDRLdzQraXBiRzdUVk1rd2RJQnVhSldiYVVNNDZWQ2Y5ZzBvNVFiMVhwVWFTbFhyRUxnRVQwQU5mMFVJeEF3b1JhQVdTS1pidklKdkxkQjlaOGxRanh3UjEzcUorOEpDeWpSV1orS3JzTU5UT0pnN0E2QWY0clQ1akJTK0hnSE9zdW1ydENJcko1Q0I1K1JKMlMwMUtZVjNkdmFLc21UZ0hvQkdoMERpTkZIUkhmZll5bGVqN3lPTDdXdThyR1A2WW9ac2FNU2R1V043UHJzdUpQSGdUSitRclNwNXEzMTVZMHBDUVlsNGRlOWJ0VkxjQTBESWMySytuWDEvZWg0d1ZnZHlkWU9JZVE3eVRlbUtENVpRTmhySHUrbFlNNDZDTmhSVWw3Slllb2lFKzRsRWRPOG1HcEwxVEdjR3pKMThMWG8wbzJJb2hGS0wrUHlKbFJkNC9hSXgzUGtSUDYwVDVLVUoxdXlVMXZXbzR4aGRZWFZhcEFMbEJJWWt1WUpEaU1yS1d5eWJRZHFKMTl5YkR5MitlZS9mVzBPYTRZYkdLT3Exa0NFODVCYm5VVmluNlAydGw3VHgxSHBRbW9iNi91Ymw4aFE1dVYxK3gyeGpHeVB4b0ErSnMxWGhJS0M5SFZTRTAwMTlmU0xRUzV1Zmo5OFJVWVlxQUN2Y0N2azRyZVNObTdYNWwxTW55ZVNEdG4vSFhHdWU1Nnhta3A5SXo2U295RDI1Zm5pR0xEL0ZZa1dCa1JvZkc2dU41dHFWbHpRWjgwR29Qck5JODJLT0s0aXoya3hQR2kyM2pvZUxqVUdnVGZlenFkRWttS1hFY3pjaURBYmRtUGVxTnNaenZxbWZJZlhiUWxTK1h5Qk1rR0N2TWU5WWNHNG1LRnlSQnlXNUYvbHF1cDNyMGZob3dXSzZYR3p2YmlDUzJNYW1wZmpJdjV2SmpTK3dHd2FaMDExeFpxZE5sbFVMUGd4QmtCT085Q0Q2b1M0NVJmcFV1cmp4UnhINzhsSHBvTk5ZLytUMVZlQlNvMDZuSHJ1WENPUktJdE1YNkg1YkpaYlhXWjl5T0VWYkc1V0hXK2d5SGtwdEk0QlRsTThmYk94SkRYTGxRTm4wd1lReEJvbThHcGlXS0NWNVU0VmNMWitFb0c3YjJzRFd5dG1vQnFIT0NEWVNKd2svWWR0cDBYd2liUVVOMXpTeTRZWGdmRHRwN0JIcFNXb1I1THpDY1pabzlLVlFFTTlvTEFvR0krU3EwaHZhMGJ5V0xWWmpBOWdpa1pHa0gwMjVOZkw1Z1c3N0RiWTZpSGRvMk8vZzlCSVk4L296bVZyeHJMSnVaSnBvNnlIUzJreVlMY1o5WG5hcnpNcE8zWkFZelFBanlEdTFUeis0czJaR1VWekJaRjZTQm1RSnhaNjRMdTJibUZNMnNlNlE5eklScDBLNG1iNUJLWXFKZHFlSEhhYWhRMFRyWnFyNXpiTVR3T2VvbUZRTWg2R2VhQlF6bjVFck1PUEpYL1B0WDhvVWhGQXZQNU9abHdab3d4b296U2d4OGp3NFZSbFJSWkRISjBTdUdVeEFGZlI0cjdZekM1emdwaVUra2E3UFd6L1JyRmEyWi9zWGRRZTBZZWVVMFgvTVNBVHo3dlJ6MEtGaDNLdG5HM1JKaDUrbFpsTldBSlNWOGo5SExQc053RzZrdUNVTjNwUCtISThQWVFKWjArVWVoV2dzeTI2S3UrTVBuV0tZRjF6YWd1dTM3SE50SWFEd1RSUUowdnNLcTNPRVRNSTluWmZSUUptbjBKcDhBWEE4d0IwOHhuV3FxMFgwKytFdjBiWlRPZjBXVFZ2aFJpZERkbEVzclM1V2ZxakF6TUd0U2h6VWd3c3NPQ0E0L0RsNUlPM2VlczNnTU1aUHgxZU9aVWpNQWJXZ3NGRHR5amNlVk9mL0VyNE9iWkpjNWsxRDNaUmMrS3lJZFp2bCtVOWx4OHpBMmlYTUZjWExMaTlYdVVuY1h3MHRVNU85RStUMHlxaWtHcEp3RlRUTW1qUUJuc3pJL1BMSHNhM3FPSWVCSExzWUJlZVkxTlNuZmI4cTd3cVFNUFF3NDBmdHJEV3pnUTZpV1lPRjMrU0k3L1pmUFRXOEdXVDlxWTl0V2R5aVRkMDRnRmJQemtwWDdQN0VSdU9Vczdpb1FRZ3lpTDcyeXBlb0E2MWZvN1R6c1FDd1E4eHVWT1o1bXFtWnIvYjBhbVNEVkdNTmFZK0gvTjZ2dWF0SnhCbXNOVkFKaTVDMm9OMTNoTWE1MFlNVlg4ZnFoajlKRTZUcFp4SGF1U0p0ZU16aHVmbXNlU0tzbDJ5cjlOZVJGRFJXcDlPRnhkalkrRU5jSUtNTXorTWViMnF2RFJYbmxtS3NFcHdFUDZmdUdnV0ZxWWhwbVp5LzRvbXdPQU5kQktabWVkN09HRTZvUGE1UG8xZVhyWmZ5UFpjWVpna1dsZW51UnJpamp5U0d4bjl3bGRrRnJRcFphWjdOejc0S25SQ2pETm5yTGo2ZEdvL1dtUE5KeFVwcFBDV3FrNFBIejFmN0JLMUlxZlZPRGFPWEdvT2xkbmFlV2NwSEczYkRvaDlDbVY3c1NOclRaN2gvaEN4bVBObmRpT3hhWTdVSDRqYUl4ckM1R3dQQ0ZRZnpEcVE2dnVkbTN0TDQ0Q1dhTGt4QjJtMTl1bnRLTzEyZHBISkxtc0FicEZNZ0I1L0xSYUN2ODh6SlI1WktCZXZZRXQzSjZiOHprZVVUdWRpRGtUOUpnK3hZMk1PWnE4ajBZVGthbWQxMmpMWktYYkYydkIrUHVCZVJCOWlJeFQ0NDcwRENpQTlSUzExeENxNDF0eVQ5aFAxZUw3aGE4MXU5TzgveS9Fai9KeThrU3phN205R1dVREVDbmxCejJhcy9MNGxnYWQvWlZvTEtVdUpWTURjY2piUlZKVHliM3BhWHRKN0cxYjRKd0liZUhqOERPRnlvekRjL0h0RVp1eDJWWmhYbzVXOW93TXl4UFpsSFFOOE9DZGVBRXdOQ3VGOURUWE9CVzhnVzU5elNZcU1uZnFPcEpwcHVMeWNQRzVTSE9aTEIrOVpwRlVnMk9YVjRnNVZUaUlqT25BakFpSHM4UzEzS1RJUFNHMS9NOFZkRDBhME5zR0hGYmhGKzNoVUtHL1EwN1VOM3p5WWdlM2FWU0Z0U21nODBWeW5pRldSUXhqSXBDUWtta1MrNUIzT3dheldaRTJoYTdmVldZNEtkb3RLMXVjNTZ5RlFnVG5BdTc0dC9qMkxjdW9ZeW9JblBOaElZdVNEV1B4aVg1ZjE1ZmlJYVhQL2hEUGxpUlhhSGJ6QmhFT3V1eVpmV0M4OTNseSttaTYyc2h0aEUzQjZJUWhTbFZiS2tsUDMvNzJycXRRY2dZcEZoZHpnK1NsbVZCVjBObExiclQ2WXo1bzczeGtkdkg4K283eURac0R2TkJyWWhxblAyOVIzNnhQLzEway80VFFBcEZDOHppOFhFS21LQ2RnNjlSR2lpeEhIVEFJYmh1bmpXYmVPdTlPUTZIQk43dXVZSGV0WWhJRFhRK1hqZnUrd1lraENmUllaOUVoQ0hoVTAyU05yL2h0MEI0OW9VZFkzR0RsTG5pQzJxR2N2K3VYQmZRMEhDbnI1Mi8ydk0vSWJCR3hCVWVzMTBLc2VtRnFiRnRhR2JUa01hUzE3Q0k2ZEhHVkU1dm9XMUNSQjB0L1p0eEFxNUtwZCswVGRLTTdhYWE2WE80S0VhM0pwbVE5R1RyZmpjZXd5K09XbkR5NlBqM2lTMndUYVlNeWJEQlYrMVVCR2J2bzJRU0R0blVkdjN1ZHc4WENpNjVmT3lORGswZWtGQkxZTlZScXhwbjBRNEsrVE9oUGZCQ3J1ZjhZUVZoRm5yNmZ4NnBJYmJkNUFuNDc2NEVGNEpINWF3VmIyK1V3TWJDSXNKRUt5MGExQm53eXRpdC80TmpXQ2Q5MDAyQXpFeG5na3ZmMWJVWXZFcFFabG5taFNoQzBuZmpWd0Vuai9TRmF0ZG5qK2JXZDRjM05PdWhkRk4wR0E5STYrVXJJakhTZFcwUUF1MnREV0tmQVhIZk9qaFUyWkxaTGlQZFUwb3htR0l2ckdrQ0NOTjk4bEhQeXpRYjJZK2syQjFOTGp1VGlpOGIrajhhYy93K0grMVNHN2Q3b3EzRG9QYXViSVFZSFczQTI0K3dRM1hjdmt0WERadXIrUGRoUDlYUE5zdFlLTkJLMHRxdVZPNTdjcnBzUmNTNTFSRUVkYVpDSTRwLzFLdk1pcXgxNTdGMVNGNlpNeDJsUldmOURHTUk0TzByamdWMzVoeFlxNk1vVXd4TEsrenBhMnhLMnZuYjNrcVNtVXIrdGdsSENOL1dTNHpKMUxwc2Z0dGttMWIyR1RFbDl6ZGtTYjJJeXNsVGVFQ21EYVA0MGozV2pvNmszZVNjNUVXWmZkOWN5MzgvR3p1WVovTmtISFJndFIzLzhSWnp6N1lkYktUNzJoOW1icVpaNVE3aXQxVDdya2haNkRlN3JiS2pPaUQzMUZqUmhPRENPTE0xN0RVQ3ErWm5QS1p2RzNYZ0ZCUjZXV3ZpRVlxd3pOVWtwMnhIN2hyRzBCU0FXZWM2SW9MOTBxRS9jQ2Ryd0syWlZoT3crMFR6VFFSZkpoVzh2bjRCK2poN0NGZk5mVHEzSGk5RmhWZ3VKcjJkeGlya3U2QWZpbWlWM1ovRFdpMnJXb08zek82WnNRU3pja1gwZ1JWZ2F2eUlhdFpmeHg4OEk3WmVtRDBtNllkUmo4YitqQkV6N1RNb1c4Y291NWRVbStjeTNNYVowSC9WRXR6SHExTCtQdHdIRjhvOG1RQmFnMGwzSzhJWmtXSS84NlgwU0ZBcmdnTlRUV0x3RTFZL0d3Q3I4dzM0VFZpL2VPRUJhVGlVR3pUVldCUkVnNHV3OVRCMENHZkhvRXg0Mk9PdFJ3WW1oMVdDU210V1pkNWV3TW9FMWxjcVJqRERLNlBaeFQ5Y1FCRGI1WlFPeUFhQ3ZXNUV3NGxnS25uZFJmWnJ5MGFtVzE4RzJkaVJYZHZJdUhEbk1ScSt0VTNjQXZBa0E2cVBrUDZGbXZneGgyVEhTWDdhdlFtRDVZYnJ2VnRCOEcrWEVWSjNrWVpXbU81YXRBSzRmdWVVUEZLalZRdjlkNnlaeXk5cVVIS1JhOWVRUE1tZWJIeDVkOTNWMGZYZ0xZQm1uTWdoVEJYOTd3MXZFSnQwTjUzQW1Sd2tCWXgzS21raTFoeExhY2NnckhWd0RtdEcrQ3Z6MnVCTDkrMnp4UDRtTDhPQ1YxWDRDYnhUQmVKSk54Wm9tbXdYQkM4NEFQYVJ4SEFxZ0MyU0dqQVdaSXdHLzVlbHUvemF2a2xhVEJqcXptZk1JNSt3VkZ1b2U2UDBORFFrQmVWQVM4MVJRQXlMS0luTmI4ZlozMWZ2V09WN3VaNm5SWHdJVnRuL1JISFRHbDhSNlFld0tCKzd0RXliTUY1Vmg4bzlidDdKSmpOSFkzbXI3ZGhzd1VNNllxL0I2Q2NHdHYrdU1MY0UwelJlSHlYaURiZ3VoelFOajIydlN4SzU5OEMwVjBQcjJrdUhjVFJ3U1FMQk0xRXFqcmNVdGM0ZXRUa0UvMllsa3hLZlFSUmhYRHRDMFQ4L0diNzdoWUdRbzRNa0phbndJbXZoNVlTb1hZYWZCeWFrQ1JhN0RvM25HR3NteSs5T0tkcldDZWZXdzF5eXBXQ0FieW9KQUhZOFpNTXh5ZEtOaG5DV2E1NStDSW1UcHErRzAydXlMa1hueEpGNlRLUE4wYUFOUEE1ZXZMaFZFQ09la3UvME9PK3NER0FKcXZFbzFvQlh4endqbytTNGJHTzRLZU5LQVFlV1oxaml1aGY4YmMxank1aFJzWFU1LzNxdzd3OXdMcjV6L09GMWxhcDMvSGtWckRYOXJVZWxnem95WGloeVZOVVJqeHNyNkYrZXBNaGpUb3RHRHo3ZjlHck9nckxSSWEzL0Iyd1Y1blZFQW1PMDlRQ2g5emxzWXB4Zm5LQUc3NGJwVm1jSklLNXcrbk1kTUpLSnFaM0RLa25EUGVBcWVnV2NaclBzTW1EdHEzQ3QzUmptK2hqNk5TVDVyKzZ5Y3pmaXBkMVBlZVBSbXlqVEdISE81VVRWcmV5MGk2NWpKbnphNzFBelBod292aEFUV21vUWVROTRuM3ZGNDBRYVJVdS96YnM1YnJ0L3R5RURBeUlJQlFkTG5Mbzl4S1BJT1VvMzNqajlZV1lqNE5INjA4RHhjQUpsbVYwUzMrRmo2b0hYeldPWFlYWXJuaGVtQjB6WkJUaUw2UExhSW9UYnRwNzVEa3dWK1M2QmZ3dHFEM3JVR2dYekZoNTNudVpqRlQ2S2xRT3dhRVlBQ0M5bTNzNGU3MGtSdk1aQWRud2FVYVcxT1RQcnliTWpOaWhrSUlGVk5TdHRPdmlaaDlHS01PWFRVdWJjMnJXSzVQOUFGM2FkUEJ4RGhRTXZad3NSakZTV3cwYzIvaHVKRTdxOGNUcEFkaDRrZEtaZDhVbHljT0J6TWd2bUpEMmFpUXZUenBCbSs1MnNuMUNMQXRGYVZEWk9vWGl0eHk0Szg4b0hKWFBqUmZNemRCdUliT2kyUThrTkN0TjlGenFCM283WjgzVGUwemFrcFpxQVpmNEhTdXJJVDVyL2tjS1hhNXhCK1FPd3NBeFkwbkJyTGY5dG9xRnBFR2c0OTJxUXNUaktwRGFTUTJPS2J6YjRHYUJqVjFWcC9oaVNsdGFqNFFCcitTVGlqV01YdzJaellrd0RFOEFqa1JIb2hpV2tPV1c4dTczNzlFYmlpUGJkSnBqMGdZZkRNOWJSZ0dyTlkrY2JZMDE0eEY3Nlc2WEU2am00Q2NuVVNWQVdqSUxCUEJmV2xVekV3NU9qSG9DdXdkbGZVWUc0dEx4WGVhMzVYakdBUFVHSzNhYjNDYnBFcmIzMStidkwySDJaWUp2R0c3OWdJazlobUhKSWRaS1JjTkdJanJVSzY4YkxGSXAvaHQ3N2dia2pZZ2JBRTFEQlI1VWpvSnB5QjQ3RjBwTXBCb3BHN0hEcnQvRzVLSHZncGo5c2gzbk9za0JZMDNqbDJHWTBqRXc4WDRPUUd0N3Vld2kvWGRka3BudlV6WmJ1MDVMVGNjWTI1VGRRMEhuSEkzamhVOWh5aVg1UXJWT0hnaEJ0dTVSYVhvb0VFTnlCSVNZakZYcWtMd1lVaVdqMWFMMTY2M1VqVnNXTldMbXdHVDg2cmZDQzJ2L2RqbjRGYk54OVhiK21JNEU3cFF4OE1YVzA4WUUzeVpWS3VWVDk1S0t2L0pRQWREc2dud0FSekxTNFVvUTFxTkVWU2ppdDIvS3gvbzNqVmpRRkVKcUFINUNwZC9zZU9wbTFHT0g0QWJhenVGam94MlY2S2YrQXRlditCTkZuZ3VNVS8rUjdaNFhpaUxWT0NOQVBXaEV5L0l0VWVZQmZqOU9GRXRlZ0QzQVVmR0x3Yk1HN0VTSWlrZlpXOUZrTEtMaWpaVlIyMmFONGZzVHQvckUwQitXYlVzSlhleCs1V2ZrMm5EakhqYSszMHltczAvdDNUZG14WTlGMmIyS3JFWU05a2oyVkZzUEloeWVSaUd1K2thalhzMCtYVlhVT2M1aFdlYmRLT3hFdFdaM1E3Yk9ER1gxT1NocUZTUEtOYTZHU0F2ZXJVS2NMUXYvVUMrb0JtNDJhNkVaRnYzZ2lpU2JiVThkS3YwMkk4cnpDcmxGa0ozQlFXdENZN25icWFOSWFpcXNVWU5ScWo3NElwK0VxZXdvelB6K2NjMFdvZDlPU29aOWFpa2VjdlAvMjFOY0NOeUhTM3NBN21yT0Q1dlhuVm1EU1dOVU9lMGFhbTNrWGFaOW1yVGovSlQ1WGtEdXM0Q2FZb3F0QUxETkl4SHZYZEpicWxodkhLZnpBczNEbGsxVUlTN0l6bGFjYitFazJWd01WSnR5dlNFQnpxS0phSjRnTlpOS3ZyL2xhWWVlcVNZbStKNXhUTEhBa1lyVUU4NGFPRnlZUGkyNUdzWGNsOUwrVndvYlQvZ25GNktHOTFsVmJReEE1MXBzVVllRmpvYnRabzF3dElPcUxPdjFpNGpqT3FHbzlHbUpudW0zNnljZzNOMlVBWnlRdUE1dG9tRENxVVI4K2FnOU1PRVBIcDlOR3M3bE1pcGZXa3grVENJZFBFcHBYRElZMk5VelBDL2hMY3I5ZkJ0WENMQnN0aWVuaTNWVnVNZnZmTXdXaUNUVld3TTg3R0lhRjhWV2kxc0E2Mk1nem1OWWl2NDgxb05SV2xOdGdacU8rejZsQ2l4cG5tWVhTMGREMk05Y0ZseHFZa2lDb0lpV21qY1JLclpyVnA4OXI3THJHaWVrSUdDS216T2VzOEpPV1pmRDl0Y0xSSGJyQzBEeGZReTA1Um5SNjFOV0dpOWZVdlcvaHFhM1VlbE00Rml2OXZ3RFVvb2UyN1JUcUs1cDd2dmFSYTcyWGhTTzhEU1FieHVXVyszbGJWMWdXVWpZUWRvMDNpeVl4RytENVFZWWVFL0JmaVh2UHozZytQcmFaMU5pSko0VnlIZEFUbFRXQ2lJems5OWh5QkVmM1VOeVF2OGV4L2k2OXRkMmNaeDdlL0g0ZEhmYWpuVHEvRThFek9vQTUyUit2ODVuY0pzTUcwV1NPblZ6azdGZ2tqdlFPM093d244Q1IwS3RqbUVpcCtwNzBQQWhnWGRySmVYdnhwdHNrOUlPaGp1aE9ZREIzL0lNTzV6enlraVVESUszYURIZzNMMUtma3NkM3ROd0tWVkhCUDNuZStEMnFWNWdzbTBCUUY5QzlBSnlOSkRBbERlU21MSEI1eFVXbVdkaThydC9TUENJNlZ0UUZMNGZod201YnRkS0l3a3cveEk2Wit3SmNQRUx5NXk1bU9zQjh3WTJURG0zd1Rvcm0rZXZ0WHJEQ21uWTMvUmlwZkxJeG9wditCU1RlcUlsQ3pzaTJtYWdibndGQmN1dXJ6ZTFpL3ViS0g4SlBBVldjSU55QjNna3RnZS9mb3ZIMC81U3oyd2h0THo2U0lIQkNHa2xmeGxwZkcyZ1RPdnY0d2hVcEVlUlkzdVpzSVNxcVlYTXZXWWI2b05LQXNIYlJ3dUgxWFByOFdiMDA3YVhtUVBqOVZYKzJ3YnVDQ21hcDMwVDBTMys0Z1B3OFRNOFR1SUs4N3lFRTZ6S2twVllQZDZtbkUwMEFPbm0rVnZtNVNneUJXdlZIL01yRHZMMGFVMmoxanRyY3R0R1lDWFNVbHhyTnVSZU9pVlowTWxrOVpvd2ZhamV3WlRtWGVFanFGZWFnTzhzSi9tZXM1eUI5QndZUkF2MHVYZ2VTTXdDb3dycjczVmcxME5kV3RIdlZMNlNyVm1XVjhiRFhmam5iT0VjYzB4c1Mrd0V5SGdkRDdwdTlHT3Vod1UvM043RVZxekxwM1Z5V0d2aXJOTGsvTS9VTEVESnFCLzB2SllaZzkxMWhYcGppWGZVZWNoTEl0R1dMdkYrK0ViYUtEZ255Y1pZUGpaekJ4M1hGYXRRYjNGSDhjbGtwQmtSNTdQUWQweFhrQjRNd3grNEQxamhWd1Bhc2FYTGxhNlRyZUt1UmVXa1R1bnU1TlZtUktzV2t5RXEwSS94QVdFMWcyWWI4ekNSNmI3clJUSUt6alVzVndaTXZ5WnFZMXNpWG1FOEh1S0V4bjNLMWdqK1hLaVplN0VadEJOTVJwTEhadGFvUTgzaFlGZVJBZFd4U1RERGlBdWEwcm8wakdXUUZHUkhmcnNqZkFSbkNGb3lyT1VTUGVSNzc3YTk4RXVQdm8wSDhMcXc0UEtRWHlNZzNEdDRTendaYjd1K2wrOFkwOEZ5Y3krcTFyWGRsbndkN2I3QVVINGtPV2xBNEhFYk5uZUhabFJBNWdxcHpCMllKcGFFbUdnTE1rZ2xVdDkxN3M2Rzl4MVlheGZ1WHpzSlUxWmpnRWJSRVB0U3RTbTA1aDdhVDJBQ2J5NVpmUUdEVGRqekRWZ2FNelpZcFJFdVE5aGd5OU4ycVFMMmVoSWljT0hsMTl6b1NoYzVlOUFKR3BQRFpCOGRhdzJkWlBQd2pDcjdaalN3Q2pSTWQ0bWlNYU9iZXQrTGFLSDdYWXJrNXc3RVJBdmJSZjM0eXBoTlFFYURCNnpqVWdadlFya05DRldoWHZiS3pNRFJzN2dHVzJEN0J4Z3o0enNiTUp2cVlLNmg1ayttR20rWHV1L24vZTBBSDdweUFVdzN6bmh1alVWVlFjNFFkNWc0bk1xVVhUV2RkTWRjL3JrUitQN0lzZ3FWZnVWd1Q0d1VGR0hGL3ZMQngyL0Y0RUdFZGlJWGxSd1Z4TFNxWExCUEhGakFxbzlmOUtkMXBQYUI5QldoN2FDQ3loTWpBVmtsenBmWVNzQTdkQ3I1T2dwbUJROW9MdWtaSVlFc3NXN3o2TFpaWHN5OFRsdXFSWXFpVGhDdnRSZEZlczNYbFlWTjJEN2Y4cEVnclVESlVpa0d3dzBhUVJtaldhZ1pPNU1Hbm1CaE81WGJQRjhtWURBN2pTSU5QSXZWNUd6WlRTL0R0VkJndDhhckR0UHdMQ25ZZnJxRldLS1lnZmMwbVBPUVF1WmkrUWl4TVZIQTlmS3UyTm9WNCt2Q3Yrck9SVnhSNzVpQmw3YXcrRkJWZ0RGNHdpTERrNCttMFVNSUlqbkZiMm8vZ3lFSVdUcWdlMUc2TTlsMGZHUGxnM1ZvOUV5NSsvZ0JOV3cvRGtoL1lYNTBYSGRJU1BGQklETEpENCs0KzJLejJ3R2JCaExmRm15N2tjRmtpTjJGZm1VTGVDTHkvVllZazRvOGFOT1dXSGVSd1c2dWFoUkpNSUtnRW1wWEVJdk13ZllJWDlidEdwTS9kM1pJU1hqMEhJL3EwT3pSc2FWRnZTU09CVG0yR01MZStUbVNSTHFLbVF5UEpBRXZzQ2ttZFBteE8wNi9aK2hnZFZsWFBXVlhCNUlwYWtaUHFxbnAzcHV4aEJjTCsxblJzeVBOZHFvdnlLNUZVU1VTeHVVY1JJOExSQ0xqbGZPcFdCRkpjS3JoVTRnSnRjMkV1bmpYWWhtTkt0eFlvbktSQ1l3cWdJdXNHSWN3V3NyZ2ZwdjA4VTBjeGE5Uyt2N1NERlJTMm9zUGdLaEVKQ3RBQ2hjZHhQRGo0bk5ycUpzMHl3L2swN0oyL01xR0JsOXBCc1FhcnVKU3VFbzd2NURiT09taVNzTmlWRVh2bVd5d1hzSytaYXhzRFpuUVArbTlGejlpdVBXV1JnQmluYTdqSUNVNmhWZnJqNVVhOFN0R0hUV1N3OFJkZDJrcUE2MlE1U2NmaEVtbnZHT1Z1amJOenJMcmdBY0lzZlNQa2kzU2xGS1p3bjBEdFZWVmZkKy9KSVZYdUVYNVJOZFJBQjhXekpna3ZkSG5scjRpWnBQQ1R2SS9HaFpXQTN1ek5XVFNkSzdtWTBscCt0alRnemhQc2tRbEpQWEtnMHFtNGdsQWxyT3BrWmhCNjhFS3F3UnBkQ1hET2dtemxGYjNqeGhRcU5tN2ZzZkN6NXhSQkdtL0cxWDFDdkZKWTBJQXp1eVVWcjhxdXZrYkx2T2FISHdlRzdPWFZJTHhBS1dYZXVvVEd1VDhvQTMwWS9jQ3RHNjR6aytnbHJRTXV5SWFWcWtvNXRkVjRBZ3JJak94a09vbEFjQkZGRU9MdHh5M3pHMjdPUXB0VXFPaVRHSXl5NXQ3Y0JkdmxoWS84OTBESHllMWd5VDAxbXErbUEvYW1XZmxBOFg4bmlFSmV3NStNSWNMbGFiejJ6NWQ3MC9uMFZzdXdGc3U4SGhwV1d3cVpwRG9ucWl4ZGxiVUpONUljL01UdzhnNlh0ZzUyRGV6UUFQa0JMNFp3cHJkMzJLYlhrc1JCY2NHS0pPNzYxRTRqUnpEN0l6UWZKb0NBT3RzUmVDRWIxS3JmN0NqSDVqVWViWDJNNytzcXVZWG1WMWQwc0o2OXNMQUNzRUFKZ0JObHB2SW9qVWtXeHdvTEhjWEVmdUY5Y3I1MWNpNFJlVnUvWm43aGdnbXJSTXlRM2E2azRyN0xqL1lTQ2dham9RTHI4VldxRjE5TTlLVVREcWZqa3kzMWFsUERxMDV0R2JRbk9SbmYwdnJJVXp1alVFR3lPamU0aWhGOThEcGNjdWRlN1YvNDlxQzJJR3NWb0M0WVpXeHBlRVpJclEzNVVvZEE1WlNyM00vQ2dXQ1BKdUt0WmdFQmZMbDhmWjhlWGo0ZEkzZGg1bjBETnAxV3NLQ1FXdkVZeDdSdEYyVnJXamxWMC9pcDVNRDFndzM0TExycnFZaVA5d0ZlaEpzSGtPZFVnZlNJd1RjekdvaU1OMzBaZ0l3WW5iQjlOZFJhbWUxOWZtcktnYVNxTjFHR25ZWFEwb2pwMWdNQjY2VXJuSzEwM1B0VUJPMkphUWg3dWdGS0w1RFZ0Y3pHRUVYSE5pQ1JEWGd1OXJjdE1DYmxzeWo4YzdENTF5ckR6dklzaHdMeHY2RnlNcXZVQXBzekFvaGtjWTh2UTMxOVM1VDZHa2dWcWlKQ2ZwNmZFTVFsR0JaZGgxN29wU0ZtSjh0aHBaaE1sUFNmMlFOQUxqRFFKbFh3ay9ZYzQ3dUMwVHpLOEMyd2lTTjRJblJjK2RpV0lqMTNucUxoS281QW1KM1dZZTd3ZTUzcUg3ZlRsL0FXY0ViZFUzUGYxZmp0NXFBQmFoYUp2b1dLWGNwUlFtZGhQYXEydFNGVnBKSFZoSklURG0xMEsrZW02clMyempHdDcvRTlhUE8vd0pUL3VoRU5BYTZqNHdPaS8ySm5JYjhYeHA4bHlXL25Tb3hqT24xZG4xeWhRNmRpTWVKV1JmN08xVDJTV1VqTmRBUlZUV2htSEdpMFBaak90OXo0dUZzdytUNVRrWWFrK1pJTFpCam1vamRMQ0RHT0NCVnZJcXRhMTZqaVNJdE4yM2g5TFVQc0dkZnJSNW1tTFpWR29DZFUvcm9WV2tkRHl0blNGZzhtbkwwbnEyM0ZRY1pTTjJ5Ym9KWE4xT3o3dmFRWkRUNHAreEtWSm1LZFJicGx2MHdCK0E0SmNYaUV4c3BNTlhONUpsV0JMQzNwTmR3L2RNNU1lVGRaU0FpdlJIUVlUVm1XL0JWWCtpUDdaYnNjNUFDL3ZnR29PaVYrTkQzTVozc094aktUU3ZEdmpQRVNSWUFnUkM0SXRFSTFQRmliV01tcERnenBtdFRkR1dOMmNSNFJKeXJyRG85bmJxRGsyOXR2cHR3QmhkS2RoRTN2eHo3U05WT2x6SXJtaWdVbTlRdC9IZkNNb2JBdU9waWNzbGE2UFk1T2JjV3o0cVZuVHd2V2hKcUErQmUxUkFGVG8zV3FTazdnem5ZWFlhQ2dzdE1aTVducHlKajA1cmUyaHovcVdPOEZacS9XbWVMQmpSRmVKT2hFRDhHWXpiazFKSHVvaFQ3UlN5ZXdSeUc0MkNTMG9rRDdDUTlwdmxUZzJxWDU3OG95VXR2aVFUQWpYZVFiaFVaa0JPZWw2L0EwdzZyR1A1clA3R3ZRZ2JJZ0c1a0tLYjNnMjc0QjdpWHRqVkRzcE9CVHdCTnBNYStOeXBtYXdsSGdmY0Q4K0hVTjlBSGlKeWpGYkVkOGVlcUFXWjlmZmRibkVoczBFUnB3Uk8zUUlRL2hEejJweDVJdXBTUStlSEVIRUI4bVJRb2tFaERXL0N3R3doTmhuNE5FMmhXNDFLbjMrQmM5NE5XaDJ5ZE9HYjFZQzNpRDJiQkxJNzVxSHMzMDZQZnNEamtqTXpaU3lXQzNDdzY0QkhPUWtYNVlLTmN6ZHc3MVN3ZzM0VFB5RzBVSm80TWdkcGk4cGE5dVVVajJHOXp2VDhvNDJLNC9vVUZ5N3NjOUpFdWNYNlY3Z1dmWGMzY1pLenFJZURpdC9EOVI5UnZTUklvQWVvVXM4UjRZcER5K3JRR1lHR01YSkNmU0JlbllINm0wVDhGNnQ4TWpOTmxoV0c5Zi9aUGZZdVd2WE8vQVM5eXh2eExUYUxUdC9OSnU1OUcyNGhZWk5uUThYWllGWGFvOGROcTNrTVdwanBTYmVnTzU1VXplVXVNcUxXRzNtc0prSWR5SS9lNXNMVWt0K0VJeVBnVm02SVQ2MEhPb1ZhL0Fsb1Q3THQ5VW1SZTVoenp1ZHdrVjZVelQwSURnOCtpSExybkQ3bDcrYmFSRVlZZVVVRWZKek0yQWQ5aXBTNnRaVVlkZGN1bGE5MGRGYWRMTGZTdVdOSE9EL1BkbE8zRWVKcGdPblBjRE9wY3hPSndBZ1YySTB6YmRIRzFCVVRzbkdvRFNnQW16N25SeGJ4SU5Gb0VNOU1ETU1qem1NRmNzMjNXdmpBRUhydW5zckVobFJlRDFEaVpCcDJPMHJPeVhIZlFSRzU4VWhRemU1TUp1bVpLQ3F4djk3aVQwL2FJUElrTU1CeDA1UWlZSlpLRnQ1WkdEY2RSMjBia1pZMmVTRm02M2NtZGZWZmdyVEVRTFpnZk5tUnRJZllVeEFNT003MUlRRHNsaXZSdklqYWg0RnJiYWZrM1FwWEJCaDY2ZEhZODVDeXNWZzV4ZURwdXVybmRjVU9QSUhDaTl0S1lMaktCYjArSkZsQXBIT1M1NVJBb0tWdVV5ZXNwdEp5QU1SeWNMOWUxNTgrSVZXaUZPWWF3OENtQ2tWTzlHdTVBMm01bEdjRE9Bd0E5M091Z01KTzlNcHhVNG5rTlBrNG91UEM1WkV6czdrMmZYNms3dVhrNGxOTWZmbUl2ZC9GZ3dNUzRkVFRoZW1DOExhbDlzeXRhUG1YanQ3TWFENzYrUEM0VzlDTXFwTytZL1kzOWZyRXJjM2JFdVNPYmY3eWdaUm5aWnBCMlJPSnJpaStzSEk0ZFlqSitSWU9GZ3ozQ01YSnlFWll2ckZMdzd4emI4V1VqNDVRTC8yVTJ0YmlVbUo0Y2ZRdXd0N1ZJK05tWXd5bEQ2aFJOOXZuYWFwTFhXSUQxOEFNZlg0OVZJZ3p6Lzd3a3dDMUkwbXZheG5PMEtpVDRYdXgzaVNTZjhabGdCMURBTXprd1hYYVVZam8vNWVqY3F1Y3U2clJQQ2tKNlppTmU0QTZrb1hIaGh5ckVhOEMzUTlJT0NldzVadmNWUmxLWkNienh5VS8yRWhBYmRvUThuQWxQZjhNMGMzMDJ4THhsSXVuODlMQ0JyTHI4bUtaS01JbGw1SzZhMFMyRkhGVDJKblI2WWFacFFTL2hYWG5vU3JqclZZY3JYbkI0N3F0Q3NUVzk5bkpLK3MvQ1AwM3h0N09XQVA3NXJ1WE1MUy8wSzk1TnNMUFVTckpKRktDUmFybUJHMktTQjV1Q3FheHkzcHJoMWVDTEZyT2NpUWZNdk04V0trVm5aT1krSytwVC9PZUdGRHIwZ3pla0hGdER1TUxsQ2VWbHVJMTR4YyswZWVXalNNd1M3RzdIMzVqRUJacW8xSy8wTVErbUx1RHdVOUZMbXgrZ2lLdmUrd2JYVE1jMUJ6SUJqOXNhZXo3WU8zMnlTby9uRTdWM3Y4YUh5NDVRRGJHVVI3dG82Uld6MWRPR2VSb29QNVUrRFlDWnB3ekd0UTlDWkVWMmxaSWZSS1pncDRYME1QL0F0RHBhRDdHS2ZGYzFlc1ZSV0RqbFlROW8wMitwT05lZDQxRVhvdjVYNEtlNmJ5UEdXV3BXNXAyQnk3ODBYRHd6ekc5ODJvM25mN0ZEclVHMjZZWklaajNxWExKQ2pFcWliekxXMDRtcFFZZi9oUGpiOFpFdGZNeXdvaEdWYzlxT3R4RHVtcEtnaVYvUEkxRWgzaVRaOXNzWlNINmFLckpGbEpLalBVNkY5Y3dYYnlTOFFydjNFdTgzYmJhTm9GZGI5Ym82VllSL3FUL1ZaUjRpZjVSVUxBM0RJcWc4RDNUZGY3Q0hvTHBNUmVUSy9vcVZ1N0QyalY5NUphMi9zRVlUbHpsdVMwWkRpdHdZaU96VE02N2Mxa0Q3ZURqMndnUVh0Wmh2aUVJNGlqb290UGVEbXhwcERCZCtVemQ5UXdSczhDUHdhbUxvaWo3UndCR09GT2JJUmdZdUhhcFNoZDR5eWtibXRIemJDRjB6emx5UU5JblZkOGV0aFdmd3JQSEJRYXBhYy9STmQxbUYyaEpQQU9FNVZxNytUS3gxYnBmRmpMcFY1OXEyenE2eE5nUVdyTWFwclN1VHF1MUdxWmFhSWdNOS9jOGZjUi9RZ3R4dUQ1TnliWjNUWjJoR0UwV3I0M3ZqV0U0aXlPaGt0OUJTQXc2QzBpbnl6cVY1UEs3RVIwMlNSb1c1MDNBNUt2ZXVVSTJPaE1OVHFWL1lMNWhUNndCZVNaOHlJTkhRYm1sR2dmaFMyK1ZRNDZMSjRJOTdQUjNSeVZ0Z3dtZUVZTGlqZmM5RkQ1UE56YTBDL0VtbTZTVEVWc3NjNUNkQis5dHptWFBQWXltRE1HaktBQURpT1Juc0pDcHBkZzRsYUJjcS9GMVc4Yy8zckkxK1VaVG9CQ2U0M3NObWZQOVBnY2NaOGdwRytVYkJ6ZVRVUlhjSXB4emc5alJia056REkyTXV3aitmSm82dUNUL0xhNEd5eVNveUFxK1BjYTJ1ZFRXZ1R5QzJnbW1wY3B6Qi9qR1dyWlhXOFN0M1pRY3VXN0lYcVFCcE5hTDV4TXlETytuUzNyMEVuZmM5MzFYUjBHdTRGaG5tQlVMV0djK21MZ2tCQnlEelpvQXhSa3FRQ3VQZ1pQUDg1Q3BHZm1lclFRby81U3luZnEyZHExZ21xb2o3U2w2bGsvNTd3UG5PWXJrZjR3Q1NVN0tsdzVlQkZrMFBOaGNGRXBkRjVXblRNME9vMHZOVnF5UHhMOTU3eUdHZmxlM0h4d3E1YXZxcHB1d2tLZDBCc3EzUjdhdlZPVTBJZENkSC9HVVBRVXJiWFVwM3BkTXhnMmROeXhmMlRXWlJaVlBEeG9BTDZhL0hDOWxUVXNJRzlFQ0Y5TXRnRDlybnFGU0hBdE5wbXJMczZTdU8zTXdjZWFHeWM2YTdJbGVNOXBPM2ZOdjJXTU9Cb0h6WHdZbU5KRzdRMUdXUFlGaFdPdkV2QjFUNmF1RGxVNU50NGFiTkRWeG5nYURQZzRyWDlYMUtIYWk4ZkpreVdZQlI2WXRGNmxsM2J3SEtsWnRBMklNT1hLV2krc3JkajJTWFdRclhqRWRRclFRd1FONlBQSk5OSWUrK1B3TGpaaVlzb01mQmpLTVlvelR3dS9xMDVXMVRncE9UU24wWTFIbEpzOG1JeUdBSUdSZUgzT0x2bWd0SFRtQTBsVkF5WVpwRlRRZFVDWlNzRHJaaTd4TkFmbGhLa3RhdGlxdXUyMkFZTkFRWjRGSExXb1N6Q0NRdHBWenM4cFBWb0wvT0ZkZk5pQzdsUitVMVpMUTg3dGpJUStaZ1NHbEhvcmtENEk3dGdPL3RFRWVYOWlTNzNJenJkNjJ2eGFsYnBGcWZoZW9mYlNVa3dscDNvbDVsOUVGaE1DY2pXSTgwa1YzMG83MERXRjZoVUp6SXRkZkJackUvZEF2L3ZmS2VvOGlQazdxR0hLSVIvNUJkUU1EZnpPZkdZTGVteGVqUGx6Qk1GWExyQ2JUSTBTRkJTWDFZKyszWW9xZEtxL25tSXpXZjhnY0VGd1hpMzM5L01mcWFZVFhNNDQyMlBzbGQxd0NMY1ArV29LbTdQYlY3cGRHVjZhQ3llUW1wWG1ocGg0SEN1MFArWVVCWWZ3bW9udmRoQmRjWHd0M1YwSnRrSlFPbXNnNU9zNGVOb2IxL2FJZzdxcXh4UmQyMHRiUG9ZdWQvUWdnUUFmRmN1d014U0ExeGh0Q2ZLUXNmUjJ6UFlicFRuaUtyMmF3SG5yZHhJZUZrbjJnbnBLTHA5VVVMRzlNWHRXMEtVWW1FeGJCazJ2MUZVNHRBeExkR1R6Rk5aQmYrRkl1amFuZEZyZXdZcGMzWHhDbUFQNkxESi9jRTNwNUYwWW91YXU5NnJGclgrcStUYWp5Z001NGNnVlc1aXlTMitZcXdvWk00QkVlNE01TXJBejdUYzdlMExpUldJOTZFeFRpQXh3QnY5cnBlVWMvekl3NTNlMmFyUE1Ob054WkpOQ0ZueVJwbmN2a3RtU2loUHRCdGVEZ2JSM0UxYzUyUElWd3kzZlJRT241ODBCT1ZJRndzb3NEaWNPaXQxK0h3bjNzVWlFTi81UVRyVlg2Wlh6bmUrb05IOVhrOXQwNnJhREp0Nklmd0RmcGZOOVRHQzNWcENNRXltTE9FcEdaTnZtS2hLWGMzbXpCamYxTW9tWGJFQWNFUS9kSXJUWFFaYVQzZGdFdXp0TmNzcU5paGlZUnc3ZVpDVnVGc3lKUGRpaFZ0NGpqOWg0NE5JbSt2bEhZRjdCRExyb1NsYkRqQWM3a1hWS05WaGFpSmw4TUtFTDdIbkoyM0E3SE9PelUrQ2ZFcEx4QkdYQ2c4Qi9aWUZ0U0NrRVp1NnNTNzk3WUNnbHVHT3dzSTlSamViQTFqTFpiVHBVWnNrZUJERXA5aHZzNGtCSTRQWGV3eHZSblJNZVl5WkthVkIzQjBEd2x3OHhLOCtiWGRQV0Zha2Q1WVZxYVhDRTdTeVRUS3lDcVllaEhlTkxPQmw5Z3lxNXFZRkJXcWd3WVErWGt3Z010Wit3eXFjRWdjclJvcUpOcVpoejFsdzRGZWN6cG43dngxNUNtTEpXY0NsM0liT2xzOHppZDdIQ0UyVVJZUWMvNTlEWHBWQ1hLWEI1NjgzZGQza1QrNVAxbjdzdytOT0hjOXlRN01POFR0TzNMYzJEdjN6UWtDalRza1dJa1RIbGNwdDBWMEpmSVZQWitPWXFwK0FQUWpQNlVCOGFGUFhiUXUyd0toZnlKWkozcklwMUZkTmVzYk1oMGlYZktvME1ZeDZBeUlYQjhRWng4K3Z3U1RZOEtKOE5hNythTTJxZEc5VlA1TWp6STRkU2huWUx5enhxeWp2dFh3SVpXUnFYakd1MXVCc2pzNHVmSldiaWhhUGdEQVlTYnNZeGtTMWpzUWtHbHp5YjZZc0JhSy91emRMaWEvRXJNL3dXN29qZFlpSFU3amVlbWx4ZFIyNGtzcFdEN2NodlhQVTgvM1RCZzM5NUMrVjV3SjdqVlVSV1poTE1ySzNidUJGN2hWb2lyZ0lFemNJRkxNQ1B1TVNNSXZuaTIwU0FjNmdFWU1KN2dUTndhN0svSlJPNU9zNWpIRnAvd2N0ZHVUSmF1WE16M3craDd4S2lhSmM2Yi9LNXU4bkh4S3NvSDNDK0c4M1NnTVdSUW0zSGk1TXd6Vk9HTnpQSGo1YUVLTENRdTY3SXNlRnBMd1hMT2Qwa1JnMmZNNkozNjRWL0lWT2drYXpzWnI1V05NSmhaQVE1L0U2T0ZPOVpneW5FaG5PbnhhbzB3OEhXZkcvcTFQYWhkeEZ3bGZuMHVDT0RzYjFueENlMUpidWtFVUl1aXQ0RXRyZ3JzUE84dXdaMkRtWGZlUUhSZUE0QzRhb3puYm1YWmZqTVZxMklqdE1MV3VhbTJEWGk3cnhZamJhU2xpbWhGaVVMc1RKUkpxZE5IdW55aHVpSDdHZktnS0FQaXZVVkcvamVmZVQ5cVN3Wko1d1Z3eDlRWVJkc1dLR2lMUlVjWFNZSHJHYUZ1Q0dHb2FVOStsTGYyQ0RjV3dlcTZmSURhU0RMbUU4VVcyMkh3L2QrYmxoVlBvdTF4bTZUMVZmaXhHQWR5Tkp3cVBqY2ZWV2xOSitVUnNjR0pHMngwcGI2dG4vQ2Y2K0RLNjlzakw4Wm9DSlJYay9xUTc1WmtLSjZvTmVxZzVnZWxyNG5vRFpVZE02MnlnampkSFlSd0FiKzdmY3BxK1pHa0VDcWh1VWVUNjJUUXIxYUhGM2NjdDF5T3pDc1BMUlBpbi80VEtrOTVkVCtGa2xPbDdqdERnNS84dk52OUFjcEpKWkF0SDY2ZmE4MzA0S0p5ZlpKaU5Nd1NTOHp2R0hFcGZjVjRibmhGSmFvRjRmNHJ4aXJGNUY4M3JvODV0UWl0MDhudDhvMjYxeG9wdG1qbjA5d0h0OHB3aHovU1R5cXZHU1hkYmxYNlgwVVh5UUI0WXVLaUhPNUZhbUNDYjhibWhKWFFQMWxqaW5QcURqNUZ5NHovcE9Ed3VEcjZzNW0zTk5PcEJQYjJLZkZVUE9MNitRVTNkYThwUGhqY1ArQ3F1NGhZVG1ySyt2ellkbU5tbWt0OHVzZitZbElHYWlYZ0o5OTJ2VGczbFE4WmVJdnNLUEhRWUVvVHJmTHNNNm43N1BmdFBPeUF3YmhvOTV4ejRNcEhtVFJyWkZxaC9VSDhsNk5yTDNTNXNzcTl6ZGc1STN6RllFYVMrclBtVm84ZEZiZnljVW02UEtpMjhOTG5IdWNEbVZ0QjlSOFpSbVFSTE1jc1YzcW9KbUora21mbVpmU2k2S3lqS2FoblA4eWYrMEpIYU1tSzJIcnJROU85WE5vU2phdWxSUmFKRktiZFJlbkZjM0xzRm9ZaUhTYTZHUEtUTTZHQ1RCbjN1VkpCRXNzQTdRTTJBcDE1K3dxUFNRdUV5OGNROG9YMzIyZzUwL1orTkgxRGwycXd4K05hK3BOYUhQNU1yb3ZqS0NvK0FZeFpYbnpDemVnblZ0STVpU0VkZnVYR0lMNzhGQ1ZaNmp3eWRrcGpSRVUvWVVCNzQvY0lSa0tmYVlhUzYzV21QSTRqSFNucjJuT2FVNUNobDIzQXdhRGpMT1NOYTdBN0FIa3FnYkZpbk9teCtYaTZBeGtXY3YrZjVyT1dHUGVjYnJBL2xDdHI2R3lXOWRuMTVOYmFKMUtLNnNSZ1RTMk93aGFzU21CSEJTNVhrUjBDang1NnBEUXVNSSsxem81Qjhkc283eGtKMDEvVUM1dWRQTnBkZmdaaU1vYUlqTmhzMFVuNGpRTDhTaXlWbzZMd3J3WkRPbHRCeXJmQStUczdFaytMbmJqQWJGeFlrQ2QwbkdSWDdTTWlKTUpER1hGUzFlSkZFd3BPSFdub2ZDM2J6SERobmFpb3llYXpxbTdaOE5OeFNnUWlUWTRPaENkK0VDMWZmMHcwSW5mMWlSUjRmaGNoV1gvUHllbVh3bW90Rlc0OHhHd3ZVREo5Vit4dWU5TytEa1luVFc0Qkc1TmxISTlUYjZnTDNNRUJ1UlRCREZ2bDBxbGVzVHlpWHlGZHN4bmdkaUZ5LzRveGZwZXdlNWZmalRUaTVFQUR0eXM4bEVsT0lrTUhrem15WG5SRll4T0FsZWt3V3B3VStBT01FOFlacWRZcFdvYlNqWkt2ODNRTk4xbGVHajYyNmw1YmI2RWxXcGRsUlcrcVdwQXpuaFJYcEt0a1hKaUdHUTVJWVRwSkYxN3l5Sk9SeEgxejRmei93a3pScUxVTFljV0lwd0RMNkVqU2gvblZYQ0VqWDJ3bk1DcE93dksyRGhDRy9kL0hNTTVjYk43OGxocDZzWG5pV3FoT2Zrc3YxRzRsTmRrVDRUcnpmSHpXMStHVDFYNjhRUjVZNFJPZnVzcFZlak9Ick9vSG5WUkRQVmROeUNOa005UER5eHhUOHZGTXV5SVB5Q0MvbDRQR0RxL3FKKzVlS0JlZFREUzlJRlVYVDVHdTVka01GYi9WR1RkcEYwTmZQZ2VCN3JLRC9yU3J4QmlCUnBUeXFzM0hucGl1M0xUS0cySzA1VEwrUTd6S3BkSUc3NGNreFp6NEwwNWdXdlMzOCtiSFk3bGFzMllVZ1pEeXpSbkhDRVZ2QW5hL1QrelNlYys4aWVlU1dpVU1SZjNNSTltM2xpMm1jUjRWaUtqYllMSDNpdkhsQUYyUUtITEo0a1RMeEVGVTZiU2ZnZGJveFBjc2tBUmZWbml3aGk0N0h2Um1RcjNOTGtsdzBpaHU0WGc3ZENCb2kzcStjSFVRS3dhbjUrUkV1SFhiZFovSzdBKzltYU9wR3Z2bU45Unpad3I2eERhQU1sYTNlSlBNd3c1and5cHA3SHhndWpOb2hvNnRsZ1RoQjJRSUVPV0hyYTJKck1sNHpOV3B4S3dIb203RE5wNElZVFpyQnh0dHpCTG1udjJ5RzBHYWI4UGhFTWFBZzk4dDZBelhveWpqL1QzMnRUZTAzSDFIWHlWMlVOUk1KWjhya2UxQ3JKM1hsRTJUQVBUaXVsZi9lMFpkMWRiQ0xhd3dsbXZDd2NmajU1Vi9jQk45NzBpMERHdFordXExbjZjbFVzMTZFR1Y5RG5nUDgrQm4vcVdZa2M4SXhyNDdwR2VtSTVLWFRCMWZ6RFphTWhWV1RsZVFnalV4ek56YTFrQVBCcHQremZ5Wi8wa3p6c09jMTY3SUV6STV1SUNRVk1rMmZ1dEVNUTFWSDBjQk1SNDBtUjdjRzR2SmdWY1N3Z0V4U3RySHpXYjlacDlxTWVoMWJBWkhTRnB6WjliY3ZBNmdKV215cVREVXdocTA4d0pvZkZsc0dNK3ZjazNNaDRGeU1JczNHbFg3aWtjdVNNWmhWdGdWSW5LdHRpWVlteHFSeXd4U0pNdklaamwwelZMdUhRMzN3VHZldU5LZ3l3NU9BUHdNMnljK2FzaDJ6UzFjdG1hSDNmaU1ZMVExT2wrdXgzcVpFMmQya3BvU1NLTjNlWGNCUzhTZGhKUjgzSmlIOGVTNmpZdjUyV0FURXEyeUFVNFF1WVovTjVSd3E2dnIrUmtoSytHY0lYaXNtb2pRdlAzY3pnbUNWUGkzSVZ2YU5kcHBOY3hHNk9LV3RvM1ArTDh0ZE5wMWU5VjIrT2YvRDM5b0NuNWFBUG1iQWdOd05jL0tCZkxPa2NZaHI2MVRNR1htS2FMRVpEN2ZJdUxsNC9NOGxBYk44dzNvWVg2NHk3Yy9lSVl1NlAzUzVUbURIU0lnRDQzbURNWmMrR3dqc0ZwS2U3Tm5OdCtFMldGTkxlRzhWYlFTSHBKZWNvcTZLbkcvMzdMOENINHUxU1ZQcXRQcjZOVkhuTzFGR0o5ZnZaMnpSZm9YQ1kwUWpRejhDVitjUzJxczJPcnZ6amdaZEtpbUdYMEdVL1psUFVzQ2MrTWxjb3lTRzBmNnk0NktiT0Y1NGNobW15RXFwbWF4TXdYaW9LSVd0YVdRYTFnMVhHVURuU3JiLzE4SjVJZFhOaHAzdHA0VzBBYmRuTjNqQXpWcThFTWRLSjh6YWUwQlBDV3Rpemllc1RwWWg5dlhrUXFKcW44TDBlZFlKengxSkY2UzVla0FsTld1OFZCQUJrYVNlbDdNRVo3T1ZRSmZZQkVXaEZZMnhPc3NiK2x0TXdManhPMmt3M0VDcWJITDJDNXNwQUhLb04xQTJZdy9abDJDRHRzN3FGTGd1dW9LMEtheG5BS21lMUJYdSthcWFjZlpHN2VPZ1pKRW5rT2VRWWR5QWpRcDV2UnpTSnh2MzhPRmZwWm83bk1Rak1uOUdjNC96L21YamliTE5KL2lTVDE4VDdZblJrUy8zMXQwUmh6SjZnMWwyNnNicVBQVVgyTlBCb1J3VFR5Ym1hVll6RnRWOWdod29jWGxwVVdacEUzZ1dkcEFsU0h0d3BjVXpuL1JLbSsrNVpYbm1qaVY3Z2dqbWV2UzR2alYzRlF0MU5EejJsNWFNVTRnMzhKa2dVbUZicnppQ0wyblJndGl2NUxpbG1mS3NsRTdZdXNDc0VVeUN2QzdYZGVJUlc1WG9vbEh4YWprTGZmaS9tVTlWQVhNbDJocFVneC9ObEU4OHQ1UWo0dUNTcE5SRk5rZDY3R3FiZzFWaWE4S2tQc2pRcnJCc1prRzI1MXF5U0dpSlJsOHlla1ppSEU2Mjd0S2FDUFVNbzRlOHN6d3FzNnBnYmJBa2YvTU1wNUw0eVJvMUlFUC9OZ0FuTlUrNWpQOGtMWDFtdEFQYjBiSW42eTRyckV1a0h0NmVmd2RSMnZlSW8rMDRpdGN3RWRGZDJxamM0TXdjcTB4Z1MzR2tPazhScHdrbnFubkdLQnN4YUk0M2pkYUlMb3haempBMU02a3Q0cWNoK1d3VXExd0pWT0RxVzNRNVdDYitkWVVoYXB1YnZDNndqSGt2eHF4cU5Bd1NrbExSVXpINXZzeHQwN09CdUVkSHFzVVYyZ0RPb1V5YWMwVVdld3RDNDhPaGlvTW55Q1ZKOWlUVkFpbzF3UUU2VXRvYVZaUHhzcVVFd01Hbjg5WXlIQ2VPMHR3cHRYeUNyVTQ4UkNPUlgxWGs5M05RUmRoVTZRWm1DSnZjK1pzd0NOcDlXVG4xUG9nYzAvNDRWSm9qaiszYXZmZGZuUnhsZlhLaHZkaFo2T3pRYVJ0aFR2UlkyM3VFWEhja01HWTNVUmpjS2pzMFRNeFloR2d5NjdWSWliMWRoYXNkbGRVU0cwZmxiaThKMUdDTElxOWFNeEZMK09zMjYreGsrWnJyVURyYUZXN2kzREZvN1N5d2VsSDMrcjhUNExPbGM4anh3K1FLYnN5dFY2ZVRWTEFUaFE1cFZUa01mQVVnZ1F3YTBLQVNaVlVhekdTZEcraUJaQTNwOHFhM0tIS3I5aklma3BuVE9HN2lXNmw5OVJZek1MdSsvVE5WcU1iSVA0NHVqTGJDUXRXb0h0RUV4b3ZBY0RHdFBmODI2Y2wwdHFFMldERUZqazZuS09YcjRoYkdXR3YwY1lYVjc1b0hocXhwb2dwMzg5dnZaVEZCUGpBT0U5aWx6TnhPNWZyV0tTMW1zTlJDUDhjcXhia29rZjFIVlNHeDlKM1VOdE1ZV1IxUTNZMWtZeXdmMllLRWpaN1lISGtWYm1Yc3RGdWNKblZRUys2eURDNU1hTjh3ZUVNWjVyNEdPclNBd2pWZ3FJMnU1eVdvWk5scHM2TXJSRFJUbElOTFZpRjJOazlKcUptbTJtb00xdVZHb1pMZ2gxcEFNZ0dBS1YxaFliWTAyUzhPWTlhcnpPOVVuS0sxa2VJdnZKQkxJNHpyd1pBZkxjM2IyQjFJcHNxS25hMkFTSnl3a3QybFhJTUh0MnNCL0lmVGZDWHVIenhBYitkOURsQWRPOHkwUHdnRlFtbUFVRGhJWTFkL3h0Qmh4bVpDdlNmYkxrcExNaElSeDRlYWRBN3dtV2k0MkdDTDFRclpDQUdJRmxmUjRpRk5ZUE5QRUtvMy9xK0U5SFhhb0dJSnFQOGpLUDZXVFJtdzB2b0poMHlJTTdCSENXSHpEUGwra3FnUHRRY2pObDdCenJQQVRJSTRBTTJ5RGxHWDJueW5XYzRWanFnbHVOT0pWanpVSmc3SVhzV3NUckJ6aU1zanYwc3Nrb3RDWks1MVlmV2daNU9jeEhTM2hEUi9uejFvdCsxMGxzeEhsVldtTFNsYWVoR1Q5cGg5LzZzeHdiQWp3bE96ZjlVRXhEYklBOWg2UmhzSFIwZVp0Qk5lWHpWTCs5ZTlYY21EV0lSNDhhbHJJY0daN0gwNW5JdFNMMHpxcjIyWWZmdVA5VnJXeC9NQlZFOUpSOW0rV1lxNDNvNUkyNGhrUkViTzNtSWhpa1Ayd3BBR00wRmNGKzRiekRLNUh4dUxXS0w1bDY3QzBUYUp3LzZPRVRtL0VyK2RMQjJQM0FqMVJudkVMalJPaGI2K25ZUHlzbXppZ0NwcHJqMTE2TW9UbHlwMGRNUWZPclNsSXgzOGtTTzMyRFFHbTNkdVQxSC8zMDM1YzFlOEh0a3BOUHJFZSt0UjJ1ZXdLc0M4T0NwUFJEZGZuR3B0RkhCYk56QU5SWFBsT2FhdHFuSUtRMVJ3cytKWXFkQVducjhkUkdQcjEyMUY4aEJKQmo2Z0pIVnZ0TTZxV0QxbDczTStkYTEyam1CQWpVMHhaU2RqNGRVNXFFR1hsMTYrTFFOOGQ1K0VTSFVXNWgySmE1azdsU3p3OG55WjZrWkN5Z1RKVWFoR1dQdyt1Y29sSm9xclZCbDZpdnZmYlVaams2aUZNY1k5RExtSFh6NTNlNDhvWWdTOTNuelQvL0RGRUVwcUUyOHhHM2xISWY5OHpBa0huRWxNRmNiQ2F0WEhKdGNDTDA3UE5mNElON0RGekxJWGRDb1poUWxReEovVU5jNlczVEVVbGpqelNwWldZclZWQnR2OFpRNnZyV2FkZW5TT3ZGdkJNL1RHUktZeUdoWmNaUXkxSGtjQ3BVLzBIWEh3aCtuNzBzbUpCclNGeVU0UU9OWmV3SmJla29VM1VETTV5MWZ6QVg0bGlZeDBRSG5FOCtta2hLaUpmb2UwV3c3bjBhbU96RDRxZWJkRlVOcXp1cWlPMWdCcDhNdHBFbWVzZ0VCa1huZEgyRlRmNmJYWm5OU2IxWWlKWDFjUFBoWU9NbmNpK3VSQlNKYUVZMnplTWI3dnhyemVmdGlYVTRTZjZlMWtzNENVeitkNFBtTzNuMWZVakxtbm1hUXZJaE9jRnZlSEdSZi9NZUk4K05yMWxnL3JPQkJvVEtmdnVSZElpT29pRmJwMlM4c3hZVmRiSzVMbzA2WnB1Z0dGMThwY1psM2MwZGFpTHdCR1hSbk5VbGs0QUpBNitlY0JaNWxQTW1MU1lDRTNmMUZaS1loOERneFN5akRQSmZGb3lBTjJuTFF0UDZJSkFJK3hpakN2MTBFQXRONXlLTkFTSlg1Yk95WTNIL1BxL3ZoSEk4VzRvNjNiVFZ5SVdZRTd5RlVnTkVBcHFWVVNyT1ZpUllXbkc2c0hUMDFsaElKQnZvOTJJZHlVSXhPcGo4UTU4TFVOby8xT1VKUEQvSTIzTGZCV2QxdVBMUGtBNGJFNUpEeXY5a3VIRWYxYlBDeWFDeC93TitidzVxb0hhZU5IeldhbVo2NUJzcnd0bkxTMGNPemZvYkUrUEZHdHlLSTIwODBnMjRjcERrUDJ4c0xRT2tIMHVVNzBPdkN4VGsvbVAyWGFDV3RtVXhzZG0zcXkyenoxckxUS1VSbXZFME4yRUZUbnFkU3hxbHh5K1JPUXkwOUhVUG8xWnlGeERTSDZFR3dzdUJFU0hVN3IwbmhoMjNwalp3UGN5aDY4Mmp5SnZIaGE0S1R2RWp5ZDdUYkRUcWdqYnplT1MvVmtHTzQ0Z091K3Qzd2N3YUZsTWRYMkRtMEppcEQ0dHE2K0FyS0xROFlsY2V0OC9zaEZVVlZCWlIyVHFPeHVQYlBlT2hrNVBZMnhsdDVvK0doZnVNNm42UmdmSGxqQXlyUTl5RGFVWkpVRGM0TDJ0VEZEMTJzaTU4UW13Q2NUd1c5K04yTmwzVENlaTk3VmVTdW9IY3lCY2w4ekJWOEQybGhpa3JwY3lURUpuYVV3dmtWdDhVTG13aUx0RWtJU1d0Mk5qTlNuSTV5c01QYm5Ha2pmenJ1Y1VnMjlDbDJtTHRPaTc5OXl0M3dpaE14cGFRNDUzY2JZMlR4cWpidjQ4eUFiSWtqRmRqekNBSHl0bGtyWFVEcWlaZnFGRTdsUVZjdm1SNWd5bzdiQUVlWUdRNWVSNWRDMUROQXNqL1lPQlMrSmd6VTg4VzB5QWhCME05UEhLRVcyU085c2RWZklKTklBaHZ1SWE1VWxuQmxDVnZPSWhaSkxaWXVFaHJCK1htNnlkd29DME54OGlGcERLeEY5dnlQeUx5V1BFcXJHY0FidjlzSUMzWWs3cWI2YnlCVC9xYU1mSHJDOCtvNFExb2FUY1J1QmNsUTdtbXROVnQ0V1loSk1OLzFnNGpOR3d6UDdQR3NKUU83MFNwdGhkNHIzbm5aaDhKeXRBLzlyc2RPUDMreHF4cFJMRDUvR096MkhZanMyMWRJUUlHZzMvbUhqakF4aDQ2RVp2bXR0SW9jUUlaUzB0V2JUaTM0SCtqQ1IvV0hBOHRkUWk1WDZuVXd5K01TYkVFbEVBK29qOEtIVmU5ejRiSlQ5a0xBVUExaWhXZmNoeURzUlBOOXp4Y1QzaFRaNWVaY3E3QTRwUW94ZzFOUHE2a3NDcFZ1bDVldnZJZjNnMVIrVVZlNndGSHdUUURWMzVWM2p0Yyt0dW9sb1JFb284MmNrU0h3WDVkN053blBaOHVlSzBKdHRKOUx3cTJsdDNKT3U4OUQ5ZFFFWTF5UGFsWXl3VGZQdFRIakJwMGZSdUlwcDR0V2VQRkNrcDBZNjgzK3dVVE9yNHAxd2g5RGlPTDNFRnNFWXRKS3BTalpGdklST3JnYVFCMHUxZW03Qzlmd2xTd3pLZVZJTzR3SlBTZXk5T2lpeWFyUittc0laL0pjTWVlL1FVZTh0bmNNOEtnT3NjQTZ2TzMxMStBSmVqcHI4bkRNZnArcWprc1R0WXhqUkhhRWc0RkwwbHJ3TnlrM3FUTjdUa0JKNytFWXVhRm5BWU95N0xib2VhcVV2UzNRRGcwU09aM0Q0Y2JjV0FTb0JMV05MVyswaVFTdFREL2JwMHVRdU1rbzltaHQ4OWR6dWRUNG9HN093b1Roa0s1TkFiM1Z1K2NUS0YwYUgvc0xmbm45ODlFK3NmRVp2RVRTV3Q1a3UwRk5RWGFOY1pJOXVwbWp2YURDNWdUNGlaVlh0UzF4SzFRRWc2U08vTmVMeWNpeXhVR1M2ZTBrM2FzMTJNdFFtdk9mSFFGSGdCenZMY1FGNEVxTkxadTZwU3V0cWsvMGFtaXJSMjdyNlliZmEwcTJTamRybi9GZU05NXVEb2ZyYWZQUWY3c3V5WURmNzh4bzNycVNudHhPUW5nWFRCNFpDbW9RVlltdnR3OUZqaFlSRkhzSGl3TVdydlB2Sm9MUWNzamlTRzNMK1h2bWg4MVdRNnZENEhjRGtzcWV4Y0FmeXRxYTRZcVFSbFVCZ2JTaDZVWm1QaXdMdlVRMENqTGtRSjBnVXlyVVNwQ3Uya1RxczE3ZFZJeTFIL2JXQWRDRUpkems1dnd3cmNkNndDeTAyQTNDZWpDdnd4R09LZDFWZ3RHd1phK1RkQTJTSGVmbjQvb3YwaU1aRm16TnRJWFJFQTNmdnlocEZrS2gvNmJuUlZsQzB3eTlTUkhXMm1pRWs2YXY0eFFaMzNMWUZTNDJWbzM4QkxGTHJEUWZUcWtpTzJ4STdUbmt4cmUwd0hMMXNjeEpQSEhpQ1NVRzBhSEVVNXlxbEp5cEZTR0VOb0puSm9SNTZBMHBxWjJuVjVidml5NEkvSmZWTmw2WVZ3UFVXcS9sMmZ5QTY1ZUNkVW5LL1JJUEdjNDRpeGdqdHN6djZZZFdpYy9kcFZlc1FFVlkzM3orUzJ2S1UzL04xUmVGdG1GRE4vWGtnakpWV1pwTzVpeFpBK2ZHT2h2R0ZkUVUrb0R5aEJzRXljS1RmcU8wanZMVzMvMmk2RGk5MFErQXB6VjJ4WGVUUW1veDR1RndsQnh2N1NocFJSWlYwZFVpYUVxdkI5ckRCa2d6OGIzMmh2bk50UnY1dUY4SUhDNVBSdDVtUzRBK29Mc3d1K0Z6WUtNT0MwdnFtMmt1dG1UYkZSY1Z0Wm4vbUN2Z3U3WEJSQlJSeWV6aUZnbnloYmJpNmE2WDRmYWdWYjV3Q0FqQWFOeDZZVEdMdEFpMnBURnZSOERVYmtHV240Z0l0MWxIb0VFZ21jMU9uaFkxU0lxZ2g0QUZXMHB4NmxvazBMSnUvdUI1VllmeHVuRWVsamltcHJoUkFqSGp6Z0pWQkRpdzV2d0x1VVY5OTZPRmw2OC93QmVyREhDWkU1VnY5MXovVEtqc0I4M1N1am5TalhXTWFnbERFMzNNK0lYQ2thWmJ1S2p0L2xvWUl3YzdrWnQ1N0xQSTd4Um9sbm0wWHJUOHlOdlBqRlhXQkRKV2tYc3E3R0FSbFMwcVRIdFk1VFZvQVkyZCs4Uk85YlFzb2hnRFZ1dHI3QmFPcTMrMVcwNzFNem05RVBPM1FjTUNEWjNhQzJkaXppSytIQTEwdjQyYkxGT0daNnd6WDE1TWN4UjFtM3FrdFA2SDJnZDBmcVBSNWkxYnRhMC9uaWhsTEh1alFNSGdxWWlYSVBzd2ZVemRuRFVPYVFwRURPeUxpdGN6bFVUOHpnNmp6dUx2RzlhUzExZGxmc1NCZmp3M1MxMTRGb1EwbENuYkh3QWx3Sm95MmY0ZXpsY3Y4eVJqMWdqSVk4ZEx4cEVNTDBiYWFkd1R5TEJwOXkrNHAySDY5a1JNcy9LQnZyU3ZYZ0RPMWFldGIvVm1ldXF4cXpjVVB1OXk4RXhobXpqWTNBejBiMTMrRU82QVpDNnFpbTZhK0J0TXk0cmwwN1NyUzlrazlza3hociswTEttamVrRlc1VmRkZW1mZ1Rpcno3MnIxSFRRdnY4NnhtSmpVU3NVa0NVdk92Z3dkQ2JBRFVIQVMxbzR6UllNK2EzblU3Vit4VzVHTEpQRlg0c05Eak14czdlODlOMzBSS3RiaitWQ0NCekY2dDJSOFlHei9CcldwSDN0VkxGbkpXSmRsdnpGQUlOa0dDc0Fhb1R2SlVjVWxJZlZaaCtQbWQ1OWNnbmkxbHhEY0xLb1Nva2NienBEY1hLclk4SURMcWZxYkNrSnRxRmkwdmxobTVtc2dLSVZSQWJrOXQ1T3BURXk5QTR3dHc3MWtUTE0wdW0ybXM4dVJHL3VuRitvRnFXUk9NTU5IVGtnVVdUNTdpNnpvcjFWSEtYZFFaemNSWFQrMjY4NDFFRzZ3bkMzOEE2SVZDS1FWdjNpelJlSlpKdVNGR3Y4TnhXYVlOYmFNaUc2QzJWRXZSMmEzTjlUR1YwK0puQ1E5dmxtLzNvVHhaQ0J4ME9Yc09vOElxM1J0bXFFYWxFL2RmRWt3dVIzT2JqRU1ySmtuaWtZbGsxeWlJeU1LVERNS2U0WXc4N29FMnpqcmo2RndqNjhseG5MN1laaEZFMktUK0JlZWxnRWg0SmNzWXVqOGpEb0ZhZEw2c3ZQb3E3NktVcVIrQlZVaEdia2ExK2cxd0JFdFM3R29KN0Z1d2tuS2luZnB3UVQ2UWJCL3VSNlgvQmN6QkhudmFwaWdMQ1h4N0hCZDE0Y2szNTN5RTdBaFlXZ1dHQjBINGRZYW9NVVFFNGRXU3ZxYWE4d3hRZkdsMVRVdlBYKzN1TFJQcVRNODJLV04zWVRXNzc0QTlUaGY2Nzk4MGc1YlZnRE1EdG9SOGs0V1JvaGsrR1dCZGdmZ1hoaCtjR2xXUlpPK3ZnNGp3SVNKcGZmNWNvY1N6U1p3N0xkTDQrRE90SXJBUjA1R1o4SkF1YjRjUjdvNDE1dzk4ajJvVDc5eVFIM2RPdDdiOUdhQTFyMnpiRlRWSWJSL3N6K3JHaHd5WmxlTjRyMlltTE9iYVExcDl0N1NxUkpuR1plWFZFRVQ3Z0ZpdlNsMU55RnA5ZUJEc2xlSFVmMis5eXNZeTVPWDdiMW5ta3FpejZpdzFWOHFjbnVabS9CaVA4SDVTYm9taVArazJMdGc5aHpFTEY2N1JUNDdtT0ZxZnFaazNNanovTTBBOWx3dVNVZlN3Qlh3M1d4aFVUK3dPaTF1TFIwZTBzejV5dmordWFDOXRHemR0a2ozTVlVZE1kZkdUTG0va2lSb2w0NkRSelpydGVna1lSOVB6TWxIUjBNOFdQMndudnpNYXV6alpNRmxybm5hd1REaVVQN2FSa0svWno3NW5NQkt1T0FWMlpBY2JpaGtQZnNTU2FHRDNabmE0U2orUVFRUTdPZzNzK3Vpenp1L2lnY1lUclJmaS9nWHRBZWdRbjlXaFBtVFA1aFo4MzBiWmxLTk5RUXZPbzhOcFZHbnJZTFJteUhuWmFCMytvOHNWeDF4bER5dzBZbTBQOXJmOUhJdGtDTEl1bEs4UzJCUmRnTDRVOFhHaVJCSFFlK1h6QmdtTDVOcFB0TGkxZWdFaDdacjVRNlg3Z3dXUUNsOW85ajFFRzFWUHpEekd2djh0QmVLOGx1ZXJScjRzUHU1aG91NitBei9EVjNJRkFtSDlTeDk5K2hZYjJtTmhnZlFYdGsvQjF6NU5PTk00bnBmWXdHR09ReTRpVUVSNW9MMlFSMGJSRmJXQ2lpVVhnWWxLOGM5SnAxZ0g5RmltNVlRQmFCVmFyVnYwdmduRHNqbnh1Y2pVMmRhVWtFQlBjZHkyc2NBTDNFb3cxSENMeW5BQzYwQUlmdCtkMGpvMEV2ZTUzSms4dnY1Q3dWZ2ZVSmlBcHBIMzN2aUpVNFVURGNaU0ozcnNlVkpENk9pd3VtVFZ6aThiTElNRGpLZ2hxTlpDN2F2MjVYQkh0R0VPNUFOcWUyR0JGeGNLYzlUZ0NKajF4T2pic210Zk8vcE5VLzFpNWZZZitFT1dQSk1sZmRHVjkrelZwL3hiN1haMEtxWld4L25DRUJPZkQ3VnlYSGxvVnUrRFNTcVVtNFFKbFkrMk9oSG1tUlI5cytHMEhPd2IwSTRVZkRCZGFtb0NiM2pGTlVtdHgyajhISktESWNJV3pBWGo5ODczbWpjeHI0eHZVcnVoYUZXZFpiS0QvL2dEVjNFd29aenkzVUNucUEwTVhsaXpEU0V1NG82bzVUU3hJQjIwQjhPdnA4L25KUCtnNjBhbCtoU2RpMnFMaE12aUNpNFhhWjRCUGZJMkd3ZGRwdnFUc1lIcTRVbDJPZDlMUllheUo3M2ZKend6ODlDMWhqVGkzaGdSTTFwNmNJTHNWQ3RMS3U5N1lRL29MYklJRGVrTm4vWXhPUUx6QjBnZU1hYWR0aER4dm9kazFkWDdrd1hKVHd0Z1VxQ081c0FjTERGQjlBSjNycldpMmZ4ZDBJL0NKK2RXM0ovY01GN3ZsSTJURkQwaGtuUUVnOEJ5dW02bGgvaEJBd3VlaEc1bDZ2c0M2YTlrQVVIanQyUEhOMjgzK3c1YkZyZDd0VGFtaVNHbk1KaFBVT21lYzhtK0pvaHBzK1RQbzdEQXRETGxiN1JRL2FNVVBEeHlqUmVTVlBEaUovbU1USWxHbk5icUZHYVZZT0hQRnl1bWh1Y3A3dVpkS2hIL2NJM1MzdGh0T2ljTGlYaWNhdkFZY3h6blZTTDhvYkt4ZEMwcFJKNUFEdENUNnBGNG9LRnU4TSs2Q1ByZHpZYnl2WGtsb2h1NWxpaUNpY2NvbzFNN0lKSGsvczZlZVJrRWhMYW9lbTJLL1VUdWF6eEpXR3JJZGpVN3dLcStZZGd1dlpkRDZDQ0JzMnE4TG5vbmxzRTFEOUpnZzJ4SmtGTUxCRFFpR0QrVXh5VE0wamZORkhBWWljaGtXSCtKL3hMQjgrR0R0dm00cTdtNFBDLzFKMDJJMVU4TWhoVU9xcXg0NFNBald3SWhWUVNKMjI5dG4xNkJuUTFFM2owakI0aFhVczFmV1Ivc3MwTDh1QzdBSlBqU1ZCcWJsRFJLV2pMZDJhK1VKdisvODUxTkpUZldUT1FuT0xIdmR1aTBWdGFGb1ZNbXpGOGt5ekFESHBnRHA4aU01MEpFbm95RU1VTFJTdFp3QTdiTk1KUkxJelRBWTZqamVXNEM1Mi9ieWVOOTVTdWtLSGtaTVh2OTFCYlhZQWliTm9IQmFaaFRFeTVibkRrdm1VSk1UUS91WW5YMUtDWkhZUTdyMjdRWDNCNVovNjFzVThDVTVTeGFzQ2pib0ltV3IreXFjZ3RidnlKRFQ3VjU0QzNWMUJmbTQ3TmIxRC9zZEpxamJ2cmtrSjgxNUs2TEZhdmlmSklwbDBFaXp6UzNEV21UUVFDVkdTSU5ncWMwdHlGKzg3SkxvS3lrYk9Od0pybGEyYk5RRDN1ajI1Y2NBVHQ3S1Nra0Q2QXRxd3Q3OTBmOFlLdW9rWlNqYmdhTGF2N1FCWnJiSkhveE9CUSt0d3pTRVp1akVYcHZhUThNbWI3dXQwTEMyckp2dmpiSW1LZmNHaTNEazJCWWgxaXUrWFJ3SGV1aHJacEhVOUlzVGtlUmZlcFdmS29ORUx4T0loR1Q4cldDNDNVZ0hMOXJGMllpcENSR0tqamFTSTlBc3BtZlc5SDY5UHAxTEswSHJRVDBwQ2RsQmwyZHByV3hyaE12Y2gwTUFCd0pocGRKelBWc1NCVXgvTFJaTVZhSmtzMGhReFZMOFVFdUZDcmx3NkdRajhHNnpjMjJLV1RST3J4S3FDV3o5dVAzSHZiQ280QTA2NFJpY1NVdEduelpOYjN4L0xzdCs2Wk5ja3FEMWJwb2xnMXp4TkhzcW4rK3dmWnd3Q2VzZDA3aS8xYkRNSzUxRnJrSFJXelJTQi9aS3B1T1dMREc2TTdwRVdoaFRrYXNGYXRoZFpFU1Nnck1xVExoWTArUmlkT0FzZHNLcmp2MHhVZkNPdUJYRFUrd0lVeDREUTFITHdoaTllRkhFNUJ6UGlyVVVYUDV1ZTc2VGlCUHBMbWRvQm1qb2g0STd5MkpnZWRxVERTSDBQbFo1ZzdWZzdsR3o4RHVybkl5bXFsVkQyRndPNFRNQVlGU0pJZDI2WExUNUpqUFpsZHNjWktKY0JaVWp5VXRQZTkrVlVKQTNzVGo0K3Fwa0srTkUweThmSEM2Y2o5SEpIOVhCRDROMXdmTzE2MEtQR2cvQ0hGNElvNFlaVEdXMUdhVll3WEJJR2kzaHR1Y1h0VzBEUlhRWFM4d3l1Q1pFMitVZi9XTVl0ckt6Z0kvZEtVRzBtMXZ4cVQ3ZTAxc09mTGdvODlmRVljUDZRR1hkb21OWHF5SHJqdzA5RjRUUk9KY0J5ZUx3dnF4TlZISml1dUMrV09mSURTTENVY3JLc1ZuSi91NnpSdHFHNjVPcy9obkNwVDBWYno0cGRMQllIMlVJby8yalhJYnFjM0dnOWhlaHV4VjF2ZGNtTlBJeUVxWVYxT0kxVjRrQTRhYjRUbWdWNUtPR3N1Uy9mNXh4dHl4NVQwQzQ1Q3AydGJXbkRENEZ3cDU4YjRTR1o1UEVuMHNFemlKYi9CT3VjNzFaQ3cveExzWjU1VGlRWDFRRmNjK0lBSFlwempWaDBNZzA1NFRLVzgwaHEveGZIVENPaFRGTTBhYUJibWRnTTB4OWU0bVk5UXR0Nk5aaVQxRGVIaXEzT09lbGJsc3BSY3JPSnl6Z3pEajlPbjZ1WXNWZTVpUndxZW16RjdTYmJodHpEbzlieG5iQUVueHJobERzSUJXZ3BQbUF1WU5wY1Q4bDNneGRtQ3IrRnNQVWZJZVVZZHV2MlBYcWZxNE5FdU9iZEVWMytQeEpFWTQxNWdSMHYyYXhOSWlqZjl2OTJ4Q0FWV2tPcjdGZDUvWW04b3ptZ2pSblJ3RVF0bkVYY2EzdnRnWlQybXI3MzI2bU5EYlkyZS9vaVlvOFp2dU8zOHIxMjFra3laVWwzbWxsNVVGOXhKN1hhNmNrWFJoK0tyYUZUZ0svN0crVnZvSHA0cE9lcUhFSG56dnpTN3ZkcnBWSHNuY1ptQVlINU5CYzlCT2JJRis2QW1aY2hZUjN4NHRndW5RRzMveTFrc24wOXIyaU9idGJqSjdWTGhWTUIrdEV4WlRmRE9IbFhKVHNDYVJhbnUwUzhvQ3Y4ZUFqeFcxeE80K21ndDVMajBWSHp1QTBqazJrM0hmVFpud1dnSEFQbVYweFlaK2p6K2ZqTWtJbFBHOWFmMkxqdm1OOVJFZjlLNWRwZ2V2S3ZpNUJRWUpIS2JWaWZRYTBjRHVRaDRML1N5QTZjTU9rNGZleUhNYktBeFdsb2ZxTEU0aVpCTW5HL3h4RWVnVFppRUw2Zm9zMkVNQTliSkFYT25WdWRLaHR0RWxsNlkzb0x1Y0V4eVpZQ3VyVXVqQ3VmL2crSUxiQWlpdkkwVWVSbTZvSDJUcnVBRm1paXFOMVV1Wml5KzhmTUd5Mi9vRi9vYVJSOGtiK01ORWZ6UVFVbmZTWG9JQkh0Ylh6UXRXR1BVZzFPTCtXVVY2UTBTN0RSK1pkdXBTYUNkQVowWG52Y1ErYWM1alBKYWE2UzI5d244U2M4TmwrakJJTHQwRk5hSG41MHROL2lDRFpxcVgxVk5lWjViVjdySDR0WEtNN2FHTjJScThvaXJiVTdvMzdNTmpDdlQxenlRZXpRU1ZDTlFGOXo1a08wdXZNd3lCaWJTVGVMVHlVeEVJdFJmamI5dll3eHJIZ3ZrK0gyWnhDN0F5dVFYMlJ1TllqWXVtYjJLbTM2TUpCaXFOTGxkZTNGMDNYMjQ5anovdHE0UlJEazNIbmh6dkpOUHNJMitPSDgrbnVuMjRBQ3lQTWVEN2c4UzBUbmIrbVdPK2xHaC9wR3ZiTmI1aWdxcVA0Z1Z2SGdKSFRFSGpSZzM5QmxPS3AyTGthUlpLaHhmUU1WRkY5Q1pTNTM2aVhicFRibCtFOFdzZFlQZ1ZkQjdkZkw3cGwycEhucVFHd1hxeCtQZzllYU1hcFBKMncxcFA0c3NlRHE5RjFPVHZ1QnN6Smdya0RFUHRQQytjUGloYVNGRnVSUDBKaENIQkoxeEZrZHVaSHZGd2x4NC9ndllPa3djSU13NjliL0FZRDZlRnhkN1F4NDRTVUJVOVVnT0JCTUV1d2ZKcG9kS1NsU2JoOGNCQVBKZ0R6RWYyYjBqcldVd3B5cW1ubWg2UVZXM05TOUI2WE9QdldLUXhRc09ZMll1cHNwdjI2ekwvRE4vM3JxNzJ6Wjl1VVhHSGlwMHM5ZnlqN2VZSm8zZ0htSGs1OTQ0Q25WaEJuWHRIWFYreUttQ1RYQmxJQlZqUWVaelhzTlRwQnFsQlMwMTI3a2YyTHFlT2VTYmxyaHJLMHExUzRBNHFEWlgvMXNrR0tkWDNiRCtsamtNUGt1SVUwNVVobTlyaHRQSGdtTmdFeGtocmlIOHd2UkFzOGFzOGFlQWdESU1VM2dYWGxnemFJeXNLamRJbXFOTUFUdUIrZWNsWUdqam5DWCtLTmVrK2V5VUpvallUN0p4ZVVoMDUxMjh0Q1IyQnVRK0h5bThVaW5SZ3A4V2hBSDZsaEVJTm9FUjFpbStIeUIza3dPMkdnNklwRXZ2RHBhL24rRCtuM1M0SXdURUIzdFNxWXVWUmNadFdiY1ZpbytHakxjNnVIb1hTRTEvbDBSekFlUnplOWNlZml3c1BWSUUwV2RvUjExSnltWERudlFqM0R0QVNrQ2E1QjQ1RklrUUQ3NHBHRjJGMU9HOUxValdTS2I1ZG10cE1uNU5CWE9jMDVKSSthS1dLMHpHTHBsVXVJR0hlQUJUellFMC9VQmFyV1pPWjJJTWs0OVM0aUkrZmtjQW1zTE5DVXMvQ2RRSWxvblIvdmlKYkRqM2oyQUJRR3ZURk10YzF5MytNa096RUtJNy9vTG93MHp6VTV1Y3ppVnoxOVdvMVpETXBRUjNFM0tXQmpGZUlpWUFyQk00V1hXWVpPOEUrc3U0NXBKUXdRc3NjcFlNdjBRenk3U1FVeHdFdzlhcHZyMTV4RXFwem5PbTFRY3FHcXB0dVdBRUFodDhzRnZQL04yblVNM0tpSC9VMFJ4OHhPaTA2a0ZMbURLcnppN3pOVndyOEtaUEQ4K2lsVlZ1Zm5OL3NWUzdsVkVJeUNJOTBxL0xEVmNOZHpCdU4xNitDRnB2blBjNllCTk45KzdWcWREd3ZWZ2dFWnk2ZmZSU2t1VWZhcUhYZ3FJWWVYQUNTZHVKWmJMRkRrSDRoNi9iL1NNUDJmaTFIM2h5eXYwVk1jWHBzcXRDanVlQy9qcTlGTmdyYmJLSXFIK255c3NwclJwNkRlaDdnRktGOXVodUFveWdqVWg0ZkhMQnBndTJvMjl2SWZIUTV1OEV0YWRWTVVveWMrNGFia1hiTmNwaFBOY05SeWk2RVZqMjZYUStEZmY3OFI0Nm1QREpubWFRSVk5UVZyQVZqS0xaR21RVDRHR3RGY1MvMnhqS05ocGU4Qmg5QVpBdHV3d1dTTnJIUEZhdHFHLzV5K3JkWXFvekhzcVo5enFkbTJXN0tIaDV0eUpneE1waEI4MDd5MXEveWNyZVhDSVNieGdLbEphbkxGUVc4UzlqLzdUS3pRekRLR21uVHpuYUFYMDFGdW9GMGhjVGZ1YTRsRnIrTDlQQkdneVVFNGdSblF3M3RJeTcwQUVoRHpkVndWSWR0d082RUN6ZXBySlIrcmlZeEg2VTdOY3h5OThzMFJWRWdpanl4T1oveDJhVTlxaE0yMENHWUtTK25rbXkvUVV3Wng2bjFwMVJ6L2pnRzZ2WTlsTk5rMGlGR212RTB6MU56NVI0amRZVUhrT3dEdHhHMEMvQWNnL085aUVkci9qQXJjMTc0UUhqdHFYeVp6YkR5bjlQVnlDbENWYWNtVjdISitUdzdiL0s3cDdHcGxHR28xbnFzNEhzQjN6TndKTW53NUhFRkUyV2lwczdiVFVCQ1R0TGJvVjZZL0xUS0ZrY3dZNU0xeDEvR0lFbU5SS2pVQVVnVTRZYStzeUxQN2RpL0ExcUpCSFMwNlZRMVQ2aW82dzVyQWY3VVVZWFBzWjFOQ3VMdGxLazlqR1ZCNXhpRTlCNGwrL0g5dDRVVG1YWHNlQ1BxRVoveXZ1dVhYYnBndjM0MWZndUxVTDVCeXdwUytrMkVyaVVZenUzb0NZNUV2WXNXZDhqMVlrVXNNWXF3TGs4eDZMbG1VRGxiWnN1Q1d5NWhmbkE1TmhWNDFRMnRNdlk4d1ExNHZDV2VoaEVNaDlzYWxWWUsrVjZwQUlzdUF3Nmd2YTRKdll1N3hMdFFFaTB3OGpnMDAvREVMZVFWeDV1T2p6cGhFWUE3S1BNV3Bjak5VU0lBUWVJWWpEY29MNTFndFVOZFJtNlVIMWNTOFVKSmZhVmZwOSttVE9GTUVyeldQWFZNdC85MVpwSzM3U1JneXdMc3VoeUtMbk5YendGb1llemZMc3BnQmwyZ3A1L0dZNWRLenN5U2VvNzRzQVNXbDlMS2RyT2M5UExGUmMyaklNTXJtR3gzNjFvV2ZYYW9jdnkxajVWQ25OR2o1dDkrZDBPd3E0SVB4dHhpVlE2U1k3Slg1dUhvVUdodzRLZGR5WGt6dU1hbCtlZHk3V1hmN2w2TGtubis2cDZ6MCtic0pkM3ZzcHNjV3dXUEZpVGMvcU45Z2U1U1FVdDBKeVBLdjErS2NDbHQ4dEozU243MytpcmRNT2lhaGM0UUN4Y3ZDN0ZFZGlQVU1LWGFWVzdJNStLNmI4eGtFL2l0bk1GSDBzZ0loSzRQYXZ2dmFtbDFkTlNMTUNndTZ5R0lNVG5wNW9vNkQwS3I2Z3BVMlUwenA0ODVzWCtrK0Y4QklrMy9wczZCUFJ0YW9ZVGVUQVR2ZlR5RXRsOGxGbGFXUTNzdm5vY2l3MElBVzhQV2FjT0lUejRuUmhucDBhSlI0V1duNVR0MitwaXZXMUZ5OE5sNWNSTElSalFMNWtVOW1ha056Wk9BYjJRUDl0UVRZaWpvMzBid1VDczdXWWo3NXdLUmVFY1BYWW90YjY5QjBUQWtSVEt3bloyazZ2UWRiKzJmZzJHZVJqMjBvU203eXllNy8vRVp1YTVodDRjc3Q0UG8xMUxtb1MxYm5RaVM1VU96Q2NoSDJYandMcklIbHI3YldBeTcwMW1qcE1xZ1JZOXVzcURSUGJSZjF5WVB1RkFHOCtDSzlmUjUzTkNIUVRpMWt4OFhHL1JXSGdxRVZXOGUyQ2ZGY3ViRngySWRHM2FVejgxK1U3dHM3VGpVVUhESWx1TW1HNkxhb1FaN1ZhYlFGUlkwVXFDK0ZINVpmWjNYM1AyM2lSUHFMdnYwTDhxUmMrM0JRVE52eXlXRmh0c2FQazMvTlJzQkFaZjRLS1pwbGZKc2c4R3FKdlMzemw5VStDVXo1QVY5ejdNNzhvemF1dFlTcGlFS0dlMlJ4SEV3b1pGS1lrRkVsak1OMHQ3NWxpNFFqYUg5Nk53T2NEa25rVjduU0kwaGxhNEpDK1dLL2ZmcjY1ait2UmdQSkxRbWxQTytrSzBxVHR0cVNCUTM5L2V0TzhNckhPTFlCeGgxT1JPdmZobzhOMVFYNllnOUtkVERVQlFkSldmcmhNMGYzdjhhaGQ5NDFCdWlZT2tuZVdTN0Jici9CZktXSXlZdnBKaEk0VXJnNVhHaVR5Y0VJZzMvbWNCSUt0RDlxbEx1Uko4UGlSQnYzN1I5ZnJOREt1bkR5YXpLVnhaUW9JMmZxeElCRFg2Zk4vVG01T2FyeElnY1NFWGpVVFZjU3dqNWs2MThLQ2dYM3R3Und2eUtFK05ucW1TRnZTNHZVdC9Vemlnc2Z4QkhxQVovenAyZ1hTUllyRDMwTTYxTUdLeklWM1YxZDgxN0h2YVlyVEVCb21TTVoybHp3M2llWlVyZVVmSmsxdG9iZENEK3pubG9ZNFJJaVJ5Z1dVQUVKbkJGMVdVRm5QbnJEYzY1eUV1K0R3cUZpS2ovSk8zZXBMUWd0ZzZzVFpKamgxclMwRGN6TllYcy91Mks0aDVCdkN3eDJ4OEZWbEtjcEF6SXFKSzVUR2d1R2Q5TVFIZlBIWGdhVDYvY2xVZ29JN2JiNHJLUFA5NERKL0tPK01oaS9ySlQ3SnhNMTZGNWRobzBwTHZadkxHUkdaSGtRRVB2OU4yWTVxTVdtYkR0UW1YYmFlMFVqd1ozT2Riak0yakZadTdnRHpISTVPaGxSd1lTT2VZZXpLUXdrd3IzTEhpN2xVQlhUMlp4VUN4ZmxNN3NxSzVPTjZQSGdFTnZLNVNLbkE1K0U1WktEVDBXUlpFMjA0RmJXTnBYYnNTMGNublJMV0ZsRmZkek5BTk1VQzVrSDJSRWRTaHhEMWpXTTI0ODRJS2RoVndIUFV2a2QxT1QyTGlKQldYaFpmUVRSZHlaM1BmUEZmRWxlK21sU3VpUTRFZVQ3SElEdzhTNmxhSm0yZTcwc2psMjNhYUlobk9NbU1OemJubVpyRzNMVXpsWUtlbHpySUtVN3l2QW9xbTFuYzVuSStyR0NsejVRakF0aFhucmtqbEN5NGkxZnR4SFVQMjU1bE1lbFF5Yk8vZEhtam94USs5YTNaZGpSQzI4ZGxNOGZDTmpLVmJCOHU0dlRwTy82ektqb2tVYTNCdkxwdktTMStNN2dnU3R3YURHZXRGcG9LUnRPMDhnYVBkQTZXME54K1RIaUhCT2FPbzd6V0E2c1d5d1YreVRTbi9PUU5ReGM5N1p4a2RDanYyTDJTVjUzYmlEMnhPZDhoU0FDQ0o0ZUlWZnJkK2k5d2JLcGcxNHJZdFZHcFZmdW9SdDBGek84QmdBQkZ3QnRlRVl1ZUJRQWF2OUJFaFJzLzJ3aGhQSG5kWGxxSzRlOFp3emFnY3pNdzRVUVVXb3o5MklxaFQwbjhCalBQTWFrMExjS2lYUjV5Vkw4SUZtVi95Rkx2SHlvSlBSZjdlSmJEV1FEc3oxMVdxQ25ZdkJEUUhIRmQ4WUlITmFXeWhtbzl3UkFGNjRRWHpxdkwvZzM0cGxnZlpqOUZkYWI4YW5UMkdicEZSVmtuUnljVzZFMFdQVlpoL2MyZS9hRjVrSzUyVzgrWlQ2UWNOWGtFTnBNcDJzYTJ3a1lDYzF0dDhpSFhEMldnOXF1RTRHcTZucmlId0ZFeE5STlVXREtiamc5N29lSk9RNHVXcmwvN3ZPaGNTOXRIQS9RMEljaXZjTTBrNXBSaWxaUG5JWkRld1JsNzRTZ1NMc3dkMDFUQWU5bFhnVFBncGFOWjdpampKdkg4c3ljVEYxQngrOVVmZ1k3Z2hPS0svSnplMDU0NFppalorZ3BmSGNtUkJBdE5hZW9jNXNFT1ZNeHV4SWI5WGx5bUVwQXVQNXdZZjFkV21oRG9SblpyRUNEaSs5U3ArR0R6cHFxYVkzMGgrZUVSRWNjY1Z4SFVSWXRhcUs3MHFtQktNbUEzVDRDVUF6WndvMXJaeGZHekprdXlkNXAzV09yTndHVnA4bE1rUVVpQWQ4R1hhTTRpbGRKa1J4VVB0TVQrSU5mWXJjL2c5WWV4S21IY09Ic0FTQ1RIUE8rcGVMbnc1N0Y0WVd3TjZVZ1FaMWhNVnlmY0FwL3pnUVBiSEJrY0hoV3AweVhtNEhNdTJmYkJ5VmQzak9HMUgvZU41QVdNYjFacDA5ZjI3aHZINTU3Uy9YdnF4eWdWWnF3RGRKMlV0Ymk4TFYwMXZKRHVnWGt6OUQva2FYWmtYeEtxOVBDa3ZlWkZlOThVVjJPeGNxUmYva1VMbkpLODMrMEpOSlVBTkZWck9Wb3k5QTl6VjEzVXErbjNSTWtFZ2VhWEUreC9MRDB4WXZvVUQ5VVpuM1dIbFhjaWJwVkdzZWdtdVZyRzZvOUd2cjlNNDE3WG5tYWxaTFExWVpZL1JiV1h1OHhjRlRNR3ZDNktRVk1peXhLVDF2Q1ZDRHlqL1JCSTBMS0xEVjM0Y3gxamRiUTRkMFZYcjhJLzVRaUhUakVlNE82ZDVCZkV3MHVLQzVTQ2tDRmtoM2xyQjI4ako3K3VUT2N6ZUZXblR0MjJVSGYrcEhBS2xicm1YaUhEUTBCa1pLYUZlWDkwVXhnVFRFVU1FMFVNbTNxSzBCUDdjZENTVUtIUTl4bWdoZG1xamlIcDFlZFV3K0hrYjJueVpCL1dNUzdTaW8xUFNzZ21HaWU2S2Y5M2V4ZDhtdnhTckJEOWl4ektZK0tiTUNVRFFQMVMvdXdhT0pubWYxZGNESkFESDlrVGE1WHhpa2JZY1hvcUpEbGYyUE1CUG1ub0ZXaEcwa2VpbmVkMkxiNHNnbFBtZEF4eE9tS2pqUDVVTkN2T0xiQWVMLzFKYk0xMlpZeTF0OGxCZGNMSlZSeWIxeW5yQUlDSTdnNGFYTHQ3QWR2Z1MzOG8wMmg2d3p6WnV2Ylg4d0ZYY210UnRQa3VGNTdQR2F1QmlaOUgyUEJxUjI4eEJSamF3aWsyQkYwU1dEdUhodDFRMVJoZXFRNnJHM3FvMkIwN0lFaEdMQ3NmeFR2Y01VbStNRFlkOE5BK1ZJOUc0M0VuZlh5Vi8yUE03R0JtZU9FYWMwN3dhN1p4WHRuTWdzOU9MTXNhWUk0Qzd6ZTBCUzFucXRRSWZRS2VYbE11dUpJNVdJRGxDaVJuZGNjSktpVkhISWJEWVlocTJOdHZJQUJLV0hJVCtPL2YrRGNkaklMREh3bW1mM3lIUmJsR0pscThVNUNWald3ZWkrMWJCbm91M3NQVWU2R25XOW52ZVpoaWRFVE1BT1ZwTllMYlUwZWMrOHpKdnNFUWdoTDlvMmVhaEtacmc3amJSaExvMnFQeWUxaklUZUZEVGUxQUgvQlZMYlliY0I3a3BUbVh1bmdEclRIVHI2djluNXdyZk5OSjlmOEZHWmJsYVZKRG5XTExxSklRTnZzRzRSL3M3WTd4bk9ZOVg0UXpkUmRzZ1JhS3JBVHY3TnZYZWFIYnJKS1FGUFRHUHBzcU8rTTZvU3R5WUFWWVNEemhkczd2dElyckJGNTh6V1o0WmVQR20xTTcxT3ZVeFdBR011OXVqaG1jdmNXa1FtbFhqZmVBQW90TmwvOGdYNUdQT2h5SmN6dlMvc0J5QU9MaWJRTlVObHpjNExraCtoN3NYck9WWm12bGlaU2hEMFF6UGNKU0NDRmN5M29scklqQWU0QkRadHlrM1hLOWlPelFxYzk3bmNqYzB4SXJkYzUrN2tKZElDd0wwcThRNnNveFpScDhaOTREbEEwTkFBdS9waCtJT3dkYUg0WVlmUFJSYVBTN2V0QTRxdjBhSGNTWUtRVkpsZExrRDBmRS8yV0ZpZUd3T0pPcHNwNWRZRGlGZE5oQ0pnMERUdC9Bc1p2NkZTNlUyTGN0SWJmTnBCU0xhanJ0c0JpUTFtUEhlYnplTlg1TjEwTEVSTnJSd2VSOU5FQk55aWlTckJ3QjBvaVRFM3QrZ1MrcHZ4a0FwczI5M3pYTmNpSGJMWlRhcFRldURXdldmcUhzYlVGL21wQVl5WlVhV3NkdFhQbk53Zy84aFJIL2ZkNTN4WDV6Z2pWK3ZwbVpQYk5pSjJJYWk2dU1jRlMxQXhTLzFBd2h5cW5TWUVKZVFrV1JGb0FUNEJENHN6WU1VS1lCU04xbDlZVjYxcVoyVXlvQlJHemtBa3o0aDdIWHZQdm0zNTdoNDRUTEpnY2toM2ZTVGc5S25PUExNSm5XV1diQUoxODREcThKRTQzQlNndmp6NElJVFlmQ1U2Rmg4QytVVGlzbFhLVXdHUE5CcDgzSzVzZmRRZEpEMXAwbUp1SVBIUnBwQm9TZTNJcm9acUhBbWxFREdOSVBFTm55NjFQV01HZUs2U2dQL2tHbUtXcWE4RzI0eEx2VG52eFZTeEhtQmFXWDVNUzZSMTc5ZTFiN1dLMm13VTNEcGZMMzgwNGZ5amNxT1FhVXBReUFjS0xqTUwyaDlWeHJxVG5LdUp6WXdNRlU5YXJqNlZyV1ZjSHVQV0VUUTlkaEg1bU0yMFkzMUM1N29kQVhuNm4raHo5NUw3V2cvd3dzNzE2OUpXZDJ3QzNlSHdPeGtTamI5b3hMQ2swMHBTTCtLOVF4V0tuQkkzazZJN2g0VGpEN1dTb1JDQzc4R2pTdmQ3aXZTbTQ0bkhjMXpDMFp4MFpjSEc3ekQvNGFxQ21KaW5aa3liRTlvNlVHRFR4VlF3cGRTcWtEL1F6SW5lVlV5dXB5c1JLNWVKRVdBdGFtOXY5MUg4NDk0bFIwT0hNU2xOSE5QcUpoTGs2ZEVuMVhUWjJYRU1PaUdEZng3eVpLN1hBZ3Ntelg2K1JBSFFuMUxKQUsrYkQ4NVlCdVI1Q1ZpdWYzRml3Z3RoZ0RyMTZ1ME5ZUFRGNVN5dkVXY2p3Rlp1c2ltNEc2TllERVhUd2ZQQjRrL1RFdkpBWTN5M2l1d0djOXk0VjJOdExRbUlSVC9qNk01WDlYWkc3M3ErM2t3ZHVoazVlK3ZGc1NReUsvcVRobDVkVk9DWi95em55TEpUQUt3RWhRdWdVTWVCc1NBVmNEbU04aG1JRkoraFEvdVdRc0gvZEJIT3hHVGlRcWpYZytSNEwwdmZYYkRuR0JLZFM2UlU0Ly9KMjM4dFR4NTBxa0grU08wMmtRUUs1T3ZhcUNzNTBXVy80WjdrSEtTVERMYU9xeXVEaGVPL2FBWm5JZktUQ1M4VzhFVWpDUUc3OW1INFhzY0JzRlNsbWl2ZExwalVFaEhxWC9CSHRNK2FXdzhwbEZGRE9wdmtSU1BWaUREVlZleDV6aXJnakRIRUt6ZDZjb3JHdmhBQTl3VjFkVEcwRlE5NFJheFZNZFJiSm1LeGlpRy95cnd6THdYWkVVZlkwVWJiWkovaU1jeW5ucFRZNWpkSUsyeVpscWtOaWhtS2N3L0w2MkJvWHFYbHFQUFdmVHJTd0lRaHE0Wmk4eVM0bGxDM1NjSGorQVhxSzZEeVBQQzlRR1ROOTlFSXp4dGdpSkNOZkt1NUppQkVWMnoyYkhMQTFVVVlabkVaRzhKYjFEZjlUclFOaEV2aUIzUlVRcmVFYXJmazJUdFNBZ1RGVU5pL2NNRjQyVzMraUU5ZVQ3Z3o4Wnpmbk9IMjJLSlBxUjFuOHFCb1dFQThUT0dweGFZOGFvcmV1Z2YxcHhNL2hVNURuMnNlZzNCLzZTTGx2RUlUMnJocy9kbmJhdElYdC9ibmwvSWRTVUZFeWd4MUV1bnJGZ01XcWQrRS9BT1A5VUc4VEQ3N092UnRMUHVMaWthaGFZVFJ1djhUb2dKaGlBa204N2pILzA5VnFUS2lsSW1RVHFub2NTZUVMV1pLSkZEbEt5UHFzNXJMY1pOTmZhRjJQS0hpTWIvUGx0Zi95V3UySUJtbUUzUUdsVXhTU1NTQkdWdkt2Q0EyNGFmNUcwemE2Ump3RTdaOEVqWk85OUQ2TEkwKzBBYnlxMjFsdktrYVh6b3RCUnVtdURDUUU2WWlNUXprVmwxakE0c2F5eldGQWJ3aWNiY0lMM0RmRzU5NHo2eUViQjBXcFZZZ0xlbGJ1QWIrOWRGNlRpT1h2enp2RnZoOEZVSlR0azI4QWtzZlIwaGZuMlp6Vm5KT0xHbUdOejAwZHV6Z25wR1RXeFZDQ2JwS3JaeC9RcHJzS1NMOGRMVlhnT3RsK0lXWDlmR3V0NVlnR1diSWc2Nlpyc2pDS3dHVEhreE44QVdiaUpjblIzNTRuOWlqRjJkMUF2cGVpeUM0R0NRRlA5S0VYWHlPVng5bVV3VHY3dTY5RWVsa0pzQ21wNGVXWmg3cVZZeU1OaDlKVnlYWEtWMjJUdnNqbkx3NkFJcGd3eC9kZEFONUM3L1dpTCtZRkhkMHR6TFhNbHF2MlRiSXIxU0k3VFVROURTdHI5M2dHWDZVSWpzalN6L29jY2Q5QXluamxmVUk3TzZTNTFsd2xaV3hVcGlqZ1FER1ZzRktYK2VIZWYvUDR2VE5NY1oxQXkvQnVRRmpzY3hhVjNYQkhTbkdhQ2VLaGlpcDh0UnFBcUpGYytYYkZCWmozSnQvN1NYcDVkd3FyaXg5U2QyVVZKL0FvT2ZwY3djY1g1WGVZcm5oVlRQbFBZZUxIS2ovRlVUOXplTzZGSUthaHdJdFNRS2RXRmhMU2NLWVBxbERnaDhUcHpERkxGWVg0cFo2dFpUVU96TjFTWXI4MDB0ZEZLTDVKU2pJOEtueGhSTjRsLzM5SGNPdHhYc3ErV2EyQ0w4eS9Xb1gzUSsxSStldjZxWEsrYnZmaGhUVzFmM1hIcWE2ejBGWDBJRTRTK0FWa1BYSlYxWVg2VFpuVlppa0JGMFZTVWJGTEhVbUxtYTBOZmxqU1lWUW9PUkw0NHdDV0hKY0YzNHN6Tkp1RnZvc013bGMva01hQmtaU3EyWkJzWHhERFExcndsbmJUTFliaWZiaVpUOVcwcHJ5ckNrRFovSWFqS2JhUVlHZkpqZjdiSk91WWR1VW9aeEZOVE80Qk5pdFlkRzJPREc0bEduWWROVmFPV1lCWjJMek5McTEvOUN6MFVXYlFpYlRXcE9JUGcyZkQzREVwMFN3ZEkveWdBUy9SZGltYWgxbG5tZ0ZKRzcybnova0ErQkdHeVJrRzN3aS9HL2NFcHQyMGNYZ3NXaTBPMmVIMDdmWlc4eThHU09rZkJ6QTJaRTJENTE5VmZYOVJQYzRYNTA3cm03VUlaNG9xa2FUTVd0U21WVXJja3VuMC8yc1JCMUVFbk5DckdqU2d2K2tvei8waUFTWVRMcmxvdGpJbmlkSTk0amUvS0lsY1lpMUMrQlZOWjUvUXVwNjNnQ1JFWFBSZzd2UGxhV2Z2WDl0TklvWFVSY1Erb1lzbmdOdVEybWRpZXQwUmp5enloVWtuNUlSYWZHU0hNelE1NmEyMUNYZ1BmYzByT21pOC8zNnFGbC9uaUlmV0NPR3N5YSt4UHRtL0FQTFo2UG1qVkduOFBibzRFQVNmSGFtM3BjM1FsSDZXMDQ2WE9jSGFDNUJSb09ONEhyWkx6UENkKzFrNEx5T0FyZy9LY3paWG1yMGFQWldkU3U5WklRQit6RXREZVFFSHc1eVVzd3lsVmRYenZsdEJWYlJ6M3FTWU9sWUxYUUV1cWwwdTVUZzZjMXo0WTVhenZvdzcvWm9TTjYydDRvU21wcXpodXRPMU1rN1ZuZjRGbHBiMWNEbzZOQWhPeUVUNmxZVjFkUVNVVW1QbHRCakJZYStadFFRLzBYOERUNmY1cC9CME0rK0RrY3dBbmo2VVFiZjhyVCtrWG1nR1M0RHlwbDFyQy94UU1JRDRBZWl1enYxREZUMHVUTTNpbXpHSEhTS1JRRWRDekR3aWNYR3lZR3hvQ1ZHVUk4cDU4K0JLbXhCWE55QWdwdG42cGxVc1lSQkV1SnZROUYyeVhQUkt1VXRxQk8zU1Qvek5zSGp2Q0tVQ0VrMHdHT2NIVnpFMGJvd3pxVzNoNHBxTTl2OUZrV0lpRFdMSklsSVNvbmpOcFhkbTIvZmFjd0F3TzNMTjBKc0QwN0NwWFFTSVNqVlkxTitUNHE2dExuSXNxMlVwVkpkZWFST3oyZDRNczU2MDUwZGlHTDRIL2ZwMFMzR3cyaHVTb05NN2lHYmFCbStzRFFiWDIzd0JSN1hIaGlNVWpJbFJyaHRHUzFJRi9wNFgvR0xSZXVQRFVGTklDeVRUOGwyb2FoenNQMWVoaFNsaWlpRG1YeHpWZnkyNkt2ci9kWHFqc0JYamZBWmZCSlVxOEp0dkFJcFcyQlppVlZLVzJWbkVpYXZBc3BBZ0cwcmdYQmNVK0Fjdkt1TmJ4REhQa1IrVWUrQTNhRkh4UGpyUEl0b3pvaFFNbW5vN3NXTCt0aHhQdFEyUDlXYWlYS0dKRHlUUG9LWmwrS0JWam5uNi83d3NBSmVIUWdQUVpQNmtXU3hUNWRxaFJXcDB4aHJvQStoendUdENFZzNaZFdFZi9oU01wc2xMM2VjdzJQQWJDM1J3RGg4bklqdnVUOWNDbXhEcEQvWG14OWhrN3o4eHE4Z1lQWHl3YjRwMDI0M0pISXdVcWs2Z3RZbXozSzRoejNkK0xXdlhYK0tqSWE4VkpXR1g3Q2pvc1dCcGNUdE5kQlJCeFhoMUhRRDdESXljbnI5NjM4NzlzUEhaMTR1WHAvM0VSWnMzZlltVTlqakEwWG9MeGpkOHhCRGJjaDUyek93bGdSTDN2YUtWZ1ZpY3lyMTNSenZWZUtaZ0Jqb1JGRmtOL05RcDFsQ09VTU9EOG1VRUVuQjBWcEx5N3ZlRCsrcDZKU3F5SzMzNHQyeVFJWEhhSHI4MlErZTdiSEh1NUFPVCtNMkd3UWRaUVpVVTZsa0w5d09RYlhPSnUzdEpPeXRLUFh4UWR2ZFB0Nml5bUw1SFkrTDMwNWtESlBYNG51ZUtsa3lLQXZwM201RVRaaklTV1NSdytoTHhHdFRBT0E3VjVGbE9zMnE2MXMrMmZkN2lHTUU5RGhpK1Z3aGFIU0hTZ1hzMXJjOGppQm5udzVkdFlzL0pobENFSy9pbk53cFF5c3M5NEJFQ2pqM09lQkJCblZnYVJUaGI1MHR4YjVDbFJFaDFDU2ZNWVFKMTk2NnlETzhDMkZsQzc4dm5Gb3g0d0hndktEZ3Q4MnhsUVpHbERYRTVsSDZKMVpRVVl2UjAwNUEvNGtWSFh1c3ZoRDNsQ0txUHZEZjJtUWZiUHprR0xRTXh4bVdLOUptNlU3WXpMdzUxY3RFaloxdCs5Q3dVQUsxa3BXZEZxRXZaa0Z2MVpXbm1XVnVMN09FazRDZHFvNm5lemZtN2M5R3VRUms0Ri91aExMSE5jQXhHaFplUVZ5b1VndUxKeElJUzFlVHRSWG1QNTRaV3FrV2JZMWw5RTc2NFRHbGxUK1BTempjTUh4Y3d0Y2JGWHZvYlFhZjNhTnZ2NHd2V0RGMG41U0ZwT1NxZVhRMjNBV3hNRDlSRDU4RUFFak10dzZVOFFLNkcvWG5aZkI5UDBoQWdlQTQ0VXU4QTJlMnNuMmtoSHozUUxmVk9iQXF1UkMrREkzekg2SlNZK2Y4Y3lCbk93ZnZOd2FDTVBaaUhGUTVWR1VvS01hRHk0MU16TWpZdzk1aG1NOEd1M1VVVEFFTXZZNUxxQmtNTkdUa0xHZWhQYTdKcExGMVJMaWd6ZzkwQ0NybGNDMGwwZ0NWd3dSaWU3ZEpaeUhVUXdGak1zUk9aR0xMUVo3eDU1OEJYeFJaUlVyUGNnbUgvcThwVHNWU2dDWU50TUxrMmpWV0p4RVZCNDRMUG41dVZkQ0ExQTlYaWdCSlZrR3BBQk9sd0swbkZGNWZxbUltN1NsSmpFejIrbktEa3pmMlhjVlUvTGlQVEJvZXZ2Q0EzcXV2ZVVDV0NBd1UwQTNYZHY0YVVRQ2oxVVJUaEc0TnNSWGVvNHZYL0sreTZ4RzIwMFJNeUk4VW5GZCt5bEwrY0RtU0xyTHRyL1BKaXN4ZEs2RDFQYXB1d1R0TEJScmdiMkEwZDdHTmhKYkZ5K28xTjcxNVN2Z0hGV0ZNa3U2ZC9oWG41V0RMRzBRZW0zMGVncnhjUGQ0aEJDRXlNYjZaSzh6dDBXRlVZaDNVSnpyaklXWEtoTlJCRTBpZk1MQXhjZE1QQUlIUmlrYlkyeFZ5YnFuNVh2TEp6V1k1N3N0UVA4TzVGOGNET2Z6cjd4NFBoaThjVnp5c1V1b0RmbUs5YWxqYUFUaTg2R0tIb08wZXpFSWt0MWR1V2c4M1p2dzVvV0xmWXIzanFNZms5U2F0bFl6MTIrTnE4WGdYWTRMelVBWWJPNWdkdHdraHdmWnQ2WDU5dTVhSzFZRWVRM1Z4K1ArQU1aNUJtTHFNc2pqQnZZTHVnQVJpTUhCdnI2Y3hnYkdQa3B2OExBVkhDZDVVY0sySE5jSTYvaXprTXVoY2dmR3pDeFNIKzNpZ3dpOHlGMnBoOTJzN2pWYi8ycFc3bFVjTVZyeXEvRkU4bHlUYWxCV0xIWmYrOERNc1hpdVc1cGtYSXRWTzBDTVZnT3g5NldraTBGK0k4M2JXMGJnRDhoR0dBMXRyYVRPcEJnY1g4eGxCVG91QWUzNzVPMStiVDlvTWFURTFZQnB4dHgvQm5EZEk3bTJ4ekI0V1B6Nm54VFZZRmZ0YWM2WE94QllyTFFNNTRNMXVEMU9QRzBpK3lpU0tENkE1cldMcjYwTWtYSGdBQ1Y2WlduMU9FQVF2N1dxRzB3WGl2NzBEMUQrMTk3c01MUS9MNGxUY3dGaTBBRG45NlVnY2ZVR0xGQjVKemlBblZjeEJ2am52OUpGT2pIQVMrbVBxQXBIdGFvblZjcjlqZktGaHlGNkVlYWFIbFVGWU9IVGIyRnpVZjhtN0dwYm1admN0SWRCazQreG9KaUY2NmNhTDdhb2kxNWc4RWlaUXNReG12T3k0V1JXU1NoK3c0djVtS2w1T3BkaXhKNUU3QjJsMnZ1UlY1bHYwVWJ2U0Zkdk12c20xUUJ5WS9pZEI0R29tS3VDVnBFM2VWZXFUT3Z2UE4wWWVPcjhzT0RNQVJvdmhvYng5a2tpdElLdVhWZmpWQXkrdkI4c2xvcWFCanJSMWhnUzdXU1dFZDU3N0lpTThlM2J6bDNmL3hNYnBjZkx3cEpBZ1VQRlZZMTBKMWRSMjZkeHFXRS8yMDNpQm1KSHFtS29tcmhNYzh4Y2kvSFY2NjI3bzlmcG1XcTJTc2F4YmZiTy9MZUs4bWF2enpYaHN3TEh3TnlXMDhhVCsrc3VIcWhRbTI2VVA5WGE3YkVaVktvb0NJS3BsWDY4RUdLOHFhUUZYSFhuMDhzQ3VSMyt4OHE0bjc3RS9yL1FTZ2I0dGJXKy85c2lGWitIQmtQZ29MMkVwaVdpV1Z1UmsxbTUyelQ4R00wS1JHU2JQOXJqODhnd2xlQmRHTG4vV2JOSDVBTUxma2lIczlmYmY1RTZQVDJJcTZXY1hEem1oV2VRNkpNdTlNMG94OHVxMWZtaDVvSTVnSlhuQUtjaFIxclY5dDUxUVNHQjJFcHlJemkydFFwTGhpclRqSVFCSG9ycGExMlBRZHBZdDhGVDM5QTF2eWFOSjN4a2VrM3BEV3g5ZWF0QnFWRDZBZzVscWhXdERBS2N1L09JbU9CRVZHb0laZVlEQmQxSGtsSGFDOG05VWpqbFdDZlFMbXlnTVhkbUVJRjhCZ3ZRbXBkTGd3NlRmelBGOVpWc3ZSaVpzUWhqZG8rRzhqeERnTzF4Ymk5SUg0ZEVWTVhWVG50dzhZQ1J0ZDk1cEduL2RrS0h6NnowZG1PTmJnQzhXdW4yVzlMN3BSeVJMTWc1M2RwU1NNTC9aR0tXMVgwNmJPUFR4SHBOYVVIQWNmK0hSUW1KTHJocUI3QTZ4U25ucHBLNlowQnlEM2QyVzMxWUVPbWtQN3lKVVUrRmczTGpkMlNFbjZKQ3Nmbk1qZzV4Vk0yL0NkcC9Wamc4RWJTa1orNGhqT0Z3dkR0U3pRNzMrU2oyZDR1VDk0cWQ0ME1uRzVDNTI1RnBnd2dlNDBiTGtlY3RmU0t6ZVk1Z1JESEFFWkVzRkVkWkpDYTM3WWJqZnJxSW5wWXNJYzVRNXRvK2NRL2Q0a040OGNrVTExQzBUYjN4MzhtNk5sbE1pUHJjWnhnTE1acmZJMCsyQ0ZiYXMyajUrY0NQS1BEMHZabDVld2RTMnVIUFkyM24xRFg2QzltZDlsaWFmVkh0eWp0Z3JsQVpYcTNnZ3BIY1AxZCs2NDZZSFpJQWJSSGdqNzVwYXNUYzVaY1pPWEliQkdiWit0T1VlL3EyU0svYmx2aVdjaEVjbG9kU2RYZUVsQmt3a1B2MFdTbHVMM3hwTGhXVFFuZ1VyVGVMUTRqNTRDMm41cytPQ05MN2JTNVNKbVNoZzFIUzFqenZXSEtRaTVqMnlndVI5QlE2RTRMSzd1eGNyZnF2RSthT3J3S0gyMWhNTkJCWFlXdXhMbElsSDhLU3QwbEd0akowQ0IrUForcko2UklDa3RDeU5YcVREME82UkhyOWFiclBwS285K0Qzbk1tTk03aXYvQmoySkdsbGNtcVZGQmZraDFudzlNUTZRS2l3dkdYWnl5YTZ6WVNMczZxelc4VGVnc0lMcTNTYUUzNmxBdXdzR29CamFmdFg1Q1Y0QTNJVVdGdGwvTTE1STVLK3lpMVJUYS9jaHBMVUdDMExtSk1sZ2pCK3BQaGhLbS9GblRtaTRMOS84NERnL1RsRGFvMitIeE1SSzF2K3pCaDdhdnk4STFXN1ZmdmczM01FNThwOHZTeE9nRER2RmY5cHl6NFRueUUyNkcvdEU2RDlDTWZKSmRKMDhleDQyajBTbHlNMXdQbWRxWG4wOUZvVmxSdmpOZFRIMUFDSjg5ZEw5NFhxRXN3d1lZZE54Z0pKREEwV3BjblNkNTFpSWY3SlA0bFNOeU1PVEx6dlYwZWtyek5XdE1WbWVJS3JTZlBSMVUwVnBzSVdMV3RPK0NhM2hmaXBmUGxJcTRka0tTcll2clFmV3VJTzFUdVdCNWRRRDRTeFhHam91SDFNRVA2dGwxNEpMWDloUFpYOTROWXp3OEE1TzZrTkpiUEZCOGVQbkpSbW1tSXYyeUhkc2x1MFdTOFh4emVqZlVQbEVjVkFTNFRSSys5QWlRTUkrVHFSYmhsNno5MHZtK2FmVzNPbWhjdFlVQldCQW95SzB3eVF4THVNdjZZRnJIRW5ZeWZLZk9UVVowZnFEOTQxcHphMGxoc1kvVE11RndoRWtzU095SitYK0J6VWpLZDFNRWExSVA5RnRYaGtEckJmRHlGMDhvaFR3VjBUQmYzR2RrUWQ0Um10OWwxY3NMU2hpOWdwQ29BRTZDcGVPbUtIV0loNlllNm5qYlBUaC9STGFEYituL2RmQllkUDJsRzlaVVhQeUpQK1dUMCthVzFQYVJ6MXpZQWNWUExtaitnU0VZaFNvQldhNy9heE41RDg5bldRWXN6d1VXenljSzdld1FEK0FoSEJQZEtOeVExRFFZdlVNaWVwRDcwcFBMY1R5NlYrSnBLai9xcWxSenVlRS84b09EMjhQNmRZR3pkTkthYXBweWRNTzBSNURyY1RFVFBHQ0M2aERCTk1kZ3FzeDd0Y0hzRUhlc0QwblA0WDFIajJhaWkzb0l1R01oemxNVWNQQ1Z3RVB2ZDNWL3hjalYxRnM0aW13Z3l5RVUwcE94SWthSDd0UmFnQWJNSjZyNkxRWUYxcS9tQnZyWFhoVEczUytYdk1PVnl2elhXWittRXdNZ1RZRDJRbWhpYjF3MktmNnpuVEV5UFg5dTF6dHFsMDBWNGp3ZjhNaEIwdHc3b3BoQldWWTJBcTlnNGFyYXlFNXJLQWY3cE1QMFdkYkZSWDA1UXorNjN6SUIzMldQRDVXRVBCR2k1TVNjbWZIUjZlTFd1RTFMQ2ZnR1I0Zi9YZGFBcmZ0cXcxSWFGWkFEdy9tWkwxU29zbWFZNFYxZDdtdmhCdE1RZCtuMVducnJRQzZxdW81SXhuY3pGRVZlbTJCckdTemlGV3ZhNmhtOEg1QjUwaWpraEdBUVNBTmN3cms4VHZWWWhJZmhueExyenIzNGlMVmxodFFnVENyekZzeFhUUzBrY2JseWNqM1Znd0djVHpwYUZUOTNGY3ljelI4MmZiZi9pRERSQzZxL3JqZDNDbmkxemRmbE8ya29OelNlQXA3aHlZczR3R1pkc0d4cUhmdEtPSkQ5SHRJQlphS2ZnMkp2TmxOcjNZTE5Kalh2WHMwMG12ejVzRkF2OUZjT2xlcGdEa0xSdlZCKy96cnp0WGJybFJuV1oxcUF2YUY1QmVPUUJBeEk5Z3RzWmVhUDFSc0doQVJXMkNGWnpzeG4yLy9rTGdvVG91cEFRRUFWZFpBUjdRTVpKUGVEcGVLazNFOW5TOFVORHZlc3N6S3BoNVhlUHFHK3UxQUZwS2FVMEo3RWZPR1I4TWNGYmQ3aHdZaVlJZnViS0JDaGx4QmhiUUlHenhvdDNWUDV4a3ZHMm9nVDB3amlRWUpmNVYxZDJWa2JGQzJhVlZQUmM0WGt0bG9xVG8yeG5leGlwQmZCK2FhZkFaZjZxNUErZ3U1MGZ6Ti8wdmZrckpGZUp0T1NEeG9wUDRpK29XUGY4ODI0TEo4ZHZNZitBTytxMmVxMjhGS3V5YWFobjBoRHRXUTJ2eXdyUWh6SStiSnZHR1RyRFZWbXMvTmw5akc5ci9KL2tBS3BYbnBkSTBxMEUwVk02RDYreHB3VFUyakVQaUx6OTNUNW1RQ1AyWnhtc0FnY0pFb1Jzd01YVkV5SGJxaFlPUTErRUc2OW5KRzJZOVhQVFJQL1YzYjZTV0o0dTg4cTJIUm5PZUw3QnE1Z0hwQ1NZOXZleHhXckl3YkNEWXpMT3J2Y3M5b1pMQnZZamtYMCtTY3kyT043aGVXUFR4dVVYS0lZdVhkRDE4MlFXSEM1Y1g5SjRsL1h0RTZ2YUNmU0ZEWW5hTUlrZGhoUGpic3E5dVFYWjZQYm5RYzJkdWlwbWlBQytzcWJnU3JNdEhUd05JVmx4T2FvYXdaenk5aURCVHNFTy9PWjcraWEvSkFtSGpXL1hjREJGTUhmZFJvakxXTi9CTU1yc3ljRXZOdWNCRGo4QWlVbWIxZjhobHJSVjczeEt0OW5ESkNwbXNGalpibDk5ZTZYd2RNejhPMEFiU2xseTNKL2h2alk2Sk1ORGk3SC9YaTZjUTg1TzlnTWNCTDlsRkNmcStNZUZUeEl5a3plTnhGcktjeUQ2MUs4V0Z5M0tONDB1K3VsTnNzTTZEWmRRWW1Ba09LeVh3Ym1sYzBHbXpCSVFqNnhtRVcxOXhuTnJXN2dGU05LZWowSmtxZWFXcW5hZ0E5T2s4QUd3SGR6emNZanBFQ1M3aWtwVnRteXN4VzNpVURVNXFzUEFsMWxaVld6U2ZaWVpMUEcwSFJKemoyYkRrQjh1YVIycFF4VXIrUFpRcHZYaHBzREFQT0xWT0dKYk9HbTlHWU9VYjJKNkNOSi8vTnlKM0ZQZ0xGa0ZWeEVWZGFqWE42eTNpc3JkbXhKWjRxNlNNY1BFdW5HNUkwM3RMRktpZFF2Ymt4NVBEYmNMeERBQTZ5M0JUMFpWL0lVdkorUy9sUXc5bDQ3djhzeFNSSG0yQnJxNURQa1B5L1hmYWRheis1V0pwalFVbXhIeXljUEpjUHBUdkVFY3ZrNXFIMjJSdHQ2aEdVSE9XcVlLY3B3OTFINDZsWFJIUkI2WUlxQjhhM3R2ZzhsUHdzaHY1S1hYZ1kzQ2poVjNvNjduL0dOSVAvMFZScHVYMytGTzBVNU9ZcjRXd1BQYStTZHhjdFlIN05RQjR0SjJDU2Zuck9ZM2piSHU1NlYwZDBKcVcvc0YwMlh3S3dXRlZudmxmbmNKVnJxSThzVUVyenhWcC9pMjVGRE5GSlYyOWtHMCtkcEFEQnpCdENKcGtsVDFLaExIZ0F3dXdMRXowRlRTaXc0M0RZSGpWMllQN3hzOWQxR0RTU0JLK0VDaTZtU2FPWnZiUDlVenhQN1hKY1F6WXFzei9nNm9nK0RDVDlybUJmbW5MUmJNUWFxdTRsY0lQZ0hPZTNUMVYwcmJZUG5zd2xvOTNsa2tlZSsybkswdEJ4c3NYTk1sbzRQVE0wbnJVRlo4eXowZ3hyT2hqVWgrc0hMVFdXT1pvRlVTVWdCYVJqMWJFRnJEbDVROEYwRGQyWDlDZnpVVFl3YUc4WWFPVU1OSWlBVXVyeFFKMGRGbit2ejhPTzdEYzRHT0VtcVo4cHFiT0dLT0RiSURJR2Z6MEIxdExKemFLNEY4eVEyRmFnUmF6a2ZrVmtkZXI2N2QycHladnhJekE0TjdFMVRyQUh6RmdUQndqdUY1SGVCbnBrTEhtbkczVEhsS1B5K1V2eVpZRVA4U3NLZy9pTS96cjB6UnFaT29WWWFIb2lZalAxbEZ5VTB3K3dpRk1UeXFKNmtvQWMwY0lVbnhpL1lkRW5icGwvNE1qQ3BGTENXRXJRdTlCODhlZktQSkhhZHo2OWlrODZQbDFzL0NqTDQ2TVYyL3orb1psMGlIZC9OVXpLRW85MklKbUZvQkpxWnA4aTJoanVVYmZIM204Qi9TWnRvOUY4QkNwb1Z5WDNtZ0MrdGlDRGRUTFN6TEJXUE5BUEU1TjRkYlBlKy95LzBOaDN2ZGRXVjd6U2lzTlBiSHE4dVoxR3hicXAveldMNGsxTDdsTU5pUFJuODdrbUJhOHV3cU90bFp5dVYxTldSdTVIa2RpWHRIanUyajR5SzJ1MkZINnJRT3EzaG5tZ0dFNkQzTzluK0xhNVVTNTRDZklUZDdnSDFVZlBtU3c3T3l6V0NzTlpqYlo1Z05yTVludE5BYVpHZWdBNVFxY3A4M1F1ZE9oT1hqdHFiUUFKclRUMFUrN2FRNk1WY08vZE03T3pUSTJzN1ErWHpnZnVHdUhwd3ZVWEhXT1kwYVNSclB4NVdMaHVSV05tenVmUHJuekU5SlIxMXFtOGMwWUhuRkRReThJZ3Y5MHN4U2czOHFJWnB6aE41VHhuQ0c4S2JZRUxENEMvTU1Va292TGc4b1ZqZnhZUURFVm9YcER6clhUNkdNczJrUnBnWVh1a1pBeStzNlN5TmZwQm5jQ3JCVW5BU2lRR2t4NmlVcWQzd2FPNmNXNGFNUjc0L3grSnROcjNaQmJXL3k3cUQxME01dXQ3YmYrcThyZW95blZ0UDhFdDN6Smp1em5hMW9lZWMzTnRsWWk4MlN6TnppdmxQWGR2YnlrcFZDMFJDS2VmSkVzTHlKTDZmOG5Ja1pqb3ErSUJFTHlQazdEK2NmZnFmbG1STS9aRDhEdmsraXpMWkFkVFZGbDljSzJQNE1PVXhKNkF2VHRkZHhHNmNhVlpSWE5SWkRiNUZPOWlNZXhTTmRKeiswMVN4eXF4WUh4bzNLc0Z4WHVwYUdLaWdZQ0M0MHowam50WGUrVVhRNUZtSlluZHE1ZHhTUTZueXRYQjhPU2Q4VjVCOGNPSFh6bFZDanozQ2ZnSEpJdUJ6cmlPRFhqMURhRnM1TGNSSkhFenBVRmpKR1BwOUFLVkhVcGJPRDV3dkFRbmNUdE1mdzlXTDk0azh6T0RTNDFqK3lSRlhpZlVWY0hzY1NwdmhJNzVKSGE3bUg2Y2tRZTJ6bEN3UUJtZDBjMnJLQkN2ZUcrWDJqUXN3WWtSUmFVREZjL25KYU8zZlYrWlYrRFBxYnh1bDA3OE1JRFdvVk5HUXh1R1I1VnhVOERwd1pnUTVaV1lHSUd5ZEpZOHJKTHRZdlBmTDJSQXB3cGUzNC9KbUJKZlN2RlBjUUYwaXVrYnl6eklud2Jwb3FSUDN6RDlvU2lvWWRPVGdsVmppczJtQmt6YUxmWDFtSmRlbzVFd0hYMmR6dEtoRzZTOC9vYkdUVzhlN2tETm9Hd0Z5UWVKNnpaNzQrcHNadG5nZHhxRE9ITm5acDd1UDVvZ05BajBheGc5NkN1MEhYallCNHBRTzB4MHNqaE50c0NLMUdOUU41ZTJCN2F2dlhqOUNBZ3NaOG9HZ0UrWFFwa0RZK2J0QzhzR08xeUFuazRNWmVSTHNOblV2NmlrMEszTnR4TVJ1UlpnYSs2YTdINExoaStwdUh4bUYxd0w0UVJ4UE12UnZCbEwxNlFEU3BHVzRuSWJyeW5acmkyZjAxTDRwV0NlNkpHRjc2SnQ1aFBBcnlvSVNzQjczTkkxT1huSXdaL1Q1VG16MEF6T1d2ZHNzZzlVUFNQUWR2aGUvOHBGYmVyb3RURnV2enRuM2ZYaVM5ZWpKNVRqWmFiVHdQRUVBeWNiU0xYdWRNUXRtcTYwV0Y1blI1YjR5bVRSRjQ1NlplM0RVRlBoRzRuN1NqbWFrVk9VczRLSjg0YzkxSTd2Y0NYT3ZVZ2NVMDJIaG9NOFpRd2ROQzIrYThnUEV3dm5XZ1ptcWsyMXhzalRoMDdlNmhPZloyck5iZHM1VHIxdWZmN0cwTVdaRlFic2M3VktKUmk1NTlzZFI3R2ZTVDRuWWZaWExmaDE1UnVyRGRlZUVNWURmUVg5OS9oUWh5Z2JrU0lFampoNW1jc2dzbW41eTJnNXkwU25md0ZrM0Y1eE5PQ1BzWk9sM0NVbTcwRGROKzdER1VGbFpVMEgyYTJWMm0rbG16YysyOEJkcFNZcGxGeDdzR3BXNllOYkFZMFBvVTlFMnFhS0hrdVZjM0NUTndxSUlRNnJWengxdFU5UFdUVnRMVGZWZG5kN0pwa0xONkppMHhIU1FkM1NrbjZFYlh0T3JQT3pjWHdoSnNnOE5qQkl4MTVqVjVZMCs1MEdDU1JoZUpZbnpWdXhReUJQcVJtb1RSekN2S0V4aW5ybDVaeWF0VjlpTHpySXE1NXplN1FJOUdTVktmZVJoV1RiRk03dk5MOGJYRGdSQWVSNm5sRW9KUkd0Z1NzK0Z0Z3VHRUpIS0lzdGIySllHN0RQei8renFCT0hJZ05Ubm12MkZra1o5b3VxSlNhUG5ZSnNWVkZVdjJmS2pJcmRITS9oQ0dsQ1R2U3BoSkI4d0RncjdzRVJ2VkFTZFM4SVoxZzhDYm45V00xdHdnOE9DR0ZUUHkyaS9kNWVYYzhIOFVOQndheHg0MmlUdzhORGg2Q2VpNkZSNCtjSHRXVS9zS2d0TzJJTmdWU1RrQjJscHFsak12OFBXUzEyTE9ZcE85QnRiaVRwRFBLckhmRFFtYWkrREZLREgvSlJNU2xJdktuSmJXOGU0TCtlQTB4SVFmVDREMzJERER6aUZ5M0FDT0ptTGtEbGF3dUxZanQrdmJPaDl4UXhxMWJ2TXdmQ0xHREhwaVVJT1Z3VHJIUk5nZE1nazZKN2JHM3FlamlSVGlEa3dTeEx3Tnp6ZTNJZFlhQXlSWE1OL3hZeHc5YlNDSTRpOVc3b1oxcHJGOHQwN3FtSDhTZGpMVm84OXBlTWFIZnZlTEUzcitLdDVTeDlYTnBXeFhPeHJleTI2eDRWZmhMcys4WnJvQ2VTMHZwR24vaXM0ZEZsbVhzS3VJWm9qcmI3enJpQVZDMnd2QUJYbDJmK050NHl5RkVzSnNCNjQ5MU11NERyakJtWTRUSlYyN0VVMWFMYUF5Y3VYZCt6dmZDQnEvNGs1bnNLZXlRNCtFLzh4bmJ0MlZFcDJScmNHVi8rMmU4QTVjOUlGblY3bHltelYrejU2TThnNE1NOFpCVVoxY2EvRkM1S2d5djVic3hwWk1XRlNLTDNhYnNxaUZ0YVVCckVLRCtNcGUwWnc0LytscDNnYjN4MVlYaGt5bHhYTTl0b2dtdGFQUUlQeEt5RDFGTHBiL3FzSVFHclRpTDdyNkZsWk9xdGRES1NKM1V6TVRZK2Y4cmJEMDV4VHkvaURyeW1aV1NlWnpFVmMzOGJ0cHQ5K3Q1NVVOWFZuOHNpSEVxV0dndS9nL21QcHJJcWc3NGtlZ2JwVklGTDdDdnBvMitVRExqTlFMVlJQT3dvNDRvNjRWeU8zaGRqOGFBNEQvMnRiSWswMHJBOGpsQW5BdzRoYnlwWWdveElhVUQ0RU1GNnJTZit5dVBpRExJVEtyS281VVg4OWpJUys0bFBTVUJJK0lxdEk4MWlxL1RSc0FpMWNXWndqQlBIWFRqZG1YUFVESDZ5djlyY0RaM0pBR202aGlYUmZQMXBRMDg5R01qVzQ1ZTFaL2VmeW1WbGhIVm9ZY0hRamxLUWtwM1kyNU92d1pVMGVKM2VybFdMQXQ1WmFlcEZCMmZKOFlaMzZFL2NDaFh5N0IrTTBzYzV1V2R3U1kzZHdad01qWmgvMS9iaDdydGNvbFIvRmNpTCtXOEtiLzhFRTVHR3grOTJvdVkxeU03eHo0bzhrOHNlTzhUNGVGQk5KNDh6SVRHb3M4OFVpalFYL0NyQnk4T3JKbVR6Ykx0QmNBUDZqY1R0UVBkcURLYWcvaHppbFY2Ykl6cThvOGErN3dLMjFlMU16ckhsVk5RNlYxVVJkZGZDUWx1clV5MVlkeVlRUmcwVTZ4b2k1TU10SVc1cHQ2bnpjMTdKdTBrNEJkblNVNVdwc0MrK2oyQjBIUUFFZ1F0OGRtaTdpaW82TzY3NGtza29wdTVCSGNqU3dOcUY5NTU0aTBhVGZ5MnVkOHozZU9TaGlZdmdGNTN2ZGwweHB5cUNBcU9tN3FZN1JHZVF4YUdSaW4vVjVtUnJJNXZmbC9ZWHN0T2lmWi9PZWQ5anY4bnhuRXhOWnNxTUVzUStnamdxdWhhZGR5ZUdlNGhzcTJGSmtUdy9pT2w1elU1NnRMWDlZMmRkQWJwd1FBL0xESzNaTVJrZC83WUQvOVd3ajJqTUMxWEhNczV6RTN0a3l5OFdqK0hHQ1JXTURXdjdvaXpad1BHSDRSSDFBNFdqVXQ4ZCtMK2hWT1p4NFN0NENMeEZ1ajZmWVBpeUxLVGMyelljSW1GeDdwWExBekxCSndiOGoxUVp4NkZZSGpJUWFtVDFEVlN0OWNyUlU2T2wybm8reExUNndwdGhxQ3o5cVNUWTlBNWRocFRqL3c3aW1BNDEyWmRzbStLcXlNTXNmZ0w4eFFJcDZ5SVdNQkNTMFN2T2l4RTIrZUpwalBnNm9wNTV2UzZKeGd0SmpPKzhJb1EzaE9pVmtlV2h2SUI4c1djTU5zNmpjSmJuQ2tOVE85V3BIdzZpWEhDdTBwTGhRQ0NQY1JwZTZzM1gzd3FTL3I3cUhLWW12OGFwQ1VRTDI3Skd6VEhoYXJnallBSDRDM1luZnlLTkZzc3dtVmxxZzE3VUR6TGJrRlFsRGJVNmdaRFcxTXdNeDRwQ29wcGswbUIyMTgveW44RkN6TTJ5VjlXenByTS9DdmpCRXBUdTBwd25VaDJ4b1kzZnJjcGtyUDl0VFVUdjdzM1JSU01Jdk0zaGI2c05EUzZHcjVFSDhmK3VmUjB5dkFUMVA4blczVER5Y2xlby9OSXFuU280TkN3Nnh3ZFFXK0NHemRuYzZSeENpcVJrTjEvUXl2aER4d1NhV1gwQm54M1pmZlNBdGNpaThhRlNoSThsU3ZuU0tOcHlHK0EySTJXbzNJVVJwdFlKUnQ3ZmhFdmI5cGdCQ21ob3JsWUlWcmNOVVJZTHg3RFAxRVM0cTdKRE1JQmQ1cjNGWEJ2cXNIWXJJWHhaa0JsWkxEV0xKRUlITWFuaFF3N0ZEUThIZHNrdkFOQXNwM29OamFYdVB3NWcyY1djNFF1M3c2WWhuTEhyeUJ1SmRvc29XVjMzNGVqckRMV3NSLzFrMmRqMnJ3aDIwYWc2N0xvNkcxSkRsYythS25FUmlVaHRLWG93TVFNWkJZOTF2ckZ4c2xGWjVOaWhrV0dnKzN0dzYyd1pYUE9FaFlxNVpEdmZZRHhOeFBIb0ZOc2E1U2UvME42SUFJR0gyN0IzTktGWFV4d2xsdEtwK1FUMUlUSEFYdnpQbVFnY0FNWWw4bkpENlh4SWlFbVpNejhEYVlISG5ZVXNJTzZ0eHcyWUxzWGVJM3NqWUgyR0FxSGVRUHNzNG1OUlZlckxEQTdZRytDYzU3MmtHdEJCdWtxek5EMVRxa29IcHRsVGozdjZ6Q2dQZ0V2MjNWQVZmSVVVOWViTGpIMVlsSkFkVnRGellEVkpETTdPT2NiOEdNU2pycDNTSkVsaDFHeHY2bU1iM0VKT0VYUlZmckphaE1CS1QyMkhWdTV0NFZsL2JEN1N0NDNkK3NnNXo2dUNyb2RxVTVnV2txWmxVQXRTdXptbzN5MFp1QXVlQjdubXVZb3Z5bllpQ2xiWmQ3L1NUVE5neks2U1BXZ1pRWGdYK21jMktiY3Q2QUZZdTE5bWxUdGUza0Q2T1JUZ1JtYUVVZUxPanFlWHdRa0U3aFNneFRIdFgrejUrODJRQWZKSEVwOFY3bFkzbnUwMjFURHlBMllmc21NaFM3ckdHRTl2b05ERi9GbldTSWZFQXloVVF5RUlORDR4K3AzWTFhMCsyN29sZHgvUXZBUmZhSUh6UHQyRXRvdGZITWJ2b29jWE83SDRRMHhPZ3VJTVJJbGVFRU93MFVJQ0pSVUllakJmR2tYaE80MllscVNKRUxVOVEvUDFBblY2YS9nUnRLRUxmN2g3VVNqK3VvNENIVGtOSHZWR1U3dUtYT2FEQXZ1TjQ3OFFDaVFnaVQrU2NlVzFEZ2w4d0w5cTNOUFg2TjgxdDNHaGVTSWh0QjlVSjIwclNtTWtqUFlNTG4xem5PcTRkbFd3bUFFNG13NGs4QlZzVmxJNXNrbTczNlhKZlhyUE9aS3lRcjFGMjRhV1ZXOGhuclpveHlpd1JYNmZGU1RDalhuYmU4MkY3cEZzaW9HNXE5Z1pSNmt6ZzROSDNsMlFlZXBXS1dsb1ZPM3BpdUdVa1dqaUZsVzRUcUVDMytseTV5NEtTcitQcStrUmQrd0VncHkwMkdLL001ZlJ3eHU2TjdPbmV0NUQ4UzNMbTlrc1VUNm5TZ1duWTBEcFZpRjF3Q3lpTC93cDExOEJTcHArdHNpRFoyWDVwbU93aTBJckMrZzYvSHdrTCs1OXdJZjJLTmlsTk5rSHRpd1hCcm51cXhVZmlHdGVFZWVkZExBbTJvdkxaVDZTZEY0SFVKYTZId2s3M1hJcFBVR3I0alliaWwwNmZhTFRHK3dERXQrUGdzTnhrZGU5WWFSRWZRT2ErdjluWUNaQTh0NFF0WU4xaDNWMXEwbENZSFBraHY1Q2hUUG43d0EvQ0lNc3dlQ3VXYmZjejNpK3JFSzVOZlJrTWs0aEVCbE9LcEZkZys2VTRadkFzMGIySTNGakIwSittZFNWOWxIVzZ6dEgwZ1I2eGZoMEFkSDBnZi9WOVlXRTNkSGlaaGk0SzNaMkdoTU5WVFQvVGFmQTl4cEhHSEN3M2laUG9XNzhqT3F1WWVjU0R6Z0NXeXppdjVNNThEdEQ1eXVCYWIzWkdIcWU1MkpIYUlDS2pIejg5TmxCcDBjVUVMN3YwMUNIMENsYUkra1R5dWZmcWtQWmNTYjJRTlRTamVqWUYvOUJFZi9jUkY4ajNlTHBTV2RVRW1hZTZ1d3hWVGU1clExbDBCRDZlWjgrOERqdGVYVVRDSW9BdjBUdVYvUEs2NTJoUG9SL2pjOHV2dFdVUnVFK2FsaGdMbnR4T3V3V3pPSVhWV2Z4N2hDQ3BZUHR3TVpMZjBaQmRQVE9FZjd4QzFBbnVxejJtWUkxTEt2b2V4dG9kWHhkaThWM3BCVjJoN2JnTEFhYlRiY3hpZ3ltVVk0Y3M4NmJYTXE0ZW9RWG1rMTlOVllOUFpBTjBXbG02bXp4SGQ2UHhPNS9GcEl6NFpjL0s5eWFreTcrQkh6T0FVeHlJL2ptYjJGUXRGN3VqK2F3d3A2ME9MaUJ2ZTBhaFlvRGJzdWVsNDN3SmZhWHN6cC9ubDJKQlNQbDlkSmFWckpTOEw2L1A4MWxlSGVqbjZpTU5vWDlhK2c5K0pSWGRpeTVNeCtZTzR6NkVzdy9lRkdkRzNLQTZUazBBdmRHSFNpdG9oSVZZSnRxNTZ3R014bnduNWRBWVBoT2xqL0xheFVwMGpqRkx2NERkVkhzSEZqc0VWcDNpVFY4RTdqUHAvQ3FlZFFuMENwOVQrUjhOdG83OEg2cmw2bDFJV0R0bktSUlpBeGdvQXYrZjRjQ0RneDdLNlE0ZkR4MHRyTEN1ZFVSaGVWZW5UT0pKbTJvcWRSK0RmSGhpeWphUm1udmZma1NXWncxQnhaUlhFaFI0NUF5QkpJSDhkaXpBUHpkRS9kdkUxSWc0SDN3NmFHdm03K1VOOFZ1dStUNDdwU0ZJUm51dFRHRnp1MllMd3h4VkhjQUV6b2d1OGxzVmpWcnU3VGE5OHdUdFpTWHR6RGVIZUNMMlp4WlgyM0Y2NW5CZzg3d0NSejNxdnYzMk1WajF3Zk80VytYRVlmZ1VKTGZvbGpuNzBZMWZINHpZTnBCLzBldHgwdXdVaEJBb3FJUm1DSDlCb015RDdDZVFBOEx5TzVpQlNmNW5hMkxBaTR6dlVwVzBxVUgxUWM5SE5XU1BVS1cwdGVLVXBINWV1Mk0xMlJSM0hhYlBKZnBUV1l6WnJHNEFneVQ4OC9mNVE2R1NSSFQ2U3lSUUg4OStxaGl1OGhYNGh2bXhacTNaRWIwRVY4NkwwRUxYV1Y1VUU1NE1wdXJhY3R4TStEbEVmV3NjTUZoaHN4ZnMwZzdmcWtDbko2UVdrRHhqa3hBa3h5RERhL0xUVUQwcDB1eFFWSXkwRzBRdG1VbUpIbklXRVFjSkZoM1hXcmMxaXgyclphY2lxbXhXOHRLTEUvRXBmY29wdzViTE52am5rd1lETVJ4Nk8xcnVIZFdSd0l3TVE4Vmk2SGkrajJaa3lCUjBIWWpTR0pRdktKb2JNTU90c2hRSElvc1pQcWgyV01oaGpUVUZjYitzejVwK09FWGhFZGVHdXB1eVRmRGdieE9JZWVNUWZKRVE1S2dLdzdHZUU5SmMzdVJ1a0d3MEZwRk1qNmFnaExBKy9hMStFU1pEZWlYdGQxVGlVaGNvU2dTR0I2SlZPUjd6QUpMczUrT0JWdVZvcWVQSEoyQ1lKUmpKeGN4V0EvYUdJak8zMGNEekQ5WHJubmlqQjF0TjN5UEgwWGtjNCtSS1ptQlBTWTE1dVMvVjdVVVd1N2dsSjI4NEFIbVJxTTVTSFJ3VkdCaHJUTjIvVGlnM0tVTVNqRE5JVkY1SXVhUHFaRVFpNTlEY0dlenlEd3pBenM5QjFFQi9pRG43R2d0cVZscmxZbU90Q2xheG1DSStwazJQaXh6WU1yWkhleEVxUndZMVNYb3FBTFhOemthVFRac2JXb1gxV1JjVHN1bFhpaC9JWEZwUFl2eEVhNkFGWmZ4NHhsTk16Zk9JdVVSY2J1KzZxdmhhU2hmaCtYdTZTa3ErVmFER05hb3VLcHhZT2I2QitBKzFUZUlJRzdMM01SKzVIdFVURFIwZ2JjTTB1S1o1NFBia3hBRzViaHNINm5IbVErMUc3RWZwZG8xbDVjSjdJSWNIaDNuR0pXc1lsM1F2dlhaYWhKdlB4NVU2Sm1MUXZvUURKY0ViclFaNTIrQzNSRnIxMXBaZC9NVVVSdjN0UWgxMi90UEIraHdyMXNtTm9CVWY5bE1TYjhBQmh1MDZ5dU5mdWIrUzdNN0RiSUhyQlJtL2ZFQmZNZWZtVllVbmdnSnAzeXVZYnFIZ2xRYjZDUVZtSHNlanFhWjlNVHowYXJDRXJBdXF5R2srVWYycmo4UUFSWmk1cUN3bnRDUDBmajBLM1FveEpsZ2ZwYnRrWkdIRlRTdENBR0JLS1kzOG5sK0pLOU9RRkZ6UUxnU0toUExORG1ySGkvWnNIR2JJZ2RTendDL1pXOXU2aW5vN3NuNElVUnUyQUNUOGlGVjhDanBHNnQ5dHlDdk5TNTFjdTlTY1FzaW45K1ZSa2pLZlcwQ09pblhOVWpuNXQ5eG8zOFhRZURjcnZqampMUEFrRUlhU1RRazkrSUVIN2NSRXdFNkhsYi9qdjlTT2ZkaWtmclFsSlpxQTkxeDJEWDNjWjU5cnhZbUJiUGVHS3NaTkZJT3ExU3psSnJCU1Jid3BHYm5UT0lxM2pZemhwNERjSklibDVremRqcFprQ0lIVDh2VnhYZ1lFYnBwYUVjSmJzY1BucVZGWUNNd20xUDRzbGI1MkxTN2JUN1I4REFDVUptYi81bG9xUVBUVVZwWkNrQ2lOalVCcEpNeE50NFpxZDQ0eWwvMWxibllDcnpZR1JPc0RmZS9OcnU5QTJ6TjN0eXhCZmhvZDNRNzlTcytMOG5MWnFaZ3MwSzBxNUh0dUZqQVUyWVArdkFMRmt3RytpYXV1bndBTjR6cjI3U1owakE3dm4xclIwM28vVHU2Y1FvTDFPWHFwVld5dHFtN08wMEZrNFhvYi9zdzZtQ0lYUzRNMHBTc2NaWWw1MDFOR0ZibDJtM1VGZlhzUHdncVRiWkhGNlhCMEdqdVJUMElmTDFWZ2I0ZWVLOENnM3JPVnNtd3JoUnYzVlp4c3lHaVp0eFlYRFZxYWY0VGZEb1JSR1RnZkk3eFIxUGQ3aG0wYmRUdXlVUWZ1Y2hlbHlLdlpIY1ZHS2NMVDUvNW56NVVIMlh4L0ppdU5KOE5SMUxUZ1FxWXlnaDFsMTVEUVE3eStDcG1BZ0EzWGVIS29Tc1Z0R1J3YWh4SEJ1VVAvd0FrNHVHYVhYRGI2SzMyUk50K3BJbVV1S0ZVWVlyQkMySXc0eXFHTm9JZDJxQmdTWGpXWko0TVhOVFI0dk85NUlWbkMvYWdLRkNVdjlvMGFmbno4WlFuQ2ZscUlRa1VBcFRNb0VBOElwVnBUNHlhbS9aRy9rWHJ2VGNxMWt4TTFGUHZ2VVEzaFdLYzQ1TzQwUVozRlpXZ2NzVzFUdi9BK0cvTDZqZ2FmWGVCcVIyaEFoZ1RpbE9kWUx0VFB3ZWtvaWNwTFo3aWZjZ1FpL2ZjZEFmN1kxQjEvaE1lSXM4UG9YTi9zNS9zVkJUc1U4QmNiVzgwemg1MkpTOVA5LzVCNnAyaCsybm5qWU9zNlZVeUpPSlFwRmN3Q2wwVFVsUEo1bzJmNEZjRHpQQXFqYkF3Y3AxakUxR21uZVkxSW5VbnFBWEJjOTZsQUZqSFB0a0hSeW1HeWdsakZoMjFZblMzY0V0MFlEOUtzdFhVYm1kTGZGSk5ZUHM0VS80cVlueHRIUzR3ajJRWSsvSzVXbzN6Uzh3bEU4Ky93bXduT3lMdk5ISS96aXJxK0l6RitSK1A5RmtPSmFEZk9qVDhzTlFBeGhVbVZsS0R2SU5KMTMvRk13OERzci9NOFB2cFFQemRRNldvWkxyVDJyRE9UcFY3b3dtYUladEg2UGtJTXZZa3lXV1hObzFMc0hCaEh0dzVjaUthSXFZYXlKcUY3ZTdPRDB2N2pvaW52bTYvcHoxdVI0ZFNKcCtXeDZzZk5sNk1TcTk4VmlBek9oTFpNY2RBOWNldmlRTWNkMDdnT1IrSDVYVmZaZjZyYWU1dGRBaVUvVkN3MXYvWUs1amF4VHdpa2o2OTlZa1BzZXpSMVdtNUM1c3NmNmhXSktUTDBiWW1yM3BHZ1Z4SnBLd0IycUIrcTVxd2VaRVB5K2J5YW9JWkVKNDBocXdlYkZObkRMS3M5bzk1dkpwL3F0TU5aSFdiaHVhWTVjdEREd3ZZZjVVNVptY3UvaFdmS1AvQ3UxbkIrelRLRHhWVUNRWGZ5b3g0R0YvQmVvVDlaWXNncU5tVzBkQmtyUTUrVXYyTEozc09Pd1pmV0xuclB5Q0l0OUszOGE3V05QclRpcnFwbkRIeUYxWmE4RmRUN1lJc0FQU3U4YVl1VU1vOWszY05xWWRPRVh0djlHc1pacVhKZTBJK2Z3UmpjL1FVTVp0N3lhOW9DTzdFcmVMUjdDWEhRemdqOUlGWEFJZDNNeTJjL3pLUWdDUDhkSTZubnNGN3NNYkI3Z09BT295bDFVSUR6VGFXZHp2aHIxU2Nma09JZEpSQTRzdU9Vc1k5N1B4NW1sdEdxcEsrSEN2UDQwVjJPRGxMUWFrU240MFByZVVmaUFDa1loQUlXaXBqZGZxUGJpOFRUL0VxNEtkRUo5RkNKMnRxRTR5dENWMXFmZVdUVzZEbERFbUNaY1FtUVJiclBGUlVaYzRTakhKYmw5N3hKait0dUI1Y1FhbEdNTWpBT3BQeFozdjMyK1pCZEN5UVQxMXpYWDg1aTZYQk9kY1d6UDZoWWVRazhTQWI3U1dqUVFMbVVXeG9nQytHOFNNTHZKTjNJUWdlUFkxY2R5dG03Q1Rib05hWGxZL1N3NTRKeE1rTDZhVzZIQkduYTd3UGJyN2xRVEl3cmNURDg0NzdsR0Uya2l6Z2hEOVo3bVlySk1DdDFWckRXK3JKdDlvMEJkd3EwNTJ2L3BqT3J0b3NwR3poVGl4aTY0RFRqd0FRdjZjYnROKzF6NENDekVBSjdJdHlMVWRtL2pPbi9BaWVtTTVqRWdHRCtzbjF6M0JEUTZnbmxsSldNYVhnT3oxSmFNTHIwUHk4a0E3T0FNK3k4L3ZOdWRFcm1xcStCenNqalRRNzlud0VXSGVtQVZsSmx2c1p3a09IZnlWZExDbU1MUFN4elBXZWs3amNvWWVINnhvY2N5Wm9yNThTUjRZVlo0ZG45ZEdqeWw0VkhPT0xHcFhjRFRxbGlWcnptQWY4RWJnUThNL0tZem16MFFhOU1uWjJ1TWtoNU1JQksrNWFqMmlPd090TjZvc3FiUGlTVkxudkNONG9wUjRWR3VTTjRPMEFxY1J6RCtqaWVveGlyOHE4YXhTbUhOU1JDVjJnZEoxRzJRU2ZkWDJ6dE9uQ0p3L3BXZkFLWGVzN2JSVk5ZNmt0MnpmUzA0cjFXU2FDSkhvRjd3YThwODd0YnJySjdGdGx6dFVodGlWYUNCTlc2SDl3Y2hCNnE5M1dHZlpqdU1zRWhlV09MMHdUM1pEOXpmM1YrWGVyYmJOdCtLVHdqRU5MUW1sUVlWNnpiK2dBM3NtczdxWEtidS9IWjZwVW5ManNHNmkyQy9LQklQNWsxblpoWCs3cXlra2tPTkRSKzFXRFZmcW52OWZLNjFaaGYxRElnQ2ZON0o1S2JLUGJkLytVK3kyQkpHQWZIZEZ3NEg3aXA1eUJncDVsU0xTMWlQU1RFamdMMURTNEE2eHliMU9jYXhCRWxJOHJBb1NvVlNEWW04QTUzQWE0K1JZK1lIU1NaaEtLdFJNOCtXTVNYa2RyZHVFbjhGT0pZVFQ0bXFpYlJWdjZnVVhXV3VleXdQWXdzbnEweWJUY1lwbmdxaGJ1RDFuVWNzSEUxT2s2alEzeWswSHdjbmJNMVFwT0lIbkkxQkxKc2ZpdERkNEdIcDZJdTMwZW8rbVM0MGZQc0wvZHo1SnliZ1dRaFoyT2pzUUdJRklFSVNXcCtXZXBuVEl2clBqdzYxeDdHcVp0V3hvYWI2MUw5OHVxUGFUalBzTGxmSFNXaFBUQ2dkUnFWVE1va2MxYVJCdXkvYml6L3kyVFRXUWlwOUZMd1NxOVRYcXBseDhPbFBPRkU0aDJSbDNYSjdRRVY5Q2dSWDAwSjlzcXVVNUJqckRSckQvVzV1eFNpanFTQ0tlVzlzdmxjbnM1VEdJLzJJVWhXU21oWkxGN3M1TmpxWTJHYlYrYzltSFZ2UXAxWWl4dENCLzl1R0x2cThFYW5xbnJQTlo0SmVJSmNTZC94eVlBSDl5VTU3VTlZdFNOeUF2RnU1ejhZckZiZVdJVE1LSi9kK3M2UkxrTXRkR0I3MHF1R0t6TkpGKzlCOXZMcmRHMHE0NGNnYlBsdmhQWXZTVHlMMEl5a1IyUloySEpyVXlybldic1F2VDdHLzBmM0xCUzgzcyt0R3ZPM2VJQlNRQ3crMTQ2YnF3eE5PT21OejY5bzFQTk55bThzRDZWaHl2RlJNcjBHQXJWUDJZZEo1eDlyWnhPOURJcnR3SlJ6RmNraFgxY3RiUno1MEh2UllPdkppQXpoUHRFaGhlTlJXOUREakRPTUVuaWJtVENtVmwwVUo5UUFxendpUTM1eWVUTk9VenpJakNsZ1NKUDBaTDFwODBJVFVkSUdXVDI0WkVrc2JqR2hiL1BPT3J6a1QvTHp1cG4rSE5ydnFSZXFaUUt1V0I1eXMzcTVieGFSMDIxODlTeEMveEpkR3U3djl5OXdpdzV3T3ZJVHpEbE9NQ1hxOGk5N3A4SktEQjVCejFYTTFZU3U2WWlxcEtqRURMZndCSHZUM3o3NjYwanJDazV1dnlJNW5GV05McjA4ZDhSYzIxOWdXMDdsZ254VlFHSVhHTXFjZGdmMEM4cHRZTXcxMEVBblJ6ME9IVzh2Z1lveHBBVFd3L29CT085Nm5yYUpNS040R01adStiNlM5c2QvZ0d5S0Y0aUtRS0oweGxRdy9idDVId296dW45V0FXeHZUNU0xS01GMXViY1R2N1JvelZHMWhabFNtblAvVzluRDFNRlpwSi9xdDFrMlNLT1ZWRFVLbi9UeWhCaFBseFJERmVDYWhJSjRXaEUwYmFFMzJxN1dPZTZTYVpYU20rbXpZbzY0K3Y0WWtuU1MrMXN2aXhaUGVlRWxDODQ4bzZpOU5PYlNZVlpaZUxKTG1rRmNGWDdCTWJGSzMvSGhMb2FBY1Z0UzZwZ2xVVU5YMjRrSE9hcHg0RURFYjd6eXlJQjBQWUtjU3liWDY3VDBEdnc3VnJ2WlZNM0RtcnZhd3Z2OU1ROVJka2xGRytTR1dIRm45c0pjN1Rpb0UzZnVrVFVyaUtWM2RaSTljazRiNW5MU0VhY1ZSQ2dMeU9xdlRjckJDeHdBcjBISXJ5aVl1NTVjT2tZckI4LzJFSFpGRHRhQUxxYUloWkh0NXQvQVVpQnM5QkZld2hmc1lTU2lkUHJXbHZRbmdKSlVrVnNQNnJCcTFRM2ovU3ZjMjVmTTkxLzBFbzgrME42c2VlVTFjZmE2Qk0yWDJBWHdIQmtkS1paRFpSK1lDaElYZHJtKzdLazVLTGtUUGtTcjArOTZTZS92SmtHdVFTWjNlN01pZWFOYXdueGF0VGRlSXZhWHZkcy9GSzRsNWhuckZCdWVEYm53TVpCV2loY0tHdGcwQVlHSUx1czNrUU8xWDJoemxpSFl4ZUFlT1Uzc0FuUWdkeDYwVnZmL0I2ZnFLcW4rUEZqYXVPVnNXWGNyWFVaT1JYTjNFSVJLOEs4M1BEVC9IaHdnS0RESjRmYVYxSkorclhxTmZJQ2FvQW9FbHBqdzhHbFpHK2phS2xIb24vZHUrSmRhcC9pQmRhbisyZ3BwSFJDSE1qK1dhQ1p0dlYyQVVjZjZsUTVVVGNvQ1djb2lLN0VrT2ZHdHFOR1Y4emhlaGM1L3dEQitmZDgydWVZNHJjc01id2phWTR0YnhoaHlSRlFsd2VhU1BjNUZQam9kTzNTcUloa0w2OUw4RVBNWHMyS1ZocmRsRktFZlRSRlk0eHBJcS9PVURsYm9kZDUrT1VVcTRjbElkV21YYklHSktIN0RUdHF5T1JSRXByUStyWDVSZjU3WWFrcURobS9sdy9xZ0ZNaDk5dWMvNDFlMEpldklvalJBSy9uMUYwOHpCOTlkYjFjaXpYQU5tdnFseGxsV2xsMGFJTmFGN2VzVUlZVlZCQTJiSFo2MG14OEtCYTVrNERkNXpxRUdSaVNoN2VDZlRtQTYrNHArV1BtZlBOZWNZQnFSZHRabExDSEc5WVpEWjQ3dDhGUlJlOEQ5STM5S3hRVjlUMU5rZGtaeFhROWp1U0hiR25vdC9IVHpPcitzYkZVRFN5TjBmT1FDS2pBOTZUOThJUHVHdXNMRHZsM1V1TUhRK2tPdjh1YSsvKzBMYWJsbkRKcTF3SGpOa0ZnTUlSblJQUzJScU15TklRZ2NKTlk2S1lzS2dobkVTVjFMQzRSQkJNb2xBY1dYODU3QzMxb2FUUHhxczVDTkIrZktiK2NnTUJTRXNEYVJjUjFsRURDRVZ3RFBSUlJpeVhMcUN6MW1vT0VncjRhVzlhd2VCOU1mSW03dms3WTd3NjJSTkYzY1QxTDF2TEZKSFlnQmtKb092QzA3NGI0OGpncUJqMmtqVGd0bDN3c05LOGlpUXFzYUM2MUV0M2JwazRBOEozcFcxT054TTd0d0kydnFya1lQcTdXZ0M1RGU1d251aVVTTDkyT1ZkbTZ3R2RsVDRIMjdGZytISjZuK00yNkhuQlF4S2FOOXdFSG9yYmdZd0hxcUxzM3g2SjBRbEhGWk9CR1hyRE1kUHJURTlxdVA3ZzRhRHRvTmltWllUMkpQM2pyRzJXZHhRdWNGWThpckpkR3ZRRW95MjJKdmtqeGpLZkJYamRQOUFuZkkrNTNWb1ZKWjNZb1FEZ0pVQk15RjM1dHZqZW1qWkhIWEhkV3RSMWU1allHakZsNDQ2WDZVWC9JL2FidFRjNVp4emhSeXRlRzVkYkQ2dmcyc0FWeTZ1YkN0NWxKc2NNM0o3WnJsVUx4QkdnMG01NW16bFhHRE5iOStocWN4Mm1xb0FQRlpjK3Z5Z0dFeTI4UjRiYnJBTzFvNjNqTHVxMjJESjcxZ0RRbnV6TUNhQi9xbjhFZW1tUU55L2ZXTTRGdWpqMzI5S2FzQmozamFyY0gxeTJCaGFIeFg5L0RwcHZQKzVrRGJ4dmVCamV0WmZ1MmRVMW9DSnIwbGppVy9QUGNCbkR3YkdSZVFxUGRQTjE3TlJaRXVMd0ZwcFJkZWNhUkEvQ1VkRlRCVnV4M04vSERLRWdRaEozVUxnSUtQNHFkVjNLZVlsY0tJV0l0WXpYNExqYnBJcnQ2RWtFSjJVODl0K3lQa0c2eEdKT3BwWS8vRC9DRDNWTHVHT3RGdFZYZnVQeURjV2NtOFpmbDFkcmoydHh3TUdJelJPMjBWK05COVQ3dVN6WVRuWWFvSUhLV3MwY0dRckJaY2ZWWXBmWVVIQ1hJRm4rUXF4eWZnd24yRldxVWkxWm5JaDlNN1NkRE5pREl1bnFZcmEycE5LdGh2WjBBSUpJdFFzR2cyRmZRU1BnUTdtNU1mbmtyM0NTeEZOVHFQQzdnWlhTUEVjN0ZVLzhsdzh1UGNIRFRjSW05Zm9jeVpzMEFlak1LNnVWRm45ZUYwaXZNbS83NWNDbjdJQ0JnVE1ITC9oWFV2RTh6ejNXOGo0ektOdHJ1Ni9BaEFqbktxaDdWRjVZLzlrQUFRQWFBRUFBQVNNQVFBQVBBUVFqQWdBQUFBQUJFUUg0UUFBQ0FBa0NBUUVLQWdBREFCbFVhR2x6SUdseklHOW1JRVpwYm1kbGNpQW9OQ2s9PC9CREI-CiAgICA8L0JJUj4KICAgIDxCSVI-CiAgICAgICAgPFZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvVmVyc2lvbj4KICAgICAgICA8Q0JFRkZWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L0NCRUZGVmVyc2lvbj4KICAgICAgICA8QklSSW5mbz4KICAgICAgICAgICAgPEludGVncml0eT5mYWxzZTwvSW50ZWdyaXR5PgogICAgICAgIDwvQklSSW5mbz4KICAgICAgICA8QkRCSW5mbz4KICAgICAgICAgICAgPEZvcm1hdD4KICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-TW9zaXA8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgIDxUeXBlPjc8L1R5cGU-CiAgICAgICAgICAgIDwvRm9ybWF0PgogICAgICAgICAgICA8Q3JlYXRpb25EYXRlPjIwMjAtMDEtMDdUMDI6Mzg6NTIuNjcxWjwvQ3JlYXRpb25EYXRlPgogICAgICAgICAgICA8VHlwZT5GaW5nZXI8L1R5cGU-CiAgICAgICAgICAgIDxTdWJ0eXBlPlJpZ2h0IE1pZGRsZUZpbmdlcjwvU3VidHlwZT4KICAgICAgICAgICAgPExldmVsPlJhdzwvTGV2ZWw-CiAgICAgICAgICAgIDxQdXJwb3NlPkVucm9sbDwvUHVycG9zZT4KICAgICAgICAgICAgPFF1YWxpdHk-CiAgICAgICAgICAgICAgICA8QWxnb3JpdGhtPgogICAgICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-SE1BQzwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgICAgIDxUeXBlPlNIQS0yNTY8L1R5cGU-CiAgICAgICAgICAgICAgICA8L0FsZ29yaXRobT4KICAgICAgICAgICAgICAgIDxTY29yZT43ODwvU2NvcmU-CiAgICAgICAgICAgIDwvUXVhbGl0eT4KICAgICAgICA8L0JEQkluZm8-CiAgICAgICAgPEJEQj5Sa2xTQURBeU1BQUFBUG5GQUFFQkFRQUErYlVINWd3Y0RDQXNBQ1FBQUFBQUFBRXdBRUFBRHdJQVFBSUFRQUlEQUFFQjlBSDBBZlFCOUFnRkhRRHVBV1VBQVBsRUFBQUFER3BRSUNBTkNvY0tBQUFBRkdaMGVYQnFjRElnQUFBQUFHcHdNaUFBQUFCSGFuQXlhQUFBQUJacGFHUnlBQUFCWlFBQUFPNEFBUWNIQUFBQUFBQVBZMjlzY2dFQUFBQUFBQkVBQUFBYWNtVnpJQUFBQUJKeVpYTmptY29BQVpuS0FBRUFBQUFBQUFCcWNESmovMC8vVVFBcEFBQUFBQUR1QUFBQlpRQUFBQUFBQUFBQUFBQUE3Z0FBQVdVQUFBQUFBQUFBQUFBQkJ3RUIvMlFBSXdBQlEzSmxZWFJ2Y2pvZ1NtRnpVR1Z5SUZabGNuTnBiMjRnTVM0NU1EQXVNZjlrQUJBQUFVVnVZMGxFT2tkU1FsUXdNZjlTQUF3QUFnQUpBQVVFQkFBQi8xd0FFMEJBU0VoUVNFaFFTRWhRU0VoUVNFaFEvNUFBQ2dBQUFBRDRUQUFCLzVQZkd4QUpWT2QzeHpjWWp1MXc2NVZGaEtwbnZQRGYwMVYxVmdOWitSUG1ZOEYzSS9wT3hrbjlLK0dyTXF6VWRCUGZKb0JvZ0lDQThZQkhIK1FEdzhHRWI3UmJQeXFBOGFCcnpZSlpSN01hS3lIZ3djNU1nUHdKZ0QyckgwaWU4c0lpVlFOSFZoMDFTdFI2MUUraFVpaysxUkVFM1lrRVdKRExUa1ZYbVA5MVNhT3lnWGFBdzFDT1FiQ0lvRm12bUs1VWFSWlJBRVcraUc5S2pBdytYMjN3N0U4ZVZJVXJPcHlRTVVEOFNxL1pKdVM5dU9VNGNhNnVNYXErbEZwcGFLNlFoa0cxT3c3emJFdWpBSEhrSXFZYTM4U0lrQlZzWThzYVZGMnhNb0Q5c0J2NTRFeDV1QzNUK3BQMUJ2eGt2d25mbVVDK1JqbG9SdDJ6Z1Znbmx1Sy9mUFdlZUI1S2JUV0JQQXdWQjNzTFMrRUZ3RlYxakU3UHNBSmUvYmwxTFBiSUhPc3hBSHgySnl2S2dsZGdJc3djczNUL1JqLzZpMkQreXBFMTJISWZyaTJ1Sk9DdVBLNlRVc1I4eHRLdllJUGh2RzFDQUxUYlFiQURYRTZQa1dnQVNMQklyNWVUYnI3TjFpK0VwV3ZPVklERm96elVpSC9zOENsL1BMaS95LzI5V2lxRitML2RyN1Fwb3pscDFMYW9oZmYvRGxIbi94ZmplamlwcnVqak1SeEtMdGlubjFESmtNam8vSDVXaFBqeDB2RXV0WXFBL0VXTkFMa1RuU2dCakxKdlMxQ2prYjZjb3B4blZ3RG0vVTVVQWVDN090WEdUSzhyZUhGemV1V2dSUFRjczFHQ2NiNExyS28wRllERmR5NG1qR1lWMldSVGQrS2tFRzhJdFBYNWlQM1RqZGdwaEcwRWdOUTJwcVFIV2lSVkRBVVhIVTFUclY5MXA1dUhXQjF2aW1DQzVhZUpiZ1BCWXYzY1VrMXdpV1NHL01DTHVieGFTTUpQbjN0YzNHWWpnNVNpdlB1RUtWdnNROVZMTVlOMUFTT09JTEFJMW10a25uYk5jaDBIYlBOSzI3UDd0OXJCdjF2MEdoc1EzYlB5ejN2N2VqMnplZWVvbytFOEc0TkFVM2Q2MEtlUGdmR3p3eVNsVXk0NjBMaWtjQzV6Y0NneG9JV0RBRWkrdlgzbTBCOHFscS9iamNUQ1RBZ2FveEhROEErcGlCZ0h4MzFwV2RGbnB5ZWFmOXBhTmtZV29ZcVRpMzV6Sko2Qno2c1J6d1RnWnlFOFVIcllYeXBRa201RWo5QTJmNG44cGZmSVRYem1NTGRjb2ViMFo3UzJ5NnRmNllsaTMxSnpnMFN4MWpSRkVSdkNJT0ZWTnROY21EaWFqTGczTG1TcndXOHJXMUxsOEp6WXkwbHlsSzg5a0swQWluVGtxaG5ud2Fyb0lyZzVpY2Z2VWQzQmFtZVJ6ZjRRWDhNaHM1bW56d2xnc0xlWVZjbjRUWTJDNzgyVHRpanZySXVoT1NXK1FlcXpkQUJTdjQwZGVXZ3FFQmVJTk0vaHJhZ2RXM1VteXc4dmNsVkZDOGlkWnZaSGM5VDluNFBLdDJOTURBamxFZldRS1pTbENZNWRTN242VkgxdExUc0hZRGFQUW5PUzVhclRJOUo1czk3WDdWaFhSeWZwY3hHOUc5ODZiTVdrcHdkVTdSN1N4YVI2YnJ0eHliMmdsSk1qNkNHeWo1S3R5dTRyVGdoQzB1UlRwQ0NreGY1RkZSNVdQa1VTbHB2aTVJZ0pKaUFrQllZYk1DZkJhWW51VENmM0k5SExZNUtVV1I4cTlHeTY4dTJVMzZVdW4zaUczNzRMU3ZvNlR6Q2UyMmp5aTV2Rm1HRVN2ajFsNDFhc3lra2NTaVNjcTdqZ1FMSjdlK0ZtekFoL05ENnpiSzdKd3UrSEVGZVE1aGxyUnBxUVEzLzhwN0Jhc2tFeG5Dc28reWI0SitwMnVmdVJVVlRjdE84cVhjc1o4ZUdFblZjVE11TmpCOHh3aFlGQ0h4aTlWcjdxL29vdndOdDRmVlZLYVk0VExkbUl2UXRHbXZqMlB4SXdRc2c3bElvdlMzRnpqMVl4bERQNmhOcXBBSUE3b3VmME1kc0ppR2l3UzJ3Qk9xRTAxT2VDS3lGSHVpS1gva3l2RFlsNVVjQXM0N2xsT0ZHRXBZNm9iWlN2QVQ5WnNTRCtRS20zekEwUVhqdUU0S3liWVhUVmZYQUVRdEF2Uk1pQ2dJRStZaWtFVEh1U2NDZUYrREpQeTdZQW9od2JIcDc1eEFBU3lkVTE5YmtqbW56bG9DTGgxZzI1Rk9lTSt2VW5Jb3ZyNWwwS1Y1UDdRQ3M1cGJ6ZmFBek9TaXlFRUEvNTVMRFY3VHpBMmRLOE5EVXFQd1BGSVBxTVNTblAvdUJRY04zRTF4K3JqemtyaTNxVTIxYThhQ0o3SWdCaFpaZkFWaEZhdDBoMWZ5cVZHbE9lMGhpUlYrakRPLzlnWnZqVmxzUWUvejZuT3RDYVpVMXZ0OFdpWWtoMDNLYjZ5cVdwemhvOStMb2E3NXJTY3JuN0Q2SDNnR2hBbGhxb2xDWkRpSThjVFl4ajB3MTlUVWd5ekJWeEJQS25VcFQyQ2daU3Yxc1RDZlRvNHpNYmp2MVFzeC9nRUJuTUE1ajVDWDFiRktPcWtJODJZSjIvelF4cEVSL1NaRVN5V3pJUHhOYlo2VnhDSmN3UjBMV29WbVEzWmhQcmNHMUY2bkM1MEV1WVVaeHVZWHQvbEdzOW1tbkxSU2EyWVhXaXJNeGVUV3JsTDhUTVJiNUJlME13azNZVGd0SE5rN3FBSGo5aHFQdkNJMHRtYXlsQndlYThtWGo1bE5vd3VMMXdBVkQvQVlhblVhN0k4cHFPTWNBdFJjaUpPMGxlczcrRER2Y0RNMDFZdGxkU2VOdnVaM1lYbnZlOExObWdlR2J1WUhIQWlWTlBEZFVwY3JVTWp2YXhYWU1BR2d4TkV5bjFidkVqMTFld0U2WnYxQSttblZsR1hBclh3Rk4rTndpT0ZJdERvYm5tTVYvZGNxUzJCTlBYMmUrYW9OalFLeisyR1ZXOEorNGtqRGZob2RFTEMwNHdGelVGbWY4MXlybnZ4bE53cEVuVlllZ2ZMcjBZVHkyb2RkRGJnQ2dsVnRBZmg1S1BnRy9PVmRQNmVVT1FpMVA5c1VOSzllK1MvZ3FUSnRBOUZVbFEwb0hGTXdPWW84dW5YcWlqRDRvTXR4TlBqNmRBVzdVZkI4Wk93d2wwNEhXbEl5c0lWUmlQYk1iTWh4Y3dNUiswbGNzcWwvanR3eUF2NkVjWUpGU2pXZHIvY2JqaktFNTdXbldQSi9yV2V5emFtWnhIeGpsSFgxTHQxSFNnOWQzTEhWbWpMWnlRVFBWNnNHZzF2blJSeHc4ejMwdzJEOFFpOVJodndhUTY2d0E3ekNlZ1RnYmJzeTVqYWhmWDI2LzJaZTF1Qm53L3FLUFFaZm9qV0hYN3RtWnU3Nm1TMU93VUl5dms0QmJzOTVtR25qdFNUZVlMUkU1d3pkODQwRkRzUExNR0o2all2ZTcydEROUmQzU2JBb3U2akI5ZEwzVDhXdEkzTHdhUXdSVUppS01scm81eldsSWJ4RjRFVCtZM3BnUlJ0VHN6YUtmZUxpeUtOOW5USWNLSXk0cmFNVXhRNnNGZFNjRk9LenlweW1FdEI4TDNQRFpsMzgvQzdUM2F4a3JZVzBxKzdTVkVZenlIMFFDdHNCNHN6aFowbm9DeFlNdG9QdFhXc2ZZeWliRWV4TkgvTkNWb0RYSkU0YllmVkdDK3VMbVRwQnhCejZYaE8rU284djlaeS9VRTFnN3Q4cmJjL3pROFY1WlIyR3dKRUlHMFQxMXlFUlRkZVFEWnJnWkxkeUdHaW0vS2RYQ3ppdlAxZDZkQU5Mcnh3eGd4TW5SSmkxSWIxQlhmWFA5Mk1ud3paVmVPakF3Qk4zQytQQytrR0hkeDRqQlhxcVg3bkRnNTd3b3p2K1hBSEJkbk4vbks5alBNTlVMbGJlNk1vMy9Ra3hZdWVKV0J4akRPV0NLOXI1bk40alBMK1JRdStyM3lKcENSTUY4RVlqQW5NUmxIU3ZQR3kwSGFtNm5DRXMvZHVteUErb3ZzT0tBTkp6eGcySEVoenU2VEQxY2N3cVk5MFdhbmp5V2hQSmtTRGtRRkFqeFh2Qm14YVJrYWFqOUtLZDRXcGNTU3YrT3M4RTE0Vko3UmlvM3NnZGxjOVh1TFoxckd6Sk53UGFTRFFsZSt1K0pZUFc5eG5Pb2Q3RzlzSkFZY3l5dHc0dVJDNFVLVWo0TkN2dDJpek5HS0VqVXp6Q3NoSURxTjN6WVlaZXJNd3ZiNjBGNWFKQ1dsMVdCQ2QrUVovekFTdE80SFRqNlRkRWJyY0dhMmFrMzhIQ0d3OHNLbTJvZjZXM1d1ZCtXOWI2VThHTUlReitZT3Y1YUZZdEZQbU9MU2VvMk1HcHZZaU1pUjBsZS92dE5rdmlxLzZZNERFWHF4MDNnM0RpbWlrSU83NnJBd0VYWENGeTJ2RUJaTVE3Z0x6cGdUSmxvMy9JUkcrQlZScHhXRWhvSXJ6cTl0V2JtOUNNZVpaQkc3VjIvVXd6TzRCNXNFakxHaC9Rd3Y2R0kvV09vQS9QblBiWFhwRXZlakROYkx3UnVtdUJ5aGZ6WU1JQ0RISHdVN0EzejFSQW5FT0trTjBvcFFTa3FZVGVGOTFtMXpWWEdFTC8zYmdhU2I2QWp1L3ZpekhwV3c1QytUTWdNQzBXaERqREV1ZkY5RVVnYVNMd3VpdW9kYVlEZHdGMWNMNVNFSTh3WVBQNXJxMHR3NDN0bmtkelNwTnlCNWJIWElPODNIc2prYXp4UDc5NWpqbHU0MjF4TkpJNUlXUTJIcnBhN09URXlheDdqcDNySVRoRHQ2YTRBNCtiTWp2NXFGeGZRSFJRYzF6NzM5WjdyblRnT2tSVVlJQ3JYdlNtWG8rQmsyQU54UnUvVENOaVU0dzBrQm9qSS9MYTQ1SnRwQzBxcTBoc3JpRmkvNmtUZ2ZBVVFlK05pbXlPeW1PWWIwTnhxdzhYQ3J3aTU2dG52OFZBSDVxUEw1RlRSVlB6eldnWk9hYUdmNDhJT0kxR0R5OGJ6dUFnazBNaGhyL0hSTW9HemNzZ3czU2dsNUpuYzBLV2V2NDBFWUF2a2g4RHlRb3hlZ3EvUFVYdktMZVlmN0dPN2FUMStRRGVtREQ1ZSsybFMzdVVwT0kwTisyaXVjaHlIdFRUL2NBWEcxRWlxQkRwc2lpb1RmQThkRkpHK2xkRitFUUkwQnVXMVB5Um42STlBUHhsRU4wcmFTdWRxaUQzWXphakNLOEp2UGNpTkFyQmt2VVFWS0wvbTRRbXpzeGlza0R6bjZxT1NrTjg1NjB6UFh4OFh5NzNHMS90WFplRjIyamhrTm00U0ZQT0QxU3p0ZFFBelVqK3Q3Wlo2cFMvRUtJNURUSm95UFN5KzdQdHNwWnNxZFRmb0d4eGFucXNRY3JkYnlnSE1OcCtpenV5Tlc3L2ptMUdVOHZ1VVNxcWlxVVhOazJyQ1IzaFArRmw5UGw5a2RNV2pEZ2pvK3VUYnFEbCtiS2NYdVh2OHZ6MkdnUFhRVFZiVUFnNDlka2FqQ3YzUVJ2d3EyNW9zZ04rby9ETUN5M0R5dnYxVEo2c0FheGVrTGVhd1RPMk52SnFmMFBpNHg1dFlMZWtRck92eTVYTy9tcUVyQWIzVmtlMlVkRUtJZENpMjdZRXVITHcyMjlUR3VaRmdQWVQvTkxMR2ZVUmJZeG43TWVRVXQ1eFplVjBYOUtIVm8zaFB2dHBOeEorelZWUU5FdG5EREVGNk0rUUtGTElHS1dMUFJHZmh6ZEFhTlduQ0JIcmtqTlV6djlETzJwUUhXdFRoTERmWVIwU0Nid2l2STZUUHRlOGdUWFlseUxWL25CcG5wWi8yZHhiem80VFVqNUZtZngrQUtmRnJjZDMvNUVxYjJudmlwVHdlVW1EVFBTYjZwQ0VvZHNHaVE3cW1mQStDOFJjNEpGT09veUNISXJycEE4QXpDWFFMSGJ1Qm9wdTNud1dUTFJhRGwzREdoNG50ZHJTcmtpMnRWZEtlNmR3UkQwN2RWUUkxdnNEZytXQWdXY2RQQVg0bFllNXA3L21wRjlzaldnc0VQLzBHeE1rYzc0SVpOcXV3ci9tUmV3dllQWVE0K3NOVXVxRGxORmU2T2paM3BGQ2RNdFRSamdmcE8wMExnVm5RWHI4SWZFbHQzL0dXdVRwMW8wMVVxMjQ2Zi9pQ01ienZmL3ZRdktSNEZKQWg2VVJpbngxMDJoeEhDYndtRVN4WkVhcWkySDJPZWJwdlI3WWxrSlRvbkJSZC9lOFFTNk9ORldiUnZTa080TlF1OGI0VEE0NzVwZmJHQlg1dHArOEptb2FJRnFHTUExdjFra1JzK042bzUweGxiUFhoRldPOEdGSUE5bStTazdWeVBJOThqODBOZ0xJenhQNEY1bVlOMHdlUHRJY3VzeTk5SmNMZzVCbkFNY2RaN05VaS9jYTRqbUdMM2JXb1YvcGIrcmJhRVlRa24zeE1Yb2JZTnB4ak9oc25GS3VSbng5cnJuczEwNTh0cVZhQWZkOEdZSVFNdXpLdzdhSnpld3crOVJxNGEwUzlhUE9hZWtYbzRzR2s4cU5qWWswSTI0T0xQdmRoazRqWTQ3SDRJZUl6WEFFSnlxWWRsZkY3SmxibnlxUVVlUSsvWWdUSEpMemlrc2dNV1ZjL0FPQWtTMHFiNlRlR2pSTVhZUGxMUGFHQ01NNldJeU12UEt5cXRKMFNTWXdtOHQ1RkZDUzNmZXNROFhKSmtIWkZkZkVIM1plTXdMQUdDWjNZVDBzWHRhbjFtSGtZWDBHN1gwdFlJVzdYdzQzcm0vay9uWDlBZThFelNlcHVaQlJzYm44VG1rVUNiZ28xWHNjYjJsRXgvaDVDamVrSTRXOG8xbExaNysyMmFsQm1WOCtHcjNVditramVkUWNWMG5KU1duNmNkS2pqRys0YnhjcGtGR0NzRzR1dE5FQ0hwWnVjMVhNWE02Q2JnN1pDN2RVLzFPU2UyelNRUWdTNTVQVzFqMnRUR3hLTXNZZGpHbENpSU1GNndleWszZ055cG5ZUUlQWTZ4V3IyRXVNRlcvcGhzajA0ak9VRElReEc1OWkvMHBVVTVyUk9JekxWNzFDeEh0V2RENnRvNldxVFUzMERKZzNtNTNscWZlS3BOaEdTRVNOb29SSTQ1UlkvRDdvbmM1VEFOR2hNeVRoZElRenVLeDQyMUUyeXhnWGlRU05LQjRsSXFHVlMyL2FPMDNjZ2NsOWEzSUJhT3dNazYxYzdIcStnMXRFWUI5Q3ErK1ZmTlJvOXplVm5FZjVPdnpjbGJPeVlDM1drbXRHS0o5UDVtVldzb3dWVTZ1SE9hSWl4Y1dtQkFnMU5GQms3U09KaUsxdENFK0JZMmlhV1AyOEJzWXhZSWpxc1hyR0hFdzRwV2J1YzFSQ2lHcnJzM3BySTBqOWhGeUxna0t6SWpHT0dlc1o5dCtpQzVQaGFORDlmdFAwY1ViM1BQT3ZXWkpZTTRXRlAvYko4OVFhajdWU1NKdFg3UEkwby9heklBTS9CQWNVdzlFYnNkcVFkUDRqSFBUY2FrVEF5WTEzazRKTlloVURFc3dhdVdwcm5LYlIwUjErVEpidXA4d2VyWG10M21nSHFpcFU5SjZubVg1bEVuRjlwSFJQTWd5RmcyaWU2TVpWK09uQ0FJVHRVRE54cXJoc0dtSEdRTlF6OE1JSUlYeGFXNWVjYVBkTGlFVWZVbktKUzdzZjlRR291Z1dTcS8wd2F0RXdkN28vai9PU1Q0U29aUWV3ZFlFc3IzcWZLVGxVOTdTSzNDcmIxN21zYVdRc2p3Ynd0a2lHWjVLSkp0Rk8rbmxoVS9jMUtSZ0VOUFgxZnR5M1VtNkZidnhBWXlsc2JUdXJ3eVRHb2I1a0p2NWw2OEx0blhIUmNtRTVJZGhkZThCYmxZMDB0R2Z2QmU2RmJNaWtuZzltS01lRjl6N2d6T1hkSzVMTmNiTkEvOXNrNXl3SzZIbW04MTRwbU5qTzlWbVBocmU1VDJiTFR5ZWZFYmRQNVhUbXBNek5MSk4yMXNvdmtaR3AydUVFNUpyTjJDVWNGWDE5eHdXaldic3oxWWdZRXdCK2tOYk0vdTlzNWcyNjQyMzFKeDhaQms2b0cwSmpzQVIrVVVnMC9kNzZJeU1kTkxxb0FIeXFwWklwMS9TRXdPelQ3Rko4ODdPMU9qNDFRcE0zVDZJTURad2NteE4rRk5JN0tqYXloaHJNL1NFaGFycm0wcllobFNrQWpnZk9sNU9kei9ab3ZXN0FyNitTdlE2bVV0T2pMWlo0S2d6eWRYZnFLRVZmTktjeEJGQm9uMEF5bTZGZ1hVeHE5VEhqSE1lTDVvU1VuOEpWMXNCSTVraEZQZE1YeU55cmpMZzNQM2N6TFBCWDhzcU1hb044akErOTlDTlI5amFRVkFXOXE2L2JlYmxtd1VrOTJjTy91UmhvZXliRmd5TmM5Y0dlenFBc2l6VHNJYWRWWnZ0cU03azBVNStLdkt0L1lpOWFwcFhhSnlTb3R4THI3WXBLOUVBcnU4YWl0U2NCZlVUbXU0WStlak9IemZTWHBvd1diTGdVbHdzNktuemV1NEpHVTd6TEQwaEtpQld2emNBTGVycmhKS2ZPOEF3bDN4UkhpK1BsMXgrU1FLakJrTzBqRnpSY20wd0lrRWpGQWlsOGs2WmlQNWp2NkoyMzJTYzgvdXVjUVVhN0tKUjVQZ1Rwa3cvVW16NW5oSDVxYjZ0dVFaaklKS0pGRVRHcm8vZnRyRVNYWnRkMmdWZDdZN09HeVNDS211WVRzMG5IamxaSy9qUTJxVjZmcHV3Yk5rRTZPRWx0ZFRmNUxOV0tqT2crUDRPSGVpZjFIaGM3TDF6MUczSjN4TmRDZkQwY1Z2VGdZVjd4cFU1MnRpYjJOQitxUnVDU0Jyc085M01HNDdTKy9EWDRDQWdJRG5uc29BNVdKOE1zNGpZMlByVlRYQzQzR2luUStNbzZWSElxdTFmSlR6d0x1SEk2YkNNQ1ZVM2N3ZlJXK1FPa3F6U2VPTlZScFpCdEMveCtkeHpiZnQ2SEdubFA0eVdGS2l3dHNWN3EyeVBFRkY3MHU4WERXdU41bVAreWt5VkhkZk9FTzE1TGdPVS9Kd0hpOStpNnBjSVFrK1ltU2xIdFVvc0JHeTRkdGE4T1JaK1hUUTRYYzhyd1luZnBRaVhFczB1eG1jQ0VzZ0p2T3pnOXBZaU1XL2U1SW9sV3F2MzluTW5Fd09lUDFJUEtjRldEb1FiaXE2MUdLMDViRnN4VjRyQy8yNEcwZ0RlTnd4eXRDQ2dwbTFnSkhseXpLK3hmb0sweEQwQnhuZFRoSm51NHV2TkkzV2I3b3ZPam52UmdCTzNNZ203NHU3ZzlaOHJFWEJJSFJHSWxjU0lhWG1pRTUzY2g3cHBwUEZtTEJSNklDOHA1S2UvMGVGdUozNStGM1pYM29FMmRwWVhFNVU0U0JsenMrWElneGY3c0Y0UG9NTW1JT0cwZjVCcktMRjB2bEU0aEt2MDZUQnZpSTBoaEkxakxDNnV2TmZSNkdxcUZvZFJXcDdrQWtHd05lMUQvWUNNT01Vb1lkYXE4ZzA2U0dObEF1YUloT3RHNFR0bzNOZHExOGh0b3BYS0orSy9jNGN5MlR5WFcyZlBIK2huOE9rTis2OGI1MzlwL2diZk9CWkJYNkRFTWRtZDJ5WFBhQy9qWGZ3ZWdUbSt1N3ljaUVudVhoeWVjQTlNbTZMMWptYnFsSTZpem9QNEltKzN6S09CSTJlZGUyUWE4alBaL25Ic1ZSNFU5V2xwT3pWRUN3TzRkcncyZjJQekZVZkFDcjB3OEVsY1U2ME8vS2dTQk5yRmhxamFVVGtWREtrVXFOV3FPTUkyQTJWNC9MdU9waTZGdGVGL3hWNS9BUm02VDkxZEZGbTFYaFVpeWVNaG9WV01yRHhUbHdGZ1FJU3FWMXcwTzRTaUhyem11TVAxdWVTT0VZVTRuSk04MGdPZVd1QlV2VmFFRnF3MmViclpHNXhYM1pmTW5JN01KcENJSmtzZTJZOEJtTHlYbHE4bzRiSU9BNTd1MWUrV21nRzNHWURVTzFwTCtPcXBIekdIc2NESlVseUNyVG9XYlpJV3ZiTnpDbnRTcVRTLzJlMEFUd2sxZGFCN01DVGRGM2RYa21mZ3cwREhsT1VhN2ZvZ2FsZ29KVCtJb1B1VnFkaHpqUWdEY0cra3ladjQrQVA4SkkzQjRQbC9YazdzNmhZdlZBM2NBU2d5L0huejA1UndUWUgzaEk3MThSeUg1VFZuYnZlZ1VSa1hVcEZ6bDU4MFBtR0pyUWhoRnJudkNFRTYvVlZobTNvMFc5UkJvNHRUL2g1eVZaRUZ5NWtqS0U4V1RQbnhubW1wVmRjcDg3VEEzMktuY1pVVlA5WUl1WTBoVkdtdmJpRzg5cGp0R3NKQXdpNVgvWnpyekNpQ3NVbXRXeFdUSWVIbDFidVUyMUp4MDcyQ2dZeG91YVRrNkpHRDV2Q01obEhldDJtdEUyODFBYk1IWXFxM2UyRkpZVWdsVVl0TDFwb1R1VTJDaVhreHhSM0t6TkVyY3RVaG56MEtrSEhETlU0dEpZM1RrQ0Nud2xqZEhsVVlpSnVJeTB1ZExsQ3VVS2ZKZXo0aCtqbVpCNnJ0Ty9qbjh5a2JyazJ5eDQ3Zll5ZzFNdTh4UXBtd0t1ck4zUFh3LzJ3ajZJU2lZWVlqRTFHVm0xSysxbWREejFsMm5GZWxwSW1mTG5NRnVjRjJmVFZMRmNUSUFxemFsOU5yVFFoc3REd0x2OXhXQVJNQ0JCcllodDBNMmdQNyt3NVNuVVN1RXFXamxIaklnSzl2OVk1NXcycWoxVW9uNVk4OTh6ZDZXNS9MQ1Jrc0xUMWVOdTBGRmF0bTFxTWl6bDRZaVlxMFQwSWpXcC9LOHFIZUR2dVM2OXFWUXhHUUx6WW94aW85WXl2MW9RdlcxTTZoYUN1bVkxU2htdjk4bERzeU5VMGVJTHhKb2Rwd0o4K1QxSWplQ0pUVlJic0hybFpUcTFlb0VzWTd4S0Q0NTRTcXVRc3hsbTROYndqZHltcGgzMy9PS2ZvL0xmcFVJa0ZCSTg1QzN6bDREcVNoTkxucm45YmJyaHh3Y2xnRjZTQmpTZG1vZTc0eFp2VnpleXI1WHRjdDBlZXh6akJVM0l6Q0dJUkFocnBYV3JyTTQvTHZseTJkbGkyY1ZWdXFpNnFMRk1WUjAwSTNIcjVJOS9mdTNnYXZvVUd5cjRYK0dIMEg1cXdSUUhxT1VEaXk1VnBuWDlxcDk2MnlsaTVLL2lSbEtINVFBVnk1UUVCd2hUdHhva1BiQWZoZXZtb3VQNThNbDhhdk5CSGhwTG1xY2czV2pKV3RxN0tEbHYydUVOc0pSRlljZnhIY3lBM2dyRlAwbVNyMitWVGVrV0VwblBPMTg4b1cvdTR5YzV4WHJOUk5RYnI5b1FxNTJTV2Q3bWRNaE1aM29uMTNzRXFWaW01U3FweitvNWdGcmUzN2YxazlYMTZhZjJMZnZySVRmZnN6ZjMrb21ESWtldlpQd0c5c2VFbUtYZnhGRXE5RkFSU2k0MjZLYmVzb3c5a0MxTDhUWTdSNWJNNFRUWFYrZHhIWEtXVlgzNGNBV2twQmlmVU96akFScmtEbmlVOGpXQ2VKd2VxV2VleC9mSU1ua2pORjNmSTBqTUc2b0JsdGcrK3dJbmNHWUhJalk2cS9HdWJqZzZEWjRCb2lYck1Bald4N0VsYm82bW0xSnVvL1BxMmRHSjRSUFhoUXRIdUFHdDY0c3lkeURkQ3c4bVNBS0Zpb3Vmdk8zZHF0ekUwWWJFRGtKK1VPRjYxZEttaFphZmgwbXhSekdWdnJsU2RuUnBMb1Y1TTluUVBKekg0djZqZ1RMcnJqaGJSVUpMZTBzQlgrb2FwY1RlYXl1ZVA0YnZHZTNTZHdvY3dTREExSWp2cEl5bUdaa2Urb3NMdmhqLy9JZjlKS2ZxSmxJRXNJRnE4MmlmRjE3ZjR4bk5MNjlKanpxcU4yaFZYeXlxVFRCeG95a2R5eGdmSDJsbkU2RXd6bkJiVHNZcWw2dmJrSEJuN0tCWU9IWWdzZVBydkozZHovMWVKd2JweVJ3bEdZcS84RFJKdEYvcTBlOHdCcVJOVkxvRzVWSi9ZMXE5bjB0anRwU053N0hqZkp3OCtNakdxd3N4OXlidlY3b1FRV0NZNk45Q0RpMjVNOHZENUZucjJpUjZGNTNMMi8wTVc5WWtZOUIxbENxaU1Dd2diSW8xZTZRVW5UZXlpRjlFTG1JZWRCUDdoK28xRFRxNjVNYXJqQVBkeTJKYnBKNjNTc2ozT3gzcjYvVmVBMk1nNnBmRDdXcG1VUzQzenl0YXc5cEJ1Z3dTZ3hyK203QXVkVElwZ2N1d2FLUFZKN1NEMFlpbHBpK0NZS0oxdVBKemZNN1ZRcXFYbmpKU2VNTmRQVVB6NWdCcC9pT0M0Kzl6WklaNmhMZUNWN1ZGS0Fjc0hrWk82NFh4Tm5LOFRPVTFJZStYRGtNK3JtVHNRcE9wYWMzY0ljL3pYSWYyRjRsQkZaTlB4dWUwMnQrSDMyZGIrVXkxblFJb00yU2E1TU1hM3pXdGNqZmRVakZHR3BoQ2U4ekxTS2RlU1l5Z1RUMkxrdmsxRHE0Y3FLeFFZekpIOUtVbUJJSzduaDY4YXJrZWVoYzBRRytZUlF1REo5UGQ2eWl5NDVpRTQyaFZvdjE2eU5rWThzUmQ0MHgxU1RvblhFV0MrRk9oR1NqVTlGRVNablRLeVdmU3doNEpTb3VodWVPMEV2enBRZFdFZGRTOUcwVFlyb09rUzMzTG53NHl3UG40Z200Y0RSTnJ3UmU0RWJRYTFwNUo3MFJNSnljWEY0V2Fwb0NCa3hESVdQcXJCbU9zRHZyUFN0QVdSREw4RjhuSXNUWjlPbUhwSmdPd1ZHTzRPclgyQ3Nla292dEpUSjN6RjlXSlV4dkVLZVlRQmhqZVdwdllrY3AxVUJVNVBEWTRrN3prYUdDT3VlT3VibDhucm03VGRkejF4ckZqRno5MmVqb0VDSStZMkpXSWxrdkNrVUROQ1BPVUJQODdWNCtrbzdIWi9ZTnFZZ1NoNWYwUEpkZEk4eGJrMDliSFFJbU5uR0oyZDJGSWQ1bTdaeno3dXJYSFpWbHdMSWMveFF2bHhQOFIzRDNWZnk1RGdUZXFSYXFrcnBvUlU0dkdldFNPWlhhaXl1N2RmYjMyVWJrcGJHVHRoMFA0S3ZXeVc2Znp2OG5ocEZUQ2NNaktlbUxrUmllMGxqNDMzYmxmbDhOK0RxY2lBNEVIT25hTS90Y0RQNEVSVlZ1WWhjOHlMQmV2cDdCR0tuU01pV0xZc0RacU8rUEMzT1ZFNndnSjFXWDBudW1OOERXOWwvQ3lPSEdhL3F0UlFVNTJYdW9ucWVlL1RSM2hkdXczSUNMVmRQdEtRNmlZV0dhUVNwSHY3N2podFNMa25QaGFxU2F0ank2MkJtUDhTclpQY2wxaVh5dmNONUtFd1pid1g4clhZeGtJY0ZlSDVVK0JHaVRzR3g1OWhOY0R0a3JaREg0eEo4Qk5RR210MXZZR05rbmY3ZU5MS0xWWXBlaWExMSszUXpQVzJxSjZOQ3Y0d1FCdEExWFdQa1RwQVVWbjhoK3dwUExlcnh2OWVmN08rNFBveW9JRk9BN1RCUkNRTzViYU9TdXN2eTlOaVlVOVA1ajhLZzNWLytnUlpzT2pxMVRzbUYwdUxNSjhKai9EdUFiN2tBVmxZeUZXTit1cjVicE5RVG5ZV1dLL1dpMWNSdFRDU0ZRNm9vOElDYnFDYmZhSDZVTkU3WlFYYzFrUUJTVTRycmNUTHRFc0NIcUZRT0NEME5LbC9GdXE4OXF0SDBWMTJvM2ZJT0RYUWppR2Vrck9VdXpkVURra0ZVRkwxUCszeVc1N0tzQ2FMaVFQQW5JdnVENi9CVm40elVyRFdSSDJJbEM3YXdSUlJBQ2M1Y21rL3lienRkZm84aWVpTXdYWUhHZC9ZSlpBZEJ4dWlJNnhlTWxjTzhhYWJEb2FwWFFIc3Rsc3VlY1FyV1o3c3IvT2JRMEJ3OG5keTlya0d1VlIxWXlxR1J0MGk0Q2RZZ2xMdzNwalhEWDZLUXpKUCtwcUJwemhNK09MdTBaOEMvR0xpQzZkSWJ1MDZnQlhIcFlueFVZbnFMd2liR25rVkJ1L2puVWFNckNtd0c1RmlDM2t0UHJOTXRSOVZmWXpkV01IYktoMDhQUjRYYUhCRG9Qd0N0eUsxQ1cxQkR3YVZjTlpYTmo5Y0VhTGhXbVZ6WVFmZ245TVNSWGRNVHY1YUErcEQwUGF4cGkzQkZuKzAwQUpOZ2hKYXI5QUJwd3J1MC9HeG9JWjRMVW1oWWJzZUNWOUdwL1FIKzV5c25NTHNFeEFWYjM4aUF0T2NjcU5td09yOTFCblFET3NJdFZqSmhyYkZLSjcrRm56Q2h1NGlKOEtNeks0TlliRGJMaVBpMG8rc2dpdzlMNzNYM0RCSVN3MGNqaXRuRWNCdDlEdjZZcFVmNkRvQThtangxU05MTDN6d2pBbitJTVhQWFFqRTkrcVFLNkpyUEJpTk1qNnZXYUdjakNGRzJMV09XdFFrZDR2dkhoNUhpZVVqVGtxZ2FlS1J5eDhURzNtd3pSMnN2ZytMK0pVaUFXRVJyTWFlOGJ3dmtkYzd3RFdBS3VZNFBRRkNaVmhSOTNGOUtjQzc2RkVZaWZyN1F6YW4rMk0vT1Q1dEVpLzFPZE82cUxKVGhMNmh2YWt0eGNJTk16OTYwdFdjZGdxNGZRK3hmQmdxOHJDRjFBY0ZZSDZCbFowWjlpK0J0K3ROQktFWWhnb1ZQV1ZuajFLaWJCZHdmZkkydjBuRGRmaFg3dW45SXFyUG5ueDE0MzZneWQ3UHE2QU5VWHoxL0NybFdrNmdOeHB4ckZZMzJPT25VSk5UMDJOSWRFVmpHbmNtbFlvYzE2bE9oczZKdkVvT3RlQVNYaVNmQkgvUWxMZExITUF4eWdNcHp6SndPKzkvS1hHSXp3TGdGSThTdlhBZXF5Rk5kZ2VxWmhnZmFiL3cxbDVwRERMY1E5dkFlUEZOeG9YRDNNU2ZWUy9IWURQa2JFV1VNTmsxTjZWOXVnc3kya3MwNjRxcS8zaFAxSzVnYkZyR25rM25RT3R4Qnc3MWtsTWhJcmNXdXdHbWwyVElUdWJpNDBFVERWNDBiRDZsVWpYTjJLU0k3Y2ZNS0hNWEtYWUtWT2dJZU1SYVRHTHg5RHM4cXQ0dVhucVNwYy9RS204cUY1VjF1TDFNNFBBWjZvVW42TVdKTUpZUEhPaUdieWtJbnptY2dLUkUrSWp0OWJ3cHpmQnh6MkdGVzF6RW4zM3NFT3BQV1RCNHdadm9kT0pwYStmekxoR01aYUZJNTNZR2hweGpTajZIMDZ2cExCbHU3U2ErSVp5U2NuYU1RWHF4bENrdUxYODdIbldnd3RkTDF4R3d3RG9BRE9NRUFPZU5oc0VlTzFFaCttcUVMVVNDbUdMWkFOUjJwNCswdTM4bUszaTNwTmVLVzhlMUdqbUhDMmpOODU1cElXQ2FIcGo0N1JBWjV0SUgzZ3NrUTJ2VmlCeTdYampMRmRKU1N1VEFmVG51aXJ2RnJrNWpWVXA5RUl4RjZrNXo0TWdwWEZYY0NxY0Rubm9IdHpnckRBS0pTeGoyMVppSFVxRHBWTmV5eFAwNFUwRUV5VFV1Ym5IR3hGejVsUndCL0JzeVpDMTN0eSs4MW9HYlBtNlc4c0RUVG91azBUUG9yTWVqYTZDaUVLNUlCaDllUEpvbzVLY2tua2p3ZUI1dStOa3pja1A2cHczWXI4eC9xc0QzQTNicXYxa05qSkdGeVJOL0JwWmNtK0VOclZyZEIrb21hYjdKZndmOXpBUFhWRTZ0UnIrSUhrcmRCUXVienlab3dzVTR3ZGFlTnpDb05BYzBvQ092bEdUOEN3UzNoaU9rNkJNekVMVUk0bFNIanE5VEhQSDB6WERHdmdJdGlrdXo5WHQyR3FxVWtoelNRQ1JOaitGREllbVlOYnZDcklJSW5KYWVoQktSWXcrdFQ4WiswejhJNjhCNHdXMnNvb0VSVTNYWlVndFJBa0pUS2tHb2NKUytuOENYVFVFNng4WFdmVGZLS1R1NEFwTHRtM2N6cDNZdlVZRGc4VjAxWHhiUTRxNzZudFRiR2VoSEIzcnZEdzYxcXVzVVV2T1dIbmZ3Wkl3aUtaQkpxb05jdXdSY29LTG5rTmxGcXhCYkJtdGZtbVpVcjdvZW8vM2RuZDZNd1hIUWswYkdhN0FjUEJrR1lnbkhQMlVaWkd6QVh5eE1uMS9FeTMzbjEyTGlXbVg0SXRWMU0yU1Y0QlRERkpieCs1eG5mN0VvdFpoWW80U1hTNFJHN00wemFsbzh0QzFMMTVEenFheCtxOGtoQWQwQk1ldnZhUWIxQStTWXE3ZXREZ0R6bXgyekdOVHJGMzVkVy9kNVFsTGZnV3kwQmZwMnppdVp5OVJ4dnRBamhMSkFKUEZOWXQ2ZVBZRmVvbEpiUW1pdWpCZmNRUDdLSHNZWHFFdVBRU00vU2I0UEJBRUh4Z0xlaXE1SE50T3RFNCs4dFdaSFJ5aFFtbEFrb2ErQkxKbW9FSFZRSUc4TGF2ZC92NVEvQWViVktrZFo5WnpGb21tbXoxMVNaY3VVc2FPakxDVEhwTWp4NytRazVMZitkeUVkN2tNOGprclJ0SjZkdDBvL3ZrQ2hCNm9EclZTeUNWdk9vaDZJeDRLVEphbUM4K3BuaUtSdzZab0prWWNhZlFmdndlaTA5UHJ3YWlBSGRLeUQ0czdGY1RTdjh2MWFlQjRMc3VJRXo4RnpBaEZLRC9FbzZBOTZSTFowRHgwbmF5ME5Lam5pSVVtUEpGaFhFVE1yaGtPaFVDZGFsMWkzRnM2dDhQcy9lcVhNN3Z1V01zYlowM2s2Ny9tVzhZR0czTmZ3ekxLMHB5UVR3SUdiV1VwU2FYQWh2UDdEa1JueE5JVDVTSUhSNCtvOVVsNkxPV2tiUDJXVlJGcDZ2L0pvZmttU0hiN2F2ZjJJSWNzMTIvQjl4Ym0rd3FOUFlubEorWURJT2JVbXo3NjluaWtYMVorSThSSVdiYXRoMnhVTGg3aHp5YmJnNzRJTDQvbGdNVmxQVUdCcW1UMmExS2JpVjM3dk1BN1R6SXNYZ2NDVVJaSThlazhHbXpCYTgyRDVTVmRZeEI5dDNmT0I5bld2MGo0bGE4YjhORXQxdFhCM3Z1R2hQaVgzWGFsU3BpcEh6YUs5ZUtEUFFxc0o5Q3FoMmhtS1k3QmprVTVScG94cHpQZHNCbU5ab243bVZPWkJhN2srVzRwaUhQUjUyTFJKME0rOTdoMGkza2xnZnVjZWRvOXNicUFabzB4Qmh6bFZlR1QrenlCRkhLTk5PZXJFVzlEYUczbmdLdFNvdTJGKzJZa0xNNWloZ2tyUTJPc3BZa0tZNUdnb1BVZGtwdFkxMGVhMjZyVlR0Z1pZYUJFYVRDZDZneDNQclFGOTd3TG5tWFRCWGxGL2pTVWlwWkt2NU9CWTdNLzZsUXJyVURKTWh5QmtlcytYend4bGJLbDIrVktpZDZLZUo4Rm8yUlFlUVlUbkRkc0JWRVY1RmdKRjlrZWtaS1k2djZ0bjg4UTNiektjOHp2MUlwOXpzSnZMN2FPdXViNER2dU1uSXpNM2NjNFlhV0MrNzlPdUdTdS9NNzRYTXowM1laOTdiT3BsYWZXQ1V6cXBEWXlxV0s1ei8zYW5NcHhlNWc0NWlCbWhYWFkvUkJKV3ExaHpwL2dGdDYxSW9neUhLeUhWWlJqekZhZjgxQ09wZlp3bmtvdktFRE91MzlRSGhXOVoveVF5SHlTY1JRYUpkbHZZcUFsNXRUdXRYSGZZK1QwcnlCWUZBdU9rMGNobFhEVHBFZ0ZjbDlCUFdaUFBleGNxdHlKSW9ybHhhT0Y5a1FQanJDU08xODU2eVJ5Y0NETzhSTGl2a0dVQjRZYVBCZ2Q4R1ZyL0RoUXZxcjZtTDBYODNKODZldjdDUHkzS2hnR2JjQlV3VDczald3Wk1qalE0MmZpcWM3QXFON0NETDRzMU4zUHhzRDNyUklQbVdvUFdqODFtQlJHYmtlekRrNS9aUzk2bGpvbm9MN1E3WFF5OHY5d1VsSWkyaGhNNW5jVElJN1NMQTRmZ0IrMlNmWDlsdkI2cXdvY0JxMGx2OTZITFJhWERIN1IxYzYvOW1RdU84K0FIYU9rWk9QcHRtbC8zZGMyOThTSHNhSVBFd1dYRTVOU202K29TajdwYzF1eVRXRVhsZGlqYVZsSTA2dys0cWVnbmJybE9iOFZyMm5Cdkl1RXU0djYwbEZ3bUd2YzQ1WHlycHNuV2xGL1JrcWdaK05rMGZGZE42RHp3Qk1uM1h1S294ejROWFJhamwxbHhSNHpuVkd3MU1WdkxGa01XU0FIL2FWWFN5dmZ4VEw2YjJTR0pvc2JrUHRsdzhOVWpzdXJVNEFraXErWGNxaHJoOXY0STR4emh6RG1KWkVxN1NhWUJ5NmlIeFNBYmZXZ2FkRmtXS3ZIWG5ZaGxzMmFxTEZMVkRNNWRyTWZsM093MW5NYjFGN2VqTmdTY2pxdFNTZVExSlk3cDJoMlR3YXFBNmEvMEVIckIvUGxkZ1ZZZ2ZIeTFpNldYMUVSV080RmpFOVFLcUpVeG8ydHFXekZoT01MNnh2OVpFOVFFaUU0dURWUVdrWi9KcVVnRWsva0YwV3hLTExVUkFWOFcxekJXSTd2VlYvdThzd0J0UU8rZENTejhObytQVXNERklhTFlrSEhQd3A4TlZxd25DQTFPcWxaYTR5Y1ZwbkF1MXNUV2lMNWxNNnZiM3VkQmVqaHQ3SGkwZ0I2UGhqdDJyb2huYUdYcWRpQndmK2dCMW5PeU1rRStSbWZhYzY4T2lGemdyeG5DZ09SUVpCRkVIdFN6d0VVWW5lMDJyZ2wweCt3VndaZUcvRGE0WC9SZWJBSzBCU21jOG1ZWUxIN2lUYzAxWjh2ZjQvRFc5MUs4c3EzTkNlNG9SSHVuUHZJcUwwSGE5NllrNVkrS3VhT1hMek5GLzNJcXFsTWFxSG9MOWh1Y2R6Q2JDZC9lUnJFZDhycGZ6S0N3cml4dytKd3FndzJKNURoK1MvNVBzU0RHekl1WkZBbURSbW00S2ZjVXVxQTlpUVF0M2dxZ2pwTFd3OGRxS0xOTjZoR244cmN6N2NHTndrd2U3UmdYUmtKQWhZWHI4ZVlkSnJsSVpLWExFWTVXMGMzc0E2ekpxWENrbW5GcTAyVk1yRVkzZVhkcFdHLzJVQndtVUVYNlh5SjVtWnVOM0lmZ0o1eEZRN2FneXBmYkRDNHhrUlpqK1ROS3N3Z29LSURsN1l1NFE2VWc5aGVBS1NiclZLMzU4QXlKTnloZDlGN2M3YTBKL1RzRnNobXdoai9hekkrV3FXM1FmeG5Gd3BQWjg1clVPM1JpeXBkNjl4RDdHbHhrUVVzdWRLZ3MrQkExVmtxYlY4WW5NbkJnUkRtT2w3eWN4U1dSTWdvT28wYmV3RG1pM1ZMaS8xZG14TkxURnB1cTF2ZHNvRys5aHhSUTdvQVk4bDlkaktYN2VsRjZHdGRZSUdya2tvUGlaRkpPTFlRb29EaEk3dk1PTllyT2tIOWZqd0VXY1RKaWg4QjJqUVk5YXFIRm4vNWx5aVpPektNaFZaWFFtZ1U2VFV2bzVWa1Zac2NzMTlLcmEwR1JhalVkVlgxOCtOZjlpVVZqNXpxUmFENjhKZW5ESndMb1l3MHVtRXBRdTd1NWppV2x2enFZZ0F6WWx3elhXZXZONUIxZEJEUDZkY0NGeWRlUGNvNjdxYmhqTU9NdDdrSWdlZlIxMVROZHFNaFJyT3dXYkZISGZEVVNXNmE2KytZamdMWjlGWGVBMzZTWFNPaGhoNjcycGg0VWFNQzZXZlpJcHRHTkxzR1F6Z0dDVXp5N3JZSXU1T2JqcjMrcUhQTHFxTGsxb0hHZFJZbVdMdjF2RmNpTXY5Tnc0VjdIZHJwaitRK1QvaEpvd1VLTXFHK2U3c3JSOXRRMXZ1QThPUm9GZ3BrV0I4N0J0NHlxVmh5NU9GQ2gyNXlGRFVxZVVnY01XaE1BYXZVcWFuRWFNdW83VjNwUnY5L0pPNjBMS3U3M05LUllEZ3E5Qm1iTkh1dEZqRUtiNDVBdmV2Z1FTazNHUXpIdUVWSW1rZEt3Uk0yMFhuc1dKQVgrdzNtakN6RVk5eHl1NTlzVHNWc00zMTZoVU40ZE5FNVNUWjF1d21neFVWazI5TkFQZzc5Z1FrWXVSSGt1M3hJWGFBVTZSVDROYnFIYy9rQ3BGTjhYQk9YazAzdkc3S2plZ2ozYnNTb2RZZTVMbHZPem42d2phSFBYdmNaNFNxaW5rcGN6dHV0a1lHSnlESmpKZVZDYUcrZzhRZWkrb0MzTWt6cm5VU1R2RTBzcGtlQVlnbnI4UkJLdDIvaDh5a2dQODFiOHY2Nko3elFPRzFIUHFkVHJjaHJTNEtlakhjYVVFMzBFQWt5eTVlSkMvMGp6N0Vkc1pseHJNSWhDNURDSE95MmRrdHRGakNIaEJ6cytqNmhDRFlhMnNWVjBUMFhFUEw3K1JWRjJDOVFEalFLZElBV1J2bjRhcjNYQUNuU1JZYVkzL0Urd3duTHNKWlo4ZENWaTBUOFVZdEpvTTBDK0pvVld0TmljdVNJWEZOeWxoREVYY05oSC9MOFJBOWN4TXBRa2lzbHIwT3ZEWVMrQmlYMWRXM21TcjVHNjhTN25aa05MR1Y5RElWNHhYZ0lkMFFybUxRLzJxeWhtVXFlQkZOek9TK0VWR0lOeVFzU3lYVmUvSWd3L016NFdvTkdxS0IrQ3FyOTA0UWRROU0yVHZDVVBmaFRQTmF0bHczVkFGWnBjcU0xaVJkTDJRMXkvSlRDMTJOUlpkVE9ncWsvTG9aUUljaldZT1RzQTltdG1udGlqQ3dld3NTUFJzVWJuUzVYdUJpT1FSeGZaQWdXY0ZnQ0VLWit0Y2dnK1B4NXpkMlFra3ZxWnVobmZjRHVHSWFqdEN3RlVscHY1cDJjUmtKQXRMUGtTR09VWnl4OG1BU2I5YnREdDExMFF0WStFMXJNZzFMeVpya2g1YWdzMjNFVWVHU0ZSd1huK3dpTHFxcTU3aHk3NUtsMVhQS0hMNWprU0s1S2JBbTIvSWloSnd0dW5lSVZ5TkxDRE1MQzArM2tmZi9LSlVNcTV1eFFjdXIzbC91eWJuOGwveVc5YXB0OHk4WjJRcTBNVlBPMW40NW5BTk4wL0RjUDBUQm5UdFhjVEJxQlUwTGIwOWQ1WngzR3A4MmdmbW5tWHpwRVpQRWdNZDJTcEY2WFlsNXp2S0pGNU5qVi9XbTBjY1JCellaMWtDdjRVSjZpeW93dDA5UG13RnhKcG45QS82aWdiaTJzcDNYQ2dCajJ2aTVEeU51YnhIMk1Yb0VkU2JQN2xKSmxFNlBUd3F2VnBrT3d3c1c3a1VnVUtYVUx4NjJISFE3N0RxOXJmOFlzN0FUSnNVcWZZYXFRam9xaU5BdlhCSnJ1ekx6S0RTbU9wanV3aDVDQjZOWU1FenJiUkFWZTl0cksvSXdlOTJ6MmgzQXhsSFJRWVZ2RTRJT2cvbVBDaEMzNDVnTDVsb2JmaVpiVmxraFozcDdVMnZJaFpKMm9NRGJxbzBzT1U2cjI4bjNkdFZ3ZTlUZEFrZ1VISWNuNEFZejEwdXZZZGh4NGZZVS90UmpmNjc3NzVDZ0t2dDdwVnE5aGl0cXFKYmc4NEUzdWJBemd2OTVXZHUydUd3eEJRT3RpLzdaVTVqajlZNHlldnVWWitKWWRGUWYzZHREdlNSam5kYjduSUt4NlU2K1FacGZFU1QyUWhOVXhPakIrekYvSENLeS93dVFLNTgvdHRsbjVGNmVsSGRGU3p1clNWL2VqWUxZNWFjenJZNkc0cXVnT3dTOWlTVHFab0M3WFhVUTFtWWQrT2d0bmdaTmtGQUsxNUhYR1oyckJNNTBHdy8ySkN4dUt6cTdNK0FCNFdaM0plQy9vMy9SWlNsUzZOQU9VektQVmhkQmhJMjgwS09Xb0VpNU83dUZWb04vSEJhVk02dlNWQTgyTDJPWmpNOTdTZC9RVllIUmFMRkVkUnZkU005dEV2MS8rRWtMRnA5UjRKU1Uvb1d1SVF3YUFlRytGelMyd0N3dU9vSVZKamFhUU1aUGtuWmFGVDBoYWVVSWgxS3B1WXZ5YjVUaE92YWpSWG5pS2dRS2cxWXRHcE4vQWVRSGo5UTc3NXdFd2NDR0ZrUkFMT1Nta2hnMjBtV0JLZTZjZUJHRFh0TmlaakE5NmZ3WVkxQ3k5WEJGOXRoMHdoQldzaGNiYUZaY0xsdmlGZjA4Mkx1TVVud0hxZHdJUGtBS2NkWVdYdEpzMjFmQWFKOTBjaWgvQ3JwaXpUdkMxQlZJWkdKbzNDWjY4UEpTSWpIL1FrWW4vUklIYzhkL3NTMVNvbzVKVCtkb3ZqdXV1ZzFiSWpKTzh0N1lidUVBanowVFl1V1FZelRPV2VhZmZJSHYxc1JySGNBSENWYkQ1bjl5a0dGTHEyZnFCQ3RIckl0cHBmaGUzNFVrLy9BRFZ0Y0ZTSU1jMlh4amQzUnVCTzdTc0xTYnpZeXFwOEFNTzc5RFZZWnlEOHZhQ0tOdEYwcWZYdGhyNWp4NFZ1aEt1VGI0YlhKNUR3K3BCajBmV2l6ejlvUlhZYnp5bjc2VElaaEtBSmFtOGVsWE5qWnY5UU5hMXdydlM1cWZZQXZjQ3pEbEdMZFp0ODhrZXRiUnlWcmxpRWJoU1BvNGZDUkNmNUxONGQ2Y09vZGM2a3Q1L3JORElZV0Q4eE5FQnhOQ2wzbU9hZ3FKYU1TY3lrOXIyTTN1ZWdHRjJwU3htSWRtcEtYb0NZdE81NjdBc3ViTkEvYS80dklzQWJOSFIvRWxwTDUxMFJDbVp6N0RaV3ZhWnZTYUpreGdjRXcyb2tIcWV5NU9Xanh3RG0yd1cxNW80NGFpeTJEaUpUbjJETmpkdUx6Q3lHNHI0bXF0SXAwZ1E2L2lrYndMVnVMUHlzTzFUYjYxekRyNTlnQUovK2R0am9oMFhsUDNtQkNvZHAycGlJWTVpWUQzWEZLV3Q3T0YvS3VnYWVhdDAzYTltYlg3TitNZjRxZEV2Rlg3Tlg3UkpMcWp5SFltWHA5cll3Y1dkdUVLQyt2M3prS1pnWGFqVkFqZXVEenFMSTNFOHdSMmZIczByZ01ydkkxUWR3M0dvWjRlN0l4SUZxdWVGSDFSTWVuMStEYlpsZzgyR3puWnMwL21zZnVQYm9KSVRJZUhnbWduUkZOOStWdjFRRUdRdURhbEpab0xXWlFMVytlbTBvNXdWN0Z1N0g3a0ladktmVUxkeXVvUDh3RktEejE2QnZOWkEvZG9oa2xXNHBRRlFHRlhSTkJXWUZtb0NyVGNKS3lkUkYwYVVPSWRVelBuUS9HanlEbEZ3cGNvOTc5TEdYdWU3NTBENWxhaDl6OTU3K04rcVdlZjgzUGRhSGZ1aTFZTXg2OXN6N2J4bFdDVmc3VjU1SVovbUdrV0hkWmhsNU9MMmZBVHRLMGpaamZyQVNKTUlKb1ZBdTlVeWJQZWtmNXg4ZXdrMW93azVXVGJhbkR2V044Q1FnVnFFRjZXblZ2dURMaThEazIwdkhCeXFGQ0VZeHNpZHo2Uk9EKzBPdVFVaDJHbjNNbWoreGhuZTcydGpFU1FvNitzcGFhTitydTRQWkp6V09WbG5wNXRYSnM4dmpzVUdETzFaL1lUZzkvcTFLaDlaSkphUEM2REdEbE9rbEo3RjdGWUhDOWpUTFpMSGthMUtRTms5TmpxZlBablZOUG56cDlyYkM5TTFFNEFzTW9CYldEWGw0bS9aN095bmFkWW9HWTNibGVpNFpWWm56RWZVcThEMWY5TUpZRVRtV3AxWFp1YmZjTFV0L2Y0VUh6dDMrMFFOaVFSTnk1Vmw1bEVoQXpZVWJBSGkzMFZXOER2S3g3dlU1ejJmRTBKSU9UdjZlZXJHek1vK3Z6WnFaUjlXMG5jbytEMmJ2RWxoMVVSbmt3cjVMbFp3T0JqMUQ3RncvRlRwNFFyR0ErdHFMYmcwY0JVeHlPSS9tYWQ0ZkpqT0R4Ni8xV3hVR1JZQUtYcENBSFlDZTBvaEFWREIrQm11QlRoSEF6eFdqZ3JGS1pCb1l3Yk9udUkzY1BrMXZ1RHRYQlVZV3BjMmFtMmwyVEVvcTlva0lXekMyM1NZZzA4YkN5ZG5iZ0NWcWMrdTVPTU1NTjZWVGJiYkFRNnBIclpJMExCeVNpNmFrVmVCUUE0NFZpSXpFOFFwNzk2OUlDR1hUdUs4SmNPN2F2RGlCMmJtRGFJa2tvRWJUbk9vOEYwcUN5dlZ4bUN3UGxpR2Z1WHlqYjUrM0VXY0xJWDZiR1RodjRKZ0lMSHhOVmEzQWozTDVLY0FYcWpubHVVSDBhZlprbXJPbHlyN3NWTktINWxFUkNjVmFWSERRQXFyQVRuUjBmR1dRTDRsSk9scWJMc0lTY2hoK3liNEFlclVvT08zYnYxR213eHIxc01qTFdzVWtNNXdLTklZRE4yUVdBcDRlT05SZUhqMzhuL1VvZEF5V0hjR0JnQms5WkdJdkd0VmlRN0tHZ0Z1ZWFVWW5MT2JsMFZUbEt5NUM5OVRZNlM0VFI3U2NCdkpxYmhRdW1EQXQwRGRZRUJKY1RpUnpGTXlLQ1hEeHgwTEgyRnFpRE1uSk9VT3NmeDZqWG9iUWhSZFZPLzR5aWx1MWx2c1ducEUvdnpNWXRmcS90RVFlR1Rsb2w0b1puVnJQQ2VROVdiUXVoOHZTeDB6L1FJdktJakVuMElNYmhWcDlldDFWd2h0amNmZ0NxOGs2RzNmY2JVMzhmalpFS09hNlRUam82dXNpc3RCUktJRHkxODg4Um1ld3VMNTJWZUQxSWFjRWIzbnJmSGloYjJJYlpITHNjamZ4Zjl4UVg1LzlZY3llRFA3Q0dQTWlCemViN2FwV3hYUnJsbWNNQjdmcWUxY3lUTTRaSmJYeXlmRVNkcm5zUjM1cXM0aGtoZGJUZk9KM2s1bm93cDF3VFEyZUxobDFWNUdGK1BUVHN4K0ZJUWo2NlJ4UVV3TXJ0VzcvY1VNcEwxVm5zUDJ5SXhiWkR1ejAyc1VNQjRBYlBkZHR4NnlLL3pYR1ZPeE5iVDZsS3BFaHFacSt3Y2hvaU1ZbDBGZDFFSUZhVm0zTmdXUURQRVh5clA0ZWQ1dzRqMy82OUtGYjEvQTh1YnhHci8wVzdEZzRjMjN5MkozSXIrOVVmeithRU5SUit3NGkwTnNzMy9rZGtHOUFBZkJxMW1GcnBBUjlsL1pQQmk3VWhxYlBkUEJNODE1WmYvaDNZUWlrdE81US8yRWw1Q3ViSkorMjZhSzltblhFMEJydlNHcEtwVW1pNWhEN0R5YXRwbnphZ09uc0orbXBSMUN4L29SZnpyNTJvbUFZN05MT1U5ZGlVTTN2T2FUb1lQVWp1UjJxb0Ezd1c4ZkRSVjVndTdtNUtrd2phUEJ3OUJvejY4TlpwZ213TzkyQk5GcGR0em82YkZJSlR0cWt2SkczblBHV2NEMklMQ2dCeDllYmkyemJGcUhJR3RWK1RqUEwydDZpLzNBSWJ4Y2Q3b2g1aW1vNjA5eVBRMTczK2g5dzJ3Q3poYll3R3ZqVDJwQTBSVzRrQXk1TmVHeTFZNllnNHgrMjdndU9oNXMzM2poMHBwcHBtYVowRXhvYSsyNWM5S3JZc29oaTYzSHdmVFhaNHVaMG1BazRqOHhJQ1RJeVcyR09QMXNrVzlWSU0wdkZoK3pmSVV3QmtONVdOQjh3dlozczJOZHFpb2VZbFlNc05keXYyaHQ4ZVZUZHhJZWFzYTM4ejlpOFhHZW1vTjloSXR1WTU0WU5BQW51R3B3T3VyLzBHeXBlUm9VcDlKVHNJNXpEaTQvaHZYenlkaUpRYjRxMlRmWW5ZWUI0YWFrZFVqZ1EwQjhtVFN6eVh5UEo5UEl6MENSOFBjZjF3V3ZmeEMxM2NNQU5sU0lUU0s3TzFOMHNHbUNqUWJseWRpVVVqZmZuNzlRZDBnQXdJZ0dQUkFLaFJURG5zOXQ4ZVVBa2hPWXpLQXhaSkYxSTF6TXFFRUJyZEhKZEhlUm5uQWVCODRha2RsTTU3K0NFU3ZDczB0aVlKL0FSdmZGdUJwc3UyZEJyTTBabXhvaS9RU1BjbThxVy9YT3RsVFYyUjFxV3MwVkZROU5aOEdtMy9Yc2ViK2YyZkF5ZTZNeDFVTzIxTEdRWFhLMWYxZFpFUTlCcnUrcDMrb0tPUzMvemFhZjE2U28yd0hNczdOeVI0UUhJYVVFdVhXN2EyM0c4Y3RZdjJRL0prb2sxdFNZK2NVbDgrT29ZZkg2WDNmNmFQb2NJcGw3VHE5MWFnd1o5T0RORkV2WU5sWTJGU2tzTW8yb1oxNnNPSXQ5aDNaRUorQ3o4dXJmWi9lQUlkQ2RLUnl6M1BxWXk0d3UreXR3QU1qWk9wRndMSklwR3FEcTRvQTVhYUVsWmVhclVhRVprVnZGa3VzK1lxYnB4L0lJajBIelRRM1owaWZlcXRCeDE3Z0pSbEtsbTZKWU85YlJ3NUdPTG5ISkY1VkQybFpPZFRGOHplM3d2L01wRXdpb1BrczNtcUo1ZnRuaCt2aktVWXlOSVIyaGdHdFBHV1JkaUxMRzFTbUJJUjZFbkZxMGVBU1FPeUhXbmp4YjFoRWowS05HUTcreWNnK3Y4VkxZdUpCU2Rsc1JsN25wcW1HcEd4SmRQVjFzZ2pYb0pIOEUyakU0NlFqTVNwL2l4ZklEN3NtZGhPRjVQUUNnTDdJdnREZmN0RUtqalY4UzlPV0U2VXVnMWE2aXlOTnZvbENFYWtOL1pBWHVNWUR0eDlDSWJDNkJtbmwzUVd1VDd6ZUJabDVKa1dGS1NhNDg1d082elpMN2hBRDYrc2lPYTAvWjNqMldiZ2dETjFsT3lRQjkvZVVsRmFvcEpvYXNudXBUdE1QOGdKUWNOZENWVXliMVJxbUpSRE1sM0lHZE0xdk1sam42eU5mODJ4cWhzdHRFQUhGSE5XNk1Eci9CYTllbDFOZDE2WXVQU3FDcXhjMWdyUGp4SUxLdTVxMkM4TWJRdzF2cFd1eDBFMlJiNThLOGVYVGxUUjEyaVJPNkI1cUFLTDlvUVRsQXE0UksyVXFpRmtGU1V0azhlUGovZVpYczF5ZkxSbUpzMUl0akZRbnAvQ2NzQzRVY0I5NGg5MVBIeFdXTEVCbHFZNXkyZ0N1YzFDK1dDQ0M2ZDc0RXFEUzc3TDlpdnRDc0hRY1JENVI1bVlJTWhXWTdqS3dUS3FFTTh5UXJseGo0VFhSODVCN1Z5REpRTm9DdkFNQ3BaTW9jUzZOODV5c3Q1OEdtS2N0ZG5qOUhlSTRHOFZvTm1oZXEzc3U1aTluRFd6VTVnVkhmdjlScjFIQWVVUU9sTGgxYzljOUh1c2pDaUtUcS9aUkNkd2V0K0VHWFl1eVJMYmdsYWxmUEQ1aVpTc1dCbE81WFJvNXZabGpWdHNOdGsva01pem1hRHAwa3ZReEwyN3BFcHNjTUY0RERoRFk3cmtZSTRVMk0vMmZFTjlCdmZxY1ZZZlc5aWR6cDZ3NzI4RkFWYXVLTHM0VGV1NWdLNExLbjE1Sk51OTlRN2pMWE9rT3hKdS9uc0ZEei9Nd2NpYUJhb3FHK3l0bnVPbUxDRFlwNFdYRTc1Z2FHQTkwZzBBQjRtNGh6OEJDeGU3NjljblR4bTkzVlkxWWZaSVFNeGJiZElCeXp2ay9mSVE1QThuenl0aEpka2ZINXNMM01GRnVUUUlzMjlObkdreVFHei9IVDh4NXR0WFBnRDJmbFVXR2IyaXVxaC8zajBydjVrTkpxRVJVZXArMmZUTWVScTZGMWFWSmgyZ2c5d2xTYmhhUUNXVGVvQmhVRFhQeVVtTmVmTTVQQk1sMXAwUFFWL3JEcEpxeThnNWEyaDJvb0xBaW9iN1V4OEpKU25nTTlibE9uWW41cTRjQ09OZTM2a2tUTklZSGNjaWdVdnplVTU3TXcrMHprM2R1QmFNRzBMZzMxM2UweE4wVFVoN2NaamM5TnhkemgwRm9uWUF5UUgva2YrNWNGY1I0SGdGd0Z1azVYQm5mNVVKRHI2dW1STVF2NVZvUnVDNHJ5SVYyay9xdkRHbkVrc2NVLzFudUE5aWdpZEJWSDZWZkhzMXJmN0h5TTFPc1NYSzZIemNLQlNPK3NWSHU4R1UvSEJnaStLTmp4L0I2UkxtdWZZTExvcHB3d05kT25TZGh5MmpONDhIcjFLd3hla2N3Z0wxUzB6RzRIS25tMGFadHBEKzVXQi9QTzIvVnUvNk90SzcwclkySCtUTElXV1BreTF4MVdGOVN5T1lzeWJ4a2FBeFNiZFZLcEZrQ3UwYWtIUllYR1pHQTJHSTNyamp4cEdCc0tWVEw4Zmt1ZWVkWjBHWTVyaFp3VXVhYWU3MHhDQ3FmN1k3YWxFSFhEeSs0Qnp1R2hTbHR6eGFpZFBXUnd3VjJNM3hxMWtKWlRmWFZEbHJnZkQ1eERhZSsrZmNiL0F0YVdHdURhT1pIR3JnQy93Y0tlUHJLSUZPeE9rSmhyVXN1aEVWNEpNK082RHkzc1dFcFdhRWdieS9IK2lsQzdtbmdtakxpd2JhZ1I0Y2tIYzltWFN6a21rajJVcm1jMUxuU01zTkNOem9aYTdHVEQyRC85SmJNMVdmQ0VXcTZrdDZTaGhwcmhpZnovTHhZcHE1VWRBa3I4cnRNZnlRWVFrcVF2MTlBZmJMdUliY3hna0lweHU4aG02ZC90T2RDSFpmb3pRRU14d0kyczkrS2VIS29rNEJoaHdMa0VwdU5uUGQwNmdiMUc5RjVuVUo5MStuYnRkczVVbENEQjF0RWU0TGU1VmpqTXdIS05waHQ1UHlEQ1VqNnB6YTY5ZzRKcjBBaURwanFkR3NpbFNiOE1FMll2bGdNZEwwV1BPZGcybGlVZTQ2YWQzR3BYVEIxTFVmQnYxU0llMklOOVNLQ1I3MTZVMzN1T3NPNDE0bGxVVGViK0xKQUthSUVFNUcxc2RPN0tJeHRIUEZUZ0VBL1BvUnAzSjRZUHRQSWhQaUpqOG1SVHB1a2czb2JSQ1gzbnFyRlo3bTBBcFhSTmZhZjY0ZVhQb29KblZCemlrYVVlMkttM1FWYVVmd0NMczVYWDl2Uk9yZVQrQ2hBTm9lQ1NnM0haaFN4TUFZTmVraFl5UGhlWW03c3J2NTlxR0Q2NVdoV25yZXpQb0VvUUQ4WnAzZ1U0UExEcGJ2MHVHek5aZDl4ZFB4VVYvbEc1Q05YYXN2MExZYUhFZTQvajI1c0YxS29oQVFHeTllTjBORTUvMXFmWW5ZeHRCVVRPcmMrMHNQcml3VzZiNnY1dFpSK3R2V3phTTJPQ0tvMkF5TnZ4bkNneGMxWUFvTlFRRDZrTUFuSG03Vm9qb3dTeEw1b0hzVXY1QURVbjRtOE1ZS3lsZzAwRlRWdHlGbFV5VFF1dmRXWk9VNHhiVyttWHdVRTVQTzRXYld2SHFaRFNZazI1Tm40eDhqYy9mOS9KbE52cjJkUzdiVlpTOFlvQXZYcjVWTmZSTXFDSUU4ZFRIc3RaVjBxQUYrdEZUUUZ4VjFGQjhuWTRNK3BubkR4MUZqNGZQSGl3M3cvR3VmanRDQ0x3THZqZ1podVVKeGpsT0tOR1pRVGl3ZVFqNDBuM0JHSUdTdHp6TFhUdUpQQWpJdTJQMmRBblFrRUYrL1U1OTA2QldqeWVHTHJYVG9WSzVVSjlEUU0wUnBqdVVjZEdSZ2F5TDl0Yjdza3hoWFJjekpXYUFyNTVNZEE4clQwSUh5R0lLWmhJTXg4NTd3SHc3aG1xdy92eTM1VTVLek9IZFk0SWozdVFtSnhQcFpyYi9TdEhoUkQyYSt0b0ZZUnJpSExJMzF1bmpwN3R5TjBMSzdDK2h3VVg0KzBTMHZyR2szTTRVQU9SajVLU050OWtRUnNPWEhlUUJHVTd6Nng3UWdUTUFEYWMwY0tHVnpuR2pFQ0ZWSEVGNzNJUWNQZktHempiak81U1FuUlpMRm1KWlNjQVRpUjZYRnBZWHRSb3UwVzBJVEVaRXFzTi9yTm5sM1QrcWpiR1I2MGttdlhHaGdjMEFMRW1hTThGMXh2VXJQd2xjVk5xZ0djWWZXbktnUUhuTjBwdUlzZS8rQnhVemVlQlhmcWJrUGpMYytYenVpNjYzMEQyMlZMaE9EM21YY0NJdTMwcmdBVG5LSlo0bXZ6MHZvT1FtWjFGTWJUOCsvMFowWnV6UTUwU3JNUW45Znc1T1lIai9EQU0zRFIvd2wzOEZNcXkxY2FGc1hwdm14cTVsUnZRU0ZSWno3U1NpMG1TSUZCb294VEVaV2tWZ2k0dlRIQnBpY3B1WWF0U0tYYTZTckJCbTBacXkvTCs4R01GQkhlZTJadGNiZGl1WmxEYXRQTW4wM0J3UzhyQ1hXb3Vqem5tNUZWQUpuOW5DM2ZqdU1jYWNMZDE3NVlvaDVkM3c0SXFkU0lhQnVsbEdyTjZuYjRBQXRGdHFDa3JyZGR2b1IyUjZpT3ZoR2ZkWWxxbDgyTzluVXJ2MWJiSTMxMHNCZU9TV3czdE5vSXRjQ0szaDJZNjdQVEZndzROZmRTSmpLTHg5T21zcXNDRk42cXRBM3FjUjRwUXJqOVh5b3hPNTliVU04amZWdmVMUFJLSDAya3RrKzNYR2dKcjJOMlFGM0N5V3BtcStvN01nSVFoVzFPMHFwZTRsVCtmdit0N01HN09Id00vcHpmemhTK0lPRG8zdTZsempFd0h6bnlFZkFWWlNzaTF1Uml6VXRNS1BTMTk4M3A3cFI1TlNadXNvRHRobmFIVjc0OUZFMDFjdElQOCs3Zkk2c0JOUFgrYWdGYUtHNWg1VGUyQVBlbGlCODVEMjY1RUJWV3duWnBTZ1ZmSXFsUExSYlJ2bTBXcW5ZdUNUdDRBcFcxUVpBMHRIKzc2cC9jd09NOXYyR2V2V0pvMGxOMTJLQXBWN0FXTGpqUlhHSmhSR2VualJpK3JtcjVtVUJmajZqK0VHWVIybVNJTHlEN25rL3hsYXdweFFZMzEyeG5BWEJKRjUyQ0k2RHQyVWo0N3cyQWVkU0h3aFUxRjdJbHNpSW9seklmOFRzZ0dNbjhZdXR1T3UvdG5oVGlMY2JnT2k2bHJFY3JwSFFueW1xaEdWOU5GcVN2OXZVSEQ4R0prUWNmbVp0MW1YVnJVTXlFZzNlL0pyVVNTNHI1c2FCTkVPWXE2dmwxbjljbEZVZ1U3OGYyYkp0RGhGc3lGSEhvSXZjM1dEWlNNUHBLRzkrUlE4cDZSMzB3ZjQyVjJTYW1lOEpuWnM4Sm54UXVCanVmd1lyRFRPTlJ2WDdWeXVCODdkdFh3Vmw1R1JOMWg4L0ttSnYxWDZQNms4bnJwdTBlbitkSzNGbFEzdkRyT3JRQ0hTOW9DSTZDNzAyYzAxTFdFTkVNVzE1SEVzeUN5eVF6RlBmRTdIMkt2cXUxdWc4WU9jY1g1RjBjem4venRnTjZ3ZG9lb05BVWc1T2JPbjFJNit5YnM5V0ljc1ZUL2xUOEMzLzJvMTJkUmdrL25YUHlwOThya3Q0VVhVVm1UalVpUG9oQnY1MkZzM1RCVjNFVm1yZStsZ1MvdERGektYcUxmaVhkNzBXaTBkU1libDJnMkxudFAydDFBeHErTm5WOXU5WUlYTVBDbFJReDVmQ1JqMFFyYWt3VmhLMnoraCt6S2k0Z2lGZlhNNEw4NHFUaXJpNzVmOUZzb0V3K0ZPMFZFQXNTM2dlZzFXM1VwTUVwcWFsWmtSbW54UEhSZWwxOHJDY1RiQXpQdWRRTE5OK1p4d0VLSGhvakVEeDVjSDNyK2s5WDNzYnBTdkQ5RC9aZWhVVFhwU0NMQjc2MVVYWENSU1hxa2JBZHhlOFJOUGZvWjRsMFhnQlZ6NzcrcHI1TUZ5Zy93a20yVnFKTmFpUERjdUFYVXpwMTVRQlQ1Wk1jRys4eXJ1Y3RiSVM2K2xFb0NaSElFRjc0aGxMWGtremY5Uyt5VEwxMVZXZlFVc1lrK0xHQS9SWExiVzZBR012YmJiYmExU0luTS8yWTd3WG96cnRzM1FRMEI3WXJqYkVZR3M1YzR2cFhjQ0RsaCttMFk2S3VVVEhFb29ad1VNa1RzeENFbjV1SjJFUFppZHN6d1htMlVBNzlXNklMNHFuL09JNDNYakZGYS80akcyYmFnaTk4K2NLSGtLbTFHK0YyTHJxNjlXTWtPZ0hRQ0ZPY0NPQmFoY3h0RnN4ck5wQzNMTzZqZENrWWtOd21OZjhKOTFTQTZvSm1Pam9RTzhvVC90TTV5YVpkTCt4QjBTdXpRUmorZklLZzdoOWVCRlBmbWtjYkpza0FSdHFrYW40dWd1K2Roa0lUTXQzdzJyQnBVWjVwdXpzM2grWkpERVhqenBnVDRXYnpZMzAxdHBmNjI3ZHBjdWkveUMzRmVGVUczY3p4Wk9wazExWEdjWTFPcWZiUnh3NHR5OVYzS3psaVNiNTdaRFBjZEdxNGJkS1BMaEVicGNCSmlqUm4rUWVmVXl5ZHlEdnVYMXlMb210cWNRa1lZMDZyS0s2Ky9BMzRXMFpHZjk2ZGF5V2RJSTI4Y2J2QksxYlVmN1FjUjgwSVRSbFJKOHM4N2xPUUxxM1I3UEprTmxyRkFYMDhQeDBrU2xiRk13OWx0SEFhWG45SXNIR1Z4VWc1OWFTN2JJajgyMWJrWGp0KzMrT2RwK3JadnhVVXk4dE1INUFySnJZSW1zM0FQOWZaNHFwaWlPczE2OUdGa3FyVmRIM2V1WFpIWkdyZWh0NzJkMDNlZ1JpU21ZcE5iUHI2VzJGR0NFSmZMVzFPdjRuRjhTUDYzNGZQNzlUbXZHWldrQlhiMEpVcXJhaUdrVVREZmdLaDRXMVlNcXFqNnJqc3VDZFVkcWNlQUUwUTFXbmlLb0MxR3lsTnlsM2VwcFYra3c1TncxaUlMWVVBV0ZVbHpHa1BsT2FjTzAvZllac1FDSVZoQ0JDbVlBZnR5R2JzZENZVll5YXcvT1RzMUcyWCs4UG9yUEdPVHc4a0hqeFVUUnlPNTRBZUFlQUM4Q2xSdWNEWndodXl5VmRqQnZNb2NqNVpqd1I3ZTI3WFNqWjdJVlJ3djEzRWtXTU4vbno3UjZMNENBZ0lDQWdJQ0E4ZjIvU0IvdCtpcy9aOXViK3o2OTcvYjlNSCt6M0IrYjdmcE4vMFBvM3I3ZnRHcjdQcnE5KzM2YUQ5dnUrY1A3UG9aLzIrLzUrMzdDWDl2MU9mZlY5RWYrMzJZdzZiNExxaCtwcGw1cUVzRmcxZ0xwR3NTRWxWMkNTMVZpR1pjUEVlVUtDdjlaaE9TRE9KUDZUaWJEeUlvcmdMNW4wcU1Yb0NWQ1VvU3AraUZOeEphSUphaVZLZE1vUWRRWEIxblFUT2lMcHZMTmltTjVoTVd4SklVWDdkeGc4T25Ib2dnOU1zYVQ2TFU1bXhaWWxGZElVZHJUZmRzQjhjTVYxK1pWWGg0TUN6a2x0d3F5VWhUZjJjZjd3dERPQU1Kc2IxUHFkMkNESWhxNzByZmpqb2lCcE91YjAwNWFEZzRuZGV2Q2pQbCtvK2JPU3QvY2xhTHg1RWVCYW1SME15b3NBTDdUY08wUUZFY2RaR1N3RXNUN0lmZk5zUnZMYncyOEFsV2tGSEZ3VXRWejVQSktDQ2tKQzhtL2JMS3c3YTRrM2plZ2JQVVRUa1FzazF0VUZCdDZYVGtINElNajdpSlBGYkh0aXhzb1Z5NC9TM2RMVkMzSHFXM0I5TnVXWWJ1ckVZV2lSOUFpUWZieGVEZEZ3R0xHNW5QQm1FcnpveDZJc3FMNm4zUExNZDhxZ2pFM1Jxc2ZVTlpPUWdsYXNVNmZta2t2U2pmOEUzTS9UWlRFVTQ1cENEK056TEJlY3pPWG9rdExtQXd2L0Rqa0lDMUVOWHBEcHNhbkhaQ1BYbU1vTFNheENXRCtpNFpjcWhIZmttaXZ0UXEvdks5a3RVdFIxSDlyd25tM1hPNyt4a0pHNkpob1AzbzVHS05pemkrVXFmTXJEN05BeXQwNzVOUWdOU1l1c2RYcjloZDVmQ0o2QVV4QWx1bU1tczk0eG5LMnFjUWoxTFkyUUNmeHpLRnhrd1FsN2Y4WVpIQzZ1RVI0b1ZOTDR4aUZIUWc1OGpSdlVnVzVjTnpkeVFsRVZ6RU9RTGRtVnhvaGNhZnJCMURoU0RkcFpWMW9UM3hUTjlBYWloRFBBcVNwWmpZUkZCbXl2SDluM0xwZGdEQVg1ZzRsckFsYzlOeUk1d0JiQlpYSzBjOCtCWksza3BpV2kwZ2VBNmg4cDYvUUVXaUs4QVhXRzFSUlR0d1dXZmxSczdxOFRCWGlwKzRRc21uL1NPekt4MlI4SWdERlRhdWloQW9Wak9GUy8xR05jaWhKd1UvVlk1NHd0akw1aEtJMDFuWndHYXhGemd1WXVsa2hvUUlrYVlIa0tNZTJDR3dWREFlTFZwNVhOWlpLNFFzcStrSHRJcHRoRGhhd3pRcm4xYnVyeTI4ZkJUcmhNWWZNWWhBS1BQaUJhaDJYc3NvbTczaVBTVzRZQTRKaDBYNkY3S1pLRHZ5YkpzdHJzNGl4eGJkQS9WRlRKRzlRVzFRK2JVdEJGclF3anNmaUoycU1lT3NpVjl6NFk5WCt2ckE1SHQzZzdCUDlBcVMxclFGZFJYMzNRVElvdTJ6SzBtR3BXWG5zVUl0aDZkS3BCZGNUQUZoS1NsQWEraTdnbE9xRUJxTDV3cHJwZys4MnBvVEpPR0FwYUlrNDFqbVJ3VC9XdERiYzVMM2xsTVcyMk5jSTZTWHVhWm1tY2VYWWkxZHVpdVUxRUJPZVJWQTR2MXZaQTNBbDdXMVB4QUlwSW1hLzZQMW51aUp3UklkbnJ3bGFuUjJGNnNXWGVhMmdOK0gyTWk3SC9PUDZ0Y2RBSitFeTY2dXJNenhKQWplNTVNRTQ0UURDeTUvaUxZUHZhMmd6eHRZRkc3Tmt2SFNydHduWDBpaEZyaUhJR2FvUGdzblg4NWM4SU5Cd0ZTSzlyWUQxWmZOeEdiR2hIc2kvaDZUcnJrL0lNS09wNGlCM2ZKQmRBUUt5cG0wNytVbWkwdVNsMmlCOGJ2U29zcXBNajAyMVR5N0NkNVJnTW1YOWNGcFBqY3VHQTZwQXBqOVdSQTRsSXF3R1AxRjZFdEJ4dnFHOFFYQzE2TU1IUk5IZE54N0pHbkFqa3BENklkT01mWHdEay9wbm93Sm5DRFBsbGExQURES2FMeURxSk1JNmFEOWtkQmdEN2JhNWNBbVpFeEtsMVE1SzhicnhQaW9xK2VyaUh0WGt1WCthQ3JxZjFIUU92MDdqcThFd0RLblBOQThVOGhPaHN3K3hoYzR6OVRMVHJLclNGUzBOc1RidGQ0Rlc2NFpxa2prMmtraEZ4a0kwN1hCV2NEUWs0RUtLYmFzOE1WcVgrTGV5ME9JSWpocmhpOWdtSVBwUnhOVEJGZE9FOWxkQkptL014Z3FtYXFRRWs1SG5hMFhuREw5ODJQa2xkSnFwMWVXQ2lLcnAvVUtpV1EzUC8xSExmUU5vRmxzZkd6Z1BUd0JKSEoxMVpxbWNuRk9FRThGaVByL3BUb3lwa3lTaTdad1pwY1Ztakg1K0s4cXJWMmR3UEFKWEphN3BpbHhEQVMvY21PS2EvM0pkWXJsdG43TVZ2MVRpRGoyZWhCSXRGbytZaTRBMS9aUkxkOEkrMHJqQ2U2SVY2Mk1rRlYrMXNKdWI1ZUQ3R2VnTTJFRVZRTzFHUzRCSW9MeCtjYldxS2xaRlVwUTNOV3dRNFhOUy8xdzQrdFB1THpVbVdLTVFoS25rN244cVVzcFc3VEZNZGxLbWF2RzRpWDZqcDc1d2xoamE1c2lYSERVajZ6VWR2U2ZSM2JGcFVoZzRIVDBZd0drWUExZ21nRHVGYWdvWitYazFHQmQ1UmhhZURrSEtiOHdLU0NXR3RtQlFLWlhnY3F0Zk1lQ2JOZ3VBZGpTcUpZbG9aalVnTFVVQlRQWWVobUVkaDdBV3NQdzlRbWtxNzdBbUNucXdGV3BTZmJMWHN3NzREVzRoZGtKTHhYTFVFQ0VveXhHZDdBR0UzZWhhMFhKZ0FLeDFXOEJBS3NTYlVad1hPMU00eGZzWFZ1TVM0OW9JY2habnQ1R3BoZUZ4UGxJcXZIZEFUQnJVam5uR3RJQUFKWG53Yzd4VGhMdGdDTEdnT054d3QvdFBaWDNWamZMaldYSWNDUmk1SXI1cXZtRjdyOUdlb2owdktsVllBUXFxZlB3RE1sVVNIc05EcDdERW01amw1c0pqakRBTWJYWFhqclByRzcrYkFxRHFyR3c3dEJiMVZGeHdCcDU2YUwwaFp6L2xudHd3T3p2NVk2Rm9Cc2p1Vm00UXcxOUY3SEcxdUI3V04zTzBvV0tsMGdSNFJPTHdSaVk4TlJZdEpxbkdJN2NpS2ZrYXJFTU1GRnpqOXpiR2xuSktpK2wrclluZGRqUmw1WUlsWGN2Vk5MSnVJWkw5Nkphb3dNdmwzanpVa1JQUUtuNE90TTFibUpnKzB2TTZ4QWxJNFZKZnkzRjBFSFZvaHQ4WXJVc3o1QTNLZDFLOXZXN2FqNW1nV0pQdGJtOEtncjc0NjFOcjhkbXBNQ0ZmYlJDbVNCOTlLZ3BEQUQ0SjdLd2FicnRMQk5hM2Z4NDFuT0FtallHMG81SitkMGRvMEJpYlpaR1Mrd0t2MGVlQlBQdkxEYjNXZmIrOUtSK01GdlNJd0NqN2RRT3lZSUJ5L1JsWGp2dVJhVkN6cG1XcVNZaFRXN21DTmZJZEJtZTRXdUpaUW1ISWNCTEVoN1RHTXdYb2ExMXRad1ZOYnptTmlpc3BacGlTa3gvWWtsYmhCTWg1czBTdmJBS0tKMHp3U2ZyRFZqZ0o4Qks2WGZuemZTTXRXRzRldTRxOXAwSlUzSUp4YURrYTk2MC9YMWJGTmRzazlVZ1pDUjEwYit3NXB4cS8xZG93ekhLOFlGbU5qZmxoOUtwb3RFcVZ0dkdkdE43VUorTUd6QXhxUkxjaTFjQ3VUUDZON3QrKzR2a2tjS0hQZkVORGN2MzNCRFlQNWJQc0RNOHRMUWUzTFF2RHpyNmUvY3YvZkQ5Y1pkR1c5RUh6SmZMeHZHS2NtRmM1U3R2dDAvaURRY3RCaUQwQ3NmSVc0dXZIRUt0WjdJV1pBRjJLaDlLWEVFVCt0dlZGZld2MnBwQ1RPd2dlQnJJQ2hKL1gySm1wb0RuNFZ1WTJHKzc2SHdudThFbC9QSjB3Kzhpb2lzS2hINFVjTFMyWnppbHdYeW5sOTJrL0RZa01JblFwM05LcmFnZkh5ZGJtcHJtZk1ONlNaTmplSzQxTnp5MnJjSHo3WXgwSVJOczVROS9HczVybjZuZ1dBVGN1cC8ySTFxQzBIK2RSWlJKRzhxQVorVll0ZWxHRlhmMXd3dlRVWUtVdG95VE90NFBWWkw3d3RNT2RDWE9mNjdkOTRUdEJhU0d3azdMTW1sTDlhOWVFTVhHb2FkcmpSZVI4NTc5NmlFcUQ2bHlQWmpvbkRWdklyWlI5ZkQ0R0swUmlHRWNNR0ZwbTlzOE1pWHNaMGx2dUpCQVA1QjBKeWZvc09XU0NGS1FzVXY2amNxV01oRG9QVy9IMmpPOE9RT0txYzZRME5FNVN4c3JMT2ViT1F4eUlyU2JUTVZoamZWa0p3L1BvMWFLUU9XMEw4TExicmM3aHBJajkwMlNHYlBBTk5xaE5td1EyZEsyaEZQekJjYjdvZVdsUkV2ay9JQjMrR2FqYjdGVEJYQnJSVDdCV2NVMDNIYjdrTk51TmFzVDd4Ni9zVHdKWHV0emxadEkyZWxkbWNGVTBGaEFxdytyQlVCa1BmR243dVovZ1d1TDRFWmxEN2dhQzRELzNiUnJpNTFrUnBEdnh0ZlBtTUxMVDFTci9LUjZNM0xxU3F2MWQvclJSWXJ0bFhNSi9TTzNQM01udUhtUVlNWGJBbW5sTnkwZ0xXTEFxRzNKTlV3OHlURkFPSzNqVk1PVUdtWUlHMG5JM3V3ZDhWQTBtTzh3VXliZWJTS2dYajlFZ2VsMk5RVjY2bnVROEZ3N2E2UkIzT0xndUl0N25qbHZVSTZ1ODFVTGNqTitVVkZBZEFiQ2xqbjR3Zk0wRWtvLzFOVW4xckdvZ2pVTGFZbDFzV1dTR2g5T1Rhc1ZKTHRISDc1SzZMNk5YVCtqcnlVZFIzZnMxK2VtZzZtS2pBRStzb1hMcGVrMkZIUjRIemdqQzBsazhTTTR2b1BWUlNVcWRKanpzM3ZkdGZhRmtLa2FYc3dFQTI0bm1FVkNwQzRPbUtNd2lJZWtwV1BPTjNPYUk1ei9CTnlrMm5aYVFUSkhzeDcydkx1aW9NRDVrQityKy9Qc1RsUHNRWm0zSjgrNjhsL3hDVm95MGJwWG8zMWVvS2Y5N01ldHBreVpaeTdVWEdVdXc0ZDhtRWxPY0hyTFVBcENjMHJPWWl5WkUyUUMyd1FvYmo0cFNXcXF1Z0VGNFBjWmlBd3dBY1lON0xVNmN6bngvUktaRE1XSVRhT3dVNm8wV2VhenVjcHBpckJsU3RraStzeGp6cjFzcVNtSUUvU3p1RHBwd1VacUJJd2VaV0NNSmk2dTFHYW0rVForRVlMMit4ZDA5dzZIMUM2L3NxbmEzeVYwV24yRWxPek1xYjRaVFp2OE1VR0NrQ3dzUHprVjI3REdxc2NDV2QwSVNFR3F1M1lRSGI4eDBxZWRFYXZpMDBJWWErV0JlNjE4Y0hOdXk1eThoWjE2ZExGSlZpd1hrUGZ6cFFyQUNHaEJ0QVhiMmhoTVd5UnRHZ0NYaUFrUUpQSGJoYlM0aVF6VG1kL0pLaXpBWnB1aXMvMWdMakw0QjZhVnhQd1BmMDJkVDhtd2NpalMzNlI0Yzd6a3Fxbnk3bHZYWFp3MTZTQ21ZU2ltcmdza29Wckd4MlQzSGlWTWU3WWlGOGp1NTJUWjhlR2VTcmVoTUlMS1FBK1ZscXpwRGxNT1l1VDZxcXFKZVJBcWRUUmtSb2FOQStGS0xuTUw2Wkh1V21GTUM5cExicDRCdmZvY3oxNVpKSDQ1UWtiM2E2c05FSUxCUWxwbHdwd2c5MDVnUkxoYUxNK0x0MDVsVWgrVkFKZjBla0FiNXVWaURoU3lpZGs3Ni9EdmEzODJQaWhMSEovcUMrN1FWVVFSaU5EQW1kRjhYTVBGUVZmbHdWMEdUdnVHUlozNTdiakVya2dPVTYya1orWWVXT1ZScFFNY0hRN1hsQWpFeENDZHFYVUNtbnBuTjc4dE9RemdRdjVQVjFPZGJHbjlNV2tqTmpaZHhWdTdCek5iZFRhOHBmK3gwczg1SVFLWmNyUEhxTFcyMGVNZ0hEZFRPSnl2YTR3RnRmR1A5Yzl1cE40ZGFMd3dtUEN0dHRXR1NZR05WSGN1b0JPWVhxRFdKc3NSRTVQMWJ0b3hqNTdjNjREZnQvMDNxb3BzNmY4SWhOMTlOazZJNnZRTmhlakpaK0hWMmJJLyttcFdVdmJXazgyVXIwa2lnTFd5cm1QQVU3blU3dzJBSG5zcFVRdGxJTXBSNHZPb2twSElNVmZBN2s5VUd0dkZKWHdlb2c0VzJ1YkU2bGpueDVEVDJ4TmlidHlUNllZK3QwOFl3OFpTd3UzczRzTWhDVFdVaU1heDNJcWRtczZoM2gwc0xEc2wvVjZ3V2pRZ1BkRTgwWkFoRU4zdnRjZkZHV2dNbW9xbGp2eGxxNUVib1oxYjlPWjV0bVRub1dvQTlHOFV4NGJRbnE0NFc0aFEzT1RmNlJ3emZCMnM4REhmQUhYY1M2bmgrcTF2OWlYWFpxZ1FWU0paZFJCeWJqK2xVK0g5RUNQU21ZUlNYRk5hVUJrS25Qa0ZNZGxGSzZnWC9ENm5zK0hHWVhHNktvWmRLRllGSzU1QzdLRHM3djNDdFBXMTdxUWkycTlHVzdLZEpqd2UyK1VpNUk3ZC9XODJFSHR3ZGJSV01jZFVLZGdhMzlzdW8vRTR1ZnQ3NS9qRTVKQzZzRWJPYWIzZ0ZpWmpESnR1UGNtdDBTMHNPSUNRdzNhekJ1Z0tFWDNteXcrRmxkZjlJUGx1a2JETFVEbWg4QjdqeWVxVEczWnhuR1QxVTNFeHh0Uk9Ca2dDOFdUbWMzb2VSNEFvY2ZLTGdzMkJzaUpFWDVoa0grNkVzWk40MGF6ZXpUaFBOTWZEOVcxYnBrUFNyQnB2cStEa3V5QXJENlUwdldRTXE3MzNnOE5RdXFoN0FOY2xZRGRSVjkrSTNjajBYSElBc2R1d0w4cWNXY09aTG1ySUtUb1IyMFpMQmo2NlpRa2krUGRXVWMxTUpvYU9OM0lnNm56MVRWZTV3UnNPQzhhVWlUSnc4elNqZUZJbVNmS0E5UW5SM2pmWHlnaUlEN044QWpxeTVVNm5GUU1uQkN6MU9jMVpNcTFmaGNKdzBLQm9oendlUkxpcVkyLzRaNUlnZE5ia2NhS3lCRmFQaHd6ajJMb1EveXR6SHZISlEvcWdwcTNvWkFiU056RERPSkN1cUpxZmkvTzJvY2tFcGYrcktJQlBGMXduQVRKdU9lczBORzhVdGpQVDM4R3hEckpxelo3QnQ0eS9keXI2dXB6Vzdic0dUdVpTWkgwcTVMUThpMHVXOVloQ3lncXQyMkkyMGxDNFVoQ2d1Z3NGclRibTZWYXJrdGZRSWJ0dTBlWDhQc1ovcVZiV3ZNQzZldHppV3hKMWkvbTVNdXdqSkRyS0JzZkNIVVJJQTB3UzR4aHFjdEI1ZytZWWhVcUxuRGpQSUx2VWEwYjVtQ0FEMXFPSi9LdjU1SUF4R1pMOGd1Z0xyK2Naek1sZlFJMGdrTmtuRGE0cXZmS2lPQnNsMHYxTEh2Z2NoU1FiNG5sdFc3b3FYNFJZdzN0MllCczdlVzlMOE5TK3hhT0VGMG1CNmxBZUIwZ1Q5alJEWmplRzBWU2RGdFRNMUdpS3JKZ3VKY0xnQmlxRDFnVlRvbDRFbFkrTlFtLzdHQUo5NDYzY054VUZQeW5LYlVrUnpDM1Ntek1nVFdadURCWWlWRnlqclZWZXZma1hFMXpBaEtwKytwRllNTUVpRWt0bW5UWFJuL2VpV1FOOFBiTWFnZHhZUFN5U1dMWDFla2RnNDBCKzNvRHRxbnVxMjIwV29peFZ2RTB0UWlMbVM1Nm5IOXAveUZCMWd4c0kxRDM4WUp6dzI2elJ6NEJGVnlGRUhhZ2toWmZjL05oSUx3VEFMb0FBVStjaHZrQUsxcEQ2Y0JzZ2o0VlZML21DT2VOaTNHNHR3SE5ITis3OE5XS2pFY0VVcSt2ZEcyN0w5OGpCcXJkeG5iR1Jkc2FuNVJDcHBsL3hyeEtpeFJyWDhibElob2FkQ3QxcFZMZDduaS9rR0FjTDNGMFFtSXFucWV3RDM5dXoxK0h3c3NlTTkramd1c0I5UTFuZCtHNHlIVXBzTzNtKy9kSEk0RFc2RnhDUUIvbmlHbHZhL0MvaDB5bUpkOWdTR0VsM2dkNU9LK3lKa3F1QnlNSlNFQURvaG1weGVGWmQ0ci93TWZMNlFBc1dQUExmZHI4S0ZuUS9JZkNzMHBiMUg4QmkwM0pGdEpXTGhQc1BuZkVVL1JPKzMvelZzeVZESkg2SVNLc3kwM21WOWpyRFQ1YjYxS0dkRE4zZFM2bG1rZjM4K2RFbGUvSURvVzZOSW42YVJFalpsdjdsZUJ0S0xsN2NPelpGN3hWOEd0cGg1MWZWU3hRR1ovUFBTL1NYcnBsUzM3dnowYmI3eHRSY2pUeE42T0ZvWlZwTlN4MEJvSmlOcVAyWW1JUkY1L3lsK21FcWFUaWorSUxWTHBYcU5seHdlZS83eFlaQzhqa1FzR1NFZEw3aHJVaGNzejN5clpwb1d3ODhGSzJOem9YTUd4elBwSjNzUVVJOXlmK0hEU3lQMzhzU3dnemJ3eWhZc3RYMU8rWEV5MmhNemMxVUZvNTJNUDhMZVJHdy9EeXRNbUhSNml3V1Y1dVRKNE9SWGE1bEU4MysxY0JpWFVFTEU4bmlPY08xSlBJZFNnRmdPYzdodExuVHFGYW93UXBGeFhrU1ozUGExUytlR1NyMEhxWkszVEhaUU5EaE9zaHNXTFQ1aE1WVlZkOUdwcWIwUUlqalpiemhoc2lQWW9RNVpGQWpLcDBqU29PN1VmTW91Q2orVWJNdFR6c1VYcXc2bmRNMFpjL3VHNURJZm1HdHpvaW5kQ3EzNHIrOFpFcGNiMC9CUU9UUnJIeUVhMkJMdk9odGs4ZTg1OUVlcmxpYzRybjJRTEUxRVNwQ21QeUhNZU5adHBtTWQxeHNyc1VhTFFFekxweU1XdlJkNDVLRnZqN21jdWJ5Q0x5cWZ2MlExemdmdXNpclROREQ4Yld1ME9reFhuZTk3ZVU5Vmt6U2IwM0RCUWZEYWluUldGZWtUenMzQXJGdEduYVFabUdvQ2FWMUtYdFUrVUhjQzBMcTgwLzhtNWtpQlE4N3RnL1R2VitLbDNxSmk3R2swUmtydFZRRkV3b3l4TW5iSzhKNW11RjBQY1lkdWsvYmFSS0VjenY5TklFL2NwOUlSR0NiNlpVdXJab0FjeG1Hbkt0YTdIN1ZXbE5WalBDV2ZocEZoRHFJRzI4TFdpRlR4RjFxcHlrekgvM29JbHhhWUdUaHVIYWx0ZFVBVEN1N2w3NHhnWmdWbDlJQk1RRDFuWFhHNkpUbUk3V29Ec3NyY2VwblcvZjVRM0htSDVUcHF6RzgybktYazBnMjBzdFRacG80cWlkVDRvaW0zSUF5eWFTazVock9HNEZFS2pvcm5kcWNWbUdjUUR6OERSdXpNdWMvdzZMYUlvVGdtUElENGJsbU84bHFvRS9aZW5mNmxoMkJFMXZQL0d2c3VYWDVwZlRJeFUvdmRlSzdBTVI1Zjl2RkJQTU1iWnRWUis4NWJEQmpnY3JHQ3ZFMEg5eUs2bGUvZ2UvMHphRUpjTlNmZUw5VGlFTVJlNlFQQ2ZnKzNEaFhNdEVQOE1rUUVaZEV3RkZkK3BRSmFDMHhaWFFHRlUzcjl1dEpMYmpCTGp6ZDVlK0Z2S1BNZXFaYXNTd0J0anBEcU4yY0VCNEwxdm5QMTdIQWphY3FiNnp3SFVIL2NxbHYyQWg1MXZ2SkFpeWpSNGR4ZUE1Y2NaVis0VDFNbGIrSjVBWVQxY3VNaFl5QlFQanJxTm9TMXpSUmhzU04vc0l3dGVDUXNmZEZtaFZkSTZ3WjhJVk9CUDRiV2FkdTFETHdCU0wwL0ZacTU3NDBNUlM5eXB1UFdmbE40aHBqWjhzK05ZR0RwVk5Yd1BPSXpPdVd0cmlLaTF0akU4Z29rNEVQNFBoVkIydldMTldDWmZSQ1RPaGtoMXhuSGY0S0M1QWNXMm51cnpqazlhbWRkYndGZ1FiT2FEamZpc1pSQ3Q3a0NHTTdBYXpXSjFIOGJYYkNKTmhVc3VMQVF4Tmx2OEsyT2ZrUkxUb2dQdDMzdTM3cUVxNHROc25nWENwMUVlR0ZRMFE2LzFPWXg5Zk1aWWI3d0pJS2pKSGRYTXIzR3JVNEFPbHhpTFNuTVBjWTdyc0JWZWY4a2hsQ2hYN0FPbXMvTm5Wb2xid2MxNHdMZDQ2SzZENG9FN2JVcjlhUldPeXNQcDZZbTlPeElEb01UelUrc1pnd3E0RUEwbUpvTm54MWJuOE1rb1lqSmlJKy9hNmtmN1dzVkN5TGhiTWwrYjVCb3VETVBCM09WV1JDRjJKdVd5bVorVU40SjdqcXB0NG9mOXJmWGtmdUpuMk42cThnbmFQQXgvUW53WGlya0lza2xVZTEwTmZiWWdZaU5PVW9iK0F6QmZTcHRURDYwckMrRmllSGFuMTBPK3hQYzBNNTJUZ1BiS0VEelRNWEl5U1d3MkJqTldSWjMyL3BLdW1FK09vM2VCMHJQNGRDV1lEWWZlMVJCQzlnZzE1ZENDTDJjOThVQllXbGJYN1I5ZmtLWit2aEVabXc5Q24xcVpuZmFoUzdEcWRyNDlUa2Q0cXlmNjNUMnhNTVRvVGR1UitLYjdBY2Z0dzVqOC9LanJhMXAwM29OaTZLRVlnZVNoY0lxeTBrN1p5bkRvK1kxSzd2clh6UmRmUWN3YlFTUHU0ajJBMU93S3ZjblRSdFVrbEhvRGpSelM0Y1VpRXk1a2Z3NS9GWGRWVXRWdG8ycjhVQlRoNEREdnFHekVmMzlWUE9NOHl6WFJOU3dtL0hsWDdlZ1JvYnhJWS91UlVvZm04am14YjF3ZktpRnNWUDhsVGduSlM0TkQzYlZkV0Voc25Ud3U0eUZEUGFGQmJrVFBIR3FBVVBteU92WjVsRkxvQTRYSHZYQlRWdUVDaHRDMnVkT1hvbEMvazRYWDZQMk1vUTV6K2cvVDZWZEJ4TWQ1TlNIaUpTVnRubHd5YVAwd1lPaWtMUlJCM3dFRHRZZ2tGYU0wbHlvaU9QZ2FrSWV0a0tQWmMyQXd2cUlxZFY5ZjNHSUNOeG9vd3lvbVlDWXZUSXErNWdJbXdvbm9JLzhRelhsMU0wa1Q3dmxHZjlhL0RQU2FhajJKSy9lb2lEZFpOeE1WZklKNHlGY1JiN3dnMUZ2a1VVaEpISHNQVWdFUEg0RExZWnhJaUlUSWRCczB0SEQyelFvT2crekRrQTl0OGF6WFlQS24wOG91TVQ1b0QzVG54b2V1NUxKRWJtbTVYYzJ5NUM4UXp0S1hVOUVxck1CNitRWUNJZ0lrMjF2L1FLdkdVeXovallpYlVXTFMxMmJlNWpVdzFEdFZXSUZOQmNBbFNOUjZtNlpzOEp3Z0pzSnJHUEFEL0h5M2tTSEFJRnRhR21ldDU3bC9QbnRqa2NEbis4SzFDWjRhWlBmU09lQk4vMitVSzRCM29MbzlNcXlqRTA0WmJ2aXJoNE5zdW4rOURsUklNS3RxSUJ2VW9lQUFlZi9TNUNuQnlvWld2czZueGVGUi9adGFWbFFleFhDM2pRNHNrK3krREsxZVFpbVBDbmRycmJpZmluVGxOTkRBc3lCbVRBRHNpT3lia2pycU5ZbTJ4emFMZnNXVWJVZzFkUytqaStIdnhaQnVqN3c5ZHVsMTlxU2JLQmRjakVJMUFzbk1vYndxQ05lM21DcktROS9UTkdzd3A5Z0xaYkNlend5emtLSFN0VnRLdENTNzc1b04yZU5SQVhnWkJaYVdaNkVaUWw0c1NGc3dWbUJBd0Q4OWZQZkxVUE1RY2kxbVFRN0dZQ21XelVKSTVYQmpJUHB6TzJpUCtXWVRGUTFjb3g0NG5PSUpyMmR1WTBXVHp6Z1RBQnFZOG9EZm5GaU1BbC8zNXg3a05SdmF3SXNGYU85L0J6cDFYdmJZc2Voc2hXVkFpcFhERFV1RUFvMGdteUNzSW9aZFFWZ3BaWEwwL3EyekpyUmFVNGtpQVNWMHN6TzNXcEFwQzFhZnBoLzNCY29KNGJhbE1Dd3pKaWR2ZjltZnhZYmZJTWFmQitSNmtLVEdIeTFTVFNZaTFmRVAvUDdYQ0x3VEdtVEdBckNhUVg1eHQ1R3MxdnRjcy9QcUlFeEdwVGdCblVLMDRLaVBvUTRhWXdBaU9kMlJjSlZ4a1FLeXNYcjBrY0g5TWxzR013L1NnWmVVWG9tcnNhZ3RhR2xkcjdWOEkvbkVwUWV1M2xYL09OWnlWdDEzaFRkR2dBdWpHNWx6NVhVb05NcHZqM2hXdkJkbEtmY1h2aDZHWXF3dTJoeEdFV1dSdGc2b1h2SnlRSWtWR2FFaHd3dTFjM3JuOEE0eW42emRVTDhHVmdtU0JBVE5DZWhjMGRRTXlKcktrT0tYQmhaMWprcHkrN2g2UW4zbEpTbWlzSjJrRmh1Q1NIbFhQVlBwalB6ZWg0TGRrTlBjNktZdHNGeTRhMFlIUWtXVXR6b0Y1V1NiSndsTnlMdGJJM0RVd1NLM2xSNlRaSXg2NlBaS2ZlRi9QdWxXS0RVUVErMEJhYWNqV1dienQ4OFJCanRJRWhGY1lzKzgvZWdxTjFrTFE4TTBmakdQZ3dqQzVEWFA4RWRiQ3FFYXRjb3ViZ0s3R21GVFowUDdZWHU2RS9MZCs5Um1ZaDJCMkpZWG5EV2g4WEtTdTdrWlJhdjJPTFJ1TnJHM0E3MEFwWGJsVXprNDZSd0R3em1LSnlZUDdVU2tQNjFsZ25rdmRKQ3JqcVdXWnNwRjRJVzBFQURmaUJKL0dJSkhVNWprY1ZrUjIvcTh1ekVLSlRHRHNyYklIcjBoVG1RZzFveHR2QmlieHhBMGxQMUJrWXYxc0xFZTFhZWFQeFhEQ2lRbXozaGIxaUwwTVloNE1WNmRBcHJ1TnFXTFdUY241ZGp4d3A1bU1nQldhMDVsWWFERWd0alRlU0F1TmJ6dXlNZGFlcW1SVUFKVmtDYVI3MWNxU25FYnc0aWNVaTFDS3c3VWN4K2x2Vkp2aHlZRXJzZW1OV1crS1RWMy9GWkNHV1FsdXcwaVZUM0liU1Y3aExvaDNqcXVnTEtCR1NQWkhIS0tyMWRCTUZhSkVpUk9pVmVZTGxTVjNVOTFxMGlVQ3hIYmNTVFZ4VG5GU3krWlhtTXV2OFQvZksrdnV5cmo1R2cyQWU1NVVySG9tY1BVQzdNNmgydmwxeWx1bHhYQWo1WCtlVW4yN3BtTDRJWERCVjNtcmJGd1Z4enRwZmdQNHpobkNhMGxFUzd0ck55VmJjcUlrb0crUEtwdDdVcnk5VllqQ2NtTjFiYVFnaFF4L1RrSXA2a3pDZzBsVlplMFppUlpvWW95UlFtWFdZbjBrbHloOU1QOHMzOW5OaGxsdmZqMnJTMkc5UDJVSjhkM0V1TldmWHliQlVGVnBIa3BtQ0diOFVYRjc4RndBS1BFM2dHRm8wbFJTYTkrTjF1cXJDZkRpOFd5SktxSFhXd09CdWRDRGdmRVhTSStBeFArZTZ6aEVhVzhaeXdQTHdHR3dndXdpN29QSWg4clBlcHlQb0twTUdGN1JmUE1vSE9ib1FpcnE1RjNmNUFsTWllc0lhZ2Qra3RKMWpmM2xuMDdlOWtRdTFZK0tOOFJudXBiRUs3NndiWU9BUDVNWlhOTklZMWFRdlFuNDhHUEtVckFtaHFXNnExWVNZWGNrMDN5c2VReTdxR05VSHByTjlyZ1l4azhLYnZOcDJNV0wrM0dVaWJCcVYvL2NWZFdGaEtMQm5PUDBQZ1RRTFBIbHJISUF4bStuY016TTFKRGF1V0w4RWJrNFVZcjZGK1gweE9oYjRncVBNQ1FyV2JTNG5TeVFWZU9wbkQzSXZlUHc3eWJFRXRNVTJTUm9waENOaXI3ZUIwS2ZJNEZrMU4yd082OXBrNnVJQXg5Q2pFcXZ2WVVOa3ROeFRZU3gzWVpHdWM1elBhZVVGa1o1YkVNWk1TOXlSOWMrUmtlNTgvdk9PMG1rMHJYa3JlNFpTaVRwNWhwejNVU0JFUzRPUDFHQll2eitndWhTaUJSaXNrRUo4MGNzSUM1UkU2cXgyRThVQnc5SEJqOWRSV2FlN0JzTHVqdllzK1pqUlVuMU53bVkrV3k5VVhBSUVLN2NXb01GNDBJcDdKMFlkaGRPU29sakNLVlJLam10ZEhtaWQ5SERWV2l0eHpVakxJUU5UM1lIRWpoQ3o4Y3FtdEUweXFtT09MakZaUjlGYmpFTWpzSmlFZFFKUkEwOUs3UnMzWjBpNkZOTnlrUmpYeVRzYXgxVE1yZEwvaURjUFRoNlBtNnJiVDlJTVBRbThyeE50Q1FMc3A5QkEwQ1dkR0xzQjFwQkFibjdqblJzZEoyYmNjblI4V3Yzc002RHZuSkR4Z1JUUDhKV3JFR0xJNkJucklsbnZoL0MyT2EzOTJDemEzaU1SVXJ6K2FIOHd3czRHUlpxQzlMbFZYWWJaUW40Ly9WODdjckF5YXNsMWoxZHAxVUYvSkVQa0tTQ2MrWlRJQzM0VWdZdm80bVowaGl0RCtaUkx2SUdqd01QM2JBMmZNZzN3d0h0QTR1SW81cXliYXVtVURCTUlWV0N3V1FtVlNZa3RIMnM4eTl5dWdRMW5lWDVLdDRNbGZvODNvZmhxcG9Nb3AyeEdTdi9XNHZtRTF1TVA5T1ZLMlVJRjJCS3pSZllBUnNBanoxa09lUVUvT1B6bC9hS2cwdWVoR0Z4WTM1VHpMOEdNVGgxMXZWODBpVG1MOG1UbGZHYmwyMGIzZEUzTkJMYzRYbS85dnJpUnlQMW4yTC8wM0hvb1plcXlERjRPYmw0blZLU1FqYWdhSkwyWkpLdDd1UEFLV2tyM3kxRnZFT1RnaVN4bit2T0k2NkVVS2FGSmhFN25acmV1WVlId1RRaityU3VCTFppRnpxWkR4Slc4WnN3K0hIcmVLbldlVjlKeU9KNVJENnpBUlF3RHpNRjZlTmdDOUtFRjMrSzIzWHlZU1NaTEI1S3FXRGJKYmM5cWt1SnQyTDhHZllkOFo5dmdPVzBZbzd6TGIvV0dJZ3hFOWFQMHpaU0tyU0lpTDlYZG85NllPVjExV1BMVGJKWGJ2NzZocEJyZ3BxZ1pReTJqK0ZDc2IrRUFONW9BNThtb042d0k3aFFYUURvRWZrNVgwbS9kNE1TOXc4NkpoVnFnbDY0Rkt2eWgzSkk5SW0wY2QyK0hXbFNQL1czR25xRzJ3eS9IL1ZBYzdpVFpSbXlLc0pNd3d5aXJIY1UyNEJ1eVF0b0xzV0VIdmIzYURuNDFXQUJON2l2Y0NDTUFqdFdMbWFkSTlyOGlpcCs5NHpBMm96b0JBeFBFSUZhUnc3b0tqbXkrbUYzUTRJZlN6VTZaODZVWFlOd0lJbUJrcVI1aG0vcW9hWTlVT2h2WDNEV2YxM0VsWUtCNWxFemlkRXUySjdDVERiaUliSkE5eVNZbjlkaXg0U0diaWFWNk16SFlKa0RCcjNNN25NVk5PTXlZWFlWQVQ5ZHZxdlpLN2hjTU1UdXBsbitQS21GdGhNR1EzRmZURnA3OWRyaEkxcUJDQ2F1WnM0RG1MVy9nTjNnNjlTbytYTDN2cjhuSkIxVk1GNXp5SmRCb1JHZkdmS1FDUGV6SVE3aHJ1d2JwOC9NV09leSt2bWVzbTVRdFJWNm1YMjBPbEtiYkp6cFlMb3kzRHhBR3Nyb3NkK1dlQ2VobE5ockFvZEFSQ2JIeUlieXpFTzdmdlhpMm9qdTVNb3FPTGFiU2VkOEN4MEFMNG1aUnRPR0ZkcVBINTRlL2NtUEVCMHFpSUxJZG90enN2OGFKL1k0RTM1ZjJ6Y1hUb1hPbnNSWTEvVm11dWNPcnB5UnpNWkJLMWFVdWlIVU15cEVBVXRzdWVaZXJ0WlhNaE5KOE5FNllkZXFESDV5WE9CMittbytVUGx3TFRBbWlwMWRQS0pRTGdWRzZnbm13N2ErdTlaeHdYWERYZVlIYzk4NUhBVkVmb0RqNHJza1ZxUGova1hqUDlISHYvYkNkZW42M05sWjVram91V0FzN2VrNE9mQkUvN0hZdzFTaFYwYVFyOFM1b2JwUVA0SWk4UDltclA4MXBpOUVSSTF0RTZwV09LY3NhRzZEM3BWcC91b0ZiREV1Ym9GR05CUm9FWXp1QzloWm5hR292dXYwVHRIR0xFRVNWdHQ5NVNnbFVrTHNCK21uVmZRMG5IZkxxRnZ6UWM5a05iZzl6Ym8rSlIwU0lGSkk1bndRSUh5NEdQalhnQVJZR0p5eDJRTm81Sm9GWWpZcHRHZDRnMy9SQ3RQSXZiTUZINVJFSlQwYkhLMUZYOGpxVjIwTjljMW8zRkxoWjJuZjNFSEc5YUptSzdSOWNjNzJhbVlyMnZnYkp5SitwTUNQWmRhQkJLbDNRWkVwQWtjQTZXNElhV1dSNWVPTGoyeXUxdTc4cGNjcnNnOXRwZEh2aURHUHZTS09tM2lkZmdMQVRZOHNyTXZmdWx1cmpzd05KK1lOV3prVjhFb3NLMG1BMzU1aVJmaUUrZmplK1hqVUYzZDk0UlNnV1pqT3c0TjJJc3hjeDNQeWozL0JaR2VNL1NtQkJXMmFVTExlTEgrelFjaTRMREsvRkloTWltWWp5RWJYNHhnTjg2Y3FqZnBqZEl6WG9oZWl5M094cmpkS1RMSTRDc0pNanNGclRCcTdHWnNta3VDQXBLRG1OVDFzK0Z3TUdhMDdBM0MvRm4ybkRUNjY1Zm9ZT3JRMEFKWVJiVC9ZVFNMdHJrMVBXWDZjdHdHWFAxWmROaDVObFNIcGFUQmlVNTBVck1vVHB0NmJReDR3U3hSemI2SElQUFZIR3RGMjJwSDEvMTJhSFdWeFhLOW9hbVBoamVFek5NdHNQRk1qMnBxSmEwYWV3d3ZyRXpTdTQ2eDhPNFNrZ3ZYUW5YUnVndWxNVzc3aGdZQzJjejBrdUgwMTJaUy9LOTZFeTlYUzV6QXlGV0V1YkRjK29aMmhnb1lFTDZlWkNPVzZJRFFsR05Wak5KeTFGQkFPV3RuNksrbTNUVDRWZ0dwdTJkL2FQTmY4RjFmcUhDNGlWVTc5K2lhYkw3T2NKK0ptWlp2a1YrSVFQWi9ndFRHRW1IRUxuOVR1MW5NVkhKR0tHOEN3ekkrQUZFbDE3bnhQTmVmTWFsMUs5WkkwdllKRVVmcDYzbXEyMGVIcXVldmd4Qm9mcG9hc01NMVY4Z0FKMzRPaWFpRU9seGFUV1VuTVM1TFNma1ZrY2w0UnRrOEhTSlc4ZWk4dE1ENlUwOFBLNnlLRWpIWFhab3JmdWh3cDc5SndpWlhkZFMwc3R0VlNrZzRQOVlidE9VVm9qT04yNGdRN1JoYVNMZzZ2QVNaR010QmtEdkhlZ2FXRTRRZTJhNE9IOXZNamVlN3hxU293N3hhZmNTQUV6SnJkNVFYRnBPUzYxV25UUlVmUHhXVHlRYmpXMzZRQkNDUnBsSnhxci9LeUg3NjNEalYxTXQxN2JXTWpjWWZicVFFV0VwZzNVc3lzMWJQL1cyYUs3OWdVNG5VN0ZaYjMvV1p1SHlXV2tqWXhYYzFUY2crOWxUZ08wMGpWcHQxcXJSVjRTamlBTmxjeDF3Sks0RTdLTVpLd1ZzbXpFRWtmY3pXdHhxOTk0aXE2a3dMNUFXdDVKZ0NRK01jdEJadllPajdKMGJvaW1WUVJNS0tYLzRHR1ZFUVUwemJCQ1FyQkFSb05wYmVMVmZtRnR4RThmaHc5eUZVdUh0QVJEdVVodmViV2RXM3gzazFwdlQzdXpnd1BsY1JoYjlrdkNua3UrclJhOFlxaGUxRUZlZzVGVG1ubTZNV1FFcUpYbjZ0ME9jWlZNZ3VrWFMvelp0dGVVOG91dTEyYWhyMFMrTDNRMjVaVzJNeklVZWZZZGZxY3dqTHJxd1kvbk8xdWdHemFFeG5ENW5tNXZqREd1R3FqWWNobW9XSElzWXpFS2JhWEZlZGl2bUR2OGU4WGxicWRoM3o1eWdKRDlYL29qeTh1NHFYMVgwdHNRdGNSRVF6VW55Q2NXOWhLUWVkR1ZkUTZ1UkFiekRNL0g3MUowRzd2T2RucmMvMTdOOVAxNnJXQXBvTDY4OUhSOEh5Tm5kZ3FnckE3UTNhaDNHcGpBbkZkQm4vWjFLRUZoSzRTVXRjSW8vQXdoRmlIQ1ZuNkFxNFUwZFlhTkRMUDZ4ZUdjVzcxQzdyWXJIdERYc3dDOGxwNW15RG53VGdCRW8ybTUzMXlSOU1pWkpDYmI1M1lVK0gzL1lHbG1WOVlpWmJ5SXJWOC9JbEppRGk1bjFWaVFURFJxaWFTdFF4QllkQk9sQUxGNEk1V1N5UWwvZ1lHT2FZYW16empiWU10N09OaW03UStpNWhHS3VRNFpoOFVndW9yR3pMWnNnRzI0TU1EQXpJYVhHVjJ3MG9QQnJmWWwyT29NR3ROQm04NG9GVW0xam9RR3FvcXNWOG5ieDBabGsyUzU0OHk1ODJrNThCVnd3T3ZvMEJ3NGNKSXJ3UlpTTFFjS2hWM1RoV1dLS04ySUhoMjgwbS9hYVJ2WEdWWHNrV2dEcmY0a3hJaDB0ZktnWkJZUmpYS0tZY2xydGRWd1pENHVNR2tCWHVXR241c29VVG9xa2ZvQ2hVbnlUdXcwemNKR1orUENPaW9yMGp6c2x4MktvaG9UM2F5RnVtdmswazdtcXF3YldNUEFWVlFQU0dYNk4vdzZEYSsvVE50RUg1SkpueXFnUUdTLzI1QzdtYTJXbXB1aHNQRDh5YUZNTWZMNnB1N3JPVERjYjFNeTdVcUk5bHNnRndEU0owTHRjR0ZnK3ZDRUNjRG5mOHRydTRzYk8zTEFUTFcwZ2xLOWFVWVV6U2Z6Qjl1VWxDNERMMENjQWV6akpnb3NQaGd6VDZtamJQaUZlaHgzMzJzcGVJd3NDVjN0UGdMaVlkMW54WklWbGthalNvWlZZVXpXVWJzRUxGYU5jWWxMZ1ZJTnZkMzcxVjd2clRQSzNremdWcHltT1Rqa2lqQ3ZaMS9CVDYzeTBJNVZ6S29aSS9sK3gxaDVUTG4rT3RobXU2aE9aVldIaEErRkE3S0wvYmRveWE1UlkrK0I1ZWwxU0NkNjZoQ0duYWpXbnhJbHZHRThraE14Y241ck5DWnZybktqRVNPTTdQYzU3bzExVTlRZFl4ZFJ6eFFSczhPZTRrb1dYZUk3TU9tQ0l3S2FxY0cyWVY4YVEyZVZsMXFMUzIrQmdkRzBPU3lnNG1SR2ZlaGxFUEZxRHc4OEM1YUhzVkpsbTRreFJ1WEJOTmRTNnBkeG1wOURWYzlTNkxiSVFqVTFxWGh5bCsxeHR5Y01Wd3daQzkwdS9ZYVg3TWowc2lYczBtZlZ2TmJBdGdHbWVwb3VYRGYzRC96MldPbVRiR09mbzU0cHhSYU9tb3pDNWhsSDRaeEJFVHVRTHpiSnpYelduRkdnV1Q5d2Z6UmhNRzRHNDBSaVZvTmQyL1liYlVRYXR1eHIwV0ZUUzNuWmFOamlWaW5rUTgxUnhOeURoUzdBR0lOOWVmSm5aQWMwRTNZS3pNMUF0a3FzeUJDSlhZZGRJNkJoKzUzRkpKbUNNcVlEdjBwK2NTUGtuU1ZzekEvRzljVFRNd0RrWXlTUEZXOWNNY1NXL09Vc0RMSUNxUGprd285b1E4ZFBsK3ZJc2hlQUdmQU0wb2duay9HT3JxRXBxZkwvZGhvVnNxUGV6TkZGbkRXUlArNFF4bDZaUXFRSkJBa3BWRmtlUHh1SE9PV3hXSDYySFVJMlZESUR0YzNydDJUU1JDNGVZb3I2TnFhYjVhbUZSYnl5ZCtIQzhsOGZVd2VJT3pqYmpIRFJUYW9DR2ZBVzlHMm51Z3QwVTMvbDJpbURCdW42dDRhZCtTdUk4bmFiaUpBeENhbFA4QnMwdWgzTnFtSFpPY2VnbDQxTFpCQytkTTEzRUFRWTNKMW9CRjBSZTBZc3haVUlBMldOV3dMTnZIQnpOLzEwdjB1eVhyRzUyVjBvZ2dCYVRDQTNsWnhTZHJEM0xaL3c1L25zMTB6NmMycTRRb0FvWkZzb0Z5SGtFaFJHTGd0T1YyRFUyZHRSRTNFb0tEMWo4WDRxUkVyWWRjaFU0aXhJd0NsS2QyZGY5cTRob1BaNEFaRG9RM3VMT2h6NkJFTG5yZ0RLQ2JFbVVUbVZMNll3aXBYMmlEN2ttTEFLeW14b0tabUZNdm81a1hpdnR0TDVzOE4rYTRoWWZXMUJiUUVRc2FkT0FEYzYrZTB0Qkh3L09jM2w1aEJBcEo3UDMvVnlpSjlmdHpqd0J0OXcyTTRvMTN6UGdubVI4eDJkNHUwdHMxelVHREF5Z3h6MFUvVzA0MG04dE9Ldktpdnk0UkwvdHZ2ODRXSlpJUVZEWlhBemJ0ZlJOMzNpdnk5eGRYakRmT1RYQUo1NC9XMGlCL3RNbzNZakFLYktKWHlIMTNiK3BIdWE1ZzZpMXlyZVJHZjgwMytXMVZYS1hQaWNHTytwLy93V1BQbTZHc0NLWW45d293bFJKK0FzVXkwWjdRdmxSaVJWcnh1cTNNa1hyYi92NDA5NDdpKyszNjRPY0loTkdEeU5XRk9pSGhlUGdVZ09yQVdyTzdCWmc3SUc3TnRhVU5TUWFkV003bGI3R05Ic1JxTTB2MHBRZG1PdHc3OTBydmNqaGRhWWVua2M4V21CVXFYSHl0UFRmTTR0QWhCL3VpMndBMXVVVHU1YW5EWVJYTUI2bHFEVWdlaWR4U2FxZC96UnBqYVhDMER6UU1RTWtwYkZrU29XMmRhR1lzSGdHd2tiSkRCUGI5UWV1cFhzVjY4OGNadkR1eG83SmtwQXZMa0dqYVhJNW5XV2VZZjBSS0M2WkVDSVAxZHpsOVo2WHl2WWxiVGJPK2h0dnQ3WWtDNzJTaC8zcVZIQnlzQmZzYVdEeVVzN2lFTHpwWmlEVmdKRUgzaHhVRVJQeDA2U2FkVWh1aHFXTE5Ec2Qxamp0Wkx2NXVTOUhKTnVCWEhSRDJrRU5MclJYWWVoMGVSY1VHVmsxQVFSd2FVSXRISzRBY2tVUkM5dDVmcW9sT1lHV3RvZmx3ckVJcnhRdkdsNnEveVZKY0N5Z3hCb1BTYnFMVnl1bjJzV1Y5cUFua1h5cnhJRGtxQmpxSFUvUnNTOWl5cmJ4M3N3eG9GYk9leHl4a1pWMnJINWtsTGJYTU56R3ptMlFuVkxCZFRqaWZRMUlJWkRrb3hESEJkbE1OYUdvTkthbysvWlVBeVZOMkgrcVc5di9WS3hCa3VXeWJObTBJSnBnTUEyVVcrY0cxYmNpQ2FVK0VYVjNmMWM1Vko5RXZmODllajdsVElCS2lKbDFJWDJoWHNuWndibDlOYjAwekE2RndtTFBWbjVmZEZLMEg4RG5JVXFCczJjTW53ZmQ2b3o2TU9mWklER3JLY0ZqUWhHSDl1c3hiTkdVVUNYb24zdXk2YVZHZU1BTFBhSUt0dy9kcmpTUmlIUlNxZXFUZkNGUVhwQnlNSlVxbFBCWlJiaFlzdEI1am1IbHBTN05ab0F3R2VoNDZPOE1EeTBQYzNyRW01L21ta1VJZXdtTjNwbGRqS2FqL3pKeXg0NW5oa2FrUmFZZmpSYjNsQm5vUDk1dW1TdldLVFhrazl2RHp0TkRpZnFKYnNET0VqajV6MHl2UWZMK3N4NngzV2JNYk1IaWd4WjFDL0FpaEhvRkN6Z00vUzZRQUpkMHNFV3UydUxkWmM1Z1NTU1hYdEhrNjlFYTc3d0pRbldMSlIwUUI3dS94YUlXaGdFRWp3WTMvak5rZ3A4MmU0U2U2NERaeU1yb3dzWFpGOVJtVGFlWFFTZFBrQ1Rmd2dsU3UzWHF1TW9UOGoxdkdYMUhtYUVzR2sxNURIUkd3VE9aaVRBZVBIdDRUTUovWk9DaXNVRUpaVnBteXk3S3E5RHdSWmtJcExBelhhbDc4ckFWWjlDQzdLd1ZDVjNnS3NvbGVaTmJ3RTVjMk00YzVVV2lpV21jNWE1YWNqOFYwWnViTHovSm9ldmZaSXE2cVJRNmtsSTFPN0NRd3lrRGZEdjRYZ3VVald5WjhtNGdFeDVhY1hkVzZFd0hxQ0xkazlIaHlwb3Y0ZHNQdjhuZlJWSy92cDJZYTZQOWNYY3VqZWh2MjBramZnQ1poQnpyd0dXck0ycEYyOFRHaEdHMXRZanN4bzIrdGl3aHNXeWg2ckpMU3ZCeko2ZEpObUZvd0hhS3YxbFZtNkxZdzZNVGxOSnhmazc5WDRXTVY0ZmkyeXVKSEVoVzRQejArL3dEODIySGNHeUE1R20xVTRZby9CYU5iNy9waThjMmV0VTNXTTR0b1VpQWNIU2N4T0c4aXhYdnBtWi9Ka1BLWGxiTEs2YUtrcld1ZitwckxsYXBDWHF2aDgxczMxWmphZU1xVmNKdG53Mi9rVi85NkZVRnlCbHg2YVFkNUE0cXFlQUdZdFlvUkxJVDE4RFRUaEdTa09uZVVpODNaeVBZbFB3d2c4SG90dXFQY2lQWlJuYkZoOWtOTlZ3c1V4anhCMzM1VWI2amZXcHF1czZXOXFMMDQyTGswL3B3WEtHL0djc0hZdWk5QWdCUzdKSnIrSzVlRUlvK2RBNllHWFlNdDM5YkFIclVJKy9ibVNwTitzS1dVREFoR3MwdkJjU0N4NEZzcWR0VTJ4djdjZkQrUFpsR3JZKzNNeTF0Q1lvTDU4ei9BYzRCREptWFo3U0l4SGRIZytGNnNNZ0ZoV095WEVockd2R3NDT0lNUFpnaURHMU1CZzFVOVVsSUpKNFVoWkZBaUJibXN0YmZhRlpQbzVpN1B2VFNaUXZFN3Y5Mnl6L2kwWFRycG54VVZyNVNHam5UOW1leWd0OVFKWDllTUhOempiR3J6U3dTczdZUFZyakZNSHhpTTNLZDJoVzZlRTlUcGNNRTAxTTdKT21HT05HR2xMRHkxTW9vUmFselYyKzU4Q25oSktjR3h3Kzk3SFpxZHduU3gxcHNYK2lFVVRVd2wwY1hiRkdWUko3YXlJWmZ5TXl6RFpuZlJqT2MxM0lXZTdTYXFFdWlNenAvaUR0d25PaTFoL25mY0VvbXAxMFc4N2tMenNaS0JyNldsQWwvWjV6Ui9Wc3ZSZkhVaFJjYmg3bDR1NUdCWnBCM2drNzl2WjI0b3dlSFNzeExhWEl1N25JRldKMWJtZGhuTnNRNUFWVzZncDY2Wm5PWjVOcHN6NkR1MlM5Yms4K2lSMDRtYzIyVjNPbiswbThNUVQrSmNkaUhTYlBZZldOTEM2d1JHdFF5RVYxczVHYlBBc2lHNDQzTDRROE93LzhQaHJnZWF0UVRJOTNvcGoyZk5uaGlkdkxMTlVWOEJWWm4wQnNEbWZTUHhuTncrMUx6bkNpZnJvTEN1ajR0aWN5amg1WmxXckNzdGh2NVF4ZDdBN1h4MHgxWUpWVUgwTDNZZkRQVU5nTVkrTEsrWTkvUEl2anZObC84b0pxRVdWS3VvRU1sUmhaK1o0Y1UvUXlGdklUTFJSdVZQZXlabHV5K3h3empSMnJsenhZWFdncVhkZ3BKMU9LbjRselR5RkZxZHl1T0FnOFJwbzFldlVSNk9iY0kvdVQrNzNoNmVLK3MwV3BHZitSUUJCWklFNWRvbmFiNVhYdGhDMHNISENNS0MrdFM1Q3lNcnJhVUZHQTFPYTBIUWFXY2pEeGN2NHQvbFFDOFhTU3dYQVZhWFVuZ2lHTXlzNDVsTEUvMU5kK3hhQjN5cXdWZmFpcHd5TnBOdEN3bmZtd3ExWTZ1a3BhcmZ2ZzBaMUt1S0JWNmE1V2tGWlRNVDdjekFyc0xOdFdFQkQrNXJHVmU2RWxiM05CaEZaTlRrWjlEc1BuY3kvdnhBWmsxOXlrYW40SlMvZE1jNU1WRkZpZW9mUDk2cTZkbStzeTFUclRtUFdxSm5ZbXdVRDNzZzU3czIrbnkxNEEzbzlzSGgzbnplczFOcFVLempiL1QrcUVya3NtMWg1ckJpK1J1TTJuaUZvZHh1M0tRWGZoZXRORUhqQWE5SFk1WTVjQ0ZDaVBJZ3dacmdnbWpKSlFvM1J3K2RId1ZiVjdSUWR6WjNpN1c4UEl2YVErZFVJUGxhU1NJUnBkNG9jdTM0VWgzdDNmR3NKTUI0cEdWTTFQNnlGOVJuYzJEQ3hsaWx2SCsrQTdyR1hlUzdoaXZEWWVjbFNWWFh3Yk0rSnRuSnBCRXRocm9BZHl1SDdWNVRObU1hUERMYmoxZFdtdnZGd2hvNG5scVhUa0dNTnRrbFJINjlleVMvUTVtZmhqc1RVRUFNcEFFMW5CVUJtMXRXbGZrVUFkWFFMQlZ5a3pqNWQyM2N6aUxFbG85TE8vYWdXSkg0RWFtaFZXTC9CRjBGQVRha0lOUWt4UnR5M1ZGL3VtbDJXQTBVT0VHMjM4MUhYeHZFNXViYTQ5eTlKdUgyMENqRkxjc2YzWXoxN1o4Tm9GNStLeVZ0R2lLK3owcXppek5uUjJvZkd2M0czMTNYSUFRNHUyY1JKSVhaY2tRcmVqb291Nlc0T2Fnb3paNlhyQXkyTllJOEZ6bGpCaFBwS3NwUWRMQTU2d3dSbkxkWDdYc3hyZjVTdXlwTkVNVWQyQVA3NnJvT1R0bGRJOXkwa3RtMWp5Y05sak8xNFRNbVZ1S3ZYZ3ZUTXdNaVJ0WDNFU3YxazJqQVpsbHREMUN6UkU1Qmp6b0QvQ2UrSW1Xc0ZRVXU1TUV5VHNBU3g3dTk5akZUYlVSc0FvZndRYnIrSk9EMFU3MHF1dzhpMWpUVjVIZlQ5cGNqbFQvSnV4SFJ2NDRJaGZlY3dKeHhVOGh2bUFaSFhTaE50eUkyZGVwdllId0ZCNG9sRkdYU1VyTmswdExrV1NaaGVrZzI2TElyZkF5ZVRnaTV1L2VYMEFBM3dmaU5qUmZtMkRyQktMZkRHUHZudFRtUkE4VUhsVnJiUGV1NGNpcmgxV2NGTGo3M3MrNUF3Wi81VUxKMUdQc25EcFhkTkZGSHhNdWdkTDNpWHdUYlpadFd5em5GY1Q4U2pLL3dUZHU1RVk0OEgrVHltL0RtUHRkdmVzc2d3bks5Rmx0NFZ2YWtaQk1ES3FSSmswcjFzcGV0KzIzdER5RjhhcmtCTTBoamVRL2dwUWtBcGhEdTQ4eVZTZUhtZFRtQk81L25kRFQzcFl0RGxCKzJFNDIvTlNTK0lmN2FsY3dGL29PSmhDd3VGcnRUUE1FUEdqWitwOEN0ZlNRc1pkbitWWCtiM0FxWGhCMkhiVC9PTmtzaHlDRE83dEdDcDFmRHdIMHFvc1hNYmNuN0xzOCtId0lRaWwyWUVMOHF3cDFwSU5lRzZ2ZGpDbmNMb3hBUFAyVVNuZVROVmVDMFBsRm0vTXJsbDI3MTZpU0hoWkthb3k3OUlPQmFab0k2Z0FnblhZZlhQamZMN2czdmkxS0Q4WTVqMWdCZUQ3aUMxZkF6NnlLK0hiQm9RZ2l3Y0lOVUFFeEV6VWYxT04xK1BzRGpobUJuV0h0YlM2elJtd016QW43TVd6OGdzcUtoMUcxNEI4K001SWp1Um1ieitNcjFFNC9wZDBBdFJGZisvV2h3cDBZc1g1UU43QWcxQnRpdkY5NDRrZGkrdVEwWmNKLzBsdlhwZ0ZZRldmemxUT29JelNKckx3aGRWQ3VLSWcvSUpZaktMb1NMSGxSbDhjT1hUK25TNjgrTXBXb2lQOVNrRHMzTnJlSkdwQ0dlZE13ZDRXQS9IMXE5aDE0MExpR1oxM3NrTU9hZDV1endnK1FiSlhoKytRdHN1VUhoc1RqRDhjckJBR2huWGk2ZzhrZUI1cGQrbVlxcHRKYmowOTlkbjRLd2d6bDduUmtjR2Z4WHhkTFUvQXJmVm5TUFRpUmFsMGNDQTdCTFRTeHZlQ2txUmZHcFYxR01sb3N3N05yVmp5SEpLOEl1b21VeXpGbEpFY3pQOXAzTGFCRmZtV0J3WitMRUVZR2krYXlHVUJ0dFNoWVZFaHVidHh4QXJNNjlDUTlLNDZzMloxbHBlanVYbkZ6RFZjQ3pwUWhqcjY2T000TWVVbWM0cW5FTnQxYnczUldMS3Y5RkJKNGZVL3Y5UE16TExZT1drb2JmY1BXM0VoNG4wWjRGZ2JWT1hmMkQ2QkV3WVNGVmJua3puVkMyMFBRMkxZcTdBNFJyWVo1VnBuTGliV0VLM1JEUjZUcWRSR2tGWU5tcXJHYkJNQ3d3bjBBbnEyRVljSy9MdzNIaHJ1eEZZUVE4R2Z4MkJQSkpDTWUvM3V1azBOY0UxZzR3SU01NEpodkszTE9QeG5aY09BZnFZQm1WYm13SGJ6Sm82Rkl0aTBUbVV5ZTljazRHbythZ0x4SFU5bittTzZ1bUdkeE9aN1BOT1psS0syWE1SRFhGbXBaVndXOHk4Z3UrckErUUh0Rnk4cmFQZEZGV0RVTHVocXJFZ0VodUhZMW1PRTJ3bGcwS2JmRGxaOHV5ZXBsSDNQVjZJYncvU3FLTFlyYjN5U0R0cEFTREVja3dPRDZWK2FPdERsZzZPU3JTVWwvaUJiTElIYlhMOWpDbG9ZQ0pEWGZjdXhvNkNhTTU5bVVJVGt0ZGltNmc3d1Y0cndxWmkyUk1ua0UvMDBTeDV6UkRyWE5ZckExY3hpWnVvZzBSZnZ4bjByWGJ1NWZqb2dUd0MvV3FZTzNiQlJjNzRia0xCVEJJTFgrcHRGTWlBVzVod3gxY0JOTnkrTG1Jdmt2V0pmNEk0c3hWZUtNUjVkbjdyM2tGY25NV2RDd2xqcHY1eDZVc1huc1Z4NHhEc3ExUUZ1YmZndXMvTEx2SkxXV0lrRFlQSUZjTG9IN1ZVL3daYUtLNURGYlA5ejdDV0UwZXNSTW42dEg4OWlsQ3hlYjdXTWNsTTZ1Rlh3S1ZsWnB4V3pYREZDQm9IVUtmOTQxelc3S2ZjUnVJUUhhcmdVTWtLNU15elB1aWZMNTZQSXgwTFBtZGFkVFUyV0ZsMzRxZm1sNHN1UFhIVDczUkNJM1VXQmxlWWd0TEwySi9MS2VIR3JVR2dXL2ZvUDIvMCsvWEMvTzM3d0pwbXVLWWlyalhpUEpPRnRRU25CWE5yUllIR0VxOHdsQlNacllQOU1zYnFVZTNlbkhabmVkYjdEYSs5UzZSUmFUZmE2WWRRYURFZTBreGQ2KzJtLzIzSkc1TGEra3gvTXpRdVBoUE1xYXRaejBtVm96djBIU2dBbHV2OVJHN3N4OWdjTVlBZ0U2TDZ1YzZ1MlRPak02WkFueUlkTjc5VWtJaHowVkpuakNUQzFYdW5mb3ZwUjg1Y01zU2RLMUp5ZWtxTmdDcHo2YnpJbUhvWkdLRGgrWG5yVnBldVpQVysvS3gxY1RZYi9MLzNRTnJvaGNMRmlUMUl5K2NZK0xuSEtYZ0I3cDdCM21qT3JGRVI0Q1JhUkpFRDlkYlREUWswdGRpaGljTno5US8xNjB6Z2ZGN0pTNHdxaUJ2R3NjWDVGL3VkTXNPVUhsSmY4VTg4VE1QNnNhM2VDYjRNZndMd04ycnBOdXpwbmJGRDR5RndLSVh0RExlZjg3RUM1WC9vbnJWL2MzL09wdCsrTER2NkpRNm9XeEE5SEZqcE1qVlU2ZDFXUWxsYXVJN2xrbkZIRGczL0RXNDhXVm1xK3ZTQXJsenIvOGs5WEJLR1hTWEdnUlVUeW1ESHV0MGxGMS9teGpzd05qMk5GK3MvekVkV2k2MFJSWHhtc21TRnFxODkySEdVckRMbG94b0dWWHVUM3N5VWVPZ3NRQ3RQc2lVU0hxWHBycXBuVXZkaEZyakN1N2NHMkw2WVVscmxqL0Irc3pKWFJXamczZEZvWERFMzhKdGpEaDJJTzdLR1FkL0FKT3p6WDI3TnlKcjJMclpTaDMrWVZmNkdsUWdManhTbXNLNDNrMHRNN3pKSzBDL1QvYzBNNTJSY2NXVlpJeG1OZGlXNWUzWEhWelRGSEl6YThRVlV6eCtXZ29ZUTZXVTVhVTlaSzhXL015ZHlkc1BTaHBBREhEL002K3NwMnNCUHBLckgxcS9IdU1GWHJBdG40bkJoZzM0NDFjdnd1bzdCdTFTTlpRU2FySG9pVnFNRVhEQ2JJZi8zakxCWjZNS3k2M2VtejhHaWFJbnlCSGZYSU1sdU1XNlpTSnBvVnc2RzRYZHdEZ1o2TEVLZ0Fqbm91dzRLeVJ6SXNhYTJqMFJJSVFFVm1mNWplR2ZYS1NUL1VJMElQMy9Vc2E1WE1pVEpOQVMxYTJuUXd1RXM4T0dvTFREMFRSN1Z6ZG5UbGkzRnNGNTNaOCsrSHhuL0JSeWg4VUNLL0hYREFNQXB6djJFbHlxNlhyN2o1SXlMQ1g5aVhwOXZBbTZlcTQ0WlZRRUpaUUdaQU45ekNYSDNOWnJNL2FySXBtNW9TaE5PdzRMNnhBK2NtWkdHVWRpOGRodDI1Y05iRmRQSnp0TlNiV21FMEVKLzZHM1BiWEpVMEtzdEdLbXJXRmxSSDVjRDRGd1lwWmV2WFBjc3UzUk9nSC9KRDRpVDgzb3FTK09ocUFBOC8vT0poYnFuYytlRXlCaU43TFdtREJqVGRqbHA0bTFHcUtzOFNTYVVLL2JIdzJGVVFxcEdac2JLQ24xTHJGWXl0MTM1SjZjdjhZdDg5S1dhVGdDa1hVWm1JeEdhcVdEay9aQ0V5bU9LdUpaM3BZandGY290T2owb1pBMVJvR2FDaSs4MHlGaUtBNHpVK21kQ2ZDaGxjK3g1YWRET1FTZlM1SnBCdFpnTW1mTlhrYmJYb3hlR1hpRjV0SjRXSXNCRDRWUXorM2VSUjdvanJ0aXFwNzdsbjRzeFRqQTdwazVSajg4bVhSSE9mb1J0NFBmSktiOWNkM1REYnpjcjd4U1JNUmQ3YVlsbld1WUF1TTFlbVptdGd2VUdjZ1JtNUdNck5XOTdXTENhd1VvcnZ6UEgxeGxET1pKWDBZV0VzUURhZFJOYUs0dERrRnhVZWo0aHpTMW8xc3M1VkJNUm45TElyWkVjcDRIaVdOU0JGUFhudEVPaWNRK3M0ck5RLzVmYUI4bTgwOU13TzZiVkhrRm9va2xzbmZzY2ZxRGtPTHplamFGNkQwSjdQOHV1aVc5Ry9vU2ZJeStudVZVdkpFMFRuVExSWnMvQ28rWTcrckphOSt5SUgzMDloV3NvMmFmRjc2Z3cya1cwTjlkeXJDcHdoV2syaWJUZzVrZDVpcjNZeUhUcGpicWNPM2xIL2gzRDdSL25qamkraHNPTE84bm1kbmNRTEdhWWFUUUo4dmdNU3h4M1FaMHhkZUZYemQwREFucVNvNE1jcWp3bW5mMWp5a0hvMXNDWU03OWlMRFNCbEZjcG0yOFlKb1RQaFFtYmp0c2lVaTI2c25DYVU5NnNFbVpaLzYzaEN4cmJiUlc5U0duZzhLZUxwSDRJWCtqVTdxODBMSUpvRHVpWWpzUy9ObC9nMVdwK3hVemtISkV0TTRaRDlOTG9ISTdNVEIxTVFianJOd3B5VndNMXcxOUx0a2FNUUtzUWNZOWZmdWdzZTBHMkk4c014MW9QbmpFY2FMTFVDbWZnZTRFRGNlWU9PSEQ4Wm50UU5rSUpoaHp5L1dEa205K2VHNVlneDBtK2hnSCtEY3pNNDM5Yld0RE1ObC9mWnRnNXhRRis1WjBPZlRBd1NDOEFwMHpWSExOMHpYbTVjbnpZOUZaUG5XY0pCNGNvNk5BWUFIM0lpZk1HU2gyWnFkNCtWVk4wSjcxNmpQRVpId0FKbCtCdDZDdXhjRERlTkExVEo5N2J0ZUFFMGdOS1REN05pQWhtZlFkK2w1MzJPcTkrWnBhbWNmR1NWVVhVNnlQbUNGaTY0Y1loVDhyanN5blZNMGNWR25MYWlvV0NTZHBqSFVrQ0hNMHk5bEkzczEyZ3lEdC9xcmthUkIrYkhLWlo3TFRicXpLWGJ5Rm1KUlRybU10MVZpZE5iQk5Da3A3ejdqY1BWY2s2dDZoK210aHBEc0MwcG1GS1k5cWxZLzMzbGNTQ3pEbXo2Ny9raWt1UTkzRThoT0xMeW5ObVlRT3FWYUdmZ0V4UW5tYmxVOHh4aExUMDRqVk1UYjMvcVB5RE8yTW9RYXE4dUNTdWdRVW1IeHQyUFRLN08vQ1ZTbWh6UHNOb1pqYmgwdmFKdjBoVDVXbEYyMlZjSzl3K1g2a290YVpUNVpsQkJuckJGM0hvQld0M09mYUtxQUkyZ29vbG1ZczFpanFIK0NvalpQOFBNcWNlOHNvcWVVenBnQWQwdXNUMlRXRTJTRitOQjNuZUMxRWZzQUQ4cDE5Q2k4aVFmOEliQkQ1eUN6NmFkdXlHc3lkRWVQMUdQeGpQTUtickdRbnJQb1dBSUZxMSt0VTM4Y3l1clh2cWJTYlJUMndlM1lmcGE4UFlpMTJ0R3g5S2daSFVJcFZDNnJyZ2VyTnd3RzBBVkQzSlNjTk02VnZ3Z2s1OGozRzlhNmJaNk15V3lKRkwzRGd4V1ZzQkRVUXRWdnB2RWtxZnlPWDFRL1dMOFo0VFNDOEpjWFIxSkRpWkVodkU2by9XWUlmcTlFMy8wKzRLY2d5UkljS0UxbUd0a243ZnE4cjROSndKOGh2dU5rTXJ0YVIrU3JyaWt4SU5LQ0Q4RE53YUo4dS9QVThabHozWFB6RlFpYmF5dTlrdFBXeUpqNDY0aE54Z2FiYThzOXRXbGgrNEQzZVgrdm16eXlnNnFUTTkyRml4L2VsckFuQmV3N0dvSkFVcGx4STMyMlQ3RUVBck9QRlF6QmJSR3V3eHo5NnI2TUtBaEF5VG5LNUhabDVUeVp3elU4NURJZHk0ZEQ4MFVTQUVscTBaQUc5eDlMR2JKQXBSVFJiNEZ6d3l2b0h2UXhVdEl6N21QTC9Ta1R4WndreEVCN3FkcjQ4RkpqYXJyVWloOGVIUVVKUHZUYzhuUVlZOHJSdXBxQ1ByT213NkIrTUtWWVdvMk52ZUdIN3poci9mT1dPbVZJNmdEMkdxVi9pTklVZ2dlNll2OEd4bzYvR0o5SHhQTzNMKzFNNmI0V3BNVHNDZnlCemg5OG9HcXByY2Jhai9ybjNUakNwRWYyMmRCcHpwZ01lK1JJMFRSa2ZJbWtPaXUvVVRKMU9tZXZ5UUdDRUQ5QjRVNkJzSE1GdVo4Y3lIMUIyWmFSbWlRdkZ4VklzeHJEQ2xTVkdCMDlBbXlzQUdXVzlIenZ1YlBTL1RMN1daVTl0UVR5Y0VhdVRZRE5PTVB5NXhWTFRicTNFYm54TGJ5RGpDQkl2UWR3WWY4dzRaMmhzcCtCRnUwck5tVDA5TlA4SmpwaXlXaWpncHQxWHpreVZyZ1NsbWUyWTg1TkVCVWc4MzRwaXl4YmYwNmNEdWs5L2RFVlJvRXJuTUxTZW5pZFFETUhuemQxRzZMclJDNmxTaFkwSkNLNVlrRm1mK040MmcySVlSN0dkWUQvQm9OVHFEZHFySGRka200eTlqRC9DVHF1cFRFQzlENXc2eXQvdlY0QWpLNGtsTU9TRHBCUjhzVlBHS3hxdm9SWUZGRlM5b1ZDUXhuWU16NmZFSFlRMTJhUHVmdEFwSlc5azg1cURKZHE2eU5tNm9STEhXNDhKNEQ2aERoWDJUWEdOUml6ZFpKMm9vSXVoNjFvMnpyVGd4T2NpbWdMejljSlNzMHREMnMrcEc1bUV5UVJrSEowL3dFT3p4TytBYjFSaWhEM0ZQT3lKSHV4TTRjY2ZiZUwyaFVIeEU3NUJOQmt3WlNwYWlNM3plaEgwTVF6MjBnSmNOYlhTNGl0QnZXeUhJV1BrRzdVT0xJRjJTVnZEVkZUQ1YzVzRUT0w2R1pUUXdhSEt0RVdxSmRwKy9xWTNFWkJtb1Y3K2Z0dG4ybHYzWG9GeFphZy9iazNjaXNUUmpmRC91b1JveVIzcjFTNzVKUnh5TXRCMWI3L0ZwaHVDOWp5N2JyYi9sYm9sZEhrSUxSK3d3L0tCWGJjOVpyMlVxWkJDQ3lQbHFreWtBM2FGdjhucWQ4TEM2WXZxZ1NJaGdDNHdLaFlCMTdOd2NBS2pXSndyRTVYWGc5TnFZVDErWDBUVktHTEJEQmhBVHA0cVJWU3RibTUreHhDcnhTUEtMODNwVEMzNXV6ZEZwa0lUaUFnZ0pIM3ZOQVdYaFE1Tk5UKzVhSm8xdjluVTRpUEpzQ3ZuRDhZN2tOMU9nRXg0MDFwanQ1Nkk0OFUvbmszSmV5dHRSKzE2NDNQYWt1ZlZBWXFqbmxkRmF3WjVMUVRmbFIxZEdSNURMb2szdGhhaGdyczVMd29oZDRRVkZ5d25IMENValJFYU1RQjZZOTR4S3pKRmJqL1dXUVFKY0hkMDRydVRqVlEvZEdaQ1llRjhSV2JEQXV5R2RhZkdEMHY1ejJHRGJiWXk0M3dtWlNlSERweXBnWGt4OFhIOHVOR3BMZ2F6K2ZSckREOTVSU1FtUGk1WUE2eHN3OHdYdlBybG5SSi80TEt5Q2NkVlNjQWpPK0NhNGE1RGtLMFdxQW4rcTJENTZ4VmVwZ08vRFJ3U0Fyck1STGlNZWxWenZxVGJoaWkxVHVCNkZJK01vVFE5YnpzMGxiTE9QeERTOWxxbnhPUDNjaUtXd2h0OUE3YWdiampxdVlrNGxCaE1MbmdON2J2Rmp6Y0ZOcHBaVytGZUFvQXVwWFhJaVUzVXFVTTY3aG1hVGNPWXhydzRGc0NGeEg4T0gzM0ZRVGdUaDBmS0xJeXpobFpLaFZnbTZwdldnNWlUM2V1VmFxcmw0bEw1eDNjOTZKVXUzeEh0blcwdnJGVUF6c29RWFI0cjZId1M4Vk1FRGZXOXJYU1FnUkNFbThZMWN1TGxGVUZ2MHY5Q3FUeFZmMDVWazA1eG5XRlpaeW4rZ0tSZVF6czV1d2JubC9OM0V3VmJRckFZeTVpdGJWQVdFeG4rNDNsd3BxT2taazJMUjAwWFpKTDBuT0lScUJvMTltWlBhd25ldlgyeC82VlNYajU4MGM4Y0tGWXNjVDl2RUZmZnRTZDBpUVBOSEhucjlEaWhiRVlHVWFJdksyWnFlWWFacEhWcDdsOGxHTVBldlUzMnJjS2w3ejA2SUk4SGNBSkEzYkJlSVJ6a0g3SXdURG5GWTNIVXhxN2FWOVlQRXA4MnQ0RXIrSGpUNWk1alBIQSs4QVUvRFNueTF5Qzc1RjlZaTlseXYyV3hKMlIvWGY1dlJyZGhDeVY5QVRVcGRWQTEwTTBHV3YyZ0FVZzhJS1hZVUk5eFlQc0hXMWxHUXR1NUF4cHpsUkswaUxYVmV2OGVHRE02VTZhWDQxQlJZTDZIOEtTNmtncWJNdlVtTk5IK3grWnMxTGlpdUxTN01jQ043Ui94ZnFxNGUzbWNCdGNqTXd3OThHTEVsZXV5dDdwa2R5bEV2aXJvY1NDcmlQSnJBK0NBc1l0Y0N2UGtLWU5uNmVENkRZaVNpZGlyWVJ2dW5qejUrWUk4RFU4ODlaZS9rZ0s2N0NjZW1valVHOXdpVkw3S0xqQmgrcjFCQ1RSaDUwOVVsTVJKNDIzVm9CeGVSczRGVnJOS0hnWm9DZG9ZV25IYUc3Z3FuRldEVlUvQWFvWFhRblJiQXl1SXNCMWkzSlhvbDFvSHREdnFnL0pFWEQ4aFRYcjJnOG1TSjM3SUgzY0x5b3hmR0lPOFdHMEJXZTQxMlBhcy80OE1SY205Z0N6R1NmcVVLNnNXZ1A0Nk9FTjU4eDBUb1NOVW83S2hLT2FITjRvbTNpQU9lRGx2MmhyVnBGM01qalNJMWhiRTZpUjlnejd5YTU0N0NvZkFjL2JJYnoycHNmbisvSXdONC9RTmtNMGNOL0lxL3UyZVlVQXJtN0N0WnBXZmxtUTlKdlZaeTJiUWg1MkVrNzIremRHREV6c1JJaFdHZUt5SStmeDFtdS9jYnhyWGY4YUNyVDd0OWNwYVpCVXdqZkE2ZnpWVWtpZXVML3ZIR0FFdkhEckxPemdNYTB1T3FiY01McUNMQzJZdkRJcU1uS0w5ZDJKbitwYkYxbGR4T0pqOG1nejNsSkE4d2pHMDN4TVZxU1VkaEFEOCsySWFqVy94K1p1b2dzc3E4M2RQQXREa1VLYWpwTFZ0TTI0cko1bUNLVVBHeVhRc2RuOGdOVDJFRG9qckhQdXF1aGJISmZteTVzTnhuYy9iK2VCY01zcTV0N0tsWWpZQXgrUzd6UkJUOHFodDk3UEQ3Tm15QzA2bi9sblU4aHJzK0VSdHRmYXdaZ3VqcFhzdHB1SUhOV1p2c29RajJTZGVja2d4aXZHTUNpaVg0cEVmaXRmNFZLWVdlaHd5NnVzdFF0am5ZN2FDaGh6a24ycitSSFBrb0lBNGhsSTFmcE9yRUtlYVJ3ckNNYjBiR29JYWxPWFZvOXZPWTZRbWNnTTR6NmlvdzNyL2x5SWtOdnE4RWhKWi9QVlJtVmxDVXBRSTc5VHYyV0tLVFV6alRaRmduWEJZaUt0cEhPcURxTjBLRUxwUXNZSTlhMkFPZ0tyVlBPS2VJREFLU0hWTlFnYlNBVjdNREtTTlZ5ZGtNUjNId2c0TWFrMDJVK3J2RFJ0UUJQbUx3OUJEdTEvVUpPaERQVkhmU09nemY2Y0x6U3lkSjRJRk1DM0w4VDBiZ1h0SE9jSXBkWXBwaksxbEViZDFMZHJ5RU9PL3cxRTBxY3BldkNBUHJyQ1NTdEFwTDE4bzBmd2tobUwxckNRVThPbE4wUVA4eGw4YWxQRGw2WmRETVBvZGZwUkp1ZXlsaVRKSDY1TnlOT0ZyeXorQXBqYXRXNmRJTzVFakh2K2V4TnBOY1I5d2JlTFJwQWJaSWVvRGVxRk55RWYzVldaOHA4SWNiaGF0U0xlMlpGRlJNeUd1RlhBMzFIWHhGbDdraGRVRkdnK0VGWmVPVnpLZWFpSVdPOGtib05SenZsTGp1WnpqT0JidEdmcm42cnQ5b1EzR0wyL2o1N3dKZXArYXVxLzlFV1F5QVRRL2N0WmhWdUlyWUtZVFQvZVlRTURwZkpIUjJ0OVV5UVFLVFNjSVcwazJhNDF2cVNjaTE4L1FFakx1dVhvQVNmZlRVdTg0L2MzMkh5elhhUEhYL0crUnBrUS9VNEcvVUpNVUJEWkRhdE00QXNmT2dXbkpNTStHTDAvMUp2OXl1aEVaZGpoY0hVeG10OHhzSGVhT1RZdEVyNTU0OUZ6WVM2aE1obW84em9ZL2QrbkVFWjdLNVpXY1Q0VFYzelNxdUhjaG5rNFZuOFMvcWh2OVl1L2NZMS9xbFRvTDJ1ZHFockhsNkhyVFJ3cnFJcXVjODJFaXJISEhzRGdiZGlNWVlTQnJNdldwNGlaRW5UZmQvNTJpeVdzYVVFWkJxazRoeS9VRXV6VnpTV0dXNllTc0FmU0pFOUNPLzBKMGhuRTBWVXBBSTBxNXladU9UdU5vR0RQQTAwejRHZUp2akw4RDJ3MmFIRlRvY1lKTWJhZHd1UFpud3pEQUEwdm85SWQvRVAwQmQ1WlNlazZaZFkyeFQxUURkSHd4YmphT3RxUXZ6Ly9mdlM0eEpDREZybkYzbzdaQXF3cWFUcCttS1I2M2NSZWxzWklJOGRLbjJQL2drOVV2SEU4bEt1cHJZckpOeWpZRFgwdm9za0gxYURyL3RMOHhpbkV2Uk5ycXp4ZVFyN2IrcTNjRENkbjRmVnFtN2oyV3VEZ3Z0aGcwbko0ckl3ajNOZDdSY2FYbXE4Y2dXc2FmWFJkNXl2d3JaODBSOStUWW9adndwcS9WVFRZUHdHVGNnUnRtWlFBSmRnbUNTakVCQ2diTDA1cFZHOVNtM2JUYlN6dkdpaVZ1VDlzTmZEM2xrb05TMHgrbW0zUTlRaWNJRHFMVEp1VVhsaUNTdGRkOWRuK0l0emVFRkVTZWJTL21idW5EQ08zUkNua0ErbjFxL3dVTkJNOXBOL1JCVncrWkxWbDF2SUVKWkZCeWxhOE54TkxaSlBGT1pTS3JPNU51WTErWnMvUXhTaWE0bWhSSHEzNkplTXljVHpFL1dSbzgydjF0UktvK0lIYTRrUzlvQ0c0V1Fab29vM215L0hzTU9UVHUvQTloZnc4ZG5yVjV2TkRNQzRBck5hYWNTbTJVd005ZE5pKzNyTnFtaEczcDVMTHlwc0p4Qm1VbldldW8xeVVNb2JqVEcwOXhGTVNlNkMzZlZIYTJ4dnRvMDltTU1IUzZoTkIrTllpaHlhTWRQcW9jT3grTEpLVkRrakxCSk9tQjVlbDZ3SHpVNzJyb0xvaW94bUFram5Ia1k1cnk4MkxiYlVTUjJLOHpzTE1VYkxnNTFIRitzNmxwNDdJNmdrb2wvQy9BY2lidVV2RXRKTHJCRXEvak85TzlWb2pOdGJsMjhkZi8wVWJzZ1RDeXpHcitJZzRQd0JtdFBVKzVXRVM4Q2pxRFZMUHhxc1c0UVNzaHpESk1TOG4wVTYxVXNhd2MvVGIvVm13SktuZXJONFY1a0RTT3JndmpzdS8rWTZLSWFVM01zN05DRWpBcUxHNU4wNEZPbktjaDRKeXpOZGlZK0t3L3dwdmRMUHhCVkJ3RGNZZmJtRUVlTEtUa2x6dTNyajk5aUFKakdWTUFmRE5TclhVSlRVNnI0VnhGU1lvY3ptQThwWE96QlNvanZaOVNwVlZiTmxEZzJTYm10SzNJNy9vZ3VUb1Q0cGVCYXBMQ0JtM3haTXpMODdidG5TVWwzMG1NNlRGSW4zYmdYaWxjQ051aDBLcHNyOEhTYmN1RUpOa3Njc2dPeC9VZEdySWxpRmtYMUlRYmxmQ05RVlZKbm9SM095SDJkK21SNVZhOFJGKzFYTXBIbG93QkR6anNBY0dtL2dXSXVwd2VZY3Z1V0QyNDRreFVoUXRQaVJmOUtjd0lSSmdzRXNaSVUvV1VFa1lnUTkwMEtaWnp2REl2emJZNHFtcWIzU250ZldrblZaMXJXdVBLTzY1ZTlmNHlYZTZCL3ZvbUw1RDVyQ0xiZ1V6SEhVVXlKQ2xiVXpON0VGRC9XVW9vMXRKY1VqdHdKVkwxZmYvR2pvb1p1RWd3Ylo2eEozdFlJQ2hsUHRDb1doNUFSekI5ZUN1dTEzOEkrd0hDMWY3TnBVMjZLYzRlb0k1YzBuL1k3WlRTdlBGSVNxZ1FxUVhGL3I4WEtuV3BlUThFQ21nOGFFakZOenFvSkVtZ0JWRE5Gc3BPN1FsOFAxcHFGVGt4empzSEFaMURraHVlQUNzTUJCTmg5R2VPMXJNMjR3aGw1Tld5OENHRU44M2d6WE1rU3VDeFZ5Y3JTNW5tUjM2WlYzcEY5aGswOG91MDV3b1VyWnVLeFVTMzMvUVEyaHpBRmtlT3VMN1RMSUQ4K2FnVjFVdmlmQlIwTEwwWEtzUzdaRXFJU3BwWjlTWjFJY3R1MllKSThEVzJMWmdCSHM0U3ZSd2k4dXI3YVdmWVM5RnVQSW9LZXd6UWxSbnpKT2RqTlZoRWt6dS8wOXk2cUVCZ09VcmRkRmlVL005dW1RM1lFR0xodnpWeU1kMUlWWk9lSk5BdE1MdVFsY2E5ZHJFVTlpRFRwdXYxUkNXRkZITEJkZ3NVck9PRVU2Z09vakFvZEJOc3hmSWVKSWNrdGtNS280S282Y3F3NTkwYVJVL0VOclMrbXVLRmg1Zms2c0wzbXNTZ3ZwYTcwdmxsZ1NIU1BZRzBnZUVBVW91c1V3MFRKaWpsVmhidlpSVkRtK0cxMDlIaTFFamJaM2RaSWNDSnk1OFFIUWNnQU5xRXUvRWlIN0RYTEtOcnVXNklqeDJac0c0RkxxNWVXSVlNTmdSR3hpZUZvdWtWZHMwUklNRGVGUjFSamgxR2ppOHdINHYvYWtGTnVBaVlOaENqSE9wblBtRjc5a2N5dzMxVjErQnM2TnQwWFdQT2tCS1gwWUdtR3lmTzFKYWE0SXRJU3RiTXJZZi9OZlJsb3RhUlBUcnI2R3FaLzByT0NjMjRPaDA1NGNHei9BU1pyV0grdTZhcTBXWllQbTNma2FjTUtJVlRVL3pNdW1KMmJqcytyZ0o4OWVDcFRxUzdURFk4MnA1d29EbmZKcEJ2ZGxnZE9LTksrQkcydDl5cEszM1dzVWVIRC9DSlc1M2lueG83bEVJNUV1ZEVLMUR0bXl2RUk3N2pOUWZ2Z0RRMWd2Y2NoamtVWGpPY0taeVpEbFhXVzdiWEQvMkdCam9tNTNOWk9vWkRFZFdBT3Q2QzMzMmIzU2tUMWpvMjRMRjVHZWZLY1d4a3FZcjByUFpyaTJ4NlJtUEFQc0RIVlNXSS92ZkZYV2ovZndJYkNxQ21qQ3JXajlIeXlvaFdmMVFCTWV5dGhkVlVZUythNFlQUy94NExzeFRlK2tmVUNvdlhhMG4rT1ZicDh1MktOTDM1dmFmbHg1QUF0L1NFV2lqYmU1QklLM0UyeGZKVDNhL0IyMWF3R1VSYjlzSUZ0Si9ydUg1L0U3b2hCUk9BL3Y4MHloeUtQSmtDYlVRRzZaNTE1TnplMmZ5OCtYampSdFZEYW94dWRXeHQ0N2hyY3lzODFOZkhRTWxoaU9HYzVjaXBHMTZMUW04amJNN09jY1YvVEpGdmN5UTg2R1JPM1lEalZiUS8xOXVrWVBEcUE4YzYwZnZVMDBBWUJJWk94UWZuODlmbE1vb2hIbDlhdW0xaXJ1MmlYbFh6aGVqWC9KR3B2aldpOXRJY0VzVVhVVWdJU3N4WTQ4ei9Hd3pNRUZMT1B1cmtzTEU4bEFaVS8yRmJYekt1cWZoTHVYK1Irc2hOWS9qQUhPajFjaHg4SFd4dENQeXBpVllDOFhNdmtGMmI3MXZzSjhRZXBTbzArclR0bG1vcmY2dVVYR3FiRW8xeEpKMTlZYmtBYjAwaE9uYkFjRkJaMmdweUdDYVN1OFJabEpMNVZQcFN4UEgvbTVSK243STlTV3c4WGowYU9kYWMyTncwOW5YaHNMWjI1K2V6R29CL1VaYWNYdDV2cll4cUZGbnhqdXBRMTlJcU16Qnl6RktweVN6aFVkSjI4VU9NZTFGbk1hZzNka3V6TG5Yc1REQXdsM3FmcjdyQWdlMnBnZkhhOTdlRnIrOHRrOHBOSU9ocVlFZzZtNEl0N2dHRml6NDJQa2dJYWFMaHJQS0YvRFNUT29VTkJld1QyUFdyOWFqL043MUU1eXZJUmszQ2pNMXhaT0NRbGk4cU9nVUgvczgwQytTditYNlp2aHJhaGhuK2VSTndGNjJNQ0oyUmd3Z1pzSzdsVVlpK3FlZk51YjdZMzlGVVdOY3NwczVMeHRWZTNLYjZBOGZDeVVyQkpEZi9TaVNsZEsyS0pKL0ZlQkMrYzkvUi9MTytNdGl3dWt4NEtmcHoxb1IxR3FkVU1tOWVaTzJqS0hvb2lUYzBoKzMzcFB1aGppUEZGUGhlWmhXMGlpdkE2NDROZ2gwa04vZzBjR0J0VHpORHdhdGY2R0p4ZTVPdUxJQnJhS083UjcxUE5sRzJUTUtmRDB4QWdPR0xaTjRLcHIweGRsMnBTVjF1TjVsbmFFWW9hemFIMnVTUmRZcWNjV3VodDVxOEJVYlVPemt4KzJib2pjVDZTR0h4eFo3b0dVZlZoYmNkenVEOUoyUGlPbkdQK1Y3bklMamhBb2VtLzhVOWRzeGx1RjFtVWxaS0p5Z2x1dWVNS2h2ZHIxcEllQ0ZEYlJSbzB5Y3NIZjl0Nm5hbWN0QysvSzhMcnlEaDFScUhMYXVCNHNrOU1oWEl4Si8xUjQxdExVcXVoZm1SRjRENngyQU91VjBBUE8yWVRjMTYrSjJCLzhmdDVQK0pRSWhhN1R0bUNTUlR5OVhWRGs5NHdOZS9QcTVQQ0RrRk9kdzROM1hBdUNhbEtKSFlDQWs2b1FUYVYzVmR3dmxlWkFqTkpNdkc5czI4TlpyTDZmK3FxRmt2Qjg0RldEREFldjNoc1FZU0FaRytIbUhWVDA3a29oMjB4R3NyQUtGMU9kVEF5MFVESnRKekpnejcyaXVudWNPS1FUY2lnTExoUG4weXY3WmVXeDl0dVFlWWtiUGJkS3ZydlNJV0d5UDNod3BHN1RlOVFBSWpXM0s0YnZkZGdBdm1zVzhTbGo1KzN6WHFTcHJ3R0wvUDdwVzR0dmtvM3hvR3RXVDNPWjR1dkR3QzcwWTc0ZHhlUDh1bHE5Tkx2SFBuVFZiVW5ORW5HTS9NMm5JeEtBOENWMi9NeG9vNmE3VGJ1UXIyc1EzNUtKMlV3MFZFVU1JVHlDRmdOVjF2NTczZkQ0SWk0bUpLdXdvY0k5a0tPR2dBelRqQ2VVakp4N0dtR1BSR1UzVkR3WWFBNzdYdEtvZ080VmRTd2x0Y3djVHN1QTlsVXhac3RSMDVqd3Z4VXRHbjUrMHN5TmhUK2ZhZFJpUUNoVFEvMWt1VzVzcDNoVnI0RW56NlJSS3l6SnhYWXJCZ0pWMEluaVJVU0E0NUhjZTJiMFpZV0FlV1dsZTdpVWY3KzFzMC9EdXQyeklEZFVVZFZLVitLZm4xalZFbjRNWTA3ellPTE9JU3gzUEMzbi9CUEo5eFRwMC9RbEZzUS9jQmQ2dE45QTV3K1RTVTZvaXNaQUhGRWc1eUZiKzJrN0hzUjJkTThPVkxQOTYrZzFaTVBhTDZvYzFkTXdLZFl5STR4bVUvaE5EdWRGVEhCS3dnanU5dW9CK052WnZrLzV2eXZoZkNGYnB3TlVJS1YwSUxnQ0YzV2dxWjB0Q01yaUNnODJ1UlA4dFBaY3hybjlpUERmaDZMcHhLNm5DR0J0QmUxVXI3c2VpU2lXaDluTUo3aElrZTJOQWRndXZxRUE1VVFTZE90MUhRdkRYM0pjdkFIQWRGSEVKd1RobFAyUVhGSkpLSXpBcHQ4ZUVnbTFsblVoeWJzMlN3MlBKYTJMdmJaei9kN0F1TEUySUdiMDB1bDVHZjBubHdGM3dlS0UxZEhHclpIRFRSdGVpQzhTNkhSWllaWTRwSHBVKzJOMUlyM1dIS0pqOFZOUXhCZDgvYXNJYlQ5TFMwK3BxdURUanVmNTBCRTI3TzJRcURLL0tJVW10aUs1Znk0aWhVTEdVdjYyTDJnemlFZUkyODF6QVc3OWo1Rnhwd0VYZ0hxTnJkNFdjR2N5NkJSUGFhVUVKL0NJbnZQTWk1Z0Z4WXBJYWFMQmtQL3BzOVFuQWhQNFptTXNYVVV6aklaY291Wis3bHRuUDd1OHY1aGpDNzVRU1FnWHVBeHV2ekRiblRPYVhRWDFrL204a2RXVzRWUFFYL1R5Y29nZjh2VG9kQzBPUnBCZU1YUDc3UHVZRFNsWDJVcGF2R0NMTmc3d0kvM25ZU3FCaGFOaGt6UHNwV2w1Q0JBSjhIY01rT2NXdEdPTGthOFVUWXRiV2JZbzFtZkh5VmhNV2h2TlpEOTB2amR4SjVKdVhCZ3lmMjZwNm5IcmlsbTBJaHBFeFdrb1BmaFZHSUoyN1FuYmkvTGszaWpKWHlKRnZXLzhzcGh1dmpQMmZzVXQxY0piWVdleCtSZkNNektRcE5TTE93ZzIyME9mY3Z6Y05VRGNvaWRROXpXUkk3cnZtY1JNbW0yMFR5MEpxQzd1U09Rd0Z3L0k1L0QwejBsYm9wK2x4STJFR3ZpL2VVWE82SGg4UEdEMzgxRVdobUVmOGEvaWZEYmNSamZuY0pLTGVKdmZNeVRIT1AyNGZnM2xGVEl4SENDL0RwN3Jic2VrUGJEQUl5b2Y4L1FmQTVjZGJic1FlV2ljZkRPSEJZWmRjVTdNWHlVRG1sRWtTdmYzdlYvUWV1R1MrbkVKcFdZWGtLRUN4MGFaNjE1T0ZYTFJET2FFYXNsL1dXdnl3NWUvTW1PeTIwKzdZSmFtdFJCSzBFZkxVQk5saHlDRzFZUTAvd2hjd05Kb01WZ0dlTFhjMFY3ckdXakx0OFNMS2hJVEw3RHJLNU1mcDNzM0tKenNrL1hCNmtOZEE2OVB2NzRPYUJiclpLaVVGMlloNkt2OFhKcTB0d255Qkk2RlNRMUlZNGxKZkptQXNibDl3bWJhUWg5dHpXWUtEaDJWT0lkQmFNQm1RMHBya3dQNUFiREdpbWJrTU8vZDVaQ3Y5SHNSdGdTb0JMUENLSGhKbnVLZzgrUDllR2VKVVdkMzg2NmxOZ3NjWXo2b2R0MlFxdjVWcENWNitEaVhLM2J3QzE5Zy9DVW1FOElLN251WThkd0FIc3BNQjBnZExaOWFnWG5YQ29kOXZVSDZJRjZmSVU5d0w2SWhTdVNnaHFveEtycWxwbSs5M2FMME1YNEp6d3k5amNyK0lWY1ZnRU5OM1VGS0UvbElESjJCZHB5NG41cTJVOU4zdlk2dlYrSkdpd1ZrOG9VelZkQVZDeDhTYVVwcUNVL2oybXpvRnZMQTc1MGFLL0I0elhka1hRN0tJRDlZbFZHSm4vWmcwcmVrM0pKcHNJQk5vM01yUXM0bXZCYmhmbUJjb2M5dDRpNCtlRFFnV2ZvbGsrazNiUVJ5YWFXTG9oWm9MYWN4ZTY1ZVJZOWMwOWZnaGZyeVdxcnZzR3FyOVBUWUV2NERjN1BNQTU1aVN0WnNyc0wxeE5nOVNFM1MrZzg0VzZjQWJuU0hobnhnaHNMN0NEVU5nM2x6MWJPL0h5OHcxL3BTNUp6bkhaR3hOQ1NiNW9lR3RaN3dCSEdHU3dXNjFhOXVLZHhETE1hZGNkcUlEV21EejFXZ0dJR1VWRG1MTTlXSUd0MTgzUmJHUHQ2V3IxVUxJRXV5YnBUTEZaVVJYZnJnRERCdXZESUJNTW5TOWxZNVRnREVNZFIxZWZSaHRFazZoWVhHY0ZUdURpUnFnMmI1Vm9jcm5YTlpOTWxidmdWNWpXeEhFT1Q3S3VkQXhRY0JDMWVKbUxaS3lFazc1Z0Zva0k5OWtnZVlLTzN4Sk5GeUEycXhvaVhFQWpnOTJGUzVQNEU3QTdtLzNwVmIyQThTUHZxUFVHbzJZK2poZ0lWNnptQjBvT0QvWFoyM3BjQU9RVjN3M3h3Tmo1bVgyUEllTmlkRFRSZXhDc3UvVGVFT2VKZXJvNVpVL1lwQmQ0S0M3Tm0yMDdIZW1wU1dpTTFEUy9nSWZXWXJuWWZ6RFZnWjhSb015WWVMdXN1eUNyd2hCOXhSUCtiM0VFbTBDNXBNZXc2WVZ3NjdaWTIwYy9TL2l2MDdqSGc2Qzd5Ujh4eS9wT0lZeERscFNmR09QVXVrK05tMmtkNTBLeTBBRkM1WC9jbTZMVHVOQmM4ZmkwSDJ4TGhlMjl3Rm5wTHZtZ210UlkveFNRcnNrcG1JK0s5dnltWlBmMSttRDByT1o2ZGIyM3BOb0dRaW5tM2hjNFhxS3NnS2hrTVU0cDVqM1I1YnNYclduNFNndjM5SllmWmZTWjBGOG5OenlROFJXNnJycm9idUpDT2piZHJJVi9pUU5uVTFlemdqQlVlTllpU0pKVDBKMDNPSCtNVTd5LzE0TFcreUhYZFBEK3pLMDdieFN3TEVGWnRncEM5L2toekp3dUIrMzdKK2tSakQzRlQ5Y291VmJFV080bDg3QllXcWJPKzZwVXl2WlFlM1kyZVlSY0hwc3pzRnY4QXlhVGtNQzhzOVo1M3dmeGRZa0JWZ1RKOFhjNFFhK0tEV2psWjBGK240T25DR3M0cmFnT2E4ODhDcE5tRFBDR2F0dEFJUVhBWk1ybFJ0emZZWmt5aDFSQmY4TjBVZEg4cXNCaU9GQ0JzcVlieEFRWk9LM29uK1lhQXA2OVpQRmxlTE9EVDVNZjVpZm5ZWUJSZHVEbUlkVTlIMVdOekZHVFZzcDNKOXNiZzB3M0I4R3djczRtM1h4T2QyNWxXMkRBckNDY0VOK2ZRV3VYUE0rMUU2cFdDbmhSVGRwVWE3ajFWZ1MyT3pDWkoyZjdPR1A4MzZielZkK2Q4bTIyOUVEYWZEWHRkUFBQd2FEQVUrTmR4RnY2aHhZS2FsdEJJVElhZXdPYmpzYVE1cGlxN3p2UnYrQTZ1anF1dW9vUEltWStESG1jVy9venVua2xXOEo3NGFWKytCUGxoa1VTR3VKRFRCYnZ0NHlWcy9ITmYyaVhGTVdGRGJlL1hwRW1OaExobDl6OHRKeFR2TDJnZXU1Q3ZMR0FQQklyN3RMS3VIcnlqc3NVcnZOSUplVXMrWFNiMDIyZnJ4WHN5ZmZsbkdQaTNIS2pRZU1icFJyeDFwaEEzcWEveERRa1VOODdjYjJ5R0I4RHh3NzdOMXNqZndpdGtYbHJpMGVVMHBVWXVHWDNCZlphNCt5TmtnOUV4UDI0OTVCcndHVjZQUDRiUDJ4Zys1dHpnWEs2c21XcW1uY3p6Tm5kSm5OK0JVa0IzOVg2WVZVWDF2dm9MUVl0WUJXaTduRDJ5SWY4TlVHM1Vidmg2amFpN2lGWXlPak5RU3RIZHdoNnVXR05FVFZJNjJSTXgwTUJzNnN1WGxuTUZYTlc1NUVsYS9CZUM4d0plZmhpd2JSNnpZMkdZMzZaVHhRRXFlNGd1czFNc2x4azEzQnFpNHNHUEJoekM4Zml2TEJvcmtkY3V2bXErMGdSVDdyYnhKZnpudFRmc1VseWoyZUY0Vlo3TC8rcE91RlZzUUdRZm1nU0RLZXRtY0ovNE16SjJjV1YrNGFoYUVxY2cvSER2emdjcGxINDJIc2EyUitNZ2treXRSNGhTWFRuZ0xMVXVFallHUEs3b0ZBRGhYRHJhT0xFYmJNSDJXV0dhRHgvMWk3NXJBeFBGYjBkbEg0OVNZNjVGZmIxU2JUT3dSSkd6Nmc2SmN0bTJCTXRQVlJvMlh6WlUxM1dxeVRBYnBkd3JiWXIyTjRVZ2RSdzV6ZGc1b3JybS9qVlBHbGpwZzNWM2N5eUU0QTRrQnZGaDZvK0IrVm9vRk9nWjVyMld5WkVkR3RtbENpdXFIQWlJdnhMVDBSZ1JxeDh5M1hxWVNRTVFwK1VkbCsrTkZ0OG5ZR2tTclBmcVFaS3JiNzJlRy8rUUNuVkJZSnRnV2g3Wjc0c3c1dm02RXNxR1FmZFZIdXRDMW1FN2NCUUUxZXFCb1N1U2tLSDlQS2VuQStId3hrWDl0dnFQMWdyOENRZWMvN0NjQmxCeWV1T2k0c1BTRDIza1A1dlZaZ1dVMDgxTk4rUEJ5bWVmSjB5VlM3bjNKNkt3UmUvQWJzNGVWTHo2dkFZbUdVSmRCYmRwTDB4UDlRTkVQZk5oWldTSzVVcjF0ZXo5cklUcWJtcWF3bVJjMWxzTXMxaHY4NUpwdUVrcEJuSXVERndKWjM5OFgreU82YnBsM1l0YlV0QXRJSmthbnhGZjJ2ckhkWDJWUFZoWmN1N2h5V01sK2RmOEczaDZOeDBmR3NuQTBwL1BQMXA3WlhCL0w5MS85ZkF4ZzhhaE9aQU9NTytTemwwT0xkdVhvazFLYitnd041NEMvOUluMSsxSk5Xb3JZQllPSEZ3OTVGaXhTbFM2dWwvVkRxWUZXc1ExQXpSZEdXRkthOTUrbTdTVGNZblQyeFhyaXAxckN3cDcrRyt0MXZna1NDekxRUjFNVDUxTHdJRnYxb3hvSGt2UGJXUmU1TkNjRURvaGg3R1FSSVhTOHM0L3pycGtoSHhzcmNHaVB1dm9DYWdJck14dlZyeG44aEMwdFpSbnVtYjljNGV1azM5YytqdFJpa3BmemQyb1gxWDlSaURHOWY5UkoyMGZ1d1BPRVFsVjk2aC9TVGNmVEVPRzRrQ0ZxczRMTmpDR3hPOERzbzIwLy9NL0FOWENaNHZGeVViSXFYTC8wZkI4cTkrdmxnaDB2Z3VXcUxNNWo3Tks2Yy91Z0pYUHpoZEM1am4wOTJmYnZuQXU1eTNlTUhDeHRhUmhITHVWODdnbWtaQURma2RIWlIxWnhLampLUUhFS3VWOHFRR0ViVUswVW1DbnltTW81cnFnR2tKOUc4em1oQXhyYWtVN0hURTRiWnFnbjRDRThXZjhaTS9NOTZ0MUMvTTZXN1I4UWRmMGUyOFA4WHdDT1B4Mm1wYWIzZmNlK0hWRFZZNDI0SDBaVndCMGwrYWFIVGdnMVBGWmMzNVlsYlpkWXpOT0hUMnlpMzZBUVZsS3c0Nkdad2hLQVFsWDhRY3lRT21vMm45NTdOVEdwa1BWb1ZVMWkvNC9jdnY5bUJTZmtxbXdtTGVRS0EzMnNxb21vNTVrcXRnNCs3R0FZd3NDOXA1UE9FYmZZYkZMbTBNOEQvVTk2UjJiYndjMnJWQmZJQytMZ3JRMW44QlhuM3FkUlZ2U3Jqb0gwZ2l0NlR0em5TNHpuUUNTd0dNa25BUWVjY1NoK1VEcjNwYkRTQi9yT2FpN1lRWGMyRGVuOEt0VjQwbGgzMysyMDh0Nmw4TkhSYUNLdzdQYjNEMFQwdzhaWnEvSjc0MjBtempQYXpEWktaT0VZSDB3ZjZsWHBnUWxDdExDVGh2QWZpa2xxc0UzYms5aHRFbUtMN0hpMGsyUmhCdHRLbXphUUxaUGZGVjFCWE00ME5QZ3FxVkQwSU8xWURlUkJrV3AyMjhlZC8yYmJJOEVjWER3KzFpUGdWZW1WTGh0aXpKRHpIamdOaXA1RTlTYnlRUFJ6d3V2amg5R2x2ZVNrS2FLZk12L2MzZ3luakZSck1vZHdTcUlSZGs5R2xjRldZZWlUdDQ4VzNMaGkzQWp3MnpvMVFvODZQVEZRdTViT1ZxTWpiTUg1QXU3T29tSkd1amExdE9CWTNFZ1A5QzU4VjN6ZFlUN2NiWUZBdmE0Kzk4RFkrUDBIRThSSStwT2VkYUswdEdsVXR3NDNjWk1zTTRFb0pzRTBRby84YVUxS1p4YlJKV3gzeXk2bUhod0NqUDFyOUxiOFlzamVGRTB3SmdQR3lldjFBUW5HMG9HSGRLQVl6RjV5ZEdjWDVnQUR0ZDNBYjVadkdvZ3NOWHNiZXVLQU1oSEtlZmRmSkxZMndQRlViUjk0cDgzVThhN3J5TWp1Z1hCaE5hRVp2WWNrQkR3TWYvVCtaaGFVNEpJSGFOeXN5a1F0amhjUU9DMG0zV1Q5RFNVby9TN0NmelVnMUFhdUhYeVUrWlhjTnQ1QlBlRXZGSDBKOWpqR2luNFhoa3VNSUx1RjNhZHB5VHc1WjZrd2FsLzZWZVB1STBaNy9Qemg0ekU4a0lzTGhlYU4rU2t1U1NwWVBzcHMxYjhGc01TeEFWTWhIY0V4RytlTXVOZU83bSs5WW5qckx5U05wclcwNDRBZ0FQRURMeFJEYkdZVDRFTUdHWGZlMitiR0ZCaE1PazhpQk0reldIWTBwTUhoeDRXOXhFWm5aOW1vb3VnQnphdCtMQUJnUWhzZmZPbmcrMjM3WHB6Y3RlRWJsT1JzSFBHdk9UYlJiZFl0TzdUTUVlY3kxRXFEUWtPZzZVU21PRkxpV09rbEdSam9MTnA3MW9lRkc1YjBjN0RubDlsTWI3NXpNSGRXZlJtYnhMUWVMbkJGaWFMVGlHUXBjN3l2UVR1WXEwUmNQeTVIditPNGhRSXV3MElFZnUyUU9pdTA1cTNjUCtlMHEvTTBQZ21UR1JCWlJlWTFSdnMrdm1LMW1RZngrbUo4czZodklnUGNReTd0LytocVJUaFBMT1EwcUtmOXFqWFNpRVA4ZVN1TFFZYWgxUVpsOEhuVTBhNlJleFlNZW1vR1QzdmhOVHdTT3ljNzRhdjhQdE91R1dzYkMraTZQZjR2VVo1c3Z1ZjhkVUpDY0V6V2ExVXJDL1RaL29HU3l6bXhyYnpwNzF1aFdhRWg0VTZlMjZkendYNFhhODJnWnAwdDlvekhlUmVDZEZhbUM4Rkg2ak1kSXhFcy92THc1QlZBd1BCTFQ0MVFTL0ZiaXZ3YXpGV1BCY25VZE0rWVNNZS9JTVFCR2trRFBTWUtSNXgzOXRuMnRlWE1KdGMzZ2tKOXZEa3IzeVBxdXplNXNsZTFOUVF0bldNS1JrOTM0UTZ5Zm41dVdzbEF1aW1JMXhlckx1d3JzMS83bXZYUDMwcnJkemxJNU5iWm1uUUhReFNLVTgrTFFuYzlTd2U5eTZpM1dkQ2pvbUFRdExKdHRrcS9kLzF6Sjd4L0VoZk1Yb3hVUkJaa1AzNkpjMnhVV0pCbXJqS2pNRGNCei9Bc1l2endUeEFzT2tVT2YxWTNXbXFYSG9NbG9BcDNVSkY4QzdtcCtvaVB6dURoNUwxQTlDMlhGeE5PblVsTVhhOFBWTWVPazlIYU1YMHEySTV4UTBGeFJCaFJ0NWZrMWF6ZFh3WFcrdWJJWGMxdDgxb2lNQUY4N2FzbHNPakJVRDJHZFBHREF4MkhENVRjTjc2TElzR0dmL2lNeHNoSHpvdFhUQmJnYSt2YW5aazlYc21nZGt4NFZMeGs4UmoyMUlKR2dIQUdDUEJOOFZtczgwUk9iUWpXWUh5RXRGVVpVaUZFZGw4eGJ5djhIQW1SM2ZjV2dqK3JwTW9LdlFhd1Y2SXcxNllRYjdweHJWWmFzYnJBSWg0V3hPa1dDWm81dVpGZ1JvMmg3R1hSSklocWdKVkxpK0ZjbnZIRmtYR0lvcXR2QVB2dGpiVldYMXhmdVNTVFRGSitJTElQVVJOM3U2QkJZVURvdW54R3NEMys5VHE1QVpMTytZV3N4elJoUVVHSERsZnByV2c1VUM0QldmTlpsWHZYTkpSaU8xSWZBemg2VkpmQWppYUNSMmhvUnpwSkN3dlVVSWtKUkt0Q0hvb1NuSnBvWXEzTFZRMm1DTlZMKytlck1EZzQ5N0d4K0ptK2lUR3VpWmx5bDNSdnV1bUw2SndUWXI1OGRncy8rWWhtWXNkYVFOU0FmRUVHQXdPMmN0SmRaeUJ4Ly96RHgvSDlBWGRyNm1sQ2lSWlJVNFQraWlqdE1PWFdIbzVKTDRDRW5ocVFVZExKOEZka2RtRlR1SVJaREg2c2Z5eEFEenNLUW1hamV3VWloOHpUOE9pSW41OFQvSjFTdmhGZ3lYOXBBQU5RY2VIU0dQKzd2YTFmSFY3NzVvL1lZeW9kWEZvT21CUWZ2ZE1YZkF4aEI0Nm9xK1g0bmJzVzNtOVUyZ3ZlVXV3TEVLblE5L1QxY3RaQzAyVFJVa1l0SkpSYmIzY3l1S3lWcUtDLytmYXBDeFR4WkMxcjVXcGY4ZVltRVhKSC9LRmdGSm4rUjJIUnA5YktKZHR3SkViL1JXZktKR1FKSlQ5U1dnY3NrTTJsaUJkeXN5SngrTm9mUHFNVXVRNTRldHM4R0xmZk53TEIvM2ZFSzE0N1FpSEh3TlJCdURORVdPaEpFaFJvN29BVytCd1JUSlo3dUs4MEtuWVhPL1hTbjA5akltUW1yWDZVSVhDODFNdGxvclprMkNSRXJxZkxrMTZiSU0vSlBqcmhaeGFtZTBZNDMrNDZCS1l3ZWdNT1RJQWh3YWVQSWExRlRMbVBocWoweHFQbFpEVGpQcHZ6R3JQUkZ0UmwxYU93ckpXcFJ2L3VvN0pRWmZlTlZCSVErZUdwaW0xVlhOUnpTVTh0dmo3SVNGL1FmZE1paExxNm9NU1pFYkJXMWtERmoyeURSRjR1NzFKQ1NuY2N3M3BZNXlzNE9rZXF4VzgxTWxMamcrczFXNC8wR1c5Ym5vZ09vaU1ZM1J6cGtDaUVMSG1BaUczeXJVb2VLL1VER3hNc0ZTY1VkT3ZJMENGTTU0Q2J6Um9TRityMU0reFM5dnVMWTQvQ0pybDB1Q2YyRlNERUZQaDNMbzlrUUZMdTE2aEF2L1l2L0FzSTV4VHUrYWtxTVZWdDVkT1BhOEZCOHVEcHpSeEJlVFBSb2VFMmsrb2tSbnhYSnhxLy9WSHBldEtuU0JZSWlWWnhtUHQ4bFFMa25jaUl1WUNQMFhFc2FIdTJDcXQ4SGVDMlprREJyQ0RvRnZMU3g0SmNHNVlSQkhCU2NwNTFEeVN1c29HL01tMGE3SUpEczljYlIwakR0SXM2MHFsRTk2bnNrbHdsa2pCSXc5bTdJOUVIMmlacjBsRWt2S051NTNaUlZDaVdsUDZjVmFZb2RUclRSVllWL29rdDY3SmNJZGUyUURjWHlDWTllUHliZHNUWnRadWpLRklqUFM5RU5nTG5EekJmSmdpOWJWU3JaYjZzOUNlUjd4dytWdG0wQ0dEUkVlSkw2UzQyZGtydHU2TGlYbXI1UDE1MGFSOWVPK3RPNnh0MzhTeGViU2lEUS9wRWJweWVwTnhKalFYOEpPbFh5QmRtakJnSnltWjE0dDliaXBxTll6dUZKUE8wcUJLOVFodHNDbzFsaWM3WDhMS3BCbjUxeTdBVDF0bnZ2QTh4VFd3MnpQcEFKSC9sOGVaV3paaWtReFlZMUdxSzNUMjkwOWRWci94dkp2SDNVSXgxSlZWNEdNOGFnYjkwUDRmVzV3M3dRY1ZvSlJLMnBsQlV0OUdTbkEweGlJWVQ3V0tmend2MU1SVnFpbmh1cEFYRVFleE9COHlZSzFYVDRacVRDaFIwSEVLQnljN3daRXFxd2hoMUtHN0FSdUszWUpCQnZ3NGwyakhCWWdLVFJEOVpmUCtIWVI3NVBTUkY5Nzd5SzNWaitDYkF0TkhFdWZ6M1AvQUhCK2ZYdTdmN1VsZVIwdXNSRjROWGYrS3JZK1VKeVo2L3ljZmt5TmNmeXhqOUVBYU5CVHlIQ1kyMkxFajNwanp1bHVhVFVzLzVibnJXZnlaWUhlMXprSWhCamEvVGdKK0F0WmcyTWRmM0NhbU5xd2xiV0I4QnNkM3V2SmtHanpxT2hVcTIrdkd5NG9pd0pNUUJEL1pBVnd1UlhFTGpjRVhpb2RyVng0MmVZd3ZHYXJCUHNBbGZCU1B0SVJ0MlZGcGdBMTVlVW5oeGJYdjc5ZW1SVXVUZVlKSXhQYzc3Z1lDTDZQTEtoQzZzWklycmZpbEp3KzRGSnUrbXlMWWtPNnJLR2x2VUsvV1N0d1ZVc3F0UVNLbHZRajhWV3BuOEpORVZJNHRjNzJIRXRTYjZERzdOM0JiemV0bEpKeGptTEEwc2dHU01OWnZiTEszNFVYWDZFSllpNEJ3ZzFwNzNxNmY3cHNldDdQWXJ4bnZnZVhidm5qTkc3UUErYnAzeldxL3dRQkRLbDFCeVEzM3NsWTdwSlVwVHJvd1dFVnlscG9sRURzb2pmWitxYzFuRGE2VmxwVVVrZ0U4Y1U5djkvejQ5TVVleTdMRFQwYlJIY2IwZzlsajh1TmltR0lpb09oMm10SzEzNEhFTmZSU0Q2RVpPQUpwc2tXcEhXZEllQVkvZzhYUHc1RDByVndkeUN2RnAxQlNQbGx1TUt1RlBKOGZ1a2xaL0RJc1NqbW0zczl2dTJaM0x3UVlDeWdQdG15T2ZqN1NKVVpZS1dKTGMxcXo2NEJ4R1Y0RmdvcGtDV2RDTVA4WEZoWXhYcjR5eitJOGhYcW8xbnVrZzJSWFdqVytGR2w5Y1lQUlhBS01JVi95WGRiczRPSVlGQ3B6d0NIZHFuQWhKRWpUcUJUV3hXWitQQWRkQ29LSnFXc3VtVmc1R2QyYk10OVNKNnk2KzVFRHY2RzNhbzNKSkRoTk9zR09LbU9HTFV3bXhxUjM0b3lEQjE5L1FuTDNvcGVxZ1k2eWZFUUx4L3U3cG1ZSmd3U24xejJMRlhKOEQ5M1pFdEw5RDBPWGIwNFZSYlpxUlJjN3lkN3ZsK1JjUnZPM1QyYjREaTI2ck1ZK1Jzcm1rYnBxNVNYZ1lrdU15MGFrVlMwMzIrK1kxa1kxdHc4eFhTRFEyS21LZ2RMVVRhSU9TVXRLVnBlL01DdkE1UTlQcmpLa1ZnMkZXYllpRWVGaHBZOGF0QmtxSnp0LzhQSi9UL0k5OUlMbUNYQWtlOEJxUkNaZTdOdndMUXBaaUhJSHJjVzA1VmQ3OXRTQ0FqRlgwYVBPdnpoV1BsK2xWVTFEZSszQ2RCcmR4dlFHS2d6bFh0QzdFL1NvZG9oeTRaWVJkUVlUOEdoaWE3ekY4SUlJa1Z6NnN5YWhtUUltRlc1cFl1OTZFRGhLVzZ4dTk3REIybXpNTnpmT3JFZkZTcENtRXBZV2JncUZMdWJXZCs5R0RYOWtYRXdvTEhuTDBudURUZmJGSGZGZGx1UXZBd1lYUzRwdmN4MzBQUXl1MmxkNkM0NmZhOTR0OHdJdTVSWVVzTDllUkl5YmdLbktXdHR2Y3BtZGhSRFB2S25XMGo4TE5YNVJTVGR4SFNyQ0crajBoT1VjTitwOUZXaXhteEdSVXNnSHp2MzFSNW1JRHVYUkZpMVZWTXFmRTFCWkU4ZkpMZXFqNUQ1MUpORGNRKzZVVU1XUllDOU1ScG9MOG1OelRaTERrc2J3cGlpZVNPMWpUTkMvVUdIdmdjS1ZPQUhZcXE3WnRFSStmc1RhaU9JQmdGYnA0UVJLOEZUbys4K0ViaGNZai9STGtqNjkyV0h0NmM4NGcvMTNlamtycGhrT0FsdDJnN1RSSE1JYVlXSEgxc05nUTVzVHJodUVEeGhSOTRUN1U0MkZLK3ErYzFxRzdTODJ1SGN1b203Ymd1c2ZCS3RLa2JIMWloanAwdWxMZC9GK1VRcXNoY3lFa0xuVDBPT1ZqMnhjSVFPaEJRUjh2SllVeE9NZFhkaXY5cmczZjk3YlAxSVNDZGtSZXRmRVBJcEZoQ2RMOUUyMzU2NGpwc01uUjhPQ2ZENGNsNzFQcGlsYStqR0Z3VCtERlZ3OURZazZiMFNiRmtyVjJlaWpYUEtFcURKd0NOa0lDc1FFQ0I5QnJqR1ZidVJBT003R1BxcjMyNWFxZmQ5TXU3VmxGZUxoazJKMmVPT0xabWV3MFR6MjFPazBybEtnNlNUVEFENEJJVzUvSFJkUUtWV0N1YUxMZUpXRlF1dEE1QXZvVERrZnN6SlVMeDNFV1A0QVR6UGxUV3JUT3ROYVFuWjFFY3lDVGVsendoV1cwVUVFZk5wQllGTHlISjBzSVN6Q0pWVzZZSVdWa0dYdVNzN2RnZWlUQldEN2FZU2ZvWXo0RC9BTHovUHhVTFEvTHdEMnYzNCt6dGxVaHd1SUk0bWNQQzhHRVZYWlNpa1JjVHFQb2lHRzVZYll1NHJYNXM0T1NDbUFyejRWdTI5VXR2TCtXWFNCNXQvaWVIVHBQWmROS0ZTRFJmMjJEWFBGMENtcFJmQnZjeWFJRU96WXBKYmRDZzAvOWRNeVlHWS9IZUdocU82SGs5Nm9QNmVEQ2tvbk1tQkRFcXlQZGIzalJiQlhhdkF0VnIvQmRyVmJPYmhoOFVZWWRET0g3WnNGTXlzRW5RSnU5U3RwQmZvMU55L0VoaHpRVGhkREFRb0YzS05JcE8weDlPc1ZyaFlxL0VONWRBbVB5VjgvQnlqbWI1UG9xQ3FsQjdkNVNseHY0TkRVM01adjFtY3l6c1dSMjA4L2VTdmEyQXlGVlYyZTNGR3lkd2xkSHo0cUhRMllReGZPcHdxRGZxZG02bGtTcG9NMGNmb1RQOUVCTGw2U3d0MS9oOGx3bDF4eUFsR2ZlMmorVHhTcWsvRHFSdlZVTm05c3EwN1ZqQ3N0bGpnSzFNcFRnL3lhN1JIZ1lnVnlJQklYVy9YTnN4cUhIU2dtSHhEMWtlNm5tbTM2V2xpVmE5TmNqdGJ4ckFSb2Vqd3RqZHVlYjRzbU5qbkU2MnFsOHJFb1NzQVJUM2QwcmtvSTRxTCtrREdiN2dYQ2Nkb0NyeVhZd2kxNURvQWtHVC8wL3lhNGkrdDF0L0xGY0hsc0kyMUpSTXpCNHZyT3J4TmZReVJpVFJiYkhlVHY0WWpVMVJ4dFpPNTNhQ0IxTVZjMzZiOFQ2NHBiTkVoeUtQUFdpMmdvYjZHSzlKdVRQczlXWUdCRFE0a1o5MWFISFI4QUhVOTlzcm1TdkNVUVd6TGpWYVdmdHF5VXBsMWF6dUljRGxqNHpycExKMllSZ1Rza2JpckdpRVRpK3JxUzIrUnlSWkRIY2dSL1pIU0VFWnV0UFRKVUIrVEx2aEdWODdTRm9VSHNIWmZtVTBhMFNHK3oxbWZwZFFwWUZDZUUvOWY4aUdHYnliZUZSYlduaW50Q0NyTmFvRG1XdXdMeUxvS3hVdzVTSWovdTVLemc4bzBaWXFMbFA3RGQ0bnpTWUZYT0RVTTh2blhiYldUWU5zNnJrQ0NHTHVzekUvWWtydlkrY0MycXl4SkRkSFdpOWlyUVBNem5ySVFjU0ZuUmFGZDJPaldRcXBna2FManJKZkZqeEVNTTZ1YXNiU1NSNjR0V0pjam1ncmUrWVo1WlJtMEt3dVNVVGlWVGxTb0NWQlRKcUMveG00d3RESkFxZXk0T0hGYTMyVmhuMlQzWnJVUnFIRC95QUhPQzh6NXlRUkN2ZnRCbUNnRmkvRFMvUFhvUk9KTDhGQnpSa1FTbENKd1dvK3BRR2xxVEViWmQxQnl4QWpVYmNGUWUwTkV1TUVvbktraDdUbG1INFNxV1BVTFJVUm9lRGNyS0tBSjF0VWFkb1gwdmxTKzJxbm9tOUM1VHNpRjJpZ1U0THRvMFEwZWsySVhuUElFZjJ2SUdWbkoycGxNQ0ZyQzA2MnR4WGpUUG5YeFNab0MySHI1dktBYlJzRXFjNGlqQm9kM1lzVkVsV0luTDhHUk5wekFQeW5rbzRyS0QvWWFIdDQ4MFFrcUtycEJ3ZU1hTXVxYUtzR0lVaysxc3FOaWtQaWZxWjczaS9KODB6RTJPRVl1KzZpV0NaVEdFWHB1cnUyTkJTWlFORUpMREpicWNxaXcxMnVaU010M1EyQ1ZSY1lGNGRzR3lPSWh2Vk1vbUVPeGNMeVphcnFSNVJUck1sdTZSck9xeCtIOURSWHZsck44OTVZbm1xT3RHT202dDZoUEc2RDJVQWRNUUxDM25PWktISkJ4ZmZTZ1pZQmk1akVza2p0b2dmbDBTY0xpamtqZTFnTno0SmJ2ZXhObTRiTXg1Y0VFbGNNNFdURGo3TU8veUtTR1hiRnBpbytNTE1PZ2laNmpxbG5YMUN6Q3RNOWxVRWZiMldHa2VBc2pYQ3pzK1EyTkdvd0JJK2hpdjhQUVY2QU1QV3IrU05QV251LzNHZG1qTDJGVnp4OE9sbE5VbytqcHdIcEYxUWpydXF4eE5kZzhlRC9SVDZ5YmJQVS9OMHpEcm1QN0lqOVlIbVViZGNhaHYzUEYrcWF4TEZjanNNS1E1SUFmTG94dUo3enJ2RmgzNXF1UHpxZGdwNXdDMWp0dkNjalJBbEIzdXF3cFpZOFYrSThRUExQWHM5UkFqZy9keWFmMzNlM2lxRVlrN2pURzB4Qm1JWUI4SEFmbFBXbEhIVjVwM1IxTnJIbUFycTVNYzErMEEycWZsNFVkWkZwUDlOYm00MW01LzkrWlMzU3ZrZE5ua3VsOHFVaGRCUkhZRkwrQ051WXNhSWh5bmd4a2VDRE1jVVZmbWxnSUI0SUYxbmo3em9HTWhPSHAvUFZLZU13bVU0aU9tc2lUYjhrYXArblFRUERmajczc0pzZEQxQmFlWmFhc0hWN3pNcmlVQTFlTEpRcFlMU08wUzZXbnR5QkRzOUFKTTg3REprUDZYUy94R2UrcWU5QU1sZ0g1S3lVay9tSjNIVjJKZHBFa1BpUGRBOUVmVkMrMk5JN1g5TW4yL2hENlorUThuODZ0Ui9CazhMVlYwSjMzekF3V3NGUCtxUnBUaEVyYjdTQVY1K0Zya2h3Z0phVTkzdVJwc3Y1cE4zYkRQUG9USGNhMFB3d1NLaTFlZ1FwYzBXMzVlS0tRVWhFVGJFa0hLSFlyZGx1LzJsTEVPT3FVZVoyUjNnd1RVTjNPREdVeTcza2ZPdklZVWh6dEdQL0lNbFNWT0x1djFqNnNDd3NoT01DalZjeWR1S3FGY2x2RHNmQmFyRytReUVxQXhKdTdJTWpETnZaSTJVdE44MGdVTjlUN1NiVnRyQjlibWNFU1VDV01VZ3JJNzFKc3kyclR4TCtKdE9IQVZQamN5bTdybURxelJlVDcxS3o5OFI0aFg5WnVsbjM0STMyTkxCRmpaUlN4bENVZ1NSaTgyMzZTZ3ovQm1WZFhMN2ZaYVdYcXFpV3BwZ1hIbGNvckVQaEVmMFlSYTJEN0Zxb0h5RTRWclBVTDNoK2ZCVVA0WmtTOEdFYzdzMzljMWtBNUtyMzl0MlRXaFI2OERhbEc0UVc1OTNtNjdWTzgxbmNXZk00Wngyak5iRDBlY1BudUVWWGMrR3grYXVwb2tqT2padTBtRVlCeHl4ZGo4L2hRT0kxRVVoSkJGQit2aHFEVFhESkRRbWlVdXdnMWdtRG5wSnk4enB1UDNJNjNNcGVReW5CRk01TFVmUDdYN0lybWJJWU5MdWdXU3hLL25jQ1UzTHdIQlpySkFvVStrcC9lVUlnT0RnN243VXpnOXY0Q0ZOUEFHM1ptRU1jSmQ1M1VhUUg1Nk9hRVBkUTRrZ1ZYcEZZSWtVeEh2WE1aYUZxaUMxRldPcWozSjc2THFKRjRhaGg5S08xdTViNjRuV044MXlXcUpKcmVqZ1RlMDJjS0hhcjBlcFMvU0ZJbldYODJNUmMxT3VOTDJlV2xZaTI0alJYNGtiOFpaZjB0ZkhOcHY5SXAreTN3Mm9vM1JQakVVNFJBaUJab0x0R2taZS9Na0VQa1Nvb2ZjSUVoYU93NGJXdXRHR1JxRlhGYUhvRjhxb3l0VVAxeFZiT051RkpwSDBrK2k5MzUzQXljaXMwQ3lXL0RxUkUwazVqc3RMNmlZTzE5dXFHMlpQc3c3Um40TXExMUxrMWN0dm8velZMaEVGNUZ0dWJobm82RzlOTUk4YlVCb3d2SmZsUWxUN2cxcURueUU5T0JJbk1kdGVqQ3hEWkVSRTM4L25HM1NLd1VvTTZEakZYMk5INUtxeUZmbVlrZHJQQkFJejVuV2FOODN5REJzMWFPM2gvbGRZYTV1ODg1VXQwZWdmZCtKVTJkazdxMzg3UUpPTldKRkovWjFaaEtSK1FzeGdBdmYwYkdCZUZVdVp2NGk5ZjZqQUhpYnhteXBlZTl0cG5zVGhWZ1hLaGp0aERyanJHeUdtRXJmVmMvd25xK3ZkZTNza1lsVDNTNHJoNUZrRnRSRE83SjlrRWpjenh2dDN3VFo5OThDbWhzeGNKbFM1Zkg4TkVJUDNxM0JmVnduNDNOM3h0aFV3Nkw5Zk50TUFEbWJTVkJHMTk0VEFQRFRleXk1YWtWeGJrVU1CR0hMMHpwS2dULzFWb3JrSWVOSHZIc2szeCtrNE9pYjR4Y1k5akJ4d1NlSjJwc0dzT2tZSStTQmFrSlBPUWo4OG5INUx5Mmx0aHAzeVZSc2ZIQlZNU3FUZzVMaVZSd3BhZ3NKWStKUG1wVFlFbkR3VjhMY3RnU0hQdDRCUk9WS2g5RURVQ0VRUmNGalZJY0gwZ0dRNGh6c0xKdE5SWGllcEhDRmxvcm04MGV6RXAyVzZCcFgzL1VCa0ppK0k4TERjOFN3eVVPV01jak5FbDA3VDkwUmdvaVhOM1Z1SjRFRHJ3cEVsRS9IakNmZnlDcHUyQ0xDTTc4VU1jMWdGaHVKYnEwa08wY2J3RURYVERGS0IzRUxlek9tQzF0S0FBTk1Pcnk1bUM2QlFOOVVUUGwzRW8rUmxmNEtJNFFsMFRkc2ZLVG5OZ3o2MVhoTkszWVdCZmdNZVE2UWtEeC84NjJSTkI4eWo3TUZLUkQ5aDlHYm5tL1R1OE4wbU1kQk9XV1VnVUR4VGM4Rmx1VmphT3BHcGMzaWFRenB0QlNPaUs3R1d4aWpQbmdxUWczcmVFZEorcWJjSHlxUGF3YlFsNmUvRm0wYTI5V0lsUVhWZTRmYWxNc2dsUDBPSjMrREVvbDJWKzdUL3pMYUVPeXFHbHBHL2VBTVpsNWt6TWs2SnJqbmJ4TDBXcGpuWDIwS0ROVUh5citaYzRqZzlqNnA0UUR2UmZEaXllOXJRSURBTzlPanlhbmRyQlZIS2RkRzVKU1ZmNEU1M29Rc3lyc1RLcjdzUE83d1huZlBDVnVOczBub2RySUpadlFHZlNuVUV3WXFYMnBiU3RlckJEU3RPUkhEcXFDaERjcWVZYTdJVUVsRjJQeWlHRnR1czhLaWZmQ3JRbUVkMFhWZ3d1cy84czBFSG5BOEhOUzlwdjJmNWYzNUl6K3dJc1d0TC9ZQk9UdTRFZzdLWmJITlRDNnE4SVlKaVNnRDI3WDFLL0FYNVU0YVhaQVZZQWZxZFphbG5TYWFFVHFGUGRCODQ0K1N0cGFiRkk1WWRyQXZwUG41b1dMSXdiUThham04aitGWmRsMHl2NmJNbjBpTWovRXhJektYVlNTZkZQdy85Wk1LVUl0UFJ4aVE4cjE3VHZUWVhQR1JNRGVZT05BUjY2YWJGT1JFMXJZMnlZbWRRRjNjakZrdVdzUHQ1UXVaUmNxMDN1L1RySER4ZnBnRi9BaGpOTUdGNDl2cUhZcHlCdzFaVXJReUdKWDVFSUZUU2hDYkUyWmRUS1MyNzJuSW5GL3dMbm5od0lFOWtnZUVEcUIra3AvVzZSMkl2clBwOXA1bW4wVDhyTHB3SCtlRSsyVkQrbkVGOHRtK0E0WDVXRE9OK2p5NnNDa3Z4NEdlMU5TOUQ5MUJkSG9BYkZYcWh3N3pVaDRRWW52WWl3UUNZNDdaTm1vWDM0elAxMFFtNVNET0RSY0xNd0FZbXhQR3dCcy9ZUFNnMUVSOVNDeTRCTkl6UVEvd1J5N3Jzc1NkS0kxWVlSVmlIM1ozUVRJaG82bFpqbEJlM1ppb2NucGQ1UVFjSzJkWGQ4cHo1eCtEUGl6YmZoNUhKVS9CUjQzbGRSUkFvdE9RS2hDTlUwc2xFc3huNXFZUnl6NmR6amo0OTNRVU9yUGl0SmNicDF5SVRUcCtxeWhpa0w2T0Z4Ri9jSkZsN0JoRHdRZ01DcDgxVzk2YkMxczhTSDdCUkIzS3NCUXhLWWNnZVlYVGpUMmFsSDVILzFLeHVYVk4rK2hRL0FZb04xMWorN3diWEhPZ2ltdmdyT3liUHVyakpWR0xucHE0d3NTcWFBRDZlaU1CK2IzUVhZRGsxTEQybHJQZHVjQ3dFTGgvTGZaRk5iTzdsUDBKSTBTb2ZQUWdjTkhzdkpPakl3M3hZVkVSRDZTZjFPa3NnSlVhT3EwOXZzdHZpanV5YmxsNUxzSjFlYml0Z3N5VWFZZ2hreDAzVkRnM2R3bVhiTXhuRCtjK1JQcnBMbCs2ZDQwcHB3UHEwUUZvbWVTbU1DcmtYQVkyWnJZcm9XYU5GY25QeXp1Q0FMOHJldVRoSzl4TndnSHpvWGl5T3VTRUN5QUtlMkR0Wi85YVVwODlTaU9OaEF5YkErcjd0YngyQlpLdUhyUDNCSENFM2hJVHR3RGFrc0ZqenJhSlZKR3djL3VaTW55MWtiSHNwZDlFOHBxWDgybmdOeUh3UWJ6T1V1VmVTeElHTm9oS0NkTWMybkFDRStGbEJwTnVzelNKTHlUaTFqTVZlcTkwTDJVVTFPemRpSjZKUWx5b0hrc0JJVFFRZENOMHZDZStFRGZIb0pLNkZKWXhvUHlKYkV0bGdDQmlvTFkySWxqTVlPYzdpQ3FMZGY1SDgyV0UrWGhJcUYzK1h4bnM3ZWRGbE9aRno3MkhDYUtSWTlWaEFvOEJKdU9SZGZwM1h0WnJsWFkzTDlYRUJjckdIM0tjeStadjdsaUg1REE5Nyt4dHlKVnd3cGVmTWdzZEpMQ0JlNWprQ0JFS3k4Mjl3TXdxSkdBdllPTHlveXVxcXpraUtmOXNtbXdWdGJIbWhFU09EZEk4T0pjRUFrZURqNGtCQ3p6QUNJSExKQ0VDNjFoSG5rWUxtN1piM2JQei9QdGZlQmwwQmhDZTJ3a2NQTUdXOThhZDU3dXpUa2ZPY2dxcnc5Z3BqVW1NcDRTTklEOGhpSVNMTytucjQwaGptbzd3ZTlidVE5VTNyS29CVHBhZVdDNUxLK1ZKQXBMekpWR3lOWExaeXRjSG9rMWhoY01Wd3VvakhMU1F6NkYzeFlVcnJtRUJ6ajJBRkE3Mkt2UmU4WVp6TlROQVpBYjgwaDNzU1A1aVg0eHQ3L1dzd0tlczBNR2tZSnJhZWNhN2U5OFpoUHpOMExYQjJsK3ltOUdXTEI2dTYzdnEya0NmbzVBbzRXdXRsVWhrbXVzeEpTbHUrVVIrbXpGVGJZRjZwcW0xaVBxSHZ6Z1RrM1dsU01DRW1Pa2w1UGEwVy9YcjlqZDA3MXhZM0xEMFp4RzYwcnJ4VTJNT05SbUlBN0Q4dTdGeklUR3Q4QTE0ODU4MmJ4cTVyRVF4cDZVZW8vbGZHWTJjUGxZSmJxMTZlTEwrVG5Md050SzlsVTlXaUdUMXRjZVZSL1lhdFptaHJtUDk0cFdsQ0E1YmIvRWhCcTVyeFVRUXV5ZzIycHpRaWNHbHR5RXVVelBrQjlZbzV5OUpvdlRrRTA1NGoya3VlK2NEYUUrOXg0K1RMN3ZTU3QyNkxlTVdRd2FCZThTa1lUVTZhR2R4ZmFZZDROaUx1ZmY5Z29lYkRUd096bXBCSU9LTTYxK2RSa2toY0tZR1hPRFI2TWlXSlp0bTU4SlpHb2E5OGt5NS8rYWI0ZWNkSWhRQnVuVlhJdW4wNW5PZnZ1bTJMVS9Oc290c3RwOVVjNEZucjZFVVdoSmNXeDJyUjVxaS9WY2NBWjZJS0RrR3pucGMvR3hQSlMwcEQ2bmxoc253Y0prZDl6b043bUhPRTdmcUExUDNIWFVLYUNSc3JmVXdqS1A0ckI0Q215WlRCOW0vUTlVYVFlNTdjWnNqd3JKLzlhVVVyd2kxTWd1c1QySDNTd2E0NmxvM2ZoVXlyTHk0WW92TnNtbHhIZkR4ZkcrMWIzc0JKOFpKYU5Db0dVb0NTMjBRd2RHMU9KeHhDZVhSL3pnQktwK3lYZnpFZk42SU9zUXdFenE1dXNqOUpPZitYUit4dmNHNVBneEE5KyttOTh1Tm5oTnErdnhUajVQTDA3Wlo4bTVmNzdwVWNhYlpubURwa0VhU3gzMlFnOERDZVFTWmRzSXVpTmMwcGROZU1laFpSL01yb3lBNzZwRVIxdFhSMjYyU2I4c0VhY1VrRng1aUphWGtEbVB5Mkk0UkpNQzRuMlFWU2JXMjJhamFQeTNrTjE0K3dwbm5pbHNtZ1dwdElkamhCWGRNa21CQktuNVdLRjN5OEdsaTZwS1hxRWxvcEpGOUtMekVwc29UcEZDRy9oUGVETHBCd056V3pwb1lzdGEzYkxCdEpLQ1RCbHZ1NUhHMVlCTjM0TXhGTkYrMTZyNWJ3L052NytnL2NyNzB2N2N4a3dVZ01zWS9HT3M3Z2JQN3diUjZNVTBQemFCSnZNOFcrall3eDE2Y1paRVduTGQvNDhNOWswRzUxUmI5WE5mcVB5eWNuZ2xaaUNaTlBYb3VjdVYzWDM4bittcFpzUHdIbkFVUEZlNGpSQm9TNWdscTgrZmg2am42aXpHR1hSWWFHeGpzMWx4OEpQcjIyYnhadzdrL2dGdTk1V2V6MVpjVXdueitYWlJzNG1icjU0a2xTMnQ4bEVKdmhiMEgwVnkvV2xTT3dHekl6M0JXRXlkKzFuZTZZRGxabGdUVTdZNjFxUWJEMmVDOGdHK1NFL0U1bittaUdTUkJ3L08yTlFnaXVORmM0cmh2YVYrRXdTd1pINXR6a0JNMGF6SVFOb0N2eVR2Y2RRVHowd2pwQmZ4dDlxWGhRaXBQa1M5L1FrbHB1RWt6eDBPMmFUblJuMnZIazFzTEl2R1FsRXVOWXA3V0hMbzVjaUdUSXFZcitkcC92TUlTSzZITERyRnhrUlVmWFlTRUx2WElwT29XcVB5c1g3L1VMQS9tYWxBdlVHRDhDaXMybVM2a052amFtODdUYUJZSFhuRk93TU9uSFlybTJxYURiaFFYTk1YY1RaWWc0S2xhY0dGdTlIWUVjbW41d1ZyeGRTWjczZkN3Uzg1SVlXRklLaUVpZVNHSk5ydittaTZmNHdjbVpmZ3lzaEZUWnB2VlRJR3pOUDFIV1VaM2EzTHo5T2FlT25aeXQ1ZzFWei9RT0d5ai9QbWtseDY0SC9CSHBYeGlrZStjV3FSRTBGd1F4QjVubGRZclpyWVVobDZhSUJKOEVmbWZjV0ZRRGpBanFySjhqU3RaMVVlc2xJRnNzaWx0a3VtTHc3K3ZiVkpsV0VsNVh4a1M1d1FZdDFwZzZobHd1cjdJS1pseThhRGQyK2lKTW5vVmhGcHd4MzM4L1FFUEZtVldzSUhpb0IzSGw2a3Y5UmJzYnQ1T0RkVjVwSDRjbXhDZ1l4UTNtLzAzUVdxNGZCMWtSQXAyZ3F1OUI2ZVZ1SU5TNjFmUVo1WUhyQ21XOVh2aXo4R2tLcEhhRHhwNHYvUndUelJhMklDU1gzWFRhTFJwbHFxaFE1MzdWQmVwb1VsbmRNRlVrWUNSeVBHTjNuVVF4eEJHaE8zdGRrRGZxanVFdXpDNGtDam0wbmN4cEM1STFQRTNVZVlUT3RqTHp5S1dtMElWWFBtUyszOE1KbnRVMy9CK3ZRcEdta0lRMkErc3hoU3RTdkpaaE91S0IzR3ZMVDNOYlk2QkhNeGhYbVk4RFZhT1J1MHVMeUtOb1lDc2RUUVl0WW0zRGJqeEFQWXpsMlR6NzNUSm5odUlBQlQ1SGZ4N013QmxabnpVaS8xRmRvWnd3TC95VTAwRGNKd2lvZENTT1F6L0tqTE9WU3pVeVk3Qyt3c0ZsRVQ3NzFUeGdCNkc0NFNIamRRaitnVDNKeFJkUkpaUUc3T1JReWFjS1FpRkZRZmtBTFBHSDAyRFZROGNIZGY5S0M0aDUyN1FuTDVUbFFxTzJHTU50WEdTcDdXR2NSY3FQa3VSSmRWT0x6QThnajF6dzgvMnAxdEhheVRVVkwvWkJCYXhham9vRS9qc0ptTnFxajVZVUM3R3Y3cVRRVDFiYU5rekt2YTZLeGxLcFZnaXdJaUJGMUVRUmxZbk5hb0k3QzZ1cUlieDl5cXRHT2h3UmxVRFY5dHlNVURiRjhOZ1hnRmQ4UTgycnhPeFVCeGk2aVV3RWdyellzOGlxMzFlS2NzMlhHZkF6WUc0eVdoNWF4NFVCeGVvNlN1M2VWb2daTlVwekxmTEhaS211RFRqTWxRU29tcFhkbVc4RDQvbjZpcHJ2YVlKcTZhZ3RRUDdIcENvd0xkUERzeU5NdnZ0bkpaOFNsc2pTdno3cmFrbHRIZFJmZFMvNkM4dGoyOGNEOVBqNFBkalloZzZMUVBnbkJLblZ3YTRuSFYyODlsUXlLbk1QenlYaW05bjRjM2NvOW1mOWNsdnB3amNrT1dqekhSZjNwWjhUWDBNSE1NSzNRNGpvT05Sai9XaytYeGZEWlE3SWpXcEQ5SUVKVVV0ME04UkMvZUQ5VlFqRVNKN0xyV285R2VKSUFnU2VYWHVDK0hQb213UHFWTEZhM0duT1E0akNpYWx1MndsY2t3MUZmWkFRWWJqMEpvVTJKK1N4c1g0ZVNRU2V4eCt1MGRoRjNaR0QxZnZ6bk5wSVNSMitTTlRWVjcrWmRvV1BydnZ5dWlHV0laVlBXRHljSldjK1FuUnI4eEtZQkxYZGxwR1FGUUF5bkplK2pDc2xrbzhnZEo0UWl2L2lsR1NVOE9wNXJDNm5VN1djckc0UHZxb05NQ0JHdnNBWnYreDViZURvVCtPTTgzMFpndHBpZjNQUUU5UmVSOVlxOEcrdDhoanVGWWJQUTJUckZFS3JzZGhCVjNOSFREMSs3b1dLZVdTL1lrL05qaC8zaTNkTTdCaEFFWFBQMkRIN2JvWFo3TlgyNWZKQVZYVzh5aWMvVi9wRFcrb2QyWEYvSXZ1R1ZIWisyeHVZMitFbXQwbnNVVlhKSVZLSnhGN2xNY2dsQlFHSk9LZzZMS010OUZkbzQrdVpjVGlCblJTR0RFWS9MemJwUFlnVkpZVTJmejJTN0xGUDVEcTlvWHE3QkdsaXB0cWhwZ25mMmxnTktybTZKSGplSE92SXZKOWI1a29DbC92SlJ3elRSNWJwM0hRNTBkcFU5QmdMZ3NmUjlCMm5aUDYwZmhFaWVGQlBHaGVYaEZGZWRXOHR2eldIdGJXME9tQm5vQlkyWXNzYjZqN2JTSFRoU1ltdVdKaEduVkNDbXVySkNhMmpUQXAxdjZwTHBHZytjSDdmQ3Q4eWovblBjMkVjemdFQUZJdGpOYTlnNzMwMVhCRHJhNnBhVVduZW5NMzRZZnFyYWdxOW5seCtpSlluZFZ3VEVHbkR2OVJrU2JGdkxyejdKOU1tYXJmT2ZBT0lmcitEQlUzSXpiQXU2K1dmVHZTTGFYdjcrSmplNWZobzFvdkNNZXBoaWdURjQxODM4Qmwvd1FOQ3pvRm1VSjJPc2FZMUJsSjdPT0RCTTA1eUpGdHJHdjFQUDNZWkhicHlIclloMjFNQ0tBUmw3c0daWllmcEdBbU9Ga3BjODRJWkk1UmxnR05KM3U0N0loclh5Qkh1b3FneTRMaWJTaHEvOXJzVkh6SnpCQ2dMZTZPWC9SZFo5WlJNbzZFb3hPVHlIK2hJUWgvUDJ6NDAvS2dZOURuS0pKV1dDM0ZsTUFkeE1JWGhjTEYzYWtYVDVzaUxxbEhTaTdVN1BnUkdsQkZ6dlM0RjVaYWZwNkNNajhXWWw4ZmlKVThjMU45TytyS0VqQWpjY1JCZ3V6amszY000djJLZ3JQcENaNk9Xa1JMM0tUc2I5cDNMQk85TjI4RlJsWjlCNzZ2aXlFbVBHcEEyQkNrUWtVckdwRzQ3RWtHV252SjVJSHBvWjYyV0toRVpRQmh0MXhMdHV2S09FdE11RG9CL1pQM0MvbU14TWcvNUIzWE5ETjl1aUZERmkxZ3hiWE9QaVIxU0xyZXVjb2N2b3dFTDAwV2F3dzE5eG5NcWg3SG1jZVFPck9jdHhyYXJrQ0NhcHlOUkRUbTc3dTJLaW1DRWlGUHI1MDB1VHdCdzZmdVExZUs4U1dySFJMdnMxVVVaMVpyZDJFT3VscFJXOWZocER6R2RiaHNZNk9JUTVvVnl6TFNRaXByWFEyUlltVlQzK0dWS0ZDOWhob0tPNXNrczlERngyc0VNeFY4ekdPYzlXa3JHbVJLRnlwZTY4SkxLR1BreDdRZ1YrTFUvQ3R0OWl1enZ0YzE4WlJHSWgxT1VtNTNCdlM1bWczZm9hRXNFeTl3K1MxZ0NqVThLSE15akZwSmYxSnBNam5EbzhYc2J5bjNGSjFuWHlpVGhONTVUa0pMWmpaby90WEdGRWU3TWtqTnNsa1JMUVl6SVYxZWlTOWJIMDVMRENZU0hYOXZiNzN2ZVIzcVVLWTBkalJEUFhmbG1RRlQ5dzVSWU5oYzZ6S2dOUlFWeDY1emJTbHd4dzhUTEUzK3dZZTBVb2tldlNvZ3YyeFRBN25EbXVqS3d5ZWhBV2RXK2owWUltVUl3SGt1enY4Z29KRjNhUStQdzR4eFVYakYyM1RHZndHSzZha09xZEhFOEdBcEFISTN3WFpGQThxQ01LZ09uK0lZS2xzSmNIK0JrZWVGekdZb3JLelFwZEdKS1JqbGcrSGd1VHZMVmVHSXoxVUtpUXNlNEVnTFIyRU53YUh6bzBPdFIxalRvdVJvVXRLaUlCRUhuUTdxTWF2RjZob0RFejBEUDdKbnFXK0dSY3VqN2VpTFBZck5sVnV3dXdmdEdJenUvOWt3SWNmcmhaREFXUEZ6d0FMUTRNVFpSOUk5VmhQSFJ1bWlzTlgxS00wdzdCZ2IydWtUT09hUW9qbUJoS28rRStmYzkvVWZJWG5nWHhZK1hIbW45OVIvYmI1cUM2MXQrV1BGbU1KNnQwY083SkZSVDVURWZJV1E1c2w5U1hNMnJ0MGxpT3g5UVFkNEozZzZzMEN0eW5PMmMvTzZ0bXBiakhaL080R3VhZXB5SmZlWlBKL3JLSnY1Zis1VXNVb1FUS01YNVM5MGExdlhxNkFUM3llSlcxRTJLbXU1eHdEaWx0bWEvMWM2b1JtRDlWenBLVVVRazBDcFc5bFVvQ1BXRk9uSzU1RnBxLzJZaVR0RGd4V3c4cFlNb29pOXJyTWsvZ1ZhOXhxVForQVpWV3lFV1FjSmFKRUVtN2dBSjdIcFBOU25EVXpqRTZrUGNEVlNvWDExTUdBY2pLU0YwUVcwTU9pbDMrRGJuV1k2d1BNbGxUL0JybDZBMmhYakVuWW42OWtiV2ZZY0lIam0yOVl5T2V1VW5VR3ArTVprb2JacXlOWGdJVTdxMEY0VDVtb1JYcUp6QWVnN3RkVDN6QXgveGsweDhTWmtoSnM4Q0k2cmpvVDEzYW1SV3F6NEJTMXNjM3I4Z21pUUFIT1dyQU1iZmZQV3FwZ0NJWHhrUXRMTHFXMFZ2S20vMVl2M1BCYXJXUHd4U2kzZ2xjUytISGNzVkFtazIrT0NFVEhYaUsvcERqMmZ6L0RPcjhGUm1nT0wraVBlRkhEL2RrSkgvM3hxV29kRU4zd3YyNjhYZEhWaDlDYitEUlBrd1pUeTREVGpJKzRKQ1dMYTNPQ2lOTGNmOFc1aTVQM2ltWWdDVHJURU5URmFvY095MHR2ckN2TmhidzhiVHkveWJjY0RsZW5xU3JMdHhUTWtMcC9FUFJHS2g5QlFkSnRtYUNhbDBuZFUvenFQZUNGQ1FiS3ZObGJFQ0hmRjFJd0FlcmRxL0J6Uk5SYUQzaXFLdTByTVd4ZkgrcGEydEZoeU1RY0NOOVVweStIWVFUbHBOZ2dkZEpYQTNrK0p3Q3VCS0lWS3ppOFNvODQ3STVRVDVuektIMm9naGd2QUErQkVsQm1qUkxyeHk1QjVHMkllSXJadTJSbmZKOVFNZHBBb0ZxaVpwQ3p5aWZZaGFuRjdiRVllOXhmZGxmSTR1VE9SeXd1cVRiTGgrM2VNTFdycmRmeDdaUlBRMVhINjFCTE5vZ21hNlpra01OME1hK3JLUDlrM0NEcnBkcVFOako0TCt1UXQ3eXM1ZEFabDBkekoySEM3R0RQUWhsUzEwOFVmNDJpRDJ5WVBzVXY2Z2dPQ0pVMFNURkNkdVlvMGVHeDJ1cjZCT3Q4c0V6ZlBqMFZGUHF4RG1Sci9pMWRzYTlIeUVQbEkzb1dRRUc3ZDNkbVRTWUZtMW43aXNGbDhqOHhzMWRrQTQ4ZlFGekR0cDQ5TzJUWWVBcDI1UGZoUDRFQkM0STVHdFFiSkluWThDYzRyQ1RvT1hvL0RZazQ2R0prbkE0eG14WGFWSk5KRnhpd0JGdUJ6aHFVaDVMWkc3S0oxa0sxZCtHSGRXcnlDL0h4Y01SQkdKdGtjL2tWVFZrLzBZb0s3a0lydUlLekxxZ1R3NTVoT2NoNlpTOGpHdzlUOWtKbWVZbC9ITHowYkQ4TVBtbk0zT1pQTytUdmhYazM3NjJFL0JUTFZQaERQN0VIbmFIVU9MclJHZHdsVVhkV0VwMHNKdWRud1RzeVIvTnE3SHUrUTlST3B1V29UZWI5OGtWR1hSdVRwU0FNb29saWF1dENZYlhFeGM5ejM4NmNJNGVvVVlTWDY1TC94Znpjemt4dFJvM1VYRi9PS1czMjRORzExd2lDWFRxcjRtdmlVNDh0TlhFWTNsSGx6YnBSckN0TzhqYmwxMDJwSmFTY3l2ZFRGSEhCZGIxSXMzVVdIU3lqUVRoSXp5YlkrVGlUdmNXaDg5ZGxUV2dDZTNYR056UCtrb2ZJb0Y2Qk1qT0dJL3g0dWswMUhhaDZJdE41OUhJTUFNclN0MWY0ektuTEw3YzBVYURDNEwzaDJ0WnJhMXEvTmVZRUdXbE1QU3F4K1ZSMHB4RlN0THlZdlhtMDhzM3dadzFWaitScC9pL2FYZUVTU3ZzZjBOVDg2cnV4d1g2a2h4TTcvVnhxR1EzbFJPL1VETDJwUVM0V1RUdG9Cd05ralowTFJ4WmhndkpRcnFEVFlRS0pZU0NMZmpVY0RvRGRyRlIvS0o1RG0rdWE2a2RsRkVCMTg3a0dYMnVFTVRVU3MyWWV2YUdkQkdaNU1DRWZBQXUxc1Q3bjBCNDRLWmFXWDlDWEE0blZnOHRqSlZuNktzZGFtMFM2NjJTSnIzTmhZdk11aDdyK3lwWVhUaGFQa1FROHR3MW5EWTNJVWJkMHFoSWE1UFEvU1R0M3AxVlBaZCs1NFlPZUhOcVFIR0plM0pPbmM3Tnp3dms3NzN1YkRHWS9oWlNTRUIxQzFkUHZJZTB6RjlFR2p2STA5VGhZcVZ5SG5BNTNqVitNcnNQVWhnU0tLVnlPSWluYldVaElwTU1Felh2MlFqUFAyZ1lQd0dkWDBvRXVvN0JmazBxSmNlcU04MGpGY1dFNEhScmtJNmk3d3pQMVBaVERMK295dEVUaTNVV25WMG5HOU1SakFVcGlRaFJuY21xUzBYeDhWL0VGaE5mdGxJRmNaRkZVZVBJeXZLa2lzNS81QTRSWUdod1RrWGtrd3VtMi9aVjRneFRUTEJHdVBPdkE2ZTJYNzVwZGR4RVZsaW03c0V3M3oxY0EwZEk0aDhYTUhlTTZsUXZkeHpsOEtSTVFmUEo2bHRXUjlLSlViWkpjcWR6NzFxL0pXVUVoVytMWm9lMFQ2bTcyWjZTd0g2Nkl4SDJ0YUZ1YjBNTzFqYlYyZFlCaUphbERIcTVVYjdJZXJPOW04Nm5laldwK2ppSFJNb256ZmFiekppRmRJbU9sNXE1WHk3d1FVeDA3ZkFpQkJMZEdzanBYZlFKcDdpYWxGek5kYzdDUDJYWFd5WnVxUWpvbzh4RU9wUlUydGdwRzJwbUJqVkhzZFIwVEpuYlhmTFFHSk1VSi9idDYweCs0cG5VbnYwTnRqMVRaZWl5V1JoaTVmWmxBUXRLemRFUjBDNWVTcXNTTHd3YkplS3N2KzVTNzRnS1dRVlFmU2YvSnk2Z0p2bDJmQStYeDlwNGJjM3d1Y0dYVG83ZXIvODkzeGF6QUpFKzJZMUJlS1VlQzMwMFNzZXd4RXgzTlR4YW9CaG95UGJuR1ozai8zWjNnNUpiYmdsYk5ZR1J1elFQWGV4L2tlYjB5d2ZBYzFYY3UzNS9pNVV6NXB4S0lJZW1yK0R6SHVKREdBT0VOdUlVUENtSmR5S2xhb3BCQUxPemxZMjJlbEk2UEZQM3hiaGdhRzd2enFFb2tHbVppVnB6QTVjdjhINkRwdExNaTc0Slg5emZFRm5tRFA5MTRSaFJ5ckx6cTlocEM4WWVkSEVmdWs3VUNLVG8wZWdpUGFuMFMzcjNZRkJhdDRBM0RaKzZtdzRsc2RzR3FpWWhGTHVNaSswTFlRY3E5YlhRcVlpL1RzRk1HaWJoS3BkZVpMWW1VNVFpR1M1QzVWSU4wUVNwMGtuNS9nYWpBSjRxWUZKNnNRTENWVUd2ZlJCUmFyRVMwVFlxQU1xNlI1ZXFZKzYyRi9icXpab1BIdmZzRGEzQ1ZmZFhVcmlzZ3JsbEY3cGZzZC9EVTB1NThienFjR3EweWRGaGZZaWhHZ3ZCaUlmRHJzSEdkekU5UkJGSWplQ0l0d291TU1GclF2NVZNNlpIKzMrMm5tNno2dGRCZUtzTmh0YUpqU1hpbi9IUUoyOVJaSk1uOWNqWEo3YU83N0k1UUZTOEZOeHI5RmNIV0lhR0hsdDRaQmJtcDRSRGpyVFcvejRaRkx4OEp5S2lsU3RxTVNsMG5TMUhSVldKaU5ITVh3eDRVVkhuWk1IOTRHQXVEaHc5emV6UFhUQ05FRnhJSE0xSlJyTE0xWHlvaWxSN1YwS1JxcGNJZWgyaDVOaGpka0RObzl3bkVBclVTSTJVeXpsRHlVN3RWVGRkVHk1TnhuV2UzeEtuQ3NyNUhkRzI1a3NLVTBvRUFWeHNaVVNCS29TRU5TbUs5K011UC9zdXFtSUkrVkxoNmRWTERtNSsveWMwR00wcSs4eWZJTmFXRlNtRHpFZ3JmV2RZNzFMYjFjbXlUZ2UxUHAvL0hlMmFmdWlSbHhrSGd1MWp0aEp5bHBUeFJlQkxBYkI1VldLbnZnMmVZN3libXhRYW8vV094bWpqTGhWb2F2SlgvUWErbEl6K0pSQVVVbkJqTTJ1UFRyNlRCeWZvcCt6T1dKWWJQSmRYUkQxUHhhNzVCRnhDRDhPSDliS2JYT0I3SXBqVnVMSHhQd1NOVkFzR2JtanZJT1I0N3ZWblZpTllhdnFsTzNJSHFSUnFSaGYvSWdWUi9ISlVNRW01UTkyekZ5YVlvcFE0alpNSFBvK2QyTjdLdENTcHBXelVWRk5OVXZiL0x0azNVbVBreGt0OGRJSkZyNGJWclpVTGRDNTYvQWJLSUNRQ3FLUGhDVy9GSXRWUCtYTk1RdU1wbXpDc2ZTdzUrdVZNVVpFQTUxNWVSWHhtN1dFZVNxNnZDOTc4clpwM0lZVGtmREVKR0dYY3g4b3h4MWJwVTJPbTRYYk5qYysrZFA3bm1TcFdobEhZbzJvT3pQRmVlT0w0cTAveHEvWVBaU2pFK0grRzZMUUZlaDdVMTllR2ZzWUxWN0JkUjY2RktTeVBIRW5rZCt6TmwzcXBnZmxRMHd6N0o0UmNkVTNZcDQ5NDVCL0JlU3dPaTZlK0kvWDM2aUs2bTVGUjdIMDUwRDN3a0RGNCt2SldJVnhYbmw5RW9wYzhETEdyZHMydDI0Mm5ySkJ5Y3VJSjBCcGFmVUgyYjZOMlBHZGJMdzFYVkZzUk5jbkZISXQybzdjdWc2enduZlI2UUFKekFFaFcrbnd2VmhhNk1zSHl2ZVBhMmQ2dkljdU04eFZ2WisvVzl0aUN2NzZtY3dnZTJhRkx3TnBIVnp4ZlpyN3ZHVlc3ZGFZMUt1SHZ4MExDeGVwK0laNi95MnpkcFo5c0RVL0dHbWRYSDdTNU9xckFlOFdpczRNYjFMMkpsaU03YTVqSnF6MlUvc1RiMHl5eGl4aHZVUlRtZnhETWNHcFNsTS95blNVcXNWZ0JSM0xRUFVhNUZBZG5YdDVIZkJTdUJxSUp5Ti9Nb0JxMnM5dFdaYllHMzdXR0xKODBMczRWVm44VVIrQTh2c1ZtMmwvazRZbXZHdHYvRllqYkNHMmMvcStlZ0JTcUhNUTBuV2JCZmliWTlRK1VsQU1YYTMvZVBJSE5jMFk0SUt2RDRJNnR4b1p0dXFra0cvOHBvMUFIZC9aVlVydFcwOUZGVEs4Wk1MdXdKSXZTZDZkalRXYzhEZ3dkb241THM0b2E3c3ZOTWFNZVhQRURUYU1Ba0s0dnNERHlxbmEzMzF5QTJoTVhUdTRaVG9PNHpYdkdyYSt1VXR1cHhmM1lJNGtHV29sKytkRnhmeEJvRjN3cU5HNERkVlM1NnRYbGxOcVlxeXZJVUxaQWFwVkFHMHFSd1B3blcrdmxhQlJqNkRLWHN2eVh3bDNuRklZYjNDMnBCeDRyNllJdWNlSUs3RzgrdXI1dlh6QlFpcTBmblRFMDRDT0pDREN0THc5UkFtdVdhZDU5dUN0QkFFWXVRZ3hndy91TXpFNS80WmVvTDdvOVIrcTkxbEpadVNmRTY0bUVzQTllNzg4WCt3ODdhZDlCWm1tZitTWFd5UVVjRHZYT2g0QW1GTkRPcWdEeEIxY0VUMXJXU0xOQUwwbHVXa0l2NC9jUDcycVpWVFZySUhlK1VSUVJsZk05cXFrbTA2cy9QcmlKZmU3dUdDUS83MkpWazdWSkVaRWlLdnp6SDdDZSthaFdVc3ZJb0FVUzZpUVBGdDJTTWE4cE1kT2MzU2FpeTI3akNOUDRCQVVLaTMvU1h1V3RocjhQMURGbFJQN09HM0FvU1ZEcXlITVZGNGtOVUhTbGJaU3N5KzAzR1BDTTZNMmZ5bUZqZk1sR3pjRXowdW00Sjk4M3dtVktzRE1DbS9EVWl1SkFybmdobVRrbjB1dThmTno0a0JWbFAyRTJCdStpRi9VNUpsdVBSN2hFaHp2Wk4xMzNhUmhMaHZtbnJUcUxUcGljYWR6eHlsUzltM09uWjRTSk1pcFR6SmY0bmJIK0RkdWoxTHA0eEswMEcvTW9iUjBDUm45Vmx0U3d6L0dXaWNwTGdmMWNBbmE0b09kSkR0WHlwbFFxWURkRGhBcWVLNlBwTmhlT2FjYmdPblA4QUl3QzBIVVdpK1ZVUDFOSGt2azdFdWtmNVV0d2FBZGVYWG9CV3I1MVUrU3VQWHNIYVhrNzlFN3ZhK2psbXUrSDV6SHRDNW1vRFB4Tm9HMWFuQVpHRFNWVm95ZGJFSXZGcEJwMTBhUnU3SFlzTTVnY1RRcng3eUtrL1FnNUlWZmgxcXl6NFJPMnlJR2FONStoTDFsTkplcmhKOVRQTmlsR0FRVTBYM2hjdFlFenExaXlsbFArOEZDbXRSaEdqQngvWlZ2V0t2NDdLcVVCUGNYQ1pFU0dObWN5VHBrWlB3SWdrTWVFdmVGN29hUFFPbVZ3OVFObW1mYTN0U0tsZ3JVQkRQcFVDTUNWYVA4amRlQ0ZhWFhiSTd2RWJJUnZhc2ZVTjNFY1RsV3k5c3d4eEdMM25vZjUvRjgzbWN4L3I5OXM5SUV3VEF5bjNGMWdJY1pyeHFSYkY4MVRTQ1NQT1ZPaTRPTjF5Z1RucG1lRVllQ1p6amZyUkN5WWNNVUF1V3haV3FQcVNRbVlEYndXY1NIUzFpM3ZQNHN6dGMyNS95aUIwMGdlSmtUcno5Um9jTW5IQzRYVmNhN0l2ZDF2WW9VaTBwS2Z3a1c5RUhmaFpYb3JUb2hYQnhPUitSMjh3VnRDL2J3Q2VMa21jQTNHbmlpRXRSL1lZaUpoVTdEZDMxOEJTdW8rdXdFSGd3dnIxOHRyN0c5SVlwbjRlREpsOG1CelpuQm1zTzNJR1NVZjJUWG5ET1o1MWdqT24vcTZmS2tTdCtmbTk2TVZIaDlmZENCUXloOG42dVMrdGFXdlpsMnBodWxHSUg3M01WamhVejE5YWM3Rllkd2RKVDJOeTFBQVIzb1U1U04yVnJ6Snl2bjh2c1FkVGx6RHFBK0JFQzcxTEpSQk04Y0NvR3JoY1lOR21tN2FNRlMzYU1SdlgrTDFZaHNqRDhBYTFtalJzUFM5VTlvei94a3BtakNLL3k2QTFBcDZTcFdiZVpnTlBzcWxtcG1KNzMzd1RzSTJaR0NmeEVicjFNcS9vRWRPbWhTVC9oTFdCb1NsVkUrbmhyWUhMV0syWEMxQ3hnd25tbDF6b3EwekU0d1ZPZnRiejl5R0VZZDRvWHFzcmZxQzU4MU5JdGZuNVNzRnJUNjJ0ZDlLMktLbGpQb0JpUXJlSFgweTM2dU1IdU12N3U4Vk9uaUFOMXBpS1E2STZYcGgxUHFqTC96aE0rWm1BMC9HNTZmR2RqS1ZKQ25JaC9Nalh5Nys2bE9hZEp4cDRLcGxhNnRURU1WemU3SnV3Mk1GeExwTk1XSk1KV3BZaCtqQldVSUp4MkRTZUpFaDBjTWtiY1FvRE83UndTbXg0MjBIOE5DSmxUSG53NmVIUkdZakg1RExaMlJPK1JyUFoxYzV3MGIzbTN6V3RDSU1wR0cvNEIzVUV4QVplZlVHN0tVYThxWW1OL2hDRWN2ZDd3dTBOM1dZMG1WWGI2blVYUWIvSytrOWVLSFFSR2YwSnR2VXorL2g0dUVaRUJyYzFHeExqemg2YklhcFpGaTFVbklkYnZ3eGxESEtiS2czcGVVcE9TWmpjdWFZWlB5cG9QOGkvdTJyWFhrVEs5dVdwd3FmRlBaTXA0Tm13ZDhrT01aMTd5MUZvN0FQcVZFbU9KNGMxUm1jcGF4cjd1QnU4a1BYdVpOVkM4Y0Y3TjR4MWVhZlZNVE5sdjU0a084dkk2dis3NHRvdTUxbjNtR1NZWFBvZnRZczc3OXVTZlgvY0pxRFRaUk9tMWFFV0Q0V2IxY1ZCa3hhN0g1TGx5Z3Rhc0dXS2pKR29WWHJsVHRiM085RGJERFFrblcyQVZNSWgrWXVvbFhNcHlxSit4dVJOVWRXZ2lVNERzeW10d1dOaFIxVXB6Yy9WQ29wc0NvcHRJV0hYcSthdkl5NHh1T2R2ck1hc2t1dEVCRytlMmVYTW1TU1p1OWZwYjlDT2RkRTdXK3M2bTh3b0ZwaGNUT0JqYmxQZ3BZSjVycFAycFc2V1ZNa1pCRXhXaVpibWRxcmlkenMzdkFObWxxTGVQUGRqRnJvMm9QaWRCODVsa1pxK1Evc0FIWVBkVmszQnM2bjFHTFozRkFKYjdnL0s0T2EvVjhYRTlwTVRway9JVmFyS2dPYlJkWWZseU95TS95a0dCYWRubjUyUG90MTVqSlJIUm9WYkhPZEloK2dacUJXN2QrYXBNcUJTZXl3RGlraWsxZFBSaEhqTnRPb21vTHdxSGM0d2tRNlFDeUJiSkh1L0YvcXc0a0Q1NHZ4SUZBM1NyWWZMZkJoTEVrMzdFTDczQTVjeU9kdDhDQ2dZeG5RL3hBSXYvR2FEcnIza2xoUzNVWDRQNGxaOVNWeGNGWW1VVGpRaWh1QUxLc3lhcytKSTlkdW9rNFNGWDZPekxKc21HbDZBMU4zek40aEFsZXk4cVlNSGFCZHpLb1BZeFFPSGI1NHNnZGs4c2pTRzdzblA4a0NVTm1sNjFPdVE2ZkJUTmFOOTlkMVM0Z0prRVdpWEFoTWxTRUNtWnV5MzJGVWxrRmNwNDhCZTdQK2pZbTRkUmNwU25YdG9sUEQ4NWRvUTZNL2t4MGJzREUwRTdkWTJLdmhWZjZDM2NiMXloZWlMeHN4SG1RV1d3Uk10T0xwNkNZS2M5a0szenE5c1hyMG16SExxSnBFczRobEpqcXFrS0kwczA2dCtIaTV3K1RtMmY5b200bDJ0NGlESGNXci9tcVRZVjc5VEJndGcxMUFBaUloVHYwZGdraHA3SHZMaUl6M3U3MDZBOC9xdjhNV3NXMEFwcjVKMnBtK1VvcEFzUm5Ga1dtK0dWUWRRSHp2dXJuYTBRbnNmbEgvVXo2dUxWV2Fmb01IRkUwT21BQ2I5QStvWStaMWdsQWFHZHR2TDFDUzNUL2xoZnZPSWNvdGEwTy8wZlB2TFVYbVNMVUdzKzNZa3gyb1FZUTdQSmFyZUFFeVhOUUYzVm1XV01pdTZ3VFNBanNtclR2NWxTdDRLSlpabFlUamlVNWZRazB6aGpYUHV2Q0Z6UlA4SDBMMm9CRlh0MkJFYVc1TDE1RW4yTWFmaGxodjFPaEthQzJERk1yaDNiLzFVMVlQL3RuWUtRWG1hNVllcDlGQ3BJT2p3emsxOTh6Wk1TK2RIT0lMdHA0RVo0ekkyL0czelUxUHhkbHhlTTlaL0tMT0U5SEZRaFQxUkQ4aVc3dXZySUVRSzlyMVhmc2hvL1JTY0k0cGxKWnZPSEhDdXc4dFB0ODJPZjViTU83Q0VkbEcvTjVQM0RyOU44VHBCa0R5dEJOcEg3NTZlSDB0QVprblhuL280QW43c3lQQk1ITngydURKSHBFanlhVnVHMUlIdmNiRkdPS0F2UzNyVXdpUmtpMFZSZU1aOEhyVFlQQ3pyRW9HejlpUGp0T1J1Nm5tRVJQeExmZkovUW4waU14MkNlb2V3VnZnRC9kaUtXZ3RaUVdYakFPRGplaFVpWExmaHphZ2c5Vko1VnNLNDIrNVZiRXYwcTN3RUluZlZ3bWl3ZWVRelpTUUwrallGQ0ladTBSUlJ6SFBJcUdQdEliODg0WHJzRUttdWJoNFFzL1dzamhDbDNkUDRGdzk2U01TRVZBanBtMnUrK2lhcmdvZm1LNVB2aTV1Y1llWHpwN1p0cUFaVW4wVHkrZmsyY2hwb2lsaEdtMWRKYkcrdVNYVVZJSTRRQytIb3RCNFRaWHdKSzVpVklnV0ZBOVFYRFdTRDIrSDdGd2I5NWFnSzJ1ZW1vMEtxaEV6SllWdjhnYkRsSC93MURCYkZ1Z2Zod24vZ2RnMnZBUndRUUNUMDFPcThPWlpHeG0rWWhiMHFJQVczUWJ6UUVENThPRVZNeXE0NmRJQ1FxVm8vejNiK1NnKzQzaEdUZnBIWUJhWHY3YU01dWZvenowL2N3WEEzdTRnRWV3bVAzdGhZU2ppVytoQy9lR1Bia0dSdEdRVkhydE1JN2dkbXlCR2J3UVppbXN2R2R6VUpuekdtNTk1SW5DaXhiMWtvRnRUeDhDeXZ2V0dhY211MjBUYUVDbXV2a2FTRXRPUytwTFdDRjh2Sk55Wk9TOGV5SDA2S0hlazhzUTRXQW1rTGNCTFBaTUVteHJ5OWFlRmRSaVgySjRpbEYrOExRUGhPdTc5S2I0QWlNT0dlQ1EwR2Q3aGM4RHdNV004SkNuZm1wSzZuNEJSTnJOM21IUC9aNGVJbUlsK1B4QVErUm5YWERNeExiUm1TcDVXcHUxSURBQlBDWnhmMVorTHhQOHBkQ2swQTJZM3lWbVYyNWlyMDFwcWNhNXVEVlpiUTdKNHdSMVdLOHo0UmFPeVg5UHBnQkNmdGVPSjkxZS9TK3VzaDFtbFdXL09XNmhIUEFXZVg4elc4THkzdTN3bW1ubGg5MExPZUM1MCtlQ0NPU1JjMTk5T3NWblpMQ3RIVWpYOGFyNWdlRDdYcWJMVXVNWGM4c0ZlYk9GOXR4WUxPQytVMVMwUlEyd21JdStJL0VNc0ZSOGdjNi9tQXNVMDR3SU00dFgrM1A0NUpFZE1ib2J6c0dXK2lWNlZNeklaTUZ0OGcxNzlUcGkrSU94RzZoSTBKV2pUOFRvMjRBOTlCMHZudzVnNStqM1EyWmx1TXFqMzNVd1hONnFOZW1zbEFlajhKUTJnaWEyM3FxaGcrSDduNU90ZjcycCtKYXJmdk9YaWtaMXJxN0E0SzlJRTBNcFNOVmE4WmlpNkxWL1VrOGVHYzdYQUtEcmNNRVhTOEhuZjlvZzJFNWprOUcrQ1ZtN09oc2tEQWN4OE1Uakt2bDZMeUEwYzV1VVNORDZNUlJTOE91dmcxSzZsZmVuL2VBYlVlQ2ZtTnJNbUtjc2hURG9QK1ZadTVwc3VKUTR2eE5xMmRnSnFtSlZHVHA0Mjh2NVRYYlZRcGw2TEJycThvbXh5cmg3RGRxdlkzZEtSNWxMSnY0c2plU1FCQUMrTkMzVUZDVnNQZFJwZThIT0JIajNVaGFaZXRscndYU0o4ODBpZW1OazRaVVlCNU95NzkveXlQRFlVdVpuTm5mNmtBS0V3bTFvUkZXZzdWTkdabTJTcXg2V2kvek9NbHVudFprT2x1ZTRtMjlYd3FoM2N3UVFMQnByTEw4VjNPTGpOaVVRcFpGcWw2aUxPQ2JEOGxyNUxhMVBVL0Z0Tkl3clZHaUJETnB2bGFVekl1UnhsWG5GNEJTVG1DUUVqNG8yelRwVTlkUnJ5cFR3cmxITlRxRUhMU0RqdEdadmlHWklPV2RqS1pGUTJTMGpRcjB5RjcxKzA2cmJKT1UwYldrV29Ub0F4MGxqS0dNc3VzNUY0RWx3OUV0TnRZNXZPVzN2TUFyamUvdWlaTDRobkFYT3JabHVsVHBTYmtDdUpMZ0VXUUV0dHNTYlNLRTVsaHZmK1ZoczZML3FRZ0xzTFpNNVIxa1ZacTdaZFFqYkhwUEFWNFV1emdkVExteHlPdnFvVHlDeFJvNUM2VWVsbTRxWXJ4anZtS1hyamFmeEdyZjdFbFNLelBBcmJuM2I2L0pha3dPdXAwT3hQdWZMRm44NkJxODkwUE5Sb0xZb25WVDVIZGd0YzMxTFZiQjRtak5DcTQvMWpETE9Ib0pjdHl5a2N5R3YrMEtzbmRTZ1pkVW15cVV6REJMbCs2Z0MwUU43UTI0c01PSnk5SU9DV2g3Yi9WazA4MWFhc1dVS1RRaVNqYzdpUTBETTJId3pyOUVKc0VYUC9hbDVZT0hIbjBIYlBUaU9JcFh3TnoyVCtNMldGdkc0UEpGTC9YWjN3SDJLdUs4a01zRnRvSzFxYkpBaFRZbmJibllZRFZUdGl3d1Q5RHZXWG4vMEtPcit4aTZwOCtVTFh4aUJWWGx6TGpOL3lkcHRoamRNUmpxd21ZdnV6UWFKTWs1TXJqY2tCQnpvdHcySmEvTnNUYml5L0JON21UZE40c0gzaWptRzZ4Y2dxRHNNdDZNSVpjVDJrU0NYVnlxUWZiUkVKYnFjSzV4dUJ6Ykw3QzFqUUUxRDZKbklYaGt0SzJ1ajlaSXRNUDFhRlR3YktrclZ2eGhxUkp5TVRwOHQyT21DTDRLTmY0R08zajFYV1l3RHdHNFpzb0wwMmpNOGU2RVBRaVZ3U0UrSkpRcXJDaHZLUzZGbEVzYi9QUVo0SnFnOWE3TmczUVB0eTZjVFllbm90RGV4KzJ6cG1ON0g5VDhSYnMzeTF0MllOVXhqZWwrOVIzRVVlUk4yMG5XeGx1MkJYOTgvSWhoSEhZa3grMUJFOUVncWJHYXZlb3BKUjRGQUdEWitaY2wvUC85a0FBUUFhQUVBQUFUQUFRQUFQQVFNd0FnQUFBQUFBN2dGbFFBQUNBQWtDQVFFS0FnQURBQmxVYUdseklHbHpJRzltSUVacGJtZGxjaUFvTXlrPTwvQkRCPgogICAgPC9CSVI-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT43PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MVo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-RmluZ2VyPC9UeXBlPgogICAgICAgICAgICA8U3VidHlwZT5SaWdodCBMaXR0bGVGaW5nZXI8L1N1YnR5cGU-CiAgICAgICAgICAgIDxMZXZlbD5SYXc8L0xldmVsPgogICAgICAgICAgICA8UHVycG9zZT5FbnJvbGw8L1B1cnBvc2U-CiAgICAgICAgICAgIDxRdWFsaXR5PgogICAgICAgICAgICAgICAgPEFsZ29yaXRobT4KICAgICAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPkhNQUM8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgICAgICA8VHlwZT5TSEEtMjU2PC9UeXBlPgogICAgICAgICAgICAgICAgPC9BbGdvcml0aG0-CiAgICAgICAgICAgICAgICA8U2NvcmU-NDc8L1Njb3JlPgogICAgICAgICAgICA8L1F1YWxpdHk-CiAgICAgICAgPC9CREJJbmZvPgogICAgICAgIDxCREI-UmtsU0FEQXlNQUFBQVQvY0FBRUJBUUFCUDh3SDVnd2NEQ0FyQXo4QUFBQUFBQUVpQUVBQUR3SUFRQUlBUUFJRkFBRUI5QUgwQWZRQjlBZ0ZIUUVSQVk4QUFUOWJBQUFBREdwUUlDQU5Db2NLQUFBQUZHWjBlWEJxY0RJZ0FBQUFBR3B3TWlBQUFBQkhhbkF5YUFBQUFCWnBhR1J5QUFBQmp3QUFBUkVBQVFjSEFBQUFBQUFQWTI5c2NnRUFBQUFBQUJFQUFBQWFjbVZ6SUFBQUFCSnlaWE5qbWNvQUFabktBQUVBQUFBQUFBQnFjREpqLzAvL1VRQXBBQUFBQUFFUkFBQUJqd0FBQUFBQUFBQUFBQUFCRVFBQUFZOEFBQUFBQUFBQUFBQUJCd0VCLzJRQUl3QUJRM0psWVhSdmNqb2dTbUZ6VUdWeUlGWmxjbk5wYjI0Z01TNDVNREF1TWY5a0FCQUFBVVZ1WTBsRU9rZFNRbFF3TWY5U0FBd0FBZ0FKQUFVRUJBQUIvMXdBRTBCQVNFaFFTRWhRU0VoUVNFaFFTRWhRLzVBQUNnQUFBQUUrWXdBQi81UGZHMkFVTFBjTHM0eXpxMENpVU93aGRLWXU3QTBvN1dZcnhjc2NVc1dwNkpLYWh2Z2JraUlveFN3MHpZbzlGZllFTFpmaEJNNnJwZk9BZ0lEeWlJZHhOaUo5Y2FSZTlCS21jSzRkbWZ6QWdJRHhBR2JyS3B5bWFZUUwrUzRBbzZqSjdNajhCNEQ5WWhCZE5UMXBLSG53QzRRQ0xBeE5UUk54MVFEM2cyNUIrU2lOQXQvSFdBQVlyWGh4eDQxL3FpVFZrS3VtTjhMTnFEVjlsSFR3U1crQW85UVI1Z0FxZ25CUGJ4UlYrRmg5YkNHVEZMMnVMQWFLYTYxcS9LVEF1ZCttcW84TDkyZ2lSaGdDT3dtWml3cW5wRnE5ZFN3WlFtQmpZYkxLb3RZQXN1a0Y0OUZCc2kzQURBWnZ4SEhncmxuc2tuMXFFQ1dNTFlGVDJmdHovdkZBeGVMaWdMcks4ZjFHc0RReEx1Rzk2NWc5eDRHNGRNQUFjSVJxemh2c1lVb0U0MU9jRzVJMGN0Ny9QOGdBM0VDL2U2ZkVVdmo4WmY4TXMrWnd2Y0VuMHQvcEw1c2wzZ0ZHczB5N0IwaTRIUElmU1plTlFYUlBjbHQrR3I4aS9PNHBaMlpxbEFObnhCRHduejl5QmlDczYxTEt4Yk9aN0ZxMElDZzJRRjJrMVBGbE8zRlpQcDM4NUpLTlFCUkN6RGlFT3BMcjBFZHFINTY0UjZlM0w3WHRBZUtIZ3VwUkxPZDlZRGtIRFdFb2IyYWUzQktMc29jRFFENUJuNUY3bklOU1ZlUXVKUHRPOEVzWWRHYWJwM0I3eVVDTzVHMGErS2RvcDFuQlV4SjVPSGlKbloxblpQNGFROHpNeFNBU0xySlFIakV6aU5temNSM2EycEk2dFFiRyt0ZnYwWFJ1NkZLK2gyTFoyKzZaQTBVOTBJYUQxUExRRlN5QW02TmpCWi9RYnlXTnhGdjZXZFNxSG1yd0xOa0pWbHl0UHpYZ2ozRjY2c2Q1VjZ5eE5UV1dTcXBGNWx0em9IaFVhN0JDenRIQlZGamxpemcvZjFOUXNPay82SnJRbXgrRVNEeVEzdlNWSVd0SkpVa1RBUFhvbzdLcG53UnRhME1vMitMKzZDOENCNlJxS1g2dGcwY0FLeFh5OXdxQWhvS1ArRFFNaEx3K1BpeHZFRE9vOXpLRXk4ZEFvNlBvTzljdS9QVDRud0FJbnBreWU1bGZjckJMWHh4bEVhQjJzTGQ3QU1lai96WXJTakNFUXhBSUZ0YnhTS2lXVW5HTHUrTTJmM1J5MUtvbXZvRG5VdHJJMDV0YjRyb0Q2bGQyWnUzOG10S0VUeHdVNW8zMUl5N0VGQ0xvamtORzV6U2RhcUV6SktaN1JwcXc0bVRLeitUSXFvMjhRUWZ1N3BVQTFFY1JFbVRrWVBsbFZjY2V1KytOVFFzU2VrMy9BRTBsRHRROHJjWGExdU1vVGE3TjBjWHBNVmxmRWhtWFNkbks3ZllhbGcwM3JUN2VIa0Jya2tsZStIY0taR2N0NktBSjJFa25nOFllK3lMSjdnMjZLRDE3TG5uNGdFUzdaRS9Ib25oQWR4UkcvSG9tZlFuT0NiVmVYcmFsOFdoWkRIWWFnSHd1RE9lWlBOUGhFdmo2MUJ2ODdxK1k5L29SNE9VZEoxNUV4WlFYYXVQSU5heHdlWFE1dUJPOVg2ZmJxeVpzVmhQeU1uUnVsVXRVbUtQa1N5R2VMaGdRWEdpZmlpTDdubWcxUE9DNFV5cXpndllidmJDL2M2VDhJVm82TDUwNGhQdXJ3UDRhRHBXODRHRTRhMVJVN1h5WXhjaEtRYTRBbzJSM09QOFNFWEIyaVBNZ3Ruei9OWHBJYW85bUMvdU44WW9SY1QyRUYyZXFPYkZyMFFDdDZFcVVkMVkxNGFoQ0JHaUpCTWF3bmtVS0VOa3djQ3BsRWtqMytuWFMvZ2Vuam5DS2I5WDhuTlBuTGVQU0NObERISjZjUm0rQjdkVEdLQ2lkVWU3QUV5bkZUQm5VekVzVUxpb0YvcTRSRTlVcWtqdlh4SEM1SUFTU0VRamFjN0ZINzZ0R3RiMlVpS0NVMTUrbEpBejVLeGJYRzFzUlBJZnZER3U2NnBEbkZNYitGcjlPcnkvOURvU2FTT0VwakhQTmlxS1hKVHJTQ3JTRFRMTm9acTN1L29mdmdCZ28xak1OMmRuc1RTcHBNMmJhN0Z1Z2hnU3pJWm1DU2pCNDBjRnFkT2RabSsrUVRuSnhESTlmK0hLS3JEakNoMVVBYUN6Qm1HdFVUQnFDMkhCbG1PdFBDMnd4NXg4WXlOT05ZUGhQa2t2TUlDd1VnbnBmc0JaUCtDb1BkbEJ1dnRQTHIwWkthV1ljK1JZYXNmSC9hL3Vjc0RTR1Bna3Vpbk5UWHRBSWpEVDlESUVJOFlkMVIxb2t2OHltOEVjckh4a21HL0hIQTNjZFlMTzg2MStVOGRLTEhHbVlPZE9PdlhtdTJwUGhFMjlKOFhhQjRqK0RlYkk5ZEs1Rmx3OXN5RVQrd0Y2VlBnc1h1L0VKNG0yVHFMWjlOOVBNK0t5UE5rR1Z0N281MDFBMWJCU0JHdmNGSGZ0RGhORnhDWmltWVY5RjhvYW9jMnRQVlJYeFZvOHJic2w3MHhycWhXN055MDhEU054VmxXeGltTXFSbFZzQTlPS3F2UU1LWjZHWUl6bDNKQithYzJFM2RaeHVsTWc1YlZjVzJHTlQ0RjVBbUpYWFplVTgxY04vVXBjWjh4bk1UWEtpK0E2YzBqOCtUMVRzaXhOU05QWlNOajRLVTZobHBmbjc4cFluUDlwdXJwY3ljZ1REZ1BFNW1kcHlYTEVhcjNJQk51MTBqMWVDK2wrcGZBSEtOaDBGRVNFcmJRdlc4eWg1R3BraHJ2Y2lIbWNGWU9DbTg0YzBOemNFY3ZjLzBzWFlpMURFL20yVTkzbVlybXJobnNhdFdUbVorYVpmUENQa0Z4ZCsydW5uMkgwNStzUUNreWtJTUJ3UHJ1Z0FtbFVBeXJaTzlPc085TTJMdTRUVlliaE1odFpUcWhYN3h0Q2lSc25zUi9pMUs3TXVIMlRtY05VdmFCdnhZdStmcWpjbTdvQmNzQm1XbFIrYko0RG5lZDNWUU1ZUUFhQ3ZkcVhvTnBtbDArM1drSThKd0QwaXlJVTlXYVdaa3JsUWo5TU1RdWtBT0R4TEJyYS85MXFjSWRyUUhrZVZwekR1dVQyakUxd2U3RVhKVTNiRlFsTGxLOVo1Ykx6WVQvT0NNNzBLb1FPU1p2dnJPS04rTTFIdjRzd2NaSnU3ajdVQmh1TTlXN0Ztc3l0ZDhjUFZmZk9iRGJVWSt6dEpoQ0FsZ0hMUVNOM3RGbU9BMjlkNXFVUFVWOXhsOU5yNHk5UEsxN1E1Yzd4OGczT2tXcmxmUFdiUkpOckRBNjVTcVhDN2VSdFhMTUZDaVZpRjRNbSt1cHFUV3VTMWttMUFmcVBKSjRBeFhEU284VjhROEg4UllUSXBIYjZTZnlJL0MyQk9xN1k0SlpidDI5aXEyUmJBU0xiVnBCeFFLTnlGRzVlR2ZCc1NWVHd5L09vWC93cWVWMGFoSFFnR3RiSWMxcGtwQU5DZ0pMU3pEaTQvUlYybnV6SzVGVlJRb21KWld2TlNTVUxyaUlGMUg1akJIS2g1ZUFFUGFXalNoS213bWRWTHc1c0RreHQrem9KaEVHbmEyek52Q0s3VkdxT3FvQ0l1YWhKNGJRQkNYTXkyZlQ2U2dud0I0RlQxN0MvQ3lqb0NWTnFyUVoydXIvY3d1L0xMMGtwUkdwbHBuTjREd2JzU0xCUllFOGVFNTJVeUpxajVxSTQyOERLZ2FpeFFBZVlISGdCU2FkUXFsMzl6VkRvakhqRnBINUsxcXZZWHo2WjdmUjNUQXhMbkgyMnlUZ3czZWlIYUl2Z1poNisyK2xaZFJsQ2k2b1RoamVWTm5uSWZRTkV0bkpxdXJrblUrTTl3YzZRNFBQQk5CeDB5MzFENHJZZXA1eURHNXhhV2VIMzV1c3Q3TnN0Yk55OUpwdXIzclhyRjVTUEc4bi9Sd1Buby9WUEE2c05lRGtQclJzd0JFY1ArRU1xWWV6V0tHY0Y1VGFselR6TW1OdUdIeGZ1S2V5MEJ0OXI0VU14a0tDUDRSR1ZHYVBXV2grWlhpUWFPWnNQd21LN3VxSEowQWs3VWcwbVQ0dTdBRzcxY2taMXEzbHVQOTZWdGhaNWR2WTZMOU5zR0lUdGptQ2xLUWNWdEJ5MGU0ZmVJVEpBUGpsUVBLOWJPb2RNZW1pY2ppRkJnVWF6dTV1NndiZjlad2Y1UTRSMjhnODZUT0ZVa1RreGlKWnB4b1F3bVBmTmZ0Q1BHQ3duYlQ1VVYrWkdNQXQ2ZnRRWFRVRnU1clR3VVdmRy9VeGhNQmtMRFZYNCtlQVlFYzZ5NGJLZVF3ait0d01scnI3MzVZbWRIcWhKeUloR2RMd3lSUDJlZkE1ZXBpVVFVZ2hSd1N3Uk1YU2lMc3JxemNRWS9wRTFROHh2M3V0VFZWYmM4dnpKelFRWkFUWEFMeXpKc0xxc216VWFMNlZWWmNtbjNXQ2xQNjA3U3drSGR6SGtCejE3OEZNcWdJTWk5c1dGQ2Z1Y1ZJTWdMcndQd01PY21CUEExMFQxOVJldE5qWkNuc0FHSGg1YytZRE1WUk1NWnNzYUNWcFNtNHpXVUQ0K0xuS0VhU1dvbGpUM3ZqMkY0bTZaeUM1RnM2RXhZbEtqT3B2Z3lYNnorRUtiOGEvU2VnNm5CN212R1R5N2FvRVJ5NzkwQllKcjZMQ0UyUllPa3hsaTB3MEZ2UUpmV0c5aExXTlZTdmdldkxVTjV2Zkw4MGpKbGNjL25IYWZkZVRIUS9uTXNCSTlKVHpKVklsR0hkcE9FdDFZSEpCcTFrd2ZNS0JKRlREYUZhV1ZHZlRGY0gwYis0N0pZNVpOOWszUk1scTdoazZvOGh5anhVUmluZlBFK1YyVnlhbHFUbFI1Mmt6c0pnMU91VTJuY0U1bHZONTd3V0VVSDNqejRaYnErQkNjV3ZFOWRMeWpSblRVMDRNbkxlczhqbE4vREg3YzNPUklpY0pqR3oyajhNNTdXS0VvYWpXOVBmOHpFQjRTc3NuclZ5TUZadjZlQi8ySHBpekM5WDZ6N1JYSUZibk5qWFNxdXRZdVM2b1B4VjdpV09KQzZrUThvZ001WVM1TGdOQ2lOYmtLT1FxUWNhcUt3WW9rcVR2QllnRnY1cGlHQlIwVE5nbW5wRXlvNVo3akZPbWQzNGNYTnZDckRoR3VURG5KNDE5Tk1pSjd5MmRrTFBmTnZYSW1kZGV1OExiRlBKVEZWTStXcG9jM0V5OVJMR1VZWTlvUEhYdXZmOGdkSkpQSWh5VHU3ZU5yMUtqSk1qSjZzTVQvbDlDTU1lbEhZS3ZkY0dtR3J6ZkZ1WkFEb3pYb01XYVpwUEl5ZWZDWVdLaExTNXVXcnE0OFFyU0dNZlA3M0YrRDRGb2FHV0loNWIvYjN2Q3BQM25MMHZlUkY0TW1XTDVxY0xCbUt2OVBqbVVzOWRuZEVjazRsNDRRK1NtWExPZFNkdkFYbk9NdjFFcDZYTVdzWDhveTkxWWQvU0NEa3p2dm96dTZRdzJTeFRhditjQmlXS1JXakY0OVUrZ3FTZWw2dEhVOTdwdCtReHNaK1Y5L09La1lnSFhlV0hKZ201REdmTVRVcGg0Nmt4ZFZ5MERHRGxoMmRxNHNtb0plQUN1aERsNlc1Y2prK29Jdk45OWVvbFBBVTEzNk9vYjhwc2VpOWpNV21aSEkzMUt0eTdQMFBpK2xIcjZpVXdNRjFMNmt4SEt2d0JrRDRWMjlIeTd1MlBTaFBhTm1HYURxM1haZ3BLbTBxeTJ5eXhjcjMxKzdDbTZQQmx2ZkFHOE14Szk2NUx0eHpCUlc2RjZsRE9UbXJ3dTVnMmVuREtrTlVSRjQyMGNxWVJzakNrL29KN1I4REFyaHdOY0NITExsZkIrWExkbFhhSU14WllvbldSbWhReFdPREtmajYxR0I0V1lYZkhPUkxwdUU1bFN2R3JJdEt2eHJhYmR6Qi96RHoyWThpUWRXVWFlNXlPMThrNE1xSkdhc1h2UWRaK1g2dCswbDQ1elpXY1czNlU1WE42NldMMEMxWVVMeW1ZSjQzbS9OZkNVZmkzN0VlWCs3VG04dTBTcFk4N3BhS1VZdm5OWVA1anZadTB6anZBZlVjRGJJZEpUMldjOGV5U0g2NUp4anRPWDIwaVNmR2p0QkQ1blc3dmplNE5NbzBvSC9ieVRHUUxLdTVyRGtrcmxDS01wSjFjTXphQWo1TnVRVzlidjY2RWNVTHdNeXV5aU9Ta214dWtHZEVURm9VSktLZ29zbmNGOVVOU045NnNLek16S1NkZWh5OEdCUC9mWlRlTjVZa1J6OTIySkxLdW5iNmxqZVV2ZXpFZmc1a1hSYU5xbVNsZzFCa0hlSFNnMGVFMTAvbW9UeHYyVDlwZURkSUVCZHRKK1ArSkptcWtIeTlZc1hGREU4bzdTSmUySjhDR0I0UG0vZWRwTlVjOXZVMmRUdHJXYUlOWk1jNHNkNlRJVjltblhJSEJGM2xtd00xdjFVUmY5WVN5WmNKL0dOeEVVek9qc28raXNDdW5tVUliUk5UZFFUVlZ6MDdwOHVaSlE1bFFHR0FPelFlRGtFeHNQc1ZwbjN1dFh1dktqZTQ1MDdsNTlXZks3MVIzU1FROFJHeEtoOCswZEUybnF4V1ZiYTF0SzJHSjZidGVPK3hlYVVoUFM0Z2NaWFljSFhpRWlTMHptMGk0Y3FzaGszZG15a21TTEdhcy9BYUFwTDV2TS9WblBIc3hjME9DUlRmM05FZWgrWHpZd2NheU5tdlBYcm9VN2d4VkIwSEo4bG54bUUvTnc3TFFWcStWQmYyV0JyT0dxbVBseURNOXMxM1k2R01sMUtWcU94SUhhT2wxbnlsbTJxYnI3cGx5bFptRGpOVHQ5ZnRXekU2UjBFZVc4QnV4WWZISGxlMUJlNGhQRVkySDZybTRZdS9zRzAzR1VYRGJCeVVPQ2tnUHJiRVJQVS9kUE9MbnZ4eWFHOVZpbEppL3lhc0FCZ2lIVHU0SlBNUE1pdmxRUGw1QUNoRlVSclBsZlVIK0RHQnQ1MnNaNDlHcFp6TWpzMHFiZ1NnL09vdDNjcFpiZGh4NDQxQVo3bG8yVWNhcVlxM01JV0RrRzVtejNuNzdNZU1JVWtocUNlL3BZVU8zR3djNEYrUGJHVXMvalBWL2RMS1pqcnZFRmVWUmg0SlZMSmN5endDeERMcDRHL2NFMXpzeVRHR25tYkhuK0hFVTd5eWN3amw2NkU0eFdSWjhIRHN3QVlmcnRONXFWVWpoandCNE1Lb0hoZ0NFenpmeGp1TDJqWjdEdytTZE5nSVIxRUcraFcyMkdvdHVZemx1RU9ocVR3alExeXVCTENIeHVJcldNM0YrTmMyN2cwYmd4WTNGQjFwUzBBc1QyZEhyUEQ3dHdpM0g4TVJvRThwTjhWdmJEdjBwNVpiRGkrRUhFc3VrZEp3Q2pIQjkvMkxvejdXWHFMSmp4d0xjNGdyeG1ZZmk1Kyt1RUlJek9BU2krMUpwRFpsZ09sbWNZZG5PS0JlYkU3TEU4QWtrRU9yMVJja3FvZ0htZFJ5ckl1OGJmRGt5eTBqekxLanNkVEh4MnNrdGZtZjZBYWtOMXpQUGlvVXprbVpJSVRIaHJFZEpzY2xYU0NlamZKcWdMMDNRMVlPaTNZSHc5L1F2R3o5MGhFUktsRDBCTk5pK0dRejIwTU5iNHFMdVA5MVQyZitNbnZoTWJJOUYxVDF3NHI0SkdxcTdnc1JtVEtOYUJabkxham95cXdXbnp0bjcrVnBkRjArV2oyM0tUa0xmTmFnQ09OenVad0gwa2hXSldUbDBuVmNtNG56cG1qcVpCOVlKcWlDSE5Eb2xjRDMzYVFhUkl0RTlqQ05mdXovUlFYTHZuTzlqL0kwbS9mVVRpWTNvUjhmSjYvU1ZQdVRBUjErK2YxMlAyU1J5aXJvZXlpaStWUGMyWllRM3NCR1F0Wk9SSzQ0Tzc0M0hzVUwyWDN1bU9nT3pFcjFFei9XdVNHUGNTME9Odkp3Y3V3VndhVXJ5cFZZTDU3VENoOTB6ekg3K2RHdDlwNTVHb2Z1bUdYWHpmWFM3eGp5T0tHbGFsZzU2TVVwdmNsWTRvSlczOUxzc0JsZ3dkUkoyRnBlWWdRUU5QbmtFUFdrNjl3WXM5YitxUUdPYXA5dDBWcjBKcStLdmJaOXZtS2FOc01kaEpOL3pPM0NaRWIvUDBrUTF3anloaFovSHBvcTBVTjUrN3lpSDVQSHFKTHRIcTNrTHZoZGJoZUpjZWEwUVlzQUhxTjdSUHJoSnlJNUk5VlhJT1BOVDJiNDlBTDJUTXpzT3Zid01zeWUwQ0swdlZLWFdoS0dsVzd2WGQvMzlPRXBVN3FzMjNKYzVBRWZic1FoMUdwU2dsUlNIazlGTzBkM3JjRndtTVRzNXExZkZ5UUk2RXFKS1J3cGdHcDJzdGI3ejF6UzFibUFrSHJZbTF3NlRVOWJ4R2hiaWJMRkhNZ052UTkzdm5lZFpTWng3YTJBODMybk5NeS9KMFdnMEg4V3FXc3d1WEZZOVd1ZnVwbk85dGhzK1d2OU5UQm1qMytFV3cvOFBNc2NSQ0w5NE1vUDNLMS8vbmM3ZXZCa2hnWkFLMThTUUhHZ3RTNnFNdXFxUDMzR3hOOFlOM0tlTElINkNhcnV3ZUU5NWU4SmFrNGtiS25UcndwUWlZbjk4Z0RDeTRQSFpaMGloYjEyb0lpeklESkdMSjRmWkdHbWdOeVRhUVI0c2xqcndGdTBsUDU3NWdqb1RkNWZPRkFjWUpsdmNBVzZkdTJuZHZCQ0hXMDd5ZGpQbUNJWXpBdVFFVHF1azNhZDhNaXlJei9nZFYxQzUvT3o5clE4TTdTZjlHZWRlRWNPYkZSTXJpQzNnNG9ZWnBnNWs1NDlMZFNmQmRJTGo0S0ZlclhmYk90MUs3UFRtc25UNkJhTVRVRkU1bm9LS2hCUHBwd0Rka1BzeFBGa0F3b0ZKZnRlZ0lQNHhiK25mTGxsNWQ5TmpFcng2UHk2ZDdJZWtJUStwQThob3dzd00wSjF4KzI0SFA5YytTMFBGMy9YN1NpNzdxb3pleG53WHdvZFBaMEFJVkFPaHlsMnZudlZRNUNiOHFBbmh3VUFqRFJhOVBoTXowV3JWOUNaaEJtTTFpUWxQTW1vbTg0Y3hUeHZ4bVlmS2UweTQzN3FxSzIvS3N4SzA3WlljdFdSdXRsUEpjNGROUHlIRHN5T3BWdXQzY0ZQRzVobVBZQWNYa2NzeElJeUloeHE0L21rNXU1aHNWR1I5WExhaDFRSWE4dE5EYU5UTTl2M0V1OWM1SXJUUmtGNUpCT053ODVuaEdYQmZOUHd6YS9ia250WmwyWEtEYVVoSTdrem5JVnVobTd1RDZSQVlKVzRRTGJrOGNLNkVXQUZOWStKL0FrZHZaRXFyWHJaSE5vMk9EeEE0M3hHOTB0SHdIZmE3bk5vUjNYMHErcUF3RXFqd0JYdkxadUtIZWNtOVF4ODJSL29IZUJycm5Oa0ZCNk93TS81UUhacVNFakZXZndrSEZOazAzLzlqTHJxYUtyRGF4bHFVUnJ0ZWxaTko0VXlQVU4yNURkNFd1ankzVzRvRkhZbUZnUG0yVE1oTkIwSitheHVlVk50Y0tCeVZRRHZyN3E4cHVPSVdYeE44V3pLNW1JMmlLUGNIcEFwd2pSaUdESlpQS0ZEdjBQUEg1M0lTVUpPSUN5cyszZ0kzWU5CaDRnM2J1RHU2WUJxamtQaTgrenloTVd4anBVQnRGVitUcHFYcUpNNUhUOXloS0kyYVVWVkppZ0VOaG8vZVFFSXpoeFNlOU9EQ0hSVVRBTkpJZFU5NGdIdnVCeWowNytTR0x0NzlGaUhGVFhvMFVnRGNLeHYvenFIMk5BZjA4RGs0Qmt5VW40STFCbXBwZzBPNkVjVEN5a2JnZnpBMG1Db1FoV2RlZHJPQytncmpXaUt1UEllUjhQZHhZQlBEL3JxZ0h0aCtmMEcwbWRTRTZvenNEZzJ1c2NtTmRpVDhBV1A1cnlCd1ZNci9QR0RjbFFIMCszdUpBZ1hLS1VCb2ZpRGp3YU5PUDFMYzhHODZIVVdBQjhlZ1dNdnpPbGlFam5qblBhRzVhd1kreUpSaStNekJyZW5CZ1Jmek5KSTlmY2xiOS9YbTdOcDRiSTg5Ynl4Rm1OWmJQRjVkaVdVVENJT3VVcCs1K3lZb3BlWUsrUzVtY0tGand5LzNReVNEY1NDREtjL2tTL1VQYndzMDZGRmtuYXNsV1ZzUjdtNjVRNE5mZ2owalNyMy9zVk1qMnBzL3hSOWpFdE0yOG9Zem1CQlNVOVErN1hGMVlHSVVNdXU2ZXBMbmY2SzUrL0FwZjBqT0YwNXhHdEUybU9XZzBwM2FxL1pNOUZBVlpSbDk2MVFYZlg2Unk1NlhaeENVLzdrekNEVndtQmhOcllvUEN1TTIxOW9pK2NxK21wWndGS1czQmExalppTEJPaHYwZEd1Z053WjFIOC9OY3ZBZml1ZFdiNWsvMWhkUW4zOGhhZ3g5UmdYNmxDZFIzeHBJZ0Y4RWRYMU1oOTB0S0ZRSko1aTZYamNOY1VjeTM3M3YrNlAwTVF2YmdjVmtYK0h5Y245VDRlWHZQOFhoaUNIQncwaC91aGMrbzRZNUE5UDRCZ2Z4NUUybUFhVFZhWmZRRURESE1ZVzdKbW1xZmxUQ3JVVTRVOGFqQXFzY1RUZGtoTXhrVWRHMWExUHlEMUlUclZGZm5NMWxiTFphMjRRTFI5MWFta1kvQkJnWExZYU9PcWJsdndibTBpU0NPV3lKVVhQejFQVjltK2Y4NldxbmZuM1dHSmROQTBjbFRkQkE2K09vaWdQTnAwV1ZITlE3QitsN1V5UEV1U2VrTWVVOU5kTEViUi8ydHNCZjR5N3MyV25uOFZoeGxiazlUZ2FQaXQ5MDlVMU5NamVrNGxKU2pobWhQS3c5WG5MQldXYkM5ZUtPYnMxLzFqWjhobWgvbEZOSFovb2JFM0ViTWpxWjErcUNmYVVMTjltaDZmZmpDN2dCbHlkZnV6eHpHc3RVaTBHNldRck13MDVMbXVuMXdHRllJT2hPV1oxeVNUOXJuMEltT2thVzFkekUrYWhFQnA2TTYyM3V6VTlONGhHWUoyanFTZk90VC9RTzRJREl3N0UzTElnWkd2U215cEVJcUpzd3FHU0ZWbkdFRmI1eVIrblVZYUtWeUJyNis0TmtMV2hiMzdsemJ1UUVRMk91TnZYZXJoeUwrQk9EZTZ0aE5lcG9CWTYxSllXYUFDbHdQc3M4UFl0elBYNTVZYTRnYVlzSWtwcC9GZmpPNVR2RXNWWDJPcFNvTmdPY01scDlFSXdvcUl0SlVYNnM5RmNHM3U0U0dHWC91TDl4NDgyZEZyUWxrakF4TTdBMC9JdVZxOTk1QVRGaW8vUzZFMUlzT3hLdmRhNVdWVmZNOTMyL3RjT1BsQ2QxV250UVdrZDB2UTd1VW82WU9lVktGdTM5eTJwSjRJU1RFOGxIQ0E1dDNFeG1EQkwvVWFlbFI3S1FYeEUvT2VManZPYVlNUFd3L3RMcGtsMVg0Q0FnSURtK21nQXdNYTB5SG9pNk1HZVkxQ2NraTFZSzJsNllsVnlnV0pvblFJVXRVNHNOM3RaNk4rNmRrZXdqMU1PQzhaYU5SUVhQRnpUTFZmOTZVZkJNelp0VkZXQW9GT2pxUllOeVdSclRHdHJwWEo5RnJDVzlaZEUxU2hwUXJkalpieXA0U2RaajNrUlQxVWYwU0NLdS93UnBWTkhHeXY1ZG1QbzBnSS9zbXhiTDdBajFPaVdJVkFUd2FqRStHYjRrNHFNRXhzOGN2RG0xRnVlVUt3Zk11UG9aNTZ2QVBsaldoZFVMbnk1aWJXMW1hQ05nMFVieUEvRHg1QlJ6c3cwQzBEbXQ1bWROenZrK3J5bU1MVFZkU0FkaTFOWEFaVDJXL3g1SzhxdHg2WDNaZnpLWTRmUldCbDMxbWh1SmJSK3V6QU5paTVzZmw3ZzhRbXJQczJjUTJabExWYXpoc2pKVzBzSWdrZ1RBZGdyZ0loTndrOEd6ZGRuV094MEZQOWFkSlVZeFZxZno1WEowdkV6NS9oY0UzMXpWbTJWWUZTZEdheTZ6eUQzYnA5eFdvYWRwVUJUOHVPYkFML1dCOGdsZ0sxWjFSWk9QZmliUHJPSUtWQmsxUUY0SEs4bjFTcFdFYytGZmVjQWxzbitWVytYclFLNnRHRys0aTdRSUVzMVhYbEJVWk5aMGZ3dGtsRzZOR292UnVQYnFsd3pMd0tCQWR3Q0k2YjFadzdBQzlJZG9WVnNvV3hGVndMOFZmVHhKbHpmSkc0eFp6YU4zd3NLU2k5Vyt6OUJFL05PeFBJYWswZXI1eDBRck9VQnhFbld1YXM0Q2VOQ1ZIVWZCWmN2R0ViRHNCWmpaQ1JpU0V2aXVrM3IzOXRXdDkrMlpCYWpUb0IxQU9rMFJucnQ0Z2xCRzFnMTlIQ1BoU1ZISlV4eXk4aWduUGh4VzAxK1pMcXdkMHE5L0xBdk9PaFE0b1piOEx6Z29nVlVKSDVPRkdlNWVCa0YxK1dVdWRyeGNQTUxiOW43c2o2Q3hPVXZzVXN4QmRqanl0ZTlicXgzdmVmcVlEY1NGWW5sUnpHSENXQjl2NUFjVlRZWURRR3VUUjZkVmFlSm1JVjBIQkFTWmkxeVUrMjB1UHNSa3dOeURPd2xTMC81SmUxSzlZdFN1dGUwOENRclkzSE9TV2llZG0vdWRpTEpZQVBJeGRXa3k5VDBaRGhwcXlCUFRBOHhkR25nVFoyZlo1VkdqMjVBOHN5TXZHMUQ1M2ZqcFhtN3M5T2oxR2ZGOUJsMnRCbDFyakVBeFg3bkt1UDRpLzF4M2RBN3ZtNG9xcXRtSHArWkUvREhFcXIrUjVZUjkrN0hES3AyUVJXeDFVdmdWMUdDcjJuenJtMHRYdnNvU1J4VS9iVFJZSkhkai9FNzhCQUo0a3NWeG5oYW1WK3hkZTJCOEVDQW9UNGprcW1RdnBtU3JaSzg0Q2pLMnJlVXAxamtNaWU4a2lFaVMxdU4wNzJXeWtQU2F5N3dseWpPQkR5VWg5VU51TjhEVXYwRFNIak5rRmRyUTZkdGdSbGVMVzhRYjJBSlRlSDAxbjRHeSsvUlY3K09mcGpwUC9DT24zRFM2N1dCd0gwa0Z4TmY1dTU4RU5nMWF4eGdwUm9rUE9vb3Y0MmhyNVpXQkgrTGRURC9HMHJsdnVGM2FobFlYSHJNWGtESUNMaWNyTmxqdVdiR04vOW1tQU1Zc0FSb2tTdC9WaFNSUFcvMUQxa2llYi80dFRKUVhsYVdUay9JMVVVQ2hXWXJndEtGZGxMZnZQQ0tFVkU3L0owaklyaEtudDB0L0xJSGJRbSthek9TYUZ4WUlHc3lpc1g0L001UjVWeld4bE0vTE14ampSQXVCOEhrWTRHNjBXR3RBTTFXem1TREdwMld3eEM2a3BqYXQ1WDRKdjNXRWFnQSs3QXNpNjZUS1pDVklCdTlGelROY0FHQlhBZ0ltZWVlbHhtaDMwVktqTjBWL0wySU8rOWY4K2VHdktGYmU0Q2R2ZTh1NGxMd0VXTmRvTEhjV0kvYXRTVjhQaEUrMXBrVlh4NTVDSm9wNWNscXhVOFlXdVZ6bWxRTjkvV3dIMEp5N2dWRzhUa01NYk5CQ0VRdEYxaXFiZGd2aVZzY2o3T2o1UmhDaW41TXZJekEzTmtZdEVlSlRNMUNGWDlFZ3BZU0xoVG52UjNtbE9zbkNVc2FKQW85dVFDUWs5YWV3TUxsK0k5ek5WWnVqQ2hLRlVSc3BaRmNZS0J6aFlucWpBUUxITEVST3JNTm9GQlpheVg5dEIweG1TZzB3WXhtcWlMblB4Tk5BZ1laRVlaUHk4cnNmQTdZSlJHSW9NWWljeWljTGxVTXdQUTFLelNQSHlqYkJBaGdVa1FtSGFFQzV0TmxsUVE2VmdVMjRJazVlcHJ3d3p1YWZVNmM5NVU4RmtVVVByZVVzMUdGWDI1ZTlqTjJJWC83SVlJZHhpeUJ1SGgxdUlEREpBUU9sSTFlL3RUVncrYVZlK2F0elZtUFBhb0FwckQvVlI1V2pPYk1NdEwybWZQa05HRjh4WTU0cUZuK2VNWjFuTExTa3lKN1lFaXZ3SmhWSWJUWnJnNHpqYkFZcnJUOUI0cTFuZFZuUnhyVHF6ZG9ZY3ZZZVBQdVVFdDFUWFNiUFI0U3lmRXRJeGpmY2tHRUc4aFVxZnNKbTlPdVVkNlZNRlZFY2JiaE5QazNnMEE5NnFSU0FDM1gxUHBjNkZwanNvcDFqSFpFcnhaV2wzeVhJVzBsa2VNR3V2bXcrNmNwTU5SN0VJTCs3Z0lUenVhQkFkUmlELzFmOVRsRk5jd2N4NGZCTlNsYlJ2Zy9XK0pCU29lSDRVOFR0QXVib2xRWXU0SXN2bEtWbkZITHFyMnVib252TXRXOUl1QnR4OHR2a25ybnJYOWhncGJMRDVPdUNxUUlpdlUvb2ZWTnBYU2lsMjF3b0RHSjBxNXY5cmRVRExLQ1ZPcTRKbFB0QlhsaVRHN25NSnZJekRTTUNQNVZMaFB5Sjc3aUttNzh1SGpaNkJjQXM0SGFOdFhzZ1o1SFIxb0svZHBtdVJyUk41Rks0T2ZHR2ZZQjhGNk1HS1lVV0lrOXh6UURWOGg2L1EySm40ZzZ2Q1RiaGZlVVJhcWF3MERKT0xDcnA3ZWFOOEN5QkV4RUFObVUwcHQxMGw1ZXQ2RDlPNTlheWY3eEM2cXpHQXlXZTl2eThScFhDOTRBWk1uMHhkZis5ZWs0d0tjd1RsUnBMeCtCVy9yakErdXhvVTRYK016L0ZGazBldnA0TjFLZ2V1V2VCSTdiMlg3K1NpaVlRZFBjb3lnY1paMi9Pd3hka2RBWnRhRTVRNzdpNmJkWDNIVnFER2hGMEcwTWQ5alN6dmg5WDM4NklGRkF3bmpzRVpiSy9qeWNvbTh0N0hVV0g1K25GYW9oMFpTUGoydDVTMGNFNFhRUnhVNHl0S3NpZW11L2I0d1FVVmZWUk1sa2F6YjZrZmtQM0pUL0ZSQ0RoVUYraXVDbUVyYW9GK2NrMVB4dE5mb0tMSU5BVWxMdk4rRkx6UUtOMlJvZmttOWdBa2RuVTZKdmFoQURLL1htMmNTWTZTM2hFMm9STHlQQjFiSHZHZHU5VGd2NGUxVlBIb242SEJNMm9Edmd3Sy9XejU3ZjJWN2VTUm5kV3NYMkdtcGM1VnNmTk5ua3lXWjVScU85VW5jQ0UrakFMRWE5aGRFQmpwRCtiZnAzUjRDck10RThTZ0g4aTRxWk1rcEIvendTTStNL2FISzI1TFNPSVNHQmIrUmlOME9hVVBJKzJxVjQySURvakNJMzhTN1orQ2JlZk1DMGdQUHZrMjRQKy9PWGpRM1o4YW9ua0FWNENzUmsvdVBNMmZ5Z1Jyc2I1Q25lcUYwbXd4TEVFUUFJTEVLN2l1Y1BhT3RlYXV4S0FzaWNsc053WnQwbFdteW5uZHNpVzluWStnMHlUUnJlOVF2NXFtdU9wY216cE5VRnhGeTAzdG5uNFpwWXJqRUsxRWNRNjRNaEIreTVkL0xNZkJCaHE2RndxenBNZDIwVXdYYTdMYjhkS252TWlhMm42dmx1L3FNbE5tYjlaV1I5Ymo5bHl6NnkvMnhpYjlkSC9XWkQrbzBmYmEwWDIwYi9COUFGKzl2QXhaUWthL2Vaa1JlaWo3QTZsVEVueWRxZnBBdVB4enlRNTN1UTlvVFIxOW5ESUdrYXU1UUhKSWhnWDZuV0hPamtxMVlMUTZ6ZjBIZFl3SW12Q1B3aDJqQlpxdklYanRudzN0SlRHTHFwNFhyRzlUV3ZMMXFqT3pXWllxdzRvQlF5UVdUNnBNNlcrbWJJN05IM0t2VkR2Y0JCL2swUmlOYTlpakNGZzMrTm14OGszekhHd1BUYmp3UFFWb3Zpb1NyUm80MGVJVUl5WDhIcVhyWC9PSzJhQmNPN2FYa0c1NTJFeG9hVVExUGNDSjJTcnZwN3VFL1ZiS3J2dmk1c3B0WExwVldiNkZZa0l4dk01ekxPSnVRNnJWNVg5R1FwV01VZDRXWjJUWkR0aWNyeHg0c1U5R2NCenhJaGJ2SnNIellWMmxoVlNsMDJLdnhwMThGWEN6QzR4U0V6dXdiNUFzT016YStxUnQ4dlRWaVNQbEtUbFAyMFJsOThQU3FjR1FkMlY2aGtuQ09CamM5enpBanVnb210UU5IRHJXbGlTc3djdmFIRDZPT0RBNDBYR2RQUzhIdEg3SUhaSXIwSmtPQURsRjFtbGxMK0R3Q3h4SGkwM3l5R2cxN3FnQWd5MkFKU1JYVHA3eDJtZUczdXFnajBDOEtCeUpoT2tSaENFaFV1OTFYUGdLdFRub1FRVFZtTTVvSGsxM0c0bTJJVTJ0SGl3OGtxT3A0UXRIZVpRaThIN29LTjRPVXVSa1hOZ0xONFNsdDd1QUkzdGNDRm54U2hKSlhHa3h4TnBxQmd6eGd3dHZZZEdOQ1JpZGtQMnpURm5BTzFtZUJrYjlTTVlveFg2MzhyTUlieC8zMzBuMGpsdXVzZDY4OExEdHZ4Y01MR1B0MmRLdGpMb2JZM0I2R0RFVFFkMlEwZ2tRMmdHb0tnMnJEK3diZkxTa2NSditZb3dTMXBrbVdkWi9EZXhkWVhnUWtZY2RudEFKM25hdElwUzk5emJpZFJsbVYyL3FKemtVLzVzQXdHV2FCMEVLbkU1TkIrb1Zibm1CbVQyaEx6QTBjejFxU2JnWXdGRTREZEsyRzZ3ZWlEc1VsNXVsVGlRTTVmdmRuRmVXdkcyZmZIRmIrTkVrZnFJV083VllQVkJXQ3JxdDFZUWNIMUZMZjRhOUVJY1JONG9VV3Z2aTQrMWJjMVRMQ0V6cDVHUzRXcFJjTWdSSHhGYVNZdllDRXVPU2xnS1dpS0ROR3dRdW0zTGlCNEJKdGlTNVhwVEpaM3JtY2JBL1BjUGRoU3FqbElHWWRKaGJWeDIrZVNNRUxqaC9hREROK3g0cU8yVWlXMkVZVm1aUTljdm84c0xDSlBjVGhsamxBWUFPT0JRQzJYU2FlQ25mOWFOZzF1SDZMVlZFSUtxSEgzNVlLRzg2OUpmd2R2SkdzK3RoMUkyT2Q5K1BnZTlDS1lybXpzWnJ6RksxamFSaUYwYStJTFZLWkFUekJoOFhEeCtrbkxuNHdKb2NWMk9RNG5SZ0tLanZXQzNaRWpZOWY5YVBLLzV6WDIvRUNDQTlqb0ZlampaOUc4NW1VanJ0cEZCUnpEcDVOMTBWU21sRHYrUkhXdzlkWGZ3QldJcjdNNmdsdWVkT09SOGVGL3RUbEt3R25qWk5NZjRHRmNOS0JueEFLRlNtQ292eStQTnc5TTVWcDZ1YlQrWThTTmM5ZFB4SVoxNEd4TjJ5cVc3bXY5SHFVcHRFRWVoYXdnWFBLTXFubzdlQm4vWTd5RGdRYWZpQkpyczB4eFUrZ05KL24yQnlRb2N3VjFneDRPR2NRd3RvaGNxRmhFaEF6bFdhbU0xTncrT3RhVjJQek1PeCtOTU9ZcFdqSjR1bEZHaXBWRG9JWFVYNkxwNjdSL3FVdDdYdjV0azhpck4yUTFBVVdsVEVKcGhON1dOQ3ArTEljd09yZHNOQ0xsb1hBZjVtNmY1K2h1NUlicElvVlljOXNZblhuK1N3aURZYmpwN1dmc3EyK0kxQUhnNXJwc1BRK0h5L2hqdmMzVHcrbFp5WmlTcGtMN1pNVitsY3hBUlNwc210bmN1L01zVmtEckdraG56TFVwcGpweERxY2VtZDc0ZmtiaEd2dFd6OUdMeUhYNitnQ3JaZ2tZOVhvSXNNc1FQRXlMVEdxZXBOWHNVdmU1MWszS3RyTU5kZHQ4UU8xWlhvL29pb2xHbzhQMTgwcjl1MFBvT1RreXA2QVE4V1ZPYzVobTR0aTlrMU9ZQnQzc0h5RnVRanZtd0V3ZFRtd0IzVFlCcUJJajdma1NSZVRvMUhVYjJMNGFDa21EMWZ3RzVCamZNNGI4dXdTNWphOEYzT25idmU0akx3aUZiOWNYR1d3bDEyZUdoL0pOdDczeDdPeFEyZGtIM0F1b2VUNXlRdG1wVGFnaGxhWUZiU3ZEUkdVd3VkUzh4aVlsd1VicEpzOXlGUjFjVmRiMXlBVE9SNVlON1BHV0pMNDBUKzhCTGlnODhHYmZTZzFTZFp1aXkveFJvdlBwN1E1TDJ2Zk9NSFRzOTBnbXUwb2NBbUhqenQySzVzY3JoSGJSQkV6VVYzOWExL3FPWk5yNlhlMFdSbVNzZlI2cHRaRXRKbW5tWkVjOWF2WmI3bWlsTTFQTmZPMitNRUJ3VW1KN2FuNGdwNVFvMUFEaXA0VGVJakRZR3hzaWxzMWhuaDgxR3pJY1hjVlpUek90SnY0bzg0ajd2a0Y4ZVpkT01pWkJsWFBDd3dxbVlHdVZzeDRJMmt2YUQyMUczVWQ4aVJaZmk0Z2M0VjZDUWVmZGZXdWhheDdac21HeitNMSt6aGFGd2xSei93UG9UWWlrUXN3Z2hYR2Z6Ni9CVzZZZkRyWjNmWG1RWFBrVTc3c3BrZVM4dzlmT1VFTWdFeXdzVWliQXBVWUJQa1EvOWw0OS9UODNtNnZkdVVReSsyYTZSbm1ZbElFSXlOMkxXRktBTkZrWFExREJBYWgveEVoSHpRNCtGKyt0dUU0MjVvRFRCaXRiWklDMlpPbXdLZEVsZ3FaWmtDYllyOVdOd1hONklTaDUvZWF1M0IrQ3BCd1lKdUpLVCtNOHhGQUdQc2R3ckRnd1lVWHVzVzFXcWQvcEViVER5T0ZidjQ1OWYxU2pHMzRncHBVQS9YOFBZbS96NVpvUDN4R3hhTUJnK2FvR3JTYXVyMHV5eDA4Mm1iMHBYbFpneXgvTEF4ZUZxUlcrVml2YmM2Z2VJcG5mbjQ2aUlKRzdnUXpjc0lRRzQ1MnFvMWpDVnBzYlgxaXoyVWU1MW91YlVnaGtpaUhReDgxbVNJR2hkU1hlN1NORWlpQVoyUFlKblIrZ05mWlJaT0g3b05rVEg2b1Q5ZkxEeG5LUTBsemY5aHZmbUNWQmlLcXJTUWxKcGsrdUNReFIzWHRFcUpjYWlRa0tqM0dzUHJSWnFVMnd4U0o1RGRycUc5bnFMdkQwb1d3MDM3OEdtaXR6M3FId01BVDk5dnpnSXhWVUlqLy9NRkNxR0pxSzBLOU0zZGJGTUF0UGVjMWlQRzJxUGEvd2F2a0NEVEV4c1RoSVVqQktycTJwN1k5NVFRejkvWjNWcG9kaU1xZkxPVGIrcDFjblRkNmxXZDlPSS82aXpqeGJjVG05cVJrY2NWenZERFZNQk83SDY1ak55Zko0RUp6cHQ5aDlNWEtvZy93dHJneUJGa2w4WlBqV2pWbXl0Y09iZ01MV1I3KzNhZkx2MDYzMVhLVFMrUHJ0SWJDdGw3RHhtWS9WSTlFN2ZlT2NMZ0oyeXRuN1Q3WjYxUE5xUnRGLzJrR0I0NHhyYlQxL1FoSTMxeVVDcWY3RUtkcjgxaCtoTDd2a3BGZFdOZ0tvRDJmWHlMOFJ2MER1RGZvREtkZkpkV2oyZzE0RW44WUVhS0lNQld0OVhqVEZnai9kMENVUDFJK3lkRlk1OWpHMGMxZzEzek5ZZ1FQWU9ldUFmT0Y1ZGw2ZVdLTm14ajN6UU8vWmNsVGJmQ2hmSUdsSVhjQW4zSHhTZWFUbWVJdnBTZWNnZ25LS0RCbTFDTkRtRmVKeVhQTmlyYnE1MHM5Z0l5bzlVWEttM1lVaEpDZC82a1hjMGMyR1o4QXFmaFkzTGVSUGVIQ3F4d1N0NWFwRDM5TUFqbmxkczh2SG9UdjM1Y004ZUU5U0ZucUI4eUM3TmlJalRWTzY5SkxoQWdKaHhocnc0cTlWSDZNM204c2tRRVRJYUhpckhzQk8zcVRJQ2RsaklIZ0FhR0pxQ2UvYlhpWDJFYmZtUFpFMmlOcm1PVisycUlsdlZMbXI0ZlpLdnljVkhoOEJwK3IyZXdGbnRYeks3ZTFvU0RVejVRZEpxQkI1QnpxZlBrUFJYQ2lsSmdSVFZoaW5mK2lKSEhWTS9hNFZkTUkxbFBQSEViejVBeTlKRlkxeFhjQjRTMWhBd2dXUGhRRzVXc0dOSzRtMjJqQVNRMmhRMnRkbmNvUHYrNDE4RGNiSU5abVl2a0FHZURvQ2lzbXUwK2w4NEtMaXdzaFJEdjBqYUE0eGo3NEhGZC82dXBPY0xKMEpUaHFRcTRBd2FGKys5bVhVck9OS3FvbUdBcVFPR1ZiK0FMUFo4ZHdIU2w0MWhMZlJQQ1FDajFSWGY5amRhV0dIb0VjMStmVDRuYjd6MDhVU2dnVkZDZWZNUlo3Tlh0aGcvWHg0NDA0eURsNDFsd0E4Z1F2RlE3SFk2ZTAyZFhPbDFURDg2NUliN21nNnJ3dzh0WDgra3ZKU2F4MWoxM1QxNEI3U1pDY1RpcGl3NzFCdFh0TjRseExkQWQyQk5hWk5VOVVFWDl2TDhjZ2RkQlJ1RW5PL0Y0MVZHZC8wQkZET2ZLWW9IbEhndTRyZ3pRVHFzYmZyaGo4UEhKNWxEZWpuSWlKc3owM0p2eFlud3NCOTh1bGtVZElQTGhuc1I3VUVEb0NXYVd0N0IwNUk3cU9tZ3VOSllpa0tISHA2dmVMcVgzNXR5bzhCemFXVk9hTWdScE1CSFFkU3BGc0owWVU5cUlNK3FkdjVXWEQwVkw0MW51YURtNXRnUStIWWFOUmVwTHlVdWZTNGZIMGl1dCt4ZkYrcDZoZmh3eTdGZ3BXSkZqOCtKemZ6NzVBb0NibDRuUmZ4T1YybENmRDA4VXVDREZoSHpJTlU0VXZlSGRqNWRNTkNnelFwRmptVDRaNjZSMUlGWk1UaEhhMzV1dWEvclVCUG90Ums0VG5RSGtuVVRVTzJ1U3BySEZRdEZHL24zVHV0eFBYeDgwZjhucmszSE1vU2hWWDc2d2QwMEc2Qlo1U1RrQkYxTWJxdGgyNUNvakpyaDA3Zko0NE9JNlVyT092dGZaVFFYYVlMNmI3K1hVTmNaWGRjS0Yyd0NJOXRuQjJ3WStQOTRuOVlMdWtIVUZNZEVnbmx5QUJ1QWlKcGNhNzEwR0RnUFN1R0lpdktUWFhITk1IQ0kzUGM5ZS9tZGNaSGZ3SHJ6cEZUek5FWFB6NHVyMmdiN0tVQ0N6bW1BRnpTSjdsSm5xcnhJZHAzSXg0TjM3QlYrZVZRbnBoT2l2eDNGSDkveTlDNHhtTmFkYjBmK2Z4RjlucFg0N0dNRVNSck1Vd3NkM2pLb0xDZEw5TElDSUpJeUFoK21tU3o2V3JSYm5mWkRnanpKMk54bENHc21oR0hNUkJNVkk2QVREeXZycy93QzBtZFowZVpYSEs0bWxtWVMrZHRXOWtya3p1dEhTZTgrNlV0REpYcS8zVkxydmVDNGxKWG1TbTVGc1JmK25Db0ZDNWdtc2JVMDYzcVNEUktyY0xaVjFDODJiM2x0TEcyUE5oSVJ1cnFIUzdZQkgxNGQwaXJIdFNVeUl3R2NmUEIvMVdZZkpYb3FHam5kMW1tZXJGOGN0ZWYyNHRvRTdpRml5RlRuTVFyZmhONFlTcHpkQ3VBZEZicnpiWUs0U01DNXNmMGl3TzFsS1NyK1FhZVFneEFQS0czaUVnck1GeTZIQlRQLzJRbUVKbzFyYTc5cWoxTnMzeGFaTUp4RlYveU9pOFNwNFlKWURJOHVGWWQrMFFqNnF2YjRxSHVSZytHMnlNUGpnZGJwdVdUQmg2NEVTYlppMEUvZkNaMDQ5K2xINEJOallmc014ZWVRUlZucXhqY3EvbmxxUHZjZTJ0MlVDb2xwUjFaOFRpdk55SmN6WkF5ZjVVUjRlUkkzN0lsV2xzcWtXdW9HZVFWekhzZlNKQU16TWVFOVFMT2ljRVpreWV2RHhDM1NEc3JlZ2xFR05ZMGhPcCs5VW0yQjlWQVVYRWt1UkpDSnduR2I1Nk9KSWYrQ05kZHpmSFhrZWptTy9SbVhxSXNrVk5hVlA4NWpVa0NPOHF1Sm5Cc3JRSFRmTTNzc1hPUmErYnFjK0hiYnRZMnhlSTRpYm1JQVd0QTBFNEZCQm1SbDNvOXp1ekZqT1c5VmFVeE1mT0VUWnZ2SW5yM2dUSnBPNHhSNTBnTWdNUmY4dmtZTHpvaW03MHRUZnFwaFJhV2Z4bjhNcVNGQ1czcGRscU1sS0FLd3dRT2NYOVhzNXdPSTlvTWtJUlFEelNjd1pBTUd0TTgydytTTDF6WkZKUWkxUHN3c0ZWVzVvbTVZY1dxNzEzUGIyR0RZd3cxT2FGNU5CSmwwd3JESnp6OFp6U1VIVjhzRE45K0s5Y3FZenVYSGpjNHM3YnV6UlNOWlhUZXNaL2U0MldLNysvT1VUby9taWMvc2JqdDlBTzNYNmhrWHUxOXFYZFJHNVFjQ2lHaG00dmtXMFN4MGhIWjJVbE5jeEtrZmdFcU5DQitoaW9rVVJwbnA5U3Q3NWtmbnY4RVkyNTlSVUw3UnNqMGJTMkN0MVFqVFRFWHRacHhkN2JGVUxsRXRWeU1HMGxoR3hpM3hyVXNFUVhVQ283eDIrR3pyVmtFSS9yNG0yRElEcVVnUTR6dWxjQ1Bldjczc0VPOVV2UGhPTU5wN2FmQzlkN25lc0o5RVBYempPWFlsWHo1TXNuMmhGZnp2S0NLQUJSc085WDUvT1JOSk9EM00wM3Z6NkxxbkRKMCtiVXMyVDBycklHOHFEeCszdlR3b0R3QThBWW1DMlVzdUtrUGVhS0hqSVIrOFhrMHE3cXNRNXZkekdEN0g2U3JUMUg1dHkzL0VPQVl2cTJVMnlhbWJ0R0xCdm1OaVNERG8vUldwbS84UFlSbEJhNTdwOHBtajdUV2ZjMk1HdE96aGtrZHk4Tk9PemhROFVBN2RqQmh3TUJMa2I1T05BeTBZWGd6dmowWElqLzFpcmR0SkhRc29HNHloaSt3cEJrUWlEdkdmZEJsY1FldTBoRzVDcUFVdmtNSTlsYm5sVktYTXJ5U1NTUHNsd2xnKysySmlzZnNuV3prc1hvbDVDKzZTUHZRVXdrTjRqTTV0NVZwT3FMa1VLNFZUOUl6V2x5ZHBMOUpBNUQrRTI0cC9JQklsaXEvV2ZNR2tuUzBZM1VhMFAyZS9aU2VwVDJ6LzFYQllmZk4rUUY0QzZCRTNuZFlnVGRMWU1ibDBVbmluSml2RkgxZmpXSFhQdk44N1VmV3hjOVBPcSsydTQ1UHYxS2NMeVp3aGd3d2JiYllMR1RRbE5WNVdFemJtWGI2bXlXd1pZRkk1WExZTEEwSTdyRFdLbVAvZDJNNFJzcm9pYkFqQmZ4ZHVwMGdUS0pjTWEvMFY2MUIyLzhyckFCeWVkb0piZFVPdTZUT0UrWklja0pzU2w1UkJ6R1Zxa3FDU25Fc0JibGpSZklDb0ZtaGFwMWo3dUZPYkxXdkxWQmxZK0dYdWxjNUw3TGRvUFRIMVRoeFZ1QTRVb01BaHkzbWxodmlyRzd3ZGhsVkVGYnRJK3dpcFNNdUhyTFh0RkFxdSsyVjdUVFd5RXdtaUNJRzlha1ExbzJ6QmsyV2R4MytVcmlXM1o5UTVRaDJJUjd2cWEySUw2TUVpbXBrYW5XYVpWZHNadWRZTXJRSGRielVjRU9CRDFDRTBYN0FnVWRYckEwazVQeGduSjFES0h0MTBxVGZSeVdwNjBMSGJmZ09IWUx4ZGkvMFgySG5QbkdnbEZoeFNFZ1BGRkRvckNrTTdSSFpISUdWNnJJMFNWU3lZN2t5WDZNc2Nac0s5ei9NV29BR3lkMU0wYnd5ME5GQkV5d3B1aW5MdjFBZ0hjK2wvYml5VTV6YkV2MVh5Vy9lREloTmZFb1Z6YXVvUjA4TXI1MnZ2VzdzeGh0MllURk03RUZJSzZNODhaeEc4UU40dEVIVmJOQ1dFV1ZKYXFWeTF3UmZhQmNNRW1vNGFoQ3A0dXA0bnhOdndXbElQOWZneW81WDNJWThQTElzcVIwV1hucytobFhPVmNQSVRSN0lBcUZ1dmthc3hQRVRPR21kdHpCZ0U1dWdSWTVjdmwzTUpNell3MnFoYVN5eTdlYmNJVTNmbjAxRmZSSGx0NGtad0d2Y2FrQmJZQmw3WFJqRGZJSjNodjRXMmkzTTJNbDd4VDhUQXpORGJ3Snd3dUM3bERFcm1NWHF5VmlkRC96SDVlWkdwQ0VpWm13SmNxNC9xY0dGMHg1cUxQVU9NTko5dUlJUms1eFFVU2RRaVdFNXgwdFZRa24weEw3aVp1WHhPU21INkdzaDVWaUJUcG42WjhoQUpkZlA2cC91YkhTSGhqOHNnODBUTXFSOGJ5b01XcWJPclR3QXlHTkhjV2dTUnBvSlhCUnRrQzVza3BaejFDeHhUckNUalRsYUtFaEgrc2M5ZTdUZ1M5SDFEY3RCMGJDTmVUVUpYMVN4bGU0bGcrNmxIVTJRQWlKdTVCd2xTZFNCT0JXa2IyL0pzZWRxUnNXYzdQOG9YZWkrZjg2eHVHcFd4aUVCenV2aGp0MlUrek5FTmR4WW5YSnB5dS9BTlN2MDIydm9EdkJ1V0QxNkZ3bGhZcWoxeHFxNFN0NlNOQWxXVWY5c3lBamdVcDNta0J4RllPZ3Nvd3duOTJSdGgzeUZkZVIwR1NKVHd0QldGMkltNXZrSWw2VjZrTVgzd3hBZ1lMeVZ0M2QrYW9Halhhd1JCNFQwbDNPcFAyOTBaSW1QbEVFa290aGpnTnVFSFQvZ3BlajR6Ui9hODhLei9wUnFuZnpsbzkrWE93TnMrbE5pVm9iek81Mkd2eDJ3QURJQk5Kc3cyb1krVXV0SjNmZFZnMEVsdnE5bitjY1B2TENmLzNJdEtFd1RlMFhoWXRySFNEVjRnalduVjUyaGViUnF0LzlxckQ2TXgwZG53bGdqYUZvcVlYcnNMNnd3R0RTOTNpMEJ3M3JhK3JJYUpTSG40UDVYMUo4QnNtYUxCR2U0NHJ4UHBYM21QZkhmakFNRVUvWW1EOWZxU2FWWEJXaFNOTkRyMnk4UHNOUXRKaFlzT3Y1ZWFQclU5VWNvUzhJL2pHcDdhdEFxeGxtVVJQNmRYNlRISVBDbVJqSWdUOGJFemlaY1lkNy9XL0lKNjZNenJnV2RFclYxa1JFTm9abzAxQUh5bUV0SFpiay81NmtLZ2V5cTZaVDVVQVlPaUFqWkhudEdhaFR5OXI5b2NvZnNadkUxNDQ2WWFRME5oeFdQNFIxM1RHM0JITzhJaWNDb3lrcE1nNHEyR1BmSVc3K1F0YUhMYTNlWGxoWEx5Uk1NYVd6aG5WOVV3dU5uUkZvV3ZQNEYyZy9PQ1RDY003WUtpMHZoaFhVbEdzUWtTSy9yaTRyNGw2UmxGZ21sRTdkc2pnb2F4QVVBVC9XOWNHTkJoM1BwNkRmUE1WS2xxQXNBQ2k1dDFSMGx1b0puaFFuM2l1enVvTytHWkhRbDVwTFpHMTFlenl0V0NZV3pYSVJVb0ZxaThZVmFLa041ZTRuVzR2M0VKbnJEcVRCYkpYMi90MU1aOElpelVkU0xKRitlUUdJTlRJdkxxRXdvM1pMTEl1cWp4eWV1b0hPREw1eGVZTFoyZWtGam9DUkxqRXNDbzR6bFl5Y3EwMC9WRW5XRFE5MTdkOFdFbkZOT1ZWUWs5ZlZSUTQveVhmeEYxNkdtUHhqYm1jZVh0ZU5oWE9kVlMzdHdvSERBVWJ1SDM1L1dEY3BVdDJsRytqS25DMVdOQ21uc0lzR2ZZZzJubEN3R0JGc214MTN2bjN3TnVycDNGR2g5T1hWYzVHSGJZcldXZG5Nem9xSWxDY2pSYTBERmFqYWd5bTRTZm93NFpjc0wveGhMTTlGbkJmMlYzWWxrOTlod1lRdzhTdXRIZ0lLOGFQUi9ZZU9oRERCMnVDUjkyWkY0ZmxLNXJnYjhTbS92SGVLZGhkT1lOYVB2STBGQndjYnZvd25USm9qc2NUeVdBL2tua2hsdy9QdUsrRXdDVWxWV1FKUW9Wem9rY0c3T1RjUk1hS1pkYm1zMmNmYnlrTXptRzI1bzE2cjVKNmpQdlBiWjZhUHc1MmF6OUFma3BGUGxYQUhJa3BNN3Fra1BiMGRWTEdxSVZMNHcydm5vOTZVSUZjWEFNdkhuUXBkUjB6Y2tVNEhoYi9kVjEwUEZqNTEyQ2g2QVEyZlc5dkRkK2syY1JXaXVIaGpvYXNLdmRKdEhoMEI1V0VSYTFab0FmdjZCM0Q0SGVDQktOcnQrZnBiNzZEMDFqMUM0MXRzRDFlWVlPcHlwUlc5aXhtenJZdXBFcnhrVUpTTEFUTnN5VXBVV01Pak5vZjBDc3o4MzB4bktyUlUyMkRXNFlXK2tGcyt3WU8xdTBwSmpWbDR1d3gxSUtYWkgxYmtkcjZzZ2hZelV6M2NFeXVXcTV3bHlET3ZSbmh3MXMwZ3I4ZXpoTTc0eVpKcThPK1ZETE1Ua3lobW5UcEQ3RTZMWC96MWQ4TDk0c2g0QVlyV1craG5PTG9meEl5VzNscnEzK1JlUnRGeXFLR3Zya25PdC93ZElaQmFYZWgyK2dmNXZJaUQzTFJYMnNkbnRZZ2xFN0pRMDlEK2RVSTY2TDhCakpzVGMzbjhQb2hvb3dLMUdLVThDdUpRQmhIWnhBSTBCOW5tNTIvcm1PME9DQTJtenI2ZEFPbUVyckRVa2QveFpiaUQwV081aXd0SnRidnI1T3lmOGZraUN5cm5ScUJNZ21SNnNaK1BIYm4zUzRHMjkvS2xiWXR6bC93NWVQN0U1ejNhUXN0YkdHbFJLQXpVRUtnWSt1T2Y2RlpiL0ZUREhqSmd5eFBLZnN2V281TE1zbmNjQWlNMCs0R0J6eHEvMVpUbTBRLzNYenpLMStDNGpPZFY1WXp5Sy9nTmZaNWYrZkFISWM5KzBxSDRSMUNKZEhhVWtlZ2sreTlJd09TUVNqazFsNDhCL1pTaEJlL2lJQThobDJabVRlc0NYY0NWYWladSt3MjhDdHVBV28rNWZHT3Z0VWlNUkwweFJBb3M5RUkvZzlJRENxcndrMmlCamJzbnFWNkRKWVZCaW9FS0F0K1NzV1Roa2lqVytyMUtGQ21sN3FzTkZxSlY1V0M4My9FV3JONWZrMWR2Qitqb0psMzdZUmZqY0xkbEpYa1RrQWM0ZVQwVHNseVhHNWFRamhkWHAvdk9yejZyUzdHOVRvb1NYQ3hJeWwrOS9OTmxnSVNNSExET1p3VFNHQnk4YzI5OHdmVnNuWW5YVjByQXN0bGxvajBOQThYcXFGYXpvdUtnSEl0eCtNQVh2ZmszRFdHNmFXQnVpR1FwS04vNWo2SHVhU1ZLSTF3QXVpeGtRSFJDaXQ2bzZtbE5nanJiWW9ySDN5RC9DSkpXZFlHd2RuU2FUTVFWY2NpVDNxZ2FXeC9UaTU1U1I3MjV0RFpXcm96NVZaRHUvZ0h3ZUZKb0ZPY0FzMDFCclNiTDNodHkwTDQ1KzZOaWI4NmJBL0ZVUUZZQlBlYVBLTURTMWJCdzRxOTllMnl3UUJkc2dVKzM4bnNGdDY4aVVkdWh6cEYwU0dNSVcvUGNkUDkyZC9kNzVsSXg1amR4Q2xYUEVwdkw2dlZUYnpQaDVleGpScGNIeHZ3MDdFamFQanRIQSt4ZVg2RmVpb25IUzNSUWN4NWd3SGcvVWFuUjRsTnJ4M2lheFNERi9ISTZzQ1hudStlSm5oc3lZK21CZmhpS2xvaG15U29POG40UVU2VFRSTXFxcW5mcmFDdWxqOWgzejQwaklRT2lHNm9Nb2lHN3Z4UHg0RlppbkRxWHpWbWlsaTc2OWNabVV5bHZXMTB6Qm5hSFpOYkdjQW5xVlkzK0wyOWRFcmFVOU0yVmFOUDJsUGJCUnQ2U2NLNHhLdmZZTmVOQzZWY1dlSHRpcjhyQi9FSytZV0lveWJZVDZZMTNKZmxKYTJCcUY3OGxoRjNoUWxlaFhjSXpvVkNieXZLSFkyUVpaUWwyditTTHdoZ2hJNThtL2dQNFhXNUhOay85NGVPZ21ncEtyQm9sbjFlRWg2Ym10R0FoTkFya09aTFdEWXZoTFdTWjArZjdsT3llZm5GMm01dVlQaEhtTkRWMFBKZ3BFOFRHTHB3L0FrUmZuSXhjd3V5WkN6ZEhseWY2aERaQWVRalZKSUdGTWpXenBKM1Y0R0dqQWZ2c3VzLzFoaE9BOE5VRkR0WFhjOTIzZlhiY2NaZHNMb2N4RDV0YW5Daml3cjlqREpIYmVmZEtJUUplc2JGZkxZNGJJYnk0Q0d1SWV3aVZQNzdtWkNoQ3ZMMy9LRU1LbUcvTXB2Yk5oVWRuRmdJVkZVMmRsSmRTdVZwMS91VzhnTFJ2aWJ2bGtZTHAvQmtLSjFQeDVpNVR3cjAxeDc1M3NMdTB1c2x5dnZ5eEJneVBocmd1N3NERW9uTHlZVjJtWk12TVFhQ0gyN25wYmo1ZU9jTW1WVmo0T2pnSmNFZ3JKMVN2NURQY0hrODdNZWFSRlNQZDdBejQ0SC8zZS9QT09Qbjk1bU9UVnZGd09kd2NOZStWUjZ5cmZLRkcyZ2ExV3VTSXYxcTZtdWs3aCtFb2p1dk41VVhzZEQzN3E4UXNxTG1rQ0hUNlduaUJNeUcvM28yTmZQVkkwTmh2ZVk3cFpKTnIrVnU3SGtvNHFKa2VEbmhVTDNhTnNTUTBpRVAxM1E4UHlUQnZ3VHJkbDQvSTVYSUY1cW5qRnVTeHBaNkZFc1BoMFJ5NFJFMVJxbE5GMS9IVTQxbWJkU1AzVnpyKzNhMzg2cmFOUzgzUFpGN3hObGdZMzBYNTlwZ1JVRVVhcEVYdlpaYkZqL1orYXcvQmJlNm9qaCtJNWhZU0VwQnVGM3BRclB4QXEraUFsQldEQm1mVUx6MG1VT0tSNHpPRm1QK3BKNzFicjB4anFyL0hKckVsNWE0Z2FKUE5MQVV0S0loU09rbk9GT2U4M0hHOE5oTmw5cXphVzZQSnVmYzcwanlXR25FSnRQeVBla3o2RG5zTHRtcnl1Tm1lK21ock5WZ2hBa0tvK1ZseHFDcUlTSnVBdDVrRGxmZUQwSGUyOVlIL1RiUGcvL2hNanVTODlNV01IVHFYZC9oZk1Ubkp5ZUhwcWh5TmU0VzBUQW43TjhUQVd1UnpWdUNzaGNjMlpZYXFZMXgxdkFHc1BHZDdYN3R0S3U3WmpLTnQ1a051YXdsT0kzVnFJN1VkTkhnczZqVzl1Z1U5VjhYN2dLYnBTN2NvTWMwaGZneXhGUHZpT0FSbVJrMzBrVUFCclZ0MWdhbXZWUFJzRUZUWWRBaGQva0dnQWl3RndzcjJma0hZNlUzRTg5aDdtT1M0c3c1eVRTSmF2eWRmQ0pnTGJjdlg1dDI0eEd0R2FzNnNLZkFaMjhpZUUxT25ZTXp3OU5CaVZOZjF2aGZ4b0NjWDRnOGgyTkRYMWhqREx4WnNRK29wazd2c2U2ZVNPbDhLalM1Vy9GbzBtNi9OMkcvZHBhQXRpUVBHTGFNcGlYZFVoZENQSmxXREk3aFptM2JYUW8zek1udnd0djlGNkViVTBvaXdudVQ1MFF3aFB5VWpGOXFnUzBUSU9hL2pRYWNZSFNzdWlETisvSFdXRmFaUGVHUTJvR2FpaHZSRnBOWU5WdU50OUxYeWhOWTBmMFFwaExXeDZXSzZWRUh2cytMRkRIYkk5ck9VQXBjM0ZEU3prSmVPVEgrZlA1bGlOM09zcTM1cU4vY0l0NVJ6UTJoVTNaNy9odHp6Q2hPM2RucU1wOVZDRGw4cmN6OTFhbXpJZVpkR3BDdUxKZ0UvVWZqTVp4NnBxSi9hL1dkM1BMRkpwd3RUUWFuV2VoUlJRUFoyZzhvNWVJN1U0VVFQeGFKY1ZOa3NvSDlHL09hcGRxOWwvTHNsQjJqQ0xWODU2bloxbVlNR2xBSmtVK2dac2RxWU1LbjRpdHBIWWlVb25JdUVIdWRRYitVY2xITDgzSXk0d3creVFSazFic0szSFVlK0h0NmJOOXZtQmhrWXRnTmVUYlJobE9kRHRNYmQ5bitodzFoVXdnNFFLbldjWXFmbWhCeGp0YnFSRmVvQUJMVnpwV2E0dkRzd3pEK1krMXVtQVVKN29vZ1VvU0x6WlAxdWhGYThnVkROUXJWYVZ4aHM3TTUzNGpzR2VkaEhkK3gwdzNqY3poWTd6NVVvRjZBVGgxZjl6NmxiSWd1c3VVWCt1ZXp5Mlh6N0VHR0tYM2szRzY2S3Y4MEJ4L0c2bUJxZXI5QVBjbFk0aU1ieFVJUVFjSXZrL0plc1libFJlMWFKTGhUZTF5OGZ5QXZBQ2VvYXowdlk1YjdQaEFuZ0ZGSHFRQ3F4MWNpbHJRM0FWTDAydUF0VG02QXdmZTl3UlM2VXVzTVpycjFBU0NudjBYMUc1b3B5WkYrVWpLTXVqcWNSZkptaFFpb0VVTzdZay9uVlFwNkdyZTduWDhOT0tTVlNJeE8ydmFJcHU3cXFHNlJ3TDJycXVxalU0czJUTGU1ZzByOTJIZENyaDlEeG5sUkNvdmJQc2lsOFRDN2xKQkpBcThYLzBLVkJFck5CSFVNMDZsRFFaaXJ4enVPb0hWMDVINDMvakNOR1lWd2srNGI5MTZqeFM2Zk9pemNGM1NYd1RrdUwzQisxSWE0a2ExM1NEQlcranBGeDl4UlFvYmt4NnM1aGhwSm5TZ2lPVnhKSEJtNHhTb2MyVjNtUHpIL1M0S3hUdzdxVENESE8zdm9COHBvOWFXSUp6VjQ5S0o1VlVwaG9sLzJMNEVZdlNxOU1EczlnZWVPQ2VBZThhbHJWbSsvOUFTT0ZOMFBwRmJBRkFaY3ZUOUl5eGRUSXRPMzNkME4yNVJ4VHJ0dWl5b1JjNWh4dnVHbXhBRCtQaE1sVXRBOXp0V1JDazVGTCs5Z1NQUy9MOWkwUHRHbUxCOXBaTEg1TGtWT3UwcFpObEtYZzhGNHB4dk5wU1lEQUMwNllBd1hvUDMzQ1g1aW5abC9oRWlEdVl3V3Q2Rk5EN2wyK0g2SUZmSHJta3VmZTRPdWZnZFI3a2lud1B0Yjlyd2NMREt6YTBMN2tDbFh5K0lzK0pBK29oNEQ2T2lnR2E4eWRxKy92MWZDb2hOUy91STFHMEhKN0V2aFdhVGw5Y2wzY3hwS1NLUS9NUWJ6cWJUN21SWmhpZlVhU0ZhT1BhOEFLaE1xUlFVMHgzTGZmVy9EV0hFUUJheENkV3RGSTNqYlBxRUtncTFsZGFuM0VKdUFTK2pMUWlVR2ppdTlXTElvMVo0ZlYzTHd3TzdjMlpnRW9YenlXcVNJZDI4ZXlsV1FLWG1pN1J0S1FzSEszbFhqOGppZ015VEExOFVvaG1aMStzd3Q0YU1uRS80S0Jyam5HSTZTdkFoTEw0WGFjT3cvcVBrSmFXS2xXZVdsREszRVlMS0ZaMVN5NkI3czVwcWJuNlpWK3h4ZU84VlgrV1UybjJOQldMeDg0R1FYTUVGbnJrOTlWRE5sb0paZFhLRC90Vm83Zko2SU11SExxNGgraVVTb09rc2ZuMjNZSkZybWZTbFNZbnFOaGI5MmNyYW5nMHY4N2p2R3ZWT2l1UWN3WlQ3TXBtU2N3TDdnVDRwdjZRLzJmMXFwMGRaSzFJNFNrNFVvM1VpVjh5R3VIZ1ZFS1JCRFZ2V3NQbVNUWnFRRFFCejYxaVlGU05TR0FvYnU3Uys3SWVJcWNaTWhMVkg4eldHL25LL3FDVFNlRU4ydU03YUVzbEpuUTQzMWJPVDRGdGc2ZXFUWExRSnNuMmJhcXAzYlc4SFlSMzhHVGJidW9ia0NQcUFWWU5OZ3R6Ymk1dDZLL3laM0dXcHd1cWpxTGNtbWE2NTNBbWJEMlM1dmhTSVZ1TlI2NFZzSXFrWEI2a3JmcVdRYW9GeFVIdkJzdytZUDJmb2RNZVl6Zmc0SEtOMVNaTFBMeEV1VWZNV2I4SmVqcXJxWjNIVkgzZVFLUFFaYTE2SXQ0V3hHVTNhSTc0VDErQmVTQlp2TFQxd2tDN2Z5eCs4RFVRVXArNFBEaEdjYU1lVExHMWNhbGJ3b3Mxd2JybmVSUUh0MEljc1BRL0tZL3IwRkJCYnBHbU9KaFM2cGVWTG44bmZDNDEyc0d4RTFtOVFHTmt6TDdRamZKbkc0NDNyZG9FYm9yRzZaNmc5dzVoNTRBVUhSemJGNmVLSWxvYmFDSzhORFJKMFZxbXVRc0I0eE9mM2pHaUtrZ0w0YUxNeE1lM0dnR1lQQi8wVDZWQmtuUlNDUThBaENzUlFBbG9PR25MNnRyOFR6UEVwemZhZ294aUYxZFB5NGpUVExpeU9WZ2NXUHNTNWIvK0Z4ZC9rVVkxWkxveGwzOERodUJyanhGRVBYZnFXUk5XQ1VvMm5kWlBRalBRYmRaMHZVZ1IzbFBFaWJERHdzb2NWaWU3a1lKcXVWbWl4UzQ2UzhHSFgvWkI0bWlYZGM4TGROdFliY3BDWm1mZkdtOEZjT2FheDd2RXY0cS84cEpIclNXVWgrK1NHV3hBMWF3TVhIYkNVL3dyY1p2Sll4d1QwZXAwMXJZR256R05iT3RHYjRhMjJYMGpmVk1jbmM0TGU1OXpENWFoWXVvdGRUWk5MQTFkMk9yRFpRUy9pWEpsY3J6cTRFT0FMcS9yQ2E0d0JHL2ZtK1FzMGxyVnAvbEVUWEVWenJjZmwxUUUwYWcxZVFQY25mYUd4c2pnb2hSUElqSDErc0c5c1YvQTkwN0tDMGw4NFg2bG9JNjF1WTNPekJxMDRwb2J6ZHV1TWtwUTlDVnEzb1J4aHdQR1h2UUgzV0tEU0NoNVZpajRrRTlpWFlleTRkSzlKZTVMTExaVUFrYUNEOERwWkJiWE8vY2hpZkMrK1JSZ2xPQ1NhMGIxUnJKNzNzbys2UDlMQmxmdzNnaW1pUitqNHhZaC9QeUZLNC91dzU0UndwMVAyaHpKMjB3MDlwNGVEV0wvV0c5UENYbHh6YjNWNGErUE11ZGVUNTEwRGRySWY0VmdXbGlzQTVlSDBhcmswbWpXWmZpU1l6NVpGVnNvNEFRUXEveTNmWFg4UjExQnhKcm8rK1FjSkJuQ05maWZSRW8xeWRlb01sdnI0SVJ5VjZIU045S3AxazJkamlwL1h1QU1zQkJNTXh6bWkxSHFrcGJYdU1TN2swMFpMNWdMb2ljemhZc1RjRCtOZmFFTTFka3FLNHBDa3ltRldCNEJEN2lVeXJRWURNam9mL2wvcFVhZG1FWVdKS1FMaG1pZlE3RnAvQkFlSzJtQWJVRHBYQzFMWUFIb016RWxCenFYZjR5czVna2QvQnNraHRSN3JST1FDQ3UzWFNXaU1qRGM0bWVZVGN6c0w5UEk5WlpLR3dVWTRVVTR6c2E4aVNrWDFHdXQwSVZvOFRGU3BZS2haTlNnQTVmT3MxY2VzV09UNjZRRzJWbGthOXY3R0d3T2hKaWlkcmdXZHM2a1NMZHpwSnlseFQvVkVMVTZRUUhVeEl1K01lN0RBVnVTQ3YweFhKV3ZHeUNCekI2ektzVHJYV0xwVWkvRzJjZGRDQUoraC9Gb08vZlYxT3ErYnI4ZnhUTEhrUHBPcVc4U3BzTmp5eEo5MzFNV3VpYkdhSmhJWVorWjcyQ1Vmd0l0ajJKdmxmWDJMT0tDR1pEYXY0b3VTQ1d0VWl2VE1ZeFR2ZWZhY0N1NzYrcjdPVmdIdi85YXh6eUlWRFNwSnpiVjlySkYzRmhUUU56a3lOQWpzamJSMGdXQ3pSNkNNN1czeUpWUHpiLzM4SmJQcEg3elVDMW9oNWJjRW0zdUptMkt1d251VmI3eGl4R1ZqeW1SY0drQ3htWk5LTElJaTMzZDlIRzllZDIwbFIxWHB2ZEd2cWN5RkRwME51TTBodG9vMHplRkx0ZnRkd0lpLzJmS3k1SnlwbW1FV1c1RUI1Slg1K3lOQXRMSHdkTWhOQ0FDSUlFNGoxSUlRRGJaMXp6VDdUNnUycWJOQ01SalRucnZpaWlmN0tCMDJuSFVLdUJFcWNsWHlIcXhqNDZYbkhIQ3hYaTRHWENaeEQ3MTFuM2JGaUJyMm1uQXVyVHovdFBsSUxvbVpTYWF6bEZTbmRrOWJ1aFRBOVRnZzR5TnF0YVpwdVVVWnBTV09iRE1HejZDRjNqY3dQdkY0TEUvN2I3MXpWU255bElLUVpkQXJEc3ZjenAxSFZrSW1IMGk2aTdJRk5PdENMTE5lTWdrbnFMTitJR2hmQnpzRmxEVHljTE5TSG83NEI0YlFwTXNUamxVRUI5UW5WRnJXYkJYYzNnZW9BOGRrK2RRajFISDRraFM2c28rd3FuSkUxT0I1WmhpWGoyRkphZXJTY3BmNG5oWERIcW8zSDVzVkQ0ZHp2OFcwV203TTRGeDNKaXNQU01URGs3eG1iMjlhQVBpdU9mc2xHUTBPdzlJZTlKTFcyWTR2clhoS3JuY1JBakZRNXhjd2pib3cweGpJVXJYVkFIRXFST1lEVGZGekZjL01DTktWK3hidnpQU0dtYmdQdEdSVmdtaVpjdlNiZXVRUUduWkV5RGltZWZLRWViMjhZZG5NMldKSVBGRytJQWpkYmJydjM3SUdPMjd4OUdvMFJVVlQyZTh0SXpSR1QxejBvdmlLWnJhNHhaRDRJSDArUWpXMVBkbCtrZkpUYVVhaUgxSmtnMFlQRVpsY0V1OStxOWx2R2hkK2VMK0xRam1RRVR4T0tpdGd4M2V2WWVtMGU2bk54ZkQrM3VvTEdRS1hEb2JCMUsrMlpGODluMVg3SWhjUkJsbXRCRDRObEFOZFVDOVVHNkM0SVZWa2dpbmVsMzMyYnFQSlV4cjJiRjljNTF0a2E2S2dCRVpPNWkxSkJ0Z2dTczRpZ1JJOE01WnpjbWRwbWt6ZnN4N0U5UWlWRnpud1F2S1M1UzM3QWpzK1FOc0hoaVgzdG1jdXhWYndESE9KRDdoT1NGdTA5Qk5HMUFTaEhrK2xacmhqRWJhS1JQNUZiOW5WZkE4UjFYbjJ3SzRaSUt5UGRaRnRvbmxxVTd3dlVIcWtlY2ZNK01TYlphRnVOT0FWRzJKK2xOanZuaGFLUG1jdHk3M09yWGFxTFR1WXM0a3VHeHJIeTBueDFtVVhEemNSd0N2T3JjLzlUdDJwOGloYzk1UTFyQzFmOE44cWNBVmo3RGVSam8xUXEzaHJ4Ulk2RFNXWm9ZYTZ4OWsyaTUyaU0wMVFZcFhTdUlYSEtYYWtOR3FhVkFYUmpvNjVuRC9DbWsyaExhUHZKSFc5dXZGM1RRbm92MzloYlIzbXVJVzF0bVcxU2hkS3l0blQzS0JaM3pySTJpWjZwNUlFbzhBeStpNFhQYjIwWE9HdU1QMnBKbWlhblVoLzMvSGFoQm1IdmZiVzlUb3h2cjhKNlVCUVhDZUoxaXVLVVppejZobzNmVDJ6VG5yNmE3S3BHNmFtcGJlQlJFSzBJKzl0OHR1SmwzV1VXWkJYbWdhc1U3ak5yS2ZuTXcrK1BEcmxDbXFGYWU0Rm1FS1ZtVCs2TjVMQUx0Rk1zMGIxWVFKMGMweUI0UDhIQkJLNlBGNFRTL1ZrSjgwZWF6NzgraDB6NFZQeHZGMksxeEdNcDR3WXRUdDY3MGtVdFVMcjMzam9LanB4V1V3cTN6K0ZjT3ZWWVNTdE01RUQvb3JuYTBSMDdJL21UbldsaDZQMHR0RkN2c2FWMjBKVWVNRDMzeHl0Y3QrdGJWaWpTWWZ5TUFBU29ZTzYzTTNZY1NPRHgxMmtxOWtpVjY3a053Z1l2dW9GZzgydUF0bUNNVFhCNDIxcDlOaDZGYjQvR0luaVh0R0hLc2ZyRlFDajlpdkpveEZERkdPejBRNk1hZjhBNTRqVlpwME1zSktNM0l4VUxCVHJIcGdtT2JLWnc1R3Q2U1Y0MlI5RUdTZFMrTzRlYTY5OFJFMFppYXN3R1JBV1I0d1IveldYc3c3UTFXUFgvRmg2MEVERHFtYlQ4VmVWQkhFQUdzNzZSUlQyNE42b09RcG0zaExkc29iNU4xTTBJa0lYRDZGbGhhREE5aUMrN1dORE9JQnBrL0tSRHBKeDR0Tjc4VVFrTW9lc1owczBuaTdKR1I4eWozY2poWDZreWllMEN4VWtLSUE5U2FLd3cxWUV6ODNvUG1zRk5ka29DQlRkK0VEc05jem02UmI5bi9FRXF1MTBYWGY3Z1NSNzZaemVEUlNWWHdzUERmOUtveDZVVjhFWEpMc3lZRTdCSWw0R3pZcmkzVnN4clU4UER2U3UyWHVWVGVUMXNCSHNpSmJzTjZwSEIyM0hvNFRLSnJOMkxRV21nMGFLeDdZdzc0VjZodHVEbTJ5WHJDWllUL24zcmRYU2xQalJrdW42b2hYM0l3Y3NIRHpIaStQdHdKaXZabzl6R0NaUktDZFdiMHRtaWt2WDZmZFhLMDB6WHlUazRVaGRlRkJ1YUxkU3pwbzUrMGN6WEZrVjZtV3l5UWhXOWdMbTVjUWs1RjdzZldHSVc3N3FvZ3VrMzFUZ3o2c29JbCtCSjVEMnhmNmwrYmtIWERUL3lLOGZPYmhMT1dPSHpyTHE2dXlCTXByMGZsYW5sRkxuRTZ5R3lyUGR5UVZLcjdRcmJZRE42US9YMFhZMFgzUEZQeHZrWHRxSnFIS05LT01nejZReUNoMHlDdk9NdHlpVDRXZUhhSG4vaHozNjJrM1I3dFlIaFZaeTlpTVoyMTVaOXdvTjQxNXYyOEJwVjZORVJteEpRVyswYmFRZXVXbmVybm5wbkJET3JETUpMYVVLY3dFbytPNFR0ckI0MmJxelNHdWNaQnU2OVhka1JPRFFZVnQrOERWNDJpMVA5WjZpMVR4R25xcWFmRlFzaWhKeHNRN0Frd3d1dDkwREtneDgrUDNoV0Y4NlYzYncxRFlaSk00M2xxNkZIbE5ldWNCbkFhR25jbHV3Smc0UVNpYmdNeFRlTGl4SElsNS9VaEFXdzZ0TEZPSWpKZjUzSTRrdXl4NmsrK2p5bDdmc3RTWm1MZWxySVJIZ243RmtpRjZ3YjdERnBNQVBlN2lxTzRzcis3elRsb1R0SzY0K0xqN21jMHpsZjlRZGJTZ0M0V1E2aDhtVnNmVXoyb3AzdmlQYmphZ0pSbVJDNCt6UGRZeEhwSUloWGdtdnpTR3RUU0JXbEZxdWZucTVXbklIdVpXTUY2aDlIWjZVbXRmMnhHT0xiVCtKOFBWSXhTbHNDVm9JVUhnZ1EvOExsdkgxY0d4SkZHc1FmaXV1Vit4VzM0U2xRcFloWXdmckhWdWdqMHdtQjFnSlVPVmdPb1FYNEQxNlBRWlpLc01CekNoR2JYWFlDTW9GeVNIU1loRzA2UzJaaU5IcTBSVFlBN1ZSU0U3aWtyYjQwdUFlNjRNNmlsL2Y0aWdXQXVnaHV6aGxuY2JPbHNvK29ER1Vub3lSOCtlNEtPZFlqY1c0VkV2VXR0S0h2TDhtWmdzWTFqZGdpNjZncmFzamE1aE51TEpDb2ZScHlXUDB5cTV2enlOY1lVTWNCeW85OVc2aWlHa3MxdUJWTWRlK0NHVVVabUNPVm4xdzhhMnBRcnBjWkJWNTEzNFVhVDhyazU0K040M2pQeEU0blpOT3dwRlc3NVZkUXZCcnFFZUd3amMxNTdsNmtXQkNQWTB1L3M4c1pDZWJJT0UzOUQzemFQUlV5SlFweXJNMGNZcUM1bC9EQWdQV3RiYmovbElxQVJoMXhiZXZWWlhjZ2JTNGsrVk1rUTJQN01RWUVpek9XTmFDYWpBanB6M1ZGUEJTUllBVzc1UVRlOE4vRzR3YmpwQjltcG5HZmpiTlNGdGxvbUxmQWp4RzQ4S1IzdlVpOVMvbXdiR3dyR1cxVE8zNkJHRk5rMW5DbHhMTk54b0V3bTBnR2tNeWdmeWkrU3hCV1NFZkVjb3ZKcm5yaWJ6d3QwcjNibERUWXVFdU9xaE5yZmZCdHhyMVhMSUVxcW1CaDVITy9WMlRWYlljUUU0TVpCNkRPUFlibm5ibWl5eEdMcFV3TGRYRE5JNHVUYVpubllUNXR4VHNDc1EyVVo0clYzNEY4RE9UUk9aRVFMeHM0MHlGMXczSUsyNlpoT29JNGtnVUJSc3pWQ2c2NTZRTXZ0T0lVdysxeU1wYWM0VmpCTFBxb0FnQXNiOUdrR1oyRDZhRUkxRWRBZ010TU05dkVDeXV6RHJnTlNFeUdBcS9wbEZXWFIxbGdGVkdFcGZRZkU5WWNIK0pWRTFwRnNjVFNBSE1LL0ZOSDFHZnhPbkJDZ21MNE5TSERaa3JFbWc0eG41MXB3TzZWSkUzWkNCb293MXJBZHliN0ZjdS9Dd3U1dkF6SnBEZXBWcUlacHloTjl0eXJ3cHNzL05UYjhaNkp2V0NJNmQ1UnZuMlBJV1dOMkFmb3BWTXJDeWRGbjZUUHdaZ2ZWUytQQzNRY2dYSTVWeUlRUEpEaE5QQVhsWWR5YzFIY28vTU1rT0R5ZTVacUdlbll3c091RDZoMVF1NWtvOU9LU1RKK3FOOEgvaE56S0NCVUc3RFFwMVdCWng1bkppTmU4bWdKZDNiblhYak5qcDEvenBKNTZVbFlvRXdSTzdENjU1S283OXdDYlVHWlBacWdZcU8xK2lkSEN3ZVgrNjVtVkYrNE5uRzc0NjVIQXcwcWFjZjU0dUJHa2tZcEpHRTI3WHhMVE8vcHBNVFU0ZnFvUUdUTVZLcFkxbGtucmhmREFTdUxNS2ppTjA1RTE0bVhvOCtlZHBVeDlPUUh5Ni9ycUxmK0Y5WGlRWEFHMjJ2cnBsM0k0b1M0UmQwckd2WGxuWmJTODltellMeUtITXZweGRpMFFpUm5aaHhOV0lPbUxDVk9sbU5jbGY3eEhnbmd5cVV6eXI0dCt6cmJEQ2NmS3Q5bWY5VVdVOE5pVHN4MTFtbG5OS0ZscWlIa3BIZEJnR3BTQ0RsZFdHVHNuTzBvWVNvZ1NteTIvOXNzVlorR0lUbDF1aHR3MGFvV1RHMHo4SkVSZ2JRRHU0WnNWNkRDTXBJb1k1YXJPUEMvdlRrVjlDUUxBUEhlVWNvYUdNcHF1dnU5NkRUZ2ZlYmFiN3RNbFN1WjFucXdCRy8xcGp4NkthSnVWUDAvbUVsRmt6eDdxQ3lTaEpGRVZHMUZvWm9nd0E4cmgvM1hkd0w1VXplS3VqYmRmNDJ4MFI2bzRSOUlZU3pnUjh1bk9xWWhiK1UzK2MyUG5xUG8rSVBVNDNwelZNOEgzOHZUaWxDVVpycUtBZ3NVMTR2ekp2ZWlKalBWREI1MHFub2ZnVjVCNUtuQXV5MjRhVW1UV0xqaVlibFVobHdvT0RvR1NTOUljTlcyYno1d21wQUF1K3ZWa0NSMFUxaHhuYjVRRHNSZTM5cVpaZVp0VjZRdEE0NVVUTk1nZE5hckRQajlLYjNkdEd4Um1kc1VzZlc4ZFJwdkRld1hnRkZnU052bUQ0RkU4S0lpMVFndnhSYUJMNVlRcHNtSXhTNXNadE91VE4yYkxsdWZ4ODFqQ0Q0SlhjNGpJVEozaDZYaEludnhQUy92bkM0NGlrdjVMVzVKKzd3T3l5bHVCZGwxQ013N1VVMG9sWXhnbUkzVEpwWUpidVA4dmxWdktmS2dZaGtIRVVxaDE4Zlp1N0thRHFxWTBuLzJCeTJjUTBtQ0cwV3BKaVFwYmozVkNHTlhZdUo0QVBMbHVJYXFmbTJLOGg3YkNDWE1FamhWV25yQmNaWXFVRUtYUXBYYmlsd0JZK01Ha1FnQVQyeTJWUjNtSVVtT2loVUxwckY0T2o0NFIxUEhLekxTcGdqL0w2aFV4bW1kYlJ6M3lyNDY0aXZhRjRBOTJKczZEWEhCZk9GR1JPbzZPcHlsVi82ajd5VVRmVWlBbjBCdXhHbTR2bnQycC9zYk5iTjBZQXl5YjMxTzFISFcyOWEzNWhVVWpCWSs0bEpwWnIwVk5PV0pLYkx2QnMySzJiSVZKVFMyL3JKdk83akhCbEQ0TFNmSWo5R25yaGxFbURqcFRGT2Zvdzk2WWV3aDYwc2FmOHdpeTBvY0hMYy92QWNSUzM4dWJqK08yN09tNzVBMmRRKy9uaU9JTkhjQmhaUG1ZSXAxdk0rQWx1U1RVc3RFWUNXcHRIZWdpTk1lQ0dJR1hERmdlZVViMXovcGFHYU8zWkdzMnlzL3RCL3FPNmxNM25zenNDOGFjRzB2N2NLbStGZDNiYkNhUVB4eVcxZFJEZHNKY0UxSldmbkM5OUxaRUNGYnhVbTg5ZmJaMCsvU09ObXhjSXRta2kyL0VJMjd1ZXU5S2l3cE5kR3pQZjZic2pzak9Fc2FjeE9BVUZCdjd5ZHMyN1JVYmJpeGdkQWI4L2FQZVhyK1lKWlEyaFNDUk0yVGkzcmw2a2VRVjFtUlM5WlhyakVpTHB4aWc2bDdvN0hlTlU2cEVDU0FKdDcyOVh6Zmx4ZlBBZ2Rlb0NoSzdvbW15eUdNUTRqZVVZRndzYmcvQkhaY2FLUEZGV29DZkp6eTRVZXV6TCszb2lJcUVXaFVkZ3RGM1JTS1pMVDI2K2RKVDBQMnp2RXhDbzQ3MU91bUhqWXdnV3lpUll3R3NmQ0ZxbE5WU1RIaHQ1NFEzNjUwYWFNb0l0d1NHWlpTOFJwRUVybmFiOXN1bnZlRFJreWdST2toMWIxdWt4ekdkY1Q4OFdVUTZ2NnRycmc2WXBmcFJZSHVNK2tlV3QzNnRMQXJadGdBZkpSNzFxVVpIWjVISTFENHBURktUdlhlNmlWb1Q4SFI0YVhhWE92bEN1YXAxQ3N1NkZrNXI2bm5iYkVaUXlXYUZoeEVISFF0QnJXUEhTcjZVQWE1SzUzM1FLa1hDMlNmbnNnZUUzM2V6Vmp2T28wM2daYkRMR2ZkeGxldUh2RlRwV0V3dWpDMHI1d1dpYlgzZUZWOHIxS0NMRG45eU5QRmU0ZWkvQVNNUjJ4TG5JbUkxSzhRRUcxR2RNc2RyZm9UU3plZFJlOGIxTWpSN1Vzc1BSQXFnTG5OS3VpcW5FUG5kVm9FTGwwaGVqVmtLUE9xYUlseEFleFZrOWMvU0huQTFtQTVXbnJxUFBvTFAzdng3QUprYlhyS3N1UmZCeW1tZ0NFZ0VsejgxRU9mRTViMnU3ek9Hdk5sakN6YytBWFJ4Q1Yzc0dTU0dpb0VTeEM1UExaazN0T2F5Z0ZJaUh4MkU5TnpVbVdtSlR6VWIwQ20wUkk1dW9hVzlOUWJhNTkwdWdoNTFQenBydmNoMGRXQ1lJcTBBT3Q0WHQ0ZkRJektmRkdzQUFxeEtyMUY4OEhuWWc4M0s2bEZ0WjBZaEdaR3lXNnRYdUluWmlOcTYycWV0SW9mdWU4Z0JSdXZQdnRDYkFrQWFpbkJ2VTZkaUtiTm9DeHNzTUcwWEJtNXgvdGpJd1RGWkNNayttcmZUc3NDbXk5emV3MHlicDhTNWFkalh4d3NDNzdOU1VHUHVoVjJ5VXJFRVlyZUYyMHJTVjFwVUNnVmE0Wkp0bE5MQm1BeEJvWVNBejh4UzJXaDRZZDUxVUFOWGQ0SEJOdjJ3Nk1ZTkFnMzlWSUJBRTNudzN3dTgrTStDOXR0YVBEUFgwT2lBSFg4cEhwbEpIY0g1ZzFFa3FTbUx4NStCM3BQanVpY1JBcGRiZHVZV0tVbnNjV1NWRVNNeHNpVmNVRlYvQTRiRXFkVituNnEzWUQ5anFLTkxFc0I2bHI4VCtWUU94UW9pbEUzRVN4ZWpVWkFQa296QlpxWFkyRWN3aEVVc2dTYjVpSzVQUEF3YjlPOC9HRm5xbGN2MnU4emFDN1J6SmhqeTJQcG94TGtFVURMNU1XSUtBaDRYeVpKb1gwT2kzd3RxQms0NVRiZWJvT2NlT1VHM3k4R1JoNGQ5TDZoSGdjWTZRWjV6ZWxDVlRmNnlOUEplYTBOT0xvY29yeENUcEtUNEJlcDVGUTlTS0Q1RG12NDZsNk5qelk2OW1XZTNBWm1NUm5iam9pajQwZkFXZkViNUFHakdqYnpzRm9iNFNWNm1rMGpOQ2FyWGd4M0dSQnY5cTZHR3V4emVrZ2twZUtIclJ2Q3hiNkNiRi9SRE4yM0luTFM3ZGRZQk1ZUHRjRjhkSGk2bHRkTWtnVHZUa1pNbStCUS9zVzVad0hhZUJLOXFjWGhjdXMwTk12RlVTZWhtcm1aK3JXSGkrTEFyVEtRM3JBTU44N2M3RzUvWmZVVmYwTFg1UnlmT1JmL0dFVnRLalVZbWZPRDY0U3ZSWTE1dDYyc1VCVi91UE90WUNHR0cxMk9ycjVwVkZTS3drak9SV3RXMndCRnVtL01HRlhwYkdmYjIzYUVIM1ZtRVNJL013ZDNheVR1WlhTc1lYc3JtalU3K2tPc29aL1AyT2hmZ0pHaDNQVVBnSXpYOVluYnVnSFNaeTYxMXh2MEVYclVUVG5nZkNnUkVpaWpHSHVNL291WHVHQ3B5RFdmUmdqWUJwM25URUludGdXU0M2RWsrUVJySlNBclJzWmJDNXh1QkZXWitlSjQxc1c2azN2ZWxCQTQ5U3ZidkI2VkprRWZldzJITTBRVGt0N3JsQzhpcWdZSnU0eGFVNWJxMWRCL0xXRDJmRVVnbUFSZS9JNzdoUXFrSU1qOU1YNzRiL1MzSDlwSS85aWVsWnRSTGlrOVZEbXdwYnFyZklJQ1pFNnl3NTZaOGN5Nk9GcXdHTGMrTkhlOXZudE41OWVpai9SclZEU29QSy9zaWpESjRpSENpbkJ3clNDU3RyNjlxblgvdmNRaVFkNU04S25wQWE3dkJGVDgwcHAwcW1DaEZDN01mWEh1RFdjSHpwdzByMW0vQmlwSnBOeHlRd20va2h4aWh4TFJsY281ay9yYjJ4NkZ0YlJQclJoYS9TYUk4ZUtrNFdRZEszMDUrNlpOQ2docFh3bjh4YzM2dEN2MWMxQzIzaU9FSzVHRGpUVUE5bHNlRTBTQk04QXpmUW1EeGhqNSs1aEpNWUtiQUEyRmNUQ3pQTEsxWWFZRUJ0aGU5Vmd1UFZtOTlyajVqN0VXejJncGhZMmRBWWFyKy9HYk5uRkRxTUh3eGxpRW4wTW9UUEZSTjVnUjdkQjNZOEZKTThveGhqcmV6dDFuc2lCSFpYV0NRK3RrOEFnSUNBZ0lDQWdPSjdSQURCOU5FaGFPeGVTQm5aR2t1cnJuMm1ROVR6UVhUaWNqWWlXQno0R3NHMEUxWmMzMi9XRjMyL1JmNmZPays3dUY3N1B1RFAyYThiOXYxY1g3UHRVZCszVWI5dWlmN05iblA4UHJLNzdmcGw5UG5WL3dmWnEzMmZhWi90MkgzN2ZxM3YyZmE4Nzl1dUg3YnEvWnFHNGYyL1VFL3M5L3pYenBQMi9ZTGZzK3dQL1QxTDM2dnBsZjAvcnFkK3JTTDlWdGZwNk5EYlhjZVh0SENUWlI3VWZnOXBMU3dXc3E4WVBLY2xnd0JRdHhSMHYvNUROY0RYalA5S1lLV2MrdVNKYWNNOTlCUDhObllkVENnSDU0U01DSTVJM0ZjSUtCemdVV3pWZXZMcVBHTzFEMWFseXo0VStGcElaVnFDLzIrQzY5QlBERGhCdFpvbVNrZnBXZXdyVFFZSmYwNlEwNStsQ0UzWWFUMTlLQnBadzB5cHN2SHBOL21HNW1PblRyTzBaM2J0SDRaQzB1R05rZVRLQTA1RHl0WTJSb1ZxVm9xYmZIcEU5b2RHYUJsWEw1dmNMeWlIUlpNNFNGZVRhN3hZcm1BVllhZkVVd2Vtc2JxbWx0bnN0Q0JyQ0gwZ3BvWGRjN3BHcjNtNFVkc2ZlNzJYeis2MS91dmZFVGxIVEVvR2JzY2J4TktPdmsvTUowV1ArSFZBNDdGM0JVL0tVWHo4eWJJSDh2MHYvM1c3a1NTaWRBdC94Wm1IU0ZXS1JRNWpwT2R4SmVIVDZzVVI4R1k0c3dZUzVENDEvTjZhaXM0aG9ITkxsN2VETHJFenMweHgwdGpaL3dmTHZkazBZR21BSXpQOFdMNlpFOXQvanRKeWxVVGROVksvbUFIdE5RN09xTEsxNXI1NXhPV1pFYWwrOXFJTzZaSDBRSHNQWXdBd2JwaHozUzBoTDU4L2E0TnZ6Wmk2OFBxdmNlNFFETWFtdmQwc2R2WFNXMVpZOUpMN3ZzaGkzWW1SWEdKais2dnNGMGRicWcrTXFsTEhPRmU4TXNzc0sxMkpqWkVIaytaalBUekFnWGY0UUs0cUY3T044T2xIRGw3eXpFVzdMc1BhRWVnYmZkM0NadEJnbHVQN053YXROQWs1eFM5NnJObFlRQzlYd1dkRmQ5K1hBdk9RUFVzQnBPanFTanpINDR6RjFUWkovdTdIL0Z3OHdiTWo1ZWdoN2dXckFoTWJPN0l2L1BDRjJYN3pnWjJxOVVsZldMMW81cTdJdVVHR2VrbHd6WTM5bDdqUXhmRUlSem9KcnVkaDhlVVpaaDR4TURZdmt5RnRlVUZjZkhibkNNZExSRFdOQVB4aXBqS0tqZ21TZWFmL1pOMFFDaXFoVFBKRSt4UVB1aDIwcmJzR2xJYWUrak9RY1p2dXlvc2wxajduRXNJVXhaQnRHZU9QM0pqb3k0NVFEWnFhRG8vK2VHeWt5MU1lZE1tUnNpZEkydE5xWHAwZVFpbU5aM1hpV2ZUSk9XTW9WRmJScWhSbTJwVUdKcUQ4UjJObElPTml6MExsdVBhWW5lTWc3S1VUVkFFVXdDV1psck5hcC9ZSlRlUUFsT2dtalpOTlFLSC9TckE0ODY1M1d1SURGbEpvVkZtZXRRSDFOMTBmM2pKSkR5VzN6d2hoNHUrSHNnd2thdDdRWm5EVGxMTFM1eHZYR0ZpcTlidys3c1ZDbkl0OGk3UThrUy9WWm1jRmRIem9WZFo0Qmw2S1pObUV5UGNNMlF0d2RLUDNESmpBUGhGZTNJODBjZTBtM2Q2aVBXbUQ0VEJjUDI1bk9VcEJOTHRuNmcyTndxM2tyd0RIRnc4KzU5SEE5Yk93cStrQm9UbjZORCtoYjE3NThidDBUbVZyMlhSYmppUFJTaHZnbG9RT3owMk1haSswYUl3VXZJOThvWHVIRGRpVlZCQ1g4OXp0ZjRabEduWUJMVUU4SlZHUmV3SXdNbVpjTGZTQlZPV1BzV0ZiQzZ6SGFBNUFHSXdrckpWclpGODJjT0NDYTluUjhvc0hUUU16NjdRcVVJMVZNVnJZZ1VoTHZUOWErWVVQR1ZEaWQrMjVpaU9SUS9CdUVpY2VGWUhzSTVqSEIxbzhnWDdGd0VZaXluRXZ2ZFZUcVVlbUIyQ3FjS1lIU3FPbUwvRzhjVU1mdHBmNTNJWk5tUUZuSld6aWdiK3doaThjTXFoSVVxWHZ1SHY0dEJkb1h4ZW9MbWpsUHVWMmdhdHBkdENTNkFIVSs4S1VlT2dKbXdaRlhoT1RsTzEyNzQ0WGpwZ0h0TnV4ZlNjNWtuUHJBb3NuVGY3ZFhXcTdTVDFtRXVkRXJ1TFdhQkN0aFZNV1g0c3RUOFRaVlNHMSswZ1lUaHp4VjY0WTBIMG9ibng0TkVtY3pxenJHSFpISUw0V0lIL1c4emlBdmxkaE91SHlHUHBVcG1VUFZndG9ZcDZUR0J6d0ZOQ2ZNLzY4L2tuR2xQZWNtcmpMcXpZNTlJNE5lVytpdXdPR1RQZ3l6cTBya0hHcktmeFFTQ1M5eUl1eld5dUhCM0E2dW85U3VvZXNqWTcwZlJabXNZZFBPVHk4RWJPUVJCRXRlTEJXZktJeHBGekFjaGs3QzJSb2NGbVRNTDVidHUxVmhoN0lsS0pka2FmTE00R2IzY1E2QnpCVkpaRW12dyt5bm1pZGkxV2RNaTdVS3RkZHFZTVJGalA2TGhrZUJPaDZtUWg1aS82dWFWYjBqWmIzRmRJSU53MEJIKzZhdHdrVDIvT1NybXRSREV3UVFmS1VoMjVzaEpEUFdCSVdxV0JiKzR0MG5Mb2FYRUN3VUFhL2xSelhyWUE4N1BQTWNYdy9paUVLUkszWW9DMUJvYkRENUZVTTQybUlTVGJCUnZqK1dTQ3ZaaldYNVpvTGRIMDB1TFQ1QkY0bFVEakRYZ3dMR2xTSnM2NFR0OXBQWnJpRS9YSDNuUUR5T2pWNjEyNmFDV1ZEQUR1dDh6UjVYR0VMMXVhRnNxUENnajk4c05hbFpkSWxFbGIrQnpwbit2YlNXWS9KdnFxbWIxZTljaWhOZFpmMDlqS3h3ZmVmSm1saEhJa25oYWd0ZTAyMjlrY0ltcXBoMFZMTFE1akNGR1JiTTNvT3RYR1ZSV21aQzBQamtJY1NESTJ4NHdJOTZ4SktwUHU3NHpDcEZmWFJsNzZTNjBoU0ZXWGF1NkJPNkxWTTcyODhEa0JWVFE0dzB0UWo0Y0ptcUYybnN0ZGpmeURJaGE4eENvOGdHWDBYTWVMT1d6Y01OUDQ0TmJKbkFIT3FYMWpOeGNhRG1UMnFiZ0Rrck1JQTFpNlJmN2JKMGFEVTgxempZeWdEOGVaQnlCaGhHcHBPS01oc3pHaURrRTdabFdyWHpFTGV2UTZTMHlCcTVaWEQ1SFNyWGI2OWQ5TkEvM3kvQlUvVWRsUWxxTzBoM256Qzc5d1UzNFJSWnR6NUhtTGhnM2ZiREdpMnVJQnlDMmdyTUtWeHFNaHRsS3NqdWhuM2g4WHQ2dlliei9lb1B5eFVNTzM4ancwOEVDS3JWVkN5S21lZklFdVQxbzlFMngvdjFBd2Vvb2lVKzZ4dHROeHozbHFaUHBrM1BmRnlEMVR3RCs1aFZycE9wWUFrK3NUbkU3bG9uMjJoNmo1bTFoK0tNNUsxNFhpMm1WaCtlMGdvWFpEeTBoV0ZPZnh2OTIrS09oZEF1b210L3VBNkJpR1daVFAwY2RjUE5WREhyQWFkVmNteTdlYlo1NWZLOUhCWFRZOWlDWWVodVI0SUhmSko5bTU3TTIzdkJxRVI0dGNxZUhCL1UvUUFUVUs1T3JmY29oM01LZ2R4aTlTVkJWMjd4b295RzFJMFQzN3pzUVlFVm9PRTZ5KzBlcEQ1QUNNNHhTblNDZlVRc3JWRiswaGN2Z05EYmRaSld3OVVXVkFrS01XOGtiT2xEYjU2Snkxam14eGF3OSs2Q2wzaWRHNjJtUnJsUmZ6NlozRlBYSkNyRlhqYy9iMHdvMXBQMitjbDVTWGpnWWxWOFRuUTVFTVNKelgzSFdhRnF0bmpBM3h0cU5CZnRtMXlSbStQWjVFbHFVZ1g2Nys0djI4ZXIzMGRrWjBsTkVxWHBwL2FjM0ZkQUNDZUVSMG9LdmhRSXdyQU11UTIyZ296MS9ORHFPQi9semxUSlRHSWN0TUNlR3hxNkloMTdMemZJZ25CNjRuWVBwbVltTUVpamFwSlFvcy8zK0VBL1J4NUREeG55clB0UXY2d2FRWkw3Q0hvQkFOanN2TkNraTRHd0tEZkNHclZNcDJBdmFqUFg1Qmp5M0dITHIyLzJCMkRCODJjMXczTXgrUU04akdlYi90WnJncjM1TklXZnBXbG9zSkFLOHN6VjVHb3JuRjdPWXJFTGVPeGJuL3FwejgraWVoOFhPNFRmcWMzdDVGczhaZ05oMjdUUCtLNVRQOGNFaksxVHBwS2E2Q091NCtCQ1NYVlJXd0dTVDliV2RlRG1CdGNyVjdlSFVwdnhWUXp1NzdUemVUajRMd2toOElJRDFoamE1Z0drUmhnb2NUTVE2MllsYW9KS0srN25xYVpubWIza1pVTjBMb1gvUXFmRTkyaWZBWUtRbzkvd1AzSHh3VFN6dWNxMTIzSSthSFpYZ280UlljdlVvcW5xOUVsMzRNdTh2M3FmejdlbXVxVkNqL1Z2dy9QSWY5QlFLNEtRaVd6WlUwbENKOTNLUXZvVEpHak43M0hFaXNkVTJvOXVJTFl4QlEva3pCdEM1TVJudUFrNkZwR1F0WllyTUtVWXJKZ3ljc0RrTVg3SlNhVFdvbmFaRE1CenRFWElueC9hSXZNaGVVZU5WOE5wdUl5ZkVjYmxvemZSTjRST0pCcmFLbk05WEljeWZVN1BGeXJiTTdwNXFnRHBtNUtyRG1XQS83NjNYK1Q5UDR4Qk9jQi9hTW1OdmdpM3ZxOFJwU0Z4c1hybXpUUEZQNjdId3dEd1FWRWF0Z3dwNVRRb0s4WkhuUEM3OVhhWW9nc0QzVzlraW1aOVdVcFBkTzhDV01zNDNpZ1ZZNk1rcTRGcmt3NXBsWmh0NFF0cjBoNlNoMll6eUVIemZqMFVZZEd0dkNxdndDOThsUVNnSkF1dGt4a0RVZ05MaTJ0dGhiaGxoUk1TazNyM2NCekZnNGFKV1Ztbm5lSmhVV1V0UmF0cWJCL0dQbnZiR2drS1JzNHpZL09Pd3ZNWitYME5nc05tckpPV2xlZ29neUYzM2NIUWNaZnpNb2VldHFWaTJBZ1VnUFlUeTZkMGFWYXE5a013V3NPd3hxeks4QWVPVjJWNGhOWWF2OGNIS3VPVW4rbUlrakpXekNjM1A5d3AzekFCbmF3WjhKK1Rob1FCV2dOU2tsK25UQ2pRVHdTcVlCL0svRzJLS0xhY05ZSlFVTXZhWkREUjJyM05oT1l6cktZYVkybEdVZi9PVjJidFdQbERMYnhORnlUYmE5Z0VCc1M0d3ZNdzh3cWxZK0ViM082VUFNbG0zR1ZjdUhjN3drMjgvd0lvdW1hR0JpWjcvR3hSOUJ4Z2ZjQnhjbXlKR3kyaXRralJMSnh6NHdOeURIczVQMUFpSWRFSHJ6ZWZ5Y1Nqc1QzVUxDS1N6M3M5d2RzT05hNlVvRmZtNHpYajZrdCtORmFoNU1ocXM0VGwxY09vNFhMdi93UFkxbUIva21XYThjODI4UVgrbFpEODNHcis0S2lZVWFiaE5KaE9UeHRnNXRwVllxdHQzQkZxeEF6Wkd1Qjh1REdzNTkya25ydjN0UG81RzNkTjJuS1hETTM5TllQcXMrVHQrUGNqM3N1aGU4anl3b3dVUDh0M0pLYUZpYjZnNUxaclg5UGx1ZGgzZUlYVlAzQzlIeVNGRjhTZmJOL25KSk1Hd0dCNnlFaUVkalJUd2E5MC9oUCs1TGtuRGYzRW1Tejh1ZUh0bXc3L2VpZW45VitCTllHZFNjZWlsVU1rVzRRaVZrNUc1TGhZd2xEVHpKZkdtWk16cFQvRXRUWXZiUU1KSTU5ZTMzSU5xcDhISUdZMkFrdWZFQUJHK2k4V3BvSVJmOGFYMG9FVTMrUWpYQjZCRVZEMHcvcWNySzkwMHQxQmxEbldGWFprU0pERXFDYjIrSFZRZGxoZkh5VUg4RTBXNVBGdnUxM1NQenFLSHY2R2d2S1ZLS1F6b0ZJVENLZUdFa1RzbkFIa3FKaEx2QUk5UHFVdXdkeS96eGw0L25TVTloY2YxM2k4OHkvK05rZXh0cVc5WmR0ZHBNbmZVQy9rNnhscUt5em81MC8rQzYyNXNqUXhyL2RBSS93YzFURzF6SkNHd25WMjBkNU9FVytyRkJiQlRndFROZWdGOXlCTUZwOFdGaFVCd0NLZ2Q3NHJDWUVuUndUTkYrY1dvWW5qdUpoQ0Q5U1d6Rlc5UngzRlhtU0dWUlNUQkdBSEZKZTAyd1JMQzRNL1hRUWlzWmFodjFxWWt2NHlUK3o3Q0hWSzJwNU96SnhsbnRQNnJmTkFqcDZtNHNGWkR3dzBXNTdGSUxLL3hucVIra0t1S1lPSXNWNktuSG5mNjdiUVFrSFR1THpCYzFOOUlIRXd0UXFBY1hGN3BOVGIzUTNXMk53YkZhVEttSmNmV1BvZlEvbHZ0VXYzbjM5cVdWbEtFZExyd1luc1pUdW4xT3pEUzFTciswRXFDTTZrTklndDlla0dOMWtEVEhQL1pBRjZYVmJqd2FRdzFTcnF5U0ExK1ppRExnYVRva0JuM0ovbnVKSXJFbjhSKzFVWjF3YXUzaGcybVBuWUV1cHd5Y1R2N2VqSVk4Y2ZVTmljaXh2cWpwUlNBVlR5RXNNZ0xRWHczb1JuOVIvZ1NHcENQTnMvR0ZBNlg5Zmw2Nk5PWHFtWEY1b0Rib3l1MTA3T1VMR3R2bCtyL205RC9TNytCOW13aVRLOEpVQ1V3WEttK2V5aTdpVUtKb3BMODFWMHRja09ocEQ2cjZxc1JoR3VRV2JzZzVKTFZ3WDZiU2xyUFlHcmRMN1ExS21mSEFYaXVuTjZZMkpUTSt2MERBMVJoaUw5Qm9jRVJoM0NnVEdZM2VWdmhmK0dONnN5emtkMDc2bGZEdTMyQld0dHpPVDkwRG9tTWVGR2JTUzJBNnV0K2t6dWdIc1VuSU5YSXdVZ095eFhjNHFKNEcwUnFWMWNjNlZ0ZXJTck15Z3gzTXBsT1FXbW5OUnduWG02TG1JRkZnZUVUVEhyNmE5NUpRWXREekdFdDVCVWxNdEx1bytNelNjcFhhUmNkWG0zTEVJRnJoeDlVTzd5SGtHUU1ZNXB0VUhDMU42WXJkRW54Z081Ykc4YWRXVnhsRDVYN0czaGNuKzBSWC9RRTZodW9YZ1BQaTFXT2xmcStvRnZtWjBwZ3QxM1lKb2FtNlF6ZXpyRTNmTk5yc3JyN0hHTUtSejlCRjg2ZHhkVEpDd1FUeVN4ZU83S0ZleDRLL2dzL2MwblEwNlp4di9SYjRkeGJZMXNQUUJqY2wrV29tZXdrSTFQSkFKYTl2YmhNV0QybUZ5dnRGUG5XZ2podUh5ZXBxcS9xNFZZV09hek0vQnl3amRQNEZUYW13UHR2VXpxT1AzOHNtVHF3NW1ubVovTUt4T25LMDhEQUJWd3lyRFNRUDM0bExEZmdmMjQvb0JOM3NnNjBEMVVBb0E2LzFIMytUOGlxOW4rUjRaQmNzVlhmcWJCai8xUW43bjdqdUVtSjF3UEQ4V0lZOVllUFRTdVNGL0dBZVhuM0dGa2V0Y2VOblBBdXE3L0h5dzlGZ2lvMndzS1hmRXRaRzRxSVh3bDFhNVBIVFhMZUkyendiNExMSlJuMU9mZjNiSXdFRU1XSkhFQmdnaVJLQTV6eDBTa3I1aElWU2h4L2doQmprVkMzbnpXcHNwZURpbVBHWE1OZWl2SGI4SkM3MlhLOUxRNDU4VUFCdGp2TTJmeHppUVp4SFZvLytFZ1JVM1g3RWlGazhMdS9Ud0JrTVo3em1Sb1N3ay9tV2dacGVueis0UlFRS2NoZ2V6QjdhbnN4QVZxaFJyS0VSQ3NFQm96aXEvOWpzRFlkY2VxcUxvMzNKczdBcUtMb0lJR2xXWWkyNThpcTI0VThCZjkrSGVlbGc1RDNSM1pwc09UaTBKZnlpcU53ZVljdGZTdEU0Yzk1emhkdGJNZ2VtcVdLOG94WVZHcU5QRW5aRHpFQmVSUU9WZzlGRjJJTWpZVjVLZWYzaGNDUFVnclY1L0Z0MnVWcVo0U3ZnazBsSWY5ZTdVcFA0QTg1K2NnQlByUHBKUEpWK3YwWHZ3SWhEejJEWmpnbG1xOGZWNkwzWXdpUjBRbXBiQlRMZVZjSVdEUEJzenNUa3kxcy81cW5tbnl2N3NhNzV4cVhOcE95dEtSc2hJY2g0RkpOeUx1S1h5b09lWlRMemdzZE0wc2pnUnVETyt3bmMrMWQ5U2JYc3lsZWZUcithOXkyTlFmdUR2Y003cmJDRXBSRnQ3UGVhYk1xRGxVcU44YVo2OGJ4dHJnaXdzaERIMWFOaUlXQ0VSbHpYVjNXNXpkeEdvWDVTNm15ZEpQNmRLMjE4TkZlc1lVc3lXVkNKMW8wY2pjNDFLM2V3dFFaQ080Q2Rxd1lBSFpnUmtvMFdha3ljNDhNUlY4N0Ivc2ptOU5HQ0h2UEVHUTBRUDJGMXRoY2JrOS9raFNmdEljQzZoNnErTEVyai9hanYvSW5kblpoMm5EbzY4bTcvbkUrSW54Ky9VVWxMY1ptQkFBYjk0SjJvNmhQa1B6NXZmT2RObm5jM3RoSDhnMVFOUE4xZlJXb2E5cWs3RmdjMEEwcEM5ZEJZQjBuNzFZUkw4b2pUL0NUODNXWStwc1c3RVZmQUNyRVlHOVhqWnpJdS8rRXRFQlI0L1llZHpXT0FpNXcrMDJaS0Vhd042WURwM0RFRWlYNTluVWFONllTNEcwbEpVYW8rOEw3N0xYbHdtaGw0Q2oxREkxWFlzWDJyRGlXdjR1a1JZZnA5SXZ4QUdZbzBOMFRqUWNwZW5NeTZMaFdKaUJqSE5hdE5kVmxJUFQxVmFIb2hBQjRPQnRiVk95VHJreG1JdVpMdW1XUkdhQ29YYTFSOThlNkJLNzk1cld4QWFJQ3Iyb2hYdlVLVVJCWUJYL0dISjFFZS9wMEYxeXc1RTJqaWVCd3crL3hRTDBZMTdpUXBtK285QWNXMGlKRFQ5ZkxVRzVZUTltYnJlZU1vcFQ2ZzRUS1UvbEtpWE00M3J3aTRoL1hzblFJcmE2WWNaRTBaczA4WDBTaUx4YmhjNGs3NGhJOFk2MnJwNE4yODFrdCtneXAzMzRhL3RyS2hCQ1BmSTljUGViNDVRVWtDZDJyWEY3WVhQcE5UdkFvMVY2bjI4MXRPVjhkVVdmL0tSekZuN1Y5YzhISzh0Umk5YkJxSVhzZEJNUFloQmhnZk01TDJpWXM4YzFEK25PaFA2d3ljcWNwZGE5T1Q5U0NKVFhmOXEwZEVENWtwdkhKdHQxU3dxYVlUS3Y4MHdCeWxYVm4velFzQzNlZGJlcWxCVWFGZVV4aEpVbFhRbzUvTTNYUFdYdUYyenVORlZyWUhiOVcwUzBDR0YyM1h6QmRGYnNIb0pLWUNkNzE5ZVQrRWVKSERIUi9SQm1NRDBRZmphTkU3VlMvMnYzUXFNTWRrMnJyWVIyN3dWcmlJcnVpbXZHSzl4NjliVmE0T1dsR3o5aWlWMjNUQWsxd3lDZy9UT0d3RWFXcWM5T3F5OTVrUmFlM1M2ZkQ1S2R0b1VIYXp4UUtkTGI5WjErMldLRDhTNGR6Z1NiaytIR0d6ME5YNHFRUmJmTzNCcVVXdjVBTVpDVU5icVVkWk5pRFN5NzdNWUdaakJWWlZnNjQzNlN1dUpCVFk4anMzQ2ZlNDlUWkg1RWRORXhMUm1ST0pwU0liUCtJelJuckR0RWtoelJBMS82bk1IMnoyS0VQN3FUS1VibXAyRWZRSE5MbTNkeEhUK1JMTitLeDV6Qk5XckdpclJkclBtKzJUTEUxK0J2MU91bWcyc2ZwRzBpZ01oKzVmdjh1RG9pQ2U3eVZoRGRJOFVaVFhqVExZQ0RLcGRURjV1RlhIU25FTUl1aGREQk5ISjJueVpsMCtVdldtUi9YY2RZNC9oQ2JEbGtOMzhXd2IzZTVyTTBTN2s4NXh4a1c2WWNsa3UxUmQwdjVoanprUEdSbmt6WlZlbTRPM1Y0Um4yMVB3bW1VUUw1eVVQUSt6bnJLZlJieFBCb2pqbFlBRENsN2xxVEpqQlAvVWdaT2ZBZ3lHaU5wS05QOWxBWlVjckN2UE1rUGNhUXIxcHFCQlp0WXNreVZQNis2T3N3VjNtZDZjcDNOaFZuU0hFVldJc2tsRUVpeU1PWWcxcGp4NjE5ZGtlT1VZTHBoL003VUlvWEk3SVNYelFyRmRuTkVybWVyM2tDWU5OTUtFOUlMQk5yU1dkRXUyaEYvRHpFQWdveDJXSFE5MXkrSHMwbDFURU54bFNpVk9BTmZJRjdZWjcwRGVLOGtrNDZudElnc2RQK0JSMDdscUcveXFuLzJKdmpLNURhUm8rZk9nb3hjQmlxOWVmZ05DdXVnRGRubjJIR0k1NG9Gd1o1Q2NYN0h5SURncVdZQWNrL2dhRGNhVFZrNzhjU2k1dnRuaXlRc3NjcThFUVowZ3VIS1NLOFJQZFBTcjNuM3hHWXI5WDVYWjhwZkpkVFhOYlI3TFU3WThjYWRKMmVwSlFkRXZScStZc0d6ZmNnTVQrV0V3d2o5Mm1TY2tzbkY5MGE0UEUwZm5CT0puTHdYVWtvd1JIVHlESDA0UlpwVGxQcjhPenVLQk1UT0hldHNtdkJ2OVR5dmszelZITjlBZ0N5bHBaVmd3RW9lMmJEVzFxNk9DYU92T3lnSHVKU29uVTNxZmZidkxyK2V0SGlkMnNZOUZuVjFJUEJqTTRPSVYrNDNRdmk1TXY1d3dTL2VuNU4wQm5SNmlydG1tRm9zMXEvZ0x5TnB6T3ExT2JtOThGOUhGTXNkTjRQOW9ZTERhcE5YVkhWY1hwL3pWMXdSbGJSL1cxR3MwWWZOeTk2eUVzZnFLcFMyZ0tPN2Z2UVFoN24zUkRIQUx2Ukg3akJWeWQzbjJjMFRtbkZQeUtMSVRDc2JJVFZUT3FjM1FRTVl5cXVIYUpFUTBYanczTG45YWpjeGVSbk0wZUNvRGZFbGVxbFNIcGJnM1dDR1MvRUJNNGhOQ1lNVXFHVmJob2FiSVY4dGI2OUNwUVk4SUEyVHBKUzdlanRUZUZlSGRiZzJtZVF0bVBIb2FjT3k2VUxGbkp5T0cyR1c1VFQzL3V5NnpFVmJRbExMRmhSS3VtVWV5TDVVZXQwZXFTeTFnQTdDS2ZmVG8yNXJ6Mm1iQ2lyNzhHa3pTTERudkVIZTlaWTFhY3hWQjBHdWZpc3RVaXJRK1pEL2JJdFpLakw3Uit1OVJiMzNGQmlWTmpLR1B0ZlRsYnlacE1iUnB4Vmo2M0hIZnhVN1V2WTFGTjBQNFZOckVRWXZhVkhjUU9rZnpONmYwQy9rdjJabHh5NElJQjdIM3YzRkJVNlBBTGI5Wmh5Q1MxUVVaOHNzcUdlTFdzQWp6WVkxTHk4Wjd1dVJFUDNpcEk3M2JDdm5oS1ozNjVhQVFKaXJ2THBGRUhydUZYbkswQW02Z053ZUxmUWRaMFBmRFArNnJRUVQvSDJrc2RLMHQvcDR5TXVhd1FnZzM1bjNNRVBqWjlYaERJSlFER2dyb2lhb1IxY0svb3gvTm5OSk5WZmR4OG9Wc09uZ1VVMEUvWGlPZ1M3dGsyWnR2VVkyN1FDQkNUQVBkdS9JKzN1NTZIbVdZdFdINnhlMDdFK0YzS013ZVNJREVpM3hxcTByV1FqZkZQMkdVOTliNXk1eU5KWGpXMTNqbncwRURvY1NEQU5sT0RucnRad3oxL2pCVHBSN3NoRHU5VGVFcVhNSFR1bzRPQ3VIM2hPVkdmaE5Uc0UxUGVFQjcwVUwwZTJUUTNlN1RzdnpNZjUvRm11Q2VMdWo3aEJCS3FobXdnWklXV3c0aUpHTkVUem9IanJtL2xpWHJ6N3ozQ1NELzRZb3p3dFM3YjBjR1Y3em5SZjhDSGh2dDVQcGRsU3JIS2huMlJ5aTc4YTcrY1c2c1dlTUZFNDJzdDBkcW1teU5JVVAwQzdvRVNjWVNXelJoelcrbVpBWExmNmF2NERlYVdudkNkRVdHdkpPeGNITVMxcXdjdHFaVm82emxibDNYZWVyM01xRjMxTXlmSlFtV05IaGtVWmlZMU5kWDc5ZGlYWFoyMnArOU1CSFFFM09GT3JveGNMR1AxaTN2bEt6MGJLYmovZm5NTWkrMk5LYXJFaEkzQURQM0sxSUdNS3FuZHRCalVKT3dCWm1jK0dhU1g0YVV2ZWdDbE1VWElUV0JtT3hvUEFUMmhHTFVIbzRKWFBTZitJMm1LME5FbXhSUXhZdGJ4SXF1V0tnZ0FvVmVGcG9zbEFvMmxuNnZGSEliaDVOYi9SNmpRTVZXN1hTQ3VtSDNsU3BkcTh2ZXpVS0VTRHN4NE4xNFp0emxXSkY0aVRQeUxBZGw1RkpLT2o2Z3U0K24vZWxQQTdidXZPcU92anB3Wi8yd3VLNTFPVkk3RDliSTJvRCsxbUNycEhua3plUHV4NU5NT1RBNkxIY0FZM0F3MUdWbmJ2eWRoZUFaN3FuSjdSQm1pT1BIR2FKcGpPSlF6ajY2TEM2Mlc5a0ZSaWk0VnErN3BYYmpGSnZkUyswVjc5UGlGaTN3b1AyNTcwS2owL21tRVhQdVRxZzNVZUR3RWtiTVFJb21hYk9yT1FCVURnNHhBK0I4cjdtNzlUL2pPZG92ME53SDR6dy9iUE5PQlBESmpoWldFNHllb25xODdaVDgvaVA0N0NYVHVIUnQwWFY3Ym9QTlJ2ayt3NU45NG1aRXIvYlZEVVVpVy9yRDdGQ0xDYytnL3dzN016TEhuejhkS1I1M25pMEJOUWNGOCt3aWMySUlUY3VmZnJ0NnIvZTBSUFdXZm9RVkxjS1FZUU1VZ0taMHdMNmE3dlpVNHQ5TWNTU1BOc1h0RmpqYXl4c2I0a1NJNm5TM3A0Q1JHZzdVQnk0N3YxQml6NkI2TEh1UnhwdkNHVytpS2NiRGJ0dFowK29QdUpUZnFzR0lyRWRLdUR1aVZPOHNpYS9HZ1VCcGZsb2V0bW42b0F4dHdsQ09wZExUSG8veWM2WnE3dFJmT2RYcFlDbTdXU1p4MG8xU3ZvMlorSCt1TGFuWE9TUmIzbXJOckVoaUhJSWVPZ2pJdzdLL0lDUktwUHhYc2hFenBjWTd5b3Zaa2FVNW5FbEtSR1ViTEtOSUQ4Vk5jODdCLzJBemNxVE9uaHpXQ01HMXhWT0FyZEo3bGtKMkY4MVpWdzFidWlYcHY4TGdGUWxHVGZtTTJ2Q3Mrem1PRmhUbVNCMkVKNk1lZUU3Y1pjSjQrZ20vUkFtejB3QmZwL1UwTVFhdmZxZGNJSFQ0ZTJOMUd3MEFMa0lmaFlKSVA1YzlFeE5oL1pOYmtSR2hPTmFwdkJyOER2dGp3bHJNVmJ4V1JiNm5veHNrQU5WSkNvVlo5YmQzTjJsWkxXb05LZDhySU10NlB4K0phNllZOUVNeVloa245QnpVcHJvSzkyeHBnOTlzVXhoY2RuSnZzL2JYcEdacW50VTdkV0U4VElEUlZ2dWdOZi9MbUptUUxPTFFmTXZFYjFYcVBHWWR4N2NqcDBHRU0yWmlqSFVyTUdHdDlPOUVZb2pPYkE0UTVidkFoYnUwSS94Sk9LeVBJK3d4SkFqd3UyV2w3c0FCYXBBYVFZaW9yWEdYRjhORndWL2dpOUg5Tmx0aW9teFhJQTNpTkJsMmJZNDN2eG5MZDhmZHY4K0FCbXgyUWt2SEdPOVNZS3BhL3dUSUtrNVJ2dUptMXlzM3lzVThGUFZaZklLY3hIUzhwTDIwQ3RrVTUvcmlEMmYxT254QUQwczZiL2s0MHZibTdScm1ZRjRxb3gxdEpESURtMHFNbVoxREVUaEFRSUdiR3Z3YlhpdWo3aHo1TEpHMW1sUWlCajB3NXViZmlzUXFJK04rY2pZN09iQXJQSk9PU1Q5WVlRN28yS1JPMlFTZXZxWjVEbS9zQTY0V1RyRFQvVkNpVVREZDlpbU5sUnNRSG1aRlpOTTJmUnJOb1REenpzekpJWldtVGlhNFp6OVpKVWJwbitYZXpPL3ZKaXBsdjJRWEw0bWU3MGIySFNqSmRiZkZDWHNMUkc5elZkWVlWYmQ5RVhDd3h5ZW1RZjRQZUxoOHJJZzljK0dZU2F5dm9VenJEYk1TL0ZmaDdQZTNvNDlDd3lNUWJ2UlhrL3ZmRndqbVV5L3BUOE9YZVZwNVk5VTNjSzYwbm9WWmhPWVV5NlFZaE43UlBxRDVmWFFpUitlMmNKeE5PaGVEL2ZPTEFZZ0dmcUNLbXpvUlF3YTFqdXFteHJCL2RoQ0Z5c1Jhem1HM3N2dmp3aGJGMGZVVFloc1UxS2FLK3hwb2Z1YzFhSU1ndGNDUlVqQmMwYU1sR3V3Z2dGd2xtUXB0a1RLY3FxS3pHU0ZkUnhtNzJ0NzNRNmpUdmZTUmo5Y01yUHFyTEhYTHFKMGdpMGsyYVVPeVYxMlpOcU9LN2FFZ0ZyUllTVS9KQmpVYmFIY2pjNDJJR2ovZmlOYUprck83ckFNZ3lvekZDdlUzVVoyU0twL3dSZHVCWEt4eWZRdFAwcHNUWDJiMnQwY0lNbmEzNjJqSVBlOEJ2TXo4ZGFiU25wOFFGakpxTUhJaEFlSjJ2M24xaXovNlk3SHI4a1ZNamJNMHNUVmxsRFdiRmZSRUNZTkVRVldlTUZYUVlKU3YvVEFFUkkzeElNY3MwN0M5T2E4TnVqK1ZxQ1NoQk5ZWWhGcUtPQ0FFTGZZb1pYd3Z6OWUvaGI1VVRLS212a3RSK0lncHdjd1BXT0o0Y1FKVW9zeTEweUUvSmVIaFYrUHF5YS9RMEtoRmpMS3poVm10bEIyYWg4eEVLdStjc1Y2N2RyVHVSUWJzSU1pZEdhT0owbG9mdHMva01GbkVCNkhKeWcxV3VFOGVzTUZsckZNa3g3UEtLdzRyakpwTkIxTG1ZOUtWWnk4STNkK0o0NWQrNWVhZVRFTmVJTE1peVdibEdvL0FJWGt3QTVwRGtsQ3B2R0JBWWttNWpMdmF5UFhGU0plZVZTVFY5N2tGNHZDMDJCcGZwanRCM0cyY1Q4SjBKQVlCTW9uV3RBekFvWTZKNXU1anZWaENpcVhGQzJLdzE2cmdLZTQyZ1VJMG8vdURFN0UrNU0vZ0psQ0FtY2cxa003ZUJ2emRxVWFTdVE5d1hpZlJaYVA3cEVPRlBmSi9UdGNXVHdLVGZOdWUrWXFsVWFYSjBkWllaVjJUdXpKaDZueTFrMFNGb2oyUG0yaU9mZ1dnTWEwUkNjK25wd0tmeU9jL0tPaU9MaEFUM1ZzbksrdWNqaW42bDVWOWE3L2c2WXpEdmphWW55ZmV1VFFkbUtsK1BUSE93eVhpQm9vbzZGamF4OGVQV1lHaWEwbmV0SXNIYnB1ZHVpSHJDRTQwMDkzeG03dFFXc2NFcUVWL2dxWHQvZDVFMm9Cay9TVzhVdTJKbi8wNjdTLzVoWG5RM1pZaituaExtZjcyN1N0MHZKdjduak1nV1dxQU5ZL29ZQUo0VGdJak41TDgrVnZaS1kyYkhwYzlpMW90ZUYzK1B0UU5aMmdCU3FkVEVwMHZqYnlJVFZ5QnRFM0ZpYjVoWUlHOWpjVjJmNkprRDc2d0I3ZldyREtiZWM0MlBRelIvRU5RQjFoVmZuRFIvSlBBdUlWam4rNS9yV1h5TTFoL1JDOThaWXhFWWU2YjY5R3lCK0JsMXJSUExmNitxYzFHdy9hZnE2blc2ZCtmN2NTdFYvWVZQclNEN1JHemdKY1lFQk9EQkZ1REcwd1VrS0pnTmpLRzQ0WXZ5dnRLOVk3aUNkMzU3QzZLYlJFTzE1a1NuOHdsTHc2RmwxRnVhRUtJRzZMa3V2K0plVEtsaTdPMm43L1FxYXc0VHVSeDJDWjM2WElLaEJFYlo5YmxQVlYvTHkvS2hWWEE1MElub3FJaHQ4a1RNQmtKa2JjZkRveE5PODJoRzNJRXVXRjh2OVdNaCtBOHlMUnlRelVCZ096bndicU80M0hVT29GRFFXUmZiSFZiemhKUkpLbUttd0RpaDJBMmxoSmFIUlE1MHh2bUtqY0FrTjZGTkp0RUVZRG9HVEhTbzhhMklBTjQrQ2lrRXhNOXVITWpWNEVPMVowREl2L0pDcGpJbW1tbHlvRnhZMTZUb3JpL2hUZ3puN0FiSG14Z3Zud01jSkxrOWhjNUhjME1IZVpUNlhBbFVQZEtvYlRxSE5UTFdVVjdYRHgzR3dxOHo2TU5zRG9SakE3dzlKRk9reUhRMkFEbHNCUkRhSkRqUjF3UGk3aUdqK1lCUyttVncyeFZ3SU5jaWMzVnVQdk41eTF4RnRtemVlV2RPRGF6K0t1Z2hYbVZROEhHemw4QXNNZWVDc0FqTGtDcFFGL2pTS3hheG1OYXZDOUoxNVViQjRFWlBhRkNlbVJQWEZFMTI1eTl3MVc1TmMzKzZoSzhnTjl5aEhHOWRDWHEzRzVSQUpPVXVVVE1mZUJiTmgzWGd0TWFadnhBenl5WGFZYnU4aXRWcWpwTGFJMVhyaUNGYWJJZ0h2M3B5S3c0MU5abFhkN1pxNjNCUWNHMUNNb25pODJ1UjJwTzFINGV1UG12L0xRak1NMUk0d2ZpVHN5MjhaOTQ4aXFmSFMxejEwSTFWRWNod0t4K3pxTzdBQ0w3OXE5SHFmMGdpd0ZnRzA5ejlIRHhUY0M2SnhPUmlzcDU3ZTVkSFU4cklrSlBWMXNucm9sWXByalFuQmZ1bEFOc3paU1N3QUxra0haOFowUUZhQzRNbCtmMDEzM3NqbXZqeStQMTRyYmFwU21JZTl4S2cxcHdKWmZqcW9ETVVuTVVxK2lORkQ3N0NzMWgxOW5MS2FhTEZIMHFWWXFlalVoRDJVcUxnTHVVOXRnVkRhb201eFI2RFpGTmpTaTNNWmpnZTNuZlJFYW1EZXBUaE1ZYit0V3VodFpteEV4R1BNdVpJclJDYnhxS3F1SlhnNlUzZVFiZmFLeVJHOE1jdHNVdHl2RWxYVm4yNUZFNy9ZRnFyaWFoQzF1SGVwTkhablZMenJpaWxyWmlhQWhhQm54M0NLNXZCWWRnVUtGY0FJdnhoUVlhOEZhMjI0TE00aWxXNnh5V3hmcHd4dmM5ZUxwRlN4VStlOTVaTmVhOWZIOUhUUUU0NHMwbFpsR3diTHdnSjlpSDlsdllDMWtscDRDeE15dFJTbExjOVpJdng3ZDZVK3RvQWt1T1ZXYnhObFlUS1hqbDM3UnhsczRobUxJYzUrdEZKQWQwZFVtVjMxakRnZHUyNmlHR0pxYzBhb2dBTUtkTGNEY3lyR2loTXI4MVFZYkgyQXFjaFpBZnBKOG5jL2tjVkxEaEZpaVpOS045S3BBdlZhcDVBQWJEWTVabnYwS1A5WC8wUlJJcW8wOEhmR1dHNDZjUEI2VlN2RUZvdUQzdi9ZUEF3eEI1SDl5Mk5XeCtLZ1ZMS25xM2oydlhBUkRmbHc4RDBBQVA4YVhRM1dZK1B1VnE0blhXYnBkSGVCWnR1S1l4VGtUaWRwOG5GL0IwaDQ2MmpxZHlJczJndHhEbkdhTUg1T1IvYUhaNW9CbTV2RUxjbUg2YUw2RFBhTk1wMXBHQU84em85N3loamZKVGpzU0RPVzdXaXpsSVZDVG9xMlhIQVhpalBnVzBxNThoRkZFUEFVUDNJNy9KejcwM3dZR0ZVU3VzS2VRWnh5TVJOYkc0Y2dDSnBYeWJ2aEtPTHRkcWZtbjQxK0kxa2dUUGt3SjVJRStoKy8xeTJRdFdLdnVua1FmdnpDMy82MU9WeGwxN3N5WVkxRmVCNUZpRDhRREdmWmpEWloya1psWlgvZmVIN2kwL08wNHNmeFVFNDZjRlBrZUwvSU5GV1JiOXlDVlRzOWhWSExiK2k2a0M3ZWxLc3FtaThBOTRrYnQwK3hIVjZxc2k2RzNwL0pJTGl1ZTA4UVBwRVU3dHE4OHNuSVNxZ2tRKzlzY2V6c1VzOHkrd0JScUN4aVZSRHFnd3RsVmlTbFVmcE8zKzZoTDF6ckhlMGgxaUhRZmhnaVlDNE5XNWdWb1NGYURGV0lpS25aMWlxK0s0VHM0Y1pPcnlDMWhMZmMvcGl4d24rc0tkeXpaTFEzWCtjWDN4MmhFdFlBdHZncXhneksvbzdUSVBLSW1rNlA2eHVvRUsvYk50VVQ2ZTdSOXFEOUJWenNQQzhtNlhKSkZOcmkwSnhOVm9vQ2ZjMXRPQmhvVnA5YmRHMzljdGpmYkpQRVNLT200UWlUWmFrYXlzQkFqZ3ExbHBKM1Q3bVJJYnd3bnBuWXhsRVZYdFdTQ3ZqenZmYmRYcXd0SVUxc1FzdG9NaHVFQ0pHRUtKYlN2OUZBZDVqRjV2Y2ZGTHZMd05icnp0WnBqNEEwRE9EUC9NWno4UXgzU0ZSUDk0Z3lSOFV5SU43ZFdYVFV2dEw3b05wNG1hZDdWbFcyZlRJRkpkRkxPM2YrMXR3aDJ6emtxYWlZSGY0UFp5SVdrUFh6UVJIYlptZ09RcXpxa01ReGM4WE5KeVNic2ZDdzhFNzNXMUtvVndPbW1SbGdTTnJlc2VkU3NwMmRHOG80UnphcC9QTHdmRThteDd5azV6YVl2N2k4c2pRWlBFdVB1SWU1NFpxRXdlS2tWNE16dXRGcS93NFlzc256U2gvQjNPL1JOMWlGcWFPc01KMFVYRDFjSjBaYm8rdWFHVXVRcWY0SnhTZTVQdjdzelRvdWFNUlhsR2JoYnR6YWp4ekg2dWJiR2JpbXlZanRJVk5BZzNxNlJ4TS9BQjEvNEpoamt1d2ZPM0xEeW0zZWMxeklCRWlYTTM0bTc3Vm1EYjg1bDRWTkgzdUJ2T1R3MXZFZGRBVjVjelhxOHEzMHh4Qk1DKzdPNnIxWjZPZGtYUVZEWk1JY052Uk5ucENLN01MWHJBQzMwTlpKU0N6V21qaXgza2lEMVcwa2hyTERGd3dwVnNzWmdzZ1dKc1lBYXFOY25MNE16WWUvc1lCczk1NG82ZmlUTmZmTmp4dll4aThVRHBRY1BLd2RQTi8zSFVTZ2J3cXhhajhTMjhsUk5lS0hORGl3c0hqUmFHQ3htai9mSm04RVlhS09Oc3grY3lSbVJIUmhuWE9YT0lJOXRSa3FLRTdTUHAxU3lqY0R4VFlXZzlXVmF3eGFjc0FkWnJVK25NdzY1WjdVcll4VmprT295VzYzdWorbDJ0N1FvWFVCZ1pycjVlM1dFNDNyWjdJRE5FMlcxS1Fya1YvdFB1UWtDVXYwcDhXS1N1VUROQnBWZ1VMa1pTMzhGUkZMdy9vOWpZL3N3OHAxZFZmMkxtNGVqNHlqZ1JjcVY3aGo0NTBBeVpLNGZwMzRQOU43UzREaXBOTncxa2JiK3h2ekVhRGdFc1hGU29MYVdibXJ3RkIvMHpBaXRqejlxZDhYWUxURlVZUG93dGs3cFdhQWsxalRsTG9GSDE5NEhCS25yR2RTK3JNa3licHZVWDcycUwzblI4RWp3a05vK0oyYU1yVUhKNU4zUDZJbTVReFdDdkhISllyZTAwMVIvbFVVQ2FRazdYQmJESGNnQ3VnVTFra08zQUl1Q3RVN3p0NDYwQm5YVGRKNXNUdmhPZUFkN3NxR2crbW1RdnJmYlIySFE1N3V1ZnNiNURPL250YnBLbTJGZGcrMzA4SlFUWWFON1U2Q0FvNzYvWmNwVXRibFdBUlFheDRrRDhIUnVYSTF6NDIxWHgzcHpLQXJIdVNwR3FBYlZnaFd0anRXUUUrYzgyZE5xRFR1MHA5cGNEWGRCMEFPR1dGZHhNYVNncnI5ZDFoTUdUY2dLdnNMZjhidkJDN2lSMEEvbDJ1aXNEYktvR3F6OVQyaHFWMUh4L25JanJhTHZMaE84eWVPRmlvRC8xb3MxSjdNQ1EzVVRJQTVabHZucVZxdEZJRkkweERXNUZnWHZUam10cDl3WnMxbFZoYWljbWFsNWwrUURQZk0ydy95K0FRUUlCQUhjRnpMVVVFenU5Wkl3ZnpMK25tZHEzVWsvQkdNdVd3ckpHQ3F5MGEvRk1abEhDeXlYRFFMSThHd1M2emNIaDBLVU5IaEF3K3Flb1FPOGpFU09kVVNlRzBDQWtrVmtHSVhraWRsT1RyaGNXY2dMZmF5SStGN1V0K0JKdGFwS2xQZUR0RTRFOTVITnpnS0hEK2E1TG5BVXRFMGc0ZDdEdElobTJvKzZac3RmaVRHd2YwUU1CcGQrRFhkT1Y1dUd2MGdzdnBzL0x5Z2dzOUtqdkJFUW1xWFlnRENXd0Z5U1FUaGlxMDNOYmcrYVdIcGxqQTl4aUZnbGlmZENuNEdQQ1VkMXRRb2Qwek1RL0VuWmZQNUcrSVorLzduMUJlYWZac1ZhejNubDh6eTY3aG9ueUxaM0N4dkxpSzE2eXZ1ZzBOUG10ZG1CVFdTVStPUHV5b0d0OWRtcUtlNlF3NVlCaHVVMFJNU2VzKzJpVGFCcXFQcDZlaGdDeHBnUmExY3djcmJwcHhPL0I5MDdCUGkxbW9RaGtwU1c3dEdjbi9XVWl5QkJsQUFHU0FReHBNQkFqUllhM0hzQlptUU16c0VScHZQS2N6dlQwbHB3Mkp2bDZDQ3BvcWRocjl1ekNseXZXSGtmbXp4V1pNNEh0WlVFUWR0bHR5UjV2eXRmaFl4WjFlZGtGSFVJUlV5dElMVUFSd1pDU1ZRaG9lNE0wWGxKSTNNTGljMFY1eE12VDN3RUhnSS9lMFc4VlBGYlduZlg1QVEyOWJ4UXdDVmdUK3VKbjdYWml5MEVPY3M3bFVxTzVTeHZ6alJtV3U3YTdEdklIeGxTNjV5U1dHdGRFaTErR1lRSGJ3bW4rbjNJN1g1NUZpY2R1TlBTMUVBWVB4ODZ6MWZiREh2Z0FlUER1VnRnYWpsWFg2NklnODF2eWRIRkN5UHNjbGh5TCtkb0pJc0JuYTVDNkVhdEp2alF3Z1B5S1E3VzFhZHR3ZjdLSTM2VFFPUUNOMUZKclVGTkNKQTMzc3NnQmVBaWppalR3WVIyTzd2QnBnekZ5OU4zT3RSOU1nTjBSNEk0aVF1QUpDZDVYM1dVTzhOYStyQ0xjWkpBSElNVTg0VUZ1cXlDQXRSNko1Wm1tNk1iTFdDWjQzVzlmS2RxTUF3bk1YMDVodXFiUW5OV0owYU9Obmt2UmJqdEErUytGTllDek5YcXlML0VyODlnYlJ3dy94UU9Jc2hpOE11N0FNUm5VME5hVlRSbCswa1A2NTNUZi9haVY2SlhiZEdqY2pOUU9MQ3R6NVBkblBQZW1zQzBMeGpUNXNKcUJ0SC9kUXoxbEJpYTNVUmxFWlArS3VlS21mWHh2bXp0V2RBOGdtK2ZnSEk2VFBPTURCOS9NMGFNMGg1emdCcDFxRkZ0WllaeVlTWjdMY0M0dWtBd1czZDFJbjZ1TTIzODdBSVp6dHVhNWw1N1hRQ3ZvSnJ1N1V4WFcyYy91VG0ya3lSQTVXRG0zdDBjajFxV05UaHpUWlhxc01KWldFTUtLeUFzUWJaZVJIQ1hWcHpTQjlZQnlTaEhwNU13cUtEZlJjNkRVcFM2T1BYSi9sSWxSZXMwc0FWcloxUWFMOHlmc1RaOFRSQm11R2FDeDFRdXcvenc4cFNuNG9oMG0wdmZoRWdLZm1DdUoxYXpQc0tFN2JoQU9pNFI5ZlBvTmtYamhjQXJSejRTdzcvenhOSVFGSHZUeks2OERDbEdOYmRnTTZRcVlKcmdtN3RNUVFiZHJYNHUxRUYrem5UUGxLQkdDSk5sUDFpOEJNWU5EQ2xhaVVlVFMvQm8vUG40eE9ma29vL3prNTdVNjdzdkJnY0tBb01PVVVKRHp2MndEOUttL1UvbWdPeS9meVhaa3l0OHl2RnREdVdOSHltbnFqa2FZWmh3UTNPUEU4MWVsdjlET3lzZjEvNCswWUJNa0ZZQnExV2VvOHN5MHpNdmpnZmxTWDNIS1JvWGFZRTVQN1duTHB5Nm1BUVRSSkw3MkU4bjVZWE11RTRuRElZRHEwUW40bEpjOTAxUElwK0s5S0Jkd3MrdXIzNW9tZEhHVGpObnZRRHFSUFpIRk5nTjErcGt4SkNrdEhXNTg1c3dwRklqL0dQMUlGUXFMWkJHM1VZYjBvbGErQWRoYXZqNkRxK1BTWmdPWnV2UjE0TEdFcmhsTDJIb2NES0lXUUxkNFprQ3hkYmpxbjU5MlAxdHM4bUtBWThMajE0MDlMU3ArQ2lhendVemxVLzM3OG01andLM09ENXhTazFWWkVSbFNORHE0Y25MLzh5OEx5eTBtSmtYQ2s4WVlOU05wZ0ZDd1dHYVdMOWg5MzVMRXVnb1R3azFWcjhDOVJISDgrcGxYMk9yUmZ4eWFFZSsra0RuZUM0U3I1TTBXUFJYSkdwQW1ucERWTG1VRUt4K29qem16OS9iMjlSMGRoM2hJRHBUU3hpd09sNFFVSHFzWk5Mc001OE45cjQ5VVdZUXRTOHJrTVg0SHJ3a1NTZnVZM084bjVpaFBUbDNRSEdudHpGTjdyQ2RmUkt3aDFXbFJTS0NaMzA1cDMxZUdQbzhkdUZ6V3NFRC9DT0FkK1JrVXQxY3NPK3JsdjdVMU8zTzd6Uk5EUmo0Y2krWG5TSUFFY1p4RVI3aXVzTDhUVDhFQm4rbUo3aTl1WCtad2RqaU94MmFUVUQxTmdxcWNMS0ZYWGI3OVVXZ2lEUUFHWnozZm9mdGFTTWlCdXcvanJtbVYyOXlKdWZOWWI3ZC9sOURxRkJzTjF3eUZaUEVWSlZ1WEo0L3FKSWNkdVo3ajVORGdNbjl1dEo3aFJMQW9DbFJsMU1idDgvUTV0bE1NTDA1T08rR1puaDJ1REN4Q25FQkpLdVdMWjVxSnBwbHo5STZtVHpON0hIQUhWdlZuOHExU1VEVHE1akowQnZsdHFyOHowTjRvVHA4ZGRDSERJZzZwS0R0VFI3ZDl0SHhEMHF1MnBiM3ZDZXZLdG5WL0NoMUV4ZjJteDB5Z2lNZCt0YStHSWw5Y2M2SHBzSk5MczVWUlNBWEd1V0RLbnFjSnA0Z3NFZkk3YUgxbEJoSXRrY2NYYlN4WklNTTVNK3Q4RmV5dUkwbHAyK2pqQkhXVDRWbituT2ZkRVd2K0M4YWRFaUdhdWtoVFJzVi9KNU9uZjdWWFV1L1I4UUxwZkZBYWFhNnpkOHFVNVBkeXFSR3lmaDhVdWI2cUVBWHdxaEFiZkZHdEZsZGdTZ2pZTW1UUDFnL0RrUWVqRitXK3hPVGszT3hXclhzbnE2VkJnRXhkM3l1UHZlcUpsOEVPUDZqMWhwODJuTXBmREw4UExDaEt5OTJEUE4yVUJuTzh6dlRYU3BURGFoRVk2UFZ6M1dmblRPUXA4U1RJdWt2bkZoa1o5Y25QaTRKSXBreHNWT0U3QlZtV0tvbWpHRTUxOXk0RVMzbnJJN3lyTSs3L00vdE9xa3IvZmJSdmRkNmdtaklBREJSYzhjRTMvSmQ2QnR1Y2FnaGtzNE1iQmJDSzBCYWFWanRKdGQyOCtNeGM3bWRIbUV1Tmw2bmZ1L2Z5U20welluTFlXWUMzelI0OG8zNTdiNDhHSWlkU3JpT1dMazJIcUJzRHYyRUJFY3B2SWptRWM5cmhxekxPVE0vd3VIZ2FVWHF1RlVrOTV4eTNVR2NSZlFCL0lDM0hVY0Q5cTYvaDIyUkdtVFlnb0ZjQ1E3R1ZlajhEeUgvQkJJUENPam9WT1NHVHUvSEdoalpBWDBja0xtVXV5UG1LQUJzMHk4OXViL2ZOT1RaTXNpTXhMMlFvbVZPdndkYmhXR0R4N2VmQVFsclArUzNVY1pldWhpRW4vUzBxZ3ZUZGJ1MWRaS3huU0F0cFhiM3U2QmJ0STZQdU1EKy9SQTVvcXNqRWYyRVFFenQ2a1pUOWdyMDJVeWRncTY5MG9rWkkwMG9xd3JxOWNXWERZVjE2TjJRelBiWUlGNWR0RHFWUlgrQ3VEdllRUmZBMVFmRVRaSW8xSkJxSnNHT1RtMjVkcTY4bFB6bXBaQ2lmTmVEb3R1UThQY2kyREtFVkJ6VkdSOE9RNHRjYW1xRVI3bWx4dnR1a0Z5RlVLbFVwNW9LcUlkN2REcUtmSlBHWFNaMXY4RlhuU2FSdCtsajA2WHd0bEl4Q0lldXF3bGE0dHNlWlR0R3pST1haeWpTdTFjMEJoZlNzSnl4UU8va3UxQnNKU1dZays5ZW01SUNsRHRaQ2cvU3VxWWdrTWxpbEhidzY3R2JrOUpJUHhGVDgxdytub2hGbHRTL0R1QnVqQnRNdDFUMCszMy9GRjdReVFJTmZHaGg4OG5LbHRlOTc1N3NTaFYxMklhRWY3Y1FlYWRWOSswTWJHU2VtSlhUczdOVkh1aTJKc3haNlBMVnpQNitJWXZ5T3NmcENZeDE2dU9ra3lvTU5GK2lUL0wwSysrMUs5NEtDdE9KU3lJWTZ6TTQyWWFNOVpDZXhVb0lPNVFXVTA1RTZFU3crYkJpS2prenVkWUdiQlNvNGZSa1VXZWN5QkpjZHo2Tnp6ckNsaWpSZDBtcG1pbUZqOXo2TlBSRUY2UHhqRzllNUdkRW5XYWtDTzVyWUY2RUJtMHR2SDczMk5NU3RLc1JDN3pLV1FCVWNlSHFrRERMUjd5ZjhlbmRQV1A5aFRBSG44Uy9uN1E3UGxrMFY5N0h0YXJUampES2lDR1V5a0gwbWVYK1ZVK0JkVWNkcnlpQ1FXZFh4Z3E5SXZZYW9qaDZpU0Noa0VsUU9hYmwyR2xIMTdCWmIrd2tSYW85T0pkYStEOFA0Zkp5UjluMndTajg1Sk9lelhMem5MYjl6b0t5RGQvTVZJaEhpZHY0amNFaW5tdExNamg3Rm5NeFFNNkRUdmFZdlduaG1reSsyall3R1VCbFIweFBnZWNyUGJHTjM4NkdnSGR3bi9CcE9raFE1MjFwcEordFlVK09QM2UvOVVSdnI2SS9MRHhPbDVscG5MQ0U0MzZFUS9CdTIwY2poSlRpSXNiZnN5VHU4RUdZM3UxQnZlYTFYbTZjanZZTVFOOFpTVG1ONVUxU0ZhbjE4YzIyMkVnblZHNitGVWUySHlBNk1kZ1dnSi9Tb090MHBHTnVpVXBSdXlnWjQ0TGlDVWs5L1FQNVhqU09lbG1Hc1VScGJ1RWZZOXEzWldhelNoZExjVEtIYit3cldKNTl6MGxGMEZ4bmxlVjlEWjJSNWlWNUo3OEpEaDFXWWRCY29lZTVvMTMrbmpjV3JvUnVzQmdqQ1FZMHFIRUJoSnphZkQ3c05OTVU4OTVNbDdlSGQ4djZuN2VnbVhQNVg1UysxN3ZWeGFrTzVET09XdEpXMXpPcC90Nkg3WU1QbWZNUWNOTENUSThKTkwveTNyWnBCZjF2aS90ZW5jVjVOWUZReEZwUXI1c1p0YXloMVU4NkVpSm96Y09pNFk4MFJQL0NjMXZDUjhxOTNuZ2dRWWNBRVBCVytjNnhMa2FySmgyQ2N4N2h4c3E4c09NZ0JYdHREL1hQcis4NUFHeHZZOElzUzlReVNxVDczZExab0F0dlFhYU5TS0hFUEdxL2ZNYXd2RkM3OVVHNVpiUGJaOVhNRnl0bklwS3ptOEdMdVNXNHVNQkh2aHVmSy9LWkpnYW1idVBteGQwa3ExUmxQUXBnUStPTDkvZ3Rad0c4bTk4VEhmamUvcHcrZ1JveCtHU2h0L2RxdFFxMjFybE5kOG10QTlqb3dmWGlXTG5idmV1VHQ4M3RrOElhRzNVNGZrOWRyTGJ0Z0tvVUZseklxb0JQZWpUaWVhdmdFcWdtOHBwaFFISytEYW9rQXNNcTNSSGMzZGFoa0pPSCtlWjZaTHlUMVJlQStta3FUeXBuVE80VWU5VmoyMHBWYlRQOXpVdVhlNTBDa1Z2dWhoYTM5dFcwY2hXbXVFNGZXc3F2NkZoa2R0aG1YU1lxYXFMaHlXNlJwc1E1aGZKUmNGaTFwVXhZcGJ6cVljM3BKbE9SR1F1RTdjZ2hLUzZRdEt3SG5kZlhjNk1yU0Z4anluaXZHTWhtcWswSmNkOWtVZlFvMEErZTBiMnBlN0hOZ252Tm1Ob3Zad3dPVUJkbEJ0bWpvelZHOE9UUTJZWWF5YnRPK242ZlRiUTVUWWxja0hyUmYycnAvb1ZiSEJhbHUxTGdIb1FrMXBMNWhGSWhYaVhnbEJ5NXRPczR1MExHWkhFcFNUUEllUWZPNWI5OHNJNWlHa1VKSEJXbTB1alRBVi9IRE92b1IwdTRXVERvVUx5OFk1WG9yb3ZaQlVaMTVLTnpoa2VjL2R2VllkUFc3Y1ZoNVRmV0N6ZkZvU0ZLNk52SS9aSUZLVHg2bzFTWlc4ZFUwZHFsbmVKTVVMcU5tSkxjVkVQTjBPL3AzMmhKV1E1TVFqaVFTalJZN1lLRytYdFFRNzNHMmF4ZmxmZWpZdFhGMFh4L2d2YjVpRnRIdmdpczRUQXBEQkdmM2ZzdDQ2TURiRUVYYUR6VnNOenFGaDVZZExsdXpUVk85U2Q1S0s1VU1vYm9ubi9uL2VPVzI2Tnc5VUJJVGR4TkE5NDF1Y21vYXhwb1QxRkhpOE9IODBpK2gwS2tSNFVqSTZpdjlvR0xNMTIzaVIwNW8xb2JhSXlVU3g5K3R5OVpWbm5jdVdmWWgrc2gyU25NeGZWaEVJZEd5OXo2b0FYSlBLOFlHVS9UU3BEb2dtSk9mZi9QSnFJQWtwNmlqZC9Xc2NnRW9vZXNPbmFMOVZXUUxOZklJN1Y5L1BLSG1GMXdwN0RRMGJFSk1qaVRGZ1lBZy9QZUNNQWxwZlVNeGZzRFNQdkYxRk9tZjkwTnRvczBlcmRVTWdnMHUwV2dVK1NvUkM3Q2dIcE5iRVluM2hqWE1SQmJMVXJ0d1huQzBOQk9CdncxQzdmSCtVV1NGT0oyamNhUGVHVEFJeE96WFFjcDFuNTFsT1JuTTlLUm9hbm5hWUpFcXFnYmtBdGNZdUZCbTd4OWZRL3VycHJMbHZ2b0Q2Q01ES2toVkN3TlRRbTNFdXdaY2RGV2ZXQ0o2djQ1RUVaTWZ5MFpUTHpxcVU5cUc3TnBLM1JPbGtmRUVxOG1wYWh5S0VLdVBrUEJTaExsMndac0d2WklHYWU5ampneFZZdnU4SUt1bWo0N29kRWNVUFpJeldTa2V6UWF4b2huSGNoWVpCczNpQlRXOWZQZE1wTHQxSmhzOFVpbkRzWjRqL1BIVUdGZE9NejJMaGExdENqRG9QTzV6b3htR3dIbVZiaTdnWCt0Q3hJYU5CU0xBelNpNHpPVXlTeE9lY09qc1pUT1pSM3RvU1FQZ1A5WUpsT2xiWlZCc0NFaEJJb1QzR1J2OGh3bHNYbDVqWG0xQmlIK3IvTDZwcFNTOElMeFZVTEdFU3lGKzVEQ2tvdmhld3N0cjQ3dDRQR25BS0tRVTJMK25DbTQ4cUdybGhaVXdJcmNXUWZsemZkREZaamhZclFBVzJiWmc0WWhoQ3hnUjBNY2d2a2dqQU5ZTjZQWjdNdnVRRm5aNElZWm5IZkF4Y09CODFCUmJoUDhaTVZibXA1QkE5NUx6enRwWTlMTjFUTkNGV2pFK1lUU1hhV25UU0pjUHorZm1wUGVGU004VFNUQ3lYRzl2dDl5K0l4eUVjUW9QWmpnZFpOY0hKN05RY3o4UmE1SWgvV1NhblhKYTZxOXYxRkwyVVJIRXZwUVJvVElwQlNPelMwSzRwTlNUaFg0THU1SzJXZjVJcUtjSkEyRGkzVnNVdmVwK0M0VWxsckFpZlZHOXdSUWZ6STg1aTZyYnBQbFQ0M0lIZFFiWFNJcmZvQ0JBenkrL0w1Y1JSa2ZDTWpRWW9NSnZBbU1TK0tMNzJTdDl1LzNZZWdDZUZnOWpPVFJHeDN6ODFGcUxhTWlqSHVOeWxwaUN0aWZNaXFHcm8vQUdQWFhueU9NRnZqSkFCTHRTK01sUkJtRWtISUtUeEV0Q0RPL2MvTDBRbmNxOGtlMzlCUXlBeU5XSUpzbW14cGliaURXL0dKOHdRQndIRTN3UTZVVkcvY2VXVUNELzVoWEcyMHN4SW5oN2pzK3d1K2xwMW5VekM5bm0raTVCMVNhdzg3TDAyUTM5c09iRE80dHlKVTlNTHQ4U2tDU3RQMGNZVmtIT1JaSUxOWW1GWVlNcFdxUEFETVg0WTRHODlFdFN2VW9mN0ZmOEVxUExYMThHYlNpVmJiUmhGQkZPMWNTWGxYSk82c0NyVHppYndCRG1OK05YQ1VDVFp0bE9XYmpmcUQ2SmNnM0M4aXhHTjluR3prcDAzbHhuWGI4anFtcVRTYTJRMnYvbkkzNE1JWkhsVmhyaEcrNmd1QVNZN0twZTJPMGdGVzhZNzVTVFB1OE12Y1RNM2xBSHV0WVFMdG5vN2FyOFlNZFJHeTVzd1l2cklHNmtxbjBudmdEdmM4QkFXclVaTzVOOTYwQlMxUFBmeUprZHRxUDNiemNyOUpNaUpoYk1Uc25OYUZZUXluQk5mNDNhMlRwTjh1bVdEaVZwbU1TdzhXRkl6NHBFTXJSbFhaTEFPU3ZldzBQemd0STJjVDV6dFA4NUViUFNvOUF1cGZyUUF6REFLVWJhWUNlTEQ4aG4yWWhQcGJXaXE2ZTBuR2pheVl0SDRROUNkNUZRY2oyRXRRcUxEMU5HTDZtN0tnS2hJOHNMZjU3T1QxQkV0c3gyK0NLMmlZY3FyNkJXYUVNMTkzTWE5V3JrNS9JUkhORExFK0E4MDJqTE8wQUZMUmVBU1ZZV0d5aytuYldoSlEvRFJpdWtUWXFCdllZdzEzUWxuOWtYUVgyczBxWllwU09xTm1ZNy9WaVRPbnluRXZ4NFhLYkMxbTllQk5GejliamdtSTVyNlhac3hoWHFxSk1JTzhhUEJNNzd2MERUVUFyQzBjalVINnBIc29kczlENlhFZ203OW9VY1ZPbEdMclppR3pRbXhEcUQzeTVMVW9Ec1NyOFlxV2V3ZmZpOWxFVDZsWDBhSFRWRGZXbjNnSThLeUJWRW5TU0x3SHQ2WWp5Z0twM3F4blBmNGZjb2hoR1dBL0FPcFJudEJidWJhcWxodjJ5SE5QdEE3OUdlR05INHBKYmtxc1VFeWdhdTd3Yk93UjBPTFV4TlZiZmdYOXVDV2xVa0EvY1ZQcmxVOHR0TTBzaDBuVFFvMENaR2FwcDZXQlErcjh4d1N3VkdHVTRXQVJoSE5pZWg5K25WUjNWL0tKS2lsQ3p0aGUrd0dzNjR5dDMxS3ZTK0pyd2JBbEt0VVM4WG94M09jeVJUZGI2MXM3a0ppYjJZVzI5MEJUelk5cCtxQmZFUk1iU2drT2JHS3BkTUduUmVmMWsrMCswNENpcVgrbGNIZlYvZEU2a09EUXZGL1JyQXpnVzVPUWJmTTB1WDZFdUc3YzFvT2M1UDJFMlUyOTJvN1cwb3FQTUpjTWE2S296cFdDZWxjVE9NM0dnVkF1NDNpVGpOS1RTNkRtOHdIbGJwdjRNY1gvckJsdFltVVprNlhNanRnYWpRQXcvMWgrZjRYazYwQ2N5c29ZaW5ZQ3M0c0xjT1p0VlFwRUY1RWdHeXVvZk8rN2locmR3YWVDR2FjTmpid2NNQzAyTm5lVDhNOW05cVd0WWhjKzNReEY1RThGbm8zcGJ5WHg5UmF0MmZ5eFpWaWM2aTZEczdXVGFIYnZ2VWZ0bW5oR0lIam1IUnRMYk9wYlo2TDM1cVlucU9STGhFbEg5Rk5JL1VOcVZqeUJsNWltSmQ0WXpvcW5PNzFkaGQvaWJKRy8wQTBRK3cyNTVyNitXeW1rVDByb3ZkeVhvTTF3ckRnVVR4NVZYSGVGK3MvcjNiY29SK3k5d3pOOUxQZ3VCa2dvV3d5S2FXcXJaVit1MVNLcEliVDFoWkRlcEU0SmZrbWFrMHBYVVhURnpwWjRZYzh0VGFXMlhVcXZraFFzQktYYTBBZmlJTHlCQ0c0ZGo2ZHNOdEw0RmtxQy9YZ3pBNTRESlFhelNlNE9UTnpCNGhJWVhwcS9KMVJsSjFmSW1yY3JLMWNUMWR6azFldVVoOU80ZVFEU1BIZWtzRkpkRGYzRUpDeEpzSmZIYUdOMWZTU2VOcEpvQlFra05GODVBRUs5WlJaWDFqUVBOeENyWFgyZW41aklhVlRpekY3bEJwSk1oQUVvTHZRbllnNXNPdFVtbzVUZm1wTkdXaUFDdmFwWmJ2c1ZDaWFWWTlWaWM4OGpPMzd1c1dCeTRaRnRwQzVrWnlac2l5QWxkRi9QL0J2bWtMdmRITWZHYksxMHpHZ2Fna2lJUWxlQ0lvVXhDYm9tbGZkU3ZTMERCRU81N0NvcVlhc3Bzam5xT0tFbmNOc2VjdzRtRU92WkpJOU5VbitRblVJVWxGcUFBNml1UlVxSEc1eEpZdlF6M3liT0w1eGxRTFdBYU9hV0N1YjE2TVRDUlBhOW5KcGxtbnhiVmxEMGpRMGozSFc2dmFGQ3Y4RmhaOERKd3E4MTZFckJ4THBaVDZIZ25PSWl5ZTlwU3NsbmpZbXdhdVlVdDlrNGRMZ1ZLSjhqRE8vSGMxcE1lMW1ocFZMeUNPZ2FwZ01oRzN1ZkcveXZoM0RYbVkzWU5sK3VmUHNmZkdXUVB5a0d4alNNcGJXaGQ0dXh4d095RWcvOE53bzVwMzlZWjUva3lXV0NPNFJ0Z21IdVg4bGlaRjcvR2FJK0VqWW9OOEpjRjJYSjI5Ym9nem52ZDU5aERpRmx5OHhJZUFEVG5DTkdkajBydUxKcGVuQ1d5R0hwY0E3SEhET0dHUFZ1bXVQSEEyVzg2VHhVYWtmdWtDYmxiUUpzUjl0OFduZzBmK0ExTzc1VWp4TitGSjZ3K1FndEdBd05zK3NhVzdKemNwdit2ckhSY3NYRUVHUDZkMEoyZ1FhS0g2T2hEcGU3QVF6bHJUUkxMWTZ2eEs4S0FXR21EL0VDNktYd3Jod0M5Szg5am9pZVJkTFduN2xaTUcxbm5oQTNtWWMveEt0eThqdytzdk1sMTFNdkdQaFd6T0lldEFuanV2aE00OTlOKzZxcWIyUUJBVWtUbjluZ1loUVE4cjhtS3dya0VBb3l5eXk4N1VYcHpKalZJUFVkelhkM0lUQ1BKOXU4c1RWajgvcDNhQXlNaEVXL3lsU1c5R2tOREdMc1JPQ2lOT3FvUjUwaEVncWtpbTl4Y3V5ZmNDRUpMak1mRkV1QkJCcGs3bW1IRXVHbW9mWXBaOFdSRVRNNXkvOWRsM1VTdjdKWDVYNWdXLzNJTmkrYmRReW1jWWtNVkN1SGU3QzBUeU0wdTVPcmRFeC8zQjF4cHEzVXk1NTBoVGJsMHl3cnI5RmVDTllTUnlMemZxR2pBNUpmQmh3dENTMnpOWnltS2ZURHpQcktPRk5aREJTVXo0bWpYNlVIWE5Weit4UitKMEw2Q1hnVFpTYjBwcUNDL2RXd2IwUVIrSU4rSUJOV0I1TjBML3JLZzZTU3JJR21zalI3TEozU3N1cmsvUFlDSmo2U0ZjZ0RmYm1hRW1Ld2tITE9wRU5VQ2x3aVpNSzdsUXZYWTdsNmpnNWFNNVhoR0FwWWZ0ZWFkUHhVMW50cmtpQmdTS1ZaTUJqTjArdWpVUisvWVZ0YTlsUWdINktPMm9YaUREb241THhjNHIyMGZTWk1wVmExekJETlJpNTFmZm5GTEhidXBWLy94Y0hUZjlrRHhLd2o3VUM0OXRPUFdHSlB4NEtsRGdNdFduSGpsWm95L1JMVGVNWXp5L0IySnJUWDBOb1pTZDJoenFGQVlzZzFRV1dDcERoQVo0UXhXS1JxdUdLRVdlby9NUGduQnA3UmxIVm1ZOXBiU2dkNlBTR3o5Vml1dkc2K3dSYTVDUllCWEdBVWoxVHAyOHBrSTl1QXhmZmszTVVUYkx4TW9TSk4xeUI1dFZuUkZwVzd2TVU4OUFTTFU1SWJVaVVjczhNOE1sOGVaTG5QdE1qSTRSWW1ZWmlYN1duN0hmWGN6aDF1RnNrbTdZTmpjOFExcGRMTGtoSThjSVpzUnZTVUtEeFVvMHhieDdoOXBMRlVvSkFqY3dsQllpNFBxNmVYb2ZTOE5sS21LbkNqNk0wbktVaElBTks5MUVoUWdJNUppRVhtcUl4aDdvaENjWEIveGp1OTZESEdrV3JiRHd1US9WeG80L3hZb0JoVjc0U1ZaMUFhblZFTEJIYkc3ZEZoNDBGSGJ4Ym9BUS9wYU5sb0tSWHcydUVhQWhKYktFWlVNSzR0b240a2ZkMWpWTVlHYklEc0ZtS1ZhQ2VwM3IzZUlXdXhiMG1ncHJtUVZNT1c4WE51eUE4Qk5pbEhKTGNjTnZDcmtocGVSU2t1dDA1NWlIcmhUSkV3RXMxbndtRG84K054MG9FK21UQlBHaVJuUlJiazhqNHZJQTNUWGs0cXJ6QTg3M3BoMHR3bk5UUU5ha1k1bWhFVkVuSEdiT3FtZm9qRm9JSEZNdHA4eUxROE5US2N5MDNWRU42c1h5aFh1TTlsTExMSTNreklJRmpmU2N2UW5TeldqOS9yZFB4ZnY2Z3ptblFTalJJNCtxYnBieE53c1lSMFFTRFFZc3liMUp1ZUt5MEFEQVBkRG5MWWQxbzFWcDBrVGFGWFFBZ1RYd0YxMnlhdjBwRjNBTmQ0YVFCL21KNE9FWWdMQ1ZiZC9EM1paT285U050TkNBa3gydzFWTWJCL09PbllzRmJsWnlBNUtqZ3I0eUtITVhaS1VmOGttZjNBK1l4NGxERDh0OWxnYmh4bUFWZ2xvbFFkTzFPWldqTEJWYko3alRIVFVpY2hFYTc1SHlhWXRjNDJqQlZiWTNwSW41akxPMTBEWGoyUUJwRWJkK1dDVjNGVmtuTFVpSUVMZVFGV1hoQWJNMHVUL2srZGt6bS93REkra05xVlZPVDAyd3dUT0lURnhLTWNUMDA2em5WOEdlTTViNndaQnVwcldXQm9yQ0gwRU96eEdYN0dRczc4NDNJRm0ySjJ6amZOZVZoNkhKVk5vZmM5TTdCeTRMUXBncVdSUE92d2h5dkNob21lSmZYNU5PdURqVkJjVWRTQ3AzMFJVVU1oTjZUeTVWRmloYkEzU1V1ZzBmVDVkNnM2TVdLMjZyMVVYeHl0T0tUV2oxNDdxeU0yOVNmT21HNEtDWHlVdWdFMU5Hd0tJNEtib09mRVJHTm5TUnkyNDVEdS84Y2xTVTBvbG1vMjFhT3NLWjg3L1kvK2MxUDR4bnQ5TWVQSUpIZUFkTU1PblhkNnNST1FXZmpyZE9NaXRPVzdMdkxjK1hYNXA5VDVYQm9DQ1o5Zlp5aFZUcHUrbmZaakpNbHlrY0Nid2N6N1g3ZklpL2l3K044SVl6TjJvUXFvdFdXek5mWGNaclZYV0paRTlXT2Nqc0t2YW9ReFg1cDdZY0t4dGpVVDVXdnJ1K0pkenJYbVBYZWppcDhTd2svVDVnS0RkSDBNMlJ1YnZ5MHZDWFFLLzQ5WDI4US9KTHZobGwrQURzUFNzUWVXMHVXamNHUXFzZXd5TXBZbnVKQ2lsSkFtL2NySGN1c1JKa3Urak1DU3dxeUJHQk9aL1BBQmk4R0FlbEVUK1VpMG4vaXc0aTF2WWFrNzR5SDc1eVVGUWNXci85MGFVK0tiMWZyY3o4Z1pjRjFlUkhwaWRkdlAwTDlkNVowc25BVVdMZHcyckRqT2VEMHM4MGxrN1h6Rm5WeG42UEh1Y0lwcWphQTdSa2h3cXFMUTVYajRaWUxHYzRLWGZEU2w5WVdRRHZOb0pXWHcxS2VNUlZsbHJ5dWJSbUdqcHVHSm5ncU0zdGYvVHluczdoUVpkSkIwMlZmNUJibTRqMHVzUHVSZVp2azFwNDJsSGVHQ1RJUmxVWnBEV08xanBEb2lRRWtteDkxT2VXNCt4M2t5VjcweTVhTTE1Wmp1RS9FbmRyVnZnZmtML3cvbVBlU2JVQ3B0UnpuQWhRdFVVRWtYaG8xMjNNbmNyalA2OEk1aXF6STFqQkNmNWZjZ1diUVpISktKZnQ1Q01DQlpCbFNFZDc1SVl4bktGRG9HWW92a2tLZFlHQ0Jlc3VmNkQ5NlhJRXd2cTFlR3BjMVRWRTAvaWp4UDdTZUlWemhTSUdVcXUrbWFkb0pJd0FKMmFydEQzR2FEN0Y4aGY5YzVxOFljWVpER3g5aXBGMnJTRXE2WTltbVdYRlRsRm14cVAyT0xmbk5odkdjQVlJUDk5WTlwVVRjeUp5cTRvdU91akVtS2hZWHQ2SnE0WlFRRk9meWU4YWlTUUhIMDJPMkRJanVzQ2RTRG1uM1JWUVVFNndPeDJRaFdSK25ycW45d2NRUm1seXM2YUtBKy85cFQxU2l0QlRXVkROUmNRdEkxM1ZQUmNvZVYySWVYWFhSZkVmTjdFay9HT0VYblAyMVV3MjNNWEQ5ZmkyVDVydXl0UnloNGVRc1V4T051MVc0dkVydnJ0ckRZQXREVzhpRmxOMGxVZllTdU51dmk3RktscGVOR2tlTTBpOFprQTRITnhUVGlZRHFPTEFhaGhMYTh1MVk5STEyUCtmNjBYWWhpaVZBcFJwNDMvOWpRN0FlcWNBd1lHY0x5cFBMcGYyOXE2SjR1REVEOHI3V2JvTkgxOGtkVGZiemtDa3loK1I0U3VrajRvb3k2eUdLN0Ztckg3NXNzQlI4STNzeDFkZUpXUnQvT25jSmFmOHBpK3lBZXVFdXQwVUN6VXk2YWZQM2pmemUzbUJDMDIzdDA1NFI2NG54S1ZYTXVLd01UUTNld2RXUGdtRU9SWHFmRVNVWFZwcnJFK0pMOWZQdStSMlBFamtBL2ZEbDRSOGI2enpZc3h3ZU5nQThmd3JEaS80YTNCQis2UnFCWkFYM0MzMXJYWnQ2cExESjROODI1bkg3dUI2VUJQZHQ0TzhwbEhYanorN1liR2tJSHBaUjRRZi9FR3ZMVVBjVHR5TDYyUnJwdkRaTEpsZmllWXJMM25pTFZTOTFUWCs2bHZ0VHlaR2ZJN0gzaGtCNlBzaldNeEZhcGk2NXFxYzJMVW5DemN2WFJCOXhjZTV0em5qZGRhM0hXTHA4bEFyblpucWl3bUp4Y0VXSytqd3Ywc2VLcWRCWXpvZVcvYmMzOW9LQkwydXB3MTYveTNEUjRZak5Td2s5MkRQU1Yxcmo1Y0RMcjRRb1dlNG1OVXRIbW5WdXFwOC9JcE52RjVYbk5DSGFFZndHOEE3OXJnMlJNdnhpYXRzMDJ5Vi90S21hTEVIclhMcFc4WnFTUGU3S0tNQ01OUmFHSmVqS0lERG5jOWFkNWlmRDc2N0czTWRXQVBCVzhxb2lPd2d0NVRiUlVLQnhmMUhVcTFtekdLc0V1Mk1NNGV3WmI4MzZvRU5yaHozRnlRWEJvYzJxc0tveDhERDBweFdVZHh0RHk5QVV3dE1ETDBSWDRtdnVDVlMvV2NUQmJ6Y3Fxd1orMjlWV3ZmaGRvVXFXWUpSSVNGQ3RJSG9aV0RKczFrYTV5dnZBVUJiRE4wdVVySkdUdmlvVkJxbjFVWkxDSFBKa1l0RkxQVklIU1lxcDQ4OGo5RC9JU1pDUzRRM241VXpaYXk2S3NKNjIvZ3I1WDhIcHcwSkFYd3lsclZkVGVvM0ZQUmd0eWtnWlpucGIyNGlMSkRndkxhQ1BMYXhXdXBPeHVqT0htNnhQSlhiL1hvazRqeWZtL05aekErRXJwK3FXVjlFRmdkSWdOVFp1S0NqVUd6UGc1Q0pmVmtvbDZjSkRoU200QUhidlVkVmtkRmcxVm5WNUJ3TlRmY1NhZEJxZVBMZlVUcXRoOEpRRmtoWTZ5VDh0ekZoTU12eUJWRndsYmlXQTJ5V21mTjlCanhYTHBTRmQ4Szg2THdUR1Eyd3JUUW1jUDRzRGZncHM5ZHNRUXh5ekpPWVk3UmV0SW5VZjExM3NkcktpZlBMY2xSRGZvY3RKUVpPMGZBWm1JME5tVm1SQjJYT0ZKU1pFQzhETC9GbXR6SHhXdis1dWpIcDBlVlovbGpGZHBobkxEOWpDWUFzVjZINWQyeVhVd2hsNkE5QzhhRWZGaDJmTC9kKzEwQ2RCWUNBSVNTZlloc0FrdU5GaHJwZ3p2cktFK0ZDVlRyVldSVGR2akdoeGd5RnhuUllNbnc0S2pPUGJUTlhJQmZ5aGNoWFRVR0xqc0dtY1QyY3dVSHdjVGNyVkR2WElLeG5oRFVSUHU4S2cveXE1d2x4ZHBjODVBR2tRWHJqazREZHozYlpkZG1DY2NNckJvUm83c3ZybmpiS0h2UnNLM2REVDhtSTNDTEJsL3h2YzFWcU5BTFBhbEVNN2htWmJIRXdMUDNGMXpKbUh3WkpYdXdWKzdxdXNzSGkxcUxHNXZ4VGgwQ3R5ZEh1aU1HaThIcldOK05hWWplM0ZaVDhZUjFuQ0pXL1BQMkgwTGt2THZvZjdqeU1kUllxUFNOQmp1WG5GZkRvZWJlRFFlL2NkckdiY1ZXZHFFd20rQ3Z0NzUvME53eTRSMDhYZnhFUkNpMS80ckUrZExnZkgzM0lvOFlIQjVKMXJXVlhmdkV3TFhzUXJCMm5NTGllYkc1Kzd1Y3VyZkZoR1UrSGpOUW10UmF1ejZXYTMzbGptRVQ3aW01VXJFeGxtVkxuNUZJdE9TNmttcGJiZDBZN3hWUXFqNnhkRFNYN2dEVjkxMEV1cXY0Z1Z4L1k0cDNZY0lJTjFIdkNmbTJYY3hMeGVLTnZ0NDVBc0tWRjZSTFdIT3lnNyttbVVzalVtZUlweHFySGdOcG9QblZ2bGFIcHlWU2k4VGR2M1Y2KzQ1SmZjanJUanFDUC9DcnJBMXdkMXdBNVhFYlc5cElRVGt3SjJQMWlSdHJYTlFyMG5hUHZhM20yZTlNdGpXOHpmdHllMkUrbWRxOTV1VU1QOHJreGQ4SkNlRVVzVjRCak5mVFcwaFRrRG01OE5RV3duUm41U0lTcEErOGZMK2JWcWNYWm8yaEhQbEdPREpGQ2dYZGhFUXJRL0pZTkI2MS84NFdTYWo5djIyYjJXTzBvK1pwMU51eWpHbUZoUklxTzJteXJFdUdLUGZJOVpYRDJ5SVc1Q283WWVYYXZycnFaTXRvaERSMjNLME1QWFNLaGZaNnFMY1FqM25yUm15VUt2aWZIcTFWdSs0RnI1TllYTXdHMjhQc3ZVMEpyamxMNE11ZmQ2MUpQQXVEOU9WQjBza3lzSGpHeE1sZ0pmaUtPcjdBYWNEOVc1a1NrU0g1cGZJcjYvTVNXb2V0MFY1b0FGNG4yWjdrMWlHU3ZsalBKTHJPV2FiVWx5ckJKZmhkSXNtNmFUUlA3dWE1eEpKS1NScWdUSTQzaE9yN1BmanY0dElpWUU3VTlxSVA3bzNhK25KaEZyOHhEUXpkeSt0OVg1Z3J1MkdGYldnb0Y5Qk0zZG5hRTdQSFhjVVBwTGNpZU9NVStvT21GbExubHZ6Vzd2Q2Z5NTlvYVRCcTJMN2w2Y29RT29mUXA3ZCs2L1JYWmowUXpDaUFhb1ZlVnV6NUdVdWUzaG52U1Bhblozd2NhUVJCejdSdjJJMVRMVjRKeWVoWHlWSEZoMFM5YVF5My9XUHpFblp4dm1mM1BqNm9uTHJuRDVuL3lmdGpCZ24raXJBSVQxY1o1MTdweXFaRzVrNUpXMzMwaXJpMzlGYUN5TGdMZ2FBanVacGdsYU9CN0wvUitoN3pyWWt6OUZRSVRzRXY4d0s5TkswMm9IZlpiRTF1cmxwbEl3VSs3QmkrRkRaMmVnbWp3anNNZ05VQUlQVU9xeTFISGozTkovWWVybldDd3lkVGJjWFFQdWtBWTFMaXhUV3BaNmNzdVBMajR2ajNHU2p5c3VLNHQxRUlvQ3NjRUE5bjlzYkp6UXhlcSttR0tyT05LQ3UzcE9Ta2VUK2l4TTd2bUxjR0JaQTYyZVd0T0xRWVNpa3lpcVJKcXVJK3RrWEc3eHdPbVJIN0FzQzF3ZW00K3B6anFGaDd4bzZpK0hiWDlDN0xWa0kvQmJNby93M2ZnamtVbmJIaWVjOW9MNk9lSHVEbHhaQ1ZPTVpOYzdBOG1SeFVnLytjWGpMdkVzeTBtVEpmNnczeEhFUlRVL0EvdnN3a3ZCTlIyQ29XK09Va3h3eUtRZUV3RzFhMkRjWXVnU0VRck5hUFRMSjZSMmF3b1NBNkZmTjh3OTcwQVVBMTZxOGZkemVqaWZqYXlqeWx4TjFHTWZvcUNvVzBvK0tEaXpZRStnVUF4a2dEWVNvaktreThneUtUNVF2ZzlsYjNtS3I2V1p2b1ZhSTNxY1NlamhXWjAyVXF3UlJKNGVNZXY1KzR2dk9VQ1Bqb3NTM1pRTE50VW9EWjlrd2xuODJDMkdqTjJwSS8xblcwSXcrd2pLbytvUnRRSFFiZlhmTExCQURLcUFnbXZUL3ZRN1FtZmFLYWpyL3ppZ3NXV0NWSE1WS3kxMVcrVkdyNlUzM3IwNFBGSktaUVJxMUhjUW8yZk9uTndNdks4eW5kSVJBTFdyMDFPa0p6Y05ic1lVamlvcjlUbzAyV2d0aktKQWxsQzk3UHRaN0ZleTFoS0l4MnZNZmJ1ZU1zTDQ3N0NaVzVzSzFVUlc5a1J0VjEwL1FNTzY3SEJQWDJ6bXJxcEVjc1o1eGE4SE9WcXprblFYSlJ5ZHFMNXIycmYrS00raGlldVNCeHVObW9OZE5ocWV4TStOK25xOE9PL3pQVWFPd1RaNURXNDhyY2s2M1pEQkNUZ0dtcUJDSGsvelZKYVl2YVp1Ukd0czU0U0ZlU2Q5cEppRCtCQXUyVUpYOURESFlUTXBsTkxuVmQzVXZvU2JBN0xLVWRPUVU1OGV5WVF1N0ZsNkJwK1dkV0VvNUtjU2RNQU14Y24xbi9MUTdPc2lEdjRUZzU5OEdmVGx1Z3JtRUdsQVRoTEpteEFlcVFQVnl5ajVRVE12L1JkWXc4RWJTTW83WXhUYkYwamI5WEg1S2FZclVUNkNodFQ2dytpSG02dDNnS2NGQXM3czRIYmhVQXZjb3lxWk5jRGpDWDNsRmFHc3ZGeFFZY3N0eEFhaldLNjlzTzFIYXNRYnNXV3NMTmxWUEM2S0tUM2k5bU00T1ZRNVpwWFQ3SWdnSTlGakZwbENoNHZNUzJMUmcxV24vRFRsMFBIUVExeXFxSTZkSWVmR2x0TGhoeGVFRnFlM1RFV3FZSW9wdWJTVUdnMXZxOVljWngvSDBmMUE0aTZjcXBJUmZTYWxBSzE4bE1VQ2pickR2aC9lWFFVZ0hoa2dOZTc5SjA4c0lkTENsSElkOFhQalpEL1ZkcXdlRVByZDhvcWc3dGtkZElROVBMN3pMNlVKOUlIRDU4NHZHY2ZDWU1HSHZPekp1MW01NWYweDFCL01FVHFDdnNIQnMvTy9rRXhPQVgzRHBMMGhYQm80YW8yQVNuNURtbXlSQ3VWR01QUEFjZEx0SldwdTc5MkVlTjJqcTZPSlB5K0g1MVFKZjgxd0REcjBQQ1BTeG5sam5qNld5NTVNUGR3MWdLc1E1S0VUZXI4NzBzV0htY1BwSUVmYUJlNUgrdzhrbGZTUVFwYmZjRFVObDI0c295bmRZaVpZRUMwK1hTVmJvNExoK0FCelpzSGVSNXpuN3FRM2pEZlpGQkJxTjNwTDMreFlPV3AxZEN5QkgyUEh0MXNBMzZWSnVvTC9oa1lLRm5qaG5UdjBlY2lyK3hrK2pTelg2REtLVmNxT01ZN1VrZ3Y0NjVmNWcvT0RlOFlkT1djaW5qUVBJMkJMRk1DblJQNWxpVGRvUGxZODlueUh0Vi8wQnhjZS9OWEJ6dVB6M2UyRUs1TDJmNHh4a2ZsSmticzdjTHMzS2xicy9MbGRGYkxQeWVnM2NmRE01eWhKSHA5c052VVIxWVRaUjJLaFJFV042MElQdEQ0a3Jid3B1QXVTYkxwZlpJdEZmK25ndGRrNzRhR0F1Z2Z4eU9uK0dubWF1bXZnTCtNYXZCSXBmYllHUUJuTnlGV3lUV1E4dFU2ektTZ1F6cTViUDN3bXFvMmFkendabUxiMEFtSEMySHpycmwrZGFEbDFsNTlCSE5iSUNRRm8rQmVVQ1ZrYllOcTg0Y3diZGZUdktPTCs5YU4yMW5qYUUyekY4YXlqb0ZRVkd5YXh2RmJ1dUdRUXJnUGI0bUZPZGVsUjdON3FIQmRnUjV1MTJ1eHJ6TDJrUTdJdU1tT0pjL2U4Y29kdXoyWmJzMTBTQXVLSC9ad3pRdGR2eitvYkJ4b0tmbDgrTGtHcFJscmczL2tFTkpKcXZIMnF1eFo1TGtDMnE2aDBqZ2R5RE1NMktxQVV2dnQ0ZE45a09sTVFvRUdPV3d4MG1JOWtOL21mRCtwY0JORTJWUGM3ejI1Y2dMMGl1OENhUldTMkQycmppWm10eGlhc0d0bC9HbnQxK0cvNlNvcFhSRWExL0FUbVRVTVVGTEI0TGhKMTl5bFRJZFhCaHhacFdFVjdqVHRMSEk3Q003K0VUTUc0cXc1NUZOSlJuWHVyZzJOeFlWTDFteFVBUkpCMnpobHl4UWg2Q1RLZU9wdVkxWkVVUjFSKzl3TWRFWFowZjk1UU9QOXZkZDVkb1JFa3NKODJNTUdaSTFRWnNjRjhCSFBqYkFGVmRQcDhQZmFjRUM4aTYrcUZDa2dVS1pNWTJRenpvbU11MnlzVFJENmhmUXFTNzBtWDd1Z1BBUnNIRjc2UVczR0kveDhnUGcycFQvVk9UN1hXbjZEdlpMS21qK2k1OW0wam4wUjRBcGluTlp6ZEMzRnhvME9lTWJXNE1kRXdLY1pqYVRUaEVuOTFyRmpZVllnUk1OV3JUQ0dLTkNyWWkxNEZvcDJBbU55Wk1xT3lqM2ZnMGZEdStEa3B4STl4Q3BPdjlmUnRaUFRCeEN2WEJkZnp5S0wxY3IrOEc0eGNtOW9tYitBSnRLeDFXaXZacnVTNERWdU8rb0ZVaE1Xbklxank0RlhyNTJnV2ZIMFNWVytoK2JHNW5FQkJxRFc0V0tTVWVFWVladmtEb1FJM0I2ZEplYWlqampEUlBROWIxNzBTSGlYVzMwa1pLN0dja1JTa0h5KzB3WHpudkN5QmF1TFNzVzNOTGk5UnJYMUtoUFVLVXBuSEUxelBCbmNiNDJzN016VUxiZlpSMmtOSFRMM0R3cTNWT3J4andWUStsNnYzOHVjR2tOT09UNjZYaFdhNHFrblBoUnFNc1k2M1dNanJYMG9xOVdKV0Z5NVNlRk8yOWU0RjVsMkc4amkzVkwvak8vOWpXNEQ3QmtTK2tTa0p5RWRkaE9wZm1hNXpiZGtUSTlYZTRkYTRucGQvSlZQZXVPZTlsSHpjdnVFZFQ1cU5ISTZabmZSbURYK1MvbWFWV3BaYW5OR2lrRC9Wb0Z6dW1xRFJZcXhRMXRMYWxLVHZoa25vT1U0d1RLOThQNXc4bUZGOXNJekVTMjBMR2NuWVFSSDJsY0picHpISGtZeVRFUFo2dlFqTXloeUg0TmVhQU9Sem9kSkV5QkdNS0VUWnhXemFZYlR4TW9GSElCQ25sa3N5RmtnYmZJNjB0anRuZXRHc3NKQWlDRUFYL1pwd1ZGOFFnZjZNMzRjSHdLYTRFWUd4ZCtJazdRbWtRdXkxd3VkRXMyMW81QXBveGRwbVJVQ253RFl1OHBOaWZBMExvV0o5ek1FWVRzU0JBdDQzWVFtOGRPVkNzazc3cEFSN2pCeVZiVmFvS3NRZnlpbHIyS2hIRFNvaS9aRUlyRXlEMDllRml3eEVDYzFieEpNdnlHaXh1d2l4azQ3SGxTK3d6Zk5tQnA1UGVrUHNXWmltUXRzVVR1RGZHYy93eUt3ODYyMEg1L1c5SzZ6SFFtRU1xd0U4REMvejFpOENPcmZDM2tyMUFJTFh1eHM5bGZSa2piUDZMOGZNMEVpbk1uOWw3aHYwek1lNFBiVSttcFpocGdFU1ZRc2xxZmg2NTE1c2MwQkUwOFhxS0pQeTJYc2MyZW1FdENENzczcmw4SVhrWm1QcklsWThiZ0ZqeXNxMXZ2OUtGUUZ2a1VXUmdFc1NzNVBDdTBsRWZvR1lNTGNyU0h1MitCdE52cmQ2YmpSVm9uTURJWXpVTVhOT3RmYjdMRWMwWk5PWStVY0JCeVBjQllYMHZjaDlIV3RCMWdTYzNvQjNIYXY0OE5sY2Q2ZlpBa01YVjVhWnd1WWwweERLeElhL3ZzSzM5RS9OQzFydGVML0VselAyaDNpZTJ2alFsV1psUGJSZEVnLzl0VjdFNVB0OXlYOHVqaHNJTzJSam9MNUh4OFF6cE40cEdXczY4cUMwR1VoaElrQUx6QzlLN3JNa3I0enFYUDlCdG90Y1RzWDM2emtWNlNUbG1NQ3cxeThOYks5a1MwY0lYcjZEREVDU2RFMXBFOElqSG1McTFVUno1bGZ4WEVSejhzSzBzeGo4SURFMnBpUmR3MlFVWWhSS2JybEhMT29TM2ZMbVFhVkhsUFl4eDZQaUNaQTBjWDBKa3dCZWcxMGpiNFc1VW80ZzlpdERiVmpjZis3dWtKbW93ejM0Z1lrMEszbjB2YmI2amg1dHdtVWl0bWVlVElXM3N4RjZyYktqcTVrVVR6NVpESHB5dzZzM3dKcGtscmJCVkRPOTVmV0lWWWRiWVRtMEVuUGV1N1ZacmtiS05kKzVsTklhUStsaHZKd0xCdWdIMldCSjY1dld0bTBOM242cGZVUkt0T2s2OGlmSlZCT3Zod0ZxWGNIRjVid25HeC95bzBBSlIvMzMvYWFEY3NzMGdwWVRUazEyd0JLT2wzV2VVZDc2dFJ3UlJQRGx6KytJcjVzTm5BUm5DUnJXQmpLVEx1c1N0Ny9ITDZXNGtJNmxHaFJjSzFqTGtPUXlVR2U5VnhVeFlwVkdTWDlCTVkyUnIrTWUwR3JpTXFxVXlsbUxUTmViY1dVZ0lhWXJsQmpHa05vUkJYUmQyNk5MV1BiYTk4NlFYaHFyVmNreTRPcm1IcW9DTjgzMlhnSnYrb1JTakc3bHYreHA0aE1hU3AxL0loaUgreU5Cb3pQaW1wVHlUMGJwcW9Oc2wyMy9MU215bTZINDloMzlHM0wyeWFoUDduMGl2dDhpN096Qnhhd09jV2lmcVVmWVdjZG4zUkdNV1kweWsvcmNBdFhQUWZYMEVlbHk1bitrNEFkMnBiWGFESldubENTQkZwaXIxVUt4V2RhbVlsdjQxR3RTZUF0Qk8rdGRmSzFNOUtpWHFEWVorVnVYU3RSaG1QeDV0dU4wQTZDYllkQlhyM2puL3ZXbUVWT1VPTitzU1hJVWVoMXZiWUwzWlQwNlZiQ2drdGQ0K1ZMMkZFV1g4RnhoZkVzZmpKRjk4enhhNW9GaEc3RUF5OWF1ZDJabEU0bU5vc1c4UlIycjhHam9ZVnFxdnZIOUxBeWJwQndodEJDRXRrTDFKd2o0SzRwbEo4bkhHcm12MzVXZVRRTU4rNXJsYTVvNGtBRVpDeXRoL2NBaThWMlk5WTJKWWpVVTNnbXV5bUdlcUFnUk4wbzJDMDFCME9YeDlRUm5lSk8wNG1VM2x0TDU5aFIwTzB6aHhxeUgrNkhnb3picXhwZGtZRFAyUXFtVGNncXNWQ0FsSXI2K0w3THZ5MEVkdW1hdDR3L045YmQzTi8wOVNyY1hMajVjWEpOMFRrT2JpVXp3QzJwYkZORFc2aVJSVnFkRndmb2xVWi9OdGo1NWR2Mld6VXlOQ2RqQTQ4M0NNdEtzMFNFNnZ4MndIRU9LZnVRNmt2aEFBU1E5S0ZqcTBnTHc3WCtkV0tGUXhmUlIvcnlSQnhpUmJrRzVndGNuUnpnRFF0bDBkb1JGOHFObzVHS3FXMnZhcFFMWmRkaGZxUHFEQkFUSlJwejVINnRvbHNzVEpuM0gvUHUxWEdIY1JQZEVuSUpZK2c2VjN2SmZKb0VkYW8zWWp2b0t4MUptWGNJWm9FZCtEM2dqUTJGTDN5VjgxeXIwQzA1Mms4ZGtNT1ZaOVF1RXI4Y1lNSm5YSmZCSDhpUmVYZFphRlhzRERVVmdsUkNYRk15bTVWVkRMQjZTOUdIMDRlTHhsOExqeFpkZkdNSjkyZy9KM0Raa1p6akRaS3ROK0lRUk4ycW1TcllLdlV0MDQ4L3RmU3NqR2ZYZ3N3OExjUXhqSzNjSHBsUExHT0xpTXJxRTQ5b0xJMTU4b29rWG0vVWh3c1VqaUZWQVNVRG9PUkpnWnUwc25lYXU5Z2I3bWJteFA5bE9LaVp2RkxFSVV2bjNraW5YbjJrOUxIVmdqNnBiRnlLYm1CbWZLRkViZlhUMDBRcG9YbzYxcHJEcUkrVEFuYVIzQjc1YnU5S3c4cyt6RDg3Q0JsY0Y3Nk90WXBvMU5mNWR3UzNXMlAxQyswdGlwaENnU0dwaWFSTVFZenk3OUpYL1k5SnY4bkhCNEt1SEs3MHN3MThNL00rVE5UQlhKbUNGL012ZlBIN05jS0hKSU5ER2krZ1cwSnMzOGc3TEJDSW1QUEpJTnFlMkVpak5IN2VsV05FZFRjMVIwanFVQVV4Tm5GakhDQ2hrTVFPalNtRGZJNWZieVpWcjlPeW1CeHp0RTlmazVBL1lSZ0VMaHhEa3E2WU56aVlLVnEvY1dSaGUyd2ZKY25HZjdQeEVMOXJyVkhiRXhBMnhjTmx1RDJheElqbng4TWR6c0poeUZTbXBtdDA4SGJZV2tTa0hWaWxiY0hqa3VQNnZLUlY4VDJJTG1iRVQ5KzdrZ3ltMUV0Q1B4T1ZSNWt4dmlJcHJJaTAxYlZLRStBSVU2dkFpKzcvbVZWOGhZWC8wRWdLdjQ3RWJzVHFBT0w1M3I3S2xoWlBLMzBlNndBLzFmOFowVlFDODJlWExnczVRdlJxMWRNMHJyS3lIczdiVGw0QmV2UkY3OURyMVBhUjI2dURxelk4Z0xBaGZjcUY5VHliek8wV2NSbHg2d01qY0MxekFld0MyZHliRHpWN1E4SE1DV3g1Y3l4MEdBWXEyWVM4Z3ZPRFdaajFrWkZPdGxNT1VGMXA4UWVqcERaUjFHL2pHNXB5cHltNXpoaDhYWDRKbjlEK0dKaUhWMk1sWUJNL1Z3Q0NoZGpVQ1g0a0NtNS9TeWpFWHJyRFVPWnVEMGo2eVNEb0ZUa1FscG1KNXpOL0lQQkFCekZseXJMWmxpdG5WQTJNd2NzaVUyVzNzOHRPb3hTZ1grcjdGL0srbXEvalgzMndwbTE1eFlEeXpyUG9wTWx2QXVJWk1hbk1QTlUwWFhmamRrUjlYeDVsUkFJWDF3eVpzTno4bGpEVm1YUExOeEJUaU9hMXB2MWJiSXl6OXB6c3QyVnB1cjY1NHZ3MCtRSVFxRDFPWjdjSWpmdVBON3ZSd3MvN3hXcUVNMmVibE9sNGVHK2Jnci9nVTE1NVJrdUgzNjZpdE5Zd0laV2QrYVhzR2VXamszRng3U0IyWXROa09NbjBMNGNtNkVGTFlDQUVEWm5SSFFCdWhxUGxKdXh1cXI5UkYydERRTjg1NGpEMERvdWN0MTlCSVMzWi9lc2pOWE9mY0d0MzhkaGNjcU1Ccmt5WDZNbmJiS1RzdDhYajhjbTQ5OTRnSzNzOU1HVWErNlk4VzhzSWRkOVk2MVRXTklXbTNIZFM3dmEwTG1TZWFLRDFMNGMxZWJGNklzVzZlSHVNekdEM2diYW41Um9NaCtkSHZ1bVpySFdCeVVuVXdNTmZPV0pOaGk1bnJHclpFalEvSjFReFZoUTNsdE9DME9rTm1tZnN4d3lidXRRdjVlMTBMa2poN1RTZEw3TlJBZ0VqSWJOV2xaYklIVk5CTk4wSzlEOVpnbzc5T21HMzlOUlQ4NVhpSCt5eGlCOWcwcHBlVkRIblVtVFV6WEk0SjU2L3BIN0VqWW1jUGJzNmxqTmVHQ3crc3JTRWgrWER6RHhIb3dRUUtncjFVeWlnaGhWYitqWHZCMGlDNDJPalJDOEJpRzdSdE9CZWp2RjFUWDJEaE93RktLWDFZMy9iZ3JiVWpaeXBBeFcvRFBuRmNGODdWMzB4TlgxbkREWjc4VlRrOTFQTFIzMnM3TU50dVY2YWl3U05WYTdqWDNxNlgyNnFpejhBbUlteVVJK0xMeURod0JlS3IvelYzMUROb3FEVDMxaVllV05WOWVmMDUvcHNOK2xiUEpZd1FjWlRxWHFueG5Nd3didjN4Z0tZVnJJcGdkRlY2MVVhZ2psWjJicjFtMFpKOWpFaW9paDhHZkovRDBjZFRqd1ZhVmhmUnhVMnkxK2duMmZPajNMQmVUdGw4SXpldnpWRGdGNUNJeGdidThVWlREa3VDdjk2aVZQUzdRNm10NysycWo5ekhzOE5YR2plVXZlS25KWHg2VEd1VjVycmM3SEdOZ2dGYVVDNUFUT3NrOU5xaE1zMzNiU2xTWE9YOWRDMm1ZRlRCMkkxQnJRYk5mUityQzM0cDVQSjZjc204RVZCcmptMThUTkpvbmw3aEVGdUhnM2JDMG1XZlRIZmlwaTh5aWJiajdIdEdaR01ZVmpFWlJQazNOZkc0S2R1ZDlEalIxRjRlUWsxL0NFci9oU0tTcyt4TE13SndVZGRKRjI3d1p1cnBvcmpkQmpvaUxBTzl5NzVZdWxZSFIvNU5mb2REV01RVnNPeUwrZU5JNUtvYys1dFMvUDlWcEpLbStpMm5saVg2NEpYQWVrM0thanM2R2hKdmdpdENNaHc5cmw5NVNjU2g4WDJ2cXZPMWlwUmx3RlgyM2laUTRnZjY0ZGEvQnY2NTI2MmVNdVU1MmVHMnQvdXlsREFKd3RzN2pCWHUxWjhCb0w1SHlTNHgyQlBMNHptSDJpelBuWTMwQ3RSbi9BSFY4alJScFAwbXBKdkJvNmg3Uy9sWWJoZDBESGh3a09wQVVmUFlFOWlPK1lra1R6Tk5UZGVYQ1Y4TW9Zdmc1bkpIQnNPS0tiVFJucDVnUk5DeGp0YXQ0UGRvcHlUeDJwbDdCU0V0d0pkWXUzUlFjc0p2L2phUngySEEzKzBCbDJxK1lKSkkyRlVwWU9kcFNHNExYalpsejUzSkh3WVJVUHhFY3hFdmRNaUh0THBuYyt2VFFIek5pdkFNNWhlSS9ueE9MejZJTWcxUVU3d3Riajk3U1M3QWZxdmlKL2MvZEpMRmVPa2tFQ1BLZmdZWWFmelhtTHQwdXBlVGVwZUVsNkFXcDAvOHlidXFXVXdUVlV4bHNaNGZ1Ym94MjJpOS9KbXhMQndEa1ZlVE9wOThNdG9sbm1HMXhYNjE0UFRZYTRtVlozbEZ3bWRaeDU1MjNJeXZmdldiQlFjbzR4NVIvVmozcHpGeDMrcGJHWEl3MmsrUC9jYk5qNTg5VjI4am9ISlVxZ2hyemYxbUQvVmtuTFBNSGFnbzM2TXZldXlzdFUzMkIzZGkwcFRGTEYzK2Z6V1FvQ3gycFFsbWVSSmVNSy9aalVCOEFHQnFYOEcrRWt3MnM4dFQxUFNSVWoybVJnYnd4QUNNc0Fkd2JHTjRyWVVqMTNXVDlmdEgxbkhxT2NUTitZTHZibVZaSTZhTUhtNWVyQjNpMXpISDU4ZDRPLzZhaU9sUFUvVGRaVSswc0I4K3E5d1lZTm5nRThwQW9xYXhmZ3B4TUxJZWovY1QyODFqbzlXMlcrd25LT1N6Mkhsa205TTFoR1Nyck81MXo2TnJnNHYrank5b0UxeDZLWURuU3RmeWZnUXo1cFdud1I1bDdIRlM0Y3VaSkplLzBMVklnNFVkdGt2dEtuUmhsaWVVYmw4eGVYRlJwWmlQK0RNK3NrNFFmem9oaVJ2RVhqS1VwQzVjVCtPV2tRYmlQbVk4UkkxV3RHaDdJYjFTRWsxNk5SOTdVU0tSQ21sd1JHcUdWazRFWFVXMVRmWkl2VkVacG5EZUdYUm1YQVp3Y2p2OTdzaU9QNHlvMnd6bTZGdXhpb3pZRGV5NTdPT0pvK0xQOTdzenpyaTlaK3ZibnhENEczbWpDNEdwcWVET1d5WXJDd0NVVzhod2M3RDAwOStqNXYyR3BQR244UytoUjZqL2ROK1h3SzBKR1hJMmtlcmdEdzJBSU9STURBOXpCTVRtbFQrd1oxVWNLZFNsR0l6anJiY3dFZEI3eDQ2QkdPbUg5ejJOKyttL1hka2JHV2hVS2xFUUgwa1R4OEpKei9hNWpBMjlZZjJwS0l5a09EVVBEekUrRUViTkdkNitLTndsM3B5cTVjWFRhZTR1cHc5Q1ljMzNCckVjVSs0YUVrczVWdEowbEJCQTcrZTVEMzdHeHdoVkR6UnliaXpwYW5wVWIyeDdLQ3A1SnhESnZzSjZ5RGdlT2Q5dzlVVHY3WnJ2Sm9GdTBOMlA3VUhvZlRBb2NCNVJaSnU0MXA2NzUrSWl6S3ZoeDdVMStaUHZ2dmtJS3ZFVjBGa3JzQ3RLZnJpa0ZkZzdYK3JKYWlFN2I3ZXBpb1JneGZtY3lDcktnNytRckxJdFRyckRvMlM3bUpKRVZUdGVsSkVQWUJyUmVCVytSdlRmWEF2WGVUWmlwY3NuNjJTUlJITStrTXN3MWhFbktVdkNyZ0tRSWN6VU9qYzI3UGZMc1R4amNVTTVFS2NzYzRkYnZpSWM5dnMrWUk1V00yN1hIa0ZZODZXYWluR1BCcGJrMjlnaEdlKy9vRlNGNk1FWTVXTXlFemtGMWJSb1RyTFBHcFpyc3EwZkI1MDlEeGMveFA4QkpVSDZkZXROOTVhQVFjSXh3RW9aTmRyRmNTV1ZmSWk1R0dTU245RHR2V3ozVlZGaVVBUFZYMU1TcnNpUXBMK3lpWWtFZlRFMDFNYk9Hb3RSVUhuYUxhQzZxdnAxTXBtNzhMalJmTmQyN1ZreFY5cVIxdkdTSGZ2RFNUVE5QVEhOb2p5Z09ZOVdUUUJIbEZ6ZUxWZEIwZ3B3THMwU0dyMEU1Wm1zbWZqMHVxR1U1T0xLMDYyT2JSR1I5anFzY0Rjd2x2TEVRVFE5a0tBOFJuSU5Ocm9RRkQxbS9Da3FoZWo5cXc1T0lkOUY5QVlzamRYWXdPWTJQV0trYS85OWk5YUJQVlZZdHM2aUxzajhkUWVSUDVxY3BXRmF4WFN2ZDJBRGtyMFVHRkxzTzBJaTltaytkQ2ozdExoRWJ2WTNqelM4emsrL1ppSjVha3IwTlFsVTB5UFprdUFlOXZtYUlTRkd2VmhyTU5mdFIyUnAzK3VEQkdqZHVDMXE3OGJXMGU5VWZ4aUk4TVRwWWdlbnBIdFJsOXBFTEg2WnFwM0xDQmg2Q29hcmlpbHJSblNuY1Fld1QzbFgwa2l4QzJlNDN3dlV5YVA0c0hqV2VHS0NTekxKNlgveEduOTIwT294MTh5RTY1NERTRDl1TFVoQUltaU9CZVNxaWpKQXhBZ0I2UXRZRndqVGxCV1hVUG9xbXVyNjd6aEdKZm1Pb3pyWC9YVnpvaXBtZHRFQzlPMXVRRGdFRFFnZ0VPNkppanhXWXI2NHpyV05yV1VGUHBVeXVIRThKSFpMS2JBL0h5VGdaOU0xdG9zS3BQczRndjhpeDgxT2RUV2hCRGRiWUZFbnREd0pGZnNxbUtDU3lnd2JpSDNEdlVlYm9WOW16dTh4WDdQa3ZOb1psTEJWWlRLcnR0bVUzQjF1aVE5V3VuMnMrUU1lRDV6TkdMVmRQZDV6RGVUSjVEekZabkQ5Q0ozVXpiRklSMDg4Y2d4OEF3b0FMdTh2UHRMRmNaOE5WNk41WktLRFlKaEhHMlV4ODQyK203VzZnTWp0QzludXBsODRHTnZueVlHWVNzdG1YNkduSGJyNVRjakx0N2swaVRSeUJRRWwyNkRhdzY2TmZVYTFxbmJqVVdUNkthRUkyUzlrcG52dVBDLzBpYUxqWTJqY1NyQTF1RjU1WjFaTWpyRE80dHBMdjh2VHo2WHRlWmpSUzlLbXhKMjdpdEFMOGZOb0dSdmI0N0FjdnByV0Z5MllkTnNPdVl3eUU5V25hSHJ4Q3NvVlFoUGFCY1pCcHpiL2dpdSs3UUdBcy9BdUkvTFVpY2Z1cjFkRFVlUFBTdHJTb09ybFVFcXRqM1c4QnZaZDNBZ29iTTl3d1ZTY3BXRCt2UFdZaFgrMTFLYjhtYTRZQ3BOMjVJc2Y0a3hoaVZMYVNqTVlQYjlxRHgvczFZbU9JMjJNc0tlVlo2T2N0cTd4TFdwWmoweDhsK2V3MFRPeHd2bml2czk3L0VBR1VKTmZRUytFVFU2a05VMkwzVURwZENDT0JCWGdnMkZyL2t6TUZ6TytBbmw5L1VGUkRzb3Jyc3hxS0JQUWR0WGVZNUpCbnlMaWFBekVKTUpiVTZGd0dJR0ZuUkVtRFAxZVgyand4dTdQS0pJbXBRMWF1ZFFCTnVPNXhkYlhuK0xDV3ExSXV4Ui8rc05rRXJFTEE3ajkvQ0pYZVE4ZEljRVVPL1ZVTC9TOGdYeDdnS0pwQlRnZVFTcW8waGJxM01Wc1cxVFhrRzFiWC9wRjk3d3B6SjBrUytibUhKYWlRcDZyQzg2S0Npa2RtdWlPRDdGU1l1NUdPS0p1YmY4VXltNEpMK0QrY0FJRjduWk81OG5hQ0VUL1hqdk5PRkdWdk9VUjZaZlVvbjlaUzczdDVPRFlsVGRNRnRkWkVDNmd6UlJkRTc4UTZXWGFCQktNOTFoOEx6blU2S0tnWk9hc2tuR1NFc0F2ZjFpUy9YMGcxZ1dOcndPMm1tRTJnRkN0cHAvVnZxYm1WdTFzdVhSR1UwUXhKN3p5WlZtQ1cyNnIxY01XeFk3SXJISzlUZVl2WTg5ek1iY1hUVzNVbEFtajlkRTBPVytuMDhvMklPdTlKNmYzYXFIdlBzRHpSSmhIekdPWFdUS0RZOWpwS3pOVDd4aHhDODdoU2I3MnNJT2s0bGVERTIyY0lUQUlMMGlMMnhDUENaRlgzbitCSEwrZHoxWnhkekF0N1Jkby9Ea2JnaUxiVnM4WnhiczlTdEpReUIxUG45ZHA4aE9rVGxXeG02cTlGUHZmSkthTk9zbHVFSGdTbjZLcnhEbnNsUmVyRmNkWFRzQmdwSFpUTjJ3anBIbzAzNk1wRVFkbUQ4N3drc1B4dVBqN3VKR1VKM1N0WVNOUk1FdERpOElITkp4aVRucGlSb0Z1Slh5d2c2WW1Ga2VsMTNIOGcvVTFBS2VGYTBoV3hPUFhhWi85WTBBN3JhaUdSWTd2dXFlaWhVTkZmbDB2OCsxODBtLzdvS09uM2VSZm1kdVlwVnlpL3FkUkJKZzI4Y2RpWlBrZFMyQmRtRUl4QWtGWUdWdVlpUUJhcDl5c2ZjZ3JsbUFrRTVHVzg5VDJ0d0hzM3ZVdjljNW8vMHk5NTJCZnlDa2hKeERLeSt6Z01wMjN6UHkrUE1yamlFZ2U4UTFGcmtBbm1naEUraFJ2UDRZSm1XbjRqdCt3MUdicklWQzNFYUxHaHhZOVVVWEh5elplUC9QTkdzYzV0WHBhelpSaVdWM0NoV0hxVEp3akU2bmxBYXkzTkRSRG5PcWQ5Z2RVRWtiQWcwREtoTWVMS21BSWhWM3A3M3F2S2pxWGlUdFVBZFpBVEpEOHNNeDJuN0xNTGY2WFc3VVl4WXA4ZEJPMG9BYXM5d1NJVW5SMEJnWmp3bWVxa1dhQ0I0Z2FYRkxCZEwvbGo1Wk0yQ3dja29xRUo4c1RwRlVTVkEyVm9rYklMeE1tMFVxc0lpUzNKNmEzWEM1OHdSajVnZWF3bS9Ka3Q0UTl6T2ViSlRMdmFsYjJJNFY0NUhxbVZnK2txMVhGTjltMEl5TFk1eVV1NG1TbjBhUlFXKzZ3YTJ5eCtacjVpN1VhMG9UNXZRR0NsODQrcXFHamFCZW5ZMUhTNi9ROGtGWmdjMEJ6NUhzVkVjM1ZHRm4wVDM3ZHBOWFNmaFg0MjkxRDhnZ2NxWkdDaEJkYURCZW94cDBqTFE2bHB4ZVpJY1psaXF6VmJZc0NVaDEzMGdmL09EblJsYnJDbFZKb1FNeklqcVA3SnliQXVQV091QU1KNXVpVmM2ektDdHk4WGZVbW5DMklNUkJsM3p0Rjg2cGRGWm02SGF4QzRNMjFrVXR2NDVNRHA3TFhMOXZHMkZlZ0VGUGtzSXJ2SjJ3dVJxTG1BVFYxTWdlOFdEY0NrRS85Wjl2L2Fna3prSzJBMGtLRHN3bFhqUDdUbWJwUndHS0lGY0o3MFVEMm0reTgwQXJiY1Nyd0NjaHJjeDVXcUFSTUU2UGJvS0pmSXBhOUdjaE9kWWJMbVp6MGdUZHVQbUFKemYxYVJsdUVRU3RzemFyQU0rdTN1Z1duQW1zVWtkdnZVY2dHZWNXZ3JqNUE0T3Q5bFl1Ymp2M2pFc0RBTi9Qb3lvc1V6cm9xYitPT3UyWlF6YzlvRHMvZVlDdGF1aldjZFV4SzU0L3JBQmd3cExBTnZmOG52bzFTUTRUZGhnZ3BvYUJMUE96WENaREl0c2cxSkQwWmR1cjdzRFJmeWJKdkVGWlFUa1ZsK0t3SWZtMk5wOHJMOEdMM2t6ekw0KzdENjBZUVYyZ2c5Y0sydklDaGQxRzMrSDB2TXZpRUZBbDc2b0pzd0FkektzbzFmWTNWV3JScEM4STF1djNBUjRjQWhMZUUxcUYwTEszUXpWOE95RFlBK0ZjMHRKTHRIdFFmb25NMHFnYzJ0UzN2aCtIb0w1TXpwSVRiMGVuSlhKdS9rMkhxaGZJbzlKOWQvak95MHcwWUFRTUtYQ3FRNGNscnhEWlFuRi9sMWlzZkhTT3F2VjRCc1JVTGpmZElxUElONFJPZTZZT2JYekdiSTZEUzZVbmVSM0s5c09xSHJaM21VVCtTUWhFN1o3UFBYbUNkemY4d0pDMUhZY0YwUlY0T1JJUThSODFDSjIyRm1Oc25WdkVpQjBDSVEycEVaeVZnZ25BaC9VOWZEaTNRTXZqSmJLbzZqT0QxTnI3NVZsREViK1RkRVdNdUlhNElydUt0V2ZZV05ka25McS9zVFZQSjgyOWNVcXlvNjNBWDhESkdEdFZmNjNKL3hsZHFXZ0dmRnNKd1F0NDJFWTQwdkhqd3JIdHlrajZFc1RzVkkzWGREVkhnSDlFRXJQa0VmakszMnNsZ1dETXV1dnR0OGhDWHYyUDBLczhqdnFHbGNJUTlwOVlnYU5CdzlmQXYydkkrNFBwTmRVZkgyTm9YdDhTaUozazBnb0IwQ3QyelNsSldnZkEvcjJJa1JXdWFlQlJXWnljaGVvemJpckdFQllFZGYvODVxbE1tMG45S3hYY1hqaVpWM1RpZmo0VkFRdjBXc3kvTkpKUzloeGZ6bmJHVFVCenB2c0s2QmlYRUR0Tmdsc2c3Z0ZwcTNObCtRVlZheVdjVG04Qm9xMWtUcGdmS0MyWHhRUDBIaTdlNkp6Z2d1WE1KaS9LajB6QXU5OFlVQW5mNXhTd0Z4Y1d0OUc0bnE2YjFUZE01UVNGc1pneVVJaEtlR3ZXTCtQVFFjYXlNcllFZ1VCWXFtdEp5a0pIc1ZUNWN4b3BIMk9kSEMraE9pY2pvYXJBYkxXckhyWXF3emhuNHpEMlo1TDlzdWZxblhoKzJtZUd4RmlGSkE4ZFkzN2NlSS81dnlMMWNwSHRhWTdzZzZPb3d4MTFNVWcvM3NBdDFPR05WMzhYbHdSWlR5Uk9YcURxZC9TK0NHN1Bqdnd5VnRsRGNZUzYxT29MdGJEUjJZWkpjZXZtanNzVFNLcDErclhUbE5XRWJWZHRvblorYStqOVlnVWxLMTgvWmh5dWZxb2U1Qk9QZnM4d3J2bnN0QWpoNmx4QkpYMUc4a09lM2RNcjVZdmNORC9EYkpWVXVzYk9JM1FsK3RibGdnL2RHNmdYdjROSEV1L0h1MUJoREpYUWNWV2RoYjlpNzNFMzhQcjNCVHdsYXJVbzBOaGhZWTJsVjl6WEFsSGVGRGtBOFZuMkhHNGZCSk9lQ2NZNDExVzJnSUFYTlFnbHBUNnEvZ0Q4akVlVndicTRTWVZEaGpKUTR1azJVeW1vUHBNUmg4L1cwRzhqaWxiaHhoN1h6L1dmVnoxU3Mycm5sWWdkcVpwTFJ5Q0Z1UVQ3eXhod1kvR0JhQWZjR3h4Y1MzVlRuZWJBcENJVFIvM2NTNnIxS0Y2VUhXdU92NEZIbHYwNks1eFl6U2xSU2hxN3l5KzhGYU0wMHBGWFhCbUxQU05RTTNvVmNLQjBYVEl4cmdkM3BSYU5yM2xkUStqVUVaYzE1dFozV2srajduOFRXV0pRK0VNaE9WNHppSmxyK1Vhc1AvZUV6amt6RThqTVl0bW04dWV1b0ZLNDZKYk5wd1VuVXVqaEhwSEgwd1VkeERCdEhwYTVOUTNJODh5cDJNc0pmUjI5UFR0ME9YdG1zUE5mUE1XYUJXV0V6ZjlpNjJqTmNRRkQ1RUttRitORURmM256bmlTT2dtZTFZOXlVN0tQVnFOTmpxQmg1VlNhdG8wdnY0MVY4TSt6ZE5kaERKcjJsdjVSMGJwNkhnSElwaDR4UWhjcm1FekREZWsxTWVST1hDOEY4bGQ1TFNSb0dtVW5RdG9scEtMMmtoc3dERDY3SzMvRzJ4WWIrTklaWVd5YWhrd1JGVXQwYnFJZ1pIbFcyNXFsOVVxMDg0Mk9JRVNGWFJvNkQxcjBlRkQvY21ZNi9MVlQ0UUxpVFJQYi9NRjIxaVJiVldHM3pTNmpXclhQMjllV0h6OHVERXRPRGNvd1ovdmJ4ZUUxdHlqVEZ5MUYzRlMrWXhSRk9aREhMZEVNajBuNGNSOENLUWdsMFVGaVV2YXdJTWUrbW94UCt5VXpnZUdMNGNjYUVhQVpHRnV2dHk2L0V4bEhacXV1ZFJLUzF1K1BHZWhqSXFZWTcrMllscVdYcTRTME9YczJhTXIzWDlqQ2Z0SUl0Q3p2ZEtrVHBWZk9GeWV5RC9VOHp0Sm5xcGFlS0Nzd3QwbWdwNWp2b294dll1a3J2dWozcStMbEtnT2FQY3RMM1dMYzB1U1l2cC80bElCQXNWbEhlWC82WjBuamlCT1NXRUR2WmM4cWVPS3pOa2d4V2c3bXhvWEpINmN2NS9VMjhBUWZtZFFjayt1MUJkTGZJSkN1TVJBcHZlODNGUHhGSWdmZEJmVVdMU0VWRDkrMmdvcmFjWllLS1NRWHJvY3gzNG1rWXhNMWFob0lPWEN1b1ZZck9tYnFGYkJ4R05relFQc1BHblBmTk0zMFJPQytXSk9zemM5cWsvR0NzY3FYUGIrSE1iR29WRERaMmdLTDNNcFIzZnQ1MTFNeFR2dWUyczVzU3lmWDk2VGRWUHFQOHA0VkdRV1FjOFdDWjYrNFlMWUdGdVJ3QmRYQStzS2g3dlZ4RWNzVU1UM2FXNS8wMXM4d3QrZXl4cFB0a0RFSTJITnNXemRCOGc5bGIrOWNTNHZpbGRIQ2JiSkhheVJmVm9QMTMxV3JOVDNnZkJ1U0lKaFFJU0h0VGVWeldrbDlLT0hEYjBXRUJHYU1veFFnWEJTYTZtaFhHYnhxcHFFSDlYOUJQbUlMTk04YmRqUTNoczdvYm1PTmdrVVEwT1dsUWc4U0lJMXFlTjJ1K24vamJwQ2dEV2JFYTVJbEZES1dmTGZZL1hIRVloME5Dc2owcFRidi9HRTlJN2xsMjFTL01LN0xoa0g5UGJlRlBJZTVrempFeGtodnhhaGNYRjFFRWhCbzhlU0N4c0tNQ3pvWHg5MzFCb1pPNW9HOU84T293TVJoTzZ0ZXM4ZjFpVDRiYzF2NGF0UUF5SXRGVFNYYkJiY0l4OGhQODVPVitLZGpONXBLUlJBZTlRcW9kcG82WDdhRSt0WEM5V1hTMll0SzZoLzdDSE9TQjJxOUVLeStwVHBBdlJ1Lyt5dHJOeEdvQ001Y2ZXaEhwVnRDWnQ1ZUNKSkJRMFRrdWkzeVl2ZGF1L1ovS09VWXpSTWhlWVZ1UFNPc2l3MWl5VERDRUgrWWplTW5udlFmRFUveU11QnlsN3VLNXp6K0xrV1JYMDdvZUR3dmFwMDFCNUFMZW1Jei9TNm1RSzFIY2pJc2lURERybTRqOWlKQzZPa2tGbHVGcGduRDIveDIzSFhVMGNHbE9pNk0xaHBMK1JDSmtxeVVpclcrT3hLVENyTUpjM3gvRERoZ3hXaFBoRzFpNjd0NWo3Uy81akprcWUzbVNyNVBJMGZBYnJvQkJTWk1Ca1BWN3VCendCazBBZy92WDJOWTNmcDJrVDJkMHIyRGFYREZia2pTMTVBak1JaXhaRkppT0prVGlZdU1Qbno1d2FRd0d3N2xwKzl1OUZDUFlMdU4xNTdoRnlvbi9JOUZZeFhUVTVrZnNKajVIbHdDbEtpOUUyMjBhVDNKbUNYQkxKQXM0dEFvNzFVOGpST05Ma1JBTDFKS1czcm5rVGVOOVJ6anRSdHdDd25wd29UeTRsZHZNbTlOM0RBNHhlNS9ZdUFQTmZoaHl1akhYdVFVOG94dTVoVkJHZUdNZXA3b3V5d2VkRDhqWk44R1lqYnJHK2dROW0zZVZXTHZXdktoNEJuMXZ1RnVxVDRIWlpaS0VGeVF2U1F6NmlmS1FhNmFmd2ZxUXlIKzFOZDhCajRveU0xSGJsanRPbEhQdmIyOWMxYjZ5MkNUOVJsaE9IdlRqK1ZpSXJVN0FlWHBzSWdqR1ZKT2JrY09BYWNWQktoMnpWTkVYTkRzL2ZCbVZsUEwwL1RLeGNUcXZySS82S3FCYUQ3dGp3bkdud2FuUU5wY1UxQ1JjbTZWelF4NHRvSSswNjVHL0Z6WXA4YXpUSE9GK01VUGVqcVp0YUwyekd6ODNDcHQ3S3Nuc1FENUFHa05LcXRMa2RxcFZWM2xhM2Q5VzBGU2JObVpqYW9RK0NzNHpLRWhDcHI5czVEWGhkZW9na0RER1ZsT1RiUjhkTkN0YUpieHhTMUtyaGxIdUFOcXhUTUtqV3NUTmowRzNCSEk0Rnl2T3ZtY3hDYUdZQ2txSTNSMUcyWkcvNWRRMStOcGlZTFByamIxRlhqWCtkdGxiQlB5RzZHRTJJdzMyd2hoeDFtK2lQV3JqNVFOcU1NWmxYZjk1enBBbmFxSXRPR2hCN2U4RThURWkrREt6b1lZWmNNMUFMUGJ1M2hNdXhCUnRCYThMZUpNRUZGaXBjOVc3cjlmSGZSbkZlZy9EOWdFS2xtdkNNMEJDd3lROHdJeEphN0RZTEtTY2YyWHk4ZkY3cEVOLzdpKzZXcDFLUzN6VC9xVWI2eDlGNHZBTWpLaDNkbm45TSs2VXQzazNKNDNNekdLMzhDODRlVUluY2g0VWFWdFErK0tQb0g5SFE2S1VraEg4alk5Z0NseWI1Zy9ZTnk5R29HZ2lRd24ybXJydURXS2NvU1YycTNNWURhdWtQYXhJM3hwOWtRdHM3c1prcmlwNGIwU1gvMktEOGZZN2ZpZ1NMR0g4U0lQNkwvbHdFaFlXbW0rOWRJbmNQYnhJdENlYUxCU2h6SVpJOHFGcU1MK29SaThpKzZaUnFpbzc0ZUI1cDFtV3hONWVMYlV6K2ttUDBIbDBwZG45Y0ZWeUcvckJMakRUYjF0dUFma3JENlRXRWFhSDFFcWtiWXZwTzdWR0JoMUlSRVZMclZleXc3T25UcisrVk1ENUM5bmVndTdlUWdwMkhRUVZ4Z3h1ckRLeDcrVnV0cFlDMEsrRGNFRVpQWllJWmVVemRneHRRMVVpdjRRd1A2b09qT3VFOXV3RCtpWGdWMGI0NDZ0TnczTHRMWGVlNWhRZkd3ZjQyT01uSTNndjFSS0tvMEhsWTNPNTdiejlDbWZnRmFsNFY4ZENEOUZkNlE3VWx2RWhlUzJZVlBmN2E2aG5OY0tQRU5oNEs5dzFHVjcwazIySmt5ZGhRV0pXNE9LU0VCNUdrTEtDWnpoSEJuLzllNWdjTGZHZlJDVGwvamhmMlFqUFdkNkh5QTcwd0w4bW1NL0RVelBFTTI2bG5oWXhjSm1mZXV5SmVWZEhOditBVHdBZXJFNjRTcXMydVZTK0RvMEhWVWlTT2dWSjMyV0ZwallJMlFyeExsL3JKR2JtNkd1dFZDU0YvaGxVUkhZK2p0NGUyT0VCY0ZTYnlyZ3JITWJZZ3ZMdXdUKzR6U1NrcTAwZHhNdFFUVlJJTnJDOXFFcmlxMFlQT3pKUzVtWU0rVnBsaWRrYjlwWDFJOGw1bVJYVkc3YXd5V1FZVWcyQW1BV1AzSUpTcENCYTZmTW45bmxxdHJ0NjZLd0sxQkdXTE93c0VnZ0hYZ1EyeFdvOXNlMWxkekI3MlJQakhJYnBXbzlsNWsvcjlIdEpyQk45YVM4cGVlU0piRUhGS1R2RHdNR3NhZ3lTdVVBNnppWTMzUk8wY2dSdVIzR3BMakNueXZOU2sxU2UyaGdISVhORWZlUXI2UDdHd3dqcm14RS9nQ2kvV1MvVVJqenNTakxmWkRRVnQ1QnZNbFVScng4enFKWUJSbElKdkN2WXI4NkxxeWxveFp6aWQ4YUUxNDdZVWdma1F6YjNvVk9sSlJLNjhHT050SXk0TFgvcEhOUmUrKzZOR3IzSTRjdHdIeXg3R3l5OXdGT0k3MmUvU3B0K1dUUFZibE9YVmJ3OFZ5cUpGRWpXMnA0OEVWTEtPQ05BNW14NTZta21xNVUveDdIQmJoalQ1WDVBNllKekNpdjAzTlI5clk5RGFyUWpKLzlNTm1WbmhhVWlQTTNxQktsQWdwYk5McGJPTktYeUo5K2Y5Nmo0S1JUY0NKSVRuYzE2SHpFWXliMTM5L1UwNjB5RFVINnJNZVNrNzJWdmc4NkFJQTJRWVFtUXpleldxVWlDaUZTUmROQU45cWlEcTFHNkFWNnNDM01RRU9WV0lGVGxLeDBlNkhrSXJ2UHF6alNCbm5uR1ZoVzUyOHFpVVk4SG0yVmdDaXZFRWlBcnpmNXVVRFBUWEFGSDFKYWszM3h2NlVyeUUwdi9LOEJKMHp6ZElzZXhWeE1jL2lBNTJqUDVzQ2RFb0xxemtQb2FadmkxTmpVSFNHbGZ3OGs1UTIxY3dCeGJ5MDN6YU9DMkNaaUt6aXR2alZ6TnlSbmxnbnM5bjJQbWpLUWgva0JoOHlGUThlRm5UelBCbTlQR3pCN3p4NWYyVVZieFJ5anpJUGdwMnppSkdYSFFNZmhtMmtoMVpKd0FwY3doSkJ1VEhrSjNLenp4bHVPOG5NZStNUVArYytrOXljSFZ4cEhkTXBnbjVJVVAwSHNCV3BtTnNJdHcrLzFrZlR1eEg5WU1hWXpNOGUyZHp4TlkrK3IzZ0tHb29sZ09pWlVLSk15MzNMOGJXYXpLZkRDa2dYKzVWc2dTNzdpcVhNVFNhL3licFlHSmdCdjFJWFcyOVRiOTlSVjdPcjFlVjJhcEFHZlAvc1Z1Mks1dVQ1aGZvSnNEVi9COVlRTnhuM2FCTncrRVozSlBrbEE5WGRGU2pVdzlGb1pyait1Um80MXVKNXdJbVVOaUcybUdGSDJZdVA4WVFJY2hKYytlUHBRS01YVWNoUHZhdHlETTdaNWpCOFRybGM5UzlMZEIveG9mTk9MTmxLdXZkVGt6djBzcVkvOFpZQy9jR3Z2dVNydWVXTnB4UmlkKzNDZnlKUEZNc0k1Y09hdnJGMnRTSlJGOFJUZC9WM2RtaU96RW84V1ZRRDV0TEpKUVZ2RjdnenZlL0oyVXNBLzNuUlhvdTRWNjdOV21vMVRjbEtEMWowNThsMXRvOGxsT1F6a1B2SXRvc2RwazdQN2cxSXp0OTBwOHhVM2RqVGtra05QZGRKaVhCT1FoanI4cVF2NXNJSmlhbTVtV3B0QzBDejd3Wm1QckRIVy9kOVAwbG0wb2xNRHhOc08yUHdUaGp1R3ZKSTY1Z2FwVUIyZThKTlNrTFFqSXlsRUEvYzFld21tUWRnZXJ0RHJhQjBFZU9mK0QyeHFwVTFydTdoa09FQmF2YlFneVZpQUtwMUdXdEk5UXgreXdQVHZlN3Z4aTlrenhhU2cveFZPV0hyVitYbS9lK093NFJIY1VzeWgybTQ1T25jNHRwa3FWYWhsVm96NmtLV3F4SWRPdlBqVmJDQThkRUx0OW9kdkNUN3g0Y0lSaGlOVHZ3eDJ2cVQwRU9jRmRLeHpFN3RWWGt5SVNpd2kweXBhaFRYb3pDM0Jac0FuNThvSWMwSTJzUVNpSVFpNVhhYkg5OGVickpITmhmQjBVeU5vSzczd0lMelhyUytJK1cxM2oySy9EdEc1eVZudXY0Mnh4R0JUTGhUczBoWDFqSytHUWtOU1NZcytmSE5ybE1oUW91MDlzYVpIWUh4V2tjQ0xEUGxza3YvemcwazFKeVBwZ2ZuQ2Q4R0ZUSjRMc1FmeEFYcm5meFdaMTQrbHBoV2phRDZGbCtJN3JBWVg1d1pCS0FibjhXSFNPc3cxVFI1ckFwSUR1cHo0OEI4UFpCNloyMVMzMkg3RHJGY1IvWXMzNW1JY1didEFJcjI1cDhLK041ZWFtcnlES3ZDZmNRQUJ0bUhwaVlzZ1RtSWlOanBoVW9McUNOOGdQamgza1ZQOG1yem1vZEFkZ3pUUm41ZWlES0xDaW9SUitubFNhWXhGeXNMY3VaODBhb21ZejA3dzNTbzJDWjk1bHFFQmIwSDB3OFNnUjZPUzVtelBrY2k0eWp4V1E5eXhJM3ZNbmpYYjV0ZmxiQngxWUJNYXo3cXVjdUdGMnpKa3BWQWhhRGcyb1lxbkNHUW40emRmWFcyRS9DbWdOQTRQOUxyQms2NFVvdnlhU3VpVkxxN1BMc3ZOZmd6U0hXNTE4Uld4TWtjbzYxdlFQb1djMHlTZHJWd3diemMvT1krWlBXVFdXa2p5NEF2Y1lqa0Q5by9CSDA3RjhJY1JjalRKKzlqYml2STRIYnRNN25veEFPYms4VzB2SDg2MVF1RVJUNnJkWVBrQ01mY2JrRTJxSlFhcGVFT01zbmF0RXFZOHlCTlVpc3UxTzZLQU84RlQ2WG9sM3NOeXMrY3ZSeEozT2d4cEFtY0Z4Nzl3RFh6UXEyc3F2Nkx5cVZsQithWi9tTjAxSnVhL3pQZmpDOURhZ1ZDQ0Y2dFF5NkFpWWJjS2FhY1VYU1dZRVFLMFhhV1hyU2NHVnBveENLTzVEMzUyYXlrMXd6ay9Mdmk5eEt0amwrM3JUbXdxbno3NWtGaVc4TmlsQzNnVHNGYU5KZStpMXppUlIxNkFyeEo5OUV0elVUUys0MDVvZktndFdtR1Q0RGtYeTNpMEZWRWI3SzB0K0ZkL1NvNDh0K1RCODNCZXdBbTFMSEsxbTBoZzFFcHZML2J3OFlsQ1BrNTJZTDh3ak1HYjdPc1pxdXpMWVBSQlhlZXRMWGFha0xJcDllVm54VmY0enp1Vlo0MEIzd09XQm1zY0k5aWdjeXF1dEN6a1ZrS2xNdC9VQXFXSm1OUGpYOVNjaTJwdldpSWhHc2M5TXBBM1VLOFExbzcxaE9aNlpBRVlmVmMxMi92Z3NEbzd6YUZXZ1hMVGg0VFJvUFVHRWdPYm9uY0pocTFDdmZXa2xEU1JsU2FrZW9tcitUMi81Tm5RdTRKYjFTZGRWN3daUU9GT0ZyU0dqcnJER0lIT0hTbE4zSmRNdjMvMFBVUXhXcFJXVWpwTjhudWh5eHhuSzkvOGpLd3psME1xeS9zekZ4ZnBibG4xSkJpa1YwcENIV1Ixd3d1REV3TEIxSkpRNndxRmhWMXBPMXVSb3BHcjgvSmpWL3RWcyt1ZURNTHcvQkNNblJ4enF4M3QyeUlIckxoWmhGclNWNnQwK0hXekdTWmZ6bWQyN3ZzVWhHbjJGbU8wTkRVQWJCSUF0a1RROUpxYXkvd2tremNOM2tROUFTQzY4VG40VTlNRDBDWTFJR3BNU0F1dXQvQlpscXhLaHZSZU9GVUUrbm9YMU1OMlNDNC9ZcGpmMk9oa0R5a2xMT04rTDQreXFWYkpVOGRodmM0UW8zYzF5MzFidU9rT3F0a25INEFseEZJK2RvcnJjcmpTYVRvd1ZNNlF1SXZNNEpyZGtxK0pDNFlxMWFZSWM0ZGlLRHFnSVVHa0QxVEMrOU5jN0pJa0g2S1VVZVlicjREWkpHTzVQSFcyL0M0c2VFTGRGNHp0R0U2SGIrSWV4TE8wb2Z4S1ZUL0xPRitXMldvN09MSFFPMkZ0MCt5MWlNYXRjZ1k0ZHMzVm5vNDg3cFUzb3BCU3czWVg4aE9BWUp6YjM4cEs2M2pNVmlkZVJnV2NtbmZ4RWZ4eWl2NmxzcmNoQXAxaHR4a2dqMUFyUkNidGRNeEtneWhLRVYyYWFaSURhWEE5ZkMxdGdFamJJbnJIdE94aGk2dHNGWVhOelhGR2tkS2VUMWdDYkRPTXF2S1VKc3UrWGlnTWEwdHFOMXVlZW5RaTdXZXBLV2tSUUcwcFUvb3JxTFZVTDFHWUlITERzYmRWZUtnTmNqdEpUZGljZkY4RlM1WW81SVRzNkV2MUtyeEJSc3lvUDZZUGo1MDFUN01CMHJNQS8vS0hxcjlNaWpEcXhOZjRzYTI5UGprVlhWQXU3eW02SS9KRWErb1lOYmRiM0lLS2ROSi9tVWdPVGxrV0EreS9xSVd2Z3JvOUZCdDZKc3RReS9VMkt0N1daQTFlMGRJckttRkVJdzNTN2ZmZ2JJZHRvT29GUlhkdFFncmpiYU1BNE9jR2xlRVl1QkZpOUloZHo1Z3NQYm4waGsyYlhmRnBBS1F1U2pJNGo0VjcrRE5YdXVUV1pnV0VDV0c5b2VyTEdSbXozNW1zS3dRdC9idVlNd0dRNEZac0tDZHNkaG53Q091QzNmcnVHRHI4eHJLcUxDRHFDYmdvVHNZWHJQdjNRVGZQZU5uMTByb0NQWnRYU2RFQVJFQ2xyRldQdVlKUWdXcUdYVzM0Y2FxTVQxVk1tcEY1MXFWeFNISi9wZ1kydkhoVlRPa3g2Q3lNTHl1VXIwaHJMNmdOMG9XeXJYdlp5S0JJb2NQZ25obXNXaEgrVitITFp4dHM2czRNWDBXOG1odkpLWjR2ZGtrSXpTZVE3bTUxaVpjdzlUMzFhQ0hJYU93dWxPd1h5QWVwZ0Z3VGw0MzFSb3c5UHFnR2tPREJDdk9SaFhMSlM2T3VLMVlJRXRQampSMVNEVzFGWjVzNGJvUFlWWnRIUWdEYWlSN09BeSt0N29YUTZPaFg5OGVRTWk0akpLSUVIcXdTbWxaVDdiU3I3QXBWZEhkcExRNEx3dXFKQjBSMTdrRmR5OFJMVktvdjJuUGhEYU1nMGxBNDRpYVJjZHpSbEJOTXVBei9RMjVtMTdNM0pyQzNRRmRvcGdBTC9CR1k0cndOb1duRDBvZFEycU11amdEQVdieHFZQitUTU80d2QzVjl3SWpyYkhSdGZQRVVwOVkvUGtyc0JydjF3Mjh0YWtOUlF0OWszMTV5SVZJTDBZNEpmVkx2SE5Dek55ZHdHbGNvcytSVjZwVlFPZU9LSWlnUG9STU5ER2NEZHVJK2Nnay9pN21McUtNdmcxS3hzNUtoRjRaS3ZBVDdZTklFZHZxY3BNS0ltbVdFZXg4YUIyV2I2cGxKbmVFd3N5ek5jWnBqS2VDdS83RGxUSmRNZHB1SGRNZ3V5cFg5dERqTXR5bGhEcEZnSnRRSk0wM3VSV29DMCtIb08yWWZzdGZQeUw5a1FhMFp4TVVRREdCSEJQK3NhQ1FlblR2cS9MRXBycFBRaUJIMVV3ZGt2OXZ5R3FndW9aMUx3Z2J1WlBBRmlOaDJpSm5lcVZsS21JcVIxYmhsaFlsYmVuSVRJVWEvMmZxNXN4T05CN0JLZ0ZQQW9ldHo0SVdDREYxVjN2ZWRIVUUvWkN5V25SNEdZK0RPWkhCQkxwa3JlUTc0QkNjeWVkRlROQ1ptdkZmUUV4R2pvR3F1cUsvTVFaNUpZSGlQdHA2MTNLN2hJcWhnQ3c5TGx1dzlXYXBHS2pwYW12YksvdVU2RFZzVTUrUTNKWTRUSCtDbW1hOWdLWjNnNVlmUWVnZmYxQUlaZHFNQXZEbWlqeElNY2daL3F2NmU1NDRJd2pZS2NkUitlUXRiWFdCQXZxdzhIQjJnR0VsZyt6L2MzVmNCY3pBd1pQSG4vUXh2SFBOVXUrN0xKdE9VYmwvcXFINWViTVVuK2pWVHVJbFMxc2IvR0xXOEpzOUt4ODM5WmxRc09lMkgwNEZUNEptUVNMOFcxSzByNXpiblpHVEJ1MXdmVzhOVUUzWmpLa1NoelpPSmgyNDJJYUhudmp2T3ZrWUZYL0duZ0FSZjlJUzk1WWhaamkyeWk4TkVRdDNxTjg0YzVqL0VXemJUYktobE1XRDFXQndKUGNCalB5aWFzUG9uQXpGTmhGT0V5MC9PQ2Fzb1orek1nR0ZBc1JPWVpBSDVmM3lZRytNa0dUWThSOFRNcnlaRjNyYWJNOUE5QlkxOUNnMDg2SFIxa1BTaVlJSkZXWTRkVHBxTGhKM0RLemNDQUw4d2xxYldHcVRoOUFkZ3VyTFhLOUxHR2U5Uk40YXYvRW5TRjVjeWg0aENZYXh1dWNMK3NaSGhPVXIydGRjMk1yWEhoWW1pbEJQZjhiWW9iTXpZRUVZZTJJSDJlcWVubUY0Q3puMnpTZEU1WG9Cbis1TGIxZEFkQXpRaTFWMTdwTzF3NmI3WHVrZnN0N2o3REJoUnp3eDNmZkRYUzUzc21nMU9kbjJhN016cHErbVJxUkhJR2lvVTNCWHR0akJ0M0VDT29hdzh5NE5oSnEvVDhUTVR2SklGRThWemFhVzdoQ3dJV05UV1JKY0R3dFNpalFlV3dXb2EzU1pqQkxkbVRTbmh5RVJJTE5KcXFlQ0FTMGsrQzBqUHk0U0lBYnRwMEFJNU05YlZZczd0d3NIejdCQ2hLdUZTVVFlaVdZK3JiRWxrQzZ0ZzYrWWJLN1h1bnU5K1BRRkVLMUdSMUtTeU43ZmFjbWtJbTBMZ2RLSXhpc2J3MDRjS2x5QWNJZ0swTldTTG1WdU1iSEVSd2V4anRWYkJvemJRcnhNanBCVjV0ZlZDcjBnREZobSt0bUhXbkwrbElsVjZXajJ3TWFSaXBjZ3ZCdDBmekNsS1hSc2V6TkxPL3RYVFJnWEVxVnFNRmdYWHAzVnczaFlVSEoyTThhejdMblYrSDVIdm5sdVpUbW40dDdmcmJoZDNYMjBUbU1YYm9vSGFRdUE5dDQyQmJiZ2xaRDVwcndGSEdRZlZJbGQ2WkxvSCtrWmltWFlsRStRWTdCdVVkejRkTW1IZGJ1Q09RejdXb3d4L3ZQRC9tRFMwcGFkalNjTmpIVjBRUXpIbUtEa0t6VzJiVDRLamo5NkErOFhpcm5CRmJDWU1vMWhuSzBqK2JaVngzWk9qdEs5ejBJeTFwcUZvYXBBZlE5WHFIS0VJZDd3NjVwbVBPSENKaHJaNW5odWh3OHZCWjBrb0FIVE9xcXRIcElmOXBneTE4NnVUdldTQU83aUg1U2ZiMnFzMmExUk1rSkg5Mll0NGdRaTI0WXZ6S0g2Z1ZITVlqbEQzSGh3dC9BNnZpUVlaSjhOMi9zTFRkZ3o2QVd6QkpVbGl1UmdScWxaU042WVFraThsMUl0ejRqTFZTMlBPMW92TlltbzJZNGE5UzlVRmFCTVkzNUdBZDIxN014L252a3Y1c1NiamhRcVJ0TlI0a0Z2UWo0MWo5cy85WlQ2dzdjZWJ6cEljTDFpbk4rc01aWW91cy9tSXBMSEhQVmVEREJHTVl3bmJXa0lSSEw0enhQT0Q5bUxENGhMN0dmSWxOUEpQRFcvcnp2YSs4M29rSXBYTjZxN1JuQkdra0taR1hHQkZTbDAydTN1cFVwRzkzOG5hMm1Ub3JEWldibWt1ZXRUK0l1WXJTVWdWWEdMaG5LcDk2clVWRXplNE53RFFDQmJVbVRwTlFiMHJQQkVDS1J4bGprZ3doQ1NjcVl6MmUzZ2dRMnF1VnRwZzhHVDlmdDJqMnRXNTNzcmlDR1ZoSkdnOXNrS2lxTFlUY085YUVNVzJTT240SVlmelhJNVlRdGJ5Q2l0RXRZemNydzBMeW9GaUhsbDRJVzc2VUJ0Q001U3IzSnN0RmRjNlUrVGtYT3hEU2V1Mno2dmZoS0F0OStJR0dLREdnOTlBWm9TNXFyRm5LdThRK2JpeFVtdjNjKzFJZjk3cDJzOWN5TG9Ob2l3eGVabU8zUmFNUUM3RmxRSXJENzJteHhrVFZ4NldDNVlhRW1sQWNrSm1jd2EvWko1bStWZ0RGSjFod3RFejF3YzArZjdONEtlRng3eEl1N3BLc2NVRVUwSWQ1SmdpelhWTUF4eUFHbmtNUnRSVFNmcjFpMlo3L1k0RlV3M3l0Zy91ZFF5UTR5ZTQ5Z200YWU0OTB2YXNjSWtwQ014WUVzdWIvV0NhOHZqTkh2OHlyOG96Q1BxNDU3cmpIOEl3NVFSUlVzNGZialBSVk56TTFRU3RRamo2bmtZaDgwRm5XcHVaR2VETzl0Ym5kbEhhYXlJZExlT1ZyM2tFSFQxYW1vZkh2SUdLUSs1cmdiTlR4K01xenpaRHRMTkYvWU5vUkp1bm5YUUhkc2gvbDI3WTdIcnFzR1Z1S0lCK1Jnek5VYnVaM0dnMUlpU0dXcDhlUGtqeFJGWElMdlRwVnBkRGtlWlpjVFc4T2FwQjY0L211Rjd3Z0Y5NWRXOXhJRWtURlY4bFlxK3JpcFhFc1N6bFRqNkxIdTdVaG5WZ2pEbkFmMTN5bm5UM0FsK0NWUHpDYVpMNEVFZmc1Tm9lSkdxM3doaDRnbSs0Z3FLVVplVjBDOU9tbFh5YWVJU2JIeHRiN0RoR2t4Rm5KN0Q5SUo5RWF1cUFvdXg4T3B1cGlyY0dpOFF2TDFocTBRSURaR1NtcmFyaWJjNlNVbFJ2VkRhMU43Vzl1cVIybUNKT0tlN1NOdmRaaHUwakxrY25jajhOWkIxV2RqQ1dxeFZBQWJOcmo3VGFsWFZISjl4WldoNVhrRjBET29qNW1HVlhhZmd0c01mV015cnlwT0VKVXYwa2Q4UzRaT1ErNHd0aXdNYUswc0ZaVk1zbjlmYXd4WDFuVTFGYTJyMENqcDBrN1FXK282OUZpZ3VqM2t3UmNjeVNKdVV6dFRZcGpad0U1ZVNMc1g2VS94S1BNMWlyMjY1S1g0QVRjY1AwVXJEY241RXhBL25wYndvTzRVb05jWWZaTHg3UkR3N0FvT1VpcDFUdjh4TzFFY0lIOStiWGhwdEdWbGJ4bTNsUTl2cWdpRnhaYkJWbThyd0RUNXl6MU9Ka21EbGthTkRUL0Z4bnQxMU1QdlRpK1B3eWVXWFVtNjVNTEtndk1JUHBsYzZaNWJaNFlIQ05YNEpiRzlVQjNJY0MwTEE0YTljT0doNzN4MEIwdU54MWRORkVTeVI5M2NuUDNtbVNJaWdpV0hUSm03ZERVWUJXVVJ2Z2JWSCtpTS81ekI5ZDQxNXlQSyswSU9QMU4rejZ4d3g5SU95Y3pwMVYza1lRbUxNY0t5VVJYZzNwSjVDRG5leDgvWUlYWWNsamR3T1VzbUNLcCtRdDRtcGFRNitJQUdHb1haQmc3b0VhdFU5RHhqRmxBSlJWSmdWZm9WTlZaM1o3Y0VNSnE2ZG1MT2ZOclJuTTVzbGJWYnpaRVFtczZTdGpDQmM2RUl3QU96aDZVdHdHbHFQRUZNU0tTU2VsMXRTUzFqc3lNUWFhQkRpWjdhR2JMbVRHeDBMeFhnUEV1dWNXTVZ3eS8wVERMdW9FcGo5Rkk3dG5RM1BnbW9FcHBqMzhXUFZ3a3hpU01WREdkcFFXWFhsajhQVXNaanJNOFJuWmpuaVM5NE9TZ0w3akx1SDE0dGFCVWR4TjJSMjBrb1pjSFRFWDFGSjJETFBnZ2V5NkVTUTltUmhsMGpzSVpOOGYvV3JoY2tseXFOUitFRUdyeVNjcFE3M3dTNGp4SlpaRFVKZ1R6NFJ5NmV2VGFkaXRGaU10WXpXSUQ5VCt0K2UvbzVacFdLeWhNd3c3T0NOV1BsbkprbHM1NG5qcm1aa20vZTl1SWVuNGlhSjBLSEdtNXNVd09vUnk5ZG94ZWhtN1BEamhTODhKNHIvb0g3ZTF2MWFTMklPaDdJNm5ZcmNGL3JhTGxNSEtETTVmV1pGNm80czZodiswSUdOTi9BRmdWd3lMZm9WUWkzNWcyMWxDRGJnZ3RhY29CTTJIM0w5T0FJaU9ZdTc4c3d5aSt1YWlTbktPdmhPOTFSS2lyNmlJZ0t1TzMwVFpUV3R6cVJYVTVxcEtRRXRGT011Zko0QzdScGFXck9MRmpVUDNPdGhPN21GUVl2NnJ2L2sxUFcxT0g2TFNsU1hraHlvU3NseW8vaWNLckdrWFlkWFkwV0RGaGVmaGFVZVpEc21ib3hudEsyaFdaenl4YnlNNDRDdU1taVZtSWtyQldiQ21GTEgwbzUxbGZpM3d5Y3VvRHNQViszWkEvNkFhb3VqZmdEZk5vQ3ZyTlZnS3VQVnNkdTNjNG9TaHZaVnNCMUxPN1FadUJOeUFpYWhLMHJVQXhFQ0NwOUVnRUFmeTZRUXRRb251amI2allrcWg1UUYrVlBkTTJxSHRDaGdwK1lpRVpMbVYxQmFYeDZ2SVBJSGZtR2dSZFVoYW5GbGNLQXQ1ZFRDQzk2VFE0N0lRc0lwYUgvdVhFQVNwWU9ZTHh2dTNMTHRRcVlDbDBhd3RQbE5ZcGJESEpFQ0dtOVpnc0ZMTGxDR3Q4TUgrSytJVVdoUDdEMUNtZ2IraDJnSVZURXdKdmt1VjVGY281aXhYS2VQNDl3bktReUwwc0ZBWTdLZC9ISVJqbXhPTG5tMDIzQTdmcUdZejBuZ2hhNWloK0xyZ3ZLdWh1d3lGcnR1bzZodmx2NnJ0Q2I0TXYyTitJRUlvZ2ZxMitPVGRVb1BCY0M4d0hIRVcxWG5BNm1aK3l1SW1wSjlrN1ZWOTh5YzZoZWlxMzd5MHNaQmJBaExGdi9LYkJ2Z3JUMy92TzAxd2MzZ09mRkRuZkZrbWQ5eUZGcXFodEU1bU5PY0RFdVdUcUlmTWxjQU1Xc1BPTSt6Zzh1c0dPOTgxZ1I3Y3A4aGJ3RC9OWGVaZVh3aW5rVmRSdDdDaXJ0UzlTVkNXK0RnNGZyTjNDU1E1cGpOSjVmdWFybDRkMXFpeTdML2x0S2s0alU5a001UU5YKzYwbjRTckw0UGM5L2JtdHRXbi9ScmpTenh3ZFl6SjlnWjc3ZytsVmk0R2RnN0R5MStSK2V2enBnSFRkSDNITm1tWmtlaTBsd3N5bXVCK3pxWkluMFcxRHhLSGs4Zy9CUSttS1ppcWlLQ2R0TDVzbXJxd21vYXJHZEdhSkREaDBnZGYzdkJEVGdKbUMwaHM2OUJ1Rlg4M1d3Zy92NlJvS0hiTEM1SkhQVnRCakx4d2VHR0dENFRzOEpFN1N1MldKcXk4TGgwM3BGRFBRRDZCdGwzMmpsVzVza3FOVjMySThweTJNYWFEMjFqYi9SUVRDR1BlNWxxRFVtMytGaEQzNlZQTERZWjZYNmxwcCsvZitjMHRPWGZiRk9jUThOUmFHZTB2VUZ5MzRVTjlTR3Qzck9LcmkrNzRzUE9mVGtEYzNEVHhMWStGS2NNYnkzYWh6blhnYVB2eDlpOTVtNnlwSWQ0VDkwUnA5TWpOTEptWU5tV1BsYmNxSHZ6WUQrbk5kS2NxUTNuMnZBTkZTNXExZnh6a1NoUUdVazV5T1ArNUlHWDJHRXMwNXZseHkwSG5SZnFISHJzVEQ5eC9LdnpNQ1NuYWZEaXFXYW5Gc0k1ZmpPeFpsVmFzZGdoeWtTRUh2b2EvMEJzYTJVV3BBNnIyMTZzYVBlWTlGWWYyb3o5bjRWa2lrRXYwUFZldkZkYk5melRteXcra0xpN1RPSEoxbmhmZGdISXVxWUxHSjRsaEh2QjRUcmVLM3cyelF0ell3OTRFNTNvMEw0Si9iTWRtMzVaUHdUVkJOeGp2b2pKakFOYTJxemFoMWxjdFVEc0t3RnhjYlBsTTJmbTlRL3lTY2xqRDlpL0N2Nllab0pmUEQ2ZWdoeHp3bkpjOW5GUlRXelVMcDkyeTRBNVJlRk16bzZtSnBqVk51ekVlb1FIZjFCVG1WZnJkTVVjbk1pNGlXZTZrbGF1MWhtZFhFVDdla05EOEE3ckFReSsxc2IxMHE1eklsbGxsYWdIa3JIRDBzZVp5L0FiYzJvN05pS1FhdlBFbEhjY3FuRzlLS0I3a0NhTTN2VWJvNWpzRk9tOTJRWU51UmZLWjBOdVZ2dHZrMWVldUhlN2JKT3hPT2ZIMkFncFpla2QrRCt5Si84VHgwTjFiWGplcGhpaHlSSFZ1QXdJcHMveDNJMlVwU0hYejlBOEdPQ1VPU1d4cnJlUEVoT0grczc4Q2RidmlaWHhYOEJtK1lwQkNPQmpVaFhycFBnNUYxU0ptRDNqUk55TU8rQmtyNk5sSUVidng3cDRieGZ2bWJQQ1NCaUNYQm5DTFhCcTdNd1I1Q2ZQNlhoNnl0NWQvSUlHbHlsaDVXT1JTckNJVGwydnB5cTR0c3ZFNm1Ec2Q4TWFsaFdUWVJlRVdRNHVHWGdPdkJtbkU1U2t4M2NZbEJWMzdrRVVDdlRPb05pVHFDMGF4R2JMS3dzVUxncnJhOGxGcS83WGlpM1lhc1BpaUswZElqNjN1NkNISG1NdWhSWUorUUt1SHJmMUFjTGd2U1ovNGo4U3ROaFNFbTZrYzRMMWN4Umk0QmhmYjh5bnA2SVRIUlZtUG5wUnBKNld3NGRSWGRxeC9DRjFDQ2UwYkxLOWFXaWxpWTdWbUVRdzBqNS85cTJ5RnBZVlg1VnBjcUtOZkJRVlR0ZTh5U1VrMlZhZGd1eStBYXBmUHBtVndTaWV0UGliWjVWNkliSmpvMUlmbWl6eVhpY2poN2FDdzVGT21XZFpCdUhIVlVFdkMrV3Z1a0UxRXZiUFJTcTNZVHdGOUFROUZQTVJsUk5zbDFjQWt0TmV5NExMbWIwNjBhQjdjZ3lzRnQzOEJqZEZIVkFQbGZRVjhnYWlEQ3hvY2lRdXBoalBzdDNzdzRYVURVbllKN296VWVQWThyMG9UQ3dRVVdpS3QwVkZ6bkhQSWd2WjZ6MEpwQTY3YytZeDV4VUFnRWZxRWFNSmdHYzVvaXE3WWhaZUhWNjh3N3VVb3plTWJRSlZmZTBlWStOLzZxWmlPK2RDc2Q2WGNQSEVNOUxtbWVScklzR0pHK1IzQUQwQzRzRHBGbmZ5OS9VZ2htc21acER0dXcwbmxJWDU0czJBTEVlQlFMV3JPU3FaTk1vM2k4QW9TWVRtKzJQbGNFOStSQnRSZWlHaGhBc0RLY0VzUkl5YjViOVo5bjQ3bHJYZ3FlV0IrTXlTNnVpVGN5QnhUWE44OEJiUy9VL2dLQ0pwZUhkdVVSMHc2c1h0cFE1dm1WVStSbnh1VVdOWGliUi9DT0NPeWlHSU56QkRIYlpTWkpMNVF4U0M0ZkQzVDY5T3ZlYlhlcTJOdTBZcnAyOUFSVFRhUmNVYVc5d2xhRDluZ1FwVWlvRFM1KzdmZk9aa3JSa0MraHBoTERna3BteE94RmtmSWtTQU5tYUl3R2x2aUVxaEFSR2JRRHRYaTZFckhtV2h5VjExWTd4MUtCaU1CR0R3QllpaVpBMFVyOHM0cnFIMXM0a2xpRGVKaEZqd3dBUXBPSjNUaWhNeDBnUGRzWGFvTGlVY3Rid1hEV3ZsR0RWMWRtU0tuZUZ2TndmZzlMbkhqeG5tcUlJVDhDNnNEdlFyclZVNmhvY2pxeTVlVzMyWC84cHVLMkdwdEVEakpVWG8rcVRkdVp2SnBoeTBEWG41YUwwUUdFMnJEazI3MXpDaTVkNTVEck52MS9HY1NKVU1ScStsKzVRL2FVMnlJcUdZOGNNR29qdUFxc0c4bk5ORDdnUW04b2FNWGtaM3JTck9udnlMc3lGK2FYRGpOc0xRNVk3L0N4TTdZUVk1NDB2amxVajJ5QzJRbkFIRGtZNEVFaForTjAzMG1jWXdMU2xIMVZMTTc4Q3VXNjNuMUVJbGpmVVlaT2J2VW1XcWlIVEpDQVZzOGlCK1lRYU1XaEIwZmlyLzQvVndNOU8xNDVLYkdEeExVS3RwYnQ0ZDllVzcxUDBGSjNoV1ZHSlRudHY3dnlOcytJQlh5WU43RlJ6VkhQbkh3b3JuOWV5c1BpSmg0d0NSOGV4ZXR1c0U4ZnRjU20vMnpIN053cFoyTnBiU2lDblRrK1VzVHFUa1gwd1UyRXF5UW0vMGNwK0tPVk85MkJFRjlkSnBNNmpaWWlpMUsvUm9yMHpsZXlsYjdRa0pWUVJCTlpOblg3emVSMVNCcVlZbUtZWWtNYmgzeHJwK2dSa3gydm1Pa1RoUGxoVFNOcFMzbmN4c1h5UWhGdW8yUjZUZEtRRzJaOXd1bTFHMnc3MktXQk9UbnZBc2dwc2pCSlc4ZU9VSzA1WEtoR3g0dzFGV29DdEtneVk3Vk1PUjRRWW9oNDRHc05kMC96Z1U3dWRZVFdISHFjZnhWNGxEWjV0ZGdYSi9JenppSUxweEZ5Nnk3ZkpUSTRzanV0MDlRWG5kbElCK1IyWFUrU1paVjBmWVJMQWg0UVdiTlBmMGQxQ0FBYnJIMG5McHEwdm0raXRHYWpMbmVMZTRMc2NWYWM5MWRlb3RvbTd4MXdBZFI0QU0xR202Z29mTURHMFhYb0xFTHpNQjNOVWpzYitnK1RGMmQxVHJLeWlRTUZnUGU4bVNTUXN1a3EyOXYyUGZRV3liU2xIdlJsb3B4OUdTUnNzdjU1TTZ4NGF0RFJWdzhLUldYYXNvWk5LY2ljWTZxcXJsaHhROXJwQ1J0WUhQTFpuMlpMVjlIUG9oS0crT25ySDl4SGJKZ3c2eGd0VmFvNEpOejFIWDRIZnJGRFJhZnU1T3plMnY3OVNUVWxwK2h2OFJtdlpvWWxQMFk3OWErcXVwOHJkVDdPeXdrN200c3luMUM1QkdKUEZLMDczTEhuRnNiWVJ2V0JNTE5mQ1lDa1hJVG1qN2dySXdCNkFpOVpZbEExVXNueHF3Y3VQOEYyV1FGRzJYSHFzaDkxdmlRUWVhTTZGVzNOT1R0UkIxeXFkV0RVakRjWU5hSHhnNmJnVzM2QmFGMm14NFlkbFc4clFZSTExeWNlQmNuVDNhdGNsZHkxU0VNU1NaVS9vTm9VeEd4U0xzNm1NY0lVVVV4bERyeHpOcXo0WjR6NlhuZm9qUURXZUdZTkJrOU1oaDkvNmN5VFRLNXZLOUFPaVJleUc0L2JUcnNwaC9YU1lyeDRGQmRVSUpzK3M5Q3FqUW9kY0NsR3prRzR5SDIwNUc0NkFPSE41dFdDa1RHVThVMU15endQMnRrcmFhajE0bkZpNUcyS21BdFB4dE0xSUhQeUN6OStNTlBHRTBkalQ4RkFmUDZyUGllVG5iaWpQK0dLVEJ1NmhHL0l6dHhZWFU0Y3ZHT2VEVnB4dDNTN3hleDZVczZSMTRwK1FuaXdyK2x5RWc4cmFwWE5OOHNqUGpKeUhlTW9QS3RoTUNJS0pJR0NGRm5RR1BlcTgwQWFnZlY1MG8yWlh3MmNkNitaZy9oN29uc3BIS0Vzd3ZMR1pCd0tCRmRySzc1WWV3c0xQUTNmUlJ5ay8zTk9IY0tuMWZzVkorRmJYZjRVWlRYbWZVQkxWS0h2UVR2d01LejhBVVJsVGQrNmxxWmxpd3pQMkFaUlpTeGZIY0hDRzVXN0N2MVVtQTZ5dUc5T1RWL2VqcVV6bmJKZHkvbXdXd1ZkdXozZkp1SmY2b1RsRyszNWZORWRLY2poVzc5ZXNPOW9wcEVVSm1TODJPQWt4azNaZ2lWbVBKZWVIbHlsNUhGcGpLdStVVXhwRjNHT3IrOW1uMmxlU0dMOG45Smt5cEdRazRVUlQ1bS9Yc281bFBUR0l2MGs1NENQeFJVaEhsVllVRlR3OWtxbjR5SENOK1pIYkNKT0dnUVZua25XaldGamw1S3M4Y2FGTk12dGthNGFuVm43bFRrT0ZKcllFT1hjWFJNRnJQOEZjbjZiaGlRVGd5ZVlXY2V3NVUrVnZGckxDQmkxN3A0TjBodWpGRzlSQU12OU1XWEhCZXlhM1U1WWlmU1cyczNraVBmaEVEcG9zcEZlSWFLSm5rV3RkeUtDZC9RbitiQlRPOVA0QTNTTmZucDBKMTVETVFHMVhRV0ZidGM1Q1M1R1VqWGg4cU9xNjVQeHpEMUdHNEF2cm1GYUZQRW9WbEZWQmxpc1BUTmdNK3ByK1BMTXcwd0Y1SXB3emhlc2V6cXhmSDQrVkNLYWlKazRJeDNSQjNtc2t0VWZ1cEZBVi9VeWhsRWlQaUczT0R5Q2tKbUJuODN2Rks2OWhDNDJMZFFURzEvSlpiZW9QSzdOM3NKOEwvNmp6TVlsUTBPT1J3bUpSQVZGVUdwTE1QZmVGbTVuTVUvRk9LUHRHVzJoOFRlNXVzbGFta3NKMU8rNWk2MEF2ZjlIZ2lZSXdIbVpQTnBJcFRsOS9TT2dwZXY5cEs1ckVWeWx0SGJDTFdla0dEaU53b1RLRFZMN3NGSThGd2VzS29kRk4veExYb0xFRmNyTFM4cTlVZUJVMmlZSzdQeEQ2QWw5MjYxY0lUT25pR1JmQmdkby9pVWlBcCtvRmZJc2wrb1NDbStOSU5DY0xwdldGYXIrV0ZBTHZpa2RuVUV4MzNiSHh6cWxGUFMyWmNPTHhmV3poZGFYWEpsL3NTUzNXYlY1OWtLV0NKcTBtQmt3dGt4YkZkSS9TU2JIdkJGcmtNcGIwTWRzYWNaL0ZGRWdRd0UzYUdOREF4STJIaFJkYS90Z0FxNjV0c0VjQWxYR1ZJb1l4dS83cFJvVXdZeG5reE1ocEpwVFYwSVVCZDhNSVNENnhkRGphTE95M0VNWWF4VURFamY3NndJS0l2S0dpUzVMQUxJTCtCRSt3S0FnSWo5TVM2cGdnUVdLcXpjWDJSWW51SUJzaUt0Zm0wMVJaTDlnUHFBT0xrMkg2QklQbTFqWm9vSUhtVERzZmlPZFJPa3pDNXVkenN2NHh5b0hTYXRuOHdUZXB6Z0VqaGlwaTFhQzkvaUw3Vi82aG51OUhFOFBMOVgxYTNkaFY3Q1htOW5vRjhjZ2ZJOUUvMTJndTE0cGUxakNhR0wrYkt4eERIRHdncXBYbU1oNzB5QS9taVd4WWV4cExTdGR3Nm9UQXBzZWVkNWtsRXhzRHBvOGEzS2NvdThDcGd5QURNdTByUTg1Vm1ScGpudGxXTnBYQUhOYnp2aVpWRGQ0OXZvU1FMbG53cVgxdEhOZlNPaUJxVEFobDRVZFRHMTJIU3NPUWRDU0psOWF3UzVDYWhXUU5uejVWeC8vVi9DWDFYWWhNNWxuWGdRMkxxRUN3UlV3YmNTbmZMaTJ2cS9rZ1pGYi9wSVBTL0VaeFlUQ2Jwa0VHRkc3VXZyREd0M0FrTENEQ2VURlhmS2htSUoxNXBZSkNzZnVWeS8rZnNQVnlTekhnMTRXeXFXR09ZSS9WQS84ZmozSGttcFc3Z2JZb3ZwaW5jR2dPSlpNY0VrcDY0UXF1UnlTVkhUR2pDUm10MDJwVUh4NS9BS2pMbEFCcWFLZ2hKZXpXYWxlektSekhFS0VnV3UvNTNmZVQ2NWdQMFQyUlR5d2J5ZFd2WkRDK0Rzc1VsYWRPSkVSekY2ZFlMbDBwRUFDVEtVNGFWN3FPV0ZLN3BrTFVBSXRXTXNHcnFTRnVoaGF3U0IvOUk3UlhmdUYwNzBxRzJnV3F5b2JpMy9iYy83RHQwNEtBR2xlSlpCRjk4WVRrbEJkY3RzeUFwelM4b1pIcXBKSXFPTWhtcDZYMTlYaEVqb3F0TS9mN0hHSkV3NUZUSFRJQm9IVXR5RXBhYjJNMTNHdDBaRkQ0SEJSQXdsRmlNQzR1U1h2L05LWjJLdjVsaWhYZnZwVGU4clpzcERkQTJRRmRMUElLWWRpZXhjYm40UUFtZWRmZlpTRmhDRGxlMmZ5My84aU5VeUdaSWkyYUo4L1dmYk9rVXhRb0ZlYzdQa210eERvYkMvbmt6VlRjZXV5Y3RNdzZ1d1pjTzZCWUE0Q2ZYZlBpZ0VLR2lROXZBOUgvdTJoUmRySzdPMnNENUR0Uko2OFZYaGFWR2ZnVGdKY2V1OHlielBnRTZHVmpIRjkxaUIwK1NiRHRPMzkzTWk0MTR5UHlFRTNTMmUxMlVhakVqNHpaMVo4K3FrQmQ1ZkZwVGR4Tmc2MEIxSXM3VVpDRFlQa3dETVVuVmp2V2VKVThrSTd5SWQ5clVzbTNCWmJLaHl6bU5zWHl4RlJ4VzVzRzhHdG5FSGNlODFsNEE3YVhheENjem82c2F2VTA1NE0yTzZ4cUpTL2JBUWo3aGVvdFRRbWoxVG1LVWJQTVFjNldSVXlIVTVna0pvK3p0NjlsNTd3bVVubW5waVZhNlkzME1CWnJ0VE11MmRNa2hOWEkyVm5hRklra2JaKzZMekRBQ2hpV3ZENnc0bVMzUUVSK3lzajNncnVEMnJQODVHZVRqZG5TbUNhVG1NODVjbnZPY0gxVEp6Tng2OFhFd0tSRHF3MHFzZGdkK2hkaVk0OHY0dUpoMjUzR0tMUEpyRVFpUVZaVEFOb01lSEt1RUl2aFZVQjJBWkY2RG84RkJPUUE4cE5zN0dySFcxcWsvQmZ1aDZtampQV3hVT0tWVURYek5sTUNtVHBVTmloZzhXSWllSFp0ejJibjRtaUpLLzQ5UEs1Z3RMcjBYMGJ0d1Aya2UwamZCSlJreWJuaXBieTI2WEFSN2hGSWM2a1paTGtreHJ0dmRRWDNWaWFCZWg4QWxVTEFwY0QvcW1oUEJmQ2NiL0JCOGRYcCtuUkRuM2xPLzkrd3Y5RDRya2NpM2I5M3kyNk1TT09SbVQ5ODNZM1Z0b09jaXlVbVJWaThoTkloa2lPOGZ3djczYmFxUEFScUJDZGNnZ0lFMks5YVh5S09GdWg5N3lFbzQ4a09aYVJMTkhQM2NDYi9PZ2sydmNQQkRFdE1DTFRNck1JNEZxelV0cEl5eUVFRjdKTnVYOTBNNnkyZjhXcEtFdHpwM21ObTg3K0lGUkttSmhYbjFaVmZWd0VVREFmVlY0RTRvZXMrdmlEdGZPVGVaczIvdk1jSTZYbVJ5QitZMzFSUGk1R0hHeDJVZUhzRDdvblJpVnZIdUZRSXdKN1NtQllCd3JhU05Hdyt3ZC9rcERFazh5WFowVFNoUUZlNldaVEhSTy9jQWdOK2hiQWdNYVc5QWxmZk10VzBUWjBjU3k2RjdtbWpxYmRSNFpyWTNucjZjWllFaUsyRFdVWFI1L201K0pQQUlwdTRvSjFVV3YzVnl4QllZcWtJWnFxbUluUVY2ejAwWWVOV3Q0NHVVdWpmUjN4eEV5TU1EdUp1aVlHTlZwc25QS2sybC82b3pKUGJ0SVEzR2w4NmRFTEZjNnQ5VE4yT2IyYUZGbTBLb3BnR1Y5YTFrdXdWZ1dETE9ldVY3YklYM1pUT1lScXl0NmM3dlpmQjR3bGU2ZXc2Tko4UHpUL2dVZnhYd3g5RFpMcGo4cWlGSDBxNTI5Snh3VE93Q2g1cFBqclh6ZUp2VFpwbUVsSzgyYVJ0QmFJNFB4MGc3b2srbi96djIrYjE4UzQ4WjRtM3gzWmVnSzJsMHNDZTR0SWhPZXJXM3BHUk5qWUVRd2E1dUhVdVJGMnR1NkJrclhZSEYzcDMzSXNFOVN6c2VBcVBCWjNsc2NMSHk3dDJ6NUEzSGs1UCtuQmFzNUJTVjZBVjdJQ1l4WU95LzE4YTU1NEgrQ1d3MGxGdHpwL3FlSnZGL0NEL0VjblVKQUhmaHdOUkRFM1k3cFJXcUFxMURDajF1cGhMQzZkOFBucHgrcE00TWxJUHBQV1dFMkJHeEZVT0xaZEpJVlFsTlZQOWRBNUFCd0Ivc09nZlQwT1NFNkhVK1NyZDFscnpVUGdNVGF0bmFQQjlmTWhEWWVPWFRabVp2SDlET1pKcmNWZm5zNGM3eFliT3IweW8xYlN0YmhsN3VRYU5JRDQ2T1kzQUhraW9mbHhRM3ZNME5vR3V5dmpkd0VCdjVLNm1PR0hhaGRHdWtTWTJ3TmZmUDJtcjcxU0U3RVJaSlpVZFZNWERaTUZaWGx2SDFURWI2b3AwN0pURDNOc3pPaEtNYW1FWlhSem9BVWV3RUpGYjhEb3U0VEt1bTBjYVZvTzR4bHE0Y1c2aDZjQW5pRUtYNDNKM0RqRE1VUUxOQUlmaGplVUFWNUw3eDE4em5tc2NhTHdzY2p5aXp1WW1hSVdpV1J3OUtYVm52VWJyMEJ5dXRCcUZNdDgrcStLTndxV2s3ZmdIbm53eTQ2UXFKalp1dU5NdFliWkNWRkduTGRSZkt6UFcvRXQzVEFlMDVjV0dxY0krZ1JSeU5IR1k2Z2dQRC8vWFVUUktNL1NUWUhNUWlEVW9tYmxDNDNWV2w1VVFIMnZrMUEvSHFWRk9zakhQeUthOFpWVzJlS2xSOVVFcGJYVWQ0eGc0bDFrTmFNUDlTT0ZTcVZKaWcvTjVhcTRqQ3krQ1R1TTNxMVNpOHlraTBoa0RJR1hKOE9YSkVFdndyY0VBUFovS0dYRXB0VThBQnFlYmNRbDE3bzFITnVBQ1dTcFRBd2w5MU5XWHV6RVFrOUt2bHMrdG5JWC9WK2ZiYllKWHk5M2VwNWZSQXdtMTBhbnk5K1hhSDE3VTBVNEs3MnZMSTJIVDhnSGx1VTd2S0c1ZGtSdHF5amxZWVh0bU5PRURWUitnRUZLcTFZbVkxUEdYRjl4ckVNcm55OTYvcGdiWTlaQ2lSdkkyTG04ZHpYUDhrMEV0djB2d0xNZVVtalY3ckxHKy9yZWRLUWg3T3ZuSGgzOXU2NCtEVzk1akdJTXlXUXVuRmM4TWcxSTJRMEwra1BPemN5R2VCS3FVSmhrWG16SHFNaXFGaXRpNS9TTmNQNVRrWm5xcWdqeVBTV1ZOM2hnTXhNNVFtNnJVMkhLeVJqYjJiRm9mcFNLWEg4ZW13QW1mbFFMS2Ivbkl1WXVqZW5DbHdDTEk0L2JKL0loTkVPQ096SG5SZkJNNFpTeXgrZmVsTzliVkU4TXZmTEtKeHRQVkUxRysweDYrRjcxeEF0Q1BYcGlpZW9la0ZnelZjc2RjMENqTkFuUS9aTnIzM1ZtcVVBdmFTUEJuUGxvaEs3NmwwWmx0OVJUODFONk01UWVnTE03WnJRczZMaE1TQ3pDcmJvZ0FRaTVIb24yMENOUzZ5RUptQnhySTR4Mlh4a1VUOXA0czZob3V3YkozcVNDVkFWTmUvbGFRa0FjaEszdjUvOTlRK3RiNVJEbGRrMlpGazdVOU4yai9DTWZXY3NJa2tPZm9MRUhLdUNUMDUrNXQybEdSMmpOV0JZVXpxQ2xiZnIyU2Y5WkpvbE9nbGlSQjBqY3B5WmkvcDZqT2lKM01JU3drNGRCVFRBN210emhzdjloZzMvekgwdzN0ZEptU0M0RjZxTzNCbFhjVit6TVF3OWF2bnZvaFJaRitDM2FxWTZXcWZ5MzcrQkc0aVFIb2p1VTMxNzVJWWI3NUR3ZC9ucVdtWVc1aDlXSG90aXR6MXBBS2U0K3BhSHp0bnZwRHVqVWRHdnFvalhZdm1ENERyQ3dRVGNPZzJVWUxZMDAvbk5kZGRTK040YWYwYVpCYUo0aHR5Y1RxZloyWHNXV2E2L2lhSkwvWFB4WTMrYm5CVjI2YmtndFBubldwY1VhSk1UNGVNNFRzSTRkRkJwb2YvYW92eDQ5RjlPcEJWSEEraktUYTBWSmZrMEl4WlZ2T3ViaXNBeW1SVXE2bk40QjlFK0NJcFZHVGd2U3BJK2pRWFUxck1ZbE5UeXBsUmtJRGQvS1dMZWZhcVFmUlRwSERDa0Mwbi9KU0k1VTFlVmxZNkdhVG9Nc0pyS3lkV0crNHRlUmJELzU5ZGRvNDdxd0hXaDZMN1hFRzV1dVg1N3BkM0xWSU5tRjg2Wko4QzFPRWZSK2MyUG9WM2NMWXFpbkJKQkZhVUlGZUtIU3FveWl4RkNzMnlhRGRQb3N1T0EwQzkyL3ZJNXFKZUZPWHJ3QVY3NG9FaVRab3Y1cWMzRWhTd2xEdFpoR3pPQXh2REpKdnMrUndmMkFHRzJuWVpPdnB6ekhHY21ReFBROUtMTi9RTENDZzljRzdnSEVSZlJKdXU3U3RiYnFTWWlkTkhBcEVoTDUyWW9LRlo4K0tDSjlWTExnbzIxanJraG1kME9CZEFUVC8zSXplTWhVZWYyenkrNXNnbVk2Qmw1bjBoOHR0bkR1VkpyQjZNTkQ5UEtmTFBKeGVwU0Q5ZHpEZ2NxN1hKcVhqTktNcFd2MXBCTjkwQnJkVmNNamdjVC9QUm5TR1BIbTdrUFFDbkRaVHR0aEpVM1FQamdlUW15SXVGeThoODhvU2IxMDlNSG5UOGVQNVpTZUwzSzNrTzlKK1VNOW5RWjVwRG0wVVdRelVvQUdSUGh1N0tlWkFpYjFmcTJuNkY2TDk4SjZLZVVsVWJWRWJSQml2N1hVZWtweENjdkdEZXNRYlJIdXlkV2xxdER5NVk5OFZvWTZOTmlvbHA3WHk0bUtzSVg5andvLzZRc3gxNjVTYzN2QnJHREtnbUZPL09yRE5GM1ZjR1hhU25qWE5OblA4b2JEY091eTZKNVEwUGdPZ0hrampCSVNPMk5oNXVQdHpTYlc0VWYrb2lianNFOTZCSHNjcXlNOWdReGludzN1eTJQakxSQzYzRjRPYlB3N1J3NDBwN21lNlRVekZ0bGFVRUUrWE9TT21nNDNXbDBWNmlFVkJqRWhpVWhWeENiUWFWRW1VV1pPR3FUREVSdmJGbUhwWDZjeDJZcUIySFhnNGdadWVrc0ZaRmlLck0wclVmaTQrQUZpRmV0SHMyb3FDZXVlTFJ0aGU3aWdrOGxqSEpaZnU4bWJvNk01RysxNU9tSnQzVHJLaHo1enFnMk1RTnE0VWcyTW8yZEJYRVcrRWVQNHcwS1NzOWdsS295Z1BFVnl6aUJaMmwzNHQyY3c1aVVJQWhaSEdCcENuK05DaWYwSFBKMmh0eGhSTVhyS3c0WkI5VXEvVkQ5aVE4M2JYYU1uTmhYUmdyWnFGdHpzWTkwM0x2WGppSWhlc2pCTXV1ZWh6aGJ4cE1EMjdqWEk3dGhyVXBwemhXQllMWlZ0L1dNRmlISmFxTXZrcXZieFFBSXEyeldDUGF3SGQ2U0pKVWZ5SDVhSjhaUHRGbmNkNnROSEhTOHFmenkzQkJwTGtNMUxRdWVMQXZRSVlvd1lsOUtWNXBRWEF2eUlZa3JlU1ZYMEUyTnpTdDNhTllFT0p1Sm9vb2xUcjBsbGZLMmgxZU8rQjhoVW1mbEVzRFhlaG0vYVFvd2JPZnA5TEtvd0lZZUJsZGhtakR3V0tZL2t2UDVwd2VlTFNEaG51VWgyWmIzbjJxMSs3RWk4ejI3L1ZHTW5rY2d1alBnVVp3dzVMNmZPaUhWaGVPZnRhWFFiTnZsMXplbWduSDlrSkNzeWZkZkY1UlR6eEI2dzlyS3k2VlhEM3p6enRaU2VqMjVQYkpTelpSV2lYQksrMlVvQzVaUXFtR2R3aEViL1FFcHk0ZWdqNW5lTXB2UFpXNTF1c3R3Y2VvMEtyOWlMUlczMFdVdFpqamcyQXNkMlpHclB6TzBqY3Mwa2t3Wmlyb002aU1mOXovRTNlaXFBaERMeWJDNlBEd2pCc3AvalV5N0tpZHN0bGtIbkloN05ucmkwdk5XdnR5RmtEVnd5S0FFT2grRHFteFc5dHJDUi93c1o1cjd3eUwrVWdNcEZRWlZORkNTYUtoaFVoU28wWUxpbjB1SXBSNkRqaEdJZEFLQVlTc0pBb3VPd3k5S3FRYVRHeU9JcDhySkxBNXVjaTlydzZmVktzNFJhN2dZVDljakJnQWs4ZXozV0czRUlvbkFVeThRVkNtS0FlT0N6eDA1MXZVNm9XQ1VIU094VUUxVW0zc2lFWkVvSmNnWXl5eDlMcXpsZVV5YnY1Wk0xWnJFUHZXaXNtUWFiaS96T0pTR1lTa2lodHFqSkxEekRqNDhOdVFoeXprM0owZVZsTEg4bzJsTzd0RU1nNmMwZTRJWHdscjRBaW45UEx1VDFpODM0UWQ0dXdwT2dJQ00ySXFOZ3B2TjBRaHNocVJYVWkxRXBGcFJWUzlCNHVsTXdrSSt0cllteGxpVGp1Z1Ntc2RNVEpiRVpSQXNBNzFWWTd3NGtacUZIYWwyTlhJajQ1c1dyd1M1YlBJdEdJcFVBWlpwUkhrallGOVNXMTdNTUppcVV1NW9Dc3h3VUNmTlc2SFJEK1BreSsyTE5rTW9SYzdtajkwczlGNWhTMUF2UjErdndIUkwwblVLOFVEN3NLUTNxaThNSVVVRm1MUXIrWEFVRDNralZtZ1FWbk50ZDY3UVR2TkFxR3V6VnpXVFlsL0FSa3RiamR2ZTUxTHErRWlFYmFIYzJVdWY3V1dJclZKTG56aUpkMVNLYm1JeFlKS0JXN1AzTG1QSm9LcGpxYnF5R0xXOWcyQU4vTnlESWp1cVNUN2RUazBveVk4WkhaaXJDYmM0YXZrWnp6TENldFBHK1c2UTJZdkdPSWk2V2JBbm9FUFA2eVJxbjNvSUo4MitvUU1hUnlYOEtXMGVHQjNzMGZNelBIckltRGRCaTFDWWxSL3hKNVlyQmpGQ1l5YTlOSngrU0IzbXExaGtIN2NUYUg1S2I0VGpRQW1RVmJDR0d2NEtvekZ2WnZBN0Yra2JxOE9mNzRhMHhZUW5PbVExbWRkWlJXSkMzWGtKZ0hOWGl2czB6Z3Vhc0RVQURMRVlBc2V0Q09wczIzVGg2cXdIQ0R2V0dRdDRpNFFvTnRJbTBIVzNJalZKdFR2UHdhWndRcXdrYXZzdFBwNzd5YWdmaDcrMW1STERmRXJHWlpwM0NsUi9mQmpOL1c4V21YelZQM2NBYlg2OGsrOC9EWUlGUWFZaFpYZGh5UXVoVzZHTVFkMTFEZHdtZUpjNE1wV2FZczk5V1lrNUYwQ1VWWUhrZWdhbTE4dXpibldNK05CSEVuTnRGZ05kRzFSNmc4aGtVVldKU1FjSjlDK1BHb0ZwRHVQU1Q4R3RpMGpWbkpkL3ZXbFpJTXhWVXhwWExtRXJKNkxrbGU1TlI2SGJjRTB1ZHk3ci9TdGY2QzBLR294OHJMazJ1RDBIYUdxSVo5S0V6bGx3bEZMNlc2U2tJKzVScEk2bzNHQjNSc3pEQldoR3YrMW0xaHVQeUdINGdjMmJxeFZiSXdsRGx0b3BCUSs1MUhjTmFhTWRqLzB5T2dhQkVlc3Q4TmpIeFRXUUZBWldVWDBOK0JIS0JFUGgwTk1Wc205aVNsUkVLS2tGbTEwc0NpM2YrM05qY1g3Zmp1cU5YVlVIdTloOTF4MHczUERzb1dIdHFHVmV4M3ZQajNRcGR3dWd4TE56SDJFVHphUGxsRlRnbXc3QUFweXZ6Z2lKbFpGS2pFZ2dIUVZCUnN5Q0JBL082RXJ2MW5EM05YUkNLaForMjZyRjVORElIOUlrNnM0Z2pBU05wMjNZSVR5UHd3ZzZDQ1hsaG9QQ3RRTVo4Wk5Bb09JZ3JmaUQvVTJSdExEUzVlVTBRa0krWDd1aGRwdDdtMitjNkhNcU9uVXZjQU9mRzBQTjZhTVBKZmdRcVJxbU5WZXR6c1IrdEpoZ3YvWkZjaGtlU2Vkemp5a25ScFZLek5VbGRVQVdMbFBLTHBKRnNOWmVxYkJPemFmWVZ5QnVLYmFtQkhybi9DbUFiZkw2QVJkRHIrTUJiTjR2THgwaDYrNEI5eG1vUFpMSUpSOTl6SFE1b1FaSnljU3RLeTQ1TkgrQzdLdE1sanhVSTlIM3UrTHFhNVhsTTA1VmlmeEpnUGhUancwLytoM0c0dHI2UG9BWGN6Rm5pTFNKWjV4ano4T1FPKzFjcWoyUml4QTdVRzgveXd3aDdwamJXM21KNXlpOFFpV1gzODg5UW1iRndKV0NUU1pYdSt2ZmxWcXJXblJTMmtyY0tKYkdna1lhamMxUzlvVy9IeTFSb2tvNGpqMzIrL0VXSlQySDRqZFVrZGs0M0QwdFVIdk5vWnhJS0dvZlpQOFJPVFZtbnN5S3huUTg1OWxTUFVlVTJKenAwbnM5eWM0c3g2MlM3U1VDMCtaazlPdnp4RGJNZW45WWVYbkNNL2pHYmw3K3NmbTE3aGx4VFcxZmJiY2lVdDhLeXJSSFZHSXFZQThMZmV4a0pwTHN0TDRDWVVxNFdidll6RUlnQnFEU0hmdnJEbGRDWGJZM0w3ZUhCOHZ5UE16alVLT0x4Yy9xNWF1OUxyYmx5RWlkOHkwOUV1dU85KzN3YkswQ2R5RWh3d2FRa3lFbU5zNU0raERrVjBZcWRJTUZiZWxoTkFnREN0VjF5Q2djUmF2NUVhOUpUbHBnZDM0c3NxZXRnaEpLY011MFp2YStXWWhtaUtXNVoxNXFpMDRoT1ZqK2JuNGFUR3dzUlFhc0t1T1ZaSWVHZnpmVzc2eSt5d1pFKytadGFFbVdGT3RvYWo4SUhTOHJtVzRUNXZwSjllMnRsSFkzM1R6aG9MRG9YVk0xWklaZ0d5dHFqM0EvelR1Z1BCUnZlNEJ1dnJETndub2FrUjg5c1UyckF4Y2JOTnlpMmpRb0JDZzhhSENnRmd2Sk9aNkpWQzZXMFptcHZkaGdpKzcvOE1pRVNFTXVwUkZ3NWlnQmdCT1Ara0JKK29IenpqZ1FZYUx5S1VVV25xMTVZMzUxU1o1cUxuZEtKNUloWE8reko0Nkh1MW9oU3NVRUV0Y3VVQjRUYXAvZDhPWjBVKzhOZFlaWUJrUlhXR0tnV3NEdzNuN2F5VjVqQXVmWU43clIrSDNySVRtazVkN1FZVkJuNC94QkY2ZnR2Zkk3bkI1S3Y4a1VYMmQ2TDVOdG1WZVQxSDNpUlI3akZOeWhKb3pJYUNYQ2wySEhTWk5zNWtXNEFrV3pQU1padTAvYnQ3Ui9vNE9hSDNSTG93MG5uY1IrcGREckdTQ3NvTU9zc2NwbG1SaldCZ09vMjdXajdDK0xnTTJWaUtReDFBWEFsNFdoQnNiOTJaUFJXcEJpdDBZS1R2b24vZWtCYXJ1c0hsdXlmeE9JUHNhK1VkYWpSRmxWRWRvL001dXUwZ1FwRm9zbGp2YmxQVXBxSzNYaVl1cnIyNWx5SW4xREVUdUlsUlgxUEx5VzhwVy9FT3hNdkVlOWVuRVA4bk1BMWJkZ3gzc052RkZOTzNBZDZoYnYzMlBMc0V6eWJUSjdjWVBncm1YMktEUE9Kazl3aDlQZUdUakdiRFBteXlVQWdLbTFMWHJ3RWQ1L2x1NkFjeEVqbWVVNElZUWdpZUlJa1JkVDBteENyWDQ3QmYrSGJlYXZMNHZPWVRlTGdBTmlSclo4eXhYWmNOVlIxNFMxOEE3NXRLMnRUTndFMGRBVXM1ajlZRXZROVA5eExxbUlTR3RnS0d5M2I1WkhTTllBSWdNRFRoc3lGWjFaa3FJeTc4RTFUV2xudkNvTHQ4TEpmWlcvOEFrOURINE1FSXpRY2dnSEs1MmF4NXdyNkVQQWtJcDVqNy9HVmYwZlJOT1hSMEJ6aXVYZHhaV3dHVE83OXNSVnBxMzJtUnNwS1ZWRXRiZ29LVnJKc2JRc3dQd0E1eG9CbE53U0FrenQ4eGRSOW4wOVZoSVV6cGNGMjBockUveERTODdONHdrRHZpU1VuanFiclJqbU9KR1M1Y0dMbjNjU283Y1ZsaHQxQ243L1VudGh3dFVRNUlQbmFwRE95bXBFTUhXYVdUTTQ3L1RXaWx2UUVIeW5iZXdqR3kxblRQZ0EwV0dmYVc5aGVOcXdWakR6TC9Eb3l0SHVLTHNkUkdITUNybU54K2dGcVJKNjFmMis2bkdLMy9xeExScUtVbCtWL3A5dStmMk8wWmJ4d0wzWk1XdDA4dXZvMXlWTzJaM0ZLaTA4N3BTYjlIWFRZQXQ1MmwxZSsveXJNRit4N0FBOHRudGFvS0ltbll5dW9HMVk2NmJ6WTZzczlQRWVteWZBc3VXdmlGM2hQWm9sTWxzTitiOThNT2pMNXU4V1AwMVNVRThiZjhyeCtSR09mZTZmaUVMODNLcUhkRU9kN2NKRDhZSm45Nk8yK05kS3MxUnZ3NURiM3A1aGxiTjNJZDNNV25QUDhML05Tb1J4d0tSQVh0WTVSSW5CQ3hKWTdzakN2c2MyY01TeENPZ1ROa2hJUnlsVFpaa05PS1dTOVdaK1RrS0FaS3RWNjhvdXdHck90Qm5PR2s4SzhHNW9RbGxEUXhUNHZObDJBSzNPT3cwVitFMFE5SHFhc3IxclAzTEMyZmpsV2ZVYXNrZzhVSWZ0cTRHNm5iSEtkUjNlNWM5a0JqRkc0aXRlY3dkWUlnMFppNmo4dkdoYkY3R2lZbWV6aWlyMXptRFhJOVV6Q0hKRmc4RVByUXA0Y3l3QU85c3FkaVJ5R3Q4RVJ5WGY3ampFaEUyU0NZVTJUSzYyeS93UjFEcG1yek00dVF1TXhwNS9CbkVWQnNCUHVTVWIza0k3T1BZWHE2c25hSys3ZUNEeTFzUjBERmsxSWdIbWlmeUorZ3VLT3lVWFNhcDJjRmRyVnRHOENvTDdzaENnckMxRUh1RzR6TWh4aUgrdmErb3pyWGdvMktLcTBqLzhwcXgrdWQ0L1o5S1lsdm1hR2l1OURSSnZ5UTBhNlNNZUl6cHcvTGllWG1NVnNlbmd0bkdZRnFCYUt3YnU1cmFPZGgyeVEzTDM3TUcrYm1CbVpvZmlJL1A1bnIrY0FEdzdrcmdSUHVrdHVka3QrUitJWHl5b29OUkhFcGFyR1J5STBXZ29uWWlSV2dqSkNzVzZjL1d3VDRhNVM1aHdYY0JremRLT3RUTHVxOXBhOUdFcDdieVBJckVaUEsyZi9TcENxcDhMQVcxRTRMNU5CYXNCWkhnWFhWemRQQUYwR0E1YW9LUjVER2xTUTA1L1hDWHZPc2p6SjVSUm9qQ1Q3dmJIVFA0YWIrcUE4VE5ZbEIyZThkZnR2OEtETzV0WFEwL2UyZlJEUGJ3YStaUGtYc0p1SnltQzh2Mno2cElkZUVIaU9pWmJEbzN3cUtxQ29ZZm5ZR2xpVFR6UGZwL3h1dCtzSllQaGdrN3p5ZVhsV1Vsb3A4WnhET3VMUC9DYWZaU1NJRU05M0E4OFJDc0V4OVk2WTlaRkdKUzFvbVlMN2taOWpNR2VmR0pBTmJqbTlGV1dCaFN4UG5QNUhVSHZDV2ZGb3RwWklBajl6aXVFL255ckRVTmg1RCtXcEc2VkJhaUdPanVzU1N4a1p3QkFnZU0rdUNZamU0dGdOcys3RHRwZjZTZGIxS2ZuMER3S0ZkZDN1K3hlU2lkcUlDT0lJUlRRaEpISENQK3VrSjJ2b0hrbWZ3WXZGWG54VjllRGczMGRTT0VSdVNhb29WMWc5UDc1aVdWVUtFc0s2dHAwSDRzWG1WUEJPeUQ0VkQvTGtZTE8xYTdLUXdnMTZEMUNnVmdrZ0E0ZjNsYng5SmZaOHhvU0NzSVdxT2w4ei9MTHN0MGE2UUlpZDY3Q2tPdmlBeVlvOW5rRFV1WHZlZGRuVWwwcDlTMG5uaEg3M1RudTBGU1ZtT0ozcCtBdHRYOGRFSWZxMG9TMUFLQUMydFZBa2RqUkc1Nm1jc09xa2J4VzVJNEhhdnZtK1BORW9CcmExMFZBRnFkOHRCZEFXbnpGa0Y3TFZpY2svOVFOMlRacVlXVi9OaDR6ai9MZ3VRbzFIczVITVVld0Nzc3h3cTNiNlJsRUNNN1RpaWM0WnN0YWp4UFBqSHRmSEZzcHJKZWhwaEJudnRwMlpVQitJTWZsRnY5Ry9GdHIwMU5XNnZsN3J3RmptUDVNMVdHQ3J5U05MTVZZR0lndW1mdm1EOFQ4Yjg4dkpsY0FsOW9ZV3Yrd1B3NnhCckhCTmx1ZExiZEVWRU5jQzRzYVBMK3huaitWQXJOL1hoOEZ6N3RnL0NaSFlFV05ZT2ZPS0FPTndCNngxQU9IUHF6Z3JDZFQ0emFlWjlzTGhDdlh2M2NUQ241Z2J4bDhWc1RBOExhLythdmJadWdJMVRYazFVK2wvY2s1UERrd2lkQ3FJa1N0c3B0bFNaUStsY3JsclI2SmVOZkNiL0JncjlLTURaL01Na3FFejI5VXZ6RHJ4VHBmUlZqMm02TGVnODdIcm5UaHAxZE4rYXR6N1RhNkpiUkxxUUFHZUdrRWJpQWtGbmlaTlFud2xOc0ovbUlxRzU1SXVBY3ZDazJRWkN4N1IxRzNQOVErMDZVV25Rd29TMklObmtGOW9oL1Z5Z25UVGVHalFYTlhHaWVNZk5QenFmb3F6eTlJaU9YVHM2MmF5VEllMDVKNDNvWUR0SkFJVDNvRS9EVjF5NWVPcTBKaHZZSlR5MW5BZGxKOEJId0g5dVRCelBYZ0hpNVB3dUNuZ3duRjljbzB5N0FMbHF0U3RSWExTeHlTSVVhUjZ1dnI4M1BSd01XMXFBZUZ6bE5VMFB3T3NETkNoWTI3VnQ2cmdodUk3bU9sU1VxR3hrRSszMXBVWGNKcjlYd0VBbHJJaDNYNW1FTHdJTVMxZGdUNXEwUGl6ZVlXdmtmTWwvVkVJeGtKbGlLOXk4UUVMQXFTVkl1M01ocmdqK2dqa3BuRlE0QUlwOFJHN2NEOFprRW1pbFhrMDE4TTZ4TjB2WW0vVVZyU3ZWK3I3QlNBVnhVY2ZqUVhnSDdsMlJCQ0pmcHRNUmRGMUZrYkwxUjc2OXhueUt2VVdvZlhVZ2VXaGxidy9pTHNjRTZFL2V3MEl5T0RHVlVFcXdUK0ZXM0o0YW55b2pVUnBwTnZIczZWL2xMN29qbUVrYlpnOU5uVnJSd0MvR1JZMjd5QUl0WUx6MHRUa0ppSU9ZbjdxeEF3QXBuckRjWGlHZ0g5eEdpbkIzWGhYZU03T3A4aWdNZ1pwOXFZY0lRTzhLWHpnNVpTOGVtUVJvZUcyU3g2Z3I2Z2d5SUhqRll3YTlEMzJNd0E0SnhJRkRiYmVaZUhkd1JLaFovaWFHVzZ0RjQvZE96bzd0dTFaQ0I3T1EzVVlQZnhaMFh0bjU5ZkZBR1h6d0pWUkZ5ZnlTV3I0ZUR4WVR2emJFdkNTaXhjcUlteE0yZ0lNaDB2b2ZET1owamNsNmdtUmFYZVVWQkdZbHZwRmJFV2lDRklmTjFoRk04MDRmdHN5UUtPQUJITVBjVWNxSVhRa0FmTkNlSjRIc0IyMHZ1YjBZeVg5TUdnK1FpTkpVckU3Y3dXa3I2SlY2eTlCbms1MS9xemJLNU41SXgxZ2xKTThvN1VsN3daOHR6THhsMmRxTm4zV3MwYll1dC9NTlA0anI5RjJtRE9Za2dmZ2NWcWJsN0FRRW5STlBxOS9MR1V1WUpaL1FQejZXZGthY3VjKzFNMlhHNFpvZEp1VDJLTFExZ2VsNXErU1E0bFFMTHpraHBkcVptVk16ZlhyOThhVjMrNWZ6YkpKSTBQN2trQzU5MWJjSzJSZkNlcHNXTloxM3p2Z2M3NTV1ZUVOd0VJNk0wWGk4cEJaZCtrOVF4K2JnTXI4RmpWcEoyN0srTkd4ZHlkb0o2cXB6VEZzQTZVckEvaW9mdHBqc0ZsZ0ZCdmtnWkZLNGIzVXE3VUJVSkY4LzJTeldTNnYzUXVsOGFTZGdFVk1reC8rQzE1QUNseHY2YmJEM0d3b1FwZFNFRWxiNlRoeWgyQjVuV2I3bzBSOEJiZGRqbWlhVk81N3BXZXpXa25TRm9wZzVEdGMwSjVYU2tuOG9OWE9LbDZoYUwvR2psVVBRUmxpY1FYUi9vb2pXeGtWZkVCR0FVVDJDZmNEQUFyNGhhQWJBOGxlRGdmUkhBMnNJMEkrOXV1SUVxN1dVZ1lmRG0rbDlmbmZBVE9oWndQOER6TXNwVlN5cnJsanhERjE5NUc3Y3NnaEZwWUZRODBCRWpaajNvdTJ5TVJHbjlLN2NyQU5uUUZGQ2IxemxZSFpobmRNMUVRTnpCaXV3U1ExV3JuemZiZE1jSzFhNHNaV1IrWGtab1JQaEZVZzdSN1d0WFFYckJwbTJCRjAwMDVJREpTS3J3ODRCZVFUc01BR1lVYkVVREJtNGg1L1F3MklXU0IzTW1tSWYyamI4SENHYjFhS3EwalVrTHhNcmx5akJGbjJ6aWpMYjkyeVFtUGt4TVpnei9QakJicnFYNTZQdG5pMGFVVjk0b2FCRU9ZVngwLzVyY1NXNHBSeDJ6QU1ZNjJPTTdXRVZ0WE1tU1JsUkhRRWhlNzY4V3BRTjJGTnhMV0FUK0c0UFhmTG5qMjNhd3I3OEtxUTFwSVQ0OENKcHVOZStESnFyeCs4WjhQM1hVdTIrem9XZHBoa0x6eUhia3Z2RkN0dmowbVNIUFhscWlIT3BLSTc2bVpMRWNoMWVSc1JucEk1N3NDeDA0Uk5hOFpiZm13bVhvR3RkdEJQUDFQcUM1azNvT0tlYWExenR2OThiM0wxZnZLMm0zMW5oUTA1WlF0MStxK1grMDF0Vk9CYlVUbjQvaTZTcEZxbURDKy9NU1dzeHRJRzRSZEVHYytCemdoUVo0ZjBZWXhQVmlRWEtqZ0Zvd2dXSWMvcWUwajJIMy9YTWJRR0ZRSFNSalp2d0lqTmxSVkRUNVluQXc3dWhnWnN1NXZXUi95NUkwQnBScXV0U0pvUWZOdDhuSTQ2ZkhhV2Q5S0JXZE40aEZRK3RrMUFlU1RkdU13cEIwUmZXTDlQOWZoZlQ1c1RYdXJwMVJjOW5WZ3N6RFQ5bE1yM1MydVYvSlh3RU95MWxBemJOQUk2eXB3Y2tCQVJ4UFZJTXN4MFNXSmZISmowMldhRnhNRDZBMjByNFNCcFpWelNxbDZRU1FiaXp1MG5rT0RlZ09RYjNnVGZTQXZCbW9uUkJSMjNqeGlIbzNabUVYQmhhZG5QWDNiaVltWkZ5NHdIMnRvRWpiZ1Rmckswd1ZkNGdQUHVhTllMLzNWS2FjUzJYWmpuZExyYlRoZkVVaEVPa2paNHlsZUgrVWlQMkhoZlFsVUlBZXp6dGdXT0g2b0ZLMGkyTTZUeko2NkZaeGs4STU2cHpKNVcxV3pBbG83SDNLNkJrVGZHSTVuNGlibTB0Wm9BdzJnY3VLSUE1RTFQdDZkc2tiTTI5TkFXODROUlVmUFlGWlNNT0VnbGtBTGkvMTVtNGFSZXpuQUJPTk16SWhGbTlSUG5VMlRva2cwL3QxcHh0dm12dXBMQlFWL0FMeWNsNUhQK2tQaTg2OVJIWFUrR2RBRnBUczZ1VVpBUTRIbmF1RGZmSTdrOUY4OThsQVExWUVmcER2aENMM1VkaHlBZWtENW5ST2UwaEJpazFuV2MyQW1Pb3BNaG1HVkpVR1BtUjZNU0FtRFZTaERQRzlSRHg1MGRrODhTdFIxN0lUTUJZTDA3NUpnUUVRdHNEZGJYSW5yTGZEYXBrUEpGcFRWd2d3NVFxMVFMOUV3dFhLZEZINS9hdTlQRld6cmt4SURwQTJzb1NjODhxTDBPci85QnF3YVJjZUhERVBwOS9iVU5NWmpuQVVKbjA2eTg0VWpUU3gzbmNUanJDWnFHbXRPTDRid2w2U0RydnZkMHF6aDlzUHBmNlZzRnVSd0tCZFU3ZDVybXVCYW01ZjllU0RVQkRQZk1YYW1lSzNBdUdOVkFCTUptVDlremU4NEdUTU5sNk1tby8wWnRlU2xSWk5OT1NpYVdhZldZeUI1NXZmQmVKQm5uRVl6dTZ5VDdOVmQ4cmRwMnBpNk5JWFl3aldZRzhwVDMrdmxVRlorMmJPZE5GeE9tSHlwSzQ5WkNtMjN5dzhZbVo5QVM5eXFObkdPaEVObTByR0JZUWlKYWlHWDYwY1FOMjA2UUtaU3Q5SFBmcHRTUERTU0dYMnNCR2NZaGVrb1NENVpsTHhBdjhmaDRBK0xXZmlINFhndWtVbzVoaE1ja01rMStSYS8zQWlnSTNBQkNsZHpoWFh6cXlGcmViOWZZaFd5UUpibTVvejNNazU3SjdxeUErL20zdnMwYjZnRzJrMmtINSszOUtpMlFuaVBKcDkwcjVHeXZWSnUxclFqL1ZLNjdDMm43VmJYQkN1YzJ6OVBJWTVwNlBUUWtTbVZ6TGdKNjhRR2psTVZnRmNJcHg1MEZoSGZ0SnZCYkttU2pKdFppNVExVytaWnpOa0dnays5Mzh0NW15VGZTR3laUkhmZ1VmZ2pqYVZyVnhoQ21NaGFsYWExUXlDZWczNWZuZjBrVDV0OHEvdzlSdEdoUHh2eENqelE5TDVRSnVncDZYME04QUxGOTZzNzBQYksybnFTRnJnQWQ2eWlqbmZLTzBxNFJrcGM5RWFxRUpNUTZlUlpDSHllc1QwaDZrNnhlNVdQYjE1Q2lpY28rRWwvNUFLS1UwU1lMTWZwWmdKaVFpK1hHaUhSZDBTUVVkREdPR0hrUUtoeTJTT0o3OEdGMzJESzZCQ0wraElFRDJOQ1pyMk1oa0VDVDhSa3JGaVc4WVQxVHJheGEyTUQxQ3dITHVmOWdwbVhSY0xDRzQ1L0tpeXNOVVExRGd2SUNBUUROK0NNWkZZRktIa24rUnZzTnpZZHFHbU5zOE1rcThLeXI1SnEyd3ZFVy9SakJqK01tdmw4L2Zmb0VRTDFITUJZZlZaNHpqY3BoaWZjeDR2dzl6QkcrSjhkcjM1blVKRzZlWmI4Rmk0Q1lFNi8vUzNsWmFqSjc5MXZGL2FMSHRtUllCT3llaWwyMmVjeGJpNkJ0elFOZFhtcjNmS2dUS2dVRWpzbTdZMzNhd3RMK0FjcUxOTW5xRDEwbnFPS2J0Y0RHK3N4MFp3RmkxSTRuazRjTDVWbFZ1NktFQzdBaXVxMGJPbmcrM1Mrek9IelNLNVp6R3drOWdhdEhiaW5IVDd2NEdlc1U1UUJzcTg4V1RzWTA5YVFQZWpzYkhWWXV5b1R6M04xeUs4eldmbVorcVB1VU9JMUVyeFJoOUxHamxFdC9heFBuY3JaNDN2RC9FTC9BbGYzeG01c29tU2NXRjF0blEyUm9COGUrWEVRaGIycTJvbkRDeEtmZWlEclcrQXgxckJROFNESm4vek1jQko1VWtuWUxFNW9SdXNoS1pNQm5aTEdib2V2ZUMrZk9nOWtnMzJuYS9nQmcxMndSSXUyeXlXelpNMElvUUZXVHYxV3pKdWgwNUZmc0Y5bmIxQmNHc0Fuc2VmdEFzajRYQkdlcStjdTJONmsyNkt2NCt5Wm5xR3EvamhOQi9VYnZOTnVHM0dXMmh5M1NUVk80YjJCSmVONjJTK0FndjVvM2t5ZG5keWwxbFhueExVSEhDdHpSeld0UTh0Tm1hRFpRWG9WUjh4NjNXVEh4aEJ2M3JHZTJtcjY1K1pMR3hQeDB0Z2k1UmhoVzhrN1lNaHpBWE5QMHFxMkN1bWVacW9JZ2RYekNaQ1VFU3k3VWRDbjJmYVpjQ2pPYklRMEJudjBaTnlpUXBlUFFvbzg3T1FMMjdjK3Jhbkx5MnlRNEhDM1JuQUpzSk43aVN2U3B6UnZzQ0pCZm1SUVVWUW1YeEQrZ0NNWmxnSlhiZTZGbnF0NVFOb3h1d2lXU0hhTlJzODVMWHFBb2p3NGI5TTVSVk9OZDZNa1lsREw2bXR1K2FBZUZ1VmtuK1A4MFY0RS80ZGx3L1VZN0hDOTZ3QXUzMnZlaWQ2Sk9TKzBaUkp6WXV1dW9HWjBpczEyNm9UQ2hmbjgyUlRlckdXUENNUlFmMEFUUTdtdExZclpFNEhNWEpyRWdrR0VTSmx6ejlha2xHRXVSWlo5Wi9CbmtWRWFDS2hSR2h3b0gyUmdSOWlBdW9kK3owczBRdWNUaUltN0xyc2VRWEF6RzFiQ2M3anZCVCtCMExzY0ZLeTJIRnRRcXltcStwMWdpK04wdVA2MVdLcGY0MUNZaXRQRGxnNDZWWDJYSEZRaUtSaUpKYTQ0eENNZ2pvdFljamtzSkE2MXExR0krNjJKa2VwNVB3OHhickRzaW5uL2pYYlU3NnF2cXY4Mm5hcTczR2NiMU1yd1hadUhiNzR6ZTN4d2I2NFczMWZhQ3RuWEZaaXlwT3ViUnJZdmlreFhHci9NVHZaNmRoZ0JLZTBUcWhONU04S1VKM2JVYkkrZXZhVTVwTDg4Z1V6VmFVN0gwK1l5a3ZHNDVYQ21vdFg3RU5obC9wSHkzOUE0d0JscFZyZDlOQVA2dDU5SkQrZWNWb1VVY3RKNTlQejRzZVJYMlMxb0IzNFlIakdhbTZ4UGwvVThmM2czSzQwUU1YejhaV3pkRzBKOUV0L2g2dE13SmFzcStaVTlORjJ2cjVMNVNKTzJ2cGRRQkRvOGdoaXk2K1pHQk1NOTR5aGNlN0hBWW8wamFjL3U2QjNKdXYwcWIxYklTYlpibUFrS0QvMElqOXpUQUtCd0U0a0ZoNHZkVUd1U3huRnV4b0dmMVMwVEJMc0FaK3Z4bW0raW1oV2ZXK2k2cHJKNmM2bkZtL1hjN0FGVzFmZ3hlVlBaNkxrdVhtNnFPeDR3S3YvT0FNL2RnRFlQSVV1ang3eCtrUU10QlpLNmkxeXc5UVQvTE5sQXR3bnVLdWo4WVRvVG9zY2N1RnZxd3dQS2VxTmVmQitsR2pMdHlXTVR3M0RMeVQySWo0YmhmS2dSVllRV2tOQkJWUXJCYU1YaHc4V3A4d2ZiVXIxZGNGd054TDJHZ3dXVEljK3BIQ0dxbjQyZ2ZtOUpVTncvbjhiMFpVSG1ucEpOdjJDTkI1MjB1Zy8zUlpIL2NmbHpjTENjVU9vU0tQL3NSRC92SXNxRS9JQWJzTWdRRlpVWlVjOThadERQQmo3WFBHTHdvckFib0dycWdONnlDb0hKYTFRMEFZeWZ4dHl3eE9Tek5idWg2Q29zZHl6SlFudERsSDNkQWFFVXNBSWFXR2RkNllDeWlVR2FwSysxbU1XSEFCaU5LSkxodDRkbFlKOTZ5bTdxRnR1NUNWWnVUSnpNZzlaQ0hMb1IrVHJJaTJPUTl2TUFuWFhsck5XWXdDUEhPcFpXN1VoQ28yMFJIUHFWQmU1UDRMOUE0UDI4amV6UEgzRlFJb1VoWjFOUUxFdDRoM3BpV0Y3a05mbUdlSzZRR05ZK1NFOW9UYTFNdnc3cFk4NjNFbUhNNlFaSkU5Q3FMdGY5aUlJdHZXSXRYb1VFZEg0ZHVtWUJFZFloa3J4ZTVIcW9RUkladXpUNmxyNkhieVFGblRjaGdDUk9EeTVMTGhLNktycUhGd0g2RXVDY1JaUTczc2hDVkFOM0k5ZnMrWVoycXVtdW1oak5jMXJ3cWkyekt3UDV0MzljV0p5UGJyeW8vQTNoaDhaQ3oxS29pZVlGNTB2Vi9nRUFqcnVrK2dDeG9uYnNtRnA1QTBtNVZWRTllNC9UT0ZWR3JBcG1TL012a1N2UzIxejhFdXgwK3F6d29VdFBGaWRhR1BHVHY5REU2d0NFRWxOTUNZY3BHYXFBNFlzZTBDNzI2d1g1OXFtQ3I3dTcrbUtLSUxnWU1zTzdMQmJKSW1FRDhyNDh3ZGtZUVBBdnlqdm04YWtaS1FsM3AyYzRrb3VYTTVFeVJjS25BNTAwUDFIOHM0QzdUVThQMmN3QVlrYTBhK1VRTm9DUkxzZ1Q0L1dwT0pWUUQwV1g4a2h3cEJlMFJURHprUWFlVGNyZ2JzTUhiZWpzY0x1Rm5UYVF3U2tKNUZHajNJaFZrWXYrQUkrNHVyVTAybjhPdlNQMFZsZmxvYm44VkZQb1JlZG9tcXB4L3dRbWFFZXZaQUtjMGR6MjVUcEFYTDZkV1BwZ0ZMamQzWTdaNXd0eFU4aFJYZ3ZvZEsrUnVQRmZib0x3KzhZL205SFNWSGdmdXF4M3lQUDlCVWlBcHFLWDI4bVdrSWtuczN5Zis3SnBxYzB3MG4xclZISzgwNVlLTHk5cWlMZDQ1djZUVVNhTXR3VzlKOEVuN1U1Zlhnc0x6c0tqUEpvWFd5NmJnUVlyMGxXMU8vMW1XOEwxeENYbXVxb2ZEYXFmNkFzdGhWYll5ZmNRNjVYdXV1YS9HZnFqci8xS3lMTFJHMUI2RVBJeENkS2RSeTN0R1c4bS8wT0psMlU0aElvVHFIOVJ2VEs5ZkVMRnM4R0pCWm16M0J4d2Vobm15b0FhTXVzL0tDdFUwUzJXSVpXbzdrbnVuMitTTWJzTWNES0E5dUVqUkRxVUUrNVkxZG9UQzQ2Zy9vM3p0aGVkVFZWK1pubno5T05vVlU5cUhqRXBrR2Q5SGxad3lML2NFSng2ODJibEdlem9ISGtUVUMwRkt2MjhEVUNTQ0ViQlBIb0dsTVhFUkU0SXQzd0hTaExBblB4VHBMQWF1R1ZsU2t0KzRwSS9UMnE1YzJoeXZCeGVOVHR5eDRsWGRhK3hyS0lNcDkxakZpSUZ0WHFIb2luQTZyVmtnZGZsQ2F4aUlESWx0YjIyMGN0bjM1ZzVVZVFHa2FJbUJyZS9Vd1NGeHVOcS9ZQUFQQmV1ekZ5a00zQlY5NVNpVDdrL2cza0p3VjVRREdSRFp3eFFsTTZ1WExvUlZuT2R6STFGODRiQ3l4MVlaY090em8zWDR6REFKRVdib085TmdCbWVGQVpuMEhVRTRuTVU3YnR1TytVaGovYjhZQU52UVBZRG9EelRBZzUvZG42VDZwRGJBRW5rLzBrNDNkOFNiTHpQamRibDM0VlMwVTBzTlpNUGpuZlk5ejRwY2FOQU51aTRnaDRINU4rTTloeWRVUWNleUU2V2ZGT1ZpbkdVdWFJSlJ2ZGpqQ3YwYlJLVkpjQ3hHbE1CeTI1cUExK2lYOEpmQ25pNEY4TG1SV1dKYkdUaG9xZGRqeHJiS255SWc4aklxYXY2SlVSVndON1B4c2tlTWRqcVZVcng5bnlhMWV1Q2puSDA1ZlR5dXhMakdIOUJuSWR3RXpOVmtQWW5BMTBMMjFnTHEyQVBMeEVKVHNvY2FiZC8yNDRQVS9ockhPTjZBZm5VL3IzNUE1U1o4c0dFZytCUTgrd0RXT0JPcnVZZ3ZtQVZkdXZxRzlnQmZSWHlpcURFclVXeWxVWTBkRElyTGp6WjR1cXpQOXNzL1FMQ0k2emNvajQ0N3FwK2gweHlMbGlHRUNMTDNXdnlnM1l3WTZTZVZITnREOUtqMll3S3pFY3o4ZWxJdmx6azJnNkZtOVNhcFlBNWxjY3k3UXlrQ0E0eVB3MWZwa2JJYlVxemd1cVQzV2VmUTFOdFR4TldSSmlnQTMyYURnRlh0NDIzVjRFQ3FDc1ZJZkdlMW5YVzM4WGpGcEN5TU5BR29ZQWhVS0gyc1dQdDlva21KMVc5alhXWnBHZmgwcWhVTjZhMGw2SjZYalhRR3h6N0xuMEQ2Z2o2eUlnZ3RNRFJtN2hSTHRKYW1hQndwUWg0cFh1NFY4ZDNxdkJseXFjblJHNkpTdGlwTGZ2M0YyQmo3NjkxeHMwYVp1SVgzWXNjc21tVHBOZWVUNmEzelZMZmVuY1pOSXRMbnRWditSUlhnaVBKT0EyVFV2MXVjb1VYanlnM25aN2dza3lEZ3BUTHZJSm1WRVdWVXdSVWNZa2paemQzNU9RcVhkai9aSjRxc0VuTzlta1piOWZ4bFB0VDM0cFpZQnRKclhjb0g3cm9mWWxkb2VXZkUrOVloN2tsNFkyOHRJU3Z6bkIrYnZYUTlpWnZ0NE9uL0tLcHF0UTM0ajBSZVc5b3NEeTNpK0IwNGpmbEE1YklNYnhEZnpCS2RreittL2hQOVFsOUNyN3poVWFjK0l6Z2pqVlpxa0ZlTjBTRzM4dTloZktRRC90bHh5Njh3T25DRzJNbzZFdFRZcWFhNGRCeXVmSWt0cENyTFpzQ3c5UitzV2NEbzlQRForVGkrdjZOM3hHVDVFL0x0K1E2c0lCcDU5RjZEYTZuOVFRUkxYWEMweVA5VGxQRVAvTm5POVlUNGEyQnZENkl1Z1JpMmR1RTlhSnkwM29Nam1CMWVHZW9QcjR3cU13dGNGdVE4Y090bE9KZVNvSHU1dy93VjhpNS84TE01d1dTWWF5d1c0OStpUm16Y2JzMUNTSDJtUDFVdWtVZW9pM0hFQncxVEV1S1FMWW8rdDlhczZjTmZHZndtUjFabjFJK05WTVRLSXZoOE5mT2pKTm01VSt3R3N3MGJQRDRKUW8wZWNUcDNOc3VBenhBMTg3cjdsTUp6c3VEdXNtVzAza0p1QmZIT2xhY1h5cHhyRW1PNG13alVtMlhxUEZTTVdpellBUy92d0JlSEhFSm1vczU1by9uMjlQaDE2b1k1dHRUSTRkdk9tRXdDVmdZYnptZGc0M2xGdVlpaWhHazRveXZpWmE3clZWUmF5ZU92aHpqMnhHTUZUMFo2akUvUXgxUjZTUlRXL3oyZGl4QUQzcjdZSE9ROUg2TnBvc3U5dHM5bUN4Y2pETXI2c205cG1lYkpTcXB4SEYvbTNqMi9OUVRLdVU3TFIvdFQwdnhWbHl0VXkxdGh4VXdKSC9OZ0NFSlNkM084OUhGaUI3eTZHMjNFQ2IrTll3VzRDYm9WS2dlZ3lic3BZSkZMem1uNllaWUNjVWhFTWJnZG9XYllNaTVkenRZa0RTY1BiL3FLZWVuc1BPNFdVNDRENWxZR2d2c1Y2VjltYm5PNFhpREEySW1VeTFtTEdhWVh6TTdqcHg1TG9Ya3R2RG9OdlRwUWp3MFBRWDdOcmY4aDJIWU5vK1h0MzdSVTMzUStZQWNRMCswMUNyc0NjTlVTUUxnQUwzNXhsbjR3dHg0SlVsNFRZamF3dEZpcDB3ZklCZzRLbjJSRmlUYWVvUDhkdWNKbGVyYmpKVHVtVWdiemxjQnVySW5rMnNMMEZvVGYxV1RkMERLNkxZanFyNWtMaVlaTFFsU0xYY3d1aldiNU5FcXlVcmdMbVM0WXZNTDlVdDJmS043QktsQnB2SHI0czdQbzZPQm8wd1Z5aEpTRGF6aXJLbWs4Tlh2YnhpVDZyUjJLMXVHclFiOUwrSEM0THVkc05HVW1FZWxZTzFKdDZGdkNLRXVJRCtwc0FGbHFteHlwcC9lYmQ0OFJibnozdnk2UkV4QWRIbi9xaFd5MzRpbUVVaTZ4OVpYNFVYKzJxWkNQWWhMdS93SjNlSHM2SkF1SWdtdWs5R1Zid0R0cE85OVN0M1BtelFvRDNLZk5rWVBvUG1VNUd3eHlkOVFLamRONk11TWYxMVp5bkFUSUZzSDcyMUJVRjdtSCtkRWdxVE55T04wL0RoTlp4M0ZYMndiWDFLWElmQ2NnbzlydXNtcTV4d3RUbXB0NWlNbE5HQ291bkR0SEtORHdHWnorSnZPY3NPTE01QlE0TjF4VEU3Q2lTRlhUSVk4RlFMVWNERUVqazlmRi9ObGp3aENsTEhWYXNlbEF4L2ZrYndJcGRnWDZqT3dnaHZaa1hHSWRLNWxuSnphekhuMzMraWY5NlZlQkwwc0Z3WW03a3lURk13L1Q2VEdTMElVNm1vRkhkdnlhWWVNcFEwc2ozNDFzSlhYN29jeHcxK1RDYjFsTjh2SjE2NEI0MWJETnpXSnh6aWZwNVlBZ00wUXFqUU5iYmVjYk83aEFFbDkrVGMyU0Q0clp4d2tzZ09Gc3dOSkphdG5aQUF3dGF4emtLNyt3eXAzSzBMd25ocVc4bEsreE9NSXNRT0ZsTTZOYUdxUkl4WDJDcEZDeVZUZUtvRTJmZHRHd09Memd3dHhVUXMyeVpNRHdQMlRhSlhETmVEZHJjY2l2NlVKbE5IUG03ZGs5K0VRbllkYjdEa3JBTk56NXQzS2krb21SVkYxaDA2U25ON3dybFFScWJZQVl4aWxDNnp1eGc1QTVndm9uWEY0ZnBNbzFNVkNPUHJmQ0VHT1N5ZEs1eXlxVG52b0dLM3BQa3JsUTIrSVZQam5rOHZpWHFYcURhaU9JREF1NS9IQWlMVFBrT1dDZU1iSWNxeUtIWHM4SDlrRTFQU21sVkNoRkgvVGp3VXVEUi9tMkFOb28wN20wamY2TVNtVlZhUUtPa05QekY5bG85a0pwdlVHR1J2d2lINE1TaVpHTUYvQysrUG9MbFZkS2tvVkRpSXUwUVByVHlSOE1PZkxhVUZkOFozM0Y1MFA2dHVwZWZSd2I4WkE4UGp6S0ROWE1PYVYxU1ZHR0p5TzZrZGQ1SWs0SWQzL2wvRHhiOWg1RGt4MFlzY05WdURtOG5Qdi9UK0J1K0QzOTJoNHJ3Tm5RcWNvQkxTMlQxc0lha1J3YnIvWUkwNHRTVUlYNVFxaTlNS3YrNWhmUFFBQVZ3RjRKdWthbTFJdWVqckwyU01jcUdWY1hMMGRMU3N2ckp2VExkRUhHR2JUVDVrYXZ1eld3OTlFZHp0TGgyZ05xKy9JUXFIbzFWSmV4cFA5Ly85a0FBUUFhQUVBQUFTSUFRQUFQQVFVaUFnQUFBQUFCRVFHUFFBQUNBQWtDQVFFS0FnQURBQmxVYUdseklHbHpJRzltSUVacGJtZGxjaUFvTlNrPTwvQkRCPgogICAgPC9CSVI-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT43PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MVo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-RmluZ2VyPC9UeXBlPgogICAgICAgICAgICA8U3VidHlwZT5MZWZ0IFRodW1iPC9TdWJ0eXBlPgogICAgICAgICAgICA8TGV2ZWw-UmF3PC9MZXZlbD4KICAgICAgICAgICAgPFB1cnBvc2U-RW5yb2xsPC9QdXJwb3NlPgogICAgICAgICAgICA8UXVhbGl0eT4KICAgICAgICAgICAgICAgIDxBbGdvcml0aG0-CiAgICAgICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5ITUFDPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICAgICAgPFR5cGU-U0hBLTI1NjwvVHlwZT4KICAgICAgICAgICAgICAgIDwvQWxnb3JpdGhtPgogICAgICAgICAgICAgICAgPFNjb3JlPjc5PC9TY29yZT4KICAgICAgICAgICAgPC9RdWFsaXR5PgogICAgICAgIDwvQkRCSW5mbz4KICAgICAgICA8QkRCPlJrbFNBREF5TUFBQUFhK25BQUVCQVFBQnI1Y0g1Z3djREI4VUFJSUFBQUFBQUFGUEFFQUFEd0lBUUFJQVFBSUdBQUVCOUFIMEFmUUI5QWdGSFFGZUFhc0FBYThtQUFBQURHcFFJQ0FOQ29jS0FBQUFGR1owZVhCcWNESWdBQUFBQUdwd01pQUFBQUJIYW5BeWFBQUFBQlpwYUdSeUFBQUJxd0FBQVY0QUFRY0hBQUFBQUFBUFkyOXNjZ0VBQUFBQUFCRUFBQUFhY21WeklBQUFBQkp5WlhOam1jb0FBWm5LQUFFQUFBQUFBQUJxY0RKai8wLy9VUUFwQUFBQUFBRmVBQUFCcXdBQUFBQUFBQUFBQUFBQlhnQUFBYXNBQUFBQUFBQUFBQUFCQndFQi8yUUFJd0FCUTNKbFlYUnZjam9nU21GelVHVnlJRlpsY25OcGIyNGdNUzQ1TURBdU1mOWtBQkFBQVVWdVkwbEVPa2RTUWxRd01mOVNBQXdBQWdBSkFBVUVCQUFCLzF3QUUwQkFTRWhRU0VoUVNFaFFTRWhRU0VoUS81QUFDZ0FBQUFHdUxnQUIvNVBmRXpBWkx5T3RIQ2pYR0ZmenZ6WlorZjF4dkU5cXc1Z01zVWtnSTc5WDB1LzByVFVDY2RNYXZwZ2xtdDExMUlXd2V2VUpYOUNBZ05lQVUyK21XRFVBMkNXK2FHUS85S0F0Z1BGdzk0UGFiTUlzZldNdU8xTkNrcVpyNXc1SFBwNUJZeUtBOFZDdWVTQmkzNEkzT0hHQjMxNVZaQ0UzNXhlL0xlNzhDZ0FCVCtsWmFIMjh4RXN2d3BRWVc4ajdhWGxYWFJ2UitpdzVzSis3SVVBM0U1bXZmSTFjWFNOSHhVaFlIM3dZVzdZSHlldnc4R1g0NnowNVJrOTBZQlMyUHVwdVFhRnJSMWtlc1lqWlBheDNra3VwNENKYmhkT2ZxZGFxY2UvK2ZCVlNXbFNBSitwdzRRM1RjZzY3clAzSzRiSHBvaGI2aEphQWZxaFpSVkJPeW9XZElxTXU1TFNhMjlUazRRQzVrTHdRYllJRnFYZXJKOGpwU1ZvK0dSZTZDV1RkSEp1NTA0dXFMamdKV2ZSK3Vua2RpSitMdHEvQ2NJWWFrSjFMN2pxMUNmNHM5K1hKeno2Y25hTDhpTkNacEl3YU5NQ2VaZjV1SElhTzRjREpqMTZIMUZaS1lXWXdpdHErMmNURVJRQ0Q1cEtxc2t1SjFYbjZxSjVtbnlTWjUvUHd2dllBR3VsanpKcjhZNytNZGZNVEFQWmdvcU0rODhSWFQrcE9WVTZjMHVKUlV2WmdKMEhLQjRzR0QxWVUwRkQvY3BoZm01TEx2ZVRQcHVNYUN0ZmtUeUpyZDRja0orZnhsdXdJZ0N6cFRPVTdaYlJwQjJXS0hFWjRFQWtIMHh6QzF6aElDYzhCYkRlTDhIZG00Y24vQ3IvQ1RRdm90dStYSjJTZSswYWphUExMU3ZDZG4vMHVXQnR5b3M2emNSd3FFYnBCcWsyR2JVcHlsMFFMT2Z0RE1aMERqUEs5MHR6TURXQmd5VWNvTjhrc2hkU1RjSVZOSm04ZWE3TCtHK0Rzc2xNSGZsOXlnU0Rxd2lvOElOM0FEbitsWk5UQXRoWXdrZVRzTWFiaDVmMlpoY01LY3BFVEk5OThVNFVKNHdvdXlFOWIzS1pxWUdTbm11STd2ajlWUE5ZbjZzeHZRS0ZxQjVhM0F5UVBSeGpaTkFpbm5BODU4OW8xckJDWC9pVVlHaFQ3QU5vSjArYkd6SjFBaU1WUi9XVThPTUE3RVphQTRnRGM2L3FuVDU4VmRTa0NrbE9yOUpEclpRZzkzRGtlYXZ1aDY5enJZS2NlekY0Q2MyT2daQ3NtRTBhaE0yY3M2ZVkybDkwaHZnN2hiL05LNVZ1VXdMcGJTZVpGU0pGWEF5aFVWbm9XQ213QjI4bkJWdXN3bWs3b1ZNdDVWWG4vYjA3ZzBrdlViRGl4cW54NDVJZ3FQWG1rR0F2M3V3MCtSVnloM1VtOFJiM2pPNDNKa0UvRThoWXdiR1l4VXREZzIzb0RYVmVhNGkzOXhmVnlReGd6YmczSUJqckd5SlZoYjU4Y2NEdzZzVXBCbXZRMmx0UXk4S2IzY2Q5ZldHNkpQcFk5Z0tPZ3B2M1pwaWgrZG1jTDRrTkEzNUVxOWpBc0NnUkM3TGVtaExYTERzL0hyb2R6dC84dldXUXIyR3JaUk9YR05rWCtGN0lyQkk3eWg1OG1weGU3NEFvenNIdjhNdWJEM2lBcU5qc1FsL2NDemlUZ2pEdW0yK1hYWmU5dExHZW5mR2xGOGhaVjUvSkNrN0JVT0RCMEduOUtpampQcEFweUlXdGtvT3QrZ3RNWi93WVpOa21paE45b2docEt2UDQrOENPbUV1clN0cHNLWUU5ejVuYmtlQ29wMTM3L1NzOW00TTRBTXVMSVpkRGhvNWdNM1BkQkd1YUpUQTVkTDNCVEsxeXJ0MHo3RTlnNVNtNWxOM0VqQVI1SVF5eTNqZGJBN2lzN0gxVVNjVkwvR2I0NkE1bmRnY1N2MmNBYkYyWlpreXN0TzFjYXRIZEdCV043Sk9mUmZmODB1VVd1ZzFNdFNtbW8vdjZhcGRHakI3b2NlTWREd3Q1MDZJeVo0WWpjV0FQeFFENHhobk1GUHZGdlhrR29wY2hXVi9VSE1tQWN2VVY3bjBwbGZlMU5FQVhjMSszZEMzak1xa0NML3hWVG5yVjFhWTltNERwQitvbVZMcXdvb3JzbnhXT3lPSm96ekd6OHFhL01uWDBzS0tKaUxVcHRDS05IMjg3RmRBdjhkY1lxcENMTmo4Y0VJWUZFUjFNdU1NbnlRaEx3MXhYNnZKdStTcjcwSUE5citrcnBDeUdYL3FRMUI4bkhhdE5Jd21vaXhWaitOc2dhQkpxV045a1AxUEdVYis5ODVpNU5JdVRIdm1wckVuSGRQQnFQeEd6c3FFcDNaUWc4STR3UmtQRmxMN0drc1Bkcy9IUE0zUTlUT2FQVi9VenFvUEdvZi94bVNoRlZVRFgyc3FBYXIybFU1bkdpcnN2MWMzR3lseXFvd1UwRVVLeGw3emxMSTY1UUUvU3VsVkxhTlEwSWY5UGgzZWtaZnltVTFwbWIyZURqZFliUmMwTGkzbVhsdjgrTnFWUWdlZVozaUowcFJpbm5XVGN2VkNjMFI3cHhTUFFMdGRTZEVEYjZZeDlMT2NXR2lZSnF6V21sMzRZdE5XYThnK1UyKzBsTTJ1dFdMT0ExNnFSc1RKTGpGZGYxWUhteTVTMCs5SnV6eHhGRjZXYnZmaTE3QkRFK2ZDU0dqU1lLaGtwSGtubUVGUjRJM0pFbzlMWWtDYWY2SUtUTjFGWHdxYW9URHNuN0JCbE4zSGZsc0pZUDVGY0pQYkk0UUVvVnhONzBtU0xvU3hsVEZ2Wjk1ZUpEQ1VzbHRDQXQza0FEblM4dXZCSDZ3UHJCQ2lYUEN4V3o4SnpVVkpFeTZtanhweFFjZlA2Ym10dDRQSTVJaXVGeUwvRDJndVFyVEdBbTZScWRTUUdjd3Z5WUdPUWVDVnBURStDd0FiVmx3ZUpXTEVpOG0zMUlhejA0OW1wSmVTUFFMQzhEMktlS3d3a0t1Wmkwa0FzVjVZeFlCazRaN1JlUkllaGhRangxWXNvQ0lLOGExQmsrS3BoQ1lGWjNXdUZFMXBVU2NWaVJIdEZXWHdWVlR2dUZPM29rR3ovMnk3RW1kRlZEOEs2RDZpTnpYTFRMZ2lPMFkxamJEdGN6SlVWV3hUVkhHS3I1eTh3MVNXNjFTdDhIWUUzSkl5OCs1ZGUydkdsMGJTMit3b3ZhOXJ1aitaaHJoSDNvRHhLS0dzdFEwR0ZldS9zVTZHMUt0ZkVqZXpLLzJwOGZHM3drRzVHazVKZlFSenoyaFRGVGVEdjQ0b3lHMGlpcmVlRmRkTnk5RnFmeTRHSnpUWUxFczAxdFhNa2FtZ2UzL1liUlhyMEZmOGpVS01kU21HUDkvamV2NldVMWQ3aklSS3lkZmNrTWM3WnFnU1FPZWtTWDV1SUs3a3VSRGxiTTE5U1ZzRzBOOExEVEJmZTRLY2x4T0EvWnhrdFRmcWNqV3dvRU5ZUTVKK1BpcHdXV2h3UE5pRXlhbkVaVW5tNVFReGgzMVFocDBoZkVTcWorN1BsZW1YS1htQ2lvUHgvMnRQdlJqaCtrUUp3SUVOdmswVkdacDJ2Q2ZYS2N5NnhTdEpDWENMejRpL1VKQ1dTTkxOSnZJQkVJNFJSc2hIOGpjQjM3T3JFVjhaNWFEZUZTT3NXSkdCMFNORG5ycEp1aFFoMnZHUk10bEh0VGM4ZHBnNVNucFJwa1kxSUpJUTlEUnQ4bWhTY0FHSzFBSmpZOW5iMHY3N2ZmZm8wRHBHR24zbmF3c0Q5Z3ozNWFuQ2F1VXR4alVuRVJpVWJtOXAxeHJRY1Y3Wkc5N0xqUEpPRU1XK25vL2tFbzlsbDVIa2ovUUZJY2doNlFldTNqRjlmbWVrY3p0NGhqN0JrZjZSb29VcER2cTNwZFhrM2svRWVOYUhuV2pWRDhiT05CK1VCREdyL2JmM09pQU5vVWhpQWttMndiaUN1VGE5bEY4c0ZZbHJzM1pwTEZMdWl3M0NjWUpRQ1N0MCtYVUdQNzZSdTUzTlR0RUxremxPT2VUc1ZhZjdGaUxkWXhua3pYamFhVjNta2hjNVV6elZ1U3d1ZTZkU2ZjV3lYSkZpMzFyb2g0Tk1GZ1VOTEdkb3B4d2g1bXlWYzNZa3ZCNC9XM2VHYjV3WDlWeTRIZ0lkN2YxRXI2ZHJEWGQ4UlU2NkNQZkRsei9mMUVPL25DUno4bm4xcVNMSHBCMkNPOE4yc09UcFIxQXJsVEpEcjViYkorQmlhTUNtYmVieDFOS0owY25sKzYyT3dqa2RtRlBzL2ZQbGpBR2dEcm1kWnJKUkhZVjVIMmFYd3pzdGkrUS9FNkZ4bzhia2R5OTdndXIzNXRSclFDdElUc2Naa3dKUlpVTFpPVDFIWWVWcXRrdTVEdWlJT29ZVGtyNFFVQTNCR2VaTnFseUlRbkw4UDhoZEVrVy9qN3lWc1JTYUlzcG90OThrS0ltZmpLNmVhU2RneGNUditBQkRUNWl0RUVpNkQ2UzhIZkhNOHRMeHh0ZWN3ekdHNWtidEFqMFA4QTA0Y0pGdjdpNGR3Smt1NDBjb1cybmxlZkdHWWFhS0ZyeGZhbFBldnN3cE4yeTFjemVQMFdVZ1UxQnpEY3JwSENSR3Rncm1pamlmT0tlcUJ3UTFBeFhQajZBVW5OSHhlbTZMZ0FkcVhzNVFLd3o4M2FGWllBVkpqY0JrZUxDVWxPSzFZb2pZYWZNYUc1YmFOYTh6eWMxQWZiN1NlVHpqTUEyeDg5Y05CV3VXenhtYVdXSDFZVzkzRG00blk5eUJwczhGR3ovdk1Mdmt6UjlCV3ZhMTFpbVV3a3c2SVJzMmhJeG10ZjFjUk14bTNIcGNPQlE0TlBDeUlUdmgvMFpSbkFyaHVjNFZGMHMwdVRTU2FTTXViUXJGWFFsQm1sc2VvcXp4WFNab2QrNWUzWkVxNEJpTVFUeWNtc09DNHVKU21ZVGJxdG9lcGFJMWErVXQyUG0wRDRrczk4Y0F0U01PcVFwQ3JjUTcrS1pTMmxONXFBMjdhazZFSU9JM2EwaXJ2V1EzbEZJUndGbVRCcFpGcnY5U2JwQnpZWFVnM0NIbEU2N0JraXVpVzlMUTVqd3hqb2hkT0lZUFYyVkx1NmFaY1E4c3MxZ1ZtaVJNNEJFaGpFVk9mcjFIYkxJeklTcGxmdUo1dXE2R0Rzc3hhNGtYUHNBYldnYmRVdllXNVBaU29ydVc4cE9rZDd1am9MQ2NUQTlSZVY4Z1gwTlNub0RZLzh0NDlVZkxYWkF5VmRCbzlld1RDZkw5aEluYWxZd05peHFNdXZoWTdiU1RTcEtIWEVDOEdDU290ODUrUFJPaytlRllXUCtYR1JHVS9oelVrS0hUR0RkcWlQMHEzdTNsaEk5d0dTUDVJT01nNU0vMzBOM2hjTzkxY2k4UVdWNS9hald2b1hjWm9La1psMFhUSGFONjk2ZnpXVk1jOUp6bGtZZTBRemtveGRiTVcvSEdUS2RuRGZSYTRIaGpxV1dyYUVETEtIZ3JXMlVZYmZEcTZkenRNcGlZWWRKZXd5S1VnclBTeDQzaG11SXR3SGZvTVVwbWVzR3l2ZFhtYnBwMEdMankwUHpkZVdLNURseTdVOVNWTXVmNzg1VHUyNXkrdk5mamVxbURMaklaNmw0UUNaT0dTbGFrWUpRdWdUNTZadUI3OERnc0ZlVnZ1NEFTNlFmVjRnWU1URkRBMFNjUjlJSGlLdUtrVUYrdVZ4KzJ0T0J5SStCTFFseVlpc2w1ZUc3T1V0YkpCbGFHekJBYVBXVmc4dTIyMmhZZ1BwZlNPc0k1VkNHUUNQSkxodVQySCtwN2dTdlEwbElNVHdMYzFMMkZwbWtCQjgvRWNyN2NZSHgrQjUzYXU4ZXZtTGJZUFh3Ulp5UlJab3BIUXBjR3dIcmw0cDNBb3gxZkJwUGcvckZFRTVIcEtHTWM4R2IvelBXYzBINGxMZExDbzUrTkFMeTRPYzZNc0pqOHBZSjQrTCtOY1pQQ3pFTzZsMDZDWlRTOFF0Uzk3d1FDa1BoNWxUYm5WeEFNMHBsS0ltUFFtYWNlWFQyNk9OSjYxT1RQUEticEM2c0cvME52WXlEQ3BoT1RyMjE1TzZVL04vQnhIQit4bEpONTV6TTJBZnQ5ZXpFbWFYb0czUXN4QnEwd3k5Ny9IVXhYaVJJWGdVaWFEOGNqUXFYcmJOZWlkeFV4N2tDeVBrazlxUkM2dGRvWnVNZGpwOGMvMXA1YVU5RWpUdW9hcFZSa1hPSHMwUjRLRkZmZjJEYnJibXNtLzVIUklqbDZZdzljZzRyUWhFY20vRnhsSUYwWWc1RE5wQ2pYcTQ5ZEF5Tk54UllRc2xBOW5NWHlnN0FVajgwbFA3YjBHRTBvenBPSUUvOGRRMlFRVGpCZng1RXJHMUdOK21QZVRKa3hNcFdlaGtDdlVieEdxZnZRNWRaQXVHREU4RTFoei9jNVI5aGZWUkU1SVRNbm5Edi9wb2lla0lVMU9uWDBqMjNvNXcwWVVVblBmazdyMW1DamVRd3dCZ1hSK0U4WkkxK2Z2Sk9NdkU2R3JYNGlLVkVUdEs4ZS9PTURkRTJ2V2xVdjhGZUxiTGdFN2U1bG0yNXpBc09NVFlkcEVZTU9nL3p0OXFpMG9zWHdrVGlyQ2cveDFnRmg5QWZKc3BGUmZESFZ1RWtPdkFRM203MXZLU0FZVWVKTHVodHUyOS9iVDlOUmhyeHhSZTZIUjlMQ2FjcUY0U3BaOEZpTzV2VHVaeGFBVFYwalRaaHpRSXVYSDB6VEJKUEk5V01XZzRTSDJtWitwTXMzb29FU3IwTnd3eGl6bU9PeVV2bXZraU94K054NnppaXNlVjVwV3VJMCtJK3lnaUVTZHYyM2ViZXFuNVBPU3l1UzdMSEE0NlRPbnJxcWllbjB3UW9MUG5YeWR0TVV6Y1BlbGdJaEZWL3B6QTNKSGNQRWpjSGQrbHBITDhZMTVBazRnczZzV293MjdZQTd3b1N4YTFIaUlQZDdZQnJzbVZtNmJwYlNxTVl5UnRzVlpsOTRIYlgrTE05OFZUa1BDMThmT0VZOThOTTBHdDVwWHZUUllhSHB1b0RMNTNXYnFSRGhSZlVya1ZuSTlRT2VnOHFDOTJabFNYdnAvRnRDZEx2ZElRRmVVZHRwZkFQR3A1U1VhZ3NQSld1clpGNGU4T29OVkZXMi9VTzBzMU5hK3dJb2w3THVLZXFQd0RkL2lLVnBlUTdHZnNYMjJUNWg2MWw4emRuQllJRGhvWlpDeUt0U0ZJQzFodVdkTm1lam96WWtDT0plUUpzc01MRHJUZFZhamZzcmovREF6V2NqVE55VjlnZm1ySG1NWHdwbVFscXV4enNpV0pDcks5N1Zjb2phM1RhRE5ZTmFBdmZmbUFWbEJYWm5ubmsrWVhFSmZ1ZHNxc2dzRm9WeC85WjRzc29uZmVDTEpmQkhNR1o4Vm11OEd5dWRsK01SRjA3emV3WjZqZjhwUFlaUHZRR2MvbVd0NnY5ZE5nelZvTUpDTUF4bnZ2TXNpQXp5ZzRSYWZCTExTeExubFBhbkU0UFRQQ2F3T096L3hQQ0pnRGNDVHFxTERndXBud3Rja2E2YUU3cGYwcmovU3QxOWh0b1BYMGpLNUxQdGNvNFpnUWZVZ05ZSTRiTmlORk0vSjJwUHpXWmZEWUZWOFNzbk5wVGhBbDh3RWpWS2FnMFlpeGljS0RXalAyOE83VlhFMDFudG0zdzR0eW82WHVvb2dHWjhnZW80SWVrYVNFejZWUE1rR05UeUtXMzJyS000czRLMUF3UVNUSUUwWTV6eWVYWno4Vml6My9mN1VxUTdveHlBQ0xabEZRWXB1VGkxQTQwZjhrVjBMSTNRSkI3WnNnenIwZFlxL0RzREt1TXdNT3JUNmFrRE50TmhCQWJXT0k0OHZhZk44SHg3Rmw0ZjlMNFNEdzljQzJ4clZsdTdmd3ZkL05nMmtoN2x2eGp2M1RDYUU3YjRwcms5YzRYalNUTFBCSFY3WkNSSWxJZmpES0JTeG9KVnhEdk55SG5HT2hCZlJZQUZReXRMeHowVjIzUmJNWEk5Nm9raGN0b3Z5SVRzRVA4WlhnWHRiVkpnaVo5Um5YQndpOTlOaDFtMmgvSVMrUS9lWWpURWpyZUxtaUIrNW8yTnFTMWJQUy9JV2NpdGtzUzg1aUpmd1dxUk42SUJFc2hsT2twRjd0L3BZaW0zQXR3Wko1Yk5NcW8vREkvdy9BcUNDcmRjdDdhalRtSDlZV3lwTC9aY0VHU25qNEJPK0dvb00ybVpaajJCT2FSa1FDTUtQU0QwV0NVMUQ1MFVkM1lBWkEvdGNzQkxOSVp3ZnMyUFV3OEtaRTB5WWxwd2FJaUx0UHpwK0pVbkpHTVhtT0NTK1I4SnNNZ2R2K09zTmZRSXB0WmxNUWhLKzZYRzM3Uk5yeEd6MGdvcWxRS29ma3FHS1dGUXJSRjlFL0VpaGtFRlMzS0ZueFBNTkRRNDUrL0hFUWFBOVJ4UlNJcVpoVUVyRmFYMmFyVHJXS2h3QldEWUlVa1N5SnlaRVZ3TWhoNVp3Z1FtV3BvTVYrVmpQd2NuS2V1ZmJSemhSUmplMElRYStsTDRFbm5HQXF1ZmlyTWtzbkdDeURHTVNzYUJIelVXVWVvN0JGa1Z0QTlwNGk0eXJqTG04THQ4Qk9RQ0hHYzhsd3IxVTZCb1BCRFhrV1hJUFVkRVFLenZUc3p6L1d6a3J4bnBxNjl0SiswUU5wOHRTYU1MTUoxTVl6M09iZ3YyOFQxRUFOUVFaL2Z1bFd2dGJsTW04S2x3MExHQXlpcHByb0Z6T016c0Y4aG9rdTg2akpseTJRMGZwOEhYSVQ2UHRoRmwvZXVRNTZoSEpUUU9lV0h2ZjFIdWNTTTRYVmhLQ21GTmQvK1BESDdrL1V3TmZYcDB1LzYzcDlXRDFQVmlvM0VlVElKY0NUeEdCM2RKa3g4Wmh5YldZcjhtZE5NQ0docktoNDdscWNlOUdFcm4yU1E4YmNMK2lTL1V6bFVEKzhlR2FQMzNJanlUVzBWRVVySi9ZMFQyRFFTNlQ4N3Btc0R5UWNaUTFMRHJCS0NmWWlFZndHSk5NdldLQ3k4b2Y5OGd6dUFNNldkY0srbFN4alJrc1ZuQlIwZVQ5SzUzbVlReU8vRm03ekRmYnlSNjVZTWZkVkRiUnRjdTdVRm5aWWdrTU9yQVNCVnFZUzRnUWNjOUt3bENGTkRvdytpMkZFSTcvTjdicXFwcVdUclMyeHJWNGFpNmpwM0hKMWhmSEJlSnQ3MXgzNjIxempBUVlsVVFPMGpZdkhaSGYvRmhuUUtsa2o5dHlRVGRLMW90TEVNTTZVWVB3Y2c1cjRacnBuNmp0Z3RyQWhPdmN4N3lvM3MwVzk4eCt1R21RS3RiYWxXSEZUVnJJMlY1NXVyb1dFMURHVkozc1o5MDVDUEFPWVpVYXhQRUZUOElRVC95NUozSm9samxuL1JiVEZSK0l1VnY2NE9WOVZoaE5weG12YmF2bStZbHI0TDJHY0dvYTVSWTBYTlpjUlZmdkZaZVhIa1VTNWVUU2x4N216UEUrVjlZeFp3TEQ1Q2NWTjdwMGlzbU1LWmpVVjR2YmQyYzJNbkNGTExnQmp0Ui92VHRlQUlrTWxZb3MxWldrUjhXbXFmR0RsbHRPVityOFltbzNXQkNsL3lmTmpjTkFSOWJTTGs3eStsZWhBOERzVURWVTJ2c0Y2cmNaMzVzZFR4b2ovZi85YmFIVVRNK29MOStHZXJ2ODdQQjhIeGZvUEkxTHpTYkZ3Qk5CcWtRb2RyOS9UNHFTWG5ubE9KODFzOXVhanNYOVRIWHVuNUFWZFFaeVF3Q3o5cFVlYTNlcDF1SzRFeHloRVo3T1JpWXMyYmo0USs5aDhKM3ZQY2RrL1ZDTG9hMkJGVVk2eHlwZFZsR25wOGNWQ3Jac1phS01IMnY3clBuSlZualNlaTZHSzVSNUFNc3dSWnltSFp0QXJOb0xSODBIL0VyR0M2T2lTYS9aZFMrb1QzRDd0Z2U3ODJRZ2MxeFEzNlEreXZLSHFGNDI0SW9oRHc2SnJVNDlzYXQ0ZkkyM1I4UjJVTzNPV3U4aGdtOUZjemxTVHNqWU5vbW8wd21TWTlpZVlGNURzcEsvZ2JyZ3l2Y08zTXc3Z29maFJROUxMWkF0dHdCcU1VZDU2dFZCQ1k2aWpnQlVBVklVK1VwU1RsY2RBNVVqUmYzaWZsMjZHM1l3Y3M1dFpNQjROV2ttWitiWWVLS1JNZTZOUG1BM0VhS3N6WEVJUWpVam94cm5pS2YybHk4WlN2QSs3MkFTdmlZSnBWR29WbDVhb2g3bnA3RHI4UnduS0lidkFQY1FEWTBGOGR1VzlVMnd0VUY3bmJGVGhFZGdkSTBvb0pkSWxtbG9mMGV1UURYMFBXWW5uWjk5RXRrSVdQemZQM1E1S1lVZjN0M081aWJPc2VFbGQ1NHBEU1lOY3RsSm5HV1d3Mk53UWQ1eXA5SHRyUmNOOFlpOENGamJPY2JRMUlyMCtuamVsOExJY3RnWHRrSENXSXZkRDBWU2k1ZGpkdG14UzdOYk4zTUdZZTdUZ0MrQWlyMFdhay9UcTJjaUxnTk1yaGxYVFdVOEVzVnlBM3NzK1F4REdTNVhqWUNlRkdLOWpSWVE4QU5PdVpOZHFJMmg1WlVTa0ttcm95UDAwRlF1V0szRVU0NE5uWjRjVmZyNVBiUEkvWDlDVG0wRkZBd2NnWVo1ZUE0OGlhYm5PZzN4eWFCUEgzU25LaE53ZTQ4Sm1CSWFHYnpWdVBXNkxWNjgyQWxBY3BlKyt0UnE2Vk80eWdTUVRRU2I3Q1BLWHdFMVdCKzI2ZnNuejVnR1huQ0dDZXR0NDlKMHhwYlI3R0J2Z1F2TFN6TndBckF1elZJbWxKNm1rTXY2Mm1yOWZad2dTVVdnQ0pHTkZRSDFPakxlcFdSNUFkSUlRak1zNjJXank5WXdsdDVWNDE0R0l3c2lGL2JuWmFzeGc2eHpiZ2lKdmxjTzJwS1plUDZHRnRMcXE0cGFPM0NEYVdsZTRwUjEwOU84UjN6Q3BOUTdLbi9YMUtqY1BjTEU1Tm94NXNqT1c4Zm5FVXlpZTVwK3dPZzJVR1RCL3kzZ05QVnBMQkxRMGhuejdHdUp3L0NjK1VJTlBnb256UGFVazBmY2VRQzZkMFZrdFV1SVozYzVnS2Nmb1VFUXk5MjZSN0NjUVlndTA1bnhZT3BZanhjUzB4MkFDb0sxcG9hcCtIQkdzOWxRWEN0aC8zZTg0SzZma0gzeS9zSXVvQ1IyMFRySGN1TmxMaGlnV0o2K1Y4LzNUMnlaYkxHbFhFRnQyN2tDVnV3YXBvb3plYTRuNGgvY1JrZUxFZ1RhWnB5cW44UmtaODQwN29xM3VPZ1p4enFTRGRVYXgrd29QbnA2dk42VmxXckI3b0hEQWU2VWlldnduRnR6WFJia1pveml5dFZSajFrVTI2U1p3RHZzQ3NmdndFbUNuK0tFL2toNklBUDg0UVhab0pXcUN6RldyRG40YkxFMzRRUDVHREtseWRsT1NaTUtsb2FCS0JkR2pvZUZPNTFocEVWVnluMlVNMXMvQ1BoRUJmVGRSN0wzVk1PNGtlYlEyeXo3a2NvRTN0bGRsSnNBdG84U3h1dnlrWjBYRXg4RDBSWmZyUGprc0hMZG03anZDVjRDd2tqZVhQamd1SnI3NG9KaEo1ZGh0dTF6QU9nb2lVN1pFTmZLQkVDOHNoaXJaOUtDMHRBNEsrSWFVd2E2ZHNuamh5WGQwbW42YjZhU0FyR2liZmZvL1lTa1lHalNPd0w1M1poNWliTGhtTjlHTHhNQnRISmZwVWZSdzdiRUpERnJCK2pJVXpESituRFp2RnE1aS9KcVRwS0tjazJuL0g5RHNXVGZ4REJFbmZROGtQZHN1NzF4ZDFRRy9Ua1AyM0lIOXZlMUFoRFRHRkhFbElkTWxBbmtkZU15S2t6MTRyYUQ2dzdlc0RMT1BSd1JhQmlXS3ZFTXVxNkovbGtKSGxhQ2dPZFU3WEg2eEYvYXlKaTBJWUh6bllVYVU5eVF0UzExMU01eHVKMkt5VjRuUDdScUw0VUVYWWZMVHRhRE1yUUdpbzVMRDR2Zk02c0p6aWFJV1hqVENzS2dkRlF5V3F2dTc2YkhJSldDcEZnOHVIUjFWZU82MHhYKzI1QU8xSnZwT0tzdkhWMDdCV0syRDlONnpMNHM4ZW1QZks2Z2FweGVYeWtVMng4d29QeWNGcElZNkFkMUpZVGx0UzJmNHltWk5pNFV0Ull3Y1FobEVZQ0VBbzR2bEF2Y1VBS1ZjQXAzVFdzUU9mYzN1UkM5L0h5NjI4dUFHWTljY1h4OUdHampBU0hYSEk2cWk5RlZGSEpxQ240WFhubjFDNW4yalpGNWkxbjV3c09SaXVnSDZINTJVeHUxOTZhSTFNOExrNkNvckZPZXB3TGVkeG4wQ0l0KzEzWk9INTVsOWVqeHVCYjU3Z0FkbHhEZ0xUV04yR2pFcFRMdlNHMjNVQk8ydjZiS1hYRy9UQTJBWitKZm5HYWtEck1Zb09ud3lVSGNLeHA4VGhJYlYwVkQvQ3ZXZ0xiMkRpQUxEOHpSSm05ZFVqLzVyUVV0b24zekI2ajcxTk9YT1BlRjhSNitZZ3g1ZkZtVG94bExjbzdUamxyckd4OUVjRXAva2pYTE9qellRUUkrZ3hMK0FPUHRseGRjM3c3OGYzTU54eU5EWVI4QnZLQzE3bjNLN2hOajFzTllUd0dBUkgySExMT2NUMXlDMnhQb0hRT2U2ajB4L3BCN0sxTmRWSS9yaTVtVlpMUmE0a0tGMzh5YVNTSXBzdkhvTWlZQm9aZytqTUJmVjBsYnoveWVSMmgvMXVmTFpJNERONXFHT3REcUVNNUZzdC94Y3BOYnFNSmNDOXk1RXRqSk1kOTRmSXVRMTRYT0UxcWN0d0JKSFVORzJiVzhMRWhIMXJ2bjhlM1lzM2lseDI1ZlRQRTd5OWN1T1VKOXVXRkhvUEZoYWl2Ujhoa1RWek5YUi92dnNDVEJaelZZY3lTZ3dXeGw4Rk45K2pTR3ptUDlKcEdnVXNFaThXTytwQ0JDNzhMTm5KMmdVZWVSVGhJL1cxd1pNN1Q2Sm9Ea0FYT2lDZnJIa1U4QWxHZUJoTEZTQ3JsYjRMSWc2dDRDclh1cTZ4NHdlU3dhTXpzMjRKRE1UUjUyTi9xNndGNHNRcEgvU1B1NGFKcUZRRU91OFMxMUYrY2ZVaEt2TnplTjNjQy9VVll6RWZDMndueEdHV3BnZmhBTVVuUHVDcituUlBMZ3NTekZFUnNWQVRZOXFyMHkyckt2U1ljVW9PVEdrTk9Fb3hLWmZiSnZzd2FYclZNYlNjZVMxNS9ZWnVJNnhTM0gwZUw1QS90YTFycS93bUxPL0wwK0VTSTVoVENkVGx6a3kxNk4wbEZSdVJjVTBPUzBJYWNLcWFTazVnV2ZaeFRzcEE0Z2NiTkRLY01zTTQyNFpSN1hNT1JTdGc1UVV5c2JXUGh0alZ5Zk1tM0QxMG01LytyYjVicko0ejNYRVVoTXFpUElmUEtTZjJKdVEzQ2lPWjVJcFpVK2t4eEJoMmZQL0ZORDlnSlk5T2J0SFUyQjZUaG1tU1MzVXI0YjRpYU82NXhEekVlYUJKTW5iMlpBOXdHTDZlNGtyQlVQRzFuaEFmWEJEZnFpVkdDejc4cjdQL1JDd2JFVU5QaEFFZEFzWmsrd0RkcDNWUmtYNzNBNXhsNGxkQ3pFeGhXOTRpYldTYUdRNEh5Y2Rhb0hpVEV3Rk93K0JzaWJjU0wraXAxMG9vYUlPb2cxQ3VnWGNNREFXVXV3ZkJ4RFV6bG1IWTQ2bHpaWlhYUkhoQzB5OTU3anUwWGNlRG85QnFsTWZqYlhBUHE5cXJBTFk2eGxDOFJOcUt4VkQvWUFGSWhXNXQ5WVU4cE81dHFMWnE4T1JQRHhUN2hSZHJvZm9JZ2pQNDdsQmVsSkY5WVhBS0I4VXRsTDIvQW5OS1p2bVlmOXZvWW4wOTB6Z0Urck9SRGtMSnN6ZUwrWXVxZE5lY0loeEx2Uldic2s0TFBTLzN1VmF3WnY2dHM3ZmpjcnMvZVRlalpHdE9LbWJRQXZIMThrSlpZL3I5cHYzOVg3UEVyV3JpSVV1YU1scVhzNTNJbTVjR243dnBwMFlJQ3huSHVkUW9IWTdpbk1SM0Q5YlBuL2NwNU9oa1ZsamtjMHhsTW9NcW5mUjVEYXpuNXE1VmwrVlVoajAzN1VOZm02clYxZDJDNnlKQ2ZkeHBUS3JJSFVGTi9hd2J1bGZzK09seUpIdHByY1htVHU0d0FNVnplZUxGUFZSaU9sSUN5WmRaVDVpZVJwTzlacXdCa0JpZzhkdlRIZFp4NC8wRGxKaGJKcURGUXNBRlgyOGlyOGtWNHVFU09kOE5WVGJRcE0xUTdrKzJnL1gweDdRUURPRUJVQVovQlVha3kxNGhZV0xCaHVNZk1nc0t1aGdzNlpqMVRDSUZsSU9GeldIYXh1L0RmbjhVdkhCZjBvcDRyRXV4NHpDOXlKUmdTY24zdS9ncmxGWHpSY2tiOHlhVTNaYkZsRWlYY3hIMGlFWkxqNDBvRVlCSTJtZDJQU3g5dG5wb2FFdmRjRnpPb2g1dytURzhyOUVMUGI5RHRuUEdCUmJKbTBieDJsNkVjZ3Z6QTh2YzJ1enBYWDlOMk0xd05EOFpSdXJmUitUVFR3STFKU25iNG9idlMxSlVDRnNkcGF1NHpFRDZvT1IyUlIzRlZxalBicTVKRjZlVTYrYlRBNDNDekhzaUw2UW9UdXpKRExiSGZIRVdlRENzTitibHdPWXRCWUZ4S1VIRFc1VUdlRWMyT0RJL2p5ZmFhQ2VGeXBzVDZxWW41QmQ0Vll0bTBxc082eGl4MkkwTy8yMWZSdnhyWkk1MFNXMGdlZTRDdVFqWW1TNDVMMXRGK2NqM1NyeUFQcWxDZ3VNSWR3OFhPalBiTFlyUUVJZ0dEak5jWW56NzkxdUtTaWp3bkxWVjNSaGt3WmtJdEZyVVNFelNQcGFKWWJBb1ZscVJueGpFanVZUHNVL3hNemFxUWx6YnBoYkVwelhYVVJ4VXEyaVZDMjdWS1VkT0h0Z29OdU9JNlgrQWJsZ2VJY0dQTU0zam1VOTQrZFlKazNIZzk0NmRWNUpJUFB3RHppSEo1endBMS9hREdJQUx6ZEF5Y2xsQXh0TDlIMkw1eGxXYnlHUU1kUFladHBaQmpVMFhDcnJFM2FqVHhOUVJKcGY1MVV0ZU9EUHBTZU15c05uWmVtTmR5SnE3OUV6Mm1FRko5djF5WkRXL2w2bUJmRlQwYTFvMjFKc3djcFZoZ2F4WWZJZUY2WlVsRWg1TzVabmJ6TVJmbUs5TEljSnlNcmtIRW95ZlBuTnJvOExBY3ladUt3ci9HOUhpOXBUUyt3bXlsS2lmdDc3LzBtZm1sNFdwY2wwMmZiWkxRZ0ZlbGczYTN6WHlPTklUMDlmbG9NbjUwdlVHQTB3M1ZqcU5pRFMyaGY4RlQ1VW96RnFkY2V0OXc1UTBlZVNTMi9pbVluVDVrY21YalNDbElObysxanhScHZwTUlEWWM2UStiSHM5QkthOThGSkp0ZkMzZGhjNlVmRE5ZRFlBdFkvVWhsSWZ2WmpWZW44dnhEVTRxa1BrZjIzTTR3WFFuTmo4VUVHbml2aVlUZEFPU0xTR2l2M0J4cUtjWTdpWTZpR1RoOTBoUHpOYzlWais2NXpqMHg0aUF2NmwyRjZwbnRaaGUwSjRCQ1VvUTVFdmJIZzhvSjVRSmFza3FCNTB5bStQdjlMT0tFemJoeWg0TGNnZHhNVU92SXIxY2hOMFpuNE53a2l4dms2cUorcE9qWGdnSm0rQWdJQ0FnT2VmWVRkTVozMXNBSmdJUnJvVWh6VjI4cUdxRU1ROUFnVSttRnBTUEl1Mzk3ODU4cXpNR1hnTkFlaEtLa2lvekFNdUh1N0Q0eFVNYk5obFI4YjlHa2MrcE5vUmZvd0M3UDFJaTVtT0VZRmtYby93Zlp0VW9MUThqTTU1QVI0Kzd3eDZPWEpOOEQyb3REaisxYXo3Z09KdkNxMXJOMWtQbmM0NGwxdWozVk0zWnJEZ0s2YWRuKzNhL3RwSUpLWUthVUpLbTVtYlZTRjZWYUxjY0RURW5Tc1JWWGFJWEZoWU9iVVRSWERrVStwVUFRUVl2RFhmeWtkVGh0M2NYQ0RwOU1kRi8wdGFnYkxQTFZlVTltdWo3NXhGTk9qOGVnL3hDeHJkclh2QlQrdEFiTW5jSG5VempUQm1nc3pqN3ZlSElUZ1NqTE9UL0ZHQllLa0pOL1dCczc4ekFuVEVPS2pxSFF5Rm5YclpSemVMQlVUMXFFYVRZTGVEbTNkZXpYMGNlT3dpYmpGbEpvY3pVWXEvK0FUNUtqQXI0cGU2VmhvbnJmU2lUWWgyRFVodk9aeWZaNVh4K0hCbXg3dHlFZngvOXU1TVRsSWNIUld4ZnpORzlwMHk3WVNaRERrdmVqbUpZUDlEVVRjSnJraXNudU96THZ0eFpJT1ZZZHdaSkZ6L05LbTFZV3RzRVoyd2g4TEJjUFcyRm1HMEFKbVQ0SmRqRHordGNkN1Qya2VEOUJ3L0tPajRSSFJjdWFNNDZzNWdzcEd2aUpSOVlOQk1iS1lHWjU4VzJZaWpsMGxuSXdjbXBnQW5lOEVXKzdiMWNNSWpCNUN4SG5nQkp2M0haNXJNZm1mMHc3MXRVLzRMRzI2bWthWjZMdTNDT29nVWkxREdUS2o4bkVpNEt2d0xtWlFNMThnakZ0YWVCd2dXei8xWm5YZ3lNRDZCa2drTGJMNElCQUJVamNnTlZxU0svbVB5c2NnTGhXYkNsR2xWbGdqazkzcUpZYSt6cVJlaFZYSE4ySGo2eGZuOVVnckpnY3ljc2dMSitpdEExdkZTM1dXVUd5OU96VWc3U2xnbE1xc0VWeEhDei9lYmloK2hxbSt1T3h5QUQzWSt6TjlYSGRPZDgwU0NtUE04bGZqM3lrOFJSMktCdFBsVXd5alZTK3pKODc5NTNrZXNSYjZJUHN6QVdCRWdYeW4vQWl6aEUyL0ttTU1PUDNQR3Q0WGRkekNIaDcyT0dSWU41d0dFc2d0eFphNEJwV0EvMXlNcldxem9wMkxZYndlRTJRUjFkNi9RN0lPY0M2YWdqSFBkeFBwa2MzUGVjVTVUQjBKSzFZUWUwek5CU0lUNFlabFZUOFkyMnFndUVDK3M5TzlHbXdiUFVmWlord0t0UFBhZGNOWnE5am1DeVdHNXFlTE1ybEMrZGQ3WVl1dmVndTNmdlNCZ3BMYWRETHNaR2FtRzZ1N0lsTkFIc1JaanFjbnJEZFpJS0Mxdzg3Q1YrR3JmdTBCUXlyZzJqWFBSaGZBVmVIYVJzVFY5WEpsWTR4VmZhSHdNM1ZQSmpLMjBYTy9TS2JSMWJYbU5QTkxXK0trbUw1ZFZvbk5QdDVFWk1iR3JqcDdzNE8yM29KckFsZ25nSjBFWHZZMENSY0l3bkN4QTZtbjc5TVFxZ3EvRHlrUFQ3bHVLKy9qYWtvclc0NVkweFZJTVdobGQ1QU02VjRhTDZCYVd4N2Y2dTMrdDFFMDhkOVVlc2h3cENTdWQxZm5mNkVFcjJGcFp3UTI4cHZtVTVBYi9QdmhuUzBpUnNLY0RhWFpoZUJnbmRLRVBpNnBJRFBPSExHcjRhNlA2Tk12b3hLa25GbkZES1RkTXY5djRza3MvaDZESlMrbDZhSHlHdFVvc0Q1bWR5WXBvbU9IWHVpT0RyUy9sMUxadFhDUXJSZmtFckhqL0NJMnZkcmt5V3JvOGZpYzlCekVGdWVZVEF1WFg2WlMxTThhYW1LMVNSOUNuaFM2TlJtb3U1cGJiVjgvOEwzWWxuSy92Nks0SE5SNUZQUGdIaFFzcnNaVHUxTy9kZVJ4Zk1uc1VLZTdlYVdaN2I2NXBvL0FNZTNJcnk5KzV6cTEwNmxITUtLL05JVUdWdFg2UDh0Yi9lcVVNaUVheDcyY3o5b05yTTV6VW1BaE9DNkxRWkVxbGJnSFJNWk1XUk9HZzZuczJTalM5OTdoSDBhT0FkdUZONXJUMnN4S1gyUEh2LzA2azZnRlZySkIzTElINzZ3b0pwa2lMTC93alFhd0dJM1dsUG02UEZLS3B0blVLNDVEL09udzRpeEpoU05jQ1NoZjhvVDZuVG5ITUhWRjVlN0lDTnBJWUxLVmhIZjYvTGRRNWRJcWZzZ3JLTEZYVmkvTnoxb0RBR2FPK2FEVFVuUkZ3a0VLQ2lVbXhuTHBoVi9Ga3ZsMzRJYTNpenN5WE9kbmRYUjBIbTdFM2VuK3FxbnAyYTlQNHlOaFl0L3BTc3J0cWF4U0dDWHZKY0tmdTZ6RHVORExpdUU0Q0NWeWkyZVBPbGhqUG1zNmhLMkZVTHg1THB6emNlbjVTSTdpa3Rra24veGtrVW5BZW9SbFoyR1N6cGU5d1IyMUVwYmFPOVZWUWsva3I2L0pIN2JUbEtqNnI4d2dWR1p0NmVUSWY5VXBYV1htSzFTVmtoQXBhUExTY3RXVjBocXU3L1JvWTZzcGVtZEpreFhMN3FNbkpNT0tKOERGTThLNkRPcnJFajloNW9XbkE0YUpRYVo4cEwva0hnbjRSeTB0cTM3T241TVg3OC9EZG5FalU0RGk5VVZlam9XY0dzMkVrUmMrQjA3T3phdCtaZEhXRG9HWjJzaUJzbVRrOE9VVW43dVFrbk8yVmlQM0lBUjVNM2NpU3ZPV0pkU05uT1pSUjdYNEJFSUVOd29hbzZtenJqeG9yWWN5czdXVHViTU9EVGJVUDduN1FzNHQ2eHhtNzgzalJxQ2xocVBUc1h4TFIzcG9KZGZwenc0eUJCa2N4TnhpMk5PcVU0bmN5dW1YZStmcWc2R1BsYXl3ZVZ1dVpyelBEWjRjZ0F2b3R4THNNZ01VMEs2NDdOTERpUC83NHAxVElWRnZRd0ZkdHhMMXJ3UjN2Y1dQR2JWZXFnWVhXZGliYlErVGFFL3M0YnFSRGdJK1JTMXVMeFRLMFVXMU9UdFd5aCtZeE5WUTFaTlYyWmJSdXFGVWR0dE1lZTNnQVo2N3BxTFYwb2RLa1g4Y0Vla0dMSVZCS1VXQnJUbXdyZjc1TVl6TjBYL0FVYzJNbzVwTmZTSnRGOW45aG16a3BTMnZ4bDVDZVdXMGdLb1lPKzk1dUN4N3BhOFRNazhiTmY3bjJzeUNkTU41UVJ5NTlJS3Ryc0JxZUhDaWtmUUVrd1RPZmIwZkdEbmVxcExKVTUxSlIxcFpXL1B4VkUra3hncHMwczliUzJ0NENXZ085YnV2NFo5NE5DODRZVXJUc2pXbWhGUHk0YzdhalNLVjN6WEJSV25CWFZSb29WU2owV0VDdmsyZUFKTFhZaFgwU29oTXF0c3JDNVhwVzFzQ0YwWUVoTVZFZXFBZ1FvdnhtSjRvdklqcnZSWkJVVXVmL2h6ODlsNmphWmNQN0RCdERlN3oxcFdsMzNDSWV1RjE2NTh2c0t0UFJzVEt4emM4d1VTbndhMkZoRmpRVGE5L3oya3VXUnhQWXV1STdteFVsUms4SUl4K3RBMm9qbUVMK0ZwMC8wdmZHUjlESU5PQS9UUXpWbXMzbkxQRWNlRExYVVlFZ2VvTXFRNU9ST0lQNnBKVlU4bEllV0FiRGt5aWY3THpPUUliU1NTUlNOQ0twSVNIdUxocHJzdFYwWDdtUy9la1hTbmV0eWpRdFBod1RuYUxMRVdQeW1LUmhSdnZkWlFyZEpJMUU4cEI5czVLR0dXT2cweTd4RS81QjlCMDhWTDVCNTUwa01ROC8zb2pjdUtoNjhzMjhMSjhJZE1sWHNBZVZHdndtci96bGJZYUsreE9Icmp6cmFTcHdaT1Q2SzdqZmZ3cG5PYURxcUZlNDdaMjJjUi9zR3lrcU1HL1Jzb3J2NEJRYzZHYTlpRlJKRGVYQmJ1QlFNUVczZW1OVWpVRFlrZWNpamhXNnAvS0xxMFNYbDBpWTZWblpySUpJdGVvcXh2MVgybWxKZlNJRzlZZEN4cWx1U2dtNzZ2eXQraEdySk9zenBsWjFKc1BaMU1HcCt5cVUrZExCY1hyMmd4bndNY1VzV3UzNG02b1RQVE9oZm5ZQ2F4ZEkyOEphNFdaRk8za2xpakY1ZTJkbzZFSG51UkY5NXJFUTd1ajd0U2p1cDFyYXJ1RVY5NGkvUFBPN0s2NSt3K3FmTkp3WVhmc2dGQVhRSEY4cG9tbWovaUpNNzV5YStCZWVpUmRKbUpBdE1CQW5CNXNwUEFEM2Z2OEI3T1kxbGpnSTVBVFhCMHRpRFZMMU1rUWowZHR4RlY5WkNCUk5lWXY3UGNZdkQ4ZytWTGxrRmpNNEdjZ3hITG5ySHFYOTVQZCtJLzlMdll6aGo0OC93bTdrU2doaUJqN2o4eEhhQmJaSkFnS1NXaldrU2RDcjdaZkxEekt3UlRtNFUzZ0JMRmpjQWdGbS9GVEZwc1JCZzNIdWFDR3MxMnBDdGxLRk55d0hPOHk4RVVjdmphQytTU0ZhZFlkbElnWFVqNFBja3dhbWNHU1VFdGtwdXBoNFVCOW9YeDhRL2hNTmp0M1pydnZsUlhSWU9pSkZZd0lqT2ZGaXlhQ2hrM3Z6R0xGdWZxRFkrZFFxNU5QYTJBMG1XQlE5TVhRT1ljQWxkeXh6cXhBK2xjaFZWWmFhY0NTL01QWUdVSHNGZkVWY2pwWXJBQmt6dEsxVXcxOXdkNDZXNDEwMnovSDV2QU9XUDlWVVNNL1Bud1ZvYm1jT0RQNWltM25JMVRiYXlIWjdRU3JBSnBCRXFzZm5acWpreVhlRnBDY2NrOVRyZmJldnh0UnE4dHlJbTB2NnozYVFoUnRRL2ZhdVRzekpDem1zQ21xbDZYcHFnL0FZSXMwSTBhaGk3TGhOYzVtWUxNU2h1TnBhckpGN2hjQk9DNDEyaVVPTGllcWhod1ZzaW5ncTVJWmxBYXh4STNMS0IvWVRSTlFOU1dmMmVabjQ1eWZHVnhVZWMzYWZXYTA4R0l3QVEySkZtSHZTcTNWOHh4M1psRlhGMHpOODFpeE45YVJSVFo0NUl4Qno3aXBIMXNGR1pQRUkyM2s3YlBPcENRQlJ0bGQxM3ovR1E3dEU5YnNlSEM0aVBMd2RPRDlwZjhvaURtSS9qTjhxQjNrdkVkbHhZdE11dVRadW9keVB6WENZTVNOdFdibWgrbGJVMGgzN1BOSmVwTFFHamJHY2FpbnRZdFZZdXR4Zit3Z3BabklTVEd5Qkp6d3pZdklkODZpd3JuTktJNk1udCtOK2dSTTdnWU5QZFZwcXY2OHRHbmNpaFFPY3FvNkVaMXVnd01iU0RGNGNJV0N5MnkxOUFKYXFiTkw3SEdWVXZzc09uRktjeURIQlpRU0l2VmJGeDZZNVJLRnhHd0kxQndxd1ZPWmFzQmwxcjZ0V2xaNnhmaGhFc1JnTTJXcWhkV0tlYUtoREJWKzFXN1kxck5iek9RQ0RkMXZJZE8vSVd6MEdFc1F0SjgrbEFDL1VJODBlV3JDRUE4ZFdBN0ZCZnIzVEw4VU1IZ1BpUFpGcWhwUGJMWWZmQkMwV0FEcmpOdlliNXM5c3FubGdBK1BjeGYxbEQzMWxlL1drcSt4am4xbE1YMk5aK3Vreit4bjMyMmNuOE85ZjkvMUNuOE82QU9nTlM0cktVQ1NQMXlHVGxyNktqTUtyZWhUT2NkU3ZMUDJ2ZkVianZvY0xIUVQ4djhEMzJDSnJvdWdpKzZTOHJoclIzbVZ0bTBUNTVKc3IxNVh2akFFWXJWdnB2TWhkVUF0VHNoclFMbjYvL205WHdKV0hSamF1eVp2L09FTG5xWnhSbWIybE1qN2pmVDlEZnlic0VrWUJVRStPdy9Cb0E2K014L2VaZ2dqTnNXYkVzWDRTV0NYNitZN3hTUmpoRzZDM1B5YmMrN01PU3BpQzdLNkdHdFBmT3BXOGlFcm02SHNkWkhEaFBBMGFIdEYvRi9NWFNFS0Y5TFRJZTE0TzBlVm1qL1RuTU5qYVlXYzFXcVpPZGRuS294cVFaT2dvLzJZcFlsYVplMTRjZDlLRUg3MzZJL3VQeUFER2ZKU3R6N0lwaEJwaWtPZjlvdURNVytYc1hqM3BqSHBKUjNCMGNhQWw4cnY1T0xSR3doWG5paG1uTGF3T1ZEZXFZWjlCNXJ0a3FVb1g0a2MyU2JhSDhjaWZQakFpQjF5Q0xYMzA4TE96aWI3MTd6TXA5N1N2em5LVVl5YytFT2dDVzV2Z3dld2JESzA3VG1US1BPYnVyeGFONFZYcDdkaEZiSTFyQ1hCOG5OWW9rSHR0ZVBaZUtwY0tpS3RzT252dnh4ajNManJ5MXVGK2VuUVJ2TElmS0hkekl5ODcweG56YUhBdUkxR0JRWjF5d1JESUh4WVFxM2xlTTZyV2pId01EdVNReHpMZTVYeVN5K0dLUWQwcENiWHJqK1F5VmU1YThxQTJ6Z0VTRkpkZm1ubkU3L0FHdE9pUzZsek1nVmRId2F3c0t5SmJWell2Z0JnWFhYSFFvN3Q3Z2VYcnBsdzNHc3pOUDZQOGgyR1NhT1dRN3hBQnZXTDVtQmdxd0pXeFNuNk1HMzcyMFR1d0R3OXRhZUZzS0ljMFBJNWdKaG4zZ1U1Y2RJYVJuYW12Q1hXejdJRi9oRXhWQktZbGFWK1JxRGdvcXMvUDJJa2FHSjdPMGxkQVFnT2hKL0pSb0RYVHhNVnlpZ0RzWE9BYzhBV25PcVJCMzMvSUhucXZrblpyNWdZNmZoRTZCSWJpL3BDWHVmV3p6TysvcXFPVXFsYmQ3UmFmOG1seUZLdEsyQ3hueU4yN1BETDhrNVpwV2NqQXJKcGU5YkxCQkQzWDJ4aGtXMTRIdVR4QXFhK3hubURSTTRvZTdjVDVRZXk2M2tRUnhlb0lPdFA0TEFwcTd2Z0xmRTVtYUM3Tm1sQkNnT3N6czM4L3d6bTZ2SzJvYnlQRFE3cUpDK3dUZ0RmL1pHb0l3MTdoUG4rSnZ4dlJQSGlUOTZUMXl1VzZhT0lqNVdUck56cEYzN1pXYTdVczZrWVJWTDhKUEd6Ti93L2FkNjV1VUJlLzFPOHdNcWpjTEJQdVpHRXp6cXBvdSswb2NYRVRiOU5jU09vcWxWNXdDZjdzMWF3blpwRzNnQWVPSFpTMXNCZWRHRWQrK1hlbVNuck8wUHhwZTNTMjUzV3V0eGZ3QWRHZkl1V2R5R0liNEl2ODBjUGl4NWtxLzB4eTlucVdCRG1HeDlVOGFGTGdOWkFUbHJ2MmJTcGxXWHJDaVQ4N0grbVZzemM3M3d0QXN6VWdMTUFXT3JoRGZYL1FFYWdZd3V3M28xaFJSMXV0d090bHVQTjZaMVVtMzBjbFBrNU05RFlQb0l3Zm1LWDhqK2FOVzhtZUp5Mm82T2NFdEJMelgxbk43M0tJWWFqSkNBWnRrL1pQSCtUQTN1UUZuZGIvR3dVaXlMUzdBVWR1L2pBMkcwcSt1NjRvSmVvczdxRWEwT1h4eXAvOHJCL0JrTWxBYS85WFJrWGFWQnd4cnBlQkxqaVJrUmc4NzZoc3lSLzdUdjZWeEx1Z3ZJSVFqUlYxSjFCTHlkMzNjVzN4M2JVYWU3VjVXOHJYRlRjR0RaTmFNWUtDRS9PNnFSVzRsQXZnNVg4cEU1ZDdSVkpId1kyekZnWlVkbkRUcFJjZkdjMHJraVR0c250R0tkZWF4V0tqaDJZMGl5dlBtQklsejUvZHBNV0MwTXVpRndGeFA4V0xwK2s0NnJYa09qZ1NrazJHbDNJeWtnK0hqUGNLTDZxZGNqc0Z4eGh1dUtvMldPUWdMckRxZG1reEt1bXBvMzA3VVdkcExoVmE3ZWpzcml4dmdIS2Y3THh2WnoxZnB3TnViL1UwdVY2VlRNYXVrS2RCY3B3M1I0L0Y2Y2ZPOHN5VnF0MXQ1RGszdmNXNlZ6c3RId3BGT0ZnMkhtNUNuTkUrZzNZMkxaT05hZWFoRmtsT2RwZEhGQ0RaVitUbnBTMVZzUFpZY0wwdEE0SVljWVJyM0FRajYxWFJTSEFPUlY5eTJXZ09iZm1rOXk0V29lWkdGQ0R0MkNkOG1oTFpoR1pOWjZqRkVULzEyYlZqUFR0UjY0NWEvZTVSWVQ1NkxBc1pHakxIdnhpS2hmUHV2Y1F1Skk5RmcxamN4aUk5TTdSZzZGcDg3eFA2T2NzYjJjZjBlOXlwSmZNMnpVRjEvZHhZRkpsSnh3WFBBRUM2bHh0YU5qMDlxUXFXbUNBZ1dKRDE3aVkxNXpQZC93aE94K0M4a0JEaC9DTjl6b3NFYVRnTzF5Q2xHSkw0TVdnSlNKcFRBcGpqYnBMeUpJWWoxK2NyVDhWU3JlMUFzRGZYL2RxVXZkV3VHd1l4K3lEeEFDVnZxZk9DdXloSEdSaFRycjdvRkpQZm5GYmkxS25NdmFMZVlFUGlESVlqa3c2OStvdEVNLzlndmJPRktmWjVCNndJRlNiaHVvbFlzb0JFNTB6MDRQeUVpeFNNWm94eVVwa1ZxVmJvMW5jK3hGdUNHa1hBQVlSNVpxbHNJenRLQjBic0w1cUcxdE1hanpaaGk1QlhUZUxKaTRZc0Z0K1hPOGJvdEU2MHl3WDd6cUhJSkwrRVFaWmVtT3JUZ05ZUmFIR0kvZWJFTllYTjdPbnUweUVuWHFBcW9jdmxnUy81VVE5aWpDNS9xQmdPcmJPUWd4a1FPR3YydkNJU1A0ME9iYjNWVi91d21rTVk4eEE2Q3VkQ015SFR5c0FPK1BpZlpFSXZuWmVFN0xtdlRlRzY5UlQ0alNocVlNa25lZk93KzFCeURMUlpTNGJ0VUtHdVBBUG9rVXZWZkRoOUxNMGhNM0FUWEJLNHNKSDRmd0g2NWlTbmVYYkI3VWlkUVg2Uko0NTZjZXA2aWV6U3g1MExsMEZKS3pxM083S1JvS20rUDJKd284c01uVkRyaGhLMG81dVJBTU01STFTcXU2amZrbFB0OGUvNkhtVktISGtvTmdwVFliTmpwTG9LZGlVdERVN0JiZ0xYQ0MwSXl5ak1VTHk1NlhqYjZXZitVaTl5K00wY0MxZ1hxeTZ2elJld3hPUkx1b1dEdFlWNUNHSktxeW56dmNPNlhjVDR2OFpHU0dleElmZE5xUTRsSFFRMzhBOE1SQUdzazhWd2gyVFJrc2dVbENVaWJleitYRmV5YjVJRGYyWE1IaHpuUUxMaXhLMHl4eVBVVld1SGlOTkZFbGp2N2ZydDgzbElZcHpPQUp0UWVXRkdOTW1aMnJRVG5XYmszQndUbVJ0M3BLU1NDdjRJc1U4VGVSNDl2djZ0Wm92NGd6U3JIVStaVVFvQ2xZSXY4ZUtraWlSNUlNeXgvYzJiSnMzZ05QMFBpV0ErWmVnMjI2azFMY1htRUVkWkxZY3ZuaE5lb1h2YzlOckdsVjFDOFgyQkU2N29DTlpOaEFFTUVVcVhFdGlZWG92N3lwRHlVU2xaaE80N0JYQlZ1Z3FEUzRIVjhrSHZzblQ2QnQydTcweVlValc3SGk5TDB4VnVOS0NGMlAwNUlOZ3FVL3I0MmFuaG5ZMmllSG9lTUdGWW5KdlNaRUVFYStNVklyWlZETFprMzFzZG9SWjllVTBpQlZ0Mk4vemlJZU5STWJJRlRBem1HQ1ZNRkFDNDQrV0RxUXFxakZIKy9wV1creFhJQy8yV0NmT0JiKzczNzZScXR0QTk4VWFicmNBdmFWUlp1NWVubTdLMlpaWEV6dVo5WFY2ZmFxN0hkQUNnNlRqZlFsdG5UZ3BBK09aS1pHQnQ5aWRHNjBUUTdNbHBHaGhDSnAvVEQ4Sm1LOTZXSUdQMGVrazdXWllSM3MyZFpNZDQ5bWNYdFcvN1RJckNRVDdTZmk5TTBuVGV5K3MwS2FUdUk3L3hibmk1QVJlV3lheHdHMW9MN1NobEJ3cW8rSTlFd3dqUjdUcjZuOTRWaUJRQVUyVnZFM1JvalkwQllzaHhiemNaejBjUnppZXhGNm83Q1VRdHpPNE5LN29aQ3pKWVptZm9ZblVwYm85c3h1NmlLTk0xaEhsbHZ5Y2g1ZzJwckFERThvWVlHc0gyZkRrRHJWMk5OQzNodHdsQi9OTDVRdjdHL3U3b0M4Y3RwSUNwbDNKNUpYY3RwYyttNnAwaVA3M2J4U1lIK3Ywb3A0Nkhuajl2UTNFU08xZXJLK0x3RFVZNWorKzQ0ckQ5dE1jR2pnTlBwYzE0Z3FDS0ZnUmpEWE54cVBtRmRacWNEQm5tOWo2YzNqWE5XM1lWUFpIRmFpdUx1TU4zeGxxd1VKRERkQnNscTJ2a2ZNdDRlZkdmUk5EN1dJQkJKOWFaZjRsbTJSMGhDMXRqUDk3Mll4eWhPSCtRdU9BQzdLWjhZZHNnVWQxS08vSWNWKzB3Z29MSFpNdlJOcSt4dXpqUHlkM1ZWWWlmQU9QeVowREdCUDNIRzJtQkNxQzRLQzJDaXR5dmpWeU1zbjF5NktLR0NXV3dueTdMc2RYcFQ5T2VHdm90Qmk3Q0Q4cG1XbHlQVlM2dWdxczcrOEdPdXBvK3BDSFp4QmhjdWlwZ3YxU2RIc0tCZURpOXIxSFowa0xEa01meXBuM0d3MXNlYUwxbGVXb2p2K0dYeWcxS0VrV0NIeGMzU1R6T2pwVmF3WHpuZW5IMXlPZ1NpRGZDSEZNeU5YdnkyK0FmcmxjMXNoekJ1WjMzSFp0Qm03SXRzbENtZzlqc0syTHBpaHVmYUdoMmxqcms4OXJLT3lHWElMbzl0V3NMcXZJUTBKbzhhOWtUWDkyblZuNHZQZHppNGlrN1BKQ1pVOTd2SUdXOXdhM3JUNnNpRzZGK1NRbWVOVnpwQXZBZ0NqLy9QZU9KVVNzbHlUZEZxK29EYkQ5MVQyL2ZJNkhVNkt5cmtYQnR6Ky9ERU4zY01QUHZTeTN1M2xaaklDU3JkdFFvOFl3b1B3V3RMRHZ3RExRZ2lxM3h4UkJTR2dTbHZBSHB2bnFXSUNEWHYyQ0lPSFphZU11UWNKY0xvQnVRNU8yMUhvZmxzTGFtbnlPcXNlcDkzc3ZURElNYlFkSzdhQjhqNFdCRlFNa1VkRUt3ekdlWnEza29IcFoyajRxazdxUlB0eHhsYXZUN2xYYUF1U2p2ZnJScHp2Y3UrMkV2YmJQRmdYU1BlWHNrZU1QWWRQWUpEZ2tmKzhIMXdQSmtBSGV6NGNVaG4rZHd5b29qcVp0T1BOZ2I3YkIvNVdCSjcyVUFIMUVvU0Rqc3NjZFYxYWFqYS9FanhNRFFTWElIOFZzZ3VyZjUxbFl2MGZTUFpURGt0ek5jVjZreU4rVk5UbWFaQTZQWnFKbUViUWlWL25FemVnVnlZcUNNbGtqNHVhdmdaUnlhOXFyTjNUNjJLaHFMN24vTmZKUXVscDZ4UUlQc0o3MFlrMSsxRVZDSVVCTDM2ZEZETDZmSm5TSHQ4eHNVTjRlczQwbGpxUms0TzhBek5xOTdmd1o0dzR4c2VrYi9NcUgvSy93YlE5czF0NmJEbHdneExOeDhmY2U2b25wOFdjdTJnMXl5NDZISVF3ZU5vY0JKWEtXZW50OTROLzE3L3hjM0JqS2JuMTdSMlJWMytzYzBjSVRaQUtkTS9yNHc5N0NpMkRYWW1oeFV4K05VMG9DNk4zbjgxcDJPd29jTyt0NHk1UE1NOUFBa0R6VVNFNFBHSC9ZY3ZUN0xIaC9tR2FIWkdiYWtHTFR5K0dBN1FaUmZMbVc1OXRYdnRzcGlQS1cvK2N4ZjhxNWEvREpIMUlTL0NQQmhTeWQ5d1JvZVlzWEcwckVRejN4T3RjekRJVUkvMXcvSEFOK3g4eVFsMzlEcVg2R3N0R2tlU3VZaUtpNHRXRFdmRk9uTDZrNm9ZSldXK3prRWtuRkY0TzRzc0I4Y2ZZNmtwdXJQOU93cy9UR2UrREhUS0d0MXMyUlVKaVVFb3d0aW1wWlZLMGRISGlmS2hyL0YyQ2tvbU1TTkhQT28yQWpUdlVNTXBKK1h0ZkJ3S0xkT0x5cVNYVjRQZWNNZUE2aW9nWlR0R05tMVp2d1JJaUorTnBDdWxFcEd2eFNzNWdXVHRCR0ZNRDd2K1B5WjQrdGEvRzVQN0w0MkZvSndwRWJhV2M0eEpDQ1M5dytJREtYb1IxZ3EwRFFCTmFNRlJUdEgvSFRPWjVaOVJrWVl1em9UbFJ3bjIzeDd1V2k5WlFybGwvenczMEpzU3VHdVFqVVcyVjhjS0ttbUVlRVdvVXNJbEdrK0hpNktka2ZWNHZodURiQXJZbjBPSWlRZC9IVXdvYVl2Qm1VNFpoT0lnMHVvQ2VEcjNKWndCUHdTeEk0V09YRFZaSE1VSGs2SlF0YmlWcnRaOTZDSW9lb2MyR01JOXFxb3ZkRVpFZzRGc3l6cmdGK2JETmtzMzQ4OVlIN2R2Q1c0eHp5NUtFamROVHpKWDRTdzgza1l3TDJ0ckJwVUYzYk1oeE82Q0l5aFg2dWNtazJUQ1QxRStYRmtyaGszZ0dCUllsOE5tUlNEckhVRFh5N1lxMGRKSlpyblQ5Mm5rQkt5RHpQM2pXMWl4VURZTnN0NUNsSno0K3hHbW9HbHBKV3VjMGNQbXFNK01sZzF4SXRFOFRqZWZyZk1sRGFTWis2RzljZWlsUmxjeWJmWlBDR2VTdXRua21oQU1SRUgrWTljeUI5ZkhOVVZJSzkvT1lTRjVDQzFzUXlhbDVoOWtETVBzL3FxTlZvckVCWWk4MlJCQ0JWeDVaTGdHM3F1MDBFakxFWXFSUFZ0R05rMHBFazR3WnU2enliUStlTmZQQU10a2Y0WHZIbDVMSmZhVmh2STJCYXdRQS8yQ3o0NkFOYnhIa3RRc1pNZXNVSFRPZVkwK1RiT3czSlp0YlY0ZzdiUXlIMnRqbFlreG81WVdzVTMzOEhzQzlQbDY0YXJGcUUvK2xtL0dOOHp6ZHFYRzB0eDVYMUZKZTVYU3V2SlRRbXNuTVV4eFlPbmxnMGxpOHBHcTUwS3dScUN5cUp0VDRuTmJrUEN4dTBEU1Rud0ZFaVVEUU16OEZ5UWRESXdTNkk4NUR2V2xPOXdGNFlKNytvSXVvN1BKR3VHL1N5bFY4MEFmNVp6S3VQTXRKVnFFRE5Vd1pJZXJ2TUR5dFIwdnl2MzBGcmsyblVuOC9RcnNzeElJNDhaOVVTZWQvSUpxODd6bi9nZHMrYUhad1h1TjluMTRVS0Y5RUlzYlBNU0tkbkRWVi9UU3NNeE1jL0lwWHFYOWtOdjIrcm5Ndks4R3VZTHNqQXJHeFUyRXJMNFpNU3hiaDZLNFVUSTl5REJGTHBrOW4zVG90WmRhc1BVVFV3Zytud0xDdWIxQlh2WWtWay9BOGFBV29RSUk1NzhPa0pGSTF5OW1lWUdxbnFUZU01Nk9nOEdVQm1WZkMxaVNTamFWbUtRTFNHQi9xZmRZNFZMTU96NUVSZ0Q3Y0YrVFlaNjlma3NRaGFUczBDWnVnbnBhTU0raGNFemNLak5JZndZRlQxY0Z2SWZjM05FUFc3UGc2VTJoMi9OU1d4bVhhS2Q4OWVlQlNIL1U2RDc0MjJSMkFrQkV6akhzR2FOY2hxNDl1Y0lBSUdjS3AvQm5ubXRTZ0Exd3JIaDlnYlRvaHUyUndROFdSQ3g5ZjRTQ0htcXFBT2RBV2t4N0tDWEVqc1p0QkFJOERaRDBJSGM3ak1HZ2ZLcXZ6cnhXeVE5UjJTUnNWNUQ2R0xwLzNXRFY1NWdKa1U3cDZPVkF5ZFpJMG5NNUtQTVNKMGhob25ZbzY1MVVnNk9FcDFjWUZVRVMvSlVNWFVFMXA3NFRENy9mOUg1VnE4RDMxeUlWaUczcEtQdVJMby96K2ZseXlSdFIwZmRBbFlqRnI0QnlnTkpiRlA2WFJQYVdxZXJ1dVkwM3M0T0lhMlpDdGI4dk0yanNqUnZLR3hvTWt5UndGMmZJYjJvZW92SEFuMWUyUVI4TkxMdnBhNCtLMWVsZ2ZLTUE4S2VCcG1MelZwUkdKVlFaY1ZTci9NUWRhUTlYYzE0M1FrNGEycWtlQXZWYWxZWmoxTDN1YnRMN2RoR1M4c3IzcnVlRGU5aEI5N2ZxaDBoK3Z1LzlaRGh3R2FwT3BpcWtQSVI1dE5xanRzYVR5enRvMnRhcWVSUzZOTEtaTWFCZ0YxeERRQ00wWFJpdnZLd2lpOC9zYXFJNnNiWnJaTU1sRi9iaXlIcGUvQzQ4SUxMWlc1QkdqYzd5dk1EbkxLTDR1bEJFYmc4NzR2TG9KV0xqRVhQZWtIS1hrUXUrc3hHOHhBSU5qa2s0b290Y2JwWUtQMDNSZFVQblBVYVBsN3ZMb3pRVTJqdWFLMUdXWTN5UWFMd3l3eWJTRTRLbnFnbDVVV0JpRmJWMm9iNWkzN2FEbnpKRXdraVlCNURDQWNVQld1TVgzNkxVUGtWczFaejJ2TGlpeUhZeXNLYjUyVHFrNjdEaEdZK3dzaEJoZ1RNSStvRTVZeXhQNXl5YlgwVlpwbkpENmpQWDh4Y1R2SzduSEdlcmd0TUMwVEg5anVhRHhzNFRHU2tnQW90bTN3QVM5M2NObGpUNHlvaGtLZ21TazBrNXQ3Vm5yRHdDakNQbEE3ZzVJb0tPYVdRdEhtSk1rbldlOTBSTFI2d3d2cEJBUmh5cjl2d1Z4N3d3bWJLYThnWWZiY09Ldmp4VWRNT2NmYW5ORDBNcGw3U3luZkVJYll4SXdoM1AxeUd0WjJnYXQ3c0d3d283aXJHcWd6N1dsWGY5ajN6bXRwYmhpZHo2ZXFZYzNGRFZoVWpQY3kvREhzbitDMjFvZENJQVNGR3VOdW0zQUFOdzVKckVaMGdqcWxMeWNSNm0wVG5HOFpIdkFXR0M2OE1ZOW1rYVhKdERSTnZmaDhSWWtySy94cFZpQ0ZQeVM0czdFenBlYkJud0xmblpsdC9aUlBrZGt0QngwQUR3SmplV3g2NHo4OUpvTlVhUFZYR3ZYcjlzWXNqcWhLdm1GbmJ6V2ZjUUdOUzA2dVhNSlgzeUtSbWVVQmZwQkNBaDBVa2pZLzZNK3NOcmQ0MnFZR0lHeHlUdlRKeFJaU1RraUtsM0RacEErREVoM2trTXpXcUZSOHZoblNnR2cwUUtuUUpCQmVYLzNjbU5KOGpZWW1Sb21aUS8zaHdwaDhPdGNUU1pRSExzTmFwOWtJb2k4ZFhqOVh2alZlcjdmaVM0d1NoTVNMcmljbFJIWHdlSFhsWE9sczNubi8xbWtSWkFKS1FlQkIxN1hxbzVUa3RIZDVidUJvcUNselNoc0pMdlR0a1I5U2ZSY0UyZktEeEJBM2YrMnozNm9jdGVsQjNJcXZMVFErUk1WdWdJd2pDYTExQmpxWkFPeWNRSWkxeU1qUlU4M3F3WVo4UFF5T0syTVpzSUk2cmdhaThyZWxUZkRDT0VlekZlMVRvNkNvTUl4YmtwMnV2SEhSUlhlS3ozS2Exb0lBRVFqdVpPM29GbXJUTXdLOXpIM01TNVBjWlRKMzFPSEpldFdDSG1GWGZrbFpRcXY4Tk9qVklUaThRREN2bXZKR0lDYUhxRTE0UlRaQk1MYkxlQ0dLYzgwVEp3c2xlaC9iRW8wY3lEVjdDN1pNMFlNbXVFcEJrU1V2T3Z4SWV4NVFiMVNMZGM5WXBIM1FWc3hjUGlqdi9mdlJocnUvL2NNdm5xMlpRdEc4N09TdThoZy8xTzhoZGliV3cxSjdFM1lhR21kWGE3bVVlUmFjaWdjRGxZYVlPUWJDOGVqSCs5ZkdIdGxNWEhPeW9kbEhsblV5OHpZS295SDdYZmluWXJZUjcweFBTNFZBWUppZWhtUU5pUzhCN1NNRzhMcnpFazhTUnUrU0ZVY2ViRzJ0S3lrSXltUUE0RjRCYjh1S0VxY3FacGcwNXRUM0o0S3RtMU1sL1l6blQxOElpY1IxaTNnWEIzMjlKTEpycFZIWFljTk9GM1BKcHp0bEZFUmVwUzZPdmF5ZnlCNHFuVDREdDkzMGxmVXRPVEtZR1VGdnhmMlFqRlF2T0tpY3RPS3dhOHJlMGg5YU9nY0t1SDM2YlJqRUVGQVNYMC9GcDUyWm9MZEpmaS9PNVNGL0NRTkZ4QUwvOUtTTmRVeWpYaGxhNjMrMVlUZGIxNGxOZkVIUnQvS1pQUlV5WGlFM2NxWC9VcWtna2dSOEJnclNvQldESE5rYlZKZjluTEVPVlJQNjBrSDlMYkNIY3ZXNjB5RUVoNUY3L0tJeGRQaFY5K1dhY2RvR0l3ODVTYUhrWnMyc3hLQjhSYkk1L1RPZng3RzMwN29YbDAvVDhicEhiazRwblExeWNjWDlWMEtkSGFwbGZZMmlqZHd4RWNCZWRPaTdmVytPbVRyNmtaQmRRVk54azNBL2NKU1NFMmVqb1p3eGZ0NjNmQXd5UWNRQkd3WjJMY1VRbDJ4K3B6a2p6OGxmSTJ6VFc1SHZWVzdwVDQrYmxjUTVTY2NoZStqOTNoZzlFRThURmxxOGhxSW1FRmcyWlF1MVFOT3lCSW10WFpodFIyWDVkditaUTdvd01jV3pFVVFBekxBVVIveFlkZjg1SE9abDdaUGR6a2VSSXVkcHptVlp5Z0hWZnZvV3UzVHpOQVI5czVQNWtGMHhKMmdtNXJvcDNYOEtQTlNtK3NCNll2dER1QXFHcGtqQTQ0aVdZSXRsNW50b1dYcHpWRkh4THlkNmxPVGVKV2tpMlpybDZOOHUzcUsrY3pGajFRSG12Q0V5cFZsdnlmZmZkNFIrU3l5eklnMnhZUFNKaFlVWVVhTGN0cXAwUDgyWFk0enBSRzZma210b2VBZnZPa3p5US9jWVJIVlZPUmhPem94SUtrd3Ayckg2cE8zaDlzai8xUVJtTld3Q1JRWmZxeXY3NTIwQlVKMGJtd01YZG1CcFJraThMWEVqRm90UFg0RnFicHZkWEVLakp6aDZsMW5sKzIvUGZ5MDhkVEh3OFI4OEs0NG9rVlBLRFl0NU5GUnptVVExOTNiaFU3dUlIQXBVN1BoODIwR05OU1llVXVlR1pkYTltNDY2dFltK1FKOTJQWkhjbFh0WnZiYTlKM2VjVDdqUXFJZ3VDWFh3K3pSbFVhbzNidnZFMUVqV3NuRktYQ0VBdi9GeGx6bzVFNU5kdXlmaDIzWWZSK2NhaTJiSGtBd1ZPNGRBZmtiR3pjYXExa01xYzNXYm1hSU9NV21VUm15Z054OE1nTXVQYk9uckgveXplWHpSZFBYZUhXL3ZTYzF6SFNiWEZ4R2xoeHlhSVB0emVEVSt3WG5NeWc4RitDSzNwUG5FVU8xaVdkemlsUTYzZk96RzhrYmtFNHBlaTc0blM3WEZhVTVoL1VQRHpJeXRzMWlscDZWVDJtQi9zeFVqa3MzS1N6SEpNaUVsY0ZjVFZwdEJoYXpwUDM0c0M5NEFZSzdnQW5aeHVjT0FuZkNsR0RKdTIvdXI3bUZDR3ZTMUkvaHBxc0NDRDRUNVMzcXYxSVdsRllTNWE1cDJCU25KM3k5anpLcTUyODNtekFncUlxZkJMZ3FaTy9rWUVubk0rTnVXUU1xZzQxMDVFb2JSaGo4T0VCRUJHd3B2L0pzNkloc0REUEFld3RJZ0RVK24vS0ZHL3VreC9XWlZWaTQ2RDZ4Rm9pZHZhdGVyYzBXOW9lY1lXOTk3aDg2Wk5DSDBsY3NHRUl6YnBVbkdDdHl1ekZhR2JHS3NhYXJKRVB5M0RGOEF6UldxNllTdWRFbHdZVDBtZml0MGd4Y0h2OFhJWGJuWlN5R1lYZmROejNkTExiNHZBZU9IY1VoQkw5MUx6MWNCUU1ML0JYWENKY1pYb1hyS2h6N3Q2bUR6ODlaVkxubXJqbWNuL0ZPTjZiVURrSmFHVDZJbGRWcmVsUktvNmtRaUVqZ20zY1RpWFk4dEJNS01rSWhNMERxOUNyQllkNjZtZXZ1SjFjRUl5Qk05L2VXK0g3N2xaL243ZEZuUzNMWFFZd0pkUUZBZFRWSEdFNkNRNlFpOWFHWWRaSVJFVHV2OVVYZ29JUEM0VFVCQ0dPVzBUR1JhdUErZ28rOHVWVFpLMzM1Y1FGYXY2N0ZIaEVRUEZsWG5pMlFtUFl1VHJrT2NVcmRCWTEwa1BPekRkNjFidThEbWdZWUhpVVBEaTNQSTZnQ2NIYXUxdk55UW5aR3BWdEQ5VG1JUXZQelpxWkxLaVI5TGcrY1pqclV2Y2xTbmcxdEdrcjJleHliQlBXb1lVdVpDVnJJT2FtaWZReGRLVDlNKyttL0FLMXQ2VE9OYzFEdTBReUdMUysvdzJZUU9rNExrUUIxTHovMWhkbWhvYXd0UXYwLzdVZ1pvbFZvVjVkaXFqUzBVY25iQitmUUpXd2t0NC9DckFldmZiNERHMmU1SEt6VlFrQVU1cE9pd0o2TWV6b1k4cHJlb29zbGZrUS9zY2pjN3ZBUWJuM1JkMjM1NnZEQVl4UnRpWEp6aSswZ01CWW1QSk5SbWlzK0pUYUltdFpBMWpPZk1sVmd3VXRmNllYUUdKZ0ZPVktXSWhnUTN3cWxMeDR4TnBlU0tpSTd0S1dhQm1CYVhjRkxrM3YwbVA0WEdudUhCK3M0NUYxTnAxYWtueG9wTGEvVzgwT2lNbS9OZ3ZkdFJCK1JINS9odHBJTjJnQS9pajI3NVNzSG5DOHZZN2k1dHJueDR1Qm9qMndSdFVQVERTK0xQNzJPZ3VyUDN1aEtmbHVZYnI4N25VUTA5TnFyNjQ0UVV5ZlpieU54cUlYc21OTy9WeUs3ajgvZUpLWjVMTVhzdDN4OFhyT250dkRPbE5ZTUc4YldhWlowYzkzbEFnMG9PMCt6VkZHRmNQcEVLVUZnMjZ5VkhvZ0tWRDdZZnVxTEJPeVozc1doYXlxeVdIank5WlorL2tVbC9HN1Q1S1YvWFJ2Tkw0ZzBvNWJkdGVseE9saHZIemhxL284aFVzbWhMTVcweUlZbnVHaWduYmI4TGYveUd4M29VV2hjbStod2JydXVsazRPc1JKNytQZ041TytGeWJJMXhnRnZRUU12UXVET0xRWUh5Ui9XT21RN205cmQ2MmpKQWJFdkJTelErYnJmZW03Rnhvamh2aWs3SzlvM0tKcmloWjhhTXNkeEpuTWcyTDFhSmszNEpTN3F0ajBaUE5sT01NMUtiTHhQVEJvM2MweUdTRFF5UytMWk55cWpRNTY3d3VMSmxUakhvUTMzTkhqSlVUV0pVdk5STDc5ekNSdUNnNnJTcXpUZGZPRmVOZ0V3ODMvQjJaaFR5aDFUajNjNFVESEtrZkQrTE16ODdLSTd2UmlJSnNRWmFxcmFCY2RZZm1uUkhNdERSSzlIaVFHOEsvUUlPRExObWt4aVYrcXQwbjhYMWpQQU5UWUNQUDhGRmJQbmRsbzlLVXhsQWIrWkhFYVNkbHF3cFdxczA4K0gyZFI5Y3FDMzVkakZWUFhibmhGdUx3Q3NJckpUcndiK1hTSnNzdXNwUk9LUXBhckk0Y1dtU0JRalpwalltb1lYamdWYWV0QUNFd2lTMDVTOGUzSVhQa05DUWlacG5tdzF0a0hKS3RWSkg4NWVUbW1mdkxJL0V5V2RScm9ZeWZ4cE55Rm1BT2t0RTJnWUpBNzBGSVN6QytqMEZZSTBxTFg5RGRjbTBERy8zZVZnUmFBVnp4RkFmMkhYRk5pZEwvSzRieVltR3BHOEZUY3gzWGRrdHhoSU9pakVHOHdLKzZ0TVBjbVJqK0FEZkpxZFpJYkdkQmRIZ0JTMlpMd1lIbGQ3OFJpc1JYTkNpZ0FYemFtanFuRmt1NWgzb3VQN3JadkwxTlJYcmNBejdkVnYzcWd1YUxxSUpEVjNrR3RQMy9lcXFhWE14L1p4blNubGdpeUxTcjlvWHVUblZZK2ppQmhhSGoxSXVLTHg3bVFrOVphdStKNm8wb1pUSFViS1ZBNHFtQzJhWHlPVDlUUG5LZ3kzQXgvbUoxQTRDcGxIWS80c01pdThMcW1vKzdXTUtyYU4yWUdlYmFCNzgwMWNaWjd6aUsrcmtHSzlTU0w0Yzd3bGVmNnJVMGpodkw3aXRkUDMrUUltbVB1NG83OXNZclNQWG5YN0s5TWtGTXp5V3Jpb2ZXVDBNVmhKMHlDemlZbUhrcG90N291NCtFUmdZWFdSaGUyWmxySjhWdWJlNDExZmJGR2hVTE9hR0xMTjdXOGlvanJCblpXU0RGMmhSWnNGcHliMWUrMklUMS96NXlZUjJtYW5OdFp1MEEyUlJiWmthTkxMd3JGR0ZaZk1iSC95WGlSUk5WbytGdGhPT3BRMlJmaExxV3lCMVc5R0pQcU1BMzJXZnJsY1Z2eWdKeFBuSTJxUDR4ZFJWajBXV3IwR2hxZUlweHJ6ZWNnTzhxU3FLSjBNd3c3c2Z3a3NGSGV5MEMzdlpqQldFTUpsbnozeXV1bmZoS1FMVDJZV3paTVZWQk51TmVFa0JjZDUvQllxcHcyZTFZNjNaU2NVK1E4WXpndWw3ZVREVGxyQlBaeHVxemIwRUYxNG1pR2dEbGovUzlGZktVM1NiODRNUTNmNm40TGUzN0tEK0VmZXR6c0oyR0hCMlNId21CRUxQSlM0Nzd2Y1A0U3BCVFMvS1c3RHRHRTlYZk5TWEhlMUJ3c2NIWTM0ZTJ1UzlZRHFUVVVaMEJvdGdmUFhLelMvcmM5bWhDWFNBUmhOZ0ZJV3F3T1FrUUtYc3NqNGFtclFFN3lTK2xsRWdXV3hoZ2lNQk9KU0NDeXhzSXUwSGpJc2pVc2dXbFJBQlRTSitXemw1WWlMek9aVDArdzFqL1lNSmgwd2l5eHYxbmtrU3I2RDVtNEhSYXhrV2ZRS2E0TEl1Z3hEU1NIdWM0aTE4djlSTitJamgwaVNsWW5FRjNzOVloVkdnVnNvVEQ0NFNlNVJwNTFwUGFqM1J5b1dFVnZlVGxkS0RXQklRTEFNRDQyUE9TZmFvbHhjWGpuanl6R3ovQ2ZLc3hVWTUyY1J3bnlUa25PbTlPbStpTXBnU3Yra2JkYTlTTUJoWnVKaFMyU1lKODd3SEpUVDNVZDFnRFo3L1pROERMQ3ljalRzSWtqbWo0eWU4ZlZ0cVdER2VOZjNLYXhibjV2YkhXTm5tVWY2ZzBaczJRcm43RDNwSHp1bklnOHcxS1VYV0Fxd0pIYWpXTW84aVJmVVNoNE9ReUJGY2JWZlpsY3NHbnBpZk51R0FtY1Fvc202NVhNS0x5SS8xaS9JY3ZDZWd1eGRMZVlHR0kvUWtzWTZnRGdackRSMnBuNkFNbTQrdHNDdkV1WEQrMTZXMnh0azBHVUplV0w5ZTZvck5QY2pWRXdyUHRhTFZNWlA1Snl6VzJzeTVoZXdUa2tFRkVjaWNPa09kb0xRZFNCZjBYYU5rZlJscmtwWWIyYWhWbEVBTS9sQVlyQnlGRENTR3N4WitRUnRzWE40SE4yTld0M1A3REd6UG96MGExOHozdmtsNkE2NDV2eW9EcVBPSlBnMEgvZk95UUtmUW83WnBIOThFb2xpL1ZjWmpxTEUvaVhCOExpOGY1OVZkdTJqdkJERFduRy9IR0piWitSNVBUMWJDUzQ1UmF5azYwU2tLcHZhTWVWbmhOWUdvY0JTREFvcldjR1hpdnZpaGtVQXUybGs0QU9OWTIrUG0rRms0SFU4SUYrUDVwOHBtcFFUcG8yeUFjU1lON0U2ck1ZL1I0ZmkwL0MrdUZDaVp6UTVJS2ZlZkl6TFNIc2krN0pEcmk3OU42VWJFQTI0M20veEErTkRpT2N2Qk80UmpWRG1vaTd1bEJrYXNaMFBJOWk5a0VBRjdWdm0wY2FuNFQxcWFCVlk2S0FSK2xHWXFJWmgwTjllU3V3UVduV2ZNZmJacGE5YmY2cEtFcWVqZ0tDRlRHTzVUQ29kdFZLVG5DODJNSzNUemNDZXJ2TjBvRERaTTJYK1QraE02UUhEV2ZxdDQ2RlYzSHMxcE43M2F4QVNqU0hjUGF4a3dvNE9tbktHdUw5L2JEd1NDVy9VUUloTWdWcTRHOVNMUWZxZnc3L2pFK1NSNXJVWWtOMjRCYWsraHJrdFVjdUJESGRJT3d3ZGd2UUlXdVNDZnNtZGloT0p0T0ZYaXJsdXV2QzNMV2NSc291SmlTNjZ2RzQ3dlQxdXc2Y0Jja1QwRmJqZnRXSDF0NkZ5eEtBZjBFRDVFbHBiRHVzdU43OVFQM0NZeTVkQlRDNFFYNys2cVJnRTFnZi9kcnh0MU55L2JtTGFrclEyVUZtYjJTcDQ4TnZodjJtelppdmxHTEtuOURXOFZqWmljMXJKaGxlbGt0SHFnTmpqYTQrYzZLUVp3Nm1YL3EzRDN1UWJaR0NXQ1dLSHF5M3JYZUJMR0wvMk1Ca3E1U2lhVUhYN0FVWWtrcC9wMVJ5Q2ZYV25xVzNEWkI4ejNqQVFMSTJ4L1B3eXNvUU5nYVF1UWtuUGw4cnhrQzVjVVVwSmV3Vm5Dd2V1R1BkOUtETHZCNUozemVRUFhaVytjYURHWkVqNUcxdjR6UVJpK2hZMnhaNzZkT0pleWJCZHU4L0JQdS8wT3lHTWNVK2ovT1FLRnduQjBZM1h4RkcwNDMxNzdKazJDOTFVbEcrZnhHL3ptYmd6WDZDUEhzeGFWL1pmNm9ESHp2OCsvYkE4eENuL3I1SlI4SXMyanNOUkJ4ZzdrRlYyM2JlWEtVNHl2MjFHN0tpQkJWN3A1cmgrY2ZzV1NsaGNEUkJMdXJNYkx4WUNKTTlQOGN2ZzJwQlJpZXhSRllmeWthVzhwV2VsVEFIVkZ4VUZubWtJZ0xMV2ZDRTg1OGVhcmZXNUswaGZxY1EvZmxOSzB4Y21XdXNwMnN5NDducXNvbDhZMHBoU1hwMkhSWHZVSGNlY25veFhiYlY0ZkNaQWFpdUE0a2h2NzB4MGlpRXQvZVJHZUR0K0srZ2FZMHoydU1RaUJPbko5Q0N3bW9TeEtXQmlHQndJU2V3MVpXWmpDdjJCdDFVc1ZudmFRaDU0bzQ0bEpRZFJRZmhVSEdUY0ZTTE5IeENFUitiaE9vUk1jNTE2b2NYRkxzL0NsSUZyeXhkd0tYbWtYTC9Cd2tzTyt0czBaQzE3M0JSVThBcldxSnJ5ZVIvM0VEUTkyVGdrWUpjQi80dUw0ZFlCMGJDZkxRYkcvUWRXaC81cVZWTGZiUGdPSkNIN3hHbzNjSGpNeE5WOWh0M3I5cWd4ZzNDb096WDYycGE4T1lTZDJaMTZ3QWNiYkRoNnBBOXZaMmx6VXBOVitDSW01TVNkbGgwZ2ZNWnpxd1FwMkh3YVd3SWJaOVJjKzFLekZTSnAvS3RGNE12d3lmdGVMU1J4R29nbmJhelhUMWVXc0tmdXNWT2ZOY1lNUTlEaEhIZmhUT1VSVmZFVHE2WDhQZjhFUlExa21JMVRLbDZZWE5ueElLQ3p1aTVZUXFJd2sxV09LcWlZZnBVNlNwQVZTa2s1RFBMMkNSUS9EaW02RDJkamJMNWlTRXlhTDRyVXVDeFVIckJQWEpPNm55VkpjS0dFa2h4MTlxdnk1QmdhZ0hXam9tT3RlbEtPaXcyeFZnM284VWxueEIzOVNrNGw3YVhtbENuTzhHeFJSZ09DRk5iUTlZWDFUOHFpc0ZVUXNQRUttalJ5N0diN0o5RnZzRnBtNHU4MlpDN0o0RE16S3czR1BTS3lTcFluSURMNFVwUEhFWkdVazl6N3Q0M2xFY1NzeCtFalVSeFNjSS92c1paeGJvb3JjR1d2NGRsZ1UvQ2Y4R21rRUtHZDRLUmZEOWFndTZUQ2s3YjljcUdlbVdoVjNtWUVqL0NrWFFtRzY2MXNlcjlUTThRaitjODZIVE4rT3MyeWxORFVrUWRrZEhCeUdNOUYwS0lZZldPSGNmeXRmdUpUSWVqV2VEQnBCcWU1OVZMUFVmRy84TjhYVDhCTEJ1NldZYjZPclp3bWdnSkd0Q2I1Wmh5QW9oMDZRQW11dzd5SVkzVFNRYjlUQyt5WnZFNmFMOVJIdlhlTGhXaVhzZmF6VlhHS3pKQStMV1VndGV3MkV0ZTk5d3dpWUc5UTQ5cWhnNkc1VWhyVkxNTk84aThqYTAzOUxzNlhEWDFPNU5ISEFUOWFVNStXaW9VT0k0aFRJSWMyS2FCS3ZCZXpyUm1yaXdPY1V4RjFxWE9aNVVoWmRuNzUxUWUybHNCWHBjYzd5SWplQ2JkOFhlUnE3cHhOTUx4VFVWYzZoajFuc1hGa0UvV3NWYjVJNmFNZDc5ZC9LWVZWQVdGZXNWRStPVHo5dkQ2QVp6QWY5TnFXS2l3WUorSGlqQUlZYlhmL1c4UXFFeUdhNkVKM1NiL1V3ckVFdDVUeXBMbkdaM3BZdXYvbldyY3RGMldveFh2V2J0NTU1aU5yTTZoakYvK3d2TDZFYW92ZzRHMHErWHB4a3k0dFJ1NnN2aTZvaExYZ3FYWDYvVzV1NHBNdWtPaFM3VDRvd1lVM3VTNytaYTUrdC9GaSs4WFpiVXdDOFdGakpkQW1BVXpGRlFvdGRnWWFsWlg2NXpRcnB6aWpETDlnSXlmcUZzK3U5azNyaVA0Rk5QZE5wZ3NRMEhmQTBieXQzaytoRmRRV0JnK3dUemNmOUUxdTFZRTNkNWtHTmpwVU9oVXQxa1IzQ21sTWZEUHBUejdsVnp6MVFRUzNrY2YzZ1czQndHTzhEdnJrZFU0OEQwYVJyS0RaNVdscG9ZU2tkZk84L2xpK2lYOVlieEQ5VHJENUdKK2hZdWJvUlV6YkdEMkV5NS9tblZnZlVhREs3dG5uVVMwcGhBa3Z5bmc5c1FkdEpkMERub3h0UHJEUVBWdkhGY1VyMTRMajhOYmh6YXlZbXFTejl1aWl3NnVOc2pBTUFNVURCTFNFYWlldllYTmY5TG9hQzY5ZW1uNk52ei9STm12WnFGOXVyZEhKcTI5bDl4Z2JhWGJxdkYyQnZtY1V0VHphako3cmt4UEdLQlpBVTdPcnpLdlloSFZwWVJRN1BaWlQ1OWxRSHVUdzhwSGk3aExodnBFVFMrRWlHZmRheWREcEZIYWxrUzIrdENCOXFGcFFnRXZMM2QzeWZNZGNJYzYzMHNGNjV1YmREMzF3UUZVS21EeG40K2FFVHoyRDcyaTFWWStBUERCdjlvbXhIQVBoMWUzbk1vcXN2STRWM21yT0Z1a3ZzUVprbGdkcjRNOENocnhMdlpIVDJFa25ib0hEY0xzRjNIVTV5MUcwbXdCZE84SkIyZU5DK0YxUloyNHpjQjl6UU43YTg1NnRUa2RiWnI3cEsxNEE2bWtEUkdqdE9UQlZMdlRVa3Z2NDVGcGtMS1pWMWJhSlM3VTUzL0ZOaXU5ZzZRdDNFeGY2ZjZRQitUU2gveExXSERuRjAzN2FwZU5KK3l2cldmTXVlMCtNSjF2dnB5a2I2QXlTSmM2d25jOTVGR2syaFZlM29jTTU1T250bzJwdDl5RzRDU091Ym85bzF6akdZUFE4MnF6bVNwY1NXMitVcTJxeFhYREpaeFJoSyt4dUxtS2ZNU2Vyd3owbkF0Yk9KQTdVZWl1UENTVTBZakdRTzFJWHJwd044RXRkRjdpWFNVdHdEcUgrQTFTdXRmSGdoaVVFTVJqclkyNFlxTWFPZzRXTm1sU2pQN25NYTRVU2lsdmpYQUVXOEJwbTRPMkY4Z1VIU1haaTh0VHJveHgzYW44SFBoYmhhTmdUZHc4RFBBbVZPMWxyOFJDSWd2QjFIelZrbnhQOXU5WnZnRVBhdkkya2tVL2R5YTFzUWZnWm52M3BPVXV3dmJTNTZkVFA4Qi9xOWNsbmZHdjZhL0pWOXQ2TTVPQ3dBRE95YmtzdGNYaGk5TEp6Mmd5UWQ2TWx1NHhOSlpGOXVMRnVFbmtibnNyYnFjU2kza25seHBMck9tR3lidVlmNzVvT3ZRY21aaTlVbHdPUnJxWFVUQmsyNDdvZmhZK1R5OWd0S3hiK3NZWlQvNmVoak10Q3A3aHI5SlMwTmlGNUZHNm9pQkF0ajhxVXkwSnhERFZrSGwyUlFoeE5OMGQyTTV0SmNsM1BpN1JXcEFZUXErQ29XSGY4WlJuSTB4SVRIemxnOEc3VUtGb1Y5SzV6VFBPSmppN0crQ1ZoZ0luNVhpY29Qakk2NForenhLUjlmajVQUnEwd1NBVWVXN0ltcVpDcXRvY21adzljQWV3cTJXVVBGTVVSTnBHWFB2ak5BNXU0Ui9oa0QrcjRzalpHKzIxdkw1em5sOS90V3VvUFdDOEFjcjFOZXR2ZC9rL3FSV2kzWHp0dElud290VC9Wand1cVV6MkVRSXFzWFFGbXp3bVRnOTgzTS9VbXlEYXRxMXgyVFhwZ1FYN2tqOUxxZEtDemUvVEpIczl3YW9SS01wMERnbmtJN2F0Skw2cm5MckFRcjVsazlYYnV2UnMrTXI0aUQvZ01GVG9yREVHQlR5Tm85TDlhTW5hdTMrNGtLQVM4aEpOUm1WeVhGWS9qL3dXOWp5a1RmdmNKblFqVXhOU0tLUjFFNWR3citFdjBQaVY0SkYxd2xUVFpWdGlTQS9SWnlBKzFFQlVjQkM2dU5jMzhiWXFMemJEeUE3V3k1Sk91Q3BWREpEcjZYblowa2Fmd01oMytpNmpmcVJ2bWJJcm9SMUVaTmtEYm4wdUNvM1dXMy9EMnVjZ2UvWjUvNGJNSE01TDBCMUNFdnBZRXl6bmhQQnhjdjk2M2lUcVZ0M1VKR1FOc3hJYTFESkpyNUtuN0ltRGZlZ3FSTVJ5bk9yNjFGYjhNYUdBeVg0bWhWOFhEM2JkbmF6cDFJY3BqQUpwS3VCR0FHNkdvUXcyWGs4MFVzS2dRM0hxS00yWmZnakxmaDhjQm01Smtzd2thSzVEWlU1cmtVQkJiR3RhM1BqZ1V6cHlkK016eTB0a0Nwd0VvNkxRN09URFlJWkhmeDRyQmFFWS9BMjBMRzNrenYyZndwV0ZML2VrL1A2ejU5Y29UemRLbTdpS2dFOEhkaUc5VXZ3RDFucnNNZ2pXUkhoNzRDWGNpeExXbk1EckJMRlV5MHZrY21WanIzVFBkUm9HekYvZE4wcDR3dnNZWktCWC9hQkhxSTlyOUVwVldYS1NKWEpJclR0V05sRFdCZTlPbXFnbmtwY1YrSGZuWmVGUlpSMEtzOXZsQ3JSNVNOOHkwNDllQzlyTklvT1hSS0JQSUcyQlliMk90ZldvNFJ2OFp1NXU4YXVvbmVZb1BucEpQYlJjLzh6c3NYUnZteTRDSUNVSTREeGtoVkdWelRIZ3FNaWxud0lWbzlDZm5aZGgrbHFPd3JhNTM2bUdScEJuS055Uko1RTFjQUhxdzA4Qkx6Q24xUmNJSUt0S3VzV3VRR1pudS9GK2JtWW9BZlhCZldQaU5wWk1rNDlwblozeTNieDJYYXIrSXpoT1d3b0pCb2xyTWZyNVVlZU1GSDc3ZmxnTzQzL1VTdHZRVEQyQWxBOUUrVDJsRU1pQ3p1MzRmTFd4ZmI0TWZ5eVh5ZlVIUUhCZ3ZZLzV2aklCb2RWYmFweDlCM2ZXN3JRek42ei9ONUxwQWZ4NTB1UTN6aWd4ZG4rS0YvaFh5NkRQbXBJZGRvSW1TUTlBRnFKcU1jVHA3Mi94amlpNVdKY0VUZnNUNFlDeXpoUDU4U01OcUFHSFpHaE1EYnIrc0VpT2pYTTJqKzc3ZCtYNUlJMG5xUUFwUkdQWG5sQjFvWG13RGVhYk9TZytZbnBwM3djenBGQjZqaUFiNEZucDk4d0cydlJiNDIrTmVPbEN6aXJFTHlrT1FTUEVramh1a0ZHVW05aEY5K2lPUUpUTE1IUmVWakpodW5EUGdtZTRBUStQN3l3Qy9TVXZHRHlOd2pwWXRoL3Z6Vyt1K1UvVUFNZkliMGF3M0FYOXA1NzJ1aHRIVUR0T215OUlYbGlYcDdTK1J5cC8xNHpkcHdEM21vWUU3c0xFZWFaVGJhRTBsMFRYYmJkQ0JYNnVDRnJGU2E3eVpSSTd6dEdNbHZGby95MU9xaHMydGQ5eXdvdmU1a1pPV0VHdG1JRUIzRDBjcHBJL0wwMHgzS043MUJGcHIwQ2QyVXI0bmdLendoKzE3Sm80b09DYk14d3pacjhVR3FyL2U3V1V1UW9rajJIVXVRUHhEZlRZaks0WkVpRVhsa1MwVjlTbHhhejEzRWpVOC9yZld6dGZ6YmlhTUhxRmRITGF4VDFYYVM2NFZqNTUxMEdRbVg2UHhzcTN5WlVGaU5pSzNLczBROThjakJuaFc2dzhCNXkza0NaN2p4MEJZUVFOUE1jejQ5NVYwMi9oMmxvTS9FdW9JQ0NZd1V6VWpQd1VVRE1EcFJvb0Y3K3I2VlVieVcvNk5JYkgyWEJlTXNRNHI5aEM0R1V3UkdOSUdORTVPbGJzWS9rTko4Z3U3MG1BRWswT3MwQ3lmSm1Kb2p6S3lndVUzL1VTOFJNMDlvQWxFRE5aSm9aNXc5ajRaVDloVXNKV2p1QU5NOXhQZWxSWFB5TkZtRXZZVnoyak9PaVFPVXlMdjdLMkF3UUhRdVRPSkVQNUE0MS9sREJEQ0ZscklzTnBGL3F6N25rNVEvd1g1YW5nOFlhbzZwdkdOY1NpODFvNzYyUVpMbk0rSk9RbVZNbmxPMzZYUkZZZmhseU5sZVk2NXQraGNRa1h2cktiSkw5M011bzJFb09JeDczQWpJVHdmdFV2WlY3VUFleVQxeW9rdGZ4L1lpWnM2OWJ5L1RHeWNYWTBuZFFjeUxrNzZ5K0dXLytBZS9maTZ5OXpqVGFtMXdpZ29maWdyOUlzRlZjK1ZmZHMyUkJxcCt3VXZjeWdmanVldUJJeWZEbHFhUUt0dTBMbzZ6cmNXeGhlK1JieEwzSjZrV0E5TmQzTktBcUViQ2gyOU4xTm5GcUZKU2Z1eXd0dVRUN3cwUDZRT2RQNHNVSWlIRjdLR0dQT2JvOG9xSG9zNlFBV3N4YWNrVzJYQUJ3bXhJUytseDlVSnNoc1RNL3lTT2lhZURoUllQUi9oNWx2RU5vZ0pFTUxTWk1yOXVpckg1b2VKNTcvdWdsbWhQZUZ4Z01paTJOMi9tWTcyaGltWjRyV2xoTVlYTEYwWlhWVWZFaUVubW1XZXhKcFQ1eTd0NXY4dHowVnd3UmVMNzlScUduOU8zUDlEb3dyZU5XRFpYZXZJSzQzbTh0aXJ5dElaSlkxRzZtZCsvQnRPcGJrNmlQbElFUTM2cTNqaHZyekMrcGNCWDNqdDBTZVFvNjNjazkwWVJzakZrNmMwN1lLOFRHY1gwVVNna0xXQzNzTW8xUGN1NFBJTjhmWFVZRENQOVYveVZJOU1EZ0JYQ0h3MkdLUFdVMFA4djJBUWltQUxUYnJ2V0tjbStnRHRObGlPSjMxZEVwMTZNSjNNOXE5Tksyd1JCUmdkZjlBTEs5RHR1R1RQSTdIcHZqYTVFQVUzT280ZytSNlJmSzV4N1Y5WlY2Z1daSWFFWldyZFJKM2NkaTROR092Q011ak9hVkVtUCtvVjlqSFM2c3ZtZEl6OFplcEhidENldlZ4N3RDbWNUcmtyeitDb3UzUTZjWXdHSEJuZUpESjd5RXk5K0g5cWduSlo2ckNzbERjNmhKYkp2N2QwQ1BzVWMxdzYyUjg0OG1BR3dDU1paMUdEbEpJcnNST2FUZkZCVHpVVUJmYTNZdGh0UTdkNWhCKzMwU2NySzdYVlVOSTJaRUpBRk0xZFFHNWF0bCtvZ0t5N3ZRMWJlRjN1cmI1MTRqd1BlMmFhaTBteEVQNHQwL0N1WVpvaEQxTzcwZVZWRFBNNjhDT1pzaUhyT08zajR6M1krSm45UFVaWlNFRmkwV2ZSQmpCaVZaT3lybXpGVzU1WWpXNlJ3RktLMVZEVG91M1REUXc4MEhFSzRTZWJEaFh2UWNBMlorMjZHRWQzWU8wQldpMXR1RFhSQ2hjL1oxWnN3RGRrckVlNkNicDVqS24vWkdNQTdGUGRTUmJCSDcyd0xhQXJYQm9OYjBWZG9HblBWaTB4NUNXTXJnYVl1OVpvdmtUdFU5cDZMR2N0YjJIQzlrWndoeHJYRnpLN0VTK1BXYXdQNUxuOXMrQVQvYWNrdG9lOEVqaFRBMExiakJKVlBnSnIyZWZIUERzZFNidGtVQVFNVkxQZFlidHpsQXVPcEJzZks1YjBjNUtYWlgyZy9kaFkvaHUwRUNvUjZlSnVveXJ4c1NKKzlZNUYybEpDVUpaUGlubnU5K2lyUTV4c1BpUGFzeDRIaEhlMWdlZ3VjUm1QMHVXb1F0VkhOZHdqaXV1VVJFOS9WS3ZKazE3QXVlRFY0TGNrZm56VUNDMWdtU1dJZ25WWWh6L0FkMllPNFFCYVN3S3JNb2xlODI3ODV2NUpsaHJLQU10azJ6OCttWEtzZG9BMG0zVVBwMmEvVTA1eTRjbThRUFpQenFTK2lZM0N2UEdqWVRHc1U2NlZncnNMa3JqckEwVDNCMGdFNFRTUnR1aE9wOVB2b0lUWGp2L1lMK0pOZ0c1N1hEYThXNThsb3NiTTA0M0NvZ0RBcE5tNjRML3cxRWRDMi9GMHFPZVhtblpuQlBrQ1NWY3BhUVpPZExFMkU1cXp6R2xZYWZWZmJDMlhLUGg2cURUOHJ3R2JQY1NUWlpKaERWWTdZUTFZOWdtOXJ3N0ZPNDJVbHhrYVU0bGpQR3ZaamNrUmh3dVRLbndoYWZDbXNBYyt3Rm1YVldqVWpIYmd1Qi9vdVBKaVhwWEFJOTJHelNtb21VbFFpU1JhOGpoWGV4Tzk4MUM5bVVib0ZhSFJNK2gyallnZ20rMmZEVHRkczA1d1NoM3lmRHUrc0tXSFRhSk1QaXBLYi9UeHhyR1ZjMTY0WmpUUW1MSVFrMVFZa1VnSTBuVnVGZXp0aCtrQTZOdlZyRjFGZmJjVmFxZGVIWTdNZ2F2clpWYmU5clhaS1g5K0g2SHJidmVjWW03VGhDWk9RWUxpU0JZb3ZQMXNuZmhKbTBzSWYvZC9kRFdVYjV0ZWVXdnFvTHRPaDF1ZDF2WDhwQ2cwM3YxWEcxRTdlWUpJdStITXRyQjZGcDBGZEpvazV3SUN4TzVIa3dRNEFML2t2RTVBOGlkcUdRRThTVWtLRVBnNDAzNjdXNE0xWjQ4RnoxZkc0aHA4RE5Yd2lzc1k1d2ZSaWdMNmdLZ0hPYmhEQjBDTTNKeEliaXZQUVQxTTU2Yk5ZeURTYUJNRk1kUUVXa3ozUVloaUtCdWVuakYvYmpramM3ZzF4L0pNSmlsUE1xVjEyczZldEltajlNeXo0cXlWRnJsUUMvYzNKYW1WdzVKcHBRZkk2SVlmanRmVWlON1B3N1lIVFppUkgvVSttenJBSHFzdWRqOUd0L1RCaExzM2dPWm53UXI5UDZEaUlrM1p6WDFMR3RHQWpGSUh1eVUxdEJyTGdDYWZBcHEraXNTQ1FqWDhyaC8zNGRvdFR1ZjZsTTVMQ2NwT0pwTFhMb1JBSS8rZG9BMXF6bDh5d1B4NGVXOWwvejV6TVM2b2tyeU92d3A4ZEIxM01WNlRIMmxLWFpQM0ZxU1VXTVdnN2dPR1cxejlyN3ZyOTdIOHA4bjl6SGtRdnE5YnVTZHp0ZEJTcEpMcFlobThLdytMRkgyK3FhQmJOSXZnMjh1MVlKcU9peFRyd1o4cm1IOGdPcm1xM1plSXZnUXVPeVpWNnAyc1pXWlM0MVh3d2ZGMHdwQmZrOEhaL3dEWmUrVnFObmZuMGNXNy9pSERGQTRVL3M2bE5mcm5CbGR6TzhsdXp1TWsyZ0lWQVRGTkQzZ3lrWG1oZmZwS0hiRWFNQnNxbGhhR2M5b1l0c0U2UlpmeWMxQmpCMFFYazNwTEpXZ3FpNjcvOVUvMlpVT0NkNUxzeXRTVm9ZZWNZbVhRaHRSQ2dib2txZEU4dE5BdmtFU3FWM2tHV0lSNFlBdVNPZUQ4UUQ5RlNZa2J2clppTDZubGVyR1UwZDlZZWdMNFdLemZLQTg5clEwQWR5c2FzUTh2ZU1SMEZRMDRrUGNodVNjdlJ1Z0hOSStxTFVxMU5tdHN1WnpoVlFHUDB5V0lWTWsxZ1hzYktjZjl5c0RzVEltdGZWaCtmdXFUNTh2eE5BSHJRdkVrNFpMc3M3Y1lWWjJmOCt3NENEZTNHYkhkNnMwTXJxSHh4Z2wzNklCMUtBa1BjRm5tV2hHTXNrU1BRa0RBaVJ4WW1maGVoanltMWthWW9TcmRsL1ZXV1RtYlNCYkNEamZIbEFpVE5BU09YdEFUdmpYNkJ1VkZYMzJKZE45Y0tQQWJ0MkdMOHNIZlhhNHYydCtkbHZFcWhic1NXT21tTzZBb2R5K25INytVbnBJWHBrSU9XeDY2VnA1ZUJYdHB2dEZwNk1NcTdMdWRiM1BmT3IrQUppTTA3SGw4SEtLbW9OcThvK21rK2JQblFHNGQxRElNTW1OaEQ4SzBhc2RieWgrWFdXYXIyeXpsWE5XL3IyQXRQQ3RkMzY2bXoxbVFYNU1KVGR3eEs5QU05Nm5BbEIwM2gvQ1RleWtQTXl6cndtdmhmTDFnVlJZZzE3dzVwOStDc1ZFOWRGZk0veVA1bDNvWGNrWGhkcjBjYXhza05WSGpxWkNTaXI4ZHZUb095MmlTNWgyN1lLRWtXU2lLRCtGYVh5WkNxZDBoY1JTbEwxWmZ4Z05SVjI2Q0FCbnJzZUt6UURvQXE5bGpaUHI0ZmQ1NFlwTjAzdHJZMFprUkRDcXZOdk4zY2tOYVg5UWc5Y01HTEQrTmppdzl2UjNWT1hhTy9mKytEbmlEeWhSaG13OS9JM2RTdmluZGthYzA1dlFCSjJFUm5GWUhoRDd1Q1NRVzQ3dEd0VU8rcUNneVhzQzcwUTlhTUJQZ3NScmFWR0tzeDZIRGtYUm1wM2REVlk5bzh3M21pTVdObHgyU0wzb1ppS0g4WmI0Zkl1ZzZjblVnQmI0THErWCtSNE5mMFpMR0o1cmpJcXVYY2VUQ0dkVk5PMmh0ckZkSUtnM2JIRXpRZHdWd2Z2SGJ5MkljY0doMnpRSW95endVTHdWc21kMy9pbFRDNkdPVlFlYkpEK01pZnJKRDVHbDEvMktrVGFVODlFMXhwRkJqZkdPUUQ5OW52eWl2OVhUOVBYODJZS0pxUFdpQ2hwL1EyRUtMd3pUdkNsdFpKSTR1L3F6aGx5cWZRUnNyUWJNVnluaWw5Tk1Id3lQSmdYMDkvYUtRbnRmMGVVM0FROTRmUEk5MG9uUHAyYWdyK2FKbG42VnV3MWFBSWN3VXAvZEVSdTBSalFlaDdPSUcwYmYzcC9XcEVUbDFFa2RDSDN4c2VjY3MrT1V6RjB0SCtsTjI4SjFrNU9Uc2tJd3QxT0xGUDVGdVJzZEdTWmZSQnRob1QybFc5UEV2RXA2WHdnUlcva0FZeEVoSVIvL0NvNDh2RXdrNHZjQ3NmZTZha1dJQ2UyQ1h2ZE5WWDhoU0IxVFZCK0ZvR2FRWllZZ0ZVZi9DenZ4a2tFbVpFbXQ2aUxhVHJsdDdaZmxzcjRvdnpwVFFwdldTK0dGRHNuanlCc0tna3IyOE9PanNUT1JpWkdReXdPTkJ4a3M4ZjVxcmNDOWl3M20raVgrR3lTUWpyc2lpdDQyeGlVNjh0MGdQR3BEWGRna1NDVVpMV0U1djZjcmk5QysrTXNzTGt0eHc0UWFtbnQ3NlppZUplUVdtYWt0N0dLTFJFUW11R1l4bmNOR01rTG8wVTVNWGFlMklpZ2ozRkxhUTBpZXZhT3ExTEZsZUpIUElGc1hYOG1ITDlFNjVCNmI1bWljN3dMdjdCZVRRY3M0QWNRdzh6bllkWmw2cTZRbk9VSmxlblRXRWhOOTFob2czY1JIREc5NzQvQVNDMTVHYUhJa0dyZjhKRGxDZGt2bElWTnRRUTFnajgwTjVQUkN2SUdLMkZsZlZKejV1V1JLTUg3S3N1WldkQ3BUWXVMSnhXMDcxd2R2ZmYwUk85eEovc0QzZmgrVW1wYzBGY1JPOURzSUdSUlRjeVkxSnp1RVNEcWlteDhjMzgzTEJvTkxDbUd0U3RtTjlRQm1NMGJHUDZ4dVp5am1oRktFYVA4YjJybmVmbmJyZDA0bGJ5enQ5amwzenhWRUU5Tk9LYk5kMVlaNG91S1dGT3U3OVg2ejR6bGgyY2RvWWFMYTVIRzg5ZGZabzNqTC9RWlpGVU9PeXcxUi9IdTZtUkdicGxFZDlBdXFETUZMKyttVlBYSU9mV1A3akhDdTN5cm5CbEFnMmt6ZXgyZVZIS1hyNE52OUFOZVc2M05hR0pUcDNoeisvd2FETmZJVFR0SUhzQ212SllXL0JVSHZTOS9lcVlrcWJSaGJ3MFZoeGo2ZVJQcmg3MGsxMTBDb25ISnRVdEFFLzF4MURTV0RWa1k4cWoyMjF5anZ3TEFEZDl6bUtFQm5nUy9EdWlMbnBSYS9yMzFQbm9kcFRRMVNkem9EUGdlTVdwNzBEOGpGNkZkRzJJZkNVcWZmUUJtMXMweC9WOXNPa2EwcFV1eWxjd0RZYW1MaFcwM0VSWXkveW9YL1FBNW9hR1A1djh3MXFMUXM4SXh2YWJ4UUt6bXpVcnRDNStUc3lwZXBwOGxYUVNZblpmeHU3NGJnRXpsZGZ3UFp5ZTlpWU1HZm1oTVhuTGIrNDUrVm4rVEhGUXp6YzZpM0dOZUsvR05OKytJTVBEeUF4Vkt6Zm5Iamw1TnBBaUdMVTgzdzdVWGw4VTl5NDg4eTZEN1RuMExQMEw1QTF2dmZEODNkNW9SaHZRaUxIVWZlVm0xZHFhb3g5TU5kME9OeTZ3dE1LWmZmS2FOZWZQMUNqZ2h4ekhRdmo2N2JtU0tjWTNZQ3dVOUlEUW5CUWlXYWlRenRvS2tYQVlodWF3VjYxd3ZrUWxSS3JKcnlsRWhvY0owdGMvMDMzRUFKZVRzaGMzdUJFQi9WYnFDTDUvTXZndUNIb0JmMlNDRDRZTlhHQ1YyWlQxVHpCNkNyZjlETTZEajAxQnY5ZkRBZWk0T25CS2hkUHNFTHRzVi9HZWZHNXliYUhmaU1JQk9iS2RoN1ZvQ092bERHbXhGUCtOaDhSOGFHdE1jT0dIVGVpTEE4M3pSNEp2MDlsT2FybEphMUhPQXJ1VmF1TzJpS25odnJIT2E1cWszYWNWRnlSNnVqa2MwNmZRVE42K2lUM09Wb1U4alIzWERjRFVzOVNwSEZ0cUprRHU5SmUzMXkxa1cyMXVUSUlXNUN1SG9vY2RnU1RJSWx0VUpSa3F3L21Ld0xMbXFORUZvaFBvOEFtL08xZ3c2b1dtMHJ4R2NOcmJ0RUtSTDNtMURIc3NuQytvMGx4LzVhNlVTSk1idDA5R2E4eXBIV2tqbXZDUjNqYWsxVERXbXlYSmE4YzhQL1czaFo0SlZQbFhTTUxtQkNhbmIycnp6WHJVUkovTDh3NnhJa1FWQ3FocVNqRUhKT1VQNm15am9MSzdPMzBldzZqcTlFbGM0VXRXdUc1cW9aNThUbUNrSHdCODFSNjhJZjU3U0VuQnYycTkwa2hQNjMwZEs1REg5NXdQdkJPb09jZEdBVzRxb0o5eUVLWHIrR1FCa2pxKzZZMW1wVlJxRUZZRkZOaHhhZFgwK3VvVWtUYWdRd080R1NHVm0zYWJ6MHU4YWJmRVhQaUVlNUc5VTR3V3dweit3L01LdXBrR0duMFlFWlk2SUtuWldjaFZuaGY3NFRNa0l1a0dHalhPVkRyNG5ubmNSUExGWWltY054N0JlM2NlWnRuUDNwOStkazFuaDZpc3k2L3FDWjRzUG8veXRwMHI4dU1jL0NYNXJFNFNjVEhpaTBVTGhvRFRaMzJabGExL3VOcWZWdHczQTNVSW9yc1d3czZPTHVBTmpBMVVqM1RyOTFYOGRSNlliR2xuWXUyNHV1N1FIaHlnNW91RC9mUTg3ZnhXTUVwdG8wV3MrVFFaZXRXZzJubHFYOWJUaEt4bEM5UUk4VjVGMUM1MC8wZGthWUkzWEVZM0VXLzRZc2Q4ZnNEbHNUT01VMXI5Q1hNelJ3MXU5WWVGdHFTeVY2QWs0UjNNcnB6SXBvNjN1Vm1pTmplTGREYVM1bGtKZ1dBZFJtNUdFQlVMT2p1TWRUYUFJS1JrMFUxb1JjWTRlZjZmbWFnNjZkVXJLM0dwRnFmUS82SkFjaE5ESm9LTjhpcGUzaUxEMkIyRlNicUJhT3gwSmJweGlDUWdUU0F1VU5TUmswZ05DU2toWlVacmNLbHNQcGpsbmljM1VkMFJVMzg2YUFYN2hwTmZZckZvbmF5aU5ld0lydmdNbWI2a09nWTQ3RVAyVUljZUs3cFdLTDBxQU91NFJycmltd1ZML2ZaYXdRZzVYaU1hZWtXaU02d01ML0Y4UWdsM01hNFJNVGVMcjVaZWV2UTZwaEQvbVNXM3o0ZFFsQnliazZocE1KRUE2YlRGaUV2akpsQStSaGlTTjA1TWN1OUZ5V05FejU3MCtERXBML2tvcVVORU0rTjBVcGlVZWJINTBuRzcxb2lJTGM0YmFIenVZZHBWWVpyUW5TMkNnajRlbXhBQ1I3VnVTMWJZTGgraWY4clY2eEFPcFpRK3R2K1RRVTNPbSsyd01pMnp5Y3I0SlZhS1N0bXpBWi9hdlhsbXdqM080cUdZS3RTeEpWSFAvQWF1cElOWDlBRzFPZWNUWkI5eVpaR2VUcjh3ZDZzcTN5OWgrQ2JKdGxnVnZteDh4L1pMNjNSNDFEUUM3RUE0blB0YjByZUdpLzRCUktCaVgxalU2QXVFVmxaMFB6YTI4L3pXWnlub01KNUhKL1pla0pRSlRyeUlYVVgzRWhibDR2aTJZMy9NaWtMcjZRVHBWV1NIZVBsWTVLMGxYR1BhZTFpWlI1OW8vWmVzWW56NjhqM2dVUjBydUxScjQwTVFKbzZkQzRuS3I1c0xIWmlXZXNLbWNDTThTTUZoWWY3YnhsNkxMcWcvS1hPcFRwV2pkUHlXZkNiODhNWW5IMEZSbXZ6ZXlFUE1Xa2dDR1hMdU9uMks2MHNZeWVlN24zczZOK2Q5YjhlaTQ0bFFIbHdOVHhMZ044WUg5RXErM0dqcUt5NDE0Zzd4WlN1dkdLdmh4c2NiZm5sWmVuaEUvOTNQN291ZUxXM2ZOQWY4dE9vcHlMTGt0SWxqYm5JRVl4cUtsOUJLWDc5UVAwOVZrbW1FM1Q0dzlzTE9uVnZFVklPSVBXNFFNQWZDSlh4bVlIVm93ODcrR0VKWGZEMk95VlpyOW40MGdlaURuZkdUcnhES2pZbDc2ZjhJUkJ3azYrMVphbmh1MzVzNEdweUR1Nk5hTUp4WHBrd3BUQVFqcWhreFJxNys5UlJFZk82V2JiUVQ0YWIzaWoxNkgwZWR1MCtvZHNOL2F5TUxiV2k0WHhhYnZvY0tIclB5RlhrNkw5QVBiT3Q2SXlhZTBWN0JSQ3Zzbll3Q3MrOVk1Y09TUjJDVVo2amNDZVZCcElta1JkT3RHQXVWcEdWQTRsQkY5bHhOWTJuVlU4NmVqWVhQMm9XS2ZTKzZFeURZMS9ya3RUZk9HcStsUXZNWm9HM2g4Wk84TWV3eTNpUGNZODZFMTZycUJMMWQ0Y1FEdlpOSWJsaEllaXlnSFQrSzY5YmhrMDdndlhxQnczMlJYQlVLbGN4cUFEQUl6Rm0ydW5iNE44YjBlZ1VoQXdHYkM3emgzRVpZVitEM3YxL3ZBRWxRWFV5QXF2QVVMSVhGQVJkK3RHdFRaU2o2N3FFczNDWVJWYTk1Wk9PZXp1SVNQOUN5NG5mUHVWU2IyNG5uRzZQeW1DV3F6TlJKMHFDUkE1aUtxTGNMcE92NHdWY2QzNXppMUdJbmJJUVVNQ2h3bHdxUXc5TS8yYXhYcEpOR2dNMngrZjIvdlFwbGlrdDF4U0lVeThxRDNjTGdrSjJGMzJ0S2xoYW9jbFNPVVpYNmNFY1hCb0dvRjU2cEprSmdFTDM2QjJrdWNzTVNXTmgvZHFoOHpLZ0lSZ2dLN1Z5ZUJ2NHNJZzNJczVvTG90UUJhT2dPeG9JaW9BYnJPN25qZzN6T2UxT3A1RnpIRjJOYWltVUc5c3NWeUpWTXBEMUpzTTVpSlEvRngvdGRONFlpMzNRWTVPTUo3WitVM2FvdDBOM2xUbGxJYmhXUlplT0o1Ym1leWlvN1FtbHlYZ0N0SmJ6STVwY2R6QSttZlgrTHFXZ1pJUjFYNFJyZUFFQ2tRd1V5MzhMZ29oVE1HUVJSSG5XRDI1dGFRcVRkK0htNURxK3FuN0RmU0UyZVpoMmxDRnk2Sk1iWFBybDFaUStDZloyYmxkbWF5NlRvNDVZK3ljOVFMamFmT1F3K2F3YSt1bkNETUd1Tk5JYUVFWTdjK0daWHVtNWQrTGFBWlJmQ1VaRWsxaUNnZ1FBQ21pWmwxcFE1RVJ0WmtKYlQ0UkpRbmd2V0kybDFudldiTUpNT1A2MFlpNHpad2FLcGE4UFZ0SmVpRjNWUXY4MSsrWTBSbjg2T25QK0VRVC9WM0xPZWxqUFAxR0hQS3VBamU2cS9DdGNieWhOTThEdy9zSmdXc2czNndhT3RVOWN4cnBTQ1N2QTg4aTd5TURpTXVzUTN2K3BLbllBZGdubFlrRUFYYjJUOTR2blE2TXNrKzd0T1ErdXhxS0c5WDN6MXc1bURTNWdNS2xGVnhTM0tBdGR4NEpoeERPT0JwZWV6QlI5ZG5QZG8vUDhGeTJ3STZNaDVKZ3JHdlN6NzY1WnVpTjM5dkNoc3B6ZDRMR0NzQkZvOEloM1IwUytQcUdrNXljWk5lRjVyeHl3OGZtMVNyUjZBSTZCeW02VmN1UjByRExLTHRBSGc4VWRndUxmOEJQWTZ3dFoxVVliblRFYk5qY3JOTmVqUDNMb29EN0wyL0lpMjFqM05hUG9PVm00LzZueVFMVUZ6MW5IL2VQVW9Sb0NVV09HUndvVVVrUFlUS0RuaEpDVkFFRFFYazZaSlluNFVObllLYllYbDVXV1REakRhOG1GWEY4T1A4OGRTa3ZrZkVMZk5qTGoybjg4djdGWndzc1M1bXl3WS9NMVZELzJoaU10ODZMYnRVRG5HYU1mQmtSdlArMFdNUjFwcUx6dzh3U2xoVFJjeC96WUJBdkRtRG8wTjRNVjhoU3RXTDBsVGRLQ001MlNtNUN0Sk5CQWliRWFXWGYvbE9zbXl5SVVSUUVZcHJDU0x5OFRreVRRYUlJaUtRVCtJc3l6MzN1cHJDcUJsaFBVaVhXVVFKT3ViSVhWdG9ZNHJPOUpOUmVHU2hVVzdlV2lML1JBcGU1b2VuQXF1RHJON21xV016dUo0TXRNb20wTnNlYVJLYTkwdUxYRlpTemtnUmpqYzAxTk1kTWtUL1JQNjc5cXBWL2JMSk5HcTJHU0t6RkRacndxbUg5SVhZMVNuMlVKY1pFTEd0QlRkMUhTaU4xSnNVQzBDMUxmYjdEQ0F3ODFyRlpIc1ozb1J6UGdSb2Rxa2x5WHBTbHpFTWExOTVKUnFuc0hxVVJSdlBKeWxQWUtmSzNQSSthWlRidXJHV2kxV1dHMWhtRmVTcTBuR3hxb1lLNXJwdWdOSjJrSkRSV1NEWERHNC9oL2pJeFdEcG4zZGxTTHQ5QVJxUE91bG1TU29XUVBjMk9yYkxZV1h5dER5V2J5dy9OclRKQ294WW4rcFVhWnUreGtxM3pGdW5rcHU2Q2ZsNHEvcWtNa1NVaG4wUXc5T2l4bi91Z2NxWlJYaEVvT05DTU1ZaTVGSVpUQnZ1NWwxNWcxdHJqcjZuOUVGQ2VWREh3SjNLYzVyRnNxVFRLN09yUEJmR3NDUFYxREhnMk5XRUpPaXMrVXRndG9iU21rbEplYXFiYmJwWnZ4SFBCNDZjR0JFRCtpZUZRZ2pNSTVacWFuUUliOTkwWjhzQmVBTkFESllWd0FsRjF0MHV1T2c1aWo5QVhML3JVYVpxaTgvek8xcEVFWmNkbCt5MGJrR2ZieThTTVZmcklsRmd1VjNCUU54akVmaEtDSTZKemtHdHJyWXRsVDNybmZ4cWJMUmNESXZPS3k0bUFHZW1JbU9vZm9xNDd4QXRuK0lYQnZ6T2owYnErekV3Z1lRZmxYSk9KQ3BTaG5UbkdVNjcxR0w2RzJRR2t0KzhrekpvSmhyNDJDS2Irb2daWDJ2SGRFelgyNkprRXhPenVxRXlnWHU3aWFxS3lidXgvS1d4YVp5bWErbUtWckVLdGlvZ1VrWUZ4K1MyWjFTY0VtWWNXaFNzL1RTdzU1QXI2Z3dUMmFNUmhJOU5yenRHblBzNWFIWDhvb2hvV0JZN1FpVStqUzViUjlsME9YUDhiU2hQQkp0YUt0bmxKNThZTlc0TzQwVGtEOFRrZkd0STdtZEt4QU9CL3BQemU4cFl3SlA4Sy9SbXk2dFZXRjIwd1pjWTcyRysrcUtJRkh6SEJZbDVxNFdoYk14ZEM5eHZpZjAxY0wrNlZBM1h6cWlzcnZTOWhSb2lhMVorbDlSYU1IcXdjSHVUNzllMkg1Zk43RDEvVENNcEFyS2xsMWZuejhtWVZoNXdSZ25SNVpQcnViRi9RaTZ1K0IvcThud1c5MzNjRlFEU2tJQWpqRDFUcEpqT3JMcGZya2FBWkZNeGNoNFRpMnp6dnEzeTFUT0dHS1d5ZjlUOGpXRzF6SVNZNFRkZFFJb0FZeHhyZ203Z1Jaek92TTBFUHAvaHljb2oyS0pYeXI2VjJJNWRDbHF2Rjl5UXU1WDdIellZK0tDKzhpdkVjV0diUTQ5U08rNU0wTTdtQlFHQ0FyeDZlVTh2dkVWZGptY1dwbXdYc2dzU1lTcWVQeUwyK2lQam5LY08wN3FiTG5WdGt2YWVFdVM2ZHZTUjgxZ0NjN2lSUWVieWVWT1Fybko3VktXd0FyUmFMYWp4bTgwUmJiUkEyVXR6NVBNL3Y1QXk5RXJWdjBTT01ReVFueWg1VU1JcDNxdW03S3RrQXdyTkpVMGZQVk4yK0w1U3VwSmVFTFpvdTVOR1NpcWlZaXVOb1EwTWVqeWRsSXE3ZlZrNjlRd296bEU3czlhTlRJVlZPU3pBL01LU0w0bXJXWUtVRmhoNkhUR2VxVUExNi9QU3ZWQVhCaVhxMlFQSEhmVjMrQWpGRERtRXpVUTlGSjNZQ2MzalAxQS9hd3VBclR2RDZ4eFpRWjhSZkZrUStQblJpTXgwb21mSHlCdHIvWGxPbytWOU5DZ2NWMURFRW9hYTZmTjhpYkw1bXRSZ1N3bW1SekZBYUp0VzJubkRSZ3dheHZpMlBodTZrME1TL3pkNUxUYW8vYlpGUytWU24zTmh6YWYvS3lVOWFTM2tIY2FOUSt6aUg0dlRGWmlqbmorYm9XVk1relRnZmlwOEFScnprVXBadlorMlRPWDhCT3l1eWthNjVUUkd2NURmQWRPZEhiUkQ0SjhkMitsMUVraEZ0MDBzYkllWmNCcHUwaDRyenVXZytZZCtJYXM1WTJ6cWhKeHVuaGhTdUFSNEFIei9HSGR3Qmw4Z1dSUnJScWMwRDJEV3hoOFYvdEV4VzUvR3EyODB1U0RKaWx5RURxQmMyZldNdXE5V0kxdmVBakpPTGNydU9tUTdTUmR3a2NtK0huSjgwb2Q3NWZDSHhubTE3N3l0ZS9kUkZrajFpY0U3NE9tcU44VTJzM09sSzZLemFZLzJSTWhlaERvOXdGdjF3Y1M2Yy9Za3hNN0laK1VKTFhIUVcyZEpkMENDYkNPY3c2c3dlR0tnT1MzVGk3Unp0Vk1uTG0vZEN4S3NIUFY5V0hhdTBqZktnK1hJcE9qVksrQlNEZ0t1eCs0UU1yN1RtVU5ZWWhRQzRsZTdKbzA4K3YwRXFqTld0RXRZbGVmZm5oQWMyWE9ZZ1dNeDVLWEs3WkJmTm9LRjB0WWNrTUcrV3gyVWZBakZXZng4Y2VEaG96TzJOYXNYUkloRHhTQnh3Q2pObE0zK1FlYVhlY2Yza1FMVjl4Nm4zdm9lQ0pmdTV2SDZJcW5TZlhGQ1B2OGlZZWZ4ZEdhOGttdm9DVm1aazJuUHJOUFFIVG5lT2RYNGc5eGhMbDFJcHZhL3cvSFA5QjF4N3VkUTEweXFxdmtwb0JYbXJsSHBOSG9pU01FQVlMempJeVRhVWlnODlWQ0JHYmJxdENocFpjRVhEaDlQNVMzaE1adXNHdVJhR0ErT1N5M0IwT0RnWXdPcWhob0RkZkRUcmh4WEQxU2hCajdUWS9EUzJHaVprc3BGSkM1aFl6QUxFYlpxdGFRUlpKNHZRbStuT1Zha0l0WXRvOWZDZ1BnT1k5di9TQ0w1eUhIL0gvOXZ3TDBDVjlNb0dUU2RSODRqRVVVL0poSEZKeXdkNFl5Y09XVEZ0RVNSRkcxcThnMGlCTlRKNmQ1ZjVrNmNMLzA5enMyYmRPNFV5cjR3ODBWRVVIUUdvWmhMMzdGTHpBck9GcXJZM0Q5M0lKTEVBbFZOTXNocU5VYngyaWRobGEyUDNHZGZleXJRRG0zYmxXTDJtVEordGV6UTRBRk1pVHJBdXlDb3BhMWVLTi9LN2FKcGpNTU1RY0I3RUdYMWdGZEZDNTZGUlE4bGVHN0pTeEh0VXZ0Z2VWT2xEbWxYYk9VaUE0TE1qTkluQkJkaGNQYUlCWnRtM21CWklYSDEyOE4vZ0lDQWdJQ0FnSUR4L2I3R1A3ZnJndjgzcmQvWjl0VCt6N2hUOXYwdFArMzdHYjluMnBYKzM2WXo5dmROK3owUi96ZW4vbSszMlUvNGZhcDc5dlpIZlo5a3YrLzdaYjluMGt1L2I5b1grejdVei9oOU1iZlYyOGZzOUJ5KzNxWTRmMit1MyszNnpUL1oxYmZzK3V2L00rd1kvYjlERi90K3NJdnAvWEVmN2ZvZzc2dXpPK24zSVgxZEhZRG5WdUNEUUpXV3Q0b1VCS1NJd3pqYy94MnJEdGs4eGVZNEFNVlBGbjRMRHpEWThLVG94dFluaWRLV2crQ1VSbmluZ2tod28xNG5vaVFkVUdUQkk1WDJaRTJhVkFTVzZuZjJ0dFFVSk1MQWNjdDlidmRBeThKd2MwRUgyTU84Wm9kQVZsOXJZUzJwa3pJb2ptVXJPL0lQVjZhOTM2Y0RKTGlFT0ViVXZJTm4wQytlUEl0dnJmVWZ6MlBjVzIxMkVuM29qTGdNV2ZLbVhjcWZtY29LalpZMHdURC9KeDA5SWdVYkhabUpmMy9ZMG1USjFBOFN6dnpxZHIxc3VFNlZaaTNVVnJxTmJOUUhpYjFTaytpc0NRYmNYcGV0eWpUVHo2b2lBYnRsMU83VExBaStTRzVTZm1JWDZOM0J6KzlQYVFSOVVnbzc1L3FrcmlheVlDbVpmNktoWmcvRU5XRmJVWVhtYUJVL1QxRWJiZDRLa3NYQ0ZXd1BmMHE1WjBJb291VENZaUhwcTVVOFVUVXVWejd6YkkzdDFiYWd3ZlFubUI3WEhXRU92N0kwM2pPTzJhWk56cVdhQ2U2emNIRWFLc2x2OTFyZ1JZYlp3YTJaWkt4YlFJUjE2eXkzWnYxWW55ZHhOdjlRZDhrSW93enkwZmlodUdqSEJ4bzVLbndGeEJEOEtQcUlkOUYwWlVJUlFoaXBSODNseUdZc0pTM3h2cDFNTThxSWpRb3IxRk1YdTQrekpsQ2ZsRmM0dEFmZEEzMjZkc0hIRy9IeTRvSmdhY2ttNlFaZzZ1dHFWVkprL1hUL09BWGpKNzJRRmZRNDZjVGNFRU1pOXdVYnBNTVhZK003dVdyZWdLV00vamR1K2xhT1JjZS9CRTNnZEVIUDRJcmhFQVFiK3ptcHduL2VudENjUXZCVWJnMnVvVWw1b0ZtOVpmbGFkMm1OME93NHVQcXIrN015TWljSXJzakpORHVBV0swVGF3QXk2RDVuekZFblhraUhteFZZSUU3NmhxV0ZCTHZKMHNKUTdCc2JQTkJJMjJySlBPNnkrdFo0Nks5MFEyMStZM0tvektWUTFNTm54RmdSN1Qwc0gxOTNEajBDa25TWEZhaHVmalp2WEl2aEhjY05UTHJwMTlQdTBiOGNFVk5xZU9ka3JwSlR2THJ1T1U2R0Fxbk42V0ZlYnI0dk40VGllVVp5K3YyYzhrSVhrTGYwYUd5ZXVyRktORVBINnFiZGNybldYeFAxNkJ0N2pnWURHSkdFSUZiSyt3aEtJcXltcGttWjlFQ0htWjJBNk81amdFcEpDQzkvYjJ6Mms5VUNXWktPYlpsZWYwN0ZrRUM0MlFMUEdkd2h4bnBRc0VDMk5BL2U1R1VtdTF0eGN4L1FwVndKNklpSEF1TUdzUkNxSDl4VllSa0RsakVnSXBqMTYrQk9aN3VEZmFvOUlTajUxUWN4UjZUZ0dveGR3QlVHV21Ld3lOTnVnd1Z1bVBVd3J3MUh1TzVjRkdVTnE5cTE0TWM2a3FZZ0ZBVG1reEpJNWZ3STZkOW1lMnc3cVNxblJTUXE3TTIxemEybEszZWs1MTZQeldPcWpUWlFkRHRSYmNnbkRGMUQzL0FPcEZ2bG1mbENNS1NMSUE0cGFkRzRFczdQRU13Nno4RGRLZm1KMU5tczhlZE9LMmVHWUp4NXY2dlNhdEd6UXZXL3Qrb3RjV0JKK0dLdjFnU2doVWNVUldhbERRNFhiNERvUmFjK3dFM0phNTdja3kyaW1TMzVpNXNZV0J1RFBiMTRSWDhUM01SNDBqKzRYbnVZZXNHUU1KNnhFSjRudmZ5S2xBeEFJeVRmWS82VE8zM3VFNm5pQnVyYnFzMzYyNHQzVEVxbHFIak5GWkZlUEM5U2hZdlk2ODBlc01VeDJpUk9UQlAwWnRzcGhVaWtMd2ExbFJYbUNrS0VWcVdXcm1sT1RpZWtUcllpb29iT09jOURwKzU5STJ3N2pOd3Rpc0tpcTVrTnRHM1JtSjNqbG5mQmEraTZ3bnNvandiYkRCa2xUUFpISU51M094aExyTDRkR3g5QU5OUWNkVjlSaWo0N3EvdGZtZVhCS01IYlp6S0hJUUF2YjlhaUJiTDl0ZG81NCtZY3BWNUF6V0l0cnNkTnhTN0k5eU1abmVzTENSTDZ5dVBNRHBsZjlVVGhqNkJXUXVWdERGTnFHMG1VNGp0YW5nSm9EeDBSNzMvRUNSbFlnbXk3T2U4elZwSlhNbU9yUmVCTy9MMFdoUUt4b3l4QWYxNDZGdFFDcWRNbWYwY21oSGJ5Uk9hb3NvVUpmY1lNekV2Y0xjY1hveUh4SWZORWt2aE9lcngvVWszL1pOaWoyWFNRM01rajArRlMrYkgxbThOR1FDSTZvejNJcTRsUHlNZ1dyL2t2bS9hRkNCWm80MFNOaTJOUEdMTE5OTHVwZEpnNVJvWDR2QU8wZlNmUm56UHEyMkNpbGtRekZIV0ZySXlXWkxuOEIxQ284ZGUyMTRSZUJqVTV3eXFoQ0w5WnFlSlFISzEwbkRzWlpYdWNXYkhleExzS3RqcE0wY1pBKzlOSmEycTAzRUo1cmJZTkxRRE1jMC9leW9MeFFWTnpWcndicVRwT3gvQi9yWUpwV1psa1lqSlpKQzJySzdHWVh3ekt1eGhTZ2JKdFlsbWtTajFPdGM0YldFaFZ5anVkekhFUTZkUXZ0NFhodFVpL1drREh6OXIybTBJUXo2L0lGc0hxRmlPYlFqaVltYkt1NHM1cEliUFgxc01Ta0FjN0cwUXJiZ2lNYWVMRDBqUG50UUh5L1pEUnorZDdFanRtU2tGTXBHR1pjRTBMNnNzSEdSdHF6SCtuSmNsQk1kcTVOa0wvZXJ0bUFZb3FCOUlMNXZFL0Y5SnczakRVamV2akVZK0puTlhXQjVtMXp2Ry9tdk9lOVpRekJublREM0sxTjJUNHNBaU5LMUdVQ3hOcFRLaHJDVXlUYTJzRStUSHhoem5FVjRCajhSZFhpM0pqUFJ1SS9jVDNVbmFVT1p1UHRNbEUzYlJpcWdzNHdmRG93eVgzb0tZeXNiUmowYzVCOGVHYXBCNlRFNFFZR2lUT2NweFZaM2MxVTZnNnhrMmxDV1NhcWplNmhCWEZJWXNqSWl4M1pHZTFwN2hvaFhNYTllNStObXBobmk3TGVWdFhESk45OE45a1FiekV3M0RpK2RTZHBLS0ovd1NUOFZPTldtVVdPVzcyWE5vN3UrTlZHUDgwbmQ3RVhtbHI0bzVjbmErQU1mOUs2Skd1bEpyTEdwWUVIODA3dDdiNC9PeDBMaHlGcjhJYTI3eFRxTGN4QXViL2dVMzBmbjlWV205bW1EbHR2ZXdlREMyanJ5OWZvOWtGZmJvbEdIZGxiQzJnaDV1dnVkRjNFWW4rTEI4dUIvdm50ODV6SGtHMFFsdW84Q0hLaThVeGxiS2dqWVlqellWWGJrbW55YXVVeEhRcGtxYTFjZ1pWQXRwbXl1a21FdUJjekg1TnpJSjRiMjZDdGVPRTFWaGQwUXJ0cUdFWEZpT3NFKzd5STR5bG1JdDY3ZG81SXphRGttYWtXZ3cwTGVCNVd6YXhkb0pmV3RLNGZocFF6c2FueUZqTjVzWmIrTVN2aUJRQ1Q5ck56TzJ3UGg5T1ljZkx0YWhGd25mYTFPREdvc1IvNUlCbHZlY0E5dzR1SHgzTk9vRG1sU1lDRkdHWjIzbEd1SHpONVl1eWNHcXczSEFrQUVpR0JsS2ZwNjJYeTNJSFZKM3BOWW5uMzMxTnhuNHdJMmZubmxaRWVpVFVCVGxxSFdINHh2R1pleW5QTEZEK0NXdmJYZXd5RHBBWHJja1Y4a0hKRkJCc3JuY242U2JZejZFMTNFOHFzcUtJditVVjVFU3hZVEpWNVN5SEhTT1RLNWkwWDNQMDVJMTFZM05iOVhVU214UXBEZjVIcUQyMkJaejIvS3JHVzR0NWROS0RSb2V3K2g1KzBSYnhvOElCSk5lMkJ6SW5sSXpyaVc0Y1pEU2s1Vkdiait2V0xYWEluclY4aE9ZNzZYd1lWUFh1MC92eThWUzJqVnUrNHNqUEJxMUlwMERuZlBIem56SlgvMVdJdndXLzh1bmdBQTcxcWRySFV0NmFHZThCL2ZwanZmZC9PWUtpaHRuR3UwbXRUQkpnZEZnWE9KdzJidWF0UDBXekc4bk5CeE9VUHdZMGRQd0J4VTQ2OHlBQjhKS3k1Y25zeW84YUFvTXk5T2duQjdLUmpJQWovblVuSWloMVVGY2NQTWVucG9pWXZsZU04WGI4RnJWMlBPRXVKN0lKYW1sVVN6eDhkcmR2Ri9QbjU3alNNQk52dW1kMFhaLzR4alE2K2RkYnVFa0xkVUdSc1N4SEFIQlpiVjQycUdvMTJvMFE0em9Cekc4WmhJTHpDVEg0ZnIzejBTeTJpM1V4N2NqUEx0THZJbU9xR01WVzIvTm5wQWpCODBRZnIrOFdkZUNmMVJKaEFvWTlpWVNVemNxNTdiczd2RFFxQm1PMC95aWNIRkYrV1M5T1FNQzJVR3N6aERPQ1ljU1lxcXNTd3NwbmhROE10SWNRaXkvRWdHY1BlcTdkandOTjV6dStzc2MyMU92NkZ2bVliQ3dIY1dielNwdk9GRU83SVRoY0NMV1dXSU1RT3orTVFPM3k3WndpempPSVpDK2FHcEkyRUZEWEIrV3BZOWRvdnFTajV6dHpiMEZheFVJS3puMkEzNGNNR2FsQ1NsMklubTNrK3RpQXowNmFVK0drZHJEWFQ5eVRKd1RGODh3QjY2citqVDUyaHZOVFVibFRRYWE0WDZXQmNDbmZNbEE2c2JrWFVQMXRRVDd0THg4SVNha1g3T09GME01YkR6aGtRajlHL3hNUCtQMVgzRG5iK1YwdWtZTy85R2pwdVhrbG4xNjRka2FuTWNSR2VPYyt1NTBnMUxDVVZlV1B1SldKY0FOSUowcVhua2s1NkNydmUwejFSVnhHUVhmQjJGemZFZVAxNjhSQ2JZb3l6c24xd0dVMWxTSE5GT3pIQytCcXByMFpFSDhKMmFDWjNudEdyUUh4U2dQemtIaExsTFZCWkx6NVFtY05PUSsyZ1hEYlBGQzZZWC8vUk5pUzIwQmg2RE5POW9aQk1sRWh5U09kM0ZSazRHS0hXUVZsZXhHVGFUeFpKRlJFVnR6S2g1c0xTWWVHclhLY3RETTIzYTlIZGZ0VnpKaS9UcEFLSm4zRXhTeStHMTFJV2RjejdDcXRzZXpUUng0c0Y0b2YzcFA1YW1DcGVxZVlrWlBONTJUdVhwTEgyZlhsTzBmeUkzd3RGSDlOOHhudTFkSy90b2F5RXljcGY5TTlxanYwU0lLSHA5ck5sOXVpdVJ0NzJBOFFkSnpqOGNJMHNlZDgybkJFVnE3UEpIQytHZ3plbmk3Q2EwalBwS3pVK1F2NTZSaGRrY3R3VUtzQzhNTWpQakpqMDVUQ21wak5YL3YwZUEwZkRkMU5KTjlIVk82RWFkbFVyTllKL3JibjFRV0JXRHFRUU5HUjhNNWsydFRzRjdRdnFKa1IyNU1JYkZJeWJObXpJc2V4Z0I4eFpVaGdVWWlHNlU1ZXlEVmZqOHo2NGFSN0lqMVYxaHBRVkNPZ0YrUVYzM0RUQ0hrUG9RTzI3d0I5UE1RcFpzdFp1Q2RTUkpRelZQaUpneHFrd3pzNnk2NVVpVWlMVDUyZ1lNRDJDTjlqWU1rTXo3VGhLU2diU3ZjVDZGay9NVVh0ZVYwMGFhSi9pbm5kb1VzNjlRTFNaTFBtb0h6ZEhRdHlDZUpwMHgybWgrK3p0QUpxbUFJeEQ0YU9oMFlaa0YvYUpvbnR4T1V3RWVYc1FLM3Q0ejFRdHFqdVdjUENSb0I3bG5VWjhNLzBMRDlSalp6ZEg2YWU1ZUpKUy9TdFVXQktwNWFQTmdMQk5Ta1pWWUJtckgvaUVOTkUwaDhXYzNhWXc2aWlkSkNnWWJrUzd4d0FhNkVuMDZUSitUd0E2R0RwN2VUU1lyUzFidjNtM1Uwanhyb1pRUENwMmFBeGRoNWN2SjY4SlhoZWRsMndvdDhpT0hRZFRWMkZRck1OeHJFTUgyWkU1engyZFo3RWZNbzBNeDNSUHpna1h4VjU3NS92cnA3dTIyV2hMVTkrRXhlN2FZOFNYRnY5T0pndHRENVgxSDJXQmhZSFNodFlSU2dEc3pvT01hdFhRTlUzbHZ1OTYzUXdpRGZMY1FIYmNuOHYzS2pKNDBBMkdoRm1RaHo1UnVhM1NraWJMWWlXOGJPbDl3dlNqbnVBU3UvZzZZV3RYQlBjM3BNK05rU2FJY0lwSU00RFhnNzdLSjJSSm4vcVgyVFpKakJoOWxwZXRNU0dLM3ZsNWFZS0NsQm1EaGMrUm8xK2Z1cjRWa2l5Zm1Gdno5elNVNTdlaVAzTkdwUit6VWRpZ1lNUVJpZEN3KzF2bkVvdEZPb2padGNGTzQ5VU9UbkFuQjN2c3Y1YnY5eU41QjVubHUxVkdjVUsvbmgwcDBSL3llcEpvOGFPQzh5YitEU29IL3p2aDMrdnN5V3h3VFJOMmtoR1JESGNVVW9odmgzUkpFY2VJTk53SUZmeEhPQkZpb3BjeXVONGlPUTBONjJpK2JncVc1dDMyQWZjaCsrNy96TGJVd1JldnpxVjNIUVNpdTNOL3lvV2hSMUFSODdtOEFtcHEyakFyRzN2U0g1SFFLcGduT1BjdmV2RTQrb3FMdUtUUkpvclB5cGx6bmRiSkFXd2Q3ZTI1dDNRVjRTZWZPa3kvc0E5US9CdHpaYkdZeDNydklYRVJGYXJvT2FhYVg5ZTk5Uk5xYmY4ZGtMbHpTQ0hsNFNJeDhFUEUxc2xoQ3JZL1Vsb216SndLNTRRU0hKdG9UN1VZRnlaZzlrTzV6MHQvcENCNHl4QWtFaTZrSlp6b0t3aHdQWmpEUTdxRVdEVzBrVVZZNHE3NGU4VHhmZDhpWHVETHlzVkVjLzBhcEFtTzE4RW1rd1NRWDdQaUJoQ2N3Zkg3eFhZMlljdmNoRzdjMHowaWh6V1FMcjIwaTdRVDI4c2o5dTEzS0JKNXF3NEtDRG5TTkU3WXhRRkJ3U1MrR095MTQ4eUVsM3JHSFpOaHo0bHA4RHdmUXJrUGJWNFZHWDcrOE5sZDBKUHI4OExrMEFsNC8xWEdPNmVBN2Q0MXBxNHVHR2VxbDc3NGRnS1FrcnZHVlJEWUZ4YmRMWGdqMlJsOUNFUlhKRXJkRC9qVlhmcklYa2xPZzNUSFh2WVZhYzIvMXViblQwTmNRMysvbGprMFlVVkVjWWpoTmRqWkNoSS9aMHpndFJxdzlYTXh1L0c3Y3czcmZLdzQ2bmFDaHNIZzNtWUVSayt4SG92NVVLWHRDY0RRNmdiaFdsaGRhMi9vT09IR0IzUDZBZ1IvaDBTUXpONy9MV2lMcndLUjBDUlk0alBZU2E3cktqVVVERlRvenN2YjZuTUMya3RjMmVLcVZieU9rOFd4MGxKVmw3NC80T1NSY2VHU0JyVnVReTNWSzlZMjJmWXh6WXViV0N2bHBBOXNYZmtkZlpkb3VPVDZDdUdKSUhVNldWZC9KSFJhdTVsWldpc0wxam94Y3NuempnNm1Md3BVR3NOZS9IWGZuZkZOUVJ1VTY5VHFPL1pLWmRLK2phdmU1N0FjZkpPRkRKNGhNV2tZbFE0d2FFTEpKZUthUGE4VVRZUkxVOFlqaWhXVlk2d2VhSmlaZS9oK2Y4VW1CQjlWTEhhL2gzdWhLTi8wV2V6Qko4aFVuVGxma3VRcVVoNGJyeEtvL0lBcHJvRVFkQlJNMWJDZGxtRUF2R0c2WlFVYmpjaWJyRlBQc1IySE9XemFWR2pJQURKVlBjNzd0NXJOTTRKNVc0ZXh0SkF2clZ6TWhBajMyaXpSUmIyOHRFSUxGaGZzVG1CT2FneHpsK2JEUHpPcXhtSGdvWFRHM2hGMXJuMFBJMnZKdjVHYTdBSzhIdVVJcS9GNzVOVlRyRWxHelN4V2RlaVJ6T3FQY1lyQ3BzNDA2Nm1iLzZnSHNpSWdnbzgwZVF6OTQrQXpXaXNMdkxwcHd5MThiNkVhMTFRc3FZZndIQUxscUl1UHpPeUF3NFprT0RoNE02RGtLRTA2VFB6Y0Rud1gvaDRpTElJcjdyVzJEK1RzekZ3QURvSUZmT1NYRzZDTUJmaEM1K3JmMC9Hdm9NZFphaHNiUFdVM3orWU14aHFGaVU1di8yQXZhRFVUU2JSTlIzY0c1eEd2RlA2RVRPSmVxQ3hxQ1piK2xZczNIY0ZnL0N5WHVERkdZSTVuSk1wWFltQ0lTMENSL1kyc2dsbHgybUtzaW9VOWYzeHhrb1BVV0J0a2l2L1EyUXd4cWRqaFZ1UWRXZ2N6RlFTOG01S1YzZHpTUGwyVkZWOGpIdTFESzJMYlA0TkZOUkZFaEJrUTdONEh1b1FNWDhiV1Q2RUlEc0tIazVtbjNmTGwxUTdoNEdmK3pidWljZTVOUDErNWw3bnl6MEU3T2MzTWJQK1JXazQ5b1dyR0FvZmZPd0ZING0wSGhQZUVpY2lZdE9BU1BYQ0tsakRWZkloaHNlMkwzV1NycC90WlFrTmtrNkcrMytPTlBTZ3BnNEF0WWU0bXloUHRaai9GSzJOZEVaZmVzRjhKT25iQ2xmdStoK1VKNml0YlkrSVliRnpVckZGQnZ5cnRUdmhhdG45S0V0ZWxxZVBqVG91cWJNTlVnNmNFWGlhbzB5SmVpa3p6V1FWdDQ2cmlwcmRHWitLNkNSbmx1ZVNVd2ZxcG1UMjYrN3VoREJsUkF2NEJjYndBWEMxdkx2RlBHUi90bysxamZCa2FzQ3FwT1lvZjZob0VLZlpETDlRcXdQZUhOMytPTXlVeUVKWXpKUi9HUTYwSGJCY2FQMGtrRGpucjFsNmxvbFNtTk0vd3ZqVWg3NEhoMjA1TFJnenk5UFc4WW5nZDgyZUhnWDEwZk96R05GTHBhc2xSUWtHZ25DdnBOK3l4UTROaTZwZGYrcU9OMVRPdWJJWkpwcS9ScEFyNVlUd25SZ1NCRVd2ZTgvb2N6YmYvMHNySkM1NnUwYmJKNHlZWDlFSk5MRnUxZzhnNXpieGowRTFlVk1FdHh6OTNZaEJhdFhFaHRjK3JsQ2RFNE45QVkyRTNSczhMSkVtd2pIN05xQVJNdTZ0YjF5UWNOWHpCKzVBY3NXTExYamdhNkQ0SmZGeFlLT1QrQStmNUNGL0dUTFNGYW1iQ0ltWlFuR2JING81WmZadEozaW9JZW85dThwTHYxTHg0N2M2L3JrY1d6RnVWU09NT2l1ZDdzLy9KSVYvS3dMR0p2VnpTTEJXY0pRemQydmtHd1AyQW1oVm1BOEwxaUgyYlVFQ0E2elFLTTVJVUYwcGFqbGgxT0o0cXVHRjErVDg4L1oyYW4yK0E3OFlmK20yc3ZoOW9lN0VtNHpUSitrRUl5aG8xaGE0M2liUGd2dVFuTG91SmpHcmk5Q3NpQi9iOG5UaGZSMDNOWjh6eHEyNVFacXVMTGFCVXVJWTAzZlZPRlhoc2FLOHNPNzRVSVd6bUlTdUJvamhyTHJ2Wjc5MHpldW8vaW1pYWM3V3RpNVNyZVkrYVc3WlU2ODc5OHRBM3dVcW5abEJGNlhndndDOFB3WnNHM1VGMC9lSy8zYXZTcC92MTNIRUY4RFNvOUt2YVZBR1ExbUZYVUlKRTRZbzBFUnZKc3VCemthdmxhY25yY1VNRlhKYjR3YVVNTDcvRHdnZWo1cWhDdHVtU1daM2dNRnFaRVFLMHVPdHI3bFhndlZGcThSa3oxMnNGU215Zm9RbmxuZWlicFNLYWNma1ZsWXpMMXRDdXdxdDI0RGNGdmlKV1MwQzlRQ2FzZmNvdkhyaXU1M2YvSGhSUG15bEsrZU1FREg0d0RKdURvakoveHpoUk00bXNBeHpxYjNLVjRlZEpGSnZqcU4zMGVWdGIrQmY5ODRaeXFrV3oyZERUMjdHL3o3Tk5Tc1ZKQVV1aUprWVF6VWFCck1HL0FoTDcyNnFZbUlJLzJlQWVwbTNZdTFQV3pDMVBoZWFjVkVXc3p0VFlMd1hlbkNPRXdCMDhTTGlRSnUxdnY4MkJNTUFFcVpyajBSVExpZ203OVpDZXJWWFhWbHFyMGNRdWJJTm9HY0lySWIwUncrY1kyRTE4TmNPd1pwNWRndTJ4VndiaE5sY3YxaU95TTJmWlFVVEVWQzBmampFNlZDZHlyeTRLVTV0eXVkNnF0cXp6bXViNHNXZUFKMGxoZkd2dUFvMW9yL3NuTjJRbmtveE4zTC9PYlpRU2s5cGhEWVAybFU2KzE1UUQrVkZSekpsbE00N1FHNVRTRGVmYVV1VnBsb2RrQ3hEa0toRy9HSWlCSmYxMUlZL1d5YkloMEptT2ZyVjBaZ3R6ZE92cHowNFBaYUk5Y1JJeEVWNlY4OGUveEhKbzlDdnNjd29aa3VmRHNSOWd3SHdhajg1MGE5SExxMmZvOWRxeHoyZmFtTjRkUVNqMXZuOGdzQWpjdFB2bzVjaE1CQUgzQmE4ejdlMFd3R2xkWmhOcEdlN0ZkVnJDWmtlUzNYMEJoUkNKYlJZaTF5UExEZnB1aWQ1dW5vUWE5blg4Z3R1VXRaVlcxS2NpWmd4Rkw4VWptKzFheHRhdHJaU2R3U01rWFN5ZDhSdjBZaGhxWjQ3US80V1JORVBZbUREV0FCT3JZRVc0SDJzQWR0VXlCdEVxcUdvVFgwODhOQVVlcC9BTUJGUVVpQ2oxV2tLVzluYmtCbURvUHhSQ00zN0lmNUY2djdwc0hBK0l6WVF0Ymp2c3U5NENYaU9XNVZiYmpoT0xDOVgrdWVET1Z3QUlkUlNXK0NTQmJPTjBSK0MvRENhZlIyS3I0QnlrK0VZQXRRZ3p5bW5Sd01sVnpOTGMzYytRUnd6eHlpT0JwZmZlT0tJNlBGNUp5Yi9NaGhXN1h0UGkvODhYMVZCdlFlaWc5ODdOdlRtR3BtRVJmTm5sbTJLUDdVdlUvczZKeTFET1hVZWNyT3d4WkNoTzlCcFBLZXVrbVVHV3BOSnQ1akZNcFprYlVqQU9FU3VxUG00c0F6SklYSmdrNmlLWTN4S1lmWlhkMjRSdjR2Z0lLMWZyaGl6NVl6eVc1bXdOMjlGMlR1a2psbFljZE1ka3BQM0lSbXlqTG9qbGtxMDJwK242TStCMzZvSStOa1JkTUZHZXBZUTVHYmQwWjFjUGc1c0E1Qzh4cjFmOEhkenJRZnhrWHNQZmlJVjVuL3Vmemx3WW9ENTVCSkhSTUYxUS9BcWxDWm4rZWlTRDdpTTMyanp0ZVVSQktRK3pFWnRseHhqbkJ6ajBydVRxVHZXUDJBaTN4TlpqU3ZUMkZzenhEa0Q2Uy9ETXUzSHE3WUF0R3FFS2ZUakR2T1E3VE0xL0xBY1lqYW5qNlMxRHoxQnF6dFlKRkM5SUt3eXJrTUo0VmZUK2o2aWk0WTdDZGY2b3diN25KTVd1TEdsS2U0MCtjbWs1bG1oSjJ3T1JtaDNqQjV1S3krSVhlUXpTUlVrMGZLakNjWGNCMUl5MFVhS3lPK29jU2ZlcytBTFRaNTdKY1NzVDB1bU1RVy9VVUZ2UDBvODFwYzFQTnN4YXVHWFBzN3Y2bHpSRVRNS3Q3WTVmZ1Ryb0NacUc4NFZjUHMwbzgyNDdQVisxOWlTU0FqaEIyWm56aWZOS0hyY0UxQ29VM092UzBJalJtM3lLcFZ3RnRPWVY2V0swMEo0cDMzT0JISmN4V3JiK3o4OGFMRWlVS252b3ZHQ3BXdFNGYzAzUkdZRE15bFlmUXpsK25QMk5pbDMxdHRvekFBOE5YOVBVdEN1ekdDMlhtYmNXam9pMjRDb2liMVJkMG1maFROWm9rWFlKajlnclJRV3lqRXhiejl1eG1nekc0RER3OXNZcVI0MjBaVGR1Y3Y3VGUrcnBEUU1HMFoxODRsTWg5Rjc5RkpKZy9DcTdscVJ4aXp3SXg3b2FEK0dpV3Uyai9aTGU5c3NaWW44eDJMT29MY2pOOGxNWUE5b0lCOVBKOTJ0OFFiZVA4ZWtJRzRvZWgxZXU2SDRBZmRXc0RQUmM2dnhqcEwwUTJIRDFVN1VZTGl6QTNTQmVJOGRMTU1BWXhwRDZQb1k3akFWU1hWdmFOYjRMRkN6eXhUNUczZFkzWjR6QkZpK0NYTm1hZ1BtM2g3d2pRRDZhMVpYSVM4WnpBM0ppU2pzcjdnYkxXd0dlRkNmenh3WGE1OFpXVDM1bmE0S29ldk5kLzZCem5uZzIreGxOc1FOVUJWc3hwdDRkVlJNcFAyaUYxSkM5QjhtMzBHbkZBN3RnbE5BZExUcEt3SjUzemt0N2hJNUxabTRKd0hiL2Z3QlZRZzJ4SXNDclJydW1QZE1mVVpPdkFZYWxIbDVUeW1vUHI0Q3lRTEYzRS8zcEhyOEdSbnNmY21Md3diVDZwTDJoVWlwVHBZZ05RcVhtVkZXV0NyclpjNFJpOHRkeGtnQVBOeVI2c25SRDcvL2VMMlRHNU8zUUoxK09RSzJaVGxrS0xUR05UWDR3b0EzaXpJQnhNcjNVYXNVVkxyZ1AxQVh0Ylc4NzdJZDhrb1pTYnB5UFgzbXJWQkU1YmZ2b29WUW1KVWZLSWdKUHpwVWM1VDFDbk0zelhLajV0UWNLMDF1bmNTK2xTRDB0S2hBYWRwQWlSV2xEMkdGUEhBVElwM1lHdHVybjk0cDNCNlV6VW5mZEVMMEprWFZTVG1vNGNuYTdMTFF3NFY0NFJCbWFlbkRaZ09GYTlGUkNhdktJRFg4U2tuS3hzSkNKMGd0OEFYQ29hK0RYRTVrZmltTkJLRlh0ZSthSHoxM1RBUEV2RzY1N1ZRU3hNUXhKSlhBOWN0enBIemZjWXBQdmRhZWozMUFOTGdZdmsyQVRRWnRLZTBWYjg2UWoxQUduRzViZDZMdFVFZzVOMTFZeVRLZTlDaGwrTmI5WU8zcFdiU2RsRXJLMG9HTUdtTkNQZFg5RXRpcmU2dmNVTEs3VFlnK1NIcHpNcFlaUUdzWUdmVzE4eUQyNDBBa3FLbWYvUGJVVzJxejJJbWN4b0UvNjFuTWVuQ3l5SmZmMGlxdDN3L3NjQ1lUWjM1RTE3TU1xOW42Y2dic0d2VFQ0S3cyNXFqMEwwUE5jd1hQcUQzZ2QvTThNNGpBY1RhUEgwWGpsK1JFQVZxcDdMd0M4ZzVrYTFnWHovQ29KeExveGpQd3ZVR1p6aTl2bmF2MFl0RUpxSjdHMktLanpyTkpDM25vNnJNRVpBT3hjcXJkRVpxSEJNZFNZSGJPM2s4U2h4NlFFRklVWks5ZGM3MllrQ3ZDdEV2TWlhMXlKY3NMMkVacTE2Q2ZvRnREcDBxbXVrZE9uOVI3OGN4R3k3SXhIb25CT1E0T0tuYXV2YXJmeGVpUHpXcm92M2VHRlFmenVTQkUyU3pQbFVlQTJ4dVpxelJ2VS9uNVZ5QUdweE9WcUtIQ2JNS3I0RXM3ZlI3OUhoeFVlYkxUVE1pa3FyL3FHVXZmZ3BjLzluMHpyTTI3bkpvcFhRS2xDa0hpZGY0QnZLaUdsaGVqTDc1SHJLZHhkOXRMU1VzdmczWHk4QlR1bEFkQnU1K0ZxRmlZR1o2UjQ2R3R6ekl4djRES0x2M242MFYyQVN1L3gvaE9tU29IeHFQTC9NOEg3Q1VUYk9jMjdsd1JjSng3eGsrNFFyQURtM0cxUCtjVThmOElOL1dhc05tNXlDNVVUQjBKSE1uMWovK0hkMldBRU9HT1lUL2RWY0tJVmJpdlRMZXFYak5sQjFpSnJheUNvWDZ0TU1oOWRzUWNRQVpieXdWWjArQ3lQalU3NVBFSXAvTnN0NUNyZWhKMTV6U0dwanZqczBvbGxNNytIcUIzTjIzYVJORWtIZXRsdFlkbmhibGNFY2FZUjY1cFd0bTM1b2U3dk1Qd0s4VWZDWk1FRmE4c1hjbUZ1MDU2d0x6eXE0WnhWVGRrSGc0Z3FBS0N3U1pERDZzSEhlcEc5SHAzeTV6aEl5UWxpZkJ0dEpOQnlzL2tmNzltdTRsb01pdzhpUVFCY1hxUDlQbXZTbTVRTDJ0UzMwK2pYNzhPc3Q0RW5XaEN3MWxZTXdBY0VhQW5wM1lMY3lJL09nejVWSDdwd1hFOTRDQlNNaElXYzY0a05GUHZXL2o3WktCUklBTlB2WERaQ2Fpd09hNStsUmprclZNd1ExbGJjYmZWQlFwbE9EQjMrSDB6SkZuVi8yZkJreEFNNWprYm1UV0pkY0JPYTFoanVrQUxzdVlmNDB0NzlheGlUN2hDUm5kaExzUHUrN0VPemt4MlU2WG4zWWFxUE8vclVkdFZMSld5STZOVmVsMU9XNWZVWVRXdTNBa3pVVkZTQ1RoV3N3NGwzaFpUalkyakdVamEreFRuQlNNYXVpWVJvZTJ5YU1LRTNmSTBNTUM4Q015NnFVNHBTUHloaTlhNnl1Z2MvTmdHV0FST0VnSWRJak9MRmhBVEU5aEJPTkVJTDRNVHNEczBsdHN1dVJ0cENzVGRHTVVRbkUyQzhpNWFrNWt4cERKK2llSEVVVGp2Z3RnaXJjMUMxY0x5VVNNMnlTaFYwQThIa2djZVFyUXQ2ZHNNSWs4eWJCRVhsL2hCYjZuZDY4N2dhWXVRNWtlTkpmS0ZkbjdXTUIwQy9aYUl3TkVVRi9IVlBoSTh6bWIybCtPQVJkM09oUWY2Uk5DdWJwY0ZZaWY4MzVpSnA3RUxhTE45ZktHTHdzTk9jWGFWaVQ4Q0V3NUlxbitnK1orc3E2b3BtK3RzNU1jcDE1Z1ZqaHRmTThVS1hLVEt6djQzTUN3clB6djBGWmNzYWdST2dNdlI2YzJqRTlyL2U3anYwVFkydnlTSWZvbUlzVG9wd1NoSTAvTE9jQUs5dk1ReUl2bkNEcGFIblQ2czFOaXcxd2QxM2FJdmszcEpncmpPUjNNSFBpcmJVSjM3akkwMGRhOWkzOERPWVV3S2VXUXZQT2hucHpwL1JUT2RSY1Qrbi84QkxRVU9FNUNKeDNSOVdWVTNpbkxkWCt3U0ozbEczTE5wanNkY0pvSzU3YWpIZE9DYTNnMGNTZGF3V3AwSXlzRHZoVWsrVlZnMW5pRVpqVUZyZjNtWnNtVTJvMmdsMFZORDVHKzNNR1QvV0NZYWZtdW5abldwTEMrcEtxUjRneWpQTTYrNm5XOVNkZ1JvODFEL2JjS0JRUVVGSnZZd1hWb1g4VUttNzRWMTdaNmlEc0M1dVRzZGIwQ1dlUHNkbXAvNEptSWVROHRvSjFrRE5rZEQ0QVNucUVrM2twaG5OZEhaWkJLOGo0WjU4RnRqem1UclZmZ1hxUjlUNnNHSWI3V3BTdkdYK0FJMmJPVzQxMGZRVWNjUnRUcFFOVzl2VU1WblhISXFZT2EwWnBvUlN6T25UM0VlRnZtaWxOanhjb1kva0FMT0F4aEk1QmV0NVRncXkyTHRCUGlVYnQwNHhweXU2MjZ6Q3VhbWlQQngzREljMjlocVZITFVYZHZ1U3hLeERNWjRyd3hIWEkrTlc5TlJabUNvelozenFUK2VBYXRvRGM0ak5WTzZWdmdSYzcwdi9xUVBpU20rZE1kY3VzNzVrSzBKOUt1V2d6aTBtODZycVZHekYvcXpXTU1iVDB3TGVqa1U2U2ZoZ3p3ZTB2d3JaMmNyTXMvRlR5NVFjNFlDbysvdTZmdVVpbWZmQ2d6NlRmNExrYTFJdGhkeDJ2REpxeGJja2dWQnhydnRvM0c3dWsvdVh2ZVVKM2V1OWZGa3ZMQ2xNUkJ5ZEU2Y2NhaVVPc01DaE9EUlFhQnAvMHVDVG80TXZwMFY2YnN2a0hKNGZhS1RUKzhXbXpOL29mMGk0RGlVUi9uSENPODdMR0Q2VmpmdmtIcUx2LzdiV2ZYZHo5OUY4djIrQXFiQkhPNnhwNWhwYXRkZFZVek1DTlRBMHl1VmZQcFNwL0E1S0FlRGh4N3NxdTBQNFVnVVF4bmxGQmlxZWZkS0drNkdza0RsMXk5STc0cUdKYmdMS3VWVGI5L2NyTlVqMDZCMWgzcTlqaDhyU21xcU02a3dTVW00RkEySHAzdldNbzg1RE5TS1NUbDcwY2wreUpsM2NVZS83bDh4Z1lLWmN1eG5paTRDUGc1Y3B3NTdKVFAvRXRCSW9mbG5QVGc5V3ZBdnQ1c2VzSkhYeFlxM0gyQ1owUWw0eFh3WEM1RDJVaXcrR3YyTGtveERCTzVrNlNTNHAvdTFtNWhFZzJpUEo4TFRxb21pVDlUYlpINTJ4eVhFdGpBRVVab3dlQ0VuMTBXZ094Z05nUDAvdUpaM3BWWUxOdWFkMU9jQW12NUtKQTQ0ajFIWDFHQkd0dEYzN1BTRnFPYzc4ejkvYVI1bFJrNUhTdSsvbTVUY3V1VXo2NXRFbkFaeTA0TW5DTjAwd3d3eVFoY2gxcURzYjdJYXpUa01xWDdNa0lPN3gzTGJiQldrOVQzS0JId05xR2pKSTJkU1kxNFhBcFhXMDlTc00zTmhSV3RybWVRR1dOMHJGL0RmWEdVQnpPM1ZQcmlLVEVlb3FUVU15U1doQUhPaEF0SkVWZ1dVR2NpckpPMnl4dkU4UXliMEx3a0ZhMEJlaFp1RHdlMTBUTVlPRHZuckdMcC9uUFhpZ3FzUmQ1QWNlUWVieXF3MXFkQ0ZlcFdSWDhkZDl2dXBueUs5dFo4UDRmQmlldjdyZXFHTG9MZFRwekk1YXJwb1ArREFWalZSWGhiN3NrbENjUStaZUdsQXlBWnFQMEU0bUNkc2NxYitUa1RZVm8wVWZscG9UbmZNNXhHNjgxWXVqMTdlNnBHUU1CTHN6SC9Ub1RLVlAyWEZqOUFvcy83aDZSeUpjcUFjd3FsYWFxSGwyc0VTc1JSQkcrOUhUUVRkRGEyR1lFWkVQSmwrY2NGaUxnQ25aclNMQ095WVBDaTAvRWovVWR0cVVVczZUcFhPYW1nbjBoWjB5SkQ4NHh3RzMrZFJkaEV5NmVuNTlvU0c0SGlKczV6UUd2ZTNRNVNsbEdlNU8zdUFZcmlxdExoV21VZFM5dkEzNU0wNlpTcG5WTGVMeG54aTVpMzY0ay95eTVGc2hLbkxhVGM2TEYwWXowL0xib2NDZWhWSkxOYndiSk1jVEtSOWxlVzF3SHVhdWFkMUp2NHUwOGlUKzFDckY1MGRQNjVVVEVuTWxzYmdvYnpobWEyVFRWazM5SStreWpTWWtzZ1JPM0JCUDdDY3JKTGhmYjEydDE4ZmdRYkY3cVFIYmZSMzZhVHBVQ2NQSkh5OTZ0S1lwM1J3YUhCNk5ma0pseG1tNUk5dEoxb2ZTZU9sK1VLMm9HYnFuODMzR1R6L1FPNVQyRjBmd0JjSjdvWWhoc0VwNHp2OHMzU1BacSsxZFo3OFc4RzRiL0JnRkdMSUVQZWo1UVNDOHBQdEQ2NnR1ZDZxRzVERVhYbmlaY25KRVlWR3B5elVQMDFUdFN4SzVNKzdmaFYzQUtrdmg1Lys0SEtVMXNkQlVsZzAyTVNTdDUvNU1HQ2lucElZMERXendpYVdYOU1wTUQyT3VTdXk3clZYZUh6ZWVmRzNsdWl3Y2ErcTJyM0xFTE43USt6NkVOcTZLMDg3K0hzZm10WEJUcVIyNFBwbEVGTmFPMVh3VW42c2Z3aDU4L2RKbCtnbmkwaVBRS1JwazU3dW8vR2tRUGhLb0hEU09wMm1KUjZOT0Jzc0tIcmxHT05aQVV1cTZzb0p4TjFhSlRsYk5uZkN4SGhlRGI0S29jL1ZXTGhocEJLY1gxMmpTaHlZU1VMMzRSTTVxa1pGWWpBdUViUmluc20rR1VoOFhHM0NRTks1OVVNTEJiNEVhY1Y4bXJSaG1Hb1RFTzV1RWxLVzFMd2tJQUczZ0VPSFdGdXgxTm1YenRxelhuYTZ6K3FtdlhqeWk3N21wL1c5YzQ4ZDYwRlR4UjFKTlRQNjNaSTNDV21zWGZVUTlJdlF3VE1jWkEzbG8rQkJTQkFVeUthYmdxc1Z6amNOd2MyZnNXa1ZlVHFNTVRrTUdDcUF5L1c5bmlkOCt3eTFBd0QxS0Z5UXFuSkJQUWd3OWMzRmZRUkR5d0ZLTjl6bDdaeFhSaDlxVmVuSTNwUkpoSWI0Nm8vVDRubkwxcnE3d2F5U3RDYm9tem8xa0krdFJ3dWZBUTQvdDFJamlidEhaeFlBWWYzcXMxVURBNXR6U3VxRUFWUGx3QlRGckE3WGFyNWhIT3N5dlRXVU0yczI2a2xZeUpEaFZZdTR5Qk15Tkk1ZXZpUVk1VC9EZkpWRkFwMUZHc3l3anhIN2x2VHd6T1o3R0hUbnRyajJXTlNjRnJIV0RFM2M5K3o3WXNTbWwxcEtMS1pBVitQL2ZxdnNGT2IrUEpGVU90S2VVcktUUEFqczdKakVmWlIvK3pxNDNoSi9WdDN4SlBMbHduZXMxUm1MdXpQdXJZSHE4dmlaMzVPbDNEZnIwd0xENTA1RWdZV0hPM3gxenlJcjJmaXU2REVHR3RXTHZoK01jMFhxbTNOQVo1QmczamdlSHNKSTUxV0pzZEhPRUhNc2piYzZneEZXVWdFZGthVzZjMy9Ldk1OdWlPOU4yV3FVYzE2aDRvQVMzRy9YRlJEVC9lY1NFUHplOEVMSkVsQVFvVWMzemtlMFZaMGVyZFExY1NpZFFUUnl5aUdBM1JoTkRyaVVkZXdRU3BiUGIvc2VYUHBnL0E3SGNxTnBSdkxOZ3Q1RnZBVzR5R0pHTzNjZHQxVEtUNXBOQ2VGUno5aWJPQm5yaHFCREpNOXVLTVpGL3hTNEljbUZnR2JTcE51NjhFYnRFeTFqRmJVWCtjS1hxdEcyZ0hkUjkzaEpOcDRXWC9OR0diWWJrM3lXYTFRNURsbC9DUnl5anUyLzJXdEJjTm5yaXQ0K3AwOGlQcFQyckRaSllJT056Q2Zva0dTOUp4bHpic1d3VFJlQVBvaDhjWGV5VGRyRmhNK3Mvb3c4RHJib0lzVjRyektvSW04SXFqbm1pMlcwT2tyaVIwVEhwRDZoZXlMcG9rMzB2ajJRNmdETHA3UVArVDlTM3FUbXVaUWxmRFlaWTU4VUdOaDBGZjVtdk9FbXFta2hpNThMdE1rOEFCWjhDdWsrMVdYNkhOWmJvU0FSellkUktWRWxOT2RIVk1OMm5vS0dTREp2cHJkTEtSMGp4aVBHb2tXV2V1SUtmNko1VXFTWTlVSXNzU0swUFJ0eXBTRjVYeXlsWVQzWGQxQ1FQbUFOOHRoaGNtODUxeFRLbnRVbUx0NXd2WGE5VzlEdzUxVHM0L0tKVSthM3RoUFBrTnkwTjcvUFJDYTcvM1kxWmZyMktBalRXR1pxVXdsQjcxRlZBajdURFlXMjIyL1pKTytKcGVYdWs3TmpKSDJXOUZLWnhrTHhoL3NES2Z0WFRuK2Q3a0tIeDlyMUtoOFk3UUwyMDB1MklKTmJxcnpycE5xS3BCR1NXcW1SZDUzcjlMYlo2ekU4N05oMGlsd2NQaUovbGprdzk2VzdybkNNWTNnWEJveWZJeEYyMzFpRGJweEp2VW13RjhMalR1WFlyVVQ0WjhzTzljaUQ5aXMxVzlaNTRBYkczd1BtWVZ4RkEzcWE3a0ZNQS9TVlB0dDFUMWc2d3BTRmQyRndxc3BoYWxWdlpteFJwVjJsZEpwdTdCTXR3TTMwUGoveU94Mkh3SUg3YVBLUW1jMEZNTWMzYkVxVW1DWWp4d0EzT2MwY0NaZTRlSmE2VDlaaTdtbmhRWUU2QlV5YnYvZnRoZW1makhjcUJUTzlrQUthVlVabjBINnFQNHRZQWhRQVhJRUVMaGwxUlF1ekFaMFVSY3FUMUgzYXl1d05TQkN5clBYWnBneU5EdW9RbVJ6MC9DV1ZTSkxaWnVnOTVZNGRzRC9ucHZuY1B0V05GaWxEZnhXQnJXSHFVbUlCcS9RUTFmUE1qRWpsSDV0UjJORUpmMXdnbXpBZ042Smp3eU5aelJuT0p2aU0reHUyL3p3YVpIdThUazRCaXJmOU9WclRCanVyU1d5WlVJY1J1TTlEM0FuTGdBaFArMXhVRjJMSWhmazZGUkZzd0xOQk5Mb05VQ3ZNbUgrWkF0cVF4Y3VoOWZQQ2pDMXAwdW9HaUdrT0N6d2Z2SE5OaHRsSkh6VGMxZ2hhZkpIVklBMlo3U1YrQVhrUWtEbkpBajBXbXdmRzVINDFNTS9WN2RqTUEydWl6TXlYaUJ4cFFBazhVTFMrbm9CQWdxVkIwVmNRQ3pDWG1FRk1kYWpST0tiOFNHV0p5QnErdWc3RGJKekpZUXRwc1E3Ujd3aEtxaVdBM09YTDJXYTBGWFBHV2Fxa1VTbGRHTlJsMmZiTGdFVlU0V2xRaHRxcG4wVzZ2RDgrdVVkdEpHMCt6cVpYWFpDRnZpcHE3SGlpRHNNYVJzVE1XTTZNc0ZMUnpFSjFqbjZKTzhWWjhYQzJFcFpOSDRDVjYwclRLZlRMOTNuQzYxNFRKZUVBNnNjSkw1TzBZODBweVVHeitWYm50N1VnTVV1WnZJcDRWWnp0RWFIZ3RBYUNyQ0JWalFJY2F5ZzlaZkd4SmpaWUtYekJQTkczVmlWZDlEcjRRZ1NwTnVQOEkzc3J1YWdKT2hkS3RxcXhtSEpiZEZYRCtlUmNwanZCdlZ0UHB2ZEVjOXV3Q0E5V2FWRSt6bHNzR1RiYkYwK2FSZnY5WlA3WW14RGQ1SGZyVHlnbVg3M1FUTnpNeDBIWE1XVGtyZHQ1RDhlUjBhUytMUkdMMEtvQ1FFcit3QlNTUlNYczB1cytYYWtCK0pydkhmVmVORWpxaEw4U2FKMVZXYTM0MWQrUmtYc2FNVE82ZXd3QjVYNVdGR2lRTWlSZjBkVHd1eWpYclhCZ2xKL1FxczBiUjhWMzA0MWN0YWtaeGMyOXNrd2ZUaGhqem1QbjRDaEgyNHRqVjZBbEkyaGcxVm1WMitSUFg4cGVXZkRaNCtRYUlIc1ppT2hKZnNNbHp6QkMwY3lmRXBFa0N3U2xHQVFXUXQrSldLYXJKVmhtcHZnS1Btd0ZTK0hpNzBtRmQ5eFVXSUtrZFoyM1VCWkJIZTVsd1N1Q0NodFI4aVgyV2FHTUFWYitKMERmcUQwZC8zdjROSXg0THQ5N3NvTGZNbkpLTDlqbExCRzlWczBGTEJvREpSU0tWN3hDK1NSOWVvQ29HK0libmVuQ2ZaUllrdlI4bmZTMW5aQmI2VnBUelZVb2pQcmljZUpiUHQ0NHB1RXdTdUsyL3J0MTV0MGZZdG9zVFIvZU5LNTI2RFNKVjFyaFRHR3ZuWDFqTUM5WVNHNi9DblI2elRRK3lyd25sTTBjeE9RRnRUR3VraFRDR2R6MGJTclNCNXFJR2V4Y3hRRGhrMytRcytLQWN5THBIWEMwSGlBSnZSZGlTSjJMd2I3dDdlMDhzREdyQ2I1MFN2UVNQNTBtK3VFeEJVNjF4L3A0bVFFc2JHK0ZDZEt5REtrOU8vYWRLMnZIVXI5WWdxajRuMHh5b1RzNHBJdUtVOHhMYm1uUjQ0M2Vwa212a1ZwaWV2SGdDaWtHSUVHZHU4WGd6NXBmN1F3UzZIeENzRFlwM2xPeGsxYUZUa3J0RzJkUmI2dHhHUVpqUlljT2YrYlQ5VG5ET1drcldhVmM3UnJLMDNHaW5UQlJENGN6ZjRuQkFjbHRNa0JHV2NzanZPa3I3SEZ5VHI1QVhhbVpJMTRtVG5naVhJakdHbTN5T3oyZHZ3aTRrVHg0a2E3Rmw4VVV2SThrU1RNNUJEeDJIMHBrM05BdEMvZk9mdTVia1M1Mkh3ZGlBR0VKM1ErOTV6Q1VLNDJGeTUreXloRDUrek4ySDQ3UDhkdnloU2VHREx5cTI4QWxPbHI2b3lrQmZwTWZVejg3L3JlcGtaV0hWY0xoaTNYQmRSOXJmUlA3WWVRVlBtU2FiMUFyWHhoL2VTbVR5YVZEZkQ1U01VdGlPeVpVTDRCVCtEbmorT2pNL3oxTmFUUjM0eXBNZWo1VUMwaEZ3L2xlTEZFK3pBdVBocGRNQkhSQzFOa1M4V21rbnBYSmptUkV1Q0JtNlppWDdqOVhwSURlZUk0OCtRNHhuZmRqalQ2K2k5Sm1IVUtwOHgxN1JMS0Q4dDAwcHl3d1BRZUplbXYzZk5PZ2Q4K3NxNVVsdFJvMHJxYzliVlVKeUNyUUNOSnJUUXQ0RG9RVXEzbnQ4WXhkaGZyK3VXcDJRbkUxM1lFdnUxa2FVREcwN096WERjVzBkcno2OEt6dVNTeU1sN0ZKWUpCSXY5eW1XS2lpWFNsZFNSazgvZUczaGoyVEVSUkRnSS9aMUxLc0RxSS8yYVR6Sk9HWUFTamxqZUpENXdDeFB4N2IrVmx5dUp0cFN3c2l6eTROZlp5bFZIRkk3R3oyUVZFUC9YVE9TNGx0S1RFZzZuMVMwZUJZY3NWSmVUUEk5WEpMcEZSeFhKNmhhNGNWa1hLOG5taW9WM054V2FJc0wySVJsVTdTRGE5MFhwOEhnam1FZmZHOFo4RFEveW8wbm1JT25kcXRCMk5ZVjBTTXZPSWRyZHdWWjBxS2QyaEZqNHhVWGpYTUduN2RpYVhHVlJ3VUVtRU96dXN4eHZISUtBZzFWRFliRGV5dStlbnBRdGgrU0ZmRXFPVDlnM0Y2Z0NUUHVTUlF3bi9lYmlYc09sK2V4aDF6VllvYlVrMGxBWGxUZDJOK0RTSFdzemxuakNlRE5IV3hqNXY4c250WE1SY3Y1SmxpbHBuUU1ib3hyUnRnWFhlc2tTSTlrN3pvTW9VU0R6TnFrTmMzTUlHU0k2WXRuV0RFby9LeFZlSjJrN05lTmh3K203eGkwTlJxbDdVWWo1TUg3RFgwbkRuTzVtSzlwNGlFRERoamNhRzNqWmxEOWR5VkhTRXA3SnpUNWorV0hONVZwSGovRzBEa0RLUlF0Nm92YVlXTGtxVzhIVzM1T0QwSm5PaUR5Vm9lbWZZcGtBVEdDTEZPWnRKWXhJUWJ5Nnk3OEZ1aFhGQ21nY1BaczZCdUZOOEUrVXVLUnhqK0c0bGlINkhodkhtREEvWXNOeEZsdktvUDhqazdvdFpUbStjckoxK2VsdVpsU3RuQW8vTUN0dE5HMkNZVkZjMHdEQW1MMVVvcjNiQ3dhS2VLbDVFU2R2V1NYTlJUKy9VOUFUeGdEWlBxMjJPQ1Q2emtDYXpyeVAyZnBLcmlaWDMvZHJiNFY3THlQYzRWODVVeFZ4MHhlMTRURDRlMkZkS1h6TW9PeGZwK3FGcituanZqWnVvQ2d6K3BoSXByYlovWXlsNHg5WkwzR0JkMFZmYThhUzg5NU04Z2NOU2hKNTN1N3RMdGRqejBGNGFFUjRFTnBKd1plSEtGYktnZThUSmgzTit2RmE3eWdaaTVCbll6MnIrV0IyUmV6SmUvTzcyRi95TWJmL3RuenN0ZExGNHdtOWFxcm5iNGZkZU1vNUhEVDNtZEhmVnUzK3h5aTZtUmIwUHVYUzUyQU1hSndSZTM5RFNZK3JTYklvcmQ1Nzcrdk5xMEE5VHZJUVMxME1VbjF3ZHl3bVU1QVZpZmIwR0pHdC9ZaUNPZEN4QjBZSE8zUzlHMnkvU2kxcjVha2JmdjRWbjl0b1FvcFh6Z213ZWZMeFE1QXFVazZpRFNtckhsMWN0MWFMQVRPdXIwVXcwQ2pxL1ppRDlhdHVXNFd6Qk9DWGpONHp4eS8zSFNsMzZaTmpla202QzFXWGVtNHlWenI3M3QwM3NIakpTRW9xMG1sUU5VcWRiZ1VXTWtUb1FwVG5HSzh0MFdEK0NCSGc5UmFDc25UcGxsdC9wWHRlMDZYQ3h6M0VZa3UyWlcwUlRpa0t4U1RWLzI4eFY3S1VRYkhEVnR5WWllT0wvcktLTUNkYkRxTStSUHo5V29palEzMUI2aE1vTlJ3bG8reFBudnpRWGdLUmc5bUQ0R2tpamc2RVRIT2Y0c1hDVGk2ZFhsb0phTXJIaW5ld0d4VXM4eEJaT1lMRCtETXBCNXB4MXpyV1huQ3VTS2lPNHhzdS9YSkx3SWtoUEhYcFUraDYwdjFWUnFNZFdtcFNGcEVtb2xWd0xaRHlhV1RUVFVwd0ZBS0Y0WGd6bHpiM3RmOVl3RGRhSjFTZzk0VEZkcVRKem5nT2VjY1JadFlyUkpxai9Yc3ZMN3J6Q0c5aG5nVmJWK2o0MHEvdWZVRVZ6VGtGVjduT1RLWmU0elh5QVE0bFprS0dmTDNTeTR3QTNNTGt2L0xqVkRXWmE5NkxRSmRhZ1FVNFQ0NGtqMnEzaGdsa0RubDBKejRjR3dNejlkQ1hPRzk1cVRNM1VjZDl6ZnVUS094M3VDMlJZYldDcTdQcjMzZ1ltQVNDSW0yelhJQ09UQ1o3Zk1PTGU4Q293aGRIMWRIYnNIMGpveCtJZXpxTnQ4YWUrdGVkdFBqdDR3bWhUSHl0TE12NXc3bW4reWNacmVFS3dGRExyZS9JMWZ4R2UrUm4wbUNiMnFsVnBaaWpyS0dncmswaEd3RzRGOGRvWmFjbWZMSUdlYXlnQnhtQzJpMmlHRDYvNDE0STF0cjB2Nmc4ZWlCa3U2VDhWUHBkTWhEbkhmUWZYU0t3R2dieDlIOC95S3dWUGdpNDJkanpiRi9lUmcwR2NURTlJTWttQUR3OUJFWTN5RXc0cHVGMWJYRFhsVjRQVHpjdndadlBMSTRiRGUrR3B5WXQvcFZ6TDc4S1BxM2g0MVRRSS9kamxid1hEVzF5R0pNeWcxZ1RlMTgrd0Zxei8ydG0xWXY1Tk1TZ2pwTmR1UlozN1J2NFhvSDM5WnBHTk5ML1lTMmp1eE9vdCtDMDF3cTNxb04wWUdXb25CQ3dndmVDOHNjOVhqNkUycnB2WGxORVFoT3hKVU5HY1lTR2s0RnZSZW5wQmZ6UlRBcDZlTDExZHFDenprTFZoQkRxM1pjZXBubGZvMFRWbDJERXhOZFRMTDhrSm5NNHhtUldyQWlWekYyUnFnQmhhMCtwaVViUGhsd2c4VzcwaVhZd3BzbVFlRzEwNVhpam1EaDllVW44b3RzdmpJbDJNQVpUZ2x0bDlDbzRLVnIvM01CYXd4MzI4UU9peGwreE9EWEFiZG9NUVAvY0cxNzBGNWFINjRqZUpSVFQ3Sm9qaEhjV0IrTFRqQVN4aTB6S0pqR1hXNCsvZmVsaEIwZ2syQVVBQ3pjUlpXMVg4b2ZqRURTVEhtUWRWdEx3MDRZMFVSZDdCSUp1WU5ENERCRjAwZ3BWcWJ2cnFwSzNsM3MrWHVOTUNtV2RJMFVJN0RTb3VOT2l2TmF4Y3ppZWExM1RmRDMxZFN3VVpHQU9lZkdTbnFlTE1lSjRORmFkb2dtcDgvZXpBMVRLeHU1ZGF3OVUwd2pHb1o2RTJRdW5ybWsyQXJzNHc0QkExR0FnMUtlSi9qZUh1dWZ2ODdWMUpmQ2JkTzlVQ21MU0hlNjVQMmNuMDN6dE1ycVZ1MG5GbGxiWm50VjB0bXhyaDlQN1NYckRiN1YxYlYxdXR3Tm1rSEFKa2p5Y1lQc0I0K1FJNjB6dkZLMXFSNjZubXVZMHNpYWtXK040ZjRDY3dHNEVaYTZyaG9aS1VvQ29uTmJFWWQ3eEN4cTZ2WTVqNkcxZ2lyZ3J4Y1Zua0U2Z3NRQ2pTemczSG15UHNhUG1KalQ5bnl2dExvMExLcEJwSU5LNU1NbUhMclpwN1M3cDhjK3JiTi9jaEViNTJ4eUpjckI2bFp4Q1BvR0FhSjQxa0t2UVphZE1BZWNtbk9HeTJWNHBodys1NnA2WkdUMVp0NFk1bUF1NXNZUk4vcTZWZ2JqV0MzdUwrRE13ek84dXlTMkJUdVZTNFY1eG9Fb3NLNmVBV0xwcXRCU0Q5OGd5Q3ZjUVJnbU12ZG1vWG5LeXlNYmRESHJVdGFwVjV4M1R4K2t1K3A1bXhXT0dyTWExeG5KVzFhMzlMM0ZBQ0hZRzY3WUtiRkx1bW5GS1NjWEdDVzNYMmNRbVk2L3ZBTEd2YUdlcGR4TG5weDBwZmVVTG42R2NiMnJaMHpkUHdnY1dnb0JXNnNJUEFZSHluTmVPdmIxYUNzTVVLc3A3aVpha3lIQ2ZOenNKWkhHM3VFVmNYWWcvRGVWTUhWRnpxU05QZmx6UjRCU3NoeXdBaGhGaGtxajBhYjhuN1htc1Y0UThkWVBaa015UFZRNFlaQmNMekdBTmtXakFKQ1BmaWZnZHdGUEVvN0RGY3l1b29Fb3QvSmN0Qytla2VGbTVFOVRjZC9oNnY3d0JtMGRaemlkZ2V0MFZvNlZRaGxUSUN2NXJOR3RJcnE2bEd2b0drRlRTU0IrMVBOSlQ3WGFjRFVJM29qK2dOQW40TlRmc2h2VEdQVWpBenorMlpDeDJjSlIzeGxkNGk4eHU3eTlVK3pidkV1RmRVdlV3RHBtamRMWmNoLzdwMFg0amNzVzRpZVZTVU5DeHJscjliZmorNnpVUG1QQWZOYkJTdHRIQ3VGUVhTUTF3dHVoRFhZaEswdEcrbXpmSUlCZXZkcTFLaUlKdU5HdlVtVEJ0ODlXUzJ4TWE1akRodWZwc21RdlJ3dG1WQmNPTU5WRnlpVnJMQWhnRXRSLzJmb29kUGl4bVFzczJUaDlVdEZ3aWFQK3ZnRzBGSnlZY3FwMVplL2N0K1R1U2xDQVd5TFc2dHl3QjM0UEgvcExsbmZ0L3oxV2pxTHluUTdkb3V2TE1PZnlEYW1KQkh0SVpzNFBHU2ZtZURFbDVyR01laU54ZmFqL2IvdzEvQklma3JJajVoZFZFNFhGdG5pSjJPVlBWRjYyQzNLNFdWZUlueUt1blladjMveWtxWjh0V0RQM0svTmhOWnFRcUV2STduVHJVVTEvSmFBMWV4c2RLZDdtODFCY2pIeW8zU3F5T0NUVWZqNXBTQnhLV0h2OHR5aEJmeUpNV1Z3aXd4T0Fjeitmc3FqN1dpS2hnMjBXV05YWFFKSzlsTGtlN0tYcHE4RkNoaFc1SS9PaGVZeXphNFZrTEpTRjA1NmdjTlFCVlY2dmNaMUFNaDdtME5FOCtlNVBON3VDTXVreituTFdhcWgzbkc3U2x4Q0QvZFpmY0tZaGRMc3g4YXZCb1NPOUcyUEtKVVkzalBXVnp5Y0tEVWluRWpmMHNtcDRybmd1MUpkSDBPaGE4QVF1a2RvU0hjVFBiSm5tMFZJVkd3eEkvOE0wL0RmQnkvSXdvTUwxY01SSWlsUFBNR2lmQUNuQ3hTNTZQNHlYcWREYThZRmZMVVloM2cwemF3bXcyY1d4WjhlZmRkVE5FRXp2MGM4Vm9oNEo1RnJaT1Jkdml0NVZYZ0J6SzRJa1BpRlg2Vjl1bnBPMFB3aEFsL2JCbENkL01QN3hwRWVnODhzOC9NOW4zWmhYeFZxaUgvbGNEdEFaSnR1S0V2enlaMmhwVi9mT2dkN3cyRHJzSUFnVDJvcGR2ZDB1dVk3ekpLYkUyMTA0YWNrQ1RvL3dHbW1KWkdPOGR2R0prNWN1amplRW10aExxT1hwdWNhRE9mVXZBWEcwME95VVkrRnZWVnNVa0w4WTdvUmtHK2ticXFhOFRkQkZDa3BkSTl5WTN3WkkvQ3ludER1MmpYUU5sQTJDWmlnckFhMVpQWTFqZTlhYUtMZ2lMdGZjdXZrYTU5bVI2SHlMOGhOY1BhYlJhVENYR3BQZDhDL0NjYzlVRDJCWDhjMHFyRGRDZEQ4N1JCWFRrRmplejRRM25BYVBUQjZXRkF2dDNIc20vRjBJU2krV0pZQU5pSXFvbEd4OW1qRjlKQWpxVm9rMGh5WVUvcGZPblkxU1FMdy9WUGlWS2w3cWlTUk9LWXZESEllbDdvMndRczlqTys2N2cyc2FyYkxUUDlUa2NzRUFsSGxYNU13TGZES2MyY1huZjVjY0c3TTZ6SWJHZk1uTWxPWjJ4Uk5uYy91WnpXYlFHb1B4KzdwQ0oySG5oV09mYTVKaDhvMG9HTFdyM3lXK2tFTnVEM1NiTDhla0hnM3lWY0wveFQ3K3RLcmh6NHk1VnJTQzdOdWNVL3lWeUk3VzhHQTBUdEYxYi9WUnNtYjc2Q1dlTFBNYi9WQi9kYTFsaVJTOThnbFlIdFdwZ1B0QldsdWZNTzhjd2d4ZW1YaThWZFZzeHJTNnM1VUU4T293SVpnSUNwYW9HT01KamttVllrMEFsMFRSd2RrVmxsc2JkVnFkR1RQd2xOUXNZczdSaHhQOHVOdHlzZUQwTE1pUU9xU01IZUhnN2g0VlphaWpGalJXQUJNTVZPYldGNTRGa1JWMlBrUzczNUhXTlJaZmZEclNXNEVzWmpMN3FBVHhFanpTOG5XR1YzUGhJYjdxYkpHT3I1WE56d1Z3QU5uK3Fhc1krdHloNWh5WUpBZ1VQeFA1ajc3c2lXeUFRRzNSd3NuN0Zvd0FpUHR5VVBkWENUUVR6WHIydHU2REtnYkJvWURzNXlObXFrMW1qbnY5ZFFXOTVnNmp0ZHg0QmZjTEx0YTFsTDlJOVAxa1pTNXJvbTgvbExTSFBxTVp2aWJCRi9SWWsxa3dFYlE1WkZBcUxFN0lwVmx2TDVYR3FkL216ZnFOc01DMlZqWmh6cjIwYWNIZ1hYd2ZJN3ZYa1B4YzJvbjhSTXRmSkVCenN0WVNpclFxOVlrK253T0VjK0JPTFJVOTJlYzJCdDNyZUxzNUNlenJiOXFpbHdjRk5YOE1CRit0d1V4b2hVQkxrY3VIWnlLWnFqNkJMeVJzeXBBVXBTQ1VYRXAvRVE2WlVJdDNBOXRld00xbTc1K0NmSUZ6WVI5VDZDMnc3a3BzcVg2ZWJuNThtS2UvVDF4SCtQcE5ERklnRXZzS05UcDJLL21BZFMxTmk0OExYWXd5cC9yREZhMUFtYTBrOTFIRkk4Q3JWVjVOVURzQW1DR3k1cGpoTWpZNGRZdGdwa3VMMXhHRjVjZnNSR1BaM2psakkyTUNrTHF4QU9XWjU5dm8yRmhJK0RINXRDT1I0NmhJeGF3ZmdIamROTXJTMml6aTN2Y2x1WEN5dmR0QUpHVFc1SGVKc09BUHNrVk5yd3UxeHgyM0VnOHJFeHFqZWRzUHR1VzdTRFk4bG1sT3RBQVdpSDUvT1RSdG9TMS9vM3BTc09VQW1rc3VZbnUwL3E4TGtZWXdIWHBIeXdYQXJCU3lqMmNnRTRkNG5Damswc2NhNzZkOUxzekJMZ01jZHh1cHZTV3ZYVm5mL3RvcklqR3Vpb1VmL0gxMFFoaE8wakVKdUJvc05MSmNVOFMzdVUzUmNYN3BTS3BFR05HVkJlV21zOS9lazF5Y3VXVmlpbDdYN1p6emJMWnk3M3pUSzJkdi9xc0s1eWgrSU9zc29Zb1ZzTnhrZTdPRmtQbWRSdXhWNDFFbUM2OGZjYStNaDZZQzZxMjExOTZ3c2J3dFJ5Z24xTXRtK2VvTFEvUlRzUFpPeE5zZjljV2pBL3B2THhsdXd5V05QUFF3UnBRSHowanZvelpoNzhZZDZ3QzNRNEx5ZC8zMDV3Ymtpb1FzUEVQSUtvVy9FQ1VuV0VIMkNPQ094YzRWWThpZ1FkN2NMMUlySHA3OC9uSDMxTjFDeG5BOTV0TmN0WTB5N2NWdFVXMW1JcW1TV09iNC94KzBhcUNuREVaZVZ0TWN4RzBHbGczeWZrejd5aFkvemtwUnZzVXc2cVNkSmdXdTNmV1c2aUpoQmNmSkFXd1FJY29RVXFKTHZzQUlacTRkUWZYWExqUmhTRm9IR1BTek9CWFljVFpmTGxNU3RvT2NXbWdnVjhRNVJ0UFNVdXdPNzdiREZTcTFqNm5OOWl1OEE1N2RjemdOS254NTRRWlhzUFByTkI5RzFaT25WZ1RmRzZTU0pVMXorU1VwQnFvWTFFL1h6WFQ0Y3V4TnB5eVNvWERlMzFmZWFCZTdKRndaa1dHcmcycytOREdMSW9ESjI1UDN6d1puYkpIWERxSmdMNVRsRXovRlhkRUhxK014SWdYa2tLd1dnQTBEK3RzTmJBdUlDdDRFdTN5R0NlWHZIbnF4K3FFR2dsVUtoMEQ2VTdpdnFNYXF4RW1zSjIyYkl3TGY3cEwwVEd0aWQwd2JtV21vamdQRWtVNFFUdnA2a2V1bmF2RldDVnNhMVpHakg2K1J6dDRIbVQ1NUJCdFFCYzRURy9scHpNUTIrdDVsUHA2WFpBMnhJa1JHVWV0L3UycVk2RWtOS2RxVVZZS1pZVVIxSHVPUkord214Y2taK1BvS2ZKcUNZMS9ON0xhdDVJSmhPWFdvTTJZWGVTbGFwekVrNUFXRXZnTjRsS1FvdWZqV0N0eVQrNmtVYkpXcGttUCtmNGw5VGo4NGd4NGV5N3lHMVR0SXNMd3hjVTVwcTdYdGJjQTNqcVB0K2dPaFA5MDlEaU8rbTcwSTZ4Q29wK0VRY0ZLWHBEdCs0bUg4UjZNZWMyN240NDR4Y0YyYlYxTUlKWUNKWVZDNThiOTIyUnFsU1RzM1Jsb3Z1NUxsTU5TMmxrdmZENktPOXp1b1R2NnBKQ2RtdG4rR0F4elJjUHhDTmhIL3hzYWNlK3hPY2tTSHpoS01ycXJMbytzUEtxL1I0Q1RlaEd6UlVVMHVCTjZBeFEvVVdqQ3liMzZwcUN6aVVyZTU1KzRVZkRhQjdiZUoxN0UzOTlGT25UTVRzNnlqOUorbDZZT0d3VXQwRHd4MjNKbnJ2T3V5WllQK0tjQ2tta3VkMnpXZFlzRGFNUXlZUFNHbm94NEhvQ3JseXlndTVyUW1TcWRXRG1UTHd0Z1Z2a21pQ2tZMm5SZjFjZGxYcnVxSXRPV04rdHA4T2c4aHF0NU1xT0t0VkJ3UjR1RXFLcVRwZm80WkFFREE4eXNmR01WbnkwWWtOVUEwMnpHRkZNMWV0ZCs1dmVqUVhodmhBbzhQaHBVUGFjWHZJSFBOTTArRTkxWTlUbXJHZkphc1FyNnpseHc1bDJLaWkzenpRa1dtRyt4SE14Y3dVSFZrRWJRU0JQWlpaMHJHdDJ2M25WOGNteUpEZ0M4V0lOQjNoNFFHcmlPNi8xNC9aclNHUTJCdVpidDJKTVNMZWJlS0FERHRYcVFNZUxWOS9tem5oUHZPZXAxWnRYaVRoR01oLzhReDA4NlNpdVByT1NybFpKalMzNG02MFBMU2VINnJDYWwxTFMzNzRWUWVmZjZrZ1FqaktBWFREM2tBRDYyZi9nMGU0YXZSZjlma09GbUtqT3FpL0Z3QnlNNXdpZVhjaWwxL3c1S0lLUHNWZHFCc1FJNHdLams2ck4zUUFUUk9VNG9wNFcrTVJnMVkxcnZuZGhwZWFleGtXWkF0eXduM0t0b0ZlanNYdzR0dHF3djc2Z1pieVpQWDhFc1V4WXNuYmQ0UXY3SVVuUUlPekJKTk5aVlhENm1JZ1UxeFZkUGU5ZEg4dUs5dndPRnRsTElXSXlOcFdZQVVJSThRNnE2TXhGQ0hTdFcwNGlUQXJwb1dZc05KVUpnQVF0UnF0bjJUdkIzSE0zamNMTTdJMDk4Q1lNenB1SWFsd1JlemRIWUw0ZXBtV0RPVkdzU1FzNitKemhCemJ2YjVmY3RncnhFRlQ0VExLbE0wb3A5YXVVd0F4czgvcVFxSE41Vmk4K2o3YnhNUGs5dVB0b3FJY2c4L3Q4dk1JaG1QbVlmRlVmeVpOOHVPSWMrcDJrdE9Ib0t5T3RjY1VXRGtmbGI4NWZPa2NKcmZIblhKT0hPb0E0eHRnaVZNRG5IT0JOQ0o5bG4wVlZFTWFhY2xmZTcvQXhUWHdTSDBzQk45K2hvdFJYeU9IVWlUbWJpTSs3RGhxNkNOd3h3a3gvY1hUODZKOWZFU0Zxa0xXemlvRHp5VnE1OWsxcXZCamVGS1JQU3BJRDdFQm1rYjczRHBZSWhXUW5rdEpjc2pUZy9tYWs1Y1RSSXc1WHVTMFh6NmdSZXBBWDQ1ckNzM2NaUUQ0THZ2T0hNNkl0UllqU00zMDNyWSs5Yy9UOU0zYXNaeGl4dVZPdWJBS04wM1R4ZEQrcVNEOHQ5ajNLMWxocldoUGgzL3VNRkNxekRDZ0RsOUFNRWRuTS80alRZQlREWFp5UFpNemtpcXN3OG9PN05GOWZLR2JuN2FqZTNKcitmc1lZeW5YSElkc0JkV0NBK3k4RENxY3NnUktjaWZGSXg4OVhodms2Y0xJNWY5UzdOOFVkU2VkSXZuMFhMUksrNjc5T0ZQQzZmbzhzMVpNS1dreVo0em5jdWFSS0V1dEtOc0lEMWszS0xCRncvczdqZklwV2VBQ3FuWkFnZVBUS0dDMW5RZzN6WEtXZTZEV0hkWTlBTUM1dkRZSm9xR0Z2SHBBRTJYUVRMMVloVndnbXVEVHhzNFkwS1dkYSsxV24vT2tORFhoQXdxS282NDhHa2lXRG5nYkpBRU40NlEybHYyZXdQclhMRlRtazlLbFNVQXNKMDZ0MkN2WlFzcnl3dFRRUkZ5QXVYeTNSOG83R2UraVRJTERNNkFOV1BRVUtTWEtZalVKZ0RNbnBuVTd6NnpteXRoL2lIckIvbzZ2T0VVcmxuWkNNc01abXF2VE1GVUFrNmdIM3ZtdlFjUVlsR3lhYld4bGhLbWF1QkdtcG5ZNDJaL3lMTlZLTS90bys4emd5WjNMZDlTU1d1ZW5yeXIreWJ2dGUxdlBsK1l1QmxKMGZrdEtkNTJTNVF2bXhxK0FIRmVUdnBKTTlFS2ZMbkVxUzFEZlU4MFl0cnZPNk5RcS82aHBtNXhUTVcwUW9NRTJKaE5EdmhKa0dHMHdvTDB2czdYOVFKemNlWUxnWmtxRy8xN1NmWHR4VGkxQXVmME5hNGc5dWx6eFNMZGZqdTJPcTdKalMrWGVCWHpTSEtUbGJ6NzdJcEV0UVYwTHA3Z0s3NW02NEVYdUxTbW41M2dXOTNlSngxZm15S0tjdm5nU2NXMXczdytld2hodmJSR2NiNi9SWE50RkFLS0NoVlNrRE4vR1lrQjBObFljYzFYRkR1MTRXVGE4RUppRmVCeVcxV0JhQ3N1RjR5UlZ3RmUybGdYR2NLRmR6RnBINDExVEdyNzZTbVYxSUVBeENNK2lSK1QrMUo2UGFwbXloWDJDVldwaVhFb2FDSENhNnpIaUhQUEFnY1oyUmFOaFZEcFA4Y1hReUhUUHRaNGNEOUVtanJFY1RyRUxyNkttK1ZjYjRoOTQvOWhBNmJMT1k3N1FxNDRSZzJHL3g0WGprcWxsT3lXSG1raytwR2R2ZmljczkxSE1yU0pIQWRrd0ZLZjBYZVFLREJxcFQ2YkduTlZ5RFRmK0NUY2Z5VGNrTjMzRExxcUQ0dU5SczFJV0dUUllxSkxjbjIrMkJrVUhiQ3pueHNtYUNVQVA2MVdvenFyOWs2VDBPdVhFMUZuVUtwS2JPSHBjWTUxZm52Ty96OGdUZ0FWSVpjZXBhS2xKaFl1dGp1NmhrbVA2TEM0a09uSmxvTFlUKytzenFoYVF1Sk1rcHhkcldOcnFzaSswTFQ4bXlhKzRmTXM3N1c3alY2RzEzbEY1aXlzL1VIQTlQTllScS9CMFJBL2lDVU5MNTlZQkxINjhoOXVjSzlJaWlVQkkxZmdzTGJVaTQxUmRmYU9jZnppYWZNRk5FVjBTNE9taXVoZ2RnZjVJU2txK1JEbFRqVURDQ0wyc2V1N1BHcWVYNzQyTjc5NHpqeHBaK1poR1JxUmhvYXJPSWRXMDFTbmR1QVZINzVGcUdXVXBXWEFwa2tQcXNLeXA0Ymh2MUtQWGE5djJxSjdIaHFiRTdCM3NpS0EvVjhKMkRnQUpsZXVmNDhxZDRsTjYrRnJBME5qOGVNdHJKTG81REJYOUVxYVhCTWNQODRBL0xzTkZ6eU9pb0RWN3FTMk5KWlUwekhIakVQZWFOSVp3U3hSdDMyclNCbGNtM1pIdjhHWGdZNUZSVFFua1ppcWd3enI0WUxlQmFEeTNQbHZlR1RnT1gzMUN5OVJYVmZlSEdYN21HTTJxeHBhcGFBN3Q4SXB2NVZMMFJTdThuNUJmQ29ESDlDdUZad2twQnpoaHVMQ2NZUGpCeVdOWUc3bHJUV1cyVmlBemxwcVFMRjFkaHUyUnN4MFFmL2gvZHVaNVZMQ2Qxb2dhM0dNM0ZIRUNiYjIrT3NzYmw5V21aZGJvdTROV2orbG5KRHNHNkg0MGNtdjZxUk56bTI3azVQVElWZ2kxU1VZTWdBbE5CNFViR1FybEJnb0Z1enREV1lYcWNWVVQ1Q3puY0RQcmF0OE1Dd0lsMk1Ec0ZERmJsZzA5NHo0U1VRTXppdVJYMGgwOHpIcmp2TGdmbkd6NHJQTUREYmlUekU2Q3dXQTJLeGEzZEZyMTRXNXNjOWo1OW5UNUdhWUZIeVIzMWtNQlV5TVBCVldhV0NuWEVlZytLOThQM3hiSmpTcVNzM1lsbFJSaFVmOEhmMVVvNTVoQ3E3cmJURWI0ZERzRmI4Sy9pRmFpdXRPclZMZDErOU96R0xvNm4wN1l0NTFPcmo1ZUFldEVGOEtqZGlTRThySk9HRkh0TDgvUEhxeXdRNUtNR0RwRmJvMjNXUDY2WkVISXdabmc0c2F5ME1NelRmcm1ma09IYkRsb1JhVHRZdGdxankxNG4yNGtTaXhYZmtHd2VSWDJiWTV4VFZPWUw0VlV3c05GanhZTVRvZEcvdGdIMU9UQjFTd2szRFNYam9weHRmV0dqei8xZkF2WW9DOU10ZWZqZC9sTDVsdXNVaHJ2MS9WbGpoWTdpMlFPdzdZdVhDd1ExRms1cW4xNERZcG8xYjFUa0l2STBsTGNiVzZKV00rRkxFUGtyaGZVZjlxRkhkS3JtdWRCMFBZNGVpUFYrMk5ka0x1dDdNciswMlBvcVB3SUdTQVpncFlpZXBVWHhzdkhSU0RkZEdveW5hUDFhR2JUZSs3MWt0RTN3d2xkREZ5SWtXQlpFQldCUmhMTzJ4THJTNDZmZG4xTzFBdWd3dGlJMXdGTG1ibVlTb3VxZWkreUo3bmFxREtnNnlFMFpxWEppYUpaaDg0S1ZqZ21yYUE5SERzem9UQndiNGloNFBXeTVyV0o3eUl3a3UvSHJxYTR0cG5BWjVscUdrMW5FSWtkQXJGVGk3TFJnTC9GK2M1VVp4dS9ldm5uNFVSdGtZY2tsVmRaM2ZMS1owekVnVi94SzFCajhYcjB5a1dwdkhOcC9PWHk5RnV3ZHhtb2t4c3Y5ZTkwNHZFeWtQUU1FT0R6T2J0a2tETGx3Y2ZiU0VlcXFmb1crbUZVSElhdG5STFVBSkQvc0NCSTN2ZithWXY1WGpsVWsvUG5qN0ZhY1FSNUNMZFlsc1loL1YyVjUwejBSV3d1SW9nakY4ekxVWk5EOUpRUThXSmtodXpQY005bWEvTUtiUzZvTll2WDBzU0ZIL0llYnN1a1MyaGVaSE14SGlDNkZ0UmlRUy9tYURVUkNMMGpMQjc5WXF4dm5NTGYzcHFIeXpVa3o0Sll6blhOb0w3ZjVRdkFvYjJVbVY3ZTlyMWFmNlgyMndMNmFINkJGT3ZOUFpIUWFBdWVVYzdSdDlWeEIyalcwakI5TmZQakdqR3owSEFCdklpc0t0cThNSllmQWNsSUJLTjVpeDFINGJibGR4NnFodC9Dc2hJdUlGWEZsWnRxZnMwOHduVWRkMzVZbHNoQ3ZUNktBcVRzWEpncEkwdVQ1azFkY0J4a0Z6TWlhbGJVWWFJU28yZzVEWTVPN2tkTUpkWXVQaXVtSzRpR2VsMUMxU0dxeFRtaVNIdGh5bjdJc0o1YVZOMjdZNE44SkRiN0NDVCtVQ0c2RW5iWmswVXh6aWhwaVZiV25KbU9mMnNMVGxIUTM5VWVyekhrKzFheDgyUXYwUUlOKzIza2JxSVdXWXY4SmpGRkprcndaT3IwWGhFWENUM3I2RnNDWUhVOTFwdVZIbkpCNFhBUXZPZlljdU4weXUzREpBODRJUDRqRFh2MnV6RjY3WVF1aHVFYUlvVlYrRWtCaHRHazVzZlZWN0pZNGdQWE8zSkY5c3JSMFdtVTdGMkxoTjQyVG5sRkRkVGNrR29YSlN4RXV2SEdKRkN5QmkzK1YxYWVBQmhQRS9Wbzc2VVpsZ2lhaUd5ampQSTcyM0xYWlc2QkU1NmcvcUFlZ3NESFM2RmlRQjdrdXRVM0FpNVlISElTV0I5QlBiUUUwYXNVZEd3QkJ0MDlXbS9KZEY2dnp6dTJwUlpPbXVIRGczYUZXQXR4TThUN1pLaWlpc1NSdlpSYlR4T0N4TmYwZTdHUEpRU2pQT0RtU3BzYmhSbE5IdjQzNEVHcUc0M1VGSWpabExVUi92TjIrbUk5RVF0K0NRaTgvQ2xBMnZrdzZjUzJPc3FxTlJ5ejB4OHJCOGdrY3ZuOTg4WGIzaENUVGZwSmI5bGFTVzJNby9JOHJTOWJaVTdOa2NZL1htaWkzcFUyb0xuUFdYWmhKa25CMzR0cWtSUS9QeFdTVzdXU21hQWVKN0o2VnBxbmxhdWpyUlZSemRLOXcyL1ZuZGo2M3huZlhFZWIyZ3Nwb3lHWVZaV2wzTTh2OUVCS2ZZUGRuY3BNTlUvd1BMc25yUzE1VWNlRGZ2TzU1bFI4NmNHVUQxZVRhSEZCSXhPYmlNeWU2dmtqd3NISXl6bVovZlBxNldwT2pZZk5Ec0VDeWFsMTk2dWxQUVM5RXMwSmw3b25LSDZRdm1maUhpWkNWZDl5b0xHa3JiODE5SnIzVEhzOUV5UDZhdTZYbXZlRm10SkgvUEwxM0hwNURkTlgwdFJ6aTNZTXJmVzJ2TUVRYnBhYm1WUVdvRkdBeUYxZWdGMFJrRitqNGtzdWNGL0dHQ0JNb2NVTm5ldVdNY1ViYytGWnM3Rm9OUXN4VzNYVDEyMUJoOURtMkNwdHBSU1lmWlF4WWN4MFJIREx2VEhiMWZCU2NVZE03TGJLU0ZUMkpiZVMzZGtPNlBnQ1B4MEJ4RG1EcXZmZTVNbTVjKzlqV2pZOXpSWS8xOW1OUkY2SmRDSFlKd2lNL2UwU1NoZCtQdDhYWVRqZXhRSGVrd1hzcXhKaENxdG9Fand4cWVLRFN0RHAvbjI3VThVeEErQkhZeGRFejE2bEloVXZCeHNYU0JDT0lLLzJqaVFOdHFQcjZTaW4yWitramh1OGo2aW9UaUNGOVdhaWVxMk9WUWM1cHlMRWovRUFCZDdrQWRJWFZzRVdybVhEeEJNOVZTaEYrZ2JiNGh0Nm1lVUJ5d2I3STRMQzNOSFQ0MXZKb1VKRElaSUxRMFdBTTJTaVJ5WDZId3NzZDc2U1V3c0dIMmgzbzdhMUhEWk4xdE1pb2NzTmRtYnVHK1NocUVLWkRYNGcxMzRicEpUYUlaY0l4c1FjQnlvbGdNc3k3TzcwWmZZQU1RQkpicGU2aEJJOHJFcjJLOXp0bm1SeDBVVUNCWmdKdmFVSXgzWFdJL0pNRWlWdXF2UUNlSU5EeFFtN1B5SVQzZzZ6SnJST3ljVWlzUjFxRXgyeVpFNktmNENOaWpRZHNHVis1VjZRclRVWTRLNlNIMmc5VDdqTTlYUW5qU1Z6VDRFdFB4M292N1UvZmg0allOend1VU5zTHBBRnBmMVZlT2lEUTlPQTM1VWh3T0JPdGFtVFpuZHAxWWZXaERzMmxoRk1NUmVXZGU3UFZrU2w4dk1vcnNQcDBOYTJhdEVielV1a0d4bUlVd0FGU0pVUTVFYy9na1pHUG9FQTByV2RMS1NRb0p3OXBneFowd1MrU3AxQ21wOUkzbzU5aWJ3Yzl5amJueGVJenFhV0REU0x4TU9UU2ltbThaR25vcUpiWk80U241ZzZJVEwrbVNidlE4NkxkMUZkZWVvZlhYa29tNlNGcEEycmd1WDdRQVlXNzdITU1WTWtGMGZ0L1hKRVdqNmk1NWw2MDBTNGpjOXdsZmlaTkFCU014NkhtbjdsSU5tZDBvNDhXUWNBcFkxRmNLU21tQ2FHZ3RxN0toRlpSYkJGN1MzVzNsQzRTcEF5blVqUFBnaFY1U0ZjUW9KTGk0NllMRnlud01RckpNVGNmNEVSWFEvMFVTalJ5ZDNQR3RmYzdGNFI1azM0dGxsMUlmM1BNeFA0Yy9zTXN5SVFHZkduWmtOTFJ3ZjBxQzdsV0t0SnpmNzFlWWJ5dHBSaUpaK1BVL1pEdG1ETlZQTy9qeEZEbDZXVm9ZMC9ad2I2NnMwQzF5RHBzODA3dGR4dnZYYnZuYmhxa3VBZkFOa0c4K2RmeHdBNitYV2VaSWZzODdIbHJkM0MycktJN05lRTErQWZldkRQVVhBMERJQjBuZktwWER5WkM3YThSUVNFcUxqZjhvQ3FQUzJpalhBVHEzaUI5OUNGanhVcCs1cUh0ZjJnTUdCanFiN2pLQ1hCT2VVY2N6TlIrUzBPdXVpU3NaYWowS1VUZ2dGdjdLZFRoRUxyWHRZZldGV2lwNTJ6Ly9GazU3dXUrOVRITDBKWmRReEx0alZpaXZESi9hYUIvNnZKZTV5Qzc0NUZoc2xJS0V5MEVBUjdHL2Q2N2F0bU5KWUlHRGVzSXZhNFhFellpazhBL2MyVGVFbTRwQTBsdUdPelpuTjFaL0pzU0x5SDZvYlZobkdjTmVtMUFUVlgwd0V6SytDektlcHlPZkNHcFdDV0FIWkRuRkh3MzZObUFnZGFwYncybTVscFR2MmQ4SWpPVDZYczVNZk5LK3pyajR5NG03TWk5ditiTVo0NUJIRFJubHNKaHRjUlZFVEZRbUhFRmhMdlhOTllhTWs0anZITGowUFNDWUNVTVZpWkEweUJMS2hnVEhxa3F3N2JOWi9DdHErWjlGMDBGbUlacjBmTk5pa1BiSURrZUNEYVRTL2JETXFLWVFnUG9uUWczQm9lSFpyNHNGamZTN1VHSUN0cXVhd3ArMTg0US8rNEJKWkJpTSttVFkwdkNlbFYrOXdrY1NJTnNCMUdDNmlXUWM2Wm5Ra0o4SWlIZDY3UDcvZGY0bVVmeXkvWnNyS1ZSNThlNjZGandmUFViVWRiWjFJaWVsUnhpZGRFYjFHazdhcXVMZEJLNzV0RTRJSzczaWZtbXRsc3RxUElKcmZtcnVvSk5mRndqS2dhOFRqd2liT1V3cjJIUk5uVlZ2S0dpb3Rldnhhc3JZU0QwSVM0NExzWThWK0JCckNYZDM2UkpwQzNGZkpEWjVPNE5Bd0k3U2NtQ0M0ejlJck0vZWN0RTBHdkRsK08vRW1OYU4yOWF1bVJZdXdZQVF3VlFPc1AzemNPMFFpVGFadFROUUJHdjRYQUNaREhMZFJibnZRb0RCdHpOVmhsdlBuSWZ3ek5UMGFXbmJ5Mi9uQ2d0ODZMeGMxR3VaUVZWa2pyWFFKdm4vV3lvTGdjd3hWeXRGNVovSWtjcnEwbXdzRUMvTGxINFpsTlBqVWpuaFMwL0xnNnZzRW4xbWg5azRENnRlN0lFVnY4eVJONWF4bSs3SlJLK1BGRDZrR1prWFdBcEhYN0tFb0t4cHlGOGY0czZxajhnT08raUhpcEExTU9aL2NYblk1VEJCYTYxL09rQXlXNTJYQXd4Tml3ODJub2twVWFGTDlEd241T2JBZW1SQzM5NWVmZlh6NVBjeGdySlVsL29rbE45ZlhIdEd6RFVMRVJta3NhZXNnWW9yazZCbkVpNHlCMW5GTXFGeDlBVnQ3dHhvbnVLNFN1NEVnYTJaZkEvOVZvY2JLL0JLTlJYRjAzdzlXaVpheFJtZ3pXbE5xSWhLNXh6QVcxWFdLdXpzUXVucDYvVitCUzZRQ3pJMjlGN3Y2SDdzWFJyVy9PK2dpcmlHUGdSK01OQldDZFNSVThuUFIzK1hFdjR3dENEVjBmVEdweHVIWjF0N2hKelNKSWdiblNQTzhXWmx6YzlsMXBpRnl2UlI1SFpMek5GOXdzWXRENDJucDF2SlNnakh3U3hTZy9VK1BxR21qSlh3cFM1Y2dGUE5PT253YTl2OUhxbU01V3RwUWphSnVVYkZ3dmVQSnB2WFRtQjIrMW81aWpPd0djLzhWSko5cjZyRVpoNHJzRkxpaGxNY04zOTZuTXphcEZib3padnUvektQK0VzMlVhcmNRVG1EbjNEM2FTS01HWGVXcTBkZU9OMUoxa1d0MWFXRis0R3VFbFduWHV4Y3NOOWI1QmhaWkV0ekVDakVWZ0ZMdm9Hc1hqUnNpTXNxNlBGZVJVY0Y4aWRTOHU5WWRaUnJMSnhLaWRUV21SUVk4NWgxZXNzdS9UYmVxTUQ3anpLL2tzTHNNS3JRRXRBeWthNVB4YlR3RllBb1o1RWRXZVdEN25tVUFveG92VFlweWRYYmZxUWJGM0ExdnRCYkkvelY1VU4vdHRtTjM4dlBIb3ZkdlEwYjl5NzJEWDRiS3VMMHN2TllPbXhraW1mcGdIeEJlRGV6bC84aFMvZEZkcVN1cUdNTGhEYVE5T29Ob3djdWVBVGd2Q0lBRjlRbkNTbWE0UVFEWkRiY2U5TGdMVE40ZW1USmlKOGtEQ3hxQjZrM3k5NHhvUGxmSlJ6SFNCWitVeFhkT3lkbUdZZGV6RytXZGkvUlBVdTJibEN2TUp6ZUs3WFJCcW9JbGVQTmcxeGFkTDJGSWlHSnp4anFJdmtKdGlFbVpwU2tPdWtPZkF2ZUM0WTZQQ0VacmFpSE1WL1RSZ1JsRGMycm5UenZtWEJabC9LZ0NBNFhYN2xBS3NVWVI4bVc4UzNScFB5V3RFSU1QRGZNWjlXU2dkZFA3RUFGME93YUdjZ2ZvSVJ1bEU0OHdKdlhSZkY0Y0dwUHhXcS9hQ25HdmxzeTAzMUpLQTdYb2IyMDNLMmFlVWlrbXFOZUo4QThOUjdhTEtnc2VkSENZQm5CUEFFR0IwQUN1TGtxKzdRUUNvNER0RkRlNVJBeVdoOUcyeW44eEZ5dzk1RXBDcDR6RmMvbStlYUE5QkU3OGVYbzB2UTlhZ0hSbjN0OE1iRmZCUVpXT1RNNWpJdmw2bUZIUHIydEQ3c0Q5OSttTjhoS2NKVGViTlF3eTYvYWRMcnRCREswV01aV2ZZdUp2SmFDOWRLNmRGdWVMV2pjTU1FTWYyV1o1OHZqRU5xRFNLc01wYWVnYWNSVGQ4dHNXc001T1hLUkZIR3NwWXdWTjlKaEpIOFV1bzkrODVGeEZDV01oVGdaTG9KaG8yU2ZZczhlQ3ovemNWRnBKNFNDdUlGZXh2NlBlT29MVjIrdHh6bjNYWDgyR3hzZmovODdMbU9iZmNZOVFKa0tXcisxS3dBZ3diTHMxTmVLNFk0eEVIb3BXS2wrT0VOUFB3czBpdWdwSmNSaHVjZ2pzV25WV2tYQUJhNWZxUHFtT0xMNi9sZVpVSHRNNmx6SGk1TnI2d01pU292S3pkS3l0cFNnWTVHdmFLeDR6UnpUMStqdnZ6SVBiZCtMVW84WUdDR0dHUGhqYXJFdkZONU9GK2taSHEvd2Q5MHl1Z3hOdmdMNFVDK29XdjhOQVRrZW9qem96czVMRHZFWWNuZlhlZ2JmVzNCRUt3NjJmUHhjcGVRZ1MrY3ZLMDdmQ3hDanh2a3VoRXB2SGVkZnFOZ3RpR1hnTFVhandhUmhvbkh2RDdMbDF0TVlqUUdPdGcwdzQ2blJHbVJhRE15bFNTRmdsQ2VZUytZTkV1ejNac0x2dlRNczVNdHNvaFJvNGlscE9GSkh2cTlkY2pYbXZ3c2RPek5ZUlJJVndJcUNVN1AwRnNyaVp0SzF2c1YySWRhSmpNNnRheDFkc0ZUenY1cG5HWk1WNnpLSTg1WVR3K1lYOGVMRWxFSnhRM0k5YVplTmxqdGc0dmpJcmFYOWdEa3RKTXVnY1dkZUl0RXQ3alNRMFl2YUswQVBGSGZvbVhhNWlJRUQvRFpUKzluMzR3RGhTS3J2eUhERFhiVmVLTkdVcEdUb1EyTk1qTmxnZ3BjcDExRWsxWXg2Qzk2MTdyODcvcEpadG5SZzBTTmZZeG9LZU1JWjQxa0pPakFxOXBvWEc5am5WdEJveGY5LzRYSkx0VVR2TEhQdnhHT3Zwc0dkdVZuMEFNQWdVMXg2aTlmWHN1em9CWjRleEozcFd3MktmWU4zcVpqYk80M1ljN1RnN1Nvalh1TWJrYjRabkJIdFZVNjU5YnZDdFVqZ0xPRkFZbWtHTExidnVBZzBJNWo1ZjdyRU1odnZ4cWpkRkhZeUtwdHk5djFyeXpIbklLRjVsUmJsTTRkNjA5RTNNUWJ5Q2tiK2ZiWkxvN3pxYU5lWVBGWW9PT3N1dGhvaEJETEFwRUI5ZmJhZGQxNWgrK0hER2c3aVE5RlFFMGlmRlYxZVNQNUV6TUpVTWlzdndnR3ZMOTVuZUdiTDdxWDd3Uk94VzVtM3djSzNwdE5OZGZHZmpUTHpUN21JQ3RYcHFBMjF5eTAwcm10Yjh3blgvUWhTYk1BeEEra3VqOTFveHNpdU5rV0l1OFJscHk0bDZCSjFYVGlSdndHdms4aUZpRHIxd1l4MDVTQTlES3MzeXB3OXprYi9jOURVUnFvcGN1dTZUMmNTYTVqbEcxQW9iNFQwMi9XQm5uYWRML0JVc2hOTWNBZHhxNGhiWktRRXNEaDV0a3hURldUWkZKYnpYazVkZi85U05lOWsvVkhPQWFEM2E1UmFqcjJ4bkpHUmtoZnBEZW1pRnZ5SDJmRHJPT3J6eUU3T3YzUTZobTRJS3VKcHdVckxuMVF3cWQ3Q0FSdk1ySnA5QldMZHZhSGYvVkN5OTZxUlNhUTkxL3dUQ1ZzNkR5bFpJUXN1NkI0U2RwbGMyTjVUc3NxRGtGa1hONVFkUWZiZTN6R1Z1MnR1Q0h1T1FXeHhhMVpxNTNnWWI5VDJ0ck8zdWFuZEhmUE5oYlN0VlVjUlRwQnNTcXpnTGRrdjJEZUVyTjhBQzRiK1VDSkJmQVB0Sk5GV2M1YXNQMVk3NVZjVXBqcTZoMVlFc0ZhSHZZZGFoQ2Vnd1VYS05VR1NUUmRPb2NhRVhwdDA0b2xJZnRYVG0rYjFsRHU3SENrOWU1clhqVHY0LzA1MWJ5V3NISkxoOE9NcWxoL0QyZzVxUWxLemJIc0wrRmJhUElsczVlLzJLb2pFY1lNb1pDKzA1eHN0cUtjd0hLYlRQOWN4NjJHZTN1VURaT3o2Q0tUYmM0RWJqVU10bkRCUnIvUXVIM3pyb3JWWUdVckN3WnhsMnlDdFE1enpTOEpROTNZUkZCT0FpOXZTaHJpVGJQODlRUS9jRUd2QmtGL1h3ZkNuNitDNnRnRzdWTzJIQmphaDZaVEJaTSs1L2RuUTk1QUZ2NEs2cHN5OVFwMFgwQkF1amkvNjV4bGdWWEZ4QVFxUlNaNjdONlBBVmdpdUJDU2RxamlwVnpRaTVVcmUyZnpEcklMQlYvNDhFOTYvaFNZOWp2UVQxdmxZY0ZvUkQxTUJSRmJJRm55M1M2S3Fic3kyVFZTb29jMEZtT1lOSkU5SzEzNmwxdUYzQnhoamFJOS9paUF0NFdEMWlFZHhDa1Z6cEdlYVp0UmNLNFhmcEdCYldhNkNKRHYxVWp1TmhUTy9TWTlSd25FTTVaK0JyZy8xeVRaOHpMN09Oc1dheGEvbUVpVC9VbkVlNmloQmpBVEZFYmY3Qzl3SjhnSjZSb3F2ektnTFA3MHIyd1FDdUE3cGo4NnhpNTYxTitNekJxN0ZmeDdpUWhHcmJnTXdrbzVROTdCOGl4eVJkbnBiYmY4NW5JWkZTM3dQRzFBU1pPYlhFcmJwWC9uRVNmNjZwZGRhdFhmU0RqMjRCSm8xRmNSazcySjlEK0lnK0dsemNkcEozSytmVkxLUnZLaUgyZnA5ZG5ZU3laQUN3U3RtUzhXNXJJWnptWlJUb29nZkYydUtjbzgyS1pZTjZFUVB0QkY3OXZGR01NT3FOSEhUSjNqSVBEeUpLM2VqVDRXcnF3Y1FoZjVxTEFlanh5cFJNdU96emcrUi8zVWpaTUxsWU5idUZwQW9DWFVvdUJOVlA0bm1RVlpTUkJzN0JyTXNLTXp0cWFaYUVNM1dVSTE5MjRqMG1NN0tzaWNaaVBMVzBmazF4cTFlU0sxbHF3RHpGN3RtT2V1K2VrTVFwTFhiWXlpc29JdHpLSXBybG0zTy94N0JTL01xQXRDZnV4cWJpVlQ5aXdUTFcxUlE4SGFiWFgrTkoraWp5T21pb1Rqb09JbExZZ2U2MFhPM21KNVRibTFTdGRRdFN3Qk1rclpJOHFGeERZRXRGSlAzR05SeDZlK0haOFI4d3QzYm0xYUlIR0xOanFzamtKemZyOW9ObktkWnZxdkRaTWRaZEQwR1dSM2NOeE1oM0pBNzdnQWQ1Vmw5NmhlbXFiTHBiSHNlM1RRenZuOVZRUHdsYldDZGxKdTJacFZxSFl3QTY2TjlEZE1KelBGbE1TdTFQVmxTRVpTT0VST0hRcmgrZWo2U2pCeGpDbW1oL2lEbjdjSFMwekNJK0NDUlhnWURIMkEreHJvNXFWQVZyMW1iRWM1S2d0ci9VcHBXSDVsM0R4cm5NbEhsc2l6SUtZcmg3eWZsL3BZWGU0dCtGQUpLMHNSNzNHUEdseHRNaDRpTUFnTHBFcFVmeDNtZFNVQjRqNjJlTDVUV0E3VUhkOStUck5tQWxNTGI4ZE4wSjN6ZmU4NDY2S0psc05veFBsdDg3MlBRU1hQM1phQUtDcHNpZzRYRXNJdHRkMStrNEY1RW44ZTZBUnVVVU1DUmFFY3pNOThVNmRGeWVvQmZFWnBOY0VTOEJrQ0l6b3FNRDdwL1lpcGFkdXJGaEcxNEh4eG1VbE1rOFJrUlB1TkVwWitMMjZTK1hRd2ZBNEoyVTBweXN3M3htOG5IZDJ4L25JZUNMNTFwakplVDltNUFnMTlESytpOTZXTy9LbXNxMnh1WWV0K01RSmRuN1ZRUm11Qjl4T0Uvcll3Rkp1V3RUUkVnYmJjdnlXVXBXNlo1UlNxZC9PR1o3V1IrbUkyVTV4SlZlSDJKYk84RU9iTnB0UkYyNHRqWVV3eVBpck41OHVQZWZZa2ZUN2ZJWStIZko0anRGNE9Ec2hmTEwxelM2elBSUHdoZzZrNFNLQy9VbmdINXVjWXpjTE8waDNpUlFJNzJjRGRsd0dNckZ3QjhUTk55akpERkVtbmw5WFJkbnVXT2kwYTc1b2dRSUlGN1hleDN0U0tUZWNtR2owVERNaGpXQkJIOUdPazh6UzZ6K3JCUlFRMVh1NlJ6SW5heWdmckI5TlEzd01FdUNsVWFpTi9ralFraVJEeGducEN6a1hpdHVFL2lXVVR5QmJEdUhTdm5JSElybG8wT2NFVVB4UWtON05HMWY3dGFZMEJ0bnpTNy9DaXIzd0ZQUkNPR0NiMzg2bWlqcGsxcG4wRzk5b0N0eFAxUGpsa2RTNnh4NDhJWVRQSm84OXppV2k0VTQxU3lUQlNDTTc3L2RRd0NwdDAwaEVQOFQ1Nmw3aURjK3QzRWRSMndUUFpUdmRCWEpJeXJwbDVCNW0xcm5qRktEeTFQaTdIcEczTndueGRhcWhISlpyK0NweTJlM0tERVJ1ci9lQ0dQdkFYSHZ5QW83Tyt5OHlsYTRZeklleWNaUG9HWG9TeGdQNkZ3dWtGN1NacmUxYlM1VjdnOEt1aWNwRTNzSWZhZ2JZSi9CcmxQMStkNEJJdlZ6N2QxdmErOWRhU1BFY2VvTWpUT01Da09Xd0NISURYUlpXL1dkOEV3cGR6cTk1cG5KaGR6MFpoWnlGcmlaU0VzVUxKUFduV3hQM2NlaGk2RDFnQ1R6RkpvdlBpRHBsbVZ6L2Rna2lEVmlXV1k0WHovRnFLdGNnQ3BtTnp5bStweHRzTWcrSUNvTm9UMjFjWVR2YXROZ2VmZkMvMGhPY1l1SWFyMzRZYUZ5V2lJTG1PSGFIVDNwcHpjTVhHcEp6Y1JNQzkySmZHcHJoSmgxbGM4dXRUNjkrYmtYRDFtc0Z3VDdtRmQvUSsyZ2ZpZldTbGNXTkxlbyt3eVN1cFV4dDZpNHIyTlNEWVdHWDNxVnQ3eStJRXRQWExiblZjTXBhdUZGMW9YTXZ2Qmg1S1NHUUdab3Z0QmRzUGNab3VnZm5VOFBWdi9QSUcwUnRscXZ2TXZQYm9PcXBEZlJZUGZLTjh1S3VOek5UUERkZmdwbVhsaW1NTEpkcUtERDVrOWF1Z2VxUGlld2QxTG40dlE1VmIvWXNsQ2VvSmp5M2VCT2tNSDRHcEhVc2dmTElZTjIycUM1VVlRNjhtanIyK1RBam5FamFUbzhYaTBYMnJnTHlEUFhKNkYrUHhxWDRnUDVUaDYrS1NSa1RqdDNpaUVXUm81YVBocm1EcWJHUis5Z1M4Z3RubHVVeUZZczBZeTUrYmUvbCtoQTZOWm5sY3djWWFDeWxXY21LMTBmQ3lzNEVlN2Z6M3lIelVQYzNzUm1MNVc4MzRTdnI2dUU4aDZZWkljZE5DQTdiY3N3YndFcmpib09abzRoZXByQjkvZ3p1bXZrMTJacVd5VGE4Y05IZVlodVdXNUdSNVFqM25DbFAxMEJNeTFOV214RUhOOC9JTUVOMkhBeHVHOThiOHFmbTNabnBsL0JuZUlaeDdYYUxPTndwMmF3TTNtUUNPdXlQZVArZEdmUWZvcGZmazkvTlZHQ0F2NXBJU3pXZ0hpYVRyNEduVXVTTmR6Y1d5SCtOUE5DYlI0dkcwRDVHNDZxSzhPWEc1dGZiUFVmcElHS0sxRksxK3ZiOXlHWkRmRUl6azNLVlpOWVJPaWEvb0JMT05RREsyNC8wVGZlMHU0dy9sMGw1V2lpTlVyUGxpL2U3SmYwQXhKUjJNQk1YckZNcDg1bnEzRUFnRlo1MzlCWEdVTGtMYTFRc3RjRDc4YnBsQ1NkQm1SeXJEL1N4UVZKTUpxMGJvRjVSbUVCck1GN0RHUmNiTzhacDlMbDNSQmlIeTg4S0VnVDkvdDAyM1c5TzBMNk5NMXBNeFd3VVN3MnN2ZGdjbVUrQW10R1B4S3ZkVnRjeDFHMlIwS2JVaHprbGNIZnZTSXZia1ltcTlYZFFWMVdOT2ZDRVU1Vmx1N0gvTjFxREo4SXRhL3NOSlgvcFNUenhvSWtMMHdIV3VmYUIxZFRUbnNCalFva0JjVlUyT3MwU3NnbW9GVnlJZUlxckpYVWxYL0d0UWIxdHBNVVE5ajVobmJVS0Q4bThCQ2M4akREcS9VM1hpSTk5VFkxMWxFYXErUjJwMit0OFVDRTZTZktkWUZhbjhvd3RndlJzOWtjRkdIbXJXVi9Xd3RxaEQxY3lxTW1DS2hTNmZtT3VCbkNFeS9ZRkI5RUVFcDZuZmpuM3Z3OXNPVTREUEJsRUNSWlNWZkFqV0lUK04wQmNpZ29renNKc21DdVB0MExZUFVwWWRmV3dJcENoU2ZvbFZEYm0yTDVRa1cyMmYxRFYwMklvT3R6cHViTFpHLzBPT0xENmxJT2pwWG1qLzZpV0xTeDFQeWdCNUNFS285aSsvU0lia2F6VGhOeDZaNWQ1b2d2UTFjUlZPZFlIMmI4RXRJM2R2WXpXSEdyM0Zpb3FjdE4rUTVUWkZicDh2TnN4eko1UURUeDVzUXFOMVlOOW9XUjlYV3VXUTRXMk9XaFI0UEtjblVuZkM2NXZtY3h5SmdMRXRXYjZrUXhOK0xYZTZ2SXBxdUVLVVpJR1ZRdVRuSk8ycWxNaXFoZXliN2RZNDduTlBUVzFRUTBTTGVwRnZGN0g4ZlBsSzhYQ21DVFdlQk01ZUREbi9MMXZwVVNhbWRFLzhVRVVhV2lzdHZYZExxSXR3Y3QrS09YY2NyWkZUL0hYZExNWnJBN1gyMmRmOXk3czB3QlpORnBOcEVabEh0VUxnUUV1b05reTh4YkNLT0Y5Z0lSdTRVbGw5RlVmRDduc3lTT2RxYUpQT1U2VGN6SVpTT0NlOHpWSjVwbldGbWk5MVp1b2k3VmRzZXFoOXVaNjZ6b0xuNmdCVUgrYTRXS0dHaVZYZ25CUVRwUHhiWjB1WElxTWtGS2lkTERqcktZMm1HL0NXV1hrT21BUDQ5dWcrYnd4OUtBdjhsTi83NzRUUmlYMWZDUFpzcXZPUVhuaDNxMVFnWmhIbVNBN3AzanpNU2d6ZnhkZDFKTURFRlJyZVh0V1AxeEYvUCtObXl5Tm1oc1hsSE5TMVBWV3RuZzhLam1TazNiWG5yZGthR0tqa3FUMUVSL2FBb05IWjlQb2RxSE5yem5jdkVOK2s0MGtOWnY4WTVjRmo0Q3ArK2NVYzdLQTRYbTZGWmp2RlBTemJDVlNlcDU2M3pwOG9iQ3Y2cklscnp2eEZVNWFXbVZKaER6RnBXUlM1aURoSUhVa1oxL3dRM2JrSnVDR2kzZkF3UHh6UnBXS3dQd2ZneW93VTJyaGFXRE9sSmF2SHZkT0pibUw3L05UbXo2blpzN0tOalNJR1dEQUI5TXUwTExpblYxYmJWRlhBQnJxQmtreU10TVZ3SDRKZHhDdU9lT2ZFL2pZc1RPRzRaRktZNFl4SmdnNjlaeTMzNzNxTm1kQzRORm9PQlJ2OXRMREtKUlZaMFVQNElGb3FaeUxrZ2FaOElUNWk2dzVuSXdVMXJGdmNTaEhlWktLTk9Cc3JIbW1ad0lTUktXV1FzWXFMVytmdkRZVEFEQ0I3N0gvb001a0h0ZVlUTzE4cnY1c21pSkZPZnAyUWhubG9LOXV5RVpuem9TZ0JZZjRNRXRUK1h6VmkwWU9uZ2ovYSt0VzdDU1c4TUhBVCtrTmRxRVJOK1JiWFNENkk2SzI4K1l2TWRmZUJFVFFuMmJwaEZrL0NrMDZUd1QrQ0l2ZjhRa2poTEZaZkZESG1GOTE5U2d2VHdONnhzdDlEUDBMTk9WZFE1VzJLcUxNb296MUNjU0c5d2xVdUoxcWJUTkhZRmtHTGVYQkRQZUIzanNhS2NDMW12Z1RqamcyTTNZNUlObkwrUzMvZE1QSXVmeGhScUVMRTZpVENYZ2xKTVpUdnpaYy96cVpwenZnU3JGUWcvR1Bka1V4c3dpeGk1OTc3bjRyK3dZWlBPeGVDSEI5VENnalk4SkxUbXBXaEx4ZHRmcFdCYTVLOWRTejZjOTJNVVRod0QvQTNpVjd4VW5xTDBlaWF3eEExVzdIZVJGWngzUzZ0V3MyZnFoUEgrdmFEanJzZ285c2Z5WGhLZnpsdmlBb1BoK1lqWFpTbTJKZU0rTUFJVS9VdGcwSitMZHhoZnhTZ2tvSjNKUEI5d2RlN3k3K2hKY2xnN2hKNjVLOVVhdzRPcEVET3lWWkF5OUdXVm1ITi9qVVh1K2ZRU3hhdVpVOGQrN2Zxd0RhTlFHNzZiZzNhVEdVTGI1Nnd6QTIzUllvYjZoNlRGYU5aK0JtVEFtK1RjWk9BMVNVNEgzU1dvMkcwbk10SGtGaXZWYStxdThVUU9FV1o4RDdHQ0t5emNZTG9ueUpTYk5BbWRrK0REN0RXTURYNzA4WE8zQlhsM2Y4R084aVhxd29Mci9Da01MMml1MFpVZUhzQ2hOSWZJUndYRlluRlRGSzU0Z3NZWC9kR2lIbzRsTHVNeGVDdTNZTkdvOFdpYUhOV3l2aFZjaVdvanVxYVowNmg2M1Foam13c3lkZmhFRGxkdWtKOFZnYXA3VmVVdjJBSVI3NjlseVFVQ2hkMnZ3WElreTYvTDArTU5GQWdydml2QlV6TWYyUE9pU01mdHV4TG93dndPLzFBcFo0bGx6c0s5dzRqMmRDWUZWRmkxVzFKMHBUYUJHNDdLK0lGTUZ5TTkremd1dVhHK0JBempVRFBsV29NejlDYTF0blY5RjFRVjVhd1B5ZEluSEdZNDdSbEFzSDlZT1haeEl5Mk5qdUFmZnI4Y2RHWFlkNGFJVFJqSXpSMEFxRjB3TUVxYmNSMDg5bXExSGVMYVhyT3ROREJkYlFFZ3FXTnpoV3JSNkxzRW1UTXlJN3orbkMyTFFFV3l0NzhTbVRsZ1A2VjFEOGYzbUJxcHJOYis0d004S01RV2U1aDV2NnJxS2JFai8wN21nZkhWNnI4NnpoOUYrRmE0aXZ2QllvdFY0STVsZktNTzgwQlJUN2dCb3FRbEU0T3RYOG54dXNyQlUxcGFDZms3SEowc2xlZ2V1UmtoNW0xckduL2FvSmlVelNKU3lzcjY3MFNJUWY0VWdKdEl6L0JOK0pkVWdYODExcTVtTUdRUlM3Wk45M1V4c0dEMUZ4bXJDSFNNYW11bUh6WGROVmViNzh2RU5UazhQN3dSZjV5Q0ZZclNRQ0NVQWZnNGJGMWFIWGJLeWFPdHZ2VDFLdmNBYUtHM0pzVjM4dEFMelYyRlRWOTRFS0c5YmcwU1Z1bGFvcHhFWmxKamN2VjdqK0U0YUxFSUtacVJRMG1CNGs0SVU2b2NhbDd1VVJnMi9oWDRpY2J5U3lVNjU1RUtjdmFubHN5Q3hCSVc0NWczZ2FFaUsxL21wSjdnb0gycC9nUFpMQUlVSVlQMGk0Ym5Rb2h6bUQ5a2VKK04zSExqblU5WDMzQ2d4ZnRhVmtRVjFXNVZHTEtOSVQvUk5rUXlwb0FOUFJvN1NnWjBDdkJraDA5S295MzNxVm1yV0UrWEFHSGtrYzZVNzdnL085bEhKZjJpZ2pjYjVjb3pyYVgvUlMwTlNwaHhWWFh0M0dJdmpvNjhaQmZlYWVuNjFHMDA4Yk9xY1cvZFJKZ0c5S01hMzcwS2JhenA3SDYwTHloMGorVWllVitHbklwQkVPcEJqVVhucDNsK1kvOWN5U3JLYVpOckdtTVR4dVZCSDh5T1ZSZCtXSnFJTWNFWjVXckEvYVB0SFFQaVppanFqWFNMQXF4Y3JDZTFDcFZoZVZGZWdKeGVBK29Yek1iN0IxdHJwS2RDVHpZNTZxNVlIeGRpcWQyVHZqbWZEYzV1dkhtK2gyK1JqeC9lNzhuVEV4YmY4RHhwRGFhMElwQWJmRnlnTjZhSDVvaDZxQ2R4MWpMRWsybEF0WnF4d3dyRk9lUG1La2ZTWGNUdnJyMjhKKys3bkRFd1drVUljTkUzYVVaRHNWbW1Cb1JlYis0WEprcVdFR1NmMThJSDJob1BVazJ5WnA1RUUrUXltV2drTEpBaXJZRlgyTmwrcjd4NEVXMytOTmpybFI5RVlqbEk0d0RTMlA1OXBuZDA4eFdlczBoZlc0NndMN28wdXMxQzlrZ3VqMFM4SklWenoydVlIRnV1cnFaRnd2ZG40dndJanVXUG9YNGMyWHZlRG5qWFp6VkxSZmpGb0xSZjRSYlJEcXRON01yOE1hSUxBOCt0QVZ4UHl2SXVpMnZ4eVJ4UWZXSUprQmhMZWdiWUxtS1IwcUV4dDdVcUVJcldxN0c2Kzk3L0FoYWpHeEdlejlGSkVtSkdJeG5UQTZkcmZkMkg3RHZ1cmg5bWdGdHRXYWN1VW5MWHhGVkhOdVpaMHRoaHV0NDcyRU9hSzFFUm5RYjV3ZUdvNjA1MksrQVJzbkRYOVNUVzRyOWtuY0dlbUxIQ01DNjlPYStsY1JLTm0vMUJhWXFaWFVzaDJTM011dzFMeXlYdU1PVGpnd3hmMHQ5UVM2TEFlRXJJaEpEN2pidXIyeU94Q1NYME9NOHpxOEgvVThEWS9LekhMd3ZxREZMYlBicUNoLzlJd0hJRnlOVk0rRDNnc0p0M1JNbnZleHkrWGp6Y2YyNDc2Mlp1MnJqZnczaE0rRys0ZEdWNVhwbmlpM25iWkZkUTJvUXBIQXpoN0lBOGM2VmF5MXZWY09IOTh1MzV4TlNDRGRoT3JFNFNwS05iOXAwK2NDY0FTaDMxNGtPeFNDQ2FrdHB1SlhETk5Sems1VGZtK1pUeU9yN2lpc2h4c0Q3eVdaRFl5ZTQwRjdsNWV1SU5kY0NhOTRFWnBkVU1jbDBrNlozay9VKzlQOTh4S2VRTVRPeVVVUHZKZjRpcy9WMVE0NDhkZ0tWbTJuM0NJVlF2L2ZYbzBUcUVtTmk4akRtRTdxZ0ZoMzdPaUtLMFpBTFBZaXZNVnJucThLNlVtZGdhb3MrSVJHS1ByMk9Gby84clkvOS8xTVRxdXAzTkF0V2Z2Z1k5S2RZSXlrbXVLTXVUL3RqVFZsVnZERWM2bC9vN2Z1a0VMNVFsOUFmRlBsb2YwWk5oRXBTcUZEZGs1akszRGZnUGpnQXlMVFNvR0lNMm5uajVpb0M2RXV3OXNvSU9iNkVrcTd1ZW51a2dpclFiZ3hZcU1XKzB4L2Rnc1cxamNLQUJvaTRzV2VlbUhOc0ZMcFdvZENTKzJqbE5jbWJTV3pXUzJsK1JhU2RJdS90VmhOenV2R3JzS0RROVRUTzRTSXkveXRjYzhYb25UUk9ZMnNHdzV1Wmp2eitZN0lKL1NoRk9ZVTBYd1MxR2NSMlQrQm9zc3MwN2NUd1ZDUjJLOTNkbVVnTlIybm13NkFNaG1Kc2Q3ZDFaelZmeEsxUEZwamVSTW9sTjdLSC9HaEtHajVpVW54aEREenVuSjlZblovYW9QK2ZBNDR4WWRPYTlzeVZSMGRGQ1IrZ2pFcXBRbDg5c3U4WFJtcS9DYnZaMk5HbzROYUp0U015dnFQY1lQdVNjc0RIZU9nOTZZRGFOMUpqSGtkK0prR1dUamR0WWE2azlYMjdLV1BZWDNnSEJaVFkzWWU5RWFNRWlna1lLZTZ6K2lMSlpOdXNWWlA5VVNBVWpac3JJNDhDUkhjUCs1UUcxZVNzSmowYWtFMnpZMFRkRHJ6MGFxSnZDeGRuTExQUWNUdkJVaU02eXdzMW9GRld0OU9ucjQ5ZDRBZFJpdlJGYnAwVGhIdURzTUlVV0pkQkczTEZpTjFXWE1kQVp0dmluRjNraC8zd1F4TWk3TEtDblVhNjNaM2cxWVRRWTluY0JLRFIyTGxodVNBQWxORHF5d2tQRmZ4OEtTM1Rmem1Tc1VkR0h3T1B6dktRWXp1Qlk4SEdBd2NWNEdLaEVqNWFYS0tTZ0pjZ21WRU1KM1pUbmthbkNXN3VpR2lpd0wrZklEZkpKZEZBYWM5TTJ1Qlk3WnJjbjJkK2k1ZHN3YkZlRFhFN0hOMHQ3TUxLNXNBOG5UNzZHUkZIbnpaY2MxWDlnVGNWdExyUEtlRC82Vk8xbVdhNGhjQ3o1OEMyV2lNQ09zU3pSSlN6S2FJaVVCdmlubG00dXZ4ejNwUmpnUElkNC8yVzQrZ2NuNWtJRC9mYnVETFQ0WG9XTDFXck55bTZjcS94ZjZ0TG41d3poR2xzTGp2UHpieDh5WmJlOW0vM2o2UHB0cDNLbGhoZVNVaEVhNzdRejlCT00ydGovZzJaNktqVSsrWm44WW9WcXNwVnVxcmgvbEx0NmdoSkpZMHI4dkZwbTVzb09uL29vS0pLU1pWcS81ZE9nNGhQSkttSnpiTThNOHhRbENGWVBSTldYdENiVVR5TzBHVmxONkFLQk9FaFVuS1ByempVMU9PbUtxVFpaNXgzTnNoRFI2YkhFNFpSck9ITkkzWDhYOGtOWndVSFR0bnkzMU92dkp5U2pCZ0Q2SXVBdThiejdEcWUyTG5ub1lBMC96dG0za29Wd0ZFZEZ6V0VOMEtSSE1yTkt4SWMweXNRNGgxRDVreTNPSFlCbE5rYWltMHFjbTh4UTdOZE8vdThEK2FSK01aenFyQVhnTUZ1WHJzZTBPSkNnZFJFSTIwY3gwbzZPUXA1WDJsM2pZZG5ybnlYbFQyWkJMR2lCVnB2b0pQMzFkNEhsMXN1NDB3L3R6a0tyZ1RsYXRPcXdXaVA2Wm0wRzgvQnRKWG44WFMvSUs2TUdtQnBhSzdRREpqNG41emlyMFU5aW1lTXNvNi9ZVjk4MldGaHY2a3JGQmpiMzFjR1pSZWRQVUJ0TVZqREdwQ3NveGFEcjZXeGtKcWJCY1BpL0dWTnhPVEhRV3BiNm5VWHR5UzA2emhtZmFDQXc2NUIrNjVlL2lkd0ltWkZJWkYzV0tQRjdzQ240dXoyL0NWVzNHZnVjZWtCVHFWVEJoUmVDSldaMG5SNzNJTkJCZjRsRmFFVEpVYkZFTkVtOS8zUFJESTNLc3E5dks0bk0rMm1HcHUxUk5nWTEzamFhQzJvU2J2SEhJSnN4ekFxcUdBR0dGT21IVUEwTjNaVWJjcCttTUJrZTZqRFZ4NzhDb3dPMngzR2kwWUM2YzdZVVhPYzBwNEhreG1WanBOSmhqUVcvVkpwZElPMjdVYVJkeTZKZG5hRklTT1QyRll4NVYzc3FGaU5YRENQUXRvNGhSV1UrQmVIenVHZVVYd29qZVRCa0lycTMwVHVjcXl4bE15TXhwZlBuYnhNL2IyUnVTK2dxdHMyOXJHWjdCV0FUNGV4bzJza3FKVGlZTEpVckdnVmtoU2JMaXRIK25HVmVwMU1kRlFlNzg2a296andFSTlENTZhUFUyZHJtcnB1d2tWRHhFbm9OL2JZWXA2ejRUeTlJT2VpNEFHSDg0YmIzNXJ3TEtmaWRQQ0kvUTM3aWxabXQwRHI0QkdzTlJiTmRDWDdSU1hEYTdkU0o5UFB0NmZyNFJWaGh5SzJQTU9wbEZtanVzd2lUMWJmMm5ieGFQUU5FTkk2dmVXMW5nNGZKZ3QwcElSc05PUGNQZ0dkQTBML2x4TTRqQldYWTBkc0p2WUdYd0thdDNmOGhraHIzM3NWM0M4dEVxUnVnY0ZrZ1NiR1ZrRlJhWjhIb3p0VUpNNWFqVzk4cHIwU21Eb1ZMTWs0MXhYOUhvbWQ2UGZaTnZhRzBpTE9JYVlrdVJpS2ZSbnFxaUpITllyd2ttUk9IZENnTll3STBXcURQRVl3Tmo5YndRTXhQREpUM21ZQ3o0Nm5FbmNOQ2d1T1JlSWMvWkJLekNKT1h5Y0t6c1c1S3VHbnZnZ2NzcG9jREdLRHRKQTZVYVhHaFZxUnN4Z3NadnRZRlA2bUtiVStTcExaQnN1eWtEVEVYNVNqclBOeGdKN0JUNUVkTUNyWFBqYllEa1ZteE95cWtNSEl5TkJGY1BSYXVjZGV4VDR2QWp0bHpscGlZdmJacmQxMHhlRkVoU0UrQlJOeXN4WFB5Z2wvMkViUjdSaEtxT0N0UWxxQVd3dEJGeml6d2xRdmM3bkgzVjZKdld4NnpDaDB0M0lBeW14R3lXdVovZ3lFeVRxWjVmVE9KRjZVZWtjYnI3SWJseVVYSkhEbFJmT3F1c3haWVgvSzViM0FLRmFBUGRaQ0dJSFBSZGd4TURYMGEzUmt3Z1B1VHFzZFlKOWd4U1E5aVJqSlE4UkFwVnJRSGRFUW9KUWtCYitJUlZ0MVhXRDV6bjlwOG5xc1dVd0RLZ1JIVXJyS2g3MHBVU1M2S0FDNnF5VHRIVEV0aVZwY2dmNU40ZFZnWXZzbDlhWkVVUURjcmNSV0V0bGgzaFI3YXhTM1prd1p5eUl1MWpQNHc4YktzdHdJM3ZuNkI5N0dTOUxHa1M2UWJXdkNRZkhYVFpxZWRhY0VTN1p1S05Va1BxekFRYnA0dzlzSzBTS21vYnQ4S1hFSGJHVVJ5Z3VzajF2eXYyZDFWTk1aWlNhemEyWUhyNi9wdSthclYwSG5pU3FObVRJb1hFd1RSajErclBCUjRVYUZDcVVWUFVHNmdESERsVVY4YWRmbVVVSGdoVE8vOXlnUTE2R3BKZ05ZSWd6UDI1WGJ5UFpLc09KQ2VvRGEwbU1tcjVIVW1kZERnMVJONENzb0hsQTJUby94emRmSXF6eE5obXg5Q1NQNk0veGdiMHJWUTRjSHJZbmFiSFdiWVpNcEF0NDhmN2JubUNnTDBXQmhjOElCRFlHZ3ZUVVM2d1FMYmhSOUY1cStsU3ZZZkQyY2ViMjZZS09HbUtTWTgxOTNNa1AzYTFuT2NqcityZTNBQ1MxekMydzYxNXNUT0Z5ZWhQMDQ0alVBMEt6Y1EwTmZkVTNraFhELzVHQkdyU1NsRmRpcjNteVFBbU1SSnY0V0dUWG1vMWpSbVdTWk9hNUNyRzl5Ynk1Mnptc2czOGJNczhCREx6cE8vOVhIWlh6akpQbFNTYTdlYk95TVZqMDhpK1FzTU5TZ1p3SmtnSGdJQ0VTTmJpRjlTeUdWZ2FCbEIvYlhseXZUSStJVjgxcUVUdDdCQWdPcHA3TFRXRkN3UkVZdlJDS2V5N0NGalMwMjRiRmU4WHp0QnBVMVNXSWVXN3FOa2dDNEZCdHB3ZG1iaE5kVVlMZlpVRy9TWUI5LzBnYTZjTkdTMGJoV3IwWDhOWDdqZTJwK0ZhYmhScVpVQkZnVnY1anI1Sk1WQ202bjhnajY2anVUcXBHcUtGdnFudnhpQ0JOdG1vbVMveUJDOGFUMTJFWW83bjF3cmdvalNnYXlKY1M4eS90MU52dm5BYUtOTXdaZFk1ZUJuc29JRU4xN21JTVJsZXo3YWIzTHdtV3RObmJrbmVPc0E0V3AyMmpHdUF6eFB5dzdSVmd0b0NBQTlDekdNaGNISFRDUXBEa3NjaEthVHRjeWcyUTJoT2VvYmEvODRNTFNnRldIYldHc0MvT202RFRQWTNBRHpiOElJM0xZN3FVYk9Hc0kwaktML2lIckswVzVoN1FzMCtuYnNFbmVXUWorSHVPTEZqSHF1YjJxSG1hMHJoNGU2SWwwZCtCNGNvV1dTNkIzbEpzOFArVVBRWFNrRHVXS3labGw5eEFlODhTY3lwS0FwaVUyRFpWT2VhLzhUd2l1NFNYZmxDOXJVL20wN21mUy95MTN2dzBxQzVUTVpKMzFmTDhWQnBkWUczU0JXR0VBUUNUVGhORkRnTEhSaFNsbzFVWnNSU0VjSG1IU2U3MmZkei8xaGs0UTNISGwwcWppK3FGWGFBLzdTR2w5OHhpbnhQcG5RT2JldUViU3VycjlRczdISU9lL0hiWWhrMGMraE9Cc3cxVk1RMTRzclJnRzVETTRRbWtJS1g2enRhbnlYbEp4aUs2SnJwdWI0d0JwUEZBdDFxWE1UanF5aE5wZjVVK05ocy83K0FUeGhvbTNrWjlxa2JRZzUzMnZ4MmRUUW9sMlZMbTcyL3ZuSC9vRTZLdlZWcWttcjRSYVZkVCtndERsVXdILzNoRkI4dGxzem02RG1Oa1ZsNE1aa3lJZXhZandxbk5wMVQ3WTJpdGUralBXWkJuN1lmaFowT3F4K3pONndENkl5ZVF1b1Nvb3ZreXBIOXNDRlR6REZjSytMWHFGL1MzMGV4NlkrY1pEMXRSWEZNSWplajVDTmR4dEdhL1d2OHRYVU55bWhxemltWmpTQ0N4Uy9NZDExYUJZdDRVblhDSEFSb1NqTFREUXJTTnQvcWpwbkpqVGZkQklsMk1FNzZBT0VQL29zQ0dTMG5RVGRhWTNKTXp0OUo2M3VYbnVMOVRkdHVtNTdvUDhESzRKcW8yQkxxOHIrOWVLeUJVR0dvaHg4NzJicXJ1c2VnSThONjIyTWdSbStyYWlEQ3ZpbTRzait0MG05eW1aaVZwMnI3Tk5xZGdiYmU4ZU1pS29UQVcrajhJbVF0bnV5a0hucThTZDVrV2wvWHExVjRnOXhSR1N5SEJGa1dYU3oxblNsRVJsSGhIbXZTV1hiemhxZm93SVhHU2ljMEI1cUt0aHdKZ3BwUEhuYlZvQVhoNDcyY0FmTXEraXZKMGk4TythNFZZUW1sbSs4dFloUGsxc1pDU3BuayswRDN3aDA4QkZHRFEyUHY5eUZQb0w5bDdKU3VSZjgrTkFGT2lrNmZxMmRTZTFHcGJKWU51YkdTTTJjcVo5T2V5WUUzSDF5VFBrdkRzRzRjNWtqMms4cHM5UmtCRHptUjliMnN4UFpmbzVYVlZ0ZWNtQjdvME9UdzByUDdNNElxN1pXQnZ4YnlEOWJueHY1Z2tFQys2emJWeVlQMGFVUlRyL1lQWmE1KzdEQSt1YUVleWtrQU85dzJMVjFCQ3BhMWcyVEQ1TENkOEl2VmQrOXA0c3VkanVwdE1EZGZvYVBsdTB2Z0xNUkpkYmd3K0ZYNTAwczFKMWFTU0FQQ01QYTJtL3RwUW5xeHNvbk9wSnQyM3I3cXdFcEt2eFJaL3BBa1NMbGZKZUExdFhiSi93a0hFU3R4MWE1NUFpL0NNbHBGZkQrR2NvQWFxSkx2WVU4bStwVThyQ1ZPcjUzNzVlUVN1ZFc0S3NZOEVYdlRWaUdoOE9QL3Q1YThRRmZLelZZaUYwcVUyaDNCTGZTUEd2Wk9HcUpLNzRSSlI1MmRRRXoxdCswNnp0OU9EcVpVNnZKWWFjMW1sdzE5Tms0dDFpZlBnc1B0Ylg2M3FUbzNYMzJublh3cEY2WFFqRHppMUJUOExGSWRvd0ZGVDBsTGdyOGxBZjNzZWtmTi9UQ1dteWJUWHBVcm5CY09SOEhWOENlVDZYdSs5ME9GMDVLYkRDdXAvQVBKL3BMaGl0aEFDeldjckVlL3RENmQxbWtsclI2b2NkK1dBaG9UNm9FV3BkcFJHOVYybGtMSVhPMXdTOVhJWDVPTHVmclY5bFRwWXA4NG15cjc4OFBFeHdCWjZBalNvVTJXaDFTczltUm1ob0N3b0RSYS9hRXFRVDhTTDVaRysxUFk4OU9BR1U3bWxiaGtuejJ1dG5sZ1MwWFM0ek5BRS9kdEtXOHJuRHRWaHRSOGNFSkdnbUJUNTFwcGx4blhnbS8rdzJiTW4vaEsrOWhWS0N0d0Q4SmwxcFdQV3pnSDl6MkQ3ME5iQ2ZEaTVvK0dxaUpzd3VjazdXQXFrY1Vub1FTbmZPNm1WSnIxeFdZOFpzNnE4MFFDSzJoT0JqN2VUeWtnVGcyd1VyOWhaSHE2QW9KZ3NSNFNDYk94ZFlMVi9DcjdKRWtpRGd1ZUIxdjQ5UGJQcU1yUWpKaytzK24wMjA4Ti9LbTFUN1pDbzBTK09kUlY3bDNSb2ZYM3puandyQmFtSkIxUnM1RGxvblp1WDk5aEMxcVNpdnFHV1h3c1oyOEhmTmhVOVdpRk1RajhieExRd1ptRk91UkRmQk5yWnpyUzdZWkttREVoOVVXUjNiWFBmUm5zYnptZlNrQldYWElGMkwwRmZjN1RvdkVQYXJVbEFYZlViaE4vamFZYldzOXNjV3B4MUtPb1Mvd3NBUFRDeHlIbEU0OXQ2YmVhcXBJbHB6Rkdqa1pETFo5SnZtSlJWUnBQbmxHS1VWMm8zZzNDNXJYdFFzRG9iYTVSSit2QTlMZW8rUy9sUG9YTHJBdDBLM0dBRmwxZHJxay91NmdBVUQ2WGRZMVFzeFZUSjJSNjRGRXF1bXJEQVA2Q1kxS3NCd09IRnUxMkZOKzdSMEY5d3gzK202aVNCODdVd1Rwekk1OE5JbzhrRHRkRFRHdWNwTDZGSjZQb0xyMkdvR3lHOFFrbnp0elRJSUFHVjNiL01FLzFDaUZqcXVsbm1ScytRczdLTTE3d3h1YUxNUkJIL1hBV2k3blBuSlNkMjFNNkhyVzE3QmlKME0xMWFlcXRFN1FJYk9jUW1QRUs3MEdYNW5OOWRSRFg2WVF4bzZwUHZKc29IdXc4OUNHY1FMSFUrdHlvQWFpNFpVTFR1Qm5Ic2tWOVo2ZDFPQ3c1Nkg0YjZuMng5enJUTTJLUm9ETlZtY0JNSUY5dVFFQWEvemJtcnJwQUtncU1LS0lxWnpKK2dlendKRmJxaC9jUWg3REk3dG05UG5wRmZqMXMvcEt4VEtKbjVDcU9RbTFsSkhDaDI0bFpONHFOcFFmSUh2aTlmYy9TSXp3d2dUcTVVMnJOa0ViYUVyOG9DMUprQ0hkWWhMYjAwQ2dpamlUYjRKaUdLVVZmQm5HRmRmZFlobzZFdDZiQUpwMWRDQlZLaGpSNVRVMnBBWldoZkk1VkZta05UQm1TN2ZBeGtwSXpYMm5UdDRDS2dHd241T09aVDcrbzAxbGRBREhSQ2xFQUlETGgraS92cUlJa1N4ZGt3eFozRlZRK1NUNkIwRldlaVk0bzhocWRVZW5NdWVKV29ROThvZFVLQmJUSlF3aWxjeDM2L0krUjJrbTRKNDYvZE9GVjkrYXZUdUVIaVRSSVdvZjJJNWdLSDBXVEh4YTNuVFNvMndCclRqeDJpc1NsS0FyYnJFT3p5VlhNL003RkJhemhWalRURlFibkxtbXlUQk1tQVFxTXhaRk9NUnpKOGtyMG9EM0FteGMrRzVmREplVXBMZGtkQnhSVXdPZ29tbmhIcVgxVlNtVU1ZZ0d0dGVDY2RqNk5lTWFGOThHOFIzdVYwRWlWcnBMaEt6SE5jdmJNV2Nmd2pFQWx4WG03ZjZ4RG04ZGZ3RCtZL3JVUVlBN2wyNmxNYzFEVmNSeGtvRXorVHN2SXNORUNzQzZXMTVadGprMEZ0cHpmVW4rNXBMa0JBeDI4N3ZXdzkzV0FiUVNyK1pSeXIyVlJnYlJwMnc5dnNFMGoyWDYxTWZjV1lUSmptVlVhWFVYOEhpR0R3K2VrQWNEcUhucStleXVHUU05RmNPUjl3aHdOVXFEbm00U0s5UHhzYWkxcXVjQk9LUFFSRVJaVTBPbzNFTStqdWlUMVFtakxIUUs3TGdxaGhlQWZrVkovWjhzbldib3g2aE9WWjg4Kyt2Qmo1SVdHWUZKOHZKL0tjdEFmSnRNWmJEdVVZbDkyZHJ0VXUxekZndVdMMms4Y0oxd0x1WVBKSk01Z2pLRXJDcmRxUzFJTmRxSHhqRmhLOVc0NE55Z3QyRG5Zek5IZExZYWlJSzNvQ1JxbThaeXZMcnV6MVFHRGZhT1dSN0IzTWFzcEYwR0ZiNHFncmxBSHZhQ2xNeEpINWR1TE4yUEtPN1RwTnNPTmRsRHNUSkErRGJobUluR2hjQnlzdmtLd3U2ZXA1ME9BWUY3QVZUZE91N0xUaStCc0x5VVdtVlczWjgzY3U5SnJEMlptNHhwTzQyTm41UmRVTzdWcHN0T3h3dWZxUVNRUXRHMktpTVN0bXhpN1JLUnpGald2V3FGNmpXMVFMNEk1Ti9vamI4eENWd0VPc3lzV1NqdUdxVU1oZlZtdVdCNE5iTHBpNFYxYytIQTJBdHBnMXlZcmgxeHUrbGhDQVJnN0hBMFBtczY1T09TUTl2QjdySVZNWlFlZmNSTjhjTk5FM3JyTVl5OVc4VmdKQklPU2hBc3hpQmxReEhkMElHL0t4SzNzeEFsUVpnOHNmNXZKbVU1aFJQZVVFV0dSbkJQM1ZRLzdHWU05NVV5NnlnblJNNlpoVE1IQ29aWW9LTUVlWHFPaVVUQzR6cVZvRFB3OW5qSUZ1aGh3cXpqMXV4TE5LeUc0WEdtMGgxSkhjWnFVM1FSRlR1U0Y4cmR6NDU2NG9WWWRNcmRtNU5HOUg5RFFRUzlEMVVuQWJHWnArT0NEL05IQUdlSzl1ZFF3UVAxSFgveFpsMVpHQkczbTNKY2VMeVUvWjFQS0p0SkdDMzdVazlkblFHNnBaMU5SSWRMV3drK0p3cDZvcm9xVUtUc0EzN0VveWtsWG15QWVJQmFiYTNsc2g5cFROUlEyQUxkZWJiUjJ1bDBLUVk4LzJ3RlhOTXAvZHdhcHd6WFJPSzBGYWJKSTJ2OUhXZXZ5ZmgzNFBaTWYyNEpweUQ1SE1mVUFaOTZlMThmdzlVVnpHV21qdzU0R3V1QmtVOXdncG5DQkpHMDFkbzJlZ2pjYUlhS2JyM2FDV3V6RlF4Q3VZYVR4Rm1oRTlOb1czbkUvRUx1K3NWTVkvS3ZWdG55NnZFUmtUOTRqUkd6TUZHdWpCZXNDeWRrdTBXMmhhQk1tTm90TEJSODhGMllGN1lEaFYzVEpVRTQveW1vbkhhWEhKSnlrU1hMbzk3VHpabm8xRmVOOXkvSWkyT0I0ZS9DVW9tSjE0a0xsa09jeUNOL05temdxL21EdGhlQi9iM256THpqSWd5akZycFpkb2hhYStlcVU2NjAvRjBrOTJNRnRxS0w5c0M1S0hKM1A5ZWhDeUpIUHJJTW8rVGk3cGw1WkZYT1gwcmNoU3NzY3JnR1lkM2ZtaVl0VlVWY205UXR4QkYvRkRFd3drL2lGTFYvRk9odTF6M2NJeFJuSWQ2MHA5RCt1dGthTHJaejBzaTZxQkFwMmFiTmhhSytzVnI2cjIxQ3RLUFgyUk5wcnJGd05nUHROemNmbzBLenlyV05ZQWlkNit2VDVscExxQjdLbmMvOXpid1lJVFZOVkI0TThvRnA5QW9oWWE1UkJpUGcwMENRRWR0UDQ1TlREM0lwR3k1V2JjK3V3Q0grMWE1SFl3b204K1hBU05DZEt3bG4ydkROenduWTNiTmJpL3M0MXp6c0ROZy9sRVVGaVRtOWtnWWpTQXhXaVpEZnBGSzNkUWJZMUF1eTB2V3NOWTZ1cWxJdk9YditXUzh0UXRUS1BPQmZxWTNnTlRmelNiYVZVSDJqRm9rOUdpUzN5Um1pZFdidHRlOHFMaGY4T0pnd2JiS05sbUc4aFhyc04zSFlVK3E1a3RObHl5ZVB2aFlRU1d3Wks4Uk5GZXo3eWJjdTVCUVFXVjNoQVJDT0pGbm1PVFRkY05sMDlqS3dWREhIdmlyRExUK01nMkg1dEZDRGMwTjBNd3VCWDBEU3NMeDl3Y0NjdWZBZzJyUDhrejdPSGVqUGlKSTlJNmw0VW53VFY1VEI4aE9ybmlsY1NEMm1LTGVRMDczWFNvalpUa1ZtcDVMdncvRSt4bDlIMEc0WS8yUmp1Vnd0T0txWjBpeE1OMHQ0UWE4SUF1SG85Z05QV0hDdkpSVnl4Q2tJSkozc1pQZ0cwUWlTb0srQVF4dHI4R1Q5ODFibjRQTnlCN0tjTTR2WFM3bDFOY0ZIUUFrLzBESVplci80Q2FFNUVqU3FlNkdPOHhpdDBBdEVOTWNUd3NocmpRbUNYM1l6SzFEa2NZNk9sclkxa1l0YlEwUGo4cVZYSE9ITVJFVnZ3R3V3SytvbHZ5ZWJqaHMzN3JJTmFwZWRSOEUyS0NyTkNwOWFDbXVaQmUwQSt5cVl2clY4ekVuUTJCVkpvb2lrWFNmN3NONmxEdkxONE5jZHN5NEp6aUZ3K3VLYm1NL2RkYUFEMm10OUpBOVpqM0E1ajBLY0Jpck15SGs0Y2NwWHU0a1ZLTVFiamJMR1U1b3hVdU9ZYkdhTmtIY3Y0NUo4b3R2U1B5MzFhN0Q1RDFYa3B6TURDUUUzREFzNkZHOFBiMVVXZ2JQZ0grS3o5ZVFZM0Ira1dnMW5mOE8rWFRZNkVRQjAxMmhwdnFFWE41TFRGTzdxc0Qrb1IwanU5OWJOczdQc3VwWGRRNzQxbVpPOVZJQ2h3YzNXdDI3QkgvRHNTcXpLY1hTTUhDM0p6QjloeWxyS2oxZFBuZzhOQTlBQ2tsZVlzSXlTYVVvaCtydFp5YmVoSXVLYy9aamFKWEUvTXVUN3ZRcE5VbXU4cm4yc0VpbVZyVGorS1RhY0hmcG1vRUFvaGpIVUx4cEZCWFZUcDRRc1hkZmptM2pKays5V0gyWEt1cU54ckhjc1l3ckE4UTVMd2UvL0R4Rmp4TWx2U2JGcEx5c29TSjlTU1hhcUVLRkZRUXg3SDNrbnNmOWlLSXhhTmNJc0o0T3JRR2NPUWFEVTBpQ0lqLzR0RlpleWRhdEdZbUw4eGMvdUZIMkcwR3cwNHl5T2tvRVQ0NVlLYUlEVVZjTDFubVF0L0VCa0dneThQeGZ1bjQ5ZlgwbytCTE1WamtaaXdGZzNmRVBHUFJzYUt6Q1ZuZkpST3FtQjhRa0UrWE1oVzFuOURGVUxvSW9zVC9peVkvWFhFUi9WVEhBVFVEMFF3eTBXRHlJWWxzcWJCY0VuTlBZUTBIMUM0UE5lVGNZRzFRbHRrd0JTSnMxTEdOSWFLS2RsV2hOc1NsY2FUcWZsWU1yQkc2SHFnUHFkSFA1RUNMNTdYU2YrUUlaai9pVHB0a0tKa1F6Q1hRZEQ2VWFlV1NOWmk4VmlmbXN2elk2OEM4VDRVNUJZa2tGeUdURUtxc29vTDdTeVdESGIxd3ZhZVA4MDFPTy9WTEhXNDREcGpYMmpJSnRPYlp4eDJPT3RXWWh2azVRRDFOTnFkcTBQQUdPZlhnQXhOK205QmpFK29hNWljSkc1K1NnMC93RTBJRHRwNWlRR0tkd0N6aWJDMGx2TDEyUWpXZkJhM04wVkp2dUZKM205dUE5eTRValJYSVJ4UGdGNElzRzkwalRRRUg4VHZzd0lhNzFqRVpGSzdHcjlSblQxN3FNTVQ1aTFDSHg1Vk81Q3ZSWXhIYzloLzExMVVYcnV2R3JwZjlvd3pKaTFRMCt1Ui9uVHRSeTJ0M3crdWVWWG5uTUVvZVpwakFOV2RjZGZDMEhqRzVWTXpxMVJXQXpFTGRGVjNDYk1MTktseW8yNzMzdWxMQmlEejBMbE10aXBVVUNSU1U5YlhiMVlOblA3WDd6aVFnZ09OVXhuRk1pc3g2M1JiUGNLU3U4d1JLRTJ1akZBMXNlR3NjMkhvMDhsTmFRL0ZsUC9IRXN6Nm9JeHBOb3VhNUhmSXhrM0FuZ2Y4MkVIbVphQWpheHZPR0xRMFlDeFdVZ1ZsTVphZ3lFT2Fqck9ucDlieFRocEFxMXl6M21HLzBpaE9yOEpFRDBEeFhHMHNtMEc1SGhyN2VYTDF3YUs3a3gzYkg2L3ZhckJsZmRSVFBKNlJwNVlxL3pqcE9UYmhPVzJwQTlkS1dsZExHMUJOTS9mUHVuemN1VzlncUlxUWtnQWt6RlloSDFmV0Z6MTFJL01nUUtRUklpRTBUODJRU3VmdzlUSVhIZnRwbEkxWFlwUTJ6RitsUm9WanF1VHFycHV3MUlJQ1ZuV0trS0xFMHRoOWdqWTFEZ0JSd1AwdEFpcC9LamNBSmI2UG5lVUpoRUt1OEFFU2REd0dLUmhUbEV1K0wyMDNyZ0dUTlZoYys2Y2ZPeXlMRi9FVTd0b1J5bmdOYnpHOWlSQVBiNXp2d0tGMFhlY2pvTm51OEVsRlhRamc1R2hjb0Y3VVBMcElETzhxdXlGNEtGNG9uOGNHejVreHRhY0l5aWRZNnppbURhRlNLRTJoVFRhT2sxRmU2VDVGWEFmc2JOTGFaaDFodkRheWNBMTdCNjVTellHRC9FZ2w0aFVkWGlGdENla2VYTTBxN1ZROTNlUmIxY1VrVGcrSzZ1dEVRRUdaWnFRWWdKeTduK3ZYWElxbmhPVWNIa05CaFBGMDlXVndXVW83NVEyZ21uc0NobTB5Q1RWZVhpcC93bENzY1JRSE53RlhaWHdUcU1wckZFTnZrYUVoblJRYjJDbkpza3R1TXc0dkJuRFdFcm1UVWlQSm5HZk1EblYvbkdYakRwdnhTbDFSQ2h0VkJCOHA1YjhWUm9vOUtoSUtmZW8xQzFUWmFNd1lKTVhDTndoakRtaldYMkxCSVFpWGlsQVZWVFptdTEydVJtUlZJWm1JTDJib3RXUHJxWDIrbGQxRjVZdlk1OHRJZzBSaUNFUk1LendXOS9wYWlNK1hBamh4cS9WMVJ0dVlYZFNXTng0SzdxcWlvSE8zZGxDdGxkUGduTkpwY2lYSldrdGxHUEl5OXFjeUpCQUptVmRNMVV5aTYrdENKc0xSTUx6RWFuMlhabWxlN1RYT0dManhwb21uWURDYU44UHlLZ2VISVpxdGd3ZFIwMklXYVl3cXUvcVZTZWFXSFRtbXZMMVhodHFCT2FpaUd6b3dpTkhhT1RXR0d2T3g4YzJ1bmYvYlF0MlhRKzk5Zy84N3R3L0Nkc1Z5UFRDa3hEaVFrNkkzTGpJZlpaeGtvczh1cEk0NS93ZVNCTHNMOU45OUdNTHZ6UVZJUDlIQTVkdG9nQzg5M1BEZHB6VFpkSUFYbUtkSnBVUFBSYkFPM2o3WE9meVdwaVV5Qmc4clVLSTJNWjIvRXp0UVF4Qm5rVCtGZVBMREkva1dRcFBqd3B4dVlmMDk2Z0h6QWRvTFZqUE5PZHV0ZE9FSmxMSWdBQ3VlR2V4cThlQU0vK0J1OVFDWUdlU01BU0xLMUpFbVhndzZpOTlzVVJlZEkxUGFadmoxejgrNU5EcUsrejRISkhQNkwxWURScCtoR1crWTY4UHI4dlBzZXI4TkROTGZLcG9mcVl2SnRNVThGNFZURkppK0FocG9MdGQxT3N4aXJYWXQzMVpCbXRtL2UxWVNDVG9SbHE5UHJDV3RaMnI2L1FHcWFiNkNKYnErTThvRi9KK3MwQitIdkpYU1RSUm5YTEZ4c01WZUkyRWl6NFFFbDNaNnc2d2FuRVNUUmtYZ3RHNHhZN01MY0VoRno3Ky8zSnJtd2tNSmZpdHk2QkdIQmZiZ1d3SE1zWGpiaDR4T0ppcURyZ1EwaU5ubUlGcUZkRjd3SGUwSG92SGYwZ1ZPVVFTQWQrcTRxalZ0NzNQK2UrcDZ6bEdSVkpJM3M3MWU5aVpYZTZYc3ovckFGQ2F6L0V4ZHFQZWFhTVpGRHF3RDI1dTRKNmwwNzMzd1lXc3BFWHRpd0hrNjZ1MVZ6enpudFRORStRSDRPd1NhL1dkL0Q5dTh1WDVDanhyN2wvVTZna0haWHRmczVKL01KeXgrQjkxMnY2V05IMEdUeVRtL1VVcFRFVmFzVTdibC82YVRsWVlRQ1NrMm1kS0RIcytHdHpmdnVVcVlEL1BHOXdhSW4zMy9YTlBxWVJJM204eW9OTnhjMzh6enVnZVo3SkM2QVY1aTgzRkZadU1yaXdsYmxIQTZpeW92bjJqZ1VwYk13YVdINkJBenBIMm5UZFVOS2FDZUtLMEZxVG8reEMyWmtSTVdXb1Ntdk5mQjc2eHM0YTJqaW80djY0ZWN0NGUzVmdLdHNrcEV0OVkyNkFMWDQ3UWloN2ZyOWN3ayt2V3VQSFJaTlBxTERxVDZORXVCYzZ5VldjUm5HbTJtMVhtVSs0VUtrdnBvQ0syUlFKVmc1S2J3QVpMUGpSN3lucC9zN0Q2WGI1K1kwLytZS0tpZUtQaWVodlpDVjhFZzQ4TVM1aG1WN3pacEg2bjVJWWtnaGk3akNNUDZ4MlVqTnVCZWNkcDB1WGIvRnhxWDlMVDd3NElaY1l2OUhOZ1YrUVB0UEZyVXlQbUloQWZxZ0k2cHRvOEd5NnNWNnlKN2dBdHJtSlQyV2xMWlZ3a1hLTlo0a21reW1XNnZ4SGdPUFRUd3Jnd1AyME5RTmROVG4yeVFKWi9ibkhEcmNBMHoyREpLNW5LeENWWjF6RE9DcXVYWTZMZTFDTm0yYVJkWFJOZzZscTdJYkFwcmlldVJ6cGI0SzREdnRtZDBtR1R1RXpFcDF2c0p5TFliWDNjZWJaZ2NPNzdXSjB4QWtiT1FxOWVEbGhscWFEZ2pVSDhmRHl4aHUzVEZwa1JlQUtrbGNscEk4emlBalBGejRKTGpHcVF2cktnUDNOSW92SFhUeXAxcVgwZ2lYWTBWVWNFSncrdFB4VUc2UU9WKzhUYUdvRDVtTVJZYjdBQVMycWxPVUpYcFNKa3Z5Rm8rdXB5M2FqYktyaWI0b2xJaDJTZ2pNNVF0QnBwVUxaRGl1NUVsTmxGVktmajJOYVlDZG5aRG9IbStGaXN1Q1VpWE5CWERweXBFNHg0ZUlFMi93ZjdFNHY0N0krelZ3S2t1V0Y2TE81ZWFFWjRsU0dFU2QxbmRTSEh1RHdGcjVMVkkwVmdWbDRnaWRmcytueENqNzRBOXdrTjh3S3lDem03VnJWOHBtcU5WQTRlOVJ4V0VscU1jcTBGejM4YVpSdWtnbDZhOFY3dWZHT1FFbXZnMmp2YVhsSStqRkNjYzNUOTdxM21yYUwybUNJWFY0LzAxL1hpbStuNkl5YW9kd3lNbXZ5b1NUTGZrRGliOGpMT3krbE5oZk05amoxUnVLd1pUWVJMa2Y0bXJlN2dRbkpOVUpvekN2bzRveTN1bmtRYVYwUUtrb20vSXJ5ejhBRVc5MXc3NFFjeVVkbEpFWnNnVk1PZGtnK1luV1hEUjJEQWpaM0RsS09tbDk3c1pLVjhKMlgrTXI5TWdZYzNFcjBRTHllZm9tbHhiKythL1hZblZVcFdBMGxrQmVEK1hDeUdmaWlGSjdoK2xKRkdnQ0IzaXFmUlAyQ09IZ05XOE5XRHAzU2QrTXpBRHk0WWZrcmp2YXpaT1JlU1VIRlY1L3JwVlpYL0ltN3hkQ2hMNlRwRkFhNTh5eDl2VTVHWVBpZ2JHMitXbFNQOHJySzZEYWMxNU1mRVhzVW5rWTViS0s3SjVQalhUMm9uY3VOdXkxUy91VG4wa3hwVUpRaUsvSzVxM2lMa3ordEVoUXNRVThDekg1UFBncFQ1aE95aTl4RnRaSjVMWTZrelJCOWNlWWV0aGRUeXV3eHdqZHdaVEQ4Y29tcEo0SU0zSXdQL1hMWUFRYUk4KzRiNlNGRWFka0ltZEtlQkhuSERmelRiWHQrM2xtR2dZUHkxZmh5L3d1YTlGTG5VTmJVNkxaK3o0OTllZ21HRDFNUUtaS1NERjhCVi8ydDYyZ1I5bG9tbEFPZ3htV2FhQWxxYXhmNlZXY1g0R3prM0ltbjNTd21LYjJpYnM5NTRzZzl3NmpNQVlBNjJpMlh0VUEzREU4YUJNQnBVdDBIQmQ1Zk5jV0Y1L3FSQWRyODVqSlVYWEJsRkROenN4YkFOcUdwajk1QjZWUncrSnlYeGw2WlNScnZ2dnNwNGsxMlpaRkNEMmFJRklsOXBvSFUra0Q2UGJMVk9FZE5xMTlmdGtRVCswUHRqbHlZVmF2TkhEOXhvVlBzcHc1NkJPNGI3M2sybGtuOEg0VHR6enJzYmROVDNudlY0bkpkQmhBaEhoQXZkQXJKemdwS3hEdjhPV0pnL0dnOW1sbytuUC9ENHY2Mmw1Z3NFYTBEV012OVVGMXVHMW8reGNRQzJIcTJXb1dGdUUvd3FUNjU2eG5kQlpWbEdsUmp0UnRoRUd1aTcxVG5HalEvZ2tZUWdwSXNmYmFuam5MdnBHeXpJczhzR1huVDdkSEZrbVMwN0hqM2F5UFUySFV6cklHSFBxczVsOXFxcHpzMlluU000YXJObGdoR0VsNXhicC9KY0N1cTNRSk1KNUxnazFpYjRNbUhiMytEWEtjWTlsNFZQaElKSGVEZjZWdVJwNUNIWk9qTVpmRW9qVm01aElBOHdYT1JYMWtqOEl5VHc2VkxJVXFtQ0hIQ1FKSFNYK2hkSUo5WkZCMEtZbWtwOHBob1FIak5MQlNYWW9lczlFeXJnUnNhdkxyU00ySlRRMHp3cVF4RFNXZW9OMUVicm81MDZIdDgzbEdjd2U5UDBvaDdTTUFHdUJsVEpsMWFuNXRDbnZpcVNMeXBqWjFyOWx0aVU1SjBVNFpRYlc0WCtGZ3RzMitiUTVUNVJ3RHdKRlA5eWNpbUFwNFFnQ3I2czBJRlkvZ1dSM2NYNTRISlN0eS9lUzB0blFGMXFJc1NiQ0UzeWlCMU9rbkcvODJiTDhTWTlyREZnUm5MM3gwdjlvT094NXRwS0N2SWxXaERkN25RaWx5OTF1YzhYQ0dpS1Y3Sk5OMm85ekhWK1BnL1J4bzl3S0wydVdCM212VE80dTQ4RmZjNmtKV3grcGlXRnczdWJ3dHhvTnNwYVlQSVB5Z3hCc29YVTE3SkZwMjRVMUw3M3Exc05uaG9wc3JMT1o4RTNEdUd2MTNwWlRBdFY0ck5lRi94dlJnV1ArMGF5Sy9wR0NPSDVFSmN4ZHZueFZkZVVINUc1ZHQyb0hvbkpYMlVKT3BRVzBibFFPUmVPV1lCSzR1bnIrQW84dkRhMityZU5kT3dHMmtZNytRczNyb20rY1pUUy9zZ0RRTjBTWE5waE9kSVo0WUh6ajlxTHRXUmV0WGQ5eHpzaVB6dWgzTmN2ejUxTVJRc2d3TEFZS0xpYWlVRDlFOG9jZ0tQTFJUYUpBTTBWQmlOVEs5SDlZODE0NkE3Z3JCUjZWYUpJZUt3dXp0dmtzbG5oVWJvUjE3NnB0aWZNOU1UWkhYdDZQSWxhbG4ybStnS0h4RXAzdkZJS3BBallPNmthVG82dDNmYkIwVFBrUWduMEhSWFlDR01YSk9aOVV3dTM0UVdqWVNnS0pGMlRURzZHSW94ZjhFRlBFbDNoQ1c2aFoxM3hCalRkZnlrM3JXR0NQMVp6QXBzNUJCaHg2aEkyblZSNnZSU1gvdURDR1lHczJhU3J1OEVrVVYzMEhMMm92bFBNV3VZWHJkNWFQdjRKdEFvNmlFMVNRR0lQdjZLdjBEeFNZWVZvWENIWjNnRkQwTUhxZngxVDBHK25JN2kvWkdhUWdSNGpxMlhrc096SXgrWXF1akZMNWkxTnZVeDU0bExLSmNWdGxyZlNkT3U2c2hZdVBWMTZnN1hrZ0ExaFN0ZSs5UTEyS2xqSXgxVGllYlZ0YnJ0TVlFOGl5WVBDQjMvUzBXWjN1WGJsVVA4N0VEYktuVmRpNmxYd3ZTbHZoa000SlBEL1RTNlh2VHBmODZ1dWkxaWNSZnhHaU9id0dmQWhzakhieWpQUk5Xek1QZFU4dXppM1ZkM29aOE5vL0hJRmpad2dWVHhrNm5CaGw4anUrQVg3VVduRFptZldmZFpyUXkybXFrOTdKSDdtVkxESVEwam9sYXhTMHB2TGxpWGJCSC9VQi81TUlPOUYzaWdhamZFVnZNaGh0R21BUGcyUStsQXJ2MzlDbGJGSnNMSmxwK3oyMVU0LzdFeFZYMFlVSndiNjFmYTBURDFqYnZlNExNTGpydlZJMUhLVmEzaFBacktmNHNpanlDL0YrWnhPaDFRR0NEZXVaQmRnaXNZd1FlTzNTdkszSnFjNU9ibDRQVWR0TDQxTVVTc25iTkRqTVhjd0xydStxQkxXMUdUYXNIdUF3Zm5KQlZkd3pseXhyejlHbHBTLzZUdVdxQkxuUkdWNVByOWJZa2hvUUdyaEpXUVlySW9sMDlpL2FVNmpoWkVic0hWT1NicjJ1M0ZzcUlVU29HaVdMOUMwL1FMUmhwTFpGTGZVUWErT3E5V3VLY0ZSUkRnMkdBUGlxU25Pd3IrcExtaEl0a3dRdW5rS3VYUGppeHlJdDl2dmlMbGJtdFhBWkNCTGFuNFFwR0hISjFXLzFlZWFlZTFCZzFEVGtiR09rL0dnSFJsWG4zMUU3S2R6bUQxQVV4SmR6bWh2R29IYUJpUEo2U1RSM0xodEpydTgzL0pYWDBJTFlXOUxUWmY1dERCd05VUUhYVjJqZXB1RTFDZHBFRm9jRm9QTUhleEVQaGJLc3hOWVA1RmtDM24zK0hLa211YVdMU01XeTZhSnA0YkFRWlBsRCtUSVlHM2N2aDVHWndjeVR3UzhmSnpHbGZDVFhmcklDU1BIa3NuQm1qZ1V2MVFjaGc0OGR5cFhKR0NLYmI4am9SOTA3RHVaazAvWkFiSzIyQ0dtczhjZWFwdzlsdDcwUjREQTN5UnlJSkU1N2ErQWhRM0J2ZFlTN01jL1d0aGsrNlA2dDJhU2U1Wi8wcjd4RG04dnZUUXA5UVFTNFRGWXdSNnFOY2ZydllRQjRhSGc1Vk9DVXAvSWdwWjFVVkhxcEpLc1NPSEZyVThrV01rVUwyYVJPQkhXcjZkWGpYZlQ0RWQ1b0RWTXkrS2Uvek04ZS9udE9WdEFaUmJNR2dGRG9ONUkwWkZxQ21mdEx1MFdRcytuOWxjQnRrUjh6NFBaUU92NVp4OFBPR3RTanhxb29ydGFsOWpsMjEwaVFUNWpzOE1YVGVQeGRhMi94Yk92d1ZPYjFyTkRKWDhHOVNMTldFeTd1c0NuZlJVSFlDeVo4WlBVc3dBc05IeEYveEZ2ZTJ4dGRYWUxtRmJNVUZVRytsUlZjLzBhandaMHIzTTFVZlZqSzRWQXZOY1IvejBTdGZKMXFzTWRaZzd6ZS8zVjJlTjBlNGtZL3RKNnFjZ2IyeENYNUNZOXZYOVJHNERVV0REaDVIcmhQanFadW1xdXgra2JCcGU3TUtwSkkzMzErWXZKTEkwcXlaTC9MUVpZYzhERXUydmNidmlIVUFyTUQ5cnl5QUtXaHp6MFJDek1tOUkvWEhMZTdLdUVNbkVzUFF0c252SXNJZ0EwR3ZXU25ZYXA0YldCZDlzU01Cb1Q4UjhTdCs1S25VSUFkZzQ2NlplbnovV1cwSmNiQWUwWXRHY3ZlaXVGblpQQmtmWDZTcWp0UU9haXplcDh3cDF0c2t5NThZQjVJb3JlaWVDNU9rcEcra0d4UkdIT2s3bHlmZXBDRkNQNkltS0I1WG5qcThPUXBHWk1kaHFVaXZIWEs4Y2pFc0JqK013Mm9vR2l4YlZXM2srSURiQzRjVXJCMDdrdy9OZUVNMnc1YWIrK2k3emloMTFIQklvUXUzck44OU5NRlJkc1JDM3RQd1ZUWEd6cHVJQ1VYZHRTZ1lUcnFGaEhMVU1aY2tuazNnRXNUaEhYeHhFdHhLekR1eWt4TEZFTFY3OXRpam45Q2s3eTF3RGwyQS9mNVBSYTBRYmV0THNqczBoVWVhdW9qQ20zR1VTUDVPUlNtQnRzakV6SE1UbkIxRTMyMys4THdLTXJVSVFGOEFHWkcvZjJUTWZ0WHVOazlNRHdhenpBNytJWkh6bkg0U0xVQSt0Rjd3cGtDc1B0SmpxTkE5Wi9xZkJ3VDdsQUpldC8vV0F3TXgrYWM3UjNwT0JVUG9YZlZsNFhQK1VjU3pFdWNva1BSUGFldm9WdnptS3JBSVhVdXRtbW1GNFFuVmhhUVhIaWVLcWJGVjlWL0RaZUJvbUxOaGtscHdRK0djbzBuR2E3WWQyTE4yem9tN2pYVUpoOTZBZFQ2OTlYenFtSFNFZEJQbG5CQkZjbVhkU1pHN25DUG53UzJlMURvaXR5eXJ2MVI1QkF1RFBOdDIrNFVhRlJPK3lhc1FtTTQ3WmxJRVl5eHZTbVRDZFlNMW05WHJ0U09nQWhBQzBIWE5yeWlwUjdrdkk4SVp5dE12bmltaG43eVhUaWN0UER1eEdZTDJBYndsV1I5TC9nQ09BQ3dXNDlndWQzbWJzcDlITHZDOXk5b0o3NFhHOGZLZExtT1Yyc2NjTlI5L2F0Q2FUb2piZFpmd2VCY2pYbXk0V25Rcjl0RlR5K3l0TU5Ralo2RFl2clkySVFJWDY2c0RyN3FDRHFwMVdXd25tSVprUGMyNjBlNHhpUmlodkhjUldVd0NrTTRvRy9sb3p1U0xRV0U3c0VPOEZhTnVCa25ZNWtuWFBCMzlkLzFKY0UrdVNIWERxSElrd3p0eDVqbDliaDBZZzl0OS9ySUwyWSt1ZzRQelhxWmFCWEhROWF4M2dmZkFpWTMwWjkyY1lVN1N3L3p4ZXhhWWVLQ1JTUmVwQ0Y3UDc5WWRDYlhDbWZoN2FvMXFLUFdNZEhmSEoyc0hkVUswTVhsRXJCQ1E4T1d0WUxScThYSG5vUTV1NURNRlhyZ0habzduTjNXdG9MV01UZTRETHlhRjVISGZqT2plakF2R1JTZ29BeVFMUmNtN3dPeTA5V2NBblV0QUZUbGhENUdldVh6ZXYxL3UwNjB2NitTakNBK3JlZ2pQaWl5SDlVSTNtSnViYnc1eFhYR2FwM3YrUHlKOCtYSlVMWFlHTU02dCtPdUdwZ3FpQUFPOXJMQ3pYTXBlVnhDMzdLSlZsaFlMNlhrYmFQWWhVMWEwS2YrZHRoNXl5VlJ0bEJGa0ovNXJVOWV3YXlzczVQc3pReXV3VXRCYkloeXBibVpFaVNrRDRiTkJDSEpnZmtHTDVUNmdmNERKaVA5ZUxqL1cxUmFhQlZFa1RBcEpMa1YzaHBpeFpLZlhWZGNlT0pFVFFwUzlSOUY5bzdaak0rVy9mSmE3d0Z0eVRmMEMvSi8xOC9JTTlZWnlwaDJqRFZEelhjYlJOTS9PWk5JaGZGT21uTHNoTVlBeEpZTEN2Rk03akt0SStrTmNBZTBsOVBlZCtScHBiSUFmQkZXeGd1a2FTTExTOEVybmdGeUwyYTFSaG41TTdDMlRMQjY3Um4rNEswcEY2cEtRdjNVNk51eGF0NHNWZGFiUXhWeGFub2VpY1Z5Zk5SNFBKYTZLc25VZTErdVBjMVYvOGJwY3EwMlRiZEh6WWJwekJIaUtjOUQ3ald3eDVBVk56eUp0WXNpK1BOZWFyL2Q5dUEyaDVWMS9mc2JROWtTZFBjTjdIdlNrNnFhWFJWTk4rbjZwVVpwbFozR1hUUkZyTXpUYUh6K0lhcW1wdGtNN2FmZlZzZXRBVlNkWXRPV3V5d1o5ZG02bkg5N3FucTV3TXE5OGUzWG1LTml0SUowTHJWK1c4TG52QzE0K3hkZGdEZGJGM2s4TjJoVEx0eXcwZTFlc1JGZjRrblBuSXBtSFV1bTNNc1FlNzZVei9EeDlUWTdWdnFBVXJQbTlyUTFrT0VlZDNrQUdjUTJpcUhsTUgzdmdDK2hqU2dWUkExZFpXU2d1SkFxQkpGZU1GNXVuZzlZSU5WVFhSK1c5OGFLWVdWRUxGay9HT0QybDJXZk9IVSthZzdGeTVDS1N0ZXF0NFc4Qlczb0NhK2FmOW5wdWpCMlZYYlQ4NUFwQmZYVm0zNU5MYVYyMzVmRVVGbVF4cWZha3dadFQ2R3cxL0JQLytGdVdaSWg2RGhMUDh6ZU43NkVDVmM4Mys0cnJveVBPN28ySlJvUmoxcXpqUXdPT25sVmg5QVBwTzc2TnRKNlQwQjZiMEdZY2E4K0NMWnQ0aUp6QXJUNmk3dTV6bmFtL3RxeTI1TXhIMkQ5Y0JrMU9FUFlGQVB5d3dMMmsxUDZQSy8rbHBFeDFOSHF3Q0R5MHFjQWNUWUpZZGQ2OG5DNk1KY0IrT2Rtblo3QnAwK1FZSHh6MHBGZlFhM3RHUWx1cS9HSGFldlpBd1RpMCtMYVh5NEtNTHBGZkRPbjN2TVRWbFZ1YTJHQklYSHJjcnVnSyt6MmFROHlCRWFuRzRXOVYvMjU5WUVuR0E5UnNvdUZTK3FBQkpsRlJ6YUQxZEJEaklXZkVWMXR6QTZBZjA1Q0N2ZHlMSTd5eXM4RFlGY01rbis0NHNaL09ISFE2c0hpUHI1UlNweDRtUkkwemNyQmRlYkIwemNIWVlnOVVsb2JyM0g3d2F3UlVXdk80NEF4U0JlSTQ2djlWaFBIcDQ1eExLR3BzZGszb1pPWHhFSW9OVmVYRlJIU3BzLzhaSS9jb3l4Rk1xeG9HVXRqL29PeURzRVMvT2JyWTBuSE9LUGVpVGlWa1NvN2RvZ0lxQkJ2WktLcXdjL3hUaEtHVU41WEJKY0FCdlVuS1pHWWxRNnN6VHJVVGV3cnFrcSt1S2tNbllXRFhDSCtEa0JlYVpLcmduTGMxd284dXAvUXRSVVZjUVZnS0xMNGV1elZSdGRXTmM5OEpPTDJiOHhKaHB2MXc1TDYzN2FTaExYRkFmSWVOQVdsTW1EUnUxczAzaXRnOUVaekhsck5PazcvamFPRHNjNlFWc0xWODNNeURnVzIrSHBUNkRSTUNKMy9odVJUVG82c3cxV3RhMU9LL0RvMlo1VDBncHRIeFlyem4xSHJ2S014Y1BTNVZCUVNKS0NXT2dxNmg0aDJYTC9aZjc1VFQ4SFg3SU10Y2V0b2s0SlFDWHBwUVlqelpnMDZZTEZYSXVVM05KMnlSQ2U5cHZuZ24vd0NyWVhIa25wcmpjY3JmY0ErYU1PV3EwVnhHa1RrcytLak0rdEwwcUtaUUEvVkdsN1dRbFV5Mk1UajYrQlJEdkNiaUE4MG5xRFJxMmhsVWw4azM3V055K084UHd5QjcwLzZyd25GMFN3dW9HTWxPYWFqbHlucTRSbXliWWNMNDZTb284NGY2a1NoOHpnNW9ZV21iSEZpQU5Ud0dxUW9Ja1VVeUxBZkMzQmNZcm10cFdXSnBUSmFEWUFLbnhLRk1icW1qaXNpL0dod1VvKysyczJNeXYvR1ErVlowa25uc3ZvQWdDZjhtaDZ4cDkxbHI2M2h5RFhhN2Y0VUROSytGaWFYSjJQWDR4VmRmS0tLa08xd09pS0hKeHFFZ2pjSlJMenpaSDI2Nmg0b0NoejhhenlBejUzQ29uQzlRWlNBMWhzbEZYY2p1QWdYRDRPRUxoT3IrWFZCalBhQ092djRueDRtQnVtcFYrZ0RXYlFuMlRLZnlmWWQ1QTFOclcxbEFpMnczT2dEblJyMXBnZ0VqZHZ2WkhqL3EzT040emF5MU4vVXdNallVVng1eTNHZ1J5VWdvMnhXdDJsMDNyTDBYc2dWL3BhZmZ1bjg1M1ZsREZMVjBoeXZGVGUvVGp1ZFM0aHJRL0pVaDJPKzZZK2xzclk4VFUzVlJ6WlErLzg3bWF3UDBMUVpEZ3RSSE5pK3BvejlwM0dnYy9jN1htdkxBVUJlYXVMOTFHWlR4bE0vTUpqN1hmRWZhYlhWeXNTSnBMNUQ5c1NzM1NuM1RDck1qWHV0a05na0FOMVlBMThYV0FDTjlHc28vTTNnZlR1NVdBaC9uWklkRSsydGxKTEVoVFZCRVk0TEtKb0wzQ3hxbThvWlRTZUt0Wi9EK2F4K05YV044VEpGZzJFc1NMa3pDRGhQcEhjaDl1RHpJVFNaWVhWZGVCajZjRloxTnQvUmJFcXZFeUhuSFhPSTd0RkdhbFlTVVRQQ1hjVUg3aTJlUW9oeWxERnFiU0FKWjBGdmE1eFk2Z3NLUzNNMmFXbFR4NldUeU5tZDAxeEFTSUw1Snc1ZDRCSHloNDZwZXgxaklXQis2b21qMjdDdG90bXZxMCsvbVltRno5RU13UWVZd1h4cm5jMCtWdzRsTWNXSDBtVTFXQTUrck5Hc2M3MStkTkcxd0k5Smxqd3VhaEZXU05ZdWVDWFlXdFUxTG85cThmbTY4RS9vQWJxMHY3N0xKN0RZb2tRWWp4RTAvZ2dQZ200K2haOXduMHBMLzRkR3V4VS9pdGwwY0k0SlNBUTQ4TUZ0c1pKYmxKN0NrbEhqQXBqUzU4QWJBajJYS1Y4VWN2QmFaV0Qwc3Y4SGZSaXFYMjNDTWNMNU1GSkM4RVIwWm5CVHo3VnJiVFE3K3IvVHBDNFhycHJEeHY4clF1alpZQVJNWDhFcWsyaXhpZTI5TW41UnFBbTk5R0VycEJ6NGptL1Z2a3UwU09xa29NU2IwVFN5M1hPdlVFZ3p6ZmE1VXBydmloeUFMWnh6Z2RFeHF1d2xRYzY1NEJFaXYyRlNSZGJwT1ZQc1J0dGtnMlFxYVRSb254Wm5aNy9naGJYS3JtMVVYVHA4R2FWR3Evc0M0clc5TllHMWt3bHZIMlMzaXlRKzc1bkp3SGgvd2xhVE5PRmFOMWZLMEFnd1VYQUtnUkR3dzFGbDRabmdCaHVGZ1dKa2NSUHRiazQ0T0xhNlBFZThlMHhQSE5SUlJSYWVSTndlRm94ajNVSG5Vbnd6YzB5WHNKVTI0ZWxxU2lhVjJta1l6RlJqbGFPZ1ExUDF2eGQ2M2QzMW1WbTRTVkI1ZWlHTEUxZ1B6MzBhRldGbXR0VFdHR2orSHRoM0NDNUFIbWt2RE9FeWRRbjNscm9HMUdKZ2MwZjVzbnYwSHNSU2J1RVhjVHRYbnJNSlpPb1JBdWxOeGFTWVhPY0JRRDNSeHB2L1V1MUE4YlM4YUlCTVZjcklEamo3dERoM3YwQ2lQSVFEL2ZUN1hTelpnZzI5NjZJRXNJZVBNdTBSWVdmaUhuaFB1SWk0MTA4MTdnc3A2YVAvc2pmZi9YRlJ6Q3kxdzQzVEt0ZEg3VjNOS2R5V3hSS3JtakxKNHhvRm1nY290SlVJYkV2dytCamxwNzREL1dSVGl5eFlMeUlmRnZiT0djV09yeFF5TFcyT3NsYTZFeE1nL210eXVQeHd4NGwzSXh4a1dSUmltbHdnZzlHa2hhV3NzdHFyR1paQzVGa0ZDM0d6ODFVKzlPN3Yvelo0YUNJTTdUaUNHMlFxdFJwWWpZdklqMXg5Y2V0ejg5TXY0SkNKRFNCeU1rRUZ1azQ2VkwreWNMbEd1WGhwbXhxb0ZjU0NqV0p0RW5BS2RXV09nWVhienJTUFVPZ1pnd3FTZnVNQ0RJTjljQ0dKc2pnc3dpSlhNaHdTUWFYbGV0KzBVM0RnY2JQbjZodkM2YnZEVXdqRFpsYzBnTVVuSFp2RXN0SXBXTFhJLzMzbENQckpzQ3pra2twcy81OThIb3dMdUttMkVRSlFXQlYvU2FhZDlyRVR1NEVkbEtZd040UTE2aW1PRkdHYVlvcjBicExxeTZ4RVBEUytKZndMaHdaakJPbTdwaWlMazVhT1JYcU9pazVuTlJVa2VuNVFhSnEwckozQlRrRUIvd0JxREhyY2N2VHpCekx0RkpsUzZUTm9VZlZJTmdVc2tUNlNoMkh4U1p1TzM0aXhtb3kvelpibmJCSzdVbmhsKzY5UnlPeEwrOWJBZ3NpNE84ZUtpc3FmclBwejBIR09oTzdYTWNSaUNHL2M0OVoxbXdrRzRpcDc1NmIvd3VLd2wrUk9pdXVsS2UwQWlFQjRXWHo4SlR4Rng1QzhRV1NkM25yMUI4Qzc2QXhnK3NQSjNEQUpEL1ZFblJIWmlsQVVHb3BKYjN0b1RucGhUUEdNdnpaQ0Y4bDRYZlVtZFo2bkh2TGtDV29hdWsxd1RpNDhrQUNnRUxyMjVnN2xtWmJJZTYzWHdjb0tQek9mbzZrakhnd1IrWFEvc0FmNTNzL1ZwQ25TaXNlWFB2WGdteW1ETkNRa3ZNZitCQlhRaWVFc1JRaHExYXAwWmRmVml1bEpYTjdTWTB3NEJkUDVyNFlxc25LWjRVcEFRV1pUNzlOYVVwbTh6eTBQeE12UlpBeGJMMCtvVlJWSzZlWElEaldnRmlkTHgvS2ZiQ0ZHdSthYmcxa0ZVd3AvMVd4V3VKcEttalpqK0VnTTZCMVExdFN3c0tMaWpaWXRHejZ3VU1nWHZPOWE4VFNsZTRVWkxuY0xVTDNnZHUrMDlGeDRjT05QWTZISENpUHBHV1BkSnpCWUVDTUlFUEp1dTdRVUYwMStzUThZRHZYV0NPekxvZHZwRm55dXlCeVJ2a0JrT3F2VGFSbWZhVnBzSng4cFBacWlMS0dqa2c4MFdpN3FaRmJJYVJKRHRlcVN5dFJYeDc3bFg4dVRMc0ptU0ZoOTl3TjVyL1lyQWJIcDRmVDF2bzJZSEdzZnFkSkwycXE3cGMvaHZBQjJNaXRncHBhWDZ6NmVIb0NiQlFMS0VHeEtiaVVtVElyaEF3cmxTZ2ZUeDFYbWVXUEV3b1E5WVNDT2xSeVFkZUtSMTUxQ29PdkVuV05CQmNxdVJURU5zQXFUQWxHUjNLeEE4ZjhIVEkrYld0MytEY0VHcWdrampRdDZOT1YxcDhzWG5FVWpGU254S2pMaFJLbUJpeEtvaHk5NVF6Z0c0eGJjVlJhOFpsUm9NUkc5NVpKOEdZak4vVmtyR0lRMEgwM2hBWXk2ZUd4ZEVQWFpWYTAxc014b3g3dUhjNkFDQm41eTdIbTRCZklkcW9NSzdqNTZ5RUVOVlUrVmFtakN2NEtxRlArWlk2TlhaUThGVVZaOHpSaUdoQU1TaWRJbDN2R0YvUW0rcHhTbkVsS3lyY0hCU1hESHJXeXFSUjA5dFBzQ0hXOFkrZ29HQUtrSU8xbzgyVXpmYmNlR3h6M0JTZzhPQjhtdzlqT2JqZE9mNW1lVEhqYjhUNFhyMmNyYUZBZE5qcHBNaWNzamlSVXRjendPaVdKbzVMWkRCYXpCV1FBUjk5T1V0eXhldmVDVDh5YmlwVlBMeWFaNTE5akV6bU1PRGdzUFZ2U3hpbDBJQnVmUGhIc2VMMHNvNEpRZk1mTWNnNjVOYWdPdVNhNlBMMSswQndWQUNrR1BnRlZBa2l4NlZNajJVMXBGYjRBMFpmQ1FMRlV6MkhZSFRsM2o2N0hMazQzV1pTT29WL0x1b1UvRVJRUk5yNjdNWC92SEE0M29uOWVlUzNMbnFURjZWTkFKU25xYXZteW9JV3c2WXZwYkpkYXhNYlVVUmhpOG5mMHVOaU0yT3NXODk0UUdqdm1sK0twcGI1VXJLeS9VYnJTOGRtN2ZYQUNGK0FiZTBzanZTSGNsMC9WWHUwbmsxWTZ4RmpTeTdRSi8yaW1wdExxZDdRMjJjK2VQZXJ1VURkc3Q5eXBQWVQwbnFiK0tZcHRxQi9pNDBITU9mR05Jd1ZDcUdiaDV6ekEvVzBjMllJcmV5empMV3dwNndPSVNId083MFBQNjRBNkFNd3U3NE9GNTlud0RzSmJaeXpPcitmcFJ4ekE3Y2s1Tys4OTBaYmV0TEk3OEY5V0hmYnN6TWl3M2JsTUhSdUlyWE1MYjJPeHFvQmxlYUJta0RkZ2doMjE4SU96cFUxY1E3Ni9MTjJDRW1oRlNtbTZPUlNib28yWkk2cGt3a1lXUjdXNElQNzcyTklEcW5XZXBhSjRRY01obUdyY2Y0ZzRZc1JwRnVwdnAwUzUzS1BUWmQ4TnNRdUloOU1XV2VFMXlwRXpMRVVPbzBPU1I5WjJtY2NWVjEwOHRxS2lkQjdHVUdRRW1iR1ZMdTV0RGJJQUszZWVJUktOd2RyajVQUEtvdlhqUThWWmZ2ZWNSRUtoMHBXZ3lqT2xlcXRkYlRVLzc4UnVlOFU3bm9xckRoWXZwbUlEeldzT0FiTklPQk0zN1I5T3lJYXg4K0t5d2FEOWRCeXRlc3VyaXJXS2lzQ01zQ3dHOTBwYXBVQm8yV2V6TFB4WFFkbW1lZnNuUHIwMjhBcWdVdVRSUFpURjZQdzducWZJazZSSmVPZktLMmpCVCtITVh0WVp6NmJxSE92UWhZVjBSQmFENDVZQUo0QTZwWEMwSXJuazRha2FncDFOR1NNYjlkcEkyUHZDSFRENDhzSnYxK2NpTlAydlFpVkdtN0gvZ0NCOUIxdzg5MFRXMWFXM0ZhS1NjTTlac3lRdnFmYlhYemZQUFlqcGJiYUwrdUJWS2N2ZkhGUmhWbW1EVHkzQ2tzQ2U5dEY5MDdtNHEwWjBTa0dsVm5sb0RvV0NZWFZ6VUdWbTZuWS84M1BsbkM1OWRzbGlxSklsQndFTUw4bmlJUm1CbFJiQXpUd1J6L0hwU2I0Qmtsa28zd2lnam5hc0srUWVLbGhlU2J0WW1lMFFBNFZJV2FIVkhMbWZnd1cxVk16WWIvMHRiSDFFZSsrZ0JtRHVrV0xsRWM5OGVXMmlPR0xhSkQ4L0NNNWl3N0RoZmFvZEVLNFM2MEt5Yk9yTzltMTc3T2RkYXgwQ0ozVDJsb3Frcm9tQmdXcGsvYXQwSmlTVldDK1pCK0ExdGVRYU5OL2tic2FQS2cxRE5SZFBNNEN6Mmlma2xmcjR1elpmTVRWZC9MWGNYSUxqSis4ZXNuL0xtSjNhaGY5alRJcWthSE5ISXBkeWZrN3JEZTdZcFAzR0dOUE9aSkRvQjFZb2lWckhOUmdka3UzY3NXamtyaVFGSUNJaGZzT1M1WkwzZ3VkcmZjMXhDc2pBTW0zczh5L0EwTk9BZ1JCZFpaMmVLeFJ6WUVBUVpOeUlMbzR4NjRvd0ZwRTNrcWNWY09XWFVuTHZDRXZFZlVNTEg0UitNcXZMc2FMVXcwa3docTNDUjBsQm0rK2hIVkdJVTQzbE42d0lZMFR5VkQrdzhGQ0drVVkzV01LN0tUU2t3eGNmSFBZeWlEN1RQNUswUzZHUTF4L0VSZFo5a29vLzRPMDFNZTdCV2tqaFMxWjYzUVBHbHI5dlY5WHVPNFpqcVZJRWJBaEZFVFdJWGRoa3JyWG5BdmRlVmp1R1VhOEozZHcvb0ppeGVUc0szeGpQeXYzRzFlK1NDV2Y4YytKRjY4KzB0VlY4TWhyTVU1bGc2SGFXKzlPSjJOZTFieGpFb1lPamdqamFVM1dUdElvQW1SR2lDUTdhZ1RFRHNtUzVrUGdjdVAwcnlWak4rK2t1SVpCYlgzUXRybVBvYTlUcGkxcmpNbVorNUtBNU9ZYUcrY1NES1NRQ2gwNjZIbzNXSkpZM05XRjZ5NElSS29nWmJkanRwOE9nYmVURUtDaVlGcU9jRDJtTWxyTXp2cTcwUFZqUklKc1BXYmc0dnpPeU9nRWJGaWFTSFpIaCsxWmlrTVFONko4Mm95N3BFczVjR1FodEhCSHRjOXVNWWQ1c2FWbS9mdUl3RFg5L01OSUd6LzNrcGxiMmc1V2p0T2JSWXo4TG13RFFVMjFhaFh4V0k4b2Q2OUo4MjFnMmw4SmRtWERiaEtlN1lqOUprWHdYV0hNdGxHWTFueE91WjdpaGFlWUlYbHRNd0hPMHljZi9WTlRZcytzNmREMFRteXVURC8wckRJRHJoWFBHRHNxUTk0U09ENUREMngzNUJlaE9LUk9JbVg0QjR2MEQwVDQ2WDVSWHZGNGUxSjNtRHlaMnVVcWhKVkFFY05zeW5mOG82M2F6bWJkVlN6L3RZcUZHdDhDMFVRT2tlTTV0SWJEb0p5TmpHR1FjR05LMWRBZTZWekk3R2sxRnplVjhydnc1VkJtVXFhUHpXNkxKRHhLRjR6WjRKZ3JoaExqZ2RjTmhyTEhYRjVBWXVzcTg5QTdMTzNyZlc0V3RpM3Y4TkM5a0pkTWVUSXZnNEtJamtCM2Y3TjQ1ODNtV25oNXRoOFhzZmhGWHA1OG1ObWJXc3pLZTB3cG9hd3pmYnhZMnl1S21oUkFEU1VPVGUxdENZUzNqWUVQbDZ4Z0VjZUplTUx0OHY2dTVvRnhodVBWR3NpZ0FySGxhUlhoYWFsczM3cVU2cFRhMWRrOVBIOXV5eERKUlM1djFCd2Vncmp0bGMyM2hGSS8rRnppY05iUEtFWVFoMDljaDd4SDhvcW1xc3VEWTVTczRtNTVpeE16MVFKeTRYN1FKZVlDcFU2TktxS0NMcDh4YUd2UlM5SzZYdUpYUExrWVFrbnhzV2xqV3E0VVRvKzNYcXFpQ1laNFNCcFBQT1MzRkVPV3o3Vk5rR21mbkRFNWhiTjUzTlJON2s2bDNIZW02NUVsSjNxcjhuSWNJQUtLbElPV0RUWnJxS21ZV3BxL1NEVk53OU1xT0NIV3cyTHFtcVZPelBoV1RmaHZYV2p3WjltN3VMUjdGOE1PSzRnOEdqU2ZxVUc1VVlqM3hiMGtuZEVXTGNjQ3pCZGIzTmRHektONkhRTUhTRFROSmRiRzZILzd4UFFsbGcvaUNNVVZJUFBTcGp2aHJSVzYyWVFSdU9Pd2c0Q3p5M0Y2d0U3L3B2MEZXTFM4QWJsSGVZckNEQnMzWmRYNVRGek15ajJjRCtwbHBGak9EdENLY3MvVDg5TFlQT0tHeWF1VVMyS3hMK09MbGl2VDRNeUVRYk0rcFJMSXF4K1g0dlhBT2N1MnJReWllVmcvendIOFRIRlBZZEVzL1A3djQ5MkVqdGU4KzIzd1lMdzM3Nm50MUdrYkZNdVlBbzVkSmVaSmZZbHpEL3ZlZGhiaUJRNno4SG93ZFFVYWxaUUxYSkdZd3RocjZJZGNmYURZZmc3WGxROURuQU9BRysyUlF6YW5JVHJYYUw3dDFRTkJNN1U4aW5GcTlZeGFRT3lzRUYyN2tjTnByVzBreTk3Zys2ckszUTI4OFMzZzRSU3pZejZMNVRvZjFFQmsxYXZwdGM2emJQUHJ0S1BPQmxpQzZiTDEreGt1TVl0TFFDWFhsVDdubUdJWDNSZ3gzQkFxWnNRR1dSUkZ4UWlFVis5bU9oeVNMWVJ3SWQvNUloYThHN3RiaWhTRzkvbnlYSFVrSUVYcHZhMDAyK0xSU2tpdEJIQzY2WFNnUk1xQWhHR2NpOUNVWUREV1BOYTVxbm9kZVBKQm9sUmthSkhsR1BvQ3QyUTlRaWJTS2wyT3d5b3JLQ0JiRGcvbzVzQWJXZmFyYnVGclF0ejNQOTVpUjVOV0NTNHBEOUludVFCd2xjMlFoSVBUUmUzKytYWngwc0ErcStUMTdWZnNYV3pVZWx4eHl2b1E4emVMUFMwYm51MjhtT0NLQWJFeUZhbUFCSSszRUpJOThndGR4RUpKRlN5ZGRHdHhPYzEzaXArK1Zyc3hmbzVlZldqYU1Jdjh3QjU0WVBZR3FHTXdBdTV0RFFuQTUrRXU3b0NaK1JCMVBhcjludVdhT0F4L2FVZHprc1hHc2o5dksySHVWUlZ6UWhsUjc3K2lhSTZtZ3NOMTluYWJ5dkd2U0hFbWhBREQvWWtmL0NGMnZHU3MzL3JNQk51NHVuVHNYcXQ1WTlyaEtROEdYeFdDY1U0Z3NrNlJFMXBhM1FSWmRvbS9SQXl3Qk9vbEFZSHBOVjdiUTNid1NTS1NjeWVodUlyMmRuMUo2YkhjZ0NrbHVZMVNHUk1BeVJUWTNNYit4emk0a1BaSVNIdkhDVWo3T3lGSVQ5aTRKTDR5Nm1FaW1MWVFsQU5OMGwwR0k1aGt6UXJkYSt6elozU01tV1VhM1gzWTBoQnMvcDVOc1RLUXFDdXhod0xVWFA3OTZXbGpabnlLRHlRenVOc2RFckpIbHB5M2dvT3BlQ0EzOTJkQWNBTnBqZTlyanR3R2gzUzNGQnhMN1RvZTEvZUZrNVdmOTNRVzVpSmh0TGJucGllVGFtaEdqdzhSajEwRGVSSFFDR2lXZHJreE53WDR1ZW9YYnpsSi9RZmxVZ0hTMXJLczYyeUs5RXdKWm1NbS8rTUFYVSs0dm1IcjN4MnBjcncwUUZHOHJhVWZmdEdGZnA0SG5tMXNKaWh5RDFZQ0FsbkErSzF0U3dIbUtoOHVIUU92M1BUbldJc2NDZEFQTjB2RG5jSVJ5aFZhWFBxVTA4c3JEcmpIdllZamFsOXIrU1JkVkJpcGVkZHpxZ09MR3VFZXhVWXlralllaytqZUMyQ3Z0K2oyZCtidit1citqVjM0ZHM5THZGUisrbVJyd1BVTXpHWmU3U0JyN09iYUFwTTNVSWJmbTVmWFZxSHVKd1hkdzRuRWhoVEtwZ0hKQ3k2dzI3akxTOUpnZDBlcTV3d04ra1lBOG5RbkQyTUIwMXU4czg4dFhUL1QwdUo1WjRlZEhrdVo5eXo2VFAvR2ZoR1FIWW04UTZVU0hFQUFwcjVRVHVUQnh1bDFNTm9jUDQwaWpERDgrYzRLZWN0ZDhMREtET3RxNStFVTV2ZXVhTSt0TzhyTS8rU0dNV1RrTUJtNEpBYU5ORFhZUmlDNnk5MXBVYzEwdUJ3dnlCTFduMDFLTXJjNm9xYzNYS3hLc2Y1Q216T2NqKzVZSlRBN1hqbjlRTnRrZThFRjJKU1RVcFhLV3VYbVR3TEFSaXRoTzZzYWovNFBEYnVwTUZ4clpBWUVyWU1saW1RbnRWanlBTzR6NGlVUzhhYThacS9kY3Z6QWkvSnBPSlBBZVJtWnN0RzdOVVBhUFZOVlQ4dUNpaFY3N1dZa2dmUWFRcU4zOFhERnR2T3FmQ2ZmVm81UkJEazZPL2RYeHRjVkhBaU90VGs0NVBCdEIvVXJLSDY3YWhwM0pYNTdCdFdXd2pWVmZlMzB6VFIwUTJ0UXpOUHpLdlVxLzhXVEFYQnZ5dnZvc0RsS2d5QnIrQW5ESVl3T2pCVGJOb013OHN3R1ArUllQSmFXOGY2QldmT2hxOWM0RW1POEl1TUFVMU9IVC9PWStWVThLYjI1cWg3R2VBTExGU2l6YXNpU21tY2JNRTFGY0dQcVNjQkJlU1E1d05kU3NXa0RXZXRYendDTGV2OE1JL3RPTHo4WkFMSm4xYmhkckczTkNvV2c4UC9nM1lORklvdG9ZbzJhUGdneHdjV2pRZUsxK0hNKzVueGhHaGdOcGNpeVIwd1BnNTdRYjgvWkd3MXFWWmtQRE1TM3hpK1JzR3lnSzlkWEpNK3NXMm5IaUwrT0NDODVZUkpVWjAxcXVXSXBjZHkxWnRGbktsaVlEeHNFc1RBK05yQ2JFNDI1ZmF0YXpLek04WGtZcnEwbXFvY3dPbHVPN3hicXUydzZ1OFNpdkpOSmMvYy93QStWZUJoMzlvLzV4UE1zZEgrS0NOam9CSmh2SUpSYlNOUXRBVUFmcXZkNjJKWVRTVUtodkxRdUE3TTRPalpFSzBQV2FPdU1YTjM5NmlYd0NoM0tzUGdocTJLSkhBdTh6QThGQlhxMHVGamN1citWVklhU3FtVGhTQmxkY0xCWjVXVzBOSERQYWZYUk5ZdXg5bHAxYTFsTitON09oNWRmWUxWaCtQb3c4T0cya0lQWUQ5R3h4bVcrQjM0YjNMeThva0JOOElkVURHelBiNCtVQmNFS0lWaHFKcWNMTURtSFYxWG9yUDYxZ1o2ak1TdXBXY1JHcDBsMXNqbFpETGZKTWx3ZCt5MmpjQTkxeTBvY1YxWW5IZ2R6eUhFcGtBa1ljazh5Vy96VXZlT2NVWE50Y3UvOHI3clMrSmpHbGpzS2JZZXhpQW5xTEs2dGJXa2NpRC96Rmk5dENrS2c0RWw4Y1lKMmVnRVZmQVQyd0xneDFsWkpxZURDV3lwcS9mQUx3RU0yakoyV3ZwaGNuYlFtdjZKUEFiVHRzenM0V3FJWUY5MHVmaUZjc1UyWmg3d011Vkx3UVJzeEJ5MzdXSzR3VTA5MzNjWDVjc3BoK3poM1ZGNkdFeGJhbUptTmZqU0pyTGlhNS9Jcmo4OU92YWtjT1ZTcmdNVWtpWnprQWhTZCtTK3MrZzgzS2NpNXA4QXhKa3JtMTBGRFhkczNkQmNiaTdWY1NtYnlWNDcwYnd3Y2E3bHdQMURZMmluZzhLNkZ4dVBkbzUxbGFtUDlrZFRPUlV6Ylc4cFhtYXgwdE5OV1ZGejVuejM0MXJGWnVpRHlYSFpycW9wUGVBMDRPYnA2REp4NDFwWWZtcUd2cmUyd1lKa2RBbjdWL2dWM3Qwc1BMeUNESUVPTlB2cmVPaTIvNXZyNlRrbVYxcUJMaGgyNGFnUDdqclN0and6Rnl2YkpNUkw4cU9YK1pBMERYUG5yQ2U1NWVuNks2c2JaTnd3aGQ5NktlKy83VVI2M1VKM0xyc2pBSVJpMUs3aFZtV1dxVGV4UzVQdnR2b3hJaHN5OUpTNmtmNnpJM3NuTzlOOEJsaXlYL1hmU1JBdmJYanZZQnQ5UDBST3NGa2VJMS8wcGxkOW4rdWVkWVZuVFZwcEpoTEx3b2h1UHVGS2hlUWp0dnBjaWpKZ2FXSUM0bEtpaXJsRk9tcDVJaFRaUGVmTGRKbHJBQm82bHZUcDRRSmR1YmNoaVpSMGpOUTBhMWVENTRzcTAwQzdnVk1HSndPcW1Zc3NPQ3J4TXVISjBscHZxQWNqeHFTZ1Jzamh1d1VLbmRoUGxnUFFPOGwvbFIxa1dFZGl3d1JZMFB6ZXE0UGdrQkVhY2ZQT0lQS2NwZnA0Znc0WUtyOUxDVHBCb25SZVlWL2ZDRnNvRHFEY0Jhb1FRNU5KOHlxa2Q4czF6K2FMRjJvZ0l0c1Axa3QyL0F6bTgyb1czNzVMV1RYZkdIaThzaWxIcFdQSk91STZ2ZEdWQlJIMXNwUGdPeDh4NEV0L1d2ck84S0pwOWZNVVpqY3AwcnhTRXpmUW52WGpxTSs4Z0Nid3Q1YlZLUVlMcDMzbHJ3b29NVDZxR2VjZzJ2MkVEaEhOZ0E0VU8rNk9oaVRhZ3c5NFpOaGdSNG5HYVYwUFdkc29WeG1EUEg0TlViSHhMQU5pT2FnRmpPTmJibVU2ekswOTJwR1ZSd1oxSWVkdmc5eVJyemdlT0J6WVRuRGpGZXQwNk5MRU5VVlBXTzlQMCtDQkdYQnF0M1ZoSm5oOHBuWUx4YzBoZjUrVllDa2tTTkFqV1RwZGFqblkwNnNxcW5pQ3FwNzhmK0NqUkFBUDZWTVVpU1RvWmhBTU5DSEo4VC9KRFpRVWQxemVJdlBOeVArVEswNnY4U3FQMEZpeFA1ek44NWlaK2JRSU9hbDVhTGg2NDFWRWNYVlFlUnIzOEhUSDRDc2VSTFZlN0NDK2pMSFRQYTlKY1ZXY3MvSUNjMWRNVlJoQndxK0dNYmdmNWlCbFNvUXpMOUNkMHBHQ3dVTlduaS9pcHRlUzR1L3NrV3pVSW9BRXlIUXZlUFR0SkRBQ3VzUVJhdDR6eFU3Vk85eDFxQ3lFR1pZNVdDTjZTY0NhRHd1eSs0NG5IMExqcG1HVkVrUXJoeExzMmhiUU92R3pZZlIxZGxVU3NaKzg1ejlReno2dkxIZmYxaTZDWkZIUmFnOXRMOWxhZmt6VDhHZTBQRnNxV3cwWFdodlBJd0FTc1A0VnFTVHRSWFQrYTdkdXRHY290dGcyS2lrUWp0UjFJaXBzWGRYenViSm81cGIwcjBwWU4rM3FDSlJOYzh6ZWJqZm9xTzQ3VHdSODRKRGtKZHYwRHFXNEhseW03dW0yaDcydDBsMzJ3WmdPR1d3eXRJRXkrbzFMdXlrNzFhQ0syQkRuS0lnNjRNQStYWWJ1cUM5VzduSUIvcG5MQlozMXBBY3ZoR0o1YUpQQzJyd3lVOFNOaWhMS1pBT1ltdnR1S2hTZ01lemZHY0EvUHhEMVByYnFIa3UvTmpJTmZxUVJlR1ZWSzNURyszYzg5WUo4dmJhdmRMNHprdnMxRmFyTEZLNDQ4dG1jZkpMREFUNmNqSEJibnMvTTlkNUhrZWYxZ09JNFh0QmE1UFpuM0N5akkwODlIeGJhVCtIRXRUNnlwWGtSNlZ3L0ZFcEx6YnNuUXFLdzZCMG52dHU4RHcwOXlmVFVxVWhCU1JYM2ZHUzd3US9rKzFlbDhJajBpVUNadUVLVi9ZV014cXN2RFJqT09TVmNiek5YZzlJZUFoeEdFVy9kS1hta0RLRHdVNnB1Mmw3SkpDK2tGYWdZOVFjT2hZZDkvRXFFa3UwSEdSWDlJZGRUZzI3bThROFUxSVZEM0RYQnBXWVd5K3lVZmRJMFNVYlcwQWZsMTg5dlJUd253Q2NnMy9XV0FDZ2d4TlJyZElFY1ZmS2tKbDhOM2l5bnlyVWZWQUl1RFJjOUJtR2YwVjlKMk5WcmRUcjhPaFRuOElxeFQxZExHUnlFZXE0TGR2cnVmcHdsekEzTjA2QThZOU9mY2Y2M1NxSjVuMEQ0b093U3RZSytrN3IzSlZ2NVJFK1grejAwY1pyMTk1RjNDNklGWDNmRWtCUmpRb3lvc0ZPa2RudTlIeVhyRyt3bVRKUWI5dEdlcVdWT0plVU1RMnRqYzl3c05RbEM5VSs1a1g2UDNXSnUxWVRGNjdRODdHNzVIVkZ1T0N0azZadk9xUE5yMkR0Z3hndUFBMnVGblRrTVRlaE96ZG1YYVpXOE8zemp5OVd4RCt2QnJXRHFVdUhIVklzcE5ZS2hQWGVnUllTcUxBY1l6QlkrYklzWUFSMFcyemdHZFpILzVEamdsckczZzVoRm1FaVR1bGh3Y1RuUDlCS01laVVFVy81bis2L1RsbGFqOEQ1Y2ZoNndXRTVQa3VRaHRPbXZXbkhoR1RuT1cxYzh1Ym9yalcwQXpVb3dVc1NlR2l4bDdwYm9TOEJvQjZ1bG9tdDVzWWFwbldrV1lZWDJUWDUwUXFCM3JVSndiSlpwUEt6VC9kaTVUOW14Z3pJR3RZK3kxTG1ZRjF4SVJ5ZEZRbTRtaFR6VGNmMXBaWWl1WUtWKzBqNGZTbG1XQUlhSlpiY3dpVTZiNnFNb3ZVVjR0SmdwYWlyakxVYnkzSVRST1RuUklBRjBCRkprTThkTzdtS0Q0OHJiMk9jL0ZCU2V0RE5zUzhHaGZUa2k2VVVZOU8vMTh0ZzBIN2FmS2tYb3NvYlh4Q1c1RDFXamJDK0xiRnpOQWYzVXU1TnFlcGtGWUNwQ1NjQ2dyYTNMSENVUDQrNW5qbkxaREJyUmx1eVAzY1AwUC9lVFFwMFJpWm1lODhtSTYzVTluVTNTUUV3RE9MVXVEamMyOXNSMGFrWjNGeDR6cUZJM0cySVpDUVM0eXhBNEhkWXVNU3hlN2QyNnBqS0NZUGlBNnpGVnpEL05NaUwveXVEdDd6QlpmaGt5Q2pOMjJRVjEyV01jZTJOenB3UzZjTVl3dTZjN09PZFAzM3oxaFk3YUgvTk1Pem1nVUdNQjVGQk5jdXp0ZDN0YlhuR0VZY01TR3FoMWpCL3pzR05OOG5KSEQ2M2N5dzRFK2xqdmV6WWpuMTFCOWNGTWNTcjFja21IYmdDUmgxNXVla0VkcWRjMUNZbk02NDV0UEg4cWFDY3kxdVBYdE5KUkRMSGJ6Y2dMRnBSV3RobHVnU1J5ZThhanFlZGNHTEFnS0hpVkVYQUdrVzRPeWxPbmNHSXhQL1BrKzhkb25ZbWp6bWcyN2Z3V0hmbWZMSWVSY2ZoMXFic2pzZ3hpOHk1WlB2OE4zOVpmOWx3Vm1mNjhVc2J3aVQwcUZXVEF0cmE0RXVrTnhtQWU3NUMwVS82dHk5dENhbGhFdWFNNlg1Q093K2lKaXlHQ3cyY1VGM3dRemliQ1NJYjVDeDBWZ0p1R01GVWk2K2gwcFVFVWlVK1RnYWxmajdraStoVGZEWXZvdTV0SUNTYjM4UlNSOEdsMU1PNThtRWQ5eUtwOWFYdE5DRHNUQmdrUGltSTNUMVluNi9lZ3NZZTE4ekVCYkhtdXpDTzk3ZG9aT013U1FTUThNVXhoK1p1Q0xxK3BkdHBMYUlUNis5ZEFiTU93bnllMWxjU2NpTEkvUFdObEJnTFIxYUQ4akhZR3EzYTJsOW9MT2NBTlpDa2h0ZVljSzBkMy9ESDFxS0pycm14a3VaWkxBcFNEdFVETVhjRGJnRG5WRS9tYzBnYjV2SFFUZmRTc3JvWjRNN3F1Si9memJXajREa3JaSDJqdFlNU0RxdGppU29xc2NXb2thYWZGTCtFOGhFVVVnY0FlcFJNZUdVNmRiVzhKTWJpb3Z5aFRnNXRQVGtGZ1N2MUkvWk1NZTNiZEp6RE9zWHdFV2hwUEpHYTUrN2I4SWNtdStwY2VMY1ZTSDQwZzRjT2Z0NjNmVmo3dGQzdmFOUmdQWjlwUWljZlFVR0lQMWRwdnpWVnhBdDNTeklkRUM0V2hrNGwvUUVNelNkSUxaNzZ1NHFsNGExUnorMVFHZFo3bkFJalErakxoN2NjeFFqcnZRUFV5ajBqcmI2R0syMzBEck5DMnkvV25xSUNKdExGVVJBS2syQ1crUlIrWW13MUdjVDRNeWVKMFVab1J4K05SclFYT0Q3SWE0L3BpMUxyaVA3YkdVK1U3NkdVR0JlcG1KZGdVdXpBNkJZSFZRb3d0MEtGdVd6R2RjTmdvTUR2QlNtUTFEbVJ3MHJkTFpZVlIrSmJMRGFuWjlBMTF0SlNPUmdUWVd4R0RUU1luVGx3SnFJSXJxRGpGRFgvdjVmelBnNlZWYlpkTE1yaVNwVU9TVWVabFJ3Ujg5aFdMOVR4SnV6d0xYa2dQTEcrc2pCZnM2YUVuVGRxM1FDN2ZYdiswV1kwVzE0RHNkb1pSWFNCdzQzNkR2VVcvd2k3WE5PZzR5Vk9vVEJQTlJrNmk3eUJCZWhxN21lQ0tUeUVCSWgzZE9BMzZBaDFyeUtFcHpuNUJHRVpNNENzZzd6bmZCQkdjZkQyOHBGeks3U01ieStuVHJlVlMrcnR6ekdBSGNCd3d4aE4zcXNHODV1WkxkRWMvcWNXTnBhZ09pT0d0blVqMEQyeTNJSXdOY1JwUmVYUlVXVFl2YUYxZjlleVhQN0JPV3BWcGU4TjRxazF4OWtaTm03VVlhNlA3ZlRiU2NCWTdWczFRVU1qRE1yTXdFcXJDK042TXNGWGZNZlgxc2pJOWNEU0tYazVLazdjQVJRWUFuQ2Z3ZGJSckJpNWtjY1VlNS92SG9EZUM4Q2M3N2hqb0t5Y28wdEJ4cis3R0JMRWVLNEFtcjBTSndET2ZCR1IzUVlwM005YjZPaGJiL09hbG9JRXJlNWI0dFV4N0d1aERUSFhjTERaM3FyS1dRN3NnaVlTTVc0ekdlcjRNOUFVVXpRMSs4Vm5FL1ViMHV3Ty9kU2J6MitNZzZqMlo1c3lESS9Oa1Y5YVIyMDdsSnYwZXJpelpMTzZHa0FYNW1kTWZNT00xdUNpVStSbkZuTElsQ2l6a1Y4MjdLWmFqZ040L09meWhjNXIrZWZjQXcvZUxGUi9ER0s5MGFwRTM4TTM1WUR1Z2U4cnRhWDRmSDV4N1lpUnhMK2ZGaXBKTUxnTVVPYmFxNVFPeGJZOG91TDUvTTZmNUFENE1hcVdGOHA4YklGRnRESkFuR1JJNzdONk1kY2VScmVFNWNSY1B1Ly9EeWI1ZjFlWmFxOE1YUDhyazlabFRCNlZoSDVrK2NHY2M0THJJVkpxODNKaVA1Rkx0TGdzNTE4NE5McEpNRVdNR3J4OFJ1RFgxVHYwSW9GblplQVYxRDJldUF4a0YwVTVyby8yWm9vanNwN0tKeklwVklxSWJPSkhGR1c1ZHlKTXRPb2puTSt2OUIxdWkrWXpML05rdHpmaXFlM0UzN1hFNFp1YmZXcnJ5L2VDdTZwV0J5MS90RWdMOVRJeGZzMW51VVZwUGpzanQ0dXZRM3Q1aU0zeTJ1ejJyQm1MR295ZGNMOWt0UHZWRU0yM3NTbTMwNGw1RkZLRG00RVJvLzZKWmIyZVh3UU9LZEdqOXpJU2dPNk1DNGM4M0I3SFJ6bFpVd1lPTkhmamRza0dSK3V5OW9rNEd4V0FLQTVhN1JwREZ1dGUyWHRnQzVPNklleUxiT2czVDZneWJIcnkwdE05dkk2czQwSDdXOTVUVVh0NjFNdTl0WEVxNmZSa0gxMEJSWU9rK1AxUVIwVDk4cm1hbXNySXU2ZzhHS0lxendPRUI2MzRhWGpFSTlkdytRazNTV2FYNTExT1N6NmZLOTlFRDdmUXFwRTNybHcrUFh3YkNoSXdVMXVZa2JxWUErWGRsNlUzc1lIdjNzTzZwZVNaREZscURjaTFTUUZ0MHZXVXRFdE1oVHdLdzVLWDRDd0hINitpUUVhazc1MXhheFE4d29ZczhQU2tOVnNsN3hrdkNSM1Q1QVI4anZ6OFA3dnZ1Ny9NdldpL29HdGZreGwrbzVxeUhHWVFReWg3VFUzSzM4MTBDUEhZOW5ocEhyeXBNTWM1dzIrczVTRVE5SGdhWDA2RkVvQjBId1U3eVVoNXVlcy93MTVNekhiS0VuS0Myd3NDTVpWN3lyVUtjdFlJVC9EdXdLTmlnRUNqTlNDdzJwbnJsOEJicUFOaEtjc0VkaVR6c3haZWlWT2Job0ZnZVVQL3FOMjdLWDRKcDN4OU8xNFBvVTQ5SSt2TEhidytudkNWanV3K21NQ3R4Z20zUnl1c2QwdXloeTVkelJQYjEvMmtSN2lNd2llZzAzcHBURG5NZjdHOHFzeTNocU1kTW41cEwwcHpQT21DOXlBT1l0VUdLNlAybDZBTFVNUWhVV29HM3NrZ0VEMW9vT1k0bDNaS3Y0aC9tdkk3d1l3aTRNWk5va3IvSitYWGZHQlplTkxISUd5WGVXUk9vZGx1OS90UThyNVFiVndxdVozMEdRcklSVXVxOVhHWHh5blcyQ0pZT1hTME9TcnBYa2ZmbU9xZ1c2eVF3YUdoVGZvbHRCTEdweUZoeFBPbm9saGdyeFJidkJtRHRjT0UrMyttTDk4Q0IvS2tFWnFiK25oTmp6WWh3OGhmSXg5TCtjazlKcFd0MER2MHZsTFVhZ3dGdzdIb3pTclpjWE0welFtb2J2aUFqTnAxWWVxZ2s0S1FzTmNrbFZxZjVvQjh5NUM5d1plbUxNYUFESThpaEd4dkIxeERvaTR6VmJJWllVY0F0L0h6bzRKbnFlTXhsRm1mcEhHL2tXMUxwZlNZTHpNQzY0T1FBR01QejRvUllMODczZ2tSbXdJYU91LzduQndMTXhZZWZuUHJ0eWdvaFJTaHU4ME56VjlNRFFZMTFuUloxbVdmclRYR3IvRzZ5TW9IN0pidXU0UjNTRVp1RjdxVUhtUFhRdDhYKzhkOFNwZzN4Y2RWZ2pudEZ5UVhIMWt3RmRzWmdWajY5WDEwUkx3cHIzbEM2aVAzUjBQOVhKM2lSazBUNjYzSEZHdUVLaXNxcmhmWmoveTloeDYra20xM0NLa2orMkZKeFFPeXF1WE9EWFNsbVYvSlVsSytuQVJ1WkpJeWxPeXp1My9ZUXVZcmFCOXBVY3d3RWtlUFRaQUk3VjdBeHl6WHRkK3ZnWlpVZ1VnbUFjQXd3RkRUWmRHUlJVbE56NTZlc1RzNm5IOUFDWk40eERjbkdIZ0t1dk5zZE9oVDR0Vnd6MllzbW5wTy9waThhOUtHbkkzTldJQlNlSXVWYlhPZEN6VVdzSTJGQlFSSlhjVTh4Umc2UTFCampQVGxrQmJ2cDV5eFFRLzRCM3htZUE5Y2lGYm1uQ2ZlS0E2bFFNQlVSV2t5amkvcGJrM0dmREhTQmw3R29KMFZPOCtpdlZEbVJNRGdhbkRveFJ1OE9abXpsSmd3MmtlY1Jtb2VNa1NBRVgxTThjRGs2Zi9ISHJrSkFPbTFtZ2VUZWFhYWpJWEd5aWtOaG9vNm8wblFRTTU3UzNLcjNlaC9ncFp4MVhGZmVyejFUWDdWUEowUWljNG95REpnTzZObFNMUXhodzdwMGtpb25qMFQveUxjN25RNkRZVk92Vk16a3ozUGt4NUZsMEJFd1FtM3NJVkxjcDFuWExrczJiMjI1ZnVjVll1aERSNkZ1Zm8xUVlQQWw2ejhocDFmaExGdUwxRjRBWkhaVG1YRWNiemhxRVNEZkFKcGd1N00xVEV3ZHJxTkc2My9vS0UwWUtyTEZqdDB2NXEyM1FsRTkvdkhFb284MHhHNTdObnFsWTcwNTFDeEtmSHkzSGY4L2VVV1NRUEVoRy9wYTh4NUk3SnErSm1zQWovVFFxaklocWk5YjczMG5jZU1tMTExbTQ5QmRXRWtjSTRqaEpoa1VPc0pRUjg3VnFXWlV6cElaODlUNitPSVNMT0RzUlk4ZysxVWh4eGsxM1l2SzJSb3BuYnhxRDlSbTJ0a3ZnSXJvTVErRHAxTnlCNDdHYzZmZXhqcUVPVTAyYkQ4WWEySGVocFVhTTZqY2JyQXRnVC93aExhVnpQRHd0YmdaNHE2ZjlrUnVwM2lNREl2bUZiRktmYVdPMlYwZURkbVBWM0k2S09UVzIzOGoyRmVLb1lKWFRBVGFaUUo5UDl5VXVnV2wwQW5qLzQzNDI1QlBraXNrLzJnK0tPNk9kQ2o5WFdRUmIzWGM3YytxV1FsV3NDbjQ5WlJsTUJ3U1lhS0FZQy9BWVVXOExBcGh5c2hLWVNOVmhqNGJwRWR2dGN3c1BuallmbS9ielJCc3FWeG1YMHlVYnRPazZDeTdOZElVMEg1ODZtdEhOdmhLcHljQ1I3Ylk4OWtPaTREU3UvQUExSzIxYkF2NElpbkdYUkFkVExudGpyTjFvbWJsYU5OenRtWjk5Sjk3bmxQN0FzeVpUNkJFaW55Q2gzb2V5ejduRmh0M2d0TVNpMUw3NEpqZjRNcStNMVNyNmh2U0luZmhWRm5hSHZWK043cTA1WFVycFFpSU1UZlFXTElDRVZXMVJZWHZXUWZPUlBUbWt1aWl0bHlaRFFWS0ZhK3l1S2QxSVVyNXl6bXBtZnRsbFRKSHMwMEZnY04vSEU3TEU4UkRPeFpkcEs3SlYyclRqQ0Zzd241OU5UeXR3dzdaT1hoR0dvVWtSQWE5QkYvSktJS0tlVGUwRnY4bE9lSkU2ZW5RQjRlbHdTempMQmpObFREZGhlcWE4dkZlbklXZ3diSWNBbUNIVUVhWXZkUkNVK2hwMng3NkR1YzY0N0NndHRmanFzZjdmT1FleGRRSm9ub3V4MEZpOFV6SEtUZTZJTzJBcXNnc0J3R1JsejRhQVUvbEtMbnRIMHN2MWtETE5LckZkTEErQTJ1NjA4UFo4U3l2VVh6bXVXWHZIVDVjWVFheUhmVm1keHJwa05MMzBNNEF1dlNOUTV5V1djaTJ3SnlOWDBaRXpJSm5wQnZYd1NIMGZzcisvdmpiaWRuam0xNzNuWWFaSkFLUkY3cDBoQlgzSUlKeVpDSFJYZUp3SWVrZ2h4R3ZXMWI1OEw1QmdaSmRPMDBiYnlhZlI3RUxyOWZITVl1Z2xNS2lVZXU1anFseWRqNXIvWjJpNkhnWE40bDJyOWx2ZDdrOEJPVWFkRVh1SkVFazFOVU1zN2g2bm0vTnplQWhLWUhLejZoNmJybk00OXdLOWU1SndMYWlWdGFUZS8vRFNxSmx5YkVTeU1LL1pYMHcxNFpWMVRCcG1VRGlFNFo4UzZpcGQ5MTg1OURicWVhZEo0cHJwcitFbWswUGwyaXhTUTJGTTYzT0MrOXppWjVmdlN5a2hYVWx2VitHdDN4bFFkZGpWVjVSZFlFZ3B0cmY4cVJrcnk5ZVN0Z0hLdWVsUmZST1NRSjV0a0E2ejFFSmwxSVRBcU1Ma2xBN1lzMnpqNmN4S2lpcWFBN2swWHNnbjlFNjIxMFhsZU41SG9JZ1BBOHhUbVZHQkt1Q0dTNTl2K25lMjNmSytZTE56RmkrT1JqNVVjYTJndnVac3BzMjJSTnRmR0diekl0US9VSEhwSFR5c1pzUmQ2emlEQW5XTXJybGpkV0cwZTRXeHl0YjNab3J1NTZ5UEhNZEdHNEc3R0ZuWGVudUF6MjVEeDdXZFhKT1pEK29abHlhS2VqVThBRzdhYVRNajEwZHJTY2lxWU1GMjFTNFdIak1MSWtPei9hS1JST2pnL3NoSHRaeEFPQ1pnak5wUnBHNEZDWnBUdEhLTWdrYXIwSVVqdEs1ZDVUYi9UOWVFZzBCcFd4WklzTmRCdWFRNFRXQXBkWktESCt2WWc0b05Ud1Ewb21GWkltVmNkN3VURFN4UmNncFRlR1pTcEJPSXhlU2VOZzBmd0hBMUQ2U1huTXhDT3JLNkg2bm9EMXhCOWVtbjcxbDJyVWwyZ3BMcUovdDRBbmFpbm03eFhQb0h2VHNjNE9uTDkzTU04SVNXSnRvTlIvUkNWK2Qzb1diamh4dGY3T1Q1c24yZmR5bXRDUVcySDVPN2tjNlBWQU5Hc0M5cUNkVmV1ajgvVWVNZEk4NWJpUm5heUl5UXJzZ3lRQlZtbGJNYW9ZK2YxYzErZVgxS1RRUk9aZFVwc3BNZkQ4L050WmNKZzZjcmlEY0NZUzZQVFhkUHZwTEJIalFUd3lQWUFXZFFPQzZIMGhDeWlmcGF1ZmNRN1RjUDZDSzU5UGZ5SE9XV1pxODlscDlIYVZZTTNKdEdsQ21OM2pGMzFzWG5Ydk5iVlZmTGJWRndzanhvQjkvRktPRWhlNWtHK2wxRDMrQlYrdEVYUm1ncVR3LzkzeEgvcGllckVkWURFQUtrSlh1SGo4Q21yUVU2U09ZK1ltV1NIVHh6THZ5bndST0g4d2pIaWIrL1RXbTdtZVVPR29yd05NWFpvQWd3VERLMHhpZUg3ZEcwUmxuMmFPcGwrWmx3RGszZ01NQlJ1YWtscVVUS04vSW5HR1BHSWJFNm9zV0NQTUNzRVJWUTJtZ3pRMHlIOEl5c2hvQnNLL2NRa1BnYXQ3cEdjQmZXbm5GV29TekJlOXJ2ZUl0NCtmcU1rWldEZHZ6UkZxZ2hoaEJIL0RQeEhvbC94MDRSenVUTm1yMGUxcmtTbHJzeXdkbnU3cThlVmVBVDc5YVlGdWtoV05EOHQ5UTFRNXJQa0MwK0VrQjFXTnlrSGVtL29qdGw1Z3d4elV5czJuZUUzbkRmWU00cHgzMnI0M3BpRzU5QUc2RW1mQkpndllvYWZ2aFJHbkVlQ05GRFIvTzliT0IycWZ5VnltTVZzenNJSnZwcHAzR1oyOWJmKzR1TDFRaitxcUZ4VGxhNXloaDlvYVMvMFcxOFVMQkhrK2NTOGJTRlUvazZwbFg0d1NTTjlMQ2IwTFZ2Z3Nya2syQXdVQW9WK2VCcWliNnI1NThPbnNmQmNWMzNPVlMyazlnNzRHOE1uWTBzaVlESVBoeW11bzJvZkRoUmdjd1BTZFJPTVpIdGtFbXNZcDhKaTA2VUVKNjc4SkJFWXJFNHVDOEJtc0ZDdm1RV2doWjFQSm9sSGw3eU5HeTBmbHRkeFNSWUtMUjh0YTRmQTNDNUd0S0xra01VNDlFWjJGWjRXekhuUWdqWlJ6SjhVVnFFRElOZXlCcGh1bFo5OS95N0pRN3gxYnp2ZTlqdWNla3I1RjJmdkJwWmZNa0YwVXFUZTdDblg4RXJBK1dXRlRFK252VlM0YWhGc0cyb1BNUE91V0NEQW44Q2VVVGd2MUFxcUE0ZGpieWgzTFRiY2J3c05YSUQrVmVuOEpLVEQ2MmN4VmhieEdZZ1ozMi9rcDk0cUlwVUdtdHdLUFc1c0F4YXdYa1VMVlozSVBZMkp3SzhOeUVsL3pxVS8yZTByUndhNTl4TDFWRW13NHZxZFBORi8wTHdOZnd2VU54bXQ0dm5oQmk3OEJGbFZXUmdPcTRacUJwcjZRWFVCYkRZSVJ0UFk3enVaQTl1eTdMZjRKWUliS2V2WEFkZzIvM0JHTzR3dUdpR2lqTGpzNklKTUpDL3hwazd2ZjN2ZTJLbGhjeE5NSzBzdi9GaHlqK3A3L1VqcStGUjVLbThGY0VxSloydFpKMm5vek9Mc0hWZWs1MStKSjQxS3loQ29yaUYvTmszMnI1Wkp0aG94NWNVamhXTTBQWG9qRXZBcHB5VldGM056Mm52ZkcrcWFQYldoWEowL2dYcXk2eHFRaDhYNDk5VDBNbzQ5TUpQM0xyRkNqalFaMkppazZ6MzdSdStpY3pOTTdlSHRVTWkrbzVURjlNeDBUNS9LZUNQTGVZK0tRMHhRc05IdmJHWFRpM3dmU2ZhR3RTU2VrSm4xV0FoTVo4Y05NRXdUUXdlK3BrSUF1MVJSbS9selNFbTdxdjhzazhxeVQrRW9WTC93dVd4T21pQk5xZTV3MVRFSEdtK2p0Nzc5bTJJRXQ1ak9YMHNINmJSYUZZVVZkZUpjZjhrc21pS3VCMjV4Q0FlaW5XaUtlMlZUeXdKcVF1QktJeS8wT2JPVFdtTGlMcXh2VlF1dE1vOTAwKzEvR2VBOUl6cUUrbG5YVkdWR1kxYWEvM2Mrak8vZjU4Sjk0ZFk3RU50Y1dLWklYSHdybU1KYVZvbDhzZEhNeGpYRm1MdWtnREErNVJ6NTlnZmNicW9QYjJaQ1RlZkU2UStGZkVQWlkwaDVjeW1kODIzN01ycnVnVXdmUTNrNnp2NytlS1A1UVZyZmtIVmU2MkhNeTcyUEV0RXVpQ2ZLUGZiT0VBcy92ZnRJS0xwYVJlZzluOU50TTFHcFEydXNkU1dodVNlMDRXcnY2UjFEV3VWU2NkellENnFQeGFYUlIwWXJMLzlPU2tBRDRIcnVJL1JDSXVFVFBlZ0ZnU0hNUFlkclhUK1RZWjhBSWdhY0Z5NHR0dTYxWUpud202SGJVbE4zKzFOZGJTbnhoUitwZUNFY3YrRHFiSW1TM2o3RTk0TGhBeWgrU1EzV01Ndnh2bmJDbUM0dXJpcWRKWmkxNnVlbFVocEhqNVpoWi9qdHA0VW9vRnZ6UXp3R3JTeTFSc2d1MHdkcGp1T29iQ0orWENneFVjWmdDOTZVY09yQUpkcmMxMkpDekRqUXdza0plMS9XcjJvTTEyYlRzLzl3YXFsZ1dUcVlUa1I0dHgydmU2dHA4Rk5URXNVeGp5QVlpVHRvZzB0SXJXNU1OQnhFNGZPNXY3OGh6dFd1d3hLamkzU3BuQTBVWkRvYVpzQU9ocDlSNWkxSUlrUUF3UjNTVlg5Q1lVdkdCYzJqSDJ4Z2xYQ3RjM3F6cVVwbFAvRVRudFYzamlBOENhYWlJQ2h2enpma2R3MVZsNkFoYVk3Qm1TclNSQlIvMlJWb1lYSmd1czBxRXlQa3V5OXJtcWdYWGNoS05VS2cyaDQ3cU9kZm1ad3N0c0lMRlNNN2NTaVc2ak5aMXlUNUFpNVpra0dhWDZDYXJpckx6WElEcyt0aUtpMW9NS3BEYnhmTC8zWnFFR3luZDVUZDhueUZQM1dFd0wzQWFubGVzUm9nSnB0V1pZc0xRekNFS1B1djVxNDcrcjNJQWRvTmszV2EwWmpKRnd6amJjR3hsTDFpdEdweTRDRS82aHppWW9mVkZNUkdLSlNuRHdHeG1RMm41VlNRY2RHNUE5YUdWZVhGK0ROZmhMa29hUW9IQURUUE5JcFZ0WXVMNzRRUldPVDZwSFV0QTY5U200N1BUaXd4TEdTOVN6RGJoVVJWWVBHaUo0VDZGdy9pUkFPNXdlV3QrTjM3c0VGWjZQY0RhcHA5NmNLWjcyd0JxTWJVZXRMQWRGeEUwNjhOYWQxNERKUFdCTXNPYWVzOHhvK21ybXFKV0pTcU1BcUhVT21qc2JwRXgzODNwN25kK0NqcHlGNDhpamFDNmJNaFhqTGNKL0pYQllMYmhJQWpiREpwb0JtbzV6SnQxUk81Q3FBYWhVbzB2VkJxdlA0N1NZaWZJZEdjZG8ycFlaQXdqczUzL243bDNzTnY5WHVOUElZWlY1bWYvU3p0NldndkNXNlBFM1ZHVDNJYnh4ZTQrYVNLSDYxRnRqQ3RPSjBycUlJUHVpUkF0U1QyWlZoMm1GTmMxWTZ4bmo1NjV1NGlYUkw2L0ZTQzBXc3krY3VWYWJhVkNyMHVaTldybVdja1hHUFgyTEpuZWJBaGM4K25aaFdnTzZjcTVTS1dhZktUVXAyT3QxTTFHTHgxUnpLYVdUbmxhMkFiZ01kL2FVTFdXbmpKL3duOE5WdDVEMnZoK2wzQmRIMEVTNjJZWkthVmJvWlBSZ3R1QTVrV29nWWtEanJ1RUFjNElLSXpRYWt2M1l0YnF0Z2QvMVBoT0lsRGdDUEp2ZG16UkNPU1lzV2FTQ3J4bDQvZHRZZ0ErUFFPQ3BmSWpzbWptR01Uc2pCU1hvczhBT2ZlQUsrc0RrdUNRR09KOXE2M2dtb09ENjNDbEZYMXozaExyaS84dkRjRFdQaStYQ292aUk3cEFMR3dzM1dCd0FtdE1FcHZDdzg0aytTRkpPb2hGa25rUExwMm9pRnZ3Y0FkeTkvOGl3eWEvS1ZnZEcvc1A2ejFIbWc1eDB2SmRkcndhVXVtcDg2NEJhWFMxcHpvTE4vYytLUGwyRWtoeFh0ZnBYWCt2RzkzNG9TVmhBMEN0RzBDY2pvdGJEWjhuclM5RWJUdHdZRGdZQWFGUG5mbTZPQU1EUSt3NTdEeUt5UXI0RkJkMWxJV1RjYkhleitiL29WRisxNHRHSmxocjREbE9VOW5uUjdTZ3pxV1FnclhuNGdKdllEdzBRNjczOHRCY0plUk1qQ1pBOHJhUlJHWmFJSCtNKzFlcllKcFprVDY5YVJzWXltK3dWRm5zdUtPem9xcGRJaGFPR250ZlM3aTM3anNCS2E5WEtkRjVoTmpINmp2eHZWdG1EU0tKbDdrMWhHd0hQa0VTNExVV2Zic1B6K09HU1VwWWx3aGdiSmZnc0N4OEpKeGtkaUVVZzRYbmlZL290MkpxQWxGWTVoYk04RUxZalFjL0IxdVVNNlNaWlJ3cThEYUk4VkZVNlQ1YzVhNUdIYlQyZ3JSZnBZNXJ6MUlxTmVJVmxyaGh2dWxaeldaVjZQR3l5MFAzaTczMzhobWNqQkp0ODRlbEQ2UDRXL1lIUFIvOHdyMWYybkdHaS82ejBCczZwZlFwOStsZW11Q0R1c3dHNmNCZU50UGczbDNaZTl0b2tMSkE4c2R4NXZqelJrMEkzRU5lTWs4Und3YVVQRkxkWE9tSVcvak1nVVorMCtUUGV2bTZXcGM1WCthWU11QkpnVzRQcDRBbmZYZmwzN3h0TXhRWkhUeituRFJKZEx4VG16UytCclE2aUxCRTRaelh5NHlqN3l5SThVY0JNRFhkYjY4OGtoOXVORm4rRGYyWXN2OFBuclR2ekFXdno0dHBEcjJtdGIxVmlpTmNGTlFKdmoybFBCNXJQd3VQendsVVNnem1GNjVoT2t6UHk3bmlNb1p1SXorMWtJRjI2dG1nb2ZuZ1V5ek4vbVRDNUIzNXlqeThFS1MzWG9jZW9Vc0pvQTNqQWt2ZVB2T1FZc25qakdYdys4TndKTGM0QzVtZTRZa0lzRU9kbFVpd1l1WmtaN0hnU1QvVmJvMEhGTnBrL0UxZi9CRW42Mmh3NUUrZ0h0eFpjMWhZSURWSmlHRGhrdjQyenkxUStKT2tCa1g2UzBiZk5UZUR1dDkwNCtVOVl4Y2x4TzZmR09SRGxacEVZR2pQUXU1QUhQZDNEU0pNVnhyUmkwbHNtZm0zTEJuM3h1RmZZc2NjZUxIZ2R2SE1PZWRLRW9HMU9HVjRjd0JlVmJtUnk4MndxbkFmdWhYUGxYM05lZ29mYkhac0VQeHpnT1dFYThldlZFSkdGZmpaVW9LNWU1Q0dwSlJEN3NZdTk4TTFIOG4rN1huUktGSHltamdTaFU2enBkcmQwb2dYZmtyd0p1QU5uUlBWSHNjTG9yNE9jcmVvdDlGTFh3QmljTmU3dmtCQjZibmNnZmlJaHliWDA4K3ptS2xIYUtUU1FjN1Jjb0VZcXc3UG81cjEySmZmTndoRUQ3L0JKNzhVZXZtWmpwYTBiS3hETEwxanFqRk9lRE55VXlRNE5nOXRWMVdCMldKYU5lRkhaaGU3dUU1YlNsUUtxazgvaTBLSlY2d3Jpd1hWeDBsdDBYY1hSMnpXRVlQNSt2QkJIMkVic2FNMTZGWmlOelprOCszMVJCODJrZFdYQ0prbzlRa3JPUitjNjV1Uzc4NDNkL2RFVkRoYitXS29wMk5MUFBzUmJXcGZMczBCTkwxMWJnNTduaTB4bEo1blM5QTd0eUNKNlB1N0VsSkVPbXFQZkt2Q1RGeGFLK1RjV0pwZHNXRWQ3U1VDWVZSZzQxdE14RjF4MzZjcmR4bWcyUFlCenlVQkF1UHpDWDR3SjdzN3ZYMDhiV3RNdDVMMEpMQWp6Z0t6a1Y1NFBSdjNib3ZsaEZqR1VFRE81MHNrWmxtYkR3czB4UURaWWk3Wlo2YytISDEwVVQ5NVQzcHp5eTdVc0dSOXJ2VEszSlVOeU1ZMVMvZHpZcDVmRkJPR3BKUVl1TmlwWXhHL1hkNlZkaHBuMzZ6WENFZlNDb2ZJeVNuU3FDODUxRGd3MnlKOENhVGM2Sk4yWEtjTkdTcUlLcFdTc1hZNGhqdmZ1REpxQmFKdUZuVzEvQjFBZVFuVEYzbU8za0VpT09aYUU5RkRpZGozaDB3RXgvYXJ1L1MzV3dBS2Rtb096SWdMWWVqVWlocW9jL1AzWEZONnJCUkV0RlA0cXk4RGVBcWVUNVRPaitOM29Ed0pwQTNsV2dwQ0V5SHNnaW5MdEVpM3JpOVVNYTVKNCtTSktPd29DU09ISjJFRFJBNlFMR0g1RGgraWI4VDdEWGlrTUZmcHMzOHFlSThTREZVV1dsU0lJa1lnaWMwemJkU1l1Q3FxRjVTTWZJU2U2UU9wZnlTOEdYWG5sYU05cEEwMkYrV2JBMGJJYUZheVVqZDF0M25LODk1Uis0ZmZTYzUvdmF3dFREVzcxdlZxQnY3S2hWMi9qNlRSR0NMMkYrNTRWT0c5bjBIUkpNVlFoK3dwOUhmZ1FvSy9PbEFSUTlmUHlPa1ZnNWV0TE0wMWZMWk5KeGFmOGl5dGZwT294aXlSOE90RDROaFFNM2EwaUhlc1BaQXhsMEdIbzg1S3pYeUtrZ2FMOGM1SlBWRHZrWFMxVHpMVG9tTVBRRkl0a25PWGovYUxkTGIxeDAxdjRaekp1SWlMR2MyejlsWnZHczJGRmdBVDluWG9zVzVBNWptd2VIclpQaHVBUVphWTNtaC9kaDZoaU0vSFE2cHpHcE03RHdKdU5GbVRaQUFEdVpBS3ovYmpCVm8xR2IraTBkVFpYY0E3OEkrVWVkc3dvRGhIMmhXeHVqZVNkZHNYVVprR214L3dIQ1VieUcybFp4Tmp4STAyVVM3bGZ3eEUrekJaOFc3UXVBb094dkVWSm9BYlY0dllEb0trRG9yWDBHYk9zOVhxZUNIYXhIclN6SGQzeEVlbEtDZ1FDNkZoWjIxR0dQZ2tWUEtxd2pnYjNmNHFUN3ZxejFiRFNIbDY2dUgrQVBaT3lxRTUxenhtSzR3dGVaTWQ4Rk1jZTZ2eUowYTZmeWg5LzBLQW1iNHMyZitkVWVmQ3VKbnhwZ0ZML1RnY2tkT0FxaStGeUNGS3BmU2lJalQ4MFVpdGI0ejlKdXpaQnFMYlRtd05XZUcrelFzelA1S1FPTldhN1NtQjhkV3pobEl5bVp5VGFEdUpta0k5MTFFUmF1TnMzdHdvYkFrd0p3WkJ1Mys3SFV1MzUyNzBGeDZ1cU15clNITVV6SmNNcEl5Nm5oMmszb3RHNEN2RVEyeVNyYXN4RGExS3RqZWUweEc1bUE5bnBzRlJXajR3dTNMRFE5RkYrR3RxSWZVVUFaNGdRVDZRTm82cG00Wi93dit1UGZ3VlUrVk1zYytSYWZabkpNdkpTeWpGQjFRUXFzSFRWT3BYdkRvYlZkbVZSenFGOVZENGhWL3cwcWIvZFh4ZUkwWDVaNE1WTUNqRlp2am53VnNyOGl4SWFiaEIwZVp4RUk2QTdST09zVXZYMUNUeHc4Q1ZVVXJzN2QwOURGb1J6aldhT2VOZU9FVC9PeC9yKzdqZzFJTVVSRWlsT2hyQVZYMkhqWWUyVlp0YVZGaDdUM3VlMVRLQmRVdFFKMm1wZlZEdHg2UGcvSGpuL3J0Q0NsbzRhNHVJSXhJYjBFWDdqOXB2QWhydU96MWJ3Z0Foem1pV3pWTWViVUhaamU5c0QzMFg2Qk14RUhvbG5qdnA2eFBJSS9QaDVEekxydmYrOFJ1M3JPa3NpdDBoeGY4UzBhVzVxc3I3U3pMd2FiR25YTWpEc2JCeVJ1REgxd1NoQXVOT1pTUk1RM2NVS1FOemExT2h1WEMxUE9QR1RjaWs0NTBGTmZ6STloY042R3RzTERSQWFoVGFvK3JLRm1XUERQcDdvM0d6SUhGNVU3NHhzMGJoYThWUjhBVXR5Mkozdm10dkdHVUoyYTIrSWgreUpoRzFYaHJHd2RaaXRPMFNJVWcxL2krRTBvR2dXdG9lUUJIL2FHeXdQY0pBZWkvY0R0TkZST3hyMlNzM3dXUVIxZUlQV1BjTXFlMDBobEM2bEFtWjZnZldQVmpQSzVLdXAxVElCV1hWeHpQYXBwZmdDSlQrdWdiN283SUVCRy92ajFtb1JpSUdmYytRaURRMElab21zZHExdFVIYXg5eVIvV3Z6YlNhQlhVendNRSt5N0Jpdk5KTFE2NWZlWlNwMXFyY2Y0N01JSlUvN2RCY0Zib3FWQUpYZjNBNHJ1enA2aWFoV3BHd3g4MWhpUm9rN0s4c3UwK2hFNnBWaTJ3TGo2d09SNDdoM2FObENpUm9IcmZlUmltbHVQOWhNNktkWGdkUStqVmF4anlRK2VwN0c5b3hNSG5VcVltV3E5a2YzVDQ0SnN1aVNReTQxYUZGSVYzVUptMDZOTTZsTFoxdDhMRWtINU5KK1pUS2JVa0lrZG9TNk8yWVB0Y2N4YXZoelFrS1B6N3VGcFdwd3hFMTJPMmcrUkpZcnJXTWZTU1hrOHpFZjBEMmlDNFJvRWh1cmY1TURPd2ZndlJwZUFtYk5rZUZQR0M4SmVPRng4bkx1U0VqaTRYWGcvMFFrdHV1Q21IT0pNZVh6RTlrejQraWxGVkl4bWZWbm9GZWpOV1JZcnBPYThWZHhBWjFKanBpRHhCWnBVODdDeXUxNzNJWURUTFQranhSUHpTY2l2STltVVIrbmV5TUNhcWhEd3c0QlJtU2N1M2dFUVd1TXQ4eVBhbFV4ZVJwQ2k3N2pBaUJ4L2ZNVjFYdUxUMlBaSHhJbDkxMUdQV29QeHhIVFVHTXRVN2NyamlwZTJrMHR2bWpHUm96L1UrVDV0N293L1h5OVE0dEg5U2Q2ajhkVDZ2OThWVmVsaEhkejNZdGtHVjk3bE1YOEtPSzF2Mk92NSszak16RmZqTWRyaXdLbTFWbWdTYmhLUkl5anBjOUlUZXdMWG01bkk3dXVrOExneUF4SEZScGxHOGN4NDFrRUh3c1BqTHlhUXRlcjVGOTBEbzF5Mi9wblRWMGxNd3V2eFphV0tTdFpCNEYvWUNZYTljS1dQdHdqNERqQ2J4eExkeFplWjZ2SUx4WGZhSjV1WmhBMndUdjQzVUpKQUU3eG1teEM0dzhSUkNSb3UxaFVaeHJsUHVFYk4zS2JMbEZvZW1ZU0hFTmhySUhyeFhZRjIzcjcvOWxhcDNCMndNeFdadTM3cEFDMTRSYWo1aEYzN3NOVjF3M25MLzRPNURJUXlHRDJUN2ZROGZmN2dDTmlYY2ZySm11V2Z2aGhCVXNmbFU1UXBIaXR5WGE1c24zK3l0aGJGbGxuYjZSYWw4V3RSSDFFSEpSbGpjTHdDazl3ZEpFVnBxM0VLVDNhNDFyR1phNHE2ZTV4MFRMNW96V3lFRU9QTmpWL3RieFh3ak5Rb1B4MmxqRFRTNXlORktqVUZycW8zQVJqd3dzaUdzcEpVWmIzTnh4dlkyODVwQzhMaGpDUWRTeElYV1RRdmRVU256NENUNllWRkdFVFNLWC9VYVA1S0RQczVLR3lIcnpsb0dhY1dJd0FzYU5jMGhUaGR2WWx1OWd6QjY4TENIUU82b3h0S3JrYW5ZOUVoK1RKbk5ybDJBVnVWUEVlczAzNnpXN2EvUGFUcU5zdy9IakY0THdtS0dSMTFYR3dReEs2cU1aVmJpTDgvVTd5RVFYbXZCMjc5aENERmJZdkM0ZGRlTjQxd1dTL0xIb1Qwai9NRjNFR01VN0pCeE4xajZLMS9wL3JtU3c2VjloTDBOeG56MCtmaFlXQlhyVHR5QjhSMGZEMU4wTlBjU3lFNGpaVjk0NS9YUVZSTGQyOHR1Vms2eEUxSG0xd1pVUHkzWUtZOWtRRDJKdTdId0lZK0NpYWkrdTJaRzNiWFZ4WWw0Vnk0K1VYUFdaVE1XTFpGK3NRYXUrem9WaUJIM0xuVFJoK2dGUUxBNUdObmhrYnNxbU03OGNKeXQyTWdaS3JsUXVOMkRMeXFuT3FVU1JnMnJIOUFDMG5OR3FLWDhERHc2b1g4UWd1a2orSVdBMk84dytBeWxQNmNjbjkzWU4zeVczUWJxSTZGbmZzVjBOME43ZXJlaGRaU2xNdnZDVkY0LzlObGJCaG9xMzc4bjdINEtaS1ZWTkdQZWY3SDhHcEFCa2VXbmFCNmVrYTBjUUZvK1JTZ01yKytYQWhkdzJWZnZQZlA2a3BDSlpReGZYRW4xS2dMK0dEbEkxK2xxTGQ5bGVsaHU2cXQ5NHhzUXRDQmw3UkFkdGVrM2hHbjNHWUtkc3lWVXRreDVFaFJGWFYwQ2E3VlBTU0pVWEhlUG84Z1BjMFVTaC9RZmJHS3BCY29aTmRLMGNaN2s5bjU4Ukg5KzJTWUZVMWRrVmx0eUVxLzJ1T21zZHFRdEMrVE03UWxFcW00KzhzSWMxdkExamppdkpBNlVFMUF4TnlGVWVDa3hYOWZvQlhHWDJEYktQUWdIaXZCVXFJTWRSSUh1dUpwVlFSTlpnY01BM1pLaHFlNnQ3NWxvZHM5YTJRc2U1SFVrSmNmWTJvSE9xYnlkeTg4MHdERFlncXNlRWpHVVBHRXlyQzF6K1pxMTl1ZnhsSFlLRnZZRjZSMHlKeFovUjZ3U3JIYyt5bUlxMzZQUkFYNnVNbEZQZDVxM3lReEhVVlBnNFJMSFN4anR5RG96UlM3bXRUWkRMN1pjampvdGhHUVJ0bUZkbjFUamMwV0JjN0o0ZUFQbDN4cVZ5RG9zUTR1bGx3M050bWhPVHV1d044RjBpbnl0dFpvMEx5Um9tRzhEZU1WR2hlQlV4dVZKYUhBS1ZwKzc0eXg1SnBZT09kaENMb3Nac0JaRzVhS2E4VEwwNE1GRDF6ZEhPak9zMXlpZU5HWkpXV1RWcXRtMmpSTTFXeGZpbWdEejRTcHRMQ1hxemxhWityRHBZeWtPTmZhbTdNMi84S0U4c3NxWEkrWklZNy9QZGQ4WHVhcnhOMEExaTRrZWRLZnhUSy9MMjRXOWNBd3pIWWxUS2ZJWVlleFRUNmFyUkFtQzF1Y1lDWWtxczkrb3cxWmNKYzhMU1VBQXJiMEFpYjZRRWdyWWlSbE5zR0lTMXBpbzhwdnhaTVZoZDRQSy9wOWpjY3pIUVpQWUpzTkdrclVwTWdybWZvcjRZaTE0OEd4OE40SkptRTE2K0k4ZWxHU0lFQ3FjckRMZmNGWlUvWlY1VDRyU0FHQ0lKd3VDK29iVDBBbUpUaW5yd2p3Y1ZDK2NpaEhpYzMvc1ZLZFY0SUY2ZUNDTEsycG9ZNWJDLzYyR2IyTlpDbDNnQ1ZnUUN6MlNPL2x0ZCs3aUFGV2VlL216dmI0R1luRnFrTkNrOFlMOHc4L2Rqck1nMkZab3llYWwzb25lK1M4MWUwY0VnQmZlV3dIY28yMHFhSE1RYmNPS3JEbTI5TGZNVGJvSDUzUDBhRW03aEhpazR5bmJ5UEhIdys1WnJkMGEyZEtvSUhKZmkrbis4eFErblNxNGJRb2krUjZ0b2pHWlFqY29GVXhTeFFPendRVEpIM1dOZWZ2aTJwdEdOajYwUHFaS3BGMHhRQ0I3VUhFeVFrb0lYM2VZbXRUc09uRUVPQWRONmYrdEszMDg4TVZ6TGFkdkJTbFZtdFVENzJKbnY1VWNVR0dMQThQaDBtMlo4T1NEdjU0RUEvMUpqbGorZnBacVl2ZnI4Wms3RUE2alMrR0R0bDN4MHZBNzBJQTAva2hib0ZLUlNWMUo1ZllmMTV2NGFEV1VjWTN0VHRudkF1dDJQbmJUWVU3azFvazYwU3hoaC9pMzdJUnR2aEd0bzdsQ09aeDM4eHc4N2FCOEJ5b1hlcEV1dEdlNkcxcDhvdHZPZFZ2WGZibTdacG5QcUFxbXIrS3UrRW5mSW9vaGtMOGdGQmlIcmxUOGZvMGo4M1gyOU9TSmV2bnBYNXFXQjEydDhWbmFNaHJzRVIzZTJLaytXelU2SVYrcGxka0c0RW1obEdzY1d2QW9FQXYyZStUeHEvRm5tUzZ6UmIxMndrUXBMblRhZlhsM0d4YnQyS0xFRzdmUGRrOExxalNYS09nZU84TmxpenhIdzdsSXg4ZXJDd0dYbGhoS1BPUVIreHpyM09tRVQwNFo4TWdUT0xHbHN3QVRCYkp6WmZQZTM5TXRLQUFnaVFUbzNmMUZHd3RURzlTc0loVlIycnBsK0dxZWVCWHBJbUR1V0pSZGFUU1A1aXdQekIzSXFwN2tVbWtTWjNvOXA0TkpzN2pUako5Q3lSYkNSL2NSTXdVd1RMbFNvcUNGVzVLc05oS2tzVzhGQ3ZhZTNadXZDMmtES09uL3FNMnBqVmFBVWRBVXljT0sxWnlXdTBEdERNa0pBKzJRZ1hwckNXRFpGOWZqRThGRU02T25SODJrZXRBUHdmWkNvakNpQWk4SUFPSmZYTnl4STlWc1NGZERySmVzcDVWTG5hN1hJS05FQ2tqaCtNMkl1VGZRdWUzTmgveUFCNTJwVllHVkcxaU1UdGE0eFVzVXBJMFNnYmtDVHJ2Ykhjd3U4bWVmekRFWXJaaEZJelhna0dnYUprM0NRYTFncjNmOW9Fa2lHVnM1THdRODNhK0d0TUFzNUhVSm9QdDdNQmd3RmFiUDdaOHRZRlcvU25Eb3F3b3pRNWYxdStoN1JNbzBpNXppT2hmYmg4YlpZWVc5cm9Wc1ZQakxNbFZoekQrUjl1VDh1M1hFZUhHUGpCai9Lejk1eEh0b0Z1WDcvWU5VeDNrVElLM2htVkgwcUJaT2pZbWpaKzhadmFmQnBGUTBkdnJqOVdOa3p0YUo5QVNoMWJjM0w3Q2xWTEs3R0RPSDJlaGNMZ3pDaE5sRTZNeUJZZStCbXJPY2hBTEY2RVVlTGlsM1dRUUsrdVZ1elJWdEZ5RnhZNlBDTGxYdHBqVXNUU1Awa3pmQ0VjN09raGxrdWFkOGlkMnB3OTNURjdvMHl3aTZ0RGtQUlhKVS96TWhHWXdhVkFYNHdQMUVkbHdCRHM5S1ZsVnVmWHZkSkIxYk94akdGL0RTQktjd3pxUTdVSkZadVFYUFZEOVMwelg2eUF2ZDNxbnNvNDFUMmUwU0tuVEd2RzlJeVlyckN2T3huTXN2U0NSemN4TEM1TGRlakpwK2Vja0I3aG9ldzZ0SXJqUERLZ205bkM3ZitBSVc4OEFWVkcrSVlEUXJIbkhBeWZDRVBLVllMa1F6L05QY0NJK0dYQ2IvUmtnMkppU2V3QzlCcklGclgzdFlOWTl5Szlzai9HTnV2OUN2dUh6TXJDWWNrdjY5MXhKMzV6Ri91bEl5VGJmSFhsTWZWb05tNjJZa1JYNTFkOGg0dFBYRWFZV2JBVEJJTWxtcFJDSDhFSW1Ca1ZkNUlmNDhiNVM4V2pDT0RXcjdaS3d2RXJOaGI1YU8zOHViMUZkVEFHNFFYZE1lNURkUXZoVnc1NGwyak1zbEtsbEk3bFBGT05lY24xV1FZT3BsQkx3SkI2dGN5NENMaEhCTDBicGhLM3RCZWFDSFZNeWNyeDViUjAycEJWdlRlU3RFMkxCWWxRRzIxcUU1a05nK1RyQ2U4OGhpVTY3dWZFY1E0enpraWRPN2gvZnRYQTlsWm5QcC8vTldoUkdaS0VnTU0xa1BwY3Y5UktYZnBJcGF5RjV5bmUyNVV1MHZYd3oxZ05zcjZOVUFNNVRIL01oT3hJRmtEa1QzUGpIUGpVeDRHMnVkOEVCOENFczJSSEY5R3BvT0pMZjBzSXJVdks3cithQlNBUndtekl3cVhRUmNVM2VVbHkrRDlOczZIQVkvb0tEUW5ZeVdzTi9URzg0U1M0UXJYQWt4bzRYQUNFVksvQ1NpRkVZeUE5Z0d0NDAyUFN4RVhiNXVCVUo5cllpT2tRMmNtWWxOWUpGYjRrZGxhU0dHeHhNYWFTZm9KelFTa2VqcmpLV2V4MGJhVVhsS3NzNzBzK0l5a0MxV3Z6KzNTNEhUQUVWazUzMW9QazFuamxHWEp0QUJWOTB2SVFjSDErVU5CZmhXY2dIUmhCR21vSVRJR0hYRlcvMTNPaTlVZkhyZnpwVzNxUzJZRStuVFlrOXU3MWFKTnNTb1NNcy9KZDBweThDb283MUN1Y1dUNnd3N2Q2SUhiS0l6RnE2K01mek01YWJIYWlQY2NXTDk3RmJjRzFWWmJsa2xValI2am5DZFJMMHkwTEZ1TWoydWdkOXFmeDdNV1NwSlRuWlZHVm5JakZla1lhMmFjd3Q0QU1MMnhCdU9ncXh5OTZ6bjN5WmxsNnZ3RUtRcXVwTXQyNXY4WURKc2l3bkRvQm9sWEpyb0MwY01oYVdnaFhqdlBGNytRdWxpdHpKN1BFTEFNMDgvQkdZOHBEY1llWkdTZVZTNVNicUhqVTErVTkzb1BLcW52aUJkWUZjSVR3b1F3RmlTNkVTYm81MVBXMVhhcmJzdnRUWUs3RjdRT0R0RndGZW92QWhqTVZjN3BKMnVGNmV4aHRLakVqcXMvWmYzZnlLT0VUT1V0MWZhelluQnZPdWc4Q3RtQ0xUUXpyNVNLZmhZQzdwRTVTNTNyY1I1WVdtaWVkeVYwdmRGVEdpcDE3aU5DUDVtSEZoNkwwYVJlRGszdi93cnhObUpqQ2g2K2J6T0NXZWYxRk9NM0FwQlFtNDRjRi9nVDNnUDUydjdRZEp4U2RYUG9Sa1k3YU1pZDduelVuL3ppblk2LzNocEdzaWx2eGpDcnE1MzUzZVF6YUMrSHQxRDBHNWhYdElSZlFEOEhTYjZLWDVZdGVhdVlhUEtxYVo5b3JnbTNlN3l5Vmo0a3VhK3UyRGdwenhwVEEwZGs2cHFsMDZhVlpERWtQUlluTnNVc1V0OXRya1RDZkdWcWFGbU1IMXU1M21rYTdUTC9EdWg0dEZzWmlaSU1jbE9OQVJ0YkN6cG5URkovUGxIZmhIcExzOCtlaHR1WGJKcmN6QStjcUpFMENwSTR2NGtNa3FJclB5UFJvZmVBVE1uTE9NQnQxL2Q2am5nclJTb0NSWGFWQngwb25YckJCNC9WejY3S0ZtaDNWelhNT216NzJmSHF4TnZ3Uy82Y09KUnJtdnVmdnZFUFpzR3BBc01hQ2dCM21aSnJKTGh2bWxYSTI5S3lGUUVMbjlpYVBNY3AyMWhUeHVXRkthN0FmcWZ1anFpU0ZxQ0UxakF3eGdqQVU0ZVIyMi9uNVV2Zk1INC96WVpIWFlzdXc0UGpLdm9vSXpPRGxvQ09jTVIxSHFVVG9yZHVMVUZRdS9yYjgvTkdPY1p6d1R2QnV2S2xNaUtFamFqTnp5VStNbGpTbmlON21WUDVaVTQ3QXk0bGJEbGR4Vm42QUdsMmZHaThzeTliZmZvNG01bzY1eFY2YWw2a3dvUitQdWFwYVl4K2FldXNYNjJoUDNTaWNaUXdPYzJSb2x5eHpkSG43Kyt1VXJ2YnQ1a1c3M09yMkZDNWIvdWo5NVQrbWc1V25MZXFRVkppMmt4OWl1UkMwTzdaMlRQUkpNbkFLcEVLSDdmSTkwNlNpNjNZMU5Bckh1OWxiMmtiQVhSMUphY1VqL3AzQnE0K0JJSGNlWjhnY3M5SXVBOE1nTmkvaFFsS0lWeUhna0NBeGw4K0tqL3F1SGpUTkp5OEpVZ1hBTFU5VkUyQmFaWmU5SkdpY3JRSjYxd2VtYjZqMURKSzYxRm0rZXkra0JqTWU3VXZhbm9sZjFYdmlKcTh3MVFCWTZkTkZPSnJ4RS9EYnNGbjhxb1FjNlYvYm4zSi94SGxrcDIzcXNaMTQ3emIyTUlUTWZ3UjhTclEzOUhLVjdjUFJ0UHdZWTRXMEhHUjhhdWlHUlJEek9QZDRvOFZyTHFGUVpJMjJ0bmp2R2k1eklhU2NvNXUyL09lSFhYRnMrSVAwL245R3BMYUpVL1VBc0Nhb1RRdkVwVzQ0RGM4aGhWQnFaZjZmbWdrcjYybnVFRjRLTVVqenJraU5nRXJmc1lQZHViUVZ3Z1lvZVJ2bWw1WHNlTzU3VzBmRlBOWXVRSkY3YXVTaDBqMXZpR1Z0TEJnRVFYZzR2OW5NTmxBN2J6N0ROdlZXNlN3TEl3OERuUFRxWlYrUllJQjEyYkE4Q1ZpT3YwN3Bya3J0RnlRdGNDK1FTV1Y5RWgyQ2NPZ0FTa1RBWjF0MWI0b3RBK3lKMjQxTDV6bmh1TjNiOWpJTC9TRE5QZFdNb3llV2dyZ09JMzQzSmhkRktMWUt0RTRlNlUyaHJGd3g5eEZwbEl6bDRxNEI1RHFocnBwc0JQSnNCTGt6WlRkZ3MwNlpIQVJVZUZKWXZUN256Q2xPS1Y5R283OFJJVkV3b0piV2lJeGRYSXEzZktJQnRZSGxZMVJXd3NlOXBtYzQ2THVjSDlWRDE2b1lpcy9QSXNOUndQWEpYYUlPanpzYlltL0tCS3ZmQmk1R1JqRkxjSXliWFc3aDdBYmxQUVdQWm9tT0YzRTAvQ283emV6cDFLOXRadGVKUURJbUM2UTZlQlZLa0hURWpGWmd6REhUMWhHbExsUmwrbFlzUDJnVDBtOWZzYml3VlkyQzZQOHhwNHlzR25GYmhLa1BFd2FXNUUrTUI0MFRDY29ra2RUL0JxemtVaTVoUUdKVHpXcjg2UDltTkVhNkE0NS8yS2tndEV2aDJaQkI2MVhPR3RFeS9EU244Wng3YitSZ2FOS3orQlRaaTRPRVloNUFEd0YwVW5mdTJRcEpwQzYzUGxIS3lpcWlsTGhTM1d2aS9ENjNxVTZEeEJ5YXIvQmdXS2daWE9qQTI2aHljMERTcit4aFFnb0lkSkRKaWFSS2NDaFVFTVNqUTNPV1ZXb0k0QUU4NWFHQnlPOVNtcUZxbTJWYU9GeElXM0o5VEcvVTNVNExzNEV2M2dyWkRVaXlMVXpacjhCQkUzbW4xZGJYYVAyYXlIb0NwU0k0S09BVVd1YVF5Z1FOcS9hTmVZVlNzM2dnb2IvUXlQYVg3aWJKZy9rYUJXS2hPbStEMFBMaGFoaW13L2dKSmFLcDlMS0pqQjJGQ3d6MW1KUHVYdS9VaW9obHcrRUk4TG9HYXRGemxFQmxDNjN4dStPZXBrbXJMNGk5OXlTTkVBZ1VuY29NMm0zSUF3cTViWWhLdjZMajNVMjBQcm9yalFYU3lVcXBVYnFzNWpXY29Lc2t2dTdyQVJXZ2hlVzZBM09MSTZIb3ZoNHpOd2ZaQmh3b0ZISHhsT3ZMN2xXVmdOaHhlV3F5ekpRREl5ZGhkdHUxcXR6Q01SZG9GY3QrQmNvZ1o5T3pwL1FEUVRHNFZVUWlZNGRuQllyT3FIOWZyVGk3STBiK3AvZ1VGMjNGR1NodUlheDZMRVFhNnlVOTlsV0d4ekRUeE95b3lnaUtjUEpobXVCTmZJKzdKeGoxYWlZaEpuOCtaNGRzbEJtTzU3akk4V3F0Ti9aVlQzYVBIb3RMSFVaZFBkQysrSWFNUWxEUzdsRXRaektlOVJqOE5meCtlS25wWDFMZjVXTkNZWjRwTGU1K3FDZG04eC9KSldMNzVNbGxUdmJIMkF6eWdmcmdmSEtmMWhCZ3F6Wm1EeU1EWU1oVFNvQU5uU2ZwQ0pqeHo3SDBqZjBjL0haRmMyM3Y3SVl6VUg4Q1Q3NGNuL0ZPd20zQlB5V2dCbWp4SmNEYnlycVRDYVc2U3UvN2N0RmszY2NOZ1hDUS9CQkw3Wm9oYlFHSUV3QjRYM2xNc2ltL2dCbHNzK3RkcG1ZamthdG83VUx0VUhxUU5NNWNpMDloa0gzVU8ra0VwMDRiL3VCd1NwOEoxa3k3WllBbjA4ZEdwMHd6ZGNEMmxHVHQweE9icEpKOGRRbXJZeWJRaEZSRkNJVmFlZ2RWUmIwdzd5c3A0UUZMdWI4YlVsWHpwQ3BoM0VobWViMUg4Z0o5QmZweWpPL1lVUFRUb01SQ2RTeCs3Q3BwZWlpZzhWN3gxeGY0V3BwYWRGRTZHU1NkQVFLMkRxZkNtVlFwa1JPMGIydXlGZlpBcjdOaDdHRWY1b3RMcE9BbmJ5em1aUDNKSTFPMHdnSmRxcDZ3NHIvUTlySmNnOXMwUlorVmF0aEYrYTJvQVQxcitkSEJIRVRucGVOMnZob3ZlQWQ1c3dQNTd0T0ZtT3dzWlVDSEU4UHlWRU53MzVWb3I2VWpCdnZmVnk0SEprU3E3dU41aWJzcmZibWltQk56eXQvSUtBdVpFQnd1aHZXdm01SVRORzZ4UTRRQzhScnlNUlp0L2VWQWhUVUMwemZUVkZqbUdySTFXdkNibzROdy9icGQ4QUV2a29YeElwWUdYcFRIbFdSRXIwMkkyVU9JVUF3clU4aGdrcVhLTjdqQlVTUUtyTG5kUElqZkdDclN4S2RXTy90Y1JiRm1KdGt1NkREd2tZYTNjZk96SXNicG10NStXSGJudkJ2Y3lyMWY2RHBTMVQ1Vlg3Rll4L1g1RzJnM0NKWW04NzdaVG1SdEpmVi93MVVqNzJSSXBsMWpoU2FJU0NxVG1BS0pKWnhlUjdXTUk3Wm1zQUhYUDNzLzNuMmN5UTlMSDRGVjJ4NTFpVzluRkxsQUxrOGFKVG4zTWZMY1ozeFVKYU95YVdGVlFCdnc2K0hUclM5bEVYTGNYSlNheUlQUGdlZTVIVDQ0V0hRb21uVjNielVLYW5ubE9nQW9IU01BYnRCQjF3UmV4L29jMzJtMHlFaDVOdTlaTmQrWmpKREFEWHlrdGdFcEpsYk05Ni9RSmdPYWRzaE9NVWdmNEN3NXdraG1wTmgycmZoTnpvNFlnVExyTldXcXNoTE9ZOFZqTS81VTlaeXcxZm5WNWdoZ1lKdFBaWk9LNFJ1Z3VUK05kekpyd1B4RTdpeGphZEdsRWYvUVUvVFpvRXZXSXE1T01Ndnp0dnpIQ1duZmFDMit4c3FMM3JKQ0g1VzB3ZkEyQjREaWpxbGJUTGNSdW16VTlrOXRRS1AwNkF1dm5yakJFamdTQVhheXhKbE1pU1o4QmRwYk95L1lhYStwM1BBNGtIZFR4cUh3S0JVNXJnNkRhcDVXelVmVWgxeU9jZU1PQUl5QlN1QnVPVTNSU1phaWZZajJoNVNSd0ZaZTFrQlhOQlZLaTFncTJSTytZZWVuSXhtSmcrSklzL3VqWElFaE1nTktxbVNjaGlVbnhCbHZsdmliK2tNM1BBNWVIWktCL29lcFhmTHNBSXlSV3krN2pBdlBKRmhEcEJPM2tlSTFoK1lROVJJcUZNdXBGT01GcGVaelUyMzVTZlM4Ym12NGVxZy90SXV6QVFYTnE5b29VZWRaTGxSakJKc2dDaVZieHBvSWRBZytDM0tEK0YrK0JieXh5VEpHRXJDYWtncXlmYUJlUnVIaUlzVzFoTUJ6U3YrMmo2M2dtY1dWeHhpWEtXZXNiMWRtbzh3MTlvOUFuMFpHdlJMakhHcDVBUzlyTzI0QXFwRDhGeVA2ejVlNjdOOHhONjE3MTVaK0RnQ2E0c0xoSFRmZUJoSFdwalRhS3JROGdrakdPOHJ1SDV4TU03TThKUEJRcDlMQ08wVWh4YllpNkRSQVpITXdNNlg5eWNBa005bUdoVWdRN1c0L2w0S1NFTmtEZm50K2l1T2FJK0ZXdXFIMmQySkVRVUpZNnBrZkZibjJERnM0aTVhdEFXU1h4a2gyZXJhbzBsekRCN1Bpejd1TXhGZUxGeWRhb0RyeXd1Mjlla2x5bytCa1J0aXhXS2RYY1BtOFpEUDV0dk5IblpoWmdWYmo5Z3krRkNtQVRTL2NRSy9MZmN4S1NVTU5pL0FWN3lxQWJLVDdqZGF5YVdBTDh5OGRMam8zQU1HSTlDUGxVM3dSSVA0UnlnYlB6cnI3V1hDVHNlZXordEtkM2d4RVVnWWNIUWhHVVMyT0pyeFRqTCtGbnprNTd3TWhtQlBTc1JERFBHK1RIYmVNSXRZWDdyZjhZRHQxU2wySzh1QlJwQ0tpbksxQ0FJbmhLMkE1d0tWamdjT1hRR3pscWFQUnFGK1VPSW5DaThPcnZWTUlHUGcwanNRN3hnZkF4WFVCQm9IMlV2U25ET0Vya1IyYlI0Nno1WDlvc2o2WGUzZkkrUGFSZ1lsY1YvUVh5SVE3UFpSWWFKRkIzWDhYTHBYMVFIMXZPM1BoLzg3MnBmUUZEQ2RTdFV1VjFKcS90bjQ1VlBHZkcwaHN6WGlHOGxrQjB4amJ1OVdtd1hsQnBWdWJyZSt0eVFxVGZQWit2S3hVMXVYUVo1bVozZnZyREJ4ZHZPK2RDckpiNW9WZktsQktSU1I2UUdEbGh3d0t3TkRpL3kzZW05ME1RWS9kaS9ZOGNXM0RaYkI2Si80WmJuQlhsdERGeHJleFp5ZnlXUi85bFpUeEZodEloeTdhU0V4QzgrL2RnR2VzVE1UUkpzNTArMFBNVFUyOGoxL2hvaTBDOVFGNjdKNEpRazN2VDBVZEN4R0VWM2l2cEZHaUEreVlyZHlQSXFuRjJHc1lsVmJsV2Z6d0o2a0UzNkkvcEtTWWY4K0lEZ1VtMkZzajF4RElSMVdaTWhIcmNTc2ZMWkhCeHAwbndmMG00c2ZTanFHelVxR1BSVmZPaEw3Y1pLMkFDSzBISmV2VUpZVmNNTzlqUTgrSFBwdFlrQ1JKQ0ZHQjcwbWMxd3NtNFRIaHZGYmh5Rm5LZzJzWUxpZG5jVW55QWREMjBza0wxampsSzBJK1NlVXVqS2FEWFU4cjdyMm8vbjNlaGJ5dWJLcDJFTW1TSENKYTdyWS9oZlZUbmRCb2xmTnZzWHl6WnBybmQ0N1NrQU5ZZmMvdzZvOXBxRW8wZU5VYk5kZjZiYlY1aWd1dkUrdytYS3BucXp0Qm9jU2g2eTYxQWNmL0oxaVVCWUgyNGNtYUJrc0xZSWszL2IvTEZ2blFaTzllSzYyemJSR0NaU3Zxc3I5dkgzS2taN1p2MUR3VDlReDUrZUppZ2s4RDJCOVc2b1FDY2hYSFEvbmVIT0czSlJ2WlcvMTRTcVk3TEl0R2tFZmxoL0tZa09NOXJ6QW9OOUNnV1RoY3N4aEhvaGMxK1ZuRXZraXVobXZrM3JaT2h2S2RtTXEyY3BEZmFjVFRwYTRRQkhuRGZEOXZmMjR6N1dvaXlKM0tpVEZkcTBUQnljcEJSeFozS0JLWHlWeGwzRVk2SktrbDdDVDJQb2JyRjVJazhWcWlxa2RZUU1Nb1l2WWUrWW45Wk1PU0Q3L0tJOXdPSWNTeHRramU2SDZqc3NGTC84MlZZWmVTc21sdDluR0cvWWQ3MUR3dHRlL2F6Qjk2cFc3ZHREZ1loTmZLalR5ZWpra01TSnVlL21BU3V1a0lidm1hRGxVRWZSZ3prdTQ4blErZHdZZ09TVyttY1Z2NGdISVRPalA5SGE0T3dDb2RuRFlUT0lad1lkNjh1NzlsSTFsSkwxeEdmZWRyU0ZzRUllYzNPZXdrYkxiSFpaRDdsVUNObDg1cnY2a3JiaWlGTDdiVnNWWTlQN3V1bFE2NlJqQWwrT0Qzam5JWG40MXNRQ0RDTmU2THBwSFlnTTNMNS9UNkFudldzVUY4VFZxKy9UNHhxN3NQOWNKQlovM3E4WDhUL1dENkNZSlNYanFqVjFLRStUVytOdzVaZXNQblZzTGY4b2pDQVZKMlVBdlV0U1I3UzNVZHd1T3lEUTQvaGlwNFJvb0k1eFpsQVRTM3d0Qjk1UzhPWUR4R3RBMHdCR3V6UG1XV0w2UWNXd3V3MWFZR2grS3ZQcEFXTFYvaWp3TnQyQjlzYkNMdngrb1hFN3RaM1VjcWxDcDJibFJJdmI1VzhBOUpRTW5iMFYwWWpjSlJ4a09ic25OUmpEcUZHbFlmWHlXTlc3M2xLR2JjL1FnelBiTlpsU3E5U3g4LzJWNVIyc1k0dGsveDVTaE1WZGh3Zm11K2UrWERiRlZqRzhGZUtMbUpvZGJweE1LSlVlWjc3cXR1SmF6MC9ncGY4UlZ5cGxEdlVlZ3hrZys3UDIvN3RlOE5QcW5lTS9iQlN5VXhvWEJhdU9oWExpYUNPV3hiNzlCWWxnbHRicGZhd3hRMUNsSndsa0E4MDNXa1pvRWhCZS9EMEpwL0tuQ1NaMWlQekowekkwZEJRU0U3YkpLMnVQblk5YXRWUEh5cXJWRlVsWS9mQy96WVNqMVArZjMvYi9jTkY5TXlUZURyQXc0WnJjYmNZRjhxY0pJeU9SMElGWDRoamZVUGpxT2NTUjVMRTNCeXVmVmlpMmlBanM1dXp0SUNnQlV3Y0NSbURnRUlRZVJUUm83dG9kcnJqNVpSYnEyeXI1QkhuSjFUZm5BM1JkaGtRWGtzNDhXTlZUSzdMNVJYWDVVaExmSkdlcDNGRUtRUlcrQVpRaTRoTHdtR2NCTENrV0tkZFFCZ2gyOW16Q0FHeUVpQjdSSVVqWTZsVmRFTC9ueU1nR0FITlBrS29TT2lGMEtNemI0Y3VySXdNRnJvZ1U2NmJLcHNxYWpsTFp5d2VmQ2x0N0c4aDY0ZVN3R1p6RWNxN3dUemdsbGRYN0ZySmdwQXI5WC9IMVNFMU5nQjVvQUdRTWtseGVsQ1VkcnFvQmlDMmtHWHQ5SkwwM29KSElUYmJZTmdBaWZpdmdJV01ENy8yZDJ2cDc1Ri9OWkFSTGpLWWNGQ2VOQ1hNUzRtRVY0RFRBSVkrSmN0Nmg4WXdpQnhVZFZROVRSYmtIRzN0UlowaDlhbks2VldicS83eTZxN1J3NG1aYlYxeVd0U0Z4WFlkZDcwakxTUVQyMzdjbGJidGEwVkt5SW1RTnF3ZmpaaFB5MDRvcmVzakFJMm8xMjg2YmowOUZjdVVpTGZpalI2enVvMnFIRjBnL0NacGMrZGZBZWZ2V2J2SGpDakNPdjJaSHQ0NEU3bUFDbWtXV0ZqcUUvK2FpM0E3R2N3ei9iWlFVZjdEaXlWZjBPSjlVdU94TmErU2lJUE5sdjl6cGFXQW1sS3AvUkI2dWc5WGF4SGU3bjRNVjcxbjk3b3NCa0lTU0M2WW1ZUzRTWCs1VlkxaGhGSlBMRXFodHlaL0pLaG41OEY2YXFBdjhkbXNrWkRJVzBONVVhcFQ0Sk9CNFoyMnZ1NDQ0RmpSVmwzOXNxMUxnanlCcHVnNGVwd2sySWpnd0RKWTRqenlpaFc0WGV6Yk4zMUlYV3JUR3JoMEtkendteTROTEwzUGo4b0lRSFQ2YmR4YVdxSXZ5NHk0ZklseUNac0RkZEpGeUtFNGw2eW9qYmY0ZkVOYzIxWWM0TFp2Y0dIOWNLZDhCS0s2ZTZTVzdLWHZ5bTEwckhOR3hJTmhXMndWb1NyVjFZSmR2Q0JHWGl5b3ZERUNoWWI1cDZ6UWxsa1BlZmRucWI3V3RnWlFtWlg0djNrSm1LN3cwNVBGdi9YV0JFUkJubEkxeG9zMnh1RUNlaG1UY3JIbEQzUGVjeWhMWW1uVjV2UDRRVEtHR2NNbXRTVVFaT0JCc3Vza3FmK2NJMTVyL2wwL0tDSWlKWWVxNVBiVmJEelF1bnFIT1AxMmtLZTJ0VEZtbldtdGZXZVo3MnhxaUM0S3RMT2h3eWhWWlg2OHpGcWZ0cFFjSzR4NWFEakhFbll4V1Rlbllja0dOLyt3Z1JTcG5nZXZoa0xEdGhXQXJjc2luOEhMYk1BdG83cE5odGpEempFK2w4c0hobzJrL3QyZVMveVZLeTl1ZHZGeXVMaE1iY3dHMFhDZ2dXNDhkL2lDYWdSRzI1b3dDRHRkdTBpbVJ3dGx4b1NTMU5GQWgrbFFramRGb2loVjdPaXNyVTBIWHMvejd6am1jQUlnU2pueXc3ZGVxVlVqTk9Fdmw4dnNjMXdSM0liOU43bEFWNzA5eUxtMkJFcHh5djNxNnJXbm5Od0QzNk5EKzdJNlFMVkt4TnYxdzhZa3JIcTQ4bzdEYkFOaFJWQWt4a0RLTnNEdFh5cFNIMWJwaVZJUjBObTJEeG9KMitsQ2ZJbU5UVFA4U0FzQklTelRVR09aZWhuOTU2MjJGRlh5d0RCdVlEZ3BBQnZqRHFVelZkRWoxL2kwVEdVeTdaN1VnVmRGUjk2c1A1b0pjb1lNY3hpTmk2dkhOeC95VjB3UzFlRFpPQjJoTzhzc0M2TWMxaEMrTVVSYVVFTHZrVW9nSFM2emZncnQvRXlOVHlmZ3BwZWQ0Y08yNjlQcTdKWDFJL09Pck4zWVZkU0lZbzZPZFVJUW1ZTXUvM3A4dU5xdDdyYjVaR2hlK243TE9ROHJBamp0SkxXVVBMVDBGc1dBUWpXSDNsWFJWbDRyb1BOZXBLV1VvTmp5bDMwL3J3Q2FhMWFtOVVNc3ZqRVVSaVU3WWVBb1RNTm9IMGZZU0UzK3BjQ0hJb2l2OVdwSTZoWHF5K25Ibk1wNThHZnd3R2lkNEMxdkcxcndtS0Q0TXJyZktURlNPYitYSlpDclZYblp1ZlRyTnAzZ3h1cEN2b210aG9BcTJZekhiTjA5UTRuNUs0N0VUZ25IQjNZWGFXT2ZrQ0taZDVLN1F1Q1RlY2NCby9QWW9UY3BmaWFLMkkwTitEcVh0S3ZhS2hJcldZWDRkTjVFSnZlcjNobklwcTRmRGdpYUhWcW5hVnpsdC96VlpnRzdzTE9acWdHZ3lOS25VekdyTFN6WVRkRFI2eWVENlV0RVlqRnFNdzFFcVoyZlVmdGhGOGhKeC9ZL0crWWRibDh6ZEs3S1NUTnNWc1U2K3NIcUFGb0RXQThscEVCK1lkZHdmRXBTUHAydDlVL3ltUmtYS0RaYzJoZHFhczEvQVhoVkwvSjdweXFTdkNuQnMrajlSRlVkUlpxaHNFMWJuZ3FlQldKSDZhRW1JTzFidTk4UDVHWUZiKy8ya3d2SUs1T0JxMk9hb0pIcGRTSitFenhkVEg2Qmw5UXpFd0pvZXVpN29CK3BlV2UyaExHc09qSEk4bk5Yc0svU2haNDFNVklQdEJKczdjNTI5S3QrREFqRCtsVy84Ums2RnBvQitVNXhZcVZGSDBlekUxaHdKSGZJVmVURFp6TXlBMnY2SjB3eU9DK1hROUVUYkl2Z0h6b3l3Ym5BRk5za2RJSUZGaDNjMlBzNXgvMjErQW5ZSTYzbkFoMlp0ZHBQczVMV1RJNGppWE1Qc1JxSU5hQyt6SkxISzFKekEzeU9QOG9qbStld0w5WDRESFdUVVQzVW92YU5aemxpanpaOWVQZGg1d1VLd2xYQWxLdUxZNzFIQzZNNG4rdUd1VXovUDVBVVBUQVhzZW9xMVN2NHJ2RStzcFYxNDZVa1BvQjNBaGpkM0pzMjkvaTExVlVneXVmalFsdUdFOFVzTnE0ajR4emJWOVpCbHZnVk4wbHFoK0xOeG1MSit2RGFVbEJuaW1JWlU2VWVpSExxalN5TWQ3WWtpb3VYRVp5TGxtdGovV0g3elpkemxZT0JMajU4elJlK3B2YVlycE5zU3dYT1NUZUVRV0JNMmRWRmI3a3VOK1VEc1YranRuODltQlYrSDR3dUg1MnhpN0hiTWxkc1VqbnE2Y0lWMVY0Um84R3F3SlEvSlhSY3BBanloTTVsRFcwTWxnSlJIMStUZS9WOEtRQ29RQUpjWlYySlFNZXVrWTd2WGFhWFVBZkNRR2dLcGdsWW8vcVJyZ2xBdE5YcXhlOURDR0tWMStrZ29YWWpDYitLTVRrZ1Z4M0Y5UkUzeEM2UEtHdzExNmtzMFhUOFl3b0phMHMzNm0xVEhqOFpaUzlKd3lRZkxBeFR6WGhtcktmdWMvSVJROEFmSHF4K0FsdlA0eDl2T0hMMFgzT0JSUDhJdW81UjhSQXFVNzVpM3MxU3RiNUV2VWtER0JFaVRWMXByY2VGTDRQbFRGM01KY0s3bDE1bGF6dFRQV3JjU1phL1YwQVlCZVR1dWY1U3N6d2o3NzdTVVp5eGU4YUFDVEZ2QXRJT3U0Vjg0ZFRsQ0UrQTBLSEpxMkxWVFlVdURidzR1dFNJSFNHUVRQUDdWc1F3aDZESW1CWVV5d01mYXpqM3kzMXJBRWVPUitlN2prRGtSWU5Ib3hVTTFYUis0T25WZ0RWaUZCbnpuQXA0TTM5eVkwRTdhc0pmdzNBTkVZU2lkUGovV0RMU1Bkc3lDU2JpWGlDeDFCYzdWTUxYSHZNY3lVakhvc0FvQ2VKMVh5c1BHREVNRmtVTU01djEyeWQ1M2tWWEQxQTZwM0dUN1ppdHdGNlFIZlp2RFU5TnJKSGU0NTVlaDRqRWU3ZVRncW01LzdKNHlpREo4Rng0Z05PS3pGa1NHSUxNbytCUnFxcyswR0VLWDZLeHM5RXRHMjVEL2g1aW1yUWk3VlIwRk40am5JRTZ2YWt2ak9hYyttdjQxZ2RJb0hMckZCbHNhT09maHRXR1dQN2Q3ZzVzNkQzVXB6ZWx0YVpnOG51eHJmZGs5V2hCODFCaGpMejRBd2Y4SkNEQnA5NUxmWEFvM2JYQWk2Mll3dkk0dW5UdGcxRDluYzF6UWtuSjZlNUNUSmwxTlVjMzljSDVCZmo2L2RxU3lMZVNnWGdNc3FRTmVKUzlQeWVuWHZGQ0M2dFB3dUZkdFF3K0lPcS9JeFV3dzFBekM5ZFd4bWt1bjlIeW1rVTBOQUFSOTE1VnhLNDJydktrWFBFbllubHBYcXhMSkNyeUNGK0xvRFFWZkt2anZyM3FxMXRNa2s0YzJFazQ0MkEvbnpnMHZ2ajlwektlRGVWVzNLNFlSanI5dk1HTzJIZ2JvbWd4NjRQbU1ITGFTSHFTekxyUjQvQ2d4Sk5STzJDelJlU2lMRUZPb2wvdmtDU1dINW9rRDEvMWt5blJrLytBTjhVazYvTjFkWE1JaFh5Q2xPbWtFMDBTMmxBNmZpWXpXUDhOVnVwNHFOeS9TQk9UQjhVVHZCUVB3TzZDYzg2Q0hna0JOaWhPQlMyRWZNaWEybThsVkh5bmNCVGtXL25VUGNUblUwanNIK3ZRRmx1V2VkSWZDYUIxTHA4cGEwbnZQTEZxZEx2VE1kK3lzUmgwMFJUYlN4OFkxdUU5ekZSVzRyMDdzRXZtNzZqNE04WUNMdElaQ3BvekhaRERMMnhJelliQXZQNTlQVXpvN1VTUCtJSlMxcGxuVzRhMDd5N3o1TTdHUjVCT2Z2dW93MUFmZk1vOWltS2VvQmVzNzVvMjNwN2JHcjV0ZUd3MC9wL1c2VnFNK09aSG1mWUJzQnhJMG1iWEVySTRFUUxVUWFkTXJTVUdNRDF6VEtmRldDZVlUQTlETm1OM25ZckhpSnIxWnFwTXZ3Q3RzNHdZT1hwUmdlL1E3bEFpYmpYK3ZLZFRLVElvRG0zb25VeEVRd3A0bVBsWnR6YmdhbldvbXFaaS9kUDVkcFJWT2g1SGROaDIxWW1hMUpNOHZhM1RMR0hLV1crWVhBQ3Q0NlpMS2pTNFBuM3Vrd3FFb0Y1RXNsRFc5LzJrdnBkZEtpOFZzaVVLK1JFL05qeVZlalBtSE9Gc1AxckFTNmNOQnYwOW5UZWFNdEZDVEJEZE1xdGhXOU5ZR3NibjFjOEh1V25wdTIrR051RGZNWWl0Q0IrZjk3YnpXRk1zTi82UEgzWXkyNm94Y0J4RWdWVXZZWUFzeWdvb1N1enZpTTZqWURySXhpVEhOa1gvVUoxanVYZHpxbnZNeWVXREJEOUltKzE4MnNsamJyZnFOMnlqbC90RjV1TkdsYWlFaUhCUEZyejEwTjJsQlc4K2owK0hkem1mSXg5SEFETkhwdmt1MnJSRnNkZXZxdUpNTFN0Mkg4NHhNaHFKVlpZbStuQ09rRmw5T3llejFSNm9uQjR4OXZxL0Y1WldYOUVnVTU5WVJCZ0ppT0J1R3JGZkQxeHl5NmFoTjl2SWJwc1M0SkFSaWp2RldZYXp0emZoU0ZTZzRQQnpHdHIyOTJKWFVHb2pXYzdDSHVhVWF6MngwMmY4eVJZaGtlenF2UjNSMmt4N2hWWFpNcGJMbkoxZ3ZQcjQ1Ni8wZkM3NGNDVmZtQ0lyWjA3WGx1dkJuYkpONDJvazNiNmVlVCtXbG11QlU5WDF0OFV4MnEzclhzTXRKVzRNUWR3WjJSN2xaMTVKcGVYdG9PcmhVTEowU003eWJ3WlBmWStQT2dhS0x4SjlzVDJCbS8vSFF6bmtvbmVNalJ5TU5PS1hWVnhZa0xDWmNuYTF4dHl3eUhJNVduT3paczUzOS9qNU5xdVpRaFRiaFBYdUFwU2RHREt6cFFsOWtya2ZSbW5hdFRHVzRTc0xuUWQvc3QwL2xUV0FSUnFCMDgvZ3lRTnZ5NVBLZkJrU0VyZmZZbHRlRDBka2VoVTZOWGVHZ05sdml5aWhkenlZQWFjZ0c3YWovdElLSGVYa0hKZmxhU1htckllUzBYMVNhUW1HNU1YZHlXMG9BOHRpZE9XWmNKcGdQMjltRGFwOHlnZDh1bmJIQVdvWkg4WHY4VE5tRkJaRU5UVFVIWlE1MlAzNnhYR1B2UVNZZjFqR2xHd1ZaaVZibm1pSEpIcGtTYkE5cW5mdGtIaVh3Uy80Q3dIM3l3R3QxZnNuaExONWhZYjB5VWpFd3MvaXZtOXIwVFJCdEs5OUdxaUJkSnR3Vys5eXY3K21xTXc0YTdVRlNIYjZLZFJWZ0VUS3NQaElIeTZBbWhxcVpWc3RUTkpMZHd1MEo2SXdha3lMRHNxUy9XSnlmdXhDLzVNdHc0OXgwS09aUXFuTXJtdHRIWFA1ZG9YYnRxdHNVbTREeTBMUERPYTNLSXNnOUpIeVo2NGVBcDdzSkZkU0ZNemx0OS9sODNUMVNwbko0T3I0SW1va3pHUlZEbUhKN1JKRWU4RTc0QjkxMGplcXlHTmFPaUo5OFZrYWFOSkVaYUxHOHpveE52eXRvL01KbGhQNFE0eUc1VDRuendFZUF3dVYvZlBiQmtNbk81dnpzYWxMa1pIa3ZwSjRieHJRTE5MY3Fpb3VBU2x5eE9Mbit2a0ZySU84bUVjdHZkajlTaTdrdDZmaHZmdDcremhFUXkrK2VoKzFETmFHelFGalpQWVc4V090aS8vVVFPZ0FDUk1Va1RuMHZHaHZOckhCZE5WR1ZtbThmYnpVUzdoMFpPOWwrczcwMnBhcTZ6aTJHWmlRbHRJdzlKd29RcUdRcTczTldEYWdLNmQrMlNjbG9ZR0pwZGo2VTZWOXFLeFBTeC8zcEpWWktidTJPZE92MVUveGR6RDBJekFtQ3NKVXdKRXBtcFl0cTMybWUxUnlxLzZZZ250YnFZdk1YRjh3K0luTmdZSFRtYXhoaFVHVWloVWxhcnVYRlp5QU1ETWxjangrUnhxZU9MNlkrdnFJL2VwZkM0Nm1TRmljaGlBY1dROWlQN1N6THovbG5qSUtnMnN6YU1yNkViaHg4Qk8wQmVoV3huZ2ZnZDdCR01rQ1M3aG9XVkMxQmNCbDJIQm0zb3JUVWJpbFBsa2JHK083ZHIrOG1zRXNxbHhGNCtlNDQ5d2UyWFJOWTZkZXdjd2NGd0VkWVNSa1RXVERoV3UwM25jcFNiWEdOWThTakRJOUMySkxsNjNnOVUrVEV0OERmRHJ5TUNyOFl3a1p3UC9zWU44WFBpanNhalFlR3VxYXYvYlZnUjB4L0d2UGxkOFp5RldNbE96WkxyRks2RUw4QXVsaVFLTUp6V0dQdCtoRDRqYjFWV1lTWjloOXgxbGJLMmFkQUE2dkxXL21rNCtGckVCeXRRUEVBamJIWkNDbXErWnh1Q2ZDYUVQNWZieC83TDNPZFdqTDcrNVpUZEtDSVM4RkRqN1VKUzJ4TkZRLzlIMmNhRE9qYzgwVnFhVmQ5M2krd291MWl6K3p2ekF4NFVxZXdabHVENWFZekswOEF1RDE1dVZPbk9hV3FqaTl3VUx4M1ZFTllWWWhoVGdXSWVhL2RNcVVoeWM5WFBTUTRESFJTWU42cy9DQk02ZGtCMXpyc2w1cURjTWRNbnIzRHVRL3Q2U0VtTGVIazVmeldkMkJLbkRaQjdsZ3FnOVRRV2E5SEZLTnhpbEE4UVJEYTg5ZzlGVHFRejFDRE94bmdwT2hHQldUdmhUR3VuT3lFOVc5T1Y5RFVqNWpmNmR6ckJGelNOVStNWGR2ZG1HR1JFMWY1MEhVRlNaODYvL3FGSHdSOXdBblhCcmRUbHdXQ0Z1clFzMTgrT3dxR2JmbFdEY005N1R5R21PR0xHTTNKQWk0WXN6aGpDN3JoVTkweUVxdkpNa1JJZmxldGJ6c29XMWJCaytLY2lHdmxIRnBXbzZIWXZybHpPYXZrcy96ODZ0V3o0bnpzbTBzTHE1MnRlbnF0RVI1emdMdFpIaDBvM2hrSzZJQS9Nd2VMaHRYY1U5R3BrODhLckdDWVpCY1Nsb0hTNTlWTzJ2ZzAxY3pPbUhYZEVFeEoyc3g0b0FJaktNTUdrLzJhdE11YnF1c2k5MVhXWk5Sb0RuZEFjSGQvdzlKeGVtSTBIV0Z4WWxGaGNaQnRIaDRMWVhaZjlpN0FzUzY1MWZxaVJlQVhSRW5LQTI5Uks2Sk1yUWZ5WTdjM0M0Wk9XRkpGbCtmc0RTSXE1Z3NuaWNUWHY4T0Z0U3A0dzU5a2VseHFCNno1MmdJRzRWcmVqZHFYeTdFYTFMLzJxaHJSNEd5ZEkvMFIzWkpLWFQyRnczejV6V05lODNuQ04xMGl1WFlHdi9mVjRkdXRRbVlRL0JQYXprNjNZZW5RYUV5MXhXRWt0R0RBVU01RitQUEh5VWFPWldhUGtsai8vQyswaDM5VURUVTYxUnBjdDE0U0pRSWdqT3MwZ3pTTzBPT1F0RXhtTDcwUUFtenNNL0ovVmQyb3FKeGVsVktWTkVaZzJuTmgxQzRlSGE1V1oxOG9QRDU5amxJeVdnbjZBQ05tL0dPdWtPSFBHdmVtTkxpa3ZvNGR4UnpBVWZtMHM2bDJYQmNTN09KMWxMWlVYc0lldEFUNWgwT1ZhaW1RR1R2eDdueVV6VC9iZDVPcXd0N3N3S2ZTekxkV3MyV0VyUExVblBkckRPUHFwM3VjMllDMzFIVElRRW05bG52MjRiM0hHN0dnMEFMYVJrbTVVR2d4bE9Bako0Y2d6QmdZZDBOVnlPdjh2QWFBdFV1STdMSEsrb2IzaDN5OUE3c2xONnJPR3NNc3hCM21jTmgwZjl2S1F3c1l3VlBGL2ZmMkZ4djR2Yk9RVm9mNkJ4dlhMZ2Y5ZlFZV3doZUlTYWJQWHJrZGQ5Y2I4UWZLRm80VkFkQXhLOE9ESXNNVzB6QXNWcTJaR0k3TllxWTRiTHFwQkNxVG9oU1d5MTMxcVU4QjdGd29wMjlBNi9QMlpvWTVtMTFZamRqeng1VE5XOGZJdkthcVIycCtoMHhKVzBubTRBTURSTndWT1NTK3QrSHBXaEMydHYzZDVlclVDQ3ZHNzVUN0xzREFDV2dUKzVVOVAzZDFVYmRid1pXZm0vZ2xoU0tFR082d2QxamVjci85VVdyWmNSWGpVakJNcFNseTJoUlVyQlVDRlQvSnIwdFRzNVBjREtIbzlSamNaYllZMTh2ZnNLY0dOV2prdWtsK3Vud3BLQzZPRDFxd1JXNzZabklJQkU5bGtFMlZRbzIrbTQxN283MlgvTkc5RWRsM0VEWmpKV3VNOFlucXE1RlV0TndWWDRQMlBkTU1SNE04dDhicGYwWWlreEY2Y0ZVRFlCcU9kT25Id0x1RkpWQ3c3L2VDang5WlVNUjdEdGxyU3RmTXN4QkorVDhFZExMR0dQVEw1a3l2SFRMaGNDRW5aWEFpSWZnaEYzQnF4a3FYN0xaWllvem9ySjQxdkdWdmdiNE5PUkVRVFlORGRDRy9Zb1Y5eXVHdnhzNE02aXVBMmYvbFFPUm82WlpPY0FxdVViblFxaXJRRG5NcTZKb05xajFrYkI5ZDNNK3BMYmE2anFZV2U3aWxPWHpvN3lBbVVhcStRQ1ZiWHpMRmxzK21qMTd6YXFUZ0VQUTRhaUl1VUV5d0tlbGV0WXhaNnMwWGprRzJSM0tld29WaTdOWDR6bnlLTXgrSWwxcTMrMy9UTGJ1L0lMaTNNaVZiUjZBRjZ5M0FTOFVBaE5yL1JZcWx6Y1VzcXpScy9nVllNdng2OTc2d1R6SVZsWmhKSWROb0NYR3Joa3lBekNpMklLV0g5cUNWRkM4eTQwZTFCcnlBWUpvamxGY0Z4OGdwdlhxekRDNFExT0JuRnVZdW1BWmNUajh0ZGs3RnJDZkFva2tTSzUwQ0pjbjJsWmNtRUgxaHhEOGk2UjRMKzBTQ0xwL01qTHlnSXg5ZEhMdGRaaW1kUm5PMzVYM2h3bkNpb25WT3duVEMxL2Y0OTFXenJZMXdkRGx3dG9rdnpETFhCWmVuQlBGb2o5bmhwU2lBWHpWQTkxdm1OYjdKVGNqcnpmU1lwR2lCTDBjdks1QjZuU29SU2tGd2tWdzZZWWFUbEVkZlp0SHhQWkZUbFJlU1NaVmRGT0FzUkVFdW45UGtaZXpnRnNzN1Fsek5nKy85LzJpQ1E4SzlBcnRpK0tkUGtmcTh6YVVMVldHNmcvU0RwUlpLUGJJcVNSSlZ5ZU9NZHRwSHBsL0JuZlZWRmFZUHl3bzQyd2lpZFovd0svU3EwaExFMnhCZHZEQU4zQ2E1Qy9JVndja1VuckovbzVBUCsrc1VLS0ltUHU1MjV2cER2UWtJeDBxb1RJbW9xRHFSd2MrUWRHTUkzWHZoZlY3RVFIUTdIT0ZjaXhwaUl4RWdOc3hBMDAwa25LTVFVSk1VYWxaZFFvSThFMXBWU3JjSm5VcFcramhTUzRWTFBiOXhKa3dIbnZpcW82WStMb213NDcyYUcrY2hQNzdwQVZTRkIwbEo2cklNeGdTb2taMTk3YzducThoRlUxeGpHcUo4elNwTVJNYzE2cXFuUWk0RlQ3Nzc3cDVsMzFzQzYyUHNjSmpnMDFsUDZZOENuV2JQWXpCWVFscWQ2K3FNUGlUY1B2UjVvcEZERnFnakxYLzgvYkhBSTVDdkY4OWtVRUhoZDBLNU9CaitGMWlyWjlSSWxrYkplZ3ZuVFN3N3NWQW5WQjBUeTc1SlY5T2tHQU10eVFIMXo2eUNiNlp4NEtWcmtRNE1jaEhmRUNsdFdOL1dESHNwVEpCb0R3ZzdjZFc2NWZUTUM5VlJHRDRYTkdnd0xCMTl4SGxLRUZPZ0R4bXJjY1VBY3RJTE1SQWxwY3JzREdQckFZNEUrMXd3VU45dHlXVmtETHh1N0Q0dGkzOFJMSHZtNFNOaWVlcExrVElnS1pzV1BvQmVEaU4rWWNDR28zb0Z1ek14Q2lyUzl3TUcyQ2NBUE9CQ2NiUDMrdDNVTlZySWhPWWRVTVB5K0l1a2t3a29NS2lvV3RHQlpaTkZ6VVpoaG95eGZqUm1Id0dVNXR1Qi9qRUlxL0JsVW0wWjZjWlVmakZ1RDljNXBpeFhBU2NxNGdreU91SEFLc2ZWZ0UxYTdvMUw3SkVKcVR0Y2RORWtkbUZPN3VTMUE5UnpTRnJGRzAwQVE1RVcxTXJrOVl4SGg0WGxNNVRudlpkZnRXK3ZxWmRNeXpMS1JQVGZBUEJwWm9Vb09meE80eStUQlpNS25zYTkrVENscmdDVHdiN2V6eldWU2dYMER5QkVYdy9zQ0tEeXVtcDVlSllyUjQrVkJaaW9ETjdpRzhBZi9iREVVdUUybkpkZEVKTC9yMmxONFZJdzBGajVGMHp0Y3praWZaaGpTcDFwcWl1amdsVmRFQ2xuc2tkZG02Z3VYTnJ0cENQZ0JUWUtoMUVtQXFhNkM3UGRqSUROdWRudE9kUVVmd1NVcE9KN0Y4ZkdlcFU3cjJnQ2FGaVliVC9MVUpSTW5vSWpXZlJBdUtaTDlnaVVZbHpOYi9IK3R2cUp6SWoxdG05N2V6bEN6aXUycnVPaEw1M3Vsc01Nb1lwUG9mYWpwSVZrSTRxQ0NWU0xNU2U4SXdXa0VWMUU4QkFUR1JKZVo1eGN0RWd1TWlPdGtXUThwSUtnaTZFMW5ETWVIcmNXbWRoVllDZEh0T3VIR2dmdU1JNkQ2SlNWbjE3YVNZM3h3QzFOUHY3SEdmdXl1WXlVMGtrQmNNU1JVN2VxRTlYK1hlTFl0MnIycTFKZmtTdXZ5ZWxWWm5XYXg0YXFpSjU1MmVPRk8vVkQyTWY5Rnc5RDFpSm5Ya2I2U1RYS0V1SGVmeW51Q0hXOHpHODdta0FhTjVEZHZIcFRyOU42ZXlDdjhNSmFnMDkwbUxONWFxa0F4LzBra1FpdU1TVkNsbVA0V0ZEMDJqZHl2ZlNtcDkvOFMyRzc3VjBsd2YyU1piTFZzcmRPd1BORG40am5sZ3pnRW4rbWphZms5NmJCMS96c0tYVmZDZ3VBR0l2VmpYNnZtMTBRWS9rVjBjVmx4UTh4UTdwdW1KNXRtTlYyOGhiSkpxMVlrZmNJUkZXdXU4OXQxWFgxQXdybUFDcWpQZ3dLRWpUd0xLWEpkemllRU1QZituNjZZRlY1akNoenRPdCtjKzE1dEJmcDRHNkFpRU1UMFFQVWVjU21RbXY4TjhKQmwzZ3dZdXEvbHoySFJaazQydzdaTk1janR4MHhJZWFSY3o4aHkxUmx1MGwzN1dsWEdTMFlySU03b0w3cngya2NGOTlBTG1YRXAwODZVeGp4dGxmTmJsZVVnQ0M3Sk8xa05mVVh6R3FKT00zWk5LREJjMzkrSXJJRXRwWkdtdUlIMXltNUM1U2VxSFljckd3clJROVlndkV5V29KQ0pzdXFwd245Um1vNnVCV2VoMG94RTFkQUQxcVl6eGdFRWhnS1RFNlM5enlCSGFmaGs3QUlidlJocXFOYWNUYW5DcDExeXBxS3ZseVV6YWRYTUxVcmhEZWkwZkgvOW1NalJKbFc0V2RCZ3VuT0dsM2lxWFVaeTRLRXpsMDMrVEZBOVVZaExqa2w1cU5FYkp4b3VLZ0oyYkpkTG9xMHZ0VmxUWS9PdEFOczRnTXdZRVhNNW0yR1NOdGhocVFabkpuUlowVEJqbUt3Wm1ET3FPc1h2NnE4TDN6U0ZuSFZkcUk4UmpWL3NhRTNQa1dLaUp3TmJuNkVxVjE2NzVJYnIrT0UrUkNXY29mUVMwMlU0YUNQWUs1T21EVDNkOTVDS3JBUEk2Wmc0OGJadmdIaUptV0libm5rbmRzN3ViNXVBdGhBaHZlZmlLeHgyYnVJb1M0d3M1RUh1ZGMrSmQ2ZG5QVDFUT1htdmR5NXNpRm1RSlNWd0hQcVA0NlFnVGp1M2lBbW53c09Cb09IdkZiazB1TDl1TGV5WHRXNEFjdEJkTEdacm0vaUpXU0UrZXA5NTVEMy9ZeUpFTXhVL2xPVFd4bTZVQmk2U0h4dnFYRDJlY040a1RUSk9SOFFoeEdFTWcrRGtpSndzbHBoeHY3TUVmUllzejNZVTZPRGpaaGdHZmlZc1QyZHRiTGVOTjRNYTVOcndnZVU1ckVHRjR0dXR4Y3BJR05pcFBaOXBVdE5xUzVWVW8zY0lQVndqbm1zdjl2OWg1OGIxSWlqZUNOQUo1M3JkLzF6K2hwN2t3Q3ZVVXVDaDJ5UU9RTGhZQjBjbDNQS0hjTWR5aGJzOTZnbEtrMTU1UXZOTk5ZWklTM0JLcnlBWmtLaDcyVlMxa2tnN1VLUjV2cWUxc21pMms4M2ZkaVplVitWN2lMYnJGMkUwdkJtejNKazNNRVBKOXlUL0F0YzFocFdNY09ySzNVMzFDMSs2Vk1HQzlRKzYybGtEY3l5ZldSL1MxNTlrSU5qSkFVNUphMmc5TSsxZWJoQU9LcmV2Z0tTa2hYcVdPTXpWVzArMzM5eTZSemF3bXlKRE01M2pMM0d3U3pFV0Z1b3dnMDc2UTdQZnFaS1puMzlFWVg0cUx2RlB0UVB4SmE2SnZYSzM5dXd0NHVuOVBvSjk2QTYvcTJTUzFHZWs0ZjBxWXpmbHo0SDUwYTBkK2t0bjRnZmVkV0dIdWVuTDRPK1NKSUVGNUR4ZVE1VVY4WjRoaE03NldMK2gwdnNYeGptYlZsRm0xTnltcHBjMHhQNFRuTFA2QnY1T0RNTzBEeGk5bkM3Zk45Z21kclp2QkFSZXJZN3g4SEg0SUIyWWttL2lUQTkxaFdMOEpVYlhLMnhTWXZxMnhWcWg1RXhKYmU5UzU4RUxla0NFaEFkeGt3NXNNR25JY1BOM1IyTFhoWi84RlF1WEd1ek9PcUtDY001UDNKOS9XSWFtZ3VlOUQ1Q3V2dlVybEhPRUUreWhVaW94L2xqaXIxZUpJWkJjNXovS2RwR1ZJVm52SGFyNVhTdzhOdnNlaEJvZXh3ZGw1ZThjZWdwc1NHZE5JUS9SK1M1am5NY3hTa0tGOUt2bS9CNVZ5elBDYmNtZ1NvRGRNMTU0TExCNDhGVG8rd05BcGVlTDNlcm12WjI3SGxmVHVqVVdEU3BtekhOWkpSaVRodCtFZDZiUjNScm42L2Z0WGRUTXl5YzhINWx0S1MwVkMvTkswaVFuRm5EcFBjWjhoMmNlZU1GMG9Tc2MraEc4ZmdrVFhxdkFaK2NJTE9TbnJXYktlbnFuZW1BSHU5MzgyajdPSWpDUmJtb3p4MFZqTW8way95eFp1YThGeTlHWHBSQms4SU81TEUzQml1Wmppc1BtNWVBdUh4T0F4VC9qN3BQS1V0eWljNkltdFlwRFVTR0xqQ0htVGdTaUxSRE5oN3JtZDdHSjBHOGI2U0VRWG9vRm5uY0F5SjE2bVlpVm1yS0xod2JGK2d4T0FTc0VLNFJhc3JJeFFaL21aTTNqVWtLR25jODkzS0VZNnBSMDhSRGc1OXpPMkx1M0Y5Rlh3Q3pZcHhVamx6NU03RXFiSXlieURxemlNRGc0TWltMlFybDhwOVRydDdxQVNkMG94T083d1BlVkpHdFAwR3FwdzVvMDEvMDZoajZJeGdhOTEwZzQveTdRczVpczZ0dTZZaHN3eGhqeVpTV0F5aUUzc1ZoNk02bitWQTltTlp0dkNpLzVYaHk4U3NKUnd1cUkwWGg3bUVnU0dNN0xLMjZSNkZNWVptTnFiUWY5OWZDc0t6aVhCZ2VHdi9HQUNJQVo4Tzl1WTI0a2llbXBldVk3Q2ZkbDZpc201cXVaNDBoRkorblNwQ0N2bEQ1MUphK0dKbEpQQ0VOL3FpU0tJVlJaNjh0aWhvcmgrS0pLaE9lSTNqNU9Ya285Z3hRYy8xMmFieXVBOG9BNFZBMDlFcS9FTHMvVUI5bWVtS3lHY1J5cTVmZGlNaytVSGJtZlRkVVJQajM4cWxTdWtOLytralRpV2x3UmpheEsyb3ZWcm1VeG5RK1FpUFlCODQ3dHhQZWJUMzRaU05YekJaM3FOTTBqdUdkUGErUWxsOGZsWHRLZEdLZlhGQnphMEZZV2pORVE4RFdJK3pTU1ExV1VaaU1QejYraktaT2M5aVI5bDBBREpzZ21KUDNiemxwaGxpTXlpM0d2SjNKTUlibHdnM2VTOXhwcXM2MFBhRTJzMWhNWG03NlhFZDZuVFY0enIyMkdabitydjRtZ0dBVGREQmtuNkxic3Y5NzNUaGJ5R2lqMXlySHFjOFA5TlRCbWJkN3dmYUNCUFF0K2w3VzU0NUtsNDFSc3E0MXR3RTYvYUsreHh4a09FUTZON0N6QndYT3ZZUjR1ZnVMNkVNeEZBR2ZZWGV1Z2k1WnptMXc1NUZLZzQvdnpSZE5EZE56WEhZaFI4STh2cUZTUWc4RGUwSnhsbkU5WEcvd2FoaDdLQk00ZTBxL2xKa3lPa05odFREcWJJYnd1Y0JkV1BGQVlPZ0Z4UEtTM2Fmem5FaGp0SStQR2JVKzhpYjdVZW9pc2g5VE5WQkluTkQvY3VwdTcyUFk5SlpZenkzcjZTS1lJbC8rQjVtTzllYzZVSTk5QVlBQ3VjQWdJcjRRQzRaMHFiU1BIOTVkS3BQS2tHemVBYmxkV0x5RmwwcVllUmVqdlZhS3Ewc1NCRmZMUy9jQzhZRlVzRmtaZDU3NTZkZTAzS0FDekdvNmpiTWFhaXJzR2FOYnUyRzVhV0NkZmhzT2h5NWpCanFDaXZiNCt5Q1lmVmFySEFVRnNPZ0t2WVlIcEJnbStJanJqRFZROU80bGUwU0VnaFFQdHdudEpZdmZPeHJkM3dKSm41dXlkbEVwTkZRVzhudFVJTndsWjdkT1ZjcUh2ZnZ0S2hOZmZqTEUwaE9WRHZJT2dZcEpCZi9SY01FVlN1M1RuTnVJVGRiSko3NEtiM1NzQ1JlMFlQVWt0bTVnZUF0ZjJKaUZhNUZiSDJ6M1dqYmZLV2NKbEZmMU9IOXhxa3BHbjdBUm51VjNGd1ZEd3JzZTR0citNQzN6Y1hvUlBiRDdBZGEyVE9jekI3UVZFZGJ0ZC95Vlh1M3M3RDk5N0FzTGwvZmxZSGZnRnhkVnQ2V0ZBY0tDQlFrVmNveUNYWHNSNE05MXdSc01wTTY4Vnp3VFVHS2VSM1FBZEgrUXhoNGdrV2d0VGhzLy8yUUFCQUJvQVFBQUJUd0JBQUE4QkJrOENBQUFBQUFGZUFhdEFBQUlBQ1FJQkFRb0NBQU1BR1ZSb2FYTWdhWE1nYjJZZ1JtbHVaMlZ5SUNnMktRPT08L0JEQj4KICAgIDwvQklSPgogICAgPEJJUj4KICAgICAgICA8VmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9WZXJzaW9uPgogICAgICAgIDxDQkVGRlZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvQ0JFRkZWZXJzaW9uPgogICAgICAgIDxCSVJJbmZvPgogICAgICAgICAgICA8SW50ZWdyaXR5PmZhbHNlPC9JbnRlZ3JpdHk-CiAgICAgICAgPC9CSVJJbmZvPgogICAgICAgIDxCREJJbmZvPgogICAgICAgICAgICA8Rm9ybWF0PgogICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5Nb3NpcDwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgPFR5cGU-NzwvVHlwZT4KICAgICAgICAgICAgPC9Gb3JtYXQ-CiAgICAgICAgICAgIDxDcmVhdGlvbkRhdGU-MjAyMC0wMS0wN1QwMjozODo1Mi42NzFaPC9DcmVhdGlvbkRhdGU-CiAgICAgICAgICAgIDxUeXBlPkZpbmdlcjwvVHlwZT4KICAgICAgICAgICAgPFN1YnR5cGU-UmlnaHQgVGh1bWI8L1N1YnR5cGU-CiAgICAgICAgICAgIDxMZXZlbD5SYXc8L0xldmVsPgogICAgICAgICAgICA8UHVycG9zZT5FbnJvbGw8L1B1cnBvc2U-CiAgICAgICAgICAgIDxRdWFsaXR5PgogICAgICAgICAgICAgICAgPEFsZ29yaXRobT4KICAgICAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPkhNQUM8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgICAgICA8VHlwZT5TSEEtMjU2PC9UeXBlPgogICAgICAgICAgICAgICAgPC9BbGdvcml0aG0-CiAgICAgICAgICAgICAgICA8U2NvcmU-Njk8L1Njb3JlPgogICAgICAgICAgICA8L1F1YWxpdHk-CiAgICAgICAgPC9CREJJbmZvPgogICAgICAgIDxCREI-UmtsU0FEQXlNQUFBQWd2TUFBRUJBUUFDQzd3SDVnd2NEQjhUQXg4QUFBQUFBQUZEQUVBQUR3SUFRQUlBUUFJQkFBRUI5QUgwQWZRQjlBZ0ZIUUZsQWZnQUFndExBQUFBREdwUUlDQU5Db2NLQUFBQUZHWjBlWEJxY0RJZ0FBQUFBR3B3TWlBQUFBQkhhbkF5YUFBQUFCWnBhR1J5QUFBQitBQUFBV1VBQVFjSEFBQUFBQUFQWTI5c2NnRUFBQUFBQUJFQUFBQWFjbVZ6SUFBQUFCSnlaWE5qbWNvQUFabktBQUVBQUFBQUFBQnFjREpqLzAvL1VRQXBBQUFBQUFGbEFBQUIrQUFBQUFBQUFBQUFBQUFCWlFBQUFmZ0FBQUFBQUFBQUFBQUJCd0VCLzJRQUl3QUJRM0psWVhSdmNqb2dTbUZ6VUdWeUlGWmxjbk5wYjI0Z01TNDVNREF1TWY5a0FCQUFBVVZ1WTBsRU9rZFNRbFF3TWY5U0FBd0FBZ0FKQUFVRUJBQUIvMXdBRTBCQVNFaFFTRWhRU0VoUVNFaFFTRWhRLzVBQUNnQUFBQUlLVXdBQi81UGZFK0FKUVRSZWxQQkUvcXRaTVMyM2dkK3NhK01nSlJ0MnhsbExOckhpRHJwTVYxMkNRM01scDVWTTdvbFpTejBOSm9KRTBDbWVzQXA1NGFOdDVneFZWNERhWUFJd1A2TmZ0QjVPZmRORXoyUWpJbm1BRDRLQWdQRGcvck9tdXZPT3BPZkdUTFdVOXZKcnp1djluYmVOSVVmamtrQ3lVb0R3MEVtVzI2SFhROVZqTmJGVkJQOEVlK0ZNbGx2U1gwQTRId2R1L0FaQVZkdUZhcFRYVHZJeldvYXlGLzdGcHkyNURCOXZ6aEhDbktDcFZhREVuWWhOVjh5UEV1WkRMZm9OMEdpOC9sZ0hEeWVSMXNDT29WRHVyRjdSNXp2Qm9vczA5USsrUEc5VjIrNWNEck1xdUJPQXgxcU9zRE9LQ1Y4NVpYMzZER1RwMFlnTmo4aWwxWlZHUnM1R25TVjFoZTMxbDM2WXkybXNrdmNuT0UyenZhd2RUQWJqaEt1eGxwQUFwcGt5U3R6SHA0VmRURU5zblNpajljamE5c0FxY2VUWG9FSzZSZkdOVVZjM0ZvY3dtWm9Kc1NDdnVra3Uyc3AvZ0dUb0VuSlQ2bzlMZ2V4YWlUTTBwQ3hyRDhKQ2NNang0Q1ZvOU1SbGwycWpPbUpGcGtIVElUNjNraHYxNE5wcGhhQ3NqVm5jVThuaVdKZ1dTZ2NiKzVNSlkzTFIzOWJQWTNSaW8zdUM2SVpoVitKakRZZGVLZm8ybGU4WlAxYlp4VVVBd2g2SmF3KzhPa3VxNHp5VnRlbEJpL1h6dXZQOFpLK01vZk9hZ0lQOERVZThSblZXS0s3ejBvNE5oK1M0QUdXdndyTzJndk5sbVdadlJtUW1UaEJSKzZGdWdWQStNYmsySisyQzVhMGJlNjZCeGZueENGNlJtNXh3bnUrQy90SHYvRjIxSFV1L1lRbHd1cElHTzhMNWVtSWZFT2hnT2hydm1SZGt4bzVGRXI4NnduMTNRRkJzV3g0cVg2Qzl6K2ZXMW53c3pMRlRzQmVwRkFhaXJwdmxCUUZ6UUdUcFEwaXY3eVRFSDVFNGRpSEc0UFgxQ1pqaG1vSS8zVjRCbDhpalJ6UE1ucmxvd3Zzc0tkcFhkT1RVUThtcVpJVE44OGl4QThtREJOL1E1d2ZZaHMzdXZ5UEZUK2h0RVJBOERhc1VKVXVDRklaSHZxNHhtZ2VQcG1TMFpsa1dsUW5NbE1HZTVhUUFqdDBpbU02NlVpM3RhbjEwV3dhVDNrbzMrL1MvcHI4emdEK2sySFdxOERzQnJIS3hsM0lnYm5GdlhTRUFUMzdzQTZwTjNUVWhGOWFGTWgwSUxjSHZsVHF0aHd0Q1FJRE8yZW9kbzVOdFBzc0NhdGNPSG03ZDZzRWp2RTBlc2Jib1VMT3NZOVNIYVVEUjNBbG5abjZodEdPeTNZalk5aVpGLzFLTGx5aDVCVm8xQlRqY09iY3VpWnZVaUIxRG80clBrYjhQUXR5VGVCc3NLM2p1SzhZNWh6Zm41eWpIRzcybUpsSHpnRUoxb2d2T0Nzd3cvUk95UkhhWDVMdEZyN0JOUHVtTGxjRG9CaElCSXhXZEU5MytoTjFzaFlYdGFzSDdPcVlnTUlTbjRPamF0a0xSeEdYakZveWVCcmdGWXdjd3dORDUyZmdxdm5yZnNOZ2FRL05TNVlNUUFGZmg2RlptT1pEMDVIWWl3OVZrd3Ixa2NrYVBQeTJTZnUzUDdDc2dCdGErQ2FTTWI1VW1Tand5SUEwTXJNa1QyQ0kxMXNYdjZjVEpHNmhZUk1jRXFkUXFsNkp4RTgwN2NPcmZiMDFJWHhnV1FsUGo2V3pkR2FZbEcxYjVGRUxENGV5aW9VNjFENmVDTGE3MVZHNENqb0k3VXNZMFRXQTl6YjNNRlF5dmVnS0cyR0N2R3A0ZTVCQ2k1Vkl5UlJzaUNuSGo2eUYwMDZlUmZDNDJUcTNEdnBXbTk4RmREbzV2eWVxMGh0OCtqTER3RGZOeFJCREg3ZG13SUU4SWNYMno5K29QbHFqS3hrVURPOVY1M3labGZWRDc2WlBBZnNQc0JlL2VsaFAwOWZyVXRqZjF2N01seC9pcTlhWWE0a0RnMTh5d05EZnc1Q1FOcEMyY2g3NDVCRE5nMStINFRKb2FyRk9rMmt3dEM4c3UxR3FHNHVJVEg4WXNCNUVBZ0VkcUluY0Y2V2ptUVJTY1ptQXQrMmpIcEwzN1NvajNNcG9sYWxMMTRsQXNyQnpUY0p6TUMrOTQ0bjltQVhPc3pLZTRBWW1pUDRONzBYazlQTkdUUUNnUDRIZm03SktDT1pFaEpxaFoyU0RXdkdRN0R5TmlEV3plS1JsQWUyOTdPQ0NWc2x3dGlDZnY1SzBoSFlxMUp3cHZsN25zcjVkbmZYTjQvalZYUGIvRFdFV0lBQnpMSzVGSU54bktDZ2hPa0ZxNDBiZEpTeEVWNW56T3F2SnFTeTNiaXEvcnJHWC9aL2pRUy8wYXpxYkRnSEwwcUVSVkxuM3hlTFk0ZCsvZ3pMWmtEdm5JOVZOZEFiQ2l5cExBWlc2STNDNm80c25Ybk1qUytURUVxN2M2L01ZcjVUQi9RNlJBWUdHM2ZUUnBSUjVZcFBjRjdGK1h0c0k1QmQvWEVCNzUxdjdlYjFkeTJ2ckF5dE8yZXJHdmFjcTRvY2xMZnI0QmhLdmRwRDlEYUdtS0p2SWFrREV1ZUErVnR1QmpGNTNHSm5YejBrdHUyYnJsbkowOWZNb2tLSmZzY2hrZHE0M0xWajNFU2RxUE96cFpORWM1TGlweTErVTBsWG9YVnRGMkJVdm9temlUZEZzTVVBR3NnK3owRkpIWi9zU1BRRXU1YXk2MVV0Y0I4SGptNnFuMFR1RWxKb29KNzRvK1JaZkJjbXRzY05wSkJoMFdndzVQbHhiYVFPdWhYSEFiUHNZWEpVbnY2UU5JOERFbnpGZ0RNaDBYRTA0WnZSbHpKV2JUTnVocktIeGhLTnBZUzFNaEdyc3FUbUxZN1FSRmEzOFpKQlpsd29pWTZlQllRQ1lzVGlBbEVlbTV3em1ncDV3V0ZaS1BuOG9ZNDBRT3B2SzBITlJxcjdMMXBuTkp4U3VIZEhqVDBFK1VZR1BlUUUyYm0vNFFGQzlXSFVIN21NR2ErQmhIeHlXakJjdUtteUNTU2FjdEFQQWdrQ1U0Z25LbDFTNjlXSk9JMkgxUkozTGxHNlN6Y0FFN284Q2pEYXRSWGhDSElhY3BSUVRHeXRFdFl5NzBydmVqWWVFSnNuS0JEcnR6dnF2bmlRWS80ZVUwSU5pY1FEc05vUEtwYUVka3BULzlROGFJaTFuT0hxd3pjTVhOaCtsb21BQzNIcnpVSGhobUNlRWY4R3Fsd0IrRkZ3YmdvS1dXTklGZWZLaUZIVnFWUzFRWmRnQXU3cHNpQWs0Nmg3em9aQnQrdXU4R3F1T2ZERFMvazFPZkVtbHdzL2xTRTJmYVd2MmpENTVFazZ6ZzJ0MXNPRlVRQWFJWVdlL01xOUJuakhqa2taa0l5VnZuZWZrOTFvK1FVUm40cEt0azFNMk5CejRGcHRGN1ZJRkFVRDRVRjJCU25zb0tFeElZQVIwRU1LNjQvSEc4OW5OeW9mWTlBazJsUEthU1NubGlkWis2ZVR5OVhlUWhqUDl6QVJzWGExNVkzSzUwcnlzZ0d2WjlwQTZod3Q3SGFxNGwycFVrNmswaXhLVm1qR1NQTkhkT2dMVE9GdStPV091azhST25CcUNFRmw5amlCUHBaZWF6eXpPQ2lLU0pXUjBleUJzbHRWcDgwNndFWkhOTUUxUWxlWWNRckc0V0JmZ3FjSUhMWHE2ZG9vRWljYkhiVjllTTJFcVFDYXBmT2FzK1BJWWczTmllRno3clNDOE5pRkxJUnhaMVE1UDNWR1N6d3BsTjRITDZJNVBjelVCTEh5OXJqV0pZWnB4TzdxdWlKMEVwK1FGNVNMOS9rK2Y4ZzJINWNTYVNPS01heFYzZi9oN1owZHl3dWR1SmpXOEdNRW5sL29GUXY5bmJKbzRkNm91SEFZK3JRVXFyK3VjbUppWlZlYzdWLzI0WDVPWS91N3FTbVFuQStJUnJES09sRnBtclRCTEhvYTdaM3Z4MklSc1R1MW5LZ1J5OEV6bVhWd1JjZXpUQ1IwdUlNbG9hOUMxOVphWG1QUzJjZkpzbGVjNG9VS2lmemhTWnlqOFN2Q3FtcktGK0RQWlI0T0VvWGRsMC9rK3IyTktNck1oMGJuV05uQVgzQVVCdGdTNnZJU2w1WjNLYU1wL2ZiWm9sOHRrMyt2OXdYeG0yVVpuYVJkQ2RleFBjQS9GN2U4cGlVajRvMmtqN2dJMVhQY2RRZGJGZ01raXBPTE9SUUVQbnRRNGFzYnJqN2lKR09NcmQwQ1lVdnYyZm0ycURaU0lyNmw4aVVQV1NYM3phcERpMmJ4ODFZaEFDbDZwTFVsYUY3QWIrNHI2SklMeVFzYUJzdkFtbUszRXVEeUJiSEwyZVB3eFozZFFTTzZpR3B1VUp3bERlVXM0TGMxR3BENEZ3U3JZWFZBSnZsaXZYeVlBcVM1YTRyWnZSaTVuQnI0TmRVMnRkZmJ3cjlBUWhrMkRvektReXJtYzlSb1dVblcyKzdQSnRITWorbk5JK2lERjJ0Q1ZZM0VWaXEyOTlTaXlFQ3ljeXdMU0VQUWwwTVpiTUN5STdIWWoybzVIZEtab05GZzlLWmVQQkVWWVplV0VmVkh3TWJGTlp1dWRQOWJURmF4RU11Z1VDS0FKdnkzcmtqa3NSNzRkRDc5cTF1aERxRi9VWUtPRStzYUY0Q3R4SkdsWU55eVIzWWZQTHJYcExtRjY4eTc2ZERVb0U2aENPRnQ3WU96bGZHRkVxNkRqNmkrWnphRDNoN1FJT3lUYlJ3Si92RTdHV1Z6V0lVWExvZC9Ta2wvVHNXWjBJM1RzaHVkaFc3b1hVMks3QjlqaEtrNmVVd1ZSUFBFTkxiV1VaclBrWnZmYUZHclVsVHNtaEs2Sks2U1cxUmpxTlY4MUprVXZLbU9MZHQ3bFlrTDF3OC9zUGt1a2tmVGZVa28rVUJCKy8vbGpRR1NhaEtkR3hUUHBDK3E2NXppWFl0dWttOXZCM3RCWHhHWXEyOWo2ekl1WGErTHUxTk1YR0ErR3Z1L0Y5cTR4QWhoZzlQVU1uRnMrcWJLTUU2WHR4MFp4bElBek5nYVlmbSszbERIY2xTaXR4OEpBaTdMUk1YM04vbVJqcmlOSUp2OUFGOXQzMFU4MTFVWC9PQTJPd090Q24vdXVUd3RJOWdSRU4rcVdYRStYUjdaeXBSbkNTUWlXY0FKbWNMWmgveVp1cGhJNDA0TE94SGdQWnZWYVdXNkxPSXZHeThzYnkwRVdUZ2V0WlMzdkhvc3AyYndsSWtsQktMZGQzNGhLMDFpRHlPSDJ0WGlIVjlTa3ZqTlEzaTFpVVpuMlJQYzJsOXBMMElLeStRRlI2MjVtTTZ3MjBWajZOQ3VoZGNnbzI3NUxBTU9WSmlzNE90OWt6WUxFc2NScncvYUJ5LzJTTERydlVHK2RGbjZGcG9SczhyR1h6MnYzZ3EvNnl2ZFRkV1dMQVNDMStjMUVuZDFBMGtqc2dZTEcyZUo2Uit5WVNGVUFBZDV3bi93VmdCayt6M3g0ZXZQOGE3bUx5bU9NL3NleWJtbzlKZDkvRnRWQnN1aEZ0RzhVclVUOFU2eTVXVEVENW1sLy9aeEVqY0NNUGxnb0lWdkh3NWNlVGthTEFhdmpocERmOWJabUFxMFdab0luZFB5SnB2Z09TeC83UC9scGVNZ3FlT0Jmemg3ZEgyYkU1TFh3Q1h2YUllR0ZjdXB6d2tRaklaOG1lbXRHdU1BRmJzYmV0SlRpQVNCZHpHZk1kRUlQeUc5YlFqR2xjck9rYk5kVHlMRW4zQWRoQnRGWDVmY3M4a2FYU1BGeWpCMHRyK05rbjZkZCtoeU5VS2ZCZk9WOFZsRWM4enBKaW0yTUcvSVNjc1BPTmZKTTFDaFZVYkRUZXY0bHRSYU9sMEJNU25mODdKWGZ5cGRhbk1rWUtZdFdNVGpQMThxL3NQbFVBam1nc1pqZzk4bCt3VUI0eFZObzVOaDBXUy9wTGp5VjNSYzFyUGcrOFdENzd5bGxJNCt2S0xONjdDQlJQdDdUcGpRZ2JwYXdEZVZVK0NvNVk5cXVpZGpwZ1UrcXk5L0V3Tk9XT2E2YkZLMW5uc2QvSjhMSEpYRXlpWXlRa0U4L1Fmb3l1QVpuM09iMTd6N0VUTmNBc3U4aExGd29mdEVVOGdqZThlSEQ2NlBHbE9DZnI2NUNtdENSc1VNVlBjOTg4UWVJWEVmVW8rc2YwT2JhQjRNQXNlby8rRHQ1OGpXWUYrTEpBMUlvSHRXZWgyR0d0K3JydVdiQTNGRTdLOE13eldvVFVMckZnWXJwRWs0UmdOeWJiekpxQnpudzAxOStTU3ZQQ1o5aDFpRmxJdkc0ZXJnZFoxY0ZTRm94TlRocVVCa2Mxd2h5M2liK0FZV2R0UW1ueWp4MkNQMEFyYi9KMkJqWHlTNzVYbko5TVh1V2pyZnV4RzhwVlUwQVVQM1g4dG1EWmtsMlIvdzRmZHRmN1lQUHhJdGtCVmExOFBCY1VaaGYzOXl1M25oR2hzN2RVMFNPZm50ak91RXVjVjhCM2praDhEUEpWQk4veFlGbFVGMm1mbllpZWtLT3JMNkN5VndDYlhnU3pYMXVKWjdSVGxjb3RHSjVkK2xaZ3RQVkd6ZmhlSWtEekEyako5RmxPSXVocE1TcGgvd24rTkwyWS9FTFk2TXR6M3VwQzV3U2RrVVFwdDl0SCtNQ0pOeGowNVMxVFBHWXhtcSt0czVKUnBxZVZBdDAzTUt4ZUhyOWZrdG03UitRVU5idGtSbVpXeXg1QWtyZUN4S2lGb0o3ZDNqK0g5eDQvUVIrL2NyQTlEcXRySUpJZ0NsNEJ4bk1BODhxY2VpRmxvWThWZFBzSGVzTExmVUwzWWYzNUlVK25EQSt6UmxWZGhMblRtQ1dySkRoejVTS2F1SEtldzFVdnFFOGp2NVl0b25LUlppU0ZoQ1RDUzNtMGhqdWxyWk80VGxYMk1idTZEUDExZnBZSmhUSDlnOUFFeFVOUkJlaGwwWmNuaTFWd1RDd2cvdDRwa3ZsSUxnbmNwWk5UMy84aWtJV1FiR2xraWZibzZwcXRGdzNnNGV1Q3FBS2NjUlA0VXlDK29DVEV0NFE3RXpYT0NyaGlFZXVKUlhkSzl6SnBCTzl6WFRFcHNreXJibFBQc053aTN5MjIvWVN1Q0c3UEZvN01RcllKS1JicGRXSWl1SGYvTkRwSGRVbjZrV0hROG5PbTNxb1lTdGFNb290YTZ2Y01LNjFLNzBlKzFWT0pjaFlWMjNkSTNaUGdRT2llVWVWZThmcUJJd3lhWnM3TXRvdGwwbGt6Z0NGUk5DODQ1VzZqaWtCaFoyOWliQXg3RlVXSnpxZzVCS2JrSDNkcG9vU3JhMFZ6Z2dKMlc4N1p4UXpBVUVsRkxRZllPcmw0NkJnWUxPR0E4RTB1L3prbWRrcGplZyt1MnIwL091QzM4MGY4d1hSYXBZNVVqcWNrUDJPRW9uTGoyaGFkZXRRYlNDaC94ZzhrOWhlcU5aZmFyTUFwY2dIVnpCK0JxK1BVa1l1VUhDdTB0a0s4bjRVdzVrNGhlRFU1RG9Sa3RiQ3B0aURIMGhSa1pBdXpueThZclFKd09PUk44aGNYQnIwQUtIUVRpNytOdS9KMUc5N2hEWk0wbk9WT0JXVmN0OFNYUkQ1SHJMektGTjFxbm5OOEZwZHBxN3BxMXZ4NnRVUEFJdVkwbXZ6UUFyRU1oUUI3aVd6K1N1TmlTTTN4SGxKUkV3UDlYOU95U2pVaTB4Z2h6Unl4SDAwRmtvT08vd1h2UHNnbWRMNmJpRXBYYzl1Z0hvSFhRS1JQMjVYditzZjEvRzNtd1FkUHFPejV6eUJ4YVltZkZhUFhaYVRNMW9xcGt4cFRuZnhNbWk1OEtidzI3Rk1RQ09aSjZPVEJ3alR2Z2lnWStTM2NEb0hvUmdjSy9hTVhzN2tCQVo1MXl0aG12bFdwbjc5VDhVOHdrenVGUS9mS25vUkhrd05waHp1RVA2Y0lXWVlZYnRiUlNpR0d1SlpFd1J3Z0xqTzdTVGJ2b2xDbUpDdTNNZUhjOVRTYnpMS2JhY3h0UC9MaEVlZU5VTWVzQnpDNGlzYkUyR2JUajRzVzZRZ05UNEMvdGIxc2FLTkdWa0w0QnhYeitFYWpTVktHY3NvT1RyNld4bTFOcVlEbWpMRVZwV1ZTVTZyTEVndFR6amZCbnh6aUcwd1lpWWErN0NmYkFEUnlmUDhlNUVEMU1EdnZTMWEyTHArQkwyV0lDbXA3Z25FZllnZzBPWFYzMGF3cEs3MVYzUUx3a09IbkVaa1VVRER3VXlrc24xVEExemo0dXNhaDYydzhWT1JFU1Jqb0J2MWxZZnI1RUd2MlVNV0JaWHh4SW50bVV6NENXQmUyTi9BbWRtdVQ5NllHSk52b2NDM1o5aE13WjFRcnhMWUlqUEdSWHdEaTRybW5nSExjUzAvL1l0Tys2MjNMWEZSQzRQZWRYNnhnU2ZUTjRUUjF6aUhlbmFMcGlmRFlSZHJrZTVQNm85bmttV3dzRUV3YVh0SmE2R0lEZ08yZWdkK2cxblpmcnFKK3liaStxY3JEUHFUS25ZRUJQMGZLRU9wLzB6UjM4WC96eVB1bldaVC9kcW0rRjRmcGJVbUt5cUFvU1pCVlJGVTRGSFpReVZJYnM5UXhVQnp4dElkWE1zdThDdUpkbERTbTBqdStrUmZqTXhzZHM4eE9nd2tTOTdZWHdUV2x5OVhBVk8reXlqR3JRanJ1QitqS2N2VlVxakE2bXJ0a1VvSE5uN25zTjViTjJERW0wMm5LeG9PQkJHT0lGeDRsbTc5eTJxOUxKamxTOXhaQWwvNC9pZjhiRk9UMm1DOGNFeHRrOWh4S2xaNG1Bd3pGejJzZTdvc0dGbjFLSUtjd3pKaDRUaUduaU5XQ2NBSllIMlA0VVhHRUZxVTlYVldLN2liV3N5enZ6YmtIZzlpaE9KazlEejB5Qm5iWVNabXJESm80MWVlSnV6d3lUeFRIYnlGQUY4M0FVU0ZuclFHcjJCTHNydVREbk5zeUZOVnZOR3pRRmFyOTBEbDVJZXNuSzY5S0d5dHM1RHdqWGtOMm1ybUZ3SDBBNndGUkhoWVM3WUtUQjdlMzJMQzZ1eEJrWW5peUNyTS9UeWgyQlpFOXl4RERBZk1Uc2FqS3NkQnZHMDNlTHQzbk5nZnd5YjhNTnl1VXR4TXFrKzdxQ2hWa0I2cWQwem9ISG9PYnIxb0pTZGVPWFZPTy9TNDE5TGpIMUh2Z1Rwd21SWlZmYVhyV3NNbVVBN1pydTBockpVcFNCVUM4ZFFGbUlCdXhNbjRONkJKOGR4cStZUUxHd3lFOG5lNHVML01OOXUzMUo4T0xhZUpLd0V0Ri94Tm5VNStoNzU0NGRQekdZZGpBNzlsaU55ZDU0cnFjTk85MG5lZWNZMXJwK25iR2oybjFQcXFDSGRJamtZU1k1MkMzSW5DdUd5dmFyNTZZdU5XWXRqVXZTV2k0Rmlqd2JMR2VmeDM5WlFmQURrZ3Jpb1BGa29VVEtVZ3Z0YjR1ZFd2QkJKNjJ4NmJ5VGJ4UzRxaEJzdlVPZ01WWHMzbjJKT1RDVlFxZ1hpK21RTys4TkVBV0tOVnQ3OXUrODViUjVlcXBuYlRwYk11V1M0bDAyT2hITmdEck8zZlltTnhQRXBiUUFkZTR0YzhNdGpHSm1NMHMrb3J1Tkd4VHQwUDhSYk1tOHo5M1BEakVKR3RQeVFJZXB6N0k0ZkFnNW1mc0lEbGRHelRqQUl2T2E1eGxBR2NaRzkvdUFqbVppWVBKb3Rmd1lRaVdtMi8rTGJTT0tBZVNaZDIxRUVCQ2VUUnRNaTJuQ0Y3aTQ1bDJEWjkyWDNJdjd0bWM3UUV2YnAwMTlONnNuK25KODNqaUE2akdqYTFSQlRFeEREYWpMQmpDUDJEY21SaTBrVWdNQjVnOGlDNEpEQmprR3J2SEJxS0k5OWsrVmZuekh3R3M0VVZHdWVUU2NISDZwSFZMb1djZzA5d1J5NkxaaGg1dm5tWU1KWThGcEtmTXVNVm5vVW0rOE11VlY2YWxUWC9ibFA0djkwN1VvVjY3dzdpU0pJZElNdURHdlNNMXNKbENIeDlWS2xaL28ybDhsRk1wSWR3YVdRbEViUXZna1plazFjTTdwL2NyM1BLNHBRcjhOcDRVWDhLaDZrcVQvanpqRmRQWHVPOUFKa1YxVTJSNmU5M1VjaklKaFUrT3FKdUpRSFkzUUdvRXFWUVlUaUU1WVhhVjF2a3JSVjRWK2VWTzN0ZWJ4dHlqQVA2Mm90WkdJSllOdWtzcUJ3SWlnQUlUTWIvM1RVVGUzUjVkUXMxeTNoWkdDZ292cnNxblZpVzRXM0RzZFFUL2dQbUM0K0xFZHJzZk1DNzBmOFhNOGdSVUpENXdPVmIxMlkreStSVzgrclQ3NnZleVdUR254U0tvbVF6MVE0NFBVTDF5V3ZlZ0ZDUGh4eVpZMmVxZE1ocUZVZUp5WE9rSjV2cnFrRUk3Y3kwS0JBY3VTRDFQNHJKeGhaY0RGYmlEYmRxMUZMeUdMWWZPTTJMcU50ejVBalJ4U3RwTE9EM3JBODBLYjVNV2VLY25zS2VjdnJlZXg2VGFDTGhxYmk1Z1kwWDEzcEpscUZBbTdvVHlHRndyRGFPV3VLRnhqcHFkMVkwVkh6YVpnWW9xNWo3OGwxYy9BWi9PWnVvem8vQUxWTzl1cGFEL0EzZzdTNzRHREV0UDBoWGhOUHNyVTdla0hSTW5ycmRjcys5WklDcm9DWkNZSE5IaktJWnlRd1gvQ2V0ZVM0a3hGTk8yOXFna05OUFB5Z3lEck5BSXZPeHlQVVdjQjJ5RVppbVkrTUdiS2xEVmRFZXVBb3hZTkFXdjFVaDRZZUNYY1Rnb21ER0t3WW12RzI3YXZVVVkvMlFjU2c5WkgveEpHNUs4eHg4cEcwZFMwWlZsNFJIbmVlVXJGOWo1SEZaTkd3NDNoOUNmZ2QzQ1hYT2ZxTTFOU0h0dmZkNnM2dk5pSEdCVkVCUUI2cTJGYWhaanVmakpJaC93Mi85Y2JWSlpYSXlVa1QvbEpWS1NNeG0vSkFlbVR0by9ib2ljdWlpSW9ld0ZudHN4OEZ3Z1FJcTBPTU83b0o5VmdQeVc5THRvNCtueEJRcmtBNUJNekNmOWJtUW1xc1FHRWdKOVBDeFFqa3JVQW1xUGJiTGg1d2E2Mk14N3hDc1ZnZGtVMVFaUlJXa2g2NmNtWGgrLzRHS2RSdDVlQkZMT0luL1N4S01YeWxidzBCRjFpaVVUaE42ZnRuT1NNeDRPQ1doRVVjMlpWSFYzWUU1TlBaL1ZnWFpQaytGNXcyN2lreVpoVG41RlhVaTgwMHBNRFVIS2pGZTFwNVhUVU5pRkUxQktxUEh5ZG8wR2dqK2haU01jTHBwcWxYMldXUDVzelZyMlFBcktyTm1oMjJ5U3ZPMFNkRWdQdERFbU5PRzRKQ2VmSUJUM0gybVVKTnM0NWZlVTdib2U0OHVNRTMzdzE3blRZWWFONkV3Q1NjbWNBMFBUUHQ2eTVPOFdZRnJvRnVHRlFndC9pYmFwS2VDSXc5OHorS2JkZW5XNEYwTjQ5eVljeXNlVkxOUHZiN2xzZ293dU5CcVJvaHJKNjMzdEswQUl6a2RQR1JWU0hMVUdWZlB2clhpRm5TbnN1QmlCUmhWQnRDR3lCckwyNmE2N09QV2ZZZVl4ZzdXanl3aFlzZWN3ejF6NFlOT1NyTE15cXAzc0lBU2ZodVdLei9wb3ROdC92MWxSeWNYMkx2c2Iva2Y4MUVxamU1d3VVVVhLMnVsb2hyZUJMZkF6Q2F3eFdKWkU1N1BMVGFZV0JSb2FEM2t5aFc4ZUZ6TTVzclFOSlRYYVd1RW1zTXUyMTJLajV3eFFrS081QWgyYlIzZFhEaW1CNS9sQTBrTUxNQUhIWnRvYnFPU1RYNWM3ZjJQam05Z0s3T0FwS2IyS2V3azkySDBjOXN4YktxSjhvMmUwTTZjKzRDQnVsR3d0ZkJVUXZOMHpzNkNMZjZ0Z0k1cmFLTkd0Z2YzekxoRm1iK1owKzlJWmtKSWJtVzk0Z3ZNWE56NFVRWVh6OXBRMEJwbWRNcmVDUkIxRjdOT29Yb2JPdW9ORGo5MStlY1JCWElGR2ZIelRQS1dSeE14dEc4R0c0VHdxUXBubFpENlc0TUNKc0NISVExa2w2SGd6YS9wOUlCOXZUdkFoQXhGbGRHNHJKQnVkMmFNQlJUUGYvcVZ5Zm9yUTcxWVA5UnZGNXZqK2JQbVlLaERlTXlIRXhZSkZLRjhFYm9kczBzWHZoTU94dzFVcndnZmlDR3VIVkxaV243RE04VXMzSGVaYUVOeGJKM3hrQ0lZbS9aR2FBL01kR01scVVYcGlvai9POU80czJ4VDc0SG4zQXdlaEE2UG0rMFlpcDhxd0FOT3lDUno3RjdTNEVPK3JiUkxXOHM2dkR6emlxbjBCYjFkbkVraWFsRGxBMXdOWmxZaDA4U3JSMzJPdFhhMUZHZUNUZVI5Y2x2RU9MNCs1RkFDdEdNbERsNUJIUlpDWXBhRzIyWUoyMEVJWlVrSDdmTkxmcldMK2hZdWJxTzU5Z0x4SythR1dzN1FrVEFOblllYUd3eWhJN0xZc0hYcVNTSlRpSnVFd0pHVHRHSDRiZDJ2YWlyKzdIei9MSXhyTVNpTElXdXZxYVNDc25GNnBIY1MyQTVKZitiTWt3dXJ5blBiVTlNMElpWmY0KzZQYmgzQThMNVgxbFBjSW1GbWFTdjczVGxTVEpTNzV3YzJNazlFcDdNaTdIQlQ5c2tNU1pKWVhya3VQTjR1bVQrWEhXU0tBT2MrZjFKMVdmTjZIVGlqTFN2dGFWbVFxWllIQWZtQ3RrTHkxVUxlblRUbHE1Nm1TK296OXc2bHBxaUFIenl0QnNpais0UzNHSVdGaGhGWittZEJJSmYwb3hFYUVyaCtyTnI4YjQ0Z2ZVK3ErMVRjWGhWSDczeTlkakw3b0JNUHRDRjUwblVmZkduSGpxNC9NQnl2eW9hYk1lZzdrdW9BUEhpV3o0RE1IQWdHSWIvVmRza2lXVTdWcWNpTnowM2tjWkFQQXMxK1Z0dFFQVTlTNXVDMldDb3RiUHJGV2dLRjhsU3lVbzMzZXRsbXVEWjF3MWp5MURKdmNhQW9TY3ZSbUN2NkZ4MENISHgvY1g3VlNqTWJnN1ovODFxbXZSeWpnTzQ4a3c2VW5zb0o2MDdubGUwYnBqV0FwM0F2ZmtMZVpraXJlQ05KdnVmcTJxaWZ2Smxmanl2dWhoajhDZ2UvcGhwSjBVMEYwR004ZmcwVG5td2FMVTZmaVJlcXZiZkdjWlhRNXI0TzRzeHBPK2hHbDFwOEdOeUNLWFoxbndqcVBIRGNSTWM5UUhWVDZTa1k2UEgzMld3ZVFvbnhadVFsUzF4aXY5UEVEd3dvc1pueFZXK3JUd3NNS1VPbEV0Z1QxVUdHM3d5cDloR2pXcUpkQzlRQ3pQYm9oNmY0RlU1bzZCZ1pnR05SS296cnI3YzU0K1ZBRDNkaGczZ3ZBTTdldUl5cXJsS1U0akpuUEVSSkVVNlQzcHNSR0NBSlRYcUk1L2NWNXpjVTNVdkJjN25xc3hWVk9XdFMrNXFqK1U4NXVSVTZ6NlhsaHBIdXpnYVRHbkJ6L1BScFR4RFNjS0ozV041eEdVbklqT1g0RXNjREwwMGVXdkUwVmZDUmFJeG1ZL2NLeTBTZzZFdDZ6ei83MnhFN29MWFdoR1dJVDVJcEg4ZllqRDd0YlFOTlE2ZTNJUHR1cHNUTDZpWlJuYllhOEVDZUxRUE5xM1dlNGg4dk1mMTQzZHcxaEd6Z0t0NHcwNlhpOHhVMENwRUI3YVhyNm80TGVYcy9WRjJMVDliWE53OHNEKzc5QXZWQWRSWldJYkF5eENVZXdCcjlrK2J6NlZvSmpHSzZoOGdLT2NMelJQajErRC9XODZiOU4wMTRpZ0lIMEg5SE9veGVmVXBwWlNGQXJld1pTdjYzM3cxbmx3M2NQZy84eU5QdGMxdzdNNDFEMzVqWUdLNFRwZ0gyUnZNc0VNc3YvYnZLYXlyTFZKcUR4NHdnSXAyOWRjekRqaGpYUHZLamE5QXVYS3kxYVFENzdUeW92ck1qTW9mSDNVa2IxWFlSZVpST3BmSDZpakhLc0ZGTGlrL1FPWkZqbVNHZnF4Y0JWM0Yxb2drVi9wb1luMEFuWi9tV3dPMFNLdkxYeEpySlhocUN0VVVnSWNpZnNRaHhTUjRBVlBwUWJDMVU4T0ZueHlTTHIrSDFYZDBuT09Bc055SklnUnVJaWNsWktnMjRTTXJEd1V4a0NmVW1YWnUwVDdVNHVZM1NBWXZlYXJUN09GeFVqWkdCTEs4cndERWw2WmF0RUc0by9mSDhkR1JBTDVGSnkzT0c2cjFFcHAvejRBT0ZNc1hNOGhsd1ozdHhrYVJnTEFGN1VzMWVhZENJN2NqU1JPRVVWcmIxUm1aTERPTjd2QmFDNlRCVWtwNUR0b241dkNrcjlsYlZGVENKRWd2RzViNmtrN1h5UHlqc29zcHhVNlZTeDRXVEZKdG0zNzdSYmdBS2k0aDFKZzYyOWJvMHZhVXlLWHcwdHVabkllS1F4bXN2elBtV2RJQUFyNmpYTTFWRFQ3WWpIdGM2QzhFMmdPK2VmVnBtWVdBMWFIZnZCTnVtdEtkYlFuUUVYNkh0bE45cHRwWmJkaHJKeFVRN2Y3bldXeWozZ0ZmN0FhLzlwckxSVTI0RE1TTWkycGlNVVlFKzkydFhHTDV6Vm55K1NEUzZ2OXF4azJOUGxSOVhLSzAvV3dqKzVSMWV0UXcvUHh1dkhPd0lWWmV1T0ppTEFrREp5dll0d1ArVXcyTnZQSzNDcUlnQk9XT2IwTUZvMkh0dWJ4OFlCZG9tM2N5ajg3RmJnTXp0VHdoc2NETWxxdWNrZVJrWjJqUXdYcFA0QS9ZV2lNV0xpZkgyb2IyUUdFTVB2ZGJQRnJpbm5wK1Zaa3hLN0dBSWFhOExHYUZaWENDU1V4bWg1WDR5a2VhSVBVSG55WjI2TlZxOW5BOHlGNWNsdWNnN2UyMEd2N1hUVE54QXQrMjBYc2o3Vi94YVhlTGVWK21DckVDaHc3TGliNjM0MWkzcTd4Szhlbmk2ZEVNZmVtWHRMVHl5NENyWnF5dm5GM3haQnlROFdMbVNiN2xaY0FlTGRZcmZwbWhTY3ExWFFTcDQ4YnEveFNPZG8rTTFpRVY3MFJmMnEwTHl1eGZ6ZU9KVzlJVEU3QnF2WTEyVFdYckdtdmNNU0piRWkrS3ZPQVVORXEyQlZZT2R1ejZFaExVY2c2N0RIK21GWFBKbVNEMXErZXBQOCtKdmxWWHo5cXBBb1A1SUNOQkk3ZWxFcDJ5bkFqWDhPdlVZTEwxekc2eFpOUVF3RXlrbitYVmRlTmViNEtBM3lKNXhodTA2WVVUM0pOTmdoZEl5TlM0NW9vVXhUUXU0L2x6RmpzQTAvWDFHV002MG9TMW5kZ0Urb2ttVXQyRGFHdE4xcEFIVVg1QWhkR3ZYZWRwdEMzTlZhUUtBb2RHMjVSTXBTaVlORkhWZEN3ZEptM21yR3lUbkJEZFNwM3lTVGFkMXZPQUdyV1hGZ09ZSndZNUNjei92MllYcVRHKzMvZjBuTjdDUDVyaGNpNnJNUzNFOTJWajZIWXRNdmpHbVYvaTRHUUttaXcrUWF3L1kxcVRXcWJTUGdZdjJHejJOajMva2IrSHIvZlhhVVVqZ0VuSjBOdEdrdE1nVktYSWRVNVJOVnNQNzY1K3E3V1pDNjVBWFdnbGFMUjZlV3Z5NUhqejlHU2pJUFFTbWowNm9ES0dqOExNVHNDZ0JidW5Pei94U0lzMVFtU3F3cWpScGY2b0Q3cnA0V3hlSzNUOW1PU2JGRnpXMktndE0zTW9oWTVMMXFPTmNpTVUrRXRLK3Q1aExvSjNCd0RVK1VoaWEvWTNjTXJHRjNFdWJRY25TV0ZvR0dlRlRBdnNQcktVaGVzbjExbkxER0hJdUJ0bi85bkJKZmxiTk1jeDVxbXJKMzdyS2lnWWJpTVZvWk8wUXpKNG1BSUVOemt6MFVuUERsZi9zZEFwajJwdGdhTWZuckM2Q2hhOEVrUUtxWHpMazFkWUdFc1F0T0hYUEZMYjNFUnNsd3ZWNmhFU0lDTE1RRmlQYUJXS3BnY2ZmSmJxZUh6ZHh0cHlKNEVtYWJZb2djUGxydHJHWndSL0RZemtvU3VWbmlYcXdaN1k1d081Sk1HRWx0V2xVTUFNdFpOMGxuaHh5Z0lzVmZQdktxVDBxKzBsdHJSK3lCY1VVdWo3ZFhERkdFeDZwQ1JxSmV4YTh3NEV0WElxdldOT25WRHVBaitoUVBZZTBEU0hoZ1FLUTNIaENLYUtUWis2MlZVOEVOZ2ZYdENDeDVTdm1CdlU0Rm4vNXhqd3BCR3VBQ0tMbkUrdWszdUpZaVZTd3doemV1NVZyaWNUR0NtcERhUVVLL2p4TklnT3EzSEJLQjIwUWxNZHN4bUY4eTUyaytWVFQzU1pZZDNYaEZBUDgzd1dycmFicnVqa2xFV01qMUZUYkVxb2dMd05HTmNYRm9sajJtR1FOS0ZTdklZYlBPZVNVTzcrY2d6Qk9UWnJaUDdLQmhWUGM1c0Mra1BVMHlqVXpzamNHaWxkNEE3bER6TkFzOEFDZnROK28yckNkaU9zN2lpZUlLbGNqMmY1WWVrNzFBaDBWTjY5RG9rN2htZEFBcE41OGlaRUNtMll5MTJ6NjdnZXIwMlZyck00R01JQm1kd0hZN1A2VkZvOWdVU21hckFQUkVEQWk2aHNtaFlMTWdCR3JsTkNWbFNCZWY2bnhxUzViZHE5U1puak9FNW5YWE9nN3ZaUExPOVY4Zk1LbUVDNDFpTlErNEVSc3V5eml5YzJIS3A1UDVnOEQ5M2tRMGw0YXBFNkJSWDdYM0lsdXFBVnpSaVN0U05DUHhtTkR3b09Hc1ZJdkJRU3orS05sbEtOVFc0WDVidEZUZFJ2OE5uN3ZZQ1diWUtyd082dkRCVk5oaEZjT2FSZWNHVGFQRFdhYUdlaGN1TGhHVmEycDlxamtMNm8vR2d0Nkg0aWVoNGRTc0txWUxOOVQya1V0NmJ6NEhBejJmTXpXUW02djE1NXNmVlo5My9LYWxla21KNEZtVGN3MHNGN2phOVltZTFYWHRnN0VmVHZtQVJESEpXb1JBK05kUnZOWDljU3pUM0tyVkg1bDVMN0dGRmUzRGVRL0kyeVdWZjc4bmF3VXpNdlE4Y3JCYk9CcjVLdlNwZnM0ajhQVWxHVW1PVENQQ1c0a1FWcVpvWmcyN3NaVWN2U2lDMnpQdWhiYWttck8wMkk5cmQ1Qndmc1h1MTRGNXJyMnZnZnhscnF2R200bTVJYURWamhZYzJSTmNtMm01UzNxS0Z1eHZHNzFnd1BCckh5T2x1L1ZuaUJiZ0k1dDBOMUpHU3U5V0d0b2cvQlp1YW5NS2dCeHZUV2tkYW9nd2pIK2hTMGY1NU53U0NQaU43eHUra2V4bEpqZVMzUWxQSk01djh5eUx5Ti9pbGUvWFkwNUxrWlBrTU1NaFFYSUNzZXhwVXE4MDdsRDdZZ1o0dG1CVU5kQjBRTXBTbXA3L0JLRVAvMlpOcGI4Z2ZTMUNBdkZpUklLSThlT0hQVGFhenFXeUZKVjNWZVM1ZHk3bTZpSUZydkpOeTNmSkJMRHFzZ2F0YTVzcU50a3BZUFhoVXJPU1ZoWjg2cUJXTHVFajJYUWRyNXRZM0NhTWhjTmtMZ1FhcERGL1FpWDlucldVRm9zNHAxSVRtbFZIUTQ4MUlaVlhlYzBmWkszcWV4OUVCUkJ2QzhrV3pHaUxoYXc4dTh6UHpiU2E3QklTZHdxMFhIMEEybUJKYkQ3d2QrT2swSlNEcSt5SjU0V3ZpbzhvdFo5b0pqOTVhTDZPb3pYYzBEU1NnL1Uxa2s4LzVRR2sreThpcU15Z0RNOGpXNmJ1NUM5TFpLOXB6bU16d1RSeTRYZ0N3aE12VDZjblZqRWMvWkIvanJBbkZMQUlRaDBaQlZyU0U2L0ZBeWRXZ3FaNStoaW5xU0hlUXhaZW5iM0FhUWZwODdkMDlDTVI5OTNiOXlFTitSNnV3SjJEWFFhR0RmOXZFdHRrdGFJM2wwU29BaUxyWG9XTVo3c05WaGtlYXZpaUFLVUViM3V0Tm9UeWROcFhsMk0rNXY2V21aVXI2bzlJNEpxNk40VmJWenkwbjZNdVNPdmlWQ1N0OWtRTmRKdFhGTE82WGlacUVQbXd3bkVpS0NCd1AxZHE0WUNOeEVrMnh2Y0hUc3JUeWZlL2xObGd0dFlBWWduSHcvQXI5ODduSHVTeE03d21LRWdYcGQxTEdmYTA1elVEUm1BK21WQzFKM0tNN281a0o3WW1za21uTHNMWFZnSXNuaXFuVmFLNXRjZzcveVJVVy84VWpxWUErTlJBN1A3cnhYWkErU3h1dVIySGszS1JFbEFLTjdnRXlhV0JkZjhNd1dMOE9lQXo4RjJUNmJKU1JTVmhnc0ErRUVsQS9JQVRBZGpuWU1kTDZxWStpWDBBM1dhWEJlR2FQY0FUUGpsSkx2R21vUEFCdmluOEtXRDRGQzNFNVRISHF3VGhlUTYzSlRjSjVnbXpQWG5uNXBhSzAzSTNtb0cyMDYycWlQaWhQMmxxS2w1Y2lpNXhmRzhOVkl0QTVxamxSc0RNUGlralpqNjJCSXYxNU10RWNRZW44bnV1RXhJRmQzWXZkR2ZpRzg2dTk1MXRQSjRxdGM0STkrbzdZeU01Q21IOTJOQjlGc2lXNFpBN25scldFN3MvdlVJRjB3eDFmQ2dnOVhqU0dOYWI2eWNmalhyMHVmMnNiTkxJZEIyOERlMFdNU1NQa0M4QWJOVXJHV2VHNlhnR21lWVF2eC8yazdpNVNwbXBZRWk4bFROQThIbHB0bk8ycS9ZZGJlMGp0bkpPSGg2b0gyc2NmMXRScUF4YVZENDUvMTFDUlAxRzkySmo3d0xISzFKbnJqZWM1WG9MUU9hVHpvWnpxVkNZc3BiWUc4Tk5WOWM5cnJHQy9ZVThMc3Rzbks2dUlyV0x5UndGMzQ1WWV5cFI4dWQrU3Z4R0hTVUFyZW1lWmVNL2dJQ0FnSURtOWEzVitldWQxMi9COUlBQXd1aHcxcEIyZ2IwakFWTEhmVlcxaDdoSy96UEM5c3krYmxraE5YOWF3ZUNWeDlwRFFWcngxSFNFNlRWM2U5ejltUU9KYTkyb0ZlUndHT01JdHhXRm9ud0cvcldUTDdIUzQyQWdrMFQra0VTLzRiNzZTRVN0SnhxVXhwMnFKRTJSeEZiUmpSaEJtNlVtZVAxMWxwQk16c3VqWmgrcjVVVFkvWCsxa2tvWGpnZklWaVRMMUIrSFBnWmd1VFFRMkF6aGZBNzRyUFNLOEY2N2xYc1F5aUp1QXg4Vm44anVadXc4NXhwakZUOXQ5Z1Z3UU9vOTN3NE1RQkUwNmJKNHFWMXhnYkFKOGVidDZyaS92dGsreDNVY2FZRzRRTVdjVllXOEZHTjNtVVBWZTEvYWplK01qSmREWkJISU53dTkrdWdRdm04Z0g2N2d3YmdtVFVQQWJjOGpoSjdRbTNmMEZvWVZHSVZPeERJRndFN1hhTmg1NURxRk42RGhFaFc5NVBOWVl2Ri9OUnBSQXJraGNqcW1VT2ZzTW5kQWpzb2FWWGdLQjA5VW9KSmtGQ3kzdkdOdCsxUFBkZFVjWkVwKzJLQmp6NWUvZEx1Y3BCbWFmWlozL3gzbDZpY0tZZFhpc0NHQVEwejBHUER4Z2o5ZkpVcDZGMnBmQW9xdWIwM2ZrUzJGMXJBWUdPUzlxSTk0ZTVEL2RWQzQ3T1ptSDd6bEx0c1hjbGphc0FoWW1jSlNqOWw1bE9GN2VoVThPRnFJTDhkK1cxYjlqSVkxZWdmQ04zeGNFL2xJRHV6L1dXMjNIMlY3cWZLZk9IQmpVUjQ3SzBpN3B5bGxaU0cwRjY4ZlkyVzg5bHgrSnc4dU9YeFBBV1VhdzF0S2tTOXlrcHU1bWNXQlNJV3RWa015eXlrODFCRmdjelB5Ui9pUmM5aEcwUm9tTUNsNWZ4SjdLWm1WeVJodlJLUVB0RlpFZDdXZW84VjVUa0E2WUxlTDVYK3dUbUg1QzNCQjVsWmtLOWpZNXJVb1VKdTJ1aTBLbE8vcVFrUjhBVlVUa3NHV3hwN0djbUo1YUhvdnErZkkzWFBGZWpRK1JYZDYvdG5xY2JqR3FLcFkzcVdaVXFsSzlIcjFnOHowRWhhRjZEdnpnUXUxVTNQNkhGanlUMS9OSWZReEhEcjdiS0tNSWlld2hHY0J5ZUlUUmxUZ0NtN3FTd3pSRlV3VVovN1RpOWJiNmlnd1BNc3RGTFNCaE9HNytDUExSdkNvKzZDaWhxeDMrTk1tNE1QZmRPWmRBZFNscUJZWFovekxpcTRMcis0QmhuUHlKL2pMbWVBOW1waVVmcEh1UXZrU1ZNT2tEcUx1TmlXbkFURTZZelFlbVZYY040aGpjTEdkTTZ2dWxYMkFIOEtNWFVra1VBZnlta204OEpES0FJUGZrangrWGM4NFpCbmFMNVhFcFRNY0h1NjdCV1NzTnhxUUoySHRnNjJPWlhHRG9rZ29wYy8zWWtIa2pFcVdsUHNIZ2JNcWJPMkdndXY1STNJdE9lRHhnUWxqQ1hPS1BIOGp4Q3o5OVFKeFMrTllac20vRksyeTdtZksrVzgzT3FoaUl1U25KS3AwK1ZjZlNSbGZhVTEwZ1liVDFGUjFjZ0Fja2Jha1UvNTl2WGlaVEZmYWRZaGo4L0xBY2RuS1JramJmM2ZkME5XdGlEZU5GeVBSbXY4dVJQOWR5clo3RjJjQ05hVlVnekE5SGdxc2RKRmU0MW1wT01tbWtBUGhOdzdEeGpCZUFQc0p6SWxkUUdVeEtPbmxiNXNtcHdnSWFabVRVcmtMQ2tnRmZaMkNZVEVHWmdTUkhLa2Z3UEYxYnhLQzN0YU8wNWNrM1lEaTZLNTVHQjNYT3ZRYU5wcFJmTEtwMWRmdk44bXRwQVZvMnV4VFhIZk1UbEJJam82MWk4ZUtzVlplc3BOTE1WR3NXekR6YzkxUVVQaFdCamo0V0JRUEVWMjB0Zk9XMFZTS1ZNYWNwUGhmYXpQM0UyK3NGNGh0WFdZaG8yZjR4VDdobjJYUXJEU0NWOHVrUktMa0I1eXAxQ0NYWS9ZREY2K3FzVXYrOEhRcFZiV0dKaDRWWmh0TmdPZEduSlJvZCs3dFNkUDhyNUErRzhDWUMrME5mNURLZ1hUYXZsVGVobkQvUmZ3bzE2NHdsK2toSENydWxtclRpcVFEbjZDMjZlUlFMcFp4aUROY3dNYTI0Y05hN2dDRlg3ektGOEFyaWV6MFkrdjJMOTQ5RWN6emU2aVNyMEo1ZkpOWUdOY3habGF0bWpWNU44ODVlSllSM1VDcG45Y0J5ZFRlVmFDTEVEUHduUFRJQkk3MHptNmUvSzdVOHQ2cWxuU0kvUk9oL1RJNWZWdXhIZ3RQd2F1RkIwdGZyY1RYdVFyOTdNQmFsT3NBaTNpT3RZcERsTEg3TmtvQ01rdlp2NWgwaDNmRjExZDBNQk4yUkRSNlQ0VjJqNUhlaC9IM0lDOURVSWZNRVBGV0lyTlRHa1A0RzYzc0oxVEdwVEplYWZSbFZxZnowMHVDMGdobzFQMUFjWmwzbzlIVzl5NUFsbWcvMytmb25pRzZqS3gyZ3oycHRGNyt4NTQ3Q21qTENLOUhZb0JVckttM0ZDbkdkSXg1aTBURmJKWndXNVBvVlJWWjEzMUJjYVhCeWdzNTFTZjdaYytraUVLL0tBYUNUbmxGQVRwU0FkVGVVM2tFQ3JmOVNXU0M3VStrKy84d3o4eEpNdWt0c052ZC9rcERTN3Vtam9uYStHT0VmMDZJQWs3b015K0N2R29KQitacWttZVphcGNzalA4RlRWclRDbXdpeFVNZ3BPVTY4bkVhWkRMUWJ6cUhRc1dqWFgxQmVIV2N6NE5qTFFHY25SQ1lPbkhwdDA0aVZ6dGNWbW0wNlRJa1hPYmhVdDhFN1lHWExSYjBJd1NFQnpvMGlZQTRsbm1lM2M5aXA1djVGb3BjQU9zZldpL2s3OUplN3cyWFI4NEQ2eEZhUUM0U0Qwb3kvQy9DSFNvV0l3a3JBSnY5b3NSZHBwRzJoTHJkZWVWRmVjYzVxZ3h2Q3NCWTlKVStraElUMnJ6d2Nlc0NRNnBRWlZSbnpqbzY4QW43bE52Sit4UHpWV2Z4Qi9qMk1veERtcHFYL3cxN1VkZ3MyREk1NmRUR1pHeE1HcWZ3eDZDckhBR3lsU01MY0k5V2ZFMm9xWVlaYTZaSVlHd0U1dFV4UlByV3F6NFJQNmtoT0ZnTDR1eDBiSEg0d3F2dEVUMnZMWm1IUWlsUXhUQ2pGVHdnbXkrWW9qNlJuUmp6a2xhWXMwcE5PMkQ4aU1MN3ZaZ0doZkY2UjFMVG5Wd0N1eFdKN1NGem1HVUhXVWNQZTRpbzBHbG9JUVhFNmp6RW1uNHpDTW9va09VaU5BaEs1NWJoaXpZN3BaY2FGUmFYRjJqV0xFMlFqZGRDZzhuMHRWQSt2cDc3OTBOazI1QWZOUEVyLzFJc2ZGbEdPV2l4SWxCM0NKVXgzSjh1ZFNkR2JycUJGd2Faa29xMnVteXNWd3o1bW9IaTRBcWlQcHRMM1FiN1JNc2ZkK1FwSUoydUxDY1pPNUNDeUY3NHBrTm91S2daTU1uaXdOdTRnRFhMZ05aSS91Wnc3bmpkdUo5YjhqcVlvSktaTFRQMUtNMU04UjhyS1NkUUM4Qk9FLzV3a3h1RXJKWUpUa0lmejJsVUI0alM4SXBzY3FtRVVqMjhPWjlDVldiNVJGUVJRY1lHTHk5YUtML1BoRXl3TCtZMDB1a2g1UDlPc0J2NUxrVnRpSGxRN2t6UnhSQ1VWbjNldW8wVW0xVUJXbTFqOFJsem5UNmFHK3VlcDZvSTAxOWRGRVdyNnNBRjYwZFVvK0hGS3loSUlDVU5zWlg5bnBtKzJIQkJoN0FVQ0pCejBvU0J0ZjQ4NW12R2FMeG53YlUwNHNLbTNRSmNqaUFQbU1BSXgwRE82ZUEzalNibVYybEQrekkzTjMxbjRDWXl2VGtpRTdpMEFwT01LMnJDZlpQQ1JKbHN1Tm5UdEtzRzBuS3F1cEs3ck9mZzlSYm5mN25lQUt2T01id3FCRkcvRExLRnBFMUlBakN2M0JaYUxodlgxQm5tQ0p1WnIzdjBZZFo3dDR6eE9oNERvZzhtQUJuL1I3bnM1eVlLNkJpRGRzWFJSeG80dnIrL21LVWs0dmJhWmRLK3lNaWwyaGVSWHBSUkV0N1pxRnZUN0lZQU9md0R0RTAyb3dWUnprRWZyWEx0OWMzQUg2eWdrU3hRYzA1T3g2RDZkWTRRaU1zYVRvaVJKenNZdEs4UlVLMXBldU1wWFBMUSs1dFhTRjNUZjl4YjlPdTFKS1p3SmMyOTVKdVc5VC9ML0hweDY3VENpTkZVdzBrd1VjN0tSRExLT0ZxcXk2aFNDaTIzUk9ka1dsWnF2eWJUcTRVTmxFeDRaZW9HUnlTVmMyS3M4QmVZTytTRUE4Yk9uVFliOFVIVmZDa0wxTUNyVTJxS2s4S2dNVHBqZTc1QlBlVDB6M3dlWm0wSjRVdXFZVW9ieG1QWFI4QVdjTWQ5Qkh3djVSNFhxWkFYVXVQcmF1R2k5SEo5NDF6M0xMN2Q2c3NDMUZ6QlZjaWxlUGxoNU0zZGEySnFCQzBleXE2YW1aaGYrUzF1alhGbFBBb0hXNS9mSFVmZTU4RTkxekxSRkwrUUNKeVlpem5pVi9UMGhqclN2eGZqeFZ1a2lFck5Bc1JPbGhTNFJISE9Bc3c1QXA4emxjeWdtUnIvZlhFN1pxLzROL3lOV2l0NDZCWmlrZzZkdlFIUnd5d1VhbHVNajRXMUZPMDBzYVgzZzBkVHNBLzVFdzMyN2xjZi9yTWdGRXZ2Z0VXQWduY2d1YTMrb2Jhdmp0QnZpc3MyV3dRL0NKZ0kxQnV3VkN1ZEFGeldBekJFSjE1T2hscHpzNVpjQXlBZVNVVnA1dWVKZ2N0RjhSN3MxMW10QXU1TWQwczVhNy9qWHl5UURIREhYandUV3MrZFRJWm9yRDJ4bDkxZWdORFN0bVhUd3BYTzB3M0ZGdytEeUprQUlUWFpKU1dxL3JSOHFISlhWUmdsdmhONWtiK0ZjRGIydW4zTk13bk5VQityektYc1I2R2FqRzIxVG9SWmw3TTE3RVNYWCtuL0RWWklLREZMa0RDekRCelFJOHpIUmEyVHNjczZ5SmE0MTRreGRsSXZ2TjJsQkhGRC9ubmlpZDNTN3BPQVdtNzltdzFyWE1yOXJIbFZna05zSDAxOURFczdBcjlpTEpjZkFZWWRjdzFxOHlQWldlTGFGRUJaNkJoVG9MZmpueGhtUmYzeS9QSFdsRnNnMVZMNTdBV21UYkdBdUdmTGxtUTZ1ZHpHRm1US1FLZER4S1N4SlV5MU1mTllycC9DNVdTMmQ2Vm1wUkxGekNKV0hUMmFZMUFRWi9RS2d0azhvVXN4Qk9PMWFUTmZlTEhnc1UvaGorNjJVeW5Ya2tJcTFvQU9sdkJTZmNJd0VEeEc1ZFhXVTlNZzRhbW5BOHlXN1c2NnNjZDZNc2Rwd01PbEoxM0ZBTDlJQ0xtZ095RnIxNDhLdlJiNDRnUGpWZHRFbzF6M0FOSTIveDFpdzBUSVJmTVB1d05tM1NjSXFyZmZ6TXRrWE5GVzBpODRpdnJoSFNsYkhQR3BjT2x3TEd0elRCblpWTzdlYThvYkd5N0NXM1MrZXJsVVVBOUlkcVcvV2l5UnR3U0JrdWx2ckY1aitaMVJoNDBlOWROUkR3TmM1TTJacXgwRUFwWktBTE1laEZwdi9IVFlJck1RWkkzK1JaWGR1aWsrVFU2TzJ4QjZZMjJ0TWJjMVZBSEZMbGlGTUMvcVAwZ1VUY2t1b2FCUDg2eEN6NEdDMURTbGlPcXlZcG9BcFl6K2VVNXdUamRyNWdpOWM2clVBZDczRGZZMjZRZ09QUk5xM0QxQ2NCcXVraVcyNTNUL00rU2RvYzlTZ3JOOWg3aVRldUhGZW9FNE91NW1GRkRXbVNqTDR3cHZYRklieEY0dTRubm9IZWxadHJhR04yTnAwaFR3eTRnbWl2RHpEc1lPWjVkYzlpVVNkRGY5NnowQXdudktoQU9Zb0xxRjlONmY0anRmWUIrVEJhTmhreGRXeUxMbXFwdVNJOXVzazBocGVJQ1pCK05xZGNuZ3kvOWdPUTBWR3B0d240bERtSUNEb0pQQVJyZnpERHRybjVvQUR6Zm5OMURjOGRkRHA4czlHN25YK3JEVmh0OGgySEtNajVDbVhrSXJQTjZqNlA2VlRLOWNvRVE2cStoNjRxWDNzNFZnNjVhc1dZQ2pEZmF5VHVrbWc0Ui9GTytIbi9ONW9CVjVPVkROWkxuTWZMcElhYitoVVlNRzdZMXgwbXkrSFVxRWRKYWt2WUhuUXcxeUxHbU80R3VSSlB1c0oyZDFwOWNYNnVkYi9Jc2I5Vk9QWWgybVVGNmt4eC9uQ0kwRncvOGJMVFVIMFZDWEJkT2E4c3pTbUwrVjhVVGlnWkx2TXBkeUY4L1FVUXFQeEhWdTQyM0QxZTg2em5IRlpDYTRtS3FsRXhHOWpHOXQvTEpaNGtHOFZxZXNGT05iVmVlNko3TjU0NmRnYzA5S1YyVnVhNFl2SnlBbXpDRHZaU2VSZ0owTVhwdVZtdXdDL2xhejk4WFo5Y0JkcFZESTN5a241TnJIZE9yZlFhSU5nWGZtMm9DdGcvU08xK2ZXVHJzckpJTFc5dzQwRFl1WnUrQWwvdk5sbGR3L1EzQVRidGV0SXpHWnAxSzVrVzY3ZEhDc3kvVGpLMUFLbDZTY2tKQUpXb0pVbTJLd2swenovdGdCSDBsaW1nSUh4dzlqNTZQZ0lDQ0EwZkhWOENGa2pMVCtPWXl4R2lxRlBKeVdKVXpaYmtmemJCVHdtTXBvTTRqRlhZUXA1UHBZZ3dHRFVJM1htT29uU3ZLd2hneUxRMmxSMTQ1OVdwTnJJcVpNcWFjQ29EczQwT0d5Zk9jSkJRWDgwQlBWWHBhRVM3M1lvTUgwaEViRm96a0QveTlyejFzVDgvVDIybVlKRlJ3dXR0WWxPcjNHTmx6cnFWWlRFSDF1SGx2MXhjM0p1cmxDbWZwT0J6K2dHMEJjWlArSEpRYktIYkdKdGNhUXFxbEtUR0w1WmUyMnY1Z3FrMDN4cFU0SitZK2p0ZjJMNi9yTWorc3N2K3hMLzFaVHI5anZmc3NjUHIwNmZ0dEcvdGVmOXUzbXZ2OHEwT1RlU1RsaDBQeUlCV1BxZ2YweWRwUDIyOWd1NkluS2Nzc25mT0dkcnQvSE1DMUtZdjRnRmxTTUd4eFdCYlZqOEE1b1Y3MVdUM21Zb0dBYXBpTUJUMkVJYnVuc2RScXdYVEk5aHFpWkhVK3FYUk5WbGtkTkhwejRUdjNNREUxWmlBbFVsbnJ2NzNOS1QxQ0RYZVpRbWdoOUhwYU1GWmFEbURxV1cxeVdIczh1MVVkTmFkc25xOWdNQW10aVBZRVU5eldZQ3pZVVc3NGpwTnF2Qk9DTXA2Tnh2WFVFU3k2YlhZQys3eG9DaEY5ZHkrNURIWlVKcG91YThxT2hJTnVaRE5wbG5PUExGT0hQQmpBSFE3TlJEbVhPek15blNBQjhtY0dBM0Q2WFhKOW1MNXoyZS9KSVZVanVEOTB5K3pEWTA3WVVSaHp6UitwcFQycHMwN0U5ditzWndZMHR1MWF1UkZuUE5NWHlKQWZRU1ZmZXVNOWRJd3dwVTRRL3dXUUIrbEhRWHVrUlFXQ0MxQ3VjaDJZZitldkc1a2RnN0tVS09ySDdRWU85VG5hSHoyQTgwQVBZNVhvTllKb25xNlc3eHc1bFFOWUM2aWFZS3ZMYU0rR0YwVExCa2NBQURVeVNlS2hwZm0xY0VROTY4MkZqRHV0cGNDZWVUTGVKZHpwYjA0US8rVHBKVlpwenU4aVRUTWdjdFF0WGMwdmJSaDhCSytOa1RuUVMrZ1RwMW9UVlZDT1lmaXdEU1kwZlV1UW9CcDgrMmowMzVQQmFsRjZVRlJSdjdpV0hvME1pbEYxSjZlZmY0VEtOOWhYWWd6WGJlMXdjRkZnMUM3WUtqby9DRGVEL2dna3l5dm51Z1ZXQWlSM2lCa083cWlXVzJ3SVMyNCt6a1NoWVBPZGl1ZUlQMThZK0FoeXNXZVgzTVlNRXBHVTlVS2Q5YitEb0FwNFNtQ09janUvQjBpN2NFTWhFSUh5QXc1VTcydVEzV3N3cktVSkRRYisyTnE1WXlOWVd6OWlDUGsyVG95Tkg4U2hSM09TeVd4VTJEL1NMblA5c0FtMktKeDZzTy85c1B3MHp3MVc1cXhES3NxbXlwRERObDhCcUY1UUNKSUdhM2tYNVJRbzVabVJqQzlaL2VPK2t6TVNGZEZQYStwVGE5eE1valRiUVU2Rll5eHZDSHFTbWsxQ3J1MmE0RTI0dkZBd243bS9lSTNBRjVmRDZxUllQeW9DRDc2aXhDUnlNNTZZYU0xdU00RTdNU2Z0QXZZM1dPWU9NcHVZajkwYlNUMFpja0Y4MCtFOTcxVDFVTGlrK2s1VTdGV2I2V3J6TTlyOFd5czRIZXlLbk9uMmV5eklWcWlFb250d3RFaUZJWWtiWkJhcWgwOC8yMEZXUTVzd2N5SlBxMitycVZkYjlZdzBlUTVRZlQ0Z2ZacG9OTUN4N3NKR1F6Z0N5cTBnUUhsdDhmdkwxRVNiY3RIeFFKSlFhT3AzdGRtayt2Ty9MVFpuT0xoSkY2L0RCWDU0QWRRdzhPN2dmR2lhUklIWVZSUExZN00xekxlTUZNWGlSVExiWWZBcVkvblJkQ2QrYjloZ2VKUWpLanBOcU4wY1p4alhia0JHYlJNMjNLWUc4cHo1NStlOWhWUVhiRm90QWZzZzF0emR0SnE5Sk9lV0lsVWhBMUx3R3JJQ3BiUjJtZ2huR1FqcmpUUGFQNEErcXlJR3NyWVB5VnFGYXdZUURnWGpvOU5EMVpIK0xNYXR1dEhUL1VrVFlUcGpvNWhSSy9ZNUhGTk1EaDBsRmFhdU9JMUxtS3hwcGNnbTdSQ2sreEdUM0FISkJhZTkrcXd4aTl6R2k2djQyc2FmSlBKckNFSmQvQ3FqcFpHb2hRR0U0VE15clBzczMxOG0ycnRHS3I1RVJLWTNrbENBNFhQYXFZYWhZRFExWTk4YStWSFFZK3hESEdWSm5PRkE0eDhKWlRVZ2ZkYWJtblY2Y0Y1cDF0VWVKYktRRlViQ3R6WmZiVlJUbzhjZmkrdGNQWGNsbXhmQlpua05BdFBDa2M0aWN2UWRQdytxTVlqQUkvWjFocjB4a3hhbW5MOXQwcGt3U0dWeWRsVVJvZlZneUF1YkdHUXdSYmYyemdBZFhWZDlQdG8yTGlXdnZkWWo5OHVLcmd0dzZkTERuZU5zWitUaWs1a08yTVJRNlgwV2FWYzJVYk9kSnhiTDVMWmJzeHVSa1VHbnQraWtJOWhmVFR2UEFoVjZCdFNPMTlOL3FXNGpNbjc1SzBJbjJSVG1HaUtxb05aSG1kNzAzVVArQzRLbG5NKzlQYm5HejhvY3loSUtCS21HTlJqQUVUa0dUWEluYW9sNXdlb3F3U3AxTThGdnpHNEs0NDRSRjkyaUxramlBdW5VZGxveGNxci9RU25nZ1VPTlBrTFZMRU5nR2NXY1ZkTjhSS3BraElMVDd3WVZ6M1lMRFVZUVNRcnNWRmtFMVd2NTFFbjJmU1BSS2dqUWtwMkxsSTVHT3B6WUNhUDZUeWp6ZmxtcFQrNGppVEh2eDY1WFpCS0FmOTZtUDZtSGdVclNQcVIvQzdWYWZIK2NxVlRLQUhCNUZiWTlpMUJzMGN1UG1uS3o2NVBUSzRhbVpROE03RFVKVURSTUl6K0JUWTcrMzVMbDRLZ3FtQTZITXBBejBsM2NrQUlmcVBINGYwRnViWHRIMnVCek1pYkFqWnM2cXZzTno1dUc0V0UzWitUQm1QR3lRekdlSTcyRysreWkvYzI2bHNsRExmRWYzQlZrckNvQSsyMlBRZ2pZNjk5SHRxamxYZlZXa0lhbng5Rms5cEFGaE0yZmVoZkkxZGFla08xWXIvaXZzbWlNTW8vMU5jMUJqMVFud0Fwa0NSbTF1NUtUNTFWa2RSSm5Sd2lidHZ6VzZkcmx6M2k5YUJHNFJKdk9abzM2am9GUXEwUlNFLzFCSkE2WWwxeTZPelVwMmtMZ3JNcGxoQUIyN3BsOXlsU0hZejY4RkZZTlI1cUFGR25ja2VVM2FIKzJ1U1FNaE5qbDdDMFU3RUJjMnBoL2JsVkEydXpRWElXTzlzWFl2c2toa2FMbUs2dXp6eDRTa3ZQeFdPRUR0dnNnZU9wZVpuU3RoMXRvZ2xVOTlqR2JydEQyaStuakUrK2FucVRRVjNQZGZzRkpmeU5UZjhLVWUwQUd6Tm9EWnJxeGdVTHNINUNyZVNIdFJpc1QwbW5VVWEyeHJiQlh5QVE4LzA0R0htOGZ5S3E3Tnl1OW1ySm1mQ1NTUWtST3kyZXdQYVR4STlZbVNBeHpiZGJ4WnpTN3BmWWRvcG8vUTd3LzkwcWwwWjJhVGZOZTYzU1BsV1loNFBidnJkcVo2aUkvNFdlbXcybVBCZjZBNXVKMWZCbFd0RUhLUk9wR0VwT0dFcGE1NW9OL2pFTnM1UXlzU01UZTRwKzZVakRHMjU2YjduTWV2cEg3V1M5Wi9XMzFzYzM2RzhjalVEUjJ4QnNNaCtJbHE2WmdENEE5ODR2OW9kenRkQ0Q5MkFFSXpxdDF1SUN3UU52YkoxczAwSm1ZUzJJT2xzckd0NTN6amhvMmF3eE1FL0RzWVJHMGpYVWcvVGNCaDZROHEyOU9RRWlTcFU1SUtkVmxubWJtSGNid2xFbFRxa0wydVN3d2UyTDNuOU4wT2xDNVhibjg4aGpNdWUyODd2Rmh3L2MzL2VwdzlFZkl4Q2kxa3RlOXBvQlpBZG9MRFJGdW5OajdXWE5rTHZhdFVKV0xWNkxpR3Q4bFJxZjhyTDNCbm1LNS9Fb2RwS3NOOVg4cXFUSUtGVWtDMXhoVmlZUDl4OVR4OHo3Z25tSWtzcDJyVnNWNkxrT2cxaUJFNVdUT1Z5aEhsM3hMKzFpVG5jWXJ2d0xyQXBjclVPLzljYXFyMWIwME9aTm9nOWpWRGs1MG80TDNBNi85dmV1c2h3TlorMWo0RzZPQll6WUZRMU52U2pOWVltNk81OGNZQWluV2crZkJKRm80N1NDcTJyT0hRVGphZTJTSTUwYXBNQkNuZGd6bm1mZGE3ZWdOMzdBUWRqL0RYaFQwakRrYnNkRlBYcm9Md2xCaGRYQlpDeGl1UHdSaDJqT1lJdUtlc3ZNR24xZ3pCUmdzL0VZUWxSVm5pRHhUdnowM3ZvMStzK0V5dCs5cWdUS25kME5Yd25MME1JRGVONUh4QlJVYkhaNExUdFZnTmg1bTIvZUtTZmJEbXhVQjRtUDcxM0dyTFlTL2tQVGVhTndUQ1IzbFpPQXhvSGp2YVRZZjBjSjVUVHpHTi8xZEVONTJjcFJtemkrMHpEME5ZZkFNZzdhYXM2ZnR3aXJKMytuYXJaTEJtSTBjdEJUamsxMDBmR052SmFKVGVtZmx4QVZsOUptdUpzZjNVL2h5UStXekY0UjFmczFDb0lkc1o1QnkwNGlyOUNXbEVsYlBuSERkRGJTTnZ5YXBTUnM5ZWVRMHFDMGM5QjIrazJTbmhEMDVzS2tiYTlsM1pLcWxuNWZYb3djWVFPL0JmVDZxUE4xbjREWkdvMHFabzFWM01GRXVZM0g4cXpJNmJwNTFnZ0JXSVJNYzdDbmRxb2tVVDgyUkxrL3YxOEVYakR2Rkp5N1h2NGVLS0I2MndvQUdYY0wzRzdhTnpIMTh1L3lGaVVKd1NUWGwrWXplbitQdDNXKzRHNWV0ZVd1NldrUXNaUGpXVDhVVFA5aDJkSGMvWVY4bldHRko2TWhvTytnKzJlREgrMmRzcWhCelY4UmZ1M1A3R3pEWnNia0tjNXBhaGFWWGVhTWRiYWJLS2dWZUNMNWxWWDVpWnJGRm4rZ1RQOVVsSm5lTi96eUJ5cHl0UE1MbmY5ajEreWl4ZXN0R2tvaVB0cUhWRm9TanRjNU5mZjRwR2JKQU5IUGo5akNlYTNkQ0FJMG10OHl2WXM3OFUzZEFteWphQmFldmhOb0tod0ZCRVovcklocjk2cktuVHUzYjZoeVJPR2ZqOHkzK3pQOXE0M25TSmhYR3RVRmoyOElvOVB2Rlk3SHIwVFo1SHRaUlpzY0paMEZ1RW1FQ2V0NGpiZENCUGhUd1ZMZjBFRUN3RGgvRlZ5TnNoNUxnclRZa0xINHBkT1NycHVEdmYzZTZqSkYxQjFSb3BUdlFoOHB3ci9qOElHa1lOQXUrYTlrbEw0cCtacmxZeEZjQURCWmZIOHFDVDVhNWZTOVJvZHdvUktwYlJXaDZrTGUwZWtGYytjakRmWUhRSEhJOEpaWGtvN053VmJ1M0RxN1Y3WXFLWnJ5Yk9BWjIwcUNpMFJlMVJBRkQ0UUU0VmJpSlhyNnRUVkRQY2UxdWwzbHErZHdsbEFnV0krVFZJTnNpOENtTzh1M2djTHNGOTZiYVJEVHpvVkgrM05MYXFCeUZrNk1LMFJRcHBFQUMwSmM2QjEvenEzamg4MmZrcU5Obm53NlFuSERVMUpYTGtzVmxwNUgyME8xN0RHUXlwWFRUaWQ2dWVEM01vQUJmaXJIMVlzNzA2VmFzUTNwNlNJemthbVcyU1hiN2J3MGIzTCs4YmJaV3EwUEJYZ1VyeWJ0c0VCMnlUZnZFK2lVclREK1hienN6L1lvODBtRDIrY2luMEJDSU8vWStERjU2QUlSSk5EUG5NSWNXQ3NvdTRkMWhYekVad3J2aVdzbXUvcjdkRVBWcWpwcnoxQVd1R2M1WGdqdmcxVnZLdVZ0cUtsMWlvdnZtcEREL25NajdCb2JHbTNXUmpHR1VORkF4UEJkM3BuY0hwbWdHaGVuWHQ5STg5aUxtZm5hTEhHdUJsT0lWK05RSHZQMXYzcDdGY3lNUkNnMW1Nb1NpbDRYQ3d0VU5zSmxoa0pTR202L0xZeFBCT3VaQkdOYTRDUXZDUUIwVmhGZVorK1NSQXBLUlRwMGxYT0dwU0NIeUsyNXB1RUxrUzhweURuWmlIeVlLL2x2MzR3MFhzeUgzc0xUVENsRTZLOUxOa3I1anoyRjRNRUJuRTVkZDBKMDhBUkxaU1l4VXZqRXpCUmFuaEFHQXMyckp1dndBWkdzRkNJaXNsU3U2eWp5Uks1WmNuVWxHakR1MG1nMHlTYVRwYTYwaWJBbDlkYmFVci9wbGNMUUFxYTJkUXlnWndaSXZ1bGp6MmptTnV2Wm9paXYxMXRDL09xY3RyWTkrT3FURDVPakNQbGxSUy8xNkZzQXRhTHp4ZXhnMXdheU91U1ptdFQ1VlVYM05Rb3hwTEdoem5CL1plYk1QbXd4eVltS0dLZVRnc1U3cnNWY3RaU2JDNHVTUWJVdXIreG5tT2ZvcFpDK1BpY3c3UFBUOE9xdk4yaGJ6dWUyU2VNZTJZclJNdXZlcHVtck4rMXVTcU1xdU5qb1VWWW1NUTE2cGR2bEdKS05XNHArSnUwYTVLNGtsbzJJVDFCL0VqQ0hIQUpkZUZQUUsrUUtIZHhORkt4ZVNzZ2o5bDJwOWg4YTlybXRFckRTOGs4bFlnQ0lPRVk1a3N5Tld0dTlxZHR0U1BFM1ArZ0I3cGt4VlpkclhWUWkxYlpLOHhMbmNsSUd6WXMwU1h1U1M4YnNWd1FzVGwrZk95QU1sdnhvaURxN1JpTXkvVmJBejRWSU9sclpqSTgvdjhhaGNIdGVVL0cxTDM2bDNoVEFJV1U4N0ZKdnd5bitWSDRJWkZRaGtYeDhiKzdleW52enpNNmZWWHRFVkVNVHBWdVloZlhIR3kyY2RxWXpRZm9Ca0ZwdjJ3a0JvaXl5SDBCbFJaNFkrNVJSakY1K1V5dk8xWGJLVFFwTVFRV095ckFTQmxZUEx6cHUzVHl6VForT2NCelN6SUllRGV3YWthRW1BVVVlRktvUk9HeTkxWE5uVkgvc1ROWWFBazZhcHlhdEdMeE9uSlNBQXBkcDk5V0RhNWhNV2JIMVMxaEY1RXVUcXBUSmR4dmY2c09BU0QyWnAwWlUxV0N1OE9XV3JxajZDOHBybUtlSjdtMHlzYzhtRCtsL01PRjRiUDVBYXFkV2pRL3d2VnZtczlySXBkalpkSTJQaThQTW9ERitmdUNkTnBZTXdCT3pwcHcrVXU3bE9nWXhoYnFZdWVDTHFvMHMwQ0pKZlEzTmRpWmxqRVBzNStJSnZ6WWxKaExCWHh0RTJpeFI1V2o2KzBuWWkyZnA1Njlzc1NJVGtkYnJlMjdLVXAxZjhWMkhRU1BhOFhvdkF6NWZJL3FpVHUwNXNRNWVGeHRUY3JHY2NFcFpMdWNRc2kxN1BlL3lXUE5pQktFZG05R25LMlRjbGRVWnBUM2VBZTkvSXUwNVJ6eHJab2NuV0J2a3NIQWNWTGZUalB4Y3c3TGp2YjNIaXJxNk90YURUTkhGaHZRdS9RWHBVNUhXcnM2eWhGdklPbTdCYjZ6dXdUNHpsNlZBNk0xcGxJTkNiU0hxT0lVTHNaKzNHWVFDbmdCWDYyLzFiS2xGVmJyY2ZHNlcvRlZ5cnMrOG1FbExQa2VsdnJTaGYvSHJSejVzbS80aU9SMi9wWm9ic0F2aGRHL0JtZnVPcUd2LzNHVVdSR3lOMURSb2VKbHR4NzFaWWplVnBycUhvcm02ZzJjeG5hT2lyZWkrZDVpLzRzaHhja1ZDc2RIbHF6OW5ScEgrck5jNFJ2RUIxSXZnbHE3MTRCMmZ0YmFzOGdFNjF6c2tGL0MwL3YzRFZyNWVFYXZudGhxdDZxUnIvRlE0M3FXOTRTMUttMzRUZ0hBSm1YZCthZmFISWdqbmRvNFA2S3h2SjhxWlVGeUJ1ZUM0MlNQalNScnlKVGZpS2NOZG80bXhYVS94WWxFSlBodXB5YUhmdDdFQXI2eXR6WEw2Q3Erd21qaXNlZVJnK3dWWW5UN1Y5Y0QrRld6ZmU5VkpHNk00djd5a01TeUZGanRVT3ZZYysxTEc4YUMzbkNyeUxrYnprUyt6a0JnVEoyUEtMaklIb1JrVkhtWVlBSzIxUTkyTkpORHZ5SXUzWGQybzM0bVpjKzgwRHNBdVBwOVkwNGI3TkFWSkg1OGt1TkJja1FZaVBKLzRCNE1YMm9RMlRGT3RHMnVYd2t2Vko3ai94dis1RnJMd0ZTZE4rU1Q0OGlodE9yTzgweHVBUDJpc2pJUkwyZUg5VFZoTGYyZnVIVHIrb3ZKV3BHOGphc0ltbWFQZXpnUzAxVUMzUGJQOXlPNnFqNXBKc2hwTFFUUjE3ckNmNEtrOVNmSTRUckYwcWVKN2hxaUJKaDhUTWRlOVUzQnc4bFhVU2JBOWN0SlhhbVR6bFpxL01aQWRta1pXRTd6MHVzNFNWenhXTHJPd0h4Yk5kbXBhYncxVTBQYThLVzRtV1N4MUJCd2pONE5rRXhvSGhlRzJxeGJ6R0FaU3J2QTlrM1dCTWNKZWxTemc4dUV6UE1ocGRhWTBRQnpoMGE0UG1JZncvYnpOTWhTWEdZNk1oSndkdmFOQWgydzdwL2hEenpoU0JudTd1aVJHWkF0dFZRWjNNbUZzVmN3NFQwMG1lTGNBbzl3NnN2Tzl6b01nNnQwSVVsY09oZGlwOVE5T0w3bEFlUXhZZml1cDVQSUI3YW0ycWc2KytYL2MvZElVUzMvYWxxSUpHZjJVUlE2dDdjayt5NjJ4eXRCQ0kycDRibmlGMkp5a3RnQ3h2WGtMTHRPaThwSnpzdldZZzNSSDJvM213S08xUWF3d29kSjZiL2ZiRW9ieTJpOXZzTlUyVFZmYlBDM1k0TS9EUzkySWx3M2VtUHdmUlpYQ2NGeXN5cEZtdU1mRVJKSmdNS3F5KzVLNjBrZWl5QmdPZkx1ZXAxOFM0bFNHY3VOZDk5VjQxTkFGeW43SXQzYzloUXhNTWxvOXBLdzN6MmZEVURTMU5xeWR3WU9aWGxJN1hIUTUrNDlwYTAyOTVGSUtDYi9rbDRJSERmMkpBWFFQMUxaWmtmdWdLTStYVmhNd2FwWVRjM2dtdzJ6OCt4RU11SytpcXdKZkdBUUxPYmZaQ0VTamI4Y3JpVTBmMUhHQlRneGVuL1g3aWFFcFRKNWVPUWF4NWo3Z2FKZUIrREdzTFNzNWhRalo4cWZKRXd0dE94d281aTdmVG1rbUxETEFiR1R4dkJlbWxUUENSR0Y4VzZybDJ4aE90YVhocmRWTGFrdHR5NVJ4UTc0SVpHd1dQSDBpTnRBQlNFNVF5R3JZZzZ4QjYyQ1JOeWtXTSt3RndPclo2UExaOHA4YkYydk5NRzV1TGZuT2p2ejhoTENvditOaUhVUFlGTkpzeWV3YmoxOHNpaUNnbC9uTmlHaXAxZUtqN1BtOW8xTGw3MEsxaU00Y28yS0JSWVdZOXpmSzcwYks4WGkzTlJlMEt0WXpaNlU4YUw5L2R3T2phY2c4M2VGdFFHZUJLYXc1d21UcjBUR0Y2Nzdma0c0L3hzMi9zRklrZy91QkUrbE15aWUyejdkZnpBdzF5Nyt6Vm0vQVpTRkNzTUh4dUVTbjdudFNXMzg0L093Z1FsdVVyVzlkVEJuWGMzN3oySnpTRk1FZzdVaW9uakx5TmVtRFhQRnI2RkdHOHd6SUx4NUlpVEozQ3dXUHM5cW1hYzNrblcxYkZSbzlyNlRwY2syeXp5QTZnQUpQc0dMMGpGc3dkS0U3bzlQdHBJTklhUW16MXlKLzB6b3NobFBYUUEzS2crWVNuVzlzQTdzNUdhQ0l4MDRjdGxKY2diU28rVDl6SUdqcWRQMXdOTFdvOVh1T2tMN1ZScjFBWStCWkx1L25UcEZ2MGtlZ2hkaVlYNUs1UHAwSy9NMlRIRGVoNjNBZXM4NWxYc3ZtdGJBUkxtMmYyWmVHNzlSamdPRHhacFVNOEJCVVVLTU1PR0ZRTnJtQUVIM2N2N0xQa25uS0ZLTGtMY04vYjd0OWRRUU1VMFBJTzFWdWVHYkY0WENMUGRsaFVNWEtQL0NhZFVBMElnMVlLWWtCWUlacitOSE1mUUpPWkl3K05FYjl1ZTUrTVdjbTF6dnNTODRMMTRKcFdSQVE3M1ZVSWIxcm90NVlHdmFPNm1acTBwMVVHWUVXZmk0Y3lLanY4SW54UHRiOVFYVUg2WFVjcDdUbmVEeWVJNmRDUnduUytReElCN25jS0xpT2VVNHc3ZmxTOGVpdGxEVTNmMkM2YXNWUS92MzgxUnZWR0xwTzNVV2lQYjJ5TithQ2RMQnV2SytSMXpCSGNxZEZXRnMyRjFTdjV5MDNWRWpNUlJzcUJUY29ldjVTaDZjVE9DMFVQRkRkTXZVKzRXNi9vZy9UQ1hmYjUwTFVFQjAxYkRVbUc1bGwzTERjZ2cvQXp0TTFpQTl2Qit3c2tYSWE5SVZiMkhQaGFGazZvK3BnQ05Kbm1Vc0tlTU5NcVB2VFVTa3ZYYzdNeDlNc2VUNitSc1ZWOGRiT1lSMzBUd0w3anRkMkEwNlk1MUlJWmx6KzVoaER2aGk5ZTBub1RkZjh1aHdxamVSazR3NlVqV29FdWIyVEdJbVkyNGJBNUFHbFU3bmZQUTBidStGRzQrM1A5T2V1dlUwSmxMOURSRXc4Qi9LYm54R3FDV0hFR3g0eC8xLzFSbkt3MjFsTjRySkFuRVFISGZXZ0IrWFpjb09jS1h6V3MrTUQ4cjJEQTBOUGFrTG91L1Y2OWlhaTVhV0hMZ2JHNXBtMkxUaWN1cjlNSFgyZVcvZUh4aFFKcXFZUWJOZG5UKzFjaGdhSVVGNlA0UFQyYWhzazJ4SXVmbmlCUXRVTExpOWJWaHA3YkpBVVBSL2tETWZzOFVwRkxJU2hFeVVQRGo1UnlYc25nV1VyOGpQRklGdDcrOHRVcDhYelhRTDUwYnl2OHgzSU1HWVdsZTlpTnkvMU8ydlJnTGRKTHNsWXpnNFp1RWNrbk5qOUdlY1NyTndEeml3LzA0Q0NobVVJWVdFbFc1V3dPbkYyanN0dHJrZ2pDVGs4ZDhJMlJqRDluTTl0ak0vTzZMWWZwcTJja2xxeTNPaW0vazIxOEwwS2FRbXJZS2ZweVVGQnRvSnhxNjBHcGx6ZWJhYjJzRkc2b0lQY2hrOUdoV1Z6MlMvMDhoZEMvcmVmaUpJS0plY0ltNS96VllvZDl1a2ErazZWRE1LMnFZdmlmNFA3RnJLWmVFYmZqc2VOS0VZTWRSQ2l5endpRG9uL0pSYiswUkZXMFZGV244VmVNQ1B1MStma2ZSbWcxbDJNUFRNT3BiRWZmNFZkRkYxMmpHOXBzL0IzVmlIQVhiS1pYbEhHUzBhWkVzRkphZFJjK2FHV0tQQmtmTFNvTUdWVHgxeHAvTk1SR0k0WkNSTU05cnBtdmNHSVpqS2E5aEF3RldwOGhCQjl2WjIyU1k5RFNTUGQwc1lCUWFPWUd5WWdWdWlEaVZUQlBBdlRKNnJOSm53cFYxSGMrWjVKQXN6S0s1eWRuK3BENjQ0L0daUUpxYndEMkF6REFxY2pXdU5XZHljUkVLSm13aXBBVGpiU09UWmdCelUyOHZXdHhxSm10VXFYT0FuVzhOQVVVQytGaVRYQUFNbVlselFJaGJ6RnpPMW53Y2hUNXM2QU50WE9kcE9nSi9RY2NZM0RrMVNuTFNYVUN6dUJpVE5FZEpMMkVmVUpYVlBIcjF2TlRtOW1KdEo0R0NkZGQzeHFkQ1p0a1pMbFdnVTBoY0NwdUVtbjAyM0hEVjdYZmRGYTdpTXI5S283dFgzWU1RVkM3RDMzNjF6SmcrR2p4OHBXdk0vc3BCaUgySk04ejY5aE5XTmFlVVYrdmFNancyRWFseVFUSW5jMWpUcXNwN29QZ2FwdGNTa3FqNjZNdDg4SS9EY0xwcyt3MkpaZmw1RTI2bWY5a2pGd0FWRmtML1pOV0Z6NjdkSjZ2bFdhTHRRQ1pjRG15ZEEvOCtFVmNmL1duOWFPVG84NG8xZjlmV0JINGg5c0xBSVZHVSt1VUZRaHJMeXpBeEFGUHE0dWQyUDdjZE91cFF5TDN5K0hIMFVrbkZrUENZVmJUY0s3ZXV5cFFaSVNoNDRSemhVc3UyTlBmNXZQV0tSdWZwbG5pM2VVdTZKK05NWGRmdjNpWTdQVWZ6Y2Rrek0rVEJKeE4xK0lRSGxGWkdqSXhzUEdBckJ3Y1A1NnN4Zk5WZHZzcWV5QSs0MTJpZjBGbHdWbnQrNzRndmtWeFVMcWVnME5tbWUxNndVU2dLRUcrSGx1SEdLYnlBQmt6NXZudWJ3N2hVMmpMSkhmWHo0bzFpa1J3NzRGZlFVRlpOajY2NkNQcnhiTitkSXFTRXpCdWk0Q0FYbVA0VVJBVGoxa29QbzlvRi96SkxlcHlyN1FMK2VISTEyT2VkS1MyWnFHcitDajJCdWlrRlBwRDR1L21GWG41V0svSUN2ZDlEc3htZWFyWVU3L3pXRDNkbHdFTVRMZ1dqWEluSGIvU3F1Y0JHNHg4bEN4MUFoQTFpaWRKbWhXeGMwcktlVWdMb2s4MG1qYWs4L3RkbWc5K2ZiMjFVNmNLZm93T0xaUkdjQndLUGwvS0FxUENqdFRqZWw0dTh1djlxeS9leUZKSUVGZGU2VFlMWWZ4NVNRSndKNFVjOU4xTDJ4b0tyMGRPWFlsTTR3bDdtQ3lGWk5aNzFkcGZBQkY2TXBXaWwzeUZmQU9tbkJoNkU1ekRzUGl6RDZLNzBOWUN6L1VpcEpNUlVHek1ZOFVxQzRFUm9sTlZrWUhsS1g0ZEJsTGVJQ0c5N085eUQ4TER2QkZRSUdaNUhYTnlxR0xackR5T2ExVFY1YVJTWGliWUZUdHBoeTFIS0ttUWxBT3RTVmpacWhRSDBOdm5maU1MazM5QW53eGJMTzF5ejUzY09QWklrZHB6SkhhQjMyNXRZZkVoYUZLSmExMXpRS3hpMEREZzVybU92QVh6M0R6b2I4b1ZsMFFqSGlHK1lJSnprdWllQVQ0OGI2Z0VqRlpYazA3d0MwTFNOMG8waHNuclNMQ25XMXd4eVVycnJTSGtRQklmSDlxQjlDeEdrUHdqVVUxUVBMYVRkRFp5SUo2eUEyWDhVTzVNUzVLT2FYV0JlY20xVnZ4aGRYa1cvL3p5Sm9pZWkyT1VSbUdzSmQ0NzdBTkNuQWFyT21COWdQS25PWTRPRzYzUGp2NGw1RUkrUGFIbXdFTGNEWXc5RjZ5dHYyaTMrakY2YXJFbHpPTk1ES1U0SnpiaEdDUUdEa0poNXFNcXBjWGFTR3Rvem9FOUZYOXZxWitsUnBqeXhyOVU4cUJUdmZkMlltOGJTd2MzczMycnVxNzBrYS9vNi9sME0yejRkWGttNmQzMHVJTVNXc0o5MkNoTG1kUnVsQVM2VHJscjZHNm03Z1ZyUjVpZ0I3bE42ZmxZbW5HZGEvRDI2MTNYczMzNjFld084ci9DSzB2UjE1VE51ejdqUzY3aW1CMWVnRm9SdEExdi9IQUEvZHpUWks5WDgrY0JQeVpZSjRzMTFlZ2o2SzlUamdTcEZoVlFHUTBFZDMrblRJNW9NWm5ON1p2bGFwTUowYndUNG1yb01lRk1kQ2tJczRBYVlVcm9BMHhScmNNVHV3a0xuYlpWOXFuamVnM2U1V2gxS0tSRTFrajdUUXFNUnlWdWR0WTJyU1E5eFBiU25QeDZUQVNuS1BJa05kN2t1OUFmZFVPZFExRDI0Y0RycVlCNEZnT1EwZmZpRVc1cEVXbDdsQU44MVY2eloxS3JzWG5HL3ZPM3B0QmtpeXkrSFhjNXluRUpkRHRVNkYvSTRWSFpQbjhEbXhKNjdYYndwU0RhWEUrOXI4aTNaVlJHK2N1bU5jNmg0VEQzb1BMTWd5TzFvZnhtUCswN2g3d05ENE5CRC92MllXSzRLUkkreHBNdDByR0NkS1hoS3owRWtCajNkOUg5b1gvMTdZMDNvZDV4bzBaR2JOSTI4WFB4OGRUUlcxbHM4bmZLd21jbUw1aFcxbWgydXpEWnJkVU9HUUhyWVZ6TEYrWTZKYjNad0RPRjVQbUlLM3BlNEtscmcrMDhNeFpFVlptazVVczkvb3FWNFA1NzNWNlkxZEdsbzlmWVNnMEdsTU1pc01oZzNlVzZVdS9semZscUUzSmRzaHEyazRrUzdoWU5ZaEMxZUlEUVB1Uk9XSEk0eDZ6NHRNUVJwYnFUTEhmdTZyeWg3TnBESFFjYVM4RitlU3lmKzFwQjBQTS8wU0ZLRGZSQ25uWE95c2tCQXZGWVcyeWxyV2ljQkFERENqTDlkemZDcHYzeStBWngzTVpnYzM0THNPNS9pOTVRVzQ3VkVOK1ZjSk5menZld25WdU1XNnF5MDRZNUxwMFZIR093aE5JQmViZkZFM2FOU1g0L2pKTUVBNDB3REJVWU9mbWRrTmpJdTRoelVnYjZzMkM1a014OUdUa25meHVGcTZwRDNaR0pOeGZ2VlVuL1JFQy9ablpzdEZLOVJaWjlxZE9rbDBRWUg2ZDYyMlR6S3JJeUllZlpTOC90SlV4a1pyTzdtc3RNbXdwMUpDSjRERFBGeStnRmVGUGhEcG9hSjlZc3M0TEd4bFd0Y04yYWVYemJ4VURNZ240TzRYVjk3QmFUZFpkYUgyN2xjZHZ1Z3dnNnRIaEZWbUJnUVFPajZQLzl5eWJPcjdiTjI1Njd2dEVVbHNKeXVvZEV3YXJyY3Y5YXU4UTYvdUZxa3gwK1czWlk2NVhTZlh3blBnQk5wOG56N3FzYXdNYW1sQXNVQ2NwdDF4TzdFbTUwekhiaHV6c2lRYkVOdmcwSEpaNzF4Q2dFcnZLQjZtNmtRczZ0RFJQZ1c4dk9SdTlMbTdsY0VKQUlEbXJrZzdjMWNHengwUkZMYjVVczVnNmJscEkxbWdzdWEwY0k1RG10Nlh1Q2M0L0hlOXZYU1lZb2doZ3hqckRZYWNYN3N4enVySlFZeldSd1ZQV2JZYTBraTRsQTNkSHFwZ2tXYTFjVU5NN0lKL3N5VGUzMG1Ickt3NndRci9TMVMvd0dpZ3dyN0o1U21INEZNekpJT0w0SUl2VWdhWXprOXlKd0l5amgvMDlkQVovYzFHZFN0b0RTMXFPRlZ6YzJ6b1FqYXp2UE5CMmx4cE8vb2x0M2lUaElaViswTnBIYlIvQ1NGL2VxT1lvWXN0cDY2YjBOcnRHcEYyQUs4MHAzZCsvbi9WNmZpeENXZjhDSFo2UjVQTlhzakgyQjcwSmpXODFsK2RFd25hMWJrN0lLRE5IV0VKd2lmaHp5V3RIZHRRTG1ZMm15ZGgwY3kvTm1RVlpodFJrNkFzWU1kUGdMMnRLeStzNHkrOHp5Qk5KTFFsQmhvcGE4Nk9pUDlrcFdZZmNiTHRKTURYdFcvamtEQ2JYVGJoa1l6Ty94WmlxQU15TEMvaytIY252MjI1SVYzcEIwTmZoeVp1QmVNZUtVb1pwQ3VRcFZnL3JPREowL3FJU1NrTGJwU0p1aVowSFVId3JRWEJPWUx3RnFNOHdUaWVGQUxLNmN1OG5XcnVBZ3hmSWlQdkdBUHF1TnQ2d29JQWdCNHpET1ovTjljWERSeHRHTnpKckx5bStqUmFsUjZPVnUyMTRZOE9wRTdQZzh4L1JVcm5uQ282aGdyM0hOa1p1SHMrcVduMFVZWXc5K3FhVE0xdXMyK3lFTGdYd29ReFdlMDlGV240ZG1UNTJ2eDgxU0lCN3BGbHNaQ2xkNkVHWXlUSytaQzhaOHAzVDVnVmZEQUY2NFhUNjI2MTNGUTVicVIyOGlkZEFMa29TaHd4Y2ZadFkvZzB0b3V2b2x5cEtEQnlqRmREMFFUNG95YzUrdjBzalJPQ3NSbnB0T0NWNk96SzU3bWdqY2VPVFFjV29hSzgyakMrempmVzhZd1puK3dvRU5UVDBDRTlGSDVlTjB6VEk2U2w4TERqeHE0aHlTdDY0SXNyclBHRU5HNlpRcFF2RW1TMkN4M0Q4NGdWYldJSTVpWjZGZE9jOE1xbXI3UnZkbnBGYm11UFFRbTJuRDlhZlN0aS9HQTBIb1ZVVjRudGh5WXgzKzd0OHpNdFp2K0hXSWZtNmlrVlJmOWFObGIzajErS2VCUWRoSEFMLzF4eUIrRHBHRTlkVXFUSmZpVXp1b1Vjd3NMZHFqY1A5ZDVrbVl0UHFveTJ6MlBKNnVsZ29yMXMvVWloVUhGM1ZrVkNCa3MyNGRZRDlXbzRDbXhiVVkyTzB6WDZMODI5TmNRclc1L1hidXhVd0R4dGlxdDROamQ3dEtrbGhmV25NSXdMWlN1bXMyNGlCa2R4NU5yMnh5NjlWQVhmbld5N0NJbzVuN1NNdlNMN0xjdkk4Q09XTFJGY1dYMjIvOFdwSDJ3R0hTT1dKMUUwbUk3NXgrY1dJVlppdUZqYVd2V1BNdW1uZ0wxd0NpN29yUngzWTdaSVZVZHR0UDNzTVFWZzZDVCtUMGtHcFIwZFF6UzY1bFgrbVpCUWlPL2JIckdHL0hBTHJHVlhZeDNoQWpwZkwzV21BUWcxdU52TTJGRVFoOTkwRHBmZE4vc2VzRDhQbVExTXU1Um45RUdyNFJkMlk3SWJXVk12NWxsQW1Sb21KYklOVXo1Sk84TC9ZUG5aSGdiVndGWWRxZ1VwOUNZQXRrakxRV3ZOckFOSmxHd2ZOKyt5aVExeGd3bzYrQ2pYMVpOSkkrc1NtazJGTzdiNjlhbFNkdS9ER0J1M1loSVorSnJUbFpHU2RwYXp3YSsrajdQRXF3NzFsRU03Zkw1ckZGejl5T2l0NEp2bFNnNHlqSWRjY01MNWJWL2pWSDQrTVZBN1lOdFNNUWRHT1dNZVdFOTJPRGRyRnJSMk84RGhlZzMrVXBwNVJTYmNPNWJTeFVpcDNCUllmY0ltM0FyUDl6eThEdThxek43QzVJREd4TlZyR0xnU3I2SXVsWkpiSyszeGtVcFpyM3M2eXFnUGtQQTY5ckgxZzgyM2NFd3U1Q091MnJ6eldseGtrS0RYVVFyZDVyODgyOXIzOStGSnBOSWpGT2s3dGM0NFl1dVZXWTFmWUFIQnFzQllabzhlMFF2c1lFS29PNHRhNmNPT201MVM5U2lTV1JaWXpFWExyUzNQSURFZEw1VENCNXF5c1lNSXZqSzF3eTlIWFYxUXBNclY0SW8zWklSQWNRcktoakE3NldjQjRQSEFaRDhjTlZGWHBWUVpIRWRVN0NEN0ltQXFZYWIvT0VRcVdUYkVna3hjMzYxdTlhVGVTaVlEQlBOc3RiUU91UEJkWVhRbnRxWnlhN1NNcDM2MTA1blZvK01tdzh6RytycWRUMXBFdWE1dFBPc2ZaSjFaYUtHdFNQRUF1eDJWMlNwWjRKdU94WEszbWtiSWVjSE1kUlhuazFaYzNDQ2FsUk5vR1RjelBpbHNmOUNsRDBRbHJGVlJpZHltbkRLVjlGQjBqVXdnZGlCNEcyY24ra0ErVm9NV01nL3pSSENmR1lrYVlMQys5OVRsSnJGdEJiY1kxKytwZWd2VHk4bWNCRXNONC9xVm1jSkNFc3ZWMjNSc3ZaR21iZUNDM1I1TTdvS20yaFg5TTM1Ynk0Vld5Qy9ER2hGSXJQMllYVk9VaE54STk4WmFVVXA4ZEdMbDJYM2JlYnZqL0hMLzB1WHFhYmVGMnRsYm5DSWllUFVaQlhsekluZnlCakhQMUE2OW5WTVNDSE9GeXBEUjdjeXlOcmdZWndVUXJ6MEs0eCtqRmQ3WE5wclBWbDRMaXVJUVU0Uk15d09TMnNISWpZazZBUGRFRE9acjZBVi9MVVdoTTF2ajFKSjF0QitScFNEVGpGMHEyY3FmMkQ0dEEyaGZ4ejRobEtrRFZaeG5jSHFLSml0MXVHM3RYbGEweUtMMXpzTENHS0FQMW92aGdKYW5weTFUMUQyQkZ0ZHpiWWlYenlaT2pEQUxrS2ExTzZtcVJJVWRLSkJZY0lvUDhpWGxOUm53Ni9QMGpYVmNicVhXbWZKRkJsU3hmOXRFS29MQzJhOU55WFdYSWpOU0t0elBGTXNHMXlXMlRLK0owWVo3anVsZDIxSk9PbHFEVktaMjR5M3Y1aWlaS0IzOWJIMlluVVlmdktXZVZzcDZSRElobW1hTkdFdER3SHJtbWo0bjM2dzNUR0hzVmJpRmJFb2JQSktWRm5USTFUWUFMUm94RGkvWVA3dFhXbkNuKzV1TUVoZE9sUXdLNG5pS2JvU2RUSWxyL3NHaytFempyMHlmU3hkbEZrMHhxY0xWb0s1ZU84emZ4dngxeEtlcjd0M2xYeUdqcU5aUVB3Z25KNTkrbDJPSDFnRjgzS0VqTUp4all3Yjlsa2h6RkdrNGUrSHNPWU83UkdwZzhJcTFGYnVBT05xWEZMUEx1SVVnL2hlRzNHVHIyZk02MEVGbGNoR0ROclBqaTRKNWhXRlFzTUFCSDNCVHZYWkFIRlF5NjRxODVoaE81dzQvM2dkZlhDSEh3WnFqMSs5OWx1RUFoU0hDOXVDVUxmUW1aSGcrMDA5aEhRalkwZ0NuRlJnazJWRzdaRDI4Y2VLelI5a0NxaFpURURaaHlOcVdUcHZMWDVmSzRuTlIwQ2NhaE16cDhhWkNIamhQUnFYYU10dnJ0RmNKdjd6S1pOTjlFS1gzcjVERktvNHZxenAxUnBXOWE1TXlrdGtia01HY2ZtdWZJSEJkbDdTZFVUSFk1R25EMi90cWFqRGVXNlk5SHV5YmE3N0xUbkdpdXNnUnc0RUxTTzhhVTdzTy9kSVZjSTdmUmhGcHplcmptcEZhcnRHck9xWi9kTEp4a3hJRFpKbmhpblM0Z3NkMzdsbVpuNDlmR1pGdGQzelA2T1Z6TUE5MnFLeFlSLzRYbUlpMDdwL3hTeWtMRnFpdEtxdGV4YmN3SFpGTVhCUlRZblVIY0JFam50YkxCMngydkRoMXRyVWtuR2xnV0J4cXIrbWtmUVNoVGZDcU5ESWdobldXRlpTTnM4OHZ5WmhzYW9mcTJlalQ0VWVKL0xxL2dIeDdxZjVKYTVwRkU5SmZTYUlPSXA2ZGFhVExFUUF5TE5FVXd3Z0FHUVdlN0JVRS9wd1RkMXo2RWkrazR3b3pFb3pYZDFMRVFnTFFXSDFzNjk3WG1hRUhrM3dTUkxKTzI2TzM5YXBvbGI4M2dYNDZ6NWROZTlMRFBod2lTQXpRRFdBVEZXMGRhZUJCS3pRT3J3T09pWHgvV0h2N1A1NUlmSmtYbGkxUU52Z3E4dTBKRkg3RDA4M3B2TzdvSm52Y3FXdm1ucGQ2cHp5NnRINGF6WlRZQWtxanJmb0RMWmdxb3I2UjNWVzh2OG5MRzlQVFpvRGxCMUFWZW1KMitFV2RFWksyaVdxcjVuMDZJOVdtUFdFZFJKUG9PWEJaOWF6RHl1bXpVaURmOVVCL0VlSlJvekVnQzV6RktvMzBpQ0RmU0NpQmdLME5oNXlVZlI5VHFZdzNnRHh6TWJpNFRsdGZadHdMeUJTNmJVbi93RTJwams2Q2h4OU9PMTJqWlFFeFhRNi9NMnh5UWxHcWwvS0pBeHJhS0YzQWdlRWMycmsrS3B1NHBYV3dFOHBXTFFNZUhxSW8zRVlaWW5xdmpUS2ZhejhzT1R6c3VQQ1B1ZTdHTEo1ZXJxRnlpeUtXMktqem0xUnNTVDBIWHF4akRQZStyUDhyYzQrZWdObHpRVUtqTENNUWVVWXk0ZzJwTzZKaHllUWt2M2J3Y3crd1duQmVYVGZXaG9seDd4TEtxVGg0TDdFbXdTcmFENXZpOVVPeDY1TUdMditaYU1tL1lMTkI3SUlMVXZrNlVESDZhVDNZVXlWZDNnQ3YwbjlDVFgxd1MyNCtZbTZqRlduNlo5RXRtQWN0aERlLzNsYUI0K2tkU3Y5SFdvdlRBUGhVSXVYcFdOeGlJMWZoMHVreXo5b3lkbGU0bzFlN1NXSWl4RkJvdE9jUVY3NEtCTG9qbDE1Y2RxR1hSRGZWQ205VVFWSndia0g5ZXZGdW83bzVHOFJmdmZoWUJaR0VVNzdxaUtQMVpOUXlnSktPQlNrZVBLSjh4bGx5eHNOMVJ1Wkc0WHF5NUx2MXV4dFFqRjJqZWwrTTJiVmF2VWVDMU5xQzhTTHdLN1NvSCtwRTEwMHVVQmxUd3NOUERmWlJvUkV4UTRjRTRnTld1TTdvYllxUGlHRVZQenpOL0xXekdhODczeWdUTzEyb2hiUGRZMHU2cUJaNnZIQWNVbHIzWUV3aVlFNHl4WmlZTFZieFNpSjRDMGdTbXBreG5nanYzYUxaRjIvR0pqclo0dml2SzJrZmxpWkdmaFk2TDdqY1lhZnlIQzh3SC9rNmp0TXBkb0w2SU14TlA1Z0xncUhRdTFQVm9vcE5QVjRDQ3VYdTlTNTVtMXVxbXpUS2VDUkZhTUtaZ1VYQUUwWURmZTdwd2VIamJhM2ZUbENLTU13ek5jNjEwMnI0S2pMU3JPUlBOUmVLcy9TNDEvSGpjV0NlTldoYlVERHVSU1pNRFdZeFdaR2RYTU1TT0ZOaGZMTHlmbyt5YnB2bC85ZXpKazhBdDhBMXRJTkFZTHFwRSs0Z09jQjRPSWVHZVM3Qis5a204YlJMVzJHTUFRazlSaTdVOGc3UGxkWDIzdjVSOW02WE55eXl0ZTdCTjhNcy9BRGZ1am5nR3Mreko5RWxlUXRESHY1dnYrSlVSZUpIZU4ydHBzSnJyVW5DVFVZTGIyR0YvWTlQcDNwK2FVWm1ZV0NNOEhwZFRiT2pVcTVsNGdIdFUxSVh0YkxZaCt2Vk5oYldzdzFjUDVjNUh1M0g3eUxMbC90Uzl4TFZxd1dKdkE5VERxUWZTSnJSU3VzVDFvMCsrbmRVRDFrRHo3blhYaFNPN1pCWURsY3lKOGJJZ2RxQWM3UGQrZWNlUkN6eWFsSkJvOWM1SVZrNXhEd09zeEJKTmtLdkJidTNtdUE3ZU1zTDVpQXNJY2Y2UTFXWHVLMS9KWWdaU2tKMTllc3dveERyYXA1dTliV09Td0dRb0dzeThhRUhwL0t1SnBUSHIrZVJPa2JoQ0hEQ2piS25EY1BGVWdmZEtxc2dEdVQxejFucWFSZTRuTWEwVXhteFFZSFNiUHBNNVJxdmtuQ2FEaW92RS9NdFo2ZGlqS09uYWwra3J2WEhXZVVoQnZMMGxPWHZNMVE3L3U5NXA3RjFCZFBGMUtGVk83OExjbGloQXdtWTRBaW8zN0o0b3BhaWNSYWJwek94NXdmSTROL0RnaW95OUNKTjhzVEphUE1nOGJjM0NaaE8xNDR0RW5YaEZRTlpjL1QydFhMdUY1ZmQ5WXM3dG8rYmxYSm0vTEhManpnTUY5blY4ZVM1WEs0aUlDVGtMWTFXcGpETmVUems1aWlFekFtaGlRc2VOditoTEgxRGhiUG9BY05BQktkV2dFemlCektCam9rWVpxM2Jaa21Edm1yelVtajZ1aVZqVmp6V1NSZmZmMFlQWWtVL1FKWVVJM3dHUFc3VlBBdnBzNzh3UmY3QmF6Vk9pcjVpMnFVcWFhQ0luQlkyVW9kWkpVWFVWcmRBSXhTbk9IZ0xETmxVZzAzNFBoVmlXeFp1MThvZ0ZOREx4d3N3cDlQWitWYWlOVXErLzNvRGVRRS9VaGJSL2NVc3dhb0dmTWVzb2NkcmtqVzYvbmZLTFlCUlNiVjJ4Mmw3dGUrWkszR2dNSGQwRHY5eTE1bzlDQkZuRXJzYmpTNkdRWlJmeFY2Z0ttVnRlTnZ2bmN6Y3Vac2JNbmZLYiswUEhEb1ZkdmZwcUdnN3V3MHRVK0NlYlRqdmNJNWdhOC9aUWVFbG93STVLUHRGTTkxZ1c4MWxHZEVwTmlrRi9FR0VndjBSbUZwNXhNYzFqK1B1MDY2eXZLeTI1N1hDcXp5eW54RUJyRzVpekJxNHl3K041MklZM2tRcmpFSmx0RHFHWlEwQXdzRzJFK1hpNk9IMWp4RXViTUs3cHBGQ2FEL0hGamxmbk01TjIrUGl5Mk1zTGJtU3F4blRScWJjMFdWNDh4US8xMUNXcG4weHBhVTM5bXkxK0txS0lMbHFhNDVGT1lqeGNsRVloalZucGJCb2hhaTFlMmkrUUhnUkJOUzRYQnEwbU0yajFDRlN6cjFRc2RxcFNTdmxXYzgrbXhwcWtaWGRlZ2JsS2o2UHRjdlh4UEFPalFndENML2RpUjdRRmJ2YVY1TUVVZXV2VThIYXNHSStDcDBNV0FCVEZiT3Y4L3ZOaDRZckNFZ0pyRzlna1JGQ096TG10Tm9GWStTcXZPNVpEN1N5ZGJnSmNzclBkSjM5eFNsYWpDeE9nbEs3dkExMGwzL2EwbDZNSXJlc0FQZFRWZEV4SEtTZmI5RWkzSGJ3VkZPT1NsdXNmSklSc3FHd3pkM0RBWEk4a3RyaThiVVFMUDBvWnRVMCtaTFlZS3g1WG0yak9IbFJ2aGZ2TXJiNUxoSUhPSWxNZHdzbUErVnV6VUc3Q3NWY3E5UXpFNEN6Tk40QzJLTktmNXZKWCthYXpsVGFzWHVpMDNyMW1tYXFHRWhYOTJSYkErRHlVaG9MbzRXK2UxUEg3VzI3Zk5OWE9tcGxoT29KZFNRSld3RXM0c3lGTHFBNy83L1RKdHhOK0NlTWZtMHVhM3ZBNnpmYzVNY2szVVNXaGxTakhsTjNyY2t1cEdHZzB0Y1h1dndxZE9yQm45UmFGQUR5TWg1Vkp4TVJEWGxkMk9VNXJ1dHBrVzlmRzhDYTYrc1lOL001cVFIcDhBclNrMWdLc1NnVnJQQi91cXA0UWRxVWxHWmxYaE5uOHI0MTdPdy9rWUw4azJrNkVJVm9sc3YvMnoxSnV3L2ZadHJDMW1Xa05haWVBWEV4cC9qRS9HYjhjWjdNTVhZTWRtYTE4dFc2dmRWYy9qWUhBTmlMblFiNFRpTU1QYTJiSmd5dHlZNUtVb2FvdFlUM0oyLzMzbXg0SDl2RUo1dm1RSW05anZYait0WndtOU5leVZIc21mU2N4Uk03SHdZQkxtTTFucEhtTTNBa3A5UEFnNGJDTDhTVDYxVHl1QVllQVJMS1R6R2pkYzFjZWFqUWE3bGNENTJLUFhCbHMrd25pT1l3Y0ZENGx4SkF4dUFLRjFvR09pN25YZFVUb2hDa2pVd0VZMFZzdldzRWRza01vdEpzUHNIUzRabUdtb3VQaXdxMmxrcHZKcU5iaGhha2hEM0dXMTV3Y2hOZEhsblhYNHkzRzBZYW1MRStENnQ1bzN5UERXQUNvS3JCdmYvVmg1bnJMbENNMXJRSDdYUjB4bm95SjVWaDVweWJLeUNKR09aaHRPRTZKZTcvbjd6bElUaElUcXlnSnk4em1TZHRwSXB1Rm9ZREdaaTdwQUhnSnhReWl6SHRKVWROMSt2a2FYbW1XaUthcHhiMzVDQ2pKL1ExeHJ2Y2Nzem56MGpLa2pUSTVsWWNYVktTQWpEOXRZWml2ZU15c3llKzdXSTAvUmdZZmtaR3pITm5IdkgrRlg5TUtRR0JBdFNrYmQ1Ymw1dUpYUS9oeGRiYjJnNWJNOEF2emgrc3A1Z1ZqYjZkcXU5cDRENmhBckNrU0hVak0yRS9qV0ppMzlkeTl1dU9YbnZrc0FubVJhZlU5cFh4ODd5SERvVFN4eDBqS0U5d0t6RDdxejFkYnR5MlhINEhub1BDNjV6cFUyMXFtanZrVXlIZjRvcW15ZFM0eGs4R1JLT3JqN0dSVExLUCtydG5uZWV5Q1N5SFBxaTlnUkxXb0RXNWhhNEJyOGNUaDYxTFlBaU1Hak1IOFpRRnZPRm5hZjY1WUNaL0JsRXcwQzhIb2M3UXBkNHhCdEZNTjdNTVhYNVZiY3VndEJYOVdRbXIvYmQ3eTd5alRNaTg4YThETENCWExkWEVjdW9DTFNabTdqRElmbGZNaU5FdGpCQnFzTVlhQjcweGxtZmF6eFNFVWwxV3BxcEJvWGN6MHlYL1RTUEFybXBLc3ZKZkNzVjdjRFBtY2ZmcklDN2NVczVKVkw2YmsvR0R1OW9mNFFBU0pqYkdyYlppcFA3MUxaWXRjZUU4RWJCRXB1ckVsbWViYi9BL1ZmZk93cTFxb3ZhTHkrUWtjcTBBZ1lWS1B2QmhlOERsY21BZUgzVXljc2pIeHFicmEwbmJPYm4wL0FBeXBleHlHdWg2YThHRXJ6aHNCY0VhL2Y2L3F6TUlCUXRYZ25MbTZWZGFrYUZQMlEwQmpVRDBxVGxzbS9jc2k2RWswKzFublZtRXlzcWdpbkVxR0pQYmQ4MDZLOHJ4SmVObkd0MXIyUS94Mi9hcUdPcGlNcUxLVU9SQnlqQVVaQWdibEJTNjhsd2lEODZWNjhCd3gyalRKODlwUUF2am9aQ2hhTXk5NmRlbTI1Slg1VVhpUzZPVHVEeWc5QU1mZ3dWR0JTeDk1VEYvMzFPMWZpVFVsU2o0M1FGQXhzeWZEOWEwWlNNRWFrSGtjUnU3SW83ZHIrZzBCK0hlcXd2N3hBQWhRRkhnWlpVQUZMTCtmSkM5N1k0emhkZVdMTkdJZnMxUFhaYisrNERQRFJzdStFb0NxbTlCcFRKeHM2TWpPN1JyZFJORTRtbndQN2pHUThZbDRNRFZ5N05oUTVJWUozVUllNTN5SmlGeFQyc2VVYVl0dXNiM1o1RHBCSlpwV3FseUdEN3hQOUZtbUZGdVJ2TGNGZXR2QlF2RGViSUEvYXo3L2RXb0VHZHY1RlJtYzcxdmJTMjc5WTByNjVpWHAreEM2cVFJVFRONzVTeG9UajU1N0lFNCtGdjRBd1RVUDJabTR3UkRhekt0RXhFcXNVaUx3MlZDeDExamUxR1FRVFRaNjYyd2p6RjV1akJKZm0vcFpaV0VGUktpcVAwQXA0cVpHdmFEY204Z2NHcmUwQW5hd0tEd1lBNGM0VHFZK1FLeFlqbkE4alJCekpJOS9vRXZsalJyZjFWcFRleHZVODV3WVEvVHp3K2VuZmR5eGQ1Yi9qK3lxVitFbnZZVll3RWlBeDViMlFKcHdob1dyWVV5d0tkRkFneHZrWFJGVFZkYm1QQm5GRDhjazdvNHBZaUtQa1d6VklmSGxpUmFXcEN2bVhrb3VaY2JKNTdYVXRWMFR3eWsxSDc3eWRNeVYxR1p6M2JmdGlKaEN1VnFtYTdacCtKYUZKS3A5VlJUaWtJbkVYZURoYm1rRXN0cGFuY3V6M25iUWtxUWhHZk0zWENJcGluVnFmWWNnUDlacE91YVRqbEdUV1EvMDJTazRnMERHcXlhbUxBVm0xdTZRdWFudUV3MWFvaDR4a1I3c0hpQkFMZEZsZ1VhZ3NwUUJZQk9jTnc5WlpwTEt6S2VPMEpaTGhzNUdyMmRvdHNJdTFiODBMeVZRa251czh2ekwyMCtIWXBSaXJLOFFETDNrMUpuYWpyVUw1QzJvNzFhT3dEMHpBZWpiL2I4SDNvdEtVRXBzTzk0YkxjR2xXL3BVeFpxWVZWSE5WbHV6WXFMeTlvSkU2WEg4bWphZEswYldURnFMZjNUS005R1hHUnEyZk5jazQxZTlHWEdMQWdIZGNDOUZQVlgrbVEzaG44RW1nakdyR1NZalNndjRkLytZdVVIcWxoRDZ1bjhiRmhvNTdwSGhTQndPWC9Tck5rM3JyTmJKZ1dDL2pxeDRrVHh3SmRqaldjSzRyZkI4bGk0MkgzYUtmQ0FFQ084ZktSZ2luYjhKMHlrVlg5bE5TakpGTlF4dU5FMCtaSVdlZUFlWGZldXowQ3d5MGxHVWxBVzFQTXdYd2lMdHZIZ1BhRzkvdWUxZURtK05jNzQ5SUlidHlORkNJTVJJdGxRdzlvamtTQlc1cVlGbjN1bVVBSVlwRlhvNDNYd0pLTVpibVBQVDNLR3JpbXRCam9FREp6T1EzcHlsemoxWS9iNS85MTFqNkFzMFEzYzBxbjd2UEhvbEovRlo3Sjc0UjlmcFZxaGMzQ2tHb2lxSGFVRXBXSjRMSkdKVDhSQ09GMHV6ZU5lQm1UYjkrTjQ2YUV6cndpMFE3MXJDWGVmUmNrSTRlM1ljZncwKzd0Tnh0Q1Qva1hUSWZmbG12OU0xVEVpL0wyQ1lqbDVYczNmVXhNQi9nTm9ja1ArMzQ5akMrUnpSby9wSUNDWit1ZFhrTEJNTDNUbVY2bW5OM0tsUGRZQlR2ZzdsMk40QWRUalROaklTTHNJNmtRbXFteFJ6YVVSRUxrZGR1RVJYZlNqQTJNYmlWL3ptYWdaMW55anFtdjdnYW83MEtHdldsTG1QNjhIVm5EVFhOQXFndUlobSt1UTczVUZNdjVGeEloT2w2Q1dzcW5COUoyeDVCRmEzbVIvSEVWbzNrTUJIU1EzK1JHbEZoR052eTQrc0hFK0JacStPbVpya01rOFdHSUQ2bStQUjhyNXo2d2ZPcjBwQllsN0lOS2Y3dUF6bmYxTm8xcGZOaXdwZkZGWmxBTFZTZGdJeHk3QlZ1RC8zWTlOMktnTE5EUHE2VHV3YzNLZEdPd3dMdmsvcmJxdEtpK1NJa1NjNHRJQ2FPcU54cWZhY2xGU3hZR3NyaUZsQ3FzODFhUU55T3NWUko0dTdvbE1OU201bGdxcC9nRHR4b2ZiY01zK3p1RUhPa2lJZjkvNk1OSWlSMEdUc2lQUGZvUVFpZXhOL2R2RG1jbGYxV05RcFdXNlYyUmVGc0hsUWk3Y2xSR2JscUlSWXY3QTBOWDF0Nm5wc1JCY2N3RUtBelJVOWZTV1ZEQyttNENmL20yNDNpNmc1akNiS2M1WENXOVErOVRBTFZwTThRdW9oT0ViME5mMCsvMy9NWS9tL3RMK1ZGNUhBWklmaWhNamdmNjVjS1FBb1VGd0VFQWdnTU5kNm50RTNXM1pCUHoraGVvTUVTQnIvZmE4bkxab0tDOWlzeG5jbDB0a0xzeG9vZ0pKZnB3UFM2VVgyeXdYSE85WE5WWksxbng1UTZWcXcwMmJEUzhzZnFnMVhqVDExaXpiRGd1aWlZWkErOHpzTHZ6OW91VUJBbFM1S3pSRFBkcTNpZ3h3UWxJVWttOWNLWVRQbkI4M0FUdWRXV0gvMmErek9UaWdGSXhBRlZJWlQwUnFOc2hyTDR4cCtaclpVSkFib3ViUm9ubDlkbWF5NXZpYTVKR2s3U200V0M0QzEyL3FzM3pZakdFSEJXSVdrcmw3cTBmV0dPemNpdzF6UG9pSG94a1NGdGhZZVRKdXlTZUIyb1hZWmdSdFVWVkZ3Z3BMeVlzWEhQREtiSCswSm45U2UzdnkyYXdRWE05WUpvcnhzSkQ5MkVYa05qL1Z0dFByck96Q2w5ZHg2NU1NRDhwUDdjeEFSb01EZU9Db1FOVW9iNzNEQWp3WmVmZW8rV2NsNFY0UCtPcXN4VnlJSTQ4TEE4bUV3RUIxRGd0RlVxeFVydnRib0d5dGZndWE0azM1T1R1VFlMdEN0bDBFeUVGOXpBU1FGRG5jc0pmRmhpSkVkcG53bVA4OHB5NjBJdktWd0h2eHNkOW5XeEZDaW5qQTRrbkl3Q3B2K0R5T1pOTlJmZDBYQVN3MG1VMzAwQWI3NUNxMVlWcWFLZXpXMnlzS3NHc0hscHQ4dUlZempIZzY4bElPaGJ5VkM5MXVoaEF4aHFZb2RjdE05UlFaQXR5MjZVT2Z3ZXQ2L1RuRm13WFhDd01ub0xTMzZKMzMxUUZLMmIzR08yZDJNNUY0TkNnNWVWNmY3WVFtMko0ZVlpdTB1R3E1b0VlM1JrYmFmdTNjTHkxN3pxL3VFb1d4aVhtQ3M3dW9LVUY2VjRlaWVLY2hIUlBOakdzd292Q29xdkZFa1hCUmNXTjdQa1NYN1NOVkNBVHdKaHhnTlV1cUhVNXlFUGJZY25SVEs3eWJIOHlSTURZa2p0L1FkZnErWmVTZzJxcnUyblJMMnpGTWNISTFnYTVmYXFpQTZWd08yem9MZUYrTmF4T1J3S05HdXlUWEFKR2lWa1hFUDUzMnZHRUVGdzBJazdJNW5VZVJDeHlwRUlpcW5rTGd2QjdMKytETjJuWS9hSllQdEhRRFdUdkhuK3pRbldEZS9CbUlvMTRmajlZRlRZWE4wdGlLZnFsenczNzVEUFlWSXZjdEtvUS93ZXo4bmE1aWU3ZnNoazRrazVuWXhYWThCT01XZnlDS1RiZk5VVmNCMTd0YXpHcDM5WEkyY2E2aHU3OFQ2dVNiKzZPbENqbFNiQjdyVlNhWExDT3YzZFZEMiswbUYxRmx0WUpQWkh3djZZMnhqajRzbWpmMFVjMm12aDNqS01EV3hwUVdodXIvc1BqdVFmMDN5dkRsZ2QyV1l0bk1TdTAzWk5LVUw1ZnFld2oxaVdEN0RTMFpRbGJkcm9RMkNyM3BSeG5GZ0pYVG1CK3ZNZnMyeHRzY2ZIQmhaSzhsRnZIVzVBd1N1RWFVMzVmV2w0NFBCS3Z5anduc1NWUVBwWWNMRldTRlpRY0JicmVKcXZrWEhsNk1XT042WVRJYXFyUXQ5Vy9ORS9JWjRxZDd3eG1sVk5SRmhiY0V4cUordXEzeEVlR2ZCU25uUjQ1QURmS1htWGlMdW5UNkpFbUYrSnFyS04wR0tvQ2s3QWNtdnJWYWFKMU5BeHRldUp3anZKQ3hQTWxhclBKMVA5NnZqOGhLY0NsekxBK0xJMWNIbkplT2dhQ2RIUkJPMStLU2toWFVabFA4OXhZQk9xRldYakxIL1oyemczSlNmdnRUUkJoTDlKYmZvNGlUS1IyelA4azVVL1Vxd3RIem9YOFdVOGZDejRHWWpVSmZXeVJ0OVhRUXZ6NHpIM0w5S09QYVBOdW00eEE5SGdsRnF5elhBSDhNZEVyUFhac1RwNTliRVNlUXQ4akNweWgzNmxjR0JRR1UxbVZMRVNyN3JzRnZjRDhxaElMMGhNVS9HS3pyRldIWE1rQ1UvS0Nuc0pzTUJ4OXlPQVJ5WHVqZXlkem5ra3dRY01pQ3EwZkdBTGJVNGNnRnRuOVU2MU9HbzNwZlA3MmtManFmV2hscGZEVVI3U0p2ZHM2ZzBKWU5GcXFzL2tkdk5maFBwckJFbHBTajJGYURrLzBJZmRFcXNmNGlVMWo1TWIxcFNtK2lTTkZ4dEhmTXIrVE1ENVdOKzhsb0dFNE83cUNmVnAzMWw2KzBIV1g1YTQ0R004U3ZsR3FoWlNxclhDS21reXFhVDF5L0p0M0pxUzk5NGZkVG5TcW1qWmhQTWdxR3FBNnRhWitDSmQxR0JXR1pzTVgzaDNZeDhOTE94Sjc4WVVuM0hpTGxPd3F4U1NCUXJka2g1Um1vYjBOZG5ncWd0K1lxaDQ4bkpDWWM1anF1UmNDdHFUdElXaEoyV3VnN1VybFREd0FweDhDb0Y4MzFqQ1g4SGFsTmN5Wm1wZlBwVmo2ODFyenAwcFZ6elV2a1Byb3M0aEZTWGhWeU53c2hqMXJMUXpTVHNhVlFUTWxyTE5sYlZZSTdiakZac3FMS2VXNHdQREtoSGpsNTdrcHBvU0VERGlENW9RVDdWMHJEbUtoL3hlOHBwNUxabEs3TVR5NXAwbzNFOVU5TUFrWU1jYmIzclF0OUNOdlFZUDRBTHBCaDdvNm1RUDZVNEsybC9TVWFPdDVQVnJYYnROZkVYUlhOVHpkeGxGVGVVY1djL2FER0lzdDV2R05FZGsvdnJCeFFEV3lER1JQbVVnU3MxbjEwakRwK3pvTmhqa3VqM2dUcWhzcEkrbUsraFRUNXExcTB4K2hla1R2WkRiL01qbXhlTndPTXpBTzh5UkExdjlzOVlmNHRNS3VkTUxlVVVuNXR4WUdLd0syYzZ4Vjh5cWpQZW9mK0dOZ0FOSEcwTkNpVWw2d0hmT0MzNlU0R2xaQk51cmo1TlN6clUxMUtVMDJxS0NkdVkxc3lmaUdaWkphVk9zUXJBTHozQ1FaWE1hNlAvREJCOXVwTkZndFROeDZ0ZTcvQnlSVVN6bXIxYXNoOU8yVWdpUFpIUHB3aHZQR3B0aTNwMks5dUZ5UWVGTVEwRmlWcW9kb3FlWmxqTGFuUkNFLzNIV2RyOW4rMU5ET3FUc042cHBTUFUza2JSL0w2c0VpK21HTnFHMml1SG1wK0V6Y3hya2Y3Rm1tdFQ0bFB6WDc1eU9nYi84OGtuOFRrZW1kSU5RcTB6cHVqakpZS093cVVSWTE4bU1uL2dXVVNhemJQZDI3Rnd2b0d2VjhlK2Y5aW9rSmtQQ0JPOGlBMW5yMitYR2NTVEFOVFo5TlRPeUhXbkpncTUwbjJTc0NEelpYOXJ3QnV0TXFNRi80ZjY4azVTbEtBYUhPZ3Z5cktHaG1xNU4vbUsyYTVhM29LT1IreUI4cXF6V2VvSEdUaHJFdUdiOW16MmlrRXlmelJhbGtNdjFHUDlUWWhZRDZkb0tkeEtudGpkTHpmZVp2WXorN3c1cjJzZ0R6SHZ1dUFMSHhhUWEvS2t0TmpaU1VCUFZ1QmdDV25Sc2QrZG5ZbUt5ajdrdjVIRDlERnNwUGt0WkFNM3JqRE5QWXNQanBPNitMYXFjU1VTUEgrTEMxMk5oWUY4ZFRjU09LVjlKeVdSV3FzWU9UaGpFU1BYSGNiUHRxcEdCUEY3YWZLelMwR1hnd2N5TDdLT0UvRk5jK0xGdStoQUQzLzBEWHpKSCttM2UvMkMvYXgvTW42VzN2UVZlUWJzVkQ0Sk9PQXA3OGlybjNvdGNRV2dvL1J3MUNDNDRIOXY5WEFCc3FxdkUzaDJJYWJOZEVzVU9JVjNYRDZpaWUraGlkQkEydy9xNHJxQkJQbDlwRk9ybzk1ZnVoWVRVUC9nLzRxSWJVSnh6ZWZIWGVDQ0dWRnNsQVg4WlkvYzF3TERnR1JFK2tPWENzeCtpQWd0K0FOVSt0b05VcWpkZ2xCWTM3N0wvdU5zTjhZSVpPMDdUUWVkNFp6WVNlUXhoT0lIY3JRdFV6clZ4Mk5zQndDQUZ4NE1FVmFYci9jYm43MHZkYWFYNEU3MUtjNUpFYlNTUnJLcjA5VUwvUExucW03VE50bXVMWCtQZ2tqaVBtQjdNT3NHSGszcFhoRkVkNzV2NGlPWm9zVkJ4bXZBNGhhbTlESmdHM01XRHk1NE1jUTdJcU1SM3NTVm0xM251dER6YkZlajNJRWNkMUxQV2NEcUM0c2xaVEs5emlORkpHaWhTbUxVakN0aUVNVjBQZUc3WHBlQlAwSmZUTW0rK2IxVjA5Q0ltaENlaVBGK0xBaVVJL1NacmptNjJnb2NWWnpSQzFrWmhIU0V6YWlOTWtPUlFVc3J4R1ZaOUR2OVpVUVZQUlM3UlNacFVYNURYZ2VrYWlpZjNRVDJWOWZLRGpZSEdtY3dTekYzK2VSZnhreTN3RitoT1JIMlQyMkpFaHZkQWFxQkUxMUJNTmlJM1BkV2NMN3J0bVNDNklqenpRUVNkdFlsSVBlei9aRE9BWEErTWFSbzRVZFZVVnlWUXE5VVc0V3JsUXByRytxdVcveTh6d2JEMTFBc0xRT3VaOXFRMzdXT3g2WWR6K3dvUzJPS3pjMU1mS0Q4QW9tQUlmVklPMnBZeHp3ZnBSanRLL3p6bXlVdmg1YktiamhQc3l0ZkFBMDZqSk1QU0kwM2tVSDhDNlBrOGhqM0h5MFE4bHEraVFGaEh6UFl6QTg4eWhtTGN1d3BacjZuSXUraVliVjhwa2c5c0JxL3FEcjZCYUlUYlZnVmYxR2trRUFuN3Y3c0VSNXI0dTFSb1Rhd1Z5SW1uVm1HWDdmV3Qzc1VaVVdlbFlBZ3pDdXRuWmU3SnFiNTBoQjNpZE9FZGlNeU41OTJENmRRSngyTkZESCtkdFNJa0NFTnNiODArMDZvd2QzMGlDS2VCR01CcXN4dFU2a3VycUZXWWsvVUdkeUY5UlF1R1pWZW5uejhFK1ArN2cvcWc2UnJFTUwrdnI0QnBsY0FCdDZkcmVOaW5rQzd6SWExc1JBdjVSRkNVMGIwbU9PczB2bG1hSUhreEI4RlRQV1IrWEVpeDVVTVRvd1BsSW81ellIL0hIQmNwL0lKWXk0UTVaU1FTcmEweWJzeDRGcWZvRTdWdHhCc3VLY0VmZG9ZeHhvcXlQUFhCMHhhemhQb0s1MmFIV0NQcDMvWkVBN2hlZEJCNTZPb21jakg3c0dua205YmJHZWFnRi9nUElUVEJlYjhNZlQwdC9qNUZ0Y0xSZC95NmlHNXkwWVFPelpteFdQRUFLZ2ZMN1BjZmUzV3hjOCs2TjBvMzIzK3dyMlAxZjNHMTVaWE1hMzhJa2tBU2JCWndpQXlHSUtnZmEwdmtiNjBNNjJlclBBbGlnRjRyUmNXZWtNWno2TU1wQy9rdGc2Kzk1eTkxcmMwbk1wTlFPS2x0aVhsS2tnMmNFazN2L0N0ZVhmM3hscDRyMStYSEowQkVLRmdRZFErRUxkVlg1MnhjVkFhd21UejdVSGZaQUI0ZHh1OExxcE1XVDQvOSsvdVlnQTJONS8xbGNxd2wveU1zRXQzY0kwMis0V0VwUDdGdmFyUGhReUY3elBUTm9CeEdScWQ2QVQwSDhqV1k2ZWxxU3F5T2JWVEVpYkN3WndMVHQvWlZjYzdMMGMxL1dqTjZtSkJTZDJUZG1nQndtS0NkVTNYL3lYVGpCakovUmthV1BxOTlTRmlsWjc2T0RodzY1bkp6ck5jMEs0ak5sblFFQXE0UlhvWURldkJnNDE2SzhHVmtObGZUTzFpcHd4dmV0VHl1dmd5TkNKRC9Tam9Ubk94b0NPbDRYZ1RkWktMYmw5MUptYUJhUElMQUQ5REFVMFY1OHZQeEJPSHZ6bmdudFBsN0hubVYyYjdOYjVpNDJLY2hHSzlHVEJsYXNxbWRFMHVkeUlTVUF6Ny92ZktOR3oyRk50a2lMaG5VMXN1Vm43emRFZW0yVklFN0xMSnIzUHRJbWw4OFBaY2E1T3dRYkhKbHB3ZkxtRERCUHRmNzdkUHdIRGxwYjJ5SzErR1lRbkVXbk1XbmxvNUJTOVpVbGN5UGxtMndCaC8wdGlLMjVlNVVoMkl2ZDhvRTlFNWYydWFWbThHVWRxdG1FSGpKbWZqbzFzd1BPWFA4WkdkaDNLYTZyaG9TdVZUSjlvajd6L1FaMG81RUQvZmJNUHBFWnZnVGdUWXRkQ2NtT3dtN2dTTWh4RjJsUXlNNTFoRGNNa21FMWROMmN4ZW5nOHdvS2xOWUVuZG5nL1h0WW5xN01tR1FNYzFPby9qWDBlcDg1eFNSbkVOcUpzRm5LMTlLZ3huRVZwWDZ1Qjd6MTZ4VUxqa3RkcWNFZ0xBbVhwVCt1RDFKWVlyN2dxNm1LMDdocndldjM0VUFGYTJXODJZejZOd3l4bEE4MFFIbjRoZ3BUd0tQWDZ2N3BVSjBDNjV1dmJldEhlTmhNbXlqeldPcmRpWHFLL0dXM3lkQzI5SzVRU2J5ZmFZRVNXNm5lZ0N1Sld5TS9KZ0huNkprek5LWjY0Ung2bFFpSWZickRyaVRJcUovME1INUhQck9pOFpDbmptNkp1KytVOEZZRlp3dFNQVmZMSkJmQS9mRzFwK1FoMmxGeVp0cENQVGJnYzl1OXRaSC9kNVlvU05ZZjUxRTBUSzVjMjVmakVnNTFIWDlGT0Q0a0xpSmVGb3VBdGprU0F6SjZsMU45cXo0OHZPb09JaDhyT3pHc3JSWlh4RWlmektqcS91ZFEzQUtGbTZkemxOanVjVkcwbkZGYlBBWXhOb3p2VWJKYmxEYUdxRmVrU1FTbmVEcHhWaHZxYjhSRWxXMDJNQkF4RFg2V3N2UWZHVUI3NUZyZFJ4cVZjZDFZOU14c0szbWU3WDVpcHlBRG9MVkNySm0yTW5PTkovcGdOMnNEb1F1U1gvNkpRSGdBc0FDZ0V4WlViQUZEbUF6TVZwdVpRcHRUNW5ZZFMrbG8wRnlUb2pubkg1aFhHOG9za0tFMkYvTnl0Z0ljRkJNSEhBMzNoUWRWTXZhNmpMODVtbW82UzBrQktEUWF1WlVHeU5RUklSQ1dlSmV0dXdaQ3htWXpMd3NMUVdHcmMzeTFNZVVpMjJyT0VZNHpxK0xmWCtrYURac0Fvd2ZvUW5nMzU1WVVpSGJiUmJaWWk1amFUS3ZJN2VibG9uVnJlVURMOUdaMUgxQ1pkVHlrL1ZSVCtoZjZ5eTZUZ1RUMmpkWWZBbUdLN3hML044MkltaW4wNmkvdUJJWHJYVGtST2psakxGUmRQRHJHYnIxbnBKTjBtYk50MEpiSFk5RmIzek1HUVF6d29MazVESS9LR29oSWxQNWZJMmZzWVhZUTFldkJpZHpJL3hNVFZQVmZIWTRkU0hQNDQzVWxIUjRhMGVPWFMvYU1MRlhnWjJxNjU1L3pGdG9qQTNyL1hLUWxoOS83SWlscEE0QWVYS1lDRzhBYlMzekloNmZYalp2bGQ3RzFEVXFNcXZjMzROZnlCdkFNdTZ1ckIvSEY3cDNwTHY3bkFkdFhsQU5yb2VxanN0Ukt1dFg5WGJ0UHVzemhjcUp3VHFSUGphcS9sUkJpOE5NdFd4TFJ2VDVwWEpWQkg0WkpIYmVLTUphbXZWK0ZpOEVkN05KTFlndjd5OWpHOGY4Yk5kdE5yOGRFNnR1QXkzZ0tZallaMEhQSjQ3UnEzbHZWdU00Y2NBNUpNYTVFejRsR3ZhekxVZk1JNkt5RS8zTmRHakRIYi94LzZ4a3h0TjZwSzVPWHVQOXBDbHhheXU0Y2ZNTWM5R2NwMmNYUUg2eVViVkkzRStNdkdLcmJZUFNJcmV2QlM4ZEI3THVSRlFqVkxYS1h4RDlGZ3FITUx1eDhDenNXenZuL1NqcUVnenBJZDkxUVVETEdSeWhEY1BQT3ZLOXVSZGVUM1V6VGRPc2ZTWWZGYkR2QUhBVkxSSXhQeWVHdUNkUEtETWxHcWJQN2FPQVBSUlFtRTFycFIvZWMrT0t0aUYyVmpYVmVKV0ZiY0syOGZhNS9VSVE2alpSNTlEZ2ZoY1R4YlBXSlBJMUFhSkNmSTBwaVZaVFlXcmRlVi95RWJKR3p0U2dyU3d6cld5NkdaWG52WEl3S2tpMzJSRmMydmRjQ254Z1U2YTRPelJIU0RBNEZrS3hvUVNGNEwvZUJpYTlLMlUzQkhPMmkrTktDTmY1WE81QVR5NEdkYUdzZTFPQ0YwbXU1YTE2bHJCYm03NXRubDBmOGRNbTQ1ZmtMS3IwTjVBTnJ4b3NSUVZQMHpUWHlkSktPWUo0dEY1NlMxc3ZZSkE3UzlkbHhoNWpxV0o4ZVFPVXFQd1F4bkdOenFHdHNOR0Yxa3ZveTFCK1d5RmxpNVJQR3haeE1NSEs0RmNKRlQ5UFF4Q24rK2dEcDUySmFNdkdhZGt6dkUwR05lcHdFNW8wb0hNVGtWZE9QdnA4NDVTdTNGdk5KQXBlMFdqZks0V0pONzlTTjM4QUg5Q0M0SzhXbFlSQTB6Y25MdW1XQmRjY2c3UjZudWlIYTM3T09nbDZ0alFkaUVaQ1R4NWJUYkd0TkkrSGZQVVRQQldZWUpzb3lZc0Zxczc4OHZYV3ZHa210dEtQZ2FKQmppdUZBcnUwLzRqc3ZYRlh6RG51ZkRoQmsxS05OS1ZQaWc2UHRFMTMvYnZxZmR6anEzRDdiNnlON1llMGFwK2pOQ3pTY00waFVjMlRFbEZqWnowS3hNQS84L3JmeHlVeUV6bUpoVHVRMlRpazkydXR1ZTZNcnVVb0t0Yi8rS2w5NHdsQ1hPd1d0UWVJYVpFSGVnQ0hNMk9qWHFrQXFCeEVUcFVzbk1zZGdWMVJaalZKd0huSkN2U2tHemVVWklQd0dhbU96Ky8wNDdOOUEzTkpRK1kwUGxLakdGK3RKYXlSNisraGp1am9oaTlsTDl3bndrS1QzMWNoWUtZNmgvaDJSQnVjSFFBNXVqTzd0YnArQlVuMm5HQXJsZHB6Uk5xMjBOM0JmV2ZJMHkxaXJodGZ1eWtMY2laMFBMdmhsRHU1WFdyd3FubXc1MGJaY2tpSDJ3Y2pUbVVERmQxLzNVMDJrS244aWZPTzNxRXcwbllZMC9VYnZqVU9KRk5ROWttMjF4R2JpWUNqL2YrZGhLMGFDVDlkOU5vSTlaUWlPR1JlWDFzWnJMWWo5ckhGTE5HY29FUHQ1K0htQlY2dy8vTWhYcHFoUmZmUDBSbEYxdWlrNEdCWisvcEovZ2ZTcHg0b1RDR2VDQUtkVktaZDF3TkxYaEhMd3k0eE5MMUNyS2pLNFBPTWdQVUozSi9ucm5SNWE1bVRsWkdqYlh6S042VVF2ek1yV0wzYWRVcXZoVytpVC94WE9vNU12RU1BeUhBdk1EVDJ1eUg4a0FWS3paazFsV0MvRkpSdDlKekU1Y2RjcWlkcjNPbVYrV0o5S0s3Sjk1L2hzT3Z3Z2NRWUY0YzZ1OFNpMzFzLzIwZXRlZUJkaEpCRnNaNUxLb0dmOUVHWUp0TVkyNUpiZTlaa1UxOVJkaVlJMVV4Q05hcHlFMzFRa1NPZ3hFOEdmbmZSQ1liWXd6NE5HdlVzamFRN3Bqdml2cFFIT2ZpazhaK3NOQ3VKMDR5eFA5bXFkdWVhektKREpuOWJFdUljenRsYlNTSjlOQ09KWS9FUFEzNWcvaW9La1Fsd3AzMlg2U2ZTeE53SUJJd0RsKzVOWW4vRlA1WjREN1JKK21vNmJtaUVEZFRldUJOdUdXZDN4R1kyaHEycnpWVmptM1A2NGRVYWJ1WDhHSXkrcFJnSnUrbnlBdDdjWG16cjlQb08wZzR5RnpuRzJ4Nkd4NGhrZXltaGZrQWFyRTdYWEkrRTR0UnMva2EwbXRKNFdab3I1Rk1GSGFZZVgxbGFNbzJzTEhheGlTM3RkanorTkJiU0dnOE9OTXJlcVNVRDNzU2Z5RmowOVBHNlhnTU02S29Hd1lyQy9WNlhKK0tnbUlNdk15RWpyY1dZeEhuSlYvUGJpVDNrRTZhbFpTUk1XcnZVNFhIeHMzYVUwUFJ4KzFIVkZrQXgzWDNDU2ZUWXNQUU1wbHJZclBEcVRTMlRYeCtpc1RITjNrK0VySm9BYUhnMm5UQ0RqdnkrRTJEanpkMERBVktOMzA1Ylc5ODRDS2pZcmRiYmlwK1NPelREM2lHdUlHSXVlTVZRM3pVS05LbmhxTkpjSUZkMzViVVMwc201b2dXWG1NL2J4emxmVEc3dzFwN2dneUtqa3RveUpaRUFZcTV5UWtsc1h3WExmZkFKbTVwTkR2emZhaVZRYldrTC9IaitPSzVwUzRJQ3dxNnMxV2NLSEc4cENDMVNyWm41SEIvQWxUTWlMTTVxZm1uV0c5c0FiY0VsREt0bWxKU3d5RFpENEV3a0wzdzhWdzVCWi95TCtxUzI5WnluU3FlV2JJa1U4bTIwd20yWVg0R0lPOGtMYXM3UW9UWUZ4a0p2U1d1OUd2U21uUUE2dmhLVW9IbHpWOE5USkhXME9SL2h2NXV2cEovZzZpcU5iYVZ5YnZiRVhxNEtOT28vK1pVYVc0TWI5Z1lhdE8rYWlMRVhaSUxWTFUrQWx0V0pjcStwVFVYWmxRY0JXRnZzN1VocmpRd21ZZ24xaFlIK3Y1Q3d0TmNkVWpnME52NlBEb3g0dDZCT1BSUm5vdlhtNGNFNm1XUndBNEpxZFdNaUtNTm82MW8xYVlmT2Q2Q2JvSEFRTmhUb3ZNN1h3VHFPZ3VWUUkzK3JQZStwWXlwWkE4Y3UrcEpkVHl1YWVLeXh6emZOT0MyQzF0a0pBU1ZJTVJhOHVBekE3a0FFbGNpbHYvd2QzSEcvSUVsa3ZwUE1XeGdQTXZ6dmxZb09HWGx1UHVaOTVJbVpkR3ZTSE04MUNVQ1lSdnlmTjByckhoWmtmbmZRV3Y0ZnEydllLT3dkWDIvblZWMVhXWGhYUDk0eXNEeE1yVXpwSkxRTE9VT0NxN3pvTE1kQXdvVE1ybFVKUWVlZXZqdG1PeUU5V0xBaTBpZGgzZWlLeU84V0c1Q2NlcmFSbnRya1ZHL2dkSFNUTzFCRUVVYThVVjRLeERCSTBVamNUbnRiVWFrK1p5cTVuZ3FmV2VjSXcrOGE2bWEyNTlPRDNhUDM0L1UzbCtNaWFrWW1oYjllREY4NGdQYVhOY1d5MDlyUUdPaHlvcEtDUEJxWFBCYlVOcU9ISlNZUWRGaW9QL3hGcWFzUjJ0Vk9hMk0rQ1Bwd3Rnalk5SnpvTVBpY0orT3JnWTVYb0hGU1hoajRDYTg3WEtkWTA5VXE5SXk4My9GYmVnYy81dWk3NzIyZktFa01jdFhLMzRhWHVvck53YU54NEh2d1JQZXdYQkI3bU1EUXA5SjIyS2d5YlNudUtaQjFISFIyRVRTbTVXV2F3WUhLcHB4bTkvQzloNmlOT0lMVFVQTllGR3g3cm16dllRL3FWc3VsZnFCN0d3TmVwWlpqUHNlU1h0WmdSZ2dyb3ZHMHpaTUwybW9WOVA2UUZQNFlzT0J4ZzVkY2ZrbTNlMENVWDBIZGJkdXJTbUdsTHd0UEpYVERSTkkxSnArY0pHQjVmNXlodVhrN0lzc1JmaEsyc0tKR25SM0hlUUpYZDVjUUt6QmsrbjFHUlhSR2prZjJOajU3clFqK2ZoT2RNZlE0MEVyMWZQNUVMVzFtQzRmTTRpMk11NnRLWHRPSHNtMDhGSUFMU29KWnN6aDVOL0dyMzZJSkVXWGZtam1WSXpZR1JWd2pjbjRxWFFvZDZudUIxZjE5Um85cnJrUUd0SUFYU0ozZUp1SDlqYlU4eEFrNFRKRitsZVV2SDYvSkk2WFU4NUNSZWs1WVRkeTBwME5TWUxVWFErV0RVdVRwYjl0eHhtdUFNVHN6cjVuTnRPQXc3bGNjTlZ6L0ZVOC9DZkxWcTdHVitaQjhOOXUwTzZmZ293cTFRRnVkWWVhQnRhRFN2S0lhOUlwaUpyNC82VTVTSmRYWlVGbDlGQW13MzgzNjZmbXZEOFh2bWV5bUN0MnJWTVFvUDBSWTVjWFJ0dWdLbkJtWjE3bVlNUkxQRC9neDRDQzBNRzlmTzhYVk02NFlpTzkxVStPcThSRUpHYS9XTFdUa0RPTUVrTmJMT2l5UG11bytMdE5wU2lYRFVpb093NHI2ZVcyK0R6N0xSU1REcEZEaG5EQU8rUm1sU0ljaUQ2MjY4eFlVQkdmcXJPU0lyNEFPbHJIRnhoTE5yMTQvWVVrM25UQVBQVnFYWTdvU2R6QmxKdGVkdnVoSmw1c1o0Sm1DYldnUDZQNS8yL2M5NzBDU2czQTBPQzZ2OWhEUzJNQ2xzWmg2eVRmdkozNnJuamxjQXJzZDUvMm8wTlRQbVVSZ2RtWDVuOFJLRDliLzBVL1RBa3NkSUsrZStORlN4T0NEZ1BTN2t1aWl5YlZvZ2w1bTIrM1JRQWZtZDRRWlBYbW1VSnhpNEl0OU1tMGRkTXBHaWhOUm5FcXNjbDNoaFVEbHU1MVB3N0hvVVhhZCtScnNNUlBta2FEREpQbjFpODNYYXludkt0WERiQm5aR2diOFF2YWhFQkxDdm5wZStqTHFCK1RleGVWQU85b0cwdnlSN3BETEg3THdGLzJjT2c5WU83b1h1NDlzVzNPdVhwSHRtSG1BQ3ROOFNKU2JHb3dXblRjK2xqUkFrc2pMMUZQUldubDZ2MHF6L1oyc1oxcXg5LzJMc3dYaFNXRlVuTHczckVUeWVVZUtKcktsdkt0TzZyMUo4M1hpdEVHbk9rQm9rcHVFN0orME1nRkdNekxZZHFrdXZVbjY1eC85bHVrT3ZzL0U3cEtlOXZmZTJQVitkZ2tpRVhrZUY2RVhUakFQOFBqZUVNc3dhY3F4cmpBcjdKVlRhQWtndXRRYlIzSjd0cyt5YVhjVlA4b1RYWEJCamVTM0pvU3ZGMlhweTdtRU0vUXpscGtBQ3dyYlBEYjVaaXhxTGI3NDQ2VHprTHV5NndKbUdIeUYrL3JtaktiditkOVVDL3luSzV0NnFiYW9VdWp4Z0FCNmtRNGVCeVpkRFZHNUZmSmtRdHhlNTQ2SWFDTENTcEZaUERYVUJGMDNseVBTOEcyME03R08vOUdkeFo1OGY3K0Z2c1psYk9oNkhzeFBHL1I5dUFmNWxWRzR0aUN3aHRxWUlSZkJHbjJnZWtLNFRLTjk3NUxuNVdiZjhZT1Y2TTJBQ2lMTkZINzFoQytVaXIyNzkvMVA2M0NNTnIzYjY1YmxJWmVkamUwUmp1WDBUVzRUKzhtQUhZSWdCbzNOVXYvTS82NUVjY3JXdXpMVjlZc2NjdVFSekNaRUJZV2VHQ2dkQjBOd2UwbkRROVp2dGRSRjNxSHlKZ09SbTJlYW5lbm9ZK2h3WlpZZ3hpUmpFbkZMbm9QdEg5VmxjeTIvMDhoc0U3aVpuOWY3MkM5Z0pCQ3Jqdy95WTZnRDNsM2J5SXJXWTZmRStTSjQxekdYcHpZMGVBeE5nRlFFL3QrWjVnaU5BMStxVHVmc2pLa2JSTFkvb2pqWmxPUEtIN0RPNDQvVTJYdml3Y3JLMTlqVklHaklHZEtEdnVEZWM3eDlWU2RMTW41R0RMZURNdmZIQXFRTVg5NXRyd1doeis0ZlNmMkIySm51SXdJK295dVcwNVZZK1RFWjVSWlM5d3FaOWF4YlpXMXNjcHVzSHpuanllcngrRHFBMCtrSnZUWkF3RktuRmo1ODVMc1pSY05wV1FzRHlZb0JvTEVGMkVCNXB4Vlg1YWI1bDljQzF2RGVLWTlTZmpxdUMzUHFiTklCeEFqYzFGTlJ5Q1JWQWhTalJLWmkxbHk0UzJ4L0Zpa2F6M1QwZlBPbUdzalRUVGxkVWEwdURkemVyS3dtOERJNFY1WTdTWnBIRk1GOGpjdUdMaDNLOXMvdFZEcmJrRTB6dDVqSCt3MG9MMUk1ZUJjMXhkYzlhUFB0SHhWUUM3dU1jLzFLU3N3ZEZFU2YxS3VCYmJRZ2REU0dPaEhKMTQ4RFdTQWlZS3hJZEp3d29odlZPdVE5OGpoRnpPb0MyNlkrRlFpejU5MzhKN2hNa3JYWk5zcG84V2R5d3FEY01ab3pXc29uYzFVZFhxNzdMbnFkMHBSMTlYOVRlcnFGTzRSdCtoZWllUVA5a05nRVhDSmVoUmdvRXVXL1lHUHJtb3pkMjJtdDExTW0rM1AyekI0WkZXWEd3TStna2d3aDBIMHJKTFQzaWhsc0hTUjNsSU9PeHdCWGN1dzdDWUtkVFVjUTE3cXdoVExqbUNpdWlENXRqdGZLZ0MveDF0cXFhc0RhWXNBc2gySHNnSUlOajJ6bG9uSG8zQ3FzYlFMdC9ENitxL2RpRFVyeTZHRnRCQTVFV01ya0lwU0M3dW9OZGg3eWl6ZlhiMkFBV29DRnlCU2dZQjlsYjdwcWRyL2E0eitkWlY5d28wM3pRZHdiVm1sWWZwb2lHRlhWT2R2NmNDY01lWU1PdmVSVkphZjErWDhlUzRESkJ4TVMveHZaci9kV0M2em4vRTRNMFVEKzNjSk9qazVlT3FRdnZ0c1dmZ0JIM3B2Zk5pYnhRRDNQMlFqQ1M2WVNhQXhpVFowaVlnWlZhTElkemsxZjRXNnNPZTdwZzJqemNKak9VSEJwS2FvZTgzTUc5OTFwWkR1WFhUYjlLekhZOFhJUzZseVFwZk10NDJHNi9RM1o5TmI1T0drWWJUWHN0Skl6emlQT0RsSjZuejArZWtZbnd6a3E4d2FhTUpCdUJTZGNLQkZqYnF1R2RCRzdxRXBXRk42eEd3VXJkRDQzWHc3WlZxQ01yQ1ovM2xCV0h3aUNqUlRGQnJhMXJxNUVJdnVSaFN1Mkcvdk5QaVo1ZUMyUDdJazhSczVRbmNGaVNOQzFaNHpYOXVnM2lsVVhVSVMzcloyejNMM1RFVXUxdCtXdkJMY2pvZ0NES1lPUTFJWjluZnh4S2dzbzFNQ2RoaTBUaGpmQkRJcWNqR2cvVkQ1SW8rTllmeVFmRFZPbnNabkw1cDB4TGg1Rm0yUnA1VWhEOG9haW5yc1BuQzRucmgvN3gxWDRsTWRyTDRpK3dYeE5KcXdLWmh1MzA1aVRlVEYwTGV4bzVBVnJQcjJhRDBoK25SbDFkN3p0V0p4SFRIdHFEdkFNRDdiMVZHa3hxZk1QSmZmdlNXeU42eS9SZWxwUDZ5UmthWCtzOEplQksza0tQNHQzejExb0pjMmhwZGxhZ1NqQis0MDJ5cTMwUEh0WDEyOHZBRDJXKzhTdlUzZk9aU1BVcVZlenlVWGlKT3dQUW1WcHBUOUc4STZkSUZFYWRxS0RFcHNwVWR5TEE0UW5jcmdsRnlBTWtQZnFQSEdhMUdKQ1plZkw2UnlDZm5SR0tiZWk4ei9INmxkQVRzOHBidSs5MVpadW9mVXgwS3pMaVVzNE1CVy9aSFcyaGM2OFRnUGxVQTFFTU9GZk9ieXIwaXc4ZGpmcElVN3JNKzN5K3puUmZlTnNmUEdJdW5pcXFsWVJqQ1NWRUtUT2RScG5nMHp0dUFNa1pMMTlOOWVvVXU5WGd1eE9pYjBLMWMxOFBNMTg5ZUZTOEVXU2EzQlpBeXE4Z0k4ZEhEdnFJNld5ZUNaaVB4TnVmUWhoYUdHQUhublVIU1VCdld5dHRTWlFmN283ODJNZEI3VXNmMFYzM2p4WlAxMU1qeFBSc0R2bVpJRVlPaWhpbGkza1FjK2l1aEZpUWdsTjFlN0JCdnJRRnlUby85RlVnZGhuT0VoV1NOVnhpN3NiTi8yWTVKd2VtTjh5b1BDYXNTVmJQbEtuc3V2Uk1YQUhRbGMxdndJcVFhNGhLWmx1Z2EyUVBXc0pOdkJMcjU2TWVWTUQ5MkIrWmQ5SUsyUjRJMG1waUU1Y2hrSGdYbzNLWHRZeU55T0I5L3pRWTBaMUExY1d3UXlLS1lha3VhUThCVkNLeFRPQlRyU1AzakwrVXF0V0lpZ2xRU0JjRWVUZERsSWtJQ2NQOU02NFV2SGRDVjNEYjFmMEZVNDBDWVBhcXFJVThjQ29mb3gyKzAvekZSM0N0QmFuVS9xeGtYc3ZSV1JvMHF6YjgyY0ZCbVRQS3pCdk91b3c2Mm9RS3VSV2kraUNqUnNOT3ZNRjJIL3FIK0twekphNEFmcEpMbGVEdmpMY0ZvNkRuaWZZaGhjQS9PUFFhbmQyN3NkUTR4cFdOcHpnRlFCUWllejJKRWVENVpBRnI1TzJ5Zk9PNDNmWUlubmp2Q0lBdndOSDFmVUEzNWFWVzZBcDdUWGNBRnJUdDNrRHpJcGhrSTB1YjJDbUFzbnNJUVRSck1PVnBkZ21BdGsrcHpseVFtUE1XTU1PQSttcDNoem1sVWF4Q0QvYS9yV0tYMllkM2RGWTJLc1VmOTE1ZlFNYi9SM05lczJUMy9kN2ZnZjlaZkoxVEZpMTM5dWFKaXRCNVZGMkljUDNQUktXdWFCY2JHYUl4OEV1aXJIVjY2MDJjRTFYUkNvb3ptUTZzUlk2aU1vOFFOK2xqR1lxOHcvdGlLdWxhT2NEYTBZaktab1puOEFCNHJ6TEpWSDgzSVVJNWZSMEc5dGZabXJIY1UzcWZLbFY3VVBaQ3NGV0J2YS9jc1VFdFlzYlkxd3ZnclNEVFZScU9ERFhCTXRzVG41eUtNRW9JQ05VcnBxb2hDWm5DeEpKdU9NdnVVSnp3NHlueWR1TXVCYWlDNnJhOE9jUjZ3VzkzNlp4WnpscVlZaXNLeEU2Nzc3a2dyT00vSndtcXgrd2FWU1JvYmZwc0QxTzg0Y2N5L0M0ZlkxcUt1cVFaMk1nVCtBK0pRUVZlS0I1UnNsQ09LKzlRdGVNZVBYM2FnS1FzckhuU2tUbmt1VnJHQUhMRUtVZ1dxa1Nrb3FCMFh3SzlMMC82ZHJ0YlhWMnE4WUxWVGNNQWlWQWxyUkhxM0VnaUREK0RBWmZVaEIxNHZDZ09XbGNDdXlUbWpYTHdsc3FXbDJiS21PcVFpbUhqbjhQVE9uQVBIZkd4TnpqMnpIZ0U3WDg2QmpxTjBJVkxKdnJ6aUhVdTg0YlZoTzl2eGRuNzU5V2RsOFBHd2dnSEdVc1BIYlE5c2RwR1lmYyt4alhDc2R5cXBxM1ZwQ1kyeE16MjNtbWZXNFdKcTQ1ZG9Bd2pzYllLcEdxRC9lRzV6eWh1S2RVV2RDZ0tSaHU4MjljeGZlcFVyakpuS21qcmlBYXZDQ0FkS0pNMlZGUW9nYjdRaDI1eUlBNHhWMTllNkhwcDV4WC9XT1FsY0hZRXcxM1RqWnRsTk45T01GMUJONlNCc2I4cFFLbzdrOVcyRE8rblJpZGlxTWY4UTVHdEFsNFh1bHgrMFkyU3ZnamtiOXBsM05qamJmc0ZLNmx0VFpwZDFHdExJR2FRQ0Qza3BJcmk1Y3VtckpiVXV5a1NFdUFPQVhrUXFyamEvZ0k0QVp5djhTajcxUHhHeThMNnJtTEd4UTZaalRpaFFjTnVRSlBNUU9RdEdOMXZHRkorY0pHeC96cXRJenRrMVNraDRBUERuMitZZnc4RjdheG9sUjFBcXRnVjJkMHJFYkgrcXk1Q09Cb1VCZDEzcW11emowNkRUZDZJZWg3Mk1aejB0SnlNcFlrS204RnZJVGtjNnJoUWtId2tjRFpXZzFsN3RFVTB5cStyMTZBZk84Wkd2eUhxa1A1UE85Uk4wb0JuVmhEaXkvdHFPZjhaR2ozZmNZRm9ReHJTTE95TUNlWHVoeko4cTNxQndGV3MxdXlyK0hQY3lTZ0xNNldwTkxXUm9abTdQVXRpcng3eEx2bVRVNlZ6Yk5mRE9aS0w0SXVyRTB0c0ZKTThxSTFCb2pPTXc0SWxCRDFMZ0lDQWdJQ0FnSUR6L0QzQTc3ZnNZZSszdFp2dCt4K3ZzKzQ2L2g5Tnp2Mi9ieWZzKzREL2NQcTZmMi9TWi90K3JqdnQ5blBuK0h2VC93ZmJhYjl2Y2QvRDdLZjkvenM3OXYwMC8zaDlwNWZaOXd0djJmVTAvd2ZUUVgyL1dVL3Q5bVRoL2I3WDM3ZnJxZjh6c1A5dCt0bC9aOW5sKzM2TkQvYjlpVit6N0Z6ZnErbGcvYjlEMSszNmNQOHIxekR4d1MveXdyVzJpUktHOWt4K3N5MXFldnhUOVV3R2lZc0FuWlgxSzROQmQyU3VWSWNDclBWelY0VllJbEhBSUowYWdnckozNTUrbTB4UFZsRDZFaUpDVVhDcFUzSWZrek85bVhPc2hGUXhrcGxHRnNQRCtwZCt5ZUhCbWRtNWpZRlMzTXVkdE5CQmx4c1VNZzhHclRBRTYyaElHS3UzY25UYVZCTC9lOG12WURvd0IxQU9tS0hxZTc1ZGxHN3VZU2psWUF0eVpBR3YxRWNLNXByaU1iNG0rQysrY3lBQitDMjZpVHJqVjZCZk5CTTlzY2hOdnNrTG12N0Zyb0gxMW53dmUrU1JtRWpkcVFyYWlHak5JbE9CRXpmZU1WK1JETCs4b0dwdVA4UGxMekk0ZVlwRzQ1b25TTXV2ZzFWSzVxTnhEUmFCS1JLSnlkMlBMaVphSTFqN0FTZ3B3SGxlVXh1RjNBZVFDZVBYNWtoSHJPQkxUeThIYnhUaDFZU2VRQ1I4Y2haaFdwTHRUYURVM29MS0tNbFhwbFpiSHMvRjIxZEJucHJoMFN0ZVBIMFZIVyt5Q2ViVWJEMXFJMmgwQTY0UkZhT0hzWXZpS1ZDK1JVSjZZLzlhcEhFWEwzbllNTm95OVVPWXZQanFzTnNyaCtPSGVBVGNvSFBXRzNIWE1ySnk5aUhaSSt0eURNd0tySHUxZEZHSnptM0dJVkIxZHlZZHZEOGVUZVJlU0x1OE9YMEFNaitmaUgyNWptRjc1TFRMdjFZNlNGdlIwNW0zTitpbGhESUxsblRNZ1FJQlFUNGd3Q2VDOXU4TG83SWU0NGlSazk3UHJ3S1lSMHBxRHBmakxjY3pNSkdLQzhwY3VjcTVFc1ljVG5laTNjc0V2R2ZYbUN1eElEWkJyUWVMMkN1M3ZsRVRCczBxMXkyT2FxeWp4dzJyOTVJTEpEeTFIN3JEQmlsSFQxNjh6TVNodHZobjNBdlpkK0dvSGpRL2wzbnBSUkRkekJ1VzJ3TlVlaW4rT1NHZk5nYVJyYW0xQzhzSS9VbUVEcmNIRkVTaVhpaCtIeXRSbWJDOXlNR0NKdW1oaGlwY0Z2TU5hTnFQMWFGWnEvTmwxSmMvdW92d25obHo4RmcxckNCVjg3eGpZVkQxOXlPb3NrRmNhOFBBRlQyYnpFNEJLV0I0Qmp2YkFzVnRkaDJUeE5tT2gyUGRwdXlDOElIa3RWWm9rVmt0OFVWdkQwaTN1Sjd0bmhGMW9ibFJOSXdpWXVWZFlDU1pSTmg1V1lKdTNUZTlzQVl0dnNKYktsVE9EMGlvOEZybG9NSHdEcVZMZ29KeWppOU96N2JXSkFXM05VZGhlL1NhaGZlL1Vrb1JSc0JRKy9PMkNNYzVSTHpGYkVTd1EvbXNrZkFNaDk2S000OVRKdVhycnE5TS95aWZvT3FsOWZ3ZFVZYWwyV0RVTjBYVWdzanBoRU91OWFmalJQMnU4UEx3aXcvd0JjQzJkVU5rNENPRmVWZlBJaGlSRHhmNEE1Ump2dkpaQ2dhaEFmTXFxVzZZaGt2aDR3eEw5SCtEcTdSV2tzMmtYRW5DQVVpTWRCK3Z4aVBzVDd1U1Eyc05vRmlzU3g4aTRaRXJZaHZlaXI1M3NZMTVLRW1EQklPeGh4NkcrQnBKOGNwMlVLTjV3enY3cGpWb0ZVUUxLU3RsdDAybUVHMGYvdFgwbEJSTU9YNDFJZkpsaWVtL0RKVXVVdk1hY05aL3QwYmJKUnhhS3pCd1Zja0tGclhYelhOUFJhaGVsMWFmWEZHaHhCaFNoT2RJOGFXWVN2dERyZVdYbVNnTHdrT1R2SkppdmNrWXMrem05T3BqNCt3YXc3WC9CbzJ3V084Y2t1Y2h3U0VmQ2dpMGtnbDdOVFZlWlR3Tlo5N0hrRWo1T1lLcnBEU3NXaDNxMXZOb0FnTDVGYkdFbUlhei9TV1FMWW5hdjl0T1VJQmlITHVhczhVREduTVdNQ24yYkRCVzRzOFMzYzVOaHRNaXdyMnNkVEVvTjVzUnIvM3hEOTNFRTVoclFxSHJMRjR1WjFTNTVWYTljQW1xbjFQWW1VN2tBek80MndnUGd1WjNRSG5QOExxVWdyUDIrK0YrMG82Y24xZ000S3JBMXovZ2VkTmxNSGZiQnpMd2w1WVhYc0ZVTENIYXVEc1FyNFBMWjRRUVJ2b2ljclh5aEJSS2x2bzJ6Nkh5S01kUlNBM3AyVi9abFBFUkxpRVVDeGdqR1hyNC9PODJ4by9MZURLS2NZZGZMdXcwWGFHWDJSS01HQk5Yam9UYVI5K1hReEtpSzlLQWNVUmRVUXk5UjFWaXJaTGNDR0VRVXgydkhKUFNVWEhEMDdFQTBua0dTUE1EVDNHNmJPTjV6cEY5T2lDc2tUUDJGbml3ekljaFVUanRtbW1zUFdSV3JEemhxOHRYb0wzTWJIU2w0TUxTVm93YnVESnRDNnd3Vzcyb1hMQVNkUDJ6L053VTZxU2JaSnJYUDJ6a1IxaE1QNk80RW5qaVdWRUJ5aFpEb3VyQlMwemczNS85WEJsTkh6d09EV3dtb09ocXNlNGcwQmtORE5rQkdQZmhGSGl0WXlMZFI0MWQvMDR1Tnlsd0xnU2p1blB1RFJPTDBxSVlxaUNlNXJyMjBNd3pVbGVqNkhrK1FVVXlRSXJFbmNhMi9nM3hjRU9Ed0RQT2FjaEd2VEtERGdyNDFFODJ5RHdRQkk5aElCV25hVkVKQzNUS0ZwOXBOd1d5aUhxOEgxSm50OWNibGtqMEFwSXRXbUZkSkpHYXFMMDVSb2hUcTdPT1Bka1kwZ094bU81cDRxWTA4TmhxTVFIMmRMbVhTQ2h3VlpLRUMrcW5GVEJ0U0p3L1N3Y2V0K3Fvam5BS1BGRnJ6L09ZeVYyRis3YjhOOXlzOHdQWGZ6Z3g5aGlZZ0sxc05UZnlmcVp2YTcvV3FDajdIdWRqM3RUK2pOamlJQ0xTR0ZKMnJkN3Jabk1HcU9aNE9vaVdzN3p5RW1TNVgrSS9BMEdreThiZXhmNCtyTEwzZmNMdUtZOXdGdDNwc1dGQWsyUjhYZjc5RlFBZ2dZT2RSRWhFR3pIWXRiNXh0Q05DSzB6YW1VMlhXNVNIMnRJem1RemR6VDBybm15ek5sdk4yTGlHTkZzbGNuYUxHOXpWQjE5L09WdGFqY01VWlU4MWJZTitlcTloeDFoVTV2SzQ4ZWdzbUZNL01jUzJZSnR5K1lrV2F4Ymk4dWVsOTJ4c1pjL1NlbTdHaG1jdHdVT3VZcVpaVGFDT25WUVJoU3lGakVBNnNDRi9BVitkZGFBK2E2YVdnZDB1MEF0TE5kdFdYdlBPSmROa1Uva3hwNHJGMGp4TzYwcnQvUTBsaDYyc283TDMwS3JBeXM2RnYzUnF2SlFObFhaeHhYOHpaQUVnODg2RzFnbXF5cTg5YStveWNtMFZFczc2eHFENDMraEhiaVQxM3ZnWitob01pTVV3b0M5MDZFWTR5YlBhdkhabDhwa3p1L0t5VndSOWk2NXhHTGY1RXQ1TnVTbHMzektRdUd6MHBscnk2OFZSbWxhRE81MVUzL3FQNkUzbnhISjFvTERvZnhMczVYcGhCR3N5ZTgrVXFpRFpETVZZSmdBaFArNU0xVTA4QmdWZTdJWE9lNEI3MjZoWFBrR0FnaEQ2dFRaWkZtZHlMKzBMelpFRVpLNmVGQXkyeHF5cExIOGcvMmZsajFYZ0k5ZlVsblU2Tnc3eTByaHpMYTFGTzMyM1NINytJOG9PeVhxOXV6amVSeDByZ0h6Z3p4bGhSQzAzcE9hU1M3Zy9WWGphUHZnM0cvUzlRam42QTVyZmd5TnpZY3NUcGFqd2FoZlJXR0RqMDAwTTU3andyZjRNYnpuZnFMNENSeDdOcWVwalQ2d0trSUVRM2N3Z3ZPZ0VtRkJjZnMrQ0RxZEdYanZGRlJnemYvRnBXa1k4RDU3aUZxTWR4QitNUmEvVGtJZTRrclRmNm9sdEtyNkxtMzgvK0syV3hmQ0xNLzZqQUZjZWUxUEI0WHF0dGE5Rm9ZVDlScDBZRnhESnVwM041VU9sVnF3Tm13cHVFRDhERHVmRzU1R3haa2lHUms5Q2VPbVFXUE1Qc0RKemhsQ202MTNEVTQ5UndqRUp3YW5Na0N0VlBYTVlGdG1IMzdMR2QzQmYwRW1XSStXY2tvSmZqNWFsQ0VOK0xMYTRJQlN5U2h1b1M0MTl4Ykc3UVRYSml2TjhpelFqTmJPaXZ5NitjSzVOSzFqUEJvWVBUQmwyb2xIMVJKekJwQTc3MG5yUEdNY1RpRmMwTGRYcDlXcll4djlncitabGJpVElSa05JakY0aThrZ081Y0J6ZEdqaFRqaHdCMmVBUVNKc1Z6L0p6NG4wZlFIc0xWaGZyNUh4ZkpXalArMHVxQ3BGU3JScVByeFRaenBDSGN3SzdndVFENnlWWFZpQlpyV0VmMVpHdTJ4eDlRaUo3NlNFbnltZUNqU2VkV0xWWm8zQ2Zzb09uQmhPL1NGeUovVzVFZG1UMnhVd1RtZkRFSmNSNFdOREhTTHRaeHRJcng1U0ZqbEFZQnZscU56OU54Uzk0NkhYbE9UcVI2Q1pSZ2ttczlkcTNDRFQ5alA1NFBrRW9XeERsd0ZDbDlHaU13WElWWXEzR08rajNxRHFCLzUxb3NaaGVLcDZOMmFUNnRUdHVxWGRkT3ptR2gzRmdta01SdEx3Q2FSVmFDYWlrVUlxVzBTcDBlbnNDcmYwbEhMa3lscU5hcnV4b1p0QjJ1bzV5TkxPa1lvQklBR1lUdERrK3lqL2g4ZW9nQVo4SzUvdXdXczc4M2grL2hDeUdHajVIUkxQNmorVDFkOTVuU2NyZERnQk5jdStrenBZRzhGMnJBWHFzSFkycVpIY05MMEtDcU55Z0IvQ3RjV0ozL0VLaWFic3FQOStzZURyRUVJU1NOSVBidS9NZk42VFFuS08xbFpkZEZDbXhQZXgxVW5mMGpGd1huZXJsRzhUUllTZ21FWHR0LzdNbUVjY2ZuNmtZNkdKaWtMTzRsSzBqRWY2UWIwaUd6SkVaZEF6YWZETG5NeXB0dkhBMG9rUlhyRmk3WStkU25KU1N4V29RZzRYWHZ0eVcvTUFvUTUvZjQyZWh3dGtZQmVlOGtUdnh0R3lOckFMR3laK3ZMREYzRm95VzhFMnhldWRrb3ZXSld6WldMck9KVzM5TStiWW9GTHlQamVTSzJNbUwzaXZmekJodEVSdHFsNVdXMnJYWWhOQlNWS1dxZVBVdThhazZaYkdKdmxqNmVHeHBseUpDVXRtOG1LTzJmZG9KZnMyUzdDbHhQVGQzL28yaHhEak8vbmFZTWN6L3JZeDJQMk9DK0txWURUNGw4MTg2c0ZxWWdoSDNxZFltaVpzU0V6aGliOXcvSDJjSWsyUHJEY21OdUJxV0xiUVgzem56a2ZHdXk4cXg2TkcyU3BkdUxMdGdkeUc1WXI2NUZLZ3lINE5weDVhejJ6MkN4RWp1ZVlndDQ0UStOTDBnK0s2Z2Vkc0ppYUZWYWRYd2RST0JvRHlwc0dPRFNIaHhxc2R2WVVnL3BDcWlPMm12RnMzUks4azlmRHpiRWgvZThBd0dqTm5RTG53ZlhZZFRQRWVNZDU2ZXZzZGREb3hkR0YwUExKNXNBR2R0QlBSQVFBVFVjNlZPV0NBTENHa0JQYzlFTnlTbGxVbmNJMCszZjhzK05hdlJSVTRtOUpUbVVKQlF3bnBaaDZsa2w2TVVEL2k1aFdYWHBndUxBaXNzUmUxM2ltSzJPZkx5Rmh6WGgyKzFjb1Ezd3BMWnRzaGh1dGpEd2cvS1JNNkNnUDVsUkxQcUpIT3ZSOGFnRGJFcU0vNmRFVTU2cVk1V2h5ZXNwMUdWYnZOTVlmM25TOFUzSGg5T2o5THNqNDhKNmpMRnk0QXAveXhncXkwSHhtL2VHMXNFRllFRk5nakM2b0JWbXJKU0ZCSjBVM1dOand5UmdTUEx4R3V4enZPeG1rditiV2lZK0xGb3JRQkF2cUg3ZFFsVlFiQzQyM0Y2eUhzSDJ5VmpoRFdNMWxjUEY0VkFuanJKd0d1QWhHZGh5d2JXN1dHQTVoWm9zZG4wMjVzV3VtbmIrQlBMbE5mNk53OHFnbGNaYVlUeTd2NWlUejlNVDl5RUNoVDAwYlFiSkpHVDg5emp0cG1odU1MT2dzUU1QalVURkJSdTgrWk5QVENGd05IdVROc2VVdFhYWHhWZmc1V3VqL3pEMFpzYzJJb2xuTS9VaGJuUm1vaHRaZnJjUVJNUkt1MmJHckFFTWJYcmx4SWppMk16YktncGxvMUJjeTB3ZE1qZkY1UFNRTGgwQjVlb1FPYzk4dUZoWWJXcm9vM3Vkd0RONGt0WkoxakJDSW4vMW1GSm1tdFhyNEtFaG9SQktJUk9hdHgxOExmVEppaFBzWHRzSTd2aEZqOWFMYW1PSzByb2ZXbXlrYmM3ME5ESTBjcFpFeUlVMENxT2pnQ1JTNmp0eGNrcWlCVkxzTEU5b3RlYzltcE5EZTJlL0R0Y2hrVzdyWXZFangybUVxWXJ3OXN1Y2lWdmUrbStGWVZxaTBJZEdZa3k4VVpnWHVXUXAyd2ExNldjeW1IVUxjTXRaSG9nSnM4YzRyZFN2TzhNajJIZHNuQ2JvcG0rSCtyN3hoMnVGakNiVWtENXd0L2RlZWQyaG5NZlQ0ZHo3NXRXS3RSUUMwSzJxWlBMUFhCSU5SOWlLdVRTbGZCbEJZaE85bGFTK0E4S1hKOGNIRFhGKzlzem1ER0ROalY2VllLRUpEalJ3b0xhTFlGM1Bsd1VXRUVYNmtBVEg4aHBEZ3RLcVR5VFpmQ2RmcnFOYjVoc2lLLzBuMkxJMVN1MncyTGhkTG5zcVpJcTVYZUxGTzhXMTMxUnkyR0RPWFlUcnhZS2UrU1hDSzBPRzVsbTNDUEIrcWhoeDhmbFgweXl4cDQ5TDU5Tm12SXU1enJEZ2ZNek1VcUliUEpiZjQrd2ZHOTRZcGpYTi8wK1AwdEErMWlDQ042Z2xTSWJleWRrTnhZc0pvMFpxWjFuSVFSYjNIZURvU204S21FZGNkemt2cWdyTnVVUzlhb000TWliVk9Eek9KVmJsdUZ4TThRS1RiSDY4Y1hMR1NXM1ZpOUxrTjZXcW1BaWsvSy80S1M5bC8yVHpsbmJMa3hodWtUcUZXRDM2Wm4wQ0dyRHJNSzlidTc5M1VreUk0dHhKWVZBUVM3S1pmSWIwZ3dnbzYwWWtQZ0RyV0ZCWktPdW44U1VUMDJ2RmtGcVdlSTRjMW81M0RDenBWemcrS1BUNUhnMEZ4T1diZGVQNmc4Vll2K0VSNkF4L1JreG4zblBnaEFkUGtHeExDTUx6aytGdjlLc1dFZlhPb3djcFZKUHIrVUd4dHFNb2hGaXpEMmNac3FpYkZjTVdyQWp2WjE0bjNsOEUzQk5ZKzN2UVovWStZem5kRFVNckpxOTJhOWRWMkZLb2J5UEl6Yk1VUDZBR25zYVYwWlRxMTVNUmd5NVFtSHhjSmIwQ0R2RFg4OU1mcFBpMVFDK3RBRnRmdFVZdkVKd21TM29MYWY0dnVoVVYrai9SQm9aZnF4QU9KYWpGRWlSdGk1dW5pS0NpcUJoRHI4QktPd2hTVWwvd3N1S2ZZaVdQdkY5UTNCRkIxY1Y3cTI2dVlPWVcxUjhta3lNb0dMUGY4MUE2RlNJR0VTU3QrbTdTQVRsemlwNndGd0c0RzgydTFmUkxRNnBIcjFISnFKTkpKbFN3ZGxKeHNrckwvL1VZcmVwK0hnWThTYXVocGNBZlJQeFNmZkRLM2JTQjlaaXJubDYwUFY5VGZyMVFDRzA5TnFqWjJKR0gxckt5c2lEVE1oc2Z2S0VCRGRtaWIwOHJEekdoQktIR1NEQnI1NFlMM2cvQkdXcUVBRlo5UGd2TTJQUzdsbmJDcVdYQnh3blo4S1ZaN3lyT05lMUZPeWQ2R0h4ekRJMGc3SnlKcjdCMlN3dXVDeXh6dkJSMWU5aENGTm00dGU3bWY1WmFlZWJmNzFma0VpOUtnK2NkU1JaaHk0eWhsRFd6dGcycG1mMW02b1JEL3pKNytobndxU2prOXlUQitZTDJDbDBFNjRIUTRVZ1c4YkNiNzBJNWpWVi9Denk3SHNRUndFOHJSNGhlNisxZnIyNE9DdFhXMkFXNnJKZnE0NGpEVnFlL2x6cjdzZFFGNENLaVRuT1lvMjd2RGpFUXNWUUs3bGtocUM2b2s2cFd1TzlYWEV5VXZ2NWFLa1ZLME9tTTkzMEhyUExKWWdVbnJQZkFZMnJWRkRkeFZDaDcwa3JYcW1SeTFYLzMycStuN2dZb1VvMVlRVGtHSlNPZ1pXbXFjZllqTE42dXhwYVI3VU9zQ3dkRkVmVnZNNzZ1aG9DS0VPL3dBMUE3YjJZZlFTTkJWYkpFeWltSDd3V3NySkVES0FDZ3N4ci9mTHNMVWJkM1BrQmZ1TGIxMFBPUDlnR1QxWUtNR2ZjdDFJbFR4a0NLUDR1RzBwamxyTTNsUDlxcjE1UUR5THlnTXlVOFBlY1lmTmExVU5OZXp0REduSUZMRFJremZLbVlramFkZzRpbklPUTRSdThCYWJRZTVPQzQvVUtOcGlheEhsSW82S29FeFRDcEtyNkUrZEJtczdlZ3dWNmErQ3lCanFvMmFuMlRyekJBSExMMUtjc05HMzRHWDB3OFQ5c014RFhNZUFnTkdsbG4zOXByYUhlNllvVWhLVFArb0hZRXEwdmV3WWZRZVhLdmRlMFBHd0xNa3RFbkVab2lxcmg0SVF2VDRxRnNaL0RicTVub1lNeUNSeDMwdm1kbm40andWRmplbklOY21yNFY3U0Jtb3ZNZm5haGJSZU1qekUyUFMrd2lIa1BGUGhQdjg4ODJkNlBwUWkvZ2gzK3Fid1c3am10YTlrYVo0TXB6eEUweDFMdnNvdDlTRjl6VTIwSCtySGpIcDljdEcrZ2MxaFdwWFloSDNGWGthUmhRdXpxV201RWQ5YitodmtXUFBUeFh3YjF5U1dlSGptZytqRytSRzhTNzVTM0h0SGJSczFwMmRONlF4Zy9qU3B3RUdvb2Qwd3hKREFONy9adkxUSm4rdXRmUGV1RzNza29YVFFCd1R0ODhJWnpDVTVTMkZnMTMxMmJCbUt5Ui9uMkhLK2QrREljdzlCMjQxcmh0TG1wNE10aWhCcVhSQ010VWtNV1hiaFg5bUxvcUhSY1dNN1ZrRkthQ2VrRVEvYWZJcFFtK0dxQ3hSeUwrQVBENUZMWUxEWjF1bkN2bzhnMlYzbHBwRWdNdjVza2N3VVJRbCtXcXpteUpWV0d5d29GVFEyUzkwWjMxd0pSK2orR29uU3NseTV4Q2FSTk1saU9VcDJPU0RXaXpyd1pUVXpRNE4zWjVhTmFsVkdkQi90Y0NjZi9mbytabkVZZzlQcFFRbm1nUlRpZEtEbVZOU3cxSGJwOWx4MmNaY01Jako3b0srRk8vS1p0OXZpRW1oTHdEeGtNR1d4a05xOS93OEhadm1OblpNZS9WSm9KWWFaM3I1TjJUdDY2Z1g4QXJWNXhxTzdyYTBINFhrQWYzZ2ptbHdKbzUxajZoM1BIZVl4ZTBiNUt5K2hvYWpCSVBYaFdjelhnc2tBZmtGQ0NIVi8yYWJ1OE1BSUdrdmFnMU9kd3hVazgzUTVGd2FLZVlka0ljdUJZSTNtRDZoWXhsaGNFYUJDcXBkL2FCdDFzNnlhQ3plckV3dVVvc2FaR2Q2ekdvZ0dseW9XWDZqK0dUd2xTK2E2alFwanZvNFZyeEJmTVpRL2RMdzhXSmgyRjE2Q3J5YWMwK3RBdGNZNEorTVlPY3o0ME51emVOeVhDZUVuSC9aaUdlTzE1NVk5V3RrSExsZHUzamc0TmpkclRoaHpXZUJMRWdZbVJTNFlSanhjZ0EvRVZtUUc4ZFRvSmhDaldjTExQTEdJcTZHN2Q5M055azg4VE5NV1BhSmlpK2NaNHVYamVPVmUzRXJQMEJvL3N5bmZ2UnZkaXFNNEhBVGxrSTFGZWpXVkNNeVJQSm9TaUp0Rjk1QzhGQWR2UXNWazB1U3R5MFNvSU5RRHVtYkxoTUFsZTVNbHdQaVIxNkdCOXJXelk5Y01lWlNkT0VZdHNQVzJFa3VoVkxqU2g1ZmRTajAveGNSVTdjTEw4cmRyeEpZZHYvWTEwVWYxYWZxNXBHUkQyb2I3TSt2ZmtHU2JweitVVEp6ZGp6OUNLTktYY3hHaTFjOHFneEVXTXQyY0dNcVVtUnZQYUxiK3NlTlVXMVpzalFxRHFocXlBMW9QRFBmcWQ5SDVQQkZPdnR5Y0dHckVsUkxQUFp0VGRDYWZpSHc0VXlnZlFpS21HcTJTem8vNnhRejFvVDkvb0NRWFZzenVsWDd5SE92bWtLd251RG5WcVExV2dEeFV4Q2puRHJkMnVSdUpmVTc3dlJXQTQwa24rWXRPOWhhb3BWKzc5TDNoV09MbUszYmJrTmRwZHQxTFhNQkxYczRFWG4wSXdwNVNGZWNtRnAwWFhRa2lzSUFPKzZEZzB1VFRXbDFySE1EL2NFSW81WEsxdlI0amRRNExrc1lOeFhVaDd4Z21wQVpJYXhHODNqUkFSbHRsa3VhK1ZjdStaQ3Z1Q3NTZ2g4YUZQSDB4RFJaTFp3b0UxbmJSVFFBbEZvN01mdTJZV2V5d1hEd3E2dlpBZno3bXZqVmdQNGdhVlRjU3Z0Y0xQZFRHWFUzQkJ5SS9Ibi9nVFpUKzN3M0ZPVmw5WWtzamVXYmlCSmp4QncrS1hMaXBjNllwbC9GbGl5KzlML0hKVUpZOHRrNlJqZjRhaE80b3BPMktsWFNBd3hSWTNLTGloRnQ4NFp4Wml2RVlOT2hHTzI0UTJnR1ZPeWl1alNCTE9GM05hQ2xMYVZvOGQ0aUVGVjh4WE5PZ3YxYlFxS2hFcjZZdmdpRG1WcUlkUU96QVlCSTloZ0VKM3czV0RrNnBmVXlKTDJqSEt0NHhaMHBTbGFxeDFteVN5OWI5Q2hydFZuMlQ1U3pXa3Z5MXh3YmErTjV2Y3llK1ZSWG42bk14dUVYd2tOMk9kcll3QWtna0RiNEFhdHZMMUQrOU1yQWFWNUsyeVd4ek9xZ0Yza0tpandBbms1M1hrWGNuT3pzMW1zZndvemF6MzJteHhlRkwrOGlxL1dkSzV6ZGE5RTZxR0xBL0ZadnJ6ZzUwcUxYRmU5bW9wRys4N3J4ZnpLOEMzMktqbkg1OG5acUxObGUxMThJTHM3bGpKU3JYZGZVK2RHN1hnWlE3YUVoRm5kZmJ0K1VjeGpneXFKcGpENzM2KzY2T1pTOU5Rd1I1VDJqdi9KNDVocnZWRGl0YzNnWXJhVldaeXZzeDV1NHR4aHVUZzFiU1oyN21QYlppakNUSWhIWGp5T3p0NFZRc1hXVzNrSEdPb09hajI0cWZjTmRGdE03VVNGeUNUNGdFcjE2UG5DWTdhZ290cGdvWXdIVXN5VDFWWXA2KzJma25kRmMxdlg5Q2d1TU1zNzVzN2JneUoxbkcxRFZ3Tk1sNk1DZC93NFdxRVM1czRTelBrb1pXN2g4RVNMeDEwZWF1TG1zMGJYTURYZFZ4SEhZLzZ5MWxzcDVJVEZGTnBuRlV2OGJLVVBUNGhtclhlaWdaQVRmc1YyL0kvUVd5OTFudkZvKzdHRk9Oc2c5ZVd2eHoxNlUvRG1lNjJ1MEhudURuRWpwTjNhbDJMNlpuclV0VElsRkIxRUoyV0tIcmFXQUdLZTYrb2VJa0VWeDE4NWhBZHV1VnNya053MUY3TXEzdmtSVTB0VXE1SjZ6MUdaQ2RVaHNSUkNBbEM3UEc3UHU0K0prUnNCZDhLTDJKK1pDc0FqTGN2eVVnTjJiS3dYeUpFOE1BYmN6WVpKVC9mOEpTRGFHM3R4YThualAvU25aRWxNNFptd3dGSkhneEhaMkszSHo2R2RCeEIxYzl1QkczOGJwdFRHblBjRnN4L09kdDdIU01QTVhOL01OWG5PRStaNnhzdTg0QjM1ZVBiOWhYc3pZMjhvZ3puVmJoL296Q2tUWDIzeHJCQitob0wrektPVGZrV1BoMG1qbUhKbTF2R3JyZmFoaTQveGk0Q2JzL0NTSEdTYlh4ZTM3NVlKZmFZOUpOS2NCSURPRmhxWjdzR2hNS2taR3dIRXk5UUZuT0dpVjU0c2V4U3NRUyttTWVFWWZEWmtIL1l0eG9mTzFub3p2T0h0c2ZvOEdDaTdLb200TDNmYTBwTjJGTzl6dlRYVDMyakZCZDdNWTJkK2l1dWVGdldFMTg0bEJFSURSOVd4RnNxbVpqc1hNV3JEQ0NZcmtwUVluQXprRzEyZWdBV3UwdGh0czRkZVA3MCtJQjEyQ1ZkYjIweUx3bkppbHY4bUhwcVVBa2pGUWsxdnc0QlFJS2QyRkdKUHlsVXMxM21KK2p5aFZ5LzJDVERGUS9ieHBHelFTektDTGhmclpUVUpPRDd6dnhBaklHZnZqMmpvRFB0ZTFyVkVTZ2dkVmlFK2l3NDVCT0NTMDVuYnkyell1VTFvZ1pwdHFZbXlMaVgzVUpKR0NkNFdtcFJoNzRWNSt3ekF3Q3FsTHZhZ2x6T1ZXSXh3Z3FENmFlNk4xMnpzQ0JyMkVHckltVUtJbmJCdUhueEFlcklIcTRLS2F0M080UkZWN1ZFVFZTQjhzQm9qNXBtN3FPQ1VQaVJueUJUeHNSM1BpWXV6OXE0bVFkeWUvTEVrU0VQNC96blVrR2kyNnlNNzdIdW5oSk1OQmpwdlRjbUMwcXN2bVJzejRmQU5oMU9qYldNQ1l3Yzh5US9DWDJIZDNtSFRPcTBBam1UN1JkRHo2TURXVDJ3c0gybGRuTEl4emxTZU04ODdVZ3VJL2ZVTHg5TUl3dEkvWmpvUmZLSVJLbHJGaG1wOU5PUnQ4SXowYVJBYmxhNTRyaVNzTGx1TnkzcTc4dFFvSDYvWjlFR0lOdExZQm1KYUl2M0Viak1waG1ETld6RjRLUHFCNlFqOCtkcHVTTXVOd0VabnVwdFU3RHR2dkhRaVRuY0lzMyt2cUFIbjljY3BpWGRjTnVjRGc3dktLS3pPSUFlM0FHVFhVbS8wMVluTVhXZHNVMzZaYno4eU9TOXJMNEhEK0FzNk5BWXZHYUN2WnJ2akZPczFXZzRSVWsvMEFjWjhMWlV0TXN1cHI2djdWbk9icURmOERMbG1QdS90SDYxTlpKWXlVT2hrRlpycmNRSm5sOU5LL09UeDdmZkZvRFhSeE5FakVYSXlVUXYxcUtRSXRMUm1RK0VvTjRacHhUb1pFTmV0ZS9NU1RYOENMU3JRQ1JUQ2xPakVNMzF6NUw2V2RGa0tLV3lMeVplWHhKOUVySnRjZlQwSHNIZ3BpTnRoRFNySjB2dU9hZEJGd2xjWTNyS0VYOUk1ZHlkT3g4VFNxc0hRQ1dPYitjZVFzMlczMDUwQnNDbFBTU3R1Q2hTaXVDVXM0emZkaHgvMnRVWWZUVUtSNnlUTHNrcmgxaFYvQ1RxaVBraVh1aW5KS0MwTE1tWTlxS01KaXo1TjBPSHNVZGdkVXNuekhwWXFUaVVwd09rQTJNb3Eza1RnYjNDQnhhSGVJNXVQYm1ZVzVDTkx4VExxckFHOW5qaEkwTTh3ZTlZZ3VXQUFUeDBhSFBMb3BVaHdMczcxZHVxYzl4MmdpMUJVMzBzOHQ3NG9aSjFSNHNlOStLRUNhWllBR1ZkZXBUNTlVSGRwREoyaHNLVkNQZEFNYjRCTEJFRFlXakVwT0k3QWUwWEoydDZ3TGh0aGN0MWUrSG5GSHlQSjZIMVY5VTBkb09UL3BEQ1htajV4OFVZaExTMTVzWjM1YUFZZWtTdDAwK0hUMzdNL1Y2aFdpZ1A3M1EvVTVWbGZpYUhzQUxlSmtyQXJKbXdSMTlWNklVNWdtVzI4SmhDVzBPeEtBVnJ3Z2dIa09JSHVIWnRjcSt4NWM0VU1uNFBYQXhsL0h4WXowY3ZLTGtlRGZNeWQySUlVS1k1MkdEYTBLa1BxN1JwQ0NEQlJJbDVrdSs0azl0UEZCSER6UzdHQjFKUEVtMmZWcnpRejczd3hwRVAyY3BSQ0lyYjZOYVdsZUtzMkg5M2FyWTdNMTBUU1JwMVhXMDVtZU4xM3pLaHlNbzY0K0trWUdHNVN0OWxPbHkrcUJoQUtkK2s4VTM3TlVzZnRYcENDU2UyNVVyMFF3ZHk2TDBuNFZOMXI4M2FZU244dzFrSEszL0pDSDZFaWdGbjFlVjRuc0FVWDZoQkJJd3Myc0l1SjJMTE9tWkFjTUU4eVBvdFpqSWIyWXJRalhva1djMXgwSTVYVWRQMGNXVTdXUnN6WnZLQnVUVTF0Smc0alBhZHUxYkRIMTJhdHZwd3BnUTY1SUpHRkxwMHhLbW9sSVRzZGs0TUZ3dUZIRXJMNmN0SE5GaFBrcmZHMzhoakk4Tkd3YmQrV1NiLzBqMGp5VVczR2htKzJXdTVDWVFaN3BhSkI0Y3FoaVEwK1BoM3VncDcrbStZKzFjUTd4aGlrS2p3SlY4TGVDYXdYQ2p2RjZrYVYrNzlXQ21uWVZjSG5SZXlzU3VkQTM4RkdZWFpvT055NTZueTVzcUNxbktRN1VnbGFXUjAweUk2VmY4MTBhVW50RHhJbll3dU5RQUpNbXczd1hYeVprTWMybjdpR1lTUDZxc2Rzak1PdkpJa2RXZHlvUURoa294UWRLazZMWUp5clo3ZkNEQk12RFlVc1FjdjhOT0l1bWRNaEdFVlVHUmptb3ZqMG5FQWdjbHJaa3ZrMldIQ1g3OTErd2Zrc0h4NUJnYjlPYWgzMkk4ZnUxd1BmT3FWNU4xRkkvUU1KSWFhbWMrbHJla2RMelF5RUFEYW5jczBOZXFQc2pLMmx2aFhMdktlTzJLN2JQQzFlb3dxTW1mSmFNcTNoZWhmOG5TWjI4Vm1FbklQN1BFNldqRUhxWEYxaU01SnhFaThlc0VmVEdLQ3RmdjJ3SnhPNEdYT3k4WVJDTjFqcXg1dDkvRXNzV1gzZWpMS2tqU1JFZDg2c0xyR0RiSXlIM0Q5b2NzN2Q0eVB5c0lodXc2Y0xrWjFFd1VMdjhveWpJbHJCSFo2b2YwazlPMjdsKzlsMjR5aThwUFdwRzl0QTRQb2tYSWRuc1VnL1JqWDU3dncveFNHdXRET1dWUVQyY0M5VWp1SW5Sd3RKcjhhNGJhTG8xVDNTL28vaFdHcUVTOWxpbXVVWERjQ3JtU2FIL2EwZy8zM3FhMW5HdG1EUzExb2lsM2MxNUFQTU9pVkExeXY2S2hrQjhSU2hxZkRnMUhTUi9lRldWcVFMRldwMDhYNjlIQjRQUFlhQjI4bGFpMkJabU9sUUtrWVYrZVZsOUEzSVIvNWw2SUtmNHBvdVVudGxjd0FGQ1l0VHMwMzduVnIxeUh6R2E2N2Zaci9Qa0FmekMvY3k3VjVCaXpzbVhMVHQ5K0hTRWc4ZWk2aUNpVVVFKzdyWWg2b1hOL0ZyWERpS0FGK0pXNFFWN21NT0NSOUt3QVZYeHNBZ0ZJcERLdGRhV3l3VXFvQ3RJMWh5ZStiZWVhU0ZsNzVtbUxONDNaTWI3QWpvaW5DZ05GUkppRVZxRG54ZmREWUlCU2VXb2Fpek52d0JGQVRCeEhRV1MzUmNmTHhFdGpVd3FHNTNQL2R6KzJCbEVBaDhSVWhmYkNvakxtS1k1dFQ2ejV1blZrbUdjMWlNWUdnMHRRQk03Y00wai9Bcm4vQzh6M3NmaGFYazlOcTlvSVVvVUFRelJVTDdSQjJBek9Zb25CbWtDYTdhdFdab28zanc2cWhtdXNPZk1RTGx2eG9sbkttN2plMGNtOVoyNXlTV0oweGh1N3dzMjA3eElpNlVta014NEd0NVRmTkRhV21xUzErNXJyRlpCSmYvTjhydjY5anZDYmNwMTZXL1dVeTVRK2ZoelNSUnpiOTNIa1NiNkdCVmVCd0ZkU25jZy84SEVKTTQ4VXdFdDJMMWIrS1VQemxCb3hlL0RENThtY3crTkpMRFJNRGE1aWJqMjNQY2tMN0NyTVdOK0x0ZThaUTM3aE5LU1NPMGl5Yjl3eW8vdEpudkRCRjgzbWJnUFVkbXF3SUtLemg3WjE3QmdyTU90QjA3MHB0cjVsQm5nNlB3cE42WTZWcmN0OWxPV2wyRGlTZDBMT2lsRGgwTkdlYjRWZXIrQ24yeHU0WCt6aEs0VDRuMUlSVk92R2Jxa0tUZ3JaL1ZUWlFob0NMMUNINDJyUE03TWFVUmxRdG41L09HY0J4SHdFOXczQVUzOGRrYTZpc2tqMUlQeFFIMjR3Rkp4U0NyQ3BrY1JQaDlpVXlBREZJV2RIVWZwMjEzMjNsSjkvdnF0Mm5pMmEvVGhRU3VZU1dmZy9SMUgxc3JGeC8wcmtqS09zYnN1RlM0bFRRZ3cwODErNlc3eTlWMWpDcUx6dTIwcHAwS0hiQ2d4M2JZcFlCOTBYMWZDSG03cytrTEEwTk9SQlNTaWNQRk43QWJIcURISnBhTmtWSWtDTjUrYzlQZGhyWDF0VmxlQjFOUmI2ZC9hcTBnaGh1TnVwRlpSQ3ZjaUZyc1l3V3hVeVJLWVRIWDBtUnUrdXlHZmxtVXllYjBRbklreXJuRUNHbTlOVlAxdkxESHVmY1FtL2VPY29BRGJyMkg0R1JrNDRZbU4wRThSWjBsbGlDTk85dURaclNIbHRTM3RRU2FDV3JQc2hyczlsYnU2M0lKb09xT00xK05uTytEYXpqYVRNVkhhanpCdHg3eGdrVjMyMThBcjRkQXkzZUZhRk4wNVd5YU5mNEZTd0l6Y3VaRlhFd3NhVjg2ZmpVeDA3K2QvTThFZW5lKzFQa2pxMjQvdm1EVVVlOUpKTEhsMmoydVNEbjhITjdHYzZ5UWlEOERpVlo3RmNtSmw1RFFObk80RVlDVmpEM0F6bytCdXJpM3hxKy9RL0NPYkdGVG5YNjFrOFIvY09QeElVd2FkalFrVUFGY0pqSDNna1Njekg4eDV6OHdkak10bGdyYnJ1c0o4UXZhY3B3cVloQ1Vhb0RzNGtjakFScjI3RUR2THNjYzMzMzQ3L2E2SktMc2xKa05ueG9UUElqQTg1Y1V3Z0hXMW9DaWtlQmFiRHZyOGtNZnZjcEJKRXF5bWRtdFkvUzRkaXUvYkVjKzRBbHVCdFJlZGdxaXlTTU1JWVVwdVBuS3BFcTRmWkxLU0IrdkozWkRNN0p5ckZiMHVtTTBpUWpPRnhzOEpnY2VseWJFeENoMEh1NHIranBmZjZsSCsybkhWdmVHRFFSUUN5bjBwQS9uVWhSOHhuTXpBWGdKRE9VT3c5TnRCVldVb2JQSlFmOWNUcE9kRW5hL1ZOMHJtZDV5Yy9nZlBUZlFJTjlNNXBIQTF3RDlGcFBuUGJTb0hhVjZrOTRmdlNqMTUxaXdlVERpTE1lQXQySS9iNkMvZElXdkVUemVGNWIxaU41c01acFNxOVJxRVhaQ21wMXd1L3lNYzU2M0piak5yOWVBVDBuR0lvWEZERUZPT2lwajNaM3d0Ukg5UEd6dHBVWG9iempvbmFSWGc0RjQzT0hwWWxGQTdLU0NPbTJpYjNKdWxMK1lMeTEvOHZKYkNvSXZIY3NsNWtrZkFqa0lzNkJ3aUZUSXdpMlYwMU5TWFRCWDJRbyt3UDBKRkQ1cTFYQ0ZWYUwyaUFGUS9FWkpSS2RwcnhNb1M5cFJXeUFHeVVHQlJxOHhsTXZ5WFA0ZTRXZXFYVTFESDUzTld4NmNiYm1MelVxUDVpbVZSU20wc3VPRmRlZ2ZnQnVORWJWYXNkMUl5ZDJvVXBXOXNBWlprbWt1NTRhUmZxQkNRUTlDZ0RyVE5FYUJ2WDlUYUk0WTNsdW42NlJMQnpvWnZhVzJWRzNDdlRqZGVOSDlmUjYwc25BcWc0YnRZSHpJcy84QmlKaEdJa3JmV0ZTdHZOUHlKNEhFbVRFeWNlSUlrQXVaOWlsZkJvL1dleDVHRFFkL0FGTXY4VjdTcUt3MjdnQWNYTmhpRzZ0ejhOK0Y3TG1CS24rM3BuTThKdy93TUxJQ1FCaWJ0Uk1KZ3gybXFpTlRWY0FPK3FSa3kxTUllNmlmQzljR2NXOWhncGtsOGduMHVGNjNKU01iUEJ1ZS9QZEdoN0VzaHAyOC9XTHRPUVFjOTdUUHh3L3ZiL0pVZmdDQ3JtS3ZMOTc5RVlrR1hoSW03RVAwaWtxS0UrZkRITTUxLzNkc0t5NndKdTFTdU9ubVRQK3hTOERPUTliRmlTN1J0aytpSWowRDh2dE1lZmYvajRUY1k2VUNtOGFOa1pVRFNtZ2NNTW9ldWI1VWM4T1V2ZGd2eFp2RGRIb1Y3aGY0V3VjVEY2V1pEZGUxMnhkc3FaUjE2WXdYcUEvRFAydzJJdXhaVE8zMExDV3N0QlhDQjdCUnVjZ0krZmd3YkpoRFZnV2F2cUxHTDZSUno4S3kvUlVrb1cxYWc4alplVW9WcVhLSUp3ZWRFb2I5VlNHR3RSTTc0aHFTaHRKT3dyUkRabVNlU2cwTTdoVUh6VlhHWTduVHlkZmdKaFI4eHozNHNMaVFLczEwc3E0Y1RsQVA2T1JtR1N1ZlF1N2g2a2dUYnF4aDU5YnptdmZsYmY2NHpkR1JjR1FrcVdVdnFLZUd6anYyYXVXRlRZRFFJSFFaWFlPRTJFeE9QUExZemZ6SCtnMlNMV3JCL2paQlA4SEN0ZWRUU0xjeDBqdkJsWU4xS0cwS25ndTN3eTE5QnRtSU5HOG1zaUV1QU9kVk9LZmkxV2dqRkpPekhaYUQ3eFZPS3Q4R3Z1YjZHN2U1bzZOeExpRVczT2hUQzFRQytZVGhWUGE1bGQ1SjhRNEVuRzF4YlNhL3RpYzZlR2p2cjk1aFZxNzRCZ3R1TFgvT3daQnJVeURITVhlWWlhL2ZJUDVyYWpmQSsrVm5BL3ZaeHl0VHU1WmIwaEtXQlorcE9NZkYvT25rdGs1eHoyL3pwYXAza1QrN0g3Q1dnMmxsa1ByRTBtRnhEZXJpUzNZekF3SWJsOFVjaFBnYVdGWWIzaFpUOVBTdnoreDRrb0hpTllGdWNTbk8yTHJZemFSRTd5c1NzWC9aTzJIMHVhcXI1eVdHcEdCUVIwckNkdTlSMXBtejhsNy95Z1FwRGFNeUU0b1hkL0hRZmhQaGVkcHJMSzYvbjNydWVtVnFNbm5mdkxrWHppNStDN3hnd0lKRVVSWkdKSGVxbWkzVFQrMnBaYkhLRjFqRlQ4ZjNiREhra2J4aWJ2WkdZd2VYVzNTZGVweG1hV2k0NGNlbFRHMzdGZWpSbCt4MzFZYnluOTZKY3R0cUdlZmdRejlVeHdlR0JtcGJvMXhSVExlejIxYWtCM3BIYjBSQVpOTWpsOUJid3laUU1YNklyL1psWnNaOVBQWnFuUTNvZGJmRFU4V3l4OTJQR2tHMno2Uk92NjVOWXF5bll6d25MSCtYZ1RNVHlKNllGS0FWakphczVxZmtJMDhZRVJCZHZUdkFkVmJUVDBkT2Vtb0lJR0xobXMxdTFCVXd2amwySEZ3ZlVjdmNtT0R1S3J5VzVrM2l3eEV5Z0d2QVNoc0psYmRxcXpxaWZCVW5TNmJqNGlFbVQ4Q2ovL0d1Sms1N1FuSnpxazE1L0V2Z1d4Ymt3TmlpSXFiWEdQa2RaOGlIQVlpYTJDUGtheDNYNy9TamdSenFNOFpxN1ByT3BkeEpWMjVzRzdKRXUrNExSUGRrbE9TaU95OS9saDZSc21sb0FHQnFpM21HTHY0QTlWcXRMUXVnWlpNa09aNExNZWhIbmVpYm8xZXZRZVRsY2w4T2J3ZVBrQ3R0bU1ZNC8wb1ZQUTc4dW9SblgzYnJhN3JObFRaUDhKakM2bHAzZFg3VGpDQVdKU3pXdGhndGx4MDQxRVphcGtkL3hleXpLK2YrWk8vVFJaQkhSVXkwR2QwZ05lMVN2Y1paSkI4aUJIdFNjVlRVckFKaXlSSC9uNmFyQzZsN21BSWc5REc4V0l4NnhMQ01vdGsyeldiNW1PNFNNUFlsYU80Qjh3Q3RzNjdJRVlMUnQxRGNCYTJzL3p3NEVDcm8zM1htRXB0T0dmTU5QRFNEUnZzRW91TzBuQXowdXNya3ZUd090Tm1aSHRYZmg2WmtWM3NVK0VFdTBZYklEQmNha0owMDJWQkVYRFpXM3FLSWRDZGJaQllLRHR3Y1dPWEVzMHIxYURERUJtdHExM1NtUjJYdUxsRWJETFdhR2NOVTFjNjZtQU4zOVBZRm1mZFphbzN0N3JmZ2c0TGVGRi9OTEd5blkrbU1FMEtydmlKT1dVY2ZnaFNLMVR0VE5XS3kvTEVqd0hTYVE4bVYxdmozRjhtQ29aRVJFNXd6Z241bnhkTXNJOVdoMlJYUmhpaTNCRjNTZEQxdUFueUJLTXJabTBJS2RhbzlqSHdtanJHdmM2QXR0TStBZmRLaDAvdEFRT3plM2p3eVBUMVJtMDhoQmRCbFpvMnhKQjNUZWdlV1IwUFVUSTJpZ1BIRTVCNVpKNUVKeDM5VFR2dXV5b3ZrelhGY1htajJDNVRrcU9VRmVCdEFncXVFcS90WWdUblZaZmtFSkMyM21wb2hEdkV0RmM4M3lCYmRlRzd5MHI4MTVVMjc4bG5ReDAwQkxWc2pEak1kc3U3UGZsRjQxek9NczVnVkVoUUp0KzZDTTc4U1hQbTM5ckhoTVdQM2RxVVBmT1FtNnFKaEN0S2Z2SGpKZzZxeWlodE9adHhIeGhGdk1Wa3Y3WFhsVTR5aXAyMnBNRE42clBOZzJxNDhsa0dGV1ZlM3RmeXlFR0RkM0p1MmZwaWJySFVSSE9FZ3BzVFoyZGdxWFh5U1RacVZHdXZ1c04xWm5ZUlZpSXQ1OEVJa0RLREZER0JhZElHR3I2MGd2OFVKeVpVTThvVFBPU3grNitRYkM3ZHJMU054cjRKVlRFNjNmVzVqY3F2N2d3VjZISkk0dEd5QUdBb3kzSm1vUVk5NkZlczdUdVNxdHlwR2FDMThPcm1EZDY4MVJ4U1JyK1A4d2UrQytKMlZLdnd3QmsrbVpkazJPM1BTMk04NkpYY3FzbDJBWHEySFlZOHc4dW1JL3FkOWZIcHRicVI5Qk0zN3VrWXB4NVRIcEMwZDBaaG9MN0J2eW5jUnAvNktoMlV4YlJXU1NacVlHUVdtUGZwL25kckJSSFRlbnZsbWRqdUZzamErY0RWSlprVDhxR1FReW9zNUs0MW1uN1dobllrL28rSVliTjFTbE9ObjJKaWJLZTdWOE5taldBaCt6YlNPRm5LQXNyMVEvUEJmWkxsdUhzanVvL2dWdldLV0M0N05wQUttYS9YS2NsSjQ0U2twbDhuVUJTcXVveTVQMTRjOFpkbmc4Wm1yZVVNRk1xclJXWXJKYlh6VlJISy9qTU01UjdwbWpRYkZCa0c2UDBrK2hqd0JHOUlNZzExNkFmb0p3Skgxdkc3MXBsYWcrekZnZXl0d2tEWW5kd29MeERyZE5KejV5R2hYZ0hwU2o2THF3RUllbVUrNlJkT09iOTh5RHhUZkZJajZ4WUdsZk9aeU83ZlZnU3BDWnVYczFCRUh1bXpqY3ZmcW1wQnBENmRVMUF4eUkzbDdXNVVhQXd0cmFHSWU0V0QwTm5uNk9IYlUwV3Nmcm5pdlZzeEVIUk9wMTJPbSt1OUIra3kzcXg4WVNkbUZFYTlPc3Nkemx3cjlGTTQ4dkM1WjdSRCtqNmVsczJuS1N1UVpWbjNldWNvUDNSV0V3R3piVHc0Z056aVdmRWM5cGZkL3c5aUdsQm1VZWlRSGhONGJSdklQNWx1c2lpTmVveW9ldjhoU1FFSmNHcFpEeFBPOWFyM3lKNXQva0NXeGNLbDl2N2Q4MDA1WnpNR09uT3UvR1o2YTR4d3R3Y1l4WlkvMmRGbkRuVThVSGU3UHJjQ3VGYm84YmZuUWRaQVdEV3BmV3Y4dU1saWpVaEplSkd6OWpwd25FWVJ0a2J5VWJjbTJqaVFOOHVWc3J1bGpKK29zRXdiVEVQTkZ1cXo0Yk1VYXYzdW10QXZSa3JMMG02WDViemdXMng1bUpiYWEzZTBHbHpNNUFMa1kxcm1EM0dFdlNYUllBVXl2YzNiVW0yOU5SSGl5N0VuZCtncDNOYnVNNzBINjFKSEhYeWR1NitzYVFUN0FHVEVWTWpJdGg0M0NiSXJDQThpYWx4eHhkVUdpUkRMZWJmNnM2U3V4Z25ieEw0RlM2cThnZm45WisxTnk5TDd3S0FmTFpMcDJScXFVZEpTSVVlRDhxMzR4QWNNeHREYTZHRlNQOS84VzlXWWJUNnIrcCtSbVVJaU05azVpYUJSRHRVVGdiVjJwejIwakJqYk9jd0lVWVllZzh2TTBRTEw3OTdFU0hxZXdQVUhtL0taR0xJenhLdHNKSXZjM29GWmNWcnRkVyt5WlQ1eEtOd3JnWUMzUnlBU2hyV2tRVFdIbWU5U1AwQlFkRkExdDREdHExVkdVcGVma2dvUzlxSi9ZNm1TbzJvakFvWFhncG9nZUw2K2N3OHJ2cEM3Q2Y2YklNSkl0UTAwUExZVEd5VjExWGRCZDQ0ajlMcTR3SEUyVVB1eUlqb2JMK1NjY1JZZk9DWElZTjVQelQ0UERuZGoxQ2x1SkRGeno2TklXdE95Ry9udHpQUEZnam1CYmZaR0pPQnVEb04vMlM4Ym9hZ3hadHZwWXdwRm5yb011eGJQL0lEdVJBVDNiUmtvY2puVTExWVR2OFAvM3hNL3piazZ6K294Y05EQ2pCdzJ1MEdoTjJ5N2FJSDExa2IxSzBLZU9sVXNCb2tzMHlrK2dTUmxYa2lTTVBrZzlkSlNPNHFzWmtTVnRUTnhCRWxQc09GbW5qRGxXTUJKMUUrSE9jR1JqY0ZWQ3VBQnVjSFV4bFB4aVJsQzR0UjVNZ3h4UndVVFdvNTFySFJxZ2ZmUG85ZGFITTlPU0s5U2ZBbDZLWWYrbVY0QWdQMkxQamJ5OXZWWHFYQ0oreUpkR29VaGJlN1FiM3V6Z3FOcS9jUjRXcE9jb0ZBZGI2VVArUFhmaDhpRUFSY1FIUCthV3VmaktibXRnMGNSR0tUL3dVcEZJUW5NcDNMZDNyNWsyTGQ5Z2UxN2RIb3IyZ1ltRlRTRm4rb0dFYVMwQ0Q4c0pUbWVjZzFNeTltN056WDB6SFpmWkFQV0VYemVVM1kzZzBDeWt0c0lYeXowT1Fua3MxQTFSbmJUUXRTajIzb3BWWkZ2MndvRkR1d21XeExMdmtWWmtXeEpYRFUwWUZUMHBMTGZqR25TMW00a2xxakdMdUpOendpUnpVNGFTajhwcUx3TGtSNDIvdFlHbEk5ZHdVZkplNDI1Z09wVUQ0VW9EaVR3TTFSVmFvMmREV242OVNEZm80VFdlY3NZSk5IMnFNUGYyRmNqNTQ4bkdFd2cyeUxBcXBRd1M2SkE4dXliVjlNMWtRMlVHb3JaVU1FMldEYlY1ckFZUTkzWFJNNDZqTWJCQWhaTG1nR2RFZENibHRNTjFSQ1JTQ2o1YkhuanVudXhTOUVCU0ttd3B0bkd2d1V6TlgxcnBoc1VMVkIyWUZGZ0g4SGQxUVFBdkVBdEFndUZFL201N3R5Tnh3dUZZdGdmck1Yek9RTU5TdTNFaWlJRkUzclNYbGd5VHZjTHFDY2t5WGt0UjZvZGxtdEhrNW5ocjVzVXI1bHU0ZGdFYTVGbk9kcVFPVGt0TlFhUWhUaWRLRHg5M1lndTY5Y0NBRlYwQStMK1dSV1pWVjkxczRhRTd4Z1ZVTGhmY2VPeUsrbWhGajlUbkV5aE5hSHRhaTBNVVd2SEd5dWNlR0tCMVI3M2dOUHpUMXVtVmhQR2pwUmZUR3drUVZXSHpPQ0FiQjk1ekhwT0ZlR1pOWEp4djJjT203eVRZY3dnL1VnaXZnaGduMVNzNlQzZVI3Nlo2a1ZmUTh3bGNqYllHVGErckZTY2FQallGYTJBejdZMXFFb1ErWmRxaURIMVVXZUVuWUlOS0h4K1AzZm9lTU9iRUs2emkxUGJiM1dRRXpsR2Q4eXBlV2lpV3Q4RVRiMkFoc29CSHZvQmc0Wm5zMks5Q0NTZllJR25iTDFTNUU3Q3EzWFE3NXZ4b3Y0SW94T1h1Wk41dzRSOVRqaVh2OEZlT1RzWmltM2pxTmttblZ3TXk4WUVPcHJKS28rdHRrVlRNT3ErUVRFTXpESUpYTzFDQUF0aGYzZlQ1dUtKWk00RmZPZmFMQVBhOXIxNjRZRzAxOU1VKzhwaHZqdVM1di9BQUt0ZWU3QXBoRkZGZHJEMnRoR2U0UlB0TVFTOEVLbzBYTW5QSG9idVVLTnJUbFg3M2tlYmg0Qy9icGRVZDEvSjdERWFDRWFkeEVZdmlacEsrOGJiK2xyQWhVMVFTNWlPYS9LRGlndlR0Z25xUkFwRUVnU29DRWNtbE5aelIvN01VaEI0bnlpeGN0TVVmWVptWGZ0Wit2TXZZR0tiVkFrNUtMZW5nVlRsb0U5RnBieVVia0poa0Uxd25PSzBtVmxWMHM1WkRJbWU0UnBQK1NyOXRVejF5QUhZU2YyQnk1Yk9ZRFZqZ3NrNllmSmRiSU91V3h1SGdsdHk0RkVMZzhnN2Z2Y1VEdUNFdWtlMWJYcUlxYW1JbitGMEFsTzNQUTN2TGc5UFhkQTJpNXJNcjVEdHhLM01RYzNSU25rYkJ3R29iUldjdlovZDJNOEZDWTJydW54SFRmT3ZrY3hHdm9PcTIyUFdySnNYVnRsYUhTcjk2a1Rvczl0NC95N0tGb2U2cVQxTW4zYjdlUWVHRjZGNUxrYjMra0ZTNGlDN2tqUlB4aW5jTnZqZXM1YlQwNGIrdk9ORzBJZVk5UE9kelVGSThpaXZEVWMwblc0TnVVeDdNbGpqOXREN1Z1N1pKanJjY2grYlVuUFlMSVpkTWN1RXJNVnI5ZVdZVTZSS0d2azM4UURxY1RvOHRLWnRqZE1DMDlGNS85QU5jWDBhQy9KZGVCNHVwbVo1eWxvWVdydjZDZG5LY2JHNzJoc3lCS0xXNHVKOWt4WnA4WVZzZWVIR2g5eFppTVE3VzlCdS9hU1pEbHBzaGRGbXZkVks1cVVoUGo1T1Y4MDNKZklTcTdTaUZGZTZrbkxtMUlOTUlnbVhiSnJLZEhmSjc2Wk10a1A0V1BjZ3pOYkFwV1hoT0ppTmRYMm1UMzBIeTh1RTBEWXlDV3F5M2lkQ1hTSkZHV2hRMUNSN0pXdllaeXdiaXh4aWFRVEw2NEQya0VBdFNGTlpMYWZxUkY5WXpmS1R1TlVFMmxMNk9yNGhQTEZwTldPRzJXQk40bGpDYmNDUkZLM2RINmNta1pnajJoY0cwZVlMdXo5L2tlV0xzNjJUb3VrazJOTlJ3blpBNkYzRjR1UDYzWm5yZDdEWEdsZXhHV3F5VnBoQmdoM0Z0b09NemVYdjdPd1ZEeUh6YW1aQmxmQ0tSdGZzeWJyRXJlVE1EcXpzVjFWODVhOW9kNGtEUTlqTU9OaVIvNEdtM3EzdVhOek9lTUsyMGJxTUlqTE9UbWJDYUM4YjdJU05wYWtyV1pFcnkyajZ5QVo4RVNQVWFxYzU3U3pHRFNvZk9KRXBwdklqdm9FMnNCTjhUNGpqNituTTJDd1Yxb1VjS3NhRTBQWHBpVmhTRy8wL1lDQWNMQThYaE5aT3lpQlBPc2NzVk5uWUlNejJwWTYrcmROUm44amEwSEdNZWo5UXM0V3lrcGZxRkFKaGIzbDN3NzZTeGc0d1kydmtXaktQYkozSERqWisxaDlCeFdGTTZIdXNqZDJPYmhteDBQNzFZeVd6R2FBK25WRElKYzEzOFRmMUpUR3FqbVpIK3BFMTcxVmRUSVVaMjJaVlZSc1lHdkEzS0YxeVVhT09pRm1SSDZnSVZTeWdPcUdiV21DYmtlOVJrOUJaMnJud0FKNWRSNzlIU2x4bUVqcG1PaFM2Q2dtQlJoVXNCMXNIQjZDT2JTdjZQeDl0eWtZZ1ZkRTl3aXBINU5HcDEzNjNrOGtqM0o4WEI2NERNV1RRb3l0QXM3VmVidU9Gckw5Y2VvQW81cldNWkhST3Q3ZjJYOUcxdzEyd0dtemFMWEpUSE9lTExKMncwQ1FuQ0htcW5mNmdLVUVVbjkzV0ZRRVk3Vy9teGs4NEVkVWgwcE5IS21uMnZObnlEZm5uQUJuYm5Ib3JQOTRFNmRrMXlCRWJHMW5JdEFwaWt6WkxtZStyaVprYWdjVVExelhvTWZZdVdHbUlLdzlhNFp1K0xQWjJydGRuZWFsNHRjUmJoUjBBTDI2TUczVVlaaWhvYXNscGpCVVF2cWFHakFvMkY5b3RqSnVkMlNleVpBWTRtbzR4WFFrZlFKdlRWd2hBQlV3RVlPMUlWQld1b0VjNVpYem4zV3RkRVBzUVRUQVpTdEF5VW84a1pQZURkeGNpcHJhR01YRCtPYmFjdEhsZ1JKcGdvODhwL2FmMm9PbUtJcEFPTUZmRkxUbnFQczZTYlhRdkFmMnZJMmlMMWUrR2U5WnYraXVUa0xrRkRDZ3FOQXA1ZG9GWkxTMzh0OHVoR0J5MlREY1dSTnJmcCszNHJYTFVZUElTTVUxRkcvdmVXdys0aVY4ZDhkK2lsTlBIU0RDR0l5M2FkTmZyU3N1VXZ1Y08ra3l6V1lhdmpuL2QzbnZlUXFCV0FnQW9ITHlyS0JZUXdrVjlzNnRlWlN0Z0ZLS1FqZEtHUitkbndMSTNhRTZrYk9OMG9URUR1S1ZibXFSNXNVcWdXMzZnQ2VWdXBsMW5LRkIzd2V0VHkwN3NnTytNKytHSTVaclYyYSt6TmRHaUI2alhpckxOWDZsKzlqZFlhdFdkQU8yRnhBLzlTMHBYbngyOVJTWlBoUHdOL2hiYk1KTHFiMEZrc2NYWlEveVFlVlhXZWxGTFBYVGl1TnlFUkRuWDF5R21zajAyK0lRejlJa0Z1b3JyTkVVUitDZ0JmNHFSODlpbnpuODV3c1NhYUVSTlkwUmFnUy9MTHYrNmlWTll1cGh0d2R4UExIanh4U1BlS0dCd3RjMFY3UkVjUENQb2ZvYzM0encyRW1oZlZvNXFQcW5oa2FNTE96UTltc1dLM09GY21Ndm1kZEI5NDVvbkZXeVFKWkp2aDRKZ1ZOL3FOeHBxZGNNbWk5ZlhxaGMxQUkrNTI5c3JxeHFWV3NuVVRacFlMbXRsOTRVU0RHRm5GQ0hEQkk3TVB5QlBHNE1uWWQ5Qm9GTGlsdTlFSG82UDVIZ0NQWlNsTXlDb25meTVZRVd6dnp3LzNJRVV1WTRDZ1RRMUcyM0VteGNHNXNJT21mSllCTURPYVd5S3RDNTFobzBja1FBdjAxaUl1STZMdjhtNWhuRFdwREJnQ200K0pRckdHaWdtWjhFT1ozWDg5a2FmSTZSQ3NTZ05adWtDUi9oSUllZG1ldVh3WVMxR2lDWEpqZTN2a2gyTXFJRjc3ZWg0Rkg5dlFLM0VkY1g2VGNDMnc1dGVuR1E3WWtIWGZxdEplVk1JbHlwK0hLMENVNFJGalNvYkk1K0NpSDVJZklGMTlTVUR0L0hQSm40VjR4UjVpZ2hFb3ZGQkZ2NVNkYTM1SUJCcU9xRmorcjd5cVFxWUhKK3NiUm9Ba1FwSkpDcE1TcHk2eHVURXp0NGViRk9iT055bmlNdURLYmtQZmxXaTBvVlVnM1MycEl5aXVZUk5MenE5UGQ2Y1g4Q2N4TzRMZTNEb3RXQURCdEVYemlkY0hKZDNPRG5KWkRBYzZ2cW5MWXpyUEp2LzMrbkF1SlM0TExNNnNUYU13cG81U0tXTGhGYjFiSzNOOTBUR1VEZmxKS3hGSThSZVprNlUrUFJxRm1PVVZjWGNGK1liU1Vwb0JxaUNsd0R3VzRuaURLNjV1LytXMTBaVkJvR0RaVGVrcnZvWFVsYk1RQzM5OXdtaisxQ0ZSNHFiWE5hNEcwWS9nOTc0WVpZZXVRN2ZsZ3E3Y3JmcVhPajR3RGo3UEF2UWdNakJiRGxhOEsyU3VEUk1xR05LM1dwT2ExV3NBWGlhYWtKV0NWbWM5OXZrM1ROMGxFUWJTVUtNd3ovQmV1QXFLU3Y0STBpdSs1VkpsWDJkRENOVFl6UVFNTU1tV0VtOHJ4bEQ4R2hVaW9GT2plZTBERzI0YS9ocnFKeUVrNkdXSWMyc0JUTGZ5b2xiMkFwUmd2dzc3Y3hoQ2FQSHBLeElvek9lOW0yQUxXZDluRW5xbXpKVENFeUUwTHMveGN6cVhBenArRER3WWZ2V1d1ZUwxL3EzTHU1NWlJWGs2dGgycUhUUm9rQWtPeGZRRXVyL3VRME1tSGMyK0IyeXBVK1dBVlc1eCsxQURyR0U4Uko5c3R0YUdNSHVZVFV3aSt0TmxVVzRUTzZWSzFGc2xnM0Y2ZzdCcEF5NjRVM0tIS2xkYUtMUVphWlNUQTRvc3pyZWRiMHNlS0ZwUGpnVXhJTGo4RmwydDJLZEl1SW5BK25vYmdKYXB3dFRINlF6cUUwMVdvRkpIQ3ZVUVdkT0ZyK1M5MVZOOTRzNHYxc3NtUjBlb29NQkNZWGNSNVBIQWZmbXdFdHNYcUZwQXpuRFVrT1liQW9kYzVYUk5CK0tOdGxZbTl4SW5YdlBpWTlMVHZGNmxUbmJpZEUwN1Uwa2tJcTZBRy9FMmJjKzJtZkliZXE5N0VFSTAvWklMU1lCMnhwSlQwcUxUeGlBd213cTJqbW8xQW9pTnVqRzNCd2FIWjl3SGNVVzN0ZFF5ZkRXNzdFcUU4WVFpMUUrTnR3WHUrNTNNOXArSU4yMy9SVDJJVGZyVG84UFd5Um1uNGtzVTZXWEdoVWwwY296ZlVJSUhSNUtPTmI0NzBxRENOb1k3K2tvcE1kSm1RSTRtSGRTc2xlN0lISkxVVlNRN1JNa2JyVlAzcHJmQ3dhWEV5UUgycWtJVE4xWllUbU4yM1ZGZmNOaUJtRlh3eEpXMWFDTlQ2Wjc2ajFTTmM2U3RrV2dyeWtIMTgrSDN3TFdqUmN1cllFR25SbE5jSzZMV1gvWHJSeVNpYTQxV2JRT1NMKzEyUndCTll4dXRQSkRBalQrOVFOanVhVmRxc2I4NTZ3b2VuMXdlS1JkcDkyR1Q2VkJqK3JZM1VEanpVVldKelhXcFQxaWE5MmFPMDUwQ200OUtPYWlTUWhGUVVjVkdsTWVzQncyYWJyQVM5Z24zWDBzNG5BVzNVTklqT080Y2NKR09lby90WEZvaXZSUGdNbktjdmdyOWFqeFNNaEplNkhWdGpKNXVEWmpmSGw5U2hCQXdaUGk2aU5sbW5NSVlFbEJLdDNmQVZpZnlnYkgybFd0Vy9tYUt2a0hQbUtZMTVFUE5ZejdIUTVRSnh1WnNUT0xnSndiMm05MDI0ZVFkbGJJajBWQWR6dDJ3MzcyUmZNNmgyL0t4ZFAzWlRFNzNXMFh5NWRyRy9YTzhQNWRrQVVhbHhLTk5zbGxmclBiZERCMmozVE9tWW8rYk1NVU5wYnhqMVdFWUY1MXBEUWZQVG5LVXkxU3JrMHJmOHZDV1JsYzdtYncwUkJvV3JJa2Qya0txK0hBVlBSUlhlNllNNTArNUNGbmhZTVFiQ3pxK3pST2NpK050VVVvMXozV0tRRHJGQU9weC82NU5IQ2loRGova2ZCM1hYOGdlK3RHa2Z2VnZVTGtpblBneWRJc0szVW9IL0hQL05HTzl1azkyNzFjVjhEMmN4VnBKZFZsdlZPRDR2a1FHRnpmVmxWQlY3WDRzZCtmUGEvQU8xbG9LTXRXaE9jRXFGNWtaSjlsUHltT1k5WTJmRGV4bHJDZWxWZXVJTFhsVFF2N0pkSGw0WnlaYWV1QmJVUkYvb2ZJMktBNkFiZEFFcEdoVGtlbFQvQmlNUmhrdnkra0RWcXFGWUE4Z00vblJQdmlnSHJjSVR0V3Y1VkRRbllzMFVjT1dRVHBhMUZ2VHlHNWl6aUM5NDlsZm9wU2k4WkpUUlhsQUJxMVZBSWhBenRLNlpnV2dzcGtmeUZwL1dwbGptd210SVU1S2lsOWJWWlZkWTBNK1QyTm5SUFJQZ004bm1hdG1GR0JxdGE4UUNTbmt5V0xDL2hRV1hCd3pIdDVuc1JROEdKNkdBNW9QcW84SGlmSE9XVnJ3VWJHSkRDTFBqRHlicU5ob05vUHpjN3hMa1ZqbENLVWE3QzROcFVoNUFTTDBYcFRSaWRKbzV6eWNUYWE4NWRsYTcvTlVwaDhOcVpnNytvMkdqNEhUVVk5cHl1ajU3QlYxMHNBalRreEZ2Z2JZam1VOXpnU2RjRDBPSS9BMWdsZXk1NndJT3NoS2YxcFlDYkdTRWQrUU11Zm1ESnE3NENtb1ZOZzlhcTV2eGNKNVlKQksyTlF4dU9qc0RRRHByYzZFVTFuaVdJNDBPck93cTViTllIenB0NG5UVVRRNXdPc2NqcGJmVzN2ZHRwYk9TeXlaT2FTWXFYR1BBVEdUU1hRSjFnNG1GSHhDOVZrTkdxUy9paU01Yy9WR1U5RUtxWEdTQmxJbnIzSUdKLytYR2lUMmdqU0RCTnNXdnBPMkdjdXFyVzZJRWRoanpyM2wvVnVET0Z3UmFGUy94VDRIUE1adXBEdFdHM05wYmJwQ1NiZjZvUkhkQXpTNnl6TGlkbWkxa1k0WHAvRjZFMklUc3JreUhibnQrV1N0dmtWb3FYZnR1S3lnN3V6SVNHd253VGtlNDQrNVNsM1RraWwzcmVGaW16Z1plTEY3UWR6VWdoZXoyVERWWTJmYVBERG45TklZZHE4VWFIN0VnMnVvMURyL1dycThkaEJscW5nL2NzYk5JQU93TnJpYnhRWi9FZzFUYWs2Mm9leEVOdDNpMnRjL3JKVG9OQkFsTkR5bWpJakVZK1ptT0JIT0x2VGRCb29tQnpYSWNOS1lNYTBQV0JKODlrMEJ1VGdKdUtaclZqY0s3Z25Ca3pMOUxTZDRNNWRlbWVWNkJtV0tEeGxmeDRQVC9CT2g0clIrbUtKZ09HRVZGVW1aZno5QTdTSTc0SFQxV2NyYnlIUWVhT01iaWFBT1hneUo0UXJKbTJ4MjJ0T3dHcmUzaVNPNytMcnJqL0JkVmVuYTlvNXdvRExueSs3WjFZZ0dURldvYWFTWWJTUWJNRzg3YVpnSnNqRmdEeW9QTnBObldUWVZ3eXByYjlIMmRJRTA5bFozMmpKQnNVUnFDWEJ4SmhXNWtBZGlHTGgxenBhSGs1SUpVSWVEcll2ZlUrMXBtZUxXUlFtc2dwZUNNUysyMHBick10d3ptalVFZG1WcXRFZkpDWjdqajVQOFlGU3VabEw0Q2lxRWtkZlI3ZHRTZUU1YnpyOTM2VnUwUStCbnV4T1QvNVVXRXZWdjdud2NWRURwR2l3R09jVnZ1MW53ekluZlFkUmsyOVI0S1JyNk9ucFo5cGI5dFh6MkpPVE1rMEd5YkFIU1JKYkg1eXlobitYTG53ZGZLbENyREpYdDhVMkhnNW42M0gxZnIvTXJ1UUd2MFZNVkJ5cTNBNUg0RzZ4SlQrOWpQcWlvNVkxZkU1ZkdBSXRDWEoyeURPOXhwby9tT3lrcm83NDZGQ0tFbktIVklhQzBwT2RTaHViSFZQVVFmZFQzUCtkeHBWR3IxT2cwbUxrY09jeTJFaFFGUGNyeEY3QWFSaVpBYWp3a0tQVEtHS2UxTFhBWFpkalpiNjhCVnMwVFQ0Tlp0bzNuZkQ1MWZVWW10OWVSUmdyemhmQmg0SFM2OGZ6enlVcDVhOTNvS0t0ZUdzWm9RakxlWG1rZWxNQ3hVdDJMR0xHcklVZGpidTlVWU9ndnE1Z3FNblo1N0hzNE05Vmo1YjJDTWl1eDB0MFE4ejZvanVhN3pTbmVKM0FxMys5OURmTU4xQS9KTUVsNWdMWnZRQ1VRRm5ZSmhYLzBWWHlZbFcxYmJ3ODNiWmNkYXdQVXdpK1hQN1ZyamVOc2lKQmVKbWlGVnEwYlozbzUrSVcrbDVRdWo4elZWYW9FWjA3RUdMUTcxcllDWHZpakZVMlZRZXZvR0ZKdjJZbnJrN0NGQWZ1N2Q1VS9kc1RXdDJZOVZuTm45OTVyN1ZuS0wvRTB3S0ZWRTdRY3NDeEdXb1ByUjZsZko1YW1CcS95Z3lLSm5uOEQ3RzdtS3BhWWtFNG5BTnorcjBPYWU5RXA4OFRZTmpXcGErU0czZzJXcmtndzFrMkZnOTVCaEYyN3BXNkkwOHVxT0J2WjZENEh2R3MrOENhR3JYM1FLV3FzUG9ucjNobHdRT3h2bWhXN2FvdWNveUdqOVE4M28reXFKTFBxbVVhTnNCRmJYSEhnQUNhRlcvdkFiWXNJajN3QWN6VGxpOUcxUER5azRpQWlWNUNLalJmM2pPZ2NkYTRYaXpwS2pQTWJ2d0xNTHRvSzhXY3VwZFhnc0pPV3YxRmlVc1RvcCtUOTN1emI1b0UwZ2VGN1hPZ21LcEphQ05yYlRQMTdPc0d6VUFZWlZTSVhzbEJnc1h3K0dkR3RkZE1xKzFpeHBaNVVGbmlHMkpXNyttUUpYMzBQaFlmZTJmdkRDOG1KVnI5c1lHYlkraVI4UzU3L1hZOEhFcDdEWVdBblBkd1JwcklRYXFoT0VneUFzSDJJUFArb1RWTzhIRHQwQ0ZjTGl5RXhpc2xGRU85OUw3dWZnVVJ4amVFZXFoWW1vWHB1bDNFYXJkOEpqbEZSWDJFb3pLVnN1S2FzNWVSbzVqL0I5OCtrQ202K1JMdnl0OU9aaUppMEJPaGxCeWdpV2FuZVprdWs2QUtKWGZHdzFZYTYrekxTOUFMaVBML2s0dTlVbk5uU3hGL1pUNGJSQ2toUjg5cCsyRkRrQmVtZ0JWRDdLbHZMVmNKWHFxZEkvMEdWN25ZRnB3VDFYV0FQZHFrYzcyeE1HRmx3dTFTMS9SQXhKdkN3TEhPZnNQaU5EbU5mVitKMW9kQlFQOUNsMHlWV1ltOVVKMmd4cVlkNHozZnpaWEYvOEszN1p4VlovdzdmRzVxYVpqTjVEYjZVUkZZOEUyekxoRVN4TEZuY0F0NGhxMFBGZ0ZyT0JPUEFkbE5GWlRIeDU1dWNNNkhVUHh4YmRQZ1dhYzRHYnBwd3BQcWVJZjhINlFxSnJOYytiWm9TY2VCcVIxUTArVkFjemw5TDlLZkVLTnd1U2REdFhYV2crSGpTQkxOOSt4bDBJU2lxRjNwS3puTk9RRU9jWXJEd3p6SzdUQlJjTTJ2VlVzS0RRMXRnRGp4ZTdaZVhMci9IZHZ5RFBHS3FzSEFEWnIwOHhUdXF5ZHNGTjd3ZGNXMGc3UEQvT1RnenljeWZzOWN0YWFlNzhON2ZBSVNWM0lvNHNGdDk0dmdnWVloaUN6UENqcXVCbVA4ZjQ1eVhieUUxdnc1L3RBWTVGWFdZV2lCY1RmVEpZbk52QWtPYXhna2o0TjFSVXBHcTQrSXVHb0NVRCt3NVhHYjJ5aHlnUHg5djcrZ1NsRmdNKzlUTGFGbEFDUG5PRGtHS3IvT0xBMDhpcC9nQ255bE5DSVYrL3pSUktOTjlpV2kxZk5QY1dQWnlQVmVLQTh4ZTRUWDRGRGVpeDBsV2JkcWV2cStlQVN4NGNWS05MTVhLSEFqYWZwSUlqM1FINWFKQ0VPaUQxNGg1L0VoN2g1U0hUQ3hLNFA1anIyemI5d3FDZVl6T0xqTm81ajlnVGVCV3NPekNXMjN0Uk9BZzc2Y2lCV3BuODZKcXlwSnQzTXhKYUJXdXJvdVgzSFlOQVN5ZHFvdEQrWXlZQWtkbS80b3puMlpxZkVZcm5rbVZ3UFB2b0E2VFpGK2k2d2FWdlgzSU15akZETzF0TzA0U3hBM09ZUU9UQkRpTHhEQnRkY09Ic2dNYXFKR2tYN2NNWGJQdkMyRmJJL2ZYcjZ6Sjhrai9OZmdsQVIzcC9PcGRQWU9UVXJqK1RLSldrWEZlWGJQSlBRWlVFbWg3RXlPYWU2cXdOeVU2c3pjdzRFcFo0dnI5OEt6aSt6WE9nUjN1bzBWU3hqWjR2N0c3ZWtDTGtEZ3VyM0JRUmI2akovUGJyY3l3MUczNFpURlZMRDJjQndYTUozSlBKZmdtK0g2M1gvVmlMSVM4THFIUmRMNmJmSDJoK1dsR3VTVWlsVzhsYldPTERnV0FoS1Y0dGJDNThxZEpVbmd3enJUT3owVU1sT0xMOWxVSUxjTWc3dy9HWHFERmhEc1JwVnNtRFZucExZMXQ0MU9KQmhiMkMyMnFpcEZXQWg3by90STVoeXRadXpUcnhwdVdoUWJhTXFBcFFOZkdrZ0ZLbXUxSldWSkVCY3YySFlRM2toUkdyVitKK0t1SVBBYk9mKzJ3aG5kcDZXMXJzWXZYVWIvZ3FOdi9CVkZLTFlnTHlhdGM5SStCUWlSTlVuWkFWbU5FcmlFb0F6bHMyY2lkbFFtK2k2Y2wrMHpxcXdwZXdXN0ZxYVhQbGZYWG1DRFQva1U3T285cG1wTG9UL2dBN1hmU216WmNrNUZYTWRhNFVRNlRkaEZNdXhLYUFvK1BEVXpEYlJ3NmtMdHZRWXNuY3YvQkYvdk5Xb0lvamFZNVBOUUNTb1pRcUU2UFk1Zy9SSlo5cVJiQnBvWkpJTEJWSmN4dEpKeGRYVzFtdFMwZUNuSlVuL1JmZzZ3MFRWUWpxbjVoQWlNeW1wcTR2SkhrNUxVOStIUm80TnMrc1R2RC9vYVEwN2NWV3VwYUxsMDQ0VnZuaXFSOFg3dUNDaURoQkU4YVRpVlFqSk5uWEtjbmhRc3ZMZjhBV2p5WktyaXBsajhBRERiQWRlemsyV0tFVDVaL0tPZnpId2paT3JvLzhMTUd0ZisxMFp1QUxEK2w0YVE1N1VPcFRBTjFzb0NXUE9BWDI5Kzd1MHVwdFhvWWo0VVI5UjRuYTFpNC9ZbENZeTQyWkRxSlB6a3Z6OVptMFVRc3VvVVppZzB4aUhqVW1WRng3WCtoT25NVUxuYyswSk0ybmJNQmNVbm9uSTM1OUVGV2tjTGdhQXlJRXVTUnN3eWY5YTVMMFMyYWNPalQzSG9wK2hHL0d5aGdZdzlqVWdSN0JQcENDZU5sS05ZV1FHL3JIL01Ic1d1UFRnaURTZ3orallpQi9yQ1BlNjQzVFRlSlN0Y2Q3VyszeG1vVHFscitMMHlFdFVrZk9jUVd0L3hyZWJLWWg0ZXIxZEJjNzhHN2NIRUYvU1dGRzdmTHJBbHhDb0lGekFpLzVuT0lNZlBIeUxMUHF1RWZWVzZERndqVmJ0ano1R1FFYzU4WGZZRVdVNnhpTkFNSHQ2N0FodWVuUnhhV2tGejE1VzhaQmFVTU8zM1ZGSE1JZHJDb0Vjd2ZhZlU0bTh3VVlQRHFaVzlFUmNsZzBRTzVnK0t4dWd4UUxBeGV4V2FSWFp5UXVkRzUvWngyV25TU3lHRjdNYk5uam5rK2lkOWxDZzlDMXZMUHdHNmhZQSt4OXQ1ZzZ0MkIwbmpYaC8xMHZkWDM0Z1Exdlk0NXlCWHRnV084dFFNZ0lEVW1oN2VPRnBBSUx6RjBPUUpZR2ZtYnA4aUNtcjhqK1p4cDlUYzNWNC9xYzRuNk1mZHVuMnVmaEhkNGNjMTcycFIwY3hPRDh0TzE4U1RwdTJqTk5zUFpvYkdrNEc3NkpYMDhXL3lVUC9LNVV1N3VUUUozeXdCK2lxeCsxQ25rWExZZmtNdHN1a0xvY3BXS0MzclkzUTUzNkRDeWkxM0tiWTAwTjZmTlpWekhwZ0VFbWVhQjJiQ3BzR3JuWHdqM1pVZHFUSUJLQkNHVUx4YzkrbmdhelJiWCthVjJCcDZwdDJ0ZHpTV0FnbFJhb0lmZ0t4cjd4UjBRUldsaHZqVk9GRE1YNUxaaE95cVNVTHVYSUhWT0lER3FOcWc4d05CWlVjRWIvSE9xR0I5YW1QNmNJeEFJRGNEZUtqeVhvOTVWbWFDWjdEOHZSaTdRRGJWYmdVbkdIS2RTNE50ME9NOEx4bUs0RWVjS1E5UklraURqUXk1MnhWL2dqNWhWZkJpYWhQa1BRRUsvRGN0S2NBVUdKOVhPMVdSNFdkQjErU1Bya2VtVWEzRUZ0TG5sQUZKTjBYMFhkTUJTSHhUQXdKMXlnYnB2TTRnQnFrVmhYdE9nbEFESDU2eU1tMGZiWmNEZzJyd2l3WmpnNjZ5S2ZDaDl4cWZaeHJSWUxOWnRsUnlxS2tQM2hCRVUwT2F4bmtrck1aVVVHYlJsQTliNXZJcnE5d25GYUt2Vy9XVXFzcUxxWHExZExTN0dKOG8yRkMrMExBZWtCaEtEY1EveW1ka3NvREpKTTB4VEFQVWlIaFhXdUpZYlg3eVFmN2NoVHZhOFc1UU54WFc5eGdreFBrTHFjUS9CamlaUWozRE5DdEo2V1hiN1JkaXI2bWN3SkMvYmFCRjhVZURVU1ZpY3VxbUxUY2hTa1hqMTc1ak02T0wreFNma2paZE9YMGVMSUk0YzdVNGljUUtGSjR4NnUyb3VybmFXL3Zac3F5SFNVWDdwdUx4dVFyd3RHY1lYTUMwemd2NGNQc0pVMk9neDhGRDFxUzY5R1hXcEZHV3FSQ2NCUUsrOWtoL1RKd08xaXZDbG5veFFvUFhzTXlFTDF1czAyTWNhbit3SXFCL2ZLSkZkbVhrWWtKM1A0bnovTitFMkorK1JsejcwNXpZcVdDNXRJWHdtNko1aFVaSUVadi9ZS01uemFSdzlRM29zSDVMa1FpcE53Mkp6WC9kK09EcURUaUE4ZG1WdzdiZmg0ZnVsVWFFSnJyWWFFdTFOTE9nL2sxWXJwb3htUG5VUnRoQ1lOZXJBdzZldnBZM3RoM2ZMQ3puaHUwb1ZEWTZFbTJhb1NsMHVFT2tXSXVoOG83Uk9kellkTFdSaVRtQ3FIZzVveDhLWjJJMWh2ak5pY0kzRCtXaFVPMW9wK2VZeE91OUpOZnIyQnRxOHJEd1pWcFV0amhQcDQrS1ppMkJNV2dlOG5uN0xLRkRvcFZERWlBUUdOUU96YnBVMFhxNHRXaWswSFVhNXRTaXpMQjlqb043ZWZudmdUMU9nTGg2SElieW1rRDVVYUhlMU5MTDk1d1A4NEVlME83R1RFVklFSVY1MldBcktKaUlQNEtKZHgwTE1YZ1Z0UU9ibldNV3dUeGlPYzl4N1RHT1ZTOHRFOGwrbW5OMm9ZVzYzZ1pBdFJRbDd2UjBHUUNEY296Ym8ydVFXU09Gd2R5NFlib2thcDhRMENua2l2Tjkvbjd4emk2OGxFTmVyLzYxVnRSdTU3ZnlOTk5PbWJaTmN3SDFCRlZPWFhKSWxzbzNWcEZhanVvNnJrWnhzSUpDL3p4YnRTWVZrVWdsakQwZ2R3eVRxTlFxUlhFcm5LVWVUTEs0YUNCcGtIK3UySXVEMlRxNmt0S1JRMW1JTDJhNno2ODdIRG9YbXBTaUNEN3lYdDlIMjJEZ3NqWVZvSW5XUEF6R1hZZjlrZTFKVGpSY3BJelRHaTFCT0JTSkNyRS9xVWNEYk5aYUJWSU1HNVNIdWtLYWRYdEMvMk5NeDA3RDZDMzA1d2Q2Zk9vazlCUjhPYmk4OEF0aW4vSVBmMHY4djZkdWI3VklwTnZlLzJJdnR6RFhrS1RMK2YraFZ6NGxXZDRYa2trUFc2RlpNNVhBRE1SeDY2V1pXN1UySk1TclljWmJPeWNmWjJKb0ZnYnhUTGR6YlJtKzd0Vk9RbDIvZUdhemkvNnRDaXNxZDFsUHZBVWhacG8yaHFVcHJtQmhYMWNleUFRZmdpZjZOQTB2akpUbHVyUisrT1RnMTF4aC9odzkvK0NWYThuQnhvL2JoSzMyQXhOdTR6YUtwWHFxTW40NXlyZDJwTDh3UEdBeVl1dEhYWWN3cmJ2c1QrcUJCc0FMVHpVQW5YNHhXajBsMmFUYVFWRnBQR3FYY1dBZUNkVlplL29tT1Y4cnFrRnBCSVRkbTZGaXoveU9DcVBLRDhVOHVGQ21zeWs3VjZORUJ0R3FoQXcycC9nQ1dyWXh5bWFBOUxIaHZxVi9GNWR3RXpwRUQ2Ly9aNndscGl1SU8vcUZRY3ZLck0yYVFEZHdxaDBCbVVGbU9jYnh6ZmhwNVJMdXZlTk1odzh3cjlxb2Y3Wm4vK0Nid21qbkNwWXFPZ3RFSkt3d3pQNDA5ZlJScjREcXlCRlBuWGdSbEh4UDdFYVFpb1RScGVTQWxzQXNXOWRpM3Rld1J0SC9oZ1VuL0tkcnNxMUl2M2pyaWo4OW9yazFqYW5UNmJ0QmU1cCtMR3pUU3hOcklrdFNzajhZT3Y0djQvem5uWGQ2QTZhRGwzZVp4RVN5TENFNmsxVVNSZmdlUkdaWUNtcHJkSUhoeGNMaEUrZXZ1MjNQRXJJYVdpZEp2YVNUNk1SQVJDWC9BdVVlOWxhQTJLeGNCL0FKcXpTN0NCcEZHcnpRN25ZSm0xK1VxVnRDKzc5ZTBFM0VsbGVhS1lUbWdOSU8wa3J3TXhtelZPUUJCbFY0d3kwME9rTmV4Snl2Y3k2ZmplZElPS0xIQ05TZ2FUTFNzTkI3bW1IcVF0OUhJaTREOWZackpyTjhpN01BMjdnSndTeUFLMmk2dUFJODNVOWNMNU15SmljejE5WkREdGpNVFRybDhMK0M2SWNJaWZQNGZXNmN5eC9qY0ZXUzlYVUhXa0J1U1lSZmErRzRMU2JwbHhwRjhjL1ZBbExhK2VhQWRKbExBanNMLzJMTXQ3bU9HZnhzeTdYRGg2UFVKdkpiMGozeGhjN3BtT2ZHdVZwMkdQWVZybzRybVh3Q3ZzTG5La0FudG85NUtpRHc2SU0zZFhsaU9CSUY0NUhldE03a3J0c21Cdlc0VitYQVhuYTRrd2IveTJ4azNqTEl6UllSdGRaV3l4MFk2b2x1RjgwU2V1WFozV3N4TXVNQ0g0SDB6NXVrZWZyQ0NQajc1ZEJWOHNlOXVrN09QNWVSZnI2SXJkVkYyWkFMakZzZE9IRThJV0UzTFk0VE93a3VKblhCeWZxSEdtTlFGOHdwZFg4SFdkK2FpNXAwTlNKSlAyVnF5MTZZTjdGV0RpdjJhMytOZGFRRm9nQndIeXRJdFVRbUpWc3ZNR3Mwc2lKdlNoczdwTVRaSmtTVE1KdWRyK3JXdngwd28weUwwaW9MYWFTOW9Pdm1BR0lBTTEwR2F6QnhUUWpTRkJKendCVkU1K24xSDI4M2FUdzAybDNqKzQrS00xNGRIMU5YZFJPYlRvN1hzc3NFcS9LMDJ2OGQ1NFRKT0NQdDRPRVV5Y3d5Z0haMzdkd1YzclpaZzFrQVliTmZJbkFpSWo3NEY5c0MvcU1Vd2IvRngvbjhaUmFzMzl3a29ZK0tMUmI0SC8wM2ExWndQbGJ1aUltZ3AzVW12SlJ0eEc1d1BZWmlPR2dETW5rbnlqY0VSV3ZrTnJKam9lV0VTSlRZbXEvUkxlQ3FIUWowT0Z2bUR5clpSTFVpVkFsUGZLU1oza1l4ZFJGTW5Hc2FjNjZHaEh3VVNENXpoQnhxNEJvNmNHSDFDeERlTnlJdk0wRDlGalBXMnRrWld3NlhTK2ZKZUdRM0tLS3VPTy91RUJROXlXcDNrbjVMMUdubitHK1JwVzViTGdiRTFyN3cvM0JhemVSWEZnSDZEV0J5OTEwNnAwakZlVFFNalJTZWdGdnY5Y0hiTEp4VGNROW9YWFpFOU8rcUxHNnd2VkpiMXAweURXblJnaTR6Z2ZPSm5IS3pMVDJZb1hMc2dQMDlwMXEyU2lFdVdJcVplcjVncHgzeGdxNEtPVWRlYjBuSVh4NGZzQVd0ZWwvSVovbDRRZVM2SHE3dG1vUkFuUG9nTk1FeHdwSTVSb0tvbzdVMi8xc2RyRFJ3dGNDZ2RHYlZhUWJCSTk1MnB5UnhUMkRnRU44VEJ1VUdNeU9peVFaR1V3eFd5SGh5dHUzY3B2SFVrREQ4b3NKREl5Vm95SEYwQkYxblVWZnd1ZTJDS0FhemdxUmxUbDRMdFh4dmhKbWZQNkFObjE0OTNIc1RtemZ5K1N0K0NlKzQxTUtBTVA3L0gwR2RzVVhidDNXYXRPdGRXNUZKMnZoUWpjTlR0RThpeGlLa1p6Z2MvWnc3STczbUhKTFNKTUhlMzR5Ukpxb1EyaDBZV3RMYkhDcjNTVkIraks3anpxV011eVhLWDllcDE1bi9WbWpaQkQzWTZaWTYyOE5iZmlYSUsra1ZnSnRFc2hJSGJnc3hOU0VteTFDU3h0OXk4dGdsMlJvYmNIaldxYXY4aTYxU0pCZm9XSHpwVHhHZkNVKzMyWEJSNEtZNWhSajFRajBvM3lyZGNkek5OOHNPSHk1OWgvbnl0K0Zjb2pxQWJzK1FQRDRXQ1MzWmJGYTIvVDFKZGs3YnFmVzZpTUtUTFFyeThSQzlId3pvZzRNWXVmVWxXYmxzakw3Q3kweW5hdU04Y3o3MzE3VkhmaGtYMlE0N3U2TUZtNGFMcnVuSHZuRTlNRXN5dE02OWxvY1dCMHppT3BReGhmWWs4Uy9HL1I1ZHlPeE9hR0F5cHkyeGJYK2Q2VHFDM0UvcHUzbjR0Z0lRNTdrSkd0Lyt4MVUzQ2F4b0dOU2hMZGZvQjR3V2xOR2YrRWlGdmgwanRJZEhGRWVzdmwxelV3VkplbUJ4N2pIdVhNSGc1RTE5TGFCY29JQXQwMzVoUGxld1dqb1p0Q2RKUzNzYlkvZ2doN0Uxb3diNG9KcVNtMS80clJvcnNZVStlVER2dWZSTFFjMUlnc1RVWnljOWh2d1dYRVRSNHZTWitIQTZBTElidG9NZjQ3djFVODNBeUhIOUg0TDIzajlNeURWN3dETE9hSmw0MFN3WTg5L3RkZlh1UnNmSWpYUldaT0kzTVN1VXl3R0JBdVhUd3lvV2p6blNQOWs5NTY4cTBWTVBPTjRZQ2pWVTg3eERIajl2MWo1Q0I0UHloTnhwdStXanFMNEVsbW1kTHRZSU9jMlgrM09kVkxzSnIyREYyakp3cmt6RzZ0MWJPSXlwZXZ4azUwaUd0Zjk5Y1k5enBMTjFKci9kRUkyODRUamwyczNCM1JEZHhhM1lvTGRSV2FnSmUrVHArbmM1dTAxU2x5QXV6Q29RN3NWcG1HRkN5MWRlMkI1c05LRUZ6Y1Nsbmw0VGlncWJmcms4THdMQzQ0VzdVTlA5elk4dFRKaEtPaldoV2szanpieEJZTG1WbEtRS0dsbkVRL3NhT3haaXFicVozTEozTmRrZWdMclRvaXNGdTQxYlVjdDJrak1uK1JWL2FqaC9UL2dqNEpzUlFWMlJNREQvRTVDM2xoTDY5dEdMR1l3Rlh1M0VJM0lYdm5FT0RydmU0L2RYZHBPM2VlbzgvMkxoTFRRSEFqTDlET01CYm1OaTZwQVpuSmI2L3VOQllnSDhHeTJVY0VTQVU2ZG1Xamd3TWpkbWRyeWRUN2Y2bTBBeG5WV3ZjbVM4eWhrTEdJSHZQSDlybkhqK1pXbVY4aVM1WGlLYnBWT2V1YjFNd1ZZVk1VQzk0SnZlUXgrWk1UcDJhclZVektnZzhIZ2ViSVZlRTVQS1ZmN1Y4U1NQeHY1cXpqUGkxQXo0OTBjNkFYd1lwRjFnQ0MremhrU0VZSitJd0I1QWJWaTZ5L2k1bnN6OVZDRVBaVGdYZHVFbEdzVyt0Z0NVSTI0am42ZEF3UG1SWnhUNWZnVHZxdUVJWTRpRUZsQzBsbVBhMFlLYmF2RGw4dnJWUTNDelE0QWtEUUlSdnpXWm9uckJmYm1oZEVvc3hqN0t5L0lyektRUGoydVNGbXgzRENqZDdmUmFEaG1GOGJ2RVVpVGJaOHBjR2lQRUYzNXl6ai9CKzZNZ0NwNXZKYVF1cjg2SWgxWEYzYmZETTZLWEEyL0pQdDR3bytSRkF0L0w5KzJIWTRDTWhaREFTTll0dVc4cE1EclduL0RxS2paeWkyZjVpRkRsQ0VXZjN6STRocVJtY2RydDE4ekZtMEZjWVRUdjB3c1J0RlplRm5ZVUQ1MnhkaFpoanZiVmJvc05FU2lWY0h4ek1DSnl0d1NFdTBZdzF0M202eTRtdTNpOGt5OStWbTFqZFVERjQrb3VncGlESXNST1Jsd1plUHdXcEtRVGEwYWJXM2s3dXRVdFk4Q3QwQWFWMnZHd1o0ZkxWV3p4RDk3ZVR2dHU3blVMVUNDc2dkSU45UktzRmJnQTlUY0c3bE9tSzVrNW1TWHNaVExWYVI1WnhpTmpLMGYwT0xETXI5SVYzYzlKUVV2K0IwaFZsYTBkSFI1Ykxab3NMNnpUOTcvQVpLTjhYb292eUFtY2ZQb2k5bG1hTXh6eXE1NTJaeWFmbWIvRkEvTmZVSnVQbXZaUWhYSFRtVVBSM2RaTWd4dVc0NFBOMFZ4SkJheFF0bWJiUkZGRDVsU09oNnlLSk5rOTYvSTNGQnlJYk1rZEI4Qit6VU02RWZoaVF0eEtVdS9qTStIYW9GMHRIS0RSbFVZbnFhR2pqc01IcFBrekpxeEUyYXFtYVdiMTVoZHZNSERkZzMzaEtDcUhZSTNIWkZ2NFNVcy9DZkFyV21OblByQ3pMT2lHZEF2a0UyZk1iZUx0dGQyTlluSXU4K3BPWnhpdHRPL25ycTdnUzFWV2FMWDZzeEszSFhwb285b01YN1V2dDNJRHNnQXRpbys5eEpNVDA0WFg2SlJFVjV5YnpIZS8zcXlxMVM5bWtyZGpodVNaNmxqUHBoUW5DYmszZkFLa0grMkVYd0pYZ1E5ME1qWXdHOGl0aVg3dGVka2JjT2ZGa2E5eUxHU0dzbTIzM2p4aGRHSlE2Q29aaDlBRkQrZzh6QTE1eXc5YkpCdFUvUjRZZ3hlc09YZG1tM0NYR2Z5cjRhYlV4NnJWQWlHU0hJKzBEa1dMODBIcVhsRXNnTVd4RlNaQ3Frdm82R0l5UDQ4NE4vSDJ2OFhFVEoxcHdXZzVKNmtjdGdPTlZYVUdtR1N6SVhGeDkyZW1wN29ncDVRNm1URitaTmFUSndrbml2ZTJSeUVicFZVcTB6cHdNaGF1dTFqbVp1S3dSclE0b0gyOFIxa0hZYzloU1VyMzJNU3NUNGExK21Ca3gzcXBmendjSWM0b3FEektOd2ZaZzd3L2hpQlZ0MzZKci9qaTdndHZ2YVo4SG1DZ3F6andpRzNqNFY1d1FGT1lJVDh6RjkvWmY5VlNzWWl5MDZPQ05nN0dRSDJqZE9hUXpmaFl4b3d2MExNNXVxK015dnBRN09vbFJkd29ESlZ2ZUN5Qm1WalpMa0pNUm9ZYzAxRVNSbTd5VTMwZmNBdUtaQ2l0WjlTOHdvK1Nveis3Q1AvZkV3VzRDdUU3UFZ6RGE0bmwzMDBkRW1zY0t5Nm8wR0xlRytYOExLSkRva0hmMW9OUUNpQVBsdzg4Y3E5WDg3RnJnV3JIYVNGdmxnV2RTWDJyN0tKVTFGNXZ4a25uK1dDK0ZVZ3loUkYyWnp3OVdzMUVNVWU5TkVmUmE4Wm5JZFUxYjl6WWFqU3Y4eWpzWktCTEdEbUJYMkd0Qm5WY1BCUzhiU2tzUzZDWWFTWGRGQmdWOWxxZ28wMHUyQmtIbmFIR3ZKYjhsVzFtRko5NGtRamVLbXdlcVJTdWVIOXp0WjAzMnZkNHBtVURDbFZBcms2aURtaWpyUitacTlITVBHWDdWelRhanVhSW5QUkVNc2ZvREhJb0xOekVCSUMxMjJWRkpPWllWUWlpSTlTN0dHNUJpNHF0NkJrMXcwaS9DOER3R2JlOEZrRWhjTHQ4c2lqU2sxU0hMbzh0d0l1TUZCdVJzV2ZKalA0STh2aitCeHBCRUFMekFDVm9FUkZUWllJRncyZ29qZ2pyOVlTOGlsMk5vNG4rTHJsYXkyUDViYU4zc3EzQjNJU1UrOHhPMGFaVmpzSVJ2Vk5DTDNnN09ySG1xWXAzS3UrQ0hnTkVXNnlWL1ZRbVB0ZzJWenZRRUNYRFlNN3A1SnVnekhrMjZxSFBTQ3FyNm1vWG82L0owa0srUzFMdEVOYmlyaHA5TElaVXdiTWlaV25jbUp1VU1jTURhaEY5SjlDdHdsK09yRTVPU2hpOUovUTdmOUM0dXY3SS9qdURlNU9FTTVMTXVnclAycFdnYUI4bWdidVl4bU1SZGlLUUNESzI3cWNaQU1IczVnTkxkcjZQQU15dnBVdEhqWkdHaGl1cVBMQlJXWTg1MVZ6K01TUDJZdTVaV2J5WGdLVWJhQ1BueVJ6OER6MDRDN1c1cWdjazBjZVVnQWZJQkY1QkYwWTRCUnNiNjh0dVd4bXc0MS93b0NIUFNGTGM0cDdnNlVqRWVTR2NDVDBFVjJjZ09URzhWaTFCdlVOR0tNeHczVE1xVU5tTU11NG5OYnRxeE1IRHJwRGVpZ3NFeU1kNG5CMDFHSkRYc1EvTm1tcHd6SDZiQm1XY2ZZZ2h5U1RxUXlVUWxOWm9Bd2VzcVJCbnM5TTlDYUJONHlCc2lyRlZsVElMTW56UXQxeHVzbUFkRW1jSVVEclVWRFZtUGZabzQ4ODZ1YXBTQUQrLzNBVTZzOEUvSGNlTm82Mk0rcU5BcmEzZ3k4WDZwZjlRU2kxMFQxNXQ2UENSV2hIcEI1Qi94Z0ttOXNmTWd6d3FrTnFqRzl3Rjg4YU1VbUNQaFBTNWw3b0RGdkNPb3BnWjhoNEZvWmgxZTBOV3JRbU90Nk14dDl3K2doWWV4RkJLY2FlNEN0SFhHdW9zYk9kZm5sK0tzZE1FUGxacUpDSXYxYjdqQy9WNjBEUVVzdlBMYXRRUSt0ME1Hak1TdVVaY081dHZSbjJQdGFadG9TNjMvQWZBRitmSnRBdzBtdGp4WHJwV0x6b0RjRkVPS1NVRkRZOEFyR21EQ2NjU0Q4SHUrVDgwclBlMTd6RUFQaWN2TlhxL1N1ODZCZ2RVcHVzcGUrd1RqSnpLOWoydkNLNTFINjRVU1d6bFBCcThsYzlISGRaSVkwa2wwZDhURjFxaWVnZlZRL1B6SjdBQnJZbVgzMGhzRS9kcUQzQmdMMU9FVWEzQk13eFpJTWJSR1hKOEgrckVlS0pYT2J6cTdqTzdUcEFBOGo0UjNOdHJXSDZJT0drUnlMNklZd3F0Y0tSK3p2aFdUY2ltVkx5UGcxRjdUYTZwVmhTR1ZsU2I4bElXaFRnOWdzeVJQSVMybi83ZFhiTjdXV1hGQ1hWUlJMTWVKM1h0cTQ4V2U3NlVoSGNLbVpZRXhqSkZnRDNxRE1uK3NpOEt4UUxLY2tvcjhseUlEQUpSNDJPWDZmR3VsQ2wxK0Uya3lFM1lKd3owQnVwRzFJV1pGaGxIVE91VWZadzRFTWNCeEgrWE1TMUZIdjRib0VEU2pvV3IzTGNtRlJHd0FTMW02T1VUQlJucWVpYTcwM1AyTjRuSk9rb0lwbEcwWEhzSTNGTk5oZWh2c2REU21ZM3YrMENjTDlWRWtqV2kwOFY5ZUkzYTJxb0R0b2pPbkI3UHJ4aHR0UmIxRTVmMmIvSWhBZ29ybVlaOUdkKzNlNGtja3J5YnR0NTNzM2xRSjdKcVNWOUFnblZRMjlDRUdFaEh4VEppeEhOcFU0UmNvRDBxUW95cFRGNXlPM1dtSWFUcGlqbGxvNS9zbXM1OWdTTU5mZ0xDNitVWFdibEhDVjhPR3FJcUQweXRvN3ZONEhiMXRnMENIOUlZZDk5OGZxNUdxRXkyc0l6NWxZT2VjTlRJNmJSWDVBTHZLWHliUkRZNFBUN1ZET3p0K2tEMlIza1JCYjhEampPZzlISitlNDhXRXRJYktKb1EvZlA2Y2sxZmY1aGl0M0k1b0dMczZLNFhtTzJNeUVZYVNCMkdsdnRhWU9NdzBBdGlSOXEwcm5uZE85RWJKdnhCcnV2T3Z6OHpBNmZlc0hiVzk3UkJ1OU53TWh4WlpuUEhaR0NCalU0bjFwNWNxcW04SzJ0a1ZHWkFHU0phTHBtTGtINWtJV1hkZWZOL1U3WG44OWdKZnRRZnhPRUs3SllwamQvRkVuMnlCSnlqSi8rMnQxVE1ENmZVeFo2a3o3SUtkbVdmbE5OL3l4MEZHRTk2M2lOZTFPQVEwaTRHL20zUEttd1dQNnMyQkkvR1lJdTZVSTU5bjJyeS9wSHRORmNkd0o1VmNOb2V5SmlxemVxU1doaFZrVDdLTUFITjRTR2RaZzIyNlpuYWtsUWxHUE1NbkxoZGJMU1E3WTIzTTh6ZnhVNEZySDNJdnZhdTFaTFRhdEo1TmVWT3I2cU84ZHNNRzd0OHA4MzJiU0dqSUpEaEdKWjc4OUIzbjdzQWdrc2E2b1ZxTFQ3OHdTMjBCQm85TmRqT2hSRzF4SWpGa0svOXNRMU9NVkJFNkN5aDExeUlXMGdTY2pOOTcxZUhEU1RTRDlWN25nYzNjVTB1T3U0bEpQUDBtQXZoNi93N2tZSTZ3WHFLZjhITUs0Mjk5OS9OWjN2Y3BFV2FTeGhicHdYQlBKOExMWm53RWI3eGM5TXZKME5RK0lKcnhJMHhpNVdXN21KdUZLRC8zeHlBb2Q1Rmg2VUpIZ09NdFJzNmZhNzdXNkZ6Q29GQjdmNUZlZ2UzZHhQS2RUMEIvZ2lxS2cwMmU2QzBTTkRJdkpybnNOTmZVOUR5OWp1ZzBIOUFlSWx1ekdvY0R4cVRENlQxT2tPL3dyaXltTS9oemFKeGpuaTNlUUViRjlhczZYVGtKdkRGL0prRnFwQzlIeG54WnBDWGNHODJsYkpsZ01kYXY0R3NiRDRQd3RkUVhiL1h5d283bHBqTUZJVFh5NUpvU3djZFJRalR5VVJ5Mys3czJkS3B2WFFZQnZwbUQ5VGgvazM1elFUakQ1ZmNnU1V3Smc2MzdDNzBQbE5weTh6WUdwc2c4WXNNdmNJNXA5L05RcUFlZWxYMi9lL1pERWlZQ0psVVNqTjQ4WXdoRkpQWmhoWVp4RGJOMndRTHBJU2t3OTZDVWh2eWQ3WmVRV3RITXJaRVhrZVlzRFJPc1k5QWxQNUlKR3drNE80ekVoVWYwVElDYWdtQnNSdHBrN1JZcjBJZ0h5bHRPejk3KzZtdmZkQnBsRHY1NmtVdmtiMlB0SVRLVS9yeDRZalRHaWJwek1nMloxaFNKZWdYcmpMZ3BJWit5bVhLNFFVMGdKVWs3bDBvVXU4NWdUeFBrVjVjdVlodms5amR6dHQ2QXhvejBRcFhvQzNMdDVkaEhHSE16WWlUM1E5OGRYWWdxaVlHYlBDQVMwQVB6K1JDdWlTTU5FVW5VLzhrOHJUZnA4TnJ0R2dENUtSdkduOVo3TVV0V0JjeUlPQVBEWW1WeklOemdlSlVuMTNoQjFFMTR0dk9FeS83R1loaVcycGEzVTZDTzdXVXA4RGk1azY4bnJrcVQ0TVBGWjFDMjIrYk41VTBxVldIY2gxVndWZlZ1akVvN3pURFFTNEVtYmpHT2FBRjF5Q29xLzhlYktZWEhNL2Fyb25YSWlncHhqYUE0TFFKUE1DVzJKN05VemNCQmhUTEF5ZUNjejMyb1k5TXQyVHlVYjBoNkpFQXd0VlZ4MUVWTGpqNDdITU5UWlVobGVvcXZMTk5rK2Yxb3JFL0p0TnE4YlFETnNPZDJiZGhVRldtbXJGcjkvaE92eWhudDFPcWpnYTdVeGFYSUszUHJwS3gyNDhBekYxZXh6blZtd0hqTnVqd3BQZ29oalVIOEhKaldFTndLTk9jVXFKOUZ0VjdzN2xkWm54TUZqQmVSZDAvVG5mQ1BSdGtHOFc0U0xOREdETlRCYUkzQ29KZDdKZjFyZXpEYzlmM0pBRkVnMjk5cHBMWitaTWZiK3N2TlI3WnJPUTR5eVIrQ25ySkpvcTVmQkxJc29aVjZtRDFRY09hVWc5cnRRL1lqUnZnN3ZBdklsOEJFMVBEQllZeHhLK0J1bVhiNXdsQmJOMWg4Q3ozOEZxcXMvSzd0MFp0U2dkZFZxVW9KZmt2RG8ySzU1c0xNTzkwYXBqZ0p4KzEvSjM4dGlhTFAyN3BvNXg0amJnVW9mU2svOUFrdVNNaXoyekVIRSswMWJFTHc1RHFteTJOVWQ0R3Mrb1Fqa0ZCNG1JZ3RONVZCRGw2dEV4KzRzNU0wMWdDeFk0MVpsRVNyakYwdjlPMUhuM0VteXc3ZEM1eVprQmNoK0I5Ynp0WDRqWGVDSFljbFZsSEhkb0luSTRKbXNXRnM0S2g0dzhUUkFlTG8rY0J2TkZKelZEMi9zN0VHWkMxaEthblpVN2xnMXNFZ1pVcEo0L01Ba2lSeTI5eGVpUUkzdnpCb3FQR3hrVEpqZzcxbDVOL3JTQnQvZWMwTWRuV240dVE1VnpXRndhUVU1RWhJaHR1Y3V0SDAyZVJoNFJEQXpZVkxhUDFMbWViOWNnLzIzU0RLMWZ6Wlhqa0wvY2VkV1dtL1l6TlBNd3pwc01hSzVtQk01Q1FVanJaelJYdStWTU45anBuTU9ISzFRQ3NsRE5mejNGWHhJalRlSE9BZDB1dkVnQzdZb2oySlo1b1BFaHZNSmZuWXgyMWVIWWRaTVZQUU1nK0RHSXpZR3dSZStTbTN1bGJVS2JLakF0MFM5WGg4SHJGSTUxWWZ6Rno0WUNCamRjY1ExS2hOaDFGMG1DZHdXL1ZlSmxZQVRTUkZKS0pKQXN4L2RkVmo3VjU5OXhCQ1lmdUZRcEtLdzdmVmFUWmZVeE5VV3k1ZUZXMkN4bFl2SmNUNTNlTk0zY1hOdGZBbUR6MmZ5cEc2SHAvWmVSYjNJNTNNOEtWUndhdUYrUlhid05YNFllSS82bWxmNEFHT01QUDZJZmhxSUk1WFEwaXhlU001MWt1NlJEK1ZZRU95S1JWemN4dkR3Z0xMRDhBbWpYV2JSenVzRVlDYlJWOVRsYVd0YVk0T2IvU05LSGREVjYzSEs5cXJxekFPaEdkY3JOTkZGci9XbWdRWTBwVUlVd050UUNsL3FMWk9vZmpMdWNVSEJxSzNYRmgwcG9QMHZkN05OdGtqdTVZSU9TZWtVc2hJVGNiZ2w0TEkydys1ZHlSTXhtS1lqVXR0SzNicDZZZ29PeHJrNjFnVXE4MFRUK3BkRFhFWjgrWEZZb3pPMjFVbVRadzNIdmZmQ2VXZjRzWk5SL0t4NUx1aUZHQTdiY2xJUXVWcFpYOVVEbTFXazdSVFJUVXdLa1lhYVYyY3pleGxxcStkVDh1S3FnSjhoeTFwbURMSDRiZzE1dVdhQWpVcXJJNGNHL1NERkgzUHdKeEozUlJweVhqODJPcVM3VlRHaTIvSkgvVW5rU1hOM09GR2lMTVk4aWJxOVJ0bHUxYThxRW5nZHZEbGRCU0EvUUJibk84RkY2a3YwQzNUM2tVMEFsM3ZqNW5hZjFtOURTT2F3eUZDM1JLTG1KZUI1VUo0QzhzV2VOdmdlQmo2dWN2MmtXL0Z6RG9MbEVSUVlIQVREdHJScndTSzdSbzA0SUoyWlg4S0Z0bjUxUzlrN3pZbW5SZWVrekFFK3l0N1c0VCt6MFlEMDgxWE45Z3J5VXZDNzhjQnBJZ3pDSFFadjA5Mm9iRTRMSEFxdVdQMmx4bENycDJ0NXpjTll5dWVjOEdGUEJhRjV1elk2c29IVnQ5RFNIYXl3S1NGTS9uL0c0TGo4UUlHNExTY3p1aFJDRUR1akRuVVpqWW1yMVNCSURkMzU2RWFabG5YaFZ5cE1SZDBZWUNaWTc1TW1wVzBIdmZtYUs4eERWUytscVhvSkhwa0hicy9TWXhQdFh3TWV6SWJodzhyOGIyd3JBUXpjd3h0eWl2UkhzVzVVOWx5SGJkSVBsOUEyWlZuLzE3OVV3V3FLYlRSbFRYT2laSlZLYTQ0bUVidERYa05Idm1XdnE2bndnU1FCTTBpeDhuQVkxMUoycVJYKzduSFlkK1pNN3l2VktVditBOWI0czNpaGcvTFhWZXd4alNDM3owb0dmY2tkVDRyZ0Q0Nm1QbTk1dXdRWkdmREMwV0JyUjVmYzJQbmxkTzFNQmdEOTBSQmhUdHFBUDg4dGViT0Raa1g5NzVxa21OeUR5TVY3NE5yeU1RT29OVW92SFdWd0lDZk1takR4Z0RrZGtDMUxMUjlBWUxNeGZQMXVRUC84QzNUbEx1RnZ2emNYMDhETzBmRVJIaVdNUkJ6TUZvdW45UHVzNXlQTU9DVkRSRVJsS2ljOHZKWnFpb3JyaXcweGx4RTEySm9XeUJHU2dyZWtId1J6UE5STVVlVTRaaCsxWXA4b2lQTjRGbnRmeWhhN3FLU0xnK08wTGh1eXNOV01zZnErcmlvZjFvNHl1REpXZ3J0L2JZYTduTnVjaGQ2dnppcktReS9oVGZ3eCsyaDY4U0Q1VDd0R2htS1I0QUVFRVV6Z3dxeUJGYXY4dHNTTGQ2WlZTTXR5V0wzRm5wSGEvMElsdzRFWXd2ekFZVmJHenJkQ2pqTHdaY3p1Yzk4K1ZjT1NqaGd6YnB4UDBQTUp4TUIyV0xnUjRWWTl4UmRVblpaVEV5bVpsWit3WjVvdHNMZHBmRVMrWlBYSzRiKzQ0dkxIVk9NK28wNWFzZHVZd2RGR1JYdy9RandsT2dYc1d6b1U0b3RyZ0dBVk9ZVkRrSGZ0bEsra1FheEJwd2swVXU5UUpEdWZkWml2RHphcFBUS2IrcnJsc056WTNJZjhBT3hYRWovUTJiQitMUXRUL0c2OURLV3ZjVndlN1JtVWZOZFRPUGg2M3h0dmZ3SzVoUXg0WVgvUDhQc3Q1RzVab0pqQnZlazFwRzE0a2JsR3hUY0Nzb2xBWExPbVp3aTNjd25YM0R3alFGV1B4cEphdGRETFU0bFBCNnRlN1pCTmZFOWtXUktSaHJKSFZCdWNtaDArTmdWRDZHdzlveG9jOExNV1RIY3ZSTXJPNXRyVEtGb0VFa2gzZzNkOCt1M25tTHplSmlpVWFMMG1nSjFlSkZCMGJ1aHJKZDJnTGNwV3dDM2FVQU54ZFp4SW1LdWNMNTV1K2ZWai9IUjBud3ZIbnd1ckljMEdJVEFjcG42N1hXak1xK0k0bDlEbllHOUdzTlpsdlcyaWZnVFRZNHFpN0dGYjdQdyt1akNnRnE0Tmg3YzNuQlI4V2cyeXdPVDJTeWZvYkQyR2dhRnI2VFZBS3QrWkRncjdlT1dHVFhtT2NJa29xZGs2cjE2T0tLWXJUSHUyM1g1SE0zY04rRFBESjVlSFJlRmQ0ZVJDVm1mZHBvdlpaTUNJL25uWS85VWpoUnV0T1VZTWtJRFBmMFZpc1lnd3Q4cnpRa1dGd2wzZTV3UnJoL3gwMExCT1F0MWV0QjlPR01PKzJmZXdLRDA4WGpYMkc0ck4rWWg0aWhpU2txZ0dYQ3A0S0UvUVd1MlZxSWs5MklMY05veUcxRGNFSlFjZ2RRVVMzc3VsR2s1RG5DTXBObkM2Snl6QUtrRUY3ZHZCanVGTGdWM1F1dE9jOGptTzZIcVNsditpWDdTLzNQMEZLc1NaU09lRGx0TVhXUWRneW91elZ1cSt2cUxUOFBOK2RqZ2JBVzl1ZXRCTnBYVll3ajByTzVoREFveEIxdW1abkdEMkVwcjloY2p5YVpodVVuMWVYaTZDZHY2Z1JnazFFRzJFdENnV094T3VoN2UrQ3R2VlUybVp5T1FPQm1uNjhmMmo4WklSTlRyNGY0OGxJNFpxUkp5Y2VIbXl6amp4TmphV2FPWGFQZVAxZ1dqVzRvRUpZcnJESFV3SGFMWkU1NVNnV3VsKzZ5cU40Y2NKV2ZoOXVsT25Nb3Y3UHU0UmwxL01KUUF3OTBWcnp4cWRTdjl4ZW0wUUZ0Z0dzNVBRSDFCRmNIMEVSZ1dzN25HVVVLQlZXYkx5OG94cUtIY21DUm9WcDlYcUl5U2prV1VrM3hTUnkwS3JOZ0s1dDZsR1U2RGFvRUM5WFRjcDZnQ1ljd1RJcVZ4Y0E3aTRzQXRocGMxYkkvejBlWEVVc1B2TCsvUVI5cVhmRG5EOFdINTl0b09FTmJoSGl2TUI2b1J1VFI2eEcySEFiSUQwOXFkV0xRUmRCUi80a01UUXFlejArN3hSenVLaWpGZ3pBZHg2dUhDdGNKQmM1ZlRBZXdaU3pEZDl1YmU5RURkRWR5QitGMm5kUnJBaC9nVVVxdjlrNWhtSzZpWFp2dzZQb3V1NnFpM3RqdHR0NkRzZS9hazdjb2M5bFlWM1R3dkVsNGxCcFhSSERxN1pKWkZpa0tBcDFxWGV2L09QVkVxL2sremlLZVR3TGgvQWdDYzFhR3NsQ1B6eVc0SWs1aEJodXNjejN5ZUNPS2dsRTNrZnB3NkhEQmlLbnY1anhoTmd3cngrR3lYM1ZrQ00yUTZVeU1FUldJUW92d2F6OW80dktOWG5TV2lRREdEaVVGVHFseUpRUUdURzdLU1N5b01wRytQekpHZWFDN1hqSnh5QjZVZ1MvcThRSTYvSjdVa0FzU1RzenhXeURGbzV5OVVoN0xzTDh2VnJNSFpESXBVdFRrUVkyRmZEQ3Z1eXNGZWs4ZS9zdFlhLysxUTdEazAwenVqNlBQU0M2aGxJM2J0QWpqUnhFbFFuQVkwMU9RMWNJV2ROR2RpQU9QMUVRdDJBYWthcmtLbHdNSnlBUW5CelNGaFp6eGpxemxhY2sxRnpiZ2VOcDUvakxsU0JYZDBJZFg4M0NsbG5XWndRcThuYnVncldQVUJBLzVLclF2L21RalcycFlSdkN6ZWtKb0FTbk1IWUlFUGdBUGpVVFQ1NlBkMlFZdUttOWRsVDlEVUN4TStQWWc0anh6dXVpK2FWWmRwVjhiSnp4cmx3cmtLOVBQWklwL0k0eHcwTkZleWpMTHpYR2dOVjZsQjBPcWZhSHNUWENDOFp6Qi84ZzBhQTZURnRBS2JTZ0RiUnJ1cWd2Yy9aVUdkbjdjMVQ5RG85cGpWc3RvQllIeFpqQXVzYUxvWU1wY3UwRXdyN0x3N0FURlZPd2wxRVVrQ2lydEVBYXRXRC9hNHJvRXdnM29MM0xrWFI5akZ1bFc4czBlcW1tNUR0QnkwQmpFZHk3UnU3bE9PbDJqWjA1cWNzWFJyYVp0OVBKSnZkRjhHYjZPM3dOV2F4Snh1b3dqNkM5M1ZYYkNKcWlNTUswbUUrQWJOTVZhS29iNjlBNi9VRERDMWNZYzhHaWNYUDQ1aVh6dW10Y3lhSVpHbFJEY2ZLVE5yYzJpeExQazB2Y0daSlFZL1FYR2xYTnFIeGZudWRqY1hUczByWlZtQjMzeUtnalhreDZ5aFhpVzBzVm4xUHZlbU9KRnk4WXI1c0cyNXRybXZldWxWOXlQcGdicXNwZi8zTGxVeXIrY05taWJjaG5rVXR1WXgrNHE5OHpzellrVjdHelFWZllqVVFOcW94TlZCbHJyTExab0syU3RpbTFBdkovREpXNEUrOVIwUllEMnplS3hVWlY5MHFQOTVWcHlacTN2eldiQjBlRFQyZUpheTlmSi9OamxXcWtJdUZRK004RStxYVhEek9JVnY1QXhuWnRyR1ZERHZubXVFUFRUMFphZjZ2Zy93ajN4R1hQUVVkNjM0UkgxV2VZU21senRnZVN2VHNzcjRSWVZIdkgxMjBWQWxaQ1U0L0h6bDRyVkpGaU14RGxlYUhJWnRISUNwUXhXQWJqZzVqdUFNejVYVmlkWm5DZG9VM2dYM3Fxb0Y3Vkd1cjJnWmk3UDFjaWFZZlJKaktBcERTM2xFQVF5WWtQUkNvMU01aCtUMDVNYWt6UjhQTHM2MTY2dUJCWVNoWlEwaFpmV3RFZ2xHak1KTkU0U3BnejluZ0hjcVd6d3lIMEtPN3I0WEJKSE9RVGUydExFdWEydFJYY0hTMXBHdWRsVjYrQ1FIVGNJVDZ4dkp0dWNPRlJ3U3BGYkcwbWE1K1dKNXEvVzdHbnFvUWhGR0tMaXRteDhYaWtxd2luVmd3Skp6U3dBNnkyUk95SVFRV1dVU1l5bmJSdW5JUVZ4RlBJTTFHWWprUzRPTkVka0MrVHpWT3dZT05lSHpHSkFtOGMwK2tFZ3MvaXBvM3BXcTVyVzljMEV1amtWR1RpWVI3ZDhEWktnNjJZU3hjL0wrRWF1cDVKWFJuN2J6ZDMxSXA4WmpzcTZMNzlYa0lMQ1U4aUI0NkV3NzhPekQ2UzJ0TitiRUV4RlF5UE42K2FoTWFFenFJd1IyUDZtbzdZYlVnbWk4M25jUmMxbjNQQXNVYkE3Z0g5U3ZPVG9LaHJWUDhob2FRa0I4NFZTN3N1azRSNm9ETHpXSFdVc0JXY0hvR3BhVUdTRmpucGdrbkxEd0hFcDRQcjBLemZtK3lRVEMwMllLRkxnaWVmVjdRM2lwMHRja0xJRHhDVUZoVGUwY2FiU3h3cXlWMHBHZWdNQStDSGpoQis0M2hUeDh4dElEUVNEeGdiTmJYMVFnd0xZclVtNDhRdFkvM29LV1Z1d0dsMDRqVDV5bDFiRVlzWFRvZ0FaZWtjRGJPYkpQb28zbEFNOUhtWXdSaVRCbXpsVzFxcjVNSy9lM3h0WVQ0ZHQ2Y3BFbDFxR0lBR3QrVVVsek1lczVqcDVnV1lzMkdYZW5HZnJQaE1ub2V5OFRDeTM4alYvWGFqWWc4SytER0tXUFJMbjZYRmt0d2VicG45NkxheXpvU09yODNLdGpiL0lWQmtRQTRYMk84c016S2VJcFRLNkNuajhiU1ROMUIyZWY1cC93M3ZYVjliaG4vOFN1RVMwWUwvRDdqT1dxUXVSRk9hakVFc01rUWNnWnFFVGtDd3FPME1OMFBjT2I1Q2hsd1c2Sm5KMGU0cEpzTzFLM0NsSUk1VC9ramlPeUQrdkRqUEFjR1NCckZPbk1Ob2tmNWMyaHVPZmozRlBkbXJVS0Mybnd1NmFrN2dCL1pnRE9zekhKeWRBa3NiSGkrVWVadFd1NFFVU09qVmRhL0Q4UG1vNVBwdkxiUnJIdTN2U2R6cVUwcGRsZ2NYcGtVQzM5WGUzQlhlMlovMUo0R1ZtbDFUaGtIUUlmVTZqYjF4RlF1RzIxZHpDUWQvQ3p5Uk9na3dWbVlKdE9ZWWk3Q2xMUFZNTmpjcUsvOFZISFZ4MGNZa0dQaTdlMmwxeE03L0dpSlJWeHM4WlU3enNNZkpwajFpNUwwZXgyeEZzRHNUNU96UmRWNU9MNkdmaVdlSzg5WWswMjl1M05XQWxPNWQyT2d2eWZNa1prenRLV0txMUZ2QTY3M3BNa2V0eXMwYjQyb3FYazJhdUYyWE9Va0NmV01Sb0N5YVF4NmJuMmdKOWo1RFBabzNnME1sTzVtYTV3RTZBVnZrekF6U1RQUmlMTTdnTTYzVVVYYkt0T3VoZEZmWVJDbWYxVnFKZUY5WHhYTkJPSUlwOVdaMUdmT0pmbyttZ3A0MTNFVi8rM3BJajVGSUErOS9PRFB2MkxtUTRvSkxvakYwOHFNVGh5cjk3SUQvU1B4S2FMa1AvSXl0eEYxVlNFYkhUOUV1RmxKZ0paVzRIMEZvY1ZtTHdmd2JkN2Q1YW95K0Z2bU5kUklkSUs0aU1Dbjh1U1lFNEVMSitNekRFc05VMzI3eFFuL3BCdjQ5bXk2ZytuWVY1aGg3VkxwVGprWUxXTks2My9seHdNWW1Xc1AxQmVGMno0d0lMM3RIQ3hmTkl4MHJCeVdFTFMxOXhRMkl6V25mTE1POXFSZ1l2b1g4b0NoaUVCME5IeC9xMjFCZGFvbFFMc0o1YjRWemNoV1BHeU5sTFErQzlkdUl4Y2NFQm1yR2kwSEJHNStrNEVOanZ6aS9ja0tCaXp5NGNCdFJpV2p3elBmTnBtamZ3SlNDd1E0c1BJNy9PSjFGTitEY3ovZStteUlZeWx5VVp3SDJvQys0VEdxVk5tMzUzYWpBWjRDcGxCUzN6aHJtdStyWjFlU0g4SUszUXlqWmNZS3BoOC9GVzd1eEYxZjg4YXhmS1pvUUZFTzI5RWxRNmx0TnlrNnVYWlgraTVwbEx6MVdoZXhZdCtIWkhzRnVNclFXWUJBNkxlaldramVGMHJWdFRQRHN6VTcvRnVBRDFEVGNxUkNSTHladDVwbWZKTDFLVlJwUzYvYzZsQnp6eGFSSU9iMDVhSlpvU0JqSVZ1N0U0REQ2UlpyZWtDcGpFZkl4YlNuVUVHMFdKdWZEV3duajNhUUhCMzNvaTFoMkZHZ2Q3Sjkvb3dwR2l3RXhGTVRsTmJLdHFpNHlnazRqMzQvUmEyVitlZHUyL1pKUmVWSWpuWHpRMXdSUEFKYWxTTmN1UFRZRkhqZ3lYdjhMK21naTVnRXJXcFRIV0hScSs0b2dwSm82K2NENjJKSjQwSmkvNnlURERpTUlpd3dmUTFzMHlvTlRlbklIYVRob3dqazBrbThLQW1mVnB4T01iMXdDdHhCTmRjU3J4dnNhWjRJSG96WWxLUEs4Tit1Nm5KeXYwNFpTY3RSRUF1bGYvS1JZUFFNZVowRDFGamZmUjhUTzNHVkhtMnd5WlBEdFNvOHlaL1ZKZU1JeVRrNlludXE2Zm9sRmpHLzZ0cXl0c1FWTU9kY0pFR0tpTVhTNjZacFhDR2xTdnM1aTl6eG9zSGZib1BNNngwbzlwcTlDdlh0VkV2UHlrUUc0S2REWHdJd3dxTEtFdCttbC96aWZTSlhwN1g4Y0YydGVWVGN4T3VmSFg1eFpNMXpJcm5ZVWtzRm44cXR4b1Ywa2lmZEdINEIwNEVGaEtyQ1RmejgveVo2TnZKZkVDd0V5UHJpb3lQbnZ3L0w0VHRCK3ZXckhlaUdnNGZSYkJSaFk5UDh3Nzl4V0pqdEZWUHVFZWI5OG44bWg0STR6d2JIS25pNU9TbmVxaFRrOVk4TFYwbEVKWHBhbXczVGplb0NFRDYyTFZtdEFkTFpWMlBjT2tZS0VReG12RHZ1RGVuWDBKbW5nV3p1S0t3M1RKS1ZsNmFpbU5WL2xjbjR1d2dla2xzdnE4NXRFUmJzc0NMRFIvajFLRXZNdlZkUXNDTGJaemRzOXhFY0tLLzlEdlNnTWpMYkF3U3RVM1dlazVpY3R6SThoNFpXWDJRS1FlWElRWHN4QVdsUVBUVE1BdEVJN1MrTTBoREEyMWdZWHp2VUY5Zk9pbG5lL0QxQnRJenVqemEwUUl6TS9IaGVZYXZDTnFHaGtNa3NmYXVPZXZnRlk1WVo1WHA2bjIwVDl5N1NaYklKMk9Hbnd2ZzhwNTBmTElCR2t5ZnpVbG9MdFNEdUpZbGVtcEkrYTEvZThSeklobWI5Y05udzNIREVYaHlEbXpDRDh5VlQ5Rk9CRUtrY2V5MjdPelFab0hFakRRUDZrajdpakxRQjhZMFp4Nmk3N2YrbkE3WWNKcnBUNWZzTVhvWXphRjVZeE0zZ1c0cmRGV21VaEU4bTF5dXZSYStJaE5SS3lYZUQ1b3BwUkRBNE5IWnZOc2ZCMG9yV1pLbjd3a2tvTnI0Z3JPUWZoK3RQd21YMFRBaVJGY0tWNUl5QnFnZm1JZm05N29FSW9KREpyOXRBRndKSCtVWlJuL2s5L1Q4cloxYmdwUGtWU3NxOWtmK2g5aEpINFVlMENGeEFiWTRPZU9NV3RrVjVRaEJpTnNscnZPYWxZTit5WWxzbGx2U3pUc3VEOXpvRUhwczA0UjBJdm9XVjVqYndlQ3pETjdjbGJFdkVKK1JzR2pxOGQ5TDlWY3dUdzVmS2NIVEJnekhjak4xWjFvclAyRUlZTHdHTGIvWkJjQW1kZ25tVnVnQW5Sa0tWWS9VRlhKZ2dzZG1YSUozV2NON05ld1YzMmh5TFczS21KeWJOZTF5WkNSWXhSS2FXb2dVWElJVHhpeFhGU3hEQXE3Q3N0b2RweEJCYnlVRUVYSTBYVTRVRGx3ZzIxekcrUnlybWFuczVJWXRIN2JpNXNIQnlDYXZIT3d5YiszTUZSazFSUGplT0VYQjAwUUUvREtpcWxoWURScE85end5TmZ4OWN6SlZkTmNEbGJ5MTF6dVk3akJMMTRUVFIvZnJXVUhwMnJXczZyNXpNeFhWMlJiYVlQOC84Q0ZHd0hQTHljKy9SUS9ET2lhVnJkMFAyOXVTSGJOZ2JvQkMxTHFJZkpCQzFqa3IyeWNoYXUyam4vVVl0NlZKNlNMUDZoSDlidlFlUFBodGhaT3owNVFHTU9yU1lkOXByNE8zaXFYeUs5Nm1GU3ZibjkwZXkzNUtndVloYzh6VDdqd2tFNVJSRUNmSm1OME43NnZpNHNiZXJiMHhFUUpXQVVmb1IwQ3pUaHh0SklvYjFuRUkwWXhuN0xqNmlTV0QzakhobGxtemlNVTJOMm1rY3hVdzRHR2dYY0JKSzhiaDY0cFp6S0hMTVhzV2diYlRNRjFjekxQWkxDR1B2V3o0ZWxwQzVDUEtVMVA3d2VLRy9rT2F6U2szSUY0RVowc2gzWGdFbStJTXJtNXhnY3hsM1Y0T3d4QmoraXluQzRxQWpxMy9EWHpmZjF6RE1nS2Jydmk2MURvaitnVGFPNENncGZ5VHZwVGxKai9SMkdCWStqYlJGRlZhVjlIN2hMclZFNHJPLzl2VHZsRDY5dmxDZXBsYUFZbkluTjFUS0tpOC9YZTYvdDBvZ0VkMG5PNE9pQjBMR3YxT1BMRDhOWGdpMWRhSERSSGtwNU5XNDZmNytKWUJFNjBxbmVFZzJnS0hNV1lIamk2R0pWVGcxekVTckVYVkJjaDVWRVpJaVd3WjlxY2VSNVVkSTQyOTdGT09yOEZUaEZVZmVJM2JHVG85THYxOVJXZEZ3UXg5UHRTTkpkUk8rWnNOV3M0bEUydGY4NkRPRnJlT3FYVU0vNEk0UWtFYytyNVo5dFFVSWJEVXp2elNnSSs0anJ0KzNpS1pGRVZWWVRnWkhsZVp0SWI3a2pDd2FwU09MNno0RS9PRUQ0N1MvTWlmamwvMy9URGdqODZseERrYnVESHFPT1A5cFFzVkI3S0ZJemllUGJKUnVWRzRJc0x0eTh5TzZDL3B3bHpTaWRrV2RWc3Z0Q2NNMnFsOTRVSUxMeWxxTTNtOHNDQmozL1ZjSEZHYnVIVzRid1V3NW02c2UwdkR2MjdtMW11OThCMzIwRUpnU1h1WU5wVDJMR2NuVnRKcEZjYmtJRElWS1o1QmVhYktEdU51VXpnSWJ2b2NWVHNDWk1RQ0JNNWFVdEVSUDIvVloybHFvbXg5NTFtZHZ4NWtIK1ZiMEprUVdIVEtsbVBRS2lMbnJvb25MSmQvbWdqWVFYSlJTRHJ5ZGhQaDlXazdHMkJrb2ZoRlBYQmY0a2VzQWdhRlBET0Qwc1d3MnVGRVpwNzB2ekp6dnBIbGVuZ0doOWZVUkEySCs2NXVDVU04SDF2c1NZeW15Q2NjS0pKcm9pMm5ZYTI2UmZTbk8rS0lMZGYxVzhsbFJJMlgvOWJRMkE5ZDNuUGJTUVpDeFlpOStGVkpWbUFtOHZMdEpaWjRyZkYraHNsYW1zRGVRWi9OeExXQS9Da3VQa0xucXFZaWJPQzFJalNsRENkNldJUHl2elNqaUQ1dWtrSTFjNnJpYlpCbHppdUVRS01iWXpjTCtEdWl4VE1iZXBKLzQ4SDZxbndEQW9pV2dIYllQUm9Pb2s0M1diaUlLUUpTY0F1NVV1UC83M0Y1NWFXWU1MZTF6Vk5RWm9rTEYwYnBaZ0g3Z2huYkxIeERhcWVFSGdWSkpaZWl4ZE9XYXBmY2lrL2VzZkNiTEF6VTM0Qk1lcTQyYWVkOXYwWE53Znk4VmVBWkEvTlJ6TFBwOTYrMFIvd3BZYXppb2Y4VUEzZFVwZTRyWkUzN2VnZ3hja3BZblM3bWhrWEZBOUJXL0RZN1dONU92c1VIamJ0VC9Rb0dKL1RCUmFRRFVlcXZPa05yek5yUmJ4VVV6eUpEZlRpUmQzc00yUVR6ajRSSWxvVWo5VDVzeTdIZkIzcldOUHBjSFJZSVVpYTZVczIvMWlwOXFoSSs1RUZiMVFhbm8veS85ZzlmbExnTGhBN2x0cnV5a0NtWHlJcWJEWi9DY1pjb1hDdkVPNFJsRDYyWENZZ3V1d1J5a3lJUEU0TVhtQmlCT0NidTZkamFBVzJIN3dtbVRab0VyYmZaWndrWEdJM1FNQWV2eHllc01QNk9nUjhHWW1oeEE0SjJnWkN6amNEY0VmMXU1aVFoQmd6em9wYlc3MmRhQUgyWUtUTEtmdnJmaGluaGM5L0oySGprRENWM0Y5RGtodFQ4MjBrQ29OVFR2RGRkVHVWNktqSGNKUUxEdVlqejRUaUZjQmR4bEdEdTVPdGJ5aDMwdXR6QWQvU2VzRExNbkR5UkxmaDliZDdHdWR2TTY3YmFjcVVnU01RVENUQUdhcE9pWFJUUmVlY3RCUFhJQWlreFJ3T3V1ZzJTa2pNV0VRNGtHTVlQSi9ZSm41MHNHMW5GZXpEVUFzemh6dHMwczJpaXdLZWFtaTFpODExaWpWWjdVanJEdjEyRCthTk9FbzhBdGIrMXdXUElZODJvTXZYTENWTm5Id0U2VkRKM2pIYytsMGtvUGZqUWZEaVhrbFZGeVo4L2dBYTE5VmFTOEJJUHgxcjF4RGNuNUxYL0lvdUgrTlNNK004YkFuZlVncE8xUVA2d0dKK2pJNlFFQkg0YjVFTTNEREo4ak5rK2ZBTzd4aStvMmJsdWlWL2psaUdmcE9hRWNSZFFoR3RuUTFNRmxyaGlVK01kcjhYdG00TzN4UHV6ZTg4TWExelRFYW41dEx3UFhaK3plamRXMG9YUzdiTTFmTHNEcTFtTGRCWGlBNm1SemxwbzQ1YkxWQVdRaDkwZUFMQzB4bHZqSlkyQmNoRHdhcUlyUjRjRXRObXhxZ1pSdlIxcWNUbFZ0SlRMaTNxdjlsMmswNC8zbDd0ZmwrUk9INm5IUy9HalgwVE5qc3RveXNDdEZLWnNFamU5T0VuYjZScEN0YTl1NUVoSVlGU205K2hiK0hRalFTV1IvSkphcTBkRGtIOTRpbmQzUitDK2xTYTdnSWtzZ25FRFpZcWZ0WWk0eHVqM0wzTy9sVmtCRnhoK1JMMTZZTDRYSE9wM3BUTU4xckYvcHRZeTBnT2FIZitlYUw2Z1RqbkgrRU5GSzFIbVBzR2dWRGQvWDRISXFHbFozaDczREdrMWlUWXJyUDJQWSszc0Jwa2pHUXhYSEtFeWkxVWsrWkVGVG9TUEcraVNFV2VMVnJEWE92UjFyV2NtcjJ2OG1qd0RnK1lySEtBaVNEdFE1VkFjRkl0TnExQlpJVnRTQldGbVFEMkVoYVN5V3Yrb0VyV0Vpb3FGUnMzQjhubW9RS3d2RTlzYXlrdkZyaXk1NzE3ZFVlMlI4SmVxK1A0ZXdGNW9zcGpzVjRvazM2VWQweEE5dURaSVBFNGI4U0NkUThZNW51NEQvWm41VmF3OEdMa3U2MG1qUkExMkFhTzlyZm1MNUxQTzlQTTRLOG1UUjhBK1lWb2V4d2g5c2xFcExnTG44OHMzVXB1WThnbFpnNjV6cVVCRFRoQ3M3WDZoR2M2Zk14dDBpYi9kejhKY0wzQXJncGxzM1NHV21pOGFLNU96RWozOENjcEFXWVVjYVFCWTczVW16bTJPb1BiM0ZZMFAzWHZheWtOREYrcHNQaFlYMTdJdGs4WlRwU21aeFBqOUJsbWNScmJpQTJyWkZ1YmU4Vk9HK2FSS2xtTU1RUzdjTE94SVZrWFhYTUNMM24wQktsa3Z1THh0Um1lcFk3TFJ4aEc5VU1LQWwxRjN4L0FuQzM5aTloVllZaXk3ZUZqWGc0UkZzYnhwM1B0Zlc4eFFHaUFoYWhscVJncGd0VGtIR0lSWjY5ZGsrWnA2WDZtU3pEMmRuUkcvQXU0RXIxUmU1OXB4WkI2WThZUXF3RC9mVnZTcUUwcnoxUUk3TEg4M0ttbTg5eFFSTE9xSTRNSGNDeEVQQnVJZVorVXVOMEtjaWYxaUZ5cEhxdFdzVXdnTlYrekN6SnVOamNwN0VuU1lGcjk5UDI3d2dqd2pxYTFyS3VJMHVpWmJSUDRrMkhndWduSXUxYkoxY2FiTEVkRWhNRGVlSWRRWUdrUlJHV0VzMm9vZDZ2QTU2citpSVNQbS9sRnFheUFxeDQzaWs2emk1ZGU2TjhhR3djZmFKU3g4aFVtb0phTVlHQUVnYk9pclFERmI3NXpiYVBZZ28rcUxBc2MzdDhQMlFndE03RWE2MCtkYVdzMTlXMzhBem03cCtzdDkwUEVlcEhNNlJJdGZsYmRIdzVacE83OC95WEtMOGQ1SlZyd3o1TjhxRjQrVlVld2JSS1ZSendFZE5wTExHaHpjR1N0RFJnbWROdW00cVE5TGFmVGhhT0NmOXl3QWdvTVpnTjRTTU5haEUwWnlITTBhVDROS1N2OUJ2NkprYkU2ZFBTcy8rQUdZRXVZc25GTHFQMzh5V2p2eWRkQmY1cEpxOEhkYXZXbUpEWmY2UlJBTGlkRTNQYncvY3V6TUpYTGZWbVhpVHpUQUNGYWYzNitPUHgyWG5DZ3RYR3dHV0ZQRzlwMHErVU5FeHR6eHRONFU5QmRDdzJ4ZG94dzhmS21CMWo2Sk1ESW9YOGZqMWljWWFiemhwTkV6T3grQzFzL3dDZHZlQ2N4SWgvOTI5V0tRUFJNZ0VVNGhVZlBPTUs0YTlyZWFhVGNDMnZFckNkZnJiVExmVXFPeGQyUWdSYTIyVitwemFUbW55L1liNnVTUUlVbFB2YlBVZUM2UE1DTzAwMmhrZG5tcEhpTDNzYVpBdkdzUUZBb3VzMXQveGJuY0VXRnJrR1A1Z1FvenpSUFRGZlgrejJJakdRc1dCZ1F4VURhNzVkVmpiQmJvVW1OWGhxNlNGaFJiNjRqU1dTc21DT3llMDNvV25UQ28zK3FvSkZLdW9YMk1DUWJLb0o5YTNid01lZnJlM3AxWW9LWTVmckgxaFBkem9TSVlEc2NTc0Z2QnoycC9UU0FYQmhyN2FXMUtyakZzTDlRZjJvSUZhdE5YQ09YOUpvQ01aL2V1M1BlVTFhTjlzRThFMk85ZXVPejBFS0VwSnhtY3Z0eVJHVWRLNzl5YWwwNkZ0bm9jWWFzTzlrN2tSZjZFNDFlS2dnazJpbE9vaXZRWWt4MWZ6UjdWT1hkZk1yeThOaUgrM2NVaFVoOUlPVC9VZk1YY29YbG9LV2VzdkllSVE0YkFWcDF4bHd5QjB3VmMvckF1UTYrb0Rsd3VKaG5HSjJrMWhJOGw4T1g0bnFGdVJxSjJReW54MGgwSXd5dHZjZnQvUVdBRzRpOTIxSmU4Yk9wZ2VnRk5RcGVRWGJ3aENmTG5reTVrOXpRTVBPTENLWEpWd2NvZFRwSW1EQXh0L25WN0o2TTMrMXR5NGVIc2RwN2M2VFdpOUtLOXlvUGpHSzU1SXZqRGpTUXFuN3pNRmU1WEo3cEtTN1IvZkFwOHRSL2swU1BweThld0tmZ0E2NVFsaVJ1ak0rRW5wZXczZnZwUUYwM2FJbS9LdFE2aXhSbUdSUklQMjBBY1hqSTd2ZmdtQ0cxN21rTmZXV1lvaW1XQnM2V3R1MGpxQyszODZ1KzkySU5zTGFNaFJCQ0hmZXJsdGY0WkJYKy85NGRIM3dPOTR4M2dDVmM5bExkN3lmWC9VcTNGTHVhOW9RNHpjRnhSNzZSRUx5bW5jTVdDSFk0WUY2Q3lSaDhoZ2dTeDRWcnpQc0pWNUtaelBIOGZUOGxsc3FCRGpwN2NNdGk5emJ5UFdVbXN0MUx4UWRRdCtoNE1nbHplc3ZtbGhkc3BsTDdtdjB3RGFubmM4WFZmRzR5UEFRaXZpVkhkK0lHbTc2TFVsL3VjTnV0UTBkYkZWbCtONTl2bzA0YXY5SlpqR2RqR21XaGJjblZ2THNYTWkvTUxWaHoyemJFaVd5c2NPS0ZPOGVrejd4TmdSak82NUJYcFdJZTB3Sm1pRjNKMGpWN0RLNDE2aWZxZjg1L2hTQlNNSTVFZ2UyN21ZVXNlazZrbEMxeEgxN1ZkdGwyRFFLOXBwbEtDZkk2N0FlczlHYW1nbng1bS9ueFNid0pJSk1jcmdUU1lMNERvelFNelhwcW5uNkdKaUpvQWtVMm5tc0hoMnVkQzlrdnNvTmhqczM5bDRINjlVN1J5VXlNc3FDOURFWjhOR0FCTFJYQ3Q1aVZpTm4yZ0g1U096K0VtVG1UZXFmQWtEb0VWUExsQ2dMVjVRNVJmbEdZSndJWmd5OXFOa3NVeFgvTU5zVEZ0dUxwbytDbDB1Q09wZ01QZUFxS2tyQVhiNWpGLzg5ZTRVeTZLNDdCQzVCQmZXdjJSWGxaN2ZiNEZKTG9EMndzNzBNOUl2L0g4TFBRRFFtWnhWcS8wOUk3aDJWL3NzaWdzcndkblhIc3JHUStYcXhmYTlzdks3WWNBS0lYcEtGbk82UHhVM0hEUUxZTlE5OE4wczgvYnFtbm9McUJPTGdER0h3WGhHYmVLSnk4S2xjNVd2SnNFekFrV256c3JPWlJLNWZKMGl5OUxNWGo2MUlJczV1V0tzTDNaekdRcnFUV0NmQVNzL3p1VEpiZmNoNGo2Y0VBOENvS1RjVHJvTHlEQ0l0UTl3V1crVFdYU0ZJMmo3WU4zZlR0bVArYndGTDFkcU5zTzZZWDNVbHhJeWd5RUhkTTd4MFBSRDRkSmppVmZzUnZQd1hxYi9xYXJxMzNIZ0h4SzJweDZ5TDVFR2hNL1dsSXZiRXZsc0wxUERxelVLeVVrZmRjSEsvV092amN0QVFwblRRUVNGbU9CTmxpMFZocy9obUt6cjVyRkdYZEpmendiOFgyd01JNGRtQ0FaZmxSK05UbkVudFR2eUdpS0lGNmNJZUs2QzhFdTREbUVDUU0xMnZHSDAzU1VLM2pBbzZDaFFZbUlrMkFVRDR2QTBXS0pBelk3ME1GeGZJa3VBQ0Y5UmNYbUlaZG9ocGtiVzlHbCttVUZyRDZkdVcyakdDeFNVWStNUDAyeHpacTRSRjlQSURGUlNWcEplUDdCQzl0czlLTjFadXpIMVZ3SitUaEt1WVFoWEFIa1RVeUFZcGsrdktvNFVPZkNycDJzaTQrcEt5WGFQdUJ1Wkpib2c2YmlCL3dsMGUzZ0RTa0w1ZFBZb0RvdVpTSUdFbk5XZVJtN3ZYSmQyUldIRFE2Q0dDbjNOdTFCeEJ6YS9yVWh4VStkVTFabGozbXJiTEJsQ3NibWRVd09INStNUlUvVE5pU1oxbnBmQ1lvY0NRQ0xUbkxkUi9RVFlpdDBqRFNja2xLUmNvWUxmSm1Na05MeHFTNWdIOS9NVndKalhWN1MwM3dkZXJTcUQzUHdYMTI4UGhFakRIN2pVQ0VIem1XZEFvbzBWTE1sWnVYZnREUmViVFYwUzVNNk5yelZSdHRvMXRXa2YrUWcxQVB3b216bXdWUlAwell6YTA0eEpWbVFQN2Z4bEhtSitHZXBFY3lrYVFxVk9uUUZWQVduekpTYVA3S0FxNjlLMDZLc3hZVmJqRVY3ekd4NVp0OWVWWUJ3R3pHVjhydklvRUpxejBxMmNoY0lyTFZhMHA1Mjk0RnVtK2lHWHIxMXR1UFdvQVVCbzlhcFowQVRZTnBUcGQyQmN4ZTIzM05wamQ3MVY0N2tUdTVscWlBZ0QrWm9tWHBNNzNyTWhCaEJxTHg1ZUJna2VQaEZiM01BTFM3VC9IanJ0VnJsUkRaMU5EQmtpTWp5eGx6Nkd2ZHNCQytwVFFXTWVkNzZxMmJsMnVHMmVUV2VRMm1aZDE5YzVWUnpzQ3pZd0x3WFBPblN1R2N5MTRaUmUrdVdtSy9ZcS81dDNVaVpmMERkdHp0SW9uTVlYUHlZUURIZEMzT2h2ZXp1emZQQnJVSG1IUEdXc3g3YktWNklVaEl4Q08wRmNEbE0wbkVWOW9pQi9TYlFGZFN0eDJIK3h0Z0RmV1BMWW5hcTdIZ05VckhIeCs2MkROY2d3a0k0TkwvQnZuUGVrcTJ1SFRFWGR3NTQ2WDI0MnR6d0dMVkl2d2YrYzFvTEdpVExqakVYV0dRSkRnSFVFamxWdUhIWFRqVFdkTjVRYzdGNy9kZi9URXV0a1B0S0VzcE1xZ1BrTmNyb2lQc0FkUkZPeisxUXU1MzJ0eU5EazVFUUpIV2Y3Z1ozZVBnclNKUXBSN3hHQ1hWdUxyU2hLZTVXWHpJZ2pCdUM4Q2diM3BoY3NqWmQ3SEs5TCtGZHFZL3FxeGdyWkVnaGxzTXNKU0k1T1puRzlOSEVzOEtzWE92QWY3dUY5bk9UM0FUM1hHMW5GS3ZaV0pBOGYrRWUyb0tjdCtYZTdQTSsrUkFhdE1CSHphMmJPYTZrRzFiMXdrZFh6SjQvVG8wZDBpdGhoTTJBbEZxWWlkZFB4WkdDeitXVUF1dTFibUZOcmZ6RmxzOCtPeW1GKzFFTlVBaFNEcUUwOXlHTlpINGp5ZFN6MUFJZlhTM1pwZzZnYjFvTVdGd25sSDZ4dGRMc2NLVWMrc1JsYmtub0xHZEJ0M3FCdHo0b01PVEl5VHpHckVOK2N6YiswdGYvTGNPQ0syQWVZVDNHNEtvVGN1RzErRFRYUG1KVXcvMVBiYndyaUZ1eXJYNm53MWhWYU1idVFSWTRUNUVuTnJXRWd2TUVQZk44UDZuSHUyaG9aWmpsR1dSb2ZvUVJIR2VWTFhBb2dSZmhaSm9ocmpJQ2NkMVZwY1NnQWNpaVV0Rno0Y09kL0IvMHhzZVlEcVlMU01lcjRvRUdoUis0ZWVGRWNYSjg4b0ZUMHpxTE1DNVhPTk1vb2hhdCtNZ0FGaHlLaFlmMjZHMlBmcGx6VFRmVktONG0ySmUrUjFNOG91NnZRSmhncHUweGVjMWQyUTcwNVhPVFh2RTdyenl4Y3NVclR3M1dENHBvRlh1M3lzN3BUeTQxVzJBb09wZTJFYmpJVTg2RE9CUGhGaXExZzFDMTBDVm9Uc3JuQnBOWkVleFowNjV3ZmNxdWZxbDB2N2k0N1lVUzBIRXVLUFREVlArK2dqTExVRkVvNm1leHM5aTRWMWJRR1loeDNETUhiUG5jYzAxNVZFTFlxdWxqYUpQS0d4c2xvSlBpanV1ZXNmZ1g3WVZKY2tTbWJnZEJ4OWdwM0pEc05BZkpqSHVrS2xuTG5SbVhWMXZ4Z2o2RVQxanBKbHJ1TlpkZ2MzZTJ1clVkWXRyZ1VkM21lWmk4VFNSR1ZtM0VNdFhvTFFMckx3MXpqUmZUbUo0Q3pUTU94SEZtc2RvMHRwWVdMalVtQ2ErSi9EaGpSU0pVRWU0Y2twQkwvaU1TOVhFMlNaaVlpUG11dExBS3FmeVRtSXNlcGVzVk1kb3ozTUI4MTBXL01ZUTZsUFNzc3N2MHc3NjlacDZERGtiUnQzci9JL3BOY2tQR3lhTmt4cWZMQktuNHVQUGdQemNjM0hyMGNuQ1NqdXJScG1iODU0MmcyQ3RKSE5vS3pLMDI1TkVlb1hITkk2S2dLaWRJc3hWcllzS3kveGxEYmJSQjhaREs3L1lMY051VTNMV0s3Y2Y1M3Q4T05qWWJwQW9yaEo1c0g0QmliZTdiMDBpK1ZCdVZZRW1ndjNUS1Job1lraHlTL3kwZlNLKzMvNFNFWkN3OUtCbnl2QU9IL0pBdjI1d3U1UFA1WGlpbFZFZ01YTmhIOXlHQzhGSG1wSFpoaU55eitSN2sxOGN2NXhCRDdQeDVKWDN6bG9VWU5EV1VGV3V0SzVnUmJ3YUhRdi9KWll2K3F2Wnk5MVhieXdzcm12dXBvSU1EYnhySmhFbXNnRzBxMWlXTjFZaHErcnd3S0lZTG9qSy9TaXZZMkdsSm9NVnk2c01XbXo1RmhXdjRvb2VVTSthL00weVVZN0hCaWZRQkpOaDcxRW92bk5sU1ROTEtBa3ZiUWo0czYzNk5DK0NLckovR04xcDMyWjh0cUNpaTlGNDAveUQ3UGRHK0pOWDlvL25yVGdiUTZQUlZJMEFMN0RRUDdweTVMZ05NUk5jRFdlaUgvQ25HQ2JYSVA5RlhPVWdzOEJxKyswWEtnNGJPeVE5bjIwR1krZ2hycFE4VkxnUDFORG5wQjRkajAwVkdIZnNjTU5OblNwbnpGbkI1ZHFKdjg3QllkQVhNWVZoN0ZQRysyN09oMUplUk5GOXFkVmRlWVVaNXRHL3dyNHBFa3JIVy9lN0s3enNmdVFGL3lCNU8zVjhLb0RERHdWYkJ5a3RuT1A5ekRXSFYzbmRTcXEwdUtRcWk1MHUwa1V6QkdVTUhETzdueHM5OTM4ZkJiTFZqVmJRWEJwWVpTL2w5U3RiUG1CcXVSM2xScGZLSzgweGJSUDQ2dkk4eTRSc2xxRGFEZHpWNDVWZTRFeVpWQi9odWo0alFwdENac0hxMXdpbVNNUUtzWWFKQ2Y1WXBLNThUQ2RqRy9uTXdnbVdRcnFic0N6QTZ0ajJoN2hNQUJTaXRZZWJQTXZyQWtDTFR2cU1QNEZHNXRFdG92aHBtSFh2UzZjTjlJNlJFQnB5eXI5K05KRFVRc2djbUJ4OWFYeEdEUkMvVU85RWoveXlmaDJsayt6dDRhVndacW53N2RQMUVvcXdueS9GNTQwbm5tWjdTYWtDVmorVEhPYWhGYXF0Q0E4UEZRblBQV2l1RkxxcitocWhWaWx4aGIxNG84TTZNYTJEVFBaQ2l3emVYckdwb202M2pRaHVYUzIwdTd6Z1gyTDdLWTVEVC9iTUVubkdLQjhMaC9UOW50ZVpGWCtocEtJbERQNWRVRVRkbTlTVlFoVEliMkNmb0ZjVEoxb1B1NjF2U3lDUXhDQW1xYjVxMEpLWmtqTjhaOWx4SWFGNzVZbXZ5aG5rZWNGZm1VcEVYY2FUdWYvQ0pMdmprQUdYU3Q0S0NNelNZeDdLYnJnK2cvSFdQUDQvMnJjRzVVR2hKVkp5czZYcFJ0VTcwRzdBUXd3SnBCTmVSRGo3RUxyektqendpd05TWGVqbE0yUHpsdHNvR0xUZGRJeVMrUndIdTdVOVJVaDdOZng0YXJkdE1aeEx0YzhrcytncHhhckhzRkw1SjBJTFhURFY2UzJuWm5pVS9YUTUwNGplc0NFVHo1M1hxdUlmNnZuNWthMzlKTDJIbStRcGhhM2diMUZoTVFvY29FaDJLaE9ERFdKK0hqeDk5M0M4eTVlbHlyL2E4NXRXSlUyZi85QlowOXRqa2E2OEVvM2d6dHVCUTYxTGJHOTlwS2JWdDlFQVg5aUNoNE5EN2I4S2kvampmNDk1V1M3YVBGRDgrSFVRT3ZNL0ZTMytLdlZIeXlhKy9TQ01meVhIOThsd09pUHgrb2hvWE9RTnNvaXBZSi9ZVmNSZFVnWjF3V2l6RFJkR28vYXJkdFNQcFhXYzRubmlIY004eEVvUVZoS3labmhUY0VEQVVHRDZ0SVBPMmpOWlJ5USsxZkNaWTREL0lxcldOZ0UxWmJ2eDB1Y2FuUlhkM3lYeE1Fc0tXN05ucnlqWmZldGFGZ0NpMUwxUGdUSWpKcHRSVWJHdGVDY0hFQ3dwaWFMMU1menpQSWFrU2NIcUxLZnFSQjdWZnZnSU1YTDhsaGxLdmNtWGhwQmRQS2VqTlVHb3dZUEs5WWNTcGpRekFwSGhhWXc3K1lrYzJuYXRXOEp1U3kvZDVVN00vQjRMZTRFQ2I0SmhjV2pxcFVWY1Zqbzk4STNNeDUxallzeDJRZkdydkVLNDhkTm56MlhtTy9DYmNLYy9HKzd4UzJEUE5wOFpuMU9CeFJDT21jSjIzQW9WUk1KSW5iSWU4eTJCbHRKemViOWhMZ1J3VGlsR0lubWg3bThmM20xSHdWN1dSK1oyWndMUjNQbXp5andIVFArSTQzTDFIOElwSndiNnE2YU1zRjl6U3JQQXFQcU1kVDRzWnA5Y3hjWHc4VGp1QndnOEtNdHRmSXhpSmppeG5BdHN0TERXMkRVVm9PNEVPT3daT2FXOHkwRjJwWE02OXFESklDMGNhSFFoREh3cCs4ZzZMcWJ2aE5XYW0xM0p5OGE0aVBvN1RGMUVveVNwM2plQnRZWWxlcFJSYlV4UHJ6d3NjeGVQUlIxalp5a3QwY2I4RU1wR2hnWmRJU3dBa2I5UTFNZ0ltV1VSc0tselJaSXdKeDN2ckVwZFJhb3pPWk1iaUtteUlBK21oMERSalFvc1RGYkxpUFFySEdpZEhwZXJDVWJva0ZxSDA3UDZlSndrYnYrZnJoVVRNdzZSOFM0Tlp0NUkyRHArRnBUVWJpb1YyUmZwVFFYU2lLSDFXaXl1N2FlQkFDaGFSaUxid1lhL3YzcklDZ1BtZXhhN2oyd1hkK3B6U2lYNFpacjUrUk52c3pjYjFVWlFnQkZqSmhGWmtxNmFJODdYekVsTDk1RlFsdlpRVmhYWEMzem1jU0NlRmloWUc3WTFpV0ZoTkZRWjQ2SS9pNVhDbjVMbnZCRUpjUi9PWDRwZ0dBL1V2cTJaaEFGNGJWSzZEdHJ0cWhOdFRXMGRNTXFQY0M2MnMrM0diOStNaUFVeDZ2dFJyTWcxVTlxNFdtMjdueFVDWGd2aC9nV09qZk5xZ0grU3FYV0ZxSEJwT3MxUFZISWtSTFdvcEs1a3J5dDQ2Y3RnbE8zNVFOUElnQ2N5cUNtRWFmd0ZNMXhwK1hPWlczRGhzV0JZekVqVFp1Wm9MV2x4QnBBVmhJUmh2dG84UXRsazV6UWxqdy91ZUhlMVJJaXBBR2FJa0Y5aHpEUzhIbHlsTXRuNW1wUGl0SlJWVlB1Zmp2WjZ4RWdNREd0OE54WW9KWWhUZnIwMlRyUVNrU0hhUkdQT3FkallPOGJjanYwVDJSNGRrRGJyWjk0Qnp5ZkRLc0U1ZkJHQ2FMQjAvMHBKY1Bxc0tJVVVUTldCK1dVTUt4VnFBcVRoK0MrQ3lMYWYrR1ZSaXNiMU55a3dLN2JxaDJwQ09mc2tTUnc5cFRNbFlyVkNGT21MR0dQTFBEb241TzZYWWZxMjBQVC9TUGRHNkZ3YzU5UllMbWlHeXpxQ0drNC9nRUkyVk9EektZZk9iNmY0RDVjTVRQWDNZNmdZeEVFL21OSjhTWk5LOTV6ZUU4OVBFN1RBV0FEYm9DUnZHVFRFY0dKYXR1YmFpVHk3b1pGMlFCNUtISDdnNTVrclpUREY3eTRqZ2pnZElGc2lJSFBvM08yaktveXR0cUluZlE0VWxVVGxiN0VPTllZNE9RaUIzWFQrZUxXUnNvUUREM1hadEJYdXo4ZXpNQVl4a1k0U0FSM3lZbUVXV2tObGNySWIvZWV1Z1ZqVC9mUTdidGxaeENPcFlQWnJhQ3dadTY2TkpwSzF0bjIxaFN2Y1haM2tnSExPTXZrY1hucUJDTmVUdHgzZDBGZERMWlI3WmZYaVJkK1lpQ2ZrVEJtWGdPZnR2V1JlMGNyOUlWWTk1dlVKWHhBdzAvcm9EUlJzZkpYcysrR1djQUNHOStIb29NRTg0MDZZMWpDdk5ZZDFKU21WMG1IaVA3RHJMNmhGODNXdlBTcklMM3VORXk0SWZXT3dMTjNUTzVkUG1GdFRkVnUvQ01uMTZoeERoS2h3Ykw3cUxWVzMyUUVXeHV3cGtCKzNjT25iaStiREZ2TVVuSkZGQzRIUS85RENadFdGYW40NUZuQzc0NmU2cHdpZWczZ3MzRGNIcmxUQUorN2gwUGJVUDlxZ2hoUTZNeCtJYmhPVHR1Z2xqdnBJdmtrandsSWJkdmllNUVUVExPU3FIQnVJdGI4U2NMalp4bXhMTW5pbVkrTkF4MXlCbGk1NnZwRk9mVHhsaTZnZzd0UStxY3dGQUNUenkxa3F6Tkg0STlOSjRlcis5aGJQK3VpT2owSi9SZ1dpZjh0ZjZpcU5oQkxpOVkrdmJhNHprbjZtTFhrdlZPTnFDanhXMnFsQ3N6SXdXMXZTemFrS1RocUxYQ1lvTDNyMVl5QlBycDYvdFptSXR4UkdEZ2ExZm9TOWMxa1B3R01OMVd6MUtYYThnR0tvYnVqZ3NtZXQvOFpBTmtJN2gyZVJzN1VocEZiVjh6Sk5GUWFSNitwWDE0K04wN1FtRThXUkl3Skk2Wkc4QjdyS1l6d2NvMUlDZStobmpkQVJYSjdDSGltN2FDamhxeU9XQS8wUlhNWVdvYktrSmR2bGZYVGdVSHo5bkVPQ0VjeGNXcFhKUlpaTzZ3R1g5U3VOb3p4N3BBMXRPcFpIWmwvTFFnQWZUemhpWDJLMXNKcUgyZW1MWnFxRC84M2pxVXdnbUgyMDM0N2QzZlRjc08vNXA0czgrNlorOWRoM1A2czUyT2ZhWGJoYlJDcTlDdCs2QXZTWHNvczdJTkFSbUVHSVB0SGVMaVd5aTZNRWZ4bDkzd3o4VjJISElrOVVJeEU1MnFEOUxVZ3JNQmZQV3Nic0tWNmRMTS9EekJrejZnMXY0azBMcGVXeU9KWVc2UHd0UUdNT2JpQnl0YlBTL3dGZVQ0bmI3RU9hZGRzMDR4SHdUU3NYNHRBSUdwQVgydjZoY0U2eGI1WGlzenhXbWIxczBpUXc1VTNFSVd2MUhBRFFZZ2hQSDA5OEM2c2k2aTZaZjF2WmFNT1pxYUlrTGNXQk1VTGY2ZFZJM21pSFNCUnNvNDZVZmo0cmR6WW0wcFlDSkhGTlkyQjdXM2VraUFyb3dFVXM1eE9tU2pDUXNKazhVYmpkVVZ2YXlIQlp6cUQ4NlE3QkVJa0RHbVdPL2I4dHl3ZDJENEVraHRMY3FodzAyeTRkeFBrRTI1dUltNW5EVzlheTFwSEVTZk9yOStTSE5TZG9jNVdtdysyMzZRdHVLTHFnNStjeVcvOXhaSCtNZjFtMi92TWN1VEQ2YnoyTHdHUFlQeHZiWFg5N0Rjc0hlcHhCeVlOeDhiaW9SRTF3U3pTNi9nZ0J4UVBCcFREa1ZqdlhtNTBjNStvRWYwcHJCOVQ1TTdlS3NCaUJsekd6dUJIM3BDbE9YMFh0UjBiZFRFNFFjeXFkSC9RRU1LaERYRnpPemZGdmplM0xlTVVvWkZSc3dkaGdXS3FRenJVSElJM0Z6ZE56bFZrNlJFQVF6d25qTlhBQTllVkVrZ2xSbGJwaVlNdkdId1dpQTZrM2h3bTdxS0tPQW5PZG1jSjR6cDNLZjlpSXBVc3BYMnloZjRzUi91bXo5Q3Z5K2JIT0s4WE8vUFNWLzlvYjhTSEdyOC9tZ3gvVjk2cFdlS1MrSWlhOWN0YWpTaE4xR3dmcnJSbnJIa0ZDVUdJYjJ6M1VmNEFhRGV4aGQrR05zN01TeUlITUhQcDJSV0FsMDBZYXVSeE9ySDZNOEh4S2I3UE0yUDJCZXduTEFSc2NLZEtBMnE0VXIvcll3N0FxcmNyWkJRa2w2SU5WYS8zenRxT2o1ejZET25TbmIvRENHK0h0UncwZDRWMW1Pa2g1SDhhUFdERXAzZzMvVTVuV3gvL2VGaTA2bXV5WWx5a2VqR1djOWtiSlMrZktzaUhyOGs0bU50SDUvQWF6dzdoK1dCZ1NIUHliZTk5ZjlObDY0cDVXbDNUYS9ScE9EdDlmVDJTa3d3OHhxeXg1YW9ncis5WElYczJWVTh3ZmNlMEozY2c5dUVWYVprR2dWckNZTy9hZWNydXdPeThocGJldW1PbG9YSFlBVGFoeGZMdk5JL3FJYlR6M0FQMmxac0tnY3JSMFpXT3ZGVS94VGxpUVJKYk82dEdYendDcDVDS0hmdnkySS90Y09SS1FYelNPYmdZamNuSXJacHQ2ZjhMQVowZ1I2cFYyQmJTTTNxdHpuL3A3ZEh6dTNLTEpSVVc1SnRMRG82bHlVQytGenN6VUl1T1VYcmo2SHVlZkdVNHhNMlNob3gzUnQ3blV5M1krTEU2dGEyWUxITzNrZlhrUjlGZFpoZWlaS1NHNENldEVhU0JjaFVBc0lvWW5BZ0pvSUZMQWhWaFB1TFIwUE5iYUg4RjBtYzdhNDFkU0psSGUwZ3UzZ2ZrQXpMZDdKWWk0U0YvREZKbUhOQnN5L0hGakxUNDA1NE1DS016Yzd2OHZpWWsrZ3FiUnMzT2hTakI3dTFxa3E3b0RSd0NhUUtocUwvc3dUREJXVEp6dW9GZXNkWEd0NThmM3JQUGpabi9NNVBkR2Z0RFJTWEducUpIeVluOFpZeUd3MGlwcHdQUnZVOXh6RnZXNEIwV1hsZUk1TXVHOVBXSmphQmovMHJQV0IwYmxsTDBKdDJWdnkxajFxK2t6ckxsWEYxZHArSWFjNm1ZQ0pjRDVQZXlFU1d3ellQcXh5UVpad1h1MGNIMko0NFhmR1Y5TDVwQ3ZNOEVHUlg3TE9CRHJyZnBlY25ESE9KWGhaN0VnS2doVkY0cXN6bmRPYUhGRDd6cFJzeFcvWm5yL0pNdWEzclZHc1YweWFJVXFYVHpJUE5YZFVsYWZvOUtVQmh5QnJhTnZnNHFqV2lJS1dZZ2tkVHlxZ0cxVEFwZ2tvYXV0MWIvU2lzKzRxV1MzR2Y0MEJ3WjU5d2xHeDZDdVdMSWppRTFPTzRHUUMwZFlMWFRFOTBJTStFYWRydVBYMUtDL2pHK2dzMWI1R3pVeUVrNlBhOU52Vm1Ud2tndmFFRG1rWDlSRStJcmNWKzN3S1QvWGI1czFyVGJSYWlKZ3NoblVUTmlzcmFXYi95bUZTNnVjeDJFMUM2SGZ6MEFjYVNvK1Y2Zm1ObjRMbEpvVnNpYnJKS28rczk3dVk3cVE4RVl6K3N4YUN4QU9LcWpUUW1ZYU90NitUL3ZNMFlkWkxWb2hJRDcySm0wcGw3WnpYUW1JSGNQMFhSZlNGdURlT1hzVm9xT2hEWjlzNS9zbjRLMmUvbWZUdXcxR2gwbzlYdVZBQ3FpaEhVRHFidURuMktLdzVwbXVjRU83c0RtMUNvWjJkd1Y2NGJoY3NVTGx4Zk1QTEh1UEtwZnE0c2RsaGpzemY5MGlpQUVtcEFXRWpFTVA1eDFhUDFuSzRQNG9tWUREeUlsR2NlVHZDZFpPS2V5a2dHOUdwM0ZyNTRuRVZaKzVqSC9ZUGxwak9NTzdLOGNVY0M2V1cxVDEvTTRMNFZhQUFSdVY5Z01BbVJVd2NhRzE1WmoyN3YvVmxKUldINmVBc1pkU3Y5dTN3QmxRamMzOHlTZEswTTAraXlJYXZwaW9CemZmaStQT0kzYnVIVnRIa3JVWUV5d09zbktzUmNsbWF1NFdkQ1U5Y1IvK2kvakVuQVZpY3dmRS8xb2ROOVlCRE1jRjRzcmpIZ3oxWHQ3TERnSEx1cG9LdjVLUmxWWHY0VGZHVjVpcS85ZU5Ia1F2blVVVkNLVzIxNkdHZm5mMENnQml2S1VNVVl2Szd4SmRoNm1WUEx6eEJqT1FQUFJzZk53K0VuQkJmOTJzdzk2L0dDQ0lpY2gzWG5PQmZyaXNldkdLRlcxczRLYk5SK3p6eVB1eHkzWms3VXhFa0s1enBWNys3Z0RCNkpNVExPSDl1UDRtVDB2dVdIRjBPR0xMWm0wS2V3dE00MlF1SkhVelZGU0wzVlJ6YUVmUm9xZzJVTHB2RENjWjl2WTRueUxDaEtKYm83Ui9BckV3R29FZldSWU9sQ3FSNThrYnJmNHY3VUUvazdpamovWW8zaVRuSjJmTWZaVmtMUG1RSTNDODI1OGdQMWtPS2tuaHRhU1Yrbi9CbktScWhpdyttTWFMTDhkMGRVeXNIa0JNYjhtVHJwNmloaXJDaWFWWlJ5Y2tSVXJ4dFF0Um9TakRiNnBpeUZ6V3BRMzlkQzlYV3F2cDJhQ1ZnQzJtT3RoUXNLNm5QWUFrd01oMFVwbVN3Ukt5UlVRSjZQT0VKazU3UlN5OThhSEJxYndieGFQTlkyOU1XRXpxTXk2cW53aXlSc2FIdC9qTEY5RGFLOGlodXhsa1VRbllOeno2RlozWUFCWWtpeHpPOTd1ZTV1Q0lvMk1CVXArWWlseDJZR1ZvUzBoRzJMQWRydmp1Um9OVzkrWnhtZUJOcTI2TU5lSVFmbEFuQ2tuWGMxZnZpVWVaOTJLNkFzT1NVT08vbWRjWENRT3d0ays0QTQybE5OSjE4clYxUTQrc0g4SW45VlVWL1lFNlhzbUZGZ0ZQNG1OSTRGdDN2aG5lYzFFblJzVFhGRFJyL3UwMHZ1VmF2ZDM1aUwzZHQ1MHp0RFg0OFk0NjN5YS9hSFNPQUl1a1o5SG0xR3l2akEyQW0zN2lEZW9XeDJXckNvMDJGSUhYd1g2b2xlY2thOVBkdDNCamRnV3ErYUduTlFPVmVYWFluQjhsQSt6by9mMm1uOWhCOHlXV3FycVk4WFlHOURycUZHSExKTkRjdE9aeEpoZ0IvdmRob2V1amNuUXh4NHlKM2duQllrbSt0VkJNbWNVZlVCRzE3YVQ3MUdMbmlvYmRZcGI3K2tiSThSZW00Zm1tTVQvYVA5SHNsQWxVTjdrY0tVQmVkYmxwV0NQY2FUMFZiejBESUxZMStjQ1R6VmFoS0FFbGpPWWd5WEl3QTR5ZzVZb3hRWFdJa3VoNjBFdHRzR2ViZDJDQWUrVnNXVG02L2xHdEc0aUw5Q3h5TmE0M0R4WHVpdEtaQ2ZRV05MV0cyZE1TZHlxak9iQnJObXRjMTc5MUhvVXhVRFRzNEV3NHIyZ1RZYUVxSTRMYVVSTUdFRjdJOTFXNmlTMFhzNERFZ0s0QVA4ZjVQYlhVb2Q2WXVuL2FwN21pRzFrZ0s0SFlVOTJWOE9WejhXRkRwSTBqNTlLYW1QK3FpaVphaytCeUNFYWRNV010OGZDM2VKclcxbGZ3QW5GT3FFODFMZmJiSjdqMmFrMnNzV3Bjc25MY0RtWWkrTitDVmdkb1VLRGRQOHU0TTllaDBxUjVzMk4vM29RTkkwZlhRL3FIbTdBVS9FYTVoSzF2TUhxOStZTlFwdzdOcjBCeTBIZWU2eWp4dkZuUVJBbG1jS0h5em9Yc0oyTllCTnNXZHpiT2xlWkhQRUFkTnhpcS9vMkJXMXFHeG05TWRmZGFMbnp0TUZoMFA1TVE5SG9UcTlRelpRQkRxa0tncUpsbUJnUEdPQTFQOHhKNU9lZEJXd2hUTFJKLzBJY0sxNUNBUHVTamUwTWRPRHpMckVudDF4QTVPM0J4K3pYUHJBL0JZeEczR3g1a2ZFZnBXN2NJL0VtSGtYaXJVTHB4T3h0NWkweVFnWG5wc0RXZmFZMVk5OXhWdHZQK2pOOGxPbWRXcEU4S1hJS1BSS3hvQmxwTk9FeXpSVkh0VXRRdHl4YjNyUDZFVERPOWdXWFRjbk12UUtUWGJwM1BqOHNoRU9CRE5WbTNzTFRoMEJ5VU11eG0ybzNEdWl5U0x1NHJuakk0c1lNSFhQZGZtQWJVV1oxdHZvS09XZW02RnZxOEI4Zys1elpJalE3UG41NG1lZnp5M3pSWE1VTld4Y2Zid1kwVEhxVVpsOTc4UVM3SVlkSTc4LzhCUnBxRWp6Z3QyeDFVSC9JbWU3L08yTUlKN0IybjhKLzJsTXNDclpIWVpmMktIZEJ6Nmo3S0tVS1dqbEdYT0grWDNNenFyemYxdXA0dXAvN2VwMkZoKzlzQ2pvMVVRL2NFNm1CeW83RTVOQ2U1MTl1cmE2MFRGdmRJYlowUGJ4ZUhXajl1cytpS2RaaldKTzRGdXNITmxscXNnaEJhSGhPaFlqeUl5RHZ6QUFUTWowL2VPSStDdE9UajFQYXBZMldhVXlPWFhhbnpHVXo1Nnc1RmovUkhEU2w0N1VnaElJWUUwQURTV0RGMFdoZDRMcG9BUWpQR2c2L3BHK0lPT08yZkM1dGxOVis2SGp1dnljRnhxMVljMStKVklUNHdkUGcxTjlRY0Y4U3VyQkpLRjZoSkJGeW5ZQUhNMyt0c3U0Ri9RM2djYVNjR1FGWVVVNm90QXRJYUIvMGl3WU1DTWRhYXZGSUZSS1FTNmxNb0t2Q25CODh3MTkrNnd3T3BzTzdpVHg4V2RIWXZVN0xsUi9SMWNaUXZXRnI0dnJtQkJzdzhWRk9UcGdhRHprU2IrRzdFeXF1cmVQclRIRTc5RTdadVIxbnFWR25FanViU3pabXMzQmlKdkRTNGRHOWpwbUJvbVluNHVncFp2aHlBT3QySlR1VFZraEtMcG9QdVpXYjhRdFR5K3BYakNnVHcvelY2MzNsWTduc3IrT0lsbHlTVW1vOFU0Z294VHJLOXBoYW1Tb1plQ0gzeHpUTTc3dmpEU2lESHFuSHcrOW9yM1Z2cXFuTXV4M0Q2bTFXc0xQamFyaGRMY2lyN0t1VW5xeEYydGwyQnF3eUtCeGJvV0tPcHU0d29Mdml1MjU4K2ZsNVE1UWF3eE90M0NvZHFaTHJpbnd4M293c1FObFRqQUM3QlViUUFhQmVZa0JJdE42a2tPcmVaTjRsU3U5a05xdk9GY3E3WmlGbzFoUkxiSHpac1RqTWJsd1VUN3VJUnBuNDBjUzlDdVo2bEtFSnpNOGkyNmVDTU9pa3dNak5yUkhHM0g3R1N5WUxZUWthRitzWmEvVm1PdWp4MjFHRkQ2b1E0MHA4SXlnMlJJY3h1TW9mOWhYdHNOZERDdzhvN2luSVhTTGlTbGZDOFJvenJwc2J3NlpIdDQ5VHFrVzBQblZvSVlFVHNRa1l5ZUJnSmo1dWEvbGkybGdvT3RlUHBmRFlRU0lMbnc2blNhUWpTbWsrc3ZDbzE0TUsvczB1YlRCdjNUSUFkamlPWDBicGMvbnFQQjdOcGJmM2hQbmI3WTYvTWVGRks4RTB0ME5BaGpBTUpQdVpHK3hSa1p0bUpFYmVYZUlZQXJtUXFiRkZXUlFYUlRmVzJyN250Qlc3WE9lc2F6ZGdrS05ET3R4b2NrZlNCbnIrNDg4SE9VTE8rVXlaVTBLZHpGSHByY2Mvc3QyMWdQTkoxL0ozeEsrM3NxaEx0V0Y4ck51MEkzRmNmdHE2UXF1N3AzYk5yc0VjY2poMGFRa1lXZ3NZdldEalFWYWRmRE1RVU1CZDhzZkMzNEdwc3pqV0VFNHYvbU1ueTNZRUJuT0V6Zzg1T1crREtuYk9KR3h3R0dUamMvVDh0UFRjY3BJVXNOb3V2Qm1pb0FlVGttOE9LRGxoT2lFWTdwMkdDWmNxUUhZSmxZWDAzakZjc3pLUjdRSWZxNGpaVjRscmlpQ1NDYjBTSFI2YklkdElxdngvZ2lkUE5IWFZGUVI1NlFoVDE4NkIyWWYvUlc0ZXhhQ3YrcFhxMStYbmdjN3pudHN4a1RwUytJVmVuUVdEUytxdDhwYnpScWtRVFljakNPSStmZW84eEJFY1M5OUg4U1NoOHVOSEJic24yYkxxWFZLMnZxS09MMWhFZkhuYzMra0VhQUcyRUY5NjNXUElXaDhtMnJPTm14ME5yV1lIeWlDZ3V5ZmI2RTFkYmFNSlk1a1BpN3laSzNQdkp3V1ZmM3JleHpKcGFTYUUxQkcxN3V6YmlzR21IWFAxZVBPU1BPd3ptOG5hNnNkeHdqWTlVYXhNUjk4eERjbEJTeG9nQk1zMnlzbVo2K25RTWZ5NkRpVDBIa0w0VlAweHl5TmFPVURpT1E5akZZWUVTd1FLMVp2V1NSY1R1dXNmWnAwRStrQ3V0dUpXQll0ODJTS3MyVVplTi9yRHFFZHFwSXpveFozZThjTlZIMTM3ckV4bDA4TW1FNktWb3ZYanlxa0lrNU9SNWFUNDJ3OEd0b2lJTkF5dFFsT0RBTGF2L2VqZFB2SENteThlWmREQ3JtRCtSSnF3ZXVsZ3FGcFAyZjU0UVd4b1JicnRydkE2czREWks3eWdBbklIWTVoV1BhNm82dDgycGYwQXl4Q3lzenRIZXRPQytxVmJFZUtSaWFKNnMrNmhsWDBTV3NycDNwTk5Xc2FHMkhIY1l5a2tBb0xJRnhCYS92UzdUTWdQSElOQzFnT3RybStMZDFFR1grS0d5RDhERmhROHVDcFhFTWxyZTBObWhoT2FyQXpmd2tZUlRqMThzNEpQMzZIWUdwRjZLVy8zTUd5L3lOTXQ5ZjBOWjhuc2hCM2h6dXJ2MWd4QnFCNFV0WS9EL2NONWFMakc3cnl2dnN4RGw1OGFpTytEQnhGYzBzV2V2c2E4SEM4Z2oyYzBJei9jc2ZOSVhFL1ZCOHE3QUJVamRrbUxId2RrWDNCbVpEem5QbHRhc2ZSTGJTNWMzT3pFWlZ4VEdNaWhNUWtscGh1OEZjNjBObUxmOVdGTGh5QTFVTzh1QVZ3THZETVRJV0tsTTMrTGYrdVdkdFRUL296U1RBVHI4MHhUeFBkVjhNTmVEbWtTeWd5MWtDdzdld25rcUpHTFhRU0hRYkQzTUs2MG92dFZsUlNWNWg3ZkJOOS81bnBUdW5HemdWODFkU3R6RUdDdHBoV3QydS9aM0x1d0p5YXFtWThQNGlucTBSTjBiMWJkMVJQek9ZYXBkdDgyc0haSnArZ3lDWGR6SkhIVGhOM3hoWTN3MlQrQWkwMXVQZTk4YS9VOFBocituWlVKcHhtOXVpMzJQR2tvbG5jejdWTkhpRzZmYXVaQi9MQU02c1JTcjRWYmt3K0h2ZkxuRjhydVZKUTV4bmJyclRWdUhEa05RY2ljV2NyRFRrZVU2UnZ4SG9yS3ZyVTVqZkE4UEx3ZlNaZVowdC9DME82UStNT0pTZWxUdXgvZXlhbjJEU3c3emFJRGs1TW9UdzAwaUhzZzdmY3ErRnJSTVRDZDlHNFBDQVhpcU5NSUhFUlFlekMyVlpmL1lKdG1CMGphZ2xycTRZb3cyV2FucVovaE5POTJVRnVxbll1blMyMFN6bmVPa1hFcFpnOWJ5MStyaGliWjhndE1FQXpMbkoyTWhqcXpNbEJXaE14bmxTQ3pJQ21QNXhBckFNMkhqT2J3S2FQOFVwYlphMTJvdVY2Vlkrc3BtWnk5bm9WQTMyaS9sbmNJYVNrNGZUVThJbmFuSFlGeXR2K0gvc3Qvc2c2U0JIUWEweGhIZkxxamQ0V2JpOUJMOFl6ZzJJWDFqN21xQksxMlRaQlB0VXJEMkhLYWlRcVNvV2VlZ01FU3ZySVBnaTMyVXhNa3B4cFV6OUVMaVRXZjRqTTJWU1ErYTZrVnpuNFZDOEY2UUNpa3Rmdk1aQkdSYjlpVmlNSWZCMHVmQklBcHFZTjByYlJLVDEzbUZYanJsbFFwcG5ONkEzZ0tFNVpIWG9CZFFvcUhJbmZPVWhneG1yU0ZHM3RHaW1kTUY0THkwVXQ4RjFQeXJlT3RHQ2JLMGxCeVo0bi9PYjJ2cS9GeG9zcklVNjFIT1dDckI0NWkxeGNTOEtpNWFGNW1SYWpJN0FHSUtzb0VucUtQeGNDVjh1dG45YWlUWjEvOUN3WFdtdURLMGl1UStORnJualVxZUJUY2tMYVBTYVF4NGh5d0ppNG9PNktGUDNXNzNxVkp5TTNQenI0MmRCdHIvUlorRkdZTVJtbEpQVWdxQkhkRXRBeC9LR3phZzJOeVFBbHI4d09nL0FjYUxVQ2M2bGJzWUk4aFpERnp4OC9jTWhHN291UDIvUXhCUzd3U3MyZHpHZkZ3OFNtZkNQUjRDMzhOMTFWTmpqb0ZQRXdxSFoxWDBUZ1N1K2xPbDJUTFhPUUNsTzdZQjBJcksrSkMvb0l4MkdscDV3UmJOQ0NMTkc3Sng5RVRsZFFzRE1uZ25CY2I4eG1kNlVYZkpCNlJsbjFLZHd3cU9PWHF5L2RxZ2RudEljMzEzMzlnNVB0U2RGTENkWmdET3JTMGJSM0JVcWFsdlpna2FwaEpCNE5kcnlyWUtPWmYzbmI4TVorbmFYdHNZa3N0bHFOV1NMdHlwaEdlN2xwRUFFQkgrdTl2Q3ZGUEluNWlHVzcxRHllRkVVM1B2dERJd0xBRFgza3hlem55RUpwYW9aTExhNklybStubGdheUdrUFIxRHo4MSs4UzlzV1RDalJEemdxMUVHNktUcWdWVnErN2o4RkppSE9FS1U1bWtPRFV3alBya0JaTXAvTW9NV3RiWENQUFlmd1RJTWpmQzNydGhTOWVkanl0b3VGMnN6M3owVVVsVGtRR1hEVlZXUExLalNXc1pOZzVwdlFFNUQvSi9ZQmZNRkQyOENuOG1zR0tHa1FuTURDcWVQbGtrSTF1d3Rtb01JVVhYNkl4VU9VaFA4Y2lGUFhoMXVJdUFHa0RpcjA0QkUrRGFiLzhHc2lqNHg2RzBTaEVWSkNYVHpBelo1eTR3ZlZTQkd2cVEzQmR4MnpqdnRxUEYyc2I4WUV2QWRITHRiTWRjODFRZVNlbngyR3IvVERSUm9xeW1HSGRYUWVEellITUdlUGhyMjZHMGpVbDA3ZlZETDhta3h5S3kreTNUMFlKekM4TzRlWEpOS3liWTBLYVZ1cFYzOVNQV3I5MHlnVjNCN3ZFR0VSVys4MWthVzNzRGJOb05kalo1YytxeXl2QkRRMkhQYm1ROTY4eVZuMk1MK1NPaFZURUNkNjMyNWJmUFV3Vjdld0Erb3VVSHl3RUFTMFBYNExhTGF5MkRlSDFHanNiWVU5RVlJVVM0L09CNEJER1dreWZQTGs2SXY3b2ZVclpwYnlDSkRrSTFoQkpNbGFZM0cwalVCQXlqVkNFVUViZVBrZjRENGhvMWxvYjJOVzd3UUpSM0x3b3Z2c0lRWTVnMnczWlJBWUN3VE5QNzBRK1ZETzJKOWxEbC9BYis1VWR4YjkyODNFMG9PYzQrRS9CSHJxekJIc1Y2b1N0bk1OczlsSzdUVThsRzhheDNoU3hrR2tSQmdZM1FZaXQ3ZUdkYVQ4UGdjUGhqTjZMTDFTb2pBN013RmNhdCs0T3cvMlNXWGFpK25JMEgvWU40b0NqN1FVOUUwczIzZ2lqTTJaOGRiUHptdmpNU00xb1QyT3V2WllpQjdPM3lrSVUxVkVlUWhpQ3V1eWxjVEFQS1RzWTM1VkhnbzFKUEVZaHM3d1NrZzVLRXpDc2k3SmdWa0dFdWhMeEU4Lzc4aFhTYk9FUk9PTVA2VnltNzVrc2NxRjQ2S25NOThLVVgrVWJSTUN6SG9BVXdOU3E3MmpjTCtTam0rQmtVYnlZZ3Y3WXEraUVtTnp0U1Z3VEV6NytBcFhzK1kzQzlxWHNRUW9qTi9tWFFBR1VTcWFlR1VMTTBZaFpwVks2UU1jNGQ0Y3BpeFIySjB5R2E3Nmczdi85QW1FQUVQVUpwcFIvYU1BOERLUDdUVC9KS05Qd01LMlBFTjF2dzNIU2tDVmhCNVRYWk40QVlqd1NTV2JlckJVM0ZoeUlsenBUMmFHUjE4WFZITTY0WDYzVW1SbVpLc1dYMHFaaEsvQmtUaGRyUEtOUVZUd0tzQkpRVG93UERsQnJPZ2pST2dJVUo2OUtkOTlzSTI2Ym1xbEN3VEorTU94ZDMvVEd4Ti9OVjBnYmVNRnpBZDRVVUd6dmUrcWxlU2hMSW5henRjM3ZyQ0FqbWp2MlNRaXN6Q3Y5SGM1ajRqaGxPWWpSTndsdHYrY1FIZWczckdxNUJVcXdCbDR0ZGh0S1VuaTMrNE5aZDFPbUxHdndvd2dnR0VORXJsTTNjdk9MeVJRbDYwUFVOYm85VFJ0RDE5M2liVUZ6V1paUXNKM1Q0bmdjcEl2bFJRcll4aXNoUFlieUYyVENzQ3JJSEpua3R3b3EvaDNra3k2WFRYcnp0M2J1cE8rVnZ6ZGJ3UUNXWWhiRjQwam9HVFdTUC9rYTd6MlNwcXBrWlhJV2xBS2kzZUovMnNkU0xZL0hxV1VjUjBZeEZLN0ROQWR0VENkbVRhMkFVVkMvRGF0dlViNHBCSW9UU3AxMEdISVdwUXRqTVMyRkhuaDg2QXQ4dGtJTTBIL21kVVFYSzJKaG5tZWRzdkRza0tSR0tkVXNFb2xWVW9mWGtYblZGR2ZLbEpXSFBiRS9xdjNUTFlyQmt4TG1VZCtJQWp1Rk9QQUprMytBVUNodEpLOS9nUTlyL05ZR21IVHNFS083RU9OU1VXaU9EODZhL25CZUdLeGV1ZFE0QitOSzFtYVAxeXI3cDMwTHBLWjBzRVRtOURwcWhnNUxrcy9RYjN1U3lEQ3hIWEh4NzlRUDBsRjJJcUpjVy9qNE53N0VLTkNvOExKRDBydllETGdSdUhrbWU4aUtIK2NGTmh0cm55ZlMycDIrZEVMM1NRaXdlU2lRSHdHRkY1WVFOSW5TVG5WeVU4Sk43bDhnQk55elgrSXJFTzZFZFBkZjJZbk9qc1JBNmRuWkcvaHN5TlNxak9VblEvSjVodTQrbVo5SFNFZGRGWDJPZTc0aTkwVVdWOFJXLzFmUTNMSlNEVWU1Y0RBbzJaVHBVMTBTaFRqYi9HeEZUWFUxQnVtN1kydE9KeVZ2aTNzYmRPRDNNS1hrNXM1MHVMVGgxd1VYVXpKL0xMMWpOMzRMclJmVTBpcmNoUS9tMzBjNEpUOC9KY3F4RVdBUWFJOVplSThrRDRxcUtVVnpTSWowa1dGSzhLOU43QWNWVWkzeGxlZ0NuYjBCbC9VTzllN011eDU4TzNxdDVjQmZ0VXB1WXh0bnJWdXk0cXUzTmp2bW9xRXlENXRTcFRFZ2hEdWFnbGlEcVB5SUJHWmJrdExSbTBoSkdvSy9MNjZWTTM4Tk9EMmVyRTN2VHBBVjVicVI2Z3VIcUgzeTJXclpCQUQzVEJNa2ZYVXZ1MUYwSzNrK3U3UVZHeWFxaSt3MktOOWtYVEpoZFhVWXF1dG1NMzRGeCtESnUxbWlEQ0RRZW9JK21DWUNpYTZCRStsY0JiOHV6ZkpHa2pvdEt6eEVmcWZLRkFCUEVFaVYwRlcvYk4rSk5jM3V3SkpielhwRlJRaWlXb2RORTlEcVA2VGwxcDNnYTUxN2d0a3lRVHloZk16MkZicW1JbnQ4Rk4wVGd2ZlJlR1dSdm1CWlB3bnFLaldkWnpraWtUdDhLRkFlTHRndE5oc3pHSm5LWXRGUVpjeEpGd0RLL3dTWTl6Tkk5QWpiL2JheVpML2d6em9vVVRUVGJWTkp0TEF6bmhUVzdHZ04xUm1LQit0RjhCYmNhT3FITmJaUnhRdjZYWHBTcjdINEZrTUREdWVWelV6aEp6a29hNGdSMm13WHIremFTb0hRVFVYTlJ2Vm5ib0VpOWo4SEx5U2NJVFV5QWxjd0hBUkladkNqMk1TMEVqWGtZMkNRV2RaYkxYdVNKRUpNZEI2bUNsOVJ6b3lCdG9XUk1GNEsvUUZEWGQzeUNPdS9DRktkcFRmbEtCWXhVZkZUeTJSb0dQYy9PQTArV3FTcS9ja0IwdE81VTZPV1NYS3BJZjhuZXhXK2tsdXFMU0JWcGs3MnAyQmF1MURLZGlUQU81QXZJVEJNUmRYRmVCY2ZhODIvSkRyRGVvTUx4Y2VTdm16RnBuN044dUdwaVVNbXgvaWs3UWQ3YVhBL1p4U0h3NUttb3lqS3FRckFyUDBFQlY1U2tYT0FFK0lmZ0RORHF0cC83YnZUc21ZYW1GRjRkUTRhcHRka1d6V2VlcThIbUREVnR6UGhLV0VtQ200TWFsY2Fqcm1IZ2ZzUzF5WFdGaVNnS2xPeFhIN1IxazZGQy93QkhpK3l2eUloYk53Mk9jeGVObmIzSk1ueTdBWWhnUjJKaFpsTXJpUkh0dkFkcjFHa0VkN25SS2lyUHhxMjFudWxIUDRFTnBTTFdNT1ZTQUlWd3JFN2tBRitEWEl6RDQ4UnJtT3NkTklsVXdaZndGUmY2aUk5Wm1CYjdNNEpNUHJHTEpKQkJVR3Y2QW9aOXBSRmpRY05FSC8yNzZOOUg2MTJRZGM4MjA3bFJZZFVPMFJKN2dQanA4U2tncTFPWUJTcUVkaVR5ZnNScGVOanJaUHJ6c3p0YXNaQUp6QWJkN3VwaWRXZ0xkTVlMbU85VUtBVWR0WmpuS2toYktxeC80UnJHTXJLM0dwMXlGQ3dsSVBYTTk0RTJrYVM0eFZsaU9hb0llNDJncUY3dEdKUWw1UzJJS0tjM2pyS0w5dWYxSzI0blN6OGRUTDVKbFp1QUluVjZzbkVCM040ZE9TaEJQTjBaVi81WWpraHFzV1dkM2xyQXRRVEhHSnR1amovbkREb2J0ckh0MU90TFROdkFHNmVFTkJKV3lqVTBtWlRaZW5JYklDNDVBVlFHckhMRWZFRGJxOFhLck9YYS9OcW5mYlFUc2kwbVNuM1R0VURVNlR2RFA1QVlZVnUvMWh0YVY4Z25GaiszWGhxUituYmo4dm56dlZpOWNlT1JyRjBBVXdUSFZRRE1pVW40WmVRaWxHRWFPeW1TUTZseHE0cm9MbS9GZEhNS01naHNoZTNLYjlyVzhhT0k1SWVnMWM0cCs0Nk15NExaZHZnNWhhU0ROeEVQMVB2dEEyNWI1T0EvYUVBV2xGcVU2NmUwSXM2YnJCbnhUeWRacmoyWDQ0eVd6ZGd6eDBjQ21yTjk5aEVKUXBoQ2V6cUU1SXpDcFpoMXlNRTdYQTRmSGV1SzJ5OEpKckIweHYzcm54WHdxRzRzN2diL1dFVFZYdUxGaGtIRzYvaTRKREV2VSt6NHFvSnErVUxPSnE2ZVlzSlNvUW53QUhrejhRMndQeVpOVG9KTWtXYjZKdVFIT3hGTlgwRElyTWdGZ1NhaVZzai9CS2JRV3U2TEVneEVwZzJIZm0xRmVPVzZ6K3E5TitzZ0MzR1ExSjhkN1ZWelRORFo4L25CQjJYYno1Z21FVEZzSUllWlR5QXNzMXBuSVJrWm50WHMrWnBBMHRSN0pFYVNCSVprR1EydGhBRC9OaDVyRnNpZE9LREM5dHBUd3FDNEtXRUNieXNNc3JhcGh1QStqOHd0UERYUTRmSEU5YWhvZkRSUEQ1a2toRTVjczRqY1NSakpMWjBUZE1JMUtPTUtQQ0VzRHoyL2VpUXhGNVJubU9KTTRNT3M5K0d6QWViR3FUc2tzU1pxbDhGWHJaWFU2K1hMUnNlc3E0c0tldmFHY2dBK1FRbzFEWWRzbVVhMlJ6ZWs4ZGxaeVRqUklvZjlZNmg5SGtNeUtJczQ2VGl6NW5hdDFTV0NONUpxL0dJMmZkRGtodU0zYzV1c29SZElEUkVRTlNvWHFtWjZpeEMzTGlUNm0zN1p5dkc0a01GSW0wRG1SUDR6cnpSV2ZBT0VFU0dPcXdva3dwMW9DV3Z0ajY3L2JTTHF0cHcrZThlV25WNTNXdTl0ZVpIa210eUp6QVdONHl5eWJPZHgwaDNoSGhZTTB6ZVpMaHRXR0ZoSE9QRnI4NmZhL2g0NWxCeitQbFRUbzFYN1VyRCtGV2VTN3l2L2VraU1EbXFxWTcvU25zN0JYcUd2UXhkU0dYWVU4OHBHNUNoLzFpRWtwOXcycjZySERGbTEydjI2YkZNalBaODZVbGdaZjFoRWJtL01mc1JHTEpUTjRta2VSWEhDRFVteTMyV1dmQngwL2tBeDJmR2pVUTZucVRGQ1hjaU5zb2ZiUVRlTW1KNFhqVTh2WERlanRiZ0ltRFZnbllsRGdUb2hiZFdhY0VYbkpNNTV0OTI0OWUvKzl4SDl0bCt5aVRtUzcwMnRQUWYxYVlqaUpoZlp0aFNMUzJrODRmZXJWc3FINlVTUmIrM25vL3lHQTQ4TkowdjU0ZjBFaFdFd0ZTaVZwbzAyc1IzSmNzZldCRFdwQStKSklzOWNNaEtwU3U4V21DbW50WHJDT3l4eWR6dE1FV2JGSGVBdGtiMWV5d1BOeDV6RndvajFWKzBacEpDaDU4TWU5NW03cURwNVRTVDlxVmc1M2ZjS09WVTRWYmZZMnQyZVBRTC9KbWRQU0RQVnZEZTYvWURyMUVORjluM2tkam4zNEFDL2t5dEdzbDViSFVYUmY4eTNKeXgrcGF6MHZoZ21pSVpqSElSZE5OS1hudHVoaVNWNVUwa3BWRjQxeERMQjQ0R3VMQTV4QiszZWExb1ZVQTJMQjJySktKdDg5dHg1eHJlN3RudEpTaDZGZUI2bWt4UUExaHlpZGtxeFo3QmRiWldvNVVBUW9nRlpkdW0yQUQ5RzB6RFMxUFVvTTFTQUtVK3Y2T1oyekkwS0dUYm96elA1Q09qSFVyR2xTRWFSS1lWdHAybWhSYWFYN0FUOStVREpSN2x3TTRVYU4yZHd4MnNUMnZQaE5oNVF1ZzNUM2JsYzlseGsyaVJheERHM3RVREI5Ri80QUVmcll0THlkTllPd044Qm85U2x5eHFtRjNEZWQrcWxPbFNSKzhUd1hQWkVKcnlJc0IvQ2hJbEovQURyZTVpQWFFZ2UzSUhIRHEzQ1FkdGNqbm1teW5wOE5JNWk5bXFLdnl0WE9pcmRCYWlmNHdVUThyVWxYVDJWUFJxSnh6Wno1SURFd2FXMkxEZmthNDJtbEdibWxSbTBvaEt3RUttZDFuaFJDTzl0N3JwMi9QcGpxRDV4amVPam0ySjVETGdkaWcyL0lxNlNJVjBJeDM5SEZzTGtzK3BETHpORTEyTzJvRVdVZXkva1ZkeEZpdDNCdHY4cExRaFVyOWs0OGsrcjFmWmxBOWF0Q0haTysrSEUyTUdxQ2IwcG1uY3F3SFUxRGswL21aUlN1S3BFQkwzbFdjTkJuN0ZzdHJvNlh6NHpIcFFjQTQrM2xhemV0MThuQ2s5YnMyd1d4blViTnZCRFBkcUNJanFjbGNvZnBReTNUZVltWlhMYmtnUjlQVjg3QWJraGNpbjdhK1Q1NTBRQ0JoWXEzKzhLR29rbzNlTThITTIvencvVVFSQlBkN1RsRk9NWVJzWFBxWmxubUhEaWdnOGNRRnorNkh6anlzbUFseEhDc3U2V2lyTm84WlVJU1NsVWdZZXJYM3YxaVJ3QllUbU1BRkpuVXR2U1YrbElNMkFvandvSDdnbmF3aTBYUFl4QWxLZ2tTcG9rNjVicTNrUGpIbk9Oa3l4c09seWY5bnliVllVRVJDOXB0MGNEUjhZSUZSbmRxcDBzVWh6NnFnSDNRTjYyZUluUU4zWHNvMTRzdmdzSXQ1d3htSnBUTjNURFBFU2orV3BsSzc5OFM5bkVXWHRJdjBXSWlINkxrbHVISmJybnkzY2RiT2U4dExyQncrNXRiWlNTY1hnZG1Ub3pQeWNHV1doQytPTWJ0WXU4anRnTFplL0U3bDNvWjJhUzYweEJMTFF1QjN4OUlIdCtpSDZURitrR0Y5MkdQaGh0VGJoWXRnaWlFYllScktXa0RDTlgwSllVUU5ZZC9FMzZvb0tYbkFUT1MrUGl0cHgvOG40ZFloS1Nhamx2KzBuMWZaVlA2TWZzSFBvMlF4U0tFd0luTDloa3RnRTB1QjNoRGk1UEptaUh4YWNkU1pmQ1BydmRRdTFwclBWMWdibDUvek1FZWphcllYdU9qUmo5a3NrWmJFRHpObHFRVHU3VGxkb25zQ1FtMUdySmE5ZEt1ZHJOOGxSbmhrVStOd0srVXJjVTdzWGRFNmhWV0lBb2doU3lCYlNELzE0andpcjZ0YVNQL3dpZTVuR1pramhpaTNIekpETkppTXhNY1Z2a3BmWGYvRHViU3ZRVzVvU3dCdy91ZzR6SWRCZzBVY1NBS21peXZ5RkZSVlhvMlFmWlFwL25PR3pZZTNzRlJ4anR3b3oxc1FIaGFleVRVZ1UrZFllR3NnY2daaDVHZWw3VVYxRjJJYitKWWZsbHl0NnE1VEEzY2FXSktxQ2lNVW5wT0RvaWRoNnRRMEE1TlNsM3VPcnVkMmgyVE1sb3VjVWJGTVlhcitIVzVWUFk3amUwOFFFNUhsVllDeDF4Vi9TNHpPejNqZHZGVnBHUlNqb2xxUms4Z3hGaWJ0TC9KTXpTOWNvTUdMMWRZRVVmdDIydzF2Z2FjTVZVZkFrVW1DaUhEYmNBMytjcEhnTGZHNFRuZUxNMmZCV2hjbUZhczVOb2ZBZ0Y4bnN0MjRBKzJ0ckVWRWM0aGVLTW1FSHBNbXlrcmFvYWswSGhDSVcrVHBRVXZ5dWFhaE9mRFZBNmMyYnN6UlY5SXpiNXRYUDVydlBUSHZ5L05LWGNOenZTaWYwcElPcEZhQjlFbGpaeERrNXZCS0dXSGVxSU81TVhOd0hnUnZORG9XQ2lRZUNwZzh6ME1GMnVKR3haMFc4OThYWWdNQVk5SFVZMjZZNnRZZGJYTDV5S2dmb1I2NXdobVJuNGh4Y0VnNWZlU0gwdU9XUzE4a0VBa3JDTDdtS1JPcEZxc2tSTVJwYjJhaXZSaDlLT0ZXRHV6WVZvVGVUMnlxaUlQK0tIN2YyQXFsV09KVnpPMnBxR1ZrMHlQbm5RM3RkQU9jL2V5YW1kMDdpSDlJUkUvcE1mdFVOOXduKzdxcnBiSFdxQll0SWtydFVaUDJ6Q28ySjhhM2F6d3k4UmRtbUFQaDdPejROeHl0dVREK3FCc243OC9Gdkp2M3NrUGJyL0pVb1QrcGtVOTFGb2hlaFpxOVdzVzNrdTRGY1BjRDdmTGVyZURmTm1ZRU5pNXI1ZENOZHpmNmpZUFlPeHBGNUMzYlpMZEViY1FzdUFmWXZEWlpOUHlLVWtoemRQRGUzMGNFcFJEL3N6Y3p0NFlGWlBEWjA1Z01OUzZ6cFgrY2FXVU42dWtzZEUwY1FaQ1hmQWkxT3hYUlhJT2VXcTJwd2plaG02Q0VJcXdhWWRwTy95ZGNVclArQ3FwckpVOUVZQVNsdWxLZEFVL1Y5ZTQwbjdLV09uZWtCRDJDZDNmSVhZUjBQekkxNG5GQ3ROeUF3dWZzdDI4d2cvTUNIWThWMFQ1b1djeE9HcGZ4T1hHK1NSMXlkV2grRnhtNG9PaEpvTWFPWWx4dFFNSE03ZmRHTWVlakh3YytyZm9KQlhsLzBtbkVkZ2ovbzk4NWxzZFVSK3cyakVKNzRjWHQvdXdTbEhKUy9ZRkl5WWRva0JVUW80dzhObUtSZ1BJYXZ2UG1Eb2JvRnJUZ1NZQ2tiVDgvV2Z4TG9NTHdobjkwTGhGRUg5WUlNZGRUb1dBclRnYVJPRHFacjNlbUUrUmcyZXZFcmQzVkV2YzhjUUUrTGQwYmJSTVdzanJ1VlZPMGhUVmdlcHM3a2pmcXVmVnFDc1FyRDlSMWtGb1RTVzJZMndQeWtEanhZMUNsekRnWGhsejJ0TldTNXR3VFA3cWxrM0tjMWE4d0lkOVZiTFFUL3pqY2RxM3dxNDA4SHQ0L2N2aUVEMGpGQ2pqa0xwdjNZM1RTSnlxOXF0TFR6b201c2dtZTMra1ZUeTdqU3VoTG5pQXF3QmJPVXJxRmhjM2VHbGx5Q25jdU0ycE5kd2Yza3FTc1c4a0ZGSHpPN0d3eEJJcjRiVzZuZVFYL2xwa0NGaWlOQW8zb1JLSGFWMVFmRXNHSGpMUDZSYkhPUXRkUWppcXpINzdnUm5XRlVwcVNjdWdDeSs1QzVTYVJBWGV3UFlzZjd1emZCWGFsNlFPNGI4NDREK3lVcGI0RGhlUFRseHZhVUYyUGJQRTR2Z0NjaU5MUUhmQTNuc2pKUkNWSjF6dkJ1ZHR2Y2ZaTFYvMmpIV0s2Z0VIM1BhZjhCVk0xNHlWT2xQQzNqYjczeVRaRkIySDdya0lsaUg0U1FvdCtheUlsS2JHS3VhamJCdXhUOTMxaklpaHRlVUZseTVpd2k5bndxaVUyR1hJSkEzVXdmd1BZRjlqaFhnVjRpMFVDTzEwS1VQUndGQWJuTW1nVHZkcUpyTkFEUGpwUjVhMHdFSVNoYjErTmNZOE90NXlMYlIzQ2RTa3dOQ2lSVmVKNUFTQXgxOEg5TkpVR2NKRENZQjc2SWZ1MEovZVJqSENxUnNmcW9nWEMvNEZWdDZhcE5zd3BaQnVHaXg0eUF0ODBYMVF1a3MrZnMwVkNHVEtNaGtwbTJ1L2JDQmNSQXdpUFNkTFJJV0VYQnNER2c0ZVpGenI0U2lyZGJNdEt0US93V3FyU0dIRldlaVE5bk5RMzFQN0dvQlRNdHFrd0xWV25HNEVtMEx1QXhyK3ZTTlZqczVlVDJ1dFNwTjFDeGx0L1FuQ3IxQVBvS2s0Y1NmS1VGMEFDajg0dnQ3elpqL2pJZmFsaGlrUEN5SllDaDl4Uy9YR2ppNndFVjc2UHVkcXUvaU90UGV1ZjFreERNbm1CNlJjWVNtUnNyMk1aVElzd0JVTmNRT093dUR0WHdRWFNjazJHTlU1dVVadDdsQkJDUjhCMzBseUlEbGtBNVl4cDRLbEZQeFhVallnMHdCU1FRR3hQQlM3UktSZ3gyREg2ajlPNkpiQWNWS0JUcWt1NlIvWUllYndVV0VYK2JEY1NtbmJxNWRROVFrVFBJVlI4NG5kUlRIS0xIUXJSQXhmU3JpTXJYNlJwY1prQVZqVFNqbVVmRVUwQVBHVm9YV2FYNjRibTAvNDdTdXNRelQ3bHROTHBzckVYZ2JSdzdtb1FYOGJYdUJWWktMUis3R052S0JKYWRSOGNDZGZnTm1hRjRMNWlyc2pNckdySS9OQjZuNzMrZ25VZmpLMnJJMURMN3lPU2Y3ZDFIaFlJNHFjRGVVVkhlcXNONXo2TTBwNnJqaWYyS0RQMkdiSTU5d0xRY3JzMjNTSlJERk5OWXhpKzJMTmRwQ3FTektqK2pEQmFtR0JVandTMnBBNmErYmtkQjliNVoxUGVzaG0rMjhKRnpMWEtFYkk0T0dOUlZuRDFGcXMyd0hBWEcvSGNvbllzbmNWbE1UM1cwOGZzOWk0dVlXek5xZE8wZ2pzOGNsMHN4czhLWEprM1VYTGszWHhUeGQ2UGdZMjBLakpmQjhlSldRaTFuV0ZNOEc1cUkyVkF3RCtDK1kzUFFCOUIrNSs1RXdVdDRSYmdzYXNWTXBVY0JORW5WSERQdmQrVDBjYnlFdEJiQ01DU2pndnZmUUZFRFk4MUhDWFYxV0R4eGttTGxZTll0Umt3RnhsQ3BEZUUvWHIxQnBoZUFFWFVUY3U0NHBBZm5xb2dsVUs0S2h5ajc0WW5tZUppQmF2b3oyekxlcWlJdXlwcVAyZ0I3My9vNlJ1QUhBbnRXQjJTWU5BSGJWK1dQdXQrTEkyODlYTm5rbUFITGpMNWNURFNhY213MS90MzJ1Sm50WDhOWHloVFFGaFpZSGJvSWFXaUd5bHgweTlFNm43VjZQNGovTGEyN3VHNTQ3YnhMYVUzSU5rWlBvT3I4RW5aRnpBVzFZdnUrOEwzaW1DQWNYdDhDeWx5bG9RTlpSTXZsRFZCKzdXYSs5M3FLcTRlbkNNQ202Wkt5MDg1K0FXSVl5QXNpcDgwNXRadmdPNkdWbWlxNTRmelNTY1JFZmdHRWZ6RDNYK3B4d2JOallvZ0d4cDBuOS9FOFJCc2ZmUkNZUWhtbi9HSjZKKzRpalRyL3FwRUYrT0g5TFd5bTJkNXZrUjdWNCt2VngxVVBHa1JVV2taY3FHK3hsUkNJZ0Uvd0JxaUdaT3k1Q3hEUkx4ZEE3MDd2NkNydDN6MTYwaUpEV09Hc0QzVitIS0VHSi95c08yMmp1Y2t5OXp2RVk1NnZza28xbi9TTGlER09PSGRJeTRIS2p4L0FESnRqNmNLYXBXd2xkc2JoVDlWa2lTeVdiU2FKMXZNNnpOYXUwQWRabjVEU21oWkEzUER6L1lBdksrZ25FRnExTDVqWnlldEl0b29helVoaFEzelhTcWwyRXN5N3dGZzE4L1p3UXh2cGlmWkdUekJ2SXVsYWdtUDhpNWFNYzNmY1Q4Rmw0OE9hMVdTaTFHM0JwR0lpVEZUeVNhSjMwem1DWG1IWnBRQmYzTEdPcnpEZ0ZkTHNCb2tOdTR1aTZLY3BEaHFXdnFrVk5DWWJFalhmZW12N3ZWbXRjY25XVFJtOWhGTG9MM0UvcnIvbGNFbFRpQ1ZEcVNDRHZxZlJqcG9qbGZzcHNFYkRzTEhEcndiMStPcnJwRC9mVjBlRzNTQ1M2czBGUysrcWNkZ3VPT3lEUENoTGdRRVRqQTA3UDNDeS9KWm1wRG5pVTU3VENhVVZwbnJjMWFPQmVhcngwc2JmVWQ5cHNJNytIZGszMnNiYWFBUk5vbG0yaituMjFUNFV3SHRnRHN6U1JCOUtxN29aWEJRaU1rckMyU0JRUU13QXVPM0w3WVFIZGJ5bTBrVUxoQmxSa2FLZE9OdEM0bXZSL3RBd294U0dtRnNzdWprZnNERk94M24zenBBZmtGY1dRVzcyRWJYdjhNZEE3UGN3QjJteS9XL3l0cjlaVDVKdEZPWG40SGl0NjhwNzNONllJR3kzWUhjWXlsUVJQUTdVcmV1cDFPcERSUWZ3STV2M1BhZWRjNGlRNFViZ2QwVDRZdkR3VEc2Sk13M2dZSGJYSXB1MXdacmdnNXJ2RzIwL1hPWUlMQWZCYUxjS1VheWUzT3l2TGp0MHZYQkg2VlpNWThQN0U3K0hDNWcxclZPVC9VM0VBZ3NoUEdZZjRIeER5ejBuZnp5MlVnTGpFazhiNjhzTjkxRko3ckN2YXJTeDdBcFVBL3pJTWFlbWpoSjRpbEFLSkdzNUZhTDRSZFZQQytpU0FlNWFwbDR6Y0RZMVR3dVlzM0tCMnZSWXdseFhkNE1uT0trMDRxTE9LSC9nOVRkbSsvMmNqUnEyWDNYRmZzcHlXNXU2bTJmVVd2YnJJVzNRSVlKbC9RaHp5VUxsWVMyTUE5d2IzZWdWc2kxS1hvV0tOUWpDbnRFaGxhTU9PQ3k5OWFjalZ3S21qUVVOUDFuNFgzUDk2R1V6OXJ6YU5BZjNHM2hVU2hGYUhjOXpGNHZtZ0RsTVYreERhTjdqZk1kWHZXOElocnh2M00zMHBpK0YzeHZvV01kWlB3NVdDWTVtaSt4Y1JyNUErbHRvQXBPL1pydFRJaFR2U3RKWjB5bnlBcjBHVFNFcWovcVRpWkJXYTl0aTh3ME0wc2gxOGdjL09GVSs4YUQzQTRNajJMY2FoYzNPRWZ4bUwwbUp2WU05bktrVzRuMTVQYXRBR2VQRGs1YlkzNG1PRjZkdXNKTlNXWmlHNVZkKzR5eGdGdmFicjZOcUxEMEx0TVhxQzV1WGVVTUNsdGtRVUgxRjFKVGNUU1RzZlEwTjdpMkROVWxXTGI1eXNQQlQ4RTRqQ1IzZXRlZmJINnJnVi8zTUt1bHlMOURySzF2UkJKTjlsUGt1VCs3dkovWi9VbGwwSGcvWTZIWjd0MVJEdkllNjByVmNFaDNMdkIvSVBkaHBTRjloa1BEdFpKQmY3YWlvNjY0bFVVQVNpemtsZldXTXZUcmNDQVRncUxhTS9pc0JLcVRSa0xMUEdDeWpZUUZ0eWxSR3htT0JUZWJwZ2dYZ3ZheG5SeGpzR3pKZkhKS2JLUmVTUGZVdEV1UjFJNk04MURnM09ta2dtN0FVZm15cnVtSzltWEV0eUJvR1NiNmpxcW82ZGdRUTdzTjRTS3Bxd0FJR0JBcGNNNTQvTmdjU2h1dHRIVENuNTdnbmllYWVobmxMc2hkTk4rZy8zVmt1L2dVUWh5aUduRmxpVVFaV1NYelRlRlNvSy94UHNyZmhLeWl3WFVzbHNLUyt1bkwzWWZqV2dGdVNpT3RyYW5JN0lDbk9HV1hYays1VSsxVU9GbnR3QTI3SExrUmdoUmdYSFNsRFJpYmZ0N0haTFpoczZsMzJFM1RKYlNWNjFhNDJxeHZUVDdjV2VaWi9HdldUcDJNOEd0RjlFL0J5R2RTRjJQY2RQbVpub0ZwTzdMWVJlZnNuaTVVMmR1MFl1YnpiSEVQdFAyNUFsREFsZTVwVUptSkRsQUkyd2E5RFFEbW1vQlBablNFSUYvcjVVT29WOXBob2tVTzJUVHpsVGkrWHJyT2lOYmVKMHd2eXRkY0RadGRRNC9CRzVGVFZVRUtDZU51N2VQZnh1NFVicENnM21xWXE5VHJFUGVreGFycEd4NW1kcVFsYU5VaWFabGo5eURZSDhicy9HKytkMzl1UmxnTlJMWDd1UllmY2tYNHNUWDBiNWJuWFRHMkhOR1dpZ0liVEVEUTd5bFk1WnlOV0VwZmNuZUFML1l6bCtpOHk5VFdoQzg2dDZwRXJML2FOTWFEQ1ViWjYzZlVoVWZYNy9hOWdHVUVRcm8rYWdScWdudi85cXY0N2tDdFBMc2ZEWVBuTWRZYzZ5TUdBMllvTTZ5Y1Rlc0RGekxxOUtPRkhsT292c1hwclhuaityakpKdGF6RG5rYWFLQVZYdTVUczcveStwU3hDenREcVZYYTBPSUk3YS8wbGFPemp3WDAxZ3hZNkxDKzZnSWsyVThjbTA5ZTdBSWRxOG9lYU1xUEFOY2pId0Q5WmwydjI2MU1RNVJib2VZejI5SmJaUXBiQWgya21CcHlvWHQwS002Yk9OTWROSmU3Z2I3cmpycVFxNzF0Qmkza0JSSFIzb0VPT21MNmhpRzJ4UE8vSXl4eVBBQUlzK3lrUHg0M3laS2FjV0tjUy9LRUtOVXlDakt6QTlRMHhZUlFqZmd3S0Z1dHhKMHVPWmZ3VXZwY3prTHhPTlYxRDZSK250ZmtKOXFOc1JwckhsdGVuTmxDMCtUdnBDZXA4NmlQaW15VGpzSk0ydWlGMG9QbitaZDhtbTdjdllRQVNMdkFRUTZqT0VjYm13UGFBaDFqVHgxQ3JadUFZTG1Tck45T1JkOWVDMElZTXhNWDd1NnczZE15SjVBcUJVWitDU0I5THlJN2NiYUx2NTZyNUV6eHIvQXU3ZGp0OWdRMWJvZzZMeFREMWlXbXdwbHhnVUZRaEFWclVaS3NqRmc1RDNEejZJMndUT3RxcnA1QnFmbzE1Z281RUZPQTlyMVVBS0x5aUdBaEN2YVl5UjlWNHRXSnJjcHY3M2NRQ2NOUmQwZjQ4UmVpWGtLcW53elIxMzRraVh4aWpwMm90dU9sdWtKZlhEeEcwSlAxT04vMEdyWVllYWtKVlVsY0k3cWgrUW9VaXJoTHpSaGpXTG5ZczRSVHNrZy83L1VITWd0RmpJRE5MZmxxVWVDbXJZbVFkVkM3MTdqQjZrRkpUY3lUK1BvOFQzcWw1VW1IMmQrUXo0Q1JUQSthUEpvSWhQRVRSa3pVVHVDOE9PUmxxOUZlREtDaDVDcEVSRkhVckYzV2U5TmxlWHRMYWlhM0xQaW5CdE1ONWgvTXdYQ21tMFp2OUZFaHQ1Rlpua0lBRzE1elRCL0o1eUQzMXZtc0srL2dwUjliWEhWcWlMMEZVQnlYTnBxQm9OcUdXL0Z0T1Qzd2NtOFN1amVrc3psSmtSb1pMTDhrUkF1MGJBOFhDRVFhaDdUcVhGL0ZOUCtjLzdaWmhQQVMrckxIRWFzS2M4RGVsQVgyM1FWWHlQL2R1WmJsZ3hrWjd3ZXdXNmZXejhNV1NXbmowbUp5Y21GdXBMbzFobWR3T1lEMmd2RnpiRWtoZEtRS044T3ZkSDU0ZUwrNGZ2cU1tTHpHOHl0N09VNlM2RHd3OXN2enNHZDhQb2RETHlQN1FESHdSTk42S2FhQ2ZYVXhzdVd6RzdRdXpCSjRiL0IxYkc4aWF2bWtxWGRwN0JlZVBKWHVFQWI5U0dyM240NmZVZ3pGQVEzTHRjWTB3V3g3bEtrM3JsL0RJbngrS1FvbUlQeHZMeE9YcEU3L2QzYW9UMVd0cDJRY29naSsvTVZYNkUwOWpLc1diQ1hUQkpGV2NkOUo4T3FJcUUyMnVRM2tramZGUUo1SmhiNCtERXZWcy9QUEVzZjVuNERLY2R0TlFDY0xQQTAvbXlBbFFMQUVUUUdlZXdURmhHK2JHRlFkT2UrbG9mV3g5eXpNSDFoUWVXQStZb0hqZGpJN3FPNm1iMnhubFZ6SFFzQkc2R2VGbk1pV3NtVzlVQzl3TkNCS3dlSlhSamEwbFRCZk9OWU1GV3NsODE4ZVY0Q21ubzkzVHNudXJPQ0todCsxczUzN2RVeGdhbUx1b0dKeVQxRGlpSXN1cXRxdUpGV2E1amhTeVZpZlBVZHFoWWpBMnJPUjRaWkxKcy9RN1FuT1hPNE1LdmxrSmhsWHFPVzdnWkVJbVljQzBTNGdxYnBIcEl0bDAvbXo0UEk2RnREdE1vd1VadWtmZW1HVzFLcXlLYVd3b1FtZTl2VHJ6SkVMUmRNMDFMZlFwRWFuNEd6bVZRdXhmL080dlAyd0tYUjhjSy9YelB4bk9rV01QbnVtUHo4RmF2cGVVK3lRSW9JZ25LL05LS0wrKzRIZnBLZURwMG1JMXhqWEIremlBSDFKbWNYeGl2UUNGcUswRjNETTJ0SHFlS3Z1VWNzcXcvVGgxQVlZVmtmM1BWd2J6ZWFpRjFXNjc5VysxaC9OSHZOM245VEtjcFZZbGtqSE43SHZnaXpRUFQ2cTJ0dWFSVENITG9zaFB0RkVmYnJLOGxFSEZNRnhBVEs4eEZ2eVJUWGlxV0ZkbHVod2NlOEpIeUs0REhhNkVvQysvNWw1OHlJaVpUMjI5cjdhbnhSY08yOXpFVmtCZG1JbXdHSFBBUmtXYnhXMUVrSXA2UkJ4NnBrM1BWUGNidzcrdHBTNHNlK1htd2ZFSjlUUXFKUjdvSm55bTRjbU1HUXhtRjIyWlk0WUE4aWlQTFlNa1hDYzdLN2FNdjBZRENMd0U2eG04Q05ROW4rMGlzQVdqWGtsblV3Z0d2UGRWd3dVRFRveHdxeWo5amsxYXpuT0NmR0V4ZThPUGN2WmRXQU8xaU5zYlUvSVVPRHJYU2l5c3NWdzROSVBESUlTY1hqTE5jbEpjSjBjM1gzb2lXQjl1Ry95Vk9sZ2lvT2hmMHVqbHlRK3lXaHpDZzd2UXh2Rk5OS1JsNEpIZmtrL0NtY09ySkFtOWp0R1FwK25CbXA0UmlldVFnRUZEeFNvQTJJOEVZM0tlOVNRaWEvWE9VQWY5d1dTcmx4SVRTVDI4SUdKNFlVMnlVSkNsYldhT1RocWsxYnB4T3E1ZFdOd3VyTytFQk9qWXJwQW83bkJ6U2ZCcmgzdWlzTFBNWmt4K3dESUcrcFdERnVialIrQzlBODFJdkJiWmI1MjJndTdhSWhFcGI0Nm5GcWFZZlJFeUNObzBzem9IVE1KMi9lSmMzV1JOaVAzZGVaQ2xQNFYzU1gyK3d2SFJ3cXVBQnZjdlhsTHE0enU3Y2hyZXhmTGZHemNjVmV2cVlhZ1BvQ0oxNnRScmVDSW4vRVQrYzRPeGJuM2xmQWZxWndCUmFyQmwwUXRrdXBpRFptZWdZQWZIbmJCWUpnZzMxVTNRMVRXcjNBbW8zT1JhVEJmaGFNUnVBVzkzUEd1TU8wZy9VMTFKTzZLSFBKRlduZUhYMHRRbmNTcUZOSDBYZUZHSGpmdUM1djQ0Mnk4ZmFNODBybWVRN2NnWENWVlJFTWxxZlJtV3BNZ2E4cU5NRFhzOEIyRzRCWXhQNG9Kend2YW5PQ2ZVTXVjOHhodDBKOU96VG5Kd2thUm9nalBiTHhkb1RzQm9UMzdmZDJOemJHNTk0c3EzNTNoNXdhcHIyb0NuNTllTEFrTEdXeE5KWTk3aXNzNmw1ekZNM0FjK2R5MDRuZlZucG9ZMnMrbExuTEhySmdDZ2pJZlRzTjA5YUcyUXBZOFdtd0hFanJQcHFmZW9Qa0VQcStDSFhRR0srcWV5cmlyZFhGamZsSG1xNlNaUEJXMTR3bjJGU0ZlNlNMcWJNc3VpQm1GRk0vaUh6Y0d2VGJ0N1VuMm00ZUFpNVhGZzBHbmVLeSsyTHdZQlE4MjFweldDUlNadytZNkFNbkk4bXV4SmFLZkY4SkxLQ1p3dlBnQllObTIrWWRxaWZqbnVtNFZYM1l2V1N3bS9BWXFqUEo3c1VkRnJ4OWsyc3lzd0RNbU5qR0NpMVFpMmdaZDdoMWFNZ3lEbStqT01YY0pzNUxlUHFNK2hHNUtkeWxoVmJvdmJURS9MSHZ2dlI2QXRHa0hZaDBkR0hCQ2g1M1hyYkxCeUxHRXVnclN6Ri9OUHprMExIRkF0S0VWR0JYOTluaHAxR3c2OUo5RE50bzBaWHpSOHd4WHdNSDZxTnZXMDBtVis5RDVTRlc3OXI5Rm16YWtXY2llajdzY3Y0cmxiZTNGVStwN2FxSUg1aytiREdqZ3lJR3ZtUitNVWdoL3EyZDFxa1dZODdXSFNHVFdNOC9ZTlBOYjhUMGduQ2ZKalRVeWsvOFVRUFU0MkFRekMxSlhXR21oNG9hTTZMVmRCVzBjQm41QVI3bHpsWEM2Ukkyd2Q2WW8vUHU4YmdNZmpKbDYzRGcvb05RaGtlbDZFSnNVb2dCVnE4NHBjUndadTRyVFFIRjMyU3NXdVdlM2drYko3QTJTdzc5TXhnd0FWQXpFRjhlT0RQWWJtYWk5NkIyVEdleWVJc0pLYVZUNWt3UlRJUS9XYjhOWkNPTk5VWWdWVEhHaVJSMUVldG9WazRMUmxXQTBuZ1NLd1VneDRnR2tlZ3VpR1N5dlZVR2tpQ1FVdEFIVlhMOVEwNHloVHNENnpmOHBpdGhSQU9JcWxEN2pkYjRXSUpaQ1BrVDRYMnduV1ZMSGVQTjh4eEpjWGpQNHdCZ0JmTGVhTW9PcFdyZTRPMk9OOXBaZ1ZhL0tQVEpyUjN5NFp5L2FFSENmRkRQRUVTZ1kzOGpoV1ZiS013M05rWlhFUVFHbFNhSFcvOGlzZ3dvRkd4SmxjYmpwNDhHMGlXaFl0ei92aEJuenkyNlpOT1c5UUhnSndWSUFKSWxCbUdqL0tIcC9HTnlJSXBnWXN4V0tpMEp3V3czQ2l3cHhBSGh2OG9aYWZIOTR6dDlSNFFCTTFSRzZBUkFxVVR1bmhtTXZRUk5HZG1BelJLLzAxNitBZ1ovcVNWUFFpaGl5a21kdG05TFYwWGtNNVdoWXJudXFWTjVDbjdpSk1IdUdLWWk2eE9TbFJpY0VucSs5R0kvbjBCaWd0N3RENEJKQU53SFVlTjlpM2RzcnVDUVlDczFUcWwyci90WnhKRjFiSGxncno5bkJFTnhPckFRQkVMQTd1dlB5OEtTeTJTaHpTUW9JWHQwQjIwdE4xTlRoK0lNR082UWdmRmFWMGc3eWtFVCtPN21mSmlMeW0za0w5ZzZwTDdYbDVYaWNoN3ZuWTdXTm82RW9RUmdTcTVCa0M4WEhIeWRFYUxhTld0OWVET1cwRy9RSE5Uc3FNbno2NXd0WUpjU2d5N2JuZGRYczFGZkpGVFJ6NGU1b2ZNRGpKZ3A3eDh3UFpDd3RKd2wzcHk2L05BVkNoaXhGSFZPcDN1czNYMUYyY3BzZEtTOThvVFQrQWg5SHRNMk9NUXIwLzc1YlN6L01Fa2JXdE5YSm1YbDZ1M3BuMlo0RnU4ZEZFRkJCL1dmWWJQdUJyZWhkYm1OV0Zta1Vwc3RwS1h6N3UzNkc3ZTZYTmNiTjhTTFoyTTI2bVlsRXREL2hWMEJPSjBYN0dKRjQ4RDJ3NE1xdlFJMVlCdHkvSHBHYmhNSS9uN3dHT2x3c0llR2xWaHNKZGE5cU05SFlaVjlSLytka3pvRzB2KzloWWpUbEtxQVJwajVpZ1N5ZlkyNFN4TGZwWTFpSEJkMndzVGpZdmJlb3BBYnlTUFE3aDdtZ2ZHdXVOL3lsa3FoTnhoKzdabXFWQjNEaHlveFFMZ3lBU1ljWFdiYzl4a3FhMnk0bWRITXl0N0Ewdm5rTFBOTWVGSzlCT25sMktYSjNvcVpoZ0dqS29BcmdFemVsTkRvYUZNL0thNmZzOUhUa2xHVFhRYkF1ZEhjUnVyMmpXSkxwTXpNUlhDcEpjaDFBbnVSLzdmck9YRncwbzJsUDArRUw4bjBkTSt6QkZPcExCcDlmcDdpdlV6RlU1Snp2Q2Q2T200U2hzREVRRGh0akxiSnZOYUpGTGRYVTU4aklWY2lnaVIxMFl5S3g2QnY3Y210N3lWMFlrNy93TFZXTEo3ZXJWWlNCdC9ZaFJwSUtOcUw1ZzNNRjZINklHU1l2bUVwMzM4N1pRdWhzaWlXQ3Y0Y3NwTWwwOGNBTzI1TnRweFVoVytiQVpmVENUVmZ1aHJhSU1Cd3lES0JhcmluRlFYNWFVeUp2SEpIZnVVcXAwWTZnSjVOTUdubTc3R1BTVytZdXpwRGFjOGxGb2IyeVROWThtK2NQcXhNWGk1M3FzNzFhRkRTclVTU2JYWUo0TUFRSHhRM1ZnU3BONm5sV2I2bHY4VG9VcGNuYVpOVnhoaVV5M1RDWXc2ck42SHlqRWFaU0pNRGVkaVFtK3ROYkhQUlZvTDhlM3J0c1dtY0doZTRlVXVBVllIajNyL0VPM2xoSUZ0Y2pxWG93d0VoUWM4dW9HM1gxWFNtZk81andnditMaVBOYlBaV1pjMEhqdG9KSUN4Vld0dHFZU0lPRmdQdk9oTitaZGxrVVE3aWhSSFZ6L0t2UTIveTRKSU9TaCt1N1BoRVl0S3hFZXlUNC9XL0gzd21Na1R2eWZUSWh5YS96VFA0YXBjcFlYNFhBL2ZUbFRWTEV6WWd0WW01MHE3TG1tc2JPR1ZLRXM4Y2ZpMkM3ejJ3MEkxKzJENzlaWmVrbStLaHpoQ2w2bHRSNlAzTlkrajFGV3VxOXk3c21yOGRha0xjc2h0WlpYZDhLejRsWmcvN0F6dlcydnhrVnhNY1hUL0lCc3dwb1hUcFZPVzZTWHcrZkJJbWt6MWVSWHZEOFRBSXZsSXpJcVFweU83RldNekpaZi9JZlpSWjlJMGh1c3NXeVZ3dE5VOVBCRnBHajJuQUZjb1A1Vkg2aVVJZ1l5UkozTDFDMUtGQVhuN0dhYW1sazdVVHY5cmVqS1hRVFRnV0VPaUJlTHZzek1uMGtTeW1EVTFtOGZWRzNiWE5pMVhkMVdiVzdoQjFkMWJDcGNhQk5GaXlnOUNjM3RJVHNYWWwzcUlkTmhqTTR1dXBrOWpIK0NNaFF1ZzNVUnE2MG8rUy9XYlM4NjZzem1uWmcrTVdPSDRRZXBjbVNPVUFuWVIyaExzTnRDVlA2ZEtob1RjcnZKdzM1QlBONi9YcDduTFFlaEZ4MGpQTi9aOW5vVVgzS3M3aG1sbHV0aVJsZjl0aTRvaDltK2N2c3pJbTdDd1RZdVQxbjl6NGFDSWUzNWFjRFQ4RmNkVFNhNVp2L3psWi8xR2Z3RzRrdFMzTkdxcHlOcjJmcXBvdC9vYkUwajYvamhyR3dvLzVLV2xLM0VDNmtpd0hza0tBa3M1a0p3akNmcVB0M3c0Q3BGVEJrTjlvQS9iU0E2VzdPV2FTdzVOT1RSREw3V2V6MmQ4WVJDVnlCb0lXZjlETEVtVFpKL2RUOHVTVFBaRis3SHRhUXFyOEdqTHJpczdyTXFQS3E3djd4WHIySWpHYVNod25kYk05WWhzb3BHdnFMS2tJdjlWM1phTWVqYTl4SmlGOUdUU1NLQ3NoNkZwdTdFa1pPeTRtbHUvRkdSUytZQ0tJMWJFMWFqQXQ3RE9OUzJKTExhaXRhc3RFQmdpN3JWYVJ0c3BRTHhDZUJOUzlMcFBpSnJBYlRiK24zU2dIMG1Yd1J4aVZBMjVKRUUzb2YxeVRXdm5udXFYZ0FEUXZOTUVqWUY3bE44OTlMRFFxbGdFNzJZTkF5T0VVK1d0eWVJV3NpdWRUcVk2cDk2NVh3MXh4QVVKRUNWaHBDTHVNQWtjRm44ZHJwVWF3TTRHNEhkd1BhNG1lNjM2WTg2aDd0emU5eCtFT0lPaUNjdS9ualVFN2M2eEFzTTg2Mi91RHZLaHhYQ3BidmlWQkk2cHJ5NW4xYnBkdFJBdUY3UlZHTEpTUkZDU3NlYlZxQXVkTjhSTUxpcmxQa0kzV1RXbWQ1eVJaekdxd0R1Tk8yNVp2alFMYVh5Z1lleWJmajBQZzFZRWdXMjFzbzR0K2VvKzNRdDh2SGEvaGV0dzY2bktkZUp3ZWlENHkxZ3M0TVRxOVBzY0h2SGl3d2hVamMzZk5ibWJ3dW5nT2d1dnprbzE5dnMwVTF1SGNSMTltT25qU1N3U2tvaWlLeno1N0RyemJPODN5a2NvcGFSNDE2bmN6eEdrL3ZQVTdNVUpra1RLTDdxYWI1MVJlQ3Rac0xFdHc1OVVJVHZZU1hRUzl5eWZzNWY5eEZ0c25PRHpTbG8rV1ljSnQ5VUx4RGdkU1ZSbXpCL2UyYkRMVTg1RGVkRWlPb3NYTEY1TGF5WVdlVlIrcVUza08vT1lTdDRubVhxQkdXMklzNWVncFBMMEUyRlRURDZUN0tRTmhPSGNWOXM5SEMxWlBTL1d0RnJRak1sMGpjdDdsamkwbEZqd1BjUzZic25ielVHdVJrQkFGbWljUXltTjlNMVNSaTFqTkZSVk9VVmRzWjBhNE1KSVNISFc2MkNmM0tHanlpQW12cGZzM0lNYWhqWk52Z002Y20xQWsxS0dydHFLdm40TDg2N1MzUFl2Q3EyNFBvbGZNSXBZMkx1anloZW52VHBDL0k4TUtpRDEzUmUwQmNuR1RDWWU2SjYwMDB2Z21ZdTQyM0dTSnllTHVKZ3cwR3dZdGtsazc5MVEwcXpibkNIR0UyU1BseFVrdGJUbDNIVUg0YzF5ME5CY1paaDVoek9Gd2dzaXdtZUFvemF1TDNIUjR5WEkxRTZtdHI3R1gvbERGMXg0dkFwSU5FaXFWR2crbUYrdmkrR2huYVprMzkwYTdHTjRrVjVNSE9aOE1waE5VSTdoelJ2UkpHNUZBcGZHTThYa0RzTEc5Qldtc0pJRHZ6NEZXQTZUd3I3U3NOQjlkL3ZKak5RbDl3bDM5RWQzcXkxN2FhQkVVc3dXWVhQNi9pTVh5VFRGNWY4NmpXaTk3dCtDdzV0dHIwWjNyMzdaWGQ3emJlcGhTcEJiaGhMbHRoN3Q0d3oreHhDY2pqM3NKdXF4NWVmWjFQZDNxRWRKK3ljcVNvWTBPUVZNNHBUdndEZGYrQm5TWjJVZHVpNlUzVGt4OXdPamZpZWEvSUtBOGxPSERUVmF0ektCWW5JTWF3NWJUMGhMVmVvZER1a3pPUFBDdXJtVjhQNG9PUXZqczhZVGxxbTZkZytETExDRFgzUllpRlhod2hYRllTRnFCZ0VqN1FxRkFFeVh5ZXBmT1VyVmlIY1ROWW1mUDlTVTN1Yng0YlBWM0s3SzhrNTdJQytnaStWN2NJUTE5a0dBdEZ2aEs0Y1NZdDBYSzJWY3oyNDMyNFBoVEF0UGtQWS9VbkwvaERzWDRpY2prc3JtKy9ZK2RlTE8rK0hkSXpPSGpXUVBsTWVZUTBtdWFNQVBDK3pxK2VQd0Y4ODdCYkJKN2lnb1dBMHlZVysyQlF1cVVnZTdWMmRXS2ZWNnAzb0dadkowRGlEcjNKL3JuNHNJWnZpVHNiOXRBSUZBUklWYmxBejNESzFnR1Z3bWswZjRhU1hmaWhWUEIwemhZS3NQb1BtRDZQOWJvWXcraFVaM3FueWdDdmlRTlAxSWdMbnk3RDNZWVFaUWhTbVlta3FLVmhUV1RlQk16TXFMbENVcUlicUN5MnpGZDhYcSsrN3ZQWmVzTy9QY2N0emZ0MWxyZlBxS01IV2pndko3TTIwT2JnVDFyc2wrQUJEVkVVOWdweW9BMEVaUjZpTnZRSWdqT24wVmhZYURZdG5hTjVIZWhLQjZMUDk0MHJINitiMHZPSVBWYnVza25DYkFkNXlodDgyMHMwT2E5VUNwbG1YT2libm15T0N4bVBYam9xS1NYWDBsZWFWWGZ4bVh4S0loaGxIY3dzZU9DYmthRzBwY0lyOFZrOSs2VzNmdDNKM0ozcEhRV2JINXpVblJqSVVXTjY5VzMySVByQzQwZHpjejVOMzJPQ1paMExOM0FUWEVtUjIyZVJpQnpzaEsvNEVDNm1oZzFLdnVHMzlzUDFGZzJBZmN1UVBnd0w0VTRXcmNiVHNSdVIvamZneUdmTlc5eTZCbmJsV2JIMDFWUU15UC9NaDZrckRISUgxR01PM1lyLzl4WXhUQllkRWZXUUhZbG9CLytWUTBmWjFJVEd5eDd3UjY0cnV0VkxuTURNUTBPRlV6TXh0VUNra2l2emQxYUMzRUdCTllPQWVuMU1UbStQczVLSjJmU21wRkZUSHZWQ05wKzhwT203dDZQT0NaY2p1QVRCdWxwWkNaMGw2cTJ0NDJEaEF0VnN6aGVLbGR6eXRldkRJcGpUYm9Da3BEMDVuaW1XdHR3UUFYUzNhQldiSEppTUFvdXNnZHNBYXpGL1pBMWNOaGswaGxvaHVTVXBYalpGUXErWDlPZW1nSWVUQmowcHgyaFBDTVdQUGwxdkFOaGJ0YWp4NzBnd2xWdGVLUlEzVnlmVFdveWFoWWlFUE15MFR6Sk9hMVBFN0dmekl0ZjJ6bmxZdUg5akFFdGtPR2Z1SWp3Y0I4a3BPMmdZb28rVUVIa20wbW9mdGQraHBHWG5VZmJNRVZvYWZndUZ0YWhEU2xqdGVHOUNQVmtsMHRxaGxKTlBqcUR6S01XakVlR1cxQStMbytjcnJNSlNqWmFGUjRLYWZuTEhoL1pEb2kyK2p4eHNTdkxGdGRaazRsaEhvNlp1ZHh0azFLMmlUeGRXZHJBMDlndlhscW9yM3ZzY2FieWV1YVdDTXM4M3l6TWtTYjlsQXAwSWVkaHNDUU1HVjQ1V2JjZnNVSzJKalhwOERLNVNZYWFsbjQvTkNMekpHV0c5OFlLSFhLYnJoUjJjVkNwbmEzSGdwZ3FFci9TVzhETHdPWEhQVUhBRHRBQTBQMGV1Z1k3M1A5Skp1ZEo1dHBCaCt6UnVQU1B1d2dqYU1oWG92MUw0d2J2TCtlNDNFU1p6eFAwN0hqU0lCbWVsaUtWS3g2c0ROb2JtWVhydEszY1lZeUU2TlhtTnlkZEZ4eDdLZnNTYXIzU3RLMEFyRENxK3B2QjlhV0VnZ08rYlBCTlNpVnZ5RVQvZDJZaVczS1haWHkzNDM2Tzc0ZWRqazJPaVIyek5lM09GbmlrSTNpLzJaTHJKeXc4RVlGOFFoUG0vTndhZ0ZXK2lESXE3dEZGODlEMjRUUlNNZzNMTmFPaUZxL1V6d2Fiek1hVWhYcmRTaDh1YnNNQjhmcDZERTVyOG9YREFPNUtNajFSSHVoOU96UjV2c2h6NmV4KzVGYUhlYlZZaDNXVkVEZHF1RU84WmRZWUdMeWFFVmwzZHJ5TWhXNEpMYjgxcWo4NEx3ZTRIRWptUzdaZVA5R0g2Q2ZHeFQvTC9peHVtZExjeGJxVzA4Z0JRRkdPTzJYQTNaSE1ZMURId3NkdUxTcUdiV0V0NXIzSm40ckZqMlI3QXcya0w5M3VaN2c4dG1MRWNtSWk1YmNMaTRuekhzSFBoeG1oWnlXN2ZZSkp5NE84aThlUDJzeVZlUjM4QWNGZjJSQVNHc2NaQzlEYnJpWnJERjI0bXo3VlRjRks5d3VVMUx0WGZReUFva0dSU1hqN3JBdmZhRk1TSDBBR2l0cklmVWNKUTZaK1ltRFRSdnA0d3JmaUFYQ2sxRXdIaGlOc001SUxXbFlqV2pJSHFIa2lPVzdhb1puK3VHUUEzVEdUeGQrZUdOV1JyWDdLazh6WW05Tnh6TkdFMVFkMkxvRVcyMXRwdkw0TVJ2YlVWK1o5TC9ubWIxYUxFbW5GL1M4RWx6WXlXcWkxWHZYZjZoNUhvbmxRWVZib2FhdW4vVDFjV1ZKb3cwMk5td3Vlc2o5TWNoM2lMR3hGcTZadEFHQkRRT0Fva0RmbVJENTJNNmgwMG5POTZub2RJVlM1ZERmeDBodUsrVmI5cGdTd0lMUWgycjdPbVV3QUVYL0ZMbHFZc3Y0ekVNTjlJb3VEV0VkdEMzM0JSWU1aUytLVkoybE41eERJSlpreEhNTURnMktMZWRNNlJmeEdncy94T0pJWktadFJFWEI5aUZtNHZRR0t4eWV6bzlHSGlXM2JpOFVNMURhVHZTTUNHN1BtdWdyaCtYRjZjeUlRbHNEenF5cFg5SjF5bmNVZzRGMFRKQ3JqVStWSDJsTDNiWjdENjNBdHBTMU82NEZOb1dFRXFHczFwNVVuY0x5aFFkRlE5ZE12bXNnOE85ZlAvOWpUM2cwQ0ZybCtEVm1QK09waHVZLzRVVW9KMFR5OGx4SnpVOUlJUUJ1QnVrR1FIVGJzbnhoN1F4TzY2b2FsNW9xSStlUUtnT0hYcTVJQUwzeGU4RFhEdE05U01jc0h3TU1KZmxQdDZJc0dFUVUzbjMreDFoZ3lHQkJEcXRiRVlSTUFFU2lVSjJuNVBjRGh3RnhvRWU5aFJJOGd2ZlpOaXlva1ZsUWpqaFlCNzNvOG9zaURFSDJNQlc2QjVDbmNuOE10WFc5QkNRVkNBaVIvbVI0TnRFS2EvdnRyM1FnazBuVzhsRG5SeUE5MUZFZitiY3NiQ3BqRW11RnNKL1JCcHFwVnc1eWlNZjFXZlhicjVEU21lR0lSejg1dU5VWnhEcERsNTFRbEMzNGswbVYvZXJSUkJYTEYrWmF6bGRIVG1lODIrUlhLUmlrT2JxOC9aMFc3K0twb2QyNzFsQXRlK1Y4czhwUW14SE1ZWHQwK2pzei9GT08waEtjWXJpeHMwaGJhR0JlMVF4WkpBbmRDYmZOODNuZTg5TlpDQ0gzNy9nQUcvcVk5UTZCZlNab0hwdmJPSjdwdGtBS2VOM0p1KzdpcEExMHN4TzZzVko0d2dLa0VoQmU1czBpTExVdHB0cU1NRmtscGUwUDBiRGJoZy9OdGV0OTFzMlF4aStjSFNkR3lqcmpOTEp3a3M4cGgrd0RFdjBkMGRKUG41VTF4L0pUWXZTQjEzRzFCUGxHV1grL1Vnbm1nYmsvb01HTnJtVWx1TmU1ZnlCV3NsaSsrSVNYOHduV0ZuYlUyd3dsRHhPZXIvNkFJU3FYcDMweGVlempRcG14V3g4Sm93RXlpTEI4Tnl5MTd6WmI1Z0JZQTlDb3l3bmlGeGtrdVZ5UkRadlo1VVN3NUZ0K29ESGxiTTBSY0J1Mi9NcXFsQkZTcHE3ZGFMUVVJeG4zcjFNUXJSdnBiR3FrbHJzbElVVm0zci9WTTVKa3l4QTdjb01CMkRadWZ0dS9QUFgxU3BYSTF1dlVqcmtiUGFUR3RLZkNkYUJlRTU2RmFRTUxBVkI0b2Z4b1ExcHduYXB5elBNTmJGQmJvSTAwRnpuQTJtOTlxVGV1MDh2b3YvWlpQRFdpTnlOeXFpMjBpbUNKWGZtVm44bStPMWxrQkFrOEMrb0VzbmUzRjMrMzJOZ2l2dDM4MUVmMHM3bGNXaDV3OGMrY3FDeUlFNEU1bEN0bmpJL2x1cDdOdWVPUnFCMnRycnRNRWgzTGNMOVR4MFJ3RmNiMGUvOW0zRnlqU3NJRTkvWWE4NzNkbUVmYkoyZVhIaW5VdHdaZDd1eEN1VzhtYzFuMlJBbjNEZTExVWl2VG0vSnZYU2RCQzRaMWVrSkNGS3JCNW9MYU5mWlUra2ovUzg4eVhHcWI3N0ZncVlXNUJ2OXZsVmVGTjMrQ0NKQkc4S0pVSzEwVTlNUDZKVkM0VG9RaERPekRLVWpQcmxRbzBQZmRITXkweFRGZlJoVUFkM2VnMUtvdmVSd1IwZEprM1VPbEdVVFNpdWEzeFdmN1NWR0FpeU9aNnBzcUx5cExhcHlQbXBGU2ZQVmIxR20wNjRSY1hnUWxxYm9mUVNGOHE2ZlAvT0VGdHNDTllOU0hnSFI2QnlMV1NaSXM0eGE2UUFPN3h3OCtoTVhNYzVJU20zYUMrU0lUZlA4ZEhPaXlVaTY2aVFPelEyZVhUNXFsclJJdjQrWXhFdlRnV2t6bk5CNWYyVEZvbTdmWW1LUE1zQUhiWDBGTDVLcE1uVGF0bDRRc2l3WHpoVCswOVI1UzlmZHlaN3VRR0pTZ0t6VWpPVDRBcnVnajRSd1BEQkdWeTdSZmJwUmk3bjJ0ekdIazZTZk5VbmFLVEk4OFlETm4wZGVRVUEwRWNBbFBnVk1SVnYyN1RWcWZDZ0FXUGo2MC93ZFNGTStzZEt1UnhlNXQ2WkRGVXQ5V0pmanN1NFM1cWRWNUlDL2VrR09mN0NIZ1ZQOE1CZ3oraThTM2ZQQy8xN05hMFUvYzYzTUp3Qm9HYnRPVitwcklScUs3Q2s5d3lMVG1jelh1QnFOdDVFcVFyejNGVk9ITWJySFh1RlZ0K2JEcnJZbGJwN0VwZTBjdTNiSE96UG9iVDZ3UUI1WW1CNGpGSXBCUldJQ0s5YXpsdnhHaGNwU3Z5WithaUhSQTYwZGRENTlPdTI3VjlEWFh6V2FONnZXS0RHbE5VMzZ5ektpOEdxN1JJMXRiZkY2Q2xna05uU2dUZ0dsYWlWZlphTW9GUkNmU25aR2ZkR01tUXNEQ1ZVWHJ5TklnQk92ck81WDNMeC8yN1pOTjJ2Rkt5QVQxVlI4OFB5YzJyZ2lFVUJqR3JxVXk3WjFoZVgyanc0WEU2eDJQVmdhNWk0Uk51c1JHZkpBbURPa1pEbTAzVXpBaEhPNjZuZWExSkhwOVZDTDB3a2JheUVPWTdDQlc0Q01sbGZuMml0ZFV4T3NZNkhjTEppRnFLS1BDSUJvaFZ2dzdGMlFvTkJ6WFN4TlhJSEc0OXNlSVloak9ZOFJ1d3hjWHJ0eDVmUUJOVERFdG00UENrWmE0bEs0T3JBYnV3Y1VXNnp5RklqNHFnbFNsWnlUT01XdFlSYzR2U2NINTRvMnNDbG9ZZVJtZVV6cFR2NlhISlZZMTV1dFdNOHlaSkQ0Q2tuQTBmcGlCUmVSeGtSa3RIeXpDeVlSSVRLeUdhVHRGb1hmYVNWYjYzbHd6RHgxZzJKR3B4dXpIYWV0M2RUWjYrWWdJbWJMN2NSNGhnS1FpWDBiU2tUYVFtQVRuejNNSUZWZmNuYjJBbHZUZnE0VmpaODluMnkzU09zbEVPR0E3UWtvV3V3ak5pZC92empRRUh2akpNUDJBQTBQdFBlSW9PL0xtZ2s2UXhVRVZ3aWpQU210b0dyaHFrWVJTdUg4TytYWXJSY3laNmc3WDUrVWVScDdVdDI1dTBjOGIwanRLSnczcWc2bXh1U0loL1g3MDVIaVNlMitWcTlJK0t6clpWTFJMdVRIQzI2cHIySU1mMEVCRE1YNERsa3hlRE16K1R5ZjFVZ256NFRHMHdmUEt3dWVCbkI4VENQWFB5RjFuZmtBZWY0elNSYXY5aGhxMDRpamVvcjh0bU9nOG1GTGhjSFpnTEN4OW1MOHZ3RWdaNjZudDBGM1ZMaGhWS29OdSt2MFJtK2JLenhGSkREUlBPNW5DRFB4d0FrQ0czNTJvQ054VGk0eW5jYS9oWXVnMkZvUHF3eDl2RThlQjQzdVJNTVo5RUZvVjNrUEwyOTJId0ZsanVMUVF4bVE5ME5FNmJzcUw4ekQwb0xzTWV0bnZPdVVsdWRmditZUHRmcG5PQkljckRMbkdFMDl2UWh5dnhlUmI3VVBiNFZYZnpIMG5tdXRuaSsvYmZsSk1yS0pMOEhhbTZldk9QU0pUdTMyS2ZHWUhEMGdhWUVuQ2FjQkVHdlRSbW5mNDNOaEg5ZHJLU0lvQXgrUXJNVG1CSGVQQ3RyTU5rdDlXOHlsbFM5ZmMxTU81TXVweWgzYVU3amZNVldOR2t3dmdHR1VJSExWaFdEMGN4bzIrWEJwYnRFNURzajBQZVd4ZlVLa2x6d3h5OGdPMVdhSjZlaFJOc1VqN24zdk1Edlg4KzYxMDJtVUdxd1FYSU1ISjMrWWJ3UTc3M29mTXBTNk1MVHZRUll1SWd6RDRuWUJQaEpmL0dwV2x3bURXRlppbUJYNmdKY3oxQ3VKczJhNWZDaVRyZ0E5eU1teUlyV3R2NVpBOEF4SlJ1cERaeDY5ckxINXVSWll2bDdVcEl1N3pYUE1iL2drbmtkdjRqaFpkanBnSnNMVXNDNFBEa2VzaHhyMW4zZ04xQUx4QUtFTkNHdzdlNVozVlcyd2hjWXFFS3RGYVd6cXEwTWh6MHB0Qy9VcWphUkxGWXM2M2NWc0RsOXlDdUxQM1MvMUU1SkhqK2JmOVdGczZzcUVLQysxN1ZDZlNNOXkzN3pPRU4vQ3ZvUkFxYit3TjBGeVlrMXg4VXZkd3I1VGpVTS9wRzlMZ3NMMVkzTjExQkhwQ1dteXVuT0thWVJ0V2ZILzJoblNGdi9uR3RmZEVONUZQb21FNG9iRTFSOVlPK0d6QXUyR3NVcjNHdEVMeEpHT3NZOHlIMXc3WnlTaktPOHJZR3c5ZDlEQjlhKy9zbmZ3TzdiK25kV2J1MFZRaHVKclVnNC9YNkhQSFpjemd1TVZndmJkblBuMk5WcWltMDRwTUNpQXp5aXUwa3JldERSMWM4SXNIenNlclBFZ0ZaT3NLcTVZUFcyWkJGRkd6Z1VwS0NOQ3A1MERPZDdUZVFPb2dER3JRQU5INGp4UlZobmp2YWZ1dHI4b1I2MUhLaWh0bFNJOEkwUGZreGFVTXVrWVA5N1hVMWRNTnZmbXZpQ3ZEOFpOdjRvNnFZcTF1b3J5eEZRbFRDdnBTeXJrTG9MWDV1V2lzKzljMFJsYTBBNVMvd3EyaUV6STB3ZVA1YUFsVmZPMEZDcFJ2ZEdTdnJjN2VBQzlVQTQ5QVhyZjFzeU1SWFZ5RHJBUnI2SUNOdE5lSTdUMW9iaFhNZllRa1NsQXdMK1ZWcWZKMGV5Rk1mL3dOZ1dwWEFQajQ0SGMvNWNtMnIzT0Zqb29yUXJnU0RCdUlFUXVaMnVpajBtY2lXSVpiUWNPT25xcWhKUjFKTG5TTm5nN0FWY1Y5K2prc3ROcUhES0kxVmZ5ZXlqQXZ4QmhBMXBBVlpsWGhYcHV1N1FWSnROcElDci9aRTA3bXFUWGNuNk9BOEN5K0FhR0xkR3FyeWR4N0oyV1grdU9uS25JV3F6WUhIK2lYL25MUmFBOWZtZjhRTnRicDFLRTlEYXU1aHZUMlNPKzFJeXR0bTRuNk03c2FDbllhaTdtUG1PVUFtVEhIWmx6Zm5EUlFIck1oRFFFTFV6cVA4UkM2eWNvNU4xa29HZzMwdDBFN2tJZ2RlU1hsRDYxampNNnBHTld4a2tBWmlKaHA1MWduN1dtVEdnY0txV2M3T0Q0TGtkWW9xMk56MGFZTm90em50U0wzSGNRd0tMRzkyU0d6MW1GTm5TWHpBZGFjLzI4UzJDdXNSbjV5OVA5VlVGNDR1bm5zOTB1TE00UnFOSGpiWDQ4ZkdYTVRCdmpjWGwvd0hNYWxHQ25OWDF4QU5SVjZoWUdxTGxFRGpxQlBIMVloYURYbVB3M2lqUm9SaDlWS25yRWtXZ0Y1TEJRMGZiSzlHd2FQYXJnaDNNUnZFSkw2cWpuVzcrbG1MdHNqZEpPMEc4N0ZSNzFYd1NTVEVGOXNTWXhURG9WMk0wdC85cEZwWjIxQ3VYNHdER2d2ZDM1YWRodGZSNEcyb2VQSW0rN01iSEVQdHdHRUJMVzIrNFYveHN2TDNEVzR5c1ZlNUNHNHp1YjV5OG4wK2hnZTJ6WmpUMWZuS200MFdlUzZONCt5a0FnYWFjQVJkZmZub0hLaFRXZWwyMUNRZ0Jld24vNUNRR0EwSVlzbHEydlZhVXJ0OWd1N2JzZnhnVEI5YnpRbXJrazRIY29KZU1iRnVXN1Z6TTZJamNzSCtNcDhHcXVaVmRGZG9vMEtHOWplSkQ3Um4rV0hGcEVYaEtRb3pIaG1tZzRBZTFVcStiTGNhUGR6S1RKRkZOUTNxYnBQQUZWSWVzbEVPRnRiMHZ1MFJJRnJVRG9OOHVvUzhXc1JLNG02K2N5dWVoeXNyY3Q5VWZTZVFjMkQ1Nzh3Qzh6T2hHNis5ZkVHaDZ3UFQ4UlpJUlJrUGRBSkV6YWdZS250SmFibzRzMEFyK01hRTRkdzZxdzZVZEZIa0dZQkRDUmhmeWdVNUtmS1lrOFNXOVVnZllMbjk0QjMwTFZ0M0ZObHIvNk96MGFBd0o1eEQ4RGE0dFZvYWtQL0hqemZPYjN4UTVqSk1rc015bDNXdjdCS2Y0ek0yVkJLRXFoeUJFdkt4OC9yaGp3aGVKOFhtQTBYcEhtVy9Fckw3N2tyemhRN0VyaGExeWdmbUw4dWtFMm5GNTJVcEF1MHV0cUMrMTRBaXJrUlYvVlV6eElHLzA1ems3R1lIZXF2MVdDQlljQlk5VTFQQk9YVWJjTlVnT3FwZHdJUHBSMHNabDZFZ003K3hhRExMd0xBVVBwN0RodjluVmJpYWM5aTdoKzFjUUhaZytNVGYwYWhPdVVSdkZyTEV0R1BKUWV2UTBKSjJsaHg0U21PK0huazBxSGRVdlJaenU2eXNPc2JzWWFweUE2a1JyOTBYN2FROUxsdGZ3MVI2N1ZrclRwQTVrV0oyc0U4dXpuVkVEb2lpQzdkayt6WmJRTW5lRVpMTE16SmNyWGRmbDN6OEkwM3FKQU03bENuaTdISDdzWUR2ZlNMU1pFSTlrTmJWc21iSkZHOEh5SWFyK0d3d3FBVE80V28vaVJ5N01IN1VPYTY5cmxSMUxEUjFNRTBnc0NBSmpTcll1eWI5TzRPLzBQaTlFWEhtSHJhZGZhbEJpRDl5VlVESTljVEhDOHV4RHp6ajFtQlNRT1JjWG1HYWZJU0w2dVNUbWM0WjNONGZSQVIwaFFHR21hUVdzNGF6QkQzK0dtVmh2SFFRYzIyc3hCZHNmSk05U1JlRTFzZTcvVWUxY0tUTUl2SjZkMStlL2xsZGRkVzFiUDQ3RTlHNnY3Rzdtc2c4dGczUlJrSm9tRzRKNVR3K3U1K2ZyMnpJV2Z4bjVsYzl5ZmpqbVpOcGtVMUdTamhKSWxkbUdkbjVMRUFnd2Q3MVZzckZ4c3Jld2NxRnVkOVlROEVJVThlRjZ1bDk2YTl4dzNDditRKytWVzhwdzJ4TlNxQzNmMVB6bFd5Y281Qk9TM0ZxeU0vaWpyRkw1RFpMMk5BbHZhSzZIUCtqWWJHR2RsZDZPc1JORldKaVhGS0tjZE54QXpNeWRidkJzVmlvdnZTcjZBT0l6YndSdHZzM1h3V3hOUlZWcncvQTIwOS9WeG5URGRPNjdFREw1M2lnbWNZSlY3bW1odlJZMXRNTzkzendIUWU2ZDBqL0c3Z3FFNzU3Wi9OWHAzNit6bzcxd2dydTIzRXVnL1pFanZFU3NrUmd3S0RxSDdkMklwZlRVODFVWmhjWmw4TFIvdUVBMThWSWtJL1Q3SzlEd212UmJkNnM4M1dVZU41ZFhBYnZScUExYVd0TFZocnNHUXg5RXd3cU83V2ZCOUVmcDNUb1EvZklzcW03QXJrekI1NjZBOVpCODRwV0V0OXFnbzFVVXdLMG9mM1pXZ0lMT3lVc0lDeElCYlJXSllOTVRsRWYyVmQzTnNmVnZ0MWFJR3J5TUJBeDl1d3V3OEQydnRpMlQ1cnB3N0s4Tm0wOGhrUWxLbmgxelpJN0RTSU1RRVU1S283eTVSNmpYQXB0bitVTkNZV3pPZXNXMHdEc0dPdGowZWdwTm9Ha2w3S0pWdlluM3ZiTkJieHZCTEJBT3VYYzlwdGxxTnEvVTJyZnI5T09saWIwVnUzaGVQRVlmWHFCUkJsMGM3NWtPZ0E3M1BKOE1JZEc5N1A1VUp0bkRNbGJudUltSEE4YVVITUFHNzNEU2xjSnhHbEw5Q011YXJqV0ZWQ3lXKzh0UTlqeFRKUWZndE1YTCtjRCtoK210d3ljTjgvNFFvYUIrT2J5ZjJSbjhuTmpFRTVyT08xZjRCcWRpQkE2UE16dmRid1lJOUFRQVZwNEVHTTNCTVB4VFlNV0FwQVM3REhKZmZWcVg4eDUxd3ZFRXVSaHQ0c2h5OUxEQTQveFBOTW0wbzBxN2hBRmd0Z1lieE03OVZqK3NyOFRhSlMrMlNXMGpyZExhLzArZlgyS3gvK2M1dFFMREFzMDhJVHUzbTBGSDNZSXJiWDhUKzcvRmZUZ2xXd2lmNGxvaWpyaGw2TWljQk9xNmMzV1llYUcxc2VHblB4MmMwK2puS2VLU2FWMUdhKzcvMXNHOXBralpxbGFLUmxGWjZvc2pJMlhnTjJzNzcwbmxNSmN1RXBIQ1FHeG02YnRTeEVkbllNZ1RlVGcwRCtnUUZsMjdFRVA2VjNSeU5FYzhmell4ZmRyRkhyaUFoY1RtMm8zVUdHL3NMWWg3MWtlYlk0VDBzKzMxTnA2VUNzU0IrNU5JbFdDOUhyWHpCRFY2UlROK3dQSzNJVkU3RDR1dmNKSHBKaDl3eS9vRG1NcndwNzB5Um8xK3M3RmlveHRGb3l2Y05DVm9hTTBNbjI1ODB1dFI2Ni9xcENKMjByZEhsaU0vMzJaQmpyU0toUnZOb21rVFVVR3lNdndmUEJsck13RmdlM1FWeUcrQXhVZHdianFBMjBYZ3RMTGtNV2RCSmphNWtDdytYc3EzK05YVERJTTJCL0NrNlk0bzN5Q3laazNRd1B1ZVh4eCtGN3VsSVFhaFgwK0RZK2JPTkErQnpScGJvWlJIbnRiVTV3dzR6ckFwdlVSeVl4MmhkMEtxOVFCS3JsM2dxZ3M0Z3M0K01vRTdmaW1aR0pHMTFHbXFEYk5XUzdLeDRML1pVMTc3bDBJQWlDWFVjRllueGRYSnpPUVBzWEI1OEVweXY2WnhoMmlkQjJRczYxdUx6RVpFeUNIb1lYaU5Na291NVA3TGtJNGo1VXJMdElienoyZDJtSlVOeXlGcDJDY0RBamxnRFB4MmYyK2pXcnRsWDE5aC9QTnJxRlFZQjVPWENRYmZhU0psazF0S0QzUFJwYXM0Nm5iVlBOaFBDWVdzMkMwRC94Qm01UGc0T3lZeVNEb0JIaGZhS2hXYlNpa04zV1BGczBhNW9YN3BwTmN4cGNxRjlvMWZWeStlak1WOUVKK3RGeDJFVnpnK0RZTjdET3FGalhHQWZRVHFLMURNY2pkbzJTQkxvNGZRZVZsVSt6cFVHSXdhTE12WDZWYlcyenByd1VScXlwQUlRL1hjS2dFUzJ3bDFVY0VQWUswUEw5S3NacUhuTGxmRzhzdWhzU3gzc0d2MFZESnFGMmM0aG9xcG55TDNPWXI3d1hNWFVONFVaWDJqZlMveElTNCt0TXNidkpiRnFwWUY3MDdPeDlsZnZyVjQ3bEJHZE5qNzR3bHlVNXBJd2YxdExWME8xN2xKVk9lUE5INnltZzBTdDBPNVBMdm5JMjBQRnhSTVUxV1Boa2huMzI3WEdNL09oSVIvTmg1U2lPcjJiVkhhSis0RzZ6cWY5aVg3Vk1WOVhzdUh5a3R4amVxcnpVOE5kN3lHcnZPNWN3RmdlNVVCcVF5K2ZOSnBRU0NiZytyeFFuVlpobzdxSmowR2JoaFhIenB4cHFrMnJrdVZqTXhGUktnNWEwNXI5ME5jdkpEN1BmQzhGT1hCNDFlbGxjWHJxSHRRQ1kyV2FOdzBEQ1lUbTZnYUJjMmRHL3Zodlp1YmJzTTl2S3JNRlltajhqdlVQc1RzaWJvM3ZQdUppbFIxV1IvZ2VteDA3RWE0R3ZwRHVnalVhbWFmRjNEaXZSYmlqUWtNaENzYXpacFVsMDk4eEs4SWNYdlpZVWNEYWdITk9iNWJvWk5nalI5RjMzdmtmNU1YRXlzM1hsNmJrRFZmZlRUWk1FNTVrRmROTmt1VkZLRUlEdE1PSWZjY01wb1J2dWxTTDJ3Z3hzekVlUFBqMDNUVm0rNVlzTEp2M2FvUm9FUHVVUEZ3RFl3ZXNkS0JSZ0FXZnp6T01lbUJYWlBiVGdkMzdhdlpjM05KZjNKWGEzK0FpbkFiZjl6eVhvbmt0UkxGZU9hYlUxT3dnZlJ3ZjJnN09wOHlPSVZVVXFXWFI3RkNWaFBWZ1RQN2UzUXJtMUIzd3NxZGJBTzE5bHFyb3Z5dkxVYndGVkJnbHhZeXFObHlVNlFQT3VnTm5MUHg0dFA3bUtnN1RJVjZFd293MS85UHZFVjhTSlkwREZBWVJ3cnhMdDVYSUZmYVZQQ01GWFMyZDI5SkVGbWhhdFgyOFpjZjRGSWErTk1HRkQydGk1c0J0aGExYjFJVlBmN3ZDYkZ5c2hWcWpuTElRMmwzUERMVmI1WFpadkpyQ0h4TmVFODZuWkhCSTZFS2EvTEFPS1VkWkRCZnU0ZHJpM0lBQWo4a0ZBak9kZkdPWHNZZkxybk5qSE9SeGVzcnpDekplRUxnUGhOSURvSkZSSkJHWkRoRmdJT2VtVnNma2QycENhR0xJUThQT1NpMnczZFprazlLallIZytDbTVxMFRwZUIrZUorU3EwbTYya01JekgyTXVTLzhvZ1NtRmdTMUdSSHdqNHhabkJYNnZUdG92eXVDbEpRNXV5c1hHVGQxWFBJSHoyZ3dnK2s1TTRCTVVLYWc5ekNtUWdhYkU5TTFxVExZM0w0Ym9MY0U2K1JXWmpXbnZDR2Y5aFpmRmlZSmRDTTRGMGZUNlUvQitHbHRWaDE2NHhWTlZLRDFCUEpmRWM1SGRsY2xPNzFEZnM5aXJKSTRQdCtaNnpaU1NpY053L1dXRTlScWJ5Z2o3MXdqWnkzVldoOURVS1ZwMnZnQWt5K0FhRjR6NUNiWmxwbGZXRUhoUkhsQmFCWkQ5Q2lBYWFzVW00ZUVUa1U0QUdiYXhwYlBlc1FRV0RKT3VCdVN1S0Fuck1tUU9Ga1YycUVsNm1KYngrSHZpSzRXaTcyZDQ4L1ZJMFdHamlGM1FieEQyTVcydDlYc1BlTlFqbW50cnhhcVJEdStVb3ZGS0tWRm1rTHdpZy9McXhOanMvaVB0T21ZbkpSY0ZHREV2aUpYK2dYQXE4bVVCVFhReXhleDRrcHdGMFoyMlhxSFJQSUhXWWZvbXQ1aVBGUnE0aVlRMjZwOFBVeFJPM21XOU9ZaWY4R0lSQ296aFhOMjFRTDUwWHI1UkRadVFRTW9tMnhsS0R4OFFhYWd1NmoxUjZQbk1Vd0owZ2lydWZvTDk3SHhRZ1ZkTGU4d1lkamVmdjIveTN3d0NDU2o2Q0E1aEljalJZTnNvd1AxRXZUbTBQS21BZit4amtlTXVYcG9sVzRKUXNDcFVWRW1tZnJXSlc4OTB1cnh6YW5jYU1PbWxPRlJRZmNuSXlrUDh4NkpINlRqYWM0NUg5SEdJbHpWUzcybDlZUVRRSnZQcWltTERMUXZkYzdmNUQ4VFBrS2J4bWlsNGtwRGt0QnMwSyt1WHNMcWdaU29QbWsyUWp4ZUJXb2tqcnVDdXQ0WDVIUTRYVXlseHZ3WHN2R2k3TG4zZ3pib0g2NEJoQVVPbXNoZTNmc2VSWGdlM0t2K3dzcE4xN21zWVVDdlBXYUo5cVF6bSttN3MxdjNRMmwrUzlnMXcvL0lSSnozNkJyZ1JjenZONkpXNE8yeXAxcDJxQVZ3c1doNG43d3R3SlBWdnAvRjJsSlNNOWVzOFlxRFlXMFNGRU9mTmZJNjBoVDZoWVg4TnlvVUZOSjZoY1k1R3VGNDZPMGFBMnNieTF4VmtVbmRhUGFhUE9rUjVZb21RWXVCNzUxSXdZY2tEd3FQcmoyRDhYSndBM3AvbVBIR2luN1FJdHIwakFybUJuVXplQllNcS9NRlJwQVMyM0hJakZCeWdMQUF6dU1HNnlWTVcxeXJPZ216dk5zdi9oejMwM3hjb0MrTXNxa3dGWFB5a3ZIOVZUMVVBMGVXNk1LK1lkQmNvYXZndTJnYmMwZlRIZ0dmTmx3MC9MNXMyWVZmZGdaZExNT2dlZnk3cTFRbEVCeDdYN2JrMnFTenhNRjlrMWtDbzA1eStYNXc1eXFob0xoTHhvWjRuZnExRm13TlZUOVA4VkhBS3c2c1J4TlpYUklKVlk4czdMc0J4Mk80UW9SKzRkV1h0cmdiUzNlZnVxQ09zcE8wTlFZVUtHL1dsVXRtdU05WUp2VzhpOFhWTENYc0dUSk54OWl2VG5pYWl5UW5jemlSSmpxRS80aTI3bjFvdDh0aXV0VmJNNXhVODZFNzlmVFZabHQxUmFjNWRiakl2N0p5RU9xOENaVHlpaWJEdEJBWVQzOExucTZHQ2lPcDd6OHpxTmFCeWphYUhzaVp2dUZXb0tVMG9kTDJVVGY1Zk5TaEpSaU1BQlJTUENjcmxnK1N4WlFNR0h6NS9GbEJ5VlFqNFArR3RvckMySjFQUVV0NGwvY2FoQ2RNQTVuR3YxcUNEd2tLTUNCWU5qejN3WTlBbEU3TS84OUJUZDF1R3ZqUlM4WlJSNDN0NjZOWnQ1Q0t5dFc1T29pMkhOZHpPN0JCM1VCWmI3cG0ydGpUbGZMemM1TDE0M21LZlE2S1h3b3RYU25BWnhuYnJLeWVGc2JqNU1yQ3NkZHBSWEpHVjl4bndiL1V3T0JxU0QwZDJ3VHZna3o5V3VGZDRQcmJPcGluVDlJSFBjMjh2S3NLejhHS3M2cEF2SHhaMTdvSW1ld1hjT1NobU1uYVliaHJuZ1haYlk4bUlZa3g2ZVA3bGZ3Z215WmVvYlJVUVVlWkZuc2o0TysrK3plelRVVFd4TkZqWFhUczZmRm85NHBhT3BuQUJOUkxUK3RKKzFkQkltTXhUUW5LSVBwUnRQK1NIcDZDSXZOdDBSQkRzYWdtZFhJRFM1RHU1L2FNMU1ITVNLNDBtdklKTDVQb0RUZlAvU3hYOXp3Z29uajhyQWsyU3ZzWVdXMjZiZnp0VzVWL1FycXhySG1kcUJNSVlObi9FY0U4WjU3OTJTbG5mcFlQREFWelVxVDlxYmNZcXJXaGt3YURKbGp6enl2TUZ3eWJUSVJzVmRzbUdtS2ZKZW9xUmtVMWRzWDRSYlUrd2lsRjBRS01rRWdLdldZR2VGc0lTcjhGYnA2WkhiVTBTRXBITlMrVGIxOUV2MWNBWndpeC9Dc3p3L1BaMjhwVmd6SzQwSHYrb3NVbEJGTFhUT2Ira1ZXalZVUDM4UHZvZGdUUjg3REpmZzArOHJtd0JOMUxhak05TVpvRmZlZGp1djJ4cm5rZUtHbWExMTQ4WWpTSW1BWi9mRmdvZmEwb3pLM0xnaE1yRkl4VGxuNzJ5MEZMczNxV052VHdyZ2xIcFM5Ylp1RGlzZExuY2lYTW1zSFNtdjFmanhzdWxnc1VjVTdLUDVxVFBCZHJmSW41TmNldUlDcWxCMzNYTU1lVmNWYWVyVkFTZ3lZNHpIaVpMc2loVlVEQ0wxeTA0Z1htSi9XMVlJWVZnUUlIQkxpdUMrdnlRUkh3TkVwMGV3OHd6M0tPV3pBMmhrM2pzdTdDcTdrSXVRSjF3MEpvRVdqdnJmT2pEcDVyK0Z1eno4eUlQTWZ5eXlrV1RIR0pZY2JuM1M2eWtYek8remdVd0hQN0dBKzBMVWlKUlVsWWwwUUxKOXZmNlFYcU1DRi8wdU5GTFNNOFliTWxScUxrWS91QjkveEd5bEUzWjhJZGxtMUFoRlVuK0M0WDRmMmV2OGY2SXBjVGIwQ2l2bXlFVXBFVVNRdHRSVzNHdWN2a2pVMm56NXROa0ZVa0lhcktvaHpZVXJneFptZTdZTjNFUDlZWE1GbHY5SERKWldzRjFEK1Vzemo0SHM4T3M5WjdDNHlXWVFKdUtCZjRwL3hCMEdFNUU0UndUOFJNMktxYzdtYkgvc2VKNU1kQlMvek9XWGcvQ1N3R09zSi8zbVFXR3Y1cVphSktTdDZmdlBMdG9RNUZkMDJCNnpJdkszYVdDRW9ZelRYdzZqeGUxbzhzNFUrWEtsTFJzblYzaktaeVh0N1dFTHFEbWFnYzJyY3NGTldJVXZ2MVBoVSt6OVFKdzZDS3lmRGJ2bm5vTW1Xa3F5V2lhcVUrcWU1VkplckFOUnBlekdZd2RtRVAvNzJ1TWJkZkV2VGNSMGpwcCtuUXFzSWxFc05Ga1oxZStZUmx3MGg0TW93a1BVY2Q2aTdEV3B2cHhmUXRRY3AzNm5raStaNjltaVYwK09vektseDJ3K2F6eUZ6V1pxY1NKM3BRNnB2eUtJanFVN3JOM0VVbGdPbnZUOFZEbG5hZk85MWg5S0hET1AzSmE2cU8vNEZwcmttbFU0cVFHaDVNRGVkUGYwL2o3Z0lQTFNzQlFCVG1XMDJLZkUvYXBrMU1GaW9sKzAvS1BCZTNDdGwrMWxqWk1YVWtGaTJ6QklLTzBCUVpsdXp5REdvY2lhS3M5Z2FCUGkxUktyMzVZZlZZazlWT0Z5RnozVVd2Vis3dmt3U3J3dEx6M1p1emFZY3p0UTNUL1dlTW8wcjFlMFRMMG9relEwS2JGRzZ5Y202eHlrb1drSGJHL0Q3UDlxeXpsRXpaQ0RkUFdROWVyaXlNdXpzbHZVOXNRektZOUZjR2RoMVBOWkowOEZmZm5MN2VIYlc5RjBwM0MvZEVreDNZdUh1TXMrM002UllwbjJzOGFQUFFjZ2YvQkV6QkJIRWJUMWdPRCtDalNzVWU1em5tU2w0WCtxRGdMa3pxSytDTzhvUWpGVGw0UWpTeStCbnVVb0F5ZEFFbHRQQkovQ1JsQ1NYZkp1NlVOV1RLRHhsVlZoUGMwKzdXZ0h5RWZpc2hpcGNqbXFpeEJUQzVyWTV0M2I2RlpqK3hsL2ZHRkVWeFYrZFRlcWViNkE4bFVveGlnaXR5QWk2KzNzZ2pJUHNha0VDS1A5RDVkeTVmd0xkVEJiZ3hrazVzNHJCVVhRekNRSWl4SjcxdE9YVjc0ejU0Q096clUzaVloK2lxYllGOFZ0eStDYzlDOWJ4OGgxTEY0Yy9RYTNLN0J4YXczMU02aVFjbTY0N0xSUUpnUVMvejFuM2dBWGQ0OFNQa3dSaEhjVHJiMkVTN3lFWXRTZWhpekN4blFCRHp3aTdiSHp2bFpaVXFiOWFYbk5UeC94VVdyRW15K09yN1JmVWRMcjV5RUxyTkE3KzVKNW8wTFZUS0xtZTFRR1E1K24yM0MxS2ZZM3dDTS96VFlhQUdGV0ptOCtZU0hPaEFWVksxOW1EUTNCT1hCVWpTTHJDNFVVODRkRW1ZVTRXRm5WTE5oRFExL2dWUDl4YjJXa0ViS1NnM0xmNnpqTDFoRlhaSU5vclNZN1EvbUp2Mmh1R3AzQlFack1WazJPbFphSWRqTVdEMnRBY0F2TW44Qmt6MHZ2N2VJeG5iVEV4ZksxUjRYRTJVVm9UdTd1b1dvRTdpRjVvMXJydThnV3NyZVo5dVN0WndURGxnSTFRcWVuZEFOU3lPb05BMzF1SitpelZWOTVpZEQ2YTE5TCtYeExiTmw5dnVtWklGekVHb1U1MXdkRVZmdWJOYUw4aTJUUlJ0SDZwUEJra3lhdkJZZFdMQkFPSmdtVDZodjc4QTlrMmVBWkN6S3VPOVNqSFA1bGhNTElkZWNyN2x3WUhHcUMxS3dzaUxydlFPN0srWmdIVytEejZzRjY0bER2VlZIR0cvMkVYY0ZRRmQzRDM0RXpURDFSbGVCaUxlMnFzVlJueGZSLzhmS0lLNVVRVTlENGUreE1oYllsR3l0TDFBbmZmWXE2TFh6aWdGY3ZqalZ6S0h2QUxObVQwY0RabXdkcm00R0xEQzJ5eVphMlI5RTd6TW00M2Q3Tzk2Njl1V2RDZmhTWDZGSFh2bElvZEVvOStmOWM1NDdqVFFaNEtQc1d1VGppV20yUHFXRE9DT2RVK0VxS1V5Z01jVU9oOWNrT296K0xpWnZVZ1pxWXpLdHV5MnE2OGM2Q3MxeW01NjFIaGZWMHBXYkM2Rm42bnZOaVpTS0NCOUFCUHo4SHJiSHdwWldWR1hUNXh6UjhxUDNFNFBVZWF2UElVU0VSdVloNHg0a3BkTEttSGduL2pNWm90TXRiejRMWDgvVWJibDNaODFxMjZhb1NMaEFsVGJUckxHQUJ0UHBVckxHRkN1Yk5pMHFQc1hZQS8rWXZVUXFCRG9DU1FWU0JjWDVNeGNXeld0V013NEhnTkZadGMxSG1mZkdsU1BpaHgyNUJmQXpocVBJM1NieFd0bTRuQ21kZC8vQzZFTEI0MXI2VXNCK2YycVNFSDYwT2lTSFZ1eFhXZTMxZGRlZUR2eFkzQSttbmkzUzYwSTM2NmQxMTRsUW1aaytFS3Bra2FCTXhyL09ERzF5MVpMekU1YWF4ZVJleTI5cVBuVzNHWHEvaXlnWDFxbzVXbkVuNUJZcDJFdWdkM0UxU0NBZDVIK203S3VmSFRLdnkyZW1QVTdTR3RjYWVLc1NPTmxCc2h3ZHpTaFhUTDluUnpFcFMwRVRyZlZhY0l4RUc3TUhPV2FkY2RSai92SWlJSFJMeEJzTnlFejluekRqQzdqa3FPc3NudlVQeUVWRVFBNXR5eTBlcEE2T21VTTB1NlVrRHFBVHF6WVl6SHEwSmt2cXhjYVhiYUQ0cHRBUk1ubndjazg4ajY5NWJOUE0va0RQRkFKSXdjYWpubWtzTmVDS25iQ3d0MG5wck0vUGtaZStxb0cwekxoaW5zT0RYcUs3dVBxcS9LMXkzeUxJdVhCLzh2SUZZWTJlelJDZ1JoS3IzZy9ZQitoWHJqQlNwVjJTUGUxYmZjd0o0WnpzenphaWtoZFluVTVEVitxZlFqR1paYVdIY1grYkJmMVlQeVI2RkEzbStRUjF2N243TjYzNjlVNDVoMnlDSmhyay9Dbzd1WGdkcERpWE9DTzZnd0wwYmlLUk1mejZnWWxDaFVPQS9jZDM4T2ZIOWRzbkNITkV6aFR6ZDYyeGkrNWJKd1AvS0ErWStSTVNUR3pmR0ZjWmRtR1B6dXkwM055S3ByK0hndmdXbmFvRFdCQjVUck1zYk9HRUtxaVd1MVFZSi9ZcGlRUWNHTE1vekpQdVFxTzJMNWE3Y1FOWWQwTHVUbFBGM1BMaGFVNTBhMFhUV2ZVTDFCOHIwbjczRHpmT2NmUW9ZR25YczNYTEhEK1crcjI1M0ttLzE1MnVTeTFjZlF1MUs3a0M5RUZ4OTNUbkZweEhUdzZWNHQydmxENlZ2WHIvYjJqSCtXRytjRVQwUVVjeTg2dHNoZk5naXdsZ2xGQThTUU9HRXRyTklvbDc2VTFVVmRhWFFNMXJiemM4YVVpbFVKd3dhN0tZRCtCWDVablMrUm1CQ1UzQlVyTTVhQzdGcXFWbjZtYlNGVDAwU2U4NEkreG5zUGFwNWlYY1RuSWVEYTB3NzhLQUtMcDNOZzhTemkzeXFUdlZiVEc1ZU1FcG5OZHhOQnlqSk1iZmsweWEyOERaQ3BmV3BaUVlZN0JJRDNXN3ZDS20wZWV3ZXJkVG1Ha2EzR21wQnA2WHJxVGNCWVY3YVpVb1ZyMytzOXdKeGUvcXJHcldxVEJHZ0xtRFQ0anhpWTdvUWloWmV0VkJsVER5cmpQR0hCQUJFQWh5S1B5YmNRcXM3OHhjRm0wMGVnZ1BUMndLdENrME9QZFlFd1JLVE5ZdENRbG9UL2Zzaiswcm42RkpGTGFEZGFCV0Erck43Qk81RVZwWUpDN2lvdWxxcjRvWWNDK3hmdGRQQTdzcWxjWmpxMXY2cy9Zcm94ekgvK3QraTVOUVVvd2l3UlFjWU9zaXIwbmdlU3FVemEvcXk3ZWV3UjdSNDF2UGNpV3BZcEdsN1Z2V1ZoUmJqd3RmOVRzTGhvNTQzYWdTT0JRWjN1MFdXQXNoQ3YvclV6QWwvckl3VElteGhvVDRicktPQUtMVGhTdUd2a0ljMkdTY2NRMFZOd1pRSytXNFdHUlpGTC9UVE91UEVrRXFDdlYySkh4aGhtR3o4SmtyRUk4dEZSVlBqeHZjTW9Qc3I3aWZPWGZKc2tVYlB3bFpvMDA1RnBLOWxZWVdYTlUvTSt6b0V5bmNaT3VkN2ZubG1KMnp6cit0ejNwVWFqcUg5cS92SUpGYTVCb3I4N0poM1JybnBVRHFRaGZNRGtWZlA3TDlvNjExcG43YklvZXBYVDMwT1huZTVQNDRsT1p6Z2VCb3Y4Um1tYldPaTdQczN4MFpzZ05QU0VsVzU5Nk13MVFVMEtXTWhtSXo3SmNqc0NGTWpHWjYydGhBWTRJQWRlK1gvN093TEl4U2lsNERES084SVlUQlVOTm1NR0JFdXdhblFKQWJQb2RkSTlFTHA5NGtIVldYLzExem92MWZvT3pTa29yZFAyb0dLMTBaL29DOThJTjQvWi9rWk1aaWlpMXVGNXh3T21GMDdOR1ZxUkxNT1YvUTVJNFlaT0wwSGNuMXpmSWRFWDdSMTZqeERENGZwUEU4Q2t0UVVDNzB6UE45L3k0MzNmdXpHb1phQnFvSW9SWVpWa1JrTlJvNm9aUFhyL2hLTVRlMUptL2g3Q1lyVk5NWWY5bU0rcjl2T1BnVEc3ZjZvTU5FUVUxbzJ4YzR4R0dpWW1sVXZDRzJyU2dBYm1JMVF0U2I0OXpPdGZoU1N0NEJFbWFxbWo2ZStaY0ZsbjZjK1FFd01rYU5OdFlldnFVVURXYjA3aU5GeDhsTFpQS2lNYmhrMElqYm1YSTlQejlXZTJaN1ZUQzN0QlU2cVg2RU9IWmxZQnJsWlViNDZaUHAvRnZ5VzlXTmVkUGkwdlcxSEVPbi9VN0JSMWZ2NFhlcnZjWVg5akdONDk4TThKZ3MyTVIwYUtsY3ByanlCZm5lWmU1RWh1UmZnSC93ejZ1VnBMZFpwSUpRYVpNRkxnR1A3Y211aVgzUkFmTGFCRTJTdEFydWxZdkxjUDI3RWFRdWNWQ2NqckxZTzdhbGtiTVZVTEVGWW1qZUlDdFRRWnc1S2hCVkJQdlo4N1NVZjhoNjNmRHlZcHJ6K25OaERadk8wcmlLU2MvM2RLYlhOTmQzZG90MXdxdEFldjhyNmVDZnh5VDJHU0pmOGUySjNHL1BLYzFqRUhWRHpDT2E2b0h2UVBZZmdxelBseFkwTEMxODhPVEFvU0pDRmUzejN5amJreXp1SFZaQUtseTltODJWVWU3QXVuMlhydmUzYkFXY0dxNTBIZzBEeklpemZzSHNvYWYxZHpsYVJGNGdjbFA4b1BpeldNNkJQYXp4MTIvcjlIR2JpdFVoSnp0WldEVEpDcUZRWjhPWEVadUhXWlN0MU95aGNhdmg3bFZqb2tyblBqcVFURUczeTFSWW54QWlqeUF6dWdYN09hV1NDRXZNRTRSK0toVVdFTnNSc2FWK1N1ZVoyVHhqSGRVakJrYmdaVXNsNWFoV1JLdmtiVEFtMjhoMTVyUFBwTVEwZTdZVHFIN2JvcUNMVFRtb1dpWi9aYk9mUlF2azlDQ29ZM3BIM0NDRFFEakE5L0pwbWNML0h3MXI5cmpQQitNYmJkdlU2Y2s5T1k4TVZCM0FvNk1iSmRvNjhybkEzTWo2UHhzNXBUYkpYRjZPUmFMWDhrQ1FmU3FicEcycm9MUm9RNmF1eEdONHdjWnRlNmdEUFpyVHRXengvL1AxWVdpUG0yTWdVUlA4akQxaHc2S0hhR2pNc1JjTGVYOGV3ckFWZmxGS2F1azl5QXFuQ28vZU5Cem1Sc00xdUNqYkJlZVhmaUNyaHpLdjJ5SHRtR05aRlhzMGxCZU5pb1Q5Wkw3SzQ1a0tmOXlDNzQ0bnJ6U28wWk01czExVUt0S240aFhRZ0hUZlpNZmhLN1JiV1lqYVRVdk9LT3VwakxOdlhsWW1QTDFUNjNENjJJakh3bHUyOUdUWHVBUkxYak1jbU5TQjNoQXovOTRzMExicjdhcHNrYnBUdzlwTFl5TEFKOWNOcFJKY2Jwak9kUlJUaHMzd204VndadmZUNXkyRnJLVkNvZTdleWJZamVkc3krL3NJa2F2ZG55SVQwUTh4V0xnNTdvUUV5VDRvS2NnUEVLc3RXenJTMWpoNjdEUlplR2VmSjJiQTFkNHNmeUVmU2dOTHMxaWRTZ1Q3UWJSZDEveVY5WXVieWlhUDVNRVNaaW4xVlk3cTh5eFhQWjBtNGxXNTdMWWZxUnVRZmZId3YvTFBQVTRkd2JlTlIxbFNKYjRkVjh2YTI1ZHlqYmFJTmp2bHFWQys0bTVVaVpZT0VlWDVjMko1VjROd1VOS1RxUFJqU2tod2IwQWpaMjNHYmd6bk5yTVRDeE15K2Y4UnZ0TDU4TzNLdXZDNW0rUkNtU040UVVtZVN4bld0dkRLb0ViaVFxWHZsN1g0b09wb3UrNFFKRDFjekpKakI4SUNpQjRIUjRVaXM1b2d1NlozSmo1eDNMM2E0eGZNY1hwM1Jldm5hOWRXdGdKM2ozL3p4dlM0M1B6OUFYa3ZCYnJReno0RUloT1AzZXZtU2Iyb0o3RVYraXE0SStBbzIwQW1JdXdWbDlxSnl4a0tkYzg5dXZydjcvVTRKR0VmK0J2R25WOXlEelRjaFZMK3cyVE9GSzVlbkowRGZmZDMwUm1WV0tmY0pCR2NEelY0RnlSeGMyVFpubWZRS2FXRStpbkExR2ZEUFVyVEZWMllhUFN3b3V0UmF6dkdsMXg4ZHlVblNxT3hibTJYNXFEWk1LaGpVelhKb2RiTTNya2taR0ZQcVNURlY2RVhsUnlTdFZJbHR2R2xINytwY2ZDZ05JMTBvbXlyWFFNL3NhWnJpUWNKTEs3TXJEVnVlc0FFQjE4UXZnenArVlBxYkh0ZGZwY1JDZEE1cDUxa1hyVWFsN1dpSzAzTVphV1R3SitqYjc4RnZhWkR4VW5FVkJVNE96QVJxVzlwUi9SYjZ1eGlhL1NvZzRtSm82a0RleFdiVFV1TFcxV0NwcTQ2T2hyeTBiWTNORWVZTFlscDRrK0U0YWtUcmg5VjBlRG9zamJ6N2dUa3NYZnlFMHhrRCtiWjdIcTJvc0R1TzRocGdNZWwxTlQ5S1QyYlF2d05jVW55eDEwWkhyemdIOXNHSXZsSCtDTG02aXFUdUFEVStuRlNZa2docnNvVXZqNVgrUFhHQnZvOFUwYlFvNEpkTkhERk1RNWx4QmpWVTMzV01waXU1aDlGSnJzWTErU0pKRUlwejM0K0pxNCtMVnJGVUtEdjgxbkY4V1ByR1gxcjNjWU05eUVHbWx2NHBUN3NEcFFuVHFoc2dEOHlxWDJHRmRQTzZPWCtBS3paYk5BNTlwVlRVMzkrRlNLS0dNdENmaVRVYjJpdkovMFkwVzVBQ3poSFNFMjQxMGU0cVI1RXorZ0RxVEhZamJaZG1nc2c3ek9uclgzUlAxVDhhdEtUWnBCWXVoYkZSUXZUTGpzOHhZR2psRkg2WjdmNS9SeXlFbUk4akRkUXlCNzJNc2NaRTU5c0tuZWtVTVdqWnE4azd1ZlNSdlo2aHFVcGlBTk03ZlR4SmZWRHhvRG4vWVdtTWViZm03dTJUaEdWV0x0SjR6SjhHdk1nU0d4Tkt4NlRBQUdyaFBrSU40bC9SYkl6bzJZQ2ZMQ1J0TTBKU0Vob3JKSC83Z0V0eGxEeDdyRDNRSFZ1dzAvT2VCRVRrNnhKOUxWUUQzc3Vob3BQOTMzSURMZEJxckQ2TWJ2Vkk0bXJzb1gxZUtoOVpuR3F2anVJN0duTUR6Q2ViZFB2TVRIS3VocEVibVZUWmM1RDJoL3FVd09VM1A4eU81R2JkSk9zNWcxdUtadkRlUEcyZWNGTTZqZ0xDYUxJQU5zSG5yRis3QXdCakpXNEZ4NjhwZVorNVBVNUFqRloyaVE0NkVwd2dSTkpMams2bnp4d3VIVE1lR1FYYmxHU00xYXdITmJIcXpHQkpyaFRZejdIa3hQWExSSW1GMjlZMnM4c2pxWWlHbTRKWlh2UUR4QXpZdkJJODBMV0xIanlGQWw1SUNxOHRGQTUvM3VHRDJvMGhTUFA4azJHL0FsTnpFQXE0QXhHbGVzdC9KaUh2SWJNc09mR0w0NnZhUHBBa0gwb2w0TERCb1JKL0hNbUlhTU9kUmVPREFlaWZBeSt2QzBNWERtbnN5NGtvYXdEeStxYy9RdWtrY0t2VHd3MnMyUDBPVkxEU3N4REMrYk1TV1BsUnI2OWZNQmdSN3hRdzlocDZvMXZVRmRnQ0pSRXd4b09HbWFyc2dweUZiL3cwWGE4QTRQWCtoOEw0Zy9WbFJlZnVXSDc5Qm04UEFjWmhlZmRjSU1zNmRZeno2b1J3cXRqZWM2cW11WG9FMGMxcDVjNE91UnpXNVNwQTVVempnK0RhZG93bTBtbnpld041VWx5K1JHZjdHTFhYOTdRSlhsc2o1Qmp3cTNFc1pCR1huTU96OXFvYnU2SHZmRENHaEYzT2NyTHBtTFV2SXRyN0lTNXBhUkxrKzl3bko3MkVSamIvMVZvaFdPS1phdk5uR01YbkZTV1phK1VYc0JZWHBQWThZL2FsRE1aNkxDdVlPQWVnRXNFc1g1ZCtCb2FCbzBKUlEyTitCMXNxZHRob2RaZHc5bXZKKzRiOHZyY2dTYjRCNzgvS3pJZEl4LzBKZy9Qd3JSVlZvby91anhNb1RFcnp2eVVJVmo2b1d0UzlWS2JlZFpIbExsRnZXYXh2UzBoMmRreVZNM1BLRmQ5ellHeWpXbGxydGFtMTU2andkMmRnOEtDSk5OaHRQOWt6c3hITi9FY3NXbHZMYlpRUFlhWDIrSG8vWkFJS3N6aEJIbHh5dmFKRnpnWlkrNE1nVEZKa2JwODlrSG9FWjM5Y3N2OG1zZVJENmFFTVJQWTc5VTBjNk1OODgybWVubzdjYk8zMTM5TWExeWNNYnBpWWMrenhpazJOaXpEVU02QUpRNnVuRDliZ0lqMTIzMUM2YysxNzg3ZEJFc2NIanU1c2hhUVpIUEt3L1UwanV4dGFoZis2K2JJT2V1SmRCV2tZcG9kbFJTTm4wamEvUGNWLzBlK2pFZ01DNFZYRTVFM2VHRG91L0lHY3hYOEY2OEQyOVJBUEN5VEF2R05hNkV6SXNqaXZHa1JpU25SZldGOGZmT0c3QXhPL0tUaG9oWnE1ZW44bS9XUHpFT1ZOcEl5MFBhNDFYcXdidGtKeHhBdkk5OGpMMGQ3R0dLVWJ0UzB4THF3Zm1Ybzh4SFM5dUxncjNkaG9USk1zU2k5L1p4UmlEekhwaWRnK0tmeUJSMHNRbXFRb3FsQlp0bFZ4cHpOa0ZkUWhaSkl6bXlZQThQSmh5dW05ZzlRYTJ1TTBnRXJkNlBoeU9HUHYvUkFZTEpxS3FSRU05K2NKR1YvS2pNN01ncGJGV1pCQ0pUdWFsZm82Q2E5TVd2TG55Wkk5R1IzWkNJdTJLbDJoaUhhMDZRUFN0TnJrUTBSTVpXRjYvT3pSSlZIZ2pMa2g2RStxTzk2OUFyQ0p2OGhrNUlLdmhXY3BVcHF6M2pMRXBxblQ1UUNyZHVvUVNWNmpYSHpBc1FIN2gyOGZtL1FVcXVQckxoeVJwZUpMaXNUSlRlNDBwRG1KVUREWE5lSG1RRC93U3p4ditWNGNWQ3BFbyswWmpQMUo0SW8vekdwbVpiaXFXb1JkT210NnNCdGJmYWptZWVZeEd4NURwSzZxOGVQeDdqdWQ0dCtaYm4rWjF5b28yeXFBck5TRkNLakZkOVhIenB2Q25Zc2dYNUNrQk5RamluWndlNm9wbGdGWjJra0ZFc1pQOFBYdW9nZnRxYUtHYzc1R093MzM5UXZpT0NjZ2dnZXRmSEJmcnpzU2JiVk5qbTU5OHdPMHA4K2FkelhzOWMxNTh1OGszQ1ovcXhCanVReDN4ZC9MaVVQVFFlRFpyWm1BUXNjRUhoWE5BVHh6eDdFT2dQRHdGWlZ1dys2b0txdVBOb3AvTHNGMFh4NFBQTzlmdGRBSjhvOHNpeHMvZk1kRE95WVlub2FTUzVoeXd3SUIzZDZTS1ZtL0FzWDYyNnVwOTJ4WENlWUhUb0F3M1NDeHg3aFhjVXNyYjY4Z3dHMlliVmY5V3ZJZnFhTXpuQXBma0FZUkZ1YlJXRnNHV1R6RG1UUkZGNVkvWHZDVnVpWVZyRGNaTElRYS93eFlJS1J3eXB1dktxMWprcW9ZNTFTNlV6cnE4d2dMdTlFTnRzWG1iVXNIZ1AwYzJzMFVrWlg3cS9uSW1QT2RsUHVEczJwelVpdFRJZzdVcTZQY2Z3SHlYTCs5a0xKWTNoejhCeTlGSnBuV3VGbG14cDZoQ2d3amRrcnR5LzFDUmx6TWZYY1dJRFNzSWRYSTl3KzZkVGhrV2VmWDRITVJHTGs3NTJGWlJFckd4dkN0aE1wREZKYTQyYjE2YWVvc1A5TTUxNzkyZ2kxdFB4dTRjTXVucDljYWN1WGJqYnROeFpIS0MxOUREcU9CVm10QVVvSUFNcjJEeUZDN3Z4cVBaMVNMSUxPVU9TajU0ajJOcHhybitiUGJlTHo2RWFmZlRVQjJPcy82WStWN0JaTG5NcEtkK0M0VEFjVnVhNlJCaUZpQWJTV2RQTW55bnMwOWJTcEVCNDdUYTZkTytCTkVFMEhOell5eXowc1B2b0JyQ2w2cFhFajVjQ3V2dlJxcFpZT1BmZmpCT2o3cHF6dWd5cDUzTUJZYjZSb2xkdTdQaEt4SnQyRlkwYWVMM3hqTE1GVTR4QXVMSFJyMm1uUnl0SGJoTW1QQ2tsWFU3ajM2YlBLOHFKQTNXbSszVjIrdVdUVDlDUjVLL3VnWEYzMG1NNHVKTURLL1I3Y3dWNG5XbmtHUVFLMFJyN3hxaUEyTmF1bWlWcVFJMzAwZFdld01QbFpwdEdnRnpXRFVLa0RLcGh4Y21NdGFISVRDbXRNeDZjSlZDaFRVdW95ZDNGUml3TWRQYkhpTVJCRUg3TldkTmVuSkozQ2hpaXhLRFdIeG5xVjVUYXNnbmJNdVZVc2x6bHZseFdaeHhtS0ZyLzNQVE9Za1JEL2MyRnZDT1FoWElYVG0xT2VDbjZnT0NvR0h4NGxDVlFuc01SV1BDZ05DMytuSFZLTFRmSVhYTHEzMEF1ZE1LTUx2VTNoRU1EaHA4czNBaHdUWHlNUElyWG9sWTFac2g4ZnJVc1Jrd0J2bUZQTFdsMUxGaFVJblVsU2hxdllZK1drdUZMVjc2VVd1cW8rK0pkbzF3VlUrZ0s2QU03QmtNVzMzcitqT1NaUGE0eGo0MDdZK2dnZ2xTNmEyd2REWERKQzhmNU9DN29DakJsa3lPZlFqUEVqR2JPRXVubnIxQ2ZBT3l5bDJHSkR5RlU3cFpFbHdWVTAzKzc1eEpYVlZTYkhPVldIQmtSd0cwZWZGZXZYUTRtcnA3RC9UVlgzbGRnaFB6R2lLZ29LbjhiS04rWm1WdFFtYm1TaTlMb3gzRTRxNm5YUlE5bkhLR2J5WklQdXZBdFJmSjU4eHB0SUxrZmJvYXZnSDI2YVhURnZySmRMYkVTZ1czdHBlYzA5OWI0NjdlYjRLUk1mOG9OS3JUT2Vid05WbHExKzBycG1RVWI4ckRKbnVhTU1QSldDTy8xTWN5bUpFOUJYUEdQNHRiMHhoeEE1MTFycHdwNDFnMHVibGx0c1FucHBlU1YxbVdBekFiSm9Fd1dCamU2TnRlOFcrTVlldWczdVM4eGNnVlQrUVpSY1NzblRkZXpXZFptN1FYUXVLdjc5RVBacHdpRUNscGM3WG9xT0JTY0pielVTVnE3Y2NRek94aUFqR2RFd1gxNzhybkJuY0tlendRMldnSjR3VGU3cVUvVDFGQ21ZWEtFa2tyK2pldUQvZ255THpWeXh3V1cydDB6K2hTZ1N6MExRYTdBVjRyd2l0Njl4NStXTjdteVNCY0xqYmJSKzZTK0JIZTZWK3ZjYkRVWVpiQUlGWXZ5S1VFUDJPTVR4T2VTajRKZ0UyMy9RQ1N2eDhVMXlKQmZEbnBUZytJOVBHY2lZY2cxNDc4WTh5Y2pUU1N0WGZlTFdzaFNpQU9zUUYyU0dQTG9ERXIzRXMyQjkyWGVIWFRBckdtOCtkS29VdjFZNENLSFBDOFUrWFpSMGkxVnRFTlRQckwwd0VOM3JLMnNUN1ZYSG9TaHpKNGtjSkdOWFk4ejc5YUtzZTNJY3pybjFmSkNFRFBMdEdjUEFURzB2UmR6VG4wTEVpdmlHQXg5NmE4Wk1pdDQ0QlUwS1BMRlM2OUxjQlllNEdOQWdvcGpFTU95Tk1uMXovOU4yalVyRUNQSmx1ZVo3UTBRWGJBV1FMcXZHZG9CVnllVGk5QVlEdURqMXJaT3RNN1BoZ0puTnROWjJnZWZRV01KOFpBczNCckkrVUZWT2t2RjNoaXVhQk5PVGdla2N5eUN1VStMRDV4djBGNjBCM1RMVXYxUzJRem1KUmsxYng0MXc4YXZjVFo0cWorY3VEb2QvWm1xY29wa0pvQ3ExK1QxY0t4T3g0c0RCOWR1SnJMWWhZSDh0UC9kbjcrUXpHUVd0dUtRVThKOWpPWTlCOXpXcUJCWlRzU0V0SmlDTURDVDBFTFlaUzRob2themZjaHBseUIzN05PWHBVdHcxaGs0WmVpMVdDSlZRcERmNVp0ajB0WTZVVXhMQmo2Q0lXZVlQcVVqbndZOE5YY05ldHhKMGpmUzgybFc1QmZyYnVHWUtaTnBoY0F3Y2lnNkRTYk1KWUJlVVo2WWpRMHVUWXc0a3lCWDN2L1cyc1VLMzV0SnM0ZnlxaVdnTWhkbFVrNUw0aVovWE90STB4ZXkydHpid1pvSlIrTHArK21nQmRHaml2clNKeUhkN01uMGZGaFFuaURMdEdWUUM0NkorbFBLYXMvcVlVZGNKZGNQWElCR1VKbE1QSnozT2EzRmRzM0pEUmVzRVdXdXFuc0VWSTNZZmRKcEY0Qy8yQU1tQ3laZDlPdDlmdlpIMzY0K0pkWDlCUXo2VU1LZjgyWXZodGljRTJEU1ZEY25nbUwwV3RjamhCa1BkMzg2VnNHQ1h5UTNBTjIrK1J1VWpOdG5qUzNBeDcrc0Jnazc3VmJPdnNZbGVlR3lLZkdtOG9USWlvSytPN2t2eWFOVTExOFRPalpJa0dadlF6RTJUS1kyQWZKT2s3Y2tFeHlBZGNlVU1mQnZqbFZRVmVzK3QzWjFMSXE3Tld5Qzk5V000eTBqRzUwZ3JRZEFuUDNOL25oZGpyMGtkUzJ1cjN2YTFQaW9wZ2h4clJiVTN4Y2ZTQllLRDIyTWxTS1JuTFhpN202MHp3UEdqcSs1dEhSdFRyVTFMYmpQVUxaYnpPNHVzeU5UNVBBQUMwTXU4NWVTM08weVpnVGJLZmF3UHNyb1JqeHMvbDNmUGY5d2l3dXllbUg4TlRxMUVJZGtrY0paNkRrMnBuVmVwVEE1Z2U3ZDZ4cEdQakIxUldLcUx5ZzB1SDVyd1dYZzY0UjkyTmtQY3JNTkt5QXoxd1IrMW5sQ3JwV05SRkFxcmhpeHVNNk1DUk9FTW9zNmM2VkdsbWRZQlBJWDF2cEUyaXViZkdVVzd1TUp6dVVNdjdxOUdZdGtta2EzZThIUlVsY2J5Q3FDQnpyR0ZPZzlSaFRpT25KbWFZNmdDVlV6dm5najhZUzhkaEJxdDlReCtBOGkwYXFNT2kyZHRpYy90Sng3SUx2VFJ0dXZTNGZzaituMzR3ZjZOL1p1citKZ2dHL2h6Tkk0Sko3eTNJRVhGRlJXT0xmTnlVZ2kzeHZuNkd3MTZoRjNiVlMxK09uM2dGZkpTTUJLRjlpc1pLU2RCaTNsS3hRQUdkYUhEMXdaemo5ZEZWb2Z3ZTIrbVlVRVQ2UDQ5dENXS3JOWGVhMDZFSnh5Z3ZTaTh2Tm5kQ044My8xL2dFMEFPMHlocHRSOVZ0Ti9OVUJTQTUwemk0WWF5TXpTNkcyN1h6Q2VzRUNTRTVXbi9BUEZXQ3ZXVm5rQUx2UWlMeVJNblRGbDk0UEFWMThxMlJwaHNvU2ZVa21tbWZNaHVzQytEVG1KMnlac1M2czZGL3FjN2k2VXlOQW9GVXN4NmQ2THJjS1llVVF1REJTek5pN1FMbXRtWTZuaDFkWFNsNjRRZjloN2lsVFdQMWZ2OGFjM1BwSHpCbmVoSnd6OXprSmN0eUh4dXZCWkRyR2hOOE5jWUU4eUpJWHZOUjJ0bVVraGdQanQwNVdKRnBLcUlqcHNTeDMyQzkxaiswMy9zZGUwNDRNeWM4V2tCRTdPLzlBdmxHSVRDZEZQaVNaOVhaZ2FuM0pnSVlOWk1pNDZObngwd3I1SGxDWEhrczBCaGFsTytaM3FFWXp6MGREWjlPR0ErWXU5alBFNE1Ib0paQm5JVXFtNUEvYWEvMjIrSEpjVEF4eldiaWVBbWluRlp6djlqM2puVjBaa2VpWW54aEJsTThuZ1FucWlVM3ZZSTVtV1haaFJ5ZElLTVNuRmVlTUIwTXRwSUpzUUE0V3J3VnB2RFZ3b2VXOU5DWndWSUtvUTVzUWYwOS8zQ0dKQk5LMUsxRnRab3E0WmczeGpFdFRHcloxZk9vZk1MMktvay8ySjBYQ3dkKzVEMFNFWmVSUjd3Q1A0ZVpUcEhsT2VtVit3RkZDSU9lay9UZVMxK0lPQkdUVm1TQWh4aC9FNDQwVmZCeE44eHZBVEgreWhEZ1B4QkFXK1MvemhVM0VGdEl1SEZ3akRCem9Cc1I4Y0FmOVF4TVFiU1FNMDdRMVh4UVMreDBVVXUvY01NN2N0eXpGaXNTc2YrK256MHpvUHRDcDNiaVZSTThXUU1Celh3RXFNd01saVpsTHd5WTh0UE9DeUlzOTN5cW0yODh2d0FLZnUrL01oa3JCT2hXV0FuWVV3YTJzamlIZ3MvRDNBMU8vVzU2UXQvQzBDcTFCR2E4VGRKY3BRSWZxTFZjNERhVTZ6WC9EVURMZ3EyL0lKRDNkV1pjcjFiOFM2OTE4aFdoUG8xdzNwVHpCV2p4b2ZNS0xsNU4vOE1lb05uNXZzRGlCKytMZW11OXJCRExJS2pwenMrTDUwVTJ1Z21ubHpLT2RoQ0xDanUxYytIR2Vmd00rYnJ6b1NsMGNCVHZjeFg4ODhLS3NZT0VNWHJXMHQyb2pXaTRvVUY0YUg4UUFNcnZGN3NqMEh5K09tNWVvd1J1NU1jSHp3OWt2L2czYVU1K3BXTW40NmZoUk9XSk53SDVDbnljc1A5ZEVzZmN4QTArSEs3ZlViQzl5Y294RkErck1WYVlIajNIWWNXa1dFdHlXaFB3VnlMREk1YWpvNWNOZ3E4bTdXQWp4S1dVaXRKV3pra1JSWlcxSk41VUFmQVdWUmtCUk9ReStHd3Z1UXR1eVc1b3FlRTRSS2lIeC91WU8ybUhJYVFvR2Foelk5RlplTHVpUlRYMEtha3EyQUdldVpvWERNcXc2MU1tUDBjQ0VSMUJJdzlFd25FVDBrZDA5Um1rMERqcjlnU3ZzUmxLU0F0bmdVZjkwdWlkSFlEZmxIMnBiTDdNamYyeHBuSjhNMERmM0w5UWhZSmZQY0JKbnFWbmQvcWc2eWI0YTd6RTZISzMvWHJUSzFlcndNUklaV1kyc0JtWkVtUWNwR0ZhUTlXLzJ4RHFBZVI4QS8xTEJTbmMyYldjOHVwT3oxNEhKcFQ0eGVWcmNaM0dWN3JyMHU1ZGpjOC8rbUczRyt1QysrZkFpakNuSDVrRnhRQjRTTmFCemk2WVA1enpoS1hyeVpTdmZuMHpqUnRpclNlY3dOcW95U1drbnQzc2dxVktEVm8rWCtSMzFrWGRuR253cVNvUXliNS9JYllBTW55Mm1DYWVhT2NGZXA4dmNLSEw1NVVKQmxaMXUzVlVLY3BJSmo1a0Q2UjFMSVhuZFFXYnphaGtNTDl3dTI2SWYrYjhWSm9FcmxTZlJBN2JOWHdicFNKcUJFcTV2bTVRcy90dWg5eFk4SGF0Zy9xUzkzam1TdlR4WC9PMjNsMXVydUlabFBPYlF1T1dyVTRxYVFOK1ltREFIUllOL1l2aTVDUXpHSzZGQVpQUS9BK1A4Zmc5bVoxZm80Uyt2cWIrN3dLSTI0NjJnRzcwdVRuTXk3UEN3cXJScGU4ckpwZlFIT1RGRHlGSGgwZHF1RVBZMHRaSGNLVXpkdmRlMUhXdWZydFg1eU1VdHl5Zk8zck9FS28yZjBYL3YwM1BZMkViSkJpUXdiRjA5REFycGY0WFR4b2s3NklDNEY4TnRxK0hmWXkxY2FXMkNoWExkN3RFbk1HUXU4VzZ2Zm5XcE9pYVF0YmlKQStOUXBaSDEzMlhjZWhBa0tqNjFDRXoxWTd3YldUOFhvNkc1S3Ava0w4dDYxRndYNkF6YWpRU0NrWDBkT2p6R3M3MWI5L2djcDl4c2RsanFVU1kvbEhkWXNYYlRiUW1ieExYcUtNRGpCeWVNV3RCakxDcHZRdlprYjJrOVJXakdxbkQxaGZVbjY3aTBHazdLV3hzVVhTL0IyczNzZThIOEZtY3V4d2E3ZElpTTErZ2JhZWNLMlAybkZxQWdMUGtQdHJ5YWZ3TUI3aEtDMkZUZUE4NWNtall6a091V0UxNlcxSGFDUnRRRFpUenl5MXJaMHZiMTJodXA3TkdyYVMrbGhmeUcxSXFzV1l5UUQ0cm5NN3ZxU2xRRTNCeklKL3hSWmNWQXNHVldlaDI3NkVJYUsxQjJ3ZFdFbFNjaHd1QTdZTDJnTkR0VzBLZWljNDhwZGpGVWcvalI2UDJuRlBpcTBhM2txMWZCVCtzbFY1ajFFVTgvY2c5cGtUZ0JNN2hZejRpNmpRYjZmRCs1ZTlaNGoyZDVQWmNzRHc0VjVWYmxXcm1Md2xMOC9sUTVvc0x0aDROUnNSUFh2MWxEbzN3R2pLbjFVWHpWK09LMjJybnZOc09OK1loYzdwajVXTXJPNnBwT0N2N0Z6OHFjaDQrSGtVbjVOdXZsTjF6bXoxRlJGSDlseDVUVVZmUVRuUlJDN21WMUZtaFoxWWVwbnJxNVo0Nm50c1JhRGhnQ3pRbGRNMVExb00xSld2TUk3WFhrRmh1Z01vT1RmWTl0VzdrVUFzMmViQ0lhWWdxdnJLNmhVTmxNNnRqN3Ura2RPSUtsd2x6RllrN0dvOVJYZlZrbHdvWExzbFUySnhlU2p5dFhUNlh0Vm1TaXlQOXZCcHRDODBvQnBlak1nV2NXSkVFSjA0eHY4aVFMZzRVUHhzdGNQY2ZqOXBGLzVWWGR5a2ZxY0J5azJrUXhRZnNDaFo3QURqZWdQRjhTM0c4OXNaKzJlVjU2QVBOZ0s2YWxnQTRoR09HbENGRDFiWjBsRGh1SFhaOWlMRWUxWDBCSDJMVkZFQUlGUmlQbHJBUDcySitRZ2FXTkorYUZQK2E1TnpFRGhucWxLQzJKejh6cXZ1eUhQdDJmUit1MnA4ZEl6aTNUb2JrVWJLUmRiQXhtWXNVbDUxUEp4NEN1d3paQkhiWXpBTG56bFlGb1lCRTZrQlJROTRHZnpzTXdCV2JyeGVBWVp0MFRWd0hHRDgrMVhjODlKUnVUSGFvSzlMU204SWd6MWtSa3hWOU5kT2hzRzhRdlFtTUdjNEpZMDN4dE5LZmxDS0VzMm9pR0pUOVZrMzBWaGF4Yk1mVVNIRGJGSjZKQURybHVrNkV6cUpnY1ZjYXRFdUlIeEVsaTd5elVZUzRJSVFzTktCTXNiUXZPNmVCbXpiT3haQjJQMkxhNHZRREN6L3ZZbUlYTVI4NGFSSStvUWZhNmgvOVFLeHdqMWtTa3YrMjBXNXJtZzE1aHFiQXhoMGpzbTJEWEtiTTVTb2pnTmlmdTJ5dkxjVlNoOXk2TVp2aTFvbXRBT1NDa3hBdjVLYXNkQWduczNKZFJ5QlZDUmN3dEtqV0VxczVpVVR3bExmcERuK0E1eHN2aXIzdFVaQjRrdmZJTFFWcE5mSDFFOGpETXAyNk5sVDNrTHRIZ1lLemtrZEMzK2tBVVRacWdocnR3dDc3elZTQnE1blovUWpLWGgxOFNLeHV4aHdHeFJlVEFjRGZiVXE4bFRZNFpKdEF0QzNXSTJkcWRHdmJyODdDYjFlcVhCaUlTb3QzOStvKzVaQlFWcWczY0huNTl3OStRQUladUZ4ZVZZUFhIOTl3Z1pnV3pxd29kck9rYnB4YlNKQ3F6U2Y2dzVPVHkwV1FRaVVUZlJ1T200RTJhNTZZM0lNdHI0S1QyYVJNNGx6ZGE1Sytldi9uTUJKYlJleDVOcTFLMUpEUm1GQzRNRjFWNkFFUVdvMkp4UTR2L2FWaytCT3VFSXo2WFh5Z0lEaXl0MHJvcGN6c3NNSmNoTkp3T0NHME1tbXdEYVltZ1RkMHA2M2k1SHZTNzBzSkl0cVY0Wkh6TUY3bEdDclN3d2ViU3gvMmlaVUlnd0RuSStxa3QrYkF0US9HK2pKZGN5M2dYa0M0eGtCRVhDa2lTU3l6aHlPb1JOSm9DbFBRRFVhakF3L2hVM2tVQzdKdHJhK2QxSThkWDFTdWdDbHhEam45NkIwcEZPQVEyV1ByaG0vaUYzU0x1T1lZZ0hBM3lHVHk0MkRVMWFpZUsrWVpaTXVjNEF1dXR2cVZqZ3E5a1hzaXRIKzF2NVluZWpwK2xRUDhKOExyREd0aE9SMnhNOURDajd5WXc0R2JucmNROWs1OEYyMWF1cmdYMVVkUURzaVFNV0l3TFRMTGtEeitTbG1mRXpYOUJBeUJ3SE9LWnZQQm53bXNaOVRUMU5rQkFUVFJSR0lXMVlmRlNMNU9Xd2hSa0VrY0ZOa0pEeXgwb3JJRmloSVNVMGtYOGJNRG9UYzJDNCtsUC9KdHpZUFhkSDk2Z2NnbFdIWGpNVE5Eai8zUERkczV4Q1FIaGRiT1ZFNmZJbjZxUE9BQ2ZxVnhFLzMvSVNBMDAwUVRsbWpOeGI2TEFOM2laOWhBblZpbTFENDNqcVoxN2pCQlpsdFFhVVRkalJBdSt2cmxrY2pBMit3OVBjNWdiaGRXM2ZGRHkzSEhxaWpEc3JPaUNCdGVzd1FjRXV6RmNOeTB5N085VG5RNGl1Umo0M3RLK0ZTV0NEanZVenVJdU1JZCt0WW5zd2Z1OFd1dUMxY0lRRmlkanBZelRUWFRGVGxNMHFGVGNWTTFyQ2w3Q0h0TEVwdlpQRlcrcVhlVUJZdDhiOTNPWWI2YzZoSEdLSFZVNXd3WkVTQ2dFMkc1RHNETkFBc0FQb2VmcVNrTXd1amRFemRuWHJyVDBSU3A1S3VQRm1TKy80dDhDWlV3RDFSL2VsRjdVQ1I2Wlo3MmxuUW83WDh3Y0tjaVhid2RzS0Y0Wng3QUVocFVRMHRIWUJsdTRHbmJGQktFWmp2NzJDanpZNkNrV3VlNUZtQ1hWbE1aeGl3Njk1U05pZE5XaVNlSjJKWFdNckJMd0JjV2c3T3dLTnhYaGh4RVhhTzhIRHBPQ1N2ckI2T0ovRDFmbktKUEFjNGMrTmg0MUJrWVE4YnZhUFJZbkNoUnhUNTFMT1dTelh3UlZkbGEzMWdrdGp3WXNiR2FKam1IRU5LblZ2MEdSSjdBblc1REJwNUJqelZUYzcwWDRtWlA2dW9pUFpqYno0bEcvb3FGbHlXdDlnWTJ6WHg2d1pTbmxkWHEyVUQ0ZEZPdWkyV1dZRXdxZlhkM1NKN1A2eGVGVjlYZytPQmREQUNnRWs1Qk9vYndFUUNOMlFIUmNOOGd1dUh4UkJtOFQrYWc0YnRZeUJ2bzRWMkdnYUxaRWdHdjlOU3JUVWR4OEEzWHJBelgrOXUxdE5nL0RjQ0hFU0tkSmpmQ25aSDZyQldGMGpQVkZyZ0VtQW5UYytQQnVTYUtnUDc5dCtZdzFXVldDVE14UXcxZWIxZWRHU0xwNTFncDdWZnVVRVc1emdqbHB5dzFkdlEwOXpySkZtRGtXekZBQjByVEpnTmNEc2pPL2g3Sm9VTVhrRTBZajNMNzhpMmFGZi9US25FUW9ibVVzVEdmRURnaEtSeExpTjZub3h4eHBDckMvV1M4ejFmRXQxTW84SXZCODVsa21OSE5CKy9XTXMvd1JVUnZFTnRDaFdYZmp3VmJGU01qWjRvUE9mNVdmYmR5S25LNWRmNHZSZWhGOHowN3lRK01TNnJVV0NLdmwvZFhHbmY4UFQ5WWtGWm1ybWQyS3NBanprdUQ0QWN4UlZkLzZ3M1Q2akMzWDFLWTNhQXlkSnhlbFFjb1lqcXEvS0VGV1ppZHN5VERWeWt1WTFMdndRajhHdllFVDh2Yks2UUpBWHRoaFc4N3pEMWFQOTB5QmVLdFlXemI4R1M3V0xYWDczb2VZNEUwTG1sSDJrWkswWG10ak5FbWV4dHlmUUg3SmxaaEd5eTNWazhQZkRaT3BDdEZkejBSRWJ3NkUwWC9vVXl1ZktvblJOQnRkM1VQQXB0REFHaExON1NSMG9LUWFvWlkzVGRPS2dIN2tybHphbnJ1Q05udnNWaEovWDFtbTl2eUpPYmhYajJSb0JKNmxhVld1NjBZNlA3QmV2RmZOYS9TdXNQeTVtRUduZVEvV0RJZ0tiV0JMM0dIVkJIMWI0RG1aNUlSV0Jnd0duQjYvS0xla1FNK2l1Y3d2bGttbjFNNDJ2MkxOcFk2UGppeUc5VnBUaDhQU3BoZ3Vsc2I1K2hRakxycElnS2JSV2d3L2xRR1pWZnd3bWpMREFrYjBmdk5hTnFEVktxTmdSeUlJOXVwckFuVjIxTHN2TjNUMVhyWTZGQVQxYlFHbmQwa3RWNkVkT2lpbGZUS1ZJc1B3aEw5Tkg3TlE0RFlLTnVaVWg0R2FkTEY4RUVyUHZQNldyb0dZQjhBU0RjL04xWjdBSGdZRVdNdTI2akNab3VBck9qQ3U0K2hNeWRRR1pvUUtoRjFQaGVQcU8wSU16Y3VRV1dPWHJCRDF4TjVQajdRS243a2hsWm1PeW91V3h6ck92TjJUbkdGZ3cxYmkzQS9uY3JNRG9sODNmNVVVK0lPdHliekc1M3hTK3FQNFlYYmpUcC9xc0ZsVSsxNXZPSHJ3dHc0Z3JaZnNoNXcwR1owbjlqaGMxVzk2VzRJNDR3ckdZbHdPbkVuK2Y4c2hPMHh3SDZBNSt3d0xnU2I0aWNWS0h0QUhnWEZFdzBoV2tmOEw4b0dDaFVwNXVaVVYxYjlvb3F2ZGQ1SkdTWjZNanRhYW1QdUpxVjVkemw0cnZ2RmdpbjhlZnllN3JaaFJpSTR0R3d3cHcxd0tRM0R2TnR2RDRiZHF6VHRycjRsTkZDL05PZTVaSFJrNy9XL3dpZzJscjRnbUxMdnJkd1RuYnpoWG1rK3Y4ekFubWh1R3VhTHBzeEFJN25YeEZUZjFOSDRkK3dtZWdEWElscTZaVys5aVM4TjlSS0FlQWdhckVYSjRuVUN2OWpHUVdPTWpOZWFOaUZ4Uk5LRE5PZ1BkRkxkcC9HZGVxOUhRSGpEaVd1b2t0dFAvSUxleHh2OVIrbU1raXZ0UXNJcVN0MzBPa3dqSDNiZHB0UXQ1bjJ6M01wZkxsc1FMYTFWQ0toUFpqbndERzBWWWJMQXBzaUNSTGRjTFRhS0VzcUlPbzZ0R3hVY0NyNmE2SlUvR29vckRtOWJ5VFZnVGU3OUtzdnFpUmQyQzU3d21OTmVVK0puOHdEeGhFeDcyUE1JaE82MjB1VUFLL2VDQTFJL2NNUnBvbTdyaStPWmxsb3Buc2FlUEp3NWJHbXNIV1NsNkpqVXc4Mm9QMDFFcUtTY1RyOHFXby9uV0V6TmtzcGhwQjZ1N05PNkFnRTAxVGtwRnlIRWZYZmNEVDdITWcrSmZ3SHo4dGNOWk9keEJ3aUpWT3g4V1RPbmdWTlBHWUp3U1Zrd0ZxVHZJYWtnbExQaXY5d3g2TlRqRkVkcFEzYjNqUVE4NFRJeU45a2JqUmJ4WEFJMkx1UnY1T3F2RHNmZW1UVU1TbFdDUkZxeUlDNHdJWDRRUTNwVnNZUEtjYjgwalhGNDcyRktYYTNlMmxTeWt0VkN3NFQyVFNYbExwdkplczkwUmkrUTBCMlZPNEQraTd0aWFad0JXR1dsTC9ERlF2UGNZVk02emVrQ2hJVjJjbG5KYWc2SlZZaEJ4WmZvbWFhUHR1bk1zckRaWjIybEhiMU9veFNIVlRhUTNweGdhS1JKUXRlck9DNGU5a3IrbzY5Q2tBWmE4SkFzRXRWUVNIN3JuenFMUkorb29OTjRzWlNuOVJPeEVTSHBpTzJWVEtMK2NVYnk4TnFFN0FwT2tPdHlxejVXMmMwanBvTGRlOTBWQzlmY3NXczBkbXovT2ViSU1rMXpUMmxyYlRhaU9tSzY4T2tiY21DSkJIQ3BvckhsN0x5cGJLZ09CSU5Va1Y5NFVNYm5sUEdENVFzeElvR2tBNVdFRlk0SDRiZ2NpaDFOd3F2cmRsZjBDdVNySzRTT0JhZ3N1UGhscXpEZ3lpc2Zodm1BOS9lak5ZN2cxbGlEYW9nY3ZNT0tST0RLVUp0cE9ZUHR0aXVzZjB3TGRIK2RCTHhZSkxzbGpUWmRjdmJTVGtVcTJNL2xLeTR2VnI4ckFMQ1dFWTJ0eDVTUnd0VkNpeEcwcFJTYzI5S0V0RlF0NmoxdktBWTIwQWF0M0w1RlprMW5sekhYNmFzT3dqRnl0WnkxNWhFQWpFVFBFYVpoSGNINU55YWw4QWlCZFBqODlTRWVCRWx3N28zTmJuZFFaRWhWdFJPOHVVeWV3cWtaK3lvUkFkVk1WYmdXM2NMM0JkdEdoQlpXS2xVem1Ud0ZLaDVtQWhZakZiZTRxVE1mNE1yNG5QalhBaGlOczNsQ1pCZU5ybnVocnkwdEV3OHJzOTZhTkY2Q2R1QlN2U1FzcDdYb2RQbU43Rko2RlVmdi9LdSt4dU1JWUJScVd1Z3BsRllhQmx2d29XaHBDZ2trWC9RWWxIbkxESDhpd0RIdFZxcEpSZzE4M0xxcWFhV00yeERCb05paWxFck5XcUVoNlBjQUR5REZrM1hjYzhZMU5YaFNXNGY4VHpyOUpmY0ZkRE1GcUNWOFgyT3V0TEpwMFlHbzV2d25ESlJ4Znp3dHQrT09SWXBaS28ydEc1OU9XTEtadzk1VkVHeHZhRU1uUm9pYlUrQU5MeityQ0RnNEMybjhOTHBHU2lYZm9GMHZ5aEU3RmRjbjVWQmorSEp3VjJZNTRQSjRiLzBVeUc2UldvaTdkR3FmU2FhQlBFbUR0a2RYcE5YZTV3UVhFdXV0NGRrSFh2cHI1bS9JK1FGU0tPaTRtS05oNkNGcG5TMWFMbEVteTh6dXpaUXVObk04aW9Fb0JWY3dPQnVONHZ0MU4yY0NYTmxCdUxKY29xTEVRZ0pDNEFLOHdPTTY4eWx2MWIzbllFMEp5UjB0RC9yMjNoVVpWSE0wTWdQSEo3N0hZRWpES1VLYTh4SFRiV05KUU9Xby9OeWg3QTE2WXhMQ1ZudVdKS2NDZ2ZjeTl3alpEbXlEWThDNjZYazQ0cW05QU1PWjQrQmViSVJDNjhKYytFa1hyN3V4YmRjczB0YmliVmlWODdTU3h6QnpwR1hRU1AxbXdUZWMrUkZCalMvZTkvcm5sSU5xelZTd0R4RVdDQWhWemcza2sxR0ZITkFiQXAxbXlsaGxvK1V2SlpDOWVHTVBnQmFaVXBDT1F6RFg1Vk5oK0NObnUzRTByTE9IOXZsYU1VZFZqTUdTREZvZ2tKZmZtcjIxZ1BFcHl2SGpVdVplWHZ1Skl5MXdmZmNLUGpBVFgvcDVNU3pWSFBHOWFXeERyY2x2YXZNc2UrSzllM0FhYTljc3FlcFYyRTJMUFIyU1JtcjN0aVJFQys5alliSnVqZm9wRlBzTTdaWkdxSmIvN3U3T1hJQ2NRYVRMc3pDTW82ejdFWHlxcDVnK2g1L1FuOG9BNXNtRVM0d1B3OFhMZS9VWitQeU1zSHAwL2FqaVRvZnpKejkrcWRDRENpci9MRnd2UEZlcWtnT1RqamN1OXo0bzZyV3QxN3I0OU5VNGRnM0owS1dvb0dvQzRQQjYrTVowaHJMOEphaDlTODJxZGxkMVZmMWVoSEpnYWNGT2xaZzN0M1hpSHZPcE5Sblk1dXhvQzU0UGt3SmlKdUR5SUpnSjRUallFSEZtaHZYYVNYL1puY25SVGNxMXFqZ0VJY3M4V1BrenE4allheUR3cHJCQjFaVlY5eVRrTTBZaDRFeUxRZ0tFOUFZY2pVSU5lYVhMcmVRdk9XZ0ZkNUJjWWI1dGRMTklJM2JHYnFKc1ExRTVQdU9kUG10N2plMnhmaW9aNDQ0bExramVsaGZrNnUrWFFXVXYvVUJLV0VJemhtY2l3TU43SkJDdkd2L2ozdnpqOS8xZUg1QlRSQjNib0N6OXgxZXdPTFV2RE52cmJUa0ZmK1IwMDJpeXRPRjJ4NnVCcy9IM1JzeVVhd29PMFJ2TjF3US9LVEJrVThaT3ZxL01qbEI2d1Z1RHErS3VnWlZDQkx5Lzh3RGtidXgxZHkrSUVUOVZaV1BmWlBiNThuVGNQOCtJOHA4MHhRWW53TWpEWit3SGRzREsraVdhUnhKd2VVL0p0NlRINmZtSGZ2OXNrKzZpbE5ONkVuY3hPeEFyeTU1bFlFK1lwRmpHblRxd1hvSCtYMk5uZzFlS2xOeDFtcjlIajd0bWZNcnJLMzE4SGQ3K0JPdFRENVZQbG5MRDRtNWw4QW1LNkYycHJuVUNVYmgzbG84MTAyVmdQN2VTcDlkNmYzdkhqakQvQWliaTlCVFlQWVcrMDlieVd1OVJBY3I2bk4wVzlRZkhpdk4vWmV1cUZ6ZXVuczR5ekVtQ2JZNHR4WDNWZTRQQVJibWtyMW10WEhpUXhURUxRdVB0ekk3SWl3enkyTVVRakI0S1FXODQrM3l4cDJ3Y0w0bEo0ZjRZYkFiWWpzcHJXTDlYWWpsZjgxOEFCYlh0dCtiUWlxU1dMZjNaM3RlSHZNZzN3YkZXbVRzRzZkUHExZ0p5Rlc1OVNOdjgvWk1pUmVaYW5JUWVGUys1MVUxekk4K0N4WE5uOGp1M1ZxRFJyY0dXN3g5NXM5K2FScElvWTBucTZmaXVnd2dwZEJ2aCt5cFZndWhYVGc5VEdNQTRVcWpCR0tFTFpMbXZYS0ZIWTZVUnhuWGduUWFycnhibTV5amtOMTh2R3lWRUxIdGpnMEVyV0ZmSndOVXFacXlxOXRIUHhKbWkwYlJEV2FMWElqUE1zU0hwbW1EUGRiY3VyTFhzNU1hV1VyVjhNdHdLWTZZb01jQnNaQTUzeFExb0cxSWU5TWx2Sm5RdS9aYjF3Rit0dWJOUDJ5Q2dhYUtHUGFoL0J3cGlEN1JOK3pHaVRZYk5UeFZTcnJCWXpubllTUEtvM3IvUkRrZzVIUkwxZUsrUzh3WmRGbzlHT1pNdTB0YnpSOHNiRTBQcVVvZFFKbi9wc0V4bnM5V29UTU5RaEpmNHMrc1k0UjNWUWUxa2ZHS2xlTXQwTU40aVFqcG9weVVoTUJzcEdUMlJFSm8zOVArRlltR0p5N21vWEZ1SjBYbllxcjZuT3pnVlE1ZURMZzY4QW5FVWFDa1o0Smh2YjdiZTB3L3N4b0VOYkVhZ1EzNGloeW82anQvcEdoTFAxNFJpdno0YXh5NDI1UjJwa09xcmlxMFZSSjgxSGtMWWN6c2dOR3VuUUZxVDVtaTdsTGdQRlQvNjZ4SHFhdDM4MG8rZUp3SnF4eDJMcWVVcmtUM05IYVIvL2Fhek9qVExpQUVTZjZuZ2J6Tm56UDJ4YjZWWVZaMjNjMXFzSC9FZ0c4TmNlYmJCWGFZR2ZrdFlQOXY3dDdlUkFIb0FBaFVrYTR6VVpEN2F6R3k4UWVYYUcrRXl6NUlFNC9XUnhXNko2Y3lDQmUyaUZWOCtzNllKcjRHL0R6RnJKT1V2WHNkVW4xUEpYU2dqQTJrNUN6N0twdXN5bHdvcDNVMnFLN0RTN3JNWEpjT3o5b2xjYXFLclh6ZHllS2NPeGJTSGVCbVN0a0c3ZnlmbWMyU1kwek05WS9xd3NoWkJtc2UxYjJJb21yWGhoaC9kdGhpTm9hNDdnc2QzOUxDWm10WUM1OG0yUVF0NG1TYkdCQnNXaDhJY09rS0ZVeWtqQXpJZlcxcnlvOStOMVZsbnN3V3V5S0ZsUXJoTWFrNnhCdWRaWnZVSTNiWFhJQWFXVXdxRkI0V2cwS3FVTmdkN2poc2ZyS3hqZ0lUV2dXbUNJdlZHWUh1WmNxTVNLNWd6ZXhDbFU4Yi9iTFlZVFV5TStQbEFmVlZESzl0bGZ4dnI3NHNBZUxjRmR6NERVa0hyaEJUazZ1Rm5hanFsblY0R1VUTVlZNWhHclVJVkEyZ0xRV0g5SlZBYzNBdjBFNUExZ2Z3ZmVtL0g0UEZnY1hucDRRRkorN1UrY2J6VXhGZDh6a3oyV3VxNVdUVS9hSEVXVXRPY2lIWTY3REZNYWZNdGk5aUR2MXFMSC9LSlREU0FVaGhDYkkzWndPQ0dESk45ZVlsMnB2NFJKV2FxNXpqQ0Q2aEJab0RZbnRrQUNkM3d3RlcvblYxa1MxR29oMjhZbzBSM0x3VlVGSjAxcDU3UnpBd2NIMlY2ZFdpVnBJWGh3WXRBd0dyMVdxcSt1SkVUdllUci9PNnZsbDNjZ3NTK3RtQTN6RFUraWk4T1NvWUp2Nkc2d0xiay9ZRDVic2FGSmxqcW5EM0V0MlozRkk0Tis5c3dsbWRkUkVEYThHWlJzNEczbDM0T3c3RVZlditnZGNuZjJITVlTYlJFUlM1R0tFYlJkQVZKcENrRnFick5nNEFZSkY0ZjVselhQcENIQ1VLOHFqS044ZGVFMUUrNE82RzdWVzJweGJrenpPVUlUUXlxeGN1ZDZNWjhld1ZtT1dwVHZ1MjU5VkRlaUZNd3hBNUJVaE5TczJkWHY5QUZyZUpMS29NV0tua3BVYjlncW9tVnlvQlBwczZUc3k3V0FFQlp2bzJ1Q1gzam1ZS0NhMk91NDg2cDNiNGovZStjdXlqRW9YRzkzelZ4R0w2QktydjQ4L3lva1RtS1J6VEhFZDdvbXVFMFRMTFVLMkRTTllaWCtwWkw3b1EzZG5iUnk2c0hPRVdZdHY3dHdmZXZ3VUNVS0xsUlMwY21XNElZMmFFMG0xdGR2azQzeE81cFVsSHRSdEZKNE9LUmI4dUVONnNsTURxTVZCK3pMUWpNU2FhbWlQazQrSG9WM05OUXNkYkEyZnhsL0VmRDUrYVBYTG9oQVdzNDhsek5hODhQbEM4aTF0UnA5TkVzQUE3YVgzei9LN0p6MTdIN2FwZ3cySU1oMXRyTVEvZW01aHBmS21WdEljVGYxUmhHeUJCS3ZLc3B4YjltQzJrbG1abmlFZ1lIR2JjYXJRSkVJWU9GdGQwZmtwUkx5YjlvNVptYXFSTWxrQ3hWaFRFa3l1a0w2cTliQytRRHd1RVQwQVBMQ2UxNlJjNGc0b0UyajF5RGwyOVFPUnlMZkdhKzJHZzNsUGVBMzF1Ykl4aDdKandua2d0cnp1L1UxbTIxNkdWcERWK0R1TFIyWWNHVk5yYkNiWjN5NE5yaldxUkg1M2FweGU2SExLRjY1MkI1R3NPOXQ0bW5FS3pFRDF6ZUp2Tmk4R25tRHZLK2tTK2hDaDU5S1lJV0x2aEdzTXg5bWJEanFjUG9WMzN6V2hGSzF1NUR3UzNhbU5oNnE3NDNQMkx0YWtkT3BiZnZ4a1hZdVpISm9LNFNodGo5cnNEcUxuV3dHTnA1N0dXQytIRFZoeXpCZVU4b2Zhd2JDS25DUnliRWZmWTA1T0tPdXRzOFNmdEgwWjJYeWI1VUNkL0hlNU1ZQ3lYWmJzL1poSmErZTJ3TCtGYkMwOHY5akdCeEl5NlNuTTd4YzN0YyswR1pRTFdDaGpzMW12NXgwdkpjUDkrMWhlZVhvOTZSNFp2U3NUNHFxZloycHhTb3gyeHZHbm9YZnhVOHAzL28rTzhzdlZiSjlsUFo1bGY5NytNbExpZEttQzhtNjBGdlh6VzcwK3ZkWFg5VmdZeUUyR0pxS0NOYzkrOG1EQ3J3bTJraHhWdjBPOUNuWVZHN0wvMDZScUtLdDREekhoWDhIUHJxNW11OHhUREt4OFhUT3VBRHZtQkxrb3JrWUdZbmlJZGRUS0lWT2xORzVQeHZuYTZqL0IzU05yc3dVQkpnVy9BcjJQN2FJQ01OblhRRWJYYnBpNmZ1cWtXdjc3NFFLekpSQlpEb3ZyalRwL2lyc2hCTXNpVVRmQ3YzRmtxa3BIM0NuMStyY01ENENFTkxvQkpIalE0OVpaS3V4U3h3Ry9BYjdmamFTdTkrVisrZEdYWVp0b0NoNCtRUTA3TW5tRWpZVEJnN2lVZjIzOVRZelhwK3pheldpd1RvQlFjTXhKL0QrYUg0RzVhZ0I4SjhDUkQzMmNDUTlKUHhxSTZwU1dqL1k2WW9XWTlqb2Q4REV3ZWwxbUIycUZyVXF3Qk5EWXZrQnJMdlBjRTNBZU1uemNzbEV1aFAzYkZUWmtwZmVFVzJoVHdKTTJxL0RTNDg4ZFVBKzFsNjdlRHhLMkNxWTBOZnU0cWRqNVB1eCtITGNZRGw0SzU4Tlh2dlZ1TmFDMGZRUjRRT3pqSitDbSt1UmZiMi84aEVMdDhqbXV2VitSa0M3aVp6dWdVc0VBTHZhTXZjNHQvRXZVbjZCYU5hUGxubldtRDBick5PcjY2ekNyTUlvZ0dPRXRud1M1cHFpUDZ4c1pKNGI1aWJDb3pybHRKL1VkQWJUcU1lRld5dWs4SkVqSERtVEo3VjJCUFB1ck4ySzltTkwwSVlibk5HQ3VWQVcvcnVyZjZZdzhSa3ZVUHBzYW1hNElobzB2bHRXeWNGU2pSMHMxS2E5UjN4RDMxWldhOWZtN2pVK1M4L3NuU1M3L1ZwUWNrK2RGV2t1RjlQR2xlL05VMnRzVTJXNXVBaGZJZldkeVdjZHI0S1JLT1hMK3VEa2RXVTAwV3RHMGg3eTFYWWpzbDkyMTZzWnBnU0xubGhmNmZMVklGVVRjek5ldmRxS1Fib3FqY3RxS2laNDhVYytvVDM1U1hYM2xYc0gvTXNxMVZhSkVlOTVwWDBJeHI4L0ZvQUF4cFdkV0dsUWlSTE93T2tmN3pOMlJZVW9pR004M3doSDdMck9jUEtYVkFEVUc5Z3V2ajE5ZW1ZTHRJK0h1SG9NYlV3VEc2SFo4b2NyQ1hOMHF4S3dWT3prZmpnWGdSZEhTQUhKV2pKQnA5RW54QjJObFNoZmVaVk9lVXRTdVEwSFAycm5aTStCRTFrM0FUcmVtbDRlODUzbGRkNG1BM3d1TTNER29WOVdocGlaTmtDMWhpa2g3R0JkQmQ3dWhSVXNoU3FPT0JmcHZqTllveVdCc0VtbHBSRnhzc1NZbTZhakMwdXJ0ZlU1Wmg3bU8yQnQ5R2xKVGg3SUJKNW5DRlJxYzdHTTFHMnhyMTJYaXZsbC9jRGdTNEFKRDMvK0RTUnlMSittY1E5QkcrNFNTMVJDenliS3NtK2N2Q2tCZENwK3FYTVE0ZVg5L2VtQldSTDMxL0ZwZXJaSzQvaDdic3l5bllxMEJHUnlIMWxsbktSc0lSa2szK1h4NkE5MkNHVUNET1psOGZaUTVSVDZLSjlOWGdsWmJoR3Y0UklrNjFqTkxkM0ZseEJnOHU1d0N5WjZPS3k0LzJNams2bDVFL3JhcFBsOGNiQjFkRGwxL2NHQ1ZRdXY1TVVOcllBMkpIbDBoSEVyekp1OEZLNFNab2pBMWtWVXZTK3JRaDhjWGo4bHdxSk9QY1c5SDJscms5OGhwTmJ1ZmEvUnE5VVhSOXlRS2k0UVp5QWEwWGQ3eUxjUVFEMWNBZ2JvTm5YbjNoNytrRmkvdituMzYrdnBIQUIrbVJRc2lJeS94cFBKMkpCR3RjWUxXNExGVXo1Z0pVdXR0akNDNlZBZE1MU3BWZFFLdDdYTFZYNWxnYmcvSWFIM0RlbDM0SmRvUk16akxxY3VvSGRUKzZEV3ZFamNhMVhjWWRicDJwdzdPMTlPNm9FaVdBRStvOTVjYVNDWG9ydm5SWHZDQmxibzBFenJhUTR2czNlOGhySGdOL0UweDExS1RFY3B1RDU1N3Q1QjNieHlma3Z5ZkJzekJ2dU5qenNsWldpUWVrTVRRVGY2T20yVnVMbW05TVJKWUV1bURsR1M2eXVjL29CNTZVbW55ZW92MGpFRXJvcDdnSXIrdW50VkE1Q2RCZzZmWjMyQXFWb2VNK2hObGtYV3p1aUZ3M0FzcjNFVXh3TVdQRjVQaDNnVGVwams5amJjdmhLNjl5eHVueWY1MU9rcnQ1OFR1dTBOL0pTODNJZDNBN3NvL3paUngwYW5pbzUwZmVSbzZ2aVdsamtmb3dmUERRSTVGSURIL3hWQ042QjRPUjlFY1VqellkaE1uVTk0aHgwME9ubXB2c2oxbUltdXJhdlFCQ2JpQTZCMEkvQ2lTbWtEa0NoRW8xam0xNEpJQTBxbzBwK1NOdzZROUtLRTZJTHMzRm1LZGZTeUpYeCtTand6aitwZFA3Wnd4alpCTFg4ZkpxSkJlU0VTTExGTTQ2Mkk0alh5Ykl2T205cGpaVkVmdVEzSm9VQ3RINnVnbFFMWmhVNERLU0gxSFNtY083US9VKzFKS0g4eERTeStaMW9EaVhyUnliNnVoNlQzOFNveThMckc3eDI2ZEFVTkkvRzVHKzdmYkNHTU5XdFZFU2tzOE41SlcrQlU4U01xdy9USkxNbXo3MjB2MHdZbVlySUpzMXJVeTlURm9namRoUlJIUmhHUWc2cE4zUnhaT0JWQUVrRG41ZzJCSjZWM1BSNWFMSTFrOVVSTElvbkhLR0RrSTZ3UEw4NFQ2OVBabE83ZWo2L1cvNEJ1WGhFcllGTjVXZktRTFAwZng5S0ZTRDZkNVJxWkFKR3VkQ0NSRmZnNy9kYWQyRjdBNFdBZ1QrOFg3TkVJa3lLRjVOWFBmZ3FHeWkwa21ENGJXQTZhdVNFU2M5STdWaEpCSTFmeWFEU0RZN29hNlR3Mkh4dW52U1pBdU5EeUlNZmkrOTAwMGFIWFQxUkhHQndZUEl1elM0OFNIWnVFbWFZc3lLZkpyMWE5TWpXZGRVUytlekFxVDZMZUdia05ibm9PMktvUXBTL1Jib2RpQ29NM3F6eTdDQWxvSUpqNEpZb0JLM01mbTlKRUsveFQ1azU1U3RiQWtyWEZmV0QzY09rWjN3UUZHelFwWHRQT0NtdjIxYWN4UzVMdGMydHZOZ1VwWUtiNmhOYzQ0S1NCWUtoNExxbWdsQm9YWDNBN3NuT0pzeHBlQlloZ3V1TnhxR2VoOUE3SytwWHVqOXRQdHgzVEE5RmgrZHpMeVMwbHQ5ZXVXbldYdGg4bWRVbWJ0VzdkK2FCV0ZUUTh0NjQ3NUpya2kwWkIzSlpWUFpuK1htcENoWkxQOWtHajVJRDk4aW9sK1h6aW81TGpRTUVjZGtLUXZFYWpidC9qK3JybXFycXYxdXdCc2FFTkJQOUlINnpyNjV1c2ZJc1MxbHdRTGVaaERtZWF4b0dRdmZWWVRIcU8yQnpoVjZRWVdDTUc5SlpTMWZmYzMwalFkMzR1YS9hazBLZVp4ZlFEN2hpZFNXWTRKWUdSdWZlY1FzNHdrYmNqZzhVWGZweHBEdnJZU1pTOFdOYlVFU2ZIejd4aDBNREhOemwvVVJMNHFYWXc4T1cxZWFGak5MRFhqcTI5UEh6OWg2SVc0bWU3eGs5Q0dVazROVGRHc0tJNWRVdnBKQ3JlZU1mSHF5Y2ZrVG9VNnBScUxTSTUzaDU0bFpIM1hLSHM1SmZqUi9nV2pDWEIvMGk2Nk1OY3RsUXpGS2pzZ1RtMVV4cTh2Mnl1eVhjcWFVZFBONGorL0FkY2x6c2c0d1dMS2pocXdvalhkY2psR05vbEl5elltbyt0bUJKdkFXZ3RZbzVucGk1ZTFuNDFham1LYlFNMk92RndvTTNaU0lJOXBmeXA0NGMya1JhL2s1dmI4Mmp0WmxvWWdrS0VTVjBoL2E0VnRUa1VrWC9uV3BxNWVyL2JHYWFKNWdtN1Bod1hobTBJUjNSOHJsUXpNSUhUSXVPVGJpdUdScmpOaG9lekJNc2RYZ3FsbzU2Y3FmQ0VmVldoMncySG5LdHU5aXhLcHRRYndmb0NrMVEvekpDV2p6WElLOFp6Q0U0TU1sNk1VUmJ4MkFhZTI3VVgrVXpuRTZSU0NtVjVKOTFJUG1VNUZ4WTBheU5ORXA5NlRuZkdoVjdUT3JUY0RGQmk5ZE0vVEVKNXhrb0NINUZIczVYNDZYVnRiMHNSNmpvUzRaZlJQMDc0dHYyNDhPL1VpOFBnOXhaY2xMb2xMbnJUWTBwVDVPYU1kMGhZcDFLYWFiNkp0MzQ1WjNEamNzZDRYTHJkQyszMDhPYko5Q05FZ2VqRHM2UUlrdzAremxMM21XaXlMazdyaThzM0pLcVBnMG5IYUdHZXgwNDhjQWpreUZnR2JHQndFakZYeldkTUdkSTFEalNpSnhtZ3ZOWXNURHhkUDg5WlM5Q1VzSzRJZ2hDZm1yVU1KQXVpT3lmYjg2aEp2U3IyN2FsM1FZeno0cVFQTCs4bWRHVVhRTGlmMXptbWRpUWtnNVB6M0doU0pEOEVGRmlSOGhueXlXbGVSQ05ZV08zS3VCZmdLekJxNm9oZUxtQkJ5RXlnNzUyVllPTTNyOUNHNElaeWVvVTRtb0FxWkxEWVZJbStoZ3N5bDlxVkRNWE96b0tlWG4ycXRTdmtYeGdkKzgxL2N5VGRBUTUrTmttWXlYV3ZJUk81WnBTVnIxbFRpU05RaVlOWFNqTitveWtGaE9ieFA4ZkI1QXlhcnF1aHZwWkttcmxvT1lJcFpxLzY3TldjYTg0aHQxOE5JNUtIZllkeWxGMlhvR0NhcFFLWWk5VERDQ1M3MUo1MFBLMzd2ZVlCNThZYnVHdVRRcXJua1FVWXYvOEFzMTR5QkZweit6V1pOYjdYV1l2UG1EcCtBQnRTN2pMcTdkaXFzSmVxdHlQdHY0Wi9DNW1lWjFBTDh2NjZaZkZPRXozRXdOaGZid21kSGR2RDlhRjdrY21ydGtuUllWSXhwV1ZDV0dTb0RKcVBFYmZoMzNVTmFzamwyV2hTQWR3Q0JrcGxBUDJVV25lVURSRDQ0MmowVmVIWE0rL1F2Qkc4K2c4OWxlMDkvdTRWRXZLWkRKSHFrQXJHRXJ3ckdqRkFLaXFpbExlU3NERGNHL1d4bFU1d0xSaHZTUXZLdmhBOGQxYm1nOU9iTnEwejJKVFhHdFlaZmlBOUwrRk1TTlY0d0dHc0pqUnVWbTNSM3BDeURYSTB0VFZZNytKbjVoSnYxakt5NWtOdElRU2hBK1RpMElreHBXNFRiV0tWVk5UNDY4eEJXZVhMekhSSW5DSTJPL1l4ODR3YkpTV1dXOFAwa0xYbVpSNlZNOVVLSkVpdUQ3R2taOWxwUFN2SWF6ZEFZalhWUWxtRUYvWjhrNVhWai9EdUpmbTVXZUdjVDc1S3F6clhQSXlvTEdzUjA5SGlqdmhtV3BteGRXN29yU1VYb1o5Y1lPM2pGeTM5YXcrTkp6Nm1NK3V6K0FGbmFNYUdEQ0tuV3RrUlNORFpXSFp2T214ZHBWWTM4ZWhNM0FBVGZaQkxiZThJNC9ReUtneG9KVi9JUzJQNWpkTkgzTTZGZkFLNDc2YWlDSFM5TTNBeHo3czFjS285bStXMTZRWkI3U0hYbTZVL012U3ZQeWptTFE1M0VaR0ExWUNsSEFLYlVTcTF4VDMyQytBUW1Zdk5qOFd2eU82MnRUbWhiMW5HbmxqZEN3QzR0Qy9GZ3A2SXRKZDh4bzBUVHZ5Ylg1ZTZXRXhNYmUrTlIwbHIxZGdMakRrODBISkV2SHRCMFVlTys4WTF1VEFlL09TVlBDU3Ryc203dWZ5MFU4NkRnYVJiNi9MQVRGeTVJdnNXUVFEdzA3ejJzY2Z3NjIvYjc3L0RYWmJQNzVjalBadEtNMEY2NkhHY2NJNThobUlaU0FTVDAxZ0xFQS8xTjNJUkhZRWk5RG83S1hUdXZRK0twenRydmVqMC9nVEJmV3ZDdHFQTkFDbWlMeFV0M3BKVVAxdjdBOVUwa3d4anhxRzJBa2UzUXVTeTNlS0FuNjdQZ09paE1ZMFRweFdEUVdEQWZ2UlpNZldhd1A5V0FrS2VLOEhuZStvZnVzOS9JcVduTHFxNTBiVCtnYmRyVnlZODVKREJYOVpqc1FkT05kWFg1M1p3NXpBK1pnS0VScjFwOEZUZy9Nd01OZmxWb2xucjZHMG94MFIvOGZ3WG9MdXp6a0owSExXNzRIWXRkdjF4NEIrdThUNHQ5S1VvRFFYTWkxcmpsdEkwSm1UbHM4U0JPY2E1KzNWSlBNYzhRdlhrZUR1ZU1qOWJKT3JVZVNrS1F0aXdHelRRMmpPUmFXTXBIaTRFTWJENVFKNUdKd252clpYY2lTOXhkRkQ4WFdZZlNSUk15U1VxT3VrMWZmdU1ta3RrSS85dFdrRDFtblVBY3Nud2dDc2RBL09IYm9VMDNCaVVTR1FjNE5COVdXMmFMTDJPdUxVWW1OS2hON2lNZ2ZCbUtVZ1crM0k3UlNCV3lTK0ZNQmk4bzhja3g0N2J6bVFjRXRlUGYvOUMrTzg1T2ZaM3RsYjZJcTBBaFhOV3Zlc3c0OGN2TUhUU0M2ZS9VaHowclIweTN4eXp3UXhzS3pGcCthS2RTMzJVcHd2aDFIaCtWUE5FdmtpT041a1BPQ0pkanhsUDlQTmtPRzJqMHBib2o3cDNremxQZzJBalB4bEtCalYyakhBU2lFcERKUGU2S0JyL01wUTRZZWFnbDB3RXMxcTN3Nlp2THpaUTk4YkFnTDU4Y2N3TDJ0N3NIYWpsWVZCVkRTa3JxWXBMWGRTZzJYZDVHWjduWHpjbDVkdllnTUIwWkFZV0p6M2cxK1pkcVIxa0lMcHB4a2YxKzgxWUt4NFpEVXBqMjZWOVl4OTNWK2Q1Z1Y1TFdLdnFjRXRtM1hGdzVTaXVMTVMrakxabmhzakZKV3pqVklySkZmUFpwVVo5OXhSQXVCOE8zQUh1Z3Y3UVM4OWx1U1JxWU1ScFdEdVJZT0NvVGJsd2tlZ1BTRlFxWjUxUHl0SjIxQmttb0pWdXIxQllEazErUDB2b201UmI3RzU5a3dpYmNJSHpXN0tpc0lTMkcvWWh4WjFETFlReFRUalZ2VExoRVJIWmY2V0Rla29nSUxmSzVmUWhvSzJ4YmI2Z1JEajZlTEF6MWMzTlBjUFR4bDhQVk1QekNVL3I3VUltaHNYdjFad2xhSGtiZ3MzRnpURlBGbyt1TlFOYXAwd1pOREtWWmlVSEVrQnI2RmpLMEhQMlM0Uk1ZUkZIL1JhWklMcGV6YTRpcnNTK1lDVkxZOEd6REdrbXR3cStIWUZYY1BsTDB3TGlsTlFzNlZmZm1KZVZtcHFMcm5SUUI4aFBqaGJBdklxL1JLcEpSTGJjdko5V2pJYkxySFF5azIzamJzb0FIR0ZmOG5SSHI1N21mUnNZdGVUSHRaU0JoV0pTbFYwdFdITksvWVpsME1uSUVmOXBBZVhMT1JUYnBjRXd4WllkMkR0K1BpSk9xUlJiNm1temJyVGxmUUNuRDl2eUNQTHZNRkdhVzdzVFJTblFPWFRacVA4TDBpWjJvWFQxQ1ZNTXZmVUdHWndnZHgrRnV3SERjUDcxUzlSdm1LN2pPSzFDeFFIT0kzaWFKQVdybEY3QnNGcE5QMkdBdnFaY0hpeHh3Y2xMU0tFeHVqa3FjQXF3UlBHcTlWWDl3Z1p1RkhGRzdQOUp4eU1mZ1BvWVhJMDIzWGkvTE9NdG9xaHhFQ0xHeXpsclhNQUltZHJYZjFCT3hGcUZ5STlqQWhxQ0FXWHAra00zMHVNbHhJQ0w3YmVkK2ROaHBjRWZ4YXJmVmcrQkZ3VXc5MkwxWkpDVWkyWnQveU5xN2pGYzBmRUhweDkzc3l6bjIzQlRndDJqcUEwa0kvRmUvTXJ4aEcrTUI3dGo1MjljUGx6MjUxQU1QanI5Smk0ekFXOWhVWkVXa3BDU040RUxadmVLdU9YL05jSWY2ZlJXaG44N2pPMmdvOWVLQmNYRG1Ca2taa2lwS2k3Q1ZsdWYwdGllZkgxbjhWNjBTeGFhUmZJdjllcElaenloNC9KODIvSzVFRXZ3azdiTFI2dndEYU40UWxqR0U3bFJvUXU0dzRpQUh4bVFYVUR6WU8rTlRVemtIT3BvOGxiSDFveG41RFZnVFJkaUdSRGhwWUdHY25CQUtYU25aZ0RzWDlXb0JSR2VHZlBRU0VaYmtCTzZqeWpFMU4wcDdJNTg3YkRlV2hSb0YvcnBIRDBLalBmK0cvQ0lIcmlCVFlnSU02dzZBMVJXTng4cER3VUVlbWtFcTd6OFFzT3QrOEppSVdlME5YdDRPaTMwaG52cHhZTENsNVpWclFSYkkySlQ5clNLeTVsRGNJajdpeUl2cEZBNHJ3OS9qZFd5VHNzUTJkRVdaLzEzMFlib1BwUWdJc1pYRWpBMXZIZ3FselpjYXNJZzhsMXRaSUZjNGVmMnBGRlVmOFVaY2tRMDFoMmQ1Q0QzZzJwUHpGSGprLzloU2JJSDJScmduYmdhdCtuQ2Nnd1RsRXdGck51MG40VkVkd3QzS1dla2FPcHRZL3VreU1kcEdSdUVHWVZ5RFBLaGFvTlh4a3A5Nzd3S3dnZGVUOFI2NXNZZG1YaEZDbkFzYlV3bDM2dDM2ak1YMWRZTVJPQU9XM2VFRUJnYjNEcE9Vendld3NaNDlIaWdVbGxka0Jya0ZoTUY5d3dXTzBHb3hveGhHc1lueDZmL05VSVpFa25ielo2UkVwLzFTQVVyOEEwN2ttT0tvdHlCZVllQXZLT2ljUU5tYitZOWJPZjFpb0ZTOTFjeGVuWXpsVGpWd1gxQURyZ2J2WnNZWVREVkY4MCtXU2JzZkd2RTdYZ3pSS0lUaUFzOFpoOE9WUnNtb0dQWHJTYmxka1lqSFhwN0RDWjd5UFl5NTFGaFowZjZpRk9ndyt6NytFYlhuYy9TQmx0TEo0NE5DUVlVT1pkeWxzODZUczZhWUNlTWNJZmZhcDNhenhoVnZMckl5YjgrTjdtb0lwU0dBZnovRjBybUowWFF6SlNhOFF5d1g4TFdqZnphMnBrei8yUVVNTFh2cVhLUStpa3Z4RHRCNEVzNGpWVjM0K2dmMys0N2tJemhYOHk2dGVhZUI1Nld6cERTVFAza2FBVEk5VDNmQlQ0ZXZHUzVOSnhaSVpBWmNRWjlaOWJLaTdOSmlqcHdYdE9ucXlCWEFCNzhpRTF4QWZOeHgvNnYzOWFCbU9aYVBKVDcxL3k4bUhjUmUybWhJcXV1alhNRlh4V1V6MDlXWk55a0V4WFlUeTdDd3RyRkw1K0tCN256MHRqeW5CQkJzbGtXZ1h4eGR6MXo3c3pQOGVGdWczaHZrWUNGV282RFArdmlYQlRDQTkzTEcyUjBHVHFRdll6S2FobE1FWDZlTFhJcERWR1FvMFlhdWZ1REkyalVld1pQN0tRdHVhNGlNbGpzYWxGZnhqYXpoNTVHM3BiK1hqa2JlWHdPZ1NWejVHWDNtbDdyd3puRWtsdXY4VlJHOVpzaDRWL0NZK25XZzRpZzY3YjA5cDRkMHFGTkpSYkptWmVuVXF6bnhYOUtMdkg0aW5ON2FzZS9Mem04TldQaWw5YUs5bDdLM1ZLVHVGQVpCcWsvWUFqL011NkEyZVdtM2I3MTU1QklNRW5iL1JUYzY3dGNQTUhKV3krWUIrc1VMMVdhUG1wdEIrRzJGTjYzVnNhOW9yOWp5ejZnVEpNZEtVNThUYXFjTEMvVWp4RjBUbEpQa3NENUl6Uy9lWXNHdWVYSnZtUFVwUGxVQWh2WXZvN2hFc0dVZm05ZnRFMDlGV2cvMk9Gblc3cFpDa08xdUVqNnh5c0xpblUzcCtjVTg0cmNjZHNrYmc4S2RLNFlYdkRBdk9YM01ZS3ZkZVRVeHlPY29KNW0wL3BrWGVwSjVPM2xqM3ZMYVRGRk5EVlltb1Jmc2tiZDgrVnVoVW5EVjZTNS9pZ3FjNzNrdUZEclpDYU8zdktTZFd2M2JTT1FydWZOOTNKY3pXYzNhTkEvS2JYTS8yRnJiUkVSYURKUnNRdTNRS242Z3NRa3JZeldCbC9SazVyUVpxSktIMmZzN0VDUEIraDhTMTRuYWQxRk1RWFNPU1djRnU0NWRNRUViY1pYTjBkU2xDZU5TWGhKcSszczJpMkMveVZlYkUxV1dIYlgzeVRmTzRVNnU0V0t3eiszUGZ0d1BNY0JTNzZXYXk0bkM1NGFySTdIMXhXbU5CTDB1cTJvYVVQOCs2TmIzWStKNEZLbFpUZG5HNlVnT0pua0RFZ0M3TkdmWFVUeGxNZHBVZzBvY29IL2JCYUFodHZ5NGtwNFRoK0JRZEJLT2Iwd0tPUFRBTCtoUWtNQlowUkZmRGtoVXQ5dzdEOFNQbDV0R015cDhpbTRzb0ZkODFvZTNhNW1TQmpIM1JUbUlNVUhWT2hzNjVTMHcrdFU2N1FSbGRxSTY5YXVCc0taa1NQdmlRWG8zdjh2emFDRzFSb2ZEdTBPMzdPMjA1N1gydnhqZm5YRWRFczJaYW43dDBWVDQ0NDhML0pWSi9tTzN0T3Q3MFhlOXVXa3Y5VVZSd0JOaXVBci9DNHExSjY0MFN6T2lObmJodFFjMTFhNlI3WHUwUWpQTzZTWVlvT0FNeExMbmMrcUtvZ25oWTN4cmhaZVVJSENuVS93bVhoSG5McmtwSlduSWEvcnBQQlNMQVE2ZEVuRDJhMlVUUk9taTYrOG5QUWxZd2dDQURXTllxbjMzN3VacDVzNVFmRm8xSnZwb0NxVEpVellsbGt2OFhJeUpXRm83c3J6ZjhFdmZYVWlQenh4bCtVVTJJNmVhZzUxQ1dIR01UVC9QWmlnK0RvQlF0bmVhZHFwRTNsVHpFK2ZsK1ZBNWtMYXFXWi9Sb25VUyt1eTlZdm1BMm8xS1o4dmZDLzloQXhyVjFhZ1JpeStWUzJ3bjhOMGh0aDNQUDE5VFZwcHdsbUd4SUhWWXU1SWJ3UkVyQlZPVGQ2QUhnT1R1YitCbzMwbktESGU1d21kc08wbmlpTGxkRGFnNy9iQXVJaTltZksxS21qbng0aEFTNjJyTjd0V2QrR2pmYURYVVY1V003RDZJSTIreDI5RHF3dHdPY1hLUDBKczdCTzUxdmlsbWxFSUl1VEJXbHhDblU1aElSejU2bFlFK0F6ek44Tmg1T0E2ZXRuS3psWmNTR0NHaUFnaDIxN0UyTCtHK2liOXZFVjlrbHVWZ3UxeDJqUkpoNlFrd2pOSFFwTDdzY1lSbnJ0V2FQc3BXUDQ1SUVOTTlqeTZuOHE5SkM0OWdyMXlkRjMrc3EwOHlkUURhRTdKV1ZiV1MxQUpkZ2JhRGYyUytYVDBLZ1djSjBkSkRrZGIxdk4xWXBJKzIzMEFRTHJDcEFtREsxQVJxeVVXMm9zZXdRMFpiaXlzbEZsQUR5dG5aS1FQRDNkeHFXYU9Ka0tFaFNyK0IvR1IwQXA2YWZPeS9qeE9GdmJoRy9ja2llTUVnQ3I4RW5teFB2cyt0QXFMRlhxZVBlS0NzaHkycFkrVTFYS0dYSHlLS3cwWXJJTHBNYnRXRkFnOFg2Q2JzcS9xNHlRaGlWNUNDVmdBU2NCWDVwTTFjUGdvWldNS3RhamNWRzhaT01LbTUyRDhsd013aFJ0d0NsQ1crMUFIOURQOVZvVlhndzlXaEdCb3FEdmtRQVo0cGFGUlhNbzNGd3ZnV0RScXIyVkc4SmNIN01GakRnS09oUXlSL2pVYmdrSDdJbGFLTDJGZ3Nxd1RCdVR2NFhFT0UvZXRJR0lyakYvOHR5bGprRDVpTXFJMURQWHA5WGtiM0w1SDRBQjEwSXhYSjNoY3RFWVFIMkVIMitlbm5uU00xUHo2LzN4NTJiNVZ5RFpIeFFQaktFYTlNY0ppNTJsVG5zdmQycnNBbnh4dCtrLzJscnZuRmFUZDI2RW1DTFFsQm1vNCtGQ3FtMGRHZkxTeFhxZHdESWZDcTBGUjF0YWNwVEI1a0ptK1FsUFBqMEQrMWkrTWc4ZWgyYURBU28wNDhLMG4zYXJMOHZWS3BSeTcvTnU4YXhXaE1PWnkyUVd2YWhtb2I1dG5YSWMzNSs2RlNUQlB6aUdXZS9Fd1huYnVxaXJXNk5RbWYvYitvZnBWRTZPa3pudm1LVXE2NW4wZzMrMGp1WXBIa2dyRWg0V1pUVDdFMEQzcWZQQjlla1hHQUJVRDh5Y09qTnNpYmgwZm1hOHBDYmhlU3Nuc2hkM3owYkZwR2JZdjc0MFhCdVV4eUx1Ylo0cUU1N1d4RGkvZ1cvVTJWUjRZclJRQTFWSFZEVWpKNEdoKzF5RGdSSFpiQ2FiZ3NhcVg5OHRqTUtaSDZJZjRDQlBkTzRZTnRNK2ExdzNQK0MxcjBYWEFvUFVEY3dwZHhCeG9JRXkrWTc5ZE1UMTc0NHBjK2hGQ0tCQ3J6anQ0U0JCb3pZNW41S0c2YXEzQzAwdzdjcmwvM3B4QlNnbWJuVmRxMXRhTGgwWHV5VW9kdU53T0lFWXpEeXBpN3FwZEY3US9CSHRiM0l6cWhUalEza3Z1SG00N2ZNUk9JSko3RW5WVGRHWW9QQWNvQnh1dlJVWFFvRmpsWHRwc0VMaWhYUEdhVTBwUi9UTWp2eFVsL1ZRcE9hbXN5WGZIdzJnQjI0b1pqc0pnZ0UvV1libUVEYjFZeGRPKzN1Vk5xdy96TlI1OVc5MVlua0pCWU5MYndFVTI0cTJlYUYxd2M1VjJQRGhFMk5od1dqM1d5ay9qMXM3YStxQ2U3TEhxNUZGSTBCS3JTdXFzUFdLbnMybE94VDFPcnNkS0ZENG5Va2ZGSUI1SlVqeFh3RkZNOER6UCtmS0h2a3VJMUpibzhpZTZET1N6UE1NQ3Y3Q3FqYlZadUNxV3FBMDJGc3VNRFVMOFdnaklnRWRLTENpME9FZXQ0TmpYb1UvK0c5U0MrNXNiYjZJMXF0L2lkSkhETTRtU0V3TE5KbXh2UHNPS2lFYXllT1J5Q0ZxcGFrcTVhcVQ5VXg0a0hvbWd3dlZZSXczODVGT2tubVI3ZFZmamdOV1F5dVQrVTF5eGpQeFdFUytFMXQ4NHhqanZiUklVKzlEdDdPMVllLzlXZFd5RnNnYit3U25xaUJWZkJ2SUhRb3NtNytFQ2gwQ0FhVXB4cEZMdHNKWVJtTzBRMU1oSmVSNUlkbWpXK0g4SllTQVMwYStPRG1aVW43S09vOFBIb09CRmhRS2NJMWMrV1k4dnV1eE9UL0JUeFFDQllycSs5cldGS3lsSDBjZjcrb0VvdW91Y0xEeEo2R2RmcGJPZnFRbWRxcjhlWG1SS0NtWFdmRGllOFRNK0dJa3c2NEVYVGxxdEVucjFGSDBYU2x0MjFDNHR3RGF6cDhBTkJrN084R1o5Z3N3STBRcmVLYWlwTDVnRzlGcEp3b3FqSE5DVHJ4Mm51L2JJVkgzTTRZZTAwVGdZRVFTbW4vbTd1bDRMWkZSOFBRS090TGMzZDJha0JpN3RrMkxPZTRBeTlVOHhVVG9PcFV5NDkwQUNyS05DSFQ4SDNSOEk2NnRJRHByNWtpelladCt2TllTRzdJWStTSDZEY0V5bXRuejBmWXJYTGNzdkE4WTNNK1RIMFdVTlViL2VIWERHaHpQZnhCM00ybi9LY0pLM3VUNjdERGpPNDl5TWJmUERPalFFVy84OTlqdEkyVE96K1ljWFdxemIyeTRtQWp1UEhFYnFxME9CTlRpMlJSelF0SzFnWm9oMUJ6Z3NqbWdXNEpEZGtvVTFnVW4yOU03VmVVLzNFQXBSUlFBYlBIcFFUYmRmQ2UwaWdNZnJKWHU1M3dTNDAxbVd0VitYUlFiYVVuS2ZzUEtycFo2cmZmcG15b0YyeURQaDY3eVhGYklrS3ZVUWlia0J1QnBhclRRZnZ4UFVtdnNLd3JPR0F1SFloU0RmM0pJOXFOWStDbHl1VmhWTHk2Q1BVSnlkUDhEOG5wdEU2b0psWUNTamlVWXlaVE9PMVN1SVc5R2hRMDFCSDRpN2pUaFhzWnd1VUY1YmQ2VmRrYkdEakcyTS9IYzVBTE5qeHdKVHc2YUlEdGthS3JRRTBJL2xvZUVWZ3FuRVZHSVYzMUlEK0tocDRxckkvZGlRa1lUWlJTWnltR0ErdmZSMGJwZU9BMDNJQmVYQlhORDl6U29oNkFISEc2c1VpK1NWR291WkZicDlrekJoa0M1MlNCVGpnVzRselNEeGV5NStkVkR0NWNIRUhhcGpEVFZIcWQ0aFZ0YjI2c0o3UngzK2U4MDU0Mzc4Y2hhNE9zVkdPZU5WZnlVU2JFSGJLbGhmTkgxcnEwM0JWOVYxc2p2UUVBOHR2SWQ5dm80Y1ppNUkvSmhtbld6ZWszWjVCc3VpL0ZLZDJRaTZYRWFjeU9SSHRvWHZqeklPbHc2ZTB0YVNlYzkrSG53TDRuU0F0SkF3TEN1MmQwczJMSEovMDNKS2VuckxCbzJsOEhaSW1Db05KYi9rYmM3THhpbkpLV0hWQThUNW8wNlIrQlhXMjQyQjlRU0Y0VnVvbkZUZnc4WjQvVmZEWnFocGpMcmtTdjRNbzNwRlpHQ3RMeWdRclJTaFVXeUw3M3h3ZWFnSFcyN3IySzE0dG85Y1gxSnh1elhJUXkwUG1DSWFST2ZxN1U3TmZZTzZTeEtHRndxcXBoVWl2QWV0VGVYR2ZlOXVFdzhnVlFVaWgvYk1MdVFJYVQ2SkFKdFdacGZtOTUvMk9wRFNGQkpXSzlFZG9sRytZSUxYSUxENGJURnlZRTRrMFQ5U3J1QXl6d0NwZ1FzK0ZWb1BWS2R4TjJ3SUcyVGZWUHBtQW40NFBIdENaSlQ2Q05VZHpwckc4akZOOEJWZFI3TzVGdENjYnIxMHZ0OTZtZkcwZWY5WHRRQ0c1Mks5MW1VQlFCQmJOaTZWT0xHQ1h1dnNUQVAxTm1pVW1zemJvNTJsU2R6U01KK2Y3YnpxR2tqSUtlaWp6Vm5Ed3QzYU1FSVJpUEpWdFBUSEhCRUtoWTRJa0VNUVcvZTV6SVRhRzJPVnBmZ1RNQk1kVEJmLzJRQUJBQm9BUUFBQlF3QkFBQThCQVVNQ0FBQUFBQUZsQWZoQUFBSUFDUUlCQVFvQ0FBTUFHVlJvYVhNZ2FYTWdiMllnUm1sdVoyVnlJQ2d4S1E9PTwvQkRCPgogICAgPC9CSVI-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT45PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MVo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-SXJpczwvVHlwZT4KICAgICAgICAgICAgPFN1YnR5cGU-UmlnaHQ8L1N1YnR5cGU-CiAgICAgICAgICAgIDxMZXZlbD5SYXc8L0xldmVsPgogICAgICAgICAgICA8UHVycG9zZT5FbnJvbGw8L1B1cnBvc2U-CiAgICAgICAgICAgIDxRdWFsaXR5PgogICAgICAgICAgICAgICAgPEFsZ29yaXRobT4KICAgICAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPkhNQUM8L09yZ2FuaXphdGlvbj4KICAgICAgICAgICAgICAgICAgICA8VHlwZT5TSEEtMjU2PC9UeXBlPgogICAgICAgICAgICAgICAgPC9BbGdvcml0aG0-CiAgICAgICAgICAgICAgICA8U2NvcmU-MTAwPC9TY29yZT4KICAgICAgICAgICAgPC9RdWFsaXR5PgogICAgICAgIDwvQkRCSW5mbz4KCQk8QkRCPlNVbFNBREF5TUFBQUFNcnNBQUVBQVFBQXl0d0g1Z3dXRGlZWEFIMEJjMTh3QmdGaEFBQUFBQUFCQVFNS1FBR25BVDBJQUFELy8vLy9BQUFBQUFBQUFBQUFBQUFBQUFES293QUFBQXhxVUNBZ0RRcUhDZ0FBQUJSbWRIbHdhbkF5SUFBQUFBQnFjRElnQUFBQUxXcHdNbWdBQUFBV2FXaGtjZ0FBQVQwQUFBR25BQUVIQndBQUFBQUFEMk52YkhJQkFBQUFBQUFSQUFBQUFHcHdNbVAvVC85UkFDa0FBQUFBQWFjQUFBRTlBQUFBQUFBQUFBQUFBQUduQUFBQlBRQUFBQUFBQUFBQUFBRUhBUUgvWkFBakFBRkRjbVZoZEc5eU9pQktZWE5RWlhJZ1ZtVnljMmx2YmlBeExqa3dNQzR4LzFJQURBQUFBQUVBQlFRRUFBSC9YQUFUUUVCSVNGQklTRkJJU0ZCSVNGQklTRkQva0FBS0FBQUFBTW5YQUFIL2s5OTlIR0dBZWRISWFDdWZJMVdCMEFrdmZqZzVKaUJOY2FIdk4xb29PVHM5Qnh2NXZ0b01tbkNWRnVFdEtiRjhJNjlqeEs4RDFmTElLajZkSGlXb3pOUU1uK2RuMlpYZ2wvamF4SjBLRGdIdUJoRmg4Q3cyK0tCRzBnYkNxd3F5WUROREsyY08wRXVnNDZWWXZ3ak1qSythdElZV0R5ak5RWFlLYTMzQmxxUkM4NEg1a2ZBd1VYeVY5azNLdWJET0VsZkgyTnNmWTd3K3h1QTdPN0pFVWhvMzV2TmpKcm9rUzV6TTRHT2R4Yk1IanNXcVVrSWZKa3IzQmphRERTMGtmbWs2aVk5RjczUENKUkhBVkE2R0swUVR3M3ZlVStYandyL0ZkMXYraXVFMVdjcGVLVVlOZC9CUGd5ay9ScWlsNS9aQVRwQXBMbHVGWTZwNmNEV3hGZlQyaCtqbWhScy9KclBUTFoxa2trMTdEZXpwSUc1OVltdGY2aE4xU1VVV3V2cUdQWEVNUnFJQ3dWV3p2ZjVhR3RRM1o1b2hvZWxBV0w4cWIxWkhidUxIQXJIZ2xXcnFoV3BPbXZtWUYvSFJ1T2JuTEtramptVXlNVEZ5WFpZUitseHZhVXYzeTNvZHE3WThUTjZrcTBYVnIzU2JIb0lFSkFXWTlaSk9yRThyOTNwODNSUmsyZUFzbTlCeE1DMms1aWFkZTF3OGppaHZYTThjZ1hWM0RTNXE4cHE3Vm91dFlJTFlUeVY2bVBFMnlYaWhZYWUrTngzSmxmV1M3RUMyYXVNUnY5dThMd1lZWUM3QzJaaERsallMV3pMaXpjdjYrRTlMdUNLZnYvZkMwZ1F5YTErVEpJQU9XUHlIMzhmWnJMSDJhNFErelcrdGt5aGVkTVRKZkhic0VkZWlic0c5Y2RSTDZsaHlKcnY2MkpFa0dGMDFjL2Z6VWN3eEVzNFNFK1hKN09wNWdRelBYYXVQWnVoMUV1K24vT1lBMEZXMTY0T1JiSUVoNm5kY3Z3bXVBSy82czhKazBFUCtsdXNjMVBRQnpueUJwTWtLaFJhcDkzdXlCWm1VTjVXakZYK3F5SkJGelAzS2tWNC83dUhGdVR4Y2l5K29iS01MdGdtTzRSNXNyTHk4U0NVVlZ2Wmd6YkVnem5xOExSQjFVWXhYb0ppVUlQWmJoZExsMnlobHptQWE0eWlaUXQwVE5lajh3MjMrOTYya3JXOURtY3FLZnlRRnZsU0NxeUU1eFMwQnNpcy9ZNVkvZGF1NDIySzMvR3k1VVR1UHNlRExnOGFRNzloYlRXTmhJWHl4UHhtdlkyYUtjQ2NkSWNZQ2U0bWVBU3YzVnV5LzlHUE5acFlHcWlDQUJZUHl4Yk9UTFJnOEJtbmY5TmZyK1Z6aGJJbHp3ZXluZ3hHUHUzR091d1FOdVNWd2kybGRMM2lSKzE5NWNiSHV4OG9LRFpncVBpdGtlZ3hGL09YdmZsQStwRC9xUnBpNjN6QjBHaGlYVlFDZnkyZVQxNnhKOG90eUdxdUxtS0IzRksyeUJtdFJoc0wvZ0RDRmN0VlJYYlZVWEt4Um91SHpSK2Rjb3hWTCtRK21PTTAwRHBKYzloM3hyK2FEdkg3V0dxOC9GOWgvd0x4cFU5TDk3SzdnbW5RVUFOaFo2bjRUbm8xVkhwVTNOZUtiMGhWN3VXS3J1cFozRGVNa3E0cnh0OXNvN2RZdmEvZytBZ1hCdHpFMkdIZG94VW5VQWZwTXhoRjliR2dHQkJCM2IwVmhDU0U5eUJOM3NIdHhEU1dsdkV0dm03NWdHSjZ2VVUrb1lhNXFCTkYxd2loaEc4RnVVOERXeitqUm1oV2NtcWhGYU5pWFNCSmRGV2lYdFZnNGVRWHFHVi9zK3B6Misxcm1XMTVzMnM5Vk1yMzMxdUxSUlFRa0lUWXJvRzJyd3FMVkxVMzRwRklHV2JqQkh0TzhRVDV6WThabWZNakFRdUlyU3N6cGZsTzNqRjR0S3ZFT3g4b2ZpcE5EaDBkMCtsYzNPOGVGNFpubnZxR2JIMXJKK0dTYS9EZUpENVhYVTdTelpLZmJuc0JWZzY5RlpzYlRWZEhqckJONHJMRW5aS2JoMnoybW02OUZXWEpuWUVOY2lLQXJwakRNZTVuVWJOaklVSlF1QjJaY28xUXhSQXlBM1B5SGxHNmFqWGRVamV6blFLdGpDNTBoZkJnT3BEK3hYQkhnTnYzNUQzRThxSzFCcEpQdkVDTEtyQ1dMdkdJUkJ5VEkzYXF3d2hoVG5ZK3gzZ2owZGU5WkxGV0JFc1RaWHR3WmY2bHcyejNZaXluVHVKcXNaNlZjbjh6THZZWFBUT1o4dWRLajgyL3d1SEpubzg5cEhJenVvS1M0ZWFGcG1IYzFNN0ZFSnRXTlp1eXUzTG1rMk44L0wwNUFsT3VjTVhpNkgxcDdyaVNQR3NWN2VYaTRCMnZtRnBaRDZ5Zm9UTXdJNFR5Q1R1K1lHblRxOFRUOGNHU2liSk0vNVBUcnhQa0ZIS1Y1VTZldmlKYStTaVhCclRQdzNuSjJYUDVqRENERmdOSWI0eEdYU0pWZDdMYkIwcFhiSWRJeS8xTG44UXFDMi9LZE5wOENzSTJNRElBMExJdm5OZjFldjB5TTUvb0pPd0I3NFl5eFB0d0Z3eU54akYwbXpxYkxkZXJSUnRZTnd5Wnd6Nk94ZFBXTnFTOTlqWlBhN0tlWHVxTksrMmF2c1p6OStPTVJKZFQybDdNL254NWhCT013cnYwWnJqcXhQT2hGaUVjN2FzUDhrWktFSFJqWTgzUnBmR1hScjVmTklYL0gyK2xmSDJlbG5INGVtY0NGVE5xQm0yMktiSG9ZQWgzcEZOWVBRZkwvRVNiNVdqY1VEWmlGNWVIVlNOZnp2TEdQZzNIQ3JMWkxmOGVqRzI0ODhuUXJoakM1VklKTkxMR3ducndZQmtiM2RBaTBOOE1LV01MTXVRZVBrQTVCaFc1NFJXTGZPWlB4RlNLLzJaMlhTSG1odTBWTGc4RkpYRlZ0OXBuL01MZkk3aVRXTUdpaWN4c0RjdjhPMVRsalE1YUtuVTVpTVNNWVlwWVdoZnhxazA2VGhDWHVnMVE4WDhCTnBZeFdEUUUweUdqQkw1a2lYTUFINzBJeVpQWUloVlZ2MzhlYXBneXJpb013QXNxWG9mVzd0V2tyNFMvMGh6b2ptN0E3NmNIdzBLZGxPcENoL3pWOGxSZ1RpdEhlclVhdFVUVGlWMlVudnp5b2Q1MHVwbmI3R29uU0ZLOEFoKzFib2RoZ1hMSWdFLzBrd3pTaThkZFc3NXZZeUtiQ3p4TGVUU250OHB2UXFaeDRUZ2NvTm1TM05pRys2K2JmRkdMcnJDRTY1ZDhzV24ra1E4Wmp4RndjZktjN1BqeXBLNTBIM004YWV6ZjdYdVhzend0ZEFZNm5TQ1gzQkh5M0thME1VWHdUckh0cjlzTU5WOU9mTXhSVEU0WCtFWlRSTGhWbi93L1Qwc0t4aEE1RU91V0xxeE9manhMQnJTRlo5WmZENnZEVGEzT051Y3ByMTBGRnM2QXRPdW9SRWpPOHpFS1NjbWJCeE1IM213NXlGR2xhYmRBQStyaFF1RENYNmQ1d0p5dFozS1NrOEM4M21zZXJoL3hJU1FVMFdZRExWbC9jdXpXNTk2SE1QMUFSWHJSbWlNMlRhTGlmenh1anpRWFliNEVMeTF2THdaK3lzZ05oY1FRVjNzS0JtUGtjTXpwcDBEbGprNE9zemthVVV0Yk5vSzNlNFNNeW9iZ3BpaDlWdW5laGpLQldCWmNyVHVSdGRKbCtVT1ZkSHdma2J1a0xaLzRNMVViZWpUcjVReFIxTVRNaHlZZGdmT2xMZDJWL0FrOEhaK3o4YXZqNElFUWV1V1M4UkdmQkpjOFUwTFpETlBrSG5sc0dtNkJCY2k5eWNjejBacExHcCtiQm94eUdOMVB0OUdlUmYvWHk5a2VLQ0E5Vm5pcWFVK0xCZzJwMzlHSjdFSGpoVllENDdKMVJpU0hFblE2MXhvS0cwZlFQRzVrb3VVUllaZ1loZkxTdTV3czlPTUJEamFrL2ZPajZkUTBYdXR5WTRuQ2tXL3hYdFZjcnNSY20xenkyUFVSMUMxZVhJN1NnZ2tSQUZIV25SSVdPYjdxQ2dQbWpiZFBLc3QvWG5nTHcyc0RlK3ZzSVJjcGF0bGRlWGVFVGhwMmVocDRIODdIM1dDa1BoNGdZUnROWXM5N2lSWXBQMGtlaXF0dzVmOG1iS0M3V2x2SHJsaTlXZGlhaUNLRXA5RlJ5ajNCM0tzNmFSdzZLMUZIc3ZSVGx6ODIzN2F3YkhTSzc4cGFqY3FyUnY2dzN2ajdkamdLTkJ3aFNNb1pxcDBJL1BZdFNrQWhLVFFvS1g3SHVqK1hqWGh6TURUUjVNN3BMODNGYXRVbzQ0YW1vSC9qdGJaZlVCMWRlcDRMOFNZWGVuTUpEbit6L1hqQmhTTTd3MVI2czlIV2ppZzlhY2NaRVllUUZvMk41cVBadm9EcmdocTZjdWxra1ZjemRVdnZWdzZhWTU3WVpMRWlZcDNNaVdiWHNkUFFJaWFPQmYxTWcyaGQzOUpqcGt2VUsxRlVhSVkwUFZMaCtoVi9lYlBNa0hDYUVQUGkzcTN0ZURyYUczTldQaTJML0ZHbDZQOWpOek9oeU04SFg2MDEydGxkM0ZrakNYREVGNmllTFFnOXducDMybW9rVExVV1cwOGRuNXZuUGgyTDhvSExyclc1WVVDM1ZGZEdWWjFVODUxMG01cWRVK3ZYRUsvM25aZXhEd2c4b0VEL056Ny91Q2RHSUxqOHkrdjNXL3dBTWRNeVlEWU1OcklkZG8yR1dJK1Rpc010TDlqbHpTWGZQZkdOUi9YWURza1lHT284UjZJbmx1UE9kOWxrSnhPRFBzT2FkcDdDM1RnRnN0bDU3VCtYRGpvSm84eVdLQzBIUVByZG9PRm4wd1ZmY0czS05NK0g3UXlJWGw4WC9jMS82YndJVnRoZ3BpeXdTaWpLYVIyM1RVZSs1TEw1R21UWU5SVXBReWVkcCtrVmhGQkxNVUFxY0wzV1lTQ0dtYWVpOTl4cHlITWdhaGoydHR3dDdLTnA5OG5wL0ZQdUI5ZTBDaktRclNIdG9LNWxkV3ZnVUQyTEw0c0djQmNtQTVSUzJFcXpHUTc5L2RWL3UyOHZPM001L2M5Qk0wc1ExQ21tU1ZZamtpYmJiQUpoWUVNQnpzVlc4bTNnd0J3dnJwa3N1a3JPK09Vbzk4QUxDMXhwTWVJVllTTFphWDZWd01lV0o5ODJqNXhaV1ZiUS8xQVV1T2dRdkRVRWhJamZldllBRjBvZ2s2Q3FXTkx1V2Q5cGp4Ui94V25nQUcwaFFaSmpFVEJaNHBMV042RE1heTYvYlo3Y3YvcXlReU5CM1JSbkw5Q0hGY3BUd1F2ZmF0Ky9LaFRwanU2aFUvckM5M0RicjRGelBMQnZhVzVzQnZTMVduaGF5VVQ3VE9sSlhYektHZDZjdlhMWHRzSU9nTm9GRm92Y0V5TW0waDBwc1hiTGlzOFhObzdVRjJWeDZ0V25rY29aMVJacFk4YldwWVJHNnhDNWpndjhhdWVrSzBrWmRaWjEvMzROUGNvc1FSRkN5R1ZSakJkU3JScWZobFNqM05rYVJKYlVUTXRLTGpwS1BGMzRhaWtuN01VbVFHWFM3Q2JrNEZDcEliWVpyTmFKN3F1ZGdFTzY0SHA3S0RoMWNiS1czb2dUSmNWeUZTU2lnc2RvMVp0bGFBUWw4eUtHMVM4THJYaVI3aEZYbjBLSGdhYTN1UHZodGF1Y0ZEYU1Mc2xhTjNGcVNQb0JFSFcwQlB4WTh1dUcyQUw3VUVHbllMOXlqdG5TK0V5RC9jck40b3NSYnNJMkZWdXQ1bFFoN25WSEN6cHFyZmhON0UxMFJybWYrUkhDK216d1BlTWtOVUl3bjlSUThLRkxMTko2K3ZaNEtwSzlsdnQ5L01WNlVFZjk2SE9RRFBjNHI0eGpPVFVwRWtPWkRMSGhlUC85M25sZ2hmOFB3Ry9EUTBoWnBOZnJRaEhaUUZqYjVBNDdHZnVLVTlIOElqUndkRStwckRCMkc0b0FZZTRyL2NlU0NTamxGMHk2R0REVk04bjRJZWxSZENKRFNuK0IyNmxlRE13REVkSjVSMXM0UTJFcVdKaWV1VnpSODJTUUZObkhNMkUvTUtDSXNoL0VwNklZR3FMTm1STEJOOE1JRHpGYk9GTFZXMVZTbnV3TUZyTkZZaHRrdkZCdnQ5VzVjUmJqUHRmSTJjdXhLSnprbExwRnpwTFlITnZjSlA2WlpGMzgxb2lhYmxNOSttVjlzaXZiOWZXZDlmL3doMGVuTlI2Z015LzhiVkQrdHIzbDlPL0FXQmZDS0JDaHFLOGVoWnhzNG9LOFRPYVh6a1lHVTlNMGYzZW1zZUNSL09TZm11dHZsSU4rMkF2UGpNaFRncTdiSUU3THpMQTNMeGdkYVZVcEE2SVY0MkNQMUtlai9RMDJKRS85SnhKUkxodDRPaGJmU2lDaGk4bERSeFJrK1hCMENMeXpaclJzS0NNbnR6bG5Xa2FjOEt5cE51S2gyQUhTaUMvelRRWXV0UmdaUVN2VmpjcGp1OW9RNk5XTzBoUWIvVmZaZTRpd2NYUWRFbEJHSmtWTmNVK2tFWTJwejZrNE96Zm5jNy9UbDhHZHZZdEVJOXMvdk5XVXZ5UDlDem9rL3p3b1hvUDkrTWlScGNpS25HYUQxNFNKNlYzMCs0cy8wUEdRREw1Z0xUdHk5KzhRcnRuR0hjSGxzeU9FeVhndDJKVi90RDI3MnBXSVdPeEwyV2RFcWJVNzNpUTdnWG92VERHcTVRSklIT2Q0MmJKYVNrS05kUmZJV1NuYk5VZkYycDYvVmJUUEdENm9UNUhoa2ZHUTQ5bjJ4MmVwaVVqSmpPNENRZ0grdGtuQUQyK0o2WHRPSExtN0d2WTlqdGtQMmpPOUNYZGdNLzJYSC9mSWVMUzVwQTZiUlJpRXNWMWh1ak50WE0yTjFiZHVUaWpoNUpBVVdaZE5wMnZrcU41RVhLenE3VG5pM2MvWDZ1TWpqbXErVDJvdFNzQnR4Yk91Zkk5MDhwMEhzTXNDV1p2UVpxSnA4RnE1bHBRSDk2ZXdqbWZPbTFKaFBoeVU4VzIzNEQ4NElhZkxqNTFjRVZjOXc4MXl5QitDTUxvVHdBS1BGV3FHSWFKVkFVelpqSTh1UXRFOS9YQlZSK1cvYUVsc0xDV2w0VzRmTW9tdzdkZDFXVDJvWG1Ca1VsMkdkRk9vWXBNc280T3Q0VEhxcDBSS0VXR2RQSUFKaERad2tYUS82d3JDM1hPQUQwdjczeDN3YnZVSGxpM1J0L0Y2WFlNaG0wL0FSdkJmQkF6ekkzdnNLVVMxZmdRYi9OZENjblRrUi9tdEc4MDBYblpUSkl4eU1pTkVUU2lOQWJPMGxoaW4zVU5SZEdXMjFMVmRidWpPU01HL2E1OWFlQW1keExaV3BNRkJoM3h1a0l0Qnh6ckJzOWVnY0Rkblk4bzBvM05OVTBab0NMSStGci9PREZaT0FDb0QvZ2NPak91L1VPamZFaEhqeGdodnlwbnJOL2R2U2FnM0pBa1YvcGQ3ZHRYeWlUK0JjV3lvZC8xdk53SzQxbFF4aXlESWFLbVphYVR4WGdEUk85RTVUZjk2aEpZaG1zWi9WMjRVR0ltYldWMzd3WWhlM0t2UzVFRVpsT0xST3VmWmZGYzlXSVJ0WGNSdU5yOUNkMXpBeHlLRlRQK0VHSm8xdnlwV2RWL1FOcmc2S05QUFcweFEyWURRRnFqREFJY2VXcXE1cEM3STdEMC9mTitNVHE0cXN4WUduczNhWmI5NlR6WUUzL0FSM1hVaDhDNkdUbW0zSHY4cERKUXdRa1BUb3NSNTYrVzd5RmpDaDBOcVhudFl0NFk4QXhoMGFoRTZ3OUZhZ3J2NnVnT1lFQzBNa0U5aERwQ2VpMnBSaTM5MjZVOFFzSjVXR1FuT0V6NzVQOFlaamh4enVUbHAzcWNVcVRDNDJ1TGx6MTBZUEZJSHl5bys2RGhxUndReFMrZWptK1dST1JtN3dxWm1RWXZkZHY3a0xrVVVZYXZ4Rm5kNHk4Vm00ODRmZnZYRU4vZk0rMENtQmdhS2Y4VEhxVjNBUkQ5UUtkaVJHMG5GTzZiZVVNRXM3RFRpZndjZUJ3R1FCZXpuOCtRSHRhcWp2VU9qSXRndG0wTGRVbC80NndHSk1kUDNNL2FMbEhhd0Z2TGhxcDRoNkNibWZoMmRVUDRmTDFsbTdVSzFNaGY3aXJramtRblBXYW84R3l2WndNVGtZclRTTjBxcWxxR0VHMy92UGJEbHRCcXBlVTVZU2NTUzc1M2I3dnNON2svM2tBa3dlczBjS0NVS2NJZkFpYzFqZ0FaZm5OR01IVERjdmI0MGNseEc0eFEzL0phbkJ3MExHQmVDcFpFNTd1OCtadkQ0MGhhRFBBTU9PbFU1Z0pHb2xZUVVnRkJzM1R3ZGNFa2FXb29tRlZQa1hjYU9xUjAybm9OTk9yd2ZnUldRVk96alA1Tk5IclR4RVNzWnpQczZ2MXJNMjFUaCt0SFN3YlRpVUthWm5UaHdxOGhaaWZYUHFLMVlDUU5YMWk4RzJGKzFTVHkrU0o1cHJNbGJHYTZ0enN6U0J3Y1hJL3pKTEVLK2VTVStVbmp1bmtheFpHLzArY05TSWNJVWlXTlhCK2RmYUxPRC9oN242UjByYW4xWTlsRDFGc0labS95WXZqODhkR1RlR09HS1FVRFFuWC9ydWNVUWJuRzJIYmN5aGc0YzduQllSNFdIcUxDa0tWV25aZ3hmUGF2SGY4R1hISXlJaHRweHRDSURhWDNSbjZBbFN2NG1uSjdZL0NvUXFxVWVOMElveWJyQytVYm5jR1NHQm1iNFovYTM5ekJ1ZXAzUzV6amZNWWJaMGNEQ2VWek41QVd2OERZaU5XWXZ2dnpQSzdEVVR2VFJjWGVxVHZmRzh1SUNjUWYwZ0hzcEtER1VYOE5EekVuZElKWm1EYkhHRG82NWNqdlJwRWp4bTdBRnhjaFUzbWFJSGViNkhrVDNGSkMvU013a212TDJxbmhBSmsvelplbFBaTHNRYmZQQStDMDN4blQzWW1SUVFSalhzY2IrV2FzYnZqT0orZXR4UVF4TWFFdlhWQjk4NmRGU3AzTmkwc2x4RjBPUDIvUW0vdDlkQ1BrNkFUNTl5WS9iOUJuZlQ5WmVQazZIWDFYN2grMzZHVDl2cndSOG5RK2ZQdk1EMFRwRm9pdW13QXBVNzViMFpCb2ROenVITk1KNkhEWmt1YmpLd2htRWV0cGYvWGNzTm5adk00UENpaloxaTFOL0pnV3IzVmRuSVhNUk1CVVF5QVN1WGdyVHZINlZacWRMMzdyeUR0LzE3cXVFelY1Wlg4VFM2YzVSanl4cW90UWhLcm96dTVGbEN2QzZYZFhQV3VoR2dWRHlOVW5nMkNtb0tVM0FLN2lOWkhwaXVTaHdSQVRkMU1LQXFBV082TTloM0ppWFNNNXJ2b2tjbG03bmlwTE0wKytxdlVsa1VScGJYNEFnNUpQOU1tZUk2WWU3ak80MFdLaXorN0tDYS9MK2JjblhOMFZWd2hyMUZOL3hscFFiYmJWb3BEUDlvYTVaZmxTeVlnOWVheWZWRDVnQXlib0RLeGovTUVKTC9SaFZidWMwOFg3S0dsWUVod3ZadllhQlZ6T1VpQ3o4cndsQU56b0lGK1lpMG9YQ1RsSnlmNXpqV1BLZjd6ZEo4bm5aTjcxQU5DVEYxOVFyQ3FkdGI3RlFLMGNEaTRYZVozb1FKMU4rY3kzUHdGbllkNEFGRjFpb3lLd1l3MEk0SUZtYVJhQXQxeW1BNlFaemg3NTRjVUlQRnRDOFA2ZXhPa1pNUnowZjdMeVZYdW9OQmxrdHZlMUMwM29iQUl6R2VNM3FBMFZCS1ZMOGRoWjVmQjZNdEFGSDZPenUrK0J0WWFSSzdhQk8rNGdpTVJjVi81VXBURFZzeXFnSkZrdWNCYXZrUXF4QnBXRnR0VDFubytBQlI4NnVpVi9XMFk5R3J0NVVMbUFpMjJZU3VZNXo0bnNicUFZUVB2SElMV3lMeUY2RHkyZ0N5MXVMSENKaGlpSENjcC9WMTV1TjBpZXJ3eno3Z3hsQThOWlFkRnF3anVYdWo5cGlzdjlaalF0Zys3dHJZa3pVeWtkeCtkWHNydE9TWHF1RGNMN0VoMGVxa3ljRmNXT1YvU1FzcnFZZVN6SEY2bFEzMHFCMXZUbUc1Wmw1M0NVVkVUc2l6d2NYSGxoOW8rZUlzMFRJQWhsbzcvaC9TZCs1NHF5d2ZpZnlkSU1SdjB5Y09VSlFLeWJqMGc1WURRb3Z3a0FRRFdBeG9nSk5uTW5BZ2ZzT051THA3cVR4TTNMc0diT205RWhuRnh3WmVpWnBHaWdJbHFOUkxNNHRtQXdFaEp5V3RFaWhMNEFyWEFFOXBWTms1VmhkdUljZExLbktGOXFYdnRML1pibk9mbVJ6dGNFM0I2WlB4bWV4dVNZdmtMS0pmU3BJTzJKaUY0ayt2UWxFV2hsRzZ3dHJTbWZwNEhsR1lveXdWV01oakpLb2p4R3N4c2pObmo2WGpUWDVkY0g5SE9LTTdReDdxMkJ3amVFakU2QXlBYWUwSzRSQUdkZll4cC9JbVMycVA1YUZsbTFvZE9LSHRoU2ZtU1Jqb3dmOC8zY21KRlpZSDMvTVBRTE1ybk9obGJVOTNRaHJ6ekRYNmRkcWxSbWZJNlpQdE9RSkQyR0llM3hPNFhjcDJ3ZmdDNDh0emZrNmg1YkNtUk01c0FTREZ1UVNiSHBmVXNGV1VwY2NRUWVDYi9OWDB6T1h1ZURIZjRaVUFUS0IweWxEcXg4aURMOHJZNCtnWUk3M2VrTnlRUHJnRHBVNEZLYmpMM1o0WVQyV1dHclltbC9SN3BCTEZqcytWNU4vdG9ENk5JclZnMVpzcDNJdm00dklaRzZYOWtwNUc5SWI1d2daclJRbVZRTDcvYlV5WFFCVWNkMmhNU2ZZcXRGY2xtTzcwZmY4SWx4bGw4a3R6c1NEcTR5R0g0ZXZJdjQ3L0pBb0hpWlI1VU9nMjlxK1NzcjdGdkFZMU9ockxnSkc5VzhrRWFRekVheEtWdnNHaXd5TFpzLzhNU2JvUkxTQWNSa28rTVgxTXptamE5ZnowSU5icnRpVHo4RTh0aVRFK0VSa3R0dlRDYk9scjl4Wkw1MHRoYllrOGhza05Md05Majk4VEFMUElSRmQ1WFRmanViZDNvL2xoOHpxSlVGeEVHVzdsTVZwdHEySFFxcGorcGhLOENMSmk2bERmZVpPNmdiRXJmdXRsNW5nWURGZ0E1eTk0TFFoZ2hQVmFTVXpscVdHenFneVFQcVYyakc0bGlFNy9SZWx1R0FON0dUOWxsNXlzd1RDVDBkNzN4NDVId3JWbDRwQUhLb25hazZjNDZ2Qkd4WlppaW5wcndaYm53bStSeXl5TDJSZmhUN2VOR3FrYk03OGRvSDBSMzg4dVVEeUluRTZ6TkluWlUvZGNENTI5RTdxREEzRzhwcEJtek5MSGFhajdlVFRVeTFtdmlhYWxSSFpYbjcwdmFVcDhWU1lmZXQ0UitHR3JaSUNtaTVZYTlHbmkyajFjYkN2cGxzT3hlZDdoRTFZM2l2YWl0ZlZFcm4xL1o1a2VGUytEYTk4VkRPNzdsR2ZQZWhpWWZhemlsb2lDNmlNdmMvQ2VkTUszS04wYklUalBqdmlIUitqSnYxL1ZvNU1iVUhPM1JIZmRnWXhvUUpQMjdQZitQdGYwZDhlVldYdHhYbmY3N01Ubmw3Nm93cnUxOEFPYitaQWN2eGZ6NWFCQWQ0VHhqMjVoVVgxbFFXT2g4cklSdlRCaXpPeTFDVU1mUkk0eExpeG5NL2VQZUpPRUlVQUFyZ3ZmbnlSaFA1a2NNMWJ6NU5aZnkrdzdTRDZVV21LcG9xZ1dBUWdvV3pSZzF1VGlOWWRaeUVhdFhQZTBORTQ4c2xweDJLZW9iSGZmakNKOTBuTWZ1NVliVkZic3BVemlsY2RhNlZZcE5QQTJYcC9IL0t2SUQ2UkplaVNEQi9iU05xT0dVeWZvWDFHVU9DNnFWMTNya3JmSWx2RUIyTzJwbDczbW9vSkZHYjRjbnN2ckRwZ0xNMzJKYlNsSkxVQlFWa2dhbURDdXZneGgrZFFSM080d0c3MkdiWUxlM2VKWS8xSzlUWGU0L05UM2h1Q1ZhUG1ZOVJhV213NzZpUFhRbkJEOHlXSDhLSTh1SzMwd2Nxa2tEaVhaQnZZYzJIZWtHSEFaSUxQbWZ2eml1UjVjZzZWNmw0ZGl5MkJBMXZVdFp5R1B5eTB6QWVqMjdoWFVNeG5jS3hRYXZyK0ZBSVVzSDNOalY1WUZGc3BZTFhXSDdhL2YxclhyYkEvRWUyc3dnVnBsaGFkeVFoQ09PMEJKN3E4T0hoc01NWXJhVjNpRFdmNWthSWZteFZ3UUpQOXVCKzJFQXg2TUlEbFZUdGhiZk5KRlJBWmgwVVdwOUk1M0U2Q3dwcXoyay8zK2tHUHh5YVhKV2hjbU5IamNMU2t2SVFGbmlhVkUvWlZOcFdGS1JBSXQrZlBGMWU3YVJyQzdlaGUwT2lkQ2ZjWEt3U3NPTGhsOUl1VnNTQlBJVFRQQzdkZDNIaFpLai95ZEhmby9USjJoSlp4VGt2blJSMTJEbmtVUURMRE94ZUtsajVNMVVWQWpPbGF3SUVqMGRmUlhEZXR1dlBvSlVBY0JCRXhPMTh3VUdJaUg2Y091V1FCZDVNSkFDbzN3cG9qZzdsL0JrTGNWUGtJVGoyZjJHZU05c1I5QkdtQUI4SnJIT0xIVXRRZkVmRnBHaVNkQ3VKb0lETTVlSE94cFhVYUI3MFF6bDU3VHJab2JYTnZ1OGsrOEdXOFE0U1hTNklGa0t4UnpSZkZzS05mbUdWZVRYSWdEZ25DL1lSYVNIQW5wZ0hURWdjb3MxL1Jxck5BUG4veXhDYU9mOXFYakluOGNlZHU0Umd5Tmx1Nkd2enNoMllaZUkwWmk2bDZIVXNJN0JwYW4yZ0t2SmFNYnRIS0pwamtzYk0za2xyUzJ2ZHFhdUgvaHB6dGhQTnFpaU91SXdmUUVqTDZheW5EMDRLOWdDWnlqK2RrMk4wN3FOVGFneml1UkhkWHVFS3dyOHRoRUhOUlNmTTZZM2Y4R09PeXNoMzZhUjQvdVdoTzFSTUt4RCtKcDVIdFREVnZzRHNDZGRCSlZZWE5RekpGVGVzdG80S0xQRGRQUVRZUW1UaFcrN3Z6b29UNStPWlJVRmhtM2U1TjJRN01taHh4WUkzNTdiUUFmRzErUFFhbGVXbitSbmE5MUxSWEkrMExiRlUxTzEwODRhdEJJS0t5STZja0ZUVlRLZisvVGx5TXhxL290ZVMrZGtGVHkvMGhHZVJHc0xYYUpSTGpNb29XOC80MXZmT0xFQ1ErUGNhenlpWnhWTzRFQVprUklaUUthU2syUnJLaG5NS0ZNK1UySks4R0tOWVhtczVidVFidmdCbnRTcytYZlhNMWZMeENhVG5YQnYyYVBvaTlwQ21KTGswZzlDUE9YQ3JmYXB3NFQxUEtzZTZBelZrSXI0aEI5THdkU1pWbFdMdEQ4ZHJuM1ZsSHNJeTB3dk9WOHhVamZrOTlxK1JqVWtNa1RiaVZTNVRuVTZNVlhPUEVCeVZCM29WbW9GdHBUMDJWbVdOQlNGbit3aGxnMHFkSnU5QTF3cWZ3VHovRG5acmZEN0xNdEo0M0RzQlJHOXppN0ZoTVJtVjNJdEluSm9NYzcxc1c1ZGx3S3BTZlJ0VlBhWThsdjJPV0xNdS9yTmVGN3lybVM4KzZZZnUvWXZjcHplVUJnb2xaL0hSTi9UdnVXdWJuQys4cGdITEpiR0pBcURwOHVWOWNlMXVwRHR4UVVoUmdCRW4vcmMvNXI1ZEZEaUdBMzdwSEd2T0ZXbk9NazRPMG4yaXlIVnZGRTBjdzJGQXdZUEpGQ2ZnQXRzb1hpdUcyZ3lkNDcxRS84YWVMazJlN1NxcVVRNGc0ZUxHQ2ZmN285RWRwWk52cUJrQmV6N2R0bWo4aC9MNzYvdS9ZQktYNUhaZ05GOWM1cGxYNUJBUkhSUk9GVTQwaTA5RXZpMVhUaXNsWEpwb1h1TnNpK0pxWWVFZTZhNjUxNFNxZFBoTjRFQ0tLY21yRTl0UytTVnlFampNYTVWbmRPYWp2d0o2RSt1b0FqZ2FCejVocWFHbFJ2SnlsODhWMjhIdy9lYVJPWk1GejB3UWt0RFZra2NZdXd6cHpWdDlESGZVeDZmZXBzUkJvNTYrajhET3g1a1lrbHhvY05hWFlSRjVucUVpcUMwYXA5U0RKSXdlNElneWo3TGJFandhMy9lQldZSDNVN1VkcXFMMnM5dFN3eFRoSXVJTzhjTElQZEFuM3BBNEt6TFdDZTcxbEhDVjRGSXVFR2I3aExXcmpQWGdONCtKRmJ2Rk1wUTFmc2ZuWlVnYjcxSEhnaVVvUW8yOXR5WTcxeFFsZ1Brc2dNeFZsV1RjbjREUzZOQVQzK3RlV2laVEVZRllIU1l1em1lVFZLaHBSbFJnUUpKS3NyaGhlSEZUTXRaemZCbVE0dVNYVmhMbG5VUlJWSW1XeHdzWVlpZW1YSE1DbkV5UE8xcEtCbmRZV2c1UU9qMjBuc1o4SHRuVEx4Y1N0SkZPQnM4M0prb2dmUm13SWlZZElmTkFQNWp3RVZ1ZUorVkpiaTlXam1CQWgrLzJNNkZNT0J5aW83N2VYTFdzNFZReWRjZXYyR1dYWlduWVJsWEhLaHZlMjM0QlJDQ1JZL0MzYmdWWFNiL2VaSUxiQk1NTTFBeHduQjl4bzYxZERjRExNRCs4NnZrbXI0T1pnVXIvOThlQUFOdkU3Vy9oTHBrblRoSWprRDNDb3BVdWV6V0tHcWduaXpSQ21SbEZsZXE5NTlza0ROWG04Y2t0Sk1DTlBtemJCWGczVUplNm11QXFHRTZjWXpjWFREVlYydnAxRzZsQ0lnK012WllrdmFlR0pIV3RYWFI3V2dFMUFzbWZFRCtNUXJTQzZzZTVPc2hGRmlBU0l3cXhGdE43VHNkSmFWU2ptcXZXUUdSSWZjd1A1cFJqQnhBSWZ4aGZTbnorSGdhbWF1WDVrM3VrSHZSdTVORU5Fcjh0czVWSjhuRElCTWFxdDBlWXFMemU5MThOZkc2SkYyZzRDZzdNZnhleDdSL2h1bEEwWjN0V1UrbUM1ZzZNMzdsWEhxTmNKVys2TzloalBLck1FTU1NeWRyM1I0RnZueGNGUlJyNXpGZmNrK2hKaFY1SXVScFJOVEdORHMrL0FmK2REaXBVR25aejY0MGZmQ2xGYVBwZVFaMmh1UmJoRDMxZTJhQkZ1NDRBRzYwZnAwMXV2Wk5FakhtZHg5dmViWjhBb040ZDdVKzVGQzVVWDhXWm9zVmo1b3ZuaythcUpmei9RTjFoc2NVUjFuLzJHOHhZa0VaeUN0dnBIbkFlb3NYVmhDUXUyWFpHVDBKa2Z5Q0pyMXh5OCtVTGFVRVRCQVFIU0M2d21QRHEyV1RLaHhrYUVCUEdxWTN0WEI2cFVtWlI4RkFOSy9RYnNsSG5vTjFjM1NCSXI4aTZyVm83dldxZUZRNzM3NjQ4WkpadTNOSndORWFoSTdtRjBJL0JBcFBranh2czFMQ2NFd3RkWEFFbjdjdk54bExROW5rS3lpRjE1WU1vbDVCbTg3eFJaY2ZlaSsydGJrVEtJZ2xNdUlMb0RwUVpxNWVqekFBd0ttQXhSV0dsSG83WWhwT3FORDBHOFR5OEVqczRkTEdscmxBUUQ0NWZjUnJoUWs5N0ZqU2tHQ0dqVy9yZnpJWTd0QURnNjRXSlBzSm9GRFQvSVc3eUlHTzN5Y1VNSzVTL3hoa0pJbk9UYXQxUnM5NVVCaHRxQU1rdlV5S3Z0RE9oUmY4NUI3TkJla3lYN2lEWDFMdk9KV0xiSGtlcXFvNG5zQ0M2WjhITE9TdUppOHpCaXVwTXZLWkMwZWpXeEwvV2kzeUplQkMyVFZkMmRJQTlUOW90Z2d3TWNVNjROTHJxUnp1WFRGdXR3UlB3azhmM0xJUmo2T3I1djYralgzTlRhSC9mckxGU0NIUGlBMGhFc3g4dGYxNTJYejFwUW5ER1hWUnRvZ3dER09aYy8waUEzOWJBNnA4aXFNeEloaTcwaUhuTG5zdURPMStETno4WC9pb3ZxRTc2c2xQMS9VWHd4My9rVDlOOWtibEhhcm5QcmhJazZSbEdtdTYxd01kSlNBenQ3WWRGaDU4YkZZZkg4eGF5V0xZUlFpQk5SUFlJMFQyOEUrN3dlLzZPR28xRExhQ2J4cHRWNlhIcWJaK3ZmaDRxL0hES1FLa2t0U3B1bGxrbmdDclpJWkg3akFnOENUUUpYMGtIanhXR0RnRDRCM1lrZjhxWGREbWdaWmYvUldhUS9INDlFcEk1OURxMkJyZ01lUjdpSDNKdExVb1FGRmdldnhDRkgxeUcwTCs0SWVPRENpUk1GamhlQnZtZHZwSXNZNnZ5ZWdUWVhNVEpxTWU0QWIxNEcvYTAzQUIvTUlJWWl0UWM5ajVWTGxLUlc0SkMxWWpQc29idVNqVUQ2ZVBZY3NVK2pZZXdkQTVkZmxjd2VpdmtHOFUyM2hhN1paaGNOSmE3K2dHODFSTjRYeDNlQ3NQcmpHdEJXOHMwNWZSSStYbFplWVREellGUXJkT1cwd1hISzNTNXpQTjRjVUhjZmx2N0h5ZGlaTW8rTTlUK05IMC9ZVHJQUUhiRVV4cFBlM2JQU2h1TGZGMWdURlMwcGk0QXg0QThHWVFFeDRiSUdkTzNCcDBFWmRYNFpUajVxOGx5YUk4eXo5NWsrbW55LzhNYUlpV29PbFdiRHBMT3lmZEhnTG9HUWI1dTFmS1VZME5CNC9YK2N5TnNBeTY5OENiMjJDL0ZvYUZyOTg4bll3b2xTcXkvT3VHSzZrcThnbnVRWEZ2RDBSRWZYWWZSQ0QrN1N0UTJDR1QxQzBHdjFMVml4eUd1aElKZmNvTTNKNUM4SVkyL1ZkNGRJMGtqTlgxS25Lbi9BR0liVVlPTHVlak95TzdkanhWU0YwRTdKQjkzVURXSXdrTUZ6amM5N0JteUpwczlaRmJ6MmFROC9oMG8wYUJ2NTF6TEh1WmVCZUdYQXdIcEh6OW9reDNXRm1VN1JoelJadVNJUWxYR1hjajVJcER2MVRDazluc29USkl6QnlSNnhidXJURW9DZEpudG1idkEzL1FxVGhicWhaL3hXL3JZNnhFWkNFNnZRL0pGeVN3SmlFZUU0VTdyUFFrNFo4U2ZtdzdGOE5mL2c2SlZ2WlRmTW4vY21SZTBwNnRVL3c0Mk5ZMURhaDdjN2RWQTZpSHRkWk9BbXF4T2tBYUJabVNSVi91bU5HTVlld3V0NXBta212QkNzMjhHRzMrL2FwQ3NUaDVXUVdOelozUzM4SG05bnhFd0M2anFKUkVsUy8vQTNEUHF1enhHSmRMcmFidHF2b1F2cFJYMlIwNlZKSHplMlZSVzJnNzJhRDYwcU9GcXVTZ1F1cG1sL3FiUU1kSkUvSEx3RXlpZjlZSHpYako4c0grT3VkeDFYajlsMGNLV2ZFNE43SFlPSFQ5V0dXbHI1aEp0alVXUHN4L3dRcnFyc2w3LzRKQUpMaWdsb3llUFhleUlXTVNCUnZGVkpnU0NnOHNZd2krbzNoR0hZTFpKMDZ5OXJkRFB2MVNjYWZhVHlQUGFnTnJybmJ4aURSazR2TjlTRmorNjNBMkx1TmJ2R1VyTUNRQk40QzVUSGlLTGVYaDNhUzhhbHFTTlFKRzJQSTB2U25FcDIvcEFvTU1CVkpaRTRBYldpZ3BjcnpQVEpUVHhpREVlTGE3QVozWXQydEI4aHMvMlAyTDJDZkdEaWJKUnV6WEZUbFpEUmxQT3FqTTB6OWtXeWZxT1VTeFhlYUNZUW9zMlV0TEFTc05hWDRKMW1hcFBKR1RlNlZzcFRBbVVpTytOTFRBcTdzWmJmYjNMWTZBZGtkek55UWo2QzdKR0JXV1k0TmhyOXJTVFlHZURkWU1vQWpqQVdmVXM2Z0ZmTHVnUnJtYnhua2RESVM4V0svSzZWandWVGZjaERpdHRLdlBkdXVWN2VUZS9JZllORlYwQXV5NW5vSytjeFQyV2VRaEpVV0ZOWS9mUXZyZjluUWRSQjVuZUsvb3ZNbTR4TmI5cXNpN3FvMDN0a0J3cmQ5WGN3VkZmYmo1S08rUC8xU29vL3JwdjdLYkJLcTFOZmQ4aHNqYkZrY1hPZ3FRTkwvSElFdno5NzNQWTZZaGxHd1hoa3M4TWdNRXZNWGpySWVxN1FYaFkrbXlGNTE5K0dZVlpTZVUxL09yckpYaXdlbnRnenVKcEF3Y04zQnZPbGl4TUxObEJaNEYwUmU0OWt1SGUwSmFEVVFpdm9KWEJXVTc2RHVmNU9nREZDakNwdTd2dm9XdHZ6YUpORWhyckcrajFGYnRKSlJ0WGJnOVNEbHVadXNEM3pPWFNEU3J3MnEzdTNMc3JCRGwrSkdSb0RzU05oZEFmeVpuZXZYYTFuYzg4SVNMeHRGOE54SzVyT1gwTnJGTnNHVVBVVldodzNQQjRmdk5hN0x4OHh1eWVuVCtFbGFyMEZ3VFJaNEJHc1M2NFV5SzZ3aE9rTTdja2lxS3o1eG9VNUMvdjkwZzdnRWIvbkhrVEdWU2JZSjNMYjVWdUY5cklGY21NRHN0dVF5TGh0TFNaaGxIY3NlUTZlaENpM3pXMHIwVjUvMnZkVG81NHBDMmNDYVVINk5GZUFWc0dTZW52RkxaOE5TemRUdkE2TTNwalVWTWU4dlVVbTBqMFpoV3ZHak82RjhkSktNRVk0QlVXM3pqV2Y4ekloNWdRUXJiUVJNdDg1YmN6cXZ4QUh3L3B0dFRUYVlYRG9VQzRkL0dMZWdkY0ZaSXE5VzhqK1lsSmN6UlZQOEFZVTNBd0pHOEFYV0Z2MUE2bHNyeDFhc1pYZk5BV01EUmdES25Fd0tWMzIrdjFKVGVmbS95b096YlJKWkVWbDlrZ000T2M1UGM1bE54VmdkQ3VXUjFRaG12QjBaV0xmMEZFODUvT1NxVG5rY2laeW1EcFlYVHYzNElEMExmSXdPaHBUL1lISjZmWjdQMExaVTlqbGdKdlVJNXRaYzFmbU9TMVZ1amhNeU84R2F3SnYrQU5pME1oK1g0ZUVDU1hnNzRSZDFHWTM4dEV1SlZNSzhhbWUrc0NPVDgwTisveDB4cncwdmwxdk1oZElPNGRac2FRc1ZtVWxjQ3I5VVVIZTcySllDVW9rWWZmdXdZUndSVkJxVnpCbWJhOTROTU53TER3OVRiM1loczN3QXRGLzQ5U3FjMXdzQ3ptNDIxd0dLT0pTSEZ0MnRSV3dXeDk4dDc3V3FWY0NHTG43c3VoQzNZeWtxSURITEh0NEoxZ1pOYm1BeTRKYk01QS9TbWoreEZ5MGNmZVlZVWNxT3pFbXRIUDUrdW90cmVNdTJwd1pRWU11QmEvcDF2bjAwYnhlanVhYTYxeE1KNW1tQy9aQ1Fyc3p5QjcxcDVUMWNTUzRHUTdmMXVqcmUveUhDdXB5MnYxNDZuRFptSldCSTEycHhZNmw0cDhMUVZ6Q1pmS2lHWkhsQ2pxN2FaRUczZ2xxcEVFZWh5Wmh5azhuVDlXTzdSc0c5bkVPWGVucTY5TldQZzUwZ2pCMVFhQjdQT2JSUmxvZDVCcGQ5anRqSTUzSDlzOHFqaEF5L3lvMGZXWkVtVWlJbVBWZTRZcDE3d0szWG52U2lmVC9nU1NNcWNkR3RPeUdNYkthSTA0WGpJMVAzVlU1cFdib2pBVjV5M25Va1FvcTZFRVh6bXA3c3BXWVlkNXFzcDBjSVJwaTRENzdFdFViNzAzczJkaDg2MzZwaU5sYjUzRmdCSWJGZFZ5a1hOQTRNN09sQWdkZWRuQlZGZzZxaC9rem5PU1I1MWFlbE5vbTZVdXRSQ0w4VU16MTFXTFNVV2hqdGx4bEdSb0s5ZHMrVEFUVE0wc2FUak5VOXJDeXdxQksvWmNIRzhiZEpwUEdhQXYzYy9XbjdkdTUrN2p6VlQ3c3lxMHE2dW9TMUxGWjRPNjFKRDFJZjFraC9uekdwcW4rVVhWYlZFTlZMUVllMGRvMXZBYVBabmVZa2wxUW5MMmk3WHl6RkxWQUhZcnYvSkRBUFFLYnRqbWdsdG9Kd3c5QW1vSHdjeGc0U2dHemJaNlEvZm5Ud3Z1UGszcHdoTHNkb2EvSENGbUMwQlNoK25mOHd5OXppUGJvRWRiajdCZTVQblNIQU5NaHRiNHdFSm1RUUw1QTdwTThtZ1J4NjROM0RzSlFlcURVZlNrOExiZFY4dEMvZmlHTFdWclZDN1EvQUp2WkhoSnZiNHZRa3FyM3lUVjlIeFU1K3pJZGt6eW9CbHNkZ1FLNUh2ZXl0YmFsOSt6VUs3aVMxS1AzeDBiSHJTaDh5TXFYdjRtSWxFQ04rSGNlK1FoZFo0QlQ1MlEvamUwZHc0NlRCa3dkU2Z6S3o0QVNya1lFN1ovYXR5RWEyaTlXeVJkSmI5dllab1R0d3BsRnFCNWxnZGpLOEZhZE5ySzA3cWNad3ZkUjhjMnAzWDJEWjEvRXRtSEE3ai94dFd2RmRzQXM2L01vWTZleDlGbFd4U2FnRWM1WTVGYm1aQ1IybmhGa2NlQ2w1N0txWXF6L3BDaHdpYTd1amM5WFBlY0FmemM3cC9nYVpLeGZlV0xPYmdXdHJJT1QrZnc4MEpXaTVDVFd1Zk84SkFKWU0yb2t0RDN1d0Y3RHB5Q0JOU01Ed2JCN2dWUUpOamxyUFZ0MENyaS9USFdZSUdoczFTUEhjaEhkOEhyRWYzSDczYWFVRElSeURqcjVvNC9Vc3pWbWppUWZhTk4vK1pkbVExbHJVelh5M2VocmdHTzJiWmFXZUw0UTgyaHh2anRFRFZiL2c0djdHdC8wUlZ6aW5kU1FTRUNNUjFYSkppbGRXVXlKMjFZaVJ3LzdBV0ZwOTJ4THU4Z2ZuNG5ZQVFFc3A3TGJVeG0rdUczNjMrY1d3OXVydW00QzFWWHlDTVdVRTloS1hPODcrQnpZRWdJbXlydVhoYjBXK2htYmIvdjlabFlnUXI5MXc2U2ZNdVJoN3c4Z3QvVFVTRElZKzhFVE43RDg0ekNsbkdIaEhucXZSc3lKaFdPbG9lR0ZxR0IreUg3NnRpclJtUTdmbFZLYXp3R2xzbWNZVFJrMkV0SWx4QTRNQng1MnJGQzNJUHFrWWhOZTJ0a0J1VmZYTllURU1JcS9UZWRVay9QUlNpcWZ1T1dBM3ZyV1dEeWtTaGlsVnNXM2thclZYYUgxV1Y4K2U2c2FRd2Y4SUpycVowMWMrQTd2VVNJRGY5NjFRUUlRcEtYMUdJOE91dmtNeUkrUzJFN2RGV3FJWVVJUkw2aDVjTUtEL2JLSEhpYUxGUFV0S3U5RGo1ZmZXT3A4NGNBaVgrRzBneW91Ym1JSHpaTE9PWStkWE1GVklHdm92VEJTaWtFTXhDSlhZQ2ZqVGVIRnlCckFsRjUvVkE2dUlwS3V0SGlZSjBqRE9YQjB3TkJ5c3NFNmJZZXZUN0JKZHFyRndCV0EvTjVXS1BlVk5pdXZ0cTdzTG1OQ2c4aUF6amtXZUk2R0hOZ3BnSzY4Ym43eWRFa3dqU2U0SkpCdDBiRGc2STk0cUR1Q25PYmVsMndQbVlCQnlEY2c2Z1dhWEhwUnlVRHNjbitpV1FRb2lmZTlOa3B2bUhrbjBUZ21QUStVZFV0T3J6UEx5MDJkTThUMEZodU9aQlQwb0xGTkxTMUQ1YlJZYlExWEdZOVdDRHI1bkU0a1hDV3FFWWd0a2xDa0NmQnc5dWxLNWk3ZzhXU2VUcUlYRy9XazlHNlpiRWtGdVZxYjFaeW9zOXlvM0l1ZlRxRG9YMk0rNWlpaEhyTWFpYVpCdWJKUFlIdWFONjBYd0wxcUV0aElrOW45S295UDVNMWtzZWJOdnd3ZHZ3enlUYW92Z0hJNzZpSks2OEpjaFBrMDB4a3hjOENWUE1Feks4Wm5sT05CUmlJQ3dMMG9wcUJsVEJ3aFoyOVFYTWdNNTh5NnVRYkp6OHNENERMVUhwWFNBbWU1NGJwQ1hCSmlUZldsb2srNE9FejJTWndDQnIvN0JGU3p6U3pXNThuQTBNRUwwbVZxQk0wRTVWUUxJRDFoUGZOUHRQSEpKM0RBN3RSZVhYWFlqdk9uR2ZMQ29RWWk2elo3TmNSTUx3c2IrZGc4c2ltdUNjZHY2eUFzUkZkL2h2ZW1mK2N4N0xISEhOTFJyMXpLZXNXaUFPQmxFQjhIbXVsWFk0RDJWOXBGVVgrY0pwbE12aHBFbkZIMUdubUZ2WGRaR2NXQzE2dGk2NzVPQVV4VzA3RnByUEJmMzJscE16WjRra0JzeEUxU1dTQ0IxVUFJT0xINDVTOWZNUkV2MzFjRUdlK0NkTi9sNEdMMUdRTDZsNFdvVTdoaTc3WEcvZUN0R1hWT0daNUJwNE9zdmpMTnN2ZFBHQjRJOCtrYWw3WmsvSUtSTm9hVk5wc2hBbzhzVDVQdmlQSnNiL0pmRGVyVHZMN0p4bWtQa2ZyNWxHdS95OUwwK0xjVUZsQW4xYUFWRmpSdC9BT01JcDBTSFN4bFJ0T0g4Mjlsb0Mwb3B1dysrS0VtSzEzYVM1NEttTWp1Vy9SQ01uSTB0dUsvVWd4R0JFcnlnS09VNlUyWlhERXg3eStwa3daOEgwMXZGemhXbjcxRU5VK0h6V3g2TnJSaVVWZDVoYi9Db2x0R0dFK0Z3S3VQbjFSOXJXYXVYcmxsSzVyaTFpekV1NUdLdWZIcmNoM011aUdBYXRGeGEvMVlTanNzZzVoTmlxaml4bEtHb0FGczRIVFhvTEw1MDZlUnEvVE5POXJyZDBBazlQQTFNVjlJQ0F0R1gvMW9pNll1TUlRa2o5ZTA3MUhCWlhFZWJISGRlZk5weHJOVjBYQ2pXSnh4ZWZmNjlvMEM4ckk4TFk2WndiRlFhV1p2b1B4alRQRWVzZmU0OTFNMlZGRjA1NUw1WVovcnB1emQyZ1RudURjOE9kRHNsTG1nUmFla2FwWWxuT0VkVE9XcVI4dlZ3WHJOZVRYNS9oQVlEMFJuNUx1M1llL1hWQkVoYVpoMGtrQ0QzWEhxalBVSXJ0UytPanZJcEsyYStRNE9sWG0vN0crWENzZTVycGY5UHJCbnhTQnprd3J6SmJEeTNpLzFJRTdSTEkyTmVBbEd6S1FqbXFlQmthM0I5TEFJUzJMdFYwU1pxRlp3MjJoQVBLWEp0TUI4N05lOEhTc2dHbUF2dnc1U216UEk3MDRkMVdQY1c4WWRLZkQ3aVc0cEVWbUh2WGJ5allxMW9uRFhTbXNPUGR6WTh3S1BaNjl1emtidVIweDdlYldTRnJxNEU2WWR6ZTF2c3lMSi8ycVBRZ1NIblY4UVRRd0tWSksydUxQNm9SKzNLT2VBRlcwNWVMalI4WldLSnpWRzhzcmV4S1VPZSt6bVVDdVc0Y092Unh0eFgycDhkNXc4MENmV3NwY2NtWDNyRldSTWR3VFNBMmlYcXFsM083UXV4R0plVUNwMzdEaHUzZjIxL2FxQVZIeHdHNnJ3MnlUUmtvWGc3Z0NGMGtJYnRaL2V2dkJGd0ZFaGd2YjJJejVKWXdsTWRJTnI0VUNGS2NNcnBxc3I3am9leEUrU1I4TjlrK3l3WjRvSCtEdnNmMko0ZTE4cmNzTWdxOE5IcnkxdVdySmREWGgxUktLSEpRWTdSRzZMWXl6N0JYeG1NbzI5dDVtNWdyOVNQcm1IbGZDM245Y1lqaWQ2QmFScTNXQmdHSWlzOEpOTEZPbk1HaUFmN0p3ckxSc1I3eG5pc1FlVStGYTJWSzJIWERKMGxrOEpNQ2FLUE9FNVRUcWtCbkRpTjlWOGhnanp3RmlqTjQvUU83ZFVaNlYvSkR2QWRmc1NNY3paMyttZEl6NDRFMlRLQzBsVGNTWUpZTTR1THBFNFNVcXhDV254SFQ3Y3h0WWo2amJCVlprK1NUMXFiYUY2WXlPRmJzdWY3c2hiOGs0NkVWZUpid3JyUUg0azZ5K1RJTEZqbHZIMVRGME5pU00vL1RFVWMyR3B1cDY1aExhSVNpeTBnRFhMb256SExaNk1uelBlcDd1UklBeDFTTHBvb0xDaEk0b1F4cTBjdFBTcndNRzVRYjBZQldYSzRXc3VjVmhqRzY1bWtmKzA5bGI1ODM2M3RENzhxNjg3OEtrUytycUhSaWMyRGRET0ZOMUdFYk9vOFdTVDhhWU5kWmFkaHJjNWtTOXFQd1BpWXphckplWmRycWx6bEV5TE91dVpUeCtEcTkrZFkvRUROL2dtZjdqU1VqM29NZnkxQkpRTFRxL3JHMDVKL20xT3JURHcyVDdoTGlsZUx0QVJPL3hhTk4xZ2VvM2owb0djRUhZNmo4bnZwMlh3ZjhkeitxUkdJUWNJNlp1YTRBMGpnK3Juc2JDSVg2MEJML2FOeVh0S0tvTUl6b3dVNFlyczRlWFNlV3VhSjE0eXFLM1FLL3h6dDVha1lVZmZrM3ZNRjNYZ1NteEcvZ3RFUk05YVJ4S05TNjZSblNJM2ZuYWFNUFZoR0J1Y0s2dTRrTG9tOXl2TnhNUVJJdy9IVGF3Vmt3NDRxTGhROTQyaDUveFRWQUw0dXVKdkV4ZjhXRTZQeWd2d3E4WFVVUnptaEp2UGY0MCsvY2JOOG1nY2M4aFJ0L3lJU3ltckhleWwxV01sM0IzcXZSbHQwb0dvL2tvdFRzaVQrRGhvR21FVnZVQW81anhobEhmYWFidlNzRWlWQkNScVVFN015QTZMN3dFNHhnT0haNFp1N28zQndzdWIwNFBXVFM0MmNScGR4d1VuWWd5K3o0Q053ZGVGNmpUcmpGMXpuQi8zRVIrdU93d01yQkcrVVF6Q29oUmw3bDZmcVQyTzVmRlBXTXhVdzV4SzZIT0hxMEdFc245c0FFa1JzVkpVWXRZYTI4VWEzcjhYdHBOcFd6bTY5ZGY1aEdvUWZ5QXZCanlUVjlBTzN0dUNKYlUzcWNiMC8rU2t2bTdpb0U2TkhVTmNYd0hmVFRiU0JWZHArM0RpdWNzVE56cGxScks2WHBmSEM4Qml1NHRaK3dHSHdOZXdScUxORkJUZHR0bC9LVkpoSUVJeEJhcFgxTnFOMU4yNHVvbG1HYmVIN09LOEpwMi82OWVDWDJtNGhVemZTK3RtZnJhUTF3ZEJKSm83VUtPU1piQ0FIcG9Qdnk1Y09oa1VKUFM1K21MVzBxNlJSTXRqRkRQbE84cktFRGpwUUtRbktkQ3dKRVViS21qcVUvaTZPM0I5eDZGUTJjQXYwRitwRnQ3aHRYV0gxcFVhNStXc0lsRzYrY0w0K0lJbzhJNFUwN1I2S25WajhyeVBUejNJMlJ0anB0Tk5LNjRuUHVMUEdjZnpGU1pzUHEyRHdnR0M1SUNJU1d6aVczSllHYVVLSHJ1eldFbFp2MSs3Rm1WOXN0N0tHZzlNRmR6YXRFS2NnSUJsdHBwenhGcVFaY3VjSjBhbHNvNjVlbWo2eXVUQXJZQ2VDQjBBcGt5d0diOG5VdkNFNUFjSEdVZWZvSFMzcHBNZnVHYXhIejlUNGVhZWlMVXFWRjdKZ0FGSU5SamRJU21ZWWdDSUUzeGM1WCtyN1I3aStsNjhLZ1pudk1YY3hsZ0ROWWdPUUEwUHEyejBOT1U1ZGlWUVJXdGtRbUV2OWVQbi9COFZ6SUw3cGdQSmtuQlkxVTJsNWNtVHlGNDRyY21UU1dLVGNUNGxUc2tnd25aeTRVMFFQSmZHdjl1REhzdVVqcHVwM0lRd0JWeEQxY3pucWJ0NWhEbXk0MXJ3WXRtanJjTE1wcEhYKy9WZlpJUGxyY3k4bEFZaW14RzNON0J0cCtBcnQ1WkhKWTUrcmZJZmRuajlJT3NBRGlJbUdud1QvR0FKQ2xYQk4wd0M2ZnVjYnlQN3BQUnNRcFpZYXZ5SnY5OGFPRDU1RElqSXZSbVlVdU9MSHlDZU8yMVJLOFB4cVB5YlhJZ3NqMld3R1M4NUpPZXpTWlVCUVRhVC9RTXcweWhTMjVvbTJkcEtRb0J0TCt6MmJQdXVzTHAyaFU1Z3g2ZUhUN0g5c1JjWEErT2Y5OUc0M2x6WGZ0c05weVlYcTNzYVFxWkw2dUtqdko4SVp0M1VKV1B5R3ZXWEc3SWJGbUFVSmlkOFc5R0RBNlJMajZRWUd2anp3ZHdKTEo3bUZsOFhjUXdPSDFpNHFJSEx2RSsxWEpUNEEvd04rUGVEb1RtMzJsVmE0d09aNHVrL2E4Rzl1bDVsSkd6UnlQQkJxWmg2Zmc4NjY4dXJ6VzRGekNoSFFhaUtnWlFoZGNvRFdWUnZSRWpHVE9wamdlQ2UzVlcxMVYzSEdEeDdhc3NnS05qalpFME51NlJKa2RLNzViZ1FKRXZONTVkZWhsem9hQzBSRnhjR0xQc0pOR0VuRnBRUW85cVROS2RCYjZFWkxxZjY4NXdhbFMzak9iLzVNS1RDYnhkV1FDd0JhM0ZUQ0NhbXpNK2Y3RUNYRXlTWFIrZkQrS3BaNS9sMDc3Tk5EVXd3Y094Nnl3UmRCQWwzZ1RINUNUeXQ0YXhsRStvOGVNb3pyeVNZcTZ1dVQ5MFNJd1NwcGJyc2l6Q255V2ZCSSs5OXBhUzR4SG5ESXVFR1lHUHFPNmIxRWlULzErekhMUFoyVktHQWxUWFdQOWVFMDE5anNpbmV0ZHV1bnE3RW0velhjMVV2NWk0czVrZVRiZDREaUVUTW5DRkpyTUs3QUQ2RnJHZjhMVmtpZk5JQU9oVzVVTTMwdU41cE1XcStqMDZGTHlWZlZCckZPNmZXY0RkemFJSFgyZVU2Z0JYNFY3WW1za1Irc1hxK3k1ZmdvNjZtL2ZaUFpIQWxOUXZrNFlzNXBueHBLbFFxOFVsbXA5SE15dndHZVdETTQwK2hxMEF2Mm5YMTJFRkFEOHRsSTBPQTB2N1lpUkZ6R3I0ZUtoaklRVUx5dEF6NnVVbzFzYTVXeUFJeFJyNWs0Qlh2NXZRcHkyRUtQUVdzKzZCeXNVd0F6VVJhdk9PNk55Q2x1RmVYSG0zeUpKc0dvQzRUWWRUL2JKOWh4aVlybmMvRTB3RXN5bFlWS1grQ015ZDlWNjMxTlNERlh5SnlpTzVrRkRoSCtWOTd5QUd4c2Q1ZTYzRnU1aWIwc3lla1I3VkhGTVZjc1U2MkxPd2ZEakh5YVVneDRyOE85dVg5cVcxZ3hic1R5WVNNdzduWW5neEpaV1ZKVjJoL2lrZUpCTjB5Zk9TTENoNzlKeHp6a3c1OVArOEFrSy80U2NMeVVPZEMvc25Jb2tKeU90cHYxckFkblBVUFN6eGtEL3VlSlFMR3prYUl2NUpCemo3aGxUZWdPRkR1N21yVGhmNVlUNlFQcDFyRTNubmd4Mnk2eDFvd1BKaEhyMjRJZXZobDZEMXhzbDY5NU5XOFNNYnRTdkVZZUFKNnBpQjdOcUtqa2dYS05KMW9mZjN5UnVDTktkSVVYSEZ0Nm92Y0RZcklNSHBKWHJNenBzSVRaOGorTDBkZFlxWGs4L2xZMUFXT1h3WUNDeURHbFpGVlBpRlFmaGlITFg3dWlRa2pYTUhkeCt6L0xpT0dMbDJ6YmZUY0VZNHJiTTVuNFd6bjdTNU5YcCtJeW9BV282Y3FtTFlENUs5bW9KWEdPNXFLRnIwd1E1anRuSUhtMldtYzZXM0JxNUp6d1UxbE5weEpvekF1SEVWcXR0dk1oMTNVZE1qRzBzaWFwYWJlZjlJeUpWV3F6OE1BTTRtZkQ0ZnFBY1Y4Y3BLOVZWM0xlL21lMDA2YVJ0SFg3d0g2YUxhWVFBSTBKWkR2M25VS09icEZSNGczK2l6MXN4YzNtWWdPOGJuMURJenFLUzFkZmVqVmNlWkdqQlpmWmdmeDBNS0ZTOFo0Z2hVYXYwc0hyS21KN1JOOE5MK09CR1h3d2VDQ2V4U2xKcUo0VzZBcnY1eHhqY3JpQ2FrR2tZUWdkeXpVK3VobXlmOWZTUGR1Zjd3UktEOUxHT1J1VGZZazZBUTErRitzMk14aUkxV29DOCtsRDB5Rm5hbXVzVlprM1lqb0lEN2plYmRIYTZmOG1GcXhva0hibS96TWYxSWhFTmFqZzc1MEN2eDRCQnlNR05JQzFURHV5VVVLdisxbFBEY3dCZWhmdU1YUDBmNVlnY0xhbjZKdXFJeTBabFpTWTlGMzJ2MnRJaWo1YnVjSjNKN2dKOTVFTFZDKzhrbmt4UnVuQkVIZTBaeENEY25yellMOFhsdzI3L3o2VkpWNWp2TUZIVHk0S3NvWTFnQ3BXM0IvL2FWY3BJelhsNTYzNVV2VGhVRHhEWFZteWQ1SG9NcHZDM3pZL3RWWVRoYk9IZWI1a0lhM2RkaDcwWUkxRWUvVW11Sy9JclBTc2RSVnFJK3pnZmpRemJCU1ZtZmpxTDVrOUpyL2ovMDBCRXB2VE9QOXN0Q0xtRFgvT3FZbkx4ZHd2QWtYYnYwSUFYdVF4d0NvSjZqQnN3eVAwS2ZycFVTT1BVSnNzYkFiSW50R2xwUEd2TXdTRWpWeGFmSUZVa21wSnp6QTFwbGN5aXBMTGF0TWdHNzYxaUJtaGJ0Y3czWXFidWRnY1RzcnR2NndPcjV2MEZ2TDdNTFBvbXNGaVhlbytrYTF2cDltcUJROW5MWEN1MXNScUVhdzJCZUxUL2hwVjNTUm0xVnZCUTQwV1JPU3dXY2xlS1dpSy9jOFBabngxbkw3T05ObE1TWlo1cGl1MXRJa2NFbGh5Q0xKRy94WTgzNzFFQ3dqRzBmQTAwT2dkMzJrRmpUTnZVQ3BKcjR2TUZyVW5mM3pIRG5SWE1wMm5nTU52Tk9MUTN3TVpDSEJ6clcvYWdnc0xTc0s0VlZlOWpoTWJoRDFpQVFBYnI1ZTBDTUU1Q1dJcS9Gb25HWlpLem9uaVZRYUZGRDVKeVhFblRDMXpNczBBZCtJdmJRSFNyYURseWUxMzFpTitnU0lKemxFZU84UEgzR3F6VzVBcTJTYVkybEZ4RlhnUnpwcUt6S09OYTVYOGZPSTRxSmVCWnB5QXlvZVJLWXphTktRUjc1aHBTYXJnRXozb0U0WWJNcWV2YUxnV0hMVmtNRlhVSXE4S3R5bkcrZVpsYkRBcnpPekkvdzlENjVIZDN3TmpmUUJhbEY2NmtRRDEvZndJWDBmVFd5cWhFSnhPR0hmb1BXSWdTdDVzZDJMRW1aWnIrcDVndlptM1NOdTR3VmpCUHl0OWp3MnFpcW9wRE85SXk1SHFRMUU1ZHJvOG9jSG9XTjV6c0FVcDA5MldVZXRHZ3h6OHhHTFhOanZWSUtwSEs0eCt0TTZyczVrMm96U05oTXJhL3hBdjlhZ1FaWjRtN1RjMzBMbXFrcFZZNW1kOG1idFJ6dDFTazdpNjF3WVdnYnoxSHJyNGZPcTh1QSsvR2JPQU9xZEJ1dGRUS1hQczFDTHgxc3lEd09wL25FY3EweUdkMjVUMnpJRzZTbXAwdCsvcHJJVForbzhpUXVsWVdya0tGeTg2TzBkUUtWNFBadG56RjlIUDRXZ1M1dkZqWDJBcWhLSS9hOUhlQVE5NGRiT0F5Q09YT0JQWDhkS0RJL0NzcTJlMVJLZkJTUkViWFcxZHBUd0s2dE9KaWxjOW1Xd1BZQ054V2Z4K0RYeXNjcE9MTmxFWEowNUhMcmJJUmtQWVJXVVBUR205R0V5ZlV0SkxWdTRZNVFYNDFreUxGU2s3bzl2ZFNIWmpWMEdMMS85ZC8weWFSd1NwTGlBb2tRVnRXY1ZMcTZPbXdvZlgwOGp0WCtlZUhZbU1OWkwxU29vUFE2NkNDckNUb3pvTExSdkJJQWxENWhRR2pibWVwdEZiNmY4T3ErOXFXQ0NodHViR05QRmpNRmJmcEVGUlpQd3dFNkYyZG5LeXhCSERSZE4vVUtYMmNOOFY4R3BVWEJxYnIyQ1ZQdndZRDE4NXllTnhSQ2UzcDgxOExieE9icTkyaC9FTVFHWW0zT0pmUFQvYTdqWkk5SkpkYWhpOXJoTDB3WmR4dEtLQ0drdHMvY1VPaGZ5MyttbHdpRTd3TzBhaTV5ZlJGOEtNbXZLOGU4cGxncGw0V1k1V29QQzFVbE9pZ3BkR1VmUTJyRHIzK0l4OUtUaFc1c0l6cW9FcGhtREk1TUYvZWZpcEpRRVRpbzBLcFdjSkt6L0wyeG42WEtFcFJqODhkaksrUCtmZG1hWXQ0Y3MwWXpLSXJleW1mRStLVEdzcVV4TnJDaTB0Tlp1VWdUbEQ4S0tYMys3YjJUMFBveHRsK1VoVkUvMlVucEl4RWRzRHVsWjhDakhDU2JMSEZrcVFiVERJOEFWKzExQjVYNFJyZUE3dUFNWHFLYlBUbVAybGR4OUV6OUt2S0k0bkJzbnVUbWpkZWlLRUYyWUk4eU0rZFp5bTNkaW8xb25kNS82bjBJUFBuZmhST2VNSWZwVk5CQ1IrTkRjK24yemJnSzRncUVWMUVPZjBOM2FLNVYrb1E5NVB3dzU4bk8vVTJKNTFjU0NZbzFoU3ZYWWxtdzd6djdnWUZhMGplTTRsT2llOHBKdFk4WGNzSEJYUmkyRVJ3REpuRTdMZHhFYTVMOWIwcnZaRUloOXZYbVV2MXRuV2tTNVpwbmFFTzFDZmtlNnIvRzNLY1laSEFHcnJOaWVWcGJPVlVHZGpjd3duR2t3SHhFR2xHYmJHRFliVnJmSGQ2cmNEWDVpRU5rNnlZNVhOYTVSd0UyUU1GQTlXWDBNSFNkdHY1NW9MYldtdHQzUjFrbndrVTBIUytuUFBNeURnY2NwRFl3ZTVlVWpQcFdqTXN1Q3pmcEdNdSt5bzV3RGY4TjgvMnNmMWV2My9WZTMxK3JwYnZuK3NSK3IyRnA4bjFjYjUvUVRuNU5zWHcrL212azdUZm4zUnd6NVB0ZjN6L1pBLzFlMlRmTjZqSjhuMXN2MWV6RFBrK3RYZlA2WE0vSnVEOG5wOTE4bmZMOC9TRGdiNVBxVi9uK21IL24rcmwrZnUvZko5R3o4LzAzTDVQbzkzeWRyTmZCMGQrVHVCdnlkYi94TmxBSXhTZ2ZMVVRpYm1wMmZyOVVIQVBtT3J2WFlCZEE4WU5DWmpVYWFEdjcvU28xalJxdHJBbUJqK1hzQjJuRFNnRFdieWxwQm9vMW9qTURNQmdvM2o1UVFoaERyMnBweldQTHdlUTVIRnd0WVl3UHFpSHh3ejFxQ2JBMXB1OEMxMHAyeHFTOVZoalBxY0NPSmRObUoxTnNGL1JoODd1MFhTc1BjbkRVRGc1OFR6OE9nR29CMDJQdDJTTndpL2hhcG11UFB1bnQrVml0alpBeHlVOHJPNVdDYWxTb1VibXNiRERjN2VzSlhoMkVDeTF0VmRGYjVEZWdHRitEc2RZalNIWGhPeWdFbkdzYTNlaERocm41VWhmSmkva2svKysrZ2l6cnZWVDJiQVlpeC9CNzhZSnV0bXJCK050MTBrQTV2VDR4TmNnQWVWNytaTEZVUUhkbWQvZS8zYUM3QVA2NEFjb1hreHpHWGdPUHpEYkM1d3oycHdoZDRhZGs3UUx4R2hNQ1A3dzAyK2I3ZElGM2pOZzcxeU80dWZnYkFtZFNRWGl6bS8xbEcxa3FjNi9NSlhvRFYyem1XVGxSNE1TWHh1YW83TVlwY0xCVkxGeW42ZVgzTWR0ZThnT29Sc0Y3QzA4UHZ4YVJxVE5mamhyZlZOaXYwemlqL0RGOWZIN0lnTDFLL0tZUnlXSzlBOGIwdXZ1R0FDdjNWWmFwUWtlTTJ4WFdOakZmaEtDdkZ3SEg3ajA1RTdUaFE2Z3J6UEhrSWRaR09LVUlMeXdnU094Q1hpRFBBRStHcmVvQlhodWJPT1F5ZjJ1ZlcwRUovb2RvY3BablZRZFNTTUhJOGZLTkpmQXI1WS9Ka281R0NYZ0E2OUxVVTZaMUFuL3NBS0JLZDZYSGE2ZTRCTm02NGFYazNEaS80U3hkNVR4TnBEeWswTGovU0VpeXA2MjB3MGZqTmZYdVhuU0d0bUgycDMyVWRzV0VUclR0bkJ4SnNYSTFLWXdaWmNubWRMRERKVERWZWdXY0psaHkydHAyblNnUFdnVmt3R0hQVTFJK29YR0orNGlEbHk2NWNYQkZUTHE0K3dCK3FSdXRSd0J0YnJEZkZVSkdsY3pySWY4RDU3YWtBMFRxempKNHNFWGFsRks2RnhUTVdHVkRsSHczVDNyanVEdHZzbTFRWGY1aWZsWitwL1YyOCt4N3dZVDVBT1BtTE1SU1ZiT296ZmdES2pzMXFHNWo3TkM5UUo5U0Z6UXhKTlFpcGVtMHVNWHdKVmFvR2Jrd05QT1RldmZlSTk0SEtaOTBJa096UnZEanZCd0ZXcDZIaW1wWkczbWh4NzRlS05nSW1ETWtnYlVOcFgvbHFGbGlYcys5K29VdDQzM3hsUGd1dFVRbjJrWXNUeXVvOXgvQmNBbGtpOGtPV0lOclpzMjIwTXVwS0xWUTRVdytGdjJjM3RWa0Z6QXhpSlhXL3ZXbDd5VFhqQXVDK3o5dFhSdUk1dEgwZk1LVThiVmVXU1h1ck5TQjcwVGt5NHI3bW1VYUxtVTJvQjRpQnlmOGs4ZmpHcW5oMXkvM2F0RDYzd0F3Y3hxZkVEUjZLS1U2Y0Y5N0wzWDdUSnBRbnRrSXNaZEVPeWl0SXZYK2F6MEcxcGVOUldQOVR3N0tZZXlPL1dldTh3SExoWEhjeHNiajk2SlFManp0K3VpVFB0K3pvQy9zbmZGaEQ0NGJzR3ZaQjFhS2pMQklxb0c1QzZpaDVzS1NtQW1uOCtvOHBrNzJsMndsdW5mZ0VXSlpaeW10aWxjYlJZejVRUkZkN2VYRjAybkdDeFYwWEpOVG1lUU1zZERnKzMvSG5XL2NGYW0wTVhqa3NUTTFpczhoYXZUT0VQbldwZ1IvdTZVTU1pZ0ZzS0pwM1hPSW1kOXBzZFBxQjloS3Q1d2UwaStMWlBhakFpUGh1cVB1UE9kNlExV1Q0c3ZqWEhqcGxYTlNxcEVVMHdsblpZVHJ5cDdzUURGbnV4dHl6eG5kNGFXbi8vQVkxcHRKZndtc2YvSWhGRkZXQVM1dUoyZlNRdmhvcGQwM2VvVVZxSjB5aUdaWlU3TWdHTndnWGtDYStMTmtFeUJxUkZSZ1JRS1phWElhYnNUOGw0dlUyM1I1dlkwbkJVeC84Tkw1ZzdLZjlhMGVNem85aWZXNjNPc1R1N2hTR0FLekNaaE9odDhNeWVFMzQxc1U4bUF6Sm54WDgxLzNuWEw0N1dxeXNJZXZQeEtuWm00Wi9JSm5hZEVaUkt3MkxSSXErT1dicDVIYnJFeHdhbmpuMTBjRFZVUnVtc3BURVhvcHQxREdsVDgvTFFuVXFDcitvMkFNNTNmb2lyRTNjUjRIa2lSa0JTQVFzUzQ5QklyaVZTdkpiK1JHcEc5K1MvdW5FWnVDVHRUZnBrdEFkRmhnemlic1FQdDhSZjExVDlmOUZWcVM0NlFFc3V6NjY4WVNvYmtqOXI0YkNXMmw1Qjc0alUwS1lQaEIxelJtSCtobU5GR1V6elVLNjRpSzgwdUZjeldWemxmWkU5d2s3Q2x3dFE0d3NjSkVKUVQwSklIbHNnMHVDN2VhdGE1RWp4WEt1cWNISHhZQjJ1OWRvWk9VOVY3ZVBXT3lWMWg0VllyUUg0RU4rQXdxa1JTSWxNRDV6bzZOVmIwa0QyZTg4RzR5aGM4Rklzb2RiOHNsWnlaOFVGekpwY2NmQ1FDaHFYbFJGb1pEWHBHZXBRSXdqQXFVWkZuc1h3WWp4NTB5M0V4Q2kvUERhK1g1a2Q3ODkyUG43YWVmbUtzbThwdkRaN1lPTjdHbjVTc0MrMnlHcDB5dlFZKzZpSkl4QTBJZ2tqZElqVi9wdmpmQXQvRCtncjQ0d1RKQmp6ZUQ1WDBiM2taZGZTSnFGTHlVRnhKRSs2WTVla01qWVVvczdnN2czMitPN1FoejNwUm8vMlRWTnhIVnVwSFBObkVmRGNNRFN2dmRpcVBHZS9SbnFHM2RyYnZ3YmRkY2ZXRE1Oc3NNUllod08yY2hiZVIzSVF5SFMxWStEcXh1UVc3c3NBdldUUmdEOTlUSEVDdkd0RVVPWUYyNmU4MHNhcW4zT0oxNEt6eDhjc0RGTkNlNU1xOFFQV1E0K1pQSDYxZkM1ZDZxdmg1QWNPQWJrMTlQUnJiaW9qbVlodW04aG40VHRBTEVzdk9NSWJsMHhYeitpMVNvSmF4QzA5anVRR0xFNjY4ZTlidGorb2hub0wvMWlaaDBYN3orU1RacGZFbFhjWnY0aldDd1V1SzJGakVTMkhPUDhIZkdqaGpycUVNeWwxaDUvRk5CQk1HNmYxZFhjN2Y2Y2tHQ0ZINVA5ZWRCTnFnd2MrWGpzVXVKWlFqc1Y0cFQwcVhYVm1HSFl0OEgreG5qQVU2WDIxSEVPeTBNUFJCVms5bmJOMW5OMFJzZHFqM2ZiWmczN1BCclFpQzA3MUxjd245QzI2TVdxOElVWjQxV2UxVG9JdDd3YkZlTDJIMVNNRk5OdHJmcnNuaWQybVd3bWUxQTNOUzBtMHRrajl6ckVtaWhGZlhEWEpNQnVYV01XQ2ZlK0U2cVVDVGlieEZIMTB5N3VSNWs3QW9FSnhlTzl2eS85UFpTS0M3QWdUc3JNKzQzMktwWFBiOFkwcndqR1FXaUVUUEFCbXU3YXV5aEZDd0dkRWpEcmNiOW5ReHRzaTI3MlJlRTN4SzZsMitxRFhDcWNzeDV3a2h1V1FEMU9HTkg4cmNwY1hpbEkyUzREVE5CMEc4dFg3T3RRR3ZMQXZsd3BXcmsvSHAzY05JUWYvWE5TS3NpWSsvd1E1MDludGRNRnNXVjcyTWdIMUlHUC9Wa0txeXZjejdhcVFXRUYwV0FQTlVqSHJGam56ZndRaXVEZWlkSTlIeTRKbWYwSHZ5MG5INGJQMjRqblRERTlKbWMzYmp0NGFuNkRTWWtXZ29ZVDZuV2tKQVV3eXQxT0tkQWtra09XTmZhZGh6VStZNWU3dCt1K0huQXRGMVdINS9CdGVDL2VKREtLYytYWmNSQVhlcGUrQlcwVkpPMjNrQ3lmZGYxM2lRNWRkR21jSk9hcXUzSGhLcXF4cjdHcGNOMmZGeE1hTjljMG9aQVg3ZXdpWi96SkRMOWtBWXppUDNtazV5d3dPWjQyZ3hJMlBMRUVxUE5aVFBheGI2MU83QkkrYlA3WlREeDdZZzliOHFDc0RkdEhoR2k4bDNkL3AyTVZuQmpTUkg5a0IvZDRCY0dNYldJbVhSSGc5WG1DNHNKQ2pjMllBMGpXTG4rNGJnbndHc1JOVnZDaXd4WWk1UUR1MnIrSmQweDZEd0NiU1J1S3Jyc2k1N0pSYi95M2J6ZFdteDJ6Zzk0L05kQVI2enZuaGFqNVk5Wit6clVFNDRENW1SMnJoV05xKzkyYUZVR1FReXR5NmZvN3NKNWJTNGZkdUljUEtaM2hsb0pqeHhFamk2M0JpZmV2bHpJaERpaUJ1U3c4aWwxRkNRYkhlRWJiODRMbGdEWTdnNUxqNlFXOUp2eGVETWNoNy9XZWN3NFovVndkNzc2cExiVm55Y253NUVtQTY0MDN5K2NHeHdsazVVUGRmZk80VTdGcXkrU1puOWd4MDVCR3BYdDR1K0tmMXdka2U2eWhLcENDaVZVS3cydzYwcTFkWDhRa3NvWnAxNW9SY0NFSVRTMjhDU20xMGQzZHB5YmRFalR6MWdnK2tlUTA0VXBoenZneWUvQ05JbDQ3UlZyMUh3WExnSzh2bW1QUXBwTHIrSFBUUnVXU1M0UmdDSERQcFk0WXZUQW91S1FmbkZJVHBCRyt5Ni92YWU3SGhFOTIzMlUxZysyNGNnNG5BbitTOVlJWXY2elVDOElZZzlleXR1WENEa3loRFJYWC9hV1Fac0UvMmlGVEJvUjdid0NHNnBzRkRjMENoKzUwVFk1dkpjVitvZ1dKVmZKdzk5bzRUWFh1dHM5TEpSZkVuNDFING5YL09jN2JiTG9TT2p6WFVDZHlSSmt1M0F5UWRpblNjMEwrdzFmUCtWcW8rVzQ3RncvV052REFPUjZ0Y3ZEZzh3R0ZEUlJqRitBcjVNWGJydjcvN0VxQmppMUYvb3FPaTlycFlmcXZ0eEJ5NlFhdnNoalhVb0hzRTc1Qk9mei92VERnTThXeUVCRkZEZUtWZ0lKVXBEdnd3dXRFeFpXSkVsTmZ5N2Fsbmw0a05mMTJmVkZucDRFRmdkdnZYUFpMZzdxbVRxdlF5Qi9McnAwMWtRNWlUSWY4QlVaWk1WK3hsRkU1VzVnQmduOGhubDJ0SVpnVjhhYTE0SmpyOEFhRDV5VGRMRXdhdll1NG0zNTViOExsVXprQ1c2QU52U2prMTJpb0pUUnk3S3dtd0Jod3FCM0VPQllXUWVsS1cwdFN5dy9udFU2aHFXNzZMcXR3YTNRbjc2eGU3VEptOTVIbDlXTEJUTU9yOFhuWmVNWmpoWG14eEg1OE14dWF4MDNJZGd5bWtlRFg5bnVORHk0RG9DejlnbEZETnIrYVZ4R2NaNTROd3NaYlpnblhibzd4S0kzaEZoVmtzaWNNck1wTHdPZm1DMDJVT1JNV1paaXJaYkxVUUVNMVl2cVZyMVg3SFBtdy9xdHBPdnE1c3E1NkRTRStKakdQZTR5S2JmOWtSREJXZ011R0k5WEl2ZGV0cTEwS2lrOGJyb0JzRlkvdkFuTTZORGE0NFpCM3didUxncENCRjNMejlRSGFYWVYvUXQ2Y0VqUHcwRDZ6N05yRzV6ak0zbXkrbWFWVjJWaTdCbk1lNEFkQTkvckZZUnJyakRzMURpM3hkYm5QaFJCbnM0MVczSjVlbjZaNHY1S2JVT1BiL1JmS05iVHBqQURSRk1sWjEwczZSbWxMTE5lN09KbTc1QVoxb1djSGQxcjNoSVRtYU90cHJqYkFsVWtkb1B0eVFrVUc2TVpZT0ozZ3EyYkZWTXdYQzNhN3ZxeHNrQWhuS0RERzBDZVp6MGJrbCttR3RMeFdRQW9lNmgybUp4TmduSk12M2s3UzRvYUMzOG5Ga0ZTUGZNQTdKNGcrc0NnUzJkaXFRSWNPNUFVYlE3T1hsc0N5MTRXbFBKZmV1N2JJZ01vZElia2R4UmdEM3o2bngycUo5UjJpWWtTTU0xM0VRbkN0b2dZUVcwWW5CL0t4NnhrVzBxUEIxTWducWl2dDlrTzgyM0FEUUlhTXlSVzNha3didkNNMkNjZUNFQXVCV0RPdWVCalY4aExSS09PbjdwcGM3Z1BPMDhZcmVTYVIrRGg3RFY4ZXFodThsZm8zVWs5SW1FUUw1bmtxWFJjcTBUZXZzR0pFNms5RmxDaDRoVjgzTlJiWjE1dEFsL0hVSTlKdG1tQnVLMDlnSXk0cUZBczlDdUNNTFJsZC8zcjBnTitrMWlkRFpHMy9uSzd3VHpvRSszbnY5WEpnb1pWU1J5WW0rbi9zNE9ESTRScEdZcVRPOFBGNEE5YXdUOXExOFE3dXFKSjg2cncwNXZqaXQ1Mk5TNHJnZmxIREI2Nm1mdnlzVlFaUktoYTdHNStRbDdMSUZta3FxUEJwVUZPWGg3NkxHNDNJUER2UzhJRC8yVjFjRFZBQ2VJRWlaWmIvQkRJcTg0Q1c3MjVreTcrY21yL2xkUStwbk5BdTk3ZHdtdHFxaHdOYkh3SDhQZEhTMnZmQ3ZhbzhmT1ZLWUtqQk42SjhmMFRIb0dvTUdlakRYQ2hPS2N2ZHVMWFBmWk9VT1VnRFM5MURRUXNaVlhCTDlDdHFJZC85bStrRnVtVEMvMndSUUdXTGNpdU53eWUxZmlZQm5vN0doa1ozdW9hak1qTkJUVVVpRVNKb1lhSmJjSk13RHNXaURwZTdlT3gybTdBaW5TY3N1Y2hEa3ZCSnQzOCtMdkdYRWJnNVRzQ3F0RFVaTTk4RFlJTEUvZXgxSFRxMGFpeCtFUzkwUDI0VEpCaXE3ekl4QnJXYmtoWFNoSWd5d3VPdCs4NWdEZXl3Mk1CblkyUXJhWDV6cmY5OXUxSzB5bnh4djJsbnp4L3Y4NGh0UTZ6TkhHc2UvWUtDTE1XVTQzcUJMRGZmU3NYSm84bU05Nm9QUWp5SVlQckpkYW9VZVJDNFIyMS9GTmQ1UWdMajZieE01UWZOcFlyY1BiUmpNUDdJMW9BNW9nUWxCMktJVGdISm9OcU9pbzVCTldxVjRxMUhTMy9nczJRYzBYeXdJZWZQcVFyRlE2ODY5bkJTQm5yRkJlbWt1bTN4RFUzczdRWG9URTQzUHJ2MGhJSE5LM2FiQUhKZnAza3VlZ1JoRFAwcXVROHZmdEcyT0V3OXV0RFlDODUyUGQvQ3k0T0RRVWg4SXBwbWcwdS9URXd0V2FWQys3NEVMU09jS0ZHQXJqNWJNQ01ETFNQanBYVk5YVmZXWVNjYzNEY2xLeGhFM1hubzhRMnVjRzNzY1ppcUNxbDFGWjBnTGVFWm41azRKY0NwWVZVQkRrcnY3WmJjbzQ0VGlSZE9kR3NQOVFZU1ZOcVEzMVB0aDhPTnBzb05aNmxoOXVqcXV2ZlJYbmxDb1hQU2VXWFhPUHUvNnFxQmd6cXJoc0VRcGk0cDl4QVNHT21NVFVsQ2xYZHN0ZlVxcVhSdDRlc2xBcWxtSW1vc0I3NEQxVzlQaHpDamwrckszVFpKbXZQTzZlUERPMVh1Ujk5V3FvRWdEOHFJYkFkamdTZ0c4cEM4WG9uWm1XZlZvSjdaZEhGUlBKdk9Bd0hwS0t4Znc0Tmdjd3hxMmY2VHZ5bHJXN2pTVGxab29JT3M4eXF3UjhUSVdPNGVlYTZMck53a1FMdmkweFRMRFpHa242cmw2Ry9FNjdwR0JwSEkzODgrQ3BKdmZxSWFOZm9sWW9SVTJFczZYYnlhVTNxUzltOFdmd2QvUjRneDR3M3prc2xySngrY1M0ZkJ3emt1MzZkUXpJUG1JOW8vYWJmYWpTSmlodzdvc2s5QmdhTEl2SVpyV3daSjIwZlhsajA5U2lDWFdqTnlYM2J0Z2FYMzlCbG1HRGVZMWtLMWNoQ1BObVNITjhxekh6b1V2YTZyc280cHQyT3czMjU3VFNxZzI5MVJVSDNFMGsxL0x4VkJESnhrUklCVEk2QUlwVzlGeUZaOHp5bVBZajNZZ1loRkdReExWRTR5SHZpMmlLbkNXODVWMGduYmpDMytGRDF5REJKeHFQZVhKVHpPa0ZyMk1XL0xaT2luN255d01yQUhmaVpGbGVjNld4QytHRjV4dzVLWUJ5R2cxWWsrN1VENVhUWnZkZ1cwZ3R2UmR5OXBZTEE3ZytURjVmbW5raXVRV0dVMFoySUdCaGt2cjc2OWo4L3dxSXFjbnJ6UC9LdG5MT3NnZHRJRlgrQWlJN0VKR284dmlPSkZSRnR3QjIxQldNYmFINzRtVDhsS0NlSnBWTno3RktLZXFzcTR0eWFJODRoRlczSUkxQjlWZVJwSWtjaDNUQUFvWURrdlNXSTI0NGNoUFRMNlh2RytaZG1BYzQ5ZDYvbldrdGJ6L1FYVFhESGJPeFhZSDJIMENrbnU5SG1VVU1Xd0pOUWhIbERKbkZTbnBvTDhYNC81ZmZlWTBwT0FIbXBaSmlWTGU0WUFEQ0RyMHVNeUhGaWZnbzd2K3ZYeU93Rk9rM1Azbmt4MzJ3ZEZvNU9kSFIzekNZazMyeDdNU1JRZ2VMOVNaMk5FS3NsM0tMNlhpc0NCaGNXV0NJbDhCdUkxYWdhZUVmRjk0MTJxRVByc3M0bmJhYjMxM3NtMSt4RG5MbGdkbEdROEwrY05Sb2QyUHNxbTNJRnNzQWE0c2h3V0h3TTFUQ1ZkUkgwNmFNSjNaR2tEMHdzUnBpWjA0SWVpeGxGZkhZRDg1cjJCUXFHMUg3QllNeVdET0oyaWYzYmxXZjhGb0NRNTdycURZU0RKalBhZVdCL2FkdHYyMG9OWEsyNkc3YWJFY0xHWWM3c0JOc05DMEt6R2JzTVBzNmJyckFkd0ZRVzM3VFM4UzZrb1FFNjd4TGcra3dHRUkzVVZDOW9TakpFZHdxY21reWVhdjFRVHJKa1lkN0Zudjc5T0Z4M0xwUUVlQWF2VWZaaEIxVm45OXdxL2J0TjFhVlhCaDhMS0pNYXd1K0lDSkszRDNXV0dDQmttVVRURzJzSzJVT3NyN1psOUxoNk1VSWxrWjdQczA5TExNWFJZZk1VdFRiUWVjU3pFT0JTTGsxVHQ2cnhlSEVuRzlwQWgrNzZRVDZxV2piZjI2VnRtWjM1eHRtd2p0TWZUZm9qRHIyNXFKdmFHL3RZRHpqWEhsZUlPWE9uUEllN1E0aGp3UDRtVkhadDdmOUg2WDVXZHBmVUxSZkxzWk1IT2ZTL0kwTGxRVjlIUXIyR2NuVFZBRG14WmhwZ1NyOXcvbVV6dldjQ241dGxzV2dJNVNXQWNkL1hjUE1nR1Rwd24rMk45MWhwS25UY1dMZG9MSzdwMlVMdGhOZTFCYS9WR2NJZjJmbWxLaFN2cytGR3pjWHRYaFlmT0dJK3RBMlBZTkFRcEFVZkhHZ0x3cE05UGthZTBaN0tDVmhuZUZLV3o3KzVzcGJBSVpZZFJuZ0R2SEZJT3FPZXNzRXVBQTNlN290b1BpaG82N2l1Q2srYVc1MUtJMlVka0o2TGxoRVpKME01cWdJWHcxWXM3VnA4NkdsZEptaGlNMjBWc2w4c2dTdGZNZENsMXhnMXlrRUdFRXFEMktnSnBhOHBKU3BQc1gza2U4bUxCRXdoZ1JPOHVUejQzUTdGdkV5UGJSV0ErTWlEbFNDbjhqcW1nUE5ZR29iU1FOVWs1bmRIOUdrdTJwVW1hbndvR1VkT1NyaG5YMlo0dSt6dnJzTzhEYkczYThuOElGNEkzRkUyWDAzU0c0MTV3clFmelhDTXQ5TWJNZlJNSEJnSzd3UWpUeit6QjVZM3dXV2xISnJsTk14RERZQnpXSTdEcS9WM1hBM1puMVFhRnM2Vk4vWTgrYytOYzFkTENQaXA3MnIrU2xoQjQ4OUVJRWoxVG9TYWhKSGJ4NmVPb25aRXUwY3JZcnpWUXhSK1U0NDE5ZXFmMXJmcmNxT3pNdjJ3SW1EWmo1anFQR0Y4TU8xTWp5SkxaZmhYOWk2Ni9LRkdwbU1SbUFkSWJtMERwSTNOVVNod2Y2endNNnNRVkFQRkh6K0pwMmJtSi95Z3c4Z2pXOG9UdXNxUWM1VzVTTHFFeG04UWxNQTI3cncrMzlFYjg0N0RGUE5hMzd0enJzRHNJWHB1R3RxQUFmaGo0OFBUZCtwVVlkQmw1a3hYekpwdzc2YXdwT2EwL2d6b2lpNENZN1FPZjdzZ2o5dU1qN3FLMmxHa1RYMWIyWjlaNXhoanVlaFlldVVzVkJ6eHdVSlloUHBsZmFSUDlkOEE3M1BEOXEwSW13MGY5c3h6OU5yYlFIbnlWdjc1SGRhRklIdytHeGdidWdHOUp2V1FWN3g0ckx2aEczVjdiaGQrSWJZcVJuTDFDWEdIdGY5d0N6RWRYUlZIZStuZGIwWTh4Q2xzOWxNU1F3elpscFVkWEZ5L3ZGQVY3ZllHMnc2NStBbll5WHpiWnNzUnFiNzRlT2xxYWFHaVhzY285c1g3aWFqU1pDZFg3OXBmQnc4QUhsL1RkN0ZJbXUwSGx3MytqNWhkVXppTk5lOFQ0cFRWemEwdW5RMEZsdXRGa3owTlU4elFwZFNBSkNHVERHbmlnbndOQTI5TElXcTdiL0lpZ292U0M4Qk8wcWxjUWoyNVNQaGwrbGpFc3NsZ3VGbkkxM3c4VkdwMW1aRTZPZ1hJRmxva01nMzJ0N0o4Y0pjUXpCSE1UaXRkWE9UY3d5WE81SVF6d05nb0FMbXNWUU02NGhQTzd3b1A0NG01QzJMdEFUNU1sOUpWZkF0UThBVmFKd203VkppbHVEbmhkZGFtbU1vaXlyR1k0YkI2NElENUpRejVNYXp6Nm5nejFibVc0TCtPYzlWdjE5N2VqQnNFY3FmRHg4MzVOZEk2QmpkbU1KK1V3NURKWjgxK1RoQUQ3TnlnSGxMVTVXRTJ6bnhPcGU3bVRaYVl5UWlnL3ZRNjNmTituelV5MldzQkNjUTUrMWp1dDhoSnVjc0Y2a1ZrQkZrSjVqWi8weS9mWTZwWkJUYmQzNjZzaXRaSHZkcHV0KzBsSzBROE5SYWtzd041Zmd1ZkQ5Sk41QldUUmFQSkNDVWpjeU84RnZOTG10UDAyWGhWM3FhVVd2V28xajF2cXBaQm1tSFkyQk1FSFIwU2F3Z2luMzJlcEJpbVppeW41aXpseTJBUUxTczFUbFYvUnhUbmlmT3hyVUtGcVpFRjVXWXZHcjFGWFlCb1VwNmV1aElaazkyVnhNU3JvQlMrZ253Zng5aGpIV3RJcUlXSTNLbkxWRUY1WXQ3SjhmblhxOXlOd0ZqdE0rN3NFcjg1K3lYTjhyWXBpSHp4YVZrNDVHUWxrKytzZkhYZktzRk9BUXplZ2hUQjZFM0R5dHZVQ3NxS0xZTGtweWRueEpqVitIUFhPQXRzWmFTRjVCRWtxcFNSbmxxZDRjanJuaGI3QWRLa1crYXpScGcwcVM5eHgxdU9BL0Z3bVE2ZjF1UFNHWXR2eWw5Q0ViSUdweHp4YVV2ZDY5MDdmT0Frek1QeTc5cmFwMkEwNWFobkMxU2FvT2FKWGY2Tm9hallsVyt6NFZQWmpwVXJKMEYycDlNV24zTVNpSS9VUW5GMjBJNjh0RXFoY2RqUDJldXZIVjVKWXNsdmRIUVprSEZEalpaVzY2aTFoUHQxL0RabjkrR1lHMStEcXM0OWFRSEZIZ2MzUW5mWjA0d3FMOTJGMWwwSkp6bFVtaGxtN3NvL1pTMkQzSmJYQ2t1TXVSeEZTSUxPNjlRRWJJdEUwUHN2MnFkeXVqOGNWaWplRmFJU2l1KzA1em10TDIwckhVL0xvNm5ZeWhoaWJhMjlqeEZ2dVlxZ2xzMkV2VzlBQkFWcUlYNTBqZGVGcDJUY0plakEzOVMrNloyQy9FNlQyaXN4cTNmNG1NUFV0c0ltNDJZblA4YlFKMlFSM1loWE9vSjJNMk4weUp0Z3pYcnc4SzBBNmxHV2RmUDdyeFF3SCtBSVBzUHdIakZQUld5eGY2ZnZCczV2djFoVmZmcmVNb2tqSUhmZFhGL0RMMUpwc29BNGdPcjN2T25uWS84a0prc2ZlRm5nTmFMYXY2aHpaT3F5WENMMU1tYWRIa0lEdU9LcHlPU3paZGRmRVZTajNHY2JjaENuVWxkek8zTWpKZ1VlVXNzc3ZlSzc5M1RyU2dLYTd2VmFFMmdDTTgxRlVUZDlYb2R1VVY1WEFiS1FOVkZLTmtJbzlNUDdaRGZldnZZdUV1RnhUTE1BaTdoN21zVmdjRFJVWld0NWdJYVZpWHNUNkpnL1IvMUM0ZG82UlI5eDJFMmlFeFBsaWxhK0hyQVFCK1FwZHNHVEZUeURjWE55TTFxU2l6TWltWmFJTUd2OGwwSVIyK21RQW5uZnlkWFdlRktLNEZpcmdtVjVJQU9sT1cxcXoyR3ZQSjFISFkxL2lpWnRMeFUzQ1lwem9VLzExTzAwL1prOTRCOE1vV1Rmd0VWakFXUkE2cVdQOHByYkQ0K3dFa3pvVXJTRWZ5K1A4L0tnYWU3L09LL3VmOVJVZVFjeFY5NklMdXIrTVRxVS9vbTJLNCtKcVJPeWtDdXZvTytWeTZGdDNYelJlVXdGamtJR0pwSXRJYmJTYWpjK3dVZm8rSVBjWnhzQUQzLzBFemlNK2IrWWxndEM4eEdIU1NLQ0hRUUVyQTUzVEdKQjl3clZWUE1RbnczVGhYN2JBNEp6UGV3NUJvMGJ4SHQwK3RUV2c1QkpjeFFJUTJhQTJWSW1ldUFJZWxTSFlTQ0ZqM05VY2hCd1gvTkJPYy9YY2wvdzhEQnR0QWQrTzVNY0ZRN1VCTGFmcVFXYzNQekhjNWNkZW5vN3NUQWFiKy91VlRmb25jUEVwYlRlR3lPWkdpN2lIbGwwUStXKzBoY0t2ZHVzQWwwdzMxeDhDVnJEZzQ1bVlzRzJFVUhuL1k1TWl0WW5hWWFPQkFhSlZPUEwxQ0JCNHFpMWpkeklrV0g5RFVoSkJ4L0ZyeFNwSWM5UXdBSmZCT0hYamhOd0FWKzlYbzJiMjJmZXhkeUpsa0duMS8xZ1lwR0oyZWhuRkxZMzREMk16dFNNSzJycVZ2dU1UOWV2N1JvOTZvcUZneWEyUHQrVTZ2M3NJbm5QSVM4dFBmUmE5N2NLZGtUUWl5Y0RIMkRBVUVnMzRROERZRzlrREJwWTVibGhyZEN3TGF2SmV4TDlsczJtcWdOZVJYTkQ4cURlWGt1NFpVemhOS1BCMHZ6SnhzNEhYRmdaalArRW1HL2NGUm5teDZFODExRStzdWpkQ09JcUxBNGJ3QnR5bk1lU09uRWRncElMQTVCT0ZLMjBoZEdWK2JyZkNhY1BwTXB5VkZyV0xZNlpGeGM4ckhvcEhPRVlBQ3hNY0xzeDFMTmVCU2RzTzUwVmlYMjNyZ2NlWUwyajhJN1Fna2NobmxKOTNTZ1dhNS9VbzhWTjBvSTlSeERUdmVFUTlURCthN1o1OVQ2NEl3NVZyQXpLNStJdWJHT3pENm5Cbnk3NWtBZS9YVy9WdVhWU2t6NERReW1vbk1ZQVRQL0VmbVNHdEgxR0ZwbE9KVkgzYkRkUkJhcTM5WGZpNVZQUnFLQzVTQ1ZTVHNKVXZPcEFvRWM1WFBuQnpCY1lIM1FDd1dhdXU4a1ZHQzNlZys0eDYyVVJ5c1hKRnFRajEvS1Y2dWgxdlFiVExLVnNXVzJCY0dWSVM1WVY4Vy90eHlWTmhnV3NkZTNpdWlJeDBWOFVOU3crV0lmK2ZhYUxlcElRQlBzQmtMakJGaTJoNXFDanBBNHU4bFFheUNYTXJiR2dwUkExSE5adEVYZk95R0dSM3Rrb2FCZll3bEM0V21xdEJSQklpcDBTdXhTZDZHUTJQUmw5WXBuU1ROMVNYSDRVYS9vcDRPQ3UvSyttaERQczNCdzVMQ1VkL0I0VU5meWkwMVBCUEQ5K2VNTEIxNU9ZSVBRdy9jbHBzcGRLcEdRNUtUZFZvNzJHNEVTdzBDK2VDNlFHdlVBRis5UCs1TmlKYnJVMloxK1NMSWJpb2VIZE1DT0Z6R3dFL1dMZVFOc010RE5uREhLanBseGFZcHQ3L2wyM2ZEMzJZY0dTVVJwa1EyZGFkeWRxdnJ5VkppMWFQSG1rWWhlUWl4bnZ3VHQ2REdHUUg3emdHVjVUak9rRlZQUzFwTyt0QUd1RUV2WllERFZ6SThpSFA5TjNxVmRyemY2Zm9nTVhyL1BPMllhMlNyUzBwVVZqMkpTK08yNS9pV0NIOGVwTUhwRVI5Q3kvbHN0M2FqOHh0UUhSc2JQZEs5TmdBbk52TUtqdzFRNkRuaEdQVXVSWDkwbjViVmh1SDE5elFGSGhBNjl2cjFDRnJiN1ZCenp1ZDBNMXBDRGtONThYZEpXSUhiN205ckVtaWVUK0JVWVJxNWdnSkR3N1ZWWUI1U1pOL2pNZHEwUUZza0J0Z2lpZVNkUGxrT3NGRHQzdUhOV3NKdXNrSFM5bkQxZWZJNURJZ29BMnVFTGZKRmtpMFIvczhURitNZFRveUo1dkdFQ3VrNzYrOTdvN2FkOWdjeVJYNTF6MGR4RUVwMzhFYzJvRVhYYS9wQ1BPQ0xwNFZTVncwdUEvTXd3N0E0ZjR0aVFYRGhaTC9iZnFIVjdRT0JsK3RzV1RHOE1aODRtSWdsRGg2K2kzOGxwcVljNFd4bzZUanZkUnlnY3hhVzBOVklzL0RzRENSQTROenUrUVVYK3g4N1hUb2ZXZlcxSncyckE3OFRaaFZIMUh5NDdYQyt4OUdYK2VRTlRpRXZkS0lORUhCVU9tcDFNUVhqWjZCR2J5SUhaVjc0cFloSGFzb1NwU2Q2N1JUdmkxNHBaTGNvYSsrYnZacjFvdzZwZ2kyWmM1UmxvbHdFMTNJdE1PWVBudk8yNTN5dGQrS1ZuOVBDb0h3STRHY3hqNHJGVUV3MVZ6WDNYVkJtK29GLytBOWc5N0p2Q3k5WVNXQmFueGkwMHFPY3lsaGpDT1ZyMkJTZkZmQUdXVVJNcEVSTkFINWVMQXMvVGRJM3ZCOWdQSGJDbHFHTmF5ZlY1TVRqMlh2RU5KbzY3ZVJ3cEVGdWJZUHlPdTZDMEtTaC9qYXV3SEJkYkg3ay9DMWFoODg5aEdJcnkvbjRFRkdNZXA5ZmYzRmdZV2loUFFvWkV1UjROeXk5L3JzL0hMNi8xeFNJOUpZdFd0NnZNN3NIV2xNN2xsYTRTTzJIN3FqSTRBZkdFL3NONTZrNC9LZmkrWVNXbXpvUERrQ0huZzRoZ2dUR3JoUDQ3V1RiVVFIdnQwTzJTNVg2WGN3UXJJU1l0UlVrZXZwSTJjTFhoUDRLekc3aDQrT3Bab2x1OHZqa1Qvd3JtTEk2ZDA0ZXkrbjlZWEhaZTUreDB2UmFFYTNoY0lWZ1dZTFVnVkRRMHRVMVRvUVdJOC93dnlrbFpWcE81anBlSk9XSUpUN040T3AvREJWdlJUandvV2szU1VRaDA5RG1SdTRoeGlSQncwRU9JNXJJdUE4ODlMM3BYZ09FaTFiblJOWlVBcFJJN2g3Qm8zYWVPODhYZDM5TDNMREtJclc0b0V2aU00d3ZvM1d6R1p6UlBXYXdnQlFhNGFTSlJHeEZyRENuOFhsUjZFQk9zcC8wNFpsVnBKTC9nYThaNGJrNTF6ZStCbWJKUTNWMVR5WHVKUGZwTTFQZkw1aFpqc01MU3UvVmlkU1E4VW5nTUhtQ2R6Z21HYnJPaUVZcnd4NjZCUEZVUHF4VmFIWkNRVFR3UVZDNnNldE9udjcrY0MwTUswdGFNUGxuZEFMZXRSU2VzV2UxQVRVNWJrbmw2S3FFUWc0eGt0Qk5BYTdZNDloTUExSzZ0QjVkWWIvS0hPdHJoOVJHcFFWOFFRWlVFbzY4b3h5YTk3WjNNWWtUQTd6Sm0zbmNkTXhzazNrV2FJUXJOdURoOWFUcnppZzJEb3ZKZUt0RTBwUWtOSTdFZVBBQ09xWWZqbDhVYWF5Rks2LzFZSG15MDhiU0MxSUFMclp0Q2g2L0VrUktiMndScWtIOTlSL0toUUtlSzRGZzFkWlYwb3dmOUZYU3BzTVY1b1pQUnBhbVFzbWwzOHNSZmlCTGc0WTFCQy9sREh2ekFSdmxMRStuSkt3eUZPRDdpcHhKaG03SGRrald4eXlwSnV4d1hPdWFlK2VaR200NWV3SGc0TFI1M043ZGdmbUUyWUJXQ3hielRTSE1YKzVkWW16Y1hMVTZIVUkyaDY4SGlvNmhUMy8xZVN6b2JIYzZZU0ZYRk05SEtKTmtra0ltM3IrVGdLckVHSlB0bjZ6NDhSckF0bmFId3lEOTR3alNKK3RBQTZHa29hWXFZaFY5WTZDNno4YzEzNDdRZkYxZkZBQmU4WjNCaHJwak5wYmlPQmRrb1A4UjVmS2RGcDNJSzBTUzNEU2JnTDIwYVpya1doS1dVYnZramRHUTNvL0RTelZuM0lBd0NNY2xjenFrRmVEejd4SzZGOGk2V0tETVhhSW12VUwxcUZzdkN5RW5PVFJHNHRPdjJRWlZyMDZYL09SUEZqUWdkV25oVnI5SkRldVVMS2tYc1JVaHZTK2xpeTBMYURxR3A0Wjk0WnZtOGtreFpQWnB0MTlEK2I5c3ArazBLenUzaVJyUDBvMnZnNDQzeWw4NytEbTM1QzF2MTNhakhaUjhFRXkrLzArSkRoQWpFdVdMbW03ZFhQQWgrcDJmd2FHaTF0NmhEeFp2TTJ1WFVieG0ydzVUUjY3VTdGSitmUFZHYTRleFhsenhQRDdsc0RHc01WczFuMFhsV1RpTi9sRUJLdlpUSHJmMCtzU05uN3F1SUZJeXRXZVFaRERQQWlKK0ZWTi9ySmFUSnRRQ0o2VlJvOXg3YmhlekRKMWNQT1MzZVlnRnJmcWttTjBhVUQwNHFRZUtNelptT24wdmNTY0tNYVM2Q3ZlQm50OHI5MTFQWkMxTEpZOW54emNONUg1eWhPSGI0eXRYQ1dndE1hbUFvd0huK1UvOEtqMVBOSHBjWjh3QVZEMnJnRlM4bWZVUmduRStQcjMvaE5Rd0hPVm91cG5qRmlPSGVCWjVWd2pjSmFnYkkwRU4rUVM4QUx4VWpESS9EMC9GMW9ENWF6WmNVN25CdTNORU9DeTFQZkl2UUpyK2NzTnlsN05lVW1CRUtaVHJQYUJZU28rK21taEdUYjZxdVhUV1pUdC9paEh1bEEyVFd0ZmxYRDZBR1NmaVpIekgxdUFMMTBTV2FJajdxS0pQSUdVQkRWM084dTV2aEtUQkl5Z3V5V0hLdEkwSFZHTHp2cXNiZ2FIdlZqdWdFRHdUdWRPNTBmN05GTWJqN0FTZkVUSGNicTNuY1V6UFpvWTZEdjA0ZklnNmhmcnFreUxxS0hWbE9DS3cyeGlRZDcvWGhqNTVBTkFzOWQ2NE5wblV1K2Z0a1pXVEYxWVhMUlVVcHdIMkZMUkxWZW1BMWYyZlRFWUdrVTRySnkvbmtxaVg3cnp2NmJnV0pUaDg2dHAyL3hCZ3ZvYS9mM2c0UWdlbmQ3R0RDVVBNYmQwcmRnT1NqZzlQb2hNNEYrT2RxamdST3lSNjZydkMrazlZbkV3YVFZeWVJU01DczZVYkdZR0VQMWE1RlF3cmhTWGQwdk4xejlvWTkrZ24vSDlwVzMzZDd4NEl2Mk5ZbThCdHhxWTd4anRjRHZTUGJLMlFHSjBnRmlqZzVYanE3TDltbEoyZGtqTTdmMGVFaVQ1L3RILzNlaWN6MXlzb0hTc1NuRCs1dWN5WHdPYmp6c1RIeE5zWXB6WXNBbXZlVVV4WVNKaWJtRjNjZ3RrV3drMUtzT0VRZllhc1RMMlNpT2pWNnhvWTBvcDRXOW1zYy9HOERWaWZ2SHAwN0dEWElDMWFVOWMwRjRPR2NLZlFvLzQ3MEs5T2ZmWWdQK2Zhb3lTK2s2ZjJERDUyQUxURUQzNTBlajZsTHQ2NGpkdmtOaENsUWtoZ0JZK2F6U1pQa1RlNXozYXplK2IxcVhaUmRUaXEveWFPTEl1N1JpWk5zUGlYUVlzalpHTWJnSUFOZ1FuVjZpOExHQmc0TTIzQm1DbkN2S0ozeHpycUxhV3BmVERYaFJuL1RLSlR1ZlBMWmc5OVIzZnBGSmtPN2kyZzZXS1BWd0RkOU5YWktSVkNLblU0ZzVPeVBodzByWGdqUVdLOUNCcUVvMEtxYmlhdUYrNktHWXVuTUdTTUNHWng2a2Z5L3B5eW02aUFQZjJtZXBGYXlZZXdBNHVnZGNGaDlzZktMVk05UUFYRHBrc3lmdzQzM3orWGExWFpoRnNOV3F2Zk1NcGh6NnFlR3FYUWpPZ3BpM0NIQ3lUZkh6eHRzaDNnSkVGZXN5T1dNWWNkdHVoM1R6YkxlQ0RRNThncFpXZWFTS1h2eDZsTENzUjJqNFAxRjl0ZVBDc0M2N1lqdEtRQnNhRGZ3U0pSUExPN1pNTnNmRzFlbHdjUVJXKzgrMW0rM2Q1Qy80OUJDWGhtQi9vaGYzUmFrY0FRVnVkSG9jTWpEUitPTHhkeDlJbU9tUVRMTHhNWkRwVkxabG9OakhBdnIycTFIZHNkVGFBbzZ3NHZhUDFveDFXRVpSWW8vci9YRWkzSWNIVEpGVXdQcCtLby9WdHhHczN5ZnpDWGduRm0yUE1DZHpvUWVtcUtMSmVWbnRqZWhRM2VFT2xtM0EyYU1kbjluZXVEVmVPb283UEJTQXlWakVHZWpQdWY0WVNGckx3dVhubEVDU04walVqeDBRT24ya3Z4dXFzZWVmV0Z2UHQ1VGNBMGZMVjluL3ZvZmxwQWpBUk1yOGxTdWZrT0RPT3BJcWZaUmhtUFY5YTcwYUxEQlFneHhHai9mMDBTSXNnUjJLWjM1ZUZJNDVFOHc4K0tBOHh4cGE1ODZpM25VZGJ5c2c3eThFZENlUW9sZVo2QnFDV05TYzRFY0h2QVIxUks5eGxRYjRBM3dxZW50cEVDVUE4UTBycHRPY3NMU1d1V29jUHZFRlJmR0ZDWFMvaFczRXV6dktQanFnUlZhMDVxby90VVdmczJwa2FsM085SnpiWmx0TmI0WXFUNHg0RHBSUmZRcnJXYlI1Z0VsWDBxd2huZUl6TTFPTVplT3VBKzBOc3ZNYUFYNmJacUdzU2xBNHpQSWh5R2FvQS9PYk9UUmNaV0JzOU5JQlNCbWlFTHRBNWhIS2ZDZVdJYlo2bWxIRUswN1BCUGhxd3R0VTVmWXNmUUtmYWNmQ3d6NXFzZC9PUWV4U1pkVXZRWWJ6S2VkZmhNUTJPNEtNSk80aVJ6M2IrbFo4ZzlzK2dpb2VJMEdNWWtKQjJ5Q3QydnBlRkJrbEpuY3h3STJ4bkc0VHFCNGltMEo4YS90SGxMQ2xWYkdyTGdKWkg2aDhpSFV3R0V0cFJ5VlhyUWo3MTlKUWZvRFl3Y1YzWjFMRUZrKzhkakJWa2lqSFNQUHlQVTcxUHRKVHQ2RFlJZDA5UGdoeGZURjkrMVpqelVOTEU4aHNwU3duaGN6YXZSUHFaQkJZSk02ZHVpMytaaCtYc2RjY0pBTy9iVE1pOVZ4TkNLc1dVVWQrSmZ0ZTJmV00vbDFRR1h3ajRyQWRjSmIwb0V5TmpaN2YxN2paN3B2bTg2cDlZVnNZL01EVlFPZHBSOVFLZWFVSUFuQW1EaFQ1cm9tVnBqWDZWbmt3S0NOVFdFQlZIaGxLSHptZXhUbXY5dmZXaE9sQ3FWVmRQTTFZVjRVVlJ0RW1tZ3BvVU9seDFxWGpYQUNjTDR1SURLV3pScmtVcFJDMkZtUW9FWW91YUpyeFhURHZ0bWNZTmVralZkTTA5N09UclIxWlZTVXlady9aRkhXeDgxR2xuN090SlN0NVFPN2NvZzREeE5FRksyMm5oYldZWlRvcjh4dTJycmk4aExpMGtuZmJRMkI2ZFdLWndpQlZ2VWhNZ1dCakRzQUw4STZNOVpGbmw4Y1BZMk52dnMvUVphUWlZN3N6NTNlM1dod2xVblR0Z2JaZ01LZEVCeUtsUUVGYjZFM1JZQVhlb3BCU2wwNG9oMW9yOVB6QjJCR1RsekUvdUY2ZktndGlzeVY4WERobFRQUk14bEwwVmZhTlNreW1JYVcrK3hzME94Ym1mci9jTExCaEw1OEo3Q0hhcTdIa0V4cnNEN0swRVBHL2JGc0prYVA1ejArS0ZZeE1KbzY2eC80WVJNeStRWjBJWkxjTlpwc2gvNEgzRlIyWW4wWjJ6a2ZXcVYrcHl4ZnRlOURHaHBGSXBGL2lGOTV1TW1kNFk2c1Vlc3cvcU5zcjl6TmZQYTBicXNMRWZnSlJ1ZUUwd3ZOYkVDMzlUbUpvM1NhczhmWEhwVzZudjBVS25sSnFSOS9aNWNRbXp6ditzWFlNVnpxRzVXeWJRQkdzcmdzVjlhbXBsQVFFSmtORGFlWlVyMnN4cmxKc00renJZeUUwMVdZL0ppZjJUa1hyTmN5THYra3c0SGlGNEt1REpBd2gvQlp1cnNZeVVPZnlMM2xrMDg3cFIzVFJZN045MHBoa1VGdnBWVFVkVlN5ekZUcFVDUUM1TU5TZWpMQ09qMW42NkVyOEF4YUJJWWdLNjlPZ1FxVC84ZVo0NWJkS3dCcm9sRXNzaUZNWktscGVxV0VkSk82T2RxUzd6ZTNqSndxZmYraHFwQ0E0MTV6ZnZ4cmpMdUR2aVcveVIzV3FnZXUvVU94U0hZWmY4dXBodTJwUE1wZWRkK3p3bVdpMlB6V201MndON0FubXZldHFoZEdIMFp1MlQ5clY0ZXIzTkZqMG5qQXBOVElxZnpveG03dkFDcURNa2FwcExYblpsdUltMURkK3hTRVNZU0N3YzFHalhLNHB4UDgyaUt6cFYrb3V1dlZJSXpvWjZRbmhhb3VTRkFIMlVHNzZYUkhLM0JsMytLV0RLMmNUTHR5WnFIQXZxaWtDVzJOUDhPMkF6L1MrSlE4NE95L0pjVXk1UG9OK0o1NmpMS1gzaVJtVjhiT3BZNHZKN0lPbitBWkcvNjNTV3puazU2KzBmNkpEeHJDT2thUlNVck1GT2hleVJaR1Y5STlZaXZ4d2xma0d1ek9yZW9YUUhiNDROMWFNNWVBNjNsK1RqQWxWNUcvdGRFTHhlMWdobDhPMzRWTnE1bmdJTHQ5ekJWVHJMSFBjL1pZSWJlcEFmaWdjamFFRllDcmdBNDlpOUlDZHFGUFg1dCt2S0lWbHBDTUpBUkl6K0NwcFlLWG9KOWovOFhFVXAzKzNWVG5Na0E3S25IcW94MjdCbEptNlVhbStFYm00aTRDQWpqMlZTRE1kV3lxa0p3dTlURDRUUDU5VzR0L1ZUZFJjVWltSEtzbnE4TmEvZ0FkejVmWHNoYUx3dUxJSTdZOTZHMWdwWnBMU2FFdTZrZTFBTGxEM1ZOMjlPNUV5MWxSOWswR2ZlZWtkWWtXd1BULzRoanJtSjN4Ri9pNlR4d2RoSkJaUDRYaUhBelBOMEZ3Mm4vR2dtTTd6cjJCTVZQNVVKcTJHSlptZUExUmlXZi9wektUdCtPOVpZNnRwNmg2TU04Z05SZ3hRZGFKaGtKTmZYNHV0MUc1dlVJVFV4OTNkSzZ5M0FZcSs0bkJwV01tVHlxQldGSUlxQzd6dEF1OW54OEJ4US9hVyszTVFIR2RyR3QyRUNsOW5BZ1ZVWVRrSXdXWGxEQXR5ZVpweFcrTkwyOEtBNWpTY3JLbUdBRUZ4UXRqdGViUVZwQmlQcVcwYitOaFBGdUZJdzJZQkFsWEdXZFNJN3ZVOFZySWw0MUo3eUU5bFNTK2dDaS9GMHFQaWVXK29jSngrUGtGWE0vT3JCOTFOQUE5YmpqNjBlcHBweVRRRzQya2FJTlViL1lsZmptdSt2TC94RVdtSXlRWTFKdHlzZkUwdnAyV285WEl0eWFBZDhKZkNpS1JOOFlFRWdxUlhEblNhMmJ5YXF6Nlk2SlNDc2luRHdwMVJYbGFnekpoOTY5ZFNYb3h1aCszNXlvTlpQeEdodFp3Q2Mzd3dWZE43MXYra0pCL0twYm1BUTZxOHBqVXQwOUUremhWWU9OZWZwNnl5aUdXUzVJVTc1WStvT00wUWM5SCtNc1EwaEFVNTVaOUs0VUdRTzd6NDJyUjJpemYrZFFkb2JDc3FjRWhhcXZwemprNG1HQ1ZrN3daM3UyM3RrUXFhTzlVY042WSs1N3Y0ME1wZDliTk50N0JNMDNkM2ZtWTJCSTRCdTM0SDkxcWxlQi9aeVVIVWxoRWpubU9SZFVtaWpvNmh1a3JsY0djUWNDN1FzLzllcmVRZzNtWSs2YXpzcU15aXcrTTJJL200aHBGMU55R1Y2VGM1dUlwVkcvczN6OFNxSWJseGVKclNjOTI0NkhUcGFPMEVFRGdRSnFPZFpEUmt2VUZnVTgwdzVRdVRCSWFGeW1SNS8vZUVCSkIvME82MHJmcXBCclZLSHlYY3VGWk8ydXFTMnYrTHFOYThaZ2ZxYnRWOWJ1SlJXSnlrTzczWGFYbHU3L1Byek14V0d5bE5PR3YxN2RleUc0dUwrbG53M3NNazZPS0JYR3VUY2hCcWFIL2NIcXE1NkZaY3hheVJDVjJQOEl6cVJUUGtuTSttYXBmTTRsU1lsRUlRMk5pNmYwQisrQkY0bzhlQnB4VXExSXZaaXRJclZMZnZNaVdIL3R1dVBoRlFlOFNIYlM4c2hlMmQ2cTFXRUtpYk9MSllzUjE2eTR0eVNJQUNNQmJJRU5Yb0t4R1lFWnpIZEtOeVFUS1BrdHJqRE05Yk9vWUw4RVlOYUdiMzFocEtZc1FzQ2pUY2crbThRVEpueWJsUG9rT05Wc3hzcEpNMDZJZ3VZcHJQbDhYdWdScW9SREdGeE4rSGJiODN1YmtIYmNVWjhuRmtTNUpvcDBYY1BqRDFJaTB2VDUrakI3Q2pIQTBOc0NxL0pyN0ZrNlRaTUhreUY3N3dxVS9BOFlkbjBHTFcxWWc4WUVPUEFFdGR5a0xpSWJKQjZ0UGVlVVZrU3dTQ3N3QnZTY0dkYk1tSW1lbnE1MHE2M3BkK2ZTc2lidlgyd1ZKUU1neDhSYUZpYWoxcGlHYUxDS0Yyb3BiR2FTbmkzQnlVaFlnNncyV0U1cWZVaUJ6OHpBeXlMTjJJbWs4L2xZZzNWK1JxY2YxL0p1UHZWL0tsd2g1a0JBbUxUWjBycFVEZExKNlNDS3JvTEJOQ2VmQzJXdW9CSCtFSDVhOEExREdxZERsaDZEdG01dHhTQkZvUkNCdElLRUJIY29zN3lWNzVoSEE1dzBpUXIrNFdiejl4NHhwQkZQNFlmWERvV2FRQTdrQk1oaWtFckxCVVR5UTFlTVZNQzVJNnV0aWNPUHVYVW0waWlJUzJRaVU0WGlSVXFxRmJaR1JybU1iWnBXSDhXQWQyZldmdWVWRDRzcWxkMFRoa096SWtoc3QreS83S1owaWVzWkJrN0ZhNVA2dk5jSnkydDVpK2N3cXlKUXpVYTMzRUlnMEVzSDRQRXIycVdqNHk1MTRWRlVqQnlJSVpWZWc0SE9qS1N0VUNsWlhmakppZTFDSUZPN0ljbDFEcWcrNEhjT3BZbDdMSUt0OGtuazJLeHM5ZFdReDJSS0lpWTlxQlpkN3BoQklrNnBGb3VoekZBeXpPbkZsSEErblR4ZElKeUZlZW5ETDgvVmtjS1JhWlgvQnpMMDI2eU94dStEdHJmVjQ1bG1zTG9ob3VHS0tia3FyYXRDWmk4NlpFa3BpYkQ5TVU3Znh5Nk9ucllxVlVxTDg0dkVQM1FXU1ArSUtRbjc4alcvRzJjNTduTnFUVVZ3WHNWY3YzcXYvWitKY1dHTmE4SW93K3Vpc1BTSEdDTGI4cGQ4ZDlFa2VzLzJqanJqYlA0WmVnc0RLazU0SE95QzhvaDBpdG42L2xpKys3WmNkOVpVRGpxZ0dwUGVDRnNlT3c0cjV6SVFMbWxqSndpenVpQmpuK0RnVTVwYjBOYWdPSDFVdW1pL3dKbWJqZjVzaFM4OTluUkNsOFF2OUhjZjNWbEJJSHFNVUo0OHFGcVBDcW1leGkwQWdJTVFEeWlFSGN2ajZWUWJqRG0zeUJ6dU9nV0hUZUhLQVdTSVo4ZTFPdkNIdmRhVnpPbFBIR2dnNE5KTGVKRjY0MkNtL3p5RWVwZ2VERVIyeDZRQXQ0WWZOdTh5djF4bjdVcDBSczZBaitnRXJHeFVldy9scy9EaGpmWExqcTVJOHZ0cXo0UTdWbW5VVVJaOVU5cElPWnZNMW5DQ3luaC9MdU9nNVlMTldabVBtajZvSEpUZVpiZjhyN01OWVJyMzJYY3JFUHFUNjVOMlVBVnZkRFRwSjBySGV2b1hqYVozcWgweXhMTVB3MG54RVFqV2dvS1Fla0pzdDI0TFRpZzg3TGFDZHEyUzVadzFqQmxnOHgzcDFOVld3V0lLa01mbEFvQ3Vib3hEdHdSWENkaThwNUc4SURKTzRObTJxc3RnUGZjMG52T3plNlNCakZRUllxMGpuaTR5TVlNMmJOaHgvT0FOY0FZYmplWkxVek1QQk9Pby9NdkNjN0JuTUVDZ0FXbjR0SVJGdEY4MHVncENvdDl6b3BVSVpCU2VPNWFvalBrMnJDd0JLdW01cWZFZFhNekI5TU5zTUU1MWJDS0x6U3lEWjVpRHIzTGxTc3hUenAzZWNZcW4venNzUlBGVnBsYmVDU05YR29CZGRvOCszVUxJQS85Mm9Mdm4vbEhUdk5uYzlaaTVrM1JMWmo2empGUG9jNkdLZkxWRHo4VFJyY3JDUWM0SWQxRThyOUVxTzRMaWRwdEg4L29ubnQ3L0JzM3pIZ2lyMGptNkZWUkMzWTVFcFhqYUF4YkhkUlFuYTZrVTJpTmVTNWd5U0Y3QXF0OENjU1dNanRqL0xmSk1QTHBCZ2FoSGRIUmw0c1Q0L2JMK2w5YTlGUmxEc0pnS3VIeVZ2b1VldUEyaWQ3bmt1STBCT1JwTStlN1IybExLUnU2ZEkxTnFLT3FVUUZYQmFsb2NkRHBNOXhaaVB0eVBJd0RZa2E4M01FcFNsajhLWDllS2llMmlWMHBUd2RGNVhOT04xWVVJc091dkIydEN3UVNtengwUFFuMGlQSG04UjJKSHd0KzYxL1U2dUQxRktucXhxdGttN3g1ajAxZkJibTIwUTZCSFQxZC9LMWl3dWR5N1JBd1NENHIzd1BDR0FCZW8rTnQ1dGRLcis5cGEyVk5GZmZLV0lidGZhRnJtQzNGVDZMb05DSzJDbFFLbmlYRzZTMUFDUUh4dWlza3RhSUcycEIzUjhkZWVNL09mM01GYlpDeHAxa2p5Zzd1Zm9PUnh1ZEJIS3krNlZQc1VsQzljdE5mdEJEaFJnOEQ1TTVKS0tHWXFURjJRVFhDZ3FBY3AxTTM1ZXdKaFIxanNTWXBPQjk5ZjZwa1A5TUs0K1pKSEp1T3NZNW5PUTBQVi9DVkFORFR6Yy9vZk11ajVIQllOVk5aOXRPL0txRCtwNXMzL01nOUJnTEJQTVlGNWNxTkVSMFpxOUlSbE5QTUk3bUNLcXowWFMySWlNVjBsY3JCVlpyWnhqZXhSSEtZQmxxOUZYeGh2b1J3TnN2UXJJV0FuV1I3akQzQTZBSklOY1BIM05FY2NEQTY3SnVnY2ZCM2tYdGU2aDFsc3lON2ROKzVCRGZhbVNuMVBJM0FWeFc2c0JnVWdJNG0xb2hRNFVrV1ArSUJURWh6NXR4WS9jeFZjVW84WVErT05welQyUzk3VDgwYUZvWnpqV2pDL2Vld3NxbzExMkE2K1l2QldYdGc4UnVTUEZ4SUNXa2ZmMXFTeFArb2FnWEVpWTVreDJBWDlENVAzMGpRMHNocGJKc2xyNUIrRkRPQmJxcm9JdTY3MTQ0WkpZdXpaN1dveE53Vi95UGRsaWJ4ejZHTzNkektVVnVDTDVCbFVJUmoyQ0RmNzZyUWNkOUVsWUtvWmJmOVJ6UWg3bC9KM1hDRVNWaE1iMW83V1BTakNKYzVNbldzdlhTbGNuZ0VCakZ3MFAzbVNsNnNHYnFWcmFnWHczOXVtbkxOblYzTWU1UG1GOW8wWWNsL0VTSjFucHdHM3Vka20zcHl6d3BjNFkwaWZva2NBc0xGTTdrUXppRTFNNHVNMFhjSTVheTVCekorUDRRRGJlOUMrTm5vTVZiZldFc2tDaUJnNlpUdi9aWHBIeElMZElZMjdSV080T0xYWU1lWXB1Nmt4RGJ3dEZiWUNJRWJlUFgvTkZpV0o1VmJKbTlRa24zY0tKRUpqb0RmNEcwaUE5YlNPcWdTaEl1WUgrZzhDSnRReGRtUGJnaEdmOUJIeVRDTWUwa2d3T1JVMy9JYVJDZGRPMlFocDNMV2RFT2VWY05tdzFZOXhQOFZlUGVMK1RXSS9zL1JmN3hrTW8yNjJYWVNGUFJkQmU0UlNFLzZiUStXV24vcnU1K2t6Q21hNjR3VlJhSEYxN1FCelJocEMwK3dnRmZLcFZHaXBWMTFoQmRSZ2xMdnBFdnlMOVBvN0tuRDlWaUdxSk9sdWh5enhDNkgxeEhDYVlWbzMzNFhyRnlXQzNXcnJYdVQ5OHZTUGNiNjhrM1l3SnB1YTlLODg2bDhiN2JPcEpuM3N5aWE4VnVwQldycXZDa2NrcDcvYVRlUVhQK1lNQjNWbFdmTEVRV2EyekIwZFdVUVRydDlCODZucjE0ZStEcUI2bEJmL2FFeExhTmZlQVlYaE9UWk9QWGlXYzdvVW5Rck14RTR0Y28xa3AwNW8wemI1SUdJK2w3SG1xbTYwNFZpYkJSL2dva094NWt0dkZPMG5PUXYxMXI3MGxyS3prOXVxUlFRSDBGLzI1NEZyVUxRUWpXbHBVa3RjaEdGbkZsVmtONVZsQ0pDMzJFSEUyOXBjQVc0T01ncHZoWHlKQ1BRYm9XL1B0ZUNYNmpIT09mOUIxL1M0M202d1ZZUGFlNzM2QUlxS1dCbU13blBSaUhrOG5Ed1NvV0krVDVMaHN2TkRCRnJpTkJOUFJISVRmQm5QZ2ozMXRrSVNTTldWOGFjN3A0V2R2VmxlYVU5Nm5zN1dXVHNoSktjYWZpakZMcEt2TURuSjM0UE9qWDR3NEhwV0tCZDJvdWxkL2J1Z001dFVxaE03djRmdVJtbzBVcjRyNEc0eUVKQkV4bmNHTDU0VGlJQTJxOFRUbEI2dEMyM2RjZUtramp4RmdZdjNKcE1lTUdRUHV3NzE2T1NxNVFYZG1HUWxuU0pqVXhGVks3ZzFyLzFEbjMxTG5pVkZHMEhBTEgrL0tQU1NiSWRtenY3Wktvb29SK0xtSGRSRGlBdzNFc3RNdXd4VVpnSDlRRnF5bW5oelFsRVM0L00zT3JGanVvd2hrRXdqVENvd0NvY1ZEa0FCQit6MjVqSUdROUpQZkZoTEkwNmhLQWNvMFc3MUZLRlZDaS9pOGpnRDU0aE04a2tycTg3L0trRzJCQUNjb01CUEpHSngwL3c2ell2eS9MeE9MNHJlOFl3N2Y5YmhvUk5YR3kyMnM2QXZEZUMxZTJ6SDczb05XMUQzUTdaODJWVlFwWjdHM2pLQWRjL1ByVVFGNmtVYVB2Sm9RejBoWmRNTDU3aWxtVWordmN4UUkwOHZFblZ3U3B5Wjl5SjBFTkhwMWN3SmFxN2hCRTJRN0NIQVlqQkd0b3dkVFd4dEl5N0RjTlMzVFNJdnBLc1JGV2NtYnovOUdLQ1pWY3NMTGNEUXM3WEp1Y2V5bnR4WW95YUNIN0IrYS8yVjB1RlAzTm9lQ2RVNFFsWDFmYmZ2SDVyRlJiV0k2STFGRERDNWFjalFIUURmUnV4VnF1WFlIN2dWUWpVVVBITUxRYVUrUVplSjBXcHAzOERtV3JscXVOQ1NoT05YTEl4bjRIRjNXTTA2NWJoMkpQTFoxbC9oMzg1T1A2MXRwb1FRd0tFOHltZW5mVE9YMWtJM1Y4d2xEZVMzU21sTWoxaXNQblowaldZS3F3Tnh5bGYxdUFUcmZtUjl0L3hUaVNNdlRSUWhQcnZkY01zYVFGQkdKbmJ6eHdYcFVibnQ3WUNrWVNXWkczM0R2SEs1WExIOVNhYW50NlJacTc0c2owN2xER1grb2ZuVEpJMTZNalVxbjhNSVFmRXNMTEs4YmF6RTBvZTA0WnVITHVDcDFzempTaTZHWEZpTStRTGNMWnZhTkc3eit4VHBOM21sOGIxOHRwZ0c5aVphT1pBeEF4UFRuVHpHbzc5aVNhMjkxdHFyaEd3c2tBNGt5cGUvOG1kdUNHS2JwdGhYR2dSOUpuS0tpeDVCWDNZU2FsdEFhWW1Kc04yMmFOVDFBOEVZL1YzeFp2MXVNc2cyN2RJa0NJb25rTEg2K0JYSEtPYVMvU2JZZW9rUG1va2JHZythcXBHcnBSaDN1R1dUREJ6ejBJOTR3NHovV1RiN1RMN3ZpZXVpVER3YjZwbEUzRVBvc0lMZ3ZmNXJSd09senNycXJRa0F4UW9lbDdzbmJKWnl6NjR3dW0zZXZmS0xEc3VnTWpjV01NOTIySGZ2d0VKOGRTZTZTVnArRlphZjhDSHZTYzNTUTFacGlEN2kyUHJjVFNjZUwxYTdpaVZvUmJ5YVNnMUFoV2lqaG1sYkNONzJRYnB2b3BLdERzUUZZWW56V1psR3dPckQ4WVZCekNtcHNuYjBHVjE5NzBTTHR5aXpCdUo0Y09yYjJRdWw5WTMvS0RFL1B4OWk0ZW1vbmZZTGtDMEF5YlZxT3dROS9KL2ZFQTBjTmZLQ2xiSFVOTTRNdktmalBpcWxMTERIOE1wcm9nMGY5WTV4dndsVHlUR2liZ1BLSUt4TlkvcFd2bC9wUXp4VlpYMjFEalNwd2RtaU1TaEt0bWFHVm1IdUxGVndRcGFzcDQveWd6OUhrUUMxZGNsTVdlUU13Y0FwU2Y5c3JXRUdPMzgzMmczb1c1bHNnVUhvaFlzMUpsSjJRYmJZWkpuRnV5K3Q2WTU4djNEbDNNRU9CazVkSmplSjdZYlRNVTZXcjQ1a0hIS3lSSnRQZ1VWUk8xRkwyREdTR2kvSkRXTUYyMERWNlYrY2pQWTVsMVErZStHRjh4dUorbVRGMEVKaTJCYit2Z1UwdEtmaGFaMHNZQmFaR1NFMS8ySEl1YnJEVE9ZeDN4TEwvUVdKNVhjdUY3VGtud2UwVE4zV3BRK2h3cXpvZUlFNCs0aUd4WWMxK2tXYWlCTzE5VEF5WkUxV28yRm1OMHZ0NFJhUTh4bWYvT01mZ1c2SGowZmNLa1RWQUgvWWo5WGdMM3RRcHlFUXg5OU81TVI4WHM4KzB5UDF5Yko3TStxalBRbElpRUh2V3BQaHBYRG44TDlvQXlrZ1ZCMkNOUU1taSsycUY3UUgwUzVRRWZLT2t6MWorM1FkeFVLc01MN0F0WjVtdzNnRnpMVWVPQ0J2TEhEWUd1UGhuc1FnTlhoZmJhcTF4Q3RScWNjSlE4aURsN2ZqWjYyaFlpVVg2bXB5VWdndCtNUXF0K2tYNk1BQTE5SnhBNVBRQjcxeG1DMEpRMTFRZGV3ODRzSlROTUZtcnJhZnJwaTlQU0pnYjgxU3Y0WVZQSU5JRkNaYXY2elo5TU1lUXgrUi9QdkNpVHoyeVZLZmpoYjRvOUxFSEJvZ2RaSlkyenp4VTV6b1YyeTRmNkM2ampkLzdYN2VpeERwR01vKzR4MDFIUGZWQ2hRLy90RXNIUEZxbXhua2FEZGpQVnFnZG5xdUpWTGVWbXBSYzBXcVg0M09XZU5zZVlmaDlxdVZveHVLTFY5V2NxOE15TjB3VmRmUy8zQ24rM1Bid1VobFlYdVprSUJqd1RZeVZjdlBCdkxwQ1ZXd1F4TEYyTzVqU3ZqWEtQdlVQT0pkVldlZWhzL1BWRERTbTVZVmtRMUVGbGVvWDZDanF3YVZ3bFo3ZlY3Y2ZNb3NleHVNSWloNkhwZm4yb0FRK2h5TCtaWVFOYXI2WmgxRGpjY0RNVHFoUlkxZFl2eW1Ndjg5dWs3a0tFNDRqVllST2U0VDVrWU5CSEFPbldBaTF0OGw2OXBFUnZHcVJzVmd2blU0N2UzQ2lqSEdRTUhSRFRqbERZS0NCbFZVajR0TDM4aEE0T05yUE10UXdYc2htTzI1QmhtQ3BZUWE2UlZRS0s2OGdRaVk5bmd5NlJNSzB0elk0Vys2UWYwK1orTkpNSnZRSmMyQjltNE50ZUNwc1RDWkZKVi9aRW9INlhDbFZ4RmVINlJaRzNGOGh2cDJycytNa0lFOHJ1KzE3UXU0R2p6dTYySFlCcFhTMkYvNWZreHZVWG5WZlF5ZnRjVlAzL21iV1B5ejF0QTNyb1dzdmJUWmpoZXg5NTBZd0NtUUVrVUx6U24zUDhnR2haVnZyRm0rYzAzTWhXUmw0YUptbGNHSVlPUnduSWlXOHZIcEFuTVFUWTdub0RIZ3g4Tk02VW1FR1Z0dDdnWS90bTZORzNOQ1Nna00wSUNGQjJxYk9BWURNbGdSSGNCNnIySWdKSTRXbVhrRERmdE1UM0s4M3VVQThtNjVOakoxUktLYy9MK290RndvL1lPakxReHNYc1RxeGFkT3BDTkVhZW5ZVE9jRDNUNDkyUTU3bG16Z24wUVYwcXJPRTh1ZWRWdE9ZOTBENUYyRlp6bTZDVXVkWVIwdWlPdFp2czNHcVkvQ0xFcjdsUWJRMDJJTWNwRXhnc0oxQ25ybnk0TlhJM2pYUWc4aFhhS2dHTm1BMVpRNFdsaFNBT3A1bkc3QkppN0l0ZUxjb0w0eGZKY0trRjJ5aTRJRmFKK2Ftb3ZSYTNNdXVMUzljN09xd2pEQWs1V29hMTBGR0xmNi9uWTVsZ01Gd1pieVB3ZTdnVXFEOWViMEtyWXcrdEhtOGIwMlJFNGlUYS9rU21EU0l0alVJcVlDanNZUkI0b2xMR09mWFFWS2xhQndhYWxuTkIyZ1ZRMFozc0R5d3A3UWpGMmpuZVBoUENXRTV1Rmx2dTJMTC9CaEJlRzI2a0JlWEljMW94djlRRnV2TjJHaG9WM2xmd1VQQkszRTY1Yld2Y0hFZk1taEFxZzhDVkJjT0g5UEkxOEtnUjBkRFFqZUp3b2E2QllMWjhRc1VSOEJ4eXZwb3hpSnYxaFJjWlgyU2FNK0FpYVNKekg5UnhXMi94NUxvVGpYSGlUZm1EVnZvZFM3dXhlWjRldXdzQ1BQNGdQN01HdDkxZ1hvd0x1dy9PYzh6SmhXOE1CUlBFNkZnS1Uzbk5XdFBIdmY3NTdwM2grRDJiK3FVWmtvdm1uQ3RNRDRzMXgyMWs4clhjaVFwbFBMT3hHOGdINTIyRVN1cVZyMkYyZWlPcGM1Ui9IVjIyMmVzM1dsa1pMZmJJTlZFMnYwYXQyeExad1FYWWZKTURlRDBwV1FwQ045U2F5Y0ZVSWQ3bWNSaE8wekNnWkVGOXNDZUgvSEl1RkI2SmtDcUUwT3NpL2lock51b3RBZmY4cmhWVWs5L2xnTzRzSlpjbnljaHpiQUZLT1VZbUozZ2lXalpuRmhJUEdud0d4ckhaeHRyeHR2NlNHeEtJY01hMzE4RXMxVnFqTDZ1cnZ6RkU2a2N0L3phcU5zUnc2YVNzdS8rSWtTOG1jbk9xTzFobHNDYlVINStFcXNDRXJZYlorbTFxdHF2SlVTRWUxWVYrUCtuaEdlNVdxamlXWWwvUmNXQ3d4V3pwOGkya0tQam5Sb2hueFROaFJiQ1YzOGVSczJSeGJUb2JRekt2N2xubGlCUFZGNStSODNKSFh1S1JlbElqQThCeE80TzRGVjhta2QxMjRiSUZyZnBBaXRDWXk3QkQ0Z0pNNjdYY2luN1VIcnlwMm5tb0VaRDlVV1B5elFyM0RpMC9zaUdaenhXMWZObkR1RjhIb3JvMnVvTWhxQVVHMHJKT09YQTVyL2F1eWpvMzcwbWhkWnVnT1RTQ3dZSm1tRVVJMFhpNy8zem9WVGpFUW12MGx3djBXNHFOdnYwa0ozSmxHMmVFZy9wZTk1MllvVjNZb2JaMnRwUGxibGZXekVKYXd0VTNCY1NrUlZlOGJrcGthWVRMUmRSa3RreFM2UVlSUmNNa3ZQWDhHd0R1Ujhjb0gvV2cwdmhpNkh0SmpjTVBzbUVaVFdZU3BzTWxEY1VSOWJLU3ZubDN4Mnh6NUpBZ3ZCVXBNb05vaE51SzNtTys5d0dYY0VkT3hzVmFrRFBhb0RrTC9JTkhjN3JLQmVTdFlHc0w4RnpkMUkybXdQNFdaTk9XQktFNUJiUXVpS3BpYUdIZXhJbjludFI5R3BZaDBuTWMrNHlPaTllSElWQUJ3eTZGVE5TS0RvTVdSZTFXTTNrUWhrTStpb0JXWC9Bd2pUR0srUXBPM0wxUkdUakVkOUdLU1JPWUpUb0UwWGliczE0amJEMTM2dFlJbGdEcWtGUlI1TlZqSHovMHpUelZvRFU4ODlyaTlIeUNRWmpTV3lRRm9CSXdxc212eXdGR2c3cW1penpNTG1TTmVrb3FSNlM0dTFia3F2M1pLZVpRK1VaNHFleGhPaW9xTXlGS1dBRkNGSjA0c0dzbHJqbzNXN1FyUGdreEZldDNhenp0U1JOTkxHejQ4c0hpOE5ZR3kxei9XbEtoMFpMdnhvNmNOVHMybStFV2tVc1FWb0JCNFpoU0NxcTNJZEhHYjJPTjZtSzZwWmsrd1F6cWRNRG9FVjZjdlJiVThoMkdTVUp2N3VpSW1uU3hFdlltVmM3NFd1S3dPY0dYYXNkZ2RBMzFoTkxPdkNKOG5Wam9vTTVXODUyYkV0dHVEckwrWERaNVJnby9QZmN0bDdXS1Z3SktNZUt3SW5pYTFjRDhaRU01YXdRWjlBQjUvNzBrS3MyRW5yYTJSUlJmOVpiT2RNMmdjTjVmWlJSNzJnMjI0SzlCYTBUcEtyL1doWDd3QVRQM3Uvd3FDamJzZkhHWkZ6NWg5cWxhdy9OWjA0bnJrZDJhUGJlbFhVeEdrbGttamQ1QXNGRVJRYkI3a0dhemJKSEZ6VW1rcXZMK3VNRy9Yb2RpaElwaWFnUngxWmIvMnZaQlBudzh0bVIwU2RTekdTTDF0eWdVYWF5ZGV5NEZrYWVDYjl1c2s1QklVSkQrOEhIdU5taXlpK3ExRnFqUnJKcGRmRU9vWmhQTEVMcHRKa3VuWnYxL3JJREdzNzhMVnZCR3d4TDJVL0NsaE90dE5HOGxJOVk2cDdDMzdPdW03TE9GU1oyVitpN0t5L2JHUWYvMjNvclJqQ1hqK1ZtRk80WG4vV2JGSjZDOUZRNVRUcXQvTWZ0SGpOQm9uTEdOVXliOEdhSkJEaXJTaHFRN0NtODBEekxiaVo3em9VYXQ1K2lzeGhPRW8weFkxdlFpTW1PMkJHTHFkM1BLd3Z6RkF5eUYvSzEwWFdKMUZnS3BjVEFFbHYzSCs0TkUvMWZFQ0lmTkUyNUgrQkw4MHRtODYzbDRKZ0Z4azA5TlN4RXlzYVpKT1lkY2lKMUFPK2xQcnRWZzFvckhTdjg3U3N0VHBzN0ljcHpsbXhqQmJvMjdUUGtXZ0UxVWM2b3VPMHVYcFFwSFgybEptQWlvQ2k3b1JEaGZXbXpPWHlxTFFuNXpRRHplbW0zcXl0ZWg2WXB2MGtWSTRBaGROV0t6QlhNQU9kb3pwUnhtTmJMV3lnb0xVSStvMmtNTUJ3djJ6eE5taHdTZTZjaFRxalhjOWpVKzVnZSsvT2o3bDlTTUYvVS81bm9tdmdqNkovWWVGRXhud3hzQTZ6L1ZmR1dIeTlIYWRTZkpLalhGVkorUFFXVUxJTS9pcDM3bElpckJYckdnVDhMSTk2M2xaUE9DT2YxNytQdWF2VzZPY0srWlBPY29zRktRTmMvZ2V1NkFiL2ZaZzRNMzcrZFVuUFBNT09ETEVJcWFSZHNITnVWVnQvTFk3TTVYMlAzU2J5aGpJb3RlZjFnUjJNREZaZGJmVEdEM2JoSmdxekFJWDlPWnZiVmU2MkltWk40SXhsaHAyYTJNNCsveFl1aVpBdktDbXlYeHBpOWY5RDcyYm93bCs4a2prdTgxSG5iZWhMa3dmVFJlbVY4MENQM0FnWktVMklOdzJmT0lnNG14VDIra0cyNzlLK1IrSlZMSG5wc2U5MnVtSVdzWVpvM0d6TENkMXo4SisvY296R3d0U0hwYnluM1ZXL3V0OWlNMndLL1dyWkVBMDgzdGpzVmV5Rm1SUjl1T0tFTzRldkhuREZqaEt6M3Zidm5vUVg4dEMvNmR3cnBqWUQ3OXpRNGJyREJ2U0JSYlVFK3FsWDEzcjVZTVVzcCs4U0JpaGp6SWNPNXpucXgyMTAycVJnQ2pPcDlvb1piWGI1eGUvQmYvMnpiMTNVS21tSEZodHBtSEsrejBEVy9tbnFUMmFsMUhxM0tmVzN4RnVrT0tJcG5HVC9lSGdld2EzWEhScnUwQi9mNG9XMDFzaHJKZXhjcDdzbG1LQTdvWWI0ZTM3dldUVGUvays1bDRSS3p1YVdWZnN0M0JpM3EvOGZTTUxhdWZMWnVFSEZ2R00zWUhBVElWZzVzZ1hpeVhrUVBGbllaejRFekJyT2xzWjRCUTk4MVRKNHc2cHpaTSswcUt5aFpPYW5jUUhZL2grcEcvODlXaWtzZ0ZpR0ZQY0hZTnRaT2dpcUNDSVJSVEdEUXYwWVJaeVp0cmkxYkxyOGRaYXBpVFV3SkNXZFEvb2FsY09NaVdoRC9wbWdkVnMySjhrWFhvT3IrUUR5VnhDVkwwcHlUZnZTdmdvSUxnR1lEQk9yanNSZGs0MWN4TXpuSVNtTCtHR1VvNGQ5TUdURDBPVmZpaUo4c0hNYzh3MDJmUGpWcitFdURoQndHMzZJWHJxYUZWMElKaDV3YloxOXQ1OFlmUkJKVkRrRDZxT1ArU25HSWxSSUFtTzVYMHo2SnRkM084Y28xYysrdHVObUtFS0dvY2lNMlZxdWZDRlNMcE9UMzEvTkg4NjJlNEw5ZG5iYUs4ZG9RbHVEajZaYUhjbkZHYUsvckozM25paVFJdFhXRGE3TmhhcHlVdFYveHhCNXZnVTVWUk10TkFUN2hBM0RtVDlIME9rMU1LdDQrck9qdVk0R0dxaXFWeDIxVXQwT0RhZkNWTHp6T0trckRscW9OdjZsbWJNd3pyVGFvbEI5TzNLOG5YdU5PcHZZaHBnb1QvQy83bHhUNDBVSDk0ZG0vT1IrZzdhSFpwM0g2MFg1MTlTbFpFdFduWnEzVXlsamI0c0N6TE5xbmlXcWxLTEhIemRsLzNFRDAvS3krV0VkUmFmNHFVSlJaMU1NVzN4RUR2Um05RG0rL0N3YTc4bkNsdUtCR1k4cUhXY2VOUTdEME9rZ00xZm1uOVVjV05SSEpaT1dUVUwvR01qbVRjY2d5a0p5THVVdlQ2QTlwNFhaUDRsajRtdFU1a2JpSjlVZzFMS3BpZDVHVjhzeGlkK0wvT2tYRlFIRlVtaXdBY1VteS9SQXo4THVrbWFFdDVTTko5Ym9BUlM5aGZyL1VDRlZRREpMSUhhREFGb1Eva3pHVWpJUUgzSTdpV1ZlSHB6Q3dGdFIvVWlJY2hsSTJSek4rQm1za0hVcm1qS2UxQ0c1bngrYXNndG51T0tTVjVoOE4xMTZZZlc2NENLdk5CanptSkMrM0NydHRGbEZ4NWg1RjdRd21zQjZyRTNGeTBra3hVdmtqR0owZDlPd082SGNHU1VPQklwQXV3UjRHd3l1U2dOK0RqNDZBN0xOa0tQcjNaTzluZFYxak5LU3JRSndBalF1NmlIL0JyVTJoeGcvdnV0SlRkQXpGcGVXUmdGVjEvQ1BsdHFja2xhMkx2SWtFYk9IbGNKWlFLRXI0VWxYOUNOMzNlS0JQTEV5V3haN2Y0THNnT0ZKa0xVT1NubkFpRXVoN2ZBN0lqaVdac1I1aWxVYTNzcWovangzejB6WVM4SEh2d1pmUXVXVmV6anNmRitXUVlXVnM0YWpYeDR0L21BQ2hDRXRwM0RNeVlPeHlJRmlWaW83NWFENzNyVWR2L1pQajJOQm1CZDhVR29NVDhXV0xOQmVSQzNMOXVZTnFrdDZpa0hlSnlRZkJxMlkrVTNCZU40Y2E5cDQ2bVllbVFZKzlQdTJlQVlPcUZGdFc3a256REFwRHlWVHFtTnJwSGErUFUvTWJybDVPU05RTWJNV0hxZ3pTQ1dzMFhNSU5IWUdEVk1kSEVDSVZGa2txK3YrTDBmRlVVNks5eGNnNmRzN0EvOXp2TlBKUEMxNDhKRFNXWWMwLzF0Z0MrRnhtZVlKRERqWmlFN0t6R0kxWmI5V3lWYkEyNCtFRVdVV1FORnNIbTN4RUdWQTd6ajRmNG54ZkQ2SmNhN1dsTE9JOEJtOTc4SzcvdHRabXRhY01wT1FjL2RGUzNrRllEbGhhVEpZTGZBWE5CeHJuZVRUdDVJckRYVGhHWkQvVHErWWJMNm14NzZhRWNaNTU0ZkhFdURna0YzaDBxSUx5M0pLQWtTdExTTEtSTno5Ylg4Uzh3U1ZMRk1ES0NoOXJNeTh0RlI1ZmxhQnpQR1pLL1FNdEZUTFV4WSsrZXpFQUNjUm02Y2pEUmd5TTc3SGZabXpEK2FEQjlSRDJFSStsSDNOZnJRc3l1QzJScFFieWhOdzJ6eWV5MWZzRUxKcTJiZWJzcmo5V2c5bjBiOGtUU2YwNWtGdWtBY0xucVRxOTdEc0QzeHlnRzBKNDdvOXp5TmR0ckxkZSsybmtSRWE5QXRqQWxNTjBUSnBhbDVrNkhUZHhObzZJY2lwRTNXSzl0QjZoVWR0Rk5DZkhPOFVlQS91NCt0N05LNmxacUxrRE5hczR4Mnh5TnRBQklTdkozZHpVbUlDa2xWbVZPWnZCUXhXWHFSSnV6K2Y1ZGU2cjdLY2llOVQ4VEg4U1ZoSE9KcWNNN2k4RDJuMW9FRWFPOHBkK2h0REw2YW5PWVBhcFgrRGpaMzA2M00ySDkwaDF6aStyQXY5RXh6OU0xWEcvMWxKaVZPb0dFUDdKemx1NXMrb3Fsa0cwRklqOE90Zk5DdFZ4M2Z3T05vNFZXaUh6blRUTFZjaGdPRzVLai9TdWN4YU9iRnkxaUVyV3Y5dUJJZnp3L1AwY1VITlJhRE9RUTkrd01DTjltdjJVZ2ZZQ1NLejFXQU1Kb3Y2bE1CQ2xBc1RDQWY5UlV3WUdIRS80dk85eTNtRnlvTkR1c29yL3VQVVVLM2RKSEVEUDZWR2s5bStUVTRzdkV5dnU2c0hndjQ0Y3AvcWlTeFVCZFl4NGYzTnhQT2ZuUnMra1Uxd2ZFYTJXdWUyQVZCNU0ydy9IcTVmTCtZRXNUU0Evc2hYWGlSMUY3bUlpZXR3YXBRRUlqY09XQXV4RlZJcUNqN0hTa1FrdS8wbkNMZlEwVWJMVmJ1eUZPcjV5bzNOUWVOcHE0d1hYMEhpS3F5b3JLRHBsR3FQK0VyTHpseXRjTy9SQjVnY0RqM2ZXQzNRdUdPRERNd1RVVmRBQkswZklkV0RrcDlrUk9nUTFJVnJnbHlYQTRQVzRxUXZLdHNqOUtOSHdvTWM3emNhUmhxRHVIRUNoSlZZQVY3Uno2dWQyRndsemkxMXR5b2ZBd3IvVWpCOVI3NjcxaGdtNGZsSGo2b29EaXpsZnorQXVWSjAwempVbWI0bFRwY0RXOFpRUkRVNEdDTzJCWEdSL3d6NU8rdFRkNDJTdDc5VzZESVRWaWVUMXNpbFNWNk5oRU9RUXdBU1pWSldTSW0zQjR6U3A4WU4xYTZBcXVCZ0pzcXNha0hVOFZFdVZGWUtKaVNOUjFkYlF2TjN6ZmJUVENibDNudkYveUNua3hrR0VpencrMWdESXdGZ3lrcnV5bzJhdzBtb3R1TjhTeEFaNVppUWVRMGp1dXlqbHBmdW5CNEpOSVpqNWtaMkV5L1lLa0F5YUF1T20zNi9vUXkrV2YzempNZVIzZXI3NS9GYytTTnk4aE9ybmpES2M3RGxJMTMreitLYWRYNXZzeDh4NEdXTXNVY0Z4SjFwV1ltS0tPRlRTdkI4N1hLSmkzZUhsS20xZUVkSXVDVGJ0K0JlKy9qZTBTcFQ1UElEWDI5SU9FaW04MmgwUlR0Z3A0SENTcExMZFZTU2ZZZUUrN1Vub1UwSG9iOENkb3lRTjF0cTJsaEJMa2xKcGVTN1ROTkM4TW5OcmlNand3RmJFbUdBK2R1V3diOTIrZTJ5WEczb1l1dzE3b2tyZlY3QldYSHZwYnZpY2NZNWFNbTFYUzRhaDJBVGcveWt3aW9iUkJLL2dsQ0g1UEpOMjl3cVJYSWdOamhLWHlCMklRSEEzWFV2bWVtSFp2bWM3bkxRQkk3d0lJeDFkUVMzRXQ5aHhiaDBBcFBlRWZ5WXdqWE4xdmtJdTAvNWJZa3dhRjI2WVBoblhiZ3I3UXlMV2lzK1RMcng1K2UwLzJybHVkenJ0cDdRQmdENHNUbUhtQWVBTFE4ejVwcWI1a1ZrV2NpMWNOK012bFh0Z1BWRHhMMTdseTlNWGJLOThiL2l4WnRWVjZqR3Rud3NVcmRESFJXMVpnOE1GTXlPUW9TZmhaSW9ScytTeWw3QVRpQ0RWbVBUSGhacS9zWE5mNTZpYlNiYVBmaGs3dUsxZVpxaGdDSExTeDJoVVlNWWJJeGV1SXJmRXdYK3lKUmx1Y0huNHdaVlZySVNwUlFXaWI3V01TbjVXazZ6WnEyN2JLSXlnSDNURURRQTlreW9QUVB1Qzd5MlV6eVNtK1B5NFRFL1Q0cHROMGlpdENMMUt6UklSRGtFb0JOQ1R2S29tY2lOcDdic01ZeU1jZWo2dVBVMHJWVUhJYjVFUFZVWk9JUVhMdHJZSkV5Z3FRcmdWbFB4YVcydEZydldnNVl6cU45blFPM2YyOTJheFBST0haRjc4NHNjaVNhekdzQlcwZFVyUTFSdnJ0QnpTL2hScmFvbk91ditoMldBQW1ZTVF1UmF2T0dqRXFPdWhzcm5weVR1bVhqSklxVHNDdVJTQnRYVDFkR3pSMGdkQWt4M3NyMnJ4cFNvNmxZVmxuVXJYOW9MZm9tUEdzYWx3dzVyZ2V0VEtxeUZEZ0I3V3daSmlOdXBWK3N5T2FHUEFWV29xZzdJL2VRamt4SzU4NjNVM1duU2xUMUVJblRxWTBmZEhYY0xyalVIb0RodUl5TXJ3d2QzWEQzUDNZdm9HNXhPN2grMlpVRC9Bc1dycm9aU2xQYlp3TFpUUkU4OHdDdEdmWStkWUs5ZjVXL1AzQ3lOYVFUZVdsWDRNZmQ2bU9oTm5HYVVCRnM0WWRwdDR3RTFlUHdEUW8wZk9NRWpWUk02NmZVZjJiMGp2amthelJEYVFYai9WRDNuZk5iMGVpK0ZZQlVKYU1rSnlmMGtUMFJQNXd4ZXd2emVCanArWG5oZElJTEZjTzNxeGhoOHpGZDI0dTRZRk9OTXM0dU9Tak4rUXczYUswb0p2S3plT3V5bmZIdEtTK3NPTVJMZEQ1aW91NTRNWDd6U1RNQXVaeTBnTEd3d3hXbHVDWVB2RlBaaEx5NDlRMWw0M1Z5Q0ZubWs5enZqUlZldVBnT3B0M1NNcjJQcUxqRkpLRjZGZWRXRkRGTzZYN1VmRDlxL1REcFB4V1lYVExaRytCN0gxS0lzanMzcXlKWEdQanlBYjJrQ0ljbG1sQVN5a25zSlJLUit1SWVvdzFGWnNuRGxOVnNGa0xlOEFYcXM0Ujdjb0dScm9IaWhFcU1ONVMwelc0dVBDNGxoVm1Ed2JPUXRSdnFySzh1RklFQURvT2RGL3pkMGFCZDduSDB6WkZkeGdDU3plRTF4d3ZBa2hPSVFqTEsxcFNBc2VKNytZNWtod1EyZGZYMTV4bjFhaTU2dlhZYVNRUVNoakxuT1FaYk5NdUxiY3ZYeUlxZ0NzZEpaWHBIN1d6bnZYTHovVzFyR0xUT3NVdkZWUlNORmxvdCs4Tk1PNmZRdkVLWGZPdnNzUzdrSHloMlZCaDR0Ry9tekNQMXZhTG42Y3RncGxXd3hLdFA3Mmo3VDBOQTkrMGQ5bVRXckJ0eUQrT3huTklkRGZQRnVULzhUZVEzQUJIMEtrVHFocDFFKy93akY5VThVOWdZSjZodlVIMEx5SXoxUXBrUXBMLzZia1QxL2l3Mi96QWlZY0hMSXhzUVlzd2cyN0g2TzdHY1poY0NJTFBaUE1zVjNaVmduVXJad3JkNU1obDFrbFNpTm9BYWhQemp0cGVsSGJyUGZmYTZsUHdyZS91bXpzU1VmNmxJaVJibkdGOWNGNVNHaTdYRVdpUWdOc280VE5sUGtRMHJZVC94SWRSUnUxMy9IbVM0TG04N2E0WVdRVi9iZS8xdktZK3U4cUFXQlVDTnhvZzZ4OUdwQThScnZVN1BFTWNOSE8xZUduclBaQmJoajZCY200RTF1akhlMERKczhwL2NoSGlmUFJsdHljYzR3eHQ2TFMvWmJQODlJTWZxbUFXTi9XSE9TTTFHbWZKa0tkZ1A5cVBGSVlzSS9qTS8xSFIxSk8vck1XeE5VejNhS29EdjF1QmZ5ZFZ1aDZteE1KbkRQWlVVSEhrZ2pXbVZCbGNKeWdBZGlZckd0SlRnUEdHSjR5cmQ1eGsxbXBEQ0pyVGpkS1laeG5jSzBSeFF0eC9zcXhNcUltU3RMc2VOTmhLUWR3SkF5aGk3Y1doUFd4ajlzTGNmRHBoYTRnTUZoQ2kzeVJldWc3bWNwNGNJNmxXTzBMT1FGeWlMU2h4TGlOTlNabWYzZVN3YUtRejF1VlN0TG1yRXVKREI1OHBWL2Y3R2lHeFNQejR4ek9GM3BxV3NreUQ2TG9ZMHkxempBWWFUZWJKcERPN0pXNXhUUmJjVDRBd1ZGY1dCVWZaVWo1NjM5L25aV2lyajJWS0x1WEVTc01iWmZGcmROd1VxcWE4NHJqVW0xUUswTUJON1NxaEN6OUYrY2R6elhrZ3JjODZaRFcxN2pTclo0WDJ2OFI5U0lzOUlJVFoxR1ZOSGh0ZU5VRmsrK3JubUdLMlozTHJCWnl1MUhSZnE4YkJqS0NVUW1td1k0ZEtNTWNnRGtrN3VqN1RvUTdtdU5vSzlhbUNDVExINWdiSy93Mk5lZ09Cd1hZNFdjWXVPVzVlMkZ6bitwU0hRMEk0YlhaOWtzN2hLK1RIV29FZUt6dXBqaTR6a2RqR25ydXY4bkM1RUNwS1hweXZ5dkV4eGdnRlprMSsrZ01XNTBoWURZMGRUWXJrL1pVTFArU085dzZsUXZFSk9VYWtLUkxnOWtHcUU4aGxkbi8yekNjdE9Tb2kxRFlLa0txYVRNRFJIb3JxWWFMbmc2c1hsejRVYkx2cDRwRzU2R09kVUZVYzVaRVVPYkxuekF5K2ErZWZUbERkSVBnZ0tTVk9xTUxFK0V6THNwc3kwTG9tU0xFdlQxVS8xcXQ3Y2liaFdCdDVwNzFVcW8vUngvZzFkTXNyYlVRcTdTd0FmeTY5TFhZcENYd0xKRUlzYWVlRmx4UGpJZkZOM1BNa3lIaXpCMkROcnRQeEJ5UFpGRzdxWkZNWjVqUTdST1d2cElaSGw3T0JKZzRZVHJORDBqcC93NE96bGdaTFd5RHpMN0hFZXlqNTkzdVJvSVFvbTV4OGdpdGhabFMzdVJXV3FZcnV0bFVIOHJJblI5OHlPNEJ5R1FMbzJTUTBncmJvR3pqUTF1bndqU1FpajYyUE5zNXVqdTgrL2dIU0hodGFTWkRkNm9kOWVoYks0dmE3ZFJWL3JuWC9YYUJRS3lQbzVEbG9wN0MvZTZ1aDF6U0xmbGRTMlZJTGdXK1NRdWdlTEJXdURvZ2hKMnJiZHRPdHpTei96N1RkRFY1Sm9TVE42ZFU1Vld4UEFkT0c3V1hFQTV4U09ZQVFXZGZJaGtTZ1dDOHZZWlRTY0RYbFExUnNuRW1GdEdlT0tiSjl1YlRYOFlnYVQ2cGUvZWhySWZGZlBCSzlSOEZqa25OVFovYTRaL0RBY1BnNklIMGNiL2F5M0RVV3JzNzMzTGp5Q2szVEV3T1ZRYTd0QWI1VzZYL0c0N0JLenNxV2lITE4yVTBXZG5adWFqMGluUFNlaE0rVCtVOE9sdXVzNFVScFJQNDM3alFlTHRBV2dVZ2ZCdkowUWM1WXhPTTZ0QnNyWWUxa3NMMEttS2UwSzFHbVltNVM5Q04zOW0vUXhkYUZxZ2dzRHZwYkh4ZlByQVVxZjZOMTRUbXZWOFhDSSt6endpNURZbTVGVXdNYUF6YlUwTFZpVTQ1VGFLa2xxTG5lN3hJQjFiUnd2VXVhR2xXTjIrdTF3UlZTeTBxcGJjWmM1ZWZuWlVzMHFGUEFPa1dHNG9ESUl0d25MMHlpZDZkSm02aU1XZE0wVnNDOTlvM0wzQXRHcUltdlpqLzBGaFAzdEFXS21BNUJmZ0hPVENLWExYdGkzbkxXQm1HVERCU3NjdUp1aHhFTDJUMlM3bVhEcExFc1J4ZXJ6NGtqUnhUYXBDbFd4SG96SU5YaUcxY3d1WmsrR2ZydURza2Eyd29rdGtONFdEcmpGb3BWaGQ5YmwwWWlKWHNuL1B6YjhUNzd5WjJXdGpma25wTHdpd2tndnU5TVJHb0k1UGNwNmZHNG9zcVJVakJOTzdMS1hiK2NFcDJ6eFJKNloxQUVMZlB3NUpmcmd3dkdBNFdOelF5TFE2eEJiWmMyT2psdmxPeWMwRVNZUEp1ZVZNK2dHbU8xVnJmSzRvS1ZOQzNndmVXbWtIb1dNMlZCTENNWTFjQUVJVGxNQzQvY25PbFJtT0EzTENFbmZXd2tWNUVQNGR2U1h1NTNyZzAxMm54TU52R0tVVkNtdkZsOWhkVUc1SDllUmVIbmJHUk1jUFp0MUxvd05FR1M5Wjl5Z0l0ZFJEQWtEZ1JsSUJLZ3Q5UkVMN2ttRDJVT05GOTN2S0cvWFFNOFdzSjdua295UGZRN0hMQjk5Q1RsZW1DZjhGZzRzUS9wNXo3ZHBhcFZydWNxVm81RWRCVkZHMGk3WUVxNS8wbG0rQVVGb09SOTl5eFlpMitHQVBCS014QWQySlIwQTFJemNnREhqRnVFZ0RpZHJGWjNQQ3k0YTQxMll1U3dtTnNHWUM1OU93SnZYVkJZZUlTTEdLUlQ4ZVloenA0c2lCbGNwZkRmeHhQUnVkcHB2dUlCVTVOZ1Q0YkY5NU9tWU1ZR3FGam9obHRhUTNlY1ZWTXhmZ0lTVjc1elBWUmRKWGR5cExKSExtZm9mNm9SeUE3OE44R20ySWY0MXNUWE85WjRsOWtNeURXUUxFdllGNHhKa05aSG5KYjRtWTczdzdXUjVub3hYczRmdmNhSHpMOVhGbUk1RDJSWW1EQVNJYUdFTTVJNklCWndFVi9La3IweUgzZDNhUWR3Q1RET1kzQkVCbFV0TjZDZ2V4d1hEL1pmWW5jSHpQZEZ0b1RtaXY4NFdLUW9QL0FpYmR3NDBpamVQQnAzV3hYUTdRSFNOc1M2RkZ4NmY2ZzBsUk1FWnVJaEhJZDlpeW1BTlVaMG5zTXl4N1hMRXQxczVXU0RtSWp3QkdrK0VZR1Y3ajhoRVhUUXZLV3hqbTl2U1FaLzBqejd1cStKUzIzRmJhbFZCcjBMbjEveEQ5OTMvdWpaS3pvUlNJRXdtcHZtTm5zMFdBVmUxZUw0VnRnQXpwQnJSU2RscnBPc0NZWHpOaFdTMXFvUU9jVjNOSlkySGdUdmtvaXBlWXc3WWltTzR2bzBNOXo2NXRENS9WVExIUWN1NUw0ZVRiTGdCRk51NFVvSTFDYjlMaGNHSnNySDRDYWNpTFVwR2pZUzdxa2RpclBCUklvaWZIOFNkS1J4bzBvMTdVNmo2VFJpMXY4QkR2by9rN25QcjVnWkR2L3hKaDAwWlpGc2tSZ3dXeS9valBwQmppaEUxZkVMOEUzTFRaTGh1SG1ZSVN5d2ZveVF0Z3d3MTh5NHlKTmhRZzc4eUZFbjNWMmgyNnV6SXNnb2VESGgyNldBVTFQVkxsRFNtaFdaRjUvcEZlZkNvTWV3eU5maERsYktMU2oybmhNNHN4VU9IUVdjWkJBbVBsSndmNWF2ZlhQNkpUb0FxYTEyMFY1VFpEcyttYnFxcjR2RllwUDFNazBHR2F3a2Zub3lTU0szQ29QaUZEaGFaRU8zS1djYnR6N1VSOUdiaXdnKzFVZHJUWjZMbTlwaDFIa3dmWnNZTk9QMlFsTkNhalMxdjlvYU5IYkE4TnZiZzZUVkViN2FDT2pGazAydW5VTWhnelhXSnBlcVMvdVF2YzNWUGY2Q0V5b25FYW1Ub3V6dTFiOHNDMFZHbVh3WWMyL28zMllQVWM4bVR4K2F0MjZJR0lDL1JjNXlja2xPRmtVcU95WnVUQmlIY1JueG0zVDBvandHa1QwQzlIL0xUWXh0aDZURUxoaEJSQndVUWd0djQzWnMrUlF1cng2Z3ZmdC91N1FUV3krN1lIbXdIb3YzWGRSS1QwTDJmeEhqaE1HakpzMXNQSlNrcVNDM1Jod2MyZVA2K3JRN2U2RG01QTdPZU1QSUxOb2tVTGpNTW4ralJybmJSWVI3R1lQMmMwMkZZK0ZtZStaTkd1ZFQ3NHJVN0JkRHlOaTJLYUdQQ1N0Y29zZ3NhNWdZaWk4NEZydFlqRWpyZmxqSDVNTEluM0l5b2U0ME1oOXlFZjZXZW8rVUtaNnlMQ2o2Y2l6eFhMNS85RG9GYm96TjNvekdqemc2bk0zYmZRVDFlK0Zqczdwa1psYkVWdTU1UFVHZGs3ZjNxaDZkZUZDbUNyK1pxZXZ2NzVIQllGalhWbTd5S0o0eDZRQ0VuU3NKRGVKdHBCODN4Rlh4TEVycmtUTCs0TUdwa3Y1b0E4UWtXU3RmTDl2ZFo1TmNTZkhjdTN0S3pFbE4ydWp0SlVSWmp4TkFxSmRDUmRpYVFxbDZyUHNzbzdSSU1QUTJsUjl3b25XMko3Y3E4NmdmVjc5OExDQ0ZwQ0FJNkUyRm9ueFk1bGdDbmJUNDExMHRKRFpxWGptSnJMRVIrU21pTU5CY0xFNUlvV3M5bzdGZjBTbFFZUlgxbkhjZTZPUzZlTVo1V25MSjlFTzI3QmtIZGk4LzhsNGZmM1pxSzZCdTY5amhTZitlVitka2hGcU9VUUo5aVJiVmJDL0xzdlZJQm1VWWFWN2UxRk9OMTc4aEZ5ZEFiSC9HUUpibWcvZzdZZytsbUhWNnRsbmlRdXM4WEowYmNXeGhjVmNkUmUxVDhQa3p0MzNjWnFYTS9EN2pUdGZGb0JCVyt3c2QxNHhuWitxRmFRdWFQV0FmUmg2RjRyNllqUEFPbjBSODB3Y0x3YmVXVXdRcGYvUGl0MHh0YVFHUHQrcmthd1hPM0Z2OERLZTVxVFlHOGtyYkx5RGdyanI1N0R6Vmo1UmloWUZkT3NwNFdVL210Ui8xY0hOdm5kRHl3R2poNkRQRFJOSGhjVjI0YmZ5Ukd4bDF1QXV0cDBvL0hhMStuOW9XSHpPZkxsL3ZxUklrQ0tzQzh3ZVFueHkrSkplREFqRTNsQzBJSXVwYlRJd1A5SERxNFFzc3l6bWFIMmtuMlJyUXcvYzUvL0hHY2hzb1dBYkc5bHd3WEtNQ3YyNjcxVmZlRTJmb2h3ak5NN2pMMGJ5c1pJUXltMlBvTFFRdm13ank5bmQ1N2Q4dVRXNy80ZUpzRlZnZEdyRUNXdzVPWTdRRlVZSHlSTGU3TDJwRUkyTDFMM1FUeXVPK1JYSHlORHdGQkRrL0grNFdKOFpuR2dwWVlNOTdmT25BYlZrS3ZwOUFvNnk2NWdJRWNERGdZZkhtNEhnYng2VmMwcTMvbVNUWjY0Q25TUWN0bTlUMUQ3bGVqc0lvUVJ6dXVpTExmNERkdWJKOGNYMithOVFWcWl3NEs1SHRzY291SU83NFlCRU5OMGVsbXE0RzZMbDR0NVdMWWUwQmVHZGxJL3dyaitPb0MvNlJJbkMrYkxFblg3YWlFU3lDbVRGWXJBdDhlSTlvY1RoWmR3SXY5VVlrWWU0ZXhraGpQZkQwODU0Qkk0MmtCWDFQcnZQNTNMMFMxOEl5VWcveWhxT1F1Mm5FNjRuYjlRS0lqV1BNcWx4a0RwVDU5aytLTmVSQ0p1MVVWVzc3VEJKSWtrTFIzMUE1YTlEekVNU3NHTjNQZUVQWXozYjNLVGlVeVB3SWo4d2RVeXppTDE1cHpIWjE2K20vOEdZenh3NG1SQ3ZmTWlrZkcxS3U0bmNEaDVEZDlNdmxjREROZzh3cHNKYTBpOUJKVHdzZHBxTXlwcjVQQUdMOEtGY0ZlWUFuUUk1NVFhRzU2aVpSTllKRk0yZjUzV1lTQ3VKZ3IwKy94R3lnVjdCeHFMTDZibDZoN1lOWDZadWFqSHdNUjZQOG8vZkhwMHVEenVRbzdYQU9RQ3VuS0pyNkZjVUl0VDF1MWErSGNxUHkvSVRRS3JhZzIzbXpaSTR4UUxadnhPWXlHdm9vOVloOTl1bkdEQ2dFN1JqSGwyU3VyWkRnSjRPYzNmeEZpQkRVcENyV0h4ckxJcEVuczcwcUZxODB4M2N2cDRnL2I4VUNvQ20xZDBnU2E3eFhUVmdwejliTnpGZWVJdzhlZ3hjZFZxMU1kYXMxSHFHNUNOTVVib3dldVpLWVdjdU5LMFM0UXlwWkpnWXFudFo3VXIrUlRHSlZITTVUYzVUU3A3N2J6VisxaEdhd3hPRHM4UVNScWdyUmovSWFCVEJ1SVJFLzJ5d2tuQ3N2ajhyUE9yN0FKTUxQSW9aakN0RFJOWURhZmQ1SEsrNWMxYW0rN1hYK0c5eVBOWDZFVmhzMXgyczBVNjF0cVlSS0lVNU9ONjZzcG8vb3RXSEttT2hUN1lYT2hRWG45TmNhTVk0ZlRyemVSM0NjNDRTNXRpMEloVVJLdElKV0hDb2svb21KVUhVQW1oeTkzUWgrYVc4ZFNEakI3cGVhYTlXcFBjYU1PU21QeWp5bzE2UndSQS8yNmtodHhVZHI5R0xQMUZscVY0d0hPbUxHTFBhTkJOQU90QzBOeDFPVDRYVHIxUzBtczhPVVVrMUZETmZMbkRyWEdoV254RHNybFM3aXYwYVk2R05CU1NTaFNxL3dHNHMreHdNTnFSOWQyK2VLdlo5NGttVExuZ29xelNvRHRZTDB6d0JnS05HQWxOMmE1cFI0N1BNOWVpUHptVTNyU2ZWVGRIUHN5WEV2aG9HMUEvckNQUm1Pck5EMDJaTmF3TDZmSzRHNkV4RER0VkJNei9RMllwckgzZDFsN05ST0M0WnVFZy9qUWtiMU9pOElMUS92T3BIMk5MZVhsYnBFNWtMQmNRd1JLQlRtMVFxNVNKdkFHT0FQdUY5elkxTkU1Z1N6dFZvNzQ1ZmExK1EvSzZWY3RMaUdXc2ZZbE1PN1ROS3ZNMzBJZGpmbVJnellMbXpRekhyVVRGUXh5V2x3UnUzQmVOVjF4bHFrOUZwcWpqU3haUllhZkxwOHAwUi9GL3JCa2E3Yi9DeGZPaU5aNXgrZytCa2tqUXhiRk14UmljSmdZUXRaV1RIaTZwOVZjY2NvVHlyd0EydWVWQnZMVVEvbTU4OWk1K2ZMbElwdUJzQTFxS0YvUmdGWk1iNkloY1NiNExIdHF0a1VuK1VGYmhEU3FWdnB3Si9MbUlQUjB3LzMxR3RrbXN4K2duL0VJekZyM1FBK2lmaDJXaiswbXkxZ3NGWXdJZmZFOXZlc2QxTjl6YzNCMDhKTXpZWXBXaHQvaEhVcEZYdWJnOW96bVVOU2h4VU5VNkVDbURCK0FjQVlaRjhrT1JlcUFaRjBkV1BsSmgreGNLb2VtdDNkaVNmaTRSKzExY2Q1RXZlQy9wcVF6ZlhDZ0NiMkorbWdQTEVlYmN5QXZjOC85bExMTnRXRWdtOHVwU3drZXBkTlRXaUJFWmNmb0xhY3duRWZIbEtRNFRWZFhLNTJ1N1FkZmx6bmpuaTRaZlB6MDhMZThNL3JrWW9sZjJKclpRM0dzRmZUcHNSWWhxM1JIaXY4SHJ1YTMrK2k3SEJQakx4RW1ISVFTQmU1WU1vNVhib0x0bXBkNWdYdEhCZnpnR2V5VEhqU21BZDJqTGxxYXFFamR0VkVmbUorQTA4c0d0alpqR1pnbkF3WWVteERYclpROFFCMnM2MGtiZWl6ajE0YUVjakdQckNFRVkycGlHRmtYZU9IbkVjODBVb0o1NWdqVXZUTTJFQlA1UUdYa1pTc3FpQkhhTGFnVHRCS1k4MUcyb3pqWFM5WHA0a1NZc3BEMTluWjZsVENObG0rQmk1bHd5T2dxL2h4VkttZXJ4c3BycWhaUHo4T3pZb2d2UnJvWHRqM3ZiNmc2Q0pRbTVHdHo5RE9aSE5MN0hXS2V4bzF6VEYvQ2s1Z0t6MWxnREkyNjVBS1I5QnRzOXh2WW9jbms4U29YMmJVQU1OREJUZ3l6SVdTcmVhZGZvbnRJOFdKSW8yMnhISXVPWUtzYUpJcU9aclNSRXhya2FqVEYwWnBvUEI2WlA4UlBuUGxnTnJiR1JwUDIrR1g4WGpTdVNKYisxRlRQaUtVMEVQTkNBV1RLOVh1alRkc2g0WVBENlRDRE0xTjVMYjVidm95U0I5dnZWaUhxeWp5aEc0aWd0ZWVVN3ZHSDlKaUkyaFBoTGJXNkpPOEVjRUloWlRRUXhKWkpPSzNqSUhDa3g0QkE5cElsazY3WkFpeXRtMUNvdmVIY3RLTUh1WFJuOWx2elZ5NUdCQVJWZ3B2c0Y2Q0pidnN2VjVqMGdqWVZEcGFGWVVnQ3FoQzZrekpxbXI0V0xtUzFpUWMrY3BZaTR2bmxBckowZW92QWdZSGFHZ1pwWHN6TTFSb2tLRXFqRW5TQWRubVppcGl5NVhPRXBVRnJyTTZuOTh5M2Rrd1Y1dk5QU2hwUWZpTzNaSGErdXQrUHNJa2ptaTlFMi92Wms3NnBnTlBOTTJxUEFzaHl3U29VSEtSSERPYUJJMUprWStEYnJJcWJRL05YenRjVVpzY3BYMzRkTkhpaTVncFdxSnA0bzFsVXpQWEJ1alEzNFB6Z2JmRHZXOGptdnhBSjJJbEF5WTR1cktDeGZNOTFNNVBkY3BHVE9HNW1ZcCtTbmdMRTQ2MEdzTklpelpDK1RidWVNZmN5dmZoN2JwMGd0Q29KUDk0MGZYWFlGZlE0NUZPY21EYzdjbFZxc25DdWxmcCtIakI4eXJrYVF2TEM4c05ta3VqcTVqTkpFbTRMdnBQRWV6ZHl2MVdPZmVyWitRWXlEYkUvM1RWS04wY0dmdFFTbG1kNE45NWZHZmlMRE9PdWtpVm1sQlVXVjFwdVJ1bUZ2ZVpBQmFvV1ArWDMrangzOU9QRmt1M3dhcGZTOThrUFN2K3hXa013b0p6MnFNM2s5aWZLMkUzZDE5WkdWc2Zaay80RWZEUWNJQjY0UXNuRGlRK2pSMEFteDkwcWhaNWdDNXFnSXJFOGU1cUt6enJDY1I4aXloUURubDZIREhIQUlCWW5zaUFXdDdnQW41eDlzTWR6OERhUzBjUlNDWEhveSsyaGZiTkc5SEtIY3YwNkxqcGFoMTdZQ2dxelBqRDJiY1cvSk01emNsYnU4RkJqQXZidFREWHdWdVpDWjhLZzNRTXU2OUc2cjJickhYV2V5ajdqclhXM2RBVmI0anFFdXlVR3VTR2pFQXNJa0FzM0E5akJueUNVSmtNSE1qT1FJanRxekFXWmxyejI0cEJLdTdSMkpqMVBqbXQrSFBObWR6MHlRS2IwdkNPYUplNFk5Y2VyOElCTDEzZFhQME9iWjFpM2k4L3B6ZWhjTUxGemhMdEN2UWk2c21DeGxXOHBoNTBSWXZ0c2traUFFaWhNMWNadk5VNWFYbFNTWlVmVkM2WXpMM010YTlGMldSLzk3d2sxc1cwWHkxTDRPQUMyYmtMOG5sWG9OSzgzc3FsZzBXd0N0ck95eDZmN1hSWWlxbksvU2cxL0dtaVRrS09GT1c0Y0dqWXhzYkNRZmwwaUtqQzAvY1lrRExrb2J3cEZ0bmtGejNPWU1pYzNjZnlYdHluQVJhOG5YbTZBYStEZktsbFhlKzlwUG5NOFdBQWhlUEI3TngwWUpvakRvcG1wWmR4N0IwT3FlL2hJc2wwVmJQVFdyZlB2S2pUUk1Na21CMkVEMEFqemxQSFJoTUpCTllzeTVnV2hObElPSHlHalZ4Y1doaURPclFHUW5XZjg1R2dmWXpyZ0M1UlBkYjh1MnhnOTl3VzR4L1pld2p2aFQ2NnVNZzQ2d29RaXVKenBSb2RIcHZQblhXeUdIZXhONzdVWXNIemgyb2lOWnFaRHpvQTRlZzhNb3FMdmZhRWNocldrWHNiTjI2clk0MEZlQTdrWVZpeWtQRjV5MTVnY01QY2Jrd3lQcHNZTUR4akFjY0I1a3dob01VdEFmTjJ5MXhpMVp6YzNjb2RIdHNycWFmMDJicUgwdVdJNmxXZjh3dWFRVXM3VTNHT0l2VkxaanZMODRGNVdWVXhzY2NPMzVqMjBoNXI1eVZjVU53b3hmYnMvbitucW40d2lSbUFubmlseklPUm1QWmNEVHJkUHoxZXVWL08rRVVDRHJnQzJEZ1k0RlI0U0lWOHB0UGFqTCtaUzc5YzNOaURDeit6c0ltQ3ZIVUNISUVaUWxqdUxvckJnUmdLSW0zV01GeGFEZlI5aDRyU1VzL2N6eU4rRmo2cldGWjlHajdKUTFnVEZzeDNFTnRoUWVPSjNGRE1lSDZ4aVhxMEcxVmFXQktuL1d5Q1lZWUhvWm42WEo4aW5YOVZsOUJEdGduTmkyUGJsTFl1TkV6Ryt1Z2M0eFZ1empsN2dDWHdIWDFmTzFNcENSNHA1Y3FTWlNqYkFJcEw0bktTMGFDUW8rOUpKREJUQjRpQ3FqVE0xL254ejAzREhtYWtsRXl2VmhFTXRRM3l6R3pkSnM3SWRnRHl2RjN1TFlobFc4cHJsVHdGVWxXQVc3c0dGWjYrVVhRcGFhck8wc29LTEwyRVVoNitvSDVPMUttSnFTVVRIQkw3Zld6UGY3Tlp2ODdNOUJZSDJCM1pWN0NIQ2cyYUhtajVtVFR6eXE2NGtjMjNFcWhVeWFzWFNFNGZGWFhUQjY3cHhIaENXVDBmckZ3OU1PS1o4NjgrN2JMWWMrYjdFSXduYzAvYUQxZW1VWTZSbkhzVzdjWmhwbmJxRiszSlhrekFHSm9ncnJOSEFVanN6QzBNMVBaYlBTcnBlZGxrS3FjWEh3R292aWl3S3FONGdwSlNVbkFtc3VFR0hRa0RRY1YzaElCdytZMVJ2THZ0bktCRmNmNE1XVkRnL1pWbmFKazh5S0N1aTZGUTdwclRZMDMvTEFsbmZNZ0VTRmFYMDlNNHBCbnhYUGxwM0ljeityclJVTkxPZUZRdkVtamNDWDNTNlV5MHU1aDZVN3lESWpQSzNldjBTZ0crcTN1TWZzSld2VTc2aUI0Um1TNUplSkx3dU5VMlBlZUI0N2M1eTZsWmxmcXZlWElJQzdkZnVWRUJ5QTBueCtFMk94R2drUUxiRFhSL0ZRWE5JbWp1Nmp4UmMvUzduTXUzT3hkOG9TU3JBa2hmTXZCTkI1NjdQMXFkZkxrRXM0eFk5QTNOSW5RTjI4V25RSXpnbHV1WmVoeTRobEFNMk1EWEpRYWEyMHJWcGV3bm05aUpPTjFRM1l2SVdvNmxmZUEvMTRNZzZkTTA5SjdmRTR2dXA4YTBCek56VUxBdkg4MXQ5WkpCeFoweEUrSm5TUzZrSVkrK0JiVGVkU3Z1MHBRTXdHV0pIWTdMS3lULzlvVGxXNSs3b3RseU5QcXY5NmNmT3VkbnFyaERDOHltN1FhY2ZEc3F0RVFQaTFOd0lqU0twRk5oTVZmdjE4ZXBnN3RObU1Uc1owVlBIMEY0Z2FOU2ZzQWpxbXpWSzhtMG1ISHVKM3ZLM2FMTGNESWh3SzdBbUl2REZMb0RRN2lNaHVuQURwNzJGM1FBdjFzaW5MRlVseVEraXR5cVlOQkdJa0lxR2F5SFRDbXh6ZHpZMkVwN2RjeHErRkliWUhWNlFTcDV6c0NkeXdTbGJOYng1V2RDTGNZU1VULzJ5alRldVYrclNnQW55YnhPcVJMUzNHeEorbHdrT2pRUG9Md3FUNGR1UU04WERqVmJ2dDZpaDNOQ3ByWjhGRWFESjgreDh5cEV5SEVWakx1bEZidXphRUF4Vks4cEZDdWFJNTIvczAxdWFNWjdRdGtBMUExM0VvRk5IUDMxQmw1aDFpNFN4VUo0WmVSeDhGc0FPUGxUYkJhRjZ4SWVkQVJyNytKOEZzb2RlWmQ5enJFak1ibmZqVEExWHR6TG9HQmxTd1dOcmx3WWhtdEU0OCszTkllNndLcEdzYVFybjVNaEZpdSs1Qmd5TzNqbjduaUZSSVZQRWZMUmhqYURSVnZnWHJTbEkzUlQ2ZXp6SlV2ci9mQzgxSEVwcys1K3NQQm1jWTF0QnhmZEZKZ0MybDNhVi9NZS9WWGF4M3M0MXhlWnVFd1dKVzNOOGlzZngxRmQwcVgxckdRMWxJSmhDdkxwSGRMdTRlQ0t6a3FoTUpVYW16SEQxQnFNT1lSNWhEQW01MWE2aXlicGViTHdyMml6YlVWMlRkSEVYR1ppSnNXMmlBZGlBMTRnNi82OVhWU3dUTWdkQlZUemYrVm56S1daUW95S2laUWRlVjVUdG9jbm9CUGdmOFhqRjJmOXpYN0ZwQVZtd3pxVEE5UEhqQ0pIOWRGUEIwK1ZTYkd4NWFhbDd0NWVOTFZPWTVKbnVBUkwvYVhSUzdTbmpWTXNobkszbTFaemhHNGVXTVd1TUE3aXQ2WTU2c3VmNWZwejhNZ21idDZiMWM3RzloQnJ5eUhTU3FzWGZkOVBsRm5XeFp3Rm8xOXo1MG5LRk02M1g3VmRGV2FDYmhyUllRTVlwemYya3hVaVQ3dGx1RXBsV2pNTHBoVWJRdk4vdTgrbGxTcmdFelZlL3hZNnpnSGJRMW5Rby96a2RBeFRSc3dpd2JRNG51VXBIQlhrc2xZeEJ4Y1ZxYzNOcVpNUGZPWGNGNXFvVy81T21LajlPS1N1YUtRVFB4TDh1d0RyWUJMR3ZwRlJjQ1NlSFp3N0ZoY2cwT0VERzZEUFpUMFZYZ0MrVVJSRXhkb25kRktpUk4vRXFvL3B6VEtEVVNDYld4ckFkazJrVWxYMnlKaW5OZXNSZGJiTzZHcmVaeFN2NUpEUFFoN2xlcXlUVjRERHlneWdnUHhPUFI4QW55U0g3L0ZXNWtoaWV2cW1QeHY2VkNWbExjaDhrWjgrcHhZMFd0eUNxa3hrOGRQcitlcjRhaW10ZGJLTFdzVVVZMm5aNU4yckdScUVsMU5KeFljdFVPejRXamVmZk1hWDNrVmRUbUlKeE1MS1pDWWFQelFhenEwSWNSOGhCaERKd2cxbVI4SUdoR0N3UVpnaHc4dzQrb25aY1VWM0tOeDNoOGdHMjhmR2hzR3hsRDZ1bDY2eWJXS2EyMGhSQ3h6UXdiVGNaYjBoMW05NW1qUWRUMEpJcWpZLzhpcURNVE9XKzY5TnJ1UjFibEJia1k5WnRaeTBLdEpmR1lnaVI5OHE1bDlOSlBUSEpOMzhVamdlSGNZMXYvSFVwTk84cDVHSWxHRERIS3RVb1FqTWExYTI5RkFpQ1BWS0k4N1VqSUNsKyttcUlHd3JxMm5QNXg0SlRqL0cxRUxWVXo2Y0RmbUd4R1dxVGh5V2pGbUsrL3pZdHlRc3lHckNycm1MUUI4NVNiTysvdXhkb3V5d1Vlci8rQ2pXcnNReTg2SjVRWXA0em02WXErSFA5eUcra0QvMzNObmY0NXFFd3FJb1ROUGs1ckJIdnJGd056dTN5WHB3NzFINjFSYzN6RmluUlQ1MndrZHdVb1YyU0QrOFRnZXFJYXdmWGlreWlTQmVKeWh6RGhxWndHN1FJdWswSHdxUllZSTlpYy9QQTZwSE8xR0ROUXpCK3E5VXNBYkFjck5FUW55MlZRMGVmclluVnFpL2tsaDY3dDd0dkNCVkw0TTVCRTFsMit0a2IyZzdLM1R4MHZZQXUwVmt2TVJ5UHpiMmh3c2MzQno2WG1kTnBlNittSjZjZnJrWmFqOUNXUCtRdEhodDlwcXNEbnV3MFp6UDVrcHI2NytMeTQ5bURWaS9jQ3crc2FkNG8vNVhlT011alVReVROanZzR0RleldqY2pGSnlUbXZ4REwyN2c1dTMrMDZvYnFZT0NRUzZ4My9iTnk1UzRGa3NSbHU2V0pBbkFQbGNqTlJtMnc4bEJjS250VHp0d29tUEdXelNGTnJRUXlhTHZXK0VnYVlCLzVEdTJlNzFyWFlUV2NDU0J5Yk0wSlNDZThzWkJMa2tvbzlCVDBMTmREeUczVVhSOTlDdURmRUZobFM4R2JFdVhQNDAzbUFQZ29sNkFRVG9lNXptRVpEMUZLajc1Q05ZWWlmT2NXN2FzL3dBOFFueEJRYkllSUxYS3FGZC9GMmJ3dWNmOTFXY25MbXBMbGVvNmdUQXNkRXZTMzJmR1Uxd1p2WVVJUmYycGE3NzNpQWJyaWk3dFNjVDd5T3dOa2dpb01XNlAwSzZTOENJYWlnaTg1NGZKWGFHbkxCSVdINVVzNDIrV3d5dXpBQjVMeGdzcXdaeWVzQ1FFaDR4dGNiYm5WZFBJMGlKa2U0dXhDM2k5Nk8vejhPV3dJRkM3QXpxVFFoU3hWSTRVeVZxVmh6ekdDZDgwU0s0eFlXcmNmeEdXTlI4LzZ5Z0d1MU5PRkt6UGFxdU9ZUEVtcm00dDFFdGZRdUY1R2ZKNUVNWFJ0cnhHaUZLUmxLaFpSeXVCaU5xNXBBLzBReTIrNVNYTFh4dWdpTVpaNmFZZ3c1Vnc2N2FhWXFaZ0YzMkxURjhHSHcwWTNDaTRsQzlzN2FZUWdqeW80WHU3TEJGTU9NVTVoallqSDV2a2RHcEk0cUwwMVJ3WGtzbWM2RWJGZ2FCNjYyWnlYeFJFbjA5cUhyVjVDZ1daL29RMHVTZVFlZm45UVgvZXRmcVdyWmY5UEw1dWdOV0NQNGJpVUt1cnpPbk9kU2k0VC8wVFp5VlpMc1IrYXFlcFVHWngyUjhNQlhkRGs2N0I4cmlHRHNVQU4vY1N6MmJWaTdqdnduMG5LMkpMMVhKbGxJbkZzTEJhSzN6UkUyWWhZUXBKT2VWZ01KY2Z3VFltSThOQVoraHd2aXA4eStoNEdScFZhVUxHWUlOeEtMTnBWa3FTaEFSRjJhdWlKbU1BSXMyamZxQ2hwdkR2VVNweTd6VGhXb1dKeCtSak9ZUGo3dDZiSEJUUWx5UGl3ZWowckhMQjlZYnZyWmpidWtEbEVHYzN4UmRBM04vbVdTVmRFbTJhY2JlL1hFMU9sZFBaeTZOd0xoaGVyVzNzM1pIM0RPNzZvSERVVG01dEg5YkU5WURMMWRXK0JqeUJ5SkQydTBZM3VCVUZpL2ZzWUxFNGQ1dGhEckRGbW5BVmJ6Njk1U0VUYVBHUERMbUlHR0xvT2xDOHl5cFZLSG4xdS9uckxuZ3pOR21QVXRpOEozSVRxRXp6V2NiakkvY0RIc0c1bWZIV3FocW9zbHRVS08xLzVteFFnSFJCaDRyQTRzR1FETmN2TVRoelkvRFhMSFZIakdremhRZDFLQjVUSm81NThGRzJQaThSVEp4dHpPNWtQeTJaRkYwOGF3Z2tnamQ3RUd3N21SMGU5RFZUbm0vYU01UHBsTlc2NGZzVDRSVG9XYnBseFlFTERRZ29qVkpneGJjeGRWSy9pYUZjSEdpS2ZpMDVsSTRjL2tHN242djV1VUdpMHIraGtsaUdRbEswVzdTcHQ2dE5ZeFVmNFg3V3I0TUY0Y056WG1TUTdBZkpuWlF6SW96UHAydlVPclE3RUtneXRWVXNBNWJ4amVkakJGUVQwKytnbXNpMEJIM0l1Tmd1QjcyZE05VDVTazY0aE5BWExuYVp1c0ZmUlhnSjg5UnF1ZTRpMjNyZVJadVBwTVZLQ2pjVGk4VnR6enBNejFGTTV1Z0JNV1JsM256MmJJNzNsUUFGRUhjY1JvcGhhWW1aelpuMlZYSUw5ZXNyOWVpTU9Yb3hnSXdUNnQza1AxTkpaMzdROFQyU0MyaHZiT29SZ0lvLzNXVDBjVDgyblJYMWhpSUN6Yi90dHMwS1MzY3hSZlBTclBxNzdEREdncEFKNHMvbklobHRqaklzeTUvUCtMR2NFTnZ4ZG9XUEk4LzhMOHg4SHNncXNSYW5ncUFEajFoTU9ZWEN5RGQrTFVUZUNWU21sUDY0S2UyVDhOaWVBWlhybjJxY1VqaGsveWMyUjZFSFpPcW1GZDNBeGU5T2RMZGs1bXVmVlJLQXNnRmxFbUdHK3dDNDRqY3dVNUtoVmZDTHJJb1BTMk8vaFhENkhwOFVZM1FXbnovNTEvVWpvRTZ4c0NJQ2J6Mi9ob1ZYWHh1bEZ6b2NIYmVmMytvQmVPVkdCQ3NoRmFBTFlqc0lEVGprVVIxL0ovV2xLaUxFZi9Za284Z0cvTHFqdUVVbnl6THU1V09ySjQvOTFOd1BtS1laQVdCbjB1L056eXFoOC93QXdaNHptTGxUNHJwSzRqTHYxSGlxcUtlNFdZM3NRTVhqWWdEZjhxc2E3YlZrTTZBSzBoRGlTVkZKWmQraUFEZ0NadzQ4N2pwUHIrSVB0VkhBaU5sdm4wN1NmajZFbVpOc2pmREQ5dUZJM1pmY2FRK2NDK241aVNtRjZ4aHlOSnZHM081YTFadlV3bGd1NmtHTDNZblJjMTRxMXE1UkJ2Uzh1YTRGV1lRLzdUSjF3VXd6czlxUzlDZ1EzbGRpZ2pwMEdCcjRhSmZBdGU1WVNYb2UvMnNTMUJidk43Zm5uWWdvSHpBV081Vml5Z0lBRTgyay9Ea1ViSG9QVGdpYXRsRk1CeW1DTStNUXhRSDFhV1c1TjlRS2EzVmFaQnk5VWh3R2I4LzhxNFdIUzBSWFpJUnFTVFFGNGt0eEJHL084WnFZcklJZkdURDBPZDZPSmRlek5YMkFWM2FFNzBPVnduWFloZXJiNFlXQkMxcXVXZ1hTNzZ6MEF4YVdsanRPRVg0Z09sNmZjdjZYZ3lVT2ZoUTRhUnFKZmd0RXhZTnkxemNuQ3hoL0ZmQlBhQnZ4U3BKS1VGQkh6MmgxSHZ2a2JMWnZNcnpsVXdWTzZId2JHZHlPamJvZHk4QWZBVUdEYzFDTDBzY0JlTjZGWDhHbENzYURuYkgwTjgwbVo5ZDVVOTc3Q0FLTW9PdU5MWHVoaVppMDhNOHlYVXI1WUF0aGgwbGk2anpzaEc2aUhqaytQZ2Q2Y1NwNWk5MUgvTm11cE9uN0RUWFhZSHA0YWR0OU8rWkJNTVhQeWpvOGgya01YbFpuZ3VVSThnVEpYS2kxRzdFWU1EVkY4REwrTlRRWEVidkRuNWVrV0FtUXFmV0hHNEtKR21EUS9aYXNyNisrODVOUVFOVDM2Y1NNODZDcENKVGM2TW8vbyt4eXIxZTBuSmxabmxIaFhTVktvREV5cjRsL1lZUGFmODluT1BKQVdobjArTy9IMDhoNllDc0t6R2hWSzdwSjNUV1lOM1hyclZId1crMWJGMzhJZFd2V2l5QUZWbWhQQ0lzRjIrMEFRVGJiVDYxSjNSS3NuSUJZZ09NV0J5aG44T2cyZHdDUXNhdFZsbTJRODFLTUViMnRqditaM2pScTgvWi9sVytkSzliSEs5KzE0eG12c3B0eVFGVHRsWE9IUHEzYndzakJNbDRMOFk5cmNaRVA1cStlUEk1RjVLaENTVURBanhQOFFFUkpVK2wwcElrYWJidXVlVkNHSlNwOW12OVhkMEJieG0vNjNIeW5jejc2L05MV2VQUzl0ZlFEV3p5eDVpcURsd1A2eStqei9MSGcyWmEwbXpFYnNmU1JLZ1czb3VRbEhTZWNmMVcrUTlsQjkxZzZSbmdqSTZkcndTNUR0ZHRMeTNpSVQ2dzExbmVlNGgrU2creDR4emZ4MlB3ZHljYkczNWxGcDIrQnFneGpyUWFtQXJuUlBybU05Rm4zL0ZFSXR3dzBJVi9mTTB0UE9SWFZUcXR6UzlkaHpWcWNnaHNyMDRwWU1xOEFWQVJPa2tVMXFpa3YvRmkzMUNGN1p5a0FBY0lCSnVzSFBtMXZ5RlFpcFNKVG5XRjV3WVpLeTZaR3ZUYnYrTnZ4eG5lb1UwOTJkQmV3aHg4K3MrdVU5VTlaOVJFWUdpQzBtUFlpdnMzem5pZzJOb1dXcWdZOHBJYnBJOUpXbTNXWkQ2eEU1RnJ4SEZxMkV0bWZFbHV2bHJFTTUwRlN6S2JCSVQ3V280TytmSGlaSE1ZK2xEUGRUckxBbjduL2V3NURZWWhyKzJaZVJmdmJuMmkrSG92dEZRS3RjR0ZWK0RXbXZHaWNWdXR1enNuUnVXLzBscnc4R3QwZ2QrL1BPTitiZW5xMCttbzFpWENQdVdkeGNqT1ExZFlhZml4RExYUVJ6TkVVYWlnK0p1K1l2b0hhZEJMNFFlRHJFenFJWTNxSjlzZ1VkWVJBZWFrVkgvQVU4QjVsYU03UWFNbE5TMWhtTFNPNjU1bjJvdGFFYmlGN3RjWm02SzlBaTVrc081UFAvZHZYT0dmd0hMRXowWFg1aFErcUo3VVlpc0toR2Z6ZnRxaCtWSFl6Q3Z6Q2RhdUdydEJ2d1R5N29qTHAvUWV5bk5ndlo3UithWW1WdSt1RHVRaE5HNnF3NHdzbDAwTVBFU0RHa0llVVhyTjZNYzloV2ZsSkxMdndYMFhwZ3ZZYjg4V1BzU2RKMXcxRzk5NzhPVGhFQWRNVys5V1BtcnQ5QjNSUUc5aXVYaHhXcTNRbUorTmtCK3FubzBtMUlKZGw0M3IydWxaeTQ3ZUR0Q3NZY1ZlTmdOV1l2d2lkVm4rR3VZWVpULzM4Mmc1T0xxQWFiWCtUSFhuVFRzeklXRVV3MGliZ0RNVkkyZVJBY0MxdFJzVlJ6MEJWR3E1aEJTdEFXTXdSdGVjYmdPMnozaG9RZE9xTnJJRS9kVUVNM3BpblVlWlczSlI1c24yMHI2enNJVGhiUmN4RGUydXpiK2JxUnM4QXBLTHNxTEZXN253OHVNYklubzlLaFVLU0syRzMzWldVczkwTDNhNGgxQm5FaHkwMjg3SFd4TnNhNWZwMmJUdGNOSVp0N3pqN2Vxcy9HVldlY3BXSzZXaS9BYjNtQU1hWU5mN3MxZmxFVENUR0pSLzlDcUljTkE1N2NwcnlVWXpzZkFnYmNpdUVPOEpOYVVaUUdPMWpPZ2kzWjM5WDZnWTZaVnI1OWxTekFDT293ejhrRzUyY0tJY2wvcThQSzBFSUI3bS9kUVFSRythUDlkNCtjZUxxN3I2ODJubEFheWJhUTlPd3ZZVDBhL1BUY3NwMDk2dUNTZXZYQWVpTlpXWVJzRGFHaG5kMmVJYVVsTkYxcllpTGRpV3JwbmN6bU1ZL3pNdm5MZi9tcUxtVHJjdCtLNkRUVEVPSi9ha2dFc0dFaWpzakpWQUFybndvdVphOEJDV0xqdmRLNjlERzRsYUpINUNkaC9FeSt6TGRGMXdIb0ZqQk1SZUZPRkhyVzNNcDFObTlJUWZ3TTVrK1hrK1p3WE5JaFRkY2xZZ2hkaHVxM21MQ2RHUk42WUw2NVBrcVdoU00rMk5OY2pFWFN5U1c2R2RDWFhOUTJkU3hjSGFVaGhaRUNNa1hoU0NhNmFvOWk5bVlxVGVBSFRqWUpsYytIT1c1cnZoK1F6V2ZGb2FHL2NPUUtsbkdselVxSk1RcjE3T09kNnVUNXJ3ckltcXZ0RG9XUVlDMTZLZFVhMy8vWjwvQkRCPgogICAgPC9CSVI-CiAgICA8QklSPgogICAgICAgIDxWZXJzaW9uPgogICAgICAgICAgICA8TWFqb3I-MTwvTWFqb3I-CiAgICAgICAgICAgIDxNaW5vcj4xPC9NaW5vcj4KICAgICAgICA8L1ZlcnNpb24-CiAgICAgICAgPENCRUZGVmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9DQkVGRlZlcnNpb24-CiAgICAgICAgPEJJUkluZm8-CiAgICAgICAgICAgIDxJbnRlZ3JpdHk-ZmFsc2U8L0ludGVncml0eT4KICAgICAgICA8L0JJUkluZm8-CiAgICAgICAgPEJEQkluZm8-CiAgICAgICAgICAgIDxGb3JtYXQ-CiAgICAgICAgICAgICAgICA8T3JnYW5pemF0aW9uPk1vc2lwPC9Pcmdhbml6YXRpb24-CiAgICAgICAgICAgICAgICA8VHlwZT45PC9UeXBlPgogICAgICAgICAgICA8L0Zvcm1hdD4KICAgICAgICAgICAgPENyZWF0aW9uRGF0ZT4yMDIwLTAxLTA3VDAyOjM4OjUyLjY3MVo8L0NyZWF0aW9uRGF0ZT4KICAgICAgICAgICAgPFR5cGU-SXJpczwvVHlwZT4KICAgICAgICAgICAgPFN1YnR5cGU-TGVmdDwvU3VidHlwZT4KICAgICAgICAgICAgPExldmVsPlJhdzwvTGV2ZWw-CiAgICAgICAgICAgIDxQdXJwb3NlPkVucm9sbDwvUHVycG9zZT4KICAgICAgICAgICAgPFF1YWxpdHk-CiAgICAgICAgICAgICAgICA8QWxnb3JpdGhtPgogICAgICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-SE1BQzwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgICAgIDxUeXBlPlNIQS0yNTY8L1R5cGU-CiAgICAgICAgICAgICAgICA8L0FsZ29yaXRobT4KICAgICAgICAgICAgICAgIDxTY29yZT4xMDA8L1Njb3JlPgogICAgICAgICAgICA8L1F1YWxpdHk-CiAgICAgICAgPC9CREJJbmZvPgoJCTxCREI-U1VsU0FEQXlNQUFBQU5pTEFBRUFBUUFBMkhzSDVnd1dEaVlXQStJQmMxOHdCZ0ZnQUFBQUFBQUJBZ01LUUFHaEFUa0lBQUQvLy8vL0FBQUFBQUFBQUFBQUFBQUFBQURZUWdBQUFBeHFVQ0FnRFFxSENnQUFBQlJtZEhsd2FuQXlJQUFBQUFCcWNESWdBQUFBTFdwd01tZ0FBQUFXYVdoa2NnQUFBVGtBQUFHaEFBRUhCd0FBQUFBQUQyTnZiSElCQUFBQUFBQVJBQUFBQUdwd01tUC9ULzlSQUNrQUFBQUFBYUVBQUFFNUFBQUFBQUFBQUFBQUFBR2hBQUFCT1FBQUFBQUFBQUFBQUFFSEFRSC9aQUFqQUFGRGNtVmhkRzl5T2lCS1lYTlFaWElnVm1WeWMybHZiaUF4TGprd01DNHgvMUlBREFBQUFBRUFCUVFFQUFIL1hBQVRRRUJJU0ZCSVNGQklTRkJJU0ZCSVNGRC9rQUFLQUFBQUFOZDJBQUgvazk5OUhHRVNMQzBiL1pZWllxVjNuUVZ4TUFXbXpYeHFEVWhlNFhZaDk3V0RISCtoR2RMV2c0bGI0elVrSlIxMFkrUjYzaTBSenl1cFc5N0VXb3RHNnRpUy9TN0JQTW9qMEFKWVpRUzQyOHU5VlJodkJHQ3p4dTIxWVhUYnpqcWpzcVhpSGhBeXBjRTkyRDRrUmxpd3FuSXE2M2l4OU9VNGE1Mm5INVEzVHZKbXVQYk9BTGkxMktTdXFNTXlGZHlqbGIvSDJOOGZZN3creHVCdEU0Ry9lblRvelg2d215SlVBSlltNWZ6SE5OcERmSTdkNWo5NFB6SEJ5eUFqQzBvRCtDeHhtRXhwL3diMENRWktMOFZiMnZyNlhXeERmaHB4ZnpYK0JBMVhUeGJBZjFzdytvOXFkWEdBVkpKRDBwQ3FqSlpSKzhXV09sYmdZSWRzbk1jSS8yaGUweTZwNUhzVVdkb0hUdUxvY1V5YlFBNUU1WUJramlvVXFHckJQVkpmcDVoREdOR3VTUGtVNGc3dlZWa0JEMlgwQWpzZU9IYWFjME1CRHR1bUhobTVMSmZ2NXZUZm1SMzdQRFBGQ3Q5VXdGUDV2VGJhQlNPcG9oUWg4SnBpRmlPSm1aQU56L21uSFoxQUJ2Y29kblArR2tKdzRhNkhENzdOaDJmSk9TRkNKMCs3WStGTGhuVndDQUE3SWlqcFkwWDZWYXhOeHQxbmxyWWJmaWIzSjBJMU1uS0FpZFhNQTBaempBNHN1U0Zkb3g4N3pQNEJqRVA3Q2xGVndlOUt6NnNzSDY3dkpGdk81Z2tBQ3RKQXdTOC9wdlJaUkFKOUJQWDhXbFlsTVNnVkxTc2ZVWU5YNDhxTVpBODZtdW8veDltdEVmWnNaRDdOY3EyK2tYdFRZQnU2MmtEOHozU3NXRTcvZk9SMC8zNVJjQVEwSlRVWXFHSTBNeWhncHlPN2xhblJjcXBpRUxLU2tqbSsveDNZR3kxRXp0a3BDT0FLNGZ6MVJOTWFYdWpoTldhcjVsb2xGSXQxRS9DdUZrbUpPM3IxSWtlOGNQOVFXb1JlT2I1OHJkQnZJRUZBVk9vZzlWWGw0czRPck9wSStPN1NLTU9HNkxkYnFQMGZvVlhPODRtUUNmUUxvdHcrYjcwaUkySm12cUNPRW14aG50dlc3WU8vRGVlOUpPbXFMV0oyVTFya0lZdGgrV25CNGZ2YXJWSmVKY24vUkVDTmtlODJnWUhUZlRWcmdpbTQyT2VBNlFRT3lHZmFtbW9rcmtya2kxUFZiMmdKYkxYdE4zOTVlWTQ3ek1zMU5NUlk2TG5YQ3k0QzVYVlhONDlvU0FFWlZOT01rdlpuR0ZjQk1BUWQrSzlzWFdWV2JNOWZ0MGtNWXJZZHZNRXdqTFpqbzNwT3BnVmZaRWg4YklOdW5jQ2UrQzlqQ0I3TytyT1J5RUpMc2g1UWlVLy9LOFFsZ2Via3VoRjRmSVJrWHBnbXdrSXZzMVBkeVgwWTR4RkFCTWpvMDl0N29iRDVMSVhUQlRhK0hldkFJZWdReWltZWIrSlRWOHhDSUdieVV1b20zOEFsQXJXWkNLTitoWk8yREo5YXNlMWVGcFk4enlUTjJ0UTJCYWxKaU9LK3JmZ09WUDhBOC9Mam5HcTZiakYvTEhTTlhEd2RwNUUvVENUT21aNTFwNHRFdEhScGk2aEFSa1d2NURzSFlzbFRLdk93dG9zdDZJZVEzZzZ4K1VyS0lwNVlmT25SdERjaW9FUDRyLzJ2RUtMaXlFMUJ2TmdsUXZsb05BSXhyVWNpV0g4UFZGOFplb1hHbEhhaEhwclZoSVlvTzRJbFVSZ0RBVkwzeWY1QTNJY2xaemFoS0Ridms1emI4YllpV0hGeEhYMHFsTGFpZGYwNzY2b29VcEVudTdCeDFESW1lTTd0TGdTVGxyNDhGTWN2Yk5YV1ZJeFo0R0lwTWI5d3lKZnlUSzV4WXlNM1NyRXpKdWpLN2o4ajEyaTVvckdtbXAwL3ZNdWQ0NmNzTG1yQkJRRlV0Yk1SOGgweXhmdG1mUFQ0aTdBUW1wSFNrZnprcytWdFJHdkMwbWIvTHIxdjAzRXN1bCtIQk9zMHRIOEo2L1poTUNKOGRhekJvSHpleFpvcWhzWXFqYUticzhJdFJDeDV1UkVaVzFyNm5nVnlhNG1qQlM2R1E2YmRnSVN5UXN3dEJSS0VHUmQyaEdONzliWUpUaU9zVkNpem9id21CSGFORlprUmtGdGRuWXdhKy9YOVRRQ1BFeXJhS1Z3YmNBbk1mVlFUdmlDMzVqWmRXSFM1dklHdWdaRVhJQndOMkptYTU3Smg3dnhXTUJ5M0wxSDVsN21qLzIyTWZCMTh2YXQycGZCdk14YzV0YVJ4TGhkOERyZ2tnNXllVUd0dHF4bXhSS2pIRUVmM0pjVEV4czM3NHMvdVMrd1R5N2ZBL3YxcHhBT2dSOHphem9CdXl2MnlQZGVVZWFQbllacE5JNVJVQXpnWEptY1RLYmRaN3kyMjRscDFjVzg2Vy90RzRTSmhkVmFnUk9va2tnVmQzNU1YYWRBbUkwTUs2eEhYSnNQVkk0ZnlWRExNMzBETkZDdEJjUkFlNThaWDM4dHBxOVMwK1Rya1prdUVVRyttZkhYRGtQWlM2L1lGeXY3c1UxWlJaa204cTNwWWNQNStjSWxZa1VzL29FdXQvM3VLaHBLR01jblVvTG0wUXArdS9sV040SGJIZ2ZwUFZ6YkNmRzNoQ2JmMWRxaGZwZ05ZMDlqdElxbWRNUUxNNC82QlBVdzdaWk5aVk5LQy9IN0U0NkJaTHFuSW1OL0hhOEdNaHNpZ3VsMngwbmhuRzY0WjlHNzNMYU56UUhkRnptZFhvS1JxOVQvUHc5TDJQdDlQdUgyZW5tQjdYSk9kYi9mc2lsWml1TFp2aVYwUUczM0I3dTc3QTM3YVU4Z29ZQklzM2tKUERyODY1Syt4bTJibHZJRTc0YzIwZ1UzZVN1QVhMMzlrYWpBU1Z5cUxzdk0vM2JXS1pmMFlldFZvQUZEQ3F1b212ZHZZRUpmQ2J1VGt3dVB4YzJsT1oycXdONmZtcWVnVjNwNytOZW5pclBodXR6aHpIOEJ2UEV2dmMrak1yOUp0MkJWVGtwbDM4NTJEYnBBL09sQkVMT0h2c0NkMFlUVEExWW5SLzNwTVpHYXJzYjM1WWtSb09PRFJmQjM1dExQU0ZUTm9lQnZEdTRtdXV4RXVEOGhQQkVhdXlXcDZNYUZVMnlFMnJjdnZHeHhZd2Z0Rm03ZEZyWHAwOGlFbDl4dXBsWkhGTDQ5ZDNkbmVxMXc0NDBFWWJVQ25XSCtXVTdlN1lRU292UmNsd2xqTW1OSDhkWExrY0Y4RExWbWJqeDljdUhMRmY2Nk1rTk1lVnhyL1VIVXd2a212TGt1bjBncXd5YlhCcXl0ZGRBN3Z1VXgyRkx6T2krZEhGK2F1OVNKQlg5YU5TaFVZYXJmb2hmREM0Vjk5S3J3K3VySW02alczdUcremRmQUJXWXVadE1FcHYrOUJHRTEyU0sxZytjbnBRV0ZUdHZJbG5yKzQvT0FGTWRJY3J2eXhyd2JDTjlDMXVSV3QyWnl2YXFaSWFVelFxTWhGTlhkdis3d25uMjBEaG1jazY4VTZZZy9CSkxHM0Ywb3JaSGV4QXdvNHdMOG1OSGhMdStybzlVZTM2MUNjdE5lekRVTlVsWDN2dGI1YnUrN0F0VnNLZkVvKzRwbXBjOGszb0VYNlQ3bGI0NEx5TjRyNjN2QkFKcEtpbUIzTndHcTU2c1BiSnR6WnVoSGttMEM0UVF0VjdtajBvR01vSGdXcjI4a0hOb2Y0MVVuUUxRbElMNEtxcS8xZit3YmdrL1prL3M2encxWXZVY08yK0pwaGdYQXNRNTliMytydzdUWlhYbG8wSG9EWGtycmJ0T0xzbXBVR3J3SjdpVzZTNWN3S1FERGRpbmgxcFFQbENwRkFmMDcxRElTcXZFMXdaUmF4ZEFiRWFQVHo1WXE3MFpwd3lFWFBONmg2OHJ6UmZwRDRPTnJsREpqV3FSVVR2KzBuSUEyYjYzQmM2T2JVRWVGUktUUmg1RWt2NThUa0hjL3l2WVFKMklQRGZCS1hVVm9qdk5UVk9ZNER0MHZhWi82d0RLQk80dUc0c2ZFMW9UeFhyN3BteStFUWJXejh2Nk1Ld2RQS0NMZXVEOU12dUNVbWl0ZG9VbkVEMklCTlpKVzdqTTg4YkxQNW0weFV1ejY1NW1Ba2ExeFFMTkJLUU5ZRFhjQmwwSTlwTnpNUm9hMEY2RnZHR29YYmhvTldrTzFrbXJNbTBvZVVyekx3MFBFYnBTRDlMSHo1Z3RNZmdPMFg0bEUxeHJ5MG1BTmhaL0F4YzhMRXJ1SFRESnk1Sjh2YXQ3Vy84OTIveGorMFdtaEwxNDZYRjRkbUZQb2h0VzRVTmV4NVN0bVdxbUdVNGc0YXo0UTUwbjljL0ZQRnBzekoraVNKaTRKR1M5dEF5K09HRGduL0lTQ3VzeXdRZDRPSTNDRHV0VmRHZ2dvZmxNS3ZjY2x2aldlUVZWaHJteFNnU3FqMFQ3T1RsdndvUEEvUXVtUEFvdEFMMTg5Y2J3ZHh2TUhLZTlhWFozM2t4RkhTUUlodVdheWtHeHR0MGRqL1ZYT1NpeE5EVUN3QStRb1Nhb0p5WXBLS2dNTEgwRUd5VjdPM28rQ2licUZ2RExsTWF6VUdZd3FIQWRDOVlSVG14QTdLREhNdFRGeDJXNXF3N29JSkxQSW82ZXdPMnhZQ0pSWFVudzUvdDdPeHZxZWdWT2NyVkgvdmw1WlRzN2JDRU9pdzI0REwrYWpQUkVqVEd3UXFySlhQSEVqUVZIcy81bndpYkcxK3NIVnpuQmEzSW9KTVJ2VG9xT041c3JDSW5RVXU2MTBVTUx1Uk5UYkxqbzRTMDhSKzlGVnFnZzZjQ1ZDQTAxKy9qWUNPODNmQlBWTWxkTHBOUGZTbnRTOHE5V21sMGdFeGNsYkh0R3BOdzZpSUNUemhSVUV5MVd4a2U4bjdBajJDbkpBN1ZCRXJPTlQ4UHE0bEx6K3R4LzVwdmZKcVE5K1FXT3BBQUVZZUdiYkpTMFpEcWg3NFpQOXdOMTlGMmN0ZDY3cDg3bzBLZitzYzJPMTRPeHBoaTVucjRDRHoyaVIwL3dqRllJVHd6enpDM0dEb2QwM1RFU1F5akdvbkZhQlh4V09xVk1Xck11Ty9EM05iSnpqSHhSQnlLcnl5a1lLTVYyeHBIK1Bwbi90OHJMa0lYSEJsS2k1QnhTRXZIdDN5d3NtQ09aL3E4c2E2TWlZeDgrWUduZEJJTmZVS1A3eWJSYkp1U2lCTDVTTzlUaG5OV21hNG8wVHpTMzZMajNPWEltcHZSQVFtNTFnSXp2UmlvQUIwU08ySExpbnhRUUtTa3N1cXMzUjBqT2xZbG1nd3JBQml2endqQzgyQ2M0NmhtNTVqYjl3QXdCY3hUekdCUWdKWG13OUFtZm5WY0tPUzExRXdCdFhXRWxBVmtHQ0JaTm1JVXpzTkVEUlo0aE9xZk1ZamxDY0tsTHB5aVl6UWVJUzlYMkVjM1dEbmNENlNuMzlROXAzQ05rSUhYNkNEL0RwZTRpdDdNYk1YWC9qVVN6ZDBOSFJyZlRIMU1HWUJzM1V3ckkvSWkrc0Y0aEgzS2xKajRseTlzTVpmOUhHcGVMNzZFZ0RhdlBVS1Fuakh3eFdxOGVNRTRvMDhZL05nTnFGdXpqbGRzTmdDbE1IcUVWYUJxa0xlUys1UXgrUjIyVkpwVUEySUxoNmozS054ZEp5Qjh1MFpsdkNXUGw3bVRYcktQYUJlZjRoNU9uWFA3SWpnaG9LZnRyRTB6NklBU01vQjhmQlR2T3hXWmg4UEU2TWlQWFFLcHNDemI0V1V3Y0pWMnBTQ3h6ZWlzeVlSYU1zbWFiWkkraCtYVTFNMFdjZ3lqY0t0TkhjbWFtSjdMMHpaU2ordG9zczU3d0xXVHNwRlIzL3hmY2lDcHBRZ3diZzZ3NTVzRU1EMjNwL3luRXNJUzVhbDdld0RuZWhOSFp5YlBCUjZRcXprb3ViTWZ0S3UrMU95YnE3M2toc244OUdPRm9tbldCSGxiNFpQWFg1emRwbm92aW0wWDNrY3NaZm1tcTA5VE5keFJicEJjS0ZiTjFjUVRIU0YzM3hLeDR1ZHZ6VTBtQzRRNjNCT0w5UUg1eXpVR1VHcVI5V2EyOXJySEpjaENjS3VMV3JZbmx2STcyZ2ZYQjV3K2MwRHpWNzE0NzRCVDZySHJrYXlIcDVpcVJxRFpha3VUVVlVbmRxTDl4WnN0VVlLTXJKelVLcWxmS0xTVmlxKzhHQTlLNTRxYlhMRHlBZW1YMGhNakRsbm5qQXRFdGEvUklkWXRSNnVnL215U25qOEd3UTNHS2VJazFVS256bnBTcE00NnF6UXZOY0hLVGZDV0xBMVNDNkFmNjBRdElDbXBnUVFCM3cyR08xUGgwL0JHZzFvMDVwdzVLMnJocVBoMThqajZYc0ZHQVJudC9kWU56RDRROE5ESkRldWw0OG9FRUNwdXN5Z2tiSWFnSjk3ZVo0L0Y4Q2xtb3drM1o3TXNDU09FV3FuVFZscTJ5Q2IvMkROWGoyMFg5d0tmYUdwYmRuamF5VEJSWC9wc1J5RmpJdndoeWlycFJKdlNJV25DTzZYcTZEUFBsVVJQbW9SbW1tOWl2eVdDQkh2U2JlWWcvNFczNGN3Ylkwak5EUHpOWXVRZDVYYmVFYnZmUU5hMTVwb0Ezb01Yd3RXUUZmMmEwV29YdnNmR0lrZ2xIcU44NEZqd0NWM2xXa3dhUERpVGhTbTh3S0tWRHR3eDVndzh3QzNob2lmNTVtT1dhU1AyMWw2aUdwZ2IzUGhVUkIxaTR3Y1RlNmxGS09XVHJoRUVmREVVTU9abTE5YzJDanBJYS9nUlkvWVJJUm5HdGFDMmNDMXBMQnFOc1lYakVOQ1lydG5mNHZIY2ltaEhjRXhLN1Noa2RUdUJrc3pMU1hCckUrcFFDVHFxcFQwR0gycGdXZ1drNVRFK2cvbE1WWHpUMHRYNW41WU9Sa3FlMGVqMjlVNy9BZUZhRGs0Mjg0ZE9VRUNHd3JZd0tVczAxenlmRWtyNmdlN2g5ZjZWT0lvQW9mNjRlci9QNGQ3TTNnTDVFSFFEVEVRMzl3L0NaWHFvdUpvUkRlbGJXczVpbEF4c2MzWDdmUnRsaURiQ0gwdkVsdFh4NXplaWVwa3JvOVVNczhyMW40WmhwL0c1UldmUWFoYm9GNDZ5ancxeGg0ZG1RWTYva1dlT1o1bnRKaHZBL2wrRWgyNVorSzVHQVJ5SDlRSTVMYWl5cUwrbVViVFZRaW5XNVQ2NjhjRFBOTVlKbUVCMUNmUzlGY3AwYlJUbkVkQ3VUNUVUbnZHVkRMam9XTGdZZDhwWnBtQTNCWCtTa1hTd3dCc1ZLZFhRalpjU3ZEQ2lSL0VyZWlya3EzREFvME5LdzZRZ3ovVU51T29RUDdkM05EZk1VQjlTNWFHUWoxRFhDdEpnVzFuZ3NZbXY3L2V3N2FNdC8yVG1UQXZwME5ZTENXNVNMeVZkVmhBbm12VnBHMEh0S09lYXlOVmtIMzY4OU9KanNRS09CN0FBeU52Tm9CZ2JTaGlkaUZ0b3NwM0dNZXk0QWRjeDBYUDZyY1RBcXB5d2xpVzNmNmMzdmZxeGRmdXdTUG5ic05oTDloalp0emwzRldmYUxJMm03b2dlclRHazhlKzU2ODFLcUJHcDlPSURNZTJFS0w4Z1NUR3dtV2JCZjlnekNCNzJvWnBtelB4Nlc2dTBETnFLa1VjZjVtcExsVXliS3RKZHVMSXNKSzN2b3NYM01uTEYxMEI4UFl5V2ozZ2JGZVM3VEw2enJOZSsvTHZYdjdBa2hUWFdlSkN0Sm1hUHV0MGhLeEtCckdXTE5KYjVXcXRTd2pVZ3J5VngyNTFUSU9OOUJmb05pamZ2THBmWnkyeDZzelJYY1oxZlQ5S0JhZCtmNnhlTTNoU0dBRGVBT1dVMHVhM2ZROGQxZk1GREo5V3g5d3hYN1NYRmRPNTREbHpiY1dEWDBDdlhPZjI5eGQ0d3c0Z2V2WUF2RXBVQzVMcTU0azQveXltUzFWYjFLa1RXN2NXZTJkWkZ0Z1lLUFQvSFNqbVJTL0JHVFZrdGp6bGE3V1A0OXU4MGE1MG5nTkduUFc0TFphZU1OVk84a0t0dGI3OEhscWk2RWU4Q05aMVBkZ0tBM0pYKzZOMDBheVVPVkF3ZWpaZ0NnUWRKaXNHZ0FKcG5LL1pWRWtWV2hVNEdxUThvYlhhK2hGZlFPZCsrRVhUbmt5bnNRVURtVzF0TkFyVUdvZE1GWGwwNWlqRkVmaHdiUFhjcThWdllLUm5GYksycDFob1V2SkxPYTFERnJnN1VGY2pTOWh2SWdkaVNyTlVnak9QbG9yZGhCRXQ5NWgxZW00SFRwcHBCM0pobVpDME5yLzRvZHE3cWwxQnByMEhZdmFPZGdYZE9FaHkzZGtoUFpLMVhjNjB6RDFqQmhlN1kvODYvTE8xb0toemw5S2FMczd2eW5taWVwZkxIYjVDdXJ1VEthS2ZreS9aOTF5UTNwMk5nM2NTeVRrZU5WUHJQbnRZME1hZWdKcnk5RXZPZXIwcUpLYXRtQUNnOHVpWjVXUjRnWmtOQkUyeGtyR08xNm5LN0hjYXRQeUhnRFZDaTFPc2hBOFdCNGE3SkppcEs5SFhKeVlJcHkxcTBObE1MNC9BcGdFUkJCeDVWYkVPVGpMQ2VVT21lQ1dPT2s5djJFU2YrVWtxTTczQnMzQWljdHFyZHd5U2dVV3EzdXFwYlRmZ2JpRU9BMjRSd1pROWFOWXFORlk4Zmh0V2o0TDhjNWlzUUEvU3REbW02dll1QzNXVzZxTCtLREhhVjVyZ2F1M3hWUGE1ZWlZOW5GbllTOWlHUGsweXI3VFdDZUFpQ0l5VUowZU5jVzNEdnhoV0FyK2xYUTM2cFBDYVg0Sm5aM1NDbTJ6SEprMW5mTm5ibEdYWE9MMmM1b2xLYWJvb0hUM2JqM2djNmpjbzZDTWNYdEFJU2w0VzlIWHdPbzIycXV3WmN1eGVKYzhDYXNNQmczc0wycXB5bnN2RHpHMU9GY2ZsQkdiOFJVNGlxSmJERStaT0RpZHA1NUgwT09vRnlCT2lGQjB4OTgraCtBYzV3TFM4UjJYN3MybHNlUmgzeGgrMVRUUDJWOE9uWHlqcHkwOFJFaUt6ZWo5VThWdkp3SU9jQlk1RURMRjVLWldQazkvbFBHYVlBWG1vMFh6aFZQOXR2R1pxNEgrdjBkeGNlVytkZjJINlVBNWF2VGs4VitGRWwwekpoeDRDV1BRSzVJaUFGR1I1Mld1VEp2eHlpTUZKekRPWXJBMmNCd1dVTXVBUktpUWU0N1hBdEIyMjAyMzB5ODBSazdlMEREaE5IbWxXcWlaemhPMUkyVWVUODFVeEpkM0d0SHJNa1dyeWVFSGMwK1AyZlIzWDAvVkpuejlXNCtUWHVQMi9TUmZzOWRQOXV2bDlWbVlmdCtrTC9aNjUwK2ZybGZWWUFNYW9kbEVuaVcvNHFBbkxTSHRoVmg5alZQQ1htRVFGaUZNTVpxZjRDNTZmZS80WkJESzQ1VFdmdzVMVTN2UjVJaHJQQ0w4RGNGeWZlWnVNcEhVc2FRMkFoaXRVbVllNVYvNXJ5c0NvYXdDOEU2ZURZMFl0RmhqT1pWUUE5MFA0TGRuZWJWeFlwZmIxN2NkUkVuN3pnays0cTJUQnJlb2hhVWY0R0RYQnRZeEpzaURCdEJ1cVF3Q1o4TSt1RVl1SGJzUXQ5eFdYdWlwNGNIVFkvQVVNMFlrZlhyNGlwbG5pSUpNSnp3d1lrSDBXVTk4RFlhOThSaUtxNjdoMUpnVUlaUWpRRU13UUZpbTIxS1VzSG91anRlQXYzODdaZzhVT1NDekt4MHI2RDhOcXdveFVXblFubGFNYTNUMk5pQXl4R3JRaTJ0d1pJbnFTaGhWb3NFZXdSaW8reHlwV2EvM0NzL0FiVmg5LysveWc3Rm9nYlhFMmJrNjFKZnAzOVVjbzFiMXRWdWUvN0JnR2V5ZVdnaFlsWGdKS0pOMXMwTDRCRzd4WE9FcSs5eGdjSy9wMlB3ZXFoSTBub3JKZGhRVHU0Q1VibEErZkgwSlRWU2dpdUZ5UXVEZGRDUkxEZ0J1V0lxaXQ0b2NRN3RxNWxnQXVySnQyMlI3UE9WRDNDZ1FuQS9kYW1DVHpGd1hkR0RvakVmOFp5dFhNblBLQTZlV3RKck5VRVg2bzlEam5VNzVNS2grb0J3WkxOQUlEQ1pGQ20vaGJqZ0gya05LRlV2TGIrWE9pbWVZTzRRcVNiVzNZdVBxb0ZiamJ3cGNqMjBzWGNmcTA1Nkx6a3lHUGVnVDNTbDhCSDhkR1IybXFkWldpSmFPaCtqUkI1MjhpSWl0VEpkTlhjRktjcFo0ZHpKV2NrcWpOV1pNWXlRYmcrL3lJVi9uc29wWEgxbE9RQ3ArNVo4N2o3RjEzM0E0YVZHK2kvWWVlMEp3UGVXbVVhbUx0SWdKa0liVEpvTFhoaVBOb1VRWnVMT2VqRXB6eTZTZWdwWWc5eThGZ0huaCt2L2JKVm8xamUvM2Z4UXdEbVRMSGJIdlNpa1BZUGZTQnpxVkU3b2M0c3BqRExleTZya1Y4b2FCbHhETk1EOE9mRkg5bktWc2l0YXJOSzQrczlBNVRGaW9Zdmdxd2NTeUxrbjF0SUlTT0srTXZaaFN0M09UTnF2YkpFck5aM3N1YVp6Q0JmMkpTR0JtdTV0L0psdUovNm56UFJqRnY4aUlTcEI1Z2pWRnRwa0RQa2VGMUkzT0J3K1RjbjZQR21BVHJTcXBNN1JGZzBYOUNGajlBcjhkTzNxMVlOZDAyblJBRlR1TThUclNiOXJzNlRUd09nL21ITU15cVQydVkzMmtUeXFhMU4xK0dmbjRxYXBQYlFJZ012WTVXWkh0YndzOHlac0t4eUtRL2JiYjJncDVlUGxpNWVJTGU4RHNQUXNBdWlZQTgyVU1iekt0TVZZZTk4cU9hL3lObUdTcFRWYXVNbTAyM1IxR3p1eU1YNURlcjZnQzFDcTJLWWRGcko2Z24vUnRDT0N3Rmt3N0FQcmxpV202VDRBWVZ4TUpycXNndVRqYjFKanZtb3Qxdm5NL0YzZ2tFOXo3TEVXMkkyZy81R3gyd29JWFp0dVoyT2FsOU04dVI2OVJTZVNBaENIMEFhNjRhWWVwNlhSV0czdUtFTGt6bSsyTmY0NUxoOUoyempScVZqNXFjZk9NcXl2bFlTVzByMk8rbkhNSTQza1RWVmd5RVFWRHIrWUc1MFhDTERVc2cxV0p5Slo4S2wvY1c5bzM2aTVYYWZwdDF6bWw5VU91UzZxQzZzL2hFQkdPY2dkU21pTE9oNUJIYjFjYS9RUFpnNElITS9Mcy9CZ0NMUTlQL1gveEU1SmQ1a3M0K1FoUDlGdFF4UThCT09oL1VJTDNpd2RucXFXVDZoNUI0dTkrR2tWUnpUYlNCYmpRRVhhRFBUb1pRRS9oM042UHo2ZVp5NzRIaXpRWWt4M1NsZEE1NmJ2bkVyWHNkRk1GWnZNQlc5ZUFnM3pSbnBaTnRFang2cll5dTNYeC81UG56VzVaZzdJd2VtMzV0a2UrZ0pwRG1HQW40Yjl4QS9Jd1ZFUlBYaFJSUmRNVUxHdW96YXhoczlsaWVudGRhWmpDaWphaFlWb21rMGZJNEIrVHNZdFdQdGhIeFJvaWE1ejJsNXN4MnRNRGVCbGl5NWF1Q2VlbTFySC9qWGdxSE9LNzIrVzlrUVYwdjd5b21WYVIrbHBFVGs3VWtaRjQwd3kxcEJaYkVvUnNUd3ZqellvQkh2TFExZnpDeG1HWjJ4R0p1Wm1VVHpCZWFUSVJ6TUhNdlBVVkhvMGJwVG1mMUZKclNuUTQ5QVdFY1R3U2oxTG4xckNHcHF0SEw2REpha3RseC93ZGUrTlFtbFc1Wkwvb1FObjk3cXhLT0ZERGRWYWJZbDFXTjJtR2JNWWhJb1dUMHZ0K3c0YzFnZmlZeDhhVFJSNTFQUDc4aUQvckpicTNuVVdzVHJBNjdESy85RG5rZzl0OFhMOVNyTzJSU2h0R0F3WkxRTS85Sm1EMDVCeWoxT1lBM0tSV2F1QXRpMWxDZmVXSDRHQk1DakxIOURJVDFIZlF6aDhBdGdVMEZjanhRb1ptNHdTR1plU0hudlIwM05SOFZYS3B2WUdpS3JzV0gybXhoL2pqcGpJUmdicC9BSHp3V0Uyc2x4ajBMckZuL0ZYbyt2KzVEZE52aDA5YzA4eC9LZFNIbVZwVlY4L0M1Zk1lczE2aS9oZXl6Rk5za0s5VDVnRXkya0Mvc1FBby95QzZnSkd3eFFObU9oVzZiYzZiVFpJaWl1UGR6bHZVK1Nnb3BEZFJ6RDBLT0lob3JNbkhoeHY4UFZSNG1LWCtWNlY4aXRSQmx6b3Z6WW0rcDJhSWZ3VG9pUmMwenZWZjFPWC83bFUzaXRsZTV2eE1GajRTMlM1ZHg0OW1pbW0veXN6cWozWUN4WUllL1NGRkMzdFNXdHBKVTUxV0hPdEhYa3RSSjBQdklwL1Z0SUVhbkd0b3RvcTh2U2lzbzJkQjFKajF4ZEk3RElCRVNGNlQ0SjIrR0FMZENUMlhIc0hVb2kvek1MZ2tFczd1RE8xeDlRMzk0ZlJPV3lGeXJXY3dwWWlQSHlvbTFET2czZ2tBWXhJM1lYTmlhSFVhUzF4VjFhaEZuUkl4U1NnUjAraGxSMWcrL0ZMUGE2cWZvS3lyVVE2MG1Ob1A1Uy9xbldJeXR3Y2gwZUY0blV5SFkwNG9VRldyaXlaN0J4SVFGcm1XMUt2eWM2d1NZekh0Z3hQN054cW5ab09Ddnhwby8xVGk5bE9BWHgvYTI2cUc1OXVhUEdsVnZWalgyRHRqTTRVZmJyRTFWRzBzWVJwYVFnVythRzZkMnJCMWw2emNocmUwaHliYm1yUDBmMlhCK1FmUlVRZHkwTUhVSlpjYTMxTGN0VFcxdXp6Q1p6WWRxYmVJWU9IOTFpbGFlNDVadGh6VEhuMmtORG1neThWNmtRaDB0KzFHdng0UzNYSlp6UDhCbG93cjFGY0FnL25oblFWb1Z1YjJpTDNuVzF0T3YwRG02THBnamFzd245TUgza0kza09mWlNicHhmZmVJbURLdHYwaFVmL0tvcmJsbkZoYkpkbDRXTnBoRWt4b3Jlcm9YK0ZpYSthU01hTFpsWHFjcU5PSkpGK29ydnFrZVIvbG51Sk05T2NmRnB5cDNHMTJiMlJ3THN5dEFTVllWb3kySGlSc0MrZFJLQU42YkttOVJxd1cydzB1bG1SZjNhWnlaZWhBbndqdk5kbXNqdHVJYXhHN1VzVUI0UWJEUUFZM0pyMEhHbWRKQmxwMVZxWkcvQmplQjlVREZsaGRMRkNMODBjZG5adzdHNEQvZWgyV2haaFlsb1ltQ2d0NmpBemc0d0NRc0FzZ2E2OEtHeXR5dEdmMnNBOVAxVnFRYWxsZEtmOHpvVVhqR1BCaGtwK3hobWEwRDdjK1VYeVZhSGl3RlpUVk5DbkcvbkZiZ2grVEJjYldFN1pqRHNUaDVpM3AwMmxIOXZWTmw3YkJsZllXM09qbE0vRG9iUnBSb1lncGYwZW9TUWFIZFo4NjN3N29RNEF1d1RBUzdXK0tNeFRVUGdvbEpyVk8rcjR5bjMwTEd6bkhXZ2k3S2xrOFU1aStWU2I3ZjNJbXlCZERXdVVOM2xMTkdQNVJaRWdQOGdQaWRZNS9GOEtXRTNHdXBDMGhmcnU2akdwYWUvUnpWVEdnTVo0a3pldmpjaTBLTFNXcFMrUGN2OHZhaU8rVFNaVlI2MU5ralkzellxRFZGemdhN0pXSXVOYytuQjFlNDNlTmxPZHBaY2pHRk5aQnF2dWl3YjB4d0YwQUQ3TzZjd2xqdzZXUCtCYlFaY0QzajJvQ09sMUVNUWxwTGFHL0wxOEkyd1dPYVJtNEFheHlGUnBDQm84T3R1WXBWYTJOM1ZPMnhFa3dyR3ZlUGxmSzVTNDNtbzNMdmtBc0haQ0ViWHptQk1KdzJJSWc3QXNSb2dacHJIUkhtUGc0ZklMY1F3MjRNVm5sem81SXh4b3pQUEZyQUpWbHhmK3Rvdk5oOElMTW1SWHNteGx6cWNYblY0c2FnUFRJTmQxNXZhYUR5OFp6R0dqUUtlbmJ3ekdjUzVySCsvL2M0TVc2M2x1VFZsTWQzZktucFp5SG43M3N4TGlPQVdQYzJWdmRjSHF4SldlR0JwZ0xFSnU0TS9kVURzMEwxTERDYlVsT0E5SWt5d2R6akp5WmU2OFhTN0dsWGZRK3pVODYxQTdrcHFoMjYvWWhHSlF5RWt6ZFZJSHFnWng1WjNPcDR5NHRxZmxFbFlRUVlQM0hCaGZJTTZISDJkc2pQYzMwSGdFZGdCN3ZRM3NvNjlweFFjSWlwYVBRRFVtOUdyeGVNSjg1MFl3QnFvaSsxU1dhYzZ6VVF0NkZFU1JYN2JaUUQzNWswckFTN09SWVdaQ3I0RXUzTEZUV0QrRGNYVmlOZ0lXb1ZTTlpHOWhLQ3JLRDh1bUZSMkNpaDViRkJGUjVsSXJETDgzVWV6RW1OMisvZE9SdzBkNTVoL0EwdUJ5NGV2cUdGcGcrbE8vclhhc3EwRUdJSlBjY3NKMndWN05FNk1WdWcrQU1HditDV04zVFlSUm11emFjdkw5WkVqMGhSYktsTzlpSCtweXhlWEduVkpuZExxUzlHN3JNaGRPWi8vUEt2dGlOMUZNYnVRcGkzaTBqTUV0eTZTQ3M4Mkt5NCt6aWVoVUtpdHZNSGVEVkxYdUs0NFJmQnRrQk9TSHpYbEZKdmpLZHRwWHZqSk1yTkZiQkszN01pT3J4ZFluOEVJYlBSMitPaFhVTTFLQTlUYzlNNitud01lYnBQMCt6YmpyOGpBYVBmVVZVdW1PYzVRb1AyNEI4ZFBIUmlGbTFmRy9zekNHclNycU0vWjJ2NUtJOGVFTWNlRmpySkZhaGJNdmZvc05pSEE4SDZrYkVwMkxZL045c04zYm9hU0hRa1lvT3doUnU3R0NtejBMSkRXNVh1U2tNOTJJdEhlRjRDRmFzQlE4d0hwdEt5bEEzOFU4a3Vjc0ROQytQVHNhN2tKdXprQlR4U1FBK0ErOUNjVkpaWFNhOCtYVndsRjNnZ2YrejMybnpBdlRhdWxuTU5LQTM2TkNIdkRVd2tsOURsQi8wV2luY21wc1RyVExodnY0TExPWWZDamlMc3YxSUhZOTZMR0pGdEFhMXREWnIvbnFMcUhFWERkVDdkWlIwZnJWK2NFQVRGbGw5ck9INmp2VWZRN2VwQ29mcHdibFQ3M25HT3A5ZnRONlBEQzViU0dsSUVQc3IvWlBDRkpEWVRzMDYzOW0xSmdwV3EvVGZCMnlTdkhMdEtRc0lsUFZGQklQTC9vdWNIRWY3VHVnczg1Z1JGTi9LWnVwOEFSZzdaVmNGOE10c2dzbC9pRU9pa2ZJVnl6azhSTWkza3FZeFp2T3h1VFBtWEp5Rzl4TjAyY1RSZ3JFTkNIWXdIa1Rxcm56aUhRT1NxNC9yc2xQNFN3MW0zVVpndkhjdEJtUzBKU3poUnByRDJBdU9KYUVHNmkvbDZ0alZjK0RlcGViU1krckI0NC9PdHFzL3kzMTFYOHZuQTNzenkvNmhOVEpIK1J2d3dGRzNEUzBmRit2eWRJOVFINGlrOCtVVW9KUXQwYi9ZREUvZkoyemxRekg5aWIrdW9GbkN1VFR4Yko4RDZmQ2gzN0xORW5QSHFVVGVBSDk1THY5YWJKWDNEQTlhK0IySkRJUE1KZTJnL3B0VkhYclBjN0FrWVRZajBvdVZ6SkZPUjJlODRSVlpoMVExb0VQbStnNFk0d1VPMWV4RDRYRlhpNWo2WndOdWF0ZEFxTDZzcUZmK09pQ3NJeERnSThCeWRjeStPK0I0S202aldaYWphbmloZjNtajJ4UVk3K2ljbndGa1JWZW02QTRUbmpBbmxzczRzMGExSjRRZVYweFRCaGo1RlZCM20xOUlDdzFtSm11YjRNUEVDVkpHRTIySzFMRmZWcEFzeXExQWpMMHZ0MXFLb2JrSXlMNUx5RFZLYm1VKzl3MVRXYWJOcTh1V0xlblhVaiszYnBTcXF6a1VYOW01dk55ZWJQZHMvRXpraHdjSmNQREhrdnMyTjFGck84VUorckJmZDlldndPRGVPZFM0SElDT1ZSYU9GazdnYUVrdWk5V1JEL1dYbXZJRlBNcUIzdys0VUYrSjZBckJRUmZXMW1ZQU54c3I4SGN2a3k1WGpFTVkxZHN3MjBTclBDODVmOXE3cUhCUHA5U3p1em94RmZlaVpHVmFWOVVONG0zcm8ycU9TVDJxeHdZdlQ5M2hOUk1WMCs3YUVpejVzRXZDQnl4ZURoR1FNYkNrZVN1VTBGZ1RDZnY0Yi9US1EvQ3pya3d5VkIveHhGT2p0Y09IU1lLQXpRYVR4K1ltRDN4ek0xYWFZMzl1dWtqTHA3M25teCtOSFBjeW04MU1tL09VWlVjNEVGa1I5NVFwM290b2M2VkZleHZzcWdlQzB5V05sS1dsU3RRUDJsdHN6M3l5THdabXVNazEwMlNQcWxjTFhBWTJObXJXT1ZEL3paTEhQU3hkQXpVTmdmWmNZVmpBdlRuWElkRVNyYWdxeDdDNGRPamxhNldBOU9Tbk16V1Z1blBPZ2trbXBHeFFJRzA1NnVieFo5akVpQk0rWFhsOVpuZmQ3Vms0cDA0QWxWZXhsVEFwa3AwelRxTURlaTR3TFU1cHo0eFIvRnlwclgxNGxEUGNCSDlsMG0yYUdPMDJlR1ZzOUZFLzhneVgwdFFvcC8xbDNQZjltWGlKRWJON1NDQmFGTEVDVStiZTZ5RS9VdGY1OEl2TWowY3hjWUs4Y242Q012VWtDeHJxMHMvSDJDVjJsOEIwMlFYMWVqcWhtV0NlS3pvN1hkL0QyMVV0Tk9IMjA4UHk3T1ZuOXNKT01aeTFPa2JqeE5lMER0OXdqaDhMVzNjdzR1TllWRi96ZkMzcG14MDdMaWIvaW5pd2lHdEd6MEJDd0dRb1RXcmM0NXdJcnFva2hiNFJXaDNTcHQ5aHpDUVd4WHk1MGQyTWxXU3UrMHUwbWtDeCt6ZXZML1pOaTZ6SGVWdTVKRHQ2Q0RHaUtOa2dCcDN3empTYkJ1a21sazgwb3IzbXNmMlBNZlF1MzhtTU9mdGlwY1hqb3B3Wlp5UVl4RVMrL2pCaTN1R0lpQzdId3B1bWVmZW9Eb2dic0xTbXNSYkg0ckxZYWl3VkJvczVUNUxuSUNpK3prdkFXeWg0UEVKTnM3RG02UXlqWDNXOEtYNUVNajFNNVlqbjRrSzl3Nmk5Y0phOEpGQ3BuL1doM0gxQ2U1RDlhdXhEaVF6eXlxa1UzNHhhZXRxMThSNEp4TWJ2UEFpQ09jS0wxNktlSzBvUTd6ak1VaWRZWDFqU2srWEg1SVFFMFNoRzdoZ0wwS0pYSmlyQ0pNbVJtZE95MDJwaUQvUkY4aWlhRkcwcW91b0RJQkRCcSttSWxTV3hSaHFBZjNHcUQweS9jU3lGMys0ZDd6UzFlT3poRkg0SGpIY3VmNzVrUGtCYURXNWZUWUpvMjVkZ1N3NWI1RU9UQkZ3eURYS3kwMnlPS1U0U1BWSVoyOFk4YmFZeWFBOENHeFlTT3AzMUNRZGJTUVAyQldaSkZ0akRkZWcyKzRVSzNUaFRnVXJqYmN2SVRyZjdlSVQ3RDFjNnpuVDFuK3l5dHl6L3JPcVdhcndQOXpvL3lOVTRlQ0NLZWMxMmNGZlVYNlRvZDJaREU5WDBodzVPcDZoTHArTkR1SktQbGVxVlR3N09ML1ovT0N4Y2QvRmxFUDk0TXk2cHY4elNaWnBhQ242OHk0cEU4WlY3UWVhVyt4YWMweU4vOTJOOXFmajNOTzJDYVE0K3Z4V1hhM0E5WjBqRVpTdjRKOWh5Z3VhYkpUdDRaTmticjVDank5aXpRckkvNUhpN3ZDbzMzVjVJNVlPSW5jTXZCWkZVZXZkV1hkVnZTaXYvOWJOa0hjWjR2UUlaWWloNkY1TktTY3RUV1hBSm1ZaGs3QUR4c21Uc2UyQjhwZkxMQ1A1clVqa2h2TmJZWFJkWlRlV3B6K3plQnl3SGR2TElzVFRJekc1bEhNcDkxN3lwTWFnSnA2UVlpMTNrbEg0emN6aHdRdmRHK1NtNmQ5RXJQd1lvK3IyV2pyMGdkNW9lc0U0VVRFYTN4SVFvemJqVFptaHljQ1JiVDBCNE5Ga2ZGZ3F2V3BucW13dUZ1QXArNXI5eWl4NzBzSFllN1JvZHlHSlU0V0R3TnBEeHJaZy85dm1sNU1uY1kvaDhBZG52OWdwamt3bWRwbkhFRU1OejFqc01SQ2tlS0RPZmtBSDdZRkpveElzajJ3UVZoU0QwY1RKWkVMaytIWGtyL1VodXdGUCtoSHZJQzJtci9Eam5hR1BYOWNVNEdsZExTQ21sdldORXpVZnFMd2lHWHFUU0MraTdYbVg2ckMyM0ZvQnllRGJjRkpzY0llN1BmVFdPQk05ZnQ5SVgyY3hmWDM4UWgxNnJJYjMzNVZlUDdhK2xKNE5UamV4cHNUdmJrVlIvdGF5aFVyekFIc3NnQ3c1cHF6Smg4WFN2dmVGUlNqV1d3QjlVQlRYZjdtaDViTkgwTEI1TlBydlZFdmc1L3J5UkxXVzhzeTRSa0JXZnR0TnhReWxzR3N4SnZzOGxPNEUwQWJZeVIwSVRidm0wODJvRjdaQmxROTNIUlZPZFRMUzRzck50Y3U4V0JySFQ4TWxhakdWRzRsaHNaK3BnbU9mWnhuNTZTQ0hjR1haOVZwd0M2Vzh5bi9HenBhY2MxdUl0NUFCWjJWclRhS0dsUWpJYWgxRGZkWFQxMmg4eUc5Y1p5OXU0MkU2TEtLTWVvWVZsd1NPL01jVkZRVkREM1JuYlFJMHBWT2l4L2Fjbld2dWVOOE9ScGI5VS85eEpoVDhmdDVqNXFyVHA4VG5nalNHZEt4aGEyU3FOK3hHZzcwdXgyTEQyNEFRMUZlck9taHRlNnU0WHk1czBYQ3gvRHoybTN1V25VL3pRWmJkR3RwS2xETnpKZ2NZODNGUWpFck1PRzN0SVFOYTJuWFJnNldZQ0NNMjA4UE5hVnh2VFhPOS9nckVJd3FNZFNSZWRZVUpzK2t5YzVhM3d0a2s5VTJwSk4vSlUvSkI5MDMzYi9qT050T3NwVVRzaVJrOXZIVjB1bVdRVzVXVUtjS0VweU5FSy9BWDhrdlpoL2JkRzh3eEphM29NRytOV2dneENEWC94TURmcHl6ekNTbUFmNkR0Ym93MjNUcFQyQS9WbG1pRVBuM0RWZmt5OFA1WlIvbldpKzdmYW4wVlRiM0RXdCtvWWdtYkFFbnpWOGVRYnduQ1VlZk5sZ24zN3dnUnkwNUZhV3FONlcwV05BMGk1d05ZdFJ4SVIvcnlOQlVaYWFhVk1hbzBmZnUyS1J3ZUxkRHdwbVJqWGVqN0lHb00vU3JQLzAyaHZuRG0zNzBNYlBCcnFBTGFoNHZBcFZCak5lN3RqYTNlWmlqeUZMUnZYeHBtcEhMM1ZOMzFUcFowa25tSy9KS1Qwc09QZEJPLzBUU1NCWHYvRG9KUlJaQ3RteHhDUk9pYzhWdlM1c3V4V3JFc2hmdEtxQ3pCSGhCZ3Z2T1Z3VWJWakt1SUR6UFozSFVHU0psaG5UbHlJSnE4WWZvTjF4ZGtrMzloaEYzcExHT2JIekh5b2FhRFpWQjdmbVpGWFFZemlHNTdFLzF3aEU5MkdKeHNCWUY1eU9mQTZXOE5ERHBHU0xmNC9IRisyN2VYcmpNMWhXd05iNllONVN6VkxBSWJFamFoNm80Yko0eW9OcXlFd3NOeml2MDgyUXJwMUkxWjFrc3lGaFhia1Q1bzYrUVNSd2xjRHRnbWxhY1JzaHhTREs5OEZvazEvbGhGbWtoaEliT29Rdk5tWFdjRVFvNzQrL2JieTFLOGp1TEFmY0JYM25yWVVCWDJ6VzA1bkRZcldTSnZCVDlkUi9vSHVBUmJqbEN5R2x3aHBPTzAxWDltczlFRGJCbUtvYStBMEN4SWhMRTAvOFBaRlBkN3NUb1kweTVwem5PSDhzUklIbTYxdU1ER0N2VDljT1F0VTBnUDAwcy91eno4QWN5eXpnOHFvSkFmcnplZGJnOVh2bzljQW5lMnFNaDFsVU5GeUNtUmNkQW1ndEF3TzhIdkFnWmZQR2pvU1IyTHB2a2MvMlVQYzh4MTRxNVdlQnI2aFRDTFhwaTdaSEZVc1NNWkM3alJ5OFdjSDQwUDFWWlNkR3RrTHdUOWtGelFyVi83WjRUZ3BQcTEyVGZqdFZEZWQ1TXo3TDhIQktwSGQ2NnFkZnRRd3hIbUMzSFJ6SUZpUjN3U3JqVXQ3ZkU3MTlHRklldHV4WXhmZFVmODhIQldvYVpVRVpLakE0M1ZQakRpTkVGM09CTTFsSVQzR21Sa0lXRHJWQ3VvM0x4Wk1FYjY1ZWtHR0N6YzdZRktpTVIrRjE5SUVSenpBRzI5THFpYWZXc2x4TzhaUGtYWjVxWXZqL0krNCtNQnFma3RhSWYrRVczUDZhNEI1NTNSVlNKM29Gbmd0eE9UNEo2Z0pIUWh3QWJRYld0QThnR1BiTUtObW5laEtwUCt1RkMvdERHTGpxRE12UGZOZHg4TDBhaGNFYTVoWmt5eGhSSldTRVNTNWRxZW4zdGRVeFNZUnNPRVY0WlZOc2VoRlIwbzVBTkx0RHM3SnpRc3RrL0dYazAwaXE4MWNsbFZ0YTI5MU9PVkRkSkRvUUQwNEZmTmdhcWhXMjQxTURUVGRFOG4zZjBhMjlHVENKSEV4eGtyclMzNzEzRlA4ZmtTaGI4UnZuT253d0tqSElPNXdObnMwUkQvNEJwQW9nNTU3YVBnUnc5bng3Tm9NK2lHdCs0MXNJRS9tRzNsL1pqZjF5V25aMGYzQ2ZuWElkZ2Z0UzJiNkcwS2t3MEJNQ2lMdElIUHkwVWFOTGtDZXVmYW14bmNXVXZCR2lvVk13bGFKdlVCVHdNc3hYaktXTFFjSnIrSGJEVm45WUxmY29HSnFhWVJBS1FLMHB6U2phYmkxSXN4ekcyQWtza25Vc0tvK2JPaFh5TFZuTWt1TjBqSXNpdDRFN2M0Nnp1WnJBVFM3QlFFOGg5d1hrNnY5ak81TUwwcFN6djYvMU1GUVFpQ1I5cTFxVU96OFo0UXRQQjR3MVhCOS9mRDZvellnNXhUU0RnNTYwMmk0V1NpYlNWenlHcnhZbjlNN1RlekVWM0xwcnplYllCQm5Lc2lHZ1J2OGpZUmMwVWRKU0pTNWRBOE43ajFoOTlDTXRZczF1aVFRNEkvME44OVV0SGk3Mjc5MGk1Tk9ML0dGM0JKbXlSdGc3cXQyVm1pTGpRMDNDZ2xaVW1UeHl1V2FvMmZZZ3RhM0NJM2FTajU4eFBwRFVWOGNta3lJNFZPWTUxQ3BmODdsaTFDYlhlT2RKZ2FEbFN0UGtXR3NIN2t6Y2dZL0pzNHZBV0doajRtYVgrQVdma3Jldk91MU12bnJ5a25OQXErd1R5VkNWRDhTSFNiTXh5a2g5UWtpdUNxY25sTlltS0F6YUYxcUR6bmxHR0tpbGswaVR1bjNuR2hhRUxhbVVEWGRDNW5kTmRSRy96U0dSQTZuY21Zd0JSZTErb0FyZ3ZtUWc5Umpxc2hvMFYwSnllRjZ5eFNQNkdhdDc4a3JFeTY5RHB1WFAvRzZKYVFXUnFyMDJXbjdKaThWbmcxRmtBNzZUNUNuam94REVFMFZIWWEzU0NROXRyUU1WdkV0UFNib0wwc1BwRjI2bmZaRVhLS0lmOHQ1NXFhdFZFTXNjTzRlT1RzNTQ1U012dFlRZENYN3hJK0l4UnlLTUtTa2FGTkFCQURNWEs4eXorUi9sT0owU3h0em13QmtUako0aS8yU0FLUjRLT3Z4aUpkUVRRNzR5ODh3ZFNhWDlaSk1zL3VuSnc2SHg5SlVKNzllNVZ2TS9EeS82Vkh0a2FKaXU5djVweUpCLzFMZ1VKbit1Y241WTVXN21wQ3RneFRXNzBGWWQ4NHdJSTREYUdOZXRiQ1dlOFF1dWFDcit4bXliek13bVBmaHVJL2hpb1RiQUNULzBkUktuSkhIMzJGVjUvellsNVpOd1o3bnlCTGp6dWQvcTlHVkUrbWtkdXovajhiWGdXdjlBL2tuTHZPaS9UQlRjUE9RYUEzNER0dkJhbDY5RXZrVmRaWGw3a2Jac2Rlbjd6YVQ4ZEM2UE9PWkdDamRnWkVhZWNoM2ZDeEFFU3RnSXpuS0tOTmlwOFZGYVFsdE00N0RIaC9PK2RPYVdrK21ZOGo1WGZvWFA5bkxldCtIVnNsRE90dGRhc001MVFEN2hldkRJTU5UWU1uaC83RjZrbGpEeW96d2dLajl3OFNvSVlyRkFVS2VnRE9EcXFndUllc1lIVWN0S2NKOXlyeEQrbFF1bWJHdER1OUZmSmQzenhydGJIaVJmUnNtdGZidytjcHg4aXZQM2JUekt0Q3o1NzJ6KzM5d09aMjJGaG5EcStQeWsyZEdWeHlnYm05WGxrbnZNeExGOUVmWW1SVDNIa29Zc055VWtPZHk1RlphUjF4d3BDZXkvODIwR2p5UENhRHBoV0pGM0FxVmF5czJzQXFUam1zKzBqbjFQdnZ6c2pmODQ4UTlVVnViclJTMkVocWpTdE9Id0FyczNKWHY1OUpwVGhlT0NlM2xQVytVWHlUdTdIS2UzRWdHakQ1bllwME14QW9zdEFlbjZtcXNRZGptWDJxbVJmdEJVWVpFTlFCS2dmeDlBNXlsQS8vQlhWTzRteWhxdnFzSVhtRmpteC9kUDQyL09pSkppTXUxUVhQWDg2UlA5MGtuaFhibHpkNExobUw3T1doNGRyN0NvTDJ2TlVrWUFEUjFrNGFlelcvYnJOdld2L0FFNmxsMG5mSGJ3ZVFLTnBLMXpzNmpURFg2VUlDcmVSRktRdkxRenNjZ3pQWThnLzRvcVY3akFLTituOUpsT3BhM0NtSFZYeFF4SVpucDNWK1poRERWWUtxM2tTNzNxTDBha3EyZ3V1UkFoWHJhTUgrd0ZaNVovRjNmamxuMEJSbFBxTmpjMEhSblpXbnptd1R2bmZJOTg0K3F2STFQd1RCYU45WlVoZlRpd0hjOEZQRU1uSmlkUTBaQnhSTEhzRTNGejVjZnRxeGhCRDBlN1pEZjdRVElBQjdRMzRmMnRxSUliNFZ1ZFpNZnJ0eVNMZ0ErUkdudG5DUE9zQ01zZTZHRWU3SG14TEZ1cit4V09vWEUvc212TTRaSFBqT2NlbDVHcnRRSFhDNE4waXJraEFNbDV6NjFwdFMyazJITEUveWE4ZTg2MFhFcUxFREhxb0RhRG9ZdzEvYkhSbmsvNklwL1ZVV21McWwwejFheW1LU1pVSWQvMG5tMTlYVW5PbkU3dDNqd0tScFF4UXpvZ0Q4elZLSkl5MXhRNERsdStUdTJRQjVzMitZZDRWaWs5dk5OQU54bHBMMDFLNGU4WUNvZHZCREpXWndpQ0lEY3FGNE15NHUwYzlmK0MwYXhwODZ4WmQva3NmeGxlallCWnltMXIrZkdKWllBbERkSVhJM0R0V3UzYnphTlpzNTNSajBCeHAwOFVIQnNsdlFveWgyS1VRdXFNbktrK1dqZGhpOGphd0FxZXVJQTdPQ3FFdFBOYWp1YmpqMU5SalUrSE5NZGQ2bzRyNHZZTEZHdFU2bG5pK3dEbzQvQ0dsVFhtRXJTbmxrMFVWWk9ocDNUczJnVGdZVEJuSk0veFZjV3hRckt3Syt5V3NtWU5FZjVIQSs1Q3NGWUV5MXhmZlN1UmpMeE1ld042V25kNDd1NkhGR1dLajRhZVZhVlQrSGhMOEh3b3Z3MXlNY0RkejVOelYzb0RWb3F1VkVrOU9HaWhWdFM3R2ZlbThkZDVxRmhXYkdJZ0NzSmJoaW1MeC9GQ0ZYVWIrMm1jQnMxaG5NWWMwbmxWbWRtVFJZa292R3Q5Q3VKM0NXMzNTUmMxckliZFRRRjUzT0RKZGxwQU1KM3hKWWFJZUpqWW55clJQYUhpaGFiL2xxUjlHYllScVpCVUJlSGFnTEdWM1FXR1Q4TXdMY2hyZWJqODNrclgwUldlT0dEYnBIT0NnMmkwZjR5dVEvc25hYkhuci9FSk9DUGF6M0JMOEFxVnpVYVNGSGd0TU04VFVmcm1zZ2QydzA4aFFES3czeXlFMktsY2JqczkwR2V0b3lhTkJCak1ZR01XWTZQRUxWNTI5eUFkQmVqTEJjZ1RRTC9SZmdvSVh2QitZL3k2bER5Zy9oWWYrSFFEL1lzak5aS2twT3VEcFIvWUY3MHBYenNGU0lpQkg1QjdpdFZKOEMxT3hSUDhWNmh1NkZpNERPNGx6U2p5QUpRVFNzS1dPSkxoYzVib3c5a3FLRU5XM1BwQU96R2oxQytBanRLUmRTai95ZDNZdW5kT3FZOERPeWhkdGIyWFZQMUQ5U0R4dDNIa0pqRUZyRnJLeFIySU5melc2TjY0UkV5SEl6S2Jrd0RBSHJrUjhIeXRlV1NCS05odTRtbWdyeldYVE1JSWZ4TmZjREl1ajU2cmdnSE01V01aaUNobGc3WHU3RG53T3FUb3FMT3FiWm1FOGxwa0JNc3htNlA1eG9UeWhiRHp0eVR5N1pYT0RsbTRiQkhkUmhQYWZ4dVRLU3BycG9RS0FLMFFScjBvZ0lPdDdkZU13K0ppT2lheE1wbTk3MjVOdExQYzBsSzFKYkxPcmNCT1FBZEtrdUNMbGJlZDBOUXpPUE1ET1Bra1Jhb3FTYU1GUG5jSXBMa3hiQUQ3Ky9DR3J3KzNpTWM1T1MrcGlPUzZqam5ydVNkRHFxNTNXWVF2dE1ZUEh6NFg1QitFM1poVzl1N0hDdzMwb3NzZkhSekNYYkxKUzZJSzVCaXFMVjE0UkRVckdKVEsrVGV0azFrcDdCcWoybXdXNk5QUlJQL2V2cElRMFgwUUZaNzB6akU4VXVoclFNUmtSdllVRUdIZ1FlZ2VnYlFsRFNxUWxyR1Jqclk0cEJqRTFzZDBmSk1zczBBYkhFb1ovckNOU3Vldm9WMWFoU1RJcUdwS0d1YlMvM0N4RXVQTUhYelVBY2s2K2ZTUXVlZkUrdGh0OWpnQ2pxc0JTVmRlQ0pOMmdJVnEwOXZjOEthWitDSWVkMkx0WXMwN2d2TytVOFgvSlIwNU4rTUFvYk5tTHZBOStwcnZ6RVZtSmhUQTRhYW1rSGRYYTIzT3loQmVZeXFrc21ZK1NjUms3Q1hjVFg3bS9leXZZZlZrNHErVVVKT1gvZ1ppWEwrbittOW5jdzFPY21WVEVuUEhMTDUvOG9CeHlvd2Nod0NRdGE4dllwVk1YWkVtcGRiTmZSYUd4ckFjMGZwK0RyUGlNTVFhU0JTUGZJL1JDeFh2aXkzdE84MG9xOFBNbjVWMk1ob3ppTmhsdHZjZ1ZHb09qaGtXd05LczV1bXAyaUFhTjM2RUh0bVJIdVVPZUIxd24rekx5c0tEeDNEZTdYMHlINHdqaHJzRkFFSXAxZHY5TzJScnhDZHllSktpRGlhZDE3ZEsyMWFKdFJFRktVQktDSjFHQk02RjdWWHFIZ3hiTFJLR1RuRDlTWk1HRkI2OWxJVnJWdjdQT2lZd2NpRUk1WXQ3blNWMHBxTkFVR3RCYUdLb2JiYUtMYlJZWDQ1c3hHRlNRY2NIbDZibGV6MXFUSGhBaklyUjRiMjd2dTM0ZHRTcTZSRzNLL2xyRlJnOHhJMGM5d3c0Vk1uZjI0UVRoeFpDU2VkWkR4NzlXcXY4SnVLQ3lWNDlCb0Q5YU9pSktCWmFHTWdFQTFFUm16QlBOdUZsZGZjRkRKNitib0VaQ2JkWGhGNWxQWVZwOHltOThzZ1NleTBFbjRraXRZelZibXBndVVlRWo2VVd2U2ZkdDBLWXlyV0NucWcvVEI0aEV2LzluOElLRk9Ibm5CQnJLdEo2bUVnYWdDWWVpT0I4eGk1czI2VEl3YnU5dVRwQ1BmNXVqYTRKRllaUGdJODJUeTVkcWxiakZxR0FlSjd3VFVWbTRpTkF1ZWVzRFh3WCtiUlR1UmxSV2ZJY2grVGNhWEdkK1RtblJuZklQUEMxUzNhRVQyenVTUXlHY2Z5Q29PRlJCcVhJRHhBdnJxcVJUbWQ4MitTRFFBcWIzTGlISVEvdWVjV3BTTjczVWRGYkhkVjNqakQ2N2VXc2pjQW9xNzBIeDBsSmFFMFVRNitSQ3hTOHFQUkZReWszbWgrWUFEdG5tK3NBSW4ydnJ5SkZWenpYOEJyRVlLUHp5SElYSHlGZlcwUUdPUGJiVWJEMEFGYTIvSURTYlBvR2EzY3p4czdQeEN5STBWU3lwdFdQcTlFWXJwTmhxbWQxWXJtSVBFR1BXZnByd0hXSUFFRWtNVTljY1hhZkJ5SENRQjNDNkRrTVh6bTNJZFE0SVNrK3NLVzNLbktSV3E2UFNoY210UWxUTVh5TmRFQ0duTlNTWktYcXZwa1lKb2dQZDl3NVZVcVdsb0g5YXM2bG0xZDhBSlYwaWtacHlFK2ZFaTFsOUwrWjM3R0R3YkxMY0lGMnlvZ29aU2Y3Y2EyUTRSekdhTXlOZlQ2Sm5lSGNKcWFGVFpKeHMxdUludGVsSEwrOG1VY0hCZ0NUek5pWFZ4eDRWRWdvdC9mZmNwR25WdzVCWFpvc1RDOHFweDB3UEh2S0RVWnRGbU9OM294cEhHOUhTbTVXdWFudWxveExLVGVKTm9mZDdnM0hlb0VTZEZINWtDdWszWS9PYm1tYW5kdjZYUGFGeGUvSEJtUXdSbkZqL0gvbnU0SVJvRk91TjltMWQ0alVhcWVpWXpYdVgvZXlRWnl1Vm9yd0ptSUVzcitXN083VVdhcUJCYUd3dU81ZFRXQ2N6NEtzcDk4dEQ5NzVFbS9BOUdDNXRWYWl4UHBGTmNLMWVKdEk3RzVkby96U1A1TCt0aFRuN1ZTMjFxd2lSK3Ird2d4UzVnVWpnbi9jL0hXRURaQVI0Sm42ZXhDaHlySUxIcTBHVStiOTBZSHBUOFBmclMwMTJPMVJwU3JqTHNBVTN6Y0VWeFhwWTJNLzJwTmYrOUhWckZTMnNFdkxrNlIyMFJhR0VXMzE4REtLNmtIdlFzcjJwV1B4eXFhbUNxUkdTcVVJSlpRSUtxSFhtT3pNdHdLL3FicmliRjIrcTZWZzZOaGhGTDMrSmt6aDFITFhmWWlraW5qM0pwUURRWnVncGIyREl4OFRRYXlGZWxQT2VOcDhyajREUU5ISmxkUEhmQVhGRDRNeE0yOVlPT1F1L3hhRFR4ZnRnYXQwSkpCMUpDcmJXblBCeWhvcFVoTW5SYmRpU0orcVRSVGVsY3E2MEpaQTVaUFFybHdBcGZKbCtCbFdPNHZiR2xwSzRQOFRXVzV1TkFNVjVTM0c5K3gwWWNMNXpvNU9rNG1NYXpCWjFVc1U5bFkyMVZ3Q2dDQlNjOVk1SnEvV0gyZnZvTTJuNW5mTExmcHFIb3ZLL3dTWlAyaEpNdktuOXdhU0xVaHp5cVR4K3lUNmlMeDVGUFlmVlBLeFM0S3g3T090L2lxYW1vTGpHS3QzWGcxbDNrYVRqR0lCV0xlVHdFZzkzbDJiWFlmbUwyYlFNNnVPY0ViU29XUGZqMHpyZktwN2dHRzVWdXZ5MS93Q1U5WG04ME4vZmVMcTFIbmNTWGY3Y3N6UTlXanFUL1haKzNscllYYXg3TlVERnNlNHMwYk5rK0ROZWoxcUtDNVB4YURKYVdzdU1TR2crSnkzSCtsbjFCNC9SQXJyazROMzBmVnhKY1QrWDdISWd5QlZtTWdsaTlHOG0yN01nMnE5NFo2Wjh2TWxwMFEvcjBJVXAxRkNxVFA1M2dkOGx1SHUwZTduRWJqck1HR0Y3c3JYZUxNUmNlRjRqVGdJai9OSFZ4aGZocFM0V0NGZjFhZGc1WXZ2ZWttTXlwc0xPUld1UktXNU9VMVRYUWJjZVFvN1MxeGZZV2NLa2J3b1ZtZ3Z5L2dEN1JoUktDUEpZU29DVkpSZzVFTUl5ZXFDb0xhQzd2RStSNE1teHVXUSszaTB2MlBQaWJ5K0RMdld5aktJL3lZM25GRjFpVHBCVCtNazVvd1U1MVdBcERXZEl4MVN4T0twTTcyd1VhZWwyd1VHUExYZkQ5SWtLZGhvMDNwL1NVVllRY3FhamF6TUFpZWxEMVlTcGtOQWNheFN5ai92bU1PS2t2ZUxKQldEZUxDNFllNStYNUZRRVRLTzM3TWZuZ0d5em85ZE5aY2VaVElpOElmOHBrSjJ1SWxBTWdJYTVieGorT2JwL0RHcVB4b1doZzdKOERuSmI5M09oTjZsVm5xbHAvbEtiMEpqVWE1U2xIRmFQd0pqUXhLRTd6SXJZSWlqZ1loY0N5ZXJDRW1ORk16SmZQRnpDa3FWR1k3SkdzbzdnV1JlOTk5TGpDTFF0Wnhyd0F6SXVqOTNoTkJKeStKNDNrNW1rdnJ1MnVrL3Fpd1BMbm0zQWZBaXBpMUV4K25HQXFIS3JNWmYrWG9XenVSTjF6NzRSSFB1dDNnSDNFVkpnVGNUOE9OQUU3REZWcTd1eWN4dHl2Sk1Sc2RwWG5CcVQzYnlRZUpKSmQydTljM1BueThBekJOUWlCZGVsNlBXc0VMVWNGUElsS0k1ZmV5eHQ2VEdBUGZXVDBCZWZZMHA0cXdPZ0lzNkh2d1dyOUJqUFdDUnU4REtWcnNtNkp4MDhrQy9qY3pGcmhXQmVOWHMxVFNiVWxVU2J0WW5KSStmSzF3T0FtRW1IZEFOWVkxL2ZSM1dDSjh3cHFUdkdaMytuTUI3azB1bDlwODBnL1BwL1kyQ2VBNHk4OEhGeE54NmFNYlRRNmdQWmNrWUZSMWZlaVhhZGhMMFBUbzV5NG4vYmxGWlhPRW1OakZIdzlBV2JVaDdkc3c3WEJyNnlhbmg2Y1BGaVFUSnVCVTYxejlLV0FoVnpNYjhsYVlqRXA4eHgra2RTaVlTL2ZYc2E3N0l2ZWJRWTRPNW44UDl0cklyT3MxVzRXc2xoeGd3S0hLckJKNlM3V3pzb0VvRFhOVkR3azRWVW1GNkpXMjNxYjNyTnVpTVhqNVVITlJ4RWc0NFpjUElUTW45QmRhNTh5MnBoYWE1U2M4enBQd3M0WGtSZ0xRL2tRcDR6TnU2VmdITmtMTWpubjBEL1gvanVxZTlNMER5dUxXNVNLd2hBaGZkMWFFNnNUcDJydWJOT203akdsSC94dFFldnJrRDNqRWhiU01SS3FtMUxsNnVkbklJUHZoalFHRXpRRElFRVJiR210czhQWDV6SDcyN1dueVMyOStXYy8ydnloTGZBZVJrMzFLb2hEMTJHSU1XNmd4NnN3VHJ4TVZuMUNDaWI3WEVHdmpWUTRGRC9Eb1YzSlhFekNLeW83M2NLWEs3WlRJOHlXais5QmlFQitIL3dZekdUQmxNK2Z2ZDI3VTlpZ3l1ODZ1WmlNcWFwZTc2THNLZnhmb2F1aXFCU04xTXg1WlZ1WDMwYUZUTFhCRTN4T05sVk1GUFgrL0FlbWlKaUxkOUVrM1E5c3ZMUkQvV0pOMEJxOUJaMVFSTlM0aktyemQ0b0taaHJ2WTliM2IyajlFMmF2Q3pGUFRuU0tDN0pKeVFoc1hydEVSRWkrTjBmUkFNNUVDMUZMc2p0VmhyQzd1SGt3WWFjRGFXNWRPSTVwNzR5eTMxTm44L2Q5amFWVUY3OThVS1p3NGcreGVwazBMeGFPS2lMYXo1R21IRU1BQjVkb3JHQUdJMmdMS3REV0ZHbEVyUzdQTkVnMERTcldUT2JGSXYzdEd3aFVCQlg1cmZZd0tZa3pFWUtScGh1NXZSOVRzVXg0a1RmL3p4dEtzR2M2OEIvUUJha1dSZFlKN0FXckhjSU9nMVJ0WkYraGdtRE5wYnpPVzVHK2djZlRneFdEbGkxZWtzblVmV3R3OUh1TTFodm1ObGIxSGtrN3FncFFWdHVWcGxhYWNabDZmQm9nZDB6SHRNZW5vRSt6WDlQN2k0Q0hrdDBiT1VSSFZ0anI1T3ZEUDhSU1BEZlpEZkRvQzdXbUc1dWtqdy9RYURFaUJYZFJ3Vit1NkJQOWdnNUt0b0RDTThnbXhxeC8xb1J2YitmZ2I5d1liVTRKOGt4cWlZQlY3NmdMZ0RXYzFaaG9NQTNkOHdIZ0pTN3hWM3FuS3BTSHhRY3E3Yis0QlpsbUpHWUpRcXRUaVlHbnV0TjlrZUI2WXFoZEhSaFZBUzhiRXdUSE5OUDlCSUVxc1RpaUtJWGlSRDRYczgyV1I1SWN3WlR0czdGKzJPcmJLNW5HdUlOUENGTlZUSGFkK1V6dlJtR2xWN3VPNitCWHIwYlZkSDFLLzJNanN2Vnl5M09qMmtRTmxSNlRHVnd1RExXM3ZDOHAyMno3MmQ1MW1MYVBOcEJsQ0xEU3kxenhXT1RybURBelRCV3JFNHFkeGZoNUQvUXlRZlJKTUpmeTU0UVFrSmlwSUN5NmhEa0llUU1xODcyNXY3UWtvR2xIcGxwMTAxOTdzZmc0NkVVa0dCYzNsMTNIOTd2aURKSHRudUQ5QmlKUUp4dG9XZDRmT1ZoSVpnRkZJWDQ0TUt5d3VHNzVGMjNqU0x4N3VkVVh3QWE4dHczeFFMWnN4b2UyZWM3N3d1VlFSa1cvU041U1Y0aFlwaUdQc1hMZWRCQjl3ZGhidVFCUEJhZC92aXRGd3A5V2lNOU1iQmZ2YkpCRFhYdEpkR2d2TGRQZHAxK1ZFSG1mK0o2THk5Q1JsNFZLdzZjOHF5YkhVZURRWVJnV2R2ZjJkRDQ5MHVTdmM5REZHaWVMYUFDbUhMUmY3aTR4NjlwSzVXZmhVV0Vic2s1U1VmSS9zL01vanE5TVVYVEZSU2M2Z25rVE9TY2tVdnhwR0dZNDNiT1lzcjBjRHlaMUQ0N1pjVUM2dkVXYmp5dkxLcldIZG83c0MzejBPeXdtTjhuRzIraHc2eDJlU01IV2pIZnRneWg1YzJOSUcraGROditBWm93ZHV3MDgrdEJ1TkxJeHFveUtHODk2bURqU0xpVnUrRnlPd3RZMHduU3Z2djAxUWxmR1E4b2QzaXpCQWFXelM0azA5SzZmKzU5NURaMnVCdFFkbnZhVERsY3BSVkMrclZkbkhzN2N0MU9DMGFhVFdmNGgrYmtuTWprTjEraFJ3eitEVWkxVTcyOGVMM2NSQ2NXYnBMMm0wWCtkLzVDTEJTN3hxRVYrbHQvZGhrcTR1VjF2Vll2NEpKbjIyWlJyM3FINzQ4QnFBd2pyRHVWQ1VqMnQwdmpCMlFZN2VEd2ZKaXk5VStUNXQ3aGxpc3NYTVRHZHNoZE5QWTJCOVdJbWlXZUkzVDBFbm1zZWdZVCtTNDIxYnRIM045amw1ZzlGZmduNzVuWUt0cHFXRTZtVTB5ZXBldHc0S01hTFR2TWtrci9lZkRueHFSWnU3UXg2Qmc5c2tKWi9tU1Y5MEVDS1ROc3JxSDlvVVR3UE5Ra3A4TXpTdUhJYklCNWRkTWpBcldDcVZNTTJsN2hkbnlLdjhLZTF0WlJON0ErNGdGNW56TUpDVll5dmp2SXJUNTJZQnEwaUxUZUI1aG0rVEgvUEkzdmcyNHNTUGc1WVVqZFhzNUwrVGROdmRLR1NDZlp1YXVST0daaWV6ajl0RDVCNVArREhGK3lRL1dhTG9KaXRiY0JKRzFBdDg3dzBSQnpEL3ltSVpweVI5ZFphRjFLRWhldjVSMFRUY3B0dkpEYk9GRC9LK1VVU1hUUFhyWEpPVU0zRFR2WU1EOERIdHlKVFhaVW4yRndRd2gzNzV5bDZiOGlXTjlXTWFHVlFiMXhzTlJmWExhTGRPa2JxY1piWHpGTlZsZkV5a3FROHFyYjkyQ2ZQUkI2aTdRYkZhZ1RGVnltcC9CZU1tR3ZnNEhnQXhVNTVlUlN0U0QzdUF1QkNUUDB2V1d5ZGJGb2ROT2NqeVBoVlVxVm4yTTZFU3ViN0xaSVpoU3JNTlJmcjBQWDZHRzFtUUpTK3crM0JqL05QaE5FNC8ybjl4a2JZalJVREdDVXhxbnppYWtsQTYzaVVXSzU5TSsyOWJUL1FOOCt4T0xMeWh3ZEJrOURQSmFxTHd0bmFVUlNpVU1lTW0rUjZ5OTZWN0J6cjBpNEV0cDNHSVMzaURBd3VIdjFNeXpuem1kclYzZ1YrRGhqQnhMMlFrSEc3WjJiWmJIOXpWbm9FTFZEQVdHYlorNmsrZFhsR054eGYvR2ZQOXlYOXZzNDUrZjdRZjUrMnMrZjdJVjlYc2FmbisxeGZKMk54K1QwTi9rN3JZL0oyanZoMjNoL1Y3elA2dmJ4L3l2ZXAvVDZGNzUvdHhmcTl2UytiN3Q4K1R2WC9xNmhMNS9UWDN6OTF2MVVkd2I1L3JjL3E5UjNmbitzbmZKMW5ueWZVQ3ZuK292Zko5WDIrVHFKbjVPNS8wbmFMUHg5T3Z5V2dNZHBZdzFhUUtIaXk1UkxtL0U2NVZvV1A5WXFUck8ycHFqdUNHVFVpMVNkRmF3a3V3Y3hScnZLS2Z1eENCVFFQZlc3WGx5ckRWZmcrdkhRSTlOVWdHbklDZzY1RVhERmJJUDZBNlNLY3pqbjFvT1lZVEJ2WGNWam1UUTM5enJoV3pZR0pBY3cyemRNT1MwQytLWkdhNm90bVlmeFM3YjJneDAyeVk0N0pJeHUyenk1SVdkMWIvSHpRR3l1OVhURXIreDNLZUJMTklqQ2F3bXAramNCQmlWQ3RieEYyZVh4MytlNWlORWtpa21nTmFJb1Y0NVZKcDRCR3JVWGhUcUY3UGlJU1BnajhUOHo2QXhMMFJzbm1ja21RTnV1TW5CcHJoc2Fuc0JtcHhYVlVlRG5jeXBHZWQwY2Jvd2ZkMWVjVHo1eTFOSGJ5Z1FHNE81WmtpWUN4WUZ4OWh1cDdWZUFFMUJ3anhSN1ZxaDZxSE9PaW84aVhZOTI3S3g2TzMrMHFUbVdkUklIVERVdTl5T3NxaGVudWo1ZUJ4VnBHamJVOUhsRE9ldE1wT3VCZjkxcE5yUzBDM3pvdUY1TmNvL3pRSUZ3djBjMWloeGFibVM2alE3cnlIaFVDOFNmaTdpb2dWcW1QNnM0b1d6dG03a3BId0Vkdlo0YVlvb1l4WkVKYlg4dUFYaDhsano1eVdobkt4V1J5eXM4UE8rUzFmNVRQLzhuTU5HZlY2eFE0YzdSQXI5eHU3QWppcEc1NnlCVjcxdGRFRHozWmlYVlRUb0RRbGNSaCtSNVZzaW5HMUdvMk5CdnRTTVpPZFBNZEhrOVlkdGtxd0hNeTlGektTZWwva0t1aGFNQllSa2s0SEx6bzlMVERMSy9zc0J5dmxtMTZRQk9QWUZWcVhIcFJvQWdDR3FrQURtZHNuYmJ1SUpEbDA1d2RYQ3V3WmZNMmVpam5FL1B3cnFqZ0lFT2ZseGovZk5rYnR4ZEN1enJWNXhldzV1dWFsN2h3dmtxZFQ5U21GUmk4ZjZVQmY4aEVOMStmK1hFbkRkTFBkekFHeEN4NEorVytRY1g2dWhIcmtEWVpzcloySGlnRHZvSWRtcXl5clJUV2pEYTkvSFdsZ1oyT0Iycnp6ZFpUNUtnMGVxa0VlM2ZIRmFVa1VQdm1YcDJCSFZXNm9wZzJjWFpRei9jaG9YNzRVYUFNTkJQbGxrazQ0Y01uRWwzRi84ejlQQWVXTGhFSW14bDNJRHRwWWxiRURXcldST29wb0JnajRaU0lqa2JXaHVCbHdUbGlIRVhzVzROOFg0ZHl4cHBpekNvWUJ5Q2UyQ1BNV1pCSmREYUl0SEg3M3VlMldSY3NsakpmZzJQeU1US3FJUXgzMHR2TTZ3U1JkU2habTQ1SUorcTRUQVl0TDQxdXMxNzIxaXBpUzRuc3hNY3hFVHlZdmFyUkFhL3NheDVXUnBUaW9rcE93WTNoQ3lHODBLOUZmVnExenVhWWJzOWVtRXVlRFprZHZqcnlKMVQ0NVN1UHJBOUlGZG4vQms1WGVmQ0xIQS9Vc09ZOVl5N3g1bEFNUGttd2JOYjFsRm1RbzMyQ1ZmZll6RXZFdWl0VTlqTm8rcTRNQjV0RStCOFVMNXpaVUJ3SWJMV3kxMG45d2xzTHp2clh3QXRROFVEV0F0NUNzNW5nY2xxZ0crV3AyR3VrNTRJOEx4TFMzUWxUWUYwMXQ1elZZc29ZUVJ6N093M1IwRUNJZjJEVTdBL3VnWE40KzFUSXdJektySnZZb0JobjhvZk9oeEVmOTBlZ2taM2wxcGNvQWFPQmU4RnE0dlQxa1MvOU9JUnhqSWhjRWpWeGZqRzlGdGpKQmNDUEw3TXhBTitQN0RCd3l5QnZ0MmN3Tkx5YS9uZzQ2SmVSOFhIWE1EUUdENWJHWE9GWkhBTnBVdXpQcGtia2tpd1ZrUEtBcmluL0k3VWg2M2xxZmE5U21VY256SFVtZ1p1ZW11eUZHMTFaUFNHcDJIMDhrdHJ3RldCTmo5UENONWI1alAvZmFTSCtHYzNXOVNVY1R2WDBlcXp6Ym10RUNEWmVPTjRpZXVtUS9uSTcreHdpa0hwZ3FLWW5BeU0vZ3o2YzlPT1B1RDIrNGk5SmVIdTdWWTlBcU9hSjlCMVlOVXZrZCtHWkNLS2I0WmN6NE1SYkh2QmJqR0d5YXRUVlRiNjBVNGlEYStDZUlLaXArUGtOa3ExMGc2bGRGbGt1dWR2TVFQMUo1c1JlVlQwK1dqMUVlWEFtVTJMOVZkbytIV25kNy9IQzB1RHZyQjdsSitra1NPcHMzb1pLaVl3T2ZxRVZ3WFFmTWVYWlVFZ0pGMWhLQXZmRkZMSEtSYnBlbG1nVnZsNjg4Y0dXYTBmR3RWOHdCUE5MdlJsTG5RZjl3WWZKQ1hTaS90ZmpIODJQY21yRU1FbWRZRUR0c3ZKcGx0cmJXSW9uaVExNXVpMlRzWng0cFUvdnV3bFliUm5sTUdlM0hHYTdsVlk0UnIzTlo1cm5YaDdIWC8wc0VWb25CbFl4ejlQNlBuZzdIdVR4TnR4dkUzM1ZaRHRPN1JnaEQyWHIrNUdSSDBZL3FoOXhEZ2V2d2hPU2tSZmQvNjI1NTN1YXB1RDIyaFdZUVdyOGZQbWloMWFWc0ZVTUZ4aU9idHhRbytnT3UvejdHcy82VlRHdW9VVGhkQ09HVVhIUmF3M1BPMGhnd3RUUitGSWVTSmNXbFVvSXN2MUtFb25JZE5nMUk0WGtxNUQ2TFRBeTkwamtrZ0NUUldMMVRzSVFjV2lJSEhJTS9oSFlqV2k3VnR1Q3ozV0xuV0ZaTFJrUSt0WUZqOXhsUmFlaEZmSCtxN1F0aDQ3UjkyT2hoNWprRCs3cXVQejN2Z2g1SHozUkZpeXMvbllDZ3FuUjlNTS9UUjROWkFCby9sb2pYL1pEa2svL0l4SFBab0ZLVnJ1OXc3cWJLL1kxWFVnT0FvOFAxV1hDbHN1c0FXQUxLQ3oxVUlFSlZjNk4vd1doZDRIeWl5VjJXc1lMejdjU0ZMTHFGUnBXVEtvdXg3QjAvbGI0ZHRpSGtaV29sck01ZHBDa1JrL2Z4Nk9xd29LanVzTmIrQWcraTJmVlprY3pHWGhGMGdLUStqaGwyaTVvcXNnVkJwUzBDUlkrTTFDQkZwTE0zWWFlbmkzMlZCNDJwUlp6TWI4VEljM0ZzRHRDNU4vSDRsSW5DSWZqanpDcnJCYkRzTzM0SzE5M1lFNGhlMG5RWXUxaVZic25tSllZSXYyWHo3WW1IeGNybVRpblF3OHFuQXcvYkxhNEpqL0poRjU3RDhsOG9oeTdVSWNWR0wwa1RYSDNxVXNiTDRiRE5uQTJxZmlmVVI1UkZVaTR4SzNLeUVUMENHK2gxN1JKMEFwY0dXUFFDSWVaQm1Yd09ncm1rRXFPRks3OUFhWGM1RzZEakhLZXVPSXRvbkZiOWFGSkVhcXVxQXJlNk5xK3IzMFAwQWNkSTJNL1pkZUVJMS9ZcHgxQ1dlQ2dudEZjb2o0WlFnUFZpY3ZwYTFaSHNoY3habzExQ1JONXZvVzNLaUN1UnBHNFkzMGU4QzdPbGVNNGVsWlliTG14dk55R1Z1UDgybGFQQVdmeXdaVDQ4OS9ETjBzYVVpQ3ZNNENvUGsyZVp5Vkx2eWhRMk9yK2lVOHhsYlNuZzB5eW13cjJnWmp2MXZNWW1rSkliRGFQNFcxbzZYd3MyeTVaOU1HV3JyaHBrZW5EWTdHTkE0ZHcxNUE1bjZFMUV6YTJ0b0d0eW5CQWRaaEpZRVpkRnhwTk9ZaUlsUUtJSmZVcGZVTFNaU2pWaEcyZGVOYjJveDhlTVRZSlRqaVFUbEhvdUQxYUoxWHVtWkZmekNrdGlrbzBHM1YxMk5HMURFQis2S1VyQXhSRkwxbTB6bXpHMTlvcUZxSzBNTkhqY3RiZW5WOHBsTlB4QVlzZ3JpOVF6amY3WUprS1NRQW1YMDhoTlBRZGVSUEM5d2l0aFo0aDMwSEFTOStqdFF1M0g0N0tZdkNwNWJTc3pNTDdOY3VSNXdDMk5PaTEvOFpNS3B1NDRvVGJmbWx5dVBJU2tzMzY1N3gvWEZacUVuYWM1NFRqNC9jMHZQZWxaYllWNnl1WWIxRE1IODEwS1BCQXo2ZHJkNTZJaUpxRDdZVXJqMFhiLzFxbU8wNDFyRUVrYzBwYkZFc0pwa09PNXZ4a3FuR1V4K3BDOWpCQkZ6NXZTdzV5c3ZUc2MwckpyUzUvanVDc0NrV3Q1TTBWNUtqdjlhdDErTDliWVcxVzE5b2hCQm9HdVh3UkdtaERKS2pyRVhBRmpXb3VzZ3l5M21vUE9YdUlXTDViWGV4eGgzSC9acktCa2JheGJ5eUY4ZEhwSStUNDk5UG5zcWxqell2cnhpV2kzbUpnaEhYcDdndjRMeU8xZ0VNNGh5Zy9tMHBzQjJmcjd4aVV5TkhxcEQwTnQ1WnpROW5qWFVFL09YV2pHTzN6WFFoK09yN2N6RDlnUk16bkw0UVNta1RPd3ZzbzM0T3NXbmJsMGdsdXNoVVQyWUdxUjUvU0FxSnhLSDdYUHlDUytBMUdWMEJsUlpzc0tXUEtDT3k4MllIMmVVSnhZV2FVOU15ZTdHeExuYjJGbnJzQ01hOGNRRjV3SG91cU90QW16S29ZNjdaYU1aNFoxNGViQnRXMDcvWWdaVkdYekgzSEVGcndyenJFbUlHdzNoYjNUSU1FZFVYdkdqdmVSV0pnY3lNNzFKbTgxLzZlOVNKdmRMenpWWDd2ek1ubzFweUc2Y2dkcmJtNTFJbVRMV2ZxZFFacVg1cnJPdWhDdXd2MUFOcUNmV213MUcxY251dytGVVNnUkg3aDZYOFpWalVlNXA5QkVsejYzb2RUWmtTeld2WHFrUWxIemh5bzJFUklKaGhxa0lvWnNTSlpPa3kwNXgvRWdhUlVkVlRUblJMVzVqcllqdlpyaC9BTS93dGV4YnUwc0k0MXZycFZIRjRGa25UZ2xoM1lNVHJDTGFhNWdUQ05CdE12ZTJSUUFEMktIQXBMOUVFZXJOWFpSd1ROYmdkaG9abFhQYU5qdDJvZjJRNnUzVlpaUjZnWG90WjFwUWdKNHE2UlNGMm0zSFoveTd0M0xZSk96YzZhaFBBVWFYdmRwVnlNeGthS2hIU2lXZHRLU1R0N1VTNTJuT3lGTmN3Wmp3Skg1Ty9DTVdQVEF1dmdJeFVST1FGcTQ0cjFUbWttYlgwNkNvWVdUSUdVdXBNS2pKYTBxa3pkdkZ3K2xIdXJZeHl1bDNadTFMbWQ0SFpmem1xMWJUMm1Ld1FibGQ4azFRMzhsc2prUUtSOEFudDBLRlgyeXB3U1BoaU5CZTFMV1htcXlubFNYb0xiY05sUGF4UTd5TUwzM0g2UUd5M0Nkb0FTRkErSEZjMysveG1OdzRvcWE3OTc1L05sSndvT0tsNDFFWm9qSFN0UmdIMTVWaGNMTVZ6aGJKVmt1K2pDa2hKZHY0WWFWdmtzdnIwRENoMHZ2ZGRZbklLSlN1Yy9IRm05RDU2TWF6QkxYWHdVOWdRb0NLWU1lcVFkRXl6RXRIWE5zL29IZ0hrT1Z5ejU5M21vdHlTeklRUjk4a0xiN2FYb09jUnFSZTNRNU1ENkdwRk1TUjV2bGlRUGJzSGc5YlB0Ni9JUHBoOGxzazFCN284UVZHbFZnSFBHOU5LdGpzYUUvWUdCOWtHajB2OUIzb2orTmlmQSsvYVV1OVBZTFUwdkVEbnY0UjJEMi9qWHRaYXRSWWZUczlxZzM1ZDdCRDJCZ0wxVTF6YmNNTFNHVEV0eVlSeU1GT3FmaDlRd2dsTit3SUdjckRVdThWOCsxbVhCcGNxWFVGUmNDL2pzS3crazRuZGgzM3FYd3NXY0ttazZrdGp3MWMzM0VwZmFoR1lqNXNPVlVnQU5PYzh0TjRTRGRxRW1yc0RoWlNJZm5lOXZSRUtRbjBhNnlJL3FHdHJBNEFXQTNQK1J6WDBoaWpSbzJDdmZBRmdrV01xR3g3Y21WRjFmS3FJSHVadkpwOFhCWnhnRGJGNzBMZjl5bjExR3U4aU9iZm1VeE9XSDFscFEzZXNJb2Vld2NnOXdXUVFLaC85QUJQc1luckJhaHQ3QkpjWnFlbkpIMzhsWjZqaysvQmNtKy9wSENESjN5ajV3VTZVQ3IvZWJXYUYwdGFocEkzR0hTZkVSQ2VxNWJSSGN5bTRPL2Q2SGVEYXpMS0dqc3pJZEV5M2YxMXh5bi92cHZxNVdEQjdFZFVMS2F0bUdINTZGaXpaSmczRG9LYTNZUVBHK295NlF5Y2E4RVB4cEEweCt6c01hWU1CQnlsUnNTbnBCMVliMERJclhQblM2TGliaTcrZkg4dDVHWm94T0RiTVdDNmhaTXNVT1pwYTN3VFkwTnora0d4WENaL3ZERUxUVThxUUFlS0h3NlBJL0p4QjRRY25YSGxJNVV5d0w5bTZoNW8yazJDbnJoSFV2cHZiTDFPZG5tL2RVMjRxWndpam1IaUVUOWxwSk9MNGlqNXp2NTNpUDlWbldSM3dJZ0ZnM0hFb2Y4ZitMNDhodkNuS1cxN1drSForNlB1bll3Tkt0d3ZPU0w3cFcySjVGcHZTT2N2MWpLVWNNUE9lV3VhRGVzUWh0WUlwN29LREpOSzIrb3JlaVRwOXZyV3Z6UmxnM3Z0b05JenNRMnZwZUpXU3JZOFFoaUZxbmM4cjh1NDZ1UlIyODdCM2RGV0xxclN5ZWlYaEVHbnZjbTJpbUZKQlhUVjFzMExsbTUwdDYvOFE4S2gveWVtclIzN2RIMDRkZHlMbUpaZk4zNUlNRFhCVjNpMm5sN2JlSnNEVXFrMkNFYXVnemdzYi9RQ3FXNHRTTExqWWdXVWNLczJ3Ulp6ODI2S2RYY3FmYlU1SGQvQyt1SE5tTHdiOWdiUFFPeTJTMFpLcndmQWJWN2xjOUR3SmdzeW90aHhBTUJkdi9XUmJESTA4OG5WbG9rNExtRFUyd1RZdW9lckNMaXBxUlVzTVlyTnpwSFlNYTJ6ZTVOdmtpYk5TamJ5MTVFbXdNd1Fta1BBdjNHbzlLanJibTVJbktFb1JYelJrWElRdENWZ291Wk45RkN1Um5XZ2JVbi9SQTl5SVI5TWZZMXpHdEM2cElsNW5Kd3dadXlkMHJUanZQdWFRWTlFRkZML1ZIM1FCdm9jZmF2RlhuT1IrUXFYanFhS3ZqRmI2ZzBXN0drZzVTcnRvVVFnY0RxVlFFNVk5WDVtcFErZGF5djJyditiOFZLNXE3dGQ3Z2ZndFMwWm1yY0VyeHpvR3ZaWkxwd0NwWHVEVExhYVpub3dSaVBRc0pCTW5ITFVaR0NhK1N2N3BYeFpKY1NlMmU3VjhHMDlnUUdDWDRkek1wSklxblFGcTl6NHRuaGMrTVlpVWdPWE9lSTNIQW9NVkw4YmR1RThrS2dMMzlXa2M1SDhEb1JHdG5MaXl0MmJnd2hWMGJXYU1tMXdWWG90UzJ4dHVUcUg4RWpUbWRlN3pJVmR2RVREbTR1NG9PalFSdDczZks4TVo5WjFKajRZcEUrQ0FaWWgwUFh6Y1dsWTNJNzZkOUlmSDlsT2Zld2FmY2QwVnYwODlJSkhRUHZrYzIwbXFPSE5JOHdCekxOM1R1b2JBY25hdTczQTBpNUtlNEpUMGg4Y21BakphWklGdVVER1R4b2tRZXBjeVR4ZGlzSEFuZjIycVEyWmJwaTdnSUU1SHkzQXVuVEdmd1ZjMm5uT05VNjFLQWdZMnFaT3dKOXY4NXJhT2FQV0tCR29ac1Y0cUlmUFpNNGc2MG9DTkd5ZGp6SVNFUGZ3WE9KUWRNRmQzWGd1R2tIVEwzQk82Y2lDYUZkK0ZVbWhuemZ6QkJRUUQrZ21BamdBb28xMm8rUkVzVnpmYXZ1TjFMWVE2WVBGMFJhS0ZkOWs4VjRPbzNEclVUakIySVNvd1BmS0JjRlNXQ1d5WUFNM1grS1psdC9RSUtZYnpEQk5rMUo0K0ZYSXVYN0lITGQzNUp6VXJqRVdyMFRUWXJtbkY0dkxRQ205TkE1TFJhZThsaFR1ZE0rYjUrS2wxRi9QcmJWVExJOXVFRXBLRUlKc3F1N29oT2dSWWlDY241NWlCTzZLSHg2Sk4wc291dDB0SG9nWEFjT2dMdUNINmVMN1lhMnNrWk01NXhuSDRWc3hmTGFFK3YvM1lya3RNa25pakwxZ21yV1lBOThSMmg1VWNseVFtMklaMFJBODhGVXFZM0hvWHc4ZDRsMzRrdm11YTc5RWl0OXM1V0JwZndsMVdaQjFwaEd5V0g0RW05Mk9xQjdJOUhnY3pSbitvSHJUT0g2Z1BOQ2xET1oyUDlYbUNHdk1hazF6ZWhOcU5meW9lbjFjUUxIWHBLbUsxU3V3QU1Bek9seEE4WWpCUXpqbTVNVmdlTzdKUGs0UFZpNlJpYTlleFpCbkFFOHVXYmdmVlVKdjhDSUdpRWRKQXNqdXZaNWlBMklaYnF4RWI2Tkt0eDRrRVJhaEQwTXEzQ3FmKzJrOXdUSDlkL29BWXlwL1M2USs3RUFJV1laN0paYndKcW5rVHl4NUZQWjNKeGVuQ2tSQWNvZzdJVUJERC9iampFSGMwQjErdXlBTVhTOHZKM1JkWHN1QUlkV2poOHRKR05RSkFpZ3IzRkhkOVJvcGxZejgvMnNyaGZ3bHJldDkwUlRZUjBqUkh0MEFwQlNSQkFpSFppZkh4b0JYa2s3K2lPcDRwbGhGNFB5UUJaSjBQZytQVWF4SUxHV2FGaFVCUUpsWDFHSlp3U01ldG8rVlZKak1ocWJjRjN4d3djRzlVVnhFUXVRZ2xEMnZuOUpWV3pETkc0WURQckZPN2ZFb1BYajVSNHR3UkhnMlh1N0FENklIeFYzeFN1cTlLUXRoM29lNjFvRWJmWG9qOEFEWktJVjRpVHJqSGxrWXZZSW1neitkVGdnSi9UMFdMZEc3SG1ZSmxNZkFvRmExcmwrTExjWGoxQXlRcVBSZ1JmQXRFUkIrUmYybkRlM3k1d0poZXhuMzFqbDk3cWZoMUxVWmErMmVrd014eVFoVHBmWXA0djcyS29vUjcwVFBQbUE3R1lVcGlPU0t4RUdrWFRRZTlUS1dOU1IvT2g3MFhFQWZCTkRBVlBXQmFXL1BMK01SUVhqd1VpWUhMcWUyMFVlVUtqNzlFdklnaktPUEdreHdnREF6dUZHN1dvMWl5S3VOMnNSUkxjSGRpeEoyVFMxN0ZzMWZzMGdyeEo0NFpVOEkyNHJZWWpUVWRCU0lxTzVHQmZXSWJLcmhFZm9YamtJc3VtdkF3YlNkL0FlRHFnZVlRd2NBUHIzb2pjVHU3eFBEQnZHa0RXQXRZcEYxOU9GQjZpOHBaWEw3d002cHJocTBhNDFTQ2NxcktmVUFrN0N5S1hOS0hrUzAyVnV6R0ZuQ3Jyb1RhUloySFJxODY3NzF0clNWdkQ2V1M0SU5UL2w1SFJoZUhvck0wV2FSaHFrUmRZb09pd2FMb1JGMWhseVV4VitKcjNWVVZoVGRyb0p1MjJNZzEyR2xlV0paL3UyM0FvUndxdkZvSWlaNTJXRjZFZ0hrdFYxR3F3bUxJS2FOYms2TXdXMGpMeitQaGFrU2N4bEdTRmJjVWdWL3czRE5lU1RsWmJtS0ZIK3k2b0MxY1pOU3M1d2RFUk8wQ1JhK3FORU5obEJaZ0dUZUVoTDZoRW03ejYzU20rNDZJOGFNMHQrdmxOQ1paa3hEdTVnL3hjRXBYWUVHWU5MR2ZLekdmbDNkVTA5dlJCMUtEOG5jcGZmUFZGNkxseDFKaXpDUWs3azRvOXRrUStRY2F2Y2xjOEpGVGVYdGwxRFhMbU4vMXMxOVhsRUlhcGtMVFYvNmNmbzBEc3JkWFZUQVMzdnZpamxyNlZ0Um40UmtINnk4UFNKZnQrVmMvZGlkbkJFU0ZSOFpGOFJBL1RhQ2JudURKM1dwWnZYVm1qZy9IMEdDakpJT0trdjR6WHk0OXRrTDRYK1FhcG5iSGFMRnExck9iTDR6czZpTGwxbnhqRVRaaFBWM1VHNWx6b3BlanBWWXBiNmsxNFA5OUFNSjk1MitlUjN2YjcxSFpLcWJaVUtDVEg2WHFvbU1YOExIUTltVDRMU3gxY3lSVzZESXkycWJxd2ZJeEJXM3dxOUUyTVpIa3QweTFiSzhKOVlOY01wd25NNlcyQWppZm14S0JIY2FkUkI5cGd4SmY5bTR3ZjkxZ3NrSVY5dGFlM2R4Rml4RHFpelNid3BsS3NOMXhUQlR4SE41MEJvNVdTZnBCZDlxaXZpQlVWM0svODlSeEMwNWx2b1BjazZRT2RnRjVaRnFKUGpZNWc1djNPbTZpM1M0ajlCcG9sZHhqU0trbXYzWWN2MGwzN0R5QzBMRnFqOHZJcnVGWG9TM2NQOVJTRUJGelFiUXpsVXZNQlBhUUlSZndYUUx2NHBpQ1huMlltY1E0b1RQd0Z0WlJ6ZWdIL05tbHFCYi9KTksvYndaRFc5QVBXUG1lVTJMVWpwS2FuaU5GSGhyNU05d2dkTUx1T0YvU2FsOVBVUG1jRXV6Y0VtMG13Nm0vTENFb3pVdWxFUlhKOExyNVM4VWxabDExZ0tYaEFhTjRsbkdMTldROCthdTZzWFE5NUN1S3VDd0FDRnVSUGQ2djZyMHZUMXNjSEdqaWxraXEzR0JpYzJyajF5QWNWRTZCZmdQQmZBVFpXWEI4Njc1U3RFR2dhd1ZqOVk4dWdGL0JWSEJ6NHAxSjJlYWNtUWNBVXFNMHhCc2tDMW5RdGphbHN1S21PZXN2TVpCNkNFbHExeWNkMExydUJSQ2tGRk43ek5EWlE1dEhDTFFOdXp6NDZBaVlyOFhoWXltTXVYSnhYMndteTlaMTViL2h5VWRzNGZLcGo3SVdXckhUTFNSa1VHZnF1ZjYxdVpucHhtWUtxOURiUU9DeUpmd2xUaXlqYW5uYXRyUlVVeCtKdVNsU09qVW9KQzRYRDJkRkVOamZLc003ZmJSSzFhcVBWN1ZYQlVRNzQ3a1RNMlRhNEpUK2ZoemNmWGJnUmVSNVh2MERvWmVIOFZ3WmE5U05COTN5anJRa1hxUVJjZ0xjVW53Ui9WY3lSajlvQXlOWmxlU3lwMGZxaUFyWStsYUdjL3NQTW5qZ25QMHdPd1FHVUZieXNubEEzeFdDRzhLQ3BydHFTQ0ZWL2dNRTF1UlV3MiswQ1NMUlZpZXd1YVhlaEJKNVN6bm82bVhhMXgzV2tmaWQ1eVdOYlJNaXFXc3pnUEo4TWttUGwrbjJ6Tm1qbnFPSStqVGZkNEpFV3gwajFsc0NLVWdnMXgvUDZkdWxtcm8vWUVEL1MrUWRwbUVhM2plNXNDanNvdnZDRGEwS2ZxVk4wbjdCQlVQdkFnM0ExMG5tVWRWUW1MdHFPOVBrQVhrdVRyb1NyeTBQUTd0NDgzSmlsSGRGNUVHNFMyZndURzRPcVFVMjQyblhUUkYraXJSMGJIN09WbUxWdE9BQ2JQcWxwNVhRY3VhSUZqeTdqNnp5T0w1VDNvRXpWQzR4YWhHMG1JWnFLZFcvU0J0N0o2aXdhVDNWV3FpNnJIQkJhZEc3ZUtPVjZNckZpc2xzMUNzbGxsMEViaExQczU3WjhyTno1SS9wVDZkS0tkWmdkUFgwMk81cEpUM0V4SVh5SzVBZUFDZCt4RjlVb0pyL0ozNTNPZCtnNmtiR2dCNzNPV3J3eFloNWhJRzl5cjlPS2s2Qzg3YVhPZVhaQ0w3ZzlEMHVrN2l3YzBDejNOK25YcEsxb2R5TVBBd3lEWW5ka1c5T2tsei9iTjhHaHNNVWxiUmZITTVIelVYKzZaVnJ6cHJvcW42a2JuVVFMK3Bwd2dTTE9iSEk4V2NkcTlrVncvOHV4eTBMYnZER2pnUUNzT3JoNWhjUE13dGt2aGhSa053NHk2MHBZbXErYyt5U0UzdXZRT2l1T0Vxak8weXJEeDBXQ1JwMDl6RnNQcTRuQVFnT21vR24zTkV1ZUx3cFluTWxyNlZZRzRZYzhsTkg0RjdTMkxYNW9vSm1tdmFaS1BBRVNjUS9PSHliNGV2cUF0ZzJDL25jWDJxaVg0aWVyYWFlaDFiSUc1RkhTbUltekcxOE5NYkp3bmp0ZzRSVHFEcXdSR2EvZHAwNW5Ld2JXVE1URnRHSEpNNlNPb1VHZ0dnSEhuQjdSS3VCc3dsNnFDMVh2U0l3OEpmdU9QMm50Z0JpNWU3NmRaczR1WDZrbjljUlR3WkJJRE9jRTE1VWRRTVZUc3hKZ2twT0V5M3pZVThsQTJPSi9uMW4xWHZxbkdpSWtSMnhCczErd2FtNUhod2tpZGkwZUp3Uk9aSWpscmpsQW03M0VPd2RYN09Gem1WeGMva0M1KzVzWnpraERrd24remJxVGx0ZHJEWXViM2hqRVZmdlBJcVE1NDJET2ZycUNCMUFYUFZyR0FQVTdscTd3MmU1Q3NqczVLbVhSMXlsaXpVRHFudzZ0ZkpFVHpYSFFBTVUydUhqUWJ5U1B6WFhYRWFmOUE5NlAyNDBuUHNKSmJ1cWtqalEzSjhvbTh1TmFUcndCS2ZHaXY5UGNacnRWbGV5MXAyTW5Gc3ZseERyTFlabWRYcVJRWDVqVjhKTEQ1MDBYNjZkL2lMNU1wOG1VOVp1K0FaTExCS2Y0WVZsYVhsY2tWKzVTcVhwd2NXSml5cnRtcTlqdDd4YWZjSnkxd3lDTDJOSnNUdFFqWWdJaVllRmFqVEJqdlhCWWI5UEhhVEFqVWhUcWxzUmZjSldHKzBrMi9SY0hSZmVhL1dvNTRWbVA2UDVxWnUxZGdQSE9XM3YzODZtd2JOQ09EWGF1LzFZOUQyR0R5WkdMa0V5ZDdJa2FuUzY4aThXbU9pek5xKzVyZnNvNWduV0prV2dWSHBIdm5qeEhtb2tpb0Nsa0xERGRkRFV6cXZCcncyVkl6MVJjZVNzT3N1V3FzRVB5c0RQTnlLTHM2STB5bXgrT05wblM5UW1ybUZTOURySkNjZXlGcmFZTDJ0MlREa3lsbHVXUXZyRWZDNkh6UHBCc1ZDTm41N2loRjRuMmNkdFBGSzVqanVqWktHR2s2Z2ZIMmlLSWlFY2dxZUh6NlhDOHI3VTZkUEp3VWFYRTNna2g5YWp4dWltbzhmc2RQZWZ3aXBaRTkzZjVHVnBoN1hqVStSZ1VWbGxZNVREby8wVitBaTRPckx5Vi85T3ducEVEL3VqbWhBM0daRnY1K0R4ODJZVmtpZ240WjFjemtkRXFnUk5QbVAwUDBOYmU0U1VJU3J6WWxCaVBNT2lYK3VoeE1OOE12YVFrZ2hjSTN4ZVM5Wk95NjZuODZJZ2dnZ2xKOHZUeTNiZWMvUXMrem1UVDNrY1QzUWZQWG5BdlRCemxaaEFOUHVOK0paamlXQm9jMnAxbmw3RXNiSUZuZnMvVlBQL0hXTzhEdFBzN0JoSFo1VjJ1eXBGdy9hVUhab21vdkUxckd2L1RyZU5DRDFIdG5xK3NQVlZUdGs1QUNyWnZNK3pNY1lhUmFSTTBYZ2hVNlpkdHdMaWtqWGlXNlEyY2VNMTA1U0ZSN044ZmxPaWNLQWhkZllRTFdTUk1hcUJXYVNEVG9GdUhKL1NHQjFXVGQvaUNZVjNyaVU4cHNHKzE3a2lNRlQ0VXNhQkNjQjRSQzVuQi9ubysyemhXMVVkTnpFZU93R1Nyc2YvZnRKdVA1eGVSWEcxQXpBbk83dDJhVEZaS3pWQ0Y1OWpXS0piUkJjNUEwdVo2K3FBV0NkTXN3M25WT0xTVkpVcmY5WWd5QS8yNHU3R1UrbkNZbG9IL0hHenhCYXlRcnpRamxzU1pZdUNua0U1V2NvMndsMHc2bG1ZYXEycHd3ZzRWMk5XYkJFcWRKSWJVSmxIUlJmZ1V6NGNycVFGa2hFdUh1cDdjSXRJK3d2NDhjSGJZRGx0QWpxalZqVVpNLzRiV3c4c2N3Q2RtMU10WGNQOHM3YWhrUkJ4MWY1QzlvWU1UUS9BV2lPSk8xekgvQ1pOOWdSYktSUlYwd2xRNnp6Yk9talozWkEwTUo5ZzBvNkFHNDNZNHU1K2F0N25SZDdtRjExWFFXQlFlRnh6UE8zU0ZLcE9maE4yUkJXMmRIaW80V3pPeU9TdGE0eEc0TUVaQU1ZYmFLMWZLSGZSenptYm1oeEh0SWVsTkNIUitPZk80SCtYVzlFZndkRk5QM3ozY3VQdFVPVytoWTVMQk9RcWNQOGs4ZnJzWnh0WHJaaGJYVlFyNHY0WWdvejZIa0c5WkxzejRwN1Y4bVIwUVgrV2ZwOGhvT3Vwcnl3VUtNU1ZiVnpIOXFLZmJ0d2h4elZOK3o1NU9peDBuR254b2xKZ3ZYQTNRZmxGa2VidXN3L0VLQkJjOXN5aDl1dkxIVjBMaGd2Yjh2MEtrNlN1NndESkdPSVF0eFBsSm91ZjhWRlJoTGZvdFJzRmQxbEhDWXdFM1VkdW1kenF3eUJxU0kwcDQzbkFXRjhieGZNSDN4YjNTLzRPSGtiRjZTamZaNDhuanNkcVUzd1NhdkR1OHRIdnprQSt1MlZSZGd4ZXRsa3lEMkZVL3V5TjdOaGxvSW1ITythVVluRVhPdXk3eFM0alVQM1VlTGVnb3NuVU03WGlpYnNqS09qamhCVFJBcVVSL0g5VGt6RHRRL1IwbnFsZjRaWGFuNldDVFdvTU1YbC9JUXBNcmpsZlFjVm80c1ViWnRFT2RwRS8vbXovREFsblcxZjNaeHVVN05nSjFkYkdFbU1zbDR1YnpBN1o3T1BHcUh5SGJka29VTDVTYUVJVDNlLzNNcFMxejhzdVlXRGxzZTlrWDVhYTFGSW1sUm5tNjIvOEpQT1Qvei9uNG5sblVTaVAydHJ3eDZPdGg0MWlLNHN6NmJRUWwzdkM5Z0lBYWNRMGFuMWNKcGQwSC9HVHZLU2JFSmlQMmZvYlZYbXphODJsR3BxcVdPY3crU1l3T0ZKc3RYbk0yekFBTkx0aUU4MVFyTUkwZ1VWYTBhRWJqT09oam5yT0h0ZXFjS2MzMDAzbFprYTRVN3k2RDgwMlRBZ2hKbmVjR2tpUHJ0elIrTzhWYjA1TCtnRE9VN2J4ZjU3SG13WGp2S0t6ZHkzS1grOEx2WmloZ1V3eGFBYlMyZnlFQUxrUGNBRm00U05ONGpSTU9TdG51Q0lmWk83RHNIbVNXZjdiaXczbDlCSlVpb2RUdEhGTXpVSElnd2g5eUR5R3FXUE4vWVJuR1hqSHdDMTd6UnR4YzR4NWo0cTkvMjBNSTFmVjZFSkNlZzlCTkRWMk1xWk54dGwvWnJRcXlpaXpSSm85NGEvZk9hUjJHUE1RRDBLYzF2MGM4RkR5Zzh5eVByWXNvLzZDNm9wUk50YW9ab2p1S3ljMVkwSUtpMW9mbzMzVzhTUldTeFNVUU5nWmsxTi8zaDg3MDcrMW9jMFNBOWl1bjVvSDlvaG8xbTYrdi9aZWdOd3ZXL3RNdm5QVHlNVFh2dDJRaThlek5WSVFUcThOdzAvVjdoVVR0U0t3VWZ4ZklUamIrQUVweFY4Si9nMEYrWnJ6ZHBacW9aelFhWUFWdUUyMmU5dmRpY09IM21HOFFOYkR5M2N2c0RrWjRQc0JvMDR0bzFleTJiUXFCY290R3orSmQzd1U4T0JJL2tLY1lPZEtYSXZDU1RIS3VuZHZRSDUweUg1b1Ryc09Cd1JLSXZNVlA5V2Q5YkZ1WGZXWTA5ajRnYndXemh6cmxFK3lmbjhKOTBRMVFRVTMzVHEzYUE5bW54bGE5Y3Y0VklTemhqSEFpcjdLSEdZaEdxNllWWDRJY01sQmxTNm1oUGhid0xuYlVwdURaMHlBcWttZEs4WitxTGR0ZERTb1hDRmxyeHo1MkFGQVE3MWVQL1VaMENaWjlGM01tTFZkWTBxeHEyYm85dGtiNGZkaExOc2xMbGNUcFcxL0ZVQlRwSUJCMVhSQ0tXL2xZS0l1MmUySi9RTFFtWW9VanRBMDJQZ2JrTFNscEZQeStsNEFxaE81MUlPQ0ZZL1pNNXRENTErN3gxNDNqcVVUaDJPR0hUOTdvdjV3ZXVkUVBjTGY2RC9lNXZlbkxTT00ybnM4WVFkbjYzZmhFb1V2ckhKWC9VWWl3QXZQb0RlbkdZcm9GVkxGakdWY0dIeEVSMTA2bURFZDZnVnNTK3BlYm04bEVybHZFWVpkSXAweFlBUFRoQjNuRGZocXpuc0RtN3JGTThMUCtZbmFpeUxvVlNNRnVrVEZOT3B0bXBaZ21pOG9kR0ZQY1pyT2VDWnlpdnFOLzBKektPNEdiL3YyM21xeVIyOW1sNm5PVDdMRkFrY1ZzL0NtYWlVeDNsZ2RhZmRiUExlOUVVYmxnMDFWSEtyd3NtZEZveFhGUFNVcE1GTmxVMDl6amZOYUpjL3NJcVhSczM3aXRJMGh6Z2piU2g1WUZEMGRNcjNZczdBQ1p0bUFMeVFUa1BWVEZscTZidDU3ZnFzMkFBTHN3TGsweld6YnRneVNKWnFEblVzbzRyZTlSQm5VUnFUc2hPQjdTV2RqK3JyM3RhSUdjdU9pc2JDVkxRTFFOdFpFV0N3dFZJaTdYbTVocXllb1o0N3Qrd0paU0NGYVN6bStnS1VLc2RrNWI3V0dxelV3UGVmYlRDbks1OGVDREFodWU5bE5rb0xMT094SkRLbUY3c211bSsxNFJ1K2hmMlZyWUh0UXJ6TWVuVGZQZ2tsOENtQ3E4TmlVNU9xeG15ZTdHeG1icHVPKy9DRDMrL0lRQ3Nud1lhYVdSM3lNQjFHVlFqMU9mTHdtcTVFMm5LN1Z4VUREdU9LeGtMQWlSK2RVN2VOcjZVa3dDM2I3Wit0ZnpFNGd6VFBYZS9DZGVWbzJHSE5DS281WmRuejdoTWMxL3hkUWZ4T3BicGR1Z0EvMUlLdW4wSlVvRU9uc0Z6dmdia0NHZFlDUExaYlBpcDlRSmZVUldSNFJIOXhBaE1NT3NDbVlvM3orRVFYNHltbVFCdEJzdWJDdm0vOGJ2c2MzSmZ6R2hBQlFRSm1DVXE5NHpIYUFicnJyWExYNzQ1RTExU21IMk1WT2xPbVpPWTVGSi8xSkhIRTd5M3J3N09TVmtETFc3L1RuYVlwOVVQS0hvVVFDdjNoY0tzZE5VRUxVRlI1c1dLZUxOYTVSOENxWS9xYlBPeHNINDQ3Y3p6cElSQ0dRVzgxaVdjLzZTcXF6ZHpHUUVjNWpQOGNIekUrV2xOMlYrSlA5dFYwSzJIR2VHSlEwR1YySUxRRCt4M2VJSnE3czVET1FEdnByM1VNTzNacmNzQmVoNWV5OGs2ZkMrVmJ6UzR1K0ZIbElDNGpqbmFTS1lZT2tQSkFqTkVJV3BsNXhJWU1jS09FUy9oZmFVc3c4RjE1M2VydUdhN1I2a2FwektGL21QT0NtSXVMb3o1L2pmdk1qMUZpQkgyU2RnTlZtampzRjg2d0UxcEdWaWkyUGZBUmZDYXhvNURBamZ2QlNnSkJoS3pVZjVLUlpMa3RJMlhaTTUxMWJwSXI5MmV6K2JJTkpBZkhyL0d6ejNkb1NnRE9sQk5WaGd5QmI5SzFlQW9lb1FmMmw0Uk5JT3huMVdmb3VYcmxoYUJoT0lLNkliYktCZWdQS1JXZ0pxc0NBMHdhalZsMEwvdnBzMEo5UTRYMlMzdjkwNWFDeGdHR1EyL3FVZXZTMWQ0d1l5S0VZUzZsYnVzODlCNFozWC9KZTRDcS91ejVsU0FLTHFSdWtOYmxVSjV1MnIxdUdwcVpKcGxrZDRmejU0eURxOVFWSDUwTWFKQnNjdUVqV1BiQkVUN3pBNzZ6bkVvUXB2THJwQ21VTXB6bkl1bzJHYnJISlVyNDlsbWo4ejlLOEVicldicXh6c3Z1ejNYZHFBYm5UUURyOWorbXB4WUpXRXlVbUdOUCt4RUlyMVVlcjRuSlhKckZNNDlhTGFJUHJvbllWcHJoempJMWVKbGJnczAyZWVFaWY1azNCdVI0NG5nQmhHOUFiZFcxRUxXeDlIekZPd1gwSGJEZm9nSXpBRk1DZlB4MkVjL2tFT0ZxdktIdFlDeVlETWVWb1JnOTZseEVveHVTeU5odWtaQ0VHL3dNa1dMMTB6R3lLYnU1ZG5ZeXY1Tk5PNlZzTVdvRnRHV1dFVjVva0NaTmsxbXNSc0srYXA0ODBYcTFPK0ZmUzRpcURGVlhzVytVaTB4OGU0cmEyZzZUUnpycWw1TW9zRTd1OXdiOFpyM09sT0FjanlsUjRxNmJkS1hQSUl1cVBkMy80SEYxbk9BQ2lSMGFlKzNjaUdaaDArUGg3aTVtSmdQRytVU004bGFwd24yaVVhaHpPTzkzSFRnV1pXNy9HY0NZNXNPOS9SeG9iaWMrY1ZTRTRKcUhrNWRHM1VRdlE4dlFGQm5XUEJ0Wkw0azVuQlRLNExRVmpYaWY2eVcvQlJ6Mk9LdGFpbHBrbG04ZTVXeVQvZ0NWZ1Y3WmljYXVQL0dwMWZ3N0NJbXE1MVVlekpKcElVS3MxQmE0MXNvTmxxMjZFZG5GL1NWQlZuS1V2OVA5OU40NkN4OFc0OStHNVQrNkIzR3ZlUWdPeHZ1QUxnbElYdVhWRFpwcEJ5VVdWVkZtVE1xSEN4L0JrWVFDMHQ0QUhQK1paVWxLN0g1em9iRS9mcmpWNWl0Vmczd3JPbUxCTXorTEE3enQ4VEorcFRMb0RleDB2SjBLbERWaFRxVUVscFNXZU96NHpYaG5CTHhYTGtpU0hpNHRUYmovY0ZRb2hVai93TCtIMlRZV1dpS25lL0Q5K3ora2ExVXlsUUhWeW1zWHdLSFFTL2NLWTNOV0s3NDRoOEFNNUhzR2xGMDVBR3d4dGg0amFCRldWOUFrZG1lMmlaMmExVzB3TTFNSVRFTjRINndsM3V0QkNDekp2azllbU1zOGFBM0tJQVJZNDBLVVczeWwybHl2RWE1clp0R3g1c3pZNWZSK3FOZjU4NHRuTHR6OHV2dGNkSTcwRGpPeE5aRDNidzhlMXhZeksrS3pVZ2s2UGswR2cwWk0rNGE0M1dVUnhnUGVocktSUHVDcmFwY0RjSTRJL25wVWxUQ1RHeFVhSjFvckFKOCtEaTVzanl2aDVaRWEwYTUzWEkxaVNjRHJpL0loK3dxOUdoR0wrVzFBYmpJNWVQWDkwb0ZTWnRab1JxQllkK3hJS2tkUnBkTjkyN3podkNlWEtNS2duYWVDNmMzSWVWN3laM3k0RDFIL3BMQnhUeGNobC92VWFkODZBRzZvZ3ZXMkd5UTBLNGxkOWJiY0V6TnBnY1FqTXRzTXdLMEVnRzNuZXliZE8xSWtBUCtXdTU2QVJ4RTE2VGVIQWNXenFueU9ybklsb2dCVHIvMnJxWGp4byt2VlJEYTJjYW15dVpLbnRzbXRDYlpBbHpmdGdmRjU0U1lna3ZhN1BZVWlndVBGdEx1Mkx1YnhMMlJHSE1MWU9PUkg5Mm9icTFYaXRuaWh3ZWkzZG5ER0diMUZHdjNyWk1LWnczdW9iVWVjV0MwQWUwTy9oQ3MxcDh2RWpFYlE1QkRLMkhLclRZMGE0N3RFRi9oVzFIZlJyZVMyR2tQb2U3WS91d3YzSXlyNS9FUXR4dGQrSXBmUXJmZ0tFb3B0L1FOUjVpcllxMGtxRHpvdW14V2FEc09zbW0zQVVKZUlOS2xYME1IaTdTR09taHlBTUhYZEpTSi92UFJRZ3lqTVc5Rk9EUXVNaTJoa2lCVndmVEdDRWh2VThCdjd2aTFGL1BKVzI5d254aTRvazEzeXNYb2tVMWQ4N3htTmd3WUdhVTAxT25qa2ZlOHVXekNGdnVaT2FUUGNIT1JXNlN5L2hHeHBVZDNFVGxnMk9jdlorK0preExKOWpXVERxdVF6eFFBWGxQdzRjQ2IrY0RWcFJWZDFqS3F1Qjl5NGFuMzNmeE5qNVQrcnBabU1ZSUMrMDFFZzhxcTZRQWZVQk1mbHpxS2JrZUFuSVc5czFKSDlIa283TVprazBaUFRaVVhMUWtWMzhCTjRBclRxbHpsb25HQW1YRUVYcUtEbDVhd010OTlEL2VReGNlL2E3RzU1cmEwY0JvY3BqbnAvcjV3Wk96UktBaTBtTkgvOWRSR1hSUnkvTkpKRnVGNURWUndWR2lLbi9ZWjV6U2gvTXROczFVakNMZ3JOK09OenRxVzVTQnhYd1Fsb2ZyQi9WMWFLa2FYbUVuYjVyL3ZOVGJCaVR6RWdQTis3ZDdqSzh6Wi9tQ1ZRY3hJcWZMUXBmRnV3bWV3SXlWQTFOL2JScHFwZXF1azVTQjlubms0NzZ5SmorYk5UTEl5RTB4akUyZU9EOGVha3Z3SlhNUXpXVXVrNDVQdlJkbTRHY2Rud28xTXdPWHJvNmgwektMRDBNQkJJOFdQczJVNWMzSlMrT21pVnZsb2gxWlByTnVBdHdHZWM2SkRWQnhnclhKRWx1MzJjNSt1Z3Q4SGVteWx4NzlpbTFpMUl5aXN2aHdLaHNFQWVpVWhzNjVUOVJKdEZVNDdiK000WTZLYnlLSmtZK29KV2k0UkJMbFJUZGpiRUZpTXJDUWl2VUJONThTWkJwRXpsYzhTbmkwaWR1NThMMUVmUjBjQ0NVSW5SZmZuaElPQnJlenJ6ejRESFkrRnVSZzNvL1d4Z1pERVR4T2hVa0FrcjVKbGJpTkhSQk9RUmloemVIZnlGM0ZLZTd0VEVLY2N0cnd6TFNSVll4Rmd3eTFndUtydDlnb0tQWTczQnRuTHREaVFSeTkydEI1ZVdBdldmZVBETHFvaVIrekxNeCtUTU1JS3FlYmVxbGEvdjNRTWdjT3VaakNqdUJXOGNEMzdySHdqVzZsYUlaYVVwdXB1N3pWNVA4SnJYeUQ2aVpzZUxkK2tyT3lXZzJxTDFIOThhTFBTL0M1V3J2WDUwY0ZEZ2JkUnJ2UWlEYUhlbXNWU3hHeGhHVEZ3NEZmbjBXZDZkaGl0Ynh3Ymo4MUJ5SDNZUWgyK3FUR0hKd2RDTzRUMTZ2TUNRRmJtQXR6SlpKMTYyb2hHTnVQajNSNGZXVmIvMEVWWisvTjlIalI5SmlmMzZJam04ZmNBSTEwNHhyblRZNjFzZjd6RWhhcDZjWUNCcmViS0tXZTJRb1Jyb3owck91OFI4ekNacEd2OUlpWlFqYVNnSXY2N1hrYzZrNU9xRExMM1QrRzh0SGhneHUwWGtvTGhVRE91aDNoODM3RVRUNjlOV2N5NC9ETkRzaGhtaWFrSFVvTUVhMXR0UnNMOW9Ib3plUW83SmxBM0xwZXN0S3NnSlYvbkU4VTUvbXRBS05sbi9IYjREZlBDbDVyaUh0eEltTWtFaE1BQ2c4Vk9sVUNKVDNwSGUreEtvZzBxdEdCWlRuMnBmQ0xPdVZ0aFpSWUI2ck5wbkZBS3Y2MGJENE9CeFZydHNuVjhtVCtwMUdySjVHQVdWMENCMmkzVEVsMnVycEhMVFFSSThjenZ4RVRQbGs5QXMxSUVLdTBhOWlSenREZldnTnRkNlIxSGVuRyt5UGV0cGVGbVZMMXduelpLOHdqQS90bFdrMS9DY0FzRkZFYkpVQVM5NVpzZHdqdHRycjV3VkZyYTdycWFROUU5V0lyTnFZdEprd1ZxdXpDWE5kUW82Y2duNzNQWjl4SWVVcWwxSXVOcHZMRkVGNEFsWElDczBZQlovRWRCREpvN3kyNm5BaC9oSi9vaW1rb3lyaGlJbTg0K2ZsRkNvWG9MaURNTDhsbXlUZFRZWTFFdHo4YmVaLzArYU5IWGRLMXRaMlBoNER5enovRzdwdVdLaGk1T0FZaUVGY2I3RGhPMUxoNm02L2hRb3YybFhtT3RnWjR4TmFBUDBScWxob2lWMmZqVllQQURlM3hhbldFRXlOQ0Z6bmN2ZjVoNEh5akQ5ZTZFbC9EZ1hsSytGNkxkV3lBRmZpaE9xYWhFZVNrNFFNUG90b1pMUDVRdGkrYk00Q2pGMEtOVmNjRHpmWFZVRTZNYkZqbzZyM1M2T2ovUUcyaEFxRnBoMlF1bmFrM3JodU5kU0QxT28vcGlPWXZIdUNjQ1V4d1JEUjJZZGxhMUpweitPa0hlTW1oblBKd0h1V1VwL2pVVk82eG9ocW5GS2dJTWszc3VUVDUvU2pWNTErOHNYcW5td2hQdGNES1FmOUEzYjRxMGdyZDhVd085aFpvRG5SVmdQRlRpM3l5Qi9UcHBJOFFZMFAwb3hybnR2QmtYZDMzbm9tV3ZyQTcwNnd5dkM1UkhPdFNKQ1dza25HR0xSeGxBb0ZOdUJIVmVQVENmbHR3WUEwbzJYZ3VMdXZyQ1dQbHU0TW9jMXVEYjVReThUKzZJUklLNGhjcTkyaGJwVWRyZjd4c1hzb2FPZ2lmT3RITytqY1ZQRWdkTldCTjZkOWNMRCtOeDMzVnhuQ25DSU02STFUTnlLczVlYnVlWGVBeEdCdFFDODJCcUlvdFJFa3RWY0tlcGtEWmtvTm5FWG9aVTVuV3dlMUF5TzdhOUkvMnE2N1JjOW9KNC9kRThUUm9kQTJpdWR1dzZoeG1YRmhwWkJVL3hINWxOZW1WMHIvQkdNU3EzK3lzSWY5MFBzYVkzVjFGeXU2SlNnZytoaGdpUXVOV3IyQnMxZUFUUHJyeE5BaWdLMU5oRjIvaGtLMnVsVXZiNEtmZkRIR1U5NmYxeGFGNXdqS0p2ZWZMdHJRT1hKK1lVWVBaejMra2tMa3ZQVU5GN3F1TlBFak53am80bldoejZpckJyamZvczVENlZUNkFlZFlXbTFoRFRGRjdMT0tGeTNVL3VWWHdwbXkwcTlEcC9CZndwU2xNNE9Wb2FwYXdiWkhTOUFHcWppVFBJaWxZQXBaeThHTjA3ejlFd2FValRQRWJXbTU0cm5WK1BMdTdHaHF0SXdxdGsrOW00QmFDVDJqanNnRVVxODM1SGdld3pNOXRldmhVUkRpOFhRaEhpRSt1TnU5anp0WTAzWGpNalFrVDZRMWtIcEZFM01uczRPQ092ZVgrcUhNVkJ5SHVCT2VBOVR4bjBPUTFJcVJqMUo4YXVhUkJIbUE3TGVsdHpLOFhQaVFvZjFzUmVIZFdPTDlzTXdCdm5NRVZNR1NuNTZJNmMrMWxhWXhCNUNJVWR0QnBqOFZMMVZvdDRUd0ZoZWxDS1crTmlKVDNueHN0aWRkVENzVHlwN0FVM3M3VjJmbEJTSk52WXV3UnFMd2xVS1NQdzBHMDg1eDFVT0Y1ZGxHNk85Ym5WUWtBbkZzNzMzcDVVQ0I1N1NJMElZV3ZrT3pZR3B3NE9zNnhxSUwyVm4rVGZHa211ZmR6ZU5CYzN3Q0w2Tm03cWxYa0FSdG9XMmw2Zmo1U0xCS2d6UUdGK1M5aXQzc29oWlZ2c2EzWExvZGloL28xODdyNE1OUGduL3dlTmZyY0Z2RFdKajVnakl3b2NlR3l0Mzl6SDJzanhYd3B1cWNjM0xxdnlZZCsxTk41WVYwa0Mxd3IrMERnc2txaW1xamdRMllmRU5SRVNlVVN1a1dEdEpCYWlDSTJxSzVhNlVpVTJWUWdnZ0J5RG1mVXZORmpSSWhUOVNDdFRNUjV4RndiVXQ2cmY2KzlKUGdoNFZGckhyU0hkY1FUaG82R1NQd21WUG12ODBXcHVKSEo0Q0l3THoyRjIyZUFHSmp5OE4xcEV5N2EvT0NURFljRkJIYWhCZFcrL2NNTmNjUS9zcE1kZnQyQ1BDS1N1NDJOd3IwaUgxTkNqSG03ekRJcWlpcjlSZGpLUlhoK0k0YjU2NTZ6S1B0ZGV1RHlnK1VJYzl1RCtpWTBSekFaVUdBZWNxR0RxaDQvVCtmM0N5OHlqZ09KZ2lDbXI4ZkdDalZZU1ArU1Q1MHQwSDU5QW9TWC9XbHEwZDhNSXFONVZnRjB4SEtkUWFKUkMxdVJ0dHBuMEdPUXIwTG9TRHpHdUhHM0J6M2gvT2NUZDZyVmkzU25FYk5RSFZjMUF4ekduV25VbjlPN0NGZHhBU09peHFMSVUvWWVZS2pSM0d5a2d0UWs3UXJucVJMZUJvZzFyU0dSVkJwTG1UNS9kSzV1cFB5dHlEVFNnQ2FML0pBNkljR01ZK2EzV0FUdUlPR1NFcEMrcHhxcUxTK3c4c1l4RWJHZCs5KzQ0MWF4akg5UUpOQ0VNNm1rWHRDaklMQ3U3OERkeUlmMVJkb3dqbUNDNjRuOVduVlAyV2I5QmVhSlhkSHRzRlI0YTh0V1dGdEE3eldUK0tVQ28zaGtiSkdJZXRRMFdkbW85QmVhcERRaitadDk3dWgwWU80b2Q0eVN2TGdlcVRNdEp4UEprc0FVZDIyUUxXemVnSHFqd2FwdVlEaVRzWU5SdThRL21kUDZ4eVBDUTBENWpZeE9XaGZwcjhuZ0dFVjE1OXM2anZsYjB3SW44Sm45REZTR0RBbnR6UHVmeGQxSXdRUG14elZJT1dIWVhQWFdnMGNoY0tqMGszbjdDQTdXUEEyQ215eFFuUmplLzBBVzBhd1cxblBsR1RHdlc5Y1J5Z1RQRXVPUEpPVnlrT0hSdWprbi9EeDZyWHNFOFJFOTlxWDBxT2w3b050VDlvK3UveEFEYjNyalVqVlB4bUFRc3JVSFFOZTdrcEV4TE5DVUVaRmlmbXdVZFc2ei9VQjhRdzA4SzVBaExCbld0S0dEMmhTNStZMmJ1TktMbXR2Z214eStRRVAvMUJqYkltb3djejFKaDBLZjJ5cytaN0RjV3hhVUllZlVYUlpDVTM5alRURDNYNkhuYkdVRS9BbElqSGtJMkEyTnp5clJEWFlha3IxSGZUbVI0c2RDcTh1aWpxdG5RV3hsUjhrV1BhdUdLVjc0aW1EZjZXY1FOVHlzYnVyUVRKUHpJeWpYSGlONjV1Z0tkVUgyTm95aldZOWpHTXVUU29VdWRsOGhRdVRKNW1UWmxNdGhYRklSUmhzV29XaHJ4Ym1lWVBGeGprdEhqQ3dCd0NpVGI4M3ZsOTZxU1VkRFExUkZwQWIxdGUwZlpKNk1WVGE4aEl5Qjd4YVMzbzg2dkNGdDErVXlNaWYwN2ZhWmRYbVNic091aVYwQU5RT0lJUGEzYVYyY1Z4eHB4blpVQ3pYaUVuWW5yUFRETFM1b09qdmlIZVVaSjljRTJWNFBLMUVpYTRMclVIajN1ajdlcDZWOVlEa2FGandEOTRvOFNpUldreUswNHNhSUQ1VUNKSHJjdFoyYkNpWWNqR2dRYmdLU21CejFKbDNyUm1tc1FBYldmRHJLRTA4R2dkd3liTGtMbllUOWxCeDJQTmplOURkVVhiRFYxNzFTS3RDdVpvWnhuYWpUMlo2UmJIQ0pmRFV4MEZmWVBnbmowMnFpSlpwSlNQZ21pbXNxSUt2ZGpvNS9TOUExMjJJMTdha2x2WGdYeWxhL2JrL3FyeXlkbTh2NVQ2SDc5SUI5dm1iY0lybjg5SlBxVUhJb0lvY21CaWhkSWJxSFE1NUlZbjZmdWRkM1hvT3hiM3pxWE5rTUp5dmVHdzdHSkVuNDlNNDdMTkVuUFNDMkh5b0ZZZEp2OTF4cmUxTEMva0xyc2RCclVIZ1crZkdoSEovdVNkUWZwaXd2blpiQzd3SVA1a2FqTzhMK2UrVUI5VjlKL3VoVGlYTDZSaUgwTWU3QVdER0lXWVRPeXFyT2cwSUdVQW5MVzRRbG5zazNXeHM5R0xxRWFYbU51QzVyUDdISlFOQytNZCtOOGlCNUhldlNPNE50V1NkR3VRTVptSFpuVkxvUWpIVXF0RGJlZ01nenhBdVlmTlZlYlFWVFJJRHBvUWVhUzYwUmovaDdicS9vcVJEOFhUMytJNGQ2UkxGOWdTVVExNTMweTd2Q1IxdlZ0ZmwwREFWZVJhcStWc3F1d29jeFB1SEJPSjhML01LcDlQUS90OHpyWDhiQjQxdXJRMHpBaVYyYWdnTXFkazBabWxneUpremNMQXBLaVZMLzB6UmhjUVVFOSsyZGVka2QwcmdKTklGcHBudnIyaklIdk9zdFo0aGVrK3pFc1F2blNEUUQ2UzRETlE1SHE2MDlXZXdaZFhzMDVHQWEwOGJ6VmJidVVzQlFHV3R3SU5aZzdNeklDUXFpWi92SmUwQmpRSGR6MDFLNXhIT2ZkQ0ZwVlJBMVhkdU1MNDNNUUxVWXdVMkJiNWtTQXZTQ0wyRHhYdUxrd052TTdwcGFPK3Q1cmZhUzZ0b2J2dW9VU3pralUvOWJnenp6amI5QkM1c0x1Z2JsUnVIT2VqNFFyeWRnWXZtTElWd3dabUoyTW1KejN2YjVOVm5ybUIySjZMeE9ienRZM2hNL21EUVdWVTJWNE5laXZFejREaG4yT3JqSElqSGZqRmNLTlZNWlU2U1d4YnlEQ3pnN29jcWR1MUdJS0doMlp2Q3BGWitkeWJCLzBYdlRBZFRvT09LMDdzbnkrRVVPdEQ2VU1kdkZaZWdnWWNERVlueHlocTAxVlhEQ2Ezd0tWQ1FJVWg4cVBEWngxcGdvV0c5eDFDd0pjQ1RHVkxtSVhKaG1WbGJMbWlJOGlXMGVqcWhnYk1oUlNLeVhNbGZiR0tUOVI3K2FtMnliL1dMQjFwL3FhVlRLNlF3blNrNGNyRGdwdWNRdEhiTDdYbFl0a0xLaWxLSDJ3ZEdTNW52b0RSWHhiZTh1RGIxRmtqcXZxSTlQR1p1MU10S3RnVHU5enp6d1ptWlJzd2Q1dG9HZUV5aWhjWEpmY2M4eWdoMGExR0dlVGhSUmg2dzJpOGJBR05iRHREKzh6a25TdlpJbFVCWUxxVGZTR1JRR3ZFbmwzUlhadnpHUG53L3FWS1paMHBvRWJEb3ZkY2E0KzZMZ1B4WThmQVhKRVN0dW9lc2F5V0dHTmd4QVlIVWhPZEdBTzcyRkpIak5LUWg4QWFyK0tZWmprbkJENllPd2RoWWdVOUh2dkFUVUtaMVUwQWMwL2RRT0krcmh5WEVXSy84ekIwdE0xb0x0ZFZPbnk0RjFHSFBzdjc4NnhwUGFlcUZ0VFdKNEdmK2VVSDlwWjBreW14VlVZcnlKQjVKT0ZIRkNWMHgycUpnZDl2RUVKWlBkVzNOajM2bTVmS1VORWliNlpXNkRRY0kyMGNPSlJLRmEzcEVGdHF3ZHlGSXl3VUhxbmdCT1NXZmxKcklCSjJNcDlaSFJia291djRyU3JGR3R3YkV4SjJDUXBCUGZLSzlyYTBhcVBDN2RhcC8xV1p2M2hYMzFmdDZhT2lEK2RCS2FFOElEZU9WOVZJTk9sNmRZRDVicHNrZFFnZ25tREVXY1B4SFdVZ2w2akdLRGZ5UlBDbXhSVkVKd1JlNUxndTVOajN6MnRJWElvZURWZTZibkpzeFNZOUtSSlpVdTBFclpvQk1VaTV6Zm8yU0hVTXczQWszZXk5VUR5d3F4YWsrTzh6SmxhWDE0ZjkyUGdadE1VTFpVYjZiZXJURTlzVXFOZVdwVCtreGZhSXNwYmJlUDdaUlE3MFNWN0VGNjI3MHpmcExXNmtRRGFLNWU4MHFmRXZUcXllckR4VGswcHQwS2V6K0dnZFdERzUwRVJJMUVKaWIwZS9OdWFzLzVHNVAvY1p6Q2VIaXBlbVVqb2FLRS8wWTNSbGpSMzNjdUo1TUtNRHQ4ZVpicVF3aElMUFNWbE85cXpyZG1aVkhzdFpTTmxVdzlDZGdwdW9MRkxtRXM0TFltb1FpN08zRjBkR2YzbkdpTkhacjJHdjk3OWcreloxaVdPTGhqL0gwLzJBc3ptd1dqWFRLdEVXSSt1QkpvTTkxZXdqQlIyUlBhUDdta2hCMlpsZ1AzUWovZkVTVUFvdG4yV0JELzNNekhSUmUvbGlUM3Vib3M0UlBURGs0NzE1blVnTncycVV6WS9jTE40VENpdXlRaitRc3h6WlMwcjNNUERJL1FVU0l5VWFRR0dxNUZZakpTTWV6Z3FhRDluM3RQYXN0SVBxVEQ0MmIvMlNTU05qbTIreFd3MklUQnVRdFk1alpidy9BTyszL2ROWWR6ZkZEQ3NrTkZBY29Ta09KMWNjVTJVTis3NHpONFJIUmZjYUwwaFNyYVJUR0lJMEJDdGNXSEt0QnNMMUMySTdzenc3cm5KUVNnSlVhdjFueFUwR3JMbCtRdVJkMk9rSVMvanVIZXk5RFBWVktHUDd6TldTSzF3OUFVb21jMnhCMFB6RTlQem16akRUbWJ1SUcyK1dOdFc0NzhFUTVEeml4VHN0TUxKMExyckhpNGFLQzBFeCtpTTVjNllIUEYrNHNuM3MwcXR2dkdGbk43OEF6aFJxNDZob3lGTVFuV05KTjhFTnJBeEZKV0EyRmVJQmdFUGp1L2t1ZTY2Smw0SFY2OS9ldGgvMFhNbHpvRUYyNy9qTGI2akNCY3NDWHlZS0xpcE9PTkc0WjNiN0tJOWc4QmNnbThoTTQ2ZDA0NkJIQ2MyMmxwNnhmdEdxT0dFdkVsc0hJSDkyU2cwRlhZYVJjbGx3b1NnY0JOWENhREl0SFVpbTdpbkVkN2RnUGlORjdSejJsM2cxeTI3ODFTMmplcmQ0dldMSVNTd2s4V1BCUFBoRGNqdyt6WmhZN0c1aVFyV05FSkVuTDI2WjNpZk9DeVNmQ0lGamxnUWVqSmVtbnhtNVJPSmNabnppbGtuc2p3aUNibmJaTERwWHEvVW4vUnNDRFFOK0xRVWp4THNraGRoQ0thcDhVVzRlaTZKek5IVStQYURNNmJXdVI2dExpZ24wcUFvbmN3MUY4RTVsbW9zbjhvaGlyVEFIbG03c1d1Sm1FSDR6dWQxbHdlNDI4UTlwV21PV3AyZ01TZ1YvRFhyZWlGTU1hNTVNa3JHU1lSWVpWeUpNbDFpMTBzZmdBbytnOFdDa2dHbkVib3JtVGQvdWlSNDVlVkVKODYyL013bGRZejhLODJLQ0dHNmdDeW0vYkFnNEFUVnlmWjMrUlFQK0RBTU1tUzVCMEhTSEkrem1WcytVU1hocnppd21HWkFLaE0zU3VBQzJWT0Y5RVpuWkluMlhaOEI2dU53enZqaStpOXBOcG1iaE96UkdoOVVsUTZuejRuaXhJOWx6MWJFbnNxZ241dTV4eGVFL3MybFhVSHFndEZHM2dwRWVjdTRBb0QwWERub2tBSEFqaFlTZzJRWlE0QVJXeGg3UU5XOGZHb1RRWElqejZmUGFwMzRSQVVzTGNxTjk0OGlGTHBORnQrS2ZPcFF3R3NhUk9EUnpJZEhVeHpwQjBaMGljclhWc3RpRk1Yd3cweTZqdG51dms3L1ZGOWNjUGE4MjBPeFJyZzJzSlM0ZGtaV2lQTVFwS0hMb2J2cEdmbnRVMisyWnNLUVErZE1xVkw0RFpkQjZTb20rYTdhZGh1QkxwTjI0R3pmVzdjY0Vrenl1ejAzN3IxcnUyL3pIOEVRWWI3V0FMME55dUlVcWQyUVhxQ2ptZmJ3V001TDVBMUNKcnNTN2pIRlZrd0FWdC9xTTl0UHc3cGFONWlnZmc3bkxaeHRBOHYrRG1VazFGNUR3WU8xaHp4NnBCbmUzREtSTUVWQlFhRjd0dU9uZDRVU2M5cTJieG5LUmhpVXFvY2FjZFNwVTNZeHlhaW4wS2RPMnVCSEQ0THFlaVg5UVlrS09lRHlYemN5WDlXbUZ0TjJvWU0xYWdEeWJjZzIzbXoyeHZ5cTh1WXh5NFRrN25kMmlZYXZsWStQcEFaSDRWVUFJYVJLRXpMT2dPOWJ4ZWVveVhnVnk4UTlaVUx2emdjTXhSNzVOOThtMDRJRUUycForYnRtUmMwclozREl1V1QyS1RsdXExano0ZW9KOVA2WVg3VFBqWHJTUjhNQzhKUTNqOTNSa1hkTFAvVWsxRTRYMkg5SURwb3F6WGZPdzE3ZW52N1g2VFB4dHB0KzhkSVVBNFJaOHI2MnIzYzNSS05kRFZLeGZRbXprMlROUS9Rc0dHeThZQldIem5aY3NNU2htSUovWkhEcjdnS0RubWExMjNYM05JMDNJdTl1VWlLUEZDd0R3SlcvWEtQYlQrWnR4VDBpUittWVV4dVRoWGVwMEU4UHorSzcvMnJJeUZ0RDNWN3FDM1p3NGgxWVo0ZzBncnQyV1ZIeGxWeG5uMDdoZThBcnNSbGNRbWh5ZVdXMnpEc1ZOQllSbEpUSlJNTmNFcDhxQ21ReERQRExMeW5mT1ljRXlRQXc2Ny9RUmlWRU11WFlYSDV1QnBmN3hpaEVXUFlKaTZ2d1YvZHAybVN0aUJkaldLSWpIZzlla2g2b2VadlY2OFF3YmU1RDVBb3RFZWJoaU9yVXlmUXMrdHZVVDZ2VGNVc1ovQThqM3VTa21Td2tlVFpGS0QwWldpQ3MrTVFUQ2oxZXdTZVlmSW5NWTZSTnVKbVZXcENzYUhYNXNlWFJSZjdVZGlCSXQ5VkNnZFhKa1E2OCtHazkvSWpRSlJBTEFJZXliMGhYOVdMQ1V1UUpsL1RXOWl2OXJ3em5GUUo0SE00Z1RmWFRPOXVVcEQyQ1NKTWZQMy94RUx2V1RwZkFlcWduM1VGemEwL3dlc1hmNVFSOXNjb0tIMndsWUNPRTg3MytmcG1JRHhrU25ySVhRd04wZlZ5Mmx4YlFnU0taM3lRR0ozUTJxUTQ5QlFjc0lQb2MxRVVUcExGUUFReHptdHd4d2pyYTJTL2xZR3Jjd0JLcmFpVWI1M1JXSm5IQWUxSXk2SkJaZThlMHpXZWc4Ylp1ODhlU000YmQydmFiWktyU0l4bWxob2NyMXFWbkd0bTdzQ2Y5c21FSEF3aDFNTnJCTlRMK2g0QjhjdVRyejlIM0lxd3dQK2VzQkEwS2lFQ2tkOG42MmFWc0dvb0x2OERFWVhUMWlTSU9ZREF6MFJxakdJMVZDVHc0c1dyUk1Ydis3ZWVoa3JHYk9oek5pa3Jta0FLTHE3VFNzMWcrcVJuNFFsZnJzV3NyNU1lSDYrRFRaVjFnNEFZaGlrYXh5VjkrNXVmSjFIMnIzeTREbm5SZEg3UHFMK3EwVmVIUHVLMndmSG9SRnZ2YjBONExjNFRlUDdQWHZTM0M4ZWphRktoeTRobDRyS3NiZjhQRzVNeE10NDB3SU5ZNTE4d0dlRUpDVlV4UGpUT1kvSWZsb0cvWXQ3UzFnZ0gxOGtVNW02Y2Q0R0lMaGoybWNtK0pCdDA3elEwdFZhQ0c4Wng4WGNMalpwdndUVGN0c3Z3K0JhMDR3T21Sa0VZY1RPT2cycmh1NlRYbmpRcEIzQkFYbmJockZxREw3MFdBV1RZWGpHcUM3akJMTWpzSndtMjBBNDM4Z3NjbnBBNDJTT1ZWQXEvby9TZHRBVUV0N3lQVWZGaWtheUo3cUZpYnptUjIzZmN4bm5PS21vT0Y4dGZ5ODZCU2oyaEN0UjMvZGdXa290eUVvV2FoMFZMdzJBbkFRdHVBdGFlRXNVZ2xtcmxuTjJ3S0ZZeFVFRFpVa0ZWTnVtbWdBT0xxcFRVUTFFeEdXZDlFa3JlYUVhN1ZKZWNQR0Zud3JvYzFMZFFOdUNtMno3RllpQjM1M21JS2dydFMxYXVkWVlTYWsrcVliUVZqUWluZjhJTG9iQ1JEU2IwZStDeTJUZ3lkdUNIaHQwSUlLTjJCRE1hK05pRXpnT2pRK0V6MVpRbG9SZ2piNVN4VkpodDA1RjV2eVlKdWlmYWpYampFczg1RnhVMjZ3OHpwcDJkK0RGZ0FlS2VCb1JDUGZGcWhiRVY3ZkVMcnFmbitKaCtaNnlHTHdaQ2dvQ2NhUGxBZFhoWXZPOTZPVVplTUp2bG9hMjFycUh5akREdEhLRUxMZU0wSG9pMStURm4xbGNnaVplUVZ5NEIydWhtT2YwT05PeVRXRi9yWVVsQ0c4MUZIRXllTTU3cU1zbVlVTWJmeDJNUm92ZVlQbXVsa2Rya1YzTVh1NlN4RFVTVEk5UkN5eVZuYnZPRU9LZUVrcElwVmhNRFZMUENsaFV3TCsyMlpXWkF1K2pBOVVQYTBPSGhSeEFTeU9hWlYzc1ltWTdtd0t0WnlpNWVtclRkYWZ6TzFWTGF0VTlva2w3VmlNMzZ1ZDlUdUlXL2FBTU1rMDhBekpPMUFoT1FDN3Z0VXUrVVhZMXJNc2JTVG9BUk4zaTM4T1R1T1U1UXBzWm1GMzZKb0VNckErU0QyaGpSdy9McUw5T3A2OEZEemJSVTZackdqZlBaS29xZlpENVM1RG9IV0dtUG9CU0JKUkZJVlhEVEVURFF0RTY1M21nU2I4aGpiWUpaOCtCb2VYL01vQzZPd3dHS1lhcVJJakJ2SDR1NEFGSnV1TXhhZE80YkdFYkNmbW1NYlJ6LzBOSVpCVXpYNnJCTEhBbGpIUFdkcFJ1dTNLMEpqbTFLRzhxTU9TYm1udS90ZHhXOXQySHQxTVpvNEVKQ0NSRHA1TTZIZThSZXVFdk1PdThtVUtKSGcxMEpBd21ROE5vYlZNVjdSem5JVS9GRThtcWlQZzhaNUR3NTFFK2JlMytDTFI4K0daeElORG1pZTAxdklWKzdJY2I5bEFjZHg1emYzTmlNZ2Z5Z1ZOK3pMaVltUldjNUlWalIraThJSTdleFdibHFraXFyTG1ydzRKWVdBanlkb2VvWmVnRkk1QXBRdGdSRjVMVkxjbUpHNXNKTk5yVTl1cERhcVNmRFRZQ2U3VldBS3MrWmlHb0pCbkhJcUZOV29TYnhHdjZYUXdVV2NLbno5TnZHakR1YnRYcVJjTkxpNzV0Wnpxb1hqMU50SlBtMDJHU1Z4R3p1SkxJRGxXeXBEenBIT0VaMDJxM2ZKbEZzZHR6QVliODFHU0YzcE93V1VLbk5vblR4QTB2R0Z6QSt1by9rb01QY2lMVG5NVHpadW5rSXFySlZTckIra1FWNXJqZmVOdTU4Y0JocW9EYXNCa0JSTTFDanlrVUY5MFluS1dINkROSUUwbWZlb25BblVQK2FGQUNZZU4rQmFWd3RsSUFuM0sybUFsamVUMnpFOEswUTBQbXBCRERIejRxcnI0dXNNYmwra3lCZjJtZjNuZ0MyOFpzMWtNZmVkYlNUSUh3T0VMdG94bDlKUG1jN21hemRVeEpjeHZSVTZpZHVDSGZnY0xuYmUwMUNPYlFVb3Jlcm1DWEtDWlFRQWNjNUlQTXdnS1lBZmFEcTEyR3h6ZnJVQnFPN0JLSVhDVU1YK0JBTnArZVAvNmlwMndyNXRWVFNXdWRISk5QS0NJR0JIQ0tnNjdnbFNIMXRxcnFzWnJvZlNkakx1NWlGUTFVcmxEZ2c5bE0wOXdBZ0kzUERiRHNKbWNQTXAzenJuYkxGbTRrYWc3WDhPQXZnZTNPdDMvem5oa2VQN2tDbzVPZE9PUFRXeEJYbDhmeWxYVnZqdjdPcEVnZ2JWWkFyWGlNM2RjNW1tV0kwZXlDR25MVlRYMmJMS3M4dU5CNy95Zm10T3A2WTBhTm5Nb1ZqakdITENTSDNTQzJJM1l2enRhVm1BUUNRQmZhTFgvNUpXb2h4aDZRLzNYUHdLODQ2cEJWeS9MbEpTMmdibVBERFdsU29aSzRZU2QvdmUzOWlkTzljM1c0V1V4NU91NHJQWjNSbEZYOVB5VnA0WXBFdVpKL1RpczZyRUNzUENQQTRKWW94Rk9nWDM3cHlaY3lOZDZQUTUrZEZ0WWgzWGdERlg4ZDhSRlV6a09ndldZNzNocFg5RUJIWFlYVWNCUXhPdzZjRUZIUGd2V3BTSzFOTjA4WkpSZm1udHNOdWw3ZlFQRVRhZnRwd0FTV00zdUcxUUFSdHE2U203TFNSV2FzMy9tODVER0VCUUhpSzV2eUppSXR5ZzgyZHpOcFZGbWVJSXJHYmhlTjNwTERweHpoU0svQ3dhRFBkZ1lYS0k2VW4wUExjdGVmSzVqMC8xN21Va05sRVo0RTlWMUlEYlpCYWw2VTl4YmIvM2pkRU45RnpqVFdIclFhZU9td2YrYXFMVHlRejFQVkdEOEVvS29TQUpWSjVKVDA1djFxdlF0aEtlSjVxSUhFZFNtazhpdVRJUW5UbGxzcFJKVWczbm0zalhOQXlIN0IxK0s0YWQwSGlOcDluY0xLNFZrV0NLMEx2dWlqSExmWW1NNDdER2xZbDIxdmJNS3AyMHdBcUt0WG1wdjNWeUJXNk1MNDZOTkdXNkU5QlhsR1pqbncwdnp3VnRXaHI4Vkw0bzZTUWZ2WlpWRjZyM0dIbnRVWGYrbjIvdExKMUFZZ3NtY1NhQ0xYWmlBZ1ZQM1M1OE1jd2g2WUJGbVhlK29LTW9uVUlUUzdLMS9zK1BYZkliZlBJSXpES2dVbXlmYThSMnZ0cnVobjUvOWJDOERrMThkZFUxeFJmc00wY1krc0kyUmhaRXlZb01PdHg1V1l4TVFUZCs2WXhGc05zSGRXSC9wbTlRLzBmVVdiQ3BLS0Z0QngvdVlNOGJUYWYrclpKdlZMTEdpM3k4UDVVb2xIWU9mcklXRDNGRUVqNno1YUVQc0lyM1VYYjNqL1dhVkFobFR0eklMQlNpZmd2M1JXcmk0K0VtNkNkcU9FWVpqc0diUDJOTTFWNmFNQnBmdHhNSnVQdnVVS09scXVJSU5nY2ZsZ2dteHRVV2Q2Tkp3K01hNVgzb0lKaEkvRDRReHRYdTRlYmdGeS9zYTdrU0ZoTlBHR2Y3VzVRQ1hoUndQaHlYNWpJRGQzMGRpQ3h5YnB4ZGpjTGFlcTAwdVdocERvMnF3RkszeTMvQVQwT3B1RzlHZ09XWC8xS090OEN3azN5MWcrRmZPaVFnaXNhVFRFbXlGT3FOVXBlcC82WTNOdE5vZ2c4ZGtJNmhpd2c3YVNDTnJXOWY0WnlSd0l4Ty9uMXdjeEV4TnB1SDgyWVl0M29iVURhbW9jZ0RWa2k1RDBGTWZMT3RWMFJjMVl3UVdMSnZidGt2VktrZ2J1R1NLaHYzQ1NhMjFKdkNFdjBjQkF1Q01tMFE2RUhlQjl2V0cvYk5lTFhTNUR4QmVqNzlaVVZibUxrUk9mK3g4T2d2b2w5TVFMeGtEcDFNUkJ3SjI3R0pickNiZk5wN3RqZlJRZWw5dFJwY2o0N2FMdmNKTVU1NUpnQVpEVXdleHc2Tk1VODRNNU4vd2RxZXYvVmFTRnRpME40MGxYbDM0b0oxNzlaZU4ySzQ5L3puZHNhRDA0TVdLNXRsZUJOMzZ3WWZMV3dmV3FSc2pGWXpwb0dDS096ZjlvcjRnWkFXM3o0TWhHd0RROXpPUXZrUHNqOWtmVkgxeDduckFWTUNSMGVMaGRYTUZnTzlOcWZIajNKcTNxb3hsaDhvbzJwWUFNWlVHUS9HcGtPZktBUjBENGp0a1pLd01WS0dGei9CSGZ1QWVQMFBhSXZzdE90VWJ2SEpoYkV3dzRYVTVYZXkrcHg2clNJdTltV2w1R1AzVHZ0R2xnMEg1d1AxMUxEODFlbHcxVnJYTGhwQzVLaTZYbWh1Z29BaFp3b0J4cHRzTXJKM2NXTjFMZ2RrTGp6UWw2WnpzYXplWkVBN2NGRDBPOU9mY3l1TzNjcUcyWmRoWEJ2enBSTGQwY0ZTME1uMHRRK0dtcnk2Zm0weFdicmhXbnNrd09FRmtZeHBUV3ptNWdyZ3VVZmhKOHJNSE40dnlrV0l3STQ2SzVQRDZybzVHNzlnTWhtakN6ZmQyT2pZMnE3TWFxbnJoYUVzRGl1NFFybGsvUnI1YXNYRDAza3p0QWZSdm9MaGN6ZWFSWWRHM2dsQ3JBZ1gzN1FjQnh0NVhxTzM3M0hwMFB6MDhnMjd0SXdnSFgzdmZyVk02aGJiQlA5bklybjJRcTVxL0lsT0w3VkpuQmE3Q2xuWmx2N2FmdlBkVzJuRmVVRmgzVHNGd3QwbUxvOUtvOWJiK2hPeDlvcnFKYVF4bk9tNitUMlQvM1k3OU1iZC9OTzk3dHhvUzdNWHN1ZDNWZ1c0NWdmSFdzUzlkRktOMmgzdFhKakJ1MWdOYUlvYTlEaThiQ3dDUjV6UGM0SlJIS0UyMUprUERZdHc5QWlpVjBHUkszVXRZc0lOc3JDZ2NaRHVyUnp6dlEzNURyaVRIR1ZxL0JXbFVyMVh6YWIvaG9vcVdnRm1pamxzUFFEaXZ1d1VzTU0zTWIrUU9rZ1NvVEtqRWk3RW8rY1ltdHF2WlBRQUYza1VqWTZldEhFTDNQR0JmVHVhY1dxaXNwdDBpd0dVdzNaVm5UVUUxY3hIZ29lczJOVkorWkM3dlkxSTkzWVlZaFpxRVNXMnk0ZlEyMXc1ekU4QzJNcGNJRzRuK0RXek9EQml0N0hqNVp1ZVR3dXZCbkhCbU5rY0lHSnluS3ZINEkxVThyVjAyVUJyL3hmODl0VFRWY1c4ZWVJMkRtQlE3T3FzWnZuK0NwTmtiRXRMclR6UWhUY0lYU3dhbEpMKytZeGl3Mjc2ZkFLRm85TVBTVmNTOXVvL2xobTVUZ2pVN2xhQllFVE5hZkJ5djZsZDczelNEakhkUFM3a2Jzd0p4OUxvSGNTMTVvQkRMZVFhYkIxYTQ0M2hWam85RUloT2Vsd09TUmdYcW93eFFPTTQ1YnRLTit3czdNUjdndmtOakVIdVRFMnhrVDJmblU2MDRjL0ozd3RSQ2QrbFJYU0p0dmp3VXdGeE1jR05mc0hRd1pRQmIvVy9heFdYeGF5cUwzMXVMa2RZU3p2cEhQY2dpc1dqYkZIdUF2Rkg2QUVrbnlJMFI5N3dadHhWWDBRWWlubXBLRFRhSlV3UUFnYXQ2OUFVUldsQ0hIaDNCWU0yekYvRU5heGJBWjA3cWJ4cmtDMjV0bHdYN2FpZUlpVGNjRDRWcjNpTkFpdkszTjJKUSt4VGpFdlRXM3JhejN3Zm9zR3I5eTZHanF3WFZJMlMwekVML2cvdjlpV2hGTWJ2Y1NaeHNKamcvWERXWWdKQnlDbXNpUGNqTi9lTW05NUpVNng4aVlqZlhRY2gvczV1OGpraHlNKzV0Y2YxWnRVekdOS2I5RHRsS3lwclBqb2xKaENwNmwwa3A3QXIvNElycERVS1VnRkpoYWdXeUpMVHJDKzFGMU8yVEpvdUk3WEtrVlc3dlpKSzdaSHNZOGM0Njk4S252d2hqM1N0OFhjeHpSSjhSeHhTUFB0SFVzQTdRaWRLKytUWExuRXdsMHdxVjROZjNaWmxSdElpdFZyYnI2cVpqTjBlSzY4VXQvaVJIUUFEU2FoOWJXZDc5MVczMmVudWhkTEU3VmRPMDV4cXA4RFB5UmhMc2VOMzVSRElnU2FrK2pIUnFoU2JDK3Jud0J4Yi9iTDRmS0x4WWlyaXBOL0ErKzljSnRQbGVWRm1jUkRjR3d0eUE0WklpbzcwdUJDWFYwZTU5OXBKb2xBSEI2Q0VpR01QSWpqdGpGandEem5rTWN4UGhRU2FnZGQweXkza2hKTEo5SnV0TUpKNURBYURTcXJxZWY5bDVvVkhMNVl4N3FiMDRjMXhPVmhrYlJRVzdOZkppeGEyc2ErWWN5OGVETXFLNzZ2eVNBZ0wwZUZHcTZkajBndk1GVHEzMTNROGJuMzUyMXlCSlRCZkhvd0dNNUNURWdGeitLWi9BNFJCN0hwV1k1TnFkNk9IbTNOTDExZjNQeDhPQnBNMldkbW5IRzBJdFJRTFFvN09acWJpdlNybXVKditYQmdiZlIwaWRqaU8xMFJUWXlNV29GK0RUbzRUb2hNWHl3NEVOK2tPc2duVzIyL2hkNi9Dem14c2Q1QTdENGR3VjBmdE5tTkh1MEJSWC9IVitjcnVBd1BZUmo0c25iSW9YK2VOeElVYXlrcE5kbDJ1dWVkSGsyVzlHdGM4TmN4WkxWdU5pTDZLNm0yQXdkWEFpL0IxQVdiWGFCUDVzcXhOSklNMU5pbEpyOFJ3SFFCRFpFVEVBcnhHVWlVa2RrZmo0WjJVNCs0ODJoQ1NzYkRWV3gyY1pad21EZlQrTThlZGJjdEpzTWxjekkxbU5jbGdZaUxDSDdYekQrayt0L2FMeUx1Z1lWaU9DWWxLUiszS0h0VUx0RjhRdVFOQ1hyYjBkTG1jWllkVyswN1BtUGVveDRna002R1dFS1JKZFlON0lkY2d4aEFwVHJCNjVhN2JUSDlzMVh4d2M3aHhpRE1IMmdaOUl1em4weWl2eWk2OVRJR1J6QlRDZDFEd2JkdjJURlM5MFJlMGVDTGFyUG1aVWwvb0hUWkxIeUw5eEZYNkx6dmh2T1V5bDZtMlFDUXFCUEFBMzB2OW9iTExTbXpwMnMrTU1FdllZdTdwZHozVndTSDdrQktQazZLL0N5K2pvM2dKUlhlMUJQeW5qU3MrZkpoYzJPL29sck5oL2JDQUNnVUFsV2xZbmNTRTJQdHJoOGIxVGV3eHhHOHdraXViQ0VYRk9NV3RJeXFzcFhaa3VpL3ZqSUR5eWx3R21wckV2dkdwM3paWit3RlNJWEpXRGpXUHlZNHBOWEdWaTdtN0xSQ1o4MWswdFRMY0ppQmVCakZvMzk5ZnFFaTRRKzg1enh5S203YXZ4VEJsVDMwUVU2Q2F1MFBvbEFXWnFVTEpCaWk3b3BDbXpVMm1yT3ZIRHFuOHJGNzFnc2NsVXkxWnduY1NvS1FsdmJZcGpzcEt4b0kyMDJHYkUzL3JlQ09HK0NvazlucFpBR2Q4cEMwL2p6U29hekU4OHJNZHRKM1BWdnUzK2FnRmx5VVdHV083V1VxSUUvWUUzNTBNQ011Q1RMNmZpU2U2Vkw1TS81V09Qckp0MEpDcGFSeW11WGo3RTVuWGZ4NUFoTWdienZWK3U5dDhhZlNNWVE3S3NSWFZKNEt1eThIYkZkamVBcWkxTnVhZm9SYmhTR21HNjlkbnFQdzhCcktVRUR4N1lTaWl4RTlpVERyc25UeHpkbXc4UEhHNGdDMXNPRnhuenZyZE8xSS9UdEN4NXhyOEd4dm5zTjI2YnhzNHdWZmxmalROYU1RYUNOT2R1THZPbWE5bnpzMEVla2lhZ25rd1FnWG9GZDZESzVzMHZnZllqYjh2RkpHRmpsUUpNSDQ3bk41eXZqWkVFUkJRcmF3bTFXMFlxamRsckR6ZE5yVm9kend3NW1ZcVRGV1FuODdZOEhvMzNwV3c4ZDFycWp5WVY0aUd5SC9peS8yVERaaXV0ekc5NDg2aDVDOVlkUTlBYmJFa2ZISW5qbTZLQ0VUdmJLRnVzL0VhY3RFT2FoczlBbmlkNjRaTFRqNmFXejVIRTFNdXFZSCs4SzloczhOYlBvNWNtWkNGOFU0d2MrVzU3Z3ArME0zZDJlbG5WUWtNYTZuN1VaM0pBMTlKeUtvL3JEL2cyK1NpZnVQSmROV0N2QXRSdHdGRjkvNzJJWkNQN0RhWFBJaG9FMXFCOUJ1NW9aOUhYdzR2U0hMNFBtVEN0NHdzZS9rTElFOFByRXdSNkxCNUVkNzdiR1NoWU9uREtjRkdBcmk3QUtYSXdpMCttdy94QXFpckdDeTJ3ZmxJMWN4V1I2cUtpdzRFcGxaSEorSFIvYTRSUW1jQmdLL1JWcC9VMEFYOFpaa2JiSGNjbk0yRkw5bWIzZ2xQbVFlOFJ4SEh4bWFYM2t1NkxDZzZyL0JGR1dxWkZpZjRIOWREZmJjcUdaVFJXUjl0SGNnQ1IzNUQzai8yWTVkcm1BWnFMR3F4Q2w3Wk8wbTZ4UWdjZHB2OEN2UEs3SW9sYXNVKzc5ZGVHUllPMEhEY211bU56S1UvQzl3Sy9LMlJJNllsY0RtdGY5YzVLTEh2YWZTNkowK2xvR0tCMFJtcnpsN1RqWXg5bnhKNmk4UUtwNE5WaXJkZnFFZnBYckdsWnF1M0RDYTY2a0RrckhZaGY0N0VIbmZnbXgwQ25GczdWUFROUUZYVVo4SUtkZnJtQzdPM25DK1l2bk1PMFFaTE9nTk9UTTNORHRoN2xsVVkrQU5Xd1BOZE84eW5zazQ2eDBxdUl0azNWcHFuT2ZiY0IvT3ZsVnNjZnc0bnZOS2EzRUlKWEloejZTYkJjSEFQMnRVR1ExSDZHUU9wdzhoSGFscVgwYUg0Y2tvRGpDZndrS0x1MDNuSTlMYU5YZ2N0VGRoT2N4WnlwQUN1U1FuekxlY1k0ZlltaXRzclZmaEwwdUY0THIrTXJYTDZocStxMjBUQmQwMUpsU05nN2RGb2lPdi85L0F0ekk0a1JuSnIxNmk4enk0Z1g3U0pwQWsxMzJkclhITE1lckZuSDBlYkpBNm80THRVbTk4ZU00TFBXS1ZmcC9maHJNS2NYRmNPQUZtV3lZL3VFcmZZcUlPYks1Zm10Z1R5NzJtNTVsZ1BSZUQ0WkRFamZUNGUwVHBRS2FYOHJVWkFtM2h0R3RJbGtlTG8xVWx6YnpjVGpyRkRuSWFrbHNIU0c4RldvUW9vSUhsV0o5cGFoZGozZHJpb1Y0UUpxUUlMdDFSQUcyZStveWdkeUp1d2dNWU1sRnhITGIwTjN2ZTFCbFVyUXBwRE5zdnBkUXgzU29McHo0UW8wZVRzeldsNjlkSjFhTEg3bG1XcnY4TElxZk5vc3NrVnQwdlUxRzNQaHg1cHlWUWRqek02VU53a3hjeG5CcXE0YWtaRmVYaVluWjFUL3FLMzAyYVFGeTRvVDJoSktaQ0xWUDRVZEkvQmVsVmNKQWwybSs1bVd5QzVPVFMyYTFjVTVvMGVxNFJyQ3JLREJjRW1rODJuQ2RhT2JPejcwMTRENi9sbGhwMytXUU1VR0pZUTBzZzhRcjI4RGFtNU9rR2lMZ0xVM3NKdVZITjZuczJmbEpIRHF1NytYcUlnMnF4UWhtRUk1a3ZlUHhqUkVRVHUrWEllK0pYdjNvVHZkd2I2R0lIYTJ0YTM3RDBzalhoUVUrV3NDLysyTDVLaGZ5NVJUNmhjVkNBaWdGa0JZa21XeWdrM0FGOXJQeWRvSFFyVWRzTmhRei9pbUE5SStTQ2RZTGxxN1g1bGp5SmhpNkpQQ2hNaHk4UWNHSnkrWUJrc3gzd0RvZGgydG9JenVjYzRwTVFMajVwSlNYaHJDZTBqQjB5WlNkL21HSEw4UXlTcm5aZ2ZJb0FQZmZCTkxXYmo4dGNHc1hDMDRGV0FJYlpVbmdaQUtwMjNERllEb3FrMjd5dXBsRzEyeWM4Tkg4T1hMQmhINWNRVWF3anpqN2cxRk9lUmNkRlY5VVh0b25yS3F2UmtiNEdveE40S0FTRHlDaUFMMzM4S3dUM2dkaUw1SDF6MEZZazZvaFlZUlU0b2R4ajJKaGxjMGF3UEh6eVpEcXdiS1lzUUR1Q3pUUHpXSURhTzdtNFZGeEYveDY2aVlLbmM4YU1xWlVkY25mTnhCTVBSZ0RDZ1NTcVlMNXlEdDVWejNTd3VDMXRONkJkNlhlWmo1a20zNm9XYVgvSEM3YWF4bGlnWU00YUZRdnVvMmZXYlNldjlydFlJb0RVMkFHU29lbHAyNzRvbkpCeXVBOUZpZnNDMlp1RytnNU9FaU1pVXEzL3ZOeHMySmlmcjJTaG9kVTllOGltcDRqYnplNEE0Qmg4SytaSWRaSC9Hb0ZUa0J0cTBXTHc4RkhhRFVaSXZXQ21HSGplVUZJaS9PWktibkR0M09OcFhVaGJmRmFuUHV3c2V6TjRUTTU4blZhQlFDRTd2Qm5ibTNHVldnUC9JdURhbU5BcmhCcEt4Z0cxZWNFM3c0cjFtajhQb05FK0M0d1NnWGZsQWwvUTQ4dWUzUXJ6OXYrRWEzbmZMSC9RUlczZ2ZoTVBqSkRQcTB2SmIvbXBWUkZyenpGaFBBaHhTcTJzOHkwV2NIenFGS1hQYnNFdE1taEl2ZENOVjFiWmh4NmVpWlI2bFBoV0lsWjhGVGcxdE9uOEROQkJwRHU3SWllNGN1ZWtJcWVQOE03dDZhVkp0STR3Nm5LSnF0RlMwSWlkMTIrelNLZ2Z1WW5UbTl3RlovbDNwckNtVklHZThmaEhsb25iWmNPU3FZQjN5M3kzejh2V0xad0tYa1dKMEJyUnM4YTBQNis3M2Nnb1JQN3hJM2tzWWsxYStoRWV3SGprZllKdlZ5ZEFkOW5NMlRWMXk0Z3F3S1czUmJzTGhSaTNTUTkrU0VGVkYxa0JZZE9kL0ZHTm5KVENEWFdENWJiMG9sVW5ob1M5K3BEODRNa2RpUjJOU3hjRWNDQmI1VUI1U0xwRDlQbC9UVENlME80OWNnVUdvSEtIY1hIR0Q4MVJnaWxaWmorZC9RcnF4UjN1cnM0MUhkTDhPTGg2SkRPVDk5UDhrTjBmeENsWjdOcUVZbkJnaDVDTWxqVVh3TWlRd21aTnJSdEpmbXloZUZrWFhla08rUmRLWlkrRHBwNUdwbW5kT1l3UklxTlFXOWU1elVjelB0ajZwTSthZnVRa0NIT1BQa0RGWVFqTloxV2FTZzFEWEdmeUxsLzBtdmtvTzVFNEJkQlJ2U2p1K3N1RWpNODJSMXhjRkRtZDF6M1R3WEhhbnRac3BpSndjUUZHYUlzbVFxOC9EWXk5d1N5KzBRcVB3SkRkcXM3TGtBNW9reExyMDUvTldLeCtXaGZwVEdhVVA2d25QS2wwdzBCbThKNTRuSUUxZGUvQTNaYUF2dzQ2clcxS084TklLZmJ2dXBRNlNGY3pmNy9IUTlWYithZm9QbWZlNUdPUlBFcDg2K2ZYR0RoQ2hOMlIxTFBsOHQ5RFJzQWpmMEd1eVRQNHZQbE1zL0FQS3Z3UVhxM3Z2dXRMVGFZUnlxdldZVkxNZm5iSW04dC95cjIyWFBsRUJsZENWc1YrL2RxSDFTN3NldjNIZGNxUnFpWTlhRUNmSWpQWXVTa01FNE9ha2VZZkR6TzdqR05RalhrbERWa1lsODlhc2J1bXdQNGlYU0tqSjFCYmVOWDJGdjMyOER1VERlYmF4OUF2NEZ6d2l4M253ek1IbVpuVUJMVmtmZDRGeVVCaUs5T1AyYlZXNldEblNCeXl3NEtSZWxTR0JyQ3V0VHJtNzlEbno5OGQyb1hQamVlVkp4ZEhqdUZZTGVFSGQwR1E5Q1NKOFk0UlJzRmt3TE0za2R5Wng0QzJGU3ZFWXJGandiR1Zjdzg0cDNiUEYrdmhzbVQybmtIczc0RU1zY0NuY3Q1VU0raFVXMHJUWnFaUkduQURwdHNNaVQ3cG9SblNodjdXYWpyMFpFMllKS21aejFmVXdzYWhxNUxodUZ4WnVqTnpRVVBJdmlnd09jcURGQmkrWTNuZDVmdlk0RDdBOXlSUFlFVXQ2ZjZ4S0t5dWp4ZDRwSUd3anp0ZERxSE9RaElJNzNZWjZEbk9XSmZWVmMvWmo3eWRCVFNPMDVJV3ZxRE5WZlRoZWRoWFZHUEd4ampVb1Rzc1BSeXVLeEYyblB6T0o3dlZoSVR0c0VzcXEyM3RjZ0ROaFBYeVFjL0FkNGExK3JlYzRpU0Z0cVAvVm42RlFWMUY1a0RHcmNqN0gxRkZtL3I3QW41QmNGdVJ4L2h6L21FaUt0UHRuWTVQbWFKeWpCbVRBYkZOVU50YnpJc05td1UwUVBvMnpRNGhKRkJSaXR4UU9EOTBhMjRERU42eUpMbzMyL1NoMTBYUjUycXVsbVpXVlp2YzgrYSt1bUNBZDQ2ZnhiRyt4RC9Scy8vTzBNSENEQU4va29JSllabHlTOXh5WSs2VlhESGlxSXUwdWtPWjEwRzhrVWZUWVFpdGozVGtrbms5TWZETzNCTU9lWWtYdFlmRzdaV0FWUXM2UGgrZWtqanREUFM5NmMvZ2Zza2xlYUpLeHgvVnBuRFhkS1NPL2o3QUszZ3l5WnA3YzhvTjIwT2h2a1NFWnVIbVBXZHZWaUZ3M1hyZ0VhSWdQb0pMSEE3UEdLYmFydUl5dkdXNFYzeVplOVZzYmZFaU5veGpEc0FVNmZCOWhiMDlUdDJwN25sM0x4Wm9SY2pKR3pPeGtQeWtjcGVhajRmSU55NVp1UVpIeDgvVDZFQTl1Z0hoL1p2VVJzR2dXLzBtLzRLYlNzSmcyRUN6UXErYkFtOWZqalozNWhRUkRXWWNkbmR6TEg1RXJWeWhiZHlNclU1WWJKUHc0MmR3MTZ6VlpDOVMyemhYSHhzR2ZnRHJiUkJGSTZBRzZNS2ZURVJjQTlPVmk0NjRyQ1ZNOThPd0M0NG5SMlk0NzhiZy9BMUh5TFNCUlowc0VYbXpYaTF0VmhVVUl4Z09QVWFGQUxTUlhvMmcrRnY0SFJsYXAxYk01a3BucHBNUlJrUmQ4eUI4UDFMak9JUEJ5N3l0NTRqRE1jMUp3Mmo4bmlmZEkwRGRVckdHNTUvTEVmRFhvcFlITXBWUUIwenhmYXZjc0RnWVhtZ3pHTUM4aW1DRnBZSUFXMHJMaldydDR5b0pMcmVkS0diY3poYVFjWjZvTDdUN2gzM0lER3EySXg1S1NURExBNGJtQm9FSFVaa0VDa05UZ2d2N3RWUGE2SWlMREVEL3ZUOWhyOFhwbHJNSWtXMUxhQ1dadmttR3Q5UkR6MEJsTEc3WFY0c1ZXQTZZSlBLK0xVQ3ozUHZKUDl6dE9DV2g3czVBYUNSTWY4cTRJeTZvbjJ5NUJ1RGI4QXIrcXVuUGRxdjNJVFBGeEV2aXRMcmZpeHhDaitERWNlVW1ZcEZLMWxOdE9wQmpXZEk5dURtSHNhWlNYZ2dCZ3ZEaEJSMW9qWnlRM0xLei9naDUvOHJGcUs4ckgvZ1hiaVdka1dmUUVtT2I4RDM5b1ZHWW9JbUNjU0NRUGI2N0x1akpJN1MzS2l3M2tPejJDbzJNU1ZaUXdoZlFMeDZWSE1RdkV5OExvdFJKbVp4N21mZ1RWeEVGSEZnMHBBQWRMbFhkSlNSZklEcUhQN2kyUEFhcWx3Z0xtVFdwTlJTTUVuSnNSVnl0TnNHZWxudFBONUNxbXhqamsrbExQTVMrcUZmcjVZK2lrcDljMTZJcnRtREcxZ20zN3FXTVNmeFMwbkZ6ZUluSmRsSGo1SytCNGM4NjFQRm1KbzNNODdnQXRQRWR4QXVrcElZdllVMHpGUTNXOHQvaGtHaklJL3NvbVA0aE1pbE9tQTIrdkRuY1Z1TDdDc1VzN0s5WG05QkcxeWlnejM2QzNuZHhPWXhlaUxxWkROZmpzZ21XSDYvRmtNWnJabVRycW5wcGdxQmNmdXpVamJzU3NXcDlaaXVMMlJOK3krWGtyd1BLUnhZNWNzRWJ0QSsxWmFEZTJrdW5haC9Dd3NsLzE3TmhSMnQ5eE4xMUJlUnR2bzd5MmRTWFhxa2hwbUw0Qm0yU1NFN3hKcDJQcC9RaHFEaFJiUGNEZCt4bXlYQVBaUFd3MmNjeWE3WjZLSWVMdEtNM21aRTdlR1Y3bTUzdkZLakJWdU94bVBSYmNMcGsvaHVHOTVIcFhWNDFUd1lmbUhNcmFhallod2Mra25JMXFWbVpVZ3YvanJXU2VoelVwMEJ2d3lyVmxSSDA3WmJueVp2N0NXcHVxTVo1b2x5M3R3aTlzWmx6TUEyMkFLeGVPeFRkNlR6Nm81MkhVZlFXaGl2SVJBQ3o1cTlLQk45S0JRWmprbnBGYzNXNzFKd3pKa0pmWm9oQlJ0UFlwWHJrWUFub1ZPVldodnBQaGhPQzlzZXB1NHNRNGhzMEtidHcyRVVTN2R2Vmt6K2l3UFFOR2dsay91RGlaQzVxS3p6SFZuSStNTE01NWFaNWxDL0FXWWh0SC9NdVhNZ01mSUlTdjlIaDFkTENSU1EyNHpZM2tXS09XNW41UUQzd051WGEybjh2aUF0Z0s4bUQzK3VKUDkwaWRRSE5La0tGVjVNR3I5UnNtcUl0NmxvMmp3eU82RS9uT0dNY2dTb2JPMWJZMVo1TFlaQm9VVnNoSlAyWDljazJ1Rlp4LzR0aFk2TEh6U0luSXlUMWxCUnRHUlNVa25FazNkSVN3RGxlM25WdzhzdWU4b0ZOSG9mQnUyWEtzVXlIMnJDc3crNlJScTh0WTh6RVV2cVNyNGIySnhMcGJEK0ZjZ0dJQXovRVRBT2d3VmUrZmRwd2RiUk04TWJBT0dKREhpd2FETXQ0QXY3Z2pFMEJYeExtM2VHaUlzVTNuQjNOaHgzUUl0U2ZXeE5HM09zdU9nRmdqQnFyQk9LL09FQ216RnNxZkN2RGRaaXVEeEQ2Wkkyd3hvMDVvOG1xdlFlY2lkZTZBYTNtSisrWnlmd05qZlhwMHVpa2dQY1h3UWpHbVY4TC9LNEFYQVlycDRMSWFaUnJ6YTFOaFNQTmgyV09zM3hWeXFMb2lBNEJ2R1VUR3N0aTU5cFUxSEdHQW03Nms2RkpTMjMrSjdBUU9WMXFFQ3hPckl1QVBFNUNGRjhIWTQ3b1BYL0tDd3I4Wk5XbHNxOHYxOGc0Y2tYbnUwb1ZXNTk3WVh1cG5yMWd5Y1k3RVBWUktUWm1BZTA0b1RJai9jbzZNY0p2Y0tMUFh0ZE9vWTNNWVVkUEFvS3prc2ovVEp0Rjl2ZzB0ditwRkdSNUhpSlB5aHc4Z3N5aUM2VlJVQy91eVVvcDB5YlRqMzdLa0szZjhlWUtMV3FsVjUrVGJEV3I2bTBLWEx5cXhDMmR6ZDZyT1ROWDZYbXRIczUybFgvWUppMk40YklNSEFaTkFIQWkvT3RITC8rMnRBVTNuY0U1Snp6VC9RZ0cvejQ3NGZiUWxJbE1WcnVFeCtDSTNOaWc0dXhwUzVQa0ExZk1mSTZia1ZQWStQMTM1RFpmMnREeVF5eXArRzZycTJxUnZqT08xN2JTSTM4aDJ4TnhWNmdIS1BCSHdJZDBBRVpqbis3cVdFK2hFL2gyclNMM2JSRTk1bDRIblQ4bmhPMTNKNmw5U04zWmhKeGZzeXptSnZSMlR1bHRRMjhROEpkYWZ5RTFGWno1dENXYzMwVUtvRDB3cFdPRyt2THZIRFdLd2p4cHZWNUtwNG5qZkx5cjZYMkM2TWxLdEYweXNyWTRBSnM1d3h2UjBiS0Q3dEFGWXd6dkpyeDJkU0tyMG9OUmU3UUhrcjJhSzNEZDh2M1N5VHFSbExmWEo4UUJ5S2J1ekxoYXJhNkt2cUsrQlFwejVkcS9pQlg5aXlHekNLRjd6ellhc3draGRZTjljMjRCZHdEcCt1WGc1WUtNSlZYa1VZbERLVDE1a2YwOUdVdnBPS2VwR1B2R2VqYTdkVFpkUzNsaHhLN0ZaUDRGS2VUc05wZ2wrTC9Rb2o3eHhJM3ZRcExlZURjSVVPdzFHNUtsdFVOODczMmtha3FWb1VlVXJhdFhPeDlGWTJHaHVBYlpKM0ZOZHdYUUpQOG9EZ1J1NHJ4RDhURFJJWEtaYWQ3MGJNR2l6Q1ozekxIUWNRWUs0Y3RrcjBEMzF3QnNKK3FML0ZqMGxGdXA2NHc5V09BdjdJaEt5bFQxejdJbEtGZFVhZXdrVlFsM3o4a2ZkWERTNGJ0TlJSdXREQWZSQktrTmtzL1JWZmhyTVBYd1lNSncwcjN3TWpJUUpHYXVqdkJqd0xONXd2dkl5b0lKSWovTjM3Wi9kLzdRWkxQVVBZOHZqTzZqbkJ0SFJyV3lEUWJOVnUxclFpVjVSbGpnOGVWc1ZqYTBFdkFiOHdIVFliUlN6ODY0UTlUVktySGlJMjhMZUt5MFJybkdZdHFIdFNPN1VKUDdZbU5aU1k1Zi9JU3loWmJ2ZWZ1SXg2dGdweWxVWTlVWW8yMHJQU2lOejI3MnJoYlptRXc4ZENkRUFBcmc1RnFvOFJBSCtmRnJDa09la2x6eEw4bnFmVDh5THNjQXNUMjlXc1FLTkx2Z0VLSTl6STN3NTJmcjZRcGk1ZHNZemZ4eHo0RmVYY1UyeUxpMk9tV3RZcFVTbEJpMUZvQ2piTGNOU0dmbWFOSGY4NE9ZZHZUdnZtWnlHK0w0eHBtcDBHOGc4VnF2aTJXcWg1MlY3OUJ4Ykc0TkcwRk5CUWN2dkZmejEvWlIxeTlpZWZ0WDZIRTdkaXFqcHVDN0ZuVWtNc3NGdzA3N04waHlIb0hJZjdIRnl4NHVjUkd0cm0wVXYwZkJMQjU4N2FBd1hTTjFRaXBUTkp3TkpmejBxTmVLK2E0MGdKTnRuTm1aNThsNURkSjdpcDdSK2VQb0REVzdpV0duSjg1ajhZaVFORTR6Umx6cUNSSW1xUXVxc20zYTUwMFlLZmdPTStjSVRqSllWaExYNTNRQ2pIenVDZTZWOTZkaEFXMk1TVGZJRE9hdzMxMUZOZU9tN1ZFSHlaUW9OOGtjWHQ4QllUaHNNUUY3ZUdhL2Jpc1REYThWbno4MUNaMXFacWUyUklNVUJOVGpyUkQ5dVpMb3dNWEF3N0c2Y1Z6OXR1OEM1bk5Od0dvRHhKcXM3N2IzQjlGVmVUZGpXRzhlME0rMlpVQWVyeE1icWtoNTh6QjRmZW4rRUUyNEk0bEZmTnlQVitKOS9HdHNvOVEzU0FjNzMwYXUxUS85RW1UekFWY0ppNWRRZzlUM0E0bEo0d1dPVW9kendBVVBPWDJ0WGtwL2hITnAxaHRHdTZOMURBNzZQcTkrNEc3c2prQWJGMCtHd0tVck9WOEVEelp0Y2ErZFZuTDZpdmJ6RElkeWxRTnZpMVQ4azFKMW1vQXhVa0RGQkVtU3hUSmp1QzZXS0J6VjZ0Ui9GdEE1RzV5NHpGR3ZUSzhPRWpwVlFJem16SHdXZHJ3SitEMXZmMHpmbEdhSXh5Wjkra3BhdDN5M1NxR0ZGUU5BMWVXVGhjSkNkSFlNWEpNaDhOclV5eWRlZy9oQS9tamJHL0tKUGx0RzhhSVZ2R1hXRFlxVFhQMHBOcHhncEh2ZGRQdXZqbm1ZdXZGL2dUa01jSWlCQTlJblRpWHJpdFYwWWllTHREZ0ZrU0svcGxyWkVxVVpjSXlvcFdlUGtMY2M5cHdwS2k3UEN5VzA3enJkcWJtV1ozK01rZFJrNEhjVG9iMjZUSFhtN3FveHBIaDB4dTV5Q3hlV0NIWUw1M2hRbW9PRk12bEFqaFdpTm1ocVYzWmVTUDlOdnhFZUdNdi9NSzRzcWdzVUFuV0w0NmF0ZjJVMWJZYWVnbFA0N2VsR0NsTXIvTUNpMGxiVlNqNk9SVjBuc1lhRTVSN0JTd1k2bUV4Yi94WmxFU1BQc2VHb1k3WnJGZHlvakFJWU0wM1kxSUFRVlNaaWpzTGF5REx6YnYxczFDbnBRcXh3RUZ1OG50dGVrYU5OU1VKbExTRW1qUmh3ZDdWd0RGNkhpWWV1VmlwaTIvZEY1M21GQUVsc0dpN1dnQTJZSXBSWnpMQ0g5UkRtRHYrbFFDN1lnTkpvR3J2dXBXSnE4R0hGSmdHNXBCV3lBMk9rSTcwem9zWlozWnU1R0dwR252OEt5ci9WVGxlclFVbHlXV01DOHFObVA2NzA1Y1kwVTMxRlgxRVlFRUdZQ29LbmgyWjRIdTRXVkFWMFNRMjdySlAvR2dRUVB5L1VlSUdLOXpvd0xhZ29vcVZNZ3I5TldwVXY3OEdhdjZEVHJ5M0VFTUNBUDZvcEkxdzYwRjkrVlhPaWN3ZEhBYkl0WUY1MlFDcGZZZ1NFV2s2TnBLVHpSSThOSVU5MUNzU2t4SjI2TWhnYmZlYlB6azN6Yk9FeThjYkl2MEkycTR4Z0pGWWYzUEY5VVBpaGRlbS9lT1pMN1ZFdVFZV05DaUJRUEMyL2dCK3QrVDEwb0ZqZm1EbWNnR3p3c25jUnRITW1MS1RhN09ic2dzWG0xY2ZBeFdLdVJBSXQ2MkpwNGg4Ym13STdwaS9EaDdQenIzMHJoS0FTdmpyS2N6R0dEZkwvbUg1a2VxbnlSK1FGdmNCUll0RXNRQnh6cXVDUWJmTnQ2anIwaDlPZUF3cFlscUtHamlVdkJGRVp1eFhFR1NSUngyUUJaUG91NEM4bnp2R0xYVnRHZ3p2K1UzaitGQnFjWDZIZGJCcmN4ZEt5bFNFWVRPSFVCVVUwOHJqTVlpek42WnFDVnhEY0tBV0ZqaUlzaGxSUWtybDRDWlpHcXkrbkFacUo3dlYyQ3YzTVdQY1FmZWhpeDZmd2piOXdCR1BDNmt6SHFWVU9CVGZrMWQwbWIrbE1MYmdsYkRlaGtOdnVCdEgyS21RVG5hcjVLR29veXRDNFFKTzQ4Z3JpcDJKNFJDZlh4UEg4MlprVm0zNnR0MEVnSy94SW10Ni8yOUYrYnJXSjlMYTg4RE5yNTJTalQ0WkJIWnRiLzJVcDBiR0p1U0Z1aXduS0JsMXY1TzJJcGJMZUFWWkZKV0ZXL0QxckZoUGNuaG5yaStpdnZsa3dpYndYMmN2WlZRR0VNc0o5aXR5d2owQUY5MmdBZGowT2ozeEVyclh1aC95cjc5aVR5N2l0dkJjWEZzdTUyRk0ydktKNzg3TlZSRlArNWxXNFFsWE81a1NFQ2Z3NnFKTTUvNzBHdUZFNnlWTmlWTVUwU2pHdEhlOUxwU3JwUi9FcDNhV1BTUTBEYVNWM2ZwL2lJS2UxRUhqclFKdzFSS3BGb2E4OE81bzVkem1BTkhQTy9YRy90SG94V1U5ZmJ5T293ZUlNRm5qWlBYc1ZtaER5NTRXbE90eFlWSU1udjR3bURVUk14NDhob3Azby9Vd2lweFIxNGtvdW5IMEhoMDJlYUFkTE5zc1BGUEZtaU5qR21aZWk3ekUyTHhpN21xZlFBMzRrVUxqckVzM0JLQ0lNNk1TMncwbWVqL0ltQ3plc2ZwcklmTU9aUkpYelU0MGgzRjA0NjJ6YmVwYk5xc2ZjMTg0cWFSSG9FYm9kRXhwK3BGaGNTWFJYSWtzQmpxTm12MUhiSllheWdBaW9lOG84d0ovRm1QUlpYcmFZSVlQejNReXY0dktzM0YzSERKSXljazFEVmhLNFRMS3cvUnZDR0Y3aTFUb1VHdnBDVXRuQktkT1VOZmlHa2hDMTNQYngyYmRabE80NGcvRlhCZzJwMW1URWR1TkEweFZWNk9jLzJKTjhzT2hwaVJVbXJGTDRjTUxPbGFCZS9Qem9xWXJtQ2lzeUgzUTBoN3Q4bzhhQmpaOVp4S1BPSHJKVzU4UmxwZHVBOFFTOGxJWUg0SnNMUDlQaFNGVXJVQys3NGRBVW5XYklvaUN1NnFHV1FGZDVhUllvaWRvWTJ6akVOSFMwdEU3WVJUaWdKSnVXQXZMZ0RtUW1jYndKd09wRlFTTTVGcGYrYUllV0dEUklUNDQ3QWZUS1BhTktuQ0tQeXBJY2xleDJLWlF0eDdFSFNVV3ErTzhxamtPM2RhN0hDTC9LSFNXVVY2ajhVY25zbFE0UG0vWlNTTXU2ZDFoTnFCVngra1BYZHJBYXlmOFN4ZEM0Q0t0Z2I0Ny8wTGZNejFnTkFDcWd4Y1ZpWHg0NHVlSWJWUzR4czNnL056QWFNaEgveDdHU3Z0Z1B1WnBYY3AwWXFsMytLeTgzY1ByMWxBU3J0YTVJaU1NRjl2NWVmMHAwQ1k3eHhFbE44UEtXOTZXOFdTUy9BRjI3RUNlMEhnT3crOWFSRTNxR244NzZWTFg4Uy9odld1eFhxU083MzFEM25sT2N1eXRISWFlV2s0NlFCT0pDNkc4eVhvN2VnZ3BFTGZNWW5VcFd4ZTVUN0tJY2R6QXBKeVRpdUc3NnJsU3BndHlFRXo2TmgxbytSbExDamlSTTJha3M0bTRGVFNYMVpaVnptTFJZTDFjT0tocjFHb01vMmFCd2dCcWhCSHJSRHVnaVl2N3R4dks1V29wKzlqdHRkRk4xd3BnNWRZK1dNWjZIUnhuRXJib1BwQ1VDQXk5QXhnZ1ZGK2dQSGV1cWhKaVE3WlZHeUx0UUVtZ0JUTktyclF5c05pYmQ1WmNwaDFRQWVmZmpxU1g4enV0WVlheG05MTZZMFV2bFFjNkxobGErTXlnWVNwNVZaOXVFUWpoTGd3SkxGQW53d0E2enJ3bXFpcnNoQWQ0dVNVK255MGlHeE9QMW5sUDJyVk5zd3dFdW1mb1Yva2gxRHQyblJxbGFOMnlDWm96aldvQ3lCS3ROdmU0NGZrYXhlcEJOQ3EyclRBRVVGWmZvUjlmdUcvK05zWlpSMng1MzNhQmdSZUJKRHo4L09sZ3FYcm9lUVZCbjlrUmZDTEhpR0VkZm50cHpkMEpHVHN6OU80eXh2VUw1ekRqTE55VGNPSnNBeWRHYjdibDd2MmVKZWlWRkdybEVvVjc5N2JrV3pTbDZyTTEzOWdUMm05YWdPa013N2oxbVZTQ1BjSmFhcVFROUVHQ2sxSy9WckRWZ3VkZHMwOE1XcnlicHRwM28rRTNtWWxjZ2w1YWIrVkJhd2Foem9IZk43U3ZpVi9mVUtXT1Q3dXBxcS9VME5JeGcrY3FLaTgxRUNicW9oMmIvNFNPcy85SDBtWmhCU0wyREI1bTJmWVRFcFJRZVk2N3puTmJKV2VsdkJYUWNrVS91dnVGZkcwaGwzamIrUGhzR3BTYUI0MTJ3NnlmbTUrS1VhQi92T1NMdTV2VXpsR1J0WGdMZzM0ODFWMlYyWi9Mam9vcVVLYVRGbE5QbXcvQUUwQklGaVVtbGdHRUtBay9ZZGdxQ3JXV2JIYW1YWGNyL09wdlFHVDNYMGt3UVVzSlZRMUdCUFdrd2sybTNOVVJ5ellQc3YwZ2g5bnZTYjBBMS9CWWJ4ZEFJT3NhMnhBaFJDQTFNK2gxWTRjZ1hDODd3Sm5qZmt6S2MxbllCTmhwSGdkV1AyOTlrUDljQjgwcW4rRkwxRElYaEl1VHlLV1FQa2kzRTNJZUl4RDlaOVZkOEdhUEFmZGhVWW14QmRJYS8wTTl4K1phTU90TlgzcUpNSjJBeDN5WU9vaWV1c0pBcmJaQ2l2djFxenRpUzNINGpUZ2xqd1ZFekxhT0FrK3Jrc1BmaDdSYjU0V1pLb0tjNXQvVDVDRjJ0TmE5WTJ3YWJFc3IrSE84UUhaOENOWGNvbHFhYTNPZ0RLN0VQWWNqN0ZJbWd6YWVHYUlmYlFuNStiQjFyTlN3WlRLTVVpR1MxZUlJSmlTcm44YXFDTjI2b2VIQlNDT0c2TWlvNGZROFlIaTV6S0RJdGovTG9TM2Q2bFVJdmZhWmZBcTY1RHQySktLNlpBMFpCMXpUTUdXV0lyNW1oZE1yak9YUUVJRjZPZ2pwb0YwSGZyNk8wUlVPM0hPMWhSSWZybGNQTG9QeTlLdTVzYWlFSHdXWElwYVB1L01yK1EzNkFpZEFyMzlWTDVjQThWSExXaWVKYzltenN6Rk8zRTVVQVJtMWVac05tclNLMlpRdUxTRFlxUXZvbHo3UkEzVExzdEZwM2ZWL2dFam0yNG9YU1diR3ZLMjBKbVhaTGNIUTF2NXJqRWUrVjc3c2FiVVNyS04xbXBFbi8xd05PUjIyOXJReDVRbVB0ZWgvamIwVkJzbjdwQ0t4bzNQeEV1TlF4bzRiQzZ4TnBCVjA0eVRNVHpLbDdkcWE3eDBWM1NXUlNqTVRqR0pYZ1cvNWliZll6Z3gwbld4M01qb1MzZ0Jyb1NSTkNvcW9kOXdRV21NTkRHV3BzalVkRGdqdkNyZ0RZcTBidmZaSjYxY3ZtK2pDaGliNk81aXVtTlEzYzlyZktZdGVDRDJxVGRpTDRwTW56Vnc1QWt1eDJFamZOQ1d6OWwyM2pTRUdBa0UydmhRK3EyK2xHRFgySmlBN1lpQWhLWEdHTWFQajJoelp0MGJMRWZ3b29XTzZBWndMWlRLa3lwUTJUamt3YTl4UVRPRGJGSlo1L3FSN2w4Y005UlY4c3BEWUhpYUN5RGhIdVZab0JKbkZoZFZLaUp0V3RuSEpiRXhYb3lDanVlN29RR2g5K2MzcjFSWm9KYVlUaFlOQTdYZ3JaRFNhSGFYVUtLbEsvMHh1N0wzR0Q3ay9vT0lyT2s3N1dOdEhGLzZOUFl5ZENURzRySGpGM2RVRXpsRjc1TnlFaDFjck1YMVRNMnNxdGpjWlpvMkIzQitGK0xURFFGUkplblNteC8wdmFoNHEweEJYSXVjc3E3d1grc1FyZW8xWUM0cWMzU3pXcnFkTkxtVHdiTEJDMGY1dGtTR3FKY29qK0pnZnFUQUJFMWhGTlNMQldYOGFQa2x2WFZ0L0dzUkJ3VHYzeDdQOFdEaHlBd1BjeDhWOTM0TnR1SDRHMkYzVkZURUtnR21MbG8xUm1yWjdxZG8wN3ZyTkpuVWhtaEtldmQ1cXBoYm84bnh3UUx1MzkzdXQrMFhJbDdpdE9jNFRVYitacEIyS2F3NlhUSjd5K1lKTzFnVmhJd2U0TFpERkFZVzlGdHc0YUhvWmQ3V1dHZWtmK2xWZFZDZGtnc3ZBemhsQWwrUUlpNmJaZWhYTkpmSno2STVCbGFLb2E4d2U1T3dCRnFxVmRVelpiL3orYi94NFluOTJGOEJLU3pJUlFjL3RIOG9lRHhVVEp1YzF1ZnBwdkZYSElqYlF6SWg5L3NpTTEwUkN5MFJJOEgydG0vMktRNUNjazI5ZUowNzdDZ0ZPQ002TndsMDZlck01UHM0NDQ2YStXWHBpQ1VTR3NROXliNVk1a1Q3QnJWQnpKc0hzSkFYd216SjJ3YWoxRnhLWHNCaFdQT0dTNGJ4T3JQUGpnM1h0djQ0Y2d0MkpyRUhYSTJzLzBYc3JmMVM3cVV3eDUwMWFncDdWcWhUUktjK0p0Q1ByMmVoK1NzY3lTdkxjeHNvZ2pwYnhaQVRwOE03RlVudTF1VzdaSVdQUldCZERUeGxRd0F3S01KVWhycUFKZkxzM29nRTlCRzRrS2gxczh4TFdHV2dVMGZJRlp4eHpNNWpQODZxaThHbFhMS0xTNVk1Kzd0M0pvVHMvZTZMVDdNZVpPL3o2QTFDN3RaOG96M0RjZGkwOU5RckQ3Nm1ZTmhLT2F0L1JETTV3Y0FtbXpxK1pFaDVFUnhYYUFqL1NaNEwyaEQ5cXVmWjZBdUVRaEExRk9FUDlFRmt1dkFFdll6Wk9oSTBoV0ZjSFhteElsU2ZsVUhPclNPV0VSbDlRcEcyYTVsZENJRkk3SVNQcEtnSjBEb3YrMldNdExJcUFsSnFYbnRSOWNmNTlGZTVOQ2N2bzFOc1R4WXFlK2NtOEZhYnBVLzNkUFN4N0tLekg0d0NqaC9SZ1pYSGY4anp2N3JCdFVtbUYvenFWRzF1SHBNV0M5by9iblRXVnpZN2ZxdjdWaEp3czNwZndBd1JpdE40R3U4c3dremhzalB0NjJvRFoxWktPTWdyVlMyblpzeWZrQ05FYzN3UzE0MHJ2TVlRalhjNkdSUlVzalRiclR3cVJDVFpETWVIcThlejlTZStiQTlIcEQ4UUFQbTJlY2RRL29CUFlHbzhkK1lqYXo2ckpsbnErV2xFMkJadHIzb3p4TGVnWVJqOXFLWWFkMmQwNW1nZGpvbitDVThmVzVWK3hHbGl6SlIvdE9HQ1ZHSnVFVjIxRlNwTEgrK2pvMjRWbE1oWG9nTnVoM3k0Z2dsYjhhUG9IWnFsWlk1ZDdTTEt3cm1HZG9PemVid3FjeFBxVzlQbG9pTG5zSVp2cjBtdGhwM0xOelQwc2Joa1hzNExIWTJ0RmpjcnJ3Q2YvR3NDVG44UkN1ZTFKSkFGR3FDUUJvRDJOUkg2RW1QOUlBMzRUMzdXU1RXMXV1dWVoMzF4NE9EaTMvSmRJM1dXa3NaQjFCRE1ZWEx3M3VhVkx5WU12T3hBc0FMMXk2cTJaVndBeVZnZHI3bEdvWUNld0g2Nkg3RXVDaWp2OXRwVXA1UzUwV0lCL3N5MGF4NGoxNUJVZ2ZBV3RnanhLYWhvYXZ4MDRjOG9sSXhTSmxUczBqVzk4c0pvT3o0UDRMQklPd3VZUlhtSzdhejlHYjhkZkJxcWhJdytTNnhBMWtkblkveWsvRkxxZ0prUVNOZWpsTXpxUlg3TTVBNm1oMFpXK25NaVNHcnBOZ3E0cU5UUUVPNkpBeElKTEsyemRmd3dOdEQ0Wm93YktFZ3VDM1lQQ05WQmJCMEhBQmxzYUF0VTNnaWI2MWVoOG1zbDBVU2ppb0ZkMFJCVm9wWGEybXRLbkJXSVRJdWJtNjdOSEVCZE4xa3Q3Qno3aWpBUDBkcmJEa21XQ3p5bExkSU9rV2pNa0U1RTBBYUJtTDJ6MVFOcVpXR0Y3UUsrbmFCQ1IzM3VXY2VxcUJaZHE2QmxNZ0hsU2ZoczduVXBBNzRlV3o0L1MwTjJNdzhKT2NrK2loSzE1UjJGTjBCTWZMY24vZVhCNUZBM2dLSlg2QlBqMTFSdmNvY3NSQ0JQR0hpOGJPcCt0ZlBacklOUTdGd1F4VG1DVmt3TXExYU5CMkxBNTRDT0R4dWl5eVZDNzNKTXhTSElQazYxVUlORnhmZ2Q2aXYwcUlxZmpPU05pWXk5UVJITVNFSlJaNHV0MG1ucExDSGhTU1Q0YVRZSUdYdzU2TktkTk5lYkMvcDFZekoycGtKcjhad2REYkx6Q2VpbUh2bm9vaEtIQXlCUkpTUm10T2w3QzZQeHh0K3ZGa3htdk5Qd1pzVWlFZE5sV0l1Q1oxVlBZWVc0QjY3ZHR0dzdIdm1rdDNzTEpTbHUrVEZjeHI4b1crNVRmQ1NkUmNmRk5hU1p6UWNaL0p2K0k4NFFkaWpnUTRSRGlwZEcvbDgybW5jUXA0MnBkRG9uMURCVWFFeGwrVjc0MzZ3dFo4dld3QURObGEraVYzclE2N0pQSElBclJrTzZMeDA0OFFtQXp1SkFRa2tYeGxVZDdxaHB2Q01abk84QVJvdGFhS2s4WFRvSVFkK0FKbW9VNk9XU0VXaFBKSzVKWG92TmZqSTdzQTZtSXMwWVY2elhBTFJCT0dNeE9lNWFMQ1BkTU5uM2ZjTzZHNVFzNWdZNFVob0xRU0tFdG9pVGZ6OWQ2YUdoQ0JEK3hIN0ExcVA3ckd2Nm41L0JCRDI2RnQybVFqeUdFSWlLWXRuU1ZtQWF3eUV6dVUrb0doSVRxeGsvZUJicW5udC9TZGQydmZUNmJZVWJreStpQXRXeWVrQ1lZaDNaUEZFb2ptakFlczV6Zzk5QlFxVVRzVXZNY3NiSmZMeFdXdDYzZEFZdnRoUXB1L1ptY2VVcWJkUFl3Zzc5cGZMOW1sb0NmbFhlZkM4L3NiNzhwQVZ0aGxBbWtlMG4vUWxQUFYyTTBuVUIvMVVEMlpoUWRteE93elBCTWRkWUcyWFNXdUlZZ2JXazFxWVM0OHc5dnU0ejRiaXc1cXFtc1JWbWJQSmczZjlaNUp0bTFFYnM1QjJoSndDT0o0aEFBTG5teEVuZzJFRkV3b2dKMzBzakpCY1k0R1FHR2wzL3c4UEJGNlpsaElZSnJudWtpUHluTmg2cEFlRjBTVzEvSFFDcFJUYmxEMUdIRDN0Rld6VDB6eThoQWs3dktPZnhQU0FOVk5tcFNsMWh4K1dzZ0pRdGgveU5jTHNOUnlxcGViRkQyckk3M2tWdVdBeS9TTk41WEI4U3BkYWZDN2FEZmpUbHJ0NDhHZy9MTStJTDlDSUNnN2dTbVlLQ1d3eDVWd3YyS2lyR09LanhtTkRncHBPb1J2QURyQXQ2S1hEVkFHSHFueHdjU2hhNUZuTXlTVGtGV2psK0Zma1VpdloxOVZGejNVeXdDaVM0dG1PbFBGa1p5aVI0SC92SGJjdjYyV1NaM1lXNFd6b25iZkthaEc1UUpUc2ZHbW9JMXN6R2UyMkJQdEUxMGpFSUwzYk1MbUpoT0RTYzF6WmN2Z3RHRjdpT2phQXVENWhtU0ZyNlR6RWJWTVdNbEliTFA5dUJnVlNjSjJTS2R0MzRrS3lvZlBScXFldjM5eHZBZlFaY2h3VXpXcXVRcExjUGNDUUVFVHpJOVpob1lZNDBBekxWNERuTkdxNklEOU91SzhiZXJIZTRtQko5YUJ6alBpa3NzdTVsSzV3MnU5V1czUVk2YUdGdEt5eVdBUExEa1lvdnlUVHppaVFCbldoOG5GWVlRcFcvUFNQeEo0anpQNVNOS3JYNUFLRDA0Smt1akt4RDFxb3lYM0p1aEhDak5YR3hZR2s4S2Y4M2V5c2p1d0t5V1BWOE05bktUMWE5cktaVkJHRUJoeTF2aVhTL2ZBc1Y5MGpPWm9OZWdRSndhS1dqcUhRaFRwVmE5bWdHaXl1UGEvZHpKb0pnMUhsRmRId0wwNlk0SjBhVnBhY1NtSFJzck5ZQnN4eGM3R3VOTER3UzNDNjRKMnk4eW1YWVhscUdJN2g4Y2JDbHNEZ3EwWTBsOHJsNENITWlyLzE1MWNML00vc0IrV3MrSVllNEJoUmt3aThTMVdYOThjL2tlMWpBaFM0N2FoaVI2UGttSWFYa0Z0bUdjVVZUa0VEc1BoRGQ1bnhGWkFXK0hqL25xQ3VrODIxS3pLWkVWUTVZNTFieENUV25DU0JIdm9LWTVyZVpReFNBYlk4SkI0Y2hXeGhPWlJTeGF5TTllaWpDYVppc2szNWRTeUdweXVod2dLZzB0ZmJzNGdJNU5sUDFSaEVuMWdpOFhTTERXd1JES3N1VzRsMk5HSExtalRoQjJvSisyQTk2M2VCa1pHN0x6WTF5anRVK0FrQ0xiRDczc3hLTkRXT2x0cXZiT25PZDZYYmlnUkxjb1lnVHlGbktxZ2dUbVlHRWNqUnMzN1NERi95VXRFQi9VbWttOVZPR1pRYlZ3dXUydkJJL3RGZjlpUWswYUlvTTBpdkpBemxTVzdtaTFYQUtZTXJkTVdrRENKRVVkNzYzK1hzZzYxN0RRUDNORjZCMTc1OHM2RytLMlk5QU0xTVlsU1JBa3VVUE9uK3crK2N4dE5yV2w2ZXNPSnpPbVZvYklhc2RJVWxhNUxvYmdlQXg1V1l4YU5reFY1LzZ5cjRYb1FkVHdUVXNNcTVOSElFdDVsZEI2K2wreDc5T25iTGtTT05SSjBPRFdDSDZpZ2VuUFNoM1hPSVNXa1k3YzlsZ28wWm4zRGdIVnFSaERVdytDTzF1eG1FMmVkL202ME1WakZoODAvMWhZTUJ1YXp4OHh3WEk5NFlNcFptaW5GdTNpZWpNT1BMZExzbUZEUHJXci92UTFHY21qdFk1QVovTkc1UFU4S2RmS1prek9Ba3JkMHNMeGJZYXRPTUNQbGkwUm9iZHI0dUNreGM4RVlRWjRROUtyK05yWXEwU3J4a0NNemxidU5OS1dZcFVoSGdWbisxejMwNEw3R0NPaG1aVlhSV1lmOER1VHNKQXc5NEJhNU41T1JlYnNkYlUyZHc5djhPbVdNN2tROGhISFdPd3A0U3NpRDZsV2F2QTlwb2ZLeVZMbTU1ZGxQU2FleFppMS9sMTVnUmJxWWFIUVV4bjlDdmU2VkhQQmpQOEtWYnVmTFIrRm1TdGlkM1lLL1g3RXJ1eGU5N3VNclA2VlV6SE5BbGRaQ2NjN2F5ckNPT0pnZ0N5dzBjTkdkWCtGRVpuZ1hVbFhqbTNJNEpzUEl3ZVZYNzhUZHF4a3d5YXA5RVJlaTRyejI0UFBxTTVsbXVoNWlRYTNCQ2Q0Q0JGY042ME0zMG01OFQvcURHVFJNdHJReVdrd0toME1QMDNNMDdnbjRHaEQ4cGZWUjdLNEdha0xIdUw3SEllM1ZVMHFpSStMTEpHdGlqd0RKWkFTRm04Uk9jWnJSdU9uenlpZklleFdNMVlmeEowcHdKSzJVZTdJRk42cnR6VmltR2Nya2cybmw4RDJPK1hzemt3MHI2bFh5aXVUNzBUZjRiU2YwZmpjQS9OMmhZdW9ITnZLbGFOWGd4VGd3VWVSMkxzdmljajFjZEJLZ0dWUityR0FWd1R6c25oU0VaMEFmekpLMG5odUIyTGhRY0ZtZnc2SExST0ZXWGdkZHBpS0hLQ1RUVmE1RjUwcytEL1hTUDdPL05UQks3YUVFaTFzMnVMSXJDRE85RzhFUjdybDN0dHZ1VGE4djVtNFBIcVJ4ek5hVlIvbDZkYlZucGhvald6c1IvWnlTK2dTZFpxK0VuNmg3NkFmUFlVczlwM2VxVk13L3ZnZ2hPN3ZqQ0FXZ25HUVFla0RIdVFPRk5DTHhuRDlJUmNPTVdudWxNUGg4eDF0Vm4wQzlVekVUTEpkR3poalhGYjZUb0toS2lzd3BrNEdPaWcvU3FpS3JDLzJ2Q2VkNUJtZFVQdDRzU1V3cWprdUY5VndCcVY0T1kyK1ptM2lFSThETE1pTmpaTFdvdUVMS0NMQlBabkxLY2xNVm9hSy95NXVuN2V6cWIzNkVrbzJ4d24wQTJNRU5DODF0cWJpNHBhdTRidU9nUzh3VnN6dU9zaUVxZnJJK3NhRXFoYlYyZmxrajhEU21ubXNNNkc4a1JibjBya3JUY1FPUVpHRUVpZWFHaXpMaDZySGdBSEV2SWJjdnNpcjVYVEtTQmdHc2ltNkY1Y2pkZUZKeitiYXJEVDNMZWFPV3hrc0VIRjBqTDAyNnE4WUlHbzBoSWdDNEwyb3lRY1MrWC9hL1B2dm5Nd3Y2SkpxWGJDZjNKMmc0UHFQbkduWVpJcVNPV1Vkcis0N29qS1h1UmNHYjZTZWFkRk9Wd3R1YnJJNWsxNHhpMkJpaVlKTVhWZ2E5cHNTcHNLTXRPZURyUUJnQWVHVUo5cTJ2dDVwelFYRWJBUUtBWTRkYlQ5M2pZRFUrb3gxTWFPL2luQWc1UFBWUzAxa0JJSXl5Nnh6UjgvOWZPc1FCbUp5NGt0QTNpRXdzV0Z6aHh4cnRLK2RmVXhkYnptWURPL25PTy96Ym5lREJBV1VYY0ZQNnlIRWhaK0pjYkFpTVR1eDNsd29za29FRzVIWGo4dTRGM1BIZXZOSlI4Q282QVJzSWczL1NhdFJ5ZXJ4eGZkR2ZRU0ZpZHJab1h1b1I3UDBOcy8vSTdvdUkyc0RJeFdqSDlqSlI4c2FPcVRDQ2p0RHg3VHdVbjIwTHVyNW9Ya2NaOEdhZk9FU2ZXdVJVeFZmaW5RVFRyTis0RXk2VnlLOHZtTTB1MjJSMnFFVmJ6bkM4aS9KUmcraWlBak9QVUpxWGtJWmE5MVU2aDN5WmhCMVFZalN4a3VMbnJtdDB3OXVlZTQ4SXBlZ0J4M2dnT0VodE0rNVJsMUhvdUNad2J4eHoxeEMxamZJREljcm51RUh0VklnbmJJN2tCazlXUzdNZmFacnNsTXB5Lzk4TkN2VENEWVRXYkNyUWFrOXVxSEpXc29lMVdJaldob3d0dStCT29pL0w2NFR4MGlITEtaL0RDeGRtRTRFcGlUYm0wYVlIT2pmZHh0RGI1KzltVXRKTjIxRVhwZFE1M3lWbE9hd0w5SThVT2d4N2RvdndHc1R5UWdCOXRTMFJZUXoyblQ2Y3RlZ2hmSmZlWko4cy9QdlJXaHJhaExxcUtWWG44czdHY3FaZXVwUjV2cjl6YkxTVEJUTHIxNlFIK0ppa0VtZC9YZS9Qalg3bSt0ZUNGUkFzOEgwRDMxOWU3ZlpGZDdud1hQbm1XK2YxN3ZkajdRMlhFZ2RnWGxyVXZLWjRWZFZZM3Bqc0pQU01zanNMajhHTjcrVWlITUFFZm16VXN5cjkrZG52RCs5a09YTU5pazdwMDJzT3FIQkpXdktQdkhTRkY3Z2NFYjlNUEFnempjbE9WKytjUTUrTVhuVnpyUElSTC8xaHFxWkVCQU01bk1Wc1kvYkFicElMUm5PWWkvZytoY2lnNk1qaDdKMFJUSm5xSVhUVTJMRFVsWWlJaXJVOXFseDFsMnJhaDlHNUhGVkZSWkZwdjM0M0hhemZuRDRrUk16ZHl0UDdEOGJOZXNoZlJjUG1ZWUVpMnBKbGtWVkhOd0ErdUI3VlNQZUFndWs5cUVhK0Qxc2xQeE01WStiNURiYkhEU0p0UjN6REZtV0ZIbzg5aVdZT0U5YVdaM0pWQ2lqc0NTN3FhN3BGKzBJZTZpazRPdXZWSjJyU0NqL1FRYzRxSEh5d1BvbEh5a2JkYTltVUdVRUxPRGJHc2llaXQ1SWFNR0NNN2NGM2lCMnNIS3V6Skx2Y29tNzJFeENjZUJTR0E0U1hXNDl5N1F3K1QxMVhBeWZWWncrUHNOTG5KcTJocGpML2dONU1wWGtRRWdIVEJqZzg3VTZsKzQ2ZmZPM01rYVlrTHF1R09nVTVIVHJlYlBxNU9IOVlTM2trNFlhczIzMjBSTzNtd2FkQk5adGp2YXFjVVZrUEk5L0VBbFJzUGtFWXdNM1JNR1d0dG1DWHU0RXpBRUw2amlDaVJpM0Qzbkoyc0hSVDM4WXljK2pROWRESUJQT29iUE5tU1BiNWNMRjZNR2hTTjRrWWlXdlM0cGE2blBGajY3ckZTcGdqYWg3SkN1c1R1TW56TWN3UzJCbHdzaE95WEg2em9yK1ZFZFRpcVdIaGNGVUZMbnFyV2RKc2Q5Y1pBUmtMZTQ4bmxtRHNyTFVVTGVsY3N3bXlvcmM2QXdNZFpxV1Z3Y1gyMnorUHo3aWNzbVRxcFB0N1J6ZmpmTytQaUdHOHBGK1pvR3RUWC9LMEVVQnk3L0VzYnhhcXMrQ25xbWs5TC9tZlNreHRBekdHL24rWThjQ0V2R0RBM0pqcEpSQnJhZFRKb0s0TVN4TFJyRzlwR2tDUTRmeVpxNDg1U1ozMmNWeGRUZ01JcG1XeXBUR1ozTjg1cnJQUlJYMU9RNllmd0w0K3dOZVZWR0hXZS9QUVdGYW5FbVJhbVdmZ0MzcmlRTVhOOCtWR01ZL0dvT3Z5MXUyZkJ4eElEMEhUTjROdkVLL0pSR0FwY1pXSVRJaWtHanRna0dYS2UxS1RHekVZUEFlT1NRdlh3YWp1cWZTMDRFN2dmYlBnQldDMllMcVZiSjNjTXNueUxLU3d6ZFJmOXc5UkFZN3UyM2tNdE1Vb0FmUEhwdVJNQ2NWSzE0bHc4SlplUWhsTlRiRExTQVNIR2dhRmljR0tWQ3o1YjlHZWlHSS8wOVBVOFRNSTFKUVR1RVVweG1rK3ZwZ3M1TWJRd2FBOTFnMm9ySVFPQWhXTDhGZVlYOTArQ2MyYTlvSzk0NSs2a29na0V2T1JubHE3Q2h6eTg0RnJxRWNwZU1HZXJidXExQ3ZlREJTRXlsLzE4U2pRNmdGZDJRUjJjaEFNeDJLOG1HUmdkbHJjUjMzejZXQWg5cnBjQnZCN1RiMUpPS3NvWXlBZGhpMzR6SVp3Z0tMVkRHZkNVZFpUYW93T3gzb3R2Q2w0Zkc2TkMwcHJydlc5RTlkVktld0EyRnBDZUVBeHY2S1RGTXorc0NIRndhK05UWjVyZmJpRjRSemE5NVZSTEpmaFIrOXVlU1R6aWVFa2hwTjE5RDBJdWpGZ0ljTzRjQUY2YjE4OFlrY1pJRzlhbzI0MlA0aDR5QUZ2eXpqK09Nd1ZBYlJpM2o5ZkJrUGkxaHdXdWVWYmJicU02QTdxUVJUYXdBc0RXTDdPWGVYTE1WZGx2YmF6VUMvS0F4UlVDOUg2L28yOGk3TTlERFJYR0dhQjBpaCtKS3JncTdlYUpFb2ZmYktyRVNZa2t2SEMwV3VIbW95bUdWUU9OWU9PbUVOK1h4bU5WMnlVVUtoRE9XU3d6WlNmdzFmaXRZYlJDRlBnLzR4QjFxQStJdFdZNUdrcnljd2Y3REMydTRHTlNsaDBkODdGV0RqSnpsdVJXSytlVFFzOEQvSzFBOUFBRkpMU3JhQUQrWmRtQzRjMHhBd2Rkb2Q0dXlPc29KL3E1RzFoUnBiSU4va0tzY2ZOeHpkOHhTY0FZSzZyQlQzV3ZHS1RaOWovTjdMRWNGQnZqOGpoM29aMzlkWENVYks1WVRLUVFmY29Pd2wybDJTR3ErK1BaK0FlUWlRbU5NZEExTkFHWjBYUVJxbmNydkQ2K29HbWY4MTMyajJ2YVlOVHpWMnV1aUxHaS9Yb3B6NWZnMFFrL0JxYnNjaDd0WS94bmErS3RxMHo4bnd5MmZBaTk1ZXhWMGFFaGZveXB1WTFQM1dUNGxjNklKSGZLZXFIODdwRW9hRjJIYVlkTWJmZC9KWWVMZk1CRW1zRytxQTM5bUtweWI4Z1dWOTYwL045bmdtM0JBY1k0aDRxNFM4TmlkYkREUkZGK2s0Y0VEWjRKZ0E5WTd6OE16b3Q0TlZpRUhtMTN6eU81c2loQ21veC9oeTJnQVhOd2xPemxJeDBVS2ZOeTlDbFpRY2kzSWVvN3N6U3VyTUJ1eWRXMnE1UGxleFQxSWFLV3YvUkNDQlh1bFlGK1FpUTQyT1lzZk5rbmNKOEM3WDFuYjdZOWNFZ0FaaDZFSzVXRVVGaDhBZ3FXNDNQcHJSUkNxeXlLK0VuVEVBYlFKbWkzZVVVeW9wbHpBclU4MVhnT3J6UkRYY3A4L0hZYnc4MjZJNGp0VU1uNk9GcUVFM3JLRmREd0JyTWkrVHV2MUVIRFE5SHFXZ3BBTUlhbmNvV1hCV2N5ZXlNSE9qYkFEUnJjcDNFdS9hbHJLa0FCSGNyVzl2RUNzanVxcmFXZWYxd0k0WllIV0pmUVJpWHE5VWdiZk54b1grRGlaay9CeHNEUm4vUTJ6K3hyUXdwa3YzbXJBUC8zNUxSSWFXUTZpMUk5SlNDclUvUGQ3Q25tMmIvaXhqa2ZTWFl6bnZOME0yY3ptRitmZHViZE5sTXgwVENuemNYOGxxUVFoLzZZVVJ4ZjJrOTY4dTB0L1ZBM3J4MGFXbGIvdVpsZmJpN3Y5eFV0cE5QMnVReTBpdk83dkcwTDF4RldsWW9MUEFWSDRtcFFUTWd1anRnV3RJcFlpNG5lR0ozM3dSZXI0T2lGUDcxVTF0bVdlNy9yMGRuU0p2aitkU0dONERrK3JMa1pVcHIyS2U3ekdhcXNmLzJRPT08L0JEQj4KICAgIDwvQklSPgogICAgPEJJUj4KICAgICAgICA8VmVyc2lvbj4KICAgICAgICAgICAgPE1ham9yPjE8L01ham9yPgogICAgICAgICAgICA8TWlub3I-MTwvTWlub3I-CiAgICAgICAgPC9WZXJzaW9uPgogICAgICAgIDxDQkVGRlZlcnNpb24-CiAgICAgICAgICAgIDxNYWpvcj4xPC9NYWpvcj4KICAgICAgICAgICAgPE1pbm9yPjE8L01pbm9yPgogICAgICAgIDwvQ0JFRkZWZXJzaW9uPgogICAgICAgIDxCSVJJbmZvPgogICAgICAgICAgICA8SW50ZWdyaXR5PmZhbHNlPC9JbnRlZ3JpdHk-CiAgICAgICAgPC9CSVJJbmZvPgogICAgICAgIDxCREJJbmZvPgogICAgICAgICAgICA8Rm9ybWF0PgogICAgICAgICAgICAgICAgPE9yZ2FuaXphdGlvbj5Nb3NpcDwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgPFR5cGU-ODwvVHlwZT4KICAgICAgICAgICAgPC9Gb3JtYXQ-CiAgICAgICAgICAgIDxDcmVhdGlvbkRhdGU-MjAyMC0wMS0wN1QwMjozODo1Mi42NzFaPC9DcmVhdGlvbkRhdGU-CiAgICAgICAgICAgIDxUeXBlPkZhY2U8L1R5cGU-CiAgICAgICAgICAgIDxTdWJ0eXBlPjwvU3VidHlwZT4KICAgICAgICAgICAgPExldmVsPlJhdzwvTGV2ZWw-CiAgICAgICAgICAgIDxQdXJwb3NlPkVucm9sbDwvUHVycG9zZT4KICAgICAgICAgICAgPFF1YWxpdHk-CiAgICAgICAgICAgICAgICA8QWxnb3JpdGhtPgogICAgICAgICAgICAgICAgICAgIDxPcmdhbml6YXRpb24-SE1BQzwvT3JnYW5pemF0aW9uPgogICAgICAgICAgICAgICAgICAgIDxUeXBlPlNIQS0yNTY8L1R5cGU-CiAgICAgICAgICAgICAgICA8L0FsZ29yaXRobT4KICAgICAgICAgICAgICAgIDxTY29yZT4wPC9TY29yZT4KICAgICAgICAgICAgPC9RdWFsaXR5PgogICAgICAgIDwvQkRCSW5mbz4KICAgICAgICA8QkRCPlJrRkRBREF6TUFBQUFRWVBBQUVBQUFBQUFRWCtCK1lNSEEwcERRSWlCUUFBQUFBQlVBQUJBQUVBQVA4QUFBQUFBQUFBQUFBQUFBQUFBQUVDQVBBQlFBQUFBUUFCQUFFRnhnQUFBQXhxVUNBZ0RRcUhDZ0FBQUJSbWRIbHdhbkF5SUFBQUFBQnFjRElnQUFBQVIycHdNbWdBQUFBV2FXaGtjZ0FBQVVBQUFBRHdBQU1IQndBQUFBQUFEMk52YkhJQkFBQUFBQUFRQUFBQUduSmxjeUFBQUFBU2NtVnpZd1BvQVA0RDZBRCtCQVFBQUFBQWFuQXlZLzlQLzFFQUx3QUFBQUFBOEFBQUFVQUFBQUFBQUFBQUFBQUFBUEFBQUFGQUFBQUFBQUFBQUFBQUF3Y0JBUWNCQVFjQkFmOWNBQTFBUUVoSVVFaElVRWhJVVA5U0FBd0FBQUFCQVFNRUJBQUIvMlFBRGdBQlRGUmZTbEF5WHpJeU52K1FBQW9BQUFBQkJQVUFBZitUMzRkKzRKdDBQTTlPY2pGMVR1K3pILzAzK01EWkxHN2ptaUQxRDZlL05YRmltZ1hnWXVsK2VZMS9rWTRHdmxzb0dBYkZ4NHBGU2x5U1JwTUU0b3RJUlk2cURiTFlSMjZIaEZjS0hPWVJ5YXE2VUtuQTBaYjZxNUgrWlFjdzlnMytMVU9JdS9mbGFzMkhoSXNYMWpCWUN4Q2kxWk5jUjBHcXhhd2FJY2VWSm9VOXA3akJaQ0hscEl1eHJrMnJiZjhqK1p0RWVndUlwVGxxU25ZYmhPS1Rhb1kwVUo5cDdTZU5pYXNOZElkSldQZCtIRG1Sem95ajR1V1R6T1ozTjFtV0VCWXdLbDhLdWRiVEJLcHJPdFd0cGE1VHR1L29vQ3p6SnNVV2t5VnFlM2xrRGhtRkcyR0VIZm1henRyV0d5TDJoRzJsOXE3YmdQYnNTUFpFc21wMlRzWDNkSHppSEZuZ2hIVy9hRmo5c2NkMXVYTXpLS3ltUkFQV0VNLysrNGhpVENuSmx0SFM5cVlPZHIvUmJURnJnZmcvWVJEVE9EdWZuVTFuRnRCVmo0SjFnaGdXQkR5VlFHMjBpOG5SQXRoZ3Q3bEx4bXhvc2MzRXM1OU1ZNmZXTHVRcXhMUkZSbmZzK1RYeC9HM056ZHlaZm9iUFBMNWNIV1hmZVFMc0RVQkhLbVdvc1BkYkMrVUNEUVJ0QWFGTEdYaHRoMkNjTkgvbDhHREo2QXhoUUlHOEZRMlIxZUlVL2pWcS96c1JyMDM4djZBNzVpR1VYSVJXYmt2Z3llWmVsMG9MNlBBZlFMY2k1OXc5T01ZVzhJWDAwRnExcHNXSEtoZWx6dUR5K1dFaVdIQXVMUE5XMlZ4R2EvOXlUYTNLL3JsNE9JalRkNjBqcFlrL2dCRFhWcVAycW01MS8yWHJKTklqY1VqallkcXJlL3hESTlodTBEVWtoWnhpdzlwOWw3NkFBZyt2UWV5OGkvQTJ5bkJwSDRXN0JyL3dZWWM0OHdiOEdtS0lCSnpDRnNFUjlFbk1yM3Rwc0VncjMrSzZjSFhFODRUMTN2bnhRSXgyQTBrQ29kMjFPRTZJZ1ljMUpCM2hiazBldnZhMVNUM1QrNGtyTjlhS0xRU1pSYkNVQWVobHpZRndoSnRlTWY3K1dqa2s4OHdZZmdyakFIa1I2NXdsNDVzZHFxeGdoQmcxR3hYa1phOXZPbTRoU1hQWWxxQ1pDTElLcGtsRE5zcitrb2x4b0JDN09kdEpPU2k5Yi9ISUF2Q09ZbnV6dTgyM2RKME5PU0NqYzFuR0tvRkVoWFJqbzAreWZCKzBuVnRwU2dHUVFLb3k5QWI0U0NqcW42SWE0ZzNMMFNKaFBQYTdpdTIrRU15ejBOMlFRdVlTVDk1Y2s5UWFIZ1EwYkcxcGRML05EcjZPaWhuZkdGUmdSZk9XdTZnaktsRVRVbHp6MUpzNGlydXFXVHUrVTFDZXVpUENPdWMzMnBKVk5iN1R2RXVMSFZOd0x5MHQ2MDErdnNpZU02d3dQejNhZkwwQzVPSU5Gb0FObGlsMHk4cEpnUXh1RzZHK1pDS3VkZmJBWVZESFp1OXliVGZrUnI2S29ZVTJLOWR4SjNjMlRseitoZWpKUEljajdXSm9yR3dwOFdoRWNReElOOHExeUN0RThUUTI0dllRZ0FaZVA4MXc4RC9hSWFOck9NRmFTVWJCRkNHTmE4K09qVWhuOFRkZHBvaWlGSHpPaXhQTURGSFZrU1h3cDBKTm0zSmM2cy84RnEyVmV0VGI2N3J6bGNOV3R1ZFNzZVdnOTZmSDFkYkFFbUdiUXU1a2pFNVpERGtjWFZ1Wno5T0hhZTJ1U1BTU1NGMGFvTGpoUnY1S255ODBmK2h0YU1XcEZ3TnBjUWhDZWRCc05HRXg2d3VQeXNiK2luQ1RidEdnMGllWXBibWJsb0hLNDR3SS9FeDVwYUoreEhrWWRkYzdCdmtGU0c3MnVxZjZZUEE0Rk8rU25nRDRkcUV6OFdlUVBVVXV5cVp4SEVtME83dllyWTEvSzcxR3dISi9ybkM1VTduK3FLNy9TdHZldjZGamxCYUhHZWJEVlZJRFJTL0k2YlYzTTYyZWtVWExTbWc0OW85M1Z2VTk4REkzcktMcDZOeDhXTFlSRWNBUEx3dUM1K2p0TW9CMnNMdm1QcDVZNXJnME44VmxPOHJCSlhWOXg4dFl5NVlpVzVpaDJXQVZMaG9ISkpiTUs0UDZhODJndVJqQnNXbWFVN1RweVdzd1RWN3Jxd1hZaUN3QnloWEltKzZ6alMrSjc5aUhPblo4NkhtMFAyRVFJNlVlekpTZFpkS2FoYldZMDJ3S0ZNd3VCa3hWeEsxOFNaTHFnM04yaFIwWENZdmo2Y04vaEJVR1NrYlJrZVpFeFp4T2l4R29YaW94aGUwSEplRzcydmdqYW9XMEIxRkVKekJ3RlRVYnUzVmlzSVYwdUk0TDEvQms5Wkl0NlRUYkdQNnZ4YmtnblQ0aml5T2VnTWlKbU1MZXZBU1RvR3VWZHNxNzcwdVZZTHMzTFErbXB0WSsva3MzZVdLblZialo1ZjBzTWRDZGtuZFhBT2dCSDJHL1NaVTRrMnFGK2VobG13MjdLdnVadU8zR2l4RnJLUFpTOFZtS2NJdk5JdlFGTFp4dU5VM0RKSUNxcG1hTzQwQ2x5YWZGVTc2aGhreTl6NDdaL0R0L24yUGx5Qnk5K2dJMVk4YTV6VjBHZm8xdmJleXV5SldDWUZIdEUyY0NReCtCY1E3NEk1SklyUmV1NjNZYWdkMzBTUHpxU2lhK0JhZ2EwZlN5c1JLdG5zaWthTnN5d3BpTCtySEpFSmFRcGlIWGl2Q3RSb2dYTzdPemJvdkNxL0E4V0MxR0w2c2d3WXYyQnNOaFROWmtRZURGQXRJbGl6MVIxckQ0ZlhWbkx2TUJob1pYNEFWaHhhRkpmY0tQZThmTmVjZnRvSUFFeTdhYXJVc1FLa1JyTmpxV2ZUQ2RybWprOHhWZERQWEdMOEQ4SkxRaWF6T0hnSkJMWG50K3dtV1BscXNXQlZmTXYwb041eko5ei9VU3pOdUp2bHFzM1VXbTRyL2ZoMWFnRk9pMzdzS0lmQWVpL3YxSkZ1bmMvcXdPTlFCVG44UmZrWHZDdWhUZHVBMmZObW56MXArM2lXalNKZGswRlRXbXlwQThsOWdMQ2tjVUY2RjkxZXFyelRDWnZvV043Y3Z5ME82c3hFWEdKeXY4ZGxVS0d5YnByRGpFMThhRUNNUU1VV2JUMlZEUXZrWnNvekdiS0FIZEdIUmZkN0xWd1hqdk9qWEx2ZGdNZXgvblRPQlRUajI3QzlXREx6YjQ0cEw1QjEySnAwdVlEa3FlWXBHZ3RTTDJLaTdKWkRUTkxzelBmUHpLQWk0eWRxQUN1c01weWhCTEJod2hDYWJnc3V1VTdvY2dhYk5tTFZKOTBHamJaMWFIam5lejlCQ0g0cVJUVFJycitWRzlnNmpETDFqTzQxa1dPTVlXSlFHRWNlODdRZjFoT3BTdVh1ZWE2WUhPcUxEdC8zc1JNZXBjSFA5SUQ1SVJCQkJnbitRaFVQUUtXNHVXUHZvR3lZc1lvcUZQUnRLNDJiVDR1NExRY1pWMXdWTlVhMVhIREdnSG5pS3ZVL25jb3Q2V3RQS1BQYXpvdTA5N0hscWRYMDhySE1oSlBvK0NVbjZVd2dqOG85d1NrYW5pMENwWVlhckJqMGthZjN4QklEbml6eURNZ1dwK21laytDYzNNZzk0ZkVIL1cyOE9WS3FYN0J1TWRoUVBoQnBENUJNV0wxMmlnQUUyL3RsTjVoMDNJMVdIN3RxRzVyN0NoZEg5OTdxZnFGOHFzVjUzN0xEVGwzSWlTbmU2WmEwcDVLbXBGOU51anowVHRkZCtOejkreXJLY3NFdDVmQnM0TjNWaG1HQjd4K2JSOUFxWWNBYnJhUWsvNGpNa3lsM1UwUUxyZ1dOdHpnRTRKa2V5SGRpNmoybWE5bWNnSGlSODVGb2JZTDhLOCs5OEdrSmt5MnVncTJEYy9zWkxaWktYRjZXRDVobFlMUm9FaDlGSGJiRVM5S2hwTnR6VmcrNnROTTdUSlNnVDNVcS8vVWw5Sm9KNWpUV0ZkSktsM0J2ZmVBbEdobSswU0ZCckY0VHFrY3pRWm5saFJJTFdpa0grZGlqaXY1SDJYMTZRejIvc2Y5MEY2VTRpV1hycVpMZStwbENXWU03Qno5UUQyM2duZlA2V2d1cU45ZjA5VTlkZHhYMU8xRnhQOVJUYmtMbmZLYTcxbnp5RW94K1pwT0JTNHJ0Q1lZb2QvejhPdEtmaDJHUitIWDREUW52QnplRGdyelMwc05EUWJ6eWRKY0t4N0xqT3lubjN5c2plbTlFaGx4UWRrT1JvMm9kNk9hSWNnYmZLQzFhRFBad0U1Mm92Qi9iN2xvbjRtQi90dEJSelFHVG9qV3FrbzRNTHNnMDRKVkIvUWxRWW9nVkw3cWlXaE8yMW9pSi9rTFBWYXh2aE1ZY2FDYXB2RHFCQ25WTURUSUN6RzUrZDJTeVJVUG93UEE3eWxza0ZhTzlhNWRyTzFSdFp0WDRYb0xDRndkK1J5UHd1LzFwVnptdHk1Z0RWSUsxYlZnQURQWmlhbHl6cktYdkpuaHZXa0VNOWdWR0g4ZGlTWXJ1akc0OTNwTzBCa2J6VjJxNnZEa1JJblVVVnFjTUxlbTcrTWc1N2ZCWkgwc1U5aStoKzgxRWoyVGpEQnVocmNrdXZkSTF6WUdzbm4wbG90YldOeVJObXNPSi9nSHQxMEhsejZON0x4N2lRNkZTOXN3ZGhLbzgxRWpacTduZCsrSit0Ym1EZHpUZUczR3czeW9zbkZwSitXRlF3a09oQUhzNVdKYkhaZTdlSHNmRHdsV2dZallUYVM5eDRndCs1RGZwSmdySGM4M212ak10US92NHhIODVnZTRTbG9DVGtJSXZYVUdvSk1PN3dmaFFOVkxZU0xkTXU0NEViU1BXaWJXZEdFNS82MkQ4RkNRU3VqekFxU2p4aVMrT0lIVnhXSThJWXhWUGVCbkVrTWpYSldmZjdFSDdSM0IvMTEyZDdFbDIyc0NtM1RZV0pBYVZvMXVwZzd5T2lCdHJuTUd1L0k2VmdJaVUyR283L2VheEo0Z3lmNGlCVTkvcldqRFRGTEVVR2pseWh5VDhPVTEyN3hacVRTNjIxVGpPU1NhV3hRWGZPZ05FZWl3Mm1MbXNGZE0waGs5SzRpM25QdnE5V0lnb0FGMFFUaFZub3dTbVFkYUFkV0NYNDh2MGNocmZTNExtL0JabmRzYS95aXpBU3FSeGVzdWpxclBTQzNuU21YVk1DNUFKQWJ1MktjS1A1MWZUN0JzbnJnM04vcFFzNGpxUTZ6ZFRnazJNcFU5djgwQXdHTWRSRk9wQW43elJyYVMvSmdpWW41QlFJR1kxaGczK1hjQm9pZVpRUjIxdGxaaDR5aEpwcmJEcURCQWRVMzZRSm9CeVRHMWtQYlJDSlZHUmVuSWFabzFjQ1RPb0FUY3lGV05QTDh0Zkx1WDJhanc5UndtL2MwVk92b3pybWk3UEdOZVBISFlXdlF4T2VvbEZLbmw1NnNkTC9iT2hnNjRXOE1VMEdwTEhaYmltT082SWpVOFdwQTJ0TEFxU005ZDVhTm9kQ2tLZW1RMVpaT1ZNVDZTcW5CNUozOFM0UUlWNlcxM2ovNTEwYytKRmZjVkMyUFhTSmYvcDRzNEMxbzVqbm1FemwyU3BFUldWN3Y5MHRGS1JxNG5Ub1Y1RzhBWDZoU0NvOTVDby9rV1lmWG5qYWtIM2I0aXRlbC9xWVFhVWF3OVg3dWJFZHBOWHM1QkhrYjBROG1WVFhCZ0p2cU5XUDdIS01TQmp5WUZEWi80cXBOSlNxa25vVndOWnNSbC9DN3IzdG9wZ05jQmJRZ1BIK0xGQzJ4V0R0WGZDVHg3TnJKR2UwTmJiKzN2b2d0Unp5dnZ3KzBqakU1SlFQeXNHcTBvUkM2dW93L0VQdlZLK0pHQkRtd3pCQ1J5TC9WWmsxM3dWY2k3UDdBNFE1ZlpLL3VsRm1COFk3T0xJZ3UxcWg0V0VFaHhoempUUFpXa2xKSXRpWjhBeVI2R2Ntek5PbWNSNjBDcmI2eTJCUFdMbFBwMXdha0t6NU5NeDkwRkhyd0h2Rk84bWs1NW03dzFZMURqQ2kxNmpKRy9xbDZnZUszenZCdGtWMko5VUE0YlIwVG5RRXk4ZjE3Sjk3Tmtxa3NDaFlIWGVKV1BWb2I1WHR3ajZVRE4vcXBHdXFzdWNMMTJiVnBlTnBEeEVqakpQaFNzK1ZESXY4SHd5aFBpQ0c0QXpvZHoxSkd0MHIwL3pZWUhWN2hoWE5nSXkvNU9tQy9kVEZLYTFhaWdWanBRTXl2VVl2MnJZekYvUXBBYWY5NFdwelMzNjV1MWRYekoraXYycEF5UThMb3VZOFJsU1B2UEJSWkJKaXdpTDFWbWpyNmMydXM2a1Z4MmxIU3NOV3pDMkx4WDFNdGhkUmcxeXQwWExldlRsTlpFb0dOQ0I1QXFoSk5BMEtPNHFjUUFYU2ROemd2QkhocGJ2ZUhOSENwU1dSdU1iNzhyL2FwaC9Qa3lmVHEveVplSjcya0RlbFJ0QitpRCs1b1lzRnFLMXg2N0dORktZY0FmYzlDOEF4aEkrUDRIaE80WVNXMm9jSEMzUE54dWorT1Q4ZDZXcVN0OW1RVUZvUVBHejFwOHkyWFFZWVJUL0dUTnJjNGdwdkRTakxzWHc1T3hZVytkWXgxc2ROWEFLZEhpSTZKUmRaa1lUNFlPdFVFWGF3RnNxTTBNVmY0eWlwWUNweE12VjVJUVc5TG5uWDFUVUIxYTFUYXEzYjBxNjR0Y0lHRzhNRys2RExuWlk3OXAwQlVqdVRMNXpLWGNYL2JTVkFvTjBaa1JRMGxxaHJMS2ZpNUNyaUZEc0xSWm5NY2taOUpOdUxidGI5MGE1YnBZWW1rZ0FSanR6TkdKcjJHNHl6L1BobXdoVklZQjl0ZytwZHF2ZFVzMS9QaXFMZ1pWQ3d4NXFmMTB0L1FPamUwNUtBc0lhbEF6RFZ3VmViV1pkcmxvVjZOMnV1MTJJTVR3c0NiNnVNelk0MW4rWEEyaG5yUHhKb2Q0bk5lcmJIdWVPQTN0d3AraWxYeUthWUhpQXNONEhNWVl2RVZ5dE1kc1NBTmtqeUFlWFhaNGRqS3kreCs1cUE2UVhRa1hDZ0Myd0UydEtkaFplYzZjbUhHLzBRWGtPS3dZeE1xYTZiVy9BbGRLWjhncmVheCt1TGNYYTEwTjZjT2R2V3lYQjYrcjl6L1NOU1JNaUhCQmFGdTVQVDloV0ZVSnpaWHcwSlN5REEwcGM0MytSSXJjT0pqVVNsaG8wZ3Zjdk03MDNmYk9KUHhuOU1EZXJSRm9pOTdsUVVhYkRBS284ckVYMXVNQ3pNZVkyaVo3d0R6bmdIeHRlRk1yTWZhbWMrNTBicWRJTDlXelMvMzhnYWhvY1g5Z1BidkJaZVJnSGxGc3FLOE56MVgzaWdYV1hGSXEzdTJMMmxnbGZNT3BPOFhITW5xTjBpM2RSYm9CeHcxOGhRSVZIeHY3dFZidGVxK2lYOGNBSk1IM05lN2hoLzVCMXRhUTgrS2wrSGlBQ2JMYnFOSWV3ODdUWE91S1lnMXEwTFVBdmZqa1ZLZ1BkYmVzUk8zd1oyeC9BQ2lUSTFVNUUwZFNhSVZJQ1ova2VlYzlBYjJ1cUxYYzZqNFhSY2h6MmRVTnBUdTA5bG1XNkxqWTZybndjZTRrQnd3eVRlVGUrdjI1TlR3Q0xETkIxMmF6RkI4dlVHOHo5ZmhtN0JsdFFIRTg3QjRrMlZsTXFoWU9xZjFLelNvWXVwamxrTkdySHNCc1R1bjFMZmRJWkxtNE42VDZkL3NSRkxKeEVRYVlkSzQ2eDZlUWJ4V0theFJXbzJ1elZJdkpONzBPZUlpaVF4NTE4WjdBVi9ZNjlyMUh1OGxDZnpLZmxLYWp5TjVrV3dNNkV5UzZSYXlNb0JtT0IzazVwSXRyOWU3VTZJbWY3YS9sejBteVppUjZHTnRxaEdNMWMvekROaGpBUk1LbGEwVGFrZG1xVUVxYVRXcG5TTHJpZUxORWZXQkNEWW5sTjVnNkRRVG92OUE0NGJweG9ucE1UeVduNXZpNSthRVJLNkg3SUJxK1R1R2hZdVM4ZUJEdWtueEw2dElxc2I5ZjlFOE5pak1MZ29MbTA2Sk5oUlFSL0JEZ2NPa3ljWEo1QTRoV2ZyY2xpTm81dk1tb2M2RmJuVVhSb0VjeXlFcGxxamg1ajUvd2ZQNlNJUG45UlVENS9VbzRRdlhqay9LTCtSeEVabEZMTmJLYklFbERLYTZlTVNsQjlXYTJmVW1WTkFZN3hxYnZKdU5HZEhVOHA5RzV0ZE5wem5xb1BzbWV3ZFpWUTZDczNMV2lUQ3h2KzYvTitSN2Z2clhIM3RxSkpuQVU2VEhZTE4zd1BxRGRmZktLTlZmQ04rNjJDOVk3WjN4bi9adWpDU0x3bW9lRnBvci96czBqTkhHQTdFZTRkeHRwWU1IYm40bUREMkszUGdZa29icHYvM1ZPM1NOWXhWanYrZ3QvUElqVzdWOTdXcEV4ZjZwUnFYMjAyUm5RNStpTFR4ejgxTjg4MUMyYUdvN2poQ291NEJoeWI0b1FsNW13T1BoWnliOXpjcGNOdTFtL05OWFN6NDN0bGVTL2RXcG1ROHNTd1pGb1A3V1ZlYWZOa2pjSkpvMlRCV0dnenpSTXk4VEcwYyt2RUptTko3aksrYW1KZWRYZStPK00vd0ExU29rWHdTS0xjUzM4VnhHWFNvaTUyZ2RkaG9vUGJ3TEI2QlBCd2ZWWExUNEorYVZhdVptSk5KQ0prRmJ0MjAxQlpGRktRUkRXUHhxdmFKcEwrL3RQRjRZNDJLREtyZWVMeWNyOFNBaWlvQVdQWnk4VkZDaDJ0c1BXaldHOW9WSUcyZytuVy9VRkdjcmhwa3dEV3dvUC96RGpEYThKbzA2ckk1VU1MQzB2SnNOOStNOVl2TE5vaU5sblIrVDVwK2hiL0N5OTVHTm1jZHBqaE1FU0ZhOUZlclFPMFcweWwwZTBpdGZvRzhGUk54ZFpwM0ZXcllqRFludWduOFlzVTJRUStoV1o1TDRXRHFtVHN3M1FaM3BlNzRUSERBVFdDMnJSaW5SUmcyN1dIcjErelNrK056N2JLRjZtMXVzeUg3bml2dmZqQW1wS0MraDJ2aHNmNkJOY1E0OG9BVGhNVDA5aTRkT2UwY21wQkhZakJYbnM4UHpWaS84d0ZoYjJDc3FTZm5hd2ZBR01GcnM4VGtLc3VyeDNrbzdqRnhicG44ZEkyZDl2RjM2dkdzVGZGZkQzYUlUcjdVTXhITjBvVWJObmV0WFpZUGNTRG1IZ0JkY2gzMzJTdFl6b3ZuZFdTRVdxNHNhUFM0c1liUk9TTjJ5M3BjeXNFUjBtalAwd0JLREtwTUpSV1BlSnFZOVp0V05CanBLc1F6Y2hoSXMvbDlmM1pJNUJnUHBjWko1eTVwK2dmNk5LelFrd09Ib0x5UDh1Q05WVk5aOWs5UjYzOEJSc3lxOGVuTTRRbUdSbjA2ZXQrS1hvRHF6SGlmSmpHeGErMWZJb1k5SlhRTklCcnJRdTk5M1lXV2dhTzBsMjRRWjI1bVdSblRRSVp0dnhOK3c2L0RZd3IxK2Q3QU9SR1FlOXFYYVUyYnN1MVJhU2d1Yk9mMmdLQ1Q3QnMzbnErZGJKdXU1dU1aVExVVDhBaVVEc3pEQUFQcVhyWjdGRTZUbjRicmVqZk1pZlVzM2NoVTdFRDZmVjlLcmU0dmZMbUNVcVFoRE05Z3Fwem9xcFp1VlV4aW5xeGd3RzY2b01UTXVGT2tBeW5rSUdoQS83ekthVDl2VGFCUzhxTFRHb2ZHVFdsajdxNEs5VnphdGF2Y1NMbFZ5YXdTbEgvS2g5dEdTTXFCWW5kK001Mm9uR3BNbitwVGk4MVZQSjh4ZkRzdWlwOFUyRkRaUWFQR0RGTUFqdm1tM0RQY2RXcEQzMllpYzZqOXMzcStmZmxPUkJNS3lzc1o0cXlmbVZkbWNZSytQcVhCMEFNZFpUK3U1NktrVG5UNjJxUmNBL0tSYUkxUjkxMnNjYmQ3UzdqcElJdEl5WFk1OGcwQW14MVpjcnd3ZTlmeitJSlVwajF4ZldGMWNodjNQb2JtQ3pvTm5tZFFZTFVxM2RVU3k2dEEzcnZ3NnlPVTVKM3p2WUNYMFpWOG1XS0U5YmVwY0NEQ0V3OUN4RDArc05uQklDL1pBb0FZb1hDeWNlK21HQ2hmR1YyeUFPQ2NORkRLNzlPOTRKamprc3dMNFVsOUlhQ1RBMVJ0cURVUzNTUnFXQ2VoblgwNjZVb2RUWVpqNFZTZkwzYVB3NWxmSXBnVnZraVBROGtEd1EzVk5HQVlYTjZnRysydFY5dlRSNGNDMzlsVmp4WEo4MytlTS9SRkI4eWFRZ25NRHg5YWNtQzJIQmtnbWFoNENKU0NqbHI0UDlub2FKRjd1TDlkUXZBalBzWllLOHJwdmFqQUNWSGNRZm9LcFE2YUROMDduNUJsQVIzRjAwaU9TRm5naXlHRnJxWkFQTk56dmNOK0JlbTU3NEp0bFNEVzNWOEJtVytnUGVHSDA0WkxIK3F6ZWtzeS9UT1IzRktReEVPUk5lZllaOGwyTk8vWGY1YzdaT3drOE1aZGhmMnBnd1hndTc5SitoWmpaUU5JczZrVlBDSHo5Ym9WL1lDVGMvS0FnUUM5Rm05VGtVZUJyUWpRV3h4RmxRbThPbDgzaVhxOVA4RlQ1QUgxMnhJdER2WlU2Q0FEc0NrSjc4UXhBdlBnUUk1RzM3bzRUYlZqOUhZMStTS25xVnNXSEUzQkVRdUxKd3VrTU42YjFheFdsb0RxQnRBRk8vTGIrTW13Wkoyb05kbzFndVFvb3pxNUV1QmJ3RWRrbjMzN2p3WXhDcDlhcmpGdksxekh6U1BtcGxFRkpQckk4b2JxVFhyRnlrd2wyUmNhUlU2RVZtQkZUdit0cGFSWkJCSllXYWxFQVVXU3hnNndueUxUdEVLSTFnSTd6WUhOaStXc0pRTnJ3SlNTemc0MGZFSXpjSmQ0US93amNzaFN5M0pKdEt3OElxQldjR0ZBZXRpUFBQK0hhcCtEbnhWNjdnNFkvSTU4ZDdPNDdmWHlGTTV2bWZDLzRsRVluODY3cUczMzkvbjhGWDgzamVIRmRidDNVUFIxeTJzUVdFV29DcXh2UFE1b2RlZnJOaTVHNUF2TEkvZnpqb090aG9vM21uK0Q4K2NCKzhFdXdQbVpNRDBmNlJBMFI4UE53R1JXVTZlYXp1VVRNS0I0Tm95SHVha3hjWS9mamVFc2w2OVUrOWFGbmU4eVBuWnk5QTVrN3NWcmMyQTRyVS9ET2F6ZXVhRE9yL1R3eVhISlFPMlZSMVVpMnZyb1V4WFZMRHcvZmxSN2FWWE5oT1pQN25IclNWY3lSZ1oySzVaRzBuMnRYeHIxKy9raWI1M1ZkUGtBVzlRMjdmV1BpYm1RNnBWMkhnbnA2TzM1K0VxOVpmT0tndVV4MmYwSVVKYXN2b3JZTlBwS0trcU1qTFUrL3Jpb3Vsc1FoRWNyM0V5ZDk3TkNJL1MxSElBSGM1OEh6K2lHSDFkTGtENS9Tc002Y0tGOEE5NURaeTVQZVp5ay9OWHNhNGlFcHlpa3NFLzY1TEU0aTkzNHBTWGwxMjBSelJxZG5Jc0J3L2FhUDBHMlVZVm1BR0loV1pkUzFmMWdEeWdjdFJzSnhsSGNYVjM3UmoyN3kwRWduSTF2ZzZMTFl2QkMyK0ovQ0JEcGszVDJlaTRBd2lyK0p6U3Q5UmhSQ1RxNzV6TkEwRmdVOXQzN0RyTXg2V1dSZnE3VHVmNE94QTluL1BRWXNWNEQvQWhmNWwwQ1MrMmV6ZHJOdUZsRFFLNmpIUlQwaWQvT3ZKT2RUVHcwQ1p1Z1J2aktqQXBtRFhVV0lGOGhtdk9XMCtXd01DMHpzcm56bmdjVDd4TXF3OVkwUVlWa3pSVW0xKzJGVkNoWm9iNGRsS09SVDVMejFVOXlUcCtkaGU2THIvT1M2MVE1NVUzWDlETnZFSWVoL3I2a2MwVytGRHQyc29mREVTSlRjZUs4OVdhcEI1cWM1Ui9wZkRVQjhFYXQ4T1lRT01aQkFaV0RHb3hVTDB4cnc4VzU3d3E4RldzelRKanVPNEplSmF6ZWFOS2tZWGVhN2xCMVp1RlArbkF6bU9TSk8vcGVsdzkzZVFQak9mS1RjOTdNU3h0dk03VDRYcWFMbkJPSVZrZ0VtYVV2TmUyRzU1YVZKZWJScWZXWXhSamJJRjQxNUtaOG4rSmZ4RWxPdlN1ZHJ5aVZxTXlXM01HdzJVMEFjcVY4VCtxQS9zdVZWbHJvZmxvT3ZhaW5CZXp5akJ0SFZrNXJMaUhWVWdremNFY2JDOXVETmxvODJmaEZxN082clRESkxVYTN4T2Zhc0NLbWFLSXVHeThNd0l3ZDkybkxWaG9pcE9xU2crQlViVFZXMzRXVXVuUWFwWTNLa1JINmQ1R0R3MU9sSDR6S2w1T0MrWHljQTN2ZU95MVFzK2U2Sm5MS1ZXUUFRYUNBVldRWi9GSjRtbC85aENrUEZXdmZiN2ZUZkFhQXY0TDFnZGhwVWZvT2ZkMkdUdktvWGxVUGZxZWxjVit2bjJlWkF3TFZ0S0hUc2w3UHd4N2p3RDR0T3d1ZUNSS1VwV3RLcXpoVEVjZVRJMm5CaExvRDhtRHkxUFo1UlhqSC9PRnRkM0QrUENveHY0LzRTbDAvTU9EdlFNZm0renlEOVAvZlNnK3NUcVZEZ2JuR1pRMUlMcDh2aE9ta2J0U1FrcE5qOVlQM1VrQlVraG1GUU1KQjNOVlNTeFAzaVRRYlhKZFhjMkxkY1lwNTdCY2RJMHZXalFpbXRyVTFQUFNvSFEvQ1E3Sm1TT2VCaUNVUWEzaTFCbUNlblBUNHFBL2ZIOVhFditHeGhtL09uM09SOVozanB6VTVPZUFXWjVpV01sNTdCMkc4cTIxamJyRnNTM0pGN3BUL0tOWEtORTNIWXA1NFRleEx4VXRHQ3NSS3E2OEpxbVlJVUsrZUl1RTh5a0w0SjJrTFU2dVM3QlRXbFRLTUhTeWpnS0hHTUxZODJyeFJMQzloa2cyMHMwc1ZZYmQzaElYcjdvOFBUMC9GYjE5T0puUkxQZ2Y5MFBFQmlkSXVQS2EyaHhFTzBzNXlkUHpkOGE5UHhpaTQwU0lYbXN5cmJMTDJLQmNzTmpSL1dlYUJHUGJKR0xIazM2ajJtMVRRNVQxY2RJc00zajJHbnRMa1NhMDJUVUFKeEQ5bUVMZUNJSUNHY24wSEM1S3QvWmNES0NxSUFUb2hhQWdmUlc2Q3FuZEJ1QWVaZnlTckZGNzAyMGs1Y2Mvd3liK2N3aFN6SExoUTJJa3BYVGQ2Y2kraGhxQVVxR0xTVms2RldTdGlxV1dMUzEraXdLNzFjSmozbFd0blBxNmYvSW5nbEczdWE2bXV5SjRFbDhPYmduM3NJZWRGREg2SjZZM0U5eXh0UFFMblB5enh6NktMbWFGZnlOdjlaYmtaeHQybnVuVm9XTWRPL1I2Y3AzZFJ5d2NRYkVndHQxTTY3M2s5andZWDYwZVJUVDF6Q2ExemUxUEt3UUZBUnM3U2JNeHltN3psMXgxTzkrYjdPdGRoTS8yVDdpYy96R1FWek13NDVHOFNpOG1JR2lBS05DYjkvdHVPM1l4bitCRG83TndpSkdaQ3VIVmhGZzV2Vis5b0hDUGJEcHIzM2YySEg0dlVnbUlRbFpTbDJGcHVpL0pYVE9uaE8vTkFUSG9tV1NrQlg0QkxxWDk4ZzZvUW1wWkdsSldrOHlqV1ltUlJ0ZmpJdnhuNUlMNG1zQ1ZkelB3b05SM2ROVDR2dUtobFJ6OXVmYVVzWXJaRVUzcW9RWjBIUWEyMHZIdDBPaHBKdml0ckJyMW4vUy85VTkyV3hzWWRVWUIvaVR5ckhvWTA1NjhHWmVtanBWN0daczd1RnpJQlo0L0N4dnZEeVQwSFZXc3ZoZnlKaEtoZ05PZUN0NlUwbnptVWNyUHpUVWFKQ2F2b1Z6UmRucXFja1A5TnZSM0tQbmVaZ1AyY1hxdEZmZVdsODN1eE1WOHdoRXQweXlLS3Q4aGYyZ2tTc1ljY2gvZmtoUFgvTmpqVC9SMXpsSFJUd2ZSVWtFL1VvL0FFS09FQ0oxcjMyZUtnRERKekk4MHNnaWwyY29xNXlrQkJ1aWJQTlkyL3pwM2xNOVgzd2o3NThkMnJncENFYWh2UTV5TmxObGZKUGJ5MUN4eVpCNEtvNldnOTdQNFhRMzFMdFJDSmVseENRZnR0N3c2VzBBYlBwUGhaT0VoNGRJRnhlWnJ0L0VZTnN2MUxDWEtkcVo1Mi94L1NyUitPR0xDOWdEN2VvWmthcWQ0Wkd0VCtiWldneXg5Y0VoZW9sQXpvelhLUk1pYWdzaUxob05ONHNjVG1wRFpNSFlubWxhYktaNkVRMlVNSXdPcSt2K0p5YjQ4RjRyWmZlZUplZS9ZRGw3TGZPbjUweG14bUszSUFVbXhaSEhrc0dHejNzUmNXQ3A2NXVzYi9HdlJwSlE0WmdMR25wUmU5dWw2WHZRY3ZyNHRUelM3bE5UbUlHOTF2eEhPTkt4RFlhNGkxbHozaHg2aW93OVR4dTJLVUVwU3A3Tk5MOTh2aTVOM2UrMm4vbDl2dFdmdzZFc3Z0OTFEK0hSdGorSHRCZnc2RXc1V1dsb0tGZ3VLN09UR2k5cENtMTZ2RzZRMnQ1QUNlcVl0SHNuODBKMFljaG9RZ2VLK1dsS0g1Q2oyRUlnOE51dmpmZHhPUXE1WUZweFBmTlVoYzEzTk1PY25aa2hKTHlqWFovZG5acXM5UklteHFGcXh1VUFPYnJQUkpXQVphRUFUcjZmOFdCZUl6aHgyVG82THFRbnBZZGFTNkxVLzhVUVBHKzJQbFV3allGODBVM29wQmJTbms1YnBpVUtSV1laNWF5QmFiRE9zTDV0MU5za0xvTDg2Qzc0c3l3UUN4dXA0RjlEWkhMTmV3RnN2YWtXeHVLSGJCYkVUZDFjZ2dMRXdHcjV2YWF2VVpXWE5wRWNPUmM3cHVQb0JhTHlGb2FKK1dtQkREemtoNWt3bTRaNWZPcWlRYnRMMjdsc0FNNjNDbXpqZjhSUk4zaERnR2tMNGQxT0RmcGZsWG9BaHVWK1lZUTdWUy9CTDNEcVpsVEdUZ0dEVFNwMXVpK1I2Rk9pd1lzMW9Rb3pENzBmTVVwbmNxRmtLSDVrL0xoS2o0UDQ2M3Y2aHE3SWdxSVhHYXZsb3hLcGl1MHBUUVMvUTVtREllbVlNY1lQZGdIaUhuNlJiQVNUWnd6VGczQ2cvS2RydmpDUkJidGN1OG4zY1VERC9McDJwWW9jNjJrOFBwMUV2TmE0MXJseFhaZFdEZHdjeWpuWGh0Q2pITVRwWE1XZ29MeFo1SGxhTjdibkhEandPL2FzRXg1Ty9yWitQT0tzYXhsVHFYWXVlY1B1ZllvVjgzbmxveDhQSlBKUGRVWW9iR0ZrSzZwdmdubU0wV3RjUURJK01aRHFGVlBBUnJSaW5SdHh5dnQ3RllUM3Rhc2FMZWpLQUtOQ2hLalFNcjhWVGo0MUJSTm5uNEtHaElQYWx4b3c1T1UwMnZqU3dsQUhIelZsbEZabzJOWlF1U2JyS3Jzb3JNekpmdVpVWTRRc0JyblM3RFQyenRscHMxS3ExMmdMZm5pZkhMaXZqREZwQjJyZFE4OXgxZHFZd0tYRFhTOTFIVFg3M2lOWFpzQi96dlQyeHd1dTJEYzgxQW9UZ3NIK0QreHhsdjRwUmJUWHRYSmNDUC9UeGl2N3dZdHZxeUM1L3pkVDlrNndsUmhKTnVyL3hTd3NQOEprU29sOXlkTmlKUnA4T2IxU0U1TFFXQXBVMVd6TlA2ZFNOV0ErUytPTWxRbjJPOTZvdU84M3pqeFIrUmgwSkZOQU1ReDg2cis2SWNOZHI5YlJ4eDRHSzFGR1kxMlF0QVM5aVZCSDZ2R3lFaVorc2pIbmpTNXZ2ZlZ3b3NYbjlvSy9mVHh2Ny9UL3pYZHdKcGxhMHRraUJlV25pWXUwQ2FIL3R4dUhlbFMvQVUxNlVxRWZDaklLb21SY1c4UjNxUGVnNGdmanI0YVNUZEh4RDhxdnJmc2pvNmhOWWpkdFpYczlSRHBvQ0FzV1c0Y0M3U0FRYlZwcyt1cHd2dXVHRXZ0MWhxbWg4dFpLeGhQZ0dyZktDQi9oRm9udDJsYlBaRTEwR2s4T09BS0gyNTBNZ2tNbjdPWktNTjdsS3dtMm50Q24xSjZUZGpDOVdmNzVCK3ExbHBoRGMxbnQ2MzBTU2VRZlMzci9VVndVdXZ5S1JDSVVnMzgyYmErZXZNUjlUVDF2MTY0blRCUTVoUENrKy9YWGVURzVQNWVUM2QwSUxOcEw3MEw2S1Z5VDFqOGFGelBZV1JNVHJqN1lkcGhoaG15TTJNL1lFTHJuc1E0ZVJVMDdGRm9PcHc4UVF0T2RaeHp3K0FMdjI3cWgrek1mTEF4WnJkQ1pvemprRU5pL3NDTGpMOURJQnRxWHBWTUNuejgzc0Frb0Y5c21VSUFVanNSRkpSSWk3T3BmM0NBV0pHUmpqaHRpWGt0N3czbFRnTXlFWk8xUU4wTlRJd2d3NTQwQjZPRHQ1eW5kN2tUdHh0NFFDWERLQlY0Q2VvQllhL0ZKOVR6M09BY3FIUkc5M1Jnb0ZKSTlyZVlYcmN1RnoxVDB4VktGZGF1Ynl6L2JFUnpBblY5cE5TYU1mblNKdVFJMFcyVU5RYW44RWpsS2ZNUVFNUU44K3RqOVFrWVFxR0tSVDY2VncvVjRZNzBOTzMyeCtNR1FuRmdOdkl0aFlzdFVIclhNaGp6NmU0ZnVuNEoxYnQvT2ltRmlnTldvNG5DNTUzQ21Ca1FnVUcwVW5lQWRKRGtaL1Y0RzJzZisvWjV0Wmh0SXRmMHZySFBweEZyNjgzUHVEWDNUTXJxbzBQbW1BcVliaDc5dTRKa2YwM0ZHMEtIb090UjlNT2JLaHpSN3d4RTFGaHp4Szg4SHlVM25LVjdyUmdRN2hWcmNreUo0VlJ3MzUzU21iQm9YZmFacUpUcWtCRE5wRlQ2Nmsvcm1CTjdhWU9mYjNmSlpoQVUyQlFuY1NuQVFlQUdqNWhPUk5ZYUxhdS9PTGFVaU56Y0FxUFZteFFtWTNDWm51Q3AzanovRVFMNVVDQ0Y2WXY2VUpNM0p0TEh0bHUwaXptVHNwOS9WcXY3RVd6enhEemk0d2lrQTJEM2kzb2hJRUxkN05IR2JGT2VBdXpjTmQxZElQeWR6bkd1TExJS3hmWXNHb3NZL3lPWE41empPcWNLMUFpRzFJcEVXUDhWL0pGdlBITEtaUmJsYjlncU5HNmtFemFlYTBpQUROYlpvcm1CMzNwUHB1MjRzRjloWXFHejIxRktiNHVFZVNaM3p6YnB6T1BWYlNIeWlQWWdnNGNaNFltdFZ5TG1CNkFiaEZLSUxXaVFhRWJuSW9nR2dUTENxRkJ0bUh1Qk5qTDdoZUxkWlN4QS96UjdhOFF0NlhRV0tpeXFSSjlVRitZakdITytxdW4zN0hsRHlVRlpSclpyT2lOdkdCZ2dqbk56UlBGOTJRbHlJeWxTQmVWM21TYjR1TmpES0lkUVVHMzBaYUdMNW96UjlteUxOTGREZGJCMUJJRFp6TEs4Q1NpTWJQWGo0MnMwMmhBQmY2bGE4TjNaQnFMblRuckJkYldoUjlFNUZmcitXOFZKTWhHSDdDbWhHS0ttSzJjMDJLVG0zeVVqZDFFbnRLNXYzb3Q5bk1DTmFzTTRhSHF4ZFYycmR6Uy9QZmhQL1lSOTVUL1Bvb3psYS9yUTZRYXAyOFgvRGluSFYvSmZTbENpNDl5NXlYUHd3T2JiZGpjdk5sRVAwYzlWNmZoTjczUVZhcU5kZnBod3ZoM0lvSHNWVlZaSmNGNFFyWmxyZGNYRUJ3dDFzVzdsMloyZCtSZXRTNkR6cnN6ekdya2dwMStXZi9jcW5TOGx1aExHeDgxWVd0VW1adkVTdlRRSS9JalVRWjVMaHowUGV4RTVpcXFEdE12MGtGQU93WXBMSkdQWFFVY3hZNGx0L2JZRWltamxPNFg3TGxhSG1ucUhzNDJjZUNPVTVETlNFOWMwTzZLZ1FEREFRZFV6T056L2VPcVkzTmw4M1VlcXZtM0Mva2Y2TzVSNklCVmZxOTRNYXkwUm5MemVIQWcycXhkbGxOdmgrdUhybC9ZK09XWEpmL1pMR0FHaTZ2WnRvVGhoR3VIS1JOMDhuQ1JZTE8rblJMTHN2a3pmalBMWjlOdXAxUjQyUVhVNmxlTkJ0YnNHcFh1YlgveUc3VVNvbk1DY2hISmtFRUpGZ0YzcEJscitDdHM5WVdCVGtmZlBpRWJ5bHNhRkVxNVdlSXVzVForc0VnNVIrN09ocTBQcTdpZnIyVjUvNTZ2RndoVlVHZzFMYkRENngrOVcyY0haSWV6TTZ3R2NXN0EwcUhtcDJoeWVQRjFodXUzWDBBczltZjhTd2VhbkhSRTFSeFZONy9TNVFoVVQ5a09DelR3VHRpelRNRVhRaXZNZ3VEdHErMGhnWnlrY2Jmb0l3RWR0SlpoWTM3YVdIeldVS2piaVdldjA4cDZaaXBxZjBweWhSMHJTM2R5SWY0dU9vQXlMV0NrK0x1WW8wNjFNU1QxODc0MnB0UG1WTFJKQndpbUoxclNvN0MzSXptL3hYcDFDQjRpaFV2OERudE5GM3FNUDZpK05mRXQrRzFDQUg4U29oTk5XREpJVmZBWEsrY3dBM0dHb01IbUFMM05obk1pV2JWZ00zeDgvMGYySTlhS2lpVkxWZnBvS2IreGczMnRubUhZTFZ2V2NFckZIM2tYc2dKdEtVTVBlSlFXRjdEclVHdTZNSEJNY3hqR2R5dE51aXpDMmpzWXlaS0ZaTXQ1MXdiSEIrVm4ycytNRHE4VnJaNWd0YXpGbHI3WDFheGhrYjlJQmwzVFRjV1NNSnNVdXpzZFU1Y1lxYXRFdklESm9IWTNnOU4zR2VmQnVka0JqazRIblNiV2ZjeGNPVUFySUpLaEI5TFIreEF4RGRBR21nanhtMWYyRUlGb1pyNURMcDNZL3kwN1lkTk9YK2dZcnZTc0JNTThqVTlndWJwNnB6eFdGbXR3UXUzZlJEU2RNMFpTTW1DL2dQMnhtUGFyVkxQWlFHeEpObTJaQVRWZ3VoNjdVbkN4RlJjUmVWN21OY2s4cXFpblBERUVyYUVydHZXem1rRktyNTB1MnduYkdld2xsd2hWVjk0Zm9xWDh6S3M1b0dlN1dEaERGYXdibnF0RGRzQ2JUaTduTWZVUUNNenM3RWFoZ1p2TTBQdnZZYUxMeFN2NTZXYkllNnJHdWhaZk91ZHhXdlVvL1FUWDJraCtXUFY0MmxHQ0lJY1JpK2VLQlgyQjU5d05GRHFOVWtYVmlSeEpoVmNXMkJlanBuV09ZRTl1RlpHQm45ektDN2V1b2lUMml4aHZDYjlCYmdraGZWV1VZdHpOeC9ISktpVW5aWFFsek5SY0VYUGVJQWdyT0JBaWNwcDcrMmRGUUdYSzliQ1VLenNlZGFKQmlMaWVPWU5RMndBSWZPTVRxYWZnaWc2U3pYVWF1M0pETmFxMVdDY2IyTkdRekNoVnlFWUlRbVp5cE53RzhGWVlURTBscWY0MTQ4NStOQ1A5MFdzaFM3dGtreTFWRXlNY1pDeVRybmhBeHVaR0dVY2ZHejIzbis3Vm5BczNPdzRVcDUrNDhkclNIL1c2Z2xDMzBiRXlndEl1S3daRjkrNFJvZUZ0elZTdTdpRjFPOEpsSVhJRlBPY25sSkU1bnp5TjJYK3FIdld2SzV3OFljcERVRUVOMnNUWTlTQXdxei9OcHdUWndBRUEzMTdOdEkzZlZ2TjF1NCtaNzNBc0VDTTlER1ZDTkN1UWdEVjdDc01DbXg1YmNlZHJhYmV2bzhnVENkdjV0STVuZm9YRTBYYkh3MzJPZ0tpLzQwSWs5OUZkVEpySzVPQS9SN1ZFYXBic25MR2hNODUzZHJIUEx2RlFCdVhZTjlOY0o0NE9lSm91ZmpsaDNpQnZlckdzVEhDb3ZIWnVRK0k5SXN1M1lMZFhnMHRjaVRJZzRoZ1Y3QXZkOXAyQVBSSWlhUldpTzlCUnY1Ujd5LzBNL0QreTg3TytjZkJXdTN3OVpsYzI1YTZmY3lyVEl1R2VjU28wN21uT3VsazVYYThucjlCVndlWWR5TmlwMzB5c3pWZ0V5RFdOSHRqOW8vU1p5Ti8xMU5NemJaVlZ6NUF6bktMcnJhcmgxZVI0bG9YcjFZSndwZGtvek1qdFdQUHZFZ3hvTXVRbnBVQjFkeWwrQ2cwc3lsV3g0V1pYN244N3dEMFB3cDhWclNJZXhwQ1g5OGl6Tlp0U2lKU1l1dGF4ZDg0RVB6WHNSWG1TZTVhbzd6MU9FaWJMRWkzT1phRmtMMGFjN2hYa0w2MGJDZTNmVG9ZN2RBVlJzOGFHa1ZlTFQycGxWcnVIcnBxK2lqZTViVzRFZWl2NXFBNzd4aFVKcXEwWnV3S0V0a016RnkwS2dLVzBNTll0bURWMG91aEw1R1YrVEtoSXlLRGVpZXhMUE81bmhocmExcHc1TnhPazl5ZUVPVnByTDU1bVhYZWhSKzdzQTBzM0dWcTdFdS8xTHZhWUx6bS9kT2VMWUhkYUNrRVJ3enFtUU9abnNYSGkxaEN5a3R5YjlGSktDS0tFT29mOENFaVB1dGIxbWVtRTlPYTRjZ1JHeG5BdWZRNGxCSmY0N09yNmdFci9aL2lCa0RDdlBRSFRLak1ta2RGeFJjSDRrRk5BVFhXTFV6amVjM21VbkhCQk10QjNaVWdEd1ZvQ3ZiRkFMMmdDbUhvUFNmUlNkdFQyQnh3Rk5JbnRJTFFVeEc3T05Ga1Azc2RxWXV1RGM5TndZWDNaZ01wMHp2TGpHa2srS1FtN2hlbDcrb3h6cHdXVDEycUdwQzFPWXZMN3Aza0x5TnU1ZXdSM0NpR2dYMThCbEE2Z3hET0RySEh5L3ZrL2YxdWtvZ3BseVNsMnJkWVpwQWwvRHUvV1FROXM5Q09WRGRRS1BPUUNDUkdnVVp6YkRUUGFNYkJoeExwTDZQNlZ6cDd0WTg1K3N0dDJ6YmFRdVdETTE1K1hTaHdlOFgrNnlNcW9SUVNhZnJFMVBrb2dtTm1la0oyUURpMGVyZThsWUFoNUVmZGNucU5WQXprVTF6enRHclFrS2MxdFFlR2tBeGNobDJ0aTlUUHVMUzFzdWVzSlpieUNFdWRrb1JXU3FRaWg2TVdtY0ZEa21TWUlBU0RIU2JIUHV0MS9mcWJSL2FFUFFENFhJbkI1WTcvQm0zbHFnUGo1Yko0Kzk0L1krbWc4TUVxZGJ4d1hUUmNEUkdTS1NSTWNRTk1YOU9Bd2VKUXlZTlFmcnVSbWhyazB4V0JyREZqeFhsMVlxeUtxc3I2dVBlVm1pWVVPNnIyU0lrbFd6MU56ZlpvV3F0OTY3akNqeDQrckhpUjNBREF5eVErUzRHemJZRmxFREpqa0xMUHhvajdSSTZLZUx4ejlwSytlM2Fla1pFcHlsQkYrZExJb3N2TEhyODl5bCtJelRBeWhiOHFEK1dCakJTNTlJVEtLbTBta3JoKy9OU0Q1WHB3MU1uNDc5UFpKdklVNEd6RW5YVEtBZDVPSEp0UEpMTWtUVk84S1lyL3VDZENpZkJMSFRMUjNPY0FVTVNiSWN3TzlPajJBenJTeDQ4M0pTeTVCQnYvR2JFYWZqYkRsbHlHaEhBWmFDbUYzZzl3cWUwSmU4dURybkVtb0NKY3ZVZS9pWDN6THlzNG83RnpkaHZoQ3FiSUVsV0RWeG5RTzRZd0R2b2ErLzJISFFTYWVrZFJlRWdQMjZNZEI0dlIrLzhmTVhrT3d6ZDE1ZnBsbDVPYlVVWVNsNlo2VE84aDhjanZzd0c1N1dZUmVDdklZMTNTVXlvdWwvV05WNXdLM1U2WjVLemYzZmw0S010WmE5UzdzRkxyZDBEa3NCM1RqWDlvdDB3RnIxRUNvMHpWS2JDWWVLaVFNSWhEOVdtWnRGMnJzSzFrdnNpWGJSeTl1Z25JUkJJbnViR2VFayt6RnpBQVhRZXpIZlZ6NWpFUWt6bXgvSjlCYmpLbG8xZlYyZlBzYlJ5aDdEaTcxT2dWWnhjaDBreXRIa1BHanhyQXJlNUsyOUEvUHd2Qjg4OENPNWRUMGlSbFR1eDZwVjNjYkt6K0FMVjFIRkoxbU13dGZkRE53dC9ZQlY1K2QyejBwVXZGOWR6eHN5RTRRdkoyWFFVU3hDYXd1bTRRY2tZTEFCVXVUbEZkYmNtTkI4VkwxUVdCQmhKK3RjSjkvUDNQblR2YkpsdTdjSjJaR0JiK3BQcjY3SnFDdFppazFpWG9DSWw5UFVSamNFWmcyNXNyYlVoWjFnMGt3aEhRMFhvWlVnZ1ltcGVkWHFwNXc3bjIvRDg5UGFQeGZoK2RaTWhhTUFsdDFMUTRtZmM3SDZGcndzTHV2U2pqMUc1T28zZGJ2MDI2MUo5ak5YL2pUTHk3KzA4ZXpYYVQxRWtxZXBXdzllb1dtL1dIQ2U5bE5mT3F3SkowM2dOT0tXK0pxVG9KeHJHT2N1Ums0S3NPTHdVRnBkRFNWVDF2NXU0REY3dWNQeDRacWtlZ0N2eFExYmk0TUV4V1hMZ0xNbE53R2VBNkJMc216OFV4TmNQZEZrVlE5SFpRUW9Bc3pORXlGZGtkL2pGcUJtc2NNU05mR0N4aStlVmhnUVVFRWlXRjEvVS9FV1F2ak9yMHlSK1pmOXVKU1d0M0RnZ1pRNjlMU1V0U0d0NXR2Ky9LcjFVbytYQVptN2c4c2I0bFRCU1JXVnFlc3ZKQmgyZjhhVE9rWC9OU3JPUU12ODkzWjUweEZpeXUzbFB6WWp0NmlCc1JoNlRJdWJBN0ZiOC9CMXBDWURDMm1FSWFlUlF0c3ZuTzUrUVBGVkVDWW10MXhjbUVxV2V5SDVUT0VjRWhhc3JwVlByUmRJaGx1bWRnNUlPM2NZcVhjdjBKRk9SZzVkUGdqTmRmZCt3UEQybjJLNGFWMVFPQ3BDRW9SeWtlMUZsNGkvaCtoeGw3MjNwaHdWZDU5N2xFeW9haW5ORnNsOEZncGlxNlRiN3NTME9PWmc3c1FRVkRIUHRpOUZkeno4R241T2p2eEZWMy9TdmFOVEF6aFJuN1hZQnhNT0NtZXNoNk5QTWs4WU9NcUhiVENtRnFXV0tJOTlFc1NwRjhrdTBMNEw1TkczTDJFcGhVRVo4SW1idW1VZ1p2YlFHOW03WFpWZC96MTFxUDNWbFM4ZDIxcG5QMzBrMnNSa3cvbktNTnFaMG15OVRFZXFYRTVWQ1dwSnl3SWgyR21wNWUyOEY4bmowVVNaSkFrZEpFRFpiWHNXditBWDMzY1hYRkNFVXVZWlJyMU14TVNKaTM2czJSeXoxc3VuVW9PN3BJbml4UHJaMHB6NjVzVlc4R0hNOG41b0tHZDBIdVR1MnY4UVo1d3F1b25ibWRIMmYwLzQ3Mno3aVF1MXJubmxGbXZzTDZidnB3Nk5KS3RiYzRPNGx4QWJ6ZlBKR20rQ0VNT1NxQW0ybXBaYm1QNGEva09nTlUxNTEzdENzK0hvODVEUnNmcWtnMUpRcFJuZnpYRXNSNENGVTZ0K1pvWithMnEwek5tbWhxNmpQWjRqTFV3Z0ZHM1B2eWhjVEh1aU1HanFGdE9yM0c2bVQzV2ZPTk5vL3JYYmlBZEc5M3UyVWxqNTR2ZFZKb0NmbC9zRWhSYWJRR0xRUDdLNlBzOFpmZzlpSlVFY1hYMDNVZ3crMllBMnFnUXNOWTJVUHNGZE0rY3ZEQnF3MFY0emJrdDBJVDhuMDA3anRtb01KODY4bjE1ZzBMVG01eEErQzVkendmNU1rTVVRRzRoRlZ4alN4bkYrdzUwK0lVMHZKaTZ5blFVNWxmcmJMYWJHNHFGaGV2Nm1Ma1ZOL24vbUhDdmhRNkJJZmF6TUVDMjJsUFNYWEU3d0FqdHZua3BUeUlDeWNSK3ZZUk83RUJrM29NY2pxaGZrRlJKOWdYRjRrZFpiaUFzSVFLcEdWUzBaRmszY0oyWXJkcnVEODFVbzBjS05nZTYrRHc5MmZsWVlsVzF0bnlTNVc4ancyZjRPQVdyYi92SUNZbTEyc2JIWk9HQ1dJZzIwUXo3dWRiWVhNeDNzbGlBanl6Mk1HSG5uNTRxa3lUdG1iZzZOU3RNek5Dd3RFL2E4c2FTemFtRlJDN1lGMHBsYm5ISkdvcm16NGdybGVTOHJxTmhSdEc5bWtJdnN5Tis4ZlJOcjVVVExXOXhLZDU0MDdqbjBZR2VmdFlneTZFR1FnclJPcWdHUTJwWGFKTmtQWGFPajlGQzM2cEt3Ujh0eUJBRjZXMC8vYW5BdU9UYzZ2YmxVdHJpMlR4VXpYY0k5RDZmRHd3eG9EdG5nV01lSklDNDhxMGFRQUp6RTBQeW8rM044cWRXM3JpZHJDUm05K3lPcHd5S29BTFlsWERWeTRkTXZSTHhDeFJrb3A1R2RwNE9mV2Fvai9wRkR1b05DT3M3MWpYWm4xcEFLWXJaeDRsM05aeGczOEdrYkdaUHJ0RXl4RUo3ckpYNGtNOS9PbU1SazRGY2dLNnlOdTliYkh5WjRsMHE3MkVQbWNVeXc1Zm00OFdPanVMWCtmdTFJeDBkYWEveVRmYnRpMFJ5NmdSQk1EVzhNMEpRcHJLSDBTejhBYXk5VHlWZXVZUlk4b2VSbDZHeVVjNW9ZU0ZYSm4vcGtoS2h2aDY1cUpCcGFSd09RUjhxeXJlZEF5MW1YZDZXd21NcmVpcnF4RGdTR3RLMm1uLzZpRDFJdm11NXlkZXRudSsyb1UvemlFc1psSUxETzlTTisyb0NaZEJJSlJVOEZNUURqWThkN3U5N2dtUjlQRnFoMklkZ1BxSEJSVWRNNGtrdmNYNWFJU2p0RjBIVkNxU1U5QWt4SWFHWG9FVVFLYktGUUxXV3A3aG05aEp4cmxDLzRpanhiQ2FjMFJDaW5kL3IvU09oWHIrK29RazduNGNsRUFTQlpES2RWdFhCZ2lxUlBWTFVCYU9wdk5OWVBBRDl2bUw4NEZCblFHYzRsdURWU2NBVElPOFgwaEdwNGo1SGJFbDhJUlAyZUpFdU5hcXltSDgvWHJjaG40QmRIY05pUE8xVkhqcUh2QWVScDV2ODFtM3RoK1JaalBDTWhWSFV0TUd6NCtGNjNnck1LRmkraktxckhDQjlIeGFUZ0p6TVdUbG5hbHdNbW9KaWoxVHVCNllDZzBEMU9RcisyVEc0aGdocHowTnhNUFkvaEJ4YnBMVXNKbS9wOVFSUlIySXRTTTU0QlZwZUhPK1JhL0NKSFdSRE9zYmYzRnJvcjVOM282T0ZUdnpFS25QeEwwYkdzMUp0Z0tORVExbCtweWdOUlZXYW5EZG5qOU1yQ1VZK2UveXIrVnhBeUFtV2tpN212RThYMjVCdFNyZGowVzc5WmNVYW10RnVIdTlZVnpqUlN0S3ZwZ244Si9kYy9rRDAxVCtiZW9xYi9kYlBJY1dsVDcwTmlXby9ZczV3RnFwcy8xd3F5UEpWV0thZ2t5aGFYVEdTWk1EYzdmSWJORnU4L1B5MG4xNk1oNlBvQ2F4T0M2VEFlL0dkZjdCZmpHNmdEYmk1WFBsLzd3dkRWRDhEZ0VUUWYycDZ6WVRPYVEva2JuQjdYWWFuV0dhVmxJRFUydXJqSzRTODFzYkl2ZmorQXl4ZUt0VHF5WDh4MzZYODVBTHNhTkFrVVVyNWxBVTJMS1FyeTlJRW5aUHBMUENEOG44aWJaaU1KTXBsYno2UmJETW9mZTZSWHMyaVdzMWI1ZzlrVEFKeVdHMXFuNUtlaUo5UzhYYUIwenk2bmk2bC9paHZtUmpLT2k2Q3ZZcEYwODJPemlkOU9LbzlMSWpxeUpXcnBaeTlQajBQMEl2NldleklWYkZoVjVMNkdXQ0s1R3FLNWF1MTB6Sm1RbG01QmxMd1lJSXBFaFNMV1crWkIrK2dldHNqb2FONEJoTUcySEdZUVpJL2MwaFROakM2bS9YYjQ0SWR5YVNJNjQ5b2d2MTMwdk50Uml3Wjg5dTJqWkZVckpRMnF2UTRTNVpMZTNHNnJadmJaMWkzdE56WG01V3JsUXJTaE5MamdqTVpoVkV2WFhNdGdwcXdCS3ZUUXU5cWRqNTB1SzBOWVMrVDZScEJqUVVxZ011R1poeWloYUt0R0lacmZCRmNRVm55alF4UU15Q3A2aDRmbU1wUTZNenlHcTB5UzZMWm1SeVc5R0Z6N1dqNjA1KzlwS3JjSitNUzhqemZiUVI1bmFoaGJETnB3RWcxMFVKblVtck1vR09ENHV6dDJIL0hlNFZ3S0FwclRDcndsT0JtdmhuWm8wcW92dW5OekVBRjJvdnprajBHVGxRSGJqQ2xlai96b3NsKzQ3QmlsdlFjZUlCZUg3K3RHbnpERXpIWndZUkNvMGJvcFZpR2wxU3orY2JldHJNZWRyQTBuZWdSU01xaUJOZ2wxL2NTM0FHMkVUK3VaeVZhWnVMNmFzZW5SdnhIeVVPcUoxZVZ6L0ZvRUZwaDhMWjYyVHhRS2l3S1pFbnJsSUNPUHJWbkplK0pxNklYUjczQXFEWUtWQ2VSVTk4STNiYW5lR0FQRnpMU0JvK0RiYUpsWjA5K0p1SGlHUnFWaE91K2FMLzk5Y2Ewa3NmMTdNcU8xYjA3TEFoTnVKZ1dPbGhYYlpXeUY5Ris5bC9WdDRMNVBxWC9uNi9ZWHovZEgvVnYzZ2gzNzB2MWFUa3lsRkhOajdRYTFtTDFUN3NRSGlsQk5mTldBbm54TUVFaE5yU2pCSDdHSlE5U1JVWmRUZHhHaFJPaVFxeXUrMVR4eXVQTlRPY0IvNWpnMUoyMnQzQkNnbHBPbXgrQkhZSkpuZnpBYjl5dXgyTEZaTlI1b0xEa0NTWGgrWFVweUZITE5rZHYxK09Ta1RwQWxYUk1FamhIOUo4N25ZeTVMT2h3YjI4UWhhSGJ5Qjd0ZEZ6UmFFSnFNU2RyRzdSQVlhNHBBdjgrYS9iMmlrajVxMzM3d2tmYkJ2bXhNd0NITWpnZGRGV2FzdURmbSt6WGpOTFFEczZJRlIrV3dHcDRZS1BqaGc0NHR6SDJLVVhTTTYvZkIrYllCS0VlWS8zR3FnTkRPOTFVV0M5bE55OXBaMjZhL29tdkpCT3p0Z0NLRVhBdWRUc0x6c3AzbWJnWTdyVVNFZ1FUaEZuNkxlbFZUYWlEU0IvTjR5V0xlYmR0V0xOem9TM1hiZHB6QjZDdHIrcHdES09kVVkrOFZ4U0M5ZVJYUHo0d0NyVndHYWZHcmswUVI2T1NjZGp1aDhqNkNGT1RpS05Pbkx6cU94TmIrWW55VHVITEJyZGJMRlU5a1lTZmJwYXFScElRL1pGcDVLZlpVM0xPME9qQ0UyNjZZZkZKL1JYQzJaZ1ZBbWIvUjUrSDFlK3NxRlRNWi8wU1QxSkZzbmg1Q2ZGekVxcFlzN3VKNzdBcTlmU2Fma3lFcVdaQlhCTWt2Tnoza1N1SWhIZ1ZkSXN4YmJ2d1V0L29KaXVrbzE1b1BIenZSNGNTVVQxcS9KZXNHcFVoK09wRlpPN01JV3l4NmJUY0pBQkthbVBaYzFpdWJueUZoa2Q4YTRVM25vWUIvWWsrY2EzVEwxcGV1VUxyNUpMZ2hocUE3d2xxWEMvUnpjUlFFeFExeHJBYmFnQUVLR3Uwa1N6cHVjeWsybitISWRGVXZCMGlVczhNS3U5OGdlYmdBbXZmT29aWTVPUEJuQjNzc2RaMlB4OS80dWs5SXlCRDdPWmNKRG1RR28vT043c3hCQVFhWUZGN2FpNkQ3SWRwOExZdjUwNWNGeDNybHc3ckhOSVN0ZDBPMitGR3NDNWkwMkIwb3B3RFp6SWJ1ankxVGRHbTBxaVcySm44bkd5ak9rWHo1bzFWeGUrelFEVVZpVVhGYk9WcjJ4cGEvUlE2MERVbXhsa1dzNFpTeFlZSHJVNWVERW80Q1R2dHBqdTQ2Yi9YRFIxd0syUWI1VVQ3UnhSQmdnbWdlczFGQTZPUmlacTBhYmVqVkZjRWhLNytnNThjRDR3RVE3Qk9rZCt6bW1qZktNZllzaGE4L2xnWlBTTUZFOGZTdGlES1poeGVxU3o3a0R5Yk9PNlNrWTRSTmRITnkzMnZPajFjNWhQUzNrS0VWa29lLzJHTGJicHY1Q1Z3RjV6dUF1N0NKSnhvM3RHa0FFR1VHb01zQ3FYUFFMZVRZNlJPS0RKWERlWkxzZGVlamFHSXNxV1NhSFBKVmJubkJPc3JCNGdyMmJiY2ZRKzNzdkNzUGY3R1JseGs0ajVSMmhGQ1k1T090a0t0MzhiS2htSmRpRGhoeUZYWTlsSkVFbWIvZnhLbm9BUHlvTFIwU1U0RjFaWnJDWmZMdnFBMzhoeVFZRUZVdXNSMFFidFh0N3hqcWVFMzROb3I4V1FicFh2MkhmNUdHOHRQSG9kQUZxbEpxb2hmaUhGME5nKzFVWEhiTXRvVGVJc2NGc0dXY0lxbXNGSTRYMjVkWWJMSThxTFFQeGM0Tm94NGhvWDZoYTE0K0o5TkxJcDIwdlBETVM0US9kbzYwZWpEWitXUGluSG9RenorU0ROYzFHV1ZHMzU4VXZXRWlpdnFqc0NSV20xaFc3OVVZbDFqODY4SWtlMGx6VmJyTTk5QTlzUVBUNURxb3VONGwycTRYNDJ1U1pqYjIzNFZNbzFSdHE0TjYrQVMvTVU3cUxQTmlLMEtBcHVlNlBVSG9SN3pwcHZDZ0tyMk44Ym9qNEZjR2htZ1UvR1JVdWducE0zazMvazdXUE9yWHNJQlhxaEt6VFBLcHhZUER1citXcEtLc255VERCUUxjcW8yZHdETE9QUHY2SmpiVk1EcjNlbE5JL3NHUE5ONzZGMWorc3lia1lDdnU1UGl2Y05UaXFVdndMUHpYMlZYUzFjWVpHZFErSnBiNDlPYW96cUUySTZqMC8wbUFNMlppQ1B5VS94U0tQS1NkbUdsaXpuMnNnLzdmVlRoQ3huK251ZDQvNzRmZHpyaUFENVBmMk83MkRqV2Z2R3dNdEIzT0dNVGJ6K01QUE83UXRTaFFCeENZeHIrcGI5eFRMWUZCWEI0SDFrbDZKWFRjUG4zbUx5U0laMDBqdUw4V1JIT3JSUnFIcHAvaVUrOWZYUTB4K1BJaWxsbnEyZm9USDd4bUJmVGk4cEF0Nm9NbkE2dmRIcytkZ3pySU9JVG1ReXVjWDh5V0taVXk2a05wVXZxaEdmRE9sUUZGRkZaWEtwMjlGY2tPN01LVHE0YW9ZY3hLN1FlaVlBc2hNeDlncElaZlRHbDdOOCs5eGVOcDNjUmptdVdxSWV4cVJuNU1Ta01Xb2VLdHByaFRSdktidlMxZXVUejB6NVdLOElwUzc5Wm9MeklNcE5GZ25QMFd0ZVhpVC9KT0FJRHpNSEVEWFBQTnBBZUxPL2Z5YzIvaEJNMDh1d1k2T1BQZlZ5RExUOFVDT2hBekZuTkZqUERJejBUbkpGQVQ5WTN0Nkl6U1RyUFFaU0R4bDlKb3VLMk5FN2dsWmNQTjNETER2bnVNUm5tZjM0YTQvV3B4eXJxREJlenNoUkNxbTdFd0F2dTgwd2p0VFlLU3RjUFRJRUNKZDJqdHlIeVZvVzdNR2JURFJZUUZkM1dFbHN4N084WkJTd3FZTTgrazdNRXNWcHFleUcvOWRDUFZmUTdqdFdjR0NVK1ZhYkU5bXVlOVRsWldGelhaNGZ0dnk0enBMOHVISHh6Uy8wVkgrQlUyMUVCTDB0alRvLzljNzdLWVNlRUxiWWdoM0Q4bjdaUFFWY25PS0lIUmdIVllGV05HRjdNdURJR29YM3ZkMXhmRkVCVGR6L1FlRlFvTllvcnZqeHdVdFEwZWx5ZEZuYjdRQjRxLzdIbDVSKzJOL0VRNGttai9NeEVQTWpNNm1jRWtkWjNQUVlGYnNwb2FSVzdNY2ZUcFYwUnBwN3h5WHluZSs3Z1o4TC9GSFZEK1h3T0ZZTk1yMUp6QzlTc1hGTDRrYjRqYlROM3V5YnlkdlRQRG5aRnpVcTV3bEZZUmkvSHF2TDY1a2kxVVBGWSt6OElTTlNJelUzMktBdGxKOTliRExZVVdxaTZ5ejdiUjN4RGRZTVp0eXFiSkE3cHQyRmFjencvZWtFdHZRNXJYdHNuSUR5a0tsZmtPSWQzNVlrbndLampwWWs4N2xIelZGOGNiTUs0Z2EzWnY0bDExbFNRN083WVV1SGhqSHkwVXM3eGlUUXZkdkZrM21LQjNYSGJtckdKVERJT214OElVcDVNWGlkRjdaZ3l4SlFhay9jSWlObU5rdGlCQS94aGp2WkJTeno3bGtSZWsxU0NLVmVvMnFFR1orSUkraFZLdkpLeld0UllldTF2b0RFMUNJTHFPb3hURStpblVLNHFUVUMyZkZobnBVOEx1Wk9FSGpmS09mUjRuaDBTMHFYSEdtTlh0SXBadVRENTRZYy9zOWdSU3duNHlpbDc4YWhUWFRqQlZsalVPV1NxeERCdXFZN01VUkNpT2sxWDY4NGVwdTUrS0xvc3o3VHdhY2V3OHUrWHFudEVaZjdtZDVIN2ZhdlpBUkN2VGlvWWVBL1M5dnNtc3kyZkdrTnRrUmJtT2ZoVTM2WWxnaW9ZTTEvVVczNkswUGJteU0xNVlxYnRCSjdaSm5iN3EvazZjWWhDUGM4Q2tVbktMT2pSelJOUFZqQUFhQmxEL0VyanlBSVB3ZnJHem1DSnV5cWRSak5zRk9zSGtOT0lQMit2dEpZTFVsc0c4LzBhSkVpSk01NXZPQUM2RXpaUmVYK3lMVkF0YnFNRlo3WlU1VENRamdkcXVVUlhKYXpBTm1xbXdoNnZCQklmbUtKOU5lbDVzdWd6WnlsKzBWT0FEKzJOczR0NUovRFVzOGcxcHBRazdEZ1BrOUJicngzZzBDeW9tT3NjSDRhWDFtNmIxZzk4aWIySWFlZUtwS2lvUkpnNkg1MTFRamFmQWlOc1hIUlVPU0JOMkcvbEtxTlVkOE1NSGowakR1NUlTUjVNaGZhUEdQUllVVGcrZkFTb21VcHNzcU9SSlVpUUh1ODM1V1kxdHRKWnl3aCtEU2szdmd6cE9YQkp1NDZsU1dJUW9pU0EycVFDL3UwamxyTGtlV1Jsa1BxVXJrZjdxdGNHZU92TTVwNVRQNGh6ZDB2eTdOYzZWUFJ4OTd2VURzUnl4L3UvL2EwVDJCZ0RRTC9VS3dkdnJ6VzJjZEUxR3Z1QW56S3piSW0vSVlrRStmM2ZOMGpvM21idTZ3anVRVSs1a0U2ajJlTFhTd3pJNUR6cjAwY0JVTjRaQjFRZTh0S2x1SWl0MHlXRy9zeVA5WUxRSHd3TUxuUUdoUUJPNEwwV0QxcGtNUHVtNG5WYld5WmdBcXRJQ0NzclVxT2hwQThaOFh6QS9xU0hWWHZVSXlXNlFyV0hRSWFxUHhUTEhZL1o4WG45VUhvQ1JpSW1DUWVZUVRPUmljT2JjM1ZiVWRsRWJENjdjbENZUXJQZXJVZ3pSVVg3Zm1ORmNOY2w0aHMvaXpVUlA3aGNMZWhMWFd0N3FZT01KVWtFOFVZZVZaclRTSFFIL2NkTGc4SVR5YnUyVUpOZ2RRcTE5d21lSkpqRVdFSDV6NGhrTG41SGh6SWJIOXo0Zjl6NmVrMGxBb05WZ2svc3RjaHJFOHlXcmR6V0ZTczloUmRLcXIwZnFvN1hZVEVJTzZpb2R2YThPTk4rbFp0T1dUazVkaGZ4VTNiVVVqdHdTQmxjbFpPckRIYlZkcERxZGFvb21zdDdMcVFDM1lpR3ByVjhLeU5hM3hwNUl0SG0vakhGby9oekdGSjBwK2NWajRiSmZRdENEYko0dkQzcHdzSFFWcDZBZjRzUW5SVTk0cTI2TUdIb25TUDZIMXI5U0RuQW40dmtLS0JLRldPUGxkeWpVUHZRaDZqbndXNUFKSHdiZ0xZcHlEZ21WdmxUY3pCMVRrSHJyY2l4RkxDQ25PbHV6OXJiQkdaK0ZEWFdVVDNOcDVMRFRvUmlkNGI3bWFKOHE4eVNUdTdiOGQ5SXhDcWNZc3F6WlZLclFITFlMVEdJT2t5Q0lrcHU5Sm5HU2JmNDlnQjN0cHN3VUFEYzcyUXk1aGR3ejhsWXFtc1ZSWXRKcVduaTAyTjNHWm1Tc1hGbW5XR0twVkxabWtEcmRETG9MRjFTbGdLTVNGNnV1T2s0L1MxeURDWkRTc3R2NFpabnVjTklOSkxBdWJURlBaMCtzM3RtTUhhQnR6djdRU2lQQy9yU3JSR0N4eFNtMGQwNUcwTWpnbWcvWjVvam1CU0hUWFRUMC9hMGluWG1kN3B1cGV3a2NXWTJiS3NWQ1QzOXNVVi9INlBOaW1WUXJHdTMxSjU1bVpQbUZLa0N4TnIwbHdtYnlrTEI1UkhjTTh2U2F6VHJzRlVYcWw0WTEzc1NyWjlpdjBpL0FkcjlPanZzcVI1bVN0US9MREloTHpUc3UrS2RwSHlHOXRzN0VpeFFsb0RWSEwxa1JZWlNYeS84OU9tcis5MnFuYVVGMWQzQzM3dk8zbzZkMFExbHM0clRvRC95TXNEb3dKRFcrY01HUWpocEdNSE1KejVUNXZXU2c0VkNLWXIxUEZLRjZ6bVVCeVVIZXlyaEVWU3ZBSWs1NVFjalNadTVrZUk1R0ZEU3ZJd1lMcy8zcGgzZzh2bVVJbHlXT0ZDb0p2bTRwalE3OUxrUmh3VDNMUnJqWGtkQ0NvWjltYldqb2psZ1cxd2I4eTMzUnMvMG4zN1hacUhnS2o4cHYvc0RQa3dRU2lTdnNCUzdzYjJIL1V4NFExVVZpZGtLRmxHNWpySVB6cDVmSXBBaVFWQ0RzZXo1NnhqeTNHdmNqYnpFMWZVaENiMFBsWnpncm82K3VvRWJGTkxpRmdCWkUxYkdsRG9rTmtOeXRKbVU3eklVbjQ2QzA5WGJ6bjZZRE02QVpvQWg3WHdUVVhyeHVHcUlZVlkyczVPUTFpSFVTbE91UUw2V210ZGZMdCtVL0c1ZVJ6bkprUGdoREpESi80QThaNkhrcDAvanZXY0xNc1k4bGNkM0NvYWNYRlBWVzdOZzdDdFV4YURlR0kwVmZFL1FGbzZLRVhuQ3YyOEJ6emJjYW56SnZxUWxWdlhwRGRyVC9WM0JCam9uRkpPTEtlSW8rTXNiWHFtd2x0WTMrSktEaGsycDZlUGs0Y0R2NEg5V2xWNk0vSnRtNVNwZkRMaUNlb3JmTjI5MVRQb2o5aXZiMUhmcUFFeUhBVXREcnhQVlhrRjdnMFdIazhZYUEyZmFOMkxmcksvd3VkQmJJbnVJYU1wbitudVF5UmVwcHNGNkV5dGJnZ2xnZW5SZ014OUpFaC9JelowM2dqVDJCSEVqeXgrREZlZEdoMC9Zams1TjVGUmdZdTRhTVFyS3FkN0ZLNm9hTlcrdEJTQkF3UUdUSml1dkNYNXNzUmt1ZzViZzdNenBnL3FXYTVVN3cyRTNzNWNYL0NBb3I5Z0Frdm0wNmxKSkVqQXFJellhVlRxNEx0a2FybFNvMitYdi9YL3ZueWZmVU1PRnM1RUxJb3JBS05lNDFsZjI5dkhKQ3NvR2Y1NUJqQUxHRTMyYVNuZWpNUHUyT2Rra0JhZUE3bWw2aWM2Z1ZRaEdJbW8yKzBZSGdxSzAyQXhjSmNVS1RkelA1V1NKaGRTSWdnZHEveGVyWjBCbmZEREF5Tlc0cmtuVTlaZ0pnRVptUzBhS1dHdzA1N0E4Z1VPd1pvUnpHUFZlUmxnb0o4d0FZaTMxWFczYXA5c0hIQmw1OE5xaklQWVRiUmQrV1pMeVJ2MVZzU1lMOTE2SGxmWGN6akNXa3VMc29XUkpFOWRqN2FHeCt2MVk3dnRlSFQwUU5RS1BLVVBBb2lFZ0hyTEFhZzVDdjFvUW1hVEdTRGlIME15ck5meXlQQXNtUGt1WUtPQ1hVcHBvL1IrYTlEbEhNSG5nMGxuYk9mSnVQdmJBT0lDU2VuTnpzN0llL0VWcHpEUHpPWjJmclMvd2l2VkFXMUluWlNxYjNnK1RGWm5GTktUQWF3Wm90QzJrYUhERUl1dklVYWhhZFZSazMrbEdmMWRIUXRxTFJJNGhVUnFXTGNWWDduZXROV05nZE52UWlIVjR1N2dNdkczbEMyZ0hmTThQMDlPNDRMN2tCSzZRYTdYUWhKL1ROV0gxamNNZUZLVm5Kd1ZXSHZDNzZqaU9nTHFGSkczS2t1eFVteW9IZ0IrUEFtRlRwZWhYdlI1UUhvZ2FKRkFCeTcySnBJZWJHWEIxRVAwWk95dGJ1VUZZaml2SHdIT21KZ1hTUktvd3YrY0RYemlOeTRGR3hBeXVVRCsxRGpJMWt5QUR3K3hEclRrKzBoYi9kZkpzai9WbGdaZWJ4T2lBWjkrL01QZmhhb3J1TFlsSVo4NFJGNDhKZnE5QllpdUlCUjVRTFFQcGdaNDRMS29yV0NxODFMMzU3b1hvOXJDMHhpLzY0T3RSbkdtbDlzbEVqWitRYkdESDJJdHl5Zi9jOTFjL0ZYRUR6TGtyV3pvNHB3V1UzcW5HVHRNRFp2bVBmQ0ZyV2FUajJDVXlaRTBHenI4T1lKQ0gxQ3Vmb2FRdkM2UlozR0ZKY0wzTlpySi9CZUI3N205N2NzYmc2WWJ2R0RiZzcwbXIzbW5LYzB1OXFiS0JPc0oxb3h1Z1c1cXRPWEhsNTZaTExRTmpJOFBxSStUL3c1N0E3NDZoMHI2d1RlYnVSQ3I0ZHpMNnh1UHJNK3BoeU5TWTNKd2puMHFUSTdxQ1cvdjV1aWdSVlVqQUpJWFpzR3M5cUMwMHgvcW1FSEd4QjlzL2M3eVFiZFY2Z1duTC9jUG5EUGM1VTkvMGZ0ckJBOTlmTG1LTm9KcW9OREV1NVdwZ2MzbnBsN2w0UmpLaWRJbXVVQXFKS2xQZGE2ZTdEZHd2SWZhVHBLTFdIRkZBYTd2aDNMUW9CcWpoQVh5cTJ1UFhQdnhrZkcwem9NK2pPL3lPU1QyREtrdGhKUUJwR3NSYjdqOFZ1YkZYanBiOFRzbGJUakR0WWIvWWlHZXVJQ0liNzJ2WW1kM0MyUmR3WnZCRDhQOG1idERtbERCSitWekRueWFqRGVtOWlxOUNsRUxmNzJrMlVySWE2UmN1dk5Gd29rdElaS2lRUTJudHNyWU83aS9FcG9tMStYWjgrZVJMVDdPcm1BUVp2T013MGphdzRtelhJa2ZUZm9hVkVvaENadVZLdkRpK2IzZTZaWFkyRVBiaUlBU2lSVWw3N0V6Sm1FcmlRTHBycTAwc0tBUlVmSlMvcWQ4SmwzODl2L3FzQ3pWNTlyY2paT3hQRmtjUllQVFBqUzNYMzVZV2J5cEVSSUI3clR1WXhTV09oZjY4WGdUejBpV0o3TXluTnVCYVk0WXhUTmRLSDFYTDgxRmdFbHI1VUlLMHNsNEZGT3ZMd3M2NEw3WlNjRU5pZVI2OXloTFdCMzRQRGdoM2lEZnBBaTMxZ3FLc3lrOHJIaTFOUkxIMlRidllERi9VQlRRWHB6c2JtUmNYNHU4aVFlcU1NZC9iSjBzMkNRMVYxRTQ2NzdTeGw5UGV3SzE5bHRNQUNla3BpL2NCNnRZeWprVkxkQ1NScDh2bmRVYWRvV3JTVFNDNFBUY05EVHBzT1lYekQzN2VUajd4U3lTWEgwbi92SVNjczdOSGE3ZEZvSHdvaG01YXUyaGF3dytkRXFUVDBEK29PcFlERzJ3WktBRERLVmhRQml4aVprcFkrSU1POEgwTW14cUJkT1ZsbW5wRENLc2tTV2s4ZS9PNmNLNVdzSnpwNXY3d2QyVmtiRlRQV1pUUjdiRTBFVGg3SHpsNG5kSXZMbWRuclRtakV3bEZDbjZYTjRYZG4wZmw3QUNlVkVDRkU4aDR2eURiSWdYcXlKQzZTcnBDcWNjZHlkT1QyczJhRlo1a1hiQUpuKzRTQnV3d2huYjNEZkZ0RkNneWpWeEd4OXUxUHFrY29oc1MybUxsZDFyVXRaanlSS0FhTWYvb29SVXkyNHY5cVNOd1ZNSFh1SEZNdldNeDFSVlcwb21FR2xjeTlvY3ZqVklBbFV2OGRBRndaa2ZMbGdsZmlnT0NSdU5pTHdBSjVVNkV4YXFkdmd0ejhPajhrM3VjQmRvdGdXZU9md01CUytzbWNDMmhKQjZITWYrQitUM3YvU2RETVAxZXk5ZlAzRHdEOFAzVS9oOUFnakh1TExjNUxrM3JOSFlQOFNocnNQMFBsYjVUWW9rTmx2OGp6UUlYQUhEYkVCSTNrQlBid2VEeE1pVldkN0kvc0FiZ3JsOGg5WExhS2x1UmNIV1U1WTRKdUtDR05jdk5MTTQ5WE8wc2JycVp1WjYwdWpGRUlwR1ZqOEMxNTZ5RlBaN2l5V1NYalJPTnRuWi9uekhTL1VTNG9JbjZPVG9VQnNJbDE1NWJ1a3VxeGhqbUpzUkZLTnM3eXhoUUYvWHlybncxdkI0S01qeW12MC9aS28wMXlETjUvTXg2SStYb25FdVBjb0thVXlGWkk0cWtNd04yeE5oTDkvMHdqN1JaajF5Wi9sMncxeFdKVGcrZUpUd2EveTdJMTZHWHJzbStDSnRJaHhFVlRpSjlrY1BkYjVrditmWjJwOVhRNVJYTDE3OUNsSkRzRlVVNHN5dnVYcWY0SldWOWMvSkRxVkFmVythMFJMNUhtdTd5Qm1HMnNxNDAzNUI3MloyYU80bTJEdk9CcGdSeEU3dzNiQkJ3dFYyQVVmcEovNStqdGJ4UENtbGQ5enlpanFJWnNXNlFZcVhwbDJqTVBPY1BKdWplRWcrUEpmWW85RmlYQ1hKb2U3YzgvY2xhcVNlQ2dyUmNDTDlFZUwxc1hpVGJtRUZDNEhPeEwrY2hTRGw0bzZWMU50QjRWQnEwcmJCNUVZdG1LMkk0cThvMFZVcmFrQUU0UFBoTWhsWis1Tk5PS2RKamxyZk9iT2ZsQ0RPTWNRM3I1ZE1Lek5OOFNlZ2trMEpOejJqaVY3d0Rwc0dSN3VUVGxnMldCdzR3cGt0eUJ1ZzZINWVBRjl3ZHlQYS9lV3V0ZkRNRFYyS2F2amNocHg5Q3lOTWpVR3paWG9ETHJwOXAxMlJPcjFHcFk3ZWxwbGY1SzF6Zmo4dVNKM29hbjJGNUE3bHRpbWNQRUtoc0hhWXdrdDArVVQ2L2JUYTNPRzhPQ1JFS0pJaGlySzdwcXVxREtldWRMWlZuSy9LWEJINXFOQUgxMjcydktJQU44alI2S0FGQ0FHSGlrK1lTUml6T3RxM3AyM2VaWWh4cHJXZTRTMmNVMUh2OW83TW1HRytBbjl3NHZuY3VwU3ZsVU9zS3RtVzBPalc0MEpSOFp6VUR4d1lqYzUxVE5SL21JdEVVQ1UvbnVWbEVhdFhHbHdjVC9YNFMwd1hKem13Z0tuZzVJR01hUUFHNE1tWjdWTHk2Q2VqRDg1TW9mRi91U3kvV3hEdzl6M2VZOWlFZ2gxT2FmMnp6VVJFVEtNSlp1ZlRWR2E5bmVCMi9pemQ3cWtJQTdCSVpQdUdQMVlTTFhqVEcxWXg1WmdaZWgydzhFeTVNVXFqZE1EOUNlc3lDTWZ4LzVhQkprc0NqUjdHK3JYcU5DRGJyNjNpWXZYUmFlaFZYc3NOb3pRNmsrN2Q3ZzRvYnlwSjB6L1VIUGJGYXdBSkNwaW1pUnJPQTNVbXRQRkpCRDVubGdNQzdiUHRYQ0t3U2JxbXducWhwYll2QTN0S1N1bzYvVXdOcU9lKzgvNzJWaGtGYU1OQWI5bkZ0citKY3RIMndpZFpYZTFmaVFzYXFlMzlpL1NFK01hNGt5ZDFVSzlRWk5UZE93OW9ITTQyVlhJcUJUQysrd2hxeDJMWDdCN3Z1dHNBcDhmU1BjY2NHYis1QzVFYTRyZ0RQQytzVmllK2d3ekw2c1RMazUrUWhPN1h4V2VxbkxPRGlIZ01TMkpHT2ZIRzIxRzJjb1NWZ3V0WllhcTNMSE9vVmVNdFgwNzRNRUZoS2R2WHRMZW9ReXVHUXlsWGgrTHlwZUE0eEtmYUVjVGdkVnFtakdlL0ZIM2ovSWhaWkJqWXNCbjRYUE9aYzBMQW0xQnhFS0YxQUZBV0Fra3VjLzBSalMrUElKRUtQbHQzUk5XZ1Zad2ZGMmRzblBHeDU5MUtKby9qOGpSUkRON2ZmY0R2ZWZBQm45ZElwMUdPQkxhOGZrQnE4bHVCbnV5d1lhRzRrcUlMMll1M090cXdoZlQvaXV4d0JPMHlyV2thMmlhL2FVdnVVQk03VDBEd2d6Tm8rUjYxSGdxT0E1M3NjV3huU0RBZkUyWStGUWJod0dzZUJtSzZEMUF3K0haUmRpeUIrV1RHTWNCR0lBdDZIYTBRREVnN1hGUmhmclRVQW4xaDc5cWxXOGhQSi8wRnBEQU5IYllXSnR0R050bGpNWHMzM2RkSDhpVGlZb2E1MnI1OVpKbW82Skg2R0NvSE9RbmRweEZmcjFITzlIUzBEaUZlaFhyc3p4Z3VsN0VyRHExMzVzcmFGem96Ym5BZnlxa3h5S3VSNzhIRE1XbWszeHJVdDNwYm9YWUJ0akRkMW5qcnpadmJNejRQVTJhUEVaNkZJUHo1R1VyanArQmQydU9sMlRhNzJTMXlNZkdWUC9WaDVJbEdCTDg2WkRMTEZ4eFZmOW1vTUIvVE9SYjNFR2dNYThjeTVrOHIvSGVaRXkyLzlyaDU0dTZGQnoyVTd1OTRndGtQUFhnQjZwOXVrYlFwaWJ5S2M0SmtjN3RwRXJUZHpSSnUyaldhMUgzNm0zRHU4WHFRaTVnZ1VLeDM0K1Q5VUVxTm1jTTcrNVd2aXFDeENwSXNBOERDRHkwcEdsdGZGQ0t1Qk1ObHNKdUkwS2k3VmZac2dWa2RpbXIydTBhbU5SM0pIYXBJZmhjTVZUV1VlcmtYVEFmSFArRFNTRzIvQkdxUitJWnhZNE9SUnlPZGxBWitRRzZSS2taTVVDQ0pwZ0UySDJDdE9QNkpHVElYNHNzdDB5NHYwL0hTVWlMeDYyMkZ6aUhBSGNwVUpRdFV6SmZ6MEVXMjF1V2lIbHlLTmk4c2ZpeWZGU29EZSt4ZThCcTloa1pBRkVCcS9RQkJUalNMZVlkdzR5eGxmWWlxWm9VK2lZN1lQUmEwdWc3RkdReTA5SlBVZ1hpQldGZlN0WFQ0RGJYK0srQ2s4RG9WQ2Q1V1FTR0lUNHVSN2xpZnZRUW5GY3pDYU9odzMweHBmUGZJNVNSUHVUUCtYOVJMbUpFMGY5d1NBVVl3TE1JdlhxWmlxclBGTEErNU1PMnVCY3QxeHA0TkVFRFlUZExqemNmUFRzeHFvMGxyaUZCMlBNVmFQYzZRRFErWTBLdDlwV0NWS3BqejVGRlA5TENWUzVsTWVSc3hZQzBOYlFGYU44bmNlMEt5QWt2Vis2cFFvNW5sOVJwb3h6ZGNXUlJtN1pnNkFDYlNia0x1REwvRnZMb0QwYWVKL1dxSEtYR000MHd0ZjgxUi9BS0ZVdGxtMkNmellVZGVKT2hqQ0NSOGRFOUlMWnU0NktFQ0ZQWHNnRUVqY1UyUEdiSjQ3cGhHbWNZOXh2cDF2Z3pOYTZlc09PcHo5Nmhxc0Rua2FGV2lqYmswSWlGWWZEVzZNTENobEhRMS9kV2hoVkRJa1pXSzNjNnc0T2dReGhvcGY4T3Ryb0xoNFNqNXBYMXIvZUFEUVZHR3dmZWNtczl2YWs3VGhJZ2RhT2lOdk1SZUQ4QkF4Yjk1UDZ3TFBYL3FoaTQ0QXFWOTBRZmtNYm1UNTNadE1GeC9MV2E1NkhWQTZqNU9sSjVGMXdKVzRhaCtZdnVxRmNsOEpodnM4VGRmakVxMmxDc2dvWmlSZkdMaFAyUHJDWEFJRnZrNENWRHVTbWJhN3F0MU5oVkhGVDY4UXBTUFhGOXd3NUJod0Q2R3ZJWHpWK1dpczhybUtOL1ZaUkVLd0tWZ21qMXRKeks0MlorbG96WG1ZZVNQcVdmblVFeVFmSy92TGRadjlLK0ZsWnZ2OHd2VWNIS1ZPdEdzeTRocjgwdmVtS2grTUFvMFAvWFhRYVo0enRGdEZFRmI0Q0dQSVBsNklLUmRMdmVvTFFvY2tMenRlV1B6UGQwclVieFhsSlZTVUdSVmRRcEhaYjNDZjJ6OFJwTU5zSXUvOVFGTnZmdndTVHFqWjNhSGl5TXFDaFBxZmtseGFyQzA5ZDBDQUxBR1VwK1lKVHdPSXd0N2lXSXVjZ2pNSlVBWDNCWFRGR1BTQWdRVmlzdWZPeEUvNXBPNU5pSW9PVnR4RUlyNXZxelE5SndEZ3pNK2wvQlZ6MUplVDhocFQ1clJSalU1RDRvZ3cySXdiUlB2bE9pV3o3dDMrbEJEUXhrT2pIYlJqZVM1anp2OG1SQkp1Zjl5anBPR2d5UkxMa25mNCtrclBuRFB6QUNPKzJKSVlkUHZCVVZnY2lHajY0cUEwKy9PRENhZmRJUzlxM0JQNnFXNGU3RkdUMGtFZ0l2TTFrWWNFcTNETkRZdDFpYUVnZHlNUG9aeU1sb3o4cGZmejVXQWs3aHN5cE1iRkZod0IyQUlvUnIvaUtJSTBoVnJ2eWRhYXY3STNmRW8yTmV5ekNKRzFIZmI1T1g5VEUyb1FyN1c2NVR1TnBuNmdkVXZNU1BvaCtUNnFmKzY4Z2RMVXd1YkdtWVg2RWFncU96RmlYeEREMkNIMmJ0VWlUOERkdndwcTcxSHlmK2M3dkcvUUdzYyt4ZUNEY2owMDdWVjhTd2t6V0ZKdDdPcm1FVjJmTEVhdmppbXl2eEtjVC96YXM1Q2V0eVV3ZjdGL2ZDUEE3MS91UkhEYnh0SGljSXh6QUQvRmhkUGFNd3F2WHVvZXRDOUtrT1publAzT1lUalZTTmo4dGtodXpqcXJFTzJ0TlBnNlR3ZEdEaU1PZzBuWlN6Y1lMemFuWTl1VC9ncldsTzZ1MXh5ckJxWFBTZkR4akR2Q0d2amZhcG5lYW1KTlJjdVZqN0g1ejJoSUM3RG16dUw5OG9qSEEzeU45Y05hRXd5cUZPdUhhOWxYS3hJM2d1blRMdjM1NEJxcjZIblZYVisrMlJWb1M0UWdNWThtenhBdkNFai9hRWNtOUZBSzdNZjNTYmg3K1NpR0FhVDdPVFdiUG1uNCtLODltdktJN3RGK1RqbVEvTzNLM05mdjI2cmJBOThpSnI4WDlUQ25vM0VEb291allFTDNrc3JFYm41N1RabTd5T3dmbUpYUnV4akhuMmY2bE1tL3hKOHVmZU41VjBMQ3ZNZHhiNXZUUVZJUmdJSGNYbW0vZStaMjFHK3lWcDJtamJlY1ZtanJ5Um1zQnFjM05ZOGRvNWcwVU1DSERTY1dyZDRRa1ViQW9DRjRlL1lNMjJ1NUxKLzJhZnlJenMyOXhETVNBNGdkSVFQRFA3Y2l5YWU5bkVqOFJyVlRLQUVDcEtEdW9BbVBxZzNpWXhXai9PRVVRekZmNXA3aExoS2dLcVpTbmZUWjd5dTNnQ2JrMUVBclBQS2RqT096WnA0Y2xTYU9uSVBqbjEvOHNLV2lBS1pORVkyaUNhdlhmR3RETUFxd2NPMUJOenRTYU9wSWNYek1nTmhZRXdReUJSY3RiRE9GcS92SjlCUmk5a3dhUU9wamFjOUFGcUpnOTgxSDIwRGZXZGIyaCtnS2lZdVFKNmpKV0UyT1VVNmdnc0FRNTZMcVc3cFNFRXZZUEhhZTNXK2lyOGp1T1BkZEhlMWcydGx4WlB6MkRIUklTS1ZMZytlT05neHgzcklMNGdJbVhTanlHRXkrT0pRM2J4UFNzSVU5VFZPLzBpSWs4emtpZ1pJTW1DcjZwU05LMGJsYysydWNVMHkrVFZkSjRMVzJ5TCtJdDFBcHhlTnFOWGtFcnY4NGJFMFBEdTBxeXdMTVg3RVdHeko4M1pYWEczYUVRMk9nWW01ZCt0bTNJRHZKcmtYWlBJdndTOXE1aDhJVXZHZFpEYlZyTWFROURIK3gvR3FCWXdyWC9ha1dpOTV0Ym1QMXh4aE0zbEYyTkwvaG1YenlDanBqQ1cwbnB1Q0tUQ0E2U0MweTFIQU1rSnVWZGtXb3FXWGgvZmRvU2U3dmJWblFpUXZnV1h3RmkxL01jV0ZFU3FyVFNuUVRaRnZQZXo5S3FFeDB1VzF2ektiT3B3Vm1wRTFveG04RHBlQ0FCeHpnTzk3cmFUN1FmR3ZEamZHMlplSTMvYkt0eitIN2V0OUNhZ2ZJd0toc3UvMWNYSWpLbmNOSzYwb0pkSjROYS94bXJRV0VEMEtnU1Z3aU4zT0NubjdFV1JEWHMvSFVZSjhEMDBheTFBN1Y4VUZFZmlBY1N5Y2t0VHBmemRNQUNiWE5YeFpiWWtDbDlmZ21obmRVZ2FmdXhzVFp5RjV1cWlCVzNqVnlxb1VXbnZpR0ZSOG9uRGtmUUdua2JqWVYzRXQ0b2t0Rk9uK2JlcUttNXFKTGxjcG1ZK1BoSXk5cVBDWUt0dktxRXRpVG53RlZmNHJoNkErQjRBYldYaE1EMzhMMkdoRk5MSzFiVnRCZU9NMUhrTXBNdE5nV1d5RjhURWxka1FxZXZkblVHRWV5L0JxSVFsNWNNWmkyTFE3WGZXWU16M0NrdUpldDkwOWJCbVp0MTArRjZ6OGtOWG5jcXZBcDQvSS9pMkkyWDFBSC9YMG5yQnh0WkJ6aG9Gb1RnZHdKV1NRWi8xN2cvMTBIeU9JTmhvQUhOUzVIOFh0OUJYZmx3WDRqR3hYbUhtMDFEem5jcXFGeTJyNWcyNHQyajJZRHEzR3o3aDlxQVdKMWllNTRLcThaeHF3cnU4SFF6Z2VSWU51TXY2eUxxTnQvdGFPeE90NG9LMExrejZKRjBwbmF4R2JrUGFuVjlwQXdxbWE5b0ExVHBsUGtybW05VkwrYy9YRHF2cU5hUVZsUldtVnFwYnpreGhEeGlCbldiSmhENU4zSk54RldGR0JQcFhmRkpVVmo5SDdVUXlhc0tkS3M0THU4VmU3Tmt2NmxuVDBFWStwamJlY2VaOTlWa1NZKzhsZHRLdFRFYmxZazU3SlJhOFQwc2dmK0Z1YmEyZTJoWHFHQnJNaGllNy9hZzlzWmMwYUhveURxUDZwV0oyRmxuNkVnQkJ5VzdFQS9uei8zN09NZi9KWnRrQTRMdVViQ1BkaldRZ3ZjZ2l0cjJBY21NczcvYVA1L21rekx2WkJXTk0wcFR0UFh4d1dOclBMb1o1V0ttQUtYOFV3a0VQdVhkblNWTWwvT2NpM1hwVnhDN2dQbVhONFpHNEdBTkx5Y1o3aUcvVkJHd210OFdtOTFacVRaUC9zd3R3djNtK0FZak81UFRmMklsVmxwR2NiVHgvVnl6d3F6QUtJSUZneWJjeHdmR3A5MW85NmFMYmozSzlwN2tReDBJSEtoTyt1b3RHSzhwSnJDNzZQVjh2Vmh3eTRJaFpSYXZjMVZSZGM2UFF4Q2E3TllRMDMvKy9rQUJnYmQ2VXFEMnZzTlN5UDFvUWV6cXEzNFRsS1F3NlpmVFh2M0VqKzRCNmZRVlBSRDNydmFKVXlYVGtBME50bmVMSG0wbzlDbDlFR0JSZlVOTEgxWi9XMU9ZTjB4cnlINTNYNXphM0R6Z2NEdG9XNEdoUXJIVXU0SDZWL3haM29nQXJ5SnlmYmNtOFkyMzBXNmt4NldBejJwMUkzL0ZGVk14WllWYmNHUnpKZCtjV3c5TVNyVVJqMXZMZ2x6Y3JwaVFGV2ZSUWY2anBJRnFZem1jemkrblJldEI3UVVxSU51YXUwajg4K1F3bXN0NmpTUkUyZnJITlZGRWxubkRtai9mRDZBZmZnMGFzK2NQeGlzeU5GK3FzSW5MSkxhVldrSVBjUjFNRitieGdwdE9BdmJ0ckpMbitVbnhiTTZITS9Reno4VUtJN1BWKzdmMjlQWk1XL05HMk1DTzA2Ti9EWDBCbUp2alVOTFR6TmlUeWJKdHpCSm11TmRDcEdnYzBSVElmRkRlc2c5N1lWUjFkN1hXY0xLOWhjN0ZkcVI5dHVFZkJpb0hXY1JXTXVGVzlpd2UzL2JibHFlenZwZGx3K0FqeTRadStTZ2VrMEJpU1JzMnZ5R0FoYWhCZkdlYzdCK3ZaTVJzVVRaRnZHenlLTG9OaFdETkxmenNlWXF1R1hCbnJLeDhiSlpzMjFGanpVTDhqYm13MnNWSSs3YW9IYVZwK200WjhWaG5jYy94dnE3MGI2dXRIOXZyRmZ0N2F2ODNzZ3ZxMzI1ZnQ5TmI5dmJiZlY2ei8xdmRCMzI5blg4T2l6aHZxN29QMjljbDlYb0dYMWRnZS9WMUJmcTNUUERYY09taTlyRTkxVTVCaG9yRUN4ckRWWExnRlUyWDhFMkJrUGFSM2xrbFZMK2VNNnhna0tGZXRpV2poWEwxcW1kdHV2VHdSNlZXSmNacHRJK2ozakxWRytHTTZPSlc4SXZ3WUZIeFZsblJvQjVBbEF6Vjl2QktjdVY3THROZUJJajJSKzQ5bVJkTWNkT0d4Z3dGNlV1b3Z4TFIwQWtFcklYb2VXcVRFZDc5UTBIYWpnZzJmUVZlMXN1bTBuSFN6ZkJNa1hUQUd0K2R2eDlPSURodWQ4M2pIR2xsRklrQUt4NlBoTEoyYmt0R2JBdmZtVWtLc3pxVE5tMjNkSTgxVldOcEZBRmJ2c3RhbjZmbk9CTDJxanFpTXgwSjBONUJ1bW85aUs2SDEyT2ZhNHBBL3JhbTZRR3hnZUVNSkhrd24xdVpGeDJaUW50eS96aVZpaUxnVHdsMCtsdUN6QldtQnJmMnhZQXcrbStTN0gvaWUyK0JSVkhuQ3p4dUhvV1ZBUDkxMGRORnY3RmV6b0xML3dKWENMRGdWcFA2QnIwQldORGJIM2U4WHc3VVU5RzZaWmpHbzdwaDZDUGgxY3JmQUNnWXBmWlNoeVNkSTdSUzZScmpFSjdQVGxFTkNjVGE2aW01SGxZYjdYcS83QlBCcVNqQWFIaDMrdXB0YXFvVDFuajh4NkwzdW52OHhOUURmUzBTMTFibThaYjF1NTNMVko2WWNmU2xTWWRaL3JPbWZ1YXU0eHF1NEM4ZWZIaysvdk02YTZCYWJQd0dydjFMOWVhdnFhWWRjbndRcHMxT2NRUlpUazJrTENMQk9GeWtUWmlSV0pxa25qSHFGK0RxOG51aHZSaHdOa1NNSGNKSC9lMUp1aG1EZnBNMEZxVHBHc3dZMEVmRjFUR0RybEZ2aEhsK1AreHVwMHZqSUxnMno0Q3gxRGl6SU5WcTU5Tms2Nk5aM0R1V3ZmaFhrM1l2Q1czU0FKeFFTVUhOM1VwbzhuNmlRZ1dOWFpDRFFyd2FxMFVxWFdFT1NSRDVqNGoxL01SZ2ZtTFJvaFhxNlVyamJyM1hyWi9rMnZyZk5UY0sxU2VGR1pKaVhWZVlhcFVNdkorUXByNGFkcVg3Mm1ZcnE5d0NYT0lQTVRJb1RqK1phcUxwV2QxM0hZeStkclBxVE9xekYvNUU0VisvZldDSzgzQ3k0M2dxZXN0eUorNElqb2h1QWR0bUVObHN5dzNoSUhQQ1dHakVlRDZ5RG5sRkR6UVQzOFJlalRDWDN3UU9sbVVJUkNVbE4ya2JPSFBXV1hDTjhVeXJaM3FIUXFvRkhvTWZJY3lRajkvOFRreFhhL1FWV1RXMDRobXVMV1Erdzh4N2hnQXBHVkJYcXlUTG5QOEtYNGRYMVlVSXdObXYrT2U4QU5JMGFoWlowd2h1VTdlcy90K0xEdjJ1RXkzdzFQbVJxZkFaUy9yeXU1eWswd1o4ak1RWnhXNVcyZncxSTJqbkFSNFFEZUNGWW1zOVp5ZXVBakhVd3ROMFFHS1cvMDFMbGRjMFYzdmx4OEtFOERpbHdlQyt0bk8vbzJHemNHMWRVcGNYbWdtcVVXbTNIci9lblZtRUlibjR1OHhTSWdZa1NxL2V2M2dDSmhBVXpOeGZOQ1drV21BU0NJRFhldVhGMFhPK2JsR0w3UHNBaGg4c1FoU3VBNm15RGw2ZzdnNGFxRHlmWWdrTTdySkdqRk9LV1pCbDhYRTA4ZmlhcmRUUXlPZmdKamNaRzFoaGY3dVhQQVczT1ZuUmF2c0NyaWNMMUUyang0TTRHRzhoelZGR1JnVHRhNjg2RXFpMUMzMUFTNCswcWZtK1lKZ0s5YWVJeUpJNGR1emY1UFFmRW5nSlZsd0E0QlZSbnltWnZwOGVaWFRTUTl4eitOVDhkeVhVbGxsTXVYbWt6Z1FTVEhXeXpQTWFTNmI0NWtISTZjV2o3ZzZ3NlBMaFJPRTdqU0hTd1o3N2tnaHlUUUtJN2xxR3ZQMWpudlJWOEtwTEtFYUh3bmFQNlptZWFJQWZlSzFRQWkyU1hNSkV0R05ITmY3dDJSSGNlU2JZUUk2T0JDbnYvWmw0SWwraGNBMmhPQW5pSDBNVmdZREhqY0MxVzNlUGY5dnNJRC8wM1E2YWwzcmJxSFMwTUF6dzl1N00xOUtDbEFPanJtdlZQU1VoYXVKVUVVMFRldEpUTGFlaDZ0aVNzcVcvSXFiSll4Y25KZFk0V2ZYckxhYXh6SjgweEJsT3lGTXZJTFFEOTBSbXRIWEFoaVJxWXRxdElXZ3VHOGF0bHk0Z3RVYVMzemFhWVp6VmM5Q29USGwvUG5zRjFtMUFnUHBJTWQvVHhyam9kaVBXR3ZVUFkyMFlLbkFNakFtVW1zZHZld0IxTmVORk81bEJXRE9mQ3lYU0FOQkN6OTUycXk3a21KbGRKVVFrcEMvdjNHTVUzaHE4OHFETW1HWVdyUjlUUk53Q0FRd0pIQkp6V3E4THlDVFljM0RYUk51Z0JtZkUxbW9BMEt2MTdLOEYrZ2l1cHRSMlFtSGpnaHRFZjRkN1JXUnhCZHpYcHBTWGNza3VzbWV2OXc1OXl0MlR4akRXV1Q5ZG5PVlpJQWNRUkdwWjUybnJKNk16KzBJcW4rVnV0T1JlRU5IUVFJdmVyUENFeDNZa2puUjVLRFUwdEQxUExjUitncENhbE81R09GcXJqcUdNRmJGbTVEMFpIUlVNek8wYzRtdUtsbEtVOEhoYkl1TytkNThFQWlYT1FHQWQrNUYvSk42MGVUV3hvaDV5cVJ1eGk1NVlHVGc2cllQQzVKYjZiOEtZRlJwemJhcVY5LzVMYW5YUFBTRXR6V0F2dnB4bm9YdEs2TWdqeDRCWitYdXVNY2ZlMkp1K2lwU2g1Q3V5a0VYbWQ0WnJ2U0M0bGdIZ0hCZFkwZStxZFZUQnZKM01COFViVmFMS0xHamtIalVGZ21od1VWbWlZZW1ORkt3aXF3UnVCbVFJcDJoM0M0ZG9uOUJ1ZktteklyU2loMVNnTFlUTEZMUmd2Z0hNdkRyc2dvRU1SUTAyWjh6TzdteG45R0h6bm9GdWJUWkVHenVJVEZpL01qNWZ4MXEyeVU2VlZuVDAyaUZEZjRWdUVLbUc1M0RycDFFMnVKNVJYL2ZQbzdhUkdiKzdGbW1oT0VzTmc1RE1HVzJZZFNYK0VlS1BKTE1CRHRCaEZsRTZkZDRXTGpRWHNQSXU2Q05xemJ1L09hdmN6aDNBMUdoem9LL0tvN2Zlc2lwRFBjenpMRnNSdTUwbTJqVUlNZzY0MGJUTm9wWVJKblVXMXA2WG9RWkd0ckVJQjlyYk9BeVJxNXR1VVpYRC9iRkxkelAyMnBDRHBjMUFwN0grSWlpTTVFK2h5Q21nNCswUmVwa1FoV3Vsd1hRYTJVa1ljREtCNDNlTXpqcHZXQ2tERzFSUzFOZHZwUGRBcmRGS1pEbXdVam5YMExXMFY4R0RCUVdSRU1zMjFQOHduNDZaTHFiSVpUNVBaWDRscmo2ZXlHY0RZbHcrRGtWSzREZk1OTUdGSEtsQjRKSFZnUEVHNVcveTVBNUt4dGlldHl2MXRBaWU5cVpzVTFVZ0NrM0Q1dTNkbmhnd0JxdlhGQjQ0Qnc4YnR2K0hwS1hVMjA3MnAxRm16N2dQK0k4Wmw3SXBRZGZUM2g2UTkvaE0yUXdIZW5XeVdERk5EamNEcitUb2xmODVTOXdSc2lzWmVUS1pmakFlNTFzekhFbDhpOEF6WmJtZkh3Z3RyUVVyVmNvb2ZiODZTZkZtWERSMWxMeTZPV1JBck85bHErRTlKUVdURHNqZ0FZdnh2emVXTGlQRGdGVm5lNlg2SUp2VCtkMDBqYlJOaGpaTGVLcmJRTFZoa1M5SVJYUm8zTEYxOE1tV2JHZW9KOGJLa0c5SVNkOCtOd2lXUXJWR0JyVjlNNG9VVVdmTlFUaHE0Zm1xbU5mbWczS2hsdkhwSTdySUN2Mzk5VWJKSENMK2FMUUxSamlDOEZvZWsvcDRuZ0w3ZUZBRGtNV0gzbVgxbmhrcXlmWXZIelRacWZueElsbXNYS2dqSm4wRzVOUmNLQmRteW9taTlQaW5qM01XWHIxRkNYbkc4WWNYWWQ1aUlqK2pQSjZVVnNzUXkraG9ad2tVdFkwWCtzTnpoL1U5WGwzb3QyeVBZYVI4bUtUbDIzSmlTd1B0YVp5UnNoZnd5c1pXTGlXNWYwdkpZWjc1UWxpcGtFMHVTU2I5QmJ6Y0Q3T1B6Qi8zTDBaaW5CaFhnWnJEUFpPdmF4MjFOTG5wbytGd3p6WlZKVUh4dzRJN1kvSUZvNWg0MFVEVmp6djRSSCt2NmZvTDNTME5xYVc5SHNvc3E2MlkyTUlqV0w2dE56T3VuQWJpNkR0Q2R4V2daenBna2RFelFBQkFMeFB4T3VBazBVL0xKRGE5SjlDVW1MMU1IaWlQOGVjcmZ0SUs0TFJnTlR0b1pVQWpCK1BMeSthMmJ3eWQwWkQrNWJIYjVtRHNnajVMVE1CSFRHajkzczgrZFFNWS9nTU5Rc0dVb2ZYQnpXN3d3N05SdHRjNUJmUStlMzNqUmtIb3AvM0J5OUpJSks2VEl1MlVLbEFOcHIraTZQam9aQVdqRnk3UkxWOGQxQnR2V1JjNm16OUZuNXZELzAweGJrYm9LaGlHR2RnMm1JNXhJbWhEZHRucGZIdGZXWnQwKzd4Q3V1eURPNXBsdFM2MENvdnMrby9CTjJVWmlvQ250TWlmVmJMODlsZXBVaVgxdUgvTkVNZWwzdldWYlc2Y2krMEVqODl5Tjhrby8ySFk2ZzhFUTc3dVg5UnhwdWZmTlpjNDAwS09kVjBsaUgvUnVUbUNNTGlTZ09xL3l6VUxVM0xvUVZ1UmZJZ1hjL2x0elJHcDlhZ3JqMk14QVRJR2V3WXl5SXduVVFlWmtMVlZ2bGhZeU5KQWhNdUdwL1hMT2tycGFWSG1pVFBEYVdSd1ZOenFQOE16c2o0ZDl2OG8rUE1TUS9qSi9KWnRETVBqRGZmQlRMSlpFYXE4VHZJSDlxejJkbkxUU080ZmNVU3VISS9XNnNWVEpZUEhiV3Y1MlZzM2czSmJ1T3grcjlCRjBzNGpuTk8zMDgrLzFhL2NLejFwcFZkTHoyV2w5Njl5MkROS01GVHl1bGV6MWRmNXJFZFlOOSs0NkNBaGcvMkhrcTdaYzkvQ0NxaGNPUm43aVFwMk9zTUZWaCtBWWhTVU80TG14ckc3S2I1YkZZNGRyQVRQclNBQ3JmYURQK2d0dDVmRUZXV2dhRi9kNzNHemN3VDcxZHZkd3RvOWlrVlNCcjRwQlpWRy93YUNKZDQya3BOeVVoOFh0VncrYWZGNEwzb2tPT2pZQi9UL2Yvb1hIRmxKTTRsd2pUVk5ITUdmVHZaLytZQkd5M1d5VlFkWGZPZk1LODMvWXhoODdxVjNjMGNXVzV6d2d2aVZzYVh0Z2hNanQ2MGpzNXN6M3M1Wjlxb3Y4ekkvd0xxdDBNQnpIWUducmVEVUVXWHRGSGNCU09ZU1JJK1E3dUg0eUR4b3pDbW5kZ3hGQitlRkF5eHBsR1dVM1VMRTBKdGJ0ZGxZR3RxbWlSTHVQVEQrVHdRbzF3RE0xS2RIQmlXLzJyV05UenRVNE1CZnI4Rzl2SFE5MGR4aHhSbXErSjFnVGxwVmZoVEpXTHdHYU5FRCs5Wm1TY1FhZWRsVVdYSGgxbGIraUxmbFJReDBRbW1namZ2NWZ3Y1ZJVVA5UGlDd2NIejNYd1ZxZEZVT3I1K0xVaEFFWGgwKzVlTEJXNWJVK2VadWdQbGg2KytZNVlpZ0hzd0FpcGlJRDhuRXpDWkxzOHBOaTZkSWg3Q0JmQ3dEU0YwZTJIVHNIYTdaN2ZwM3krcVZMSUtoL2YwN0xTNE9VVzRRL1lvZlQwSVlodEFvQWU0b0x0Z1lpV1NUQy8xdVJDZXl4ejBCdlJKRG0wL2xJRnZScVVsR2d2QzBDd3JQWGl5QzdBbjFIMDZ6OU90MWhuUi9oQ0dYeEU4QnFCb0hnbWIyc1VtbmdWT2pBZ3Racks5VncrUDIzcVQwZ3VFclRwREdraEVDUXlhMGFJakdzOHZmL05PVzF0NUtSYWc4dm5TVHFJcWVBeHVNZFFDby9wQWhTZzNOenlJTXhQSHdIdFFJWHRPWHhqREkvck9hM25SWVFieTdObEhWcmtia2JZYzdGVUFnaFZmdTgycnlvNHV1OTdpUUI1VTFZaHdMRUlTTS9rb25vVzRNR3RyY1pTemRhU1U2dkszMFNmMktXM1ZaS3ZHMGsrd0FlZUNJNThSVG9VczRzNVhFMjZXRnltYzd4eU5DSGg4SVh5SEtOZStRNVozanYrUDFlZm1IM3ozTGZocHJoK096WXM2RjM2TTIxcjFJUHNQM01vRFBkWlFleVc2UmRLNnEvSkh6MkRUUTdBT09QaWxORjRVTmlEaHEvTUlaMFQ1RWZBTlBURHpVcmdoMGZMYncxclZLQlB0dzVOd3NJaHl4cC9jN0RMOUQzR1lnQW1YNkdmbXExWGtJcmxqb0dHV1BYTW53VGw2SGdjek56clhtR2lxQVRrWTRQRG9lTXFNeUJKME96VlNxc0VuL2VaQkpXRHNHaFNXOXA5SU1QUVdxNXZneEU5MkxjNENPUHp5aHMyV0N1TldLWk0yNmt4VFNYb2lOaWszYzVINVNnMkdHdlp0YmVpOHVBMnIrcjlLZWZDVWxXL0RUVHcvMi9iK1BtcVB0TFh1SHJZbWdxaEJTNmJraWEycnFNcVFTRXp1ekIwM3ZCMEs0VzY5QWFrRUhZdVBFdjhzSGtKZW04MFBrdzJUOHNCZGQzSWlOdlprbkwvd2JTUUdKek91QXNzZGI4TWRKRFBSVnhyMjVYVEx1TkNQbXIxbzJ4dFM5NnBWQUlhL21KZFhwdFRsbUZtYkRhVFlnVFRIU24ySUdDT0ZBd0d1YThrbVBoekpoOEZtMGQrVzVzc1hXdDhvNG16cVRiem5ubklWOElYd0YyNVlZMnFTVXV6VjA5Rm45UytZdFFUM21POWx6TVZVVXg4T1FhL25ub1pTNktGRjgranlQT0ZhRVlJd3lQL25JTk5hb2h2TThXM3lNSG05c3NjcDM2UzFPNWpSQ1NybjhpTUxHZEdmTnptY3N3R3lCeDZFRTF0Ym5zallDQ2oxSGg4bUd1ZnBDelJMNk43ak9EblQxUGhsTUhXUUVMQXVrZzZzRGNOMDk3ZU5WeEpjckVzb2pVK1NOMVkwY1djUXNYRUdrSWRNdlR6VGYxZGRiajBndG1rRGtwVlBXOUUzTDRDUGUrcEI2OEhmWktNNFI4dzZSMWNIdVpqSzVMUmZBbU1SZkh4RG9xbHFHM2w5eUxnTklMVlhwTE5LclZLaURLYzlLZTE3Q2ZyL09qcitmN0d6VmdJeVpTRnd5am1nQTA2dmlxK0ZCYXY5QXJNMnJtdWNjajRmQnZHazBieHJ5dW5OWndyd2ptRzVGQWJLdU15cEN2Uy9jeVYyci9MMXlKV1kxY3VFTzJYRGptaTFWVG5TM05IMDNDWVYyaDg0Y2Z3ZTF6UnpHK01CcjNLL3I0cjI4aWJtSUZOeGFXcXlaV0JsVDNVNEFFb3VJZG5PTFdUY2xKY2tCcDlWYWUrWXhic2NHbE9sZ2tRekxvQ3cwWnNhM2dHMmtNcWxONUNTT2ZWaGNRTjEzMGZ1QXhSMmhGek4vcXFyTHhMMXR5VExqN3UwRmFOendwYjdQOFdxM1RmRk1lZFJ2Q3JVL25iTTY0SVRBMXVKT0EwdUVBcURyQmM2VmJNOXVQZGxrckt6RzJDcTRERXRBNUhNR2hZcjdMNDlXV2Fvb1R1YTJEL1ZvL0xQK3l6cUZsVG02WXU4NEhKckNHSUEzVVVFMzc4bXFNYm91NVJMaExHQjJCMVNyZVI0STd6eFIxdm1lTEhkYmg5YkhMYWFsMjk2OFR5bGhsMDFEcnNaK3ZPYkNQUjJvK0NZdU45OEVyYmErdCtnN0pBdlhYVWl0bUFsb1BFRklWVTFmVVQ1S2J6MjNCRHlMUUlzZ0JXMzhhWDdKTXd1aW9raHZ5WnBBdEpUM1dyR1ZRR1B5Y1dwVnJubFM1aWNzVkk1ZThFcG55NEZsb2JBVS9jUG00aGEyQzFaNzJtd3hvbjU2d1Q0NnJHZk5EMjFWbTd2bkFFbllSY3hhTTVlNXVoZk5LdXltazhhRjFPalcyaDdqSUNXMlA1SjBWMFhhdVpnaWc4d1dqOVR0L1FXbUZ2RGpzU3RvZDhabzYxUVBzclFzRmtXYmRoOVViTnN5cEJWVGlwY1pqMnNLTnZnSzFLWnpUb3ZMaWFDUE9CL0R1c2EwR1UrRjA0ekZIQ1I1ZXk1cTBidjFIdjh3bmpxM0JSWnhjUVJybXZFTTlyMjl0Q3dxSUY3VUZ0QUlQVDJqMk1YZjRTTHBJc01jNG9vK2ZiM24vQitncFJxU1U2RkNGZ0hmWTJKMCszVlB4WDFZbXB2ZkhLL01aZk83RGNKNHVybHpEbHRaTUVkUU1KRkZweDM5R29EaSt1bHQ0eFZaYytrK1JVUWtCcDNqUW83eFNJUlhQWVFHRmJDNlVWVnpjb2MzRXBnZDhYaUpuYjR3NmplZmVZTEVyVi9mMFlCZVZURG1KNGFKNW5ZdVVNdFhSaGowN3F4K2RvWlhJNzVnSi9aUWx2UWtlS2F0ZFBvcXlOQzNXMm9kR0c4cUt4blIxbytEanlMU09yWVUvd0w1b0lQSS8yNkQ0WldjN2FLeVhSWFRSbUd4VThueXZzQUR2ZWlVWExzc0JqU0tGTHRRQlNOcW9WYmh4em5ScDBYRlJRRlNHTjZQRElwODZ0ZTBTcWphYm82YlJhUUJubUFLcGZleVYzOXR4V0Niam9QbVRDZUhyWUljZ0g0a1JrOVJ5SjVZZ29XWUt4Y0JYT0g4VFdLOTJFYkhYam1UckF2aEkrLzExQkVLRW1NSnlWMW9DQlBHQW1wUzVPN28xbWZod1dNUWxWOFFUMkNyTjQzVzBZU2VpTkxiSnZFVCtVUVZYc21SR3NlQnpqMkM0OFFyZFQrMGVGU1FXbERsTlR1bXZWRWhabUcyZHdMRjJjRkpWa3RDMk5UVWlrT0tIVWlIb3dBcEtTWjY5QlpUZmNramUyZm1oTm1xc0ZOeUM2cEJab3R5Z0pxYnUyVm5MNTU2MnhQRGY1bG1NZU9GbkY3Vk5SWWI0S1dFL0NteHVJYTJQYnE1QUIyQU1kbjNabFBXMHV3U2xLSUIzTXVZOE0rWXpPUUVRSTIyczZ1c2RIMDBubkpKVjJ0TFE5d21DazBTMVVlY1g1d0lUK3V2SldQL0VqUEdDVXZrWDMwUk9QOWgxU2pMRXRiSExxa1NCRmU4eWVUUmZTTEdnUDF4aVV0aGQ3cUxCMjgxU0FmdDRxOUhNRlBldTVTWEVtMkYrbVhnSmNrUUxUWGJkaWNpdnVPa3lBVVBaRjJYeTJJbzJzbVRBNnAzZGdES1dvTXN2VXMvY1hxa29CeHVISWhHZWpLZ1FaZnNPS0tHYXRqMVhoZ04wVXJkT293N0I5bXVsTFNzOUpiVk05TWxRK2lENXVlcE0yRGRINnRTb3BvZmFQOSt6TEEwOERGb25wTkMxazdzbmhBSy9HZURkUjl0aklaY010SEx1OU41NE5IdHFFSERxdmJqbHIydG52YU1XeHVXemt3Ukp6Vk5LQS9xbVdwWEhxd1ZueWtzbzBBdFdCRGMyYmkvdnNsRzZ0My9IT0w3VFdjbDU3T3o4VlBrd2hRRTNHOUVSTnlXSDl5TXBJQkdoWTJ6dktsczNhZEdyRHp0NU9pVWlxVk1yMXdRTnlQMFkvcVk3RmxWTUh2S0QzYk50elRyWWk4d01Qb1lUbUt4R0lNZGZUNzc3bHRFNDZyUWltaW1uSW5FS0w5OTNSaEowVjExY1l1RTl0M1pZb2tNQmRZU0QzczlxK01uS2VGR1N2WTNiczhtbjJ4eWdzdHVLWWc1ZTdhbnpOVUNKakN0RjdwZ2pzdXF5NTFGNWRXQ2VLUllaa1RSSHgzbS9xTkN0bW1iTmhaeW4yNHJXaW5OWVFWWm0vRHpsMEp5dlozVUhsMlBqUXljQjUzOGNsZTlQcHZ6Q2o0bXRaajVGZ2cwd2hTSXlOR003RkhxdXNJbWtlMFh2b3F2S1FVMHppMWNjVGhheEZ1MzBIVnR3Q0FhWXdzYmhhU1BUOU8yWmxUb1l1MDlsdFV3SE1NMm9hTEs4b2FMTXpXMzlrT0lPQ1Y1TU1venhrcEJ2ZVlScitBSDVmUEtNdG5JcEo1Z3crSWFqRzMvS0dDMWFnd24zc1Rlckx6OTQxWWVkSW9BNk5XRGhWdnpsRmtCbGR5UlhGNVc0bUtCTHVkNUs4U3ZCWkZhTGRyd3pmOWhrWFNFa0thOFdZbDRhYVIzZEVZNEFONG1LZElPMHh4NE9POTA3RTFpWGcyNFM4TzYwUEkrWk9PQTYxNDZGbG9YREVzd0tyTDdPMVZFQjM2TTFDSlVtdmVFUFNWUWJ5RWpwZkp4NVhYS09HR1pac0ZYZEpRMnZsVmxkc0tKV0FmaHpIeEJxeUNWRWYyQTBZNmFyd2NXVnZFZEZCblhTTTd3RE9PWE5LUVV1VStCN1RzZGV5VEh2bDZzMlJSNURvUFNQb2RMVDhZaFFvQUxKdldnWWZFOGd3b2JhdjJkcWN6bExrbTNVN0U0MkxyYVlHMXFUZDlOUU84K2NKa0k4bzVrSnRyUk9sWXNxcEJ5OWtLbXBGK3lyUlN6NFJGODBybmx1MURKd3JueTcvSUdjRFgrS3hySEdGMkthbXExbkt0ejBWcGVaNFVtWTk3RWtOWGtqaGI0OVBLQXJ4QjNHZGkyM2Q5Sy9zOXVibnB1L3BzdE41R3pGbkRKU0V0emhodldFVFRyRHY5eWc3enRxd3lHdm92Rzd0WXkrVnJURDJNVHgvQXFRNGEwUmZXdUJHczFSM1NvWm1ZcVNGblg5cnllRFRTdi9qNVROeDRLVXlnY2sreTBUVVNkVFZMOHQvc25KN0NvQTlORkJKa2tVdUZHdmtpY1AwQ0N2ZS9YaTQzNlhJTWcxV1JsKy9ZZG5yVW5TMi9CSzRmb1puYmRldGtHZm9YMHN6RFZoSHlIMXUxdGhkcW5FRDRQOFJGVk5FY3l1TzdmcHVDTStZbjdwUEVkVmI5SU9qZUM0TWN3RGJzUVJET2NmeGdjbkdwelZtcWRIc284UDUrbUlubkQzb2hRK3dFTmlCZUt4K1VlQ3c5Rld3MkxVTE9yU1FEN0ZGcW93blVNam9JZlNqT09kUkErRzZvdjVqM0NVNXFIRmVSQ3daWnNtVlVnMWJSMVIyUnoxTGkvSFNlYk44RTBSQ1ozaC9WZHh0RUQwb0VLMGZ6a1ROOHQzLzVMM3pQYU1KcXZJQlpEZStwZE11VjdLQm4zeXlFSkZHZ2tZTTl6eTdESVJ4LzYwSWR5UGsyWFNYR2FNMlFqS3Rla1R5UGlpTVM0SVV5LzZOQm9Md2lTM0FWOXE5eTl6NS8xWTFrSllEL3BGMHlES053SUFEcys1UFh2L0tEQXJpMFBOYWVIRjRmbnZlU0VJL1FZZ3N0OE81UVYzVkxmWVBBaUp1dEFlc0cxMTlIWTJlZ0RaTm5RU0NpcnprSkl4Uzd5SE9BNXdZR0F2eGpORHZuSEtKSHpPUDhhRVI3czhTTE0wSkNCcXJoWm9ZY2gzcnA4RC85eWc2MFdCY213M2tzZC9USHl1RHF5UlpKUEpzOG11djVoaXJyTTNZQjY3bVRJM1ZjWktDQitKQnBYYko5MHNpcFB6NHgveVpCUVRWWDlyQ01hSWdlYzczNW5zT0JnSFdHMlliZzNqRTNib1pHR1FtOWp0cnZJbzladTAxMFFyRDJQdkpPNmg2OGI4d3E5TXkwS2FKYTREUnRTNXZTUEJOQjBRZFZMN2JaQ2hXbFBiWUtzY0d4YjNUYUFjRGtoTzVsRzFXek80L2IwbmJZS1BPV2xXdkZQTlhmREhVRURqMHlNMlhJaXhYU25YZnBhb3JGYkNReXY0MVZKUm5CaUhrbUxmTG1MSGM3OU5wVFliRWNJOEd2dlFaRlJpdjcvTmtTa2hGTTUvakdQMzYrVUZpQ1N0NmVWVmpJVkd3alBkTXZRRndVOWc4SHJvRTZMZTl4VnAweGhKclpwemEvUC8ybFRqdFU3MDViNG1HL2NmSDNFWDVIamxaMkpTSC9lWW1Tak14V1BRR0w0cFRsK3N5YW5Yb3IwQlJUVUZZV1JnZ1BmaEhqQlpoSGNoQVBkMHVsWUlzdWtKdHFOL1JRc2VGUDRTN3BTKzAzUDNNY3k3Tm4yM3A2ZVhGRHIzbm50ejZTZzVDSzFRR1d4SjZXUThQYmswdXBLMEZRdDZzTUtEUEZneFVPcWxQZ2xiM2RSMjhzbmQ5ajVIdytKd2xDZkFmVFlwUlJtWDlxZUhEN3BEblA0cHJmTmVLQm1Xb1BEMzR4L2haTzZEa1JaMEpPemtRa2FKTGFVM2p1M0RPaVNvNWpTaU42SkZzRlNvejFzakhKN25GUVdsdngyemp5QUtFWTI4elJoOTE3bDlpUkErZ0RORlJDVC96b0hTMkdXUllOU2JYdlQzd0doZXFOSWVGZUlnTGVoQ05HL2lDZHdRNis5WHU1VTNicUpVTEFINklYcVVSYVFqWnRzYXAyS0JmY0V0YzM5Tml3NUtyakdpRlllME9GYi9wenlxZE9DdUNOTTFCS3V0OWUreDJFdHR5di9TZnFKOTl1NEZzL1dhaGhoWXZnR1U0MTBWMGxwd0pCaGE1SUFFT2lIMmkvMVYwOSsyQUVzVE15YlNaZEtEN09GcWpyMmQrWHNsemk0aU00U3pGbjFKQTlPc2YwRGhxamcycDVJem9HdTYwQzFQT09pQytmNVBZVDg0dCtWNVZvV0RWNzh2U3BLQnU1UmVaVVNHQmtLWGRqWW5pVUc4SHZYYk80OTRHY2ttOVVWSGk0Y3gzNHI3ZTg0UUhWWHg5VW14aTBKZ0dySjhDcXVQajYwWHJ6aGFDNHFQOFo5WWFGazNCblNjdTJHWE5wZFdmbTRaN0FBSFpEaTE5Y0s0T0srb01PaHBoejRmVkF4YlJWRjZEcUNKcTVjZUU3c2tqYkFORC9Vclg2V2YxeS94N2VWRHJFWnEydGhuOEI4VkdQS0ZIWkgyanJMN25va1hhYWRzeWtJMUNwb1JhL3pteW5rY0MreHVoYngwZjZGdjNIa1M1R2ord3VmUWQ1VUpEQlZDZ1I3eWR6WVlZVnY3MUpiWXlXdUVmNSs1Qm9CN1Rjbm9kL2Z6RG4rS2tBcGpHbkdjZ1liTjRNWURFRURkZXdtQTIvMHBoVE56cjZUWEh1aTdCaDFLUHhGcFFseG8vNTJ2QkxvRE44TFZsWENrTzN0NFkwcWc5T0R4L2k2YW5RcVdvOEZjWDhQeWpFOXV6amFleVI3TUI0bk04UmswMHlFMUt0dEovR0QzZWMvd29KeHkyRkZiSnc0TUF4cUdFSklPRzZTOGc2ZnlSVU15a3E2bGhUekZYY0FSUDhoZlp5eHVZWVpWR2NwQjFlRytXaGNhYllYK0FETk43anlJckhXV0o4ZXNBamxUOUFjZFJxRlFCbmgrYStoai9QQ3MxMDF0cTJUem41OHA5RXh0MHp6S25vV0RLMFhpZTMvZVpZYlNuVXNKWGwwdkZJUCtCdzJ3RUtkcVRHNndJTUU2ZDU5SWRWbUJxaStWVjlNOHgwTXRJZjVmMURzSGZYRFYrRmxROHJlaU5HVDR1T1FLcTJzS1BHUTJVY2hKekswMVpCdHQ5T1dCY3JmcEpaT1J5MVdRZFFGbE5leWMrbDA0TmlQVHpvYVVuYmp6OTRaVWhJcS9aUWNNeWNyNE1JRDU4eUVnRGNoMnVCNUcraVdoc2JKb3pCeVp1Mk5Na0ZaNGZWL25mTGtrVmwvWWNLUlBna2xiR3d6ekNTaFVRdEc5QUxFYTd4TFcvR0NtV0ZSK0J1cE8yQ0QycXpSU1NKb29HMkI5Vm5HU0J6d2swRDE3SFRuV2E3OW1wRlhGb0trZ1dqTmh0a0Jrbnl0eTU5V2ZBbEw2SlU3SzFBZFc0cDduMjZraVIyRFdFK2QxTytBaDRXOWs4QjZqT0VOWXR3eVoxbWhDaHNPT2pKSnhFMEJaYzRnY3JpSW1WZnNka3FsVm9sYzFLWEVXZFo2WDhBODBtRHJIZFZ4VkxTNEljaUFMRS96ejB4Y1IvakNFMEtWK1BJSFowYnZGSmlEbU9JZ1VTaWt1QXpkdnpKRGpiazhpNjRsb3kweWlINUFHYWJsbUVOTTZSSENEVFJqZFlCS0g2TmphOFREbWNyc0hQbXltYUx2d1VNdERhTEhMby8zMDdZTjRNRG1pSlhqWE1pSG9YaDB5NDJMSzNGUStkd1ZJS2hPcUFML2FDbU1zWFNXOTVMWHdIRXJYbWtwTk0zdm9ySy85QnZQZm0vWmp6T1ZOUUVNSmtPRXVSTmltSTlGNEhFZUtxVGMvT0M3b2VOS2l3ZTA5ZEwyc3JlTUtxSGMwRmNtbUEvbjJUL05nTUN6enhhaW9hbHRoWWdYZ203Q0FlYk43YllvN2tXTXo4WFg4TkZ0c0xES3Bjd0hwekk3TzhNZzFvcEJjYklQbzNhWkZBQkd4TVFITi9rcFVxTnY1WHYxcFdFS1ZtLzk4bGxpZlRTdFZ4MWFMSlFuN1lmM0ViQzljbmYyRHg2YkREb2xtN210V3JpZU0yNVBiVUhHNU5idDd0UkZJbnVKQVhWUVZHZlpmR1FmV3Nia3J0NnJ2UWhKOTZsTmVOMzQ2RTB0bkRuWGdWN1lneUZDc2h5VndOQzR1RnFWKzRUVVlWajcvMWtVamlNQndCTlB1NklWTGwvc282WDNMbkV4MGJnWWtNSW4rZE1YMnA5YTc1TEdTd0Y2cDJBQmMzbkUrOGZYb2Y4YWMydlFmOWRVOFhBMHNoT2J1aGhldmNIK3MrT2Y5eWVFQkxMaEduMVVtWXlxUXlBbGsyUEF4STdtWEdTSDFSM1BYS0VqbXRjVTJKRFM1ekFxRnBRZTlNemFtTnRSc2VRTkxJblZISnlFQUU4ak0zY010V3BzNWEydW9Ia1Z4a3BaUWVncDZvaFZjZ3ROWWxXTy9CaUszMUk1N3dta2N3U0xxbmk0QWJtWU0xREZqOWtsMDF1NWhBRWphdVZRNmhSNjlPSDJLRktxUVJFRXNlaWRzdmJhNjkrN0tyeEF6MFpPRzQ5ZW5zRVhZQ2xwRjlPSHBaNlRzUG1hejdPOVVPa0NYUXV0TEV1NUNUc21aYkNFWmRydnpTU0g3M29OQjdWa0xxcGcxMExuWGxzdis0WmlZUmZXNFc2MUJtQzRMbzU0N3dWQmF1bHoxaFB5ZjIwc1FuTkIveTdqZ1JWK0IzK09sd2U0QkV0VkZrNGtIalRUSm1zUlUxd3QyVHBBeDAvRU5Md3ZlSjF4bmZkSyt3eXF2c3BJUnp4V1RPd1hWNFRmQmtiU2J3K09PM0NPMFRXZStZM0ovdnJTTzF6TkFsZTBPdkRlNVZKUzIrUFdsRExYejIxNEVoSi93VU0vd0RqWFl5QkEzN1pES0pGWmVKYXFmTXJJclBiaUJyQ2RxQjltZ0lWR1FTb3M3bFhOWGRoKzNITURkMWxodW4xWGhrWWZkQVdRbnBMdnI3b2s4Z1loc2J3SE8wM2IxbkcxZXVzd1lqQWpCa2E3Zm9iQ0lRMy9VSXQ0ZkNuNkt3THpYbjJ1S3RSRGgyajlGSHpOeXFZMjhFa0dCbUxUc0JNVVRYREsrSFhNaG1UQjBodzFJRlc4SmlIV0FrUDZQM1o1Z2tMUkhvRFBqdTUrMDIvK1ArcDJRZ2tDUWFjc1o3SjlpYzZTaitkSVBNbFhjUDdYN1k5M0E1NWRvNndKOTFOSlpnZ1Z4alRFaldHdUpSdDgyaDZwMnlRY0cxSFpJa3d6aVRSU1lYMFpEbVNxcG1aeUpCeW1hbnQ0RTR3cnJCSUNBcVRpYUxSR2hNVnVmdmtBaEhRSmZadUtUTGtjbVV5NlJVYUVOZVRGQ2VsS0dHRTViRlpHT2IwSS9xTERoR0FNaVh2UGc1ZzR6UTZOTXhzaGExbFhnTHFpVGlRZCtIbVN6MENmb0JrWDAybGJoSzZxOTc3VmZ5QkxhN1ZCbFRyUk1xZVJocnlFZGNnQkZxNFFBZzdnWUpRNmdESTBnMERucHh2dmVSdXEwd3BwalpDTWw1d29PZk54QXV3SFBpMnlMNkxDeU9WZkZzTDhnVzJSZEdsdHdaR3E4Rzc0bTdWVExzNklySFhadjlWOG82USs4VHZIQXR1Z1lOdnhuT0NNbXRJSWNMTmZmNWdESHM5eHRsMHdkUTVvdTA5RzFicC9zMXBZak9YSGZ4WmVQemkyNDEwb3JwRnhaTGJsL2xuRGZoam10Qlg5UU82dWNWUkFoeWFWbGpRVU9jL2c1UjN0SGVTWGMrbVA4bDdaYTJyTUVuM1IxVS9UMHRkQ3RQYTlxWUJCUDZTTHY4SG1iYmlMR1ZNWUFia0xmcTllRVJNNWdYcWRXK0M2NkhxdnVreVJGdW5DQUg2TVpxY25mK1lwMWtON0xYSGx6QWppWk45cWN3K1AydkJlS3phc2hNRytyTGs1aGw4YUhKUmZoWDZ2ejhUVGNsK1VxK1l0WkNpZk9tRjA0YTl4VjNlRmY5b3AvbDJtbXRHWU5DWEdWSGd5MllNWWxjSi9QT2xkWWxwOXZNOVAraDhVR1hCelpwaFZhcTU4dURYWXNPWWFkRWtpcHF2ZWxxOUxZSnJ3dHJORGNHMjFidHVrL3pLNTNFV2IvV3Jrb0ZBYXJqbGxMZTBPdkhWM3Y2UWtQUGNsTEpPdXdZR01lQ2c1ckFzM1BjbzVQQzM2aHVuVG45MGVGOWhidnlKVE12dk1YYUFoL1E1d1poSE81clExdUdVell2Sk0zbHloTmRsQmsvZ2NNNitwT2RsVmpNZlgyTjVqaFNQZ2NqSDlJZTVJZjBpVUQzaHUzdHdveGdjemlWR2xrdGpTR2JFWERjTmNiK2x0SG1Sa1crY1lFQ3lOeXhIT0RQdHZTV2pBZG5laEtlMFRhZUpGbFFFREFJQ042WXBvQXNjcUZzN3RuOWM3elZTTDFuQXliQmpySkNHWDIxTFNIVDNsY1d3N2ZBRTVtdmg0dDJTTHdtWlZEbmhWK0cvSlQ0ZXZpL0RjL1hnb250eW84SUxaaFZvVThYeTVYZUJOVWRSMTVuZUMzdUd2cjQrbEluSFgxSkpzaytYdDJsdFNuZ1poS3U0Q2o4WkZpT1dJUUEvODVROFVQRnlQY0ZUUUcweFFXdGVZVzY3c1JXQ21UVm51dUxuVWZOa0NvMUc4OVBrRDZicVQ2N0QwOUU0VVVYMFZlZVBCVXVVRHBvMVFLZ1dRc3VidDZaSzVpNXhJdWNwVlZOS3hyOWpqMWJEcUNFZUVLUDIycmlIVjBPUXBNdVhJZFA4cDMrMkllUDl1NnNNNlBMaFlIL2VlS0tzZ010NzVqM2h3UllZWnA0dnh0ZDA1RDZXMVZwOHBtSFhVT25qQ3R2b240TVZEdUxUZmFRSVZVYkdaWHMyaDVuNmNNcmZyMU1yU0thVkFmU2VYNVhMb01idGY1ZStGayt3V1pEejcwOXpEL1RFemlENktSNTRjNldqcExFNmRucFlXS2NNTTlYNmtVSDFyUVRaWEc5UkhsRktndndxS0hQNmZhbWw4b1VjeTF1Y0JjRi85Zmt3ekdkTFRTWWZJUDl0UEJnci9TUW94SXlKOTRPYlhPMWhmVEVPMjFTdUdndmpKdVZ4SVRjNVI3SHpiU3Y0SklIeUQ3eXpBTjhTUU1pb1doL0xIVkl3VnRydWxOYis3S1lnYTF4MUhXRFJDQS84eksyZllXOFhRZkxBT29rUTBBS2xpWDZXL0ZkNXRJY0V4bFBKZk9wMnM5RWU3cTFXV2NnRlhNYmw3dkI2OEpiQlBNMjhwYm9paFB4S0hJTjI0NkRWTW11QWRKTHhpQ3I3WW1BVWtLdjZkbmRDRU5aSGs3UStKOWRvOWdrOFVzOWVWaFJQSk9NcytrNWVKeVNSS2dzY2FCRXp0TEI3NHNrZkVyeDBteFdWSlZGQ2hRemlnSjJPSzVIT2F6SE5DLzA3TkpYbDViYzRmMGpjdHBFakROUlVWdXd1YmtFQTM5ZDl0aGZLQ2hVZzF4MmNOdEdpWUFnRUZNcWJ4Yll0Z2ZxVElpOWlkQVJpR2ZpMTlZcWsyTmFyeGVObXdlSlFHaFFnU1IwZ3RqTEc5R0JBV3N6T2NaUWZtcFlkUHZZWGxXcm05aDR6TlQ0Z3ZXdEVMZ2RqUTlUK2dqSU8xRkRsYVNKaldERjVvdWFudm5HeU9JYUNLQkVGTmxLM2RwNEROU21Kb0Mrem1GTzI3RDNTWVdVQk1nRVNCZkRGS2xTSnFaRlVPWHVGSlFpaDdmRjl4Y0lZUmROcGNlSTBsUnpsSERhc3ZUMzdvZGJBTXNlaTJLZVVvVXNMZmhZY3FHZzR5SXNpRzBYVU55dThZTStZNkJWbTdadWtEdmhDb0pSSXd3ZXZEYUhpc2k4Q3FhYW54VTIxQWk2T09SWU52Qk5KYzNhOVJLSVgwOHRkSEEvSEdXdEg3aFJCTWtDNkY2dHBWZVc1ZTR1L0dDcHdaYmtVZkUzWmF1NzFuMUQ3L0VOMnVidkN2THJPZDA4S0RWTmJjSFlWTkw4MWFkaDZTbnpuMGYrbklreGNWRTUvTmtuYWZSUGxvdEwwUGtzL2c5Z08vRXFQTzA5YVZTd0UrYjJKN3RrMkFIZTRnRmVuVHY4aVhiaERKaWdub1FSODltdS85L1UrQ3NRZmZyMGsrMFhvWXBrK0JsWUVsU2Z3cWovQ0x0Q0d2cG0rY3l0L2JpcC9Wc1RoK1pEOFZGaU9CVE5TL0EwVGg5bzZ6aCtWd1pxQWRpM2FUdHZuNlRDMkhLWTNEeUlYNjlxeEp6NW9pMXlPZHdQUkgwTU1HYjlTMGRoZFJmNzBGTDN5d3lrTWhvY1U0Vi9vQ3lSZmsvMEpkTmxtTXdCWHc1T2Z0WjNLOGZabjFLR21ydlNockhxU3J0MngvUUdnQWw4Z2laQ2kwb0JyR05US1RITEZKUkhvWmpYY2tKdXlZTzhvQWhSbHhsWjlPOTlnNXZvRkhSQXVwT3RsY1hKWGI3dDByWDh1U1BSNHNEbmxXdnBwMGg3eEYrRDJROW14Um1HSTVCWW5EYWdEZ0g5MURtcnZnaXNPOTlPUFBBRitHZksrMnQxNFpHQ0dDYlRmQ3pDbVF2UU1Pa3BLaEliZVViWDNlTjlSR2IrcDNvYlBlTnJkQy9WSUVTd2lNUXB2OTlZSXpxdEhqTTZPM1Avc1BZUERNVWx1eHNPV2U5cXlXb1ErZHlQTzFFU1BYSW5QTi9xUDZuMG5TSDhBNmllVGJacGJFVWxvUnJ2ZDhsNHljU3hhYS9nN1BPVkwrTUFGU2RxcG5PdnB3Mmx6ZXZjU0VhbFJQWW11ekpIYkpCUnBNK1ZncWZkb3V2SzhxanVFVEI1MlpLY05pT3N1T2NuelBvU3hvMlFCRDVka3Jmb005WDBFbGlzWnF1MEg2V25sWnRqaEtVVlJRYXdLVEQrWlZ5L0RpSTV3ZjhNUnBSTGl5cGF1b1ViNStVSWg5THMyQW44YWh2M3VWdXoyVEhiamNpTTJCYVpoZzRyRFVIL244MGtIYVRqU3l0RXl4UHVHMkJXOHBGdW1uVCtuMFpqQmtEMjR6RjA1Umd2WDRCV0VqRTluQ3c2V0lGdXBxVWNSb2x6WEt0QnFNNHBDVnRDaDVkc3JZS2FZQ2h1R1hmNG5TUlA4ZDIvMWIwRm44QkIxMmZXWUl0Qm0vWTN2d1FaV3ZOS2hEb2dEeHlwbUxobVZ5SGgwdWZmb3ZkR2ErSHVYM2o1dy95NU5xMTFhbmVMcFUvTFJZcVNmVXo2NlczOGNOSEtVUFVzWW5vSUZ3SjdDQk9YeXdCNmoyYVVCVG9qUWVCK3VvSlA5NVNlakFhV011dUhWaFhNMjNxNmJWOXY4ZUsyay9MYU5QekpoM0FEa3JwS1ErVEhDRFo0akt5cm9HcEMwQVV0T0tXR1hjZVFaTy9jdG00TnRyK3B4b2NieGxVTENFOGhSdkNta2xRK2pWSUhyemNNYkVHSkQ0TlFsTGlqZERjVDB5ejNOc2NRMnkxWStacTJIbjI3RXJYTjQ3NVhjUkgva0l1MkpJcEpNOU1JQ3Fzc3BqZTE2V0ZONkc0dWswOFpoWUQrRHlaVXhmbktZUDZkVG91OFlsMTF3ZmptSm44MUJWeUQ4eXNWWTRkNHhVWjJ6TGozU3Jpb1ErTmh6Q0RyTkkxOUNyZVREYmZhOXRhRzRWZUVtcFp0ZWNwdnFnN21MRGV5WjNHdVNsdzR5dkNMTDEydXZzY21BV3h0S2lGMS9oaXh1WnVWeWJ2WXhZWUUzYy9QblZUczBma2p4dHpjRW00R0s5MVFaR3dxc1loT1FNbjVMUDJJbk9mbEtTdkVPR1dlVmdHVlYvQ3pITWRCaitZMjU0Qm1SYURuSWRhTmpCT2Q5cGg5UCtxV0hDTXo4L2Zvbk0wc3JpWGtRTS9jaGo1bnNPMDRScUVTSDg5MjFJZllzYXQxRlpIc3dwbVY4d2lqZ3dUYWI4U2FPOGVUUGtNaUsySVlmbXk0YkFmWTdzYnlQTE9KMWE3T1dmY0MrazhUdGszenpSRVd3a0psSmgva0xIL3hBbVB0Rk5WekdzVzdpMUFla1VUK2wrTFpjM2FiYTBUQmZsb0xDSVZpbVE2bjM0VWNkdGpBWFhhSWpPKzJtUExWd2Erc2RMOUJ1TE5NcUlNZFJjQXJQbFVBaTlwSytYZnIrYnM1WjFwWkpFdkdONXdjSVk0QlpNYzU4Ty9wVzBPVkc5UVhOb2pMR0ExUmVMZlN0WUlzOThpTCtBWlEwZ0Q5Yi9KbDhmMTIzbS9Nem9tbitxSjUxMTUzd3Z1anRQNzBEeFJmY3FObHBhWmdQQlVobjQxc0RpclFNSEYvOVZzZXUzQU9NWFRxQmpadVE2NzZFZmZydnBsNHIwelYva1dkSmdqOG1RSFpUNUN6amhaY24vREdKQ2t4YjRrQVFxVGRReGlTWnNGQ0tjaU5kdmxMbVVXbjlWQ0xscW1DMGZjb2ZVckhJWTE4bXBId2h3NkNUR2tjY3VPVEowVDBMRVNaMnFFSTEybnNRVVBHRGJuNmlKSTNqaCt4YnZ2aEZVZDNWY2l0b2NmTU5JT21ZdHAvQmNWVHBzM25HaFR1ZTZ0V0VReldOc0NBVkY0YitZN2dkc2R3TDdNNzM0T0dWTTZ1N1JlRTdXc3ZuM3NESkIxUU9XLzJ3ZWNjT2l2WDAyQi9mSmhkTThGS1RNV0I2TkJTbEJaN1RQekREcVZEUnFxT2VxUnZ4VWQ1NkRPQXJIM2lsWjFuLzJDYzV4eUJoWGJuTGNYREtnaSs0V2R4YmZ2ZWREYjVQaWlJMENmVU5VL0c2cFk2UC9Cc2N4VTVxelRsdGRsTDVpUWlJQ0JpTU90dlVVWksrSmg5MHBsa2lDNExLMldVbjA0TTRKZW93R1JCYWw4ZnRPVTROaVlObkpJRVlXbjJOQlhLRnhYMHplaUxHcnFsc0RBVlJQMFRZR2dENi9WR1FkU0pRSVRvUyt1bURLT3ZCNmRhaDFWSGR3blE1V2tralE3eER4YUJCV0EwcEU2bTNwQzdaTjlsaE53NVloRDd5blRWUEMxTVFlQTFXaHdXd2FJRXNHYW56TSt5bi9ZWDg1ME1YL1YxdTBFQ3l0MERQMTMrUm9iOXZZRnQwTytqUFNkaUVML0d5dGQ5Tnd3b05Tc0xJTDMvQnBnaHFwc29JaFl2RCtqckJwdnpHY2YrNGVTZTB2dWU5d3QwbEJLb3ZwSjFFOGRMc2czVUtWM0VrTENCVDVGZVlvQWNiRmlFYjFaSXdyTkxick5jT1NiaGQ1Nlk5NkJucFlpeFppb3BaSy9QOENKclNqNS9XbXE3TEttVENnODRWZTVEeHRXYlQwV1BZV1NmVVpZQ0MvTnBtZDMyNmJWUWo2bG1TRXNhZDdpQ3g2bk0vRjlqQ2VUeE8zTWtoWXRLWVpUeUtPVGZUNnVpNXhCRC9Pb1JHMkwrZ2JUVis5bVErRDErYjNSejNGTVA0S3JIamhrWENTVmRxdVZKUGNtSFRJaTA5R0RaL1kyem9TSWNJMUdlSTBiTkZNcUxNeGZOMXRESGZmT3RjdDN2TjlwT2V4L3pGNU0zLzk5WkRmMHhLNUxlSDdNQUlVWTVGK2g4U1htTjNib0RYL1h0bGI1MVlMSXY4bHh0WVdwaTVhYk1vZE9PTmdlcno3N1pvc2RkOUc2a3kwYktqbGdmUXVTRWlyb0M3NFVwNXF4ZVFjcGY0eURtRUY0ME51azIwUk1lWHMwOGtYUEJ2bWJ3cTRJTUhmb2czMFoyS0hLTklsUkdyVzVUdldCd3J4czdhYnBBdkpmbUY5T3k3cjdBYzlIemhZZHBBNkM0WlNGWmNGNWdoUTZEdDlrSGFaRmFvNUZhbk01Q1l6U1FjN3Azd1A4YUlvUzhGeUw0dXoxU1hxM0wwM2RONUZsakt1dnkwdVJHMjRVWENyYkVoZzBNc2plU3JZQ1VJeGtpM29LV25ZNU1XMW1mQjE1SWRFMTk3TExBbStyb3VHT2dYWHVJdkxWR1RrYm1XVXdSaXpYajNuZ2ZVeUxrakJyZVp5RUZrVXZEV2lMeWxlRzNhalg3ZFBla2hsZDhZOWxOeFpuTDN2RGx2blA0QWhhYXdIWFNvS0Q2MWNBdHptL0I3a21Rb1N4VVRhb1ZPeklDcndjdUxDMFNHcklMeFp4aVFSUE9uck5NQnN2NFl6djNTaEhEcWJXVVU2NWF4eGp5NEloYmtZb1lQOVNjUVJDL0ZicmNPSEhqaXBtSHhzU044RWNHbHdPeDRLcm9WQUVGK0lJRTladkRjeVFTeUkzVjNUaG0vUFRQcUFaTGNkbyt1cWc3bHlpZkRjampWU0ZaclMwVTFqdDZJd0FUSzB3eGhpZTdlQThTc3lEdWQzNzlVRFk0a0pjMU9lcjJVdkpuNk5lUGUxTmNZdFlYVzYvZ0ZhT3BhNUlCYVhLSGZZaDFZN0pJOFhQbDZXbFlhbnpTWHJPN3RTNUVtQWNNVDVUQkxkZmgwVjlzeFdONkt2QzNtUnV3UTBpSytDZER5ajQzWVFzbE01ZUZobDFXdTFjcGxteXhRVDh5c2JrNVl0NEV3SjU2d3FOQzR6V0loSDI5eGJYaFgvQThsRVdlNWh6MWxmVVNMZUQwa1NqbnFiTHUrSUR1Ujk2Z0plUWhPRytLUDgzdnI5MVBJM3lIWWN1TTZjaTVoaU1yQzR2KzF1elJKYndjdVR6eTJpQ0lkbGJseFhPTkVuY0drcnpiOTNhSjJsUWRaRTd1L1VVdjg2cTIvaHpGSzdYbndYeTVSUDkrTzgxZ1kyWUlqYmlMekd0aEU5aE5oUWloQm9DemhRTXNjMzJETXpXcHNjNW9tYVp4MENZUkhkVlV6V1k1STF1M0lQUjU3dVlJZ0RocDYvdDR5WGJyTDQxWEQxUkJyQ0xHMkM2VmM1endmQnExdEpyL01rMHM3enRLNGtpKzg5TnB3dXg3ZDNtMHBTK3BEY2x5dUV3czBZYitJQUY4Z2VjNk1pdTdGQlpTZUhzdmxIaFZQZ2ZkUFhiRVdYNHYzeVl5dE9JQlpEVE1CK1JSK09IeEVQbkpnMk5CV0U5Mk9XVENEeWhNbkw2L3F0WG1pWUNHU0kvZjNoZEwrNy9hcmxCdG9NblM2VWJOVXQ0ZXN5dEw0RWViY3Z6NnJveWd4M2J6S3A4dWs0VEIvYmM3ODNja21SeE5lR2czakNLVFoyR293WFNGNFFvWE96MGNpdjJYc3FrN3czcEp4VVBZZE5PME9BZGsxQXNqZW5zYWs0eDVXeFZ0VEFiYWRCWEtFWTVGNGxTV1g0a2tSZE5WLzNqNVI4aDlLUUlKS2EzU0hlUDhrbmlVdzBWUkk0blh2bXNyQkY0dWJjd2NkSU9hSXFlM1RJNkluajZqa1RzbHJIS0NJT2NTd01pTm1sV21XMFF3dmkwRXdZZS9kc1R1WE0rLzlwUGxMeXF4aXU2cVZqajdNUVJPS2JuY3pQVEZZWmF4aW1pelR2UTZQUjdwWjRWVHJOZXhQckVIcTRUV3ZHODZBak1vRE1vUVlVVkswNDFJMGk5WkhNM0pHZmpwODlqeFIzcW1abEkzb2pBZkNTc0d1WnI3WnZXd1kzQ00xcXpmaDlhTktFWElSMWRGSDlpNGpmMzdtc2dNUmJkbGI5OWErVzFLaTZ3RGxsT3RIRWt3dXU1Y2lBOVFIRkNpckMzTWduck4yRVorajZQKzRvaXNaTUgrQ00rTEY1ZytENjJxVXNmcEZTVVMrN0ZsbnFYMW5Bczd0SUZCZG1WYkxJTmwzb0tvZG00RE9jUDREek1ES3p1dU5qVDdrUDhKMkNUOEk0Mk1TVDdpQ243TnlyL211byt3Qk9pMG1VZmlFQ2d2RG1RQzVuOHA5Mk43bVJPY2gyWWJCMkEwckJ1djVmS2EycnpsbGtIQlQ3YXVxRWxQRjJPL1pwMFlNRXpzc1J3Z042YXU0VTU0SExnQnRYVDlqakhJYWxiaHZBbUZXNC9QaXh6ekVyQzR3eVJoU1VPTmJPTlEzUWN4Z2tyTWRaZDFuZ2lROExLQ3ZSUndNelNOd2N2L2I2YnNuc05MLytBOFpnSE1QbWJmdjNTUlZxL0Rmd3YvVFB1RUJqL1NhRVNWeGZNVVNJL1FhWWYvVGlBajBidFlLVGRZZG5NRFhMOVIzRk1RSjlFdnBWS2N0Q1BJWVY5YmJtbWRpckNOazhKS2VhV09UeFg3N1h5UXNXck55SGxYb3NRbUpvUVBPUXZvMEo1Z2VyTSs3QlRTZ24xU0NDOUdKZzdwNHloSUthMXV4RXZxOE1mQXBUbUhaNTlheXdrQ2lLeURYQ0JHeDJGZGdScGZ0ZkJSNG1NMy9ySXZ6anF0OWkwWmJKM3ozOXd0dEZ3Y1dpNnVoN2FZamZ6Wk9BVW1vZ3NNTGJOK1B6Y1hDVE1PUFNBcjU2ZDVobE9WcDhKc3IrUnc3WCtpL0lhNWV6bGZITkZjdytOdnRFTlFheVZPUitjbXhoUWNQbEFJUG5VRk9aZDN0UlljWkpCcWQ1SWF3NXBSQlI3bUZHdDIzVWNoeXlLOXRTc3paRExmWWUxQjExMitwQTdVWkMzYkcwM21Ma1c1Q0JER3JJWnZmeCtTZWgrVDgrcXZSQko0anVLanNTTDlPZE1TMFF2NkpUb01wSE1wVDV0QmpYMnBMc0ZiVU5WL3IxaXRSSmxMc2FWR3VKbHM1cDk4bE5hTjNMaERyeVVvTTMzQ2dEVVk5VVp1SG1QSHMzMTJ0ZElVVSt3R29ocmZXdEh3Qm1LaGgwaHJ1YXNFOGFrNkc4ZTRRMHpWM0xOYzBhSjgxdk1hRkU0cEwyeDJ5U1lNNHFBUWdFdUpyOE40NjZpRVdCcDZHYjFSckZQOGFRSWZwUUVIWUc4Vk5pRWpuOS9MMTg4TFpQODNEYzBkOFcrTUJsMmFIdkMrZTNMMm82eHdIU1ltM3QwZ0JzMWppSWJBN3JMUDVteHpSWktxZmt3MnlYMHZUVnBXT1Z5Zk5Tc0JaeU5aRStyTlMrTXpjVVpVQTlEOTNsOS9lU0R1VzNEc0J6b2laL0oreDZiTTFmR2FOcFYrSFZXdzR5WkJwdEhuZ2tLOXRVZGRxTk11NjFiWVB3cHM3cWtFMUZ2S0dGR3EvUFZVT2oxbGs3dzMvMi9BNVB5aENOMElKUGN5Z0FuVkNMZlZMd0NWUTk2RSs4SHI1Tmc5N1ZKaEdhTkhsVDRuZzZYQ2ZhaDRRTUJtNUgwWmpNQm5kMEdLL0M4WDlyNlVoaHpzUXFVS2pkVUpDb0t1L0cyOW1mNEJvSUowWFpCU2hucDdrR3FFSWRvaE9DalNETGJDc3VFSVlYZmxuK096dllhVTc4bldjV0krbHRxYW1uaWJxUHRUdHJzWjhTUFB2TThYV2hIcmdQQUxybTliQlRXN2Qvb0swbFI4K3Rnb1MvRHBzUTNHd3N1Y1l3Z0Q3ck5HY1RlUGpRczEranM0UHdadzFCTTJCUmlSbndlWThHYzdvV21NbWUyaENqRHlLVU8zelRDZjgrd1ZjUjloK2JyZzNudTIzaUxmWDduVGZjS2RrUDlvU0lWaU4xL2Jva2tyaVBoWktWS2dsQXpySEM1cXB1K05nb3dKdFkzTDRNazkwMExtU3ZCL0U2NXBOcGZ2a3VpTjZXTXk5MkxPMHVqcXVWalZJTEZxZjhpUU5ZUUZ5dC9UQk80eVZndGZtVkE5Tks3M2JJRHdkOU9GYXdHcGl0bmxSMWg4NjVpWW83NFk0bEZIU1RwM00xTWRFUFdkK3ZkUExIaXh0TVE0bDZQUWZaUXorMFdXU3MvT1FhVjU0UHk4dDNsdjRXMzQ3cjFocklsQ0FQWG4vRXJBc3VLRUNUR0JvS2tldjZnTk11dkh3WjI1aDNCcXdRc0hwUnROV2g2MzVPZW9uNWJUOGxuVHBxS2pBOE94Y2oyb1Q2NEpMdk1vTWJWQXo5d2dRV3kyU2UzYnFTcXhKVmFpWU1sbmVPYnhGeEhRaFRwbTJwY0xjMHJ0d3BKVHF1YncvUVJ1OW1XMGRMNjIyajk5enVxcFQ4aW5CRDZhTzFlQUtIb014ZzhhSkx0OFNuei9oNUxpYzJDeW1oTnZTL2NQSUdhWWhqK2xEcGJUSHl2RzFhTFZ5Z3RPMGRmSVMrSVh5bXJHbmhLMWU5bngrZHpRNG44T1RKeXlueGJiTi9DZkhaRklxZjlpbUJ6OFhuME96YjkxSU9TNVN6K0xjVGwxS0g5ZzFyY2xVWlR2djdtUE5rbm1zaGwrY0E5dWhwb0J2dFdMNUJmbVBqVFF6L1ZzZkZMSXlmdEEvYWlQemo0N2htU1JjWVlyb1BXQnlPTUZoK2crbXM4UEN0S2V4elBMTW80QTV5RUlpVjdEcEo0M2hWM0YwMVVabHRURjkyVEpoallPTHB6ZUxiWTRkVG1OanZoQzh0OVk1ZmFYWUZVeWJRVnZsVjhoNmVpR2dpUG5nazRSWVV2QkRreXVLdzZqU2ZnQTVPb01pS1FIaTI1QlNreUp6QlJEQ05FekE4OUxkVUNmQ01uT3gwemRaMEJ6L1hQSVZVR2orekYwZ1lKVklIdHZvS3p6dDhGU1BTMjJLaFdreHVmdm5vQzFzMndoK3RXak1hU2dpUmtOcWhveWlqNC9weCtTSkF3K3h6NVlQQjNjQ29XWU9CODVDb1NrcE9nUmZrRGgwREw5Rng3U09BanZJOUFSb0x5WEtWZUZJVEF2d3VOMTd0cVc3NjZZa2JxbUU0d2ZkOW9CTlJGaERYUEtyeG5Zb0ZUbWhWR1RvS05QOTRnaTBqNTlhNk1xRFBSYmpaVm00aDdDN24vcEk1T3l1MlN3bWZxdzMwS0xtb0o2U1EweVA3WHlhT0F1WEg5c3JLMThObDYrNkZvT2RZbU5ldkZNbFFJL1I3bW1ra0tBTDMzZUhRa3N0NWZaNzg4VU43SzI0UE5WeS8wYmNUNlFMOGNGWXNMdmJuTklmR2c4blVmVnlhcVFRbXJoNHQ0VGJxZ3RyOUk2VXdwZDZ0cE5UY0hpRHY4RmZEOWpzNzlTLzVQY3pmRDlUZitmMDAzeWRkdURmSjlTUytUMmUvbitrbGZKNnZQOC9xRi9QNkhlQkh2MjllL1d2Nzl2bnYxeC81Kzl2NSt5UXQxK3N2N1hXQVVEY3QzRGt6QWlnQzkza0JTUmlVN0hjeUswamRqOW8raGRUNUZ0UFkveVRRbFZ3dGZPcmgxTzBOcGVVQjR2RzVXcmdWSTdva2tuRStmMjdQM1RsZXAxYjBZMG1LeEZGQjdySTh4WXhVdGFtQkkyL3V5Y3hKZGljUnRob0VjSjYrRmtQMWltZ1EycUxuZUN6UDVMNkVXZUZ3RjRYeit4c3VBT3JmRjVZcUZRcVBFWlFobkpuZ2xqQUhwKy82UnBJS2g2MjlsdGhnRkpnMWdYVkhKZko1MUhsQXNMRnFpY0Z2VDB6L2ZEVHhiYzg5V2IvVkRJUFFFb2ZySTFESCtYYUdrZzkrKzBma1Q4S1lRUllTdDROaDlxMStqa2pZOGlhbzZldHRqK2Y2a2FJMHdPRUJpU3lxaVdYVURGdnpkcG8xUDBFeGVnZkhSaVpMMnpGUzhvY29uK280L3dqMFJ3RmVxTEprb3BtR3d3Sjhua0hWblZ3TTZFRC9idEhmcnRSZjBuWHB2OW9UZWoxTlpCRjY0amJ6aXF0VjlNNW52SXZrc0FuOVV1ZzJDaFN6a01uWE1zZ3VQZER0bDVCay91eG1sbVZjbllMa0ZYOGE1M0RFMmRtdkk2K05zUUloOE1BU3RpU05kNGI0VGRrZ25MdlFaakkrZ3IzQ2p2UGxVQ1ZCTlhaRDlmUXRVZmZ5ZFh2ek5sQWl1dURLNWc2QmxLR0VYS1E4MXltOHh1YkVJRm1DY2RiVDVKWmxWMWtySWFhRWNGb0wyL2pVckVrVEtJV2dMbnVEY1I3SWNzWWpiWGlOYnRLYnVUcC9Pc1NCR3ZMTDJTVFZNa3V5aTQ1OUt0MDlEOXdGQkRpMEFMRmRFWG1PMVVIVGRiRUhnV3pyNExHVUpXQkZZN1dEaHgybVREYUxSaEZoRkozTVJGditpT1hFWHg3dG1VNlFpRjVnODFTb2dkQlhOTnhZdlUrbTdReDFDeXlWRUZJQkxMY2h2ZHlVU1licnZMTTEyd3cvMHN3cGVGL21yY3F3RFh3ejFNS3BrU1NlMHRJVUhPd0lhc3R1b20zcUlydnZkenhTR1JkV05ONUxiNXZSTFMwSFVjOXhja05jOEhrSDZxMGpTcHMyOXg3Ylhub01ZSXdNeGFQVEswUGtSUkhwVFRjMFN0UGxVTjhiOXVQK0JMaWpnRGphV2xQSnFiR3hFazJDTmhSSEo3N25EeFo1cVpTQmJoZE9qa1pwTXRZRkcyWGMrZDA0NGZZSXd2VUcvRnAvS1VsQnkxSEZrWnlWbksyeHE0dVM5STBsVlNjR1g3OFAxbExYa1F6d0FpbHY5a0xlSFJjbFBCNDBnYWU1NVZnTm9JR09qTEo5SXdIN2I3emQ3MmpXK0tod2lEakNRVFN3bEdvSExtdnRzNEVoWjYwaDhEUXE5ZXZzeHRGclVrMXpLdFMySDA5OGVrVzJqTExJenRoM2dZU0JKa29hd2JYSndpRnpnL3ZJWlVybmN6bjl6RmN3dlEwS2JVVW9qMk1pN2JhSktJOVJiZitGSURNZTlrekFPelI1SzVkMEJxRVFyeGpTSkJwM0dYZk9YQVVhdWgxRTZBc1ZPOHdZdkhNRUpOeUNIaWRHK1FTL2FLQ0toRjlsNW9iV2I4YkF5QTRYK1NMSklmcG9GQ1RlbDBZc0E0N1FvUXZLcTVKUzQzdk9WVndIaUhQOStMSmlSNW9GQzFNQ3FzTWpVWUp2Y1l1Ti9PdERtaFZ6c01FN2JmNTBGNU9jR3hmQUt4T0RCUnA3cUJSNFduSnU2bFc2NWtJOG85Tm1pc1N3RXphYWJQUW1zVlVzTGhZUWk1YkdxdmlwMjhmY0JVOGRtVG1BQUNaaCtnL2IyMlFCK2dRYjRxMzJYNGJ2OWVFait6MDZmSW9sSVlJM2k2eDRUOWpmTHhidUMvaXhNc1E5eCt0WDAzOTFwMVVxWC9tZVZadVNWZHNSOEdSMXIrcTh4dENYcWJBbzd3RzZNSmliMTdWVHBOMmgweThBUFNoNnRISWhOWjFvSHB0elVhMXdJb3U4RzRnUDRETkpTVStOL3kzaEc1bGpNWEdtOTVxKzF1cTVQRnRRRlBEQUpHNklVb21TM1ZYV1EveTJydzQ3cFNNZjVxOHBibHcrNTNZOVA1MjE2OVlMNGpoYjRXUmN6Q2o3T0E4MGVpUkFQZTFIUnU0YUlsRnYyaHdJWGNIcit4RW5WdVd0U3pXM0ZiM1RXSWcvc2FIaEhLcWtmaEpObForWm04VmE5MUpFdlhQQVBpMlpvVjdPc2VZTGRTT1hPRlJSL3BqNm9RUnF3ZWhBdno5REdoNUxCbnNwVkEvWkNmYXJkQkttOStIYmpEamJ3akhVYVZpRW56V2RwLzloOC9PVGJab3VSTFVaa1JMVTV3MGZwUnM0anM0WTNPZTEzR3hPM2dhSFNYZ1pQZllER3VUL1hmZ1RKclp5bWYxWWVKZzdVeDdFR3VGRmt4RFhiYW1vUFREd1Rmc1NPaWZ5U2lDcVNmZmpza0dBQjBDMnhqeTh5M05ZamdEK1dPSXJTRjBlakdCV3RZKy9mOHFqcHV3WnRlb2VtaG91OWtmU0QvVGdWMzNnNnVjamRGT294MGNMc1lqbDFJQitJZmlsYVgwaGFYNTJGb0d4SnZpR2tEcEtHWUxSQlVlYVNqOWxDSGpXTmZXRUdvTEhXMC9mVzVvTTR2aXh4YjAwczRVMTZ6cmpJZDhoM3dWNXZ3R0MxMFQ1UTVONTU1V2xVdHZVMWNLM210NXorTCt2OXROMko2YXpBRzlBQUJzNUpveVFjWWJwM01KVDhDcVRxV3JzOTlpS0hwZmQwR25hUm1qTEFBWWF5eXVCeXpKWjNDck9ZSG1HN0tIWXhvQ0tCRHZ1dGlvM0hMVzdTTWcyZ1loeTdaTERLSXpONVVwak1jbGROUExBSW1NOEtkcFlRR1ZKdkROd1E0Y3dGTkJ4SDluS0d2aXp4WjRMNy9ON3dNbjF3TllPV2NaQ3dGZlBONkhGWW9QYUxNd05vOG81UVhJdFZ2TWtIRkwxNmh4TU9yZGhLSXQ3VFhGYVhoNFdjajhPeUxobllTcnQvUUJ5RXZQRFc1R3M4QzhwMUlDUmtBSzdVU0twUFgyeERzWTczWlVWY3ZQVTBadDM1QVNmS3Y5TWpiYlZsOUZ2aWFTMCtnWEY1MG5pMVVpWXpwejY1WXhWUkRGUndJTGdkVmRjMTNlSVhQdGo3UzNOOFEyekJmMTZoQ1d5eHRYYVZLRXg5eUJWUEdvVVVZSkc5RVlKcWpmVlE5alltcUsvMkNWaUhkSXgyOXh2WEEweXh0aVg4NjZmUEFPNldxdVZneHlFcEIwTXFwMVFHZVkyK25WaTcxazFjZUs4WWpNcmVCSEVXdXhtM2xDOUU1cUowUWVFL2NVdGFjMkw2RUZSem5QZ3VreXQrK1BRc1RJdVVudUpZSGVPbVZ6YnFTY1IvTGMySjZKYkpNOXJmcjFJMVVFVzJVbzcyMzRueFNnNVRVTFNqYS9od2RNZW4xcjNOVWE1Q3IrZWtxSVRCS0ZoZDg0bFZQTjlnVm1uREtoYkVFQ2N6YXRJSWtqdmRmTCs3TzM4UDM3aGZ4TUNOeGcvSC9JajNHVHUrcy9yZ2F2eUtSRU1XNVRnaGlvNW5xNjlkWGZNTEorc3dUUnZSMkdpM1p2amh4V0VqSE5jR3BWeW8wdlh3Sy9iT3E0cVhzOW8rR0lrT0lLT2FKZjQxWXRzMTQ5SDZmU0JsWGZmSDFjOG9mRTJCZW92VmRyVnhZdlhHMFM1U2xmc3djK0FNMWNteXBnQVRPNWhiSWpkREl6czhRK0dJc29CRFpIZ1pwc2QyV2xGSVRKWjRCNXB5dEJIWEFieG4raFQ1TE9hSnk0M0ltU2RLWldvZU5ObUJGRmdqR2JWL0Z2UEpuV3ZWVDdreUtxYmZyd3lzaVVVbTFLRyt5cWJXeEl0K2cxWGVEaWRaOTdCdzhjNmhjSkFudHQxYnJnVkxSbnEyekhpWXM2QlJ5VWlybDFuWWhPZStHbVduUlhHbERGQlFzWlhzSllyT0w2c0NWdDNRbmJSbHhsSkFFRWlSbFRNWHVWUkg2cXJ6ZnQ5R3c5ZWFLd2lXRkF3dDR2U1E4Y2RRbUh6azhFY3dPMHl6NnlQckMrSlc3dkhOSkxqZU5pb3llNjcwMEJjMThDR1B4MGNvQWRXWFMreEo0Z05WamhlanNtQWNGR3FlMnZwRmVOcGZWUlRDVG5FUTlLMisyZ042bG1IUENmWWpOSHM0bHJkMHA5WkxwMHFFTTY4VmlNY1dvVC9TejJkVFQyNU9iSGViNVpqNW9MZWRVakdOWkI1QzlpM2FMVVlOZXNFMFdFenhYUWRUaU95REVsdjU2TnV6ZXVYcUJhV0V4YXpSQUx0c01NWU0wRTZMWWwxMjA3dG5HM1VOTDZIekQzdUR2RE1ncnExM1ZBSG1OWXlTVDZSdEo2dURYMm9mMmlvR0JMUHBrK3J5a21iY3pCVHpoSHpzeklpMVg2Q2dvWTVlOFFtMVdaWVJpOEdNcTIzQU50U2V4WGk2bXhGSUNnbk8zLzBwZHFkMlFXa3hFNHdoeVR4RUtuQ2h2OXRvRkFWT2xyY0o2NTBHWmZrSm1mVGtJSnpnQzFFcXZueFNiMnNRUXFvR0MrSGsrRk9vZEhnU2pCQ0JXS0hHeUc0NzZJdlE1M2xBVStJS0REaEUzaXdFNkNGUCtyQ2pVbU91cEh5eW40WTJmREJtMmV4dW1CVkJoUmlNMy9RQ1dqTkU4YjFoMFBYckRnczJCNFg3Qno4ODVuVTFpUlhXVURKZ0RtdjB6QTN0YTRsSUcyQlQ0cEp6M1o4K0lOYXdiNmpzYURjVFZLN3FZRlFLeDduZmd2MEFOQUY0d1BsYnltZGVXWENsRkNkRnM5ajlEOGVkdTJyYUVBc2RuckxZaUZOWnBqWVJEVWFhUFhndXRsQXFrUkZOa1lYbzd3bCs1dFRRQ1ZON0EwT08xZzBBa3o2WjV0M1BUcHNGVHFQUUJJNFNENm90OUdCdGRRdmdyZlNVNmdvSEFaMG90R1Nmbk9sYWlUVTA5dzY5WHlBUzI1bmxuTDN6SElib3hyZWFVcEIvR0EyeDJsVEJVRThzb0tNZVlMc085cFRCVzhPL1BBbUFYV2VZd2hOSWxabitRbWhaY2w5M1dLTHZWMEtkb1M2UHJBeE9JT2VjUzFVQlgzK25zWEYwVHZuOTFpSzVjSk1udno2UGFJVUFLbitiazJ6TC91TW1ibnRNOFpGNGFweXlnWHBPY1ZhUEltVjZkK2ZzQ2lvZzd2eUppU0JwODduNzFRcTVuMFhseWtnSlBqSFZXYTRUeHBnRWovbUs3TFFJZm1ORnQ1a2JMOTYvWG1TYTIzOHFNSkYrR3BwSHdlYmJBd3lNZGJHVUptZStKQWlUNGZ2cWw1L0NlSjZTVXIycG5ZWE9EMmxIYjJCcUVvbVhCQUVHZ2tuK3hOVEs1eXE0KzRTWUw4cndLditDS1FjMlRHbXN1aHVzRWlpaThKRlg3akFJVy9wTmtreDNqZlVNdUpDNTQ4L25tSjN1NmxkbnhDN0xML05sZVJhSDBFV3NTcHZtWGdNcWtUa2xIbUxYamYrQ2VKMTlOZDhqUnp3V2srcWQ0cDNZak1BazV6ZG9HNkUxMFBzTmMvUDNrYUV5UVdCSzNYekFvRXdQME5ia0wvWTJHUHMzSko3WFcxYmdtdlhnbnExR3crMEIwTDlWWnJWUFhWZDk1UElTb2d5Ull3b2ZRcmNLeHdyZzdLaUZhbjUzRDhQMDZ1emg1a01RSUlkNWdYRnRPWFh0cWJ0Y3p6WUxLSXJSbE1qdW9sUEhnOS9ac1AwdkNBcVdNMGo0bS9TUFpXRW9jNzM4YUE2UlZDZTVRL29QcWtKZ0dJRE9IVllhT2FZV3IwSFlJWEZPdmI3NC96M3ZuVm5TNWJIY0QwRWx6Z0JYL0hmcTg0TmZmWkdtd1ByMGk5b1d3T3E0MCthd2RvZjNFckFoQzFaUFI3bVlYdFM0S2IrRWYyajkvRG1iZGlidTdwVjNTaEkvaDdHQWlRMXp0M3l2cDVVS25tM1lJalRkUHJIMDVoRExRSjl3RER1WmlGWWRQOXFBOGthT1RUeisxWERNZHR5RDJkTWVCTkFYZk0vSWJyV05wUExKaXFDZVZaa3MzYmQweDk3Rlp3eTd1SFRTY241UlRKeGtLaEpsbkVGTC9oUFNHRWtUZU0xVUhnWU56VGpjcXVWRmtLcmZMS1JwMEpERTc4ODJjL1MwWERDSmo3SWR3SkJGWk1uc0JudW9EOHdobi96M25qaXJaUGxuYXBpTmFSWWpOa25xSFNLdFJQOVU5Z0gyLzhjNVptUjI3eXkvWW11c3FuMGpMbVgxOUdsTkJSTHlLOG9BWkRlejRiNEhrbStGSE05b0R5UTFqTExKOFgwd2xoZEVaQnZQQ09ab2RxK29jWExEelpQR0dxUzl3VnJzcEo4NjBPcHM4aUJaOXZyTnJkeUg0ZEpZUUdNeVplaFkrdzFIQVM2c2xWZUd2QWFDQWwrZ2xqWjV2Sk81WEpsMnZoQ0h0UjNoMHdhWisySmJCTVJJakQzdUZ6eVhzRmg3S29PNGZiN2MxK2tKbVZkRFcvNTRPclhvQlJLZFRVY2piSm1JYnFyRHBvSm85aGFxQnRuWVZscWRrQkxhd2FoU3EwSGlNZEhtWkc3RDI1OW9FTUdxWjJhQlRoQkVRTEJCdDBROFk1c2hWaTZtMjdDYkk4V3h1eWhVMVVYVkFBSVkxMHFMSFJEYXZUNWJpdDdPVzNtVTBSQXZEN1FvL2EzNWVKV0NOV1VPR0ZxV2U2OEZ3eWkwRlUvMDEwRzkxdHh2QUtYTCtmMTc2RFNvV25IQnRQVmNYZ0N2ZnNLY2ZENFVaQ0tmYi9CT0lzYjlIRU5pZTNhMGwyZWIvMWxNL2JaeG15SXp1eHJLUklwK1ZmWTF6enY1OHFBNG5hdWtUSjEraG5kS2YwRkMzNDNVa3FmR0pBblExQ0Q2TmxSaVIwSGFzNjBMNzhMVmNZMDcyRXM3S09zT1lvbTlRL1NOblBEeU5hVzl5Z1BJbzhIQWNrL0hicjJtdXVycGJlTE9Sc0NwSmIvWDhoNkJsdWdPODdVY3AveUFmd2FieFFaRUtYbERuK2J5VnFXNWQ3MWpoWlpnSWRCRldrd1RlWjd2T2dJblVUZ3lGS1NKZ0IrdjRUQlpjZlBmVTYxNXVmektHQmMwOFBGa1psZTh4UDBOU1MvWFBIa0g3cm5iY0pNNXp1UTRvSjhHbDBoa2Q3ODZPZ2RTSEhmZ0tnQzE5WGFsV0RrOWhwUkc3M0xsSHdNUXJzcDNXZUo0bjV2TExCZ3dQajQ4TFdPdHVPRFUrY0d4ZUFhcG1QRlgzREFGUVQyam5taWY3eFVSRmZuUSt5akZoc3RmL1VTeTIySjlzcEhKRklZSFB5Tm5NaURyV3gvcWFvMDFDaWdFL01ORXVVY2loMUh0TU9UK0IxWVN4UjBWNnBDdDB1bG1VanZtMnpQYllHNWlNa3N5dzVVZHQyZjN0a1hVVStoczNHbG9GNDJHSTJvSXZ2aEZjNWdHVkNsMkJtSW1Cb2ZEZzlCZCt6QVE2b2J5ZnlOVldZQnRQNlJjajBjQzgzQ2VlaEoyMkRZdmIyWTFiQ2R3cXh0Y3VQSXVMR0NXdXozT24xZnZwQW9hZ0V1Nm5abDkwd2g2S3Z2bEhhUHdIMmxPSk1RaGF3cGN6cXFJUnRobkVkdC95OGxYRTVwVThWZkE3aGJqY3dudTNPNVlZSEpqNWFmS09wRnJ2d2NCNVNHSzk2ZXNkL1pMNG5lTnlUSHhkUGFBWHRBTkw5L0lKSmFVZjlsMW9KK1YrampoWERObE1PbCtOaFQ2QkVLRkRMWnM1MFRYcEJCN0Qwajl3bFhkRDdlcmVWdDlTYk5GK2MxdlFGMXBCbWg5Nzl6YXl2NmZZajVIM1NZbk9FcGNqQ2cwS3JRMTlSRVBKdjV6Nml3ajZpUVp2VUJNSWlIREJyV2lhaWRmeTJ4R0xrVmppZTc0cmZDVHFrVFBndkY1eFh2L2tuT0pCNTlSNjRCcE9UTHpJcUFlM1c3SmNVRjlkNWdVUjlTZHNsQUozcmRYUVdIQTJIS1NwK0xjay9sM3Y1SUJ4bHVoenRuSVhjLzdFaGp1elp4RHh2TEpBVVU5WUtNVEFLcHBNaVdWZFhEWnhCbkxKK294dCt6bC9HR0ZDS1NQbjZuSDVML1dzQ2d4RmhvTzY4YkJJRXlzOVI4Z2pOeDR2Tmk3S2lVaHc5cHphakQvVGxqcGVHa0g2NWs2eW9YbFhYRG9xcDdUaUFJSnpTKzFUc0R5ZEpINk5WbkJvYUpreWV6Wm9YQkhPT2hSSUZCcW5tdFJ6R1hlZjdmL2NFRDFES3gwUnhUbHE4YlA4cEgvMW5lclV1ZWd4SVYyRyt3Y0dvOXhjU0R6MGE3VXhKamxoaW9NTVVVR3JXb2RORXRKTThsbXpEcmIzS1F2OWdRUXpMVzdKcTRZU0IzRXE4emxNNWMxVk9yaEVuOGJ5ZTNzQ084eERSalNIYWVPeWFvMThoVFA4YUN0ZGVQVmNsaTdacEM0NUxLa3pubGUvQU1jbmpGdW1KRXRFTHUzZk5vRTZyRXB5aXp2TVBsOW1PUTNtNDQyVG13ZWtVbGplS1M1UGNUazYrMWpzb3lCWHNNVUp0YUZjTmttWmhPMDNwU1FiSDUyYTNnQUt6ZFBLQzNpN0tpYkRsTGZITllkaXM1R0VxcWFzRGlleENDUFRLZFh5Q2hmeFptbkI0dkZvalB3bGJmNzYrYnJleFpDcTlFTXdJMXB6YmJRclhRbWQ3cG9JQXBWejZ2cVhEQVEvcjhlQ3FsNVBBTkxZdGpUMHBsSi83Y3pZeG4zT3J1S2s1OEJGbGN4cVVtdElTM0RQbHdxWWNpVDVTU25uTU5KMXMvTUlHeGRESlFwZjlWV2RGNGFzQWV3Qzh3WkNDYzBMTUVRZVQ2a0FybEl2dWZ6R0tOdnBpbFNEWERZemhyQ3JCT25USVJpY2dzT0dtV2Z3SXVCczNNWGFRSGdaZEdsWWJBTWJNKzVSK20vZjB3cUNQQW93UklINU1LZFBVV1FQeWgwNUtKVWFXM3lmMzc1WkJ1Uy9QdFlHS2hSd29mb1ZpTFkzaVF5WnpiRWlhaytnZ0JLdUF3aHExVjhKU3NvSWZWL1I1MjdVdGtwSFNGQUp6eU91emtqalNEcUFPOXRLVE0xbVhCSEd0cWdPZEp2eElnTUJYQ2pXUXFDeVZOMjZvTnNod0QzN3I5SVhCL24vNnJQajVNMFNHY2dmeitQbmNSOWwzUjU3RFBnam5nZXBBK05EK0dHMCtHMTFGWlRzMXdPbWFScHA2SzZ2b2U2cXdMcmV4WTJQUEQ4WjFZRDdmd3l4K3NxcWxaUmJ0N2tuZitnMnhhQzdRblRCNzFSOVBoUFl1NS9COFFqQjZBR1VYMjdUQ0FIZGp2MWljQXJDR1A0RVF2SUJqNEJqN25pamMyWmpxcW02bXBVNlJaU1hCRHRYTFV2MWQ5bSs1SXVWTFB5RHdYS0w1UmYyY0hlc2JmS1o5T0V0TG1xa2NoOFIrRWZ3K0E1TVF1VlRMMkRNM2ZoU2ZiRkE5NXYzWXlScXAzZlBnK25UTmFwWFJ3YVdLOXR6c3Z5bUUyakFRangwcDVLYllJOWhHVDNUUHBNb3U3aWpWRXpOUXFrQlQrOFdKcGZjZHljVlY4QjZFRDFGZ0M5WUpMUjZnT2FueEtUY1V5MlRjUUJlMjBqamYvaE1jN3ZBWE1mR3VEaWNZR3MwZGlQT0lBWkJvWjQxMWZkTUhsUm5vYVE3bW83UmlsejM1Q3N0aDhOckUvMW4yWDBtelU0Zk9wR3JtYWkyZkJEQ0txcTQ3a1NiY0pTc3ZwS1hSdFdTb0FwdC8xd2dPclRvYmwzN1ozdHRTdFYxbXVBMTRwMFYza0xScWRVbzduSnhqYmN6RUoxSzdGMDZUbkhhRzkxTCtaR2tzZ2pvSXJGbm5BUVQwZDlXd0dTRUlmdmswOFBoSzdMU2x6L21VYlpkS29JUWdnUXdGVkp6NURBTmtjaWxoYXZBam1xSUliZkw2RWhOTzlkR1ZoVWZVa3hXU1AyVEZmeHhsckN6d3NJQ1k4ZC95clRDREFPVWwyejRTUmppWTFwaGU1UzYvdmFlVi9GN3d0MDlLcndJZ2kxZ0RkZ0RHdXpaQ0VHWFl4VHp0WnZzaDBPbFYvT3RiZGFvRklpVUNlL01rTEt5ZDFmSS9PNVRQaEU1dXFLcjJaK1RjMURRSjN4bUpBOU4xdXdoOVBnMXlGUnAwME5pd1EvSmVFMVRJdUxFVUdKdW1FeW9FN3I0MEozd3ZmSXVwYWtXemZhZFE1cXRqdlhvNGZyMm4yT05HSkdqdGVlSE5ITEdrbEM4ZXFCZk82MUVRRFBnMzNLMkZIVi9rNTRpSDY3Y0NhcnRIL05jd0x6SDJ5T3ZhWVZCa0RldHhQK0VmaEUrRTJIZ0hKVURocXgyOTAzU0R6SWdkbEx5cXRYaWx5cjlVZVRhS2RWOHdob2c2ZXgxY3YveW9MZnFjbGd0OWhvUFZHMmh2R0dnYW9iVnRjSWU1dGFkYXZ2L2RZN0I1UXlZKzROb3Z5eTlhOWg4T205TGcrN1c2ME1RRTl1MUFKajdEVE5xQUx3VlJaRkNjZWxpc1V0UXcybWJPc2hlZmZrNkY3SWVaZmdrV3pPUldMUk5ZMWRnSGRaNEtTR1RIbFV4RGpZNFIwaTZSdWtERm9ieHdKcllEMUs0bHBsRnFXMmRmdHFTSU9TVHdkUi9ISndCek9SUGtxMzZTV1N3MDJsMDcrQUdQcDRxMFo4aW9GVUxqRUlMRmhWOStFZTl1RW5iSUZRdmFIU2dNT3V4NEZLeW1XdU9aWXpOOUIyUDdNbml1Z3pDNURQZE5rQnB2cEVqR0hMcXNyTTROME4wemRRMTdNS1dEUjd2aWh4WXNzWTFJZ3dLTmQ0R2ZVN3d3S1doMk5od2V6S2hLbFA1dkwrSkZnMFpFa0hNMG13OEJPRENJTC9hUWZJOEU2K2ZyNU9ZKzFORm9QNXFDZ2tuNmthUXRCY1JsU2lFM2dNc2NpNmMxdkJQZkE5akUrSVFJemg2M1lGdUoxNENkVkNTNHRZUEozekdkOFFOcHRhQXAybXB3Sy9LekUxQWdUZXE1QVNLVFNsaThtL0N1VFZ4dlBxTEpiTUFwTWc4dk5mb0NsTGl2ekdCV3hVQkJmOXd1b0VJTldNMTdTcFdhbzhKUjhPOTZyNi9uMUtzdmQ5T3UzMVNqYTRRbFRvUU8yZUhnMFloRzVNaWVYUzNVeFowNGU5eFdUWVMvOEZQdzZDRlg0N2Z5d1VSWXdybExJdjltWFN4L09JWElNUFd4bXk0cFR0K3hOREk2YnpjdWd4VGRXMlZNOXNjdit6b3ljTWtzdDQ3Yk15WjRXQjQ4MWFSQ01jSUhqdnRPSnFrWi9DODdMNWtLR1VtcktqcmZtbldHU3hNWFdWdVZ3WTN4bWZyL0podmZZNjQzMm13NldpNGtJYms0dllWRkZRUk5lclBrNngvelNGSWRQcm9TYnZIbEtrR2FSL3hjQlNKQVd2NlJkRTZubmJ4WEdOb3FnbjM2dXZXeXlDQ1kzb3FKc0dDY3daOTNmU3JlaXdLZ3MzcjRPMUNmUTBuR0RYQmNXcmpFdmZjQ2szcDRSbXBzTTBuZ3l5b2l5dnBMcW5ZTHpQcjNxUDNPTTc0RXVTNGI5ZEJFdDdxWkd4dm9uTU0xcURacm9PVDJ2ZkJvclI4ejBYRm9hWXg2c0M4Yjd5czJ6Q0JiZUQyaXFHM0JOdUFlUERjemVWcTVYRjNKT3JqZk9RME1ENlpSeDlFMVlYRUIzbkNGNTBORmtzSTY0c0xhUWJWT2dCajY2NFBiVkdQNVpIRkdvZkJBU2ZVMHlYRXY3emJHV2dFdFAzWnRxVkFXYWtOWHV1NDBxUVZrbHZRN2JwQjBUK0RpRkRrSGI0NmU1K0d4NFUzQUd2OGdBQzk3MVZwV1RRcHJBM084WURsSVRIL3J1ZlBUMlhnMjNqQ2QxbnJIdzdzb3BGU1FsSHQ0U2syK2hqK3ZFbzFwOGNMa0xVbFpURlh3TGN1MlhYZ3d2SGUzaHM4STh2OUxZUC9xV0xlenZydlBaeDZmM3dDbTYwYnlSME9MOWFWcGg0czhSWFFHQzZONjVQMFIzNlBLU3VHYjUvcEgzWUh3QlFkUytaQmNZcXVZNFZZWlNsbVNnblFDNkxLdklXckZVNWp5M01kL0lXVlZEeVRXU0Y4WC9Ucy9ZUWxTeHNucXlyS3JlNXdJQXJOWnEzeVhCbld4eSs4dEIyZTc2cUltbHJOanQxeWl0TGREQkR6akIxMW9oRElKUG9lZHhJa2E2S3drYWNWRW51ZVBJSmNmNEpoYWIzMTJISTRkNXNMMkxUOWhDNFFiY3FBQ0c3K0dsVGZZNURUM2hjM1RvVU0rUGNZdlpyNElVbVNUWHBhc1VPenJpNFJlZ3ZFZGxPMVQxK29wd014Z2hSZk03dHVqL1ZCd0I4OFN4dlgrUFVCSjhMb2tHZDFqaFE4NzhmYS9aWTZISVB5QnpIRzVCaWxZam9mUEFvTmxzRTJIVFhpWG5GU1F1dG1CQ1RpQXNvdU8yb1NMbnNnM1M4ZHBmM3VVNmlaUjN2Q3o3cjlvdWliaXlEL0lUL0ZGOFVFV2lURU9FaS96eDFZR1V1b1ZtSmtNdi9iK2dXS0JGRkljbG1IUGQvSWh6UDlrdzk0am5DT2c4NkdxNWxnV0xTQUZzU0k2UG9qa0tIN050djh5QnVEOG1Hay9OamZZZGJEd05tWlNNWTVCYWZ1Y2pneUFKdzUyb1FNVnV5UVBwSlVSaXNPa0RpT1FWRmZydjlGSGd0aWxzV2tvbWF5dHJpMmx5cDV0blBhZStDV3JaSHQrbE5Zd0NCTE03Q1ZYcFFoeGw0WVJvL3JpM1k4V2wyVTIzbHJoelVQVzMrQ1BLa2R4WlR4MUdldmhSckFYajJjTG11RkNydElTeWI3MEMrTzN1Q0UzVzY4Mk8xTFdSYlNIQzJHWmpZSkY4aGgrTmQ0MldndVhXSDNKSDEvNmI3RFY5dFpQVlo2K0ozcmVSZUNqd3hYRHRWV1UrRXhvN1EvUmkxNDJBaDNUdFpTSGx0T3ZUL05IS0ZwRHNuQmNyNEJnbmJqR0dicnRQK2hJNFgvRkxCcUVUbk1pUitpRm1OeFNGcWtad3hLMkVjNWN5ZlhCamdwR2s3R001U1pxQnY0UkpjR0hyRHd0ZUtIV2NqZzlSKzBHQXVaKyt1cDdkWENBeGp5eGlTV2FNa1pLNlg4Wm93c2JMTWd5ZitBcEhxNEYrMFN6QzFyTmJGeEdYUTBEakRXZ0FtZWQ0c2oyTnAzNHRGaU9jTms5R2ErY2IyM0dlSFp1V1EwMW1WVW1waVJWTmFmT3hBNkZ5TS84c2UwcGQwbzBaZXp0cWhRb3VZSFMxSjg1REd5a2N3QjUwdHJDZFROU0JBQ3pYc0o3bWl0NlJpNE1LR0tnNWp2djFlVUxYR29DYmQweTV6M3FDdHErOHJ1b3Nza3BLZlNSSko3MGFxYlM2ZG1aU29oR0RDYkVreXpPN2lXMDdhc3haZWs2L1czUW1jK3NZY0RFZzhlTkl4QUp4OFBnZXRKTmZVQzlNYmc5NzRLUnJEOUkrVFdhc0VqSE12TjcrWHNCQ0NOYjRBOGVqNS8xNVAvY1Z4VEx1dzhTK25RK2JPSWhnY1NmNGhNOVlxZ1k3R1cwMVYxdklqbzh5b0VIZFhHQnBIelo3VnlFaHo2eVRES3M2dk4rRVNBYXdrbVNIQ296YVhmOHozOGkxLzFXVS9CckNxNFBKbVMwZWZvNkJ3SGdKRSs1UldqWVY0NFFiMlNjb3ovd3lBKy9sSXBmbXFpQmI3bjVzbkRaWUpzdGpxdjgycHVqZEVEK1dWaVVyV1N0dWNJMlFaWHllTVhwaVpCZ2c1V1pXN3g2YThOSWdsUFpnWVpQc0VQWElmOXp1TmZ5QWozY3Nod2dLQytJb3dUQ2I5eHZXcFA4MG83NTltVUh4Zk5QcWJwQnAxNENIU2w2b2Ird1dxL0xiT2phUjdxY3dqMU1TTzN2RjEwT0gwMFo1UGlDNmFVS2J0RDE5WDhCTjVZU0dWV1JwT0p4RXFsWm1LNTNjbytYOGl4S09QTTNFMC94cTRKRHk1K0htM2l1aVRlRjVtYzhrdFFQVXhWd2pBSFFUY29qdHIyeXZTdGkvOXFYSlg0aGptbE9HRk1CSGE5VkgrbkdDVi9wS3dZS2ttdGZaZWJoTUp1V3dmZDhFVnRvR3cvelNLYXFCYnE2VVpDdXU5eEY3NTY2Z3E5WFFJR3RaczRFY0pPVzVMZ2h3UVRSS0puSUJ2RStIRk5ZbkxpK09RTzBFSWVRN2FyWHdMTzlJQ2lQK3BIMjM1RG1Ld0NzWWNEZFZWRUhlOTNKbVpLakVpVy91TjQ1OVFjUnJMR1UwT0E0WDhmVStCREdQM3pHdFgrWTRVL285cUprT05md3I2bGpMbDNsazhCZXg4QkJ1bW5QY2YzVE4wNU1RZEZFWUIrU2VkVXMrS0Z2UGJZc2V3RDZkWlF3VWp1TS9LRzlGUzJ5L0tvOEpqelUrem5ORXpWVzVXRk5JWkZRa3gyYUVNdm9NNnBxME43TzdFQSt6NC9ENlRncTEyK1QvQjNhak1rNDc5UWV2SndtaFE1SHFXR0tnQ21UQVFhNElSaTQrN0lmRmhQUXd3WUVZcWZtUHhFNkw3K0FhUkdieGlPYTJybVFmRnVhQlllV0U0a013QkdpaHJLdGFQeDI1QTkwcXRqODJPcno5dkZnS2dxV1JGNE5EaXliYW0za0lrSzZHUGVPK2xwYk9vZDJ2eXdadE9iWmUyZEU2NndFM3ZWWThmQ3F5RUlsWHFJNUZBZ1JRQ0hNdDNHZUE1eFdFUnczbmlqcTBncDdvcnJHUzJpRjFwd1VZN3M0SUJ3ZHZJS2s1bWtQWjZ1VDZ6dWFLaVpkZ01LY1JHYlUxci9BeHhvYUNNNWU0Ry80eDhxcE96WENnNGpzbkJjaElyZ3dKRWlnbUhQTzhFL2xsVVRFc3FVdWl5UXA1NzJ2RFpNL0Y3Zyt4K1NheFJBQWlyOC9TT3Y4VEFucjFWMm5nNDMyWnluYnRuQzBRT2R1aEJxb2FKRmQ4d0NMdDdVVmltMTBQVE1YU2lYRE9RdzIyVlJocVFVclgwR01uU0VLSlk4MTVBelU0ZjhZNFQ0T3ROTTJ5NWpqeWwwOERGZXcrYWZWVGpvdlA1Qm9JU1UyRTJHQWM2Nkh4YUh3cUhPeVdvRUFYMHBreTN4bjI2T3VLM3ZqcGJJRERaczBPOHpjRGc1Q0k5c0N4bmZVdG1RRXJXc05Qcjg4K3NGYVRvemxIRUE5UVAwT3ZTRm1qblk0RTFNQWl4cC9rNytZNVFYRTRwQUZNVVNtejlzTXhuU1lSVm8wSXU0ajUwVllTWG1aMGI2bDF4b01ESG9FN0J3NmwyWVl2UFoxVXNWSzBLRzhsNUZuQXlxTG81RWNlSjVQZjg5YnA1L3MybE1mSVdHNmdPMnZSNWZFTWhLM2k2RkRhUXpRRVpxOHJXK3JmS2FBQnZ6akUxbGM2eEN3NXBSZ2QzaVdXLzJmYUlaUW43NldDcEJzQjZ0bm5GQXRvOERqN0pubkNLbXdEQ05iWXJUbEFyQXJpQlpMVStNZlVqNlBOR3JVRG03VTVJV1A4QmZjYWdKcFlGaFUyOXRGK1ZWZnU5WjhkK1RnUDllOFpQL3dWSkFsZDJlSThJejFHR3p4SEVUa00vYTVwbWMvODd2SThaRnRGYjRsa1k0Y3NwNDNmNEZDZ01najJMRVJvbGJZa3RSSmQ0dmowb1ozUjRDNVdSYjZiZWZJMEV4b3lwWXliMXBxZnRNL1kwa29HZmlpMkFCSEVSV21HZ29IRGxwSGRyTkJ0cElIYWVsa1BsdUNGZS9OSThlOXFrc1JpZHY1RTNCazhjK0RGUklndUJ0VDVscnRybURmd2F6SitHNlFDektZd0Ntdzlrak9nRkdlTzNFMDlMLzl5MlZTcmdlRThPWjJlYm1wN0gwemFqdE1rQUNsWDR2eTNNZzhiUVFXeTk0SHE5QXl4dzBFMTdaa1JUbTMyblpRWVUrSFNscmdRajFBSGV5a05URlpPK0M3VnVrTjhjeWxPRzM4NGsxTXNXaXRFanBXMDA4QjZybXIwMG5MT1dnMW1NdzA4L0tsQ0hnZ1hteVd3Vmx4ajdyZ1EzMzgxS1NxaUsxNytvSDFXUDF1VEJ0U2lvdEwwbzRJdDNrMWpyaVZ2VXZmUTBiczRvWUhhWnZJd3BYVEtGOVZxY0lLOWptWHRMeU90dCs3WTdKMDd6aGJ4aHdSTnFDRHpOTGhDYlFFWVVBSkc5SXM4SW5Xa2sySmFOWkJzNEVVT0ZBUVdtQU8vNmxJV1dqNnFBaVZTVG1sU1JLWHZsNWVLYmVyeVl4ZGVuYkxSVERWL3YyK08zcmhLV1RuU0Nja0tXS0NPV0xXcHJJMGVUamJzdUxKdEFUNmtMWjRkT1F0a25rcG9vUy8rdlVmbkU5ODE5UzVhODZETEQ1WW4yenZNQ2dEY1JCSllVQTFZajBCOThlSzJXN056Q21zRTNONHNxZHJJMU91VldLY25wMUpkWStvK0FpVjJmSjBrREtaYWwyQ1dkc1BabmtvYzZLWEtQbk1lNUtqZDFXWHY1WUNwMXgvb2g2TEgxNHpLcGlFbFdybVVKTUtoQno2amM1RU1IRGRsbXEwaDQvc0JyM2V5RzdUNkx4MStZT1pVTVcyVWJCUUZGM2IxZHVja2FNcXgrY015SHVUc1I1dW12d0NGYVJLNkVKOVRqK0FlcDZyRlF4bHUyeVBxYjNHQUVvZm45S2RQelNpME8rY3k0bVpLb3IzYXpEK21kOXBYaGpOZXd4Z2lBVFNzVkR2RzByaG5KR05UYzE1RU9SZXRvVXBFZFVidmx5djJxUm9wa3ZKRURiTk9ubjFNd01ScE5WWlAwejRBREZodkcxc2NuaS9lZUlvWU52SStGdmY2cFVRcEkxRGdGaktmY3JTckdON2o4QUNwR0tUT2ZqdE9pYUpqMk5jeFUvNENTamV6ZElLZThBbk5HRjc3S1ZGRmlKQVo1bk12UmFSWXVQZzNkS1dYS1hkcTV2STdIajhIVFpSdVo0VnBVT3RUMDkrQVk2RGJqam9rUzFqQUdKcEcweGVVaTJnL3c4cXJJd0lsRjJoajVpZXdCQTVZam83N09odjk1UUhqN0l2SXlhemJBWDJkbmhHdVJwbXpsTlM4SEN4TjMwUU1EekUzVHIwSEIxVWVFTEVvQmY0dUVHd0YyZW1VN2lodW0wRU5TMEVNRGQ3U0tzS0FBSzNqMUF6YndXd0JsQnFYTmlieUFWQ3dxL2ltdmRvdzRWUWRuU3p2alhZaVozQkxHOXNoT0RDVjNDZEYyYU1md1pPcXpVV0YyT05rb21BTVpyRkRkT3duNWNQL09NcFYzSFpTd3VjeHpXY3RVczZGNzVRcExaVEtvbW1jbmVabzk0YnBhbWprMGFEbVBlL1Q3ek0ydXhPQUFrYjJyVkt6eDU1eUdYbFk5NDUyTE01T2txcSthNllFaUNUVkVTZ2N1OTB3ZGRmMVEwWTJoMVNSdFZJell1UHQ5Nmc3blhTSDZMdzVJeXg5YzJUcy9lWnNwUEhPZThZMlF4Mnd0TUNjNWQzbGNrK2ZlaTM2ZUFMUXlEcWFBdTIyRjFReEpMM0NvaSt1TEN4djd3N215QmRlck84bFdwRlUyNjRmcVJkMjNLYlhrNTBlUWw5eXlvbnBMbEwzVHZ4RTgxaGZBYjloY2pndXpNWU9oTHlqMm11cWtuSkRrRDk1RTJ2MWZUWm5WcDhmWEtyUkNsKzRhUXgzOHVWWEhrSlNjZFNwdnhuUWhqT0xydlF5MVpwZ3h5Mzg5WWlwL3pCL1BUc2o0djFpOUs1clRsQ2k1RDZqQ2pqd2lnbEphV2lISHFPQytDTVlxa3VhSjNlVHRQUXMvZVVOYWx3M002WjVlNnlFclJTSUFoVWJBeHo1WTlCMGdHdVhsVEcrNTVDWEE3cENVOUEzMnEvYlpRNjk1UlUrNE5QajFiUFAvZ00veW9FSTdvQ2M5cHJPY0Fmb3RxTEphbUFYcTNJdndRb3M0SW1HWDc5UGxkOEo5aHhvcHduV2FTa3dWc1djTkNQakdtcHpVL2F0YS9VdU91a3lyaEFqSGxURGF2cm5WeDl1d2p0Z0ZiZStIK0VYQWlDc20yRERacHJsR0xXenhwNjNWcEtxWXNBUEVIQlNHWGpmYmJVOWZSMGtjeTVCNEtpcUQ2bTFiM3I4eEw5Nkg4bzJIVS9xVHdmYU8zTUh3WjE4OE52L0J4b1hzdUpqZFVQQjVPakFnMnlsQzIyVm5vSzNYbStuaGphMlZtU25TbVN2T3hlNUl2bFhiZ0F3aTlaaGREajRJTWlueWxHTDJWZmpIWmZCL3prYWZNOTVUV0J3bmw4MCtiTFFETWphcGhqaFR4V2J1YUZ5NmRIeVIvUE1vWHlXbXhQNnl1K2RubDh3VkJ6ajIxc3JILzd2TEFhWWp6QnQ2T29OenJYcGQxTWZ3V0FYVWE0MUVkZk5kL3M3c3JnUnYvVUtkb1U4MVhjcWpCUmswRXJ2VGYwNFJLTW45MjU2TDhTWncyZnpob09NcmhTaVQvUnZtR1NzRmZrS2tYRUZRcloxRWd1eVE0Wk9zUHN6cUNQUzFoMWhGNVVhMmhqbXpRakRoNzFFRmw0d2NHK2c4citTTnZocVd4akMvSHEvWjZReHN2Qys4UkZTeUxSSGQ5aDRGeDhDRzE3Q0pCQittVXdKSEtlY2NRb2ZCYmp2L1E1NnFpMW1KSlQ2OUFwYzdrR0lNbGlXVW1jYndTWW85MVZkOERpTmE2d1E5Y3dGNUo3QUR1U3FiS3o5M2QxOFp1aC9tOXBIZnhpQUxEajQxb29GNUJrMHRBanVTQTg1ZXRqU1dDZWxENFRNUS9FakQ0Ym1lWDhzZWdQdmswUnNuN1hZR2xvWmRqNXU0eU5VYWhKLytraCtZRkduTEFQby9TdDIzeWZ1aGpiNm5VZ3BKNHNlRUNSL2ZaU3hyTWJQTzFIY09PSzlQaUJtcUVWRGl4Ukxjekw5M2RNa21nbVc5SXBQd042SEp2YXVRdEplY2JqRGNKTThOcnNuL0xPeGt4WEcvSHRYeExScXdqY3FMQjR5QXFsanU4WncrSW1oakIrVnFqK1puay9UQmdKa0xIMWlQU05WdVhBSUk5aVNRdDRPcC9KVkJKNFVRanB1OFZLa3ZWMDRQVy9Bb2Jyc25iY0Q0SnluODV0b0lDODE0b3V4cnVlOThxc0tzd2Z1eHErNXBRUXpnV21ob0I5NDBIZHhyWHlpdnVKeGt6TGdPdEhobTVIZnlGR3FnNzIwU3lOWUhvQmw5b05qcjVEcDdPa0NIaEVSY1RhK0F2Z3JNQWZSWlQ5SzBEWlp6NGlLTVhjUjN0ZklOeXpNanp6Z2RwVy9wa3RPM1Z3ZTVWd0VLUUhSSVE3cnZMZEVXbVVWM3IvNk55UWQ4cFptVm1BOHFWbjFNbU9OTGh6WHNwUE9CbXZvaHU0SGc1U2ZTbDU1dVJrM2J5UWlwMTM4end5M3E0UzVGQ2FyWEVXT3NBc3I1dW5pNCt5TVpmS3k4dWx2YVJzUUR5d2FxNDFjMXRVMTJUNm5XbUcxUHdVeHFiK29OTUFhMDFPamV6djZ4ZTZYc2h0Ym9LSHRxTEVxL2NPaFY2THJrOVB5b1NITWR2dGd1cmpCbmpMSk0rbUNBbnR5cDhEeTZUZ3ZGKzVhd3RuYWM1NjBlUmlBZzJWUU9SSWRncEp2UzVGaFJYdDdhMGp4SDhKUjNEWW1oYUhKRGFibkh5YjdJMXk4WXlCM0plWjJhb2JSUEtUdDNZT2t5Zzd1am9UWTRyUVFCSm4rWkIrbDhubGpYMmZWS0tzZitUNlRuRHF1SmVrd1FMOFBaSkc4VVVEVE5ONmVkUjh2cEkzL0dMY0srYTA2dlVTVDh6ZzJydE12bVMzb2locC95ZkRMZkozK0RRTUtmc2hjK3pPeklWeGc3SWptQ2NSUjc2Mmc2bkNrUlJWbmprdUdXS3NaL0NlZ3BYNkg0U201OWNxV0ZvSE55aytnRkFFZTAxeEk1T29JL1JnQWQyaUV0bHlCNWE5QzUwc2JhcnhlaHJQTHJPL1lMbjJKbHhGTG9NVFdGNTV4QzQrbWV1NTN3Ymc2aW5kUXlWMm9lOFRUOW85K3JUZllVWi9ic2dzSXJOc0tMaEoxZVhaTDFtQXlSd1Bvc25hSW5BMjhydFhPKzAvNzdhQUZTNklyUGt4N0psT2RwV1c1QWxING5CTHNPaGRqSUdvdUM4bjNLb1JiVUUvZ3UwT2UzVjZyVjJ6OWN0SUw1d0Y4SkcwMDVMc3E4L2l5UmJCZldEb1I3QVM4S2RZTHBOSTNtekZkRGxYeTR2WUwwSnJ4S0VKUUdBSitEOC8zUE5pbTNET0Q3V3BONXRRTFFiS0xVOERjeXdGaG02OWRHYmdYSW1wQXFDa0FXNFEvTGx1UnlZV1FNVklDSHV5NjlNMFVCK1hzaElSa0JYWTBkejBFL3lIbDNibDdJRzdVRFByOW9WbE56MVhqcjBYbDVuanVLVkNnTG1QMXVlK25kZ202dmV5cXBTTGUxZDB0T2hMZkFpeHpOMVVtaVJ4eWZMeVdTU2pDVGFKY1JmeXRCL2w2Q1h1aDYvVWdQZVVWYzc0T01Bb1RCTlllOUFSS1ZVbDhhVkRERi9IRHF0djd1V1dyd2tJWE0yVmYzNmROTFlKZ2ttRnRVeldQOFZRTXJoKzFqVHhCUk9JcXBKc1U1N0Ezbzk3NFJobVFwWHZxeFVOa0RMb2xBS01ISW5kanlEcmkvbUNYeXZkOTd4ZUJnclF3ME51MmhWbmg0Tk9QUGtPQ0UwWDJOU2lXUVFJTlYvVGxOWkRFNEc4VW9oNkFJbUd1VjlsM253YXZNTEFXZERKTjJBNGJIZ2MzNFU3VExzVGdabnpSNm9DVjBXK3MxcXpjSFBmUjhWUElEeld2dGtmQS9OOGFKd2JNcktxbjV4TDZ6czE5c04xdkZ2OUxZTzFvc08vQXZuUU9BWGNBb0Q3TzlZOHRoempLdU1tdmdyUHBiMWE3S0JkMzFyQlVia1hpNTlHQmkySkhucjhFdlFlTE9hS08vMTFtb1Izck14NTBlNHp1emN3ZFZrODlCRDJxb3dMelJiM0hPdEV2cXVvTU1FTVZ0NTBQNE5QNjFENldmVTlQaVE1NVdidEZjQXpGOXhRRVNJNGVtN292UkxKNDJ2RUxaM1JId0dDbzFSYk9aY3piNzQwZ0IyKzVURE93cmxMVnFSVmMzaloyT2xzcVNvcGNPRW5aQ1lGa2JpcVNKdmg0VVgzSVkySWpCMlcvRDhTREtJSDlCZ1Z3VEx1M0ZoU1NCSzJkcldYNE9aTDRRL0c1LzJTZ0x2UnZUMGZzSk5UdFpha1A1WU9PVmt4bm5td0Y3UlBUemd3K2JaMmVuaW1XRERMMjhSNVVyOGttYm1ubEFPK2tHS1J4NWN5RG9DSGpJR1RwL3d1R3VJdUV2ak1pS0RSWFlDS1BXVmdBZGFUcWRjQmxGSnUyblFVbFdhZFA5M0NRbzBRV3B4UndUU2FjZTFRRWJRNnZQbHN0ak9pRFA3UEZpejQrdXhxeHBucjRUNHlaT09hNVFPQVozTHJvVHRkR3NyNjhYUFVDSWovcXMrRkpQaFI2UFNvOE1YdnlkdDJXREFhaExOaGFpWk5TRW04bG50TGl0N0VtRFJiKytacm1GT3UwUFJodXY3Y3Fnb1hISkRxR1FzcmxMRjlQbmkrc0cwdkMwUVNQditIeFp6ZWNwajRwY2VUcmxyMzd3ZGRjUCswU0pxSTA4VkppWm1iZ3hUSnFMZEVEdFczcXZHd1dWT1MveGltU0VkczBpRjF4bm5QdjhhL1hIYTFzS0s1Y1E2Z1BOOUJFc2UxYzRvaHBrT3lQcGl2QXZSVmlCemM3aHByYkpCQVNXYXQzbTRrdXU5ZHBUR2VLREg1TVBBclFaRDVRdC9idUhtelRmS3dpcjZHVXNUZGRuRndIa1A4RU8zZllrWGlTT1J0UkNKMHBLNTdDR3Y4QmlRUjg5NHh5K2VnVU11dmVGK3BSN0dBbUJxNkFncmRhV0M5cGZGTjlWeXhEK09rOExpZ3NmdjFKZWdVbDgyNTYvMlhxUnlxWWs1cHh6WllGVDRTWjhZRXlmYTQ3ZFBuUXZmZjZBUUQ2U3l3WDFYSzZ4QVVYMUR2bU1hUVd1MFNaYWpGcExuOEluK2YvYU1BNGh5VnJwVW9UWWY4WGdyNkFmSm9ndHhIeEo3dFRHZDBydFJMWHJadXpaQ0R1ZmhpVCtRc0NtVlo1NWx5S2MrU1BmcHhzOUFjSkpBZE5TTEY4QTRLQUFVcWxWaGxoNWRxMVBaeklibkIxQVJnelprL1VOR2xjTGpBYUxqWlRBQmRqenJNbDIzOURjR0ltRmFLNFFSWFRpZW1UYTI2K28yQ1JMQVNnSDFlbkVZaE1nWlhXL3hTb0JsS1BMOG8xZS96WFhFZGpXS2NPVjdVWnlNMHc0MzROZkIyMXBKSVBKLzlNKy9iOWJYcG5Db2ZlWkxOQ3RFdGRBM2NSWVdJS1h5NWVFMklTdmdkVXhNMllma1dxRmwzK2Z5RDkya0dhVCtKZms4SDVWS2Y0R2pKbm9iakEzQ2ZGL3VSblhNWmtKdFNsQWtseXpGZVBsUDliZGVhK3czcVhEdkpQcWZWQ3BHUC9OTGM0VXdpSnpHYmozNDZORUJKNGZzVFZmN3dhQXZqQThxdFkxckNGOVI5bnNKb2MzUWk3RjlNbCt5UzBSaGNSYXdFQUtSdUNGVWNzWWt4SWVjbUxIYUJEb1JRYzBqb3d5UUpmb2djWlN5MGNxTWdaNmdmQ2p5UWRHU1RmS3lZaXRCMUZiTERSVUpLM1dhN2RieWRRbEszbEZFWUd1OTNiaVQ4WGMzWU1Td1Z6S2cvODZ3NStSTUtJK2t1dFRySFNXcTVZWVcrd2NEYzI4b09yb3RNK0pndEY3cHMwRVdxclNaRmN2TFpjQjRrMUpvSEZYL0xEUVo3SVlPMTV3dzlmTGJVNUpiZWt6d3VqdjVrMXJLa0NLK3E0OHVXTXhnQys1MHA0OWthMWgxVWVjdEgyS1NhQ25YVncwajRZeU14M0ZndldTT2VPNnQyUTczQjdDbW5TOVhpdWFIZk9SMUxEYzU2NWduSTRLUDJHdmI2bG1OTDVjWjZaN3pCZTJQQ2dlZlpINVlQTUxaL0c2WFNKdjhWN0FqOERiWFRjdW9tL044b04wSTdaUTBUK2lMcUo2bEdjbTZGNnNldGU0VlJQNGdHTUtPK3g5RloybWxqdCtlUzE1eFpCZ0U4ekxrZGZSTUxGajBGdGxzbndiNVBYMUhmcC8zbjlzdDhucXczNU82WDVPdEhEK3IwVnA4bnN0bnllN09mSjZwTmZKMzQvUDJzNEJlL2F2NzlidnYybWUvV3gvaDl2L3dmWXdCNXM1UDBmTUg3UC9yWWQwR1l5YUxlWTgzVGJqVmtZRUNwMTNVMStVbDIxdlRnQjVnM2hockxHbm5wK1lZKzhQbG5Kb3RaZkxYUzZEVWlFMUNqc3VZWkljS2hVVTliOHZpZWtPWnZhc3orSTJVZUtPeHhnWjd0czBoQ29Iekg1OE93SUE5L3c0T3BlS1pKTjF2a2FZY2VheHgyNUR4UTBrSW8wT1dhN3ZHbGJrTGtLazd6UDlDb3VJanFkMnVtdUF3RjkyUzhNSmVSbEdONWhIVHY0Y0xLeDZ0KzQvTmxBMFhiV2RoeXorZ2ZnWERMenZaMGEraDZISUdEdmRSKzJpeUlpYnJyYzIvYVFSNFNYRDBhdVljdmNPcE5xbUdlTmw1RGlRa3FSN05GOUhPSW0zM1ZscGcwUTRjTkQ5dWJ0eDVCbzRtaC9veEtKV0lPVlJ6NHJFZ0pMU3ZJWDYyNkFmRkx0V2hrbk5neEZlQzhoa1BNVXcrRlgvbDlmaEp5a2lZM295Sk9KNEppVjkzajFmRUZhTlQ3ZjhYTGlWOTFZcHMycktPb0Y2VEc1SEp0WklEVHZKc29vZk1ZZzg2Z2FzdkVhbWFFeCs3RGtJZjlvaE85S3k2d0Q4NjJWMkN0MzVGV2V6d1JndzdFdWlIdzczMkIrWmhYRWtYc0ZDaVBzbDhUMXJPOTUwN2loOW1KVWx6RmFIbENXYUR4Z1k3NjcyM29FYmF5bmlwZmVLakYyZFRhY3FaZmpmWlZXekg0M0srd1ZuaFdnWENhYlVYY01vT2cxYUMzR1pVS1ErbG9udGdWL1QzNi9OWW5OdGxMYVlacmVJMTRsbEhjYXZueStqaUl1RUNnUUhhK2Fpamd6bGRnckJZTGI4Sys1cGdXOVpjV2s5SFFRakx1WWZ0VExCdzl2ZGhPQ01BT3A5ZVEvYkY2cHlINmVVUkQ1bEcrc3NLMEZKVS9INm83YmZEaUlpRUJwTDlEZ0hjK2NnTE02UzFENkZsMmNnV0pGekhZMXgwaVhNazVNdGh3Tmpxa0VBVStXTTlvczhEOVFZNiswbldYWlIvUE82bFNhS1ZjRnkrTnBmM3J3Q2pDOTVQamNneWpBbFFpRWVKWWlJSVZBc2VoaG5NTEh3R3RCSGVCQ0NLOThJZ1NrU0VxcWdMekZubnVlb3Q4U1pra2g2eCsvT2xLWlpCc1NOU0FxbzZDVU90UTJlQTZCMWZ3aVdSMVJyc1pieGFDOTdHdDdQY3A3dE9rQmVWQm5zTTg0WnBxU0dKZG9KU2Z1K2QzeXFjZTFVbG5RSytnd3NhV1ZDN1EwMXBLRENjYUZEL2N4ZVdQOUwxLzZtRVdldnc5NG1yTlVoYmpLNHVIb1pqR2ZKS2svQjBVeDE0TTgxT0plbDNTcjNxMklUN25ta1lERlpLMnFZbFc3S1k0bS9Dd0Z1V2FRRUV0VEd1VDBkaDVTQnJJaURuWXNoTGdRZExYdkR3RllzTzlGY1dKTFVoUVhuM3MrVjlaVnlTRXlsV042KzdtaUZuSnQvakVqVHJacFRnSWIwSis0cjFKb25TK2k3T013aEVyWDZacE1VS05HazdmOXJjcHhMRXFvY2hqKytHeTV0YmtmdExFVDR3V2FHK2o0SDRzcCtWL2dVSlp3NDZyQTBOWVRCUlZPcmVDZFFlTWdTLzhQcm5VeFUwMVEzYmFQMzEwNlZDYnVhb0RIUFViSW5TODM4TmR3NkpHeGhyRjN5OW1HVTArdGtWSTZ3bFVQb0srbldDajk1emowMjMyMFdSYUUxNFVjL3pGMk1mRVN4WFB6M2xzSTNtZThMV3l0VU5nVmN1NE5BUTIycnFCQ095S3lTbjhIcnVxNGZ2dnFGU2w1bFZEaXJ3Z01aYWJJdHA1VlUrQitMbmJmd203ZDVJUU5lTm5QR0E2Rnd4c2tvU2hwbGhMRkhrUkFiQnN3NzJTVTdSYkY0Uy9yZFdpOCtFMHFERHYxWGdhTXVwRVJTRkcvOVUxZmhwTGN1TUxHSDQrcDZEYWpvaFIzbTFuQ3NqbVUyU3UyRUZaTWZHa2RaOVpUdVZnanpXVWFFRmpHWTcyekczbzRRUHhCaDNmRysxYjRQN0c2RWVjOWllODYzSXJsQ0tPY1VJRzA3VlNubWhOVXJhYVptekMrWjd5eGkvZGVZTGZ5R3l2OEJHNVZEZDgxTi9zUUdmcDQwNTdUd3lENExpdEdqS3R3b3p2d2xMaStEZGhwRk9FU2todW9nYTBnclo4WGRLUEdhWmQ3NDUxYXltNkNBUUFEREtxdGNuRHZ6YmYvZWtkUEgwRzFkd00wTGZ2VGtJTnZSeGZpazZyTVpMaFN2cXp1VFlnM3YyOW0xcTFsYnRmVUp6QnljTllwRjhid1NtQjFTeUFUWVc4RGdSTVVRU1Yvamk0dHJxWXN5N2UzVEtQbEh6VjEybFJWa2dzYU5wSGIrV3VEb3p5T0I5cEpjdXBLUE1GUFd3NnVDYzJOQ0dJV2NHaE1QUmVBcHF4eGFSN2VyYmdmdW1HemhYc29PMUY2aUwwVW5wM09IZFVPSFpoSE0rck9aQ3FFc3VHbEE0TWkxTFZZTC81dDc3bnJZZGdGVXliUmt6TnRPbzFUTjczK21uZ05nTEtkRmF3ZGp5YTZNRXRQWTZ5eU5MWEhMMmM5b3RLS1NGRUJCRGZIdzUwaFFadURpZXI3ZEZRQ1pJUkdsS29zMTF1Q0I3NjM0SVNBVlJ6cDRjTEh1YXFkZ2VZV3U5QzdLSm5qYVR6TVNEZTF1UXlzd09PVTdzUGJSL3NDL0JxVDJqWm94dzdVbjFJYzRoR3paYWsvMHFMTWxjY1R4MHA5ajEwc3FJbDhZNFh5V0xrcTlTdjhCekh1ZFBSeFQ5dmFCMkszOFNNQUEzb2tnUE0vUjZsb3p0a0FkendQMVRHaFc0VENHdUdKN0dxeTl5T2Fyd3lnbjBiSGpqNnhuZ0tTODVVZVVWKzBiejlCT2dVYkdvVEJRMHl3SUcrdDloSGloc05jdVZmNlp6R2pueEFJZTJ6c0tVb3k5dnFTTXFNUjd5cUN2dUhXa1BSZ2tJam5hbGpWdmNKY2JEd2k5enJPNzBlRHFaTm40bTNzWnBBbERxV1hWMVBEQkdub2Q4dkJ3bHBtTCtxVll2SUZ2N0c3bmR4bHpXYTV6STlKYndBakRRaW9xSG9ZcThYc1dSVy9mN0M1eGxVTHM2ZnJYaEhndDVweTBXaEU2dHBiY01NSEpnVzBqNkRDV0JSOEYxYjdaVmcxMjlaMnd5a1oxRktER1hYZWhmcXdyenNCOUFMSmxHK21LdTVSMVNKZDFLNlRRQzArSmxYZ2RLYXArMWpXdEs0QVVkbFR2WHBxRHhlVk5pOG9URHA2WHEyKzRmcFV4dHByQUE5ckVRNXliM2JuWkVyMG1rUUZSblAwdGFpcnMvRFBWTWdMQ3Y4aXlWZndFdmNGenMwVHVxaExlTjdndXlsMnFsOWRZNEhTVjNOWC9vajNqM0hVbk1qeFZ0bHByc1psQUszeVVwTVNSR3JocWNjNWZRY1hmenN5bDIyOVhtVW1CMjB6ZGZLTmFaR1QwdzdhVlJZN1l6YjlTVnZKY0daSVdoSGV2RTNwbUdGNE4vZXRjdlVyVkVMcjUwaWdxNExoZldQNWhJbUk5L1dWU2lIMGNRUVhMclQ3Q1lIWkVYbEI5YUZ0VVdIT2NFNTMzajRBejhTVGlzR0FWUTZOV2dFRWE1TXhUVzZ6QXZtTy9ZdzBObEFpRmRieGQ0dFNzeVg5VzY1YncrN0xndUdycWZyMWF6WVJkUHlTcld1c09rN2JtWmNDb085WUlxNzErK0JoYk5RQ2dJaWNDZUhyUHdTTWxqSGtlaGpJaUFDTzBDNHhncGk5bklKWVZiVWxnRFJKcHU5eXhxUHh2KzdjU3hnbGxDUGFnelRmSTVJcFpDWGx2VHU0OXFQeWlxZ0RyUXJpWmhCb3g1bnNXV002VlhSanRoMFkwd0JYNFYxV20xbzJ3SVhQcVpSK2QzMTI3YlFCelZDRkhrSnMzcDljek1jTllxa3RRbXNiZ1QzYTYyZ2NnblliWXY3VmVhS3l6RWZJNnhxSVRGK0g3QzAvTWhWUm1LTnIxMi91S1dWTXNBSFJqdW1RVWltQVZQb0R6cVEvQjhzR1FVaHFNcGcwSXg5S0ZxUVFLcUZxWGZvTVNaTEY4MSttS2JTOUxIWUtVRVFqV1IxTkhYRnAzai9OOTdwSndlaFhiVWhYTWRHeVdkWC9la0ZnWE1Da2hTc2x5TVhSMVgvdVhMdTRCWnNWKzVCZnM0Y1YwazVwL1k5c3VMZ3lyUGRjd2tYWWF2bHVoanVLV00xaFhFV3JEak5zSlRkRzhDN3Uyd2RiN2tCY0FObFdyUlVqSWpKWGNNeEZacDB5djVHblFKcGpLaHNkQXVxOGZIcy9FcVphRnlxaWlpWFJmZ1R5cm55UXNVVDZWRHhlRmNyRXpjZ1NkNlFoNFh5YWsrTEhXRVJiTGNYZExTUHNEN2twci9xVkRiN09jQjRmTXlMZVpHR0J5Z1RNTFZ6UDdseklCS2VWVmhrcFZMUVJWcitrYlQ1bDVnR2pMM082Qjcydno2RHZvZWFQdGh5dnVSeXlEU2k1OUVlMHcvak1RYTJNaFByY3JIakRLTGJiSld5YXRnWDkyTTFNV3FLNTVoeE84VHBwdDY3N2lWSWI1d0htMThrOTdSRVZlZldTNXJ3NXVQSGR4dXJLdmhBTG5GWmlEVmlKTXdkSmdnNlhnbnFPdW5UK0l5QVBhMFRaNnhXRGJQZXJoeGdqdUlDZzE3TlorM1hzN2ZmM3daTE1qeGVxVWxPbnNJT3UrQ3k4TDMwcFF2V0FjWThBUHFQUEhjdERIOTQ1M3Z5eFRQcWNlUGRGSHQvVVFLN3ZCUkVqWFVwL2cyVzlETVJUYVBxRE9lcUoxZTd5eXFpakNPU2lna0Rsc1NEZDhTdGU3Y3dOd0dVL2MwdTZWSU5JZEwzVVByV2pNNkRJWVhhYkZ2VTZyNWxOWGJmcFpORnZCdHlhTWhGelp2dmliQUhqSzJQaU5lU09jSk1KaGNHZ0IzaGZLdjlQcjNTc0h1SEpxMFczVjRSZnA3TEZEUXVzb0dsa3VoVGtpYXE1cHNlV3VZSGg2aGd3cThSTG44aGRSZExHZWZGZXdYM1ZyYmM0YzN1U2s1U0V3Y3ZWRlU3elBkd1BJQUlzQjlTQklVY3NUYzZMa1BUcmlZUTVMM25ZNTF2bGNuOXkwTHZ4dmNjOGl2ZmJDMHUvVGNINmVmaVFXbzZXd1ZBSk83ekJvbCtRSG9LTFR1S3pia2hYclBTaHVRSTdNbkZXeXlRbG0rNDNWM2hXQ0E4YkRNMWprMmFKUGxQd2FwUW1sUlpUWm00ZVgrODZxeThNZWhOeGx1NTd0WXJyTzN5eE9EZXN3NnBjM3FGTlpaR09NWEsvY05IdG4wM0M0dDViOThkblV5eEovVXpZbzlUWXV0K0hyeERZRHZlaE03SU5zSkVZL3hkWTd5R3BFVFh0c0JQaklEYzRka1kvdzFuY2pXdit5M1JUQm5Wb29GY2ppY0pEMlZiYnA5RFlqTmZFc1lyZFVnSzZ0OFg2L2VhWEVWdTA1VWxBMjJQeG9QOTB2R083SDdJSHJEeHJ6UFZpeGtGRVIrYjFDTE0wT0wwRE5YY3UrQ09XcDBQQVMvQ3BLcVZ4RDRuWU1XMk9yeWFYYVBNdTFoM0ZDSzMxTSt3WDBBRXhtdmtrcnRKZVY2NnMveWlhMXZBMTQySFFTY1RqWmUvQnQ0UndyVHpLb0FPNGlhWlhoYzhsaVFrQnNXZHU1czAzZlVJcW9GdEE3VDBBUGRqMXc1d1lia1VqZm9EWExHUnQvQ2VRZW5PSGdVMm90bGpkWmtkbDZWQVdvdWo5S0FMRGJUd1AvTTNqbXZ1ais0S3U4ZE5tWGtjaUJKYit5UjFrZ2ducCtZZFRDMjBlMDREVVc0dVNNNlJRRGo4WlFxSjBSZXdwcW5GUk41UDh1N1F5V2lZVlU4YSs2bnZGc0pFa2tZOWtqdStJMWpVeU9mZ09tYTdZWDJFZVJGRnVCaXpDUUtpQW1PT0E2aUN4L2NiUERyZ0p4UjlJRXB0dDBwK1VCUHp0NGZuMFJKeGdJcmJkQ3hpQ0RlY3VNcU54U0V2MnN6cFJHRWxYTWV2cFByQTNmcGptUVo4bnhlRmk1NzJnVVlDUWZMNUc4SnFwajlUUFJPdnNQV1UrNzdHZTJnSUwzRTQvOERON0lhVzBTUjV3WXhFZ1dIV1lnNi94amlpRG1tNnFnS0Z6cit0cnNteDVucHl5NVh5YWhVQk5MVjRDYmJsNnpoUjl3MFEwTGpmL2ZXb1JMMVZJNXpYSWZHZmpaY25KVjRTTnFXNFF6UlBSUDlOckR6NnVPOXQ5V1Z3WnlHa2JLZ0tuN21wNmdUUG9LMm9KUEIxdWkxOTR5MGlNOUZoMGFaTXYvU2hQMVNJWkZyUlBVUGxURG84ZWl0UFd5K2tDQlowU1ZGNHRIYmlzNC93eVpoK2lUYkk4WklOVi9JY1VUQjRlRjNSU3QvZDYrMGlvOEpKam1RME52WGV6NVlPYlVHMFlkYVZWZUtJaEJsTnJEbXAxSlBJWE5rK1FxcHpzTTlKQ0N5RFAvQk8wZVFSdnNQaUVlczhlZGZNd2RqM0k3Wmh5Mk9DS3pBaW5kUHBDcnA5empaK05DalV3V1JhMVhXR2U4R3RNMGl3RWhJbGwzMDRwdWhIUEZaQ1V3c1M5d3RhVElaOHEwTnZUbWpWU3VqeEZGQjRZMTNEWjhPR0hMQ0Y3MmFGUW51VG5iYjFlNFlXY0E0VGFoeWdCZitzc1ZiRlQwdGtFcmh5aUdlSTVRcGtpbTFvUGhFdll2VDRRNFJxUExaZ3Jwb2FhcnAzWDZlM29iQWhsaGwwZXJmV0Y3WnRJRzFqYVZQSDlxV0dnSHN4bVZRQWdkaThkOGVwQW9jM3BkaVMyajg1TGtQNUdFRlN6VGkwRTNvRHd2ZndvdlZkWGdLNmtmeUpmSGQrd2gveWQvdXJJMGYwYm9scFNGMHl5aGU5Sy9ZY1lyVit1SVB0eDBmU2gvZC9LcWlSUllPSEg3R2pTWTNiWFpxODlRUzE0UFE2dGxtanIzWVRseFJHSzdIRmxWVWNmK0tYcHh0Vko1NmU0ekdpOWFnY0llQndiZzhzdTdqaldYVjEydXpMQ0FWRXpKUlo5bFk5QklaOUs2REowaDZHNGZVTW9RaWR2cEZaMG1mT3VBaFBzTzBFb0V4bUFUb1QrMWRBSGErVzROTkJCRXdqa0lnOTQ1dHNZc2ZwOWZEWE5hay8zYVhKQ0x2dFhGcjJ3alB3OXUvZDRUZkZWMVVJU3ZKSnFmbklXVU4zc0IyV0VUaGdhbVNJZTNGS2Mzclg2amJwRm5GVExyMStGMHBjQndpOS90cUJtSUQ0SjRGSkxYdjF0MGhqMVN5cysxdlAyVDVnbVJSNHVWQjZqSzNMQ0lIYUxJU0pUS1lZTlJqZGJsY1N1UW13cVdObWRLbVlkakEveXo4NVFSNHkvUVRpdnVnakZEMmNvZUNGN216SmZwSTVlM0hSTTAwRW4yclhmNWlKVEdzODJ3UldQL0hYRU1iR016ZXNVWEdPS2gxdmw2UFBkc0ZETkVvOUtYbEpzK0UvRnBVM0ZaeTA0dW5XeS8zM1VsVHZzYnN0dEhXcHVUOG5EL3lGcGw5VE5ENTk1ZnFKR01XSmhhNEp5cEY1OG1jcmNIdUdEVnZueVRsTzYxc0pPV2RRaE1hdUNOVW5GS3J3QVZVcFZtQW1oMW01YTNPZGFxS0t6aEIyMUpXa0IyODZoS25CMHRDcXAyallFWGlmb25LYno4eDF2SzZIeHJVWGVFN1cwYUc4YS9iUE1xcW14WjZtdnh3cHZ3ejNpSnZKdUI3aFpzR1ZkN0NObCtwQ01xSVBEL2ZlR2pmMUh2OTNnOFlqUkNNUjZ3TkpUL2Y3MDZJWHVZWmRlR254OTl1QWtMeTk4TkljUitCZlcrWUFobi9rWXdERG0rQnNpNm1oSXZPV3JxWmU3azRodEhwbFI1SU9pRW4zZU5ZZUtXNWdyV3JqMGcwMW1DcEJOMFd0S2lyd3Y3V3VrWlFpMy96Q1hVcjkvMzhNT3RpUlB1Nnhrc2hmWWRaaVNwT28wY29BanBERHdhTnVJdW1QRkU4N3VkM1hsTkhGNEl5a21GY1FOMWlaaS9rVXJQYWZxaVE0UUVIUDhtUnIwMXFDOWVUY0djbEdKZ1dEVk9yUjFVMzgyUm1zZGxHZVdIaWU2bWxoL1F6SGtwc1B0QW5FYllrY210K1VNSmhWanRRQjZZZFMxM0pMRTVIeXdHWC8xSlhabXlEZTN2dXh5dDJEQUh0UmdkbEhjQStsWmk0c1UwQXdrRUFkckNvRE1FRVFBeDJVUzRxaFNmUGRoaDdCTWkwS0hmazNmUnJDYnhVcjMvcmVWUWM1WEtLR0ZSK1pNKzQ3RXVRUGhDeUNkZThaOGsrUm93SnRwVkxUQm8rVjVVRklHMmhWTTZGSG1nYUlNZnMwK054KzZhQzR0cUJzTkZxUXlsb2RKRm52Ri9YSnZUaWc2emZIRys4bDdBOXdhTDh2TmhoYUZmYXhWdmFtMnFBSFFIZmR2Vmx5TnNXNXQwaGRxc0lxK01OZ2NGd3ByN0pySDBiM0xwZGh3VWZGUGJ6MFRMNm5WYWthSlBydTVzd2hJbFRXVS9UcWM4TnNWV1RSckF4OUhvY2pYeE1rd3FndEg0bmkzMWUvZzhzQW1GLzIzd0xrVzVqT0hLSFdYb05ROFJTblBraW1JMUdnUzlFVjdvc3RMWHBNNG1wYnMwTC9JWTRmNjR4RzBRcGRIUVhVZ0Z6R3ZGMWVhZDhZQXlUejdDL24yK0FRT3dJQ3BJMlN3UTF1VlRHc2pUQnlWUkZDcDlxVHV6US9mYjZ5bVI0VEVlVWlwcFZUSDJzL0c4K0FIdHhXd25xNS9DVFRici93L0xuS1c1NnhwaDFURXQ0U0VDK2pLTUwxVkprRnE2b05wcnZ1UHBwSWMzZnBYNnhFcUdqMkZDcHpMa0RGWmFzSk1MUXR0ajlEQkw4eTZWUk1IYlY3d3ZEZnBMSFRtYkZ4VktBWmoyL2VwWUVGYmJ2QWpGVnN5K0JDNmd4WFNJbTE0OHFGNmhKblFSMlVqeFNlZldzcGoyVEV4MGpHRythZlhJdWQzNXNiSzVWZC93cVovekR3Sk5ubmR5UGJobCtmbTlHKytqYlVqeDI3MGFZdksyNDdUWWtuUzJpdGF4TEswQlFvSmxLOGZablBaVE5HUDRoaldaL25vNElmdlhMRHB1RzRKMDhaQXN6VWNuSU1PTlRRV05HRlN5WjZScFREUnJIS3JBV2pYT3B0aVJTNWJIekE2TjcvM0Y2Mmp1Rmo4clhnR01hU0pYc2FGNjlxTXBta0hsSHN2V1g4LzhyaHVmd29JVy9uRGVhQlBqek5SdURTdWkvR1VOVDlhekZvenkra0tESGQwUHErcFhTTUxxNkxReU11c29GTkpNbHFZVXpzSFdQWW5QVFhaSUpqL1VVNGFxeFoxc2EzRWdQbitqMWF0dlVIM1NuaWpUZzl0TFpqUk14MzNBR3Z5VnQ3aGY4dWtsU25NWmZ0Mm41RlhqN2hFR0xTa0pFdldiWWRUUW8vTGtWU2s4dU1jNzNnMHpmdEVBcGl6TEREOGtTdFgydU5idVoza3U2dVZ2N01JYXN6amdYTUVXMDJzZHF1MktZU0tIVmtBUUdBanluUXYwUURnZnd6Y3pLQUcvSFpwVmt6QmROMWNQYXE5TU9GY1RLK0Q2YjlTVHM4dEZCMGNJdXNMbWRBVXdVeXN6RTYzL2NCSzdhZHVEOFZCQldNeW9RSCtzNks5blR3S0Y1c21nQTFpUndPUE5NQmpFL0pVUk5xUXNteElVZDJkRWVyb3Y1Y1pRNWFhMVJFN2FyVlVCZngzTDMxazJRaWZJK21Gd21TcnM2OGdmaVU5dFZ3R202SFZrc24zenlRYUtiakMyVGFtZVQ5RERqK1d4am40cmR5ZjhuaG9VczA0aEJlSU5EU1d6U0tiZURlMHpwUTg4WmI5ZmZ3R3lBRzRsaGJuMVdFUVpjdEhROFhRbjZsRmwvbUlPcjVRa1VYcGd3T1g0RG1QU3oybVg4YmxDYkdLNmExZGhzU0V4TGwvU1VWUjFUdHlGaDk4NTBxdGdyaGVMS2NicXpaVmR3dEk4SThNbUhvSS96eHVxOWREV3dweVpnTzdWdjlVc0t4MUF0aDZ5a1pCdkhnSVFBTGNqQi95SnZudDBud1dra0hiai9LZnRhN0tXUU1yVEZOczJYNmVhMXVOMXlvVFh5eTlpTE5mN3hlTjQ4dFd6dmhLblE4MUhjVFlWY2ZyVVpZazI0bGhlUTNRcERrZFVkU3BicUxQNXZaMlVjS2h1R1lWNGEvWnc3WlAxKzA3M2MwenRpNGZKQVJhdUJ2NmRTN0F0Q1k5Vkx3NU50OFo0NW5ZY0dYMG51QkpMTVR5VEp4Qlo0dnE3cDNyWVU5cUVWcUZVWUpMOTJlUzVoNnFvQ3lyQzR3QXF5blNMTmRMTjNGTGVQVU93MnVNWUJUTWF4ZW9ITndtaEVKbFNOOFIrSE9STjRzSWxZcHdQc2h2NjZLVHRoMjE4QTVYU092VWpoV0cwM2tJUkRKZGpMc1A5TzF0YzZqRDJTWEtaZy9xL0pIQzUyRDAvZ0VCTmJjYTI3SFR3N0NFYXhGVHpqeFRENFNDN3FtbXhGbXZtWU9aenFOWW1EUGgyZDNzRmRuenhSdTB1QkdIK2tGM2hZeE5LUzh2TmtTdHBSZ1NpWFhuUDROcU1LUTlwMEh1M2d5T2xJUEUyaVJiak9VZ2owaFVyeFJCZ05KYTI5UEhCL21KRm81U1BSOXpLS1lpK0FRajYxdDZYMEZkbWZpUVRxemFybVVzVk5SaUdjd2NKZE9ycFMrV21TVmk3cXl1YzJTaFhWN3V1dk1MQmlmWFZKUnVoU1pwVER3NHpZbG5iaHFFQjB5a1U5d2w2bmgyTEwyeHNoWVpNOGVqQ0hvZWNkTnFiNVpHSjVuMnMzZ1MrWmFxNVI0Yncxa0VUcXpybndHUklTbnV4SUE1dTZuMXdZV3JvYjhNTTB3SVYyY1llL3FYcmRQN2ZuSmdlS2paTERLNXFSNXlLU2U5RUlnSjVnb1hmb0M4OGlWaldDN2lvSWpkWWVmVnI5R2V3TnZNSGdXWlIvV3puOHZuWWRlZzQvMjhLQ1kzS1ZlS09BaWRuQmxqWE02L2pyVVMxWlFGekxCSzhXd3NqcXJFYWx0NVVheU9nOUh6eDNRQjhNZXdtUWVUL2tHUWRyOUEwOU14YmtFT25ZTjN0UGx2eDREb2RsdGlwYmxtbjVESm5LUjMvMWIwcXBVd3ZJcGtEekExZmQweXFxdGFYcDg4WHgwc3hNbVREbWZtTFI2QXJTWUpwUXJ2SU1GSDRqT05tb0tBVG03NC9zQ1dzczkzdkVkQkw4alExb3Zia3FzK3pKTkNHSTBaejk1OXJ4eE9NaCszM1E5UmJYY2VFS1p1YnhWWG9SRFNpOFZZTi9SN29mMExmZHV3VkhoQ00rODBFRUxsbUluVTQ5dzI4b1czdU5nNnFzdUxLaVNGL3l5MlVIZ3dLR0duaUpDb3lLVDV6UWwwSi84amQ3MHJrOHBYRGV1NDNmelBIMGdTZ29qU0FvSCt5a1hJcTQvc3laaFQrK29xQWRHMnJlcEtQclJvZGthZVN3NStxaWlXZkFOb1Q3WTJCM0JlU2VhRmYxVVA0VHRKTXhZaVU3aFdzYjhxUkRvMEVzem9JSWZJUE54clF3dDg1a2phZTR2aWVlbkZpYTUrZGRCVkE5NVozRVQwMlNNSUZENlVOTEVCMDcxaU1yS1RkOThMa0lhc2NWb2tuTzgwUWZNNGJpK09MWUpISnNTN3REOSs1aDE2aHFkeUdwVG9WT2xQU2pzRFczdkY3Z1NNSXIzSGZKSGovU09sdXZzSWtUeEIrZ1dDVTljMzVhVWJiRlJtOTc1MHpMVTNHam92dy9YVUxaQjBQTitzMUNGVW9lYTgrZlFxL1Btd05mMW5QS1h4RFBuVzQ1bndJL3RXRE1jR2RtRnJnbnVLTzFvN1N1V1BHWHFIazF1T2NEYW9rZnBwT1FhdENqcHpQUkhGTXp6TjVwZ01aaDlMU2UwQllORFhoKzhpR0E2eGxUZFZzb2NpNWszQ3NnMVgwbWNvUVFaVEQyUU1JZzIvS3pOa2ZoTEZZQ3RFWnhPdFZqT01iam5PVmMzV0V2bC8yK05lcXhRN210TW04YkxyUXJhTWZHbVhUeVhTZ3JnanJXVERGdVloaXVOaER4NENVcnpPYk56RnVsV3NNdmo1L0RZMGh3R1ZJS2ZWTW5QK3FMYU5LbGJ0Z0dSSVdYNjdQVitOeTZLOXRla1JzWVFRem4rb3lDcGNTa05OOE45L1VqRWdoUkNTdGpiaE1CQWIvUldGOXBFdEJOaHlyTXZzQ2l3WlBPQ0c3Nm95RUJSOTBGU1QyYTgyMEZTd2FnTlNDQnpZY014cS9WQkFYV2ZWZEtDeFQxUndvOWdEK255dTZtVjEzK2dRNk83L2RMQmZweWhqYzZTc3E4ZkhhSFdKSGJLeUNrUVJ5N2JMV1dMTUQrczFIVmpKdGJpMVdwZUs3L2c0S01ja25zdXdBRnAyYmVyWFpyR0VrRFdodUFJOVVDa1lyem5YcXU4NWRYRUZzTldSVFFwdFZFeWl2RW5JL2g3YlFEYTAyM2NDSTJWODBDWWFSSTdBS3ExK2hNMkJNMzcvWis3d09JVVkxTDhxaWZMNFFqaFJDbTBZOXdvUTZGTzFzeXRIbVNkMDEzLzloVTZsanN1Rk9uTVE1QVU5bjlIbUkwVk1RY1ZKV3I0Q3lxcXpCZXJVKzlncVdQUloyMUVxeUlOSDhJRS9lUE1ySUxvNjJWNmRFcFR0U0FxUS8xZDRiZ0tFVGZUekRsZjA3ZFl4TzBXTk1JV05icjFsdlh4OUo5V2hXSzNLSnFkbzFyaWJ2cXJjam5yMGJBbzR6K01nN0tUYTVadFloL01peWJrQXZjRXpDSGV0VlpTb2psQkh6dlVJTWVJcnF5T2o1MDBYVGZtdEpGYjlHbDJ4N29QUzd2MHFlZEVidTVMV2RYQUhmNmkyN0x2Y3hrT1NMVHpVYjlSN25yRkpDVXBEb1hGeVVQeGdCUm9yYXlraW8zanZWY2dFcDJhMm1mNDFyWFpRRE5pV1BWV1FUVm44dE42OENMTG01Mi9pb1M3dk9sWURlWXI2VGxpb1dGVGdSakdoNlI4TXZzWEVHRWtGZUpncDcyWnpoaE4rbE5zN2lLVHFrK0g3RkRkU1B2TFpWL0FJNXNvSml1cGl1c2xtY2hGZ281R3I3SVpxcmpqUDNGZHFnNFRnck1OcUEzbHdWcWcxL2ltRXJGMWZEREJvemJ6NXd6UnlNc3NJakMzK3VjSmowM3YxR0EwbVduTHY0M0t5Zlo2TEtBcm5ydlpxMkZQUWJ2SUo2VzJHZ0RCbmFoWXJhZ1ladDZpVm56cHlWVXZaaW9XVnpHVXY3MjhmNGNtWlNETFFhOVgrVlBNc01PbXFKbExPcTU0WjIxdzA3ZGRTOHNPMzJQT0gwbVovdmZwczFZQ1MrajJKV2MxaFZHUzBBNFdmc05vS2ZTRFV0ajF4MWdOWllDTG1OWE9IUHNoOTFUd3E1MTZpR2tvT3JHcER1ZEZVVVN5WVRVV3JZenFYNDhBRFJSWVViWS9HTFpGY0pWc3FaTU9Ec2w5M3QwYXQzbnArMDJFRTZWZzN0TUhDYUszdVd3LytTYnliQUMyWXM2dzhmeitBNWRVT2lUSzg0end4RkhDMWk3Q0FxWXdaUmtyMEZuTzJCT3RBZHQyUy9KM1lIRWQ2V0ZpYkd1VHNEQ3NaV2F3cDFoVzZ0U3ZvWStMQ3Q1dDltV3UyaFR4TVFDckpLcFJOTnVzNEt5NEFOZjhwVThmWktVMlRscjZxWlA2TjRGanIwSEM5dUVYWmgxQ0R1Q01SZ2h4TTdBeVNhWXhkTWZSdGFaZHhSYnlrQWtCVmRYVXFKN0cwWkYwVTBYcEg2ZTNRSDZ5bUlUdVlBZUFLQitlNUhIczYxZWliOHNzU0VDcGQ1Y3IxNnozdkdrRlFUYzFHZG1uUmc4OTUwOVovQ0k0Q1VTTEpFS0J6Y3hiRkoxRlVPRUZrN09ma3NZNU10b3FUR05oVGRML2xnL3h3M2ZYVGUvWHRZODliSHFkbTgxc0hQU01DMGJsVmZwNXd3NndRRHNCRjB5algyV08ydmJkeTFtN0JQdE1EMThzaWx3VVhTT29YdW9ENE9ON3JSeUhBeEp6Ni9YNUtvY2tEeXl2ZGgxUUpBa1BzNGFpc3hSQ3BwOTR3dVNpWDVIUDl4dlViclRLUGpkaS9HMUJPMDRjd210YVZ5aGxmUDlMWGlkRHV0YkQ2SFQyZU5YWkFzWTQveUJDcDhUT0tTRjVyWUN2NStlUnAzajVITHhJd3JRVCtabEtmRy9Cc3VkekpPQ1BUTzI0WURWTUV2Y3dGbHlqeWxqMVQ2OHhqYjhTbmc1bTRDa3JnMnZnSjJ0TmM2TlZNck1wOS92ODR4WHVmcE5uWTIvQVIvS3NVNE1mYlZJMzRGVGNUV3ZzSkxERDdoMjlpTWlkNTljVldYbU14WDZXUURNR2c0eDFXbW9KN3drQWhBTE9zbTZnbE9nMDlqQ1p4Z2t6UUZIZEZvc2xtVkRSWkJwaDhORmt6UE4zY0dVVUNWK3I0ZXN2T1FFRkJ3aVJrdk93bUNveGhTT1dxNjFyQnkvczFyUDNWenF2dkJseHprNlBINy93WDVJUk55eWs4N1B2L1VXVkpSSE43dTN3Skp4NjFFcUxSVW43d1FPWUQ0aVlwRWljVmdGTC9IZmQzRFpHYXJYblZrcUNZVENhamFoYURCV2JkVUJiNW1kTTJPbkhuZUVJN3Y4RFNwdXlGN1hwYkJaM3ZjL1JycG9rVUJ5aEhTR3RmZTNYNjd4SDR2a3NMN0FTeng1OGhzcDk3UUxaMHlIOGFDRkJBMTVLbWY2S2gySVdYMnROVDBURVRxMVB2Wm1hdW02QWFwT05nN00ydWdzVkhwbzhDQlpwSndFL2plMnZJNlRZMlg0cUZ3Qk8wN0VGZnAyTVdiUjlMbkxIQVNjY3ppTTU4d2l4UWxGU2ptYThGTmRsN1QrRmxFYUhRcy9OT3F1M0NLUE1oMFhjdHpRVS8vUTRpbjdlalNYQ1hqMGhLSVhFaUJvY3VpdkNDV1lIUUJXajVyeXM5WlphbkdXNEZ3dXBJRnIyLzlzcFdKRDM4TXN4R3BXUi9jRTk1KzNzNUFiaFp2Z1hHdGk4ajlMYmptYUt0b0pyc0V1STBSNllxVXlHZXlVMFplWTRST1k2b2oxZWNKWlhpWERYSkc3cEVKQXdmVUVVOEYySFVxWkdvV1hScmRvRXhJcnpSNG85ZUc1V1NwWmhnQWpJYTIwVWZaS1NZZzRWRmUvb1Q2UFR4L3dVTlRpLzhWYkIrRVNaUEk3OTNIdHFlQU5MamtoWDZvckZYM1p2aUZLeE5FbzJZNzVSZkVHbWo5ZnJnYzFJcFhtcnpkWmV5anpWb3hmZUI5YWdra3pyZUZLeCtZY05lbzBrcTZnaU1CSTNRV2djY0R0eGZkeGM4ZW9OWU0zc3hjYjVSNE54M3QvdXNVWFZuOUpoN2tMUXdEcTM3eVluUTlxNzFuTW5tdGpBbE5kM216c05XZTRPUnNZekpncTNtQzJkWWtxNHA0WWNodTA1clljbUpxMWVLbjlCRmlmcTNPOXROVnlNcHpIVEVMYWg0UlI3Unl6K3BHdlRyaVV6azdDV1VzNTRhd3FGbWFsZ0Z0bnAvd1g2bGRJd0ZhSFl3UjlWdnpNbTVtNDRNOEYvSGhOR2RDWHovZVVOTkFRVDNKOHdCL1BYL1NsM25nZnY5NERydkJlNlBnNXJ2US9jQW1VOGlaNWZoMEFlS29zMklDaEF4YjFPMmVUQklYS0ZDQ21PdjlDNDdRWXowdXdDTmZKZEFIWVBra0xPWXJ4TkY2czdNbVhCZjlHMVdIQ1paN3YzdmpVWndaUlJiZVByU1o2TUNQMmgxT0pvU0lydzVzZzVQTWttZkhRRjQzQTNMY08wanNPWXdQcE1xWUgrR3o2TStvWVZMZWJNeEhsR0dwbERFUWRyWm9NRkUwZEZaeXNxMkJUN3ZkOU5sS3RreWxnNnVFVVhiTXN1Y3ozZHFSMXRlcktpN1ZOQ2J0N0VRUnhXWFptcXNJVEREWEdyRUNyN04zakdkN0RRSXpRYXh4K2g3eHpaQWdMcHlBOEV1enhKdjJEM1hwTmFFZkh0MnBIbEpaWW5tc3FyQ2VWSmg3UEhGMzdjZ2J4U0VidElqaEtvckVBNlY0SE81bUttZzgxTEhtcmFxSWhrVVUySHhTQllIaFVTSzdqK05aczlzUHB1b2lKMkgrY09LY2FyQTV5NGozK2QrY2ZTb1hjL0tGMHhGakZnQ1Q2VS9TdHRzMm1FMkozUU5OTTM5bDFuQmhJTStyTHJjdEdMODA5V0VxZ3BIblhTMktidE9PUVdsUm5MUU9Mc0k2cktIWVdVU3dzMlFBVEI2SVdQRUFXUzRQSTU2c1VRaHZoYWdkS2N0Z1Y3djNSWjluaWlFNWZtOGUvSU5Qeko1T1lKb3RsbnYxamNSOGZWWFU1OWltR0xJWkxQcmRLa2RJMDlXV1B2eVhjMWN6a0xXSjYzNnJrNzlOQ2NkN3FQMTc4ejdoOUR2dVk0eU9oNVIyMTRZZmFQOG1JQjBwNTlRMkVZeGhwZmQydlM2NDVrNW1mNVIxcFpkM2hWU1g3VUNxS2l0YmxhTkZPR3dVc3RjNnB2WENML2xoS0d2ZU85dTZDK3NDZmVGZWFhaE9LcHRseUZNUkx2SXBPUk1QQTdraHpmSW9ucWxLbHNmOHVhM3lFYXBIRk1qeG5LU0NtdU5seGRTaW5ibnNKOWJadnFhaDg0M1ZHMlFyZ3lwZGR3WUxpNDU5U0NsR1hQdUNGYzBRSUpTSUx3K1UzQmUrQnkvOHRrTkY0QlJ5d0tVK2pORVcvNXhaMXY5d3A0dG1adjRMeThQYm9zSkI2YXVWSk12YlU4MG9yY2lML0kxV3l2emhOdU9RZFJuNm9KMkg1REtMVTdLUFdOb0tEbmpEenprZ1pvMWprMXFuK1FvVEp1TVk3S0VLUHcyeXArb1RycE9UaTMvSUlYanJBTDFIeWF0Z3A3VUxDWXlIQ1E5SFBReThwZTBjWVdlbUpOanFObXhRNjhmUG96YzJQcFpCcW4rcHY3L2IwZFNEa3ljSk1YRGNkaGdQaU9sVWYwYTRHTDdpYkVLdnJySHlLSjZUMjBoUnY3OTR2S2k4VDEyb1BrbUdYSjM1VjA1d3U1bWEvYnRmbFRaNEpmVFhQQmFWVEQwQStiRGRyVkpMMUpJVU04Q0JKKzF5V2tvanQrd1cveGpzZ3l1YlA4YjVEMENWZkFBcU9tTWsyWVB2d3BWeDVLd3F0WGhqbkZmZHczSjZ2Y0tIK1lkZkNva2J3MjNBUzdNNUZKUTZrTjFmalBuN28yUTF6QkwxeXBmQVdoTUNUOFRxUVA0K3R5WnpHL0dRakNraWdzanBKeUNuTzVHbEpPcWtUSWtTT2ZLTDltWlpPSXRxYW5ySmRob085VUJrWEJJL1lTamlYcXpvOXNtNFRCOG9Pb00yVjU1Rk5lSFhxVk9sQ0VBQ2F6bWwrUm1GTEJxY2dJbjJkZVU3YS81MEdVUUtheU1BbXBVMHluVWx5bEEzYmJ5Nk1oMGp4YXpFWTJKMlBhVzBRUWUwRjNmVkNWS2xNYjNubC9YQk5MNHB6dkd3bUFQLzIwaDRmanRpWlVnOEE1OG5GSEZnMmJDTExITWRqdFh6TE4weGhpaXl3bHUvQkhqcEhoZEkwMjRNZHVFNHR2SkdaejYwRldMV0FoV2RtR1ZHVC9lbHl6YlNpUTJjRmx4QjB1eklLL1hWMkNmRnF6WjUrU3ZVVTBRdWJDcG5wczFtck1QV1Y2R1o1elNxbWZRdmtlQSt1Wm50VUMyWDZNYmduSFg2YjFQNHpWQXJZd3V1eVQ4aEo5M1dSNHZLaDJGOHJHWEJFdVRIZnB0dStPaFRmQ2xVaDYvektiaWRUZ1ZUSlZqM0x5ek90WkdVRHZZZ3Y5cTl5d3d0NmtzSitSWDdDTURPTUJzRTdrYks3SWdvSUpFbDB0dHR1RHphaWNTZW9hUndMUVhlTEZ1TU84c1hBSlF0ei9hQWgvNWVTL1B3dkhLOTVlMnZjNFFXVi9LbDhsRXloRGE3dTRjcUlUSG9IazRkbGM4QTBsWHV2OVBCUzRiY1VOU1lFNEpRczFGZVNOSVJxbjBXbVJvUzJINDdPWkpBVmIvYUNRVlg0Z3RpaWttT0NmRG8rQlkwQlBoeVorMjlHeVQ3N21oYlhqaEFNcENTSC9PVWxZMkIwVHdOQTdvckZlcmRtYXg3aXFzcS9yQTVFT3p2aFF3M3FyTlBXZXBpcFBoNzNQdGEySWRwaTMvZVFIS3FkTktjVGhRTHdINGZFcVgwcHVpOCs2ZHI1a0tONVFTaHo0VHFNSnhXcGRmSWRqU2M2WkVWRXE5MWt4NXBiSlh6T3lXekpKS1BYOFpMMGNrN1Z4Vy9PcTFVbDQ2dlVEV3poTXFmSWRkaUFXeU1wQ0xyZEVpMkZNU1picDRMbGd0WTlYazFEWlRkY0pBQS9QL1AwM0R3cnd5SWdpQjVoaTJTUko3Wm05T0QycURMZFFzMUFVUjY0UGoycHZHWXFrMzdTL1I0NXhCaUo3c1dqUkVnb3B5N2JJSmRldnJrZjh3R1A1amE3ZE9TTGZoREhtbDVqN3R0c0ZmeUNEWHo1N216eXEzS3JXNVRKRGkzRjh4K21pZS9DNXdWZ1E1SXQ1dkhaOHFhajJ1M1YraXdiSjc4aFhMU2Z4MW5yS1lNM1BqNFFPQklTYVFaYnltVjVBVGdUMEhCOEZGT2Z0Ull4ZW1uamxWWWF5TWVCVzVPekJQZ2NiK09tcmdjZHVGMlFDM01vTGpiaE1LTEpPakJITXFVZFFGTzUyNTZUbHdpZWhhdHdrWmRxeGNaQzlOakZCMEVodFF2MEh1K3hFR3NWSTVWcm1lOW1Mb3pZRE00Y3FFVUZ2eGZtcjA1K0pXUFFFSkVqdS9mTXVTQTdIb0pLamM0UWVYOExKcFVhSVNKRXMyZ0RQVkJPOXVIV25mU3ZsM3AxV2ZPL1krcVFDMGNDWXFXSExRUDFhN1g3c2hHRk9VUjVuSExDZ3RLdDYrT0IrQkU1bnR2cDhzajlCWE9CVVRjdWIyLzNoRXBSTkJ3c2ZWQmc0eEg3R3dvMEVMTEhZTG03aGpXQmtyYUtXZG1zTnpwM08vYklxckE1YzcvYVpDSEU4YytIQXFyZ1JGcWZRdlgzY20yeHE3Q2NCbU1iZWplTmVtY2NwUnFzemMzeUovcjczRlFkczJGSW9UK3plNEtXRHc3OWdRZkkxcUVkUDhUZThXMWNXMStTR2RxOExRUk9vNFNLd0ZVSEZaTm9NVi9KL281L0NDUDhQVjBPL1QvUkl5c1NFRTdzeDZzYTRVQ0M4dnpmN29VejE1STcxZWVXNXp2Y2s5dVZRZGpJY0Y3VlBRRjBTVTByVHdTSDg4clpVNEVJSDBrQ1U1ZXZjL3JoYkdaNy9HVXlDckFsK1hzLzlTdEFQWUl1VHovSXZIR0FtamRGWGlpZ2FXQmdQVDF0VVRGMjM5aDJ1ZmswVU9LM3NldFJJR2J0eEJ4QmVNd2oxMkVJdVJiMGRpeThJdll3cVU4dzd3Zk9XL1o2enI2MkZnb0E0VHBtTmdBR1Y4Yitob2pZQUxJRlNjZUFJOTRJcThNTHN6OGQxUGlSS3NQNlNRQjNPZ1RMbDNmUTdWaEk0TzdYNkpCekp4N3dYZGJweWZTWXlLS1F6L0tWUjBFS1R2RjFKd2k5T01Mdjg2VlR6cDdJTFU5ODJsWFNkVEk1eEQ5cVlBekxoZUdWY3RIdnBxY0FKM3hYSjBEb3FkYlkveXJaL2g4V2M4bGNWbXQ1bzVUbHVtb21rTkxFUnRMWlZ4QTBSOENvQWU1NTc3TnREazF0cmxLbUluSVd0TlBhUG05S1dpby84ano4c0hRRGdRME9QcTJYT29uazZKZnB0Mzh6Wi8ya2oyNlNyN25yMjdJRlJHeHJ5SnlzNmx2RmRTdU5YSVY3eDhkMVpyaTNtdWE5NWNpcC94VFF2aUVZWDR1OXpDZWZOcC9ieXZodkpUZnowZVBabDU2b2xUeDY5azNKT05PZjljblljY09sbUI1amFZVDFrdk5Vd1Q1QlpSaGgrSE95TzdDNFRrcElsUnRuNkN5c25VejNLWnBLOWkxbHhtUzVLcStETGQvV3I0TEU3cFlzemwycDJ0aHFXc0pUbFFrTE1vTmZYTEgxc1lUaStFcG9pM0hNWGQxRE9na1V0VzVMWDRYZWJiMWZoZTljWER0eWVCODdkWWVVQklIZnUvdFdWYklvVncvcGZGc0NnM0Jkc1RvRStVU3pLRkRhcVlpOXB5Z1VkSyt4UjVaQXhlY2N4cE9QSmVwL2pxUkhXZjRTN2p4UEJOYnY3MEhVS0pmTTgvTHhGVCs4Y0pKRy83S05EV1B2bmY2UVNieXVKamJIcERWdHp3bm1XNUFXMlorOU8xUjNTbGJjanl3dzNvck8zZ3ltK0VFMHhHbWJ3cnMrTjJmdW01QjgrZmU2Uko3S2t5MThRZDAyVzVKeC9MQ3pITzM3MnE5UW5PMjh6YzdJQytsY2toakF1WjQyQ3U3WWUweFZIcDRYV3ZMU1RxZWdMR3FGQWlXNDJobks1NFJJaFd3WTZxYmRBeVZEbFpGT0dDV3pUQXk5TG5ycUJiQWVhUjlxT2kyU25sYlByTm11elZyNDVxclI5L1duZTZ5bTZoclBEWHQ0Vm1hVGFWZVNxQjkyQmtXT1hFa1NtRkNjazlWQVljNWsreVlYNWFKUDl3NUVQV2JVWlVsTEtzRzl4eGt1Vi96ZDBrcm9VZ0hBUk84bW01ZmNGTlJRd3VOR3lDaWZkY2FNcTZkdXMwQXNzNkFjM2c1Vkg5VlVzRlFPcjJLcjQ1R1lWSUhqaFg3WnoxR2RMbG1tdlFGWjgzbHpEeklObmVhbm9LVmFGRlhJV1cwUHM2eTRHNUcvcDd0MEdWN1JncjFqS0dMQ1l4VUh0dUR0U0tscmIzdGp4Uko0dUU2UUIvRldCV3IyVXpvK3ArSVp3NXhudlNPc3Q1M1VhQ1U2MmI0QldSRDhoY1BRQVpzL0srS2thL00yY2l4QXJmdys1aXZDNzFoUlM5cGFsdFBuQkhBaldZMHhXZVlGSmRqSENUWDhONm9yRHg0UjZNY3k5NFVyQzJ2NmRObG5jdmIvOUFTTHVwUlVvZGl5OTBhUUdHQXBwZUQ5NDl3cEVZTDJDUCtuSHBGVnJrWWZORUF5NXhYVTBWTlJqUHRSRW10ZTlTRVZZZm9qQThkWDJvcVg3WnVtekN5MDJBZW4vLzJRPT08L0JEQj4KICAgIDwvQklSPgo8L0JJUj4K \ No newline at end of file diff --git a/performance-test/support-files/center_machine_id_values.csv b/performance-test/support-files/center_machine_id_values.csv new file mode 100644 index 000000000..2d6e7f93d --- /dev/null +++ b/performance-test/support-files/center_machine_id_values.csv @@ -0,0 +1,6 @@ +machineID,centerID +10001,10001 +10002,10002 +10003,10003 +10004,10004 +10005,10005 diff --git a/pom.xml b/pom.xml index 3921015a8..3a2734f75 100644 --- a/pom.xml +++ b/pom.xml @@ -1,55 +1,67 @@ 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.0.2.RELEASE - - - io.mosip + io.inji mimoto - 0.2.0-SNAPSHOT + 0.22.0-SNAPSHOT mimoto + https://github.com/inji/mimoto Mobile server backend supporting Inji. - - 11 + + + MPL 2.0 + https://www.mozilla.org/en-US/MPL/2.0/ + + + + scm:git:git://github.com/inji/mimoto.git + scm:git:ssh://github.com:inji/mimoto.git + https://github.com/inji/mimoto + HEAD + + + + Mosip + mosip.emailnotifier@gmail.com + io.inji + https://inji.io/ + + + UTF-8 - - 1.2.0 - 1.2.0 + + 1.3.0 + 1.3.0 + 1.3.0 + 1.3.0 + 1.3.0 + + 3.9.1 - - 11 - 11 + + 21 + 21 3.8.0 3.0.2 3.1.0 2.22.0 - 0.8.5 + 0.8.11 3.7.0.1746 - 3.0.1 - - - 2.0.2.RELEASE - + 3.6.3 - - 2.1.210 + + 3.3.10 + 3.4.0 + 3.4.1 - + 4.13.1 - 2.23.4 - 1.7.4 - 2.0.7 + 5.15.2 - - 1.2.3 - - + 2.13.2 2.13.2.2 @@ -57,31 +69,92 @@ 2.2.10 20180813 - + 4.5.13 - 1.18.8 + 1.18.30 2.8.9 1.1.1 - 1.4.2 - 1.70 + 1.78.1 3.8.1 3.3.3 - 7.2.0 - 2.0.0 + 8.0.3 + 5.0.3 5.5.13 3.6.1 3.7 2.11.0 1.15 - - + 2.9.2 + 1.5.10 + 0.9.1 + 42.7.2 + 0.7.0 - **/constant/**,**/config/**,**/httpfilter/**,**/cache/**,**/dto/**,**/entity/**,**/model/**,**/exception/**,**/repository/**,**/security/**,**/*Config.java,**/*BootApplication.java,**/*VertxApplication.java,**/cbeffutil/** + **/constant/**,**/config/**,**/httpfilter/**,**/cache/**,**/dto/**,**/entity/**,**/dbentity/**,**/model/**,**/exception/**,**/repository/**,**/security/**,**/*Config.java,**/*BootApplication.java,**/*VertxApplication.java,**/cbeffutil/**,**/core/http/**,**/util/** **/dto/**,**/entity/**,**/config/** + + + + io.mosip.kernel + kernel-bom + 1.3.0 + pom + import + + + + + + io.mosip.kernel + kernel-core + 1.3.0 + + + org.junit.vintage + junit-vintage-engine + + + junit + junit + + + + + org.apache.logging.log4j + log4j-core + + + io.micrometer + micrometer-core + + + com.android.identity + identity-credential + 20231002 + + + io.inji + vcverifier-jar + 1.8.1 + + + io.inji + inji-openid4vp-jar + 1.0.0-SNAPSHOT + + + io.micrometer + micrometer-registry-prometheus + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + org.springframework.boot spring-boot-starter-web @@ -112,14 +185,39 @@ org.springframework.boot spring-boot-starter-test + + + org.mockito + mockito-core + + + test + + + org.springframework.security + spring-security-core test - org.springframework.boot spring-boot-starter-data-jpa ${spring.boot.version} + + org.springframework.boot + spring-boot-starter-oauth2-client + ${spring.boot.version} + + + org.springframework.session + spring-session-data-redis + ${session.redis.version} + + + org.springframework.boot + spring-boot-starter-data-redis + ${starter.redis.version} + @@ -127,17 +225,22 @@ kernel-logger-logback ${mosip.kernel.core.version} - io.mosip.kernel kernel-auth-adapter - ${mosip.kernel.core.version} + ${mosip.kernel.auth.adapter.version} + + + org.springframework.security + spring-security-test + + io.mosip.kernel kernel-websubclient-api - ${mosip.kernel.core.version} + 1.3.0 @@ -150,24 +253,26 @@ org.projectlombok lombok - ${lombok.version} compile com.googlecode.json-simple json-simple - 1.1.1 org.json json - 20190722 com.google.code.gson gson ${google.gson.version} + + com.jayway.jsonpath + json-path + 2.10.0 + junit @@ -175,10 +280,11 @@ ${junit.version} test + - com.h2database - h2 - ${h2.version} + org.junit.vintage + junit-vintage-engine + test @@ -186,16 +292,6 @@ slf4j-api 1.7.25 - - ch.qos.logback - logback-classic - 1.2.6 - - - ch.qos.logback - logback-core - 1.2.9 - joda-time joda-time @@ -206,6 +302,11 @@ velocity 1.7 + + commons-validator + commons-validator + 1.7 + org.apache.velocity velocity-tools @@ -227,9 +328,14 @@ commons-lang3 ${commons-lang3} + + commons-logging + commons-logging + 1.2 + org.bouncycastle - bcprov-jdk15on + bcprov-jdk18on ${bouncycastle.version} @@ -260,12 +366,6 @@ javax.activation activation - 1.1 - - - org.glassfish.jaxb - jaxb-runtime - 2.3.0-b170127.1453 com.itextpdf @@ -301,7 +401,6 @@ org.apache.httpcomponents httpclient - 4.5.13 org.mockito @@ -310,28 +409,157 @@ test + + com.nimbusds + nimbus-jose-jwt + 9.41.2 + + + io.jsonwebtoken + jjwt + + + org.bitbucket.b_c + jose4j + 0.9.2 + + + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.20 + + + io.inji + pixelpass-jar + 0.8.0 + + + io.inji + injivcrenderer-jar + 0.2.0 + + + org.apache.pdfbox + pdfbox + 2.0.33 + + + org.apache.xmlgraphics + batik-transcoder + 1.19 + + + org.apache.xmlgraphics + fop + 2.11 + + + io.mosip.kernel + kernel-biometrics-api + ${kernel.biometrics.api.version} + + + org.junit.vintage + junit-vintage-engine + + + + + + org.springframework + spring-core + 5.1.12.RELEASE + + + io.mosip.kernel + kernel-cbeffutil-api + ${kernel.cbeffutil.version} + + + org.junit.vintage + junit-vintage-engine + + + + + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + + org.postgresql + postgresql + ${postgresql.version} + + + io.mosip.kernel + kernel-keymanager-service + 1.4.0 + lib + + + org.springframework.security + spring-security-test + + + + + io.vertx + vertx-web + 3.9.13 + + + com.authlete + sd-jwt + 1.5 + - ossrh - Central Repository - https://oss.sonatype.org/content/repositories/snapshots + ossrh-central + MavenCentralRepository + https://central.sonatype.com/repository/maven-snapshots default true + + central + MavenCentral + default + https://repo1.maven.org/maven2 + + false + + + + google-maven + https://maven.google.com/ + + + danubetech-maven-public + https://repo.danubetech.com/repository/maven-public/ + ossrh - https://oss.sonatype.org/content/repositories/snapshots + https://central.sonatype.com/repository/maven-snapshots ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://central.sonatype.com/api/v1/publisher @@ -345,7 +573,14 @@ ${skipTests} false - ${argLine} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --illegal-access=permit + ${argLine} --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/sun.security.jca=ALL-UNNAMED + --add-opens java.base/sun.security.util=ALL-UNNAMED + --add-opens java.base/jdk.internal.util=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.time=ALL-UNNAMED + --illegal-access=permit @@ -355,13 +590,14 @@ ${maven.jacoco.version} + jacoco-initialize prepare-agent - report - prepare-package + jacoco-site + package report @@ -468,14 +704,13 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 + org.sonatype.central + central-publishing-maven-plugin + ${central.publishing.maven.plugin.version} true - ossrh - https://oss.sonatype.org/ - false + ossrh + false @@ -492,8 +727,31 @@ - - + + pl.project13.maven + git-commit-id-plugin + 3.0.1 + + + get-the-git-infos + + revision + + validate + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.build.(time|version)$ + ^git.commit.id.(abbrev|full)$ + + full + ${project.basedir}/.git + + + org.apache.maven.plugins maven-gpg-plugin diff --git a/scripts/generate_THIRDPARTY.md.py b/scripts/generate_THIRDPARTY.md.py deleted file mode 100644 index f2371f882..000000000 --- a/scripts/generate_THIRDPARTY.md.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generates THIRDPARTY.md license report as gathered from pom.xml -# Usage: -# pip install markdownify -# python scripts/generate_THIRDPARTY.md.py -# -# A THIRDPARTY.md file is generated at project's root folder. - -import markdownify -import subprocess -import sys - -subprocess.run(['mvn', 'clean']) -subprocess.run(['mvn', 'project-info-reports:dependencies']) -html = open('target/site/dependencies.html').read() -h = markdownify.markdownify(html, heading_style="ATX") -with open('THIRDPARTY.md','w') as f: - for line in h.splitlines(): - if 'Project Dependency Graph' in line: - break - print(line, file=f) diff --git a/src/main/java/io/mosip/mimoto/MimotoServiceApplication.java b/src/main/java/io/mosip/mimoto/MimotoServiceApplication.java index b89cf5fa0..3e9f51d18 100644 --- a/src/main/java/io/mosip/mimoto/MimotoServiceApplication.java +++ b/src/main/java/io/mosip/mimoto/MimotoServiceApplication.java @@ -2,34 +2,56 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.kernel.biometrics.spi.CbeffUtil; +import io.mosip.kernel.cbeffutil.impl.CbeffImpl; +import io.mosip.kernel.keygenerator.bouncycastle.KeyGenerator; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import lombok.extern.slf4j.Slf4j; import org.json.simple.JSONObject; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import io.mosip.mimoto.service.impl.CbeffImpl; -import io.mosip.mimoto.spi.CbeffUtil; @SpringBootApplication(scanBasePackages = { "io.mosip.mimoto.*", + "io.mosip.kernel.websub.*", + "io.mosip.kernel.cryptomanager.*", + "io.mosip.kernel.keymanager.*", + "io.mosip.kernel.keymanagerservice.helper", + "io.mosip.kernel.keymanagerservice.util", + "io.mosip.kernel.keymanagerservice.service", + "io.mosip.kernel.keymanagerservice.validator", + "io.mosip.kernel.crypto", "${mosip.auth.adapter.impl.basepackage}" }, exclude = { - SecurityAutoConfiguration.class, - DataSourceAutoConfiguration.class, - HibernateJpaAutoConfiguration.class, - CacheAutoConfiguration.class + SecurityAutoConfiguration.class }) +@ConfigurationPropertiesScan +@EntityScan(basePackages = {"io.mosip.mimoto.model", "io.mosip.kernel.keymanagerservice.entity"}) +@EnableJpaRepositories(basePackages = {"io.mosip.mimoto.repository", "io.mosip.kernel.keymanagerservice.repository"}) +@Slf4j @EnableScheduling @EnableAsync +@SecurityScheme( + name = "SessionAuth", + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.COOKIE, + paramName = "SESSION", + description = "Session-based authentication using a session ID stored in the cookie. The client must send the 'SESSION' cookie (e.g., SESSION=) with each request." +) public class MimotoServiceApplication { + @Bean @Primary public CbeffUtil getCbeffUtil() { @@ -44,6 +66,11 @@ public ThreadPoolTaskScheduler taskScheduler() { return threadPoolTaskScheduler; } + @Bean + public KeyGenerator keyGenerator() { + return new KeyGenerator(); + } + public static JSONObject getGitProp() { try { return (new ObjectMapper()).readValue( @@ -51,14 +78,14 @@ public static JSONObject getGitProp() { JSONObject.class ); } catch (Exception e) { - System.err.println("Error when trying to read build.json file: " + e); + log.error("Error when trying to read build.json file: " + e); } return new JSONObject(); } public static void main(String[] args) { JSONObject gitProp = getGitProp(); - System.out.println( + log.info( String.format( "Mimoto Service version: %s - revision: %s @ branch: %s | build @ %s", gitProp.get("git.build.version"), diff --git a/src/main/java/io/mosip/mimoto/config/AppConfig.java b/src/main/java/io/mosip/mimoto/config/AppConfig.java new file mode 100644 index 000000000..70c5f5ac4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/AppConfig.java @@ -0,0 +1,26 @@ +package io.mosip.mimoto.config; + + +import io.mosip.injivcrenderer.InjiVcRenderer; +import io.mosip.pixelpass.PixelPass; +import io.mosip.vercred.vcverifier.CredentialsVerifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + @Bean + public CredentialsVerifier credentialsVerifier() { + return new CredentialsVerifier(); + } + + @Bean + public PixelPass pixelPass() { + return new PixelPass(); + } + + @Bean + public InjiVcRenderer injiVcRenderer() { + return new InjiVcRenderer("inji-web"); + } +} diff --git a/src/main/java/io/mosip/mimoto/config/CacheConfig.java b/src/main/java/io/mosip/mimoto/config/CacheConfig.java new file mode 100644 index 000000000..67a325f36 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/CacheConfig.java @@ -0,0 +1,176 @@ +package io.mosip.mimoto.config; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; +import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +/** + * Cache configuration for the application. + * + * This class provides customizers to configure two cache providers based on + * the 'spring.cache.type' property. + * + * 1. CaffeineCacheManager – configured by a `CacheManagerCustomizer` bean, + * allowing customization of specific cache settings like per-cache TTLs, + * without creating the full bean from scratch. + * + * 2. RedisCacheManager – configured by a `RedisCacheManagerBuilderCustomizer` bean, + * which gives full control over the Redis cache configuration (value serializer, + * per-cache TTLs, etc.) without creating the full bean from scratch. + * + * Both customizer beans are only created if the corresponding cache type is + * specified. If 'spring.cache.type' is not set, Spring Boot will auto-detect the cache + * provider based on the libraries on the classpath, or fall back to the + * SimpleCacheManager (in-memory ConcurrentHashMap) if no cache library is available. + * + * NOTE: If 'spring.cache.type' is set to an invalid or unsupported value, + * the application will fail to start. Spring Boot does not fall back to a + * default cache manager in that case. + */ +@Configuration +@EnableCaching +@Slf4j +public class CacheConfig { + + @Value("${spring.cloud.config.label}") + private String springConfigLabel; + + private String cachePrefix; + + @PostConstruct + public void init() { + cachePrefix = "injiweb:" + springConfigLabel; + } + + private static final String ISSUER_WELLKNOWN_CACHE = "issuerWellknown"; + private static final String ISSUERS_CONFIG_CACHE = "issuersConfig"; + private static final String AUTH_SERVER_WELLKNOWN_CACHE = "authServerWellknown"; + private static final String PRE_REGISTERED_TRUSTED_VERIFIERS_CACHE = "preRegisteredTrustedVerifiers"; + + @Value("${cache.credential-issuer.wellknown.expiry-time-in-min:60}") + private Long issuerWellknownExpiryTimeInMin; + + @Value("${cache.issuers-config.expiry-time-in-min:60}") + private Long issuersConfigExpiryTimeInMin; + + @Value("${cache.credential-issuer.authserver-wellknown.expiry-time-in-min:60}") + private Long authServerWellknownExpiryTimeInMin; + + @Value("${cache.pre-registered-trusted-verifiers.expiry-time-in-min:60}") + private Long preRegisteredTrustedVerifiersExpiryTimeInMin; + + @Value("${cache.default.expiry-time-in-min:60}") + private long defaultCacheExpiryTimeInMin; + + /** + * Creates a reusable Caffeine builder configuration. + * + * @param expiryTime The custom expiry time in minutes for the cache. + * @return A configured Caffeine builder instance. + */ + private Caffeine createCaffeineCacheConfig(Long expiryTime) { + return Caffeine.newBuilder().expireAfterWrite( + Objects.requireNonNullElse(expiryTime, defaultCacheExpiryTimeInMin), TimeUnit.MINUTES + ); + } + + /** + * This method defines a CacheManagerCustomizer bean to customize the + * auto-configured CaffeineCacheManager. It registers custom caches + * with their specific configurations. This is the preferred Spring Boot approach. + * This bean is only created if 'spring.cache.type' is set to 'caffeine'. + * + * @return A CacheManagerCustomizer instance. + */ + @Bean + @ConditionalOnProperty(name = "spring.cache.type", havingValue = "caffeine") + public CacheManagerCustomizer caffeineCacheManagerCustomizer() { + log.info("******* Customizing Caffeine cache provider *******"); + + return cacheManager -> { + cacheManager.registerCustomCache( + ISSUER_WELLKNOWN_CACHE, + createCaffeineCacheConfig(issuerWellknownExpiryTimeInMin).build() + ); + cacheManager.registerCustomCache( + ISSUERS_CONFIG_CACHE, + createCaffeineCacheConfig(issuersConfigExpiryTimeInMin).build() + ); + cacheManager.registerCustomCache( + AUTH_SERVER_WELLKNOWN_CACHE, + createCaffeineCacheConfig(authServerWellknownExpiryTimeInMin).build() + ); + cacheManager.registerCustomCache( + PRE_REGISTERED_TRUSTED_VERIFIERS_CACHE, + createCaffeineCacheConfig(preRegisteredTrustedVerifiersExpiryTimeInMin).build() + ); + // Set the default Caffeine config for any other caches + cacheManager.setCaffeine(createCaffeineCacheConfig(null)); + }; + } + + /** + * This method defines a RedisCacheManagerBuilderCustomizer bean to customize the + * auto-configured RedisCacheManager. It registers per-cache configurations. + * This is the preferred Spring Boot approach. + * This bean is only created if 'spring.cache.type' is set to 'redis'. + * + * @return A RedisCacheManagerBuilderCustomizer instance. + */ + @Bean + @ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis") + public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { + log.info("******* Customizing Redis cache provider *********"); + GenericJackson2JsonRedisSerializer jacksonSerializer = new GenericJackson2JsonRedisSerializer(); + + return builder -> { + // Default config for the Redis cache manager + RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSerializer)) + .entryTtl(Duration.ofMinutes(defaultCacheExpiryTimeInMin)) + .disableCachingNullValues(); + if (cachePrefix != null && !cachePrefix.isEmpty()) { + defaultCacheConfig = defaultCacheConfig.prefixCacheNameWith(cachePrefix); + } + + // Per-cache configs + Map cacheConfigurations = Map.of( + ISSUER_WELLKNOWN_CACHE, createRedisConfigWithTtl(defaultCacheConfig, issuerWellknownExpiryTimeInMin), + ISSUERS_CONFIG_CACHE, createRedisConfigWithTtl(defaultCacheConfig, issuersConfigExpiryTimeInMin), + AUTH_SERVER_WELLKNOWN_CACHE, createRedisConfigWithTtl(defaultCacheConfig, authServerWellknownExpiryTimeInMin), + PRE_REGISTERED_TRUSTED_VERIFIERS_CACHE, createRedisConfigWithTtl(defaultCacheConfig, preRegisteredTrustedVerifiersExpiryTimeInMin) + ); + + // Apply default and per-cache configurations + builder.cacheDefaults(defaultCacheConfig) + .withInitialCacheConfigurations(cacheConfigurations); + }; + } + + /** + * Helper method to create a RedisCacheConfiguration with a specific TTL. + * + * @param baseConfig The base RedisCacheConfiguration. + * @param expiryTime The expiry time in minutes, with a fallback to the default. + * @return The new RedisCacheConfiguration with the specified TTL. + */ + private RedisCacheConfiguration createRedisConfigWithTtl(RedisCacheConfiguration baseConfig, Long expiryTime) { + return baseConfig.entryTtl(Duration.ofMinutes(Objects.requireNonNullElse(expiryTime, defaultCacheExpiryTimeInMin))); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/config/Config.java b/src/main/java/io/mosip/mimoto/config/Config.java index 51be07df1..4dd1f401f 100644 --- a/src/main/java/io/mosip/mimoto/config/Config.java +++ b/src/main/java/io/mosip/mimoto/config/Config.java @@ -1,68 +1,245 @@ package io.mosip.mimoto.config; +import io.mosip.mimoto.security.oauth2.CustomOAuth2UserService; +import io.mosip.mimoto.security.oauth2.OAuth2AuthenticationFailureHandler; +import io.mosip.mimoto.security.oauth2.OAuth2AuthenticationSuccessHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.session.SessionRepository; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.OncePerRequestFilter; -import io.mosip.kernel.auth.defaultadapter.filter.AuthFilter; -import io.mosip.kernel.auth.defaultadapter.filter.CorsFilter; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.util.LoggerUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static io.mosip.mimoto.exception.ErrorConstants.LOGIN_SESSION_INVALIDATE_EXCEPTION; @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity @Order(1) -public class Config extends WebSecurityConfigurerAdapter { - private Logger logger = LoggerUtil.getLogger(Config.class); +@Slf4j +public class Config { + + // CSRF Configuration Constants + private static final String CSRF_COOKIE_PATH = "/"; + private static final String CSRF_COOKIE_NAME = "XSRF-TOKEN"; + private static final String CSRF_HEADER_NAME = "X-XSRF-TOKEN"; + private static final String CSRF_COOKIE_HEADER_PREFIX = "XSRF-TOKEN="; + + // HTTP Method Constants + private static final String HTTP_METHOD_GET = "GET"; @Value("${mosipbox.public.url}") private String baseUrl; - @Value("${mosip.security.csrf-enable:false}") - private boolean isCSRFEnable; - @Value("${mosip.security.cors-enable:false}") private boolean isCORSEnable; - @Value("${mosip.security.origins:localhost:8080}") + @Value("${mosip.security.origins:localhost:8088}") private String origins; - @Override - protected void configure(HttpSecurity http) throws Exception { + @Value("${mosip.security.ignore-auth-urls}") + private String[] ignoreAuthUrls; - if (!isCSRFEnable) { - http = http.csrf().disable(); - } - // http.authorizeRequests().antMatchers("*").authenticated().and().exceptionHandling() - // .authenticationEntryPoint(new AuthEntryPoint()).and().sessionManagement() - // .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + @Value("${mosip.security.csrf-ignore-urls}") + private String[] csrfIgnoreUrls; + + @Value("${mosip.inji.web.url}") + private String injiWebUrl; + + @Bean + @ConfigurationProperties(prefix = "mosip.inji") + public Map injiConfig() { + return new HashMap<>(); + } + + @Autowired + private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; + + @Autowired + private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; + + @Autowired + private CustomOAuth2UserService customOAuth2UserService; - // http.addFilterBefore(authFilter(), - // UsernamePasswordAuthenticationFilter.class); + private CookieCsrfTokenRepository csrfTokenRepository; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, SessionRepository sessionRepository) throws Exception { + configureCsrf(http); + http.addFilterAfter(new CsrfTokenCookieFilter(csrfTokenRepository), org.springframework.security.web.csrf.CsrfFilter.class); - // FIXME: web security is disabled for all enpoints - http.authorizeRequests().antMatchers("*").authenticated().anyRequest().permitAll().and().exceptionHandling(); if (isCORSEnable) { - http.addFilterBefore(new CorsFilter(origins), AuthFilter.class); + http.cors(corsCustomizer -> corsCustomizer + .configurationSource(corsConfigurationSource())); + } + http.headers(headersEntry -> { + headersEntry.cacheControl(Customizer.withDefaults()); + headersEntry.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin); + }); + + setupOauth2Config(http, sessionRepository); + + http.exceptionHandling(exceptionHandling -> + exceptionHandling.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ); + + + return http.build(); + + } + + private void configureCsrf(HttpSecurity http) throws Exception { + csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + csrfTokenRepository.setCookiePath(CSRF_COOKIE_PATH); + csrfTokenRepository.setCookieName(CSRF_COOKIE_NAME); + csrfTokenRepository.setHeaderName(CSRF_HEADER_NAME); + + CsrfTokenRequestHandler requestHandler = new CsrfTokenRequestAttributeHandler(); + + if (csrfIgnoreUrls != null && csrfIgnoreUrls.length > 0) { + http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository).csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers(csrfIgnoreUrls)); + } else { + http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository).csrfTokenRequestHandler(requestHandler)); } - http.headers().cacheControl(); - http.headers().frameOptions().sameOrigin(); + } + + private void setupOauth2Config(HttpSecurity http, SessionRepository sessionRepository) throws Exception { + configureOAuth2Login(http); + configureLogout(http, sessionRepository); + configureAuthorization(http); + configureSessionManagement(http); + } + + private void configureOAuth2Login(HttpSecurity http) throws Exception { + http.oauth2Login(oauth2Login -> oauth2Login + .loginPage(injiWebUrl + "/") + .authorizationEndpoint(authorization -> authorization.baseUri("/oauth2/authorize")) + .redirectionEndpoint(redirect -> redirect.baseUri("/oauth2/callback/*")) + .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .successHandler(oAuth2AuthenticationSuccessHandler) + .failureHandler(oAuth2AuthenticationFailureHandler) + ); + } + private void configureLogout(HttpSecurity http, SessionRepository sessionRepository) throws Exception { + http.logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessHandler((request, response, authentication) -> { + // If authentication is not null, it was a valid logout. + if (authentication != null) { + response.setStatus(HttpStatus.OK.value()); + response.setContentType("application/json"); + response.getWriter().write("{\"status\":\"success\", \"message\":\"Logout successful\"}"); + } else { + response.setStatus(HttpStatus.NOT_FOUND.value()); + response.setContentType("application/json"); + String jsonResponse = String.format("{\"errors\":[{\"errorCode\":\"%s\",\"errorMessage\":\"%s\"}]}", + LOGIN_SESSION_INVALIDATE_EXCEPTION.getErrorCode(), + LOGIN_SESSION_INVALIDATE_EXCEPTION.getErrorMessage()); + response.getWriter().write(jsonResponse); + } + }) + ); } + + private void configureAuthorization(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(authz -> authz + .requestMatchers(ignoreAuthUrls).permitAll() + .anyRequest().authenticated() + ); + } + + private void configureSessionManagement(HttpSecurity http) throws Exception { + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); + } + + // Define CORS configuration @Bean - public TaskScheduler taskScheduler() { - ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setPoolSize(5); - threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler"); - return threadPoolTaskScheduler; + public CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Arrays.asList(origins.split(","))); // Allow all origins + corsConfiguration.addAllowedHeader("*"); // Allow all headers + corsConfiguration.addAllowedMethod("*"); // Allow all HTTP methods + corsConfiguration.setAllowCredentials(true);// Allow cookies to be sent + source.registerCorsConfiguration("/**", corsConfiguration); + return source; } + + /** + * Filter to ensure CSRF tokens are generated and stored in cookies on all requests, + * including GET requests. This ensures the XSRF-TOKEN cookie is available for clients + * to read and use in subsequent state-changing requests. + */ + static class CsrfTokenCookieFilter extends OncePerRequestFilter { + + private final CsrfTokenRepository csrfTokenRepository; + + public CsrfTokenCookieFilter(CsrfTokenRepository csrfTokenRepository) { + this.csrfTokenRepository = csrfTokenRepository; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + boolean isGetRequest = HTTP_METHOD_GET.equalsIgnoreCase(request.getMethod()); + if (isGetRequest) { + if (csrfToken == null) { + csrfToken = csrfTokenRepository.loadToken(request); + if (csrfToken == null) { + csrfToken = csrfTokenRepository.generateToken(request); + } + request.setAttribute(CsrfToken.class.getName(), csrfToken); + } + + boolean cookieAlreadySet = false; + Collection setCookieHeaders = response.getHeaders("Set-Cookie"); + for (String cookieHeader : setCookieHeaders) { + if (cookieHeader != null && cookieHeader.startsWith(CSRF_COOKIE_HEADER_PREFIX)) { + cookieAlreadySet = true; + break; + } + } + + if (!cookieAlreadySet) { + csrfTokenRepository.saveToken(csrfToken, request, response); + } + } + + filterChain.doFilter(request, response); + } + } + } diff --git a/src/main/java/io/mosip/mimoto/config/IssuersValidationConfig.java b/src/main/java/io/mosip/mimoto/config/IssuersValidationConfig.java new file mode 100644 index 000000000..363d7b06e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/IssuersValidationConfig.java @@ -0,0 +1,91 @@ +package io.mosip.mimoto.config; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.service.IssuersService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.Validator; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@Component +public class IssuersValidationConfig implements ApplicationRunner { + @Autowired + IssuersService issuersService; + @Autowired + private Validator validator; + private final String VALIDATION_ERROR_MSG = "\n\nValidation failed in Mimoto-issuers-config.json:"; + @Override + public void run(ApplicationArguments args) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + log.info("Validation for mimoto-issuers-config.json STARTED"); + + List allErrors = new ArrayList<>(); + AtomicReference> credentialIssuers = new AtomicReference<>(new HashSet<>()); + + IssuersDTO issuerDTOList = null; + try { + issuerDTOList = issuersService.getAllIssuers(); + } catch (Exception e) { + log.error(VALIDATION_ERROR_MSG, e); + throw new RuntimeException(VALIDATION_ERROR_MSG, e); + } + + if (issuerDTOList != null) { + for (int index = 0; index < issuerDTOList.getIssuers().size(); index++) { + IssuerDTO issuerDTO = issuerDTOList.getIssuers().get(index); + if (!issuerDTO.getProtocol().equals("OTP")) { + Errors errors = new BeanPropertyBindingResult(issuerDTO, "issuerV2DTO"); + validator.validate(issuerDTO, errors); + String issuerId = issuerDTO.getIssuer_id(); + boolean issuerHasErrors = false; + + StringBuilder issuerErrors = new StringBuilder(); + issuerErrors.append(String.format("Errors for issuer at index: %d with issuerId - %s\n", index, issuerId)); + + if (errors.hasErrors()) { + issuerHasErrors = true; + errors.getFieldErrors().stream() + .sorted(Comparator.comparing(FieldError::getField)) + .forEach(error -> issuerErrors.append(String.format("- %s %s\n", error.getField(), error.getDefaultMessage()))); + + } + + Set currentIssuers = credentialIssuers.get(); + + if (!currentIssuers.add(issuerId)) { + issuerHasErrors = true; + issuerErrors.append("- Duplicate value found for the issuerId. More than one issuer is having the same issuerId").append("\n"); + } + + if (issuerHasErrors) { + allErrors.add(issuerErrors.toString()); + } + + credentialIssuers.set(currentIssuers); + } + }; + } + + if (!allErrors.isEmpty()) { + StringBuilder fieldErrorsBuilder = new StringBuilder(); + allErrors.forEach(fieldErrorsBuilder::append); + String fieldErrorsString = VALIDATION_ERROR_MSG + "\n" + fieldErrorsBuilder; + log.error(fieldErrorsString); + throw new RuntimeException(fieldErrorsString); + } + + log.info("Validation for mimoto-issuers-config.json COMPLETED"); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/config/JwtConfig.java b/src/main/java/io/mosip/mimoto/config/JwtConfig.java new file mode 100644 index 000000000..bcbec0233 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/JwtConfig.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +@Configuration +public class JwtConfig { + + @Bean + @Qualifier("googleJwtDecoder") + public JwtDecoder googleIdTokenDecoder(@Value("${spring.security.oauth2.client.provider.google.jwk-set-uri}") String jwkSetUri) { + return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + } +} diff --git a/src/main/java/io/mosip/mimoto/config/KeyManagerConfig.java b/src/main/java/io/mosip/mimoto/config/KeyManagerConfig.java new file mode 100644 index 000000000..ebd80e3cf --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/KeyManagerConfig.java @@ -0,0 +1,37 @@ +package io.mosip.mimoto.config; + +import io.mosip.kernel.keymanagerservice.dto.KeyPairGenerateRequestDto; +import io.mosip.kernel.keymanagerservice.service.KeymanagerService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class KeyManagerConfig implements ApplicationRunner { + + + @Autowired + private KeymanagerService keymanagerService; + + + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("===================== MIMOTOSERVICE ROOT KEY CHECK ========================"); + String objectType = "CSR"; + KeyPairGenerateRequestDto rootKeyRequest = new KeyPairGenerateRequestDto(); + rootKeyRequest.setApplicationId("ROOT"); + // Set the reference id to empty string, as keymanager is expecting the same for initialization + rootKeyRequest.setReferenceId(org.apache.commons.lang3.StringUtils.EMPTY); + keymanagerService.generateMasterKey(objectType, rootKeyRequest); + log.info("===================== MIMOTOSERVICE MASTER KEY CHECK ========================"); + KeyPairGenerateRequestDto masterKeyRequest = new KeyPairGenerateRequestDto(); + masterKeyRequest.setApplicationId("MIMOTO"); + // Set the reference id to empty string, as keymanager is expecting the same for initialization + masterKeyRequest.setReferenceId(org.apache.commons.lang3.StringUtils.EMPTY); + keymanagerService.generateMasterKey(objectType, masterKeyRequest); + log.info("===================== MIMOTO KEY SETUP COMPLETED ========================"); + } +} diff --git a/src/main/java/io/mosip/mimoto/config/OpenApiProperties.java b/src/main/java/io/mosip/mimoto/config/OpenApiProperties.java new file mode 100644 index 000000000..7fd584252 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/OpenApiProperties.java @@ -0,0 +1,47 @@ +package io.mosip.mimoto.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConfigurationProperties(prefix = "openapi") +@Data +public class OpenApiProperties { + private InfoProperty info; + private Service service; + private Group group; +} + +@Data +class InfoProperty { + private String title; + private String description; + private String version; + private LicenseProperty license; +} + +@Data +class LicenseProperty { + private String name; + private String url; +} + +@Data +class Service { + private List servers; +} + +@Data +class Server { + private String description; + private String url; +} + +@Data +class Group { + private String name; + private List paths; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/config/SessionCacheConfig.java b/src/main/java/io/mosip/mimoto/config/SessionCacheConfig.java new file mode 100644 index 000000000..e94461393 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/SessionCacheConfig.java @@ -0,0 +1,60 @@ +package io.mosip.mimoto.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Cache; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.MapSessionRepository; +import org.springframework.session.Session; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +import java.time.Duration; + +/** + * Configuration for HTTP session store. + * + * This class provides a MapSessionRepository bean that uses Caffeine as the session store + * for certain cases and acts as a fallback to ensure the application always has a session repository. + * + * Behavior based on 'spring.session.store-type': + * - If set to 'caffeine': uses the Caffeine-backed repository with TTL and a maximum entry records limit. + * - If set to 'none', missing, or empty: uses the Caffeine-backed repository as a fallback. + * - If set to a supported Spring Session provider (e.g., Redis, JDBC, Mongo) and the dependency + * is available on the classpath (in pom.xml): + * - Spring Boot auto-configures the SessionRepository for that provider. + * - The Caffeine repository is ignored. + * - If set to a supported Spring Session provider but the dependency is NOT available on the classpath: + * - The application will fail to start due to a missing SessionRepository bean. + * - If set to an unsupported or invalid value: + * - The application may fail to start unless a valid fallback bean is provided. + * + * Notes: + * - The fallback Caffeine repository ensures safe startup even when no provider is set. + * - The fallback in-memory repository does not persist sessions across application restarts. + */ +@Configuration +@EnableSpringHttpSession +@Slf4j +public class SessionCacheConfig { + + @Value("${server.servlet.session.timeout:30m}") + private Duration springSessionTimeout; + + @Bean + @ConditionalOnExpression( + "('${spring.session.store-type:none}'.toLowerCase() == 'caffeine') or " + + "('${spring.session.store-type:none}'.toLowerCase() == 'none') or " + + "T(org.springframework.util.StringUtils).isEmpty('${spring.session.store-type:}')" + ) + public MapSessionRepository caffeineSessionRepository() { + log.info("******* Initializing session repository using Caffeine cache provider *******"); + Cache sessionCache = Caffeine.newBuilder() + .expireAfterAccess(springSessionTimeout) + .maximumSize(2000) + .build(); + return new MapSessionRepository(sessionCache.asMap()); + } +} diff --git a/src/main/java/io/mosip/mimoto/config/SwaggerConfig.java b/src/main/java/io/mosip/mimoto/config/SwaggerConfig.java new file mode 100644 index 000000000..c2dcf9fbf --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/SwaggerConfig.java @@ -0,0 +1,49 @@ +package io.mosip.mimoto.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.servers.Server; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +public class SwaggerConfig { + + @Autowired + private OpenApiProperties openApiProperties; + + @Bean + public OpenAPI openApi() { + OpenAPI api = new OpenAPI() + .components(new Components()) + .info(new Info() + .title(openApiProperties.getInfo().getTitle()) + .version(openApiProperties.getInfo().getVersion()) + .description(openApiProperties.getInfo().getDescription()) + .license(new License() + .name(openApiProperties.getInfo().getLicense().getName()) + .url(openApiProperties.getInfo().getLicense().getUrl()))); + + openApiProperties.getService().getServers().forEach(server -> { + api.addServersItem(new Server().description(server.getDescription()).url(server.getUrl())); + }); + log.info("swagger open api bean is ready"); + return api; + } + + @Bean + public GroupedOpenApi groupedOpenApi() { + return GroupedOpenApi.builder().group(openApiProperties.getGroup().getName()) + .pathsToMatch(openApiProperties.getGroup().getPaths().stream().toArray(String[]::new)) + .build(); + } + +} diff --git a/src/main/java/io/mosip/mimoto/config/WalletPasscodeConfig.java b/src/main/java/io/mosip/mimoto/config/WalletPasscodeConfig.java new file mode 100644 index 000000000..86b351bf3 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/WalletPasscodeConfig.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.config; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Slf4j +@Getter +@Setter +@ConfigurationProperties(prefix = "wallet.passcode") +public class WalletPasscodeConfig { + private long retryBlockedUntil = 30000; + private int maxFailedAttemptsAllowedPerCycle = 1; + private int maxLockCyclesAllowed = 1; + + @PostConstruct + public void validateAndCorrectDefaults() { + if (maxLockCyclesAllowed <= 0) { + log.warn("Invalid value for maxLockCyclesAllowed: {}. It must be > 0. Setting to default: 1", maxLockCyclesAllowed); + maxLockCyclesAllowed = 1; + } + + if (maxFailedAttemptsAllowedPerCycle <= 0) { + log.warn("Invalid value for maxFailedAttemptsAllowedPerCycle: {}. It must be > 0. Setting to default: 1", maxFailedAttemptsAllowedPerCycle); + maxFailedAttemptsAllowedPerCycle = 1; + } + } +} diff --git a/src/main/java/io/mosip/mimoto/config/oauth2/OAuth2ProviderProperties.java b/src/main/java/io/mosip/mimoto/config/oauth2/OAuth2ProviderProperties.java new file mode 100644 index 000000000..e79b72929 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/config/oauth2/OAuth2ProviderProperties.java @@ -0,0 +1,29 @@ +package io.mosip.mimoto.config.oauth2; + +import io.mosip.mimoto.dto.ProviderDataConfig; +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "spring.security.oauth2.client") +@Slf4j +@Data +public class OAuth2ProviderProperties { + private Map provider; + + @PostConstruct + public void logProviders() { + if (provider == null || provider.isEmpty()) { + log.warn("No OAuth2 providers configured under 'spring.security.oauth2.client.provider'"); + } else { + log.info("Loaded OAuth2 providers: {}", provider.keySet()); + provider.forEach((key, prov) -> log.debug("Provider {}: {}", key, prov)); + } + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/constant/ApiName.java b/src/main/java/io/mosip/mimoto/constant/ApiName.java index bea12ea47..35e288701 100644 --- a/src/main/java/io/mosip/mimoto/constant/ApiName.java +++ b/src/main/java/io/mosip/mimoto/constant/ApiName.java @@ -2,7 +2,7 @@ /** * The Enum ApiName. - * + * * @author Rishabh Keshari */ public enum ApiName { @@ -103,4 +103,13 @@ public enum ApiName { RESIDENT_CREDENTIAL_REQUEST_STATUS, RESIDENT_AUTH_LOCK, RESIDENT_AUTH_UNLOCK, + RESIDENT_INDIVIDUALID_OTP, + RESIDENT_AID_GET_INDIVIDUALID, + + IDP_LINK_TRANSACTION, + IDP_AUTHENTICATE, + IDP_CONSENT, + IDP_OTP, + BINDING_OTP, + WALLET_BINDING } diff --git a/src/main/java/io/mosip/mimoto/constant/BiometricType.java b/src/main/java/io/mosip/mimoto/constant/BiometricType.java deleted file mode 100644 index 6d86345e7..000000000 --- a/src/main/java/io/mosip/mimoto/constant/BiometricType.java +++ /dev/null @@ -1,68 +0,0 @@ -// -// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.8-b130911.1802 -// See http://java.sun.com/xml/jaxb -// Any modifications to this file will be lost upon recompilation of the source schema. -// Generated on: 2019.05.13 at 03:22:53 PM IST -// - -package io.mosip.mimoto.constant; - -import javax.xml.bind.annotation.XmlEnum; -import javax.xml.bind.annotation.XmlEnumValue; -import javax.xml.bind.annotation.XmlType; - -@XmlType(name = "SingleTypeType") -@XmlEnum -public enum BiometricType { - - @XmlEnumValue("Scent") - SCENT("Scent"), - @XmlEnumValue("Dna") - DNA("DNA"), - @XmlEnumValue("Dna") - EAR("Ear "), - @XmlEnumValue("Face") - FACE("Face"), - @XmlEnumValue("Finger") - FINGER("Finger"), - @XmlEnumValue("Foot") - FOOT("Foot"), - @XmlEnumValue("Vein") - VEIN("Vein"), - @XmlEnumValue("HandGeometry") - HAND_GEOMETRY("HandGeometry"), - @XmlEnumValue("Iris") - IRIS("Iris"), - @XmlEnumValue("Retina") - RETINA("Retina"), - @XmlEnumValue("Voice") - VOICE("Voice"), - @XmlEnumValue("Gait") - GAIT("Gait"), - @XmlEnumValue("Keystroke") - KEYSTROKE("Keystroke"), - @XmlEnumValue("LipMovement") - LIP_MOVEMENT("LipMovement"), - @XmlEnumValue("SignatureSign") - SIGNATURE_SIGN("SignatureSign"); - - private final String value; - - BiometricType(String v) { - value = v; - } - - public String value() { - return value; - } - - public static BiometricType fromValue(String v) { - for (BiometricType c : BiometricType.values()) { - if (c.value.equalsIgnoreCase(v)) { - return c; - } - } - throw new IllegalArgumentException(v); - } - -} diff --git a/src/main/java/io/mosip/mimoto/constant/CbeffConstant.java b/src/main/java/io/mosip/mimoto/constant/CbeffConstant.java index 57becc3d7..3059b336d 100644 --- a/src/main/java/io/mosip/mimoto/constant/CbeffConstant.java +++ b/src/main/java/io/mosip/mimoto/constant/CbeffConstant.java @@ -1,10 +1,14 @@ package io.mosip.mimoto.constant; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + /** * @author Ramadurai Pandian * * A Constant Class to hold all the default identifier values */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class CbeffConstant { public static final long FORMAT_OWNER = 257; diff --git a/src/main/java/io/mosip/mimoto/constant/CredentialFormat.java b/src/main/java/io/mosip/mimoto/constant/CredentialFormat.java new file mode 100644 index 000000000..156bbba80 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/CredentialFormat.java @@ -0,0 +1,34 @@ +package io.mosip.mimoto.constant; + +public enum CredentialFormat { + VC_SD_JWT("vc+sd-jwt"), + DC_SD_JWT("dc+sd-jwt"), + LDP_VC("ldp_vc"); + + private final String format; + + CredentialFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } + + public static CredentialFormat fromString(String format) { + for (CredentialFormat cf : CredentialFormat.values()) { + if (cf.format.equalsIgnoreCase(format)) { + return cf; + } + } + throw new IllegalArgumentException("Unknown credential format: " + format); + } + + /** + * Returns true if the given format string is an SD-JWT family format (vc+sd-jwt or dc+sd-jwt). + * Both share the same SD-JWT structure, parser, and KB-JWT signing. + */ + public static boolean isSdJwt(String format) { + return VC_SD_JWT.format.equalsIgnoreCase(format) || DC_SD_JWT.format.equalsIgnoreCase(format); + } +} diff --git a/src/main/java/io/mosip/mimoto/constant/LdpVcV1Constants.java b/src/main/java/io/mosip/mimoto/constant/LdpVcV1Constants.java new file mode 100644 index 000000000..460fab330 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/LdpVcV1Constants.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.constant; + +/** + * Constants for LDP VC credentials with V1.1 context + */ +public class LdpVcV1Constants { + // Language Object and its properties + public static final String LANGUAGE = "lang"; + public static final String VALUE = "value"; +} diff --git a/src/main/java/io/mosip/mimoto/constant/LdpVcV2Constants.java b/src/main/java/io/mosip/mimoto/constant/LdpVcV2Constants.java new file mode 100644 index 000000000..3e445bef1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/LdpVcV2Constants.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.constant; + +/** + * Constants for LDP VC credentials with V2.0 context + */ +public class LdpVcV2Constants { + // Language Object and its properties + public static final String LANGUAGE = "@language"; + public static final String VALUE = "@value"; + + // Credential context and URL + public static final String CONTEXT = "@context"; + public static final String V2_CONTEXT_URL = "https://www.w3.org/ns/credentials/v2"; + + // Render method and its properties + public static final String RENDER_METHOD = "renderMethod"; + public static final String TEMPLATE = "template"; + public static final String RENDER_SUITE = "renderSuite"; + public static final String SVG_MUSTACHE_RENDER_SUITE = "svg-mustache"; +} diff --git a/src/main/java/io/mosip/mimoto/constant/LoggerFileConstant.java b/src/main/java/io/mosip/mimoto/constant/LoggerFileConstant.java index ef82a0f6c..2b1db6a98 100644 --- a/src/main/java/io/mosip/mimoto/constant/LoggerFileConstant.java +++ b/src/main/java/io/mosip/mimoto/constant/LoggerFileConstant.java @@ -7,5 +7,9 @@ public enum LoggerFileConstant { REFFERENCEID, USERID, APPLICATIONID, - UIN + UIN; + public static final String REST_CLIENT_SERVICE_IMPL_POST_API_ENTRY = "RestClientServiceImpl::postApi()::entry"; + public static final String REST_CLIENT_SERVICE_IMPL_POST_API_EXIT = "RestClientServiceImpl::postApi()::exit"; + + public static final String DELIMITER = " --> "; } diff --git a/src/main/java/io/mosip/mimoto/constant/MappingJsonConstants.java b/src/main/java/io/mosip/mimoto/constant/MappingJsonConstants.java index 4f8d825f2..a484d53e4 100644 --- a/src/main/java/io/mosip/mimoto/constant/MappingJsonConstants.java +++ b/src/main/java/io/mosip/mimoto/constant/MappingJsonConstants.java @@ -1,5 +1,9 @@ package io.mosip.mimoto.constant; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class MappingJsonConstants { public static final String IDENTITY = "identity"; diff --git a/src/main/java/io/mosip/mimoto/constant/OpenID4VPConstants.java b/src/main/java/io/mosip/mimoto/constant/OpenID4VPConstants.java new file mode 100644 index 000000000..554cb1419 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/OpenID4VPConstants.java @@ -0,0 +1,70 @@ +package io.mosip.mimoto.constant; + +/** + * Constants for OpenID4VP structure keys used in presentation definitions, authorization requests, and VP session data. + */ +public class OpenID4VPConstants { + + // VP Session data keys + public static final String CREATED_AT = "createdAt"; + public static final String OPENID4VP_INSTANCE = "openID4VPInstance"; + + // Authorization request structure keys + public static final String AUTHORIZATION_REQUEST = "authorizationRequest"; + public static final String AUTHORIZATION_REQUEST_URL = "authorizationRequestUrl"; + + // Presentation definition structure keys + public static final String PRESENTATION_DEFINITION = "presentationDefinition"; + public static final String ID = "id"; + public static final String INPUT_DESCRIPTORS = "inputDescriptors"; + public static final String FORMAT = "format"; + public static final String CONSTRAINTS = "constraints"; + public static final String LIMIT_DISCLOSURE = "limitDisclosure"; + public static final String FIELDS = "fields"; + public static final String PATH = "path"; + public static final String FILTER = "filter"; + public static final String TYPE = "type"; + public static final String PATTERN = "pattern"; + + // VP Token signing result keys + public static final String JWS = "jws"; + public static final String PROOF_VALUE = "proofValue"; + public static final String SIGNATURE_ALGORITHM = "signatureAlgorithm"; + public static final String SIGNATURE = "signature"; + public static final String MDOC_AUTHENTICATION_ALGORITHM = "mdocAuthenticationAlgorithm"; + public static final String DOCUMENT_TYPE_SIGNATURES = "documentTypeSignatures"; + + // MSO_MDOC specific keys + public static final String DOC_TYPE_TO_DEVICE_AUTHENTICATION_BYTES = "docTypeToDeviceAuthenticationBytes"; + + // Algorithm names + public static final String ALGORITHM_ES256 = "ES256"; + public static final String ALGORITHM_EDDSA = "EdDSA"; + + // JWT separators and delimiters + public static final String DETACHED_JWT_SEPARATOR = ".."; + + // JWT header critical parameters + public static final String JWT_CRITICAL_PARAM_B64 = "b64"; + + // DID (Decentralized Identifier) related constants + public static final String DID_JWK_PREFIX = "did:jwk:"; + public static final String DID_KEY_FRAGMENT = "#0"; + + // Response status constants + public static final String STATUS_SUCCESS = "success"; + public static final String STATUS_ERROR = "error"; + + // Response message constants + public static final String MESSAGE_PRESENTATION_SUCCESS = "Presentation successfully submitted"; + public static final String MESSAGE_PRESENTATION_SHARE_FAILED = "Failed to submit Verifiable Presentation"; + + // Credential selection keys + public static final String SELECTED_CREDENTIALS = "selectedCredentials"; + public static final String SELECTED_SD_CLAIMS = "selectedSdClaims"; + + // URL query parameter names for OpenID4VP authorization requests + public static final String CLIENT_ID_PARAM = "client_id"; + public static final String RESPONSE_URI_PARAM = "response_uri"; +} + diff --git a/src/main/java/io/mosip/mimoto/constant/SdJwtVcConstants.java b/src/main/java/io/mosip/mimoto/constant/SdJwtVcConstants.java new file mode 100644 index 000000000..1bfde734c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SdJwtVcConstants.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.constant; + +/** + * Constants for Sd-Jwt credentials + */ +public class SdJwtVcConstants { + // Language Object and its properties (custom implementation for SD-JWT VCs) + public static final String LANGUAGE = "language"; + public static final String VALUE = "value"; +} diff --git a/src/main/java/io/mosip/mimoto/constant/SessionKeys.java b/src/main/java/io/mosip/mimoto/constant/SessionKeys.java new file mode 100644 index 000000000..08553b416 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SessionKeys.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.constant; + +public class SessionKeys { + // User session keys + public static final String USER_ID = "userId"; + public static final String USER_METADATA = "userMetadata"; + public static final String WALLET_KEY = "wallet_key"; + public static final String WALLET_ID = "wallet_id"; + public static final String CLIENT_REGISTRATION_ID = "clientRegistrationId"; + + public static final String PRESENTATIONS = "presentations"; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/constant/SigningAlgorithm.java b/src/main/java/io/mosip/mimoto/constant/SigningAlgorithm.java new file mode 100644 index 000000000..18b229c3f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SigningAlgorithm.java @@ -0,0 +1,37 @@ +package io.mosip.mimoto.constant; + +import com.nimbusds.jose.JWSAlgorithm; + +public enum SigningAlgorithm { + RS256(JWSAlgorithm.RS256, "RSA"), + ES256(JWSAlgorithm.ES256, "EC"), + ES256K(JWSAlgorithm.ES256K, "EC"), + ED25519(JWSAlgorithm.Ed25519, "Ed25519"); + + private final JWSAlgorithm jwsAlgorithm; + private final String keyFactoryAlgorithm; + + SigningAlgorithm(JWSAlgorithm jwsAlgorithm, String keyFactoryAlgorithm) { + this.jwsAlgorithm = jwsAlgorithm; + this.keyFactoryAlgorithm = keyFactoryAlgorithm; + } + + public JWSAlgorithm getJWSAlgorithm() { + return jwsAlgorithm; + } + + public String getKeyFactoryAlgorithm() { + return keyFactoryAlgorithm; + } + + public static SigningAlgorithm fromString(String value) { + return switch (value.toUpperCase()) { + case "RS256" -> RS256; + case "ES256" -> ES256; + case "ES256K" -> ES256K; + case "ED25519", "EDDSA" -> ED25519; + default -> throw new IllegalArgumentException("Unsupported signing algorithm: " + value); + }; + } + +} diff --git a/src/main/java/io/mosip/mimoto/constant/SigningAlgorithmConstants.java b/src/main/java/io/mosip/mimoto/constant/SigningAlgorithmConstants.java new file mode 100644 index 000000000..14e37ab2d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SigningAlgorithmConstants.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.constant; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class SigningAlgorithmConstants { + + // Algorithm Names + public static final String RSA = "RSA"; + public static final String EC = "EC"; + public static final String ED25519 = "Ed25519"; + + // EC curve names (JCA) + public static final String CURVE_SECP256K1 = "secp256k1"; + public static final String CURVE_SECP256R1 = "secp256r1"; + + // Provider Names + public static final String BC_PROVIDER = "BC"; +} diff --git a/src/main/java/io/mosip/mimoto/constant/SwaggerExampleConstants.java b/src/main/java/io/mosip/mimoto/constant/SwaggerExampleConstants.java new file mode 100644 index 000000000..8174ed3ee --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SwaggerExampleConstants.java @@ -0,0 +1,74 @@ +package io.mosip.mimoto.constant; + +public class SwaggerExampleConstants { + + public static final String ALL_PROPERTIES_EXAMPLE = """ + { + "response": { + "modelDownloadMaxRetry": "10", + "audience": "ida-binding", + "datashare.host": "https://datashare-inji.collab.mosip.net", + "allowedInternalAuthType": "otp,bio-Finger,bio-Iris,bio-Face", + "openId4VCIDownloadVCTimeout": "30000", + "qr.code.height": "650", + "ovp.error.redirect.url.pattern": "%s?error=%s&error_description=%s", + "vcDownloadMaxRetry": "10", + "ovp.qrdata.pattern": "INJI_OVP://https://injiweb.collab.mosip.net/authorize?response_type=vp_token&resource=%s&presentation_definition=%s", + "minStorageRequiredForAuditEntry": "2", + "minStorageRequired": "2", + "vcDownloadPoolInterval": "6000", + "issuer": "residentapp", + "ovp.redirect.url.pattern": "%s#vp_token=%s&presentation_submission=%s", + "allowedAuthType": "demo,otp,bio-Finger,bio-Iris,bio-Face", + "allowedEkycAuthType": "demo,otp,bio-Finger,bio-Iris,bio-Face", + "warningDomainName": "https://api.collab.mosip.net", + "qr.code.width": "650", + "web.host": "https://injiweb.collab.mosip.net", + "web.redirect.url": "https://injiweb.collab.mosip.net/authorize", + "aboutInjiUrl": "https://docs.mosip.io/inji/inji-mobile-wallet/overview", + "qr.data.size.limit": "10000", + "faceSdkModelUrl": "https://api.collab.mosip.net/inji" + }, + "errors": [] + } + """; + + public static final String FETCH_USER_PROFILE_FROM_DB_SUCCESS = """ + { + "display_name": "John Doe", + "profile_picture_url": "https://example.com/profile.jpg", + "email": "john.doe@example.com", + } + """; + public static final String FETCH_USER_CACHE_PROFILE_SUCCESS = """ + { + "display_name": "John Doe", + "profile_picture_url": "https://example.com/profile.jpg", + "email": "john.doe@example.com", + "walletId": "123e4567-e89b-12d3-a456-426614174000" + } + """; + + public static final String FETCH_ALL_WALLETS_OF_USER_SUCCESS = """ + [ + { + "walletId": "123e4567-e89b-12d3-a456-426614174000" + }, + { + "walletId": "223e4567-e89b-12d3-a456-426614174001" + } + ] + """; + + public static final String FETCH_ALL_CREDENTIALS_OF_WALLET_SUCCESS = """ + [ + { + "issuer_name": "Mosip", + "issuer_logo": "https://example.com/logo.png", + "credential_type": "MosipVerifiableCredential", + "credential_type_logo": "https://example.com/credential-logo.png", + "credential_id": "1234567890" + } + ] + """; +} diff --git a/src/main/java/io/mosip/mimoto/constant/SwaggerLiteralConstants.java b/src/main/java/io/mosip/mimoto/constant/SwaggerLiteralConstants.java new file mode 100644 index 000000000..a062b02eb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/SwaggerLiteralConstants.java @@ -0,0 +1,169 @@ +package io.mosip.mimoto.constant; + +public class SwaggerLiteralConstants { + + /* Attestation Controller */ + public static final String ATTESTATION_NAME = "Attestation"; + public static final String ATTESTATION_DESCRIPTION = "All the attestation related endpoints"; + + /* Common Inji Controller */ + public static final String COMMON_INJI_NAME = "Inji Wallet Properties"; + public static final String COMMON_INJI_DESCRIPTION = "All endpoints related to Inji Wallet properties"; + public static final String COMMON_INJI_GET_PROPERTIES_SUMMARY = "Retrieve all Inji Wallet properties"; + public static final String COMMON_INJI_GET_PROPERTIES_DESCRIPTION = "This endpoint allow you to retrieve all the Inji Wallet properties"; + + /* Credentials Controller */ + public static final String CREDENTIALS_NAME = "Credentials download using OpenId4VCI"; + public static final String CREDENTIALS_DESCRIPTION = "All the credentials related endpoints"; + public static final String CREDENTIALS_DOWNLOAD_VC_SUMMARY = "Download credentials as PDF"; + public static final String CREDENTIALS_DOWNLOAD_VC_DESCRIPTION = "This endpoint allow you to download the credentials as PDF"; + + /* Credentials Share Controller */ + public static final String CREDENTIALS_SHARE_NAME = "Credential Share"; + public static final String CREDENTIALS_SHARE_DESCRIPTION = "All the credential download endpoints"; + public static final String CREDENTIALS_SHARE_HANDLE_SUBSCRIBED_EVENT_SUMMARY = "Notify through web sub once credential is downloaded"; + public static final String CREDENTIALS_SHARE_HANDLE_SUBSCRIBED_EVENT_DESCRIPTION = "This endpoint allow web sub to callback once the credential is issued"; + public static final String CREDENTIALS_SHARE_REQUEST_VC_SUMMARY = "request for credential issue"; + public static final String CREDENTIALS_SHARE_REQUEST_VC_DESCRIPTION = "This endpoint allow you to request for credential issue"; + public static final String CREDENTIALS_SHARE_REQUEST_VC_STATUS_SUMMARY = "polling for credential issue status"; + public static final String CREDENTIALS_SHARE_REQUEST_VC_STATUS_DESCRIPTION = "This endpoint allow you to poll for credential issue status"; + public static final String CREDENTIALS_SHARE_DOWNLOAD_VC_SUMMARY = "Download the credential using OTP Flow"; + public static final String CREDENTIALS_SHARE_DOWNLOAD_VC_DESCRIPTION = "This endpoint allow you to download credential issued"; + + /* IDP Controller */ + public static final String IDP_NAME = "Wallet Binding"; + public static final String IDP_DESCRIPTION = "All the authorization related endpoints"; + public static final String IDP_BINDING_OTP_SUMMARY = "Invoke OTP request for wallet binding"; + public static final String IDP_BINDING_OTP_DESCRIPTION = "This endpoint allow you to invoke OTP for wallet binding"; + public static final String IDP_WALLET_BINDING_SUMMARY = "Wallet Binding"; + public static final String IDP_WALLET_BINDING_DESCRIPTION = "This endpoint allow you to perform the wallet binding"; + public static final String IDP_GET_TOKEN_SUMMARY = "Retrieve accessToken for OIDC flow"; + public static final String IDP_GET_TOKEN_DESCRIPTION = "This endpoint allow you to retrieve the access token in exchange for authorization code"; + public static final String IDP_GET_TOKEN_V2_SUMMARY = "Retrieve accessToken for OIDC flow with DPoP support"; + public static final String IDP_GET_TOKEN_V2_DESCRIPTION = "This endpoint allows you to retrieve access token in exchange for authorization code and return the response received from authorization server as is"; + + /* Issuers Controller */ + public static final String ISSUERS_NAME = "Issuers"; + public static final String ISSUERS_DESCRIPTION = "All the issuers related endpoints"; + public static final String ISSUERS_GET_ISSUERS_SUMMARY = "Retrieve all onboarded issuers"; + public static final String ISSUERS_GET_ISSUERS_DESCRIPTION = "This endpoint allow you to retrieve all the onboarded issuers"; + public static final String ISSUERS_GET_SPECIFIC_ISSUER_SUMMARY = "Retrieve specific issuer's config"; + public static final String ISSUERS_GET_SPECIFIC_ISSUER_DESCRIPTION = "This endpoint allow you to retrieve the complete configuration of the specific issuer"; + public static final String ISSUERS_GET_ISSUER_WELLKNOWN_SUMMARY = "Retrieve specific issuer's well known"; + public static final String ISSUERS_GET_ISSUER_WELLKNOWN_DESCRIPTION = "This endpoint allow you to retrieve the well known of the specific issuer. Since version 0.16.0, this endpoint is deprecated and will be removed in a future release. Use issuers new endpoint issuers/{issuer-id}/configuration instead."; + public static final String ISSUERS_GET_ISSUER_CONFIGURATION_SUMMARY = "Retrieve specific issuer's and its corresponding authorization server well-known config"; + public static final String ISSUERS_GET_ISSUER_CONFIGURATION_DESCRIPTION = "This endpoint allows you to retrieve the well-known configuration of a specific issuer and its corresponding authorization server"; + + /* Issuers V2 Controller */ + public static final String ISSUERS_V2_NAME = "Issuers V2"; + public static final String ISSUERS_V2_DESCRIPTION = "V2 API for issuers with minimal responses"; + public static final String ISSUERS_V2_GET_ISSUERS_SUMMARY = "Retrieve all onboarded issuers (V2)"; + public static final String ISSUERS_V2_GET_ISSUERS_DESCRIPTION = "Retrieve all onboarded issuers in V2 response format with issuer_id, client_id, credential_issuer_host, qr_code_type, token_endpoint"; + public static final String ISSUERS_V2_GET_ISSUER_SUMMARY = "Retrieve specific issuer (V2)"; + public static final String ISSUERS_V2_GET_ISSUER_DESCRIPTION = "Retrieve a single issuer in V2 response format by issuer ID"; + + /* Prensentation Controller */ + public static final String PRESENTATION_NAME = "Presentation"; + public static final String PRESENTATION_DESCRIPTION = "All the online sharing related endpoints"; + public static final String PRESENTATION_AUTHORIZE_SUMMARY = "Perform the authorization"; + public static final String PRESENTATION_AUTHORIZE_DESCRIPTION = "This endpoint allow you to redirect the token back to the caller post authorization"; + + /* Resident Service Controller */ + public static final String RESIDENT_NAME = "Resident Service"; + public static final String RESIDENT_DESCRIPTION = "All the resident service related endpoints"; + public static final String RESIDENT_REQUEST_OTP_SUMMARY = "Request for OTP"; + public static final String RESIDENT_REQUEST_OTP_DESCRIPTION = "This endpoint allow you to request OTP for credential download"; + public static final String RESIDENT_REQUEST_INDIVIDUALID_OTP_SUMMARY = "Request OTP for retrieving Individual Id"; + public static final String RESIDENT_REQUEST_INDIVIDUALID_OTP_DESCRIPTION = "This endpoint allow you to request OTP to retrieve Individual Id"; + public static final String RESIDENT_GET_INDIVIDUALID_SUMMARY = "Retrieve Individual Id using AID"; + public static final String RESIDENT_GET_INDIVIDUALID_DESCRIPTION = "This endpoint allow you to retrieve the Individual Id using AID"; + + /* Verifiers Controller */ + public static final String VERIFIERS_NAME = "Verifiers"; + public static final String VERIFIERS_DESCRIPTION = "All the verifiers related endpoints"; + public static final String VERIFIERS_GET_VERIFIERS_SUMMARY = "Retrieve all trusted verifiers"; + public static final String VERIFIERS_GET_VERIFIERS_DESCRIPTION = "This endpoint allow you to retrieve all the trusted verifiers"; + + /* Users Controller */ + public static final String USERS_NAME = "Users"; + public static final String USERS_DESCRIPTION = "All the User Profile related endpoints"; + + /* Wallets Controller */ + public static final String RETRIEVE_ALL_WALLETS_SUMMARY = "Retrieve all wallets for the user"; + public static final String RETRIEVE_ALL_WALLETS_DESCRIPTION = "This API is secured using session-based authentication. The session ID is extracted from the Cookie header to authenticate the user. The user's ID is obtained from the session stored in Redis, and all wallets associated with the user are fetched from the database. If successful, the list of wallets is returned; otherwise, an appropriate error response is returned."; + + public static final String WALLETS_NAME = "Wallets"; + public static final String WALLETS_DESCRIPTION = "All the Wallet related endpoints"; + public static final String WALLETS_DELETE_SUMMARY = "Delete a Wallet"; + public static final String WALLETS_DELETE_DESCRIPTION = "This endpoint allows you to delete a specific wallet. The API is secured using session-based authentication. The session ID is extracted from the Cookie header to authenticate the user. The user's ID is obtained from the session and used to validate ownership of the Wallet before deletion. If the Wallet is successfully deleted, a 200 OK response is returned; otherwise, an appropriate error response is returned."; + + /* Wallet Credentials Controller */ + public static final String WALLET_CREDENTIALS_NAME = "Wallet Credentials"; + public static final String WALLET_CREDENTIALS_DESCRIPTION = "All the Wallet Credentials related endpoints"; + public static final String WALLET_CREDENTIALS_DELETE_SUMMARY = "Delete a credential from a wallet"; + public static final String WALLET_CREDENTIALS_DELETE_DESCRIPTION = "This endpoint allows you to delete a specific credential from a wallet"; + public static final String WALLET_CREDENTIALS_FETCH_ALL_SUMMARY = "Fetch all credentials for a wallet"; + public static final String WALLET_CREDENTIALS_FETCH_ALL_DESCRIPTION = "This endpoint allows you to retrieve all credentials for a specific wallet"; + + /* OAuth2 ID Token Authentication Controller */ + public static final String ID_TOKEN_AUTHENTICATION_NAME = "OAuth2 ID Token Authentication"; + public static final String ID_TOKEN_AUTHENTICATION_DESCRIPTION = "All the OAuth2 ID Token Authentication related endpoints"; + + /* Wallet Presentations Controller */ + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUMMARY = "Processes Verifiable Presentation Authorization Request and provides details about the verifier and presentation."; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_DESCRIPTION = "This API is secured using session-based authentication. Upon receiving a request, the session is first retrieved using the session ID extracted from the Cookie header to authenticate the user. Once authenticated, the API processes the received Verifiable Presentation Authorization Request from the Verifier for a specific wallet. It validates the session, verifies the authenticity of the request, and checks if the Verifier is pre-registered and trusted by the wallet. If all validations pass, the API returns a response containing the presentation details; otherwise, an appropriate error response is returned."; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_REQ_EXAMPLE_NAME = "Verifier Verifiable Presentation Authorization Request"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_REQ_EXAMPLE_VALUE = "{ \"authorizationRequestUrl\": \"client_id=mock-client&presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D\" }"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUCCESS_EXAMPLE_NAME = "Success response"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUCCESS_EXAMPLE_VALUE = "{ \"presentationId\": \"123e4567-e89b-12d3-a456-426614174000\", \"verifier\": { \"id\": \"mock-client\", \"name\": \"Requester name\", \"logo\": \"https://api.collab.mosip.net/inji/verifier-logo.png\", \"isTrusted\": true, \"isPreregisteredWithWallet\": true, \"redirectUri\": \"https://injiverify.collab.mosip.net/redirect\" } }"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_MISSING_RESPONSE_TYPE_NAME = "response_type is missing in Authorization request"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_MISSING_RESPONSE_TYPE_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Missing Input: response_type param is required\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_INVALID_WALLET_ID_NAME = "Invalid Wallet ID"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_INVALID_WALLET_ID_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_WALLET_LOCKED_NAME = "Wallet ID not found in session"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_WALLET_LOCKED_VALUE = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_401_USER_NOT_FOUND_NAME = "User ID is not present in session"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_401_USER_NOT_FOUND_VALUE = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_VERIFIERS_FETCH_FAILED_NAME = "Failed to fetch pre-registered trusted verifiers"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_VERIFIERS_FETCH_FAILED_VALUE = "{\"errorCode\": \"RESIDENT-APP-026\", \"errorMessage\": \"Api not accessible failure\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_SERVER_ERROR_NAME = "Unexpected Server Error"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_SERVER_ERROR_VALUE = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_INVALID_URI_SYNTAX_NAME = "Invalid URI syntax"; + public static final String WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_INVALID_URI_SYNTAX_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Incorrect URI parameters in the request\"}"; + + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUMMARY = "Get matching credentials for a presentation request"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_DESCRIPTION = "This API retrieves credentials from the wallet that match the presentation definition requirements. It returns available credentials that can satisfy the presentation request along with any missing claims that are required but not available."; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUCCESS_EXAMPLE_NAME = "Success response"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUCCESS_EXAMPLE_VALUE = "{ \"availableCredentials\": [{ \"credentialId\": \"cred-123\", \"credentialTypeDisplayName\": \"W3C VC\", \"credentialTypeLogo\": \"https://mosip.github.io/inji-config/logos/mosipid-logo.png\", \"type\": [\"IDCredential\"], \"claims\": { \"birthdate\": \"1990-01-01\" } }], \"missingClaims\": [] }"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_INVALID_WALLET_ID_NAME = "Invalid Wallet ID"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_INVALID_WALLET_ID_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_PRESENTATION_NOT_FOUND_NAME = "Presentation not found"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_PRESENTATION_NOT_FOUND_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Presentation not found in session\"}"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_401_USER_NOT_FOUND_NAME = "User ID is not present in session"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_401_USER_NOT_FOUND_VALUE = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_500_SERVER_ERROR_NAME = "Unexpected Server Error"; + public static final String WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_500_SERVER_ERROR_VALUE = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"; + + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_SUMMARY = "Submit presentation or reject verifier"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_DESCRIPTION = "This API handles both presentation submission and verifier rejection based on the request content. For submission: include selectedCredentials array, and optionally selectedSdClaims to selectively disclose the chosen claims for SD-JWT credentials (vc+sd-jwt and dc+sd-jwt). Only the explicitly selected SD-JWT disclosures are shared; when selectedSdClaims is omitted (or empty) for an SD-JWT credential, none of its selectively-disclosable claims are shared. For rejection: include errorCode and errorMessage fields."; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_EXAMPLE_NAME = "Submit Presentation"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_EXAMPLE_VALUE = "{ \"selectedCredentials\": [\"cred-123\", \"cred-456\"] }"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_SDJWT_EXAMPLE_NAME = "Submit Presentation (SD-JWT with selective disclosure)"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_SDJWT_EXAMPLE_VALUE = "{ \"selectedCredentials\": [\"cred-123\"], \"selectedSdClaims\": { \"cred-123\": [\"name\", \"dob\"] } }"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_REJECT_EXAMPLE_NAME = "Reject Verifier"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_REJECT_EXAMPLE_VALUE = "{ \"errorCode\": \"access_denied\", \"errorMessage\": \"User denied authorization to share credentials\" }"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_200_SUBMITTED_NAME = "Presentation submitted"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_200_SUBMITTED_VALUE = "{ \"presentationId\": \"presentation-123\", \"status\": \"SUCCESS\", \"message\": \"Presentation successfully submitted and shared with verifier\" }"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_200_REJECTED_NAME = "Verifier rejected"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_200_REJECTED_VALUE = "{\"status\": \"success\", \"message\": \"Presentation request rejected. An OpenID4VP error response has been sent to the verifier.\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_FORMAT_NAME = "Invalid request format"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_FORMAT_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Request must contain either selectedCredentials or both errorCode and errorMessage\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_WALLET_ID_NAME = "Invalid Wallet ID"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_WALLET_ID_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_PRESENTATION_NOT_FOUND_NAME = "Presentation not found"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_400_PRESENTATION_NOT_FOUND_VALUE = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Presentation not found in session\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_401_UNAUTHORIZED_NAME = "unauthorized"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_401_UNAUTHORIZED_VALUE = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_500_SERVER_ERROR_NAME = "Server error"; + public static final String WALLET_PRESENTATIONS_HANDLE_ACTION_500_SERVER_ERROR_VALUE = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"; +} diff --git a/src/main/java/io/mosip/mimoto/constant/VCSpecificationVersion.java b/src/main/java/io/mosip/mimoto/constant/VCSpecificationVersion.java new file mode 100644 index 000000000..168c7d0a5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/constant/VCSpecificationVersion.java @@ -0,0 +1,15 @@ +package io.mosip.mimoto.constant; + +import lombok.Getter; + +@Getter +public enum VCSpecificationVersion { + DRAFT_13("draft-13"), + V1("v1"); + + private final String version; + + VCSpecificationVersion(String version) { + this.version = version; + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/AttestationServiceController.java b/src/main/java/io/mosip/mimoto/controller/AttestationServiceController.java index ca4a52afb..9da08703f 100644 --- a/src/main/java/io/mosip/mimoto/controller/AttestationServiceController.java +++ b/src/main/java/io/mosip/mimoto/controller/AttestationServiceController.java @@ -1,7 +1,12 @@ package io.mosip.mimoto.controller; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.dto.mimoto.AttestationStatement; +import io.mosip.mimoto.dto.mimoto.GenericResponseDTO; +import io.mosip.mimoto.util.AttestationOfflineVerify; +import io.mosip.mimoto.util.AttestationOnlineVerify; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -10,25 +15,19 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.dto.mimoto.AttestationStatement; -import io.mosip.mimoto.dto.mimoto.GenericResponseDTO; -import io.mosip.mimoto.util.AttestationOfflineVerify; -import io.mosip.mimoto.util.AttestationOnlineVerify; -import io.mosip.mimoto.util.LoggerUtil; - -@SpringBootApplication @RestController @RequestMapping(value = "/safetynet") +@Tag(name = SwaggerLiteralConstants.ATTESTATION_NAME, description = SwaggerLiteralConstants.ATTESTATION_DESCRIPTION) public class AttestationServiceController { - private final Logger logger = LoggerUtil.getLogger(AttestationServiceController.class); + private final AttestationOfflineVerify attestationOfflineVerify; - @Autowired - AttestationOfflineVerify attestationOfflineVerify; + private final AttestationOnlineVerify attestationOnlineVerify; - @Autowired - AttestationOnlineVerify attestationOnlineVerify; + public AttestationServiceController(AttestationOfflineVerify attestationOfflineVerify, AttestationOnlineVerify attestationOnlineVerify) { + this.attestationOfflineVerify = attestationOfflineVerify; + this.attestationOnlineVerify = attestationOnlineVerify; + } /** * Safetynet attestation verify using offline method. @@ -36,8 +35,9 @@ public class AttestationServiceController { * @param attestation * @return */ + @Operation(hidden = true) @PostMapping(path = "/offline/verify", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity processOffline(@RequestBody String attestation) + public ResponseEntity processOffline(@RequestBody String attestation) { try { AttestationStatement attestationStatement = attestationOfflineVerify.parseAndVerify(attestation); @@ -46,7 +46,7 @@ public ResponseEntity processOffline(@RequestBody String attestation) GenericResponseDTO responseDTO = new GenericResponseDTO(); responseDTO.setStatus("Error"); responseDTO.setMessage(e.getMessage()); - + return new ResponseEntity<>(responseDTO, HttpStatus.OK); } } @@ -57,8 +57,9 @@ public ResponseEntity processOffline(@RequestBody String attestation) * @param attestation * @return */ + @Operation(hidden = true) @PostMapping(path = "/online/verify", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity processOnline(@RequestBody String attestation) + public ResponseEntity processOnline(@RequestBody String attestation) { try { AttestationStatement attestationStatement = attestationOnlineVerify.parseAndVerify(attestation); @@ -67,7 +68,7 @@ public ResponseEntity processOnline(@RequestBody String attestation) GenericResponseDTO responseDTO = new GenericResponseDTO(); responseDTO.setStatus("Error"); responseDTO.setMessage(e.getMessage()); - + return new ResponseEntity<>(responseDTO, HttpStatus.OK); } } diff --git a/src/main/java/io/mosip/mimoto/controller/CommonInjiController.java b/src/main/java/io/mosip/mimoto/controller/CommonInjiController.java new file mode 100644 index 000000000..abf6214c0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/CommonInjiController.java @@ -0,0 +1,40 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerExampleConstants; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@Tag(name = SwaggerLiteralConstants.COMMON_INJI_NAME, description = SwaggerLiteralConstants.COMMON_INJI_DESCRIPTION) +public class CommonInjiController { + + private final Map injiConfig; + + public CommonInjiController(Map injiConfig) { + this.injiConfig = injiConfig; + } + + @Operation( summary = SwaggerLiteralConstants.COMMON_INJI_GET_PROPERTIES_SUMMARY, description = SwaggerLiteralConstants.COMMON_INJI_GET_PROPERTIES_DESCRIPTION) + @ApiResponses({@ApiResponse(responseCode = "200",content = { @Content(schema = @Schema(implementation = ResponseWrapper.class), + examples = @ExampleObject( value = SwaggerExampleConstants.ALL_PROPERTIES_EXAMPLE),mediaType = "application/json") } ) }) + @GetMapping(value = "/allProperties", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity>> getAllProperties() { + ResponseWrapper> responseWrapper = new ResponseWrapper<>(); + responseWrapper.setResponse(injiConfig); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/CredentialShareController.java b/src/main/java/io/mosip/mimoto/controller/CredentialShareController.java index 23f1cb5a2..880ccd7d0 100644 --- a/src/main/java/io/mosip/mimoto/controller/CredentialShareController.java +++ b/src/main/java/io/mosip/mimoto/controller/CredentialShareController.java @@ -1,30 +1,10 @@ package io.mosip.mimoto.controller; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.Gson; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.env.Environment; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import io.mosip.kernel.core.logger.spi.Logger; import io.mosip.kernel.websub.api.annotation.PreAuthenticateContentAndVerifyIntent; import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; import io.mosip.mimoto.core.http.RequestWrapper; import io.mosip.mimoto.core.http.ResponseWrapper; import io.mosip.mimoto.dto.mimoto.AppCredentialRequestDTO; @@ -37,28 +17,37 @@ import io.mosip.mimoto.model.EventModel; import io.mosip.mimoto.service.RestClientService; import io.mosip.mimoto.service.impl.CredentialShareServiceImpl; -import io.mosip.mimoto.util.CryptoCoreUtil; -import io.mosip.mimoto.util.DateUtils; -import io.mosip.mimoto.util.LoggerUtil; -import io.mosip.mimoto.util.Utilities; +import io.mosip.mimoto.util.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; @RestController @RequestMapping(value = "/credentialshare") +@Slf4j +@Tag(name = SwaggerLiteralConstants.CREDENTIALS_SHARE_NAME, description = SwaggerLiteralConstants.CREDENTIALS_SHARE_DESCRIPTION) public class CredentialShareController { - private Logger logger = LoggerUtil.getLogger(CredentialShareController.class); - - @Autowired - private CredentialShareServiceImpl credentialShareService; + private final CredentialShareServiceImpl credentialShareService; - @Autowired - public RestClientService restClientService; + private final RestClientService restClientService; - @Autowired - Environment env; - - @Value("${mosip.event.topic}") - private String topic; + private final Environment env; @Value("${mosip.partner.encryption.key}") private String partnerEncryptionKey; @@ -66,13 +55,19 @@ public class CredentialShareController { @Value("${mosip.partner.id}") private String partnerId; - @Autowired - public CryptoCoreUtil cryptoCoreUtil; + private final RequestValidator requestValidator; private Gson gson = new Gson(); - @Autowired - private Utilities utilities; + private final Utilities utilities; + + public CredentialShareController(CredentialShareServiceImpl credentialShareService, RestClientService restClientService, Environment env, RequestValidator requestValidator, Utilities utilities) { + this.credentialShareService = credentialShareService; + this.restClientService = restClientService; + this.env = env; + this.requestValidator = requestValidator; + this.utilities = utilities; + } /** * Websub callback for Verifiable Credential share. @@ -81,26 +76,27 @@ public class CredentialShareController { * @return * @throws Exception */ - @PostMapping(path = "/callback/notify", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthenticateContentAndVerifyIntent(secret = "${mosip.event.secret}", callback = "${mosip.event.callBackUrl}", topic = "${mosip.event.topic}") + @PostMapping(path = {"/callback/notify", "/callback/notify/"},consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthenticateContentAndVerifyIntent(secret = "${mosip.event.secret}", callback = "/v1/mimoto/credentialshare/callback/notify", topic = "${mosip.event.topic}") + @Operation(summary = SwaggerLiteralConstants.CREDENTIALS_SHARE_HANDLE_SUBSCRIBED_EVENT_SUMMARY, description = SwaggerLiteralConstants.CREDENTIALS_SHARE_HANDLE_SUBSCRIBED_EVENT_DESCRIPTION) public ResponseEntity handleSubscribeEvent(@RequestBody EventModel eventModel) throws Exception { - logger.info("Received websub event:: transaction id = " + eventModel.getEvent().getTransactionId()); + log.info("Received websub event:: transaction id = " + eventModel.getEvent().getTransactionId()); GenericResponseDTO responseDTO = new GenericResponseDTO(); Path vcRequestIdPath = Path.of( utilities.getDataPath(), String.format(CredentialShareServiceImpl.VC_REQUEST_FILE_NAME, eventModel.getEvent().getTransactionId()) ); - // Only process event if request id file exists in the storange. + // Only process event if request id file exists in the storage. if (vcRequestIdPath.toFile().exists()) { boolean documentGenerated = credentialShareService.generateDocuments(eventModel); - logger.info("Credential share process status: {} for event id: {}", documentGenerated, eventModel.getEvent().getId()); + log.info("Credential share process status: {} for event id: {}", documentGenerated, eventModel.getEvent().getId()); } else { - logger.warn("Event transaction id could not found in the storage, skipping..."); + log.warn("Event transaction id could not found in the storage, skipping..."); } responseDTO.setStatus("OK"); responseDTO.setMessage("Successfully issued."); - return new ResponseEntity<>(responseDTO, HttpStatus.OK); + return new ResponseEntity(responseDTO, HttpStatus.OK); } /** @@ -111,8 +107,14 @@ public ResponseEntity handleSubscribeEvent(@RequestBody Even * @throws Exception */ @PostMapping(path = "/request", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity request(@RequestBody AppCredentialRequestDTO requestDTO) + @Operation(summary = SwaggerLiteralConstants.CREDENTIALS_SHARE_REQUEST_VC_SUMMARY, description = SwaggerLiteralConstants.CREDENTIALS_SHARE_REQUEST_VC_DESCRIPTION) + public ResponseEntity request(@RequestBody AppCredentialRequestDTO requestDTO) throws Exception { + + if (StringUtils.isEmpty(requestDTO.getIndividualId())) { + log.error("Received empty individual id for transaction id - " + requestDTO.getTransactionID()); + } + // Call mosip resident service to issue a new credential RequestWrapper mosipCredentialRequestPayload = new RequestWrapper(); @@ -154,8 +156,9 @@ public ResponseEntity request(@RequestBody AppCredentialRequestDTO reque * @throws Exception */ @GetMapping(path = "/request/status/{requestId}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = SwaggerLiteralConstants.CREDENTIALS_SHARE_REQUEST_VC_STATUS_SUMMARY, description = SwaggerLiteralConstants.CREDENTIALS_SHARE_REQUEST_VC_STATUS_DESCRIPTION) @SuppressWarnings("unchecked") - public ResponseEntity requestStatus(@PathVariable String requestId) + public ResponseEntity> requestStatus(@PathVariable("requestId") String requestId) throws Exception { List pathSegment = new ArrayList(); @@ -176,14 +179,18 @@ public ResponseEntity requestStatus(@PathVariable String requestId) * @throws Exception */ @PostMapping(path = "/download", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity download(@RequestBody CredentialDownloadRequestDTO requestDTO) + @Operation(summary = SwaggerLiteralConstants.CREDENTIALS_SHARE_DOWNLOAD_VC_SUMMARY, description = SwaggerLiteralConstants.CREDENTIALS_SHARE_DOWNLOAD_VC_DESCRIPTION) + public ResponseEntity download(@Valid @RequestBody CredentialDownloadRequestDTO requestDTO, BindingResult result) throws Exception { try { + requestValidator.validateInputRequest(result); JsonNode decryptedCredentialJSON = utilities.getDecryptedVC(requestDTO.getRequestId()); - + JsonNode requestedCredentialJSON = utilities.getRequestVC(requestDTO.getRequestId()); JsonNode credentialJSON = utilities.getVC(requestDTO.getRequestId()); + // Combine original encrypted verifiable credential and decrypted if (decryptedCredentialJSON != null && credentialJSON != null) { + requestValidator.validateCredentialDownloadRequest(requestDTO, requestedCredentialJSON); CredentialDownloadResponseDTO credentialDownloadBody = new CredentialDownloadResponseDTO(); credentialDownloadBody.setCredential(decryptedCredentialJSON); credentialDownloadBody.setVerifiableCredential(credentialJSON); diff --git a/src/main/java/io/mosip/mimoto/controller/CredentialsController.java b/src/main/java/io/mosip/mimoto/controller/CredentialsController.java new file mode 100644 index 000000000..ed9ba9a7d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/CredentialsController.java @@ -0,0 +1,88 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.exception.VCVerificationException; +import io.mosip.mimoto.service.CredentialService; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Map; + +import static io.mosip.mimoto.exception.PlatformErrorMessages.MIMOTO_PDF_SIGN_EXCEPTION; + +@RestController +@RequestMapping(value = "/credentials") +@Slf4j +@Tag(name = SwaggerLiteralConstants.CREDENTIALS_NAME, description = SwaggerLiteralConstants.CREDENTIALS_DESCRIPTION) +public class CredentialsController { + + private final CredentialService credentialService; + + private final IdpService idpService; + + public CredentialsController(CredentialService credentialService, IdpService idpService) { + this.credentialService = credentialService; + this.idpService = idpService; + } + + @Operation(summary = SwaggerLiteralConstants.CREDENTIALS_DOWNLOAD_VC_SUMMARY, description = SwaggerLiteralConstants.CREDENTIALS_DOWNLOAD_VC_DESCRIPTION) + @ApiResponses({ + @ApiResponse(responseCode = "200", content = {@Content(mediaType = "application/pdf")}), + @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = ResponseWrapper.class), mediaType = "application/json")})}) + @PostMapping("/download") + public ResponseEntity downloadCredentialAsPDF(@RequestParam Map params) { + //TODO: remove this default value after the apitest is updated + params.putIfAbsent("vcStorageExpiryLimitInTimes", "-1"); + + try { + String issuerId = params.get("issuer"); + String credentialType = params.get("credential"); + String credentialValidity = params.get("vcStorageExpiryLimitInTimes"); + String locale = params.get("locale"); + log.info("Initiated Token Call"); + TokenResponseDTO response = idpService.getTokenResponse(params); + + log.info("Initiated Download Credential Call"); + ByteArrayInputStream inputStream = credentialService.downloadCredentialAsPDF(issuerId, credentialType, response, credentialValidity, locale); + return ResponseEntity + .ok() + .contentType(MediaType.APPLICATION_PDF) + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Disposition") + .body(new InputStreamResource(inputStream)); + } catch (ApiNotAccessibleException | IOException exception) { + log.error("Exception occurred while fetching credential types ", exception); + return Utilities.handleErrorResponse(exception, MIMOTO_PDF_SIGN_EXCEPTION.getCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (InvalidCredentialResourceException invalidCredentialResourceException) { + log.error("Exception occurred while pushing the data to data share ", invalidCredentialResourceException); + return Utilities.handleErrorResponse(invalidCredentialResourceException, MIMOTO_PDF_SIGN_EXCEPTION.getCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (VCVerificationException exception) { + log.error("Exception occurred while verification of the verifiable Credential" + exception); + return Utilities.handleErrorResponse(exception, MIMOTO_PDF_SIGN_EXCEPTION.getCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (Exception exception) { + log.error("Exception occurred while generating pdf ", exception); + return Utilities.handleErrorResponse(exception, MIMOTO_PDF_SIGN_EXCEPTION.getCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/IdpController.java b/src/main/java/io/mosip/mimoto/controller/IdpController.java new file mode 100644 index 000000000..e918d9ad5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/IdpController.java @@ -0,0 +1,257 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.IdpException; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.PlatformErrorMessages; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.util.JoseUtil; +import io.mosip.mimoto.util.RequestValidator; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@Slf4j +@Tag(name = SwaggerLiteralConstants.IDP_NAME, description = SwaggerLiteralConstants.IDP_DESCRIPTION) +public class IdpController { + private static final boolean USE_BEARER_TOKEN = true; + private static final String DPOP_HEADER = "DPoP"; + private static final Set HOP_BY_HOP_HEADERS = Set.of( + HttpHeaders.TRANSFER_ENCODING, + HttpHeaders.CONNECTION, + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "TE", + "Trailer", + HttpHeaders.UPGRADE + ); + + private final RestClientService restClientService; + + private final JoseUtil joseUtil; + + private final IdpService idpService; + + private final RequestValidator requestValidator; + + public IdpController(RestClientService restClientService, JoseUtil joseUtil, IdpService idpService, RequestValidator requestValidator) { + this.restClientService = restClientService; + this.joseUtil = joseUtil; + this.idpService = idpService; + this.requestValidator = requestValidator; + } + + @Operation(summary = SwaggerLiteralConstants.IDP_BINDING_OTP_SUMMARY, description = SwaggerLiteralConstants.IDP_BINDING_OTP_DESCRIPTION) + @PostMapping(value = "/binding-otp", produces = MediaType.APPLICATION_JSON_VALUE) + @SuppressWarnings("unchecked") + public ResponseEntity> otpRequest(@Valid @RequestBody BindingOtpRequestDto requestDTO, BindingResult result) throws Exception { + log.debug("Received binding-otp request"); + requestValidator.validateInputRequest(result); + requestValidator.validateNotificationChannel(requestDTO.getRequest().getOtpChannels()); + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + ResponseWrapper internalResponse = (ResponseWrapper) restClientService.postApi(ApiName.BINDING_OTP, requestDTO, ResponseWrapper.class, USE_BEARER_TOKEN); + if (internalResponse == null) + throw new IdpException(); + return ResponseEntity.status(HttpStatus.OK).body(internalResponse); + } catch (Exception e) { + log.error("Wallet binding otp error occurred.", e); + String[] errorObj = Utilities.handleExceptionWithErrorCode(e, PlatformErrorMessages.MIMOTO_OTP_BINDING_EXCEPTION.getCode()); + List errors = Utilities.getErrors(errorObj[0], errorObj[1]); + responseWrapper.setResponse(null); + responseWrapper.setErrors(errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + + } + + @Operation(summary = SwaggerLiteralConstants.IDP_WALLET_BINDING_SUMMARY, description = SwaggerLiteralConstants.IDP_WALLET_BINDING_DESCRIPTION) + @PostMapping(path = "/wallet-binding", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> request(@RequestBody WalletBindingRequestDTO requestDTO) + throws Exception { + + log.debug("Received wallet-binding request "); + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + WalletBindingInnerRequestDto innerRequestDto = new WalletBindingInnerRequestDto(); + innerRequestDto.setChallengeList(requestDTO.getRequest().getChallengeList()); + innerRequestDto.setIndividualId(requestDTO.getRequest().getIndividualId()); + innerRequestDto.setPublicKey(JoseUtil.getJwkFromPublicKey(requestDTO.getRequest().getPublicKey())); + innerRequestDto.setAuthFactorType(requestDTO.getRequest().getAuthFactorType()); + innerRequestDto.setFormat(requestDTO.getRequest().getFormat()); + + WalletBindingInternalRequestDTO req = new WalletBindingInternalRequestDTO(requestDTO.getRequestTime(), innerRequestDto); + + ResponseWrapper internalResponse = (ResponseWrapper) restClientService + .postApi(ApiName.WALLET_BINDING, + req, ResponseWrapper.class, USE_BEARER_TOKEN); + + if (internalResponse == null) + throw new IdpException(); + + responseWrapper = joseUtil.addThumbprintAndKeyId(internalResponse); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (Exception e) { + log.error("Wallet binding error occurred ", e); + String[] errorObj = Utilities.handleExceptionWithErrorCode(e, PlatformErrorMessages.MIMOTO_WALLET_BINDING_EXCEPTION.getCode()); + List errors = Utilities.getErrors(errorObj[0], errorObj[1]); + responseWrapper.setResponse(null); + responseWrapper.setErrors(errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } + + @Operation(summary = SwaggerLiteralConstants.IDP_GET_TOKEN_SUMMARY, description = SwaggerLiteralConstants.IDP_GET_TOKEN_DESCRIPTION) + @ApiResponses({ + @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = TokenResponseDTO.class), mediaType = "application/json")}), + @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = ResponseWrapper.class), mediaType = "application/json")})}) + @PostMapping(value = {"/get-token/{issuer}"}, consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getToken(@RequestParam Map params, @PathVariable(required = true, name = "issuer") String issuer) { + log.info("Reached the getToken Controller for Issuer " + issuer); + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + params.put("issuer", issuer); + TokenResponseDTO response = idpService.getTokenResponse(params); + return ResponseEntity.status(HttpStatus.OK).body(response); + } catch (Exception ex) { + log.error("Exception Occurred while Invoking the Token Endpoint : ", ex); + String[] errorObj = Utilities.handleExceptionWithErrorCode(ex, PlatformErrorMessages.MIMOTO_FETCHING_TOKEN_EXCEPTION.getCode()); + List errors = Utilities.getErrors(errorObj[0], errorObj[1]); + responseWrapper.setErrors(errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } + + @Operation( + summary = SwaggerLiteralConstants.IDP_GET_TOKEN_V2_SUMMARY, + description = SwaggerLiteralConstants.IDP_GET_TOKEN_V2_DESCRIPTION + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "Token response from authorization server", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = TokenResponseDTO.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Authorization server error response returned as-is or invalid request detected by Mimoto", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema( + example = "{\"error\":\"invalid_request\",\"error_description\":\"Invalid token request\"}" + ) + ) + ), + @ApiResponse( + responseCode = "401", + description = "Authorization server unauthorized response returned as-is", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema( + example = "{\"error\":\"invalid_client\",\"error_description\":\"Client authentication failed\"}" + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Internal Mimoto error before receiving authorization server response", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema( + example = "{\"error\":\"MIMOTO-XXX\",\"error_description\":\"Unable to fetch token\"}" + ) + ) + ) + }) + @PostMapping( + value = "/v2/get-token/{issuer}", + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public ResponseEntity getTokenV2(@RequestParam Map params, + @PathVariable(name = "issuer") String issuer, + @RequestHeader(value = DPOP_HEADER, required = false) String dpopProof) { + log.info("Reached the getTokenV2 Controller for Issuer {}", issuer); + + try { + Map tokenParams = new HashMap<>(params); + tokenParams.put("issuer", issuer); + + ResponseEntity response = idpService.getTokenResponseV2(tokenParams, dpopProof); + + return ResponseEntity + .status(response.getStatusCode()) + .headers(getForwardableHeaders(response.getHeaders())) + .body(response.getBody()); + + } catch (InvalidRequestException ex) { + log.error("Invalid token request: ", ex); + + String[] errorObj = Utilities.handleExceptionWithErrorCode( + ex, + PlatformErrorMessages.MIMOTO_FETCHING_TOKEN_EXCEPTION.getCode() + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of( + "error", errorObj[0], + "error_description", errorObj[1] + )); + + } catch (Exception ex) { + log.error("Exception occurred while invoking the token endpoint: ", ex); + + String[] errorObj = Utilities.handleExceptionWithErrorCode( + ex, + PlatformErrorMessages.MIMOTO_FETCHING_TOKEN_EXCEPTION.getCode() + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of( + "error", errorObj[0], + "error_description", errorObj[1] + )); + } + } + + private static HttpHeaders getForwardableHeaders(HttpHeaders sourceHeaders) { + HttpHeaders forwardHeaders = new HttpHeaders(); + forwardHeaders.putAll(sourceHeaders); + + HOP_BY_HOP_HEADERS.forEach(forwardHeaders::remove); + sourceHeaders.getConnection().forEach(forwardHeaders::remove); + + return forwardHeaders; + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/IssuersController.java b/src/main/java/io/mosip/mimoto/controller/IssuersController.java new file mode 100644 index 000000000..123ba1cc8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/IssuersController.java @@ -0,0 +1,135 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.exception.PlatformErrorMessages.*; + +@RestController +@Slf4j +@RequestMapping(value = "/issuers") +@Tag(name = SwaggerLiteralConstants.ISSUERS_NAME, description = SwaggerLiteralConstants.ISSUERS_DESCRIPTION) +public class IssuersController { + + private final IssuersService issuersService; + + public IssuersController(IssuersService issuersService) { + this.issuersService = issuersService; + } + + @Operation(summary = SwaggerLiteralConstants.ISSUERS_GET_ISSUERS_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_GET_ISSUERS_DESCRIPTION) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAllIssuers(@RequestParam(required = false, name = "search") String search) { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + responseWrapper.setResponse(issuersService.getIssuers(search)); + } catch (ApiNotAccessibleException | IOException e) { + log.error("Exception occurred while fetching issuers ", e); + responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } + + /** + * @deprecated Since version 0.16.0, this endpoint is deprecated and will be removed in a future release. + * Please use new endpoint {@link #getIssuerConfiguration(String)} instead. + */ + @Deprecated(since = "0.16.0", forRemoval = true) + @Operation(summary = SwaggerLiteralConstants.ISSUERS_GET_ISSUER_WELLKNOWN_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_GET_ISSUER_WELLKNOWN_DESCRIPTION) + @GetMapping(value = "/{issuer-id}/well-known-proxy", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getIssuerWellknown(@PathVariable("issuer-id") String issuerId) { + try { + CredentialIssuerConfiguration issuerConfigurationResponse = issuersService.getIssuerConfiguration(issuerId); + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = new CredentialIssuerWellKnownResponse( + issuerConfigurationResponse.getCredentialIssuer(), + issuerConfigurationResponse.getAuthorizationServers(), + issuerConfigurationResponse.getCredentialEndPoint(), + issuerConfigurationResponse.getCredentialConfigurationsSupported()); + return ResponseEntity.status(HttpStatus.OK).body(credentialIssuerWellKnownResponse); + } catch (Exception exception) { + log.error("Exception occurred while fetching issuers wellknown ", exception); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } + } + + @Operation(summary = SwaggerLiteralConstants.ISSUERS_GET_SPECIFIC_ISSUER_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_GET_SPECIFIC_ISSUER_DESCRIPTION) + @GetMapping(value = "/{issuer-id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getIssuerConfig(@PathVariable("issuer-id") String issuerId) { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + IssuerDTO issuerDTO; + try { + issuerDTO = issuersService.getIssuerDetails(issuerId); + responseWrapper.setResponse(issuerDTO); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (InvalidIssuerIdException exception) { + log.error("invalid issuer id {} passed - ", issuerId); + responseWrapper.setErrors(List.of(new ErrorDTO(INVALID_ISSUER_ID_EXCEPTION.getCode(), INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseWrapper); + } catch (Exception exception) { + log.error("Exception occurred while fetching issuers ", exception); + String[] errorObj = Utilities.handleExceptionWithErrorCode(exception, INVALID_ISSUER_ID_EXCEPTION.getCode()); + List errors = Utilities.getErrors(errorObj[0], errorObj[1]); + responseWrapper.setResponse(null); + responseWrapper.setErrors(errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } + + @Operation(summary = SwaggerLiteralConstants.ISSUERS_GET_ISSUER_CONFIGURATION_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_GET_ISSUER_CONFIGURATION_DESCRIPTION) + @GetMapping(value = "/{issuer-id}/configuration", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getIssuerConfiguration(@PathVariable("issuer-id") String issuerId) { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + CredentialIssuerConfiguration issuerConfiguration = issuersService.getIssuerConfiguration(issuerId); + List credentials = new LinkedList<>(); + + issuerConfiguration.getCredentialConfigurationsSupported().forEach((key, value) -> { + List display = value.getDisplay().stream() + .map(displayConfig -> new CredentialDisplayResponse( + displayConfig.getName(), + displayConfig.getLocale(), + displayConfig.getLogo().getUrl())) + .collect(Collectors.toList()); + CredentialsResponse credential = new CredentialsResponse(key, + value.getScope(), display); + credentials.add(credential); + }); + + CredentialIssuerConfigurationResponseDTO credentialIssuerConfigurationResponseDTO = new CredentialIssuerConfigurationResponseDTO( + credentials, + issuerConfiguration.getAuthorizationServerWellKnownResponse().getAuthorizationEndpoint(), + issuerConfiguration.getAuthorizationServerWellKnownResponse().getGrantTypesSupported()); + responseWrapper.setResponse(credentialIssuerConfigurationResponseDTO); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (Exception exception) { + log.error("Exception occurred while fetching issuers configurations - ", exception); + String[] errorObj = Utilities.handleExceptionWithErrorCode(exception, INVALID_ISSUER_ID_CONFIGURATION.getCode()); + List errors = Utilities.getErrors(errorObj[0], errorObj[1]); + responseWrapper.setResponse(null); + responseWrapper.setErrors(errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/IssuersV2Controller.java b/src/main/java/io/mosip/mimoto/controller/IssuersV2Controller.java new file mode 100644 index 000000000..51be0615a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/IssuersV2Controller.java @@ -0,0 +1,81 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; + +import static io.mosip.mimoto.exception.PlatformErrorMessages.API_NOT_ACCESSIBLE_EXCEPTION; +import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION; + +@RestController +@Slf4j +@RequestMapping("/v2/issuers") +@Tag(name = SwaggerLiteralConstants.ISSUERS_V2_NAME, description = SwaggerLiteralConstants.ISSUERS_V2_DESCRIPTION) +public class IssuersV2Controller { + + private final IssuersService issuersService; + + public IssuersV2Controller(IssuersService issuersService) { + this.issuersService = issuersService; + } + + @Operation(summary = SwaggerLiteralConstants.ISSUERS_V2_GET_ISSUERS_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_V2_GET_ISSUERS_DESCRIPTION) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAllIssuers() { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + responseWrapper.setResponse(issuersService.getIssuersV2DTO()); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (ApiNotAccessibleException | IOException e) { + log.error("Exception occurred while fetching issuers (V2)", e); + responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(responseWrapper); + } catch (Exception e) { + log.error("Exception occurred while fetching issuers (V2)", e); + String[] errorObj = Utilities.handleExceptionWithErrorCode(e, API_NOT_ACCESSIBLE_EXCEPTION.getCode()); + responseWrapper.setErrors(Utilities.getErrors(errorObj[0], errorObj[1])); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } + + @Operation(summary = SwaggerLiteralConstants.ISSUERS_V2_GET_ISSUER_SUMMARY, description = SwaggerLiteralConstants.ISSUERS_V2_GET_ISSUER_DESCRIPTION) + @GetMapping(value = "/{issuer-id}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getIssuerById(@PathVariable("issuer-id") String issuerId) { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + IssuerV2DTO issuer = issuersService.getIssuerV2Details(issuerId); + responseWrapper.setResponse(issuer); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (InvalidIssuerIdException e) { + log.error("Invalid issuer id {} passed (V2)", issuerId); + responseWrapper.setErrors(List.of(new ErrorDTO(INVALID_ISSUER_ID_EXCEPTION.getCode(), INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseWrapper); + } catch (ApiNotAccessibleException | IOException e) { + log.error("Exception occurred while fetching issuer {} (V2)", issuerId, e); + responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(responseWrapper); + } catch (Exception e) { + log.error("Exception occurred while fetching issuer {} (V2)", issuerId, e); + String[] errorObj = Utilities.handleExceptionWithErrorCode(e, API_NOT_ACCESSIBLE_EXCEPTION.getCode()); + responseWrapper.setErrors(Utilities.getErrors(errorObj[0], errorObj[1])); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/PresentationController.java b/src/main/java/io/mosip/mimoto/controller/PresentationController.java new file mode 100644 index 000000000..2e0b8e7c4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/PresentationController.java @@ -0,0 +1,104 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.dto.openid.presentation.PresentationDefinitionDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.exception.InvalidVerifierException; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.PresentationService; +import io.mosip.mimoto.service.VerifierService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +@RestController +@Slf4j +@Tag(name = SwaggerLiteralConstants.PRESENTATION_NAME, description = SwaggerLiteralConstants.PRESENTATION_DESCRIPTION) +public class PresentationController { + + private final PresentationService presentationService; + + @Value("${mosip.inji.ovp.error.redirect.url.pattern}") + String injiOvpErrorRedirectUrlPattern; + + @Value("${mosip.inji.web.redirect.url}") + String injiWebRedirectUrl; + + private final VerifierService verifierService; + + private final ObjectMapper objectMapper; + + public PresentationController(PresentationService presentationService, VerifierService verifierService, ObjectMapper objectMapper) { + this.presentationService = presentationService; + this.verifierService = verifierService; + this.objectMapper = objectMapper; + } + + @Operation( summary = SwaggerLiteralConstants.PRESENTATION_AUTHORIZE_SUMMARY, description = SwaggerLiteralConstants.PRESENTATION_AUTHORIZE_DESCRIPTION) + @ApiResponses({ @ApiResponse(responseCode = "302", content = { @Content(mediaType = "application/text") }) }) + @GetMapping("/authorize") + public void performAuthorization(HttpServletResponse response, + @RequestParam("response_type") @Schema(description = "Response Type of the Presentation" , defaultValue = "vp_token") String responseType, + @RequestParam("resource") @Schema(description = "URL Encoded Resource url of the Credential ") String resource, + @RequestParam("presentation_definition") @Schema(description = "URL Encoded presentation definition") String presentationDefinition, + @RequestParam("client_id") @Schema(description = "URL Encoded Client Id") String clientId, + @RequestParam(name = "redirect_uri", required = false) @Schema(description = "URL Encoded Redirect URI") String redirectUri, + @RequestParam(name = "state", required = false) @Schema(description = "Unique session identifier to prevent CSRF") String state, + @RequestParam(name = "nonce", required = false) @Schema(description = "Cryptographic challenge used to bind the presentation to this request") String nonce, + @RequestParam(name = "response_uri", required = false) @Schema(description = "Where to send the VP token") String responseUri, + @RequestParam(name = "response_mode", required = false) @Schema(description = "Specifies how the Verifier will receive the Authorization Response from the Wallet") String responseMode) throws IOException { + + try { + log.info("Started Presentation Authorization in the controller."); + verifierService.validateVerifier(clientId, responseUri, redirectUri); + PresentationDefinitionDTO presentationDefinitionDTO = objectMapper.readValue(presentationDefinition, PresentationDefinitionDTO.class); + PresentationRequestDTO presentationRequestDTO = PresentationRequestDTO.builder() + .responseType(responseType) + .resource(resource) + .presentationDefinition(presentationDefinitionDTO) + .clientId(clientId) + .redirectUri(redirectUri) + .state(state) + .nonce(nonce) + .responseUri(responseUri) + .responseMode(responseMode) + .build(); + String redirectString = presentationService.authorizePresentation(presentationRequestDTO); + log.info("Completed Presentation Authorization in the controller."); + response.sendRedirect(redirectString); + } catch( InvalidVerifierException exception){ + sendRedirect(response, injiWebRedirectUrl, exception.getErrorCode(), exception.getErrorText(), exception); + } catch(VPNotCreatedException | InvalidCredentialResourceException exception){ + sendRedirect(response, redirectUri, exception.getErrorCode(), exception.getErrorText(), exception); + } catch (Exception exception){ + sendRedirect(response, redirectUri, ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage(), exception); + } + } + + private void sendRedirect(HttpServletResponse response, String domain, String code, String message, Exception exception) throws IOException { + log.error("Exception Occurred in Authorizing the presentation : \n\t code - " + code + "\n\t message - " + message + "\n\t Trace - " + Arrays.toString(exception.getStackTrace())); + String injiVerifyRedirectString = String.format(injiOvpErrorRedirectUrlPattern, + domain, + code, + URLEncoder.encode(message, StandardCharsets.UTF_8)); + response.setStatus(302); + response.sendRedirect(injiVerifyRedirectString); + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/ResidentServiceController.java b/src/main/java/io/mosip/mimoto/controller/ResidentServiceController.java index 19d727c82..29c79ee04 100644 --- a/src/main/java/io/mosip/mimoto/controller/ResidentServiceController.java +++ b/src/main/java/io/mosip/mimoto/controller/ResidentServiceController.java @@ -1,43 +1,42 @@ package io.mosip.mimoto.controller; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.RequestWrapper; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.resident.*; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.util.DateUtils; +import io.mosip.mimoto.util.RequestValidator; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.constant.ApiName; -import io.mosip.mimoto.core.http.RequestWrapper; -import io.mosip.mimoto.core.http.ResponseWrapper; -import io.mosip.mimoto.dto.mimoto.AppOTPRequestDTO; -import io.mosip.mimoto.dto.mimoto.AppVIDGenerateRequestDTO; -import io.mosip.mimoto.dto.resident.AuthLockRequestDTO; -import io.mosip.mimoto.dto.resident.AuthLockUnlockResponseDTO; -import io.mosip.mimoto.dto.resident.AuthUnlockRequestDTO; -import io.mosip.mimoto.dto.resident.CredentialRequestResponseDTO; -import io.mosip.mimoto.dto.resident.OTPRequestDTO; -import io.mosip.mimoto.dto.resident.VIDGenerateRequestDTO; -import io.mosip.mimoto.dto.resident.VIDGeneratorResponseDTO; -import io.mosip.mimoto.service.RestClientService; -import io.mosip.mimoto.util.DateUtils; -import io.mosip.mimoto.util.LoggerUtil; +import jakarta.validation.Valid; -@SpringBootApplication @RestController +@Tag(name = SwaggerLiteralConstants.RESIDENT_NAME, description = SwaggerLiteralConstants.RESIDENT_DESCRIPTION) public class ResidentServiceController { - private final Logger logger = LoggerUtil.getLogger(ResidentServiceController.class); + private final RestClientService restClientService; - @Autowired - public RestClientService restClientService; + private final RequestValidator requestValidator; - @Autowired - Environment env; + private final Environment env; + + public ResidentServiceController(RestClientService restClientService, RequestValidator requestValidator, Environment env) { + this.restClientService = restClientService; + this.requestValidator = requestValidator; + this.env = env; + } /** * Request a new OTP for OTP required API. @@ -47,8 +46,11 @@ public class ResidentServiceController { * @throws Exception */ @PostMapping("/req/otp") + @Operation(summary = SwaggerLiteralConstants.RESIDENT_REQUEST_OTP_SUMMARY, description = SwaggerLiteralConstants.RESIDENT_REQUEST_OTP_DESCRIPTION) @SuppressWarnings("unchecked") - public ResponseEntity otpRequest(@RequestBody AppOTPRequestDTO requestDTO) throws Exception { + public ResponseEntity> otpRequest(@Valid @RequestBody AppOTPRequestDTO requestDTO, BindingResult result) throws Exception { + requestValidator.validateInputRequest(result); + requestValidator.validateNotificationChannel(requestDTO.getOtpChannel()); OTPRequestDTO mosipOTPRequestPayload = new OTPRequestDTO(); mosipOTPRequestPayload.setVersion("1.0"); mosipOTPRequestPayload.setId("mosip.identity.otp.internal"); @@ -72,6 +74,7 @@ public ResponseEntity otpRequest(@RequestBody AppOTPRequestDTO requestDT * @throws Exception */ @PostMapping("/vid") + @Operation(hidden = true) @SuppressWarnings("unchecked") public ResponseEntity vidGenerate(@RequestBody AppVIDGenerateRequestDTO requestDTO) throws Exception { VIDGenerateRequestDTO vidRequestDTO = new VIDGenerateRequestDTO(); @@ -100,6 +103,7 @@ public ResponseEntity vidGenerate(@RequestBody AppVIDGenerateRequestDTO * @throws Exception */ @PostMapping("/req/auth/lock") + @Operation(hidden = true) @SuppressWarnings("unchecked") public ResponseEntity authLock(@RequestBody AuthLockRequestDTO requestDTO) throws Exception { RequestWrapper mosipAuthLockRequestPayload = new RequestWrapper<>(); @@ -121,6 +125,7 @@ public ResponseEntity authLock(@RequestBody AuthLockRequestDTO requestDT * @throws Exception */ @PostMapping("/req/auth/unlock") + @Operation(hidden = true) @SuppressWarnings("unchecked") public ResponseEntity authUnlock(@RequestBody AuthUnlockRequestDTO requestDTO) throws Exception { RequestWrapper mosipAuthUnlockRequestPayload = new RequestWrapper<>(); @@ -135,4 +140,39 @@ public ResponseEntity authUnlock(@RequestBody AuthUnlockRequestDTO reque return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); } + @PostMapping("/req/individualId/otp") + @Operation(summary = SwaggerLiteralConstants.RESIDENT_REQUEST_INDIVIDUALID_OTP_SUMMARY, description = SwaggerLiteralConstants.RESIDENT_REQUEST_INDIVIDUALID_OTP_DESCRIPTION) + @SuppressWarnings("unchecked") + public ResponseEntity individualIdOtpRequest(@RequestBody IndividualIdOtpRequestDTO requestDTO) throws Exception { + IndividualIdOTPRequestDTO mosipOTPRequestPayload = new IndividualIdOTPRequestDTO(); + mosipOTPRequestPayload.setId("mosip.identity.otp.internal"); + mosipOTPRequestPayload.setVersion("1.0"); + mosipOTPRequestPayload.setRequestTime(DateUtils.getRequestTimeString()); + mosipOTPRequestPayload.setIndividualId(requestDTO.getAid()); + mosipOTPRequestPayload.setOtpChannel(requestDTO.getOtpChannel()); + mosipOTPRequestPayload.setTransactionId(requestDTO.getTransactionID()); + + OTPResponseDTO responseWrapper = (OTPResponseDTO) restClientService + .postApi(ApiName.RESIDENT_INDIVIDUALID_OTP, "", "", mosipOTPRequestPayload, OTPResponseDTO.class, MediaType.APPLICATION_JSON); + + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } + + @PostMapping("/aid/get-individual-id") + @Operation(summary = SwaggerLiteralConstants.RESIDENT_GET_INDIVIDUALID_SUMMARY, description = SwaggerLiteralConstants.RESIDENT_GET_INDIVIDUALID_DESCRIPTION) + @SuppressWarnings("unchecked") + public ResponseEntity> aidGetIndividualId(@RequestBody AidStatusRequestDTO requestDTO) throws Exception { + InternalAidStatusDTO req = new InternalAidStatusDTO(requestDTO.getAid(), requestDTO.getOtp(), requestDTO.getTransactionID()); + RequestWrapper mosipAuthLockRequestPayload = new RequestWrapper<>(); + mosipAuthLockRequestPayload.setId("mosip.resident.checkstatus"); + mosipAuthLockRequestPayload.setVersion("1.0"); + mosipAuthLockRequestPayload.setRequesttime(DateUtils.getRequestTimeString()); + mosipAuthLockRequestPayload.setRequest(req); + + ResponseWrapper responseWrapper = (ResponseWrapper) restClientService + .postApi(ApiName.RESIDENT_AID_GET_INDIVIDUALID, "", "", mosipAuthLockRequestPayload, ResponseWrapper.class, MediaType.APPLICATION_JSON); + + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } + } diff --git a/src/main/java/io/mosip/mimoto/controller/TokenAuthController.java b/src/main/java/io/mosip/mimoto/controller/TokenAuthController.java new file mode 100644 index 000000000..fe429670f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/TokenAuthController.java @@ -0,0 +1,129 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import io.mosip.mimoto.service.TokenService; +import io.mosip.mimoto.service.TokenServiceFactory; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RestController +@Tag(name = SwaggerLiteralConstants.ID_TOKEN_AUTHENTICATION_NAME, description = SwaggerLiteralConstants.ID_TOKEN_AUTHENTICATION_DESCRIPTION) +public class TokenAuthController { + + private static final String BEARER_PREFIX = "Bearer "; + private static final String SESSION_CREATED = "Session created."; + private static final String INVALID_TOKEN_MESSAGE = "Bearer ID token required."; + + + private final TokenServiceFactory tokenServiceFactory; + + @Autowired + public TokenAuthController(TokenServiceFactory tokenServiceFactory) { + this.tokenServiceFactory = tokenServiceFactory; + } + + @Operation( + summary = "Login and create session using OAuth2 ID token", + description = """ + This API accepts an OAuth2 ID token in the Authorization header and establishes a session + by populating the Spring Security context. + + Fetch the ID token from a supported OAuth2 provider (such as Google or Microsoft) and provide + it in the request as a Bearer token. + """, + operationId = "loginWithOAuth2IdToken", + security = @SecurityRequirement(name = "bearerAuth"), + parameters = { + @Parameter( + name = "provider", + in = ParameterIn.PATH, + required = true, + description = "OAuth2 provider name. Example values: 'google', 'microsoft', 'facebook'.", + schema = @Schema(type = "string", example = "google") + ), + @Parameter( + name = "Authorization", + in = ParameterIn.HEADER, + required = true, + description = "OAuth2 ID token prefixed with 'Bearer '.", + schema = @Schema(type = "string", example = "Bearer eyJhbGciOi...") + ) + }, + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully logged in and session created.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(type = "string"), + examples = @ExampleObject(value = "Session created.") + ) + ), + @ApiResponse( + responseCode = "400", + description = "Bad Request - Missing/invalid header or unsupported provider.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = io.mosip.mimoto.dto.ErrorDTO.class), + examples = { + + @ExampleObject(name = "MissingToken", value = """ + { + "errorCode": "INVALID_REQUEST", + "errorMessage": "Bearer ID token required." + } + """), + @ExampleObject(name = "InvalidToken", value = """ + { + "errorCode": "invalid_token", + "errorMessage": "An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature" + } + """), + @ExampleObject(name = "ExpiredToken", value = """ + { + "errorCode": "invalid_token", + "errorMessage": "An error occurred while attempting to decode the Jwt: Jwt expired at 2025-06-10T12:00:00Z" + } + """ + ) + } + ) + ), + } + ) + @PostMapping("/auth/{provider}/token-login") + public ResponseEntity createSessionFromIdToken(@RequestHeader(value = "Authorization", required = false) String authorization, @PathVariable("provider") String provider, HttpServletRequest request, HttpServletResponse response) { + if (authorization == null || !authorization.startsWith(BEARER_PREFIX)) { + return Utilities.buildErrorResponse(HttpStatus.BAD_REQUEST, ErrorConstants.INVALID_REQUEST.getErrorCode(), INVALID_TOKEN_MESSAGE); + } + + String idToken = authorization.substring(BEARER_PREFIX.length()); + try { + TokenService tokenService = tokenServiceFactory.getTokenService(provider); + tokenService.processToken(idToken, provider, request, response); + return ResponseEntity.ok(SESSION_CREATED); + } catch (OAuth2AuthenticationException e) { + return Utilities.getErrorResponseEntityWithoutWrapper(e, e.getErrorCode(), HttpStatus.UNAUTHORIZED, MediaType.APPLICATION_JSON); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/controller/UsersController.java b/src/main/java/io/mosip/mimoto/controller/UsersController.java new file mode 100644 index 000000000..4303738cc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/UsersController.java @@ -0,0 +1,143 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.constant.SwaggerExampleConstants; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import io.mosip.mimoto.repository.UserMetadataRepository; +import io.mosip.mimoto.service.EncryptionService; +import io.mosip.mimoto.util.Utilities; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping(value = "/users/me") +@Tag(name = SwaggerLiteralConstants.USERS_NAME, description = SwaggerLiteralConstants.USERS_DESCRIPTION) +public class UsersController { + private final UserMetadataRepository userMetadataRepository; + + private final EncryptionService encryptionService; + + public UsersController(UserMetadataRepository userMetadataRepository, EncryptionService encryptionService) { + this.userMetadataRepository = userMetadataRepository; + this.encryptionService = encryptionService; + } + + /** + * Retrieves user profile information, first checking the cache and then the database if needed + * + * @param authentication The authentication object containing user information + * @param session The HTTP session + * @return ResponseEntity containing user metadata + */ + @Operation( + summary = "Retrieve user metadata", + description = "First attempts to retrieve user metadata from the session cache. If not available, fetches from the database. This API is secured using session-based authentication.", + operationId = "getUserProfile", + security = @SecurityRequirement(name = "SessionAuth") + ) + @ApiResponse( + responseCode = "200", + description = "User profile retrieved successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UserMetadataDTO.class), + examples = { + @ExampleObject(name = "Success response from DB", value = SwaggerExampleConstants.FETCH_USER_PROFILE_FROM_DB_SUCCESS), + @ExampleObject(name = "Success response from cache", value = SwaggerExampleConstants.FETCH_USER_CACHE_PROFILE_SUCCESS) + } + ) + ) + @ApiResponse(responseCode = "401", description = "User data not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "User not found", value = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User not found. Please check your credentials or login again\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"), + @ExampleObject(name = "Decryption Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"Failed to process user data\"}"), + })) + @ApiResponse( + responseCode = "503", + description = "Service unavailable", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorDTO.class), + examples = @ExampleObject( + name = "Database connection failure", + value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}" + ) + ) + ) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getUserProfileInfo(Authentication authentication, HttpSession session) throws Exception { + UserMetadataDTO userMetadataDTO = (UserMetadataDTO) session.getAttribute(SessionKeys.USER_METADATA); + + if (userMetadataDTO == null) { + log.info("User metadata not found in cache, fetching from database"); + String identityProvider = (String) session.getAttribute("clientRegistrationId"); + UserMetadata userMetadata; + try { + userMetadata = fetchUserMetadata(authentication.getName(), identityProvider); + + // In case of fetching user info from DB, walletId is null. + // Here login is required as data is unavailable in session + userMetadataDTO = new UserMetadataDTO( + encryptionService.decrypt(userMetadata.getDisplayName()), + encryptionService.decrypt(userMetadata.getProfilePictureUrl()), + encryptionService.decrypt(userMetadata.getEmail()), + null + ); + session.setAttribute(SessionKeys.USER_METADATA, userMetadataDTO); + } catch (UnauthorizedAccessException exception) { + log.error("Error occurred while retrieving user profile: ", exception); + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, + ErrorConstants.INVALID_USER.getErrorCode(), + HttpStatus.UNAUTHORIZED, + MediaType.APPLICATION_JSON + ); + } catch (DecryptionException e) { + log.error("Error occurred while decrypting user data: ", e); + return Utilities.getErrorResponseEntityWithoutWrapper( + new RuntimeException("Failed to process user data"), + ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), + HttpStatus.INTERNAL_SERVER_ERROR, + MediaType.APPLICATION_JSON + ); + } + } else { + // If data is available in cache, take walletId also from cache + Object walletIdObj = session.getAttribute(SessionKeys.WALLET_ID); + if (walletIdObj instanceof String walletId) { + userMetadataDTO.setWalletId(walletId); + } + log.info("Retrieved user metadata from cache"); + } + + return ResponseEntity.status(HttpStatus.OK).body(userMetadataDTO); + } + + private UserMetadata fetchUserMetadata(String providerSubjectId, String identityProvider) throws UnauthorizedAccessException { + return userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(providerSubjectId, identityProvider). + orElseThrow(() -> + new UnauthorizedAccessException(ErrorConstants.UNAUTHORIZED_ACCESS.getErrorCode(), "User not found. Please check your credentials or login again") + ); + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/VerifiersController.java b/src/main/java/io/mosip/mimoto/controller/VerifiersController.java new file mode 100644 index 000000000..d49c29200 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/VerifiersController.java @@ -0,0 +1,47 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.service.VerifierService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.Collections; + + +@RestController +@RequestMapping(value = "/verifiers") +@Slf4j +@Tag(name = SwaggerLiteralConstants.VERIFIERS_NAME, description = SwaggerLiteralConstants.VERIFIERS_DESCRIPTION) +public class VerifiersController { + + private final VerifierService verifierService; + + public VerifiersController(VerifierService verifierService) { + this.verifierService = verifierService; + } + + @Operation(summary = SwaggerLiteralConstants.VERIFIERS_GET_VERIFIERS_SUMMARY, description = SwaggerLiteralConstants.VERIFIERS_GET_VERIFIERS_DESCRIPTION) + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getAllTrustedVerifiers() { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + try { + responseWrapper.setResponse(verifierService.getTrustedVerifiers()); + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } catch (ApiNotAccessibleException | IOException e) { + log.error("Exception occurred while fetching trusted verifiers ", e); + responseWrapper.setResponse(new VerifiersDTO(Collections.emptyList())); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/WalletCredentialsController.java b/src/main/java/io/mosip/mimoto/controller/WalletCredentialsController.java new file mode 100644 index 000000000..e72124bb7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/WalletCredentialsController.java @@ -0,0 +1,316 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SwaggerExampleConstants; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.VerifiableCredentialRequestDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.dto.resident.WalletCredentialResponseDTO; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.service.WalletCredentialService; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.util.LocaleUtils; +import io.mosip.mimoto.util.Utilities; +import io.mosip.mimoto.util.WalletUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static io.mosip.mimoto.exception.ErrorConstants.CREDENTIAL_DOWNLOAD_EXCEPTION; +import static io.mosip.mimoto.exception.ErrorConstants.INVALID_REQUEST; +import static io.mosip.mimoto.util.WalletUtil.validateWalletId; + +/** + * REST controller for managing wallet credentials. + */ +@Slf4j +@RestController +@RequestMapping(value = "/wallets/{walletId}/credentials") +@Tag(name = SwaggerLiteralConstants.WALLET_CREDENTIALS_NAME, description = SwaggerLiteralConstants.WALLET_CREDENTIALS_DESCRIPTION) +public class WalletCredentialsController { + + private final WalletCredentialService walletCredentialService; + private final IdpService idpService; + + @Autowired + public WalletCredentialsController(WalletCredentialService walletCredentialService, + IdpService idpService) { + this.walletCredentialService = walletCredentialService; + this.idpService = idpService; + } + + /** + * Downloads and stores a Verifiable Credential in the specified wallet. + * + * @param walletId The unique identifier of the wallet. + * @param verifiableCredentialRequest Request body parameters including issuer, credential type, storage expiry, and locale. + * @param httpSession The HTTP session containing wallet key and ID. + * @return The stored Verifiable Credential details. + * @throws InvalidRequestException If input parameters or session are invalid. + */ + @Operation(summary = "Download and store a Verifiable Credential under a specific Wallet", description = "This API is secured using session-based authentication. Upon receiving a request, the session ID is extracted from the Cookie header and used to retrieve session details from Redis for authentication. It then retrieves the wallet key from the session and use it to decrypt the signing algorithm's secret key(which is used for signing the JWT in credential request) and encrypt the downloaded Verifiable Credential. If the process completes successfully, the credential is stored in the database and certain fields will be returned in the response. In case of any issues, an appropriate error response is returned.", operationId = "downloadCredential", security = @SecurityRequirement(name = "SessionAuth"), parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the wallet", schema = @Schema(type = "string")), + @Parameter(name = "Accept-Language", in = ParameterIn.HEADER, description = "The locale for the Verifiable Credential. It follows 2 letter language code", schema = @Schema(type = "string", defaultValue = "en"))}, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body parameters including issuer, credential type, storage expiry, and locale.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = VerifiableCredentialRequestDTO.class), + examples = @ExampleObject( + name = "VerifiableCredentialRequest Example", + value = "{ \"issuer\": \"issuerId\", \"credentialConfigurationId\": \"MockVerifiableCredential\", \"code\": \"authCode\", \"grantType\": \"authorization_code\", \"redirectUri\": \"https://example.com/cb\", \"codeVerifier\": \"verifier\" }" + ) + ) + ) + ) + @ApiResponse(responseCode = "200", description = "Verifiable Credential downloaded and stored successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = VerifiableCredentialResponseDTO.class))) + @ApiResponse(responseCode = "400", description = "Bad request - Wallet key is null / blank or Wallet ID is null / blank / mismatch with session Wallet ID or required params are missing / invalid", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid header Accept-Language", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Locale must be a 2-letter code\"}"), + @ExampleObject(name = "Wallet ID is empty or blank", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet ID cannot be blank\"}"), + @ExampleObject(name = "Wallet ID not found in session", value = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"), + @ExampleObject(name = "Invalid Wallet ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Wallet key not found in session", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet key not found in session\"}"), + @ExampleObject(name = "Invalid issuer", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"issuerId cannot be blank\"}"), + @ExampleObject(name = "Invalid credentialConfigurationId", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"credentialConfigurationId cannot be blank\"}"), + @ExampleObject(name = "Invalid code", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"code cannot be blank\"}"), + @ExampleObject(name = "Invalid grantType", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"grantType cannot be blank\"}"), + @ExampleObject(name = "Invalid redirectUri", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"redirectUri cannot be blank\"}"), + @ExampleObject(name = "Invalid codeVerifier", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"codeVerifier cannot be blank\"}")}) + ) + @ApiResponse(responseCode = "500", description = "Internal server error - error occurred while serializing the VC response, encrypting the credential, or storing it", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = {@ExampleObject(name = "Credential already exists", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Duplicate credential for issuer and type\"}"), @ExampleObject(name = "Issuer config error", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Unable to fetch issuer configuration\"}"), @ExampleObject(name = "Failed to generate VC request", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Unable to generate credential request\"}"), @ExampleObject(name = "Signature verification failed", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"), @ExampleObject(name = "Unexpected server error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}")})) + @ApiResponse(responseCode = "503", description = "Service unavailable - error while fetching issuer or auth server well-known, downloading credential, or DB connection failure", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = {@ExampleObject(name = "API is not accessible", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Failed to download and store the credential\"}"), @ExampleObject(name = "Failed to download credential", value = "{\"errorCode\": \"server_unavailable\", \"errorMessage\": \"Unable to download credential from issuer\"}"), @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}")})) + @PostMapping + public ResponseEntity downloadCredential( + @RequestHeader(value = "Accept-Language", required = false, defaultValue = "en") @Pattern(regexp = "^[a-z]{2}$", message = "Locale must be a 2-letter code") String locale, + @PathVariable("walletId") @NotBlank(message = "Wallet ID cannot be blank") String walletId, + @RequestBody @Valid VerifiableCredentialRequestDTO verifiableCredentialRequest, + HttpSession httpSession) throws InvalidRequestException { + if (!LocaleUtils.isValidLanguageCode(locale)) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Locale must be a valid 2-letter code"); + } + validateWalletId(httpSession, walletId); + String base64EncodedWalletKey = WalletUtil.getSessionWalletKey(httpSession); + + String issuerId = verifiableCredentialRequest.getIssuer(); + String credentialConfigurationId = verifiableCredentialRequest.getCredentialConfigurationId(); + + log.info("Initiating token call for issuer: {}", issuerId); + TokenResponseDTO tokenResponse; + try { + tokenResponse = idpService.getTokenResponse(verifiableCredentialRequest); + } catch (ApiNotAccessibleException | IOException | AuthorizationServerWellknownResponseException | + InvalidWellknownResponseException e) { + log.error("Error fetching token response for issuer: {}", issuerId, e); + return Utilities.getErrorResponseEntityFromPlatformErrorMessage( + CREDENTIAL_DOWNLOAD_EXCEPTION, HttpStatus.SERVICE_UNAVAILABLE, MediaType.APPLICATION_JSON); + } + + log.info("Fetching and storing Verifiable Credential for walletId: {}", walletId); + + try { + VerifiableCredentialResponseDTO credentialResponseDTO = walletCredentialService.downloadVCAndStoreInDB( + issuerId, credentialConfigurationId, tokenResponse, locale, walletId, base64EncodedWalletKey); + return ResponseEntity.status(HttpStatus.OK).body(credentialResponseDTO); + } catch (ExternalServiceUnavailableException e) { + return Utilities.getErrorResponseEntityWithoutWrapper( + e, e.getErrorCode(), HttpStatus.SERVICE_UNAVAILABLE, MediaType.APPLICATION_JSON); + } catch (CredentialProcessingException e) { + log.error("Error processing credential download for walletId: {}", walletId, e); + return Utilities.getErrorResponseEntityWithoutWrapper( + e, e.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } + } + + /** + * Fetches all credentials for a given wallet. + * + * @param walletId The unique identifier of the wallet. + * @param locale The locale for credential retrieval. + * @param httpSession The HTTP session containing wallet key and ID. + * @return List of Verifiable Credential details. + * @throws InvalidRequestException If session or wallet ID is invalid. + */ + @Operation(summary = "Fetch all credentials for the given wallet", description = "This API is secured using session-based authentication. When a request is made, the session ID is extracted from the Cookie header and used to fetch session details from Redis for authentication. It then retrieves the wallet key from the session and uses it to decrypt all stored Verifiable Credentials for the given wallet. If successful, it returns a list of credentials otherwise an appropriate error is returned.", operationId = "fetchAllCredentialsForWallet", security = @SecurityRequirement(name = "SessionAuth"), parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the user's wallet from where the credential will be fetched", schema = @Schema(type = "string")), + @Parameter(name = "Accept-Language", in = ParameterIn.HEADER, description = "Locale is used to determine the language in which credentials should be rendered. It follows 2 letter language code", schema = @Schema(type = "string"))}) + @ApiResponse(responseCode = "200", description = "Credentials retrieved successfully", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = VerifiableCredentialResponseDTO.class)), examples = @ExampleObject(value = SwaggerExampleConstants.FETCH_ALL_CREDENTIALS_OF_WALLET_SUCCESS))) + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid Wallet Id", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Wallet ID not found in session", value = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"), + @ExampleObject(name = "Wallet key not found in session", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet key not found in session\"}"), + @ExampleObject(name = "Invalid Accept-Language header", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Locale must be a 2-letter code\"}"), + })) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"))) + @ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}"))) + @GetMapping + public ResponseEntity> fetchAllCredentialsForGivenWallet( + @PathVariable("walletId") @NotBlank(message = "Wallet ID cannot be blank") String walletId, + @RequestHeader(value = "Accept-Language", defaultValue = "en") @Pattern(regexp = "^[a-z]{2}$", message = "Locale must be a 2-letter code") String locale, + HttpSession httpSession) throws InvalidRequestException { + if (!LocaleUtils.isValidLanguageCode(locale)) { + log.info("Invalid Accept-Language header. Input was: {} ", locale); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Locale must be a valid 2-letter code"); + } + + validateWalletId(httpSession, walletId); + String base64EncodedWalletKey = WalletUtil.getSessionWalletKey(httpSession); + + log.info("Fetching all credentials for walletId: {}", walletId); + + List credentials = walletCredentialService.fetchAllCredentialsForWallet( + walletId, base64EncodedWalletKey, locale); + return ResponseEntity.status(HttpStatus.OK).body(credentials); + + } + + /** + * Fetches a specific Verifiable Credential as a PDF. + * + * @param walletId The unique identifier of the wallet. + * @param credentialId The unique identifier of the credential. + * @param locale The locale for credential retrieval. + * @param action The action (inline or download) for PDF display. + * @param httpSession The HTTP session containing wallet key and ID. + * @return The Verifiable Credential as a PDF stream. + * @throws InvalidRequestException If session or parameters are invalid. + */ + @Operation(summary = "Fetch a specific Verifiable Credential", description = "This API is protected using session-based authentication. When a request is received, the session ID is extracted from the Cookie header and used to fetch session data from Redis for user authentication. Upon successful authentication, the wallet key is retrieved from the session and used to decrypt the credential data obtained from the database.\n\nThe API retrieves a specific Verifiable Credential based on the provided wallet ID and credential ID. Depending on the action query parameter (inline or download) received in the request, the Content-Disposition header in the response is adjusted to either display the credential in the browser or prompt a file download. On success, the API returns the credential as a PDF byte stream. In case of any errors, an appropriate error response is returned.", operationId = "fetchVerifiableCredentialById", security = @SecurityRequirement(name = "SessionAuth"), parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the user's wallet", schema = @Schema(type = "string")), + @Parameter(name = "credentialId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the Verifiable Credential to be retrieved", schema = @Schema(type = "string")), + @Parameter(name = "Accept-Language", in = ParameterIn.QUERY, required = true, description = "Language preference in which the user expects the Verifiable Credential to be rendered", schema = @Schema(type = "string")), + @Parameter(name = "action", in = ParameterIn.QUERY, description = "Controls how the credential should be displayed. If sent as download, the response will include a header to download the file. Defaults to inline.", schema = @Schema(type = "string", defaultValue = "inline"))}) + @ApiResponse(responseCode = "200", description = "Verifiable Credential fetched successfully", headers = @Header(name = HttpHeaders.CONTENT_DISPOSITION, description = "Indicates if the verifiable credential is displayed inline or downloaded", schema = @Schema(type = "string", example = "inline; filename=\"credential.pdf\"")), content = @Content(mediaType = "application/pdf", schema = @Schema(type = "string", format = "binary"), examples = @ExampleObject(value = "PDF content in Binary Format"))) + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid Wallet Id", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Wallet key not found in session", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet key not found in session\"}"), + @ExampleObject(name = "Wallet ID not found in session", value = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"), + @ExampleObject(name = "Invalid Accept header", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Accept header must be application/pdf\"}"), + })) + @ApiResponse(responseCode = "404", description = "Not Found - Credential not found for given Wallet ID and Credential ID", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "No credentials found", value = "{\"errorCode\": \"resource_not_found\", \"errorMessage\": \"The requested resource doesn’t exist.\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = {@ExampleObject(name = "Failed to decrypt the Credential", value = "{\"errorCode\": \"credential_fetch_error\", \"errorMessage\": \"Decryption failed\"}"), @ExampleObject(name = "Credential Type is invalid", value = "{\"errorCode\": \"credential_fetch_error\", \"errorMessage\": \"Invalid credential type configuration\"}"), @ExampleObject(name = "Exception occurred while fetching Issuer config", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Unable to fetch issuer configuration\"}"), @ExampleObject(name = "Exception occurred when generating PDF", value = "{\"errorCode\": \"credential_download_error\", \"errorMessage\": \"Failed to generate credential PDF\"}"), @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}")})) + @ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}"))) + @GetMapping("/{credentialId}") + public ResponseEntity getVerifiableCredential( + @PathVariable("walletId") @NotBlank(message = "Wallet ID cannot be blank") String walletId, + @PathVariable("credentialId") @NotBlank(message = "Credential ID cannot be blank") String credentialId, + @RequestHeader(value = "Accept") String accept, + @RequestHeader(value = "Accept-Language", defaultValue = "en") @Pattern(regexp = "^[a-z]{2}$", message = "Locale must be a 2-letter code") String locale, + @RequestParam(value = "action", defaultValue = "inline") @Pattern(regexp = "^(inline|download)$", message = "Action must be 'inline' or 'download'") String action, + HttpSession httpSession) throws InvalidRequestException { + + try { + if (!Objects.equals(accept, MediaType.APPLICATION_PDF_VALUE)) { + log.error("Invalid Accept header: {}. Valid Accept headers are - [\"application/pdf\"]", accept); + throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Accept header must be application/pdf"); + } + validateWalletId(httpSession, walletId); + String base64EncodedWalletKey = WalletUtil.getSessionWalletKey(httpSession); + + log.info("Fetching credentialId: {} from walletId: {}", credentialId, walletId); + + WalletCredentialResponseDTO walletCredentialResponseDTO = walletCredentialService.fetchVerifiableCredential( + walletId, credentialId, base64EncodedWalletKey, locale); + + String dispositionType = "download".equalsIgnoreCase(action) ? "attachment" : "inline"; + String contentDisposition = String.format("%s; filename=\"%s\"", dispositionType, walletCredentialResponseDTO.getFileName()); + + return ResponseEntity.ok() + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + .contentType(MediaType.APPLICATION_PDF) + .body(walletCredentialResponseDTO.getFileContentStream()); + } catch (CredentialNotFoundException e) { + log.error("Credential not found for walletId: {} and credentialId: {}", walletId, credentialId, e); + return Utilities.getErrorResponseEntityWithoutWrapper( + e, e.getErrorCode(), HttpStatus.NOT_FOUND, MediaType.APPLICATION_JSON); + } catch (CredentialProcessingException e) { + log.error("Error processing credential for walletId: {} and credentialId: {}", walletId, credentialId, e); + return Utilities.getErrorResponseEntityWithoutWrapper( + e, e.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (InvalidRequestException exception) { + log.error("Error processing credential for walletId: {} and credentialId: {}", walletId, credentialId, exception); + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, exception.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (Exception exception){ + log.error("Unexpected error processing credential for walletId: {} and credentialId: {}", walletId, credentialId, exception); + return Utilities.getErrorResponseEntityWithoutWrapper( + new CredentialProcessingException(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), "We are unable to process request now"), + ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), + HttpStatus.INTERNAL_SERVER_ERROR, + MediaType.APPLICATION_JSON); + } + + } + + /** + * Deletes a credential by its ID + * + * @param walletId The ID of the wallet that owns the credential + * @param credentialId The ID of the credential to delete + * @return ResponseEntity with HTTP status 200 if successful, 404 if credential not found, or 500 for other errors + */ + @Operation(summary = SwaggerLiteralConstants.WALLET_CREDENTIALS_DELETE_SUMMARY, description = SwaggerLiteralConstants.WALLET_CREDENTIALS_DELETE_DESCRIPTION) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Credential successfully deleted"), + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid Wallet Id", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Wallet ID not found in session", value = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"), + })), + @ApiResponse(responseCode = "404", description = "Credential not found"), + @ApiResponse(responseCode = "500", description = "Internal server error", content = {@Content(mediaType = "application/json")})}) + @DeleteMapping("/{credentialId}") + public ResponseEntity deleteCredential(@PathVariable("walletId") String walletId, + @PathVariable("credentialId") String credentialId, + HttpSession httpSession) { + try { + log.info("Deleting credential with ID: {} for walletId: {}", credentialId, walletId); + + validateWalletId(httpSession, walletId); + + // Delete the credential + walletCredentialService.deleteCredential(credentialId, walletId); + return ResponseEntity.status(HttpStatus.OK).build(); + } catch (InvalidRequestException exception) { + log.error("Invalid request: {}", exception.getMessage()); + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, + exception.getErrorCode(), + HttpStatus.BAD_REQUEST, + MediaType.APPLICATION_JSON); + } catch (CredentialNotFoundException exception) { + log.error("Credential not found: {}", exception.getMessage()); + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, + exception.getErrorCode(), + HttpStatus.NOT_FOUND, + MediaType.APPLICATION_JSON); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/controller/WalletPresentationsController.java b/src/main/java/io/mosip/mimoto/controller/WalletPresentationsController.java new file mode 100644 index 000000000..e94bb0e36 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/WalletPresentationsController.java @@ -0,0 +1,233 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.MatchingCredentialsResponseDTO; +import io.mosip.mimoto.dto.MatchingCredentialsDTO; +import io.mosip.mimoto.dto.SubmitPresentationRequestDTO; +import io.mosip.mimoto.dto.VPAuthorizationRequestDTO; +import io.mosip.mimoto.dto.VPResponseDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.WalletPresentationService; +import io.mosip.mimoto.service.impl.SessionManager; +import io.mosip.mimoto.util.Utilities; +import io.mosip.mimoto.util.WalletUtil; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.time.Instant; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +@Slf4j +@RestController +@RequestMapping("/wallets/{walletId}/presentations") +public class WalletPresentationsController { + + private final WalletPresentationService walletPresentationService; + + private final SessionManager sessionManager; + + public WalletPresentationsController(WalletPresentationService walletPresentationService, SessionManager sessionManager) { + this.walletPresentationService = walletPresentationService; + this.sessionManager = sessionManager; + } + + /** + * Processes the Verifiable Presentation Authorization Request for a specific wallet. + * + * @param walletId The unique identifier of the wallet. + * @param httpSession The HTTP session containing wallet details such as wallet ID. + * @param vpAuthorizationRequest The Verifiable Presentation Authorization Request parameters. + * @return The processed Verifiable Presentation details, including information about the verifier. + */ + @Operation(summary = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUMMARY, description = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_DESCRIPTION, operationId = "processVPAuthorizationRequest", security = @SecurityRequirement(name = "SessionAuth"), parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "The unique identifier of the Wallet.", schema = @Schema(type = "string"))}, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body containing the Verifiable Presentation Authorization Request parameters.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = VPAuthorizationRequestDTO.class), + examples = @ExampleObject( + name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_REQ_EXAMPLE_NAME, + value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_REQ_EXAMPLE_VALUE + ) + ) + ) + ) + @ApiResponse(responseCode = "200", description = "Successfully processed the Verifiable Presentation Authorization Request.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = VPResponseDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUCCESS_EXAMPLE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_SUCCESS_EXAMPLE_VALUE))) + @ApiResponse(responseCode = "400", description = "Invalid request or missing required parameters.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_MISSING_RESPONSE_TYPE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_MISSING_RESPONSE_TYPE_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_INVALID_WALLET_ID_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_INVALID_WALLET_ID_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_WALLET_LOCKED_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_400_WALLET_LOCKED_VALUE) + })) + @ApiResponse(responseCode = "401", description = "Unauthorized user performing the Verifiable Presentation flow", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_401_USER_NOT_FOUND_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_401_USER_NOT_FOUND_VALUE))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_VERIFIERS_FETCH_FAILED_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_VERIFIERS_FETCH_FAILED_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_SERVER_ERROR_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_SERVER_ERROR_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_INVALID_URI_SYNTAX_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_AUTHORIZATION_500_INVALID_URI_SYNTAX_VALUE) + })) + @PostMapping + public ResponseEntity handleVPAuthorizationRequest(@PathVariable("walletId") String walletId, HttpSession httpSession, @RequestBody VPAuthorizationRequestDTO vpAuthorizationRequest) { + try { + WalletUtil.validateWalletId(httpSession, walletId); + + VPResponseDTO verifiablePresentationResponseDTO = walletPresentationService.handleVPAuthorizationRequest(vpAuthorizationRequest.getAuthorizationRequestUrl(), walletId); + + VerifiablePresentationSessionData verifiablePresentationSessionData = new VerifiablePresentationSessionData( + verifiablePresentationResponseDTO.getPresentationId(), + vpAuthorizationRequest.getAuthorizationRequestUrl(), + Instant.now(), + verifiablePresentationResponseDTO.getVerifiablePresentationVerifierDTO().isPreregisteredWithWallet(), + null, + verifiablePresentationResponseDTO.isDcql()); + + sessionManager.storePresentationSessionData(httpSession, verifiablePresentationSessionData, walletId); + + return ResponseEntity.status(HttpStatus.OK).body(verifiablePresentationResponseDTO); + } catch (OpenID4VPExceptions exception) { + log.error("Error occurred while processing the received VP Authorization Request from Verifier: ", exception); + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, exception.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (ApiNotAccessibleException | IOException | VPNotCreatedException exception) { + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, WALLET_CREATE_VP_EXCEPTION.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (URISyntaxException exception) { + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + } + + /** + * Gets matching credentials for a specific presentation request. + * + * @param walletId The unique identifier of the wallet. + * @param presentationId The unique identifier of the presentation. + * @param httpSession The HTTP session containing wallet details. + * @return The matching credentials response with available credentials and + * missing claims. + */ + @Operation(summary = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUMMARY, description = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_DESCRIPTION, operationId = "getMatchingCredentials", security = @SecurityRequirement(name = "SessionAuth"), parameters = {@Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "The unique identifier of the Wallet.", schema = @Schema(type = "string")), @Parameter(name = "presentationId", in = ParameterIn.PATH, required = true, description = "The unique identifier of the Presentation.", schema = @Schema(type = "string"))}) + @ApiResponse(responseCode = "200", description = "Successfully retrieved matching credentials.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MatchingCredentialsResponseDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUCCESS_EXAMPLE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_SUCCESS_EXAMPLE_VALUE))) + @ApiResponse(responseCode = "400", description = "Invalid request or missing required parameters.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = {@ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_INVALID_WALLET_ID_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_INVALID_WALLET_ID_VALUE), @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_PRESENTATION_NOT_FOUND_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_400_PRESENTATION_NOT_FOUND_VALUE)})) + @ApiResponse(responseCode = "401", description = "Unauthorized user performing the Verifiable Presentation flow", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_401_USER_NOT_FOUND_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_401_USER_NOT_FOUND_VALUE))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_500_SERVER_ERROR_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_GET_MATCHING_CREDENTIALS_500_SERVER_ERROR_VALUE))) + @GetMapping("/{presentationId}/credentials") + public ResponseEntity getMatchingCredentials(@PathVariable("walletId") String walletId, @PathVariable("presentationId") String presentationId, HttpSession httpSession) { + + WalletUtil.validateWalletId(httpSession, walletId); + + String base64Key = (String) httpSession.getAttribute(SessionKeys.WALLET_KEY); + if (base64Key == null) { + log.warn("Wallet key not found in session for walletId: {}", walletId); + return Utilities.getErrorResponseEntityFromPlatformErrorMessage(UNAUTHORIZED_ACCESS, HttpStatus.UNAUTHORIZED, MediaType.APPLICATION_JSON); + } + try { + VerifiablePresentationSessionData sessionData = sessionManager.getPresentationSessionData(httpSession, walletId, presentationId); + MatchingCredentialsDTO matchingCredentials = walletPresentationService.getMatchingCredentials(sessionData, walletId, base64Key); + // Store the matching credentials and pre-filtered matched credentials in session cache before returning + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData(httpSession, walletId, sessionData, matchingCredentials.getMatchingCredentials()); + return ResponseEntity.status(HttpStatus.OK).body(matchingCredentials.getMatchingCredentialsResponse()); + } catch (ApiNotAccessibleException | IOException | VPNotCreatedException exception) { + return Utilities.getErrorResponseEntityWithoutWrapper( + exception, WALLET_CREATE_VP_EXCEPTION.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (IllegalArgumentException exception) { + return Utilities.getErrorResponseEntityWithoutWrapper(exception, INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + + } + + /** + * Handles presentation submission or verifier rejection based on the request content. + * + * If the request contains selectedCredentials (and no error fields), it submits the presentation. + * If the request contains errorCode and errorMessage (and no selected credentials), it rejects the verifier. + * + * @param walletId The unique identifier of the wallet. + * @param httpSession The HTTP session containing wallet details. + * @param presentationId The unique identifier of the presentation. + * @param request The request containing either selected credentials or error details. + * @return Response indicating success/failure of the operation. + */ + @Operation( + summary = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_SUMMARY, + description = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_DESCRIPTION, + operationId = "handlePresentationAction", + security = @SecurityRequirement(name = "SessionAuth"), + parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "The unique identifier of the Wallet.", schema = @Schema(type = "string")), + @Parameter(name = "presentationId", in = ParameterIn.PATH, required = true, description = "The presentation session identifier.", schema = @Schema(type = "string")) + }, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Request body containing either selected credentials for submission or error details for rejection.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SubmitPresentationRequestDTO.class), + examples = { + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_EXAMPLE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_EXAMPLE_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_SDJWT_EXAMPLE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_SUBMIT_SDJWT_EXAMPLE_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_REJECT_EXAMPLE_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_REQ_REJECT_EXAMPLE_VALUE) + } + ) + ) + ) + @ApiResponse(responseCode = "200", description = "Successfully processed the request.", content = @Content(mediaType = "application/json", examples = { + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_200_SUBMITTED_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_200_SUBMITTED_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_200_REJECTED_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_200_REJECTED_VALUE) + })) + @ApiResponse(responseCode = "400", description = "Invalid request or missing required parameters.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_FORMAT_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_FORMAT_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_WALLET_ID_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_INVALID_WALLET_ID_VALUE), + @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_PRESENTATION_NOT_FOUND_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_400_PRESENTATION_NOT_FOUND_VALUE) + })) + @ApiResponse(responseCode = "401", description = "Unauthorized user", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_401_UNAUTHORIZED_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_401_UNAUTHORIZED_VALUE))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_500_SERVER_ERROR_NAME, value = SwaggerLiteralConstants.WALLET_PRESENTATIONS_HANDLE_ACTION_500_SERVER_ERROR_VALUE))) + @PatchMapping("/{presentationId}") + public ResponseEntity handlePresentationAction(@PathVariable("walletId") String walletId, HttpSession httpSession, @PathVariable("presentationId") String presentationId, @Valid @RequestBody SubmitPresentationRequestDTO request) { + + try { + WalletUtil.validateWalletId(httpSession, walletId); + + VerifiablePresentationSessionData vpSessionData = sessionManager.getPresentationSessionData(httpSession, walletId, presentationId); + + if (vpSessionData == null) { + log.error("No presentation session data found in session for presentationId: {}", presentationId); + return Utilities.getErrorResponseEntityWithoutWrapper(new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "presentationId not found in session"), INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + + String base64Key = (String) httpSession.getAttribute(SessionKeys.WALLET_KEY); + if (base64Key == null) { + log.warn("Wallet key not found in session for walletId: {}", walletId); + return Utilities.getErrorResponseEntityFromPlatformErrorMessage(UNAUTHORIZED_ACCESS, HttpStatus.UNAUTHORIZED, MediaType.APPLICATION_JSON); + } + + return walletPresentationService.handlePresentationAction(walletId, presentationId, request, vpSessionData, base64Key); + + } + catch (IllegalArgumentException exception){ + return Utilities.getErrorResponseEntityWithoutWrapper(exception, INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/controller/WalletTrustedVerifiersController.java b/src/main/java/io/mosip/mimoto/controller/WalletTrustedVerifiersController.java new file mode 100644 index 000000000..a06a2cdd8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/WalletTrustedVerifiersController.java @@ -0,0 +1,97 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.service.TrustedVerifierService; +import io.mosip.mimoto.util.Utilities; +import io.mosip.mimoto.util.WalletUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.mosip.mimoto.dto.TrustedVerifierResponseDTO; + +import static io.mosip.mimoto.exception.ErrorConstants.ERROR_ADDING_TRUSTED_VERIFIER; + +@Slf4j +@RestController +@Validated +@RequestMapping("/wallets/{walletId}/trusted-verifiers") +public class WalletTrustedVerifiersController { + + private final TrustedVerifierService trustedVerifierService; + + public WalletTrustedVerifiersController(TrustedVerifierService trustedVerifierService) { + this.trustedVerifierService = trustedVerifierService; + } + + /** + * Adds a trusted verifier for the specified wallet. + * + * Validates that the caller has an active session and that the provided + * path `walletId` matches the wallet stored in the session. The request + * body contains verifier details (for example: verifierId. + * On success the created verifier record is returned with HTTP 201 Created. + * + * @param walletId The unique identifier of the wallet. Must match the wallet id in session. + * @param trustedVerifierRequest Trusted verifier details required to register the verifier for the wallet. + * @param httpSession HTTP session containing authenticated user and wallet information. + * @return ResponseEntity containing the created TrustedVerifierResponseDTO and HTTP 201 status on success. + */ + @Operation( + summary = "Add a trusted verifier to a wallet", + description = "Adds a trusted verifier for the specified wallet after validating the session and request. " + + "Requires an active session; the path walletId must match the wallet in session. Returns the created verifier details on success.", + operationId = "addTrustedVerifier", + security = @SecurityRequirement(name = "SessionAuth"), + parameters = { + @Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "The unique identifier of the wallet.", schema = @Schema(type = "string")) + }, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Trusted verifier details to add to the wallet.", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = TrustedVerifierRequest.class), + examples = @ExampleObject( + name = "Trusted Verifier Request", + value = "{ \"verifierId\": \"mock-client\" }" + ) + ) + ) + ) + @ApiResponse(responseCode = "201", description = "Trusted verifier created successfully.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = TrustedVerifierResponseDTO.class), examples = @ExampleObject(name = "Success response", value = "{ \"id\": \"2d598dfc-7218-cb7a7afd5a07\" }"))) + @ApiResponse(responseCode = "400", description = "Invalid request or missing required parameters.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Missing verifierId", value = "{\"errorCode\":\"invalid_request\",\"errorMessage\":\"Missing Input: verifierId is required\"}"), + @ExampleObject(name = "Invalid Wallet ID", value = "{\"errorCode\":\"invalid_request\",\"errorMessage\":\"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Duplicate verifier", value = "{\"errorCode\":\"duplicate_verifier\",\"errorMessage\":\"Verifier is already trusted for this wallet\"}") + })) + @ApiResponse(responseCode = "401", description = "Unauthorized - session invalid or missing user.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unauthorized", value = "{\"errorCode\":\"unauthorized\",\"errorMessage\":\"User ID not found in session\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\":\"internal_server_error\",\"errorMessage\":\"Failed to add trusted verifier\"}"))) + @PostMapping + public ResponseEntity addTrustedVerifier(@PathVariable(name = "walletId") String walletId, @Valid @RequestBody TrustedVerifierRequest trustedVerifierRequest, HttpSession httpSession) { + try { + WalletUtil.validateWalletId(httpSession, walletId); + + TrustedVerifierResponseDTO trustedVerifierResponseDTO = trustedVerifierService.addTrustedVerifier(walletId, trustedVerifierRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(trustedVerifierResponseDTO); + } catch (DataAccessException e) { + log.error("Unexpected error while adding trusted verifier for walletId: {} - Error: {}", walletId, e.getMessage(), e); + return Utilities.getErrorResponseEntityWithoutWrapper(e, + ERROR_ADDING_TRUSTED_VERIFIER.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } + } + +} diff --git a/src/main/java/io/mosip/mimoto/controller/WalletsController.java b/src/main/java/io/mosip/mimoto/controller/WalletsController.java new file mode 100644 index 000000000..251352a0d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/controller/WalletsController.java @@ -0,0 +1,202 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.constant.SwaggerLiteralConstants; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import io.mosip.mimoto.service.WalletService; +import io.mosip.mimoto.util.WalletUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static io.mosip.mimoto.constant.SwaggerLiteralConstants.RETRIEVE_ALL_WALLETS_DESCRIPTION; +import static io.mosip.mimoto.constant.SwaggerLiteralConstants.RETRIEVE_ALL_WALLETS_SUMMARY; + + +/** + * REST controller for managing wallets. + */ +@Slf4j +@RestController +@RequestMapping(value = "/wallets") +@Tag(name = SwaggerLiteralConstants.WALLETS_NAME, description = SwaggerLiteralConstants.WALLETS_DESCRIPTION) +public class WalletsController { + + private final WalletService walletService; + + @Autowired + public WalletsController(WalletService walletService) { + this.walletService = walletService; + } + + /** + * Creates a new Wallet for a user. + * + * @param wallet The Wallet creation request containing name and PIN. + * @param httpSession The HTTP session containing user details. + * @return The unique identifier of the created Wallet. + * @throws InvalidRequestException If the request is invalid. + * Example: Wallet PIN and Confirm Wallet PIN received in request don't match + */ + @Operation( + summary = "Create a new wallet", + description = "This API is secured using session-based authentication. The session ID is extracted from the Cookie header and used to retrieve session details from Redis for authentication. The user ID is obtained from the session and used with the provided wallet name and PIN to create a new wallet in the database. If successful, a unique identifier for the wallet is returned; otherwise, an appropriate error response is provided.", + operationId = "createWallet", + security = @SecurityRequirement(name = "SessionAuth"), + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "Wallet creation request containing the wallet name and PIN", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = CreateWalletRequestDto.class), + examples = @ExampleObject(value = "{\"walletName\": \"My Personal Wallet\", \"walletPin\": \"123456\"}")) + ) + ) + @ApiResponse(responseCode = "200", description = "Wallet created successfully", content = @Content(mediaType = "application/json", schema = @Schema(type = "string", description = "Unique identifier of the Wallet stored in the database"), examples = @ExampleObject(name = "Success response", value = "{\"walletId\":\"d94a2e70-1234-4f2f-9eae-98765\", \"walletName\":\"My Wallet\"}"))) + @ApiResponse(responseCode = "400", description = "Invalid wallet creation request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid User ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"User ID cannot be null or empty\"}"), + @ExampleObject(name = "Invalid Wallet name", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet name must be alphanumeric with allowed special characters\"}"), + @ExampleObject(name = "Invalid Wallet Pin", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"\"}"), + @ExampleObject(name = "Wallet Pin and confirm Pin Mismatch", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Entered PIN and Confirm PIN do not match\"}")})) + @ApiResponse(responseCode = "401", description = "Unauthorized access", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "User ID not found in session", value = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error while creating wallet", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"))) + @ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}"))) + @PostMapping + public ResponseEntity createWallet( + @RequestBody @Valid CreateWalletRequestDto wallet, + HttpSession httpSession) throws InvalidRequestException, UnauthorizedAccessException { + String userId = (String) httpSession.getAttribute(SessionKeys.USER_ID); + log.info("Creating wallet for user: {}, name: {}", userId, wallet.getWalletName()); + WalletResponseDto walletResponseDto = walletService.createWallet(userId, wallet.getWalletName(), wallet.getWalletPin(), wallet.getConfirmWalletPin()); + return ResponseEntity.status(HttpStatus.OK).body(walletResponseDto); + } + + /** + * Retrieves all wallets for the authenticated user. + * + * @param httpSession The HTTP session containing user details. + * @return List of Wallet details. + * @throws InvalidRequestException If the request fails. + */ + @Operation( + summary = RETRIEVE_ALL_WALLETS_SUMMARY, + description = RETRIEVE_ALL_WALLETS_DESCRIPTION, + operationId = "getWallets", + security = @SecurityRequirement(name = "SessionAuth") + ) + @ApiResponse(responseCode = "200", description = "List of wallets retrieved successfully", content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = WalletDetailsResponseDto.class)), examples = @ExampleObject(name = "Success response", value = "[{\"walletName\": \"My Personal Wallet1\",\"walletId\": \"123e4567-e89b-12d3-a456-426614174000\", \"walletStatus\": null}, {\"walletName\": \"My Personal Wallet2\",\"walletId\": \"223e4567-e89b-12d3-a456-426614174001\", \"walletStatus\": \"temporarily_locked\"}]"))) + @ApiResponse(responseCode = "400", description = "Invalid Wallets fetching request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Invalid User ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"User ID cannot be null or empty\"}"))) + @ApiResponse(responseCode = "401", description = "Unauthorized access", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "User ID not found in session", value = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"))) + @ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}"))) + @GetMapping + public ResponseEntity> getWallets(HttpSession httpSession) throws InvalidRequestException { + String userId = (String) httpSession.getAttribute(SessionKeys.USER_ID); + log.info("Retrieving wallets for user: {}", userId); + List response = walletService.getWallets(userId); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + /** + * Unlocks an existing wallet by retrieving its key and storing it in the session. + * + * @param walletId The unique identifier of the Wallet. + * @param wallet The request containing the Wallet PIN. + * @param httpSession The HTTP session containing user details. + * @return The wallet details if unlocked successfully. + * @throws InvalidRequestException If the wallet or PIN is invalid. + */ + @Operation( + summary = "Unlock an existing Wallet", + description = "This API is secured using session-based authentication. The session ID is extracted from the Cookie header to authenticate the user. The user's ID is obtained from the session, and the provided wallet ID and PIN are used to fetch the wallet key, which is stored in the session. If successful, the wallet ID is returned; otherwise, an appropriate error response is returned.", + operationId = "unlockWallet", + security = @SecurityRequirement(name = "SessionAuth"), + parameters = {@Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the wallet to be unlocked", schema = @Schema(type = "string"))}, + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = UnlockWalletRequestDto.class))) + ) + @ApiResponse(responseCode = "200", description = "Wallet unlocked successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = WalletResponseDto.class), examples = @ExampleObject(name = "Success response", value = "{\"walletId\": \"123e4567-e89b-12d3-a456-426614174000\"}"))) + @ApiResponse(responseCode = "400", description = "Invalid Wallet unlock request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Wallet not found", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet not found\"}"), + @ExampleObject(name = "Invalid User ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"User ID cannot be null or empty\"}"), + @ExampleObject(name = "Invalid Wallet Pin", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"PIN must be numeric with 6 digits\"}"), + @ExampleObject(name = "Incorrect Wallet PIN", value = "{\"errorCode\": \"invalid_pin\", \"errorMessage\": \"Invalid PIN or wallet key provided\"}"), + @ExampleObject(name = "Only one attempt left to unlock Wallet before Permanent Lockout", value = "{\"errorCode\": \"last_attempt_before_lockout\", \"errorMessage\": \"Incorrect passcode. Last attempt remaining before your Wallet is permanently locked\"}") + })) + @ApiResponse(responseCode = "401", description = "Unauthorized access", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "User ID not found in session", value = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"))) + @ApiResponse(responseCode = "423", description = "Wallet locked Temporarily or Permanently", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Wallet is locked Temporarily", value = "{\"errorCode\": \"temporarily_locked\", \"errorMessage\": \"You’ve reached the maximum number of attempts. Your wallet is now temporarily locked\"}"), + @ExampleObject(name = "Wallet is locked Permanently", value = "{\"errorCode\": \"permanently_locked\", \"errorMessage\": \"Your wallet has been permanently locked due to multiple failed attempts. Please click on forgot password to reset your wallet to continue\"}") + })) + @ApiResponse(responseCode = "500", description = "Internal server error - any error occurred while decrypting the Wallet key with Wallet PIN or when fetching Wallet details from the database", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"))) + @ApiResponse(responseCode = "503", description = "Service unavailable", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Database connection failure", value = "{\"errorCode\": \"database_unavailable\", \"errorMessage\": \"Failed to connect to the database\"}"))) + @PostMapping("/{walletId}/unlock") + public ResponseEntity unlockWallet( + @PathVariable("walletId") @NotBlank(message = "Wallet ID cannot be blank") String walletId, + @RequestBody @Valid UnlockWalletRequestDto wallet, + HttpSession httpSession) throws InvalidRequestException { + String userId = (String) httpSession.getAttribute(SessionKeys.USER_ID); + log.info("Unlocking wallet: {} for user: {}", walletId, userId); + WalletResponseDto responseDto = walletService.unlockWallet(walletId, wallet.getWalletPin(), userId); + + httpSession.setAttribute(SessionKeys.WALLET_KEY, responseDto.getDecryptedWalletKey()); + httpSession.setAttribute(SessionKeys.WALLET_ID, walletId); + return ResponseEntity.status(HttpStatus.OK).body(responseDto); + } + + /** + * Deletes a Wallet by its ID + * + * @param walletId The ID of the Wallet to delete + * @param httpSession The HTTP session containing user details + * @return ResponseEntity with HTTP status 200 if successful, 401 if unauthorized + * @throws InvalidRequestException If input parameters or session are invalid. + */ + @Operation( + summary = SwaggerLiteralConstants.WALLETS_DELETE_SUMMARY, + description = SwaggerLiteralConstants.WALLETS_DELETE_DESCRIPTION, + operationId = "deleteWallet", + security = @SecurityRequirement(name = "SessionAuth"), + parameters = {@Parameter(name = "walletId", in = ParameterIn.PATH, required = true, description = "Unique identifier of the wallet to be deleted", schema = @Schema(type = "string"))} + ) + @ApiResponse(responseCode = "200", description = "Wallet successfully deleted") + @ApiResponse(responseCode = "400", description = "Invalid Wallet deletion request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = { + @ExampleObject(name = "Invalid User ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"User ID cannot be null or empty\"}"), + @ExampleObject(name = "Invalid Wallet ID", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Invalid Wallet ID. Session and request Wallet ID do not match\"}"), + @ExampleObject(name = "Wallet ID not found in session", value = "{\"errorCode\": \"wallet_locked\", \"errorMessage\": \"Wallet is locked\"}"), + @ExampleObject(name = "Wallet not found in database", value = "{\"errorCode\": \"invalid_request\", \"errorMessage\": \"Wallet not found\"}")})) + @ApiResponse(responseCode = "401", description = "Unauthorized user deleting a Wallet", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "User ID not found in session", value = "{\"errorCode\": \"unauthorized\", \"errorMessage\": \"User ID not found in session\"}"))) + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDTO.class), examples = @ExampleObject(name = "Unexpected Server Error", value = "{\"errorCode\": \"internal_server_error\", \"errorMessage\": \"We are unable to process request now\"}"))) + @DeleteMapping("/{walletId}") + public ResponseEntity deleteWallet(@PathVariable("walletId") String walletId, HttpSession httpSession) { + String userId = (String) httpSession.getAttribute(SessionKeys.USER_ID); + String sessionWalletId = (String) httpSession.getAttribute(SessionKeys.WALLET_ID); + if (sessionWalletId != null) { + WalletUtil.validateWalletId(httpSession, walletId); + httpSession.removeAttribute(SessionKeys.WALLET_KEY); + httpSession.removeAttribute(SessionKeys.WALLET_ID); + } + + walletService.deleteWallet(userId, walletId); + log.info("Cleared Wallet session attributes for Wallet with Id: {}", walletId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/io/mosip/mimoto/core/http/RequestWrapper.java b/src/main/java/io/mosip/mimoto/core/http/RequestWrapper.java index d9a0b2c9f..f0482c081 100644 --- a/src/main/java/io/mosip/mimoto/core/http/RequestWrapper.java +++ b/src/main/java/io/mosip/mimoto/core/http/RequestWrapper.java @@ -1,7 +1,7 @@ package io.mosip.mimoto.core.http; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; diff --git a/src/main/java/io/mosip/mimoto/core/http/ResponseWrapper.java b/src/main/java/io/mosip/mimoto/core/http/ResponseWrapper.java index 14b794e03..0be513708 100644 --- a/src/main/java/io/mosip/mimoto/core/http/ResponseWrapper.java +++ b/src/main/java/io/mosip/mimoto/core/http/ResponseWrapper.java @@ -1,25 +1,20 @@ package io.mosip.mimoto.core.http; -import java.util.ArrayList; -import java.util.List; - -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import io.mosip.mimoto.dto.ErrorDTO; import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; @Data +@NoArgsConstructor public class ResponseWrapper { - private String id; - private String version; - String str; - private String responsetime; - private Object metadata; @NotNull @Valid private T response; - private List errors = new ArrayList<>(); - } diff --git a/src/main/java/io/mosip/mimoto/dto/AuditRequestDto.java b/src/main/java/io/mosip/mimoto/dto/AuditRequestDto.java index 611fbd491..0259fa928 100644 --- a/src/main/java/io/mosip/mimoto/dto/AuditRequestDto.java +++ b/src/main/java/io/mosip/mimoto/dto/AuditRequestDto.java @@ -1,7 +1,7 @@ package io.mosip.mimoto.dto; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/io/mosip/mimoto/dto/BackgroundImageDTO.java b/src/main/java/io/mosip/mimoto/dto/BackgroundImageDTO.java new file mode 100644 index 000000000..22f306bdf --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/BackgroundImageDTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto; + +import com.google.gson.annotations.Expose; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BackgroundImageDTO { + @Expose + @URL + @NotBlank + String uri; +} diff --git a/src/main/java/io/mosip/mimoto/dto/CreateWalletRequestDto.java b/src/main/java/io/mosip/mimoto/dto/CreateWalletRequestDto.java new file mode 100644 index 000000000..6b226097c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/CreateWalletRequestDto.java @@ -0,0 +1,21 @@ +package io.mosip.mimoto.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Data +@ToString +@ApiModel(description = "Model representing a create user wallet request") +@Schema(description = "Create Wallet Request DTO", requiredProperties = {"walletPin", "confirmWalletPin"}) +public class CreateWalletRequestDto { + @Schema(description = "PIN used to unlock the Wallet") + String walletPin; + + @Schema(description = "Re-entered PIN used to confirm the Wallet PIN") + String confirmWalletPin; + + @Schema(description = "Name of the Wallet") + String walletName; +} diff --git a/src/main/java/io/mosip/mimoto/dto/CredentialDTO.java b/src/main/java/io/mosip/mimoto/dto/CredentialDTO.java new file mode 100644 index 000000000..92e0ea468 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/CredentialDTO.java @@ -0,0 +1,44 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CredentialDTO { + + @JsonProperty("credentialId") + @Schema(description = "Unique identifier of the credential") + private String credentialId; + + @JsonProperty("credentialTypeDisplayName") + @Schema(description = "Display name of the credential type") + private String credentialTypeDisplayName; + + @JsonProperty("credentialTypeLogo") + @Schema(description = "Logo URL for the credential type") + private String credentialTypeLogo; + + @JsonProperty("format") + @Schema(description = "Format of the credential (e.g., ldp_vc, vc+sd-jwt, dc+sd-jwt)") + private String format; + + @JsonProperty("claims") + @Schema(description = "Claims that are always disclosed as part of the credential") + private List claims; + + @JsonProperty("sdClaims") + @Schema(description = "Selective Disclosure Claims that require user consent to be shared") + private List sdClaims; +} + diff --git a/src/main/java/io/mosip/mimoto/dto/CredentialSetInfo.java b/src/main/java/io/mosip/mimoto/dto/CredentialSetInfo.java new file mode 100644 index 000000000..a0beefb76 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/CredentialSetInfo.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CredentialSetInfo { + + @JsonProperty("required") + @Schema(description = "True when the user must satisfy one option in this section") + private boolean required; + + @JsonProperty("options") + @Schema(description = "List of options; each option is a list of queryIds that must all be presented together") + private List> options; +} diff --git a/src/main/java/io/mosip/mimoto/dto/DataShare.java b/src/main/java/io/mosip/mimoto/dto/DataShare.java index 2ca7181c1..9b4ff2a08 100644 --- a/src/main/java/io/mosip/mimoto/dto/DataShare.java +++ b/src/main/java/io/mosip/mimoto/dto/DataShare.java @@ -2,6 +2,8 @@ import lombok.Data; +import java.io.Serializable; + /** * Instantiates a new data share. */ @@ -10,7 +12,7 @@ * Instantiates a new data share. */ @Data -public class DataShare { +public class DataShare implements Serializable { /** The url. */ private String url; diff --git a/src/main/java/io/mosip/mimoto/dto/DataShareResponseDto.java b/src/main/java/io/mosip/mimoto/dto/DataShareResponseDto.java index bbd2051c0..96a5a8203 100644 --- a/src/main/java/io/mosip/mimoto/dto/DataShareResponseDto.java +++ b/src/main/java/io/mosip/mimoto/dto/DataShareResponseDto.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; @Data -@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor public class DataShareResponseDto extends BaseRestResponseDTO { private static final long serialVersionUID = 1L; diff --git a/src/main/java/io/mosip/mimoto/dto/DcqlCredentialSelection.java b/src/main/java/io/mosip/mimoto/dto/DcqlCredentialSelection.java new file mode 100644 index 000000000..85347d6a8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/DcqlCredentialSelection.java @@ -0,0 +1,26 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DcqlCredentialSelection { + + @JsonProperty("queryId") + private String queryId; + + @JsonProperty("selectedCredentialIds") + private List selectedCredentialIds; + + @JsonProperty("selectedSdClaims") + private Map> selectedSdClaims; +} diff --git a/src/main/java/io/mosip/mimoto/dto/DcqlQueryGroup.java b/src/main/java/io/mosip/mimoto/dto/DcqlQueryGroup.java new file mode 100644 index 000000000..bdbc8b554 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/DcqlQueryGroup.java @@ -0,0 +1,36 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DcqlQueryGroup { + + @JsonProperty("queryId") + @Schema(description = "DCQL credential query id (dcql_query.credentials[i].id)") + private String queryId; + + @JsonProperty("required") + @Schema(description = "Whether this query must be satisfied before submission") + private boolean required; + + @JsonProperty("multiple") + @Schema(description = "Whether more than one credential may be selected for this query") + private boolean multiple; + + @JsonProperty("availableCredentials") + private List availableCredentials; + + @JsonProperty("missingClaims") + private Set missingClaims; +} diff --git a/src/main/java/io/mosip/mimoto/dto/DecryptedCredentialDTO.java b/src/main/java/io/mosip/mimoto/dto/DecryptedCredentialDTO.java new file mode 100644 index 000000000..5589945a8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/DecryptedCredentialDTO.java @@ -0,0 +1,58 @@ +package io.mosip.mimoto.dto; + +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.model.CredentialMetadata; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; + +/** + * DTO that contains decrypted credential data for session caching. + * This avoids the need to decrypt credentials multiple times. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DecryptedCredentialDTO implements Serializable { + + /** + * The credential ID from the original VerifiableCredential. + */ + private String id; + + /** + * The wallet ID from the original VerifiableCredential. + */ + private String walletId; + + /** + * The decrypted credential data as VCCredentialResponse. + */ + private VCCredentialResponse credential; + + /** + * The credential metadata from the original VerifiableCredential. + */ + private CredentialMetadata credentialMetadata; + + /** + * The creation timestamp from the original VerifiableCredential. + */ + private Instant createdAt; + + /** + * The update timestamp from the original VerifiableCredential. + */ + private Instant updatedAt; + + /** + * InputDescriptor.id (Draft-23) or DCQL CredentialQuery.id (OVP 1.0). + * Bridge key from credential matching to VP submission. + */ + private String identifier; +} diff --git a/src/main/java/io/mosip/mimoto/dto/DisplayDTO.java b/src/main/java/io/mosip/mimoto/dto/DisplayDTO.java new file mode 100644 index 000000000..2689a0f1f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/DisplayDTO.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.dto; + +import com.google.gson.annotations.Expose; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; + +@Data +public class DisplayDTO { + @Expose + @NotBlank + @Schema(description = "Display Name of the Issuer") + String name; + @Expose + @Valid + @Schema(description = "Display Logo of the Issuer") + LogoDTO logo; + @Expose + @NotBlank + @Schema(description = "Display title of the Issuer") + String title; + @Expose + @NotBlank + @Schema(description = "Display description of the Issuer") + String description; + @Expose + @NotBlank + @Schema(description = "Display language of the Issuer") + String language; +} diff --git a/src/main/java/io/mosip/mimoto/dto/ErrorDTO.java b/src/main/java/io/mosip/mimoto/dto/ErrorDTO.java index b10d156e4..5656b4037 100644 --- a/src/main/java/io/mosip/mimoto/dto/ErrorDTO.java +++ b/src/main/java/io/mosip/mimoto/dto/ErrorDTO.java @@ -4,7 +4,10 @@ import com.fasterxml.jackson.annotation.JsonAlias; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,19 +16,24 @@ * * @param errorcode the errorcode * @param message the message - * + * * @author Rishabh Keshari */ @Data @AllArgsConstructor @NoArgsConstructor +@Builder public class ErrorDTO implements Serializable { private static final long serialVersionUID = 2452990684776944908L; /** The errorcode. */ + @Schema(description = "It represents the type or category of the error") + @NotBlank(message = "errorCode is required") private String errorCode; + @Schema(description = "A human-readable message providing more details about the error") + @NotBlank(message = "errorMessage is required") /** The message. */ @JsonAlias("message") private String errorMessage; diff --git a/src/main/java/io/mosip/mimoto/dto/IssuerDTO.java b/src/main/java/io/mosip/mimoto/dto/IssuerDTO.java new file mode 100644 index 000000000..0342096db --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/IssuerDTO.java @@ -0,0 +1,68 @@ +package io.mosip.mimoto.dto; + +import io.mosip.mimoto.model.QRCodeType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import java.util.List; + + +@Data +public class IssuerDTO { + @NotBlank + @Schema(description = "Unique Identifier of the Issuer") + String issuer_id; + + @NotBlank + @Schema(description = "protocol of the download flow", allowableValues = {"OTP", "OpenId4VCI"}) + String protocol; + + @Valid + @NotEmpty + @Schema(description = "Display Properties of the Issuer") + List display; + + @NotBlank + @Schema(description = "Client Id of the Onboarded Mimoto OIDC Client") + String client_id; + + @URL + @Schema(description = "Wellknown endpoint of the credential issuer") + String wellknown_endpoint; + + @Schema(description = "Redirect URI configured while creating the OIDC Client") + String redirect_uri; + + @Schema(description = "Authorization Audience for retrieving Token from token endpoint") + String authorization_audience; + + @URL + @Schema(description = "Mimoto Token Endpoint Fetching the Token From Authorization Server with Client Assertion") + String token_endpoint; + + @URL + @Schema(description = "Token Endpoint for Fetching the Token From Authorization Server") + String proxy_token_endpoint; + + @NotBlank + @Schema(description = "Client Alias of the Issuer in the keyStore file") + String client_alias; + + @Schema(description = "QR code type of issuer is used to decide whether the downloaded Verifiable Credential is allowed for online sharing or not") + QRCodeType qr_code_type; + + @NotBlank + @Schema(description = "Toggle to Enable / Disable the Issuer", defaultValue = "false") + String enabled; + + @Schema(description = "Unique Identifier of the Issuer") + String credential_issuer; + + @URL + @NotBlank + @Schema(description = "Credential Issuer Host") + String credential_issuer_host; +} diff --git a/src/main/java/io/mosip/mimoto/dto/IssuerV2DTO.java b/src/main/java/io/mosip/mimoto/dto/IssuerV2DTO.java new file mode 100644 index 000000000..107ff47e9 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/IssuerV2DTO.java @@ -0,0 +1,61 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.mosip.mimoto.model.QRCodeType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import java.util.List; + +@Data +public class IssuerV2DTO { + + @NotBlank + @Schema(description = "Unique Identifier of the Issuer") + @JsonProperty("issuer_id") + private String issuerId; + + @NotBlank + @Schema(description = "protocol of the download flow", allowableValues = {"OTP", "OpenId4VCI"}) + private String protocol; + + @Valid + @NotEmpty + @Schema(description = "Display Properties of the Issuer") + private List display; + + @NotBlank + @Schema(description = "Client Id of the Onboarded Mimoto OIDC Client") + @JsonProperty("client_id") + private String clientId; + + @NotBlank + @JsonProperty("client_alias") + @Schema(description = "Client Alias of the Issuer in the keyStore file") + private String clientAlias; + + @URL + @JsonProperty("token_endpoint") + @Schema(description = "Mimoto Token Endpoint Fetching the Token From Authorization Server with Client Assertion") + private String tokenEndpoint; + + + @JsonProperty("qr_code_type") + @Schema(description = "QR code type of issuer is used to decide whether the downloaded Verifiable Credential is allowed for online sharing or not") + private QRCodeType qrCodeType; + + @NotBlank + @Schema(description = "Toggle to Enable / Disable the Issuer", defaultValue = "false") + private String enabled; + + @URL + @NotBlank + @JsonProperty("credential_issuer_host") + @Schema(description = "Credential Issuer Host") + private String credentialIssuerHost; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/IssuersDTO.java b/src/main/java/io/mosip/mimoto/dto/IssuersDTO.java new file mode 100644 index 000000000..a3abee216 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/IssuersDTO.java @@ -0,0 +1,23 @@ +package io.mosip.mimoto.dto; + +import com.google.gson.annotations.Expose; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import jakarta.validation.Valid; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IssuersDTO { + + @Expose + @Valid + @Schema(description = "List of Onboarded Issuers") + List issuers; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/IssuersV2DTO.java b/src/main/java/io/mosip/mimoto/dto/IssuersV2DTO.java new file mode 100644 index 000000000..2d7d5ae23 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/IssuersV2DTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import jakarta.validation.Valid; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class IssuersV2DTO { + + @Valid + @Schema(description = "List of Onboarded Issuers") + List issuers; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/LogoDTO.java b/src/main/java/io/mosip/mimoto/dto/LogoDTO.java new file mode 100644 index 000000000..9d8fe49c1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/LogoDTO.java @@ -0,0 +1,26 @@ +package io.mosip.mimoto.dto; + + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.URL; + +@Data +public class LogoDTO { + @Expose + @NotBlank + @URL + @JsonAlias("uri") + @SerializedName(value = "url", alternate = {"uri"}) + @Schema(description = "Display logo uri of the Issuer") + String url; + @Expose + @NotBlank + @Schema(description = "Display logo alt text of the Issuer") + String alt_text; +} diff --git a/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsDTO.java b/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsDTO.java new file mode 100644 index 000000000..c508150a1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsDTO.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * DTO that contains both the matching credentials response and the wallet credentials from repository. + * This helps avoid duplicate database hits when both pieces of data are needed. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MatchingCredentialsDTO { + + private MatchingCredentialsResponseDTO matchingCredentialsResponse; + + @JsonIgnore + private List matchingCredentials; +} diff --git a/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsResponseDTO.java new file mode 100644 index 000000000..2e048e982 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/MatchingCredentialsResponseDTO.java @@ -0,0 +1,36 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MatchingCredentialsResponseDTO { + + @JsonProperty("availableCredentials") + @Schema(description = "Flat credential list for Draft-23") + private List availableCredentials; + + @JsonProperty("missingClaims") + @Schema(description = "Missing claims for Draft-23") + private Set missingClaims; + + @JsonProperty("queryGroups") + @Schema(description = "Per-query groups for OVP 1.0 / DCQL") + private List queryGroups; + + @JsonProperty("credentialSets") + @Schema(description = "Option-grouping layer from the verifier's DCQL credential_sets; synthesised from individual queries when absent in the verifier request") + private List credentialSets; +} diff --git a/src/main/java/io/mosip/mimoto/dto/ProviderDataConfig.java b/src/main/java/io/mosip/mimoto/dto/ProviderDataConfig.java new file mode 100644 index 000000000..d1cc9b630 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/ProviderDataConfig.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The ProviderDataConfig class represents a configuration for provider data attributes. + * It contains fields for various attributes such as username, name, email, picture, and phone number. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProviderDataConfig { + private String userNameAttribute; + private String nameAttribute; + private String emailAttribute; + private String pictureAttribute; + private String phoneNumberAttribute; +} diff --git a/src/main/java/io/mosip/mimoto/dto/SecretKeyRequest.java b/src/main/java/io/mosip/mimoto/dto/SecretKeyRequest.java index eefd71811..bb1e945e3 100644 --- a/src/main/java/io/mosip/mimoto/dto/SecretKeyRequest.java +++ b/src/main/java/io/mosip/mimoto/dto/SecretKeyRequest.java @@ -1,12 +1,12 @@ package io.mosip.mimoto.dto; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; +import lombok.*; @Data @Setter @Getter +@AllArgsConstructor +@NoArgsConstructor public class SecretKeyRequest { public String clientId; public String secretKey; diff --git a/src/main/java/io/mosip/mimoto/dto/SelectedCredentials.java b/src/main/java/io/mosip/mimoto/dto/SelectedCredentials.java new file mode 100644 index 000000000..0e66175bb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/SelectedCredentials.java @@ -0,0 +1,56 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.mosip.mimoto.dto.deserializer.SelectedCredentialsDeserializer; +import io.mosip.mimoto.dto.serializer.SelectedCredentialsSerializer; +import lombok.Getter; + +import java.util.List; + +/** + * Polymorphic holder for {@code selectedCredentials} in submit requests: + *
    + *
  • Draft-23: {@code ["cred-id-1", "cred-id-2"]}
  • + *
  • DCQL: {@code [{"queryId":"q1","selectedCredentialIds":["cred-id-1"]}]}
  • + *
+ */ +@Getter +@JsonDeserialize(using = SelectedCredentialsDeserializer.class) +@JsonSerialize(using = SelectedCredentialsSerializer.class) +public class SelectedCredentials { + + private final List credentialIds; + private final List dcqlSelections; + + private SelectedCredentials(List credentialIds, List dcqlSelections) { + if (credentialIds != null && dcqlSelections != null) { + throw new IllegalArgumentException( + "selectedCredentials must be either credential ID strings or DCQL selection objects, not both"); + } + this.credentialIds = credentialIds; + this.dcqlSelections = dcqlSelections; + } + + public static SelectedCredentials ofStrings(List ids) { + return new SelectedCredentials(ids, null); + } + + public static SelectedCredentials ofDcql(List selections) { + return new SelectedCredentials(null, selections); + } + + /** + * {@code true} when the payload uses the DCQL selection shape (non-empty objects). + */ + public boolean isDcql() { + return dcqlSelections != null && !dcqlSelections.isEmpty(); + } + + public boolean isEmpty() { + if (dcqlSelections != null) { + return dcqlSelections.isEmpty(); + } + return credentialIds == null || credentialIds.isEmpty(); + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/ServiceConfiguration.java b/src/main/java/io/mosip/mimoto/dto/ServiceConfiguration.java new file mode 100644 index 000000000..84378075a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/ServiceConfiguration.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.dto; + +import lombok.Data; + +@Data +public class ServiceConfiguration { + String authorizationEndpoint; + String tokenEndpoint; + String credentialEndpoint; + String credentialAudience; +} diff --git a/src/main/java/io/mosip/mimoto/dto/SubmitPresentationRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/SubmitPresentationRequestDTO.java new file mode 100644 index 000000000..a6640c1ae --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/SubmitPresentationRequestDTO.java @@ -0,0 +1,100 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.mosip.mimoto.dto.deserializer.SelectedCredentialsDeserializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.mosip.mimoto.util.SelectedSdClaimsUtil; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Request payload for submitting a presentation or rejecting a verifier") +public class SubmitPresentationRequestDTO { + + @Schema(description = "Draft-23: credential ID strings. DCQL: objects with queryId + selectedCredentialIds.") + @JsonDeserialize(using = SelectedCredentialsDeserializer.class) + private SelectedCredentials selectedCredentials; + + @Schema(description = "Selected SD-JWT claim paths per credential ID for selective disclosure (only for SD-JWT credentials)", + example = "{\"cred-123\": [\"name\", \"dob\"]}") + private Map> selectedSdClaims; + + @Schema(description = "Error code for rejecting the verifier (used when user denies the presentation request)", + example = "access_denied") + private String errorCode; + + @Schema(description = "Error message for rejecting the verifier (used when user denies the presentation request)", + example = "User denied authorization to share credentials") + private String errorMessage; + + /** + * Checks if this is a submission request (has selected credentials and NO error fields) + */ + public boolean isSubmissionRequest() { + boolean hasCredentials = selectedCredentials != null && !selectedCredentials.isEmpty(); + boolean hasErrorFields = (errorCode != null && !errorCode.trim().isEmpty()) + || (errorMessage != null && !errorMessage.trim().isEmpty()); + return hasCredentials && !hasErrorFields; + } + + /** + * Returns {@code true} when {@code selectedCredentials} uses the DCQL format + * (objects with {@code queryId} and {@code selectedCredentialIds} per query). + * Used to branch submission logic between DCQL and Draft-23 presentation flows. + */ + public boolean isDcqlSubmission() { + return selectedCredentials != null && selectedCredentials.isDcql(); + } + + /** + * Returns the flat list of credential IDs for Draft-23 submissions + * ({@code ["cred-id-1", "cred-id-2"]}). Returns {@code null} for DCQL submissions. + */ + public List getSelectedCredentialIds() { + return selectedCredentials == null ? null : selectedCredentials.getCredentialIds(); + } + + /** + * Returns per-query credential selections for DCQL submissions + * ({@code [{queryId, selectedCredentialIds, ...}]}). Returns {@code null} for Draft-23 submissions. + */ + public List getDcqlSelections() { + return selectedCredentials == null ? null : selectedCredentials.getDcqlSelections(); + } + + /** + * Unions top-level {@code selectedSdClaims} with any nested inside DCQL selection objects, + * combining disclosure paths per credential ID. + */ + public Map> resolveEffectiveSelectedSdClaims() { + Map> merged = new LinkedHashMap<>(); + SelectedSdClaimsUtil.mergeInto(merged, selectedSdClaims); + if (isDcqlSubmission()) { + for (DcqlCredentialSelection selection : getDcqlSelections()) { + SelectedSdClaimsUtil.mergeInto(merged, selection.getSelectedSdClaims()); + } + } + return merged.isEmpty() ? null : merged; + } + + /** + * Checks if this is a rejection request (has error code and message, NO credentials, NO SD-claim selections) + */ + public boolean isRejectionRequest() { + boolean hasErrorFields = errorCode != null && !errorCode.trim().isEmpty() + && errorMessage != null && !errorMessage.trim().isEmpty(); + boolean hasCredentials = selectedCredentials != null && !selectedCredentials.isEmpty(); + boolean hasSdClaims = resolveEffectiveSelectedSdClaims() != null; + return hasErrorFields && !hasCredentials && !hasSdClaims; + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/SubmitPresentationResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/SubmitPresentationResponseDTO.java new file mode 100644 index 000000000..09e580ce6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/SubmitPresentationResponseDTO.java @@ -0,0 +1,26 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Response payload for successful response submission") +public class SubmitPresentationResponseDTO { + @Schema(description = "Status of the presentation submission", + example = "SUCCESS") + private String status; + @Schema(description = "Redirection URI to which the user-agent should be redirected after submission", + example = "https://verifier.example.com/callback?state=af0ifjsldkj") + private String redirectUri; + @Schema(description = "Message indicating the result of the submission", + example = "Presentation successfully submitted and shared with verifier") + private String message; +} diff --git a/src/main/java/io/mosip/mimoto/dto/TrustedVerifierRequest.java b/src/main/java/io/mosip/mimoto/dto/TrustedVerifierRequest.java new file mode 100644 index 000000000..0938c4144 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/TrustedVerifierRequest.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class TrustedVerifierRequest { + @NotBlank(message = "verifierId is required") + @Size(max = 255, message = "verifierId must not exceed 255 characters") + private String verifierId; +} diff --git a/src/main/java/io/mosip/mimoto/dto/TrustedVerifierResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/TrustedVerifierResponseDTO.java new file mode 100644 index 000000000..6e8b0d200 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/TrustedVerifierResponseDTO.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.dto; + +import lombok.Data; + +@Data +public class TrustedVerifierResponseDTO { + private String id; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/UnlockWalletRequestDto.java b/src/main/java/io/mosip/mimoto/dto/UnlockWalletRequestDto.java new file mode 100644 index 000000000..b8f91ec95 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/UnlockWalletRequestDto.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Data +@ToString +@ApiModel(description = "Model representing a unlock user wallet request") +@Schema(description = "Unlock Wallet Request DTO", requiredProperties = {"walletPin"}) +public class UnlockWalletRequestDto { + @Schema(description = "PIN used to unlock the Wallet") + String walletPin; + + @Schema(description = "Name of the Wallet") + String walletName; +} diff --git a/src/main/java/io/mosip/mimoto/dto/VPAuthorizationRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/VPAuthorizationRequestDTO.java new file mode 100644 index 000000000..66cad3cea --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/VPAuthorizationRequestDTO.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.dto; + +import lombok.Data; + +@Data +public class VPAuthorizationRequestDTO { + private String authorizationRequestUrl; +} diff --git a/src/main/java/io/mosip/mimoto/dto/VPResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/VPResponseDTO.java new file mode 100644 index 000000000..3dee24a8e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/VPResponseDTO.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VPResponseDTO { + + @Schema(description = "Unique identifier for the Verifiable Presentation") + private String presentationId; + + @JsonProperty("verifier") + @Schema(description = "Information about the Verifier who sent the Verifiable Presentation request") + private VerifiablePresentationVerifierDTO verifiablePresentationVerifierDTO; + + /** Whether the auth request used DCQL; session-only, not part of the API JSON. */ + @JsonIgnore + private boolean dcql; +} diff --git a/src/main/java/io/mosip/mimoto/dto/VerifiableCredentialRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/VerifiableCredentialRequestDTO.java new file mode 100644 index 000000000..ce79af54a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/VerifiableCredentialRequestDTO.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class VerifiableCredentialRequestDTO { + @Schema(description = "The unique identifier of the issuer") + @NotBlank(message = "issuerId cannot be blank") + String issuer; + + @Schema(description = "The unique identifier of the credential type from the issuer well-known configuration") + @NotBlank(message = "credentialConfigurationId cannot be blank") + String credentialConfigurationId; + + @Schema(description = "The authorization code received from the authorization server") + @NotBlank(message = "code cannot be blank") + String code; + + @Schema(description = "The grant type for the authorization request") + @NotBlank(message = "grantType cannot be blank") + String grantType; + + @Schema(description = "The redirect URI for the authorization request") + @NotBlank(message = "redirectUri cannot be blank") + String redirectUri; + + @Schema(description = "The code verifier used for PKCE (Proof Key for Code Exchange)") + @NotBlank(message = "codeVerifier cannot be blank") + String codeVerifier; +} diff --git a/src/main/java/io/mosip/mimoto/dto/VerifiablePresentationVerifierDTO.java b/src/main/java/io/mosip/mimoto/dto/VerifiablePresentationVerifierDTO.java new file mode 100644 index 000000000..e60afcdde --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/VerifiablePresentationVerifierDTO.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Data +@AllArgsConstructor +public class VerifiablePresentationVerifierDTO { + + @Schema(description = "Unique identifier (client_id) of the Verifier") + private String id; + + @Schema(description = "Name of the Verifier") + private String name; + + @URL + @Schema(description = "Logo URL of the Verifier") + private String logo; + + @Schema(description = "Indicates if the Verifier is trusted by the User through the wallet") + private boolean isTrusted; + + @Schema(description = "Indicates if the Verifier is pre-registered with the wallet") + private boolean isPreregisteredWithWallet; + + @URL + @Schema(description = "Redirect URI used to redirect the User back to the Verifier after the presentation is completed") + private String redirectUri; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/VidResDTO.java b/src/main/java/io/mosip/mimoto/dto/VidResDTO.java index 0dc7f08fa..468101b08 100644 --- a/src/main/java/io/mosip/mimoto/dto/VidResDTO.java +++ b/src/main/java/io/mosip/mimoto/dto/VidResDTO.java @@ -4,6 +4,8 @@ import lombok.Data; +import java.io.Serializable; + /** * This class will provide values to hold uin,vid and vidStatus * @@ -11,7 +13,7 @@ * */ @Data -public class VidResDTO { +public class VidResDTO implements Serializable { /** The Value Of UIN in Decrypted value */ @JsonProperty("UIN") diff --git a/src/main/java/io/mosip/mimoto/dto/WalletDetailsResponseDto.java b/src/main/java/io/mosip/mimoto/dto/WalletDetailsResponseDto.java new file mode 100644 index 000000000..0853ab8c7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/WalletDetailsResponseDto.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto; + +import io.mosip.mimoto.model.WalletLockStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + + +@Data +@AllArgsConstructor +@Builder +public class WalletDetailsResponseDto { + @Schema(description = "Unique identifier of the Wallet") + String walletId; + @Schema(description = "Wallet name provided by user") + String walletName; + @Schema(description = "Wallet status indicating if it is locked or unlocked") + WalletLockStatus walletStatus; +} diff --git a/src/main/java/io/mosip/mimoto/dto/WalletResponseDto.java b/src/main/java/io/mosip/mimoto/dto/WalletResponseDto.java new file mode 100644 index 000000000..90a860337 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/WalletResponseDto.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder +public class WalletResponseDto { + @Schema(description = "Unique identifier of the Wallet") + String walletId; + + @Schema(description = "Wallet name provided by user") + String walletName; + + @JsonIgnore + @Schema(hidden = true) + String decryptedWalletKey; +} diff --git a/src/main/java/io/mosip/mimoto/dto/deserializer/SelectedCredentialsDeserializer.java b/src/main/java/io/mosip/mimoto/dto/deserializer/SelectedCredentialsDeserializer.java new file mode 100644 index 000000000..e99b61acf --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/deserializer/SelectedCredentialsDeserializer.java @@ -0,0 +1,58 @@ +package io.mosip.mimoto.dto.deserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.DcqlCredentialSelection; +import io.mosip.mimoto.dto.SelectedCredentials; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SelectedCredentialsDeserializer extends JsonDeserializer { + + private static final String INVALID_SELECTED_CREDENTIALS_FORMAT_ERROR_MESSAGE = + "selectedCredentials must be either all credential ID strings or all DCQL selection objects"; + + @Override + public SelectedCredentials deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { + JsonNode node = parser.getCodec().readTree(parser); + if (!node.isArray()) { + throw ctxt.weirdStringException(node.toString(), SelectedCredentials.class, + "selectedCredentials must be a JSON array"); + } + if (node.isEmpty()) { + return SelectedCredentials.ofStrings(List.of()); + } + + JsonNode first = node.get(0); + if (first.isTextual()) { + List ids = new ArrayList<>(); + for (JsonNode item : node) { + if (!item.isTextual()) { + throw JsonMappingException.from(parser, INVALID_SELECTED_CREDENTIALS_FORMAT_ERROR_MESSAGE); + } + ids.add(item.asText()); + } + return SelectedCredentials.ofStrings(ids); + } + + if (first.isObject()) { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + List selections = new ArrayList<>(); + for (JsonNode item : node) { + if (!item.isObject()) { + throw JsonMappingException.from(parser, INVALID_SELECTED_CREDENTIALS_FORMAT_ERROR_MESSAGE); + } + selections.add(mapper.treeToValue(item, DcqlCredentialSelection.class)); + } + return SelectedCredentials.ofDcql(selections); + } + + throw JsonMappingException.from(parser, INVALID_SELECTED_CREDENTIALS_FORMAT_ERROR_MESSAGE); + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/idp/TokenRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/idp/TokenRequestDTO.java new file mode 100644 index 000000000..3ecaf1661 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/idp/TokenRequestDTO.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.idp; + +import lombok.Data; + +@Data +public class TokenRequestDTO { + private String grant_type; + private String code; + private String client_id; + private String client_secret; + private String redirect_uri; + private String client_assertion_type; + private String client_assertion; +} diff --git a/src/main/java/io/mosip/mimoto/dto/idp/TokenResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/idp/TokenResponseDTO.java new file mode 100644 index 000000000..a63b696c0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/idp/TokenResponseDTO.java @@ -0,0 +1,30 @@ +package io.mosip.mimoto.dto.idp; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +@Getter +@Setter +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class TokenResponseDTO { + @JsonInclude(JsonInclude.Include.NON_NULL) + private String id_token; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String token_type; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String access_token; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private int expires_in; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String scope; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String c_nonce; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AidStatusRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AidStatusRequestDTO.java new file mode 100644 index 000000000..f60723a66 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AidStatusRequestDTO.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class AidStatusRequestDTO { + + private String aid; + private String otp; + private String transactionID; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AppOTPRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AppOTPRequestDTO.java index 74c59fc4b..e86f7c5ad 100644 --- a/src/main/java/io/mosip/mimoto/dto/mimoto/AppOTPRequestDTO.java +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AppOTPRequestDTO.java @@ -3,10 +3,19 @@ import java.util.List; import lombok.Data; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + @Data public class AppOTPRequestDTO { + @NotNull private String individualId; + @Pattern(regexp = "UIN|VID", message = "Only UIN or VID is allowed") private String individualIdType; + @NotEmpty + @NotNull private List otpChannel; + @NotNull private String transactionID; -} \ No newline at end of file +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AttestationStatement.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AttestationStatement.java index e5449d20d..2771318e0 100644 --- a/src/main/java/io/mosip/mimoto/dto/mimoto/AttestationStatement.java +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AttestationStatement.java @@ -18,13 +18,14 @@ package io.mosip.mimoto.dto.mimoto; import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.json.webtoken.JsonWebToken.Payload; import com.google.api.client.util.Base64; import com.google.api.client.util.Key; /** * A statement returned by the Attestation API. */ -public class AttestationStatement extends JsonWebSignature.Payload { +public class AttestationStatement extends Payload { /** * Embedded nonce sent as part of the request. */ diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AuthAndConsentRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthAndConsentRequestDto.java new file mode 100644 index 000000000..567775407 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthAndConsentRequestDto.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; + +@Data +public class AuthAndConsentRequestDto { + private String linkedTransactionId; + private String individualId; + private List acceptedClaims; + private List permittedAuthorizeScopes; + private List challengeList; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AuthFactorDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthFactorDto.java new file mode 100644 index 000000000..7460f2f40 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthFactorDto.java @@ -0,0 +1,17 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthFactorDto { + + private String type; + private Integer count; + private List subTypes; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/AuthorizationServerWellKnownResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthorizationServerWellKnownResponse.java new file mode 100644 index 000000000..4680488e1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/AuthorizationServerWellKnownResponse.java @@ -0,0 +1,38 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import java.util.List; + + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthorizationServerWellKnownResponse { + + @NotBlank + @URL + @SerializedName("authorization_endpoint") + @JsonProperty("authorization_endpoint") + @Schema(description = "Endpoint for Authenticating & Authorizing the user") + private String authorizationEndpoint; + + + @NotEmpty + @SerializedName("grant_types_supported") + @JsonProperty("grant_types_supported") + @Schema(description = "List of grant types_supported by Authorization Server") + private List<@NotBlank String> grantTypesSupported; + + @NotBlank + @URL + @SerializedName("token_endpoint") + @JsonProperty(value = "token_endpoint") + @Schema(description = "Endpoint for getting the token from the authorization server") + private String tokenEndpoint; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpInnerReqDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpInnerReqDto.java new file mode 100644 index 000000000..f0f1981f6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpInnerReqDto.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +@Data +public class BindingOtpInnerReqDto { + + @NotNull + @Schema(description = "Individual ID For Binding OTP") + private String individualId; + @NotNull + @NotEmpty + @Schema(description = "Notifying medium in which OTP is sent", allowableValues = {"PHONE", "EMAIL"}) + private List otpChannels; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpRequestDto.java new file mode 100644 index 000000000..c1a3bad19 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpRequestDto.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BindingOtpRequestDto { + @Schema(description = "Request Time of the Binding OTP") + private String requestTime; + + @Valid + @NotNull + @Schema(description = "Binding OTP Request") + private BindingOtpInnerReqDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpResponseDto.java new file mode 100644 index 000000000..13c30dd49 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/BindingOtpResponseDto.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class BindingOtpResponseDto { + @Schema(description = "Transaction Id of Binding OTP") + private String transactionId; + @Schema(description = "Masked Email of Binding OTP") + private String maskedEmail; + @Schema(description = "Masked Mobile Number of Binding OTP") + private String maskedMobile; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java new file mode 100644 index 000000000..abb63e359 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class CredentialDefinitionResponseDto { + + @NotEmpty + @SerializedName("type") + @JsonProperty("type") + @Schema(description = "Type of the Credential Definition") + private List<@NotEmpty String> type; + + @Valid + @SerializedName("credentialSubject") + @JsonProperty("credentialSubject") + @Schema(description = "Credential Subject of the VC") + private Map<@NotEmpty String, @Valid CredentialDisplayResponseDto> credentialSubject; + + @SerializedName("@context") + @JsonProperty("@context") + @Schema(description = "context of the Credential Definition") + private List<@NotEmpty String> context; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponse.java new file mode 100644 index 000000000..0915e889c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponse.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CredentialDisplayResponse { + + @Schema(description = "Name of the Supported Credential") + private String name; + + @Schema(description = "Locale of the Supported Credential") + private String locale; + + @Schema(description = "Logo of the Supported Credential") + private String logo; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java new file mode 100644 index 000000000..be330f46c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java @@ -0,0 +1,21 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Data +public class CredentialDisplayResponseDto { + + @NotEmpty + @Valid + @SerializedName("display") + @JsonProperty("display") + @Schema(description = "Display Properties of the Credential Issuer") + private List<@Valid CredentialIssuerDisplayResponse> display; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDownloadRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDownloadRequestDTO.java index 21d3e511c..942638cac 100644 --- a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDownloadRequestDTO.java +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDownloadRequestDTO.java @@ -2,8 +2,12 @@ import lombok.Data; +import jakarta.validation.constraints.NotNull; + @Data public class CredentialDownloadRequestDTO { + @NotNull private String individualId; + @NotNull private String requestId; } diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfiguration.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfiguration.java new file mode 100644 index 000000000..8d0473afe --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfiguration.java @@ -0,0 +1,23 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +public class CredentialIssuerConfiguration { + private String credentialIssuer; + + private List authorizationServers; + + private String credentialEndPoint; + + private Map credentialConfigurationsSupported; + + @JsonIgnoreProperties({"token_endpoint"}) + private AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfigurationResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfigurationResponseDTO.java new file mode 100644 index 000000000..bd348108f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerConfigurationResponseDTO.java @@ -0,0 +1,26 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class CredentialIssuerConfigurationResponseDTO { + + @JsonProperty("credentials_supported") + @Schema(description = "List of Credential types Supported and their configurations") + private List credentials; + + @JsonProperty("authorization_endpoint") + @Schema(description = "Endpoint for Authenticating & Authorizing the user") + private String authorizationEndpoint; + + @JsonProperty("grant_types_supported") + @Schema(description = "List of grant types supported by Authorization Server") + private List grantTypesSupported; + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java new file mode 100644 index 000000000..0a8a13b41 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java @@ -0,0 +1,21 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class CredentialIssuerDisplayResponse { + + @SerializedName("name") + @JsonProperty("name") + @Schema(description = "Name of the Credential Issuer") + private String name; + + @SerializedName("locale") + @JsonProperty("locale") + @Schema(description = "Locale of the Credential Issuer") + private String locale; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java new file mode 100644 index 000000000..06506db94 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java @@ -0,0 +1,40 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CredentialIssuerWellKnownResponse { + @JsonProperty("credential_issuer") + private String credentialIssuer; + + @JsonProperty("authorization_servers") + private List authorizationServers; + + @JsonProperty("credential_endpoint") + private String credentialEndPoint; + + @JsonProperty("credential_configurations_supported") + private Map credentialConfigurationsSupported; + + @JsonProperty("nonce_endpoint") + private String nonceEndpoint; + + @JsonProperty("version") + private VCSpecificationVersion version; + + public CredentialIssuerWellKnownResponse(String credentialIssuer, List authorizationServers, String credentialEndPoint, Map credentialConfigurationsSupported) { + this.credentialIssuer = credentialIssuer; + this.authorizationServers = authorizationServers; + this.credentialEndPoint = credentialEndPoint; + this.credentialConfigurationsSupported = credentialConfigurationsSupported; + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java new file mode 100644 index 000000000..6c3ea9a74 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java @@ -0,0 +1,55 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.mosip.mimoto.dto.BackgroundImageDTO; +import io.mosip.mimoto.dto.LogoDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import java.util.Map; + +@Data +public class CredentialSupportedDisplayResponse { + + @Expose + @NotBlank + @Schema(description = "Name of the Supported Credential") + String name; + + @Expose + @NotBlank + @Schema(description = "Locale of the Supported Credential") + String locale; + + @Expose + @NotNull + @Valid + @Schema(description = "Logo of the Supported Credential") + LogoDTO logo; + + @JsonProperty("background_image") + @SerializedName("background_image") + @Expose + @Valid + @Schema(description = "Background Image of the Supported Credential") + BackgroundImageDTO backgroundImage; + + @JsonProperty("background_color") + @SerializedName("background_color") + @Expose + @NotBlank + @Schema(description = "Background Colour of the Supported Credential") + String backgroundColor; + + @JsonProperty("text_color") + @SerializedName("text_color") + @Expose + @NotBlank + @Schema(description = "Text Colour of the Supported Credential") + String textColor; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsResponse.java new file mode 100644 index 000000000..5158d08dd --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsResponse.java @@ -0,0 +1,23 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class CredentialsResponse { + + @Schema(description = "name of the Credential") + private String name; + + @Schema(description = "Scope of the Credential") + private String scope; + + @JsonProperty("display") + @Schema(description = "Display Properties of the Supported Credential") + private List display; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java new file mode 100644 index 000000000..49a434560 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java @@ -0,0 +1,61 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@Data +public class CredentialsSupportedResponse { + + @NotBlank(message = "Format must not be blank") + @Schema(description = "Format of the Credential") + private String format; + + @NotBlank(message = "Scope must not be blank") + @Schema(description = "Scope of the Credential") + private String scope; + + @JsonInclude(NON_NULL) + @Schema(description = "document Type of the Credential") + private String doctype; + + @NotEmpty(message = "Proof types supported must not be empty") + @Valid + @SerializedName("proof_types_supported") + @JsonProperty("proof_types_supported") + @Schema(description = "List of proof types supported") + private Map<@NotEmpty String, @Valid ProofTypesSupported> proofTypesSupported; + + @JsonInclude(NON_NULL) + @Schema(description = "List of Claims") + private Map claims; + + @SerializedName("credential_definition") + @JsonProperty("credential_definition") + @JsonInclude(NON_NULL) + @Schema(description = "Credential Definition of the VC") + private CredentialDefinitionResponseDto credentialDefinition; + + @NotEmpty(message = "Display information must not be empty") + @Valid + @SerializedName("display") + @JsonProperty("display") + @Schema(description = "Display Properties of the Supported Credential") + private List<@Valid CredentialSupportedDisplayResponse> display; + + @Schema(description = "Order of the Credentials Subject Render") + private List order; + + @Schema(description = "Identify the type or schema of the claims included") + private String vct; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/Draft13VCCredentialRequest.java b/src/main/java/io/mosip/mimoto/dto/mimoto/Draft13VCCredentialRequest.java new file mode 100644 index 000000000..4fb8f483b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/Draft13VCCredentialRequest.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Draft13VCCredentialRequest { + + @NotBlank + private String format; + + @Valid + @NotNull + private VCCredentialRequestProof proof; + + @JsonProperty("credential_definition") + @Valid + @NotNull + private VCCredentialDefinition credentialDefinition; + + private String vct; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthAndConsentDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthAndConsentDto.java new file mode 100644 index 000000000..d51d79faa --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthAndConsentDto.java @@ -0,0 +1,9 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class IdpAuthAndConsentDto { + private String requestTime; + private AuthAndConsentRequestDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthChallangeDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthChallangeDto.java new file mode 100644 index 000000000..16c720618 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthChallangeDto.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class IdpAuthChallangeDto { + private String authFactorType; + private String challenge; + private String format; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthInternalRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthInternalRequestDto.java new file mode 100644 index 000000000..fb142ba9c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthInternalRequestDto.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdpAuthInternalRequestDto { + private String linkedTransactionId; + private String individualId; + private List challengeList; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthRequestDto.java new file mode 100644 index 000000000..1b66ad8cc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthRequestDto.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdpAuthRequestDto { + private String requestTime; + private IdpAuthInternalRequestDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthenticateRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthenticateRequestDto.java new file mode 100644 index 000000000..cf811a9d0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpAuthenticateRequestDto.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; + +@Data +public class IdpAuthenticateRequestDto { + private String requesttime; + private String linkTransactionId; + private String individualId; + private List challengeList; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpChallangeDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpChallangeDto.java new file mode 100644 index 000000000..31b38274d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpChallangeDto.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class IdpChallangeDto { + @Schema(description = "Auth Factory type", allowableValues = {"OTP"}) + private String authFactorType; + @Schema(description = "IDP Challenge") + private String challenge; + @Schema(description = "IDP Format", allowableValues = {"jwt"}) + private String format; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentDto.java new file mode 100644 index 000000000..95186e6ac --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentDto.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdpConsentDto { + private String requestTime; + private IdpConsentRequestDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentRequestDto.java new file mode 100644 index 000000000..1015066d6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpConsentRequestDto.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdpConsentRequestDto { + private String linkedTransactionId; + private List acceptedClaims; + private List permittedAuthorizeScopes; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeDto.java new file mode 100644 index 000000000..d5c9385db --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeDto.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IdpLinkCodeDto { + private String linkCode; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeResponseDto.java new file mode 100644 index 000000000..1b140a8fd --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkCodeResponseDto.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class IdpLinkCodeResponseDto { + private String transactionId; + private String linkCode; + private String expireDateTime; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkTransactionReqDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkTransactionReqDto.java new file mode 100644 index 000000000..07b28da6a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpLinkTransactionReqDto.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IdpLinkTransactionReqDto { + + private String requestTime; + private IdpLinkCodeDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReq.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReq.java new file mode 100644 index 000000000..32659c0c6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReq.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class IdpOtpReq { + + private String requestTime; + private IdpOtpReqDto request; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReqDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReqDto.java new file mode 100644 index 000000000..d56fc4099 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IdpOtpReqDto.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IdpOtpReqDto { + private String transactionId; + private String individualId; + private List otpChannels; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IndividualIdOtpRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IndividualIdOtpRequestDTO.java new file mode 100644 index 000000000..5c0edd734 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IndividualIdOtpRequestDTO.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; + +@Data +public class IndividualIdOtpRequestDTO { + private String aid; + private List otpChannel; + private String transactionID; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/InternalAidStatusDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/InternalAidStatusDTO.java new file mode 100644 index 000000000..884847da2 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/InternalAidStatusDTO.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class InternalAidStatusDTO { + + private String individualId; + private String otp; + private String transactionId; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerConfig.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerConfig.java new file mode 100644 index 000000000..0073a8ad4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerConfig.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.mosip.mimoto.dto.IssuerDTO; +import lombok.Data; + +@Data +public class IssuerConfig { + private final IssuerDTO issuerDTO; + private final CredentialIssuerWellKnownResponse wellKnownResponse; + private final CredentialsSupportedResponse credentialsSupportedResponse; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/JwkDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/JwkDto.java new file mode 100644 index 000000000..dbc8fbf2a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/JwkDto.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class JwkDto { + private String kty; + private String e; + private String n; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/LinkCodeResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkCodeResponse.java new file mode 100644 index 000000000..8e81bc0a6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkCodeResponse.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class LinkCodeResponse { + private String linkTransactionId; + private String clientName; + private String logoUrl; + private List> authFactors; + private List authorizeScopes; + private List essentialClaims; + private List voluntaryClaims; + private Map configs; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionRequestDto.java new file mode 100644 index 000000000..938c42729 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionRequestDto.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; + +@Data +public class LinkTransactionRequestDto { + private String requestTime; + private String linkCode; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionResponseDto.java new file mode 100644 index 000000000..9067be23f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkTransactionResponseDto.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.mosip.mimoto.dto.ErrorDTO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LinkTransactionResponseDto { + + private String responseTime; + + private LinkCodeResponse response; + + private List errors; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/LinkedTransactionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkedTransactionResponseDto.java new file mode 100644 index 000000000..52b72327e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/LinkedTransactionResponseDto.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class LinkedTransactionResponseDto { + private String linkedTransactionId; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/NonceResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/NonceResponse.java new file mode 100644 index 000000000..dd7ff7163 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/NonceResponse.java @@ -0,0 +1,15 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NonceResponse { + + @JsonProperty("c_nonce") + private String cNonce; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/ProofTypesSupported.java b/src/main/java/io/mosip/mimoto/dto/mimoto/ProofTypesSupported.java new file mode 100644 index 000000000..20857e194 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/ProofTypesSupported.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Data +public class ProofTypesSupported { + + @NotEmpty + @JsonProperty("proof_signing_alg_values_supported") + @SerializedName("proof_signing_alg_values_supported") + @Schema(description = "Support Alg for proof Signing") + private List proofSigningAlgValuesSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/UserMetadataDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/UserMetadataDTO.java new file mode 100644 index 000000000..1d48cbd3f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/UserMetadataDTO.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +public class UserMetadataDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "Display name of the user provided by the Identity Provider") + private String displayName; + + @Schema(description = "Profile picture of the user provided by the Identity Provider") + private String profilePictureUrl; + + @Schema(description = "Email of the user provided by the Identity Provider") + private String email; + + @Schema(description = "Wallet id of the user in use") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String walletId; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/V1Credential.java b/src/main/java/io/mosip/mimoto/dto/mimoto/V1Credential.java new file mode 100644 index 000000000..f108c9ef5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/V1Credential.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class V1Credential { + private Object credential; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialRequest.java b/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialRequest.java new file mode 100644 index 000000000..00e96c7a9 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialRequest.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class V1VCCredentialRequest { + + @JsonProperty("credential_configuration_id") + @NotBlank + private String credentialConfigurationId; + + @JsonProperty("proofs") + @NotNull + private Map> proofs; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialResponse.java new file mode 100644 index 000000000..a820ecb5d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/V1VCCredentialResponse.java @@ -0,0 +1,27 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class V1VCCredentialResponse { + + private List credentials; + + private String error; + + @JsonProperty("error_description") + private String errorDescription; + + public boolean hasError() { + return error != null; + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java new file mode 100644 index 000000000..747e4ad4d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java @@ -0,0 +1,24 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class VCCredentialDefinition { + + @JsonProperty("@context") + private List<@NotBlank String> context; + + @NotEmpty + private List<@NotBlank String> type; + + private Map credentialSubject; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java new file mode 100644 index 000000000..23ffad922 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class VCCredentialIssueBody { + private VCCredentialProperties credential; + private String credentialSchemaId; + private String createdAt; + private String createdBy; + private String updatedAt; + private String updatedBy; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java new file mode 100644 index 000000000..83df8db7a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java @@ -0,0 +1,55 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +@Data +@JsonPropertyOrder({"@context", "credentialSubject", "validUntil", "id", "validFrom", "issuer", "proof", "type", "issuanceDate", "expirationDate"}) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VCCredentialProperties implements Serializable { + private String issuer; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String id; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String validFrom; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String issuanceDate; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String validUntil; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String expirationDate; + + private VCCredentialResponseProof proof; + + private Map credentialSubject; + + @JsonProperty("@context") + private Object context; + + @NotEmpty + private List<@NotBlank String> type; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Map credentialStatus; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Object renderMethod; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java new file mode 100644 index 000000000..d665d0c5e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +@Data +@Builder +public class VCCredentialRequestProof { + + @JsonProperty("proof_type") + @NotBlank + private String proofType; + + private String jwt; + + private String cwt; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java new file mode 100644 index 000000000..469c92348 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VCCredentialResponse implements Serializable { + + @NotBlank + private String format; + + @Valid + @NotNull + private Object credential; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java new file mode 100644 index 000000000..f6ae44850 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java @@ -0,0 +1,33 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VCCredentialResponseProof implements Serializable { + @NotBlank + private String type; + @NotBlank + private String created; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String proofPurpose; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String verificationMethod; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String jws; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String proofValue; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponse.java new file mode 100644 index 000000000..b4ea57a86 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponse.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.mimoto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VerifiableCredentialResponse { + + @Valid + @NotNull + private Object credential; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponseDTO.java new file mode 100644 index 000000000..5cb4b1965 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VerifiableCredentialResponseDTO.java @@ -0,0 +1,68 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.mosip.mimoto.dto.DisplayDTO; +import io.mosip.mimoto.dto.IssuerDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static io.mosip.mimoto.util.LocaleUtils.getCredentialDisplayDTOBasedOnLocale; +import static io.mosip.mimoto.util.LocaleUtils.getIssuerDisplayDTOBasedOnLocale; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor + +// Holds details related to credential UUID as per stored in DB and other metadata to display in wallet +public class VerifiableCredentialResponseDTO { + @Schema(description = "Name of the issuer") + private String issuerDisplayName; + + @Schema(description = "logo of the issuer") + private String issuerLogo; + + @Schema(description = "Name of the credential type") + private String credentialTypeDisplayName; + + @Schema(description = "logo of the credential type") + private String credentialTypeLogo; + + @Schema(description = "Unique Identifier of the Credential in database") + private String credentialId; + + public static VerifiableCredentialResponseDTO fromIssuerConfig( + IssuerConfig issuerConfig, String locale, String credentialId) { + String issuerName = ""; + String issuerLogo = ""; + String credentialType = ""; + String credentialTypeLogo = ""; + DisplayDTO issuerDisplayDTO; + CredentialSupportedDisplayResponse credentialTypeDisplayDTO; + if (null != issuerConfig) { + IssuerDTO issuerDTO = issuerConfig.getIssuerDTO(); + CredentialsSupportedResponse credentialsSupportedResponse = issuerConfig.getCredentialsSupportedResponse(); + if (issuerDTO != null) { + issuerDisplayDTO = getIssuerDisplayDTOBasedOnLocale(issuerDTO.getDisplay(), locale); + issuerName = issuerDisplayDTO.getName(); + issuerLogo = issuerDisplayDTO.getLogo().getUrl(); + } + if (credentialsSupportedResponse != null) { + credentialTypeDisplayDTO = getCredentialDisplayDTOBasedOnLocale( + credentialsSupportedResponse.getDisplay(), locale); + credentialType = credentialTypeDisplayDTO.getName(); + credentialTypeLogo = credentialTypeDisplayDTO.getLogo().getUrl(); + } + } + + return VerifiableCredentialResponseDTO.builder() + .issuerDisplayName(issuerName) + .issuerLogo(issuerLogo) + .credentialTypeDisplayName(credentialType) + .credentialTypeLogo(credentialTypeLogo) + .credentialId(credentialId) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingDto.java new file mode 100644 index 000000000..da20b3c76 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingDto.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class WalletBindingDto { + private String individualId; + private String otp; + private String transactionID; + private String publicKey; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerReq.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerReq.java new file mode 100644 index 000000000..e43998cbd --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerReq.java @@ -0,0 +1,21 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class WalletBindingInnerReq { + + @Schema(description = "Individual Id ") + private String individualId; + @Schema(description = "List of Challenges") + private List challengeList; + @Schema(description = "public key in JWK") + private String publicKey; + @Schema(description = "Auth Factory type", allowableValues = {"WLA"}) + private String authFactorType; + @Schema(description = "IDP format", allowableValues = {"jwt"}) + private String format; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerRequestDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerRequestDto.java new file mode 100644 index 000000000..a0bd3397c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInnerRequestDto.java @@ -0,0 +1,21 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.nimbusds.jose.jwk.JWK; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +public class WalletBindingInnerRequestDto { + @Schema(description = "Individual Id of the Wallet Binding") + private String individualId; + @Schema(description = "Challenge List of the Wallet Binding") + private List challengeList; + @Schema(description = "Public Key of the Wallet Binding in JWK") + private JwkDto publicKey; + @Schema(description = "Auth Factor Type of the Wallet Binding") + private String authFactorType; + @Schema(description = "Format of the Wallet Binding") + private String format; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalRequestDTO.java new file mode 100644 index 000000000..3abcebfe8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalRequestDTO.java @@ -0,0 +1,17 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class WalletBindingInternalRequestDTO { + @Schema(description = "Request Time of the Wallet Binding") + private String requestTime; + @Schema(description = "Body of the Request") + private WalletBindingInnerRequestDto request; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalResponseDto.java new file mode 100644 index 000000000..e008fb0e1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingInternalResponseDto.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class WalletBindingInternalResponseDto { + @Schema(description = "Certificate of the Wallet Binding") + private String certificate; + @Schema(description = "Wallet User Id of the Wallet Binding") + private String walletUserId; + @Schema(description = "Date Time Expiry of the Wallet Binding") + private String expireDateTime; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingRequestDTO.java new file mode 100644 index 000000000..7473aa0d7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingRequestDTO.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class WalletBindingRequestDTO { + + @Schema(description = "Request time of the wallet binding") + private String requestTime; + @Schema(description = "Request of the wallet binding") + private WalletBindingInnerReq request; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingResponseDto.java new file mode 100644 index 000000000..b1752ea6b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/WalletBindingResponseDto.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.mimoto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class WalletBindingResponseDto { + @Schema(description = "Certificate ID For Wallet Binding ") + private String certificate; + @Schema(description = "Encrypted Wallet Binding ID Received in the Binding OTP") + private String encryptedWalletBindingId; + @Schema(description = "Date Time of the Expiry") + private String expireDateTime; + @Schema(description = "Thumbprint Received in the Binding OTP") + private String thumbprint; + @Schema(description = "Key Id for Wallet Binding") + private String keyId; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/BackgroundImageDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/BackgroundImageDTO.java new file mode 100644 index 000000000..9e50e42e6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/BackgroundImageDTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.google.gson.annotations.Expose; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BackgroundImageDTO { + @Expose + @URL + @NotBlank + String uri; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialDefinitionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialDefinitionResponseDto.java new file mode 100644 index 000000000..06a0493be --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialDefinitionResponseDto.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class CredentialDefinitionResponseDto { + + @NotEmpty + @SerializedName("type") + @JsonProperty("type") + @Schema(description = "Type of the Credential Definition") + private List<@NotEmpty String> type; + + @Valid + @SerializedName("credentialSubject") + @JsonProperty("credentialSubject") + @Schema(description = "Credential Subject of the VC") + private Map credentialSubject; + + @SerializedName("@context") + @JsonProperty("@context") + @Schema(description = "context of the Credential Definition") + private List<@NotEmpty String> context; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialSupportedDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialSupportedDisplayResponse.java new file mode 100644 index 000000000..222e2d303 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialSupportedDisplayResponse.java @@ -0,0 +1,51 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class CredentialSupportedDisplayResponse { + + @Expose + @NotBlank + @Schema(description = "Name of the Supported Credential") + String name; + + @Expose + @NotBlank + @Schema(description = "Locale of the Supported Credential") + String locale; + + @Expose + @NotNull + @Valid + @Schema(description = "Logo of the Supported Credential") + LogoDTO logo; + + @JsonProperty("background_image") + @SerializedName("background_image") + @Expose + @Valid + @Schema(description = "Background Image of the Supported Credential") + BackgroundImageDTO backgroundImage; + + @JsonProperty("background_color") + @SerializedName("background_color") + @Expose + @NotBlank + @Schema(description = "Background Colour of the Supported Credential") + String backgroundColor; + + @JsonProperty("text_color") + @SerializedName("text_color") + @Expose + @NotBlank + @Schema(description = "Text Colour of the Supported Credential") + String textColor; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialsSupportedResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialsSupportedResponse.java new file mode 100644 index 000000000..0257ca21a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/CredentialsSupportedResponse.java @@ -0,0 +1,61 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@Data +public class CredentialsSupportedResponse { + + @NotBlank(message = "Format must not be blank") + @Schema(description = "Format of the Credential") + private String format; + + @NotBlank(message = "Scope must not be blank") + @Schema(description = "Scope of the Credential") + private String scope; + + @JsonInclude(NON_NULL) + @Schema(description = "document Type of the Credential") + private String doctype; + + @NotEmpty(message = "Proof types supported must not be empty") + @Valid + @SerializedName("proof_types_supported") + @JsonProperty("proof_types_supported") + @Schema(description = "List of proof types supported") + private Map<@NotEmpty String, @Valid ProofTypesSupported> proofTypesSupported; + + @JsonInclude(NON_NULL) + @Schema(description = "List of Claims") + private Map claims; + + @SerializedName("credential_definition") + @JsonProperty("credential_definition") + @JsonInclude(NON_NULL) + @Schema(description = "Credential Definition of the VC") + private CredentialDefinitionResponseDto credentialDefinition; + + @NotEmpty(message = "Display information must not be empty") + @Valid + @SerializedName("display") + @JsonProperty("display") + @Schema(description = "Display Properties of the Supported Credential") + private List<@Valid CredentialSupportedDisplayResponse> display; + + @Schema(description = "Order of the Credentials Subject Render") + private List order; + + @Schema(description = "Identify the type or schema of the claims included") + private String vct; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/Draft13WellKnownResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/Draft13WellKnownResponse.java new file mode 100644 index 000000000..351841508 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/Draft13WellKnownResponse.java @@ -0,0 +1,44 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import org.hibernate.validator.constraints.URL; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class Draft13WellKnownResponse { + @NotBlank + @URL + @SerializedName("credential_issuer") + @JsonProperty("credential_issuer") + @Schema(description = "Unique Identifier of the Credential Issuer") + private String credentialIssuer; + + @NotEmpty + @SerializedName("authorization_servers") + @JsonProperty("authorization_servers") + @Schema(description = "List of Authorization Server Endpoint") + private List<@NotBlank @URL String> authorizationServers; + + @NotBlank + @URL + @SerializedName("credential_endpoint") + @JsonProperty("credential_endpoint") + @Schema(description = "Endpoint to download the Credential") + private String credentialEndPoint; + + @NotEmpty + @Valid + @SerializedName("credential_configurations_supported") + @JsonProperty("credential_configurations_supported") + @Schema(description = "List of Credential Configurations Supported") + private Map<@NotBlank String, @Valid CredentialsSupportedResponse> credentialConfigurationsSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/LogoDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/LogoDTO.java new file mode 100644 index 000000000..4cd5a2f2d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/LogoDTO.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Data +public class LogoDTO { + @Expose + @NotBlank + @URL + @JsonAlias("uri") + @SerializedName(value = "url", alternate = {"uri"}) + @Schema(description = "Display logo uri of the Issuer") + String url; + + @Expose + @NotBlank + @Schema(description = "Display logo alt text of the Issuer") + String alt_text; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/ProofTypesSupported.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/ProofTypesSupported.java new file mode 100644 index 000000000..9ffb4b196 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/draft13/ProofTypesSupported.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.draft13; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Data +public class ProofTypesSupported { + + @NotEmpty + @JsonProperty("proof_signing_alg_values_supported") + @SerializedName("proof_signing_alg_values_supported") + @Schema(description = "Support Alg for proof Signing") + private List proofSigningAlgValuesSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/BackgroundImageDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/BackgroundImageDTO.java new file mode 100644 index 000000000..d6196c7de --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/BackgroundImageDTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.google.gson.annotations.Expose; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class BackgroundImageDTO { + @Expose + @URL + @NotBlank + String uri; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialDefinitionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialDefinitionResponseDto.java new file mode 100644 index 000000000..5e53e32fb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialDefinitionResponseDto.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class CredentialDefinitionResponseDto { + + @NotEmpty + @SerializedName("type") + @JsonProperty("type") + @Schema(description = "Type of the Credential Definition") + private List<@NotEmpty String> type; + + @Valid + @SerializedName("credentialSubject") + @JsonProperty("credentialSubject") + @Schema(description = "Credential Subject of the VC") + private Map credentialSubject; + + @SerializedName("@context") + @JsonProperty("@context") + @Schema(description = "context of the Credential Definition") + private List<@NotEmpty String> context; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialMetaData.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialMetaData.java new file mode 100644 index 000000000..42ec97a9f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialMetaData.java @@ -0,0 +1,29 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@Data +public class CredentialMetaData { + + @NotEmpty(message = "Display information must not be empty") + @Valid + @SerializedName("display") + @JsonProperty("display") + @Schema(description = "Display Properties of the Supported Credential") + private List<@Valid CredentialSupportedDisplayResponse> display; + + @JsonInclude(NON_NULL) + @Schema(description = "List of Claims") + private List> claims; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialSupportedDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialSupportedDisplayResponse.java new file mode 100644 index 000000000..16fcd7d0d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialSupportedDisplayResponse.java @@ -0,0 +1,51 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class CredentialSupportedDisplayResponse { + + @Expose + @NotBlank + @Schema(description = "Name of the Supported Credential") + String name; + + @Expose + @NotBlank + @Schema(description = "Locale of the Supported Credential") + String locale; + + @Expose + @NotNull + @Valid + @Schema(description = "Logo of the Supported Credential") + LogoDTO logo; + + @JsonProperty("background_image") + @SerializedName("background_image") + @Expose + @Valid + @Schema(description = "Background Image of the Supported Credential") + BackgroundImageDTO backgroundImage; + + @JsonProperty("background_color") + @SerializedName("background_color") + @Expose + @NotBlank + @Schema(description = "Background Colour of the Supported Credential") + String backgroundColor; + + @JsonProperty("text_color") + @SerializedName("text_color") + @Expose + @NotBlank + @Schema(description = "Text Colour of the Supported Credential") + String textColor; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialsSupportedResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialsSupportedResponse.java new file mode 100644 index 000000000..f0c1ad748 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/CredentialsSupportedResponse.java @@ -0,0 +1,45 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +@Data +public class CredentialsSupportedResponse { + @NotBlank(message = "Format must not be blank") + @Schema(description = "Format of the Credential") + private String format; + + @NotBlank(message = "Scope must not be blank") + @Schema(description = "Scope of the Credential") + private String scope; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Schema(description = "document Type of the Credential") + private String doctype; + + @NotEmpty(message = "Proof types supported must not be empty") + @Valid + @SerializedName("proof_types_supported") + @JsonProperty("proof_types_supported") + @Schema(description = "List of proof types supported") + private Map<@NotEmpty String, @Valid ProofTypesSupported> proofTypesSupported; + + @NotNull(message = "Credential metadata must not be null") + @Valid + @SerializedName("credential_metadata") + @JsonProperty("credential_metadata") + @Schema(description = "Credential Metadata containing display and claims") + private CredentialMetaData credentialMetadata; + + @Schema(description = "Identify the type or schema of the claims included") + private String vct; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/LogoDTO.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/LogoDTO.java new file mode 100644 index 000000000..95b33e913 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/LogoDTO.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Data +public class LogoDTO { + @Expose + @NotBlank + @URL + @JsonAlias("uri") + @SerializedName(value = "url", alternate = {"uri"}) + @Schema(description = "Display logo uri of the Issuer") + String url; + + @Expose + @NotBlank + @Schema(description = "Display logo alt text of the Issuer") + String alt_text; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/ProofTypesSupported.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/ProofTypesSupported.java new file mode 100644 index 000000000..a9393c3a2 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/ProofTypesSupported.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +@Data +public class ProofTypesSupported { + + @NotEmpty + @JsonProperty("proof_signing_alg_values_supported") + @SerializedName("proof_signing_alg_values_supported") + @Schema(description = "Support Alg for proof Signing") + private List proofSigningAlgValuesSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/V1WellKnownResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/V1WellKnownResponse.java new file mode 100644 index 000000000..871f9302f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/wellknown/v1/V1WellKnownResponse.java @@ -0,0 +1,51 @@ +package io.mosip.mimoto.dto.mimoto.wellknown.v1; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import org.hibernate.validator.constraints.URL; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class V1WellKnownResponse { + @NotBlank + @URL + @SerializedName("credential_issuer") + @JsonProperty("credential_issuer") + @Schema(description = "Unique Identifier of the Credential Issuer") + private String credentialIssuer; + + @NotEmpty + @SerializedName("authorization_servers") + @JsonProperty("authorization_servers") + @Schema(description = "List of Authorization Server Endpoint") + private List<@NotBlank @URL String> authorizationServers; + + @NotBlank + @URL + @SerializedName("credential_endpoint") + @JsonProperty("credential_endpoint") + @Schema(description = "Endpoint to download the Credential") + private String credentialEndPoint; + + @SerializedName("nonce_endpoint") + @JsonProperty("nonce_endpoint") + @Schema(description = "Endpoint to obtain a nonce for credential requests") + private String nonceEndpoint; + + @NotEmpty + @Valid + @SerializedName("credential_configurations_supported") + @JsonProperty("credential_configurations_supported") + @Schema(description = "List of Credential Configurations Supported") + private Map<@NotBlank String, @Valid CredentialsSupportedResponse> credentialConfigurationsSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionDeserializer.java b/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionDeserializer.java new file mode 100644 index 000000000..dbf83a846 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionDeserializer.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.openid; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import io.mosip.openID4VP.constants.SpecVersion; + +import java.io.IOException; + +class SpecVersionDeserializer extends JsonDeserializer { + + @Override + public SpecVersion deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return switch (parser.getValueAsString()) { + case "v1" -> SpecVersion.V1; + case "draft23" -> SpecVersion.DRAFT_23; + default -> throw new IllegalArgumentException("Unknown spec_version: " + parser.getValueAsString()); + }; + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionSerializer.java b/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionSerializer.java new file mode 100644 index 000000000..7483d10d4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/SpecVersionSerializer.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.openid; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.mosip.openID4VP.constants.SpecVersion; + +import java.io.IOException; + +class SpecVersionSerializer extends JsonSerializer { + + @Override + public void serialize(SpecVersion value, JsonGenerator generator, SerializerProvider serializers) throws IOException { + generator.writeString(value == SpecVersion.DRAFT_23 ? "draft23" : "v1"); + } +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/VerifierDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/VerifierDTO.java new file mode 100644 index 000000000..77ab0acda --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/VerifierDTO.java @@ -0,0 +1,52 @@ +package io.mosip.mimoto.dto.openid; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.mosip.openID4VP.constants.SpecVersion; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VerifierDTO { + @JsonProperty("client_id") + @Schema(description = "Client Id of the Verifier") + String clientId; + + @JsonProperty("redirect_uris") + @Schema(description = "Redirect URIs of the Verifier") + List redirectUris; + + @JsonProperty("response_uris") + @Schema(description = "Response URIs of the Verifier") + List responseUris; + + @JsonProperty("jwks_uri") + @Schema(description = "JWKS URI of the Verifier") + @JsonInclude(NON_NULL) + String jwksUri; + + @JsonProperty("allow_unsigned_request") + @Schema(description = "Flag to indicate if unsigned Authorization Request is allowed") + @Default + Boolean allowUnsignedRequest = false; + + @JsonProperty("spec_version") + @JsonDeserialize(using = SpecVersionDeserializer.class) + @JsonSerialize(using = SpecVersionSerializer.class) + @Schema(description = "OpenID4VP specification version supported by the Verifier") + @Default + SpecVersion specVersion = SpecVersion.V1; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/VerifiersDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/VerifiersDTO.java new file mode 100644 index 000000000..45f1b776f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/VerifiersDTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.openid; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VerifiersDTO { + @Schema(description = "Trusted Verifiers List") + List verifiers; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseDTO.java new file mode 100644 index 000000000..0b610ca1c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseDTO.java @@ -0,0 +1,29 @@ +package io.mosip.mimoto.dto.openid.datashare; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DataShareResponseDTO { + + @Schema(description = "url of the data share") + private String url; + + @Schema(description = "The valid for in minutes.") + private int validForInMinutes; + + @Schema(description = "The transactions allowed") + private int transactionsAllowed; + + @Schema(description = "Static policy id") + private String policyId; + + @Schema(description = "Static subscriber id") + private String subscriberId; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseWrapperDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseWrapperDTO.java new file mode 100644 index 000000000..3db0037a0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/datashare/DataShareResponseWrapperDTO.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.dto.openid.datashare; + +import io.mosip.mimoto.dto.ErrorDTO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DataShareResponseWrapperDTO { + private String id; + private String version; + private String responsetime; + private DataShareResponseDTO dataShare; + private List errors = new ArrayList<>(); +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/ConstraintsDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/ConstraintsDTO.java new file mode 100644 index 000000000..f89018619 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/ConstraintsDTO.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ConstraintsDTO { + FieldDTO[] fields; + + @JsonProperty("limitDisclosure") + private String limitDisclosure; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/FieldDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/FieldDTO.java new file mode 100644 index 000000000..0dcdd2d77 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/FieldDTO.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.openid.presentation; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FieldDTO { + String[] path; + FilterDTO filter; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/FilterDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/FilterDTO.java new file mode 100644 index 000000000..973fea5bb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/FilterDTO.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.openid.presentation; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FilterDTO { + String type; + String pattern; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/InputDescriptorDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/InputDescriptorDTO.java new file mode 100644 index 000000000..b01c11d1f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/InputDescriptorDTO.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class InputDescriptorDTO { + String id; + Map>> format; + ConstraintsDTO constraints; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationDefinitionDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationDefinitionDTO.java new file mode 100644 index 000000000..eed13571c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationDefinitionDTO.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PresentationDefinitionDTO { + + String id; + @JsonProperty("input_descriptors") + List inputDescriptors; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationRequestDTO.java new file mode 100644 index 000000000..739e8ced0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationRequestDTO.java @@ -0,0 +1,33 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PresentationRequestDTO { + + @JsonProperty("response_type") + String responseType; + String resource; + @JsonProperty("presentation_definition") + PresentationDefinitionDTO presentationDefinition; + @JsonProperty("client_id") + String clientId; + @JsonProperty("redirect_uri") + String redirectUri; + @JsonProperty("state") + String state; + @JsonProperty("nonce") + String nonce; + @JsonProperty("response_uri") + String responseUri; + @JsonProperty("response_uri") + String responseMode; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationSubmissionDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationSubmissionDTO.java new file mode 100644 index 000000000..0551275db --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/PresentationSubmissionDTO.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PresentationSubmissionDTO { + String id; + String definition_id; + @JsonProperty("descriptor_map") + List descriptorMap; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/SubmissionDescriptorDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/SubmissionDescriptorDTO.java new file mode 100644 index 000000000..32a24d738 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/SubmissionDescriptorDTO.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SubmissionDescriptorDTO { + String id; + String format; + String path; +} diff --git a/src/main/java/io/mosip/mimoto/dto/openid/presentation/VerifiablePresentationDTO.java b/src/main/java/io/mosip/mimoto/dto/openid/presentation/VerifiablePresentationDTO.java new file mode 100644 index 000000000..4d1687005 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/openid/presentation/VerifiablePresentationDTO.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.dto.openid.presentation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Data +@JsonPropertyOrder({"@context", "type", "verifiableCredential"}) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VerifiablePresentationDTO { + @JsonProperty("@context") + private Object context; + + @NotEmpty + private List<@NotBlank String> type; + + List verifiableCredential; +} diff --git a/src/main/java/io/mosip/mimoto/dto/resident/AidStatusResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/resident/AidStatusResponseDTO.java new file mode 100644 index 000000000..1bbb2e72f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/resident/AidStatusResponseDTO.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.resident; + +import lombok.Data; + +@Data +public class AidStatusResponseDTO { + + private String individualId; + private String individualIdType; + private String transactionID; + private String aidStatus; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/resident/CredentialRequestResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/resident/CredentialRequestResponseDTO.java index 62c314ff5..d8444ef7f 100644 --- a/src/main/java/io/mosip/mimoto/dto/resident/CredentialRequestResponseDTO.java +++ b/src/main/java/io/mosip/mimoto/dto/resident/CredentialRequestResponseDTO.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import io.mosip.mimoto.dto.ErrorDTO; import lombok.Data; diff --git a/src/main/java/io/mosip/mimoto/dto/resident/IndividualIdOTPRequestDTO.java b/src/main/java/io/mosip/mimoto/dto/resident/IndividualIdOTPRequestDTO.java new file mode 100644 index 000000000..1cd9e0942 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/resident/IndividualIdOTPRequestDTO.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.resident; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class IndividualIdOTPRequestDTO { + + private String id; + private String version; + private String transactionId; + private String requestTime; + private List otpChannel; + private String individualId; + private Map metadata; + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/resident/VerifiablePresentationSessionData.java b/src/main/java/io/mosip/mimoto/dto/resident/VerifiablePresentationSessionData.java new file mode 100644 index 000000000..cd3442402 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/resident/VerifiablePresentationSessionData.java @@ -0,0 +1,23 @@ +package io.mosip.mimoto.dto.resident; + +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VerifiablePresentationSessionData implements Serializable { + private String presentationId; + private String authorizationRequest; + private Instant createdAt; + private boolean isVerifierClientPreregistered; + private List matchingCredentials; + /** True when the authorization request had a DCQL query; selects DCQL vs PE flow. */ + private boolean dcql; +} diff --git a/src/main/java/io/mosip/mimoto/dto/resident/WalletCredentialResponseDTO.java b/src/main/java/io/mosip/mimoto/dto/resident/WalletCredentialResponseDTO.java new file mode 100644 index 000000000..ed13dd79d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/resident/WalletCredentialResponseDTO.java @@ -0,0 +1,24 @@ +package io.mosip.mimoto.dto.resident; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.core.io.InputStreamResource; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class WalletCredentialResponseDTO { + + @JsonProperty("file_content_stream") + @Schema(description = "This is a PDF input stream containing the requested credential data in byte format") + InputStreamResource fileContentStream; + + @JsonProperty("file_name") + @Schema(description = " Name of the PDF file") + String fileName; +} diff --git a/src/main/java/io/mosip/mimoto/dto/serializer/SelectedCredentialsSerializer.java b/src/main/java/io/mosip/mimoto/dto/serializer/SelectedCredentialsSerializer.java new file mode 100644 index 000000000..064b21842 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/serializer/SelectedCredentialsSerializer.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.dto.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.mosip.mimoto.dto.SelectedCredentials; + +import java.io.IOException; + +public class SelectedCredentialsSerializer extends StdSerializer { + + public SelectedCredentialsSerializer() { + super(SelectedCredentials.class); + } + + @Override + public void serialize(SelectedCredentials value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeStartArray(); + if (value.isDcql()) { + for (var selection : value.getDcqlSelections()) { + gen.writeObject(selection); + } + } else if (value.getCredentialIds() != null) { + for (String id : value.getCredentialIds()) { + gen.writeString(id); + } + } + gen.writeEndArray(); + } +} diff --git a/src/main/java/io/mosip/mimoto/entity/BDBInfo.java b/src/main/java/io/mosip/mimoto/entity/BDBInfo.java deleted file mode 100644 index 5b93c9aed..000000000 --- a/src/main/java/io/mosip/mimoto/entity/BDBInfo.java +++ /dev/null @@ -1,212 +0,0 @@ -package io.mosip.mimoto.entity; - -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.List; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlList; -import javax.xml.bind.annotation.XmlSchemaType; -import javax.xml.bind.annotation.XmlType; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import io.mosip.mimoto.constant.BiometricType; -import io.mosip.mimoto.constant.ProcessedLevelType; -import io.mosip.mimoto.constant.PurposeType; -import io.mosip.mimoto.constant.QualityType; -import io.mosip.mimoto.util.DateAdapter; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author Ramadurai Pandian - * - */ -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "BDBInfoType", propOrder = { "challengeResponse", "index", "format", "encryption", "creationDate", - "notValidBefore", "notValidAfter", "type", "subtype", "level", "product", "captureDevice", - "featureExtractionAlgorithm", "comparisonAlgorithm", "compressionAlgorithm", "purpose", "quality" }) -@Data -@NoArgsConstructor -@JsonDeserialize(builder = BDBInfo.BDBInfoBuilder.class) -public class BDBInfo implements Serializable { - - @XmlElement(name = "ChallengeResponse") - private byte[] challengeResponse; - @XmlElement(name = "Index") - private String index; - @XmlElement(name = "Format") - private RegistryIDType format; - @XmlElement(name = "Encryption") - private Boolean encryption; - @XmlElement(name = "CreationDate") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime creationDate; - @XmlElement(name = "NotValidBefore") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime notValidBefore; - @XmlElement(name = "NotValidAfter") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime notValidAfter; - @XmlList - @XmlElement(name = "Type") - private List type; - @XmlList - @XmlElement(name = "Subtype") - private List subtype; - @XmlElement(name = "Level") - @XmlSchemaType(name = "string") - private ProcessedLevelType level; - @XmlElement(name = "Product") - private RegistryIDType product; - @XmlElement(name = "CaptureDevice") - private RegistryIDType captureDevice; - @XmlElement(name = "FeatureExtractionAlgorithm") - private RegistryIDType featureExtractionAlgorithm; - @XmlElement(name = "ComparisonAlgorithm") - private RegistryIDType comparisonAlgorithm; - @XmlElement(name = "CompressionAlgorithm") - private RegistryIDType compressionAlgorithm; - @XmlElement(name = "Purpose") - @XmlSchemaType(name = "string") - private PurposeType purpose; - @XmlElement(name = "Quality") - private QualityType quality; - - public BDBInfo(BDBInfoBuilder bDBInfoBuilder) { - this.challengeResponse = bDBInfoBuilder.challengeResponse; - this.index = bDBInfoBuilder.index; - this.format = bDBInfoBuilder.format; - this.encryption = bDBInfoBuilder.encryption; - this.creationDate = bDBInfoBuilder.creationDate; - this.notValidBefore = bDBInfoBuilder.notValidBefore; - this.notValidAfter = bDBInfoBuilder.notValidAfter; - this.type = bDBInfoBuilder.type; - this.subtype = bDBInfoBuilder.subtype; - this.level = bDBInfoBuilder.level; - this.product = bDBInfoBuilder.product; - this.purpose = bDBInfoBuilder.purpose; - this.quality = bDBInfoBuilder.quality; - this.captureDevice = bDBInfoBuilder.captureDevice; - this.featureExtractionAlgorithm = bDBInfoBuilder.featureExtractionAlgorithm; - this.comparisonAlgorithm = bDBInfoBuilder.comparisonAlgorithm; - this.compressionAlgorithm = bDBInfoBuilder.compressionAlgorithm; - } - - public static class BDBInfoBuilder { - private byte[] challengeResponse; - private String index; - private RegistryIDType format; - private Boolean encryption; - private LocalDateTime creationDate; - private LocalDateTime notValidBefore; - private LocalDateTime notValidAfter; - private List type; - private List subtype; - private ProcessedLevelType level; - private RegistryIDType product; - private PurposeType purpose; - private QualityType quality; - private RegistryIDType captureDevice; - private RegistryIDType featureExtractionAlgorithm; - private RegistryIDType comparisonAlgorithm; - private RegistryIDType compressionAlgorithm; - - public BDBInfoBuilder withChallengeResponse(byte[] challengeResponse) { - this.challengeResponse = challengeResponse; - return this; - } - - public BDBInfoBuilder withIndex(String index) { - this.index = index; - return this; - } - - public BDBInfoBuilder withFormat(RegistryIDType format) { - this.format = format; - return this; - } - - public BDBInfoBuilder withEncryption(Boolean encryption) { - this.encryption = encryption; - return this; - } - - public BDBInfoBuilder withCreationDate(LocalDateTime creationDate) { - this.creationDate = creationDate; - return this; - } - - public BDBInfoBuilder withNotValidBefore(LocalDateTime notValidBefore) { - this.notValidBefore = notValidBefore; - return this; - } - - public BDBInfoBuilder withNotValidAfter(LocalDateTime notValidAfter) { - this.notValidAfter = notValidAfter; - return this; - } - - public BDBInfoBuilder withType(List type) { - this.type = type; - return this; - } - - public BDBInfoBuilder withSubtype(List subtype) { - this.subtype = subtype; - return this; - } - - public BDBInfoBuilder withLevel(ProcessedLevelType level) { - this.level = level; - return this; - } - - public BDBInfoBuilder withProduct(RegistryIDType product) { - this.product = product; - return this; - } - - public BDBInfoBuilder withPurpose(PurposeType purpose) { - this.purpose = purpose; - return this; - } - - public BDBInfoBuilder withQuality(QualityType quality) { - this.quality = quality; - return this; - } - - public BDBInfo build() { - // TODO - return new BDBInfo(this); - } - - public BDBInfoBuilder withCaptureDevice(RegistryIDType captureDevice) { - this.captureDevice = captureDevice; - return this; - } - - public BDBInfoBuilder withFeatureExtractionAlgorithm(RegistryIDType featureExtractionAlgorithm) { - this.featureExtractionAlgorithm = featureExtractionAlgorithm; - return this; - } - - public BDBInfoBuilder withComparisonAlgorithm(RegistryIDType comparisonAlgorithm) { - this.comparisonAlgorithm = comparisonAlgorithm; - return this; - } - - public BDBInfoBuilder withCompressionAlgorithm(RegistryIDType compressionAlgorithm) { - this.compressionAlgorithm = compressionAlgorithm; - return this; - } - } -} diff --git a/src/main/java/io/mosip/mimoto/entity/BIR.java b/src/main/java/io/mosip/mimoto/entity/BIR.java deleted file mode 100644 index c155d1546..000000000 --- a/src/main/java/io/mosip/mimoto/entity/BIR.java +++ /dev/null @@ -1,136 +0,0 @@ -package io.mosip.mimoto.entity; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlType; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import io.mosip.mimoto.util.Base64Adapter; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * - * BIR class with Builder to create data - * - * @author Ramadurai Pandian - * - */ -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "BIRType", propOrder = { "version", "cbeffversion", "birInfo", "bdbInfo", "bdb", - "sb", "birs", "sbInfo", "others" }) -@XmlRootElement(name = "BIR") -@Data -@NoArgsConstructor -@JsonDeserialize(builder = BIR.BIRBuilder.class) -public class BIR implements Serializable { - - @XmlElement(name = "Version") - private VersionType version; - @XmlElement(name = "CBEFFVersion") - private VersionType cbeffversion; - @XmlElement(name = "BIRInfo", required = true) - private BIRInfo birInfo; - @XmlElement(name = "BDBInfo") - private BDBInfo bdbInfo; - @XmlElement(name = "BDB") - @XmlJavaTypeAdapter(Base64Adapter.class) - private byte[] bdb; - @XmlElement(name = "SB") - private byte[] sb; - @XmlElement(name = "BIR") - protected List birs; - @XmlElement(name = "SBInfo") - private SBInfo sbInfo; - @XmlElement(name = "Others") - private Map others; - - public BIR(BIRBuilder birBuilder) { - this.version = birBuilder.version; - this.cbeffversion = birBuilder.cbeffversion; - this.birInfo = birBuilder.birInfo; - this.bdbInfo = birBuilder.bdbInfo; - this.bdb = birBuilder.bdb; - this.sb = birBuilder.sb; - this.sbInfo = birBuilder.sbInfo; - this.setOthers(birBuilder.others); - } - - public static class BIRBuilder { - private VersionType version; - private VersionType cbeffversion; - private BIRInfo birInfo; - private BDBInfo bdbInfo; - private byte[] bdb; - private byte[] sb; - private SBInfo sbInfo; - private Map others; - - public BIRBuilder withOther(String key, Object value) { - if (Objects.isNull(others)) - this.others = new HashMap<>(); - - this.others.put(key, value); - return this; - } - - public BIRBuilder withOthers(Map others) { - if (Objects.isNull(this.others)) - this.others = new HashMap<>(); - if (!Objects.isNull(others)) - this.others.putAll(others); - return this; - } - - public BIRBuilder withVersion(VersionType version) { - this.version = version; - return this; - } - - public BIRBuilder withCbeffversion(VersionType cbeffversion) { - this.cbeffversion = cbeffversion; - return this; - } - - public BIRBuilder withBirInfo(BIRInfo birInfo) { - this.birInfo = birInfo; - return this; - } - - public BIRBuilder withBdbInfo(BDBInfo bdbInfo) { - this.bdbInfo = bdbInfo; - return this; - } - - public BIRBuilder withBdb(byte[] bdb) { - this.bdb = bdb; - return this; - } - - public BIRBuilder withSb(byte[] sb) { - this.sb = sb; - return this; - } - - public BIRBuilder withSbInfo(SBInfo sbInfo) { - this.sbInfo = sbInfo; - return this; - } - - public BIR build() { - return new BIR(this); - } - - } - -} diff --git a/src/main/java/io/mosip/mimoto/entity/BIRInfo.java b/src/main/java/io/mosip/mimoto/entity/BIRInfo.java deleted file mode 100644 index fc6751975..000000000 --- a/src/main/java/io/mosip/mimoto/entity/BIRInfo.java +++ /dev/null @@ -1,115 +0,0 @@ -package io.mosip.mimoto.entity; - -import java.io.Serializable; -import java.time.LocalDateTime; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlSchemaType; -import javax.xml.bind.annotation.XmlType; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import io.mosip.mimoto.util.DateAdapter; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author Ramadurai Pandian - * - */ -@XmlAccessorType(XmlAccessType.FIELD) -@XmlType(name = "BIRInfoType", propOrder = { "creator", "index", "payload", "integrity", "creationDate", - "notValidBefore", "notValidAfter" }) -@Data -@NoArgsConstructor -@JsonDeserialize(builder = BIRInfo.BIRInfoBuilder.class) -public class BIRInfo implements Serializable { - - /** - * - */ - private static final long serialVersionUID = -2466414332099574792L; - @XmlElement(name = "Creator") - private String creator; - @XmlElement(name = "Index") - private String index; - @XmlElement(name = "Payload") - private byte[] payload; - @XmlElement(name = "Integrity") - private Boolean integrity; - @XmlElement(name = "CreationDate") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime creationDate; - @XmlElement(name = "NotValidBefore") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime notValidBefore; - @XmlElement(name = "NotValidAfter") - @XmlSchemaType(name = "dateTime") - @XmlJavaTypeAdapter(DateAdapter.class) - private LocalDateTime notValidAfter; - - public BIRInfo(BIRInfoBuilder bIRInfoBuilder) { - this.creator = bIRInfoBuilder.creator; - this.index = bIRInfoBuilder.index; - this.payload = bIRInfoBuilder.payload; - this.integrity = bIRInfoBuilder.integrity; - this.creationDate = bIRInfoBuilder.creationDate; - this.notValidBefore = bIRInfoBuilder.notValidBefore; - this.notValidAfter = bIRInfoBuilder.notValidAfter; - } - - public static class BIRInfoBuilder { - private String creator; - private String index; - private byte[] payload; - private Boolean integrity; - private LocalDateTime creationDate; - private LocalDateTime notValidBefore; - private LocalDateTime notValidAfter; - - public BIRInfoBuilder withCreator(String creator) { - this.creator = creator; - return this; - } - - public BIRInfoBuilder withIndex(String index) { - this.index = index; - return this; - } - - public BIRInfoBuilder withPayload(byte[] payload) { - this.payload = payload; - return this; - } - - public BIRInfoBuilder withIntegrity(Boolean integrity) { - this.integrity = integrity; - return this; - } - - public BIRInfoBuilder withCreationDate(LocalDateTime creationDate) { - this.creationDate = creationDate; - return this; - } - - public BIRInfoBuilder withNotValidBefore(LocalDateTime notValidBefore) { - this.notValidBefore = notValidBefore; - return this; - } - - public BIRInfoBuilder withNotValidAfter(LocalDateTime notValidAfter) { - this.notValidAfter = notValidAfter; - return this; - } - - public BIRInfo build() { - return new BIRInfo(this); - } - - } -} diff --git a/src/main/java/io/mosip/mimoto/entity/BiometricRecord.java b/src/main/java/io/mosip/mimoto/entity/BiometricRecord.java deleted file mode 100644 index 5b1dcba74..000000000 --- a/src/main/java/io/mosip/mimoto/entity/BiometricRecord.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.mosip.mimoto.entity; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -import lombok.Data; - -/** - * - * BIR class with Builder to create data - * - * @author Ramadurai Pandian - * - */ - -@Data -public class BiometricRecord implements Serializable { - - protected VersionType version; - protected VersionType cbeffversion; - protected BIRInfo birInfo; - /** - * This can be of any modality, each subtype is an element in this list. - * it has type and subtype info in it - */ - protected List segments; - - public BiometricRecord() { - this.segments = new ArrayList<>(); - } - - public BiometricRecord(VersionType version, VersionType cbeffversion, BIRInfo birInfo) { - this.version = version; - this.cbeffversion = cbeffversion; - this.birInfo = birInfo; - this.segments = new ArrayList(); - } - -} diff --git a/src/main/java/io/mosip/mimoto/exception/ApisResourceAccessException.java b/src/main/java/io/mosip/mimoto/exception/ApisResourceAccessException.java index 5d6c88671..fe2b85d70 100644 --- a/src/main/java/io/mosip/mimoto/exception/ApisResourceAccessException.java +++ b/src/main/java/io/mosip/mimoto/exception/ApisResourceAccessException.java @@ -35,7 +35,4 @@ public ApisResourceAccessException(String message) { public ApisResourceAccessException(String message, Throwable cause) { super(PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getCode(), message, cause); } - - public ApisResourceAccessException(String code, String message, Exception e) { - } } \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/exception/AuthorizationServerWellknownResponseException.java b/src/main/java/io/mosip/mimoto/exception/AuthorizationServerWellknownResponseException.java new file mode 100644 index 000000000..0fbe511ac --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/AuthorizationServerWellknownResponseException.java @@ -0,0 +1,7 @@ +package io.mosip.mimoto.exception; + +public class AuthorizationServerWellknownResponseException extends BaseCheckedException { + public AuthorizationServerWellknownResponseException(String message) { + super(PlatformErrorMessages.INVALID_AUTHORIZATION_SERVER_WELLKNOWN_RESPONSE_EXCEPTION.getCode(), PlatformErrorMessages.INVALID_AUTHORIZATION_SERVER_WELLKNOWN_RESPONSE_EXCEPTION.getMessage() + "\n" + message); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/CorruptedEncryptedDataException.java b/src/main/java/io/mosip/mimoto/exception/CorruptedEncryptedDataException.java new file mode 100644 index 000000000..661224a26 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/CorruptedEncryptedDataException.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.exception; + +public class CorruptedEncryptedDataException extends BaseCheckedException { + private static final long serialVersionUID = 1L; + + + public CorruptedEncryptedDataException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } + + public CorruptedEncryptedDataException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/CredentialNotFoundException.java b/src/main/java/io/mosip/mimoto/exception/CredentialNotFoundException.java new file mode 100644 index 000000000..828a07cd1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/CredentialNotFoundException.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.exception; + +import java.io.Serial; + +public class CredentialNotFoundException extends BaseCheckedException { + @Serial + private static final long serialVersionUID = 1L; + + public CredentialNotFoundException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + } + + public CredentialNotFoundException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/CredentialPdfGenerationException.java b/src/main/java/io/mosip/mimoto/exception/CredentialPdfGenerationException.java new file mode 100644 index 000000000..eef1f4eb5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/CredentialPdfGenerationException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class CredentialPdfGenerationException extends BaseUncheckedException { + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + public CredentialPdfGenerationException() { + super(); + } + + public CredentialPdfGenerationException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } + + public CredentialPdfGenerationException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/CredentialProcessingException.java b/src/main/java/io/mosip/mimoto/exception/CredentialProcessingException.java new file mode 100644 index 000000000..45f332e32 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/CredentialProcessingException.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.exception; + +public class CredentialProcessingException extends BaseUncheckedException { + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + public CredentialProcessingException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } + + public CredentialProcessingException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/DecryptionException.java b/src/main/java/io/mosip/mimoto/exception/DecryptionException.java new file mode 100644 index 000000000..79dfc8398 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/DecryptionException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class DecryptionException extends BaseCheckedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + + public DecryptionException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } + + public DecryptionException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/EncryptionException.java b/src/main/java/io/mosip/mimoto/exception/EncryptionException.java new file mode 100644 index 000000000..967c906fc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/EncryptionException.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.exception; + +import java.io.Serial; + +public class EncryptionException extends BaseUncheckedException { + + @Serial + private static final long serialVersionUID = 1L; + + public EncryptionException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } + + public EncryptionException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/ErrorConstants.java b/src/main/java/io/mosip/mimoto/exception/ErrorConstants.java new file mode 100644 index 000000000..8abdf8339 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/ErrorConstants.java @@ -0,0 +1,62 @@ +package io.mosip.mimoto.exception; + +import io.mosip.mimoto.model.WalletLockStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorConstants { + + INVALID_REQUEST("invalid_request", "Some incorrect parameters in the request"), + UNSUPPORTED_FORMAT("unsupported_format", "No VC of this format is found"), + RESOURCE_NOT_FOUND("resource_not_found", "The requested resource doesn’t exist."), + SERVER_UNAVAILABLE("server_unavailable", "The server is not reachable right now."), + RESOURCE_EXPIRED("resource_expired", "The requested resource expired."), + RESOURCE_INVALID("invalid_resource", "The requested resource is invalid."), + REQUEST_TIMED_OUT("request_timed_out", "We are unable to process your request right now"), + URI_TOO_LONG("uri_too_long", "Resource URI is too long to be handled"), + INVALID_CLIENT("invalid_client", "The requested client doesn’t match."), + INVALID_REDIRECT_URI("invalid_redirect_uri", "The requested redirect uri doesn’t match."), + INVALID_RESPONSE_URI("invalid_response_uri", "The requested response uri doesn’t match."), + INTERNAL_SERVER_ERROR("internal_server_error", "We are unable to process request now"), + + PROOF_TYPE_NOT_SUPPORTED_EXCEPTION("proof_type_not_supported", "Proof Type available in received credentials is not matching with supported proof terms"), + SIGNATURE_VERIFICATION_EXCEPTION("signature_verification_failed", "Error while doing signature verification"), + JSON_PARSING_EXCEPTION("json_parsing_failed", "Given data couldn't be parsed to JSON string"), + UNKNOWN_EXCEPTION("unknown_exception", "Error while doing verification of verifiable credential"), + PROOF_DOCUMENT_NOT_FOUND_EXCEPTION("proof_document_not_found", "Proof document is not available in the received credentials"), + PUBLIC_KEY_NOT_FOUND_EXCEPTION("public_key_not_found", "Proof document is not available in the received credentials"), + OAUTH2_AUTHENTICATION_EXCEPTION("user_authentication_error", "Failed to authenticate user via OAuth Identity Provider during login"), + LOGIN_SESSION_INVALIDATE_EXCEPTION("user_logout_error", "Exception occurred when invalidating the session of a user"), + SESSION_EXPIRED_OR_INVALID("session_invalid_or_expired", "User session is missing or expired. Please log in again."), + DATABASE_CONNECTION_EXCEPTION("database_unavailable", "Failed to connect to the database"), + REDIS_CONNECTION_EXCEPTION("redis_unavailable", "Failed to connect to the redis"), + ENCRYPTION_FAILED("encryption_failed", "Failed to encrypt the data"), + DECRYPTION_FAILED("decryption_failed", "Failed to decrypt the data"), + SCHEMA_MISMATCH("schema_mismatch", "Failed to restored the stored data"), + INVALID_USER("invalid_user", "User does not exist in database"), + CREDENTIAL_DOWNLOAD_EXCEPTION("credential_download_error", "Failed to download and store the credential"), + CREDENTIAL_FETCH_EXCEPTION("credential_fetch_error", "Failed to fetch the credential"), + + UNAUTHORIZED_ACCESS("unauthorized", "You are not authorized to access this resource"), + WALLET_LOCKED("wallet_locked", "Wallet is locked"), + INVALID_PIN("invalid_pin", "Invalid PIN or wallet key provided"), + WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT.getValue(),"Incorrect passcode. Last attempt remaining before your Wallet is permanently locked"), + WALLET_TEMPORARILY_LOCKED(WalletLockStatus.TEMPORARILY_LOCKED.getValue(), "You’ve reached the maximum number of attempts. Your wallet is now temporarily locked"), + WALLET_PERMANENTLY_LOCKED(WalletLockStatus.PERMANENTLY_LOCKED.getValue(), "Your wallet has been permanently locked due to multiple failed attempts. Please click on forgot password to reset your wallet to continue"), + + WALLET_CREATE_VP_EXCEPTION("wallet_vp_creation_failed", "Failed to create Verifiable Presentation and store the details in session cache for the Wallet"), + DUPLICATE_VERIFIER("duplicate_verifier", "This verifier is already trusted."), + ERROR_ADDING_TRUSTED_VERIFIER("error", "Failed to add trusted verifier"), + REJECT_VERIFIER_EXCEPTION("error", "Failed to submit Verifiable Presentation."), + REJECTED_VERIFIER("success", "Presentation request rejected. An OpenID4VP error response has been sent to the verifier."), + JWT_SIGNING_ERROR("jwt_signing_error", "Failed to sign JWT token during presentation submission"), + KEY_GENERATION_ERROR("key_generation_error", "Failed to generate or retrieve cryptographic key"), + DECRYPTION_ERROR("decryption_error", "Failed to decrypt data"), + INVALID_RESPONSE_MODE("invalid_response_mode", "The requested response mode is not supported."); + + private final String errorCode; + private final String errorMessage; + +} diff --git a/src/main/java/io/mosip/mimoto/exception/ExceptionUtils.java b/src/main/java/io/mosip/mimoto/exception/ExceptionUtils.java index 4db3bcfe2..eff363700 100644 --- a/src/main/java/io/mosip/mimoto/exception/ExceptionUtils.java +++ b/src/main/java/io/mosip/mimoto/exception/ExceptionUtils.java @@ -1,25 +1,21 @@ package io.mosip.mimoto.exception; +import lombok.extern.slf4j.Slf4j; + import java.io.PrintWriter; import java.io.StringWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.databind.JsonNode; - /** * This utils contains exception utilities. - * + * * @author Shashank Agrawal * @author Ritesh Sinha * @since 1.0.0 * */ +@Slf4j public final class ExceptionUtils { - private static final Logger logger = LoggerFactory.getLogger(ExceptionUtils.class); - private ExceptionUtils() { } @@ -27,7 +23,7 @@ private ExceptionUtils() { /** * Returns an String object that can be used after building the exception stack * trace. - * + * * @param message the exception message * @param cause the cause * @return the exception stack @@ -48,7 +44,7 @@ public static String buildMessage(String message, Throwable cause) { /** * This method returns the stack trace - * + * * @param throwable the exception to be added to the list of exception * @return the stack trace */ @@ -59,22 +55,8 @@ public static String getStackTrace(Throwable throwable) { return sw.getBuffer().toString(); } - /** - * This method provide jsonvalue based on propname mention. - * - * @param node the jsonnode. - * @param propName the property name. - * @return the property value. - */ - private static String getJsonValue(JsonNode node, String propName) { - if (node.get(propName) != null) { - return node.get(propName).asText(); - } - return null; - } - public static void logRootCause(Throwable exception) { - logger.error("Exception Root Cause: {} ", exception.getMessage()); - logger.debug("Exception Root Cause:", exception); + log.error("Exception Root Cause: {} ", exception.getMessage()); + log.debug("Exception Root Cause:", exception); } } diff --git a/src/main/java/io/mosip/mimoto/exception/ExternalServiceUnavailableException.java b/src/main/java/io/mosip/mimoto/exception/ExternalServiceUnavailableException.java new file mode 100644 index 000000000..b21a425dc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/ExternalServiceUnavailableException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class ExternalServiceUnavailableException extends BaseCheckedException { + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + public ExternalServiceUnavailableException() { + super(); + } + + public ExternalServiceUnavailableException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + + } + + public ExternalServiceUnavailableException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/IdpException.java b/src/main/java/io/mosip/mimoto/exception/IdpException.java new file mode 100644 index 000000000..2d62ebd08 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/IdpException.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.exception; + +public class IdpException extends BaseUncheckedException { + /** The Constant serialVersionUID. */ + private static final long serialVersionUID = 1L; + + /** + * Instantiates a new Idp exception. + * + */ + public IdpException() { + super(PlatformErrorMessages.MIMOTO_IDP_GENERIC_EXCEPTION.getCode(), + PlatformErrorMessages.MIMOTO_IDP_GENERIC_EXCEPTION.getMessage()); + } + + public IdpException(String message) { + super(PlatformErrorMessages.MIMOTO_IDP_GENERIC_EXCEPTION.getCode(), message); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidCredentialResourceException.java b/src/main/java/io/mosip/mimoto/exception/InvalidCredentialResourceException.java new file mode 100644 index 000000000..1efc763b1 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/InvalidCredentialResourceException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class InvalidCredentialResourceException extends BaseUncheckedException { + + private static final long serialVersionUID = -5350213197226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + public InvalidCredentialResourceException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } + + public InvalidCredentialResourceException(String errorMessage) { + super(ErrorConstants.RESOURCE_NOT_FOUND.getErrorCode(), errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidInputException.java b/src/main/java/io/mosip/mimoto/exception/InvalidInputException.java index 279ed8239..edb6e5c5c 100644 --- a/src/main/java/io/mosip/mimoto/exception/InvalidInputException.java +++ b/src/main/java/io/mosip/mimoto/exception/InvalidInputException.java @@ -24,4 +24,8 @@ public InvalidInputException(String errorCode, String errorMessage) { super(errorCode, errorMessage); } + public InvalidInputException(String errorMessage) { + super(PlatformErrorMessages.MIMOTO_PGS_INVALID_INPUT_PARAMETER.getCode(), errorMessage); + } + } diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidIssuerIdException.java b/src/main/java/io/mosip/mimoto/exception/InvalidIssuerIdException.java new file mode 100644 index 000000000..87bcda928 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/InvalidIssuerIdException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class InvalidIssuerIdException extends BaseUncheckedException { + + /** + * Unique id for serialization + */ + private static final long serialVersionUID = -5350213127226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + + public InvalidIssuerIdException() { + super(PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION.getCode(), PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION.getMessage()); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidRequestException.java b/src/main/java/io/mosip/mimoto/exception/InvalidRequestException.java new file mode 100644 index 000000000..e7e3eb006 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/InvalidRequestException.java @@ -0,0 +1,17 @@ +package io.mosip.mimoto.exception; + +import java.io.Serial; + +public class InvalidRequestException extends BaseUncheckedException { + + @Serial + private static final long serialVersionUID = 1L; + + public InvalidRequestException(String errorCode, String errorMessage, Throwable cause) { + super(errorCode, errorMessage, cause); + } + + public InvalidRequestException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidVerifierException.java b/src/main/java/io/mosip/mimoto/exception/InvalidVerifierException.java new file mode 100644 index 000000000..82a6844d2 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/InvalidVerifierException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class InvalidVerifierException extends BaseUncheckedException { + + private static final long serialVersionUID = -5350213197226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + public InvalidVerifierException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } + + public InvalidVerifierException(String errorMessage) { + super(PlatformErrorMessages.MIMOTO_PGS_INVALID_INPUT_PARAMETER.getCode(), errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/InvalidWellknownResponseException.java b/src/main/java/io/mosip/mimoto/exception/InvalidWellknownResponseException.java new file mode 100644 index 000000000..549a6ef24 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/InvalidWellknownResponseException.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.exception; + +public class InvalidWellknownResponseException extends BaseCheckedException { + + public InvalidWellknownResponseException() { + super(PlatformErrorMessages.INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION.getCode(), PlatformErrorMessages.INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION.getMessage()); + } + + public InvalidWellknownResponseException(String message) { + super(PlatformErrorMessages.INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION.getCode(), PlatformErrorMessages.INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION.getMessage() + "\n" + message); + + } + + public InvalidWellknownResponseException(String message, Throwable cause) { + super(PlatformErrorMessages.INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION.getCode(), message, cause); + } + + public InvalidWellknownResponseException(String code, String message) { + super(code, message); + } + +} diff --git a/src/main/java/io/mosip/mimoto/exception/IssuerOnboardingException.java b/src/main/java/io/mosip/mimoto/exception/IssuerOnboardingException.java new file mode 100644 index 000000000..930210ef9 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/IssuerOnboardingException.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.exception; + +public class IssuerOnboardingException extends BaseUncheckedException{ + + private static final long serialVersionUID = 1L; + + public IssuerOnboardingException(String errorMessage) { + super(PlatformErrorMessages.MIMOTO_ISSUER_ONBOARDING_EXCEPTION.getCode(), errorMessage); + } + +} diff --git a/src/main/java/io/mosip/mimoto/exception/KeyGenerationException.java b/src/main/java/io/mosip/mimoto/exception/KeyGenerationException.java new file mode 100644 index 000000000..a231e940b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/KeyGenerationException.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.exception; + +public class KeyGenerationException extends BaseUncheckedException { + + public KeyGenerationException(String code, String message) { + super(code, message); + } + + public KeyGenerationException(String code, String message, Throwable rootCause) { + super(code, message, rootCause); + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/exception/LoginSessionException.java b/src/main/java/io/mosip/mimoto/exception/LoginSessionException.java new file mode 100644 index 000000000..76f2125b2 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/LoginSessionException.java @@ -0,0 +1,17 @@ + +package io.mosip.mimoto.exception; + +import org.springframework.http.HttpStatus; + +public class LoginSessionException extends BaseCheckedException { + private final HttpStatus status; + + public LoginSessionException(String code, String message, HttpStatus status) { + super(code, message); + this.status = status; + } + + public HttpStatus getStatus() { + return status; + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/OAuth2AuthenticationException.java b/src/main/java/io/mosip/mimoto/exception/OAuth2AuthenticationException.java new file mode 100644 index 000000000..bf9c9131e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/OAuth2AuthenticationException.java @@ -0,0 +1,17 @@ +package io.mosip.mimoto.exception; + +import org.springframework.http.HttpStatus; + +public class OAuth2AuthenticationException extends BaseCheckedException { + private final HttpStatus status; + + public OAuth2AuthenticationException(String code, String message, HttpStatus status) { + super(code, message); + this.status=status; + } + + public HttpStatus getStatus() { + return status; + } +} + diff --git a/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java b/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java index b94fe3848..d3647a4ec 100644 --- a/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java +++ b/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java @@ -35,7 +35,7 @@ public enum PlatformErrorMessages { /** The rgs json parsing exception. */ MIMOTO_RGS_JSON_PARSING_EXCEPTION(PlatformConstants.PREFIX + "017", "JSON Parsing Failed"), /** The invalid input parameter. */ - MIMOTO_PGS_INVALID_INPUT_PARAMETER(PlatformConstants.PREFIX + "011", "Invalid Input Parameter - %s"), + MIMOTO_PGS_INVALID_INPUT_PARAMETER(PlatformConstants.PREFIX + "011", "Invalid Input Parameter"), /** The rgs json mapping exception. */ MIMOTO_RGS_JSON_MAPPING_EXCEPTION(PlatformConstants.PREFIX + "016", "JSON Mapping Failed"), MIMOTO_PDF_SIGNATURE_EXCEPTION(PlatformConstants.PREFIX + "024", "PDF Signature error"), @@ -73,7 +73,24 @@ public enum PlatformErrorMessages { API_NOT_ACCESSIBLE_EXCEPTION(PlatformConstants.PREFIX + "026", "Api not accessible failure"), CERTIFICATE_THUMBPRINT_ERROR(PlatformConstants.PREFIX + "026", "certificate thumbprint failure"), MIMOTO_INVALID_KEY_EXCEPTION(PlatformConstants.PREFIX + "027", "invalid key"), - MIMOTO_PDF_SIGN_EXCEPTION(PlatformConstants.PREFIX + "028", "error occured while signing pdf"); + MIMOTO_PDF_SIGN_EXCEPTION(PlatformConstants.PREFIX + "028", "error occured while signing pdf"), + MIMOTO_WALLET_BINDING_EXCEPTION(PlatformConstants.PREFIX + "028", "Wallet binding exception occured"), + MIMOTO_OTP_BINDING_EXCEPTION(PlatformConstants.PREFIX + "029", "OTP binding exception occured"), + MIMOTO_LINK_TRANSACTION_EXCEPTION(PlatformConstants.PREFIX + "030", "Idp link transaction exception occured"), + MIMOTO_IDP_AUTHENTICATE_EXCEPTION(PlatformConstants.PREFIX + "031", "Idp authenticate exception occured"), + MIMOTO_IDP_CONSENT_EXCEPTION(PlatformConstants.PREFIX + "032", "Idp consent exception occured"), + MIMOTO_IDP_OTP_EXCEPTION(PlatformConstants.PREFIX + "033", "IDP Otp error occured"), + MIMOTO_IDP_GENERIC_EXCEPTION(PlatformConstants.PREFIX + "034", "Could not get response from server"), + INVALID_ISSUER_ID_EXCEPTION(PlatformConstants.PREFIX + "035", "Invalid issuer ID"), + INVALID_ISSUER_ID_CONFIGURATION(PlatformConstants.PREFIX + "035", "Exception occurred while fetching issuer configuration"), + INVALID_CREDENTIAL_TYPE_EXCEPTION(PlatformConstants.PREFIX + "036", "Invalid Credential Type Id"), + MIMOTO_ISSUER_ONBOARDING_EXCEPTION(PlatformConstants.PREFIX + "037", "Issuers Onboarding exception occurred "), + INVALID_VERIFIER_ID_EXCEPTION(PlatformConstants.PREFIX + "038", "Invalid Verifier ClientId"), + INVALID_VERIFIER_REDIRECT_URI_EXCEPTION(PlatformConstants.PREFIX + "039", "Invalid Redirect Uri"), + INVALID_CREDENTIAL_RESOURCE_URI_EXCEPTION(PlatformConstants.PREFIX + "040", "The Credential Resource is Invalid"), + INVALID_CREDENTIAL_ISSUER_WELLKNOWN_RESPONSE_EXCEPTION(PlatformConstants.PREFIX + "041", "Invalid Wellknown from Issuer"), + INVALID_AUTHORIZATION_SERVER_WELLKNOWN_RESPONSE_EXCEPTION(PlatformConstants.PREFIX + "042", "Invalid Authorization Server well-known from server:"), + MIMOTO_FETCHING_TOKEN_EXCEPTION(PlatformConstants.PREFIX + "043", "Exception occurred when fetching the Token"); /** The error message. */ private final String errorMessage; @@ -112,4 +129,5 @@ public String getCode() { return this.errorCode; } -} \ No newline at end of file +} + diff --git a/src/main/java/io/mosip/mimoto/exception/UnauthorizedAccessException.java b/src/main/java/io/mosip/mimoto/exception/UnauthorizedAccessException.java new file mode 100644 index 000000000..fb4771c7b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/UnauthorizedAccessException.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.exception; + +import java.io.Serial; + +public class UnauthorizedAccessException extends BaseUncheckedException { + + @Serial + private static final long serialVersionUID = 1L; + + public UnauthorizedAccessException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/VCVerificationException.java b/src/main/java/io/mosip/mimoto/exception/VCVerificationException.java new file mode 100644 index 000000000..67f03ab8a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/VCVerificationException.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.exception; + +public class VCVerificationException extends BaseUncheckedException { + + private static final long serialVersionUID = -5350213197226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + public VCVerificationException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/VPErrorNotSentException.java b/src/main/java/io/mosip/mimoto/exception/VPErrorNotSentException.java new file mode 100644 index 000000000..cbbd8f87e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/VPErrorNotSentException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class VPErrorNotSentException extends BaseUncheckedException { + + private static final long serialVersionUID = -5350213197226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + public VPErrorNotSentException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } + + public VPErrorNotSentException(String errorMessage) { + super(ErrorConstants.REJECT_VERIFIER_EXCEPTION.getErrorCode(), errorMessage); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/exception/VPNotCreatedException.java b/src/main/java/io/mosip/mimoto/exception/VPNotCreatedException.java new file mode 100644 index 000000000..cc15e9548 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/VPNotCreatedException.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.exception; + +public class VPNotCreatedException extends BaseUncheckedException { + + private static final long serialVersionUID = -5350213197226295789L; + + /** + * Constructor with errorCode, and errorMessage + * + * @param errorCode The error code for this exception + * @param errorMessage The error message for this exception + */ + public VPNotCreatedException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } + + public VPNotCreatedException(String errorMessage) { + super(ErrorConstants.INVALID_REQUEST.getErrorCode(), errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/exception/WalletLockedException.java b/src/main/java/io/mosip/mimoto/exception/WalletLockedException.java new file mode 100644 index 000000000..04243231e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/exception/WalletLockedException.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.exception; + +import java.io.Serial; + +public class WalletLockedException extends BaseUncheckedException { + + @Serial + private static final long serialVersionUID = 1L; + + public WalletLockedException(String errorCode, String errorMessage) { + super(errorCode, errorMessage); + } +} diff --git a/src/main/java/io/mosip/mimoto/model/CredentialMetadata.java b/src/main/java/io/mosip/mimoto/model/CredentialMetadata.java new file mode 100644 index 000000000..4798ae4ce --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/CredentialMetadata.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.io.Serializable; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CredentialMetadata implements Serializable { + private String issuerId; + private String credentialType; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/model/KeyMetadata.java b/src/main/java/io/mosip/mimoto/model/KeyMetadata.java new file mode 100644 index 000000000..36821a73e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/KeyMetadata.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.model; + +import lombok.Data; + +@Data +public class KeyMetadata { + private String algorithmName; +} diff --git a/src/main/java/io/mosip/mimoto/model/PasscodeControl.java b/src/main/java/io/mosip/mimoto/model/PasscodeControl.java new file mode 100644 index 000000000..0232764b6 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/PasscodeControl.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.model; + +import lombok.Data; + +@Data +public class PasscodeControl { + private int failedAttemptCount = 0; + private int currentCycleCount = 0; + private Long retryBlockedUntil = null; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/model/ProofSigningKey.java b/src/main/java/io/mosip/mimoto/model/ProofSigningKey.java new file mode 100644 index 000000000..976b5a67f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/ProofSigningKey.java @@ -0,0 +1,49 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.security.PrivateKey; +import java.time.Instant; + +@Entity +@Table(name = "proof_signing_key") +@Getter +@Setter +public class ProofSigningKey { + + @Id + @Column(length = 36, updatable = false, nullable = false) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wallet_id", nullable = false) + private Wallet wallet; // Foreign key reference to the wallet table + + @Column(nullable = false) + private String publicKey; // Public key for wallet + + @Column(name = "secret_key", nullable = false) + private String encryptedSecretKey; // Secret key, encrypted using walletKey + + @Transient + private PrivateKey secretKey; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private KeyMetadata keyMetadata; // Metadata about the public and private keys + + @Column(nullable = false, updatable = false) + private Instant createdAt = Instant.now(); + + @Column(nullable = false) + private Instant updatedAt = Instant.now(); + + @PreUpdate + public void preUpdate() { + this.updatedAt = Instant.now(); + } +} diff --git a/src/main/java/io/mosip/mimoto/model/QRCodeType.java b/src/main/java/io/mosip/mimoto/model/QRCodeType.java new file mode 100644 index 000000000..9b23b2c6d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/QRCodeType.java @@ -0,0 +1,6 @@ +package io.mosip.mimoto.model; +public enum QRCodeType { + OnlineSharing, + EmbeddedVC, + None; +} diff --git a/src/main/java/io/mosip/mimoto/model/QualityScore.java b/src/main/java/io/mosip/mimoto/model/QualityScore.java deleted file mode 100644 index bede64451..000000000 --- a/src/main/java/io/mosip/mimoto/model/QualityScore.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.mosip.mimoto.model; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import lombok.Data; - -/** - * The Class QualityScore. - * - * @author Sanjay Murali - */ -@Data -public class QualityScore { - - /** The score - 0 - 100 score that represents quality as a percentage */ - private float score; - private List errors; - /** The analytics info - detailed breakdown and other information */ - private Map analyticsInfo; - - public QualityScore() { - this.score = 0; - this.errors = new ArrayList<>(); - this.analyticsInfo = new HashMap(); - } -} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/model/TrustedVerifier.java b/src/main/java/io/mosip/mimoto/model/TrustedVerifier.java new file mode 100644 index 000000000..0ab6c740c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/TrustedVerifier.java @@ -0,0 +1,34 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.Instant; + +@Entity +@Table(name = "trusted_verifiers") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TrustedVerifier { + + @Id + @Column(length = 36, updatable = false, nullable = false) + private String id; // Primary Key + + @Column(name = "wallet_id", nullable = false) + private String walletId; // Foreign key referencing wallets table + + @Column(name = "verifier_id", nullable = false, length = 255) + private String verifierId; // Storing the verifier client ID + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; // Time at which the user gave consent to accept the Verifier's request + + @PrePersist + public void prePersist() { + this.createdAt = Instant.now(); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/model/UserMetadata.java b/src/main/java/io/mosip/mimoto/model/UserMetadata.java new file mode 100644 index 000000000..959dbecc3 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/UserMetadata.java @@ -0,0 +1,46 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; + +@Entity +@Table(name = "user_metadata") +@Getter +@Setter +public class UserMetadata { + + @Id + @Column(length = 36, updatable = false, nullable = false) + private String id; + + @Column(name = "provider_subject_id", nullable = false) + private String providerSubjectId; + + @Column(name = "identity_provider", nullable = false) + private String identityProvider; + + @Column(name = "display_name", nullable = false, columnDefinition = "TEXT") + private String displayName; + + @Column(name = "profile_picture_url", columnDefinition = "TEXT") + private String profilePictureUrl; + + @Column(name = "phone_number") + private String phoneNumber; + + @Column(name = "email", nullable = false, columnDefinition = "TEXT") + private String email; + + @Column(name = "created_at", updatable = false) + private Instant createdAt; + + @Column(name = "updated_at") + private Instant updatedAt; +} + diff --git a/src/main/java/io/mosip/mimoto/model/VerifiableCredential.java b/src/main/java/io/mosip/mimoto/model/VerifiableCredential.java new file mode 100644 index 000000000..896a5118d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/VerifiableCredential.java @@ -0,0 +1,40 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import java.time.Instant; + +@Entity +@Table(name = "verifiable_credentials") +@Getter +@Setter +public class VerifiableCredential { + + @Id + @Column(length = 36, updatable = false, nullable = false) + private String id; + + @Column(name = "wallet_id", nullable = false) + private String walletId; + + @Column(nullable = false, columnDefinition = "TEXT") + private String credential; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(nullable = false, columnDefinition = "jsonb") + private CredentialMetadata credentialMetadata; + + @Column(nullable = false, updatable = false) + private Instant createdAt = Instant.now(); + + @Column(nullable = false) + private Instant updatedAt = Instant.now(); + + @PreUpdate + public void preUpdate() { + this.updatedAt = Instant.now(); + } +} diff --git a/src/main/java/io/mosip/mimoto/model/VerifiablePresentation.java b/src/main/java/io/mosip/mimoto/model/VerifiablePresentation.java new file mode 100644 index 000000000..ba8712756 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/VerifiablePresentation.java @@ -0,0 +1,55 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDateTime; + +/** + * Entity class for verifiable_presentations table + */ +@Entity +@Table(name = "verifiable_presentations") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VerifiablePresentation { + + @Id + @Column(name = "id", length = 255) + private String id; + + @Column(name = "wallet_id", length = 255, nullable = false) + private String walletId; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "auth_request", columnDefinition = "jsonb") + private String authRequest; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "presentation_data", columnDefinition = "jsonb") + private String presentationData; + + @Column(name = "verifier_id", length = 255) + private String verifierId; + + @Column(name = "status", length = 50, nullable = false) + private String status; + + @Column(name = "requested_at") + private LocalDateTime requestedAt; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "consent", nullable = false) + private Boolean consent; +} diff --git a/src/main/java/io/mosip/mimoto/model/Wallet.java b/src/main/java/io/mosip/mimoto/model/Wallet.java new file mode 100644 index 000000000..67002b982 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/Wallet.java @@ -0,0 +1,59 @@ +package io.mosip.mimoto.model; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.Instant; +import java.util.List; + +@Entity +@Table(name = "wallet") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Wallet { + + @Id + @Column(length = 36, updatable = false, nullable = false) + private String id; + + @Column(nullable = false) + private String userId; // Foreign key reference to user_metadata + + @Column(nullable = false) + private String walletKey; // Encrypted wallet key using AES256-GCM + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private WalletMetadata walletMetadata; // Metadata about the wallet, including encryption info + + @OneToMany(mappedBy = "wallet", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List proofSigningKeys; // One wallet can have many keys + + @Column(nullable = false, updatable = false) + private Instant createdAt; + + @Column(nullable = false) + private Instant updatedAt; + + @PrePersist + public void prePersist() { + Instant now = Instant.now(); + this.createdAt = now; + this.updatedAt = now; + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = Instant.now(); + } +} + diff --git a/src/main/java/io/mosip/mimoto/model/WalletLockStatus.java b/src/main/java/io/mosip/mimoto/model/WalletLockStatus.java new file mode 100644 index 000000000..4f5823f62 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/WalletLockStatus.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum WalletLockStatus { + // Indicates that the wallet is temporarily locked due to exceeding the maximum allowed failed attempts within a cycle. Unlocking will be possible after the lock duration expires. + TEMPORARILY_LOCKED("temporarily_locked"), + // Indicates that the wallet is permanently locked after exceeding the maximum allowed lock cycles. Unlocking the Wallet is no longer possible. + PERMANENTLY_LOCKED("permanently_locked"), + // Indicates that the wallet is in the last attempt before being permanently locked. This serves as a warning state. + LAST_ATTEMPT_BEFORE_LOCKOUT("last_attempt_before_lockout"), + // Indicates that the temporary lock has expired, and the wallet can now be accessed again. + LOCK_EXPIRED("lock_expired"); + + private final String value; + + WalletLockStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/io/mosip/mimoto/model/WalletMetadata.java b/src/main/java/io/mosip/mimoto/model/WalletMetadata.java new file mode 100644 index 000000000..7186bfc7c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/model/WalletMetadata.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.model; + +import lombok.Data; + +@Data +public class WalletMetadata { + private String encryptionAlgo; + private String encryptionType; + private String name; + private PasscodeControl passcodeControl; + private WalletLockStatus lockStatus = null; +} diff --git a/src/main/java/io/mosip/mimoto/repository/ProofSigningKeyRepository.java b/src/main/java/io/mosip/mimoto/repository/ProofSigningKeyRepository.java new file mode 100644 index 000000000..394ce7f27 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/ProofSigningKeyRepository.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.repository; + +import io.mosip.mimoto.model.ProofSigningKey; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface ProofSigningKeyRepository extends JpaRepository { + @Query(value = "SELECT * FROM proof_signing_key p WHERE p.wallet_id = :walletId AND p.key_metadata->>'algorithmName' = :algorithmName", nativeQuery = true) + Optional findByWalletIdAndAlgorithm(@Param("walletId") String walletId, @Param("algorithmName") String algorithmName); + + Optional findByWalletId(String walletId); +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/repository/UserMetadataRepository.java b/src/main/java/io/mosip/mimoto/repository/UserMetadataRepository.java new file mode 100644 index 000000000..08d961354 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/UserMetadataRepository.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.repository; + +import io.mosip.mimoto.model.UserMetadata; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface UserMetadataRepository extends JpaRepository { + Optional findByProviderSubjectIdAndIdentityProvider(String providerSubjectId, String identityProvider); +} diff --git a/src/main/java/io/mosip/mimoto/repository/VerifiablePresentationsRepository.java b/src/main/java/io/mosip/mimoto/repository/VerifiablePresentationsRepository.java new file mode 100644 index 000000000..655959cdc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/VerifiablePresentationsRepository.java @@ -0,0 +1,14 @@ +package io.mosip.mimoto.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import io.mosip.mimoto.model.VerifiablePresentation; + +/** + * Repository interface for VerifiablePresentation entity + */ +@Repository +public interface VerifiablePresentationsRepository extends JpaRepository { + +} diff --git a/src/main/java/io/mosip/mimoto/repository/VerifierRepository.java b/src/main/java/io/mosip/mimoto/repository/VerifierRepository.java new file mode 100644 index 000000000..c6a43b699 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/VerifierRepository.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.repository; + +import io.mosip.mimoto.model.TrustedVerifier; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VerifierRepository extends JpaRepository { + boolean existsByWalletIdAndVerifierId(String walletId, String verifierClientId); +} diff --git a/src/main/java/io/mosip/mimoto/repository/WalletCredentialsRepository.java b/src/main/java/io/mosip/mimoto/repository/WalletCredentialsRepository.java new file mode 100644 index 000000000..ea69f69ec --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/WalletCredentialsRepository.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.repository; + +import io.mosip.mimoto.model.VerifiableCredential; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + +@Repository +public interface WalletCredentialsRepository extends JpaRepository { + Optional findByIdAndWalletId(String id, String walletId); + + @Query(value = "SELECT EXISTS (SELECT 1 FROM verifiable_credentials WHERE credential_metadata->>'issuerId' = :issuerId AND credential_metadata->>'credentialType' = :credentialType AND wallet_id = :walletId)", nativeQuery = true) + boolean existsByIssuerIdAndCredentialTypeAndWalletId(@Param("issuerId") String issuerId, @Param("credentialType") String credentialType, @Param("walletId") String walletId); + + List findByWalletIdOrderByCreatedAtDesc(String walletId); +} diff --git a/src/main/java/io/mosip/mimoto/repository/WalletRepository.java b/src/main/java/io/mosip/mimoto/repository/WalletRepository.java new file mode 100644 index 000000000..c1f847671 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/repository/WalletRepository.java @@ -0,0 +1,16 @@ +package io.mosip.mimoto.repository; + +import io.mosip.mimoto.model.Wallet; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface WalletRepository extends JpaRepository { + @Query("SELECT w FROM Wallet w WHERE w.userId = :userId ORDER BY w.createdAt ASC") + List findWalletByUserId(@Param("userId") String userId); + + Optional findByUserIdAndId(String userId, String walletId); +} diff --git a/src/main/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserService.java b/src/main/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserService.java new file mode 100644 index 000000000..6f72365fb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,125 @@ +package io.mosip.mimoto.security.oauth2; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.config.oauth2.OAuth2ProviderProperties; +import io.mosip.mimoto.dto.ProviderDataConfig; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.service.UserMetadataService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * Custom implementation of the {@link DefaultOAuth2UserService} to handle + * user information retrieval and processing for OAuth2 authentication. + * + * This service customizes the loading of user details from an OAuth2 provider + * by enriching the user attributes, updating user metadata, and handling + * provider-specific configurations. + */ +@Service +@Slf4j +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final Map providers; + private final UserMetadataService userMetadataService; + + public CustomOAuth2UserService(OAuth2ProviderProperties providerProperties, + UserMetadataService userMetadataService) { + this.providers = providerProperties.getProvider(); + this.userMetadataService = userMetadataService; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + OAuth2User oAuth2User = loadFromProvider(userRequest); + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + ProviderDataConfig provider = getProviderConfig(registrationId); + + Map attributes = extractAttributes(oAuth2User, provider); + String userId = updateUserMetadata(attributes, provider, registrationId); + enrichAttributes(attributes, userId); + + String userNameAttribute = StringUtils.defaultIfBlank(provider.getUserNameAttribute(), "sub"); + return new DefaultOAuth2User(oAuth2User.getAuthorities(), attributes, userNameAttribute); + } + + private ProviderDataConfig getProviderConfig(String registrationId) { + ProviderDataConfig provider = providers.get(registrationId); + if (provider == null) { + log.error("Missing OAuth2 provider config for: {}", registrationId); + throw new IllegalArgumentException("Missing OAuth2 provider config for: " + registrationId); + } + return provider; + } + + private Map extractAttributes(OAuth2User oAuth2User, ProviderDataConfig provider) { + Map attributes = new HashMap<>(oAuth2User.getAttributes()); + attributes.put("name", safeGet(attributes, provider.getNameAttribute())); + attributes.put("email", safeGet(attributes, provider.getEmailAttribute())); + attributes.put("phone", safeGet(attributes, provider.getPhoneNumberAttribute())); + attributes.put("picture", extractProfilePicture(attributes, provider.getPictureAttribute())); + return attributes; + } + + private String extractProfilePicture(Map attributes, String pictureAttribute) { + if (StringUtils.isBlank(pictureAttribute) || !attributes.containsKey(pictureAttribute)) { + return null; + } + + Object picture = attributes.get(pictureAttribute); + if (picture instanceof Map picMap) { + Object data = picMap.get("data"); + if (data instanceof Map dataMap) { + Object url = dataMap.get("url"); + return url != null ? url.toString() : null; + } + } else if (picture instanceof String picUrl) { + return picUrl; + } + + return null; + } + + private String updateUserMetadata(Map attributes, ProviderDataConfig provider, String registrationId) { + String providerSubjectId = safeGet(attributes, StringUtils.defaultIfBlank(provider.getUserNameAttribute(), "sub")); + try { + return userMetadataService.updateOrCreateUserMetadata( + providerSubjectId, + registrationId, + safeGet(attributes, "name"), + safeGet(attributes, "picture"), + safeGet(attributes, "email") + ); + } catch (DecryptionException e) { + log.error("Error updating user metadata", e); + throw new RuntimeException("Metadata update failed", e); + } + } + + private void enrichAttributes(Map attributes, String userId) { + attributes.put(SessionKeys.USER_ID, userId); + } + + private String safeGet(Map attributes, String key) { + if (StringUtils.isBlank(key)) { + return null; + } + Object value = attributes.get(key); + if (value == null) { + return null; + } + return value.toString(); + } + + protected OAuth2User loadFromProvider(OAuth2UserRequest userRequest) { + return super.loadUser(userRequest); + } +} diff --git a/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationFailureHandler.java b/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationFailureHandler.java new file mode 100644 index 000000000..15cd5e61c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationFailureHandler.java @@ -0,0 +1,49 @@ +package io.mosip.mimoto.security.oauth2; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Component +@Slf4j +public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Value("${mosip.inji.web.url}") + private String injiWebUrl; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) + throws IOException { + String errorMessage = ""; + + if (exception != null) { + if (exception instanceof OAuth2AuthenticationException) { + Throwable cause = exception.getCause(); + if (cause instanceof java.net.SocketTimeoutException) { + errorMessage = "Timeout while connecting to the IDP for authorization"; + } else if (cause instanceof java.net.ConnectException) { + errorMessage = "Could not connect to the IDP for authorization"; + } else if (exception.getMessage().contains("access_denied")) { + errorMessage = "Consent was denied to share the details with the application. Please give consent and try again"; + } else { + errorMessage = "Login is failed due to : " + exception.getMessage(); + } + } else { + errorMessage = "Login is failed due to : " + exception.getMessage(); + } + } + + String encodedErrorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); + String redirectUrl = injiWebUrl + "/?status=error&error_message=" + encodedErrorMessage; + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandler.java b/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 000000000..959e1ba96 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,55 @@ +package io.mosip.mimoto.security.oauth2; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Slf4j +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final String authenticationSuccessRedirectUrl; + + public OAuth2AuthenticationSuccessHandler(@Value("${mosip.inji.web.authentication.success.redirect.url}") String authenticationSuccessRedirectUrl) { + this.authenticationSuccessRedirectUrl = authenticationSuccessRedirectUrl; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication; + OAuth2User oAuth2User = oauth2Token.getPrincipal(); + + HttpSession session = request.getSession(false); + if (session == null) { + log.error("Session not available"); + throw new ServletException("Session not available"); + } + + String clientRegistrationId = oauth2Token.getAuthorizedClientRegistrationId(); + session.setAttribute("clientRegistrationId", clientRegistrationId); + + // Add user info to session for UI + String displayName = oAuth2User.getAttribute("name"); + String profilePictureUrl = oAuth2User.getAttribute("picture"); + String email = oAuth2User.getAttribute("email"); + + session.setAttribute(SessionKeys.USER_METADATA, new UserMetadataDTO(displayName, profilePictureUrl, email, null)); + + String userId = oAuth2User.getAttribute("userId"); + session.setAttribute(SessionKeys.USER_ID, userId); + + response.sendRedirect(authenticationSuccessRedirectUrl); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/CbeffContainerI.java b/src/main/java/io/mosip/mimoto/service/CbeffContainerI.java deleted file mode 100644 index ebc1f045e..000000000 --- a/src/main/java/io/mosip/mimoto/service/CbeffContainerI.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.mosip.mimoto.service; - -import java.util.List; - -/** - * @author Ramadurai Pandian - * - * Container Interface to be used for create and update - * - */ -public abstract class CbeffContainerI { - - public abstract U createBIRType(List bir) throws Exception; - - public abstract U updateBIRType(List bir, byte[] fileBytes) throws Exception; - - public abstract boolean validateXML(byte[] fileBytes, byte[] xsdBytes) throws Exception; - -} diff --git a/src/main/java/io/mosip/mimoto/service/CredentialFormatHandler.java b/src/main/java/io/mosip/mimoto/service/CredentialFormatHandler.java new file mode 100644 index 000000000..cc20ec807 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialFormatHandler.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.mimoto.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +public interface CredentialFormatHandler { + + /** + * Get the supported format for this processor + */ + String getSupportedFormat(); + + /** + * Extract credential subject properties from VC response + */ + Map extractCredentialClaims(VCCredentialResponse vcCredentialResponse); + + /** + * Load display properties from well-known configuration + */ + LinkedHashMap> loadDisplayPropertiesFromWellknown( + Map credentialProperties, + CredentialsSupportedResponse credentialsSupportedResponse, + String userLocale); + + /** + * Extract all properties from the Credential + */ + Map extractAllCredentialProperties(VCCredentialResponse vcCredentialResponse); +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/CredentialFormatHandlerFactory.java b/src/main/java/io/mosip/mimoto/service/CredentialFormatHandlerFactory.java new file mode 100644 index 000000000..ad8bc722c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialFormatHandlerFactory.java @@ -0,0 +1,30 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.CredentialFormat; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class CredentialFormatHandlerFactory { + + private final Map handlers; + + @Autowired + public CredentialFormatHandlerFactory(Map handlers) { + this.handlers = handlers; + } + + public CredentialFormatHandler getHandler(String format) { + CredentialFormatHandler processor = handlers.get(format); + if (processor == null) { + throw new IllegalArgumentException("Unsupported credential format: " + format); + } + + return processor; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/CredentialMatchingService.java b/src/main/java/io/mosip/mimoto/service/CredentialMatchingService.java new file mode 100644 index 000000000..210955151 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialMatchingService.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.service; + + +import io.mosip.mimoto.dto.MatchingCredentialsDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; + +import java.io.IOException; + +public interface CredentialMatchingService { + MatchingCredentialsDTO getMatchingCredentials(VerifiablePresentationSessionData sessionData, String walletId, String base64Key) throws ApiNotAccessibleException, IOException; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/CredentialPDFGeneratorService.java b/src/main/java/io/mosip/mimoto/service/CredentialPDFGeneratorService.java new file mode 100644 index 000000000..062536542 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialPDFGeneratorService.java @@ -0,0 +1,424 @@ +package io.mosip.mimoto.service; + +import com.authlete.sd.Disclosure; +import com.authlete.sd.SDJWT; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.itextpdf.html2pdf.ConverterProperties; +import com.itextpdf.html2pdf.HtmlConverter; +import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.nimbusds.jose.util.Base64URL; +import io.mosip.injivcrenderer.InjiVcRenderer; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.constant.LdpVcV1Constants; +import io.mosip.mimoto.constant.LdpVcV2Constants; +import io.mosip.mimoto.constant.SdJwtVcConstants; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.openid.presentation.PresentationDefinitionDTO; +import io.mosip.mimoto.model.QRCodeType; +import io.mosip.mimoto.service.impl.PresentationServiceImpl; +import io.mosip.mimoto.util.LocaleUtils; +import io.mosip.mimoto.util.SvgFixerUtil; +import io.mosip.mimoto.util.Utilities; +import io.mosip.pixelpass.PixelPass; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +@Service +public class CredentialPDFGeneratorService { + + private record SelectedFace(String key, String face) {} + + private final ObjectMapper objectMapper; + + private final PresentationServiceImpl presentationService; + + private final Utilities utilities; + + private final PixelPass pixelPass; + + private final CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + private final InjiVcRenderer injiVcRenderer; + + private final SvgFixerUtil svgFixerUtil; + + public CredentialPDFGeneratorService(ObjectMapper objectMapper, PresentationServiceImpl presentationService, Utilities utilities, PixelPass pixelPass, CredentialFormatHandlerFactory credentialFormatHandlerFactory, InjiVcRenderer injiVcRenderer, SvgFixerUtil svgFixerUtil) { + this.objectMapper = objectMapper; + this.presentationService = presentationService; + this.utilities = utilities; + this.pixelPass = pixelPass; + this.credentialFormatHandlerFactory = credentialFormatHandlerFactory; + this.injiVcRenderer = injiVcRenderer; + this.svgFixerUtil = svgFixerUtil; + } + + @Value("${mosip.inji.ovp.qrdata.pattern}") + private String ovpQRDataPattern; + + @Value("${mosip.inji.qr.code.height:500}") + Integer qrCodeHeight; + + @Value("${mosip.inji.qr.code.width:500}") + Integer qrCodeWidth; + + @Value("${mosip.inji.qr.data.size.limit:4096}") + Integer allowedQRDataSizeLimit; + + @Value("${mosip.injiweb.vc.subject.face.keys.order:image,face,photo,picture,portrait}") + private String faceImageLookupKeys; + + @Value("${mosip.injiweb.mask.disclosures:true}") + private boolean maskDisclosures; + + private static final String CLAIM_169_KEY = "claim169"; + + public ByteArrayInputStream generatePdfForVerifiableCredential(String credentialConfigurationId, VCCredentialResponse vcCredentialResponse, IssuerDTO issuerDTO, CredentialsSupportedResponse credentialsSupportedResponse, String dataShareUrl, String credentialValidity, String locale) throws Exception { + // Check if the credential can support SVG based rendering + if (isSvgBasedRenderingSupported(vcCredentialResponse)) { + log.info("Detected LDP VC v2 credential with svg template, using InjiVcRenderer for PDF generation"); + return generatePdfUsingSvgTemplate(vcCredentialResponse, issuerDTO, dataShareUrl); + } else { + log.info("Using v1 data model flow for credential"); + // Get the appropriate processor based on format + CredentialFormatHandler processor = credentialFormatHandlerFactory.getHandler(vcCredentialResponse.getFormat()); + + // Extract credential properties using the specific processor + Map credentialProperties = processor.extractCredentialClaims(vcCredentialResponse); + + // Load display properties using the specific processor + LinkedHashMap> displayProperties = + processor.loadDisplayPropertiesFromWellknown(credentialProperties, credentialsSupportedResponse, locale); + + Map data = getPdfResourceFromVcProperties(displayProperties, credentialsSupportedResponse, + vcCredentialResponse, issuerDTO, dataShareUrl, credentialValidity, locale); + + return renderVCInCredentialTemplate(data, issuerDTO.getIssuer_id(), credentialConfigurationId); + } + } + + private Map getPdfResourceFromVcProperties( + LinkedHashMap> displayProperties, + CredentialsSupportedResponse credentialsSupportedResponse, + VCCredentialResponse vcCredentialResponse, + IssuerDTO issuerDTO, + String dataShareUrl, + String credentialValidity, + String userLocale) throws IOException, WriterException { + + Map data = new HashMap<>(); + LinkedHashMap rowProperties = new LinkedHashMap<>(); + + CredentialSupportedDisplayResponse resolvedDisplay = + Optional.ofNullable(credentialsSupportedResponse.getDisplay()) + .filter(list -> !list.isEmpty()) + .map(list -> list.stream() + .filter(d -> LocaleUtils.matchesLocale(d.getLocale(), userLocale)) + .findFirst() + .orElseGet(list::getFirst)) + .orElse(null); + + String backgroundColor = resolvedDisplay != null ? resolvedDisplay.getBackgroundColor() : null; + String backgroundImage = resolvedDisplay != null && resolvedDisplay.getBackgroundImage() != null + ? resolvedDisplay.getBackgroundImage().getUri() + : null; + String textColor = resolvedDisplay != null ? resolvedDisplay.getTextColor() : null; + String credentialSupportedType = resolvedDisplay != null ? resolvedDisplay.getName() : null; + + SelectedFace selectedFace = extractFace(vcCredentialResponse); + String face = selectedFace.face(); + String selectedFaceKey = selectedFace.key(); + + Set disclosures; + if (CredentialFormat.VC_SD_JWT.getFormat().equals(vcCredentialResponse.getFormat())) { + SDJWT sdjwt = SDJWT.parse((String) vcCredentialResponse.getCredential()); + disclosures = sdjwt.getDisclosures().stream() + .map(Disclosure::getClaimName) + .collect(Collectors.toSet()); + } else { + disclosures = new LinkedHashSet<>(); + } + + LinkedHashMap disclosuresProps = new LinkedHashMap<>(); + displayProperties.forEach((key, valueMap) -> { + boolean isFaceKey = selectedFaceKey != null && key.trim().equals(selectedFaceKey); + + valueMap.forEach((display, val) -> { + String displayName = display.getName(); + String locale = display.getLocale(); + String strVal = formatValue(val, locale); + if (disclosures.contains(key)) { + disclosuresProps.put(key, displayName); + if (maskDisclosures) { + strVal = Utilities.maskValue(strVal); + } + } + if (!isFaceKey && displayName != null) { + rowProperties.put(key, Map.of(displayName, strVal)); + } + }); + }); + + String qrCodeImage = ""; + if (QRCodeType.OnlineSharing.equals(issuerDTO.getQr_code_type())) { + qrCodeImage = constructQRCodeWithAuthorizeRequest(vcCredentialResponse, dataShareUrl); + } else if (QRCodeType.EmbeddedVC.equals(issuerDTO.getQr_code_type())) { + String claim169Qr = extractClaim169Qr(vcCredentialResponse); + if(!claim169Qr.isEmpty()) { + qrCodeImage = constructQRCode(claim169Qr); + } + else { + qrCodeImage = constructQRCodeWithVCData(vcCredentialResponse); + } + } + + // is sd-jwt and has disclosures + boolean isSdJwtWithDisclosures = CredentialFormat.VC_SD_JWT.getFormat().equals(vcCredentialResponse.getFormat()) && CollectionUtils.isNotEmpty(disclosures); + + data.put("isMaskedOn", maskDisclosures); + data.put("isSdJwtWithDisclosures", isSdJwtWithDisclosures); + data.put("qrCodeImage", qrCodeImage); + data.put("credentialValidity", credentialValidity); + data.put("logoUrl", issuerDTO.getDisplay().stream().map(d -> d.getLogo().getUrl()).findFirst().orElse("")); + data.put("rowProperties", rowProperties); + data.put("disclosures", disclosuresProps); + data.put("textColor", textColor); + data.put("backgroundColor", backgroundColor); + data.put("backgroundImage", backgroundImage); + data.put("titleName", credentialSupportedType); + data.put("face", face); + return data; + } + + private String extractClaim169Qr(VCCredentialResponse vcCredentialResponse) { + CredentialFormatHandler credentialFormatHandler = credentialFormatHandlerFactory.getHandler(vcCredentialResponse.getFormat()); + Map credentialSubject = credentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + Object claim169QrObj = credentialSubject.get(CLAIM_169_KEY); + if (claim169QrObj instanceof Map claim169Map) { + if (!claim169Map.isEmpty()) { + Object firstValue = claim169Map.values().iterator().next(); + return firstValue != null ? firstValue.toString() : ""; + } + return ""; + } + return ""; + } + + private SelectedFace extractFace(VCCredentialResponse vcCredentialResponse) { + // Use the appropriate credentialFormatHandler to extract credential properties + CredentialFormatHandler credentialFormatHandler = credentialFormatHandlerFactory.getHandler(vcCredentialResponse.getFormat()); + Map credentialSubject = credentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // handling face extraction based on configured keys + List faceKeys = Arrays.asList(faceImageLookupKeys.split(",")); + for (String faceKey : faceKeys) { + String trimmedKey = faceKey.trim(); + Object faceValue = credentialSubject.get(trimmedKey); + if (faceValue != null && !faceValue.toString().isEmpty()) { + log.debug("Found face data using key: '{}'", trimmedKey); + // Return the trimmedKey directly + return new SelectedFace(trimmedKey, faceValue.toString()); + } + } + return new SelectedFace(null, null); + } + + private String formatValue(Object val, String locale) { + if (val instanceof Map) { + return Optional.ofNullable(Stream.of( + ((Map) val).get(LdpVcV2Constants.VALUE), + ((Map) val).get(LdpVcV1Constants.VALUE), + ((Map) val).get(SdJwtVcConstants.VALUE) + ).filter(Objects::nonNull) + .findFirst() + .orElse(null)) + .map(Object::toString) + .orElse(""); + } else if (val instanceof List) { + List list = (List) val; + if (list.isEmpty()) return ""; + if (list.getFirst() instanceof String) { + return String.join(", ", (List) list); + } else if (list.getFirst() instanceof Map) { + return list.stream() + .filter(Objects::nonNull) + .map(item -> (Map) item) + .filter(m -> { + Object lang = Stream.of( + m.get(LdpVcV2Constants.LANGUAGE), + m.get(LdpVcV1Constants.LANGUAGE), + m.get(SdJwtVcConstants.LANGUAGE) + ).filter(Objects::nonNull) + .findFirst() + .orElse(null); + + return lang != null && LocaleUtils.matchesLocale(lang.toString(), locale); + }) + .map(m -> { + Object value = Stream.of( + m.get(LdpVcV2Constants.VALUE), + m.get(LdpVcV1Constants.VALUE), + m.get(SdJwtVcConstants.VALUE) + ).filter(Objects::nonNull) + .findFirst() + .orElse(null); + + return value != null ? value.toString() : null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElse(""); + } + } + return val != null ? val.toString() : ""; + } + + private ByteArrayInputStream renderVCInCredentialTemplate(Map data, String issuerId, String credentialConfigurationId) { + String credentialTemplate = utilities.getCredentialSupportedTemplateString(issuerId, credentialConfigurationId); + Properties props = new Properties(); + props.setProperty("resource.loader", "class"); + props.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + Velocity.init(props); + VelocityContext velocityContext = new VelocityContext(data); + + StringWriter writer = new StringWriter(); + Velocity.evaluate(velocityContext, writer, "Credential Template", credentialTemplate); + + String mergedHtml = writer.toString(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + PdfWriter pdfwriter = new PdfWriter(outputStream); + DefaultFontProvider defaultFont = new DefaultFontProvider(true, false, false); + ConverterProperties converterProperties = new ConverterProperties(); + converterProperties.setFontProvider(defaultFont); + HtmlConverter.convertToPdf(mergedHtml, pdfwriter, converterProperties); + return new ByteArrayInputStream(outputStream.toByteArray()); + } + + private String constructQRCodeWithVCData(VCCredentialResponse vcCredentialResponse) throws JsonProcessingException, WriterException { + String qrData = pixelPass.generateQRData(objectMapper.writeValueAsString(vcCredentialResponse.getCredential()), ""); + if (allowedQRDataSizeLimit > qrData.length()) { + return constructQRCode(qrData); + } + return ""; + } + + private String constructQRCodeWithAuthorizeRequest(VCCredentialResponse vcCredentialResponse, String dataShareUrl) throws WriterException, JsonProcessingException { + PresentationDefinitionDTO presentationDefinitionDTO = presentationService.constructPresentationDefinition(vcCredentialResponse); + String presentationString = objectMapper.writeValueAsString(presentationDefinitionDTO); + String qrData = String.format(ovpQRDataPattern, URLEncoder.encode(dataShareUrl, StandardCharsets.UTF_8), URLEncoder.encode(presentationString, StandardCharsets.UTF_8)); + return constructQRCode(qrData); + } + + private String constructQRCode(String qrData) throws WriterException { + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + BitMatrix bitMatrix = qrCodeWriter.encode(qrData, BarcodeFormat.QR_CODE, qrCodeWidth, qrCodeHeight); + BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(bitMatrix); + return Utilities.encodeToString(qrImage, "png"); + } + + private boolean isSvgBasedRenderingSupported(VCCredentialResponse vcCredentialResponse) { + if (!CredentialFormat.LDP_VC.getFormat().equals(vcCredentialResponse.getFormat())) { + return false; + } + + Object credentialPayload = vcCredentialResponse.getCredential(); + if (!(credentialPayload instanceof Map)) return false; + + @SuppressWarnings("unchecked") + Map credentialPayloadMap = (Map) credentialPayload; + + return containsV2Context(credentialPayloadMap) + && containsSvgTemplate(credentialPayloadMap); + } + + private boolean containsV2Context(Map credentialPayloadMap) { + Object contextField = credentialPayloadMap.get(LdpVcV2Constants.CONTEXT); + + if (contextField instanceof List contextList) + return contextList.stream().anyMatch(entry -> LdpVcV2Constants.V2_CONTEXT_URL.equals(entry.toString())); + + return false; + } + + private boolean containsSvgTemplate(Map credentialPayloadMap) { + Object renderMethod = credentialPayloadMap.get(LdpVcV2Constants.RENDER_METHOD); + if (renderMethod instanceof List renderMethodList) { + return renderMethodList.stream().allMatch(entry -> { + if (!(entry instanceof Map renderMethodProperties)) return false; + + String renderSuite = (String) renderMethodProperties.get(LdpVcV2Constants.RENDER_SUITE); + return renderMethodProperties.containsKey(LdpVcV2Constants.TEMPLATE) && LdpVcV2Constants.SVG_MUSTACHE_RENDER_SUITE.equals(renderSuite); + }); + } + if (renderMethod instanceof Map renderMethodMap) { + String renderSuite = (String) renderMethodMap.get(LdpVcV2Constants.RENDER_SUITE); + return renderMethodMap.containsKey(LdpVcV2Constants.TEMPLATE) && LdpVcV2Constants.SVG_MUSTACHE_RENDER_SUITE.equals(renderSuite); + } + + return false; + } + + private ByteArrayInputStream generatePdfUsingSvgTemplate(VCCredentialResponse vcCredentialResponse, IssuerDTO issuerDTO, String dataShareUrl) throws Exception { + try { + // Get the ldp_vc credential and convert to string + String credentialJsonString = objectMapper.writeValueAsString(vcCredentialResponse.getCredential()); + + // Generate the QR code data to embed into the svg for Online Sharing + String qrCodeData = null; + if (QRCodeType.OnlineSharing.equals(issuerDTO.getQr_code_type())) { + PresentationDefinitionDTO presentationDefinitionDTO = presentationService.constructPresentationDefinition(vcCredentialResponse); + String presentationString = objectMapper.writeValueAsString(presentationDefinitionDTO); + qrCodeData = String.format(ovpQRDataPattern, URLEncoder.encode(dataShareUrl, StandardCharsets.UTF_8), URLEncoder.encode(presentationString, StandardCharsets.UTF_8)); + } + + // Generate list of rendered svg strings using InjiVcRenderer + List generatedSvgObjects = injiVcRenderer.generateCredentialDisplayContent( + io.mosip.injivcrenderer.constants.CredentialFormat.LDP_VC, null, credentialJsonString, qrCodeData); + + if (generatedSvgObjects.isEmpty()) { + throw new Exception("No SVG content generated for v2 credential"); + } + + List svgStrings = generatedSvgObjects.stream() + .map(Object::toString) + .map(svgFixerUtil::addMissingOffsetToStopElements) + .toList(); + + log.debug("Fixed {} SVG elements for PDF conversion", svgStrings.size()); + + String base64PdfContent = injiVcRenderer.convertSvgToPdf(svgStrings); + byte[] decodedPdfBytes = Base64URL.from(base64PdfContent).decode(); + return new ByteArrayInputStream(decodedPdfBytes); + } catch (Exception e) { + log.error("Error generating PDF for v2 credential using InjiVcRenderer: {}", e.getMessage(), e); + throw new Exception("Failed to generate PDF for v2 credential: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/service/CredentialService.java b/src/main/java/io/mosip/mimoto/service/CredentialService.java new file mode 100644 index 000000000..47ff0f14b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialService.java @@ -0,0 +1,44 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.exception.*; + +import java.io.ByteArrayInputStream; + +public interface CredentialService { + + /** + * Downloads credential as PDF. + * + * @param issuerId The issuer ID + * @param credentialType The credential type + * @param response The token response + * @param credentialValidity The credential validity + * @param locale The locale + * @return ByteArrayInputStream containing the PDF + * @throws Exception If any error occurs during processing + */ + ByteArrayInputStream downloadCredentialAsPDF(String issuerId, String credentialType, TokenResponseDTO response, String credentialValidity, String locale) throws Exception; + + /** + * Downloads credential and stores it in the database. + * + * @param tokenResponse The token response containing the access token + * @param credentialConfigurationId The type of the credential + * @param walletId The ID of the wallet + * @param base64Key The Base64-encoded wallet key + * @param issuerId The ID of the issuer + * @param locale The locale for the response + * @return The stored VerifiableCredential response + * @throws InvalidRequestException If input parameters are invalid + * @throws CredentialProcessingException If processing fails + * @throws ExternalServiceUnavailableException If an external service is unavailable + * @throws VCVerificationException If credential verification fails + * @throws InvalidCredentialResourceException If the credential resource is invalid + */ + VerifiableCredentialResponseDTO downloadCredentialAndStoreInDB( + TokenResponseDTO tokenResponse, String credentialConfigurationId, String walletId, + String base64Key, String issuerId, String locale) + throws InvalidRequestException, CredentialProcessingException, ExternalServiceUnavailableException, VCVerificationException, InvalidCredentialResourceException; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/CredentialVerifierService.java b/src/main/java/io/mosip/mimoto/service/CredentialVerifierService.java new file mode 100644 index 000000000..62381800b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/CredentialVerifierService.java @@ -0,0 +1,9 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.exception.VCVerificationException; + +public interface CredentialVerifierService { + boolean verify(VCCredentialResponse credentialResponse) throws JsonProcessingException, VCVerificationException; +} diff --git a/src/main/java/io/mosip/mimoto/service/DataProtectionService.java b/src/main/java/io/mosip/mimoto/service/DataProtectionService.java new file mode 100644 index 000000000..8cf699563 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/DataProtectionService.java @@ -0,0 +1,365 @@ +package io.mosip.mimoto.service; + +import io.mosip.kernel.core.util.CryptoUtil; +import io.mosip.kernel.cryptomanager.dto.CryptomanagerRequestDto; +import io.mosip.kernel.cryptomanager.dto.CryptomanagerResponseDto; +import io.mosip.kernel.cryptomanager.service.CryptomanagerService; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.openID4VP.common.DecoderKt; +import io.mosip.openID4VP.common.EncoderKt; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +import static io.mosip.mimoto.exception.ErrorConstants.DECRYPTION_FAILED; +import static io.mosip.mimoto.exception.ErrorConstants.ENCRYPTION_FAILED; + +/** + * Utility class for encryption and decryption operations. + */ +@Slf4j +@Service +public class DataProtectionService { + private static final String AES_ALGORITHM = "AES/GCM/NoPadding"; + private static final int NONCE_LENGTH = 12; // Recommended nonce length for GCM + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final int TAG_LENGTH = 128; // Authentication tag length in bits (16 bytes) + + private final CryptomanagerService cryptomanagerService; + + private final String appId; + + public DataProtectionService(CryptomanagerService cryptomanagerService, + @Value("${mosip.inji.app.id:MIMOTO}") String appId) { + this.cryptomanagerService = cryptomanagerService; + this.appId = appId; + } + + /** + * Encrypts data using the provided reference ID, AAD, and salt. + * + * @param data The data to encrypt. + * @param refId The reference ID. + * @param aad The additional authentication data. + * @param salt The salt for encryption. + * @return The encrypted data. + */ + public String encrypt(String data, String refId, String aad, String salt) { + if (data == null) { + log.warn("Encryption skipped: Input data is null"); + return null; + } + try { + CryptomanagerRequestDto request = new CryptomanagerRequestDto(); + request.setApplicationId(appId); + request.setReferenceId(refId); + request.setData(CryptoUtil.encodeToURLSafeBase64(data.getBytes(StandardCharsets.UTF_8))); + request.setAad(aad); + request.setSalt(salt); + CryptomanagerResponseDto response = cryptomanagerService.encrypt(request); + log.debug("Data encrypted successfully for refId: {}", refId); + return response.getData(); + } catch (Exception e) { + log.error("Encryption failed for refId: {}", refId, e); + throw new RuntimeException("Failed to encrypt data", e); + } + } + + /** + * Decrypts data using the provided reference ID, AAD, and salt. + * + * @param data The data to decrypt. + * @param refId The reference ID. + * @param aad The additional authentication data. + * @param salt The salt for decryption. + * @return The decrypted data. + */ + public String decrypt(String data, String refId, String aad, String salt) { + if (data == null) { + log.warn("Decryption skipped: Input data is null"); + return null; + } + try { + CryptomanagerRequestDto request = new CryptomanagerRequestDto(); + request.setApplicationId(appId); + request.setReferenceId(refId); + request.setData(data); + request.setAad(aad); + request.setSalt(salt); + CryptomanagerResponseDto response = cryptomanagerService.decrypt(request); + String decryptedData = new String(CryptoUtil.decodeURLSafeBase64(response.getData()), StandardCharsets.UTF_8); + log.debug("Data decrypted successfully for refId: {}", refId); + return decryptedData; + } catch (Exception e) { + log.error("Decryption failed for refId: {}", refId, e); + throw new RuntimeException("Failed to decrypt data", e); + } + } + + /** + * Encrypts data with AES/GCM/NoPadding using the provided key. + * + * @param key The AES key. + * @param data The data to encrypt. + * @return The encrypted data, including the nonce prepended, encoded in Base64. + */ + public String encryptWithAES(SecretKey key, byte[] data) { + if (data == null || data.length == 0) { + log.warn("AES encryption skipped: Input data is null or empty"); + return null; + } + try { + // Generate a random nonce + byte[] nonce = new byte[NONCE_LENGTH]; + SECURE_RANDOM.nextBytes(nonce); + GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, nonce); + + // Initialize cipher + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec); + + // Encrypt data + byte[] encryptedData = cipher.doFinal(data); + + // Prepend nonce to encrypted data (nonce + ciphertext + tag) + byte[] combined = new byte[nonce.length + encryptedData.length]; + System.arraycopy(nonce, 0, combined, 0, nonce.length); + System.arraycopy(encryptedData, 0, combined, nonce.length, encryptedData.length); + + // Encode to Base64 + String result = Base64.getEncoder().encodeToString(combined); + log.debug("AES/GCM encryption successful, data length: {}", data.length); + return result; + } catch (Exception e) { + log.error("AES/GCM encryption failed", e); + throw new RuntimeException("Failed to encrypt with AES/GCM", e); + } + } + + /** + * Decrypts data with AES/GCM/NoPadding using the provided key. + * + * @param key The AES key. + * @param data The encrypted data (Base64-encoded, with nonce prepended). + * @return The decrypted data. + */ + public byte[] decryptWithAES(SecretKey key, String data) { + if (data == null || data.isEmpty()) { + log.warn("AES decryption skipped: Input data is null or empty"); + return null; + } + try { + // Decode Base64 data + byte[] decodedData = Base64.getDecoder().decode(data); + + // Extract nonce and encrypted data + if (decodedData.length < NONCE_LENGTH) { + log.error("Invalid encrypted data: too short"); + throw new IllegalArgumentException("Invalid encrypted data length"); + } + byte[] nonce = new byte[NONCE_LENGTH]; + byte[] encryptedData = new byte[decodedData.length - NONCE_LENGTH]; + System.arraycopy(decodedData, 0, nonce, 0, NONCE_LENGTH); + System.arraycopy(decodedData, NONCE_LENGTH, encryptedData, 0, encryptedData.length); + + // Initialize cipher + GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH, nonce); + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec); + + // Decrypt data + byte[] decryptedData = cipher.doFinal(encryptedData); + log.debug("AES/GCM decryption successful, decrypted length: {}", decryptedData.length); + return decryptedData; + } catch (Exception e) { + log.error("AES/GCM decryption failed", e); + throw new RuntimeException("Failed to decrypt with AES/GCM", e); + } + } + + /** + * Converts a byte array to a SecretKey for AES. + * + * @param keyBytes The byte array representing the key. + * @return The SecretKey. + */ + public static SecretKey bytesToSecretKey(byte[] keyBytes) { + if (keyBytes == null || keyBytes.length == 0) { + log.error("Invalid key bytes: null or empty"); + throw new IllegalArgumentException("Key bytes cannot be null or empty"); + } + try { + return new javax.crypto.spec.SecretKeySpec(keyBytes, "AES"); + } catch (Exception e) { + log.error("Failed to convert bytes to SecretKey", e); + throw new RuntimeException("Failed to create SecretKey", e); + } + } + + /** + * Converts a byte array to a PrivateKey for the specified algorithm. + * + * @param privateKeyBytes The byte array representing the private key. + * @param algorithmName The algorithm name (e.g., RSA, EC). + * @return The PrivateKey. + * @throws Exception If key conversion fails. + */ + public static PrivateKey bytesToPrivateKey(byte[] privateKeyBytes, String algorithmName) throws Exception { + if (privateKeyBytes == null || privateKeyBytes.length == 0) { + log.error("Invalid private key bytes: null or empty"); + throw new IllegalArgumentException("Private key bytes cannot be null or empty"); + } + if (algorithmName == null || algorithmName.isEmpty()) { + log.error("Invalid algorithm name: null or empty"); + throw new IllegalArgumentException("Algorithm name cannot be null or empty"); + } + try { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(algorithmName); + return keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + log.error("Failed to convert bytes to PrivateKey for algorithm: {}", algorithmName, e); + throw new Exception("Failed to create PrivateKey", e); + } + } + + /** + * Encrypts credential data using a wallet key. + * + * @param credentialData The credential data to encrypt. + * @param base64EncodedWalletKey The Base64-encoded wallet key. + * @return The encrypted credential data. + * @throws EncryptionException If encryption fails. + */ + public String encryptCredential(String credentialData, String base64EncodedWalletKey) throws EncryptionException { + if (credentialData == null || credentialData.isEmpty()) { + log.error("Credential data is null or empty"); + throw new EncryptionException(ENCRYPTION_FAILED.getErrorCode(), "Credential data cannot be null or empty"); + } + if (base64EncodedWalletKey == null || base64EncodedWalletKey.isEmpty()) { + log.error("Wallet key is null or empty"); + throw new EncryptionException(ENCRYPTION_FAILED.getErrorCode(), "Wallet key cannot be null or empty"); + } + try { + byte[] decodedWalletKey = Base64.getDecoder().decode(base64EncodedWalletKey); + SecretKey walletKey = bytesToSecretKey(decodedWalletKey); + String encryptedData = encryptWithAES(walletKey, stringToBytes(credentialData)); + log.debug("Credential encrypted successfully"); + return encryptedData; + } catch (Exception e) { + log.error("Failed to encrypt credential", e); + throw new EncryptionException(ENCRYPTION_FAILED.getErrorCode(), "Failed to encrypt credential", e); + } + } + + /** + * Decrypts credential data using a wallet key. + * + * @param encryptedCredentialData The encrypted credential data. + * @param base64EncodedWalletKey The Base64-encoded wallet key. + * @return The decrypted credential data. + * @throws DecryptionException If decryption fails. + */ + public String decryptCredential(String encryptedCredentialData, String base64EncodedWalletKey) throws DecryptionException { + if (encryptedCredentialData == null || encryptedCredentialData.isEmpty()) { + log.error("Encrypted credential data is null or empty"); + throw new DecryptionException(DECRYPTION_FAILED.getErrorCode() , "Encrypted credential data cannot be null or empty"); + } + if (base64EncodedWalletKey == null || base64EncodedWalletKey.isEmpty()) { + log.error("Wallet key is null or empty"); + throw new DecryptionException(DECRYPTION_FAILED.getErrorCode(), "Wallet key cannot be null or empty"); + } + try { + byte[] decodedWalletKey = Base64.getDecoder().decode(base64EncodedWalletKey); + SecretKey walletKey = bytesToSecretKey(decodedWalletKey); + byte[] decryptedData = decryptWithAES(walletKey, encryptedCredentialData); + String result = bytesToString(decryptedData); + log.debug("Credential decrypted successfully"); + return result; + } catch (Exception e) { + log.error("Failed to decrypt credential", e); + throw new DecryptionException(DECRYPTION_FAILED.getErrorCode(), "Failed to decrypt credential", e); + } + } + + /** + * Converts a string to a byte array using UTF-8 encoding. + * + * @param data The input string. + * @return The byte array. + */ + private static byte[] stringToBytes(String data) { + if (data == null) { + return null; + } + return data.getBytes(StandardCharsets.UTF_8); + } + + /** + * Converts a byte array to a string using UTF-8 encoding. + * + * @param data The byte array. + * @return The string. + */ + private static String bytesToString(byte[] data) { + if (data == null) { + return null; + } + return new String(data, StandardCharsets.UTF_8); + } + + /** + * Creates a detached JWT signing input by encoding the header and concatenating it with the payload bytes. + * This is used for creating the input to sign for detached JWTs where the payload is not included in the final JWT. + * Uses OpenID4VP library utilities for Base64URL encoding/decoding. + * Format: base64url(header) + '.' + payload_bytes + * + * @param headerJson The JWT header as a JSON string. + * @param payloadBase64Url The Base64URL encoded payload string to decode. + * @return byte array containing header bytes + '.' + payload bytes for signing + * @throws IllegalArgumentException if headerJson or payloadBase64Url is null + */ + public byte[] createDetachedJwtSigningInput(String headerJson, String payloadBase64Url) { + if (headerJson == null) { + log.error("Header JSON cannot be null"); + throw new IllegalArgumentException("Header JSON cannot be null"); + } + if (payloadBase64Url == null) { + log.error("Payload Base64URL string cannot be null"); + throw new IllegalArgumentException("Payload Base64URL string cannot be null"); + } + + try { + // Decode the Base64URL encoded payload + byte[] payloadBytes = DecoderKt.decodeFromBase64Url(payloadBase64Url); + + // Encode the header JSON to Base64URL + String header64 = EncoderKt.encodeToBase64Url(headerJson.getBytes(StandardCharsets.UTF_8)); + + // Create input bytes for detached JWT signing: header + '.' + payload + byte[] headerBytes = header64.getBytes(StandardCharsets.UTF_8); + byte[] inputBytes = new byte[headerBytes.length + 1 + payloadBytes.length]; + System.arraycopy(headerBytes, 0, inputBytes, 0, headerBytes.length); + inputBytes[headerBytes.length] = (byte) '.'; + System.arraycopy(payloadBytes, 0, inputBytes, headerBytes.length + 1, payloadBytes.length); + + log.debug("Detached JWT signing input created successfully, header length: {}, payload length: {}", + headerBytes.length, payloadBytes.length); + return inputBytes; + } catch (Exception e) { + log.error("Failed to create detached JWT signing input", e); + throw new RuntimeException("Failed to create detached JWT signing input", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/Draft13CredentialRequestService.java b/src/main/java/io/mosip/mimoto/service/Draft13CredentialRequestService.java new file mode 100644 index 000000000..f8aec64f5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/Draft13CredentialRequestService.java @@ -0,0 +1,114 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.util.Draft13CredentialRequestBuilder; +import io.mosip.mimoto.util.SigningKeyUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.security.KeyPair; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class Draft13CredentialRequestService { + + @Value("${signing.algorithms.priority.order:ED25519,ES256K,ES256,RS256}") + private String signingAlgorithmsPriorityOrder; + + private static final SigningAlgorithm FALLBACK_SIGNING_ALG = SigningAlgorithm.ED25519; + + private final Draft13CredentialRequestBuilder draft13CredentialRequestBuilder; + + private final KeyPairRetrievalService keyPairService; + + public Draft13CredentialRequestService(Draft13CredentialRequestBuilder draft13CredentialRequestBuilder, KeyPairRetrievalService keyPairService) { + this.draft13CredentialRequestBuilder = draft13CredentialRequestBuilder; + this.keyPairService = keyPairService; + } + + public Set getSigningAlgorithmsPriorityOrder() { + return Arrays.stream(signingAlgorithmsPriorityOrder.split(",")) + .map(String::trim).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + public Draft13VCCredentialRequest buildRequest(IssuerDTO issuerDTO, + String credentialConfigurationId, + CredentialIssuerWellKnownResponse wellKnownResponse, + String cNonce, + String walletId, + String base64EncodedWalletKey, + boolean isLoginFlow) throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = wellKnownResponse.getCredentialConfigurationsSupported().get(credentialConfigurationId); + + SigningAlgorithm signingAlgorithm = resolveAlgorithm(credentialsSupportedResponse); + + String jwt; + if (isLoginFlow) { + jwt = generateJwtUsingDBKeys(walletId, base64EncodedWalletKey, signingAlgorithm, wellKnownResponse, issuerDTO, cNonce); + } else { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(signingAlgorithm); + log.debug("Generated KeyPair for signing signingAlgorithm: {}", signingAlgorithm); + jwt = SigningKeyUtil.generateJwt(signingAlgorithm, wellKnownResponse.getCredentialIssuer(), issuerDTO.getClient_id(), cNonce, keyPair); + } + + String format = credentialsSupportedResponse.getFormat(); + return credentialsSupportedResponse.getProofTypesSupported() + .keySet() + .stream() + .findFirst() + .map(proofType -> { + VCCredentialRequestProof proof = VCCredentialRequestProof.builder() + .proofType(proofType) + .jwt(jwt) + .build(); + return draft13CredentialRequestBuilder.buildCredentialRequest(format, proof, credentialsSupportedResponse); + }) + .orElseThrow(() -> new IllegalArgumentException("No proof type available")); + } + + private SigningAlgorithm resolveAlgorithm(CredentialsSupportedResponse credentialsSupportedResponse) { + Map proofTypesSupported = credentialsSupportedResponse.getProofTypesSupported(); + ProofTypesSupported proofSigningAlgValuesSupported = proofTypesSupported.get("jwt"); + Set signingAlgoPriorityOrderSet = getSigningAlgorithmsPriorityOrder(); + + + return Optional + .ofNullable(proofSigningAlgValuesSupported) + .map(ProofTypesSupported::getProofSigningAlgValuesSupported) + .flatMap(issuerSupportedAlgorithms -> signingAlgoPriorityOrderSet.stream() + .filter(priorityAlgorithm -> issuerSupportedAlgorithms.stream(). + anyMatch(issuerSupportedAlgorithm -> issuerSupportedAlgorithm.equalsIgnoreCase(priorityAlgorithm))) + .findFirst()) + .map(SigningAlgorithm::fromString) + .orElseGet(() -> { + if (proofSigningAlgValuesSupported == null) { + log.warn("JWT proof type is missing in proof_types_supported field of Issuer well-known so falling back to {}", FALLBACK_SIGNING_ALG); + } else { + log.warn("None of the Issuer Supported Algorithms: {} are found in the predefined signing algorithms priority order: {} so falling back to {}", + proofSigningAlgValuesSupported.getProofSigningAlgValuesSupported(), signingAlgoPriorityOrderSet, FALLBACK_SIGNING_ALG); + } + return FALLBACK_SIGNING_ALG; + }); + } + + private String generateJwtUsingDBKeys(String walletId, + String base64EncodedWalletKey, + SigningAlgorithm signingAlgorithm, + CredentialIssuerWellKnownResponse wellKnownResponse, + IssuerDTO issuerDTO, + String cNonce) throws Exception { + + KeyPair keyPair = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, signingAlgorithm); + + return SigningKeyUtil.generateJwt(signingAlgorithm, + wellKnownResponse.getCredentialIssuer(), + issuerDTO.getClient_id(), + cNonce, + keyPair); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/EncryptionService.java b/src/main/java/io/mosip/mimoto/service/EncryptionService.java new file mode 100644 index 000000000..bece16f57 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/EncryptionService.java @@ -0,0 +1,9 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.mimoto.exception.DecryptionException; + +public interface EncryptionService { + String encrypt(String data) throws EncryptionException; + String decrypt(String data) throws DecryptionException; +} diff --git a/src/main/java/io/mosip/mimoto/service/IdpService.java b/src/main/java/io/mosip/mimoto/service/IdpService.java new file mode 100644 index 000000000..70acec149 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/IdpService.java @@ -0,0 +1,27 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.VerifiableCredentialRequestDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.exception.IssuerOnboardingException; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.util.Map; + +public interface IdpService { + HttpEntity> constructGetTokenRequest(Map params, String issuerId, String tokenEndpoint) throws IOException, IssuerOnboardingException, ApiNotAccessibleException, InvalidIssuerIdException; + + String getTokenEndpoint(String issuerId) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException; + + TokenResponseDTO getTokenResponse(Map params) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException; + + TokenResponseDTO getTokenResponse(VerifiableCredentialRequestDTO verifiableCredentialRequest) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException; + + ResponseEntity getTokenResponseV2(Map params, String dpopProof) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException, IssuerOnboardingException; +} diff --git a/src/main/java/io/mosip/mimoto/service/IssuersService.java b/src/main/java/io/mosip/mimoto/service/IssuersService.java new file mode 100644 index 000000000..908756c84 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/IssuersService.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuerV2DTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.IssuersV2DTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.constraints.NotBlank; + +import java.io.IOException; + +public interface IssuersService { + IssuersDTO getIssuers(String search) throws ApiNotAccessibleException, IOException; + + IssuerDTO getIssuerDetails(String issuerId) throws ApiNotAccessibleException, IOException, InvalidIssuerIdException; + + IssuersDTO getAllIssuers() throws ApiNotAccessibleException, IOException; + + CredentialIssuerConfiguration getIssuerConfiguration(String issuerId) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException; + + CredentialIssuerWellKnownResponse getIssuerWellKnownResponse(String credentialIssuerHost) throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException; + + IssuerConfig getIssuerConfig(String issuerId, @NotBlank String credentialType) throws ApiNotAccessibleException, InvalidIssuerIdException; + + IssuersV2DTO getIssuersV2DTO() throws ApiNotAccessibleException, IOException; + + IssuerV2DTO getIssuerV2Details(String issuerId) throws ApiNotAccessibleException, IOException; +} diff --git a/src/main/java/io/mosip/mimoto/service/KeyPairRetrievalService.java b/src/main/java/io/mosip/mimoto/service/KeyPairRetrievalService.java new file mode 100644 index 000000000..fb99923eb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/KeyPairRetrievalService.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.KeyGenerationException; + +import java.security.KeyPair; + +/** + * Service interface for managing key pairs from database + */ +public interface KeyPairRetrievalService { + + /** + * Retrieves a key pair from the database for the given wallet ID and signing algorithm + * + * @param walletId The wallet ID + * @param base64EncodedWalletKey The base64 encoded wallet key for decryption + * @param signingAlgorithm The signing algorithm + * @return The key pair + * @throws KeyGenerationException if key generation or retrieval fails + * @throws DecryptionException if decryption of private key fails + */ + KeyPair getKeyPairFromDB(String walletId, String base64EncodedWalletKey, SigningAlgorithm signingAlgorithm) throws KeyGenerationException, DecryptionException; +} diff --git a/src/main/java/io/mosip/mimoto/service/PresentationActionService.java b/src/main/java/io/mosip/mimoto/service/PresentationActionService.java new file mode 100644 index 000000000..2a5cf6485 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/PresentationActionService.java @@ -0,0 +1,33 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.SubmitPresentationRequestDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import org.springframework.http.ResponseEntity; + +/** + * Service interface for handling presentation actions (submission or rejection) + */ +public interface PresentationActionService { + + /** + * Handles presentation action - either submission with credentials or rejection with error + *

+ * This method handles all business logic and exceptions internally, returning appropriate + * HTTP responses. The controller should only catch generic exceptions for unexpected errors. + * + * @param walletId The wallet ID + * @param presentationId The presentation ID + * @param request The request containing either selected credentials or error details + * @param vpSessionData The session data containing presentation information + * @param base64Key The wallet key for decryption (required for submission) + * @return ResponseEntity with appropriate response type and HTTP status + */ + ResponseEntity handlePresentationAction( + String walletId, + String presentationId, + SubmitPresentationRequestDTO request, + VerifiablePresentationSessionData vpSessionData, + String base64Key + ); +} + diff --git a/src/main/java/io/mosip/mimoto/service/PresentationService.java b/src/main/java/io/mosip/mimoto/service/PresentationService.java new file mode 100644 index 000000000..19a796280 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/PresentationService.java @@ -0,0 +1,11 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; + +import java.io.IOException; + +public interface PresentationService { + + String authorizePresentation(PresentationRequestDTO presentationRequestDTO) throws ApiNotAccessibleException, IOException; +} diff --git a/src/main/java/io/mosip/mimoto/service/PresentationSubmissionService.java b/src/main/java/io/mosip/mimoto/service/PresentationSubmissionService.java new file mode 100644 index 000000000..1ee52da56 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/PresentationSubmissionService.java @@ -0,0 +1,40 @@ +package io.mosip.mimoto.service; + +import com.nimbusds.jose.JOSEException; +import io.mosip.mimoto.dto.SubmitPresentationResponseDTO; +import io.mosip.mimoto.dto.SubmitPresentationRequestDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.KeyGenerationException; + +import java.io.IOException; + +/** + * Service interface for handling presentation submission operations + */ +public interface PresentationSubmissionService { + + /** + * Submits a presentation with selected credentials + * + * @param sessionData The session data containing presentation information + * @param walletId The wallet ID + * @param presentationId The presentation ID + * @param request The submission request with selected credentials + * @param base64Key The user's private key for signing + * @return The submission response + * @throws ApiNotAccessibleException if API is not accessible + * @throws IOException if there's an IO error + * @throws JOSEException if JWT signing fails + * @throws KeyGenerationException if key pair retrieval or generation fails + * @throws DecryptionException if decryption of private key fails + */ + SubmitPresentationResponseDTO submitPresentation( + VerifiablePresentationSessionData sessionData, + String walletId, + String presentationId, + SubmitPresentationRequestDTO request, + String base64Key + ) throws ApiNotAccessibleException, IOException, JOSEException, KeyGenerationException, DecryptionException; +} diff --git a/src/main/java/io/mosip/mimoto/service/RestClientService.java b/src/main/java/io/mosip/mimoto/service/RestClientService.java index 33aa8edda..f139777de 100644 --- a/src/main/java/io/mosip/mimoto/service/RestClientService.java +++ b/src/main/java/io/mosip/mimoto/service/RestClientService.java @@ -74,7 +74,7 @@ public T postApi(ApiName apiName, String queryParam, String queryParamValue, T r * the response type * @param mediaType * the content type - * + * * @return the t * @throws ApisResourceAccessException * the apis resource access exception @@ -121,4 +121,7 @@ public T postApi(ApiName apiName, List pathsegments, String queryParam, public T postApi(ApiName apiName, MediaType mediaType, List pathsegments, List queryParam, List queryParamValue, T requestedData, Class responseType) throws ApisResourceAccessException; + + public T postApi(ApiName apiName, T requestedData, Class responseType, boolean useBearerToken) + throws ApisResourceAccessException; } diff --git a/src/main/java/io/mosip/mimoto/service/TokenDecoder.java b/src/main/java/io/mosip/mimoto/service/TokenDecoder.java new file mode 100644 index 000000000..f932a85d5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/TokenDecoder.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jwt.Jwt; + +public interface TokenDecoder { + Jwt decode(String idToken) throws OAuth2AuthenticationException; +} diff --git a/src/main/java/io/mosip/mimoto/service/TokenService.java b/src/main/java/io/mosip/mimoto/service/TokenService.java new file mode 100644 index 000000000..e039f99eb --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/TokenService.java @@ -0,0 +1,9 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface TokenService { + void processToken(String idToken, String provider, HttpServletRequest request, HttpServletResponse response) throws OAuth2AuthenticationException; +} diff --git a/src/main/java/io/mosip/mimoto/service/TokenServiceFactory.java b/src/main/java/io/mosip/mimoto/service/TokenServiceFactory.java new file mode 100644 index 000000000..66e0db522 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/TokenServiceFactory.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class TokenServiceFactory { + private static final String UNSUPPORTED_PROVIDER_MESSAGE = "Unsupported provider: %s"; + private final Map tokenServices; + + @Autowired + public TokenServiceFactory(Map tokenServices) { + this.tokenServices = tokenServices; + } + + public TokenService getTokenService(String provider) throws OAuth2AuthenticationException { + TokenService service = tokenServices.get(provider.toLowerCase()); + if (service == null) { + throw new OAuth2AuthenticationException(ErrorConstants.INVALID_REQUEST.getErrorCode(), String.format(UNSUPPORTED_PROVIDER_MESSAGE, provider), HttpStatus.BAD_REQUEST); + } + return service; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/TrustedVerifierService.java b/src/main/java/io/mosip/mimoto/service/TrustedVerifierService.java new file mode 100644 index 000000000..515aaa0c7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/TrustedVerifierService.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.TrustedVerifierRequest; +import io.mosip.mimoto.dto.TrustedVerifierResponseDTO; + +public interface TrustedVerifierService { + + TrustedVerifierResponseDTO addTrustedVerifier(String walletId, TrustedVerifierRequest trustedVerifierRequest); + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/UserMetadataService.java b/src/main/java/io/mosip/mimoto/service/UserMetadataService.java new file mode 100644 index 000000000..391300892 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/UserMetadataService.java @@ -0,0 +1,94 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.repository.UserMetadataRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@Service +public class UserMetadataService { + private final UserMetadataRepository repository; + private final EncryptionService encryptionService; + + @Autowired + public UserMetadataService(UserMetadataRepository repository, EncryptionService encryptionService) { + this.repository = repository; + this.encryptionService = encryptionService; + } + + public UserMetadata getUserMetadata(String providerSubjectId, String identityProvider) + throws DecryptionException { + return repository.findByProviderSubjectIdAndIdentityProvider(providerSubjectId, identityProvider) + .map(user -> { + try { + UserMetadata decryptedUser = new UserMetadata(); + decryptedUser.setId(user.getId()); + decryptedUser.setProviderSubjectId(user.getProviderSubjectId()); + decryptedUser.setIdentityProvider(user.getIdentityProvider()); + + decryptedUser.setDisplayName(encryptionService.decrypt(user.getDisplayName())); + decryptedUser.setProfilePictureUrl(encryptionService.decrypt(user.getProfilePictureUrl())); + decryptedUser.setEmail(encryptionService.decrypt(user.getEmail())); + + return decryptedUser; + } catch (DecryptionException e) { + throw new RuntimeException("Failed to decrypt user metadata", e); + } + }) + .orElse(null); + } + + public String updateOrCreateUserMetadata(String providerSubjectId, String identityProvider, + String displayName, String profilePictureUrl, String email) + throws EncryptionException, DecryptionException { + Instant now = Instant.now(); + UserMetadata existingUser = getUserMetadata(providerSubjectId, identityProvider); + if (null != existingUser) { + return updateUser(existingUser, displayName, profilePictureUrl, email, now); + } else { + return createUser(providerSubjectId, identityProvider, displayName, profilePictureUrl, email, now); + } + } + + private String updateUser(UserMetadata user, String displayName, String profilePictureUrl, String email, Instant now) + throws EncryptionException { + boolean updated = updateIfChanged(user::getDisplayName, user::setDisplayName, displayName); + updated = updateIfChanged(user::getProfilePictureUrl, user::setProfilePictureUrl, profilePictureUrl) || updated; + updated = updateIfChanged(user::getEmail, user::setEmail, email) || updated; + + if (updated) { + user.setUpdatedAt(now); + repository.save(user); + } + return user.getId(); + } + + private boolean updateIfChanged(Supplier getter, Consumer setter, String newValue) + throws EncryptionException { + String current = getter.get(); + setter.accept(encryptionService.encrypt(newValue)); + return !current.equals(newValue); + } + + private String createUser(String providerSubjectId, String identityProvider, String displayName, + String profilePictureUrl, String email, Instant now) + throws EncryptionException { + UserMetadata user = new UserMetadata(); + user.setId(UUID.randomUUID().toString()); + user.setProviderSubjectId(providerSubjectId); + user.setIdentityProvider(identityProvider); + user.setDisplayName(encryptionService.encrypt(displayName)); + user.setProfilePictureUrl(encryptionService.encrypt(profilePictureUrl)); + user.setEmail(encryptionService.encrypt(email)); + user.setCreatedAt(now); + user.setUpdatedAt(now); + return repository.save(user).getId(); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/V1CredentialRequestService.java b/src/main/java/io/mosip/mimoto/service/V1CredentialRequestService.java new file mode 100644 index 000000000..5e46baa10 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/V1CredentialRequestService.java @@ -0,0 +1,91 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.util.RestApiClient; +import io.mosip.mimoto.util.SigningKeyUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; + +import java.security.KeyPair; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class V1CredentialRequestService { + + private static final SigningAlgorithm FALLBACK_SIGNING_ALG = SigningAlgorithm.ED25519; + private static final String PROOF_TYPE_JWT = "jwt"; + private final RestApiClient restApiClient; + private final KeyPairRetrievalService keyPairService; + @Value("${signing.algorithms.priority.order:ED25519,ES256K,ES256,RS256}") + private String signingAlgorithmsPriorityOrder; + + public V1CredentialRequestService(RestApiClient restApiClient, KeyPairRetrievalService keyPairService) { + this.restApiClient = restApiClient; + this.keyPairService = keyPairService; + } + + public V1VCCredentialRequest buildRequest(IssuerDTO issuerDTO, String credentialConfigurationId, CredentialIssuerWellKnownResponse wellKnownResponse, String walletId, String base64EncodedWalletKey, boolean isLoginFlow) throws Exception { + + CredentialsSupportedResponse credentialsSupportedResponse = wellKnownResponse.getCredentialConfigurationsSupported().get(credentialConfigurationId); + + SigningAlgorithm signingAlgorithm = resolveAlgorithm(credentialsSupportedResponse); + + String cNonce = fetchNonce(wellKnownResponse.getNonceEndpoint()); + + String jwt; + KeyPair keyPair; + if (isLoginFlow) { + keyPair = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, signingAlgorithm); + } else { + keyPair = SigningKeyUtil.generateKeyPair(signingAlgorithm); + log.debug("Generated KeyPair for signing algorithm: {}", signingAlgorithm); + } + jwt = SigningKeyUtil.generateJwt(signingAlgorithm, wellKnownResponse.getCredentialIssuer(), issuerDTO.getClient_id(), cNonce, keyPair); + + Map> proofs = Map.of(PROOF_TYPE_JWT, List.of(jwt)); + + return V1VCCredentialRequest.builder().credentialConfigurationId(credentialConfigurationId).proofs(proofs).build(); + } + + private String fetchNonce(String nonceEndpoint) throws Exception { + if (nonceEndpoint == null || nonceEndpoint.isBlank()) { + log.debug("No nonce_endpoint configured, building request without nonce"); + return null; + } + + log.debug("Fetching nonce from endpoint: {}", nonceEndpoint); + NonceResponse nonceResponse = restApiClient.postApi(nonceEndpoint, MediaType.APPLICATION_JSON, null, NonceResponse.class); + + if (nonceResponse == null || nonceResponse.getCNonce() == null) { + log.warn("Nonce endpoint returned null or empty response"); + return null; + } + + return nonceResponse.getCNonce(); + } + + public Set getSigningAlgorithmsPriorityOrder() { + return Arrays.stream(signingAlgorithmsPriorityOrder.split(",")).map(String::trim).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private SigningAlgorithm resolveAlgorithm(CredentialsSupportedResponse credentialsSupportedResponse) { + Map proofTypesSupported = credentialsSupportedResponse.getProofTypesSupported(); + ProofTypesSupported proofSigningAlgValuesSupported = proofTypesSupported.get(PROOF_TYPE_JWT); + Set signingAlgoPriorityOrderSet = getSigningAlgorithmsPriorityOrder(); + + return Optional.ofNullable(proofSigningAlgValuesSupported).map(ProofTypesSupported::getProofSigningAlgValuesSupported).flatMap(issuerSupportedAlgorithms -> signingAlgoPriorityOrderSet.stream().filter(priorityAlgorithm -> issuerSupportedAlgorithms.stream().anyMatch(issuerSupportedAlgorithm -> issuerSupportedAlgorithm.equalsIgnoreCase(priorityAlgorithm))).findFirst()).map(SigningAlgorithm::fromString).orElseGet(() -> { + if (proofSigningAlgValuesSupported == null) { + log.warn("JWT proof type is missing in proof_types_supported field of Issuer well-known so falling back to {}", FALLBACK_SIGNING_ALG); + } else { + log.warn("None of the Issuer Supported Algorithms: {} are found in the predefined signing algorithms priority order: {} so falling back to {}", proofSigningAlgValuesSupported.getProofSigningAlgValuesSupported(), signingAlgoPriorityOrderSet, FALLBACK_SIGNING_ALG); + } + return FALLBACK_SIGNING_ALG; + }); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/VCDownloadHandler.java b/src/main/java/io/mosip/mimoto/service/VCDownloadHandler.java new file mode 100644 index 000000000..4587ea8cd --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/VCDownloadHandler.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.exception.CredentialProcessingException; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; + +public interface VCDownloadHandler { + + /** + * Downloads the credential from the issuer. + * + * @param issuerDTO The issuerDTO + * @param credentialConfigurationId The type of the credential + * @param credentialIssuerWellKnownResponse The well-known response of the credential issuer + * @param tokenResponse The token response containing the access token + * @param walletId The ID of the wallet + * @param base64Key The Base64-encoded wallet key + * @param isLoginFlow Flag indicating if the flow is for login + * @throws CredentialProcessingException If there is an error during credential processing + * @throws InvalidCredentialResourceException If the credential resource is invalid + * @throws ExternalServiceUnavailableException If an external service is unavailable + */ + VCCredentialResponse downloadCredential(IssuerDTO issuerDTO, String credentialConfigurationId, CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse, TokenResponseDTO tokenResponse, String walletId, String base64Key, boolean isLoginFlow) throws CredentialProcessingException, InvalidCredentialResourceException, ExternalServiceUnavailableException; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/VCDownloadHandlerFactory.java b/src/main/java/io/mosip/mimoto/service/VCDownloadHandlerFactory.java new file mode 100644 index 000000000..78ffcc957 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/VCDownloadHandlerFactory.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class VCDownloadHandlerFactory { + + private final Map handlers; + + public VCDownloadHandlerFactory(Map handlers) { + this.handlers = handlers; + } + + public VCDownloadHandler getHandler(VCSpecificationVersion version) { + if(version == null) { + throw new NullPointerException("Version cannot be null"); + } + VCDownloadHandler processor = handlers.get(version.getVersion()); + if (processor == null) { + throw new IllegalArgumentException("Unsupported download version: " + version); + } + + return processor; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/VerifierService.java b/src/main/java/io/mosip/mimoto/service/VerifierService.java new file mode 100644 index 000000000..e97d2bc88 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/VerifierService.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.openID4VP.authorizationRequest.Verifier; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; + +public interface VerifierService { + Optional getVerifierByClientId(String clientId) throws ApiNotAccessibleException, IOException; + void validateVerifier(String clientId, String responseUri, String redirectUri) throws ApiNotAccessibleException, JsonProcessingException; + VerifiersDTO getTrustedVerifiers() throws ApiNotAccessibleException, IOException; + boolean isVerifierTrustedByWallet(String verifierId, String walletId); + boolean isVerifierClientPreregistered(List preRegisteredVerifiers, String urlEncodedVPAuthorizationRequest) throws URISyntaxException; +} diff --git a/src/main/java/io/mosip/mimoto/service/WalletCredentialService.java b/src/main/java/io/mosip/mimoto/service/WalletCredentialService.java new file mode 100644 index 000000000..a9ae76ba4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/WalletCredentialService.java @@ -0,0 +1,62 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.dto.resident.WalletCredentialResponseDTO; +import io.mosip.mimoto.exception.*; + +import java.util.List; + +/** + * Service interface for managing wallet credentials. + */ +public interface WalletCredentialService { + + /** + * Fetches and stores a credential for a wallet. + * + * @param issuerId The issuer ID. + * @param credentialConfigurationId The type of credential. + * @param tokenResponse The token response containing the access token. + * @param locale The locale for display purposes. + * @param walletId The wallet ID. + * @param base64Key The base64-encoded key for encryption. + * @return The stored credential response. + * @throws CredentialProcessingException If processing fails. + */ + VerifiableCredentialResponseDTO downloadVCAndStoreInDB(String issuerId, String credentialConfigurationId, + TokenResponseDTO tokenResponse, + String locale, String walletId, String base64Key) + throws CredentialProcessingException, ExternalServiceUnavailableException; + + /** + * Fetches all credentials for a wallet. + * + * @param walletId The wallet ID. + * @param base64Key The base64-encoded key for decryption. + * @param locale The locale for display purposes. + * @return List of credential responses. + */ + List fetchAllCredentialsForWallet(String walletId, String base64Key, String locale); + + /** + * Fetches a single verifiable credential by ID. + * + * @param walletId The wallet ID. + * @param credentialId The credential ID. + * @param base64Key The base64-encoded key for decryption. + * @param locale The locale for display purposes. + * @return The credential response with PDF stream. + * @throws CredentialNotFoundException If the credential is not found. + * @throws CredentialProcessingException If processing fails. + */ + WalletCredentialResponseDTO fetchVerifiableCredential(String walletId, String credentialId, String base64Key, + String locale) + throws CredentialNotFoundException, CredentialProcessingException; + + void deleteCredential(String credentialId, String walletId) throws CredentialNotFoundException; + + List getDecryptedCredentials(String walletId, String base64Key); + +} diff --git a/src/main/java/io/mosip/mimoto/service/WalletLockService.java b/src/main/java/io/mosip/mimoto/service/WalletLockService.java new file mode 100644 index 000000000..231178e2d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/WalletLockService.java @@ -0,0 +1,88 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.config.WalletPasscodeConfig; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.model.WalletLockStatus; +import org.springframework.stereotype.Service; + +@Service +public class WalletLockService { + private final WalletPasscodeConfig walletPasscodeConfig; + private static final int MINUTES_TO_MILLIS = 60 * 1000; + + public WalletLockService(WalletPasscodeConfig walletPasscodeConfig) { + this.walletPasscodeConfig = walletPasscodeConfig; + } + + public Wallet enforceLockCyclePolicy(Wallet wallet) { + wallet = initializePasscodeControlInWallet(wallet); + WalletMetadata walletMetadata = wallet.getWalletMetadata(); + PasscodeControl passcodeControl = walletMetadata.getPasscodeControl(); + + // Increment the current attempt count + passcodeControl.setFailedAttemptCount(passcodeControl.getFailedAttemptCount() + 1); + + if (passcodeControl.getCurrentCycleCount() == 0) { + passcodeControl.setCurrentCycleCount(1); + } + + if (passcodeControl.getFailedAttemptCount() == walletPasscodeConfig.getMaxFailedAttemptsAllowedPerCycle()) { + passcodeControl.setCurrentCycleCount(passcodeControl.getCurrentCycleCount() + 1); + + if (passcodeControl.getCurrentCycleCount() > walletPasscodeConfig.getMaxLockCyclesAllowed()) { + passcodeControl.setRetryBlockedUntil(null); + walletMetadata.setLockStatus(WalletLockStatus.PERMANENTLY_LOCKED); + } else { + passcodeControl.setRetryBlockedUntil(System.currentTimeMillis() + (walletPasscodeConfig.getRetryBlockedUntil() * MINUTES_TO_MILLIS)); + walletMetadata.setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + } + } else if (isLastSecondAttemptBeforePermanentLock(passcodeControl)) { + walletMetadata.setLockStatus(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT); + } + + return wallet; + } + + private boolean isLastSecondAttemptBeforePermanentLock(PasscodeControl passcodeControl) { + return passcodeControl.getFailedAttemptCount() == walletPasscodeConfig.getMaxFailedAttemptsAllowedPerCycle() - 1 && passcodeControl.getCurrentCycleCount() == walletPasscodeConfig.getMaxLockCyclesAllowed(); + } + + public Wallet resetTemporaryLockIfExpired(Wallet wallet) { + wallet = initializePasscodeControlInWallet(wallet); + WalletMetadata walletMetadata = wallet.getWalletMetadata(); + PasscodeControl passcodeControl = walletMetadata.getPasscodeControl(); + + if (isTemporaryLockExpired(walletMetadata, passcodeControl)) { + passcodeControl.setRetryBlockedUntil(null); + passcodeControl.setFailedAttemptCount(0); + walletMetadata.setLockStatus(WalletLockStatus.LOCK_EXPIRED); + } + return wallet; + } + + private static boolean isTemporaryLockExpired(WalletMetadata walletMetadata, PasscodeControl passcodeControl) { + return walletMetadata.getLockStatus() == WalletLockStatus.TEMPORARILY_LOCKED && passcodeControl.getRetryBlockedUntil() != null && System.currentTimeMillis() > passcodeControl.getRetryBlockedUntil(); + } + + public Wallet resetLockState(Wallet wallet) { + wallet = initializePasscodeControlInWallet(wallet); + WalletMetadata walletMetadata = wallet.getWalletMetadata(); + PasscodeControl passcodeControl = walletMetadata.getPasscodeControl(); + + passcodeControl.setFailedAttemptCount(0); + passcodeControl.setCurrentCycleCount(0); + passcodeControl.setRetryBlockedUntil(null); + walletMetadata.setLockStatus(null); + return wallet; + } + + private Wallet initializePasscodeControlInWallet(Wallet wallet) { + PasscodeControl passcodeControl = wallet.getWalletMetadata().getPasscodeControl(); + if (passcodeControl == null) { + wallet.getWalletMetadata().setPasscodeControl(new PasscodeControl()); + } + return wallet; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/WalletPresentationService.java b/src/main/java/io/mosip/mimoto/service/WalletPresentationService.java new file mode 100644 index 000000000..bb576a1c0 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/WalletPresentationService.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.MatchingCredentialsDTO; +import io.mosip.mimoto.dto.SubmitPresentationRequestDTO; +import io.mosip.mimoto.dto.VPResponseDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import org.springframework.http.ResponseEntity; + +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * Service interface for handling wallet presentation operations + */ +public interface WalletPresentationService { + VPResponseDTO handleVPAuthorizationRequest(String urlEncodedVPAuthorizationRequest, String walletId) throws ApiNotAccessibleException, IOException, OpenID4VPExceptions, URISyntaxException; + ResponseEntity handlePresentationAction(String walletId, String presentationId, SubmitPresentationRequestDTO request, VerifiablePresentationSessionData vpSessionData, String base64Key); + MatchingCredentialsDTO getMatchingCredentials(VerifiablePresentationSessionData sessionData, String walletId, String base64Key) throws ApiNotAccessibleException, IOException; +} + diff --git a/src/main/java/io/mosip/mimoto/service/WalletService.java b/src/main/java/io/mosip/mimoto/service/WalletService.java new file mode 100644 index 000000000..4ec826989 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/WalletService.java @@ -0,0 +1,47 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.WalletDetailsResponseDto; +import io.mosip.mimoto.dto.WalletResponseDto; +import io.mosip.mimoto.exception.InvalidRequestException; + +import java.util.List; + +/** + * Service interface for managing wallets. + */ +public interface WalletService { + + /** + * Creates a new wallet for a user. + * + * @param userId The user ID. + * @param name The wallet name. + * @param pin The wallet PIN. + * @return The created wallet ID. + * @throws InvalidRequestException If the request is invalid. + * Example: Wallet PIN and Confirm Wallet PIN received in request don't match + */ + WalletResponseDto createWallet(String userId, String name, String pin, String confirmPin) throws InvalidRequestException; + + /** + * Unlocks a wallet for a user. + * + * @param walletId The wallet ID. + * @param pin The wallet PIN. + * @param userId The user ID. + * @return WalletResponseDto containing wallet details. + * @throws InvalidRequestException If the wallet is not found or PIN is invalid. + */ + WalletResponseDto unlockWallet(String walletId, String pin, String userId) throws InvalidRequestException; + + /** + * Retrieves all wallets for a user. + * + * @param userId The user ID. + * @return List of wallet responses. + * @throws InvalidRequestException If the request fails. + */ + List getWallets(String userId); + + void deleteWallet(String userId, String walletId) throws InvalidRequestException; +} diff --git a/src/main/java/io/mosip/mimoto/service/WalletUnlockService.java b/src/main/java/io/mosip/mimoto/service/WalletUnlockService.java new file mode 100644 index 000000000..209d8e7c8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/WalletUnlockService.java @@ -0,0 +1,80 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.WalletLockedException; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.util.WalletLockStatusUtils; +import io.mosip.mimoto.util.WalletUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class WalletUnlockService { + private final WalletUtil walletUtil; + private final WalletLockService walletLockService; + + private final WalletRepository walletRepository; + + public WalletUnlockService(WalletUtil walletUtil, WalletLockService walletLockService, WalletRepository walletRepository) { + this.walletUtil = walletUtil; + this.walletLockService = walletLockService; + this.walletRepository = walletRepository; + } + + public String handleUnlock(Wallet wallet, String pin) throws InvalidRequestException { + wallet = walletLockService.resetTemporaryLockIfExpired(wallet); + throwExceptionIfWalletIsLocked(wallet); + try { + String decryptedWalletKey = walletUtil.decryptWalletKey(wallet.getWalletKey(), pin); + wallet = walletLockService.resetLockState(wallet); + walletRepository.save(wallet); + return decryptedWalletKey; + } catch (InvalidRequestException ex) { + log.error("Failed to unlock Wallet {} due to: {}", wallet.getId(), ex.getMessage(), ex); + handleFailedUnlock(wallet); + throw ex; + } + } + + private void handleFailedUnlock(Wallet wallet) { + try { + wallet = walletLockService.enforceLockCyclePolicy(wallet); + throwExceptionIfLastAttemptLeftForUnlock(wallet); + wallet = walletLockService.resetTemporaryLockIfExpired(wallet); + throwExceptionIfWalletIsLocked(wallet); + walletRepository.save(wallet); + } catch (Exception ex) { + walletRepository.save(wallet); + throw ex; + } + } + + private void throwExceptionIfWalletIsLocked(Wallet wallet) throws WalletLockedException { + ErrorDTO errorDTO = WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet); + + if (errorDTO != null) { + String errorCode = errorDTO.getErrorCode(); + if (errorCode.equals(WalletLockStatus.TEMPORARILY_LOCKED.getValue()) || errorCode.equals(WalletLockStatus.PERMANENTLY_LOCKED.getValue())) { + throw new WalletLockedException(errorDTO.getErrorCode(), errorDTO.getErrorMessage()); + } + } + } + + private void throwExceptionIfLastAttemptLeftForUnlock(Wallet wallet) throws InvalidRequestException { + ErrorDTO errorDTO = WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet); + + if (errorDTO != null) { + String errorCode = errorDTO.getErrorCode(); + if (errorCode.equals(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT.getValue())) { + throw new InvalidRequestException( + ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), + ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/BioApiImpl.java b/src/main/java/io/mosip/mimoto/service/impl/BioApiImpl.java deleted file mode 100644 index 434b788ff..000000000 --- a/src/main/java/io/mosip/mimoto/service/impl/BioApiImpl.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.mosip.mimoto.service.impl; - -import java.util.Arrays; -import java.util.Objects; -import java.util.Optional; - -import org.springframework.stereotype.Component; - -import io.mosip.mimoto.constant.QualityType; -import io.mosip.mimoto.entity.BDBInfo; -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.entity.MatchDecision; -import io.mosip.mimoto.model.KeyValuePair; -import io.mosip.mimoto.model.QualityScore; -import io.mosip.mimoto.model.Response; -import io.mosip.mimoto.spi.IBioApi; - -/** - * The Class BioApiImpl. - * - * @author Sanjay Murali - * @author Manoj SP - * - */ -@Component -public class BioApiImpl implements IBioApi { - - /* - * (non-Javadoc) - * - * @see - * io.mosip.kernel.core.bioapi.spi.IBioApi#checkQuality(io.mosip.kernel.core. - * bioapi.model.BIR, io.mosip.kernel.core.bioapi.model.KeyValuePair[]) - */ - @Override - public Response checkQuality(BIR sample, KeyValuePair[] flags) { - QualityScore qualityScore = new QualityScore(); - int major = Optional.ofNullable(sample.getBdbInfo()).map(BDBInfo::getQuality).map(QualityType::getScore) - .orElse(0L).intValue(); - qualityScore.setScore(major); - Response response = new Response<>(); - response.setStatusCode(200); - response.setResponse(qualityScore); - return response; - } - - /* - * (non-Javadoc) - * - * @see - * io.mosip.kernel.core.bioapi.spi.IBioApi#match(io.mosip.kernel.core.bioapi. - * model.BIR, io.mosip.kernel.core.bioapi.model.BIR[], - * io.mosip.kernel.core.bioapi.model.KeyValuePair[]) - */ - @Override - public Response match(BIR sample, BIR[] gallery, KeyValuePair[] flags) { - MatchDecision matchingScore[] = new MatchDecision[gallery.length]; - int count = 0; - for (BIR recordedValue : gallery) { - matchingScore[count] = new MatchDecision(); - if (Objects.nonNull(recordedValue) && Objects.nonNull(recordedValue.getBdb()) - && recordedValue.getBdb().length != 0 && Arrays.equals(recordedValue.getBdb(), sample.getBdb())) { - matchingScore[count].setMatch(true); - } else { - matchingScore[count].setMatch(false); - } - count++; - } - Response response = new Response<>(); - response.setStatusCode(200); - response.setResponse(matchingScore); - return response; - } - - /* - * (non-Javadoc) - * - * @see - * io.mosip.kernel.core.bioapi.spi.IBioApi#extractTemplate(io.mosip.kernel.core. - * bioapi.model.BIR, io.mosip.kernel.core.bioapi.model.KeyValuePair[]) - */ - @Override - public Response extractTemplate(BIR sample, KeyValuePair[] flags) { - Response response = new Response<>(); - response.setStatusCode(200); - response.setResponse(sample); - return response; - } - - /* - * (non-Javadoc) - * - * @see - * io.mosip.kernel.core.bioapi.spi.IBioApi#segment(io.mosip.kernel.core.bioapi. - * model.BIR, io.mosip.kernel.core.bioapi.model.KeyValuePair[]) - */ - @Override - public Response segment(BIR sample, KeyValuePair[] flags) { - BIR[] bir = new BIR[1]; - bir[0] = sample; - Response response = new Response<>(); - response.setStatusCode(200); - response.setResponse(bir); - return response; - } - -} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/CbeffContainerImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CbeffContainerImpl.java deleted file mode 100644 index 393c9e3a4..000000000 --- a/src/main/java/io/mosip/mimoto/service/impl/CbeffContainerImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.mosip.mimoto.service.impl; - -import java.util.List; - -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.entity.BIRInfo; -import io.mosip.mimoto.entity.BIRInfo.BIRInfoBuilder; -import io.mosip.mimoto.service.CbeffContainerI; -import io.mosip.mimoto.util.CbeffValidator; -import io.mosip.mimoto.util.CbeffXSDValidator; - -/** - * @author Ramadurai Pandian - * - * A Container Class where the BIR is created and updated - * - */ -public class CbeffContainerImpl extends CbeffContainerI { - - private BIR bir; - - /** - * Method where the initialization of BIR happens - * - * @param birList List of BIR data - * @return BIR data with all images - */ - @Override - public BIR createBIRType(List birList) { - load(); - bir.setBirs(birList); - return bir; - } - - private void load() { - // Creating first version of Cbeff - bir = new BIR(); - // Initial Version - // VersionType versionType = new VersionType(); - // versionType.setMajor(1); - // versionType.setMinor(1); - // VersionType cbeffVersion = new VersionType(); - // cbeffVersion.setMajor(1); - // cbeffVersion.setMinor(1); - // BIR.setVersion(versionType); - // BIR.setCBEFFVersion(cbeffVersion); - - BIRInfoBuilder infoBuilder = new BIRInfoBuilder().withIntegrity(false); - BIRInfo birInfo = new BIRInfo(infoBuilder); - bir.setBirInfo(birInfo); - } - - /** - * Method to the update of BIR - * - * @param birList List of BIR data - * - * @param fileBytes Cbeff XML data as bytes - * - * @return BIR BIR data with all images - */ - @Override - public BIR updateBIRType(List birList, byte[] fileBytes) throws Exception { - BIR biometricRecord = CbeffValidator.getBIRFromXML(fileBytes); - // BIR.getVersion().setMajor(BIR.getVersion().getMajor() + 1); - // BIR.getCBEFFVersion().setMajor(BIR.getCBEFFVersion().getMajor()); - for (BIR bir : birList) { - biometricRecord.getBirs().add(bir); - } - return biometricRecord; - } - - /** - * Method to the validate the BIR - * - * @param xmlBytes Cbeff XML data as bytes array - * - * @param xsdBytes Cbeff XSD data as bytes array - * - * @return boolean of validated XML against XSD - */ - @Override - public boolean validateXML(byte[] xmlBytes, byte[] xsdBytes) throws Exception { - return CbeffXSDValidator.validateXML(xsdBytes, xmlBytes); - } - -} diff --git a/src/main/java/io/mosip/mimoto/service/impl/CbeffImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CbeffImpl.java deleted file mode 100644 index 06fda5e15..000000000 --- a/src/main/java/io/mosip/mimoto/service/impl/CbeffImpl.java +++ /dev/null @@ -1,201 +0,0 @@ -package io.mosip.mimoto.service.impl; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.List; -import java.util.Map; - -import javax.annotation.PostConstruct; - -import org.apache.commons.io.IOUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.spi.CbeffUtil; -import io.mosip.mimoto.util.CbeffValidator; - -/** - * This class is used to create,update, validate and search Cbeff data. - * - * @author Ramadurai Pandian - */ -@Component -public class CbeffImpl implements CbeffUtil { - - /* - * XSD storage path from config server - */ - - /** The config server file storage URL. */ - @Value("${mosip.kernel.xsdstorage-uri}") - private String configServerFileStorageURL; - - /* - * XSD file name - */ - - /** The schema name. */ - @Value("${mosip.kernel.xsdfile}") - private String schemaName; - - /** The xsd. */ - private byte[] xsd; - - /** - * Load XSD. - * - * @throws IOException Signals that an I/O exception has occurred. - */ - @PostConstruct - public void loadXSD() throws IOException { - try (InputStream xsdBytes = new URL(configServerFileStorageURL + schemaName).openStream()) { - xsd = IOUtils.toByteArray(xsdBytes); - } - } - - /** - * Method used for creating Cbeff XML. - * - * @param birList pass List of BIR for creating Cbeff data - * @return return byte array of XML data - * @throws Exception exception - */ - @Override - public byte[] createXML(List birList) throws Exception { - CbeffContainerImpl cbeffContainer = new CbeffContainerImpl(); - BIR bir = cbeffContainer.createBIRType(birList); - return CbeffValidator.createXMLBytes(bir, xsd); - } - - /** - * Method used for creating Cbeff XML with xsd. - * - * @param birList pass List of BIR for creating Cbeff data - * @param xsd byte array of XSD data - * @return return byte array of XML data - * @throws Exception Exception - */ - - @Override - public byte[] createXML(List birList, byte[] xsd) throws Exception { - CbeffContainerImpl cbeffContainer = new CbeffContainerImpl(); - BIR bir = cbeffContainer.createBIRType(birList); - return CbeffValidator.createXMLBytes(bir, xsd); - } - - /** - * Method used for updating Cbeff XML. - * - * @param birList pass List of BIR for creating Cbeff data - * @param fileBytes the file bytes - * @return return byte array of XML data - * @throws Exception Exception - */ - @Override - public byte[] updateXML(List birList, byte[] fileBytes) throws Exception { - CbeffContainerImpl cbeffContainer = new CbeffContainerImpl(); - BIR bir = cbeffContainer.updateBIRType(birList, fileBytes); - return CbeffValidator.createXMLBytes(bir, xsd); - } - - /** - * Method used for validating XML against XSD. - * - * @param xmlBytes byte array of XML data - * @param xsdBytes byte array of XSD data - * @return boolean if data is valid or not - * @throws Exception Exception - */ - @Override - public boolean validateXML(byte[] xmlBytes, byte[] xsdBytes) throws Exception { - CbeffContainerImpl cbeffContainer = new CbeffContainerImpl(); - return cbeffContainer.validateXML(xmlBytes, xsdBytes); - } - - /* - * (non-Javadoc) - * - * @see io.mosip.kernel.core.cbeffutil.spi.CbeffUtil#validateXML(byte[]) - */ - @Override - public boolean validateXML(byte[] xmlBytes) throws Exception { - return validateXML(xmlBytes, xsd); - } - - /** - * Method used for validating XML against XSD. - * - * @param fileBytes byte array of XML data - * @param type to be searched - * @param subType to be searched - * @return bdbMap Map of type and String of encoded biometric data - * @throws Exception Exception - */ - @Override - public Map getBDBBasedOnType(byte[] fileBytes, String type, String subType) throws Exception { - BIR bir = CbeffValidator.getBIRFromXML(fileBytes); - return CbeffValidator.getBDBBasedOnTypeAndSubType(bir, type, subType); - } - - /** - * Method used for getting list of BIR from XML bytes * - * - * @param xmlBytes byte array of XML data - * @return List of BIR data extracted from XML - * @throws Exception Exception - */ - @Override - public List getBIRDataFromXML(byte[] xmlBytes) throws Exception { - BIR bir = CbeffValidator.getBIRFromXML(xmlBytes); - return bir.getBirs(); - } - - /** - * Method used for getting Map of BIR from XML bytes with type and subType. - * - * @param xmlBytes byte array of XML data - * @param type type - * @param subType subType - * @return bdbMap Map of BIR data extracted from XML - * @throws Exception Exception - */ - @Override - public Map getAllBDBData(byte[] xmlBytes, String type, String subType) throws Exception { - BIR bir = CbeffValidator.getBIRFromXML(xmlBytes); - return CbeffValidator.getAllBDBData(bir, type, subType); - } - - /* - * (non-Javadoc) - * - * @see - * io.mosip.kernel.core.cbeffutil.spi.CbeffUtil#getBIRDataFromXMLType(byte[], - * java.lang.String) - */ - @Override - public List getBIRDataFromXMLType(byte[] xmlBytes, String type) throws Exception { - return CbeffValidator.getBIRDataFromXMLType(xmlBytes, type); - } - - /* - * public static void main(String arg[]) throws Exception { byte[] xsd = - * Files.readAllBytes(Paths.get("C:\\Users\\M1044287\\Desktop\\cbeff.xsd")); - * CbeffImpl cbeffImpl = new CbeffImpl(); List birs = - * cbeffImpl.getBIRDataFromXML(readCreatedXML()); - * - * - * byte[] ac = new CbeffImpl().createXML(birs, xsd); - * FileUtils.writeByteArrayToFile(new - * File("C:\\Users\\M1044287\\Desktop\\a.xml"), ac); - * - * } - * - * private static byte[] readCreatedXML() throws IOException { byte[] - * fileContent = - * Files.readAllBytes(Paths.get("C:\\Users\\M1044287\\Desktop\\cbeff-dev.xml")); - * return fileContent; } - */ - -} diff --git a/src/main/java/io/mosip/mimoto/service/impl/CredentialMatchingServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CredentialMatchingServiceImpl.java new file mode 100644 index 000000000..2d028f92c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/CredentialMatchingServiceImpl.java @@ -0,0 +1,802 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.CredentialDTO; +import io.mosip.mimoto.dto.CredentialSetInfo; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.DcqlQueryGroup; +import io.mosip.mimoto.dto.MatchingCredentialsDTO; +import io.mosip.mimoto.dto.MatchingCredentialsResponseDTO; +import io.mosip.mimoto.dto.mimoto.IssuerConfig; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.service.CredentialFormatHandlerFactory; +import io.mosip.mimoto.service.CredentialFormatHandler; +import io.mosip.mimoto.service.CredentialMatchingService; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.service.WalletCredentialService; +import io.mosip.mimoto.util.JwtUtils; +import io.mosip.mimoto.util.DcqlClaimSetHelper; +import io.mosip.mimoto.util.DcqlCredentialSetHelper; +import io.mosip.mimoto.util.DcqlMatchingHelper; +import io.mosip.openID4VP.authorizationRequest.presentationDefinition.*; +import io.mosip.openID4VP.dcql.evaluator.MatchingCredential; +import io.mosip.openID4VP.dcql.evaluator.MatchingCredentialsResult; +import io.mosip.openID4VP.dcql.evaluator.QueryMatchResult; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.CredentialSetQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.helper.DCQLHelper; +import io.mosip.openID4VP.wallet.Credential; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.stereotype.Service; + +import static io.mosip.mimoto.exception.ErrorConstants.INVALID_REQUEST; +import static io.mosip.mimoto.exception.ErrorConstants.UNSUPPORTED_FORMAT; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +@Slf4j +@Service +public class CredentialMatchingServiceImpl implements CredentialMatchingService { + + private static final String JSON_PATH_PREFIX = "$."; + private static final String LDP_VC_FORMAT = "ldp_vc"; + private static final String PROOF_TYPE_KEY = "proof_type"; + private static final String SD_JWT_ALG_VALUES_KEY = "sd-jwt_alg_values"; + private static final String CREDENTIAL_SUBJECT_PREFIX = "credentialSubject."; + private static final String ALG = "alg"; + private static final String CREDENTIAL_SUBJECT = "credentialSubject"; + private static final String SD = "_sd"; + + private final ObjectMapper objectMapper; + + private final IssuersService issuersService; + + private final OpenID4VPService openID4VPService; + + private final WalletCredentialService walletCredentialService; + + private final CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + private final DCQLHelper dcqlHelper = new DCQLHelper(); + + public CredentialMatchingServiceImpl(ObjectMapper objectMapper, IssuersService issuersService, OpenID4VPService openID4VPService, WalletCredentialService walletCredentialService, CredentialFormatHandlerFactory credentialFormatHandlerFactory) { + this.objectMapper = objectMapper; + this.issuersService = issuersService; + this.openID4VPService = openID4VPService; + this.walletCredentialService = walletCredentialService; + this.credentialFormatHandlerFactory = credentialFormatHandlerFactory; + } + + + @Override + public MatchingCredentialsDTO getMatchingCredentials(VerifiablePresentationSessionData sessionData, String walletId, String base64Key) throws ApiNotAccessibleException, IOException { + validateMatchingCredentialsRequest(sessionData, walletId); + + List decryptedCredentials = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + if (sessionData.isDcql()) { + return matchWithDcqlQuery(sessionData, walletId, decryptedCredentials); + } + return matchWithPresentationDefinition(sessionData, walletId, base64Key, decryptedCredentials); + } + + private void validateMatchingCredentialsRequest(VerifiablePresentationSessionData sessionData, String walletId) { + if (sessionData == null + || StringUtils.isBlank(sessionData.getPresentationId()) + || StringUtils.isBlank(sessionData.getAuthorizationRequest())) { + throw new IllegalArgumentException("Session data cannot be null or empty"); + } + if (StringUtils.isBlank(walletId)) { + throw new IllegalArgumentException("Wallet ID cannot be null or empty"); + } + } + + private MatchingCredentialsDTO matchWithPresentationDefinition( + VerifiablePresentationSessionData sessionData, + String walletId, + String base64Key, + List decryptedCredentials) throws ApiNotAccessibleException, IOException { + + // Extract presentation definition from the session data + PresentationDefinition presentationDefinition = openID4VPService.resolvePresentationDefinition( + sessionData.getPresentationId(), sessionData.getAuthorizationRequest(), sessionData.isVerifierClientPreregistered()); + + validateInputParameters(presentationDefinition, base64Key); + + if (decryptedCredentials.isEmpty()) { + MatchingCredentialsResponseDTO emptyResponse = createEmptyResponseWithMissingClaims(presentationDefinition); + return MatchingCredentialsDTO.builder() + .matchingCredentialsResponse(emptyResponse) + .matchingCredentials(new ArrayList<>()) + .build(); + } + + List descriptors = presentationDefinition.getInputDescriptors(); + Map> matchingCredentialsByDescriptor = new HashMap<>(); + Map credentialToInputDescriptor = new HashMap<>(); + Set missingClaims = new HashSet<>(); + + IntStream.range(0, descriptors.size()) + .forEach(i -> { + InputDescriptor descriptor = descriptors.get(i); + List descriptorMatches = decryptedCredentials.stream() + .filter(decrypted -> matchesInputDescriptor(decrypted.getCredential(), descriptor)) + .collect(Collectors.toList()); + + List matches = descriptorMatches.stream() + .map(this::buildAvailableCredential) + .collect(Collectors.toList()); + + if (!matches.isEmpty()) { + descriptorMatches.forEach(dto -> credentialToInputDescriptor.put(dto.getId(), descriptor.getId())); + matchingCredentialsByDescriptor.put(i, matches); + } else { + boolean hasFormatMatch = decryptedCredentials.stream() + .anyMatch(decrypted -> matchesFormat(decrypted.getCredential(), descriptor.getFormat())); + if (hasFormatMatch) { + missingClaims.addAll(extractClaimsFromInputDescriptor(descriptor)); + } else { + missingClaims.addAll(extractFormatConstraintKeys(descriptor)); + } + } + }); + + // Flatten all matching credentials into a single list, removing duplicates by credential ID + Set addedCredentialIds = new HashSet<>(); + List availableCredentials = matchingCredentialsByDescriptor.values().stream() + .flatMap(List::stream) + .filter(credential -> addedCredentialIds.add(credential.getCredentialId())) + .collect(Collectors.toList()); + + MatchingCredentialsResponseDTO matchingCredentialsResponse = MatchingCredentialsResponseDTO.builder() + .availableCredentials(availableCredentials) + .missingClaims(missingClaims) + .build(); + + Set matchedCredentialIds = availableCredentials.stream() + .map(CredentialDTO::getCredentialId) + .collect(Collectors.toSet()); + + List matchingCredentials = decryptedCredentials.stream() + .filter(credential -> matchedCredentialIds.contains(credential.getId())) + .peek(credential -> credential.setIdentifier(credentialToInputDescriptor.get(credential.getId()))) + .collect(Collectors.toList()); + + return MatchingCredentialsDTO.builder() + .matchingCredentialsResponse(matchingCredentialsResponse) + .matchingCredentials(matchingCredentials) + .build(); + } + + private MatchingCredentialsDTO matchWithDcqlQuery( + VerifiablePresentationSessionData sessionData, + String walletId, + List decryptedCredentials) throws ApiNotAccessibleException, IOException { + + DCQLQuery dcqlQuery = openID4VPService.resolveDcqlQuery( + sessionData.getPresentationId(), + sessionData.getAuthorizationRequest(), + sessionData.isVerifierClientPreregistered()); + + if (dcqlQuery == null) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Authorization request does not contain a DCQL query"); + } + + Map credentialsById = decryptedCredentials.stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, dto -> dto, (a, b) -> a, LinkedHashMap::new)); + + DCQLQuery normalizedDcqlQuery = DcqlMatchingHelper.normalizeDcqlQuery(dcqlQuery); + + List credentialWithCredentialFormat = + DcqlMatchingHelper.constructCredentialWithCredentialFormat(decryptedCredentials, objectMapper); + + if (credentialWithCredentialFormat.isEmpty() && !decryptedCredentials.isEmpty()) { + log.warn("matchWithDcqlQuery: no wallet credentials could be mapped for inji-openid4vp evaluation; " + + "SD-JWT credentials must be stored as the raw token string"); + } + + MatchingCredentialsResult evaluationResult = + dcqlHelper.getMatchingCredentials(credentialWithCredentialFormat, normalizedDcqlQuery); + + List queryGroups = new ArrayList<>(); + Map matchedById = new LinkedHashMap<>(); + + for (CredentialQuery credentialQuery : normalizedDcqlQuery.getCredentials()) { + QueryMatchResult queryMatch = evaluationResult.getQueryMatches().get(credentialQuery.getId()); + List matches = resolveMatchedCredentials( + credentialsById, credentialQuery, queryMatch, matchedById); + + Set missingClaims = matches.isEmpty() + ? DcqlMatchingHelper.resolveMissingClaims(credentialQuery, queryMatch) + : Collections.emptySet(); + + // Index the library's per-credential matched claims so sdClaims reflects only + // what the library evaluated as matched — not every claim in the query. + Map> matchedClaimsByCredId = buildMatchedClaimsIndex(queryMatch); + + List credentialDTOs = matches.stream() + .map(match -> buildAvailableCredential( + match, + matchedClaimsByCredId.getOrDefault(match.getId(), Collections.emptyList()))) + .collect(Collectors.toList()); + + queryGroups.add(DcqlQueryGroup.builder() + .queryId(credentialQuery.getId()) + .required(true) + .multiple(credentialQuery.getMultiple()) + .availableCredentials(credentialDTOs) + .missingClaims(missingClaims) + .build()); + } + + List credentialSets = DcqlCredentialSetHelper.resolveEffectiveCredentialSets(dcqlQuery) + .stream() + .map(this::toCredentialSetInfo) + .collect(Collectors.toList()); + + log.info("matchWithDcqlQuery: walletId={}, queries={}, credentialSets={}, totalMatched={}, dcqlSuccess={}", + walletId, queryGroups.size(), credentialSets.size(), matchedById.size(), evaluationResult.getSuccess()); + + MatchingCredentialsResponseDTO matchingCredentialsResponse = MatchingCredentialsResponseDTO.builder() + .queryGroups(queryGroups) + .credentialSets(credentialSets) + .build(); + + return MatchingCredentialsDTO.builder() + .matchingCredentialsResponse(matchingCredentialsResponse) + .matchingCredentials(new ArrayList<>(matchedById.values())) + .build(); + } + + private List resolveMatchedCredentials( + Map credentialsById, + CredentialQuery credentialQuery, + QueryMatchResult queryMatch, + Map matchedById) { + if (queryMatch == null || queryMatch.getMatchingCredentials() == null) { + return List.of(); + } + + List matches = new ArrayList<>(); + for (MatchingCredential matchingCredential : queryMatch.getMatchingCredentials()) { + DecryptedCredentialDTO dto = credentialsById.get(matchingCredential.getCredentialId()); + if (dto == null) { + continue; + } + dto.setIdentifier(credentialQuery.getId()); + matchedById.putIfAbsent(dto.getId(), dto); + matches.add(dto); + } + return matches; + } + + private CredentialSetInfo toCredentialSetInfo(CredentialSetQuery credentialSetQuery) { + return CredentialSetInfo.builder() + .required(credentialSetQuery.getRequired()) + .options(credentialSetQuery.getOptions()) + .build(); + } + + private void validateInputParameters(PresentationDefinition presentationDefinition, String base64Key) throws IllegalArgumentException { + if (base64Key == null || base64Key.trim().isEmpty()) { + throw new IllegalArgumentException("Base64 key cannot be null or empty"); + } + + if (presentationDefinition == null) { + throw new IllegalArgumentException("Presentation definition cannot be null"); + } + + if (presentationDefinition.getInputDescriptors().isEmpty()) { + throw new IllegalArgumentException("Presentation definition must contain at least one input descriptor"); + } + + IntStream.range(0, presentationDefinition.getInputDescriptors().size()) + .filter(i -> { + InputDescriptor descriptor = presentationDefinition.getInputDescriptors().get(i); + return descriptor.getId().trim().isEmpty(); + }) + .findFirst() + .ifPresent(i -> { throw new IllegalArgumentException("Input descriptor at index " + i + " must have a valid ID"); }); + } + + private MatchingCredentialsResponseDTO createEmptyResponseWithMissingClaims(PresentationDefinition presentationDefinition) { + log.info("No credentials found for wallet"); + Set missingClaims = presentationDefinition.getInputDescriptors().stream() + .flatMap(descriptor -> extractFormatConstraintKeys(descriptor).stream()) + .collect(Collectors.toSet()); + return MatchingCredentialsResponseDTO.builder() + .availableCredentials(Collections.emptyList()) + .missingClaims(missingClaims) + .build(); + } + + private List extractClaimsFromInputDescriptor(InputDescriptor inputDescriptor) { + return extractClaimsFromFields(inputDescriptor.getConstraints().getFields(), false); + } + + /** + * Common method to extract claims from an array of fields. + * + * @param fields List of Fields objects to extract claims from + * @param deduplicate Whether to deduplicate claims using LinkedHashSet + * @return List of extracted claim keys + */ + private List extractClaimsFromFields(List fields, boolean deduplicate) { + if (fields == null) { + return Collections.emptyList(); + } + + Stream claimsStream = fields.stream() + .filter(Objects::nonNull) + .filter(field -> !field.getPath().isEmpty()) + .flatMap(field -> field.getPath().stream()) + .map(this::extractClaimKeyFromPath) + .filter(Objects::nonNull) + .filter(claim -> !claim.isBlank()); + + if (deduplicate) { + return claimsStream.distinct().collect(Collectors.toList()); + } else { + return claimsStream.collect(Collectors.toList()); + } + } + + private boolean matchesInputDescriptor(VCCredentialResponse vc, InputDescriptor inputDescriptor) { + Map>> formatToCheck = inputDescriptor.getFormat(); + + if (!matchesFormat(vc, formatToCheck)) { + return false; + } + + if (inputDescriptor.getConstraints().getFields() != null) { + return matchesConstraints(vc, inputDescriptor.getConstraints()); + } + return true; + } + + private boolean matchesFormat(VCCredentialResponse vc, Map>> descriptorFormat) { + if (descriptorFormat == null) { + return true; + } + + String vcFormat = vc.getFormat(); + + if (CredentialFormat.isSdJwt(vcFormat) && descriptorFormat.containsKey(vcFormat.toLowerCase())) { + return matchesSdJwtAlgorithm(vc, descriptorFormat, vcFormat.toLowerCase()); + } + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(vcFormat) && descriptorFormat.containsKey(LDP_VC_FORMAT)) { + return matchesLdpVcFormat(vc, descriptorFormat); + } + return false; + } + + private boolean matchesLdpVcFormat(VCCredentialResponse vc, Map>> descriptorFormat) { + Map> ldpVcFormat = descriptorFormat.get(LDP_VC_FORMAT); + + if (ldpVcFormat == null) { + return false; + } + + if (!ldpVcFormat.containsKey(PROOF_TYPE_KEY)) { + return false; + } + + VCCredentialProperties ldpCredential = objectMapper.convertValue(vc.getCredential(), VCCredentialProperties.class); + String vcProofType = ldpCredential.getProof() != null ? ldpCredential.getProof().getType() : null; + List requiredProofTypes = ldpVcFormat.get(PROOF_TYPE_KEY); + + if (requiredProofTypes == null || requiredProofTypes.isEmpty()) { + return true; + } + + return vcProofType != null && requiredProofTypes.contains(vcProofType); + } + + private boolean matchesSdJwtAlgorithm(VCCredentialResponse vc, Map>> requestFormat, String formatKey) { + if (vc.getCredential() == null || !(vc.getCredential() instanceof String sdJwtString)) { + return false; + } + + Map> sdJwtFormat = requestFormat.get(formatKey); + List requestAlgorithms = sdJwtFormat.get(SD_JWT_ALG_VALUES_KEY); + if (requestAlgorithms != null) { + return requestAlgorithms.contains(extractSdJwtAlgorithm(sdJwtString)); + } + return true; // No sd-jwt_alg_values constraint → any algorithm is acceptable + } + + private String extractSdJwtAlgorithm(String sdJwtString) { + if (sdJwtString == null || sdJwtString.trim().isEmpty()) { + return null; + } + try { + Map header = JwtUtils.parseJwtHeader(sdJwtString); + return (String) header.get(ALG); + } catch (Exception e) { + log.warn("Failed to extract algorithm from SD-JWT header", e); + return null; + } + } + + private boolean matchesConstraints(VCCredentialResponse vc, Constraints constraints) { + if (constraints.getFields() == null) { + return true; + } + + return constraints.getFields().stream().allMatch(field -> { + if (field.getPath().isEmpty()) { + return true; + } + return field.getPath().stream().anyMatch(path -> matchesFieldPath(vc, path, field.getFilter())); + }); + } + + private boolean matchesFieldPath(VCCredentialResponse vc, String path, Filter filter) { + Object credentialData = getCredentialData(vc); + + List matches = evaluateJsonPath(path, credentialData); + + if (matches.isEmpty()) { + return false; + } + + return matches.stream().anyMatch(match -> matchesFilter(match, filter)); + } + + private Object getCredentialData(VCCredentialResponse vc) { + String format = vc.getFormat(); + CredentialFormatHandler credentialFormatHandler = credentialFormatHandlerFactory.getHandler(vc.getFormat()); + + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(format)) { + return credentialFormatHandler.extractAllCredentialProperties(vc); + } else if (CredentialFormat.isSdJwt(format)) { + Map extractedMap = credentialFormatHandler.extractAllCredentialProperties(vc); + if (extractedMap == null) { + return Collections.emptyMap(); + } + Map credentialClaimsMap = new HashMap<>(); + mergeClaimProperties(credentialClaimsMap, extractedMap.get("publicClaims")); + Map sdClaimValues = asStringObjectMap(extractedMap.get("sdClaimValues")); + if (sdClaimValues != null && !sdClaimValues.isEmpty()) { + mergeClaimProperties(credentialClaimsMap, sdClaimValues); + } else { + // Fallback for legacy handler responses: existence checks only. + mergeClaimProperties(credentialClaimsMap, extractedMap.get("sdClaims")); + } + return credentialClaimsMap; + } else { + throw new InvalidRequestException(UNSUPPORTED_FORMAT.getErrorCode(), "Unsupported credential format: " + format); + } + } + + private void mergeClaimProperties(Map target, Object section) { + Map properties = asStringObjectMap(section); + if (properties != null) { + target.putAll(properties); + } + } + + @SuppressWarnings("unchecked") + private Map asStringObjectMap(Object section) { + if (!(section instanceof Map rawMap)) { + return null; + } + Map properties = new LinkedHashMap<>(); + for (Map.Entry entry : rawMap.entrySet()) { + if (entry.getKey() instanceof String key) { + properties.put(key, entry.getValue()); + } + } + return properties; + } + + private boolean matchesFilter(Object match, Filter filter) { + if (filter == null) { + return true; + } + + String matchValue = match.toString(); + return matchValue.contains(filter.getPattern()); + + } + + private List evaluateJsonPath(String path, Object json) { + if (path == null || path.trim().isEmpty()) { + return Collections.emptyList(); + } + + if (!path.startsWith(JSON_PATH_PREFIX)) { + return Collections.emptyList(); + } + + if (json == null) { + return Collections.emptyList(); + } + + try { + Object result = JsonPath.read(json, path); + + if (result == null) { + return Collections.emptyList(); + } + + if (result instanceof List) { + return (List) result; + } + + return Collections.singletonList(result); + + } catch (PathNotFoundException e) { + log.debug("Path not found in JSON: {}", path); + return Collections.emptyList(); + } + } + + private List extractRequiredClaims(PresentationDefinition presentationDefinition) { + + List allFields = presentationDefinition.getInputDescriptors().stream() + .filter(id -> id.getConstraints().getFields() != null) + .flatMap(id -> id.getConstraints().getFields().stream()) + .collect(Collectors.toList()); + + return extractClaimsFromFields(allFields, true); + } + + private String extractClaimKeyFromPath(String path) { + if (path == null || path.isBlank()) { + return null; + } + int lastDot = path.lastIndexOf('.'); + String tail = lastDot >= 0 ? path.substring(lastDot + 1) : path; + if (tail.startsWith("$")) { + tail = tail.substring(1); + } + return tail; + } + + private CredentialDTO buildAvailableCredential(DecryptedCredentialDTO decryptedCredentialDTO) { + String issuerId = decryptedCredentialDTO.getCredentialMetadata().getIssuerId(); + String credentialType = decryptedCredentialDTO.getCredentialMetadata().getCredentialType(); + + String credentialTypeDisplayName = "Unknown Credential"; + String credentialTypeLogo = null; + try { + IssuerConfig issuerConfig = issuersService.getIssuerConfig(issuerId, credentialType); + if (issuerConfig != null) { + VerifiableCredentialResponseDTO credentialResponse = VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, "en", decryptedCredentialDTO.getId()); + credentialTypeDisplayName = credentialResponse.getCredentialTypeDisplayName(); + credentialTypeLogo = credentialResponse.getCredentialTypeLogo(); + } + } catch (InvalidIssuerIdException | ApiNotAccessibleException e) { + log.warn("Failed to fetch issuer config for issuerId: {}, credentialType: {}", issuerId, credentialType, e); + } + + String format = decryptedCredentialDTO.getCredential().getFormat(); + List publicClaims = null; + List sdClaims = null; + + if (CredentialFormat.isSdJwt(format)) { + CredentialFormatHandler credentialFormatHandler = credentialFormatHandlerFactory.getHandler(format); + Map> allClaims = (Map>) credentialFormatHandler.extractAllCredentialProperties(decryptedCredentialDTO.getCredential()); + + Map publicClaimsMap = allClaims.get("publicClaims"); + Map sdClaimsMap = allClaims.get("sdClaims"); + + publicClaims = publicClaimsMap != null ? extractPublicClaimPaths(publicClaimsMap) : new ArrayList<>(); + sdClaims = sdClaimsMap != null ? extractSdClaimPaths(sdClaimsMap) : new ArrayList<>(); + } + + return CredentialDTO.builder() + .credentialId(decryptedCredentialDTO.getId()) + .credentialTypeDisplayName(credentialTypeDisplayName) + .credentialTypeLogo(credentialTypeLogo) + .format(format) + .claims(publicClaims) + .sdClaims(sdClaims) + .build(); + } + + /** + * Indexes the library's per-credential evaluation result into a map of credentialId → matched ClaimsQuery list. + * This is the source of truth for which claims were actually satisfied, accounting for {@code claim_sets}. + */ + private Map> buildMatchedClaimsIndex(QueryMatchResult queryMatch) { + if (queryMatch == null || queryMatch.getMatchingCredentials() == null) { + return Collections.emptyMap(); + } + Map> index = new LinkedHashMap<>(); + for (MatchingCredential mc : queryMatch.getMatchingCredentials()) { + if (mc.getCredentialId() != null) { + index.putIfAbsent(mc.getCredentialId(), + mc.getMatchingClaims() != null ? mc.getMatchingClaims() : Collections.emptyList()); + } + } + return index; + } + + /** + * DCQL-aware variant: builds a {@link CredentialDTO} whose {@code sdClaims} list is restricted + * to the claims the library evaluated as matched ({@code MatchingCredential.getMatchingClaims()}), + * not every claim in the query. This respects {@code claim_sets} resolution done by the library. + */ + private CredentialDTO buildAvailableCredential(DecryptedCredentialDTO decryptedCredentialDTO, List matchedClaims) { + String issuerId = decryptedCredentialDTO.getCredentialMetadata().getIssuerId(); + String credentialType = decryptedCredentialDTO.getCredentialMetadata().getCredentialType(); + + String credentialTypeDisplayName = "Unknown Credential"; + String credentialTypeLogo = null; + try { + IssuerConfig issuerConfig = issuersService.getIssuerConfig(issuerId, credentialType); + if (issuerConfig != null) { + VerifiableCredentialResponseDTO credentialResponse = VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, "en", decryptedCredentialDTO.getId()); + credentialTypeDisplayName = credentialResponse.getCredentialTypeDisplayName(); + credentialTypeLogo = credentialResponse.getCredentialTypeLogo(); + } + } catch (InvalidIssuerIdException | ApiNotAccessibleException e) { + log.warn("Failed to fetch issuer config for issuerId: {}, credentialType: {}", issuerId, credentialType, e); + } + + String format = decryptedCredentialDTO.getCredential().getFormat(); + List publicClaims = null; + List sdClaims = null; + + if (CredentialFormat.isSdJwt(format)) { + CredentialFormatHandler credentialFormatHandler = credentialFormatHandlerFactory.getHandler(format); + Map> allClaims = (Map>) credentialFormatHandler.extractAllCredentialProperties(decryptedCredentialDTO.getCredential()); + + Map publicClaimsMap = allClaims.get("publicClaims"); + Map sdClaimsMap = allClaims.get("sdClaims"); + + publicClaims = publicClaimsMap != null ? extractPublicClaimPaths(publicClaimsMap) : new ArrayList<>(); + sdClaims = sdClaimsMap != null ? extractDcqlFilteredSdClaimPaths(sdClaimsMap, matchedClaims) : new ArrayList<>(); + } + + return CredentialDTO.builder() + .credentialId(decryptedCredentialDTO.getId()) + .credentialTypeDisplayName(credentialTypeDisplayName) + .credentialTypeLogo(credentialTypeLogo) + .format(format) + .claims(publicClaims) + .sdClaims(sdClaims) + .build(); + } + + private List extractPublicClaimPaths(Map publicClaimsMap) { + List paths = new ArrayList<>(); + Object credentialSubject = publicClaimsMap.get(CREDENTIAL_SUBJECT); + if (credentialSubject instanceof Map) { + Map csMap = (Map) credentialSubject; + collectPaths(csMap, "$", paths); + } else { + List metadataKeys = Arrays.asList("vct", "cnf", "iss", "sub", "aud", "exp", "nbf", "iat", "jti", SD, "_sd_alg", "id"); + Map filteredMap = new HashMap<>(publicClaimsMap); + metadataKeys.forEach(filteredMap::remove); + collectPaths(filteredMap, "$", paths); + } + return paths; + } + + private List extractSdClaimPaths(Map sdClaimsMap) { + List paths = new ArrayList<>(); + for (String key : sdClaimsMap.keySet()) { + String cleanKey = key.startsWith(CREDENTIAL_SUBJECT_PREFIX) + ? key.substring(CREDENTIAL_SUBJECT_PREFIX.length()) + : key; + paths.add(JSON_PATH_PREFIX + cleanKey); + } + return paths; + } + + /** + * Filters SD-JWT claim paths to only those present in the library's evaluated + * {@code MatchingCredential.getMatchingClaims()} list. This ensures {@code sdClaims} + * reflects exactly what the library determined was matched — including correct + * {@code claim_sets} resolution — per the DCQL spec (OpenID4VP §6). + */ + private List extractDcqlFilteredSdClaimPaths(Map sdClaimsMap, List matchedClaims) { + if (matchedClaims == null || matchedClaims.isEmpty()) { + return new ArrayList<>(); + } + + Set matchedPaths = matchedClaims.stream() + .filter(cq -> cq.getPath() != null && !cq.getPath().isEmpty()) + .map(cq -> DcqlClaimSetHelper.buildJsonPath(cq.getPath())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + List paths = new ArrayList<>(); + for (String key : sdClaimsMap.keySet()) { + String cleanKey = key.startsWith(CREDENTIAL_SUBJECT_PREFIX) + ? key.substring(CREDENTIAL_SUBJECT_PREFIX.length()) + : key; + String jsonPath = JSON_PATH_PREFIX + cleanKey; + if (matchedPaths.contains(jsonPath)) { + paths.add(jsonPath); + } + } + return paths; + } + + private void collectPaths(Map publicClaimsMap, String prefix, List paths) { + for (Map.Entry entry : publicClaimsMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // Skip _sd keys + if (SD.equals(key)) { + continue; + } + + String currentPath = prefix + "." + key; + + if (value instanceof Map) { + collectPaths((Map) value, currentPath, paths); + } else if (value instanceof List listValue) { + if (hasUniformKeys(listValue)) { + paths.add(currentPath); + } else { + paths.add(currentPath); + for (Object item : listValue) { + if (item instanceof Map mapItem) { + collectPaths((Map) mapItem, currentPath, paths); + } + } + } + } else { + paths.add(currentPath); + } + } + } + + private Set extractFormatConstraintKeys(InputDescriptor descriptor) { + Map>> format = descriptor.getFormat(); + if (format == null || format.isEmpty()) { + return Collections.singleton(descriptor.getId()); + } + + Set result = new HashSet<>(); + if (format.containsKey(CredentialFormat.VC_SD_JWT.getFormat()) || format.containsKey(CredentialFormat.DC_SD_JWT.getFormat())) { + result.add(SD_JWT_ALG_VALUES_KEY); + } + if (format.containsKey(LDP_VC_FORMAT)) { + result.add(PROOF_TYPE_KEY); + } + + return result.isEmpty() ? Collections.singleton(descriptor.getId()) : result; + } + + private boolean hasUniformKeys(List list) { + List> keySets = list.stream() + .filter(item -> item instanceof Map) + .map(item -> { + Map m = (Map) item; + return new HashSet(m.keySet()); + }) + .collect(Collectors.toList()); + + if (keySets.size() < 2 || keySets.size() != list.size()) { + return false; + } + + Set intersectionKeys = new HashSet<>(keySets.getFirst()); + keySets.forEach(intersectionKeys::retainAll); + + return !intersectionKeys.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/CredentialServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CredentialServiceImpl.java new file mode 100644 index 000000000..f77318649 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/CredentialServiceImpl.java @@ -0,0 +1,234 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.model.QRCodeType; +import io.mosip.mimoto.model.VerifiableCredential; +import io.mosip.mimoto.repository.WalletCredentialsRepository; +import io.mosip.mimoto.service.CredentialPDFGeneratorService; +import io.mosip.mimoto.service.CredentialService; +import io.mosip.mimoto.service.CredentialVerifierService; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.service.DataProtectionService; +import io.mosip.mimoto.service.VCDownloadHandler; +import io.mosip.mimoto.service.VCDownloadHandlerFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.util.UUID; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +@Slf4j +@Service +public class CredentialServiceImpl implements CredentialService { + + private final ObjectMapper objectMapper; + private final DataProtectionService dataProtectionService; + private final WalletCredentialsRepository walletCredentialsRepository; + private final IssuersService issuersService; + private final CredentialVerifierService credentialVerifierService; + private final CredentialPDFGeneratorService credentialPDFGeneratorService; + private final DataShareServiceImpl dataShareService; + private final VCDownloadHandlerFactory vcDownloadHandlerFactory; + + public CredentialServiceImpl( + ObjectMapper objectMapper, + DataProtectionService dataProtectionService, + WalletCredentialsRepository walletCredentialsRepository, + IssuersService issuersService, + CredentialVerifierService credentialVerifierService, + CredentialPDFGeneratorService credentialPDFGeneratorService, + DataShareServiceImpl dataShareService, + VCDownloadHandlerFactory vcDownloadHandlerFactory) { + + this.objectMapper = objectMapper; + this.dataProtectionService = dataProtectionService; + this.walletCredentialsRepository = walletCredentialsRepository; + this.issuersService = issuersService; + this.credentialVerifierService = credentialVerifierService; + this.credentialPDFGeneratorService = credentialPDFGeneratorService; + this.dataShareService = dataShareService; + this.vcDownloadHandlerFactory = vcDownloadHandlerFactory; + } + + + @Override + public ByteArrayInputStream downloadCredentialAsPDF(String issuerId, String credentialConfigurationId, TokenResponseDTO tokenResponse, String credentialValidity, String locale) throws Exception { + IssuerDTO issuerDTO = issuersService.getIssuerDetails(issuerId); + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = issuersService.getIssuerWellKnownResponse(issuerDTO.getCredential_issuer_host()); + CredentialsSupportedResponse credentialsSupportedResponse = credentialIssuerWellKnownResponse.getCredentialConfigurationsSupported().get(credentialConfigurationId); + + VCDownloadHandler processor = vcDownloadHandlerFactory.getHandler(credentialIssuerWellKnownResponse.getVersion()); + VCCredentialResponse vcCredentialResponse = processor.downloadCredential(issuerDTO, credentialConfigurationId, credentialIssuerWellKnownResponse, tokenResponse, null, null, false); + + boolean verificationStatus = verifyCredential(vcCredentialResponse, issuerId, credentialConfigurationId); + if (verificationStatus) { + String dataShareUrl = QRCodeType.OnlineSharing.equals(issuerDTO.getQr_code_type()) ? dataShareService.storeDataInDataShare(objectMapper.writeValueAsString(vcCredentialResponse), credentialValidity) : ""; + return credentialPDFGeneratorService.generatePdfForVerifiableCredential(credentialConfigurationId, vcCredentialResponse, issuerDTO, credentialsSupportedResponse, dataShareUrl, credentialValidity, locale); + } + throw new VCVerificationException(SIGNATURE_VERIFICATION_EXCEPTION.getErrorCode(), SIGNATURE_VERIFICATION_EXCEPTION.getErrorMessage()); + } + + /** + * Download credential and stores a credential using the provided token and parameters. + * + * @param tokenResponse The token response containing the access token. + * @param credentialConfigurationId The type of the credential. + * @param walletId The ID of the wallet. + * @param base64Key The Base64-encoded wallet key. + * @param issuerId The ID of the issuer. + * @param locale The locale for the response. + * @return The stored VerifiableCredential response. + * @throws InvalidRequestException If input parameters are invalid. + * @throws CredentialProcessingException If processing fails. + * @throws ExternalServiceUnavailableException If an external service is unavailable. + * @throws VCVerificationException If credential verification fails. + * @throws InvalidCredentialResourceException If the credential resource is invalid. + */ + public VerifiableCredentialResponseDTO downloadCredentialAndStoreInDB( + TokenResponseDTO tokenResponse, String credentialConfigurationId, String walletId, + String base64Key, String issuerId, String locale) + throws InvalidRequestException, CredentialProcessingException, ExternalServiceUnavailableException, VCVerificationException, InvalidCredentialResourceException { + + // Validate inputs + validateInputs(tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId); + + // Fetch issuer configuration + IssuerConfig issuerConfig = fetchIssuerConfig(issuerId, credentialConfigurationId); + + // Download credential from issuer + VCDownloadHandler processor = vcDownloadHandlerFactory.getHandler(issuerConfig.getWellKnownResponse().getVersion()); + VCCredentialResponse vcCredentialResponse = processor.downloadCredential(issuerConfig.getIssuerDTO(), credentialConfigurationId, issuerConfig.getWellKnownResponse(), tokenResponse, walletId, base64Key, true); + + // Verify credential + boolean verificationStatus = verifyCredential(vcCredentialResponse, issuerId, credentialConfigurationId); + if (!verificationStatus) { + log.error("Signature verification failed for issuerId: {}, credentialConfigurationId: {}", issuerId, credentialConfigurationId); + throw new VCVerificationException( + SIGNATURE_VERIFICATION_EXCEPTION.getErrorCode(), + SIGNATURE_VERIFICATION_EXCEPTION.getErrorMessage()); + } + + // Serialize, encrypt and store credential + String encryptedCredentialData = processAndEncryptCredential(vcCredentialResponse, base64Key, issuerId, credentialConfigurationId); + VerifiableCredential savedCredential = saveCredential(walletId, encryptedCredentialData, issuerId, credentialConfigurationId); + + return VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, locale, savedCredential.getId()); + } + + /** + * Validates input parameters for credential download. + */ + private void validateInputs(TokenResponseDTO tokenResponse, String credentialConfigurationId, + String walletId, String base64Key, String issuerId) throws InvalidRequestException { + if (tokenResponse == null || StringUtils.isBlank(tokenResponse.getAccess_token())) { + log.error("Invalid token response: null or missing access token"); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Token response or access token cannot be null"); + } + if (StringUtils.isBlank(credentialConfigurationId)) { + log.error("Invalid credential type: null or blank"); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Credential configuration id cannot be null or blank"); + } + if (StringUtils.isBlank(walletId)) { + log.error("Invalid wallet ID: null or blank"); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Wallet ID cannot be null or blank"); + } + if (StringUtils.isBlank(base64Key)) { + log.error("Invalid wallet key: null or blank"); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Wallet key cannot be null or blank"); + } + if (StringUtils.isBlank(issuerId)) { + log.error("Invalid issuer ID: null or blank"); + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Issuer ID cannot be null or blank"); + } + } + + /** + * Fetches issuer configuration. + */ + private IssuerConfig fetchIssuerConfig(String issuerId, String credentialConfigurationId) throws CredentialProcessingException { + try { + return issuersService.getIssuerConfig(issuerId, credentialConfigurationId); + } catch (Exception e) { + log.error("Failed to fetch issuer config for issuerId: {}", issuerId, e); + throw new CredentialProcessingException( + CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), + "Unable to fetch issuer configuration", e); + } + } + + /** + * Verifies credential signature. + */ + private boolean verifyCredential(VCCredentialResponse vcCredentialResponse, String issuerId, String credentialConfigurationId) + throws VCVerificationException { + try { + return credentialVerifierService.verify(vcCredentialResponse); + } catch (VCVerificationException | JsonProcessingException e) { + log.error("Credential verification failed for issuerId: {}, credentialConfigurationId: {}", issuerId, credentialConfigurationId, e); + throw new VCVerificationException( + SIGNATURE_VERIFICATION_EXCEPTION.getErrorCode(), + "Credential verification failed"); + } + } + + /** + * Processes and encrypts credential data. + */ + private String processAndEncryptCredential(VCCredentialResponse vcCredentialResponse, String base64Key, + String issuerId, String credentialConfigurationId) throws CredentialProcessingException { + try { + String vcResponseAsJsonString = objectMapper.writeValueAsString(vcCredentialResponse); + return dataProtectionService.encryptCredential(vcResponseAsJsonString, base64Key); + } catch (JsonProcessingException e) { + log.error("Failed to serialize credential response for issuerId: {}, credentialConfigurationId: {}", issuerId, credentialConfigurationId, e); + throw new CredentialProcessingException( + CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), + "Unable to serialize credential response", e); + } catch (Exception e) { + log.error("Failed to encrypt credential for issuerId: {}, credentialConfigurationId: {}", issuerId, credentialConfigurationId, e); + throw new CredentialProcessingException( + CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), + "Unable to encrypt credential data", e); + } + } + + /** + * Saves the credential to the repository. + * + * @param walletId The wallet ID. + * @param encryptedCredential The encrypted credential data. + * @param issuerId The issuer ID. + * @param credentialConfigurationId The credential configuration id. + * @return The stored VerifiableCredential. + */ + private VerifiableCredential saveCredential(String walletId, String encryptedCredential, String issuerId, + String credentialConfigurationId) throws CredentialProcessingException { + CredentialMetadata credentialMetadata = new CredentialMetadata(); + credentialMetadata.setIssuerId(issuerId); + credentialMetadata.setCredentialType(credentialConfigurationId); + + VerifiableCredential verifiableCredential = new VerifiableCredential(); + verifiableCredential.setId(UUID.randomUUID().toString()); + verifiableCredential.setWalletId(walletId); + verifiableCredential.setCredential(encryptedCredential); + verifiableCredential.setCredentialMetadata(credentialMetadata); + + try { + return walletCredentialsRepository.save(verifiableCredential); + } catch (Exception e) { + log.error("Failed to save credential for walletId: {}, issuerId: {}, credentialConfigurationId: {}", walletId, issuerId, credentialConfigurationId, e); + throw new CredentialProcessingException( + CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), + "Unable to save credential to database", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/CredentialShareServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CredentialShareServiceImpl.java index 5bd9cacb0..984b20268 100644 --- a/src/main/java/io/mosip/mimoto/service/impl/CredentialShareServiceImpl.java +++ b/src/main/java/io/mosip/mimoto/service/impl/CredentialShareServiceImpl.java @@ -1,8 +1,35 @@ package io.mosip.mimoto.service.impl; +import com.google.gson.Gson; +import io.mosip.biometrics.util.CommonUtil; +import io.mosip.kernel.biometrics.constant.BiometricType; +import io.mosip.kernel.biometrics.entities.BDBInfo; +import io.mosip.kernel.biometrics.entities.BIR; +import io.mosip.kernel.core.websub.spi.PublisherClient; +import io.mosip.mimoto.constant.*; +import io.mosip.mimoto.dto.CryptoWithPinRequestDto; +import io.mosip.mimoto.dto.CryptoWithPinResponseDto; +import io.mosip.mimoto.dto.JsonValue; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.EventModel; +import io.mosip.mimoto.service.CredentialShareService; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; import java.io.ByteArrayInputStream; import java.io.DataInputStream; -import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -12,81 +39,9 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.google.gson.Gson; - -import org.joda.time.DateTime; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.env.Environment; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Service; - -import io.mosip.biometrics.util.CommonUtil; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.kernel.core.websub.spi.PublisherClient; -import io.mosip.mimoto.constant.BiometricType; -import io.mosip.mimoto.constant.DocumentGeneratorExceptionCodeConstant; -import io.mosip.mimoto.constant.EventId; -import io.mosip.mimoto.constant.EventName; -import io.mosip.mimoto.constant.EventType; -import io.mosip.mimoto.constant.IdType; -import io.mosip.mimoto.constant.LoggerFileConstant; -import io.mosip.mimoto.constant.ModuleName; -import io.mosip.mimoto.constant.PlatformSuccessMessages; -import io.mosip.mimoto.dto.CryptoWithPinRequestDto; -import io.mosip.mimoto.dto.CryptoWithPinResponseDto; -import io.mosip.mimoto.dto.DataShare; -import io.mosip.mimoto.dto.JsonValue; -import io.mosip.mimoto.entity.BDBInfo; -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.exception.ApiNotAccessibleException; -import io.mosip.mimoto.exception.CryptoManagerException; -import io.mosip.mimoto.exception.DataShareException; -import io.mosip.mimoto.exception.DocumentGeneratorException; -import io.mosip.mimoto.exception.ExceptionUtils; -import io.mosip.mimoto.exception.IdentityNotFoundException; -import io.mosip.mimoto.exception.ParsingException; -import io.mosip.mimoto.exception.PlatformErrorMessages; -import io.mosip.mimoto.exception.UINNotFoundInDatabase; -import io.mosip.mimoto.model.CredentialStatusEvent; -import io.mosip.mimoto.model.EventModel; -import io.mosip.mimoto.model.StatusEvent; -import io.mosip.mimoto.service.CredentialShareService; -import io.mosip.mimoto.service.RestClientService; -import io.mosip.mimoto.spi.CbeffUtil; -import io.mosip.mimoto.util.AuditLogRequestBuilder; -import io.mosip.mimoto.util.CbeffToBiometricUtil; -import io.mosip.mimoto.util.CryptoCoreUtil; -import io.mosip.mimoto.util.CryptoUtil; -import io.mosip.mimoto.util.DataShareUtil; -import io.mosip.mimoto.util.DateUtils; -import io.mosip.mimoto.util.JsonUtil; -import io.mosip.mimoto.util.LogDescription; -import io.mosip.mimoto.util.LoggerUtil; -import io.mosip.mimoto.util.RestApiClient; -import io.mosip.mimoto.util.Utilities; -import io.mosip.mimoto.util.WebSubSubscriptionHelper; +import java.util.*; +@Slf4j @Service public class CredentialShareServiceImpl implements CredentialShareService { public static final String VC_REQUEST_FILE_NAME = "%s.json"; @@ -95,109 +50,68 @@ public class CredentialShareServiceImpl implements CredentialShareService { public static final String CARD_JSON_FILE_NAME = "%s_VCD.json"; /** The Constant FINGER. */ - public static final String FINGER = "Finger"; + public static final String DATA_IMAGE_JPG_BASE_64 = "data:image/jpg;base64,"; + public static final String DATA_BINARY_BASE_64 = "data:binary;base64,"; private Gson gson = new Gson(); - public String topic = "CREDENTIAL_STATUS_UPDATE"; - - @Autowired - public WebSubSubscriptionHelper webSubSubscriptionHelper; + private final WebSubSubscriptionHelper webSubSubscriptionHelper; - @Autowired - public DataShareUtil dataShareUtil; + private final DataShareUtil dataShareUtil; - @Autowired - CryptoUtil cryptoUtil; + private final DerivedKeyCryptoUtil derivedKeyCryptoUtil; - @Autowired - public RestApiClient restApiClient; + private final RestApiClient restApiClient; - @Autowired - public CryptoCoreUtil cryptoCoreUtil; + private final P12KeyStoreManager p12KeyStoreManager; - /** The Constant FILE_SEPARATOR. */ - public static final String FILE_SEPARATOR = File.separator; + private final CbeffToBiometricUtil util; /** The Constant VALUE. */ public static final String VALUE = "value"; - /** The Constant UIN_CARD_TEMPLATE. */ - public static final String UIN_CARD_TEMPLATE = "RPR_UIN_CARD_TEMPLATE"; - - /** The Constant MASKED_UIN_CARD_TEMPLATE. */ - public static final String MASKED_UIN_CARD_TEMPLATE = "RPR_MASKED_UIN_CARD_TEMPLATE"; - - /** The Constant FACE. */ - public static final String FACE = "Face"; - /** The Constant UIN_TEXT_FILE. */ - public static final String UIN_TEXT_FILE = "textFile"; - - /** The Constant APPLICANT_PHOTO. */ - public static final String APPLICANT_PHOTO = "ApplicantPhoto"; + private static final String UIN_TEXT_FILE = "textFile"; - /** The Constant UINCARDPASSWORD. */ - public static final String UINCARDPASSWORD = "mosip.registration.processor.print.service.uincard.password"; - - private Logger logger = LoggerUtil.getLogger(CredentialShareServiceImpl.class); + @Value("${vercred.type.vid:PCN}") + private String vid; /** The core audit request builder. */ - @Autowired - public AuditLogRequestBuilder auditLogRequestBuilder; + private final AuditLogRequestBuilder auditLogRequestBuilder; /** The utilities. */ - @Autowired - public Utilities utilities; + private final Utilities utilities; /** The rest client service. */ - @Autowired - public RestClientService restClientService; - - /** The Constant INDIVIDUAL_BIOMETRICS. */ - public static final String INDIVIDUAL_BIOMETRICS = "individualBiometrics"; - - /** The Constant VID_CREATE_ID. */ - public static final String VID_CREATE_ID = "registration.processor.id.repo.generate"; - - /** The Constant REG_PROC_APPLICATION_VERSION. */ - public static final String REG_PROC_APPLICATION_VERSION = "registration.processor.id.repo.vidVersion"; - - /** The Constant DATETIME_PATTERN. */ - public static final String DATETIME_PATTERN = "mosip.print.datetime.pattern"; - - public static final String NAME = "name"; - - public static final String VID_TYPE = "registration.processor.id.repo.vidType"; - - /** The cbeffutil. */ - @Autowired - public CbeffUtil cbeffutil; + private final RestClientService restClientService; /** The env. */ - @Autowired - public Environment env; + private final Environment env; - @Autowired - public PublisherClient pb; - - @Value("${mosip.datashare.partner.id}") - public String partnerId; - - @Value("${mosip.datashare.policy.id}") - public String policyId; - - @Value("${mosip.template-language}") - public String templateLang; + private final PublisherClient pb; @Value("#{'${mosip.mandatory-languages:}'.concat('${mosip.optional-languages:}')}") - public String supportedLang; + private String supportedLang; + + public CredentialShareServiceImpl(RestApiClient restApiClient, WebSubSubscriptionHelper webSubSubscriptionHelper, DataShareUtil dataShareUtil, DerivedKeyCryptoUtil derivedKeyCryptoUtil, P12KeyStoreManager p12KeyStoreManager, CbeffToBiometricUtil util, AuditLogRequestBuilder auditLogRequestBuilder, Utilities utilities, RestClientService restClientService, Environment env, PublisherClient pb) { + this.restApiClient = restApiClient; + this.webSubSubscriptionHelper = webSubSubscriptionHelper; + this.dataShareUtil = dataShareUtil; + this.derivedKeyCryptoUtil = derivedKeyCryptoUtil; + this.p12KeyStoreManager = p12KeyStoreManager; + this.util = util; + this.auditLogRequestBuilder = auditLogRequestBuilder; + this.utilities = utilities; + this.restClientService = restClientService; + this.env = env; + this.pb = pb; + } public boolean generateDocuments(EventModel eventModel) throws Exception { Path eventFilePath = Path.of(utilities.getDataPath(), String.format(EVENT_JSON_FILE_NAME, eventModel.getEvent().getTransactionId())); Files.write(eventFilePath, gson.toJson(eventModel).getBytes(), StandardOpenOption.CREATE); - logger.info("Event data saved:: " + eventFilePath.toFile().getAbsolutePath()); + log.info("Event data saved:: " + eventFilePath.toFile().getAbsolutePath()); Map byteMap = new HashMap<>(); String decodedCredential = null; @@ -213,31 +127,30 @@ public boolean generateDocuments(EventModel eventModel) throws Exception { credential = restApiClient.getApi(dataShareUri, String.class); } String encryptionPin = eventModel.getEvent().getData().get("protectionKey").toString(); - decodedCredential = cryptoCoreUtil.decrypt(credential); + decodedCredential = p12KeyStoreManager.decrypt(credential); @SuppressWarnings("unchecked") Map proofMap = (Map) eventModel.getEvent().getData().get("proof"); String sign = proofMap.get("signature").toString(); - + credentialSubject = getCredentialSubject(decodedCredential); org.json.JSONObject credentialJSON = decrypt(decodedCredential, credentialSubject, encryptionPin); - String uin = credentialJSON.getString("UIN"); - + Path vcTextFilePath = Path.of(utilities.getDataPath(), String.format(VC_JSON_FILE_NAME, eventModel.getEvent().getTransactionId())); Files.deleteIfExists(vcTextFilePath); Files.write(vcTextFilePath, gson.toJson(gson.fromJson(decodedCredential, TreeMap.class)).getBytes(), StandardOpenOption.CREATE); - + String credentialType = eventModel.getEvent().getData().get("credentialType").toString(); byteMap = getDocuments(credentialJSON, credentialType, eventModel.getEvent().getTransactionId(), getSignature(sign, credential)); - + Path textFilePath = Path.of(utilities.getDataPath(), String.format(CARD_JSON_FILE_NAME, eventModel.getEvent().getTransactionId())); Files.deleteIfExists(textFilePath); Files.write(textFilePath, byteMap.get(UIN_TEXT_FILE), StandardOpenOption.CREATE); - logger.info("Generated files from event:: " + textFilePath.toFile().getAbsolutePath()); + log.info("Generated files from event:: " + textFilePath.toFile().getAbsolutePath()); } catch (Exception e){ - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); return false; } @@ -252,7 +165,7 @@ public org.json.JSONObject decrypt(String credential, String credentialSubject, } catch (Exception e) { description.setMessage(PlatformErrorMessages.MIMOTO_VC_DECRYPTION_FAILED.getMessage()); description.setCode(PlatformErrorMessages.MIMOTO_VC_DECRYPTION_FAILED.getCode()); - logger.error(description.toString(), e); + log.error(description.toString(), e); throw new DocumentGeneratorException(DocumentGeneratorExceptionCodeConstant.VC_DECRYPTION_EXCEPTION.getErrorCode(), e.getMessage() + ExceptionUtils.getStackTrace(e)); } @@ -265,41 +178,33 @@ public Map getDocuments( String credentialType, String requestId, String sign) { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), "CredentialShareServiceImpl::getDocuments()::entry"); Map byteMap = new HashMap<>(); - String uin = null; + String id = null; LogDescription description = new LogDescription(); String individualBio = null; - Map attributes = new LinkedHashMap<>(); boolean isTransactionSuccessful = false; try { individualBio = credentialJSON.getString("biometrics"); - String individualBiometric = new String(individualBio); - uin = credentialJSON.getString("UIN"); - setTemplateAttributes(credentialJSON.toString(), attributes); - attributes.put(IdType.UIN.toString(), uin); + String individualBiometric = individualBio; + if (credentialJSON.has("UIN")) + id = credentialJSON.getString("UIN"); + else if (credentialJSON.has(vid)) + id = credentialJSON.getString(vid); - byte[] textFileByte = createJSONFile(credentialJSON, individualBiometric, attributes); + byte[] textFileByte = createJSONFile(credentialJSON, individualBiometric); byteMap.put(UIN_TEXT_FILE, textFileByte); // TODO: Uncomment this after datashare creation API fixed. // statusUpdate(requestId, textFileByte, credentialType); isTransactionSuccessful = true; - } catch (UINNotFoundInDatabase e) { - description.setMessage(PlatformErrorMessages.MIMOTO_UIN_NOT_FOUND_IN_DATABASE.getMessage()); - description.setCode(PlatformErrorMessages.MIMOTO_UIN_NOT_FOUND_IN_DATABASE.getCode()); - - logger.error(PlatformErrorMessages.MIMOTO_UIN_NOT_FOUND_IN_DATABASE.name(), e); - throw new DocumentGeneratorException(DocumentGeneratorExceptionCodeConstant.PDF_EXCEPTION.getErrorCode(), - e.getErrorText()); - } catch (Exception e) { description.setMessage(PlatformErrorMessages.MIMOTO_DOCUMENT_GENERATION_FAILED.getMessage()); description.setCode(PlatformErrorMessages.MIMOTO_DOCUMENT_GENERATION_FAILED.getCode()); - logger.error(description.toString(), e); + log.error(description.toString(), e); throw new DocumentGeneratorException(DocumentGeneratorExceptionCodeConstant.PDF_EXCEPTION.getErrorCode(), e.getMessage() + ExceptionUtils.getStackTrace(e)); @@ -327,26 +232,20 @@ public Map getDocuments( : description.getCode(); String moduleName = ModuleName.MIMOTO_SERVICE.toString(); auditLogRequestBuilder.createAuditRequestBuilder(description.getMessage(), eventId, eventName, eventType, - moduleId, moduleName, uin); + moduleId, moduleName, id); } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), "", + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), "", "CredentialShareServiceImpl::getDocuments()::exit"); return byteMap; } @SuppressWarnings("unchecked") - public byte[] createJSONFile(org.json.JSONObject credential, String individualBiometric, - Map attributes) throws IOException { + public byte[] createJSONFile(org.json.JSONObject credential, String individualBiometric) throws IOException { org.json.JSONObject outputJSON = new org.json.JSONObject(); if (credential == null) { throw new IdentityNotFoundException(PlatformErrorMessages.MIMOTO_PIS_IDENTITY_NOT_FOUND.getMessage()); } - // Get JSON template from MOSIP config. - // String printTextFileJson = utilities.getPrintTextFileJson(utilities.getConfigServerFileStorageURL(), - // utilities.getRegistrationProcessorPrintTextFile()); - // JSONObject templateJSON = JsonUtil.objectMapperReadValue(printTextFileJson, JSONObject.class); - // Use local JSON template to include biometrics. JSONObject templateJSON = utilities.getTemplate(); Set templateKeys = templateJSON.keySet(); @@ -355,27 +254,17 @@ public byte[] createJSONFile(org.json.JSONObject credential, String individualBi for (String value : templateValue.split(",")) { Object object = credential.has(value) ? credential.get(value) : null; if (object instanceof ArrayList) { - org.json.simple.JSONArray node = new org.json.simple.JSONArray(); - node.addAll(credential.getJSONArray(value).toList()); - JsonValue[] jsonValues = JsonUtil.mapJsonNodeToJavaObject(JsonValue.class, node); - for (JsonValue jsonValue : jsonValues) { - if (supportedLang.contains(jsonValue.getLanguage())) { - outputJSON.put(value + "_" + jsonValue.getLanguage(), jsonValue); - } - } + appendLangToOutputJSONFields(credential, outputJSON, value); } else if (object instanceof LinkedHashMap) { org.json.JSONObject json = credential.getJSONObject(value); outputJSON.put(value, json.get(VALUE)); + } else if (key.equals("biometrics")) { + outputJSON.put(value, getBiometricsDataJSON(individualBiometric)); } else { - if (key.equals("biometrics")) { - outputJSON.put(value, getBiometricsDataJSON(individualBiometric, attributes)); - } else { - outputJSON.put(value, object); - } + outputJSON.put(value, object); } } } - return outputJSON.toString().getBytes(); } @@ -383,13 +272,11 @@ public byte[] createJSONFile(org.json.JSONObject credential, String individualBi * Decode biometrics data from CBEFF and return biometrics JSON object. * * @param individualBiometric - * @param attributes * @return */ - public org.json.JSONObject getBiometricsDataJSON(String individualBiometric, Map attributes) { + public org.json.JSONObject getBiometricsDataJSON(String individualBiometric) { org.json.JSONObject biometrics = new org.json.JSONObject(); if (individualBiometric != null) { - CbeffToBiometricUtil util = new CbeffToBiometricUtil(cbeffutil); try { List bIRTypeList = util.getBIRTypeList(individualBiometric); for (int i = 0; i < bIRTypeList.size(); i++) { @@ -398,34 +285,16 @@ public org.json.JSONObject getBiometricsDataJSON(String individualBiometric, Map List subType = bdbInfo.getSubtype(); byte[] photoBytes = util.getPhotoByTypeAndSubType(bIRTypeList, type, subType); if (photoBytes != null) { - String data; - if (type.equalsIgnoreCase(BiometricType.FACE.value())) { - // Convert face image from JPEG2000 to JPG and encode with base64. - // Assume that the face image is in JPEG2000 format. - data = java.util.Base64.getEncoder().encodeToString(CommonUtil.convertJP2ToJPEGBytes(extractFaceImageData(photoBytes))); - } else { - data = java.util.Base64.getEncoder().encodeToString(photoBytes); - } + String data = getData(type, photoBytes); if (subType.isEmpty()) { - String formatPrefix = null; - if (type.equalsIgnoreCase(BiometricType.FACE.value())) { - formatPrefix = "data:image/jpg;base64,"; - } else { - formatPrefix = "data:binary;base64,"; - } - biometrics.put(type, formatPrefix + data); + addTypeToBiometricWithFormatPrefixAndData(biometrics, type, data); } else { - org.json.JSONObject typeList = biometrics.has(type) ? biometrics.getJSONObject(type) - : new org.json.JSONObject(); - typeList.put(String.join("_", subType).toLowerCase(), "data:binary;base64," + data); - if (!biometrics.has(type)) { - biometrics.put(type, typeList); - } + addTypeToBiometricIfNotExists(biometrics, type, subType, data); } } } } catch (Exception e) { - logger.error("Something wrong when trying to get biometrics data", e); + log.error("Something wrong when trying to get biometrics data", e); } } @@ -439,78 +308,8 @@ public String getSignature(String sign, String credential) { return signature; } - /** - * Gets the artifacts. - * - * @param idJsonString the id json string - * @param attribute the attribute - * @return the artifacts - * @throws IOException Signals that an I/O exception has occurred. - * @throws ParseException - */ - @SuppressWarnings("unchecked") - public void setTemplateAttributes(String jsonString, Map attribute) - throws IOException, ParseException { - try { - JSONObject demographicIdentity = JsonUtil.objectMapperReadValue(jsonString, JSONObject.class); - if (demographicIdentity == null) - throw new IdentityNotFoundException(PlatformErrorMessages.MIMOTO_PIS_IDENTITY_NOT_FOUND.getMessage()); - - String mapperJsonString = utilities.getIdentityMappingJson(utilities.getConfigServerFileStorageURL(), - utilities.getGetRegProcessorIdentityJson()); - JSONObject mapperJson = JsonUtil.objectMapperReadValue(mapperJsonString, JSONObject.class); - JSONObject mapperIdentity = JsonUtil.getJSONObject(mapperJson, - utilities.getGetRegProcessorDemographicIdentity()); - - List mapperJsonKeys = new ArrayList<>(mapperIdentity.keySet()); - for (String key : mapperJsonKeys) { - LinkedHashMap jsonObject = JsonUtil.getJSONValue(mapperIdentity, key); - Object obj = null; - String values = jsonObject.get(VALUE); - for (String value : values.split(",")) { - // Object object = demographicIdentity.get(value); - Object object = demographicIdentity.get(value); - if (object != null) { - try { - obj = new JSONParser().parse(object.toString()); - } catch (Exception e) { - obj = object; - } - - if (obj instanceof JSONArray) { - // JSONArray node = JsonUtil.getJSONArray(demographicIdentity, value); - JsonValue[] jsonValues = JsonUtil.mapJsonNodeToJavaObject(JsonValue.class, (JSONArray) obj); - for (JsonValue jsonValue : jsonValues) { - /* - * if (jsonValue.getLanguage().equals(primaryLang)) attribute.put(value + "_" + - * primaryLang, jsonValue.getValue()); if - * (jsonValue.getLanguage().equals(secondaryLang)) attribute.put(value + "_" + - * secondaryLang, jsonValue.getValue()); - */ - if (supportedLang.contains(jsonValue.getLanguage())) - attribute.put(value + "_" + jsonValue.getLanguage(), jsonValue.getValue()); - - } - - } else if (object instanceof JSONObject) { - JSONObject json = (JSONObject) object; - attribute.put(value, (String) json.get(VALUE)); - } else { - attribute.put(value, String.valueOf(object)); - } - } - - } - } - - } catch (JsonParseException | JsonMappingException e) { - logger.error("Error while parsing Json file", e); - throw new ParsingException(PlatformErrorMessages.MIMOTO_RGS_JSON_PARSING_EXCEPTION.getMessage(), e); - } - } - @SuppressWarnings("unused") - public byte[] extractFaceImageData(byte[] decodedBioValue) { + private byte[] extractFaceImageData(byte[] decodedBioValue) { try (DataInputStream din = new DataInputStream(new ByteArrayInputStream(decodedBioValue))) { @@ -560,41 +359,10 @@ public byte[] extractFaceImageData(byte[] decodedBioValue) { public String getCredentialSubject(String credential) { org.json.JSONObject jsonObject = new org.json.JSONObject(credential); - String credentialSubject = jsonObject.get("credentialSubject").toString(); + String credentialSubject = jsonObject.get("credentialSubject").toString(); return credentialSubject; } - /** - * Create datashare and update printing status via websub. - * Should be call at the end of the event handler section. - * - * @param requestId - * @param data - * @param credentialType - * @throws DataShareException - * @throws ApiNotAccessibleException - * @throws IOException - * @throws Exception - */ - public void statusUpdate(String requestId, byte[] data, String credentialType) - throws DataShareException, ApiNotAccessibleException, IOException, Exception { - DataShare dataShare = null; - dataShare = dataShareUtil.getDataShare(data, policyId, partnerId); - CredentialStatusEvent creEvent = new CredentialStatusEvent(); - LocalDateTime currentDtime = DateUtils.getUTCCurrentDateTime(); - StatusEvent sEvent = new StatusEvent(); - sEvent.setId(UUID.randomUUID().toString()); - sEvent.setRequestId(requestId); - sEvent.setStatus("printing"); - sEvent.setUrl(dataShare.getUrl()); - sEvent.setTimestamp(Timestamp.valueOf(currentDtime).toString()); - creEvent.setPublishedOn(new DateTime().toString()); - creEvent.setPublisher("PRINT_SERVICE"); - creEvent.setTopic(topic); - creEvent.setEvent(sEvent); - webSubSubscriptionHelper.publish(topic, creEvent); - } - /** * Decrypt protected (encrypted) attributes. * @@ -607,7 +375,6 @@ public void statusUpdate(String requestId, byte[] data, String credentialType) public org.json.JSONObject decryptAttribute(org.json.JSONObject data, String encryptionPin, String credential) throws ParseException { - // org.json.JSONObject jsonObj = new org.json.JSONObject(credential); JSONParser parser = new JSONParser(); // this needs the "json-simple" library Object obj = parser.parse(credential); JSONObject jsonObj = (org.json.simple.JSONObject) obj; @@ -622,10 +389,10 @@ public org.json.JSONObject decryptAttribute(org.json.JSONObject data, String enc cryptoWithPinRequestDto.setUserPin(encryptionPin); cryptoWithPinRequestDto.setData(data.getString(str.toString())); try { - cryptoWithPinResponseDto = cryptoUtil.decryptWithPin(cryptoWithPinRequestDto); + cryptoWithPinResponseDto = derivedKeyCryptoUtil.decryptWithPin(cryptoWithPinRequestDto); } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException - | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { - logger.error("Error while decrypting the data", e); + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + log.error("Error while decrypting the data", e); throw new CryptoManagerException(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); } @@ -637,4 +404,90 @@ public org.json.JSONObject decryptAttribute(org.json.JSONObject data, String enc return data; } + private void appendLangToOutputJSONFields(org.json.JSONObject credential, org.json.JSONObject outputJSON, String value) { + JSONArray node = new JSONArray(); + node.addAll(credential.getJSONArray(value).toList()); + JsonValue[] jsonValues = mapJsonNodeToJavaObject(JsonValue.class, node); + for (JsonValue jsonValue : jsonValues) { + if (supportedLang.contains(jsonValue.getLanguage())) { + outputJSON.put(value + "_" + jsonValue.getLanguage(), jsonValue); + } + } + } + + /** + * Map json node to java object. + * Maps JSONArray elements to Java objects by setting language and value fields. + * + * @param the generic type + * @param genericType the generic type + * @param demographicJsonNode the demographic json node + * @return the array of mapped objects + */ + @SuppressWarnings("unchecked") + private T[] mapJsonNodeToJavaObject(Class genericType, JSONArray demographicJsonNode) { + String language; + String value; + T[] javaObject = (T[]) java.lang.reflect.Array.newInstance(genericType, demographicJsonNode.size()); + try { + for (int i = 0; i < demographicJsonNode.size(); i++) { + T jsonNodeElement = (T) genericType.getDeclaredConstructor().newInstance(); + JSONObject objects = getJSONObjectFromArray(demographicJsonNode, i); + if (objects != null) { + language = (String) objects.get("language"); + value = (String) objects.get("value"); + FieldUtils.writeField(jsonNodeElement, "language", language, true); + FieldUtils.writeField(jsonNodeElement, "value", value, true); + javaObject[i] = jsonNodeElement; + } + } + } catch (InstantiationException | IllegalAccessException | java.lang.reflect.InvocationTargetException | NoSuchMethodException e) { + throw new InstantanceCreationException(PlatformErrorMessages.MIMOTO_SYS_INSTANTIATION_EXCEPTION.getMessage(), e); + } catch (SecurityException e) { + throw new FieldNotFoundException(PlatformErrorMessages.MIMOTO_SYS_NO_SUCH_FIELD_EXCEPTION.getMessage(), e); + } + return javaObject; + } + + /** + * Iterates the JSONArray and returns JSONObject for given index. + * + * @param jsonArray the json array + * @param index the index + * @return the JSON object + */ + @SuppressWarnings("unchecked") + private JSONObject getJSONObjectFromArray(JSONArray jsonArray, int index) { + Object object = jsonArray.get(index); + if (object instanceof LinkedHashMap) { + LinkedHashMap identity = (LinkedHashMap) jsonArray.get(index); + return identity != null ? new JSONObject(identity) : null; + } else { + return (JSONObject) object; + } + } + + private String getData(String type, byte[] photoBytes) { + if (type.equalsIgnoreCase(BiometricType.FACE.value())) { + // Convert face image from JPEG2000 to JPG and encode with base64. + // Assume that the face image is in JPEG2000 format. + return Base64.getEncoder().encodeToString(CommonUtil.convertJP2ToJPEGBytes(extractFaceImageData(photoBytes))); + } else { + return Base64.getEncoder().encodeToString(photoBytes); + } + } + + private static void addTypeToBiometricWithFormatPrefixAndData(org.json.JSONObject biometrics, String type, String data) { + String formatPrefix = type.equalsIgnoreCase(BiometricType.FACE.value()) ? DATA_IMAGE_JPG_BASE_64 : DATA_BINARY_BASE_64; + biometrics.put(type, formatPrefix + data); + } + + private static void addTypeToBiometricIfNotExists(org.json.JSONObject biometrics, String type, List subType, String data) { + org.json.JSONObject typeList = biometrics.has(type) ? biometrics.getJSONObject(type) + : new org.json.JSONObject(); + typeList.put(String.join("_", subType).toLowerCase(), DATA_BINARY_BASE_64 + data); + if (!biometrics.has(type)) { + biometrics.put(type, typeList); + } + } } diff --git a/src/main/java/io/mosip/mimoto/service/impl/CredentialVerifierServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/CredentialVerifierServiceImpl.java new file mode 100644 index 000000000..2280ccb0a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/CredentialVerifierServiceImpl.java @@ -0,0 +1,35 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.exception.VCVerificationException; +import io.mosip.mimoto.service.CredentialVerifierService; +import io.mosip.vercred.vcverifier.CredentialsVerifier; +import io.mosip.vercred.vcverifier.constants.CredentialFormat; +import io.mosip.vercred.vcverifier.data.VerificationResult; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +public class CredentialVerifierServiceImpl implements CredentialVerifierService { + + private final ObjectMapper objectMapper; + + private final CredentialsVerifier credentialsVerifier; + + public CredentialVerifierServiceImpl(ObjectMapper objectMapper, CredentialsVerifier credentialsVerifier) { + this.objectMapper = objectMapper; + this.credentialsVerifier = credentialsVerifier; + } + + public boolean verify(VCCredentialResponse response) throws JsonProcessingException, VCVerificationException { + String credentialString = response.getCredential() instanceof String ? (String) response.getCredential() : objectMapper.writeValueAsString(response.getCredential()); + VerificationResult result = credentialsVerifier.verify(credentialString, Objects.requireNonNull(CredentialFormat.Companion.fromValue(response.getFormat()))); + if (!result.getVerificationStatus()) { + throw new VCVerificationException(result.getVerificationErrorCode().toLowerCase(), result.getVerificationMessage()); + } + return true; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/DataShareServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/DataShareServiceImpl.java new file mode 100644 index 000000000..d3c5a512a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/DataShareServiceImpl.java @@ -0,0 +1,180 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.DataShareResponseDto; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.openid.datashare.DataShareResponseWrapperDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.util.RestApiClient; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.PathMatcher; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; + +@Slf4j +@Service +public class DataShareServiceImpl { + + private static final Pattern SAFE_URL_SEGMENT_PATTERN = Pattern.compile("^[A-Za-z0-9._\\-]+$"); + + private final RestApiClient restApiClient; + + private final String dataShareHostUrl; + + private final String dataShareCreateUrl; + + private final String dataShareGetUrlPattern; + + private final Integer maxRetryCount; + + private final ObjectMapper objectMapper; + + private final PathMatcher pathMatcher = new AntPathMatcher(); + + public DataShareServiceImpl( + RestApiClient restApiClient, + ObjectMapper objectMapper, + @Value("${mosip.data.share.url}") String dataShareHostUrl, + @Value("${mosip.data.share.create.url}") String dataShareCreateUrl, + @Value("${mosip.data.share.get.url.pattern}") String dataShareGetUrlPattern, + @Value("${mosip.data.share.create.retry.count}") Integer maxRetryCount) { + + this.restApiClient = restApiClient; + this.objectMapper = objectMapper; + this.dataShareHostUrl = dataShareHostUrl; + this.dataShareCreateUrl = dataShareCreateUrl; + this.dataShareGetUrlPattern = dataShareGetUrlPattern; + this.maxRetryCount = maxRetryCount; + } + + public String storeDataInDataShare(String data, String credentialValidity) throws InvalidCredentialResourceException { + ByteArrayResource contentsAsResource = new ByteArrayResource(data.getBytes()) { + @Override + public String getFilename() { + return "credential_file"; + } + }; + LinkedMultiValueMap map = new LinkedMultiValueMap<>(); + map.add("file", contentsAsResource); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> requestEntity = new HttpEntity<>(map, headers); + DataShareResponseWrapperDTO dataShareResponseWrapperDTO = pushCredentialIntoDataShare(requestEntity, credentialValidity); + log.info("Data pushed into DataShare -> "); + return dataShareResponseWrapperDTO.getDataShare().getUrl(); + } + + private DataShareResponseWrapperDTO pushCredentialIntoDataShare(HttpEntity> requestEntity, String credentialValidity) throws InvalidCredentialResourceException { + int attempt =0 ; + DataShareResponseWrapperDTO dataShareResponseWrapperDTO = null; + while(attempt++ < maxRetryCount ){ + try { + dataShareResponseWrapperDTO = restApiClient.postApi(dataShareCreateUrl + "?usageCountForStandaloneMode=" + credentialValidity, MediaType.MULTIPART_FORM_DATA, requestEntity, DataShareResponseWrapperDTO.class); + } catch (Exception e) { + log.error(attempt + " attempt to push credential failed"); + } + } + if(dataShareResponseWrapperDTO == null){ + throw new InvalidCredentialResourceException( + ErrorConstants.REQUEST_TIMED_OUT.getErrorCode(), + ErrorConstants.REQUEST_TIMED_OUT.getErrorMessage()); + } + return dataShareResponseWrapperDTO; + } + + public VCCredentialResponse downloadCredentialFromDataShare(PresentationRequestDTO presentationRequestDTO) throws JsonProcessingException { + log.info("Started the Credential Download From DataShare"); + // Create custom headers + HttpHeaders customHeaders = new HttpHeaders(); + customHeaders.add(HttpHeaders.ACCEPT, "application/json"); + customHeaders.add(HttpHeaders.ACCEPT_CHARSET, "UTF-8"); + + // Get the credentials URI and validate it + String credentialsResourceUri = presentationRequestDTO.getResource(); + if (!pathMatcher.match(dataShareGetUrlPattern, credentialsResourceUri)) { + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + ErrorConstants.RESOURCE_INVALID.getErrorMessage()); + } + + validateResourceURL(credentialsResourceUri); + + + // Call the API with the custom headers + String vcCredentialResponseString = restApiClient.getApiWithCustomHeaders(credentialsResourceUri, String.class, customHeaders); + if (vcCredentialResponseString == null) { + throw new InvalidCredentialResourceException( + ErrorConstants.SERVER_UNAVAILABLE.getErrorCode(), + ErrorConstants.SERVER_UNAVAILABLE.getErrorMessage()); + } + VCCredentialResponse vcCredentialResponse = objectMapper.readValue(vcCredentialResponseString, VCCredentialResponse.class); + + if(vcCredentialResponse.getCredential() == null){ + DataShareResponseDto dataShareResponse = objectMapper.readValue(vcCredentialResponseString, DataShareResponseDto.class); + String errorCode = dataShareResponse.getErrors().get(0).getErrorCode(); + throw new InvalidCredentialResourceException(errorCode.equals("DAT-SER-008") ? ErrorConstants.RESOURCE_NOT_FOUND.getErrorMessage() : ErrorConstants.RESOURCE_EXPIRED.getErrorMessage()); + } + return vcCredentialResponse; + } + + private static void validateResourceURL(String credentialsResourceUri) { + try { + URI parsedUri = new URI(credentialsResourceUri); + String decodedURI = parsedUri.getPath(); + + String wildcardPart = getWildcardPart(decodedURI); + + if (!SAFE_URL_SEGMENT_PATTERN.matcher(wildcardPart).matches()) { + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + "Invalid characters in wildcard segment"); + } + } catch (URISyntaxException e) { + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + "Malformed resource URL"); + } + } + + @NotNull + private static String getWildcardPart(String decodedPath) { + if (decodedPath.contains("..") || decodedPath.contains("//")) { + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + "Invalid path structure in resource URL"); + } + + String[] segments = decodedPath.split("/"); + String wildcardPart; + if( segments.length == 0 ){ + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + "Invalid resource identifier in URL"); + } else { + wildcardPart = segments[segments.length - 1]; + } + + if (wildcardPart.isEmpty()) { + throw new InvalidCredentialResourceException( + ErrorConstants.RESOURCE_INVALID.getErrorCode(), + "Invalid resource identifier in URL"); + } + return wildcardPart; + } + +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/DcSdJwtCredentialFormatHandler.java b/src/main/java/io/mosip/mimoto/service/impl/DcSdJwtCredentialFormatHandler.java new file mode 100644 index 000000000..373d339be --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/DcSdJwtCredentialFormatHandler.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.service.impl; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("dc+sd-jwt") +public class DcSdJwtCredentialFormatHandler extends VcSdJwtCredentialFormatHandler { + public DcSdJwtCredentialFormatHandler(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public String getSupportedFormat() { + return CredentialFormat.DC_SD_JWT.getFormat(); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/Draft13VCDownloadHandler.java b/src/main/java/io/mosip/mimoto/service/impl/Draft13VCDownloadHandler.java new file mode 100644 index 000000000..1069ec252 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/Draft13VCDownloadHandler.java @@ -0,0 +1,69 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.Draft13VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponse; +import io.mosip.mimoto.exception.CredentialProcessingException; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.service.Draft13CredentialRequestService; +import io.mosip.mimoto.service.VCDownloadHandler; +import io.mosip.mimoto.util.RestApiClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import static io.mosip.mimoto.exception.ErrorConstants.CREDENTIAL_DOWNLOAD_EXCEPTION; +import static io.mosip.mimoto.exception.ErrorConstants.SERVER_UNAVAILABLE; + +@Slf4j +@Component("draft-13") +public class Draft13VCDownloadHandler implements VCDownloadHandler { + private final Draft13CredentialRequestService draft13CredentialRequestService; + private final RestApiClient restApiClient; + + public Draft13VCDownloadHandler(Draft13CredentialRequestService draft13CredentialRequestService, RestApiClient restApiClient) { + this.draft13CredentialRequestService = draft13CredentialRequestService; + this.restApiClient = restApiClient; + } + + @Override + public VCCredentialResponse downloadCredential(IssuerDTO issuerDTO, String credentialConfigurationId, CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse, TokenResponseDTO tokenResponse, String walletId, String base64Key, boolean isLoginFlow) throws CredentialProcessingException, InvalidCredentialResourceException, ExternalServiceUnavailableException { + Draft13VCCredentialRequest vcCredentialRequest; + try { + vcCredentialRequest = draft13CredentialRequestService.buildRequest(issuerDTO, credentialConfigurationId, credentialIssuerWellKnownResponse, tokenResponse.getC_nonce(), walletId, base64Key, isLoginFlow); + } catch (Exception e) { + log.error("Failed to generate VC credential request for issuerId: {}", issuerDTO.getIssuer_id(), e); + throw new CredentialProcessingException(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), "Unable to generate credential request", e); + } + + return fetchCredential(credentialIssuerWellKnownResponse.getCredentialEndPoint(), vcCredentialRequest, tokenResponse.getAccess_token(), issuerDTO.getIssuer_id(), credentialConfigurationId); + } + + private VCCredentialResponse fetchCredential(String credentialEndpoint, Draft13VCCredentialRequest vcCredentialRequest, String accessToken, String issuerId, String credentialConfigId) throws InvalidCredentialResourceException, ExternalServiceUnavailableException { + VerifiableCredentialResponse response; + + try { + response = restApiClient.postApi(credentialEndpoint, MediaType.APPLICATION_JSON, + vcCredentialRequest, VerifiableCredentialResponse.class, accessToken); + } catch (Exception e) { + String message = String.format("Unable to download credential from issuerId: %s, credentialConfigurationId: %s", issuerId, credentialConfigId); + throw new ExternalServiceUnavailableException(SERVER_UNAVAILABLE.getErrorCode(), message, e); + } + + if (response == null) { + String message = String.format("Unable to download credential from issuerId: %s, credentialConfigurationId: %s", issuerId, credentialConfigId); + throw new ExternalServiceUnavailableException(SERVER_UNAVAILABLE.getErrorCode(), message); + } + + if (response.getCredential() == null) { + throw new InvalidCredentialResourceException("Credential response did not contain a credential"); + } + + log.debug("VC Credential Response received"); + return new VCCredentialResponse(vcCredentialRequest.getFormat(), response.getCredential()); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/EncryptionServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/EncryptionServiceImpl.java new file mode 100644 index 000000000..8efec30d4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/EncryptionServiceImpl.java @@ -0,0 +1,32 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.service.DataProtectionService; +import io.mosip.mimoto.service.EncryptionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class EncryptionServiceImpl implements EncryptionService { + private static final String USER_PII_KEY_REFERENCE_ID = "user_pii"; + private static final String EMPTY_AAD = ""; + private static final String EMPTY_SALT = ""; + + private final DataProtectionService dataProtectionService; + + @Autowired + public EncryptionServiceImpl(DataProtectionService dataProtectionService) { + this.dataProtectionService = dataProtectionService; + } + + @Override + public String encrypt(String data) throws EncryptionException { + return dataProtectionService.encrypt(data, USER_PII_KEY_REFERENCE_ID, EMPTY_AAD, EMPTY_SALT); + } + + @Override + public String decrypt(String data) throws DecryptionException { + return dataProtectionService.decrypt(data, USER_PII_KEY_REFERENCE_ID, EMPTY_AAD, EMPTY_SALT); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenDecoder.java b/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenDecoder.java new file mode 100644 index 000000000..adc36beb7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenDecoder.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import io.mosip.mimoto.service.TokenDecoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.stereotype.Component; + +@Component +@Qualifier("googleJwtDecoder") +public class GoogleTokenDecoder implements TokenDecoder { + private final JwtDecoder jwtDecoder; + + @Autowired + public GoogleTokenDecoder(@Qualifier("googleJwtDecoder") JwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + } + + @Override + public Jwt decode(String idToken) throws OAuth2AuthenticationException { + try { + return jwtDecoder.decode(idToken); + } catch (JwtException e) { + throw new InvalidRequestException("invalid_token", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenService.java b/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenService.java new file mode 100644 index 000000000..b4da18615 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/GoogleTokenService.java @@ -0,0 +1,122 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import io.mosip.mimoto.service.TokenDecoder; +import io.mosip.mimoto.service.TokenService; +import io.mosip.mimoto.service.UserMetadataService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service("google") +@Slf4j +public class GoogleTokenService implements TokenService { + private static final String MISSING_EMAIL_ERROR = "Missing email in ID token"; + private static final String INVALID_ISSUER_ERROR = "Invalid issuer"; + private static final String INVALID_AUDIENCE_ERROR = "Invalid audience"; + + private final TokenDecoder tokenDecoder; + private final UserMetadataService userMetadataService; + private final SecurityContextManager securityContextManager; + private final SessionManager sessionManager; + private final String expectedIssuer; + private final String expectedAudience; + + @Autowired + public GoogleTokenService( + @Qualifier("googleJwtDecoder") TokenDecoder tokenDecoder, + UserMetadataService userMetadataService, + SecurityContextManager securityContextManager, + SessionManager sessionManager, + @Value("${spring.security.oauth2.client.registration.google.client-id}") String clientId, + @Value("${google.issuer:https://accounts.google.com}") String issuer) { + this.tokenDecoder = tokenDecoder; + this.userMetadataService = userMetadataService; + this.securityContextManager = securityContextManager; + this.sessionManager = sessionManager; + this.expectedAudience = clientId; + this.expectedIssuer = issuer; + } + + @Override + public void processToken(String idToken, String provider, HttpServletRequest request, HttpServletResponse response) + throws OAuth2AuthenticationException { + Jwt jwt = validateToken(idToken); + String email = extractClaim(jwt, "email"); + if (email == null) { + log.error(MISSING_EMAIL_ERROR); + throw new OAuth2AuthenticationException("missing_email", MISSING_EMAIL_ERROR, HttpStatus.UNAUTHORIZED); + } + + String name = extractClaim(jwt, "name"); + String picture = extractClaim(jwt, "picture"); + String sub = extractClaim(jwt, "sub"); + + String userId; + try { + UserMetadata userMetadata = userMetadataService.getUserMetadata(sub, provider); + if (userMetadata != null) { + if (name == null || name.isBlank()) { + name = userMetadata.getDisplayName(); + } + if (picture == null || picture.isBlank()) { + picture = userMetadata.getProfilePictureUrl(); + } + } + userId = userMetadataService.updateOrCreateUserMetadata(sub, provider, name, picture, email); + } catch (DecryptionException e) { + log.error("Failed to store the user info in the database", e); + throw new RuntimeException(); + } + sessionManager.setupSession(request, provider, new UserMetadataDTO(name, picture, email,null), userId); + securityContextManager.setupSecurityContext(createOAuth2Token(provider, sub, name, picture, email), request, response); + } + + private Jwt validateToken(String idToken) throws OAuth2AuthenticationException { + Jwt jwt = tokenDecoder.decode(idToken); + if (!expectedIssuer.equals(jwt.getIssuer().toString())) { + log.error("Invalid issuer: {}", jwt.getIssuer()); + throw new OAuth2AuthenticationException("invalid_issuer", INVALID_ISSUER_ERROR, HttpStatus.UNAUTHORIZED); + } + if (!expectedAudience.equals(jwt.getAudience().stream().findFirst().orElse(null))) { + log.error("Invalid audience: {}", jwt.getAudience()); + throw new OAuth2AuthenticationException("invalid_audience", INVALID_AUDIENCE_ERROR, HttpStatus.UNAUTHORIZED); + } + return jwt; + } + + private String extractClaim(Jwt jwt, String claimName) { + return (String) jwt.getClaims().get(claimName); + } + + private OAuth2AuthenticationToken createOAuth2Token(String provider, String sub, String name, String picture, String email) { + Map attributes = new HashMap<>(); + attributes.put("sub", sub); + attributes.put("name", name); + attributes.put("picture", picture); + attributes.put("email", email); + + List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + OAuth2User oAuth2User = new DefaultOAuth2User(authorities, attributes, "sub"); + return new OAuth2AuthenticationToken(oAuth2User, authorities, provider); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/IdpServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/IdpServiceImpl.java new file mode 100644 index 000000000..7b9becf50 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/IdpServiceImpl.java @@ -0,0 +1,195 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.VerifiableCredentialRequestDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerConfiguration; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.util.JoseUtil; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpStatus; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Service +public class IdpServiceImpl implements IdpService { + + private static final String DPOP_HEADER = "DPoP"; + + @Value("${mosip.oidc.client.assertion.type}") + String clientAssertionType; + + @Value("${mosip.oidc.p12.filename}") + private String fileName; + + @Value("${mosip.oidc.p12.password}") + private String cyptoPassword; + + @Value("${mosip.oidc.p12.path}") + String keyStorePath; + + private final JoseUtil joseUtil; + + private final RestTemplate restTemplate; + + private final IssuersService issuersService; + + public IdpServiceImpl(JoseUtil joseUtil, @Qualifier("restTemplate") RestTemplate restTemplate, IssuersService issuersService) { + this.joseUtil = joseUtil; + this.restTemplate = restTemplate; + this.issuersService = issuersService; + } + + @Override + public HttpEntity> constructGetTokenRequest(Map params, String issuerId, String tokenEndpoint) throws IOException, IssuerOnboardingException, ApiNotAccessibleException, InvalidIssuerIdException { + HttpHeaders headers = new HttpHeaders(); + MultiValueMap map = new LinkedMultiValueMap<>(); + validateCodeVerifier(params.get("code_verifier")); + IssuerDTO issuerDTO = issuersService.getIssuerDetails(issuerId); + + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String clientAssertion = joseUtil.getJWT(issuerDTO.getClient_id(), keyStorePath, fileName, issuerDTO.getClient_alias(), cyptoPassword, tokenEndpoint); + map.add("code", params.get("code")); + map.add("client_id", issuerDTO.getClient_id()); + map.add("grant_type", params.get("grant_type")); + map.add("redirect_uri", params.get("redirect_uri")); + map.add("client_assertion", clientAssertion.replace("[", "").replace("]", "")); + map.add("client_assertion_type", clientAssertionType); + map.add("code_verifier", params.get("code_verifier")); + + return new HttpEntity<>(map, headers); + } + + @Override + public String getTokenEndpoint(String issuerId) + throws ApiNotAccessibleException, IOException, + AuthorizationServerWellknownResponseException, + InvalidWellknownResponseException { + CredentialIssuerConfiguration credentialIssuerConfiguration = + issuersService.getIssuerConfiguration(issuerId); + return credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse().getTokenEndpoint(); + } + + @Override + public TokenResponseDTO getTokenResponse(VerifiableCredentialRequestDTO verifiableCredentialRequest) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + return getTokenResponse(convertVerifiableCredentialRequestToMap(verifiableCredentialRequest)); + } + + @Override + public TokenResponseDTO getTokenResponse(Map params) + throws ApiNotAccessibleException, IOException, + AuthorizationServerWellknownResponseException, + InvalidWellknownResponseException { + try { + String issuerId = params.get("issuer"); + String tokenEndpoint = getTokenEndpoint(issuerId); + + HttpEntity> request = + constructGetTokenRequest(params, issuerId, tokenEndpoint); + + TokenResponseDTO response = restTemplate.postForObject( + tokenEndpoint, + request, + TokenResponseDTO.class + ); + + if (response == null) { + throw new IdpException("Exception occurred while performing the authorization"); + } + + return response; + + } catch (InvalidIssuerIdException e) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Invalid issuer"); + + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.BAD_REQUEST) { + throw new InvalidRequestException( + INVALID_REQUEST.getErrorCode(), + "Request failed due to invalid input detected by an external service." + ); + } + throw e; + } + } + + + + @Override + public ResponseEntity getTokenResponseV2(Map params, String dpopProof) + throws ApiNotAccessibleException, IOException, + AuthorizationServerWellknownResponseException, + InvalidWellknownResponseException, + IssuerOnboardingException { + + try { + String issuerId = params.get("issuer"); + String tokenEndpoint = getTokenEndpoint(issuerId); + + HttpEntity> request = + constructGetTokenRequest(params, issuerId, tokenEndpoint); + + HttpHeaders headers = new HttpHeaders(); + headers.addAll(request.getHeaders()); + + if (StringUtils.hasText(dpopProof)) { + headers.set(DPOP_HEADER, dpopProof); + } + + HttpEntity> requestWithDpop = + new HttpEntity<>(request.getBody(), headers); + + return restTemplate.exchange( + tokenEndpoint, + HttpMethod.POST, + requestWithDpop, + String.class + ); + + } catch (InvalidIssuerIdException e) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Invalid issuer"); + + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()) + .headers(e.getResponseHeaders()) + .body(e.getResponseBodyAsString()); + } + } + + private void validateCodeVerifier(String codeVerifier) { + if (codeVerifier == null || !codeVerifier.matches("^[A-Za-z0-9\\-._~]{43,128}$")) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Invalid code verifier."); + } + } + + private Map convertVerifiableCredentialRequestToMap(VerifiableCredentialRequestDTO verifiableCredentialRequest) { + Map params = new HashMap<>(); + params.put("code", verifiableCredentialRequest.getCode()); + params.put("redirect_uri", verifiableCredentialRequest.getRedirectUri()); + params.put("grant_type", verifiableCredentialRequest.getGrantType()); + params.put("code_verifier", verifiableCredentialRequest.getCodeVerifier()); + params.put("issuer", verifiableCredentialRequest.getIssuer()); + + return params; + } + +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java new file mode 100644 index 000000000..9b00717dd --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java @@ -0,0 +1,194 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuerV2DTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.IssuersV2DTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.util.IssuerConfigUtil; +import io.mosip.mimoto.util.Utilities; +import jakarta.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.beans.factory.annotation.Value; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@Slf4j +@Validated +public class IssuersServiceImpl implements IssuersService { + + private final Utilities utilities; + + private final ObjectMapper objectMapper; + + private final IssuerConfigUtil issuersConfigUtil; + + private final String mosipApiPublicUrl; + + private final String contextPath; + + private static final String GET_TOKEN_PATH = "/get-token/"; + + public IssuersServiceImpl(Utilities utilities, ObjectMapper objectMapper, IssuerConfigUtil issuersConfigUtil, + @Value("${mosip.api.public.url}") String mosipApiPublicUrl, + @Value("${server.servlet.context-path}") String contextPath) { + this.utilities = utilities; + this.objectMapper = objectMapper; + this.issuersConfigUtil = issuersConfigUtil; + this.mosipApiPublicUrl = mosipApiPublicUrl; + this.contextPath = contextPath; + } + + @Override + @Cacheable(value = "issuersConfig", key = "#p0 ?: 'allIssuersConfig'") + public IssuersDTO getIssuers(String search) throws ApiNotAccessibleException, IOException { + IssuersDTO issuersDTO = getAllIssuers(); + issuersDTO = getAllEnabledIssuers(issuersDTO); + issuersDTO = getFilteredIssuers(issuersDTO, search); + + return issuersDTO; + } + + @Override + public IssuerDTO getIssuerDetails(String issuerId) throws ApiNotAccessibleException, IOException { + IssuersDTO issuersDTO = getAllIssuers(); + issuersDTO = getAllEnabledIssuers(issuersDTO); + + return issuersDTO.getIssuers().stream() + .filter(issuer -> issuer.getIssuer_id().equals(issuerId)) + .findFirst() + .orElseThrow(InvalidIssuerIdException::new); + } + + private IssuersDTO getAllEnabledIssuers(IssuersDTO issuersDTO) { + return new IssuersDTO(issuersDTO.getIssuers().stream() + .filter(issuer -> "true".equals(issuer.getEnabled())) + .collect(Collectors.toList())); + } + + private IssuersDTO getFilteredIssuers(IssuersDTO issuersDTO, String search) { + if (StringUtils.isEmpty(search)) { + return issuersDTO; + } + + return new IssuersDTO(issuersDTO.getIssuers().stream() + .filter(issuer -> issuer.getDisplay().stream() + .anyMatch(displayDTO -> displayDTO.getTitle().toLowerCase().contains(search.toLowerCase()))) + .collect(Collectors.toList())); + } + + @Override + public IssuersDTO getAllIssuers() throws ApiNotAccessibleException, IOException { + IssuersDTO issuersDTO; + String issuersConfigJsonValue = utilities.getIssuersConfigJsonValue(); + if (issuersConfigJsonValue == null) { + throw new ApiNotAccessibleException(); + } + + issuersDTO = objectMapper.readValue(issuersConfigJsonValue, IssuersDTO.class); + + if (issuersDTO != null && issuersDTO.getIssuers() != null) { + String baseTokenEndpoint = new StringBuilder() + .append(mosipApiPublicUrl).append(contextPath).append(GET_TOKEN_PATH) + .toString(); + + issuersDTO.getIssuers().stream() + .filter(java.util.Objects::nonNull) + .forEach(issuer -> { + if (StringUtils.isBlank(issuer.getToken_endpoint())) { + issuer.setToken_endpoint(baseTokenEndpoint + issuer.getIssuer_id()); + } + }); + } + + return issuersDTO; + } + + @Override + public CredentialIssuerConfiguration getIssuerConfiguration(String issuerId) throws ApiNotAccessibleException, IOException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + String credentialIssuerHost = getIssuerDetails(issuerId).getCredential_issuer_host(); + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = getIssuerWellKnownResponse(credentialIssuerHost); + + if(credentialIssuerWellKnownResponse.getAuthorizationServers() == null || credentialIssuerWellKnownResponse.getAuthorizationServers().isEmpty()) { + credentialIssuerWellKnownResponse.setAuthorizationServers(List.of(credentialIssuerHost)); + } + AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse = issuersConfigUtil.getAuthServerWellknown(credentialIssuerWellKnownResponse.getAuthorizationServers().getFirst()); + + return new CredentialIssuerConfiguration( + credentialIssuerWellKnownResponse.getCredentialIssuer(), + credentialIssuerWellKnownResponse.getAuthorizationServers(), + credentialIssuerWellKnownResponse.getCredentialEndPoint(), + credentialIssuerWellKnownResponse.getCredentialConfigurationsSupported(), + authorizationServerWellKnownResponse + ); + } + + @Override + public CredentialIssuerWellKnownResponse getIssuerWellKnownResponse(String credentialIssuerHost) throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + return issuersConfigUtil.getIssuerWellknown(credentialIssuerHost); + } + + @Override + public IssuerConfig getIssuerConfig(String issuerId, @NotBlank String credentialType) throws ApiNotAccessibleException, InvalidIssuerIdException { + log.info("Fetching issuer config for issuerId: {}", issuerId); + try { + IssuerDTO issuerDTO = getIssuerDetails(issuerId); + CredentialIssuerWellKnownResponse wellKnownResponse = issuersConfigUtil.getIssuerWellknown(issuerDTO.getCredential_issuer_host()); + return new IssuerConfig( + issuerDTO, + wellKnownResponse, + wellKnownResponse.getCredentialConfigurationsSupported().get(credentialType) + ); + } catch (Exception e) { + log.error("Failed to fetch issuer config for issuerId: {}", issuerId, e); + if (e instanceof InvalidIssuerIdException) { + throw (InvalidIssuerIdException) e; + } + throw new ApiNotAccessibleException("Unable to fetch issuer configuration for issuerId: " + issuerId, e); + } + } + + @Override + public IssuersV2DTO getIssuersV2DTO() throws ApiNotAccessibleException, IOException { + IssuersDTO issuersDTO = getIssuers(null); + List list = issuersDTO.getIssuers().parallelStream().map(this::toIssuerV2DTO).collect(Collectors.toList()); + return new IssuersV2DTO(list); + } + + @Override + public IssuerV2DTO getIssuerV2Details(String issuerId) throws ApiNotAccessibleException, IOException { + IssuerDTO issuerDTO = getIssuerDetails(issuerId); + return toIssuerV2DTO(issuerDTO); + } + + /** + * Maps an {@link IssuerDTO} (from config) to {@link IssuerV2DTO} (API response). + */ + private IssuerV2DTO toIssuerV2DTO(IssuerDTO issuer) { + + IssuerV2DTO issuerV2DTO= new IssuerV2DTO(); + + issuerV2DTO.setIssuerId(issuer.getIssuer_id()); + issuerV2DTO.setProtocol(issuer.getProtocol()); + issuerV2DTO.setDisplay(issuer.getDisplay()); + issuerV2DTO.setClientId(issuer.getClient_id()); + issuerV2DTO.setTokenEndpoint(issuer.getToken_endpoint()); + issuerV2DTO.setClientAlias(issuer.getClient_alias()); + issuerV2DTO.setQrCodeType(issuer.getQr_code_type()); + issuerV2DTO.setEnabled(issuer.getEnabled()); + issuerV2DTO.setCredentialIssuerHost(issuer.getCredential_issuer_host()); + + return issuerV2DTO; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/KeyPairRetrievalServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/KeyPairRetrievalServiceImpl.java new file mode 100644 index 000000000..cd4e3188c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/KeyPairRetrievalServiceImpl.java @@ -0,0 +1,90 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.KeyGenerationException; +import io.mosip.mimoto.model.ProofSigningKey; +import io.mosip.mimoto.repository.ProofSigningKeyRepository; +import io.mosip.mimoto.service.DataProtectionService; +import io.mosip.mimoto.service.KeyPairRetrievalService; +import io.mosip.mimoto.util.SigningKeyUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.util.Base64; +import java.util.Optional; + +/** + * Service implementation for managing key pairs from database + */ +@Slf4j +@Service +public class KeyPairRetrievalServiceImpl implements KeyPairRetrievalService { + + private final ProofSigningKeyRepository proofSigningKeyRepository; + + private final DataProtectionService dataProtectionService; + + public KeyPairRetrievalServiceImpl(ProofSigningKeyRepository proofSigningKeyRepository, DataProtectionService dataProtectionService) { + this.proofSigningKeyRepository = proofSigningKeyRepository; + this.dataProtectionService = dataProtectionService; + } + + @Override + public KeyPair getKeyPairFromDB(String walletId, String base64EncodedWalletKey, SigningAlgorithm signingAlgorithm) throws KeyGenerationException, DecryptionException { + log.debug("Retrieving key pair for walletId: {} with algorithm: {}", walletId, signingAlgorithm); + + // Step 1: Retrieve proof signing key from database + Optional proofSigningKey = proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, signingAlgorithm.name()); + + if (proofSigningKey.isEmpty()) { + log.error("No proof signing key found for walletId: {} with algorithm: {}", walletId, signingAlgorithm); + throw new KeyGenerationException("KEY_NOT_FOUND", "No proof signing key found for walletId: " + walletId + " with algorithm: " + signingAlgorithm); + } + + // Step 2: Decode wallet key + byte[] decodedWalletKey; + try { + decodedWalletKey = Base64.getDecoder().decode(base64EncodedWalletKey); + } catch (IllegalArgumentException e) { + log.error("Invalid base64 encoded wallet key for walletId: {}", walletId, e); + throw new DecryptionException("INVALID_WALLET_KEY", "Invalid base64 encoded wallet key", e); + } + + SecretKey walletKey = DataProtectionService.bytesToSecretKey(decodedWalletKey); + + // Step 3: Decode public key + byte[] publicKeyBytes; + try { + publicKeyBytes = Base64.getDecoder().decode(proofSigningKey.get().getPublicKey()); + } catch (IllegalArgumentException e) { + log.error("Invalid base64 encoded public key for walletId: {}", walletId, e); + throw new KeyGenerationException("INVALID_PUBLIC_KEY", "Invalid base64 encoded public key", e); + } + + // Step 4: Decrypt private key + byte[] privateKeyInBytes; + try { + privateKeyInBytes = dataProtectionService.decryptWithAES(walletKey, proofSigningKey.get().getEncryptedSecretKey()); + } catch (Exception e) { + log.error("Failed to decrypt private key for walletId: {} with algorithm: {}", walletId, signingAlgorithm, e); + throw new DecryptionException("DECRYPTION_FAILED", "Failed to decrypt private key for walletId: " + walletId, e); + } + + // Step 5: Generate KeyPair from stored keys + KeyPair keyPair; + try { + keyPair = SigningKeyUtil.generateKeyPair(signingAlgorithm, publicKeyBytes, privateKeyInBytes); + } catch (Exception e) { + log.error("Failed to generate KeyPair for signing algorithm: {} for walletId: {}", signingAlgorithm, walletId, e); + throw new KeyGenerationException("KEY_GENERATION_FAILED", "Failed to generate KeyPair for algorithm: " + signingAlgorithm, e); + } + + log.debug("Successfully retrieved KeyPair for signing algorithm: {} from database", signingAlgorithm); + + return keyPair; + + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/LdpVcCredentialFormatHandler.java b/src/main/java/io/mosip/mimoto/service/impl/LdpVcCredentialFormatHandler.java new file mode 100644 index 000000000..fe2a85dbc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/LdpVcCredentialFormatHandler.java @@ -0,0 +1,149 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.service.CredentialFormatHandler; +import io.mosip.mimoto.util.LocaleUtils; +import static io.mosip.mimoto.util.IssuerConfigUtil.camelToTitleCase; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Slf4j +@Component("ldp_vc") +public class LdpVcCredentialFormatHandler implements CredentialFormatHandler { + + private final ObjectMapper objectMapper; + + public LdpVcCredentialFormatHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String getSupportedFormat() { + return CredentialFormat.LDP_VC.getFormat(); + } + + @Override + public Map extractCredentialClaims(VCCredentialResponse vcCredentialResponse) { + VCCredentialProperties credential = objectMapper.convertValue(vcCredentialResponse.getCredential(), VCCredentialProperties.class); + return (Map) credential.getCredentialSubject(); + } + + private void addFallbackDisplayProperties( + Map credentialProperties, + LinkedHashMap localizedDisplayMap, + String resolvedLocale) { + // fallback for missing display properties from issuer well-known + Set credentialFields = credentialProperties.keySet(); + Set missingDisplayFields = new HashSet<>(credentialFields); + missingDisplayFields.removeAll(localizedDisplayMap.keySet()); + // remove metadata fields that are not part of the display properties + missingDisplayFields.remove("id"); + + // Generate fallbacks for fields without well-known display properties + for (String missingField : missingDisplayFields) { + String displayName = camelToTitleCase(missingField); + + CredentialIssuerDisplayResponse fallbackDisplay = new CredentialIssuerDisplayResponse(); + fallbackDisplay.setName(displayName); + fallbackDisplay.setLocale("en"); + + localizedDisplayMap.put(missingField, fallbackDisplay); + } + } + + @Override + public LinkedHashMap> loadDisplayPropertiesFromWellknown( + Map credentialProperties, + CredentialsSupportedResponse credentialsSupportedResponse, + String userLocale) { + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + Set orderedKeys = Optional.ofNullable(credentialsSupportedResponse.getOrder()) + .map(LinkedHashSet::new) // preserve order + .orElse(new LinkedHashSet<>()); + + // Add remaining keys from credentialProperties that are not already in orderedKeys + for (String key : credentialProperties.keySet()) { + orderedKeys.add(key); + } + + // LDP VC format — display config is in "credential_definition.credential_subject" + if (credentialsSupportedResponse.getCredentialDefinition() == null || + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() == null) { + log.info("Issuer well-known has no credential definition or credential subject for LDP VC format; falling back to claim-based display properties"); + return buildFallbackDisplayProperties(credentialProperties, new ArrayList<>(orderedKeys)); + } + + Map displayConfigMap = + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject(); + + String resolvedLocale = LocaleUtils.resolveLocaleWithFallback(displayConfigMap, userLocale); + + LinkedHashMap localizedDisplayMap = new LinkedHashMap<>(); + + if (resolvedLocale != null) { + displayConfigMap.forEach((key, dto) -> { + dto.getDisplay().stream() + .filter(display -> LocaleUtils.matchesLocale(display.getLocale(), resolvedLocale)) + .findFirst() + .ifPresent(display -> localizedDisplayMap.put(key, display)); + }); + } + + addFallbackDisplayProperties(credentialProperties, localizedDisplayMap, resolvedLocale); + + List fieldKeys = (orderedKeys != null && !orderedKeys.isEmpty()) + ? new ArrayList<>(orderedKeys) + : new ArrayList<>(localizedDisplayMap.keySet()); + + for (String key : fieldKeys) { + CredentialIssuerDisplayResponse display = localizedDisplayMap.get(key); + Object value = credentialProperties.get(key); + if (display != null && value != null) { + displayProperties.put(key, Map.of(display, value)); + } + } + + return displayProperties; + } + + @Override + public Map extractAllCredentialProperties(VCCredentialResponse vcCredentialResponse) { + return objectMapper.convertValue(vcCredentialResponse.getCredential(), LinkedHashMap.class); + } + + private LinkedHashMap> buildFallbackDisplayProperties( + Map credentialProperties, + List orderedKeys) { + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + // Determine field order (prefer issuer-provided 'order' if any) + List fieldKeys = (orderedKeys != null && !orderedKeys.isEmpty()) + ? new ArrayList<>(orderedKeys) + : new ArrayList<>(credentialProperties.keySet()); + + // Exclude non-claim metadata + fieldKeys.remove("id"); + + // Build default display entries from claims + for (String key : fieldKeys) { + Object value = credentialProperties.get(key); + if (value == null) { + continue; + } + + // Generate fallback display using vc keys + CredentialIssuerDisplayResponse display = new CredentialIssuerDisplayResponse(); + display.setName(camelToTitleCase(key)); + display.setLocale("en"); + + displayProperties.put(key, Map.of(display, value)); + } + return displayProperties; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/OpenID4VPService.java b/src/main/java/io/mosip/mimoto/service/impl/OpenID4VPService.java new file mode 100644 index 000000000..66a390558 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/OpenID4VPService.java @@ -0,0 +1,179 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.service.VerifierService; +import io.mosip.openID4VP.OpenID4VP; +import io.mosip.mimoto.util.AuthorizationRequestHelper; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.authorizationRequest.LdpVpFormatSupported; +import io.mosip.openID4VP.authorizationRequest.SdJwtVpFormatSupported; +import io.mosip.openID4VP.authorizationRequest.VPFormatSupported; +import io.mosip.openID4VP.authorizationRequest.Verifier; +import io.mosip.openID4VP.authorizationRequest.WalletConfig; +import io.mosip.openID4VP.authorizationRequest.presentationDefinition.PresentationDefinition; +import io.mosip.openID4VP.common.OpenID4VPErrorCodes; +import io.mosip.openID4VP.constants.ProofType; +import io.mosip.openID4VP.constants.VPFormatType; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import io.mosip.openID4VP.verifier.VerifierResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; + +import static io.mosip.openID4VP.authorizationRequest.WalletConfigDefaultsKt.getDefaultClientIdPrefixesSupported; +import static io.mosip.openID4VP.authorizationRequest.WalletConfigDefaultsKt.getDefaultEncryptionAlgorithmSupported; +import static io.mosip.openID4VP.authorizationRequest.WalletConfigDefaultsKt.getDefaultEncryptionMethodSupported; +import static io.mosip.openID4VP.authorizationRequest.WalletConfigDefaultsKt.getDefaultResponseTypeSupported; +import static io.mosip.openID4VP.authorizationRequest.WalletConfigDefaultsKt.getDefaultSignatureAlgorithmSupported; + +@Component +@Slf4j +public class OpenID4VPService { + + private final VerifierService verifierService; + + public OpenID4VPService(VerifierService verifierService) { + this.verifierService = verifierService; + } + + public OpenID4VP create(String presentationId, List trustedVerifiers, boolean validateTrustedVerifier) { + return new OpenID4VP(presentationId, buildWalletConfig(trustedVerifiers, validateTrustedVerifier)); + } + + private WalletConfig buildWalletConfig(List trustedVerifiers, boolean validateTrustedVerifier) { + Map vpFormatsSupported = Map.of( + VPFormatType.LDP_VC, new LdpVpFormatSupported(List.of(ProofType.Ed25519Signature2020), null), + VPFormatType.VC_SD_JWT, new SdJwtVpFormatSupported(List.of("ES256", "EdDSA"), List.of("ES256", "EdDSA")), + VPFormatType.DC_SD_JWT, new SdJwtVpFormatSupported(List.of("ES256", "EdDSA"), List.of("ES256", "EdDSA")) + ); + + WalletConfig defaultConfig = new WalletConfig(); + return new WalletConfig( + vpFormatsSupported, + getDefaultClientIdPrefixesSupported(), + getDefaultSignatureAlgorithmSupported(), + getDefaultEncryptionAlgorithmSupported(), + getDefaultEncryptionMethodSupported(), + getDefaultResponseTypeSupported(), + defaultConfig.isPresentationDefinitionUriSupported(), + trustedVerifiers != null ? trustedVerifiers : List.of(), + validateTrustedVerifier + ); + } + + /** + * Authenticates the verifier authorization request and returns the presentation definition + * for Draft-23 (Presentation Exchange) requests. Returns {@code null} for DCQL requests + * or when {@code presentationId} or {@code authRequest} are missing. + * + * @param presentationId presentation session identifier from session data + * @param authRequest raw authorization request from session data + * @param isVerifierClientPreregistered whether the verifier client is pre-registered + * @return the presentation definition for Presentation Exchange requests, otherwise {@code null} + */ + public PresentationDefinition resolvePresentationDefinition( + String presentationId, String authRequest, boolean isVerifierClientPreregistered) + throws ApiNotAccessibleException, IOException { + + if (presentationId == null || authRequest == null) { + return null; + } + + OpenID4VP openID4VP = create(presentationId, getPreRegisteredVerifiers(), isVerifierClientPreregistered); + AuthorizationRequest authorizationRequest = openID4VP.authenticateVerifier(authRequest); + + if (authorizationRequest instanceof AuthorizationPresentationExchangeRequest peRequest) { + return peRequest.getPresentationDefinition(); + } + return null; + } + + /** + * Authenticates the verifier authorization request and returns the DCQL query + * for OVP 1.0 (DCQL) requests. Returns {@code null} for Presentation Exchange requests + * or when {@code presentationId} or {@code authRequest} are missing. + * + * @param presentationId presentation session identifier from session data + * @param authRequest raw authorization request from session data + * @param isVerifierClientPreregistered whether the verifier client is pre-registered + * @return the DCQL query for DCQL authorization requests, otherwise {@code null} + */ + public DCQLQuery resolveDcqlQuery( + String presentationId, String authRequest, boolean isVerifierClientPreregistered) + throws ApiNotAccessibleException, IOException { + + if (presentationId == null || authRequest == null) { + return null; + } + + OpenID4VP openID4VP = create(presentationId, getPreRegisteredVerifiers(), isVerifierClientPreregistered); + AuthorizationRequest authorizationRequest = openID4VP.authenticateVerifier(authRequest); + return AuthorizationRequestHelper.extractDcqlQuery(authorizationRequest); + } + + /** + * Reconstructs OpenID4VP from session data, authenticates the verifier, and sends the OpenID4VP error to the verifier. + * + * @param sessionData session containing presentation id and original authorization request + * @param payload the error payload to forward + * @return network response from verifier + * @throws ApiNotAccessibleException when verifier list can't be fetched + * @throws IOException for underlying IO failures + * @throws URISyntaxException when the verifier response URI is invalid + */ + public VerifierResponse sendErrorToVerifier( + VerifiablePresentationSessionData sessionData, ErrorDTO payload) + throws ApiNotAccessibleException, IOException, URISyntaxException { + + if (sessionData == null || sessionData.getPresentationId() == null + || sessionData.getAuthorizationRequest() == null) { + throw new IllegalArgumentException("Invalid presentation session data"); + } + + OpenID4VP openID4VP = create( + sessionData.getPresentationId(), + getPreRegisteredVerifiers(), + sessionData.isVerifierClientPreregistered()); + + // authenticateVerifier to populate internal state in OpenID4VP before sending error + openID4VP.authenticateVerifier(sessionData.getAuthorizationRequest()); + + Exception errorForVerifier = openId4VPErrorException(payload); + VerifierResponse verifierResponse = openID4VP.sendErrorInfoToVerifier(errorForVerifier); + log.info("Sent error to verifier for presentationId={}. Response: {}", + sessionData.getPresentationId(), verifierResponse); + return verifierResponse; + } + + public List getPreRegisteredVerifiers() throws ApiNotAccessibleException, IOException { + return verifierService.getTrustedVerifiers().getVerifiers().stream() + .map(v -> new Verifier(v.getClientId(), v.getResponseUris(), v.getJwksUri(), v.getAllowUnsignedRequest(), v.getSpecVersion())) + .toList(); + } + + /** + * Maps wallet {@link ErrorDTO#errorCode} to inji-openid4vp exceptions {@code error} + * matches (e.g. {@link OpenID4VPErrorCodes#INVALID_TRANSACTION_DATA} vs {@link OpenID4VPErrorCodes#ACCESS_DENIED}). + */ + private Exception openId4VPErrorException(ErrorDTO payload) { + if (payload == null) { + throw new IllegalArgumentException("Error payload must not be null"); + } + String message = payload.getErrorMessage() != null ? payload.getErrorMessage() : ""; + String code = payload.getErrorCode(); + + if (OpenID4VPErrorCodes.INVALID_TRANSACTION_DATA.equals(code)) { + return new OpenID4VPExceptions.InvalidTransactionData(message, "OpenID4VPService"); + } + // Default to AccessDenied for any unrecognized error codes + return new OpenID4VPExceptions.AccessDenied(message, "OpenID4VPService"); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/PresentationServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/PresentationServiceImpl.java new file mode 100644 index 000000000..c541cfe97 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/PresentationServiceImpl.java @@ -0,0 +1,300 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponseProof; +import io.mosip.mimoto.dto.openid.presentation.*; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.PresentationService; +import io.mosip.mimoto.util.RestApiClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.util.JwtUtils.extractJwtPayloadFromSdJwt; +import static io.mosip.mimoto.util.JwtUtils.parseJwtHeader; + +@Slf4j +@Service +public class PresentationServiceImpl implements PresentationService { + + private final DataShareServiceImpl dataShareService; + + private final ObjectMapper objectMapper; + + private final RestApiClient restApiClient; + + private final String injiOvpRedirectURLPattern; + + private final Integer maximumResponseHeaderSize; + + public PresentationServiceImpl( + DataShareServiceImpl dataShareService, + ObjectMapper objectMapper, + RestApiClient restApiClient, + @Value("${mosip.inji.ovp.redirect.url.pattern}") String injiOvpRedirectURLPattern, + @Value("${server.tomcat.max-http-response-header-size:65536}") Integer maximumResponseHeaderSize + ) { + this.dataShareService = dataShareService; + this.objectMapper = objectMapper; + this.restApiClient = restApiClient; + this.injiOvpRedirectURLPattern = injiOvpRedirectURLPattern; + this.maximumResponseHeaderSize = maximumResponseHeaderSize; + } + + + @Override + public String authorizePresentation(PresentationRequestDTO presentationRequestDTO) throws IOException { + VCCredentialResponse vcCredentialResponse = dataShareService.downloadCredentialFromDataShare(presentationRequestDTO); + PresentationDefinitionDTO presentationDefinitionDTO = presentationRequestDTO.getPresentationDefinition(); + if (presentationDefinitionDTO == null) { + throw new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage()); + } + + log.info("Started the Constructing VP Token"); + + return presentationDefinitionDTO.getInputDescriptors() + .stream() + .findFirst() + .map(inputDescriptorDTO -> { + try { + return processInputDescriptor(vcCredentialResponse, inputDescriptorDTO, presentationRequestDTO, presentationDefinitionDTO); + } catch (JsonProcessingException e) { + log.error("Exception occured during processInputDesciptor: {}", e.getMessage()); + throw new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage()); + } + }) + .orElseThrow(() -> new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage())); + } + + + + public PresentationDefinitionDTO constructPresentationDefinition(VCCredentialResponse vcRes) { + String vcFormat = vcRes.getFormat(); + List inputDescriptors = new ArrayList<>(); + + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(vcFormat)) { + VCCredentialProperties ldp = objectMapper.convertValue(vcRes.getCredential(), VCCredentialProperties.class); + String lastType = ldp.getType().get(ldp.getType().size() - 1); + String proofType = Optional.ofNullable(ldp.getProof()).map(VCCredentialResponseProof::getType).orElse(null); + + FieldDTO field = FieldDTO.builder() + .path(new String[]{"$.type"}) + .filter(FilterDTO.builder().type("String").pattern(lastType).build()) + .build(); + + Map>> format = Map.of( + "ldpVc", Map.of("proofTypes", List.of(proofType)) + ); + + inputDescriptors.add(InputDescriptorDTO.builder() + .id(UUID.randomUUID().toString()) + .constraints(ConstraintsDTO.builder().fields(new FieldDTO[]{field}).build()) + .format(format) + .build()); + + } else if (CredentialFormat.VC_SD_JWT.getFormat().equalsIgnoreCase(vcFormat) || CredentialFormat.DC_SD_JWT.getFormat().equalsIgnoreCase(vcFormat)) { + Map jwtPayload = extractJwtPayloadFromSdJwt((String) vcRes.getCredential()); + List typeList = (List) jwtPayload.get("type"); + String lastType = null; + if (typeList != null && !typeList.isEmpty()) { + Object lastItem = typeList.get(typeList.size() - 1); + if (lastItem instanceof Map) { + Object value = ((Map) lastItem).get("_value"); + lastType = value != null ? value.toString() : null; + } else { + lastType = lastItem.toString(); + } + } + Map jwtHeaders = parseJwtHeader((String) vcRes.getCredential()); + String algo = (String) jwtHeaders.get("alg"); + + FieldDTO field = FieldDTO.builder() + .path(new String[]{"$.type"}) + .filter(FilterDTO.builder().type("String").pattern(lastType).build()) + .build(); + Map>> format = Map.of( + vcRes.getFormat(), Map.of( + "sd-jwt_alg_values", List.of(algo) + ) + ); + inputDescriptors.add(InputDescriptorDTO.builder() + .id(UUID.randomUUID().toString()) + .constraints(ConstraintsDTO.builder().fields(new FieldDTO[]{field}).build()) + .format(format) + .build()); + + } + return PresentationDefinitionDTO.builder() + .id(UUID.randomUUID().toString()) + .inputDescriptors(inputDescriptors) + .build(); + } + + + private String processInputDescriptor(VCCredentialResponse vcCredentialResponse, InputDescriptorDTO inputDescriptorDTO, + PresentationRequestDTO presentationRequestDTO, PresentationDefinitionDTO presentationDefinitionDTO) throws JsonProcessingException { + String format = vcCredentialResponse.getFormat(); + VerifiablePresentationDTO vpDTO; + + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(format)) { + VCCredentialProperties ldpCredential = objectMapper.convertValue(vcCredentialResponse.getCredential(), VCCredentialProperties.class); + if (inputDescriptorDTO.getFormat().get("ldpVc").get("proofTypes") + .stream().anyMatch(proofType -> ldpCredential.getProof().getType().equals(proofType))) { + vpDTO = constructVerifiablePresentationString(ldpCredential); + } else { + throw new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage()); + } + } else if (CredentialFormat.VC_SD_JWT.getFormat().equalsIgnoreCase(format) + || CredentialFormat.DC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + String credential = objectMapper.convertValue(vcCredentialResponse.getCredential(), String.class); + Map jwtHeaders = parseJwtHeader(credential); + String responseAlgo = (String) jwtHeaders.get("alg"); + if (inputDescriptorDTO.getFormat().get(format).get("sd-jwt_alg_values") + .stream().anyMatch(responseAlgo::equals)) { + vpDTO = constructVerifiablePresentationStringForSDjwt(credential); + } else { + throw new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage()); + } + } else { + throw new VPNotCreatedException(ErrorConstants.INVALID_REQUEST.getErrorMessage()); + } + + // Create VP Token + String vpToken = createVpToken(vpDTO); + + // Create PresentationSubmission + String presentationSubmission = constructPresentationSubmission(format, vpDTO, presentationDefinitionDTO, inputDescriptorDTO); + + // If response_uri is present, POST the response + if (presentationRequestDTO.getResponseMode() != null && "direct_post".equals(presentationRequestDTO.getResponseMode())) { + return postVpToResponseUri( + presentationRequestDTO.getResponseUri(), + presentationRequestDTO.getRedirectUri(), + vpToken, + presentationSubmission, + presentationRequestDTO.getState(), + presentationRequestDTO.getNonce() + ); + } + + // Otherwise, do redirect + String redirectString = buildRedirectString( + vpToken, + presentationRequestDTO.getRedirectUri(), + presentationSubmission + ); + + if (redirectString.length() > maximumResponseHeaderSize) { + throw new VPNotCreatedException(ErrorConstants.URI_TOO_LONG.getErrorCode(), ErrorConstants.URI_TOO_LONG.getErrorMessage()); + } + + return redirectString; + } + + private String buildRedirectString(String vpToken, String redirectUri, String presentationSubmission) { + return String.format(injiOvpRedirectURLPattern, + redirectUri, + Base64.getUrlEncoder().encodeToString(vpToken.getBytes(StandardCharsets.UTF_8)), + URLEncoder.encode(presentationSubmission, StandardCharsets.UTF_8)); + } + + private String createVpToken(VerifiablePresentationDTO vpDTO) throws JsonProcessingException { + return objectMapper.writeValueAsString(vpDTO); + } + + private String postVpToResponseUri(String responseUri, String redirectUri, String vpToken, String presentationSubmission, String state, String nonce) throws JsonProcessingException { + MultiValueMap postRequest = new LinkedMultiValueMap<>(); + postRequest.add("vp_token", Base64.getUrlEncoder().encodeToString(vpToken.getBytes(StandardCharsets.UTF_8))); + postRequest.add("presentation_submission", presentationSubmission); + + if (state != null) { + postRequest.add("state", state); + } + + log.info("Posting VP to response_uri: {}", responseUri); + try { + Map postResponse = restApiClient.postApi( + responseUri, + MediaType.APPLICATION_FORM_URLENCODED, + postRequest, + Map.class + ); + + log.info("Response from verifier after POST: {}", postResponse); + + // Check for redirect_uri in response first + if (postResponse != null && postResponse.containsKey("redirect_uri")) { + String responseRedirectUri = (String) postResponse.get("redirect_uri"); + if (responseRedirectUri != null && !responseRedirectUri.isEmpty()) { + return responseRedirectUri; + } + } + + // Use request's redirectUri if it's non-blank + if (redirectUri != null && !redirectUri.isBlank()) { + log.info("Using redirectUri from request: {}", redirectUri); + return redirectUri; + } + + // Fallback behavior if redirect_uri is not provided + log.warn("No redirect_uri received from verifier in POST response. Falling back to response_uri."); + return responseUri + "?status=vp_sent"; + + } catch (Exception e) { + log.error("Exception while submitting the vp_token to the response_uri", e); + throw new VPNotCreatedException(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage()); + } + } + + private VerifiablePresentationDTO constructVerifiablePresentationString(VCCredentialProperties vcCredentialProperties) { + Object context = vcCredentialProperties.getContext(); + List contextList = (context instanceof List list) + ? (List) list + : List.of(context); + + return VerifiablePresentationDTO.builder() + .verifiableCredential(Collections.singletonList(vcCredentialProperties)) + .type(Collections.singletonList("VerifiablePresentation")) + .context(contextList) + .build(); + } + + private VerifiablePresentationDTO constructVerifiablePresentationStringForSDjwt(String vcCredential) { + return VerifiablePresentationDTO.builder() + .verifiableCredential(Collections.singletonList(vcCredential)) + .type(Collections.singletonList("VerifiablePresentation")) + .build(); + } + + private String constructPresentationSubmission(String format, VerifiablePresentationDTO verifiablePresentationDTO, PresentationDefinitionDTO presentationDefinitionDTO, InputDescriptorDTO inputDescriptorDTO) throws JsonProcessingException { + AtomicInteger atomicInteger = new AtomicInteger(0); + List submissionDescriptorDTOList = verifiablePresentationDTO.getVerifiableCredential() + .stream().map(verifiableCredential -> SubmissionDescriptorDTO.builder() + .id(inputDescriptorDTO.getId()) + .format(format) + .path("$.verifiableCredential[" + atomicInteger.getAndIncrement() + "]").build()).collect(Collectors.toList()); + + PresentationSubmissionDTO presentationSubmissionDTO = PresentationSubmissionDTO.builder() + .id(UUID.randomUUID().toString()) + .definition_id(presentationDefinitionDTO.getId()) + .descriptorMap(submissionDescriptorDTOList).build(); + return objectMapper.writeValueAsString(presentationSubmissionDTO); + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/RestClientServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/RestClientServiceImpl.java index cfadd6ed2..5d533e634 100644 --- a/src/main/java/io/mosip/mimoto/service/impl/RestClientServiceImpl.java +++ b/src/main/java/io/mosip/mimoto/service/impl/RestClientServiceImpl.java @@ -1,45 +1,45 @@ package io.mosip.mimoto.service.impl; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; +import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.constant.LoggerFileConstant; +import io.mosip.mimoto.exception.ApisResourceAccessException; +import io.mosip.mimoto.exception.PlatformErrorMessages; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.util.RestApiClient; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.constant.ApiName; -import io.mosip.mimoto.exception.ApisResourceAccessException; -import io.mosip.mimoto.exception.PlatformErrorMessages; -import io.mosip.mimoto.service.RestClientService; -import io.mosip.mimoto.util.LoggerUtil; -import io.mosip.mimoto.util.RestApiClient; +import java.util.List; /** * The Class RestClientServiceImpl. - * + * * @author Rishabh Keshari */ +@Slf4j @Service public class RestClientServiceImpl implements RestClientService { - /** The logger. */ - Logger logger = LoggerUtil.getLogger(RestClientServiceImpl.class); - /** The rest api client. */ - @Autowired - private RestApiClient restApiClient; + private final RestApiClient restApiClient; /** The env. */ - @Autowired - private Environment env; + private final Environment env; + + public RestClientServiceImpl(RestApiClient restApiClient, Environment env) { + this.restApiClient = restApiClient; + this.env = env; + } /* * (non-Javadoc) - * + * * @see io.mosip.registration.processor.core.spi.restclient. * RestClientService#getApi(io.mosip.registration. * processor .core.code.ApiName, @@ -49,7 +49,7 @@ public class RestClientServiceImpl implements RestClientService { @Override public Object getApi(ApiName apiName, List pathsegments, String queryParamName, String queryParamValue, Class responseType) throws ApisResourceAccessException { - logger.debug("RestClientServiceImpl::getApi()::entry"); + log.debug("RestClientServiceImpl::getApi()::entry"); Object obj = null; String apiHostIpPort = env.getProperty(apiName.name()); @@ -58,40 +58,29 @@ public Object getApi(ApiName apiName, List pathsegments, String queryPar if (apiHostIpPort != null) { builder = UriComponentsBuilder.fromUriString(apiHostIpPort); - if (!((pathsegments == null) || (pathsegments.isEmpty()))) { - for (String segment : pathsegments) { - if (!((segment == null) || (("").equals(segment)))) { - builder.pathSegment(segment); - } - } - + if (!CollectionUtils.isEmpty(pathsegments)) { + addPathSegments(pathsegments, builder); } - if (!((queryParamName == null) || (("").equals(queryParamName)))) { - - String[] queryParamNameArr = queryParamName.split(","); - String[] queryParamValueArr = queryParamValue.split(","); - for (int i = 0; i < queryParamNameArr.length; i++) { - builder.queryParam(queryParamNameArr[i], queryParamValueArr[i]); - } - + if (!StringUtils.isEmpty(queryParamName)) { + addQueryParam(queryParamName, queryParamValue, builder); } try { uriComponents = builder.build(false).encode(); - logger.debug(uriComponents.toUri().toString(), "URI"); + log.debug(uriComponents.toUri().toString(), "URI"); obj = restApiClient.getApi(uriComponents.toUri(), responseType); } catch (Exception e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); throw new ApisResourceAccessException( PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getCode(), e); } } - logger.debug("RestClientServiceImpl::getApi()::exit"); + log.debug("RestClientServiceImpl::getApi()::exit"); return obj; } @@ -99,7 +88,7 @@ public Object getApi(ApiName apiName, List pathsegments, String queryPar public Object getApi(ApiName apiName, List pathsegments, List queryParamName, List queryParamValue, Class responseType) throws ApisResourceAccessException { - logger.debug("RestClientServiceImpl::getApi()::entry"); + log.debug("RestClientServiceImpl::getApi()::entry"); Object obj = null; String apiHostIpPort = env.getProperty(apiName.name()); @@ -108,16 +97,11 @@ public Object getApi(ApiName apiName, List pathsegments, List qu if (apiHostIpPort != null) { builder = UriComponentsBuilder.fromUriString(apiHostIpPort); - if (!((pathsegments == null) || (pathsegments.isEmpty()))) { - for (String segment : pathsegments) { - if (!((segment == null) || (("").equals(segment)))) { - builder.pathSegment(segment); - } - } - + if (!CollectionUtils.isEmpty(pathsegments)) { + addPathSegments(pathsegments, builder); } - if (((queryParamName != null) && (!queryParamName.isEmpty()))) { + if (!CollectionUtils.isEmpty(queryParamName)) { for (int i = 0; i < queryParamName.size(); i++) { builder.queryParam(queryParamName.get(i), queryParamValue.get(i)); } @@ -127,24 +111,24 @@ public Object getApi(ApiName apiName, List pathsegments, List qu try { uriComponents = builder.build(false).encode(); - logger.debug(uriComponents.toUri().toString(), "URI"); + log.debug(uriComponents.toUri().toString(), "URI"); obj = restApiClient.getApi(uriComponents.toUri(), responseType); } catch (Exception e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); throw new ApisResourceAccessException( PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getCode(), e); } } - logger.debug("RestClientServiceImpl::getApi()::exit"); + log.debug("RestClientServiceImpl::getApi()::exit"); return obj; } public Object postApi(ApiName apiName, String queryParamName, String queryParamValue, Object requestedData, Class responseType, MediaType mediaType) throws ApisResourceAccessException { - logger.debug("RestClientServiceImpl::postApi()::entry"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_ENTRY); Object obj = null; String apiHostIpPort = env.getProperty(apiName.name()); @@ -153,33 +137,28 @@ public Object postApi(ApiName apiName, String queryParamName, String queryParamV builder = UriComponentsBuilder.fromUriString(apiHostIpPort); if (builder != null) { - if (!((queryParamName == null) || (("").equals(queryParamName)))) { - String[] queryParamNameArr = queryParamName.split(","); - String[] queryParamValueArr = queryParamValue.split(","); - - for (int i = 0; i < queryParamNameArr.length; i++) { - builder.queryParam(queryParamNameArr[i], queryParamValueArr[i]); - } + if (!StringUtils.isEmpty(queryParamName)) { + addQueryParam(queryParamName, queryParamValue, builder); } try { obj = restApiClient.postApi(builder.toUriString(), mediaType, requestedData, responseType); } catch (Exception e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); throw new ApisResourceAccessException( PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getMessage(), e); } } - logger.debug("RestClientServiceImpl::postApi()::exit"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_EXIT); return obj; } /* * (non-Javadoc) - * + * * @see io.mosip.registration.processor.core.spi.restclient. * RestClientService#postApi(io.mosip.registration. * processor.core.code.ApiName, @@ -194,7 +173,7 @@ public Object postApi(ApiName apiName, String queryParamName, String queryParamV /* * (non-Javadoc) - * + * * @see io.mosip.registration.processor.core.spi.restclient. * RestClientService#postApi(io.mosip.registration. * processor.core.code.ApiName, java.util.List, java.lang.String, @@ -204,7 +183,7 @@ public Object postApi(ApiName apiName, String queryParamName, String queryParamV public Object postApi(ApiName apiName, List pathsegments, String queryParamName, String queryParamValue, Object requestedData, Class responseType) throws ApisResourceAccessException { - logger.debug("RestClientServiceImpl::postApi()::entry"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_ENTRY); Object obj = null; String apiHostIpPort = env.getProperty(apiName.name()); UriComponentsBuilder builder = null; @@ -212,35 +191,30 @@ public Object postApi(ApiName apiName, List pathsegments, String queryPa builder = UriComponentsBuilder.fromUriString(apiHostIpPort); if (builder != null) { - if (!((pathsegments == null) || (pathsegments.isEmpty()))) { + if (!CollectionUtils.isEmpty(pathsegments)) { for (String segment : pathsegments) { - if (!((segment == null) || (("").equals(segment)))) { + if (!StringUtils.isEmpty(segment)) { builder.pathSegment(segment); } } } - if (!checkNull(queryParamName)) { - String[] queryParamNameArr = queryParamName.split(","); - String[] queryParamValueArr = queryParamValue.split(","); - - for (int i = 0; i < queryParamNameArr.length; i++) { - builder.queryParam(queryParamNameArr[i], queryParamValueArr[i]); - } + if (!StringUtils.isEmpty(queryParamName)) { + addQueryParam(queryParamName, queryParamValue, builder); } try { obj = restApiClient.postApi(builder.toUriString(), null, requestedData, responseType); } catch (Exception e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); throw new ApisResourceAccessException( PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getMessage(), e); } } - logger.debug("RestClientServiceImpl::postApi()::exit"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_EXIT); return obj; } @@ -249,7 +223,7 @@ public Object postApi(ApiName apiName, MediaType mediaType, List pathseg List queryParamValue, Object requestedData, Class responseType) throws ApisResourceAccessException { - logger.debug("RestClientServiceImpl::postApi()::entry"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_ENTRY); Object obj = null; String apiHostIpPort = env.getProperty(apiName.name()); UriComponentsBuilder builder = null; @@ -257,13 +231,8 @@ public Object postApi(ApiName apiName, MediaType mediaType, List pathseg builder = UriComponentsBuilder.fromUriString(apiHostIpPort); if (builder != null) { - if (!((pathsegments == null) || (pathsegments.isEmpty()))) { - for (String segment : pathsegments) { - if (!((segment == null) || (("").equals(segment)))) { - builder.pathSegment(segment); - } - } - + if (!CollectionUtils.isEmpty(pathsegments)) { + addPathSegments(pathsegments, builder); } if (!CollectionUtils.isEmpty(queryParamName)) { @@ -276,27 +245,50 @@ public Object postApi(ApiName apiName, MediaType mediaType, List pathseg obj = restApiClient.postApi(builder.toUriString(), mediaType, requestedData, responseType); } catch (Exception e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); throw new ApisResourceAccessException( PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getMessage(), e); } } - logger.debug("RestClientServiceImpl::postApi()::exit"); + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_EXIT); return obj; } - /** - * Check null. - * - * @param queryParamName - * the query param name - * @return true, if successful - */ - private boolean checkNull(String queryParamName) { + @Override + public Object postApi(ApiName apiName, + Object requestedData, Class responseType, boolean useBearerToken) throws ApisResourceAccessException { + + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_ENTRY); + Object obj = null; + String apiHostIpPort = env.getProperty(apiName.name()); + if (apiHostIpPort != null) { + try { + obj = restApiClient.postApi(apiHostIpPort, MediaType.APPLICATION_JSON, requestedData, responseType, useBearerToken); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new ApisResourceAccessException( + PlatformErrorMessages.MIMOTO_RCT_UNKNOWN_RESOURCE_EXCEPTION.getMessage(), e); + } + } + log.debug(LoggerFileConstant.REST_CLIENT_SERVICE_IMPL_POST_API_EXIT); + return obj; + } - return ((queryParamName == null) || (("").equals(queryParamName))); + private static void addQueryParam(String queryParamName, String queryParamValue, UriComponentsBuilder builder) { + String[] queryParamNameArr = queryParamName.split(","); + String[] queryParamValueArr = queryParamValue.split(","); + for (int i = 0; i < queryParamNameArr.length; i++) { + builder.queryParam(queryParamNameArr[i], queryParamValueArr[i]); + } } -} \ No newline at end of file + private void addPathSegments(List pathSegments, UriComponentsBuilder builder) { + for (String segment : pathSegments) { + if (!StringUtils.isEmpty(segment)) { + builder.pathSegment(segment); + } + } + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/SecurityContextManager.java b/src/main/java/io/mosip/mimoto/service/impl/SecurityContextManager.java new file mode 100644 index 000000000..6430f15fe --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/SecurityContextManager.java @@ -0,0 +1,25 @@ +package io.mosip.mimoto.service.impl; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.stereotype.Service; + +@Service +public class SecurityContextManager { + private final SecurityContextRepository securityContextRepository; + + public SecurityContextManager() { + this.securityContextRepository = new HttpSessionSecurityContextRepository(); + } + + public void setupSecurityContext(OAuth2AuthenticationToken oauth2AuthenticationToken, HttpServletRequest request, HttpServletResponse response) { + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(oauth2AuthenticationToken); + securityContextRepository.saveContext(context, request, response); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/SessionManager.java b/src/main/java/io/mosip/mimoto/service/impl/SessionManager.java new file mode 100644 index 000000000..d39a72c97 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/SessionManager.java @@ -0,0 +1,113 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class SessionManager { + + private final ObjectMapper objectMapper; + + public SessionManager(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public void setupSession(HttpServletRequest request, String provider, UserMetadataDTO userMetadata, String userId) { + HttpSession session = request.getSession(true); + session.setAttribute(SessionKeys.CLIENT_REGISTRATION_ID, provider); + session.setAttribute(SessionKeys.USER_METADATA, userMetadata); + session.setAttribute(SessionKeys.USER_ID, userId); + } + + public void storePresentationSessionData(HttpSession httpSession, VerifiablePresentationSessionData sessionData, String walletId) { + Map presentations = getOrCreateSessionMap(httpSession, SessionKeys.PRESENTATIONS + "::" + walletId); + + // Adds the new presentation to the map if it is not already present + presentations.computeIfAbsent(sessionData.getPresentationId(), id -> sessionData); + + // Store the updated presentations map in the session + httpSession.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + } + + /** + * Retrieves the verifiable presentation session data from session for a given presentation + * ID. + * + * @param httpSession The HTTP session. + * @param presentationId The presentation ID. + * @return The verifiable presentation session data if found, null otherwise. + */ + public VerifiablePresentationSessionData getPresentationSessionData(HttpSession httpSession, String walletId, String presentationId) { + + validateInputParameters(httpSession, walletId, presentationId); + + Map presentations = (Map) httpSession.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + if (presentations == null || !presentations.containsKey(presentationId)) + throw new IllegalArgumentException("presentationId not found in session"); + return presentations.get(presentationId); + } + + private void validateInputParameters(HttpSession httpSession, String walletId, String presentationId) { + if (httpSession == null) { + throw new IllegalArgumentException("HTTP session is invalid"); + } + if (walletId == null || walletId.trim().isEmpty()) { + throw new IllegalArgumentException("Wallet ID is invalid"); + } + if (presentationId == null || presentationId.trim().isEmpty()) { + throw new IllegalArgumentException("presentationId not found in session"); + } + } + + /** + * Stores the matching credentials response and filtered decrypted credentials in the session cache. + * + * @param httpSession The HTTP session. + * @param walletId The wallet ID. + * @param existingSessionData The existing object in cache. + * @param credentials The decrypted credentials to cache. + */ + public void storeMatchingWalletCredentialsInPresentationSessionData(HttpSession httpSession, String walletId, VerifiablePresentationSessionData existingSessionData, List credentials) { + VerifiablePresentationSessionData updatedSessionData = new VerifiablePresentationSessionData( + existingSessionData.getPresentationId(), + existingSessionData.getAuthorizationRequest(), + existingSessionData.getCreatedAt(), + existingSessionData.isVerifierClientPreregistered(), + credentials, + existingSessionData.isDcql()); + + // Update the presentations map with the updated session data + Map presentations = (Map) httpSession.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + presentations.put(updatedSessionData.getPresentationId(), updatedSessionData); + httpSession.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + } + + /** + * Helper method to get or create a session map attribute for VerifiablePresentationSessionData objects. + * This eliminates duplication in session map handling across store methods. + * + * @param httpSession The HTTP session. + * @param key The session attribute key. + * @return The existing map or a new HashMap if none exists. + */ + @SuppressWarnings("unchecked") + private Map getOrCreateSessionMap(HttpSession httpSession, String key) { + Map sessionMap = (Map) httpSession.getAttribute(key); + if (sessionMap == null) { + sessionMap = new HashMap<>(); + } + return sessionMap; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/TrustedVerifierServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/TrustedVerifierServiceImpl.java new file mode 100644 index 000000000..6a5322bb5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/TrustedVerifierServiceImpl.java @@ -0,0 +1,43 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.TrustedVerifierRequest; +import io.mosip.mimoto.dto.TrustedVerifierResponseDTO; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.model.TrustedVerifier; +import io.mosip.mimoto.repository.VerifierRepository; +import io.mosip.mimoto.service.TrustedVerifierService; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +@Service +public class TrustedVerifierServiceImpl implements TrustedVerifierService { + + private final VerifierRepository verifierRepository; + + public TrustedVerifierServiceImpl(VerifierRepository verifierRepository) { + this.verifierRepository = verifierRepository; + } + + @Override + public TrustedVerifierResponseDTO addTrustedVerifier(String walletId, TrustedVerifierRequest trustedVerifierRequest) { + // Check if already exists to avoid duplicates + if (verifierRepository.existsByWalletIdAndVerifierId(walletId, trustedVerifierRequest.getVerifierId())) { + throw new InvalidRequestException(DUPLICATE_VERIFIER.getErrorCode(), DUPLICATE_VERIFIER.getErrorMessage()); + } + + TrustedVerifier trustedVerifier = TrustedVerifier.builder() + .id(UUID.randomUUID().toString()) // Generate unique ID + .walletId(walletId) + .verifierId(trustedVerifierRequest.getVerifierId()) + .build(); + + TrustedVerifier row = verifierRepository.save(trustedVerifier); + + TrustedVerifierResponseDTO trustedVerifierResponseDTO = new TrustedVerifierResponseDTO(); + trustedVerifierResponseDTO.setId(row.getId()); + return trustedVerifierResponseDTO; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/V1VCDownloadHandler.java b/src/main/java/io/mosip/mimoto/service/impl/V1VCDownloadHandler.java new file mode 100644 index 000000000..adb71bd96 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/V1VCDownloadHandler.java @@ -0,0 +1,90 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.V1VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.V1VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.V1Credential; +import io.mosip.mimoto.exception.CredentialProcessingException; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.service.V1CredentialRequestService; +import io.mosip.mimoto.service.VCDownloadHandler; +import io.mosip.mimoto.util.RestApiClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static io.mosip.mimoto.exception.ErrorConstants.CREDENTIAL_DOWNLOAD_EXCEPTION; +import static io.mosip.mimoto.exception.ErrorConstants.SERVER_UNAVAILABLE; + +@Slf4j +@Component("v1") +public class V1VCDownloadHandler implements VCDownloadHandler { + + private static final String INVALID_NONCE = "invalid_nonce"; + private static final String DOWNLOAD_FAILURE_MESSAGE = "Unable to download credential from issuerId: %s, credentialConfigurationId: %s"; + private static final String EMPTY_CREDENTIAL_MESSAGE = "Credential response did not contain any credentials"; + private static final String REQUEST_BUILD_FAILURE_MESSAGE = "Unable to generate credential request"; + + private final V1CredentialRequestService v1CredentialRequestService; + private final RestApiClient restApiClient; + + public V1VCDownloadHandler(V1CredentialRequestService v1CredentialRequestService, RestApiClient restApiClient) { + this.v1CredentialRequestService = v1CredentialRequestService; + this.restApiClient = restApiClient; + } + + @Override + public VCCredentialResponse downloadCredential(IssuerDTO issuerDTO, String credentialConfigurationId, CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse, TokenResponseDTO tokenResponse, String walletId, String base64Key, boolean isLoginFlow) throws CredentialProcessingException, InvalidCredentialResourceException, ExternalServiceUnavailableException { + + V1VCCredentialRequest vcCredentialRequest = buildCredentialRequest(issuerDTO, credentialConfigurationId, credentialIssuerWellKnownResponse, walletId, base64Key, isLoginFlow); + + String credentialEndpoint = credentialIssuerWellKnownResponse.getCredentialEndPoint(); + String accessToken = tokenResponse.getAccess_token(); + String issuerId = issuerDTO.getIssuer_id(); + + V1VCCredentialResponse response = postCredentialRequest(credentialEndpoint, vcCredentialRequest, accessToken); + + String nonceEndpoint = credentialIssuerWellKnownResponse.getNonceEndpoint(); + if (response != null && response.hasError() && INVALID_NONCE.equals(response.getError()) + && nonceEndpoint != null && !nonceEndpoint.isBlank()) { + log.info("Received invalid_nonce error for issuerId: {}. Retrying with fresh nonce.", issuerId); + vcCredentialRequest = buildCredentialRequest(issuerDTO, credentialConfigurationId, credentialIssuerWellKnownResponse, walletId, base64Key, isLoginFlow); + response = postCredentialRequest(credentialEndpoint, vcCredentialRequest, accessToken); + } + + if (response == null || response.hasError()) { + String errorDetail = response != null ? response.getError() + ": " + response.getErrorDescription() : "no response"; + throw new ExternalServiceUnavailableException(SERVER_UNAVAILABLE.getErrorCode(), + String.format(DOWNLOAD_FAILURE_MESSAGE, issuerId, credentialConfigurationId) + " - " + errorDetail); + } + + List credentials = response.getCredentials(); + if (credentials == null || credentials.isEmpty()) { + throw new InvalidCredentialResourceException(EMPTY_CREDENTIAL_MESSAGE); + } + + String format = credentialIssuerWellKnownResponse.getCredentialConfigurationsSupported().get(credentialConfigurationId).getFormat(); + + log.debug("V1 VC Credential Response received for issuerId: {}", issuerId); + return new VCCredentialResponse(format, credentials.getFirst().getCredential()); + } + + private V1VCCredentialRequest buildCredentialRequest(IssuerDTO issuerDTO, String credentialConfigurationId, CredentialIssuerWellKnownResponse wellKnownResponse, String walletId, String base64Key, boolean isLoginFlow) throws CredentialProcessingException { + try { + return v1CredentialRequestService.buildRequest(issuerDTO, credentialConfigurationId, wellKnownResponse, walletId, base64Key, isLoginFlow); + } catch (Exception e) { + log.error("Failed to generate V1 VC credential request for issuerId: {}", issuerDTO.getIssuer_id(), e); + throw new CredentialProcessingException(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), REQUEST_BUILD_FAILURE_MESSAGE, e); + } + } + + private V1VCCredentialResponse postCredentialRequest(String credentialEndpoint, V1VCCredentialRequest request, String accessToken) { + return restApiClient.postApiWithErrorResponse(credentialEndpoint, MediaType.APPLICATION_JSON, request, V1VCCredentialResponse.class, accessToken); + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/VcSdJwtCredentialFormatHandler.java b/src/main/java/io/mosip/mimoto/service/impl/VcSdJwtCredentialFormatHandler.java new file mode 100644 index 000000000..e99ef0809 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/VcSdJwtCredentialFormatHandler.java @@ -0,0 +1,391 @@ +package io.mosip.mimoto.service.impl; + + +import com.authlete.sd.Disclosure; +import com.authlete.sd.SDJWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.service.CredentialFormatHandler; +import io.mosip.mimoto.util.LocaleUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.util.IssuerConfigUtil.camelToTitleCase; +import static io.mosip.mimoto.util.JwtUtils.parseJwtPayload; + +@Slf4j +@Component("vc+sd-jwt") +public class VcSdJwtCredentialFormatHandler implements CredentialFormatHandler { + + private static final String KEY = "..."; + private static final String SD = "_sd"; + + private final ObjectMapper objectMapper; + + public VcSdJwtCredentialFormatHandler(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String getSupportedFormat() { + return CredentialFormat.VC_SD_JWT.getFormat(); + } + + + @Override + public Map extractCredentialClaims(VCCredentialResponse vcCredentialResponse) { + Object credential = vcCredentialResponse.getCredential(); + if (credential instanceof String) { + return extractClaimsFromSdJwt((String) credential); + } + log.warn("Unexpected credential format in response for SD-JWT VC:"); + return Collections.emptyMap(); + } + + @Override + public LinkedHashMap> loadDisplayPropertiesFromWellknown( + Map credentialProperties, + CredentialsSupportedResponse credentialsSupportedResponse, + String userLocale) { + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + // Start with ordered fields + Set orderedKeys = Optional.ofNullable(credentialsSupportedResponse.getOrder()) + .map(LinkedHashSet::new) // preserve order + .orElse(new LinkedHashSet<>()); + + // Add remaining keys from credentialProperties that are not already in orderedKeys + for (String key : credentialProperties.keySet()) { + orderedKeys.add(key); // Set ensures no duplicates + } + + if (credentialsSupportedResponse.getClaims() == null || credentialsSupportedResponse.getClaims().isEmpty()) { + log.info("Issuer well-known has no claims for SD-JWT format; falling back to claim-based display properties"); + return buildFallbackDisplayProperties(credentialProperties, orderedKeys); + } + + // Extract raw claims and convert to DTOs + Map rawClaims = Optional.ofNullable(credentialsSupportedResponse.getClaims()) + .map(map -> (map.size() == 1 && map.values().iterator().next() instanceof Map) + ? (Map) map.values().iterator().next() + : map) + .orElse(Collections.emptyMap()); + + Map convertedClaimsMap = rawClaims.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> objectMapper.convertValue(entry.getValue(), CredentialDisplayResponseDto.class) + )); + + if (convertedClaimsMap.isEmpty()) { + log.info("No display configuration found for SD-JWT format"); + return buildFallbackDisplayProperties(credentialProperties, orderedKeys); + } + + String resolvedLocale = LocaleUtils.resolveLocaleWithFallback(convertedClaimsMap, userLocale); + LinkedHashMap localizedDisplayMap = new LinkedHashMap<>(); + + if (resolvedLocale != null) { + convertedClaimsMap.forEach((key, dto) -> { + dto.getDisplay().stream() + .filter(display -> LocaleUtils.matchesLocale(display.getLocale(), resolvedLocale)) + .findFirst() + .ifPresent(display -> localizedDisplayMap.put(key, display)); + }); + } + + for (String key : orderedKeys) { + Object value = credentialProperties.get(key); + if (value == null) { + continue; // Skip fields without a value + } + + CredentialIssuerDisplayResponse display = localizedDisplayMap.get(key); + + // Fallback if not found in metadata + if (display == null) { + display = new CredentialIssuerDisplayResponse(); + display.setName(camelToTitleCase(key)); + display.setLocale("en"); + } + + displayProperties.put(key, Map.of(display, value)); + } + + return displayProperties; + } + + @Override + public Map> extractAllCredentialProperties(VCCredentialResponse vcCredentialResponse) { + Object credential = vcCredentialResponse.getCredential(); + if (credential instanceof String) { + return extractAllPropertiesFromSdJwt((String) credential); + } + return Collections.emptyMap(); + } + + public Map extractClaimsFromSdJwt(String sdJwtString) { + try { + SDJWT sdJwt = SDJWT.parse(sdJwtString); + + // Extract public claims from the credential JWT + Map claims = extractPublicClaims(sdJwt); + + // Extract disclosures and merge with claims + Map sdClaims = extractSdClaims(sdJwt); + claims.putAll(sdClaims); + + // Remove standard JWT claims and SD-JWT metadata + List metadataKeys = Arrays.asList("vct", "cnf", "iss", "sub", "aud", "exp", "nbf", "iat", "jti", SD, "_sd_alg", "id"); + metadataKeys.forEach(claims::remove); + + // Return claims directly as result + return claims; + + } catch (IllegalArgumentException e) { + log.error("Error parsing SD-JWT with Authlete library: {}", e.getMessage(), e); + return Collections.emptyMap(); + } catch (Exception e) { + log.error("Unexpected error processing SD-JWT", e); + return Collections.emptyMap(); + } + } + + public Map> extractAllPropertiesFromSdJwt(String sdJwtString) { + try { + SDJWT sdJwt = SDJWT.parse(sdJwtString); + + Map> claims = new LinkedHashMap<>(); + + // Extract public claims from the credential JWT + claims.put("publicClaims", extractPublicClaims(sdJwt)); + + // Disclosure blobs for submit/UI; decoded values for DCQL/PE claim matching (see sdClaimValues). + claims.put("sdClaims", extractSdClaimsForOVP(sdJwt)); + claims.put("sdClaimValues", extractSdClaimValuesForMatching(sdJwt)); + + return claims; + + } catch (IllegalArgumentException e) { + log.error("Error parsing SD-JWT with Authlete library: {}", e.getMessage(), e); + return Collections.emptyMap(); + } catch (Exception e) { + log.error("Unexpected error processing SD-JWT", e); + return Collections.emptyMap(); + } + } + + private Map extractPublicClaims(SDJWT sdJwt) { + String credentialJwt = sdJwt.getCredentialJwt(); + Map publicClaims = new LinkedHashMap<>(); + if (credentialJwt != null) { + Map jwtPayload = parseJwtPayload(credentialJwt); + if (jwtPayload != null) { + publicClaims.putAll(jwtPayload); + } + } + return publicClaims; + } + + private Map extractSdClaims(SDJWT sdJwt) { + Map sdClaims = new LinkedHashMap<>(); + List disclosures = sdJwt.getDisclosures(); + if (disclosures != null && !disclosures.isEmpty()) { + for (Disclosure disclosure : disclosures) { + try { + String claimName = disclosure.getClaimName(); + Object claimValue = disclosure.getClaimValue(); + + if (claimName != null && claimValue != null) { + sdClaims.put(claimName, claimValue); + } + } catch (Exception e) { + log.warn("Failed to process disclosure: {}", e.getMessage()); + } + } + } + return sdClaims; + } + + private Map extractSdClaimsForOVP(SDJWT sdJwt) { + Map digestToDisclosure = new HashMap<>(); + Map digestToDisclosureB64 = new HashMap<>(); + List disclosures = sdJwt.getDisclosures(); + if (disclosures != null) { + for (Disclosure disclosure : disclosures) { + String digest = disclosure.digest(); + digestToDisclosure.put(digest, disclosure); + digestToDisclosureB64.put(digest, disclosure.getDisclosure()); + } + } + + String credentialJwt = sdJwt.getCredentialJwt(); + Map payload = parseJwtPayload(credentialJwt); + if (payload == null || payload.isEmpty()) { + return Collections.emptyMap(); + } + + Map pathToDisclosures = new LinkedHashMap<>(); + resolveDisclosures(payload, "", Collections.emptyList(), digestToDisclosure, digestToDisclosureB64, pathToDisclosures); + return pathToDisclosures; + } + + /** + * Path → decoded disclosure values for DCQL/PE matching ({@code CredentialMatchingServiceImpl#getCredentialData()}). + * Unlike {@link #extractSdClaimsForOVP}, this returns claim values (e.g. {@code true}, {@code "John"}), not base64 blobs. + */ + private Map extractSdClaimValuesForMatching(SDJWT sdJwt) { + Map digestToDisclosure = new HashMap<>(); + List disclosures = sdJwt.getDisclosures(); + if (disclosures != null) { + for (Disclosure disclosure : disclosures) { + digestToDisclosure.put(disclosure.digest(), disclosure); + } + } + + String credentialJwt = sdJwt.getCredentialJwt(); + Map payload = parseJwtPayload(credentialJwt); + if (payload == null || payload.isEmpty()) { + return Collections.emptyMap(); + } + + Map pathToValues = new LinkedHashMap<>(); + resolveDecodedClaimValues(payload, "", digestToDisclosure, pathToValues); + return pathToValues; + } + + /** Walks the SD-JWT payload tree and records decoded claim values at each JSON path (mirrors {@link #resolveDisclosures}). */ + private void resolveDecodedClaimValues(Object value, String path, Map digestToDisclosure, + Map pathToValues) { + if (value instanceof List list) { + for (int i = 0; i < list.size(); i++) { + Object item = list.get(i); + String currentPath = path + "[" + i + "]"; + if (item instanceof Map mapItem) { + if (mapItem.size() == 1 && mapItem.containsKey(KEY)) { + Disclosure disclosure = digestToDisclosure.get(mapItem.get(KEY)); + if (disclosure != null) { + pathToValues.put(currentPath, disclosure.getClaimValue()); + resolveDecodedClaimValues(disclosure.getClaimValue(), currentPath, digestToDisclosure, pathToValues); + } + continue; + } + } + resolveDecodedClaimValues(item, currentPath, digestToDisclosure, pathToValues); + } + return; + } + + if (value instanceof Map map) { + Object sdDigestsObj = map.get(SD); + if (sdDigestsObj instanceof List sdDigests) { + for (Object digestObj : sdDigests) { + String digest = (String) digestObj; + Disclosure disclosure = digestToDisclosure.get(digest); + if (disclosure == null || disclosure.getClaimName() == null) { + continue; + } + String claimName = disclosure.getClaimName(); + if (SD.equals(claimName) || KEY.equals(claimName)) { + continue; + } + + String fullPath = path.isEmpty() ? claimName : path + "." + claimName; + pathToValues.put(fullPath, disclosure.getClaimValue()); + resolveDecodedClaimValues(disclosure.getClaimValue(), fullPath, digestToDisclosure, pathToValues); + } + } + + for (Map.Entry entry : map.entrySet()) { + if (SD.equals(entry.getKey())) { + continue; + } + String fullPath = path.isEmpty() ? entry.getKey().toString() : path + "." + entry.getKey(); + resolveDecodedClaimValues(entry.getValue(), fullPath, digestToDisclosure, pathToValues); + } + } + } + + private void resolveDisclosures(Object value, String path, List parentDisclosures, Map digestToDisclosure, Map digestToDisclosureB64, Map pathToDisclosures) { + if (value instanceof List) { + List list = (List) value; + for (int i = 0; i < list.size(); i++) { + Object item = list.get(i); + String currentPath = path + "[" + i + "]"; + if (item instanceof Map) { + Map mapItem = (Map) item; + if (mapItem.size() == 1 && mapItem.containsKey(KEY)) { + String digest = (String) mapItem.get(KEY); + Disclosure disclosure = digestToDisclosure.get(digest); + if (disclosure != null) { + List currentDisclosures = new ArrayList<>(parentDisclosures); + currentDisclosures.add(digestToDisclosureB64.get(digest)); + pathToDisclosures.put(currentPath, currentDisclosures); + resolveDisclosures(disclosure.getClaimValue(), currentPath, currentDisclosures, digestToDisclosure, digestToDisclosureB64, pathToDisclosures); + } + continue; + } + } + resolveDisclosures(item, currentPath, parentDisclosures, digestToDisclosure, digestToDisclosureB64, pathToDisclosures); + } + return; + } + + if (value instanceof Map) { + Map map = (Map) value; + + Object sdDigestsObj = map.get(SD); + if (sdDigestsObj instanceof List) { + for (Object digestObj : (List) sdDigestsObj) { + String digest = (String) digestObj; + Disclosure disclosure = digestToDisclosure.get(digest); + if (disclosure == null || disclosure.getClaimName() == null) continue; + String claimName = disclosure.getClaimName(); + if (SD.equals(claimName) || KEY.equals(claimName)) continue; + + String fullPath = path.isEmpty() ? claimName : path + "." + claimName; + List currentDisclosures = new ArrayList<>(parentDisclosures); + currentDisclosures.add(digestToDisclosureB64.get(digest)); + pathToDisclosures.put(fullPath, currentDisclosures); + resolveDisclosures(disclosure.getClaimValue(), fullPath, currentDisclosures, digestToDisclosure, digestToDisclosureB64, pathToDisclosures); + } + } + + for (Map.Entry entry : map.entrySet()) { + if (SD.equals(entry.getKey())) continue; + String fullPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey(); + resolveDisclosures(entry.getValue(), fullPath, parentDisclosures, digestToDisclosure, digestToDisclosureB64, pathToDisclosures); + } + } + } + + private LinkedHashMap> buildFallbackDisplayProperties(Map credentialProperties, Set orderedKeys) { + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + // Use ordered keys from parameter (already includes all fields) + List fieldKeys = new ArrayList<>(orderedKeys); + + fieldKeys.remove("id"); + + // Build default display entries from claims + for (String key : fieldKeys) { + Object value = credentialProperties.get(key); + if (value == null) { + continue; + } + + // Generate fallback display using claims keys + CredentialIssuerDisplayResponse display = new CredentialIssuerDisplayResponse(); + display.setName(camelToTitleCase(key)); + display.setLocale("en"); + + displayProperties.put(key, Map.of(display, value)); + } + return displayProperties; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/VerifierServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/VerifierServiceImpl.java new file mode 100644 index 000000000..9ed8b04c4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/VerifierServiceImpl.java @@ -0,0 +1,144 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidVerifierException; +import io.mosip.mimoto.repository.VerifierRepository; +import io.mosip.mimoto.service.VerifierService; +import io.mosip.mimoto.constant.OpenID4VPConstants; +import io.mosip.mimoto.util.UrlParameterUtils; +import io.mosip.mimoto.util.Utilities; +import io.mosip.openID4VP.authorizationRequest.Verifier; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.validator.routines.UrlValidator; +import org.springframework.stereotype.Service; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.cache.annotation.Cacheable; + +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.apache.commons.validator.routines.UrlValidator.ALLOW_ALL_SCHEMES; +import static org.apache.commons.validator.routines.UrlValidator.ALLOW_LOCAL_URLS; + +@Slf4j +@Service +public class VerifierServiceImpl implements VerifierService { + + private final Utilities utilities; + + private final ObjectMapper objectMapper; + + private final VerifierRepository verifierRepository; + + private static final PathMatcher pathMatcher; + private static final UrlValidator urlValidator; + + static { + pathMatcher = new AntPathMatcher(); + urlValidator = new UrlValidator(ALLOW_ALL_SCHEMES+ALLOW_LOCAL_URLS); + } + + public VerifierServiceImpl(Utilities utilities, ObjectMapper objectMapper, VerifierRepository verifierRepository) { + this.utilities = utilities; + this.objectMapper = objectMapper; + this.verifierRepository = verifierRepository; + } + + @Cacheable(value = "preRegisteredTrustedVerifiersCache", key = "'preRegisteredTrustedVerifiers'") + public VerifiersDTO getTrustedVerifiers() throws ApiNotAccessibleException, JsonProcessingException { + String trustedVerifiersJsonValue = utilities.getTrustedVerifiersJsonValue(); + if (trustedVerifiersJsonValue == null) { + throw new ApiNotAccessibleException(); + } + return objectMapper.readValue(trustedVerifiersJsonValue, VerifiersDTO.class); + } + + public Optional getVerifierByClientId(String clientId) throws ApiNotAccessibleException, JsonProcessingException { + VerifiersDTO verifiersDTO = getTrustedVerifiers(); + return verifiersDTO.getVerifiers().stream().filter(verifier -> verifier.getClientId().equals(clientId)).findFirst(); + } + + @Override + public void validateVerifier(String clientId, String responseUri, String redirectUri) throws ApiNotAccessibleException, JsonProcessingException { + log.info("Started the presentation Validation"); + if (responseUri != null && !responseUri.trim().isEmpty()) { + getVerifierByClientId(clientId).ifPresentOrElse( + (verifierDTO) -> { + List registeredResponseUris = Optional.ofNullable(verifierDTO.getResponseUris()) + .orElseGet(Collections::emptyList); + boolean isValidVerifier = registeredResponseUris.stream().anyMatch(registeredResponseUri -> + urlValidator.isValid(registeredResponseUri) && + urlValidator.isValid(responseUri) && + pathMatcher.match(registeredResponseUri, responseUri)); + if (!isValidVerifier) { + throw new InvalidVerifierException( + ErrorConstants.INVALID_RESPONSE_URI.getErrorCode(), + ErrorConstants.INVALID_RESPONSE_URI.getErrorMessage()); + } + }, + () -> { + throw new InvalidVerifierException( + ErrorConstants.INVALID_CLIENT.getErrorCode(), + ErrorConstants.INVALID_CLIENT.getErrorMessage()); + } + ); + } else { + getVerifierByClientId(clientId).ifPresentOrElse( + (verifierDTO) -> { + List registeredRedirectUris = Optional.ofNullable(verifierDTO.getRedirectUris()) + .orElseGet(Collections::emptyList); + boolean isValidVerifier = registeredRedirectUris.stream().anyMatch(registeredRedirectUri -> + urlValidator.isValid(registeredRedirectUri) && + urlValidator.isValid(redirectUri) && + pathMatcher.match(registeredRedirectUri, redirectUri)); + if (!isValidVerifier) { + throw new InvalidVerifierException( + ErrorConstants.INVALID_REDIRECT_URI.getErrorCode(), + ErrorConstants.INVALID_REDIRECT_URI.getErrorMessage()); + } + }, + () -> { + throw new InvalidVerifierException( + ErrorConstants.INVALID_CLIENT.getErrorCode(), + ErrorConstants.INVALID_CLIENT.getErrorMessage()); + } + ); + } + } + + @Override + public boolean isVerifierTrustedByWallet(String verifierId, String walletId) { + return verifierRepository.existsByWalletIdAndVerifierId(walletId, verifierId); + } + + @Override + public boolean isVerifierClientPreregistered(List preRegisteredVerifiers, String urlEncodedVPAuthorizationRequest) throws URISyntaxException { + if (urlEncodedVPAuthorizationRequest == null || urlEncodedVPAuthorizationRequest.trim().isEmpty()) { + log.warn("URL encoded VP authorization request is null or empty"); + return false; + } + + String clientId = UrlParameterUtils.extractQueryParameter(urlEncodedVPAuthorizationRequest, OpenID4VPConstants.CLIENT_ID_PARAM); + String responseUriValue = UrlParameterUtils.extractQueryParameter(urlEncodedVPAuthorizationRequest, OpenID4VPConstants.RESPONSE_URI_PARAM); + List responseUris = responseUriValue != null ? List.of(responseUriValue) : List.of(); + + if (clientId == null || clientId.trim().isEmpty()) { + log.warn("No client_id found in the authorization request URL"); + return false; + } + if (responseUris.isEmpty()) { + log.warn("No response_uri found in the authorization request URL"); + return false; + } + return preRegisteredVerifiers.stream().anyMatch(verifier -> clientId.equals(verifier.getClientId()) && verifier.getResponseUris().containsAll(responseUris)); + } + +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/WalletCredentialServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/WalletCredentialServiceImpl.java new file mode 100644 index 000000000..68e50c3df --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/WalletCredentialServiceImpl.java @@ -0,0 +1,265 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.IssuerConfig; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.dto.resident.WalletCredentialResponseDTO; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.model.QRCodeType; +import io.mosip.mimoto.model.VerifiableCredential; +import io.mosip.mimoto.repository.WalletCredentialsRepository; +import io.mosip.mimoto.service.CredentialPDFGeneratorService; +import io.mosip.mimoto.service.CredentialService; +import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.service.WalletCredentialService; +import io.mosip.mimoto.service.DataProtectionService; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.InputStreamResource; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.IllegalArgumentException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +/** + * Implementation of {@link WalletCredentialService} for managing wallet credentials. + */ +@Slf4j +@Service +public class WalletCredentialServiceImpl implements WalletCredentialService { + + @Value("${mosip.inji.wallet.issuersWithSingleVcLimit:Mosip}") + private String issuersWithSingleVcLimit; + + @Value("${mosip.inji.wallet.vc.validity.count:-1}") + private String credentialValidity; + + private final WalletCredentialsRepository repository; + private final IssuersService issuersService; + private final CredentialService credentialService; + private final ObjectMapper objectMapper; + private final DataProtectionService dataProtectionService; + private final CredentialPDFGeneratorService credentialPDFGeneratorService; + private final DataShareServiceImpl dataShareService; + + @Autowired + public WalletCredentialServiceImpl(WalletCredentialsRepository repository, + IssuersService issuersService, + CredentialService credentialService, + ObjectMapper objectMapper, + DataProtectionService dataProtectionService,CredentialPDFGeneratorService credentialPDFGeneratorService,DataShareServiceImpl dataShareService) { + this.repository = repository; + this.issuersService = issuersService; + this.credentialService = credentialService; + this.objectMapper = objectMapper; + this.dataProtectionService = dataProtectionService; + this.credentialPDFGeneratorService = credentialPDFGeneratorService; + this.dataShareService = dataShareService; + } + + @Override + public VerifiableCredentialResponseDTO downloadVCAndStoreInDB(String issuerId, String credentialConfigurationId, + TokenResponseDTO tokenResponse, + String locale, String walletId, String base64Key) + throws CredentialProcessingException, ExternalServiceUnavailableException { + log.info("Fetching and storing credential for wallet: {}, issuer: {}, type: {}", walletId, issuerId, credentialConfigurationId); + + Set issuers = Arrays.stream(issuersWithSingleVcLimit.split(",")) + .map(String::trim) + .collect(Collectors.toSet()); + if (issuers.contains(issuerId) && repository.existsByIssuerIdAndCredentialTypeAndWalletId(issuerId, credentialConfigurationId, walletId)) { + log.warn("Duplicate credential found for issuer: {}, type: {}, wallet: {}", issuerId, credentialConfigurationId, walletId); + throw new InvalidRequestException(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), "Duplicate credential for issuer and type"); + } + + + VerifiableCredentialResponseDTO credential; + + credential = credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale); + + log.debug("Credential stored successfully: {}", credential.getCredentialId()); + return credential; + + } + + @Override + public List fetchAllCredentialsForWallet(String walletId, String base64Key, String locale) { + log.info("Fetching all credentials for wallet: {}", walletId); + + List credentials = repository.findByWalletIdOrderByCreatedAtDesc(walletId); + + return credentials.stream().map(credential -> { + String issuerId = credential.getCredentialMetadata().getIssuerId(); + IssuerConfig issuerConfig = null; + try { + issuerConfig = issuersService.getIssuerConfig(issuerId, credential.getCredentialMetadata().getCredentialType()); + } catch (InvalidIssuerIdException | ApiNotAccessibleException e) { + log.error("Failed to fetch issuer details for issuerId: {}", issuerId, e); + } + return VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, locale, credential.getId()); + }).toList(); + + } + + @Override + public WalletCredentialResponseDTO fetchVerifiableCredential(String walletId, String credentialId, + String base64Key, String locale) + throws CredentialNotFoundException, CredentialProcessingException { + log.info("Fetching credential: {} for wallet: {}", credentialId, walletId); + VerifiableCredential credential = repository.findByIdAndWalletId(credentialId, walletId) + .orElseThrow(getCredentialNotFoundExceptionSupplier(walletId, credentialId)); + + try { + String decryptedCredential = dataProtectionService.decryptCredential(credential.getCredential(), base64Key); + + WalletCredentialResponseDTO response = generateCredentialResponse(decryptedCredential, credential.getCredentialMetadata(), locale); + log.debug("Credential fetched successfully: {}", credentialId); + return response; + } catch (DecryptionException e) { + log.error("Decryption failed for credential: {}", credentialId, e); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Decryption failed", e); + } + } + + @Override + public void deleteCredential(String credentialId, String walletId) throws CredentialNotFoundException { + log.info("Deleting credential with ID: {} for wallet: {}", credentialId, walletId); + + repository.findByIdAndWalletId(credentialId, walletId) + .orElseThrow(getCredentialNotFoundExceptionSupplier(walletId, credentialId)); + // Delete the credential + repository.deleteById(credentialId); + log.info("Successfully deleted credential with ID: {}", credentialId); + } + + @NotNull + private static Supplier getCredentialNotFoundExceptionSupplier(String walletId, String credentialId) { + return () -> { + log.warn("Credential not found: {} for wallet: {}", credentialId, walletId); + return new CredentialNotFoundException(RESOURCE_NOT_FOUND.getErrorCode(), RESOURCE_NOT_FOUND.getErrorMessage()); + }; + } + + private WalletCredentialResponseDTO generateCredentialResponse(String decryptedCredential, CredentialMetadata credentialMetadata, String locale) throws CredentialProcessingException { + log.info("Generating credential response for issuerId: {}, credentialType: {}", credentialMetadata.getIssuerId(), credentialMetadata.getCredentialType()); + try { + // Parse decrypted credential + VCCredentialResponse vcCredentialResponse = objectMapper.readValue(decryptedCredential, VCCredentialResponse.class); + + // Fetch issuer details + IssuerDTO issuerDTO = issuersService.getIssuerDetails(credentialMetadata.getIssuerId()); + + // Fetch issuer configuration + IssuerConfig issuerConfig = issuersService.getIssuerConfig(credentialMetadata.getIssuerId(), credentialMetadata.getCredentialType()); + + if (null == issuerConfig) { + log.error("Credentials supported response not found in wellknown for credentialType: {}", credentialMetadata.getCredentialType()); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Invalid credential type configuration"); + } + + // Find credentials supported response for the credential type + CredentialsSupportedResponse credentialsSupportedResponse = issuerConfig.getCredentialsSupportedResponse(); + if (credentialsSupportedResponse == null ) { + log.error("Credentials supported response not found for credentialType: {}", credentialMetadata.getCredentialType()); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Invalid credential type configuration"); + } + + String dataShareUrl = QRCodeType.OnlineSharing.equals(issuerDTO.getQr_code_type()) ? dataShareService.storeDataInDataShare(objectMapper.writeValueAsString(vcCredentialResponse), credentialValidity) : ""; + + + // Generate PDF + // keep the datashare url and credential validity as defaults in downloading VC as PDF as logged-in user + // This is because generatePdfForVerifiableCredentials will be used by both logged-in and non-logged-in users + ByteArrayInputStream pdfStream = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + credentialMetadata.getCredentialType(), + vcCredentialResponse, + issuerDTO, + credentialsSupportedResponse, + dataShareUrl, + credentialValidity, + locale + ); + + // Construct response + String fileName = String.format("%s_credential.pdf", credentialMetadata.getCredentialType()); + return WalletCredentialResponseDTO.builder() + .fileName(fileName) + .fileContentStream(new InputStreamResource(pdfStream)) + .build(); + } catch (JsonProcessingException e) { + log.error("Failed to parse decrypted credential for issuerId: {}, credentialType: {}", credentialMetadata.getIssuerId(), credentialMetadata.getCredentialType(), e); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Failed to parse decrypted credential"); + } catch (ApiNotAccessibleException | IOException | AuthorizationServerWellknownResponseException | + InvalidWellknownResponseException | InvalidIssuerIdException e) { + log.error("Failed to fetch issuer details or configuration for issuerId: {}", credentialMetadata.getIssuerId(), e); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Failed to fetch issuer configuration"); + } catch (Exception e) { + log.error("Failed to generate PDF for credentialType: {}", credentialMetadata.getCredentialType(), e); + throw new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Failed to generate credential PDF"); + } + } + + @Override + public List getDecryptedCredentials(String walletId, String base64Key) { + List walletCredentials = repository.findByWalletIdOrderByCreatedAtDesc(walletId); + List decryptedCredentials = walletCredentials.stream().map(credential -> { + try { + VCCredentialResponse decryptedCredential = decryptAndParseCredential(credential, base64Key); + return Optional.of(DecryptedCredentialDTO.builder() + .id(credential.getId()) + .walletId(credential.getWalletId()) + .credential(decryptedCredential) + .credentialMetadata(credential.getCredentialMetadata()) + .createdAt(credential.getCreatedAt()) + .updatedAt(credential.getUpdatedAt()) + .build()); + } catch (IOException | IllegalArgumentException | DecryptionException e) { + log.warn("Failed to decrypt credential {}: {}", credential.getId(), e.getMessage()); + return Optional.empty(); + } + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + log.info("Successfully decrypted {} out of {} credentials", decryptedCredentials.size(), walletCredentials.size()); + return decryptedCredentials; + } + + private VCCredentialResponse decryptAndParseCredential(VerifiableCredential credential, String base64Key) throws IOException, IllegalArgumentException, DecryptionException { + if (credential == null) { + throw new IllegalArgumentException("Credential cannot be null"); + } + + if (credential.getCredential() == null) { + throw new IllegalArgumentException("Credential data cannot be null"); + } + + String decryptedCredential = dataProtectionService.decryptCredential(credential.getCredential(), base64Key); + if (decryptedCredential == null || decryptedCredential.trim().isEmpty()) { + throw new IllegalArgumentException("Failed to decrypt credential or decrypted data is empty"); + } + + return objectMapper.readValue(decryptedCredential, VCCredentialResponse.class); + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/service/impl/WalletPresentationServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/WalletPresentationServiceImpl.java new file mode 100644 index 000000000..e3cc5c558 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/WalletPresentationServiceImpl.java @@ -0,0 +1,935 @@ +package io.mosip.mimoto.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.util.Base64URL; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.constant.OpenID4VPConstants; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.VerifiablePresentation; +import io.mosip.mimoto.repository.VerifiablePresentationsRepository; +import io.mosip.mimoto.service.*; +import io.mosip.mimoto.util.AuthorizationRequestHelper; +import io.mosip.mimoto.util.SigningKeyUtil; +import io.mosip.mimoto.util.Utilities; +import io.mosip.mimoto.util.UrlParameterUtils; +import io.mosip.mimoto.util.DcqlClaimSetHelper; +import io.mosip.mimoto.util.DcqlMatchingHelper; +import io.mosip.mimoto.util.DcqlCredentialSetHelper; +import io.mosip.mimoto.util.SelectedSdClaimsUtil; +import io.mosip.openID4VP.OpenID4VP; +import io.mosip.openID4VP.authorizationRequest.AuthorizationDcqlRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.authorizationRequest.Verifier; +import io.mosip.openID4VP.authorizationRequest.clientMetadata.ClientMetadata; +import io.mosip.openID4VP.authorizationRequest.clientMetadata.ClientMetadataDraft23; +import io.mosip.openID4VP.authorizationResponse.unsignedVPToken.UnsignedVPToken; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.VPTokenSigningResult; +import io.mosip.openID4VP.constants.FormatType; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.CredentialSetQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import io.mosip.openID4VP.verifier.VerifierResponse; +import io.mosip.openID4VP.wallet.Credential; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +/** + * Service implementation for handling wallet presentation operations. + */ +@Slf4j +@Service +public class WalletPresentationServiceImpl implements WalletPresentationService { + + private static final String UNKNOWN_VERIFIER = "unknown"; + private static final String EMPTY_JSON = "{}"; + + private final VerifierService verifierService; + private final OpenID4VPService openID4VPService; + private final ObjectMapper objectMapper; + private final KeyPairRetrievalService keyPairService; + private final CredentialMatchingService credentialMatchingService; + private final VerifiablePresentationsRepository verifiablePresentationsRepository; + private final CredentialFormatHandlerFactory credentialFormatHandlerFactory; + private final WalletCredentialService walletCredentialService; + + public WalletPresentationServiceImpl( + VerifierService verifierService, + OpenID4VPService openID4VPService, + ObjectMapper objectMapper, + KeyPairRetrievalService keyPairService, + CredentialMatchingService credentialMatchingService, + VerifiablePresentationsRepository verifiablePresentationsRepository, + CredentialFormatHandlerFactory credentialFormatHandlerFactory, + WalletCredentialService walletCredentialService) { + this.verifierService = verifierService; + this.openID4VPService = openID4VPService; + this.objectMapper = objectMapper; + this.keyPairService = keyPairService; + this.credentialMatchingService = credentialMatchingService; + this.verifiablePresentationsRepository = verifiablePresentationsRepository; + this.credentialFormatHandlerFactory = credentialFormatHandlerFactory; + this.walletCredentialService = walletCredentialService; + } + + @Override + public VPResponseDTO handleVPAuthorizationRequest(String urlEncodedVPAuthorizationRequest, String walletId) + throws ApiNotAccessibleException, IOException, URISyntaxException { + + String presentationId = UUID.randomUUID().toString(); + // Initialize OpenID4VP instance with presentationId as traceability id for each new Verifiable Presentation request + List preRegisteredVerifiers = openID4VPService.getPreRegisteredVerifiers(); + boolean shouldValidateClient = verifierService.isVerifierClientPreregistered( + preRegisteredVerifiers, urlEncodedVPAuthorizationRequest); + + OpenID4VP openID4VP = openID4VPService.create(presentationId, preRegisteredVerifiers, shouldValidateClient); + AuthorizationRequest authorizationRequest = openID4VP.authenticateVerifier(urlEncodedVPAuthorizationRequest); + boolean dcql = AuthorizationRequestHelper.hasDcqlQuery(authorizationRequest); + + VerifiablePresentationVerifierDTO verifierDTO = + createVPResponseVerifierDTO(preRegisteredVerifiers, authorizationRequest, walletId); + + return new VPResponseDTO(presentationId, verifierDTO, dcql); + } + + @Override + public MatchingCredentialsDTO getMatchingCredentials( + VerifiablePresentationSessionData sessionData, String walletId, String base64Key) + throws ApiNotAccessibleException, IOException { + return credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + @Override + public ResponseEntity handlePresentationAction( + String walletId, String presentationId, SubmitPresentationRequestDTO request, + VerifiablePresentationSessionData vpSessionData, String base64Key) { + + try { + // Determine the action based on request content + if (request.isSubmissionRequest()) { + if (base64Key == null || base64Key.isBlank()) { + return Utilities.getErrorResponseEntityWithoutWrapper( + new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Wallet key is required for credential presentation"), + INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + SubmitPresentationResponseDTO response = + submitPresentation(vpSessionData, walletId, presentationId, request, base64Key); + return ResponseEntity.ok(response); + } + if (request.isRejectionRequest()) { + return handleVerifierRejection(walletId, vpSessionData, request); + } + return Utilities.getErrorResponseEntityWithoutWrapper( + new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Request must contain selectedCredentials / dcqlSelections or both errorCode and errorMessage"), + INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + + } catch (JOSEException ex) { + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, JWT_SIGNING_ERROR.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (KeyGenerationException | DecryptionException ex) { + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, KEY_GENERATION_ERROR.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (ApiNotAccessibleException | IOException ex) { + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, WALLET_CREATE_VP_EXCEPTION.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (VPErrorNotSentException ex) { + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, REJECT_VERIFIER_EXCEPTION.getErrorCode(), HttpStatus.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON); + } catch (IllegalStateException | java.lang.IllegalArgumentException ex) { + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, INVALID_REQUEST.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } catch (OpenID4VPExceptions ex) { + log.error("OpenID4VP error during presentation submission for presentationId={}", presentationId, ex); + if (ex.getCause() != null) { + log.error("OpenID4VP root cause for presentationId={}: {}", presentationId, + ex.getCause().getMessage(), ex.getCause()); + } + return Utilities.getErrorResponseEntityWithoutWrapper( + ex, ex.getErrorCode(), HttpStatus.BAD_REQUEST, MediaType.APPLICATION_JSON); + } + } + + /** + * Submits a presentation with selected credentials (Draft-23 or DCQL). + */ + public SubmitPresentationResponseDTO submitPresentation( + VerifiablePresentationSessionData sessionData, String walletId, String presentationId, + SubmitPresentationRequestDTO request, String base64Key) + throws ApiNotAccessibleException, IOException, JOSEException, + KeyGenerationException, DecryptionException, OpenID4VPExceptions { + + validateSubmissionRequest(request); + LocalDateTime requestedAt = LocalDateTime.now(); + + // Step 1: Create OpenID4VP instance and authenticate the verifier from session data + List preRegisteredVerifiers = openID4VPService.getPreRegisteredVerifiers(); + OpenID4VP openID4VP = openID4VPService.create( + presentationId, preRegisteredVerifiers, sessionData.isVerifierClientPreregistered()); + openID4VP.authenticateVerifier(sessionData.getAuthorizationRequest()); + + // Step 2: Load wallet credentials and resolve effective SD-JWT claim paths for submission + Map walletCredentialsById = walletCredentialService + .getDecryptedCredentials(walletId, base64Key).stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, dto -> dto, (a, b) -> a)); + + if (request.isDcqlSubmission()) { + validateDcqlSelections(request, sessionData); + } + + Map> effectiveSelectedSdClaims = + resolveSubmissionSdClaims(request, sessionData, walletCredentialsById); + + // Step 3: Build credential map for inji-openid4vp (descriptor-based for Draft-23, query-based for DCQL) + Map> credentialMap; + if (request.isDcqlSubmission()) { + credentialMap = buildQueryCredentialMap( + request.getDcqlSelections(), sessionData, effectiveSelectedSdClaims, walletCredentialsById); + } else { + List selected = fetchSelectedCredentials( + sessionData, request.getSelectedCredentialIds()); + credentialMap = buildDescriptorCredentialMap(selected, effectiveSelectedSdClaims, walletCredentialsById); + } + + // Step 4: Construct unsigned VP tokens and sign them + List unsignedVPTokens = openID4VP.constructUnsignedVPToken(credentialMap); + List signingResults = signVPTokens(unsignedVPTokens, walletId, base64Key); + + // Step 5: Share verifiable presentation with verifier using OpenID4VP JAR + try { + VerifierResponse response = openID4VP.sendVPResponseToVerifier(signingResults); + boolean success = response.getStatusCode() >= 200 && response.getStatusCode() < 300; + // Step 6: Store presentation record in database + storePresentationRecord(walletId, presentationId, request, sessionData, success, requestedAt); + return buildSubmitResponse( + success, + response.getRedirectUri(), + success ? OpenID4VPConstants.MESSAGE_PRESENTATION_SUCCESS + : OpenID4VPConstants.MESSAGE_PRESENTATION_SHARE_FAILED); + } catch (Exception ex) { + log.error("Failed to send VP to verifier for presentationId={}", presentationId, ex); + storePresentationRecord(walletId, presentationId, request, sessionData, false, requestedAt); + return buildSubmitResponse( + false, null, OpenID4VPConstants.MESSAGE_PRESENTATION_SHARE_FAILED); + } + } + + private SubmitPresentationResponseDTO buildSubmitResponse(boolean success, String redirectUri, String message) { + return SubmitPresentationResponseDTO.builder() + .redirectUri(redirectUri) + .status(success ? OpenID4VPConstants.STATUS_SUCCESS : OpenID4VPConstants.STATUS_ERROR) + .message(message) + .build(); + } + + /** + * Builds Draft-23 credential map keyed by input descriptor id. + */ + private Map> buildDescriptorCredentialMap( + List selected, + Map> selectedSdClaims, + Map walletCredentialsById) { + + Map> result = new LinkedHashMap<>(); + for (DecryptedCredentialDTO dto : selected) { + DecryptedCredentialDTO credentialForSubmission = resolveCredentialForSubmission(dto, walletCredentialsById); + String key = (credentialForSubmission.getIdentifier() != null && !credentialForSubmission.getIdentifier().isBlank()) + ? credentialForSubmission.getIdentifier() : credentialForSubmission.getId(); + result.computeIfAbsent(key, k -> new ArrayList<>()) + .add(toLibraryCredential(credentialForSubmission, selectedSdClaims)); + } + return result; + } + + /** + * Builds DCQL credential map keyed by query id. + */ + private Map> buildQueryCredentialMap( + List selections, + VerifiablePresentationSessionData sessionData, + Map> effectiveSdClaims, + Map walletCredentialsById) { + + Map cache = Optional.ofNullable(sessionData.getMatchingCredentials()) + .orElse(List.of()).stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, d -> d)); + + Map> result = new LinkedHashMap<>(); + for (DcqlCredentialSelection selection : selections) { + List credentials = new ArrayList<>(); + for (String id : selection.getSelectedCredentialIds()) { + DecryptedCredentialDTO sessionDto = cache.get(id); + if (sessionDto == null) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Selected credential not found in session: " + id); + } + if (sessionDto.getIdentifier() == null || sessionDto.getIdentifier().isBlank()) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Credential " + id + " has no query mapping — call GET /credentials before submit"); + } + DecryptedCredentialDTO credentialForSubmission = + resolveCredentialForSubmission(sessionDto, walletCredentialsById); + credentials.add(toLibraryCredential(credentialForSubmission, effectiveSdClaims)); + } + String mapKey = resolveDcqlMapKey(selection, cache); + result.merge(mapKey, credentials, (existing, incoming) -> { + List merged = new ArrayList<>(existing); + merged.addAll(incoming); + return merged; + }); + } + return result; + } + + /** + * Resolves the DCQL query id used as the inji-openid4vp map key. The client-supplied + * {@code queryId} is authoritative when present, since one wallet credential may match + * multiple DCQL queries and the session stores only the first match's identifier. + */ + private String resolveDcqlMapKey(DcqlCredentialSelection selection, Map cache) { + if (selection.getQueryId() != null && !selection.getQueryId().isBlank()) { + return selection.getQueryId(); + } + List selectedIds = selection.getSelectedCredentialIds(); + if (selectedIds == null || selectedIds.isEmpty()) { + return selection.getQueryId(); + } + String sessionQueryId = null; + for (String id : selectedIds) { + DecryptedCredentialDTO dto = cache.get(id); + if (dto == null || dto.getIdentifier() == null || dto.getIdentifier().isBlank()) { + continue; + } + if (sessionQueryId == null) { + sessionQueryId = dto.getIdentifier(); + } else if (!sessionQueryId.equals(dto.getIdentifier())) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Selected credentials belong to different DCQL queries: " + + sessionQueryId + " vs " + dto.getIdentifier()); + } + } + return sessionQueryId; + } + + /** + * Merges session credential metadata with the latest decrypted credential payload from the wallet. + */ + private DecryptedCredentialDTO resolveCredentialForSubmission( + DecryptedCredentialDTO sessionDto, Map walletCredentialsById) { + DecryptedCredentialDTO walletDto = walletCredentialsById.get(sessionDto.getId()); + if (walletDto == null) { + return sessionDto; + } + return DecryptedCredentialDTO.builder() + .id(sessionDto.getId()) + .walletId(walletDto.getWalletId()) + .credential(walletDto.getCredential()) + .credentialMetadata(walletDto.getCredentialMetadata()) + .createdAt(walletDto.getCreatedAt()) + .updatedAt(walletDto.getUpdatedAt()) + .identifier(sessionDto.getIdentifier()) + .build(); + } + + /** + * Converts a wallet credential to the inji-openid4vp {@link Credential} type. + * For SD-JWT credentials, disclosures are pre-filtered to the user's selectedSdClaims. + */ + private Credential toLibraryCredential(DecryptedCredentialDTO dto, Map> selectedSdClaims) { + VCCredentialResponse vc = dto.getCredential(); + if (CredentialFormat.isSdJwt(vc.getFormat())) { + FormatType format = mapToFormatType(vc.getFormat()); + String sdJwt = buildFilteredSdJwt( + dto, selectedSdClaims != null ? selectedSdClaims.get(dto.getId()) : null); + return new Credential(format, sdJwt, dto.getId()); + } + if (!CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(vc.getFormat())) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Unsupported credential format: " + vc.getFormat()); + } + // inji-openid4vp expects LDP credentials as a JSON object (Map), not a typed POJO. + Credential mapped = DcqlMatchingHelper.toLibraryCredential(dto, objectMapper); + if (mapped == null) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Credential " + dto.getId() + " could not be mapped for OpenID4VP submission"); + } + log.info("mapped>>>>>> {}", mapped); + return mapped; + } + + private void validateDcqlSelections(SubmitPresentationRequestDTO request, VerifiablePresentationSessionData sessionData) + throws ApiNotAccessibleException, IOException { + + DCQLQuery dcqlQuery = openID4VPService.resolveDcqlQuery( + sessionData.getPresentationId(), + sessionData.getAuthorizationRequest(), + sessionData.isVerifierClientPreregistered()); + if (dcqlQuery == null) { + return; + } + + Set validQueryIds = dcqlQuery.getCredentials().stream() + .map(CredentialQuery::getId) + .collect(Collectors.toSet()); + + Map sessionById = Optional.ofNullable(sessionData.getMatchingCredentials()) + .orElse(List.of()).stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, d -> d, (a, b) -> a)); + + Map selectionCount = new LinkedHashMap<>(); + for (DcqlCredentialSelection selection : request.getDcqlSelections()) { + String resolvedQueryId = resolveDcqlMapKey(selection, sessionById); + int count = selection.getSelectedCredentialIds() != null ? selection.getSelectedCredentialIds().size() : 0; + selectionCount.merge(resolvedQueryId, count, Integer::sum); + + if (!validQueryIds.contains(resolvedQueryId)) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Unknown DCQL query id '" + resolvedQueryId + "'. Valid query ids: " + validQueryIds); + } + } + + for (CredentialQuery query : dcqlQuery.getCredentials()) { + if (!query.getMultiple()) { + int count = selectionCount.getOrDefault(query.getId(), 0); + if (count > 1) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "DCQL query '" + query.getId() + "' has multiple=false but " + count + " credential(s) were selected"); + } + } + } + + for (CredentialSetQuery setQuery : DcqlCredentialSetHelper.resolveEffectiveCredentialSets(dcqlQuery)) { + if (!setQuery.getRequired()) { + continue; + } + Set allQidsInSet = setQuery.getOptions().stream() + .flatMap(List::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Set selectedForSet = allQidsInSet.stream() + .filter(qid -> selectionCount.getOrDefault(qid, 0) > 0) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + boolean exactlyOneOptionMatched = setQuery.getOptions().stream() + .anyMatch(option -> new LinkedHashSet<>(option).equals(selectedForSet)); + + if (!exactlyOneOptionMatched) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Credential selection must satisfy exactly one option in credential_set. " + + "Selected query ids: " + selectedForSet + ", Options: " + setQuery.getOptions()); + } + } + } + + /** + * Merges explicit {@code selectedSdClaims} with DCQL {@code claim_sets} resolution. + * When a query defines {@code claim_sets} and the client omits {@code selectedSdClaims} for a credential, + * the first satisfiable claim set (or {@code selectedClaimIds}) determines which SD-JWT paths are disclosed. + */ + private Map> resolveSubmissionSdClaims( + SubmitPresentationRequestDTO request, + VerifiablePresentationSessionData sessionData, + Map walletCredentialsById) + throws ApiNotAccessibleException, IOException { + + Map> merged = new LinkedHashMap<>(); + Map> explicit = request.resolveEffectiveSelectedSdClaims(); + SelectedSdClaimsUtil.mergeInto(merged, explicit); + if (!request.isDcqlSubmission()) { + return merged.isEmpty() ? null : merged; + } + + DCQLQuery dcqlQuery = openID4VPService.resolveDcqlQuery( + sessionData.getPresentationId(), + sessionData.getAuthorizationRequest(), + sessionData.isVerifierClientPreregistered()); + if (dcqlQuery == null) { + return merged.isEmpty() ? null : merged; + } + + Map queriesById = dcqlQuery.getCredentials().stream() + .collect(Collectors.toMap(CredentialQuery::getId, q -> q, (a, b) -> a, LinkedHashMap::new)); + Map sessionById = Optional.ofNullable(sessionData.getMatchingCredentials()) + .orElse(List.of()).stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, d -> d, (a, b) -> a)); + + for (DcqlCredentialSelection selection : request.getDcqlSelections()) { + String queryId = resolveDcqlMapKey(selection, sessionById); + CredentialQuery credentialQuery = queriesById.get(queryId); + if (credentialQuery == null || selection.getSelectedCredentialIds() == null) { + continue; + } + for (String credentialId : selection.getSelectedCredentialIds()) { + if (hasExplicitSdClaimsForCredential(explicit, credentialId, selection)) { + continue; + } + DecryptedCredentialDTO sessionDto = sessionById.get(credentialId); + if (sessionDto == null) { + continue; + } + DecryptedCredentialDTO credForSd = + resolveCredentialForSubmission(sessionDto, walletCredentialsById); + Map sdClaimsMap = extractSdClaimsMap(credForSd); + + if (DcqlClaimSetHelper.hasClaimSets(credentialQuery)) { + // claim_sets present: resolve the first satisfiable set + List claimIds = DcqlClaimSetHelper.resolveClaimIdsForSubmission( + credentialQuery, + null, + path -> sdClaimsMap != null && hasDisclosureForPath(sdClaimsMap, path)); + List claimPaths = DcqlClaimSetHelper.resolveClaimPaths(credentialQuery, claimIds); + if (!claimPaths.isEmpty()) { + SelectedSdClaimsUtil.mergePaths(merged, credentialId, claimPaths); + log.info("DCQL claim_sets resolved for credential {} query '{}': claimIds={}, paths={}", + credentialId, queryId, claimIds, claimPaths); + } + } else { + // No claim_sets: per DCQL spec §6, all queried claims that have SD + // disclosures in this credential must be included in the VP. + resolveAllQueriedSdClaims(credentialQuery, credentialId, sdClaimsMap, merged, queryId); + } + } + } + return merged.isEmpty() ? null : merged; + } + + private boolean hasExplicitSdClaimsForCredential( + Map> explicit, + String credentialId, + DcqlCredentialSelection selection) { + if (explicit != null && explicit.containsKey(credentialId)) { + return true; + } + return selection.getSelectedSdClaims() != null && selection.getSelectedSdClaims().containsKey(credentialId); + } + + /** + * When {@code claim_sets} is absent, all claims in {@code CredentialQuery.getClaims()} that have + * an SD disclosure in the credential must be included in the VP per DCQL spec §6. + * Public claims (those in the JWT payload, not in SD disclosures) are skipped — they are + * always present in the credential JWT and require no explicit selection. + */ + private void resolveAllQueriedSdClaims(CredentialQuery credentialQuery, String credentialId, + Map sdClaimsMap, Map> merged, String queryId) { + List claims = credentialQuery.getClaims(); + if (claims == null || claims.isEmpty()) { + return; + } + List sdPaths = claims.stream() + .filter(cq -> cq.getPath() != null && !cq.getPath().isEmpty()) + .map(cq -> DcqlClaimSetHelper.buildClaimPath(cq.getPath())) + .filter(path -> sdClaimsMap != null && hasDisclosureForPath(sdClaimsMap, path)) + .collect(Collectors.toList()); + if (!sdPaths.isEmpty()) { + SelectedSdClaimsUtil.mergePaths(merged, credentialId, sdPaths); + log.info("DCQL all-claims resolved for credential {} query '{}': paths={}", + credentialId, queryId, sdPaths); + } + } + + @SuppressWarnings("unchecked") + private Map extractSdClaimsMap(DecryptedCredentialDTO dto) { + try { + if (!CredentialFormat.isSdJwt(dto.getCredential().getFormat())) { + return null; + } + Map allProps = credentialFormatHandlerFactory + .getHandler(dto.getCredential().getFormat()) + .extractAllCredentialProperties(dto.getCredential()); + if (allProps == null || !(allProps.get("sdClaims") instanceof Map raw)) { + return null; + } + return (Map) raw; + } catch (Exception e) { + log.warn("Could not extract sdClaims for credential {}: {}", dto.getId(), e.getMessage()); + return null; + } + } + + private boolean hasDisclosureForPath(Map sdClaimsMap, String claimPath) { + String normalized = normalizeSdClaimPath(claimPath); + Object disc = resolveSdClaimDisclosures(sdClaimsMap, normalized); + if (disc instanceof List list) { + return !list.isEmpty(); + } + return disc instanceof String s && !s.isBlank(); + } + + /** + * Signs each unsigned VP token. LDP_VC uses detached JWT signing; SD-JWT signs the KB-JWT + * payload bytes provided by the library. Signers are cached per algorithm. + */ + private List signVPTokens( + List tokens, String walletId, String base64Key) + throws JOSEException, KeyGenerationException, DecryptionException { + + List results = new ArrayList<>(); + // Cache one signer per algorithm so multiple credentials sharing an alg reuse the same key fetch + Map signerCache = new EnumMap<>(SigningAlgorithm.class); + + for (UnsignedVPToken token : tokens) { + SigningAlgorithm algorithm = SigningAlgorithm.fromString( + Optional.ofNullable(token.getSignatureAlgorithm()).filter(s -> !s.isBlank()) + .orElse(SigningAlgorithm.ED25519.getJWSAlgorithm().getName())); + + JWSSigner signer = signerCache.get(algorithm); + if (signer == null) { + KeyPair keyPair = keyPairService.getKeyPairFromDB(walletId, base64Key, algorithm); + JWK jwk = SigningKeyUtil.generateJwk(algorithm, keyPair); + signer = SigningKeyUtil.createSigner(algorithm, jwk); + signerCache.put(algorithm, signer); + } + + byte[] dataToSign = token.getDataToSign(); + Base64URL signature; + try { + if (token.getFormat() == FormatType.LDP_VC) { + // LDP_VC: sign payload bytes after the first '.' separator + int dotIndex = indexOfDot(dataToSign); + String headerB64 = new String(dataToSign, 0, dotIndex, StandardCharsets.US_ASCII); + byte[] payload = Arrays.copyOfRange(dataToSign, dotIndex + 1, dataToSign.length); + JWSHeader header = JWSHeader.parse(new Base64URL(headerB64)); + signature = signer.sign(header, payload); + } else { + // SD-JWT: standard JWT signing input is ASCII bytes of "headerB64.payloadB64" + String unsignedJwt = new String(dataToSign, StandardCharsets.US_ASCII); + String headerB64 = unsignedJwt.substring(0, unsignedJwt.indexOf('.')); + JWSHeader header = JWSHeader.parse(new Base64URL(headerB64)); + signature = signer.sign(header, dataToSign); + } + } catch (ParseException e) { + throw new JOSEException("Failed to parse JWS header for VP token signing", e); + } + results.add(new VPTokenSigningResult(token.getId(), signature.decode())); + } + return results; + } + + private int indexOfDot(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == '.') { + return i; + } + } + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Invalid LDP dataToSign: missing '.' separator"); + } + + /** + * Builds an SD-JWT string containing only the user-selected disclosures. + * If selectedPaths is null/empty, no disclosures are shared for that credential. + */ + private String buildFilteredSdJwt(DecryptedCredentialDTO credential, List selectedPaths) { + if (!(credential.getCredential().getCredential() instanceof String sdJwtString)) { + return String.valueOf(credential.getCredential().getCredential()); + } + // The issuer-signed credential JWT is everything before the first '~' + String credentialJwt = sdJwtString.split("~", -1)[0]; + // No selection for this credential -> share zero disclosures + if (selectedPaths == null || selectedPaths.isEmpty()) { + return credentialJwt + "~"; + } + try { + Map allProps = credentialFormatHandlerFactory + .getHandler(credential.getCredential().getFormat()) + .extractAllCredentialProperties(credential.getCredential()); + if (!(allProps.get("sdClaims") instanceof Map rawSdClaims)) { + return credentialJwt + "~"; + } + @SuppressWarnings("unchecked") + Map sdClaimsMap = (Map) rawSdClaims; + Set normalizedPaths = selectedPaths.stream() + .map(this::normalizeSdClaimPath) + .collect(Collectors.toCollection(LinkedHashSet::new)); + // Collect disclosure blobs for the selected paths (preserving order, deduplicating) + Set disclosures = new LinkedHashSet<>(); + for (String path : normalizedPaths) { + Object disc = resolveSdClaimDisclosures(sdClaimsMap, path); + int before = disclosures.size(); + addDisclosureStrings(disclosures, disc); + if (disclosures.size() == before) { + log.warn("No SD-JWT disclosures found for credential {} path '{}' (available keys: {})", + credential.getId(), path, sdClaimsMap.keySet()); + } + } + log.info("SD-JWT filtering for credential {}: selectedPaths={}, disclosuresIncluded={}", + credential.getId(), normalizedPaths, disclosures.size()); + // Reconstruct: credentialJwt ~ disc1 ~ disc2 ~ (trailing ~ required by SD-JWT) + StringBuilder sb = new StringBuilder(credentialJwt); + for (String disc : disclosures) { + sb.append("~").append(disc); + } + sb.append("~"); + return sb.toString(); + } catch (Exception e) { + log.warn("SD-JWT filtering failed for credential {}: {}", credential.getId(), e.getMessage()); + return credentialJwt + "~"; + } + } + + private String normalizeSdClaimPath(String path) { + if (path == null) { + return ""; + } + String normalized = path.startsWith("$.") ? path.substring(2) : path; + return normalized.replaceFirst("^credentialSubject(\\[\\d+\\])?\\.", ""); + } + + /** + * Resolves disclosure blobs for a UI claim path. Matching returns paths like {@code $.dateOfBirth} + * while the SD-JWT handler may store keys as {@code credentialSubject.dateOfBirth} or + * {@code credentialSubject[0].dateOfBirth}. + */ + private Object resolveSdClaimDisclosures(Map sdClaimsMap, String normalizedPath) { + String targetPath = normalizeSdClaimPath(normalizedPath); + for (Map.Entry entry : sdClaimsMap.entrySet()) { + if (sdClaimPathMatches(entry.getKey(), targetPath)) { + return entry.getValue(); + } + } + return null; + } + + private boolean sdClaimPathMatches(String mapKey, String normalizedPath) { + if (mapKey == null || normalizedPath == null) { + return false; + } + return normalizeSdClaimPath(mapKey).equals(normalizedPath); + } + + private void addDisclosureStrings(Set disclosures, Object disc) { + if (disc instanceof List list) { + list.forEach(d -> { + if (d != null) { + disclosures.add(d.toString()); + } + }); + } else if (disc instanceof String s && !s.isBlank()) { + disclosures.add(s); + } + } + + /** + * Maps format string to FormatType enum. + */ + private FormatType mapToFormatType(String format) { + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(format)) { + return FormatType.LDP_VC; + } + if (CredentialFormat.VC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + return FormatType.VC_SD_JWT; + } + if (CredentialFormat.DC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + return FormatType.DC_SD_JWT; + } + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Unsupported credential format: " + format); + } + + /** + * Fetches selected credentials from the session cache. + * Every requested credential ID must exist in the session (fail-fast, same as DCQL submit). + */ + private List fetchSelectedCredentials( + VerifiablePresentationSessionData sessionData, List selectedCredentialIds) { + if (sessionData == null || sessionData.getMatchingCredentials() == null) { + throw new IllegalStateException("No matching credentials in session — call GET /credentials first"); + } + Map cache = sessionData.getMatchingCredentials().stream() + .collect(Collectors.toMap(DecryptedCredentialDTO::getId, dto -> dto, (a, b) -> a)); + + List result = new ArrayList<>(); + for (String id : selectedCredentialIds) { + DecryptedCredentialDTO dto = cache.get(id); + if (dto == null) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), + "Selected credential not found in session: " + id); + } + result.add(dto); + } + return result; + } + + /** + * Validates all input parameters for presentation submission. + */ + private void validateSubmissionRequest(SubmitPresentationRequestDTO request) { + if (request == null) { + throw new java.lang.IllegalArgumentException("Request cannot be null"); + } + if (request.getSelectedCredentials() == null || request.getSelectedCredentials().isEmpty()) { + throw new java.lang.IllegalArgumentException("Selected credentials cannot be null or empty"); + } + } + + /** + * Creates a VerifiablePresentationVerifierDTO from the authorization request. + */ + private VerifiablePresentationVerifierDTO createVPResponseVerifierDTO( + List preRegisteredVerifiers, AuthorizationRequest authorizationRequest, String walletId) { + boolean preRegistered = preRegisteredVerifiers.stream() + .map(Verifier::getClientId).anyMatch(id -> id.equals(authorizationRequest.getClientId())); + boolean trusted = verifierService.isVerifierTrustedByWallet(authorizationRequest.getClientId(), walletId); + String clientName = resolveClientName(authorizationRequest); + String logo = resolveLogoUri(authorizationRequest); + return new VerifiablePresentationVerifierDTO( + authorizationRequest.getClientId(), clientName, logo, trusted, preRegistered, + authorizationRequest.getRedirectUri()); + } + + private String resolveClientName(AuthorizationRequest authorizationRequest) { + if (authorizationRequest instanceof AuthorizationDcqlRequest dcqlRequest) { + return Optional.ofNullable(dcqlRequest.getClientMetadata()) + .map(ClientMetadata::getClientName).filter(n -> !n.isBlank()) + .orElse(authorizationRequest.getClientId()); + } + if (authorizationRequest instanceof AuthorizationPresentationExchangeRequest peRequest) { + return Optional.ofNullable(peRequest.getClientMetadata()) + .map(ClientMetadataDraft23::getClientName).filter(n -> !n.isBlank()) + .orElse(authorizationRequest.getClientId()); + } + return authorizationRequest.getClientId(); + } + + private String resolveLogoUri(AuthorizationRequest authorizationRequest) { + if (authorizationRequest instanceof AuthorizationDcqlRequest dcqlRequest) { + return Optional.ofNullable(dcqlRequest.getClientMetadata()) + .map(ClientMetadata::getLogoUri).orElse(null); + } + if (authorizationRequest instanceof AuthorizationPresentationExchangeRequest peRequest) { + return Optional.ofNullable(peRequest.getClientMetadata()) + .map(ClientMetadataDraft23::getLogoUri).orElse(null); + } + return null; + } + + /** + * Handles verifier rejection with error details. + */ + private ResponseEntity handleVerifierRejection( + String walletId, VerifiablePresentationSessionData vpSessionData, SubmitPresentationRequestDTO request) + throws VPErrorNotSentException { + // Create ErrorDTO from the request + ErrorDTO payload = new ErrorDTO(); + payload.setErrorCode(request.getErrorCode()); + payload.setErrorMessage(request.getErrorMessage()); + return ResponseEntity.ok(rejectVerifier(walletId, vpSessionData, payload)); + } + + /** + * Rejects the verifier by sending error information. + */ + private SubmitPresentationResponseDTO rejectVerifier( + String walletId, VerifiablePresentationSessionData vpSessionData, ErrorDTO payload) + throws VPErrorNotSentException { + try { + VerifierResponse verifierResponse = openID4VPService.sendErrorToVerifier(vpSessionData, payload); + SubmitPresentationResponseDTO result = new SubmitPresentationResponseDTO(); + result.setStatus(REJECTED_VERIFIER.getErrorCode()); + result.setMessage(REJECTED_VERIFIER.getErrorMessage()); + result.setRedirectUri(verifierResponse.getRedirectUri()); + return result; + } catch (ApiNotAccessibleException | IOException | URISyntaxException | java.lang.IllegalArgumentException e) { + throw new VPErrorNotSentException("Failed to send rejection to verifier — " + e.getMessage()); + } + } + + /** + * Stores presentation record in the database. + */ + private void storePresentationRecord( + String walletId, String presentationId, SubmitPresentationRequestDTO request, + VerifiablePresentationSessionData sessionData, boolean success, LocalDateTime requestedAt) { + try { + if (sessionData == null) { + return; + } + // Extract verifier information from session data + VerifiablePresentation presentation = VerifiablePresentation.builder() + .id(presentationId) + .walletId(walletId) + .authRequest(extractVerifierAuthRequest(sessionData)) + .presentationData(buildPresentationDataJson(request)) + .verifierId(extractVerifierId(sessionData)) + .status(success ? OpenID4VPConstants.STATUS_SUCCESS : OpenID4VPConstants.STATUS_ERROR) + .requestedAt(requestedAt) + .consent(true) + .build(); + // Save to database + verifiablePresentationsRepository.save(presentation); + } catch (Exception e) { + log.error("Failed to store presentation record for presentationId={}", presentationId, e); + } + } + + /** + * Creates presentation data JSON with selected credentials and SD claim selections. + */ + private String buildPresentationDataJson(SubmitPresentationRequestDTO request) throws JsonProcessingException { + Map presentationData = new LinkedHashMap<>(); + presentationData.put(OpenID4VPConstants.SELECTED_CREDENTIALS, request.getSelectedCredentials()); + Map> selectedSdClaims = request.resolveEffectiveSelectedSdClaims(); + if (selectedSdClaims != null) { + presentationData.put(OpenID4VPConstants.SELECTED_SD_CLAIMS, selectedSdClaims); + } + return objectMapper.writeValueAsString(presentationData); + } + + /** + * Extracts verifier ID from session data. + */ + private String extractVerifierId(VerifiablePresentationSessionData sessionData) { + try { + // Since authorizationRequest is a URL, extract client_id from URL parameters + if (sessionData.getAuthorizationRequest() != null) { + return UrlParameterUtils.extractQueryParameter( + sessionData.getAuthorizationRequest(), OpenID4VPConstants.CLIENT_ID_PARAM); + } + } catch (Exception e) { + log.warn("Failed to extract verifier ID", e); + } + return UNKNOWN_VERIFIER; + } + + /** + * Extracts verifier authorization request as JSON. + */ + private String extractVerifierAuthRequest(VerifiablePresentationSessionData sessionData) { + try { + if (sessionData.getAuthorizationRequest() != null) { + // Convert the URL string to a JSON object + return objectMapper.writeValueAsString(Map.of( + OpenID4VPConstants.AUTHORIZATION_REQUEST_URL, sessionData.getAuthorizationRequest())); + } + } catch (Exception e) { + log.warn("Failed to extract verifier auth request", e); + } + return EMPTY_JSON; + } +} diff --git a/src/main/java/io/mosip/mimoto/service/impl/WalletServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/WalletServiceImpl.java new file mode 100644 index 000000000..7d042f702 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/service/impl/WalletServiceImpl.java @@ -0,0 +1,112 @@ +package io.mosip.mimoto.service.impl; + +import io.mosip.mimoto.dto.WalletDetailsResponseDto; +import io.mosip.mimoto.dto.WalletResponseDto; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.service.WalletLockService; +import io.mosip.mimoto.service.WalletService; +import io.mosip.mimoto.service.WalletUnlockService; +import io.mosip.mimoto.util.WalletLockStatusUtils; +import io.mosip.mimoto.util.WalletUtil; +import io.mosip.mimoto.util.WalletValidator; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.function.Supplier; + + +/** + * Implementation of {@link WalletService} for managing wallets. + */ +@Slf4j +@Service +public class WalletServiceImpl implements WalletService { + + private final WalletRepository repository; + private final WalletUtil walletUtil; + private final WalletValidator validator; + private final WalletUnlockService walletUnlockService; + + private final WalletLockService walletLockService; + + @Autowired + public WalletServiceImpl(WalletRepository repository, WalletUtil walletUtil, WalletValidator validator, WalletUnlockService walletUnlockService, WalletLockService walletLockService) { + this.repository = repository; + this.walletUtil = walletUtil; + this.validator = validator; + this.walletUnlockService = walletUnlockService; + this.walletLockService = walletLockService; + } + + @Override + public WalletResponseDto createWallet(String userId, String name, String pin, String confirmPin) throws InvalidRequestException { + log.info("Creating wallet for user: {}, name: {}", userId, name); + + validator.validateUserId(userId); + validator.validateWalletName(name); + validator.validateWalletPin(pin); + + if (!pin.equals(confirmPin)) { + log.warn("Wallet PIN and confirm PIN are not matching: {}", userId); + throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Entered PIN and Confirm PIN do not match"); + } + + String walletId = walletUtil.createWallet(userId, name, pin); + log.debug("Wallet created successfully: {}", walletId); + return WalletResponseDto.builder().walletId(walletId).walletName(name).build(); + } + + @Override + public WalletResponseDto unlockWallet(String walletId, String pin, String userId) throws InvalidRequestException { + log.info("Unlocking Wallet: {} for User: {}", walletId, userId); + + validator.validateUserId(userId); + validator.validateWalletPin(pin); + + return repository.findByUserIdAndId(userId, walletId).map(wallet -> { + String decryptedWalletKey = walletUnlockService.handleUnlock(wallet, pin); + return new WalletResponseDto(walletId, wallet.getWalletMetadata().getName(), decryptedWalletKey); + }).orElseThrow(getWalletNotFoundExceptionSupplier(userId, walletId)); + } + + @Override + public List getWallets(String userId) { + log.debug("validating user ID provided"); + validator.validateUserId(userId); + + log.info("Retrieving wallets for user: {}", userId); + + return repository.findWalletByUserId(userId).stream().map(wallet -> { + wallet = walletLockService.resetTemporaryLockIfExpired(wallet); + repository.save(wallet); + return WalletDetailsResponseDto.builder() + .walletId(wallet.getId()) + .walletName(wallet.getWalletMetadata().getName()) + .walletStatus(WalletLockStatusUtils.getWalletLockStatus(wallet)) + .build(); + }).toList(); + } + + @Override + public void deleteWallet(String userId, String walletId) throws InvalidRequestException { + validator.validateUserId(userId); + Wallet existingWallet = repository.findByUserIdAndId(userId, walletId) + .orElseThrow(getWalletNotFoundExceptionSupplier(userId, walletId)); + repository.delete(existingWallet); + log.info("Successfully deleted wallet with ID: {} for user: {}", walletId, userId); + } + + @NotNull + private static Supplier getWalletNotFoundExceptionSupplier(String userId, String walletId) { + return () -> { + log.warn("Wallet not found: {} for user: {}", walletId, userId); + return new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Wallet not found"); + }; + } +} diff --git a/src/main/java/io/mosip/mimoto/spi/CbeffUtil.java b/src/main/java/io/mosip/mimoto/spi/CbeffUtil.java deleted file mode 100644 index 9cb9bb964..000000000 --- a/src/main/java/io/mosip/mimoto/spi/CbeffUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.mosip.mimoto.spi; - -import java.util.List; -import java.util.Map; - -import io.mosip.mimoto.entity.BIR; - -/** - * @author - * - * - * Interface for Cbeff Interface - * - */ -public interface CbeffUtil { - - public byte[] createXML(List cbeffPack) throws Exception; - - public byte[] updateXML(List cbeffPackList, byte[] fileBytes) throws Exception; - - public boolean validateXML(byte[] xmlBytes, byte[] xsdBytes) throws Exception; - - public boolean validateXML(byte[] xmlBytes) throws Exception; - - public Map getBDBBasedOnType(byte[] fileBytes, String type, String subType) throws Exception; - - public List getBIRDataFromXML(byte[] xmlBytes) throws Exception; - - public Map getAllBDBData(byte[] xmlBytes, String type, String subType) throws Exception; - - public byte[] createXML(List birList, byte[] xsd) throws Exception; - - public List getBIRDataFromXMLType(byte[] xmlBytes, String type) throws Exception; - -} diff --git a/src/main/java/io/mosip/mimoto/spi/IBioApi.java b/src/main/java/io/mosip/mimoto/spi/IBioApi.java deleted file mode 100644 index 34159c04e..000000000 --- a/src/main/java/io/mosip/mimoto/spi/IBioApi.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.mosip.mimoto.spi; - -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.entity.MatchDecision; -import io.mosip.mimoto.model.KeyValuePair; -import io.mosip.mimoto.model.QualityScore; -import io.mosip.mimoto.model.Response; - -/** - * The Interface IBioApi. - * - * @author Sanjay Murali - * @author Manoj SP - * - */ -public interface IBioApi { - - /** - * It checks the quality of the provided biometric image and render the - * respective quality score. - * - * @param sample the sample - * @param flags the flags - * @return the response - */ - Response checkQuality(BIR sample, KeyValuePair[] flags); - - /** - * It compares the biometrics and provide the respective matching scores. - * - * @param sample the sample - * @param gallery the gallery - * @param flags the flags - * @return the response - */ - Response match(BIR sample, BIR[] gallery, KeyValuePair[] flags); - - /** - * Extract template. - * - * @param sample the sample - * @param flags the flags - * @return the response - */ - Response extractTemplate(BIR sample, KeyValuePair[] flags); - - /** - * It segment the single biometric image into multiple biometric images. Eg: - * Split the thumb slab into multiple fingers - * - * @param sample the sample - * @param flags the flags - * @return the response - */ - Response segment(BIR sample, KeyValuePair[] flags); -} diff --git a/src/main/java/io/mosip/mimoto/util/AttestationOfflineVerify.java b/src/main/java/io/mosip/mimoto/util/AttestationOfflineVerify.java index 32b2355e6..9c7d1c58f 100644 --- a/src/main/java/io/mosip/mimoto/util/AttestationOfflineVerify.java +++ b/src/main/java/io/mosip/mimoto/util/AttestationOfflineVerify.java @@ -19,6 +19,7 @@ import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; +import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.springframework.stereotype.Component; @@ -33,9 +34,9 @@ /** * Verify the device attestation statement offline. */ +@Slf4j @Component public class AttestationOfflineVerify { - private final Logger logger = LoggerUtil.getLogger(AttestationOfflineVerify.class); private final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier(); @@ -64,8 +65,8 @@ public AttestationStatement parseAndVerify(String signedAttestationStatment) thr if (!verifyHostname("attest.android.com", cert)) { throw new Exception("Certificate isn't issued for the hostname attest.android.com."); } - - logger.info("Sucessfully verified the signature of the attestation statement using offline method."); + + log.info("Sucessfully verified the signature of the attestation statement using offline method."); // Extract and use the payload data. AttestationStatement stmt = (AttestationStatement) jws.getPayload(); @@ -88,7 +89,7 @@ private boolean verifyHostname(String hostname, X509Certificate leafCert) { HOSTNAME_VERIFIER.verify(hostname, leafCert); return true; } catch (SSLException e) { - logger.error("Error when verifying hostname", e); + log.error("Error when verifying hostname", e); } return false; diff --git a/src/main/java/io/mosip/mimoto/util/AttestationOnlineVerify.java b/src/main/java/io/mosip/mimoto/util/AttestationOnlineVerify.java index c0e2a5b0c..9d94361ef 100644 --- a/src/main/java/io/mosip/mimoto/util/AttestationOnlineVerify.java +++ b/src/main/java/io/mosip/mimoto/util/AttestationOnlineVerify.java @@ -29,6 +29,7 @@ import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.util.Base64; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -47,8 +48,8 @@ * otherwise all requests will fail. */ @Component +@Slf4j public class AttestationOnlineVerify { - private final Logger logger = LoggerUtil.getLogger(AttestationOnlineVerify.class); private final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); @@ -118,7 +119,7 @@ public AttestationStatement parseAndVerify(String signedAttestationStatment) thr throw new Exception("The cryptographic signature of the attestation statement couldn't be " + "verified."); } - logger.info("Sucessfully verified the signature of the attestation statement using online method."); + log.info("Sucessfully verified the signature of the attestation statement using online method."); // The signature is valid, extract the data JSON from the JWS signature. byte[] data = extractJwsData(signedAttestationStatment); diff --git a/src/main/java/io/mosip/mimoto/util/AuditLogRequestBuilder.java b/src/main/java/io/mosip/mimoto/util/AuditLogRequestBuilder.java index 552cceda2..b00255f09 100644 --- a/src/main/java/io/mosip/mimoto/util/AuditLogRequestBuilder.java +++ b/src/main/java/io/mosip/mimoto/util/AuditLogRequestBuilder.java @@ -1,14 +1,5 @@ package io.mosip.mimoto.util; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - import io.mosip.mimoto.constant.ApiName; import io.mosip.mimoto.constant.AuditLogConstant; import io.mosip.mimoto.constant.LoggerFileConstant; @@ -18,18 +9,20 @@ import io.mosip.mimoto.dto.AuditResponseDto; import io.mosip.mimoto.exception.ApisResourceAccessException; import io.mosip.mimoto.service.RestClientService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; /** * The Class AuditRequestBuilder. - * + * * @author Rishabh Keshari */ +@Slf4j @Component public class AuditLogRequestBuilder { - /** The logger. */ - private final Logger logger = LoggerFactory.getLogger(AuditLogRequestBuilder.class); - /** The registration processor rest service. */ @Autowired private RestClientService registrationProcessorRestService; @@ -57,12 +50,10 @@ public class AuditLogRequestBuilder { * @return the audit response dto */ @SuppressWarnings("unchecked") - public ResponseWrapper createAuditRequestBuilder(String description, String eventId, - String eventName, String eventType, String registrationId, ApiName apiname) { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), + public ResponseWrapper createAuditRequestBuilder(String description, String eventId, String eventName, String eventType, String registrationId, ApiName apiname) { + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), registrationId, - "AuditLogRequestBuilder:: createAuditRequestBuilder(String description, String eventId, String eventName, String eventType,\r\n" - + " String registrationId, ApiName apiname)::entry"); + "AuditLogRequestBuilder:: createAuditRequestBuilder(String description, String eventId, String eventName, String eventType,\r\n String registrationId, ApiName apiname)::entry"); AuditRequestDto auditRequestDto = new AuditRequestDto(); RequestWrapper requestWrapper = new RequestWrapper<>(); @@ -93,10 +84,10 @@ public ResponseWrapper createAuditRequestBuilder(String descri "", requestWrapper, ResponseWrapper.class); } catch (ApisResourceAccessException arae) { - logger.error(arae.getMessage()); + log.error(arae.getMessage()); } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), registrationId, "AuditLogRequestBuilder:: createAuditRequestBuilder(String description, String eventId, String eventName, String eventType,\r\n" + " String registrationId, ApiName apiname)::exit"); @@ -107,7 +98,7 @@ public ResponseWrapper createAuditRequestBuilder(String descri @SuppressWarnings("unchecked") public ResponseWrapper createAuditRequestBuilder(String description, String eventId, String eventName, String eventType, String moduleId, String moduleName, String registrationId) { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), registrationId, "AuditLogRequestBuilder:: createAuditRequestBuilder(String description, String eventId, String eventName, String eventType,String moduleId,String moduleName,\r\n" + " String registrationId)::entry"); @@ -145,10 +136,10 @@ public ResponseWrapper createAuditRequestBuilder(String descri } catch (ApisResourceAccessException arae) { - logger.error(arae.getMessage()); + log.error(arae.getMessage()); } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), + log.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), registrationId, "AuditLogRequestBuilder:: createAuditRequestBuilder(String description, String eventId, String eventName, String eventType,String moduleId,String moduleName,\r\n" + " String registrationId)::exit"); diff --git a/src/main/java/io/mosip/mimoto/util/AuthorizationRequestHelper.java b/src/main/java/io/mosip/mimoto/util/AuthorizationRequestHelper.java new file mode 100644 index 000000000..b69162200 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/AuthorizationRequestHelper.java @@ -0,0 +1,34 @@ +package io.mosip.mimoto.util; + +import io.mosip.openID4VP.authorizationRequest.AuthorizationDcqlRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.dcql.query.DCQLQuery; + +/** + * Shared helpers for classifying OpenID4VP authorization requests using the + * library's parsed {@code dcql_query}. + */ +public final class AuthorizationRequestHelper { + + private AuthorizationRequestHelper() { + } + + /** + * Returns {@code true} when the library parsed a DCQL request with a non-null + * {@code dcql_query}. + */ + public static boolean hasDcqlQuery(AuthorizationRequest authorizationRequest) { + return authorizationRequest instanceof AuthorizationDcqlRequest dcqlRequest + && dcqlRequest.getDcqlQuery() != null; + } + + /** + * Extracts the DCQL query from a parsed authorization request, if present. + */ + public static DCQLQuery extractDcqlQuery(AuthorizationRequest authorizationRequest) { + if (authorizationRequest instanceof AuthorizationDcqlRequest dcqlRequest) { + return dcqlRequest.getDcqlQuery(); + } + return null; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/CbeffToBiometricUtil.java b/src/main/java/io/mosip/mimoto/util/CbeffToBiometricUtil.java index 61afc3b22..1174d03b7 100644 --- a/src/main/java/io/mosip/mimoto/util/CbeffToBiometricUtil.java +++ b/src/main/java/io/mosip/mimoto/util/CbeffToBiometricUtil.java @@ -1,85 +1,31 @@ package io.mosip.mimoto.util; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - +import io.mosip.kernel.biometrics.constant.BiometricType; +import io.mosip.kernel.biometrics.entities.BIR; +import io.mosip.kernel.biometrics.spi.CbeffUtil; import org.apache.commons.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.constant.BiometricType; -import io.mosip.mimoto.constant.LoggerFileConstant; -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.exception.BiometricException; -import io.mosip.mimoto.exception.BiometricTagMatchException; -import io.mosip.mimoto.exception.PlatformErrorMessages; -import io.mosip.mimoto.model.KeyValuePair; -import io.mosip.mimoto.service.impl.BioApiImpl; -import io.mosip.mimoto.service.impl.CbeffImpl; -import io.mosip.mimoto.spi.CbeffUtil; -import io.mosip.mimoto.spi.IBioApi; +import java.util.List; /** * The Class CbeffToBiometricUtil. - * - * @author M1048358 Alok - * @author M1030448 Jyoti + * + * @author Monobikash Das */ +@Component public class CbeffToBiometricUtil { - Logger logger = LoggerUtil.getLogger(CbeffToBiometricUtil.class); - /** The cbeffutil. */ - private CbeffUtil cbeffutil = new CbeffImpl(); + @Autowired + private CbeffUtil cbeffutil; /** the bioApi */ - private IBioApi bioAPi = new BioApiImpl(); - - /** - * Instantiates a new cbeff to biometric util. - * - * @param cbeffutil the cbeffutil - */ - public CbeffToBiometricUtil(CbeffUtil cbeffutil) { - this.cbeffutil = cbeffutil; - } - - /** - * Instantiates biometric util - * - */ - public CbeffToBiometricUtil() { - - } - - /** - * Gets the photo. - * - * @param cbeffFileString the cbeff file string - * @param type the type - * @param subType the sub type - * @return the photo - * @throws Exception the exception - */ - public byte[] getImageBytes(String cbeffFileString, String type, List subType) throws Exception { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::getImageBytes()::entry"); - - byte[] photoBytes = null; - if (cbeffFileString != null) { - List bIRTypeList = getBIRTypeList(cbeffFileString); - photoBytes = getPhotoByTypeAndSubType(bIRTypeList, type, subType); - } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::getImageBytes()::exit"); - - return photoBytes; - } /** * Gets the photo by type and sub type. * - * @param bIRTypeList the b IR type list + * @param bIRList the b IR type list * @param type the type * @param subType the sub type * @return the photo by type and sub type @@ -124,105 +70,6 @@ private boolean isBiometricType(String type, List biometricTypeLi return isType; } - /** - * Merge cbeff. - * - * @param cbeffFile1 the cbeff file 1 - * @param cbeffFile2 the cbeff file 2 - * @return the input stream - * @throws Exception the exception - */ - - public InputStream mergeCbeff(String cbeffFile1, String cbeffFile2) throws Exception { - byte[] mergedCbeffByte = null; - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::mergeCbeff()::entry"); - - List file1BirList = getBIRTypeList(cbeffFile1); - List file2BirList = getBIRTypeList(cbeffFile2); - - if (isBiometricTypeSame(file1BirList, file2BirList)) { - throw new BiometricTagMatchException(PlatformErrorMessages.MIMOTO_UTL_BIOMETRIC_TAG_MATCH.getCode()); - } - - file1BirList.addAll(file2BirList); - mergedCbeffByte = cbeffutil.createXML(file2BirList); - - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::mergeCbeff()::exit"); - - return new ByteArrayInputStream(mergedCbeffByte); - } - - /** - * Checks if is same type. - * - * @param file1BirTypeList the file 1 bir type list - * @param file2BirTypeList the file 2 bir type list - * @return true, if is same type - */ - private boolean isBiometricTypeSame(List file1BirList, List file2BirList) { - boolean isTypeSame = false; - for (BIR bir1 : file1BirList) { - List biometricTypeList1 = bir1.getBdbInfo().getType(); - for (BIR bir2 : file2BirList) { - List biometricTypeList2 = bir2.getBdbInfo().getType(); - if (biometricTypeList1.equals(biometricTypeList2)) { - isTypeSame = true; - break; - } - } - if (isTypeSame) - break; - } - return isTypeSame; - } - - /** - * Extract cbeff with types. - * - * @param cbeffFile the cbeff file - * @param types the types - * @return the input stream - * @throws Exception the exception - */ - public InputStream extractCbeffWithTypes(String cbeffFile, List types) throws Exception { - List extractedBIR = new ArrayList<>(); - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::extractCbeffWithTypes()::entry"); - - byte[] newCbeffByte = null; - List file2BirTypeList = getBIRTypeList(cbeffFile); - for (BIR bir : file2BirTypeList) { - List biometricTypeList = bir.getBdbInfo().getType(); - for (String type : types) { - if (biometricTypeList.get(0).value().equalsIgnoreCase(type)) { - extractedBIR.add(bir); - } - } - } - if (!extractedBIR.isEmpty()) { - newCbeffByte = cbeffutil.createXML(extractedBIR); - } else { - return null; - } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "CbeffToBiometricUtil::extractCbeffWithTypes()::exit"); - - return new ByteArrayInputStream(newCbeffByte); - } - - /** - * Convert BIRTYP eto BIR. - * - * @param listOfBIR the list of BIR - * @return the list - *//* - * public List convertBIRTYPEtoBIR(List listOfBIR) { - * - * return cbeffutil.convertBIRTypeToBIR(listOfBIR); } - */ - /** * Gets the BIR type list. * @@ -235,27 +82,4 @@ public List getBIRTypeList(String cbeffFileString) throws Exception { return cbeffutil.getBIRDataFromXML(Base64.decodeBase64(cbeffFileString)); } - /** - * Gets the BIR type list. - * - * @param xmlBytes byte array of XML data - * @return the BIR type list - * @throws Exception the exception - */ - public List getBIRDataFromXML(byte[] xmlBytes) throws Exception { - return cbeffutil.getBIRDataFromXML(xmlBytes); - } - - /** - * Gets the BIR template - * - * @param sample the sample - * @param flags the flags - * @return the biometric record - * @throws BiometricException - */ - public BIR extractTemplate(BIR sample, KeyValuePair[] flags) throws BiometricException { - return bioAPi.extractTemplate(sample, flags).getResponse(); - } - } diff --git a/src/main/java/io/mosip/mimoto/util/CbeffValidator.java b/src/main/java/io/mosip/mimoto/util/CbeffValidator.java deleted file mode 100644 index 3e9538ece..000000000 --- a/src/main/java/io/mosip/mimoto/util/CbeffValidator.java +++ /dev/null @@ -1,453 +0,0 @@ -package io.mosip.mimoto.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.OutputStreamWriter; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Collectors; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; -import javax.xml.transform.stream.StreamSource; - -import org.apache.commons.codec.binary.Base64; -import org.xml.sax.SAXException; - -import io.mosip.mimoto.constant.BiometricType; -import io.mosip.mimoto.constant.CbeffConstant; -import io.mosip.mimoto.entity.BDBInfo; -import io.mosip.mimoto.entity.BIR; -import io.mosip.mimoto.entity.BIRInfo; -import io.mosip.mimoto.entity.RegistryIDType; -import io.mosip.mimoto.entity.SingleAnySubtypeType; -import io.mosip.mimoto.entity.VersionType; -import io.mosip.mimoto.exception.CbeffException; - -/** - * @author Ramadurai Pandian - * - * An Utility Class to validate the data before generating an valid - * CBEFF XML and to get all the data based on Type and SubType - * - */ -public class CbeffValidator { - - /** - * Method used for custom validation of the BIR - * - * @param birRoot BIR data - * - * @return boolean value if BIR is valid - * - * @exception CbeffException when any condition fails - * - */ - public static boolean validateXML(BIR birRoot) throws CbeffException { - if (birRoot == null) { - throw new CbeffException("BIR value is null"); - } - List birList = birRoot.getBirs(); - for (BIR bir : birList) { - if (bir != null) { - if (bir.getBdb().length < 1) { - throw new CbeffException("BDB value can't be empty"); - } - if (bir.getBdbInfo() != null) { - BDBInfo bdbInfo = bir.getBdbInfo(); - // if - // (!Long.valueOf(bdbInfo.getFormat().getOrganization()).equals(CbeffConstant.FORMAT_OWNER)) - // { - // throw new CbeffException("Patron Format Owner should be standard specified of - // value " - // + CbeffConstant.FORMAT_OWNER); - // } - List biometricTypes = bdbInfo.getType(); - if (biometricTypes == null || biometricTypes.isEmpty()) { - throw new CbeffException("Type value needs to be provided"); - } - if (!validateFormatType(Long.valueOf(bdbInfo.getFormat().getType()), biometricTypes)) { - throw new CbeffException("Patron Format type is invalid"); - } - } else { - throw new CbeffException("BDB information can't be empty"); - } - } - } - return false; - - } - - /** - * Method used for validation of Format Type - * - * @param formatType format type - * - * @param biometricTypes List of types - * - * @return boolean value if format type is matching with type - * - */ - private static boolean validateFormatType(long formatType, List biometricTypes) { - BiometricType biometricType = biometricTypes.get(0); - switch (biometricType.value()) { - case "Finger": - return formatType == CbeffConstant.FORMAT_TYPE_FINGER - || formatType == CbeffConstant.FORMAT_TYPE_FINGER_MINUTIAE; - case "Iris": - return formatType == CbeffConstant.FORMAT_TYPE_IRIS; - case "Face": - return formatType == CbeffConstant.FORMAT_TYPE_FACE; - case "HandGeometry": - return formatType == CbeffConstant.FORMAT_TYPE_FACE; - } - - return false; - } - - /** - * Method used for getting Format Type Id from type string - * - * @param type format type - * - * @return long format type id - * - */ - private static long getFormatType(String type) { - switch (type.toLowerCase()) { - case "finger": - return CbeffConstant.FORMAT_TYPE_FINGER; - case "iris": - return CbeffConstant.FORMAT_TYPE_IRIS; - case "fmr": - return CbeffConstant.FORMAT_TYPE_FINGER_MINUTIAE; - case "face": - return CbeffConstant.FORMAT_TYPE_FACE; - case "handgeometry": - return CbeffConstant.FORMAT_TYPE_FACE; - } - return 0; - } - - /** - * Method used for creating XML bytes using JAXB - * - * @param bir BIR type - * @param xsd xml schema definition - * @return byte[] byte array of XML data - * - * @exception Exception exception - * - */ - public static byte[] createXMLBytes(BIR bir, byte[] xsd) throws Exception { - CbeffValidator.validateXML(bir); - JAXBContext jaxbContext = JAXBContext.newInstance(BIR.class); - Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); - jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // To - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(baos); - jaxbMarshaller.marshal(bir, writer); - byte[] savedData = baos.toByteArray(); - writer.close(); - try { - CbeffXSDValidator.validateXML(xsd, savedData); - } catch (SAXException sax) { - String message = sax.getMessage(); - message = message.substring(message.indexOf(":")); - throw new CbeffException("XSD validation failed due to attribute " + message); - } - return savedData; - } - - /* - * private static byte[] readXSD(String name) throws IOException { byte[] - * fileContent = Files.readAllBytes(Paths.get(tempPath + "/schema/" + name + - * ".xsd")); return fileContent; } - */ - - /** - * Method used for BIR Type - * - * @param fileBytes byte array of XML data - * - * @return BIR BIR data - * - * @exception Exception exception - * - */ - public static BIR getBIRFromXML(byte[] fileBytes) throws Exception { - JAXBContext jaxbContext = JAXBContext.newInstance(BIR.class); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - JAXBElement jaxBir = unmarshaller - .unmarshal(new StreamSource(new ByteArrayInputStream(fileBytes)), BIR.class); - BIR bir = jaxBir.getValue(); - return bir; - } - - /** - * Method used for searching Cbeff data based on type and subtype - * - * @param biometricType BIR data - * - * @param type format type - * - * @param subType format subtype - * - * @return bdbMap - * - * @exception Exception exception - * - */ - public static Map getBDBBasedOnTypeAndSubType(BIR bir, String type, String subType) - throws Exception { - - if (type == null && subType == null) { - return getAllLatestDatafromBIR(bir); - } - BiometricType biometricType = null; - SingleAnySubtypeType singleAnySubType = null; - Long formatType = null; - if (type != null) { - biometricType = getBiometricType(type); - formatType = getFormatType(type); - } - if (subType != null) { - singleAnySubType = getSingleAnySubtype(subType); - } - Map bdbMap = new HashMap<>(); - if (bir.getBirs() != null && !bir.getBirs().isEmpty()) { - populateBDBMap(bir, biometricType, singleAnySubType, formatType, bdbMap); - } - Map map = new TreeMap<>(bdbMap); - Map finalMap = new HashMap<>(); - for (Map.Entry mapEntry : map.entrySet()) { - String pattern = mapEntry.getKey().substring(0, mapEntry.getKey().lastIndexOf("_")); - if (mapEntry.getKey().contains(pattern)) { - finalMap.put(mapEntry.getKey().substring(0, mapEntry.getKey().lastIndexOf("_")), mapEntry.getValue()); - } - } - return finalMap; - } - - private static void populateBDBMap(BIR birRoot, BiometricType biometricType, - SingleAnySubtypeType singleAnySubType, Long formatType, Map bdbMap) { - for (BIR bir : birRoot.getBirs()) { - BDBInfo bdbInfo = bir.getBdbInfo(); - - if (bdbInfo != null) { - List singleSubTypeList = bdbInfo.getSubtype(); - List biometricTypes = bdbInfo.getType(); - String bdbFormatType = bdbInfo.getFormat().getType(); - boolean formatMatch = Long.valueOf(bdbFormatType).equals(formatType); - if (singleAnySubType == null && biometricTypes.contains(biometricType) && formatMatch) { - bdbMap.put( - biometricType.toString() + "_" + String.join(" ", singleSubTypeList) + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - Base64.encodeBase64String(bir.getBdb())); - } else if (biometricType == null && singleSubTypeList.contains(singleAnySubType.value())) { - List singleTypeStringList = convertToList(biometricTypes); - bdbMap.put( - String.join(" ", singleTypeStringList) + "_" + String.join(" ", singleSubTypeList) + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - Base64.encodeBase64String(bir.getBdb())); - } else if (biometricTypes.contains(biometricType) - && singleSubTypeList.contains(singleAnySubType != null ? singleAnySubType.value() : null) - && formatMatch) { - bdbMap.put( - biometricType.toString() + "_" + singleAnySubType.value() + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - Base64.encodeBase64String(bir.getBdb())); - } - } - } - } - - private static Map getAllLatestDatafromBIR(BIR birRoot) throws Exception { - Map bdbMap = new HashMap<>(); - if (birRoot.getBirs() != null && birRoot.getBirs().size() > 0) { - for (BIR bir : birRoot.getBirs()) { - BDBInfo bdbInfo = bir.getBdbInfo(); - - if (bdbInfo != null) { - List singleSubTypeList = bdbInfo.getSubtype(); - List biometricTypes = bdbInfo.getType(); - if (singleSubTypeList.isEmpty()) { - singleSubTypeList = new ArrayList<>(); - singleSubTypeList.add("No Subtype"); - } - String bdbFormatType = bdbInfo.getFormat().getType(); - bdbMap.put( - String.join(" ", biometricTypes.get(0).toString()) + "_" - + String.join(" ", singleSubTypeList) + "_" + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - Base64.encodeBase64String(bir.getBdb())); - } - } - } - Map map = new TreeMap<>(bdbMap); - Map finalMap = new HashMap<>(); - for (Map.Entry mapEntry : map.entrySet()) { - String pattern = mapEntry.getKey().substring(0, mapEntry.getKey().lastIndexOf("_")); - if (mapEntry.getKey().contains(pattern)) { - finalMap.put(mapEntry.getKey().substring(0, mapEntry.getKey().lastIndexOf("_")), mapEntry.getValue()); - } - } - return finalMap; - } - - /** - * Method to convert single type list to string - * - */ - private static List convertToList(List biometricTypeList) { - return biometricTypeList.stream().map(Enum::name).collect(Collectors.toList()); - } - - /** - * Method to get enum type from string type - * - */ - private static BiometricType getBiometricType(String type) { - if (isInEnum(type, BiometricType.class)) { - return BiometricType.valueOf(type); - } else { - switch (type) { - case "FMR": - return BiometricType.FINGER; - default: - return BiometricType.fromValue(type); - } - } - } - - public static > boolean isInEnum(String value, Class enumClass) { - for (E e : enumClass.getEnumConstants()) { - if (e.name().equals(value)) { - return true; - } - } - return false; - } - - public static Map getAllBDBData(BIR birRoot, String type, String subType) throws Exception { - BiometricType biometricType = null; - SingleAnySubtypeType singleAnySubType = null; - Long formatType = null; - if (type != null) { - biometricType = getBiometricType(type); - } - if (subType != null) { - singleAnySubType = getSingleAnySubtype(subType); - } - if (type != null) { - formatType = getFormatType(type); - } - Map bdbMap = new HashMap<>(); - List birs = birRoot.getBirs(); - if (birs != null && !birs.isEmpty()) { - for (BIR bir : birs) { - BDBInfo bdbInfo = bir.getBdbInfo(); - - if (bdbInfo != null) { - List singleSubTypeList = bdbInfo.getSubtype(); - List singleTypeList = bdbInfo.getType(); - String bdbFormatType = bdbInfo.getFormat().getType(); - boolean formatMatch = Long.valueOf(bdbFormatType).equals(formatType); - if (singleAnySubType == null && singleTypeList.contains(biometricType) && formatMatch) { - bdbMap.put( - biometricType.toString() + "_" + String.join(" ", singleSubTypeList) + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - new String(bir.getBdb(), "UTF-8")); - } else if (biometricType == null && singleSubTypeList.contains(singleAnySubType.value())) { - List singleTypeStringList = convertToList(singleTypeList); - bdbMap.put( - String.join(" ", singleSubTypeList) + "_" + String.join(" ", singleTypeStringList) + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - new String(bir.getBdb(), "UTF-8")); - } else if (singleTypeList.contains(biometricType) - && singleSubTypeList.contains(singleAnySubType != null ? singleAnySubType.value() : null) - && formatMatch) { - bdbMap.put( - singleAnySubType.toString() + "_" + biometricType.toString() + "_" - + String.valueOf(bdbFormatType) + "_" - + bdbInfo.getCreationDate().toInstant(ZoneOffset.UTC).toEpochMilli(), - new String(bir.getBdb(), "UTF-8")); - } - } - } - } - return bdbMap; - } - - /** - * Method to get enum sub type from string subtype - * - */ - private static SingleAnySubtypeType getSingleAnySubtype(String subType) { - return subType != null ? SingleAnySubtypeType.fromValue(subType) : null; - } - - private static List getBIRList(List birs) { - List birList = new ArrayList<>(); - for (BIR bir : birs) { - RegistryIDType format = new RegistryIDType(); - format.setOrganization(bir.getBdbInfo().getFormat().getOrganization()); - format.setType(bir.getBdbInfo().getFormat().getType()); - BIR.BIRBuilder birBuilder = new BIR.BIRBuilder(); - birBuilder.withBdb(bir.getBdb()).withOthers(bir.getOthers()) - .withBirInfo(new BIRInfo.BIRInfoBuilder().withIntegrity(bir.getBirInfo().getIntegrity()).build()) - .withBdbInfo(new BDBInfo.BDBInfoBuilder().withFormat(format) - .withQuality(bir.getBdbInfo().getQuality()).withType(bir.getBdbInfo().getType()) - .withSubtype(bir.getBdbInfo().getSubtype()) - .withPurpose(bir.getBdbInfo().getPurpose()).withLevel(bir.getBdbInfo().getLevel()) - .withCreationDate(bir.getBdbInfo().getCreationDate()).build()); - - VersionType versionType = bir.getVersion(); - if (versionType != null) { - birBuilder.withVersion(versionType); - } - - VersionType cbeffversionType = bir.getCbeffversion(); - if (cbeffversionType != null) { - birBuilder.withCbeffversion(cbeffversionType); - } - - birList.add(birBuilder.build()); - } - return birList; - } - - public static List getBIRDataFromXMLType(byte[] xmlBytes, String type) throws Exception { - BiometricType biometricType = null; - List updatedBIRList = new ArrayList<>(); - JAXBContext jaxbContext = JAXBContext.newInstance(BIR.class); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - JAXBElement jaxBir = unmarshaller - .unmarshal(new StreamSource(new ByteArrayInputStream(xmlBytes)), BIR.class); - BIR birRoot = jaxBir.getValue(); - for (BIR bir : birRoot.getBirs()) { - if (type != null) { - biometricType = getBiometricType(type); - BDBInfo bdbInfo = bir.getBdbInfo(); - if (bdbInfo != null) { - List biometricTypes = bdbInfo.getType(); - if (biometricTypes != null && biometricTypes.contains(biometricType)) { - updatedBIRList.add(bir); - } - } - } - } - return updatedBIRList; - } -} diff --git a/src/main/java/io/mosip/mimoto/util/CredentialShareExceptionHandler.java b/src/main/java/io/mosip/mimoto/util/CredentialShareExceptionHandler.java index 934da8823..89ac82138 100644 --- a/src/main/java/io/mosip/mimoto/util/CredentialShareExceptionHandler.java +++ b/src/main/java/io/mosip/mimoto/util/CredentialShareExceptionHandler.java @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import io.mosip.kernel.core.logger.spi.Logger; import io.mosip.mimoto.controller.CredentialShareController; import io.mosip.mimoto.dto.CredentialShareResponse; import io.mosip.mimoto.dto.ErrorDTO; @@ -23,7 +22,7 @@ /** * The Class CredentialShareExceptionHandler. - * + * * @author M1048358 Alok */ @RestControllerAdvice(assignableTypes = CredentialShareController.class) @@ -42,8 +41,6 @@ public class CredentialShareExceptionHandler { @Autowired private Environment env; - private Logger logger = LoggerUtil.getLogger(CredentialShareExceptionHandler.class); - @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity badRequest(MethodArgumentNotValidException ex) { @@ -98,4 +95,4 @@ private ResponseEntity buildApiExceptionResponse(Except return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); } -} \ No newline at end of file +} diff --git a/src/main/java/io/mosip/mimoto/util/DataShareUtil.java b/src/main/java/io/mosip/mimoto/util/DataShareUtil.java index 68bc8681f..a7456efac 100644 --- a/src/main/java/io/mosip/mimoto/util/DataShareUtil.java +++ b/src/main/java/io/mosip/mimoto/util/DataShareUtil.java @@ -1,9 +1,13 @@ package io.mosip.mimoto.util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.dto.DataShare; +import io.mosip.mimoto.dto.DataShareResponseDto; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.DataShareException; +import io.mosip.mimoto.service.RestClientService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpEntity; @@ -14,16 +18,9 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.constant.ApiName; -import io.mosip.mimoto.dto.DataShare; -import io.mosip.mimoto.dto.DataShareResponseDto; -import io.mosip.mimoto.dto.ErrorDTO; -import io.mosip.mimoto.exception.ApiNotAccessibleException; -import io.mosip.mimoto.exception.DataShareException; -import io.mosip.mimoto.service.RestClientService; - -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; @Component public class DataShareUtil { @@ -34,8 +31,6 @@ public class DataShareUtil { @Autowired private ObjectMapper mapper; - Logger logger = LoggerUtil.getLogger(DataShareUtil.class); - private static final String GET_DATA = "getDataShare"; private static final String DATASHARE = "DataShareUtil"; diff --git a/src/main/java/io/mosip/mimoto/util/DateUtils.java b/src/main/java/io/mosip/mimoto/util/DateUtils.java index 3c6b0a629..c55b7e2a0 100644 --- a/src/main/java/io/mosip/mimoto/util/DateUtils.java +++ b/src/main/java/io/mosip/mimoto/util/DateUtils.java @@ -635,7 +635,7 @@ public static String getUTCCurrentDateTimeString(String pattern) { * Provide UTC Current DateTime string for requestTime attribute. */ public static String getRequestTimeString() { - return DateUtils.getUTCCurrentDateTimeString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + return DateUtils.getUTCCurrentDateTimeString(UTC_DATETIME_PATTERN); } /** diff --git a/src/main/java/io/mosip/mimoto/util/DcqlClaimSetHelper.java b/src/main/java/io/mosip/mimoto/util/DcqlClaimSetHelper.java new file mode 100644 index 000000000..9ac923a3e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/DcqlClaimSetHelper.java @@ -0,0 +1,245 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.openID4VP.dcql.query.ClaimValue; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Shared DCQL {@code claim_sets} logic for credential matching and presentation submission. + * A {@code claim_sets} entry is satisfied when every claim id in that set matches the credential. + * The query is satisfied when any claim set matches (OR). + */ +public final class DcqlClaimSetHelper { + + /** Simple JSON keys use dot notation; keys with dots/hyphens need bracket notation (e.g. {@code org.iso.18013.5.1}). */ + private static final Pattern SIMPLE_PATH_SEGMENT = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$"); + + private DcqlClaimSetHelper() { + } + + /** + * Builds a Jayway JSONPath from DCQL path segments, quoting segments that are not simple identifiers. + */ + public static String buildJsonPath(List pathSegments) { + if (pathSegments == null || pathSegments.isEmpty()) { + return "$"; + } + StringBuilder jsonPath = new StringBuilder("$"); + appendPathSegments(jsonPath, pathSegments); + return jsonPath.toString(); + } + + /** + * Builds a credential claim path (without {@code $.}) for SD-JWT lookup and missing-claim reporting. + */ + public static String buildClaimPath(List pathSegments) { + if (pathSegments == null || pathSegments.isEmpty()) { + return ""; + } + StringBuilder claimPath = new StringBuilder(pathSegments.get(0).toString()); + if (pathSegments.size() > 1) { + appendPathSegments(claimPath, pathSegments.subList(1, pathSegments.size())); + } + return claimPath.toString(); + } + + private static void appendPathSegments(StringBuilder path, List pathSegments) { + for (Object segment : pathSegments) { + String value = segment.toString(); + if (SIMPLE_PATH_SEGMENT.matcher(value).matches()) { + path.append('.').append(value); + } else { + path.append("['").append(escapePathSegment(value)).append("']"); + } + } + } + + private static String escapePathSegment(String segment) { + return segment.replace("\\", "\\\\").replace("'", "\\'"); + } + + public static boolean hasClaimSets(CredentialQuery credentialQuery) { + return credentialQuery.getClaimSets() != null && !credentialQuery.getClaimSets().isEmpty(); + } + + public static boolean matchesClaims(CredentialQuery credentialQuery, VCCredentialResponse vc, + BiPredicate claimMatcher) { + boolean hasClaimSets = hasClaimSets(credentialQuery); + List claims = credentialQuery.getClaims(); + boolean hasClaims = claims != null && !claims.isEmpty(); + + if (!hasClaims) { + return !hasClaimSets; + } + if (hasClaimSets) { + return credentialQuery.getClaimSets().stream() + .anyMatch(claimIds -> satisfiesClaimSet(credentialQuery, vc, claimIds, claimMatcher)); + } + return claims.stream() + .allMatch(claimQuery -> claimMatcher.test(vc, claimQuery)); + } + + public static boolean satisfiesClaimSet(CredentialQuery credentialQuery, VCCredentialResponse vc, + List claimIds, + BiPredicate claimMatcher) { + if (claimIds == null || claimIds.isEmpty()) { + return false; + } + Map claimsById = indexClaimsById(credentialQuery); + for (String claimId : claimIds) { + ClaimsQuery claimQuery = claimsById.get(claimId); + if (claimQuery == null || !claimMatcher.test(vc, claimQuery)) { + return false; + } + } + return true; + } + + public static boolean isValidClaimSetSelection(List> claimSets, List selectedClaimIds) { + if (selectedClaimIds == null || selectedClaimIds.isEmpty() || claimSets == null) { + return false; + } + Set selected = toUniqueClaimIdSet(selectedClaimIds); + if (selected == null) { + return false; + } + return claimSets.stream() + .map(DcqlClaimSetHelper::toUniqueClaimIdSet) + .filter(Objects::nonNull) + .anyMatch(selected::equals); + } + + /** + * Resolves claim ids for submission. + *

+ * Iterates {@code claim_sets} in order and returns the first set whose every claim path + * has an actual SD disclosure on the credential (checked via {@code hasDisclosure}). + * Returns an empty list when no claim set has any disclosure — this means all claims in + * every set are public payload claims and no SD filtering is needed. + *

+ */ + public static List resolveClaimIdsForSubmission(CredentialQuery credentialQuery, + List selectedClaimIds, + Predicate hasDisclosure) { + if (!hasClaimSets(credentialQuery)) { + return Collections.emptyList(); + } + if (selectedClaimIds != null && !selectedClaimIds.isEmpty()) { + return normalizeClaimIds(selectedClaimIds); + } + for (List claimSet : credentialQuery.getClaimSets()) { + List normalized = normalizeClaimIds(claimSet); + List paths = resolveClaimPaths(credentialQuery, normalized); + if (paths.size() != normalized.size()) { + continue; + } + if (!paths.isEmpty() && paths.stream().allMatch(hasDisclosure)) { + return normalized; + } + } + // No claim set resolved to an SD disclosure — all claims are public; no filtering needed. + return Collections.emptyList(); + } + + /** + * Maps DCQL claim ids to credential claim paths (without {@code $.} prefix). + */ + public static List resolveClaimPaths(CredentialQuery credentialQuery, List claimIds) { + if (claimIds == null || claimIds.isEmpty()) { + return Collections.emptyList(); + } + Map claimsById = indexClaimsById(credentialQuery); + List paths = new ArrayList<>(); + for (String claimId : claimIds) { + ClaimsQuery claimQuery = claimsById.get(claimId); + if (claimQuery == null || claimQuery.getPath() == null || claimQuery.getPath().isEmpty()) { + return Collections.emptyList(); + } + paths.add(buildClaimPath(claimQuery.getPath())); + } + return paths; + } + + public static List resolveJsonPaths(CredentialQuery credentialQuery, List claimIds) { + if (claimIds == null || claimIds.isEmpty()) { + return Collections.emptyList(); + } + Map claimsById = indexClaimsById(credentialQuery); + List paths = new ArrayList<>(); + for (String claimId : claimIds) { + ClaimsQuery claimQuery = claimsById.get(claimId); + if (claimQuery == null || claimQuery.getPath() == null || claimQuery.getPath().isEmpty()) { + return Collections.emptyList(); + } + paths.add(buildJsonPath(claimQuery.getPath())); + } + return paths; + } + + private static Map indexClaimsById(CredentialQuery credentialQuery) { + if (credentialQuery.getClaims() == null) { + return Collections.emptyMap(); + } + Map claimsById = new LinkedHashMap<>(); + for (ClaimsQuery claimQuery : credentialQuery.getClaims()) { + if (claimQuery.getId() != null) { + claimsById.putIfAbsent(claimQuery.getId(), claimQuery); + } + } + return claimsById; + } + + private static List normalizeClaimIds(List claimIds) { + return claimIds.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(id -> !id.isEmpty()) + .collect(Collectors.toList()); + } + + /** + * Normalizes claim ids and returns a set when the input has no duplicates; otherwise {@code null}. + */ + private static Set toUniqueClaimIdSet(List claimIds) { + List normalized = normalizeClaimIds(claimIds); + Set unique = new LinkedHashSet<>(normalized); + return unique.size() == normalized.size() ? unique : null; + } + + public static boolean dcqlValueMatches(Object actual, ClaimValue expected) { + if (expected instanceof ClaimValue.StringValue sv) { + return actual != null && sv.getValue().equals(actual.toString()); + } + if (expected instanceof ClaimValue.LongValue lv) { + if (actual instanceof Number number) { + return lv.getValue() == number.longValue(); + } + return false; + } + if (expected instanceof ClaimValue.BoolValue bv) { + if (actual instanceof Boolean bool) { + return bv.getValue() == bool; + } + return false; + } + return false; + } + + public static Set pathsForClaimIds(CredentialQuery credentialQuery, List claimIds) { + return new LinkedHashSet<>(resolveJsonPaths(credentialQuery, claimIds)); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/DcqlCredentialSetHelper.java b/src/main/java/io/mosip/mimoto/util/DcqlCredentialSetHelper.java new file mode 100644 index 000000000..a3b0aeda4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/DcqlCredentialSetHelper.java @@ -0,0 +1,34 @@ +package io.mosip.mimoto.util; + +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.CredentialSetQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Shared DCQL {@code credential_sets} resolution for matching and submission validation. + */ +public final class DcqlCredentialSetHelper { + + private DcqlCredentialSetHelper() { + } + + /** + * Returns explicit {@code credential_sets} from the authorization request, or synthesises + * one required set per credential query when absent (OpenID4VP / inji-openid4vp default). + */ + public static List resolveEffectiveCredentialSets(DCQLQuery dcqlQuery) { + if (dcqlQuery.getCredentialSets() != null) { + return dcqlQuery.getCredentialSets(); + } + return dcqlQuery.getCredentials().stream() + .map(DcqlCredentialSetHelper::syntheticSetForQuery) + .collect(Collectors.toList()); + } + + private static CredentialSetQuery syntheticSetForQuery(CredentialQuery query) { + return new CredentialSetQuery(List.of(List.of(query.getId())), true); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/DcqlMatchingHelper.java b/src/main/java/io/mosip/mimoto/util/DcqlMatchingHelper.java new file mode 100644 index 000000000..9b9ecb2ac --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/DcqlMatchingHelper.java @@ -0,0 +1,179 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.openID4VP.constants.FormatType; +import io.mosip.openID4VP.dcql.evaluator.ClaimFailure; +import io.mosip.openID4VP.dcql.evaluator.QueryMatchResult; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.wallet.Credential; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Maps mimoto wallet credentials for inji-openid4vp {@link io.mosip.openID4VP.helper.DCQLHelper} + * evaluation and derives UI missing-claim hints from library match results. + */ +public final class DcqlMatchingHelper { + + private DcqlMatchingHelper() { + } + + public static DCQLQuery normalizeDcqlQuery(DCQLQuery dcqlQuery) { + List credentials = dcqlQuery.getCredentials().stream() + .map(DcqlMatchingHelper::normalizeCredentialQuery) + .collect(Collectors.toList()); + return new DCQLQuery(credentials, dcqlQuery.getCredentialSets()); + } + + public static List constructCredentialWithCredentialFormat( + List decryptedCredentials, + ObjectMapper objectMapper) { + List credentialWithCredentialFormat = new ArrayList<>(); + for (DecryptedCredentialDTO dto : decryptedCredentials) { + Credential mapped = toLibraryCredential(dto, objectMapper); + if (mapped == null) { + continue; + } + credentialWithCredentialFormat.add(mapped); + FormatType alternateFormat = alternateSdJwtFormat(mapped.getFormat()); + if (alternateFormat != null) { + credentialWithCredentialFormat.add(new Credential(alternateFormat, mapped.getData(), mapped.getCredentialId())); + } + } + return credentialWithCredentialFormat; + } + + public static Credential toLibraryCredential(DecryptedCredentialDTO dto, ObjectMapper objectMapper) { + VCCredentialResponse vc = dto.getCredential(); + if (vc == null) { + return null; + } + String format = vc.getFormat(); + Object credentialData = vc.getCredential(); + if (format == null || credentialData == null) { + return null; + } + + if (CredentialFormat.isSdJwt(format)) { + String sdJwt = asSdJwtString(credentialData, objectMapper); + if (sdJwt == null) { + return null; + } + FormatType formatType = mapToFormatType(format); + if (formatType == null) { + return null; + } + return new Credential(formatType, sdJwt, dto.getId()); + } + + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(format)) { + Map credentialMap = asCredentialMap(credentialData, objectMapper); + if (credentialMap == null) { + return null; + } + return new Credential(FormatType.LDP_VC, credentialMap, dto.getId()); + } + + return null; + } + + public static Set resolveMissingClaims(CredentialQuery credentialQuery, QueryMatchResult queryMatch) { + if (queryMatch != null && queryMatch.getFailedClaims() != null && !queryMatch.getFailedClaims().isEmpty()) { + return queryMatch.getFailedClaims().stream() + .map(ClaimFailure::getClaim) + .filter(Objects::nonNull) + .map(claim -> DcqlClaimSetHelper.buildClaimPath(claim.getPath())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + return extractMissingClaimsFromQuery(credentialQuery); + } + + private static Set extractMissingClaimsFromQuery(CredentialQuery credentialQuery) { + if (credentialQuery.getClaims() == null) { + return Set.of(); + } + if (DcqlClaimSetHelper.hasClaimSets(credentialQuery)) { + return credentialQuery.getClaimSets().stream() + .flatMap(claimIds -> DcqlClaimSetHelper.pathsForClaimIds(credentialQuery, claimIds).stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + return credentialQuery.getClaims().stream() + .filter(cq -> cq.getPath() != null && !cq.getPath().isEmpty()) + .map(cq -> DcqlClaimSetHelper.buildClaimPath(cq.getPath())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static CredentialQuery normalizeCredentialQuery(CredentialQuery query) { + Map meta = query.getMeta(); + if (meta != null) { + return query; + } + return new CredentialQuery( + query.getId(), + query.getFormat(), + query.getMultiple(), + Map.of(), + query.getRequireCryptographicHolderBinding(), + query.getClaims(), + query.getClaimSets()); + } + + private static FormatType alternateSdJwtFormat(FormatType format) { + if (format == FormatType.VC_SD_JWT) { + return FormatType.DC_SD_JWT; + } + if (format == FormatType.DC_SD_JWT) { + return FormatType.VC_SD_JWT; + } + return null; + } + + private static String asSdJwtString(Object credentialData, ObjectMapper objectMapper) { + if (credentialData instanceof String sdJwt) { + return sdJwt.isBlank() ? null : sdJwt; + } + return null; + } + + private static FormatType mapToFormatType(String format) { + if (CredentialFormat.LDP_VC.getFormat().equalsIgnoreCase(format)) { + return FormatType.LDP_VC; + } + if (CredentialFormat.VC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + return FormatType.VC_SD_JWT; + } + if (CredentialFormat.DC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + return FormatType.DC_SD_JWT; + } + return null; + } + + @SuppressWarnings("unchecked") + private static Map asCredentialMap(Object credentialData, ObjectMapper objectMapper) { + if (credentialData instanceof Map rawMap) { + Map credentialMap = new LinkedHashMap<>(); + for (Map.Entry entry : rawMap.entrySet()) { + if (entry.getKey() instanceof String key) { + credentialMap.put(key, entry.getValue()); + } + } + return credentialMap; + } + if (credentialData instanceof VCCredentialProperties properties) { + return objectMapper.convertValue(properties, Map.class); + } + return null; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/CryptoUtil.java b/src/main/java/io/mosip/mimoto/util/DerivedKeyCryptoUtil.java similarity index 88% rename from src/main/java/io/mosip/mimoto/util/CryptoUtil.java rename to src/main/java/io/mosip/mimoto/util/DerivedKeyCryptoUtil.java index e4cee3eb4..90feb218d 100644 --- a/src/main/java/io/mosip/mimoto/util/CryptoUtil.java +++ b/src/main/java/io/mosip/mimoto/util/DerivedKeyCryptoUtil.java @@ -27,7 +27,7 @@ import io.mosip.mimoto.exception.PlatformErrorMessages; @Component -public class CryptoUtil { +public class DerivedKeyCryptoUtil { private static final int GCM_NONCE_LENGTH = 12; @@ -35,14 +35,20 @@ public class CryptoUtil { private static final String AES_KEY_TYPE = "AES"; - @Value("${mosip.kernel.crypto.hash-symmetric-key-length:256}") - private int symmetricKeyLength; + private final int symmetricKeyLength; - @Value("${mosip.kernel.crypto.hash-iteration:100000}") - private int iterations; + private final int iterations; - @Value("${mosip.kernel.crypto.hash-algorithm-name:PBKDF2WithHmacSHA512}") - private String passwordAlgorithm; + private final String passwordAlgorithm; + + public DerivedKeyCryptoUtil( + @Value("${mosip.kernel.crypto.hash-symmetric-key-length:256}") int symmetricKeyLength, + @Value("${mosip.kernel.crypto.hash-iteration:100000}") int iterations, + @Value("${mosip.kernel.crypto.hash-algorithm-name:PBKDF2WithHmacSHA512}") String passwordAlgorithm) { + this.symmetricKeyLength = symmetricKeyLength; + this.iterations = iterations; + this.passwordAlgorithm = passwordAlgorithm; + } public CryptoWithPinResponseDto decryptWithPin(CryptoWithPinRequestDto requestDto) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, @@ -71,7 +77,7 @@ private byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] iv, byte[] aa } Cipher cipher = null; try { - cipher = Cipher.getInstance("AES/GCM/PKCS5Padding"); + cipher = Cipher.getInstance("AES/GCM/NoPadding"); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new CryptoManagerException(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); @@ -89,7 +95,7 @@ private byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] iv, byte[] aa private static byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] aad) { byte[] output = null; try { - Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding"); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); byte[] randomIV = Arrays.copyOfRange(data, data.length - cipher.getBlockSize(), data.length); SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, randomIV); diff --git a/src/main/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidator.java b/src/main/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidator.java new file mode 100644 index 000000000..a675e3f2c --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidator.java @@ -0,0 +1,56 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.CredentialDefinitionResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import org.apache.commons.lang.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.Set; + +@Component +public class Draft13CredentialIssuerWellknownResponseValidator { + + public static final String MSO_MDOC = "mso_mdoc"; + public static final String LDP_VC = "ldp_vc"; + + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + Set> violations = validator.validate(response); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder("Validation failed:"); + for (ConstraintViolation violation : violations) { + sb.append("\n").append(violation.getPropertyPath()).append(": ").append(violation.getMessage()); + } + throw new InvalidWellknownResponseException(sb.toString()); + } + + for (CredentialsSupportedResponse supportedCredentialConfiguration : response.getCredentialConfigurationsSupported().values()) { + if (MSO_MDOC.equals(supportedCredentialConfiguration.getFormat())) { + if (StringUtils.isBlank(supportedCredentialConfiguration.getDoctype())) { + throw new InvalidWellknownResponseException("Mandatory field 'doctype' missing"); + } + if (CollectionUtils.isEmpty(supportedCredentialConfiguration.getClaims())) { + throw new InvalidWellknownResponseException("Mandatory field 'claims' missing"); + } + } + + if (LDP_VC.equals(supportedCredentialConfiguration.getFormat())) { + if (supportedCredentialConfiguration.getCredentialDefinition() == null) { + throw new InvalidWellknownResponseException("credentialDefinition: must not be null"); + } + Set> credentialDefinitionViolations = validator.validate(supportedCredentialConfiguration.getCredentialDefinition()); + if (!credentialDefinitionViolations.isEmpty()) { + StringBuilder sb = new StringBuilder("Validation failed:"); + for (ConstraintViolation violation : credentialDefinitionViolations) { + sb.append("\n").append(violation.getPropertyPath()).append(": ").append(violation.getMessage()); + } + throw new InvalidWellknownResponseException(sb.toString()); + } + } + } + } +} diff --git a/src/main/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilder.java b/src/main/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilder.java new file mode 100644 index 000000000..b91ea707d --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilder.java @@ -0,0 +1,45 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.Draft13VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.VCCredentialDefinition; +import io.mosip.mimoto.dto.mimoto.VCCredentialRequestProof; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class Draft13CredentialRequestBuilder { + + public Draft13VCCredentialRequest buildCredentialRequest(String format, VCCredentialRequestProof proof, CredentialsSupportedResponse credentialsSupportedResponse) { + if (CredentialFormat.LDP_VC.getFormat().equals(format)) + return buildLdpVcCredentialRequest(proof, credentialsSupportedResponse); + else if (CredentialFormat.VC_SD_JWT.getFormat().equals(format) || CredentialFormat.DC_SD_JWT.getFormat().equals(format)) + return buildSdJwtCredentialRequest(format, proof, credentialsSupportedResponse); + else + throw new IllegalArgumentException("Unsupported credential format: " + format); + } + + private Draft13VCCredentialRequest buildLdpVcCredentialRequest(VCCredentialRequestProof proof, CredentialsSupportedResponse credentialsSupportedResponse) { + List credentialContext = credentialsSupportedResponse.getCredentialDefinition().getContext(); + if (credentialContext == null || credentialContext.isEmpty()) { + credentialContext = List.of("https://www.w3.org/2018/credentials/v1"); + } + + return Draft13VCCredentialRequest.builder().format(CredentialFormat.LDP_VC.getFormat()) + .proof(proof) + .credentialDefinition(VCCredentialDefinition.builder() + .type(credentialsSupportedResponse.getCredentialDefinition().getType()) + .context(credentialContext).build()) + .build(); + } + + private Draft13VCCredentialRequest buildSdJwtCredentialRequest(String format, VCCredentialRequestProof proof, CredentialsSupportedResponse credentialsSupportedResponse) { + return Draft13VCCredentialRequest + .builder() + .format(format) + .vct(credentialsSupportedResponse.getVct()).proof(proof) + .build(); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/GlobalExceptionHandler.java b/src/main/java/io/mosip/mimoto/util/GlobalExceptionHandler.java new file mode 100644 index 000000000..604ccb114 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/GlobalExceptionHandler.java @@ -0,0 +1,147 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.dto.resident.CredentialRequestResponseDTO; +import io.mosip.mimoto.exception.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindingResult; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import java.util.Collections; +import java.util.stream.Collectors; + +@ControllerAdvice +@ResponseBody +@Slf4j +public class GlobalExceptionHandler { + @ExceptionHandler(Throwable.class) // Catch-all for unexpected exceptions + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorDTO handleGenericException(Exception ex) { + log.error("An unexpected error occurred: ", ex); + return new ErrorDTO(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage()); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public ErrorDTO handleUnsupportedRequestException(HttpRequestMethodNotSupportedException ex) { + log.error("HTTP method not supported: {}", ex.getMessage(), ex); + return new ErrorDTO(ErrorConstants.INVALID_REQUEST.getErrorCode(), ex.getMessage()); + } + + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorDTO handleNoResourceFoundException(NoResourceFoundException ex) { + log.error("An unexpected error occurred: ", ex); + return new ErrorDTO(ErrorConstants.INVALID_REQUEST.getErrorCode(), ex.getMessage()); + } + + @ExceptionHandler(WalletLockedException.class) + @ResponseStatus(HttpStatus.LOCKED) + public ErrorDTO handleWalletUnlockEligibilityException(WalletLockedException ex) { + log.error("Wallet is not eligible to be unlocked due to the error: {}", ex.getMessage(), ex); + return new ErrorDTO(ex.getErrorCode(), ex.getErrorText()); + } + + @ExceptionHandler( value = InvalidInputException.class) + public ResponseEntity handleInvalidInput(InvalidInputException ex) { + CredentialRequestResponseDTO credentialRequestResponseDTO = new CredentialRequestResponseDTO(); + ErrorDTO errors = new ErrorDTO(ex.getErrorCode(), ex.getMessage()); + credentialRequestResponseDTO.setVersion("1.0"); + credentialRequestResponseDTO.setErrors(Collections.singletonList(errors)); + return new ResponseEntity<>(credentialRequestResponseDTO, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataAccessResourceFailureException.class) + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + public ErrorDTO handleDatabaseException(Exception ex) { + log.error("Database error occurred:", ex); + return new ErrorDTO(ErrorConstants.DATABASE_CONNECTION_EXCEPTION.getErrorCode(), ErrorConstants.DATABASE_CONNECTION_EXCEPTION.getErrorMessage()); + } + + @ExceptionHandler(ExternalServiceUnavailableException.class) + @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + public ErrorDTO handleExternalServiceUnavailable(ExternalServiceUnavailableException ex) { + log.error("Connection to external service failed: ", ex); + return new ErrorDTO(ex.getErrorCode(), ex.getMessage()); + } + + @ExceptionHandler(UnauthorizedAccessException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ErrorDTO handleUnAuthorizedAccess(UnauthorizedAccessException ex) { + log.error("UnAuthorized access detected: ", ex); + return new ErrorDTO(ex.getErrorCode(), ex.getErrorText()); + } + + @ExceptionHandler(InvalidRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDTO handleInvalidRequest(InvalidRequestException ex) { + log.error("Invalid request parameters: ", ex); + return new ErrorDTO(ex.getErrorCode(), ex.getErrorText()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDTO handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) { + log.error("Invalid request body: {}", ex.getMessage(), ex); + String errorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + String errorMessage; + + if (ex.getCause() instanceof com.fasterxml.jackson.core.JsonParseException) { + errorMessage = "Invalid JSON format in request body"; + } else if (ex.getCause() instanceof com.fasterxml.jackson.databind.JsonMappingException) { + errorMessage = "Request body does not match expected structure"; + } else if (ex.getMessage().contains("Required request body is missing")) { + errorMessage = "Required request body is missing"; + } else if (ex.getMessage().contains("Content type")) { + errorMessage = "Unsupported content type in request"; + } else { + errorMessage = "Unable to process request body"; + } + + return new ErrorDTO(errorCode, errorMessage); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorDTO handleNoHandlerFoundException(NoHandlerFoundException ex) { + log.error("No handler found for request: {} {}", ex.getHttpMethod(), ex.getRequestURL()); + return new ErrorDTO(ErrorConstants.RESOURCE_NOT_FOUND.getErrorCode(), ErrorConstants.RESOURCE_NOT_FOUND.getErrorMessage()); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorDTO handleHandlerMethodValidationException(HandlerMethodValidationException ex) { + log.error("Validation error in handler method: {}", ex.getMessage()); + String errorMessage = ex.getAllValidationResults() + .stream() + .flatMap(r -> r.getResolvableErrors().stream()) + .map(MessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + return new ErrorDTO(ErrorConstants.INVALID_REQUEST.getErrorCode(), errorMessage); + } + + // Handler method to return 400 for validation failures + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + BindingResult br = ex.getBindingResult(); + String message = br.getFieldErrors().stream() + .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) + .collect(Collectors.joining("; ")); + log.warn("Validation failed: {}", message); + ErrorDTO err = new ErrorDTO("invalid_request", message); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/IssuerConfigUtil.java b/src/main/java/io/mosip/mimoto/util/IssuerConfigUtil.java new file mode 100644 index 000000000..17c7611a7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/IssuerConfigUtil.java @@ -0,0 +1,118 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.parser.WellknownParserFactory; +import io.mosip.mimoto.util.parser.WellknownResponseParser; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import java.io.IOException; +import java.util.Set; + + +@Service +@Slf4j +public class IssuerConfigUtil { + + private final RestApiClient restApiClient; + private final VCSpecVersionDetector versionDetector; + private final WellknownParserFactory parserFactory; + private final ObjectMapper objectMapper; + private final Validator validator; + + public IssuerConfigUtil(RestApiClient restApiClient, VCSpecVersionDetector versionDetector, WellknownParserFactory parserFactory, ObjectMapper objectMapper, Validator validator) { + this.restApiClient = restApiClient; + this.versionDetector = versionDetector; + this.parserFactory = parserFactory; + this.objectMapper = objectMapper; + this.validator = validator; + } + + public static String camelToTitleCase(String input) { + if (input == null || input.isEmpty()) return input; + + // Insert space before: + // 1. Lowercase followed by uppercase (e.g., pinFor -> pin For) + // 2. Acronym followed by normal word (e.g., PRACondition -> PRA Condition) + String result = input.replaceAll("(?<=[a-z])(?=[A-Z])|(?<=[A-Z]{2,})(?=[A-Z][a-z])", " "); + + // Capitalize first letter of each word, preserve acronyms + String[] words = result.split(" "); + StringBuilder finalResult = new StringBuilder(); + + for (String word : words) { + if (word.matches("[A-Z]{2,}")) { + // Acronym (all uppercase, length >= 2) + finalResult.append(word); + } else { + // Capitalize normally + finalResult.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)); + } + finalResult.append(" "); + } + + return finalResult.toString().trim(); + } + + @Cacheable(value = "issuerWellknown", key = "#p0") + public CredentialIssuerWellKnownResponse getIssuerWellknown(String credentialIssuerHost) throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + String wellknownEndpoint = credentialIssuerHost + "/.well-known/openid-credential-issuer"; + String wellknownResponse = restApiClient.getApi(wellknownEndpoint, String.class); + if (wellknownResponse == null) { + throw new ApiNotAccessibleException(); + } + + JsonNode jsonNode = objectMapper.readTree(wellknownResponse); + VCSpecificationVersion version = versionDetector.detectVersion(jsonNode); + + WellknownResponseParser parser = parserFactory.getParser(version); + CredentialIssuerWellKnownResponse parsedResponse = parser.parse(wellknownResponse); + parser.validate(parsedResponse, validator); + + return parsedResponse; + } + + @Cacheable(value = "authServerWellknown", key = "#p0") + public AuthorizationServerWellKnownResponse getAuthServerWellknown(String authorizationServerHostUrl) throws AuthorizationServerWellknownResponseException { + try { + String wellknownEndpoint = authorizationServerHostUrl + "/.well-known/oauth-authorization-server"; + log.debug("fetching Authorization Server Wellknown by calling :: " + wellknownEndpoint); + String wellknownResponse = restApiClient.getApi(wellknownEndpoint, String.class); + if (wellknownResponse == null) { + throw new Exception("well-known api is not accessible"); + } + AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse = objectMapper.readValue(wellknownResponse, AuthorizationServerWellKnownResponse.class); + validate(authorizationServerWellKnownResponse); + + return authorizationServerWellKnownResponse; + } catch (Exception e) { + log.error("Exception occurred while fetching Authorization Server wellknown ", e); + throw new AuthorizationServerWellknownResponseException(e.toString()); + } + } + + public void validate(AuthorizationServerWellKnownResponse response) throws Exception { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + Set> violations = validator.validate(response); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder("Validation failed:"); + for (ConstraintViolation violation : violations) { + sb.append("\n").append(violation.getPropertyPath()).append(": ").append(violation.getMessage()); + } + + throw new Exception(sb.toString()); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/util/JoseUtil.java b/src/main/java/io/mosip/mimoto/util/JoseUtil.java new file mode 100644 index 000000000..39e4ab045 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/JoseUtil.java @@ -0,0 +1,200 @@ +package io.mosip.mimoto.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.X509CertUtils; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.mosip.kernel.core.util.CryptoUtil; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.mimoto.JwkDto; +import io.mosip.mimoto.dto.mimoto.WalletBindingInternalResponseDto; +import io.mosip.mimoto.dto.mimoto.WalletBindingResponseDto; +import io.mosip.mimoto.exception.IssuerOnboardingException; +import lombok.extern.slf4j.Slf4j; +import org.jose4j.jws.JsonWebSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.*; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Component +public class JoseUtil { + private static final String BEGIN_KEY = "-----BEGIN PUBLIC KEY-----"; + private static final String END_KEY = "-----END PUBLIC KEY-----"; + private static final String ALG_RS256 = "RS256"; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private P12KeyStoreManager p12KeyStoreManager; + + public static JwkDto getJwkFromPublicKey(String publicKeyString) throws NoSuchAlgorithmException, InvalidKeySpecException { + + RSAPublicKeySpec keySpec = publicKeyString.startsWith(BEGIN_KEY) ? getKeySpecForPublicKey(publicKeyString) : getKeySpecForRSAKey(publicKeyString); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + // Convert to JWK format + JWK jwk = new RSAKey.Builder((RSAPublicKey)publicKey) + .keyUse(KeyUse.SIGNATURE) + .keyID(UUID.randomUUID().toString()) + .build(); + + JwkDto jwkDto = new JwkDto(jwk.getKeyType().toString(), ((RSAKey) jwk).getPublicExponent().toString(), ((RSAKey) jwk).getModulus().toString()); + return jwkDto; + + } + + private static RSAPublicKeySpec getKeySpecForRSAKey(String publicKeyString) { + String key = publicKeyString.replace("\n", "") + .replace("-----BEGIN RSA PUBLIC KEY-----", "") + .replace("-----END RSA PUBLIC KEY-----", ""); + + org.bouncycastle.asn1.pkcs.RSAPublicKey pkcs1PublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey.getInstance(CryptoUtil.decodePlainBase64(key)); + BigInteger modulus = pkcs1PublicKey.getModulus(); + BigInteger publicExponent = pkcs1PublicKey.getPublicExponent(); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent); + return keySpec; + + } + + private static RSAPublicKeySpec getKeySpecForPublicKey(String publicKeyString) throws NoSuchAlgorithmException, InvalidKeySpecException { + String key = publicKeyString.replace("\n", "") + .replace(BEGIN_KEY, "") + .replace(END_KEY, ""); + X509EncodedKeySpec spec = + new X509EncodedKeySpec(CryptoUtil.decodePlainBase64(key)); + KeyFactory kf = KeyFactory.getInstance("RSA"); + RSAPublicKey generatePublic = (RSAPublicKey) kf.generatePublic(spec); + BigInteger modulus = generatePublic.getModulus(); + BigInteger exponent = generatePublic.getPublicExponent(); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); + return keySpec; + } + + + public ResponseWrapper addThumbprintAndKeyId(ResponseWrapper responseDto) { + + log.debug("Inside addThumbprintAndKeyId "); + + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + responseWrapper.setErrors(responseDto.getErrors()); + + try { + if (responseDto.getResponse() != null) { + WalletBindingInternalResponseDto resp = mapper.readValue( + mapper.writeValueAsString(responseDto.getResponse()), WalletBindingInternalResponseDto.class); + // generate thumbprint and key id + String certificate = resp.getCertificate(); + X509Certificate x509Certificate = X509CertUtils.parse(certificate); + JsonWebSignature jwSign = new JsonWebSignature(); + jwSign.setKeyIdHeaderValue(x509Certificate.getSerialNumber().toString(10)); + jwSign.setX509CertSha256ThumbprintHeaderValue(x509Certificate); + + // map fields with thumbprint and key id + WalletBindingResponseDto response = new WalletBindingResponseDto(); + response.setCertificate(resp.getCertificate()); + response.setEncryptedWalletBindingId(resp.getWalletUserId()); + response.setExpireDateTime(resp.getExpireDateTime()); + response.setKeyId(jwSign.getKeyIdHeaderValue()); + response.setThumbprint(jwSign.getX509CertSha256ThumbprintHeaderValue()); + + responseWrapper.setResponse(response); + } + } catch (Exception e) { + log.error("Exception occured while setting thumbprint ", e); + } + + return responseWrapper; + } + + public String getJWT(String clientId, String keyStorePath, String fileName, String alias, String cyptoPassword, String audience) throws IssuerOnboardingException, IOException { + + Map header = new HashMap<>(); + header.put("alg", ALG_RS256); + + String keyStorePathWithFileName = keyStorePath + fileName; + Date issuedAt = Date.from(Instant.now()); + Date expiresAt = Date.from(Instant.now().plusMillis(120000)); + RSAPrivateKey privateKey = null; + try { + KeyStore.PrivateKeyEntry privateKeyEntry = p12KeyStoreManager.loadP12(keyStorePathWithFileName, alias, cyptoPassword); + if(privateKeyEntry == null){ + throw new IssuerOnboardingException("Private Key Entry is Missing for the alias " + alias); + } + privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); + } catch (IOException e) { + log.error("Exception happened while loading the p12 file for invoking token call."); + throw e; + } + return JWT.create() + .withHeader(header) + .withIssuer(clientId) + .withSubject(clientId) + .withAudience(audience) + .withExpiresAt(expiresAt) + .withIssuedAt(issuedAt) + .withClaim("jti", UUID.randomUUID().toString()) + .sign(Algorithm.RSA256(null, privateKey)); + } + + public String generateJwt(String audience, String clientId, String cNonce) throws Exception { + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + RSAKey jwk = new RSAKey.Builder((RSAPublicKey) kp.getPublic()) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS256) + .build(); + + Map header = new HashMap<>(); + header.put("alg", ALG_RS256); + header.put("typ", "openid4vci-proof+jwt"); + header.put("jwk", jwk.getRequiredParams()); + + Date issuedAt = Date.from(Instant.now()); + Date expiresAt = Date.from(Instant.now().plusMillis(120000000)); + RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); + + Map payload = new HashMap<>(); + payload.put("sub", clientId); + payload.put("aud", audience); + payload.put("nonce", cNonce); + payload.put("iss", clientId); + payload.put("exp", expiresAt.toInstant().getEpochSecond()); + payload.put("iat", issuedAt.toInstant().getEpochSecond()); + payload.put("jti", UUID.randomUUID().toString()); + + return Jwts.builder() + .setHeader(header) + .setIssuer(clientId) + .setSubject(clientId) + .setAudience(audience) + .setExpiration(expiresAt) + .setIssuedAt(issuedAt) + .setClaims(payload) + .signWith(SignatureAlgorithm.RS256, privateKey) + .compact(); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/JsonUtil.java b/src/main/java/io/mosip/mimoto/util/JsonUtil.java index 3be13fbe1..e69de29bb 100644 --- a/src/main/java/io/mosip/mimoto/util/JsonUtil.java +++ b/src/main/java/io/mosip/mimoto/util/JsonUtil.java @@ -1,280 +0,0 @@ -package io.mosip.mimoto.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.LinkedHashMap; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; - -import io.mosip.mimoto.dto.JsonValue; -import io.mosip.mimoto.exception.FieldNotFoundException; -import io.mosip.mimoto.exception.InstantanceCreationException; -import io.mosip.mimoto.exception.PlatformErrorMessages; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; - -/** - * This class provides JSON utilites. - * - * @author Pranav Kumar - * @since 0.0.1 - */ -public class JsonUtil { - - /** The Constant LANGUAGE. */ - private static final String LANGUAGE = "language"; - - /** The Constant VALUE. */ - private static final String VALUE = "value"; - - private static ObjectMapper objectMapper = new ObjectMapper(); - /** - * Instantiates a new json util. - */ - private JsonUtil() { - - } - - /** - * This method converts InputStream to JavaObject. - * - * @param stream - * The stream that needs to be converted - * @param clazz - * The class to which conversion is required - * @return The converted Java object - * @throws UnsupportedEncodingException - * the unsupported encoding exception - * @throws JsonSyntaxException - * the json syntax exception - * @throws JsonIOException - * the json IO exception - */ - public static Object inputStreamtoJavaObject(InputStream stream, Class clazz) - throws UnsupportedEncodingException { - JsonParser jsonParser = new JsonParser(); - Gson gson = new Gson(); - JsonObject jsonObject = (JsonObject) jsonParser.parse(new InputStreamReader(stream, "UTF-8")); - try { - return gson.fromJson(jsonObject, clazz); - } catch (Exception e) { - throw new UnsupportedEncodingException(PlatformErrorMessages.MIMOTO_CMB_UNSUPPORTED_ENCODING.getMessage()); - } - } - - /** - * This method returns the Json Object as value from identity.json - * object(JSONObject). jsonObject -> then identity demographic json object key - * -> demographic json label name EX:- demographicIdentity : { "identity" : { - * "fullName" : [ { "language" : "eng", "value" : "Taleev Aalam" }, { - * "language": "ara", "value" : "Taleev Aalam" } ] } - * - * method call :- getJSONObject(demographicIdentity,identity) - * - * @param jsonObject - * the json object - * @param key - * the key - * @return the JSON object - */ - public static JSONObject getJSONObject(JSONObject jsonObject, Object key) { - if(jsonObject == null) - return null; - LinkedHashMap identity = (LinkedHashMap) jsonObject.get(key); - return identity != null ? new JSONObject(identity) : null; - } - - /** - * This method returns JSONArray from JSONObject. argument 'jsonObject' -> - * demographic identity json as JSONObject. argument key -> label name of - * demographic identity json. Ex:- "identity" : { "fullName" : [ { "language" : - * "eng", "value" : "Taleev Aalam" }, { "language" : "ara", "value" : "Taleev - * Aalam" } ] } - * - * @param jsonObject - * the json object - * @param key - * the key - * @return the JSON array - */ - public static JSONArray getJSONArray(JSONObject jsonObject, Object key) { - ArrayList value = (ArrayList) jsonObject.get(key); - if (value == null) - return null; - JSONArray jsonArray = new JSONArray(); - jsonArray.addAll(value); - - return jsonArray; - - } - - public static String writeValueAsString(Object obj) throws IOException { - return objectMapper.writeValueAsString(obj); - } - - /** - * Object mapper read value. This method maps the jsonString to particular type - * - * @param the generic type - * @param jsonString the json string - * @param clazz the clazz - * @return the t - * @throws JsonParseException the json parse exception - * @throws JsonMappingException the json mapping exception - * @throws IOException Signals that an I/O exception has occurred. - */ - @SuppressWarnings("unchecked") - public static T readValue(String jsonString, Class clazz) throws IOException { - return (T) objectMapper.readValue(jsonString, clazz); - } - - /** - * Gets the JSON value. - * - * @param - * the generic type - * @param jsonObject - * the json object - * @param key - * the key - * @return the JSON value - */ - public static T getJSONValue(JSONObject jsonObject, String key) { - if(jsonObject == null) - return null; - T value = (T) jsonObject.get(key); - return value; - } - - /** - * Iterates the JSONArray and returns JSONObject for given index. - * - * @param jsonObject - * the json object - * @param key - * the key - * @return the JSON object - */ - public static JSONObject getJSONObjectFromArray(JSONArray jsonObject, int key) { - Object object = jsonObject.get(key); - if(object instanceof LinkedHashMap) { - LinkedHashMap identity = (LinkedHashMap) jsonObject.get(key); - return identity != null ? new JSONObject(identity) : null; - }else { - return (JSONObject)object; - } - } - /** - * Object mapper read value. This method maps the jsonString to particular type - * - * @param - * the generic type - * @param jsonString - * the json string - * @param clazz - * the clazz - * @return the t - * @throws JsonParseException - * the json parse exception - * @throws JsonMappingException - * the json mapping exception - * @throws IOException - * Signals that an I/O exception has occurred. - */ - @SuppressWarnings("unchecked") - public static T objectMapperReadValue(String jsonString, Class clazz) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - return (T) objectMapper.readValue(jsonString, clazz); - } - - /** - * Gets the json values. Returns JsonValue[] java array for the particular key - * in demographic json. - * - * @param identityKey - * the identity key - * @return the json values - */ - public static JsonValue[] getJsonValues(JSONObject demographicIdentity, Object identityKey) { - JSONArray demographicJsonNode = null; - - if (demographicIdentity != null) - demographicJsonNode = JsonUtil.getJSONArray(demographicIdentity, identityKey); - return (demographicJsonNode != null) - ? (JsonValue[]) mapJsonNodeToJavaObject(JsonValue.class, demographicJsonNode) - : null; - - } - - /** - * Map json node to java object. - * - * @param - * the generic type - * @param genericType - * the generic type - * @param demographicJsonNode - * the demographic json node - * @return the t[] - */ - @SuppressWarnings("unchecked") - public static T[] mapJsonNodeToJavaObject(Class genericType, JSONArray demographicJsonNode) { - String language; - String value; - T[] javaObject = (T[]) Array.newInstance(genericType, demographicJsonNode.size()); - try { - for (int i = 0; i < demographicJsonNode.size(); i++) { - - T jsonNodeElement = (T) genericType.newInstance(); - - JSONObject objects = JsonUtil.getJSONObjectFromArray(demographicJsonNode, i); - if (objects != null) { - language = (String) objects.get(LANGUAGE); - value = (String) objects.get(VALUE); - - Field languageField = jsonNodeElement.getClass().getDeclaredField(LANGUAGE); - languageField.setAccessible(true); - languageField.set(jsonNodeElement, language); - - Field valueField = jsonNodeElement.getClass().getDeclaredField(VALUE); - valueField.setAccessible(true); - valueField.set(jsonNodeElement, value); - - javaObject[i] = jsonNodeElement; - } - } - } catch (InstantiationException | IllegalAccessException e) { - - throw new InstantanceCreationException(PlatformErrorMessages.MIMOTO_SYS_INSTANTIATION_EXCEPTION.getMessage(), - e); - - } catch (NoSuchFieldException | SecurityException e) { - - throw new FieldNotFoundException(PlatformErrorMessages.MIMOTO_SYS_NO_SUCH_FIELD_EXCEPTION.getMessage(), e); - - } - - return javaObject; - - } - - - public static String objectMapperObjectToJson(Object obj) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writeValueAsString(obj); - } - -} diff --git a/src/main/java/io/mosip/mimoto/util/JwtUtils.java b/src/main/java/io/mosip/mimoto/util/JwtUtils.java new file mode 100644 index 000000000..ae7fd5650 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/JwtUtils.java @@ -0,0 +1,60 @@ +package io.mosip.mimoto.util; + +import com.authlete.sd.SDJWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; + +@Slf4j +public class JwtUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static Map extractJwtPayloadFromSdJwt(String sdJwtString) { + if (StringUtils.isBlank(sdJwtString)) { + return null; + } else { + String[] elements = sdJwtString.split("~", -1); + return parseJwtPayload(elements[0]); + } + } + + public static Map parseJwtPayload(String jwt) { + try { + String[] parts = jwt.split("\\."); + if (parts.length < 3) { + log.error("Invalid JWT format for payload parsing: "); + return Collections.emptyMap(); + } + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + return objectMapper.readValue(payloadJson, Map.class); + } catch (Exception e) { + log.error("Error parsing JWT payload", e); + return Collections.emptyMap(); + } + } + + /** + * Extracts the JWT header as a Map. + */ + public static Map parseJwtHeader(String jwt) { + try { + String[] parts = jwt.split("\\."); + if (parts.length < 3) { + log.warn("Invalid JWT format for header parsing: "); + return Collections.emptyMap(); + } + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + return objectMapper.readValue(headerJson, Map.class); + } catch (Exception e) { + log.error("Error parsing JWT header", e); + return Collections.emptyMap(); + } + } +} + diff --git a/src/main/java/io/mosip/mimoto/util/LocaleUtils.java b/src/main/java/io/mosip/mimoto/util/LocaleUtils.java new file mode 100644 index 000000000..11383f53a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/LocaleUtils.java @@ -0,0 +1,78 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.DisplayDTO; +import io.mosip.mimoto.dto.mimoto.CredentialDisplayResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +public class LocaleUtils { + // Method to convert the input locale strings into 3-letter codes and compare + public static boolean matchesLocale(String localeString1, String localeString2) { + if(localeString1 == null || localeString2 == null){ + return false; + } + + String locale1Iso3Language = Locale.forLanguageTag(localeString1).getISO3Language(); // 3-letter code for locale1 + String locale2Iso3Language = Locale.forLanguageTag(localeString2).getISO3Language(); // 3-letter code for locale2 + + return locale1Iso3Language.equals(locale2Iso3Language); + } + + public static String resolveLocaleWithFallback(Map credentialSubject, String locale) { + String selectedLocale = null; + + // Iterate through the credentials to find a display object that supports the requested locale. If none is found, use the first available display object. + for (String VCProperty : credentialSubject.keySet()) { + List displayList = credentialSubject.get(VCProperty).getDisplay(); + // If no matching locale is found for any of the fields, use the locale of the first display object + if(selectedLocale == null && displayList!=null && !displayList.isEmpty()){ + selectedLocale = displayList.get(0).getLocale(); + } + // Check if any display object supports the requested locale + Optional filteredResponse = displayList.stream() + .filter(obj -> matchesLocale(obj.getLocale(), locale)) + .findFirst(); + if (filteredResponse.isPresent()) { + selectedLocale = filteredResponse.get().getLocale(); + break; // Break once a matching record is found + } + } + + return selectedLocale; + } + + + public static DisplayDTO getIssuerDisplayDTOBasedOnLocale(List displayDTOList, String locale) { + if (displayDTOList == null || displayDTOList.isEmpty()) { + return null; + } + + return displayDTOList.stream() + .filter(obj -> matchesLocale(obj.getLanguage(), locale)) + .findFirst() + .orElse(displayDTOList.get(0)); // Return first display object if no match is found for the received locale + } + + public static CredentialSupportedDisplayResponse getCredentialDisplayDTOBasedOnLocale(List displayDTOList, String locale) { + if (displayDTOList == null || displayDTOList.isEmpty()) { + return null; + } + + return displayDTOList.stream() + .filter(obj -> matchesLocale(obj.getLocale(), locale)) + .findFirst() + .orElse(displayDTOList.get(0)); // Return first display object if no match is found for the received locale + } + + public static boolean isValidLanguageCode(String code) { + return (code != null && + code.length() == 2 && + code.matches("[a-zA-Z]{2}") && + java.util.Arrays.asList(Locale.getISOLanguages()).contains(code.toLowerCase())); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/CryptoCoreUtil.java b/src/main/java/io/mosip/mimoto/util/P12KeyStoreManager.java similarity index 87% rename from src/main/java/io/mosip/mimoto/util/CryptoCoreUtil.java rename to src/main/java/io/mosip/mimoto/util/P12KeyStoreManager.java index c6e981fa3..60531586e 100644 --- a/src/main/java/io/mosip/mimoto/util/CryptoCoreUtil.java +++ b/src/main/java/io/mosip/mimoto/util/P12KeyStoreManager.java @@ -33,6 +33,7 @@ import javax.crypto.spec.PSource.PSpecified; import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.digests.SHA256Digest; @@ -47,15 +48,15 @@ import io.mosip.mimoto.exception.CryptoManagerException; import io.mosip.mimoto.exception.PlatformErrorMessages; +@Slf4j @Component -public class CryptoCoreUtil { - static Logger logger = LoggerUtil.getLogger(CryptoCoreUtil.class); +public class P12KeyStoreManager { private final static String RSA_ECB_OAEP_PADDING = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"; private final static int THUMBPRINT_LENGTH = 32; private final static int NONCE = 12; - private final static int AADSize = 32; + private final static int AAD_SIZE = 32; public static final byte[] VERSION_RSA_2048 = "VER_R2".getBytes(); @Value("${mosip.partner.prependThumbprint:true}") @@ -78,12 +79,12 @@ public String decrypt(String data) throws Exception { byte[] data1 = decryptData(dataBytes, privateKeyEntry); decryptedData = new String(data1); }catch (Exception e){ - logger.error( "Not able to decrypt the data", e); + log.error( "Not able to decrypt the data", e); } return decryptedData; } - public PrivateKeyEntry loadP12() throws IOException { + public PrivateKeyEntry loadP12(String fileName, String alias, String cyptoPassword) throws IOException { PrivateKeyEntry privateKeyEntry = null; InputStream keystoreResourceStream = null; try { @@ -98,7 +99,7 @@ public PrivateKeyEntry loadP12() throws IOException { ProtectionParameter password = new PasswordProtection(cyptoPassword.toCharArray()); privateKeyEntry = (PrivateKeyEntry) mosipKeyStore.getEntry(alias, password); } catch (UnrecoverableEntryException | CertificateException | KeyStoreException | IOException|NoSuchAlgorithmException e) { - logger.error( "Not able to decrypt the data", e); + log.error( "Not able to decrypt the data", e); } finally { if (keystoreResourceStream != null) { keystoreResourceStream.close(); @@ -106,6 +107,9 @@ public PrivateKeyEntry loadP12() throws IOException { } return privateKeyEntry; } + public PrivateKeyEntry loadP12() throws IOException { + return loadP12(fileName, alias, cyptoPassword); + } public byte[] decryptData(byte[] requestData, PrivateKeyEntry privateKey) throws Exception { String keySplitter = "#KEY_SPLITTER#"; @@ -124,9 +128,9 @@ public byte[] decryptData(byte[] requestData, PrivateKeyEntry privateKey) throws if (Arrays.equals(headerBytes, VERSION_RSA_2048)) { encryptedSymmetricKey = Arrays.copyOfRange(encryptedKey, THUMBPRINT_LENGTH + VERSION_RSA_2048.length, encryptedKey.length); - byte[] aad = Arrays.copyOfRange(encryptedData, 0, AADSize); + byte[] aad = Arrays.copyOfRange(encryptedData, 0, AAD_SIZE); byte[] nonce = Arrays.copyOfRange(aad, 0, NONCE); - byte[] encData = Arrays.copyOfRange(encryptedData, AADSize, encryptedData.length); + byte[] encData = Arrays.copyOfRange(encryptedData, AAD_SIZE, encryptedData.length); decryptedSymmetricKey = asymmetricDecrypt(privateKey.getPrivateKey(), ((RSAPrivateKey) privateKey.getPrivateKey()).getModulus(), encryptedSymmetricKey); symmetricKey = new SecretKeySpec(decryptedSymmetricKey, 0, decryptedSymmetricKey.length, "AES"); @@ -144,7 +148,7 @@ public byte[] decryptData(byte[] requestData, PrivateKeyEntry privateKey) throws return symmetricDecrypt(symmetricKey, encryptedData, null); } } catch (Exception e) { - logger.error( "Not able to decrypt the data {}", e); + log.error( "Not able to decrypt the data {}", e); } return null; } @@ -174,7 +178,7 @@ private static int getSplitterIndex(byte[] encryptedData, int keyDemiliterIndex, } /** - * + * * @param privateKey * @param keyModulus * @param data @@ -198,7 +202,7 @@ private static byte[] asymmetricDecrypt(PrivateKey privateKey, BigInteger keyMod cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams); return cipher.doFinal(data); } catch (java.security.NoSuchAlgorithmException e) { - logger.error("Not able to decrypt the data {}", e); + log.error("Not able to decrypt the data {}", e); throw new NoSuchAlgorithmException(e); } catch (NoSuchPaddingException e) { throw new NoSuchPaddingException(e.getMessage()); @@ -209,27 +213,10 @@ private static byte[] asymmetricDecrypt(PrivateKey privateKey, BigInteger keyMod } } - /** - * - * @param paddedPlainText - * @return - * @throws InvalidCipherTextException - * @throws InvalidKeyException - */ - private static byte[] unpadOAEPPadding(byte[] paddedPlainText, BigInteger keyModulus) - throws InvalidCipherTextException { - - OAEPEncoding encode = new OAEPEncoding(new RSAEngine(), new SHA256Digest()); - BigInteger exponent = new BigInteger("1"); - RSAKeyParameters keyParams = new RSAKeyParameters(false, keyModulus, exponent); - encode.init(false, keyParams); - return encode.processBlock(paddedPlainText, 0, paddedPlainText.length); - } - private static byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] aad) { byte[] output = null; try { - Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding"); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); byte[] randomIV = Arrays.copyOfRange(data, data.length - cipher.getBlockSize(), data.length); SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, randomIV); @@ -247,12 +234,10 @@ private static byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] aad) { public byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] nonce, byte[] aad) throws InvalidAlgorithmParameterException { - // Objects.requireNonNull(key, null); - // CryptoUtils.verifyData(data); byte[] output = null; Cipher cipher; try { - cipher = Cipher.getInstance("AES/GCM/PKCS5Padding"); + cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key.getEncoded(), "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, nonce); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); @@ -261,34 +246,34 @@ public byte[] symmetricDecrypt(SecretKey key, byte[] data, byte[] nonce, byte[] } output = cipher.doFinal(data, 0, data.length); } catch (InvalidAlgorithmParameterException e) { - logger.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), + log.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); throw new InvalidParamSpecException(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); } catch (IllegalBlockSizeException e) { - logger.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), + log.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); } catch (BadPaddingException e) { - logger.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), + log.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); } catch (NoSuchAlgorithmException e) { - logger.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), + log.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); } catch (NoSuchPaddingException e) { - logger.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), + log.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); } catch (InvalidKeyException e) { - logger.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), + log.error(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getCode(), PlatformErrorMessages.MIMOTO_INVALID_KEY_EXCEPTION.getMessage(), e); @@ -300,7 +285,7 @@ public static byte[] getCertificateThumbprint(Certificate cert) { try { return DigestUtils.sha256(cert.getEncoded()); } catch (java.security.cert.CertificateEncodingException e) { - logger.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), + log.error(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); throw new CryptoManagerException(PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getCode(), PlatformErrorMessages.CERTIFICATE_THUMBPRINT_ERROR.getMessage(), e); diff --git a/src/main/java/io/mosip/mimoto/util/RequestValidator.java b/src/main/java/io/mosip/mimoto/util/RequestValidator.java new file mode 100644 index 000000000..3886a4f1b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/RequestValidator.java @@ -0,0 +1,58 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.JsonNode; +import io.mosip.mimoto.dto.mimoto.CredentialDownloadRequestDTO; +import io.mosip.mimoto.exception.InvalidInputException; +import io.mosip.mimoto.exception.PlatformErrorMessages; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + + +@Component +@Slf4j +public class RequestValidator { + + @Value("${mosip.notificationtype:EMAIL|PHONE}") + private String notificationType; + public void validateInputRequest(BindingResult result){ + if(result.hasErrors()){ + FieldError fieldError = result.getFieldError(); + if(fieldError != null) { + String errorMessage = fieldError.getField() + " " + fieldError.getDefaultMessage(); + throw new InvalidInputException(errorMessage); + } + } + } + + public void validateNotificationChannel(List otpChannelList) { + List incorrectNotificationChannel; + log.info("\n Notification Types from application-default.properties in mosip-config - > {}", notificationType); + incorrectNotificationChannel = otpChannelList + .stream() + .filter(otpChannel -> !notificationType.contains(otpChannel)) + .collect(Collectors.toList()); + if (incorrectNotificationChannel.size() > 0) { + log.error("Invalid Input is received in otpChannels {}", String.join(",", incorrectNotificationChannel)); + throw new InvalidInputException(PlatformErrorMessages.MIMOTO_PGS_INVALID_INPUT_PARAMETER.getMessage() + " - " + "otpChannels"); + } + } + + public void validateCredentialDownloadRequest(CredentialDownloadRequestDTO requestDTO, JsonNode requestedCredentialJSON) throws IOException, NoSuchFieldException { + String response = "response"; + if(requestedCredentialJSON.has(response) && requestedCredentialJSON.get(response).has("id")) { + String requestedIndividualId = requestedCredentialJSON.get(response).get("id").asText(); + if (!requestDTO.getIndividualId().equals(requestedIndividualId)) { + throw new InvalidInputException(PlatformErrorMessages.MIMOTO_PGS_INVALID_INPUT_PARAMETER.getMessage() + " - " + "individualId"); + } + } + } +} diff --git a/src/main/java/io/mosip/mimoto/util/RestApiClient.java b/src/main/java/io/mosip/mimoto/util/RestApiClient.java index ec224637e..00662539b 100644 --- a/src/main/java/io/mosip/mimoto/util/RestApiClient.java +++ b/src/main/java/io/mosip/mimoto/util/RestApiClient.java @@ -1,11 +1,20 @@ package io.mosip.mimoto.util; -import java.io.IOException; -import java.net.URI; -import java.util.Iterator; - +import com.google.gson.Gson; +import io.mosip.mimoto.core.http.RequestWrapper; +import io.mosip.mimoto.dto.SecretKeyRequest; +import io.mosip.mimoto.exception.TokenGenerationFailedException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -14,29 +23,61 @@ import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import io.mosip.kernel.core.logger.spi.Logger; +import java.io.IOException; +import java.net.URI; +import java.util.Iterator; +import java.util.Objects; /** * The Class RestApiClient. * * @author Rishabh Keshari */ +@Slf4j @Component public class RestApiClient { - /** The logger. */ - private Logger logger = LoggerUtil.getLogger(RestApiClient.class); + public static final String TOKEN = "token"; + public static final String CONTENT_TYPE = "Content-Type"; - /** The builder. */ + private static final String AUTHORIZATION = "Authorization="; + /** + * The builder. + */ @Autowired @Qualifier("selfTokenRestTemplate") private RestTemplate restTemplate; @Autowired - Environment environment; + @Qualifier("plainRestTemplate") + private RestTemplate plainRestTemplate; + + @Value("${wallet.binding.partner.id}") + private String partnerId; + @Value("${wallet.binding.partner.api.key}") + private String partnerApiKey; + + @Value("${mosip.authmanager.client-token-endpoint}") + private String authBaseUrl; + + @Value("${mosip.iam.adapter.clientid}") + private String clientId; + + @Value("${mosip.iam.adapter.clientsecret}") + private String secret; + + @Value("${mosip.iam.adapter.appid}") + private String appId; + + @Value("${mosip.iam.adapter.disable-self-token-rest-template:false}") + private boolean disableSelfTokenRestTemplate; + + @Autowired + Environment environment; /** * HTTP GET API * @@ -49,19 +90,23 @@ public class RestApiClient { @SuppressWarnings("unchecked") public T getApi(URI uri, Class responseType) throws Exception { T result = null; + RestTemplate rt = restTemplate; + if (disableSelfTokenRestTemplate) { + rt = plainRestTemplate; + } try { - logger.info("RestApiClient::getApi()::entry uri: {}", uri); - result = (T) restTemplate.exchange(uri, HttpMethod.GET, setRequestHeader(null, null), responseType) + log.info("RestApiClient::getApi()::entry uri: {}", uri); + result = (T) rt.exchange(uri, HttpMethod.GET, setRequestHeader(null, null), responseType) .getBody(); } catch (Exception e) { - logger.error("RestApiClient::getApi()::error uri: {} {} {}", uri, e.getMessage(), e); + log.error("RestApiClient::getApi()::error uri: {} {} {}", uri, e.getMessage(), e); } return result; } /** * HTTP GET API - * + * * @param * @param url * @param responseType @@ -70,10 +115,15 @@ public T getApi(URI uri, Class responseType) throws Exception { @SuppressWarnings("unchecked") public T getApi(String url, Class responseType) { T result = null; + RestTemplate rt = restTemplate; + if (disableSelfTokenRestTemplate) { + rt = plainRestTemplate; + } try { - result = (T) restTemplate.getForObject(url, responseType); + log.info("RestApiClient::getApi()::entry uri: {}", url); + result = (T) rt.getForObject(url, responseType); } catch (Exception e) { - logger.error("RestApiClient::getApi()::error uri:{} {} {}", url, e.getMessage(), e); + log.error("RestApiClient::getApi()::error uri:{} {} {}", url, e.getMessage(), e); } return result; } @@ -92,15 +142,99 @@ public T getApi(String url, Class responseType) { @SuppressWarnings("unchecked") public T postApi(String uri, MediaType mediaType, Object requestType, Class responseClass) throws Exception { T result = null; + RestTemplate rt = restTemplate; + if (disableSelfTokenRestTemplate) { + rt = plainRestTemplate; + } try { - logger.info("RestApiClient::postApi()::entry uri: {}", uri); - result = (T) restTemplate.postForObject(uri, setRequestHeader(requestType, mediaType), responseClass); + log.info("RestApiClient::postApi()::entry uri: {}", uri); + result = (T) rt.postForObject(uri, setRequestHeader(requestType, mediaType), responseClass); } catch (Exception e) { - logger.error("RestApiClient::postApi()::error uri: {} {} {}", uri, e.getMessage(), e); + log.error("RestApiClient::postApi()::error uri: {} {} {}", uri, e.getMessage(), e); } return result; } + public T postApi(String uri, MediaType mediaType, Object requestType, Class responseClass, boolean useBearerToken) throws Exception { + T result = null; + try { + log.info("RestApiClient::postApi()::entry uri: {}", uri); + result = (T) plainRestTemplate.postForObject(uri, setRequestHeader(requestType, mediaType, useBearerToken), responseClass); + } catch (Exception e) { + log.error("RestApiClient::postApi()::error uri: {} {} {}", uri, e.getMessage(), e); + if (e instanceof HttpClientErrorException) { + HttpClientErrorException ex = (HttpClientErrorException)e; + if (ex.getStatusCode().value() == 401) { + // bearer token renew logic. Set token as empty so that it will auto-renew + System.setProperty(TOKEN, ""); + // try one more time to pass existing call + result = (T) plainRestTemplate.postForObject( + uri, setRequestHeader(requestType, mediaType, useBearerToken), responseClass); + } + } + } + return result; + } + + public T postApi(String uri, MediaType mediaType, Object requestType, Class responseClass, String bearerToken){ + T result = null; + try { + log.info("RestApiClient::postApi()::entry uri: {}", uri); + result = (T) plainRestTemplate.postForObject(uri, setRequestHeader(requestType, mediaType, bearerToken), responseClass); + } catch (Exception e) { + log.error("RestApiClient::postApi()::error uri: {} {} {}", uri, e.getMessage(), e); + } + return result; + } + + public T postApiWithErrorResponse(String uri, MediaType mediaType, Object requestType, Class responseClass, String bearerToken) { + try { + log.info("RestApiClient::postApiWithErrorResponse()::entry uri: {}", uri); + return plainRestTemplate.postForObject(uri, setRequestHeader(requestType, mediaType, bearerToken), responseClass); + } catch (HttpClientErrorException e) { + log.error("RestApiClient::postApiWithErrorResponse()::client error uri: {} status: {}", uri, e.getStatusCode()); +log.debug("RestApiClient::postApiWithErrorResponse()::client error response body: {}", e.getResponseBodyAsString()); + try { + return new com.fasterxml.jackson.databind.ObjectMapper().readValue(e.getResponseBodyAsString(), responseClass); + } catch (Exception ex) { + log.error("RestApiClient::postApiWithErrorResponse()::failed to parse error body as {}: {}", responseClass.getSimpleName(), ex.getMessage()); + return null; + } + } catch (Exception e) { + log.error("RestApiClient::postApiWithErrorResponse()::error uri: {} {}", uri, e.getMessage(), e); + return null; + } + } + + private HttpEntity setRequestHeader(Object requestType, MediaType mediaType, String bearerToken){ + MultiValueMap headers = new LinkedMultiValueMap(); + if (mediaType != null) { + headers.add(CONTENT_TYPE, mediaType.toString()); + } + headers.add("Authorization", "Bearer "+bearerToken); + if (requestType != null) { + try { + HttpEntity httpEntity = (HttpEntity) requestType; + HttpHeaders httpHeader = httpEntity.getHeaders(); + Iterator iterator = httpHeader.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (!(headers.containsKey(CONTENT_TYPE) && key.equals(CONTENT_TYPE))) + headers.add(key, Objects.requireNonNull(httpHeader.get(key)).get(0)); + } + + return new HttpEntity(httpEntity.getBody(), headers); + } catch (ClassCastException | NullPointerException e) { + return new HttpEntity(requestType, headers); + } + } else + return new HttpEntity(headers); + } + + private HttpEntity setRequestHeader(Object requestType, MediaType mediaType) throws IOException { + return setRequestHeader(requestType, mediaType, false); + } + /** * this method sets token to header of the request * @@ -110,11 +244,21 @@ public T postApi(String uri, MediaType mediaType, Object requestType, Class< * @throws IOException */ @SuppressWarnings("unchecked") - private HttpEntity setRequestHeader(Object requestType, MediaType mediaType) throws IOException { + private HttpEntity setRequestHeader(Object requestType, MediaType mediaType, boolean useBearerToken) throws IOException { MultiValueMap headers = new LinkedMultiValueMap(); if (mediaType != null) { - headers.add("Content-Type", mediaType.toString()); + headers.add(CONTENT_TYPE, mediaType.toString()); } + + if (useBearerToken) { + String bearerToken = System.getProperty(TOKEN); + if (StringUtils.isEmpty(bearerToken)) + bearerToken = getBearerToken(); + headers.add("Authorization", bearerToken); + headers.add("partner-id", partnerId); + headers.add("partner-api-key", partnerApiKey); + } + if (requestType != null) { try { HttpEntity httpEntity = (HttpEntity) requestType; @@ -122,8 +266,8 @@ private HttpEntity setRequestHeader(Object requestType, MediaType mediaT Iterator iterator = httpHeader.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); - if (!(headers.containsKey("Content-Type") && key.equals("Content-Type"))) - headers.add(key, httpHeader.get(key).get(0)); + if (!(headers.containsKey(CONTENT_TYPE) && key.equals(CONTENT_TYPE))) + headers.add(key, Objects.requireNonNull(httpHeader.get(key)).get(0)); } return new HttpEntity(httpEntity.getBody(), headers); } catch (ClassCastException | NullPointerException e) { @@ -132,4 +276,45 @@ private HttpEntity setRequestHeader(Object requestType, MediaType mediaT } else return new HttpEntity(headers); } + + private String getBearerToken() throws IOException { + + SecretKeyRequest request = new SecretKeyRequest(clientId, secret, appId); + RequestWrapper req = new RequestWrapper<>(); + req.setRequest(request); + + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost post = new HttpPost(authBaseUrl); + Gson gson = new Gson(); + + StringEntity postingString = new StringEntity(gson.toJson(req)); + post.setEntity(postingString); + post.setHeader("Content-type", "application/json"); + HttpResponse response = httpClient.execute(post); + Header[] cookie = response.getHeaders("Set-Cookie"); + if (cookie.length == 0) + throw new TokenGenerationFailedException(); + String token = response.getHeaders("Set-Cookie")[0].getValue(); + token = token.replace(AUTHORIZATION, ""); + token = "Bearer " + token.substring(0, token.indexOf(';')); + System.setProperty(TOKEN, token); + return token; + } + + public T getApiWithCustomHeaders(String url, Class responseType, HttpHeaders customHeaders) { + T result = null; + RestTemplate rt = restTemplate; + if (disableSelfTokenRestTemplate) { + rt = plainRestTemplate; + } + try { + log.info("RestApiClient::getApiWithCustomHeaders()::entry url: {}", url); + // Add custom headers to the request + HttpEntity requestEntity = new HttpEntity<>(customHeaders); + result = (T) rt.exchange(url, HttpMethod.GET, requestEntity, responseType).getBody(); + } catch (Exception e) { + log.error("RestApiClient::getApiWithCustomHeaders()::error url:{} {} {}", url, e.getMessage(), e); + } + return result; + } } diff --git a/src/main/java/io/mosip/mimoto/util/SelectedSdClaimsUtil.java b/src/main/java/io/mosip/mimoto/util/SelectedSdClaimsUtil.java new file mode 100644 index 000000000..50cbd293a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/SelectedSdClaimsUtil.java @@ -0,0 +1,40 @@ +package io.mosip.mimoto.util; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * Unions SD-JWT disclosure paths per credential ID when paths come from multiple sources + * (top-level {@code selectedSdClaims}, nested DCQL selections, or {@code claim_sets} resolution). + * Used by {@link io.mosip.mimoto.dto.SubmitPresentationRequestDTO#resolveEffectiveSelectedSdClaims()} + * and {@link io.mosip.mimoto.service.impl.WalletPresentationServiceImpl} during DCQL submit. + */ +public final class SelectedSdClaimsUtil { + + private SelectedSdClaimsUtil() { + } + + public static void mergeInto(Map> target, Map> source) { + if (source == null) { + return; + } + for (Map.Entry> entry : source.entrySet()) { + mergePaths(target, entry.getKey(), entry.getValue()); + } + } + + public static void mergePaths(Map> target, String credentialId, List paths) { + if (credentialId == null || paths == null || paths.isEmpty()) { + return; + } + target.merge(credentialId, new ArrayList<>(paths), SelectedSdClaimsUtil::unionPaths); + } + + private static List unionPaths(List existing, List incoming) { + LinkedHashSet union = new LinkedHashSet<>(existing); + union.addAll(incoming); + return new ArrayList<>(union); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/ServerUtil.java b/src/main/java/io/mosip/mimoto/util/ServerUtil.java index 7b60710be..af97f3def 100644 --- a/src/main/java/io/mosip/mimoto/util/ServerUtil.java +++ b/src/main/java/io/mosip/mimoto/util/ServerUtil.java @@ -1,29 +1,26 @@ package io.mosip.mimoto.util; +import lombok.extern.slf4j.Slf4j; + import java.net.InetAddress; import java.net.UnknownHostException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * This class provides Server IP and Name. * * @author Kiran Raj M1048860 */ +@Slf4j public class ServerUtil { /** The server instance. */ private static ServerUtil serverInstance = null; - /** The Constant LOGGER. */ - private static final Logger LOGGER = LoggerFactory.getLogger(ServerUtil.class); - /** The host not found. */ private String noHost = "HOST_NOT_FOUND"; /** - * + * * Instantiates a new server util. */ private ServerUtil() { @@ -57,7 +54,7 @@ public String getServerIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { - LOGGER.error(noHost, e.getMessage()); + log.error(noHost, e.getMessage()); return "UNKNOWN-HOST"; } @@ -73,7 +70,7 @@ public String getServerName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { - LOGGER.error(noHost, e.getMessage()); + log.error(noHost, e.getMessage()); return "UNKNOWN-HOST"; } } diff --git a/src/main/java/io/mosip/mimoto/util/SigningKeyUtil.java b/src/main/java/io/mosip/mimoto/util/SigningKeyUtil.java new file mode 100644 index 000000000..509bd181f --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/SigningKeyUtil.java @@ -0,0 +1,190 @@ +package io.mosip.mimoto.util; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jwt.*; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.KeyGenerationException; +import io.mosip.mimoto.util.factory.SigningAlgorithmHandler; +import io.mosip.mimoto.util.factory.SigningAlgorithmHandlerFactory; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Date; + +import io.mosip.mimoto.model.KeyMetadata; +import io.mosip.mimoto.model.ProofSigningKey; + +import java.time.Instant; +import java.util.Base64; +import java.util.UUID; + +import static io.mosip.mimoto.exception.ErrorConstants.ENCRYPTION_FAILED; + +/** + * Unified utility for key generation and JWT signing operations. + */ +@Slf4j +public class SigningKeyUtil { + + private static final int JWT_EXPIRATION_SECONDS = 18000; // 5 hours + private static final String OPENID4VCI_PROOF_JWT = "openid4vci-proof+jwt"; + + /** + * Generates a key pair for the given signing algorithm. + * + * @param algorithm The algorithm to use (e.g., "RS256", "ES256", "ES256K", "Ed25519"). + * @return KeyPair object. + * @throws NoSuchAlgorithmException + * @throws InvalidAlgorithmParameterException + * @throws NoSuchProviderException + */ + public static KeyPair generateKeyPair(SigningAlgorithm algorithm) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(algorithm); + return handler.generateKeyPair(); + } + + /** + * Reconstructs a KeyPair from stored byte arrays (from database). + * + * @param algorithm The signing algorithm + * @param publicKeyBytes Public key bytes + * @param privateKeyBytes Private key bytes + * @return Reconstructed KeyPair + * @throws NoSuchAlgorithmException If the algorithm is not supported + * @throws InvalidAlgorithmParameterException If algorithm parameters are invalid + * @throws NoSuchProviderException If the provider is not available + * @throws InvalidKeySpecException If the key bytes are invalid + */ + public static KeyPair generateKeyPair(SigningAlgorithm algorithm, byte[] publicKeyBytes, byte[] privateKeyBytes) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException, InvalidKeySpecException { + + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(algorithm); + KeyFactory keyFactory = handler.getKeyFactory(); + + PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); + + return new KeyPair(publicKey, privateKey); + } + + /** + * Generates a secret key for encryption (e.g., AES). + * This is separate from signing keys and remains algorithm-agnostic. + * + * @param algorithm The algorithm to use (e.g., "AES"). + * @param keySize The key size in bits. + * @return SecretKey object. + */ + public static SecretKey generateEncryptionKey(String algorithm, int keySize) { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm); + keyGenerator.init(keySize); + return keyGenerator.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new KeyGenerationException(ENCRYPTION_FAILED.getErrorCode(), ENCRYPTION_FAILED.getErrorMessage(), e); + } + } + + /** + * Generates a complete signed JWT token. + * + * @param signingAlgorithm The signing algorithm to use + * @param audience The JWT audience + * @param clientId The client ID (used as subject and issuer) + * @param cNonce The challenge nonce + * @param keyPair The key pair to use for signing + * @return Serialized signed JWT string + * @throws JOSEException If JWT generation fails + */ + public static String generateJwt(SigningAlgorithm signingAlgorithm, String audience, String clientId, String cNonce, KeyPair keyPair) throws JOSEException { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(signingAlgorithm); + + // Use handler to create JWK and signer + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + JWTClaimsSet claimsSet = createClaims(clientId, audience, cNonce); + JWSHeader header = new JWSHeader.Builder(signingAlgorithm.getJWSAlgorithm()).type(new JOSEObjectType(OPENID4VCI_PROOF_JWT)).jwk(jwk.toPublicJWK()).build(); + + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + signedJWT.sign(signer); + return signedJWT.serialize(); + } + + /** + * Creates a JWK (JSON Web Key) from a KeyPair. + * + * @param algorithm The signing algorithm + * @param keyPair The key pair + * @return JWK object + */ + public static JWK generateJwk(SigningAlgorithm algorithm, KeyPair keyPair) { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(algorithm); + return handler.createJWK(keyPair); + } + + /** + * Creates a JWSSigner from a JWK. + * + * @param algorithm The signing algorithm + * @param jwk The JSON Web Key + * @return JWSSigner object + * @throws JOSEException If signer creation fails + */ + public static JWSSigner createSigner(SigningAlgorithm algorithm, JWK jwk) throws JOSEException { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(algorithm); + return handler.createSigner(jwk); + } + + /** + * Creates a ProofSigningKey entity for the given algorithm. + * Generates a key pair and wraps it with metadata for database storage. + * + * @param algorithm The signing algorithm to use + * @return ProofSigningKey object ready for database persistence + * @throws KeyGenerationException If key generation fails + */ + public static ProofSigningKey createProofSigningKey(SigningAlgorithm algorithm) { + try { + KeyPair keyPair = generateKeyPair(algorithm); + + KeyMetadata keyMetadata = new KeyMetadata(); + keyMetadata.setAlgorithmName(algorithm.name()); + + ProofSigningKey proofSigningKey = new ProofSigningKey(); + proofSigningKey.setId(UUID.randomUUID().toString()); + proofSigningKey.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())); + proofSigningKey.setSecretKey(keyPair.getPrivate()); + proofSigningKey.setKeyMetadata(keyMetadata); + proofSigningKey.setCreatedAt(Instant.now()); + proofSigningKey.setUpdatedAt(Instant.now()); + + return proofSigningKey; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) { + log.error("Error while generating key", e); + throw new KeyGenerationException(ENCRYPTION_FAILED.getErrorCode(), "Failed to generate proof signing key for algorithm: " + algorithm, e); + } + } + + /** + * Creates JWT claims set with standard fields. + */ + private static JWTClaimsSet createClaims(String clientId, String audience, String cNonce) { + long nowSeconds = System.currentTimeMillis() / 1000; + Date issuedAt = new Date(nowSeconds * 1000); + Date expiresAt = new Date((nowSeconds + JWT_EXPIRATION_SECONDS) * 1000); + + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().subject(clientId).audience(audience).issuer(clientId).issueTime(issuedAt).expirationTime(expiresAt); + + if (cNonce != null) { + builder.claim("nonce", cNonce); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/SvgFixerUtil.java b/src/main/java/io/mosip/mimoto/util/SvgFixerUtil.java new file mode 100644 index 000000000..88b8a6a78 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/SvgFixerUtil.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.util; + +import org.springframework.stereotype.Component; + +@Component +public class SvgFixerUtil { + + // Adds missing offset attribute to elements in SVG content + public String addMissingOffsetToStopElements(String svgContent) { + if (svgContent == null) return null; + + // Add offset="0" if missing in elements + return svgContent.replaceAll( + "]*offset=)", + " params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8); + + for (NameValuePair param : params) { + if (parameterName.equals(param.getName())) { + return param.getValue(); + } + } + return null; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/Utilities.java b/src/main/java/io/mosip/mimoto/util/Utilities.java index ecaddeeed..80b939a1f 100644 --- a/src/main/java/io/mosip/mimoto/util/Utilities.java +++ b/src/main/java/io/mosip/mimoto/util/Utilities.java @@ -1,40 +1,43 @@ package io.mosip.mimoto.util; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.mimoto.constant.ApiName; -import io.mosip.mimoto.constant.LoggerFileConstant; -import io.mosip.mimoto.constant.MappingJsonConstants; +import com.google.common.collect.Lists; import io.mosip.mimoto.core.http.ResponseWrapper; import io.mosip.mimoto.dto.ErrorDTO; -import io.mosip.mimoto.dto.IdResponseDTO; -import io.mosip.mimoto.exception.ApisResourceAccessException; +import io.mosip.mimoto.exception.ErrorConstants; import io.mosip.mimoto.exception.ExceptionUtils; -import io.mosip.mimoto.exception.IdRepoAppException; -import io.mosip.mimoto.exception.PlatformErrorMessages; -import io.mosip.mimoto.service.RestClientService; import io.mosip.mimoto.service.impl.CredentialShareServiceImpl; +import jakarta.annotation.PostConstruct; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static io.mosip.mimoto.constant.LoggerFileConstant.DELIMITER; @Component +@Slf4j @Data public class Utilities { private ClassLoader classLoader = Utilities.class.getClassLoader(); @@ -53,65 +56,86 @@ public class Utilities { // Sample VC websub event private String SAMPLE_EVENT = "%s_EVENT.json"; - private Logger logger = LoggerUtil.getLogger(Utilities.class); - - /** The Constant FILE_SEPARATOR. */ - public static final String FILE_SEPARATOR = "\\"; - - @Autowired - private ObjectMapper objMapper; - @Autowired private RestApiClient restApiClient; - /** The rest client service. */ - @Autowired - private RestClientService restClientService; - - /** The config server file storage URL. */ + /** + * The config server file storage URL. + */ @Value("${config.server.file.storage.uri}") private String configServerFileStorageURL; - /** The get reg processor identity json. */ - @Value("${registration.processor.identityjson}") - private String getRegProcessorIdentityJson; + /** + * The get reg issuers config json. + */ + @Value("${mosip.openid.issuers}") + private String issuersConfigPath; + + @Value("${mosip.openid.verifiers}") + private String trustedVerifiersPath; + + @Value("${mosip.openid.htmlTemplate}") + private String credentialTemplatePath; + + private String issuersConfigJsonString = null; - /** The get reg processor demographic identity. */ - @Value("${registration.processor.demographic.identity}") - private String getRegProcessorDemographicIdentity; + private String trustedVerifiersJsonString = null; - /** The registration processor abis json. */ - @Value("${registration.processor.print.textfile}") - private String registrationProcessorPrintTextFile; + private String credentialTemplateHtmlString = null; - private String mappingJsonString = null; + @Value("${spring.profiles.active}") + private String activeProfile; - private String identityMappingJsonString = null; + @PostConstruct + public void setUp() throws IOException { + if(activeProfile.contains("local")) { + Resource resource = new ClassPathResource(issuersConfigPath); + Resource trustedVerifiersResource = new ClassPathResource(trustedVerifiersPath); + Resource credentialTemplateResource = new ClassPathResource("templates/"+ credentialTemplatePath); - private String printTextFileJsonString = null; + trustedVerifiersJsonString = (Files.readString(trustedVerifiersResource.getFile().toPath())); + issuersConfigJsonString = (Files.readString(resource.getFile().toPath())); + credentialTemplateHtmlString = (Files.readString(credentialTemplateResource.getFile().toPath())); + } + } + public static String encodeToString(BufferedImage image, String type) { + String imageString = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); - public JSONObject getTemplate() throws JsonParseException, JsonMappingException, IOException { + try { + ImageIO.write(image, type, bos); + byte[] imageBytes = bos.toByteArray(); + Base64.Encoder encoder = Base64.getEncoder(); + imageString = encoder.encodeToString(imageBytes); + bos.close(); + } catch (Exception e) { + log.error(ExceptionUtils.getStackTrace(e)); + } + return imageString; + } + + public JSONObject getTemplate() throws IOException { return objectMapper.readValue(classLoader.getResourceAsStream(defaultTemplate), JSONObject.class); } - public JsonNode getEvent(String requestId, String individualId) throws JsonParseException, JsonMappingException, IOException { - Path resourcePath = Path.of(dataPath, String.format(CredentialShareServiceImpl.EVENT_JSON_FILE_NAME, requestId)); + public JsonNode getVC(String requestId) throws IOException { + Path resourcePath = Path.of(dataPath, String.format(CredentialShareServiceImpl.VC_JSON_FILE_NAME, requestId)); if (Files.exists(resourcePath)) { return objectMapper.readValue(resourcePath.toFile(), JsonNode.class); } return null; } - public JsonNode getVC(String requestId) throws JsonParseException, JsonMappingException, IOException { - Path resourcePath = Path.of(dataPath, String.format(CredentialShareServiceImpl.VC_JSON_FILE_NAME, requestId)); + public JsonNode getRequestVC(String requestId) throws IOException { + Path resourcePath = Path.of(dataPath, String.format(CredentialShareServiceImpl.VC_REQUEST_FILE_NAME, requestId)); if (Files.exists(resourcePath)) { return objectMapper.readValue(resourcePath.toFile(), JsonNode.class); } return null; } - public JsonNode getDecryptedVC(String requestId) throws JsonParseException, JsonMappingException, IOException { + public JsonNode getDecryptedVC(String requestId) throws IOException { Path resourcePath = Path.of(dataPath, String.format(CredentialShareServiceImpl.CARD_JSON_FILE_NAME, requestId)); if (Files.exists(resourcePath)) { return objectMapper.readValue(resourcePath.toFile(), JsonNode.class); @@ -130,7 +154,7 @@ public void removeCacheData(String requestId) throws IOException { try { Files.delete(filePath); } catch (Exception e) { - logger.error("Cannot delete file: " + filePath, e); + log.error("Cannot delete file: " + filePath, e); } } } @@ -139,166 +163,129 @@ public void removeCacheData(String requestId) throws IOException { /** * Gets the json. * - * @param configServerFileStorageURL - * the config server file storage URL - * @param uri - * the uri + * @param configString the stringified config + * @param resourcePath path of the resource * @return the json */ - public String getJson(String configServerFileStorageURL, String uri) { - String json = null; + public String getJson(String configString, String resourcePath) { + String json = configString; try { - json = restApiClient.getApi(URI.create(configServerFileStorageURL + uri), String.class); + if(StringUtils.isEmpty(json)){ + json = restApiClient.getApi(URI.create(configServerFileStorageURL + resourcePath), String.class); + } } catch (Exception e) { - logger.error(ExceptionUtils.getStackTrace(e)); + log.error(ExceptionUtils.getStackTrace(e)); } return json; } - public String getPrintTextFileJson(String configServerFileStorageURL, String uri) { - printTextFileJsonString = (printTextFileJsonString != null && !printTextFileJsonString.isEmpty()) ? - printTextFileJsonString : getJson(configServerFileStorageURL, uri); - return printTextFileJsonString; + public String getIssuersConfigJsonValue() { + return getJson(issuersConfigJsonString, issuersConfigPath); } - - public String getIdentityMappingJson(String configServerFileStorageURL, String uri) { - identityMappingJsonString = (identityMappingJsonString != null && !identityMappingJsonString.isEmpty()) ? - identityMappingJsonString : getJson(configServerFileStorageURL, uri); - return identityMappingJsonString; + public String getTrustedVerifiersJsonValue() { + return getJson(trustedVerifiersJsonString, trustedVerifiersPath); } - - /** - * retrieving identity json ffrom id repo by UIN. - * - * @param uin the uin - * @return the JSON object - * @throws ApisResourceAccessException the apis resource access exception - * @throws IdRepoAppException the id repo app exception - * @throws IOException Signals that an I/O exception has - * occurred. - */ - @SuppressWarnings("unchecked") - public JSONObject retrieveIdrepoJson(String uin) - throws ApisResourceAccessException, IdRepoAppException, IOException { - - if (uin != null) { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveIdrepoJson()::entry"); - List pathSegments = new ArrayList<>(); - pathSegments.add(uin); - ResponseWrapper idResponseDto; - - idResponseDto = (ResponseWrapper) restClientService.getApi(ApiName.IDREPOGETIDBYUIN, pathSegments, "", "", - ResponseWrapper.class); - if (idResponseDto == null) { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveIdrepoJson()::exit idResponseDto is null"); - return null; + public String getCredentialSupportedTemplateString(String issuerId, String credentialConfigurationId) { + String templateFileName = String.format("%s-%s-template.html", issuerId, credentialConfigurationId); + if(activeProfile.contains("local")) { + Path basePath = Paths.get("templates").toAbsolutePath().normalize(); + Path resolvedPath = basePath.resolve(templateFileName).normalize(); + + if (!resolvedPath.startsWith(basePath)) { + throw new SecurityException("Attempted path traversal attack: " + resolvedPath); } - if (!idResponseDto.getErrors().isEmpty()) { - List error = idResponseDto.getErrors(); - logger.error(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveIdrepoJson():: error with error message " + error.get(0).getErrorMessage()); - throw new IdRepoAppException(error.get(0).getErrorMessage()); - } - String response = objMapper.writeValueAsString(idResponseDto.getResponse().getIdentity()); - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveIdrepoJson():: IDREPOGETIDBYUIN GET service call ended Successfully"); + + Resource credentialTemplateResource = new ClassPathResource(resolvedPath.toString()); try { - return (JSONObject) new JSONParser().parse(response); - } catch (org.json.simple.parser.ParseException e) { - logger.error(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - ExceptionUtils.getStackTrace(e)); - throw new IdRepoAppException("Error while parsing string to JSONObject", e); + return Files.readString(credentialTemplateResource.getFile().toPath()); + } catch (IOException e) { + log.error(ExceptionUtils.getStackTrace(e)); } + return credentialTemplateHtmlString; + } + String specificCredentialPDFTemplate = getJson("", templateFileName); + return !StringUtils.isEmpty(specificCredentialPDFTemplate)? specificCredentialPDFTemplate : getJson("", credentialTemplatePath); + } + + public static String[] handleExceptionWithErrorCode(Exception exception, String flowErrorCode) { + String errorMessage = exception.getMessage(); + String errorCode = flowErrorCode; + if (errorMessage.contains(DELIMITER)) { + String[] errorSections = errorMessage.split(DELIMITER); + errorCode = errorSections[0]; + errorMessage = errorSections[1]; } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveIdrepoJson()::exit UIN is null"); - return null; + return new String[]{errorCode, errorMessage}; } + public static ResponseEntity> handleErrorResponse( + Exception exception, String flowErrorCode, HttpStatus status, MediaType contentType) { + String errorMessage = exception.getMessage(); + String errorCode = flowErrorCode; + + if (errorMessage.contains(DELIMITER)) { + String[] errorSections = errorMessage.split(DELIMITER); + errorCode = errorSections[0]; + errorMessage = errorSections[1]; + } - /** - * Gets registration processor mapping json from config and maps to - * RegistrationProcessorIdentity java class. - * - * @return the registration processor identity json - * @throws IOException - * Signals that an I/O exception has occurred. - */ - public JSONObject getRegistrationProcessorMappingJson() throws IOException { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "Utilities::getRegistrationProcessorMappingJson()::entry"); + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + responseWrapper.setResponse(null); + responseWrapper.setErrors(Utilities.getErrors(errorCode, errorMessage)); + ResponseEntity.BodyBuilder responseEntity = ResponseEntity.status(status); + if (contentType != null) { + responseEntity.contentType(contentType); + } + return responseEntity.body(responseWrapper); + } - mappingJsonString = (mappingJsonString != null && !mappingJsonString.isEmpty()) ? - mappingJsonString : getJson(configServerFileStorageURL, getRegProcessorIdentityJson); - ObjectMapper mapIdentityJsonStringToObject = new ObjectMapper(); - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.USERID.toString(), "", - "Utilities::getRegistrationProcessorMappingJson()::exit"); - return JsonUtil.getJSONObject(mapIdentityJsonStringToObject.readValue(mappingJsonString, JSONObject.class), MappingJsonConstants.IDENTITY); + public static ResponseEntity getErrorResponseEntityWithoutWrapper( + Exception exception, String flowErrorCode, HttpStatus status, MediaType contentType) { + String errorMessage = exception.getMessage(); + String errorCode = flowErrorCode; + if (errorMessage.contains(DELIMITER)) { + String[] errorSections = errorMessage.split(DELIMITER); + errorCode = errorSections[0]; + errorMessage = errorSections[1]; + } + + ResponseEntity.BodyBuilder responseEntity = ResponseEntity.status(status); + if (contentType != null) { + responseEntity.contentType(contentType); + } + return (ResponseEntity) responseEntity.body(new ErrorDTO(errorCode, errorMessage)); } - public String getMappingJsonValue(String key) throws IOException { - JSONObject jsonObject = getRegistrationProcessorMappingJson(); - Object obj = jsonObject.get(key); - if (obj instanceof LinkedHashMap) { - LinkedHashMap hm = (LinkedHashMap) obj; - return hm.get("value") != null ? hm.get("value").toString() : null; + public static ResponseEntity getErrorResponseEntityFromPlatformErrorMessage( + ErrorConstants message, HttpStatus status, MediaType contentType) { + String errorMessage = message.getErrorMessage(); + String errorCode = message.getErrorCode(); + + ResponseEntity.BodyBuilder responseEntity = ResponseEntity.status(status); + if (contentType != null) { + responseEntity.contentType(contentType); } - return jsonObject.get(key) != null ? jsonObject.get(key).toString() : null; + return (ResponseEntity) responseEntity.body(new ErrorDTO(errorCode, errorMessage)); } + public static ResponseEntity buildErrorResponse(HttpStatus status, String errorCode, String message) { + return ResponseEntity + .status(status) + .contentType(MediaType.APPLICATION_JSON) + .body(new ErrorDTO(errorCode, message)); + } - /** - * retrieve UIN from IDRepo by registration id. - * - * @param regId the reg id - * @return the JSON object - * @throws ApisResourceAccessException the apis resource access exception - * @throws IdRepoAppException the id repo app exception - * @throws IOException Signals that an I/O exception has - * occurred. - */ - @SuppressWarnings("unchecked") - public JSONObject retrieveUIN(String regId) throws ApisResourceAccessException, IdRepoAppException, IOException { - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), - regId, "Utilities::retrieveUIN()::entry"); - - if (regId != null) { - List pathSegments = new ArrayList<>(); - pathSegments.add(regId); - ResponseWrapper idResponseDto; - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), - regId, "Utilities::retrieveUIN():: RETRIEVEIDENTITYFROMRID GET service call Started"); - - idResponseDto = (ResponseWrapper) restClientService.getApi(ApiName.RETRIEVEIDENTITYFROMRID, pathSegments, "", - "", ResponseWrapper.class); - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - "Utilities::retrieveUIN():: RETRIEVEIDENTITYFROMRID GET service call ended successfully"); - - if (!idResponseDto.getErrors().isEmpty()) { - logger.error(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), - regId, - "Utilities::retrieveUIN():: error with error message " - + PlatformErrorMessages.MIMOTO_PVM_INVALID_UIN.getMessage() + " " - + idResponseDto.getErrors().toString()); - throw new IdRepoAppException( - PlatformErrorMessages.MIMOTO_PVM_INVALID_UIN.getMessage() + idResponseDto.getErrors().toString()); - } - String response = objMapper.writeValueAsString(idResponseDto.getResponse().getIdentity()); - try { - return (JSONObject) new JSONParser().parse(response); - } catch (org.json.simple.parser.ParseException e) { - logger.error(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.UIN.toString(), "", - ExceptionUtils.getStackTrace(e)); - throw new IdRepoAppException("Error while parsing string to JSONObject", e); - } + public static List getErrors(String errorCode, String errorMessage) { + ErrorDTO errorDTO = new ErrorDTO(errorCode, errorMessage); + return Lists.newArrayList(errorDTO); + } + public static String maskValue(String value) { + if (StringUtils.isEmpty(value)) { + return value; } - logger.debug(LoggerFileConstant.SESSIONID.toString(), LoggerFileConstant.REGISTRATIONID.toString(), "", - "Utilities::retrieveUIN()::exit regId is null"); - - return null; + // mask of length 20 or less + return "X".repeat(Math.min(value.length(), 20)); } } diff --git a/src/main/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidator.java b/src/main/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidator.java new file mode 100644 index 000000000..db7bf7f8e --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidator.java @@ -0,0 +1,49 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import org.apache.commons.lang.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.Set; + +@Component +public class V1CredentialIssuerWellknownResponseValidator { + public static final String MSO_MDOC = "mso_mdoc"; + public static final String LDP_VC = "ldp_vc"; + + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + Set> violations = validator.validate(response); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder("Validation failed:"); + for (ConstraintViolation violation : violations) { + sb.append("\n").append(violation.getPropertyPath()).append(": ").append(violation.getMessage()); + } + throw new InvalidWellknownResponseException(sb.toString()); + } + + for (CredentialsSupportedResponse supportedCredentialConfiguration : response.getCredentialConfigurationsSupported().values()) { + if (MSO_MDOC.equals(supportedCredentialConfiguration.getFormat())) { + if (StringUtils.isBlank(supportedCredentialConfiguration.getDoctype())) { + throw new InvalidWellknownResponseException("Mandatory field 'doctype' missing"); + } + if (CollectionUtils.isEmpty(supportedCredentialConfiguration.getClaims())) { + throw new InvalidWellknownResponseException("Mandatory field 'claims' missing"); + } + } + + if (LDP_VC.equals(supportedCredentialConfiguration.getFormat())) { + if (CollectionUtils.isEmpty(supportedCredentialConfiguration.getDisplay())) { + throw new InvalidWellknownResponseException("Mandatory field 'display' missing for V1 ldp_vc"); + } + if (CollectionUtils.isEmpty(supportedCredentialConfiguration.getClaims())) { + throw new InvalidWellknownResponseException("Mandatory field 'claims' missing for V1 ldp_vc"); + } + } + } + } +} diff --git a/src/main/java/io/mosip/mimoto/util/VCSpecVersionDetector.java b/src/main/java/io/mosip/mimoto/util/VCSpecVersionDetector.java new file mode 100644 index 000000000..5fe5c32a3 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/VCSpecVersionDetector.java @@ -0,0 +1,43 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.JsonNode; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class VCSpecVersionDetector { + + private static final String NONCE_ENDPOINT = "nonce_endpoint"; + private static final String CREDENTIAL_CONFIGURATIONS_SUPPORTED = "credential_configurations_supported"; + private static final String CREDENTIAL_METADATA = "credential_metadata"; + private static final String DISPLAY = "display"; + + public VCSpecificationVersion detectVersion(JsonNode wellknownResponse) { + if (wellknownResponse.has(VCSpecVersionDetector.NONCE_ENDPOINT) && !wellknownResponse.get(VCSpecVersionDetector.NONCE_ENDPOINT).asText().isBlank()) { + log.debug("Detected V1 specification version: nonce_endpoint field present"); + return VCSpecificationVersion.V1; + } + + JsonNode configurationsSupported = wellknownResponse.get(CREDENTIAL_CONFIGURATIONS_SUPPORTED); + if (configurationsSupported != null && configurationsSupported.isObject()) { + for (JsonNode config : configurationsSupported) { + if (config.has(CREDENTIAL_METADATA)) { + log.debug("Detected V1 specification version: credential_metadata found in credential configuration"); + return VCSpecificationVersion.V1; + } + } + + for (JsonNode config : configurationsSupported) { + if (config.has(DISPLAY)) { + log.debug("Detected Draft-13 specification version: display found in credential configuration"); + return VCSpecificationVersion.DRAFT_13; + } + } + } + + log.debug("Defaulting to V1 specification version"); + return VCSpecificationVersion.V1; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/WalletLockStatusUtils.java b/src/main/java/io/mosip/mimoto/util/WalletLockStatusUtils.java new file mode 100644 index 000000000..d3321c148 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/WalletLockStatusUtils.java @@ -0,0 +1,35 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.model.WalletLockStatus; + +public final class WalletLockStatusUtils { + + private WalletLockStatusUtils() { + // Prevent instantiation + } + + public static WalletLockStatus getWalletLockStatus(Wallet wallet) { + return wallet.getWalletMetadata().getLockStatus(); + } + + public static ErrorDTO getErrorBasedOnWalletLockStatus(Wallet wallet) { + WalletMetadata metadata = wallet.getWalletMetadata(); + WalletLockStatus currentStatus = metadata.getLockStatus(); + ErrorDTO errorDTO = null; + if (currentStatus == WalletLockStatus.PERMANENTLY_LOCKED) { + errorDTO = new ErrorDTO(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), + ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage()); + } else if (currentStatus == WalletLockStatus.TEMPORARILY_LOCKED) { + errorDTO = new ErrorDTO(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), + ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage()); + } else if (currentStatus == WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT) { + errorDTO = new ErrorDTO(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), + ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage()); + } + return errorDTO; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/WalletUtil.java b/src/main/java/io/mosip/mimoto/util/WalletUtil.java new file mode 100644 index 000000000..ab6b92635 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/WalletUtil.java @@ -0,0 +1,173 @@ +package io.mosip.mimoto.util; + +import io.mosip.kernel.cryptomanager.dto.CryptoWithPinRequestDto; +import io.mosip.kernel.cryptomanager.dto.CryptoWithPinResponseDto; +import io.mosip.kernel.cryptomanager.service.CryptomanagerService; +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.model.ProofSigningKey; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.service.DataProtectionService; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import static io.mosip.mimoto.exception.ErrorConstants.*; + +@Component +@Slf4j +public class WalletUtil { + + @Autowired + private WalletRepository walletRepository; + + @Autowired + private DataProtectionService dataProtectionService; + + @Autowired + private CryptomanagerService cryptomanagerService; + + public String decryptWalletKey(String encryptedWalletKey, String pin) { + try { + return decryptWithPin(encryptedWalletKey, pin); + } catch (Exception e) { + throw new InvalidRequestException(INVALID_PIN.getErrorCode(), INVALID_PIN.getErrorMessage(), e); + } + } + + public String createWallet(String userId, String walletName, String pin) { + SecretKey encryptionKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + return saveWallet(userId, walletName, pin, encryptionKey, "AES", "encryptWithPin"); + } + + public String saveWallet(String userId, String walletName, String walletPin, SecretKey encryptionKey, String encryptionAlgorithm, String encryptionType) { + + String walletId = UUID.randomUUID().toString(); + PasscodeControl passcodeControl = new PasscodeControl(); + WalletMetadata walletMetadata = createWalletMetadata(walletName, encryptionAlgorithm, encryptionType, passcodeControl); + String walletKey = encryptKeyWithPin(encryptionKey, walletPin); + Wallet newWallet = Wallet.builder() + .id(walletId) + .userId(userId) + .walletMetadata(walletMetadata) + .walletKey(walletKey) + .build(); + + List proofSigningKeys = createProofSigningKeys(encryptionKey, newWallet); + newWallet.setProofSigningKeys(proofSigningKeys); + + walletRepository.save(newWallet); + return walletId; + } + + private WalletMetadata createWalletMetadata(String walletName, String encryptionAlgorithm, String encryptionType, PasscodeControl passcodeControl) { + WalletMetadata walletMetadata = new WalletMetadata(); + walletMetadata.setEncryptionAlgo(encryptionAlgorithm); + walletMetadata.setEncryptionType(encryptionType); + walletMetadata.setName(walletName); + walletMetadata.setLockStatus(null); + walletMetadata.setPasscodeControl(passcodeControl); + return walletMetadata; + } + + private List createProofSigningKeys(SecretKey encryptionKey, Wallet wallet) { + List proofSigningKeys = new ArrayList<>(); + List algorithms = List.of(SigningAlgorithm.RS256, SigningAlgorithm.ES256, SigningAlgorithm.ES256K, SigningAlgorithm.ED25519); + for (SigningAlgorithm algorithm : algorithms) { + ProofSigningKey signingKey = SigningKeyUtil.createProofSigningKey(algorithm); + signingKey.setWallet(wallet); + signingKey.setEncryptedSecretKey(dataProtectionService.encryptWithAES(encryptionKey, + signingKey.getSecretKey().getEncoded())); + proofSigningKeys.add(signingKey); + } + return proofSigningKeys; + } + + public static String getSessionWalletKey(HttpSession session) { + Object key = session.getAttribute(SessionKeys.WALLET_KEY); + if (key == null) + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Wallet key not found in session"); + return key.toString(); + } + + public static void validateWalletId(HttpSession session, String walletIdFromRequest) { + Object sessionWalletId = session.getAttribute(SessionKeys.WALLET_ID); + if (sessionWalletId == null) + throw new InvalidRequestException(WALLET_LOCKED.getErrorCode(), WALLET_LOCKED.getErrorMessage()); + + String walletIdInSession = sessionWalletId.toString(); + if (!walletIdInSession.equals(walletIdFromRequest)) { + throw new InvalidRequestException(INVALID_REQUEST.getErrorCode(), "Invalid Wallet ID. Session and request Wallet ID do not match"); + } + } + + /** + * Encrypts a SecretKey with a user PIN. + * + * @param encryptionKey The SecretKey to encrypt. + * @param pin The user PIN. + * @return The encrypted key data. + */ + private String encryptKeyWithPin(SecretKey encryptionKey, String pin) { + if (encryptionKey == null) { + log.error("Encryption key is null"); + throw new IllegalArgumentException("Encryption key cannot be null"); + } + if (pin == null || pin.isEmpty()) { + log.error("PIN is null or empty"); + throw new IllegalArgumentException("PIN cannot be null or empty"); + } + try { + CryptoWithPinRequestDto requestDto = new CryptoWithPinRequestDto(); + requestDto.setUserPin(pin); + String dataAsString = Base64.getEncoder().encodeToString(encryptionKey.getEncoded()); + requestDto.setData(dataAsString); + CryptoWithPinResponseDto responseDto = cryptomanagerService.encryptWithPin(requestDto); + log.debug("Key encrypted with PIN successfully"); + return responseDto.getData(); + } catch (Exception e) { + log.error("Failed to encrypt key with PIN", e); + throw new RuntimeException("Failed to encrypt key with PIN", e); + } + } + + /** + * Decrypts data using a user PIN. + * + * @param encryptedString The encrypted data. + * @param pin The user PIN. + * @return The decrypted data. + */ + private String decryptWithPin(String encryptedString, String pin) { + if (encryptedString == null || encryptedString.isEmpty()) { + log.error("Encrypted string is null or empty"); + throw new IllegalArgumentException("Encrypted string cannot be null or empty"); + } + if (pin == null || pin.isEmpty()) { + log.error("PIN is null or empty"); + throw new IllegalArgumentException("PIN cannot be null or empty"); + } + try { + CryptoWithPinRequestDto requestDto = new CryptoWithPinRequestDto(); + requestDto.setUserPin(pin); + requestDto.setData(encryptedString); + CryptoWithPinResponseDto responseDto = cryptomanagerService.decryptWithPin(requestDto); + log.debug("Data decrypted with PIN successfully"); + return responseDto.getData(); + } catch (Exception e) { + log.error("Failed to decrypt with PIN", e); + throw new RuntimeException("Failed to decrypt with PIN", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/WalletValidator.java b/src/main/java/io/mosip/mimoto/util/WalletValidator.java new file mode 100644 index 000000000..d7f9c59a5 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/WalletValidator.java @@ -0,0 +1,69 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Utility class for validating wallet-related requests. + */ +@Slf4j +@Component +public class WalletValidator { + + @Value("${mosip.inji.user.wallet.pin.validation.regex:^\\d{6}$}") + private String pinRegex; + + @Value("${mosip.inji.user.wallet.name.validation.regex:^[A-Za-z0-9 _.-]{0,50}$}") + private String nameRegex; + + /** + * Validates the user ID. + * + * @param userId The user ID. + * @throws InvalidRequestException If the User ID is invalid. + */ + public void validateUserId(String userId) throws InvalidRequestException { + log.debug("Validating User: {}", userId); + if(userId == null){ + log.warn("User ID is not available in the session"); + throw new UnauthorizedAccessException(ErrorConstants.UNAUTHORIZED_ACCESS.getErrorCode(), "User ID not found in session"); + } + if (StringUtils.isBlank(userId)) { + log.warn("Invalid user ID: null or empty"); + throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "User ID cannot be null or empty"); + } + } + + /** + * Validates the Wallet name. + * + * @param name The Wallet name. + * @throws InvalidRequestException If the Wallet name is invalid. + */ + public void validateWalletName(String name) throws InvalidRequestException { + log.debug("Validating Wallet name: {}", name); + if (name != null && (StringUtils.isBlank(name) || !name.matches(nameRegex))) { + log.warn("Invalid Wallet name: {}", name); + throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Wallet name must be alphanumeric with allowed special characters"); + } + } + + /** + * Validates the Wallet PIN. + * + * @param pin The Wallet PIN. + * @throws InvalidRequestException If the Wallet PIN is invalid. + */ + public void validateWalletPin(String pin) throws InvalidRequestException { + log.debug("Validating Wallet PIN"); + if (pin == null || !pin.matches(pinRegex)) { + log.warn("Invalid PIN"); + throw new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "PIN must be numeric with 6 digits"); + } + } +} diff --git a/src/main/java/io/mosip/mimoto/util/WebSubSubscriptionHelper.java b/src/main/java/io/mosip/mimoto/util/WebSubSubscriptionHelper.java index 591572c50..2c5941bca 100644 --- a/src/main/java/io/mosip/mimoto/util/WebSubSubscriptionHelper.java +++ b/src/main/java/io/mosip/mimoto/util/WebSubSubscriptionHelper.java @@ -1,5 +1,12 @@ package io.mosip.mimoto.util; +import io.mosip.kernel.core.websub.spi.PublisherClient; +import io.mosip.kernel.core.websub.spi.SubscriptionClient; +import io.mosip.kernel.websub.api.exception.WebSubClientException; +import io.mosip.kernel.websub.api.model.SubscriptionChangeRequest; +import io.mosip.kernel.websub.api.model.SubscriptionChangeResponse; +import io.mosip.kernel.websub.api.model.UnsubscriptionRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; @@ -8,14 +15,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import io.mosip.kernel.core.logger.spi.Logger; -import io.mosip.kernel.core.websub.spi.PublisherClient; -import io.mosip.kernel.core.websub.spi.SubscriptionClient; -import io.mosip.kernel.websub.api.exception.WebSubClientException; -import io.mosip.kernel.websub.api.model.SubscriptionChangeRequest; -import io.mosip.kernel.websub.api.model.SubscriptionChangeResponse; -import io.mosip.kernel.websub.api.model.UnsubscriptionRequest; - +@Slf4j @Component public class WebSubSubscriptionHelper { @@ -40,14 +40,12 @@ public class WebSubSubscriptionHelper { @Autowired private PublisherClient pb; - private Logger logger = LoggerUtil.getLogger(WebSubSubscriptionHelper.class); - @Scheduled( fixedDelayString = "${websub-resubscription-delay-millisecs}", initialDelayString = "${mosip.event.delay-millisecs}" ) public void initSubscriptions() { - logger.info("Initializing subscriptions..."); + log.info("Initializing subscriptions..."); subscribeEvent(topic, callBackUrl, webSubSecret); } @@ -57,10 +55,10 @@ public SubscriptionChangeResponse unSubscribeEvent(String topic, String callBack unsubscriptionRequest.setCallbackURL(callBackUrl); unsubscriptionRequest.setHubURL(webSubHubSubUrl); unsubscriptionRequest.setTopic(topic); - logger.info("Unsubscription request : {}", unsubscriptionRequest); + log.info("Unsubscription request : "); return sb.unSubscribe(unsubscriptionRequest); } catch (WebSubClientException e) { - logger.info("Websub unsubscription error: {} ", e.getMessage()); + log.info("Websub unsubscription error: {} ", e.getMessage()); } return null; } @@ -72,10 +70,10 @@ public SubscriptionChangeResponse subscribeEvent(String topic, String callBackUr subscriptionRequest.setHubURL(webSubHubSubUrl); subscriptionRequest.setSecret(secret); subscriptionRequest.setTopic(topic); - logger.info("Subscription request : {}", subscriptionRequest); + log.info("Subscription request : "); return sb.subscribe(subscriptionRequest); } catch (WebSubClientException e) { - logger.info("Websub subscription error: {}", e.getMessage()); + log.info("Websub subscription error: {}", e.getMessage()); } return null; } @@ -85,7 +83,7 @@ public void publish(String topic, Object payload) { HttpHeaders headers = new HttpHeaders(); pb.publishUpdate(topic, payload, MediaType.APPLICATION_JSON_UTF8_VALUE, headers, webSubHubPubUrl); } catch (WebSubClientException e) { - logger.info("Websub publish update error: {}", e.getMessage()); + log.info("Websub publish update error: {}", e.getMessage()); } } @@ -95,7 +93,7 @@ public void registerTopic(String topic) { try { pb.registerTopic(topic, webSubHubPubUrl); } catch (WebSubClientException e) { - logger.info("Topic already registered: {}", e.getMessage()); + log.info("Topic already registered: {}", e.getMessage()); } } @@ -105,7 +103,7 @@ public void unRegisterTopic(String topic) { try { pb.unregisterTopic(topic, webSubHubPubUrl); } catch (WebSubClientException e) { - logger.info("Topic already unregistered: {}", e.getMessage()); + log.info("Topic already unregistered: {}", e.getMessage()); } } diff --git a/src/main/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtil.java b/src/main/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtil.java new file mode 100644 index 000000000..445480668 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtil.java @@ -0,0 +1,33 @@ +package io.mosip.mimoto.util.factory; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Provider; +import java.security.Security; + +/** + * Shared utility for BouncyCastle provider access. + * Ensures single instance across all algorithm handlers. + */ +public final class BouncyCastleProviderUtil { + + private static final Provider BC_PROVIDER = new BouncyCastleProvider(); + + static { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BC_PROVIDER); + } + } + + private BouncyCastleProviderUtil() { + + } + + /** + * Returns the BouncyCastle provider instance. + * This is a singleton instance shared across all handlers. + */ + public static Provider getProvider() { + return Security.getProvider(BouncyCastleProvider.PROVIDER_NAME); + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandler.java b/src/main/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandler.java new file mode 100644 index 000000000..cbb4ff9b7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandler.java @@ -0,0 +1,52 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import io.mosip.mimoto.constant.SigningAlgorithmConstants; + +import java.security.*; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; + +/** + * Algorithm handler for ES256 (ECDSA with SHA-256) signing. + * Uses secp256r1 (P-256) curve and BouncyCastle provider for signing operations. + */ +public class ES256AlgorithmHandler implements SigningAlgorithmHandler { + + @Override + public KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(SigningAlgorithmConstants.EC); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(SigningAlgorithmConstants.CURVE_SECP256R1); + generator.initialize(ecSpec); + return generator.generateKeyPair(); + } + + @Override + public KeyFactory getKeyFactory() throws NoSuchAlgorithmException { + return KeyFactory.getInstance(SigningAlgorithmConstants.EC); + } + + @Override + public JWK createJWK(KeyPair keyPair) { + ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); + ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); + + return new ECKey.Builder(Curve.P_256, publicKey).privateKey(privateKey).algorithm(JWSAlgorithm.ES256).keyUse(KeyUse.SIGNATURE).build(); + } + + @Override + public JWSSigner createSigner(JWK jwk) throws JOSEException { + ECDSASigner signer = new ECDSASigner((ECKey) jwk); + signer.getJCAContext().setProvider(BouncyCastleProviderUtil.getProvider()); + return signer; + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandler.java b/src/main/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandler.java new file mode 100644 index 000000000..afa2f06b4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandler.java @@ -0,0 +1,55 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import io.mosip.mimoto.constant.SigningAlgorithmConstants; + +import java.security.*; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; + +/** + * Algorithm handler for ES256K (ECDSA with SHA-256 using secp256k1 curve) signing. + * Uses secp256k1 curve and requires BouncyCastle provider for key generation and signing. + */ +public class ES256KAlgorithmHandler implements SigningAlgorithmHandler { + + private static final String CURVE_NAME = SigningAlgorithmConstants.CURVE_SECP256K1; + private static final String PROVIDER = SigningAlgorithmConstants.BC_PROVIDER; + + @Override + public KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(SigningAlgorithmConstants.EC, PROVIDER); + ECGenParameterSpec ecSpec = new ECGenParameterSpec(CURVE_NAME); + generator.initialize(ecSpec); + return generator.generateKeyPair(); + } + + @Override + public KeyFactory getKeyFactory() throws NoSuchAlgorithmException, NoSuchProviderException { + return KeyFactory.getInstance(SigningAlgorithmConstants.EC, PROVIDER); + } + + @Override + public JWK createJWK(KeyPair keyPair) { + ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); + ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); + + return new ECKey.Builder(Curve.SECP256K1, publicKey).privateKey(privateKey).algorithm(JWSAlgorithm.ES256K).keyUse(KeyUse.SIGNATURE).build(); + } + + @Override + public JWSSigner createSigner(JWK jwk) throws JOSEException { + ECDSASigner signer = new ECDSASigner((ECKey) jwk); + signer.getJCAContext().setProvider(BouncyCastleProviderUtil.getProvider()); + return signer; + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandler.java b/src/main/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandler.java new file mode 100644 index 000000000..c2e04c8c8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandler.java @@ -0,0 +1,89 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jca.JCAContext; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.util.Base64URL; +import io.mosip.mimoto.constant.SigningAlgorithmConstants; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.signers.Ed25519Signer; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.EdECPrivateKey; +import java.security.interfaces.EdECPublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +/** + * Algorithm handler for Ed25519 (Edwards-curve Digital Signature Algorithm) signing. + * Uses Ed25519 curve and implements custom JWSSigner with BouncyCastle for signing operations. + */ +public class Ed25519AlgorithmHandler implements SigningAlgorithmHandler { + + private static final int ED25519_KEY_SIZE_BYTES = 32; + + @Override + public KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(SigningAlgorithmConstants.ED25519); + return generator.generateKeyPair(); + } + + @Override + public KeyFactory getKeyFactory() throws NoSuchAlgorithmException { + return KeyFactory.getInstance(SigningAlgorithmConstants.ED25519); + } + + @Override + public JWK createJWK(KeyPair keyPair) { + EdECPublicKey edECPublicKey = (EdECPublicKey) keyPair.getPublic(); + EdECPrivateKey edECPrivateKey = (EdECPrivateKey) keyPair.getPrivate(); + + byte[] x = Arrays.copyOfRange(edECPublicKey.getEncoded(), edECPublicKey.getEncoded().length - ED25519_KEY_SIZE_BYTES, edECPublicKey.getEncoded().length); + byte[] d = Arrays.copyOfRange(edECPrivateKey.getEncoded(), edECPrivateKey.getEncoded().length - ED25519_KEY_SIZE_BYTES, edECPrivateKey.getEncoded().length); + + return new OctetKeyPair.Builder(Curve.Ed25519, Base64URL.encode(x)).d(Base64URL.encode(d)).algorithm(JWSAlgorithm.Ed25519).keyUse(KeyUse.SIGNATURE).build(); + } + + @Override + public JWSSigner createSigner(JWK jwk) throws JOSEException { + OctetKeyPair okp = (OctetKeyPair) jwk; + byte[] privateKeyBytes = okp.getD().decode(); + + return new JWSSigner() { + private final JCAContext jcaContext = new JCAContext(); + + @Override + public Base64URL sign(JWSHeader header, byte[] input) throws JOSEException { + try { + Ed25519Signer signer = new Ed25519Signer(); + signer.init(true, new Ed25519PrivateKeyParameters(privateKeyBytes, 0)); + signer.update(input, 0, input.length); + return Base64URL.encode(signer.generateSignature()); + } catch (Exception e) { + throw new JOSEException("Ed25519 signing failed", e); + } + } + + @Override + public Set supportedJWSAlgorithms() { + return Set.of(JWSAlgorithm.Ed25519, JWSAlgorithm.EdDSA); + } + + @Override + public JCAContext getJCAContext() { + return jcaContext; + } + }; + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandler.java b/src/main/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandler.java new file mode 100644 index 000000000..502a087f7 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandler.java @@ -0,0 +1,50 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import io.mosip.mimoto.constant.SigningAlgorithmConstants; + +import java.security.*; +import java.security.interfaces.*; + +/** + * Algorithm handler for RS256 (RSA with SHA-256) signing. + * Uses RSA 2048-bit keys and BouncyCastle provider for signing operations. + */ +public class RS256AlgorithmHandler implements SigningAlgorithmHandler { + + private static final int RSA_KEY_SIZE = 2048; + + @Override + public KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(SigningAlgorithmConstants.RSA); + generator.initialize(RSA_KEY_SIZE); + return generator.generateKeyPair(); + } + + @Override + public KeyFactory getKeyFactory() throws NoSuchAlgorithmException { + return KeyFactory.getInstance(SigningAlgorithmConstants.RSA); + } + + @Override + public JWK createJWK(KeyPair keyPair) { + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + return new RSAKey.Builder(publicKey).privateKey(privateKey).algorithm(JWSAlgorithm.RS256).keyUse(KeyUse.SIGNATURE).build(); + } + + @Override + public JWSSigner createSigner(JWK jwk) throws JOSEException { + RSASSASigner signer = new RSASSASigner((RSAKey) jwk); + signer.getJCAContext().setProvider(BouncyCastleProviderUtil.getProvider()); + return signer; + } + +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandler.java b/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandler.java new file mode 100644 index 000000000..4d995cde9 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandler.java @@ -0,0 +1,30 @@ +package io.mosip.mimoto.util.factory; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; + +/** + * Strategy interface for algorithm-specific cryptographic operations. + * Implementations encapsulate the logic for key generation, key factory retrieval, + * JWK creation, and JWSSigner creation for a specific signing algorithm. + * + * @implNote All implementations should be thread-safe and stateless. + */ +public interface SigningAlgorithmHandler { + + KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException; + + KeyFactory getKeyFactory() throws NoSuchAlgorithmException, NoSuchProviderException; + + JWK createJWK(KeyPair keyPair); + + JWSSigner createSigner(JWK jwk) throws JOSEException; + +} diff --git a/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactory.java b/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactory.java new file mode 100644 index 000000000..5c0301d5a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactory.java @@ -0,0 +1,42 @@ +package io.mosip.mimoto.util.factory; + +import io.mosip.mimoto.constant.SigningAlgorithm; + +import java.util.EnumMap; +import java.util.Map; + +/** + * Factory for managing and retrieving algorithm-specific cryptographic handlers. + * Uses the Strategy pattern to provide the appropriate handler implementation + * for each signing algorithm. + */ +public class SigningAlgorithmHandlerFactory { + + private static final Map handlers; + + static { + handlers = new EnumMap<>(SigningAlgorithm.class); + handlers.put(SigningAlgorithm.RS256, new RS256AlgorithmHandler()); + handlers.put(SigningAlgorithm.ES256, new ES256AlgorithmHandler()); + handlers.put(SigningAlgorithm.ES256K, new ES256KAlgorithmHandler()); + handlers.put(SigningAlgorithm.ED25519, new Ed25519AlgorithmHandler()); + } + + /** + * Gets the algorithm handler for the specified algorithm. + * + * @param algorithm The signing algorithm + * @return The corresponding algorithm handler + * @throws IllegalArgumentException If the algorithm is not supported + */ + public static SigningAlgorithmHandler getHandler(SigningAlgorithm algorithm) { + if (algorithm == null) { + throw new IllegalArgumentException("Algorithm cannot be null"); + } + SigningAlgorithmHandler handler = handlers.get(algorithm); + if (handler == null) { + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + return handler; + } +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/util/parser/Draft13WellknownParser.java b/src/main/java/io/mosip/mimoto/util/parser/Draft13WellknownParser.java new file mode 100644 index 000000000..77e990574 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/parser/Draft13WellknownParser.java @@ -0,0 +1,42 @@ +package io.mosip.mimoto.util.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.wellknown.draft13.Draft13WellKnownResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.Draft13CredentialIssuerWellknownResponseValidator; +import jakarta.validation.Validator; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class Draft13WellknownParser implements WellknownResponseParser { + + private final ObjectMapper objectMapper; + private final Draft13CredentialIssuerWellknownResponseValidator wellknownResponseValidator; + + public Draft13WellknownParser(ObjectMapper objectMapper, Draft13CredentialIssuerWellknownResponseValidator wellknownResponseValidator) { + this.objectMapper = objectMapper; + this.wellknownResponseValidator = wellknownResponseValidator; + } + + @Override + public VCSpecificationVersion getSupportedVersion() { + return VCSpecificationVersion.DRAFT_13; + } + + @Override + public CredentialIssuerWellKnownResponse parse(String jsonResponse) throws IOException { + Draft13WellKnownResponse draft13Response = objectMapper.readValue(jsonResponse, Draft13WellKnownResponse.class); + CredentialIssuerWellKnownResponse wellKnownResponse = objectMapper.convertValue(draft13Response, CredentialIssuerWellKnownResponse.class); + wellKnownResponse.setVersion(getSupportedVersion()); + return wellKnownResponse; + } + + @Override + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + wellknownResponseValidator.validate(response, validator); + } +} diff --git a/src/main/java/io/mosip/mimoto/util/parser/V1WellknownParser.java b/src/main/java/io/mosip/mimoto/util/parser/V1WellknownParser.java new file mode 100644 index 000000000..4598a550b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/parser/V1WellknownParser.java @@ -0,0 +1,100 @@ +package io.mosip.mimoto.util.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.wellknown.v1.CredentialMetaData; +import io.mosip.mimoto.dto.mimoto.wellknown.v1.V1WellKnownResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.V1CredentialIssuerWellknownResponseValidator; +import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class V1WellknownParser implements WellknownResponseParser { + + private final ObjectMapper objectMapper; + private final V1CredentialIssuerWellknownResponseValidator wellknownResponseValidator; + + public V1WellknownParser(ObjectMapper objectMapper, V1CredentialIssuerWellknownResponseValidator wellknownResponseValidator) { + this.objectMapper = objectMapper; + this.wellknownResponseValidator = wellknownResponseValidator; + } + + @Override + public VCSpecificationVersion getSupportedVersion() { + return VCSpecificationVersion.V1; + } + + @Override + public CredentialIssuerWellKnownResponse parse(String jsonResponse) throws IOException { + V1WellKnownResponse v1Response = objectMapper.readValue(jsonResponse, V1WellKnownResponse.class); + return toWellKnownResponse(v1Response); + } + + @Override + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + wellknownResponseValidator.validate(response, validator); + } + + private CredentialIssuerWellKnownResponse toWellKnownResponse(V1WellKnownResponse v1Response) { + CredentialIssuerWellKnownResponse wellKnownResponse = new CredentialIssuerWellKnownResponse(); + wellKnownResponse.setCredentialIssuer(v1Response.getCredentialIssuer()); + wellKnownResponse.setAuthorizationServers(v1Response.getAuthorizationServers()); + wellKnownResponse.setCredentialEndPoint(v1Response.getCredentialEndPoint()); + wellKnownResponse.setCredentialConfigurationsSupported(toCredentialConfigurations(v1Response)); + wellKnownResponse.setNonceEndpoint(v1Response.getNonceEndpoint()); + wellKnownResponse.setVersion(getSupportedVersion()); + return wellKnownResponse; + } + + private Map toCredentialConfigurations(V1WellKnownResponse v1Response) { + LinkedHashMap resultMap = new LinkedHashMap<>(); + v1Response.getCredentialConfigurationsSupported().forEach((key, value) -> resultMap.put(key, toCredentialSupported(value))); + return resultMap; + } + + private CredentialsSupportedResponse toCredentialSupported(io.mosip.mimoto.dto.mimoto.wellknown.v1.CredentialsSupportedResponse v1CredentialSupportedResponse) { + CredentialsSupportedResponse credentialSupported = new CredentialsSupportedResponse(); + credentialSupported.setFormat(v1CredentialSupportedResponse.getFormat()); + credentialSupported.setScope(v1CredentialSupportedResponse.getScope()); + credentialSupported.setDoctype(v1CredentialSupportedResponse.getDoctype()); + credentialSupported.setProofTypesSupported(objectMapper.convertValue(v1CredentialSupportedResponse.getProofTypesSupported(), objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, io.mosip.mimoto.dto.mimoto.ProofTypesSupported.class))); + credentialSupported.setVct(v1CredentialSupportedResponse.getVct()); + + CredentialMetaData metadata = v1CredentialSupportedResponse.getCredentialMetadata(); + if (metadata != null) { + if (metadata.getDisplay() != null) { + List display = metadata.getDisplay().stream().map(d -> objectMapper.convertValue(d, io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse.class)).collect(Collectors.toList()); + credentialSupported.setDisplay(display); + } + credentialSupported.setClaims(normalizeClaims(metadata.getClaims())); + } + + return credentialSupported; + } + + private Map normalizeClaims(List> claimsList) { + if (claimsList == null || claimsList.isEmpty()) { + return null; + } + LinkedHashMap claimsMap = new LinkedHashMap<>(); + for (Map claim : claimsList) { + List path = (List) claim.get("path"); + if (path == null || path.isEmpty()) continue; + String key = path.getLast(); + if (key == null) continue; + claimsMap.put(key, Map.of("display", claim.getOrDefault("display", List.of()))); + } + return claimsMap.isEmpty() ? null : claimsMap; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/parser/WellknownParserFactory.java b/src/main/java/io/mosip/mimoto/util/parser/WellknownParserFactory.java new file mode 100644 index 000000000..12fee8c9b --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/parser/WellknownParserFactory.java @@ -0,0 +1,31 @@ +package io.mosip.mimoto.util.parser; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class WellknownParserFactory { + + private final Map parsers; + + public WellknownParserFactory(List parserList) { + this.parsers = parserList.stream() + .collect(Collectors.toMap( + WellknownResponseParser::getSupportedVersion, + Function.identity() + )); + } + + public WellknownResponseParser getParser(VCSpecificationVersion version) { + WellknownResponseParser parser = parsers.get(version); + if (parser == null) { + throw new IllegalArgumentException("No wellknown parser found for version: " + version); + } + return parser; + } +} diff --git a/src/main/java/io/mosip/mimoto/util/parser/WellknownResponseParser.java b/src/main/java/io/mosip/mimoto/util/parser/WellknownResponseParser.java new file mode 100644 index 000000000..ff87062ec --- /dev/null +++ b/src/main/java/io/mosip/mimoto/util/parser/WellknownResponseParser.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.util.parser; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.Validator; + +import java.io.IOException; + +public interface WellknownResponseParser { + + VCSpecificationVersion getSupportedVersion(); + + CredentialIssuerWellKnownResponse parse(String jsonResponse) throws IOException; + + void validate(CredentialIssuerWellKnownResponse response, Validator validator) + throws InvalidWellknownResponseException; +} diff --git a/src/main/resources/application-default.properties b/src/main/resources/application-default.properties new file mode 100644 index 000000000..54a145d5e --- /dev/null +++ b/src/main/resources/application-default.properties @@ -0,0 +1,235 @@ +# Inji default properties +mosip.inji.allowedAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedEkycAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedInternalAuthType=otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.faceSdkModelUrl=https://api.collab.mosip.net/inji +# maximum number of retry for downloading vc +mosip.inji.vcDownloadMaxRetry=10 +# pool interval in milliseconds +mosip.inji.vcDownloadPoolInterval=6000 +# validate binding audience url to be sent in token +mosip.inji.audience=ida-binding +# issuer to be sent in token +mosip.inji.issuer=residentapp +# warning screen domain name +mosip.inji.warningDomainName=${mosip.api.public.url} +#timeout for vc download api via OpenId4VCI flow in milliseconds +mosip.inji.openId4VCIDownloadVCTimeout=30000 +# inji documentation url +mosip.inji.aboutInjiUrl=https://docs.inji.io/inji-wallet/inji-mobile +# minimum storage space required for making audit entry in MB +mosip.inji.minStorageRequiredForAuditEntry=2 +# minimum storage space required for downloading / receiving vc in MB +mosip.inji.minStorageRequired=2 + +#------------------------------------ Key-manager specific properties -------------------------------------------------- + +# Crypto asymmetric algorithm name +mosip.kernel.crypto.asymmetric-algorithm-name=RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING +# Crypto symmetric algorithm name +mosip.kernel.crypto.symmetric-algorithm-name=AES/GCM/NoPadding +# Encrypted data and encrypted symmetric key separator +mosip.kernel.data-key-splitter=#KEY_SPLITTER# +# GCM tag length +mosip.kernel.crypto.gcm-tag-length=128 +# Hash algo name +mosip.kernel.crypto.hash-algorithm-name=PBKDF2WithHmacSHA512 +# Symmetric key length used in hash +mosip.kernel.crypto.hash-symmetric-key-length=256 +# No of iterations in hash +mosip.kernel.crypto.hash-iteration=10000 +# Keygenerator symmetric algorithm name +mosip.kernel.keygenerator.symmetric-algorithm-name=AES +# Symmetric algorithm key length +mosip.kernel.keygenerator.symmetric-key-length=256 +# Asymmetric algorithm key length +mosip.kernel.keygenerator.asymmetric-key-length=2048 +# Keygenerator asymmetric algorithm name +mosip.kernel.keygenerator.asymmetric-algorithm-name=RSA +# Defines the file path for the PKCS#12 keystore used by the KeyManager service for cryptographic keys +# E.g., place the .p12 file in 'mimoto/certs' folder and provide its path here +mosip.kernel.keymanager.hsm.config-path=/home/mosip/encryption/encryptionkeystore.p12 +# Defines the type of the keystore used by the Key Manager as PKCS#12 +mosip.kernel.keymanager.hsm.keystore-type=PKCS12 +# Password to access the KeyManager PKCS#12 keystore file +mosip.kernel.keymanager.hsm.keystore-pass=${mimoto.oidc.keystore.password} + +# Default Certificate properties +mosip.kernel.keymanager.certificate.default.common-name=www.example.com +mosip.kernel.keymanager.certificate.default.organizational-unit=EXAMPLE-CENTER +mosip.kernel.keymanager.certificate.default.organization=IIITB +mosip.kernel.keymanager.certificate.default.location=BANGALORE +mosip.kernel.keymanager.certificate.default.state=KA +mosip.kernel.keymanager.certificate.default.country=IN + +# keymanager database properties +# Specifies the driver class name for the PostgreSQL database, required for the application to interact with the database +keymanager.persistence.jdbc.driver=org.postgresql.Driver +# Specifies the JDBC URL for connecting to the PostgreSQL database, required to establish a connection to the inji_mimoto database +keymanager_database_url = jdbc:postgresql://${inji.mimoto.database.hostname}:${inji.mimoto.database.port}/inji_mimoto +# Defines the password for accessing the PostgreSQL database, required for authentication +keymanager_database_password=${db.dbuser.password} +# Defines the username for accessing the PostgreSQL database, required for authentication +keymanager_database_username= mimotouser +# Specifies the schema name used by the Keymanager in the PostgreSQL database +keymanager.persistence.jdbc.schema=mimoto + +# Hibernate properties +hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +hibernate.hbm2ddl.auto=none +hibernate.jdbc.lob.non_contextual_creation=true +hibernate.show_sql=false +hibernate.format_sql=false +hibernate.connection.charSet=utf8 +hibernate.cache.use_second_level_cache=false +hibernate.cache.use_query_cache=false +hibernate.cache.use_structured_entries=false +hibernate.generate_statistics=false +hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext + +#Inji Web Config +# Specifies the URL to redirect the user after successful authentication using Identity Provider(e.g., Goggle), pointing to the passcode page in Inji Web +mosip.inji.web.authentication.success.redirect.url=${mosip.inji.web.url}/user/passcode +# Defines the List of Issuers that are limited to issue only single Verifiable Credential +mosip.inji.wallet.issuersWithSingleVcLimit=Mosip +# Defines the maximum number of characters allowed to be encoded into a QR code. The value can be set to anything less than or equal to 4096. +# If the data size exceeds 4096 characters (equivalent to 4KB), the 'pixel pass' library will fail to generate the QR code. +mosip.inji.qr.data.size.limit=4096 +# Defines the height (in Pixels) of the generated QR code +mosip.inji.qr.code.height=400 +# Defines the width (in Pixels) of the generated QR code +mosip.inji.qr.code.width=400 + +#User Wallet Config +# Regular expression for validating wallet PINs, ensuring it is exactly 6 digits +mosip.inji.user.wallet.pin.validation.regex=^\\d{6}$ +# Regular expression for validating wallet names, allowing alphanumeric characters, spaces, underscores, hyphens, and periods with a maximum length of 50 characters +mosip.inji.user.wallet.name.validation.regex=^[A-Za-z0-9 _.-]{0,50}$ + +#Google OAuth2 configuration +# Specifies the client ID for Google OAuth2 authentication, required to identify the application during OAuth2 requests. +spring.security.oauth2.client.registration.google.client-id=${mosip.injiweb.google.client.id} +# Specifies the client secret for Google OAuth2 authentication, used to authenticate the application securely +spring.security.oauth2.client.registration.google.client-secret=${mosip.injiweb.google.client.secret} +# Defines the scopes requested during Google OAuth2 authentication, specifying the level of access required (e.g., profile and email) +spring.security.oauth2.client.registration.google.scope=profile,email +# Specifies the redirect URI for Google OAuth2 authentication callbacks, where Google sends the authorization response +spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +# Defines the authorization grant type for Google OAuth2 authentication, indicating the flow used (e.g., authorization code) +spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code +# Defines the human-readable name that will be displayed for the Google login option +# For example, if it is set to "Login with Google" it will appear as "Login with Google" on a spring security generated login page +spring.security.oauth2.client.registration.google.client-name=Google +# Specifies the authorization endpoint URI for Google OAuth2, where users are redirected to authenticate +spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth?prompt=select_account +# Specifies the token endpoint URI for Google OAuth2, used to exchange the authorization code for an access token +spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token +# Specifies the user information endpoint URI for Google OAuth2, used to fetch user details after authentication +spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo +# Specifies the URI for Google's JSON Web Key Set (JWK) used for token validation, ensuring secure communication +spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs +# Maps the 'sub' attribute from the Google UserInfo endpoint response to the Spring Security principal's name (username) +spring.security.oauth2.client.provider.google.userNameAttribute=sub +# Maps the 'name' attribute from the Google UserInfo endpoint response to the Spring Security principal's accessible name +spring.security.oauth2.client.provider.google.nameAttribute=name +# Maps the 'email' attribute from the Google UserInfo endpoint response to the Spring Security principal's email address +spring.security.oauth2.client.provider.google.emailAttribute=email +# Maps the 'picture' attribute from the Google UserInfo endpoint response to the Spring Security principal's profile picture URL +spring.security.oauth2.client.provider.google.pictureAttribute=picture +# Maps the 'phone_number' attribute from the Google UserInfo endpoint response to the Spring Security principal's phone number +spring.security.oauth2.client.provider.google.phoneNumberAttribute=phone_number + +#Database configuration +# Specifies the JDBC URL for connecting to the PostgreSQL database, required to establish a connection to the inji_mimoto database +spring.datasource.url=jdbc:postgresql://${inji.mimoto.database.hostname}:${inji.mimoto.database.port}/inji_mimoto +# Defines the username for accessing the PostgreSQL database, required for authentication +spring.datasource.username=mimotouser +# Defines the password for accessing the PostgreSQL database, required for authentication +spring.datasource.password=${db.dbuser.password} +# Configures the Hibernate behavior for database schema management, setting it to 'none' prevents Hibernate from automatically creating, updating, or dropping the database schema on startup +spring.jpa.hibernate.ddl-auto=none +# Enables or disables the display of SQL statements in the logs, useful for debugging database queries +spring.jpa.show-sql=true +# Specifies the driver class name for the PostgreSQL database, required for the application to interact with the database +spring.datasource.driver-class-name=org.postgresql.Driver + +#Redis configuration +# Specifies the type of session store to use (Redis in this case) for storing http sessions in User Login flow, enabling distributed session management. +spring.session.store-type=redis +# Defines the hostname of the Redis server, required to connect to the Redis instance. +spring.data.redis.host=${redis.host:localhost} +# Specifies the port number for the Redis server, required to connect to the Redis instance. +spring.data.redis.port=${redis.port:6379} +# Sets the password for connecting to the Redis server (empty if no password is required), ensuring secure access. +spring.data.redis.password=${redis.password:""} +# Sets the duration for which the application waits to establish a connection with the Redis server, preventing long delays. +spring.data.redis.connect-timeout=10s +# Specifies the duration the application waits for a response from Redis operations (e.g., setting or retrieving keys), ensuring timely responses. +spring.data.redis.timeout=10s +# Specifies the duration of inactivity after which a user session in configured session store (Ex: redis) will expire, ensuring session cleanup. +server.servlet.session.timeout=30m +# Specifies the namespace for Redis session keys, used to avoid key collisions in Redis. +spring.session.redis.namespace=injiweb:session: +# Configures the database dialect for Hibernate (PostgreSQL in this case), ensuring compatibility with the database. +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +# List of URLs that can be accessed without any user authentication by Identity Provider +mosip.security.ignore-auth-urls=/safetynet/**,/actuator/**,/swagger-ui/**,/v3/api-docs/**,\ + /allProperties,/credentials/**,/credentialshare/**,/binding-otp,/wallet-binding,/get-token/**,/v2/get-token/**,\ + /issuers,/issuers/**,/v2/issuers,/v2/issuers/**,/authorize,/req/otp,/vid,/req/auth/**,/req/individualId/otp,/aid/get-individual-id,\ + /verifiers, /auth/*/token-login + +# CSRF Security Configuration +# CSRF protection is always enabled by default. State-changing requests (POST, PUT, DELETE, PATCH) +# require a valid CSRF token to be sent in the X-XSRF-TOKEN header. +# List of URLs that are excluded from CSRF protection. These URLs can still require authentication. +# OAuth2 endpoints and public endpoints are typically excluded from CSRF protection. +mosip.security.csrf-ignore-urls=/oauth2/**,/logout,/safetynet/**,/credentials/**,/credentialshare/**,/binding-otp,/wallet-binding,\ + /get-token/**,/v2/get-token/**,/req/otp,/vid,/req/auth/**,/req/individualId/otp,/auth/*/token-login + +# Inji mobile wallet configuration +# This template defines the list of the fields of the downloaded verifiable credential will be included in the event files when generating different documents +credential.template=template.json +# Specifies the base directory path where the credentials which are downloaded in the Inji mobile wallet will be stored +credential.data.path=data +safetynet.api.key= +safetynet.api.url=https://www.googleapis.com/androidcheck/v1/attestations/verify?key=${safetynet.api.key} + +registration.processor.print.textfile=registration-processor-print-text-file.json + +# Application ID used for identifying and managing the Mimoto service keys in Key Manager service +mosip.inji.app.id=MIMOTO + +# Face key mappings - first available key is used for image display +mosip.injiweb.vc.subject.face.keys.order=image,face,photo,picture,portrait + +# Enables or disables the display of disclosures for VCs +mosip.injiweb.mask.disclosures=true + +#Caffeine cache related configurations +# Cache related configurations +# Specifies which cache type to use (e.g., caffeine, redis) +spring.cache.type=redis + +# Cache expiry time in minutes for the issuer's well-known endpoint response. +cache.credential-issuer.wellknown.expiry-time-in-min = 60 +# Cache expiry time in minutes for issuers configurations read from a config file. +cache.issuers-config.expiry-time-in-min = 60 +# Cache expiry time in minutes for the authentication server's well-known endpoint response. +cache.credential-issuer.authserver-wellknown.expiry-time-in-min = 60 +# Cache expiry time in minutes for the pre-registered trusted verifiers list. +cache.pre-registered-trusted-verifiers.expiry-time-in-min=60 +# Default cache expiry time in minutes for others cache types +cache.default.expiry-time-in-min = 60 + +# Wallet Passcode Configuration +# Duration (in minutes) for which the wallet remains locked after exceeding failed attempts in a cycle. +# Must be a whole number; decimal/fractional values (e.g., 0.5) will throw an error during Mimoto launch +wallet.passcode.retryBlockedUntil=60 +# Maximum number of failed passcode attempts allowed in a single cycle (defaults to 1 if <=0) +wallet.passcode.maxFailedAttemptsAllowedPerCycle=5 +# Maximum number of lock cycles allowed before the wallet is permanently locked (defaults to 1 if <=0) +wallet.passcode.maxLockCyclesAllowed=3 + +# Signing Algorithms Priority Order Configuration +signing.algorithms.priority.order=ED25519,ES256K,ES256,RS256 \ No newline at end of file diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 3e1dd2537..bc19fa0f2 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -1,61 +1,78 @@ - # MOSIP -mosipbox.public.url=https://dev2.mosip.net - -public.url=https://your-ngrok-public-enpoint.ngrok.io - -# Resident service -mosip.resident.base.url=${mosipbox.public.url}/resident/v1 - +# Defines the base URLs for various MOSIP services and Keycloak for internal and external communication +mosip.api.public.url=http://localhost:${server.port} +mosipbox.public.url=https://api-internal.collab.mosip.net +keycloak.internal.url=http://keycloak.keycloak +keycloak.external.url=https://iam.collab.mosip.net +mosip.kernel.masterdata.url=http://masterdata.kernel +mosip.kernel.authmanager.url=http://authmanager.kernel/ + +# Resident service Configurations used in downloading the Verifiable Credential of MosipOtp issuer +# Base URL for Resident service +mosip.resident.base.url=https://api.collab.mosip.net/resident/v1 +# This URL used for fetching the OTP from resident service prior to initiating the download flow on the Mobile Wallet RESIDENT_OTP=${mosip.resident.base.url}/req/otp +# This URL used for fetching the OTP while getting the UIN/VID from AID(Application ID) +RESIDENT_INDIVIDUALID_OTP=${mosip.resident.base.url}/individualId/otp +# This URL is used to get the status about whether the UIN/VID is generated from AID or not +RESIDENT_AID_GET_INDIVIDUALID=${mosip.resident.base.url}/aid/status +# URL to request the Verifiable Credential download from the Resident service RESIDENT_CREDENTIAL_REQUEST=${mosip.resident.base.url}/req/credential +# URL to check if the Verifiable Credential is downloaded yet or not RESIDENT_CREDENTIAL_REQUEST_STATUS=${RESIDENT_CREDENTIAL_REQUEST}/status +# URL used to get the VID for the respective UIN RESIDENT_VID=${mosip.resident.base.url}/vid RESIDENT_AUTH_LOCK=${mosip.resident.base.url}/req/auth-lock RESIDENT_AUTH_UNLOCK=${mosip.resident.base.url}/req/auth-unlock +#Esignet service +# Base URL for e-Signet service +mosip.esignet.host=https://esignet.collab.mosip.net +# Endpoint for fetching the OTP in Wallet binding flow +BINDING_OTP=${mosip.esignet.host}/v1/esignet/binding/binding-otp +# Endpoint for binding the credentials to the Wallet +WALLET_BINDING=${mosip.esignet.host}/v1/esignet/binding/wallet-binding -# Mimoto mobileid backend -credential.template=template.json -credential.sample=sample_credential.json -credential.data.path=data -safetynet.api.key= -safetynet.api.url=https://www.googleapis.com/androidcheck/v1/attestations/verify?key=${safetynet.api.key} - -registration.processor.print.textfile=registration-processor-print-text-file.json -# Websub - -# Localhost websub -# mosip.event.hubUrl=http://localhost:9191/websub -# mosip.event.hub.subUrl=${mosip.event.hubUrl}/hub -# mosip.event.hub.pubUrl=${mosip.event.hubUrl}/publish +# Websub service configurations # Dev sandbox -mosip.event.hubUrl=${mosipbox.public.url}/hub/ +# Base URL for WebSub service +mosip.websub.url=http://websub.websub/ +# Hub URL for WebSub service +mosip.event.hubUrl=${mosip.websub.url}/hub/ +# WebSub hub URL used to subscribe to a specific topic mosip.event.hub.subUrl=${mosip.event.hubUrl} +# WebSub hub URL used to publish the data to a specific topic mosip.event.hub.pubUrl=${mosip.event.hubUrl} +# Delayed WebSub subscription. Default is 5 seconds in ms. +mosip.event.delay-millisecs=5000 +# WebSub re-subscription workaround for losing subscribed topic when MOSIP WebSub update or restart. Default is 5 minutes in ms. +websub-resubscription-delay-millisecs=300000 -# MOSIP partner +# Callback URL for receiving the WebSub event notifications +mosip.event.callBackUrl=${mosip.api.public.url}/v1/mimoto/credentialshare/callback/notify +# Kafka topic to which the CREDENTIAL_ISSUED for specific partner id is published +mosip.event.topic=${mosip.partner.id}/CREDENTIAL_ISSUED +# Secret key used for validating the authenticity of the WebSub event notifications +mosip.event.secret=Kslk30SNF2AChs2 +# MOSIP partner +# Unique identifier which is being used as the Issuer during MOSIP OTP issuer Verifiable Credential download requests mosip.partner.id=mpartner-default-mobile +# Name of the PKCS#12 keystore file for cryptographic operations mosip.partner.crypto.p12.filename=keystore.p12 +# Password for accessing the PKCS#12 keystore file mosip.partner.crypto.p12.password=abc123 +# Alias used to access the secure credentials like private key of partner within the PKCS#12 keystore file mosip.partner.crypto.p12.alias=partner +# Encryption key used for securing partner data mosip.partner.encryption.key=123456 - +# Property which indicates whether the data includes its thumbprint (a unique hash for integrity verification) or not mosip.partner.prependThumbprint=true -mosip.datashare.partner.id=mpartner-default-resident -mosip.datashare.policy.id=mpolicy-default-resident -mosip.event.callBackUrl=${public.url}/credentialshare/callback/notify -mosip.event.topic=${mosip.partner.id}/CREDENTIAL_ISSUED -mosip.event.secret=Kslk30SNF2AChs2 +# Property which defines whether protection against Cross-Site Request Forgery (CSRF) attacks should be enabled or disabled csrf.disabled=true -# Delayed websub subscription. Default is 5 seconds in ms. -mosip.event.delay-millisecs=5000 -# Websub re-subscription workaround for losing subscribed topic when MOSIP websub update or restart. Default is 5 minutes in ms. -websub-resubscription-delay-millisecs=300000 #-------------TOKEN GENERATION---------------- #Token generation request id @@ -73,16 +90,16 @@ token.request.clientId=mosip-regproc-client #Token generation secret key token.request.secretKey=abc123 #Token generation issuer url -token.request.issuerUrl=${mosipbox.public.url}/keycloak/auth/realms/mosip +token.request.issuerUrl=${keycloak.internal.url}/auth/realms/mosip #Audit Service -AUDIT=${mosipbox.public.url}/v1/auditmanager/audits -AUDIT_URL=${mosipbox.public.url}/v1/auditmanager/audits -KEYBASEDTOKENAPI=${mosipbox.public.url}/v1/authmanager/authenticate/clientidsecretkey +AUDIT=${mosip.kernel.auditmanager.url}/v1/auditmanager/audits +AUDIT_URL=${mosip.kernel.auditmanager.url}/v1/auditmanager/audits +KEYBASEDTOKENAPI=${mosip.kernel.authmanager.url}/v1/authmanager/authenticate/clientidsecretkey #Master Data Services # MASTER=http://kernel-masterdata-service/v1/masterdata -MASTER=${mosipbox.public.url}/v1/masterdata +MASTER=${mosip.kernel.masterdata.url}/v1/masterdata TEMPLATES=${MASTER}/templates #Packet receiver application version @@ -102,19 +119,17 @@ mosip.country.code=MOR registration.processor.signature.isEnabled=true # Language Supported By Platform - ISO -mosip.supported-languages=eng,ara,fra - -mosip.template-language=eng +# Defines the list of optional languages supported by the platform mosip.optional-languages=fra,ara +# Defines the list of mandatory languages supported by the platform mosip.mandatory-languages=eng - # mosip.primary-language=eng # mosip.secondary-language=ara #----------------------- CBEFF Util-------------------------------------------------- # Cbeff URL where the files will be stored in git, change it accordingly in case of change of storage location. # mosip.kernel.xsdstorage-uri=${spring.cloud.config.uri}/${spring.application.name}/${spring.profiles.active}/${spring.cloud.config.label}/ -mosip.kernel.xsdstorage-uri=${mosipbox.public.url}/config/print/mz/develop2-v2/ +mosip.kernel.xsdstorage-uri=https://raw.githubusercontent.com/mosip/mosip-config/qa-1201-B2/ # Cbeff XSD file name in config server mosip.kernel.xsdfile=mosip-cbeff.xsd @@ -143,7 +158,7 @@ server.tomcat.accesslog.prefix=stdout server.tomcat.accesslog.buffered=false server.tomcat.accesslog.suffix= server.tomcat.accesslog.file-date-format= -server.tomcat.accesslog.pattern={"@timestamp":"%{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}t","level":"ACCESS","level_value":70000,"traceId":"%{X-B3-TraceId}i","statusCode":%s,"req.requestURI":"%U","bytesSent":%b,"timeTaken":%T,"appName":"${spring.application.name}"} +server.tomcat.accesslog.pattern={"@timestamp":"%{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}t","level":"ACCESS","level_value":70000,"traceId":"%{X-B3-TraceId}i","appId":"%{X-AppId}i","statusCode":%s,"req.requestURI":"%U","bytesSent":%b,"timeTaken":%T,"appName":"${spring.application.name}"} server.tomcat.accesslog.className=io.mosip.kernel.core.logger.config.SleuthValve registration.processor.unMaskedUin.length=5 @@ -154,29 +169,110 @@ CREATEDATASHARE=${mosipbox.public.url}/v1/datashare/create DECRYPTPINBASSED=${mosipbox.public.url}/v1/keymanager/decryptWithPin # config.server.file.storage.uri=${spring.cloud.config.uri}/${spring.application.name}/${spring.profiles.active}/${spring.cloud.config.label}/ -config.server.file.storage.uri=${mosipbox.public.url}/config/print/mz/develop2-v2/ +config.server.file.storage.uri=https://raw.githubusercontent.com/mosip/inji-config/develop/ # websub authentication - auth adapter -mosip.iam.adapter.clientid=mpartner-default-mobile -mosip.iam.adapter.clientsecret=abc123 mosip.iam.adapter.appid=partner - -mosip.iam.adapter.issuerURL=${mosipbox.public.url}/keycloak/auth/realms/mosip -mosip.authmanager.base-url=${mosipbox.public.url}/v1/authmanager +mosip.iam.adapter.clientid=mpartner-default-mobile +#dev2 +mosip.iam.adapter.clientsecret=mJQ3g1dPbonDFNW8 +#qa-inji +#mosip.iam.adapter.clientsecret=vl3ox2rvSeW78LzL +auth.server.admin.issuer.uri=${keycloak.external.url}/auth/realms/ + +mosip.iam.adapter.issuerURL=${keycloak.internal.url}/auth/realms/mosip +mosip.authmanager.base-url=${mosip.kernel.authmanager.url}/v1/authmanager mosip.authmanager.client-token-endpoint=${mosip.authmanager.base-url}/authenticate/clientidsecretkey -auth.server.admin.validate.url=${mosip.authmanager.base-url}/v1/authmanager/authorize/admin/validateToken - +auth.server.admin.validate.url=${mosip.kernel.authmanager.url}/v1/authmanager/authorize/admin/validateToken # in minutes mosip.iam.adapter.validate-expiry-check-rate=1440 - # in minutes mosip.iam.adapter.renewal-before-expiry-interval=1440 -#this should be false if you don’t use this restTemplate true if you do - -auth.server.admin.issuer.uri=${mosipbox.public.url}/keycloak/auth/realms/ +#this should be false if you don?t use this restTemplate true if you do mosip.iam.adapter.self-token-renewal-enable=true mosip.auth.filter_disable=false mosip.auth.adapter.impl.basepackage=io.mosip.kernel.auth.defaultadapter mosip.kernel.auth.appids.realm.map={prereg:'mosip',ida:'mosip',registrationclient:'mosip',regproc:'mosip',partner:'mosip',resident:'mosip',admin:'mosip',crereq:'mosip',creser:'mosip',datsha:'mosip',idrepo:'mosip'} +wallet.binding.partner.id=mpartner-default-keybindingtoken +wallet.binding.partner.api.key=Aci9jg28B8mO_LDfDXo3ZTp5_HKgEMun2tYyHCa1e8k +#wallet.binding.partner.id=mpartner-default-mimotobinding +#wallet.binding.partner.api.key=14378 +mosip.iam.adapter.disable-self-token-rest-template=true + +# Mosip notification otp channel config - defines the predefined list of otp channels allowed for sending the OTP to the user +mosip.notificationtype=SMS|EMAIL|PHONE + +# OpenID4VCI related configurations +# Specifies the file name which includes the list of Issuers Configurations, including their well-known endpoints and other details +mosip.openid.issuers=mimoto-issuers-config.json +# Fallback HTML template used to render the Verifiable Credential if no issuer-specific template is found +mosip.openid.htmlTemplate=credential-template.html +# Specifies the type of client assertion used for OIDC authentication with e-signet (e.g., JWT bearer) +mosip.oidc.client.assertion.type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer +# PKCS#12 keystore file name containing secured credentials (e.g., private key and client alias) for each onboarded issuer, used in the authentication flow with e-Signet +mosip.oidc.p12.filename=oidckeystore.p12 +# Password to access the PKCS#12 keystore file +mosip.oidc.p12.password=mosip123 +# Relative path to the directory which contains PKCS#12 keystore file +mosip.oidc.p12.path=certs/ + +# Inji Web Config +# Base URL for Inji Web application +mosip.inji.web.url=http://localhost:3004 +# Inji Web Redirect URL which is used to show the error message in case of failure in OpenID4VP flow +mosip.inji.web.redirect.url=${mosip.inji.web.url}/authorize + +#OpenId4VP related Configuration +# Pattern for including the OpenID4VP Authorization request in the QR code data of the Verifiable Credential, with placeholders for including resource(DataShare URL) and Presentation Definition +mosip.inji.ovp.qrdata.pattern=INJI_OVP://http://localhost:${server.port}/v1/mimoto/authorize?response_type=vp_token&resource=%s&presentation_definition=%s +# Pattern for returning the response in redirect URL for successful Verifiable Presentation in OpenId4VP flow +mosip.inji.ovp.redirect.url.pattern=%s#vp_token=%s&presentation_submission=%s +# Pattern for returning the error in redirect URL in case of Verifiable Presentation failure in OpenId4VP flow +mosip.inji.ovp.error.redirect.url.pattern=%s?error=%s&error_description=%s +# File name containing trusted verifiers for OpenId4VP. It is used for validating the verifiers during the OpenID4VP process +mosip.openid.verifiers=mimoto-trusted-verifiers.json + +#DataShare Config +# Base URL for DataShare Service +mosip.data.share.url=http://localhost:8097 +# Endpoint for creating DataShare requests and storing downloaded Verifiable Credentials +mosip.data.share.create.url=${mosip.data.share.url}/v1/datashare/create/static-policyid/static-subscriberid +# URL pattern used to validate DataShare URL and retrieve Verifiable Credential from DataShare +mosip.data.share.get.url.pattern=${mosip.data.share.url}/v1/datashare/get/static-policyid/static-subscriberid/* +# Specifies the maximum number of retry attempts for creating a DataShare request to store Verifiable Credential, ensuring reliability in case of transient failures +mosip.data.share.create.retry.count=3 + +# This config enables CORS for the application, allowing it to handle cross-origin requests +mosip.security.cors-enable=true +# Predefined list of cross origins which are allowed to access the application resources +mosip.security.origins=http://localhost:3004 + +# Cache related configurations +# Specifies which cache type to use (e.g., caffeine, redis) +spring.cache.type=caffeine + +# Http session cache configuration +# Specifies which session store type to be used for storing http sessions in User Login flow +spring.session.store-type=caffeine + +# Defines the hostname where the database is hosted +inji.mimoto.database.hostname=localhost +# Defines the port number on which database is running and available +inji.mimoto.database.port=5432 + +# Database configuration +# Defines the username for accessing the PostgreSQL database, required for authentication +spring.datasource.username=postgres +# Defines the password for accessing the PostgreSQL database, required for authentication +spring.datasource.password=postgres + +# Key-manager specific properties +# Defines the file path for the PKCS#12 keystore used by the KeyManager service for cryptographic keys +# E.g., place the .p12 file in 'mimoto/certs' folder and provide its path here +mosip.kernel.keymanager.hsm.config-path=certs/oidckeystore.p12 +# Defines the type of the keystore used by the Key Manager as PKCS#12 +mosip.kernel.keymanager.hsm.keystore-type=PKCS12 +# Password to access the KeyManager PKCS#12 keystore file +mosip.kernel.keymanager.hsm.keystore-pass=mosip123 \ No newline at end of file diff --git a/src/main/resources/bootstrap.properties b/src/main/resources/bootstrap.properties index 1cfebbaaa..2daeda44f 100644 --- a/src/main/resources/bootstrap.properties +++ b/src/main/resources/bootstrap.properties @@ -1,12 +1,32 @@ spring.cloud.config.uri=localhost spring.cloud.config.label=develop -spring.profiles.active=local -spring.cloud.config.name=mimoto +spring.profiles.active=default,local +spring.cloud.config.name=mimoto,inji spring.application.name=mimoto +config.server.file.storage.uri=${spring.cloud.config.uri}/print/${spring.profiles.active}/${spring.cloud.config.label}/ + management.endpoint.health.show-details=always management.endpoints.web.exposure.include=info,health,refresh -server.port=8088 +# The port number on which the Mimoto service will be running +server.port=8099 +# Base URL for all the APIs in Mimoto service server.servlet.context-path=/v1/mimoto +# Sets the maximum allowed total size (in bytes) for all HTTP response headers generated by Tomcat, including the Location header for redirects +server.tomcat.max-http-response-header-size=65536 health.config.enabled=false + +openapi.info.title=${spring.application.name} +openapi.info.description=${spring.application.name} +openapi.info.version=1.0 +openapi.info.license.name=Mosip +openapi.info.license.url=https://docs.mosip.io/platform/license +mosipbox.public.url=http://localhost:${server.port} +openapi.service.servers[0].url=${mosipbox.public.url}${server.servlet.context-path} +openapi.service.servers[0].description=${spring.application.name} +openapi.group.name=${openapi.info.title} +openapi.group.paths[0]=/** +springdoc.swagger-ui.disable-swagger-default-url=true +springdoc.swagger-ui.tagsSorter=alpha +springdoc.swagger-ui.operationsSorter=alpha diff --git a/src/main/resources/mimoto-issuers-config.json b/src/main/resources/mimoto-issuers-config.json new file mode 100644 index 000000000..eb612f8f7 --- /dev/null +++ b/src/main/resources/mimoto-issuers-config.json @@ -0,0 +1,31 @@ +{ + "issuers": [ + { + "issuer_id": "Mock", + "display": [ + { + "name": "Mock Identity", + "logo": { + "url": "https://inji.github.io/inji-config/logos/mosipid-logo.png", + "alt_text": "mosip-logo" + }, + "title": "Mock Identity", + "description": "Download Mock Identity Credential", + "language": "en" + } + ], + "protocol": "OpenId4VCI", + "client_id": "wallet-demo", + "client_alias": "wallet-demo-client", + "qr_code_type": "OnlineSharing", + "credential_issuer_host": "https://injicertify-mock.collab.mosip.net", + "wellknown_endpoint": "https://injicertify-mock.collab.mosip.net/v1/certify/issuance/.well-known/openid-credential-issuer", + "redirect_uri": "io.mosip.residentapp.inji://oauthredirect", + "authorization_audience": "https://esignet-mock.collab.mosip.net/v1/esignet/oauth/v2/token", + "token_endpoint": "https://localhost:8099/v1/mimoto/get-token/Mock", + "proxy_token_endpoint": "https://esignet-mock.collab.mosip.net/v1/esignet/oauth/v2/token", + "credential_issuer": "Mock", + "enabled": "true" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/mimoto-trusted-verifiers.json b/src/main/resources/mimoto-trusted-verifiers.json new file mode 100644 index 000000000..52118ed73 --- /dev/null +++ b/src/main/resources/mimoto-trusted-verifiers.json @@ -0,0 +1,16 @@ +{ + "verifiers": [ + { + "client_id": "injiverify.collab.mosip.net", + "redirect_uris": [ + "https://injiverify.collab.mosip.net/" + ], + "response_uris": [ + "https://injiverify.collab.mosip.net/v1/verify/vp-submission/vp-direct-post" + ], + "jwks_uri": "https://injiverify.collab.mosip.net/.well-known/jwks.json", + "allow_unsigned_request": true, + "spec_version": "draft23" + } + ] +} diff --git a/src/main/resources/templates/credential-template.html b/src/main/resources/templates/credential-template.html new file mode 100644 index 000000000..53701081c --- /dev/null +++ b/src/main/resources/templates/credential-template.html @@ -0,0 +1,117 @@ + + + + + Identity Card + + + + + +
+
+ $titleName +
+
+
+ #if (!$face) +
+ Logo +
+ #else + Logo + #end +
+ Logo +
+
+ +
+ #foreach($entry in $rowProperties.entrySet()) ## Iterate over the outer map + #if($entry.value) ## Check if the value exists for this entry + #set($nestedMap = $entry.value) ## Get the nested value + #if($nestedMap && $nestedMap.size() > 0) ## Check if nestedMap is a Map with entries + #foreach($nestedEntry in $nestedMap.entrySet()) ## Iterate over the nested map + #if($nestedEntry.value) ## Ensure that there’s a value for this nested entry +
+
+ $nestedEntry.key +
+ #if($nestedEntry.value.startsWith("data:image/")) + Embedded Visual + #else +
+ $nestedEntry.value +
+ + #end +
+ #end + #end + #else +
+
+ $entry.key +
+ #if($entry.value.startsWith("data:image/")) + Embedded Visual + + #else +
+ $entry.value +
+ #end +
+ #end + #end + #end +
+
+
+ +#if($isSdJwtWithDisclosures) + #if($isMaskedOn) +
+ i +
+ Your card includes specific details marked by the issuer as selectively disclosable. Fields marked (X) are selectively disclosable and can be shared independently during credential presentation. Other fields not listed here can only be disclosed as part of the full credential, if requested by the verifier. +
+
+ #else +
+ i +
+ Your credential includes fields that can be shared selectively. These fields are not marked separately for you to identify visually, but you will see and choose them when presenting this credential to a verifier. +
+
+ #end +#end + +
+ QR Code +
+
+
+ Please note: + + #if ($credentialValidity != -1) + This credential is limited to a maximum of [$credentialValidity] verifications by authorized verifiers. Once the specified number of verifications has been reached, this credential will no longer be valid for further verification attempts. + #else + This credential can be used for verification by authorized verifiers without any limit on the number of verifications. It provides + continuous, secure access for credential validation. + #end + +
+
+ + \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/OAuthLoginTestBootApplication.java b/src/test/java/io/mosip/mimoto/OAuthLoginTestBootApplication.java new file mode 100644 index 000000000..8ca5c56d3 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/OAuthLoginTestBootApplication.java @@ -0,0 +1,2 @@ +package io.mosip.mimoto;public class OAuthLoginTestBootApplication { +} diff --git a/src/test/java/io/mosip/mimoto/ResidentServiceApplicationTests.java b/src/test/java/io/mosip/mimoto/ResidentServiceApplicationTests.java deleted file mode 100644 index 2cdd7f9e1..000000000 --- a/src/test/java/io/mosip/mimoto/ResidentServiceApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.mosip.mimoto; - -// import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ResidentServiceApplicationTests { - - // @Test - // void contextLoads() { - // } - -} diff --git a/src/test/java/io/mosip/mimoto/TestBootApplication.java b/src/test/java/io/mosip/mimoto/TestBootApplication.java new file mode 100644 index 000000000..9d690a1aa --- /dev/null +++ b/src/test/java/io/mosip/mimoto/TestBootApplication.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "io.mosip.mimoto.*") +public class TestBootApplication { + + public static void main(String[] args) { + SpringApplication.run(TestBootApplication.class, args); + } +} diff --git a/src/test/java/io/mosip/mimoto/config/ConfigTest.java b/src/test/java/io/mosip/mimoto/config/ConfigTest.java new file mode 100644 index 000000000..d23ad4af4 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/config/ConfigTest.java @@ -0,0 +1,610 @@ +package io.mosip.mimoto.config; + +import io.mosip.mimoto.security.oauth2.CustomOAuth2UserService; +import io.mosip.mimoto.security.oauth2.OAuth2AuthenticationFailureHandler; +import io.mosip.mimoto.security.oauth2.OAuth2AuthenticationSuccessHandler; +import jakarta.servlet.FilterChain; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.session.SessionRepository; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.cors.CorsConfigurationSource; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.cors.CorsConfiguration; + +import java.util.Collection; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConfigTest { + + @Mock + private CustomOAuth2UserService customOAuth2UserService; + + @Mock + private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; + + @Mock + private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; + + @Mock + private SessionRepository sessionRepository; + + @Mock + private CsrfTokenRepository csrfTokenRepository; + + @Mock + private CsrfToken csrfToken; + + private Config config; + + @BeforeEach + void setUp() { + config = new Config(); + ReflectionTestUtils.setField(config, "baseUrl", "http://localhost:8080"); + ReflectionTestUtils.setField(config, "isCORSEnable", true); + ReflectionTestUtils.setField(config, "origins", "localhost:8088,localhost:3000"); + ReflectionTestUtils.setField(config, "ignoreAuthUrls", new String[]{"/public/**", "/actuator/**"}); + ReflectionTestUtils.setField(config, "csrfIgnoreUrls", new String[]{"/oauth2/**", "/logout"}); + ReflectionTestUtils.setField(config, "injiWebUrl", "http://localhost:3000"); + ReflectionTestUtils.setField(config, "customOAuth2UserService", customOAuth2UserService); + ReflectionTestUtils.setField(config, "oAuth2AuthenticationSuccessHandler", oAuth2AuthenticationSuccessHandler); + ReflectionTestUtils.setField(config, "oAuth2AuthenticationFailureHandler", oAuth2AuthenticationFailureHandler); + } + + @Test + void testInjiConfigBean() { + Map injiConfig = config.injiConfig(); + assertNotNull(injiConfig); + assertInstanceOf(Map.class, injiConfig); + } + + @Test + void testCorsConfigurationSource() { + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + org.springframework.web.cors.CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + // Origins are split by comma - check that both origins are present + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:8088"), "Should contain localhost:8088"); + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:3000"), "Should contain localhost:3000"); + assertEquals(2, corsConfig.getAllowedOrigins().size(), "Should have exactly 2 origins"); + assertTrue(corsConfig.getAllowedMethods().contains("*")); + assertTrue(corsConfig.getAllowedHeaders().contains("*")); + assertTrue(corsConfig.getAllowCredentials()); + } + + @Test + void testCorsConfigurationSourceWithSingleOrigin() { + ReflectionTestUtils.setField(config, "origins", "localhost:8088"); + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + org.springframework.web.cors.CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + assertEquals(1, corsConfig.getAllowedOrigins().size()); + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:8088")); + } + + @Test + void testCsrfTokenCookieFilterGetRequestGeneratesToken() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(null); + when(csrfTokenRepository.generateToken(any())).thenReturn(csrfToken); + + // Create filter instance using reflection + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).generateToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + assertNotNull(request.getAttribute(CsrfToken.class.getName())); + } + + @Test + void testCsrfTokenCookieFilterGetRequestLoadsExistingToken() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestPreventsDuplicateCookie() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", "XSRF-TOKEN=existing-token; Path=/"); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not save again if cookie already exists + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterPostRequestDoesNotInterfere() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + // Simulate CsrfFilter already set the token + request.setAttribute(CsrfToken.class.getName(), csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not process POST requests + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterPutRequestDoesNotInterfere() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("PUT"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not process PUT requests + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterDeleteRequestDoesNotInterfere() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("DELETE"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not process DELETE requests + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterHeadRequestDoesNotProcess() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("HEAD"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - HEAD requests should not be processed (only GET is handled) + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestWithTokenAlreadySet() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.setAttribute(CsrfToken.class.getName(), csrfToken); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should use existing token and save it + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository).saveToken(eq(csrfToken), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestChecksForDuplicateCookie() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", "XSRF-TOKEN=test-token-123; Path=/"); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should detect duplicate and not save again + Collection setCookieHeaders = response.getHeaders("Set-Cookie"); + long xsrfTokenCount = setCookieHeaders.stream().filter(header -> header != null && header.startsWith("XSRF-TOKEN=")).count(); + + assertEquals(1, xsrfTokenCount, "Should have only one XSRF-TOKEN cookie"); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + } + + @Test + void testCsrfTokenCookieFilterGetRequestWithNullCookieHeader() throws Exception { + // Setup - test null cookie header handling + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", null); // Add null header + response.addHeader("Set-Cookie", "OTHER-COOKIE=value; Path=/"); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should handle null cookie header gracefully and save token + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestWithMultipleCookies() throws Exception { + // Setup - multiple cookies but no XSRF-TOKEN + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", "SESSION_ID=abc123; Path=/"); + response.addHeader("Set-Cookie", "OTHER_TOKEN=xyz; Path=/"); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should save token since XSRF-TOKEN is not present + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterPatchRequestDoesNotInterfere() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("PATCH"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not process PATCH requests + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterOptionsRequestDoesNotInterfere() throws Exception { + // Setup + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("OPTIONS"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should not process OPTIONS requests + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestLowercaseMethod() throws Exception { + // Setup - test case-insensitive method check + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("get"); // lowercase + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(null); + when(csrfTokenRepository.generateToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should process lowercase GET + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).generateToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCorsConfigurationSourceWithEmptyOrigins() { + ReflectionTestUtils.setField(config, "origins", ""); + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + // Empty string split by comma results in array with one empty string + assertTrue(corsConfig.getAllowedOrigins().contains("")); + } + + @Test + void testCorsConfigurationSourceWithWhitespaceInOrigins() { + ReflectionTestUtils.setField(config, "origins", "localhost:8088 , localhost:3000"); + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + // Should contain origins with whitespace (not trimmed) + assertTrue(corsConfig.getAllowedOrigins().size() >= 2); + } + + @Test + void testCsrfTokenCookieFilterGetRequestCookieHeaderWithNullValue() throws Exception { + // Setup - test null handling in cookie header loop + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + // Add a null header value + response.addHeader("Set-Cookie", null); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should handle null gracefully and save token + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestMultipleXSRFTokenCookies() throws Exception { + // Setup - multiple XSRF-TOKEN cookies (edge case) + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", "XSRF-TOKEN=token1; Path=/"); + response.addHeader("Set-Cookie", "XSRF-TOKEN=token2; Path=/"); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should detect duplicate and not save again + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository, never()).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestWithCookieNotStartingWithXSRF() throws Exception { + // Setup - cookie header that contains XSRF but doesn't start with it + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader("Set-Cookie", "OTHER=XSRF-TOKEN=value; Path=/"); // Contains but doesn't start with + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should save token since XSRF-TOKEN doesn't start the cookie header + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestWithEmptyCookieHeaders() throws Exception { + // Setup - no cookie headers at all + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should save token since no cookies exist + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCsrfTokenCookieFilterGetRequestTokenSetButCookieNotSet() throws Exception { + // Setup - token already set in request but cookie not in response + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.setAttribute(CsrfToken.class.getName(), csrfToken); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should save token since cookie is not already set + verify(csrfTokenRepository, never()).loadToken(any()); + verify(csrfTokenRepository, never()).generateToken(any()); + verify(csrfTokenRepository).saveToken(eq(csrfToken), any(), any()); + verify(filterChain).doFilter(request, response); + } + + @Test + void testCorsConfigurationSourceWithThreeOrigins() { + ReflectionTestUtils.setField(config, "origins", "localhost:8088,localhost:3000,localhost:9000"); + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + assertEquals(3, corsConfig.getAllowedOrigins().size()); + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:8088")); + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:3000")); + assertTrue(corsConfig.getAllowedOrigins().contains("localhost:9000")); + } + + @Test + void testCorsConfigurationSourceWithSingleComma() { + ReflectionTestUtils.setField(config, "origins", ","); + CorsConfigurationSource corsConfigurationSource = config.corsConfigurationSource(); + assertNotNull(corsConfigurationSource); + + HttpServletRequest request = new MockHttpServletRequest(); + CorsConfiguration corsConfig = corsConfigurationSource.getCorsConfiguration(request); + assertNotNull(corsConfig); + assertEquals(0, corsConfig.getAllowedOrigins().size()); + } + + @Test + void testInjiConfigBeanIsEmptyMap() { + Map injiConfig = config.injiConfig(); + assertNotNull(injiConfig); + assertTrue(injiConfig.isEmpty()); // Should be empty initially + } + + @Test + void testCsrfTokenCookieFilterGetRequestCaseInsensitiveMethod() throws Exception { + // Setup - test case-insensitive GET method + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GeT"); // Mixed case + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + + when(csrfTokenRepository.loadToken(any())).thenReturn(null); + when(csrfTokenRepository.generateToken(any())).thenReturn(csrfToken); + + Config.CsrfTokenCookieFilter filter = createFilterInstance(); + + // Execute + filter.doFilterInternal(request, response, filterChain); + + // Verify - should process mixed case GET + verify(csrfTokenRepository).loadToken(any()); + verify(csrfTokenRepository).generateToken(any()); + verify(csrfTokenRepository).saveToken(any(), any(), any()); + verify(filterChain).doFilter(request, response); + } + + /** + * Helper method to create filter instance + */ + private Config.CsrfTokenCookieFilter createFilterInstance() { + return new Config.CsrfTokenCookieFilter(csrfTokenRepository); + } +} + diff --git a/src/test/java/io/mosip/mimoto/config/InjiConfiguration.java b/src/test/java/io/mosip/mimoto/config/InjiConfiguration.java new file mode 100644 index 000000000..a0332f0fa --- /dev/null +++ b/src/test/java/io/mosip/mimoto/config/InjiConfiguration.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class InjiConfiguration { + @Bean + @ConfigurationProperties(prefix = "mosip.inji") + public Map injiConfig() { + return new HashMap<>(); + } + +} diff --git a/src/test/java/io/mosip/mimoto/config/IssuersValidationConfigTest.java b/src/test/java/io/mosip/mimoto/config/IssuersValidationConfigTest.java new file mode 100644 index 000000000..009c3752b --- /dev/null +++ b/src/test/java/io/mosip/mimoto/config/IssuersValidationConfigTest.java @@ -0,0 +1,105 @@ +package io.mosip.mimoto.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.service.impl.IssuersServiceImpl; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import java.util.List; + +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {IssuersValidationConfig.class, LocalValidatorFactoryBean.class, ObjectMapper.class}) +public class IssuersValidationConfigTest { + + @Autowired + private IssuersValidationConfig issuersValidationConfig; + + @MockBean + private IssuersServiceImpl issuersService; + + IssuersDTO issuers = new IssuersDTO(); + + private static final String VALIDATION_ERROR_MSG = "\n\nValidation failed in Mimoto-issuers-config.json:"; + + @Test + public void shouldNotThrowAnyExceptionForValidIssuersConfig() { + try { + issuers.setIssuers(List.of(getIssuerConfigDTO("Issuer8"), getIssuerConfigDTO("Issuer7"))); + when(issuersService.getAllIssuers()).thenReturn(issuers); + + issuersValidationConfig.run(mock(ApplicationArguments.class)); + } catch (Exception e) { + Assert.fail("Exception message: " + e.getMessage()); + } + } + + @Test + public void shouldNotValidateIssuersWithProtocolOtp() { + try { + IssuerDTO otpIssuer = getIssuerConfigDTO("OtpIssuer"); + otpIssuer.setProtocol("OTP"); + issuers.setIssuers(List.of(otpIssuer)); + when(issuersService.getAllIssuers()).thenReturn(issuers); + + issuersValidationConfig.run(mock(ApplicationArguments.class)); + } catch (Exception e) { + Assert.fail("OTP protocol issuers should be skipped from validation; got: " + e.getMessage()); + } + } + + @Test + public void shouldThrowRuntimeExceptionWhenGetAllIssuersThrows() throws Exception { + when(issuersService.getAllIssuers()).thenThrow(new RuntimeException("config unavailable")); + + RuntimeException thrown = Assert.assertThrows(RuntimeException.class, + () -> issuersValidationConfig.run(mock(ApplicationArguments.class))); + + assertEquals(VALIDATION_ERROR_MSG, thrown.getMessage()); + assertNotNull(thrown.getCause()); + assertEquals("config unavailable", thrown.getCause().getMessage()); + } + + @Test + public void shouldThrowExceptionIfTheFieldValuesOfIssuerAreNotSatisfyingNotBlankAnnotation() { + try { + issuers.setIssuers(List.of(getIssuerConfigDTOWithInvalidFieldValues("Issuer1", true, false), getIssuerConfigDTOWithInvalidFieldValues("Issuer2", true, false))); + when(issuersService.getAllIssuers()).thenReturn(issuers); + + issuersValidationConfig.run(mock(ApplicationArguments.class)); + } catch (Exception exception) { + String expectedErrorMsg = "\n\nValidation failed in Mimoto-issuers-config.json:\nErrors for issuer at index: 0 with issuerId - \n- client_alias must not be blank\n- client_id must not be blank\n- credential_issuer_host must not be blank\n- display[0].description must not be blank\n- display[0].language must not be blank\n- display[0].logo.url must be a valid URL\n- display[0].name must not be blank\n- display[0].title must not be blank\n- enabled must not be blank\n- issuer_id must not be blank\n- protocol must not be blank\nErrors for issuer at index: 1 with issuerId - \n- client_alias must not be blank\n- client_id must not be blank\n- credential_issuer_host must not be blank\n- display[0].description must not be blank\n- display[0].language must not be blank\n- display[0].logo.url must be a valid URL\n- display[0].name must not be blank\n- display[0].title must not be blank\n- enabled must not be blank\n- issuer_id must not be blank\n- protocol must not be blank\n- Duplicate value found for the issuerId. More than one issuer is having the same issuerId\n"; + String actualErrorMsg = exception.getMessage(); + + assertEquals(expectedErrorMsg, actualErrorMsg); + } + } + + @Test + public void shouldThrowExceptionIfTheFieldValuesOfIssuerAreNotSatisfyingUrlAnnotation() { + try { + issuers.setIssuers(List.of(getIssuerConfigDTOWithInvalidFieldValues("Issuer1", false, true), getIssuerConfigDTOWithInvalidFieldValues("Issuer2", false, true))); + when(issuersService.getAllIssuers()).thenReturn(issuers); + + issuersValidationConfig.run(mock(ApplicationArguments.class)); + } catch (Exception exception) { + String expectedErrorMsg = "\n\nValidation failed in Mimoto-issuers-config.json:\nErrors for issuer at index: 0 with issuerId - Issuer1id\n- credential_issuer_host must be a valid URL\n- token_endpoint must be a valid URL\nErrors for issuer at index: 1 with issuerId - Issuer2id\n- credential_issuer_host must be a valid URL\n- token_endpoint must be a valid URL\n"; + String actualErrorMsg = exception.getMessage(); + + assertEquals(expectedErrorMsg, actualErrorMsg); + } + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/config/MimotoConfig.java b/src/test/java/io/mosip/mimoto/config/MimotoConfig.java new file mode 100644 index 000000000..0a127fdce --- /dev/null +++ b/src/test/java/io/mosip/mimoto/config/MimotoConfig.java @@ -0,0 +1,103 @@ +package io.mosip.mimoto.config; + +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.socket.ConnectionSocketFactory; +import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.*; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.SSLContext; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + + +@Configuration +@ComponentScan(basePackages = { + "io.mosip.mimoto.*" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = { + Config.class, OpenApiProperties.class, SwaggerConfig.class})) +public class MimotoConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable); + http.headers(headersEntry -> { + headersEntry.cacheControl(Customizer.withDefaults()); + headersEntry.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin); + }); + + return http.build(); + } + + @Bean + public RestTemplate selfTokenRestTemplate() + throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + + final RestTemplate restTemplate = new RestTemplate(); + + SSLContext sslContext = SSLContextBuilder.create() + .loadTrustMaterial((X509Certificate[] certificateChain, String authType) -> true) // <--- accepts each certificate + .build(); + + Registry socketRegistry = RegistryBuilder.create() + .register(URIScheme.HTTPS.getId(), new SSLConnectionSocketFactory(sslContext)) + .register(URIScheme.HTTP.getId(), new PlainConnectionSocketFactory()) + .build(); + + HttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(new PoolingHttpClientConnectionManager(socketRegistry)) + .setConnectionManagerShared(true) + .build(); + + ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + restTemplate.setRequestFactory(requestFactory); + return restTemplate; + + } + + @Bean + public RestTemplate plainRestTemplate() + throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + + final RestTemplate restTemplate = new RestTemplate(); + + SSLContext sslContext = SSLContextBuilder.create() + .loadTrustMaterial((X509Certificate[] certificateChain, String authType) -> true) // <--- accepts each certificate + .build(); + + Registry socketRegistry = RegistryBuilder.create() + .register(URIScheme.HTTPS.getId(), new SSLConnectionSocketFactory(sslContext)) + .register(URIScheme.HTTP.getId(), new PlainConnectionSocketFactory()) + .build(); + + HttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(new PoolingHttpClientConnectionManager(socketRegistry)) + .setConnectionManagerShared(true) + .build(); + + ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + restTemplate.setRequestFactory(requestFactory); + return restTemplate; + + } + +} diff --git a/src/test/java/io/mosip/mimoto/config/WalletPasscodeConfigTest.java b/src/test/java/io/mosip/mimoto/config/WalletPasscodeConfigTest.java new file mode 100644 index 000000000..30941d38c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/config/WalletPasscodeConfigTest.java @@ -0,0 +1,8 @@ +package io.mosip.mimoto.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(WalletPasscodeConfig.class) +public class WalletPasscodeConfigTest {} diff --git a/src/test/java/io/mosip/mimoto/constant/CredentialFormatTest.java b/src/test/java/io/mosip/mimoto/constant/CredentialFormatTest.java new file mode 100644 index 000000000..680440b76 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/constant/CredentialFormatTest.java @@ -0,0 +1,64 @@ +package io.mosip.mimoto.constant; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CredentialFormatTest { + + @Test + void shouldReturnCorrectFormatStringForEachValue() { + assertEquals("vc+sd-jwt", CredentialFormat.VC_SD_JWT.getFormat()); + assertEquals("dc+sd-jwt", CredentialFormat.DC_SD_JWT.getFormat()); + assertEquals("ldp_vc", CredentialFormat.LDP_VC.getFormat()); + } + + @Test + void shouldHaveThreeEnumValues() { + assertEquals(3, CredentialFormat.values().length); + } + + @Test + void fromStringShouldResolveEachFormatCaseInsensitively() { + assertEquals(CredentialFormat.VC_SD_JWT, CredentialFormat.fromString("vc+sd-jwt")); + assertEquals(CredentialFormat.DC_SD_JWT, CredentialFormat.fromString("dc+sd-jwt")); + assertEquals(CredentialFormat.LDP_VC, CredentialFormat.fromString("ldp_vc")); + // case-insensitive + assertEquals(CredentialFormat.VC_SD_JWT, CredentialFormat.fromString("VC+SD-JWT")); + assertEquals(CredentialFormat.DC_SD_JWT, CredentialFormat.fromString("DC+SD-JWT")); + } + + @Test + void fromStringShouldThrowForUnknownFormat() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> CredentialFormat.fromString("unknown_format")); + assertEquals("Unknown credential format: unknown_format", exception.getMessage()); + } + + @Test + void isSdJwtShouldBeTrueForBothSdJwtFormats() { + assertTrue(CredentialFormat.isSdJwt("vc+sd-jwt")); + assertTrue(CredentialFormat.isSdJwt("dc+sd-jwt")); + // case-insensitive + assertTrue(CredentialFormat.isSdJwt("VC+SD-JWT")); + assertTrue(CredentialFormat.isSdJwt("DC+SD-JWT")); + } + + @Test + void isSdJwtShouldBeFalseForLdpVc() { + assertFalse(CredentialFormat.isSdJwt("ldp_vc")); + } + + @Test + void isSdJwtShouldBeFalseForNull() { + assertFalse(CredentialFormat.isSdJwt(null)); + } + + @Test + void isSdJwtShouldBeFalseForUnknownFormat() { + assertFalse(CredentialFormat.isSdJwt("mso_mdoc")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/constant/VCSpecificationVersionTest.java b/src/test/java/io/mosip/mimoto/constant/VCSpecificationVersionTest.java new file mode 100644 index 000000000..78077399c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/constant/VCSpecificationVersionTest.java @@ -0,0 +1,29 @@ +package io.mosip.mimoto.constant; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VCSpecificationVersionTest { + + @Test + void shouldReturnCorrectVersionStringForDraft13() { + assertEquals("draft-13", VCSpecificationVersion.DRAFT_13.getVersion()); + } + + @Test + void shouldReturnCorrectVersionStringForV1() { + assertEquals("v1", VCSpecificationVersion.V1.getVersion()); + } + + @Test + void shouldHaveTwoEnumValues() { + assertEquals(2, VCSpecificationVersion.values().length); + } + + @Test + void shouldResolveFromName() { + assertEquals(VCSpecificationVersion.DRAFT_13, VCSpecificationVersion.valueOf("DRAFT_13")); + assertEquals(VCSpecificationVersion.V1, VCSpecificationVersion.valueOf("V1")); + } +} diff --git a/src/test/java/io/mosip/mimoto/controller/AttestationServiceControllerTest.java b/src/test/java/io/mosip/mimoto/controller/AttestationServiceControllerTest.java new file mode 100644 index 000000000..8dbe6a02b --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/AttestationServiceControllerTest.java @@ -0,0 +1,74 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.dto.mimoto.AttestationStatement; +import io.mosip.mimoto.util.AttestationOfflineVerify; +import io.mosip.mimoto.util.AttestationOnlineVerify; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AttestationServiceController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class AttestationServiceControllerTest { + + + @MockBean + private AttestationOfflineVerify attestationOfflineVerify; + + @MockBean + private AttestationOnlineVerify attestationOnlineVerify; + + @Autowired + private MockMvc mockMvc; + + @Test + public void processOfflineTest() throws Exception { + AttestationStatement attestationStatement = new AttestationStatement(); + Mockito.when(attestationOfflineVerify.parseAndVerify(Mockito.anyString())).thenReturn(attestationStatement); + this.mockMvc.perform(post("/safetynet/offline/verify").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("offlineStr")) + .andExpect(status().isOk()); + } + + @Test + public void processOfflineFailureTest() throws Exception { + AttestationStatement attestationStatement = new AttestationStatement(); + Mockito.when(attestationOfflineVerify.parseAndVerify(Mockito.anyString())).thenThrow(new Exception("Exception")); + this.mockMvc.perform(post("/safetynet/offline/verify").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("offlineStr")) + .andExpect(status().isOk()); + } + + @Test + public void processOnlineTest() throws Exception { + AttestationStatement attestationStatement = new AttestationStatement(); + Mockito.when(attestationOnlineVerify.parseAndVerify(Mockito.anyString())).thenReturn(attestationStatement); + this.mockMvc.perform(post("/safetynet/online/verify").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("offlineStr")) + .andExpect(status().isOk()); + } + + @Test + public void processOnlineFailureTest() throws Exception { + AttestationStatement attestationStatement = new AttestationStatement(); + Mockito.when(attestationOnlineVerify.parseAndVerify(Mockito.anyString())).thenThrow(new Exception("Exception")); + this.mockMvc.perform(post("/safetynet/online/verify").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("offlineStr")) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/CredentialShareControllerTest.java b/src/test/java/io/mosip/mimoto/controller/CredentialShareControllerTest.java new file mode 100644 index 000000000..f5fb3cf80 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/CredentialShareControllerTest.java @@ -0,0 +1,152 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.mosip.kernel.core.util.JsonUtils; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.mimoto.AppCredentialRequestDTO; +import io.mosip.mimoto.dto.mimoto.CredentialDownloadRequestDTO; +import io.mosip.mimoto.dto.resident.CredentialRequestResponseDTO; +import io.mosip.mimoto.dto.resident.CredentialRequestResponseInnerResponseDTO; +import io.mosip.mimoto.dto.resident.CredentialRequestStatusResponseDTO; +import io.mosip.mimoto.model.Event; +import io.mosip.mimoto.model.EventModel; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.service.impl.CredentialShareServiceImpl; +import io.mosip.mimoto.util.RequestValidator; +import io.mosip.mimoto.util.RestApiClient; +import io.mosip.mimoto.util.Utilities; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.IOException; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CredentialShareController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class CredentialShareControllerTest { + + + @Autowired + private MockMvc mockMvc; + + @MockBean + public RestApiClient restApiClient; + + + @MockBean + public RestClientService restClientService; + + @MockBean + private CredentialShareServiceImpl credentialShareService; + + @MockBean + Utilities utilities; + + @MockBean + RequestValidator requestValidator; + + @Before + public void setup() { + Mockito.when(utilities.getDataPath()).thenReturn("target"); + } + + @Test + public void handleSubscribeEventTest() throws Exception { + Event event = new Event(); + event.setId("id"); + event.setTransactionId("transId"); + EventModel eventModel = new EventModel(); + eventModel.setEvent(event); + + Mockito.when(credentialShareService.generateDocuments(Mockito.any())).thenReturn(true); + + this.mockMvc.perform(post("/credentialshare/callback/notify").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(eventModel))) + .andExpect(status().isOk()); + } + + @Test + public void requestTest() throws Exception { + AppCredentialRequestDTO requestDTO = new AppCredentialRequestDTO(); + requestDTO.setUser("mono"); + requestDTO.setTransactionID("transactionId"); + requestDTO.setOtp("123456"); + requestDTO.setIndividualId("123456"); + requestDTO.setCredentialType("VERCRED"); + + CredentialRequestResponseInnerResponseDTO dto = new CredentialRequestResponseInnerResponseDTO(); + dto.setRequestId("requestId"); + CredentialRequestResponseDTO response = new CredentialRequestResponseDTO(); + response.setResponse(dto); + + Mockito.when(restClientService.postApi(Mockito.any(), Mockito.anyString(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + Mockito.when(credentialShareService.generateDocuments(Mockito.any())).thenReturn(true); + + this.mockMvc.perform(post("/credentialshare/request").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void requestStatusTest() throws Exception { + + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), Mockito.anyString(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(responseWrapper); + + this.mockMvc.perform(get("/credentialshare/request/status/1234567890").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()); + } + + @Test + public void downloadTest() throws Exception { + + CredentialDownloadRequestDTO requestDTO = new CredentialDownloadRequestDTO(); + requestDTO.setRequestId("requestId"); + requestDTO.setIndividualId("individualId"); + + JsonNode credentialJSON = JsonNodeFactory.instance.objectNode(); + Mockito.when(utilities.getVC(Mockito.anyString())).thenReturn(credentialJSON); + + JsonNode decryptedCredentialJSON = JsonNodeFactory.instance.objectNode(); + Mockito.when(utilities.getDecryptedVC(Mockito.anyString())).thenReturn(decryptedCredentialJSON); + + JsonNode requestCredentialJSON = JsonNodeFactory.instance.objectNode(); + Mockito.when(utilities.getRequestVC(Mockito.anyString())).thenReturn(requestCredentialJSON); + + this.mockMvc.perform(post("/credentialshare/download").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + + Mockito.when(utilities.getDecryptedVC(Mockito.anyString())).thenReturn(null); + + this.mockMvc.perform(post("/credentialshare/download").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isNotFound()); + + Mockito.when(utilities.getDecryptedVC(Mockito.anyString())).thenThrow(new IOException("Exception")); + + this.mockMvc.perform(post("/credentialshare/download").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isNotFound()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/CredentialsControllerTest.java b/src/test/java/io/mosip/mimoto/controller/CredentialsControllerTest.java new file mode 100644 index 000000000..8ee0b89c2 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/CredentialsControllerTest.java @@ -0,0 +1,145 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.service.impl.CredentialServiceImpl; +import io.mosip.mimoto.util.TestUtilities; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.ByteArrayInputStream; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CredentialsController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class CredentialsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private CredentialServiceImpl credentialService; + + @MockBean + private IdpService idpService; + + private String locale = "test-local", issuer = "test-issuer", credential = "test-credential", requestContent; + private TokenResponseDTO tokenResponseDTO; + + @Before + public void setUp() throws Exception { + tokenResponseDTO = TestUtilities.getTokenResponseDTO(); + Mockito.when(idpService.getTokenResponse(Mockito.anyMap())).thenReturn(tokenResponseDTO); + requestContent = EntityUtils.toString(new UrlEncodedFormEntity(List.of( + new BasicNameValuePair("grant_type", "authorization_code"), + new BasicNameValuePair("code", "test-code"), + new BasicNameValuePair("redirect_uri", "test-redirect_uri"), + new BasicNameValuePair("code_verifier", "test-code_verifier"), + new BasicNameValuePair("issuer", issuer), + new BasicNameValuePair("vcStorageExpiryLimitInTimes", "3"), + new BasicNameValuePair("credential", credential), + new BasicNameValuePair("locale", locale) + ))); + } + + @Test + public void downloadPDFSuccessfully() throws Exception { + Mockito.when(credentialService.downloadCredentialAsPDF(issuer, credential, tokenResponseDTO, "3", locale)) + .thenReturn(new ByteArrayInputStream("test-data".getBytes())); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_PDF)); + } + + @Test + public void throwExceptionOnFetchingTokenResponseFailure() throws Exception { + Mockito.when(idpService.getTokenResponse(Mockito.anyMap())) + .thenThrow(new IdpException("Exception occurred while performing the authorization")); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is("RESIDENT-APP-034"))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is("Exception occurred while performing the authorization"))); + + } + + @Test + public void throwExceptionOnFetchingIssuerOrAuthServerWellknownFailureDuringTokenGeneration() throws Exception { + Mockito.when(idpService.getTokenResponse(Mockito.anyMap())) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is("RESIDENT-APP-026"))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is("Api not accessible failure"))); + } + + + @Test + public void throwExceptionWhenPDFGenerationFailed() throws Exception { + Mockito.when(credentialService.downloadCredentialAsPDF(issuer, credential, tokenResponseDTO, "3", locale)) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is("RESIDENT-APP-026"))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is("Api not accessible failure"))); + } + + @Test + public void throwExceptionOnInvalidCredentialResource() throws Exception { + Mockito.when(credentialService.downloadCredentialAsPDF(issuer, credential, tokenResponseDTO, "3", locale)) + .thenThrow(new InvalidCredentialResourceException( + ErrorConstants.REQUEST_TIMED_OUT.getErrorCode(), + ErrorConstants.REQUEST_TIMED_OUT.getErrorMessage())); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is("request_timed_out"))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is("We are unable to process your request right now"))); + } + + @Test + public void throwExceptionOnVCVerificationFailure() throws Exception { + Mockito.when(credentialService.downloadCredentialAsPDF(issuer, credential, tokenResponseDTO, "3", locale)) + .thenThrow(new VCVerificationException("Verification Failed!", "Error occurred when verifying the downloaded credential")); + + mockMvc.perform(post("/credentials/download") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(requestContent)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is("Verification Failed!"))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is("Error occurred when verifying the downloaded credential"))); + } +} diff --git a/src/test/java/io/mosip/mimoto/controller/IdpControllerTest.java b/src/test/java/io/mosip/mimoto/controller/IdpControllerTest.java new file mode 100644 index 000000000..5fb7fa58e --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/IdpControllerTest.java @@ -0,0 +1,257 @@ +package io.mosip.mimoto.controller; + +import com.google.common.collect.Lists; +import io.mosip.kernel.core.util.JsonUtils; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.BaseUncheckedException; +import io.mosip.mimoto.exception.IdpException; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.service.impl.IdpServiceImpl; +import io.mosip.mimoto.util.*; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.Arrays; + +import static io.mosip.mimoto.util.TestUtilities.getTokenResponseDTO; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import io.mosip.mimoto.exception.InvalidRequestException; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = IdpController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class IdpControllerTest { + + + @Autowired + private MockMvc mockMvc; + + @MockBean + public RestApiClient restApiClient; + + @MockBean + private JoseUtil joseUtil; + + @MockBean + RequestValidator requestValidator; + + @MockBean + public RestClientService restClientService; + + @MockBean + private IdpServiceImpl idpService; + + @Test + public void otpRequestTest() throws Exception { + BindingOtpInnerReqDto innerReqDto = new BindingOtpInnerReqDto(); + innerReqDto.setIndividualId("individualId"); + innerReqDto.setOtpChannels(Lists.newArrayList("EMAIL")); + BindingOtpRequestDto requestDTO = new BindingOtpRequestDto(DateUtils.getUTCCurrentDateTimeString(), innerReqDto); + + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(response).thenReturn(null).thenThrow(new BaseUncheckedException("Exception")); + + this.mockMvc.perform(post("/binding-otp").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + + + this.mockMvc.perform(post("/binding-otp").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode").value("RESIDENT-APP-034")) + .andExpect(jsonPath("$.errors[0].errorMessage").value("Could not get response from server")); + + this.mockMvc.perform(post("/binding-otp").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isBadRequest()); + } + + @Test + public void walletBindingTest() throws Exception { + WalletBindingInnerReq innerReqDto = new WalletBindingInnerReq(); + innerReqDto.setPublicKey("-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAn6+frMlD7DQqbxZW943hRLBApDj1/lHIJdLYSKEGIfwhd58gc0Y4\n1q11mPnpv7gAZ/Wm0iOAkWSzcIWljXFmGnLrUrBsp4WYKdPjqn4tkrCOjiZa5RPk\nY03a40Kz1lx0W9f94Naozglf6KFUSq+qAwuC5kiPxaxsjFA/LWIP+zT2QX/MnrX9\nv7gt2g0BC4pQ01eTTzhhwO2A7k5z3ucsb56ohND4xdIsdCMm1IczBjW0URSO60Bb\n7m5dlO8BFHJ6inV8awO2KHoADbp3wZgid4KqLJ0eVGyNViVFzj4rxSxL3vcYbyKS\nORWSlPZIZL9ZWO1cyPO9+Wxu29IKj4DQEt8glgITlBZ4L29uT7gFPAbypSn/8SvU\nBrNno8+GIe9XWsrDTMT9dfLGzLUitF3A+wwVZuRVhCqYIisOOGuGE18YK0jmdk9l\n89OpK4PduGiUh66zZTcH3thdtaOz6jj+FLKMg2Q3gNqQ1Y0cezO175RNVVX1ffOu\n5qss1RWams5RAXDqqt/MhiopG3DhlyaSC4xdqei7SI8d+S4Bvflub9rypPnhW67g\nNhZvQDJ7Tb1AWHxKmU0wQvEMtwSm9xtsMs4bqotn2M/09BuRqbrhpvAfrfZArkVO\nv8eLXhtDvo2J9gRwHZIS/JZ1Fo+tep1QFHz1Lr5iGRqwLWQlGbKFuL0CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"); + innerReqDto.setIndividualId("individualId"); + WalletBindingRequestDTO requestDTO = new WalletBindingRequestDTO(); + requestDTO.setRequestTime(DateUtils.getUTCCurrentDateTimeString()); + requestDTO.setRequest(innerReqDto); + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(response).thenReturn(null).thenThrow(new BaseUncheckedException("Exception")); + + this.mockMvc.perform(post("/wallet-binding").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + + this.mockMvc.perform(post("/wallet-binding").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode").value("RESIDENT-APP-034")) + .andExpect(jsonPath("$.errors[0].errorMessage").value("Could not get response from server")); + + this.mockMvc.perform(post("/wallet-binding").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnTokenResponseForValidIssuerAndParams() throws Exception { + String issuer = "test-issuer"; + Mockito.when(idpService.getTokenResponse(Mockito.anyMap())).thenReturn(getTokenResponseDTO()); + + mockMvc.perform(post("/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(EntityUtils.toString(new UrlEncodedFormEntity(Arrays.asList( + new BasicNameValuePair("grant_type", "authorization_code"), + new BasicNameValuePair("code", "test-code"), + new BasicNameValuePair("redirect_uri", "test-redirect_uri"), + new BasicNameValuePair("code_verifier", "test-code_verifier"), + new BasicNameValuePair("issuer", issuer), + new BasicNameValuePair("vcStorageExpiryLimitInTimes", "3"), + new BasicNameValuePair("credential", "test-credential"), + new BasicNameValuePair("locale", "test-locale") + ))))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id_token").value("test-id-token")) + .andExpect(jsonPath("$.access_token").value("test-accesstoken")) + .andExpect(jsonPath("$.expires_in").value(12345)) + .andExpect(jsonPath("$.scope").value("test-scope")) + .andExpect(jsonPath("$.token_type").value("test-token-type")); + } + + @Test + public void shouldReturnBadRequestWithErrorIfTokenResponseIsNull() throws Exception { + String issuer = "test-issuer"; + Mockito.when(idpService.getTokenResponse(Mockito.anyMap())) + .thenThrow(new IdpException("Exception occurred while performing the authorization")); + + mockMvc.perform(post("/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", "authorization_code") + .param("code", "test-code") + .param("redirect_uri", "test-redirect_uri") + .param("code_verifier", "test-code_verifier") + .param("issuer", issuer) + .param("vcStorageExpiryLimitInTimes", "3") + .param("credential", "test-credential") + .param("locale", "test-locale")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode").value("RESIDENT-APP-034")) + .andExpect(jsonPath("$.errors[0].errorMessage").value("Exception occurred while performing the authorization")); + } + + @Test + public void shouldReturnTokenResponseForV2ValidIssuerAndParams() throws Exception { + String issuer = "test-issuer"; + String responseBody = "{\"access_token\":\"test-access-token\",\"token_type\":\"DPoP\",\"expires_in\":12345}"; + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add("DPoP-Nonce", "test-nonce"); + + Mockito.when(idpService.getTokenResponseV2(Mockito.anyMap(), Mockito.eq("test-dpop-proof"))) + .thenReturn(ResponseEntity.ok() + .headers(responseHeaders) + .contentType(MediaType.APPLICATION_JSON) + .body(responseBody)); + + mockMvc.perform(post("/v2/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header("DPoP", "test-dpop-proof") + .param("grant_type", "authorization_code") + .param("code", "test-code") + .param("redirect_uri", "test-redirect_uri") + .param("code_verifier", "test-code_verifier")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.access_token").value("test-access-token")) + .andExpect(jsonPath("$.token_type").value("DPoP")) + .andExpect(jsonPath("$.expires_in").value(12345)) + .andExpect(header().string("DPoP-Nonce", "test-nonce")); + } + + @Test + public void shouldReturnAuthorizationServerErrorAsIsForV2() throws Exception { + String issuer = "test-issuer"; + String errorBody = "{\"error\":\"use_dpop_nonce\",\"error_description\":\"DPoP nonce required\"}"; + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add("DPoP-Nonce", "server-nonce-123"); + + Mockito.when(idpService.getTokenResponseV2(Mockito.anyMap(), Mockito.eq("stale-dpop-proof"))) + .thenReturn(ResponseEntity.status(401) + .headers(responseHeaders) + .contentType(MediaType.APPLICATION_JSON) + .body(errorBody)); + + mockMvc.perform(post("/v2/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header("DPoP", "stale-dpop-proof") + .param("grant_type", "authorization_code") + .param("code", "test-code") + .param("redirect_uri", "test-redirect_uri") + .param("code_verifier", "test-code_verifier")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string("DPoP-Nonce", "server-nonce-123")) + .andExpect(jsonPath("$.error").value("use_dpop_nonce")) + .andExpect(jsonPath("$.error_description").value("DPoP nonce required")); + } + + @Test + public void shouldReturnInternalServerErrorForV2InternalException() throws Exception { + String issuer = "test-issuer"; + + Mockito.when(idpService.getTokenResponseV2(Mockito.anyMap(), Mockito.any())) + .thenThrow(new RuntimeException("Unexpected error")); + + mockMvc.perform(post("/v2/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", "authorization_code") + .param("code", "test-code") + .param("redirect_uri", "test-redirect_uri") + .param("code_verifier", "test-code_verifier")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.error_description").exists()); + } + @Test + public void shouldReturnBadRequestForV2InvalidRequestException() throws Exception { + String issuer = "test-issuer"; + + Mockito.when(idpService.getTokenResponseV2(Mockito.anyMap(), Mockito.any())) + .thenThrow(new InvalidRequestException("invalid_request", "Invalid issuer")); + + mockMvc.perform(post("/v2/get-token/{issuer}", issuer) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", "authorization_code") + .param("code", "test-code") + .param("redirect_uri", "test-redirect_uri") + .param("code_verifier", "test-code_verifier")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").exists()) + .andExpect(jsonPath("$.error_description").exists()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java b/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java new file mode 100644 index 000000000..551f33ae7 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java @@ -0,0 +1,48 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.config.InjiConfiguration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.ResourceUtils; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.File; +import java.nio.file.Files; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {CommonInjiController.class, InjiConfiguration.class}) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class InjiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void getAllPropertiesTest() throws Exception { + File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "responses/InjiConfig.json"); + String expectedResponse = new String(Files.readAllBytes(file.toPath())); + + String actualResponse = this.mockMvc.perform(get("/allProperties") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JSONAssert.assertEquals(expectedResponse, actualResponse, JSONCompareMode.LENIENT); + + } + +} diff --git a/src/test/java/io/mosip/mimoto/controller/IssuersControllerTest.java b/src/test/java/io/mosip/mimoto/controller/IssuersControllerTest.java new file mode 100644 index 000000000..3d9c0ac36 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/IssuersControllerTest.java @@ -0,0 +1,283 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerConfiguration; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.service.impl.IssuersServiceImpl; +import io.mosip.mimoto.util.Utilities; +import org.hamcrest.Matchers; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.List; +import java.util.stream.Collectors; + +import static io.mosip.mimoto.exception.PlatformErrorMessages.API_NOT_ACCESSIBLE_EXCEPTION; +import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION; +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = IssuersController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class IssuersControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private IssuersServiceImpl issuersService; + + @MockBean + private Utilities utilities; + + @MockBean + ObjectMapper objectMapper; + + @Test + public void getIssuersTestForSearchValueNull() throws Exception { + IssuersDTO issuers = new IssuersDTO(); + issuers.setIssuers((List.of(getIssuerDTO("Issuer2"), getIssuerDTO("Issuer4")))); + Mockito.when(issuersService.getIssuers(null)) + .thenReturn(issuers) + .thenThrow(new ApiNotAccessibleException()); + + IssuersDTO filteredIssuers = new IssuersDTO(); + filteredIssuers.setIssuers(issuers.getIssuers().stream().filter(issuer -> issuer.getDisplay().stream() + .anyMatch(displayDTO -> displayDTO.getTitle().toLowerCase().contains("Issuer1".toLowerCase()))) + .collect(Collectors.toList())); + + Mockito.when(issuersService.getIssuers("Issuer1")) + .thenReturn(filteredIssuers) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(get("/issuers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.issuers", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("issuer_id"), + Matchers.hasKey("credential_issuer"), + Matchers.hasKey("display"), + Matchers.hasKey("client_id"), + Matchers.hasKey("wellknown_endpoint"), + Matchers.hasKey("proxy_token_endpoint"), + Matchers.hasKey("token_endpoint"), + Matchers.hasKey("credential_issuer_host"), + Matchers.hasKey("authorization_audience"), + Matchers.not(Matchers.hasKey("redirect_url")), + Matchers.not(Matchers.hasKey("authorization_endpoint")), + Matchers.not(Matchers.hasKey("credential_endpoint")), + Matchers.not(Matchers.hasKey("credential_audience")), + Matchers.not(Matchers.hasKey("additional_headers")), + Matchers.not(Matchers.hasKey("scopes_supported")) + ) + ))); + + + mockMvc.perform(get("/issuers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuersTestForSomeSearchValue() throws Exception { + IssuersDTO issuers = new IssuersDTO(); + issuers.setIssuers((List.of(getIssuerDTO("Issuer2"), getIssuerDTO("Issuer3")))); + + IssuersDTO filteredIssuers = new IssuersDTO(); + filteredIssuers.setIssuers(issuers.getIssuers().stream().filter(issuer -> issuer.getDisplay().stream() + .anyMatch(displayDTO -> displayDTO.getTitle().toLowerCase().contains("Issuer2".toLowerCase()))) + .collect(Collectors.toList())); + + Mockito.when(issuersService.getIssuers("Issuer2")) + .thenReturn(filteredIssuers) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(get("/issuers?search=Issuer2").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.issuers", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("issuer_id"), + Matchers.hasKey("credential_issuer"), + Matchers.hasKey("display"), + Matchers.hasKey("client_id"), + Matchers.hasKey("wellknown_endpoint"), + Matchers.hasKey("proxy_token_endpoint"), + Matchers.hasKey("token_endpoint"), + Matchers.hasKey("authorization_audience"), + Matchers.hasKey("credential_issuer_host"), + Matchers.not(Matchers.hasKey("redirect_url")), + Matchers.not(Matchers.hasKey("authorization_endpoint")), + Matchers.not(Matchers.hasKey("credential_endpoint")), + Matchers.not(Matchers.hasKey("credential_audience")), + Matchers.not(Matchers.hasKey("additional_headers")), + Matchers.not(Matchers.hasKey("scopes_supported")) + ) + ))); + + mockMvc.perform(get("/issuers?search=Issuer2").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuerDetailsTestForValidIssuerId() throws Exception { + Mockito.when(issuersService.getIssuerDetails("id1")) + .thenReturn(getIssuerDTO("Issuer1")) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(get("/issuers/id1").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()); + + mockMvc.perform(get("/issuers/id1").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuerDetailsLogoDtoWithUrlProperty() throws Exception { + IssuerDTO issuer = getIssuerConfigDTO("Issuer1"); + issuer.getDisplay().get(0).getLogo().setUrl("https://example.com/logo-via-url.png"); + issuer.getDisplay().get(0).getLogo().setAlt_text("alt-url"); + + Mockito.when(issuersService.getIssuerDetails("id-url")).thenReturn(issuer); + + mockMvc.perform(get("/issuers/id-url").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.display[0].logo.url", Matchers.is("https://example.com/logo-via-url.png"))) + .andExpect(jsonPath("$.response.display[0].logo.alt_text", Matchers.is("alt-url"))); + } + + @Test + public void getIssuerDetailsLogoDtoFromJsonWithUriAlias() throws Exception { + String issuerJson = """ + { + "issuer_id": "Issuer1id", + "protocol": "OpenId4VCI", + "display": [{ + "name": "Issuer1", + "title": "Download via Issuer1", + "description": "Issuer1 description", + "language": "en", + "logo": { + "uri": "https://example.com/logo-via-uri.png", + "alt_text": "alt-uri" + } + }], + "client_id": "123", + "client_alias": "test-client-alias", + "enabled": "true", + "credential_issuer_host": "https://issuer.env.net", + "token_endpoint": "https://dev/Issuer1id" + } + """; + IssuerDTO issuer = new ObjectMapper().readValue(issuerJson, IssuerDTO.class); + + Mockito.when(issuersService.getIssuerDetails("id-uri")).thenReturn(issuer); + + mockMvc.perform(get("/issuers/id-uri").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.display[0].logo.url", Matchers.is("https://example.com/logo-via-uri.png"))) + .andExpect(jsonPath("$.response.display[0].logo.alt_text", Matchers.is("alt-uri"))); + } + + @Test + public void getIssuerWellknownTest() throws Exception { + String issuerId = "issuer1"; + String expectedCredentialIssuerWellknownResponse = getExpectedWellKnownJson(); + CredentialIssuerConfiguration expectedCredentialIssuerConfiguration = getCredentialIssuerConfigurationResponseDto(issuerId, "CredentialType1", List.of()); + Mockito.when(issuersService.getIssuerConfiguration(issuerId)).thenReturn(expectedCredentialIssuerConfiguration); + + String actualResponse = mockMvc.perform(get("/issuers/" + issuerId + "/well-known-proxy").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JSONAssert.assertEquals(new JSONObject(expectedCredentialIssuerWellknownResponse), new JSONObject(actualResponse), JSONCompareMode.LENIENT); + } + + @Test + public void getIssuerDetailsTestForInvalidIssuerId() throws Exception { + Mockito.when(issuersService.getIssuerDetails("invalidId")).thenThrow(InvalidIssuerIdException.class); + + mockMvc.perform(get("/issuers/invalidId").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + } + + + @Test + public void getIssuerWellknownWhenServiceThrowsReturnsNotFound() throws Exception { + String issuerId = "issuer1"; + Mockito.when(issuersService.getIssuerConfiguration(issuerId)).thenThrow(new RuntimeException("well-known unreachable")); + + mockMvc.perform(get("/issuers/" + issuerId + "/well-known-proxy").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResponse().getContentAsString().isEmpty())); + } + + @Test + public void getIssuerConfigurationTest() throws Exception { + String issuerId = "id1"; + + //get the IssuerConfig from the json file expectedIssuerConfig and wrap it inside response to test the response of configuration endpoint response wrapper + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode originalJson = objectMapper.readTree(new ClassPathResource("responses/expectedIssuerConfig.json").getInputStream()); + ObjectNode wrappedJson = objectMapper.createObjectNode(); + wrappedJson.set("response", originalJson); + String expectedJsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(wrappedJson); + if (expectedJsonString.startsWith("\uFEFF")) { + expectedJsonString = expectedJsonString.substring(1); + } + String finalExpectedJsonString = expectedJsonString; + CredentialIssuerConfiguration expectedCredentialIssuerConfiguration = getCredentialIssuerConfigurationResponseDto(issuerId, "CredentialType1", List.of()); + Mockito.when(issuersService.getIssuerConfiguration(issuerId)).thenReturn(expectedCredentialIssuerConfiguration); + + mockMvc.perform(get("/issuers/" + issuerId + "/configuration") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(result -> { + String actualResponse = result.getResponse().getContentAsString().trim(); + JSONAssert.assertEquals(finalExpectedJsonString, actualResponse, JSONCompareMode.LENIENT); + }); + } + + @Test + public void shouldReturnExceptionInResponseOnGetIssuerConfigIfAnyExceptionOccurredWhenFetchingIssuerWellknown() throws Exception { + String issuerId = "id1"; + Mockito.when(issuersService.getIssuerConfiguration(issuerId)).thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(get("/issuers/" + issuerId + "/configuration") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/IssuersV2ControllerTest.java b/src/test/java/io/mosip/mimoto/controller/IssuersV2ControllerTest.java new file mode 100644 index 000000000..c69d0cb83 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/IssuersV2ControllerTest.java @@ -0,0 +1,166 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.dto.IssuerV2DTO; +import io.mosip.mimoto.dto.IssuersV2DTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.service.IssuersService; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.IOException; +import java.util.List; + +import static io.mosip.mimoto.exception.PlatformErrorMessages.API_NOT_ACCESSIBLE_EXCEPTION; +import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION; +import static io.mosip.mimoto.util.TestUtilities.getIssuerResponseDTO; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = IssuersV2Controller.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class IssuersV2ControllerTest { + + private static final String LIST_PATH = "/v2/issuers"; + private static final String ISSUER_PATH = "/v2/issuers/{issuer-id}"; + + @Autowired + private MockMvc mockMvc; + + @MockBean + private IssuersService issuersService; + + @Before + public void setUp() { + Mockito.reset(issuersService); + } + + private ResultActions performGetList() throws Exception { + return mockMvc.perform(get(LIST_PATH).accept(MediaType.APPLICATION_JSON_VALUE)); + } + + private ResultActions performGetIssuer(String issuerId) throws Exception { + return mockMvc.perform(get(ISSUER_PATH, issuerId).accept(MediaType.APPLICATION_JSON_VALUE)); + } + + + @Test + public void getAllIssuersSuccessReturnsOkWithIssuersV2DTO() throws Exception { + List issuerList = List.of(getIssuerResponseDTO("IssuerA"), getIssuerResponseDTO("IssuerB")); + Mockito.when(issuersService.getIssuersV2DTO()).thenReturn(new IssuersV2DTO(issuerList)); + + performGetList() + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.issuers", Matchers.hasSize(2))) + .andExpect(jsonPath("$.response.issuers[*].issuer_id", Matchers.everyItem(Matchers.anyOf(Matchers.is("IssuerAid"), Matchers.is("IssuerBid"))))) + .andExpect(jsonPath("$.response.issuers[*]", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("issuer_id"), + Matchers.hasKey("protocol"), + Matchers.hasKey("display"), + Matchers.hasKey("client_id"), + Matchers.hasKey("token_endpoint"), + Matchers.hasKey("credential_issuer_host") + )))); + } + + @Test + public void getAllIssuersApiNotAccessibleExceptionReturnsServiceUnavailableWithApiNotAccessibleError() throws Exception { + Mockito.when(issuersService.getIssuersV2DTO()).thenThrow(new ApiNotAccessibleException()); + + performGetList() + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getAllIssuersIOExceptionReturnsServiceUnavailableWithApiNotAccessibleError() throws Exception { + Mockito.when(issuersService.getIssuersV2DTO()).thenThrow(new IOException("config read failed")); + + performGetList() + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getAllIssuersGenericExceptionReturnsBadRequestWithHandledError() throws Exception { + String message = "Unexpected failure"; + Mockito.when(issuersService.getIssuersV2DTO()).thenThrow(new RuntimeException(message)); + + performGetList() + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(message))); + } + + + @Test + public void getIssuerByIdValidIdReturnsOkWithIssuerV2DTO() throws Exception { + IssuerV2DTO issuer = getIssuerResponseDTO("MyIssuer"); + Mockito.when(issuersService.getIssuerV2Details("MyIssuerid")).thenReturn(issuer); + + performGetIssuer("MyIssuerid") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.issuer_id", Matchers.is("MyIssuerid"))) + .andExpect(jsonPath("$.response.protocol", Matchers.is("OpenId4VCI"))) + .andExpect(jsonPath("$.response.credential_issuer_host", Matchers.is("https://issuer.env.net"))); + } + + @Test + public void getIssuerByIdInvalidIssuerIdExceptionReturnsNotFoundWithInvalidIssuerError() throws Exception { + Mockito.when(issuersService.getIssuerV2Details("NonExistent")).thenThrow(new InvalidIssuerIdException()); + + performGetIssuer("NonExistent") + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuerByIdApiNotAccessibleExceptionReturnsServiceUnavailableWithApiNotAccessibleError() throws Exception { + Mockito.when(issuersService.getIssuerV2Details("id1")).thenThrow(new ApiNotAccessibleException()); + + performGetIssuer("id1") + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuerByIdIOExceptionReturnsServiceUnavailableWithApiNotAccessibleError() throws Exception { + Mockito.when(issuersService.getIssuerV2Details("id1")).thenThrow(new IOException("io error")); + + performGetIssuer("id1") + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + + @Test + public void getIssuerByIdGenericExceptionReturnsBadRequestWithHandledError() throws Exception { + String message = "Internal error"; + Mockito.when(issuersService.getIssuerV2Details("id1")).thenThrow(new IllegalStateException(message)); + + performGetIssuer("id1") + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(message))); + } +} diff --git a/src/test/java/io/mosip/mimoto/controller/PresentationControllerTest.java b/src/test/java/io/mosip/mimoto/controller/PresentationControllerTest.java new file mode 100644 index 000000000..60e2b0436 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/PresentationControllerTest.java @@ -0,0 +1,441 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.openid.presentation.PresentationDefinitionDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.exception.InvalidVerifierException; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.PresentationService; +import io.mosip.mimoto.service.VerifierService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PresentationController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +@TestPropertySource(properties = { + "mosip.inji.ovp.error.redirect.url.pattern=%s?error_code=%s&error_message=%s", + "mosip.inji.web.redirect.url=https://inji.web.redirect.url" +}) +public class PresentationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PresentationService presentationService; + + @MockBean + private VerifierService verifierService; + + @MockBean + private ObjectMapper objectMapper; + + private static final String RESPONSE_TYPE = "vp_token"; + private static final String RESOURCE = "https://example.com/resource"; + private static final String CLIENT_ID = "test-client-id"; + private static final String REDIRECT_URI = "https://example.com/callback"; + private static final String PRESENTATION_DEFINITION_JSON = "{\"id\":\"test\",\"input_descriptors\":[]}"; + private static final String SUCCESS_REDIRECT_URL = "https://success.redirect.url"; + private static final String RESPONSE_URI = "https://example.com/v0/verify/vp-submission/direct-post?session=abc"; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPerformAuthorizationSuccess() throws Exception { + // Arrange + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenReturn(SUCCESS_REDIRECT_URL); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(SUCCESS_REDIRECT_URL)); + + // Verify service calls + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(objectMapper).readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PresentationRequestDTO.class); + verify(presentationService).authorizePresentation(captor.capture()); + + PresentationRequestDTO capturedRequest = captor.getValue(); + assertEquals(RESPONSE_TYPE, capturedRequest.getResponseType()); + assertEquals(RESOURCE, capturedRequest.getResource()); + assertEquals(CLIENT_ID, capturedRequest.getClientId()); + assertEquals(REDIRECT_URI, capturedRequest.getRedirectUri()); + assertEquals(mockPresentationDefinitionDTO, capturedRequest.getPresentationDefinition()); + } + + @Test + public void testPerformAuthorizationInvalidVerifierException() throws Exception { + // Arrange + String errorCode = "INVALID_VERIFIER"; + String errorMessage = "Invalid verifier provided"; + InvalidVerifierException exception = new InvalidVerifierException(errorCode, errorMessage); + + doThrow(exception).when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + + String expectedRedirectUrl = String.format( + "https://inji.web.redirect.url?error_code=%s&error_message=%s", + errorCode, + URLEncoder.encode(errorMessage, StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(presentationService, never()).authorizePresentation(any()); + } + + @Test + public void testPerformAuthorizationVPNotCreatedException() throws Exception { + // Arrange + String errorCode = "VP_NOT_CREATED"; + String errorMessage = "VP creation failed"; + VPNotCreatedException exception = new VPNotCreatedException(errorCode, errorMessage); + + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenThrow(exception); + + String expectedRedirectUrl = String.format( + "%s?error_code=%s&error_message=%s", + REDIRECT_URI, + errorCode, + URLEncoder.encode(errorMessage, StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(presentationService).authorizePresentation(any(PresentationRequestDTO.class)); + } + + @Test + public void testPerformAuthorizationInvalidCredentialResourceException() throws Exception { + // Arrange + String errorCode = "INVALID_CREDENTIAL_RESOURCE"; + String errorMessage = "Invalid credential resource"; + InvalidCredentialResourceException exception = new InvalidCredentialResourceException(errorCode, errorMessage); + + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenThrow(exception); + + String expectedRedirectUrl = String.format( + "%s?error_code=%s&error_message=%s", + REDIRECT_URI, + errorCode, + URLEncoder.encode(errorMessage, StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(presentationService).authorizePresentation(any(PresentationRequestDTO.class)); + } + + @Test + public void testPerformAuthorizationGenericException() throws Exception { + // Arrange + RuntimeException exception = new RuntimeException("Unexpected error occurred"); + + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenThrow(exception); + + String expectedRedirectUrl = String.format( + "%s?error_code=%s&error_message=%s", + REDIRECT_URI, + ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), + URLEncoder.encode(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage(), StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(presentationService).authorizePresentation(any(PresentationRequestDTO.class)); + } + + @Test + public void testPerformAuthorizationJsonParsingException() throws Exception { + // Arrange + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenThrow(new RuntimeException("JSON parsing failed")); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + + String expectedRedirectUrl = String.format( + "%s?error_code=%s&error_message=%s", + REDIRECT_URI, + ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), + URLEncoder.encode(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage(), StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + + verify(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + verify(presentationService, never()).authorizePresentation(any()); + } + + @Test + public void testPerformAuthorizationMissingRequiredParameters() throws Exception { + // Test missing response_type + mockMvc.perform(get("/authorize") + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isBadRequest()); + + // Test missing resource + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isBadRequest()); + + // Test missing presentation_definition + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isBadRequest()); + + // Test missing client_id + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isBadRequest()); + + // Verify no service calls were made + verify(verifierService, never()).validateVerifier(anyString(), anyString(), anyString()); + verify(presentationService, never()).authorizePresentation(any()); + } + + @Test + public void testPerformAuthorizationEmptyParameters() throws Exception { + // Arrange + doThrow(new InvalidVerifierException("EMPTY_CLIENT_ID", "Client ID cannot be empty")) + .when(verifierService).validateVerifier("", "", ""); + + String expectedRedirectUrl = String.format( + "https://inji.web.redirect.url?error_code=%s&error_message=%s", + "EMPTY_CLIENT_ID", + URLEncoder.encode("Client ID cannot be empty", StandardCharsets.UTF_8) + ); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", "") + .param("resource", "") + .param("presentation_definition", "") + .param("client_id", "") + .param("redirect_uri", "") + .param("response_uri", "")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + } + + @Test + public void testPerformAuthorizationSpecialCharactersInParameters() throws Exception { + // Arrange + String specialClientId = "client@#$%^&*()"; + String specialRedirectUri = "https://example.com/callback?param=value&other=test"; + String specialResource = "https://example.com/resource?id=123&type=credential"; + + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + String specialResponseUri = "https://example.com/v0/verify/vp-submission/direct-post?session=abc"; + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenReturn(SUCCESS_REDIRECT_URL); + doNothing().when(verifierService).validateVerifier(specialClientId, specialResponseUri, specialRedirectUri); + + // Act & Assert + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", specialResource) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", specialClientId) + .param("redirect_uri", specialRedirectUri) + .param("response_uri", specialResponseUri)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(SUCCESS_REDIRECT_URL)); + + verify(verifierService).validateVerifier(specialClientId, specialResponseUri, specialRedirectUri); + verify(presentationService).authorizePresentation(any(PresentationRequestDTO.class)); + } + +// Add to PresentationControllerTest.java + + @Test + public void testPerformAuthorizationWithInvalidHttpMethod() throws Exception { + // POST instead of GET should return 405 Method Not Allowed + mockMvc.perform(post("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI)) + .andExpect(status().isMethodNotAllowed()); + } + + @Test + public void testPerformAuthorizationWithExtraParameters() throws Exception { + PresentationDefinitionDTO mockPresentationDefinitionDTO = new PresentationDefinitionDTO(); + when(objectMapper.readValue(PRESENTATION_DEFINITION_JSON, PresentationDefinitionDTO.class)) + .thenReturn(mockPresentationDefinitionDTO); + when(presentationService.authorizePresentation(any(PresentationRequestDTO.class))) + .thenReturn(SUCCESS_REDIRECT_URL); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", PRESENTATION_DEFINITION_JSON) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI) + .param("extra_param", "extra_value")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(SUCCESS_REDIRECT_URL)); + } + + @Test + public void testPerformAuthorizationWithMalformedPresentationDefinitionJson() throws Exception { + String malformedJson = "{invalid_json}"; + when(objectMapper.readValue(malformedJson, PresentationDefinitionDTO.class)) + .thenThrow(new RuntimeException("Malformed JSON")); + doNothing().when(verifierService).validateVerifier(CLIENT_ID, RESPONSE_URI, REDIRECT_URI); + + String expectedRedirectUrl = String.format( + "%s?error_code=%s&error_message=%s", + REDIRECT_URI, + ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(), + URLEncoder.encode(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage(), StandardCharsets.UTF_8) + ); + + mockMvc.perform(get("/authorize") + .param("response_type", RESPONSE_TYPE) + .param("resource", RESOURCE) + .param("presentation_definition", malformedJson) + .param("client_id", CLIENT_ID) + .param("redirect_uri", REDIRECT_URI) + .param("response_uri", RESPONSE_URI)) + .andExpect(status().isFound()) + .andExpect(redirectedUrl(expectedRedirectUrl)); + } + + @Test + public void testPerformAuthorizationWithNullParameters() throws Exception { + mockMvc.perform(get("/authorize") + .param("response_type", (String) null) + .param("resource", (String) null) + .param("presentation_definition", (String) null) + .param("client_id", (String) null) + .param("redirect_uri", (String) null)) + .andExpect(status().isBadRequest()); + + verify(verifierService, never()).validateVerifier(anyString(), anyString(), anyString()); + verify(presentationService, never()).authorizePresentation(any()); + } + +} diff --git a/src/test/java/io/mosip/mimoto/controller/ResidentServiceControllerTest.java b/src/test/java/io/mosip/mimoto/controller/ResidentServiceControllerTest.java new file mode 100644 index 000000000..d45e784ca --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/ResidentServiceControllerTest.java @@ -0,0 +1,153 @@ +package io.mosip.mimoto.controller; + +import com.google.common.collect.Lists; +import io.mosip.kernel.core.util.JsonUtils; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.mimoto.AidStatusRequestDTO; +import io.mosip.mimoto.dto.mimoto.AppOTPRequestDTO; +import io.mosip.mimoto.dto.mimoto.AppVIDGenerateRequestDTO; +import io.mosip.mimoto.dto.mimoto.IndividualIdOtpRequestDTO; +import io.mosip.mimoto.dto.resident.*; +import io.mosip.mimoto.service.RestClientService; +import io.mosip.mimoto.util.RequestValidator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ResidentServiceController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class ResidentServiceControllerTest { + + @MockBean + public RestClientService restClientService; + + @MockBean + public RequestValidator requestValidator; + + @Autowired + private MockMvc mockMvc; + + @Test + public void residentOtpRequestTest() throws Exception { + AppOTPRequestDTO requestDTO = new AppOTPRequestDTO(); + requestDTO.setIndividualId("IndividualId"); + requestDTO.setIndividualIdType("UIN"); + requestDTO.setTransactionID("1234567890"); + requestDTO.setOtpChannel(Lists.newArrayList("EMAIL")); + + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(response); + + this.mockMvc.perform(post("/req/otp").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void vidGenerateTest() throws Exception { + AppVIDGenerateRequestDTO requestDTO = new AppVIDGenerateRequestDTO(); + requestDTO.setIndividualId("IndividualId"); + requestDTO.setIndividualIdType("UIN"); + requestDTO.setTransactionID("1234567890"); + requestDTO.setOtp("111111"); + + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + this.mockMvc.perform(post("/vid").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void authLockTest() throws Exception { + AuthLockRequestDTO requestDTO = new AuthLockRequestDTO(); + requestDTO.setIndividualId("IndividualId"); + requestDTO.setIndividualIdType("UIN"); + requestDTO.setTransactionID("1234567890"); + requestDTO.setOtp("111111"); + + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + this.mockMvc.perform(post("/req/auth/lock").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void authUnLockTest() throws Exception { + AuthUnlockRequestDTO requestDTO = new AuthUnlockRequestDTO(); + requestDTO.setIndividualId("IndividualId"); + requestDTO.setIndividualIdType("UIN"); + requestDTO.setTransactionID("1234567890"); + requestDTO.setOtp("111111"); + + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + this.mockMvc.perform(post("/req/auth/unlock").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void individualIdOtpRequestTest() throws Exception { + IndividualIdOtpRequestDTO requestDTO = new IndividualIdOtpRequestDTO(); + requestDTO.setAid("IndividualId"); + requestDTO.setOtpChannel(Lists.newArrayList("EMAIL")); + requestDTO.setTransactionID("1234567890"); + + OTPResponseDTO response = new OTPResponseDTO(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + this.mockMvc.perform(post("/req/individualId/otp").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } + + @Test + public void aidGetIndividualIdTest() throws Exception { + AidStatusRequestDTO requestDTO = new AidStatusRequestDTO(); + requestDTO.setAid("IndividualId"); + requestDTO.setTransactionID("1234567890"); + requestDTO.setOtp("111111"); + + ResponseWrapper response = new ResponseWrapper<>(); + + Mockito.when(restClientService.postApi(Mockito.any(), + Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(response); + + this.mockMvc.perform(post("/aid/get-individual-id").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(JsonUtils.javaObjectToJsonString(requestDTO))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/TokenAuthControllerTest.java b/src/test/java/io/mosip/mimoto/controller/TokenAuthControllerTest.java new file mode 100644 index 000000000..cfa843202 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/TokenAuthControllerTest.java @@ -0,0 +1,142 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import io.mosip.mimoto.service.TokenService; +import io.mosip.mimoto.service.TokenServiceFactory; +import io.mosip.mimoto.util.GlobalExceptionHandler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {TokenAuthController.class, GlobalExceptionHandler.class}) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class TokenAuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TokenServiceFactory tokenServiceFactory; + + @MockBean + private TokenService tokenService; + + private String provider; + private String idToken; + private String authorizationHeader; + + @Before + public void setUp() { + provider = "google"; + idToken = "dummyIdToken"; + authorizationHeader = "Bearer " + idToken; + } + + @Test + public void shouldReturnSuccessForValidTokenAndProvider() throws Exception { + when(tokenServiceFactory.getTokenService(eq(provider))).thenReturn(tokenService); + doNothing().when(tokenService).processToken(eq(idToken), eq(provider), any(), any()); + + mockMvc.perform(post("/auth/{provider}/token-login", provider) + .header("Authorization", authorizationHeader) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", is("Session created."))); + + verify(tokenServiceFactory).getTokenService(eq(provider)); + verify(tokenService).processToken(eq(idToken), eq(provider), any(), any()); + } + + @Test + public void shouldReturnBadRequestForUnsupportedProvider() throws Exception { + + OAuth2AuthenticationException exception = new OAuth2AuthenticationException( + "INVALID_REQUEST", + String.format("Unsupported provider: %s", provider), + HttpStatus.BAD_REQUEST + ); + + when(tokenServiceFactory.getTokenService(eq(provider))).thenThrow(exception); + + + mockMvc.perform(post("/auth/{provider}/token-login", provider) + .header("Authorization", authorizationHeader) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.errorMessage", is(String.format("Unsupported provider: %s", provider)))); + + verifyNoInteractions(tokenService); + } + + @Test + public void shouldReturnBadRequestForMissingAuthorizationHeader() throws Exception { + mockMvc.perform(post("/auth/google/token-login", provider) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(tokenService); + } + + @Test + public void shouldReturnBadRequestForInvalidAuthorizationHeader() throws Exception { + String invalidAuthorizationHeader = "InvalidToken"; + + mockMvc.perform(post("/auth/google/token-login", provider) + .header("Authorization", invalidAuthorizationHeader) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.errorMessage", is("Bearer ID token required."))); + } + + @Test + public void shouldReturnUnauthorizedForInvalidToken() throws Exception { + when(tokenServiceFactory.getTokenService(eq(provider))).thenReturn(tokenService); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException("invalid_token", "Invalid token format", HttpStatus.BAD_REQUEST); + doThrow(exception).when(tokenService).processToken(eq(idToken), eq(provider), any(), any()); + + mockMvc.perform(post("/auth/{provider}/token-login", provider) + .header("Authorization", authorizationHeader) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.errorCode", is("invalid_token"))) + .andExpect(jsonPath("$.errorMessage", is("Invalid token format"))); + verify(tokenServiceFactory).getTokenService(eq(provider)); + verify(tokenService).processToken(eq(idToken), eq(provider), any(), any()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/UsersControllerTest.java b/src/test/java/io/mosip/mimoto/controller/UsersControllerTest.java new file mode 100644 index 000000000..ae4541c11 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/UsersControllerTest.java @@ -0,0 +1,168 @@ +package io.mosip.mimoto.controller; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.repository.UserMetadataRepository; +import io.mosip.mimoto.service.EncryptionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = UsersController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +class UsersControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserMetadataRepository userMetadataRepository; + + @MockBean + private EncryptionService encryptionService; + + @Autowired + private WebApplicationContext context; + + private MockHttpSession mockHttpSession; + + private static final String PROVIDER_SUBJECT_ID = "test-user"; + private static final String IDENTITY_PROVIDER = "test-provider"; + private static final String ENCRYPTED_DISPLAY_NAME = "encrypted-name"; + private static final String ENCRYPTED_PROFILE_PIC = "encrypted-pic"; + private static final String ENCRYPTED_EMAIL = "encrypted-email"; + private static final String DECRYPTED_DISPLAY_NAME = "Test User"; + private static final String DECRYPTED_PROFILE_PIC = "https://test.com/pic.jpg"; + private static final String DECRYPTED_EMAIL = "test@example.com"; + private static final String WALLET_ID = "test-wallet-id"; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + MockitoAnnotations.openMocks(this); + mockHttpSession = new MockHttpSession(); + + } + + @Test + void getUserProfileInfoFromCacheSuccess() throws Exception { + UserMetadataDTO userMetadataDTO = new UserMetadataDTO(DECRYPTED_DISPLAY_NAME, DECRYPTED_PROFILE_PIC, DECRYPTED_EMAIL, WALLET_ID); + mockHttpSession.setAttribute(SessionKeys.USER_METADATA, userMetadataDTO); + + mockMvc.perform(get("/users/me") + .with(SecurityMockMvcRequestPostProcessors.user(DECRYPTED_DISPLAY_NAME).roles("USER")) + .session(mockHttpSession)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.displayName").value(DECRYPTED_DISPLAY_NAME)) + .andExpect(jsonPath("$.profilePictureUrl").value(DECRYPTED_PROFILE_PIC)) + .andExpect(jsonPath("$.email").value(DECRYPTED_EMAIL)) + .andExpect(jsonPath("$.walletId").value("test-wallet-id")); + } + + @Test + void getUserProfileInfoFromDatabaseSuccess() throws Exception { + UserMetadata userMetadata = new UserMetadata(); + userMetadata.setDisplayName(ENCRYPTED_DISPLAY_NAME); + userMetadata.setProfilePictureUrl(ENCRYPTED_PROFILE_PIC); + userMetadata.setEmail(ENCRYPTED_EMAIL); + mockHttpSession.setAttribute(SessionKeys.USER_METADATA, null); + mockHttpSession.setAttribute("clientRegistrationId", IDENTITY_PROVIDER); + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(PROVIDER_SUBJECT_ID, IDENTITY_PROVIDER)) + .thenReturn(Optional.of(userMetadata)); + when(encryptionService.decrypt(ENCRYPTED_DISPLAY_NAME)).thenReturn(DECRYPTED_DISPLAY_NAME); + when(encryptionService.decrypt(ENCRYPTED_PROFILE_PIC)).thenReturn(DECRYPTED_PROFILE_PIC); + when(encryptionService.decrypt(ENCRYPTED_EMAIL)).thenReturn(DECRYPTED_EMAIL); + + mockMvc.perform(get("/users/me") + .with(SecurityMockMvcRequestPostProcessors.user(PROVIDER_SUBJECT_ID).roles("USER")) + .session(mockHttpSession)) + .andExpect(status().isOk()) + .andExpect(result -> result.getResponse().getContentAsString()) + .andExpect(jsonPath("$.displayName").value(DECRYPTED_DISPLAY_NAME)) + .andExpect(jsonPath("$.profilePictureUrl").value(DECRYPTED_PROFILE_PIC)) + .andExpect(jsonPath("$.email").value(DECRYPTED_EMAIL)); + // assert that the user metadata is stored in the session + UserMetadataDTO userMetadataDTO = (UserMetadataDTO) mockHttpSession.getAttribute(SessionKeys.USER_METADATA); + assertEquals(DECRYPTED_DISPLAY_NAME, userMetadataDTO.getDisplayName()); + assertEquals(DECRYPTED_PROFILE_PIC, userMetadataDTO.getProfilePictureUrl()); + assertEquals(DECRYPTED_EMAIL, userMetadataDTO.getEmail()); + assertNull(userMetadataDTO.getWalletId()); + } + + @Test + void getUserProfileInfoWhenUserNotFoundInDB() throws Exception { + mockHttpSession.setAttribute(SessionKeys.USER_METADATA, null); + mockHttpSession.setAttribute("clientRegistrationId", IDENTITY_PROVIDER); + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(PROVIDER_SUBJECT_ID, IDENTITY_PROVIDER)) + .thenReturn(Optional.empty()); + + mockMvc.perform(get("/users/me") + .with(SecurityMockMvcRequestPostProcessors.user(DECRYPTED_DISPLAY_NAME).roles("USER")) + .session(mockHttpSession)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value("unauthorized")) + .andExpect(jsonPath("$.errorMessage").value("User not found. Please check your credentials or login again")); + } + + @Test + void getUserProfileInfoWhenDecryptionExceptionOccursWhileProcessingDataFromDB() throws Exception { + UserMetadata userMetadata = new UserMetadata(); + userMetadata.setDisplayName(ENCRYPTED_DISPLAY_NAME); + userMetadata.setProfilePictureUrl(ENCRYPTED_PROFILE_PIC); + userMetadata.setEmail(ENCRYPTED_EMAIL); + mockHttpSession.setAttribute(SessionKeys.USER_METADATA, null); + mockHttpSession.setAttribute("clientRegistrationId", IDENTITY_PROVIDER); + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(PROVIDER_SUBJECT_ID, IDENTITY_PROVIDER)) + .thenReturn(Optional.of(userMetadata)); + when(encryptionService.decrypt(ENCRYPTED_DISPLAY_NAME)).thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + + mockMvc.perform(get("/users/me") + .with(SecurityMockMvcRequestPostProcessors.user(PROVIDER_SUBJECT_ID).roles("USER")) + .session(mockHttpSession)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value("Failed to process user data")); + } + + @Test + void getUserProfileInfoWhenIdentityProviderIsMissing() throws Exception { + mockHttpSession.setAttribute(SessionKeys.USER_METADATA, null); + mockHttpSession.setAttribute("clientRegistrationId", null); + + mockMvc.perform(get("/users/me") + .with(SecurityMockMvcRequestPostProcessors.user(DECRYPTED_DISPLAY_NAME).roles("USER")) + .session(mockHttpSession)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value("unauthorized")) + .andExpect(jsonPath("$.errorMessage").value("User not found. Please check your credentials or login again")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/VerifiersControllerTest.java b/src/test/java/io/mosip/mimoto/controller/VerifiersControllerTest.java new file mode 100644 index 000000000..b7ddd8f4c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/VerifiersControllerTest.java @@ -0,0 +1,127 @@ +package io.mosip.mimoto.controller; + +import io.mosip.openID4VP.constants.SpecVersion; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.service.impl.VerifierServiceImpl; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = VerifiersController.class) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class VerifiersControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private VerifierServiceImpl verifierService; + + + @Test + public void getAllTrustedVerifiers() throws Exception { + VerifierDTO verifierDTO = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("https://test-responseUri")).build(); + + VerifiersDTO trustedVerifiers = VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierDTO)).build(); + + Mockito.when(verifierService.getTrustedVerifiers()) + .thenReturn(trustedVerifiers); + + + mockMvc.perform(get("/verifiers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.verifiers", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("client_id"), + Matchers.hasKey("redirect_uris"), + Matchers.hasKey("response_uris") + ) + ))); + } + + @Test + public void shouldReturnEmptyResponseIfAnyErrorOccurredWhileFetchingVerifiersList() throws Exception { + Mockito.when(verifierService.getTrustedVerifiers()) + .thenThrow(new ApiNotAccessibleException()); + + mockMvc.perform(get("/verifiers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.response.verifiers").isArray()) + .andExpect(jsonPath("$.response.verifiers").isEmpty()); + } + + @Test + public void shouldIncludeAllProvidedPropertiesInTrustedVerifiers() throws Exception { + Map metadata = new HashMap<>(); + metadata.put("client_name", "Test-Verifier"); + + VerifierDTO verifierDTO = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("https://test-responseUri")) + .jwksUri("https://test/.well-known/jwks.json") + .allowUnsignedRequest(true) + .specVersion(SpecVersion.DRAFT_23) + .build(); + + VerifiersDTO trustedVerifiers = VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierDTO)).build(); + + Mockito.when(verifierService.getTrustedVerifiers()) + .thenReturn(trustedVerifiers); + + mockMvc.perform(get("/verifiers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.verifiers[0].allow_unsigned_request").value(true)) + .andExpect(jsonPath("$.response.verifiers[0].client_id").value("test-clientId")) + .andExpect(jsonPath("$.response.verifiers[0].spec_version").value("draft23")) + .andExpect(jsonPath("$.response.verifiers[0].jwks_uri").value("https://test/.well-known/jwks.json")); + } + + @Test + public void shouldReturnDefaultValuesWhenOptionalFieldsAreNotProvided() throws Exception { + // Not providing jwks_uri and allow_unsigned_request in VerifierDTO + VerifierDTO verifierDTO = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("https://test-responseUri")) + .build(); + + VerifiersDTO trustedVerifiers = VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierDTO)).build(); + + Mockito.when(verifierService.getTrustedVerifiers()) + .thenReturn(trustedVerifiers); + + mockMvc.perform(get("/verifiers").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.verifiers[0].allow_unsigned_request").value(false)) // allow_unsigned_request is not passed means it should take default value false + .andExpect(jsonPath("$.response.verifiers[0].spec_version").value("v1")) // spec version is not passed means it should take default value v1 + .andExpect(jsonPath("$.response.verifiers[0].jwks_uri").doesNotExist()); // jwks_uri is not passed means it should not be present in trusted verifiers + } +} diff --git a/src/test/java/io/mosip/mimoto/controller/WalletCredentialsControllerTest.java b/src/test/java/io/mosip/mimoto/controller/WalletCredentialsControllerTest.java new file mode 100644 index 000000000..6a8aacd53 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/WalletCredentialsControllerTest.java @@ -0,0 +1,651 @@ +package io.mosip.mimoto.controller; + +import com.jayway.jsonpath.JsonPath; +import io.mosip.mimoto.dto.VerifiableCredentialRequestDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import io.mosip.mimoto.dto.resident.WalletCredentialResponseDTO; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.service.IdpService; +import io.mosip.mimoto.service.WalletCredentialService; +import io.mosip.mimoto.util.GlobalExceptionHandler; +import jakarta.servlet.http.HttpSession; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.ByteArrayInputStream; +import java.util.Collections; +import java.util.List; + +import static io.mosip.mimoto.exception.ErrorConstants.*; +import static io.mosip.mimoto.util.TestUtilities.createRequestBody; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WalletCredentialsController.class, GlobalExceptionHandler.class}) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +public class WalletCredentialsControllerTest { + + @MockBean + private WalletCredentialService walletCredentialService; + + @MockBean + private IdpService idpService; + + @Mock + private HttpSession httpSession; + + @Autowired + private MockMvc mockMvc; + + private VerifiableCredentialResponseDTO verifiableCredentialResponseDTO; + private WalletCredentialResponseDTO walletCredentialResponseDTO; + private final String walletId = "wallet123"; + private final String credentialId = "cred456"; + private final String walletKey = "encodedKey"; + private final String issuer = "issuer1"; + private final String credentialConfigurationId = "type1"; + private final String code = "code"; + private final String grantType = "authorization-code"; + private final String redirectUri = "https://.../redirect"; + private final String codeVerifier = "code-verifier"; + private final String locale = "en"; + VerifiableCredentialRequestDTO verifiableCredentialRequest; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + verifiableCredentialRequest = new VerifiableCredentialRequestDTO(); + + verifiableCredentialResponseDTO = VerifiableCredentialResponseDTO.builder() + .issuerDisplayName("issuerName123") + .issuerLogo("issuerLogo") + .credentialTypeDisplayName("credentialType123") + .credentialTypeLogo("credentialTypeLogo") + .credentialId("credentialId123") + .build(); + + walletCredentialResponseDTO = new WalletCredentialResponseDTO(); + walletCredentialResponseDTO.setFileName("credential.pdf"); + walletCredentialResponseDTO.setFileContentStream(new InputStreamResource(new ByteArrayInputStream("test-pdf".getBytes()))); + + when(httpSession.getAttribute("wallet_id")).thenReturn(walletId); + when(httpSession.getAttribute("wallet_key")).thenReturn(walletKey); + } + + // Tests for downloadCredential + @Test + public void shouldDownloadCredentialSuccessfully() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + when(idpService.getTokenResponse(verifiableCredentialRequest)).thenReturn(new TokenResponseDTO()); + when(walletCredentialService.downloadVCAndStoreInDB(eq(issuer), eq(credentialConfigurationId), any(), eq(locale), eq(walletId), eq(walletKey))) + .thenReturn(verifiableCredentialResponseDTO); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.issuerDisplayName").value("issuerName123")) + .andExpect(jsonPath("$.issuerLogo").value("issuerLogo")) + .andExpect(jsonPath("$.credentialTypeDisplayName").value("credentialType123")) + .andExpect(jsonPath("$.credentialTypeLogo").value("credentialTypeLogo")) + .andExpect(jsonPath("$.credentialId").value("credentialId123")); + } + + @Test + public void shouldReturnErroResponseWhenRequestedCredentialIsAlreadyAvailableInWallet() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + when(idpService.getTokenResponse(verifiableCredentialRequest)).thenReturn(new TokenResponseDTO()); + when(walletCredentialService.downloadVCAndStoreInDB(eq(issuer), eq(credentialConfigurationId), any(), eq(locale), eq(walletId), eq(walletKey))) + .thenThrow(new InvalidRequestException(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), "Duplicate credential for issuer and type")); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("credential_download_error")) + .andExpect(jsonPath("$.errorMessage").value("Duplicate credential for issuer and type")); + } + + @Test + public void shouldReturnErrorResponseWhenSessionDoesNotHaveWalletIdAndKey() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .content(createRequestBody(verifiableCredentialRequest)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } + + @Test + public void shouldCallServiceWithCorrectParameters() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", "fr") + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)); + + verify(idpService).getTokenResponse(verifiableCredentialRequest); + verify(walletCredentialService + ).downloadVCAndStoreInDB(eq(issuer), eq(credentialConfigurationId), any(), eq("fr"), eq(walletId), eq(walletKey)); + } + + @Test + public void shouldSetDefaultAndProceedWhenOptionalRequestParametersAreNotPassed() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)); + + // Default value for vcStorageExpiryLimit is -1 and locale is "en" + verify(walletCredentialService).downloadVCAndStoreInDB(any(), any(), any(), eq("en"), eq(walletId), eq(walletKey)); + } + + @Test + public void shouldThrowExceptionWhenInvalidLocaleIsPassed() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", "eng") // Three letter language code passed which is not valid + .header("Accept-Language", "invalid") + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Locale must be a 2-letter code")); + } + + @Test + public void shouldThrowInvalidRequestForWalletIdMismatch() throws Exception { + when(httpSession.getAttribute("wallet_id")).thenReturn("differentWalletId"); + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", "differentWalletId") + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Invalid Wallet ID. Session and request Wallet ID do not match")); + } + + @Test + public void shouldThrowInvalidRequestForMissingWalletKey() throws Exception { + when(httpSession.getAttribute("wallet_key")).thenReturn(null); + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Wallet key not found in session")); + } + + @Test + public void shouldThrowInvalidRequestForMissingIssuerAndCredentialConfigurationIdInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(null, null, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header("Accept-Language", "fr") + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(result -> { + String errorMessage = JsonPath.read(result.getResponse().getContentAsString(), "$.errorMessage"); + String[] messages = errorMessage.split(",\\s*"); + + assertEquals(2, messages.length); + assertThat(messages).anySatisfy(msg -> + assertThat(msg.trim()).isEqualTo("issuerId cannot be blank")); + assertThat(messages).anySatisfy(msg -> + assertThat(msg.trim()).isEqualTo("credentialConfigurationId cannot be blank")); + }); + } + + @Test + public void shouldThrowInvalidRequestForMissingIssuerInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(null, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("issuerId cannot be blank")); + } + + @Test + public void shouldThrowInvalidRequestForMissingCredentialConfigurationIdInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(issuer, null, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("credentialConfigurationId cannot be blank")); + } + + @Test + public void shouldThrowInvalidRequestForMissingCodeInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, null, grantType, redirectUri, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("code cannot be blank")); + } + + @Test + public void shouldThrowInvalidRequestForMissingGrantTypeInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, null, redirectUri, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("grantType cannot be blank")); + } + + @Test + public void shouldThrowInvalidRequestForMissingRedirectUriInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, null, codeVerifier); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("redirectUri cannot be blank")); + } + + @Test + public void shouldThrowInvalidRequestForMissingCodeVerifierInDownloadCredentialApi() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, null); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .header("Accept-Language", "fr") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("codeVerifier cannot be blank")); + } + + @Test + public void shouldThrowServiceUnavailableForTokenResponseFailure() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + when(idpService.getTokenResponse(verifiableCredentialRequest)) + .thenThrow(new ApiNotAccessibleException("API not accessible")); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errorCode").value("credential_download_error")); + } + + @Test + public void shouldThrowServiceUnavailableForExternalServiceFailure() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + when(idpService.getTokenResponse(anyMap())).thenReturn(new TokenResponseDTO()); + when(walletCredentialService.downloadVCAndStoreInDB(anyString(), anyString(), any(), anyString(), anyString(), anyString())) + .thenThrow(new ExternalServiceUnavailableException("Service unavailable", "Service unavailable")); + + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isServiceUnavailable()) + .andExpect(jsonPath("$.errorCode").value("Service unavailable")) + .andExpect(jsonPath("$.errorMessage").value("Service unavailable")); + } + + // Tests for fetchAllCredentialsForGivenWallet + + @Test + public void shouldFetchAllCredentialsSuccessfully() throws Exception { + List credentials = Collections.singletonList(verifiableCredentialResponseDTO); + when(walletCredentialService.fetchAllCredentialsForWallet(walletId, walletKey, locale)).thenReturn(credentials); + + mockMvc.perform(get("/wallets/{walletId}/credentials", walletId) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].issuerDisplayName").value("issuerName123")) + .andExpect(jsonPath("$[0].issuerLogo").value("issuerLogo")) + .andExpect(jsonPath("$[0].credentialTypeDisplayName").value("credentialType123")) + .andExpect(jsonPath("$[0].credentialId").value("credentialId123")) + .andExpect(jsonPath("$[0].credentialTypeLogo").value("credentialTypeLogo")); + } + + @Test + public void shouldThrowInvalidRequestForInvalidLocale() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials", walletId) + .header("Accept-Language", "invalid") + .param("issuer", issuer) + .param("credentialConfigurationId", credentialConfigurationId) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldReturnErrorResponseOnFetchAllCredentialsForGivenWalletWhenSessionDoesNotHaveWalletId() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + mockMvc.perform(get("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .sessionAttr("wallet_key", walletKey) + .content(createRequestBody(verifiableCredentialRequest)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } + + @Test + public void shouldThrowInvalidRequestForMissingWalletKeyInFetchAll() throws Exception { + when(httpSession.getAttribute("wallet_key")).thenReturn(null); + + mockMvc.perform(get("/wallets/{walletId}/credentials", walletId) + .header("Accept-Language", locale) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Wallet key not found in session")); + } + + @Test + public void shouldThrowInvalidRequestForWalletIdMismatchInFetchAll() throws Exception { + when(httpSession.getAttribute("wallet_id")).thenReturn("differentWalletId"); + + mockMvc.perform(get("/wallets/{walletId}/credentials", walletId) + .header("Accept-Language", locale) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", "differentWalletId") + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Invalid Wallet ID. Session and request Wallet ID do not match")); + } + + // Tests for getVerifiableCredential + @Test + public void shouldFetchVerifiableCredentialAsPdfSuccessfullyInline() throws Exception { + when(walletCredentialService.fetchVerifiableCredential(walletId, credentialId, walletKey, locale)) + .thenReturn(walletCredentialResponseDTO); + + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_PDF) + .header("Accept-Language", locale) + .param("action", "inline") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Disposition", "inline; filename=\"credential.pdf\"")) + .andExpect(content().contentType(MediaType.APPLICATION_PDF)); + } + + @Test + public void shouldFetchVerifiableCredentialAsDownload() throws Exception { + when(walletCredentialService.fetchVerifiableCredential(walletId, credentialId, walletKey, locale)) + .thenReturn(walletCredentialResponseDTO); + + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_PDF) + .header("Accept-Language", locale) + .param("action", "download") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Disposition", "attachment; filename=\"credential.pdf\"")) + .andExpect(content().contentType(MediaType.APPLICATION_PDF)); + } + + @Test + public void shouldThrowInvalidRequestForInvalidAcceptHeader() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .header("Accept-Language", locale) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Accept header must be application/pdf")); + } + + @Test + public void shouldReturnErrorResponseOnGetVerifiableCredentialWhenSessionDoesNotHaveWalletId() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .header("Accept-Language", locale) + .accept(MediaType.APPLICATION_PDF) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } + + @Test + public void shouldReturnErrorResponseOnGetVerifiableCredentialWhenSessionDoesNotHaveWalletKey() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .header("Accept-Language", locale) + .accept(MediaType.APPLICATION_PDF) + .sessionAttr("wallet_id", walletId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Wallet key not found in session")); + } + + @Test + public void shouldThrowInvalidRequestForInvalidAction() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", locale) + .param("action", "invalid") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()); + } + + @Test + public void shouldThrowCredentialNotFoundException() throws Exception { + when(walletCredentialService.fetchVerifiableCredential(walletId, credentialId, walletKey, locale)) + .thenThrow(new CredentialNotFoundException(RESOURCE_NOT_FOUND.getErrorCode(), RESOURCE_NOT_FOUND.getErrorMessage())); + + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_PDF) + .header("Accept-Language", locale) + .param("action", "inline") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errorCode").value("resource_not_found")) + .andExpect(jsonPath("$.errorMessage").value("The requested resource doesn’t exist.")); + } + + @Test + public void shouldThrowInvalidRequestForMissingCredentialId() throws Exception { + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, "")) + .andExpect(status().isNotFound()); + } + + @Test + public void shouldThrowCredentialProcessingExceptionForDecryptionError() throws Exception { + when(walletCredentialService.fetchVerifiableCredential(walletId, credentialId, walletKey, locale)) + .thenThrow(new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "Decryption failed")); + + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_PDF) + .header("Accept-Language", locale) + .param("action", "inline") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("credential_fetch_error")) + .andExpect(jsonPath("$.errorMessage").value("Decryption failed")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionForCorruptedData() throws Exception { + when(walletCredentialService.fetchVerifiableCredential(walletId, credentialId, walletKey, locale)) + .thenThrow(new CredentialProcessingException(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), "CORRUPTED_DATA")); + + mockMvc.perform(get("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .accept(MediaType.APPLICATION_PDF) + .header("Accept-Language", locale) + .param("action", "inline") + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value(CREDENTIAL_FETCH_EXCEPTION.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value("CORRUPTED_DATA")); + } + + // Tests for deleteCredential + @Test + public void shouldDeleteCredentialSuccessfully() throws Exception { + mockMvc.perform(delete("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()); + } + + @Test + public void shouldReturnErrorResponseOnDeleteCredentialWhenSessionDoesNotHaveWalletId() throws Exception { + mockMvc.perform(delete("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } + + @Test + public void shouldThrowCredentialNotFoundExceptionWhenDeletingNonExistentCredential() throws Exception { + doThrow(new CredentialNotFoundException(RESOURCE_NOT_FOUND.getErrorCode(), RESOURCE_NOT_FOUND.getErrorMessage())) + .when(walletCredentialService).deleteCredential(credentialId, walletId); + + mockMvc.perform(delete("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errorCode").value("resource_not_found")) + .andExpect(jsonPath("$.errorMessage").value("The requested resource doesn’t exist.")); + } + + @Test + public void shouldThrowInvalidRequestForWalletIdMismatchInDeleteCredential() throws Exception { + when(httpSession.getAttribute("wallet_id")).thenReturn("differentWalletId"); + + mockMvc.perform(delete("/wallets/{walletId}/credentials/{credentialId}", walletId, credentialId) + .sessionAttr("wallet_id", "differentWalletId") + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Invalid Wallet ID. Session and request Wallet ID do not match")); + } + + private void buildVerifiableCredentialRequest(String issuer, String credentialConfigurationId, String code, String grantType, String redirectUri, String codeVerifier) { + verifiableCredentialRequest.setIssuer(issuer); + verifiableCredentialRequest.setCredentialConfigurationId(credentialConfigurationId); + verifiableCredentialRequest.setCode(code); + verifiableCredentialRequest.setGrantType(grantType); + verifiableCredentialRequest.setRedirectUri(redirectUri); + verifiableCredentialRequest.setCodeVerifier(codeVerifier); + } + + @Test + public void shouldThrowInvalidRequestForInvalidLocaleCode() throws Exception { + buildVerifiableCredentialRequest(issuer, credentialConfigurationId, code, grantType, redirectUri, codeVerifier); + + // "zz" is not a valid ISO 639-1 language code + mockMvc.perform(post("/wallets/{walletId}/credentials", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header("Accept-Language", "zz") + .content(createRequestBody(verifiableCredentialRequest)) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Locale must be a valid 2-letter code")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/WalletPresentationsControllerTest.java b/src/test/java/io/mosip/mimoto/controller/WalletPresentationsControllerTest.java new file mode 100644 index 000000000..d1b8a8454 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/WalletPresentationsControllerTest.java @@ -0,0 +1,575 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.PresentationActionService; +import io.mosip.mimoto.service.WalletPresentationService; +import io.mosip.mimoto.service.impl.SessionManager; +import io.mosip.mimoto.util.GlobalExceptionHandler; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WalletPresentationsController.class, GlobalExceptionHandler.class}) +@AutoConfigureMockMvc(addFilters = false) +@EnableWebMvc +@Slf4j +public class WalletPresentationsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private WalletPresentationService walletPresentationService; + + @Mock + private HttpSession httpSession; + + @MockBean + private SessionManager sessionManager; + + @MockBean + private PresentationActionService presentationActionService; + + private final String walletId = "wallet123"; + private final String walletKey = "encodedKey"; + + private VPResponseDTO presentationResponseDTO; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + VerifiablePresentationVerifierDTO presentationVerifierDTO = new VerifiablePresentationVerifierDTO("mock-client", "verifier123", "https://veriifer-logo.png", false, true, "https://verifier-redirect"); + presentationResponseDTO = new VPResponseDTO("presentationId-123", presentationVerifierDTO, false); + + when(httpSession.getAttribute("wallet_id")).thenReturn(walletId); + when(httpSession.getAttribute("wallet_key")).thenReturn(walletKey); + } + + @Test + public void testCreatePresentationSuccess() throws Exception { + String authorizationRequestUrl = "client_id=mock-client&presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequest.getAuthorizationRequestUrl(), walletId)).thenReturn(presentationResponseDTO); + String expectedResponse = new ObjectMapper().writeValueAsString(presentationResponseDTO); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(result -> { + String actualResponse = result.getResponse().getContentAsString(); + assertEquals(expectedResponse, actualResponse, "The response does not match the expected output"); + }); + } + + @Test + public void shouldThrowExceptionWhenBadAuthorizationRequestIsReceivedFromVerifier() throws Exception { + String authorizationRequestUrl = "presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequest.getAuthorizationRequestUrl(), walletId)).thenThrow(new OpenID4VPExceptions.MissingInput(List.of("client_id"), "client_id request param is Missing", "AuthorizationRequest", true)); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Missing Input: client_id param is required")); + } + + @Test + public void shouldThrowSpecificErrorCodeAndMessageWhenAnyCustomExceptionIsThrown() throws Exception { + String authorizationRequestUrl = "presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequest.getAuthorizationRequestUrl(), walletId)).thenThrow(new ApiNotAccessibleException("Error occurred while fetching trusted verifiers")); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("RESIDENT-APP-026")) + .andExpect(jsonPath("$.errorMessage").value("Error occurred while fetching trusted verifiers")); + } + + @Test + public void shouldThrowWalletCreateExceptionWhenAnyUnexpectedErrorOccurs() throws Exception { + String authorizationRequestUrl = "presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequest.getAuthorizationRequestUrl(), walletId)).thenThrow(new RuntimeException("Error occurred while creating presentation in openid4vp flow")); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowUnauthorizedWhenWalletKeyIsMissingFromSession() throws Exception { + String presentationId = "presentation123"; + + when(httpSession.getAttribute("wallet_key")).thenReturn(null); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value("unauthorized")) + .andExpect(jsonPath("$.errorMessage").value("You are not authorized to access this resource")); + } + + @Test + public void shouldThrowBadRequestWhenApiNotAccessibleExceptionIsThrown() throws Exception { + String presentationId = "presentation123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(httpSession, walletId, presentationId)).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new ApiNotAccessibleException("Error occurred while fetching credentials")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowInternalServerErrorWhenIOExceptionIsThrown() throws Exception { + String presentationId = "presentation123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(httpSession, walletId, presentationId)).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new IOException("Error occurred while processing credentials")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowInternalServerErrorWhenVPNotCreatedExceptionIsThrown() throws Exception { + String presentationId = "presentation123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(httpSession, walletId, presentationId)).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new VPNotCreatedException("Error occurred while creating VP")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowInternalServerErrorWhenIllegalArgumentExceptionIsThrown() throws Exception { + String presentationId = "presentation123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(httpSession, walletId, presentationId)).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new IllegalArgumentException("Invalid argument provided")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowBadRequestWhenWalletIdInSessionIsNull() throws Exception { + String presentationId = "presentation123"; + + when(httpSession.getAttribute("wallet_id")).thenReturn(null); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } + + @Test + public void shouldThrowBadRequestWhenWalletIdInSessionDoesNotMatchRequest() throws Exception { + String presentationId = "presentation123"; + String differentWalletId = "differentWallet123"; + + when(httpSession.getAttribute("wallet_id")).thenReturn(differentWalletId); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", differentWalletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Invalid Wallet ID. Session and request Wallet ID do not match")); + } + + @Test + public void shouldThrowInternalServerErrorWhenSessionDataIsNull() throws Exception { + String presentationId = "presentation123"; + + when(sessionManager.getPresentationSessionData(httpSession, walletId, presentationId)).thenReturn(null); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void testUserRejectedVerifierSuccess() throws Exception { + String presentationId = "presentation-123"; + + // create a mock session data object (constructor signature may vary, so mock it) + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + + // Mock the SessionManager to return the correct session data + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))) + .thenReturn(sessionData); + + // Prepare the expected RejectedVerifierDTO and stub the service to return it + SubmitPresentationResponseDTO expectedRejected = SubmitPresentationResponseDTO.builder() + .status("success") + .message("Presentation request rejected. An OpenID4VP error response has been sent to the verifier.") + .redirectUri("") // redirectUri may be empty string or null + .build(); + doReturn(ResponseEntity.ok(expectedRejected)) + .when(presentationActionService).handlePresentationAction( + eq(walletId), eq(presentationId), any(SubmitPresentationRequestDTO.class), eq(sessionData), eq(walletKey)); + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", walletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"errorCode\":\"access_denied\",\"errorMessage\":\"User denied authorization\"}") + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()); + } + + @Test + public void testUserRejectedVerifierInvalidWalletId() throws Exception { + String presentationId = "presentation-123"; + String invalidWalletId = "invalid-wallet"; + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", invalidWalletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"errorCode\":\"access_denied\",\"errorMessage\":\"User denied authorization\"}") + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(presentationActionService); + } + + // NOTE: Exception handling test removed - PresentationActionService handles exceptions internally + // and returns appropriate ResponseEntity. Test moved to PresentationActionServiceTest. + + @Test + public void testUserRejectedVerifierNullSessionData() throws Exception { + String walletId = "wallet-123"; + String presentationId = "presentation-123"; + ErrorDTO payload = new ErrorDTO("access_denied", "User denied authorization"); + + // Arrange: make session manager return null to simulate missing session data + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))) + .thenReturn(null); + + // Serialize payload to ensure MockMvc .content(...) is not null + String content = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(payload); + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", walletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()); + } + + // NOTE: Exception handling test removed - PresentationActionService handles exceptions internally + // and returns appropriate ResponseEntity. Test moved to PresentationActionServiceTest. + + @Test + public void testHandleVPAuthorizationRequestWithURISyntaxException() throws Exception { + String authorizationRequestUrl = "invalid://url with spaces"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequestUrl, walletId)) + .thenThrow(new java.net.URISyntaxException("invalid://url with spaces", "Invalid URI")); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")); + } + + @Test + public void testHandleVPAuthorizationRequestWithVPNotCreatedException() throws Exception { + String authorizationRequestUrl = "client_id=test&response_type=vp_token"; + VPAuthorizationRequestDTO authorizationRequest = new VPAuthorizationRequestDTO(); + authorizationRequest.setAuthorizationRequestUrl(authorizationRequestUrl); + + when(walletPresentationService.handleVPAuthorizationRequest(authorizationRequestUrl, walletId)) + .thenThrow(new VPNotCreatedException("Failed to create VP")); + + mockMvc.perform(post("/wallets/{walletId}/presentations", walletId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(authorizationRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")); + } + + @Test + public void testGetMatchingCredentialsSuccess() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + MatchingCredentialsResponseDTO responseDTO = new MatchingCredentialsResponseDTO(); + MatchingCredentialsDTO matchingCredentials = new MatchingCredentialsDTO(); + matchingCredentials.setMatchingCredentialsResponse(responseDTO); + matchingCredentials.setMatchingCredentials(java.util.Collections.emptyList()); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenReturn(matchingCredentials); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()); + + verify(sessionManager).storeMatchingWalletCredentialsInPresentationSessionData( + any(HttpSession.class), eq(walletId), eq(sessionData), anyList()); + } + + @Test + public void testGetMatchingCredentialsWithIllegalArgumentException() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new IllegalArgumentException("Invalid argument")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").exists()); + } + + @Test + public void testGetMatchingCredentialsWithApiNotAccessibleException() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new ApiNotAccessibleException("API not accessible")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("RESIDENT-APP-026")); + } + + @Test + public void testGetMatchingCredentialsWithIOException() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new java.io.IOException("IO error")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("wallet_vp_creation_failed")); + } + + @Test + public void testGetMatchingCredentialsWithVPNotCreatedException() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))).thenReturn(sessionData); + when(walletPresentationService.getMatchingCredentials(sessionData, walletId, walletKey)) + .thenThrow(new VPNotCreatedException("VP_ERROR", "VP creation failed")); + + mockMvc.perform(get("/wallets/{walletId}/presentations/{presentationId}/credentials", walletId, presentationId) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("VP_ERROR")); + } + + @Test + public void testHandlePresentationSubmissionSuccess() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + SubmitPresentationRequestDTO submitRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(java.util.Arrays.asList("cred-1", "cred-2"))) + .build(); + + SubmitPresentationResponseDTO submitResponse = new SubmitPresentationResponseDTO(); + submitResponse.setStatus("SUCCESS"); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))) + .thenReturn(sessionData); + doReturn(ResponseEntity.ok(submitResponse)) + .when(presentationActionService).handlePresentationAction( + eq(walletId), eq(presentationId), any(SubmitPresentationRequestDTO.class), eq(sessionData), eq(walletKey)); + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", walletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submitRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()); + + } + + @Test + public void testHandlePresentationSubmissionMissingWalletKey() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + SubmitPresentationRequestDTO submitRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(java.util.Arrays.asList("cred-1"))) + .build(); + + when(httpSession.getAttribute("wallet_key")).thenReturn(null); + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))) + .thenReturn(sessionData); + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", walletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(submitRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value("unauthorized")); + } + + @Test + public void testHandlePresentationActionInvalidRequestFormat() throws Exception { + String presentationId = "presentation-123"; + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequestUrl", java.time.Instant.now(), true, null, false); + + SubmitPresentationRequestDTO invalidRequest = SubmitPresentationRequestDTO.builder() + .build(); + + when(sessionManager.getPresentationSessionData(any(HttpSession.class), eq(walletId), eq(presentationId))) + .thenReturn(sessionData); + + // Mock the service to return BAD_REQUEST for invalid format + ErrorDTO errorResponse = new ErrorDTO("invalid_request", "Request must contain either selectedCredentials or both errorCode and errorMessage"); + doReturn(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse)) + .when(presentationActionService).handlePresentationAction( + eq(walletId), eq(presentationId), any(SubmitPresentationRequestDTO.class), eq(sessionData), eq(walletKey)); + + mockMvc.perform(patch("/wallets/{walletId}/presentations/{presentationId}", walletId, presentationId) + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(invalidRequest)) + .accept(MediaType.APPLICATION_JSON) + .sessionAttr("wallet_id", walletId) + .sessionAttr("wallet_key", walletKey)) + .andExpect(status().isOk()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/WalletTrustedVerifiersControllerTest.java b/src/test/java/io/mosip/mimoto/controller/WalletTrustedVerifiersControllerTest.java new file mode 100644 index 000000000..8730db812 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/WalletTrustedVerifiersControllerTest.java @@ -0,0 +1,251 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.TrustedVerifierRequest; +import io.mosip.mimoto.dto.TrustedVerifierResponseDTO; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.service.TrustedVerifierService; +import io.mosip.mimoto.util.GlobalExceptionHandler; +import io.mosip.mimoto.util.WalletUtil; +import jakarta.servlet.http.HttpSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static io.mosip.mimoto.exception.ErrorConstants.DUPLICATE_VERIFIER; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +public class WalletTrustedVerifiersControllerTest { + + @Mock + private TrustedVerifierService trustedVerifierService; + + @InjectMocks + private WalletTrustedVerifiersController controller; + + private MockMvc mockMvc; + private ObjectMapper objectMapper; + private MockHttpSession session; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .setMessageConverters(new MappingJackson2HttpMessageConverter()) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + objectMapper = new ObjectMapper(); + session = new MockHttpSession(); + } + + @Test + void testAddTrustedVerifierSuccess() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId("test-verifier-id"); + + TrustedVerifierResponseDTO response = new TrustedVerifierResponseDTO(); + response.setId("generated-id"); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + when(trustedVerifierService.addTrustedVerifier(walletId, request)).thenReturn(response); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value("generated-id")); + + verify(trustedVerifierService).addTrustedVerifier(walletId, request); + } + } + + @Test + void testAddTrustedVerifierMissingVerifierId() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId(""); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")); + + verifyNoInteractions(trustedVerifierService); + } + } + + @Test + void testAddTrustedVerifierBlankVerifierId() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId(" "); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")); + + verifyNoInteractions(trustedVerifierService); + } + } + + @Test + void testAddTrustedVerifierNullVerifierId() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId(null); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")); + + verifyNoInteractions(trustedVerifierService); + } + } + + @Test + void testAddTrustedVerifierDuplicateVerifier() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId("existing-verifier-id"); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + when(trustedVerifierService.addTrustedVerifier(walletId, request)) + .thenThrow(new InvalidRequestException(DUPLICATE_VERIFIER.getErrorCode(), DUPLICATE_VERIFIER.getErrorMessage())); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").exists()); + + verify(trustedVerifierService).addTrustedVerifier(walletId, request); + } + } + + + @Test + void testAddTrustedVerifierWalletValidationFailure() throws Exception { + // Given + String walletId = "invalid-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId("test-verifier-id"); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenThrow(new RuntimeException("Invalid wallet ID")); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").exists()); + + verifyNoInteractions(trustedVerifierService); + } + } + + @Test + void testAddTrustedVerifierServiceException() throws Exception { + // Given + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId("test-verifier-id"); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + when(trustedVerifierService.addTrustedVerifier(walletId, request)) + .thenThrow(new RuntimeException("Database connection failed")); + + // When & Then + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").exists()); + + verify(trustedVerifierService).addTrustedVerifier(walletId, request); + } + } + + @Test + void testAddTrustedVerifierServiceDataAccessException() throws Exception { + String walletId = "test-wallet-id"; + TrustedVerifierRequest request = new TrustedVerifierRequest(); + request.setVerifierId("test-verifier-id"); + + try (MockedStatic walletUtilMock = mockStatic(WalletUtil.class)) { + walletUtilMock.when(() -> WalletUtil.validateWalletId(any(HttpSession.class), eq(walletId))) + .thenAnswer(invocation -> null); + + when(trustedVerifierService.addTrustedVerifier(walletId, request)) + .thenThrow(new DataAccessResourceFailureException("DB down")); + + mockMvc.perform(post("/wallets/{walletId}/trusted-verifiers", walletId) + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").exists()); + + verify(trustedVerifierService).addTrustedVerifier(walletId, request); + } + } + +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/WalletsControllerTest.java b/src/test/java/io/mosip/mimoto/controller/WalletsControllerTest.java new file mode 100644 index 000000000..bdd21ce10 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/controller/WalletsControllerTest.java @@ -0,0 +1,483 @@ +package io.mosip.mimoto.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import io.mosip.mimoto.exception.WalletLockedException; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.mimoto.service.WalletService; +import io.mosip.mimoto.util.GlobalExceptionHandler; +import jakarta.servlet.http.HttpSession; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.List; + +import static io.mosip.mimoto.util.TestUtilities.createRequestBody; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WalletsController.class, GlobalExceptionHandler.class}) +@AutoConfigureMockMvc +@EnableWebMvc +@EnableWebSecurity +public class WalletsControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private WalletService walletService; + + CreateWalletRequestDto createWalletRequestDto; + + MockHttpSession mockSession; + + String userId, walletName, walletPin, confirmWalletPin, walletId; + + @Before + public void setUp() { + walletName = "My Wallet"; + walletPin = "1234"; + confirmWalletPin = "1234"; + walletId = "walletId123"; + createWalletRequestDto = new CreateWalletRequestDto(); + createWalletRequestDto.setWalletName(walletName); + createWalletRequestDto.setWalletPin(walletPin); + createWalletRequestDto.setConfirmWalletPin(confirmWalletPin); + mockSession = new MockHttpSession(); + mockSession.setAttribute("clientRegistrationId", "google"); + mockSession.setAttribute(SessionKeys.USER_ID, "user123"); + userId = (String) mockSession.getAttribute(SessionKeys.USER_ID); + } + + // Test for creating a wallet + + @Test + public void shouldReturnWalletIdForSuccessfulWalletCreation() throws Exception { + when(walletService.createWallet(userId, walletName, walletPin, confirmWalletPin)).thenReturn( + WalletResponseDto.builder().walletId(walletId).walletName(walletName).build()); + + mockMvc.perform(post("/wallets") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createWalletRequestDto)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()) + .andExpect(content().json("{\"walletId\": \"walletId123\",\"walletName\": \"My Wallet\"}", true)); + } + + @Test + public void shouldThrowExceptionIfAnyErrorOccurredWhenCreatingWallet() throws Exception { + when(walletService.createWallet(userId, walletName, walletPin, confirmWalletPin)) + .thenThrow(new RuntimeException("Exception occurred when creating Wallet for given userId")); + + mockMvc.perform(post("/wallets") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createWalletRequestDto)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowExceptionIfReceivedPINAndConfirmPINDoNotMatch() throws Exception { + createWalletRequestDto.setConfirmWalletPin("2345"); + when(walletService.createWallet(userId, walletName, walletPin, "2345")) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Entered PIN and Confirm PIN do not match")); + + mockMvc.perform(post("/wallets") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createWalletRequestDto)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Entered PIN and Confirm PIN do not match")); + } + + @Test + public void shouldReturnUnauthorizedWhenUserIdIsMissingForCreateWallet() throws Exception { + mockSession.clearAttributes(); + MockHttpSession sessionWithoutUserId = mockSession; + doThrow(new UnauthorizedAccessException(ErrorConstants.UNAUTHORIZED_ACCESS.getErrorCode(), "User ID not found in session")).when(walletService).createWallet(null, walletName, walletPin, confirmWalletPin); + + mockMvc.perform(post("/wallets") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createWalletRequestDto)) + .session(sessionWithoutUserId) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.errorCode").value("unauthorized")) + .andExpect(jsonPath("$.errorMessage").value("User ID not found in session")); + } + + // Test for fetching wallets + + @Test + public void shouldReturnListOfWalletIDsForValidUserId() throws Exception { + List wallets = List.of(new WalletDetailsResponseDto("walletId1", "Wallet1", null), + new WalletDetailsResponseDto("walletId2", "Wallet2", WalletLockStatus.TEMPORARILY_LOCKED)); + when(walletService.getWallets(userId)).thenReturn(wallets); + + mockMvc.perform(get("/wallets") + .accept(MediaType.APPLICATION_JSON) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()) + .andExpect(content().string("[{\"walletId\":\"walletId1\",\"walletName\":\"Wallet1\",\"walletStatus\":null},{\"walletId\":\"walletId2\",\"walletName\":\"Wallet2\",\"walletStatus\":\"temporarily_locked\"}]")); + } + + @Test + public void shouldThrowExceptionIfAnyErrorOccurredWhileFetchingWalletsForGivenUserId() throws Exception { + when(walletService.getWallets(userId)).thenThrow(new RuntimeException("Exception occurred when fetching the wallets for given userId")); + + mockMvc.perform(get("/wallets") + .session(mockSession) + .accept(MediaType.APPLICATION_JSON) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + // Test for deleting a wallet + + @Test + public void shouldDeleteWalletSuccessfully() throws Exception { + mockMvc.perform(delete(String.format("/wallets/%s", walletId)) + .accept(MediaType.APPLICATION_JSON) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()); + } + + @Test + public void shouldClearSessionAttributesWhenDeletingCurrentWallet() throws Exception { + mockSession.setAttribute(SessionKeys.WALLET_ID, walletId); + mockSession.setAttribute(SessionKeys.WALLET_KEY, "walletKey123"); + + mockMvc.perform(delete(String.format("/wallets/%s", walletId)) + .accept(MediaType.APPLICATION_JSON) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()); + + // Session attributes should be cleared + assertEquals(null, mockSession.getAttribute(SessionKeys.WALLET_ID)); + assertEquals(null, mockSession.getAttribute(SessionKeys.WALLET_KEY)); + } + + @Test + public void shouldReturnUnauthorizedWhenUserIdIsMissingForDeleteWallet() throws Exception { + mockSession.clearAttributes(); + MockHttpSession sessionWithoutUserId = mockSession; + doThrow(new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "User ID cannot be null or empty")).when(walletService).deleteWallet(null, walletId); + + mockMvc.perform(delete(String.format("/wallets/%s", walletId)) + .accept(MediaType.APPLICATION_JSON) + .session(sessionWithoutUserId) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("User ID cannot be null or empty")); + } + + @Test + public void shouldReturnBadRequestWhenInvalidRequestExceptionIsThrownByService() throws Exception { + doThrow(new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Wallet not found")).when(walletService).deleteWallet(userId, walletId); + + mockMvc.perform(delete("/wallets/" + walletId) + .accept(MediaType.APPLICATION_JSON) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user("user123").roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Wallet not found")); + } + + // Unlock wallet tests + + @Test + public void shouldUnlockWalletSuccessfully() throws Exception { + String decryptedWalletKey = "decryptedKey"; + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + + when(walletService.unlockWallet(walletId, walletPin, userId)).thenReturn(new WalletResponseDto(walletId, walletName, decryptedWalletKey)); + + MvcResult result = mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.walletId").value(walletId)) + .andReturn(); + + HttpSession httpSession = result.getRequest().getSession(); + assertEquals(httpSession.getAttribute(SessionKeys.WALLET_ID), walletId); + assertEquals(httpSession.getAttribute(SessionKeys.WALLET_KEY), decryptedWalletKey); + } + + @Test + public void shouldThrowExceptionWhenUserIdIsMissingForUnlockWallet() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + when(walletService.unlockWallet(walletId, walletPin, null)) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "User ID cannot be null or empty")); + mockSession.clearAttributes(); + MockHttpSession sessionWithoutUserId = mockSession; + sessionWithoutUserId.setAttribute("clientRegistrationId", "google"); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(sessionWithoutUserId) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("User ID cannot be null or empty")); + } + + @Test + public void shouldThrowExceptionWhenWalletNotFoundForUnlock() throws Exception { + String nonExistentWalletId = "nonExistentWalletId"; + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + + when(walletService.unlockWallet(nonExistentWalletId, walletPin, userId)) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_REQUEST.getErrorCode(), "Wallet not found")); + + mockMvc.perform(post("/wallets/{walletId}/unlock", nonExistentWalletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_request")) + .andExpect(jsonPath("$.errorMessage").value("Wallet not found")); + } + + @Test + public void shouldThrowExceptionWhenInvalidPinProvidedForUnlock() throws Exception { + String invalidPin = "invalidPin"; + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(invalidPin); + + when(walletService.unlockWallet(walletId, invalidPin, userId)) + .thenThrow(new InvalidRequestException("invalid_pin", "Invalid PIN or wallet key provided")); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("invalid_pin")) + .andExpect(jsonPath("$.errorMessage").value("Invalid PIN or wallet key provided")); + } + + @Test + public void shouldThrowTemporarilyLockedExceptionWhenUnlockingWalletWhichIsAlreadyLockedTemporarily() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + + String errorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage() + " for 1 hour(s)"; + when(walletService.unlockWallet(walletId, walletPin, userId)) + .thenThrow(new WalletLockedException(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), errorMessage)); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value(errorMessage)); + } + + @Test + public void shouldThrowTemporarilyLockedExceptionWhenUnlockingWalletWithInvalidPinInLastAttemptBeforeTemporaryLock() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin("invalidPin"); + + String errorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage() + " for 1 hour(s)"; + when(walletService.unlockWallet(walletId, "invalidPin", userId)) + .thenThrow(new WalletLockedException(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), errorMessage)); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value(errorMessage)); + } + + @Test + public void shouldThrowPermanentlyLockedExceptionWhenUnlockingWalletWhichIsAlreadyLockedPermanently() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + + String errorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + when(walletService.unlockWallet(walletId, walletPin, userId)) + .thenThrow(new WalletLockedException(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), errorMessage)); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value(errorMessage)); + } + + @Test + public void shouldThrowExceptionWhenUnlockingWalletWithInvalidPinInLastSecondAttemptBeforePermanentLock() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin("invalidPin"); + + String errorMessage = ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage(); + when(walletService.unlockWallet(walletId, "invalidPin", userId)) + .thenThrow(new InvalidRequestException(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), errorMessage)); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value(errorMessage)); + } + + @Test + public void shouldThrowPermanentlyLockedExceptionWhenUnlockingWalletWithInvalidPinInLastAttemptBeforePermanentLock() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin("invalidPin"); + + String errorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + when(walletService.unlockWallet(walletId, "invalidPin", userId)) + .thenThrow(new WalletLockedException(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), errorMessage)); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isLocked()) + .andExpect(jsonPath("$.errorCode").value(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode())) + .andExpect(jsonPath("$.errorMessage").value(errorMessage)); + } + + @Test + public void shouldThrowExceptionForServerErrorDuringUnlock() throws Exception { + UnlockWalletRequestDto unlockRequest = new UnlockWalletRequestDto(); + unlockRequest.setWalletPin(walletPin); + + when(walletService.unlockWallet(walletId, walletPin, userId)) + .thenThrow(new RuntimeException("Error decrypting wallet key")); + + mockMvc.perform(post("/wallets/{walletId}/unlock", walletId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(unlockRequest)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldThrowExceptionIfAnyErrorOccurredWhileFetchingWalletDataForGivenUserIdAndWalletId() throws Exception { + when(walletService.unlockWallet(walletId, walletPin, userId)).thenThrow(new RuntimeException("Exception occurred when fetching the wallet data for given walletId and userId")); + + mockMvc.perform(post(String.format("/wallets/%s/unlock", walletId)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createWalletRequestDto)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errorCode").value("internal_server_error")) + .andExpect(jsonPath("$.errorMessage").value("We are unable to process request now")); + } + + @Test + public void shouldReturnWalletLockedErrorDetailsWhenWalletIDIsMissingInSession() throws Exception { + mockSession.removeAttribute(SessionKeys.WALLET_ID); + when(walletService.unlockWallet(walletId, walletPin, userId)). + thenThrow(new InvalidRequestException("wallet_locked", "Wallet is locked")); + + mockMvc.perform(post(String.format("/wallets/%s/unlock", walletId)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(createRequestBody(createWalletRequestDto)) + .session(mockSession) + .with(SecurityMockMvcRequestPostProcessors.user(userId).roles("USER")) + .with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errorCode").value("wallet_locked")) + .andExpect(jsonPath("$.errorMessage").value("Wallet is locked")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserServiceTest.java b/src/test/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserServiceTest.java new file mode 100644 index 000000000..e0fbe4e52 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/security/oauth2/CustomOAuth2UserServiceTest.java @@ -0,0 +1,253 @@ +package io.mosip.mimoto.security.oauth2; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.config.oauth2.OAuth2ProviderProperties; +import io.mosip.mimoto.dto.ProviderDataConfig; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.service.UserMetadataService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CustomOAuth2UserServiceTest { + + @Mock + private OAuth2ProviderProperties providerProperties; + + @Mock + private UserMetadataService userMetadataService; + + private CustomOAuth2UserService customOAuth2UserService; + + private ClientRegistration clientRegistration; + private OAuth2AccessToken accessToken; + private OAuth2UserRequest oAuth2UserRequest; + private Map providersConfig; + + private static class TestableOAuth2UserService extends CustomOAuth2UserService { + private final OAuth2User testUser; + + public TestableOAuth2UserService(OAuth2ProviderProperties properties, UserMetadataService service, OAuth2User testUser) { + super(properties, service); + this.testUser = testUser; + } + + @Override + protected OAuth2User loadFromProvider(OAuth2UserRequest userRequest) { + return testUser; + } + } + + @BeforeEach + void setUp() { + clientRegistration = ClientRegistration.withRegistrationId("test-provider") + .clientId("test-client-id") + .clientSecret("test-client-secret") + .authorizationUri("https://example.com/oauth/authorize") + .tokenUri("https://example.com/oauth/token") + .userInfoUri("https://example.com/oauth/userinfo") + .userNameAttributeName("sub") + .clientName("Test Provider") + .redirectUri("{baseUrl}/oauth2/callback/{registrationId}") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .build(); + + accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "test-token", + Instant.now(), + Instant.now().plusSeconds(3600) + ); + + oAuth2UserRequest = new OAuth2UserRequest(clientRegistration, accessToken); + providersConfig = new HashMap<>(); + } + + @Test + void testLoadUserSuccessfulFlow() throws DecryptionException { + // Arrange + Map userAttributes = new HashMap<>(); + userAttributes.put("sub", "test-subject"); + userAttributes.put("name", "Test User"); + userAttributes.put("email", "test@example.com"); + userAttributes.put("phone", "123-456-7890"); + userAttributes.put("picture", "https://example.com/avatar.jpg"); + + OAuth2User defaultUser = new DefaultOAuth2User( + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")), + userAttributes, + "sub" + ); + + ProviderDataConfig providerConfig = new ProviderDataConfig(); + providerConfig.setUserNameAttribute("sub"); + providerConfig.setNameAttribute("name"); + providerConfig.setEmailAttribute("email"); + providerConfig.setPhoneNumberAttribute("phone"); + providerConfig.setPictureAttribute("picture"); + + providersConfig.put("test-provider", providerConfig); + when(providerProperties.getProvider()).thenReturn(providersConfig); + when(userMetadataService.updateOrCreateUserMetadata(anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn("test-user-id"); + + customOAuth2UserService = new TestableOAuth2UserService(providerProperties, userMetadataService, defaultUser); + + // Act + OAuth2User actualUser = customOAuth2UserService.loadUser(oAuth2UserRequest); + + // Assert + assertNotNull(actualUser); + assertEquals("Test User", actualUser.getAttributes().get("name")); + assertEquals("test@example.com", actualUser.getAttributes().get("email")); + assertEquals("123-456-7890", actualUser.getAttributes().get("phone")); + assertEquals("https://example.com/avatar.jpg", actualUser.getAttributes().get("picture")); + assertEquals("test-user-id", actualUser.getAttributes().get(SessionKeys.USER_ID)); + + verify(userMetadataService).updateOrCreateUserMetadata( + "test-subject", + "test-provider", + "Test User", + "https://example.com/avatar.jpg", + "test@example.com" + ); + } + + @Test + void testLoadUserWithNestedPictureData() throws DecryptionException { + // Arrange + Map pictureMap = new HashMap<>(); + Map dataMap = new HashMap<>(); + dataMap.put("url", "https://example.com/nested.jpg"); + pictureMap.put("data", dataMap); + + Map userAttributes = new HashMap<>(); + userAttributes.put("sub", "nested-subject"); + userAttributes.put("name", "Nested User"); + userAttributes.put("email", "nested@example.com"); + userAttributes.put("phone", "9999999999"); + userAttributes.put("picture", pictureMap); + + OAuth2User nestedPictureUser = new DefaultOAuth2User( + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")), + userAttributes, + "sub" + ); + + ProviderDataConfig providerConfig = new ProviderDataConfig(); + providerConfig.setUserNameAttribute("sub"); + providerConfig.setNameAttribute("name"); + providerConfig.setEmailAttribute("email"); + providerConfig.setPhoneNumberAttribute("phone"); + providerConfig.setPictureAttribute("picture"); + + providersConfig.put("test-provider", providerConfig); + when(providerProperties.getProvider()).thenReturn(providersConfig); + when(userMetadataService.updateOrCreateUserMetadata(anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn("nested-user-id"); + + customOAuth2UserService = new TestableOAuth2UserService(providerProperties, userMetadataService, nestedPictureUser); + + // Act + OAuth2User actualUser = customOAuth2UserService.loadUser(oAuth2UserRequest); + + // Assert + assertEquals("https://example.com/nested.jpg", actualUser.getAttributes().get("picture")); + assertEquals("nested-user-id", actualUser.getAttributes().get(SessionKeys.USER_ID)); + } + + @Test + void testLoadUserThrowsRuntimeExceptionOnDecryptionFailure() throws DecryptionException { + // Arrange + Map userAttributes = new HashMap<>(); + userAttributes.put("sub", "error-subject"); + userAttributes.put("name", "Error User"); + userAttributes.put("email", "error@example.com"); + userAttributes.put("phone", "0000000000"); + userAttributes.put("picture", "https://example.com/error.jpg"); + + OAuth2User errorUser = new DefaultOAuth2User( + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")), + userAttributes, + "sub" + ); + + ProviderDataConfig providerConfig = new ProviderDataConfig(); + providerConfig.setUserNameAttribute("sub"); + providerConfig.setNameAttribute("name"); + providerConfig.setEmailAttribute("email"); + providerConfig.setPhoneNumberAttribute("phone"); + providerConfig.setPictureAttribute("picture"); + + providersConfig.put("test-provider", providerConfig); + when(providerProperties.getProvider()).thenReturn(providersConfig); + when(userMetadataService.updateOrCreateUserMetadata(any(), any(), any(), any(), any())) + .thenThrow(new DecryptionException(ErrorConstants.DECRYPTION_FAILED.getErrorCode(), "Decryption failed")); + + customOAuth2UserService = new TestableOAuth2UserService(providerProperties, userMetadataService, errorUser); + + // Act & Assert + RuntimeException exception = assertThrows(RuntimeException.class, + () -> customOAuth2UserService.loadUser(oAuth2UserRequest)); + + assertTrue(exception.getCause() instanceof DecryptionException); + assertEquals("Metadata update failed", exception.getMessage()); + } + + @Test + void testLoadUserWithMissingOptionalAttributes() throws DecryptionException { + // Arrange + Map userAttributes = new HashMap<>(); + userAttributes.put("sub", "minimal-subject"); + userAttributes.put("name", "Minimal User"); + + OAuth2User minimalUser = new DefaultOAuth2User( + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")), + userAttributes, + "sub" + ); + + ProviderDataConfig providerConfig = new ProviderDataConfig(); + providerConfig.setUserNameAttribute("sub"); + providerConfig.setNameAttribute("name"); + providerConfig.setEmailAttribute("email"); + providerConfig.setPhoneNumberAttribute("phone"); + providerConfig.setPictureAttribute("picture"); + + providersConfig.put("test-provider", providerConfig); + when(providerProperties.getProvider()).thenReturn(providersConfig); + when(userMetadataService.updateOrCreateUserMetadata(any(), any(), any(), any(), any())) + .thenReturn("minimal-user-id"); + + customOAuth2UserService = new TestableOAuth2UserService(providerProperties, userMetadataService, minimalUser); + + // Act + OAuth2User actualUser = customOAuth2UserService.loadUser(oAuth2UserRequest); + + // Assert + assertEquals("Minimal User", actualUser.getAttributes().get("name")); + assertNull(actualUser.getAttributes().get("email")); + assertNull(actualUser.getAttributes().get("phone")); + assertNull(actualUser.getAttributes().get("picture")); + } +} diff --git a/src/test/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandlerTest.java b/src/test/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandlerTest.java new file mode 100644 index 000000000..c82c20416 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/security/oauth2/OAuth2AuthenticationSuccessHandlerTest.java @@ -0,0 +1,115 @@ +package io.mosip.mimoto.security.oauth2; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.io.IOException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class OAuth2AuthenticationSuccessHandlerTest { + + private OAuth2AuthenticationSuccessHandler successHandler; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HttpSession session; + + @Mock + private OAuth2AuthenticationToken oauth2Token; + + @Mock + private OAuth2User oauth2User; + + private static final String AUTH_SUCCESS_REDIRECT_URL = "https://example.com/passcode"; + private static final String CLIENT_REGISTRATION_ID = "google"; + private static final String DISPLAY_NAME = "John Doe"; + private static final String PROFILE_PICTURE_URL = "https://example.com/profile.jpg"; + private static final String EMAIL = "john.doe@example.com"; + private static final String USER_ID = "user456"; + + @Before + public void setUp() throws Exception { + successHandler = new OAuth2AuthenticationSuccessHandler(AUTH_SUCCESS_REDIRECT_URL); + when(request.getSession(false)).thenReturn(session); + } + + @Test + public void testOnAuthenticationSuccessSetsAttributesAndRedirects() throws Exception { + when(oauth2Token.getAuthorizedClientRegistrationId()).thenReturn(CLIENT_REGISTRATION_ID); + when(oauth2Token.getPrincipal()).thenReturn(oauth2User); + when(oauth2User.getAttribute("name")).thenReturn(DISPLAY_NAME); + when(oauth2User.getAttribute("picture")).thenReturn(PROFILE_PICTURE_URL); + when(oauth2User.getAttribute("email")).thenReturn(EMAIL); + when(oauth2User.getAttribute("userId")).thenReturn(USER_ID); + + successHandler.onAuthenticationSuccess(request, response, oauth2Token); + + verify(session).setAttribute("clientRegistrationId", CLIENT_REGISTRATION_ID); + verify(session).setAttribute(SessionKeys.USER_ID, USER_ID); + verify(response).sendRedirect(AUTH_SUCCESS_REDIRECT_URL); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserMetadataDTO.class); + verify(session).setAttribute(eq(SessionKeys.USER_METADATA), captor.capture()); + + UserMetadataDTO dto = captor.getValue(); + assertEquals(DISPLAY_NAME, dto.getDisplayName()); + assertEquals(PROFILE_PICTURE_URL, dto.getProfilePictureUrl()); + assertEquals(EMAIL, dto.getEmail()); + } + + @Test + public void testOnAuthenticationSuccessWithNullAttributes() throws Exception { + when(oauth2Token.getAuthorizedClientRegistrationId()).thenReturn(CLIENT_REGISTRATION_ID); + when(oauth2Token.getPrincipal()).thenReturn(oauth2User); + when(oauth2User.getAttribute("name")).thenReturn(null); + when(oauth2User.getAttribute("picture")).thenReturn(null); + when(oauth2User.getAttribute("email")).thenReturn(null); + when(oauth2User.getAttribute("userId")).thenReturn(USER_ID); + + successHandler.onAuthenticationSuccess(request, response, oauth2Token); + + verify(session).setAttribute("clientRegistrationId", CLIENT_REGISTRATION_ID); + verify(session).setAttribute(SessionKeys.USER_ID, USER_ID); + verify(response).sendRedirect(AUTH_SUCCESS_REDIRECT_URL); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserMetadataDTO.class); + verify(session).setAttribute(eq(SessionKeys.USER_METADATA), captor.capture()); + + UserMetadataDTO dto = captor.getValue(); + assertNull(dto.getDisplayName()); + assertNull(dto.getProfilePictureUrl()); + assertNull(dto.getEmail()); + } + + @Test + public void testOnAuthenticationSuccessWithNullSessionThrows() throws IOException { + when(request.getSession(false)).thenReturn(null); + + ServletException thrown = assertThrows(ServletException.class, () -> + successHandler.onAuthenticationSuccess(request, response, oauth2Token) + ); + + assertEquals("Session not available", thrown.getMessage()); + verify(response, never()).sendRedirect(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/CredentialFormatHandlerFactoryTest.java b/src/test/java/io/mosip/mimoto/service/CredentialFormatHandlerFactoryTest.java new file mode 100644 index 000000000..8900c75fd --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialFormatHandlerFactoryTest.java @@ -0,0 +1,201 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.CredentialFormat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CredentialFormatHandlerFactoryTest { + + private CredentialFormatHandler mockDcSdJwtHandler; + private CredentialFormatHandler mockVcSdJwtHandler; + private CredentialFormatHandler mockLdpVcHandler; + + private CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + @BeforeEach + void setUp() { + mockDcSdJwtHandler = mock(CredentialFormatHandler.class); + mockLdpVcHandler = mock(CredentialFormatHandler.class); + mockVcSdJwtHandler = mock(CredentialFormatHandler.class); + } + + private static Map toHandlerMap(List handlers) { + return handlers.stream() + .collect(Collectors.toMap( + CredentialFormatHandler::getSupportedFormat, + h -> h + )); + } + + @Test + void constructorWithValidHandlersShouldInitializeHandlersMap() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + when(mockLdpVcHandler.getSupportedFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + when(mockVcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.VC_SD_JWT.getFormat()); + + List handlers = Arrays.asList( + mockDcSdJwtHandler, mockLdpVcHandler, mockVcSdJwtHandler + ); + + // When + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // Then + assertNotNull(credentialFormatHandlerFactory); + verify(mockDcSdJwtHandler, times(1)).getSupportedFormat(); + verify(mockLdpVcHandler, times(1)).getSupportedFormat(); + verify(mockVcSdJwtHandler, times(1)).getSupportedFormat(); + } + + @Test + void constructorWithEmptyHandlersListShouldInitializeEmptyMap() { + // Given + List handlers = Collections.emptyList(); + + // When + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // Then + assertNotNull(credentialFormatHandlerFactory); + } + + @Test + void getHandlerWithValidDcSdJwtFormatShouldReturnCorrectHandler() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + List handlers = Arrays.asList(mockDcSdJwtHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // When + CredentialFormatHandler result = credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat()); + + // Then + assertNotNull(result); + assertEquals(mockDcSdJwtHandler, result); + } + + @Test + void getHandlerWithValidLdpVcFormatShouldReturnCorrectHandler() { + // Given + when(mockLdpVcHandler.getSupportedFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + List handlers = Arrays.asList(mockLdpVcHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // When + CredentialFormatHandler result = credentialFormatHandlerFactory.getHandler(CredentialFormat.LDP_VC.getFormat()); + + // Then + assertNotNull(result); + assertEquals(mockLdpVcHandler, result); + } + + @Test + void getHandlerWithUnsupportedFormatShouldThrowIllegalArgumentException() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + List handlers = Arrays.asList(mockDcSdJwtHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + String unsupportedFormat = "unsupported_format"; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> credentialFormatHandlerFactory.getHandler(unsupportedFormat) + ); + + assertEquals("Unsupported credential format: " + unsupportedFormat, exception.getMessage()); + } + + @Test + void getHandlerWithNullFormatShouldThrowIllegalArgumentException() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + List handlers = Arrays.asList(mockDcSdJwtHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> credentialFormatHandlerFactory.getHandler(null) + ); + + assertEquals("Unsupported credential format: null", exception.getMessage()); + } + + @Test + void getHandlerWithEmptyFormatShouldThrowIllegalArgumentException() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + List handlers = Arrays.asList(mockDcSdJwtHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + String emptyFormat = ""; + + // When & Then + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> credentialFormatHandlerFactory.getHandler(emptyFormat) + ); + + assertEquals("Unsupported credential format: ", exception.getMessage()); + } + + @Test + void getHandlerWithMultipleHandlersShouldReturnCorrectHandlerForEachFormat() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + when(mockLdpVcHandler.getSupportedFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + + List handlers = Arrays.asList( + mockDcSdJwtHandler, mockLdpVcHandler + ); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + // When & Then + CredentialFormatHandler sdJwtResult = credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat()); + CredentialFormatHandler ldpVcResult = credentialFormatHandlerFactory.getHandler(CredentialFormat.LDP_VC.getFormat()); + + assertEquals(mockDcSdJwtHandler, sdJwtResult); + assertEquals(mockLdpVcHandler, ldpVcResult); + } + + @Test + void getHandlerWithVcSdJwtFormatButNoDcSdJwtHandlerShouldThrowException() { + // Given - Only LDP VC handler available, no DC_SD_JWT handler + when(mockLdpVcHandler.getSupportedFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + List handlers = Arrays.asList(mockLdpVcHandler); + credentialFormatHandlerFactory = new CredentialFormatHandlerFactory(toHandlerMap(handlers)); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat()) + ); + + assertEquals("Unsupported credential format: " + CredentialFormat.VC_SD_JWT.getFormat(), + exception.getMessage()); + } + + @Test + void constructorWithNullHandlerInListShouldHandleGracefully() { + // Given + when(mockDcSdJwtHandler.getSupportedFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + List handlers = Arrays.asList(mockDcSdJwtHandler, null); + + // When & Then - Should handle null handler gracefully (might throw NPE during construction) + assertThrows(NullPointerException.class, () -> new CredentialFormatHandlerFactory(toHandlerMap(handlers))); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/CredentialMatchingServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialMatchingServiceTest.java new file mode 100644 index 000000000..3e148b3f6 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialMatchingServiceTest.java @@ -0,0 +1,2783 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.service.impl.CredentialMatchingServiceImpl; +import io.mosip.mimoto.service.impl.OpenID4VPService; +import io.mosip.openID4VP.authorizationRequest.presentationDefinition.*; +import io.mosip.openID4VP.dcql.query.ClaimValue; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.CredentialSetQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Method; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class CredentialMatchingServiceTest { + + @InjectMocks + private CredentialMatchingServiceImpl credentialMatchingService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private WalletCredentialService walletCredentialService; + + @Mock + private DataProtectionService dataProtectionService; + + @Mock + private IssuersService issuersService; + + @Mock + private OpenID4VPService openID4VPService; + + @Mock + private CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + @Mock + private CredentialFormatHandler credentialFormatHandler; + + private VerifiablePresentationSessionData sessionData; + private String walletId; + private String base64Key; + private PresentationDefinition presentationDefinition; + private List walletCredentials; + + @Before + public void setUp() throws JsonProcessingException { + walletId = "test-wallet-id"; + base64Key = "test-base64-key"; + sessionData = new VerifiablePresentationSessionData(); + sessionData.setPresentationId("presentation-123"); + sessionData.setAuthorizationRequest("client_id=test-client&response_type=vp_token"); + presentationDefinition = createMockPresentationDefinition(); + walletCredentials = createMockWalletCredentials(); + + when(credentialFormatHandlerFactory.getHandler(eq("ldp_vc"))).thenReturn(credentialFormatHandler); + when(credentialFormatHandlerFactory.getHandler(eq(CredentialFormat.VC_SD_JWT.getFormat()))).thenReturn(credentialFormatHandler); + + when(credentialFormatHandler.extractAllCredentialProperties(any(VCCredentialResponse.class))) + .thenAnswer(invocation -> { + VCCredentialResponse vc = invocation.getArgument(0); + String format = vc.getFormat(); + + if (CredentialFormat.isSdJwt(format)) { + Map publicClaims = new LinkedHashMap<>(); + publicClaims.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + Map> result = new LinkedHashMap<>(); + result.put("publicClaims", publicClaims); + result.put("sdClaims", new LinkedHashMap<>()); + return result; + } + + Object credential = vc.getCredential(); + if (credential instanceof Map) { + return new LinkedHashMap<>((Map) credential); + } + return new LinkedHashMap<>(); + }); + } + + @Test + public void testGetMatchingCredentialsSuccess() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + IssuerConfig issuerConfig = createMockIssuerConfig(); + when(issuersService.getIssuerConfig(anyString(), anyString())) + .thenReturn(issuerConfig); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertNotNull(result.getMatchingCredentials()); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + + verify(openID4VPService).resolvePresentationDefinition(any(), any(), anyBoolean()); + verify(walletCredentialService).getDecryptedCredentials(eq(walletId), any()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsNullWalletId() throws Exception { + // Act + credentialMatchingService.getMatchingCredentials(sessionData, null, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsEmptyWalletId() throws Exception { + // Act + credentialMatchingService.getMatchingCredentials(sessionData, "", base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsNullSessionData() throws Exception { + credentialMatchingService.getMatchingCredentials(null, walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsEmptySessionData() throws Exception { + credentialMatchingService.getMatchingCredentials(new VerifiablePresentationSessionData(), walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsBlankPresentationId() throws Exception { + sessionData.setPresentationId(" "); + credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsBlankAuthorizationRequest() throws Exception { + sessionData.setAuthorizationRequest(""); + credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsNullBase64Key() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + + // Act + credentialMatchingService.getMatchingCredentials(sessionData, walletId, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsEmptyInputDescriptors() throws Exception { + // Arrange + PresentationDefinition emptyPd = new PresentationDefinition("test", Collections.emptyList(), null, null, null); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(emptyPd); + + // Act + credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsEmptyInputDescriptorId() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test", Arrays.asList(descriptor), null, null, null); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + + // Act + credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsWhitespaceWalletId() throws Exception { + // Act + credentialMatchingService.getMatchingCredentials(sessionData, " ", base64Key); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetMatchingCredentialsWhitespaceBase64Key() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + + // Act + credentialMatchingService.getMatchingCredentials(sessionData, walletId, " "); + } + + @Test + public void testGetMatchingCredentialsEmptyWalletCredentials() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Collections.emptyList()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentials().isEmpty()); + assertFalse(result.getMatchingCredentialsResponse().getMissingClaims().isEmpty()); + assertNull(result.getMatchingCredentialsResponse().getQueryGroups()); + } + + @Test + public void testGetMatchingCredentialsDecryptionFailure() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(walletCredentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithConstraints() throws Exception { + // Arrange + PresentationDefinition pdWithConstraints = createMockPresentationDefinitionWithConstraints(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithConstraints); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + } + + @Test + public void testGetMatchingCredentialsFormatMismatch() throws Exception { + // Arrange + PresentationDefinition pdWithFormat = createMockPresentationDefinitionWithSpecificFormat(); + List credentialsWithDifferentFormat = createMockWalletCredentialsWithSdJwtFormat(); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithFormat); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentialsWithDifferentFormat); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsFormatMatchingWithProofType() throws Exception { + // Arrange + PresentationDefinition pdWithFormat = createMockPresentationDefinitionWithSpecificFormat(); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithFormat); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMatchingFormat()); + when(objectMapper.convertValue(any(), eq(VCCredentialProperties.class))) + .thenReturn(createMockVCCredentialPropertiesWithProof()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsFormatMatchingWithoutProofType() throws Exception { + // Arrange + PresentationDefinition pdWithoutProofType = createMockPresentationDefinitionWithoutProofType(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithoutProofType); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsFormatMatchingWithNullFormat() throws Exception { + // Arrange + PresentationDefinition pdWithNullFormat = createMockPresentationDefinitionWithNullFormat(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithNullFormat); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithConstraintsMatching() throws Exception { + // Arrange + PresentationDefinition pdWithConstraints = createMockPresentationDefinitionWithConstraints(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithConstraints); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSubject()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + } + + @Test + public void testGetMatchingCredentialsWithConstraintsNotMatching() throws Exception { + // Arrange + PresentationDefinition pdWithConstraints = createMockPresentationDefinitionWithConstraints(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithConstraints); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(walletCredentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertFalse(result.getMatchingCredentialsResponse().getMissingClaims().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithNullConstraints() throws Exception { + // Arrange + PresentationDefinition pdWithNullConstraints = createMockPresentationDefinitionWithNullConstraints(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithNullConstraints); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithEmptyFieldPaths() throws Exception { + // Arrange + PresentationDefinition pdWithEmptyPaths = createMockPresentationDefinitionWithEmptyPaths(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithEmptyPaths); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithFilterNotMatching() throws Exception { + // Arrange + PresentationDefinition pdWithFilter = createMockPresentationDefinitionWithFilter(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithFilter); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(walletCredentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithNullFilter() throws Exception { + // Arrange + PresentationDefinition pdWithNullFilter = createMockPresentationDefinitionWithNullFilter(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithNullFilter); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithJsonPathErrors() throws Exception { + // Arrange + PresentationDefinition pdWithInvalidPath = createMockPresentationDefinitionWithInvalidPath(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithInvalidPath); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(walletCredentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsWithSdJwtFormat() throws Exception { + // Arrange + PresentationDefinition pd = createMockPresentationDefinition(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSdJwtFormat()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert - PD has no format constraint; SD-JWT credential with $.type satisfies the constraint + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertEquals(CredentialFormat.VC_SD_JWT.getFormat(), + result.getMatchingCredentialsResponse().getAvailableCredentials().get(0).getFormat()); + } + + @Test + public void testGetMatchingCredentialsWithDcSdJwtFormat() throws Exception { + // Arrange + PresentationDefinition pd = createMockPresentationDefinition(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + when(credentialFormatHandlerFactory.getHandler(eq(CredentialFormat.DC_SD_JWT.getFormat()))) + .thenReturn(credentialFormatHandler); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithDcSdJwtFormat()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert - dc+sd-jwt is handled like vc+sd-jwt and surfaces as an available credential + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertEquals(CredentialFormat.DC_SD_JWT.getFormat(), + result.getMatchingCredentialsResponse().getAvailableCredentials().get(0).getFormat()); + } + + @Test + public void testGetMatchingCredentialsWithIssuerConfigException() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(issuersService.getIssuerConfig(anyString(), anyString())) + .thenThrow(new InvalidIssuerIdException()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertEquals("Unknown Credential", + result.getMatchingCredentialsResponse().getAvailableCredentials().get(0).getCredentialTypeDisplayName()); + } + + @Test + public void testGetMatchingCredentialsWithApiNotAccessibleException() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(presentationDefinition); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(issuersService.getIssuerConfig(anyString(), anyString())) + .thenThrow(new ApiNotAccessibleException()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertEquals("Unknown Credential", + result.getMatchingCredentialsResponse().getAvailableCredentials().get(0).getCredentialTypeDisplayName()); + } + + @Test + public void testGetMatchingCredentialsWithMultipleDescriptors() throws Exception { + // Arrange + PresentationDefinition pdWithMultipleDescriptors = createMockPresentationDefinitionWithMultipleDescriptors(); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pdWithMultipleDescriptors); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMultipleCredentials()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + Set credentialIds = result.getMatchingCredentialsResponse().getAvailableCredentials() + .stream().map(CredentialDTO::getCredentialId).collect(Collectors.toSet()); + assertEquals(2, credentialIds.size()); + } + + private PresentationDefinition createMockPresentationDefinition() { + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + return pd; + } + + private PresentationDefinition createMockPresentationDefinitionWithConstraints() { + Fields field1 = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Filter filter = new Filter("type", "String"); + Fields field2 = new Fields(Arrays.asList("$.credentialSubject.name"), null, null, null, filter, null); + Constraints constraints = new Constraints(Arrays.asList(field1, field2), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor-with-constraints", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition-with-constraints", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithSpecificFormat() { + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + ldpVcFormat.put("proof_type", Arrays.asList("Ed25519Signature2020")); + format.put("ldp_vc", ldpVcFormat); + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + return pd; + } + + private List createMockWalletCredentials() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialProperties properties = new VCCredentialProperties(); + properties.setType(Arrays.asList("VerifiableCredential", "TestCredential")); + VCCredentialResponse response = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(properties) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private VCCredentialResponse createMockVCCredentialResponse() { + VCCredentialResponse response = new VCCredentialResponse(); + response.setFormat(CredentialFormat.LDP_VC.getFormat()); + + Map credentialData = new HashMap<>(); + credentialData.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + credentialData.put("credentialSubject", createMockCredentialSubject()); + + response.setCredential(credentialData); + return response; + } + + private VCCredentialResponse createMockVCCredentialResponseWithSubject() { + VCCredentialResponse response = createMockVCCredentialResponse(); + @SuppressWarnings("unchecked") + Map credentialData = (Map) response.getCredential(); + Map subject = new HashMap<>(); + subject.put("name", "John Doe"); + credentialData.put("credentialSubject", subject); + return response; + } + + private Map createMockCredentialSubject() { + Map subject = new HashMap<>(); + subject.put("id", "did:example:123456789"); + subject.put("name", "Test Subject"); + return subject; + } + + private Map createMockCredentialMap() { + Map credentialMap = new HashMap<>(); + credentialMap.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + credentialMap.put("credentialSubject", createMockCredentialSubject()); + return credentialMap; + } + + private IssuerConfig createMockIssuerConfig() { + IssuerDTO issuerDTO = new IssuerDTO(); + DisplayDTO display = new DisplayDTO(); + display.setName("Test Issuer"); + LogoDTO logoDTO = new LogoDTO(); + logoDTO.setUrl("dsds"); + display.setLogo(logoDTO); + issuerDTO.setDisplay(Collections.singletonList(display)); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + CredentialSupportedDisplayResponse credentialSupportedDisplayResponse = new CredentialSupportedDisplayResponse(); + credentialSupportedDisplayResponse.setName("test"); + credentialSupportedDisplayResponse.setLogo(logoDTO); + credentialsSupportedResponse.setDisplay(Collections.singletonList(credentialSupportedDisplayResponse)); + IssuerConfig config = new IssuerConfig(issuerDTO, new CredentialIssuerWellKnownResponse(), credentialsSupportedResponse); + return config; + } + + private PresentationDefinition createMockPresentationDefinitionWithoutProofType() { + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + format.put("ldp_vc", ldpVcFormat); + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, format, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithNullFormat() { + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithNullConstraints() { + Constraints constraints = new Constraints(null, null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithEmptyPaths() { + Fields field = new Fields(Collections.emptyList(), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithFilter() { + Filter filter = new Filter("John", "String"); + Fields field = new Fields(Arrays.asList("$.credentialSubject.name"), null, null, null, filter, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithNullFilter() { + Fields field = new Fields(Arrays.asList("$.credentialSubject.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithInvalidPath() { + Fields field = new Fields(Arrays.asList("$.invalid.path"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + } + + private PresentationDefinition createMockPresentationDefinitionWithMultipleDescriptors() { + Fields field1 = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints1 = new Constraints(Collections.singletonList(field1), null); + InputDescriptor descriptor1 = new InputDescriptor("test-descriptor-1", null, null, null, constraints1); + + Fields field2 = new Fields(Arrays.asList("$.credentialSubject.id"), null, null, null, null, null); + Constraints constraints2 = new Constraints(Collections.singletonList(field2), null); + InputDescriptor descriptor2 = new InputDescriptor("test-descriptor-2", null, null, null, constraints2); + + return new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor1, descriptor2), null, null, null); + } + + private List createMockWalletCredentialsWithMatchingFormat() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(createMockCredentialMap()) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private List createMockWalletCredentialsWithSubject() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = createMockVCCredentialResponseWithSubject(); + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private List createMockWalletCredentialsWithSdJwtFormat() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlRlc3RDcmVkZW50aWFsIl19.signature") + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private List createMockWalletCredentialsWithDcSdJwtFormat() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + Map credentialData = new HashMap<>(); + credentialData.put("iss", "https://example.com"); + credentialData.put("sub", "did:example:123456789"); + credentialData.put("type", Arrays.asList("DataCredential", "TestCredential")); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format(CredentialFormat.DC_SD_JWT.getFormat()) + .credential(credentialData) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private List createMockWalletCredentialsWithMultipleCredentials() throws JsonProcessingException { + DecryptedCredentialDTO credential1 = new DecryptedCredentialDTO(); + credential1.setId("test-credential-id-1"); + credential1.setWalletId(walletId); + credential1.setCredential(createMockVCCredentialResponse()); + CredentialMetadata metadata1 = new CredentialMetadata(); + metadata1.setIssuerId("test-issuer-id-1"); + metadata1.setCredentialType("TestCredential1"); + credential1.setCredentialMetadata(metadata1); + credential1.setCreatedAt(Instant.now()); + credential1.setUpdatedAt(Instant.now()); + + DecryptedCredentialDTO credential2 = new DecryptedCredentialDTO(); + credential2.setId("test-credential-id-2"); + credential2.setWalletId(walletId); + credential2.setCredential(createMockVCCredentialResponseWithSubject()); + CredentialMetadata metadata2 = new CredentialMetadata(); + metadata2.setIssuerId("test-issuer-id-2"); + metadata2.setCredentialType("TestCredential2"); + credential2.setCredentialMetadata(metadata2); + credential2.setCreatedAt(Instant.now()); + credential2.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential1, credential2); + } + + private VCCredentialProperties createMockVCCredentialPropertiesWithProof() { + VCCredentialProperties properties = new VCCredentialProperties(); + properties.setType(Arrays.asList("VerifiableCredential", "TestCredential")); + + VCCredentialResponseProof proof = new VCCredentialResponseProof(); + proof.setType("Ed25519Signature2020"); + properties.setProof(proof); + + return properties; + } + + private List createMockWalletCredentialsWithMapData() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + Map credentialData = new HashMap<>(); + credentialData.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + Map subject = new HashMap<>(); + subject.put("id", "did:example:123456789"); + subject.put("name", "John Doe"); + credentialData.put("credentialSubject", subject); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialData) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + private List createMockWalletCredentialsWithSimpleFilterData() throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + Map credentialData = new HashMap<>(); + credentialData.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + credentialData.put("name", "John Doe"); + credentialData.put("id", "did:example:123456789"); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialData) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + @Test + public void testJsonPathEvaluationTypePathWithSampleData() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + credentialData.put("id", "did:rcw:2121a1df-29fe-496e-ae24-219dcOb3ac49"); + credentialData.put("type", Arrays.asList("VerifiableCredential", "LifeInsuranceCredential")); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + List credentials = createMockWalletCredentialsWithSampleData(credentialData); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + IssuerConfig mockIssuerConfig = createMockIssuerConfig(); + when(issuersService.getIssuerConfig(eq("test-issuer-id"), eq("TestCredential"))) + .thenReturn(mockIssuerConfig); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testJsonPathEvaluationNestedPath() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + Map credentialSubject = new HashMap<>(); + credentialSubject.put("name", "John Doe"); + credentialSubject.put("age", 30); + credentialData.put("credentialSubject", credentialSubject); + credentialData.put("type", Arrays.asList("VerifiableCredential", "IdentityCredential")); + + Fields field = new Fields(Arrays.asList("$.credentialSubject.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + List credentials = createMockWalletCredentialsWithSampleData(credentialData); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + IssuerConfig mockIssuerConfig = createMockIssuerConfig(); + when(issuersService.getIssuerConfig(eq("test-issuer-id"), eq("TestCredential"))) + .thenReturn(mockIssuerConfig); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testJsonPathEvaluationNonExistentPath() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + credentialData.put("id", "did:rcw:2121a1df-29fe-496e-ae24-219dcOb3ac49"); + credentialData.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + + Fields field = new Fields(Arrays.asList("$.nonExistentField"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + List credentials = createMockWalletCredentialsWithSampleData(credentialData); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testJsonPathEvaluationComplexNestedStructure() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + Map credentialSubject = new HashMap<>(); + Map address = new HashMap<>(); + address.put("street", "123 Main St"); + address.put("city", "New York"); + credentialSubject.put("address", address); + credentialSubject.put("name", "John Doe"); + credentialData.put("credentialSubject", credentialSubject); + credentialData.put("type", Arrays.asList("VerifiableCredential", "IdentityCredential")); + + Fields field = new Fields(Arrays.asList("$.credentialSubject.address.city"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + List credentials = createMockWalletCredentialsWithSampleData(credentialData); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + IssuerConfig mockIssuerConfig = createMockIssuerConfig(); + when(issuersService.getIssuerConfig(eq("test-issuer-id"), eq("TestCredential"))) + .thenReturn(mockIssuerConfig); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testJsonPathEvaluationListIndexPath() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + credentialData.put("items", Arrays.asList("item1", "item2", "item3")); + credentialData.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + + Fields field = new Fields(Arrays.asList("$.items.1"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-presentation-definition", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + List credentials = createMockWalletCredentialsWithSampleData(credentialData); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + private List createMockWalletCredentialsWithSampleData(Map credentialData) throws JsonProcessingException { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialData) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Arrays.asList(credential); + } + + @Test + public void testGetMatchingCredentialsNullPresentationDefinitionFromService() throws Exception { + // Arrange + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(null); + + // Act & Assert + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + }); + + assertEquals("Presentation definition cannot be null", exception.getMessage()); + } + + @Test + public void testExtractRequiredClaimsBlankPath() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList(" "), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test-descriptor", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())) + .thenReturn(pd); + + List credentials = createMockWalletCredentialsWithMapData(); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(credentials); + + // Act + MatchingCredentialsDTO result = credentialMatchingService + .getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertNotNull(result.getMatchingCredentialsResponse()); + } + + @Test + public void testMatchesSdJwtFormatWhenAlgorithmMatchesCredentialIsSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + + Map publicClaims = new LinkedHashMap<>(); + publicClaims.put("type", Arrays.asList("VerifiableCredential", "TestCredential")); + setupSdJwtHandlerMock(publicClaims, new LinkedHashMap<>()); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertEquals(CredentialFormat.VC_SD_JWT.getFormat(), + result.getMatchingCredentialsResponse().getAvailableCredentials().get(0).getFormat()); + } + + @Test + public void testMatchesSdJwtFormatWhenAlgorithmDoesNotMatchCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("ES256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenNoAlgValuesInFormatCredentialIsSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), new HashMap<>()); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + Map publicClaims = new LinkedHashMap<>(); + publicClaims.put("type", Arrays.asList("VerifiableCredential")); + setupSdJwtHandlerMock(publicClaims, new LinkedHashMap<>()); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert - no sd-jwt_alg_values constraint means any algorithm is acceptable + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenCredentialIsNullCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(null))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenCredentialIsNotStringCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + + DecryptedCredentialDTO cred = new DecryptedCredentialDTO(); + cred.setId("sd-jwt-cred"); + cred.setWalletId(walletId); + cred.setCredential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(Map.of("key", "value")) + .build()); + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + cred.setCredentialMetadata(metadata); + cred.setCreatedAt(Instant.now()); + cred.setUpdatedAt(Instant.now()); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(cred)); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenFormatConfigValueIsNullCredentialSelected() throws Exception { + // Arrange - sd-jwt_alg_values key present but value is explicitly null → any alg acceptable + Map> sdJwtFormatConfig = new HashMap<>(); + sdJwtFormatConfig.put("sd-jwt_alg_values", null); + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), sdJwtFormatConfig); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert - null alg list means no restriction, credential should be selected + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenJwtHeaderMissingAlgCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_NO_ALG))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenEmptyJwtStringCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(" "))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesSdJwtFormatWhenJwtHeaderAlgIsNonStringCredentialNotSelected() throws Exception { + // Arrange + PresentationDefinition pd = createSdJwtPresentationDefinition(Arrays.asList("HS256")); + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_NON_STRING_ALG))); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetCredentialDataWhenSdJwtExtractsNullCredentialNotSelected() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + when(credentialFormatHandler.extractAllCredentialProperties(any(VCCredentialResponse.class))) + .thenReturn(null); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testGetCredentialDataWhenSdJwtFlattensPropertiesCredentialSelected() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("name", "John"); + setupSdJwtHandlerMock(new LinkedHashMap<>(), sdClaims); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + + @Test + public void testBuildAvailableCredentialWhenSdJwtWithNullClaimsReturnsEmptyClaimLists() throws Exception { + // Arrange + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(null, null)); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertTrue(credential.getClaims().isEmpty()); + assertTrue(credential.getSdClaims().isEmpty()); + assertEquals(CredentialFormat.VC_SD_JWT.getFormat(), credential.getFormat()); + } + + @Test + public void testBuildAvailableCredentialWhenSdJwtWithCredentialSubjectExtractsFromSubject() throws Exception { + Map publicClaims = new LinkedHashMap<>(); + Map subject = new LinkedHashMap<>(); + subject.put("name", "John"); + subject.put("age", 30); + subject.put("_sd", Arrays.asList("hash1", "hash2")); + Map address = new LinkedHashMap<>(); + address.put("street", "123 Main St"); + address.put("city", "NYC"); + subject.put("address", address); + List> contacts = Arrays.asList( + createMapOf("type", "email", "value", "john@example.com"), + createMapOf("type", "phone", "value", "555-1234")); + subject.put("contacts", contacts); + List privileges = Arrays.asList( + createMapOf("vehicle_code", "C", "issue_date", "2020-01-01")); + subject.put("privileges", privileges); + List tags = new ArrayList<>(); + tags.add(createMapOf("code", "A", "label", "TagA")); + tags.add(createMapOf("code", "B", "label", "TagB")); + tags.add("plain-string"); + subject.put("tags", tags); + List> disjoint = Arrays.asList( + createMapOf("alpha", 1), + createMapOf("beta", 2)); + subject.put("disjoint", disjoint); + publicClaims.put("credentialSubject", subject); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(publicClaims, new LinkedHashMap<>())); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertTrue(credential.getClaims().contains("$.name")); + assertTrue(credential.getClaims().contains("$.age")); + assertTrue(credential.getClaims().contains("$.address.street")); + assertTrue(credential.getClaims().contains("$.address.city")); + assertTrue(credential.getClaims().contains("$.contacts")); + assertFalse(credential.getClaims().contains("$.contacts.type")); + assertFalse(credential.getClaims().contains("$.contacts.value")); + assertTrue(credential.getClaims().contains("$.privileges")); + assertTrue(credential.getClaims().contains("$.privileges.vehicle_code")); + assertTrue(credential.getClaims().contains("$.privileges.issue_date")); + assertTrue(credential.getClaims().contains("$.tags")); + assertTrue(credential.getClaims().contains("$.tags.code")); + assertTrue(credential.getClaims().contains("$.tags.label")); + assertTrue(credential.getClaims().contains("$.disjoint")); + assertTrue(credential.getClaims().contains("$.disjoint.alpha")); + assertTrue(credential.getClaims().contains("$.disjoint.beta")); + assertFalse(credential.getClaims().contains("$._sd")); + assertFalse(credential.getClaims().contains("$.address")); + } + + @Test + public void testBuildAvailableCredentialWhenSdJwtWithoutCredentialSubjectRemovesMetadataAndExtracts() throws Exception { + // Arrange + Map publicClaims = new LinkedHashMap<>(); + publicClaims.put("vct", "IdentityCredential"); + publicClaims.put("iss", "https://example.com"); + publicClaims.put("cnf", new LinkedHashMap<>(Map.of("jwk", "key"))); + publicClaims.put("sub", "did:example:123"); + publicClaims.put("iat", 1234567890); + publicClaims.put("exp", 1234567899); + publicClaims.put("nbf", 1234567880); + publicClaims.put("jti", "urn:uuid:abc"); + publicClaims.put("aud", "https://verifier.com"); + publicClaims.put("_sd", Arrays.asList("hash1")); + publicClaims.put("_sd_alg", "sha-256"); + publicClaims.put("id", "did:example:123"); + publicClaims.put("given_name", "John"); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(publicClaims, new LinkedHashMap<>())); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertTrue(credential.getClaims().contains("$.given_name")); + assertFalse(credential.getClaims().stream().anyMatch(c -> + c.contains("vct") || c.contains("iss") || c.contains("cnf") || c.contains("sub") || + c.contains("_sd") || c.contains("_sd_alg") || c.contains("id"))); + } + + @Test + public void testBuildAvailableCredentialWhenSdJwtWithSdClaimsExtractsSdClaimPaths() throws Exception { + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("family_name", "Doe"); + sdClaims.put("birth_date", "1990-01-01"); + sdClaims.put("credentialSubject.NumberOfCAR", 5); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertEquals(3, credential.getSdClaims().size()); + assertTrue(credential.getSdClaims().contains("$.family_name")); + assertTrue(credential.getSdClaims().contains("$.birth_date")); + assertTrue(credential.getSdClaims().contains("$.NumberOfCAR")); + } + + @Test + public void testBuildAvailableCredentialWhenSdJwtWithIssuerConfigExceptionUsesDefaultDisplayName() throws Exception { + // Arrange + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), new LinkedHashMap<>())); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertEquals("Unknown Credential", credential.getCredentialTypeDisplayName()); + assertNull(credential.getCredentialTypeLogo()); + } + + @Test + public void testCollectPathsWhenMapHasScalarValuesCollectsLeafPaths() throws Exception { + // Arrange + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("given_name", "John"); + sdClaims.put("family_name", "Doe"); + sdClaims.put("age", 30); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(3, paths.size()); + assertTrue(paths.containsAll(Arrays.asList("$.given_name", "$.family_name", "$.age"))); + } + + @Test + public void testExtractSdClaimPathsWhenMapHasNestedMapProducesTopLevelKey() throws Exception { + Map address = new LinkedHashMap<>(); + address.put("street", "123 Main St"); + address.put("city", "NYC"); + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("address", address); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.address")); + } + + @Test + public void testExtractSdClaimPathsWhenMapHasSdKeyIncludesSdKey() throws Exception { + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("name", "John"); + sdClaims.put("_sd", Arrays.asList("hash1", "hash2")); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(2, paths.size()); + assertTrue(paths.contains("$.name")); + assertTrue(paths.contains("$._sd")); + } + + @Test + public void testCollectPathsWhenListHasUniformKeysStopsAtListLevel() throws Exception { + // Arrange + Map sdClaims = new LinkedHashMap<>(); + List> genderList = Arrays.asList( + createMapOf("code", "M", "value", "Male"), + createMapOf("code", "F", "value", "Female")); + sdClaims.put("gender", genderList); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertTrue(paths.contains("$.gender")); + assertFalse(paths.contains("$.gender.code")); + assertFalse(paths.contains("$.gender.value")); + assertEquals(1, paths.size()); + } + + @Test + public void testExtractSdClaimPathsWhenListHasSingleMapItemProducesTopLevelKey() throws Exception { + Map privilege = new LinkedHashMap<>(); + privilege.put("vehicle_category_code", "C"); + privilege.put("issue_date", "2020-01-01"); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("driving_privileges", Arrays.asList(privilege)); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.driving_privileges")); + } + + @Test + public void testCollectPathsWhenListHasNonMapItemsSkipsNonMapItems() throws Exception { + // Arrange + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("tags", Arrays.asList("tag1", "tag2", "tag3")); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.tags")); + } + + @Test + public void testExtractSdClaimPathsWhenListItemsHaveNoCommonKeysProducesTopLevelKey() throws Exception { + Map sdClaims = new LinkedHashMap<>(); + List> items = Arrays.asList( + createMapOf("alpha", 1), + createMapOf("beta", 2)); + sdClaims.put("mixed", items); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.mixed")); + } + + @Test + public void testExtractSdClaimPathsComplexDrivingPrivilegesProducesTopLevelKey() throws Exception { + Map privilege = new LinkedHashMap<>(); + privilege.put("vehicle_category_code", "C"); + privilege.put("issue_date", "2020-01-01"); + privilege.put("expiry_date", "2025-01-01"); + List> codes = Arrays.asList( + createMapOf("code", "A", "value", "V1"), + createMapOf("code", "B", "value", "V2")); + privilege.put("codes", codes); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("driving_privileges", List.of(privilege)); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.driving_privileges")); + } + + @Test + public void testExtractSdClaimPathsWhenListContainsMixedTypesProducesTopLevelKey() throws Exception { + Map sdClaims = new LinkedHashMap<>(); + List mixedList = new ArrayList<>(); + mixedList.add(createMapOf("key1", "val1")); + mixedList.add("plain-string"); + mixedList.add(42); + sdClaims.put("mixed_types", mixedList); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.mixed_types")); + } + + @Test + public void testCollectPathsWhenEmptyMapProducesNoPaths() throws Exception { + // Arrange + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), new LinkedHashMap<>())); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + assertTrue(credential.getSdClaims().isEmpty()); + assertTrue(credential.getClaims().isEmpty()); + } + + @Test + public void testCollectPathsWhenListWithPartiallyOverlappingKeysTreatedAsUniform() throws Exception { + // Arrange + Map sdClaims = new LinkedHashMap<>(); + List> codes = Arrays.asList( + createMapOf("code", "A", "value", "V1", "sign", "+"), + createMapOf("code", "B", "value", "V2")); + sdClaims.put("codes", codes); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertTrue(paths.contains("$.codes")); + assertFalse(paths.contains("$.codes.code")); + assertFalse(paths.contains("$.codes.value")); + assertFalse(paths.contains("$.codes.sign")); + assertEquals(1, paths.size()); + } + + @Test + public void testCollectPathsWhenEmptyListProducesPathButNoRecursion() throws Exception { + // Arrange + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("empty_list", new ArrayList<>()); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + // Assert + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.empty_list")); + } + + @Test + public void testExtractSdClaimPathsWhenDeeplyNestedMapsProducesTopLevelKey() throws Exception { + Map level3 = new LinkedHashMap<>(); + level3.put("deep_value", "found"); + Map level2 = new LinkedHashMap<>(); + level2.put("level3", level3); + Map level1 = new LinkedHashMap<>(); + level1.put("level2", level2); + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("level1", level1); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.level1")); + } + + + @Test + public void testMatchesFormatWhenLdpVcProofTypeNullCredentialNotSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + ldpVcFormat.put("proof_type", Arrays.asList("Ed25519Signature2020")); + format.put("ldp_vc", ldpVcFormat); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + VCCredentialProperties propsWithNullProof = new VCCredentialProperties(); + propsWithNullProof.setType(Arrays.asList("VerifiableCredential")); + propsWithNullProof.setProof(null); + doReturn(propsWithNullProof).when(objectMapper).convertValue(any(), eq(VCCredentialProperties.class)); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFormatWhenLdpVcProofTypeListEmptyCredentialSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + ldpVcFormat.put("proof_type", Collections.emptyList()); + format.put("ldp_vc", ldpVcFormat); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + VCCredentialProperties props = new VCCredentialProperties(); + props.setType(Arrays.asList("VerifiableCredential")); + doReturn(props).when(objectMapper).convertValue(any(), eq(VCCredentialProperties.class)); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFormatWhenLdpVcRequiredProofTypesNullCredentialSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + ldpVcFormat.put("proof_type", null); + format.put("ldp_vc", ldpVcFormat); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + VCCredentialProperties props = new VCCredentialProperties(); + props.setType(Arrays.asList("VerifiableCredential")); + doReturn(props).when(objectMapper).convertValue(any(), eq(VCCredentialProperties.class)); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFormatWhenLdpVcNullLdpVcFormatCredentialSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + format.put("ldp_vc", null); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFormatWhenLdpVcProofTypeMismatchCredentialNotSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + Map> ldpVcFormat = new HashMap<>(); + ldpVcFormat.put("proof_type", Arrays.asList("Ed25519Signature2020")); + format.put("ldp_vc", ldpVcFormat); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMatchingFormat()); + + VCCredentialProperties propsWithWrongProof = new VCCredentialProperties(); + propsWithWrongProof.setType(Arrays.asList("VerifiableCredential")); + VCCredentialResponseProof wrongProof = new VCCredentialResponseProof(); + wrongProof.setType("EcdsaSecp256k1Signature2019"); + propsWithWrongProof.setProof(wrongProof); + when(objectMapper.convertValue(any(), eq(VCCredentialProperties.class))).thenReturn(propsWithWrongProof); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFormatWhenLdpVcCredentialAndDescriptorLacksLdpVcKeyCredentialNotSelected() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), new HashMap<>()); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + + @Test + public void testEvaluateJsonPathWhenPathDoesNotStartWithDollarDotNoMatch() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testEvaluateJsonPathWhenJsonPathValueIsNullNoMatch() throws Exception { + // Arrange + Map credentialData = new HashMap<>(); + credentialData.put("name", null); + credentialData.put("type", Arrays.asList("VerifiableCredential")); + + Fields field = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSampleData(credentialData)); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testEvaluateJsonPathWhenResultIsSingleValueWrapsInList() throws Exception { + // Arrange + Fields nameField = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(nameField), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSimpleFilterData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertFalse(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + + @Test + public void testExtractClaimsFromFieldsWhenFieldsContainNullSkipsNullFields() throws Exception { + // Arrange + List fieldsList = new ArrayList<>(); + fieldsList.add(null); + fieldsList.add(new Fields(Arrays.asList("$.type"), null, null, null, null, null)); + Constraints constraints = new Constraints(fieldsList, null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Collections.emptyList()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("test")); + } + + @Test + public void testExtractClaimsFromFieldsWhenFieldsIsNullReturnsEmptyList() throws Exception { + // Arrange + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), new HashMap<>()); + + Constraints constraints = new Constraints(null, null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertNotNull(result); + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("sd-jwt_alg_values")); + } + + @Test + public void testExtractRequiredClaimsWhenDeduplicateEnabledRemovesDuplicates() throws Exception { + // Arrange + Fields field1 = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints1 = new Constraints(Collections.singletonList(field1), null); + InputDescriptor desc1 = new InputDescriptor("desc-1", null, null, null, constraints1); + + Fields field2 = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints2 = new Constraints(Collections.singletonList(field2), null); + InputDescriptor desc2 = new InputDescriptor("desc-2", null, null, null, constraints2); + + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(desc1, desc2), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Collections.emptyList()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertEquals(2, result.getMatchingCredentialsResponse().getMissingClaims().size()); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("desc-1")); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("desc-2")); + } + + + @Test + public void testExtractSdJwtAlgorithmWhenSdJwtStringIsNullReturnsNull() throws Exception { + // Arrange + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod("extractSdJwtAlgorithm", String.class); + method.setAccessible(true); + + // Act + String result = (String) method.invoke(credentialMatchingService, (String) null); + + // Assert + assertNull(result); + } + + @Test + public void testMatchesConstraintsWhenFieldsAreNullReturnsTrue() throws Exception { + // Arrange + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod( + "matchesConstraints", VCCredentialResponse.class, Constraints.class); + method.setAccessible(true); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(new HashMap<>()) + .build(); + Constraints constraints = new Constraints(null, null); + + // Act + boolean result = (boolean) method.invoke(credentialMatchingService, vc, constraints); + + // Assert + assertTrue(result); + } + + @Test + public void testEvaluateJsonPathWhenPathIsNullReturnsEmptyList() throws Exception { + // Arrange + Fields field = new Fields(Collections.singletonList(null), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testEvaluateJsonPathWhenJsonIsNullReturnsEmptyList() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(credentialFormatHandler.extractAllCredentialProperties(any(VCCredentialResponse.class))) + .thenReturn(null); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + } + + @Test + public void testMatchesFieldPathWhenPathNotFoundInCredentialReturnsFalse() throws Exception { + // Arrange + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod( + "matchesFieldPath", VCCredentialResponse.class, String.class, Filter.class); + method.setAccessible(true); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(new HashMap<>()) + .build(); + + // Act + boolean result = (boolean) method.invoke(credentialMatchingService, vc, "$.nonexistent", null); + + // Assert + assertFalse(result); + } + + @Test + public void testExtractRequiredClaimsWhenDescriptorHasNullFieldsFilteredOut() throws Exception { + // Arrange + Constraints constraintsWithNullFields = new Constraints(null, null); + InputDescriptor descNullFields = new InputDescriptor("desc-null", null, null, null, constraintsWithNullFields); + + Fields field = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraintsWithFields = new Constraints(Collections.singletonList(field), null); + InputDescriptor descWithFields = new InputDescriptor("desc-valid", null, null, null, constraintsWithFields); + + PresentationDefinition pd = new PresentationDefinition( + "test-pd", Arrays.asList(descNullFields, descWithFields), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Collections.emptyList()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertEquals(2, result.getMatchingCredentialsResponse().getMissingClaims().size()); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("desc-null")); + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("desc-valid")); + } + + @Test + public void testExtractClaimKeyFromPathWhenTailStartsWithDollarStrippsDollarPrefix() throws Exception { + // Arrange + Fields field = new Fields(Arrays.asList("$.$id"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Collections.emptyList()); + + // Act + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + // Assert + assertTrue(result.getMatchingCredentialsResponse().getMissingClaims().contains("test")); + } + + @Test + public void testExtractSdClaimPathsWhenListHasMapsAndNonMapsProducesTopLevelKey() throws Exception { + Map sdClaims = new LinkedHashMap<>(); + List mixedList = new ArrayList<>(); + mixedList.add(createMapOf("code", "A", "value", "V1")); + mixedList.add(createMapOf("code", "B", "value", "V2")); + mixedList.add("not-a-map"); + sdClaims.put("data", mixedList); + + MatchingCredentialsDTO result = executeSdJwtNoConstraintFlow( + createSdJwtAllClaimsMap(new LinkedHashMap<>(), sdClaims)); + + CredentialDTO credential = result.getMatchingCredentialsResponse().getAvailableCredentials().get(0); + List paths = credential.getSdClaims(); + assertEquals(1, paths.size()); + assertTrue(paths.contains("$.data")); + } + + private static final String SD_JWT_HS256 = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.sig"; + + private static final String SD_JWT_NO_ALG = + "eyJ0eXAiOiJKV1QifQ.eyJpc3MiOiJ0ZXN0In0.sig"; + + private static final String SD_JWT_NON_STRING_ALG = + "eyJhbGciOjEyM30.eyJpc3MiOiJ0ZXN0In0.sig"; + + private PresentationDefinition createSdJwtPresentationDefinition(List algorithmValues) { + Map>> format = new HashMap<>(); + Map> sdJwtFormat = new HashMap<>(); + sdJwtFormat.put("sd-jwt_alg_values", algorithmValues); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), sdJwtFormat); + + Fields field = new Fields(Arrays.asList("$.type"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(field), null); + InputDescriptor descriptor = new InputDescriptor("sd-jwt-descriptor", null, null, format, constraints); + return new PresentationDefinition("sd-jwt-pd", Arrays.asList(descriptor), null, null, null); + } + + private DecryptedCredentialDTO createDecryptedSdJwtCredential(Object credentialPayload) { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("sd-jwt-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(credentialPayload) + .build(); + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return credential; + } + + private void setupSdJwtHandlerMock(Map publicClaims, Map sdClaims) { + setupSdJwtHandlerMock(publicClaims, sdClaims, null); + } + + private void setupSdJwtHandlerMock(Map publicClaims, Map sdClaims, + Map sdClaimValues) { + when(credentialFormatHandler.extractAllCredentialProperties(any(VCCredentialResponse.class))) + .thenAnswer(invocation -> { + VCCredentialResponse vc = invocation.getArgument(0); + String format = vc.getFormat(); + + if (CredentialFormat.VC_SD_JWT.getFormat().equalsIgnoreCase(format) + || CredentialFormat.DC_SD_JWT.getFormat().equalsIgnoreCase(format)) { + Map> result = new LinkedHashMap<>(); + result.put("publicClaims", new LinkedHashMap<>(publicClaims)); + result.put("sdClaims", new LinkedHashMap<>(sdClaims)); + if (sdClaimValues != null) { + result.put("sdClaimValues", new LinkedHashMap<>(sdClaimValues)); + } + return result; + } + Object cred = vc.getCredential(); + if (cred instanceof Map) { + return new LinkedHashMap<>((Map) cred); + } + return new LinkedHashMap<>(); + }); + } + + private MatchingCredentialsDTO executeSdJwtNoConstraintFlow( + Map> handlerReturn) throws Exception { + Constraints constraints = new Constraints(null, null); + InputDescriptor descriptor = new InputDescriptor("sd-jwt-test", null, null, null, constraints); + PresentationDefinition pd = new PresentationDefinition("sd-jwt-pd", + Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(Arrays.asList(createDecryptedSdJwtCredential(SD_JWT_HS256))); + when(credentialFormatHandler.extractAllCredentialProperties(any(VCCredentialResponse.class))) + .thenAnswer(invocation -> handlerReturn); + + return credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + } + + private Map> createSdJwtAllClaimsMap( + Map publicClaims, Map sdClaims) { + return createSdJwtAllClaimsMap(publicClaims, sdClaims, null); + } + + private Map> createSdJwtAllClaimsMap( + Map publicClaims, Map sdClaims, Map sdClaimValues) { + Map> allClaims = new LinkedHashMap<>(); + allClaims.put("publicClaims", + publicClaims != null ? new LinkedHashMap<>(publicClaims) : null); + allClaims.put("sdClaims", + sdClaims != null ? new LinkedHashMap<>(sdClaims) : null); + if (sdClaimValues != null) { + allClaims.put("sdClaimValues", new LinkedHashMap<>(sdClaimValues)); + } + return allClaims; + } + + private Map createMapOf(Object... keyValues) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + map.put((String) keyValues[i], keyValues[i + 1]); + } + return map; + } + + // --- extractFormatConstraintKeys --- + + @Test + public void testExtractFormatConstraintKeysWhenVcSdJwtFormatReturnsSdJwtAlgValues() throws Exception { + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod("extractFormatConstraintKeys", InputDescriptor.class); + method.setAccessible(true); + + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), new HashMap<>()); + InputDescriptor descriptor = new InputDescriptor("test-id", null, null, format, new Constraints(null, null)); + + @SuppressWarnings("unchecked") + Set result = (Set) method.invoke(credentialMatchingService, descriptor); + assertEquals(1, result.size()); + assertTrue(result.contains("sd-jwt_alg_values")); + } + + @Test + public void testExtractFormatConstraintKeysWhenLdpVcFormatReturnsProofType() throws Exception { + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod("extractFormatConstraintKeys", InputDescriptor.class); + method.setAccessible(true); + + Map>> format = new HashMap<>(); + format.put("ldp_vc", new HashMap<>()); + InputDescriptor descriptor = new InputDescriptor("test-id", null, null, format, new Constraints(null, null)); + + @SuppressWarnings("unchecked") + Set result = (Set) method.invoke(credentialMatchingService, descriptor); + assertEquals(1, result.size()); + assertTrue(result.contains("proof_type")); + } + + @Test + public void testExtractFormatConstraintKeysWhenNullFormatReturnsDescriptorId() throws Exception { + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod("extractFormatConstraintKeys", InputDescriptor.class); + method.setAccessible(true); + + InputDescriptor descriptor = new InputDescriptor("my-descriptor-id", null, null, null, new Constraints(null, null)); + + @SuppressWarnings("unchecked") + Set result = (Set) method.invoke(credentialMatchingService, descriptor); + assertEquals(1, result.size()); + assertTrue(result.contains("my-descriptor-id")); + } + + @Test + public void testExtractFormatConstraintKeysWhenBothFormatsPresentReturnsBothKeys() throws Exception { + Method method = CredentialMatchingServiceImpl.class.getDeclaredMethod("extractFormatConstraintKeys", InputDescriptor.class); + method.setAccessible(true); + + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), new HashMap<>()); + format.put("ldp_vc", new HashMap<>()); + InputDescriptor descriptor = new InputDescriptor("test-id", null, null, format, new Constraints(null, null)); + + @SuppressWarnings("unchecked") + Set result = (Set) method.invoke(credentialMatchingService, descriptor); + assertEquals(2, result.size()); + assertTrue(result.contains("sd-jwt_alg_values")); + assertTrue(result.contains("proof_type")); + } + + // --- getMatchingCredentials else-block: hasFormatMatch paths --- + + @Test + public void testGetMatchingCredentialsWhenFormatMatchesButConstraintFailsUsesMissingFieldNames() throws Exception { + // Descriptor: ldp_vc with empty proof_type list + $.name constraint + // Wallet: ldp_vc credential that passes matchesFormat but not matchesConstraints + // Expected: missingClaims = ["name"] (field names), not "proof_type" (format key) + Map> ldpVcFormatMap = new HashMap<>(); + ldpVcFormatMap.put("proof_type", Collections.emptyList()); + Map>> format = new HashMap<>(); + format.put("ldp_vc", ldpVcFormatMap); + + Fields nameField = new Fields(Arrays.asList("$.name"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(nameField), null); + InputDescriptor descriptor = new InputDescriptor("ldp-desc", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + // ldp_vc credential without a top-level "name" field → constraint fails + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + VCCredentialProperties props = new VCCredentialProperties(); + doReturn(props).when(objectMapper).convertValue(any(), eq(VCCredentialProperties.class)); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertTrue("field name must appear when format matched", + result.getMatchingCredentialsResponse().getMissingClaims().contains("name")); + assertFalse("format key must NOT appear when format matched", + result.getMatchingCredentialsResponse().getMissingClaims().contains("proof_type")); + } + + @Test + public void testGetMatchingCredentialsWhenNoFormatMatchReturnsSdJwtAlgValuesKey() throws Exception { + // Descriptor: vc+sd-jwt format; wallet has only ldp_vc credential + // Expected: missingClaims = ["sd-jwt_alg_values"] (format key), not constraint field names + Map> sdJwtFormatMap = new HashMap<>(); + sdJwtFormatMap.put("sd-jwt_alg_values", Arrays.asList("ES256")); + Map>> format = new HashMap<>(); + format.put(CredentialFormat.VC_SD_JWT.getFormat(), sdJwtFormatMap); + + Fields vctField = new Fields(Arrays.asList("$.vct"), null, null, null, null, null); + Constraints constraints = new Constraints(Collections.singletonList(vctField), null); + InputDescriptor descriptor = new InputDescriptor("sdjwt-desc", null, null, format, constraints); + PresentationDefinition pd = new PresentationDefinition("test-pd", Arrays.asList(descriptor), null, null, null); + + when(openID4VPService.resolvePresentationDefinition(any(), any(), anyBoolean())).thenReturn(pd); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); // ldp_vc credential only + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials(sessionData, walletId, base64Key); + + assertTrue(result.getMatchingCredentialsResponse().getAvailableCredentials().isEmpty()); + assertTrue("format key must appear when no format match", + result.getMatchingCredentialsResponse().getMissingClaims().contains("sd-jwt_alg_values")); + assertFalse("field name must NOT appear when no format match", + result.getMatchingCredentialsResponse().getMissingClaims().contains("vct")); + } + + // --- DCQL matching (matchWithDcqlQuery) --- + + @Test + public void testGetMatchingCredentialsDcqlSuccessWithLdpVcMatch() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery credentialQuery = mock(CredentialQuery.class); + when(credentialQuery.getId()).thenReturn("identity-query"); + when(credentialQuery.getFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + when(credentialQuery.getMultiple()).thenReturn(false); + when(credentialQuery.getClaims()).thenReturn(null); + stubCredentialQueryBasics(credentialQuery); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(issuersService.getIssuerConfig(anyString(), anyString())).thenReturn(createMockIssuerConfig()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + assertNotNull(result.getMatchingCredentialsResponse().getQueryGroups()); + assertEquals(1, result.getMatchingCredentialsResponse().getQueryGroups().size()); + DcqlQueryGroup group = result.getMatchingCredentialsResponse().getQueryGroups().get(0); + assertEquals("identity-query", group.getQueryId()); + assertFalse(group.getAvailableCredentials().isEmpty()); + assertTrue(group.getMissingClaims().isEmpty()); + assertNull(group.getAvailableCredentials().get(0).getClaims()); + assertEquals(1, result.getMatchingCredentials().size()); + assertEquals("identity-query", result.getMatchingCredentials().get(0).getIdentifier()); + verify(openID4VPService).resolveDcqlQuery(anyString(), anyString(), anyBoolean()); + verify(openID4VPService, never()).resolvePresentationDefinition(any(), any(), anyBoolean()); + } + + @Test + public void testGetMatchingCredentialsDcqlNullWalletId() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + stubDcqlQuery(mock(DCQLQuery.class)); + + try { + credentialMatchingService.getMatchingCredentials(dcqlSession, null, base64Key); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("Wallet ID cannot be null or empty", e.getMessage()); + } + } + + @Test + public void testGetMatchingCredentialsDcqlEmptyWalletId() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + stubDcqlQuery(mock(DCQLQuery.class)); + + try { + credentialMatchingService.getMatchingCredentials(dcqlSession, " ", base64Key); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("Wallet ID cannot be null or empty", e.getMessage()); + } + } + + @Test + public void testGetMatchingCredentialsDcqlWhenResolveReturnsNull() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())).thenReturn(null); + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + try { + credentialMatchingService.getMatchingCredentials(dcqlSession, walletId, base64Key); + fail("Expected InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains("Authorization request does not contain a DCQL query")); + } + } + + @Test + public void testGetMatchingCredentialsDcqlNoFormatMatchReturnsMissingClaims() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery credentialQuery = mock(CredentialQuery.class); + when(credentialQuery.getId()).thenReturn("sdjwt-query"); + when(credentialQuery.getFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + when(credentialQuery.getMultiple()).thenReturn(false); + when(credentialQuery.getClaims()).thenReturn(null); + stubCredentialQueryBasics(credentialQuery); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + DcqlQueryGroup group = result.getMatchingCredentialsResponse().getQueryGroups().get(0); + assertTrue(group.getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentials().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsDcqlIncludesCredentialSets() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery credentialQuery = mock(CredentialQuery.class); + when(credentialQuery.getId()).thenReturn("pan"); + when(credentialQuery.getFormat()).thenReturn(CredentialFormat.LDP_VC.getFormat()); + when(credentialQuery.getMultiple()).thenReturn(false); + when(credentialQuery.getClaims()).thenReturn(null); + stubCredentialQueryBasics(credentialQuery); + + CredentialSetQuery setQuery = mock(CredentialSetQuery.class); + when(setQuery.getRequired()).thenReturn(true); + when(setQuery.getOptions()).thenReturn(List.of(List.of("pan"), List.of("aadhaar"))); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(List.of(setQuery)); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(issuersService.getIssuerConfig(anyString(), anyString())).thenReturn(createMockIssuerConfig()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + assertEquals(1, result.getMatchingCredentialsResponse().getCredentialSets().size()); + CredentialSetInfo credentialSet = result.getMatchingCredentialsResponse().getCredentialSets().get(0); + assertTrue(credentialSet.isRequired()); + assertEquals(List.of(List.of("pan"), List.of("aadhaar")), credentialSet.getOptions()); + } + + @Test + public void testGetMatchingCredentialsDcqlSynthesizesCredentialSetsWhenAbsent() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery governmentIdentity = new CredentialQuery( + "government-identity", CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, null, null); + CredentialQuery ageProof = new CredentialQuery( + "age-proof", CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, null, null); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(governmentIdentity, ageProof)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + List credentialSets = result.getMatchingCredentialsResponse().getCredentialSets(); + assertEquals(2, credentialSets.size()); + assertTrue(credentialSets.get(0).isRequired()); + assertEquals(List.of(List.of("government-identity")), credentialSets.get(0).getOptions()); + assertTrue(credentialSets.get(1).isRequired()); + assertEquals(List.of(List.of("age-proof")), credentialSets.get(1).getOptions()); + } + + @Test + public void testGetMatchingCredentialsDcqlMatchesWithClaimPath() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + ClaimsQuery nameClaim = new ClaimsQuery( + "name-claim", List.of("credentialSubject", "name"), null); + CredentialQuery credentialQuery = new CredentialQuery( + "identity-query", CredentialFormat.LDP_VC.getFormat(), false, + Map.of(), false, List.of(nameClaim), null); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + when(issuersService.getIssuerConfig(anyString(), anyString())).thenReturn(createMockIssuerConfig()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + assertFalse(result.getMatchingCredentialsResponse().getQueryGroups().get(0).getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentials().size() == 1); + } + + @Test + public void testGetMatchingCredentialsDcqlNoMatchWithClaimSetsReturnsMissingClaimPaths() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + ClaimsQuery ageClaim = new ClaimsQuery( + "age-above-18", List.of("age_above_18"), + List.of(new ClaimValue.BoolValue(true))); + ClaimsQuery dobClaim = new ClaimsQuery( + "date-of-birth", List.of("dateOfBirth"), null); + CredentialQuery credentialQuery = new CredentialQuery( + "age-proof", CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, List.of(ageClaim, dobClaim), + List.of(List.of("age-above-18"), List.of("date-of-birth"))); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithMapData()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + DcqlQueryGroup group = result.getMatchingCredentialsResponse().getQueryGroups().get(0); + assertTrue(group.getAvailableCredentials().isEmpty()); + assertTrue(group.getMissingClaims().contains("$.age_above_18")); + assertTrue(group.getMissingClaims().contains("$.dateOfBirth")); + } + + @Test + public void testGetMatchingCredentialsDcqlMatchesSdJwtClaimValueWithDecodedDisclosures() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + ClaimsQuery ageClaim = new ClaimsQuery( + "age-above-18", List.of("age_above_18"), + List.of(new ClaimValue.BoolValue(true))); + CredentialQuery credentialQuery = new CredentialQuery( + "age-proof", CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, List.of(ageClaim), null); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithDcSdJwtToken(true)); + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + setupSdJwtHandlerMock( + Map.of("age_above_18", true), + Collections.emptyMap(), + Collections.emptyMap()); + when(issuersService.getIssuerConfig(anyString(), anyString())).thenReturn(createMockIssuerConfig()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + DcqlQueryGroup group = result.getMatchingCredentialsResponse().getQueryGroups().get(0); + assertFalse(group.getAvailableCredentials().isEmpty()); + assertTrue(group.getMissingClaims().isEmpty()); + } + + @Test + public void testGetMatchingCredentialsDcqlSdJwtFormatAliasMatch() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery credentialQuery = mock(CredentialQuery.class); + when(credentialQuery.getId()).thenReturn("sdjwt-query"); + when(credentialQuery.getFormat()).thenReturn(CredentialFormat.DC_SD_JWT.getFormat()); + when(credentialQuery.getMultiple()).thenReturn(false); + when(credentialQuery.getClaims()).thenReturn(null); + stubCredentialQueryBasics(credentialQuery); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSdJwtFormat()); + when(issuersService.getIssuerConfig(anyString(), anyString())).thenReturn(createMockIssuerConfig()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + assertFalse(result.getMatchingCredentialsResponse().getQueryGroups().get(0).getAvailableCredentials().isEmpty()); + assertEquals(1, result.getMatchingCredentials().size()); + } + + @Test + public void testGetMatchingCredentialsDcqlRejectsSdJwtWithoutHolderBindingWhenRequired() throws Exception { + VerifiablePresentationSessionData dcqlSession = createDcqlSessionData(); + CredentialQuery credentialQuery = new CredentialQuery( + "bound-card", + CredentialFormat.VC_SD_JWT.getFormat(), + false, + Map.of("vct_values", List.of("https://example.com/employee")), + true, + null, + null); + + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + stubDcqlQuery(dcqlQuery); + + when(walletCredentialService.getDecryptedCredentials(eq(walletId), any())) + .thenReturn(createMockWalletCredentialsWithSdJwtFormat()); + + MatchingCredentialsDTO result = credentialMatchingService.getMatchingCredentials( + dcqlSession, walletId, base64Key); + + DcqlQueryGroup group = result.getMatchingCredentialsResponse().getQueryGroups().get(0); + assertTrue(group.getAvailableCredentials().isEmpty()); + assertTrue(result.getMatchingCredentials().isEmpty()); + } + + private VerifiablePresentationSessionData createDcqlSessionData() { + VerifiablePresentationSessionData data = new VerifiablePresentationSessionData(); + data.setPresentationId("presentation-123"); + data.setAuthorizationRequest("client_id=test-client&response_type=vp_token"); + data.setCreatedAt(Instant.parse("2025-09-08T12:34:56Z")); + data.setVerifierClientPreregistered(true); + data.setDcql(true); + return data; + } + + private void stubDcqlQuery(DCQLQuery dcqlQuery) throws Exception { + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + } + + private void stubCredentialQueryBasics(CredentialQuery credentialQuery) { + when(credentialQuery.getMeta()).thenReturn(Map.of()); + when(credentialQuery.getRequireCryptographicHolderBinding()).thenReturn(false); + } + + private List createMockWalletCredentialsWithDcSdJwtToken(boolean includeAgeClaim) + throws Exception { + Map payload = new HashMap<>(); + payload.put("vct", "https://example.com/age"); + payload.put("cnf", Map.of("kid", "key-1")); + if (includeAgeClaim) { + payload.put("age_above_18", true); + } + + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId(walletId); + + VCCredentialResponse response = VCCredentialResponse.builder() + .format(CredentialFormat.DC_SD_JWT.getFormat()) + .credential(buildTestSdJwtToken(payload)) + .build(); + + credential.setCredential(response); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer-id"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return List.of(credential); + } + + private String buildTestSdJwtToken(Map payload) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + String header = encoder.encodeToString(mapper.writeValueAsBytes(Map.of("alg", "none"))); + String encodedPayload = encoder.encodeToString(mapper.writeValueAsBytes(payload)); + return header + "." + encodedPayload + ".signature"; + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/CredentialPDFGeneratorServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialPDFGeneratorServiceTest.java new file mode 100644 index 000000000..314816a47 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialPDFGeneratorServiceTest.java @@ -0,0 +1,2395 @@ + +package io.mosip.mimoto.service; + +import com.authlete.sd.Disclosure; +import com.authlete.sd.SDJWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.injivcrenderer.InjiVcRenderer; +import io.mosip.mimoto.constant.LdpVcV1Constants; +import io.mosip.mimoto.constant.LdpVcV2Constants; +import io.mosip.mimoto.constant.SdJwtVcConstants; +import io.mosip.mimoto.dto.BackgroundImageDTO; +import io.mosip.mimoto.dto.DisplayDTO; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.LogoDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.openid.presentation.PresentationDefinitionDTO; +import io.mosip.mimoto.model.QRCodeType; +import io.mosip.mimoto.service.impl.LdpVcCredentialFormatHandler; +import io.mosip.mimoto.service.impl.PresentationServiceImpl; +import io.mosip.mimoto.service.impl.VcSdJwtCredentialFormatHandler; +import io.mosip.mimoto.util.SvgFixerUtil; +import io.mosip.mimoto.util.Utilities; +import io.mosip.pixelpass.PixelPass; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.ByteArrayInputStream; +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Stream; + +import static io.mosip.mimoto.constant.LdpVcV2Constants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CredentialPDFGeneratorServiceTest { + + @Mock private ObjectMapper objectMapper; + @Mock private PresentationServiceImpl presentationService; + @Mock private Utilities utilities; + @Mock private PixelPass pixelPass; + @Mock private SvgFixerUtil svgFixerUtil; + @Mock private InjiVcRenderer injiVcRenderer; + @Mock + private CredentialFormatHandlerFactory credentialFormatHandlerFactory; + @Mock + private LdpVcCredentialFormatHandler credentialFormatHandler; + + @Mock + private VcSdJwtCredentialFormatHandler sdJwtCredentialFormatHandler; + + @InjectMocks + private CredentialPDFGeneratorService credentialPDFGeneratorService; + + private VCCredentialResponse vcCredentialResponse; + private IssuerDTO issuerDTO; + private CredentialsSupportedResponse credentialsSupportedResponse; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(credentialPDFGeneratorService, "ovpQRDataPattern", "test-pattern-%s-%s"); + ReflectionTestUtils.setField(credentialPDFGeneratorService, "qrCodeHeight", 500); + ReflectionTestUtils.setField(credentialPDFGeneratorService, "qrCodeWidth", 500); + ReflectionTestUtils.setField(credentialPDFGeneratorService, "allowedQRDataSizeLimit", 2000); + ReflectionTestUtils.setField(credentialPDFGeneratorService, "pixelPass", pixelPass); + ReflectionTestUtils.setField(credentialPDFGeneratorService, "faceImageLookupKeys", + "image,face,photo,picture,portrait"); + + setupTestData(); + } + + private void setupTestData() { + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("dateOfBirth", "1990-01-01"); + subjectData.put("face", "base64-encoded-image"); + + VCCredentialProperties vcProperties = VCCredentialProperties.builder() + .credentialSubject(subjectData) + .type(List.of("VerifiableCredential")) + .build(); + + vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(vcProperties) + .build(); + + issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id("test-issuer"); + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + DisplayDTO display = new DisplayDTO(); + display.setName("Issuer Display Name"); + display.setTitle("Issuer Title"); + display.setDescription("Issuer Description"); + display.setLanguage("en"); + + LogoDTO logo = new LogoDTO(); + logo.setUrl("https://example.com/logo.png"); + display.setLogo(logo); + + issuerDTO.setDisplay(List.of(display)); + + credentialsSupportedResponse = new CredentialsSupportedResponse(); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + credentialSubjectMap.put("dateOfBirth", createDisplay("Date of Birth")); + + CredentialDefinitionResponseDto definition = new CredentialDefinitionResponseDto(); + definition.setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setCredentialDefinition(definition); + credentialsSupportedResponse.setOrder(new ArrayList<>(List.of("name", "dateOfBirth"))); + + CredentialSupportedDisplayResponse credDisplay = new CredentialSupportedDisplayResponse(); + credDisplay.setBackgroundColor("#FFFFFF"); + credDisplay.setTextColor("#000000"); + credDisplay.setName("Test Credential"); + BackgroundImageDTO bgImage = new BackgroundImageDTO(); + bgImage.setUri("https://example.com/bg.png"); + credDisplay.setBackgroundImage(bgImage); + + credentialsSupportedResponse.setDisplay(List.of(credDisplay)); + } + + private CredentialDisplayResponseDto createDisplay(String name) { + CredentialDisplayResponseDto dto = new CredentialDisplayResponseDto(); + CredentialIssuerDisplayResponse display = new CredentialIssuerDisplayResponse(); + display.setName(name); + display.setLocale("en"); + dto.setDisplay(List.of(display)); + return dto; + } + + @Test + void testGeneratePdfForVerifiableCredential() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + PresentationDefinitionDTO presentationDef = new PresentationDefinitionDTO(); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(presentationDef); + when(objectMapper.writeValueAsString(presentationDef)) + .thenReturn("{\"presentation\":\"definition\"}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "2025-12-31", "en"); + + assertNotNull(result); + } + + @Test + void testGeneratePdfForEmbeddedVCQR() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + verify(pixelPass).generateQRData(anyString(), anyString()); + verify(presentationService, never()).constructPresentationDefinition(any()); + assertNotNull(result); + } + } + + @Test + void testGeneratePdfShouldGeneratePresentationDefinitionForOnlineSharingQrTypeWithNonEmptyDataShareUrl() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test", "", "en"); + + verify(presentationService).constructPresentationDefinition(any()); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + assertNotNull(result); + } + } + + @Test + void testHandleMapWithListValue() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + Map skills = new HashMap<>(); + skills.put("skills", List.of("Java", "Spring")); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(skills); + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() + .put("skills", createDisplay("Skills")); + credentialsSupportedResponse.setOrder(List.of("skills")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + PresentationDefinitionDTO presentationDef = new PresentationDefinitionDTO(); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(presentationDef); + when(objectMapper.writeValueAsString(presentationDef)) + .thenReturn("{\"presentation\":\"definition\"}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + } + + @Test + void testNullFaceImageHandling() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + Map mutableSubject = new HashMap<>(((VCCredentialProperties)vcCredentialResponse.getCredential()).getCredentialSubject()); + mutableSubject.remove("face"); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(mutableSubject); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + PresentationDefinitionDTO presentationDef = new PresentationDefinitionDTO(); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(presentationDef); + when(objectMapper.writeValueAsString(presentationDef)) + .thenReturn("{\"presentation\":\"definition\"}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + } + + @Test + void testGeneratePdfWithNullOrder() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + credentialsSupportedResponse.setOrder(null); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + PresentationDefinitionDTO presentationDef = new PresentationDefinitionDTO(); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(presentationDef); + when(objectMapper.writeValueAsString(presentationDef)) + .thenReturn("{\"presentation\":\"definition\"}"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + } + } + + @Test + void testGeneratePdfWithMapValueFormatting() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup credential with map containing "value" key + Map subjectWithMapValue = new HashMap<>(); + subjectWithMapValue.put("education", Map.of("value", "Bachelor's Degree")); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectWithMapValue); + + // Setup display for education field + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() + .put("education", createDisplay("Education")); + credentialsSupportedResponse.setOrder(List.of("education")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Education: $rowProperties.education"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Verify the PDF generation succeeded - formatValue was called internally + } + + @Test + void testGeneratePdfWithStringListFormatting() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup credential with list of strings + Map subjectWithList = new HashMap<>(); + subjectWithList.put("skills", List.of("Java", "Spring", "Boot")); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectWithList); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() + .put("skills", createDisplay("Skills")); + credentialsSupportedResponse.setOrder(List.of("skills")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Skills: $rowProperties.skills"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // The formatValue method handles list formatting internally + } + + @Test + void testGeneratePdfWithLocaleSpecificMapListFormatting() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup credential with locale-specific map list + Map subjectWithLocaleMap = new HashMap<>(); + List> localeData = List.of( + Map.of(LdpVcV2Constants.LANGUAGE, "en", LdpVcV2Constants.VALUE, "English Name"), + Map.of(LdpVcV2Constants.LANGUAGE, "fr", LdpVcV2Constants.VALUE, "French Name") + ); + subjectWithLocaleMap.put("localizedName", localeData); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectWithLocaleMap); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() + .put("localizedName", createDisplay("Localized Name")); + credentialsSupportedResponse.setOrder(List.of("localizedName")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Name: $rowProperties.localizedName"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // formatValue should select "English Name" based on locale "en" + } + + @Test + void testGeneratePdfWithNumericValueFormatting() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup credential with numeric value + Map subjectWithNumber = new HashMap<>(); + subjectWithNumber.put("age", 25); + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectWithNumber); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject() + .put("age", createDisplay("Age")); + credentialsSupportedResponse.setOrder(List.of("age")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Age: $rowProperties.age"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // formatValue converts number to string internally + } + + @Test + void testFaceKeyFallbackFromPrimaryToSecondary() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: No "face" key, but has "photo" key + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("photo", "base64-photo-image"); + subjectData.put("dateOfBirth", "1990-01-01"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + // Setup credential display without face key to avoid it appearing in rowProperties + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + credentialSubjectMap.put("dateOfBirth", createDisplay("Date of Birth")); + // Note: No "photo" in display properties - should be excluded from rowProperties + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name", "dateOfBirth")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Face: $face, Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Indirectly tests that "photo" was used as fallback for $face variable + } + + @Test + void testFaceKeyFallbackToPortrait() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: No "face" or "photo", but has "portrait" + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("portrait", "base64-portrait-image"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Portrait: $face, Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Tests fallback to "portrait" key + } + + @Test + void testFaceKeyFallbackToImage() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: Only "image" key available + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("image", "base64-generic-image"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Image: $face, Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Tests fallback to "image" key + } + + @Test + void testFaceKeyFallbackToPicture() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: Only "picture" key available + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("picture", "base64-picture-image"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Picture: $face, Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Tests fallback to "picture" key + } + + @Test + void testFaceKeyPriorityOrder() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: Multiple face keys present - should use first available in priority order + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("picture", "base64-picture-image"); // Lower priority + subjectData.put("photo", "base64-photo-image"); // Lower priority + subjectData.put("image", "base64-image"); // HIGHEST priority now + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + // Include all face keys in display to test that only "image" gets excluded + credentialSubjectMap.put("picture", createDisplay("Picture")); + credentialSubjectMap.put("photo", createDisplay("Photo")); + credentialSubjectMap.put("image", createDisplay("Image")); // Should be excluded (used for $face) + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name", "picture", "photo", "image")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Face: $face, Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Should use "image" (highest priority) over "photo" and "picture" + // Only "image" should be excluded from rowProperties, "photo" and "picture" should appear + } + + @Test + void testOnlySelectedFaceKeyExcludedFromRowProperties() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: Multiple face keys - only the selected one ("image") should be excluded from rowProperties + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("image", "base64-image"); // Highest priority - will be selected for $face + subjectData.put("face", "base64-face-image"); // Should appear in rowProperties + subjectData.put("photo", "base64-photo-image"); // Should appear in rowProperties + subjectData.put("portrait", "base64-portrait-image"); // Should appear in rowProperties + subjectData.put("email", "john@example.com"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + // Include all face keys in display properties + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + credentialSubjectMap.put("image", createDisplay("Image")); // Should be excluded (used for $face) + credentialSubjectMap.put("face", createDisplay("Face Photo")); // Should appear in rowProperties + credentialSubjectMap.put("photo", createDisplay("Photo")); // Should appear in rowProperties + credentialSubjectMap.put("portrait", createDisplay("Portrait")); // Should appear in rowProperties + credentialSubjectMap.put("email", createDisplay("Email Address")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name", "image", "face", "photo", "portrait", "email")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Face: $face
Properties: $rowProperties"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Only "image" should be excluded from rowProperties. "face", "photo" and "portrait" should appear in rowProperties + } + + @Test + void testMultipleFaceKeysExcludedFromRowProperties() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: Multiple face keys present + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("face", "base64-face-image"); + subjectData.put("photo", "base64-photo-image"); + subjectData.put("portrait", "base64-portrait-image"); + subjectData.put("email", "john@example.com"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + // Include all face keys in display properties + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + credentialSubjectMap.put("face", createDisplay("Face Photo")); + credentialSubjectMap.put("photo", createDisplay("Photo")); + credentialSubjectMap.put("portrait", createDisplay("Portrait")); + credentialSubjectMap.put("email", createDisplay("Email Address")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name", "face", "photo", "portrait", "email")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Face: $face
Properties: $rowProperties"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // All face-related keys should be excluded from rowProperties + // Only "name" and "email" should appear in rowProperties + } + + @Test + void testNoFaceKeysAvailable() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup: No face-related keys in credential + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("email", "john@example.com"); + subjectData.put("dateOfBirth", "1990-01-01"); + + ((VCCredentialProperties)vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put("name", createDisplay("Full Name")); + credentialSubjectMap.put("email", createDisplay("Email Address")); + credentialSubjectMap.put("dateOfBirth", createDisplay("Date of Birth")); + + credentialsSupportedResponse.getCredentialDefinition().setCredentialSubject(credentialSubjectMap); + credentialsSupportedResponse.setOrder(List.of("name", "email", "dateOfBirth")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Face: $face
Properties: $rowProperties"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + // Should handle gracefully when no face keys are available ($face will be null) + } + + @Test + void testMaskingForSelectivelyDisclosableClaimsAndNonMaskedFieldsInSDJWT() throws Exception { + ReflectionTestUtils.setField(credentialPDFGeneratorService, "maskDisclosures", true); + + when(credentialFormatHandlerFactory.getHandler("vc+sd-jwt")).thenReturn(sdJwtCredentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + String validSdJwt = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiYWJjMTIzIl19.signature~WyJzYWx0IiwgIm5hbWUiLCAiSm9obiBEb2UiXQ~"; + + VCCredentialResponse sdJwtVcResponse = VCCredentialResponse.builder() + .format("vc+sd-jwt") + .credential(validSdJwt) + .build(); + + Map extractedClaims = new HashMap<>(); + extractedClaims.put("name", "John Doe"); + when(sdJwtCredentialFormatHandler.extractCredentialClaims(sdJwtVcResponse)).thenReturn(extractedClaims); + + CredentialIssuerDisplayResponse displayResponse = createDisplayResponse("Name", "en"); + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(displayResponse, "John Doe")); + when(sdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn(""); + + try (MockedStatic mockedSDJWT = mockStatic(SDJWT.class); + MockedStatic mockedUtilities = mockStatic(Utilities.class)) { + + Disclosure nameDisclosure = mock(Disclosure.class); + when(nameDisclosure.getClaimName()).thenReturn("name"); + + SDJWT mockSdJwt = mock(SDJWT.class); + when(mockSdJwt.getDisclosures()).thenReturn(List.of(nameDisclosure)); + mockedSDJWT.when(() -> SDJWT.parse(validSdJwt)).thenReturn(mockSdJwt); + + mockedUtilities.when(() -> Utilities.maskValue("John Doe")).thenReturn("****"); + mockedUtilities.when(() -> Utilities.encodeToString(any(), anyString())).thenReturn("encoded-image"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "credentialConfigId", sdJwtVcResponse, issuerDTO, + credentialsSupportedResponse, "http://datashare.url", "2025-12-31", "en"); + + assertNotNull(result); + mockedUtilities.verify(() -> Utilities.maskValue("John Doe")); + } + } + + @Test + void testNoMaskingForLdpFormat() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + // Setup LDP-VC format (not SD-JWT) + vcCredentialResponse.setFormat("ldp_vc"); + + // This test uses the LDP-VC handler (already set up in @BeforeEach) + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + claims.put("age", "30"); + + // Use the specific LDP-VC handler + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(claims); + + // Use LinkedHashMap instead of HashMap + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + displayProps.put("age", Map.of(createDisplayResponse("Age", "en"), "30")); + + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test Template"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + } + + private CredentialIssuerDisplayResponse createDisplayResponse(String name, String locale) { + CredentialIssuerDisplayResponse response = new CredentialIssuerDisplayResponse(); + response.setName(name); + response.setLocale(locale); + return response; + } + + @Test + void testFormatValueWithMap() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + // Setup credential with map containing "value" + Map subject = Map.of("education", Map.of("value", "Bachelor's Degree")); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subject); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("education", Map.of(createDisplayResponse("Education", "en"), Map.of("value", "Bachelor's Degree"))); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject().put("education", createDisplay("Education")); + credentialsSupportedResponse.setOrder(List.of("education")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Education: $rowProperties.education"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, "", "", "en"); + + assertNotNull(result); // Verifies formatValue extracted "Bachelor's Degree" + } + + @Test + void testFormatValueWithStringList() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + // Setup credential with list of strings + Map subject = Map.of("skills", List.of("Java", "Spring", "Boot")); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subject); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("skills", Map.of(createDisplayResponse("Skills", "en"), List.of("Java", "Spring", "Boot"))); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject().put("skills", createDisplay("Skills")); + credentialsSupportedResponse.setOrder(List.of("skills")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Skills: $rowProperties.skills"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, "", "", "en"); + + assertNotNull(result); // Verifies formatValue joined to "Java, Spring, Boot" + } + + @Test + void testFormatValueWithLocaleMapList() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + // Setup credential with locale-specific list + List> localeData = List.of( + Map.of(LdpVcV2Constants.LANGUAGE, "en", LdpVcV2Constants.VALUE, "English Name"), + Map.of(LdpVcV2Constants.LANGUAGE, "fr", LdpVcV2Constants.VALUE, "French Name") + ); + Map subject = Map.of("name", localeData); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subject); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), localeData)); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject().put("name", createDisplay("Name")); + credentialsSupportedResponse.setOrder(List.of("name")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Name: $rowProperties.name"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, "", "", "en"); + + assertNotNull(result); // Verifies formatValue selected "English Name" for locale "en" + } + + @Test + void testFormatValueWithEmptyList() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + // Setup credential with empty list + Map subject = Map.of("tags", List.of()); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subject); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("tags", Map.of(createDisplayResponse("Tags", "en"), List.of())); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject().put("tags", createDisplay("Tags")); + credentialsSupportedResponse.setOrder(List.of("tags")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Tags: $rowProperties.tags"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, "", "", "en"); + + assertNotNull(result); // Verifies formatValue returned "" for empty list + } + + @Test + void testFormatValueWithPrimitive() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + // Setup credential with number + Map subject = Map.of("age", 25); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subject); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("age", Map.of(createDisplayResponse("Age", "en"), 25)); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject().put("age", createDisplay("Age")); + credentialsSupportedResponse.setOrder(List.of("age")); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Age: $rowProperties.age"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, "", "", "en"); + + assertNotNull(result); // Verifies formatValue converted 25 to "25" + } + + @Test + void testFormatValueWithMap_AtValue() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService(objectMapper, presentationService, utilities, pixelPass, credentialFormatHandlerFactory, injiVcRenderer, svgFixerUtil ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map value = new HashMap<>(); + value.put(LdpVcV2Constants.VALUE, "Green Valley Farm"); + + String result = (String) formatValueMethod.invoke(service, value, "en"); + + assertEquals("Green Valley Farm", result); + } + + @Test + void testFormatValueWithMap_Value() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService(objectMapper, presentationService, utilities, pixelPass, credentialFormatHandlerFactory, injiVcRenderer, svgFixerUtil ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map value = new HashMap<>(); + value.put(LdpVcV1Constants.VALUE, "Green Valley Farm"); + + String result = (String) formatValueMethod.invoke(service, value, "en"); + + assertEquals("Green Valley Farm", result); + } + + @Test + void testFormatValueWithListOfMaps_AtLanguageAndAtValue() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService(objectMapper, presentationService, utilities, pixelPass, credentialFormatHandlerFactory, injiVcRenderer, svgFixerUtil ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map mapEn = new HashMap<>(); + mapEn.put(LdpVcV2Constants.VALUE, "Green Valley Farm"); + mapEn.put(LdpVcV2Constants.LANGUAGE, "en"); + + Map mapFr = new HashMap<>(); + mapFr.put(LdpVcV2Constants.VALUE, "Ferme Vallée Verte"); + mapFr.put(LdpVcV2Constants.LANGUAGE, "fr"); + + Map mapFil = new HashMap<>(); + mapFil.put(LdpVcV2Constants.VALUE, "Bukid sa Luntiang Lambak"); + mapFil.put(LdpVcV2Constants.LANGUAGE, "fil"); + + List> value = Arrays.asList(mapEn, mapFr, mapFil); + + String resultEn = (String) formatValueMethod.invoke(service, value, "en"); + String resultFr = (String) formatValueMethod.invoke(service, value, "fr"); + String resultFil = (String) formatValueMethod.invoke(service, value, "fil"); + String resultNoMatchingLang = (String) formatValueMethod.invoke(service, value, "hi"); // matching language not present, should return empty string + String resultNullLang = (String) formatValueMethod.invoke(service, value, null); // null language, should return empty string + + assertEquals("Green Valley Farm", resultEn); + assertEquals("Ferme Vallée Verte", resultFr); + assertEquals("Bukid sa Luntiang Lambak", resultFil); + assertEquals("", resultNoMatchingLang); + assertEquals("", resultNullLang); + } + + @Test + void testFormatValueWithListOfMaps_LanguageAndValue() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService(objectMapper, presentationService, utilities, pixelPass, credentialFormatHandlerFactory, injiVcRenderer, svgFixerUtil ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map mapEn = new HashMap<>(); + mapEn.put(SdJwtVcConstants.VALUE, "Green Valley Farm"); + mapEn.put(SdJwtVcConstants.LANGUAGE, "en"); + + Map mapFr = new HashMap<>(); + mapFr.put(SdJwtVcConstants.VALUE, "Ferme Vallée Verte"); + mapFr.put(SdJwtVcConstants.LANGUAGE, "fr"); + + Map mapFil = new HashMap<>(); + mapFil.put(SdJwtVcConstants.VALUE, "Bukid sa Luntiang Lambak"); + mapFil.put(SdJwtVcConstants.LANGUAGE, "fil"); + + List> value = Arrays.asList(mapEn, mapFr, mapFil); + + String resultEn = (String) formatValueMethod.invoke(service, value, "en"); + String resultFr = (String) formatValueMethod.invoke(service, value, "fr"); + String resultFil = (String) formatValueMethod.invoke(service, value, "fil"); + String resultNoMatchingLang = (String) formatValueMethod.invoke(service, value, "hi"); // matching language not present, should return empty string + String resultNullLang = (String) formatValueMethod.invoke(service, value, null); // null language, should return empty string + + assertEquals("Green Valley Farm", resultEn); + assertEquals("Ferme Vallée Verte", resultFr); + assertEquals("Bukid sa Luntiang Lambak", resultFil); + assertEquals("", resultNoMatchingLang); + assertEquals("", resultNullLang); + } + + @Test + void testFormatValueWithListOfMaps_langAndValue() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService(objectMapper, presentationService, utilities, pixelPass, credentialFormatHandlerFactory, injiVcRenderer, svgFixerUtil ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map mapEn = new HashMap<>(); + mapEn.put(LdpVcV1Constants.VALUE, "Green Valley Farm"); + mapEn.put(LdpVcV1Constants.LANGUAGE, "en"); + + Map mapFr = new HashMap<>(); + mapFr.put(LdpVcV1Constants.VALUE, "Ferme Vallée Verte"); + mapFr.put(LdpVcV1Constants.LANGUAGE, "fr"); + + Map mapFil = new HashMap<>(); + mapFil.put(LdpVcV1Constants.VALUE, "Bukid sa Luntiang Lambak"); + mapFil.put(LdpVcV1Constants.LANGUAGE, "fil"); + + List> value = Arrays.asList(mapEn, mapFr, mapFil); + + String resultEn = (String) formatValueMethod.invoke(service, value, "en"); + String resultFr = (String) formatValueMethod.invoke(service, value, "fr"); + String resultFil = (String) formatValueMethod.invoke(service, value, "fil"); + String resultNoMatchingLang = (String) formatValueMethod.invoke(service, value, "hi"); // matching language not present, should return empty string + String resultNullLang = (String) formatValueMethod.invoke(service, value, null); // null language, should return empty string + + assertEquals("Green Valley Farm", resultEn); + assertEquals("Ferme Vallée Verte", resultFr); + assertEquals("Bukid sa Luntiang Lambak", resultFil); + assertEquals("", resultNoMatchingLang); + assertEquals("", resultNullLang); + } + + @Test + void testInjiVcRendererInvokedForLdpVcWithContextListAndTemplate() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL, "https://w3id.org/security/v1")); + credentialMap.put(RENDER_METHOD, List.of(Map.of(TEMPLATE, "", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE))); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(injiVcRenderer.generateCredentialDisplayContent(any(), any(), anyString(), any())) + .thenReturn(List.of("")); + when(svgFixerUtil.addMissingOffsetToStopElements(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(injiVcRenderer.convertSvgToPdf(anyList())).thenReturn("AQID"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testInjiVcRendererInvokedForLdpVcWhenRenderMethodListWithTemplates() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of( + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE, + TEMPLATE, Map.of("id", "https://certify/rendering-template/5b9c") + ), + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE, + TEMPLATE, Map.of("id", "https://certify/rendering-template/6b9c") + ), + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE, + TEMPLATE, Map.of("id", "https://certify/rendering-template/7b9c") + ) + )); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(injiVcRenderer.generateCredentialDisplayContent(any(), any(), anyString(), any())) + .thenReturn(List.of("")); + when(svgFixerUtil.addMissingOffsetToStopElements(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(injiVcRenderer.convertSvgToPdf(anyList())).thenReturn("AQID"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testV2ContextNullDisablesSvgRendering() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, null); // contextField is null + credentialMap.put(RENDER_METHOD, List.of(Map.of(TEMPLATE, "", RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE))); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(any())).thenReturn(Map.of("name", "John")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(new LinkedHashMap<>()); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vc, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedWhenRenderSuiteIsNotSvgMustache() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, Map.of(TEMPLATE, "", RENDER_SUITE, "pdf-mustache")); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(any())).thenReturn(Map.of("name", "John")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(new LinkedHashMap<>()); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vc, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedForLdpVcWhenV2ContextAbsent() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of("https://www.w3.org/ns/credentials/v1")); + credentialMap.put(RENDER_METHOD, List.of(Map.of(TEMPLATE, ""))); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(Map.of("name", "John")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(new LinkedHashMap<>()); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedForSdJwt() throws Exception { + when(credentialFormatHandlerFactory.getHandler("vc+sd-jwt")).thenReturn(sdJwtCredentialFormatHandler); + String validSdJwt = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOltdLCJuYW1lIjoiSm9obiBEb2UifQ.signature~WyJzYWx0IiwgIm5hbWUiLCAiSm9obiBEb2UiXQ"; + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("vc+sd-jwt") + .credential(validSdJwt) + .build(); + + Map extractedClaims = new HashMap<>(); + extractedClaims.put("name", "John Doe"); + when(sdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(extractedClaims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(sdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())) + .thenReturn(new PresentationDefinitionDTO()); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedForLdpVcWithRenderMethodButNoTemplate() throws Exception { + // Credential with v2 context and renderMethod, but missing "template" + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of(Map.of(RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE))); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedForLdpVcWithoutRenderMethod() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testInjiVcRendererNotInvokedForLdpVcWhenOneRenderMethodListEntryMissingTemplate() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of( + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE, + TEMPLATE, Map.of("id", "https://certify/rendering-template/5b9c") + ), + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE + // missing TEMPLATE + ), + Map.of( + "type", "TemplateRenderMethod", + RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE, + TEMPLATE, Map.of("id", "https://certify/rendering-template/5b8c") + ) + )); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(Map.of("name", "John Doe")); + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), anyString(), anyString(), anyString()); + } + + @Test + void testGeneratePdfForV2CredentialThrowsExceptionWhenRendererReturnsEmptyList() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of(Map.of(TEMPLATE, "", RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE))); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(injiVcRenderer.generateCredentialDisplayContent(any(), any(), anyString(), any())) + .thenReturn(Collections.emptyList()); + + assertThrows(Exception.class, () -> credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vc, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en")); + + verify(injiVcRenderer).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testGeneratePdfForV2CredentialCatchBlockWhenRendererThrows() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of(Map.of(TEMPLATE, "", RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE))); + + VCCredentialResponse vc = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(injiVcRenderer.generateCredentialDisplayContent(any(), any(), anyString(), any())) + .thenThrow(new RuntimeException("Renderer failed")); + + assertThrows(Exception.class, () -> credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vc, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "", "en")); + } + + @Test + void testExtractClaim169QrWithValidIdentityQRCode() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 containing identityQRCode + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", "valid-qr-code-data-from-claim169"); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-claim169"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + mocked.verify(() -> Utilities.encodeToString(any(), anyString())); + } + } + + @ParameterizedTest + @MethodSource("provideClaim169QrFallbackScenarios") + void testExtractClaim169QrFallbackToVCData(String scenarioName, Map claim169Map) throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-vc"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + // Should fallback to VC data QR generation since identityQRCode is null/empty/missing + verify(pixelPass).generateQRData(anyString(), anyString()); + } + } + + private static Stream provideClaim169QrFallbackScenarios() { + // Scenario 1: claim169 Map with null identityQRCode + Map nullIdentityQRCodeMap = new HashMap<>(); + nullIdentityQRCodeMap.put("identityQRCode", null); + + // Scenario 2: claim169 Map with empty string identityQRCode + Map emptyIdentityQRCodeMap = new HashMap<>(); + emptyIdentityQRCodeMap.put("identityQRCode", ""); + + return Stream.of( + Arguments.of("null identityQRCode", nullIdentityQRCodeMap), + Arguments.of("empty string identityQRCode", emptyIdentityQRCodeMap) + ); + } + + @Test + void testExtractClaim169QrWithClaim169NotAsMap() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 as String (not Map) + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", "not-a-map-value"); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-vc"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + // Should fallback to VC data QR generation since claim169 is not a Map + verify(pixelPass).generateQRData(anyString(), anyString()); + } + } + + @Test + void testExtractClaim169QrWithClaim169Missing() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential without claim169 + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("email", "john@example.com"); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + displayProps.put("email", Map.of(createDisplayResponse("Email", "en"), "john@example.com")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-vc"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass).generateQRData(anyString(), anyString()); + } + } + + @Test + void testExtractClaim169QrWithWhitespaceOnlyIdentityQRCode() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 containing whitespace-only identityQRCode + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", " \t\n "); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-claim169"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + verify(objectMapper, never()).writeValueAsString(vcCredentialResponse.getCredential()); + mocked.verify(() -> Utilities.encodeToString(any(), anyString())); + } + } + + @Test + void testExtractClaim169QrWithSdJwtFormat() throws Exception { + when(credentialFormatHandlerFactory.getHandler("vc+sd-jwt")).thenReturn(sdJwtCredentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + vcCredentialResponse.setFormat("vc+sd-jwt"); + String mockSDJWTString = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOltdfQ.signature"; + vcCredentialResponse.setCredential(mockSDJWTString); + + // Setup credential with claim169 containing identityQRCode for SD-JWT format + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", "sd-jwt-qr-code-data"); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(sdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(sdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + try (MockedStatic mockedSDJWT = mockStatic(SDJWT.class); + MockedStatic mocked = mockStatic(Utilities.class)) { + + // Mock SDJWT parsing for disclosure extraction + SDJWT mockSDJWT = mock(SDJWT.class); + mockedSDJWT.when(() -> SDJWT.parse(mockSDJWTString)).thenReturn(mockSDJWT); + when(mockSDJWT.getDisclosures()).thenReturn(Collections.emptyList()); + + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-claim169"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + mocked.verify(() -> Utilities.encodeToString(any(), anyString())); + mockedSDJWT.verify(() -> SDJWT.parse(mockSDJWTString)); + } + } + + @Test + void testExtractClaim169QrWithNumericIdentityQRCode() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 containing numeric identityQRCode (will be converted to string) + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", 12345); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-claim169"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + mocked.verify(() -> Utilities.encodeToString(any(), anyString())); + } + } + + @Test + void testExtractClaim169QrWithEmptyClaim169Map() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 as empty Map + Map claim169Map = new HashMap<>(); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-vc"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass).generateQRData(anyString(), anyString()); + } + } + + @Test + void testExtractClaim169QrWithClaim169AsList() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.EmbeddedVC); + + // Setup credential with claim169 as List (not Map) + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", List.of("item1", "item2")); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(pixelPass.generateQRData(anyString(), anyString())).thenReturn("generated-qr-data"); + + try (MockedStatic mocked = mockStatic(Utilities.class)) { + mocked.when(() -> Utilities.encodeToString(any(), anyString())) + .thenReturn("base64-encoded-qr-from-vc"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass).generateQRData(anyString(), anyString()); + } + } + + @Test + void testExtractClaim169QrNotCalledWhenQRCodeTypeIsNone() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + // Setup credential with claim169 containing identityQRCode + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", "valid-qr-code-data-from-claim169"); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + verify(presentationService, never()).constructPresentationDefinition(any()); + } + + @Test + void testExtractClaim169QrNotCalledWhenQRCodeTypeIsNull() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(null); + + // Setup credential with claim169 containing identityQRCode + Map claim169Map = new HashMap<>(); + claim169Map.put("identityQRCode", "valid-qr-code-data-from-claim169"); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("claim169", claim169Map); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)) + .thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("Test"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "", "en"); + + assertNotNull(result); + verify(pixelPass, never()).generateQRData(anyString(), anyString()); + verify(presentationService, never()).constructPresentationDefinition(any()); + } + + @Test + void testTitleNameWithMatchingLocaleFromGetPdfResource() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(Map.of("name", "John")); + issuerDTO.setQr_code_type(QRCodeType.None); + + CredentialSupportedDisplayResponse fr = new CredentialSupportedDisplayResponse(); + fr.setName("French Title"); + fr.setLocale("fr"); + + CredentialSupportedDisplayResponse en = new CredentialSupportedDisplayResponse(); + en.setName("English Title"); + en.setLocale("en"); + + credentialsSupportedResponse.setDisplay(List.of(fr, en)); + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + Map data = ReflectionTestUtils.invokeMethod( + credentialPDFGeneratorService, + "getPdfResourceFromVcProperties", + displayProperties, + credentialsSupportedResponse, + vcCredentialResponse, + issuerDTO, + "", + "-1", + "en" + ); + + assertNotNull(data); + assertEquals("English Title", data.get("titleName")); + } + + @Test + void testTitleNameFallsBackToFirstWhenLocaleNotFoundFromGetPdfResource() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(Map.of("name", "John")); + issuerDTO.setQr_code_type(QRCodeType.None); + + CredentialSupportedDisplayResponse fr = new CredentialSupportedDisplayResponse(); + fr.setName("French Title"); + fr.setLocale("fr"); + + CredentialSupportedDisplayResponse en = new CredentialSupportedDisplayResponse(); + en.setName("English Title"); + en.setLocale("en"); + + credentialsSupportedResponse.setDisplay(List.of(fr, en)); + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + Map data = ReflectionTestUtils.invokeMethod( + credentialPDFGeneratorService, + "getPdfResourceFromVcProperties", + displayProperties, + credentialsSupportedResponse, + vcCredentialResponse, + issuerDTO, + "", + "-1", + "es" + ); + + assertNotNull(data); + assertEquals("French Title", data.get("titleName")); // fallback to first + } + + @Test + void testTitleNameNullWhenDisplayListNullOrEmptyFromGetPdfResource() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(Map.of("name", "John")); + issuerDTO.setQr_code_type(QRCodeType.None); + + LinkedHashMap> displayProperties = new LinkedHashMap<>(); + + // null display list + credentialsSupportedResponse.setDisplay(null); + Map dataNull = ReflectionTestUtils.invokeMethod( + credentialPDFGeneratorService, + "getPdfResourceFromVcProperties", + displayProperties, + credentialsSupportedResponse, + vcCredentialResponse, + issuerDTO, + "", + "-1", + "en" + ); + assertNotNull(dataNull); + assertNull(dataNull.get("titleName")); + + // empty display list + credentialsSupportedResponse.setDisplay(new ArrayList<>()); + Map dataEmpty = ReflectionTestUtils.invokeMethod( + credentialPDFGeneratorService, + "getPdfResourceFromVcProperties", + displayProperties, + credentialsSupportedResponse, + vcCredentialResponse, + issuerDTO, + "", + "-1", + "en" + ); + assertNotNull(dataEmpty); + assertNull(dataEmpty.get("titleName")); + } + + @SuppressWarnings("unchecked") + private Map invokePdfResourceFromVcProperties( + LinkedHashMap> displayProperties, + CredentialsSupportedResponse credentialsSupportedResponse, + VCCredentialResponse vcCredentialResponse, + IssuerDTO issuerDTO, + String dataShareUrl, + String credentialValidity, + String locale) throws Exception { + Method method = CredentialPDFGeneratorService.class.getDeclaredMethod( + "getPdfResourceFromVcProperties", + LinkedHashMap.class, + CredentialsSupportedResponse.class, + VCCredentialResponse.class, + IssuerDTO.class, + String.class, + String.class, + String.class); + method.setAccessible(true); + return (Map) method.invoke( + credentialPDFGeneratorService, + displayProperties, credentialsSupportedResponse, + vcCredentialResponse, issuerDTO, + dataShareUrl, credentialValidity, locale); + } + + @Test + void testIsFaceKeyFalseWhenSelectedFaceKeyNotNullButKeyDoesNotMatch() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("dateOfBirth", "1990-01-01"); + subjectData.put("face", "base64-encoded-image"); + ((VCCredentialProperties) vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + displayProps.put("dateOfBirth", Map.of(createDisplayResponse("Date of Birth", "en"), "1990-01-01")); + + Map data = invokePdfResourceFromVcProperties( + displayProps, credentialsSupportedResponse, vcCredentialResponse, + issuerDTO, "https://example.com/share", "2025-12-31", "en"); + + @SuppressWarnings("unchecked") + Map rowProperties = (Map) data.get("rowProperties"); + + assertTrue(rowProperties.containsKey("name"), + "Expected 'name' in rowProperties because isFaceKey=false for 'name'"); + assertTrue(rowProperties.containsKey("dateOfBirth"), + "Expected 'dateOfBirth' in rowProperties because isFaceKey=false for 'dateOfBirth'"); + assertFalse(rowProperties.containsKey("face"), + "Expected 'face' absent from rowProperties (not in displayProperties)"); + } + + @Test + void testIsFaceKeyTrueWhenSelectedFaceKeyMatchesCurrentKey() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("face", "base64-encoded-image"); + ((VCCredentialProperties) vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + displayProps.put("face", Map.of(createDisplayResponse("Face Image", "en"), "base64-encoded-image")); + + Map data = invokePdfResourceFromVcProperties( + displayProps, credentialsSupportedResponse, vcCredentialResponse, + issuerDTO, "https://example.com/share", "2025-12-31", "en"); + + @SuppressWarnings("unchecked") + Map rowProperties = (Map) data.get("rowProperties"); + + + assertFalse(rowProperties.containsKey("face"), + "Expected 'face' excluded from rowProperties because isFaceKey=true"); + + assertTrue(rowProperties.containsKey("name"), + "Expected 'name' in rowProperties because isFaceKey=false for 'name'"); + + assertEquals("base64-encoded-image", data.get("face"), + "Expected the face value to be stored in data['face']"); + } + + @Test + void testIsFaceKeyFalseWhenSelectedFaceKeyIsNull() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("dateOfBirth", "1990-01-01"); + ((VCCredentialProperties) vcCredentialResponse.getCredential()).setCredentialSubject(subjectData); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + displayProps.put("dateOfBirth", Map.of(createDisplayResponse("Date of Birth", "en"), "1990-01-01")); + + Map data = invokePdfResourceFromVcProperties( + displayProps, credentialsSupportedResponse, vcCredentialResponse, + issuerDTO, "https://example.com/share", "2025-12-31", "en"); + + @SuppressWarnings("unchecked") + Map rowProperties = (Map) data.get("rowProperties"); + + // selectedFaceKey = null → isFaceKey = false for ALL keys → all must appear in rowProperties + assertTrue(rowProperties.containsKey("name"), + "Expected 'name' in rowProperties because selectedFaceKey=null → isFaceKey=false"); + assertTrue(rowProperties.containsKey("dateOfBirth"), + "Expected 'dateOfBirth' in rowProperties because selectedFaceKey=null → isFaceKey=false"); + // face value in data must be null (no face key found) + assertNull(data.get("face"), + "Expected data['face'] to be null when no face key exists in credential"); + } + + @Test + void testMaskingDisabledForSelectivelyDisclosableClaims() throws Exception { + ReflectionTestUtils.setField(credentialPDFGeneratorService, "maskDisclosures", false); + try { + when(credentialFormatHandlerFactory.getHandler("vc+sd-jwt")).thenReturn(sdJwtCredentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + + String validSdJwt = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiYWJjMTIzIl19.signature~WyJzYWx0IiwgIm5hbWUiLCAiSm9obiBEb2UiXQ~"; + + VCCredentialResponse sdJwtVcResponse = VCCredentialResponse.builder() + .format("vc+sd-jwt") + .credential(validSdJwt) + .build(); + + Map extractedClaims = new HashMap<>(); + extractedClaims.put("name", "John Doe"); + when(sdJwtCredentialFormatHandler.extractCredentialClaims(sdJwtVcResponse)).thenReturn(extractedClaims); + + CredentialIssuerDisplayResponse displayResponse = createDisplayResponse("Name", "en"); + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(displayResponse, "John Doe")); + when(sdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("$rowProperties"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + + try (MockedStatic mockedSDJWT = mockStatic(SDJWT.class); + MockedStatic mockedUtilities = mockStatic(Utilities.class)) { + SDJWT mockSdjwt = mock(SDJWT.class); + Disclosure mockDisclosure = mock(Disclosure.class); + when(mockDisclosure.getClaimName()).thenReturn("name"); + when(mockSdjwt.getDisclosures()).thenReturn(List.of(mockDisclosure)); + mockedSDJWT.when(() -> SDJWT.parse(anyString())).thenReturn(mockSdjwt); + mockedUtilities.when(() -> Utilities.encodeToString(any(), anyString())).thenReturn("encoded-image"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", sdJwtVcResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "2025-12-31", "en"); + + assertNotNull(result); + mockedUtilities.verify(() -> Utilities.maskValue(anyString()), never()); + } + } finally { + ReflectionTestUtils.setField(credentialPDFGeneratorService, "maskDisclosures", true); + } + } + + @Test + void testDisplayNameNullExcludesFromRowProperties() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map extractedClaims = new HashMap<>(); + extractedClaims.put("name", "John Doe"); + extractedClaims.put("hiddenField", "some-value"); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(extractedClaims); + + CredentialIssuerDisplayResponse nullNameDisplay = createDisplayResponse(null, "en"); + CredentialIssuerDisplayResponse nameDisplay = createDisplayResponse("Full Name", "en"); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(nameDisplay, "John Doe")); + displayProps.put("hiddenField", Map.of(nullNameDisplay, "some-value")); // displayName is null + + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("$rowProperties"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "https://example.com/share", "2025-12-31", "en"); + + assertNotNull(result); + } + + @Test + void testExtractFaceReturnsNullFaceWhenFaceValueIsNull() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("face", null); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("$rowProperties"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "2025-12-31", "en"); + + assertNotNull(result); + } + + @Test + void testExtractFaceReturnsNullFaceWhenFaceValueIsEmptyString() throws Exception { + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + issuerDTO.setQr_code_type(QRCodeType.None); + + Map subjectData = new HashMap<>(); + subjectData.put("name", "John Doe"); + subjectData.put("face", ""); + + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(subjectData); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Full Name", "en"), "John Doe")); + displayProps.put("face", Map.of(createDisplayResponse("Face", "en"), "")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())) + .thenReturn(displayProps); + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())) + .thenReturn("$rowProperties"); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "TestCredential", vcCredentialResponse, issuerDTO, credentialsSupportedResponse, + "", "2025-12-31", "en"); + + assertNotNull(result); + } + + @Test + void testFormatValueWithListOfNonStringNonMap() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService( + objectMapper, + presentationService, + utilities, + pixelPass, + credentialFormatHandlerFactory, + injiVcRenderer, + svgFixerUtil + ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + List intList = Arrays.asList(1, 2, 3); + + String result = (String) formatValueMethod.invoke(service, intList, "en"); + + assertEquals("[1, 2, 3]", result); + } + + @Test + void testFormatValueWithListOfMapsWithNoLangKey() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService( + objectMapper, + presentationService, + utilities, + pixelPass, + credentialFormatHandlerFactory, + injiVcRenderer, + svgFixerUtil + ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map mapNoLang = new HashMap<>(); + mapNoLang.put("someOtherKey", "someValue"); + mapNoLang.put(LdpVcV1Constants.VALUE, "No Lang Value"); + + List> value = List.of(mapNoLang); + + String result = (String) formatValueMethod.invoke(service, value, "en"); + + assertEquals("", result); + } + + @Test + void testFormatValueWithListOfMapsWithNullValueKey() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService( + objectMapper, + presentationService, + utilities, + pixelPass, + credentialFormatHandlerFactory, + injiVcRenderer, + svgFixerUtil + ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + Map mapNoValue = new HashMap<>(); + mapNoValue.put(LdpVcV1Constants.LANGUAGE, "en"); + + List> value = List.of(mapNoValue); + + String result = (String) formatValueMethod.invoke(service, value, "en"); + + assertEquals("", result); + } + + @Test + void testFormatValueWithNullVal() throws Exception { + CredentialPDFGeneratorService service = new CredentialPDFGeneratorService( + objectMapper, + presentationService, + utilities, + pixelPass, + credentialFormatHandlerFactory, + injiVcRenderer, + svgFixerUtil + ); + Method formatValueMethod = CredentialPDFGeneratorService.class.getDeclaredMethod("formatValue", Object.class, String.class); + formatValueMethod.setAccessible(true); + + String result = (String) formatValueMethod.invoke(service, null, "en"); + + assertEquals("", result); + } + + @Test + void testInjiVcRendererNotInvokedWhenRenderMethodListContainsNonMapEntry() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of("not-a-map-entry")); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "credentialConfigId", vcCredentialResponse, issuerDTO, + credentialsSupportedResponse, "http://datashare.url", "2025-12-31", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testInjiVcRendererNotInvokedWhenRenderMethodListHasTemplateButWrongRenderSuite() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, List.of( + Map.of(TEMPLATE, "", RENDER_SUITE, "SomeOtherRenderSuite") + )); + + VCCredentialResponse vcCredentialResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(vcCredentialResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "credentialConfigId", vcCredentialResponse, issuerDTO, + credentialsSupportedResponse, "http://datashare.url", "2025-12-31", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testInjiVcRendererNotInvokedWhenRenderMethodIsMapWithoutTemplate() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, Map.of(RENDER_SUITE, SVG_MUSTACHE_RENDER_SUITE)); + + VCCredentialResponse localVcResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(localVcResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "credentialConfigId", localVcResponse, issuerDTO, + credentialsSupportedResponse, "http://datashare.url", "2025-12-31", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } + + @Test + void testInjiVcRendererNotInvokedWhenRenderMethodIsMapWithTemplateButWrongRenderSuite() throws Exception { + Map credentialMap = new HashMap<>(); + credentialMap.put(CONTEXT, List.of(V2_CONTEXT_URL)); + credentialMap.put(RENDER_METHOD, Map.of(TEMPLATE, "", RENDER_SUITE, "SomeOtherRenderSuite")); + + VCCredentialResponse localVcResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(credentialMap) + .build(); + + issuerDTO.setQr_code_type(QRCodeType.OnlineSharing); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(credentialFormatHandler); + + Map claims = new HashMap<>(); + claims.put("name", "John Doe"); + when(credentialFormatHandler.extractCredentialClaims(localVcResponse)).thenReturn(claims); + + LinkedHashMap> displayProps = new LinkedHashMap<>(); + displayProps.put("name", Map.of(createDisplayResponse("Name", "en"), "John Doe")); + when(credentialFormatHandler.loadDisplayPropertiesFromWellknown(any(), any(), anyString())).thenReturn(displayProps); + + when(utilities.getCredentialSupportedTemplateString(anyString(), anyString())).thenReturn(""); + when(objectMapper.writeValueAsString(any())).thenReturn("{}"); + when(presentationService.constructPresentationDefinition(any())).thenReturn(new PresentationDefinitionDTO()); + + ByteArrayInputStream result = credentialPDFGeneratorService.generatePdfForVerifiableCredential( + "credentialConfigId", localVcResponse, issuerDTO, + credentialsSupportedResponse, "http://datashare.url", "2025-12-31", "en"); + + assertNotNull(result); + verify(injiVcRenderer, never()).generateCredentialDisplayContent(any(), any(), anyString(), any()); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/CredentialRequestServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialRequestServiceTest.java new file mode 100644 index 000000000..18f46053d --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialRequestServiceTest.java @@ -0,0 +1,263 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.repository.ProofSigningKeyRepository; +import io.mosip.mimoto.service.impl.LdpVcCredentialFormatHandler; +import io.mosip.mimoto.util.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.mockito.ArgumentCaptor; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {Draft13CredentialRequestService.class, Draft13CredentialRequestBuilder.class}) +@TestPropertySource(locations = "classpath:application-test.properties") +public class CredentialRequestServiceTest { + @MockBean + private ObjectMapper objectMapper; + + @Autowired + private Draft13CredentialRequestService credentialRequestServiceImpl; + + @MockBean + private ProofSigningKeyRepository proofSigningKeyRepository; + + @MockBean + private CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + @MockBean + private KeyPairRetrievalService keyPairService; + + @Autowired + private Draft13CredentialRequestBuilder draft13CredentialRequestBuilder; + + private final MockedStatic keyGenerationUtilMockedStatic = Mockito.mockStatic(SigningKeyUtil.class, Mockito.withSettings().defaultAnswer(Mockito.CALLS_REAL_METHODS)); + + IssuerDTO issuerDTO; + String issuerId; + + @Before + public void setUp() { + issuerId = "issuer1"; + issuerDTO = getIssuerConfigDTO(issuerId); + when(credentialFormatHandlerFactory.getHandler("ldp_vc")).thenReturn(new LdpVcCredentialFormatHandler(objectMapper)); + } + + @After + public void tearDown() { + if(keyGenerationUtilMockedStatic != null) { + keyGenerationUtilMockedStatic.close(); + } + } + + @Test + public void shouldHandleNullContextInCredentialSupportedResponse() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + credentialsSupportedResponse.getCredentialDefinition().setContext(null); + + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + + Draft13VCCredentialRequest result = credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + assertNotNull(result.getCredentialDefinition().getContext()); + assertEquals("https://www.w3.org/2018/credentials/v1", result.getCredentialDefinition().getContext().getFirst()); + } + + @Test + public void shouldHandleEmptyContextInCredentialSupportedResponse() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + + // Set an empty list as context + credentialsSupportedResponse.getCredentialDefinition().setContext(List.of()); + + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + Draft13VCCredentialRequest result = credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + assertNotNull(result.getCredentialDefinition().getContext()); + assertEquals("https://www.w3.org/2018/credentials/v1", result.getCredentialDefinition().getContext().getFirst()); + } + + + @Test + public void shouldHandleExistingContextWithSpecificValue() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + + // Set context with a specific value + credentialsSupportedResponse.getCredentialDefinition().setContext(List.of("https://www.w3.org/ns/credentials/v2")); + + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + + Draft13VCCredentialRequest result = credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + assertNotNull(result.getCredentialDefinition().getContext()); + assertEquals("https://www.w3.org/ns/credentials/v2", result.getCredentialDefinition().getContext().getFirst()); + } + + @Test + public void shouldReturnFallbackAlgorithmWhenJwtProofTypeOfProofTypesSupportedIsNull() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + credentialsSupportedResponse.getProofTypesSupported().put("jwt", null); + + + credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + keyGenerationUtilMockedStatic.verify(() -> SigningKeyUtil.generateKeyPair(argumentCaptor.capture())); + SigningAlgorithm capturedAlgorithm = argumentCaptor.getValue(); + assertEquals(SigningAlgorithm.ED25519, capturedAlgorithm); + } + + @Test + public void shouldReturnFallbackAlgorithmWhenJwtProofTypeOfProofTypesSupportedIsEmpty() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + ProofTypesSupported proofTypesSupported = new ProofTypesSupported(); + proofTypesSupported.setProofSigningAlgValuesSupported(Collections.emptyList()); + credentialsSupportedResponse.getProofTypesSupported().put("jwt", proofTypesSupported); + + credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + keyGenerationUtilMockedStatic.verify(() -> SigningKeyUtil.generateKeyPair(argumentCaptor.capture())); + SigningAlgorithm capturedAlgorithm = argumentCaptor.getValue(); + assertEquals(SigningAlgorithm.ED25519, capturedAlgorithm); + } + + @Test + public void shouldUseHighestPriorityAlgorithmInPredefinedSigningAlgoPrioritySetWhichIsAlsoSupportedByIssuer() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + credentialsSupportedResponse.getProofTypesSupported().get("jwt").setProofSigningAlgValuesSupported(List.of("es256k")); + + + credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + keyGenerationUtilMockedStatic.verify(() -> SigningKeyUtil.generateKeyPair(argumentCaptor.capture())); + SigningAlgorithm capturedAlgorithm = argumentCaptor.getValue(); + assertEquals(SigningAlgorithm.ES256K, capturedAlgorithm); + } + + @Test + public void shouldUseFallbackPrioritySigningAlgorithmIfNoneOfIssuerSupportedAlgoAreSupportedByMimto() throws Exception { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + CredentialIssuerWellKnownResponse issuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + issuerWellKnownResponse.setCredentialIssuer("https://example-issuer.com"); + issuerWellKnownResponse.setCredentialConfigurationsSupported( + Map.of("CredentialType1", credentialsSupportedResponse) + ); + credentialsSupportedResponse.getProofTypesSupported().get("jwt").setProofSigningAlgValuesSupported(List.of("ps256")); + + + credentialRequestServiceImpl.buildRequest( + issuerDTO, + "CredentialType1", + issuerWellKnownResponse, + "test-cnonce", + "walletId", + "walletKey", + false + ); + + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + keyGenerationUtilMockedStatic.verify(() -> SigningKeyUtil.generateKeyPair(argumentCaptor.capture())); + SigningAlgorithm capturedAlgorithm = argumentCaptor.getValue(); + assertEquals(SigningAlgorithm.ED25519, capturedAlgorithm); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/CredentialServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialServiceTest.java new file mode 100644 index 000000000..393b47c08 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialServiceTest.java @@ -0,0 +1,551 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.VCVerificationException; +import io.mosip.mimoto.model.QRCodeType; +import io.mosip.mimoto.model.VerifiableCredential; +import io.mosip.mimoto.service.impl.CredentialServiceImpl; +import io.mosip.mimoto.service.impl.IssuersServiceImpl; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import io.mosip.mimoto.util.TestUtilities; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import static org.mockito.Mockito.mock; +import io.mosip.mimoto.repository.WalletCredentialsRepository; +import io.mosip.mimoto.dto.mimoto.IssuerConfig; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponseDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import static io.mosip.mimoto.exception.ErrorConstants.INVALID_REQUEST; +import io.mosip.mimoto.exception.CredentialProcessingException; +import static io.mosip.mimoto.exception.ErrorConstants.CREDENTIAL_DOWNLOAD_EXCEPTION; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import static io.mosip.mimoto.exception.ErrorConstants.SERVER_UNAVAILABLE; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.mosip.mimoto.exception.ErrorConstants.SIGNATURE_VERIFICATION_EXCEPTION; +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +@SpringBootTest +public class CredentialServiceTest { + + @Mock + VCDownloadHandlerFactory vcDownloadHandlerFactory; + + @Mock + VCDownloadHandler vcDownloadHandler; + + @Mock + CredentialVerifierService credentialVerifierService; + + @InjectMocks + CredentialServiceImpl credentialService; + + @Mock + CredentialPDFGeneratorService credentialUtilService; + + @Mock + IssuersServiceImpl issuersService; + + @Mock + WalletCredentialsRepository walletCredentialsRepository; + + @Mock + DataProtectionService dataProtectionService; + + @Mock + ObjectMapper objectMapper; + + TokenResponseDTO expectedTokenResponse; + String tokenEndpoint, issuerId; + IssuerDTO issuerDTO; + HttpEntity> mockRequest; + CredentialIssuerConfiguration issuerConfig; + CredentialIssuerWellKnownResponse wellKnownResponse; + + @Before + public void setUp() throws Exception { + issuerId = "issuer1"; + issuerDTO = getIssuerConfigDTO(issuerId); + issuerConfig = getCredentialIssuerConfigurationResponseDto(issuerId, "CredentialType1", List.of()); + + wellKnownResponse = getCredentialIssuerWellKnownResponseDto(issuerId, + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + wellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + Mockito.when(issuersService.getIssuerDetails(issuerId)).thenReturn(issuerDTO); + Mockito.when(issuersService.getIssuerWellKnownResponse(issuerDTO.getCredential_issuer_host())).thenReturn(wellKnownResponse); + + tokenEndpoint = issuerConfig.getAuthorizationServerWellKnownResponse().getTokenEndpoint(); + mockRequest = new HttpEntity<>(new LinkedMultiValueMap<>(Map.of( + "grant_type", List.of("client_credentials"), + "client_id", List.of("test-client") + ))); + expectedTokenResponse = getTokenResponseDTO(); + } + + @Test + public void shouldParseHtmlStringToDocument() { + String htmlContent = "

$message

"; + Map data = new HashMap<>(); + data.put("message", "PDF"); + VelocityContext velocityContext = new VelocityContext(); + StringWriter writer = new StringWriter(); + velocityContext.put("message", data.get("message")); + Velocity.evaluate(velocityContext, writer, "Credential Template", htmlContent); + String mergedHtml = writer.toString(); + assertTrue(mergedHtml.contains("PDF")); + } + + @Test + public void shouldThrowExceptionIfDownloadedVCSignatureVerificationFailed() throws Exception { + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(IssuerDTO.class), any(String.class), + any(CredentialIssuerWellKnownResponse.class), any(TokenResponseDTO.class), + any(), any(), eq(false))).thenReturn(getVCCredentialResponseDTO("CredentialType1")); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(false); + VCVerificationException actualException = assertThrows(VCVerificationException.class, () -> + credentialService.downloadCredentialAsPDF(issuerId, "CredentialType1", expectedTokenResponse, "once", "en")); + + assertEquals("signature_verification_failed --> Error while doing signature verification", actualException.getMessage()); + } + + @Test + public void shouldReturnDownloadedVCAsPDFIfSignatureVerificationIsSuccessful() throws Exception { + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(IssuerDTO.class), any(String.class), + any(CredentialIssuerWellKnownResponse.class), any(TokenResponseDTO.class), + any(), any(), eq(false))).thenReturn(getVCCredentialResponseDTO("CredentialType1")); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(true); + issuerDTO.setQr_code_type(QRCodeType.None); + + ByteArrayInputStream expectedPDFByteArray = generatePdfFromHTML(); + Mockito.when(credentialUtilService.generatePdfForVerifiableCredential( + eq("CredentialType1"), + any(VCCredentialResponse.class), + eq(issuerDTO), + eq(wellKnownResponse.getCredentialConfigurationsSupported().get("CredentialType1")), + eq(""), + eq("once"), + eq("en") + )).thenReturn(expectedPDFByteArray); + + ByteArrayInputStream actualPDFByteArray = + credentialService.downloadCredentialAsPDF(issuerId, "CredentialType1", expectedTokenResponse, "once", "en"); + + assertEquals(expectedPDFByteArray, actualPDFByteArray); + } + + @Test + public void shouldDownloadCredentialAndStoreInDBSuccessfully() throws Exception { + // Setup test data + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + VerifiableCredential savedCredential = new VerifiableCredential(); + savedCredential.setId("credential-id-123"); + + // Setup issuer config mocks + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + + // Mock service calls + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), eq(credentialConfigurationId), any(), any(), eq(walletId), eq(base64Key), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(true); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(dataProtectionService.encryptCredential(any(), eq(base64Key))).thenReturn("encrypted-credential"); + when(walletCredentialsRepository.save(any(VerifiableCredential.class))).thenReturn(savedCredential); + + // Execute + VerifiableCredentialResponseDTO result = credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale); + + // Verify + assertNotNull(result); + assertEquals("credential-id-123", result.getCredentialId()); + } + + @Test + public void shouldThrowVCVerificationExceptionWhenVerificationFails() throws Exception { + // Setup test data + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + // Setup issuer config mocks + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + + // Mock service calls + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + when(credentialVerifierService.verify(any())).thenReturn(false); + + // Execute and verify exception + VCVerificationException exception = assertThrows(VCVerificationException.class, () -> + credentialService.downloadCredentialAndStoreInDB(tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(SIGNATURE_VERIFICATION_EXCEPTION.getErrorCode(), exception.getErrorCode()); + } + + @Test + public void shouldThrowInvalidRequestExceptionForNullTokenResponse() { + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + null, "CredentialType1", "wallet123", "testKey123", "issuer1", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Token response or access token cannot be null")); + } + + @Test + public void shouldThrowInvalidRequestExceptionForBlankAccessToken() { + TokenResponseDTO tokenResponse = new TokenResponseDTO(); + tokenResponse.setAccess_token(""); // blank token + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, "CredentialType1", "wallet123", "testKey123", "issuer1", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Token response or access token cannot be null")); + } + + @Test + public void shouldThrowInvalidRequestExceptionForBlankCredentialConfigurationId() { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, "", "wallet123", "testKey123", "issuer1", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Credential configuration id cannot be null or blank")); + } + + @Test + public void shouldThrowInvalidRequestExceptionForBlankWalletId() { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, "CredentialType1", "", "testKey123", "issuer1", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Wallet ID cannot be null or blank")); + } + + @Test + public void shouldThrowInvalidRequestExceptionForBlankBase64Key() { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, "CredentialType1", "wallet123", "", "issuer1", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Wallet key cannot be null or blank")); + } + + @Test + public void shouldThrowInvalidRequestExceptionForBlankIssuerId() { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, "CredentialType1", "wallet123", "testKey123", "", "en")); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Issuer ID cannot be null or blank")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionWhenFetchIssuerConfigFails() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock issuersService to throw exception + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)) + .thenThrow(new RuntimeException("Issuer service unavailable")); + + // Execute and verify exception + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Unable to fetch issuer configuration")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionWhenBuildCredentialRequestFails() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until buildRequest + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + + // Mock vcDownloadHandler to throw exception (simulating build credential request failure) + when(vcDownloadHandler.downloadCredential(any(), eq(credentialConfigurationId), any(), any(), eq(walletId), eq(base64Key), eq(true))) + .thenThrow(new CredentialProcessingException(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), "Unable to generate credential request")); + + // Execute and verify exception + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Unable to generate credential request")); + } + + @Test + public void shouldThrowExternalServiceUnavailableExceptionWhenDownloadCredentialFromIssuerFails() throws Exception { + TokenResponseDTO tokenResponse = TestUtilities.getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until downloadCredential + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = TestUtilities.getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + + // Mock vcDownloadHandler to throw exception during credential download + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenThrow(new ExternalServiceUnavailableException(SERVER_UNAVAILABLE.getErrorCode(), SERVER_UNAVAILABLE.getErrorMessage())); + + // Execute and verify exception + ExternalServiceUnavailableException exception = assertThrows(ExternalServiceUnavailableException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(SERVER_UNAVAILABLE.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains(SERVER_UNAVAILABLE.getErrorMessage())); + } + + @Test + public void shouldThrowVCVerificationExceptionWhenCredentialVerifierServiceThrowsException() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until verify + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + + // Mock credentialVerifierService to throw exception + when(credentialVerifierService.verify(any(VCCredentialResponse.class))) + .thenThrow(new JsonProcessingException("JSON processing failed") {}); + + // Execute and verify exception + VCVerificationException exception = assertThrows(VCVerificationException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(SIGNATURE_VERIFICATION_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Credential verification failed")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionWhenSerializationFails() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until serialization + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(true); + + // Mock objectMapper to throw JsonProcessingException + when(objectMapper.writeValueAsString(any())) + .thenThrow(new JsonProcessingException("Serialization failed") {}); + + // Execute and verify exception + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Unable to serialize credential response")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionWhenEncryptionFails() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until encryption + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(true); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + + // Mock dataProtectionService to throw exception + when(dataProtectionService.encryptCredential(any(), eq(base64Key))) + .thenThrow(new RuntimeException("Encryption failed")); + + // Execute and verify exception + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Unable to encrypt credential data")); + } + + @Test + public void shouldThrowCredentialProcessingExceptionWhenSaveCredentialFails() throws Exception { + TokenResponseDTO tokenResponse = getTokenResponseDTO(); + String credentialConfigurationId = "CredentialType1"; + String walletId = "wallet123"; + String base64Key = "testKey123"; + String issuerId = "issuer1"; + String locale = "en"; + + // Mock dependencies for success until save + IssuerConfig issuerConfig = mock(IssuerConfig.class); + IssuerDTO mockIssuerDTO = getIssuerConfigDTO(issuerId); + CredentialIssuerWellKnownResponse mockWellKnownResponse = new CredentialIssuerWellKnownResponse(); + mockWellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + mockWellKnownResponse.setVersion(VCSpecificationVersion.DRAFT_13); + + when(issuerConfig.getIssuerDTO()).thenReturn(mockIssuerDTO); + when(issuerConfig.getWellKnownResponse()).thenReturn(mockWellKnownResponse); + when(issuersService.getIssuerConfig(issuerId, credentialConfigurationId)).thenReturn(issuerConfig); + when(vcDownloadHandlerFactory.getHandler(VCSpecificationVersion.DRAFT_13)).thenReturn(vcDownloadHandler); + when(vcDownloadHandler.downloadCredential(any(), any(), any(), any(), any(), any(), eq(true))) + .thenReturn(getVCCredentialResponseDTO(credentialConfigurationId)); + when(credentialVerifierService.verify(any(VCCredentialResponse.class))).thenReturn(true); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"credential\":\"data\"}"); + when(dataProtectionService.encryptCredential(any(), eq(base64Key))).thenReturn("encrypted-credential"); + + // Mock walletCredentialsRepository to throw exception + when(walletCredentialsRepository.save(any(VerifiableCredential.class))) + .thenThrow(new RuntimeException("Database connection failed")); + + // Execute and verify exception + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + credentialService.downloadCredentialAndStoreInDB( + tokenResponse, credentialConfigurationId, walletId, base64Key, issuerId, locale)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertTrue(exception.getMessage().contains("Unable to save credential to database")); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/CredentialShareServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialShareServiceTest.java new file mode 100644 index 000000000..5ea2a6334 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialShareServiceTest.java @@ -0,0 +1,739 @@ +package io.mosip.mimoto.service; + +import com.google.common.collect.Lists; +import io.mosip.kernel.biometrics.constant.BiometricType; +import io.mosip.kernel.biometrics.entities.BDBInfo; +import io.mosip.kernel.biometrics.entities.BIR; +import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.CryptoWithPinResponseDto; +import io.mosip.mimoto.exception.DocumentGeneratorException; +import io.mosip.mimoto.exception.IdentityNotFoundException; +import io.mosip.mimoto.model.Event; +import io.mosip.mimoto.model.EventModel; +import io.mosip.mimoto.service.impl.CredentialShareServiceImpl; +import io.mosip.mimoto.util.*; +import org.json.simple.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.net.URI; +import java.security.InvalidKeyException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class CredentialShareServiceTest { + + @Spy + @InjectMocks + private CredentialShareServiceImpl service; + + @Mock + public RestApiClient restApiClient; + + @Mock + public AuditLogRequestBuilder auditLogRequestBuilder; + + @Mock + private P12KeyStoreManager p12KeyStoreManager; + + @Mock + DerivedKeyCryptoUtil derivedKeyCryptoUtil; + + @Mock + private Utilities utilities; + + @Mock + private WebSubSubscriptionHelper webSubSubscriptionHelper; + + @Mock + private DataShareUtil dataShareUtil; + + @Mock + private RestClientService restClientService; + + @Mock + private org.springframework.core.env.Environment env; + + @Mock + private io.mosip.kernel.core.websub.spi.PublisherClient pb; + + @Mock + private CbeffToBiometricUtil util; + + private EventModel eventModel = null; + + @Before + public void setup() throws Exception { + Map proofMap = new HashMap<>(); + proofMap.put("signature", "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJQUzI1NiJ9..XSGdtAvEdcSrdHiyC8pP8orML1v3akfHru821aMVNKSa9ftx3DIS1ZbqNL-DNxWmdLjcINpsWIbwyD7Lbn0MZNIsmR5SjJJa31cB710aFpMfQgHY0R2cgh8XJWIU4Whs8Tvt9RWJ2la0CkIdeukEjxKQs2Ier1x3wzbVSK6OeoYO6tM77saDsDciu88JGRLt5zdkNyc9hN0XNwH3SPG1hJOKV4QUHW3CzIFGKvcR-VZHD7iAyqrgVNjEt1IU7ghdqH38xiTZ-2QfJDkH90yUmUEkHJ-AcXlFZATIi3YYPY9UNSBEZeHaN-fOwrPETftTo6a_DZiYIYeW1eYVs_hvnA\",\n" + + " \"proofPurpose\": \"assertionMethod"); + Map data = new HashMap<>(); + data.put("credential", "credential"); + data.put("protectionKey", "12345"); + data.put("credentialType", "VERCRED"); + data.put("proof", proofMap); + + Event event = new Event(); + event.setDataShareUri("https://www.datashare.com"); + event.setTransactionId("transactionid"); + event.setData(data); + + eventModel = new EventModel(); + eventModel.setEvent(event); + + CryptoWithPinResponseDto cryptoWithPinResponseDto = new CryptoWithPinResponseDto(); + cryptoWithPinResponseDto.setData("biometrics"); + + Mockito.when(derivedKeyCryptoUtil.decryptWithPin(ArgumentMatchers.any())).thenReturn(cryptoWithPinResponseDto); + Mockito.when(utilities.getDataPath()).thenReturn("target"); + Mockito.when(restApiClient.getApi(Mockito.any(URI.class), Mockito.any(Class.class))).thenReturn("credential"); + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn("{\"credentialSubject\":{\"biometrics\":\"biometrics\"},\"protectedAttributes\":[\"biometrics\"]}"); + + Map templateMap = new HashMap<>(); + templateMap.put("biometrics", "biometrics"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + + Mockito.when(auditLogRequestBuilder.createAuditRequestBuilder( + Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), + Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.any())).thenReturn(new ResponseWrapper<>()); + + BIR bir = new BIR(); + BDBInfo bdbInfo = new BDBInfo(); + bdbInfo.setType(Lists.newArrayList(BiometricType.FACE)); + bir.setBdbInfo(bdbInfo); + List birs = Lists.newArrayList(bir); + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenReturn(birs); + Mockito.when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("face image strring".getBytes()); + + } + + @Test + public void generateDocumentsTest() throws Exception { + + boolean result = service.generateDocuments(eventModel); + + assertTrue(result); + } + + @Test + public void documentExceptionTest() throws Exception { + + Mockito.when(derivedKeyCryptoUtil.decryptWithPin(ArgumentMatchers.any())).thenThrow(new InvalidKeyException("exception")); + + boolean result = service.generateDocuments(eventModel); + + assertFalse(result); + + } + + @Test + public void generateDocumentsWithNullDataShareUriTest() throws Exception { + eventModel.getEvent().setDataShareUri(null); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + Mockito.verify(restApiClient, Mockito.never()).getApi(Mockito.any(URI.class), Mockito.any(Class.class)); + } + + @Test + public void generateDocumentsWithEmptyDataShareUriTest() throws Exception { + eventModel.getEvent().setDataShareUri(""); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + Mockito.verify(restApiClient, Mockito.never()).getApi(Mockito.any(URI.class), Mockito.any(Class.class)); + } + + @Test + public void generateDocumentsWithUINTest() throws Exception { + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())) + .thenReturn("{\"credentialSubject\":{\"UIN\":\"1234567890\",\"biometrics\":\"biometrics\"},\"protectedAttributes\":[\"biometrics\"]}"); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithVIDTest() throws Exception { + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())) + .thenReturn("{\"credentialSubject\":{\"PCN\":\"9876543210\",\"biometrics\":\"biometrics\"},\"protectedAttributes\":[\"biometrics\"]}"); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithExceptionInGetDocumentsTest() throws Exception { + Mockito.reset(p12KeyStoreManager); + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())) + .thenThrow(new RuntimeException("Decryption failed")); + boolean result = service.generateDocuments(eventModel); + assertFalse(result); + } + + @Test + public void generateDocumentsWithNullCredentialJSONTest() throws Exception { + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())) + .thenReturn("{\"credentialSubject\":null,\"protectedAttributes\":[]}"); + boolean result = service.generateDocuments(eventModel); + assertFalse(result); + } + + @Test + public void generateDocumentsWithArrayListFieldTest() throws Exception { + String credentialWithArrayList = "{\"credentialSubject\":{" + + "\"biometrics\":\"biometrics\"," + + "\"name\":[{\"language\":\"eng\",\"value\":\"John Doe\"},{\"language\":\"ara\",\"value\":\"جون دو\"}]" + + "},\"protectedAttributes\":[\"biometrics\"]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithArrayList); + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "name"); + templateMap.put("biometrics", "biometrics"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithMultiLanguageFieldsFilteredBySupportedLang() throws Exception { + ReflectionTestUtils.setField(service, "supportedLang", "eng,ara"); + + java.util.LinkedHashMap engEntry = new java.util.LinkedHashMap<>(); + engEntry.put("language", "eng"); + engEntry.put("value", "John Smith"); + java.util.LinkedHashMap araEntry = new java.util.LinkedHashMap<>(); + araEntry.put("language", "ara"); + araEntry.put("value", "جون سميث"); + java.util.LinkedHashMap fraEntry = new java.util.LinkedHashMap<>(); + fraEntry.put("language", "fra"); + fraEntry.put("value", "Jean Smith"); + + org.json.simple.JSONArray node = new org.json.simple.JSONArray(); + node.add(engEntry); + node.add(araEntry); + node.add(fraEntry); + + io.mosip.mimoto.dto.JsonValue[] jsonValues = + invokeMapJsonNodeToJavaObject(io.mosip.mimoto.dto.JsonValue.class, node); + + org.json.JSONObject outputJSON = new org.json.JSONObject(); + String supportedLang = "eng,ara"; + for (io.mosip.mimoto.dto.JsonValue jsonValue : jsonValues) { + String lang = (String) ReflectionTestUtils.getField(jsonValue, "language"); + if (lang != null && supportedLang.contains(lang)) { + outputJSON.put("fullName_" + lang, jsonValue); + } + } + + assertTrue("Expected fullName_eng in output", outputJSON.has("fullName_eng")); + assertTrue("Expected fullName_ara in output", outputJSON.has("fullName_ara")); + assertFalse("Expected fullName_fra absent from output (fra not in supportedLang)", + outputJSON.has("fullName_fra")); + } + + @Test + public void generateDocumentsWithNullProtectedAttributesInDecryptAttribute() throws Exception { + String credentialWithNullProtectedAttrs = "{\"credentialSubject\":{" + + "\"biometrics\":\"biometrics\"," + + "\"fullName\":\"Test User\"" + + "},\"protectedAttributes\":null}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithNullProtectedAttrs); + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "fullName"); + templateMap.put("biometrics", "biometrics"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithEmptyProtectedAttributesInDecryptAttribute() throws Exception { + String credentialWithEmptyProtectedAttrs = "{\"credentialSubject\":{" + + "\"biometrics\":\"biometrics\"," + + "\"fullName\":\"Test User\"" + + "},\"protectedAttributes\":[]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithEmptyProtectedAttrs); + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "fullName"); + templateMap.put("biometrics", "biometrics"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithLinkedHashMapFieldTest() throws Exception { + String credentialWithLinkedHashMap = "{\"credentialSubject\":{" + + "\"biometrics\":\"biometrics\"," + + "\"dateOfBirth\":{\"value\":\"1990/01/01\"}" + + "},\"protectedAttributes\":[\"biometrics\"]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithLinkedHashMap); + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "dateOfBirth"); + templateMap.put("biometrics", "biometrics"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithBiometricsKeyTest() throws Exception { + String credentialWithBiometrics = "{\"credentialSubject\":{" + + "\"biometrics\":\"encodedBiometricData\"," + + "\"fullName\":\"Test User\"" + + "},\"protectedAttributes\":[\"biometrics\"]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithBiometrics); + Map templateMap = new HashMap<>(); + templateMap.put("biometrics", "biometrics"); + templateMap.put("demographics", "fullName"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithMultipleTemplateFieldsTest() throws Exception { + String credentialWithMultipleFields = "{\"credentialSubject\":{" + + "\"biometrics\":\"biometricData\"," + + "\"fullName\":\"Jane Smith\"," + + "\"address\":[{\"language\":\"eng\",\"value\":\"123 Main St\"},{\"language\":\"ara\",\"value\":\"123 الشارع الرئيسي\"}]," + + "\"gender\":{\"value\":\"Female\"}" + + "},\"protectedAttributes\":[\"biometrics\"]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithMultipleFields); + Map templateMap = new HashMap<>(); + templateMap.put("biometrics", "biometrics"); + templateMap.put("demographics", "fullName,address,gender"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithNullIndividualBiometricTest() throws Exception { + String credentialWithNullBiometrics = "{\"credentialSubject\":{" + + "\"biometrics\":null," + + "\"fullName\":\"Test User\"" + + "},\"protectedAttributes\":[]}"; + Mockito.when(p12KeyStoreManager.decrypt(Mockito.anyString())).thenReturn(credentialWithNullBiometrics); + Map templateMap = new HashMap<>(); + templateMap.put("biometrics", "biometrics"); + templateMap.put("demographics", "fullName"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithEmptyBIRTypeListTest() throws Exception { + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenReturn(Lists.newArrayList()); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithNullPhotoBytesTest() throws Exception { + Mockito.when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(null); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithEmptySubTypeTest() throws Exception { + BIR bir = new BIR(); + BDBInfo bdbInfo = new BDBInfo(); + bdbInfo.setType(Lists.newArrayList(BiometricType.FINGER)); + bdbInfo.setSubtype(Lists.newArrayList()); + bir.setBdbInfo(bdbInfo); + List birs = Lists.newArrayList(bir); + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenReturn(birs); + Mockito.when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn("finger image with empty subtype".getBytes()); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithNonEmptySubTypeTest() throws Exception { + BIR bir = new BIR(); + BDBInfo bdbInfo = new BDBInfo(); + bdbInfo.setType(Lists.newArrayList(BiometricType.FINGER)); + bdbInfo.setSubtype(Lists.newArrayList("Left", "Thumb")); + bir.setBdbInfo(bdbInfo); + List birs = Lists.newArrayList(bir); + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenReturn(birs); + Mockito.when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("finger image with subtype".getBytes()); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithMultipleBiometricTypesTest() throws Exception { + BIR faceBir = new BIR(); + BDBInfo faceBdbInfo = new BDBInfo(); + faceBdbInfo.setType(Lists.newArrayList(BiometricType.FACE)); + faceBdbInfo.setSubtype(Lists.newArrayList()); + faceBir.setBdbInfo(faceBdbInfo); + + BIR fingerBir = new BIR(); + BDBInfo fingerBdbInfo = new BDBInfo(); + fingerBdbInfo.setType(Lists.newArrayList(BiometricType.FINGER)); + fingerBdbInfo.setSubtype(Lists.newArrayList("Left", "IndexFinger")); + fingerBir.setBdbInfo(fingerBdbInfo); + + BIR irisBir = new BIR(); + BDBInfo irisBdbInfo = new BDBInfo(); + irisBdbInfo.setType(Lists.newArrayList(BiometricType.IRIS)); + irisBdbInfo.setSubtype(Lists.newArrayList("Right")); + irisBir.setBdbInfo(irisBdbInfo); + + List birs = Lists.newArrayList(faceBir, fingerBir, irisBir); + + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenReturn(birs); + Mockito.lenient().when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.eq("face"), Mockito.any())) + .thenReturn("face image data".getBytes()); + Mockito.lenient().when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.eq("finger"), Mockito.any())) + .thenReturn("finger image data".getBytes()); + Mockito.lenient().when(util.getPhotoByTypeAndSubType(Mockito.any(), Mockito.eq("iris"), Mockito.any())) + .thenReturn("iris image data".getBytes()); + + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void generateDocumentsWithBiometricProcessingExceptionTest() throws Exception { + Mockito.when(util.getBIRTypeList(Mockito.anyString())).thenThrow(new RuntimeException("Biometric processing error")); + boolean result = service.generateDocuments(eventModel); + assertTrue(result); + } + + @Test + public void testGetDocumentsWithVidOnly() throws Exception { + ReflectionTestUtils.setField(service, "vid", "PCN"); + + org.json.JSONObject credentialJson = new org.json.JSONObject(); + credentialJson.put("biometrics", "dummyBiometric"); + credentialJson.put("PCN", "9876543210"); + + Mockito.doReturn(new org.json.JSONObject()).when(service).getBiometricsDataJSON(Mockito.anyString()); + + Map result = service.getDocuments(credentialJson, "VERCRED", "req-123", "dummySign"); + + assertNotNull(result); + String uinTextFile = (String) ReflectionTestUtils.getField(CredentialShareServiceImpl.class, "UIN_TEXT_FILE"); + assertTrue(result.containsKey(uinTextFile)); + } + + @Test(expected = DocumentGeneratorException.class) + public void testGetDocumentsThrowsDocumentGeneratorExceptionAndSetsFailureAudit() throws Exception { + org.json.JSONObject credentialJson = new org.json.JSONObject(); + credentialJson.put("UIN", "1234567890"); + credentialJson.put("biometrics", "dummyBiometric"); + + Mockito.doThrow(new RuntimeException("forced failure")) + .when(service).createJSONFile(Mockito.any(org.json.JSONObject.class), Mockito.anyString()); + + service.getDocuments(credentialJson, "VERCRED", "req-456", "dummySign"); + } + + @Test(expected = IdentityNotFoundException.class) + public void testCreateJSONFileWithNullCredential() throws Exception { + service.createJSONFile(null, "biometricData"); + } + + @Test + public void testCreateJSONFileWithArrayListField() throws Exception { + ReflectionTestUtils.setField(service, "supportedLang", "eng,ara"); + + java.util.LinkedHashMap engMap = new java.util.LinkedHashMap<>(); + engMap.put("language", "eng"); + engMap.put("value", "John"); + java.util.LinkedHashMap fraMap = new java.util.LinkedHashMap<>(); + fraMap.put("language", "fra"); + fraMap.put("value", "Jean"); + org.json.simple.JSONArray node = new org.json.simple.JSONArray(); + node.add(engMap); + node.add(fraMap); + + io.mosip.mimoto.dto.JsonValue[] jsonValues = + invokeMapJsonNodeToJavaObject(io.mosip.mimoto.dto.JsonValue.class, node); + + assertNotNull(jsonValues); + assertEquals(2, jsonValues.length); + + org.json.JSONObject outputJSON = new org.json.JSONObject(); + String supportedLang = "eng,ara"; + for (io.mosip.mimoto.dto.JsonValue jsonValue : jsonValues) { + String lang = (String) ReflectionTestUtils.getField(jsonValue, "language"); + if (lang != null && supportedLang.contains(lang)) { + outputJSON.put("fullName_" + lang, jsonValue); + } + } + + assertTrue("Expected fullName_eng in output", outputJSON.has("fullName_eng")); + assertFalse("Expected fullName_fra absent (not in supportedLang)", outputJSON.has("fullName_fra")); + } + + @Test + public void testCreateJSONFileWithLinkedHashMapField() throws Exception { + org.json.JSONObject credential = new org.json.JSONObject( + "{\"gender\":{\"value\":\"Male\"}}" + ); + + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "gender"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + + byte[] result = service.createJSONFile(credential, null); + + assertNotNull(result); + String output = new String(result, java.nio.charset.StandardCharsets.UTF_8); + assertTrue(output.contains("Male")); + } + + @Test + public void testCreateJSONFileWithMissingTemplateKey() throws Exception { + org.json.JSONObject credential = new org.json.JSONObject(); + credential.put("fullName", "John Doe"); + + Map templateMap = new HashMap<>(); + templateMap.put("demographics", "nonExistentField"); + JSONObject templateJSON = new JSONObject(templateMap); + Mockito.when(utilities.getTemplate()).thenReturn(templateJSON); + + byte[] result = service.createJSONFile(credential, null); + + assertNotNull(result); + assertTrue(result.length > 0); + String output = new String(result, java.nio.charset.StandardCharsets.UTF_8); + assertFalse("Missing key should not appear in output", output.contains("nonExistentField")); + } + + + @Test + public void testGetBiometricsDataJSONWithNullInput() { + org.json.JSONObject result = service.getBiometricsDataJSON(null); + + assertNotNull(result); + assertEquals(0, result.length()); + } + + private static byte[] buildValidFaceRecord(byte[] imagePayload) throws Exception { + ByteArrayOutputStream repBaos = new ByteArrayOutputStream(); + DataOutputStream repDos = new DataOutputStream(repBaos); + + repDos.write(new byte[14]); + repDos.writeByte(1); + repDos.write(new byte[5]); + repDos.writeShort(1); + repDos.write(new byte[15]); + repDos.write(new byte[8]); + repDos.writeByte(0); + repDos.writeByte(0); + repDos.write(new byte[9]); + repDos.writeInt(imagePayload.length); + repDos.write(imagePayload); + repDos.flush(); + byte[] repData = repBaos.toByteArray(); + + ByteArrayOutputStream outerBaos = new ByteArrayOutputStream(); + DataOutputStream outerDos = new DataOutputStream(outerBaos); + outerDos.write(new byte[4]); + outerDos.write(new byte[4]); + outerDos.writeInt(0); + outerDos.writeShort(1); + outerDos.writeByte(0); + outerDos.write(new byte[2]); + outerDos.writeInt(repData.length + 4); + outerDos.write(repData); + outerDos.flush(); + return outerBaos.toByteArray(); + } + + private static byte[] buildValidFaceRecordNoQualityNoLandmarks(byte[] imagePayload) throws Exception { + ByteArrayOutputStream repBaos = new ByteArrayOutputStream(); + DataOutputStream repDos = new DataOutputStream(repBaos); + + repDos.write(new byte[14]); + repDos.writeByte(0); + repDos.writeShort(0); + repDos.write(new byte[15]); + repDos.writeByte(0); + repDos.writeByte(0); + repDos.write(new byte[9]); + repDos.writeInt(imagePayload.length); + repDos.write(imagePayload); + repDos.flush(); + byte[] repData = repBaos.toByteArray(); + + ByteArrayOutputStream outerBaos = new ByteArrayOutputStream(); + DataOutputStream outerDos = new DataOutputStream(outerBaos); + outerDos.write(new byte[4]); + outerDos.write(new byte[4]); + outerDos.writeInt(0); + outerDos.writeShort(1); + outerDos.writeByte(0); + outerDos.write(new byte[2]); + outerDos.writeInt(repData.length + 4); + outerDos.write(repData); + outerDos.flush(); + return outerBaos.toByteArray(); + } + + private CredentialShareServiceImpl buildTestService() { + CredentialShareServiceImpl testService = new CredentialShareServiceImpl( + restApiClient, webSubSubscriptionHelper, dataShareUtil, + derivedKeyCryptoUtil, p12KeyStoreManager, util, auditLogRequestBuilder, + utilities, restClientService, env, pb + ); + ReflectionTestUtils.setField(testService, "util", util); + return testService; + } + + @Test + public void testExtractFaceImageDataWithQualityBlocksAndLandmarkPoints() throws Exception { + byte[] expectedImage = "FAKE_JPEG_DATA".getBytes(); + byte[] faceRecord = buildValidFaceRecord(expectedImage); + + CredentialShareServiceImpl testService = buildTestService(); + + byte[] result = ReflectionTestUtils.invokeMethod(testService, "extractFaceImageData", faceRecord); + + assertNotNull(result); + assertArrayEquals(expectedImage, result); + } + + @Test + public void testExtractFaceImageDataWithNoQualityBlocksAndNoLandmarkPoints() throws Exception { + byte[] expectedImage = "FAKE_JPEG_NO_Q_L".getBytes(); + byte[] faceRecord = buildValidFaceRecordNoQualityNoLandmarks(expectedImage); + + CredentialShareServiceImpl testService = buildTestService(); + + byte[] result = ReflectionTestUtils.invokeMethod(testService, "extractFaceImageData", faceRecord); + + assertNotNull(result); + assertArrayEquals(expectedImage, result); + } + + @SuppressWarnings("unchecked") + private T[] invokeMapJsonNodeToJavaObject(Class type, + org.json.simple.JSONArray array) throws Exception { + java.lang.reflect.Method m = CredentialShareServiceImpl.class.getDeclaredMethod( + "mapJsonNodeToJavaObject", Class.class, org.json.simple.JSONArray.class); + m.setAccessible(true); + return (T[]) m.invoke(service, type, array); + } + + @Test + public void testMapJsonNodeToJavaObject_happyPath_withLinkedHashMapEntries() throws Exception { + ReflectionTestUtils.setField(service, "supportedLang", "eng,ara"); + + java.util.LinkedHashMap engMap = new java.util.LinkedHashMap<>(); + engMap.put("language", "eng"); + engMap.put("value", "John"); + + java.util.LinkedHashMap fraMap = new java.util.LinkedHashMap<>(); + fraMap.put("language", "fra"); + fraMap.put("value", "Jean"); + + org.json.simple.JSONArray node = new org.json.simple.JSONArray(); + node.add(engMap); + node.add(fraMap); + + io.mosip.mimoto.dto.JsonValue[] result = + invokeMapJsonNodeToJavaObject(io.mosip.mimoto.dto.JsonValue.class, node); + + assertNotNull(result); + assertEquals(2, result.length); + assertEquals("eng", ReflectionTestUtils.getField(result[0], "language")); + assertEquals("John", ReflectionTestUtils.getField(result[0], "value")); + assertEquals("fra", ReflectionTestUtils.getField(result[1], "language")); + assertEquals("Jean", ReflectionTestUtils.getField(result[1], "value")); + } + + @Test + public void testMapJsonNodeToJavaObject_nullObjectInArray() throws Exception { + org.json.simple.JSONArray node = new org.json.simple.JSONArray(); + node.add(null); + + io.mosip.mimoto.dto.JsonValue[] result = + invokeMapJsonNodeToJavaObject(io.mosip.mimoto.dto.JsonValue.class, node); + + assertNotNull(result); + assertEquals(1, result.length); + assertNull(result[0]); + } + + @Test + public void testMapJsonNodeToJavaObject_throwsInstantanceCreationException() throws Exception { + java.util.LinkedHashMap entry = new java.util.LinkedHashMap<>(); + entry.put("language", "eng"); + entry.put("value", "test"); + + org.json.simple.JSONArray node = new org.json.simple.JSONArray(); + node.add(entry); + + java.lang.reflect.Method m = CredentialShareServiceImpl.class.getDeclaredMethod( + "mapJsonNodeToJavaObject", Class.class, org.json.simple.JSONArray.class); + m.setAccessible(true); + + try { + m.invoke(service, Integer.class, node); + fail("Expected InstantanceCreationException wrapped in InvocationTargetException"); + } catch (java.lang.reflect.InvocationTargetException e) { + assertTrue( + "Expected InstantanceCreationException but got: " + e.getCause(), + e.getCause() instanceof io.mosip.mimoto.exception.InstantanceCreationException); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testMapJsonNodeToJavaObject_throwsFieldNotFoundException_onSecurityException() throws Exception { + org.json.simple.JSONArray securityThrowingArray = new org.json.simple.JSONArray() { + @Override + public int size() { + return 1; + } + + @Override + public Object get(int index) { + throw new SecurityException("simulated security restriction"); + } + }; + + java.lang.reflect.Method m = CredentialShareServiceImpl.class.getDeclaredMethod( + "mapJsonNodeToJavaObject", Class.class, org.json.simple.JSONArray.class); + m.setAccessible(true); + + try { + m.invoke(service, io.mosip.mimoto.dto.JsonValue.class, securityThrowingArray); + fail("Expected FieldNotFoundException wrapped in InvocationTargetException"); + } catch (java.lang.reflect.InvocationTargetException e) { + assertTrue( + "Expected FieldNotFoundException but got: " + e.getCause(), + e.getCause() instanceof io.mosip.mimoto.exception.FieldNotFoundException); + } + } +} diff --git a/src/test/java/io/mosip/mimoto/service/CredentialVerifierServiceTest.java b/src/test/java/io/mosip/mimoto/service/CredentialVerifierServiceTest.java new file mode 100644 index 000000000..16892a17a --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/CredentialVerifierServiceTest.java @@ -0,0 +1,108 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponseProof; +import io.mosip.mimoto.exception.VCVerificationException; +import io.mosip.mimoto.service.impl.CredentialVerifierServiceImpl; +import io.mosip.vercred.vcverifier.CredentialsVerifier; +import io.mosip.vercred.vcverifier.constants.CredentialFormat; +import io.mosip.vercred.vcverifier.data.VerificationResult; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Map; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class CredentialVerifierServiceTest { + + @InjectMocks + private CredentialVerifierServiceImpl credentialVerifier; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private CredentialsVerifier credentialsVerifier; + + @Mock + private VerificationResult verificationResult; + + private VCCredentialResponse vcCredentialResponse; + + private final String credentialJson = "{\"credential\":\"test\"}"; + + @Before + public void setUp() throws Exception { + VCCredentialProperties credentialProperties = VCCredentialProperties.builder() + .type(Arrays.asList("VerifiableCredential", "test")) + .credentialSubject(Map.of("name", "John Doe")) + .proof(new VCCredentialResponseProof()) + .build(); + + vcCredentialResponse = VCCredentialResponse.builder() + .format(CredentialFormat.LDP_VC.getValue()) + .credential(credentialProperties) + .build(); + + when(objectMapper.writeValueAsString(any())).thenReturn(credentialJson); + } + + @Test + public void testVerifySuccess() throws Exception { + // Arrange + when(credentialsVerifier.verify(eq(credentialJson), eq(CredentialFormat.LDP_VC))).thenReturn(verificationResult); + when(verificationResult.getVerificationStatus()).thenReturn(true); + + // Act + boolean result = credentialVerifier.verify(vcCredentialResponse); + + // Assert + assertTrue(result); + verify(credentialsVerifier).verify(credentialJson, CredentialFormat.LDP_VC); + } + + @Test(expected = VCVerificationException.class) + public void testVerifyFailureThrowsException() throws Exception { + // Arrange + when(credentialsVerifier.verify(eq(credentialJson), eq(CredentialFormat.LDP_VC))).thenReturn(verificationResult); + when(verificationResult.getVerificationStatus()).thenReturn(false); + when(verificationResult.getVerificationErrorCode()).thenReturn("ERR_CODE"); + when(verificationResult.getVerificationMessage()).thenReturn("Verification failed"); + + // Act + credentialVerifier.verify(vcCredentialResponse); + } + + @Test(expected = JsonProcessingException.class) + public void testVerifyJsonProcessingException() throws Exception { + // Arrange + when(objectMapper.writeValueAsString(any())).thenThrow(JsonProcessingException.class); + + // Act + credentialVerifier.verify(vcCredentialResponse); + } + + @Test(expected = NullPointerException.class) + public void testVerifyWithNullCredential() throws Exception { + // Arrange + VCCredentialResponse nullCredentialResponse = VCCredentialResponse.builder() + .format(CredentialFormat.LDP_VC.getValue()) + .credential(null) + .build(); + + // Act + credentialVerifier.verify(nullCredentialResponse); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/DataProtectionServiceTest.java b/src/test/java/io/mosip/mimoto/service/DataProtectionServiceTest.java new file mode 100644 index 000000000..2d2321b1c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/DataProtectionServiceTest.java @@ -0,0 +1,405 @@ +package io.mosip.mimoto.service; + +import io.mosip.kernel.cryptomanager.dto.CryptomanagerRequestDto; +import io.mosip.kernel.cryptomanager.dto.CryptomanagerResponseDto; +import io.mosip.kernel.cryptomanager.service.CryptomanagerService; +import io.mosip.kernel.core.util.CryptoUtil; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.mimoto.util.SigningKeyUtil; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.util.Arrays; +import java.util.Base64; + +import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DataProtectionServiceTest { + + @Mock + private CryptomanagerService cryptomanagerService; + + private DataProtectionService dataProtectionService; + + private final String refId = "ref123"; + private final String aad = "aad123"; + private final String salt = "salt123"; + private final String encryptedData = "encryptedData"; + private KeyPair keyPair; + + private SecretKey encryptionKey; + + + @Before + public void setUp() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException { + String appId = "MIMOTO"; + dataProtectionService = new DataProtectionService(cryptomanagerService, appId); + encryptionKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.ED25519); + } + + @Test + public void shouldEncryptDataSuccessfully() { + CryptomanagerResponseDto responseDto = new CryptomanagerResponseDto(); + responseDto.setData(encryptedData); + when(cryptomanagerService.encrypt(any(CryptomanagerRequestDto.class))).thenReturn(responseDto); + + String data = "testData"; + String result = dataProtectionService.encrypt(data, refId, aad, salt); + + assertEquals(encryptedData, result); + } + + @Test + public void shouldReturnNullIfDataToEncryptIsNull() { + String result = dataProtectionService.encrypt(null, refId, aad, salt); + + assertNull(result); + } + + @Test + public void shouldDecryptDataSuccessfully() { + CryptomanagerResponseDto responseDto = new CryptomanagerResponseDto(); + String decryptedData = "testData"; + responseDto.setData(CryptoUtil.encodeToURLSafeBase64(decryptedData.getBytes(StandardCharsets.UTF_8))); + when(cryptomanagerService.decrypt(any(CryptomanagerRequestDto.class))).thenReturn(responseDto); + + String result = dataProtectionService.decrypt(encryptedData, refId, aad, salt); + + assertEquals(decryptedData, result); + } + + @Test + public void shouldReturnNullIfDataToDecryptIsNull() { + String result = dataProtectionService.decrypt(null, refId, aad, salt); + assertNull(result); + } + + + @Test + public void shouldEncryptPrivateKeyWithAESSuccessfully() throws Exception { + SecretKey aesKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.ED25519); + + String encryptedPrivateKey = dataProtectionService.encryptWithAES(aesKey, keyPair.getPrivate().getEncoded()); + + assertNotNull(encryptedPrivateKey); + assertFalse(StringUtils.isBlank(encryptedPrivateKey)); + } + + @Test + public void testIVChangesButCiphertextRemainsSameForSameEncryptionKeyAndSecretKey() throws Exception { + String encryptedPrivateKey1 = dataProtectionService.encryptWithAES(encryptionKey, keyPair.getPrivate().getEncoded()); + String encryptedPrivateKey2 = dataProtectionService.encryptWithAES(encryptionKey, keyPair.getPrivate().getEncoded()); + + byte[] encryptedBytes1 = Base64.getDecoder().decode(encryptedPrivateKey1); + byte[] encryptedBytes2 = Base64.getDecoder().decode(encryptedPrivateKey2); + + byte[] iv1 = Arrays.copyOfRange(encryptedBytes1, 0, 12); + byte[] iv2 = Arrays.copyOfRange(encryptedBytes2, 0, 12); + + byte[] decryptedPrivateKey1Bytes = dataProtectionService.decryptWithAES(encryptionKey, encryptedPrivateKey1); + byte[] decryptedPrivateKey2Bytes = dataProtectionService.decryptWithAES(encryptionKey, encryptedPrivateKey2); + PrivateKey decryptedPrivateKey1 = DataProtectionService.bytesToPrivateKey(decryptedPrivateKey1Bytes, "ed25519"); + PrivateKey decryptedPrivateKey2 = DataProtectionService.bytesToPrivateKey(decryptedPrivateKey2Bytes,"ed25519"); + + assertFalse(Arrays.equals(iv1, iv2), "IVs should be different"); + assertEquals(decryptedPrivateKey1, decryptedPrivateKey2); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionWhenEncryptionFails() { + String data = "testData"; + String refId = "refId"; + String aad = "aad"; + String salt = "salt"; + when(cryptomanagerService.encrypt(any(CryptomanagerRequestDto.class))) + .thenThrow(new RuntimeException("Simulated encryption failure")); + + dataProtectionService.encrypt(data, refId, aad, salt); + } + + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionWhenDecryptionFails() { + String data = "encryptedData"; + String refId = "refId"; + String aad = "aad"; + String salt = "salt"; + when(cryptomanagerService.decrypt(any(CryptomanagerRequestDto.class))) + .thenThrow(new RuntimeException("Simulated decryption failure")); + + dataProtectionService.decrypt(data, refId, aad, salt); + } + + + @Test + public void shouldReturnNullIfDataToEncryptWithAESIsNull() { + SecretKey key = encryptionKey; + byte[] data = null; + String result = dataProtectionService.encryptWithAES(key, data); + assertNull(result); + } + + @Test + public void shouldReturnNullIfDataToEncryptWithAESEmpty() { + SecretKey key = encryptionKey; + byte[] data = new byte[0]; + String result = dataProtectionService.encryptWithAES(key, data); + assertNull(result); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionWhenEncryptWithAESFails() throws Exception { + SecretKey badKey = new javax.crypto.spec.SecretKeySpec(new byte[8], "AES"); + byte[] data = "test".getBytes(StandardCharsets.UTF_8); + dataProtectionService.encryptWithAES(badKey, data); + } + + + @Test + public void shouldReturnNullIfDataToDecryptWithAESIsNull() { + SecretKey key = encryptionKey; + String data = null; + byte[] result = dataProtectionService.decryptWithAES(key, data); + assertNull(result); + } + + @Test + public void shouldReturnNullIfDataToDecryptWithAESEmpty() { + SecretKey key = encryptionKey; + String data = ""; + byte[] result = dataProtectionService.decryptWithAES(key, data); + assertNull(result); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionIfEncryptedDataTooShort() { + SecretKey key = encryptionKey; + // Base64 for 5 bytes, less than NONCE_LENGTH (12) + String shortData = Base64.getEncoder().encodeToString(new byte[5]); + dataProtectionService.decryptWithAES(key, shortData); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionWhenDecryptWithAESFails() { + // Create valid encrypted data with a good key + String plainText = "test"; + String encrypted = dataProtectionService.encryptWithAES(encryptionKey, plainText.getBytes(StandardCharsets.UTF_8)); + // Use a bad key for decryption + SecretKey badKey = new javax.crypto.spec.SecretKeySpec(new byte[8], "AES"); + dataProtectionService.decryptWithAES(badKey, encrypted); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfKeyBytesNull() { + DataProtectionService.bytesToSecretKey(null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfKeyBytesEmpty() { + DataProtectionService.bytesToSecretKey(new byte[0]); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfPrivateKeyBytesNull() throws Exception { + DataProtectionService.bytesToPrivateKey(null, "RSA"); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfPrivateKeyBytesEmpty() throws Exception { + DataProtectionService.bytesToPrivateKey(new byte[0], "RSA"); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfAlgorithmNameNull() throws Exception { + DataProtectionService.bytesToPrivateKey(new byte[16], null); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfAlgorithmNameEmpty() throws Exception { + DataProtectionService.bytesToPrivateKey(new byte[16], ""); + } + + @Test(expected = Exception.class) + public void shouldThrowExceptionIfKeyConversionFails() throws Exception { + // Invalid key bytes for RSA + byte[] invalidKey = new byte[16]; + DataProtectionService.bytesToPrivateKey(invalidKey, "RSA"); + } + + + @Test(expected = EncryptionException.class) + public void shouldThrowEncryptionExceptionIfCredentialDataNull() throws EncryptionException { + dataProtectionService.encryptCredential(null, "validBase64Key"); + } + + @Test(expected = EncryptionException.class) + public void shouldThrowEncryptionExceptionIfCredentialDataEmpty() throws EncryptionException { + dataProtectionService.encryptCredential("", "validBase64Key"); + } + + @Test(expected = EncryptionException.class) + public void shouldThrowEncryptionExceptionIfWalletKeyNull() throws EncryptionException { + dataProtectionService.encryptCredential("credential", null); + } + + @Test(expected = EncryptionException.class) + public void shouldThrowEncryptionExceptionIfWalletKeyEmpty() throws EncryptionException { + dataProtectionService.encryptCredential("credential", ""); + } + + @Test(expected = EncryptionException.class) + public void shouldThrowEncryptionExceptionIfWalletKeyInvalid() throws EncryptionException { + // Not a valid Base64 string + dataProtectionService.encryptCredential("credential", "not_base64"); + } + + @Test + public void shouldEncryptCredentialSuccessfully() throws EncryptionException { + String credential = "credential"; + SecretKey walletKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + String base64WalletKey = Base64.getEncoder().encodeToString(walletKey.getEncoded()); + String encrypted = dataProtectionService.encryptCredential(credential, base64WalletKey); + assertNotNull(encrypted); + assertFalse(encrypted.isEmpty()); + } + + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfEncryptedCredentialDataNull() throws DecryptionException { + dataProtectionService.decryptCredential(null, "validBase64Key"); + } + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfEncryptedCredentialDataEmpty() throws DecryptionException { + dataProtectionService.decryptCredential("", "validBase64Key"); + } + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfWalletKeyNullForDecryption() throws DecryptionException { + dataProtectionService.decryptCredential("encrypted", null); + } + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfWalletKeyEmptyForDecryption() throws DecryptionException { + dataProtectionService.decryptCredential("encrypted", ""); + } + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfWalletKeyInvalidForDecryption() throws DecryptionException { + // Not a valid Base64 string + dataProtectionService.decryptCredential("encrypted", "not_base64"); + } + + @Test(expected = DecryptionException.class) + public void shouldThrowDecryptionExceptionIfDecryptionFails() throws DecryptionException { + // Valid Base64 key, but not matching encrypted data + SecretKey walletKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + String base64WalletKey = Base64.getEncoder().encodeToString(walletKey.getEncoded()); + dataProtectionService.decryptCredential("invalidEncryptedData", base64WalletKey); + } + + @Test + public void shouldDecryptCredentialSuccessfully() throws Exception { + String credential = "credential"; + SecretKey walletKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + String base64WalletKey = Base64.getEncoder().encodeToString(walletKey.getEncoded()); + + String encrypted = dataProtectionService.encryptCredential(credential, base64WalletKey); + String decrypted = dataProtectionService.decryptCredential(encrypted, base64WalletKey); + + assertNotNull(decrypted); + assertEquals(credential, decrypted); + } + + + @Test + public void shouldReturnNullWhenStringToBytesInputIsNull() throws Exception { + // Use reflection to access private static method + java.lang.reflect.Method method = DataProtectionService.class.getDeclaredMethod("stringToBytes", String.class); + method.setAccessible(true); + assertNull(method.invoke(null, (Object) null)); + } + + @Test + public void shouldReturnUtf8BytesWhenStringToBytesInputIsNotNull() throws Exception { + java.lang.reflect.Method method = DataProtectionService.class.getDeclaredMethod("stringToBytes", String.class); + method.setAccessible(true); + String input = "hello"; + byte[] result = (byte[]) method.invoke(null, input); + assertArrayEquals(input.getBytes(StandardCharsets.UTF_8), result); + } + + @Test + public void shouldReturnNullWhenBytesToStringInputIsNull() throws Exception { + java.lang.reflect.Method method = DataProtectionService.class.getDeclaredMethod("bytesToString", byte[].class); + method.setAccessible(true); + assertNull(method.invoke(null, (Object) null)); + } + + @Test + public void shouldReturnStringWhenBytesToStringInputIsNotNull() throws Exception { + java.lang.reflect.Method method = DataProtectionService.class.getDeclaredMethod("bytesToString", byte[].class); + method.setAccessible(true); + String input = "world"; + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + String result = (String) method.invoke(null, (Object) bytes); + assertEquals(input, result); + } + + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfHeaderJsonNull() { + dataProtectionService.createDetachedJwtSigningInput(null, "payload"); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowIllegalArgumentExceptionIfPayloadBase64UrlNull() { + dataProtectionService.createDetachedJwtSigningInput("{}", null); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowRuntimeExceptionIfPayloadBase64UrlInvalid() { + // Not a valid Base64URL string + dataProtectionService.createDetachedJwtSigningInput("{}", "!!!not_base64url!!!"); + } + + @Test + public void shouldCreateDetachedJwtSigningInputSuccessfully() { + String headerJson = "{\"alg\":\"EdDSA\",\"typ\":\"JWT\"}"; + String payload = "test-payload"; + // Encode payload to Base64URL + String payloadBase64Url = java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes(StandardCharsets.UTF_8)); + byte[] result = dataProtectionService.createDetachedJwtSigningInput(headerJson, payloadBase64Url); + assertNotNull(result); + // The result should contain a '.' separator + int dotIndex = -1; + for (int i = 0; i < result.length; i++) { + if (result[i] == (byte)'.') { + dotIndex = i; + break; + } + } + assertTrue(dotIndex > 0); + } + +} diff --git a/src/test/java/io/mosip/mimoto/service/DataShareServiceTest.java b/src/test/java/io/mosip/mimoto/service/DataShareServiceTest.java new file mode 100644 index 000000000..20ea0df30 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/DataShareServiceTest.java @@ -0,0 +1,212 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.DataShareResponseDto; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.openid.datashare.DataShareResponseWrapperDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.service.impl.DataShareServiceImpl; +import io.mosip.mimoto.util.RestApiClient; +import io.mosip.mimoto.util.TestUtilities; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import static io.mosip.mimoto.util.TestUtilities.getDataShareResponseDTO; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +@RunWith(MockitoJUnitRunner.class) + +public class DataShareServiceTest { + + @Mock + RestApiClient restApiClient; + @Mock + ObjectMapper objectMapper; + + DataShareServiceImpl dataShareService; + PresentationRequestDTO presentationRequestDTO; + + private static final String TEST_HOST_URL = "https://test-url"; + private static final String TEST_CREATE_URL = "https://test-url"; + private static final String TEST_GET_URL_PATTERN = "http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/*"; + private static final String MISCONFIGURED_GET_URL_PATTERN = "http://datashare.datashare/*"; + private static final int DEFAULT_MAX_RETRY_COUNT = 1; + + @Before + public void setUp() { + presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + + dataShareService = new DataShareServiceImpl( + restApiClient, + objectMapper, + TEST_HOST_URL, + TEST_CREATE_URL, + TEST_GET_URL_PATTERN, + DEFAULT_MAX_RETRY_COUNT + ); + } + + @Test + public void storeDataInDataShareWhenProperDataIsPassed() throws Exception { + DataShareResponseWrapperDTO dataShareResponseWrapperDTO = TestUtilities.getDataShareResponseWrapperDTO(); + Mockito.when(restApiClient.postApi(Mockito.anyString(), Mockito.eq(MediaType.MULTIPART_FORM_DATA), Mockito.any(), Mockito.eq(DataShareResponseWrapperDTO.class))) + .thenReturn(dataShareResponseWrapperDTO); + String actualDataShareLink = dataShareService.storeDataInDataShare("SampleData", "3"); + String expectedDataShareLink = dataShareResponseWrapperDTO.getDataShare().getUrl(); + Assert.assertEquals(expectedDataShareLink, actualDataShareLink); + } + + @Test(expected = InvalidCredentialResourceException.class) + public void throwRequestTimedOutExceptionWhenMaxCountIsReached() throws Exception { + dataShareService = new DataShareServiceImpl(restApiClient, objectMapper, TEST_HOST_URL, TEST_CREATE_URL, TEST_GET_URL_PATTERN, 0); + dataShareService.storeDataInDataShare("SampleData", "3"); + } + + @Test(expected = InvalidCredentialResourceException.class) + public void throwServiceUnavailableExceptionWhenCredentialPushIsNotDone() throws Exception { + Mockito.when(restApiClient.postApi(Mockito.anyString(), Mockito.eq(MediaType.MULTIPART_FORM_DATA), Mockito.any(), Mockito.eq(DataShareResponseWrapperDTO.class))) + .thenThrow(InvalidCredentialResourceException.class); + dataShareService.storeDataInDataShare("SampleData", "3"); + } + + @Test + public void downloadCredentialWhenRequestIsProper() throws Exception { + VCCredentialResponse vcCredentialResponseDTO = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + String credentialString = TestUtilities.getObjectAsString(vcCredentialResponseDTO); + Mockito.when(restApiClient.getApiWithCustomHeaders(Mockito.eq("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test"), Mockito.eq(String.class), Mockito.any(HttpHeaders.class))) + .thenReturn(credentialString); + Mockito.when(objectMapper.readValue(credentialString, VCCredentialResponse.class)) + .thenReturn(vcCredentialResponseDTO); + + VCCredentialResponse actualVCCredentialResponse = dataShareService.downloadCredentialFromDataShare(presentationRequestDTO); + + Assert.assertEquals(vcCredentialResponseDTO, actualVCCredentialResponse); + } + + @Test + public void throwInvalidResourceExceptionWhenResourceURLDoesNotMatchPattern() { + presentationRequestDTO.setResource("test-resource"); + String expectedExceptionMsg = "invalid_resource --> The requested resource is invalid."; + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwInvalidResourceExceptionOnDownloadingCredentialFromDataShareFailure() { + Mockito.when(restApiClient.getApiWithCustomHeaders(Mockito.eq("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test"), Mockito.eq(String.class), Mockito.any(HttpHeaders.class))) + .thenReturn(null); + String expectedExceptionMsg = "server_unavailable --> The server is not reachable right now."; + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceExpiredExceptionWhenCredentialIsExpired() throws JsonProcessingException { + VCCredentialResponse vcCredentialResponseDTO = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + vcCredentialResponseDTO.setCredential(null); + String credentialString = TestUtilities.getObjectAsString(vcCredentialResponseDTO); + Mockito.when(restApiClient.getApiWithCustomHeaders(Mockito.eq("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test"), Mockito.eq(String.class), Mockito.any(HttpHeaders.class))) + .thenReturn(credentialString); + Mockito.when(objectMapper.readValue(credentialString, VCCredentialResponse.class)).thenReturn(vcCredentialResponseDTO); + Mockito.when(objectMapper.readValue(credentialString, DataShareResponseDto.class)).thenReturn(getDataShareResponseDTO("")); + String expectedExceptionMsg = "resource_not_found --> The requested resource expired."; + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceNotFoundExceptionWhenCredentialIsNotFoundInDataShare() throws JsonProcessingException { + VCCredentialResponse vcCredentialResponseDTO = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + vcCredentialResponseDTO.setCredential(null); + String credentialString = TestUtilities.getObjectAsString(vcCredentialResponseDTO); + Mockito.when(restApiClient.getApiWithCustomHeaders(Mockito.eq("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test"), Mockito.eq(String.class), Mockito.any(HttpHeaders.class))) + .thenReturn(credentialString); + Mockito.when(objectMapper.readValue(credentialString, VCCredentialResponse.class)).thenReturn(vcCredentialResponseDTO); + Mockito.when(objectMapper.readValue(credentialString, DataShareResponseDto.class)).thenReturn(getDataShareResponseDTO("DAT-SER-008")); + String expectedExceptionMsg = "resource_not_found --> The requested resource doesn’t exist."; + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLHasIllegalDirectoryCharacter() { + String expectedExceptionMsg = "invalid_resource --> Invalid path structure in resource URL"; + + presentationRequestDTO.setResource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/te..st"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLHasIllegalForwardSlashCharacter() { + String expectedExceptionMsg = "invalid_resource --> Invalid path structure in resource URL"; + + presentationRequestDTO.setResource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid//test"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLIsMisconfiguredAndHasNoWildcard() { + String expectedExceptionMsg = "invalid_resource --> Invalid resource identifier in URL"; + dataShareService = new DataShareServiceImpl(restApiClient, objectMapper, TEST_HOST_URL, TEST_CREATE_URL, MISCONFIGURED_GET_URL_PATTERN, DEFAULT_MAX_RETRY_COUNT); + + presentationRequestDTO.setResource("http://datashare.datashare/"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLHasIllegalCharacters() { + String expectedExceptionMsg = "invalid_resource --> Invalid characters in wildcard segment"; + presentationRequestDTO.setResource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test$"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLIsMalformed() { + String expectedExceptionMsg = "invalid_resource --> Malformed resource URL"; + presentationRequestDTO.setResource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/%%illegal"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void throwResourceInvalidRequestExceptionWhenCredentialURLHasDoubleEncodedPathTraversal() { + String expectedExceptionMsg = "invalid_resource --> Invalid characters in wildcard segment"; + presentationRequestDTO.setResource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/%252e%252e"); + + InvalidCredentialResourceException actualException = assertThrows(InvalidCredentialResourceException.class, + () -> dataShareService.downloadCredentialFromDataShare(presentationRequestDTO)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/Draft13VCDownloadHandlerTest.java b/src/test/java/io/mosip/mimoto/service/Draft13VCDownloadHandlerTest.java new file mode 100644 index 000000000..624fefcce --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/Draft13VCDownloadHandlerTest.java @@ -0,0 +1,194 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.Draft13VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VerifiableCredentialResponse; +import io.mosip.mimoto.exception.CredentialProcessingException; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.service.impl.Draft13VCDownloadHandler; +import io.mosip.mimoto.util.RestApiClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; + +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class Draft13VCDownloadHandlerTest { + + @Mock + private Draft13CredentialRequestService credentialRequestService; + + @Mock + private RestApiClient restApiClient; + + @InjectMocks + private Draft13VCDownloadHandler handler; + + private IssuerDTO issuerDTO; + private CredentialIssuerWellKnownResponse wellKnownResponse; + private TokenResponseDTO tokenResponse; + + @BeforeEach + void setUp() { + issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id("issuer-123"); + + wellKnownResponse = new CredentialIssuerWellKnownResponse(); + wellKnownResponse.setCredentialEndPoint("https://example.com/credential"); + + tokenResponse = new TokenResponseDTO(); + tokenResponse.setC_nonce("nonce-123"); + tokenResponse.setAccess_token("valid-access-token"); + } + + @Test + void shouldReturnVCCredentialResponseWhenDownloadCredentialIsSuccessful() throws Exception { + String credentialConfigurationId = "config-1"; + String walletId = "wallet-1"; + String base64Key = "base64-key"; + boolean isLoginFlow = false; + + Draft13VCCredentialRequest request = getVCCredentialRequestDTO(); + request.setFormat("jwt_vc"); + + when(credentialRequestService.buildRequest(eq(issuerDTO), eq(credentialConfigurationId), + eq(wellKnownResponse), eq(tokenResponse.getC_nonce()), eq(walletId), eq(base64Key), eq(isLoginFlow))) + .thenReturn(request); + + VerifiableCredentialResponse mockResponse = new VerifiableCredentialResponse(); + mockResponse.setCredential("mock-credential-data"); + + when(restApiClient.postApi( + eq("https://example.com/credential"), + eq(MediaType.APPLICATION_JSON), + eq(request), + eq(VerifiableCredentialResponse.class), + eq("valid-access-token") + )).thenReturn(mockResponse); + + VCCredentialResponse result = handler.downloadCredential( + issuerDTO, credentialConfigurationId, wellKnownResponse, tokenResponse, walletId, base64Key, isLoginFlow); + + assertNotNull(result); + assertEquals(request.getFormat(), result.getFormat()); + assertEquals(mockResponse.getCredential(), result.getCredential()); + } + + @Test + void shouldThrowCredentialProcessingExceptionWhenBuildRequestFails() throws Exception { + String credentialConfigurationId = "config-1"; + String walletId = "wallet-1"; + String base64Key = "base64-key"; + boolean isLoginFlow = false; + + when(credentialRequestService.buildRequest(any(), anyString(), any(), anyString(), anyString(), anyString(), anyBoolean())) + .thenThrow(new RuntimeException("Build failed")); + + assertThrows(CredentialProcessingException.class, () -> + handler.downloadCredential(issuerDTO, credentialConfigurationId, wellKnownResponse, tokenResponse, walletId, base64Key, isLoginFlow) + ); + } + + @Test + void shouldThrowExternalServiceUnavailableExceptionWhenRestApiReturnsNull() throws Exception { + String credentialConfigurationId = "config-1"; + String walletId = "wallet-1"; + String base64Key = "base64-key"; + boolean isLoginFlow = false; + + Draft13VCCredentialRequest request = getVCCredentialRequestDTO(); + request.setFormat("jwt_vc"); + + when(credentialRequestService.buildRequest(eq(issuerDTO), eq(credentialConfigurationId), + eq(wellKnownResponse), eq(tokenResponse.getC_nonce()), eq(walletId), eq(base64Key), eq(isLoginFlow))) + .thenReturn(request); + + when(restApiClient.postApi( + eq("https://example.com/credential"), + eq(MediaType.APPLICATION_JSON), + eq(request), + eq(VerifiableCredentialResponse.class), + eq("valid-access-token") + )).thenReturn(null); + + ExternalServiceUnavailableException exception = assertThrows( + ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, credentialConfigurationId, wellKnownResponse, tokenResponse, walletId, base64Key, isLoginFlow) + ); + + assertTrue(exception.getMessage().contains("Unable to download credential from issuerId")); + } + + @Test + void shouldThrowInvalidCredentialResourceExceptionWhenRestApiReturnsResponseWithNullCredential() throws Exception { + String credentialConfigurationId = "config-1"; + String walletId = "wallet-1"; + String base64Key = "base64-key"; + boolean isLoginFlow = false; + + Draft13VCCredentialRequest request = getVCCredentialRequestDTO(); + request.setFormat("jwt_vc"); + + when(credentialRequestService.buildRequest(eq(issuerDTO), eq(credentialConfigurationId), + eq(wellKnownResponse), eq(tokenResponse.getC_nonce()), eq(walletId), eq(base64Key), eq(isLoginFlow))) + .thenReturn(request); + + VerifiableCredentialResponse mockResponse = new VerifiableCredentialResponse(); + mockResponse.setCredential(null); + + when(restApiClient.postApi( + eq("https://example.com/credential"), + eq(MediaType.APPLICATION_JSON), + eq(request), + eq(VerifiableCredentialResponse.class), + eq("valid-access-token") + )).thenReturn(mockResponse); + + InvalidCredentialResourceException exception = assertThrows( + InvalidCredentialResourceException.class, + () -> handler.downloadCredential(issuerDTO, credentialConfigurationId, wellKnownResponse, tokenResponse, walletId, base64Key, isLoginFlow) + ); + + assertTrue(exception.getMessage().contains("Credential response did not contain a credential")); + } + + @Test + void shouldThrowExternalServiceUnavailableExceptionWhenRestApiThrowsException() throws Exception { + String credentialConfigurationId = "config-1"; + String walletId = "wallet-1"; + String base64Key = "base64-key"; + boolean isLoginFlow = false; + + Draft13VCCredentialRequest request = getVCCredentialRequestDTO(); + request.setFormat("jwt_vc"); + + when(credentialRequestService.buildRequest(eq(issuerDTO), eq(credentialConfigurationId), + eq(wellKnownResponse), eq(tokenResponse.getC_nonce()), eq(walletId), eq(base64Key), eq(isLoginFlow))) + .thenReturn(request); + + when(restApiClient.postApi(anyString(), any(), any(), any(), anyString())) + .thenThrow(new RuntimeException("API down")); + + ExternalServiceUnavailableException exception = assertThrows( + ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, credentialConfigurationId, wellKnownResponse, tokenResponse, walletId, base64Key, isLoginFlow) + ); + + assertTrue(exception.getMessage().contains("Unable to download credential from issuerId")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/GoogleTokenServiceTest.java b/src/test/java/io/mosip/mimoto/service/GoogleTokenServiceTest.java new file mode 100644 index 000000000..b03b26b75 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/GoogleTokenServiceTest.java @@ -0,0 +1,359 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.service.impl.GoogleTokenService; +import io.mosip.mimoto.service.impl.SecurityContextManager; +import io.mosip.mimoto.service.impl.SessionManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static io.mosip.mimoto.exception.ErrorConstants.INVALID_REQUEST; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GoogleTokenServiceTest { + + @Mock + private TokenDecoder tokenDecoder; + + @Mock + private UserMetadataService userMetadataService; + + @Mock + private SecurityContextManager securityContextManager; + + @Mock + private SessionManager sessionManager; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @InjectMocks + private GoogleTokenService googleTokenService; + + @Captor + private ArgumentCaptor userMetadataDTOArgumentCaptor; + + @Captor + private ArgumentCaptor oauth2TokenCaptor; + + private String idToken; + private String provider; + private Jwt validJwt; + private Map claims; + + @BeforeEach + void setUp() { + idToken = "dummyIdToken"; + provider = "google"; + + // Set expectedIssuer and expectedAudience via constructor injection simulation + googleTokenService = new GoogleTokenService(tokenDecoder, userMetadataService, securityContextManager, sessionManager, "google-client-id", "https://accounts.google.com"); + + claims = new HashMap<>(); + claims.put("email", "test@example.com"); + claims.put("name", "Test User"); + claims.put("picture", "http://example.com/picture.jpg"); + claims.put("sub", "google-subject-id"); + claims.put("iss", "https://accounts.google.com"); + claims.put("aud", "google-client-id"); + claims.put("exp", Instant.now().plusSeconds(3600).getEpochSecond()); + + validJwt = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claims)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + } + + @Test + void processTokenValidTokenShouldCallDependenciesAndSetSessionAndSecurityContext() throws Exception { + when(tokenDecoder.decode(idToken)).thenReturn(validJwt); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/picture.jpg", "test@example.com")).thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(tokenDecoder).decode(idToken); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/picture.jpg", "test@example.com"); + verify(sessionManager).setupSession(eq(request), eq(provider), userMetadataDTOArgumentCaptor.capture(), eq("test-user-id")); + verify(securityContextManager).setupSecurityContext(oauth2TokenCaptor.capture(), eq(request), eq(response)); + + UserMetadataDTO capturedUserMetadata = userMetadataDTOArgumentCaptor.getValue(); + assertEquals("Test User", capturedUserMetadata.getDisplayName()); + assertEquals("http://example.com/picture.jpg", capturedUserMetadata.getProfilePictureUrl()); + assertEquals("test@example.com", capturedUserMetadata.getEmail()); + + OAuth2AuthenticationToken capturedToken = oauth2TokenCaptor.getValue(); + assertEquals(provider, capturedToken.getAuthorizedClientRegistrationId()); + assertEquals("google-subject-id", capturedToken.getPrincipal().getAttribute("sub")); + } + + @Test + void processTokenInvalidIssuerShouldThrowException() throws OAuth2AuthenticationException { + Jwt invalidIssuerJwt = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claims)) + .issuer("https://invalid.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(invalidIssuerJwt); + + OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, () -> + googleTokenService.processToken(idToken, provider, request, response)); + + assertEquals("invalid_issuer", exception.getErrorCode()); + verify(tokenDecoder).decode(idToken); + verifyNoInteractions(userMetadataService, sessionManager, securityContextManager); + } + + @Test + void processTokenInvalidAudienceShouldThrowException() throws OAuth2AuthenticationException { + Jwt invalidAudienceJwt = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claims)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("invalid-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(invalidAudienceJwt); + + OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, () -> + googleTokenService.processToken(idToken, provider, request, response)); + + assertEquals("invalid_audience", exception.getErrorCode()); + verify(tokenDecoder).decode(idToken); + verifyNoInteractions(userMetadataService, sessionManager, securityContextManager); + } + + @Test + void processTokenMissingEmailClaimShouldThrowException() throws OAuth2AuthenticationException { + Map claimsWithoutEmail = new HashMap<>(claims); + claimsWithoutEmail.remove("email"); + Jwt jwtWithoutEmail = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claimsWithoutEmail)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(jwtWithoutEmail); + + OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, () -> + googleTokenService.processToken(idToken, provider, request, response)); + + assertEquals("missing_email", exception.getErrorCode()); + verify(tokenDecoder).decode(idToken); + verifyNoInteractions(userMetadataService, sessionManager, securityContextManager); + } + + @Test + void processTokenEmptyTokenShouldThrowException() throws OAuth2AuthenticationException { + // Stub tokenDecoder.decode to throw a JwtException for an empty token + when(tokenDecoder.decode("")).thenThrow(new InvalidRequestException("invalid_request","Empty token")); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + googleTokenService.processToken("", provider, request, response)); + + assertEquals(INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + verifyNoInteractions(userMetadataService, sessionManager, securityContextManager); + verify(tokenDecoder).decode(""); + } + + @Test + void processTokenInvalidTokenFormatShouldThrowException() throws OAuth2AuthenticationException { + when(tokenDecoder.decode(idToken)).thenThrow(new InvalidRequestException("invalid_token", "Invalid token format")); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + googleTokenService.processToken(idToken, provider, request, response)); + + assertEquals("invalid_token", exception.getErrorCode()); + verify(tokenDecoder).decode(idToken); + verifyNoInteractions(userMetadataService, sessionManager, securityContextManager); + } + + @Test + void processTokenWithExistingUserMetadataAndNullNameShouldUseSavedName() throws Exception { + // Test when userMetadata exists and name is null - should use saved display name + UserMetadataDTO existingMetadata = new UserMetadataDTO("Saved Name", "http://example.com/saved-pic.jpg", "test@example.com", null); + + Map claimsWithoutName = new HashMap<>(claims); + claimsWithoutName.remove("name"); + Jwt jwtWithoutName = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claimsWithoutName)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(jwtWithoutName); + when(userMetadataService.getUserMetadata("google-subject-id", provider)) + .thenReturn(createUserMetadata("Saved Name", "http://example.com/saved-pic.jpg")); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Saved Name", "http://example.com/picture.jpg", "test@example.com")) + .thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Saved Name", "http://example.com/picture.jpg", "test@example.com"); + verify(sessionManager).setupSession(eq(request), eq(provider), userMetadataDTOArgumentCaptor.capture(), eq("test-user-id")); + + UserMetadataDTO capturedUserMetadata = userMetadataDTOArgumentCaptor.getValue(); + assertEquals("Saved Name", capturedUserMetadata.getDisplayName()); + } + + @Test + void processTokenWithExistingUserMetadataAndBlankNameShouldUseSavedName() throws Exception { + // Test when userMetadata exists and name is blank - should use saved display name + Map claimsWithBlankName = new HashMap<>(claims); + claimsWithBlankName.put("name", " "); + Jwt jwtWithBlankName = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claimsWithBlankName)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(jwtWithBlankName); + when(userMetadataService.getUserMetadata("google-subject-id", provider)) + .thenReturn(createUserMetadata("Saved Display Name", "http://example.com/saved-pic.jpg")); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Saved Display Name", "http://example.com/picture.jpg", "test@example.com")) + .thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Saved Display Name", "http://example.com/picture.jpg", "test@example.com"); + } + + @Test + void processTokenWithExistingUserMetadataAndNullPictureShouldUseSavedPicture() throws Exception { + // Test when userMetadata exists and picture is null - should use saved picture URL + Map claimsWithoutPicture = new HashMap<>(claims); + claimsWithoutPicture.remove("picture"); + Jwt jwtWithoutPicture = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claimsWithoutPicture)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(jwtWithoutPicture); + when(userMetadataService.getUserMetadata("google-subject-id", provider)) + .thenReturn(createUserMetadata("Test User", "http://example.com/saved-picture.jpg")); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/saved-picture.jpg", "test@example.com")) + .thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/saved-picture.jpg", "test@example.com"); + verify(sessionManager).setupSession(eq(request), eq(provider), userMetadataDTOArgumentCaptor.capture(), eq("test-user-id")); + + UserMetadataDTO capturedUserMetadata = userMetadataDTOArgumentCaptor.getValue(); + assertEquals("http://example.com/saved-picture.jpg", capturedUserMetadata.getProfilePictureUrl()); + } + + @Test + void processTokenWithExistingUserMetadataAndBlankPictureShouldUseSavedPicture() throws Exception { + // Test when userMetadata exists and picture is blank - should use saved picture URL + Map claimsWithBlankPicture = new HashMap<>(claims); + claimsWithBlankPicture.put("picture", ""); + Jwt jwtWithBlankPicture = Jwt.withTokenValue(idToken) + .header("alg", "RS256") + .claims(claimsMap -> claimsMap.putAll(claimsWithBlankPicture)) + .issuer("https://accounts.google.com") + .audience(Collections.singletonList("google-client-id")) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + + when(tokenDecoder.decode(idToken)).thenReturn(jwtWithBlankPicture); + when(userMetadataService.getUserMetadata("google-subject-id", provider)) + .thenReturn(createUserMetadata("Test User", "http://example.com/saved-pic.jpg")); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/saved-pic.jpg", "test@example.com")) + .thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/saved-pic.jpg", "test@example.com"); + } + + @Test + void processTokenWithDecryptionExceptionShouldThrowRuntimeException() throws Exception { + // Test the DecryptionException catch block + when(tokenDecoder.decode(idToken)).thenReturn(validJwt); + when(userMetadataService.getUserMetadata("google-subject-id", provider)) + .thenThrow(new io.mosip.mimoto.exception.DecryptionException("DEC_001", "Decryption failed")); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> + googleTokenService.processToken(idToken, provider, request, response)); + + verify(tokenDecoder).decode(idToken); + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verifyNoInteractions(sessionManager, securityContextManager); + } + + @Test + void processTokenWithNullUserMetadataShouldProceedNormally() throws Exception { + // Test when userMetadata is null - should proceed with token claims + when(tokenDecoder.decode(idToken)).thenReturn(validJwt); + when(userMetadataService.getUserMetadata("google-subject-id", provider)).thenReturn(null); + when(userMetadataService.updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/picture.jpg", "test@example.com")) + .thenReturn("test-user-id"); + + googleTokenService.processToken(idToken, provider, request, response); + + verify(userMetadataService).getUserMetadata("google-subject-id", provider); + verify(userMetadataService).updateOrCreateUserMetadata("google-subject-id", provider, "Test User", "http://example.com/picture.jpg", "test@example.com"); + verify(sessionManager).setupSession(eq(request), eq(provider), any(UserMetadataDTO.class), eq("test-user-id")); + verify(securityContextManager).setupSecurityContext(any(OAuth2AuthenticationToken.class), eq(request), eq(response)); + } + + // Helper method to create UserMetadata object for testing + private UserMetadata createUserMetadata(String displayName, String pictureUrl) { + UserMetadata metadata = new UserMetadata(); + metadata.setDisplayName(displayName); + metadata.setProfilePictureUrl(pictureUrl); + return metadata; + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/IdpServiceTest.java b/src/test/java/io/mosip/mimoto/service/IdpServiceTest.java new file mode 100644 index 000000000..63d9fd455 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/IdpServiceTest.java @@ -0,0 +1,327 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.VerifiableCredentialRequestDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.AuthorizationServerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerConfiguration; +import io.mosip.mimoto.exception.IdpException; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.IssuerOnboardingException; +import io.mosip.mimoto.service.impl.IdpServiceImpl; +import io.mosip.mimoto.util.JoseUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.mosip.mimoto.util.TestUtilities.getCredentialIssuerConfigurationResponseDto; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class IdpServiceTest { + @InjectMocks + private IdpServiceImpl idpService; + + @Mock + JoseUtil joseUtil; + + @Mock + private RestTemplate restTemplate; + + @Mock + private IssuersService issuersService; + + @Mock + private CredentialIssuerConfiguration credentialIssuerConfiguration; + + @Mock + private AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse; + + @Mock + private TokenResponseDTO tokenResponseDTO; + + private Map params; + private final String authorizationAudience = "https://example.com/auth"; + + + + @Before + public void setUp() throws IOException { + params = new HashMap<>(); + params.put("issuer", "issuer123"); + params.put("code", "sampleCode"); + params.put("grant_type", "authorization_code"); + params.put("redirect_uri", "https://myapp.com/callback"); + params.put("code_verifier", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + } + + @Test + public void shouldConstructTokenRequestForTheValidIssuerAndParams() throws Exception { + IssuerDTO issuerDTO = new IssuerDTO(); + issuerDTO.setClient_id("client123"); + issuerDTO.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(issuerDTO); + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq(authorizationAudience))) + .thenReturn("jwt-token"); + + HttpEntity> httpEntity = + idpService.constructGetTokenRequest(params, "issuer123", authorizationAudience); + + HttpHeaders headers = httpEntity.getHeaders(); + MultiValueMap body = httpEntity.getBody(); + assertEquals(MediaType.APPLICATION_FORM_URLENCODED, headers.getContentType()); + assertNotNull(body); + assertAll( + () -> assertEquals("sampleCode", body.getFirst("code")), + () -> assertEquals("client123", body.getFirst("client_id")), + () -> assertEquals("authorization_code", body.getFirst("grant_type")), + () -> assertEquals("https://myapp.com/callback", body.getFirst("redirect_uri")), + () -> assertEquals("jwt-token", body.getFirst("client_assertion")), + () -> assertEquals("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", body.getFirst("code_verifier")) + ); + } + + @Test + public void shouldThrowExceptionIfThereIsAnyErrorOccurredWhileFetchingP12File() throws Exception { + IssuerDTO issuerDTO = new IssuerDTO(); + issuerDTO.setClient_id("client123"); + issuerDTO.setClient_alias("clientAlias"); + + String expectedExceptionMsg = "RESIDENT-APP-037 --> Private Key Entry is Missing for the alias clientAlias"; + when(issuersService.getIssuerDetails("issuer123")).thenReturn(issuerDTO); + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq(authorizationAudience))) + .thenThrow(new IssuerOnboardingException("Private Key Entry is Missing for the alias clientAlias")); + + IssuerOnboardingException actualException = assertThrows(IssuerOnboardingException.class, () -> + idpService.constructGetTokenRequest(params, "issuer123", authorizationAudience)); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void shouldReturnTokenEndpointFromCredentialIssuerConfigurationResponse() throws Exception { + CredentialIssuerConfiguration credentialIssuerConfiguration = + getCredentialIssuerConfigurationResponseDto("issuer1", "CredentialType1", List.of()); + String expectedTokenEndpoint = "https://dev/token"; + + when(issuersService.getIssuerConfiguration("issuer1")).thenReturn(credentialIssuerConfiguration); + + String actualTokenEndpoint = idpService.getTokenEndpoint("issuer1"); + + assertEquals(expectedTokenEndpoint, actualTokenEndpoint); + } + + @Test + public void shouldThrowExceptionIfResponseIsNullWhenFetchingTokenResponse() throws Exception { + params.put("issuer", "issuer123"); + + IssuerDTO mockIssuer = new IssuerDTO(); + mockIssuer.setClient_id("client123"); + mockIssuer.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(mockIssuer); + when(issuersService.getIssuerConfiguration("issuer123")).thenReturn(credentialIssuerConfiguration); + + when(credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()) + .thenReturn(authorizationServerWellKnownResponse); + when(authorizationServerWellKnownResponse.getTokenEndpoint()) + .thenReturn("https://example.com/token"); + + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq("https://example.com/token"))) + .thenReturn("jwt-token"); + + when(restTemplate.postForObject(eq("https://example.com/token"), any(HttpEntity.class), eq(TokenResponseDTO.class))) + .thenReturn(null); + + params.put("code_verifier", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + + IdpException ex = assertThrows(IdpException.class, () -> + idpService.getTokenResponse(params)); + + assertEquals("RESIDENT-APP-034 --> Exception occurred while performing the authorization", ex.getMessage()); + } + + @Test + public void shouldReturnTokenResponseForValidTokenEndpoint() throws Exception { + params.put("issuer", "issuer123"); + + IssuerDTO mockIssuer = new IssuerDTO(); + mockIssuer.setClient_id("client123"); + mockIssuer.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(mockIssuer); + when(issuersService.getIssuerConfiguration("issuer123")).thenReturn(credentialIssuerConfiguration); + when(credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()) + .thenReturn(authorizationServerWellKnownResponse); + when(authorizationServerWellKnownResponse.getTokenEndpoint()) + .thenReturn("https://example.com/token"); + + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq("https://example.com/token"))) + .thenReturn("jwt-token"); + + when(restTemplate.postForObject(eq("https://example.com/token"), any(HttpEntity.class), eq(TokenResponseDTO.class))) + .thenReturn(tokenResponseDTO); + + params.put("code_verifier", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + + TokenResponseDTO response = idpService.getTokenResponse(params); + + assertNotNull(response); + assertEquals(tokenResponseDTO, response); + } + + @Test + public void shouldThrowInvalidRequestExceptionOnBadRequestFromTokenEndpoint() throws Exception { + params.put("issuer", "issuer123"); + + IssuerDTO mockIssuer = new IssuerDTO(); + mockIssuer.setClient_id("client123"); + mockIssuer.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(mockIssuer); + when(issuersService.getIssuerConfiguration("issuer123")).thenReturn(credentialIssuerConfiguration); + when(credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()) + .thenReturn(authorizationServerWellKnownResponse); + when(authorizationServerWellKnownResponse.getTokenEndpoint()) + .thenReturn("https://example.com/token"); + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq("https://example.com/token"))) + .thenReturn("jwt-token"); + + HttpClientErrorException badRequestException = HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, "Bad Request", null, null, null); + + when(restTemplate.postForObject(eq("https://example.com/token"), any(HttpEntity.class), eq(TokenResponseDTO.class))) + .thenThrow(badRequestException); + + params.put("code_verifier", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + + InvalidRequestException ex = assertThrows(InvalidRequestException.class, () -> + idpService.getTokenResponse(params)); + + assertEquals("invalid_request --> Request failed due to invalid input detected by an external service.", ex.getMessage()); + } + + @Test + public void shouldReturnTokenResponseForVerifiableCredentialRequestDTO() throws Exception { + VerifiableCredentialRequestDTO requestDTO = new VerifiableCredentialRequestDTO(); + requestDTO.setCode("sampleCode"); + requestDTO.setRedirectUri("https://myapp.com/callback"); + requestDTO.setGrantType("authorization_code"); + requestDTO.setCodeVerifier("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + requestDTO.setIssuer("issuer123"); + + IssuerDTO mockIssuer = new IssuerDTO(); + mockIssuer.setClient_id("client123"); + mockIssuer.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(mockIssuer); + when(issuersService.getIssuerConfiguration("issuer123")).thenReturn(credentialIssuerConfiguration); + when(credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()) + .thenReturn(authorizationServerWellKnownResponse); + when(authorizationServerWellKnownResponse.getTokenEndpoint()) + .thenReturn("https://example.com/token"); + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq("https://example.com/token"))) + .thenReturn("jwt-token"); + when(restTemplate.postForObject(eq("https://example.com/token"), any(HttpEntity.class), eq(TokenResponseDTO.class))) + .thenReturn(tokenResponseDTO); + + TokenResponseDTO response = idpService.getTokenResponse(requestDTO); + + assertNotNull(response); + assertEquals(tokenResponseDTO, response); + } + + @Test + public void shouldPassThroughSuccessResponseAsIsForV2TokenResponse() throws Exception { + String tokenEndpoint = "https://as.example.com/token"; + String body = "{\"access_token\":\"abc\",\"token_type\":\"DPoP\"}"; + ResponseEntity asResponse = ResponseEntity.status(HttpStatus.OK).body(body); + + setupV2TokenEndpoint(tokenEndpoint); + when(restTemplate.exchange(eq(tokenEndpoint), eq(HttpMethod.POST), any(), eq(String.class))) + .thenReturn(asResponse); + + ResponseEntity result = idpService.getTokenResponseV2(params, "dpop-proof-jwt"); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals(body, result.getBody()); + } + + @Test + public void shouldPassThroughUseDpopNonceErrorWithNonceHeaderAsIsForV2TokenResponse() throws Exception { + String tokenEndpoint = "https://as.example.com/token"; + String errorBody = "{\"error\":\"use_dpop_nonce\",\"error_description\":\"nonce required\"}"; + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add("DPoP-Nonce", "server-issued-nonce-123"); + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, "Bad Request", responseHeaders, + errorBody.getBytes(), null); + + setupV2TokenEndpoint(tokenEndpoint); + when(restTemplate.exchange(eq(tokenEndpoint), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(exception); + + ResponseEntity result = idpService.getTokenResponseV2(params, "dpop-proof-jwt"); + + assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); + assertEquals(errorBody, result.getBody()); + assertEquals("server-issued-nonce-123", result.getHeaders().getFirst("DPoP-Nonce")); + } + + @Test + public void shouldPassThroughWwwAuthenticateHeaderOnUnauthorizedForV2TokenResponse() throws Exception { + String tokenEndpoint = "https://as.example.com/token"; + String errorBody = "{\"error\":\"invalid_dpop_proof\"}"; + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add("WWW-Authenticate", "DPoP error=\"invalid_dpop_proof\""); + responseHeaders.add("DPoP-Nonce", "nonce-on-401"); + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.UNAUTHORIZED, "Unauthorized", responseHeaders, + errorBody.getBytes(), null); + + setupV2TokenEndpoint(tokenEndpoint); + when(restTemplate.exchange(eq(tokenEndpoint), eq(HttpMethod.POST), any(), eq(String.class))) + .thenThrow(exception); + + ResponseEntity result = idpService.getTokenResponseV2(params, "dpop-proof-jwt"); + + assertEquals(HttpStatus.UNAUTHORIZED, result.getStatusCode()); + assertEquals(errorBody, result.getBody()); + assertEquals("DPoP error=\"invalid_dpop_proof\"", result.getHeaders().getFirst("WWW-Authenticate")); + assertEquals("nonce-on-401", result.getHeaders().getFirst("DPoP-Nonce")); + } + + private void setupV2TokenEndpoint(String tokenEndpoint) throws Exception { + IssuerDTO issuerDTO = new IssuerDTO(); + issuerDTO.setClient_id("client123"); + issuerDTO.setClient_alias("clientAlias"); + + when(issuersService.getIssuerDetails("issuer123")).thenReturn(issuerDTO); + when(issuersService.getIssuerConfiguration("issuer123")).thenReturn(credentialIssuerConfiguration); + when(credentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()) + .thenReturn(authorizationServerWellKnownResponse); + when(authorizationServerWellKnownResponse.getTokenEndpoint()) + .thenReturn(tokenEndpoint); + when(joseUtil.getJWT(eq("client123"), any(), any(), eq("clientAlias"), any(), eq(tokenEndpoint))) + .thenReturn("jwt-token"); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java b/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java new file mode 100644 index 000000000..865ac872d --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java @@ -0,0 +1,490 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.IssuerV2DTO; +import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.IssuersV2DTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerConfiguration; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.IssuerConfig; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidIssuerIdException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.service.impl.IssuersServiceImpl; +import io.mosip.mimoto.util.IssuerConfigUtil; +import io.mosip.mimoto.util.Utilities; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class IssuersServiceTest { + + @InjectMocks + IssuersServiceImpl issuersService; + + @Mock + Utilities utilities; + + @Mock + IssuerConfigUtil issuersConfigUtil; + + @Spy + ObjectMapper objectMapper; + + String issuerWellKnownUrl, issuerId, credentialIssuerHostUrl, authServerWellknownUrl, issuersConfigJsonValue; + CredentialIssuerConfiguration expectedCredentialIssuerConfiguration; + IssuersDTO issuers = new IssuersDTO(); + + CredentialIssuerWellKnownResponse expectedCredentialIssuerWellKnownResponse; + + private final String publicUrl = "https://dev"; + private final String context = ""; + + @Before + public void setUp() throws Exception { + issuerWellKnownUrl = "https://issuer.env.net/.well-known/openid-credential-issuer"; + authServerWellknownUrl = "https://auth-server.env.net"; + issuerId = "Issuer3id"; + credentialIssuerHostUrl = "https://issuer.env.net"; + + issuers.setIssuers(List.of(getIssuerConfigDTO("Issuer3"), getIssuerConfigDTO("Issuer4"))); + issuers.getIssuers().forEach(i -> i.setToken_endpoint(null)); + issuersConfigJsonValue = new Gson().toJson(issuers); + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(issuersConfigJsonValue); + Mockito.when(objectMapper.readValue(issuersConfigJsonValue, IssuersDTO.class)).thenReturn(issuers); + + expectedCredentialIssuerWellKnownResponse = getCredentialIssuerWellKnownResponseDto(issuerId, + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + Mockito.when(issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl)) + .thenReturn(expectedCredentialIssuerWellKnownResponse); + + expectedCredentialIssuerConfiguration = getCredentialIssuerConfigurationResponseDto(issuerId, "CredentialType1", List.of()); + Mockito.when(issuersConfigUtil.getAuthServerWellknown(authServerWellknownUrl)).thenReturn(expectedCredentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()); + + issuersService = new IssuersServiceImpl(utilities, objectMapper, issuersConfigUtil, publicUrl, context); + } + + @Test + public void shouldReturnAllIssuersWhenSearchValueIsNull() throws ApiNotAccessibleException, IOException { + issuers.setIssuers(List.of(getIssuerConfigDTO("Issuer1"), getIssuerConfigDTO("Issuer2"))); + issuers.getIssuers().forEach(i -> i.setToken_endpoint(null)); + issuersConfigJsonValue = new Gson().toJson(issuers); + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(issuersConfigJsonValue); + Mockito.when(objectMapper.readValue(issuersConfigJsonValue, IssuersDTO.class)).thenReturn(issuers); + IssuersDTO expectedIssuers = new IssuersDTO(); + List issuers = new ArrayList<>(List.of(getIssuerConfigDTO("Issuer1"), getIssuerConfigDTO("Issuer2"))); + issuers.forEach(i -> i.setToken_endpoint(publicUrl + context + "/get-token/" + i.getIssuer_id())); + expectedIssuers.setIssuers(issuers); + + IssuersDTO allIssuers = issuersService.getIssuers(null); + + assertEquals(expectedIssuers, allIssuers); + } + + @Test + public void shouldReturnMatchingIssuersWhenSearchValuePatternMatchesWithIssuerName() throws ApiNotAccessibleException, IOException { + issuers.setIssuers(List.of(getIssuerConfigDTO("Issuer1"), getIssuerConfigDTO("Issuer2"))); + issuers.getIssuers().forEach(i -> i.setToken_endpoint(null)); + issuersConfigJsonValue = new Gson().toJson(issuers); + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(issuersConfigJsonValue); + Mockito.when(objectMapper.readValue(issuersConfigJsonValue, IssuersDTO.class)).thenReturn(issuers); + IssuersDTO expectedFilteredIssuers = new IssuersDTO(); + List filteredIssuersList = new ArrayList<>(List.of(getIssuerConfigDTO("Issuer1"))); + filteredIssuersList.forEach(i -> i.setToken_endpoint(publicUrl + context + "/get-token/" + i.getIssuer_id())); + expectedFilteredIssuers.setIssuers(filteredIssuersList); + + IssuersDTO filteredIssuers = issuersService.getIssuers("Issuer1"); + + assertEquals(expectedFilteredIssuers, filteredIssuers); + } + + @Test(expected = ApiNotAccessibleException.class) + public void shouldThrowApiNotAccessibleExceptionWhenIssuersJsonStringIsNullForGettingAllIssuers() throws IOException, ApiNotAccessibleException { + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + issuersService.getIssuers(null); + } + + @Test + public void shouldReturnIssuerDataAndConfigForTheIssuerIdIfExist() throws ApiNotAccessibleException, IOException, InvalidIssuerIdException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + IssuerDTO expectedIssuer = getIssuerConfigDTO("Issuer3"); + expectedIssuer.setToken_endpoint(publicUrl + context + "/get-token/" + expectedIssuer.getIssuer_id()); + + IssuerDTO issuer = issuersService.getIssuerDetails("Issuer3id"); + + assertEquals(expectedIssuer, issuer); + } + + @Test + public void shouldReturnIssuerDataAndConfigForAllIssuer() throws ApiNotAccessibleException, IOException { + IssuersDTO expectedIssuers = new IssuersDTO(); + List issuers = new ArrayList<>(List.of(getIssuerConfigDTO("Issuer3"), getIssuerConfigDTO("Issuer4"))); + issuers.forEach(i -> i.setToken_endpoint(publicUrl + context + "/get-token/" + i.getIssuer_id())); + expectedIssuers.setIssuers(issuers); + + IssuersDTO issuersDTO = issuersService.getAllIssuers(); + + assertEquals(expectedIssuers, issuersDTO); + } + + @Test(expected = InvalidIssuerIdException.class) + public void shouldThrowExceptionIfTheIssuerIdNotExists() throws ApiNotAccessibleException, IOException, InvalidIssuerIdException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + issuersService.getIssuerDetails("Issuer5id"); + } + + @Test(expected = ApiNotAccessibleException.class) + public void shouldThrowApiNotAccessibleExceptionWhenIssuersJsonStringIsNullForGettingIssuerConfig() throws IOException, ApiNotAccessibleException, InvalidIssuerIdException, AuthorizationServerWellknownResponseException, InvalidWellknownResponseException { + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + issuersService.getIssuerDetails("Issuers1id"); + } + + @Test + public void shouldReturnOnlyEnabledIssuers() throws IOException, ApiNotAccessibleException { + IssuersDTO issuers = new IssuersDTO(); + IssuerDTO enabledIssuer = getIssuerConfigDTO("Issuer1"); + IssuerDTO disabledIssuer = getIssuerConfigDTO("Issuer2"); + enabledIssuer.setToken_endpoint(null); + disabledIssuer.setToken_endpoint(null); + disabledIssuer.setEnabled("false"); + issuersConfigJsonValue = new Gson().toJson(issuers); + issuers.setIssuers(List.of(enabledIssuer, disabledIssuer)); + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(issuersConfigJsonValue); + Mockito.when(objectMapper.readValue(issuersConfigJsonValue, IssuersDTO.class)).thenReturn(issuers); + IssuersDTO expectedIssuersDTO = new IssuersDTO(); + enabledIssuer.setToken_endpoint(publicUrl + context + "/get-token/" + enabledIssuer.getIssuer_id()); + expectedIssuersDTO.setIssuers(List.of(enabledIssuer)); + + IssuersDTO actualIssuersDTO = issuersService.getIssuers(""); + + assertEquals(expectedIssuersDTO, actualIssuersDTO); + assertEquals("true", actualIssuersDTO.getIssuers().getFirst().getEnabled()); + assertEquals(1, actualIssuersDTO.getIssuers().size()); + } + + @Test + public void shouldReturnProperCredentialConfigurationsForTheRequestedIssuer() throws AuthorizationServerWellknownResponseException, ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + CredentialIssuerConfiguration actualCredentialIssuerConfiguration = issuersService.getIssuerConfiguration("Issuer3id"); + + assertEquals(expectedCredentialIssuerConfiguration, actualCredentialIssuerConfiguration); + } + + @Test + public void shouldSetAuthorizationServersToCredentialIssuerHostWhenNull() throws AuthorizationServerWellknownResponseException, ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + expectedCredentialIssuerWellKnownResponse.setAuthorizationServers(null); + Mockito.when(issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl)) + .thenReturn(expectedCredentialIssuerWellKnownResponse); + Mockito.when(issuersConfigUtil.getAuthServerWellknown(credentialIssuerHostUrl)) + .thenReturn(expectedCredentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()); + + CredentialIssuerConfiguration actual = issuersService.getIssuerConfiguration(issuerId); + + CredentialIssuerConfiguration expected = new CredentialIssuerConfiguration( + expectedCredentialIssuerWellKnownResponse.getCredentialIssuer(), + List.of(credentialIssuerHostUrl), + expectedCredentialIssuerWellKnownResponse.getCredentialEndPoint(), + expectedCredentialIssuerWellKnownResponse.getCredentialConfigurationsSupported(), + expectedCredentialIssuerConfiguration.getAuthorizationServerWellKnownResponse() + ); + assertEquals(expected, actual); + verify(issuersConfigUtil, times(1)).getAuthServerWellknown(credentialIssuerHostUrl); + verify(issuersConfigUtil, never()).getAuthServerWellknown(authServerWellknownUrl); + } + + @Test + public void shouldSetAuthorizationServersToCredentialIssuerHostWhenEmpty() throws AuthorizationServerWellknownResponseException, ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + expectedCredentialIssuerWellKnownResponse.setAuthorizationServers(Collections.emptyList()); + Mockito.when(issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl)) + .thenReturn(expectedCredentialIssuerWellKnownResponse); + Mockito.when(issuersConfigUtil.getAuthServerWellknown(credentialIssuerHostUrl)) + .thenReturn(expectedCredentialIssuerConfiguration.getAuthorizationServerWellKnownResponse()); + + CredentialIssuerConfiguration actual = issuersService.getIssuerConfiguration(issuerId); + + CredentialIssuerConfiguration expected = new CredentialIssuerConfiguration( + expectedCredentialIssuerWellKnownResponse.getCredentialIssuer(), + List.of(credentialIssuerHostUrl), + expectedCredentialIssuerWellKnownResponse.getCredentialEndPoint(), + expectedCredentialIssuerWellKnownResponse.getCredentialConfigurationsSupported(), + expectedCredentialIssuerConfiguration.getAuthorizationServerWellKnownResponse() + ); + assertEquals(expected, actual); + verify(issuersConfigUtil, times(1)).getAuthServerWellknown(credentialIssuerHostUrl); + verify(issuersConfigUtil, never()).getAuthServerWellknown(authServerWellknownUrl); + } + + @Test + public void issuersConfigShouldThrowExceptionIfAnyErrorOccurredWhileFetchingIssuersWellknown() { + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + ApiNotAccessibleException actualException = assertThrows(ApiNotAccessibleException.class, () -> issuersService.getIssuerConfiguration(issuerId)); + + assertEquals("RESIDENT-APP-026 --> Api not accessible failure", actualException.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + + @Test + public void issuersConfigShouldThrowExceptionIfAnyErrorOccurredWhileFetchingIssuersAuthorizationServerWellknown() throws AuthorizationServerWellknownResponseException { + Mockito.when(issuersConfigUtil.getAuthServerWellknown(authServerWellknownUrl)).thenThrow(new AuthorizationServerWellknownResponseException("well-known api is not accessible")); + + AuthorizationServerWellknownResponseException actualException = assertThrows(AuthorizationServerWellknownResponseException.class, () -> issuersService.getIssuerConfiguration("Issuer3id")); + + assertEquals("RESIDENT-APP-042 --> Invalid Authorization Server well-known from server:\n" + + "well-known api is not accessible", actualException.getMessage()); + verify(issuersConfigUtil, times(1)).getAuthServerWellknown(authServerWellknownUrl); + } + + // Existing imports and class setup remain unchanged +// Add these test cases to the existing IssuersServiceTest class + + @Test + public void shouldReturnIssuerConfigForValidIssuerIdAndCredentialType() throws ApiNotAccessibleException, IOException, InvalidIssuerIdException, InvalidWellknownResponseException { + // Arrange + String issuerId = "Issuer3id"; + String credentialType = "CredentialType1"; + IssuerDTO expectedIssuerDTO = getIssuerConfigDTO("Issuer3"); + expectedIssuerDTO.setToken_endpoint(publicUrl + context + "/get-token/" + expectedIssuerDTO.getIssuer_id()); + CredentialIssuerWellKnownResponse wellKnownResponse = getCredentialIssuerWellKnownResponseDto( + issuerId, Map.of(credentialType, getCredentialSupportedResponse(credentialType))); + IssuerConfig expectedIssuerConfig = new IssuerConfig( + expectedIssuerDTO, + wellKnownResponse, + wellKnownResponse.getCredentialConfigurationsSupported().get(credentialType) + ); + + // Act + IssuerConfig actualIssuerConfig = issuersService.getIssuerConfig(issuerId, credentialType); + + // Assert + assertEquals(expectedIssuerConfig, actualIssuerConfig); + assertEquals(expectedIssuerDTO, actualIssuerConfig.getIssuerDTO()); + assertEquals(wellKnownResponse, actualIssuerConfig.getWellKnownResponse()); + assertEquals(wellKnownResponse.getCredentialConfigurationsSupported().get(credentialType), + actualIssuerConfig.getCredentialsSupportedResponse()); + verify(issuersConfigUtil, times(1)).getIssuerWellknown(credentialIssuerHostUrl); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldThrowInvalidIssuerIdExceptionForNonExistentIssuerId() throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + // Arrange + String issuerId = "InvalidIssuerId"; + String credentialType = "CredentialType1"; + + // Act & Assert + InvalidIssuerIdException exception = assertThrows(InvalidIssuerIdException.class, + () -> issuersService.getIssuerConfig(issuerId, credentialType)); + + assertEquals("RESIDENT-APP-035 --> Invalid issuer ID", exception.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + verify(issuersConfigUtil, never()).getIssuerWellknown(anyString()); + } + + @Test + public void shouldThrowApiNotAccessibleExceptionWhenIssuersConfigJsonIsNull() throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + // Arrange + String issuerId = "Issuer3id"; + String credentialType = "CredentialType1"; + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + // Act & Assert + ApiNotAccessibleException exception = assertThrows(ApiNotAccessibleException.class, + () -> issuersService.getIssuerConfig(issuerId, credentialType)); + + assertEquals("RESIDENT-APP-026 --> Unable to fetch issuer configuration for issuerId: Issuer3id; \n" + + "nested exception is io.mosip.mimoto.exception.ApiNotAccessibleException: RESIDENT-APP-026 --> Api not accessible failure", exception.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + verify(issuersConfigUtil, never()).getIssuerWellknown(anyString()); + } + + @Test + public void shouldThrowApiNotAccessibleExceptionWhenGetIssuerWellknownFails() throws IOException, InvalidWellknownResponseException, ApiNotAccessibleException { + // Arrange + String issuerId = "Issuer3id"; + String credentialType = "CredentialType1"; + Mockito.when(issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl)) + .thenThrow(new ApiNotAccessibleException("Well-known endpoint inaccessible")); + + // Act & Assert + ApiNotAccessibleException exception = assertThrows(ApiNotAccessibleException.class, + () -> issuersService.getIssuerConfig(issuerId, credentialType)); + + assertEquals("RESIDENT-APP-026 --> Unable to fetch issuer configuration for issuerId: Issuer3id; \n" + + "nested exception is io.mosip.mimoto.exception.ApiNotAccessibleException: RESIDENT-APP-026 --> Well-known endpoint inaccessible", exception.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + verify(issuersConfigUtil, times(1)).getIssuerWellknown(credentialIssuerHostUrl); + } + + @Test + public void shouldLogErrorWhenApiNotAccessibleExceptionOccurs() throws IOException, InvalidWellknownResponseException, ApiNotAccessibleException { + // Arrange + String issuerId = "Issuer3id"; + String credentialType = "CredentialType1"; + ApiNotAccessibleException apiException = new ApiNotAccessibleException("Well-known endpoint inaccessible"); + Mockito.when(issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl)).thenThrow(apiException); + + // Act & Assert + ApiNotAccessibleException exception = assertThrows(ApiNotAccessibleException.class, + () -> issuersService.getIssuerConfig(issuerId, credentialType)); + + assertEquals("RESIDENT-APP-026 --> Unable to fetch issuer configuration for issuerId: Issuer3id; \n" + + "nested exception is io.mosip.mimoto.exception.ApiNotAccessibleException: RESIDENT-APP-026 --> Well-known endpoint inaccessible", exception.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + verify(issuersConfigUtil, times(1)).getIssuerWellknown(credentialIssuerHostUrl); + // Note: Logging verification requires a logging framework setup (e.g., Logback with ListAppender) + } + + + @Test + public void shouldReturnIssuersV2DTOWhenGetIssuersV2DTO() throws Exception { + IssuersV2DTO result = issuersService.getIssuersV2DTO(); + + assertEquals(2, result.getIssuers().size()); + IssuerV2DTO first = result.getIssuers().getFirst(); + assertEquals("Issuer3id", first.getIssuerId()); + assertEquals("OpenId4VCI", first.getProtocol()); + assertEquals(getIssuerConfigDTO("Issuer3").getDisplay(), first.getDisplay()); + assertEquals("123", first.getClientId()); + assertEquals(publicUrl + context + "/get-token/Issuer3id", first.getTokenEndpoint()); + assertEquals("test-client-alias", first.getClientAlias()); + assertEquals(getIssuerConfigDTO("Issuer3").getQr_code_type(), first.getQrCodeType()); + assertEquals("true", first.getEnabled()); + assertEquals("https://issuer.env.net", first.getCredentialIssuerHost()); + IssuerV2DTO second = result.getIssuers().get(1); + assertEquals("Issuer4id", second.getIssuerId()); + assertEquals("https://issuer.env.net", second.getCredentialIssuerHost()); + verify(utilities, atLeast(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldReturnIssuerV2DetailsForValidIssuerId() throws Exception { + IssuerV2DTO result = issuersService.getIssuerV2Details("Issuer3id"); + + assertEquals("Issuer3id", result.getIssuerId()); + assertEquals("OpenId4VCI", result.getProtocol()); + assertEquals(getIssuerConfigDTO("Issuer3").getDisplay(), result.getDisplay()); + assertEquals("123", result.getClientId()); + assertEquals(publicUrl + context + "/get-token/Issuer3id", result.getTokenEndpoint()); + assertEquals("test-client-alias", result.getClientAlias()); + assertEquals(getIssuerConfigDTO("Issuer3").getQr_code_type(), result.getQrCodeType()); + assertEquals("true", result.getEnabled()); + assertEquals("https://issuer.env.net", result.getCredentialIssuerHost()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldThrowInvalidIssuerIdWhenGetIssuerV2DetailsForInvalidId() { + InvalidIssuerIdException exception = assertThrows(InvalidIssuerIdException.class, + () -> issuersService.getIssuerV2Details("NonExistentId")); + + assertEquals("RESIDENT-APP-035 --> Invalid issuer ID", exception.getMessage()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldPropagateApiNotAccessibleWhenGetIssuersV2DTOFails() { + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + assertThrows(ApiNotAccessibleException.class, () -> issuersService.getIssuersV2DTO()); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldPropagateApiNotAccessibleWhenGetIssuerV2DetailsFails() { + Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); + + assertThrows(ApiNotAccessibleException.class, () -> issuersService.getIssuerV2Details("Issuer3id")); + verify(utilities, times(1)).getIssuersConfigJsonValue(); + } + + @Test + public void shouldGenerateTokenEndpointWhenMissingInV1() throws Exception { + String publicUrl = "https://api.dev.mosip.net"; + String context = "/v4/mimoto"; + String getTokenPath = "/get-token/"; + IssuersServiceImpl serviceWithConfig = new IssuersServiceImpl( + utilities, objectMapper, issuersConfigUtil, publicUrl, context); + + String issuerIdMissing = "Issuer-Missing"; + String issuerIdExisting = "Issuer-Existing"; + String existingUrl = "https://external-idp.com/token"; + + IssuerDTO issuerA = getIssuerConfigDTO(issuerIdMissing); + issuerA.setIssuer_id(issuerIdMissing); + issuerA.setToken_endpoint(null); + + IssuerDTO issuerB = getIssuerConfigDTO(issuerIdExisting); + issuerB.setIssuer_id(issuerIdExisting); + issuerB.setToken_endpoint(existingUrl); + + IssuersDTO mockIssuers = new IssuersDTO(List.of(issuerA, issuerB)); + String json = objectMapper.writeValueAsString(mockIssuers); + + when(utilities.getIssuersConfigJsonValue()).thenReturn(json); + when(objectMapper.readValue(json, IssuersDTO.class)).thenReturn(mockIssuers); + + IssuersDTO result = serviceWithConfig.getAllIssuers(); + + String expectedGeneratedUrlIssuerA = publicUrl + context + getTokenPath + issuerIdMissing; + + assertEquals(expectedGeneratedUrlIssuerA, result.getIssuers().get(0).getToken_endpoint()); + assertEquals(existingUrl, result.getIssuers().get(1).getToken_endpoint()); + } + + @Test + public void shouldGenerateTokenEndpointWhenMissingInV2() throws Exception { + // Arrange + String publicUrl = "https://api.dev.mosip.net"; + String context = "/v4/mimoto"; + String getTokenPath = "/get-token/"; + + IssuersServiceImpl serviceWithConfig = new IssuersServiceImpl( + utilities, objectMapper, issuersConfigUtil, publicUrl, context); + + String issuerIdMissing = "IssuerV2-Missing"; + String issuerIdExisting = "Issuer-Existing"; + String existingUrl = "https://external-idp.com/v2/get-token/Issuer-Existing"; + + IssuerDTO issuerA = getIssuerConfigDTO(issuerIdMissing); + issuerA.setIssuer_id(issuerIdMissing); + issuerA.setToken_endpoint(""); + + IssuerDTO issuerB = getIssuerConfigDTO(issuerIdExisting); + issuerB.setIssuer_id(issuerIdExisting); + issuerB.setToken_endpoint(existingUrl); + + IssuersDTO mockIssuers = new IssuersDTO(List.of(issuerA, issuerB)); + String json = objectMapper.writeValueAsString(mockIssuers); + + when(utilities.getIssuersConfigJsonValue()).thenReturn(json); + when(objectMapper.readValue(json, IssuersDTO.class)).thenReturn(mockIssuers); + + IssuersV2DTO result = serviceWithConfig.getIssuersV2DTO(); + + String expectedGeneratedUrlIssuerA = publicUrl + context + getTokenPath + issuerIdMissing; + + assertEquals(expectedGeneratedUrlIssuerA, result.getIssuers().get(0).getTokenEndpoint()); + assertEquals(existingUrl, result.getIssuers().get(1).getTokenEndpoint()); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/KeyPairServiceTest.java b/src/test/java/io/mosip/mimoto/service/KeyPairServiceTest.java new file mode 100644 index 000000000..de6eab327 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/KeyPairServiceTest.java @@ -0,0 +1,488 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.KeyGenerationException; +import io.mosip.mimoto.model.ProofSigningKey; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.repository.ProofSigningKeyRepository; +import io.mosip.mimoto.service.impl.KeyPairRetrievalServiceImpl; +import io.mosip.mimoto.util.SigningKeyUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.time.Instant; +import java.util.Base64; +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class KeyPairServiceTest { + + @InjectMocks + private KeyPairRetrievalServiceImpl keyPairService; + + @Mock + private ProofSigningKeyRepository proofSigningKeyRepository; + + @Mock + private DataProtectionService dataProtectionService; + + private String walletId; + private String base64EncodedWalletKey; + private ProofSigningKey proofSigningKey; + private KeyPair keyPair; + + @Before + public void setUp() throws Exception { + walletId = "test-wallet-id"; + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("Ed25519"); + keyPair = keyPairGenerator.generateKeyPair(); + + byte[] walletKeyBytes = new byte[32]; + for (int i = 0; i < 32; i++) { + walletKeyBytes[i] = (byte) i; + } + base64EncodedWalletKey = Base64.getEncoder().encodeToString(walletKeyBytes); + + Wallet wallet = new Wallet(); + wallet.setId(walletId); + + proofSigningKey = new ProofSigningKey(); + proofSigningKey.setId("key-id"); + proofSigningKey.setWallet(wallet); + proofSigningKey.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())); + proofSigningKey.setEncryptedSecretKey("encrypted-private-key"); + proofSigningKey.setCreatedAt(Instant.now()); + proofSigningKey.setUpdatedAt(Instant.now()); + } + + @Test + public void testGetKeyPairFromDBSuccessWithED25519() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + assertEquals(keyPair, result); + verify(proofSigningKeyRepository).findByWalletIdAndAlgorithm(walletId, algorithm.name()); + verify(dataProtectionService).decryptWithAES(any(SecretKey.class), eq("encrypted-private-key")); + } + } + + @Test + public void testGetKeyPairFromDBSuccessWithRS256() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.RS256; + + KeyPairGenerator rsaKeyGen = KeyPairGenerator.getInstance("RSA"); + rsaKeyGen.initialize(2048); + KeyPair rsaKeyPair = rsaKeyGen.generateKeyPair(); + + proofSigningKey.setPublicKey(Base64.getEncoder().encodeToString(rsaKeyPair.getPublic().getEncoded())); + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = rsaKeyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(rsaKeyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + assertEquals(rsaKeyPair, result); + } + } + + @Test + public void testGetKeyPairFromDBSuccessWithES256() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ES256; + + KeyPairGenerator ecKeyGen = KeyPairGenerator.getInstance("EC"); + ecKeyGen.initialize(256); + KeyPair ecKeyPair = ecKeyGen.generateKeyPair(); + + proofSigningKey.setPublicKey(Base64.getEncoder().encodeToString(ecKeyPair.getPublic().getEncoded())); + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = ecKeyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(ecKeyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + assertEquals(ecKeyPair, result); + } + } + + @Test(expected = KeyGenerationException.class) + public void testGetKeyPairFromDBThrowsKeyGenerationExceptionWhenKeyNotFound() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.empty()); + + try { + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + } catch (KeyGenerationException e) { + assertEquals("KEY_NOT_FOUND", e.getErrorCode()); + assertTrue(e.getErrorText().contains("No proof signing key found")); + assertTrue(e.getErrorText().contains(walletId)); + assertTrue(e.getErrorText().contains(algorithm.toString())); + throw e; + } + } + + @Test + public void testGetKeyPairFromDBThrowsDecryptionExceptionForInvalidBase64WalletKey() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + String invalidBase64Key = "Invalid!@#$%Base64"; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + try { + keyPairService.getKeyPairFromDB(walletId, invalidBase64Key, algorithm); + fail("Should have thrown DecryptionException"); + } catch (DecryptionException e) { + assertEquals("INVALID_WALLET_KEY", e.getErrorCode()); + assertTrue(e.getErrorText().contains("Invalid base64 encoded wallet key")); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof IllegalArgumentException); + } + } + + @Test(expected = KeyGenerationException.class) + public void testGetKeyPairFromDBThrowsKeyGenerationExceptionForInvalidBase64PublicKey() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + proofSigningKey.setPublicKey("Invalid!@#$%Base64PublicKey"); + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class)) { + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + try { + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + } catch (KeyGenerationException e) { + assertEquals("INVALID_PUBLIC_KEY", e.getErrorCode()); + assertTrue(e.getErrorText().contains("Invalid base64 encoded public key")); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof IllegalArgumentException); + throw e; + } + } + } + + @Test(expected = DecryptionException.class) + public void testGetKeyPairFromDBThrowsDecryptionExceptionWhenDecryptionFails() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenThrow(new RuntimeException("Decryption error")); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class)) { + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + try { + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + } catch (DecryptionException e) { + assertEquals("DECRYPTION_FAILED", e.getErrorCode()); + assertTrue(e.getErrorText().contains("Failed to decrypt private key")); + assertTrue(e.getErrorText().contains(walletId)); + assertNotNull(e.getCause()); + throw e; + } + } + } + + @Test(expected = KeyGenerationException.class) + public void testGetKeyPairFromDBThrowsKeyGenerationExceptionWhenKeyPairGenerationFails() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenThrow(new RuntimeException("Key generation error")); + + try { + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + } catch (KeyGenerationException e) { + assertEquals("KEY_GENERATION_FAILED", e.getErrorCode()); + assertTrue(e.getErrorText().contains("Failed to generate KeyPair")); + assertTrue(e.getErrorText().contains(algorithm.toString())); + assertNotNull(e.getCause()); + throw e; + } + } + } + + @Test + public void testGetKeyPairFromDBWithDifferentWalletIds() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + String wallet1 = "wallet-1"; + String wallet2 = "wallet-2"; + + ProofSigningKey key1 = new ProofSigningKey(); + key1.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())); + key1.setEncryptedSecretKey("encrypted-key-1"); + + ProofSigningKey key2 = new ProofSigningKey(); + key2.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())); + key2.setEncryptedSecretKey("encrypted-key-2"); + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(wallet1, algorithm.name())) + .thenReturn(Optional.of(key1)); + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(wallet2, algorithm.name())) + .thenReturn(Optional.of(key2)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), anyString())) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + KeyPair result1 = keyPairService.getKeyPairFromDB(wallet1, base64EncodedWalletKey, algorithm); + KeyPair result2 = keyPairService.getKeyPairFromDB(wallet2, base64EncodedWalletKey, algorithm); + + assertNotNull(result1); + assertNotNull(result2); + verify(proofSigningKeyRepository).findByWalletIdAndAlgorithm(wallet1, algorithm.name()); + verify(proofSigningKeyRepository).findByWalletIdAndAlgorithm(wallet2, algorithm.name()); + verify(dataProtectionService).decryptWithAES(any(SecretKey.class), eq("encrypted-key-1")); + verify(dataProtectionService).decryptWithAES(any(SecretKey.class), eq("encrypted-key-2")); + } + } + + @Test + public void testGetKeyPairFromDBWithAllSigningAlgorithms() throws Exception { + for (SigningAlgorithm algorithm : SigningAlgorithm.values()) { + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + } + + reset(proofSigningKeyRepository, dataProtectionService); + } + } + + @Test(expected = KeyGenerationException.class) + public void testGetKeyPairFromDBWithNullProofSigningKeyRepository() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.empty()); + + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + } + + @Test + public void testGetKeyPairFromDBVerifiesRepositoryIsCalledWithCorrectParameters() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ES256; + String testWalletId = "specific-wallet-id"; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(testWalletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + keyPairService.getKeyPairFromDB(testWalletId, base64EncodedWalletKey, algorithm); + + verify(proofSigningKeyRepository, times(1)).findByWalletIdAndAlgorithm(testWalletId, algorithm.name()); + } + } + + @Test + public void testGetKeyPairFromDBHandlesEmptyEncryptedSecretKey() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + proofSigningKey.setEncryptedSecretKey(""); + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq(""))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + verify(dataProtectionService).decryptWithAES(any(SecretKey.class), eq("")); + } + } + + @Test + public void testGetKeyPairFromDBThrowsDecryptionExceptionForCorruptedWalletKey() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenThrow(new RuntimeException("Corrupted key - decryption failed")); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class)) { + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + try { + keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + fail("Should have thrown DecryptionException"); + } catch (DecryptionException e) { + assertEquals("DECRYPTION_FAILED", e.getErrorCode()); + assertTrue(e.getErrorText().contains("Failed to decrypt private key")); + assertTrue(e.getErrorText().contains(walletId)); + assertNotNull(e.getCause()); + } + } + } + + @Test + public void testGetKeyPairFromDBLogsDebugMessages() throws Exception { + SigningAlgorithm algorithm = SigningAlgorithm.ED25519; + + when(proofSigningKeyRepository.findByWalletIdAndAlgorithm(walletId, algorithm.name())) + .thenReturn(Optional.of(proofSigningKey)); + + byte[] decryptedPrivateKey = keyPair.getPrivate().getEncoded(); + when(dataProtectionService.decryptWithAES(any(SecretKey.class), eq("encrypted-private-key"))) + .thenReturn(decryptedPrivateKey); + + try (MockedStatic mockedStaticUtil = Mockito.mockStatic(DataProtectionService.class); + MockedStatic mockedStaticKeyGen = Mockito.mockStatic(SigningKeyUtil.class)) { + + mockedStaticUtil.when(() -> DataProtectionService.bytesToSecretKey(any(byte[].class))) + .thenReturn(mock(SecretKey.class)); + + mockedStaticKeyGen.when(() -> SigningKeyUtil.generateKeyPair( + eq(algorithm), any(byte[].class), any(byte[].class))) + .thenReturn(keyPair); + + KeyPair result = keyPairService.getKeyPairFromDB(walletId, base64EncodedWalletKey, algorithm); + + assertNotNull(result); + } + } +} + diff --git a/src/test/java/io/mosip/mimoto/service/LdpVcCredentialFormatHandlerTest.java b/src/test/java/io/mosip/mimoto/service/LdpVcCredentialFormatHandlerTest.java new file mode 100644 index 000000000..3ce19076c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/LdpVcCredentialFormatHandlerTest.java @@ -0,0 +1,559 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.service.impl.LdpVcCredentialFormatHandler; +import io.mosip.mimoto.util.LocaleUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LdpVcCredentialFormatHandlerTest { + + @InjectMocks + private LdpVcCredentialFormatHandler ldpVcCredentialFormatHandler; + + private VCCredentialResponse vcCredentialResponse; + private VCCredentialProperties vcCredentialProperties; + private CredentialsSupportedResponse credentialsSupportedResponse; + private CredentialDefinitionResponseDto credentialDefinition; + @Spy + private ObjectMapper objectMapper; + + + @BeforeEach + void setUp() { + vcCredentialResponse = new VCCredentialResponse(); + vcCredentialProperties = new VCCredentialProperties(); + credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialDefinition = new CredentialDefinitionResponseDto(); + } + + @Test + void extractCredentialClaimsWithValidCredentialShouldReturnCredentialSubject() { + // Given + Map credentialSubject = new HashMap<>(); + credentialSubject.put("firstName", "John"); + credentialSubject.put("lastName", "Doe"); + credentialSubject.put("dateOfBirth", "1990-01-01"); + + vcCredentialProperties.setCredentialSubject(credentialSubject); + vcCredentialResponse.setCredential(vcCredentialProperties); + + // When + Map result = ldpVcCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // Then + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("John", result.get("firstName")); + assertEquals("Doe", result.get("lastName")); + assertEquals("1990-01-01", result.get("dateOfBirth")); + } + + @Test + void extractCredentialClaimsWithNullCredentialSubjectShouldReturnNull() { + // Given + vcCredentialProperties.setCredentialSubject(null); + vcCredentialResponse.setCredential(vcCredentialProperties); + + // When + Map result = ldpVcCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // Then + assertNull(result); + } + + @Test + void extractCredentialClaimsWithEmptyCredentialSubjectShouldReturnEmptyMap() { + // Given + Map credentialSubject = new HashMap<>(); + vcCredentialProperties.setCredentialSubject(credentialSubject); + vcCredentialResponse.setCredential(vcCredentialProperties); + + // When + Map result = ldpVcCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void loadDisplayPropertiesFromWellknownWithValidDataShouldReturnDisplayProperties() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + credentialsSupportedResponse.setOrder(Arrays.asList("firstName", "lastName")); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("firstName")); + assertTrue(result.containsKey("lastName")); + + Map firstNameMap = result.get("firstName"); + assertNotNull(firstNameMap); + assertEquals(1, firstNameMap.size()); + assertEquals("John", firstNameMap.values().iterator().next()); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullCredentialDefinitionShouldReturnEmptyMap() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + + credentialsSupportedResponse.setCredentialDefinition(null); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then - now returns fallback for firstName + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + + CredentialIssuerDisplayResponse display = result.get("firstName").keySet().iterator().next(); + assertEquals("First Name", display.getName()); + assertEquals("en", display.getLocale()); + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullCredentialSubjectShouldReturnEmptyMap() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + + credentialDefinition.setCredentialSubject(null); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then - now returns fallback for firstName + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + + CredentialIssuerDisplayResponse display = result.get("firstName").keySet().iterator().next(); + assertEquals("First Name", display.getName()); + assertEquals("en", display.getLocale()); + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullDisplayConfigMapShouldReturnEmptyMap() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + + credentialDefinition.setCredentialSubject(null); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then - now returns fallback for firstName + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + + CredentialIssuerDisplayResponse display = result.get("firstName").keySet().iterator().next(); + assertEquals("First Name", display.getName()); + assertEquals("en", display.getLocale()); + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullResolvedLocaleShouldReturnEmptyMap() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("fr"))) + .thenReturn(null); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "fr"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); // Should generate fallback for firstName + assertTrue(result.containsKey("firstName")); + + // Verify fallback display properties with default "en" locale + CredentialIssuerDisplayResponse display = result.get("firstName").keySet().iterator().next(); + assertEquals("First Name", display.getName()); + assertEquals("en", display.getLocale()); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithCustomOrderShouldRespectOrder() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + credentialsSupportedResponse.setOrder(Arrays.asList("lastName", "firstName")); // Custom order + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + List keyOrder = new ArrayList<>(result.keySet()); + assertEquals("lastName", keyOrder.get(0)); + assertEquals("firstName", keyOrder.get(1)); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithEmptyOrderShouldUseNaturalOrder() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + credentialsSupportedResponse.setOrder(new ArrayList<>()); // Empty order + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("firstName")); + assertTrue(result.containsKey("lastName")); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullValuesShouldSkipNullValues() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", null); // Null value + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); // Only firstName should be included + assertTrue(result.containsKey("firstName")); + assertFalse(result.containsKey("lastName")); + } + } + + @Test + void getSupportedFormatShouldReturnLdpVcFormat() { + // When + String result = ldpVcCredentialFormatHandler.getSupportedFormat(); + + // Then + assertEquals(CredentialFormat.LDP_VC.getFormat(), result); + } + + @Test + void extractAllCredentialPropertiesWithMapCredentialReturnsLinkedHashMap() { + // Arrange + Map credentialMap = new LinkedHashMap<>(); + credentialMap.put("type", Arrays.asList("VerifiableCredential")); + credentialMap.put("credentialSubject", Map.of("name", "John")); + vcCredentialResponse.setCredential(credentialMap); + + // Act + Map result = ldpVcCredentialFormatHandler.extractAllCredentialProperties(vcCredentialResponse); + + // Assert + assertNotNull(result); + assertTrue(result instanceof LinkedHashMap); + assertTrue(result.containsKey("type")); + assertTrue(result.containsKey("credentialSubject")); + } + + @Test + void loadDisplayPropertiesFromWellknownWhenOrderHasKeyNotInCredentialPropertiesSkipsIt() { + // Arrange - order has "extraKey" not present in credentialProperties + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + + Map credentialSubjectConfig = createCredentialSubjectConfig(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + credentialsSupportedResponse.setOrder(Arrays.asList("firstName", "nonExistentKey")); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // Act + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Assert - nonExistentKey skipped (display null for it since not in credentialProperties) + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + assertFalse(result.containsKey("nonExistentKey")); + } + } + + @Test + void buildFallbackDisplayPropertiesWhenOrderedKeysIsNullUsesCredentialPropertiesKeySet() { + // Arrange - null credentialDefinition triggers fallback; null order + Map credentialProperties = new LinkedHashMap<>(); + credentialProperties.put("name", "John"); + credentialProperties.put("age", 30); + + credentialsSupportedResponse.setCredentialDefinition(null); + credentialsSupportedResponse.setOrder(null); + + // Act + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("name")); + assertTrue(result.containsKey("age")); + } + + // Helper methods + private Map createCredentialSubjectConfig() { + Map config = new LinkedHashMap<>(); + + // Create firstName display config + CredentialDisplayResponseDto firstNameDto = new CredentialDisplayResponseDto(); + CredentialIssuerDisplayResponse firstNameDisplay = new CredentialIssuerDisplayResponse(); + firstNameDisplay.setName("First Name"); + firstNameDisplay.setLocale("en"); + firstNameDto.setDisplay(List.of(firstNameDisplay)); + config.put("firstName", firstNameDto); + + // Create lastName display config + CredentialDisplayResponseDto lastNameDto = new CredentialDisplayResponseDto(); + CredentialIssuerDisplayResponse lastNameDisplay = new CredentialIssuerDisplayResponse(); + lastNameDisplay.setName("Last Name"); + lastNameDisplay.setLocale("en"); + lastNameDto.setDisplay(List.of(lastNameDisplay)); + config.put("lastName", lastNameDto); + + return config; + } + + private Map createCredentialSubjectConfigForFullName() { + Map config = new LinkedHashMap<>(); + + // Create fullName display config only + CredentialDisplayResponseDto fullNameDto = new CredentialDisplayResponseDto(); + CredentialIssuerDisplayResponse fullNameDisplay = new CredentialIssuerDisplayResponse(); + fullNameDisplay.setName("Full Name"); + fullNameDisplay.setLocale("en"); + fullNameDto.setDisplay(List.of(fullNameDisplay)); + config.put("fullName", fullNameDto); + + return config; + } + + @Test + void loadDisplayPropertiesFromWellknownWithFullNameScenario() { + // Given - only fullName has wellknown config, others need fallbacks + Map credentialProperties = new HashMap<>(); + credentialProperties.put("fullName", "John Doe"); + credentialProperties.put("dob", "1990-01-01"); // Missing + credentialProperties.put("mobile", "123-456-7890"); // Missing + + // Only fullName has wellknown display config + Map credentialSubjectConfig = createCredentialSubjectConfigForFullName(); + credentialDefinition.setCredentialSubject(credentialSubjectConfig); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale("en", "en")) + .thenReturn(true); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(3, result.size()); // All 3 fields should be present + + // Verify wellknown field is preserved + assertTrue(result.containsKey("fullName")); + CredentialIssuerDisplayResponse fullNameDisplay = result.get("fullName").keySet().iterator().next(); + assertEquals("Full Name", fullNameDisplay.getName()); // From wellknown + + // Verify fallback fields are generated + assertTrue(result.containsKey("dob")); + assertTrue(result.containsKey("mobile")); + + CredentialIssuerDisplayResponse dobDisplay = result.get("dob").keySet().iterator().next(); + assertEquals("Dob", dobDisplay.getName()); // Generated fallback + assertEquals("en", dobDisplay.getLocale()); + + CredentialIssuerDisplayResponse mobileDisplay = result.get("mobile").keySet().iterator().next(); + assertEquals("Mobile", mobileDisplay.getName()); // Generated fallback + assertEquals("en", mobileDisplay.getLocale()); + } + } + + @Test + void buildFallbackDisplayPropertiesWithOrderedKeysShouldRespectOrder() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + credentialProperties.put("email", "john@example.com"); + + List orderedKeys = Arrays.asList("email", "lastName", "firstName"); + credentialsSupportedResponse.setOrder(orderedKeys); + credentialsSupportedResponse.setCredentialDefinition(null); // Trigger fallback + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(3, result.size()); + + // Verify order is preserved + List resultKeys = new ArrayList<>(result.keySet()); + assertEquals("email", resultKeys.get(0)); + assertEquals("lastName", resultKeys.get(1)); + assertEquals("firstName", resultKeys.get(2)); + } + + @Test + void buildFallbackDisplayPropertiesWithNullValuesShouldSkipFields() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("middleName", null); + credentialProperties.put("lastName", "Doe"); + + credentialsSupportedResponse.setCredentialDefinition(null); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); // middleName should be skipped + assertTrue(result.containsKey("firstName")); + assertTrue(result.containsKey("lastName")); + assertFalse(result.containsKey("middleName")); + } + + @Test + void buildFallbackDisplayPropertiesWithIdFieldShouldExcludeIt() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("id", "12345"); + credentialProperties.put("firstName", "John"); + + credentialsSupportedResponse.setCredentialDefinition(null); + + // When + LinkedHashMap> result = + ldpVcCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertFalse(result.containsKey("id")); // id should be excluded + assertTrue(result.containsKey("firstName")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/OpenID4VPServiceTest.java b/src/test/java/io/mosip/mimoto/service/OpenID4VPServiceTest.java new file mode 100644 index 000000000..90e95f9f9 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/OpenID4VPServiceTest.java @@ -0,0 +1,710 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.openID4VP.networkManager.NetworkResponse; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.service.impl.OpenID4VPService; +import io.mosip.openID4VP.OpenID4VP; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationDcqlRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.authorizationRequest.presentationDefinition.PresentationDefinition; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.common.OpenID4VPErrorCodes; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import io.mosip.openID4VP.verifier.VerifierResponse; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +@Slf4j +public class OpenID4VPServiceTest { + + @Mock + private VerifierService verifierService; + + @InjectMocks + private OpenID4VPService openID4VPService; + + private VerifiersDTO mockVerifiersDTO; + private VerifierDTO mockVerifierDTO; + private AuthorizationPresentationExchangeRequest mockAuthorizationRequest; + private PresentationDefinition mockPresentationDefinition; + + @Before + public void setUp() { + // Setup mock VerifierDTO with required vp_formats + mockVerifierDTO = VerifierDTO.builder() + .clientId("test-client-id") + .responseUris(List.of("https://example.com/response")) + .jwksUri("https://example.com/.well-known/jwks.json") + .allowUnsignedRequest(true) + .build(); + + // Setup mock VerifiersDTO + mockVerifiersDTO = VerifiersDTO.builder() + .verifiers(List.of(mockVerifierDTO)) + .build(); + + + // Setup mock AuthorizationRequest + mockAuthorizationRequest = mock(AuthorizationPresentationExchangeRequest.class); + + // Setup mock PresentationDefinition + mockPresentationDefinition = mock(PresentationDefinition.class); + } + + @Test + public void testCreateReturnsValidOpenID4VP() { + OpenID4VP openID4VP = openID4VPService.create("presentation-123", List.of(), true); + + assertNotNull(openID4VP); + assertEquals("io.mosip.openID4VP.OpenID4VP", openID4VP.getClass().getName()); + } + + @Test + public void testCreateWithValidPresentationIdReturnsValidOpenID4VP() { + OpenID4VP openID4VP = openID4VPService.create("valid-presentation-id", List.of(), true); + + assertNotNull(openID4VP); + assertEquals("io.mosip.openID4VP.OpenID4VP", openID4VP.getClass().getName()); + } + + @Test + public void testCreateWithSpecialCharactersPresentationIdReturnsValidOpenID4VP() { + OpenID4VP openID4VP = openID4VPService.create("presentation-123_with.special@chars", List.of(), true); + + assertNotNull(openID4VP); + assertEquals("io.mosip.openID4VP.OpenID4VP", openID4VP.getClass().getName()); + } + + @Test + public void testResolvePresentationDefinitionSuccess() throws Exception { + // Setup mocks + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + // Create a mock OpenID4VP to control the authenticateVerifier method + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())) + .thenReturn(mockAuthorizationRequest); + when(mockAuthorizationRequest.getPresentationDefinition()) + .thenReturn(mockPresentationDefinition); + + // Use reflection to replace the create method behavior + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + // Execute + PresentationDefinition result = spyService.resolvePresentationDefinition( + "presentation-123", + "authorization-request", + true + ); + + // Verify + assertNotNull(result); + assertEquals(mockPresentationDefinition, result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + verify(mockAuthorizationRequest).getPresentationDefinition(); + } + + @Test + public void testResolvePresentationDefinitionWithNullPresentationIdReturnsNull() throws Exception { + // Execute + PresentationDefinition result = openID4VPService.resolvePresentationDefinition( + null, + "authorization-request", + true + ); + + // Verify + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolvePresentationDefinitionWithNullAuthorizationRequestReturnsNull() throws Exception { + // Execute + PresentationDefinition result = openID4VPService.resolvePresentationDefinition( + "presentation-123", + null, + true + ); + + // Verify + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolvePresentationDefinitionWithBothNullParametersReturnsNull() throws Exception { + // Execute + PresentationDefinition result = openID4VPService.resolvePresentationDefinition( + null, + null, + true + ); + + // Verify + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolvePresentationDefinitionWithEmptyPresentationIdReturnsNull() throws Exception { + // Setup mocks for empty string scenario - service doesn't check for empty strings, so it continues execution + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + // Create a mock OpenID4VP to control the authenticateVerifier method + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())) + .thenReturn(mockAuthorizationRequest); + when(mockAuthorizationRequest.getPresentationDefinition()) + .thenReturn(mockPresentationDefinition); + + // Use reflection to replace the create method behavior + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + // Execute + PresentationDefinition result = spyService.resolvePresentationDefinition( + "", + "authorization-request", + true + ); + + // Verify - Empty string is not null, so it will proceed to call verifierService + assertNotNull(result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + verify(mockAuthorizationRequest).getPresentationDefinition(); + } + + @Test + public void testResolvePresentationDefinitionWithEmptyAuthorizationRequestReturnsNull() throws Exception { + // Setup mocks for empty string scenario - service doesn't check for empty strings, so it continues execution + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + // Create a mock OpenID4VP to control the authenticateVerifier method + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())) + .thenReturn(mockAuthorizationRequest); + when(mockAuthorizationRequest.getPresentationDefinition()) + .thenReturn(mockPresentationDefinition); + + // Use reflection to replace the create method behavior + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + // Execute + PresentationDefinition result = spyService.resolvePresentationDefinition( + "presentation-123", + "", + true + ); + + // Verify - Empty string is not null, so it will proceed to call verifierService + assertNotNull(result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("")); + verify(mockAuthorizationRequest).getPresentationDefinition(); + } + + @Test + public void testResolvePresentationDefinitionWithVerifierServiceExceptionThrowsException() throws Exception { + // Setup mocks + when(verifierService.getTrustedVerifiers()) + .thenThrow(new ApiNotAccessibleException()); + + // Execute and verify exception + assertThrows(ApiNotAccessibleException.class, () -> { + openID4VPService.resolvePresentationDefinition( + "presentation-123", + "authorization-request", + true + ); + }); + + verify(verifierService).getTrustedVerifiers(); + } + + @Test + public void testResolvePresentationDefinitionWithIOExceptionThrowsException() throws Exception { + // Setup mocks + when(verifierService.getTrustedVerifiers()) + .thenThrow(new IOException("Network error")); + + // Execute and verify exception + assertThrows(IOException.class, () -> { + openID4VPService.resolvePresentationDefinition( + "presentation-123", + "authorization-request", + true + ); + }); + + verify(verifierService).getTrustedVerifiers(); + } + + @Test + public void testResolvePresentationDefinitionWithEmptyVerifiersList() throws Exception { + // Setup mocks with empty verifiers list + VerifiersDTO emptyVerifiersDTO = VerifiersDTO.builder() + .verifiers(List.of()) + .build(); + when(verifierService.getTrustedVerifiers()).thenReturn(emptyVerifiersDTO); + + // Create a mock OpenID4VP to control the authenticateVerifier method + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())) + .thenReturn(mockAuthorizationRequest); + when(mockAuthorizationRequest.getPresentationDefinition()) + .thenReturn(mockPresentationDefinition); + + // Use reflection to replace the create method behavior + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + // Execute + PresentationDefinition result = spyService.resolvePresentationDefinition( + "presentation-123", + "authorization-request", + false + ); + + // Verify + assertNotNull(result); + assertEquals(mockPresentationDefinition, result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier("authorization-request"); + verify(mockAuthorizationRequest).getPresentationDefinition(); + } + + @Test + public void testResolvePresentationDefinitionWithNullPresentationDefinition() throws Exception { + // Setup mocks + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + // Create a mock OpenID4VP to control the authenticateVerifier method + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())) + .thenReturn(mockAuthorizationRequest); + when(mockAuthorizationRequest.getPresentationDefinition()) + .thenReturn(null); + + // Use reflection to replace the create method behavior + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + // Execute + PresentationDefinition result = spyService.resolvePresentationDefinition( + "presentation-123", + "authorization-request", + true + ); + + // Verify + assertNull(result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + verify(mockAuthorizationRequest).getPresentationDefinition(); + } + + @Test + public void testResolveDcqlQuerySuccess() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + DCQLQuery mockDcqlQuery = mock(DCQLQuery.class); + AuthorizationDcqlRequest mockDcqlRequest = mock(AuthorizationDcqlRequest.class); + when(mockDcqlRequest.getDcqlQuery()).thenReturn(mockDcqlQuery); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockDcqlRequest); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + DCQLQuery result = spyService.resolveDcqlQuery( + "presentation-123", "authorization-request", true); + + assertNotNull(result); + assertEquals(mockDcqlQuery, result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + verify(mockDcqlRequest).getDcqlQuery(); + } + + @Test + public void testResolveDcqlQueryWithNullPresentationIdReturnsNull() throws Exception { + DCQLQuery result = openID4VPService.resolveDcqlQuery(null, "authorization-request", true); + + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolveDcqlQueryWithNullAuthorizationRequestReturnsNull() throws Exception { + DCQLQuery result = openID4VPService.resolveDcqlQuery("presentation-123", null, true); + + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolveDcqlQueryWithBothNullParametersReturnsNull() throws Exception { + DCQLQuery result = openID4VPService.resolveDcqlQuery(null, null, true); + + assertNull(result); + verifyNoInteractions(verifierService); + } + + @Test + public void testResolveDcqlQueryReturnsNullForPresentationExchangeRequest() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + DCQLQuery result = spyService.resolveDcqlQuery( + "presentation-123", "authorization-request", true); + + assertNull(result); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + verify(mockAuthorizationRequest, never()).getPresentationDefinition(); + } + + @Test + public void testResolveDcqlQueryWithNullDcqlQuery() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + AuthorizationDcqlRequest mockDcqlRequest = mock(AuthorizationDcqlRequest.class); + when(mockDcqlRequest.getDcqlQuery()).thenReturn(null); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockDcqlRequest); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + DCQLQuery result = spyService.resolveDcqlQuery( + "presentation-123", "authorization-request", false); + + assertNull(result); + verify(mockDcqlRequest).getDcqlQuery(); + } + + @Test + public void testResolveDcqlQueryWithVerifierServiceExceptionThrowsException() throws Exception { + when(verifierService.getTrustedVerifiers()).thenThrow(new ApiNotAccessibleException()); + + assertThrows(ApiNotAccessibleException.class, () -> + openID4VPService.resolveDcqlQuery("presentation-123", "authorization-request", true)); + + verify(verifierService).getTrustedVerifiers(); + } + + @Test + public void testResolveDcqlQueryWithIOExceptionThrowsException() throws Exception { + when(verifierService.getTrustedVerifiers()).thenThrow(new IOException("Network error")); + + assertThrows(IOException.class, () -> + openID4VPService.resolveDcqlQuery("presentation-123", "authorization-request", true)); + + verify(verifierService).getTrustedVerifiers(); + } + + @Test + public void testSendErrorToVerifierSuccess() throws Exception { + // Setup mocks + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorMessage()).thenReturn("access_denied"); + + // Execute + VerifierResponse response = spyService.sendErrorToVerifier(sessionData, payload); + + // Verify + assertNotNull(response); + assertEquals(mockResponse, response); + verify(verifierService).getTrustedVerifiers(); + verify(mockOpenID4VP).authenticateVerifier(eq("authorization-request")); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(errorCaptor.capture()); + assertTrue(errorCaptor.getValue() instanceof OpenID4VPExceptions.AccessDenied); + assertEquals(OpenID4VPErrorCodes.ACCESS_DENIED, + ((OpenID4VPExceptions) errorCaptor.getValue()).getErrorCode()); + } + + @Test + public void testSendErrorToVerifierUsesInvalidTransactionDataWhenErrorCodeSet() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorCode()).thenReturn(OpenID4VPErrorCodes.INVALID_TRANSACTION_DATA); + when(payload.getErrorMessage()).thenReturn("No matching credentials"); + + VerifierResponse response = spyService.sendErrorToVerifier(sessionData, payload); + + assertNotNull(response); + assertEquals(mockResponse, response); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(captor.capture()); + assertTrue(captor.getValue() instanceof OpenID4VPExceptions.InvalidTransactionData); + assertEquals(OpenID4VPErrorCodes.INVALID_TRANSACTION_DATA, + ((OpenID4VPExceptions) captor.getValue()).getErrorCode()); + } + + @Test + public void testSendErrorToVerifierWithNullErrorPayload() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + assertThrows(IllegalArgumentException.class, () -> spyService.sendErrorToVerifier(sessionData, null)); + verify(mockOpenID4VP, never()).sendErrorInfoToVerifier(any()); + } + + @Test + public void testSendErrorToVerifierMapsBlankErrorCodeToAccessDenied() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorCode()).thenReturn(" "); + when(payload.getErrorMessage()).thenReturn("msg"); + + spyService.sendErrorToVerifier(sessionData, payload); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(captor.capture()); + assertTrue(captor.getValue() instanceof OpenID4VPExceptions.AccessDenied); + assertEquals(OpenID4VPErrorCodes.ACCESS_DENIED, + ((OpenID4VPExceptions) captor.getValue()).getErrorCode()); + } + + @Test + public void testSendErrorToVerifierMapsAccessDeniedCodeToAccessDenied() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorCode()).thenReturn(OpenID4VPErrorCodes.ACCESS_DENIED); + when(payload.getErrorMessage()).thenReturn("user denied"); + + spyService.sendErrorToVerifier(sessionData, payload); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(captor.capture()); + assertTrue(captor.getValue() instanceof OpenID4VPExceptions.AccessDenied); + assertEquals(OpenID4VPErrorCodes.ACCESS_DENIED, + ((OpenID4VPExceptions) captor.getValue()).getErrorCode()); + } + + @Test + public void testSendErrorToVerifierMapsUnknownErrorCodeToAccessDenied() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorCode()).thenReturn("invalid_request"); + when(payload.getErrorMessage()).thenReturn("bad"); + + spyService.sendErrorToVerifier(sessionData, payload); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(captor.capture()); + assertTrue(captor.getValue() instanceof OpenID4VPExceptions.AccessDenied); + assertEquals(OpenID4VPErrorCodes.ACCESS_DENIED, + ((OpenID4VPExceptions) captor.getValue()).getErrorCode()); + } + + @Test + public void testSendErrorToVerifierUsesEmptyMessageWhenErrorMessageNull() throws Exception { + when(verifierService.getTrustedVerifiers()).thenReturn(mockVerifiersDTO); + + OpenID4VP mockOpenID4VP = mock(OpenID4VP.class); + VerifierResponse mockResponse = mock(VerifierResponse.class); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(mockOpenID4VP.sendErrorInfoToVerifier(any())).thenReturn(mockResponse); + + OpenID4VPService spyService = spy(openID4VPService); + doReturn(mockOpenID4VP).when(spyService).create(anyString(), anyList(), anyBoolean()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("authorization-request"); + when(sessionData.isVerifierClientPreregistered()).thenReturn(true); + + ErrorDTO payload = mock(ErrorDTO.class); + when(payload.getErrorCode()).thenReturn(OpenID4VPErrorCodes.ACCESS_DENIED); + when(payload.getErrorMessage()).thenReturn(null); + + spyService.sendErrorToVerifier(sessionData, payload); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Exception.class); + verify(mockOpenID4VP).sendErrorInfoToVerifier(captor.capture()); + assertEquals("", captor.getValue().getMessage()); + } + + @Test + public void testSendErrorToVerifierWithNullSessionData() { + // Setup + VerifiablePresentationSessionData sessionData = null; + ErrorDTO payload = mock(ErrorDTO.class); + + // Execute and verify + assertThrows(IllegalArgumentException.class, + () -> openID4VPService.sendErrorToVerifier(sessionData, payload)); + verifyNoInteractions(verifierService); + } + + @Test + public void testSendErrorToVerifierWithNullPresentationId() { + // Setup + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn(null); + + ErrorDTO payload = mock(ErrorDTO.class); + + // Execute and verify + assertThrows(IllegalArgumentException.class, + () -> openID4VPService.sendErrorToVerifier(sessionData, payload)); + verifyNoInteractions(verifierService); + } + + @Test + public void testSendErrorToVerifierWithNullAuthorizationRequest() { + // Setup + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn(null); + + ErrorDTO payload = mock(ErrorDTO.class); + + // Execute and verify + assertThrows(IllegalArgumentException.class, + () -> openID4VPService.sendErrorToVerifier(sessionData, payload)); + verifyNoInteractions(verifierService); + } + + @Test + public void testSendErrorToVerifierPropagatesApiNotAccessibleException() throws Exception { + // Setup + when(verifierService.getTrustedVerifiers()).thenThrow(new ApiNotAccessibleException()); + + VerifiablePresentationSessionData sessionData = mock(VerifiablePresentationSessionData.class); + when(sessionData.getPresentationId()).thenReturn("presentation-123"); + when(sessionData.getAuthorizationRequest()).thenReturn("auth-request"); + // Remove this line - isVerifierClientPreregistered is never called due to early exception + // when(sessionData.isVerifierClientPreregistered()).thenReturn(false); + + ErrorDTO payload = mock(ErrorDTO.class); + + // Execute and verify + assertThrows(ApiNotAccessibleException.class, + () -> openID4VPService.sendErrorToVerifier(sessionData, payload)); + verify(verifierService).getTrustedVerifiers(); + } + +} diff --git a/src/test/java/io/mosip/mimoto/service/PresentationServiceTest.java b/src/test/java/io/mosip/mimoto/service/PresentationServiceTest.java new file mode 100644 index 000000000..44932cf51 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/PresentationServiceTest.java @@ -0,0 +1,729 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.VCCredentialProperties; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponseProof; +import io.mosip.openID4VP.constants.SpecVersion; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.dto.openid.presentation.FieldDTO; +import io.mosip.mimoto.dto.openid.presentation.InputDescriptorDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationDefinitionDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.VPNotCreatedException; +import io.mosip.mimoto.service.impl.DataShareServiceImpl; +import io.mosip.mimoto.service.impl.PresentationServiceImpl; +import io.mosip.mimoto.util.JwtUtils; +import io.mosip.mimoto.util.RestApiClient; +import io.mosip.mimoto.util.TestUtilities; +import io.mosip.openID4VP.authorizationRequest.Verifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.*; + +import static io.mosip.mimoto.util.JwtUtils.parseJwtHeader; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class PresentationServiceTest { + @Mock + DataShareServiceImpl dataShareService; + + @Mock + ObjectMapper objectMapper; + + @Mock + RestApiClient restApiClient; + + PresentationServiceImpl presentationService; + + String walletId, clientId, urlEncodedVPAuthorizationRequest; + VerifiersDTO verifiersDTO; + VerifierDTO verifierDTO; + List preRegisteredVerifiers; + UUID fixedUuid; + Instant fixedInstant; + + @Before + public void setup() throws JsonProcessingException { + presentationService = new PresentationServiceImpl(dataShareService, objectMapper, restApiClient, "%s#vp_token=%s&presentation_submission=%s", 65536); + when(objectMapper.writeValueAsString(any())).thenReturn("test-data"); + + // Setup for Wallet presentation tests + walletId = "wallet-123"; + clientId = "test-clientId"; + urlEncodedVPAuthorizationRequest = + "client_id=test-clientId&presentation_definition_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fpresentation_definition_uri&response_type=vp_token&response_mode=direct_post&nonce=NHgLcWlae745DpfJbUyfdg%253D%253D&response_uri=https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response&state=pcmxBfvdPEcjFObgt%252BLekA%253D%253D"; + + verifierDTO = new VerifierDTO( + clientId, + List.of("redirect-uri"), + List.of("https%3A%2F%2Finji-verify.collab.mosip.net%2Fverifier%2Fvp-response"), + null, + false, + SpecVersion.V1 + ); + verifiersDTO = new VerifiersDTO(); + verifiersDTO.setVerifiers(List.of(verifierDTO)); + preRegisteredVerifiers = List.of( + new Verifier(verifierDTO.getClientId(), verifierDTO.getResponseUris(), null) + ); + + fixedUuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + } + + @Test + public void credentialProofMatchingWithVPRequest() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + + Map mockPostResponse = Map.of("redirect_uri", "https://verifier.example.com/success"); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockPostResponse); + + String actualRedirectUrl = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("https://verifier.example.com/success", actualRedirectUrl); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + + @Test + public void authorizePresentation_redirectMode_returnsRedirectUrlWithVpTokenAndPresentationSubmission() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode(null); + + doReturn(vcCredentialResponse).when(dataShareService).downloadCredentialFromDataShare(eq(presentationRequestDTO)); + doReturn((VCCredentialProperties) vcCredentialResponse.getCredential()).when(objectMapper) + .convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class)); + doReturn("test-data").when(objectMapper).writeValueAsString(any()); + + String vpToken = "test-data"; + String presentationSubmission = "test-data"; + String redirectUri = presentationRequestDTO.getRedirectUri(); + String expected = String.format("%s#vp_token=%s&presentation_submission=%s", + redirectUri, + Base64.getUrlEncoder().encodeToString(vpToken.getBytes(StandardCharsets.UTF_8)), + URLEncoder.encode(presentationSubmission, StandardCharsets.UTF_8)); + + String actual = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals(expected, actual); + verify(restApiClient, never()).postApi(anyString(), any(), any(), eq(Map.class)); + } + + @Test + public void authorizePresentation_redirectMode_throwsWhenRedirectUrlExceedsMaxHeaderSize() throws Exception { + PresentationServiceImpl serviceWithSmallHeaderLimit = new PresentationServiceImpl( + dataShareService, + objectMapper, + restApiClient, + "%s#vp_token=%s&presentation_submission=%s", + 200 + ); + + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode(null); + + doReturn(vcCredentialResponse).when(dataShareService).downloadCredentialFromDataShare(eq(presentationRequestDTO)); + doReturn((VCCredentialProperties) vcCredentialResponse.getCredential()).when(objectMapper) + .convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class)); + doReturn("p".repeat(500)).when(objectMapper).writeValueAsString(any()); + + VPNotCreatedException ex = assertThrows(VPNotCreatedException.class, + () -> serviceWithSmallHeaderLimit.authorizePresentation(presentationRequestDTO)); + + assertEquals( + ErrorConstants.URI_TOO_LONG.getErrorCode() + " --> " + ErrorConstants.URI_TOO_LONG.getErrorMessage(), + ex.getMessage()); + verify(restApiClient, never()).postApi(anyString(), any(), any(), eq(Map.class)); + } + + @Test(expected = VPNotCreatedException.class) + public void credentialProofMismatchWithVPRequest() throws IOException { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("RSASignature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + presentationService.authorizePresentation(TestUtilities.getPresentationRequestDTO()); + } + + @Test + public void sdJwtCredentialMatchingWithVPRequest() throws Exception { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + PresentationRequestDTO presentationRequestDTO = createSDJwtPresentationRequest(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + Map mockPostResponse = Map.of("redirect_uri", "https://verifier.example.com/success"); + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(String.class))) + .thenReturn("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.test.signature"); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockPostResponse); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + String actualRedirectUrl = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("https://verifier.example.com/success", actualRedirectUrl); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + } + + @Test(expected = VPNotCreatedException.class) + public void nullPresentationDefinitionWithVPRequest() throws IOException { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setPresentationDefinition(null); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test + public void constructPresentationDefinitionForLdpVcCredential() { + VCCredentialResponse vcCredentialResponse = createLdpVcCredentialResponse(); + VCCredentialProperties credential = (VCCredentialProperties) vcCredentialResponse.getCredential(); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn(credential); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals(1, result.getInputDescriptors().size()); + + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + assertNotNull(inputDescriptor.getId()); + assertTrue(inputDescriptor.getFormat().containsKey("ldpVc")); + assertTrue(inputDescriptor.getFormat().get("ldpVc").containsKey("proofTypes")); + } + + @Test + public void constructPresentationDefinitionForSdJwtCredential() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + Map jwtPayload = Map.of("type", Arrays.asList("VerifiableCredential", "TestCredential")); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("vc+sd-jwt")); + } + } + + // Tests for handleVPAuthorizationRequest removed - method moved to WalletPresentationService + + // Helper methods + private VCCredentialResponse createSDJwtCredentialResponse(String format) { + VCCredentialResponse response = new VCCredentialResponse(); + response.setFormat(format); + response.setCredential("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.test.signature"); + return response; + } + + private PresentationRequestDTO createSDJwtPresentationRequest() { + PresentationRequestDTO request = new PresentationRequestDTO(); + request.setRedirectUri("test_redirect_uri"); + + Map>> format = Map.of( + "vc+sd-jwt", Map.of("sd-jwt_alg_values", Arrays.asList("ES256")) + ); + + InputDescriptorDTO inputDescriptor = InputDescriptorDTO.builder() + .id("test-id") + .format(format) + .build(); + + PresentationDefinitionDTO presentationDefinition = PresentationDefinitionDTO.builder() + .id("test-presentation-def") + .inputDescriptors(Arrays.asList(inputDescriptor)) + .build(); + + request.setPresentationDefinition(presentationDefinition); + return request; + } + + private VCCredentialResponse createLdpVcCredentialResponse() { + VCCredentialResponse response = new VCCredentialResponse(); + response.setFormat(CredentialFormat.LDP_VC.getFormat()); + + VCCredentialProperties credential = new VCCredentialProperties(); + credential.setType(Arrays.asList("VerifiableCredential", "TestCredential")); + credential.setContext("https://www.w3.org/2018/credentials/v1"); + + VCCredentialResponseProof proof = new VCCredentialResponseProof(); + proof.setType("Ed25519Signature2020"); + credential.setProof(proof); + + response.setCredential(credential); + return response; + } + + @Test(expected = RuntimeException.class) + public void authorizePresentationNullCredentialThrowsException() throws Exception { + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setCredential(null); + vcCredentialResponse.setFormat(CredentialFormat.LDP_VC.getFormat()); + + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(null), eq(VCCredentialProperties.class))).thenReturn(null); + + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test(expected = VPNotCreatedException.class) + public void authorizePresentationUnsupportedCredentialFormatThrowsException() throws Exception { + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setCredential("dummy"); + vcCredentialResponse.setFormat("unsupported-format"); + + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test(expected = RuntimeException.class) + public void authorizePresentationObjectMapperThrowsException() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenThrow(new RuntimeException("Mapping error")); + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test(expected = NullPointerException.class) + public void constructPresentationDefinitionNullCredentialCausesNPE() { + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setCredential(null); + vcCredentialResponse.setFormat(CredentialFormat.LDP_VC.getFormat()); + + when(objectMapper.convertValue(eq(null), eq(VCCredentialProperties.class))).thenReturn(null); + + presentationService.constructPresentationDefinition(vcCredentialResponse); + } + + // Test for constructPresentationDefinition with proper null handling + @Test + public void constructPresentationDefinitionWithValidCredential() { + VCCredentialResponse vcCredentialResponse = createLdpVcCredentialResponse(); + VCCredentialProperties credential = (VCCredentialProperties) vcCredentialResponse.getCredential(); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn(credential); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals(1, result.getInputDescriptors().size()); + } + + @Test + public void constructPresentationDefinitionUnsupportedFormatReturnsNull() { + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setCredential("dummy"); + vcCredentialResponse.setFormat("unsupported-format"); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertTrue(result.getInputDescriptors().isEmpty()); + } + + @Test + public void testDirectPostResponseMode() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + presentationRequestDTO.setState("test-state"); + presentationRequestDTO.setNonce("test-nonce"); + + Map mockResponse = Map.of("redirect_uri", "https://verifier.example.com/success"); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockResponse); + + String result = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("https://verifier.example.com/success", result); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + + @Test + public void testDirectPostResponseModeWithNoRedirectURIParam() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + presentationRequestDTO.setState("test-state"); + presentationRequestDTO.setNonce("test-nonce"); + presentationRequestDTO.setRedirectUri(""); + + Map mockResponse = Map.of("redirect_uri", "https://verifier.example.com/success"); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockResponse); + + String result = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("https://verifier.example.com/success", result); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + + @Test(expected = VPNotCreatedException.class) + public void testSdJwtUnsupportedAlgorithm() throws Exception { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + PresentationRequestDTO presentationRequestDTO = createSDJwtPresentationRequest(); + Map jwtHeaders = Map.of("alg", "RS256", "typ", "JWT"); // Unsupported algorithm + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(String.class))) + .thenReturn("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.test.signature"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + presentationService.authorizePresentation(presentationRequestDTO); + } + } + + @Test(expected = VPNotCreatedException.class) + public void testMissingInputDescriptors() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + + // Set empty input descriptors list + PresentationDefinitionDTO presentationDefinition = presentationRequestDTO.getPresentationDefinition(); + presentationDefinition.setInputDescriptors(Arrays.asList()); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test + public void testDirectPostResponseModeWithoutRedirectUri() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + presentationRequestDTO.setState("test-state"); + presentationRequestDTO.setNonce("test-nonce"); + + // Mock response without redirect_uri + Map mockResponse = Map.of("status", "success"); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockResponse); + + String result = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("test_redirect_uri", result); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + + @Test(expected = VPNotCreatedException.class) + public void testDirectPostResponseModeWithException() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + presentationRequestDTO.setState("test-state"); + presentationRequestDTO.setNonce("test-nonce"); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))) + .thenThrow(new RuntimeException("Network error")); + + presentationService.authorizePresentation(presentationRequestDTO); + } + + @Test + public void testConstructPresentationDefinitionForSdJwtWithMapType() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("dc+sd-jwt"); + + // Create complex type structure with Map containing _value + Map typeMap = new HashMap<>(); + typeMap.put("_value", "TestCredential"); + + Map jwtPayload = Map.of("type", Arrays.asList("VerifiableCredential", typeMap)); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("dc+sd-jwt")); + + // Verify the filter pattern is set to the extracted type + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + FieldDTO field = inputDescriptor.getConstraints().getFields()[0]; + assertEquals("TestCredential", field.getFilter().getPattern()); + } + } + + @Test + public void testConstructPresentationDefinitionForSdJwtWithStringType() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + + // Create simple type structure with String + Map jwtPayload = Map.of("type", Arrays.asList("VerifiableCredential", "TestCredential")); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("vc+sd-jwt")); + + // Verify the filter pattern is set to the extracted type + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + FieldDTO field = inputDescriptor.getConstraints().getFields()[0]; + assertEquals("TestCredential", field.getFilter().getPattern()); + } + } + + @Test + public void testConstructPresentationDefinitionForSdJwtWithNullType() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + + // Create payload with null type using HashMap to allow null values + Map jwtPayload = new HashMap<>(); + jwtPayload.put("type", null); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("vc+sd-jwt")); + + // Verify the filter pattern is null when type is null + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + FieldDTO field = inputDescriptor.getConstraints().getFields()[0]; + assertNull(field.getFilter().getPattern()); + } + } + + @Test + public void testConstructPresentationDefinitionForSdJwtWithEmptyTypeList() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("dc+sd-jwt"); + + // Create payload with empty type list + Map jwtPayload = Map.of("type", Arrays.asList()); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("dc+sd-jwt")); + + // Verify the filter pattern is null when type list is empty + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + FieldDTO field = inputDescriptor.getConstraints().getFields()[0]; + assertNull(field.getFilter().getPattern()); + } + } + + @Test + public void testConstructPresentationDefinitionForSdJwtWithMapTypeNullValue() { + VCCredentialResponse vcCredentialResponse = createSDJwtCredentialResponse("vc+sd-jwt"); + + // Create complex type structure with Map containing null _value + Map typeMap = new HashMap<>(); + typeMap.put("_value", null); + + Map jwtPayload = Map.of("type", Arrays.asList("VerifiableCredential", typeMap)); + Map jwtHeaders = Map.of("alg", "ES256", "typ", "JWT"); + + try (MockedStatic jwtUtilsMock = mockStatic(JwtUtils.class)) { + jwtUtilsMock.when(() -> JwtUtils.extractJwtPayloadFromSdJwt(anyString())).thenReturn(jwtPayload); + jwtUtilsMock.when(() -> JwtUtils.parseJwtHeader(anyString())).thenReturn(jwtHeaders); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + assertEquals(1, result.getInputDescriptors().size()); + assertTrue(result.getInputDescriptors().get(0).getFormat().containsKey("vc+sd-jwt")); + + // Verify the filter pattern is null when _value is null + InputDescriptorDTO inputDescriptor = result.getInputDescriptors().get(0); + FieldDTO field = inputDescriptor.getConstraints().getFields()[0]; + assertNull(field.getFilter().getPattern()); + } + } + + @Test + public void testDirectPostResponseModeWithEmptyRedirectUri() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTOWithEmptyRedirectURI(); + presentationRequestDTO.setResponseMode("direct_post"); + presentationRequestDTO.setResponseUri("https://verifier.example.com/response"); + presentationRequestDTO.setState("test-state"); + presentationRequestDTO.setNonce("test-nonce"); + + // Mock response with empty redirect_uri + Map mockResponse = Map.of("redirect_uri", ""); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + when(restApiClient.postApi(anyString(), any(), any(), eq(Map.class))).thenReturn(mockResponse); + + String result = presentationService.authorizePresentation(presentationRequestDTO); + + assertEquals("https://verifier.example.com/response?status=vp_sent", result); + verify(restApiClient).postApi(eq("https://verifier.example.com/response"), any(), any(), eq(Map.class)); + } + + @Test + public void testAuthorizePresentationWithJsonProcessingException() throws Exception { + VCCredentialResponse vcCredentialResponse = TestUtilities.getVCCredentialResponseDTO("Ed25519Signature2020"); + PresentationRequestDTO presentationRequestDTO = TestUtilities.getPresentationRequestDTO(); + + when(dataShareService.downloadCredentialFromDataShare(eq(presentationRequestDTO))).thenReturn(vcCredentialResponse); + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn((VCCredentialProperties) vcCredentialResponse.getCredential()); + + // Mock objectMapper.writeValueAsString to throw JsonProcessingException + when(objectMapper.writeValueAsString(any())).thenThrow(new JsonProcessingException("JSON processing error") {}); + + // Act & Assert + VPNotCreatedException exception = assertThrows(VPNotCreatedException.class, () -> { + presentationService.authorizePresentation(presentationRequestDTO); + }); + + assertEquals(ErrorConstants.INVALID_REQUEST.getErrorCode() + " --> " + ErrorConstants.INVALID_REQUEST.getErrorMessage(), exception.getMessage()); + } + + // Tests for rejectVerifier removed - method moved to WalletPresentationService + + @Test + public void constructPresentationDefinitionWithRenderMethod() throws Exception{ + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setFormat(CredentialFormat.LDP_VC.getFormat()); + + Map template = Map.of( + "id", "https://degree.example/credential-templates/bachelors", + "mediaType", "image/svg+xml", + "digestMultibase", "zQmerWC85Wg6wFl9znFCwYxApG270iEu5h6JqWAPdhyxz2dR" + ); + Map renderMethod = Map.of( + "type", "TemplateRenderMethod", + "renderSuite", "svg-mustache", + "template", template + ); + + VCCredentialProperties credential = new VCCredentialProperties(); + credential.setType(Arrays.asList("VerifiableCredential", "TestCredential")); + credential.setContext("https://www.w3.org/2018/credentials/v2"); + credential.setRenderMethod(renderMethod); + VCCredentialResponseProof proof = new VCCredentialResponseProof(); + proof.setType("Ed25519Signature2020"); + credential.setProof(proof); + + vcCredentialResponse.setCredential(credential); + + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn(credential); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(credential); + assertTrue(json.contains("renderMethod")); + } + + @Test + public void constructPresentationDefinitionWithoutRenderMethod() throws Exception { + VCCredentialResponse vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setFormat(CredentialFormat.LDP_VC.getFormat()); + + VCCredentialProperties credential = new VCCredentialProperties(); + credential.setType(Arrays.asList("VerifiableCredential", "TestCredential")); + credential.setContext("https://www.w3.org/2018/credentials/v2"); + // renderMethod not set + VCCredentialResponseProof proof = new VCCredentialResponseProof(); + proof.setType("Ed25519Signature2020"); + credential.setProof(proof); + + vcCredentialResponse.setCredential(credential); + + when(objectMapper.convertValue(eq(vcCredentialResponse.getCredential()), eq(VCCredentialProperties.class))) + .thenReturn(credential); + + PresentationDefinitionDTO result = presentationService.constructPresentationDefinition(vcCredentialResponse); + + assertNotNull(result); + + //Not mocking the ObjectMapper here to test the actual serialization : renderMethod should be absent + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(credential); + assertFalse(json.contains("renderMethod")); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/RestClientServiceTest.java b/src/test/java/io/mosip/mimoto/service/RestClientServiceTest.java new file mode 100644 index 000000000..7db422d81 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/RestClientServiceTest.java @@ -0,0 +1,205 @@ +package io.mosip.mimoto.service; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.Lists; +import io.mosip.mimoto.constant.ApiName; +import io.mosip.mimoto.dto.AuditRequestDto; +import io.mosip.mimoto.dto.AuditResponseDto; +import io.mosip.mimoto.exception.ApisResourceAccessException; +import io.mosip.mimoto.service.impl.RestClientServiceImpl; +import io.mosip.mimoto.util.RestApiClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.web.client.ResourceAccessException; + +@RunWith(MockitoJUnitRunner.class) +@SpringBootTest +public class RestClientServiceTest { + + private static final String API_NAME = "AUDIT"; + + @InjectMocks + private RestClientServiceImpl registrationProcessorRestClientService; + /** The rest api client. */ + @Mock + private RestApiClient restApiClient; + + /** The env. */ + @Mock + private Environment env; + private AuditResponseDto auditResponseDto; + + @Before + public void setUp() { + auditResponseDto = new AuditResponseDto(); + auditResponseDto.setStatus(true); + + } + + @Test + public void getObjectSuccessTest() throws Exception { + + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.getApi(ArgumentMatchers.any(URI.class), ArgumentMatchers.any())).thenReturn(auditResponseDto); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.getApi(ApiName.AUDIT, + null, "query1", "12345", AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test + public void getSuccessTest() throws Exception { + + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.getApi(ArgumentMatchers.any(URI.class), ArgumentMatchers.any())).thenReturn(auditResponseDto); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.getApi(ApiName.AUDIT, + Lists.newArrayList("pathsegnemts"), Lists.newArrayList("query1"), Lists.newArrayList("12345"), AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test + public void postObjecSuccessTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(auditResponseDto); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.postApi(ApiName.AUDIT, + "query1", "12345", auditRequestDto, AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test(expected = ApisResourceAccessException.class) + public void getObjecTestFailureTest() throws Exception { + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + ResourceAccessException exp = new ResourceAccessException("errorMessage"); + Mockito.when(restApiClient.getApi(ArgumentMatchers.any(URI.class), ArgumentMatchers.any())).thenThrow(exp); + + registrationProcessorRestClientService.getApi(ApiName.AUDIT, Arrays.asList("abc", "def"), "query1", "12345", + AuditResponseDto.class); + } + + @Test(expected = ApisResourceAccessException.class) + public void getFailureTest() throws Exception { + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + ResourceAccessException exp = new ResourceAccessException("errorMessage"); + Mockito.when(restApiClient.getApi(ArgumentMatchers.any(URI.class), ArgumentMatchers.any())).thenThrow(exp); + + registrationProcessorRestClientService.getApi(ApiName.AUDIT, Arrays.asList("abc", "def"), + Lists.newArrayList("query1"), Lists.newArrayList("12345"), + AuditResponseDto.class); + } + + @Test(expected = ApisResourceAccessException.class) + public void postObjecTestFailureTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + ResourceAccessException exp = new ResourceAccessException("errorMessage"); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenThrow(exp); + registrationProcessorRestClientService.postApi(ApiName.AUDIT, "query1", "12345", auditRequestDto, + AuditResponseDto.class); + } + + @Test + public void getObjectForArgListSuccessTest() throws Exception { + + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.getApi(ArgumentMatchers.any(URI.class), ArgumentMatchers.any())).thenReturn(auditResponseDto); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.getApi(ApiName.AUDIT, + null, Arrays.asList("query1"), Arrays.asList("12345"), AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test + public void postObjectSuccessTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(auditResponseDto); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.postApi(ApiName.AUDIT, + pathSegments, "query1", "12345", auditRequestDto, AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test + public void postSuccessTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyBoolean())) + .thenReturn(auditResponseDto); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.postApi(ApiName.AUDIT, + auditRequestDto, AuditResponseDto.class, true); + assertEquals(true, resultDto.isStatus()); + } + + @Test(expected = ApisResourceAccessException.class) + public void postFailedTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyBoolean())) + .thenThrow(new ApisResourceAccessException("Exception")); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.postApi(ApiName.AUDIT, + auditRequestDto, AuditResponseDto.class, true); + } + + @Test(expected = ApisResourceAccessException.class) + public void postObjectTestFailureTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + ResourceAccessException exp = new ResourceAccessException("errorMessage"); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenThrow(exp); + registrationProcessorRestClientService.postApi(ApiName.AUDIT, pathSegments, "query1", "12345", auditRequestDto, + AuditResponseDto.class); + } + + @Test + public void postObjectForArgListSuccessTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(auditResponseDto); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + AuditResponseDto resultDto = (AuditResponseDto) registrationProcessorRestClientService.postApi(ApiName.AUDIT, + null, pathSegments, Arrays.asList("query1"), Arrays.asList("12345"), auditRequestDto, + AuditResponseDto.class); + assertEquals(true, resultDto.isStatus()); + } + + @Test(expected = ApisResourceAccessException.class) + public void postObjectForArgListTestFailureTest() throws Exception { + AuditRequestDto auditRequestDto = new AuditRequestDto(); + Mockito.when(env.getProperty(ArgumentMatchers.any())).thenReturn(API_NAME); + ResourceAccessException exp = new ResourceAccessException("errorMessage"); + List pathSegments = new ArrayList<>(); + pathSegments.add("test"); + Mockito.when(restApiClient.postApi(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenThrow(exp); + registrationProcessorRestClientService.postApi(ApiName.AUDIT, null, pathSegments, Arrays.asList("query1"), + Arrays.asList("12345"), auditRequestDto, AuditResponseDto.class); + } + +} diff --git a/src/test/java/io/mosip/mimoto/service/SessionManagerTest.java b/src/test/java/io/mosip/mimoto/service/SessionManagerTest.java new file mode 100644 index 000000000..acd68aeed --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/SessionManagerTest.java @@ -0,0 +1,454 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.mimoto.UserMetadataDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.service.impl.SessionManager; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpSession; + +import java.time.Instant; +import java.util.*; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class SessionManagerTest { + + UserMetadataDTO userMetadataDTO; + private SessionManager sessionManager; + @Mock + private ObjectMapper objectMapper; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + sessionManager = new SessionManager(objectMapper); + userMetadataDTO = new UserMetadataDTO("Test user", "https://test.com/pic.jpg", "test@example.com", "wallet123"); + } + + @Test + public void shouldSetupSessionWithProvidedUserAndOAuth2ProviderDetails() { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpSession session = new MockHttpSession(); + when(request.getSession(true)).thenReturn(session); + + String provider = "google"; + String userId = "123"; + + sessionManager.setupSession(request, provider, userMetadataDTO, userId); + + assertEquals(provider, session.getAttribute(SessionKeys.CLIENT_REGISTRATION_ID)); + assertEquals(userMetadataDTO, session.getAttribute(SessionKeys.USER_METADATA)); + assertEquals(userId, session.getAttribute(SessionKeys.USER_ID)); + } + + @Test + public void shouldCreateAndStorePresentationDetailsInSession() { + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "123e4567-e89b-12d3-a456-426614174000"; + VerifiablePresentationSessionData presentationSessionData = new VerifiablePresentationSessionData(presentationId, "authorizationRequest", fixedInstant, false, null, false); + + sessionManager.storePresentationSessionData(session, presentationSessionData, walletId); + + @SuppressWarnings("unchecked") + Map presentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(presentations); + assertTrue(presentations.containsKey(presentationId)); + + VerifiablePresentationSessionData storedPresentationSessionDetails = presentations.get(presentationId); + assertNotNull(storedPresentationSessionDetails); + assertEquals(presentationSessionData.getAuthorizationRequest(), storedPresentationSessionDetails.getAuthorizationRequest()); + assertEquals(presentationSessionData.getCreatedAt(), storedPresentationSessionDetails.getCreatedAt()); + } + + @Test + public void shouldStorePresentationDetailsProperlyInSessionForMultiplePresentations() { + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + MockHttpSession session = new MockHttpSession(); + + String walletId1 = "wallet123"; + String walletId2 = "wallet456"; + String presentationId1 = "123e4567-e89b-12d3-a456-426614174000"; + String presentationId2 = "123e4567-e89b-12d3-a456-426614174001"; + + VerifiablePresentationSessionData sessionData1 = new VerifiablePresentationSessionData(presentationId1, "authorizationRequest", fixedInstant, true, null, false); + VerifiablePresentationSessionData sessionData2 = new VerifiablePresentationSessionData(presentationId2, "authorizationRequest", fixedInstant, false, null, false); + + sessionManager.storePresentationSessionData(session, sessionData1, walletId1); + sessionManager.storePresentationSessionData(session, sessionData2, walletId2); + + @SuppressWarnings("unchecked") + Map presentations1 = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId1); + @SuppressWarnings("unchecked") + Map presentations2 = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId2); + + assertNotNull(presentations1); + assertNotNull(presentations2); + + assertTrue(presentations1.containsKey(presentationId1)); + assertTrue(presentations2.containsKey(presentationId2)); + + VerifiablePresentationSessionData storedData1 = presentations1.get(presentationId1); + VerifiablePresentationSessionData storedData2 = presentations2.get(presentationId2); + + assertNotNull(storedData1); + assertNotNull(storedData2); + + assertEquals(sessionData1.getAuthorizationRequest(), storedData1.getAuthorizationRequest()); + assertEquals(sessionData1.getCreatedAt(), storedData1.getCreatedAt()); + assertEquals(sessionData2.getAuthorizationRequest(), storedData2.getAuthorizationRequest()); + assertEquals(sessionData2.getCreatedAt(), storedData2.getCreatedAt()); + } + + @Test + public void shouldReturnPresentationDefinitionFromSessionSuccessfully() { + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + String presentationId = "test-presentation-id"; + String walletId = "wallet123"; + MockHttpSession session = new MockHttpSession(); + + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData(presentationId,"mockOpenID4VP", fixedInstant, true, null, false); + + Map presentations = new HashMap<>(); + presentations.put(presentationId, sessionData); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + VerifiablePresentationSessionData result = sessionManager.getPresentationSessionData(session, walletId, presentationId); + + assertNotNull(result); + assertNotNull(result.getAuthorizationRequest()); + assertEquals(fixedInstant, result.getCreatedAt()); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldReturnNullWhenNoPresentationsInSession() { + String presentationId = "test-presentation-id"; + String walletId = "wallet123"; + MockHttpSession session = new MockHttpSession(); + + sessionManager.getPresentationSessionData(session, walletId, presentationId); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldReturnNullWhenPresentationIdNotFound() { + String presentationId = "non-existent-id"; + String walletId = "wallet123"; + MockHttpSession session = new MockHttpSession(); + + Map presentations = new HashMap<>(); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + sessionManager.getPresentationSessionData(session, walletId, presentationId); + } + + @Test + public void shouldRejectNullHttpSession() { + try { + sessionManager.getPresentationSessionData(null, "wallet123", "test-presentation-id"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("HTTP session is invalid", e.getMessage()); + } + } + + @Test + public void shouldRejectNullWalletId() { + MockHttpSession session = new MockHttpSession(); + try { + sessionManager.getPresentationSessionData(session, null, "test-presentation-id"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("Wallet ID is invalid", e.getMessage()); + } + } + + @Test + public void shouldRejectBlankWalletId() { + MockHttpSession session = new MockHttpSession(); + try { + sessionManager.getPresentationSessionData(session, " ", "test-presentation-id"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("Wallet ID is invalid", e.getMessage()); + } + } + + @Test + public void shouldRejectNullPresentationId() { + MockHttpSession session = new MockHttpSession(); + try { + sessionManager.getPresentationSessionData(session, "wallet123", null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("presentationId not found in session", e.getMessage()); + } + } + + @Test + public void shouldRejectBlankPresentationId() { + MockHttpSession session = new MockHttpSession(); + try { + sessionManager.getPresentationSessionData(session, "wallet123", " "); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("presentationId not found in session", e.getMessage()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void shouldHandleExceptionWhenRetrievingPresentationSessionData() { + // Arrange + String presentationId = "test-presentation-id"; + String walletId = "wallet123"; + HttpSession session = mock(HttpSession.class); + + // Mock session to throw exception when getting attribute + when(session.getAttribute(anyString())).thenThrow(new IllegalArgumentException("Session error")); + + // Act + sessionManager.getPresentationSessionData(session, walletId, presentationId); + } + + @Test + public void shouldStoreMatchingWalletCredentialsInPresentationSessionData() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + // Create existing session data + VerifiablePresentationSessionData existingSessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, false); + + // Create credentials to store + List credentials = createMockCredentials(); + + // Set up existing presentations map in session + Map presentations = new HashMap<>(); + presentations.put(presentationId, existingSessionData); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + // Act + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData(session, walletId, existingSessionData, credentials); + + // Assert + @SuppressWarnings("unchecked") + Map updatedPresentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(updatedPresentations); + assertTrue(updatedPresentations.containsKey(presentationId)); + + VerifiablePresentationSessionData updatedSessionData = updatedPresentations.get(presentationId); + assertNotNull(updatedSessionData); + assertEquals(presentationId, updatedSessionData.getPresentationId()); + assertEquals("authorizationRequest", updatedSessionData.getAuthorizationRequest()); + assertEquals(fixedInstant, updatedSessionData.getCreatedAt()); + assertTrue(updatedSessionData.isVerifierClientPreregistered()); + assertFalse(updatedSessionData.isDcql()); + assertEquals(credentials, updatedSessionData.getMatchingCredentials()); + } + + @Test + public void shouldPreserveDcqlFlagWhenStoringMatchingWalletCredentials() { + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + VerifiablePresentationSessionData existingSessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, true); + List credentials = createMockCredentials(); + + Map presentations = new HashMap<>(); + presentations.put(presentationId, existingSessionData); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData( + session, walletId, existingSessionData, credentials); + + @SuppressWarnings("unchecked") + Map updatedPresentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertTrue(updatedPresentations.get(presentationId).isDcql()); + } + + @Test + public void shouldStoreMatchingWalletCredentialsWithNullExistingPresentations() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + // Create existing session data + VerifiablePresentationSessionData existingSessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, false); + + // Create credentials to store + List credentials = createMockCredentials(); + + // Don't set up existing presentations map - should handle null case + + // Act & Assert - Should throw NullPointerException when trying to get null presentations map + try { + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData(session, walletId, existingSessionData, credentials); + fail("Expected NullPointerException to be thrown"); + } catch (NullPointerException e) { + // Expected behavior + } + } + + @Test + public void shouldCreateNewSessionMapWhenNoneExists() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + VerifiablePresentationSessionData sessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, false); + + // Act + sessionManager.storePresentationSessionData(session, sessionData, walletId); + + // Assert + @SuppressWarnings("unchecked") + Map presentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(presentations); + assertTrue(presentations.containsKey(presentationId)); + assertEquals(sessionData, presentations.get(presentationId)); + } + + @Test + public void shouldNotOverwriteExistingPresentationWhenStoringSamePresentationId() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + // Create original session data + VerifiablePresentationSessionData originalSessionData = new VerifiablePresentationSessionData( + presentationId, "originalRequest", fixedInstant, true, null, false); + + // Create new session data with same presentation ID + VerifiablePresentationSessionData newSessionData = new VerifiablePresentationSessionData( + presentationId, "newRequest", fixedInstant.plusSeconds(100), false, null, false); + + // Store original data + sessionManager.storePresentationSessionData(session, originalSessionData, walletId); + + // Act - Try to store new data with same presentation ID + sessionManager.storePresentationSessionData(session, newSessionData, walletId); + + // Assert - Should still have original data due to computeIfAbsent + @SuppressWarnings("unchecked") + Map presentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(presentations); + assertTrue(presentations.containsKey(presentationId)); + + VerifiablePresentationSessionData storedData = presentations.get(presentationId); + assertEquals("originalRequest", storedData.getAuthorizationRequest()); // Should be original, not new + assertTrue(storedData.isVerifierClientPreregistered()); // Should be original, not new + } + + @Test + public void shouldHandleNullCredentialsInStoreMatchingWalletCredentials() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + VerifiablePresentationSessionData existingSessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, false); + + // Set up existing presentations map + Map presentations = new HashMap<>(); + presentations.put(presentationId, existingSessionData); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + // Act + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData(session, walletId, existingSessionData, null); + + // Assert + @SuppressWarnings("unchecked") + Map updatedPresentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(updatedPresentations); + VerifiablePresentationSessionData updatedSessionData = updatedPresentations.get(presentationId); + assertNull(updatedSessionData.getMatchingCredentials()); + } + + @Test + public void shouldHandleEmptyCredentialsListInStoreMatchingWalletCredentials() { + // Arrange + MockHttpSession session = new MockHttpSession(); + String walletId = "wallet123"; + String presentationId = "test-presentation-id"; + Instant fixedInstant = Instant.parse("2025-09-08T12:34:56Z"); + + VerifiablePresentationSessionData existingSessionData = new VerifiablePresentationSessionData( + presentationId, "authorizationRequest", fixedInstant, true, null, false); + + // Set up existing presentations map + Map presentations = new HashMap<>(); + presentations.put(presentationId, existingSessionData); + session.setAttribute(SessionKeys.PRESENTATIONS + "::" + walletId, presentations); + + // Act + sessionManager.storeMatchingWalletCredentialsInPresentationSessionData(session, walletId, existingSessionData, Collections.emptyList()); + + // Assert + @SuppressWarnings("unchecked") + Map updatedPresentations = + (Map) session.getAttribute(SessionKeys.PRESENTATIONS + "::" + walletId); + + assertNotNull(updatedPresentations); + VerifiablePresentationSessionData updatedSessionData = updatedPresentations.get(presentationId); + assertNotNull(updatedSessionData.getMatchingCredentials()); + assertTrue(updatedSessionData.getMatchingCredentials().isEmpty()); + } + + // Helper method to create mock credentials + private List createMockCredentials() { + DecryptedCredentialDTO credential = new DecryptedCredentialDTO(); + credential.setId("test-credential-id"); + credential.setWalletId("wallet123"); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId("test-issuer"); + metadata.setCredentialType("TestCredential"); + credential.setCredentialMetadata(metadata); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + return Collections.singletonList(credential); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/TokenServiceFactoryTest.java b/src/test/java/io/mosip/mimoto/service/TokenServiceFactoryTest.java new file mode 100644 index 000000000..db18ac7d9 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/TokenServiceFactoryTest.java @@ -0,0 +1,68 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.OAuth2AuthenticationException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class TokenServiceFactoryTest { + + @InjectMocks + private TokenServiceFactory tokenServiceFactory; + + @Mock + private TokenService googleTokenService; + + @Before + public void setUp() { + Map tokenServices = new HashMap<>(); + tokenServices.put("google", googleTokenService); + tokenServiceFactory = new TokenServiceFactory(tokenServices); + } + + @Test + public void testGetTokenService_ValidProvider_ReturnsService() throws OAuth2AuthenticationException { + // Act + TokenService result = tokenServiceFactory.getTokenService("google"); + + // Assert + assertNotNull(result); + assertEquals(googleTokenService, result); + } + + @Test + public void testGetTokenService_CaseInsensitiveProvider_ReturnsService() throws OAuth2AuthenticationException { + // Act + TokenService result = tokenServiceFactory.getTokenService("GOOGLE"); + + // Assert + assertNotNull(result); + assertEquals(googleTokenService, result); + } + + @Test + public void testGetTokenService_UnsupportedProvider_ThrowsException() { + // Arrange + String invalidProvider = "facebook"; + + // Act & Assert + OAuth2AuthenticationException exception = assertThrows(OAuth2AuthenticationException.class, + () -> tokenServiceFactory.getTokenService(invalidProvider)); + + assertEquals(ErrorConstants.INVALID_REQUEST.getErrorCode(), exception.getErrorCode()); + assertEquals("invalid_request --> Unsupported provider: " + invalidProvider, exception.getMessage()); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); + } + +} diff --git a/src/test/java/io/mosip/mimoto/service/TrustedVerifierServiceTest.java b/src/test/java/io/mosip/mimoto/service/TrustedVerifierServiceTest.java new file mode 100644 index 000000000..46f157d70 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/TrustedVerifierServiceTest.java @@ -0,0 +1,208 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.TrustedVerifierRequest; +import io.mosip.mimoto.dto.TrustedVerifierResponseDTO; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.model.TrustedVerifier; +import io.mosip.mimoto.repository.VerifierRepository; +import io.mosip.mimoto.service.impl.TrustedVerifierServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static io.mosip.mimoto.exception.ErrorConstants.DUPLICATE_VERIFIER; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TrustedVerifierServiceTest { + + @Mock + private VerifierRepository verifierRepository; + + @InjectMocks + private TrustedVerifierServiceImpl trustedVerifierService; + + private TrustedVerifierRequest request; + private String walletId; + + @BeforeEach + void setUp() { + walletId = "test-wallet-id"; + request = new TrustedVerifierRequest(); + request.setVerifierId("test-verifier-id"); + } + + @Test + void testAddTrustedVerifierSuccess() { + // Given + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, request.getVerifierId())) + .thenReturn(false); + + TrustedVerifier savedVerifier = TrustedVerifier.builder() + .id("generated-uuid") + .walletId(walletId) + .verifierId(request.getVerifierId()) + .build(); + + when(verifierRepository.save(any(TrustedVerifier.class))).thenReturn(savedVerifier); + + // When + TrustedVerifierResponseDTO response = trustedVerifierService.addTrustedVerifier(walletId, request); + + // Then + assertNotNull(response); + assertEquals("generated-uuid", response.getId()); + + verify(verifierRepository).existsByWalletIdAndVerifierId(walletId, request.getVerifierId()); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } + + @Test + void testAddTrustedVerifierDuplicateThrowsException() { + // Given + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, request.getVerifierId())) + .thenReturn(true); + + // When & Then + InvalidRequestException exception = assertThrows(InvalidRequestException.class, + () -> trustedVerifierService.addTrustedVerifier(walletId, request)); + + assertEquals(DUPLICATE_VERIFIER.getErrorCode(), exception.getErrorCode()); + assertEquals(DUPLICATE_VERIFIER.getErrorMessage(), exception.getErrorText()); + + verify(verifierRepository).existsByWalletIdAndVerifierId(walletId, request.getVerifierId()); + verifyNoMoreInteractions(verifierRepository); + } + + @Test + void testAddTrustedVerifierWithNullWalletId() { + // Given + String nullWalletId = null; + + when(verifierRepository.existsByWalletIdAndVerifierId(nullWalletId, request.getVerifierId())) + .thenReturn(false); + + TrustedVerifier savedVerifier = TrustedVerifier.builder() + .id("generated-uuid") + .walletId(nullWalletId) + .verifierId(request.getVerifierId()) + .build(); + + when(verifierRepository.save(any(TrustedVerifier.class))).thenReturn(savedVerifier); + + // When + TrustedVerifierResponseDTO response = trustedVerifierService.addTrustedVerifier(nullWalletId, request); + + // Then + assertNotNull(response); + assertEquals("generated-uuid", response.getId()); + + verify(verifierRepository).existsByWalletIdAndVerifierId(nullWalletId, request.getVerifierId()); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } + + @Test + void testAddTrustedVerifierWithNullRequest() { + // Given + TrustedVerifierRequest nullRequest = null; + + // When & Then + assertThrows(NullPointerException.class, + () -> trustedVerifierService.addTrustedVerifier(walletId, nullRequest)); + + verifyNoInteractions(verifierRepository); + } + + @Test + void testAddTrustedVerifierWithNullVerifierId() { + // Given + request.setVerifierId(null); + + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, null)) + .thenReturn(false); + + TrustedVerifier savedVerifier = TrustedVerifier.builder() + .id("generated-uuid") + .walletId(walletId) + .verifierId(null) + .build(); + + when(verifierRepository.save(any(TrustedVerifier.class))).thenReturn(savedVerifier); + + // When + TrustedVerifierResponseDTO response = trustedVerifierService.addTrustedVerifier(walletId, request); + + // Then + assertNotNull(response); + assertEquals("generated-uuid", response.getId()); + + verify(verifierRepository).existsByWalletIdAndVerifierId(walletId, null); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } + + @Test + void testAddTrustedVerifierRepositorySaveReturnsNull() { + // Given + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, request.getVerifierId())) + .thenReturn(false); + + when(verifierRepository.save(any(TrustedVerifier.class))).thenReturn(null); + + // When & Then + assertThrows(NullPointerException.class, + () -> trustedVerifierService.addTrustedVerifier(walletId, request)); + + verify(verifierRepository).existsByWalletIdAndVerifierId(walletId, request.getVerifierId()); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } + + @Test + void testAddTrustedVerifierWithEmptyStrings() { + // Given + String emptyWalletId = ""; + request.setVerifierId(""); + + when(verifierRepository.existsByWalletIdAndVerifierId(emptyWalletId, "")) + .thenReturn(false); + + TrustedVerifier savedVerifier = TrustedVerifier.builder() + .id("generated-uuid") + .walletId(emptyWalletId) + .verifierId("") + .build(); + + when(verifierRepository.save(any(TrustedVerifier.class))).thenReturn(savedVerifier); + + // When + TrustedVerifierResponseDTO response = trustedVerifierService.addTrustedVerifier(emptyWalletId, request); + + // Then + assertNotNull(response); + assertEquals("generated-uuid", response.getId()); + + verify(verifierRepository).existsByWalletIdAndVerifierId(emptyWalletId, ""); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } + + @Test + void testAddTrustedVerifierRepositoryThrowsException() { + // Given + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, request.getVerifierId())) + .thenReturn(false); + + when(verifierRepository.save(any(TrustedVerifier.class))) + .thenThrow(new RuntimeException("Database error")); + + // When & Then + assertThrows(RuntimeException.class, + () -> trustedVerifierService.addTrustedVerifier(walletId, request)); + + verify(verifierRepository).existsByWalletIdAndVerifierId(walletId, request.getVerifierId()); + verify(verifierRepository).save(any(TrustedVerifier.class)); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/UserMetadataServiceTest.java b/src/test/java/io/mosip/mimoto/service/UserMetadataServiceTest.java new file mode 100644 index 000000000..1f04f43d7 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/UserMetadataServiceTest.java @@ -0,0 +1,170 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.exception.DecryptionException; +import io.mosip.mimoto.exception.EncryptionException; +import io.mosip.mimoto.model.UserMetadata; +import io.mosip.mimoto.repository.UserMetadataRepository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class UserMetadataServiceTest { + + @Mock + private UserMetadataRepository userMetadataRepository; + + @Mock + private EncryptionService encryptionService; + + @InjectMocks + private UserMetadataService userMetadataService; + + private String providerSubjectId, identityProvider, displayName, profilePictureUrl, email, userId; + private Instant now; + private UserMetadata userMetadata; + + @Before + public void setUp() { + providerSubjectId = "provider123"; + identityProvider = "google"; + displayName = "Name 123"; + profilePictureUrl = "http://profile.pic"; + email = "name.123@example.com"; + now = Instant.now(); + userId = UUID.randomUUID().toString(); + + userMetadata = new UserMetadata(); + userMetadata.setId(userId); + userMetadata.setProviderSubjectId(providerSubjectId); + userMetadata.setIdentityProvider(identityProvider); + userMetadata.setDisplayName("encryptedDisplayName"); + userMetadata.setProfilePictureUrl("encryptedProfilePictureUrl"); + userMetadata.setEmail("encryptedEmail"); + userMetadata.setCreatedAt(now); + userMetadata.setUpdatedAt(now); + } + + @Test + public void shouldUpdateUserMetadataForSameProviderSubjectIdAndSameIdentityProviderButWithDifferentDisplayName() throws DecryptionException, EncryptionException { + String updatedDisplayName = "Name 124"; + String encryptedUpdatedDisplayName = "encryptedUpdatedDisplayName"; + + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(providerSubjectId, identityProvider)).thenReturn(Optional.of(userMetadata)); + when(encryptionService.decrypt("encryptedDisplayName")).thenReturn(displayName); + when(encryptionService.encrypt(updatedDisplayName)).thenReturn(encryptedUpdatedDisplayName); + when(encryptionService.decrypt("encryptedProfilePictureUrl")).thenReturn(profilePictureUrl); + when(encryptionService.encrypt(profilePictureUrl)).thenReturn("encryptedProfilePictureUrl"); + when(encryptionService.decrypt("encryptedEmail")).thenReturn(email); + when(encryptionService.encrypt(email)).thenReturn("encryptedEmail"); + when(userMetadataRepository.save(any(UserMetadata.class))).thenReturn(userMetadata); + + String storedUserId = userMetadataService.updateOrCreateUserMetadata(providerSubjectId, identityProvider, updatedDisplayName, profilePictureUrl, email); + + assertEquals(userId, storedUserId); + ArgumentCaptor userMetadataCaptor = ArgumentCaptor.forClass(UserMetadata.class); + verify(userMetadataRepository).save(userMetadataCaptor.capture()); + UserMetadata savedUserMetadata = userMetadataCaptor.getValue(); + assertEquals(encryptedUpdatedDisplayName, savedUserMetadata.getDisplayName()); + assertEquals("encryptedProfilePictureUrl", savedUserMetadata.getProfilePictureUrl()); + assertEquals("encryptedEmail", savedUserMetadata.getEmail()); + verify(encryptionService).decrypt("encryptedDisplayName"); + verify(encryptionService).encrypt(updatedDisplayName); + } + + @Test + public void shouldCreateNewUserMetadataForSameProviderSubjectIdAndDifferentIdentityProvider() throws DecryptionException, EncryptionException { + String newIdentityProvider = "facebook"; + String newUserId = UUID.randomUUID().toString(); + UserMetadata newUserMetadata = new UserMetadata(); + newUserMetadata.setId(newUserId); + newUserMetadata.setProviderSubjectId(providerSubjectId); + newUserMetadata.setIdentityProvider(newIdentityProvider); + newUserMetadata.setDisplayName("encryptedDisplayName"); + newUserMetadata.setProfilePictureUrl("encryptedProfilePictureUrl"); + newUserMetadata.setEmail("encryptedEmail"); + newUserMetadata.setCreatedAt(now); + newUserMetadata.setUpdatedAt(now); + + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(providerSubjectId, newIdentityProvider)).thenReturn(Optional.empty()); + when(encryptionService.encrypt(displayName)).thenReturn("encryptedDisplayName"); + when(encryptionService.encrypt(profilePictureUrl)).thenReturn("encryptedProfilePictureUrl"); + when(encryptionService.encrypt(email)).thenReturn("encryptedEmail"); + when(userMetadataRepository.save(any(UserMetadata.class))).thenReturn(newUserMetadata); + + String storedUserId = userMetadataService.updateOrCreateUserMetadata(providerSubjectId, newIdentityProvider, displayName, profilePictureUrl, email); + + assertEquals(newUserId, storedUserId); + ArgumentCaptor userMetadataCaptor = ArgumentCaptor.forClass(UserMetadata.class); + verify(userMetadataRepository).save(userMetadataCaptor.capture()); + UserMetadata capturedUserMetadata = userMetadataCaptor.getValue(); + assertEquals(providerSubjectId, capturedUserMetadata.getProviderSubjectId()); + assertEquals(newIdentityProvider, capturedUserMetadata.getIdentityProvider()); + assertEquals("encryptedDisplayName", capturedUserMetadata.getDisplayName()); + assertEquals("encryptedProfilePictureUrl", capturedUserMetadata.getProfilePictureUrl()); + assertEquals("encryptedEmail", capturedUserMetadata.getEmail()); + verify(encryptionService, times(3)).encrypt(anyString()); + } + + @Test + public void shouldCreateNewUserMetadataForDifferentProviderSubjectIdAndSameIdentityProvider() throws DecryptionException, EncryptionException { + String newProviderSubjectId = "provider124"; + String newUserId = UUID.randomUUID().toString(); + UserMetadata newUserMetadata = new UserMetadata(); + newUserMetadata.setId(newUserId); + newUserMetadata.setProviderSubjectId(newProviderSubjectId); + newUserMetadata.setIdentityProvider(identityProvider); + newUserMetadata.setDisplayName("encryptedDisplayName"); + newUserMetadata.setProfilePictureUrl("encryptedProfilePictureUrl"); + newUserMetadata.setEmail("encryptedEmail"); + newUserMetadata.setCreatedAt(now); + newUserMetadata.setUpdatedAt(now); + + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(newProviderSubjectId, identityProvider)).thenReturn(Optional.empty()); + when(encryptionService.encrypt(displayName)).thenReturn("encryptedDisplayName"); + when(encryptionService.encrypt(profilePictureUrl)).thenReturn("encryptedProfilePictureUrl"); + when(encryptionService.encrypt(email)).thenReturn("encryptedEmail"); + when(userMetadataRepository.save(any(UserMetadata.class))).thenReturn(newUserMetadata); + + String storedUserId = userMetadataService.updateOrCreateUserMetadata(newProviderSubjectId, identityProvider, displayName, profilePictureUrl, email); + + assertEquals(newUserId, storedUserId); + ArgumentCaptor userMetadataCaptor = ArgumentCaptor.forClass(UserMetadata.class); + verify(userMetadataRepository).save(userMetadataCaptor.capture()); + UserMetadata capturedUserMetadata = userMetadataCaptor.getValue(); + assertEquals(newProviderSubjectId, capturedUserMetadata.getProviderSubjectId()); + assertEquals(identityProvider, capturedUserMetadata.getIdentityProvider()); + assertEquals("encryptedDisplayName", capturedUserMetadata.getDisplayName()); + assertEquals("encryptedProfilePictureUrl", capturedUserMetadata.getProfilePictureUrl()); + assertEquals("encryptedEmail", capturedUserMetadata.getEmail()); + verify(encryptionService, times(3)).encrypt(anyString()); + } + + @Test + public void shouldNotCreateNewUserMetadataForSameProviderSubjectIdAndSameIdentityProvider() throws DecryptionException, EncryptionException { + when(userMetadataRepository.findByProviderSubjectIdAndIdentityProvider(providerSubjectId, identityProvider)).thenReturn(Optional.of(userMetadata)); + when(encryptionService.decrypt(userMetadata.getDisplayName())).thenReturn(displayName); + when(encryptionService.decrypt(userMetadata.getProfilePictureUrl())).thenReturn(profilePictureUrl); + when(encryptionService.decrypt(userMetadata.getEmail())).thenReturn(email); + + String storedUserId = userMetadataService.updateOrCreateUserMetadata(providerSubjectId, identityProvider, displayName, profilePictureUrl, email); + + assertEquals(userId, storedUserId); + verify(userMetadataRepository, times(0)).save(any(UserMetadata.class)); + verify(encryptionService).decrypt(userMetadata.getDisplayName()); + verify(encryptionService).decrypt(userMetadata.getProfilePictureUrl()); + verify(encryptionService).decrypt(userMetadata.getEmail()); + verify(encryptionService, times(3)).encrypt(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/V1CredentialRequestServiceTest.java b/src/test/java/io/mosip/mimoto/service/V1CredentialRequestServiceTest.java new file mode 100644 index 000000000..8d9186467 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/V1CredentialRequestServiceTest.java @@ -0,0 +1,228 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.util.RestApiClient; +import io.mosip.mimoto.util.SigningKeyUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; + +import java.security.KeyPair; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class V1CredentialRequestServiceTest { + + private static final String CREDENTIAL_CONFIG_ID = "config-1"; + private static final String NONCE_ENDPOINT = "https://example.com/nonce"; + private static final String CREDENTIAL_ISSUER = "https://example-issuer.com"; + @Mock + private RestApiClient restApiClient; + @Mock + private KeyPairRetrievalService keyPairService; + private V1CredentialRequestService service; + private MockedStatic signingKeyUtilMock; + private IssuerDTO issuerDTO; + private CredentialIssuerWellKnownResponse wellKnownResponse; + private CredentialsSupportedResponse credentialsSupportedResponse; + + @BeforeEach + void setUp() { + if (java.security.Security.getProvider("BC") == null) { + java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + service = new V1CredentialRequestService(restApiClient, keyPairService); + ReflectionTestUtils.setField(service, "signingAlgorithmsPriorityOrder", "ED25519,ES256K,ES256,RS256"); + + signingKeyUtilMock = Mockito.mockStatic(SigningKeyUtil.class, Mockito.withSettings().defaultAnswer(Mockito.CALLS_REAL_METHODS)); + + issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id("issuer-123"); + issuerDTO.setClient_id("client-123"); + + ProofTypesSupported proofTypesSupported = new ProofTypesSupported(); + proofTypesSupported.setProofSigningAlgValuesSupported(List.of("Ed25519")); + + credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialsSupportedResponse.setFormat("vc+sd-jwt"); + credentialsSupportedResponse.setProofTypesSupported(new HashMap<>(Map.of("jwt", proofTypesSupported))); + + wellKnownResponse = new CredentialIssuerWellKnownResponse(); + wellKnownResponse.setCredentialIssuer(CREDENTIAL_ISSUER); + wellKnownResponse.setNonceEndpoint(NONCE_ENDPOINT); + wellKnownResponse.setCredentialConfigurationsSupported(Map.of(CREDENTIAL_CONFIG_ID, credentialsSupportedResponse)); + } + + @AfterEach + void tearDown() { + if (signingKeyUtilMock != null) { + signingKeyUtilMock.close(); + } + } + + @Test + void shouldBuildRequestWithNonceWhenNonceEndpointIsPresent() throws Exception { + NonceResponse nonceResponse = new NonceResponse("test-nonce"); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + assertEquals(CREDENTIAL_CONFIG_ID, result.getCredentialConfigurationId()); + assertNotNull(result.getProofs()); + assertTrue(result.getProofs().containsKey("jwt")); + assertEquals(1, result.getProofs().get("jwt").size()); + + ArgumentCaptor nonceCaptor = ArgumentCaptor.forClass(String.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(any(SigningAlgorithm.class), eq(CREDENTIAL_ISSUER), eq("client-123"), nonceCaptor.capture(), any(KeyPair.class))); + assertEquals("test-nonce", nonceCaptor.getValue()); + } + + @Test + void shouldBuildRequestWithoutNonceWhenNonceEndpointIsAbsent() throws Exception { + wellKnownResponse.setNonceEndpoint(null); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + assertEquals(CREDENTIAL_CONFIG_ID, result.getCredentialConfigurationId()); + assertNotNull(result.getProofs()); + + ArgumentCaptor nonceCaptor = ArgumentCaptor.forClass(String.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(any(SigningAlgorithm.class), eq(CREDENTIAL_ISSUER), eq("client-123"), nonceCaptor.capture(), any(KeyPair.class))); + assertNull(nonceCaptor.getValue()); + } + + @Test + void shouldBuildRequestWithoutNonceWhenNonceEndpointIsBlank() throws Exception { + wellKnownResponse.setNonceEndpoint(" "); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor nonceCaptor = ArgumentCaptor.forClass(String.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(any(SigningAlgorithm.class), eq(CREDENTIAL_ISSUER), eq("client-123"), nonceCaptor.capture(), any(KeyPair.class))); + assertNull(nonceCaptor.getValue()); + } + + @Test + void shouldBuildRequestWithoutNonceWhenNonceEndpointReturnsNull() throws Exception { + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(null); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor nonceCaptor = ArgumentCaptor.forClass(String.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(any(SigningAlgorithm.class), eq(CREDENTIAL_ISSUER), eq("client-123"), nonceCaptor.capture(), any(KeyPair.class))); + assertNull(nonceCaptor.getValue()); + } + + @Test + void shouldBuildRequestWithoutNonceWhenNonceResponseHasNullCNonce() throws Exception { + NonceResponse nonceResponse = new NonceResponse(null); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor nonceCaptor = ArgumentCaptor.forClass(String.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(any(SigningAlgorithm.class), eq(CREDENTIAL_ISSUER), eq("client-123"), nonceCaptor.capture(), any(KeyPair.class))); + assertNull(nonceCaptor.getValue()); + } + + @Test + void shouldUseDBKeysForLoginFlow() throws Exception { + NonceResponse nonceResponse = new NonceResponse("test-nonce"); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + KeyPair mockKeyPair = Mockito.mock(KeyPair.class); + + when(keyPairService.getKeyPairFromDB("wallet-1", "key-base64", SigningAlgorithm.ED25519)).thenReturn(mockKeyPair); + + signingKeyUtilMock.when(() -> SigningKeyUtil.generateJwt(eq(SigningAlgorithm.ED25519), eq(CREDENTIAL_ISSUER), eq("client-123"), eq("test-nonce"), eq(mockKeyPair))).thenReturn("mocked-jwt-token"); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, "wallet-1", "key-base64", true); + + assertNotNull(result); + assertEquals(CREDENTIAL_CONFIG_ID, result.getCredentialConfigurationId()); + assertEquals(List.of("mocked-jwt-token"), result.getProofs().get("jwt")); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateJwt(eq(SigningAlgorithm.ED25519), eq(CREDENTIAL_ISSUER), eq("client-123"), eq("test-nonce"), eq(mockKeyPair))); + } + + @Test + void shouldUseFallbackAlgorithmWhenJwtProofTypeIsNull() throws Exception { + credentialsSupportedResponse.getProofTypesSupported().put("jwt", null); + + NonceResponse nonceResponse = new NonceResponse("test-nonce"); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor algCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateKeyPair(algCaptor.capture())); + assertEquals(SigningAlgorithm.ED25519, algCaptor.getValue()); + } + + @Test + void shouldUseFallbackAlgorithmWhenProofAlgorithmsAreEmpty() throws Exception { + ProofTypesSupported emptyProof = new ProofTypesSupported(); + emptyProof.setProofSigningAlgValuesSupported(Collections.emptyList()); + credentialsSupportedResponse.getProofTypesSupported().put("jwt", emptyProof); + + NonceResponse nonceResponse = new NonceResponse("test-nonce"); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor algCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateKeyPair(algCaptor.capture())); + assertEquals(SigningAlgorithm.ED25519, algCaptor.getValue()); + } + + @Test + void shouldUseHighestPriorityMatchingAlgorithm() throws Exception { + credentialsSupportedResponse.getProofTypesSupported().get("jwt").setProofSigningAlgValuesSupported(List.of("ES256K")); + + NonceResponse nonceResponse = new NonceResponse("test-nonce"); + when(restApiClient.postApi(eq(NONCE_ENDPOINT), eq(MediaType.APPLICATION_JSON), isNull(), eq(NonceResponse.class))).thenReturn(nonceResponse); + + V1VCCredentialRequest result = service.buildRequest(issuerDTO, CREDENTIAL_CONFIG_ID, wellKnownResponse, null, null, false); + + assertNotNull(result); + + ArgumentCaptor algCaptor = ArgumentCaptor.forClass(SigningAlgorithm.class); + signingKeyUtilMock.verify(() -> SigningKeyUtil.generateKeyPair(algCaptor.capture())); + assertEquals(SigningAlgorithm.ES256K, algCaptor.getValue()); + } + + @Test + void shouldReturnCorrectSigningAlgorithmsPriorityOrder() { + assertEquals(4, service.getSigningAlgorithmsPriorityOrder().size()); + assertTrue(service.getSigningAlgorithmsPriorityOrder().contains("ED25519")); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/V1VCDownloadHandlerTest.java b/src/test/java/io/mosip/mimoto/service/V1VCDownloadHandlerTest.java new file mode 100644 index 000000000..ccee06b03 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/V1VCDownloadHandlerTest.java @@ -0,0 +1,403 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.mimoto.V1Credential; +import io.mosip.mimoto.exception.CredentialProcessingException; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidCredentialResourceException; +import io.mosip.mimoto.service.impl.V1VCDownloadHandler; +import io.mosip.mimoto.util.RestApiClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class V1VCDownloadHandlerTest { + + private static final String CREDENTIAL_ISSUER = "https://credential-issuer.example.com"; + private static final String CREDENTIAL_ENDPOINT = "https://credential-issuer.example.com/credential"; + private static final String NONCE_ENDPOINT = "https://credential-issuer.example.com/nonce"; + private static final String CREDENTIAL_CONFIG_ID = "SD_JWT_VC_example_in_OpenID4VCI"; + private static final String FORMAT = "dc+sd-jwt"; + private static final String ISSUER_ID = "issuer-123"; + private static final String CLIENT_ID = "mimoto-oidc-client"; + private static final String ACCESS_TOKEN = "valid-access-token"; + private static final String WALLET_ID = "wallet-1"; + private static final String BASE64_KEY = "base64-encoded-wallet-key"; + + @Mock + private V1CredentialRequestService v1CredentialRequestService; + @Mock + private RestApiClient restApiClient; + @InjectMocks + private V1VCDownloadHandler handler; + + private IssuerDTO issuerDTO; + private CredentialIssuerWellKnownResponse wellKnownResponse; + private TokenResponseDTO tokenResponse; + + @BeforeEach + void setUp() { + issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id(ISSUER_ID); + issuerDTO.setClient_id(CLIENT_ID); + + ProofTypesSupported proofTypesSupported = new ProofTypesSupported(); + proofTypesSupported.setProofSigningAlgValuesSupported(List.of("ES256")); + + CredentialsSupportedResponse credSupported = new CredentialsSupportedResponse(); + credSupported.setFormat(FORMAT); + credSupported.setScope(CREDENTIAL_CONFIG_ID); + credSupported.setVct(CREDENTIAL_CONFIG_ID); + credSupported.setProofTypesSupported(Map.of("jwt", proofTypesSupported)); + + wellKnownResponse = new CredentialIssuerWellKnownResponse(); + wellKnownResponse.setCredentialIssuer(CREDENTIAL_ISSUER); + wellKnownResponse.setCredentialEndPoint(CREDENTIAL_ENDPOINT); + wellKnownResponse.setNonceEndpoint(NONCE_ENDPOINT); + wellKnownResponse.setCredentialConfigurationsSupported(Map.of(CREDENTIAL_CONFIG_ID, credSupported)); + + tokenResponse = new TokenResponseDTO(); + tokenResponse.setAccess_token(ACCESS_TOKEN); + } + + private V1VCCredentialRequest buildRequest(String jwtToken) { + return V1VCCredentialRequest.builder() + .credentialConfigurationId(CREDENTIAL_CONFIG_ID) + .proofs(Map.of("jwt", List.of(jwtToken))) + .build(); + } + + private V1Credential v1Credential(Object credentialValue) { + V1Credential cred = new V1Credential(); + cred.setCredential(credentialValue); + return cred; + } + + @Test + void shouldReturnVCCredentialResponseOnSuccessfulDownload() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(eq(issuerDTO), eq(CREDENTIAL_CONFIG_ID), + eq(wellKnownResponse), isNull(), isNull(), eq(false))) + .thenReturn(request); + + V1VCCredentialResponse mockResponse = V1VCCredentialResponse.builder() + .credentials(List.of(v1Credential("eyJhbGciOiJFUzI1NiJ9.credential-payload.signature"))) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(mockResponse); + + VCCredentialResponse result = handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false); + + assertNotNull(result); + assertEquals(FORMAT, result.getFormat()); + assertEquals("eyJhbGciOiJFUzI1NiJ9.credential-payload.signature", result.getCredential()); + } + + @Test + void shouldReturnFirstCredentialWhenResponseContainsMultiple() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(eq(issuerDTO), eq(CREDENTIAL_CONFIG_ID), + eq(wellKnownResponse), isNull(), isNull(), eq(false))) + .thenReturn(request); + + V1VCCredentialResponse mockResponse = V1VCCredentialResponse.builder() + .credentials(List.of(v1Credential("first-credential"), v1Credential("second-credential"), v1Credential("third-credential"))) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(mockResponse); + + VCCredentialResponse result = handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false); + + assertNotNull(result); + assertEquals("first-credential", result.getCredential()); + } + + @Test + void shouldPassWalletIdAndBase64KeyForLoginFlow() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-login-token"); + + when(v1CredentialRequestService.buildRequest(eq(issuerDTO), eq(CREDENTIAL_CONFIG_ID), + eq(wellKnownResponse), eq(WALLET_ID), eq(BASE64_KEY), eq(true))) + .thenReturn(request); + + V1VCCredentialResponse mockResponse = V1VCCredentialResponse.builder() + .credentials(List.of(v1Credential("login-credential-data"))) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(mockResponse); + + VCCredentialResponse result = handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, WALLET_ID, BASE64_KEY, true); + + assertNotNull(result); + assertEquals(FORMAT, result.getFormat()); + assertEquals("login-credential-data", result.getCredential()); + verify(v1CredentialRequestService).buildRequest(eq(issuerDTO), eq(CREDENTIAL_CONFIG_ID), + eq(wellKnownResponse), eq(WALLET_ID), eq(BASE64_KEY), eq(true)); + } + + @Test + void shouldThrowCredentialProcessingExceptionWhenBuildRequestFails() throws Exception { + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenThrow(new RuntimeException("Build failed")); + + assertThrows(CredentialProcessingException.class, () -> + handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + } + + @Test + void shouldRetryWithFreshNonceWhenFirstAttemptReturnsInvalidNonce() throws Exception { + V1VCCredentialRequest firstRequest = buildRequest("jwt-token-1"); + V1VCCredentialRequest retryRequest = buildRequest("jwt-token-2"); + + when(v1CredentialRequestService.buildRequest(eq(issuerDTO), eq(CREDENTIAL_CONFIG_ID), + eq(wellKnownResponse), isNull(), isNull(), eq(false))) + .thenReturn(firstRequest) + .thenReturn(retryRequest); + + V1VCCredentialResponse invalidNonceResponse = V1VCCredentialResponse.builder() + .error("invalid_nonce") + .errorDescription("Nonce Transaction could not be found.") + .build(); + + V1VCCredentialResponse successResponse = V1VCCredentialResponse.builder() + .credentials(List.of(v1Credential("retry-credential-data"))) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(firstRequest), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(invalidNonceResponse); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(retryRequest), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(successResponse); + + VCCredentialResponse result = handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false); + + assertNotNull(result); + assertEquals(FORMAT, result.getFormat()); + assertEquals("retry-credential-data", result.getCredential()); + verify(v1CredentialRequestService, times(2)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + } + + @Test + void shouldThrowExceptionWhenRetryAlsoReturnsInvalidNonce() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse invalidNonceResponse = V1VCCredentialResponse.builder() + .error("invalid_nonce") + .errorDescription("Nonce Transaction could not be found.") + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(invalidNonceResponse); + + ExternalServiceUnavailableException exception = assertThrows( + ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + assertTrue(exception.getMessage().contains(ISSUER_ID)); + assertTrue(exception.getMessage().contains("invalid_nonce")); + } + + @Test + void shouldThrowExceptionWithoutRetryWhenNoNonceEndpoint() throws Exception { + wellKnownResponse.setNonceEndpoint(null); + + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse invalidNonceResponse = V1VCCredentialResponse.builder() + .error("invalid_nonce") + .errorDescription("Nonce Transaction could not be found.") + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(invalidNonceResponse); + + assertThrows(ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + verify(v1CredentialRequestService, times(1)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + } + + @Test + void shouldThrowExceptionWithoutRetryWhenNonceEndpointIsBlank() throws Exception { + wellKnownResponse.setNonceEndpoint(" "); + + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse invalidNonceResponse = V1VCCredentialResponse.builder() + .error("invalid_nonce") + .errorDescription("Nonce Transaction could not be found.") + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(invalidNonceResponse); + + assertThrows(ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + verify(v1CredentialRequestService, times(1)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + } + + @Test + void shouldThrowInvalidCredentialResourceExceptionWhenCredentialsListIsNull() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse mockResponse = V1VCCredentialResponse.builder() + .credentials(null) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(mockResponse); + + InvalidCredentialResourceException exception = assertThrows( + InvalidCredentialResourceException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + assertTrue(exception.getMessage().contains("Credential response did not contain any credentials")); + } + + @Test + void shouldThrowInvalidCredentialResourceExceptionWhenCredentialsListIsEmpty() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse mockResponse = V1VCCredentialResponse.builder() + .credentials(List.of()) + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(mockResponse); + + InvalidCredentialResourceException exception = assertThrows( + InvalidCredentialResourceException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + assertTrue(exception.getMessage().contains("Credential response did not contain any credentials")); + } + + @Test + void shouldThrowExceptionWhenResponseIsNull() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + when(restApiClient.postApiWithErrorResponse(anyString(), any(), any(), any(), anyString())) + .thenReturn(null); + + ExternalServiceUnavailableException exception = assertThrows( + ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + assertTrue(exception.getMessage().contains(ISSUER_ID)); + assertTrue(exception.getMessage().contains("no response")); + verify(v1CredentialRequestService, times(1)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + } + + @Test + void shouldNotRetryForNonNonceErrors() throws Exception { + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + V1VCCredentialResponse errorResponse = V1VCCredentialResponse.builder() + .error("invalid_token") + .errorDescription("The access token is expired") + .build(); + + when(restApiClient.postApiWithErrorResponse(eq(CREDENTIAL_ENDPOINT), eq(MediaType.APPLICATION_JSON), + eq(request), eq(V1VCCredentialResponse.class), eq(ACCESS_TOKEN))) + .thenReturn(errorResponse); + + ExternalServiceUnavailableException exception = assertThrows( + ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + assertTrue(exception.getMessage().contains("invalid_token")); + verify(v1CredentialRequestService, times(1)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + verify(restApiClient, times(1)) + .postApiWithErrorResponse(anyString(), any(), any(), any(), anyString()); + } + + @Test + void shouldNotRetryWhenResponseIsNullAndNoNonceEndpoint() throws Exception { + wellKnownResponse.setNonceEndpoint(null); + + V1VCCredentialRequest request = buildRequest("jwt-token"); + + when(v1CredentialRequestService.buildRequest(any(), anyString(), any(), any(), any(), anyBoolean())) + .thenReturn(request); + + when(restApiClient.postApiWithErrorResponse(anyString(), any(), any(), any(), anyString())) + .thenReturn(null); + + assertThrows(ExternalServiceUnavailableException.class, + () -> handler.downloadCredential(issuerDTO, CREDENTIAL_CONFIG_ID, + wellKnownResponse, tokenResponse, null, null, false)); + + verify(v1CredentialRequestService, times(1)) + .buildRequest(any(), anyString(), any(), any(), any(), anyBoolean()); + verify(restApiClient, times(1)) + .postApiWithErrorResponse(anyString(), any(), any(), any(), anyString()); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/VCDownloadHandlerFactoryTest.java b/src/test/java/io/mosip/mimoto/service/VCDownloadHandlerFactoryTest.java new file mode 100644 index 000000000..6e650d9d6 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/VCDownloadHandlerFactoryTest.java @@ -0,0 +1,49 @@ +package io.mosip.mimoto.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.HashMap; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.Mock; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +@ExtendWith(MockitoExtension.class) +class VCDownloadHandlerFactoryTest { + @Mock + private VCDownloadHandler mockDraft13Handler; + + private VCDownloadHandlerFactory factory; + + @BeforeEach + void setUp() { + Map handlers = new HashMap<>(); + handlers.put(VCSpecificationVersion.DRAFT_13.getVersion(), mockDraft13Handler); + factory = new VCDownloadHandlerFactory(handlers); + } + + @Test + void shouldReturnHandlerForValidVersion() { + VCDownloadHandler handler = factory.getHandler(VCSpecificationVersion.DRAFT_13); + assertNotNull(handler); + assertEquals(mockDraft13Handler, handler); + } + + @Test + void shouldThrowExceptionForUnsupportedVersion() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> factory.getHandler(VCSpecificationVersion.V1)); + assertEquals("Unsupported download version: V1", exception.getMessage()); + } + + @Test + void shouldThrowExceptionForNullVersion() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> factory.getHandler(null)); + assertEquals("Version cannot be null", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/VcSdJwtCredentialFormatHandlerTest.java b/src/test/java/io/mosip/mimoto/service/VcSdJwtCredentialFormatHandlerTest.java new file mode 100644 index 000000000..82fb0626e --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/VcSdJwtCredentialFormatHandlerTest.java @@ -0,0 +1,1183 @@ +package io.mosip.mimoto.service; + +import com.authlete.sd.Disclosure; +import com.authlete.sd.SDJWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.service.impl.VcSdJwtCredentialFormatHandler; +import io.mosip.mimoto.util.JwtUtils; +import io.mosip.mimoto.util.LocaleUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class VcSdJwtCredentialFormatHandlerTest { + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private VcSdJwtCredentialFormatHandler vcSdJwtCredentialFormatHandler; + + private VCCredentialResponse vcCredentialResponse; + private CredentialsSupportedResponse credentialsSupportedResponse; + private String sampleSdJwtString; + private String sampleJwtString; + + @BeforeEach + void setUp() { + vcCredentialResponse = new VCCredentialResponse(); + credentialsSupportedResponse = new CredentialsSupportedResponse(); + + // Sample SD-JWT string (simplified for testing) + sampleSdJwtString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.invalid"; + + // Sample JWT string for payload parsing + sampleJwtString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.invalid"; + } + + @Test + void extractCredentialClaimsWithStringCredentialShouldReturnClaims() { + vcCredentialResponse.setCredential(sampleSdJwtString); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(new ArrayList<>()); + + // Mock JWT payload with credentialSubject + Map jwtPayload = new HashMap<>(); + jwtPayload.put("name", "John Doe"); + jwtPayload.put("admin", true); + jwtPayload.put("iss", "https://example.com"); + jwtPayload.put("sub", "1234567890"); + jwtPayload.put("iat", 1516239022); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)) + .thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + assertNotNull(result); + assertEquals("John Doe", result.get("name")); + assertEquals(true, result.get("admin")); + // Metadata fields should be removed + assertFalse(result.containsKey("iss")); + assertFalse(result.containsKey("sub")); + assertFalse(result.containsKey("iat")); + } + } + + @Test + void extractCredentialClaimsWithoutCredentialSubjectShouldReturnAllClaims() { + vcCredentialResponse.setCredential(sampleSdJwtString); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(new ArrayList<>()); + + // Mock JWT payload without credentialSubject + Map jwtPayload = new HashMap<>(); + jwtPayload.put("name", "John Doe"); + jwtPayload.put("admin", true); + jwtPayload.put("iss", "https://example.com"); + jwtPayload.put("sub", "1234567890"); + jwtPayload.put("iat", 1516239022); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)) + .thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + assertNotNull(result); + assertEquals("John Doe", result.get("name")); + assertEquals(true, result.get("admin")); + // Metadata fields should be removed + assertFalse(result.containsKey("iss")); + assertFalse(result.containsKey("sub")); + assertFalse(result.containsKey("iat")); + } + } + + @Test + void extractCredentialClaimsWithNonStringCredentialShouldReturnEmptyMap() { + // Given + vcCredentialResponse.setCredential(new Object()); + + // When + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void extractCredentialClaimsWithInvalidSdJwtShouldReturnEmptyMap() { + // Given + vcCredentialResponse.setCredential("invalid-sd-jwt"); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class)) { + mockedSdJwt.when(() -> SDJWT.parse("invalid-sd-jwt")) + .thenThrow(new IllegalArgumentException("Invalid SD-JWT")); + + // When + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + void extractCredentialClaimsWithDisclosuresShouldIncludeDisclosedClaims() { + vcCredentialResponse.setCredential(sampleSdJwtString); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + Disclosure mockDisclosure = mock(Disclosure.class); + List disclosures = Arrays.asList(mockDisclosure); + + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(disclosures); + when(mockDisclosure.getClaimName()).thenReturn("disclosedClaim"); + when(mockDisclosure.getClaimValue()).thenReturn("disclosedValue"); + + // Mock JWT payload + Map jwtPayload = new HashMap<>(); + jwtPayload.put("name", "John Doe"); + jwtPayload.put("iss", "https://example.com"); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)) + .thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + assertNotNull(result); + assertEquals("John Doe", result.get("name")); + assertEquals("disclosedValue", result.get("disclosedClaim")); + // Metadata fields should be removed + assertFalse(result.containsKey("iss")); + } + } + + @Test + void extractCredentialClaimsWithNullCredentialJwtShouldHandleGracefully() { + vcCredentialResponse.setCredential(sampleSdJwtString); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class)) { + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(null); + when(mockSdJwt.getDisclosures()).thenReturn(new ArrayList<>()); + + Map result = vcSdJwtCredentialFormatHandler.extractCredentialClaims(vcCredentialResponse); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithValidDataShouldReturnDisplayProperties() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John Doe"); + credentialProperties.put("age", 30); + + Map claims = createSampleClaims(); + credentialsSupportedResponse.setClaims(claims); + credentialsSupportedResponse.setOrder(Arrays.asList("name", "age")); + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("Name", "en"); + CredentialDisplayResponseDto ageDto = createCredentialDisplayResponseDto("Age", "en"); + + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto, ageDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("name")); + assertTrue(result.containsKey("age")); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithNestedClaimsShouldHandleCorrectly() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John Doe"); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("name", new HashMap<>()); + + Map wrappedClaims = new HashMap<>(); + wrappedClaims.put("wrapper", nestedClaims); + + credentialsSupportedResponse.setClaims(wrappedClaims); + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("Name", "en"); + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("name")); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullClaimsShouldUseConvertedLabel() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("UINValue", "12345"); + + credentialsSupportedResponse.setClaims(null); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("firstName")); + assertTrue(result.containsKey("UINValue")); + + // Check that fallback display for "firstName" + Map displayMap = result.get("firstName"); + CredentialIssuerDisplayResponse display = displayMap.keySet().iterator().next(); + assertEquals("First Name", display.getName()); // convertKeyToLabel should convert camelCase to Pascal Case + assertEquals("en", display.getLocale()); + assertEquals("John", displayMap.get(display)); + + // Check fallback display for "UINValue" + Map uinValueDisplayMap = result.get("UINValue"); + CredentialIssuerDisplayResponse uinValueDisplay = uinValueDisplayMap.keySet().iterator().next(); + assertEquals("UIN Value", uinValueDisplay.getName()); // convertKeyToLabel should convert camelCase to Pascal Case + assertEquals("en", uinValueDisplay.getLocale()); + assertEquals("12345", uinValueDisplayMap.get(uinValueDisplay)); + + } + + @Test + void loadDisplayPropertiesFromWellknownWithNullResolvedLocaleShouldUseConvertedLabel() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John Doe"); + + Map claims = createSampleClaims(); + credentialsSupportedResponse.setClaims(claims); + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("firstName", "en"); + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("fr"))) + .thenReturn(null); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "fr"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + + // Check that fallback display was created + Map displayMap = result.get("firstName"); + CredentialIssuerDisplayResponse display = displayMap.keySet().iterator().next(); + assertEquals("First Name", display.getName()); + assertEquals("en", display.getLocale()); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithCustomOrderShouldRespectOrder() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John Doe"); + credentialProperties.put("age", 30); + + Map claims = createSampleClaims(); + credentialsSupportedResponse.setClaims(claims); + credentialsSupportedResponse.setOrder(Arrays.asList("age", "name")); // Custom order + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("Name", "en"); + CredentialDisplayResponseDto ageDto = createCredentialDisplayResponseDto("Age", "en"); + + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto, ageDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + List keyOrder = new ArrayList<>(result.keySet()); + assertEquals("age", keyOrder.get(0)); + assertEquals("name", keyOrder.get(1)); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWithCustomOrderShouldRespectOrderAndAppendAdditionalFieldsNotPresentInWellknownClaimsAtEnd() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John Doe"); + credentialProperties.put("age", 30); + credentialProperties.put("dob", "1990-01-01"); // Additional field not present in well-known claims + credentialProperties.put("email", "xyz@gmail.com"); // Additional field not present in well-known claims + + Map claims = createSampleClaims(); + credentialsSupportedResponse.setClaims(claims); + credentialsSupportedResponse.setOrder(Arrays.asList("age", "name")); // Custom order + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("Name", "en"); + CredentialDisplayResponseDto ageDto = createCredentialDisplayResponseDto("Age", "en"); + CredentialDisplayResponseDto dobDto = createCredentialDisplayResponseDto("DOB", "en"); + CredentialDisplayResponseDto emailDto = createCredentialDisplayResponseDto("Email", "en"); + + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto, dobDto, emailDto, ageDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(4, result.size()); + List keyOrder = new ArrayList<>(result.keySet()); + assertEquals("age", keyOrder.get(0)); + assertEquals("name", keyOrder.get(1)); + assertEquals("dob", keyOrder.get(2)); // Additional field should be appended at the end + assertEquals("email", keyOrder.get(3)); // Additional field should be appended at the end + } + } + + @Test + void getSupportedFormatShouldReturnCorrectFormat() { + // When + String result = vcSdJwtCredentialFormatHandler.getSupportedFormat(); + + // Then + assertEquals(CredentialFormat.VC_SD_JWT.getFormat(), result); + } + + @Test + void buildFallbackDisplayPropertiesWithOrderedKeysShouldRespectOrder() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + credentialProperties.put("email", "john@example.com"); + + List orderedKeys = Arrays.asList("email", "lastName", "firstName"); + credentialsSupportedResponse.setOrder(orderedKeys); + credentialsSupportedResponse.setClaims(null); // Trigger fallback + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(3, result.size()); + + // Verify order is preserved + List resultKeys = new ArrayList<>(result.keySet()); + assertEquals("email", resultKeys.get(0)); + assertEquals("lastName", resultKeys.get(1)); + assertEquals("firstName", resultKeys.get(2)); + } + + @Test + void buildFallbackDisplayPropertiesWithNullValuesShouldSkipFields() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("middleName", null); + credentialProperties.put("lastName", "Doe"); + + credentialsSupportedResponse.setClaims(null); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); // middleName should be skipped + assertTrue(result.containsKey("firstName")); + assertTrue(result.containsKey("lastName")); + assertFalse(result.containsKey("middleName")); + } + + @Test + void buildFallbackDisplayPropertiesWithIdFieldShouldExcludeIt() { + // Given + Map credentialProperties = new LinkedHashMap<>(); + credentialProperties.put("id", "12345"); + credentialProperties.put("firstName", "John"); + + credentialsSupportedResponse.setClaims(null); + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("firstName")); + } + + @Test + void buildFallbackDisplayPropertiesWithEmptyClaimsShouldTriggerFallback() { + // Given + Map credentialProperties = new HashMap<>(); + credentialProperties.put("firstName", "John"); + credentialProperties.put("lastName", "Doe"); + + credentialsSupportedResponse.setClaims(new HashMap<>()); // Empty claims + + // When + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + + CredentialIssuerDisplayResponse firstNameDisplay = result.get("firstName").keySet().iterator().next(); + assertEquals("First Name", firstNameDisplay.getName()); + assertEquals("en", firstNameDisplay.getLocale()); + + CredentialIssuerDisplayResponse lastNameDisplay = result.get("lastName").keySet().iterator().next(); + assertEquals("Last Name", lastNameDisplay.getName()); + assertEquals("en", lastNameDisplay.getLocale()); + } + + // ============================================================ + // Tests for extractAllCredentialProperties + // ============================================================ + + @Test + void extractAllCredentialPropertiesWhenCredentialIsStringReturnsProperties() { + vcCredentialResponse.setCredential(sampleSdJwtString); + + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(null); + + Map jwtPayload = new LinkedHashMap<>(); + jwtPayload.put("name", "John"); + jwtPayload.put("iss", "https://example.com"); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(jwtPayload); + + Map> result = (Map>) + vcSdJwtCredentialFormatHandler.extractAllCredentialProperties(vcCredentialResponse); + + assertNotNull(result); + assertTrue(result.containsKey("publicClaims")); + assertTrue(result.containsKey("sdClaims")); + assertEquals("John", result.get("publicClaims").get("name")); + } + } + + @Test + void extractAllCredentialPropertiesWhenCredentialIsNotStringReturnsEmptyMap() { + vcCredentialResponse.setCredential(new HashMap<>()); + + Map result = (Map) vcSdJwtCredentialFormatHandler.extractAllCredentialProperties(vcCredentialResponse); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + // ============================================================ + // Tests for extractAllPropertiesFromSdJwt + // ============================================================ + + @Test + void extractAllPropertiesFromSdJwtWhenIllegalArgExceptionReturnsEmptyMap() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class)) { + mockedSdJwt.when(() -> SDJWT.parse("bad-jwt")) + .thenThrow(new IllegalArgumentException("Invalid SD-JWT")); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt("bad-jwt"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWhenGenericExceptionReturnsEmptyMap() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class)) { + mockedSdJwt.when(() -> SDJWT.parse("bad-jwt")) + .thenThrow(new RuntimeException("Unexpected")); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt("bad-jwt"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWhenNullPayloadReturnsSdClaimsEmpty() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(null); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(null); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.get("publicClaims").isEmpty()); + assertTrue(result.get("sdClaims").isEmpty()); + } + } + + // ============================================================ + // Tests for resolveDisclosures via extractAllPropertiesFromSdJwt + // ============================================================ + + @Test + void extractAllPropertiesFromSdJwtWithSdDigestsInPayloadResolvesSdClaims() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.digest()).thenReturn("digest1"); + when(disclosure.getDisclosure()).thenReturn("base64disclosure1"); + when(disclosure.getClaimName()).thenReturn("given_name"); + when(disclosure.getClaimValue()).thenReturn("John"); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + Map payload = new LinkedHashMap<>(); + payload.put("_sd", Arrays.asList("digest1")); + payload.put("iss", "https://example.com"); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertTrue(sdClaims.containsKey("given_name")); + assertEquals("John", result.get("sdClaimValues").get("given_name")); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithNullDisclosureForDigestSkipsDigest() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(Collections.emptyList()); + + Map payload = new LinkedHashMap<>(); + payload.put("_sd", Arrays.asList("unknownDigest")); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.get("sdClaims").isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWhenClaimNameIsSdOrKeySkipsDigest() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure sdDisclosure = mock(Disclosure.class); + when(sdDisclosure.digest()).thenReturn("digest_sd"); + when(sdDisclosure.getDisclosure()).thenReturn("b64sd"); + when(sdDisclosure.getClaimName()).thenReturn("_sd"); + + Disclosure keyDisclosure = mock(Disclosure.class); + when(keyDisclosure.digest()).thenReturn("digest_key"); + when(keyDisclosure.getDisclosure()).thenReturn("b64key"); + when(keyDisclosure.getClaimName()).thenReturn("..."); + + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(sdDisclosure, keyDisclosure)); + + Map payload = new LinkedHashMap<>(); + payload.put("_sd", Arrays.asList("digest_sd", "digest_key")); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertFalse(sdClaims.containsKey("_sd")); + assertFalse(sdClaims.containsKey("...")); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithNestedMapContainingSdDigestsResolvesNestedPaths() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.digest()).thenReturn("nested_digest"); + when(disclosure.getDisclosure()).thenReturn("b64nested"); + when(disclosure.getClaimName()).thenReturn("city"); + when(disclosure.getClaimValue()).thenReturn("Berlin"); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + Map addressMap = new LinkedHashMap<>(); + addressMap.put("_sd", Arrays.asList("nested_digest")); + addressMap.put("country", "Germany"); + + Map payload = new LinkedHashMap<>(); + payload.put("address", addressMap); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertTrue(sdClaims.containsKey("address.city")); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithArrayDisclosureResolvesArrayItems() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.digest()).thenReturn("arr_digest"); + when(disclosure.getDisclosure()).thenReturn("b64arr"); + when(disclosure.getClaimValue()).thenReturn("disclosed_item"); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + // Array with a disclosure marker {"...": "arr_digest"} and a regular item + Map disclosureMarker = new LinkedHashMap<>(); + disclosureMarker.put("...", "arr_digest"); + + List arrayWithDisclosure = new ArrayList<>(); + arrayWithDisclosure.add(disclosureMarker); + arrayWithDisclosure.add("regular_item"); + + Map payload = new LinkedHashMap<>(); + payload.put("nationalities", arrayWithDisclosure); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertTrue(sdClaims.containsKey("nationalities[0]")); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithArrayDisclosureNotFoundSkipsMarker() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(Collections.emptyList()); + + Map disclosureMarker = new LinkedHashMap<>(); + disclosureMarker.put("...", "unknown_digest"); + + List arrayWithDisclosure = new ArrayList<>(); + arrayWithDisclosure.add(disclosureMarker); + + Map payload = new LinkedHashMap<>(); + payload.put("items", arrayWithDisclosure); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.get("sdClaims").isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithArrayContainingRegularMapRecursesIntoMap() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.digest()).thenReturn("inner_digest"); + when(disclosure.getDisclosure()).thenReturn("b64inner"); + when(disclosure.getClaimName()).thenReturn("secret"); + when(disclosure.getClaimValue()).thenReturn("value"); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + // Regular map (not a disclosure marker) inside a list, containing _sd + Map innerMap = new LinkedHashMap<>(); + innerMap.put("_sd", Arrays.asList("inner_digest")); + innerMap.put("visible", "yes"); + + List listValue = new ArrayList<>(); + listValue.add(innerMap); + + Map payload = new LinkedHashMap<>(); + payload.put("entries", listValue); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertTrue(sdClaims.containsKey("entries[0].secret")); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithEmptyPayloadReturnsSdClaimsEmpty() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(null); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)) + .thenReturn(new HashMap<>()); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.get("sdClaims").isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWhenDisclosureClaimNameNullSkipsDisclosure() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.digest()).thenReturn("digest_null_name"); + when(disclosure.getDisclosure()).thenReturn("b64null"); + when(disclosure.getClaimName()).thenReturn(null); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + Map payload = new LinkedHashMap<>(); + payload.put("_sd", Arrays.asList("digest_null_name")); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.get("sdClaims").isEmpty()); + } + } + + @Test + void extractAllPropertiesFromSdJwtWithFiveLevelsOfNestedDisclosuresResolvesAllLevels() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Map level5Value = new LinkedHashMap<>(); + level5Value.put("deep_key", "deep_value"); + + Disclosure d5 = mock(Disclosure.class); + when(d5.digest()).thenReturn("d5"); + when(d5.getDisclosure()).thenReturn("b64d5"); + when(d5.getClaimName()).thenReturn("l5"); + when(d5.getClaimValue()).thenReturn(level5Value); + + Map level4Value = new LinkedHashMap<>(); + level4Value.put("_sd", Arrays.asList("d5")); + + Disclosure d4 = mock(Disclosure.class); + when(d4.digest()).thenReturn("d4"); + when(d4.getDisclosure()).thenReturn("b64d4"); + when(d4.getClaimName()).thenReturn("l4"); + when(d4.getClaimValue()).thenReturn(level4Value); + + Map level3Value = new LinkedHashMap<>(); + level3Value.put("_sd", Arrays.asList("d4")); + + Disclosure d3 = mock(Disclosure.class); + when(d3.digest()).thenReturn("d3"); + when(d3.getDisclosure()).thenReturn("b64d3"); + when(d3.getClaimName()).thenReturn("l3"); + when(d3.getClaimValue()).thenReturn(level3Value); + + Map level2Value = new LinkedHashMap<>(); + level2Value.put("_sd", Arrays.asList("d3")); + + Disclosure d2 = mock(Disclosure.class); + when(d2.digest()).thenReturn("d2"); + when(d2.getDisclosure()).thenReturn("b64d2"); + when(d2.getClaimName()).thenReturn("l2"); + when(d2.getClaimValue()).thenReturn(level2Value); + + Map level1Value = new LinkedHashMap<>(); + level1Value.put("_sd", Arrays.asList("d2")); + + Disclosure d1 = mock(Disclosure.class); + when(d1.digest()).thenReturn("d1"); + when(d1.getDisclosure()).thenReturn("b64d1"); + when(d1.getClaimName()).thenReturn("l1"); + when(d1.getClaimValue()).thenReturn(level1Value); + + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(d1, d2, d3, d4, d5)); + + Map payload = new LinkedHashMap<>(); + payload.put("_sd", Arrays.asList("d1")); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(payload); + + Map> result = vcSdJwtCredentialFormatHandler.extractAllPropertiesFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + Map sdClaims = result.get("sdClaims"); + assertTrue(sdClaims.containsKey("l1")); + assertTrue(sdClaims.containsKey("l1.l2")); + assertTrue(sdClaims.containsKey("l1.l2.l3")); + assertTrue(sdClaims.containsKey("l1.l2.l3.l4")); + assertTrue(sdClaims.containsKey("l1.l2.l3.l4.l5")); + assertEquals(5, sdClaims.size()); + } + } + + // ============================================================ + // Tests for extractSdClaims uncovered branches (via extractClaimsFromSdJwt) + // ============================================================ + + @Test + void extractClaimsFromSdJwtWhenDisclosuresNullReturnsPublicClaimsOnly() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(null); + + Map jwtPayload = new LinkedHashMap<>(); + jwtPayload.put("name", "John"); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertEquals("John", result.get("name")); + } + } + + @Test + void extractClaimsFromSdJwtWhenDisclosureClaimNameNullSkipsDisclosure() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.getClaimName()).thenReturn(null); + when(disclosure.getClaimValue()).thenReturn("someValue"); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + Map jwtPayload = new LinkedHashMap<>(); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertFalse(result.containsValue("someValue")); + } + } + + @Test + void extractClaimsFromSdJwtWhenDisclosureClaimValueNullSkipsDisclosure() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure disclosure = mock(Disclosure.class); + when(disclosure.getClaimName()).thenReturn("age"); + when(disclosure.getClaimValue()).thenReturn(null); + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(disclosure)); + + Map jwtPayload = new LinkedHashMap<>(); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertFalse(result.containsKey("age")); + } + } + + @Test + void extractClaimsFromSdJwtWhenDisclosureThrowsExceptionSkipsAndContinues() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + + Disclosure badDisclosure = mock(Disclosure.class); + when(badDisclosure.getClaimName()).thenThrow(new RuntimeException("decode error")); + + Disclosure goodDisclosure = mock(Disclosure.class); + when(goodDisclosure.getClaimName()).thenReturn("name"); + when(goodDisclosure.getClaimValue()).thenReturn("Alice"); + + when(mockSdJwt.getDisclosures()).thenReturn(Arrays.asList(badDisclosure, goodDisclosure)); + + Map jwtPayload = new LinkedHashMap<>(); + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(jwtPayload); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertEquals("Alice", result.get("name")); + } + } + + @Test + void extractClaimsFromSdJwtWhenGenericExceptionReturnsEmptyMap() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class)) { + mockedSdJwt.when(() -> SDJWT.parse("crash-jwt")) + .thenThrow(new RuntimeException("Unexpected crash")); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt("crash-jwt"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + void extractClaimsFromSdJwtWhenJwtPayloadNullReturnsEmptyPublicClaims() { + try (MockedStatic mockedSdJwt = mockStatic(SDJWT.class); + MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + + SDJWT mockSdJwt = mock(SDJWT.class); + mockedSdJwt.when(() -> SDJWT.parse(sampleSdJwtString)).thenReturn(mockSdJwt); + when(mockSdJwt.getCredentialJwt()).thenReturn(sampleJwtString); + when(mockSdJwt.getDisclosures()).thenReturn(Collections.emptyList()); + + mockedJwtUtils.when(() -> JwtUtils.parseJwtPayload(sampleJwtString)).thenReturn(null); + + Map result = vcSdJwtCredentialFormatHandler.extractClaimsFromSdJwt(sampleSdJwtString); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + // ============================================================ + // Tests for loadDisplayPropertiesFromWellknown uncovered branches + // ============================================================ + + @Test + void loadDisplayPropertiesFromWellknownWhenValueNullInOrderedKeysSkipsField() { + // Arrange - credentialProperties has a key with null value + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John"); + credentialProperties.put("address", null); + + Map claims = new HashMap<>(); + claims.put("name", new HashMap<>()); + claims.put("address", new HashMap<>()); + credentialsSupportedResponse.setClaims(claims); + credentialsSupportedResponse.setOrder(Arrays.asList("name", "address")); + + CredentialDisplayResponseDto nameDto = createCredentialDisplayResponseDto("Name", "en"); + CredentialDisplayResponseDto addressDto = createCredentialDisplayResponseDto("Address", "en"); + when(objectMapper.convertValue(any(), eq(CredentialDisplayResponseDto.class))) + .thenReturn(nameDto, addressDto); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey("name")); + assertFalse(result.containsKey("address")); + } + } + + @Test + void loadDisplayPropertiesFromWellknownWhenDisplayNullFallsBackToGeneratedDisplay() { + // Arrange - orderedKeys has a key not in claims config (display not found in localizedDisplayMap) + Map credentialProperties = new HashMap<>(); + credentialProperties.put("name", "John"); + credentialProperties.put("extraField", "extraValue"); + + Map claims = new HashMap<>(); + claims.put("name", new HashMap<>()); + credentialsSupportedResponse.setClaims(claims); + credentialsSupportedResponse.setOrder(Arrays.asList("name", "extraField")); + + try (MockedStatic mockedLocaleUtils = mockStatic(LocaleUtils.class)) { + mockedLocaleUtils.when(() -> LocaleUtils.resolveLocaleWithFallback(any(), eq("en"))) + .thenReturn("en"); + mockedLocaleUtils.when(() -> LocaleUtils.matchesLocale(eq("en"), eq("en"))) + .thenReturn(true); + + LinkedHashMap> result = + vcSdJwtCredentialFormatHandler.loadDisplayPropertiesFromWellknown( + credentialProperties, credentialsSupportedResponse, "en"); + + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("extraField")); + + CredentialIssuerDisplayResponse extraDisplay = result.get("extraField").keySet().iterator().next(); + assertEquals("Extra Field", extraDisplay.getName()); + assertEquals("en", extraDisplay.getLocale()); + } + } + + // Helper methods + private Map createSampleClaims() { + Map claims = new HashMap<>(); + claims.put("name", new HashMap<>()); + claims.put("age", new HashMap<>()); + return claims; + } + + private CredentialDisplayResponseDto createCredentialDisplayResponseDto(String name, String locale) { + CredentialDisplayResponseDto dto = new CredentialDisplayResponseDto(); + CredentialIssuerDisplayResponse display = new CredentialIssuerDisplayResponse(); + display.setName(name); + display.setLocale(locale); + dto.setDisplay(Arrays.asList(display)); + return dto; + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/VerifierServiceTest.java b/src/test/java/io/mosip/mimoto/service/VerifierServiceTest.java new file mode 100644 index 000000000..c724c80ee --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/VerifierServiceTest.java @@ -0,0 +1,439 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.dto.openid.presentation.PresentationRequestDTO; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.InvalidVerifierException; +import io.mosip.mimoto.repository.VerifierRepository; +import io.mosip.mimoto.service.impl.VerifierServiceImpl; +import io.mosip.mimoto.util.TestUtilities; +import io.mosip.mimoto.util.Utilities; +import io.mosip.openID4VP.authorizationRequest.Verifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class VerifierServiceTest { + + @Mock + Utilities utilities; + @Mock + ObjectMapper objectMapper; + @InjectMocks + VerifierServiceImpl verifiersService; + + @Mock + VerifierRepository verifierRepository; + + private static final String VALID_CLIENT_ID = "https://injiverify.collab.mosip.net"; + private static final String ENCODED_CLIENT_ID = "https%3A%2F%2Finjiverify.collab.mosip.net"; + private static final String VALID_RESPONSE_URI = "https://example.com/callback"; + private static final String ENCODED_RESPONSE_URI = "https%3A%2F%2Fexample.com%2Fcallback"; + + @Before + public void setUp() throws JsonProcessingException { + VerifiersDTO verifiersDTO = TestUtilities.getTrustedVerifiers(); + String verifiersListString = TestUtilities.getObjectAsString(verifiersDTO); + when(utilities.getTrustedVerifiersJsonValue()).thenReturn(verifiersListString); + when(objectMapper.readValue(eq(verifiersListString), eq(VerifiersDTO.class))).thenReturn(verifiersDTO); + } + + @Test + public void shouldReturnAllTrustedIssuers() throws ApiNotAccessibleException, JsonProcessingException { + VerifierDTO verifierDTO = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("https://test-responseUri")).build(); + VerifiersDTO expectedTrustedVerifiers = VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierDTO)).build(); + + VerifiersDTO actualTrustedVerifiers = verifiersService.getTrustedVerifiers(); + + assertNotNull(actualTrustedVerifiers); + assertEquals(actualTrustedVerifiers, expectedTrustedVerifiers); + } + + @Test + public void getCorrectVerifierWhenCorrectClientIdIsPassed() throws ApiNotAccessibleException, IOException { + Optional verifierDTO = verifiersService.getVerifierByClientId("test-clientId"); + assertNotNull(verifierDTO.get()); + assertEquals(verifierDTO.get().getClientId(), "test-clientId"); + } + + @Test + public void getNullWhenInvalidClientIdIsPassed() throws ApiNotAccessibleException, IOException { + Optional verifierDTO = verifiersService.getVerifierByClientId("test-clientId2"); + assertTrue(verifierDTO.isEmpty()); + } + + @Test + public void getVerifierByClientIdReturnsRegisteredRedirectUrisForClient() throws ApiNotAccessibleException, IOException { + Optional verifier = verifiersService.getVerifierByClientId("test-clientId"); + + assertTrue(verifier.isPresent()); + assertEquals(Collections.singletonList("https://test-redirectUri"), verifier.get().getRedirectUris()); + } + + @Test + public void shouldThrowApiNotAccessibleExceptionOnFetchingTrustedVerifiersListFailure() { + when(utilities.getTrustedVerifiersJsonValue()).thenReturn(null); + String expectedExceptionMsg = "RESIDENT-APP-026 --> Api not accessible failure"; + + ApiNotAccessibleException actualException = assertThrows(ApiNotAccessibleException.class, () -> { + verifiersService.getVerifierByClientId("test-clientId2"); + }); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void validateTrustedVerifiersAndDoNothing() throws ApiNotAccessibleException, IOException { + PresentationRequestDTO presentationRequestDTO = PresentationRequestDTO.builder().clientId("test-clientId").responseUri("https://test-responseUri").redirectUri("https://test-redirectUri").build(); + verifiersService.validateVerifier(presentationRequestDTO.getClientId(), presentationRequestDTO.getResponseUri(), presentationRequestDTO.getRedirectUri()); + } + + @Test + public void validateVerifier_succeedsWhenResponseUriNullAndRedirectUriMatches() throws ApiNotAccessibleException, IOException { + verifiersService.validateVerifier("test-clientId", null, "https://test-redirectUri"); + } + + @Test + public void validateVerifierSucceedsWhenResponseUriEmptyAndRedirectUriMatches() throws ApiNotAccessibleException, IOException { + verifiersService.validateVerifier("test-clientId", "", "https://test-redirectUri"); + } + + @Test + public void validateVerifierThrowsInvalidRedirectUriWhenRedirectDoesNotMatchRegistered() throws ApiNotAccessibleException, IOException { + String expectedMsg = "invalid_redirect_uri --> The requested redirect uri doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", null, "https://other-app.example/callback")); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test + public void validateVerifierThrowsInvalidRedirectUriWhenRedirectUriIsNotAValidUrl() throws ApiNotAccessibleException, IOException { + String expectedMsg = "invalid_redirect_uri --> The requested redirect uri doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", null, "not-a-valid-url")); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test + public void validateVerifierThrowsInvalidRedirectUriWhenRedirectUriNull() throws ApiNotAccessibleException, IOException { + String expectedMsg = "invalid_redirect_uri --> The requested redirect uri doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", null, null)); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test + public void validateVerifierThrowsInvalidClientWhenClientNotFoundAndResponseUriSkippedForRedirectValidation() throws ApiNotAccessibleException, IOException { + String expectedMsg = "invalid_client --> The requested client doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("unknown-client-id", null, "https://test-redirectUri")); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test + public void validateVerifierThrowsInvalidRedirectUriWhenRegisteredRedirectUriIsNotAValidUrl() throws Exception { + VerifierDTO verifier = VerifierDTO.builder() + .clientId("client-with-bad-registered-redirect") + .redirectUris(Collections.singletonList("not-a-valid-registered-redirect-uri")) + .responseUris(Collections.singletonList("https://cb.example/resp")) + .build(); + VerifiersDTO dto = VerifiersDTO.builder().verifiers(Collections.singletonList(verifier)).build(); + String json = new ObjectMapper().writeValueAsString(dto); + when(utilities.getTrustedVerifiersJsonValue()).thenReturn(json); + when(objectMapper.readValue(anyString(), eq(VerifiersDTO.class))).thenReturn(dto); + + String expectedMsg = "invalid_redirect_uri --> The requested redirect uri doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("client-with-bad-registered-redirect", null, + "https://valid-redirect.example/callback")); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test + public void validateVerifierThrowsInvalidRedirectUriWhenRegisteredRedirectValidButPathDoesNotMatch() throws ApiNotAccessibleException, IOException { + String expectedMsg = "invalid_redirect_uri --> The requested redirect uri doesn’t match."; + + InvalidVerifierException ex = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", null, + "https://test-redirectUri/extra-path")); + + assertEquals(expectedMsg, ex.getMessage()); + } + + @Test(expected = InvalidVerifierException.class) + public void validateTrustedVerifiersAndThrowInvalidVerifierExceptionWhenClientIdIsIncorrect() throws ApiNotAccessibleException, IOException { + PresentationRequestDTO presentationRequestDTO = PresentationRequestDTO.builder().clientId("test-clientId2").responseUri("https://test-responseUri").build(); + verifiersService.validateVerifier(presentationRequestDTO.getClientId(), presentationRequestDTO.getResponseUri(), presentationRequestDTO.getRedirectUri()); + } + + @Test + public void validateTrustedVerifiersAndThrowInvalidVerifiersExceptionForAInvalidClientId() throws ApiNotAccessibleException, IOException { + PresentationRequestDTO presentationRequestDTO = PresentationRequestDTO.builder().clientId("test-clientId2").responseUri("https://test-responseUri").build(); + String expectedExceptionMsg = "invalid_client --> The requested client doesn’t match."; + InvalidVerifierException actualException = assertThrows(InvalidVerifierException.class, () -> verifiersService.validateVerifier(presentationRequestDTO.getClientId(), presentationRequestDTO.getResponseUri(), presentationRequestDTO.getRedirectUri())); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void validateTrustedVerifiersAndThrowInvalidVerifiersExceptionWhenClientIdIsValidAndResponseUriIsIncorrect() throws ApiNotAccessibleException, IOException { + PresentationRequestDTO presentationRequestDTO = PresentationRequestDTO.builder().clientId("test-clientId").responseUri("https://test-reponseUri/invalid-uri").build(); + String expectedExceptionMsg = "invalid_response_uri --> The requested response uri doesn’t match."; + + InvalidVerifierException actualException = assertThrows(InvalidVerifierException.class, () -> verifiersService.validateVerifier(presentationRequestDTO.getClientId(), presentationRequestDTO.getResponseUri(), presentationRequestDTO.getRedirectUri())); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void validateVerifierThrowsExceptionWhenResponseUriIsInvalidUrl() throws ApiNotAccessibleException, IOException { + String expectedExceptionMsg = "invalid_response_uri --> The requested response uri doesn’t match."; + + InvalidVerifierException actualException = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", "not-a-valid-url", "")); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void validateVerifierThrowsExceptionWhenRegisteredResponseUriIsInvalidUrl() throws Exception { + VerifierDTO verifierWithBadUri = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("not-a-valid-registered-uri")).build(); + VerifiersDTO verifiersWithBadUri = VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierWithBadUri)).build(); + + String json = new ObjectMapper().writeValueAsString(verifiersWithBadUri); + when(utilities.getTrustedVerifiersJsonValue()).thenReturn(json); + when(objectMapper.readValue(eq(json), eq(VerifiersDTO.class))).thenReturn(verifiersWithBadUri); + + String expectedExceptionMsg = "invalid_response_uri --> The requested response uri doesn’t match."; + + InvalidVerifierException actualException = assertThrows(InvalidVerifierException.class, + () -> verifiersService.validateVerifier("test-clientId", "https://test-responseUri", "")); + + assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void testIsVerifierTrustedByWallet_TrustedVerifier() { + String walletId = "wallet123"; + String verifierId = "verifier123"; + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, verifierId)).thenReturn(true); + + boolean result = verifiersService.isVerifierTrustedByWallet(verifierId, walletId); + + assertTrue(result); + } + + @Test + public void testIsVerifierTrustedByWallet_UntrustedVerifier() { + String walletId = "wallet123"; + String verifierId = "verifier123"; + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, verifierId)).thenReturn(false); + + boolean result = verifiersService.isVerifierTrustedByWallet(verifierId, walletId); + + assertFalse(result); + } + + @Test + public void testIsVerifierTrustedByWallet_NullInputs() { + String walletId = null; + String verifierId = null; + when(verifierRepository.existsByWalletIdAndVerifierId(walletId, verifierId)).thenReturn(false); + + boolean result = verifiersService.isVerifierTrustedByWallet(verifierId, walletId); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredValidClientIdAndMatchingVerifier() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&response_uri=" + ENCODED_RESPONSE_URI; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertTrue(result); + } + + @Test + public void testIsVerifierClientPreregisteredValidClientIdButNoMatchingVerifier() throws URISyntaxException { + List verifiers = List.of(new Verifier("https://other-verifier.com", List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredNullClientId() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?other_param=value"; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredEmptyClientId() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id="; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredWhitespaceOnlyClientId() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=%20%20"; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredEmptyVerifiersList() throws URISyntaxException { + List verifiers = Collections.emptyList(); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredPartialResponseUrisMatch() throws URISyntaxException { + List verifierResponseUris = Arrays.asList("https://example.com/callback1", "https://example.com/callback2"); + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, verifierResponseUris, null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&response_uri=" + ENCODED_RESPONSE_URI; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredNullUrl() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = null; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredEmptyUrl() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = ""; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredWhitespaceOnlyUrl() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = " "; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredSpecialCharactersInClientId() throws URISyntaxException { + String specialClientId = "https://test.com/path?param=value&other=123"; + String encodedSpecialClientId = "https%3A%2F%2Ftest.com%2Fpath%3Fparam%3Dvalue%26other%3D123"; + List verifiers = List.of(new Verifier(specialClientId, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=" + encodedSpecialClientId; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredSpecialCharactersInResponseUri() throws URISyntaxException { + String specialResponseUri = "https://test.com/callback?param=value&other=123"; + String encodedSpecialResponseUri = "https%3A%2F%2Ftest.com%2Fcallback%3Fparam%3Dvalue%26other%3D123"; + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(specialResponseUri), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&response_uri=" + encodedSpecialResponseUri; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertTrue(result); + } + + @Test + public void testIsVerifierClientPreregisteredMultipleVerifiers() throws URISyntaxException { + List verifiers = Arrays.asList(new Verifier("https://verifier1.com", List.of("https://callback1.com"), null), new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null), new Verifier("https://verifier3.com", List.of("https://callback3.com"), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&response_uri=" + ENCODED_RESPONSE_URI; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertTrue(result); + } + + @Test + public void testIsVerifierClientPreregisteredNullResponseUris() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } + + @Test + public void testIsVerifierClientPreregisteredEmptyResponseUris() throws URISyntaxException { + List verifiers = List.of(new Verifier(VALID_CLIENT_ID, List.of(VALID_RESPONSE_URI), null)); + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&response_uri="; + + boolean result = verifiersService.isVerifierClientPreregistered(verifiers, url); + + assertFalse(result); + } +} diff --git a/src/test/java/io/mosip/mimoto/service/WalletCredentialServiceTest.java b/src/test/java/io/mosip/mimoto/service/WalletCredentialServiceTest.java new file mode 100644 index 000000000..7d155641c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/WalletCredentialServiceTest.java @@ -0,0 +1,722 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.IssuerDTO; +import io.mosip.mimoto.dto.idp.TokenResponseDTO; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.resident.WalletCredentialResponseDTO; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.model.VerifiableCredential; +import io.mosip.mimoto.repository.WalletCredentialsRepository; +import io.mosip.mimoto.service.impl.WalletCredentialServiceImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.mosip.mimoto.exception.ErrorConstants.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class WalletCredentialServiceTest { + + @InjectMocks + private WalletCredentialServiceImpl walletCredentialService; + + @Mock + private WalletCredentialsRepository walletCredentialsRepository; + + @Mock + private IssuersService issuersService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private CredentialService credentialService; + + @Mock + private CredentialPDFGeneratorService credentialPDFGeneratorService; + + @Mock + private DataProtectionService dataProtectionService; + + private final String walletId = "wallet123"; + private final String issuerId = "issuer123"; + private final String credentialType = "CredentialType1"; + private final String credentialId = "cred123"; + private final String base64Key = "ZHVtbXlrZXkxMjM0NTY3OA=="; // Base64 of "dummykey12345678" + private final String locale = "en"; + + private TokenResponseDTO tokenResponse; + private VerifiableCredential verifiableCredential; + private IssuerConfig issuerConfig; + + private VerifiableCredential testCredential1; + private VerifiableCredential testCredential2; + private VCCredentialResponse testVcResponse1; + private VCCredentialResponse testVcResponse2; + + @Before + public void setUp() throws Exception { + tokenResponse = new TokenResponseDTO(); + tokenResponse.setAccess_token("accessToken"); + + verifiableCredential = new VerifiableCredential(); + verifiableCredential.setId(credentialId); + verifiableCredential.setWalletId(walletId); + verifiableCredential.setCredential("encryptedCredential"); + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId(issuerId); + metadata.setCredentialType(credentialType); + verifiableCredential.setCredentialMetadata(metadata); + verifiableCredential.setCreatedAt(Instant.now()); + verifiableCredential.setUpdatedAt(Instant.now()); + + IssuerDTO issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id(issuerId); + CredentialIssuerWellKnownResponse wellKnownResponse = new CredentialIssuerWellKnownResponse(); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + issuerConfig = new IssuerConfig(issuerDTO, wellKnownResponse, credentialsSupportedResponse); + + ReflectionTestUtils.setField(walletCredentialService, "issuersWithSingleVcLimit", "Mosip"); + + testCredential1 = createTestCredential("cred1", "issuer1", "type1"); + testCredential2 = createTestCredential("cred2", "issuer2", "type2"); + testVcResponse1 = createTestVCCredentialResponse("type1"); + testVcResponse2 = createTestVCCredentialResponse("type2"); + } + + @Test + public void shouldDownloadVCAndStoreInDBSuccessfully() throws Exception { + String mosipIssuerId = "Mosip"; + VerifiableCredentialResponseDTO expectedResponse = new VerifiableCredentialResponseDTO(); + expectedResponse.setCredentialId(credentialId); + + when(walletCredentialsRepository.existsByIssuerIdAndCredentialTypeAndWalletId(mosipIssuerId, credentialType, walletId)).thenReturn(false); + when(credentialService.downloadCredentialAndStoreInDB(tokenResponse, credentialType, walletId, base64Key, mosipIssuerId, locale)) + .thenReturn(expectedResponse); + + VerifiableCredentialResponseDTO actualResponse = walletCredentialService.downloadVCAndStoreInDB( + mosipIssuerId, credentialType, tokenResponse, locale, walletId, base64Key); + + assertEquals(expectedResponse, actualResponse); + verify(walletCredentialsRepository).existsByIssuerIdAndCredentialTypeAndWalletId(mosipIssuerId, credentialType, walletId); + verify(credentialService).downloadCredentialAndStoreInDB(tokenResponse, credentialType, walletId, base64Key, mosipIssuerId, locale); + } + + @Test + public void shouldThrowDuplicateCredentialExceptionForMosipIssuer() { + when(walletCredentialsRepository.existsByIssuerIdAndCredentialTypeAndWalletId("Mosip", credentialType, walletId)).thenReturn(true); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletCredentialService.downloadVCAndStoreInDB("Mosip", credentialType, tokenResponse, locale, walletId, base64Key)); + + assertEquals(CREDENTIAL_DOWNLOAD_EXCEPTION.getErrorCode(), exception.getErrorCode()); + assertEquals("credential_download_error --> Duplicate credential for issuer and type", exception.getMessage()); + verify(walletCredentialsRepository).existsByIssuerIdAndCredentialTypeAndWalletId("Mosip", credentialType, walletId); + verifyNoInteractions(credentialService); + } + + @Test + public void shouldFetchAndStoreNonMosipIssuerCredential() throws Exception { + VerifiableCredentialResponseDTO expectedResponse = new VerifiableCredentialResponseDTO(); + expectedResponse.setCredentialId(credentialId); + + when(credentialService.downloadCredentialAndStoreInDB(tokenResponse, credentialType, walletId, base64Key, issuerId, locale)) + .thenReturn(expectedResponse); + + VerifiableCredentialResponseDTO actualResponse = walletCredentialService.downloadVCAndStoreInDB( + issuerId, credentialType, tokenResponse, locale, walletId, base64Key); + + assertEquals(expectedResponse, actualResponse); + verify(credentialService).downloadCredentialAndStoreInDB(tokenResponse, credentialType, walletId, base64Key, issuerId, locale); + } + + @Test + public void shouldThrowExternalServiceUnavailableException() throws Exception { + String mosipIssuerId = "Mosip"; // Use Mosip to trigger repository check + + when(walletCredentialsRepository.existsByIssuerIdAndCredentialTypeAndWalletId(mosipIssuerId, credentialType, walletId)).thenReturn(false); + when(credentialService.downloadCredentialAndStoreInDB(any(), anyString(), anyString(), anyString(), anyString(), anyString())) + .thenThrow(new ExternalServiceUnavailableException("SERVICE_UNAVAILABLE", "Service unavailable")); + + ExternalServiceUnavailableException exception = assertThrows(ExternalServiceUnavailableException.class, () -> + walletCredentialService.downloadVCAndStoreInDB(mosipIssuerId, credentialType, tokenResponse, locale, walletId, base64Key)); + + assertEquals("SERVICE_UNAVAILABLE", exception.getErrorCode()); + assertEquals("SERVICE_UNAVAILABLE --> Service unavailable", exception.getMessage()); + verify(walletCredentialsRepository).existsByIssuerIdAndCredentialTypeAndWalletId(mosipIssuerId, credentialType, walletId); + verify(credentialService).downloadCredentialAndStoreInDB(tokenResponse, credentialType, walletId, base64Key, mosipIssuerId, locale); + } + + @Test + public void shouldFetchAllCredentialsForWalletSuccessfully() throws Exception { + VerifiableCredentialResponseDTO responseDTO = new VerifiableCredentialResponseDTO(); + responseDTO.setCredentialId(credentialId); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)).thenReturn(List.of(verifiableCredential)); + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenReturn(issuerConfig); + + try (MockedStatic factoryMock = mockStatic(VerifiableCredentialResponseDTO.class)) { + factoryMock.when(() -> VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, locale, credentialId)) + .thenReturn(responseDTO); + + List actualCredentials = walletCredentialService.fetchAllCredentialsForWallet(walletId, base64Key, locale); + + assertEquals(1, actualCredentials.size()); + assertEquals(responseDTO, actualCredentials.getFirst()); + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(issuersService).getIssuerConfig(issuerId, credentialType); + factoryMock.verify(() -> VerifiableCredentialResponseDTO.fromIssuerConfig(issuerConfig, locale, credentialId)); + } + } + + @Test + public void shouldHandleIssuerConfigFetchFailure() throws Exception { + VerifiableCredentialResponseDTO responseDTO = new VerifiableCredentialResponseDTO(); + responseDTO.setCredentialId(credentialId); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)).thenReturn(List.of(verifiableCredential)); + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenThrow(new ApiNotAccessibleException("API error")); + + try (MockedStatic factoryMock = mockStatic(VerifiableCredentialResponseDTO.class)) { + factoryMock.when(() -> VerifiableCredentialResponseDTO.fromIssuerConfig(null, locale, credentialId)) + .thenReturn(responseDTO); + + List actualCredentials = walletCredentialService.fetchAllCredentialsForWallet(walletId, base64Key, locale); + + assertEquals(1, actualCredentials.size()); + assertEquals(responseDTO, actualCredentials.getFirst()); + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(issuersService).getIssuerConfig(issuerId, credentialType); + factoryMock.verify(() -> VerifiableCredentialResponseDTO.fromIssuerConfig(null, locale, credentialId)); + } + } + + @Test + public void shouldFetchVerifiableCredentialSuccessfully() throws Exception { + // Mocked VC JSON as String + VCCredentialResponse vcResponse = VCCredentialResponse.builder() + .format("ldp_vc") + .credential(VCCredentialProperties.builder() + .type(List.of(credentialType)) + .issuer("issuer123") + .issuanceDate("2024-01-01T00:00:00Z") + .credentialSubject(Map.of("name", "John Doe")) + .build()) + .build(); + + String decryptedCredentialJson = new ObjectMapper().writeValueAsString(vcResponse); + + // Setup repository + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)) + .thenReturn(Optional.of(verifiableCredential)); + + // Setup decryption + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)) + .thenReturn(decryptedCredentialJson); + when(objectMapper.readValue(decryptedCredentialJson, VCCredentialResponse.class)) + .thenReturn(vcResponse); + // IssuerDTO + IssuerDTO issuerDTO = new IssuerDTO(); + issuerDTO.setIssuer_id(issuerId); + when(issuersService.getIssuerDetails(issuerId)).thenReturn(issuerDTO); + + // Credential Definition that matches VC type + CredentialDefinitionResponseDto credentialDefinition = new CredentialDefinitionResponseDto(); + credentialDefinition.setType(List.of(credentialType)); + credentialDefinition.setCredentialSubject(Map.of()); // safe stub + credentialDefinition.setContext(List.of("https://www.w3.org/2018/credentials/v1")); + + // Full IssuerConfig + CredentialsSupportedResponse supportedResponse = new CredentialsSupportedResponse(); + supportedResponse.setCredentialDefinition(credentialDefinition); + supportedResponse.setProofTypesSupported(Map.of("ldp_vc", new ProofTypesSupported())); + supportedResponse.setDisplay(List.of()); + supportedResponse.setFormat("ldp_vc"); + supportedResponse.setScope("scope"); + + IssuerConfig issuerConfig = new IssuerConfig(issuerDTO, new CredentialIssuerWellKnownResponse(), supportedResponse); + + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenReturn(issuerConfig); + + // Setup PDF stream + ByteArrayInputStream pdfContent = new ByteArrayInputStream("PDF Content".getBytes()); + when(credentialPDFGeneratorService.generatePdfForVerifiableCredential( + eq(credentialType), + any(VCCredentialResponse.class), + eq(issuerDTO), + eq(supportedResponse), + eq(""), + eq(null), + eq(locale) + )).thenReturn(pdfContent); + + // Run + WalletCredentialResponseDTO actualResponse = walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale); + + // Assert + assertNotNull(actualResponse); + assertEquals(String.format("%s_credential.pdf", credentialType), actualResponse.getFileName()); + assertNotNull(actualResponse.getFileContentStream()); + + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verify(dataProtectionService).decryptCredential("encryptedCredential", base64Key); + verify(issuersService).getIssuerDetails(issuerId); + verify(issuersService).getIssuerConfig(issuerId, credentialType); + verify(credentialPDFGeneratorService).generatePdfForVerifiableCredential( + eq(credentialType), + any(VCCredentialResponse.class), + eq(issuerDTO), + eq(supportedResponse), + eq(""), + eq(null), + eq(locale) + ); + } + + + + @Test + public void shouldThrowCredentialNotFoundException() { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.empty()); + + CredentialNotFoundException exception = assertThrows(CredentialNotFoundException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(RESOURCE_NOT_FOUND.getErrorCode(), exception.getErrorCode()); + assertEquals(RESOURCE_NOT_FOUND.getErrorCode() +" --> "+RESOURCE_NOT_FOUND.getErrorMessage(), exception.getMessage()); + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verifyNoInteractions(dataProtectionService, credentialPDFGeneratorService); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnDecryptionFailure() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)) + .thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verify(dataProtectionService).decryptCredential("encryptedCredential", base64Key); + verifyNoInteractions(credentialPDFGeneratorService); + } + + @Test + public void shouldDeleteCredentialSuccessfully() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + + walletCredentialService.deleteCredential(credentialId, walletId); + + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verify(walletCredentialsRepository).deleteById(credentialId); + } + + @Test + public void shouldThrowCredentialNotFoundExceptionWhenDeletingNonExistentCredential() { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.empty()); + + CredentialNotFoundException exception = assertThrows(CredentialNotFoundException.class, () -> + walletCredentialService.deleteCredential(credentialId, walletId)); + + assertEquals(RESOURCE_NOT_FOUND.getErrorCode(), exception.getErrorCode()); + assertEquals(RESOURCE_NOT_FOUND.getErrorCode() + " --> " + RESOURCE_NOT_FOUND.getErrorMessage(), exception.getMessage()); + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verify(walletCredentialsRepository, never()).deleteById(anyString()); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnJsonProcessingError() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)).thenReturn("bad-json"); + when(objectMapper.readValue("bad-json", VCCredentialResponse.class)).thenThrow(new com.fasterxml.jackson.core.JsonProcessingException("error") {}); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + verify(walletCredentialsRepository).findByIdAndWalletId(credentialId, walletId); + verify(dataProtectionService).decryptCredential("encryptedCredential", base64Key); + verify(objectMapper).readValue("bad-json", VCCredentialResponse.class); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnNullIssuerConfig() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)).thenReturn("{}"); + VCCredentialResponse vcResponse = VCCredentialResponse.builder().credential(VCCredentialProperties.builder().type(List.of(credentialType)).build()).build(); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))).thenReturn(vcResponse); + when(issuersService.getIssuerDetails(issuerId)).thenReturn(new IssuerDTO()); + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenReturn(null); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnCredentialTypeMismatch() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)).thenReturn("{}"); + VCCredentialResponse vcResponse = VCCredentialResponse.builder().credential(VCCredentialProperties.builder().type(List.of("OtherType")).build()).build(); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))).thenReturn(vcResponse); + when(issuersService.getIssuerDetails(issuerId)).thenReturn(new IssuerDTO()); + + CredentialDefinitionResponseDto credentialDefinition = new CredentialDefinitionResponseDto(); + credentialDefinition.setType(List.of(credentialType)); + CredentialsSupportedResponse supportedResponse = new CredentialsSupportedResponse(); + supportedResponse.setCredentialDefinition(credentialDefinition); + + IssuerConfig issuerConfig = new IssuerConfig(new IssuerDTO(), new CredentialIssuerWellKnownResponse(), supportedResponse); + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenReturn(issuerConfig); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnIssuerServiceException() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)).thenReturn("{}"); + VCCredentialResponse vcResponse = VCCredentialResponse.builder().credential(VCCredentialProperties.builder().type(List.of(credentialType)).build()).build(); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))).thenReturn(vcResponse); + when(issuersService.getIssuerDetails(issuerId)).thenThrow(new ApiNotAccessibleException("API error")); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + } + + @Test + public void shouldThrowCredentialProcessingExceptionOnPdfGenerationException() throws Exception { + when(walletCredentialsRepository.findByIdAndWalletId(credentialId, walletId)).thenReturn(Optional.of(verifiableCredential)); + when(dataProtectionService.decryptCredential("encryptedCredential", base64Key)).thenReturn("{}"); + VCCredentialResponse vcResponse = VCCredentialResponse.builder().credential(VCCredentialProperties.builder().type(List.of(credentialType)).build()).build(); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))).thenReturn(vcResponse); + when(issuersService.getIssuerDetails(issuerId)).thenReturn(new IssuerDTO()); + + CredentialDefinitionResponseDto credentialDefinition = new CredentialDefinitionResponseDto(); + credentialDefinition.setType(List.of(credentialType)); + CredentialsSupportedResponse supportedResponse = new CredentialsSupportedResponse(); + supportedResponse.setCredentialDefinition(credentialDefinition); + + IssuerConfig issuerConfig = new IssuerConfig(new IssuerDTO(), new CredentialIssuerWellKnownResponse(), supportedResponse); + when(issuersService.getIssuerConfig(issuerId, credentialType)).thenReturn(issuerConfig); + + when(credentialPDFGeneratorService.generatePdfForVerifiableCredential(any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RuntimeException("PDF error")); + + CredentialProcessingException exception = assertThrows(CredentialProcessingException.class, () -> + walletCredentialService.fetchVerifiableCredential(walletId, credentialId, base64Key, locale)); + + assertEquals(CREDENTIAL_FETCH_EXCEPTION.getErrorCode(), exception.getErrorCode()); + } + + @Test + public void shouldGetDecryptedCredentialsSuccessfully() throws Exception { + List walletCredentials = List.of(testCredential1, testCredential2); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)) + .thenReturn(walletCredentials); + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(new ObjectMapper().writeValueAsString(testVcResponse1)); + when(dataProtectionService.decryptCredential("encryptedcred2", base64Key)) + .thenReturn(new ObjectMapper().writeValueAsString(testVcResponse2)); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))) + .thenReturn(testVcResponse1, testVcResponse2); + + List result = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + assertEquals(2, result.size()); + assertEquals("cred1", result.get(0).getId()); + assertEquals("cred2", result.get(1).getId()); + assertEquals(testVcResponse1, result.get(0).getCredential()); + assertEquals(testVcResponse2, result.get(1).getCredential()); + + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(dataProtectionService).decryptCredential("encryptedcred2", base64Key); + verify(objectMapper, times(2)).readValue(anyString(), eq(VCCredentialResponse.class)); + } + + @Test + public void shouldGetDecryptedCredentialsWithPartialFailures() throws Exception { + List walletCredentials = List.of(testCredential1, testCredential2); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)) + .thenReturn(walletCredentials); + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(new ObjectMapper().writeValueAsString(testVcResponse1)); + when(dataProtectionService.decryptCredential("encryptedcred2", base64Key)) + .thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + when(objectMapper.readValue(anyString(), eq(VCCredentialResponse.class))) + .thenReturn(testVcResponse1); + + List result = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + assertEquals(1, result.size()); + assertEquals("cred1", result.get(0).getId()); + assertEquals(testVcResponse1, result.get(0).getCredential()); + + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(dataProtectionService).decryptCredential("encryptedcred2", base64Key); + verify(objectMapper).readValue(anyString(), eq(VCCredentialResponse.class)); + } + + @Test + public void shouldGetDecryptedCredentialsWithJsonParsingFailure() throws Exception { + List walletCredentials = List.of(testCredential1, testCredential2); + String validJson = new ObjectMapper().writeValueAsString(testVcResponse2); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)) + .thenReturn(walletCredentials); + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn("invalid-json"); + when(dataProtectionService.decryptCredential("encryptedcred2", base64Key)) + .thenReturn(validJson); + + when(objectMapper.readValue("invalid-json", VCCredentialResponse.class)) + .thenThrow(new com.fasterxml.jackson.core.JsonProcessingException("Invalid JSON") {}); + when(objectMapper.readValue(validJson, VCCredentialResponse.class)) + .thenReturn(testVcResponse2); + + List result = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + assertEquals(1, result.size()); + assertEquals("cred2", result.get(0).getId()); + assertEquals(testVcResponse2, result.get(0).getCredential()); + + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(dataProtectionService).decryptCredential("encryptedcred2", base64Key); + verify(objectMapper).readValue("invalid-json", VCCredentialResponse.class); + verify(objectMapper).readValue(validJson, VCCredentialResponse.class); + } + + @Test + public void shouldGetDecryptedCredentialsWithEmptyWallet() { + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)) + .thenReturn(List.of()); + + List result = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + assertTrue(result.isEmpty()); + + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verifyNoInteractions(dataProtectionService); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldGetDecryptedCredentialsWithAllFailures() throws Exception { + List walletCredentials = List.of(testCredential1, testCredential2); + + when(walletCredentialsRepository.findByWalletIdOrderByCreatedAtDesc(walletId)) + .thenReturn(walletCredentials); + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + when(dataProtectionService.decryptCredential("encryptedcred2", base64Key)) + .thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + + List result = walletCredentialService.getDecryptedCredentials(walletId, base64Key); + + assertTrue(result.isEmpty()); + + verify(walletCredentialsRepository).findByWalletIdOrderByCreatedAtDesc(walletId); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(dataProtectionService).decryptCredential("encryptedcred2", base64Key); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldDecryptAndParseCredentialSuccessfully() throws Exception { + String decryptedJson = new ObjectMapper().writeValueAsString(testVcResponse1); + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(decryptedJson); + when(objectMapper.readValue(decryptedJson, VCCredentialResponse.class)) + .thenReturn(testVcResponse1); + + VCCredentialResponse result = ReflectionTestUtils.invokeMethod( + walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key); + + assertEquals(testVcResponse1, result); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(objectMapper).readValue(decryptedJson, VCCredentialResponse.class); + } + + @Test + public void shouldThrowIllegalArgumentExceptionWhenCredentialIsNull() throws Exception { + java.lang.IllegalArgumentException exception = assertThrows(java.lang.IllegalArgumentException.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", null, base64Key)); + + assertEquals("Credential cannot be null", exception.getMessage()); + verifyNoInteractions(dataProtectionService); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowIllegalArgumentExceptionWhenCredentialDataIsNull() throws Exception { + VerifiableCredential credentialWithNullData = createTestCredential("cred1", "issuer1", "type1"); + credentialWithNullData.setCredential(null); + + java.lang.IllegalArgumentException exception = assertThrows(java.lang.IllegalArgumentException.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", credentialWithNullData, base64Key)); + + assertEquals("Credential data cannot be null", exception.getMessage()); + verifyNoInteractions(dataProtectionService); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowIllegalArgumentExceptionWhenDecryptedDataIsNull() throws Exception { + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(null); + + java.lang.IllegalArgumentException exception = assertThrows(java.lang.IllegalArgumentException.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key)); + + assertEquals("Failed to decrypt credential or decrypted data is empty", exception.getMessage()); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowIllegalArgumentExceptionWhenDecryptedDataIsEmpty() throws Exception { + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(""); + + java.lang.IllegalArgumentException exception = assertThrows(java.lang.IllegalArgumentException.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key)); + + assertEquals("Failed to decrypt credential or decrypted data is empty", exception.getMessage()); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowIllegalArgumentExceptionWhenDecryptedDataIsWhitespace() throws Exception { + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(" "); + + java.lang.IllegalArgumentException exception = assertThrows(java.lang.IllegalArgumentException.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key)); + + assertEquals("Failed to decrypt credential or decrypted data is empty", exception.getMessage()); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowDecryptionExceptionWhenDecryptionFails() throws Exception { + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenThrow(new DecryptionException("DECRYPTION_ERROR", "Decryption failed")); + + Exception thrownException = assertThrows(Exception.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key)); + + DecryptionException exception = null; + if (thrownException instanceof java.lang.reflect.UndeclaredThrowableException) { + Throwable cause = thrownException.getCause(); + assertTrue("Expected DecryptionException but got: " + cause.getClass().getName(), + cause instanceof DecryptionException); + exception = (DecryptionException) cause; + } else if (thrownException instanceof DecryptionException) { + exception = (DecryptionException) thrownException; + } else { + fail("Expected DecryptionException but got: " + thrownException.getClass().getName()); + } + + assertNotNull("Exception should not be null", exception); + assertEquals("DECRYPTION_ERROR", exception.getErrorCode()); + assertEquals("DECRYPTION_ERROR --> Decryption failed", exception.getMessage()); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verifyNoInteractions(objectMapper); + } + + @Test + public void shouldThrowIOExceptionWhenJsonParsingFails() throws Exception { + String invalidJson = "invalid-json"; + + when(dataProtectionService.decryptCredential("encryptedcred1", base64Key)) + .thenReturn(invalidJson); + when(objectMapper.readValue(invalidJson, VCCredentialResponse.class)) + .thenThrow(new com.fasterxml.jackson.core.JsonProcessingException("Invalid JSON") {}); + + Exception thrownException = assertThrows(Exception.class, () -> + ReflectionTestUtils.invokeMethod(walletCredentialService, "decryptAndParseCredential", testCredential1, base64Key)); + + IOException exception = null; + if (thrownException instanceof java.lang.reflect.UndeclaredThrowableException) { + Throwable cause = thrownException.getCause(); + assertTrue("Expected IOException but got: " + cause.getClass().getName(), + cause instanceof IOException); + exception = (IOException) cause; + } else if (thrownException instanceof IOException) { + exception = (IOException) thrownException; + } else { + fail("Expected IOException but got: " + thrownException.getClass().getName()); + } + + assertNotNull("Exception should not be null", exception); + assertEquals("Invalid JSON", exception.getMessage()); + verify(dataProtectionService).decryptCredential("encryptedcred1", base64Key); + verify(objectMapper).readValue(invalidJson, VCCredentialResponse.class); + } + + private VerifiableCredential createTestCredential(String id, String issuerId, String credentialType) { + VerifiableCredential credential = new VerifiableCredential(); + credential.setId(id); + credential.setWalletId(walletId); + credential.setCredential("encrypted" + id); + credential.setCreatedAt(Instant.now()); + credential.setUpdatedAt(Instant.now()); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId(issuerId); + metadata.setCredentialType(credentialType); + credential.setCredentialMetadata(metadata); + + return credential; + } + + private VCCredentialResponse createTestVCCredentialResponse(String credentialType) { + return VCCredentialResponse.builder() + .format("ldp_vc") + .credential(VCCredentialProperties.builder() + .type(List.of(credentialType)) + .issuer("issuer123") + .issuanceDate("2024-01-01T00:00:00Z") + .credentialSubject(Map.of("name", "John Doe")) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/WalletLockServiceTest.java b/src/test/java/io/mosip/mimoto/service/WalletLockServiceTest.java new file mode 100644 index 000000000..164d2192b --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/WalletLockServiceTest.java @@ -0,0 +1,255 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.config.WalletPasscodeConfig; +import io.mosip.mimoto.config.WalletPasscodeConfigTest; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.mimoto.util.TestUtilities; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {WalletPasscodeConfigTest.class, WalletLockService.class}) +@TestPropertySource(locations = "classpath:application-test.properties") +public class WalletLockServiceTest { + + @Autowired + private WalletLockService walletLockService; + + @Autowired + private WalletPasscodeConfig walletPasscodeConfig; + + private Wallet wallet; + + @Before + public void setUp() { + String userId = UUID.randomUUID().toString(); + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(0, 0, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, "encryptedWalletKey", walletMetadata); + } + + @Test + public void testWalletPasscodeConfigInitialization() { + assertNotNull(walletPasscodeConfig, "WalletPasscodeConfig should be initialized"); + assertEquals(60, walletPasscodeConfig.getRetryBlockedUntil(), "RetryBlockedUntil should be 60 minutes"); + assertEquals(5, walletPasscodeConfig.getMaxFailedAttemptsAllowedPerCycle(), "MaxFailedAttemptsAllowedPerCycle should be 5"); + assertEquals(3, walletPasscodeConfig.getMaxLockCyclesAllowed(), "MaxLockCyclesAllowed should be 3"); + } + + @Test + public void enforceLockCyclePolicyShouldTemporarilyLockWalletOnInvalidLastAttemptInCurrentCycle() { + wallet.getWalletMetadata().getPasscodeControl().setFailedAttemptCount(4); + wallet.getWalletMetadata().getPasscodeControl().setCurrentCycleCount(1); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(wallet); + + assertEquals(WalletLockStatus.TEMPORARILY_LOCKED, updatedWallet.getWalletMetadata().getLockStatus()); + assertNotNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + assertTrue(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil() > System.currentTimeMillis()); + } + + @Test + public void enforceLockCyclePolicyShouldPermanentlyLockWalletOnInvalidLastAttemptInLastCycle() { + wallet.getWalletMetadata().getPasscodeControl().setFailedAttemptCount(4); + wallet.getWalletMetadata().getPasscodeControl().setCurrentCycleCount(3); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(wallet); + + assertEquals(WalletLockStatus.PERMANENTLY_LOCKED, updatedWallet.getWalletMetadata().getLockStatus()); + assertNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + } + + @Test + public void enforceLockCyclePolicyShouldSetStatusToLastAttemptBeforeLockoutOnInvalidPenultimateAttempt() { + wallet.getWalletMetadata().getPasscodeControl().setFailedAttemptCount(3); + wallet.getWalletMetadata().getPasscodeControl().setCurrentCycleCount(3); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(wallet); + + assertEquals(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT, updatedWallet.getWalletMetadata().getLockStatus()); + assertNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + } + + @Test + public void testResetTemporaryLockIfLockIsExpired() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + wallet.getWalletMetadata().getPasscodeControl().setRetryBlockedUntil(System.currentTimeMillis() - 1000); + + Wallet updatedWallet = walletLockService.resetTemporaryLockIfExpired(wallet); + + assertEquals(WalletLockStatus.LOCK_EXPIRED, updatedWallet.getWalletMetadata().getLockStatus()); + assertEquals(0, updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount()); + assertNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + } + + @Test + public void testResetTemporaryLockIfLockIsNotExpired() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + wallet.getWalletMetadata().getPasscodeControl().setRetryBlockedUntil(System.currentTimeMillis() + 1000); + + Wallet updatedWallet = walletLockService.resetTemporaryLockIfExpired(wallet); + + assertEquals(WalletLockStatus.TEMPORARILY_LOCKED, updatedWallet.getWalletMetadata().getLockStatus()); + assertNotNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + assertEquals(wallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount(), + updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount()); + } + + @Test + public void testResetLockState() { + wallet.getWalletMetadata().getPasscodeControl().setFailedAttemptCount(6); + wallet.getWalletMetadata().getPasscodeControl().setCurrentCycleCount(2); + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + + Wallet updatedWallet = walletLockService.resetLockState(wallet); + + assertEquals(0, updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount()); + assertEquals(0, updatedWallet.getWalletMetadata().getPasscodeControl().getCurrentCycleCount()); + assertNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + assertNull(updatedWallet.getWalletMetadata().getLockStatus()); + } + + @Test + public void enforceLockCyclePolicyShouldInitializeCycleCountWhenCurrentCycleCountIsZero() { + wallet.getWalletMetadata().getPasscodeControl().setFailedAttemptCount(0); + wallet.getWalletMetadata().getPasscodeControl().setCurrentCycleCount(0); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(wallet); + + assertEquals(1, updatedWallet.getWalletMetadata().getPasscodeControl().getCurrentCycleCount()); + assertEquals(1, updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount()); + } + + @Test + public void enforceLockCyclePolicyShouldNotSetLastAttemptStatusWhenConditionsNotMet() { + // Test scenarios where LAST_ATTEMPT_BEFORE_LOCKOUT should NOT be set + // Format: {initialFailedCount, currentCycle, expectedFailedCountAfter, description} + Object[][] testCases = { + {2, 3, 3, "neither penultimate attempt nor last cycle"}, + {3, 2, 4, "penultimate attempt but not last cycle"}, + {1, 3, 2, "last cycle but not penultimate attempt"} + }; + + for (Object[] testCase : testCases) { + int initialFailedCount = (int) testCase[0]; + int currentCycle = (int) testCase[1]; + int expectedFailedCountAfter = (int) testCase[2]; + String description = (String) testCase[3]; + + // Reset wallet for each test case + String userId = UUID.randomUUID().toString(); + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(initialFailedCount, currentCycle, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + Wallet testWallet = TestUtilities.createWallet(userId, "encryptedWalletKey", walletMetadata); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(testWallet); + + assertNotEquals(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT, + updatedWallet.getWalletMetadata().getLockStatus(), + "Should NOT set LAST_ATTEMPT_BEFORE_LOCKOUT when " + description); + assertEquals(expectedFailedCountAfter, + updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount(), + "Failed attempt count mismatch when " + description); + } + } + + @Test + public void testResetTemporaryLockIfLockStatusIsNotTemporarilyLocked() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.PERMANENTLY_LOCKED); + wallet.getWalletMetadata().getPasscodeControl().setRetryBlockedUntil(System.currentTimeMillis() - 1000); + + Wallet updatedWallet = walletLockService.resetTemporaryLockIfExpired(wallet); + + assertEquals(WalletLockStatus.PERMANENTLY_LOCKED, updatedWallet.getWalletMetadata().getLockStatus()); + assertNotNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + } + + @Test + public void testResetTemporaryLockIfRetryBlockedUntilIsNull() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + wallet.getWalletMetadata().getPasscodeControl().setRetryBlockedUntil(null); + + Wallet updatedWallet = walletLockService.resetTemporaryLockIfExpired(wallet); + + assertEquals(WalletLockStatus.TEMPORARILY_LOCKED, updatedWallet.getWalletMetadata().getLockStatus()); + assertNull(updatedWallet.getWalletMetadata().getPasscodeControl().getRetryBlockedUntil()); + } + + @Test + public void enforceLockCyclePolicyShouldInitializePasscodeControlWhenNull() { + String userId = UUID.randomUUID().toString(); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", null, null); + Wallet walletWithNullPasscode = TestUtilities.createWallet(userId, "encryptedWalletKey", walletMetadata); + + assertNull(walletWithNullPasscode.getWalletMetadata().getPasscodeControl()); + + Wallet updatedWallet = walletLockService.enforceLockCyclePolicy(walletWithNullPasscode); + + assertNotNull(updatedWallet.getWalletMetadata().getPasscodeControl()); + assertEquals(1, updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount()); + assertEquals(1, updatedWallet.getWalletMetadata().getPasscodeControl().getCurrentCycleCount()); + } + + @Test + public void shouldInitializePasscodeControlWhenNullForAllMethods() { + Object[][] testCases = { + {"enforceLockCyclePolicy", 1, 1}, + {"resetTemporaryLockIfExpired", null, null}, + {"resetLockState", 0, 0} + }; + + for (Object[] testCase : testCases) { + String methodName = (String) testCase[0]; + Integer expectedFailedCount = (Integer) testCase[1]; + Integer expectedCycleCount = (Integer) testCase[2]; + + String userId = UUID.randomUUID().toString(); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", null, null); + Wallet walletWithNullPasscode = TestUtilities.createWallet(userId, "encryptedWalletKey", walletMetadata); + + assertNull(walletWithNullPasscode.getWalletMetadata().getPasscodeControl(), + "PasscodeControl should be null before calling " + methodName); + + Wallet updatedWallet; + switch (methodName) { + case "enforceLockCyclePolicy": + updatedWallet = walletLockService.enforceLockCyclePolicy(walletWithNullPasscode); + break; + case "resetTemporaryLockIfExpired": + updatedWallet = walletLockService.resetTemporaryLockIfExpired(walletWithNullPasscode); + break; + case "resetLockState": + updatedWallet = walletLockService.resetLockState(walletWithNullPasscode); + break; + default: + throw new IllegalArgumentException("Unknown method: " + methodName); + } + + assertNotNull(updatedWallet.getWalletMetadata().getPasscodeControl(), + "PasscodeControl should be initialized after calling " + methodName); + + if (expectedFailedCount != null) { + assertEquals(expectedFailedCount, + updatedWallet.getWalletMetadata().getPasscodeControl().getFailedAttemptCount(), + "Failed attempt count mismatch for " + methodName); + } + if (expectedCycleCount != null) { + assertEquals(expectedCycleCount, + updatedWallet.getWalletMetadata().getPasscodeControl().getCurrentCycleCount(), + "Current cycle count mismatch for " + methodName); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/WalletPresentationServiceTest.java b/src/test/java/io/mosip/mimoto/service/WalletPresentationServiceTest.java new file mode 100644 index 000000000..fa18f0f12 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/WalletPresentationServiceTest.java @@ -0,0 +1,2343 @@ +package io.mosip.mimoto.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.util.Base64URL; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.constant.OpenID4VPConstants; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.openID4VP.constants.SpecVersion; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.exception.*; +import io.mosip.mimoto.model.VerifiablePresentation; +import io.mosip.mimoto.repository.VerifiablePresentationsRepository; +import io.mosip.mimoto.service.impl.OpenID4VPService; +import io.mosip.mimoto.service.impl.WalletPresentationServiceImpl; +import io.mosip.mimoto.util.SigningKeyUtil; +import io.mosip.mimoto.util.UrlParameterUtils; +import io.mosip.openID4VP.OpenID4VP; +import org.springframework.http.ResponseEntity; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationDcqlRequest; +import io.mosip.openID4VP.authorizationRequest.clientMetadata.ClientMetadata; +import io.mosip.openID4VP.authorizationRequest.clientMetadata.ClientMetadataDraft23; +import io.mosip.openID4VP.authorizationResponse.unsignedVPToken.UnsignedVPToken; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.VPTokenSigningResult; +import io.mosip.openID4VP.common.OpenID4VPErrorCodes; +import io.mosip.openID4VP.constants.FormatType; +import io.mosip.openID4VP.exceptions.OpenID4VPExceptions; +import io.mosip.openID4VP.verifier.VerifierResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.time.Instant; +import java.util.*; +import java.util.Base64; + +import static io.mosip.mimoto.exception.ErrorConstants.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class WalletPresentationServiceTest { + + @Mock + private VerifierService verifierService; + + @Mock + private OpenID4VPService openID4VPService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private KeyPairRetrievalService keyPairService; + + @Mock + private VerifiablePresentationsRepository verifiablePresentationsRepository; + + @Mock + private CredentialMatchingService credentialMatchingService; + + @Mock + private CredentialFormatHandlerFactory credentialFormatHandlerFactory; + + @Mock + private CredentialFormatHandler credentialFormatHandler; + + @Mock + private WalletCredentialService walletCredentialService; + + @InjectMocks + private WalletPresentationServiceImpl walletPresentationService; + + private String walletId; + private String presentationId; + private String urlEncodedVPAuthorizationRequest; + private String base64Key; + private VerifiersDTO verifiersDTO; + private VerifierDTO verifierDTO; + private AuthorizationRequest mockAuthorizationRequest; + private OpenID4VP mockOpenID4VP; + private VerifiablePresentationSessionData sessionData; + private SubmitPresentationRequestDTO submitRequest; + private DecryptedCredentialDTO credentialDTO; + private VCCredentialResponse vcCredentialResponse; + private KeyPair keyPair; + private JWK jwk; + private JWSSigner jwsSigner; + + @Before + public void setUp() throws Exception { + walletId = "wallet-123"; + presentationId = "presentation-456"; + base64Key = "base64-encoded-key"; + urlEncodedVPAuthorizationRequest = "client_id=test-client&response_type=vp_token"; + + verifierDTO = new VerifierDTO( + "test-client", + List.of("https://verifier.com/response"), + List.of("https://verifier.com/jwks"), + null, + false, + SpecVersion.V1 + ); + verifiersDTO = new VerifiersDTO(); + verifiersDTO.setVerifiers(List.of(verifierDTO)); + + mockOpenID4VP = mock(OpenID4VP.class); + mockAuthorizationRequest = mock(AuthorizationRequest.class); + when(mockAuthorizationRequest.getClientId()).thenReturn("test-client"); + when(mockAuthorizationRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + + sessionData = new VerifiablePresentationSessionData(); + sessionData.setPresentationId(presentationId); + sessionData.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + sessionData.setCreatedAt(Instant.now()); + sessionData.setVerifierClientPreregistered(true); + + vcCredentialResponse = new VCCredentialResponse(); + vcCredentialResponse.setFormat(CredentialFormat.LDP_VC.getFormat()); + vcCredentialResponse.setCredential(Map.of("type", "VerifiableCredential")); + + credentialDTO = DecryptedCredentialDTO.builder() + .id("cred-123") + .walletId(walletId) + .credential(vcCredentialResponse) + .build(); + + sessionData.setMatchingCredentials(List.of(credentialDTO)); + + submitRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + keyPair = mock(KeyPair.class); + jwk = mock(JWK.class); + jwsSigner = mock(JWSSigner.class); + + JWK publicJWK = mock(JWK.class); + when(jwk.toPublicJWK()).thenReturn(publicJWK); + Map jwkJsonObject = new HashMap<>(); + jwkJsonObject.put("kty", "OKP"); + jwkJsonObject.put("crv", "Ed25519"); + when(publicJWK.toJSONObject()).thenReturn(jwkJsonObject); + + when(walletCredentialService.getDecryptedCredentials(anyString(), anyString())) + .thenReturn(Collections.emptyList()); + } + + private void stubOpenId4VpCreate(OpenID4VP openID4VP) throws Exception { + when(openID4VPService.getPreRegisteredVerifiers()).thenReturn(List.of()); + when(openID4VPService.create(anyString(), anyList(), anyBoolean())).thenReturn(openID4VP); + } + + private UnsignedVPToken mockLdpUnsignedToken() { + UnsignedVPToken token = mock(UnsignedVPToken.class); + String headerB64 = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"EdDSA\"}".getBytes(StandardCharsets.UTF_8)); + when(token.getId()).thenReturn("token-id-ldp"); + when(token.getDataToSign()).thenReturn((headerB64 + ".payload").getBytes(StandardCharsets.US_ASCII)); + when(token.getFormat()).thenReturn(FormatType.LDP_VC); + when(token.getSignatureAlgorithm()).thenReturn("EdDSA"); + return token; + } + + @Test + public void testHandleVPAuthorizationRequestSuccess() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(true); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(true); + + AuthorizationPresentationExchangeRequest peRequest = mock(AuthorizationPresentationExchangeRequest.class); + when(peRequest.getClientId()).thenReturn("test-client"); + when(peRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + ClientMetadataDraft23 clientMetadata = mock(ClientMetadataDraft23.class); + when(clientMetadata.getClientName()).thenReturn("Test Verifier"); + when(clientMetadata.getLogoUri()).thenReturn("https://verifier.com/logo.png"); + when(peRequest.getClientMetadata()).thenReturn(clientMetadata); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(peRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertNotNull(result.getPresentationId()); + assertNotNull(result.getVerifiablePresentationVerifierDTO()); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getId()); + assertEquals("Test Verifier", result.getVerifiablePresentationVerifierDTO().getName()); + verify(openID4VPService).create(anyString(), anyList(), anyBoolean()); + verify(openID4VPService).getPreRegisteredVerifiers(); + } + + @Test + public void testHandleVPAuthorizationRequestWithBlankClientName() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(true); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(false); + + AuthorizationPresentationExchangeRequest peRequest = mock(AuthorizationPresentationExchangeRequest.class); + when(peRequest.getClientId()).thenReturn("test-client"); + when(peRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + ClientMetadataDraft23 clientMetadata = mock(ClientMetadataDraft23.class); + when(clientMetadata.getClientName()).thenReturn(" "); + when(peRequest.getClientMetadata()).thenReturn(clientMetadata); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(peRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getName()); + } + + @Test + public void testHandleVPAuthorizationRequestWithNullClientMetadata() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(false); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(false); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getName()); + assertNull(result.getVerifiablePresentationVerifierDTO().getLogo()); + } + + @Test + public void testHandleVPAuthorizationRequestDcqlUsesClientMetadataName() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(true); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(true); + + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getClientId()).thenReturn("test-client"); + when(dcqlRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + when(dcqlRequest.getDcqlQuery()).thenReturn(mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class)); + ClientMetadata clientMetadata = mock(ClientMetadata.class); + when(clientMetadata.getClientName()).thenReturn("DCQL Verifier"); + when(clientMetadata.getLogoUri()).thenReturn("https://verifier.com/dcql-logo.png"); + when(dcqlRequest.getClientMetadata()).thenReturn(clientMetadata); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(dcqlRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertTrue(result.isDcql()); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getId()); + assertEquals("DCQL Verifier", result.getVerifiablePresentationVerifierDTO().getName()); + assertEquals("https://verifier.com/dcql-logo.png", result.getVerifiablePresentationVerifierDTO().getLogo()); + } + + @Test + public void testHandleVPAuthorizationRequestDcqlFallsBackToClientIdWhenClientNameBlank() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(false); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(false); + + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getClientId()).thenReturn("test-client"); + when(dcqlRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + when(dcqlRequest.getDcqlQuery()).thenReturn(mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class)); + ClientMetadata clientMetadata = mock(ClientMetadata.class); + when(clientMetadata.getClientName()).thenReturn(" "); + when(dcqlRequest.getClientMetadata()).thenReturn(clientMetadata); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(dcqlRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertTrue(result.isDcql()); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getName()); + assertNull(result.getVerifiablePresentationVerifierDTO().getLogo()); + } + + @Test + public void testHandleVPAuthorizationRequestDcqlWithNullClientMetadata() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(false); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(false); + + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getClientId()).thenReturn("test-client"); + when(dcqlRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + when(dcqlRequest.getDcqlQuery()).thenReturn(mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class)); + when(dcqlRequest.getClientMetadata()).thenReturn(null); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(dcqlRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertTrue(result.isDcql()); + assertEquals("test-client", result.getVerifiablePresentationVerifierDTO().getName()); + assertNull(result.getVerifiablePresentationVerifierDTO().getLogo()); + } + + @Test + public void testHandleVPAuthorizationRequestDcqlWithoutDcqlQueryIsNotDcql() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(verifierService.isVerifierClientPreregistered(anyList(), anyString())).thenReturn(false); + when(verifierService.isVerifierTrustedByWallet(anyString(), anyString())).thenReturn(false); + + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getClientId()).thenReturn("test-client"); + when(dcqlRequest.getRedirectUri()).thenReturn("https://verifier.com/redirect"); + when(dcqlRequest.getDcqlQuery()).thenReturn(null); + when(dcqlRequest.getClientMetadata()).thenReturn(null); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(dcqlRequest); + + VPResponseDTO result = walletPresentationService.handleVPAuthorizationRequest( + urlEncodedVPAuthorizationRequest, walletId); + + assertNotNull(result); + assertFalse(result.isDcql()); + } + + @Test + public void testHandlePresentationActionSubmissionRequestSuccess() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + @Test + public void testHandlePresentationActionRejectionRequestSuccess() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/rejected"); + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))).thenReturn(verifierResponse); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + verify(openID4VPService).sendErrorToVerifier(any(), any(ErrorDTO.class)); + } + + @Test + public void testHandlePresentationActionInvalidRequest() { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(null) + .errorCode(null) + .errorMessage(null) + .build(); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(400, response.getStatusCode().value()); + } + + @Test + public void testHandlePresentationActionJOSEException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + OpenID4VP testOpenID4VP = mock(OpenID4VP.class); + stubOpenId4VpCreate(testOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + when(testOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(testOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())) + .thenThrow(new JOSEException("JWT signing error")); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue("Response body should be ErrorDTO", response.getBody() instanceof ErrorDTO); + ErrorDTO errorDTO = (ErrorDTO) response.getBody(); + assertEquals(JWT_SIGNING_ERROR.getErrorCode(), errorDTO.getErrorCode()); + } + } + + @Test + public void testHandlePresentationActionOpenID4VPException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + WalletPresentationServiceImpl spyService = spy(walletPresentationService); + doThrow(new OpenID4VPExceptions.AccessDenied("Access denied", "WalletPresentationServiceTest")) + .when(spyService).submitPresentation(any(), anyString(), anyString(), any(), anyString()); + + ResponseEntity response = spyService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(400, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue("Response body should be ErrorDTO", response.getBody() instanceof ErrorDTO); + ErrorDTO errorDTO = (ErrorDTO) response.getBody(); + assertEquals(OpenID4VPErrorCodes.ACCESS_DENIED, errorDTO.getErrorCode()); + assertEquals("Access denied", errorDTO.getErrorMessage()); + } + + @Test + public void testHandlePresentationActionKeyGenerationException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + UnsignedVPToken ldpToken = mockLdpUnsignedToken(); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(List.of(ldpToken)); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))) + .thenThrow(new KeyGenerationException(KEY_GENERATION_ERROR.getErrorCode(), "Key generation failed")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testHandlePresentationActionDecryptionException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + VerifiablePresentationSessionData testSessionData = new VerifiablePresentationSessionData(); + testSessionData.setPresentationId(presentationId); + testSessionData.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + testSessionData.setCreatedAt(Instant.now()); + testSessionData.setVerifierClientPreregistered(true); + testSessionData.setMatchingCredentials(List.of(credentialDTO)); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + UnsignedVPToken ldpToken = mockLdpUnsignedToken(); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(List.of(ldpToken)); + + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))) + .thenThrow(new DecryptionException(DECRYPTION_ERROR.getErrorCode(), "Decryption failed")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, testSessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue("Response body should be ErrorDTO", response.getBody() instanceof ErrorDTO); + ErrorDTO errorDTO = (ErrorDTO) response.getBody(); + assertEquals(DECRYPTION_ERROR.getErrorCode(), errorDTO.getErrorCode()); + } + + @Test + public void testHandlePresentationActionApiNotAccessibleException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + when(openID4VPService.getPreRegisteredVerifiers()).thenThrow(new ApiNotAccessibleException("API not accessible")); + when(openID4VPService.create(anyString(), anyList(), anyBoolean())).thenReturn(mockOpenID4VP); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testHandlePresentationActionVPErrorNotSentException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))) + .thenThrow(new VPErrorNotSentException("Failed to send error")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testHandlePresentationActionIllegalArgumentException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + VerifiablePresentationSessionData emptySessionData = new VerifiablePresentationSessionData(); + emptySessionData.setPresentationId(presentationId); + emptySessionData.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + emptySessionData.setVerifierClientPreregistered(true); + emptySessionData.setMatchingCredentials(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class)) { + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, emptySessionData, base64Key); + + assertNotNull(response); + assertEquals(400, response.getStatusCode().value()); + } + } + + @Test + public void testSubmitPresentationSuccess() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + assertEquals(OpenID4VPConstants.STATUS_SUCCESS, result.getStatus()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + @Test + public void testSubmitPresentationDcqlNestedSelectedSdClaimsSucceeds() throws Exception { + String credentialId = "a3e92fcf-b107-46c9-8d68-f10cdbe8214b"; + Map> nestedSdClaims = Map.of(credentialId, List.of("dateOfBirth")); + DcqlCredentialSelection dcqlSelection = DcqlCredentialSelection.builder() + .queryId("government-identity") + .selectedCredentialIds(List.of(credentialId)) + .selectedSdClaims(nestedSdClaims) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(dcqlSelection))) + .build(); + + credentialDTO.setId(credentialId); + credentialDTO.setIdentifier("government-identity"); + sessionData.setMatchingCredentials(List.of(credentialDTO)); + + io.mosip.openID4VP.dcql.query.CredentialQuery credentialQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(credentialQuery.getId()).thenReturn("government-identity"); + when(credentialQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + assertEquals(OpenID4VPConstants.STATUS_SUCCESS, result.getStatus()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + @Test + public void testSubmitPresentationDcqlUsesClientQueryIdAsMapKey() throws Exception { + String credentialId = "a3e92fcf-b107-46c9-8d68-f10cdbe8214b"; + String clientQueryId = "government-identity"; + DcqlCredentialSelection dcqlSelection = DcqlCredentialSelection.builder() + .queryId(clientQueryId) + .selectedCredentialIds(List.of(credentialId)) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(dcqlSelection))) + .build(); + + credentialDTO.setId(credentialId); + credentialDTO.setIdentifier("pid_query"); + sessionData.setMatchingCredentials(List.of(credentialDTO)); + + io.mosip.openID4VP.dcql.query.CredentialQuery credentialQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(credentialQuery.getId()).thenReturn(clientQueryId); + when(credentialQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + @SuppressWarnings("unchecked") + ArgumentCaptor mapCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockOpenID4VP).constructUnsignedVPToken(mapCaptor.capture()); + assertTrue(mapCaptor.getValue().containsKey(clientQueryId)); + assertFalse(mapCaptor.getValue().containsKey("pid_query")); + } + } + + @Test + public void testSubmitPresentationDcqlMergesDuplicateQuerySelectionsWhenMultipleTrue() throws Exception { + String queryId = "employment-proof"; + String credentialIdOne = "cred-employment-1"; + String credentialIdTwo = "cred-employment-2"; + DcqlCredentialSelection firstSelection = DcqlCredentialSelection.builder() + .queryId(queryId) + .selectedCredentialIds(List.of(credentialIdOne)) + .build(); + DcqlCredentialSelection secondSelection = DcqlCredentialSelection.builder() + .queryId(queryId) + .selectedCredentialIds(List.of(credentialIdTwo)) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(firstSelection, secondSelection))) + .build(); + + DecryptedCredentialDTO firstCredential = DecryptedCredentialDTO.builder() + .id(credentialIdOne) + .walletId(walletId) + .identifier(queryId) + .credential(vcCredentialResponse) + .build(); + DecryptedCredentialDTO secondCredential = DecryptedCredentialDTO.builder() + .id(credentialIdTwo) + .walletId(walletId) + .identifier(queryId) + .credential(vcCredentialResponse) + .build(); + sessionData.setMatchingCredentials(List.of(firstCredential, secondCredential)); + when(walletCredentialService.getDecryptedCredentials(anyString(), anyString())) + .thenReturn(List.of(firstCredential, secondCredential)); + + io.mosip.openID4VP.dcql.query.CredentialQuery credentialQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(credentialQuery.getId()).thenReturn(queryId); + when(credentialQuery.getMultiple()).thenReturn(true); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken(), mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + @SuppressWarnings("unchecked") + ArgumentCaptor>> mapCaptor = + ArgumentCaptor.forClass(Map.class); + verify(mockOpenID4VP).constructUnsignedVPToken(mapCaptor.capture()); + assertEquals(2, mapCaptor.getValue().get(queryId).size()); + } + } + + @Test + public void testSubmitPresentationDcqlClaimSetsAutoResolvesSdClaimPaths() throws Exception { + String credentialId = "d0b504e8-adaf-4df1-9ba0-2d2a5c52e519"; + String queryId = "age-proof"; + DcqlCredentialSelection dcqlSelection = DcqlCredentialSelection.builder() + .queryId(queryId) + .selectedCredentialIds(List.of(credentialId)) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(dcqlSelection))) + .build(); + + String sdJwt = "header.payload.sig~ageDiscB64~dobDiscB64~"; + vcCredentialResponse = VCCredentialResponse.builder() + .format(CredentialFormat.DC_SD_JWT.getFormat()) + .credential(sdJwt) + .build(); + credentialDTO = DecryptedCredentialDTO.builder() + .id(credentialId) + .identifier(queryId) + .credential(vcCredentialResponse) + .build(); + sessionData.setMatchingCredentials(List.of(credentialDTO)); + when(walletCredentialService.getDecryptedCredentials(anyString(), anyString())) + .thenReturn(List.of(credentialDTO)); + + io.mosip.openID4VP.dcql.query.ClaimsQuery ageClaim = + new io.mosip.openID4VP.dcql.query.ClaimsQuery( + "age-above-18", List.of("age_above_18"), + List.of(new io.mosip.openID4VP.dcql.query.ClaimValue.BoolValue(true))); + io.mosip.openID4VP.dcql.query.ClaimsQuery dobClaim = + new io.mosip.openID4VP.dcql.query.ClaimsQuery( + "date-of-birth", List.of("dateOfBirth"), null); + io.mosip.openID4VP.dcql.query.CredentialQuery credentialQuery = + new io.mosip.openID4VP.dcql.query.CredentialQuery( + queryId, CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, List.of(ageClaim, dobClaim), + List.of(List.of("age-above-18"), List.of("date-of-birth"))); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("age_above_18", List.of("ageDiscB64")); + sdClaims.put("dateOfBirth", List.of("dobDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + allProps.put("publicClaims", Map.of("vct", "AgeProofCredential")); + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + stubOpenId4VpCreate(mockOpenID4VP); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockSdJwtUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + @SuppressWarnings("unchecked") + ArgumentCaptor>> mapCaptor = + ArgumentCaptor.forClass(Map.class); + verify(mockOpenID4VP).constructUnsignedVPToken(mapCaptor.capture()); + io.mosip.openID4VP.wallet.Credential submitted = + mapCaptor.getValue().get(queryId).get(0); + String filteredSdJwt = (String) submitted.getData(); + assertEquals("header.payload.sig~ageDiscB64~", filteredSdJwt); + } + } + + @Test + public void testSubmitPresentationDcqlUnionsClaimSetPathsForSameCredentialAcrossQueries() throws Exception { + String credentialId = "d0b504e8-adaf-4df1-9ba0-2d2a5c52e519"; + String ageQueryId = "age-proof"; + String dobQueryId = "dob-proof"; + DcqlCredentialSelection ageSelection = DcqlCredentialSelection.builder() + .queryId(ageQueryId) + .selectedCredentialIds(List.of(credentialId)) + .build(); + DcqlCredentialSelection dobSelection = DcqlCredentialSelection.builder() + .queryId(dobQueryId) + .selectedCredentialIds(List.of(credentialId)) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(ageSelection, dobSelection))) + .build(); + + String sdJwt = "header.payload.sig~ageDiscB64~dobDiscB64~"; + vcCredentialResponse = VCCredentialResponse.builder() + .format(CredentialFormat.DC_SD_JWT.getFormat()) + .credential(sdJwt) + .build(); + credentialDTO = DecryptedCredentialDTO.builder() + .id(credentialId) + .identifier(ageQueryId) + .credential(vcCredentialResponse) + .build(); + sessionData.setMatchingCredentials(List.of(credentialDTO)); + when(walletCredentialService.getDecryptedCredentials(anyString(), anyString())) + .thenReturn(List.of(credentialDTO)); + + io.mosip.openID4VP.dcql.query.ClaimsQuery ageClaim = + new io.mosip.openID4VP.dcql.query.ClaimsQuery( + "age-above-18", List.of("age_above_18"), + List.of(new io.mosip.openID4VP.dcql.query.ClaimValue.BoolValue(true))); + io.mosip.openID4VP.dcql.query.ClaimsQuery dobClaim = + new io.mosip.openID4VP.dcql.query.ClaimsQuery( + "date-of-birth", List.of("dateOfBirth"), null); + io.mosip.openID4VP.dcql.query.CredentialQuery ageCredentialQuery = + new io.mosip.openID4VP.dcql.query.CredentialQuery( + ageQueryId, CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, List.of(ageClaim), + List.of(List.of("age-above-18"))); + io.mosip.openID4VP.dcql.query.CredentialQuery dobCredentialQuery = + new io.mosip.openID4VP.dcql.query.CredentialQuery( + dobQueryId, CredentialFormat.DC_SD_JWT.getFormat(), false, + Map.of(), false, List.of(dobClaim), + List.of(List.of("date-of-birth"))); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(ageCredentialQuery, dobCredentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("age_above_18", List.of("ageDiscB64")); + sdClaims.put("dateOfBirth", List.of("dobDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + allProps.put("publicClaims", Map.of("vct", "AgeProofCredential")); + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + stubOpenId4VpCreate(mockOpenID4VP); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockSdJwtUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + @SuppressWarnings("unchecked") + ArgumentCaptor>> mapCaptor = + ArgumentCaptor.forClass(Map.class); + verify(mockOpenID4VP).constructUnsignedVPToken(mapCaptor.capture()); + String filteredSdJwt = (String) mapCaptor.getValue().get(ageQueryId).get(0).getData(); + assertEquals("header.payload.sig~ageDiscB64~dobDiscB64~", filteredSdJwt); + } + } + + @Test + public void testValidateDcqlSelectionsRejectsMultipleFalseWithTwoCredentials() throws Exception { + String queryId = "government-identity"; + DcqlCredentialSelection dcqlSelection = DcqlCredentialSelection.builder() + .queryId(queryId) + .selectedCredentialIds(List.of("cred-1", "cred-2")) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(dcqlSelection))) + .build(); + + io.mosip.openID4VP.dcql.query.CredentialQuery credentialQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(credentialQuery.getId()).thenReturn(queryId); + when(credentialQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(credentialQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + + try { + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains("has multiple=false but 2 credential(s) were selected")); + } + } + + @Test + public void testValidateDcqlSelectionsRejectsCredentialSetWithMixedOptions() throws Exception { + io.mosip.openID4VP.dcql.query.CredentialSetQuery setQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialSetQuery.class); + when(setQuery.getRequired()).thenReturn(true); + when(setQuery.getOptions()).thenReturn(List.of(List.of("pan"), List.of("aadhaar"))); + + io.mosip.openID4VP.dcql.query.CredentialQuery panQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(panQuery.getId()).thenReturn("pan"); + when(panQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.CredentialQuery aadhaarQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(aadhaarQuery.getId()).thenReturn("aadhaar"); + when(aadhaarQuery.getMultiple()).thenReturn(false); + + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(panQuery, aadhaarQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(List.of(setQuery)); + + DcqlCredentialSelection panSelection = DcqlCredentialSelection.builder() + .queryId("pan") + .selectedCredentialIds(List.of("cred-pan")) + .build(); + DcqlCredentialSelection aadhaarSelection = DcqlCredentialSelection.builder() + .queryId("aadhaar") + .selectedCredentialIds(List.of("cred-aadhaar")) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(panSelection, aadhaarSelection))) + .build(); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + + try { + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains( + "Credential selection must satisfy exactly one option in credential_set")); + } + } + + @Test + public void testValidateDcqlSelectionsRejectsPartialSelectionWhenCredentialSetsSynthesized() throws Exception { + DcqlCredentialSelection governmentSelection = DcqlCredentialSelection.builder() + .queryId("government-identity") + .selectedCredentialIds(List.of("cred-government")) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(governmentSelection))) + .build(); + + io.mosip.openID4VP.dcql.query.CredentialQuery governmentQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(governmentQuery.getId()).thenReturn("government-identity"); + when(governmentQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.CredentialQuery ageQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(ageQuery.getId()).thenReturn("age-proof"); + when(ageQuery.getMultiple()).thenReturn(false); + + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(governmentQuery, ageQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + + try { + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains( + "Credential selection must satisfy exactly one option in credential_set")); + assertTrue(e.getMessage().contains("age-proof")); + } + } + + @Test + public void testSubmitPresentationDcqlCredentialSetSingleOptionSucceeds() throws Exception { + String panCredentialId = "cred-pan-001"; + DcqlCredentialSelection panSelection = DcqlCredentialSelection.builder() + .queryId("pan") + .selectedCredentialIds(List.of(panCredentialId)) + .build(); + SubmitPresentationRequestDTO dcqlRequest = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofDcql(List.of(panSelection))) + .build(); + + DecryptedCredentialDTO panCredential = DecryptedCredentialDTO.builder() + .id(panCredentialId) + .identifier("pan") + .walletId(walletId) + .credential(vcCredentialResponse) + .build(); + sessionData.setMatchingCredentials(List.of(panCredential)); + + io.mosip.openID4VP.dcql.query.CredentialSetQuery setQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialSetQuery.class); + when(setQuery.getRequired()).thenReturn(true); + when(setQuery.getOptions()).thenReturn(List.of(List.of("pan"), List.of("aadhaar"))); + + io.mosip.openID4VP.dcql.query.CredentialQuery panQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(panQuery.getId()).thenReturn("pan"); + when(panQuery.getMultiple()).thenReturn(false); + io.mosip.openID4VP.dcql.query.CredentialQuery aadhaarQuery = + mock(io.mosip.openID4VP.dcql.query.CredentialQuery.class); + when(aadhaarQuery.getId()).thenReturn("aadhaar"); + when(aadhaarQuery.getMultiple()).thenReturn(false); + + io.mosip.openID4VP.dcql.query.DCQLQuery dcqlQuery = + mock(io.mosip.openID4VP.dcql.query.DCQLQuery.class); + when(dcqlQuery.getCredentials()).thenReturn(List.of(panQuery, aadhaarQuery)); + when(dcqlQuery.getCredentialSets()).thenReturn(List.of(setQuery)); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + when(openID4VPService.resolveDcqlQuery(anyString(), anyString(), anyBoolean())) + .thenReturn(dcqlQuery); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"selectedCredentials\":[]}"); + + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, dcqlRequest, base64Key); + + assertEquals(OpenID4VPConstants.STATUS_SUCCESS, result.getStatus()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + private UnsignedVPToken mockSdJwtUnsignedToken() { + UnsignedVPToken token = mock(UnsignedVPToken.class); + when(token.getId()).thenReturn("token-id-sdjwt"); + when(token.getFormat()).thenReturn(FormatType.DC_SD_JWT); + when(token.getSignatureAlgorithm()).thenReturn("EdDSA"); + String header = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"EdDSA\"}".getBytes(StandardCharsets.UTF_8)); + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"sub\":\"test\"}".getBytes(StandardCharsets.UTF_8)); + when(token.getDataToSign()).thenReturn((header + "." + payload).getBytes(StandardCharsets.US_ASCII)); + return token; + } + + @Test + public void testSubmitPresentationShareFailed() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(500); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/error"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + assertEquals(OpenID4VPConstants.STATUS_ERROR, result.getStatus()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + @Test + public void testSubmitPresentationExceptionDuringShare() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenThrow(new RuntimeException("Network error")); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + assertEquals(OpenID4VPConstants.STATUS_ERROR, result.getStatus()); + assertNull(result.getRedirectUri()); + verify(verifiablePresentationsRepository).save(any(VerifiablePresentation.class)); + } + } + + @Test(expected = java.lang.IllegalArgumentException.class) + public void testSubmitPresentationNullRequest() throws Exception { + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, null, base64Key); + } + + @Test(expected = java.lang.IllegalArgumentException.class) + public void testSubmitPresentationEmptyCredentials() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(Collections.emptyList())) + .build(); + + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, request, base64Key); + } + + @Test + public void testHandlePresentationSubmissionNullBase64Key() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, null); + + assertNotNull(response); + assertEquals(400, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue("Response body should be ErrorDTO", response.getBody() instanceof ErrorDTO); + ErrorDTO errorDTO = (ErrorDTO) response.getBody(); + assertEquals(INVALID_REQUEST.getErrorCode(), errorDTO.getErrorCode()); + } + + @Test + public void testHandlePresentationSubmissionBlankBase64Key() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, " "); + + assertNotNull(response); + assertEquals(400, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertTrue("Response body should be ErrorDTO", response.getBody() instanceof ErrorDTO); + ErrorDTO errorDTO = (ErrorDTO) response.getBody(); + assertEquals(INVALID_REQUEST.getErrorCode(), errorDTO.getErrorCode()); + } + + @Test + public void testSignVPTokenUnsupportedFormat() throws Exception { + VCCredentialResponse unsupportedFormatCredential = new VCCredentialResponse(); + unsupportedFormatCredential.setFormat("jwt_vc_json"); + unsupportedFormatCredential.setCredential(Map.of("type", "VerifiableCredential")); + + DecryptedCredentialDTO credWithUnsupportedFormat = DecryptedCredentialDTO.builder() + .id("cred-123") + .walletId(walletId) + .credential(unsupportedFormatCredential) + .build(); + + VerifiablePresentationSessionData sessionDataWithUnsupportedFormat = new VerifiablePresentationSessionData(); + sessionDataWithUnsupportedFormat.setMatchingCredentials(List.of(credWithUnsupportedFormat)); + sessionDataWithUnsupportedFormat.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + sessionDataWithUnsupportedFormat.setVerifierClientPreregistered(true); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class)) { + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + + try { + walletPresentationService.submitPresentation( + sessionDataWithUnsupportedFormat, walletId, presentationId, submitRequest, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains("Unsupported credential format")); + } + } + } + + @Test(expected = IllegalStateException.class) + public void testFetchSelectedCredentialsNullMatchingCredentials() throws Exception { + VerifiablePresentationSessionData nullSessionData = new VerifiablePresentationSessionData(); + nullSessionData.setPresentationId(presentationId); + nullSessionData.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + nullSessionData.setVerifierClientPreregistered(true); + nullSessionData.setMatchingCredentials(null); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + walletPresentationService.submitPresentation( + nullSessionData, walletId, presentationId, request, base64Key); + } + + @Test + public void testFetchSelectedCredentialsRejectsUnknownCredentialId() throws Exception { + VerifiablePresentationSessionData sessionDataWithDifferentCred = new VerifiablePresentationSessionData(); + DecryptedCredentialDTO differentCred = DecryptedCredentialDTO.builder() + .id("cred-999") + .walletId(walletId) + .credential(vcCredentialResponse) + .build(); + sessionDataWithDifferentCred.setMatchingCredentials(List.of(differentCred)); + sessionDataWithDifferentCred.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + sessionDataWithDifferentCred.setVerifierClientPreregistered(true); + + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123"))) + .build(); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + try { + walletPresentationService.submitPresentation( + sessionDataWithDifferentCred, walletId, presentationId, request, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains("Selected credential not found in session: cred-123")); + } + } + + @Test + public void testFetchSelectedCredentialsRejectsPartiallyUnknownCredentialIds() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .selectedCredentials(SelectedCredentials.ofStrings(List.of("cred-123", "bogus-id"))) + .build(); + + stubOpenId4VpCreate(mockOpenID4VP); + when(mockOpenID4VP.authenticateVerifier(anyString())).thenReturn(mockAuthorizationRequest); + + try { + walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, request, base64Key); + fail("Should throw InvalidRequestException"); + } catch (InvalidRequestException e) { + assertTrue(e.getMessage().contains("Selected credential not found in session: bogus-id")); + } + } + + @Test(expected = InvalidRequestException.class) + public void testMapStringToFormatTypeNullFormat() throws Exception { + VCCredentialResponse nullFormatCredential = new VCCredentialResponse(); + nullFormatCredential.setFormat(null); + nullFormatCredential.setCredential(Map.of("type", "VerifiableCredential")); + + DecryptedCredentialDTO credWithNullFormat = DecryptedCredentialDTO.builder() + .id("cred-123") + .walletId(walletId) + .credential(nullFormatCredential) + .build(); + + VerifiablePresentationSessionData sessionDataWithNullFormat = new VerifiablePresentationSessionData(); + sessionDataWithNullFormat.setMatchingCredentials(List.of(credWithNullFormat)); + sessionDataWithNullFormat.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + sessionDataWithNullFormat.setVerifierClientPreregistered(true); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class)) { + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + + walletPresentationService.submitPresentation( + sessionDataWithNullFormat, walletId, presentationId, submitRequest, base64Key); + } + } + + @Test(expected = InvalidRequestException.class) + public void testMapStringToFormatTypeUnsupportedFormat() throws Exception { + VCCredentialResponse unsupportedFormatCredential = new VCCredentialResponse(); + unsupportedFormatCredential.setFormat("jwt_vc"); + unsupportedFormatCredential.setCredential(Map.of("type", "VerifiableCredential")); + + DecryptedCredentialDTO credWithUnsupportedFormat = DecryptedCredentialDTO.builder() + .id("cred-123") + .walletId(walletId) + .credential(unsupportedFormatCredential) + .build(); + + VerifiablePresentationSessionData sessionDataWithUnsupportedFormat = new VerifiablePresentationSessionData(); + sessionDataWithUnsupportedFormat.setMatchingCredentials(List.of(credWithUnsupportedFormat)); + sessionDataWithUnsupportedFormat.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + sessionDataWithUnsupportedFormat.setVerifierClientPreregistered(true); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class)) { + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + + walletPresentationService.submitPresentation( + sessionDataWithUnsupportedFormat, walletId, presentationId, submitRequest, base64Key); + } + } + + @Test + public void testStorePresentationRecordNullSessionData() throws Exception { + VerifiablePresentationSessionData nullSessionData = null; + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + + try { + walletPresentationService.submitPresentation( + nullSessionData, walletId, presentationId, submitRequest, base64Key); + } catch (Exception e) { + } + } + } + + @Test + public void testStorePresentationRecordExceptionDuringSave() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + when(verifiablePresentationsRepository.save(any(VerifiablePresentation.class))) + .thenThrow(new RuntimeException("Database error")); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + } + } + + @Test + public void testExtractVerifierIdNullAuthorizationRequest() throws Exception { + VerifiablePresentationSessionData sessionDataWithNullAuth = new VerifiablePresentationSessionData(); + sessionDataWithNullAuth.setAuthorizationRequest(null); + sessionDataWithNullAuth.setMatchingCredentials(List.of(credentialDTO)); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionDataWithNullAuth, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + verify(verifiablePresentationsRepository).save(argThat(presentation -> + "unknown".equals(((VerifiablePresentation) presentation).getVerifierId()) + )); + } + } + + @Test + public void testExtractVerifierIdExceptionDuringExtraction() throws Exception { + VerifiablePresentationSessionData sessionDataWithInvalidAuth = new VerifiablePresentationSessionData(); + sessionDataWithInvalidAuth.setAuthorizationRequest("invalid-url"); + sessionDataWithInvalidAuth.setMatchingCredentials(List.of(credentialDTO)); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenThrow(new RuntimeException("URL parsing error")); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionDataWithInvalidAuth, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + verify(verifiablePresentationsRepository).save(argThat(presentation -> + "unknown".equals(((VerifiablePresentation) presentation).getVerifierId()) + )); + } + } + + @Test + public void testExtractVerifierAuthRequestNullAuthorizationRequest() throws Exception { + VerifiablePresentationSessionData sessionDataWithNullAuth = new VerifiablePresentationSessionData(); + sessionDataWithNullAuth.setAuthorizationRequest(null); + sessionDataWithNullAuth.setMatchingCredentials(List.of(credentialDTO)); + + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + when(objectMapper.writeValueAsString(any())).thenReturn("{\"kty\":\"OKP\"}"); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionDataWithNullAuth, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + } + } + + @Test + public void testExtractVerifierAuthRequestExceptionDuringExtraction() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + when(objectMapper.writeValueAsString(any())) + .thenReturn("{\"kty\":\"OKP\"}") + .thenThrow(new JsonProcessingException("JSON error") {}); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + } + } + + @Test + public void testCreatePresentationDataExceptionDuringCreation() throws Exception { + stubOpenId4VpCreate(mockOpenID4VP); + when(verifierService.getTrustedVerifiers()).thenReturn(verifiersDTO); + when(keyPairService.getKeyPairFromDB(anyString(), anyString(), any(SigningAlgorithm.class))).thenReturn(keyPair); + + List unsignedTokens = List.of(mockLdpUnsignedToken()); + when(mockOpenID4VP.constructUnsignedVPToken(anyMap())).thenReturn(unsignedTokens); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getStatusCode()).thenReturn(200); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/success"); + when(mockOpenID4VP.sendVPResponseToVerifier(any())).thenReturn(verifierResponse); + + when(objectMapper.writeValueAsString(any())) + .thenReturn("{\"kty\":\"OKP\"}") + .thenThrow(new JsonProcessingException("JSON error") {}); + + try (MockedStatic jwtUtilMock = mockStatic(SigningKeyUtil.class); + MockedStatic urlUtilMock = mockStatic(UrlParameterUtils.class)) { + + jwtUtilMock.when(() -> SigningKeyUtil.generateJwk(any(), any())).thenReturn(jwk); + jwtUtilMock.when(() -> SigningKeyUtil.createSigner(any(), any())).thenReturn(jwsSigner); + + urlUtilMock.when(() -> UrlParameterUtils.extractQueryParameter(anyString(), anyString())) + .thenReturn("test-client"); + + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("signature")); + SubmitPresentationResponseDTO result = walletPresentationService.submitPresentation( + sessionData, walletId, presentationId, submitRequest, base64Key); + + assertNotNull(result); + } + } + + @Test + public void testRejectVerifierSuccess() throws Exception { + ErrorDTO errorDTO = new ErrorDTO(); + errorDTO.setErrorCode("access_denied"); + errorDTO.setErrorMessage("User denied access"); + + VerifierResponse verifierResponse = mock(VerifierResponse.class); + when(verifierResponse.getRedirectUri()).thenReturn("https://verifier.com/rejected"); + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))).thenReturn(verifierResponse); + + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(200, response.getStatusCode().value()); + verify(openID4VPService).sendErrorToVerifier(any(), any(ErrorDTO.class)); + } + + @Test + public void testRejectVerifierApiNotAccessibleException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))) + .thenThrow(new ApiNotAccessibleException("API not accessible")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testRejectVerifierIOException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))) + .thenThrow(new IOException("IO error")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testRejectVerifierURISyntaxException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))) + .thenThrow(new URISyntaxException("invalid", "URI syntax error")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testRejectVerifierIllegalArgumentException() throws Exception { + SubmitPresentationRequestDTO request = SubmitPresentationRequestDTO.builder() + .errorCode("access_denied") + .errorMessage("User denied access") + .build(); + + when(openID4VPService.sendErrorToVerifier(any(), any(ErrorDTO.class))) + .thenThrow(new java.lang.IllegalArgumentException("Invalid argument")); + + ResponseEntity response = walletPresentationService.handlePresentationAction( + walletId, presentationId, request, sessionData, base64Key); + + assertNotNull(response); + assertEquals(500, response.getStatusCode().value()); + } + + @Test + public void testSignVPTokensSignsSdJwtKBT() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "signVPTokens", List.class, String.class, String.class); + method.setAccessible(true); + + String kbHeader = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"ES256\"}".getBytes(StandardCharsets.UTF_8)); + String kbPayload = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"nonce\":\"abc\"}".getBytes(StandardCharsets.UTF_8)); + String unsignedKBT = kbHeader + "." + kbPayload; + + UnsignedVPToken mockToken = mock(UnsignedVPToken.class); + when(mockToken.getId()).thenReturn("token-id-kbt"); + when(mockToken.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(mockToken.getDataToSign()).thenReturn(unsignedKBT.getBytes(StandardCharsets.US_ASCII)); + when(mockToken.getSignatureAlgorithm()).thenReturn("ES256"); + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("test-sig")); + + KeyPair mockKeyPair = mock(KeyPair.class); + JWK mockJwk = mock(JWK.class); + when(keyPairService.getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ES256))) + .thenReturn(mockKeyPair); + + try (MockedStatic mockedSigningKeyUtil = mockStatic(SigningKeyUtil.class)) { + mockedSigningKeyUtil.when(() -> SigningKeyUtil.generateJwk(SigningAlgorithm.ES256, mockKeyPair)) + .thenReturn(mockJwk); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.createSigner(SigningAlgorithm.ES256, mockJwk)) + .thenReturn(jwsSigner); + + @SuppressWarnings("unchecked") + List results = (List) method.invoke( + walletPresentationService, List.of(mockToken), "wallet-1", "base64Key"); + + assertNotNull(results); + assertEquals(1, results.size()); + verify(jwsSigner).sign(any(JWSHeader.class), eq(unsignedKBT.getBytes(StandardCharsets.US_ASCII))); + } + } + + @Test + public void testSignVPTokensThrowsWhenJwsHeaderCannotBeParsed() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "signVPTokens", List.class, String.class, String.class); + method.setAccessible(true); + + String badHeader = Base64.getUrlEncoder().withoutPadding() + .encodeToString("not-valid-jws-header".getBytes(StandardCharsets.UTF_8)); + String unsignedKbt = badHeader + ".payload"; + + UnsignedVPToken mockToken = mock(UnsignedVPToken.class); + when(mockToken.getId()).thenReturn("token-id-bad"); + when(mockToken.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(mockToken.getDataToSign()).thenReturn(unsignedKbt.getBytes(StandardCharsets.US_ASCII)); + when(mockToken.getSignatureAlgorithm()).thenReturn("ES256"); + + KeyPair mockKeyPair = mock(KeyPair.class); + JWK mockJwk = mock(JWK.class); + when(keyPairService.getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ES256))) + .thenReturn(mockKeyPair); + + try (MockedStatic mockedSigningKeyUtil = mockStatic(SigningKeyUtil.class)) { + mockedSigningKeyUtil.when(() -> SigningKeyUtil.generateJwk(SigningAlgorithm.ES256, mockKeyPair)) + .thenReturn(mockJwk); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.createSigner(SigningAlgorithm.ES256, mockJwk)) + .thenReturn(jwsSigner); + + try { + method.invoke(walletPresentationService, List.of(mockToken), "wallet-1", "base64Key"); + fail("Should throw JOSEException"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof JOSEException); + assertEquals("Failed to parse JWS header for VP token signing", e.getCause().getMessage()); + } + } + } + + @Test + public void testBuildFilteredSdJwtWhenSelectedPathsIsNullSharesNoDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~disc1~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + // No selected paths -> user disclosed nothing -> credential JWT with zero disclosures + String result = (String) method.invoke(walletPresentationService, credential, null); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenSelectedPathsFiltersToSelectedDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~emailDiscB64~nameDiscB64~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("email", List.of("emailDiscB64")); + sdClaims.put("name", List.of("nameDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals("header.payload.sig~emailDiscB64~", result); + } + + @Test + public void testBuildFilteredSdJwtNormalizesPathsWithoutDollarDotPrefix() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~emailDiscB64~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("email", List.of("emailDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + // "email" without "$." prefix should produce the same result as "$.email" + String result = (String) method.invoke(walletPresentationService, credential, List.of("email")); + assertEquals("header.payload.sig~emailDiscB64~", result); + } + + @Test + public void testBuildFilteredSdJwtResolvesCredentialSubjectPrefixedSdClaimKeys() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~dobDiscB64~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("credentialSubject.dateOfBirth", List.of("dobDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("dateOfBirth")); + assertEquals("header.payload.sig~dobDiscB64~", result); + } + + @Test + public void testBuildFilteredSdJwtResolvesArrayIndexedCredentialSubjectPaths() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~dobDiscB64~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("credentialSubject[0].dateOfBirth", List.of("dobDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.dateOfBirth")); + assertEquals("header.payload.sig~dobDiscB64~", result); + } + + @Test + public void testBuildFilteredSdJwtDoesNotMatchNestedPathBySuffix() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~wrongDiscB64~nameDiscB64~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("parent.name", List.of("wrongDiscB64")); + sdClaims.put("name", List.of("nameDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.name")); + assertEquals("header.payload.sig~nameDiscB64~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenSdClaimsKeyMissingSharesNoDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~disc~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + Map allProps = new HashMap<>(); + allProps.put("publicClaims", new HashMap<>()); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + // sdClaims key missing -> cannot resolve selections -> credential JWT with zero disclosures + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenSelectedPathsEmptySharesNoDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential("header.payload.sig~disc1~disc2~") + .build()) + .build(); + + String result = (String) method.invoke(walletPresentationService, credential, Collections.emptyList()); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenSdClaimsMapEmptySharesNoDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential("header.payload.sig~disc~") + .build()) + .build(); + + Map allProps = new HashMap<>(); + allProps.put("sdClaims", new LinkedHashMap<>()); // present but empty + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenSelectedPathNotInSdClaimsSharesNoDisclosures() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential("header.payload.sig~nameDiscB64~") + .build()) + .build(); + + Map sdClaims = new LinkedHashMap<>(); + sdClaims.put("name", List.of("nameDiscB64")); + Map allProps = new HashMap<>(); + allProps.put("sdClaims", sdClaims); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + doReturn(allProps).when(credentialFormatHandler).extractAllCredentialProperties(any()); + + // user selects "email" but only "name" is selectively disclosable -> no disclosures shared + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testBuildFilteredSdJwtWhenCredentialNotStringReturnsRawValue() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + Map mapCredential = new HashMap<>(); + mapCredential.put("type", "not-a-string-sdjwt"); + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(mapCredential) + .build()) + .build(); + + // Non-String payload cannot be SD-JWT filtered; the raw value is returned as-is + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals(String.valueOf(mapCredential), result); + } + + @Test + public void testExtractSdClaimsMapReturnsNullWhenHandlerThrows() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "extractSdClaimsMap", DecryptedCredentialDTO.class); + method.setAccessible(true); + + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("sd-cred-1") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.DC_SD_JWT.getFormat()) + .credential("header.payload.sig~disc~") + .build()) + .build(); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.DC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractAllCredentialProperties(any())) + .thenThrow(new RuntimeException("parse failure")); + + @SuppressWarnings("unchecked") + Map result = (Map) method.invoke(walletPresentationService, credential); + + assertNull(result); + } + + @Test + public void testExtractVerifierAuthRequestReturnsEmptyJsonWhenSerializationFails() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "extractVerifierAuthRequest", VerifiablePresentationSessionData.class); + method.setAccessible(true); + + VerifiablePresentationSessionData data = new VerifiablePresentationSessionData(); + data.setAuthorizationRequest(urlEncodedVPAuthorizationRequest); + + when(objectMapper.writeValueAsString(any())) + .thenThrow(new JsonProcessingException("serialization failed") {}); + + String result = (String) method.invoke(walletPresentationService, data); + assertEquals("{}", result); + } + + @Test + public void testBuildFilteredSdJwtReturnsCredentialJwtOnlyWhenFilteringFails() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "buildFilteredSdJwt", DecryptedCredentialDTO.class, List.class); + method.setAccessible(true); + + String originalSdJwt = "header.payload.sig~disc1~disc2~"; + DecryptedCredentialDTO credential = DecryptedCredentialDTO.builder() + .id("cred-id") + .credential(VCCredentialResponse.builder() + .format(CredentialFormat.VC_SD_JWT.getFormat()) + .credential(originalSdJwt) + .build()) + .build(); + + when(credentialFormatHandlerFactory.getHandler(CredentialFormat.VC_SD_JWT.getFormat())) + .thenReturn(credentialFormatHandler); + when(credentialFormatHandler.extractAllCredentialProperties(any())) + .thenThrow(new RuntimeException("filter failure")); + + String result = (String) method.invoke(walletPresentationService, credential, List.of("$.email")); + assertEquals("header.payload.sig~", result); + } + + @Test + public void testSignVPTokensBuildsPerCredentialSignersForDifferentAlgs() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "signVPTokens", List.class, String.class, String.class); + method.setAccessible(true); + + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"nonce\":\"abc\"}".getBytes(StandardCharsets.UTF_8)); + String es256Kbt = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"ES256\"}".getBytes(StandardCharsets.UTF_8)) + "." + payload; + String eddsaKbt = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"EdDSA\"}".getBytes(StandardCharsets.UTF_8)) + "." + payload; + + UnsignedVPToken es256Token = mock(UnsignedVPToken.class); + when(es256Token.getId()).thenReturn("token-id-es256"); + when(es256Token.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(es256Token.getDataToSign()).thenReturn(es256Kbt.getBytes(StandardCharsets.US_ASCII)); + when(es256Token.getSignatureAlgorithm()).thenReturn("ES256"); + + UnsignedVPToken eddsaToken = mock(UnsignedVPToken.class); + when(eddsaToken.getId()).thenReturn("token-id-eddsa"); + when(eddsaToken.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(eddsaToken.getDataToSign()).thenReturn(eddsaKbt.getBytes(StandardCharsets.US_ASCII)); + when(eddsaToken.getSignatureAlgorithm()).thenReturn("EdDSA"); + + KeyPair mockKeyPair = mock(KeyPair.class); + JWK mockJwk = mock(JWK.class); + when(keyPairService.getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ES256))) + .thenReturn(mockKeyPair); + when(keyPairService.getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ED25519))) + .thenReturn(mockKeyPair); + + JWSSigner es256Signer = mock(JWSSigner.class); + JWSSigner eddsaSigner = mock(JWSSigner.class); + when(es256Signer.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("es256-sig")); + when(eddsaSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("eddsa-sig")); + + try (MockedStatic mockedSigningKeyUtil = mockStatic(SigningKeyUtil.class)) { + mockedSigningKeyUtil.when(() -> SigningKeyUtil.generateJwk(SigningAlgorithm.ES256, mockKeyPair)).thenReturn(mockJwk); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.generateJwk(SigningAlgorithm.ED25519, mockKeyPair)).thenReturn(mockJwk); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.createSigner(SigningAlgorithm.ES256, mockJwk)).thenReturn(es256Signer); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.createSigner(SigningAlgorithm.ED25519, mockJwk)).thenReturn(eddsaSigner); + + @SuppressWarnings("unchecked") + List results = (List) method.invoke( + walletPresentationService, List.of(es256Token, eddsaToken), "wallet-1", "base64Key"); + + assertEquals(2, results.size()); + verify(es256Signer).sign(any(JWSHeader.class), eq(es256Kbt.getBytes(StandardCharsets.US_ASCII))); + verify(eddsaSigner).sign(any(JWSHeader.class), eq(eddsaKbt.getBytes(StandardCharsets.US_ASCII))); + } + } + + @Test + public void testSignVPTokensReusesSignerForSameAlg() throws Exception { + Method method = WalletPresentationServiceImpl.class.getDeclaredMethod( + "signVPTokens", List.class, String.class, String.class); + method.setAccessible(true); + + String header = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"alg\":\"ES256\"}".getBytes(StandardCharsets.UTF_8)); + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString("{\"nonce\":\"abc\"}".getBytes(StandardCharsets.UTF_8)); + String kbt = header + "." + payload; + + UnsignedVPToken token1 = mock(UnsignedVPToken.class); + when(token1.getId()).thenReturn("token-id-1"); + when(token1.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(token1.getDataToSign()).thenReturn(kbt.getBytes(StandardCharsets.US_ASCII)); + when(token1.getSignatureAlgorithm()).thenReturn("ES256"); + + UnsignedVPToken token2 = mock(UnsignedVPToken.class); + when(token2.getId()).thenReturn("token-id-2"); + when(token2.getFormat()).thenReturn(FormatType.VC_SD_JWT); + when(token2.getDataToSign()).thenReturn(kbt.getBytes(StandardCharsets.US_ASCII)); + when(token2.getSignatureAlgorithm()).thenReturn("ES256"); + + KeyPair mockKeyPair = mock(KeyPair.class); + JWK mockJwk = mock(JWK.class); + when(keyPairService.getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ES256))) + .thenReturn(mockKeyPair); + when(jwsSigner.sign(any(JWSHeader.class), any(byte[].class))).thenReturn(Base64URL.encode("sig")); + + try (MockedStatic mockedSigningKeyUtil = mockStatic(SigningKeyUtil.class)) { + mockedSigningKeyUtil.when(() -> SigningKeyUtil.generateJwk(SigningAlgorithm.ES256, mockKeyPair)).thenReturn(mockJwk); + mockedSigningKeyUtil.when(() -> SigningKeyUtil.createSigner(SigningAlgorithm.ES256, mockJwk)).thenReturn(jwsSigner); + + @SuppressWarnings("unchecked") + List results = (List) method.invoke( + walletPresentationService, List.of(token1, token2), "wallet-1", "base64Key"); + + assertEquals(2, results.size()); + verify(keyPairService, times(1)).getKeyPairFromDB(eq("wallet-1"), eq("base64Key"), eq(SigningAlgorithm.ES256)); + verify(jwsSigner, times(2)).sign(any(JWSHeader.class), eq(kbt.getBytes(StandardCharsets.US_ASCII))); + } + } +} diff --git a/src/test/java/io/mosip/mimoto/service/WalletServiceTest.java b/src/test/java/io/mosip/mimoto/service/WalletServiceTest.java new file mode 100644 index 000000000..028926115 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/WalletServiceTest.java @@ -0,0 +1,370 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.exception.WalletLockedException; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.service.impl.WalletServiceImpl; +import io.mosip.mimoto.util.TestUtilities; +import io.mosip.mimoto.util.WalletLockStatusUtils; +import io.mosip.mimoto.util.WalletUtil; +import io.mosip.mimoto.util.WalletValidator; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.*; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WalletServiceImpl.class}) +@TestPropertySource(locations = "classpath:application-test.properties") +public class WalletServiceTest { + + @MockBean + private WalletRepository walletRepository; + + @MockBean + private WalletUtil walletHelper; + + @MockBean + private WalletValidator walletValidator; + + @MockBean + private WalletUnlockService walletUnlockService; + + @MockBean + private WalletLockService walletLockService; + + @Autowired + private WalletServiceImpl walletService; + + MockHttpSession mockSession; + + private String userId, name, walletId, walletPin, walletConfirmPin, encryptedWalletKey, decryptedWalletKey; + private Wallet wallet; + + @Before + public void setUp() { + userId = UUID.randomUUID().toString(); + walletId = UUID.randomUUID().toString(); + walletPin = "1234"; + walletConfirmPin = "1234"; + name = "default"; + encryptedWalletKey = "encryptedKey"; + decryptedWalletKey = "decryptedKey"; + + mockSession = new MockHttpSession(); + mockSession.setAttribute("clientRegistrationId", "google"); + mockSession.setAttribute(SessionKeys.USER_ID, userId); + + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(1, 1, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, encryptedWalletKey, walletMetadata); + walletId = wallet.getId(); + + } + + @Value("${wallet.passcode.retryBlockedUntil}") + private long retryBlockedUntil; + + @Test + public void shouldCreateWalletSuccessfully() { + String newWalletId = UUID.randomUUID().toString(); + when(walletHelper.createWallet(userId, name, walletPin)).thenReturn(newWalletId); + + WalletResponseDto result = walletService.createWallet(userId, name, walletPin, walletConfirmPin); + + assertEquals(newWalletId, result.getWalletId()); + assertEquals("default", result.getWalletName()); + verify(walletValidator).validateUserId(userId); + verify(walletValidator).validateWalletName(name); + verify(walletValidator).validateWalletPin(walletPin); + verify(walletHelper).createWallet(userId, name, walletPin); + } + + + @Test + public void shouldThrowUnAuthorizedAccessExceptionOnCreatingWalletIfUserIdNotFoundInSession() { + // In case of userId (or any key) not available in session, the default value will be null. + doThrow(new UnauthorizedAccessException("unauthorized", "User ID not found in session")).when(walletValidator).validateUserId(null); + + UnauthorizedAccessException exception = assertThrows(UnauthorizedAccessException.class, () -> + walletService.createWallet(null, name, walletPin, walletConfirmPin)); + + assertEquals("unauthorized", exception.getErrorCode()); + assertEquals("unauthorized --> User ID not found in session", exception.getMessage()); + } + + @Test + public void shouldCreateWalletSuccessfullyIfWalletNameIsNotProvided() { + String newWalletId = UUID.randomUUID().toString(); + when(walletHelper.createWallet(userId, null, walletPin)).thenReturn(newWalletId); + + WalletResponseDto result = walletService.createWallet(userId, null, walletPin, walletConfirmPin); + + assertEquals(newWalletId, result.getWalletId()); + assertNull(result.getWalletName()); + verify(walletValidator).validateUserId(userId); + verify(walletValidator).validateWalletName(null); + verify(walletValidator).validateWalletPin(walletPin); + + verify(walletHelper).createWallet(userId, null, walletPin); + } + + @Test + public void shouldUnlockWalletSuccessfully() { + when(walletRepository.findByUserIdAndId(userId, walletId)).thenReturn(Optional.of(wallet)); + when(walletUnlockService.handleUnlock(wallet, walletPin)).thenReturn(decryptedWalletKey); + + WalletResponseDto responseDto = walletService.unlockWallet(walletId, walletPin, userId); + + verify(walletRepository).findByUserIdAndId(userId, walletId); + assertEquals(walletId, responseDto.getWalletId()); + assertEquals("Test Wallet", responseDto.getWalletName()); + assertEquals(decryptedWalletKey, responseDto.getDecryptedWalletKey()); + } + + @Test + public void shouldThrowInvalidRequestExceptionIfWalletNotFoundForGivenUserIdAndWalletId() { + when(walletRepository.findByUserIdAndId(userId, walletId)).thenReturn(Optional.empty()); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletService.unlockWallet(walletId, walletPin, userId)); + + assertEquals("invalid_request", exception.getErrorCode()); + assertEquals("invalid_request --> Wallet not found", exception.getMessage()); + verify(walletRepository).findByUserIdAndId(userId, walletId); + verifyNoInteractions(walletHelper); + } + + @Test + public void shouldThrowInvalidPinErrorCodeExceptionWhenDecryptingWalletKeyWithInvalidPin() { + String invalidPin = "12345678"; + when(walletRepository.findByUserIdAndId(userId, walletId)).thenReturn(Optional.of(wallet)); + doThrow(new InvalidRequestException(ErrorConstants.INVALID_PIN.getErrorCode(), "Invalid PIN or wallet key provided")) + .when(walletUnlockService).handleUnlock(wallet, invalidPin); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletService.unlockWallet(walletId, invalidPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, walletId); + assertEquals("invalid_pin", exception.getErrorCode()); + assertEquals("invalid_pin --> Invalid PIN or wallet key provided", exception.getMessage()); + } + + @Test + public void shouldThrowTemporarilyLockedExceptionWhenUnlockingWalletWhichIsAlreadyLockedTemporarily() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(6, 2, retryBlockedUntil); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, WalletLockStatus.TEMPORARILY_LOCKED); + wallet = TestUtilities.createWallet(userId, "mock-encrypted-key", walletMetadata); + walletId = wallet.getId(); + when(walletRepository.findByUserIdAndId(userId, wallet.getId())).thenReturn(Optional.of(wallet)); + String errorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage() + " for " + (retryBlockedUntil / (60 * 60 * 1000)) + " hour(s)"; + String expectedErrorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode() + " --> " + errorMessage; + doThrow(new WalletLockedException(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), errorMessage)) + .when(walletUnlockService).handleUnlock(wallet, walletPin); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletService.unlockWallet(walletId, walletPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, wallet.getId()); + assertEquals(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + public void shouldThrowTemporarilyLockedExceptionWhenUnlockingWalletWithInvalidPinInLastAttemptBeforeTemporaryLock() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(5, 1, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, encryptedWalletKey, walletMetadata); + walletPin = "12345678"; + walletId = wallet.getId(); + + when(walletRepository.findByUserIdAndId(userId, wallet.getId())).thenReturn(Optional.of(wallet)); + when(walletHelper.decryptWalletKey(encryptedWalletKey, walletPin)).thenThrow(new InvalidRequestException("invalid_pin", "Invalid PIN or wallet key provided")); + String errorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage() + " for " + (retryBlockedUntil / (60 * 60 * 1000)) + " hour(s)"; + String expectedErrorMessage = ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode() + " --> " + errorMessage; + doThrow(new WalletLockedException(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), errorMessage)) + .when(walletUnlockService).handleUnlock(wallet, walletPin); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletService.unlockWallet(walletId, walletPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, walletId); + assertEquals(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + public void shouldThrowPermanentlyLockedExceptionWhenUnlockingWalletWhichIsAlreadyLockedPermanently() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(6, 4, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, WalletLockStatus.PERMANENTLY_LOCKED); + wallet = TestUtilities.createWallet(userId, "mock-encrypted-key", walletMetadata); + walletId = wallet.getId(); + when(walletRepository.findByUserIdAndId(userId, wallet.getId())).thenReturn(Optional.of(wallet)); + String expectedErrorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode() + " --> " + ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + doThrow(new WalletLockedException(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage())) + .when(walletUnlockService).handleUnlock(wallet, walletPin); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> walletService.unlockWallet(walletId, walletPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, wallet.getId()); + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + public void shouldThrowExceptionWhenUnlockingWalletWithInvalidPinInLastSecondAttemptBeforePermanentLock() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(4, 3, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, encryptedWalletKey, walletMetadata); + walletPin = "12345678"; + walletId = wallet.getId(); + + when(walletRepository.findByUserIdAndId(userId, wallet.getId())).thenReturn(Optional.of(wallet)); + when(walletHelper.decryptWalletKey(encryptedWalletKey, walletPin)).thenThrow(new InvalidRequestException("invalid_pin", "Invalid PIN or wallet key provided")); + String expectedErrorMessage = ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode() + " --> " + ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage(); + doThrow(new WalletLockedException(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage())) + .when(walletUnlockService).handleUnlock(wallet, walletPin); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletService.unlockWallet(walletId, walletPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, walletId); + assertEquals(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + public void shouldThrowPermanentlyLockedExceptionWhenUnlockingWalletWithInvalidPinInLastAttemptBeforePermanentLock() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(5, 3, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT); + wallet = TestUtilities.createWallet(userId, encryptedWalletKey, walletMetadata); + walletPin = "12345678"; + walletId = wallet.getId(); + + when(walletRepository.findByUserIdAndId(userId, wallet.getId())).thenReturn(Optional.of(wallet)); + when(walletHelper.decryptWalletKey(encryptedWalletKey, walletPin)).thenThrow(new InvalidRequestException("invalid_pin", "Invalid PIN or wallet key provided")); + String expectedErrorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode() + " --> " + ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + doThrow(new WalletLockedException(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage())) + .when(walletUnlockService).handleUnlock(wallet, walletPin); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletService.unlockWallet(walletId, walletPin, userId)); + + verify(walletRepository).findByUserIdAndId(userId, walletId); + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + + @Test + public void shouldReturnListOfWalletResponseDTOsForGivenUserId() { + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(5, 3, null); + + WalletMetadata walletMetadata1 = TestUtilities.createWalletMetadata("Test Wallet1", passcodeControl, WalletLockStatus.TEMPORARILY_LOCKED); + Wallet wallet1 = TestUtilities.createWallet("mock-user-id-1", "mock-encrypted-key", walletMetadata1); + + WalletMetadata walletMetadata2 = TestUtilities.createWalletMetadata("Test Wallet2", passcodeControl, null); + Wallet wallet2 = TestUtilities.createWallet("mock-user-id-2", "mock-encrypted-key", walletMetadata2); + + List mockWallets = List.of(wallet1, wallet2); + + when(walletLockService.resetTemporaryLockIfExpired(wallet1)).thenReturn(wallet1); + when(walletLockService.resetTemporaryLockIfExpired(wallet2)).thenReturn(wallet2); + when(walletRepository.findWalletByUserId(userId)).thenReturn(mockWallets); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getWalletLockStatus(wallet1)).thenReturn(WalletLockStatus.TEMPORARILY_LOCKED); + mockedStatic.when(() -> WalletLockStatusUtils.getWalletLockStatus(wallet2)).thenReturn(null); + + List result = walletService.getWallets(userId); + + verify(walletRepository).findWalletByUserId(userId); + assertEquals(mockWallets.size(), result.size()); + for (int i = 0; i < mockWallets.size(); i++) { + Wallet expectedWallet = mockWallets.get(i); + + assertEquals(expectedWallet.getId(), result.get(i).getWalletId()); + assertEquals(expectedWallet.getWalletMetadata().getName(), result.get(i).getWalletName()); + assertEquals(expectedWallet.getWalletMetadata().getLockStatus(), result.get(i).getWalletStatus()); + } + } + } + + @Test + public void shouldReturnEmptyListIfNoWalletsFoundForGivenUserId() { + when(walletRepository.findWalletByUserId(userId)).thenReturn(List.of()); + + List result = walletService.getWallets(userId); + + assertTrue(result.isEmpty()); + verify(walletRepository).findWalletByUserId(userId); + } + + @Test + public void shouldThrowInvalidRequestExceptionIfAnyErrorOccurredWhileCreatingWallet() { + when(walletHelper.createWallet(userId, name, walletPin)).thenThrow(new InvalidRequestException("INVALID_REQUEST", "Test Exception")); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletService.createWallet(userId, name, walletPin, walletConfirmPin)); + + assertEquals("INVALID_REQUEST", exception.getErrorCode()); + assertEquals("INVALID_REQUEST --> Test Exception", exception.getMessage()); + verify(walletHelper).createWallet(userId, name, walletPin); + } + + @Test + public void shouldThrowInvalidRequestExceptionIfReceivedPINAndConfirmPINDoNotMatch() { + walletConfirmPin = "2345"; + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletService.createWallet(userId, name, walletPin, walletConfirmPin)); + + assertEquals("invalid_request", exception.getErrorCode()); + assertEquals("invalid_request --> Entered PIN and Confirm PIN do not match", exception.getMessage()); + } + + @Test + public void shouldDeleteWalletSuccessfully() { + when(walletRepository.findByUserIdAndId(userId, walletId)).thenReturn(Optional.of(wallet)); + + walletService.deleteWallet(userId, walletId); + + verify(walletValidator).validateUserId(userId); + verify(walletRepository).findByUserIdAndId(userId, walletId); + verify(walletRepository).delete(wallet); + } + + @Test + public void shouldThrowInvalidRequestExceptionWhenDeletingNonExistentWallet() { + when(walletRepository.findByUserIdAndId(userId, walletId)).thenReturn(Optional.empty()); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletService.deleteWallet(userId, walletId)); + + assertEquals("invalid_request", exception.getErrorCode()); + assertEquals("invalid_request --> Wallet not found", exception.getMessage()); + verify(walletValidator).validateUserId(userId); + verify(walletRepository).findByUserIdAndId(userId, walletId); + verify(walletRepository, never()).delete(any(Wallet.class)); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/service/WalletUnlockServiceTest.java b/src/test/java/io/mosip/mimoto/service/WalletUnlockServiceTest.java new file mode 100644 index 000000000..8f46f3c3f --- /dev/null +++ b/src/test/java/io/mosip/mimoto/service/WalletUnlockServiceTest.java @@ -0,0 +1,189 @@ +package io.mosip.mimoto.service; + +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.WalletLockedException; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.util.TestUtilities; +import io.mosip.mimoto.util.WalletLockStatusUtils; +import io.mosip.mimoto.util.WalletUtil; +import org.junit.jupiter.api.*; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {WalletUnlockService.class}) +@TestPropertySource(locations = "classpath:application-test.properties") +class WalletUnlockServiceTest { + + @MockBean + private WalletUtil walletUtil; + + @MockBean + private WalletLockService walletLockService; + + @MockBean + private WalletRepository walletRepository; + + @Autowired + private WalletUnlockService walletUnlockService; + + MockHttpSession mockSession; + private String walletPin, encryptedWalletKey, decryptedWalletKey; + private Wallet wallet; + + @BeforeEach + void setUp() { + String userId = UUID.randomUUID().toString(); + walletPin = "1234"; + encryptedWalletKey = "encryptedKey"; + decryptedWalletKey = "decryptedKey"; + + mockSession = new MockHttpSession(); + mockSession.setAttribute("clientRegistrationId", "google"); + mockSession.setAttribute(SessionKeys.USER_ID, userId); + + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(1, 1, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, encryptedWalletKey, walletMetadata); + } + + @Test + void shouldUnlockWalletSuccessfullyAndReturnDecryptedWalletKeyWhenValidPinIsProvided() throws InvalidRequestException { + when(walletLockService.resetTemporaryLockIfExpired(wallet)).thenReturn(wallet); + when(walletUtil.decryptWalletKey(encryptedWalletKey, walletPin)).thenReturn(decryptedWalletKey); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet)).thenReturn(null); + + String result = walletUnlockService.handleUnlock(wallet, walletPin); + + assertEquals(decryptedWalletKey, result); + mockedStatic.verify(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet), times(1)); + verify(walletUtil).decryptWalletKey(encryptedWalletKey, walletPin); + verify(walletLockService).resetTemporaryLockIfExpired(wallet); + verify(walletLockService).resetLockState(wallet); + } + } + + @Test + void shouldThrowExceptionAndHandleUnlockFailureWhenInvalidPinIsProvided() throws InvalidRequestException { + String invalidPin = "12345678"; + when(walletLockService.resetTemporaryLockIfExpired(wallet)).thenReturn(wallet).thenReturn(wallet); + when(walletLockService.enforceLockCyclePolicy(wallet)).thenReturn(wallet); + when(walletUtil.decryptWalletKey(encryptedWalletKey, invalidPin)) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_PIN.getErrorCode(), "Invalid PIN")); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet)).thenReturn(null); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletUnlockService.handleUnlock(wallet, invalidPin)); + + assertEquals(ErrorConstants.INVALID_PIN.getErrorCode(), exception.getErrorCode()); + assertEquals("invalid_pin --> Invalid PIN", exception.getMessage()); + + verify(walletUtil).decryptWalletKey(encryptedWalletKey, invalidPin); + verify(walletLockService).enforceLockCyclePolicy(wallet); + mockedStatic.verify(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet), times(3)); + verify(walletLockService, times(2)).resetTemporaryLockIfExpired(wallet); + verify(walletLockService, never()).resetLockState(wallet); + verify(walletRepository, times(1)).save(wallet); + } + } + + @Test + void shouldThrowExceptionWhenWalletIsLockedPermanentlyAfterValidatingInvalidPin() throws InvalidRequestException { + String invalidPin = "12345678"; + when(walletLockService.resetTemporaryLockIfExpired(wallet)).thenReturn(wallet).thenReturn(wallet); + when(walletUtil.decryptWalletKey(encryptedWalletKey, invalidPin)) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_PIN.getErrorCode(), "Invalid PIN")); + when(walletLockService.enforceLockCyclePolicy(wallet)).thenReturn(wallet); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet)) + .thenReturn(null) + .thenReturn(null) + .thenReturn(new ErrorDTO(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage())); + String expectedErrorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode() + " --> " + ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletUnlockService.handleUnlock(wallet, invalidPin)); + + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + + verify(walletUtil).decryptWalletKey(encryptedWalletKey, invalidPin); + verify(walletLockService).enforceLockCyclePolicy(wallet); + verify(walletLockService, times(2)).resetTemporaryLockIfExpired(wallet); + mockedStatic.verify(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet), times(3)); + verify(walletLockService, never()).resetLockState(wallet); + verify(walletRepository, times(1)).save(wallet); + } + } + + @Test + void shouldThrowExceptionWhenOnlyOneAttemptIsLeftBeforePermanentLockoutAfterValidatingInvalidPin() throws InvalidRequestException { + String invalidPin = "12345678"; + when(walletLockService.resetTemporaryLockIfExpired(wallet)).thenReturn(wallet).thenReturn(wallet); + when(walletUtil.decryptWalletKey(encryptedWalletKey, invalidPin)) + .thenThrow(new InvalidRequestException(ErrorConstants.INVALID_PIN.getErrorCode(), "Invalid PIN")); + when(walletLockService.enforceLockCyclePolicy(wallet)).thenReturn(wallet); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet)) + .thenReturn(null) + .thenReturn(new ErrorDTO(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage())); + String expectedErrorMessage = ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode() + " --> " + ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> + walletUnlockService.handleUnlock(wallet, invalidPin)); + + assertEquals(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + + verify(walletUtil).decryptWalletKey(encryptedWalletKey, invalidPin); + verify(walletLockService).enforceLockCyclePolicy(wallet); + verify(walletLockService, times(1)).resetTemporaryLockIfExpired(wallet); + mockedStatic.verify(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet), times(2)); + verify(walletLockService, never()).resetLockState(wallet); + verify(walletRepository, times(1)).save(wallet); + } + } + + @Test + void shouldThrowExceptionImmediatelyWhenUnlockingWalletIsAlreadyLockedPermanently() throws InvalidRequestException { + when(walletLockService.resetTemporaryLockIfExpired(wallet)).thenReturn(wallet); + + try (MockedStatic mockedStatic = mockStatic(WalletLockStatusUtils.class)) { + mockedStatic.when(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet)) + .thenReturn(new ErrorDTO(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage())); + String expectedErrorMessage = ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode() + " --> " + ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(); + + WalletLockedException exception = assertThrows(WalletLockedException.class, () -> + walletUnlockService.handleUnlock(wallet, walletPin)); + + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), exception.getErrorCode()); + assertEquals(expectedErrorMessage, exception.getMessage()); + + verify(walletLockService, times(1)).resetTemporaryLockIfExpired(wallet); + mockedStatic.verify(() -> WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet), times(1)); + } + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/AuthorizationRequestHelperTest.java b/src/test/java/io/mosip/mimoto/util/AuthorizationRequestHelperTest.java new file mode 100644 index 000000000..068f1e161 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/AuthorizationRequestHelperTest.java @@ -0,0 +1,56 @@ +package io.mosip.mimoto.util; + +import io.mosip.openID4VP.authorizationRequest.AuthorizationDcqlRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AuthorizationRequestHelperTest { + + @Test + public void hasDcqlQueryReturnsTrueWhenDcqlQueryPresent() { + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getDcqlQuery()).thenReturn(mock(DCQLQuery.class)); + + assertTrue(AuthorizationRequestHelper.hasDcqlQuery(dcqlRequest)); + } + + @Test + public void hasDcqlQueryReturnsFalseWhenDcqlQueryMissing() { + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getDcqlQuery()).thenReturn(null); + + assertFalse(AuthorizationRequestHelper.hasDcqlQuery(dcqlRequest)); + } + + @Test + public void hasDcqlQueryReturnsFalseForPresentationExchangeRequest() { + AuthorizationRequest peRequest = mock(AuthorizationPresentationExchangeRequest.class); + + assertFalse(AuthorizationRequestHelper.hasDcqlQuery(peRequest)); + } + + @Test + public void extractDcqlQueryReturnsQueryForDcqlRequest() { + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + AuthorizationDcqlRequest dcqlRequest = mock(AuthorizationDcqlRequest.class); + when(dcqlRequest.getDcqlQuery()).thenReturn(dcqlQuery); + + assertEquals(dcqlQuery, AuthorizationRequestHelper.extractDcqlQuery(dcqlRequest)); + } + + @Test + public void extractDcqlQueryReturnsNullForPresentationExchangeRequest() { + AuthorizationRequest peRequest = mock(AuthorizationPresentationExchangeRequest.class); + + assertNull(AuthorizationRequestHelper.extractDcqlQuery(peRequest)); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/DcqlClaimSetHelperTest.java b/src/test/java/io/mosip/mimoto/util/DcqlClaimSetHelperTest.java new file mode 100644 index 000000000..c58074681 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/DcqlClaimSetHelperTest.java @@ -0,0 +1,268 @@ +package io.mosip.mimoto.util; + +import com.jayway.jsonpath.JsonPath; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.openID4VP.dcql.query.ClaimValue; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DcqlClaimSetHelperTest { + + @Test + public void matchesClaimsReturnsFalseWhenClaimsEmptyButClaimSetsPresent() { + CredentialQuery query = mock(CredentialQuery.class); + when(query.getClaims()).thenReturn(List.of()); + when(query.getClaimSets()).thenReturn(List.of(List.of("age-above-18"))); + + VCCredentialResponse vc = mock(VCCredentialResponse.class); + + assertFalse(DcqlClaimSetHelper.matchesClaims(query, vc, (ignored, claim) -> true)); + } + + @Test + public void satisfiesClaimSetRejectsUnknownClaimIds() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(query.getClaims()).thenReturn(List.of(ageClaim)); + + VCCredentialResponse vc = mock(VCCredentialResponse.class); + + assertFalse(DcqlClaimSetHelper.satisfiesClaimSet( + query, vc, List.of("age-above-18", "unknown-claim"), (ignored, claim) -> true)); + } + + @Test + public void satisfiesClaimSetRejectsClaimSetWithOnlyUnknownClaimIds() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(query.getClaims()).thenReturn(List.of(ageClaim)); + + VCCredentialResponse vc = mock(VCCredentialResponse.class); + + assertFalse(DcqlClaimSetHelper.satisfiesClaimSet( + query, vc, List.of("unknown-claim"), (ignored, claim) -> true)); + } + + @Test + public void resolveClaimPathsReturnsEmptyWhenAnyClaimIdUnknown() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(query.getClaims()).thenReturn(List.of(ageClaim)); + + assertTrue(DcqlClaimSetHelper.resolveClaimPaths(query, List.of("unknown-claim")).isEmpty()); + assertTrue(DcqlClaimSetHelper.resolveClaimPaths( + query, List.of("age-above-18", "unknown-claim")).isEmpty()); + } + + @Test + public void matchesClaimsWhenAnyClaimSetSatisfied() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(ageClaim.getValues()).thenReturn(List.of(new ClaimValue.BoolValue(true))); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(dobClaim.getPath()).thenReturn(List.of("dateOfBirth")); + when(dobClaim.getValues()).thenReturn(null); + + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + when(query.getClaimSets()).thenReturn(List.of( + List.of("age-above-18"), + List.of("date-of-birth"))); + + VCCredentialResponse vc = mock(VCCredentialResponse.class); + + assertTrue(DcqlClaimSetHelper.matchesClaims(query, vc, (ignored, claim) -> { + if ("age-above-18".equals(claim.getId())) { + return false; + } + return "date-of-birth".equals(claim.getId()); + })); + } + + @Test + public void matchesClaimsRequiresAllClaimsWhenClaimSetsAbsent() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + when(query.getClaimSets()).thenReturn(null); + + VCCredentialResponse vc = mock(VCCredentialResponse.class); + + assertFalse(DcqlClaimSetHelper.matchesClaims(query, vc, (ignored, claim) -> + "age-above-18".equals(claim.getId()))); + } + + @Test + public void isValidClaimSetSelectionMatchesExactOption() { + List> claimSets = List.of( + List.of("age-above-18"), + List.of("date-of-birth")); + assertTrue(DcqlClaimSetHelper.isValidClaimSetSelection(claimSets, List.of("date-of-birth"))); + assertFalse(DcqlClaimSetHelper.isValidClaimSetSelection(claimSets, List.of("age-above-18", "date-of-birth"))); + } + + @Test + public void isValidClaimSetSelectionIgnoresClaimIdOrder() { + List> claimSets = List.of( + List.of("given-name", "family-name", "age-above-18"), + List.of("date-of-birth")); + assertTrue(DcqlClaimSetHelper.isValidClaimSetSelection( + claimSets, List.of("age-above-18", "given-name", "family-name"))); + assertFalse(DcqlClaimSetHelper.isValidClaimSetSelection( + claimSets, List.of("given-name", "family-name"))); + } + + @Test + public void isValidClaimSetSelectionRejectsDuplicateClaimIds() { + List> claimSets = List.of(List.of("given-name", "family-name")); + assertFalse(DcqlClaimSetHelper.isValidClaimSetSelection( + claimSets, List.of("given-name", "given-name"))); + } + + @Test + public void buildJsonPathQuotesDottedSegments() { + Map payload = Map.of( + "org.iso.18013.5.1", Map.of("family_name", "Doe")); + + String dottedKeyPath = DcqlClaimSetHelper.buildJsonPath(List.of("org.iso.18013.5.1")); + String nestedDottedPath = DcqlClaimSetHelper.buildJsonPath( + List.of("credentialSubject", "org.iso.18013.5.1", "family_name")); + + assertEquals("$['org.iso.18013.5.1']", dottedKeyPath); + assertEquals(Map.of("family_name", "Doe"), JsonPath.read(payload, dottedKeyPath)); + assertEquals("$.credentialSubject['org.iso.18013.5.1'].family_name", nestedDottedPath); + + Map nestedPayload = Map.of( + "credentialSubject", Map.of("org.iso.18013.5.1", Map.of("family_name", "Doe"))); + assertEquals("Doe", JsonPath.read(nestedPayload, nestedDottedPath)); + } + @Test + public void buildJsonPathKeepsSimpleSegmentsDotSeparated() { + assertEquals("$.credentialSubject.dateOfBirth", + DcqlClaimSetHelper.buildJsonPath(List.of("credentialSubject", "dateOfBirth"))); + } + + @Test + public void buildClaimPathQuotesDottedNestedSegments() { + assertEquals("org.iso.18013.5.1", + DcqlClaimSetHelper.buildClaimPath(List.of("org.iso.18013.5.1"))); + assertEquals("credentialSubject['org.iso.18013.5.1'].family_name", + DcqlClaimSetHelper.buildClaimPath( + List.of("credentialSubject", "org.iso.18013.5.1", "family_name"))); + } + + @Test + public void resolveJsonPathsUsesBracketSafeSegments() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery mdlClaim = mock(ClaimsQuery.class); + when(mdlClaim.getId()).thenReturn("family-name"); + when(mdlClaim.getPath()).thenReturn(List.of("org.iso.18013.5.1", "family_name")); + when(query.getClaims()).thenReturn(List.of(mdlClaim)); + + assertEquals(List.of("$['org.iso.18013.5.1'].family_name"), + DcqlClaimSetHelper.resolveJsonPaths(query, List.of("family-name"))); + } + + @Test + public void resolveClaimPathsMapsClaimIdsToPaths() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(dobClaim.getPath()).thenReturn(List.of("dateOfBirth")); + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + + assertEquals(List.of("age_above_18"), + DcqlClaimSetHelper.resolveClaimPaths(query, List.of("age-above-18"))); + } + + @Test + public void resolveClaimIdsForSubmissionPicksFirstClaimSetWithDisclosure() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(dobClaim.getPath()).thenReturn(List.of("dateOfBirth")); + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + when(query.getClaimSets()).thenReturn(List.of( + List.of("age-above-18"), + List.of("date-of-birth"))); + + // age_above_18 is public (no disclosure); dateOfBirth IS selectively disclosable + List resolved = DcqlClaimSetHelper.resolveClaimIdsForSubmission( + query, null, path -> path.equals("dateOfBirth")); + + assertEquals(List.of("date-of-birth"), resolved); + } + + @Test + public void resolveClaimIdsForSubmissionReturnsEmptyWhenAllClaimsArePublic() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(dobClaim.getPath()).thenReturn(List.of("dateOfBirth")); + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + when(query.getClaimSets()).thenReturn(List.of( + List.of("age-above-18"), + List.of("date-of-birth"))); + + // Both are public — no SD filtering needed + List resolved = DcqlClaimSetHelper.resolveClaimIdsForSubmission( + query, null, path -> false); + + assertTrue(resolved.isEmpty()); + } + + @Test + public void resolveClaimIdsForSubmissionHonoursExplicitSelectedClaimIds() { + CredentialQuery query = mock(CredentialQuery.class); + ClaimsQuery ageClaim = mock(ClaimsQuery.class); + ClaimsQuery dobClaim = mock(ClaimsQuery.class); + when(ageClaim.getId()).thenReturn("age-above-18"); + when(ageClaim.getPath()).thenReturn(List.of("age_above_18")); + when(dobClaim.getId()).thenReturn("date-of-birth"); + when(dobClaim.getPath()).thenReturn(List.of("dateOfBirth")); + when(query.getClaims()).thenReturn(List.of(ageClaim, dobClaim)); + when(query.getClaimSets()).thenReturn(List.of( + List.of("age-above-18"), + List.of("date-of-birth"))); + + List resolved = DcqlClaimSetHelper.resolveClaimIdsForSubmission( + query, List.of("date-of-birth"), path -> false); + + assertEquals(List.of("date-of-birth"), resolved); + } + + @Test + public void dcqlValueMatchesBooleanAndString() { + assertTrue(DcqlClaimSetHelper.dcqlValueMatches(true, new ClaimValue.BoolValue(true))); + assertTrue(DcqlClaimSetHelper.dcqlValueMatches("yes", new ClaimValue.StringValue("yes"))); + assertFalse(DcqlClaimSetHelper.dcqlValueMatches(null, new ClaimValue.StringValue("yes"))); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/DcqlCredentialSetHelperTest.java b/src/test/java/io/mosip/mimoto/util/DcqlCredentialSetHelperTest.java new file mode 100644 index 000000000..5fa0dafb2 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/DcqlCredentialSetHelperTest.java @@ -0,0 +1,45 @@ +package io.mosip.mimoto.util; + +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.CredentialSetQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DcqlCredentialSetHelperTest { + + @Test + public void resolveEffectiveCredentialSetsReturnsExplicitSets() { + CredentialSetQuery explicitSet = mock(CredentialSetQuery.class); + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentialSets()).thenReturn(List.of(explicitSet)); + + assertEquals(List.of(explicitSet), DcqlCredentialSetHelper.resolveEffectiveCredentialSets(dcqlQuery)); + } + + @Test + public void resolveEffectiveCredentialSetsSynthesisesOneRequiredSetPerQueryWhenAbsent() { + CredentialQuery governmentQuery = new CredentialQuery( + "government-identity", "dc+sd-jwt", false, Map.of(), false, null, null); + CredentialQuery ageQuery = new CredentialQuery( + "age-proof", "dc+sd-jwt", false, Map.of(), false, null, null); + DCQLQuery dcqlQuery = mock(DCQLQuery.class); + when(dcqlQuery.getCredentialSets()).thenReturn(null); + when(dcqlQuery.getCredentials()).thenReturn(List.of(governmentQuery, ageQuery)); + + List sets = DcqlCredentialSetHelper.resolveEffectiveCredentialSets(dcqlQuery); + + assertEquals(2, sets.size()); + assertTrue(sets.get(0).getRequired()); + assertEquals(List.of(List.of("government-identity")), sets.get(0).getOptions()); + assertTrue(sets.get(1).getRequired()); + assertEquals(List.of(List.of("age-proof")), sets.get(1).getOptions()); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/DcqlMatchingHelperTest.java b/src/test/java/io/mosip/mimoto/util/DcqlMatchingHelperTest.java new file mode 100644 index 000000000..dede302ac --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/DcqlMatchingHelperTest.java @@ -0,0 +1,140 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.DecryptedCredentialDTO; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; +import io.mosip.openID4VP.constants.FormatType; +import io.mosip.openID4VP.dcql.query.ClaimValue; +import io.mosip.openID4VP.dcql.query.ClaimsQuery; +import io.mosip.openID4VP.dcql.query.CredentialQuery; +import io.mosip.openID4VP.dcql.query.DCQLQuery; +import io.mosip.openID4VP.dcql.evaluator.ClaimFailure; +import io.mosip.openID4VP.dcql.evaluator.DCQLEvaluationErrorCodes; +import io.mosip.openID4VP.dcql.evaluator.QueryMatchResult; +import io.mosip.openID4VP.helper.DCQLHelper; +import io.mosip.openID4VP.wallet.Credential; +import org.junit.Test; + +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class DcqlMatchingHelperTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final DCQLHelper dcqlHelper = new DCQLHelper(); + + @Test + public void toLibraryCredential_mapsLdpVcMapPayload() { + DecryptedCredentialDTO dto = walletCredential("cred-1", CredentialFormat.LDP_VC.getFormat(), + Map.of("credentialSubject", Map.of("id", "did:example:holder", "name", "Alice"))); + + Credential credential = DcqlMatchingHelper.toLibraryCredential(dto, objectMapper); + + assertNotNull(credential); + assertEquals(FormatType.LDP_VC, credential.getFormat()); + assertEquals("cred-1", credential.getCredentialId()); + } + + @Test + public void toLibraryCredential_mapsSdJwtStringPayload() throws Exception { + String token = buildSdJwtToken(Map.of("vct", "https://example.com/employee", "cnf", Map.of("kid", "k1"))); + DecryptedCredentialDTO dto = walletCredential("cred-2", CredentialFormat.DC_SD_JWT.getFormat(), token); + + Credential credential = DcqlMatchingHelper.toLibraryCredential(dto, objectMapper); + + assertNotNull(credential); + assertEquals(FormatType.DC_SD_JWT, credential.getFormat()); + } + + @Test + public void libraryEvaluation_rejectsSdJwtWithoutCnfWhenBindingRequired() throws Exception { + String token = buildSdJwtToken(Map.of("vct", "https://example.com/employee")); + DecryptedCredentialDTO dto = walletCredential("cred-3", CredentialFormat.VC_SD_JWT.getFormat(), token); + + CredentialQuery query = new CredentialQuery( + "employee-card", + CredentialFormat.VC_SD_JWT.getFormat(), + false, + Map.of("vct_values", List.of("https://example.com/employee")), + true, + null, + null); + DCQLQuery dcqlQuery = new DCQLQuery(List.of(query), null); + + var result = dcqlHelper.getMatchingCredentials( + List.of(DcqlMatchingHelper.toLibraryCredential(dto, objectMapper)), dcqlQuery); + + assertFalse(result.getSuccess()); + assertTrue(result.getQueryMatches().get("employee-card").getMatchingCredentials() == null + || result.getQueryMatches().get("employee-card").getMatchingCredentials().isEmpty()); + } + + @Test + public void libraryEvaluation_matchesSdJwtWhenQueryUsesAlternateSdJwtFormat() throws Exception { + String token = buildSdJwtToken(Map.of("vct", "https://example.com/employee", "cnf", Map.of("kid", "k1"))); + DecryptedCredentialDTO dto = walletCredential("cred-4", CredentialFormat.VC_SD_JWT.getFormat(), token); + + CredentialQuery query = new CredentialQuery( + "employee-card", + CredentialFormat.DC_SD_JWT.getFormat(), + false, + Map.of(), + false, + null, + null); + DCQLQuery dcqlQuery = new DCQLQuery(List.of(query), null); + + var result = dcqlHelper.getMatchingCredentials( + DcqlMatchingHelper.constructCredentialWithCredentialFormat(List.of(dto), objectMapper), dcqlQuery); + + assertTrue(result.getSuccess()); + assertEquals("cred-4", result.getQueryMatches().get("employee-card").getMatchingCredentials().get(0).getCredentialId()); + } + + @Test + public void resolveMissingClaims_usesFailedClaimPathsFromLibraryResult() { + CredentialQuery query = new CredentialQuery( + "age-proof", + CredentialFormat.DC_SD_JWT.getFormat(), + false, + Map.of(), + false, + List.of(new ClaimsQuery("age-above-18", List.of("age_above_18"), List.of(new ClaimValue.BoolValue(true)))), + null); + QueryMatchResult queryMatch = new QueryMatchResult( + null, + List.of(new ClaimFailure( + new ClaimsQuery("age-above-18", List.of("age_above_18"), null), + DCQLEvaluationErrorCodes.CLAIM_UNAVAILABLE)), + DCQLEvaluationErrorCodes.REQUIRED_CLAIMS_NOT_SATISFIED, + false); + + Set missingClaims = DcqlMatchingHelper.resolveMissingClaims(query, queryMatch); + + assertEquals(Set.of("age_above_18"), missingClaims); + } + + private static DecryptedCredentialDTO walletCredential(String id, String format, Object payload) { + DecryptedCredentialDTO dto = new DecryptedCredentialDTO(); + dto.setId(id); + dto.setCredential(VCCredentialResponse.builder().format(format).credential(payload).build()); + return dto; + } + + private static String buildSdJwtToken(Map payload) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + String header = encoder.encodeToString(mapper.writeValueAsBytes(Map.of("alg", "none"))); + String encodedPayload = encoder.encodeToString(mapper.writeValueAsBytes(payload)); + return header + "." + encodedPayload + ".signature"; + } +} diff --git a/src/test/java/io/mosip/mimoto/util/DerivedKeyCryptoUtilTest.java b/src/test/java/io/mosip/mimoto/util/DerivedKeyCryptoUtilTest.java new file mode 100644 index 000000000..656426820 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/DerivedKeyCryptoUtilTest.java @@ -0,0 +1,255 @@ +package io.mosip.mimoto.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.mosip.mimoto.dto.CryptoWithPinRequestDto; +import io.mosip.mimoto.dto.CryptoWithPinResponseDto; +import io.mosip.mimoto.exception.CryptoManagerException; +import io.mosip.mimoto.exception.ParseException; + +@ExtendWith(MockitoExtension.class) +class DerivedKeyCryptoUtilTest { + + private DerivedKeyCryptoUtil derivedKeyCryptoUtil; + + private static final String AES_KEY_TYPE = "AES"; + private static final String TEST_DATA = "testdata"; + private static final String USER_PIN = "12345"; + private static final byte[] SALT = "testsalt123456789012345678901234".getBytes(); + private static final byte[] NONCE = "testnonce123".getBytes(); + private static final int DEFAULT_KEY_SIZE = 256; + private static final int DEFAULT_ITERATIONS = 100000; + private static final String DEFAULT_ALGORITHM = "PBKDF2WithHmacSHA512"; + + @BeforeEach + void setUp() { + derivedKeyCryptoUtil = new DerivedKeyCryptoUtil(DEFAULT_KEY_SIZE, DEFAULT_ITERATIONS, DEFAULT_ALGORITHM); + } + + private byte[] encrypt(String data, SecretKey key, byte[] nonce, byte[] aad) throws Exception { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, nonce); + cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec); + if (aad != null) { + cipher.updateAAD(aad); + } + return cipher.doFinal(data.getBytes()); + } + + @Test + void testDecryptWithPin() throws Exception { + SecretKey derivedKey = getDerivedKey(USER_PIN, SALT); + byte[] encryptedData = encrypt(TEST_DATA, derivedKey, NONCE, SALT); + + byte[] combined = new byte[SALT.length + NONCE.length + encryptedData.length]; + System.arraycopy(SALT, 0, combined, 0, SALT.length); + System.arraycopy(NONCE, 0, combined, SALT.length, NONCE.length); + System.arraycopy(encryptedData, 0, combined, SALT.length + NONCE.length, encryptedData.length); + + CryptoWithPinRequestDto requestDto = new CryptoWithPinRequestDto(); + requestDto.setData(Base64.getEncoder().encodeToString(combined)); + requestDto.setUserPin(USER_PIN); + + CryptoWithPinResponseDto response = derivedKeyCryptoUtil.decryptWithPin(requestDto); + assertNotNull(response); + assertEquals(TEST_DATA, response.getData()); + } + + @Test + void testSymmetricDecryptWithIV() throws Exception { + SecretKey key = new SecretKeySpec("testkey123456789".getBytes(), AES_KEY_TYPE); + byte[] encryptedData = encrypt(TEST_DATA, key, NONCE, null); + + byte[] decryptedData = invokeSymmetricDecrypt(key, encryptedData, NONCE, null); + assertEquals(TEST_DATA, new String(decryptedData)); + } + + @Test + void testSymmetricDecryptWithIVAndAad() throws Exception { + SecretKey key = new SecretKeySpec("testkey123456789".getBytes(), AES_KEY_TYPE); + byte[] aad = "testaad".getBytes(); + byte[] encryptedData = encrypt(TEST_DATA, key, NONCE, aad); + + byte[] decryptedData = invokeSymmetricDecrypt(key, encryptedData, NONCE, aad); + assertEquals(TEST_DATA, new String(decryptedData)); + } + + @Test + void testSymmetricDecryptWithIVWithNullIV() throws Exception { + SecretKey key = new SecretKeySpec("testkey123456789".getBytes(), AES_KEY_TYPE); + byte[] iv16 = "testnonce1234567".getBytes(); + byte[] encryptedData = encrypt(TEST_DATA, key, iv16, null); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] combined = new byte[encryptedData.length + cipher.getBlockSize()]; + System.arraycopy(encryptedData, 0, combined, 0, encryptedData.length); + System.arraycopy(iv16, 0, combined, encryptedData.length, iv16.length); + + + byte[] decryptedData = invokeSymmetricDecrypt(key, combined, null, null); + assertEquals(TEST_DATA, new String(decryptedData)); + } + + @Test + void testSymmetricDecryptWithIVWithNullIVAndAad() throws Exception { + SecretKey key = new SecretKeySpec("testkey123456789".getBytes(), AES_KEY_TYPE); + byte[] aad = "testaad".getBytes(); + byte[] iv16 = "testnonce1234567".getBytes(); // 16 bytes + byte[] encryptedData = encrypt(TEST_DATA, key, iv16, aad); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] combined = new byte[encryptedData.length + cipher.getBlockSize()]; + System.arraycopy(encryptedData, 0, combined, 0, encryptedData.length); + System.arraycopy(iv16, 0, combined, encryptedData.length, iv16.length); + + + byte[] decryptedData = invokeSymmetricDecrypt(key, combined, null, aad); + assertEquals(TEST_DATA, new String(decryptedData)); + } + + @Test + void testSymmetricDecryptWithIVThrowsInvalidKeyException() throws Exception { + SecretKey key = new SecretKeySpec("testkey".getBytes(), AES_KEY_TYPE); + byte[] data = "testdata".getBytes(); + byte[] iv = "testiv".getBytes(); + byte[] aad = "testaad".getBytes(); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + invokeSymmetricDecrypt(key, data, iv, aad); + }); + + assertEquals(InvalidKeyException.class, exception.getTargetException().getClass()); + } + + @Test + void testSymmetricDecryptWithoutIVThrowsCryptoManagerException() throws Exception { + SecretKey key = new SecretKeySpec("testkey12345678901234567".getBytes(), AES_KEY_TYPE); + byte[] data = "testdata".getBytes(); + byte[] aad = "testaad".getBytes(); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + invokeSymmetricDecrypt(key, data, null, aad); + }); + + assertEquals(CryptoManagerException.class, exception.getTargetException().getClass()); + } + + @Test + void testSymmetricDecryptThrowsCryptoManagerException() throws Exception { + SecretKey key = new SecretKeySpec("0123456789ABCDEF".getBytes(), AES_KEY_TYPE); + byte[] iv = NONCE; + byte[] encrypted = encrypt(TEST_DATA, key, iv, null); + + try (MockedStatic mocked = Mockito.mockStatic(Cipher.class)) { + mocked.when(() -> Cipher.getInstance("AES/GCM/NoPadding")) + .thenThrow(new NoSuchAlgorithmException("for tests")); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + invokeSymmetricDecrypt(key, encrypted, iv, null); + }); + + Throwable targetException = exception.getTargetException(); + assertEquals(CryptoManagerException.class, targetException.getClass()); + assertTrue(targetException.getCause() instanceof NoSuchAlgorithmException); + } + } + + @Test + void testGetDerivedKey() throws Exception { + SecretKey derivedKey = getDerivedKey(USER_PIN, SALT); + assertNotNull(derivedKey); + assertEquals(AES_KEY_TYPE, derivedKey.getAlgorithm()); + } + + @Test + void testHash() throws Exception { + String hash = invokeHash(USER_PIN.getBytes(), SALT); + assertNotNull(hash); + } + + @Test + void testHashThrowsCryptoManagerException() { + derivedKeyCryptoUtil = new DerivedKeyCryptoUtil(DEFAULT_KEY_SIZE, DEFAULT_ITERATIONS, "InvalidAlgo"); + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + invokeHash(USER_PIN.getBytes(), SALT); + }); + assertEquals(CryptoManagerException.class, exception.getTargetException().getClass()); + } + + @Test + void testHexDecode() throws Exception { + String hexData = "7465737464617461"; + byte[] decodedBytes = invokeHexDecode(hexData); + assertEquals(TEST_DATA, new String(decodedBytes)); + } + + @Test + void testHexDecodeWithOddLength() { + String hexData = "7465737464617461F"; + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + invokeHexDecode(hexData); + }); + assertEquals(ParseException.class, exception.getTargetException().getClass()); + } + + @Test + void testStaticSymmetricDecryptWithEmptyAadDoesNotUseAad() throws Exception { + SecretKey key = new SecretKeySpec("testkey123456789".getBytes(), AES_KEY_TYPE); + byte[] iv16 = "testnonce1234567".getBytes(); + byte[] encryptedData = encrypt(TEST_DATA, key, iv16, null); + + byte[] combined = new byte[encryptedData.length + iv16.length]; + System.arraycopy(encryptedData, 0, combined, 0, encryptedData.length); + System.arraycopy(iv16, 0, combined, encryptedData.length, iv16.length); + + Method staticMethod = DerivedKeyCryptoUtil.class.getDeclaredMethod("symmetricDecrypt", SecretKey.class, byte[].class, byte[].class); + staticMethod.setAccessible(true); + byte[] decrypted = (byte[]) staticMethod.invoke(null, key, combined, new byte[0]); + assertEquals(TEST_DATA, new String(decrypted)); + } + + private byte[] invokeSymmetricDecrypt(SecretKey key, byte[] data, byte[] iv, byte[] aad) throws Exception { + Method method = DerivedKeyCryptoUtil.class.getDeclaredMethod("symmetricDecrypt", SecretKey.class, byte[].class, byte[].class, byte[].class); + method.setAccessible(true); + return (byte[]) method.invoke(derivedKeyCryptoUtil, key, data, iv, aad); + } + + private SecretKey getDerivedKey(String userPin, byte[] salt) throws Exception { + Method method = DerivedKeyCryptoUtil.class.getDeclaredMethod("getDerivedKey", String.class, byte[].class); + method.setAccessible(true); + return (SecretKey) method.invoke(derivedKeyCryptoUtil, userPin, salt); + } + + private String invokeHash(byte[] data, byte[] salt) throws Exception { + Method method = DerivedKeyCryptoUtil.class.getDeclaredMethod("hash", byte[].class, byte[].class); + method.setAccessible(true); + return (String) method.invoke(derivedKeyCryptoUtil, data, salt); + } + + private byte[] invokeHexDecode(String hexData) throws Exception { + Method method = DerivedKeyCryptoUtil.class.getDeclaredMethod("hexDecode", String.class); + method.setAccessible(true); + return (byte[]) method.invoke(derivedKeyCryptoUtil, hexData); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidatorTest.java b/src/test/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidatorTest.java new file mode 100644 index 000000000..774c0164f --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/Draft13CredentialIssuerWellknownResponseValidatorTest.java @@ -0,0 +1,229 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.CredentialDefinitionResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Path; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.*; + +import static io.mosip.mimoto.util.TestUtilities.getCredentialIssuerWellKnownResponseDto; +import static io.mosip.mimoto.util.TestUtilities.getCredentialSupportedResponse; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; + +@SpringBootTest(classes = {Draft13CredentialIssuerWellknownResponseValidator.class, CredentialIssuerWellKnownResponse.class, ValidationAutoConfiguration.class}) +class Draft13CredentialIssuerWellknownResponseValidatorTest { + + @Autowired + private Validator validator; + + @Autowired + private CredentialIssuerWellKnownResponse response; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + } + + @Test + void shouldNotThrowExceptionWhenResponseIsFullyValid() { + Draft13CredentialIssuerWellknownResponseValidator validatorInstance = new Draft13CredentialIssuerWellknownResponseValidator(); + + assertDoesNotThrow(() -> validatorInstance.validate(response, validator)); + } + + @SuppressWarnings("unchecked") + @Test + void shouldThrowExceptionWhenBeanValidationReturnsViolations() { + Draft13CredentialIssuerWellknownResponseValidator validatorInstance = new Draft13CredentialIssuerWellknownResponseValidator(); + + Path mockPath = mock(Path.class); + doReturn("credentialIssuer").when(mockPath).toString(); + + ConstraintViolation violation = mock(ConstraintViolation.class); + doReturn(mockPath).when(violation).getPropertyPath(); + doReturn("must not be blank").when(violation).getMessage(); + + Set> violations = new HashSet<>(); + violations.add(violation); + + Validator mockValidator = mock(Validator.class); + doReturn(violations).when(mockValidator).validate(any()); + + InvalidWellknownResponseException exception = assertThrows(InvalidWellknownResponseException.class, () -> + validatorInstance.validate(response, mockValidator)); + String message = exception.getMessage(); + assertTrue(message.contains("Validation failed:")); + assertTrue(message.contains("credentialIssuer: must not be blank")); + } + + @Test + void shouldNotThrowExceptionWhenFormatIsNullSinceNeitherFormatSpecificCheckApplies() { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + credentialsSupportedResponse.setFormat(null); + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", credentialsSupportedResponse)); + + Draft13CredentialIssuerWellknownResponseValidator validatorInstance = new Draft13CredentialIssuerWellknownResponseValidator(); + + assertDoesNotThrow(() -> validatorInstance.validate(response, validator)); + } + + @Test + void shouldNotThrowExceptionWhenMultipleFieldsAreNullSinceNoCascadeValidation() { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + credentialsSupportedResponse.setFormat(null); + credentialsSupportedResponse.setScope(null); + credentialsSupportedResponse.setDisplay(null); + credentialsSupportedResponse.setProofTypesSupported(null); + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", credentialsSupportedResponse)); + + Draft13CredentialIssuerWellknownResponseValidator validatorInstance = new Draft13CredentialIssuerWellknownResponseValidator(); + + assertDoesNotThrow(() -> validatorInstance.validate(response, validator)); + } + + @Test + void shouldNotThrowExceptionWhenDisplayHasInvalidNestedFieldsSinceNoCascadeValidation() { + List displayList = new ArrayList<>(); + CredentialSupportedDisplayResponse invalidDisplay = new CredentialSupportedDisplayResponse(); + invalidDisplay.setName(null); + invalidDisplay.setLocale(null); + invalidDisplay.setLogo(null); + invalidDisplay.setTextColor(null); + invalidDisplay.setBackgroundColor(null); + displayList.add(invalidDisplay); + response.getCredentialConfigurationsSupported().get("CredentialType1").setDisplay(displayList); + + Draft13CredentialIssuerWellknownResponseValidator validatorInstance = new Draft13CredentialIssuerWellknownResponseValidator(); + + assertDoesNotThrow(() -> validatorInstance.validate(response, validator)); + } + + @Nested + class LdpVcFormatWellKnownResponseValidationTest { + @Test + void shouldDetectMissingMandatoryFieldsCredentialDefinitionOfCredentialSupportedResponse() { + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + response.getCredentialConfigurationsSupported().get("CredentialType1").setCredentialDefinition(null); + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + + InvalidWellknownResponseException invalidWellknownResponseException = assertThrows(InvalidWellknownResponseException.class, () -> + credentialIssuerWellknownResponseValidator.validate(response, validator)); + assertEquals("RESIDENT-APP-041", invalidWellknownResponseException.getErrorCode()); + assertEquals(""" + RESIDENT-APP-041 --> Invalid Wellknown from Issuer + credentialDefinition: must not be null""", invalidWellknownResponseException.getMessage()); + } + + @Test + void shouldDetectMissingMandatoryFieldsOfCredentialDefinitionInWellknownResponse() { + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + CredentialDefinitionResponseDto credentialDefinitionResponseDto = new CredentialDefinitionResponseDto(); + credentialDefinitionResponseDto.setCredentialSubject(null); + credentialDefinitionResponseDto.setType(null); + response.getCredentialConfigurationsSupported().get("CredentialType1").setCredentialDefinition(credentialDefinitionResponseDto); + + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + InvalidWellknownResponseException invalidWellknownResponseException = assertThrows(InvalidWellknownResponseException.class, () -> + credentialIssuerWellknownResponseValidator.validate(response, validator) + ); + + // Update to check message contains validation errors + String message = invalidWellknownResponseException.getMessage(); + assertTrue(message.contains("RESIDENT-APP-041 --> Invalid Wellknown from Issuer")); + assertTrue(message.contains("type: must not be empty")); + } + + @Test + void shouldThrowExceptionWhenCredentialDefinitionTypeIsEmpty() { + response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + response.getCredentialConfigurationsSupported().get("CredentialType1").getCredentialDefinition().setType(Collections.emptyList()); // Invalid empty list + + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + + InvalidWellknownResponseException invalidWellknownResponseException = assertThrows(InvalidWellknownResponseException.class, () -> + credentialIssuerWellknownResponseValidator.validate(response, validator)); + assertEquals("RESIDENT-APP-041", invalidWellknownResponseException.getErrorCode()); + assertEquals(""" + RESIDENT-APP-041 --> Invalid Wellknown from Issuer + Validation failed: + type: must not be empty""", invalidWellknownResponseException.getMessage()); + } + } + + @Nested + class MsoMdocFormatWellKnownResponseValidationTest { + + @Test + void shouldThrowInvalidWellKnownResponseExceptionWhenMandatoryFieldDocTypeIsNotPresent() { + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + CredentialsSupportedResponse credentialSupportedResponse1 = getCredentialSupportedResponse("CredentialType1", "mso_mdoc"); + credentialSupportedResponse1.setDoctype(""); + CredentialIssuerWellKnownResponse wellKnownResponseWithoutDocType = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", credentialSupportedResponse1)); + + InvalidWellknownResponseException invalidWellknownResponseException = assertThrows(InvalidWellknownResponseException.class, () -> + credentialIssuerWellknownResponseValidator.validate(wellKnownResponseWithoutDocType, validator) + ); + + assertEquals(""" + RESIDENT-APP-041 --> Invalid Wellknown from Issuer + Mandatory field 'doctype' missing""", invalidWellknownResponseException.getMessage()); + } + + @Test + void shouldThrowInvalidWellKnownResponseExceptionWhenMandatoryFieldClaimIsNotPresent() { + CredentialsSupportedResponse credentialSupportedResponse = getCredentialSupportedResponse("CredentialType1", "mso_mdoc"); + credentialSupportedResponse.setClaims(Map.of()); + CredentialIssuerWellKnownResponse wellKnownResponseWithoutClaims = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", credentialSupportedResponse)); + + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + InvalidWellknownResponseException invalidWellknownResponseException = assertThrows(InvalidWellknownResponseException.class, () -> + credentialIssuerWellknownResponseValidator.validate(wellKnownResponseWithoutClaims, validator) + ); + + assertEquals(""" + RESIDENT-APP-041 --> Invalid Wellknown from Issuer + Mandatory field 'claims' missing""", invalidWellknownResponseException.getMessage()); + } + + + @Test + void shouldNotThrowInvalidWellKnownResponseExceptionWhenMandatoryFieldsAreNotPresentInMsoMdocVc() { + CredentialIssuerWellKnownResponse wellKnownResponseWithoutClaims = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1", "mso_mdoc"))); + + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator = new Draft13CredentialIssuerWellknownResponseValidator(); + + assertDoesNotThrow(() -> + credentialIssuerWellknownResponseValidator.validate(wellKnownResponseWithoutClaims, validator)); + + } + } + +} diff --git a/src/test/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilderTest.java b/src/test/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilderTest.java new file mode 100644 index 000000000..09a56423a --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/Draft13CredentialRequestBuilderTest.java @@ -0,0 +1,128 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.constant.CredentialFormat; +import io.mosip.mimoto.dto.mimoto.CredentialDefinitionResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.Draft13VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.VCCredentialRequestProof; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class Draft13CredentialRequestBuilderTest { + + @InjectMocks + private Draft13CredentialRequestBuilder builder; + + private CredentialsSupportedResponse credentialsSupportedResponse; + private CredentialDefinitionResponseDto credentialDefinition; + private VCCredentialRequestProof proof; + + @BeforeEach + void setUp() { + credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialDefinition = new CredentialDefinitionResponseDto(); + proof = VCCredentialRequestProof.builder() + .proofType("jwt") + .jwt("sample.jwt.token") + .build(); + } + + @Test + void buildCredentialRequest_LdpVcFormatWithValidContext_ReturnsCorrectRequest() { + List types = Arrays.asList("VerifiableCredential", "IdentityCredential"); + List context = Arrays.asList("https://www.w3.org/2018/credentials/v1", "https://example.com/contexts/identity/v1"); + + credentialDefinition.setType(types); + credentialDefinition.setContext(context); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + Draft13VCCredentialRequest result = builder.buildCredentialRequest( + CredentialFormat.LDP_VC.getFormat(), proof, credentialsSupportedResponse); + + assertNotNull(result); + assertEquals(CredentialFormat.LDP_VC.getFormat(), result.getFormat()); + assertEquals(proof, result.getProof()); + assertNotNull(result.getCredentialDefinition()); + assertEquals(types, result.getCredentialDefinition().getType()); + assertEquals(context, result.getCredentialDefinition().getContext()); + } + + @Test + void buildCredentialRequest_LdpVcFormatWithNullContext_UsesDefaultContext() { + List types = Arrays.asList("VerifiableCredential", "IdentityCredential"); + + credentialDefinition.setType(types); + credentialDefinition.setContext(null); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + Draft13VCCredentialRequest result = builder.buildCredentialRequest( + CredentialFormat.LDP_VC.getFormat(), proof, credentialsSupportedResponse); + + assertNotNull(result); + assertNotNull(result.getCredentialDefinition()); + assertEquals(types, result.getCredentialDefinition().getType()); + assertEquals(List.of("https://www.w3.org/2018/credentials/v1"), result.getCredentialDefinition().getContext()); + } + + @Test + void buildCredentialRequest_LdpVcFormatWithEmptyContext_UsesDefaultContext() { + List types = Arrays.asList("VerifiableCredential", "IdentityCredential"); + + credentialDefinition.setType(types); + credentialDefinition.setContext(new ArrayList<>()); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinition); + + Draft13VCCredentialRequest result = builder.buildCredentialRequest( + CredentialFormat.LDP_VC.getFormat(), proof, credentialsSupportedResponse); + + assertNotNull(result); + assertNotNull(result.getCredentialDefinition()); + assertEquals(types, result.getCredentialDefinition().getType()); + assertEquals(List.of("https://www.w3.org/2018/credentials/v1"), result.getCredentialDefinition().getContext()); + } + + @Test + void buildCredentialRequest_SdJwtFormat_ReturnsCorrectRequest() { + credentialsSupportedResponse.setVct("IdentityCredential"); + + Draft13VCCredentialRequest result = builder.buildCredentialRequest( + CredentialFormat.VC_SD_JWT.getFormat(), proof, credentialsSupportedResponse); + + assertNotNull(result); + assertEquals(CredentialFormat.VC_SD_JWT.getFormat(), result.getFormat()); + assertEquals(proof, result.getProof()); + assertEquals("IdentityCredential", result.getVct()); + } + + @Test + void buildCredentialRequest_DcSdJwtFormat_ReturnsCorrectRequest() { + credentialsSupportedResponse.setVct("IdentityCredential"); + + Draft13VCCredentialRequest result = builder.buildCredentialRequest( + CredentialFormat.DC_SD_JWT.getFormat(), proof, credentialsSupportedResponse); + + assertNotNull(result); + assertEquals(CredentialFormat.DC_SD_JWT.getFormat(), result.getFormat()); + assertEquals(proof, result.getProof()); + assertEquals("IdentityCredential", result.getVct()); + } + + @Test + void buildCredentialRequest_UnsupportedFormat_ThrowsIllegalArgumentException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + builder.buildCredentialRequest("UNSUPPORTED_FORMAT", proof, credentialsSupportedResponse); + }); + + assertEquals("Unsupported credential format: UNSUPPORTED_FORMAT", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/GlobalExceptionHandlerTest.java b/src/test/java/io/mosip/mimoto/util/GlobalExceptionHandlerTest.java new file mode 100644 index 000000000..3a6ebe892 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/GlobalExceptionHandlerTest.java @@ -0,0 +1,246 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.exception.ExternalServiceUnavailableException; +import io.mosip.mimoto.exception.InvalidRequestException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.servlet.resource.NoResourceFoundException; + +import java.lang.reflect.Method; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GlobalExceptionHandlerTest { + + @InjectMocks + private GlobalExceptionHandler globalExceptionHandler; + + @Before + public void setUp() { + // Initialize any mocks if needed + } + + @Test + public void handleGenericExceptionReturnsInternalServerError() throws Exception { + // Arrange + Exception ex = new RuntimeException("Unexpected error"); + String expectedErrorCode = ErrorConstants.INTERNAL_SERVER_ERROR.getErrorCode(); + String expectedErrorMessage = ErrorConstants.INTERNAL_SERVER_ERROR.getErrorMessage(); + + // Act + ErrorDTO result = globalExceptionHandler.handleGenericException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertEquals(expectedErrorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleGenericException", Exception.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, responseStatus.value()); + } + + @Test + public void handleDatabaseExceptionReturnsServiceUnavailable() throws Exception { + // Arrange + DataAccessResourceFailureException ex = new DataAccessResourceFailureException("Database connection failed"); + String expectedErrorCode = ErrorConstants.DATABASE_CONNECTION_EXCEPTION.getErrorCode(); + String expectedErrorMessage = ErrorConstants.DATABASE_CONNECTION_EXCEPTION.getErrorMessage(); + + // Act + ErrorDTO result = globalExceptionHandler.handleDatabaseException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertEquals(expectedErrorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleDatabaseException", Exception.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, responseStatus.value()); + } + + @Test + public void handleExternalServiceUnavailableReturnsServiceUnavailable() throws Exception { + // Arrange + String errorCode = "EXT_SERVICE_ERROR"; + String errorMessage = "EXT_SERVICE_ERROR --> External service unavailable"; + ExternalServiceUnavailableException ex = new ExternalServiceUnavailableException(errorCode, errorMessage); + + // Act + ErrorDTO result = globalExceptionHandler.handleExternalServiceUnavailable(ex); + + // Assert + assertEquals(errorCode, result.getErrorCode()); + assertEquals(errorCode + " --> " + errorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleExternalServiceUnavailable", ExternalServiceUnavailableException.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, responseStatus.value()); + } + + @Test + public void handleInvalidRequestReturnsBadRequest() throws Exception { + // Arrange + String errorCode = "INVALID_REQUEST"; + String errorMessage = "Invalid input provided"; + InvalidRequestException ex = new InvalidRequestException(errorCode, errorMessage); + + // Act + ErrorDTO result = globalExceptionHandler.handleInvalidRequest(ex); + + // Assert + assertEquals(errorCode, result.getErrorCode()); + assertEquals(errorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleInvalidRequest", InvalidRequestException.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.BAD_REQUEST, responseStatus.value()); + } + + @Test + public void handleHttpMessageNotReadableExceptionReturnsBadRequest() throws Exception { + // Arrange + HttpMessageNotReadableException ex = new HttpMessageNotReadableException("Invalid JSON format"); + String expectedErrorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + String expectedErrorMessage = "Unable to process request body"; + + // Act + ErrorDTO result = globalExceptionHandler.handleHttpMessageNotReadableException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertEquals(expectedErrorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleHttpMessageNotReadableException", HttpMessageNotReadableException.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.BAD_REQUEST, responseStatus.value()); + } + + @Test + public void handleValidationExceptionReturnsBadRequest() throws Exception { + // Arrange: create BindingResult with a field error + Object target = new Object(); + org.springframework.validation.BeanPropertyBindingResult bindingResult = + new org.springframework.validation.BeanPropertyBindingResult(target, "errorDTO"); + bindingResult.addError(new org.springframework.validation.FieldError("errorDTO", "errorCode", "errorCode is required")); + + // Create MethodParameter pointing to the handler's parameter to construct the exception + java.lang.reflect.Method handlerMethod = GlobalExceptionHandler.class.getMethod("handleValidationException", org.springframework.web.bind.MethodArgumentNotValidException.class); + org.springframework.core.MethodParameter methodParameter = new org.springframework.core.MethodParameter(handlerMethod, 0); + + org.springframework.web.bind.MethodArgumentNotValidException ex = + new org.springframework.web.bind.MethodArgumentNotValidException(methodParameter, bindingResult); + + // Act + org.springframework.http.ResponseEntity response = globalExceptionHandler.handleValidationException(ex); + + // Assert + org.junit.Assert.assertEquals(org.springframework.http.HttpStatus.BAD_REQUEST, response.getStatusCode()); + io.mosip.mimoto.dto.ErrorDTO body = response.getBody(); + org.junit.Assert.assertNotNull(body); + org.junit.Assert.assertEquals("invalid_request", body.getErrorCode()); + org.junit.Assert.assertTrue(body.getErrorMessage().contains("errorCode")); + } + + @Test + public void testHandleUnsupportedRequestExceptionReturnsMethodNotAllowed() throws Exception { + // Arrange + String errorMessage = "Request method 'DELETE' not supported"; + HttpRequestMethodNotSupportedException ex = mock(HttpRequestMethodNotSupportedException.class); + when(ex.getMessage()).thenReturn(errorMessage); + String expectedErrorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + + // Act + ErrorDTO result = globalExceptionHandler.handleUnsupportedRequestException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertEquals(errorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleUnsupportedRequestException", HttpRequestMethodNotSupportedException.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.METHOD_NOT_ALLOWED, responseStatus.value()); + } + + @Test + public void testHandleUnsupportedRequestExceptionWithExceptionMessage() throws Exception { + // Arrange + String errorMessage = "Request method 'PATCH' not supported. Supported methods are: GET, POST"; + HttpRequestMethodNotSupportedException ex = mock(HttpRequestMethodNotSupportedException.class); + when(ex.getMessage()).thenReturn(errorMessage); + String expectedErrorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + + // Act + ErrorDTO result = globalExceptionHandler.handleUnsupportedRequestException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertNotNull(result.getErrorMessage()); + assertEquals(errorMessage, result.getErrorMessage()); + } + + @Test + public void testHandleNoResourceFoundExceptionReturnsNotFound() throws Exception { + // Arrange + String errorMessage = "No static resource found at path /api/invalid"; + NoResourceFoundException ex = mock(NoResourceFoundException.class); + when(ex.getMessage()).thenReturn(errorMessage); + String expectedErrorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + + // Act + ErrorDTO result = globalExceptionHandler.handleNoResourceFoundException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertEquals(errorMessage, result.getErrorMessage()); + + // Verify ResponseStatus annotation + Method method = GlobalExceptionHandler.class.getMethod("handleNoResourceFoundException", NoResourceFoundException.class); + ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class); + assertNotNull(responseStatus); + assertEquals(HttpStatus.NOT_FOUND, responseStatus.value()); + } + + @Test + public void testHandleNoResourceFoundExceptionWithExceptionMessage() throws Exception { + // Arrange + String errorMessage = "No static resource found at path /api/test/resource"; + NoResourceFoundException ex = mock(NoResourceFoundException.class); + when(ex.getMessage()).thenReturn(errorMessage); + String expectedErrorCode = ErrorConstants.INVALID_REQUEST.getErrorCode(); + + // Act + ErrorDTO result = globalExceptionHandler.handleNoResourceFoundException(ex); + + // Assert + assertEquals(expectedErrorCode, result.getErrorCode()); + assertNotNull(result.getErrorMessage()); + assertEquals(errorMessage, result.getErrorMessage()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/IssuerConfigUtilTest.java b/src/test/java/io/mosip/mimoto/util/IssuerConfigUtilTest.java new file mode 100644 index 000000000..e6b24abb4 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/IssuerConfigUtilTest.java @@ -0,0 +1,205 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.AuthorizationServerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.exception.ApiNotAccessibleException; +import io.mosip.mimoto.exception.AuthorizationServerWellknownResponseException; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.parser.WellknownParserFactory; +import io.mosip.mimoto.util.parser.WellknownResponseParser; +import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import static io.mosip.mimoto.util.TestUtilities.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Slf4j +@RunWith(MockitoJUnitRunner.class) +public class IssuerConfigUtilTest { + + @InjectMocks + IssuerConfigUtil issuersConfigUtil; + + @Mock + RestApiClient restApiClient; + + @Mock + ObjectMapper objectMapper; + + @Mock + Validator validator; + + @Mock + Draft13CredentialIssuerWellknownResponseValidator credentialIssuerWellknownResponseValidator; + + @Mock + VCSpecVersionDetector versionDetector; + + @Mock + WellknownParserFactory parserFactory; + + @Mock + WellknownResponseParser wellknownResponseParser; + + String authorizationServerWellknownUrl, authorizationServerHostUrl, issuerWellKnownUrl,exceptionMsgPrefix, issuerId, credentialIssuerHostUrl, authServerWellknownUrl; + + CredentialIssuerWellKnownResponse expectedCredentialIssuerWellKnownResponse; + + + @Before + public void setUp() throws Exception { + authorizationServerWellknownUrl = "https://dev/authorize/.well-known/oauth-authorization-server"; + authorizationServerHostUrl = "https://dev/authorize"; + exceptionMsgPrefix = "RESIDENT-APP-042 --> Invalid Authorization Server well-known from server:\n"; + + credentialIssuerHostUrl = "https://issuer.env.net"; + issuerWellKnownUrl = "https://issuer.env.net/.well-known/openid-credential-issuer"; + expectedCredentialIssuerWellKnownResponse = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", getCredentialSupportedResponse("CredentialType1"))); + String expectedWellknownJson = getExpectedWellKnownJson(); + Mockito.when(restApiClient.getApi(issuerWellKnownUrl, String.class)) + .thenReturn(expectedWellknownJson); + Mockito.when(objectMapper.readTree(expectedWellknownJson)) + .thenReturn(Mockito.mock(JsonNode.class)); + Mockito.when(versionDetector.detectVersion(any(JsonNode.class))) + .thenReturn(VCSpecificationVersion.DRAFT_13); + Mockito.when(parserFactory.getParser(eq(VCSpecificationVersion.DRAFT_13))) + .thenReturn(wellknownResponseParser); + Mockito.when(wellknownResponseParser.parse(expectedWellknownJson)) + .thenReturn(expectedCredentialIssuerWellKnownResponse); + } + + @Test + public void shouldThrowExceptionIfResponseIsNullWhenGettingAuthServerWellknownConfig() { + String expectedExceptionMessage = exceptionMsgPrefix + "java.lang.Exception: well-known api is not accessible"; + Mockito.when(restApiClient.getApi(authorizationServerWellknownUrl, String.class)).thenReturn(null); + + AuthorizationServerWellknownResponseException actualException = assertThrows(AuthorizationServerWellknownResponseException.class, () -> { + issuersConfigUtil.getAuthServerWellknown(authorizationServerHostUrl); + }); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + } + + @Test + public void shouldThrowExceptionIfAuthorizationEndpointIsMissingInAuthServerWellknownResponse() throws Exception { + String expectedExceptionMessage = exceptionMsgPrefix + "java.lang.Exception: Validation failed:\n" + "authorizationEndpoint: must not be blank"; + AuthorizationServerWellKnownResponse expectedAuthorizationServerWellKnownResponse = getAuthServerWellknownResponseDto(List.of("authorization_endpoint")); + String expectedIssuersConfigJson = getExpectedIssuersConfigJson(); + Mockito.when(restApiClient.getApi(authorizationServerWellknownUrl, String.class)).thenReturn(expectedIssuersConfigJson); + Mockito.when(objectMapper.readValue(expectedIssuersConfigJson, AuthorizationServerWellKnownResponse.class)).thenReturn(expectedAuthorizationServerWellKnownResponse); + + AuthorizationServerWellknownResponseException actualException = assertThrows(AuthorizationServerWellknownResponseException.class, () -> { + issuersConfigUtil.getAuthServerWellknown(authorizationServerHostUrl); + }); + + assertEquals(expectedExceptionMessage, actualException.getMessage()); + } + + @Test + public void shouldReturnResponseIfTheAuthServerHostUrlAndWellknownResponseAreValid() throws Exception { + AuthorizationServerWellKnownResponse expectedAuthorizationServerWellKnownResponse = getAuthServerWellknownResponseDto(List.of()); + String expectedIssuersConfigJson = getExpectedIssuersConfigJson(); + Mockito.when(restApiClient.getApi(authorizationServerWellknownUrl, String.class)).thenReturn(expectedIssuersConfigJson); + Mockito.when(objectMapper.readValue(expectedIssuersConfigJson, AuthorizationServerWellKnownResponse.class)).thenReturn(expectedAuthorizationServerWellKnownResponse); + + AuthorizationServerWellKnownResponse actualAuthorizationServerWellKnownResponse = issuersConfigUtil.getAuthServerWellknown(authorizationServerHostUrl); + + assertEquals(expectedAuthorizationServerWellKnownResponse, actualAuthorizationServerWellKnownResponse); + } + + @Test + public void shouldReturnIssuerWellknownForTheRequestedIssuerId() throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + CredentialIssuerWellKnownResponse actualCredentialIssuerWellKnownResponse = issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl); + + assertEquals(expectedCredentialIssuerWellKnownResponse, actualCredentialIssuerWellKnownResponse); + verify(restApiClient, times(1)).getApi(issuerWellKnownUrl, String.class); + verify(parserFactory, times(1)).getParser(eq(VCSpecificationVersion.DRAFT_13)); + verify(wellknownResponseParser, times(1)).validate(eq(expectedCredentialIssuerWellKnownResponse), eq(validator)); + } + + @Test + public void shouldThrowExceptionIfAnyIssuerOccurredWhileFetchingIssuerWellknown() throws ApiNotAccessibleException, IOException, InvalidWellknownResponseException { + Mockito.when(restApiClient.getApi(issuerWellKnownUrl, String.class)) + .thenReturn(null); + + ApiNotAccessibleException actualException = assertThrows(ApiNotAccessibleException.class, () -> { + issuersConfigUtil.getIssuerWellknown(credentialIssuerHostUrl); + }); + + assertEquals("RESIDENT-APP-026 --> Api not accessible failure", actualException.getMessage()); + verify(restApiClient, times(1)).getApi(issuerWellKnownUrl, String.class); + } + + @Test + public void testCamelToTitleCaseNullInput() { + String result = issuersConfigUtil.camelToTitleCase(null); + assertNull(result); + } + + @Test + public void testCamelToTitleCaseEmptyInput() { + String result = issuersConfigUtil.camelToTitleCase(""); + assertEquals("", result); + } + + @Test + public void testCamelToTitleCaseSingleWord() { + String result = issuersConfigUtil.camelToTitleCase("name"); + assertEquals("Name", result); + } + + @Test + public void testCamelToTitleCaseCamelCase() { + String result = issuersConfigUtil.camelToTitleCase("firstName"); + assertEquals("First Name", result); + } + + @Test + public void testCamelToTitleCaseMultipleCamelCase() { + String result = issuersConfigUtil.camelToTitleCase("firstNameAndLastName"); + assertEquals("First Name And Last Name", result); + } + + @Test + public void testCamelToTitleCaseWithAcronym() { + String result = issuersConfigUtil.camelToTitleCase("PRACondition"); + assertEquals("PRA Condition", result); + } + + @Test + public void testCamelToTitleCaseAcronymAtEnd() { + String result = issuersConfigUtil.camelToTitleCase("conditionPRA"); + assertEquals("Condition PRA", result); + } + + @Test + public void testCamelToTitleCaseAllUppercase() { + String result = issuersConfigUtil.camelToTitleCase("URL"); + assertEquals("URL", result); + } + + @Test + public void testCamelToTitleCaseMixedCase() { + String result = issuersConfigUtil.camelToTitleCase("pinFor"); + assertEquals("Pin For", result); + } + +} + diff --git a/src/test/java/io/mosip/mimoto/util/JoseUtilTest.java b/src/test/java/io/mosip/mimoto/util/JoseUtilTest.java new file mode 100644 index 000000000..035eb6570 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/JoseUtilTest.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.JwkDto; +import org.junit.Test; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static org.junit.Assert.assertEquals; + +public class JoseUtilTest { + @Test + public void getJwkFromPublicKeyTest() throws NoSuchAlgorithmException, InvalidKeySpecException { + String publicKey = "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAn6+frMlD7DQqbxZW943hRLBApDj1/lHIJdLYSKEGIfwhd58gc0Y4\n1q11mPnpv7gAZ/Wm0iOAkWSzcIWljXFmGnLrUrBsp4WYKdPjqn4tkrCOjiZa5RPk\nY03a40Kz1lx0W9f94Naozglf6KFUSq+qAwuC5kiPxaxsjFA/LWIP+zT2QX/MnrX9\nv7gt2g0BC4pQ01eTTzhhwO2A7k5z3ucsb56ohND4xdIsdCMm1IczBjW0URSO60Bb\n7m5dlO8BFHJ6inV8awO2KHoADbp3wZgid4KqLJ0eVGyNViVFzj4rxSxL3vcYbyKS\nORWSlPZIZL9ZWO1cyPO9+Wxu29IKj4DQEt8glgITlBZ4L29uT7gFPAbypSn/8SvU\nBrNno8+GIe9XWsrDTMT9dfLGzLUitF3A+wwVZuRVhCqYIisOOGuGE18YK0jmdk9l\n89OpK4PduGiUh66zZTcH3thdtaOz6jj+FLKMg2Q3gNqQ1Y0cezO175RNVVX1ffOu\n5qss1RWams5RAXDqqt/MhiopG3DhlyaSC4xdqei7SI8d+S4Bvflub9rypPnhW67g\nNhZvQDJ7Tb1AWHxKmU0wQvEMtwSm9xtsMs4bqotn2M/09BuRqbrhpvAfrfZArkVO\nv8eLXhtDvo2J9gRwHZIS/JZ1Fo+tep1QFHz1Lr5iGRqwLWQlGbKFuL0CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"; + JwkDto expectedJWK = new JwkDto("RSA", "AQAB", "n6-frMlD7DQqbxZW943hRLBApDj1_lHIJdLYSKEGIfwhd58gc0Y41q11mPnpv7gAZ_Wm0iOAkWSzcIWljXFmGnLrUrBsp4WYKdPjqn4tkrCOjiZa5RPkY03a40Kz1lx0W9f94Naozglf6KFUSq-qAwuC5kiPxaxsjFA_LWIP-zT2QX_MnrX9v7gt2g0BC4pQ01eTTzhhwO2A7k5z3ucsb56ohND4xdIsdCMm1IczBjW0URSO60Bb7m5dlO8BFHJ6inV8awO2KHoADbp3wZgid4KqLJ0eVGyNViVFzj4rxSxL3vcYbyKSORWSlPZIZL9ZWO1cyPO9-Wxu29IKj4DQEt8glgITlBZ4L29uT7gFPAbypSn_8SvUBrNno8-GIe9XWsrDTMT9dfLGzLUitF3A-wwVZuRVhCqYIisOOGuGE18YK0jmdk9l89OpK4PduGiUh66zZTcH3thdtaOz6jj-FLKMg2Q3gNqQ1Y0cezO175RNVVX1ffOu5qss1RWams5RAXDqqt_MhiopG3DhlyaSC4xdqei7SI8d-S4Bvflub9rypPnhW67gNhZvQDJ7Tb1AWHxKmU0wQvEMtwSm9xtsMs4bqotn2M_09BuRqbrhpvAfrfZArkVOv8eLXhtDvo2J9gRwHZIS_JZ1Fo-tep1QFHz1Lr5iGRqwLWQlGbKFuL0"); + + JwkDto actualJWK = JoseUtil.getJwkFromPublicKey(publicKey); + + assertEquals(expectedJWK, actualJWK); + } + +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/P12KeyStoreManagerTest.java b/src/test/java/io/mosip/mimoto/util/P12KeyStoreManagerTest.java new file mode 100644 index 000000000..096d4d667 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/P12KeyStoreManagerTest.java @@ -0,0 +1,379 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.exception.CryptoManagerException; +import org.apache.commons.codec.binary.Base64; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.cert.Certificate; +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class P12KeyStoreManagerTest { + + @InjectMocks + private P12KeyStoreManager p12KeyStoreManager; + + private KeyPair keyPair; + private KeyStore.PrivateKeyEntry privateKeyEntry; + private SecretKey symmetricKey; + private static final String KEY_SPLITTER = "#KEY_SPLITTER#"; + private static final byte[] VERSION_RSA_2048 = "VER_R2".getBytes(); + private static final int THUMBPRINT_LENGTH = 32; + private static final int NONCE = 12; + + @Before + public void setUp() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + + Certificate certificate = generateSelfSignedCertificate(keyPair); + Certificate[] certChain = new Certificate[]{certificate}; + privateKeyEntry = new KeyStore.PrivateKeyEntry(keyPair.getPrivate(), certChain); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + symmetricKey = keyGen.generateKey(); + + ReflectionTestUtils.setField(p12KeyStoreManager, "fileName", "test.p12"); + ReflectionTestUtils.setField(p12KeyStoreManager, "cyptoPassword", "test123"); + ReflectionTestUtils.setField(p12KeyStoreManager, "alias", "testalias"); + ReflectionTestUtils.setField(p12KeyStoreManager, "isThumbprint", true); + } + + private java.security.cert.X509Certificate generateSelfSignedCertificate(KeyPair keyPair) throws Exception { + long now = System.currentTimeMillis(); + java.util.Date startDate = new java.util.Date(now); + org.bouncycastle.asn1.x500.X500Name dnName = new org.bouncycastle.asn1.x500.X500Name("CN=Test"); + java.math.BigInteger certSerialNumber = java.math.BigInteger.valueOf(now); + + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.setTime(startDate); + calendar.add(java.util.Calendar.YEAR, 1); + java.util.Date endDate = calendar.getTime(); + + org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder certBuilder = + new org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder( + dnName, certSerialNumber, startDate, endDate, dnName, keyPair.getPublic()); + + org.bouncycastle.operator.ContentSigner contentSigner = + new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder("SHA256WithRSA") + .build(keyPair.getPrivate()); + + org.bouncycastle.cert.X509CertificateHolder certHolder = certBuilder.build(contentSigner); + return new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter().getCertificate(certHolder); + } + + @Test + public void testDecrypt_Success() throws Exception { + String plainText = "Test Data"; + P12KeyStoreManager spyManager = spy(p12KeyStoreManager); + doReturn(privateKeyEntry).when(spyManager).loadP12(); + doReturn(plainText.getBytes()).when(spyManager).decryptData(any(byte[].class), any(KeyStore.PrivateKeyEntry.class)); + + String result = spyManager.decrypt(Base64.encodeBase64String("test".getBytes())); + + assertEquals(plainText, result); + } + + @Test + public void testDecrypt_WithNullData() throws Exception { + String result = p12KeyStoreManager.decrypt(null); + assertNull(result); + } + + @Test + public void testDecrypt_ExceptionHandling() throws Exception { + P12KeyStoreManager spyManager = spy(p12KeyStoreManager); + doThrow(new IOException("Test exception")).when(spyManager).loadP12(); + + String result = spyManager.decrypt("invalid_data"); + assertNull(result); + } + + @Test + public void testLoadP12_FileNotFound() throws Exception { + KeyStore.PrivateKeyEntry entry = p12KeyStoreManager.loadP12("nonexistent.p12", "alias", "password"); + assertNull(entry); + } + + @Test + public void testLoadP12_InvalidFile() throws Exception { + java.io.File tempFile = java.io.File.createTempFile("invalid", ".p12"); + tempFile.deleteOnExit(); + try { + try (java.io.FileWriter writer = new java.io.FileWriter(tempFile)) { + writer.write("Not a valid PKCS12 file"); + } + + KeyStore.PrivateKeyEntry entry = p12KeyStoreManager.loadP12(tempFile.getAbsolutePath(), "alias", "password"); + assertNull(entry); + } finally { + // Ensure cleanup even if writer.write(...) or loadP12(...) throws + tempFile.delete(); + } + } + + @Test + public void testDecryptData_InvalidFormat() throws Exception { + byte[] invalidData = "invalid_data_no_splitter".getBytes(); + byte[] result = p12KeyStoreManager.decryptData(invalidData, privateKeyEntry); + assertNull(result); + } + + @Test + public void testDecryptData_WithNullPrivateKey() throws Exception { + byte[] data = ("key" + KEY_SPLITTER + "data").getBytes(); + byte[] result = p12KeyStoreManager.decryptData(data, null); + assertNull(result); + } + + @Test + public void testParseEncryptKeyHeader_WithVersionHeader() { + byte[] encryptedKey = new byte[THUMBPRINT_LENGTH + VERSION_RSA_2048.length + 256]; + System.arraycopy(VERSION_RSA_2048, 0, encryptedKey, 0, VERSION_RSA_2048.length); + + byte[] result = p12KeyStoreManager.parseEncryptKeyHeader(encryptedKey); + assertArrayEquals(VERSION_RSA_2048, result); + } + + @Test + public void testParseEncryptKeyHeader_WithoutVersionHeader() { + byte[] encryptedKey = new byte[THUMBPRINT_LENGTH + 256]; + Arrays.fill(encryptedKey, (byte) 0); + + byte[] result = p12KeyStoreManager.parseEncryptKeyHeader(encryptedKey); + assertEquals(0, result.length); + } + + @Test + public void testGetCertificateThumbprint_Success() throws Exception { + Certificate mockCert = mock(Certificate.class); + when(mockCert.getEncoded()).thenReturn("test certificate data".getBytes()); + + byte[] thumbprint = P12KeyStoreManager.getCertificateThumbprint(mockCert); + + assertNotNull(thumbprint); + assertEquals(32, thumbprint.length); + } + + @Test(expected = CryptoManagerException.class) + public void testGetCertificateThumbprint_Exception() throws Exception { + Certificate mockCert = mock(Certificate.class); + when(mockCert.getEncoded()).thenThrow(new java.security.cert.CertificateEncodingException("Test exception")); + + P12KeyStoreManager.getCertificateThumbprint(mockCert); + } + + @Test + public void testSymmetricDecrypt_Success() throws Exception { + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + + byte[] nonce = new byte[NONCE]; + new SecureRandom().nextBytes(nonce); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonce); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] encryptedBytes = cipher.doFinal(plainBytes); + + byte[] result = p12KeyStoreManager.symmetricDecrypt(symmetricKey, encryptedBytes, nonce, null); + + assertNotNull(result); + assertEquals(plainText, new String(result, StandardCharsets.UTF_8)); + } + + @Test + public void testSymmetricDecrypt_WithAAD() throws Exception { + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + + byte[] nonce = new byte[NONCE]; + new SecureRandom().nextBytes(nonce); + byte[] aad = new byte[32]; + System.arraycopy(nonce, 0, aad, 0, NONCE); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonce); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + cipher.updateAAD(aad); + byte[] encryptedBytes = cipher.doFinal(plainBytes); + + byte[] result = p12KeyStoreManager.symmetricDecrypt(symmetricKey, encryptedBytes, nonce, aad); + + assertNotNull(result); + assertEquals(plainText, new String(result, StandardCharsets.UTF_8)); + } + + @Test(expected = CryptoManagerException.class) + public void testSymmetricDecrypt_WrongKey() throws Exception { + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + byte[] nonce = new byte[NONCE]; + new SecureRandom().nextBytes(nonce); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, nonce); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] encryptedBytes = cipher.doFinal(plainBytes); + + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + SecretKey differentKey = keyGen.generateKey(); + + p12KeyStoreManager.symmetricDecrypt(differentKey, encryptedBytes, nonce, null); + } + + @Test + public void testAsymmetricDecrypt_PrivateMethod() throws Exception { + String plainText = "Test Symmetric Key"; + byte[] plainBytes = plainText.getBytes(); + + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); + javax.crypto.spec.OAEPParameterSpec oaepParams = new javax.crypto.spec.OAEPParameterSpec("SHA-256", "MGF1", + java.security.spec.MGF1ParameterSpec.SHA256, javax.crypto.spec.PSource.PSpecified.DEFAULT); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic(), oaepParams); + byte[] encryptedBytes = cipher.doFinal(plainBytes); + + java.lang.reflect.Method method = P12KeyStoreManager.class.getDeclaredMethod("asymmetricDecrypt", + java.security.PrivateKey.class, java.math.BigInteger.class, byte[].class); + method.setAccessible(true); + + byte[] result = (byte[]) method.invoke(null, keyPair.getPrivate(), + ((java.security.interfaces.RSAPrivateKey) keyPair.getPrivate()).getModulus(), encryptedBytes); + + assertNotNull(result); + assertArrayEquals(plainBytes, result); + } + + @Test + public void testSymmetricDecrypt_PrivateMethod() throws Exception { + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] aad = "additional data".getBytes(); + cipher.updateAAD(aad); + byte[] encryptedData = cipher.doFinal(plainBytes); + + java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); + outputStream.write(encryptedData); + outputStream.write(iv); + byte[] dataWithIV = outputStream.toByteArray(); + + java.lang.reflect.Method method = P12KeyStoreManager.class.getDeclaredMethod("symmetricDecrypt", + SecretKey.class, byte[].class, byte[].class); + method.setAccessible(true); + + byte[] result = (byte[]) method.invoke(null, symmetricKey, dataWithIV, aad); + + assertNotNull(result); + assertEquals(plainText, new String(result, StandardCharsets.UTF_8)); + } + + @Test + public void testDecryptData_WithThumbprint_NoVersionHeader() throws Exception { + ReflectionTestUtils.setField(p12KeyStoreManager, "isThumbprint", true); + + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + + Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); + aesCipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] encryptedData = aesCipher.doFinal(plainBytes); + + java.io.ByteArrayOutputStream dataStream = new java.io.ByteArrayOutputStream(); + dataStream.write(encryptedData); + dataStream.write(iv); + byte[] fullEncryptedData = dataStream.toByteArray(); + + Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); + javax.crypto.spec.OAEPParameterSpec oaepParams = new javax.crypto.spec.OAEPParameterSpec("SHA-256", "MGF1", + java.security.spec.MGF1ParameterSpec.SHA256, javax.crypto.spec.PSource.PSpecified.DEFAULT); + rsaCipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic(), oaepParams); + byte[] encryptedSymmetricKey = rsaCipher.doFinal(symmetricKey.getEncoded()); + + java.io.ByteArrayOutputStream keyStream = new java.io.ByteArrayOutputStream(); + keyStream.write(new byte[32]); // THUMBPRINT_LENGTH = 32 + keyStream.write(encryptedSymmetricKey); + byte[] fullEncryptedKey = keyStream.toByteArray(); + + java.io.ByteArrayOutputStream finalStream = new java.io.ByteArrayOutputStream(); + finalStream.write(fullEncryptedKey); + finalStream.write("#KEY_SPLITTER#".getBytes()); + finalStream.write(fullEncryptedData); + byte[] finalData = finalStream.toByteArray(); + + byte[] result = p12KeyStoreManager.decryptData(finalData, privateKeyEntry); + + assertNotNull(result); + assertEquals(plainText, new String(result, StandardCharsets.UTF_8)); + } + + @Test + public void testDecryptData_WithoutThumbprint_NoVersionHeader() throws Exception { + ReflectionTestUtils.setField(p12KeyStoreManager, "isThumbprint", false); + + String plainText = "Test Data"; + byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8); + + Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getEncoded(), "AES"); + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); + aesCipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + byte[] encryptedData = aesCipher.doFinal(plainBytes); + + java.io.ByteArrayOutputStream dataStream = new java.io.ByteArrayOutputStream(); + dataStream.write(encryptedData); + dataStream.write(iv); + byte[] fullEncryptedData = dataStream.toByteArray(); + + Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); + javax.crypto.spec.OAEPParameterSpec oaepParams = new javax.crypto.spec.OAEPParameterSpec("SHA-256", "MGF1", + java.security.spec.MGF1ParameterSpec.SHA256, javax.crypto.spec.PSource.PSpecified.DEFAULT); + rsaCipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic(), oaepParams); + byte[] encryptedSymmetricKey = rsaCipher.doFinal(symmetricKey.getEncoded()); + + java.io.ByteArrayOutputStream finalStream = new java.io.ByteArrayOutputStream(); + finalStream.write(encryptedSymmetricKey); + finalStream.write("#KEY_SPLITTER#".getBytes()); + finalStream.write(fullEncryptedData); + byte[] finalData = finalStream.toByteArray(); + + byte[] result = p12KeyStoreManager.decryptData(finalData, privateKeyEntry); + + assertNotNull(result); + assertEquals(plainText, new String(result, StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/RestApiClientTest.java b/src/test/java/io/mosip/mimoto/util/RestApiClientTest.java new file mode 100644 index 000000000..cb01490e1 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/RestApiClientTest.java @@ -0,0 +1,307 @@ +package io.mosip.mimoto.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.*; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RestApiClientTest { + + private static final String TEST_URI = "https://example.com/api"; + private static final String ACCESS_TOKEN = "test-access-token"; + + @Mock + private RestTemplate restTemplate; + + @Mock + private RestTemplate plainRestTemplate; + + @InjectMocks + private RestApiClient restApiClient; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(restApiClient, "disableSelfTokenRestTemplate", false); + } + + // --- getApi(URI, Class) --- + + @Test + void getApiByUri_shouldReturnResponseBody() throws Exception { + URI uri = URI.create(TEST_URI); + ResponseEntity responseEntity = new ResponseEntity<>("response-body", HttpStatus.OK); + + when(restTemplate.exchange(eq(uri), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + String result = restApiClient.getApi(uri, String.class); + + assertEquals("response-body", result); + } + + @Test + void getApiByUri_shouldUsePlainRestTemplateWhenSelfTokenDisabled() throws Exception { + ReflectionTestUtils.setField(restApiClient, "disableSelfTokenRestTemplate", true); + URI uri = URI.create(TEST_URI); + ResponseEntity responseEntity = new ResponseEntity<>("plain-response", HttpStatus.OK); + + when(plainRestTemplate.exchange(eq(uri), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + String result = restApiClient.getApi(uri, String.class); + + assertEquals("plain-response", result); + verify(restTemplate, never()).exchange(any(URI.class), any(), any(), any(Class.class)); + } + + @Test + void getApiByUri_shouldReturnNullOnException() throws Exception { + URI uri = URI.create(TEST_URI); + + when(restTemplate.exchange(eq(uri), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenThrow(new ResourceAccessException("Connection refused")); + + String result = restApiClient.getApi(uri, String.class); + + assertNull(result); + } + + // --- getApi(String, Class) --- + + @Test + void getApiByUrl_shouldReturnResponse() { + when(restTemplate.getForObject(eq(TEST_URI), eq(String.class))) + .thenReturn("url-response"); + + String result = restApiClient.getApi(TEST_URI, String.class); + + assertEquals("url-response", result); + } + + @Test + void getApiByUrl_shouldUsePlainRestTemplateWhenSelfTokenDisabled() { + ReflectionTestUtils.setField(restApiClient, "disableSelfTokenRestTemplate", true); + + when(plainRestTemplate.getForObject(eq(TEST_URI), eq(String.class))) + .thenReturn("plain-url-response"); + + String result = restApiClient.getApi(TEST_URI, String.class); + + assertEquals("plain-url-response", result); + verify(restTemplate, never()).getForObject(anyString(), any()); + } + + @Test + void getApiByUrl_shouldReturnNullOnException() { + when(restTemplate.getForObject(eq(TEST_URI), eq(String.class))) + .thenThrow(new ResourceAccessException("Timeout")); + + String result = restApiClient.getApi(TEST_URI, String.class); + + assertNull(result); + } + + // --- postApi(String, MediaType, Object, Class) --- + + @Test + void postApi_shouldReturnResponse() throws Exception { + when(restTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(String.class))) + .thenReturn("post-response"); + + String result = restApiClient.postApi(TEST_URI, MediaType.APPLICATION_JSON, "request-body", String.class); + + assertEquals("post-response", result); + } + + @Test + void postApi_shouldUsePlainRestTemplateWhenSelfTokenDisabled() throws Exception { + ReflectionTestUtils.setField(restApiClient, "disableSelfTokenRestTemplate", true); + + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(String.class))) + .thenReturn("plain-post-response"); + + String result = restApiClient.postApi(TEST_URI, MediaType.APPLICATION_JSON, "request-body", String.class); + + assertEquals("plain-post-response", result); + verify(restTemplate, never()).postForObject(anyString(), any(), any()); + } + + @Test + void postApi_shouldReturnNullOnException() throws Exception { + when(restTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(String.class))) + .thenThrow(new ResourceAccessException("Connection refused")); + + String result = restApiClient.postApi(TEST_URI, MediaType.APPLICATION_JSON, "request-body", String.class); + + assertNull(result); + } + + // --- postApi(String, MediaType, Object, Class, String bearerToken) --- + + @Test + void postApiWithBearerToken_shouldReturnResponse() { + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(String.class))) + .thenReturn("bearer-response"); + + String result = restApiClient.postApi(TEST_URI, MediaType.APPLICATION_JSON, "request", String.class, ACCESS_TOKEN); + + assertEquals("bearer-response", result); + } + + @Test + void postApiWithBearerToken_shouldReturnNullOnException() { + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(String.class))) + .thenThrow(new ResourceAccessException("Timeout")); + + String result = restApiClient.postApi(TEST_URI, MediaType.APPLICATION_JSON, "request", String.class, ACCESS_TOKEN); + + assertNull(result); + } + + // --- postApiWithErrorResponse --- + + @Test + void postApiWithErrorResponse_shouldReturnResponseOnSuccess() { + TestResponse expected = new TestResponse("ok", null); + + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(TestResponse.class))) + .thenReturn(expected); + + TestResponse result = restApiClient.postApiWithErrorResponse( + TEST_URI, MediaType.APPLICATION_JSON, "request", TestResponse.class, ACCESS_TOKEN); + + assertNotNull(result); + assertEquals("ok", result.status); + } + + @Test + void postApiWithErrorResponse_shouldParseErrorBodyOnHttpClientError() { + String errorJson = "{\"status\":null,\"error\":\"invalid_nonce\"}"; + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, "Bad Request", + HttpHeaders.EMPTY, errorJson.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(TestResponse.class))) + .thenThrow(exception); + + TestResponse result = restApiClient.postApiWithErrorResponse( + TEST_URI, MediaType.APPLICATION_JSON, "request", TestResponse.class, ACCESS_TOKEN); + + assertNotNull(result); + assertEquals("invalid_nonce", result.error); + } + + @Test + void postApiWithErrorResponse_shouldReturnNullWhenErrorBodyCannotBeParsed() { + String malformedBody = "not-valid-json"; + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, "Bad Request", + HttpHeaders.EMPTY, malformedBody.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(TestResponse.class))) + .thenThrow(exception); + + TestResponse result = restApiClient.postApiWithErrorResponse( + TEST_URI, MediaType.APPLICATION_JSON, "request", TestResponse.class, ACCESS_TOKEN); + + assertNull(result); + } + + @Test + void postApiWithErrorResponse_shouldReturnNullOnNonClientError() { + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(TestResponse.class))) + .thenThrow(new ResourceAccessException("Connection refused")); + + TestResponse result = restApiClient.postApiWithErrorResponse( + TEST_URI, MediaType.APPLICATION_JSON, "request", TestResponse.class, ACCESS_TOKEN); + + assertNull(result); + } + + @Test + void postApiWithErrorResponse_shouldHandleHttpServerError() { + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.FORBIDDEN, "Forbidden", + HttpHeaders.EMPTY, "{}".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + + when(plainRestTemplate.postForObject(eq(TEST_URI), any(HttpEntity.class), eq(TestResponse.class))) + .thenThrow(exception); + + TestResponse result = restApiClient.postApiWithErrorResponse( + TEST_URI, MediaType.APPLICATION_JSON, "request", TestResponse.class, ACCESS_TOKEN); + + assertNotNull(result); + } + + // --- getApiWithCustomHeaders --- + + @Test + void getApiWithCustomHeaders_shouldReturnResponse() { + HttpHeaders customHeaders = new HttpHeaders(); + customHeaders.set("X-Custom", "value"); + + ResponseEntity responseEntity = new ResponseEntity<>("custom-response", HttpStatus.OK); + + when(restTemplate.exchange(eq(TEST_URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + String result = restApiClient.getApiWithCustomHeaders(TEST_URI, String.class, customHeaders); + + assertEquals("custom-response", result); + } + + @Test + void getApiWithCustomHeaders_shouldUsePlainRestTemplateWhenSelfTokenDisabled() { + ReflectionTestUtils.setField(restApiClient, "disableSelfTokenRestTemplate", true); + + HttpHeaders customHeaders = new HttpHeaders(); + ResponseEntity responseEntity = new ResponseEntity<>("plain-custom", HttpStatus.OK); + + when(plainRestTemplate.exchange(eq(TEST_URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenReturn(responseEntity); + + String result = restApiClient.getApiWithCustomHeaders(TEST_URI, String.class, customHeaders); + + assertEquals("plain-custom", result); + } + + @Test + void getApiWithCustomHeaders_shouldReturnNullOnException() { + HttpHeaders customHeaders = new HttpHeaders(); + + when(restTemplate.exchange(eq(TEST_URI), eq(HttpMethod.GET), any(HttpEntity.class), eq(String.class))) + .thenThrow(new ResourceAccessException("Timeout")); + + String result = restApiClient.getApiWithCustomHeaders(TEST_URI, String.class, customHeaders); + + assertNull(result); + } + + static class TestResponse { + public String status; + public String error; + + public TestResponse() {} + + public TestResponse(String status, String error) { + this.status = status; + this.error = error; + } + } +} diff --git a/src/test/java/io/mosip/mimoto/util/SelectedSdClaimsUtilTest.java b/src/test/java/io/mosip/mimoto/util/SelectedSdClaimsUtilTest.java new file mode 100644 index 000000000..3302060aa --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/SelectedSdClaimsUtilTest.java @@ -0,0 +1,48 @@ +package io.mosip.mimoto.util; + +import org.junit.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class SelectedSdClaimsUtilTest { + + @Test + public void mergePathsUnionsDisclosurePathsForSameCredential() { + Map> merged = new LinkedHashMap<>(); + SelectedSdClaimsUtil.mergePaths(merged, "cred-1", List.of("email")); + SelectedSdClaimsUtil.mergePaths(merged, "cred-1", List.of("phone")); + + assertEquals(List.of("email", "phone"), merged.get("cred-1")); + } + + @Test + public void mergePathsDeduplicatesPathsPreservingOrder() { + Map> merged = new LinkedHashMap<>(); + SelectedSdClaimsUtil.mergePaths(merged, "cred-1", List.of("email", "phone")); + SelectedSdClaimsUtil.mergePaths(merged, "cred-1", List.of("phone", "address")); + + assertEquals(List.of("email", "phone", "address"), merged.get("cred-1")); + } + + @Test + public void mergeIntoUnionsAcrossMaps() { + Map> merged = new LinkedHashMap<>(); + SelectedSdClaimsUtil.mergeInto(merged, Map.of("cred-1", List.of("email"))); + SelectedSdClaimsUtil.mergeInto(merged, Map.of("cred-1", List.of("phone"), "cred-2", List.of("name"))); + + assertEquals(List.of("email", "phone"), merged.get("cred-1")); + assertEquals(List.of("name"), merged.get("cred-2")); + } + + @Test + public void mergeIntoIgnoresNullSource() { + Map> merged = new LinkedHashMap<>(); + SelectedSdClaimsUtil.mergeInto(merged, null); + assertNull(merged.get("cred-1")); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/SigningKeyUtilTest.java b/src/test/java/io/mosip/mimoto/util/SigningKeyUtilTest.java new file mode 100644 index 000000000..fc9a9d534 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/SigningKeyUtilTest.java @@ -0,0 +1,187 @@ +package io.mosip.mimoto.util; + +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jwt.SignedJWT; +import io.mosip.mimoto.constant.SigningAlgorithm; +import io.mosip.mimoto.exception.KeyGenerationException; +import io.mosip.mimoto.model.ProofSigningKey; +import org.junit.Before; +import org.junit.Test; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.spec.InvalidKeySpecException; + +import static org.junit.Assert.*; + +/** + * Test cases for SigningKeyUtil. + */ +public class SigningKeyUtilTest { + + @Before + public void setUp() { + // Ensure BouncyCastle provider is registered + if (java.security.Security.getProvider("BC") == null) { + java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + } + + @Test + public void shouldGenerateKeyPairForRS256() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + + assertNotNull("KeyPair should not be null", keyPair); + assertEquals("Algorithm should be RSA", "RSA", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGenerateKeyPairForES256() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.ES256); + + assertNotNull("KeyPair should not be null", keyPair); + assertEquals("Algorithm should be EC", "EC", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGenerateKeyPairForES256K() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.ES256K); + + assertNotNull("KeyPair should not be null", keyPair); + assertEquals("Algorithm should be EC", "EC", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGenerateKeyPairForED25519() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.ED25519); + + assertNotNull("KeyPair should not be null", keyPair); + assertEquals("Algorithm should be EdDSA", "EdDSA", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGenerateKeyPair() throws Exception { + KeyPair originalKeyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + byte[] publicKeyBytes = originalKeyPair.getPublic().getEncoded(); + byte[] privateKeyBytes = originalKeyPair.getPrivate().getEncoded(); + + KeyPair reconstructedKeyPair = SigningKeyUtil.generateKeyPair( + SigningAlgorithm.RS256, publicKeyBytes, privateKeyBytes); + + assertNotNull("Reconstructed KeyPair should not be null", reconstructedKeyPair); + assertEquals("Public key algorithm should match", + originalKeyPair.getPublic().getAlgorithm(), + reconstructedKeyPair.getPublic().getAlgorithm()); + } + + @Test(expected = InvalidKeySpecException.class) + public void shouldThrowExceptionForInvalidKeyBytes() throws Exception { + byte[] invalidPublicKey = new byte[]{1, 2, 3}; + byte[] invalidPrivateKey = new byte[]{4, 5, 6}; + + SigningKeyUtil.generateKeyPair( + SigningAlgorithm.RS256, invalidPublicKey, invalidPrivateKey); + } + + @Test + public void shouldGenerateEncryptionKey() { + SecretKey secretKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + + assertNotNull("SecretKey should not be null", secretKey); + assertEquals("Algorithm should be AES", "AES", secretKey.getAlgorithm()); + } + + @Test(expected = KeyGenerationException.class) + public void shouldThrowExceptionForUnsupportedEncryptionAlgorithm() { + SigningKeyUtil.generateEncryptionKey("UNSUPPORTED", 256); + } + + @Test + public void shouldGenerateJwtSuccessfully() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + String jwt = SigningKeyUtil.generateJwt( + SigningAlgorithm.RS256, + "test-audience", + "test-client-id", + "test-nonce", + keyPair + ); + + assertNotNull("JWT should not be null", jwt); + assertFalse("JWT should not be empty", jwt.isEmpty()); + + SignedJWT signedJWT = SignedJWT.parse(jwt); + assertEquals("Subject should match", "test-client-id", signedJWT.getJWTClaimsSet().getSubject()); + assertEquals("Audience should match", "test-audience", signedJWT.getJWTClaimsSet().getAudience().getFirst()); + assertEquals("Nonce should match", "test-nonce", signedJWT.getJWTClaimsSet().getStringClaim("nonce")); + } + + @Test + public void shouldGenerateJwtWithoutNonceClaimWhenCNonceIsNull() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + String jwt = SigningKeyUtil.generateJwt(SigningAlgorithm.RS256, "test-audience", "test-client-id", null, keyPair); + + assertNotNull("JWT should not be null", jwt); + assertFalse("JWT should not be empty", jwt.isEmpty()); + + SignedJWT signedJWT = SignedJWT.parse(jwt); + assertEquals("Subject should match", "test-client-id", signedJWT.getJWTClaimsSet().getSubject()); + assertEquals("Audience should match", "test-audience", signedJWT.getJWTClaimsSet().getAudience().getFirst()); + assertFalse("Nonce claim should be absent", signedJWT.getJWTClaimsSet().getClaims().containsKey("nonce")); + } + + @Test + public void shouldGenerateJwkSuccessfully() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + JWK jwk = SigningKeyUtil.generateJwk(SigningAlgorithm.RS256, keyPair); + + assertNotNull("JWK should not be null", jwk); + assertNotNull("JWK algorithm should not be null", jwk.getAlgorithm()); + } + + @Test + public void shouldCreateSignerSuccessfully() throws Exception { + KeyPair keyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + JWK jwk = SigningKeyUtil.generateJwk(SigningAlgorithm.RS256, keyPair); + JWSSigner signer = SigningKeyUtil.createSigner(SigningAlgorithm.RS256, jwk); + + assertNotNull("Signer should not be null", signer); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionForInvalidJwkInSignerCreation() throws Exception { + KeyPair rsaKeyPair = SigningKeyUtil.generateKeyPair(SigningAlgorithm.RS256); + JWK rsaJwk = SigningKeyUtil.generateJwk(SigningAlgorithm.RS256, rsaKeyPair); + + // Try to use RS256 JWK with ES256 signer + SigningKeyUtil.createSigner(SigningAlgorithm.ES256, rsaJwk); + } + + @Test + public void shouldCreateProofSigningKeySuccessfully() { + ProofSigningKey proofSigningKey = SigningKeyUtil.createProofSigningKey(SigningAlgorithm.RS256); + + assertNotNull("ProofSigningKey should not be null", proofSigningKey); + assertNotNull("ID should not be null", proofSigningKey.getId()); + assertNotNull("Public key should not be null", proofSigningKey.getPublicKey()); + assertNotNull("Secret key should not be null", proofSigningKey.getSecretKey()); + assertNotNull("Key metadata should not be null", proofSigningKey.getKeyMetadata()); + assertEquals("Algorithm name should match", + SigningAlgorithm.RS256.name(), + proofSigningKey.getKeyMetadata().getAlgorithmName()); + } + + @Test + public void shouldCreateProofSigningKeyForAllAlgorithms() { + for (SigningAlgorithm algorithm : SigningAlgorithm.values()) { + ProofSigningKey proofSigningKey = SigningKeyUtil.createProofSigningKey(algorithm); + + assertNotNull("ProofSigningKey should not be null for " + algorithm, proofSigningKey); + assertEquals("Algorithm name should match for " + algorithm, + algorithm.name(), + proofSigningKey.getKeyMetadata().getAlgorithmName()); + } + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/SvgFixerUtilTest.java b/src/test/java/io/mosip/mimoto/util/SvgFixerUtilTest.java new file mode 100644 index 000000000..7cb3ba3f2 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/SvgFixerUtilTest.java @@ -0,0 +1,35 @@ +package io.mosip.mimoto.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SvgFixerUtilTest { + + private final SvgFixerUtil util = new SvgFixerUtil(); + + @Test + void testAddMissingOffsetToStopElements_withMissingOffset() { + String input = ""; + String expected = ""; + Assertions.assertEquals(expected, util.addMissingOffsetToStopElements(input)); + } + + @Test + void testAddMissingOffsetToStopElements_withExistingOffset() { + String input = ""; + String expected = ""; + Assertions.assertEquals(expected, util.addMissingOffsetToStopElements(input)); + } + + @Test + void testAddMissingOffsetToStopElements_multipleStops() { + String input = ""; + String expected = ""; + Assertions.assertEquals(expected, util.addMissingOffsetToStopElements(input)); + } + + @Test + void testAddMissingOffsetToStopElements_nullInput() { + Assertions.assertNull(util.addMissingOffsetToStopElements(null)); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/TestUtilities.java b/src/test/java/io/mosip/mimoto/util/TestUtilities.java new file mode 100644 index 000000000..9d7f9355e --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/TestUtilities.java @@ -0,0 +1,570 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.itextpdf.html2pdf.ConverterProperties; +import com.itextpdf.html2pdf.HtmlConverter; +import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfReader; +import io.mosip.mimoto.dto.resident.VerifiablePresentationSessionData; +import io.mosip.mimoto.model.CredentialMetadata; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.model.VerifiableCredential; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletMetadata; +import io.mosip.mimoto.dto.*; +import io.mosip.mimoto.dto.mimoto.*; +import io.mosip.mimoto.dto.openid.VerifierDTO; +import io.mosip.mimoto.dto.openid.VerifiersDTO; +import io.mosip.mimoto.dto.openid.datashare.DataShareResponseDTO; +import io.mosip.mimoto.dto.openid.datashare.DataShareResponseWrapperDTO; +import io.mosip.mimoto.dto.openid.presentation.*; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.openID4VP.OpenID4VP; +import io.mosip.openID4VP.authorizationRequest.AuthorizationPresentationExchangeRequest; +import io.mosip.openID4VP.authorizationRequest.presentationDefinition.*; +import org.springframework.util.ResourceUtils; + +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Instant; +import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor; + +public class TestUtilities { + + public static CredentialsSupportedResponse getCredentialSupportedResponse(String credentialSupportedName) { + LogoDTO logo = new LogoDTO(); + logo.setUrl("https://logo"); + logo.setAlt_text("logo-url"); + CredentialSupportedDisplayResponse credentialSupportedDisplay = new CredentialSupportedDisplayResponse(); + credentialSupportedDisplay.setBackgroundImage(new BackgroundImageDTO("https://bgimage")); + credentialSupportedDisplay.setLogo(logo); + credentialSupportedDisplay.setName(credentialSupportedName); + credentialSupportedDisplay.setLocale("en"); + credentialSupportedDisplay.setTextColor("#FFFFFF"); + credentialSupportedDisplay.setBackgroundColor("#B34622"); + + CredentialIssuerDisplayResponse credentialIssuerDisplayResponseForName = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponseForName.setName("Given Name"); + credentialIssuerDisplayResponseForName.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDtoForName = new CredentialDisplayResponseDto(); + credentialDisplayResponseDtoForName.setDisplay(List.of(credentialIssuerDisplayResponseForName)); + + CredentialIssuerDisplayResponse credentialIssuerDisplayResponseForEmail = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponseForEmail.setName("Given Email"); + credentialIssuerDisplayResponseForEmail.setLocale("pt"); + CredentialDisplayResponseDto credentialDisplayResponseDtoForEmail = new CredentialDisplayResponseDto(); + credentialDisplayResponseDtoForEmail.setDisplay(List.of(credentialIssuerDisplayResponseForEmail)); + + CredentialIssuerDisplayResponse credentialIssuerDisplayResponseForUIN = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponseForUIN.setName("UIN"); + credentialIssuerDisplayResponseForUIN.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDtoForUIN = new CredentialDisplayResponseDto(); + credentialDisplayResponseDtoForUIN.setDisplay(List.of(credentialIssuerDisplayResponseForUIN)); + + CredentialIssuerDisplayResponse credentialIssuerDisplayResponseForDOB = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponseForDOB.setName("dob"); + credentialIssuerDisplayResponseForDOB.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDtoForDOB = new CredentialDisplayResponseDto(); + credentialDisplayResponseDtoForDOB.setDisplay(List.of(credentialIssuerDisplayResponseForDOB)); + + + CredentialDefinitionResponseDto credentialDefinitionResponseDto = new CredentialDefinitionResponseDto(); + credentialDefinitionResponseDto.setType(List.of("VerifiableCredential", credentialSupportedName)); + credentialDefinitionResponseDto.setCredentialSubject(Map.of( + "name", createCredentialDisplayResponse("Given Name", "en"), + "email", createCredentialDisplayResponse("Given Email", "pt"), + "uin", createCredentialDisplayResponse("UIN", "en"), + "dob", createCredentialDisplayResponse("dob", "en"))); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialsSupportedResponse.setFormat("ldp_vc"); + credentialsSupportedResponse.setScope(credentialSupportedName + "_vc_ldp"); + credentialsSupportedResponse.setDisplay(Collections.singletonList(credentialSupportedDisplay)); + HashMap proofTypesSupportedHashMap = new HashMap<>(); + ProofTypesSupported proofTypesSupported = new ProofTypesSupported(); + proofTypesSupported.setProofSigningAlgValuesSupported(List.of("RS256")); + proofTypesSupportedHashMap.put("jwt", proofTypesSupported); + credentialsSupportedResponse.setProofTypesSupported(proofTypesSupportedHashMap); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinitionResponseDto); + return credentialsSupportedResponse; + } + + private static CredentialDisplayResponseDto createCredentialDisplayResponse(String name, String locale) { + CredentialIssuerDisplayResponse issuerDisplayResponse = new CredentialIssuerDisplayResponse(); + issuerDisplayResponse.setName(name); + issuerDisplayResponse.setLocale(locale); + + CredentialDisplayResponseDto displayResponseDto = new CredentialDisplayResponseDto(); + displayResponseDto.setDisplay(List.of(issuerDisplayResponse)); + + return displayResponseDto; + } + + public static CredentialsSupportedResponse getCredentialSupportedResponse(String credentialSupportedName, String format) { + LogoDTO logo = new LogoDTO(); + logo.setUrl("https://logo"); + logo.setAlt_text("logo-url"); + CredentialSupportedDisplayResponse credentialSupportedDisplay = new CredentialSupportedDisplayResponse(); + credentialSupportedDisplay.setLogo(logo); + credentialSupportedDisplay.setName(credentialSupportedName); + credentialSupportedDisplay.setLocale("en"); + credentialSupportedDisplay.setTextColor("#FFFFFF"); + credentialSupportedDisplay.setBackgroundColor("#B34622"); + credentialSupportedDisplay.setBackgroundImage(new BackgroundImageDTO("https://bgimage")); + CredentialIssuerDisplayResponse credentialIssuerDisplayResponse = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponse.setName("Given Name"); + credentialIssuerDisplayResponse.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDto = new CredentialDisplayResponseDto(); + credentialDisplayResponseDto.setDisplay(Collections.singletonList(credentialIssuerDisplayResponse)); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialsSupportedResponse.setFormat(format); + credentialsSupportedResponse.setScope(credentialSupportedName + "_vc_" + format); + credentialsSupportedResponse.setDisplay(Collections.singletonList(credentialSupportedDisplay)); + HashMap proofTypesSupportedHashMap = new HashMap<>(); + ProofTypesSupported proofTypesSupported = new ProofTypesSupported(); + proofTypesSupported.setProofSigningAlgValuesSupported(List.of("RS256")); + proofTypesSupportedHashMap.put("jwt", proofTypesSupported); + credentialsSupportedResponse.setProofTypesSupported(proofTypesSupportedHashMap); + credentialsSupportedResponse.setDoctype("org.iso.18018"); + credentialsSupportedResponse.setClaims(Map.of("org.iso.18018", Map.of("given_name", Map.of("display", List.of(Map.of("name", "Given Name", "locale", "en")))))); + return credentialsSupportedResponse; + } + + public static CredentialIssuerWellKnownResponse getCredentialIssuerWellKnownResponseDto(String issuerName, Map credentialsSupportedResponses) { + return new CredentialIssuerWellKnownResponse( + "https://dev/" + issuerName, + List.of("https://auth-server.env.net"), + "https://dev/issuance/credential", + credentialsSupportedResponses); + } + + public static CredentialIssuerConfiguration getCredentialIssuerConfigurationResponseDto(String issuerName, String credentialType, List nullFields) { + AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse = getAuthServerWellknownResponseDto(nullFields); + Map credentialsSupportedResponses = Map.of(credentialType, getCredentialSupportedResponse(credentialType)); + return new CredentialIssuerConfiguration("https://dev/" + issuerName, List.of("https://auth-server.env.net"), "https://dev/issuance/credential", credentialsSupportedResponses, authorizationServerWellKnownResponse); + } + + public static AuthorizationServerWellKnownResponse getAuthServerWellknownResponseDto(List nullFields) { + AuthorizationServerWellKnownResponse authorizationServerWellKnownResponse = new AuthorizationServerWellKnownResponse(); + if (!nullFields.contains("authorization_endpoint")) { + authorizationServerWellKnownResponse.setAuthorizationEndpoint("https://dev/authorize"); + } + if (!nullFields.contains("token_endpoint")) { + authorizationServerWellKnownResponse.setTokenEndpoint("https://dev/token"); + } + if (!nullFields.contains("grant_types_supported")) { + authorizationServerWellKnownResponse.setGrantTypesSupported(List.of("authorization_code")); + } + return authorizationServerWellKnownResponse; + } + + public static IssuerDTO getIssuerDTO(String issuerName) { + LogoDTO logo = new LogoDTO(); + logo.setUrl("/logo"); + logo.setAlt_text("logo-url"); + DisplayDTO display = new DisplayDTO(); + display.setName(issuerName); + display.setTitle("Download via " + issuerName); + display.setDescription(issuerName + " description"); + display.setLanguage("en"); + display.setLogo(logo); + IssuerDTO issuer = new IssuerDTO(); + issuer.setIssuer_id(issuerName + "id"); + issuer.setCredential_issuer_host("https://issuer.env.net"); + issuer.setDisplay(Collections.singletonList(display)); + issuer.setClient_id("123"); + + return issuer; + } + + /** + * Builds an IssuerResponseDTO for tests (e.g. IssuersController v1 which returns IssuerResponseDTO). + */ + public static IssuerV2DTO getIssuerResponseDTO(String issuerName) { + IssuerDTO issuer = getIssuerConfigDTO(issuerName); + + IssuerV2DTO issuerV2DTO= new IssuerV2DTO(); + + issuerV2DTO.setIssuerId(issuer.getIssuer_id()); + issuerV2DTO.setProtocol(issuer.getProtocol()); + issuerV2DTO.setDisplay(issuer.getDisplay()); + issuerV2DTO.setClientId(issuer.getClient_id()); + issuerV2DTO.setTokenEndpoint(issuer.getToken_endpoint()); + issuerV2DTO.setClientAlias(issuer.getClient_alias()); + issuerV2DTO.setQrCodeType(issuer.getQr_code_type()); + issuerV2DTO.setEnabled(issuer.getEnabled()); + issuerV2DTO.setCredentialIssuerHost(issuer.getCredential_issuer_host()); + + return issuerV2DTO; + } + + public static IssuerDTO getIssuerConfigDTO(String issuerName) { + LogoDTO logo = new LogoDTO(); + logo.setUrl("https://logo"); + logo.setAlt_text("logo-url"); + DisplayDTO display = new DisplayDTO(); + display.setName(issuerName); + display.setTitle("Download via " + issuerName); + display.setDescription(issuerName + " description"); + display.setLanguage("en"); + display.setLogo(logo); + IssuerDTO issuer = new IssuerDTO(); + issuer.setIssuer_id(issuerName + "id"); + issuer.setDisplay(Collections.singletonList(display)); + issuer.setClient_id("123"); + issuer.setClient_alias("test-client-alias"); + issuer.setEnabled("true"); + issuer.setProtocol("OpenId4VCI"); + issuer.setCredential_issuer_host("https://issuer.env.net"); + issuer.setToken_endpoint("https://dev/" + issuerName + "id"); + return issuer; + } + + public static IssuerDTO getIssuerConfigDTOWithInvalidFieldValues(String issuerName, boolean emptyValues, boolean invalidUrls) { + LogoDTO logo = new LogoDTO(); + logo.setUrl(emptyValues ? "/logo" : "https://logo"); + logo.setAlt_text("logo-url"); + + DisplayDTO display = new DisplayDTO(); + display.setName(emptyValues ? "" : issuerName); + display.setTitle(emptyValues ? "" : "Download via " + issuerName); + display.setDescription(emptyValues ? "" : issuerName + " description"); + display.setLanguage(emptyValues ? "" : "en"); + display.setLogo(logo); + + IssuerDTO issuer = new IssuerDTO(); + issuer.setIssuer_id(emptyValues ? "" : issuerName + "id"); + issuer.setDisplay(Collections.singletonList(display)); + issuer.setClient_id(emptyValues ? "" : "123"); + issuer.setClient_alias(emptyValues ? "" : "test-client-alias"); + issuer.setEnabled(emptyValues ? "" : "true"); + issuer.setProtocol(emptyValues ? "" : "OpenId4VCI"); + + // Handle valid and invalid URLs + issuer.setCredential_issuer_host(emptyValues ? "" : (invalidUrls ? "https//issuer.env.net" : "https://issuer.env.net")); + issuer.setToken_endpoint(emptyValues ? "" : (invalidUrls ? "h://dev/token" : "https://dev/token")); + + return issuer; + } + + public static String getExpectedWellKnownJson() throws IOException { + return new String(Files.readAllBytes(ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "responses/expectedWellknown.json").toPath())).trim(); + } + + public static String getExpectedIssuersConfigJson() throws IOException { + return new String(Files.readAllBytes(ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "responses/expectedIssuerConfig.json").toPath())).trim(); + } + + public static ByteArrayInputStream generatePdfFromHTML() { + String htmlContent = "

PDF

"; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PdfWriter pdfwriter = new PdfWriter(outputStream); + DefaultFontProvider defaultFont = new DefaultFontProvider(true, false, false); + ConverterProperties converterProperties = new ConverterProperties(); + converterProperties.setFontProvider(defaultFont); + HtmlConverter.convertToPdf(htmlContent, pdfwriter, converterProperties); + + return new ByteArrayInputStream(outputStream.toByteArray()); + } + + public static String extractTextFromPdf(InputStream pdfStream) throws IOException { + PdfDocument pdfDocument = new PdfDocument(new PdfReader(pdfStream)); + StringBuilder text = new StringBuilder(); + + for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++) { + text.append(PdfTextExtractor.getTextFromPage(pdfDocument.getPage(i))); + } + + pdfDocument.close(); + return text.toString().trim(); + } + + public static PresentationRequestDTO getPresentationRequestDTO() { + return PresentationRequestDTO.builder() + .presentationDefinition(getPresentationDefinitionDTO()) + .clientId("test_client_id") + .resource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test") + .responseType("test_response_type") + .redirectUri("test_redirect_uri").build(); + } + + public static PresentationRequestDTO getPresentationRequestDTOWithEmptyRedirectURI() { + return PresentationRequestDTO.builder() + .presentationDefinition(getPresentationDefinitionDTO()) + .clientId("test_client_id") + .resource("http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/test") + .responseType("test_response_type") + .redirectUri("").build(); + } + + public static VCCredentialProperties getVCCredentialPropertiesDTO(String type) { + + ArrayList contextList = new ArrayList<>(); + contextList.add("context-1"); + contextList.add("context-2"); + + List typeList = new ArrayList<>(); + typeList.add("VerifiableCredential"); + typeList.add("VCTypeCredential"); + + Map credentialSubject = new HashMap<>(); + credentialSubject.put("name", List.of(Map.of("value", "full name", "language", "en"))); + credentialSubject.put("email", "@gmail.com"); + credentialSubject.put("uin", "2144641120"); + credentialSubject.put("dob", ""); + + VCCredentialResponseProof vcCredentialResponseProof = VCCredentialResponseProof.builder() + .type(type) + .proofPurpose("test-proofPurpose") + .proofValue("test-proofValue") + .jws("test-jws") + .verificationMethod("test-verificationMethod").build(); + + return VCCredentialProperties.builder() + .id("test-id") + .issuer("test-issuer") + .issuanceDate("test-issuanceDate") + .expirationDate("test-expirationDate") + .context(contextList) + .type(typeList) + .proof(vcCredentialResponseProof) + .credentialSubject(credentialSubject).build(); + } + + public static Draft13VCCredentialRequest getVCCredentialRequestDTO() { + CredentialsSupportedResponse credentialsSupportedResponse = getCredentialSupportedResponse("CredentialType1"); + return Draft13VCCredentialRequest.builder() + .format(credentialsSupportedResponse.getFormat()) + .proof(VCCredentialRequestProof.builder() + .proofType(credentialsSupportedResponse.getProofTypesSupported().keySet().stream().findFirst().get()) + .jwt("jwt") + .build()) + .credentialDefinition(VCCredentialDefinition.builder() + .type(credentialsSupportedResponse.getCredentialDefinition().getType()) + .context(List.of("https://www.w3.org/2018/credentials/v1")) + .build()) + .build(); + } + + public static VCCredentialResponse getVCCredentialResponseDTO(String type) { + return VCCredentialResponse.builder() + .credential(getVCCredentialPropertiesDTO(type)) + .format("ldp_vc").build(); + } + + public static VerifiableCredentialResponse getVerifiableCredentialResponseDTO(String type) { + return VerifiableCredentialResponse.builder() + .credential(getVCCredentialPropertiesDTO(type)).build(); + } + + public static DataShareResponseDto getDataShareResponseDTO(String errorCode) { + return DataShareResponseDto.builder() + .dataShare(new DataShare()) + .errors(List.of(new ErrorDTO(errorCode.equals("") ? "Expired!" : errorCode, "Download is failed as credential is expired"))) + .build(); + } + + + public static io.mosip.mimoto.dto.idp.TokenResponseDTO getTokenResponseDTO() { + return io.mosip.mimoto.dto.idp.TokenResponseDTO.builder() + .id_token("test-id-token") + .access_token("test-accesstoken") + .expires_in(12345) + .scope("test-scope") + .token_type("test-token-type") + .c_nonce("test-cnonce") + .build(); + } + + public static PresentationDefinitionDTO getPresentationDefinitionDTO() { + FilterDTO filterDTO = FilterDTO.builder().type("String").pattern("test-credential").build(); + FieldDTO fieldDTO = FieldDTO.builder().path(new String[]{"$.type"}).filter(filterDTO).build(); + ConstraintsDTO constraintsDTO = ConstraintsDTO.builder().fields(new FieldDTO[]{fieldDTO}).build(); + Map> proofTypes = Map.of("proofTypes", Collections.singletonList("Ed25519Signature2020")); + Map>> format = Map.of("ldpVc", proofTypes); + InputDescriptorDTO inputDescriptorDTO = InputDescriptorDTO.builder().id("test-input-id").format(format).constraints(constraintsDTO).build(); + + return PresentationDefinitionDTO.builder() + .inputDescriptors(Collections.singletonList(inputDescriptorDTO)) + .id("test-id").build(); + } + + public static VerifiablePresentationDTO getVerifiablePresentationDTO() { + List contextList = new ArrayList<>(); + contextList.add("https://www.w3.org/2018/credentials/v1"); + + List typeList = new ArrayList<>(); + typeList.add("VerifiablePresentation"); + + return VerifiablePresentationDTO.builder() + .verifiableCredential(Collections.singletonList(getVCCredentialPropertiesDTO("Ed25519Signature2020"))) + .context(contextList) + .type(typeList).build(); + } + + public static VerifiersDTO getTrustedVerifiers() { + VerifierDTO verifierDTO = VerifierDTO.builder() + .clientId("test-clientId") + .redirectUris(Collections.singletonList("https://test-redirectUri")) + .responseUris(Collections.singletonList("https://test-responseUri")).build(); + + return VerifiersDTO.builder() + .verifiers(Collections.singletonList(verifierDTO)).build(); + } + + public static String getObjectAsString(Object object) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(object); + } + + public static DataShareResponseWrapperDTO getDataShareResponseWrapperDTO() { + ErrorDTO errorDTO = ErrorDTO.builder().errorCode("test-errorCode").errorMessage("test-errorMessage").build(); + DataShareResponseDTO dataShareResponseDTO = DataShareResponseDTO.builder() + .url("https://test-url") + .validForInMinutes(1) + .transactionsAllowed(1) + .policyId("static-policyid") + .subscriberId("static-subscriberId").build(); + + return DataShareResponseWrapperDTO.builder() + .id("test-id") + .version("test-version") + .responsetime("test-responsetime") + .dataShare(dataShareResponseDTO) + .errors(Collections.singletonList(errorDTO)).build(); + } + + public static VerifiableCredential getVerifiableCredential(String id, String walletId, String credential, String issuerId, String credentialType) { + VerifiableCredential verifiableCredential = new VerifiableCredential(); + verifiableCredential.setId(id); + verifiableCredential.setWalletId(walletId); + verifiableCredential.setCredential(credential); + + CredentialMetadata metadata = new CredentialMetadata(); + metadata.setIssuerId(issuerId); + metadata.setCredentialType(credentialType); + verifiableCredential.setCredentialMetadata(metadata); + return verifiableCredential; + } + + public static VerifiableCredentialResponseDTO getVerifiableCredentialResponseDTO(String issuerName, String issuerLogo, String credentialType, String credentialTypeLogo, String credentialId) { + return VerifiableCredentialResponseDTO.builder() + .issuerDisplayName(issuerName) + .issuerLogo(issuerLogo) + .credentialTypeDisplayName(credentialType) + .credentialTypeLogo(credentialTypeLogo) + .credentialId(credentialId) + .build(); + } + + public static String createRequestBody(Object request) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(request); + } + + public static Wallet createWallet(String userId, String walletKey, WalletMetadata walletMetadata) { + Wallet wallet = new Wallet(); + wallet.setId(UUID.randomUUID().toString()); + wallet.setUserId(userId); + wallet.setWalletKey(walletKey); + wallet.setWalletMetadata(walletMetadata); + wallet.setCreatedAt(Instant.now()); + wallet.setUpdatedAt(Instant.now()); + return wallet; + } + + public static WalletMetadata createWalletMetadata(String name, PasscodeControl passcodeControl, WalletLockStatus walletStatus) { + WalletMetadata walletMetadata = new WalletMetadata(); + walletMetadata.setName(name); + walletMetadata.setEncryptionAlgo("AES"); + walletMetadata.setEncryptionType("symmetric"); + walletMetadata.setPasscodeControl(passcodeControl); + walletMetadata.setLockStatus(walletStatus); + return walletMetadata; + } + + public static PasscodeControl createPasscodeControl(int failedAttemptCount, int currentCycleCount, Long retryBlockedUntil) { + PasscodeControl passcodeControl = new PasscodeControl(); + passcodeControl.setFailedAttemptCount(failedAttemptCount); + passcodeControl.setCurrentCycleCount(currentCycleCount); + passcodeControl.setRetryBlockedUntil(retryBlockedUntil); + return passcodeControl; + } + + public static VPResponseDTO getVerifiablePresentationResponseDTO(String clientId, String clientName, String clientLogo, boolean isTrusted, boolean isPreRegisteredWithWallet, String redirectUri, OpenID4VP openID4VP, Instant time) { + VerifiablePresentationVerifierDTO presentationVerifierDTO = new VerifiablePresentationVerifierDTO( + clientId, + clientName, + clientLogo, + isTrusted, + isPreRegisteredWithWallet, + redirectUri + ); + VerifiablePresentationSessionData presentationSessionData = new VerifiablePresentationSessionData("123e4567-e89b-12d3-a456-426614174000", "authorizationRequest", time, true, null, false); + + VPResponseDTO presentationResponseDTO = new VPResponseDTO( + presentationSessionData.getPresentationId(), presentationVerifierDTO, false); + + return presentationResponseDTO; + } + + public static PresentationDefinition getPresentationDefinition() { + Map> innerMap = new HashMap<>(); + innerMap.put("jwt_vc", List.of("ES256", "EdDSA")); + + Map>> vpFormats = new HashMap<>(); + vpFormats.put("ldp_vc", innerMap); + + + Fields emailField = new Fields( + List.of("$.credentialSubject.email"), + null, null, null, + new Filter("String", "@gmail.com"), + null + ); + + Constraints constraints = new Constraints(List.of(emailField), null); + + InputDescriptor inputDescriptor = new InputDescriptor( + "id123", + "", + "", + vpFormats, + constraints + ); + + return new PresentationDefinition( + "pd123", + List.of(inputDescriptor), + null, + "requesting your digital ID for the purpose of Self-Authentication", + vpFormats + ); + } + + + public static AuthorizationPresentationExchangeRequest getPresentationAuthorizationRequest(String clientId, String responseUri) { + return new AuthorizationPresentationExchangeRequest( + clientId, + "vp_token", + "direct_post", + "https://test-responseUri", + responseUri, + "mock-nonce", + null, + "mock-state", + getPresentationDefinition(), + null + ); + } +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/util/UrlParameterUtilsTest.java b/src/test/java/io/mosip/mimoto/util/UrlParameterUtilsTest.java new file mode 100644 index 000000000..f2816210f --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/UrlParameterUtilsTest.java @@ -0,0 +1,175 @@ +package io.mosip.mimoto.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class UrlParameterUtilsTest { + + private static final String VALID_CLIENT_ID = "https://injiverify.collab.mosip.net"; + private static final String ENCODED_CLIENT_ID = "https%3A%2F%2Finjiverify.collab.mosip.net"; + private static final String VALID_RESPONSE_URI = "https://example.com/callback"; + private static final String ENCODED_RESPONSE_URI = "https%3A%2F%2Fexample.com%2Fcallback"; + + @Test + public void testExtractQueryParameterWithValidClientId() throws Exception { + String url = "https://example.com?client_id=" + ENCODED_CLIENT_ID + "&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertEquals(VALID_CLIENT_ID, result); + } + + @Test + public void testExtractQueryParameterWithNoQueryParameters() throws Exception { + String url = "https://example.com"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithNoParameter() throws Exception { + String url = "https://example.com?other_param=value&another_param=value2"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithMalformedParameter() throws Exception { + String url = "https://example.com?client_id&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithNullInput() throws Exception { + String url = null; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithEmptyValue() throws Exception { + String url = "https://example.com?client_id=&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertEquals("", result); + } + + @Test + public void testExtractQueryParameterWithValidResponseUri() throws Exception { + String url = "https://example.com?response_uri=" + ENCODED_RESPONSE_URI + "&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertEquals(VALID_RESPONSE_URI, result); + } + + @Test + public void testExtractQueryParameterWithNoResponseUriParameter() throws Exception { + String url = "https://example.com?other_param=value&another_param=value2"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithMalformedResponseUri() throws Exception { + String url = "https://example.com?response_uri&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertNull(result); + } + + @Test + public void testExtractQueryParameterWithEmptyResponseUri() throws Exception { + String url = "https://example.com?response_uri=&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertEquals("", result); + } + + @Test + public void testExtractQueryParameterWithCommaValue() throws Exception { + String url = "https://example.com?response_uri=,"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertEquals(",", result); + } + + @Test + public void testExtractQueryParameterWithMultipleCommas() throws Exception { + String url = "https://example.com?response_uri=uri1,uri2,uri3"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertEquals("uri1,uri2,uri3", result); + } + + @Test + public void testExtractQueryParameterWithSpecialCharacters() throws Exception { + String specialClientId = "client-id_with.special+chars"; + String encodedClientId = "client-id_with.special%2Bchars"; + String url = "https://example.com?client_id=" + encodedClientId + "&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertEquals(specialClientId, result); + } + + @Test + public void testExtractQueryParameterWithSpecialCharactersInUri() throws Exception { + String specialUri = "https://example.com/callback?param=value&other=test"; + String encodedUri = "https%3A%2F%2Fexample.com%2Fcallback%3Fparam%3Dvalue%26other%3Dtest"; + String url = "https://example.com?response_uri=" + encodedUri + "&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "response_uri"); + + assertEquals(specialUri, result); + } + + @Test + public void testExtractQueryParameterWithWhitespace() throws Exception { + String url = "https://example.com?client_id=%20&other_param=value"; + + String result = UrlParameterUtils.extractQueryParameter(url, "client_id"); + + assertEquals(" ", result); + } + + @Test + public void testExtractQueryParameterWithAnyParameterName() throws Exception { + String url = "https://example.com?custom_param=value123&other_param=value2"; + + String result = UrlParameterUtils.extractQueryParameter(url, "custom_param"); + + assertEquals("value123", result); + } + + @Test + public void testExtractQueryParameterWithMultipleParameters() throws Exception { + String url = "https://example.com?param1=value1¶m2=value2¶m3=value3"; + + String result1 = UrlParameterUtils.extractQueryParameter(url, "param1"); + String result2 = UrlParameterUtils.extractQueryParameter(url, "param2"); + String result3 = UrlParameterUtils.extractQueryParameter(url, "param3"); + + assertEquals("value1", result1); + assertEquals("value2", result2); + assertEquals("value3", result3); + } + +} diff --git a/src/test/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidatorTest.java b/src/test/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidatorTest.java new file mode 100644 index 000000000..e690dc868 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/V1CredentialIssuerWellknownResponseValidatorTest.java @@ -0,0 +1,99 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; + +import static io.mosip.mimoto.util.TestUtilities.getCredentialIssuerWellKnownResponseDto; +import static io.mosip.mimoto.util.TestUtilities.getCredentialSupportedResponse; +import static org.junit.jupiter.api.Assertions.*; + +class V1CredentialIssuerWellknownResponseValidatorTest { + + private Validator validator; + private final V1CredentialIssuerWellknownResponseValidator v1Validator = new V1CredentialIssuerWellknownResponseValidator(); + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void shouldPassValidationForV1LdpVcWithDisplayAndClaims() { + CredentialsSupportedResponse config = getCredentialSupportedResponse("CredentialType1"); + config.setCredentialDefinition(null); + config.setClaims(Map.of("given_name", Map.of("display", java.util.List.of(Map.of("name", "Given Name", "locale", "en"))))); + + CredentialIssuerWellKnownResponse response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", config)); + + assertDoesNotThrow(() -> v1Validator.validate(response, validator)); + } + + @Test + void shouldFailValidationForV1LdpVcWithoutDisplay() { + CredentialsSupportedResponse config = getCredentialSupportedResponse("CredentialType1"); + config.setCredentialDefinition(null); + config.setDisplay(null); + config.setClaims(Map.of("given_name", Map.of())); + + CredentialIssuerWellKnownResponse response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", config)); + + InvalidWellknownResponseException exception = assertThrows(InvalidWellknownResponseException.class, + () -> v1Validator.validate(response, validator)); + assertTrue(exception.getMessage().contains("Mandatory field 'display' missing for V1 ldp_vc")); + } + + @Test + void shouldFailValidationForV1LdpVcWithEmptyDisplay() { + CredentialsSupportedResponse config = getCredentialSupportedResponse("CredentialType1"); + config.setCredentialDefinition(null); + config.setDisplay(Collections.emptyList()); + config.setClaims(Map.of("given_name", Map.of())); + + CredentialIssuerWellKnownResponse response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", config)); + + InvalidWellknownResponseException exception = assertThrows(InvalidWellknownResponseException.class, + () -> v1Validator.validate(response, validator)); + assertTrue(exception.getMessage().contains("Mandatory field 'display' missing for V1 ldp_vc")); + } + + @Test + void shouldFailValidationForV1LdpVcWithoutClaims() { + CredentialsSupportedResponse config = getCredentialSupportedResponse("CredentialType1"); + config.setCredentialDefinition(null); + config.setClaims(null); + + CredentialIssuerWellKnownResponse response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", config)); + + InvalidWellknownResponseException exception = assertThrows(InvalidWellknownResponseException.class, + () -> v1Validator.validate(response, validator)); + assertTrue(exception.getMessage().contains("Mandatory field 'claims' missing for V1 ldp_vc")); + } + + @Test + void shouldFailValidationForV1LdpVcWithEmptyClaims() { + CredentialsSupportedResponse config = getCredentialSupportedResponse("CredentialType1"); + config.setCredentialDefinition(null); + config.setClaims(Map.of()); + + CredentialIssuerWellKnownResponse response = getCredentialIssuerWellKnownResponseDto("Issuer1", + Map.of("CredentialType1", config)); + + InvalidWellknownResponseException exception = assertThrows(InvalidWellknownResponseException.class, + () -> v1Validator.validate(response, validator)); + assertTrue(exception.getMessage().contains("Mandatory field 'claims' missing for V1 ldp_vc")); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/VCSpecVersionDetectorTest.java b/src/test/java/io/mosip/mimoto/util/VCSpecVersionDetectorTest.java new file mode 100644 index 000000000..82b033cb4 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/VCSpecVersionDetectorTest.java @@ -0,0 +1,169 @@ +package io.mosip.mimoto.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VCSpecVersionDetectorTest { + + private VCSpecVersionDetector detector; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + detector = new VCSpecVersionDetector(); + objectMapper = new ObjectMapper(); + } + + @Test + void shouldDetectV1WhenNonceEndpointIsPresent() throws Exception { + JsonNode node = objectMapper.readTree(""" + {"nonce_endpoint": "https://example.com/nonce"} + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldFallThroughWhenNonceEndpointIsBlank() throws Exception { + ObjectNode root = objectMapper.createObjectNode(); + root.put("nonce_endpoint", ""); + root.putObject("credential_configurations_supported") + .putObject("cred1").put("display", "something"); + + assertEquals(VCSpecificationVersion.DRAFT_13, detector.detectVersion(root)); + } + + @Test + void shouldDetectV1WhenCredentialMetadataExistsInConfiguration() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": { + "credential_metadata": {"display": []} + } + } + } + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldDetectDraft13WhenDisplayExistsInConfiguration() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": { + "display": [{"name": "Test"}] + } + } + } + """); + assertEquals(VCSpecificationVersion.DRAFT_13, detector.detectVersion(node)); + } + + @Test + void shouldDefaultToV1WhenNoIndicatorsPresent() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": { + "format": "ldp_vc" + } + } + } + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldDefaultToV1WhenConfigurationsSupportedIsNull() throws Exception { + JsonNode node = objectMapper.readTree(""" + {"credential_issuer": "https://example.com"} + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldDefaultToV1WhenConfigurationsSupportedIsNotAnObject() throws Exception { + JsonNode node = objectMapper.readTree(""" + {"credential_configurations_supported": "not_an_object"} + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldDefaultToV1ForEmptyResponse() throws Exception { + JsonNode node = objectMapper.readTree("{}"); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldPrioritizeNonceEndpointOverCredentialMetadata() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "nonce_endpoint": "https://example.com/nonce", + "credential_configurations_supported": { + "cred1": { + "credential_metadata": {"display": []}, + "display": [{"name": "Test"}] + } + } + } + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldPrioritizeCredentialMetadataOverDisplay() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": { + "credential_metadata": {"display": []}, + "display": [{"name": "Test"}] + } + } + } + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldCheckAllConfigurationsForCredentialMetadata() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": {"format": "ldp_vc"}, + "cred2": {"credential_metadata": {"display": []}} + } + } + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } + + @Test + void shouldCheckAllConfigurationsForDisplay() throws Exception { + JsonNode node = objectMapper.readTree(""" + { + "credential_configurations_supported": { + "cred1": {"format": "ldp_vc"}, + "cred2": {"display": [{"name": "Test"}]} + } + } + """); + assertEquals(VCSpecificationVersion.DRAFT_13, detector.detectVersion(node)); + } + + @Test + void shouldDefaultToV1WhenConfigurationsSupportedIsEmptyObject() throws Exception { + JsonNode node = objectMapper.readTree(""" + {"credential_configurations_supported": {}} + """); + assertEquals(VCSpecificationVersion.V1, detector.detectVersion(node)); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/WalletLockStatusUtilsTest.java b/src/test/java/io/mosip/mimoto/util/WalletLockStatusUtilsTest.java new file mode 100644 index 000000000..a6d37ab6f --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/WalletLockStatusUtilsTest.java @@ -0,0 +1,69 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.dto.ErrorDTO; +import io.mosip.mimoto.exception.ErrorConstants; +import io.mosip.mimoto.model.PasscodeControl; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.model.WalletLockStatus; +import io.mosip.mimoto.model.WalletMetadata; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@RunWith(SpringRunner.class) +public class WalletLockStatusUtilsTest { + + private Wallet wallet; + + @Before + public void setUp() { + String userId = UUID.randomUUID().toString(); + PasscodeControl passcodeControl = TestUtilities.createPasscodeControl(0, 0, null); + WalletMetadata walletMetadata = TestUtilities.createWalletMetadata("Test Wallet", passcodeControl, null); + wallet = TestUtilities.createWallet(userId, "encryptedWalletKey", walletMetadata); + } + + @Test + public void testGetWalletStatusShouldReturnWalletStatus() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + + WalletLockStatus status = WalletLockStatusUtils.getWalletLockStatus(wallet); + + assertEquals(WalletLockStatus.TEMPORARILY_LOCKED, status); + } + + @Test + public void testGetErrorBasedOnWalletLockStatusShouldReturnErrorObjForPermanentlyLockedWallet() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.PERMANENTLY_LOCKED); + + ErrorDTO errorDTO = WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet); + + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorCode(), errorDTO.getErrorCode()); + assertEquals(ErrorConstants.WALLET_PERMANENTLY_LOCKED.getErrorMessage(), errorDTO.getErrorMessage()); + } + + @Test + public void testGetErrorBasedOnWalletLockStatusShouldReturnErrorObjForTemporarilyLockedWallet() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.TEMPORARILY_LOCKED); + + ErrorDTO errorDTO = WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet); + + assertEquals(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorCode(), errorDTO.getErrorCode()); + assertEquals(ErrorConstants.WALLET_TEMPORARILY_LOCKED.getErrorMessage(), errorDTO.getErrorMessage()); + } + + @Test + public void testGetErrorBasedOnWalletLockStatusShouldReturnErrorObjWhenOnlyOneAttemptLeftBeforePermanentLockout() { + wallet.getWalletMetadata().setLockStatus(WalletLockStatus.LAST_ATTEMPT_BEFORE_LOCKOUT); + + ErrorDTO errorDTO = WalletLockStatusUtils.getErrorBasedOnWalletLockStatus(wallet); + + assertEquals(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorCode(), errorDTO.getErrorCode()); + assertEquals(ErrorConstants.WALLET_LAST_ATTEMPT_BEFORE_LOCKOUT.getErrorMessage(), errorDTO.getErrorMessage()); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/WalletUtilTest.java b/src/test/java/io/mosip/mimoto/util/WalletUtilTest.java new file mode 100644 index 000000000..e94c0cd06 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/WalletUtilTest.java @@ -0,0 +1,179 @@ +package io.mosip.mimoto.util; + +import io.mosip.kernel.cryptomanager.dto.CryptoWithPinRequestDto; +import io.mosip.kernel.cryptomanager.dto.CryptoWithPinResponseDto; +import io.mosip.kernel.cryptomanager.service.CryptomanagerService; +import io.mosip.mimoto.constant.SessionKeys; +import io.mosip.mimoto.model.Wallet; +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.repository.WalletRepository; +import io.mosip.mimoto.service.DataProtectionService; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.SecretKey; +import java.security.Security; +import java.util.Base64; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@Slf4j +class WalletUtilTest { + + @Mock + private WalletRepository walletRepository; + + @Mock + private DataProtectionService dataProtectionService; + + @Mock + private CryptomanagerService cryptomanagerService; + + @InjectMocks + private WalletUtil walletUtil; + + private String pin; + private String name; + private String userId; + private SecretKey encryptionKey; + private String encryptedPrivateKey; + private String encryptedWalletKey; + private String decryptedWalletKey; + private String encryptionAlgorithm; + private String encryptionType; + + @BeforeEach + void setUp() { + // Ensure BouncyCastle provider is registered (required for ES256K algorithm) + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + pin = "1234"; + name = "default"; + userId = UUID.randomUUID().toString(); + encryptionKey = SigningKeyUtil.generateEncryptionKey("AES", 256); + encryptedPrivateKey = "encryptedPrivateKey"; + encryptedWalletKey = "encryptedWalletKey"; + decryptedWalletKey = Base64.getEncoder().encodeToString(encryptionKey.getEncoded()); + encryptionAlgorithm = "AES"; + encryptionType = "encryptWithPin"; + } + + @Test + void shouldDecryptWalletKeySuccessfully() { + CryptoWithPinResponseDto responseDto = new CryptoWithPinResponseDto(); + responseDto.setData(decryptedWalletKey); + when(cryptomanagerService.decryptWithPin(any(CryptoWithPinRequestDto.class))).thenReturn(responseDto); + + String decrypted = walletUtil.decryptWalletKey(encryptedWalletKey, pin); + + assertEquals(decryptedWalletKey, decrypted); + } + + @Test + void shouldThrowErrorWhenDecryptionOfWalletKeyFails() { + when(cryptomanagerService.decryptWithPin(any(CryptoWithPinRequestDto.class))).thenThrow(new RuntimeException("Failed to decrypt with PIN")); + + InvalidRequestException ex = assertThrows(InvalidRequestException.class, + () -> walletUtil.decryptWalletKey(encryptedWalletKey, pin)); + assertEquals("invalid_pin", ex.getErrorCode()); + assertEquals("Failed to decrypt with PIN", ex.getCause().getMessage()); + } + + + @Test + void shouldCreateNewWalletSuccessfully() { + CryptoWithPinResponseDto responseDto = new CryptoWithPinResponseDto(); + responseDto.setData(encryptedWalletKey); + when(cryptomanagerService.encryptWithPin(any(CryptoWithPinRequestDto.class))).thenReturn(responseDto); + when(dataProtectionService.encryptWithAES(any(SecretKey.class), any(byte[].class))).thenReturn(encryptedPrivateKey); + + String walletId = walletUtil.saveWallet(userId, name, pin, encryptionKey, encryptionAlgorithm, encryptionType); + + assertNotNull(walletId); + } + + @Test + void shouldCreateEd25519WalletSuccessfully() { + CryptoWithPinResponseDto responseDto = new CryptoWithPinResponseDto(); + responseDto.setData(encryptedWalletKey); + when(cryptomanagerService.encryptWithPin(any(CryptoWithPinRequestDto.class))).thenReturn(responseDto); + when(dataProtectionService.encryptWithAES(any(SecretKey.class), any(byte[].class))).thenReturn(encryptedPrivateKey); + + String walletId = walletUtil.createWallet(userId, name, pin); + + assertNotNull(walletId); + } + + @Test + void shouldVerifyWalletObjectOnCreateNewWallet() { + CryptoWithPinResponseDto responseDto = new CryptoWithPinResponseDto(); + responseDto.setData(encryptedWalletKey); + when(cryptomanagerService.encryptWithPin(any(CryptoWithPinRequestDto.class))).thenReturn(responseDto); + when(dataProtectionService.encryptWithAES(any(SecretKey.class), any(byte[].class))).thenReturn(encryptedPrivateKey); + + String walletId = walletUtil.saveWallet(userId, name, pin, encryptionKey, encryptionAlgorithm, encryptionType); + + ArgumentCaptor walletCaptor = ArgumentCaptor.forClass(Wallet.class); + verify(walletRepository).save(walletCaptor.capture()); + + Wallet savedWallet = walletCaptor.getValue(); + + assertEquals(walletId, savedWallet.getId()); + assertEquals(userId, savedWallet.getUserId()); + assertEquals(name, savedWallet.getWalletMetadata().getName()); + assertEquals(encryptedWalletKey, savedWallet.getWalletKey()); + assertEquals(encryptionAlgorithm, savedWallet.getWalletMetadata().getEncryptionAlgo()); + assertEquals(encryptionType, savedWallet.getWalletMetadata().getEncryptionType()); + assertNotNull(savedWallet.getProofSigningKeys()); + assertEquals(4, savedWallet.getProofSigningKeys().size()); + } + + @Test + void testValidWalletId() { + HttpSession session = mock(HttpSession.class); + when(session.getAttribute(SessionKeys.WALLET_ID)).thenReturn("wallet-123"); + assertDoesNotThrow(() -> WalletUtil.validateWalletId(session, "wallet-123")); + } + + @Test + void testMissingWalletIdInSession() { + HttpSession session = mock(HttpSession.class); + when(session.getAttribute(SessionKeys.WALLET_ID)).thenReturn(null); + InvalidRequestException ex = assertThrows(InvalidRequestException.class, + () -> WalletUtil.validateWalletId(session, "wallet-123")); + assertEquals("wallet_locked", ex.getErrorCode()); + assertEquals("wallet_locked --> Wallet is locked", ex.getMessage()); + } + + @Test + void testMismatchedWalletId() { + HttpSession session = mock(HttpSession.class); + when(session.getAttribute(SessionKeys.WALLET_ID)).thenReturn("wallet-abc"); + InvalidRequestException ex = assertThrows(InvalidRequestException.class, + () -> WalletUtil.validateWalletId(session, "wallet-123")); + assertEquals("invalid_request", ex.getErrorCode()); + assertEquals("invalid_request --> Invalid Wallet ID. Session and request Wallet ID do not match", ex.getMessage()); + } + + @Test + void testNonStringWalletIdInSession() { + HttpSession session = mock(HttpSession.class); + when(session.getAttribute(SessionKeys.WALLET_ID)).thenReturn(12345); + assertDoesNotThrow(() -> WalletUtil.validateWalletId(session, "12345")); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/WalletValidatorTest.java b/src/test/java/io/mosip/mimoto/util/WalletValidatorTest.java new file mode 100644 index 000000000..4c884839a --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/WalletValidatorTest.java @@ -0,0 +1,67 @@ +package io.mosip.mimoto.util; + +import io.mosip.mimoto.exception.InvalidRequestException; +import io.mosip.mimoto.exception.UnauthorizedAccessException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = WalletValidator.class) +@TestPropertySource(locations = "classpath:application-test.properties") +public class WalletValidatorTest { + + @Autowired + private WalletValidator walletValidator; + + @Test + void testValidateWalletRequest_validData() { + walletValidator.validateUserId("user1"); + walletValidator.validateWalletName("wallet1"); + walletValidator.validateWalletPin("123456"); + } + + @Test + void testValidatePin_invalidPin() { + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> { + walletValidator.validateUserId("user1"); + walletValidator.validateWalletName("My Wallet"); + walletValidator.validateWalletPin("12"); + }); + + assertEquals("invalid_request --> PIN must be numeric with 6 digits", exception.getMessage()); + } + + @Test + void testValidateWalletName_invalidName() { + InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> { + walletValidator.validateUserId("user1"); + walletValidator.validateWalletName("My Wallet!@"); + walletValidator.validateWalletPin("123456"); + }); + + assertEquals("invalid_request --> Wallet name must be alphanumeric with allowed special characters", exception.getMessage()); + } + + @Test + void testValidateWalletName_validName() { + walletValidator.validateUserId("user1"); + walletValidator.validateWalletName("wallet1"); + walletValidator.validateWalletPin("123456"); + } + + @Test + void testValidateUserId_nullUserId() { + UnauthorizedAccessException exception = assertThrows(UnauthorizedAccessException.class, () -> { + walletValidator.validateUserId(null); + }); + + assertEquals("unauthorized --> User ID not found in session", exception.getMessage()); + } +} diff --git a/src/test/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtilTest.java b/src/test/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtilTest.java new file mode 100644 index 000000000..7ca32cfd2 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/BouncyCastleProviderUtilTest.java @@ -0,0 +1,38 @@ +package io.mosip.mimoto.util.factory; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Test; + +import java.security.Provider; +import java.security.Security; + +import static org.junit.Assert.*; + +/** + * Test cases for BouncyCastleProviderUtil. + */ +public class BouncyCastleProviderUtilTest { + + @Test + public void shouldReturnValidBouncyCastleProvider() { + Provider provider = BouncyCastleProviderUtil.getProvider(); + + assertNotNull("Provider should not be null", provider); + assertTrue("Provider should be instance of BouncyCastleProvider", provider instanceof BouncyCastleProvider); + assertEquals("Provider name should be BC", BouncyCastleProvider.PROVIDER_NAME, provider.getName()); + + Provider registeredProvider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME); + assertNotNull("BouncyCastle provider should be registered", registeredProvider); + assertEquals("Registered provider name should match", BouncyCastleProvider.PROVIDER_NAME, registeredProvider.getName()); + } + + @Test + public void shouldReturnSameInstanceOnMultipleCalls() { + Provider provider1 = BouncyCastleProviderUtil.getProvider(); + Provider provider2 = BouncyCastleProviderUtil.getProvider(); + + assertNotNull("Provider should not be null", provider1); + assertSame("Should return the same instance", provider1, provider2); + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandlerTest.java b/src/test/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandlerTest.java new file mode 100644 index 000000000..9144ddf20 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/ES256AlgorithmHandlerTest.java @@ -0,0 +1,78 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.Test; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Security; + +import static org.junit.Assert.*; + +/** + * Test cases for ES256AlgorithmHandler. + */ +public class ES256AlgorithmHandlerTest { + + private final ES256AlgorithmHandler handler = new ES256AlgorithmHandler(); + + @Before + public void setUp() { + // Ensure BouncyCastle provider is registered (required for signing operations) + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + public void shouldGenerateKeyPairSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + + assertNotNull("KeyPair should not be null", keyPair); + assertNotNull("Public key should not be null", keyPair.getPublic()); + assertNotNull("Private key should not be null", keyPair.getPrivate()); + assertEquals("Algorithm should be EC", "EC", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGetKeyFactorySuccessfully() throws Exception { + KeyFactory keyFactory = handler.getKeyFactory(); + + assertNotNull("KeyFactory should not be null", keyFactory); + assertEquals("Algorithm should be EC", "EC", keyFactory.getAlgorithm()); + } + + @Test + public void shouldCreateJWKSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + + assertNotNull("JWK should not be null", jwk); + assertTrue("JWK should be ECKey", jwk instanceof ECKey); + assertEquals("Algorithm should be ES256", JWSAlgorithm.ES256, jwk.getAlgorithm()); + } + + @Test + public void shouldCreateSignerSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + assertNotNull("Signer should not be null", signer); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionWhenJWKIsInvalid() throws Exception { + // Create invalid JWK (not ECKey) + KeyPair rsaKeyPair = new RS256AlgorithmHandler().generateKeyPair(); + JWK invalidJwk = new RS256AlgorithmHandler().createJWK(rsaKeyPair); + + handler.createSigner(invalidJwk); + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandlerTest.java b/src/test/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandlerTest.java new file mode 100644 index 000000000..3b62b4857 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/ES256KAlgorithmHandlerTest.java @@ -0,0 +1,78 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.Test; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Security; + +import static org.junit.Assert.*; + +/** + * Test cases for ES256KAlgorithmHandler. + */ +public class ES256KAlgorithmHandlerTest { + + private final ES256KAlgorithmHandler handler = new ES256KAlgorithmHandler(); + + @Before + public void setUp() { + // Ensure BouncyCastle provider is registered (required for ES256K) + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + public void shouldGenerateKeyPairSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + + assertNotNull("KeyPair should not be null", keyPair); + assertNotNull("Public key should not be null", keyPair.getPublic()); + assertNotNull("Private key should not be null", keyPair.getPrivate()); + assertEquals("Algorithm should be EC", "EC", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGetKeyFactorySuccessfully() throws Exception { + KeyFactory keyFactory = handler.getKeyFactory(); + + assertNotNull("KeyFactory should not be null", keyFactory); + assertEquals("Algorithm should be EC", "EC", keyFactory.getAlgorithm()); + } + + @Test + public void shouldCreateJWKSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + + assertNotNull("JWK should not be null", jwk); + assertTrue("JWK should be ECKey", jwk instanceof ECKey); + assertEquals("Algorithm should be ES256K", JWSAlgorithm.ES256K, jwk.getAlgorithm()); + } + + @Test + public void shouldCreateSignerSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + assertNotNull("Signer should not be null", signer); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionWhenJWKIsInvalid() throws Exception { + // Create invalid JWK (not ECKey) + KeyPair rsaKeyPair = new RS256AlgorithmHandler().generateKeyPair(); + JWK invalidJwk = new RS256AlgorithmHandler().createJWK(rsaKeyPair); + + handler.createSigner(invalidJwk); + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandlerTest.java b/src/test/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandlerTest.java new file mode 100644 index 000000000..d4d66015b --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/Ed25519AlgorithmHandlerTest.java @@ -0,0 +1,127 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.util.Base64URL; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * Test cases for ED25519AlgorithmHandler. + */ +public class Ed25519AlgorithmHandlerTest { + + private final Ed25519AlgorithmHandler handler = new Ed25519AlgorithmHandler(); + + @Test + public void shouldGenerateKeyPairSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + + assertNotNull("KeyPair should not be null", keyPair); + assertNotNull("Public key should not be null", keyPair.getPublic()); + assertNotNull("Private key should not be null", keyPair.getPrivate()); + assertEquals("Algorithm should be EdDSA", "EdDSA", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGetKeyFactorySuccessfully() throws Exception { + KeyFactory keyFactory = handler.getKeyFactory(); + + assertNotNull("KeyFactory should not be null", keyFactory); + assertEquals("Algorithm should be Ed25519", "Ed25519", keyFactory.getAlgorithm()); + } + + @Test + public void shouldCreateJWKSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + + assertNotNull("JWK should not be null", jwk); + assertTrue("JWK should be OctetKeyPair", jwk instanceof OctetKeyPair); + assertEquals("Algorithm should be Ed25519", JWSAlgorithm.Ed25519, jwk.getAlgorithm()); + } + + @Test + public void shouldCreateSignerSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + assertNotNull("Signer should not be null", signer); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionWhenJWKIsInvalid() throws Exception { + KeyPair rsaKeyPair = new RS256AlgorithmHandler().generateKeyPair(); + JWK invalidJwk = new RS256AlgorithmHandler().createJWK(rsaKeyPair); + + handler.createSigner(invalidJwk); + } + + @Test + public void shouldSignAndProduce64ByteSignature() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.Ed25519).build(); + byte[] payload = "test-payload".getBytes(StandardCharsets.UTF_8); + + com.nimbusds.jose.util.Base64URL signature = signer.sign(header, payload); + assertNotNull("Signature must not be null", signature); + byte[] sigBytes = signature.decode(); + assertEquals("Ed25519 signature should be 64 bytes", 64, sigBytes.length); + + assertTrue("supported algorithms must contain Ed25519", signer.supportedJWSAlgorithms().contains(JWSAlgorithm.Ed25519)); + assertNotNull("JCAContext should not be null", signer.getJCAContext()); + } + + @Test(expected = JOSEException.class) + public void shouldThrowJOSEExceptionWhenPrivateKeyIsInvalidDuringSign() throws Exception { + byte[] fakeX = new byte[32]; + for (int i = 0; i < fakeX.length; i++) fakeX[i] = (byte) (i + 1); + byte[] invalidD = new byte[1]; + + OctetKeyPair invalidOkp = new OctetKeyPair.Builder(Curve.Ed25519, Base64URL.encode(fakeX)) + .d(Base64URL.encode(invalidD)) + .algorithm(JWSAlgorithm.Ed25519) + .keyUse(KeyUse.SIGNATURE) + .build(); + + JWSSigner signer = handler.createSigner(invalidOkp); + + signer.sign(new JWSHeader.Builder(JWSAlgorithm.Ed25519).build(), "payload".getBytes(StandardCharsets.UTF_8)); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionWhenCreateJWKWithNonEdKeyPair() throws Exception { + KeyPair rsaKeyPair = new RS256AlgorithmHandler().generateKeyPair(); + handler.createJWK(rsaKeyPair); + } + + @Test + public void shouldSupportBothEd25519AndEdDSAAlgorithms() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + Set supported = signer.supportedJWSAlgorithms(); + assertTrue("signer must declare Ed25519 (pre-standard Nimbus name)", supported.contains(JWSAlgorithm.Ed25519)); + assertTrue("signer must declare EdDSA (IANA RFC 8037 name used in KB-JWT alg header)", supported.contains(JWSAlgorithm.EdDSA)); + assertEquals(2, supported.size()); + } + +} + diff --git a/src/test/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandlerTest.java b/src/test/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandlerTest.java new file mode 100644 index 000000000..02d4ff51c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/RS256AlgorithmHandlerTest.java @@ -0,0 +1,78 @@ +package io.mosip.mimoto.util.factory; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.Test; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Security; + +import static org.junit.Assert.*; + +/** + * Test cases for RS256AlgorithmHandler. + */ +public class RS256AlgorithmHandlerTest { + + private final RS256AlgorithmHandler handler = new RS256AlgorithmHandler(); + + @Before + public void setUp() { + // Ensure BouncyCastle provider is registered (required for signing operations) + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + public void shouldGenerateKeyPairSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + + assertNotNull("KeyPair should not be null", keyPair); + assertNotNull("Public key should not be null", keyPair.getPublic()); + assertNotNull("Private key should not be null", keyPair.getPrivate()); + assertEquals("Algorithm should be RSA", "RSA", keyPair.getPublic().getAlgorithm()); + } + + @Test + public void shouldGetKeyFactorySuccessfully() throws Exception { + KeyFactory keyFactory = handler.getKeyFactory(); + + assertNotNull("KeyFactory should not be null", keyFactory); + assertEquals("Algorithm should be RSA", "RSA", keyFactory.getAlgorithm()); + } + + @Test + public void shouldCreateJWKSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + + assertNotNull("JWK should not be null", jwk); + assertTrue("JWK should be RSAKey", jwk instanceof RSAKey); + assertEquals("Algorithm should be RS256", JWSAlgorithm.RS256, jwk.getAlgorithm()); + } + + @Test + public void shouldCreateSignerSuccessfully() throws Exception { + KeyPair keyPair = handler.generateKeyPair(); + JWK jwk = handler.createJWK(keyPair); + JWSSigner signer = handler.createSigner(jwk); + + assertNotNull("Signer should not be null", signer); + } + + @Test(expected = ClassCastException.class) + public void shouldThrowExceptionWhenJWKIsInvalid() throws Exception { + // Create invalid JWK (not RSAKey) + KeyPair ecKeyPair = new ES256AlgorithmHandler().generateKeyPair(); + JWK invalidJwk = new ES256AlgorithmHandler().createJWK(ecKeyPair); + + handler.createSigner(invalidJwk); + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactoryTest.java b/src/test/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactoryTest.java new file mode 100644 index 000000000..0fe255f3c --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/factory/SigningAlgorithmHandlerFactoryTest.java @@ -0,0 +1,81 @@ +package io.mosip.mimoto.util.factory; + +import io.mosip.mimoto.constant.SigningAlgorithm; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Test cases for SigningAlgorithmHandlerFactory. + */ +public class SigningAlgorithmHandlerFactoryTest { + + @Test + public void shouldReturnHandlerForRS256() { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(SigningAlgorithm.RS256); + + assertNotNull("Handler should not be null", handler); + assertTrue("Handler should be RS256AlgorithmHandler", handler instanceof RS256AlgorithmHandler); + } + + @Test + public void shouldReturnHandlerForES256() { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(SigningAlgorithm.ES256); + + assertNotNull("Handler should not be null", handler); + assertTrue("Handler should be ES256AlgorithmHandler", handler instanceof ES256AlgorithmHandler); + } + + @Test + public void shouldReturnHandlerForES256K() { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(SigningAlgorithm.ES256K); + + assertNotNull("Handler should not be null", handler); + assertTrue("Handler should be ES256KAlgorithmHandler", handler instanceof ES256KAlgorithmHandler); + } + + @Test + public void shouldReturnHandlerForED25519() { + SigningAlgorithmHandler handler = SigningAlgorithmHandlerFactory.getHandler(SigningAlgorithm.ED25519); + + assertNotNull("Handler should not be null", handler); + assertTrue("Handler should be ED25519AlgorithmHandler", handler instanceof Ed25519AlgorithmHandler); + } + + @Test + public void shouldThrowExceptionWhenAlgorithmIsNull() { + try { + SigningAlgorithmHandlerFactory.getHandler(null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue("Exception message should mention null", + e.getMessage().contains("null") || e.getMessage().contains("Unsupported")); + } + } + + @Test + public void shouldThrowExceptionWhenHandlerNotPresentInMap() throws Exception { + Field handlersField = SigningAlgorithmHandlerFactory.class.getDeclaredField("handlers"); + handlersField.setAccessible(true); + + @SuppressWarnings("unchecked") + Map handlers = + (Map) handlersField.get(null); + + SigningAlgorithmHandler removed = handlers.remove(SigningAlgorithm.RS256); + try { + SigningAlgorithmHandlerFactory.getHandler(SigningAlgorithm.RS256); + fail("Should throw IllegalArgumentException when handler is missing in map"); + } catch (IllegalArgumentException e) { + assertTrue("Exception message should mention unsupported", e.getMessage().contains("Unsupported")); + } finally { + if (removed != null) { + handlers.put(SigningAlgorithm.RS256, removed); + } + } + } +} + diff --git a/src/test/java/io/mosip/mimoto/util/parser/Draft13WellknownParserTest.java b/src/test/java/io/mosip/mimoto/util/parser/Draft13WellknownParserTest.java new file mode 100644 index 000000000..f2f48fdda --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/parser/Draft13WellknownParserTest.java @@ -0,0 +1,275 @@ +package io.mosip.mimoto.util.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.Draft13CredentialIssuerWellknownResponseValidator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class Draft13WellknownParserTest { + + private Draft13WellknownParser parser; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + parser = new Draft13WellknownParser(objectMapper, new Draft13CredentialIssuerWellknownResponseValidator()); + } + + @Test + void shouldReturnDraft13AsVersion() { + assertEquals(VCSpecificationVersion.DRAFT_13, parser.getSupportedVersion()); + } + + @Test + void shouldParseDraft13ResponseWithDisplayAndOrder() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "UniversityDegree": { + "format": "ldp_vc", + "scope": "university_degree", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "display": [ + {"name": "University Degree", "locale": "en"} + ], + "order": ["name", "degree"], + "vct": "UniversityDegreeCredential" + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals("https://issuer.example.com", result.getCredentialIssuer()); + assertEquals(List.of("https://auth.example.com"), result.getAuthorizationServers()); + assertEquals("https://issuer.example.com/credential", result.getCredentialEndPoint()); + assertNull(result.getNonceEndpoint()); + assertEquals(VCSpecificationVersion.DRAFT_13, result.getVersion()); + + assertNotNull(result.getCredentialConfigurationsSupported()); + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("UniversityDegree"); + assertNotNull(cred); + assertEquals("ldp_vc", cred.getFormat()); + assertEquals("university_degree", cred.getScope()); + assertEquals("UniversityDegreeCredential", cred.getVct()); + assertNotNull(cred.getDisplay()); + assertEquals(1, cred.getDisplay().size()); + assertEquals("University Degree", cred.getDisplay().get(0).getName()); + assertEquals(List.of("name", "degree"), cred.getOrder()); + } + + @Test + void shouldMapAllFieldsFromDraft13Config() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth1.com", "https://auth2.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "Cred1": { + "format": "ldp_vc", + "scope": "scope1", + "doctype": "doctype1", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]}, + "cwt": {"proof_signing_alg_values_supported": ["EdDSA"]} + }, + "claims": {"field1": {}, "field2": {}}, + "credential_definition": { + "type": ["VerifiableCredential"], + "credentialSubject": {"name": {"display": [{"name": "Name", "locale": "en"}]}} + }, + "display": [ + {"name": "Credential One", "locale": "en"}, + {"name": "Credenziale Uno", "locale": "it"} + ], + "order": ["name", "dob"], + "vct": "VCT1" + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals(2, result.getAuthorizationServers().size()); + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("Cred1"); + assertEquals("ldp_vc", cred.getFormat()); + assertEquals("scope1", cred.getScope()); + assertEquals("doctype1", cred.getDoctype()); + assertEquals(2, cred.getProofTypesSupported().size()); + assertEquals(2, cred.getClaims().size()); + assertNotNull(cred.getCredentialDefinition()); + assertEquals(List.of("VerifiableCredential"), cred.getCredentialDefinition().getType()); + assertEquals(2, cred.getDisplay().size()); + assertEquals(List.of("name", "dob"), cred.getOrder()); + assertEquals("VCT1", cred.getVct()); + } + + @Test + void shouldParseMultipleCredentialConfigurations() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "Cred1": { + "format": "ldp_vc", + "scope": "scope1", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "display": [{"name": "Cred1", "locale": "en"}] + }, + "Cred2": { + "format": "mso_mdoc", + "scope": "scope2", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "display": [{"name": "Cred2", "locale": "en"}] + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals(2, result.getCredentialConfigurationsSupported().size()); + assertEquals("ldp_vc", result.getCredentialConfigurationsSupported().get("Cred1").getFormat()); + assertEquals("mso_mdoc", result.getCredentialConfigurationsSupported().get("Cred2").getFormat()); + } + + @Test + void shouldNotSetNonceEndpointForDraft13() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": {} + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertNull(result.getNonceEndpoint()); + } + + @Test + void shouldThrowIOExceptionForInvalidJson() { + assertThrows(IOException.class, () -> parser.parse("not valid json")); + } + + @Test + void shouldDelegateValidationToValidator() { + CredentialIssuerWellKnownResponse response = new CredentialIssuerWellKnownResponse(); + response.setCredentialConfigurationsSupported(Map.of()); + + Validator noOpValidator = new NoOpValidator(); + + assertDoesNotThrow(() -> parser.validate(response, noOpValidator)); + } + + @Test + void shouldPropagateValidationException() { + Draft13WellknownParser throwingParser = new Draft13WellknownParser(objectMapper, new ThrowingValidator()); + + CredentialIssuerWellKnownResponse response = new CredentialIssuerWellKnownResponse(); + Validator noOpValidator = new NoOpValidator(); + + assertThrows(InvalidWellknownResponseException.class, + () -> throwingParser.validate(response, noOpValidator)); + } + + @Test + void shouldHandleNullOptionalFields() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "Cred1": { + "format": "ldp_vc", + "scope": "scope1", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "display": [{"name": "Test", "locale": "en"}] + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("Cred1"); + assertNull(cred.getDoctype()); + assertNull(cred.getClaims()); + assertNull(cred.getCredentialDefinition()); + assertNull(cred.getOrder()); + } + + private static class ThrowingValidator extends Draft13CredentialIssuerWellknownResponseValidator { + @Override + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + throw new InvalidWellknownResponseException("test error"); + } + } + + @SuppressWarnings("unchecked") + private static class NoOpValidator implements Validator { + @Override + public Set> validate(T object, Class... groups) { + return new HashSet<>(); + } + + @Override + public Set> validateProperty(T object, String propertyName, Class... groups) { + return new HashSet<>(); + } + + @Override + public Set> validateValue(Class beanType, String propertyName, Object value, Class... groups) { + return new HashSet<>(); + } + + @Override + public jakarta.validation.metadata.BeanDescriptor getConstraintsForClass(Class clazz) { + return null; + } + + @Override + public T unwrap(Class type) { + return null; + } + + @Override + public jakarta.validation.executable.ExecutableValidator forExecutables() { + return null; + } + } +} diff --git a/src/test/java/io/mosip/mimoto/util/parser/V1WellknownParserTest.java b/src/test/java/io/mosip/mimoto/util/parser/V1WellknownParserTest.java new file mode 100644 index 000000000..90fed8f30 --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/parser/V1WellknownParserTest.java @@ -0,0 +1,474 @@ +package io.mosip.mimoto.util.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import io.mosip.mimoto.util.V1CredentialIssuerWellknownResponseValidator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class V1WellknownParserTest { + + private V1WellknownParser parser; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + parser = new V1WellknownParser(objectMapper, new V1CredentialIssuerWellknownResponseValidator()); + } + + @Test + void shouldReturnV1AsVersion() { + assertEquals(VCSpecificationVersion.V1, parser.getSupportedVersion()); + } + + @Test + void shouldParseV1ResponseWithCredentialMetadata() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "nonce_endpoint": "https://issuer.example.com/nonce", + "credential_configurations_supported": { + "UniversityDegree": { + "format": "ldp_vc", + "scope": "university_degree", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [ + {"name": "University Degree", "locale": "en"} + ] + }, + "vct": "UniversityDegreeCredential" + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals("https://issuer.example.com", result.getCredentialIssuer()); + assertEquals(List.of("https://auth.example.com"), result.getAuthorizationServers()); + assertEquals("https://issuer.example.com/credential", result.getCredentialEndPoint()); + assertEquals("https://issuer.example.com/nonce", result.getNonceEndpoint()); + assertEquals(VCSpecificationVersion.V1, result.getVersion()); + + assertNotNull(result.getCredentialConfigurationsSupported()); + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("UniversityDegree"); + assertNotNull(cred); + assertEquals("ldp_vc", cred.getFormat()); + assertEquals("university_degree", cred.getScope()); + assertEquals("UniversityDegreeCredential", cred.getVct()); + assertNotNull(cred.getDisplay()); + assertEquals(1, cred.getDisplay().size()); + assertEquals("University Degree", cred.getDisplay().get(0).getName()); + } + + @Test + void shouldParseV1ResponseWithNullCredentialMetadata() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "mso_mdoc", + "scope": "test_scope", + "doctype": "org.iso.18013.5.1.mDL", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "vct": "TestVCT" + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("TestCred"); + assertNotNull(cred); + assertNull(cred.getDisplay()); + assertNull(cred.getClaims()); + assertEquals("mso_mdoc", cred.getFormat()); + assertEquals("org.iso.18013.5.1.mDL", cred.getDoctype()); + assertNull(cred.getCredentialDefinition()); + assertNull(result.getNonceEndpoint()); + } + + @Test + void shouldMapAllFieldsFromV1Config() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth1.com", "https://auth2.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "nonce_endpoint": "https://issuer.example.com/nonce", + "credential_configurations_supported": { + "Cred1": { + "format": "ldp_vc", + "scope": "scope1", + "doctype": "doctype1", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]}, + "cwt": {"proof_signing_alg_values_supported": ["EdDSA"]} + }, + "credential_metadata": { + "display": [ + {"name": "Credential One", "locale": "en"}, + {"name": "Credenziale Uno", "locale": "it"} + ], + "claims": [ + {"path": ["field1"], "display": [{"name": "Field 1", "locale": "en"}]}, + {"path": ["field2"], "display": [{"name": "Field 2", "locale": "en"}]} + ] + }, + "vct": "VCT1" + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals(2, result.getAuthorizationServers().size()); + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("Cred1"); + assertEquals("ldp_vc", cred.getFormat()); + assertEquals("scope1", cred.getScope()); + assertEquals("doctype1", cred.getDoctype()); + assertEquals(2, cred.getProofTypesSupported().size()); + assertEquals(2, cred.getClaims().size()); + assertNull(cred.getCredentialDefinition()); + assertEquals(2, cred.getDisplay().size()); + assertEquals("VCT1", cred.getVct()); + } + + @Test + void shouldParseMultipleCredentialConfigurations() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "Cred1": { + "format": "ldp_vc", + "scope": "scope1", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [{"name": "Cred1", "locale": "en"}] + } + }, + "Cred2": { + "format": "mso_mdoc", + "scope": "scope2", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertEquals(2, result.getCredentialConfigurationsSupported().size()); + assertNotNull(result.getCredentialConfigurationsSupported().get("Cred1").getDisplay()); + assertNull(result.getCredentialConfigurationsSupported().get("Cred2").getDisplay()); + } + + @Test + void shouldParseV1ResponseWithCredentialMetadataButNoDisplay() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "ldp_vc", + "scope": "test_scope", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "claims": [ + {"path": ["given_name"], "display": [{"name": "Given Name", "locale": "en"}]} + ] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("TestCred"); + assertNotNull(cred); + assertNull(cred.getDisplay()); + assertEquals("ldp_vc", cred.getFormat()); + assertNotNull(cred.getClaims()); + assertEquals(1, cred.getClaims().size()); + } + + @Test + void shouldParseV1ArrayFormatClaims() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "nonce_endpoint": "https://issuer.example.com/nonce", + "credential_configurations_supported": { + "EmployeeCred": { + "format": "vc+sd-jwt", + "scope": "employee.read", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "vct": "EmployeeCredential", + "credential_metadata": { + "display": [{"name": "Employee Credential", "locale": "en"}], + "claims": [ + {"path": ["employeeId"], "display": [{"name": "Employee ID", "locale": "en"}]}, + {"path": ["name"], "display": [{"name": "Name", "locale": "en"}, {"name": "Nombre", "locale": "es"}]}, + {"path": ["address", "city"], "display": [{"name": "City", "locale": "en"}]} + ] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("EmployeeCred"); + assertNotNull(cred); + assertNotNull(cred.getClaims()); + assertEquals(3, cred.getClaims().size()); + + assertTrue(cred.getClaims().containsKey("employeeId")); + assertTrue(cred.getClaims().containsKey("name")); + assertTrue(cred.getClaims().containsKey("city")); + + Map nameClaimValue = (Map) cred.getClaims().get("name"); + assertNotNull(nameClaimValue.get("display")); + List> nameDisplays = (List>) nameClaimValue.get("display"); + assertEquals(2, nameDisplays.size()); + assertEquals("Name", nameDisplays.get(0).get("name")); + } + + @Test + void shouldUseLastPathElementAsClaimKey() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "vc+sd-jwt", + "scope": "test.read", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [{"name": "Test", "locale": "en"}], + "claims": [ + {"path": ["org.iso.18013.5.1", "given_name"], "display": [{"name": "Given Name", "locale": "en"}]}, + {"path": ["org.iso.18013.5.1.aamva", "organ_donor"]} + ] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + CredentialsSupportedResponse cred = result.getCredentialConfigurationsSupported().get("TestCred"); + assertNotNull(cred.getClaims()); + assertEquals(2, cred.getClaims().size()); + assertTrue(cred.getClaims().containsKey("given_name")); + assertTrue(cred.getClaims().containsKey("organ_donor")); + } + + @Test + void shouldReturnNullClaimsWhenClaimsArrayIsEmpty() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "vc+sd-jwt", + "scope": "test.read", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [{"name": "Test", "locale": "en"}], + "claims": [] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertNull(result.getCredentialConfigurationsSupported().get("TestCred").getClaims()); + } + + @Test + void shouldSkipClaimEntriesWithEmptyPath() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "vc+sd-jwt", + "scope": "test.read", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [{"name": "Test", "locale": "en"}], + "claims": [ + {"path": [], "display": [{"name": "Empty Path", "locale": "en"}]}, + {"path": ["valid_field"], "display": [{"name": "Valid", "locale": "en"}]} + ] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + Map claims = result.getCredentialConfigurationsSupported().get("TestCred").getClaims(); + assertNotNull(claims); + assertEquals(1, claims.size()); + assertTrue(claims.containsKey("valid_field")); + } + + @Test + void shouldReturnNullClaimsWhenAllEntriesAreSkipped() throws IOException { + String json = """ + { + "credential_issuer": "https://issuer.example.com", + "authorization_servers": ["https://auth.example.com"], + "credential_endpoint": "https://issuer.example.com/credential", + "credential_configurations_supported": { + "TestCred": { + "format": "vc+sd-jwt", + "scope": "test.read", + "proof_types_supported": { + "jwt": {"proof_signing_alg_values_supported": ["ES256"]} + }, + "credential_metadata": { + "display": [{"name": "Test", "locale": "en"}], + "claims": [ + {"path": [], "display": [{"name": "No Path", "locale": "en"}]}, + {"display": [{"name": "Missing Path", "locale": "en"}]}, + {"path": ["ns", null], "display": [{"name": "Null Key", "locale": "en"}]} + ] + } + } + } + } + """; + + CredentialIssuerWellKnownResponse result = parser.parse(json); + + assertNull(result.getCredentialConfigurationsSupported().get("TestCred").getClaims()); + } + + @Test + void shouldThrowIOExceptionForInvalidJson() { + assertThrows(IOException.class, () -> parser.parse("not valid json")); + } + + @Test + void shouldDelegateValidationToValidator() { + CredentialIssuerWellKnownResponse response = new CredentialIssuerWellKnownResponse(); + response.setCredentialConfigurationsSupported(Map.of()); + + Validator noOpValidator = new NoOpValidator(); + + assertDoesNotThrow(() -> parser.validate(response, noOpValidator)); + } + + @Test + void shouldPropagateValidationException() { + V1WellknownParser throwingParser = new V1WellknownParser(objectMapper, new ThrowingValidator()); + + CredentialIssuerWellKnownResponse response = new CredentialIssuerWellKnownResponse(); + Validator noOpValidator = new NoOpValidator(); + + assertThrows(InvalidWellknownResponseException.class, + () -> throwingParser.validate(response, noOpValidator)); + } + + private static class ThrowingValidator extends V1CredentialIssuerWellknownResponseValidator { + @Override + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + throw new InvalidWellknownResponseException("test error"); + } + } + + @SuppressWarnings("unchecked") + private static class NoOpValidator implements Validator { + @Override + public Set> validate(T object, Class... groups) { + return new HashSet<>(); + } + + @Override + public Set> validateProperty(T object, String propertyName, Class... groups) { + return new HashSet<>(); + } + + @Override + public Set> validateValue(Class beanType, String propertyName, Object value, Class... groups) { + return new HashSet<>(); + } + + @Override + public jakarta.validation.metadata.BeanDescriptor getConstraintsForClass(Class clazz) { + return null; + } + + @Override + public T unwrap(Class type) { + return null; + } + + @Override + public jakarta.validation.executable.ExecutableValidator forExecutables() { + return null; + } + } +} diff --git a/src/test/java/io/mosip/mimoto/util/parser/WellknownParserFactoryTest.java b/src/test/java/io/mosip/mimoto/util/parser/WellknownParserFactoryTest.java new file mode 100644 index 000000000..c106feb6a --- /dev/null +++ b/src/test/java/io/mosip/mimoto/util/parser/WellknownParserFactoryTest.java @@ -0,0 +1,79 @@ +package io.mosip.mimoto.util.parser; + +import io.mosip.mimoto.constant.VCSpecificationVersion; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.exception.InvalidWellknownResponseException; +import jakarta.validation.Validator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class WellknownParserFactoryTest { + + private WellknownParserFactory factory; + private WellknownResponseParser v1Parser; + private WellknownResponseParser draft13Parser; + + @BeforeEach + void setUp() { + v1Parser = new StubParser(VCSpecificationVersion.V1); + draft13Parser = new StubParser(VCSpecificationVersion.DRAFT_13); + factory = new WellknownParserFactory(List.of(v1Parser, draft13Parser)); + } + + @Test + void shouldReturnV1ParserForV1Version() { + WellknownResponseParser parser = factory.getParser(VCSpecificationVersion.V1); + assertSame(v1Parser, parser); + } + + @Test + void shouldReturnDraft13ParserForDraft13Version() { + WellknownResponseParser parser = factory.getParser(VCSpecificationVersion.DRAFT_13); + assertSame(draft13Parser, parser); + } + + @Test + void shouldThrowExceptionForUnregisteredVersion() { + WellknownParserFactory singleParserFactory = new WellknownParserFactory(List.of(v1Parser)); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> singleParserFactory.getParser(VCSpecificationVersion.DRAFT_13)); + + assertTrue(exception.getMessage().contains("DRAFT_13")); + } + + @Test + void shouldHandleEmptyParserList() { + WellknownParserFactory emptyFactory = new WellknownParserFactory(List.of()); + + assertThrows(IllegalArgumentException.class, + () -> emptyFactory.getParser(VCSpecificationVersion.V1)); + } + + private static class StubParser implements WellknownResponseParser { + private final VCSpecificationVersion version; + + StubParser(VCSpecificationVersion version) { + this.version = version; + } + + @Override + public VCSpecificationVersion getSupportedVersion() { + return version; + } + + @Override + public CredentialIssuerWellKnownResponse parse(String jsonResponse) throws IOException { + return new CredentialIssuerWellKnownResponse(); + } + + @Override + public void validate(CredentialIssuerWellKnownResponse response, Validator validator) throws InvalidWellknownResponseException { + } + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 000000000..61b555364 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,60 @@ +# MOSIP partner + +mosip.partner.id=partnerId +mosip.partner.encryption.key=partnerkey +mosip.event.topic=topic + +mosip.inji.allowedAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedEkycAuthType=demo,otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.allowedInternalAuthType=otp,bio-Finger,bio-Iris,bio-Face +mosip.inji.vcDownloadMaxRetry=10 +mosip.inji.vcDownloadPoolInterval=6000 +mosip.inji.issuer=residentapp +mosip.inji.openId4VCIDownloadVCTimeout=30000 +mosip.inji.user.wallet.pin.validation.regex=^\\d{6}$ +mosip.inji.user.wallet.name.validation.regex=^[A-Za-z0-9 _.-]{0,50}$ + +mosipbox.public.url=https://api.dev1.mosip.net +public.url=https://api.dev1.mosip.net + +mosip.oidc.client.assertion.type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer +mosip.oidc.p12.filename=oidckeystore.p12 +mosip.oidc.p12.password=5YBx6QT2wbY8Ls6w +mosip.oidc.p12.path=certs/ + + +#OpenId4VP related Configuration START + +#Inji Web Config +mosip.inji.web.url=https://injiweb.dev1.mosip.net +mosip.inji.web.redirect.url=${mosip.inji.web.url}/authorize +mosip.inji.qr.data.size.limit=4096 +mosip.inji.qr.code.height=650 +mosip.inji.qr.code.width=650 + +#OVP Config +mosip.inji.ovp.qrdata.pattern=INJI_OVP://${mosip.inji.web.url}/authorize?response_type=vp_token&resource=%s&presentation_definition=%s +mosip.inji.ovp.redirect.url.pattern=%s#vp_token=%s&presentation_submission=%s +mosip.inji.ovp.error.redirect.url.pattern=%s?error=%s&error_description=%s + +#DataShare Config +mosip.data.share.url=https://datashare-inji.dev1.mosip.net +mosip.data.share.create.url=${mosip.data.share.url}/v1/datashare/create/static-policyid/static-subscriberid +mosip.data.share.get.url.pattern=http://datashare.datashare/v1/datashare/get/static-policyid/static-subscriberid/* +mosip.data.share.create.retry.count=3 + +#OpenId4VP related Configuration END + +spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs + +# Wallet Passcode Configuration +# Duration (in minutes) for which the wallet remains locked after exceeding failed attempts in a cycle. +# Must be a whole number; decimal/fractional values (e.g., 0.5) will throw an error during Mimoto launch +wallet.passcode.retryBlockedUntil=60 +# Maximum number of failed passcode attempts allowed in a single cycle (defaults to 1 if <=0) +wallet.passcode.maxFailedAttemptsAllowedPerCycle=5 +# Maximum number of lock cycles allowed before the wallet is permanently locked (defaults to 1 if <=0) +wallet.passcode.maxLockCyclesAllowed=3 + +#Signing Algorithms Priority Order Configuration +signing.algorithms.priority.order = ED25519,ES256K,ES256,RS256 \ No newline at end of file diff --git a/src/test/resources/bootstrap.properties b/src/test/resources/bootstrap.properties new file mode 100644 index 000000000..99c082bf4 --- /dev/null +++ b/src/test/resources/bootstrap.properties @@ -0,0 +1,21 @@ +spring.cloud.config.uri=localhost +spring.cloud.config.label=develop +spring.profiles.active=test +spring.cloud.config.name=mimoto,inji +spring.application.name=mimoto +spring.main.allow-bean-definition-overriding=true + +server.port=8088 +server.servlet.context-path=/v1/mimoto + +openapi.info.title=${spring.application.name} +openapi.info.description=${spring.application.name} +openapi.info.version=1.0 +openapi.info.license.name=Mosip +openapi.info.license.url=https://docs.mosip.io/platform/license +mosipbox.public.url=http://localhost:${server.port} +openapi.service.servers[0].url=${mosipbox.public.url}${server.servlet.context-path} +openapi.service.servers[0].description=${spring.application.name} +openapi.group.name=${openapi.info.title} +openapi.group.paths[0]=/** + diff --git a/src/test/resources/responses/InjiConfig.json b/src/test/resources/responses/InjiConfig.json new file mode 100644 index 000000000..f4590fd90 --- /dev/null +++ b/src/test/resources/responses/InjiConfig.json @@ -0,0 +1,12 @@ +{ + "response": { + "allowedAuthType": "demo,otp,bio-Finger,bio-Iris,bio-Face", + "allowedEkycAuthType": "demo,otp,bio-Finger,bio-Iris,bio-Face", + "allowedInternalAuthType": "otp,bio-Finger,bio-Iris,bio-Face", + "openId4VCIDownloadVCTimeout": "30000", + "vcDownloadMaxRetry": "10", + "vcDownloadPoolInterval": "6000", + "issuer": "residentapp" + }, + "errors": [] +} diff --git a/src/test/resources/responses/VerifiableCredential.json b/src/test/resources/responses/VerifiableCredential.json new file mode 100644 index 000000000..a5c095fef --- /dev/null +++ b/src/test/resources/responses/VerifiableCredential.json @@ -0,0 +1,56 @@ +{ + "format": "format", + "credential":{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://api.qa-inji.mosip.net/.well-known/mosip-ida-context.json", + { + "sec": "https://w3id.org/security#" + } + ], + "credentialSubject": { + "face": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wAARCAFAAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD6G/bH+LGh/Bb9l/xp8QPEWuRadbW+jm0W9mfasUl1Itohz2w06nPQYycAE1+ZL+JLO6iW7s7xZIZF3Ruk25GBAIIPQjp0r37/AIOIfiivhb9j/wAM/C+3udlx4u8bRyOm7/WWtlBJI4x7TzWpz7e4r8jfhv8AGrx58K7kR+H9U86xZ8zaVdDfC/clR/yzb3Uj3yOKXs+eN0dEqjjKzP1u/wCCalpb+JP2p7O8jLOdH0K/vOf4cxi3JI/7eAPxHtX6J7fm21+an/BBP4j6F8aPHXj7xpp+m3FpeaH4dtLG/guU3eUbq4Mq7ZB98H7E3XB+XpX6Ukjf70ofCP7Og7BxmmOO9BfDdOKazdzVCF2N6U5E/wDsajVt1P3dKnlAlRcDdU8a5OarRyYPFTxyZ44qiUi9bhQPu/pXnP7Uf7Mun/tFeGI9U8P28MPjPR4dulXLEINShGT9ilY8ZzkxM33WJBIVia9EhftVmKTHNHxGkXyvQ/OjTbS8065l0zULeS2ubeVop4bmMxyRSKcNGynBBBGCCOoPSur023Uqv3T9e1fR/wC1b+zrH8SbWb4t+B9LDeIbOHdrdhCuDqkKjHnqB1nRRyOsijj5lAb5x0efdGnkycN0ZeM/T61EY2Z0Rlymva2cb/xKegz0zV2KyjyF4H93cv6VFYPIyL/rGbsN3P8Ahz/KrsU827bujX/tkBjHrx/U/SrD3hXswx/eRr/n/P8A+qomgj+8sbbd2V+n+f61Jqsl4umT/Z5FWfyj5AVvlEhHy5HGRnGRkZ/Kub+B3jrWPix8F/CPxO1zR7WzufEXhbT9UntrPPkxPcW6zbV3kkhfMC8kk4yTkmp0uGsToltPk3JJGvy/dYj/ABqC6jkVS3y+u5at3MDAD93kD/ZH9RWff7kQ/vMHtuPH+fyqg5ijFpH9u6pDpcbxq1w+1N38PGTk+mAfyNch4x+F9/ba9byW6yXdvZeczi2Ik/ek5MhEZbAI4BOPukdqt+K/7Y+32dxpbK6QzP8AaFckKFMbDnHTjcfw6Yrz+Hxj8SPFtwdR8N6h/YOlyxPDc2zNHP8A2nCcfLLvTAUjICDsxyTnjOTHeTN6/srqzlEdxZtEcZVZEIYj2yB+ma9A+FH7Ff7TvxwsIde+H/wqvH0u4G6HWtRljs7Vo84LLJKV81QQeYw3SuP+Gvg7X/it460j4f2cawx3V2HvJIbiRvJiUEyyfOTjC7goPcqM9K+/Y726svD9n4Tt7yddL020S3sLDzmMMESjCqq5wMCs1zS3KlK2x4l4U/4JY+G9KKXHx2/aQ0m2bjzdJ8GWcl/Mfbz5Aqxt9YyP6+neFP2cP2KfheVk8MfANvFF5H01Dx5ffa1YjkE2o/0c/XYDWo1wRxVe4n3LtbnNU4vqL2kzo9Q+LXii1sV0fw/JZ6HYxriKy8P2SWkcY/2dg3D868v+IPiXUNQWT7ReSzSSfelllLsfqTzW3qE8joduTXP3Glm7mzMv8WKnpYzO2/Yv1Pxp4c1TVrPwHpumz61eSWxtP7YuWjt4t00cHmybAXcKZlYqvO1WOQBXSfG/4vfGLxPHH4D+NGqeGb7VNL1eZkn8NWssMcSKZYcHzCchwFlGCcArk5zXmdlpn2NVkhyrp0dOGGfepYLCOMfLHt7/AC1PLFmbp807n4Ef8FB/+CiPxI/4KCap4V1Dxz4F0nw7D4RtbuGzs9JuZpI5pLlomllPmnIOIIU47Ka+c5pBCrbum7DEmv6HtV/4N5f+CR+r3TXFn4B+KWko3/LvpvxJVo1+nn2cjep5J6/SsXUP+Daj/glHqIfbrvxwtdyEfL420yRQSMZwdMBODzjcMkc8V1RqRUbHO4yb1Pmn/g2aXwwnwv8Ai1dWfiCxk1q88QaWLnS1uV+1R2dvBN5c5izu8ky3UqhsbdykZzX6dluf881+QH7cn/BFz9oL/gl/f/8ADZn7Bf7QmreKPD/hOVry9u4LZbTxF4dtyMNJcQpmO9tMcTMoC7C3mQ+V5jD6W/4Jr/8ABaX4c/tXxab8If2iP7O8H/EebZBZXIfytK8RSdB5DOf9GuW6fZ2O2Q48piWEKxK+6Kjpoz7oLjPJqMnJzTpI2jYrJlTuwysvINRyDIqyhrNj+lSLLhRmoG6n60o+Xp/47QBYV+easW75qmh5+tTxHZxmgDRgerETkjiqMD5HX61YE2OP50AX7W6ktZluLd2R0YFHHVT7V8xftW+HfBfh34qrceD7eO3l1KxF3q+nRLiO3uWZhuXH3A4HmbexJIGGAr3rx1460f4ceD77xprzboLGHcIVOGnkJwkS+7MQPbk9Aa/P39oH9qvw38OJrzxx8RL64vNa1KYy/wBm2ODISxwCQT+6hXgAnOAAAGPFT9o3T0PVLQyOg8yNl443pjNXo0jjPGF/4D+VfG/jf/gof4607R7a48D/AAptdPWeVY7i58SX0ivESATshKRtJtBzl1jXjGCCM83pv7TP7XHj7Uo9YtfHm20lyttaaFpEEAU8ZIluwVkYZA+6cHIGcsa6IYerMXtIRPvKQwoEaT7qkM3HPrXG/svWyw/sz/DaMctH8PNCUsepxp0HP+fQfj4LL8X/ANoCDw/DBJ8SLm12osdxI9paXFzKG4Zv+PdkB4JJ3gc4C5xjq/g58ZPFnhjwjpvguOOPUItHs4rKG3uLY28sVtbxiJFBIClsKoGSC3PXBFb/ANnYjdWI+sUtj6FuW2Ar5nv9P5/5FY+rXAij+9kVn+F/idofiie10kW91p91dyCKKHUXiC+acHy/MRyoPI+8V6jvxXpmqfslftMXVstxb/BPX7iJ03RPb2PmK4PIKlCQc+orjqU6lJ2krGkZRnseD+OdZuLXTb2a1k3PJam3jjfdgSTMFVgAfvKofn065BrFtbaSz0+O3uJJJHjjAd5W5J7kn/P61Y8bGS88RWOjx/MI3e7nU8Ebf3cefx8zrWx8OPAmqfEjxvp/guz3Rm+uALi4Uj9zCPmlk+oUHAPU4Fcxrynvn7Ffwz/sHwtdfEvVrf8A0rWP3Gnbh9y1VuW/4G4/KNCOtezTz7jTLay0/SNNt9H0u1WC1tbdIbaFPupGoCqo+gAqKR8HrWkfhIb5mK8mD/OoJZMmlkbHWo5PlO6pk7iIpQjDio0hVmp8h3HHrT4kVV+WpAPL460gix8tScqelIW2jd6UAaHwr+Pvhv44fCnTPjN8GLdfFmgalYXVwl3oeoRSNKbZ5IZordZNv2lhNDLEoUjcV4yCK534Rftt/AX476heeEfAHjCa38UadHu1jwL4n0yfSNf03jJ86wu0jnAXjMihos/xGukS/vFhVY2j2Km0IEOAPQc8Vx3xc+Dnwl+OF74f1T4ueBNP1m88JarBqfhvUrjKXmlTwzLMpt7hMSRIWUeZGG8uQcSKwrTkOc6u+vIdVMsV7bx3EU8bxXFtOm5JYmBDRsp4KsCQQeoJFfz0/wDBSD9jS4/Yj/ag1v4UafZyt4U1NP7V8EXVwxcS6XMxCwMx6vAwkgYnk+UJMASCv3Wf9qz9lnW/Bnhv4pfD/wCKiX/hPxTrtvoOia3sluIbjU3lkgWJpY4yIlaVDGJZtsZkKr5hMkYPjX/BW/8AYw/4a7/ZYurjwjpP2vxx4H+0at4VjgjJkvIzGPtmnjuTPFGrxgDme3t+QM1rGRMo3R8hf8EpP+CxPjbwwY/2e/2rNYvfEWjWcK/8I94nmYzalYW44MMx+9eRJ8pGczKu7BkVVjH6teHfEPh/xl4fs/FnhHXLTVNL1CETWOo2FwJIZ4z3Vhwe4I6ggg8g1/MT4d1268Ka1ZeMNDmVrmxuRPbEH5JRjlT3wykg+zH1r9GP2O/20/HPwI+y+KPAerfbvDes7bi68NX8x+z3jMMccEwTjGDIo42YYMF21M+aHoEZXVj9ZXj+brSBPT/x6uL+A37Qfw4/aO8Ntr3w91CT7VboP7U0S7ULd2Ln/nooJ3qe0ikqemQQVHfx6beMf+PWT0+4aXMiiBEyf/selTKMferQ0jSLwXB8yzkUBf4kNNvtMvFuG8uzkwO4Q0KSYEMblfmqSOT5C34/LTWsrxF3fY5PX/VGvhf/AILCf8FSbX9knwVJ8BfgfrQ/4WlrkQ3TRYY+HbMgN9qYc4nZSDEp6AiUgjaGoPUw/wDgpR/wUg+H/h/xxffCPwjqUOrzeE3Md3ZwXA8ptTOVPmt/0z5jCjkt5oHJ4+EPGvxi1bxvqaa58QNet7O1icTLp6XOJJ58YVpQOQBkjYpCxKCmCW/d/Mz+LLrRXa+kuHutWuJWma5mmZnhduWlZs5MzdS2eB0IJ3U/w3JrmsXWLFYix4mvLxwkcY44y/yjPTkHPYAYrqp2hstTKUrn0p4f1SxluWuNLuDd3E25dyxmO3SPJJwXwsvCk7I/mPzEe/qXgHwzrhsk1i4je4vGUkR2TMyp0zJ8/wDskE7QP9rCkk+C/CPwxo5vlkhvNa8RX1wzW6w6DDjz5DglS0hDOM8+UDnhThQcD1MWnw98IXMdlefBqG71FTmb7TrRunTqdrG3ikWIhj/qjJyc5IGCfSw611FKVz1qwTULTTTZ6l4dm2C4VoZotOkZl5G0tCJJlGCASenQ+pGj4b8O69A8OoL4mvdWkOSsF9cmZcDJKxRynNpyQCIVC7QUA6A8Ro3jP4maxatD4f8AhprUNrMsgkuXvJ9PtLYkE4QRuwP+7Im7BIJ9M3UNJ+JkepXFvZ+JPEVveLcF1l0G8kZTkg9kiUqzbR8yEqG4Ar0onLvqexWOra5eQrY6XbvqV1GrLN4a1EhryNcDcwD4aTG7naysR0U9a+hv2S/22/H3w3S1+HfiDxpeX2g26iK3hi1CZrqx6FlG+Tf8m7JikzIAwwT3+IdG1/44axdQ6H8XPANjftazR/ZNXlnjO6LzctKs0G6RGRjnJ2gGMFlPLR97r154Z+IGor4fvPEHl65ZMESebbDdxSg7QA8WDPksXMchbk/IyiTdFjiKdKvDlmbU5ShK6PrP9rH4b6Pa+M4/jh4TuI7rR/FiAM9tgxQ3i8uB2xJ/rR6sZh0Wu+/Y7+G8eheGrj4lahb4utYXyNP3DBS1VuT/AMDcflEp714r+x/4rXxz4c1P9nf4n+NLORtcgD2V1u+VdSBP2a6XnKFwh8yMg7vm2EBsHqfhx+1h4g+C93/wq/4uaDJJZ6PMbFntkUXNgY22lSOBOoP0OOctwK+VxVF4ery9D0oVPaQPqGaXI4qvJJz1rO8JeN/CfxB0OPxJ4L8Q22pWEpws9s+Qr/3WHBRhxlGAI9KuyHJrEBrP/E1Qyycc1M23/wDXVeVc8LVRAbv+b3/3akE4Q1XLYYinfShrlYE5nc0jS8GoaxPiT8RPBvwj8Daj8RPiBrUdjpWmQeZcTOu5nPRYkXq7sfkVBySakCK18P8Ah/w7P9o0/Q7VZFbKuLdVLfkK6a1vY5oFubXEbHldvBB9DWJHaXF1bLH128Ruy9PY+38qfp981pKbO6jaHs6sOhraPumckj5g1z4A+Dvgf+0D4h+CviTS4ZfgZ+0i10jaOXMSaD4xKea8EDYxAL2KJp4CMFbq2CR7fLiDe8/s1eMfH0vhq4+F/wAXNUa78ZeC5ksdR1Zht/ty1xmz1ZR63EQHmgf6udJ04AFaXxf+F3hH42fDfWPhT44e6XT9YthG93p03l3VjOrLLb3lvJ/yzngmWKaKTtJEp5GQfAh49+Mc1ifiBe2c138XfgXdvY/Ebwxo/wAsfjXw/Om9ri2hBAJuoIhqFmDzFd21xacZlzPwu4t0fnz/AMFmf2L/APhmf9pWX4geC9JW38F/ESW41bSY4YwsVjf7gb+zAHQCSVbiMcARTmMcQnHk37IfjWSXVv8AhV+oTLthml1DSyT6xMssI7/8tPMGPSQmv2h/a5/Z/wDAH/BQb9kK88D+HNc0+8XWtPt9d+HviTOYI74RF7O4DdRDMkpik4z5NxLxnGPxf/Y6+FvjLVv2to/hyfB98niLT/t9k+hSxYuYbyNhbPAwzwyMzBs8DByQATW3xwsznkuWR9f/AAp+GHjj4p+NtP8ABXw5t3fVb7IWRZjGkEQ5lmlkH3IlHLH8ACSAfr25/wCCXHgO802FtL/bQ1iyuxCn2hNS8GzSxGXHzbTFdAhd2cZBOMV3f7Nn7PWg/s6+BTo8MkN1r2pIjeIdXiXhyORbwnr5CH8ZG+c4+VV76aTaKyjBdS436lr/AIJn/wDBPHQfAnibxxcfED4iaV44t5rGxTS5LaGdPspDXBk8yOXoWzFjBP8AqzntXzL+2/8A8E/v2nviN+03fax+zz4VhvPDtp4kt3+y2niu0tZY4YtPYN+5nnjPNwV+UdQc9OR+iH7D3ifwf4V0DxJd+KPGGl6bNfXkUFnHqOoRQvOViLERhyC/3u1eH/E9VvPil4ivOPm1WX5mX0OP6VHL79hxumz4d1n9i39uDwhE1xrHwH8bttGW+wQSXmfXm3eSvyf+Olh4o074m+JNY8fTSf8ACRS63cJfRX+RJburEESA8pjhQGAxjAHykD+jrTNd1zS2X+z9UuINrfL5MzLj8iK/Gv8A4LRad4P8bftTO3wn0s/2ho9sqeIdYXDNc382DDCGIJcxIwIckmPzflIAGeijTlfQKj5kfENvYx28/n3FvJczEHELIxJOc7nA59Ds6+uMCt3w/d+IPtcck1wttt4ineJVMIOARCo4Q9weuf4uSD7L+yl+wt44+LNyniSbzJNPcYRpZuA6jlyPY9M+4zkV7l8VP+CZuqeDNP8A7Y0eS4kRhufb0HUnnPQNnrjjvnispY2nCv7NvU76GUYitQ9ojwn4Z+J/FGlQxW+gXNmsnEaX3iHVo1Yg8lY0SP8AdDBOAFB5wWO3I9h+HafECyj8zRPHmh6VK1wWiifRWkW44+U+ZG+4gYwAPMBPPUCuO8PfB74geFNVVfD/AJRuI/lEMyAeo4aRG68joRjPYE1s6l4w+L/h5l0vxRoVpb26PjH2Asu77oxsEKjJOOcdPbFe3hqkbHm1qM6UrSPdbLw78bJLPbb+Io7bMJDnT2WJZeOqtIkLBhycZG3IyD3Zr8vxR0tF/wCEosb5ZJlO+aw1Jpnhxn7wjlkGcZBQAjGAM5NeRWnj/wC2JDb6hp9xHaywnd9j0kTRLMu0tKRFcyRIfXKMfX2Zd3lwtsbjw/p+j7osyvdTaFiVQCBmOYlU2ngEo3DMM4GAfUVT3dDl5bSsyLxd418beL5PtXhO10fUTbz5t4YN0F1LJF1C70B3BRg4gwwxmTFcvqXxg1TULl5Ne0G4W9jjMXnN5bvauMhUk6ZUHaOB0JA65qP4k+D4/FNtceKNDtZLG++/dTWsrfv8DJLK58xOT9/aQuRwBXk914lvJ7htK1z5riFsJdu+DgcFGKZGB7fUAY58uvUkpWNoroe/fD/9qXULLxFaaXrGoQ2MkliqWNxMg2xEHoTnICsOo5w554Kj6q8Y/G3w7+0H8ONM+L1xqENr4ts7iPR/GGlPJukuD5crWt6p/wCWmYoJIpDk4MUZJzIa/MlvEcllemPVN0htZmazljbnOefoeuQOODjI4r3j9gaw8dfEn4+eG/hb4L1Cxhm1zU82o1W58q1VYd0kwdsEhVWHPygnDEKMnFebiG6kNToo+7M+pvCXj34q/BfWofGXhHULzR5JkQ7pbaQQ3cfJCurjbKpw2Mg8gkYIyPqL4Ff8FBPhf8TddtPhx8QLiDw54puLRp4Q5IsLwJIkZ8uZ/wDVMWlTETnnOAzHIHq2hfB/4OWnhlvCfxe8Nv8AEuSaSOa7vNbvJ7KFZVzj7PDbuvlqM9WJYnnjgDM1D9k//gn3qWrLrl5+xnZrdrbSW6TWnj7WYsRMVZlwLjHJjU9O1cMY2idd4nbu02/y1jO4fw7eayfE/jXwf4Mhabxp4w0rR025Y6rqUVuP/IhFVrz4Mfst6joVv4Vm+HPjqHTbaHyrewtvjHq/kpH2Xy3JBA6DOcDgcVx93+wx+wHeSm4k+FHjO1dmyXg8cmQ5/wC2kR/Wi8ok6GZ4p/bi/Zf8LFo5vidHqEy8iHR7Ga43fSQII/8Ax6vNfF//AAVK+Hems0fgn4V6zqRX7smpX8Nov5R+cT06cfhXpl5/wTx/YDv1/eWfxUtwev2fxNYvj/v5amvgX9oHwBp/wq+NHij4d6PHdNp+k63NFpj3rKbhrMnzLZpSgC7zAyFioAJJwAMU/i3Eey6t/wAFMv2iPGWox+H/AId+C9Dsbq7mEVjb2OmT313NIfuqgdyJGPYCPJr0LQP2Lv2kv2jG0/xN+3R8XL7TNJtpPtNj4Rs0g/tBiwxkxRILezJU4DsJJQCybVya8H/4J0/E638P/taabY6f9stbp4brTryaRFVVM9qzxeSQSTkiNC52kbiAO9fonLdZPmbjk8lvU0h6jUg+zf6RDqF5ZdzFcW+5R+n8uP5VoI7XMHmeZaXu37ypIBn8+n4/4CorXxB4VjQR2eqE+nk3y7R+HmN+laNu+nXKCWTVLdsjIZ7Tew/FEB/nW0fh0OdtyM+4Vbddv2Oa3XoolGVI9jXk37RngvxRpWp6V+1B8IdFuL/xb4Ks3g1LRNPjHn+KPD7N5tzpi9C9xGw+1WnPE6GLhbqWvals7MHy/wC0Ej90e4UfgvluKgm0K4im8+zuFZeu7bIMH6lBScfeGuVHh/wG8c/D7wD4k0Xwf4D1j7X4B+JyXXiD4aalD/x6RXUoN7d6ZFwPKWRGlvoIiBgC8iAUQKta1v8As5fAD4R/tRa1+15HpcNj4p8fWFrol3qE3EEVyAdxj7RS3axQIxP+sktsZ8yciTj9a/ZH8V6b488WeD9A8TrY/DDxg0niO2k/tCNbj4c+LYJluU1LTEkAH2ea4DXTQhyonLDZHDLN5nrumeHbv4zfA2Hwv8a9L0hbjxFoKReIYPD1+bqzjmIBMtpM6DeocLNESOPkzkg0480VYJWlqdHKu0/vM5HDe1QSjcetfLfwn/aX+Iv7O3jm9+AP7TGqf2tbaPcC2ttbhsZPtMEfBikOZGa4t2jKuv8Ay1VWx82BGPp/T9T0vXdMt9a0LUoLyzuoRJbXdtKHjlQ9CpHBojK4mrGl4W8YeKvBVzdXnhXV2tGvbOS1udsSuJIZAVZSHBHIJGeoz1rPiRix8w5Y87j1NSBOOTTlXA/nVCIyZAzR/L83Tf0HY59h/WvyK+OvijwfqXxJ1688uQxy61fanHf3aYkkkmuFIbbjgqkkPXnqDznP67vF57eXHIw+cdOvvj0yOK/Cj9svULyD4ya3b2NqsHk+J7uwl05R8qASLCuMdgYg/H8TZ5BzW9GpGlGTCMeaUT9Mf2IPhN4b8MfAbTdY06zRZNXXzy/3iEP3R+X54z1r6Itfh1a6/oSxSW6hSpPzDqOleO/CW8t/gl+zx4d/4SaOaa4t9Kt4fstvF+8ll28KFz1wPwqO5/4KC3XwxjaPxV4PtxugFw1umoxloY+wIcDsPvA9e3TPyFJVcVWco7s+6nUhhaCT6EXxc/Y90rxDLJ5ejxqrfN8mRt+mOleMeMP2X/jl4cjkbwreC4hQHyYb9WkXGOwR1PQAfQdO9e+/DX/grR+zv8RvFUHgPVNHu11C4dY0a3RdrSN92PcSvJ7HjP14H054esPh/wDECxjks/8AlpDv+ZNvfke3Tv8A0NdCxGNwj5YzszNRwmLSc4J+Z+OnxC8F/EzTbkXnjD4L6ddOM/6Ravdwsr89Sjlto6YBA6dsivM7fxLHpt81wmnzaPNGufMufErShCuOWmnj82Mdso4B5GB3/cjxP+zh4F1QH7RYrtLbtzYIHv0r5P8A22v2Rv2ffCvhG68QeJPD8k8ijhLGL98+Tj5TwR1wOR6dOK7qGf42ErVLSX3M48RkmAqK9K8X96Py21fxVp+q6rJHeaw1rqUkzMN8K3MM/PVZwgnQ54+4V9TjOfN/FV3HoHiHzNQuBOsajlYyGjyTjhwD3OGxtbjk5Fd78cPhl8O77V7yTwM0dqd5ZYp5olfPfdGBtccYyQCOORmuX8J/AT44a9a/aNL0+a8tYX/dMl0FDEf88w5xnoMKQeAea9qeJp1KanLQ+Y+qVvbckdbdjz3xBLcaffTTSXCvp93/AKiVm5iI4247/Lxj2PoSfvr/AIIGfDC38ZePvEXjrxJK/wDxRM8Nzo6GIcy3MU0LNnrjaD27nnjn5t+Mv7JHjzwr8F9N+NV1cafqGhyxIQ8SeTc20gk8oiVHAwVl/dnk7cKMY4r7e/4IN+GbiDwl4+8eRw/6NeNpdlE+8cSILiV1PuFlhOemGxzg1zKtCcPddzSeHqUalpqx+hj3OW/4FSNNntVMz/Nz/eoM7E8VjdjLYmOOKTzk96rLNu6YpSSTk0J2Alebt5n8VfE//BRjwjb6V8Z7HxhHbfLr2iRl3/vXFufJb8ovs9fZskjYZugUEt6ADvXyJ/wUA+Lfwi8e6Zovhvwn4st9V1bR9Rma5ksG8y3hgkjw484fI7Fo4uFJwFOcd0B8z/BjxHb+Af2k18WLJtXTZtE1KVvZZ7gN+BWAj6Zr9Wbj5JWjz91iPyr8rtG+EHxKn+Jl/cf8Kz8RNDd+HNPWKRdEuNrFZ70kA7MHiYfnX1xpv7Wnx60fT7fT9Y+Ac8zwwRxmY2d4jSFRjefkPJxk+9BVvdLfir9rD9qz4cTrY/FD4N2ulsTtT7fpd5arIf8AYLyFX+qkiqFv+3j4wT5NQ+E+jyN7Xsoz+YNfWWm+INUtrVrFL6T7PIu2S2c7onHoUfKn8RXmfx1/Z58F/EfS11Lwn4V0fRtWt97E6bp620d5nHEqx4XPHDgA8nr0p3Zye/ujyXR/2/rPVI5JB8EdNkWOaSGV4tXyPMjYqw5hI4YEH0II6ipE/a08BvcfbJPg/qNmzNuxpfimeBc9fux+WPwxivFdK8Aav4a8YW/hvVLOSzzN4huJ7OZSDvGpxGEkehSdiPUMDzxXU/8ACKxlf4Vp8zHzTPYrH9t3wHOjabqnw71WSG4HluLi+UqFbg5KSR8YPJPb8c+i6p+0Ha6DDeXU3gvVpYbHTpLv/QtMKQSQRLz5Esnlwy4UDARicYPQ5r5WHhOMMf3Y/wCBV59+1B8NviP4m+Fc8PgPxpr9uunxs9zoNhrE62t7Dg7/APRw4R5QCxGQcjcOTtFNSkTzW6Gr8VP2tvg3+3P46074lfCnwnqmmNoOgvp2qnW0iivC0twzJFLFFJIgCCGQq+4kicjjbgb3wg+PHxC+CfjbT9J8N3iXGi3lje3OpaPecxTSI9sqNGesTjzT86/ewA4bAx8O/Ar4mL8HfiknijVXMeg6sY7PxOi8JECT5N5/wBjgnGTGXwORX6OfAv8AZIs/i1q0njzxF8RLXTdL0+M2SWGnxG4v5XkKSyNtOIo4mCxBZCzElX/dnbk3KFncmNRy2Ott/wBvSODH9sfC/avQyW+u/wDsph/rXsXgLxd8QPHWkDXB8D/EGm28ibrZtUurK1a59PKW5miZx334CkdCasfDz4U/Cf4Rut18O/A8EF/H/wAxzUmF5f59VlcbYP8Atisf410FzeTXUjXFzcNJI3Lu77mb6nrQrmhzOu+N9W8Jyw/8JZ8I/HNnDcTGP7XD4WlvLeLEbSZkms/OSNcRkby2CxVM5YA/lp+2/wCA/B/i3/go9at4BnkvrDxprGkukL2zRNBfzTwrOpjkRWIIRm6EZkHpiv1y07UrzT7j7Rp93Nbtu+/bylD+Yr5W/b70K38Z/tUfBvxtrGqeZq2k63bRCWZt0sls024Nu68MroM5xk+tTVk4Um/JnRhKftK8V5r8zvPHOg2cevad4k1ST/QtFtpXjiEhQNK+0Bjg9gDjg43Gsmb4N+GfjzLPb+KW0yS9T5W0i205r26hVh1aCIM0ZIxklfTPauk+N3hbxtq3gie58E6e1xcgEwwrMUJcA4OQRj6gg+hFfMOq+A/2mPij+zl4m0vRf2gdS8F+MdJNv/wi/hDTtQ/sjTb2MSqbiKW5TblpkDhQ7gKzjfK2dy/I4WnKpVvJ2Xr/AJH31R8lG6V35ajfiH+xJ4L+CnjVdY8H3Frb6l5xe20rUrOaynY4y3kLKFMgwOQu4DHOK9y/ZE/aB8RXXipfBOsRyw3TLtVZP4u+fr/SvA/2Qf2J/wBpbxx8OPEcn7SHxY1bSfE11HaR+E/D1h42g1q1jaLzDLdXsTzXUG1wYAAmWHlGQqGKlvrj4Nfs/XHhM6FJ45t7OTxRYySQXeo6azeTdRiQeVIA+WRjHjcCWw27BxiqxuHnCfNGWnqXhatGdHllG0vuPdPHPifWPDvhj+39Sj8mFId5f2HWvhH9oj9p2T4uz32iW9jGun24eO5vriby41j6MST0G3vmv0A/aT8Px6z8LY7eO3/1kIV9qYyO/wCdfGPiX9j5fHOt2MdteLofhmySOfU/sDOL7ULjdIWxJ5UnlKB5ZUp5cmQ48wbgV53RnPEciZvSdCnS5prU83+BP7CXwj8byr48k+GV9rVxH+8S4XRVjhBH8SC7kjEo7hkDA8c9K7DxFoPgfwF4jt9H0/SZtMu5m2JY6lo7W/nAAnakmPKlwMuURyRjJHGa8D/as/4JpeMPFP7RE2qfsx/HDw7YeDdahhS9Hi7WZrjUvD7mOITGEfM9x80XmxFGBLStG21QJG9j0X9nj4g6j8W7XwX8I/ip4g1PwZpttE0i+M7kXcs8qdxIULIDyRkbgejbcKvdiaEVSVp6+pxUK0p1nzU7R9Eip+138IbO/wD2LviJZwaeqx/2Rd6lbpEpbaQBO+PqVY/XB6mu9/4JTfD3SvBn7DPg9tPs4VuNXa9vr14Uxvka7lUZ+ixqPwrv/wBoz4fx6T+zP4t0W6Ut53g/UIn2LyCbSQGvN/2Of2fvC9v8A/Bfii8+LnxGsby60a2SPRdH15YdLiQTSHdsKFgzEndgjjaAOMnuyly9g0+/6Hg53bni/I+l9X0GbTI4pJJM+Z/s9KzJDtNel/th+Cvgn4j0Pwnp/h3x54g0l7Vrj7e/w98SxW87vtTC3BdJcjqRkDnPNeDeLfgV4b1DwzeWfw/+Pnxasdckhxpt/rni2G5s4pe3nRR2yu6HodrAjORuxtPqHhxs0dY93b2dtJqF5cRQwQIXmuJpQiRoOpZjwAPU8V4L8cP+CivwZ+GI/sfwXJ/wkmovL5EVxAzR2CSnoDPgmQ9xsBjPP71etfJ/xy+Jvxut9fvvhn8aPEGtS32i3JS40rUb1pI0lABVgMlXDKVZZBkMrKQSCK+3PgH+zB+zP8PdA0j4geDdLh8aahcW8N/pvi3xHaRyKm5Q0c1ra8xW5wcgt5synI8wcigeljxnTvhj+2R+2dax638RNc/4RDwXc/PF9vhktbWePqDBZg+de+oeQmPriUV7j8Hf2a/gf8Amh1Hwf4XOsa9DgjxP4hjjlnjb1t4f9XbY7MA0uP8Aloa9D1C+vNRuXvdQvJJppGy8srlmP4mqMx4xQSdEfiz8SkTb/wALC1xVAwFGrTcf+P1nar8ZfiQEaO38ea0zEd9Vl/8Ai6x5BmPavFMWzjPzbRQBvaTqljqdol5pd7DdQSDKTW0wkRh2IKHBqfUNVs9Ns5NQ1S8htbeJczXFxII40HqWPA/GvKbHwHqlvOLrT7hoZP4ZYn2sPxHNWZvh3qmoyfaNUuGuG/v3Dlz+tLUzcUup4Z/wUB+JP7LvxC8G3dz8PP29PBPgb4j6NZyvpN9p/iiwu/tQ4ZrK5g3sMSFUAY7WjYK+SokVvg2D44ftyWkLSaL+2p4R1Hy0JVFttBm3+gyFY1+tP/CnNDvV26loFjcf9d7NX/mKin/Zg+C+qkf2x8H/AApeZzkXPhq0kz0/vx1pH3d0ZyjzbM/Lm1+NX/BS0DOn/EPwtqy/3h4Yibd6f6iHFXIf2if+CoVjukk8F+ErwREfO/hq8XPGf4NuO3pX6XzfsQfsp3sfl3v7M/w/dRn5f+EMsgOevSLvWLef8E3f2K7o+Z/wy74Lib+9aaOsGPp5RXH4VXPDsZ8ku5+Sfi/Q/jPqeu6v4m8YeB9E01rybN3Z29lPa2u50BkCo5kIBLHPzYyvGAAtd18CP+Conxw/Yp8V6Lp3iXwno/iDRYbCx07Ul33MV1daXDJM0YSVyI/NQTOisy8bFBGC2f0e1X/gmz+yK0Dw2/wd+w70Kh9P17UYvLJBG8L9o2EjOQGVhkDINfnX+0D8BPFH7O3xYuvA+ryX39o6Bc/btF1DS9Sn059SsnGPLSaL5olnQGNtrfI+9STsreHLWVjKSdOVz9f/AIU/Fv4ffHL4c6P8W/hR4og1jw7r1mLnS7+D+NOQVYdY5UYNG8Z+aORWU8g10MkzAc1+Pnwk/wCCk9l8F4JbLwP8LP2idAtZrnzprNvH9prkPmEDnbqelyKCVC5IwTgexr2Dw/8A8F5/CehJ5Xir4SfE692oC76loWmFwPXNubVR0/u4qXCa6F80X1P0ft52Mn3f/Ha8G/bZ0CFvFPgH4heWvnaT4q0y337SW8ma6JdemP8AWRwnnAwG/H54s/8Ag4c/Zht7qO31b4J/EGFDnzpjZ2aFPmUcJ9pbPBkPUcqBzuyvaeGf+Cjv7KH7f2k6/wCBPhamvWWv+GdBHiC3h8SafFbGT7Ne2jHyDHLJvYc/JxkE8EA45cXSm6Oh6GVThDGLmPuD4T/Ydds10+ZRgABl9a0Nf/ZT+HuvXralNo/zSHL+TKUDfXYRXnHwT8bwy20NxHJ/rFBX16f1/rX0l4M1u31CxSSaQE4B+avlaSXPyyPuOZr3os5TwV8FfDPgWyaPQdDt7UHltiYLH1J7/U5rLFn9p8TjyIwWjl+96c16t4jnhttEmkjwzbCQyHpxXjfhH4leAdP8RQ/bPE1q11NmV7fzlL7d3Jx1x2+orbE9FfQrDp3cup6V8TdNkbwvZrLb5XGHHPHf+tcV4J0LT5buS3WMYZcYxXdfFb4y+AbvwjH9n1SNPJj3FmI6Yxx7f59K88+GniXSNa8RW82h6pFcLNEW2o44xiud29unFnRJSlS1NXxP+zp8P/F0q3OteFLO4k2/LI8I3D6HFWNI+EXg/wADWLLo+kw267TlYkAzXo893bx2I3Rj/ePpx/jXDePPEEdqskcdwrKq8bfXrWlVpa9Tnipctmz58/bQ1GOD4KeLIYbjyt+g3cKHGdpaJlB9OM5qr8G/B8fg74P+F/CdxbbJNP0O2R0bna20MR+BJH+NZf7RmueH9Rs4dJ8QSJ/Zt1eRfb/4swofNkGO5wmPxqL/AIal/Zvt7dIV+MmgqEQAAXLbRgY44r1sti1SZ8/m+rjY7u7llu51kvLiSRlARXkcsQB0HPb2prHB/wDiq8t8Sftpfss+GbKbUNQ+NGmz+TbvP9l02Ce8uJUUqCI4YI2eQ5ZRgA/e9Mkcv8HvjR8Qv24fC+u+IPh3qniT4P8AhWz1VdO0vWL/AMJQXev60Qu64khiuZRb6dEm6JBIUvGkLPgwNFz6R4Zm/wDBSHwV8E9U+Hdr408aeNtJ0HxpYQhfDsN1dRrca5B5mDaCLO+RVJZ1kAIjO4EgSEjmf+Cdf7RVnpWgXXwO+IHiC1tIbXdd+G7y8uljj2M2ZrTc5AzuPmqO+6b0Arr9B/4Iz/8ABPy68R3Xjz4s+EfHnxU16+kEl7rPxP8AiJd3E1xJxgt9gFoXxgABmYYGOa9i8Gfs2/swfBdYf+FQfsvfDfw5NBxDf6f4IsmvF/7epY5Lg/jIaOaOyCMn1Rr2+saTqFm2qafrFpcWyjLXMFyrx49SwOKyZPHXgOVDJH480MqvVhrEGB/4/XVyeMvFn2gXH/CUaisi/cdL6Qbfpg8fhXJ/E74dfB343QyW/wAdPgf4H8a+YNrXPiXwraXF0M9dtyEFwh91kFAElt4t8F3U8dnb+MtIkmmOIoRqcJZzkDgb8nkgfUj1rN1n4lWtt4nuPh74N8L6p4o8RWtsk9zpWiwqI7WNvuyT3MpWCJCSOS5P+yeK8gg/4J+f8E3Ph/8AHLwJ4907R/EHw+11/FtrZ+GtN0jx3c3Gn6rqEr4jtTa6iLqV1bBciOWPaE3ZULXoOtfBZ/gz/wAFQPHFtDrlxdafr3wV0DV4LB7rKW8ralf2zYjzxkWQO7HOcZ4qr8oRfv2Z7tF4OgQYMKf981KfCsaf8uq15Fpn7cvxglKpqn/BNX4xK3f+ytQ0G9A+mzUBn8q3tO/bF8cXzbbj/gnD+0xH72vw1jvR/wCS11Jn8KVmc8qive5348OIP+WCipI/D8Zf/j3PTtXL2n7UNw77dZ/Yr/aV0odS+ofAPWgq/ikTV0Gn/HjwHcp5l18O/iZYgqPl1T4R69Bj677PFVyzJ5kX18PxnrG/+ztpRoOB/qmrPvP2kfgNpQP9ueLLzTlX739peGNRt8fXzLcVUj/a6/ZHZvLk/aI8Jwn+7dakID/5EApNE+0Rp3Xhfzl2/N/3zXjv7Tv7Dfwv/afsNPt/H0mqWtxpjSi11HRLlYLkRSjDxFnjkBTOHHGVZQQRk59isv2iP2Y9TYf2f+0R4Fl3cDZ4rtMn/wAiVq2nxE+Deqlf7P8Aix4VuPmH/Hv4jtW/lJVKUoyuhOUZKzPhjVv+CNPwraQyaX8RvGCFvvLcTWcin/vi2Xrk/gfpWDq3/BHW3ktltdP+KmpLtctun06N9xO30KjqCf8Ato3rX6O2Ufh7VB5mm6xZXKnvbXkcn8jVv/hE45Pu25b/AHRmtPrFbuY8kH0Pyt1T/gjT40XVLW40f40JhZCty8/h4s3leUcYIuRk+asJx6Bue1dX8H/+CXfi74SePYfGF78UreYQ2k0NoieHlULKVk8kkSSspCMVYjGW8oDvX6RS+Cmz/wAesn/fqmXXgLT9UspNP1PS1ngkXEscseQe4/EHnPUEZHNTUr16lNwvua4f2dCuqltmfH/wS1fUND02PQ9Q8lrrT5TaXv2f/V+bEdpx14wPU+mT1r6Y+GXirzrNIt7f7LFv6f5614L+0n4G039n7xzZ6h4e0NbHRdTswoSFMKtzGxLE5ycuHBySSSCfWug8CfEy3t9LjvJLpY4/K3l3f5QmM7s/5xXyuLpyo1j9AwdaOJoKcT6lbUrefSvLuZiyuuHWvzU/aH/Yv8L65+13a/EC68cTSQ6TJ5mgXOnX80c2mTPINokNu4MnAIAO3cuc7gCT63c/t13HxP1qTwV8M7tbez/e2suovJtflWXzFOCoOSCvOP3ZJJXAPFeAfEvwH0/4gxeKtU8YNrmowlIL67bUVt7K6CqB5RmId58FTyqqpyRluc7OhKUVzaM3w9WpGTUNblb4pfsxfEDxf4a1PRPih8fLrXNFuLZre50rSTLZtcIVPyNMjhtpxyAfmAwSc4Ps/wDwS++Cmj/BbwLHpeqeMJr6+WH7NpumzEBrO3RjkH95IXbIxuJxhQABnm1r/wAWP2d7/wAP6jDF4X0wedbCG/XWNe85IEkwwCmJImcDaMEsCCPYZ810T40/Buz1iXw/4P8AiVb+GtbS6e5s0i1VZtPnJXDKsnWN2IzsfdkDG6pjhYwdzeVfGKm4yWnXQ+6fEWtQx2H+sA45G3n1rx/4ieJZDFMzSbV2n+KvJPAX7a2pa54t/wCFY+OGhmuGiR7PU7fAW4znsOMjuRhTkEDnAu/F3x02naFcXnmKm0dX468Y59c4/GuWrTqKrysinUi4XR5J8dZfAnjzQdfn8e+LJ9P03SPCWq3ckVlfmC4mme3ktoEiKHd5pMx2heRKE78V+T1x8PPjfaSNFbftD+KV/uJeedOp/wCBNL0684HCk4A6/b3xQ1Wx8R+LdT8QXkrw21qqW4+0HAhSIHzM9vlcy5PGcEnvXnnxM8W+A/hR4Uu/GnjS+8u3t1xDBGwM15KfuxRA/fYn8AMuSACR9JhKbo00fJZpWjWradD5ci8OftNw3kd1pfxrNxMsTpDLc6YCfmKEjmNupVPfJUDkkV+qn7MX7YP7K/hL4beGvhPZ6/r2lNo+mW8Ek+v6HJuu7luZp2ktfNj3STs7HBC5fAwBgflRc6F4o+MXj7T9a8aaTDdeJfE15Fa+FdAluDHb6ZA0oCySnrsXJJGGJy7lTlgPsn4K/syeF/gn4bbR9Duri7uroRNql/cOf9IlVSMrHkiNfmOBktgjLMRmuiaPNpKTl5H6YeC/iX8MvG9ojeC/iJoWqNIAVhsNVhkl5APMYO4cEHkdx61F401zR/DNs2oeJNYtNNt1zm41G5WBB/wKQgV+ft14Dt7oeXcfvF64cAj9az9U+HOk3E63V5YwzTBdqyywhmA7DJHTjp7CseWUdTb2aPrrxZ+2D+yv4PZl1r49eHZGUkMmmXh1BgRxjFoJTnPGPXjrXmvjH/gqP+zLoED/APCN2/ibX22nyjp2krDGR6lrmSMop452k/MODkZ+dbr4f6LuW3tdCt3dhtSKO1UluvAAH+RmtbRf2c9NunW88QaPayM3P2VYFKjOfvHHz9Tx05PXNVfuPlijwvU/28/2rdX/AGvfB/7Rnj3SdDuPD3hPxra6nYeD9MuubawjkO+FJpFBMjxh8zEfNIQdqqojX1n9t7/gqd+1H+1f8V7T4lfBH9nrw/oOmaT4fXR9+p6k099fRxzzTBswTReWB5+3yw7DcC2R5gUdvpf7J/wxuox/xb/SY18yR2b7Cud7NuY568sTx/8Aqr6M/Z+/YU+BuufDBby48Hwq7XsyP5TFOOOOD6dulVGpCXQjktrc+lLTwbHAR5asp9nIra0698QeHh5+n6hcR7efllNdZonh1b6RVaEYavVNA/Z/8L+ItAa4t7yOO9Rd32eZvkl9vatIystTglT5jiPhP+1t8R/DlpNbXWsSXSRXEcSRXJ3rghieD9BXtHh39tS0uoVOteGF3Y5ME23+ea+bPFngyz8Ma1cWNrY/Zws5dodxO0nHQ+nH61HaTtEV+b/gNdEZRluclpKWjPriD9rbwPNHuOl3St/d3g1LcftJ/CvV4vJ1TSJpl/uT28bj9TXydHqm3iP/AMeNWItamB+Vv/Hq05aXYlzqo+krrxl+yxr6ltc+E+jT7v8An78O20m78xWRdeBf+CfuvPjV/wBmvwBcN/09+BLB/wCcZrwyLWrgj5pBn+7U0erSE/e/Kq9lTexPtqh65efszf8ABMvXyz3n7Lvw4Q/xeV4LtYcf9+4x+lUp/wBhn/gmJPmNfgN4ZttyhsWIubfgjIx5Tr2PavOrfWZiPTH+11rSGrSSSr+8P3EXdn0UCj2EB+1kdan/AAT8/wCCbj82Xgi4tt3GLXx1rkAH4JdgUSfsD/8ABPq2H7tNfj29o/idr3H/AJOVzlvqUzcRyN/vZq1Bouqa6fLs7gKTzudsCtI4OD3IeJqLSyOf/a0/YU/ZbT9lDxtYfCGxv5ddt7SPV9Kk1LxTqWpN9ptQ7iNPtM8nl+YjSxnbjmRSfugj8q/CfxKuLjQH0+TM9jPbmN9rA5Qg5HvkHnt1z7fsVZWdwts2n3nzLIhSUbuoIwR+Vfj7+1Fp/hf9nH9oPxj4FkjaHTLXxJMbNEt2k22sj7o1BAyAu5UBOeB14NeVnOX8kFOJ9Jw1mTlOVKfr/mcT8P8A4C+EvGt9rlv8Urq//su+fzI10rUpLaeA+XgyKydCDk4KlSBgjB2j6I/Zf/Zu/wCCddporeG9R+Fek+J5bZsfbdU1Iz3wcr/y1aR87umMYxzjjArifhL4W0LxzaR65od8v2e6QFdsnr2I7f5+ldV4u/4JqeE/imw8RWuvSaTqKqD9rsLmWGaQZ5xJG6kH+teFSxVSjJqaufb0/ZqalGTj6HtDfs8/8E4/CUVxrGpfBMXke35o9Y8Q3E9u+PujbJKVAHB6Z4xnpXgfxD8AfsD/ABX8axw2Xwl8OzTabdrLaW3hLNjt25IWaa2Ks684MZZieCQD1mT/AIJG+D/FKR2/jj4yeJtSWFzm01LX7i5jIPbbJIRn3OTn8q9Es/2KPCvwl0/+x/BclvbwwrkmKMAk++Mf5/OjEY98loRX4G83TqTvKbfzZ5Cnw70fwz8RrTxRp/7mOxQ/ZLa3O1IG3BhgE4wMHgjBzWn8UviVHY6FNqmobnliQtDDjO9+i57fex+Bqf4nxaX8OrS4vtQ1BWESkqZpdqsfTPv7c+g7H5v+MPj3WLjw/a2cl5DJdXU0Vy/y5NvukEaru6EFTu4C4zyB1rPBUJ1pc8tjzMwxcKFJtbmH8RfEWlad4H1rUNd1SOGCDSrma/uZmwgJjOc+7McYHJLADJIFfM+qeOr/APaf+I9lq9jpX9oSNcyW3gTwT52PL6lr28wcISELMc4VYsAkRljseO/FN/8AHXX7fQ9HtptS0CPUBF4f0S2wJPEl+AT55zwLZOSCx2gBpWPKhfXP2F9O0W++H998VLiRb3XNW1a5tbm+ns/KmsoISqrZCPJ8hQV8zaMFtyFskDH0Mvchc+K96rO51v7PP/BNr9ow/GLTPjh8NNFuvF3h/wAL+G729+KusNeQQQ6ZMYp0inijldSV2TNHHEgaTy4JG25Jr2KaC3jYq3Hb5aqaZ4pv9NsJrO31CaOG4A86FJSFkwcjI74PNZeq+JbeFNzXGP7xY1lznTGPKaF3Nbx7umf7orMt7e88R3x0/R4wQpxcTlf3cP19T6IOfXA5C6B4Z1jxRMt7fRzWtq2Co+7JIPp/APrz9ODXonh7wzZ6bbx2tvbrHHH92NBgY/zn86lz5jTl5dyh4V8C6bpKH7Lb+ZcMoE1xJy0nf8B7Diuk03w1GX/dru6bfrkf41paXYbtqmPA2/LW7pulxq7N3VcH25XmpE9DN0zwvDIBuwvGMewFfTX7Mejxt8O7q38tf3ery/w+sUR/xrwS+1Pw/wCEtJm8QeLtatNMsrdN019fzCOKMD1J9e2OtaX7Mn7eEfirXNQ+GvwK/ZZ+KXxAkez/ALZh1TR9Eis7a6sxKLZprc38sLXCCYeX5ijbngE84a93Uz9+R9p6aI9Ont7fyW3Tb23cYUKBnP5gcetaN349vNLj8u3uNuPfpXMeMD4ttry28Uafpto0VrZzQto4vCJ5UlaFvMEpAiEi+TsERG0+Yf36jrQ8Uaf4g0i1t9Q1PT2jtbwZtLpJElhkOMmMSxlk3juudy9wK66fLI8l1BvjLW5NYuheXLbpGGWb9KxILry5Vkb+Eg1HrF5HFIv2i4VFWCMsXfHBUE/zqB5rmCWSOO3fzI1Hyum0NkA4BP8Asn6A8Egg40MXqXY7naakW6BqrCkjQJcNbyIkjYXeOh9PTNSxxtj2PDc9a6CZPQtxXsj4XrVqO78seZJJtUcszNwBWbaWcMxWaSFSyOQjbemCR/StKCzSVdskYKsuCpXINbrYxb5S5bXsLDd9pU7SA3z9Cela0J3ye2eGXvWDJoUJglkjt4pPlLeVNGu1+vBOwnHJHfgmul03wzHFM8ccaiNpHYxLCFByTk8dzk/rXTTp8xl7SRo6Rb+Y67lr1v4bXuh6TZNHeaRBLuXG50BxXn+h6RIGVvKrN+I/7R/wH+Bt9Dp/xS+Lljo1y5CzWYmE01uNpYNLBGjSgNgAfLzngdTXZGjGehNNV60+Wmm35K7K37cf7VHwj/Yx+DGrfHj4gW7vDbYi03SbVgsuo3TZ8uFT0jXu0hGFXPBO1W/KPxhp2tf8FPvgpaftGfbLPRfFXiCW7u7Sa0QrboBPLCLfGTlMRKMkk8HLHJJ7b/gu/wDtHeHf2lfhRLa/CvVbrUPDWl+HZFtrt7aSHzrmWTM04RwGClEgQZUHKHHDVx3/AASaljg/Y78KeH1lWRbIXMbemDdSsDj3DA14XENSdKlDl7/ofc5HlP1anerH3pK7v010X6+p8kfCf9o/4w/sofGG5+H/AIw0eZbON/Ia2lztlfGBKrngjp3JGfrX174M/wCCofhO3ij0/XLxbOdrNZlL/KpRunrwScDqeOccgeg/tX/si+B/jtoi3154bt/t1vFm2uYNsc3mAqykN0wPLHB4/PNfnR8ef2TPiJ4Sury3uI7i5MV4/wBgacbTLDuJSLJxngAEL0IwAASx8GnLA4xWq6SPUlTx2Flel70e3Y+2Y/8AgqF8KV1KS80/4iRr5L/vkuBuUEkYXjBPXrgfj3yPGv8AwVq8D22lx/2XJPdySplVSPaMjBYEv04J69MZ56N+Zfh/wX8QW8QzaPH4fkkmZnCxbJT5nJ9ARwCRjOBgHnofaPhb+xl8UPiNfW+m3kDqtxGSXsWO2MZGFlJO7v2BOAQOSS0zwWWUHzTenqFPE5jXjaMNfTY9V0z4kfEz9tf4kReHNHt7v7L54N/cpb747NGPOTsKk4HG4qOOM5xXD/t3eK/C/h74zXHwh1aPXF8LCQW+tXWgeWs88NvBFEbGAv8AL5rz7TKOoEoOG83Ffdv7Ln7L3hv9mn4fSWOnxo95cOZLu7UEeYSdxxkkgZPAJPbjrn4d8D6F4X+Of7Y3x7+BfxP0CHUbDUPGOo3NkJUy1tNmWASIw/1ZaNsb+cHacZAxvga0MZiHCmrJLQzxeCqOgoyd5Pr8ti3+zT8F7f4YeHoPGF1p93/a2q6bHiG4naf+zLVsSrZoz/N8o27jgbpFJwAFA9Y/tzwnpnh210Pwj8I9E0CSO7urzVtS0fTZlutbu7h973V5I7t5jjAVQoVVAOFyWJ2fFMPgHQ7qPSfhXpmuw+H7Gzt7XTB4p1WO+1KSOKGOMyXE8UUKSSuytISsagbsc43HnrzxVHagr9jRm3fKNpOT9KqVSmt2eJToVUrWIJvFupXFwtnY6fO0jthN0TKB9SRxXb/DvwBcX066pqredL1X5flj+g/r/KsfwhoeqanerfappqWyHlI1yXP19Pp1+lfRXwn+HUl1pUc0kJI25+7XLOrzfDsdsKbiveRzun+GvLjRY4cf3VrYsPDzAq3ln1+ldr4n0/wj4A0pdc8aa5a6dA7bIfPb5p3/ALsUYy0jf7KAn2qhpvg34y/Eor/whvhP/hFtKk/5jnim2/0qRT/FBZA5B6EGcgEdVralzSMaiMe+utF8LaXJrnibWLXT7G3G64vL64EUUYA5JZyBx1rsfgJ8B/2qv2uvKn/Zg+ECx+HbjAPxK8e+bpui+XnO+0i2G51E4BKmKPyGIwZ4+o7P4G/sd/Cvw94utvFnjTS5PGGvB1P9s+K1W7aE7gw+zwkeTbqrgOuxQVIHzcA1+mvwGukk8HRRs/zjli3U10U6PNqzmrS5INxPg/RP+CLv7G3ivxh4c8OftQfGvx18XPEbax5d9bWNwuj+H4pPK1H5BbW582OIPpd1GQbqacSYDSbGwPefBfwk8L/Dn/goX4ETS9Pjjtrn4B6tpcMGMqq2OraaVQZ7KJq9J8KfCDxHp/xWbxRfwwWdpZ+IZp0knm/eXe6bWLiPyQvy4I1cqSxBzCwweo5zxrcxWX7fPwclF0jG68K+PrXKf9fekzAH6eWa6PZwRyQq1ZJq/RnkHiWPVIP9ZbleMbS6/wCNc9pXijxFoF5ND4fvFC3eI7qyuI1mtrodlmhcFJACeMgkHkEHBqbVr+a6HzW9uvPzeVCF/wA/SsqPTZNSulsbeNmaU7eAT14zwCf0rjw1at9qxy4iOEX8Jv52/wAjd8beKrmXV9V8L+G/JsdF/tGWOK1sIRH9oiSU+V5z/fuMYUjzGYAgYAxXPx2seP8AXIGz90vTZYZprt7j7Q6tI5YhccE/Wrkce632yXSsTz89vG2D+Vd3PLsZQVN/FL8/8iTRdR1DQrv7fpske4qVdJoEmikQ9QySApID6MCKseP30/StDt/F3maXoMN9ZPdvJPM7WVqqyzQHIkkUpl4S+DKRhsDAxjg/jV8UrP4O+DP+EguPLubiaYW+n2W0L5r4yTxyFUDJwO6jjcDXxn8QvjR44+J3xAv9c8Q24s5G8oRWdo7GO3jWMKrQ8klSRuYZ5ZiRg4r2cDgqmJ97ZHrZdkNfMffUrQ7/AOS0+/Y+nviF+278Mfh6I7XRo5PEs3nbJpoZhawg+YQQJCGPA2j7pHQ5IPHOJ/wVC+HdvPcae3wj1qS88rfaw22qwFD82DveQR8DjovODnaK+SvF5+2aRJbSfupWc29zE6/6tiNoIPfGVIwOm3oBXl9n4lvJ/CVn4gk2teLNc2Vw+z7soPOfxP6/hXtxy3DwWp9RT4ayqMVGUXJ923+h+gPhP/gqzb2ms/2hcfA/T9U0+N2jeBvEMkbCTGF3HyWHUgjpkj350br/AIK2XU8S6P4T+EWl2eryf6n+29bkuIZPTCxxQkn/AIF/SvhfwPZ/2ev2fzC6zRYuVc8Nnvn15qx488KXmsaQmsafGsn2d+6g8r61ssFQWtvxZu+HMm39l+L/AMz6g+IX/BQP9szxz4YvtPTxJp/h+1mjIhm8HosE443EGZ8zx/LzujZT05wcjxaXUbjxfLpvgeRp7xbzUsalq8srK9wCC00g/iztB2sTknGRya4d/E+paV8NdX8WXEzKkNmBEskpIMhPQD1A3YPWul/Z8Mn2H7ZeXeG06z3/AGiaXAi3Zk3FnOBhYm5PTGfetvZwgtEd1DC4TCrlowUfRW+86L4o6NZ654b1Cx+xxvbxopit0b5FhUbQMYGeMdeBn2q/+xD4X0v4RWC+ANHkLabatssQzfdi4IjJ7soIBP0OBnFcZ4p/bd/Yv8CXo0zxz+0b4SJbKyxaZdS6pG0XRgTp8c4Bz2Jyfft1dje2PhHw5ovxC8DW13rNnqsdteRT6asjrc2MsQl+0oCmJAsJBABXO0jGTXzmd4Snj8K40neUdV/kUqmHrSag02vmfXtvZ2d7Z7JeQy45bFcL8QvgzoOupJ5mlwzZ/vxAjH05HYcn0rf+F/iOTV9Pt5HuIbq3nQG2vLdw0cynoQw4x9K7Wbw/cyHd9nbbuz8ycV+Y1YVFp1JjzJnzX/wzBo99ctJ/Y8Cru3fPCAP5da6/wh8HtL8JqskduFx6dq9nHhvdD/x7t6dKyPEHhmTT4PtEwkw3KJt61xOEnuburJqxw2s2/wC4+x26+20V+fHhb4ZXfhr/AIKbfEPxHpNrtt727juA69nkt4nkHufMLHjpuGeoz+j13DDo2lXGtarZskccJZdw+tfMPg/wTu+K1548uLNo5L6+mubhynyugjIXrgvhVGMYUc4/ikk+x4Uw0p4lz6Ja/oYeyjOd30Opl8G/BPU/Ps7zw/DHcc/uXlWNgWBI289PTHpXmOmfCuPTtR8xtH8uaNeHliO4Z7jPr6+hq/8AFaz8SQWUfj7w3dXCTafuj1W1hlZfMtgWYSrjnK7mz2ZSRxtweg8P+OFvNO+z6pqQjjuLcNZ3iuFVN4z8wIK5GDzjH65+hzDhiniY81CXK+z2/r7zH+zIQbcH95W0Xw6qa1Y6Vb2txPdXs3l2NhZ2jT3N0392GGMF5W9lBI+lfYfw2/ZF/ac17SIdPv8ASbX4c2DQo73OqRxX+sSg5wUtkJt7boeZmlYZGYQcivFv2dfjT4f/AGcfEEfizw38J7TWJZtgvNWGuSNeXAB+680okYgYJ2g7VAOAoxX194V/4KefB/x5Mtx4h8G6ho8jBYn+yX8F6nB5YnMb457IT7c5ryY5DjKEfejf0/q55VbC4yMn7unyf4GX4T/Ym+HfwzvW8UW9ndarrjJiXxJ4huvtmoOPQSvxEv8A0zhWNfauku/hm1sjXH2dtgcK0u04ycnGfU4P5GvR7T4mfCD4wL5fwo8b2V4zRkfY7m7WG4DAfN+7mEbYz3xj35qjr9pcWUkljO3+rlIdVcMpI46jIPfkcc8Vg4zpPlaseXytytNanIeDrTR/D2uW93q2hrewLMoeAysm4bh3TmvePhz8QNPS8ZtL077Dauw8u1Exk8sf7x5NeJzQ7ZfM/wBsde3NdFoGtNBMrqsce7HyR8L6U4yuQ4Ri7M9S1zzXsYdB07V7GW6fxxFrBtotQXzobTzxNIzJndjhk/4GK828cXoj/bW+BuqPKv8AyF/F9ozA/wDPawMwX65gH5V5l4y+IOm6f+1Bp9r8ZPjF8SPD2nzaxp6eAdMsT9n8MarP5cJEFxNFGxkuWuRKPKuGiUgoF3ZArpPinra2n7R/wT1TcPl+Kt1aBv8Ar48PanIf1SqcpSGqEVCy63/I4PTNUtdRtI9S+zl0k2MsZcfMO4JB9eOD759Y9Gg+w3PnLczSON0jTSPljgZx7D5R8gwM9skmmLOYbRTukk+QfNnezfU9/rTIbuYCe4jkkXZF/FBnAJC/+zV59Opa1z52nSlUWhHqcepXmr2+qrrkyrGzi6tzFG6XQPdiRvDDqCGA5O8NxjkfGvx18L/DqKW38WWN1DqIQtbWCgMLsc7ZI5RxsYjHOJF6mMcZ6ibUrOwtJNQ1a8WK2to2muZpV2rHGoyxP0Ffnv4i8VaxB8TJ/FHii6urttcmLfaZ2ztdjx14AHAA6DAr6bJ8IsdJt/Cv6sezkuSfXq79q7Rjb538/kdh8QvHXij4h+OG8QeLNSZribhI0TMVvCMnykB+4oB79TknJJNcx4yL32ofZ/C9xGt/pq/Ln+KTklSfTsefStq4vrK7Gn6bb6UsdwuXubn7QczDqPZAFBHfPX0rySDxbqmmfEe+GoSLC9w/7tC/HmAnaf6fQ96+zhGEIqMVZH6JCNOlTUIKyWiOv1Q/8JLoV54kWEwyTWweeE53QzRHY8ZI7nco/A+nPi2paf8A2b4T1xfmKwePLpF+XgA2lo36lifxr2HStUjn8Ut5LbbPxBYG5RG/guoxgjB/2thx/wBND7Z4Lx3o8w+Hniq8t4yvl+NppTu+bA+x2Wc/X39frTnH3NAtd2R0HgWykuNRhl+X5bbzGLL0GM/0P6V2PgCO+bTZJJdrbp2ADfxjd3z2xgf/AKqq/DfSrWTw018sY+a0Aifk4yPzrsPC1hBpei+dNGv7tMfMD15/HpVI1vyo8v8AjZ4G/s/wFJo+mwTLaec0v3+rE9fwyR7elfOf7X2p+M/F/wCztdeEPCXiFrLTftcc+u20LbftkKRsYkJ7oJQWI6Elc8gA/ejaRp/iDTP7L1Da0c0Zbfv5B7EV89+OP2etKt9d1LQ11Caxs9Qjki2pCr/u5CN6jPAzjII5BHFY1oe0puHc4sTS9vTce6Py2sPgv4pvjboluyfaJFSMbCcljgAADOScAfWvZP2SP+Cj37UH7FtkPAvhC7sta8PM4ubfRNfSWRbBpCGaS1eORXty3BKglCeSpOTX2Z45/ZB8K6X4eiuvD8kn+jhWzs+YY5BB9jj/AD0+S/iF+yZ4k0DTfCfx8m/s+Twx411TWrHRore73XEZsL5oG86LYPLBB+XnkKeACM+HUy6WHXNB6nxmKwNbAYqHsnbm0Pp34W/8Fz/HHgnV5L7xP+ynp9xFqVoGjn0nxLLEGfdEVm8t4SruEjkiPALCQAsPLjx+if8AwTk/bn8D/th/Cixk8uDS/FVrbomqaGbld+4R7m8kZLOqjjJAbA34IOa/KX4QfAfS722/4Re800NFH/plgz8kFcFlB7ZXJwO65r2z4YfCrWvhT4ss/iJ4R1xfD+s6WwfSNfFuuJgOTbsxI2E8ABjtbjIyOeTFZBHG03LaXfz8z6bB0q/L77ufsPGlvn97ZndS6r4LtdVt/tn2cHanybu1eeeF/wBrH4SWP7Omk/tAfF7x7o3hTSbiJIdRvdd1KG2iivR8jRZJC7iRuCLztINZHiL9qaf4meGLXS/gTcSTDxBJFbaLrtqFukuzMwji+ztFJsO8sAJA+B7HDL8VSybGVsQ6XLs7NvZfP/I6pTjE4n9rrxR/ZnhjUPAvh3VrN9YjewM1mMZSGbULeJwx6I3kNNIB94xwu4GFZl+Zfjj8c/hN+y1pGkeK/i7q81pZarr40tDGS7rJLBMxmO1wyomwbpYzuQyIepFbf7V37QPwT/ZK8K614W+M/wASYbe6Z4rq+0zTMHVdWuntUmIig3+fhfOaFZJDGi/vJC0TXD1+Tf7Yn7U/xD/bk8RaRrP/AAikeg+GdBWa08L+Hbe6a4aMtsae4nlYDzbiQLArSAICIoxjgk/cYPD0snwns6T5pPX8vuXZHmYrNoYaj7msnsv8z9c9HvNB1zRofEnhnUIbi3njR4ZbeaOSN1YbkdSBgqRyCAO/AIZV5Hxv4H/4RWGC40y3VbGV2e2CMP3AbOY8DhNpPyj0OOcE18B/sR/tP/tJfsz+Gm0TVvA974n8FWpO22guBDdacGbLtbu6ujqcsfJkVl3EMpRwDX3D8IP27v2Vv2gNObwPcfEuw0a61BUii0/xFay6fdW1ySdgVCJUl2kjlJznB7Hafcw2JjUim9H2f9anoYPHxxEFJ+6+z/TuYvirzLXTZLiaOMSrEfKdYhvAHOQeoOPT1+lanweme18N23iHVrhpuCxWZzzz6/j+lU9djm1wf2Wpyc7GaKXOD/vD3+vrW9qWkf8ACO+EodLjb5liw2F65HPTtxXcdnMyzB8R5l8RQ6louoSwuuF8qKTaGXnGR2bJ/wA4r3rwt+1z8bvAug28kPj5phs2x2erOLmA8fKo83LIpOPuMpr47N5cWHiu3j3ELNIB85wpH9M9q77xf4guLrVIdGjugpS7Coq9RtOP5j9TXNWwtGvpNJ+plKlSqaTVz9Jvgf8AHzw/8Z9Lht5/s1j4gW3Et5pUcxKuucGWEvy6ZByPvKevBDN6JEzR1+bMniXVNKkt77S9Sms57WGOKK5tpfLkRJFwQpByPmYjjB5btX0x8HP20vCfhzw1a+D/AIqf2lJdw/u7TVIVE5uAeVjfe4IYZxnnIAzjk18pmGS1Kb9pR1Xbqv8AM8THZRUj+8parsdf8YLnxRqHxMmuNc/aO1q18IaXPa3V54A0bwnZr9oezFveMJr+U+a6PlHKLtAAZM5BNdP8cdXks/in8HNSMmfs/wAbtO3n/rrpepW//tUV5n8e9D+LXjLxEuofC/wXpN9o/iHSoDeapqniWKzksPtlvJY8w7GMjKssU4AOJPJeJD5m3PXftMXq2kXgbxB0+w/F/wAMSqT283UEth/6Px+NeBzb3PJjKUZKJA2pLpdjHJr11DGc7XuERlizzgknPlg4H3jjJABJxV1Jo7ewuLqSRUjwoLu2FAJ3ZPthetVtRsvGmnW/2i48D32dgPkpNErgkDIIkdcYORyR0NeefEDxPpXgX4b+INJ/s2TQ01LTZbO102ZYCoeQSqZIUSRgmQ7E7duNpJALAsYShKtWUF1Pn6FCdfERpR3bseA/Hj9obWvjBq1zovhvXJrDRbeYpZ223AuNvSWUY5JxkI3CjHGdxPjetXvizR91l4ukXWtJmy0s9mmJ7c8fMo/jwP4cdzXbyQ6fp9+LHWrNVjmzs+Ucp+OMenpxV3VfCWj3Nm3l7lM2BhDuDn2HWv03D0KeGpKFNWSP1ihh6GCoKnBWS/q/qea3+t6hb+HX0+z8RSQ2eo2fl6V4ls4nkKjg8/7WODnnGe4IryldQ1Oy8Tt8L/iBcN/b62xutC1d5t0eownn5W/jP15zwRXp1/pniL4a61NdaLtuLO4maS5sJnxFKn94ZPyOOpI6/Wsf9on4W6P8efAlj4m8L6q1rqVifP0m9hb5rWYYIHqOfwPvzWkr9Cal+W8Sh4F8U3mo+IdN8K3Eeby11m0vY4mxkwvIsdwvtgkSY/6aegroZrb+2vg941uLdgqt4tvCh29o4oIj+sR/zmvC/wBlbxfqms/tFR2/jDS2g1a3muptXRW4huktCJFOf+WUmxJoj3zMOsea+kfgPoUmufAa4jukk3aleX1y2V6F5mOe3t/9aiMueFxYeoqnvI3fhKrzeCrFePmtwc/xDjv3/E10WpTeRp/9nxs27lmX0OOM/pXP/s9fZ/8AhFm0O8Zmk0+4eFu/AJx9f0x+NdDft52oXlvawt8s4UHuCRz9P/r9qOY1cvesa+jCP+yo/tkm5lbqvO3Gevp0+vWsnxlpun+NNJa60r5prc/Oj8MAOSPY56cV0ej6dGmlx24XCyIDux1rnronwxrQmh/1Mj/Mo455GfxH+c1PmRHU4tdXabSG0u+3RsqFV3rj8P8A9ea83v8A4AvrPwqttHk1C4ltfDvjDULjTbZ5iY7eLU1jmkVB0H72zmfgZ/eY6AV7F8RPDkaI3iTRYdyyAPMiN8oyf5cj8feo/A17Z6p4d1zT4wqm3htp3RVwQRcRw9P+3jt2P4US5Zo569GFWza2a/y/U5XRfh5Z6dpWn31paxpdWrAOduBLjO5T9VyPbOa9IWGE3C2Go6faz2WpID9nMJCpIVGA2c/KR6DAKnjsKdja26TNZyLujdA7I3QH/wCuCM46102kaFp+r+HLy+8QeJNN0HQtPs459b1vUoJnSxWRvLVv3DmXO5gAFA9wowaqUlBXZry+9ZHzd+2F8H9P1Xwxp8uqWcc0Wl3l1b2OmiZZIreW4VWEoiwi7z5MgLY3MoyeFr5n8N/sg6z4WtE1e08Y61pvnXKX0Nrpt/JAGlRg8U2EIwwYKynqCARjAr9Bfit4TuLHRNY8P6xeXM9zpl9DG6ar8swkRljjyuzOTDMCdzFlLgHkMTzkXw4g1ZY5ry1kKsuX224HPPv06ehH5Vyzw9Os+ZmNXD06k7yWx+c3xT/ZrvLXRLi8t7d2k2Bd/wB7OPU/T+VeseFvgT4K1Pwt8I/Bfhr4VfY5tJ+HMN/4nv8A7KFm1TU9SnkuB5suT5gFusBUHHlxygdK+xNc+BPh+TSZ4zo8TySRHmc5BHfHAz9f/wBVaXhfwN4f02cat9ijZbXTNNihBTGxIdPt4h/6L9hzWVPBU6c+c4ZZXRni41Oxg6D8GfB/h/wPb+E5NDgjLWwMyRwgOhyPqeT6+9Y/wo+G/gvwtrd1c6X4dtY98zATNArv7ckcfhivUYoodQsbq4k3fMmPXZg+n4//AFqxPC9nbyasLe3XarSHC4LEf1/Ku6KjueqqcY6Gbp/gz+zfGU0En3PN86Mqv3kb5lA9cA/pVjxW/wBpP2WGPKqecvj9e1dnrOiQ3yK1nJGby1BVd3DEHqD/ADH/ANc1zM1plZJLmMbuR8y4K/19/wAas1jseV+NrH7LqNldf8851O9m2kc4J/Id/wAK09D1Kz1b4nXnhm4jZbuPXi5LnpC/zhhn2P8AOrXjjRPt0pb+KGUN8oPHc/z9+/45d9/xKv2q7eaOP5bjQElmXsNsCqDn3IP+NBlL3JHowuLe/wBNjkvpNsbv9ouVL4JjHzbfbEoxx1+UemDw9FqGp3EWuXSy7mcy2quvCg5BmP16KB7n0zHHH9uEeivHugjhja9fPUnnyc/7XDHr8pHrXRpLHZ6a93HG3mMn8Xyrjt9AKGtDrg/dPoP4Q/Fyz8e6HD8O/HnkyXVtppt7aZzg3McME/2dJPV0aU7T1OR3GT3n7VmrLJ8E9O1ZpPmg+IXgm53+mPFGlZ/Qmvj/AMAazdXUja5bs3nxzDZscg4HRvr16c17x8cvjFpfjn9nK+09ppF1i2udHvZrdYTtkNtqdncsylMjhYWcjjAB44r43OcrlGftKK0e9uh8pm+XTo1fa0Ve+/l/wDtLD9sjS/iR+z9ofxw0OS4k/tjR4Z5LGaHy2iuiNssGf4wsyyJvwAwXeOCK+a77x540+Imtah4g+IDSJfM5itoeVjihB4WIdh7jqcEknmuR/Z88W6p4p+HU3huS8n+z6K8dlaRTS7vLjiG1Fx6hQOpNaem67pc+sNo/klRu+Xc5XzP857f4V6WRYen9VVa2svyO3LMvoYOU6ijq27dWlfRf59yXUrH7Xcm3uI4bmNFTYHfDj/J4qjqhuPDlwLdbj7RasuBFK3Mfrzz6ng1ua4kdpHujgndpDlG85sDtnr/hVG1T7Xtt7m1Ub+x42/T/ABr6A9iUrxuc3rZ0vV4GW4i29dnyg/N/vdOPY+3Nee6BN/wiPjhvDsmP7N1jcBt27Vk7gcde/fp279Vq2pLpmuy2OoW5jXzcJ/ecfiff2rB1zwpJdeGb6S3kZ5dPvUurR3JOHzyD+Hye/NARXQ8J8A29v4c+NHiIXUaf2xoei3mkvcyqfNmjeaFoWP8AeyoyD1G49dxx9Xfs920dn8MbezjXiN5YVwdxHOOn5V87+PtKXTv2gf8AhKmVY08T6Lakq3ynzreSQOCP91ofyNfRPwQLQeDHt/Mb/kJSYVuP+WcZHbHXnt+tEY8sDGmuW6E0Lw/qvgTxxe6hptr5lrefOIQ+N2Rz+VbHjO509raHWrfMFxJIFlSQYYOvOfbjIz3zXVSaP9sSO4jkV3jYkMvJ9+c/h+f1PI/EF1gjtZOGZZ9uzcQfX29/y+tR1L5rs7W2uYZdOgby9hVMqqdB3+vWsrWNLh1azn0+4+ZWUqML1x/WrmgC2Gkxt5nzFANu3OP8/wBKmuEwG+YqqZO9e/8Annj/AOvTXYiPu7HnnhrVvsGozeC/EEyssz/6OX54Ix6+1VPD+jf8I/4o8RaZJGfLm8NyyZ5GQssJA/NUq98V9Akl01fEmnq32qzfzARgdOen+e1Q+GfEkfizw/f+JraSP7db+G3hkX+JSb2yjGR/wI0SWhVSzVzS0tbiS+ik8vKbvm3YO3Iz/wDrHoa6vTv7Y0+2s9Q8J+LLjRbyO2jjjvrNIJxsYqZFljuEkjP+rUglcqUUgg81yfhi8kli8uTja5BXHbj2rtdMi82JWfyZVVEbHOcc4OSevT0FEuXqTJyjqcz4msdN8N/DvVo7zxJ9sZpvtN/cS3LSXFzcNdrPNcTSO5aWVnJkaQgsS3XHXa0s2YsY9kkcgaMBWY9PqPf/AD71/iRKt14H1ezkR0X+znDq6sQ4zzjnaBz357CszREuLW1t2tmXZwrIvy4xjp/nilFE76s3dcCS2UkcjL/qyV2Ljkev+e/1zzslwttoCxySMyiG2QNuPB+zxA/nj+Va/iFri90a4i0/cs80Dxw/3jIw2j8ckfT8qy/EIt5/OWzwsN3qc0ltjoITKfK/8c2j/IypfDYqPxL+uxKYWj8OSW8ajcIjs+mOnr3xVHwBaSG/a424Kgj73r/n271vXixpbfZ0mxweicEDjH6iqfhG3aO5kj+bCk/h+v8An1qo7GifMWddj/s+5XVrO38sxsN+xxg9fXrVTVFt7myg1S3XKzJ856YPp+HT9PU1q3yvc280Nwv8B+/6d/yxXNeGtT8xpfC+oblWTcIS/wAoVs/T1FUV8Jg3lrH/AGuI/lJk42uw4x261zvjjRrm1+Pmk+IIWGI/DAUynopUz9v++fTpXZa3pMljqUeI2baQD8oGD9Py/nXM/EvUrew+I1pJbzbnh8P3cM8O3o6eXKM/9/T34zVR3JqRujP8O+M9P0HwVDq2seIo40SJJbu+ucKvmNyzM3Cli2eF6dAMACpJPjVZm3EWoaHqErXCBtF02CHbcXYOf3rKf9VD0wzgbs5xXmvgzw7faDpukWeo51vxjcWyvaW12N1voyY5l8r7oZR3IJyOMZwfYvhf8NNP8Lp/a2qSNe6pcPm8vrlyzu/vk8/56dKJGkZPdkOheH/ix4uKzax4im8PWJOE0vQnSNv+BTEFifpiut8O6dJ4X16y8P6fruoXl1NMjmW/uDI6gEZ9AO3Nacl99kj22sKOx+Vd7YUH64/lU+ieHzpd2dW1RfM1W7X/AFu35Ujx269Of/rnrlP4SpSco2G/D74VW/wu+Gkfg+zvGury7mlvNUvH6zTSHOCcDhV8tR67ScDJFcrr2lyWN/8AaI7gQuMEHHP4d/1/rXq+sXBt4m2/MF4z3/rkc15z4qeO8RonjiDBOu4jHXBx+HXjrWVCnCjBU6eiWxhDsXtI8Vya1ZZkm8ySPhlRvmVu4xUgure4k8vzO4Vxu6Vwmh6zqHh/Uo5JIW8liQS33TnPU44/H+ldnZ3UN08dx5ax+YAdpUfMPX0J7V0CcTK+Jnh631DQJNYjjxPbjcr7+SBz06Vw3h7UpNY0K88PxyMs1xbbrcM+NxB7V6Z4y1m3ksV09raMFm5YIPuDjnj1/wAmvM9Q0gb3mspFhmtjm3lU4DEdP6UGi+A4/VbYeILzw9NqFx9mntdVNvI5jViBJCTjkgcsijj9a9m8E28fhrQ/LvtZtTcXUvmIkqCBicAc5fb0Axhj05C8CvO/EOu2632hyXFljVJfENpFEiqrecRIGMmOn3Q36V6BqM+oWU0Rhskn8yEcSJ13DPoR36j3p62E4qR2PhS8vLhfJk2yK33WjwwJ6dQT/nsa5z4hWmqPqVrHHo8kkcZZwyRs2D2/l+tUdIm09tQSSSzmt5e+zG09fvdD1wOCRz2rWsvsMeqtIzXDbVBwFyMdieTWXvIi1pHReGLmQaVH9sk2t5ZGHU9zx2FXLq4WTd/pACrz8j//AFv84rifEF5JNIsMdxcRybSdoXbwc/41esJdStrW6+0apIzeUGV7q5/dgY4ySe/pjJJA5JqkxcvUu655N5ayR/Mytnhugz/n35rzTTrabwHe+N9Qt23Wy+GLaZF/hWRta0zcPyP6/SvS9X024tjeW0d5G26WaSzeWKMsbfzHWJiRwDhcN12srpwVIHm/jTTNQm8E+L9Pt76RprzwSsqI3zBpINW0iRm5G4MRJggnaAowFO4mZS0uTKVo/Nfmdnod1pt7Y/25a3AVWiLeW/QkDOfzr0DTrKKGeSTzGm7q/wA2F6jHI5z1z0IIx7fNfwH8cSXWkS6BqW5dqFow/YY5Xnnr39a+gNK1z7YPnC7mOGPkmMg8kgqScHn8c56GiXwjkuaOhV+Jl5JL4H1i3t7gMzWIj2s+CASq8A9R+8HTHUVS0Expp/kyAjbjbv64+tP8U3048P332yCTb9pgErNHlUxcQDnP16jtn1FTW0O8xyWkn3uVC8ev+RVR2HGOg6O+/s29s7hk5t7+G4A2doCJj+kR/wDr1kw2v2efTtN8skwW8a4Jxuwo/nitDVopGKyd47C++bdwC9nNAP8Ax6Zf0qrsabXvMZfmUfdYdsUNe8Omvfb8iW/EU1y3lnd82cL6DFW/D1mYWaa4jBV1xnIwef8APHpVOK1ae6Vo1O3Zz8vTkf4f54rWjCp9cYVdv4e3p/nBpvYOYhHMreXJj5sLu/z/AJ/GuI8YWUlhfPdW9x9n8uTzoi3Tnkj8/wDPr3arGjtj75PReMGsfxTY/agV8vduUhSmOM+/4fn+NJP3i09Q0K+tfEWn2uqXluryLyr9c4x6df8A69eM6petr2t33irzJNwn1N9xUHCZRVA9eIx/+rFek+D5f7HnNvNI0kMp+TAzt9OPX+dfOUXxOj0P4faz4XvrOZr681z7NaYzjy5pwsxPf5FWRvoD0o5owlqOTUFqehabrHhjwDok3izxBdNZ28jHz7mb5rm/kHaNUxxnGAOB6mtbRPiL8QPE0C3Hhf4fnSbAjMWqa/IN5HYiIeo6dTXm/hrxNH4y8c22sa1p/wBqmiixpFnK26O1QZxJjgFj+g578e0aHY3l+v2zxJeiTyU/1XmYVB/tH8+g4+lCfNqLmvG5oaD4b1zWLJPEGqTX2o7bgKvkr88kgx8uOBEnqetd94b0zWphJqGuRrG8jArCWHy8kEfhxXH+GNfk0KX/AISDWLhbXSeY48r81zJngRr+PX2rU0Hxz4i1mdrnSfA80dm0pCyXM0ilhnqB+XWpmGttDptcvIWikjk3SNu+SJOv+RXlXjLxKuma8tntxbmXMq4HzA89/TP6D8PSZnjU+Zk/OvK7Ov07ev1yK4P4i+B49Yc6haptkVv3eE6Zz+f6U1GxcVyhqNjHqGkR3Fupk2t95edwYHHPfv7cVdsZLLQ9OiW8k+bZ5jwiMAIfT345/wA5rkND1XXvCt2bG6t/MjwGCOwGfpk49+eKseINU1TXhv8AKkRZf7qYwO4zVkuLKniTxl/a103kkrGX+5u6gdBj6dua5Xxfrex4dPs5lDM29yOSoGcfX8R3rXvdMuEQ+Zat5cf31dwMcDnn1FYMkNnb6hJcNbxNI335N25j04/z/wDXoKOfurC41H4heF4Y7jEker73cOQxSOGU4B4PUcAEda90vNG1gP8AaJLqO4tVQeS5YbiBjG7pj0yAc9cDpXk/gGfS9e+OdjBHsd9H0qe92MuF85jHDHn1+UzfiB9K9SvrzWIpRHb24lt5mQ7VmDFCPvAHHGefyqto2CEOZ6Dp5I7W0W8uP3MYlI864LQlee+9B3/DjrWppTxrH9quLhJJHTzB5N3EUYADGDnnPPPt2rMsdcvJLwjWI98f3NjtuyP5cdMV0OiX9kiSR3lwrSs5ISGIbeT2+n4mpJqJxRRFxcyj7RNYymPbuV4k8xjxn+DJ9O3/ANac39vdyzaXJZSQgWoX5oWXcDgHBMZ/lzjHBIIXWbzURbtJb3DIq4x5SDLfU1T0jSGYyX1wPNkmb5ppcH8vpjjtQTzdTY8XeNYNXlvrjw/Y3eZIza2ksltKSkJuJpfMkJQZZpbiZ+ANq7U+YjzJOXmWS78ZHw/cbZoZvA+qB2aQtsRYY514JPU2fU4B28cVfvPD5vYn/wCJn5TKpxBNbrJbTAkcMp9hn/JrntA0y20/42LpNnGVik8ManHNAlrFCPm069iX5Yx6BcHrg89cVEopR0MpqKpsxU8E23hu/g1yxjaEg7HVGPJPGSP5j8a7i113XLO1g/tC4X7C0sqptm80RlQRw3Jx8vBP04xxxVp4o+2+CTp9xFuvbS6Ie7nlBjljG3aGAGQeCSQed3A4rZ8BX7a293pOl+G9BtZFv57dLOW8+zvqcvmn93BmNY/4lUySNGCzgZJJzTstWawjKaskavjTXbjWfDV1cR6XIAby0VLma2ly3+lWxxnOB1OARzx2AFdDpGpSWH+jv8zBFO1iMZ9/wrk/iDp2l634WTUPB19cXVrefYZre4MpLCI3NlIqsHIP3SOGXIBJBySA+K11DSN15dXG9lfds5BI6fof504r3RdDs9TuYZoLiO3PzeTBafe+6ZbqKYfmlnMPpnrmnW3OoSf6vn7+0dhxx9a5fSvEUUljpjXkknmajq9zKo3Y/d20SQxn6GW5vB/2z9a6ywT/AE5pBHt6ge31/lQZ05Xu/wCuxNbwxwMfL3feIXPfp1/KrX+jzjbt+794Htzz+fP0quhj+aSSLbHvxlU6e/f1rE8SeOrHQx5nMjFf3SZx16/U4+lBVmzoJg8rbiFHJ/Ad8e5qnqUatb/NGzYyFYAgjnP+NLoutW+q6fHqMMZHmfN8/wDD14qZ5I3Vmjxx/fYbcVCi4yLOSmsVguZUjj3Lt3o7LwSAOPSvkLx1YsPjbrWh3EmIodXuLh3zyol/ej8drY59frX2Tqmm3lrerPbyMq8707HryP8A9Xevij9p7XdH0L9oa6t7jUGtZNW0qC4lcJucqN0OAOnSHGSe1TUUbamNaaUL+Z1HwK8U2LeIbnXL7ase0si9No6Ko9McD8O9fS3w48M6j4vi/wCEk1q3ZbONsW9nE2PtD9i3oB7elfLXwrvPCfh3ydUt7XzI+C8kz73XnkgDgY9uRzzX2X4H1vQ7jwtYyW4byGjBSUfT86It8hpS1hqaFv4a0eC/XVNYjjubyNP3NuufKtVA4Cr+n+cVpRxyCAXEkrN32K52pj69On61k6jqGnmZY7e8CmRwCG646n8/696j1DxRa2WjP5dxvZlK4zkCT1/lz7+tFnyj96WhJrlxJZW/mbGb5cdPu9fT8Kx4dTFxH+7CsNvyqQCTjk8fSrmo65KcRyyKyc/IMcH8T/n9Kx/KsZH+3WdwqNGMn1Hp2z3rU1JtW0qz1CyaS8Vcxqdn8Xufp9K8/wBY1jw/NdyWclwu5WwrKePfj6/5xW14y8ZHT9IlW3uBHLJFhMKCV98f56j0rx+41H7AZL1/MG3+LdnPf/P1qXLlD3kYv7Q/7S3hv4M3S6Hb+H7q+1C4iEriADZDHzgsc/ePXA6da8g039ty08UyPpq6TJZSN/FLjHvivS7zwppvxdh1CHVPDc/nG2d0unTGx1HAzjrkf5ORXxE9vpv/AAsa88E2d0zbdXmjuLqLpHCspXIPr9Pz6V5WLxNenUVno9jyMfiq1CSs9GfX/wCyf4pvPGXxG1rWvtDBdQkisbKVz/rIog2X3d/3jy/gBX1/pmk2vhvSxHdXEcTSRZfnG49CfToMfX0r5q+CXgnwnp+iWureG41tY1ti0Xkrt56YHoRz+f4V7J4eu9Pv0jvNd1qR+ANuenBx7d/89a9WnFqikz0cLz+ztJmprfiHS0do7OHc38UYbqfUA8/48UvhK+ura6ENxuWNsFMFQoPbGf60yOLw3MD/AGXHtbaVZzJuJPc/UdcVUa0t4Eb91hUOY9r9zgZP+T9Ko2ep38fl6pCu248tc8b/AOLv/nPapBdR27LHbybn3Yy3O7oK5vwvqd0zj7QyvGwyFV+/v+fSuoEmJEk8rajY6cAfn6/0oMuXuJc6jZw2s9rJuWWaECL5vvcjqPoCPx9643wZJb3X7RGm28O5Y2hish5q4YvN564PAO4+d0PQMPbHcQtJ9iaNvMDSEJiNfmbnPB7fhXm2nas2jftA/wDCUXlxMtvY6tYPEJssybfIJ3E8kkKck+v40SV1YznG8ZJditrWjyeFrpdUt7d5ra4tiLm0VSCx8vKgbOQTz0OfTFdHDb+F77w/JJ4X+Hf9oTw6h9ps7J/EMtiltMs8f7m6UA+YiuvnKAwBYsOhjaPk/Hs95Hr9x4Tk/dyW+sNYyRkyJnazRk5jBY9ONoJ9KtWWlSaX4Q028urfUmtri2QzQw3km0lhnK7/AJgCSevbAqpJTjqVTqTg1KLsU/Emv6p4L0+OPVLdlmhvra13Iyybdv2Y48xH2jPlM2zHyljj7wxc8WfEm402wbUI5G/1RZFb+FscdPr+vSuW8W3LW2oWP7vVnSbWoW26g0ewjy7k7l2D748rBJPUn8NnwNaW/jX4t+HdB1Tatm2qwy3y7esUR82UEY7Ijn0qXLliTKfLByfQs+OtfvvD3xFj8Jrchm8M6Pa6XcDHD3Yc3F4cdj9qnnX6KvNd/wCA/idompwrZ6leRwz7sbXb7wHqffn9c14TqGqTeLPE994k1CTbNqV5NdTKzZwZZTKx/Nj9f1qr4juJI5F0qzuGHloDJ82Md+vY5/Skr21CjFqmk/6Z7t4r+LGjpJNa6TfRSpC+1nVvld+h/LAHpXGz6nqHipzJHHvYOGlB52gZzx7AHOM15za3UiJ5ckZfam5264z09/071raNf3FxdxR28ixMSA6FmOOQM9PXHtzTN17p7B4D1ua2kazy43BSi465+nfn867m1MlyCtvub+8wXHOa8ktGvLO7VZLlXO0DIfJyRzt459eO4rsNE+ItvYJ5eoR9UID7uSemDjP+T2oJkjW8eanZ6J4fmvLiQsyxHbnjbgf5/KvzO/4KIprLfFLwh4p0tZozd6TNakqTgmGbfj04E/avtv4s+NZPEE8mn2/+rdcl2fhuen+c8186ftZ3cNz8HpNUvI0aLSPHGj3ZlZc4tyt3ay8nkDdcrnHUotcuNjfDv5fmcOYwTwbV+z/FHA/BGLWJPD6R3Fw7sz8K7Zx2/Kvub4OTf2F8PtL0m+bzP9Ey4d+VOO/5/wA6+dvhF4WtZdDjureNfLTHygdSeee/avZvDPiZSsdn0ZUG3HRgMfyxWlCm4QOjCRlGlZnpn2e4vJFWO48t0l43KODnPp6mnTeHbcwbry43qoDMu3rk5H1PB/WqPhie3ku2tSzb/LT5jkn7v/6/yqv8RPGNvpEH2POyaXcmwfjn19P/ANVbHQua5Dp1pY3KN5epXCr0G27LrIRjpkkY/wDrVR1q11Cyt5Wt9cn8kff8xQePrgHknv8A415Zqmt+MPhy6yeEdcj1K2DZn0l23K2OpUnkE+nGOeva9rfxf0/4zfDm48N+E9Qk0/UA+Z7N5CswI6rng465A5wAemKHIIzjcpeKtXjl1FvtnigSBch06Y/L0rNu/F3g/QbVrzWLwSRwqeXlCrwPcd/8815+mm3ljeTW959qjmhbaUckAdcED0OPw4zk1g/GS/s38OSwrIqpt3yKeiqOTz3/AE4zXHUxEoRbsZyqcquQ/tDftv8Ahjwl4duvDfwzaS51i8t3ihfcfJswRjzM/wAZHYDuPSvmH4AabC/i99Q1SHzgzRxs83IYvuPJ65yAc+vWsG+aTX9ZudSbdtZzsD9Qo6D64r0f4J+F11SE/YLeRpl2sXRsFDyP6dK8ONapi8Upz6bI+VlVrYvGKctlex9T/DPWNU8P6N/ZckayQL87RMOAenTp3P5fjXqHgjVhfAyyRq0TB22rP0GB7HP4+leY/Cy902/8OD+05lMmQrsnQnp+mDn6iu08HzR6ZeCPd8irsZnzySc8Dr619PCXuH0mHl7p6pZpZ2lvH9ns/PaRARIh+YDng+/61L9lt7vbH9sjjIQdTwPwHY9/84xdKmjQmTzOMEfezj/Hp+FXXucwrv3JJuwj5yew/r/Ie9M7DY0uxOlSP/xOIGUthm2+n4Y6H8Otb1lqEk7Dy7iRlGMo3O4e3c9z71y/h5I45FkumEhYfImCFGM/54xXSrLb/wCs/wBWFXoMH24/woM5mjFqm+Q28fms21eIQOR/Tqefp9a4G8t7fVNY1S3uC0bNcFUC/wAJEYOf5Dr+Vdg+oySl42s7dbdCm+abcd/tgc/l61g6ZpscGp3lx+7XzbmZkOMbOcHpnGMdOcdPegCj8bLAH4lateRQ2sf2vVVvl+13PkxSx3DCYbm7Aibtyc/nk2Nj8ULOys7O31S2ZJLCIJbXd15yn5R8pJ55GTz/AFxXefEDy5P7H8QR30irN4fCKqTLG4a3ZrPhn4B2wKcHB+bI5rn74fBvW5I7XVry9s72CMbJoZCrSNgDr07dfaiPwmNJXikeW/EB9Q0i/st+kxWcjagXeW3SP97t+2Z+ZCSdu8D5sEZJHBq98HdR1C0k8XeOLjcF0fwTfNA7fwz3bJYR4z6faZCD0+U1J8VPCMjazosdnHHGtx9pkLwo/nShdh8xyflJ/eDlcAZOO1dPpvg+40T4K31mI/Lk8R69Db7X4329nC0sn4GW4gHuUrGpzPQmrGTXKurS/wA/wueQQeNbO1tVNnDJHM0eIfNX5cnjIx+XI7/nDBql/BN5MkhkkZSZPMXIbHXI9z7HrXead8HI5bkXEexvLbKbEOM9fx6fjXSWPwI1S7Cwx28Ma7E23H8K9u5564x1z+dUlN7nTy8r1PN9NF5cMFvI2jDEb5SMBh1H07fr1r0DwP4XVpxdNG0i7eHVflb09e3H5123hz4CaHpMi3moTNdt97y3TAyepx+f1rprgafZWYjs7OKONPupEoXH1H+f1rQOZdDkJ7WG0uP3nyhsfMW4I9xj/PNcxrV9HE822T5k3jYrEDofy7/n+XReO/Gmj6ZHJvmUTMg8r6DnqM/y4rzjUNZa7XzN2fMU/Mo54zmgfNYq6/fXCSL5i7oc7WVOWHXn6+3tXjn7Y6BvgH400WCNZGuNGgnh46LHqVnMXHX+ASc+5r2Br1ri28uzjZpNvLdl/p/9avM/i7pA8X6VrPg+0HnFvBuvOhRN29o9MmnUDjL4aHHHJOMdjXPio82GkvJnn5hrhJrumYP7HHx90bxt4Ej0u9uhDqtiAt/bu/8ArDjG8A9Q2M+3I7c+5aPL9qnju9PjNw2/JSPk+3T618L+HPh5qHw316x1rT5JoWVQXcZ+cMOfqD6c9vrXvvgvx/4ksri3mhnMx3hZU39fUA+/BBPeufCYifslCa1M8urzlRUZ7r8T6b0Lx7caIHvPsdx9oZwsQ2YycdMnv3x1qzofgjxV4y8Qr4g8WW7W0Af5YZfvNjBAx1H4+gqz8MtJ0O8uG1y6VZ7kHA83+HPPfuOMD2Nd4/l/6xZv7m3bj2989eR+P4egescvZad4T8Wac39qRrfJLwJXwJF+jAbv6V87/tPfs93HgDUrXxx8O7y7tTOpEV9HNgrMMnypMAA8DIbr19M16VbfETwfdGSHXlj0++3bZdRsBsZT6yw9DknnIBwc+lUfEfjWzunvfhv4q1C1uIZokkFxCpA55SaLPPGM9fUHAOKmcYzRhOKmeK+EPiNefFFpPC/iWO3sPGFmm2KaRgsd2g5+bsOO/br6ivGf2g/EOuSatF4MuLOe1m34vUmQhlGcqM9w2Mgg8gH157L4n6BdaDLF8RvDeltNc2but5Hb4DAg4Mg45XrkenPY11Wg3vh/9oPwZ/Z/iTS401W3w1hc4CyRY3HAPccn5O/XjGa8uvTnVjyX1/M4589WLp31/NHy3oHhnfbtD9nG5cgK3JBrsPgvqlx4Z1xrfzFRZPLDhjjIDc/n0/H2qx/Yv9h+KbnS5I2XybySJuPmJBI/KprLQI7bxtFZw7lF1CXj7BjkHp645/D8Bx0aTpSUkeVCm4TVu57h4ahk3SahZv5KwsWdYhls9Tn06/jiuv0HW5Lq1t7ixt/MG8iJOMgDp/PP515d4P8AEcfhtJJLzIj2hWWXI4OQfp0PHaui0K/j0nV5NU0+RfsqgyW8bS4554A9ODXsU60T2ISPZrbxdeQKsf2NVj+677M5fjI6/wCGOPx6LwtpOqaq/mXEm23Zd3zqSRnkY9Pr/jXmfhfx7HdxLcXnl/e+eLqqjsD+XH0ruNL+K2nxDyY22rv6r90cc+vXv/8AXrqTjLY7oyVj1ZNJ06wsjJDHtVIgM/3eeSMfj09PwGfdSxxlh/E6fIzN/nsOn+Fec6v8VrybT/8ARfLfdyjL26cAfTH14+tXPDnjXxIIIbiTSRtcj5y20dhyKBbaHoFtql5ZouY1hi84ql1cLJk4B6KmGOeBxzyPeuLm+IWn2Gty2bS7pGuJpj833cyMQDkDB+vqOldTZ+LG1i0eTy4bdY4SGdbYSbzwAPnPAwevbBycivKZLL7defZ+sVwpmnm2HcQTx07enb8aBHtvg3X9L8RfDpdQtbgTNo+q3UEwmtJJUAuYopot2w9A1tPgk4y45BII1NY1TTbyHy9c+HMk0Dptaf7HGwHvtxnn8/asf4MTaXpOlalo+lyMq3FibpEQbZDJCytxnqfJNweORyR0NdBe6759lHrEmh31vCTtSa/05kWbAB+Rj8p+Ug8HoQemamO5z03Lnkuz/P8A4NzgfFXh/T77xVoKaZbafBCsN822yjVJP+XY/vdiD5vrngV2HiPRrOyh0fw/JGHi0+wGWdRzLOfPk/HEqIcc/uh6VhTRR+JviB4Y0f7K1uuoXUtssjYyRIYAW9SACTk+mOau+JvEEut31x4gWQLHdXstxAmQQkbMSqj2AOMH0H4O2qK1lXS6LX5/1c09O0zT4QkNvHGoXhcdunt/nNW59S0vRoPtNxcR7d44AAxnt6dzXnuv/EC40fTftFvDI00gwny9+OD/AJ/+t5P4g1bxn4uv5Lezum2vnbKr7VYdcjOPb9Kcnym0lLc9y8QfHXwnppFrHqUby/eX5htZDnn145B7/WvOfHH7QccsrNo8kecn5HOOhPOc9P8APrXnEXwc8SXLD7PIrMrFjcDLMemcemefX3xWjbfs2+JrxGW6vGkVeSMcHofXn8/Sseep0Rnr0MfxL4wvPFFvHc/bJPMjly2GwoHIA9u/r0FUjrmqYih1IM67AqRI/wB0fTuT789fx9F0/wDZ2ZIvM0/cwXC79xG9wSCenT8fwzxWi37Mkcv/AB+XiDa+S7P3z3Pt/nrRGNRkyjM8yg8SXFvEIZrpvmTKnACpn16flXZfsxafHqf7Rnh28+xx3kdvHdl4ZnAjeN7eSAg8EFT5o4PUVNe/s+/2feCSS4aVOdkjH1b/AA9eldh+yz4AtdM+J2p3v2iQvZ2ccaTI2Nu45bntjC5rnxsp0sJJs83Mak6eEk32PIfjR+z/AA/B/wCKN58K9Uh86GFFk0G7uIf+PzT2/wBSckfOVCmJj3kiY4xWfBZ2OiiOOz0WNQj5ZUxtB9SOPxPt7V9t/ty/s7XHxd/ZzPj7wp5g8V+A4ZNSsdj7mubER7ruHGeSFjWVR6xEAAyGvg/wp8Q9P10fY9YZbTUB2b7kvTkH/J5rHA4qFemr7rRiyvFQr0F3WjPQdD+J95os6/abORreWPZOqqSyjjnn8uld3pnxL0++gxb6xJG3B2StjjIJ9xzj8R78eTCWa0uFk8szBsqm0ct/+vir2mapG06ww6asb8BTIcZPTp+I/PNeoe7TlbQNSs9D+Nvg+O+0/UGt7xYCLPU4VCzwyDgxuDxkEYIOR9ODXA+NR4gg0iHUbzzP7W8NyhzsRwZbRiFdSP8AYOJB2xv55NN1XxbJ8JfEMHiixaSLSdcl238ez5bebHyt14zjYeucD8dzxJ8QNN13wxqzSWu64bSpvJdmwsm6JsY/PPf7oPBrFzTg+5wymn11PN/h38WdO8QeJNe+H+o26pPYXj+XKp++mQQP94ZA9OPwrvvh5oXhvTNZMn/Hu4R5fMiwY3Qdc+h/xr5p+CGj3l1qkniS6udst4z3E7O4ZnBOScZz19jgH3FfUPwy/Z58efEP4Y+IPiVrEd5Y+HrPRr59NM2UbVLxYX8tUU4JhSUAtJjBbagyfM2+fTxVoc00cNDF2p89X+ux4lrtr/bGv3mst1uLiR3/ABbIHv8AWrGraZcLY6f4wt428zTLhPtHloMiMnaf5/0rW/swR2HneVgbsgE9cc9B/nk9a2PBYjaVrW4j3RXHyvvxjuDn68E+xoVubU29nzMh0HwxrnjXUjJ4T1jyhJgXKzx4R+nXHP5Y5z60al4G1zQLw2esW62s2crcWrny5MkEn0znr1P513Xwvv7PRPFE/h+SJbfdhYVHCnAz/jXdatFpuq/8S3WLf5NuPMZfywevp+tdiw8Jwv1OiNK8TwvTtD8ZWrTf2XcK7tlm83crc9cDkHPPHHT8+i8A6vrGgTyR6ro91MuYzG3m71A5Jc8nHTOff6131n8KbUXf9nw6pi1mt1MzjgHHTP6f0qxp/wAMtQgvFj0PxZE626bES7tWmWTJ+6WJHsPlPftRGjKD0NI0ZJkOhfEjR7y+Xbp8cwVyVR3+Yyc8HPGfz6/l1Fp44uPETf8ACP6f4fih89tjpcp8ijncZGHb0PJPQVWPhq4e7jj8caPDc7UCxXMLhmA2nAG/bjGAB8xwOnPFOis8Qt4f8Pxrbws6vPdx27jHGCoZxmRjyOhUAg5/hPQufqayTNLStauJo/7D0u6/0d4fMuWkx8gAILM0m47flOEBHpx8zGp8Omj8T6ld6hbx5hlfy4duf9UvyqenoM/jWL8ShH4Q8EzaXpd9N52szR2uXclpHY4ZiepO0MM9hgdMCuu+EtjH4b0GP7Uqwq0WPn4Cj+VWtWZSlaR6J8NrzTvCuswx3Ea7RPFFduMFRDITC+5ScFdjnPGPXA4O98QfFtj4glm0G4+J2k61rlz4qhuoNM0e8iuJtKgt5ZDdy3qxRg229AtqsMzGUtK2B5cUhXx7V/iHpv22fS7O8b55kgE1u+G2Aj09Ru5J25B4Paz8Q/GcjXeqalY6olt/a0MV2NihPMkuFWU47/62Ug4yeG47lVKa50zOGIdGs0t5K33W/wAzqtA1vT28TS+JLCS38vQPD+qXW62TG2WaKG3i3Y4P724jPH1z3qbT5fP0T7HbyIrKoAf0x6f57VwPgrxCLX4PateXkg8zUtdsdNiLNnMYWW6lUj1HlWY/LqOrrDxxbwHbZ3m7d94Fx8w9f8//AKknuVR96bkvT+vvL09jea3e/wBk3lm7STSlGbqqrxk/kOf5V22jeBdL0+BpDbxLJtzK/lDB+nUen+HeuHHxGuLW2by70hnIK71zgA4447Yz1PSob74j64ID/pBKsMuqsGBHU/5x0q/dOrlbPR2h8N2hS3kjjwM/MvVu3+RzU7+INHdRb2scbY+Xag9/p+H5+9eQ3HjuaKX7RdSSXD4KoA2fXA9znH+FSWniPWNSk/0S1VVZtvfb16/1+lHuhKny7npk3jKOBJo7e3ihWI/cV+nY/wAx/wDWqlBq9xrlyrD7oTPGSGHoef8ACsDStG166uFuL5VVG/iVcnOOeTx+XqapfFL40+E/hR4fkj+2LJeSKQIlOZJH+ncjueg9qvmhGN2TLlirl34qfEnwn8PdGvNY8QajHHFbRl3+fOcdAO5PGABkngDNYX/BOvxtqnxNufEXjLUbOQLeeInS3g27jFCsMWBwcZGc88ZJwfT5U+K/i3xd8WtZ/tbxFeeXawM5trBW+WL/AGif429z+AHOfrb/AIJoWUeg+ADb/YvMW41K5mDqh7xhAeD7DofXrmvCzKu6sOVbHzWeVpTw9o7XR+ifw/uLO2kjaS3ZfJHzu2fmIHufYjmvyw/4Kf8A7Hy/sqfG8ah4Ls9ng3xUsuoeGAG2ixdGHnaecc7YS8ZjIz+6ljBOQxr9QvAZupJY/MuJAzvhleUj0BPPP+T+GX+3f+zJZ/tc/sk+IPA+n2yyeJtGj/trwazSDd/acEbbYAfS4iaW2O75czBz90Y8bD1HRreTPAy7FSoV1fZ6P/P5H4+/Dvx5qOnWa29xeSTQYIVnILo46qT/APrr1r4R+KPC2v6u2jXUi2d7Nn7NJMm5W56D3z+PJ9K+d7LVv7E1oaXH80d0+Yyvqeh/ke2MV7T4D+GNtLoUHi3xJM0kcyb7aztnIxg4ySPm/LFfU4eU5xsfeYeo5OyP/9k=", + "gender": [ + { + "language": "eng", + "value": "Male" + } + ], + "phone": "9876785789", + "city": [ + { + "language": "eng", + "value": "Kenitra" + } + ], + "fullName": [ + { + "language": "eng", + "value": "kumar" + } + ], + "addressLine1": [ + { + "language": "eng", + "value": "saravangar" + } + ], + "dateOfBirth": "1999/01/01", + "id": "did:jwk:eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsInVzZSI6InNpZyIsImFsZyI6IlJTMjU2IiwibiI6InBQZTNzOXRCRUstak15dFN6emN3SDAxUHFhTmU0WXQxQ1ByNkxULU9oVVgyQjVJbm5TWERFT294ZC1GalFYRHRJN0VKcW9YdF9DalZaOXJQQ0gxX2R3dzljSVdCWUh4S0R3cEQzQmhLTUhVWTdGdDk3ZTVaYlJkcjlyME5kVmlPRzFXbGxoZ0Q0ZTFWYWJsbDJTNzZ5QjQ1STZ6V05rM0paSzBoX3E2NmxMM2FFVUREbFFJbDFBQVBySHEzaGVzTjZjY2x2R1FoSmEwams2a3ZZQ1BpWXp3N1N3em4wcldMVmg3MDJ6U2ZOSWtXUl9KX0dMd2VhNTNfemJCVzRVdU8tbjVJbGNLdk56RGRSQ21YWGZIOExXWjlmd3BJeGttX0Q0X01aazFfV3hwbmdJUGd0MWYyLXRMU3h5dzJWV25BZnFsS3IyY2M2LS1GMXFLaEx3UGpFdyJ9", + "UIN": "3974189657", + "email": "Aswin@gmail.com" + }, + "id": "https://api.qa-inji.mosip.net/credentials/a45bffe3-ecdf-46c1-b059-0288f3dfe2fa", + "issuanceDate": "2024-07-22T07:49:22.219Z", + "issuer": "https://api.qa-inji.mosip.net/.well-known/ida-controller.json", + "proof": { + "created": "2024-07-22T07:49:22Z", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJraWQiOiJRVmRMTW9uWmNqM0doTUxjbmhuZEdmT0J2RVdHZHg0LU5qazRKRGNxb19NIiwiYWxnIjoiUFMyNTYifQ..lhM2BzYapSIWNI8oHhYKx4na7lOEO-TSzJOndP0PowTGbT1op2AOWdxnozidPckdBIFAPaIAbS4pmVj-jW-QeqiWqERbcHSNePLc-5ykz0VA-q6rFNJs-PJVldZlQMl1Ww_nNpRbTwTirGum3f0tp7MhXcqV_66tQQrFr81odP-rAntPdg-BUpsZ0tXbWUxczmmWshRIw5a67ZrslxVXx0umtE_rx3lVIStSXhUXdbwGOkZPNwAj4AdFTlSbYlRrrazWg1sAqzfT1EE8Kdj7D16TlEyKy011srtMqu2n-sRJBg69mZlcsqlDF6_sIspILMZnKueQvxZqeBOMRmQtpw", + + "proofPurpose": "assertionMethod", + "type": "RsaSignature2018", + "verificationMethod": "https://api.qa-inji.mosip.net/.well-known/ida-public-key.json" + }, + "type": ["VerifiableCredential", "MOSIPVerifiableCredential"] + } +} \ No newline at end of file diff --git a/src/test/resources/responses/expectedIssuerConfig.json b/src/test/resources/responses/expectedIssuerConfig.json new file mode 100644 index 000000000..04e423c31 --- /dev/null +++ b/src/test/resources/responses/expectedIssuerConfig.json @@ -0,0 +1,19 @@ +{ + "credentials_supported": [ + { + "name": "CredentialType1", + "scope": "CredentialType1_vc_ldp", + "display": [ + { + "name": "CredentialType1", + "locale": "en", + "logo": "https://logo" + } + ] + } + ], + "authorization_endpoint": "https://dev/authorize", + "grant_types_supported": [ + "authorization_code" + ] +} diff --git a/src/test/resources/responses/expectedWellknown.json b/src/test/resources/responses/expectedWellknown.json new file mode 100644 index 000000000..862274684 --- /dev/null +++ b/src/test/resources/responses/expectedWellknown.json @@ -0,0 +1,43 @@ +{ + "credential_issuer": "https://dev/issuer1", + "authorization_servers": ["https://auth-server.env.net"], + "credential_endpoint": "https://dev/issuance/credential", + "credential_configurations_supported": { + "CredentialType1": { + "format": "ldp_vc", + "scope": "CredentialType1_vc_ldp", + "proof_types_supported": {}, + "credential_definition": { + "type": [ + "VerifiableCredential", + "CredentialType1" + ], + "credentialSubject": { + "name": { + "display": [ + { + "name": "Given Name", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "CredentialType1", + "locale": "en", + "logo": { + "url": "https://logo", + "alt_text": "logo-url" + }, + "background_image": { + "uri": "https://bgimage" + }, + "background_color": "#B34622", + "text_color": "#FFFFFF" + } + ] + } + } +} \ No newline at end of file