[
  {
    "path": ".codeclimate.yml",
    "content": "version: 2\nplugins:\n  duplication:\n    enabled: true\n    config:\n      languages:\n        javascript:\n          mass_threshold: 50\n\n  # TODO: errors\n  fixme:\n    enabled: false\n\nexclude_patterns:\n- '**/english.ts'\n- '**/benchmarks/'\n- '**/ember-cli-build.js'\n- 'benchmarks/'\n- 'client/android-wrapper/'\n- 'relays/'\n- 'client/web/emberclear/config/'\n- 'client/web/emberclear/tests/'\n- 'client/web/emberclear/vendor/'\n- 'client/web/emberclear/dist/'\n- 'client/web/emberclear/types/'\n- '**/node_modules/'\n- '**/dist/'\n- '**/tests/'\n- '**/config/'\n- '**/script/'\n- '**/vendor/'\n- '**/*.d.ts'\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "// Docs:\n// https://docs.renovatebot.com/configuration-options/\n{\n  \"extends\": [\n    \"config:base\",\n    \"github>NullVoxPopuli/renovate:weekly.json5\",\n    \"github>NullVoxPopuli/renovate:npm-groups.json5\"\n  ],\n  \"automerge\": false,\n  \"masterIssue\": true,\n  \"ignorePaths\": [\n      // Deployed once and forgotten\n      \"client/android-wrapper\"\n  ],\n  \"packageRules\": [\n    {\n      // These are one-time experiements and don't need to be kept up to date\n      \"paths\": [\"benchmarks/**\"],\n      \"enabled\": false\n    },\n    {\n      // libraries and addons aka \"non-apps\"\n      \"paths\": [\"client/web/addons/**/package.json\", \"client/web/libraries/**/package.json\"],\n      \"rangeStrategy\": \"bump\",\n      \"schedule\": [\"after 9pm on sunday\"]\n    },\n    {\n      \"paths\": [\"client/web/emberclear/package.json\", \"client/web/pinochle/package.json\"],\n      // Pin requires more package.json/lockfile churn,\n      // but it allows us to determine if an otherwise floating dep\n      // introduced a regression\n      \"rangeStrategy\": \"pin\",\n      // then setting a schedule reduces the churn and allows for PRs to be combined with the groups below\n      \"schedule\": [\"after 9pm on sunday\"]\n    },\n    {\n      \"paths\": [\"client/web/emberclear/Dockerfile\"],\n      \"enabled\": false\n    },\n    ////////////////////////////////////////\n    // Grouping namespaced packages together\n    ////////////////////////////////////////\n    {\n      \"packagePatterns\": [\"^@babel*\"],\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"groupName\": \"Babel Transpilation\"\n    },\n    {\n      \"packagePatterns\": [\"^@ember-data*\"],\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"groupName\": \"Ember Data\"\n    },\n    {\n      \"packagePatterns\": [\"^@faltest*\"],\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"groupName\": \"FalTest Smoke Testing by CrowdStrike\"\n    },\n    {\n      \"packagePatterns\": [\"^@types\\/*\"],\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"groupName\": \"Type Definitions\"\n    },\n    {\n      \"packagePatterns\": [\"^@embroider*\"],\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"groupName\": \"embroider\"\n    },\n    {\n      \"groupName\": \"Lint Dependencies\",\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"packageNames\": [\n        \"eslint\",\n        \"babel-eslint\",\n        \"ember-template-lint\",\n        \"prettier\"\n      ],\n      \"packagePatterns\": [\n        \"eslint-plugin-.*\",\n        \"eslint-config-.*\",\n        \".*typescript-eslint.*\",\n        \"^@commitlint\\/*\",\n        \"^remark-*\"\n      ],\n    },\n     // These are dependencies that come default when\n    // generating a new ember addon\n    {\n      \"groupName\": \"Framework Dependencies\",\n      \"packageNames\": [\n        \"@ember/optional-features\",\n        \"@glimmer/component\",\n        \"@glimmer/tracking\",\n        \"ember-disable-prototype-extensions\",\n        \"ember-export-application-global\",\n        \"ember-load-initializers\",\n        \"ember-maybe-import-regenerator\",\n        \"ember-resolver\",\n        \"ember-source\",\n        \"ember-cli-page-title\"\n      ]\n    },\n    {\n      \"groupName\": \"CLI Dependencies\",\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"packageNames\": [\n        \"broccoli-asset-rev\",\n        \"ember-cli\",\n        \"ember-auto-import\",\n        \"ember-cli-dependency-checker\",\n        \"ember-cli-inject-live-reload\",\n        \"ember-cli-sri\",\n        \"ember-cli-terser\",\n        \"ember-cli-htmlbars\"\n      ]\n    },\n    {\n      \"groupName\": \"Testing Dependencies\",\n      \"schedule\": [\"after 9pm on sunday\"],\n      \"packageNames\": [\n        \"qunit-dom\",\n        \"ember-try\",\n        \"ember-source-channel-url\",\n        \"ember-qunit\",\n        \"qunit\",\n        \"npm-run-all\",\n        \"@xstate/test\",\n        \"ember-cli-page-object\",\n        \"fractal-page-object\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/web-addon-crypto.yml",
    "content": "name: '@emberclear/crypto'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/crypto/'\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/crypto/\n  full: client/web/addons/crypto/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        # - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        # - \"ember-release\"\n        # - \"ember-beta\"\n        # - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n"
  },
  {
    "path": ".github/workflows/web-addon-encoding.yml",
    "content": "name: '@emberclear/encoding'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/encoding/'\n\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/encoding/\n  full: client/web/addons/encoding/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        # - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        # - \"ember-release\"\n        # - \"ember-beta\"\n        # - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n"
  },
  {
    "path": ".github/workflows/web-addon-local-account.yml",
    "content": "name: '@emberclear/local-account'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/local-account/'\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/local-account/\n  full: client/web/addons/local-account/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        # - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        # - \"ember-release\"\n        # - \"ember-beta\"\n        # - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n"
  },
  {
    "path": ".github/workflows/web-addon-networking.yml",
    "content": "name: '@emberclear/networking'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/networking/'\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/networking/\n  full: client/web/addons/networking/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        # - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        # - \"ember-release\"\n        # - \"ember-beta\"\n        # - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n"
  },
  {
    "path": ".github/workflows/web-addon-test-helpers.yml",
    "content": "name: '@emberclear/test-helpers'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/test-helpers/'\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/test-helpers/\n  full: client/web/addons/test-helpers/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        # - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        # - \"ember-release\"\n        # - \"ember-beta\"\n        # - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n"
  },
  {
    "path": ".github/workflows/web-addon-tracked-local-storage.yml",
    "content": "name: 'ember-tracked-local-storage'\n\n# Inspiration:\n# https://github.com/alexdiliberto/ember-transformicons/blob/master/.github/workflows/ci.yml\non:\n  pull_request:\n  push:\n    # filtering branches here prevents duplicate builds from pull_request and push\n    branches:\n      - master\n    paths:\n      - 'client/web/addons/tracked-local-storage/'\n\nenv:\n  CI: true\n  root: client/web/\n  addon: addons/tracked-local-storage/\n  full: client/web/addons/tracked-local-storage/\n\njobs:\n  lint:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.full }}\n      run: yarn eslint . --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n    # - uses: wagoid/commitlint-github-action@v1\n    #   env:\n    #     GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n\n  tests_ember_compat:\n    if: \"! contains(toJSON(github.event.commits.*.message), '[skip ci]')\"\n    name: Ember Compatability\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        scenario:\n        - \"ember-lts-3.16\"\n        - \"ember-lts-3.20\"\n        - \"ember-release\"\n        - \"ember-beta\"\n        - \"ember-canary\"\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: yarn install\n      working-directory: ${{ env.root }}\n\n    - name: \"Test: ${{ matrix.scenario }}\"\n      working-directory: ${{ env.full }}\n      run: yarn test:try-one ${{ matrix.scenario }}\n\n  # TODO: figure out if this supports working-directory\n  # publish:\n  #   name: Release\n  #   runs-on: ubuntu-latest\n  #   if: github.ref == 'refs/heads/master'\n  #   needs: [tests_ember_compat, lint]\n\n  #   steps:\n  #     - uses: actions/checkout@v1\n  #     - uses: volta-cli/action@v1\n\n  #     - run: yarn install\n  #       working-directory: ${{ env.root }}\n\n  #     - name: Release\n  #       working-directory: ${{ env.root }}\n  #       env:\n  #         GITHUB_TOKEN: ${{ secrets.GH_PAT }}\n  #         NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n  #       run: yarn semantic-release\n"
  },
  {
    "path": ".github/workflows/web-addon-ui.yml",
    "content": "name: 'Web @emberclear/ui'\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/addons/ui/**'\n    - 'client/web/package.json'\n\nenv:\n  root: client/web/\n  addon: addons/ui/\n  full: client/web/addons/ui/\n\njobs:\n  lint:\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.root }}\n      run: yarn eslint ${{ env.addon }} --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: \"Styles\"\n      working-directory: ${{ env.root }}\n      run: yarn stylelint ${{ env.addon }}**/*.css\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n  test:\n    name: 'Test'\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: Ember\n      working-directory: \"${{ env.root }}/${{ env.addon }}\"\n      run: yarn test\n\n\n\n\n\n"
  },
  {
    "path": ".github/workflows/web-app-deploy.yml",
    "content": "name: Web App Deploy\non:\n  push:\n    branches: [master]\n    paths:\n    - 'client/web/**'\n\nenv:\n  cwd: client/web/emberclear\n\njobs:\n  tests:\n    name: Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - name: Test\n      working-directory: ${{ env.cwd }}\n      env:\n        PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}\n      # run: ./scripts/test-with-coverage.sh\n      run: yarn ember test\n\n    # Coverage Disabled while these are worked out:\n    # https://github.com/babel/ember-cli-babel/issues/350\n    # https://github.com/kategengler/ember-cli-code-coverage/issues/265\n    # - name: Upload Coverage to Coveralls\n    #   uses: coverallsapp/github-action@v1.0.1\n    #   with:\n    #     github-token: ${{ secrets.github_token }}\n    #     path-to-lcov: ./client/web/emberclear/coverage/lcov.info\n\n    # - name: Upload Coverage Artifacts\n    #   uses: actions/upload-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n  deploy:\n    name: Deploy to Netlify\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs:\n    - tests\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    # - name: Download Coverage Artifacts\n    #   uses: actions/download-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - run: yarn global add netlify-cli\n    - name: Deploy to Netlify\n      env:\n        FRONTEND: client/web/emberclear\n        NETLIFY_ACCESS_TOKEN: ${{ secrets.NETLIFY_ACCESS_TOKEN }}\n        NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}\n      run: |\n        COVERAGE_DIR=\"client/web/emberclear/coverage\"\n\n        if [ -d \"$COVERAGE_DIR\" ]; then\n          mv $COVERAGE_DIR client/web/emberclear/public/\n        fi\n\n        ( cd client/web/emberclear \\\n          && time yarn analyze \\\n          && time yarn build:production\n        )\n\n        time ./scripts/publish\n\n    - name: Upload Built Asset Artifacts\n      uses: actions/upload-artifact@master\n      with:\n        name: frontend-assets\n        path: client/web/emberclear/dist/\n\n  deploy_docker:\n    name: Deploy Docker Image\n    runs-on: ubuntu-latest\n    needs: [tests, deploy]\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Download Built Asset Artifacts\n      uses: actions/download-artifact@master\n      with:\n        name: frontend-assets\n        path: client/web/emberclear/dist/\n\n    - name: Publish to Registry\n      uses: elgohr/Publish-Docker-Github-Action@master\n      with:\n        name: nullvoxpopuli/emberclear\n        username: ${{ secrets.DOCKERHUB_USER }}\n        password: ${{ secrets.DOCKERHUB_PASSWORD }}\n        snapshot: true\n        workdir: client/web/emberclear\n        dockerfile: Dockerfile.release\n\n  tests_e2e:\n    name: E2E Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs: [deploy]\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Test\n      run: |\n        cd client/web/smoke-tests \\\n        && yarn \\\n        && yarn test --headless\n\n\n# Deploy via Script (requires docker environment on VM)\n#     - uses: actions/docker/cli@master\n#     - name: Publish to DockerHub\n#       env:\n#         DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}\n#         DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}\n#         # DOCKER_HOST: tcp://docker:2375/\n#         # DOCKER_DRIVER: overlay2\n#       run: sh ./scripts/dockerhub\n\n\n\n"
  },
  {
    "path": ".github/workflows/web-app-quality.yml",
    "content": "name: Web App Quality\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/emberclear/**'\n    - 'client/web/package.json'\n\nenv:\n  root: client/web/\n  app: emberclear/\n  full: client/web/emberclear/\n\njobs:\n  security:\n    name: Dependency Security\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: volta install node && volta install snyk\n\n    - name: Snyk\n      working-directory: ${{ env.root }}\n      if: github.event == 'pull_request'\n      env:\n        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n      run: snyk test --severity-threshold=high\n\n  lint:\n    name: \"Lint JS/TS & Type Checking\"\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.root }}\n      run: yarn eslint ${{ env.app }} --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: \"Styles\"\n      working-directory: ${{ env.root }}\n      run: yarn stylelint ${{ env.app }}**/*.css\n\n    - name: \"Translations\"\n      working-directory: ${{ env.full }}\n      run: yarn lint:i18n\n      continue-on-error: true\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n"
  },
  {
    "path": ".github/workflows/web-app-tests.yml",
    "content": "name: Web App Tests\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/**'\n\nenv:\n  cwd: client/web/emberclear\n  name: emberclear\n\n##############################################################\n\njobs:\n  tests:\n    name: Tests\n    strategy:\n      matrix:\n        # os: [ubuntu-latest, macOS-latest, windows-latest]\n        # browsers: [chrome, firefox, safari, edge]\n        ci_browser:\n        - Chrome\n        # Firefox is flaky in Github........\n        # - Firefox\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - uses: actions/cache@v2\n      with:\n        path: '**/node_modules'\n        key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - name: Test\n      working-directory: ${{ env.cwd }}\n      env:\n        PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}\n        CI_BROWSER: ${{ matrix.ci_browser }}\n      # run: ./scripts/test-with-coverage.sh\n      run: yarn ember test\n\n    # Coverage Disabled while these are worked out:\n    # https://github.com/babel/ember-cli-babel/issues/350\n    # https://github.com/kategengler/ember-cli-code-coverage/issues/265\n    #\n    # - name: Upload Coverage to Coveralls\n    #   uses: coverallsapp/github-action@v1.0.1\n    #   with:\n    #     github-token: ${{ secrets.github_token }}\n    #     path-to-lcov: ./client/web/emberclear/coverage/lcov.info\n\n    # - name: Upload Coverage Artifacts\n    #   uses: actions/upload-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n\n##############################################################\n#\n  bundle_analysis:\n    name: Bundle Analysis\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - uses: actions/cache@v2\n      with:\n        path: '**/node_modules'\n        key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - name: Analyze Bundle\n      working-directory: ${{ env.cwd }}\n      run: yarn analyze\n\n    - name: Upload Bundle Analysis Artifacts\n      uses: actions/upload-artifact@v2\n      with:\n        name: built_bundle_analysis\n        path: client/web/emberclear/public/bundle\n\n\n  build_app:\n    name: Build App\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - uses: actions/cache@v2\n      with:\n        path: '**/node_modules'\n        key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n    - name: Install and Build\n      run: |\n        ( cd client/web/emberclear && yarn install && yarn build:production )\n        cp ${{ env.cwd }}/config/netlify/_redirects ${{ env.cwd }}/dist\n\n    - name: Upload App Artifacts\n      uses: actions/upload-artifact@v2\n      with:\n        name: built_app\n        path: client/web/emberclear/dist/\n\n##############################################################\n\n  tests_e2e:\n    name: E2E Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs: [deploy_preview]\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Get Deploy URL\n      uses: actions/download-artifact@master\n      with:\n        name: deploy-url\n        path: ./\n\n    - name: Test\n      run: |\n        export DEPLOY_URL=$(cat ./deploy-url.txt)\n        cd client/web/smoke-tests\n        DETECT_CHROMEDRIVER_VERSION=true yarn\n        yarn test --target pull-request --headless\n\n\n##############################################################\n\n  deploy_preview:\n    name: Deploy Preview\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs: [bundle_analysis, build_app]\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Download Built Bundle Analysis Artifacts\n      uses: actions/download-artifact@master\n      with:\n        name: built_bundle_analysis\n        path: ./deploy/bundle-analysis/\n\n    - name: Download Built App Artifacts\n      uses: actions/download-artifact@master\n      with:\n        name: built_app\n        path: ./deploy/app/\n\n    - name: Combine Bundle Analysis with App\n      run: |\n        mkdir -p ./deploy/dist/bundle/\n        mv ./deploy/app/* ./deploy/dist/\n        cp ./deploy/bundle-analysis/* ./deploy/dist/bundle/\n        cp ${{ env.cwd }}/config/netlify/_redirects ./deploy/dist/\n\n    - name: Deploy to Netlify\n      id: deploy\n      uses: nwtgck/actions-netlify@v1.2.2\n      with:\n        publish-dir: './deploy/dist'\n        production-branch: __handled_separately__\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n      env:\n        NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_ACCESS_TOKEN }}\n        NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}\n\n    - run: echo \"${{ steps.deploy.outputs.deploy-url }}\" > deploy-url.txt\n\n    - name: Upload URL as Artifact\n      uses: actions/upload-artifact@v2\n      with:\n        name: deploy-url\n        path: deploy-url.txt\n\n\n##############################################################\n\n  lhci:\n    name: Lighthouse CI\n    runs-on: ubuntu-latest\n    needs: [build_app]\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Download Built App Artifacts\n      uses: actions/download-artifact@master\n      with:\n        name: built_app\n        path: client/web/emberclear/dist/\n\n    - name: run Lighthouse\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}\n      run: |\n        volta install node\n        volta install @lhci/cli@0.3.x\n        cd client/web/emberclear\n\n        lhci collect \\\n          --upload.target=temporary-public-storage \\\n          --staticDistDir=./dist \\\n          --githubToken $GITHUB_TOKEN \\\n          --githubAppToken $LHCI_GITHUB_APP_TOKEN\n"
  },
  {
    "path": ".github/workflows/web-pinochle-deploy.yml",
    "content": "name: Web Pinochle Deploy\non:\n  push:\n    branches: [master]\n    paths:\n    - 'client/web/pinochle/**'\n    - '.github/workflows/web-pinochle-*'\n\nenv:\n  cwd: client/web/pinochle\n\njobs:\n  tests:\n    name: Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - name: Test\n      working-directory: ${{ env.cwd }}\n      # run: ./scripts/test-with-coverage.sh\n      run: yarn ember test\n\n    # Coverage Disabled while these are worked out:\n    # https://github.com/babel/ember-cli-babel/issues/350\n    # https://github.com/kategengler/ember-cli-code-coverage/issues/265\n    # - name: Upload Coverage to Coveralls\n    #   uses: coverallsapp/github-action@v1.0.1\n    #   with:\n    #     github-token: ${{ secrets.github_token }}\n    #     path-to-lcov: ./client/web/emberclear/coverage/lcov.info\n\n    # - name: Upload Coverage Artifacts\n    #   uses: actions/upload-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n  deploy:\n    name: Deploy to Netlify\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs:\n    - tests\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    # - name: Download Coverage Artifacts\n    #   uses: actions/download-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - run: yarn global add netlify-cli\n    - name: Deploy to Netlify\n      env:\n        FRONTEND: ${{ env.cwd }}\n        NETLIFY_ACCESS_TOKEN: ${{ secrets.NETLIFY_ACCESS_TOKEN }}\n        NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PINOCHLE }}\n        NETLIFY_CLI_VERSION: 0.4.0\n      run: |\n        COVERAGE_DIR=\"${{ env.cwd }}/coverage\"\n\n        if [ -d \"$COVERAGE_DIR\" ]; then\n          mv $COVERAGE_DIR ${{ env.cwd }}/public/\n        fi\n\n        ( cd ${{ env.cwd }} \\\n          && time yarn build:production\n        )\n\n        time ./scripts/publish\n\n    - name: Upload Built Asset Artifacts\n      uses: actions/upload-artifact@master\n      with:\n        name: frontend-assets\n        path: ${{ env.cwd }}/dist/\n\n  # tests_e2e:\n  #   name: E2E Tests\n  #   runs-on: ubuntu-latest\n  #   timeout-minutes: 15\n  #   needs:\n  #   - deploy\n\n  #   steps:\n  #   - name: 'Wait for status checks'\n  #     id: waitforstatuschecks\n  #     uses: \"wyrihaximus/github-action-wait-for-status@v2\"\n  #     with:\n  #       ignoreActions: tests_e2e,\"E2E Tests\"\n  #       checkInterval: 30\n\n  #   - uses: actions/checkout@v2\n  #   - uses: volta-cli/action@v1\n\n  #   - name: Test\n  #     run: |\n  #       cd client/web/smoke-tests \\\n  #       && yarn \\\n  #       && yarn test --headless\n\n\n# Deploy via Script (requires docker environment on VM)\n#     - uses: actions/docker/cli@master\n#     - name: Publish to DockerHub\n#       env:\n#         DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}\n#         DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}\n#         # DOCKER_HOST: tcp://docker:2375/\n#         # DOCKER_DRIVER: overlay2\n#       run: sh ./scripts/dockerhub\n\n\n\n"
  },
  {
    "path": ".github/workflows/web-pinochle-quality.yml",
    "content": "name: Web Pinochle Quality\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/pinochle/**'\n    - 'client/web/package.json'\n\nenv:\n  root: client/web/\n  app: pinochle/\n  full: client/web/pinochle/\n\njobs:\n  security:\n    name: Dependency Security\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      run: volta install node && volta install snyk\n\n    - name: Snyk\n      working-directory: ${{ env.root }}\n      if: github.event == 'pull_request'\n      env:\n        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n      run: snyk test --severity-threshold=high\n\n  lint:\n    name: \"Lint JS/TS & Type Checking\"\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.root }}\n      run: yarn eslint ${{ env.app }} --ext js,ts\n\n    - name: \"Templates\"\n      working-directory: ${{ env.full }}\n      run: yarn ember-template-lint .\n\n    - name: \"Styles\"\n      working-directory: ${{ env.root }}\n      run: yarn stylelint ${{ env.app }}**/*.css\n\n    - name: 'Type Correctness'\n      working-directory: ${{ env.full }}\n      run: yarn tsc --build\n\n"
  },
  {
    "path": ".github/workflows/web-pinochle-tests.yml",
    "content": "name: Web Pinochle Tests\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/pinochle/**'\n\nenv:\n  cwd: client/web/pinochle\n  name: pinochle\n\n##############################################################\n\njobs:\n  tests:\n    name: Tests\n    strategy:\n      matrix:\n        # os: [ubuntu-latest, macOS-latest, windows-latest]\n        # browsers: [chrome, firefox, safari, edge]\n        ci_browser:\n        - Chrome\n        - Firefox\n        # - Safari\n\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - uses: actions/cache@v2\n      with:\n        path: '**/node_modules'\n        key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n\n    - name: Install\n      working-directory: ${{ env.cwd }}\n      run: yarn install\n\n    - name: Test\n      working-directory: ${{ env.cwd }}\n      env:\n        CI_BROWSER: ${{ matrix.ci_browser }}\n      # run: ./scripts/test-with-coverage.sh\n      run: yarn ember test\n\n    # Coverage Disabled while these are worked out:\n    # https://github.com/babel/ember-cli-babel/issues/350\n    # https://github.com/kategengler/ember-cli-code-coverage/issues/265\n    #\n    # - name: Upload Coverage to Coveralls\n    #   uses: coverallsapp/github-action@v1.0.1\n    #   with:\n    #     github-token: ${{ secrets.github_token }}\n    #     path-to-lcov: ./client/web/emberclear/coverage/lcov.info\n\n    # - name: Upload Coverage Artifacts\n    #   uses: actions/upload-artifact@v1\n    #   with:\n    #     name: coverage\n    #     path: client/web/emberclear/coverage/\n\n\n##############################################################\n#\n  # bundle_analysis:\n  #   name: Bundle Analysis\n  #   runs-on: ubuntu-latest\n  #   timeout-minutes: 15\n\n  #   steps:\n  #   - uses: actions/checkout@v2\n  #   - uses: volta-cli/action@v1\n  #   - uses: actions/cache@v2\n  #     with:\n  #       path: '**/node_modules'\n  #       key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n\n  #   - name: Install\n  #     working-directory: ${{ env.cwd }}\n  #     run: yarn install\n\n  #   - name: Analyze Bundle\n  #     working-directory: ${{ env.cwd }}\n  #     run: yarn analyze\n\n  #   - name: Upload Bundle Analysis Artifacts\n  #     uses: actions/upload-artifact@v2\n  #     with:\n  #       name: built_bundle_analysis\n  #       path: client/web/emberclear/public/bundle\n\n\n  build_app:\n    name: Build App\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n    - uses: actions/cache@v2\n      with:\n        path: '**/node_modules'\n        key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}\n    - name: Install and Build\n      run: |\n        ( cd ${{ env.cwd }} && yarn install && yarn build:production )\n        cp ${{ env.cwd }}/config/netlify/_redirects ${{ env.cwd }}/dist\n\n    - name: Upload App Artifacts\n      uses: actions/upload-artifact@v2\n      with:\n        name: built_${{ env.name }}\n        path: ${{ env.cwd }}/dist/\n\n##############################################################\n# TODO: scope e2e tests to app\n\n  # tests_e2e:\n  #   name: E2E Tests\n  #   runs-on: ubuntu-latest\n  #   timeout-minutes: 15\n  #   needs: [deploy_preview]\n\n  #   steps:\n  #   - uses: actions/checkout@v2\n  #   - uses: volta-cli/action@v1\n\n  #   - name: Get Deploy URL\n  #     uses: actions/download-artifact@master\n  #     with:\n  #       name: deploy-url\n  #       path: ./\n\n  #   - name: Test\n  #     run: |\n  #       export DEPLOY_URL=$(cat ./deploy-url.txt)\n  #       cd client/web/smoke-tests\n  #       DETECT_CHROMEDRIVER_VERSION=true yarn\n  #       yarn test --target pull-request --headless\n\n\n##############################################################\n\n  deploy_preview:\n    name: Deploy Preview\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    needs:\n    # - bundle_analysis\n    - build_app\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    # - name: Download Built Bundle Analysis Artifacts\n    #   uses: actions/download-artifact@master\n    #   with:\n    #     name: built_bundle_analysis\n    #     path: ./deploy/bundle-analysis/\n\n    - name: Download Built App Artifacts\n      uses: actions/download-artifact@master\n      with:\n        name: built_${{ env.name }}\n        path: ./deploy/app/\n\n    - name: Combine Bundle Analysis with App\n      run: |\n        mkdir -p ./deploy/dist/bundle/\n        mv ./deploy/app/* ./deploy/dist/\n        cp ${{ env.cwd }}/config/netlify/_redirects ./deploy/dist/\n    #     cp ./deploy/bundle-analysis/* ./deploy/dist/bundle/\n\n    - name: Deploy to Netlify\n      id: deploy\n      uses: nwtgck/actions-netlify@v1.2.2\n      with:\n        publish-dir: './deploy/dist'\n        production-branch: __handled_separately__\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n      env:\n        NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_ACCESS_TOKEN }}\n        NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PINOCHLE }}\n\n    - run: echo \"${{ steps.deploy.outputs.deploy-url }}\" > deploy-url.txt\n\n    - name: Upload URL as Artifact\n      uses: actions/upload-artifact@v2\n      with:\n        name: deploy-url\n        path: deploy-url.txt\n\n\n##############################################################\n\n  # lhci:\n  #   name: Lighthouse CI\n  #   runs-on: ubuntu-latest\n  #   needs: [build_app]\n\n  #   steps:\n  #   - uses: actions/checkout@v2\n  #   - uses: volta-cli/action@v1\n\n  #   - name: Download Built App Artifacts\n  #     uses: actions/download-artifact@master\n  #     with:\n  #       name: built_app\n  #       path: ${{ env.cwd }}/dist/\n\n  #   - name: run Lighthouse\n  #     env:\n  #       GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  #       LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}\n  #     run: |\n  #       volta install node\n  #       volta install @lhci/cli@0.3.x\n  #       cd ${{ env.cwd }}\n\n  #       lhci collect \\\n  #         --upload.target=temporary-public-storage \\\n  #         --staticDistDir=./dist \\\n  #         --githubToken $GITHUB_TOKEN \\\n  #         --githubAppToken $LHCI_GITHUB_APP_TOKEN\n"
  },
  {
    "path": ".github/workflows/web-smoke-tests.yml",
    "content": "name: Web Smoke Tests\non:\n  pull_request:\n    branches: [master]\n    paths:\n    - 'client/web/smoke-tests/**'\n    - 'client/web/package.json'\n\nenv:\n  root: client/web/\n  dir: smoke-tests/\n  full: client/web/addons/ui/\n\n##############################################################\n\njobs:\n  lint:\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Install\n      working-directory: ${{ env.root }}\n      run: yarn install\n\n    - name: \"JS/TS\"\n      working-directory: ${{ env.root }}\n      run: yarn eslint ${{ env.dir }} --ext js,ts\n\n    # No TypeScript (yet)\n    # - name: 'Type Correctness'\n    #   working-directory: ${{ env.full }}\n    #   run: yarn tsc --skipLibCheck --noEmit\n\n##############################################################\n\n  tests:\n    name: E2E Tests\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n\n    steps:\n    - uses: actions/checkout@v2\n    - uses: volta-cli/action@v1\n\n    - name: Test Production\n      run: |\n        cd client/web/smoke-tests\n        DETECT_CHROMEDRIVER_VERSION=true yarn\n        yarn test --headless\n\n\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndeclarations/\ntmp\ndist\n_build\ndeps\n*.log\n.idea/\n.DS_Store\n\nclient/keystore.jks\nclient/upload_certificate.pem\nclient/Google Play Store Info\n\n# Local Netlify folder\n.netlify\n\n# Local lighthouse ci\n.lighthouseci\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"relays/phoenix-relay\"]\n\tpath = relays/phoenix-relay\n\turl = https://github.com/NullVoxPopuli/mesh-relay-phoenix.git\n"
  },
  {
    "path": ".sonarcloud.properties",
    "content": "sonar.exclusions=**/*-test.{ts,js}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n          \"type\": \"node\",\n          \"request\": \"launch\",\n          \"name\": \"emberclear UI\",\n          \"skipFiles\": [\"<node_internals>/**\"],\n          \"cwd\": \"${workspaceFolder}/client/web/emberclear/\",\n          \"runtimeExecutable\": \"yarn\",\n          \"runtimeArgs\": [\"start\"],\n        },\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Frontend E2E Tests\",\n            \"skipFiles\": [\"<node_internals>/**\"],\n            \"cwd\": \"${workspaceFolder}/client/web/smoke-tests/\",\n            \"program\": \"${workspaceFolder}/client/web/smoke-tests/node_modules/@faltest/cli/bin/index.js\",\n            \"args\": [\n              \"--browsers\", \"2\",\n              \"--timeouts-override\", \"900000\",\n              \"--target\",\n              \"ember\",\n              // \"local\",\n            ]\n\n        }\n    ]\n}\n"
  },
  {
    "path": ".whitesource",
    "content": "{\n  \"checkRunSettings\": {\n    \"vulnerableCheckRunConclusionLevel\": \"failure\"\n  },\n  \"issueSettings\": {\n    \"minSeverityLevel\": \"LOW\"\n  }\n}"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Change Log\n\n### 2019-01\n \n - Bugfix: configurable relays did not correctly implement relay selection during app boot\n\n### 2018-12\n\n - Enhancement: Split out the settings screen for future expandability.\n - Enhancement: Allow the relays to be configured.\n - Enhancement: Ensure that the default relays always exist as configuration options.\n - Bugfix: Sometimes the toast messages would not have a background color, making them very hard to read.\n - Chore: Reorganize some toplevel components to be a part of the application route's private collection.\n - Enhancement: for browsers that are not compatible, show a compatibility message.\n - Bugfix: Regression where notification prompt would not hide\n - Enhancement: Can mark a message for automatic resend. When the recipient isn't online, a message cannot be sent to them. With a message marked for automatic resend, the next time the recipient comes online, the message will be immediately sent, without the need to interact with the Chat.\n\n\n - Chore: Change Log Created. Tagged releases will be monthly.\n - See git log for prior enhancements, bugfixes, etc\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Building\nemberclear can be built and run with\n```\ncd client/web/emberclear\nyarn install\nyarn start:dev\n```\nand then can be visited at `https://localhost:4201/`.\n\n## Testing\nRun the tests locally with\n```\ncd client/web/emberclear\nyarn test\n```\n\n## For working with the Relay\n```bash\ngit submodule update --init --recursive\ncd client/web/emberclear && yarn start:dev\n```\n\n\n#### Debugging\n\nModule Resolution:\n```js\n// shows all detected services\nObject.keys(window.requirejs.entries).filter(b => b.includes(\"service\"))\n```\n\nFile Watch Problems?\n```bash\necho fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "\nGNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc.\n\nEveryone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\n\nPreamble The GNU General Public License is a free, copyleft license for software and other kinds of works.\n\nThe licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.\n\nSome devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and modification follow.\n\nTERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License.\n\n“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.\n\n“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.\n\nTo “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.\n\nA “covered work” means either the unmodified Program or a work based on the Program.\n\nTo “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.\n\nTo “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.\n\n1. Source Code. The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.\n\nA “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.\n\nThe “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.\n\nThe “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.\n\nThe Corresponding Source for a work in source code form is that same work.\n\n2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.\n\nWhen you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.\n\n4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:\n\na) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.\n\n6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:\n\na) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.\n\nA “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.\n\n“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.\n\nIf you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).\n\nThe requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.\n\n7. Additional Terms. “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:\n\na) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.\n\n8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).\n\nHowever, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.\n\nTermination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.\n\n9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.\n\nAn “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.\n\n11. Patents. A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.\n\nA contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.\n\nIn the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.\n\nA patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.\n\n14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.\n\nLater license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.\n\n15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.\n\nCopyright (C)\n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:\n\nCopyright (C)\n\nThis program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.\n\nYou should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see .\n\nThe GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .\n"
  },
  {
    "path": "README.md",
    "content": "# [emberclear](https://emberclear.io)\n\nemberclear is published at: https://emberclear.io\nand can be run locally with docker via\n```\ndocker run -d -p 4201:80 nullvoxpopuli/emberclear\n```\nand then can be visited at `http://localhost:4201`.\n\n## Project Directory\n\n- [Browser Client](https://github.com/NullVoxPopuli/emberclear/tree/master/client/web/emberclear)\n- [Phoenix Relay](https://github.com/NullVoxPopuli/mesh-relay-phoenix)\n- [Benchmarks](https://github.com/NullVoxPopuli/emberclear/tree/master/benchmarks)\n\n## Another Chat App?\n\nYes, there is a lack of trust that manifests when existing chat apps are closed source and centralized. Emberclear, by design, is trustless -- meaning that, while there is a server component, the server knows nothing more than your \"_public_ key\".  The server(s) are also meant to be a hot-swappable member of a mesh network, so no one implementation matters, as long as the same protocol is used.\n\n<a href='https://docs.google.com/spreadsheets/d/116MpTXfga_f8N0tLSY_Glt_fd4GIag9T5-P_mag7RlQ/edit#gid=0'  target='_blank'>\n  Here is a table of detailing out some differences between emberclear and other chat apps:\n  <img src='https://gitlab.com/NullVoxPopuli/emberclear/raw/master/images/comparison.png'>\n</a>\n\n## Development\n\nSee: [CONTRIBUTING.md](https://github.com/NullVoxPopuli/emberclear/blob/master/CONTRIBUTING.md)\n\n## Special Thanks\n\n<a href='http://browserstack.com' target='_blank'><img src='https://p14.zdusercontent.com/attachment/1015988/tPHKnEGj5UmlAZin6VBzV2PXP?token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.._iQRaP0Z2EIo_bydcVxYgw.a45ScjGVDLEUj-eKschCJj2H2GnIwrb3H7fcFAHZsJIhdlVh2SLlVb3_DQcig6s1S4osAt-jNocejQdDlB-jq4DotpLlG2xXvIOO-MssjlDu5QQbCU5XwPyT2hk_0fHTVyCznoiup70QSnwfUm-xcl0bbxZI8ljgy1wQtzoqTd2CRovrOwfzQNXFg_MQ6TWkx5tkQDzhV0GbxIffZwN6s-4f5AHRNRP-3rbxtuEy6Lkz3WdQXbdynMcL2ElOS4h_zt7hEj0XRs1xNIQQhTsnjay4ZQvYSVfH13_aY3jVgVI.n_nXLbZaW3gj-FJcQxKD4A' width=100></a>\n - Cross-Browser / Cross-Platform Testing and Automation\n\n## License\n\n[GNU General Public License version 3](https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)#summary)\n"
  },
  {
    "path": "benchmarks/crypto/.babelrc.js",
    "content": "module.exports = {\n  presets: [\n    ['@babel/env', {\n      targets: {\n        node: '10'\n      }\n    }],\n    '@babel/preset-typescript',\n  ],\n  plugins: [\n    '@babel/plugin-transform-modules-commonjs',\n    '@babel/plugin-syntax-dynamic-import',\n    '@babel/plugin-proposal-class-properties',\n    ['@babel/plugin-proposal-decorators', { legacy: true }],\n    '@babel/plugin-transform-runtime',\n  ]\n};\n"
  },
  {
    "path": "benchmarks/crypto/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "benchmarks/crypto/package.json",
    "content": "{\n  \"name\": \"crypto-benchmarks\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"babel ./src --out-dir ./build --extensions '.ts'\",\n    \"start\": \"node build/index.js\",\n    \"bench\": \"yarn build && yarn start\",\n    \"debug\": \"node --inspect-brk=9229 build/index.js\"\n  },\n  \"dependencies\": {\n    \"js-nacl\": \"^1.3.2\",\n    \"libsodium-wrappers\": \"^0.7.3\",\n    \"tweetnacl\": \"^1.0.0\",\n    \"tweetnacl-util\": \"^0.15.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"7.6.2\",\n    \"@babel/core\": \"7.6.2\",\n    \"@babel/node\": \"7.6.2\",\n    \"@babel/plugin-proposal-class-properties\": \"7.5.5\",\n    \"@babel/plugin-proposal-decorators\": \"7.6.0\",\n    \"@babel/plugin-syntax-dynamic-import\": \"7.2.0\",\n    \"@babel/plugin-transform-modules-commonjs\": \"7.6.0\",\n    \"@babel/plugin-transform-runtime\": \"7.6.2\",\n    \"@babel/preset-env\": \"7.6.2\",\n    \"@babel/preset-typescript\": \"7.6.0\",\n    \"@babel/register\": \"7.6.2\",\n    \"@babel/runtime\": \"7.6.2\",\n    \"@types/js-nacl\": \"1.2.0\",\n    \"@types/libsodium-wrappers\": \"0.7.5\",\n    \"@types/node\": \"12.7.12\",\n    \"asyncmark\": \"0.3.1\",\n    \"typescript\": \"3.6.3\"\n  }\n}\n"
  },
  {
    "path": "benchmarks/crypto/run",
    "content": "#!/bin/bash\n\nyarn\nyarn build\nyarn start\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/base64.ts",
    "content": "import { Suite } from \"asyncmark\";\nimport libsodiumWrapper from \"libsodium-wrappers\";\n\nimport * as jsNaCl from \"../lib/js-nacl\";\nimport {\n  libsodium,\n  tweetnacl,\n} from \"../lib/utils\";\n\nconst msg = Uint8Array.from([104, 101, 108, 101, 111]); // hello\n\nexport const base64 = new Suite({\n  async before() {\n    await libsodiumWrapper.ready;\n    await jsNaCl.setInstance();\n\n    console.log(\"\\nround trip base64 encode + decode\");\n  }\n});\n\nbase64.add({\n  name: \"libsodium\",\n  fun: async () => {\n    libsodium.fromBase64(await libsodium.toBase64(msg));\n  }\n});\n\nbase64.add({\n  name: \"tweetnacl\",\n  fun: async () => {\n    tweetnacl.fromBase64(await tweetnacl.toBase64(msg));\n  }\n});\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/hex.ts",
    "content": "import { Suite } from \"asyncmark\";\nimport libsodiumWrapper from \"libsodium-wrappers\";\n\nimport * as jsNaCl from \"../lib/js-nacl\";\n\nimport {\n  libsodium,\n  tweetnacl,\n  jsnacl,\n} from \"../lib/utils\";\nimport { Buffer } from \"buffer\";\n\nconst msg = Uint8Array.from([104, 101, 108, 101, 111]); // hello\n\nexport const hex = new Suite({\n  async before() {\n    await libsodiumWrapper.ready;\n    await jsNaCl.setInstance();\n\n    console.log(\"\\nround trip uint8 <-> string encode + decode\");\n  }\n});\n\nhex.add({\n  name: \"libsodium\",\n  fun: async () => {\n    libsodium.fromHex(await libsodium.toHex(msg));\n  }\n});\n\n// hex.add({\n//   name: \"tweet-nacl\",\n//   fun: async () => {\n//     tweetnacl.fromString(await tweetnacl.toString(msg));\n//   }\n// });\n\nhex.add({\n  name: \"js-nacl\",\n  fun: async () => {\n    jsnacl.fromHex(await jsnacl.toHex(msg));\n  }\n});\n\nhex.add({\n  name: \"native \",\n  fun: async() => {\n    const hex = Array.from(msg).map (b => b.toString(16).padStart(2, \"0\")).join(\"\");\n    return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));\n  }\n});"
  },
  {
    "path": "benchmarks/crypto/src/bench/key-generation.ts",
    "content": "import { Suite } from 'asyncmark';\n\nimport * as libsodiumjs from '../lib/libsodium';\nimport * as tweetNaCl from  '../lib/tweet-nacl';\nimport * as jsNaCl from '../lib/js-nacl';\n\nexport const keyGeneration = new Suite({\n  async before() {\n    console.log('\\nKey Generation');\n    await jsNaCl.setInstance();\n  }\n});\n\nkeyGeneration.add({\n  name: 'libsodium',\n  fun: libsodiumjs.generateAsymmetricKeys,\n\n});\n\nkeyGeneration.add({\n  name: 'tweetnacl',\n  fun: tweetNaCl.generateAsymmetricKeys,\n});\n\nkeyGeneration.add({\n  name: 'js-nacl',\n  fun: jsNaCl.generateAsymmetricKeys,\n})\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/nonce-generation.ts",
    "content": "import { Suite } from 'asyncmark';\n\nimport * as libsodiumjs from '../lib/libsodium';\nimport * as tweetNaCl from  '../lib/tweet-nacl';\nimport * as jsNaCl from '../lib/js-nacl';\n\nexport const nonceGeneration = new Suite({\n  async before() {\n    console.log('\\nNonce Generation');\n    await jsNaCl.setInstance();\n  }\n});\n\nnonceGeneration.add({\n  name: 'libsodium',\n  fun: libsodiumjs.generateNonce,\n\n});\n\nnonceGeneration.add({\n  name: 'tweetnacl',\n  fun: tweetNaCl.generateNonce,\n});\n\nnonceGeneration.add({\n  name: 'js-nacl',\n  fun: jsNaCl.generateNonce,\n});\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/round-trip-long.ts",
    "content": "import { Suite } from 'asyncmark';\nimport libsodiumWrapper from 'libsodium-wrappers';\n\nimport { fromString } from '../utils';\nimport * as jsNaCl from '../lib/js-nacl';\nimport { libsodium, jsnacl, tweetnacl } from '../lib/round-trip-implementations';\n\nconst lorem = `\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse libero risus, porttitor nec urna ut, pellentesque feugiat ex. Integer viverra enim at pulvinar congue. Nunc et turpis vitae nisi maximus laoreet. Mauris malesuada lorem arcu, ut suscipit ante dictum nec. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Maecenas eget ex at ligula accumsan tincidunt ac vel erat. Nunc non nunc et dui feugiat finibus. Fusce efficitur, tortor a viverra consequat, sem nulla viverra quam, imperdiet malesuada dui justo vel lacus. Aenean malesuada gravida eros ut dictum. In vehicula vestibulum lacus vel auctor. Proin rutrum ut felis sit amet sagittis.\n\nVivamus quis sapien vel sapien rutrum posuere eu nec nulla. Fusce fermentum nulla et vehicula pharetra. Duis tempor libero cursus, accumsan tellus id, accumsan nulla. Nulla facilisi. In aliquet hendrerit pulvinar. Mauris nulla nibh, vulputate vitae malesuada at, pulvinar id magna. Aliquam eget elit maximus, pretium nunc sit amet, molestie felis. Mauris in dolor imperdiet metus lobortis vestibulum non ac risus. Nunc pretium mattis sapien, a scelerisque libero pharetra nec. Pellentesque nisi est, sollicitudin vitae feugiat ac, fringilla vitae lacus. Donec ullamcorper fringilla dolor, commodo vulputate metus accumsan mollis. Morbi maximus vehicula velit, in congue neque vestibulum sed. Nam volutpat, urna eu posuere consequat, leo metus mollis enim, ut scelerisque lectus leo eu urna. Aliquam porttitor sapien ut risus vehicula imperdiet. Sed nunc nisi, cursus quis porta ac, ultricies non erat. Sed tristique ante at accumsan malesuada.\n\nProin ante urna, lacinia at lacinia quis, condimentum in nunc. Proin ultricies velit nisl, at gravida lectus ultricies in. Aliquam laoreet, purus at commodo feugiat, felis nisl dignissim augue, non dapibus turpis nisi non lacus. In sit amet libero ut risus laoreet tristique eget sit amet erat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Integer varius dolor eu pharetra lobortis. Nam non risus eu lectus congue maximus.\n\nProin mauris justo, condimentum eu rutrum at, lacinia nec urna. Vivamus vitae tristique tortor, non ultrices ante. Aliquam quis tortor et eros dapibus posuere sed non enim. Fusce a dui fringilla, imperdiet neque vel, vestibulum erat. Praesent mi lectus, dignissim posuere vulputate a, tempus et quam. Pellentesque ornare congue neque sit amet rutrum. Ut convallis ac dolor id hendrerit. Duis placerat est sit amet orci egestas congue. Donec sed nunc id leo vestibulum blandit eu eu mauris. Nam eget tempus arcu, ac lobortis metus.\n\nSed dolor nibh, pulvinar sit amet dui at, dictum aliquam quam. Nulla condimentum iaculis arcu. Maecenas vel metus egestas, placerat magna in, mattis massa. Cras et hendrerit purus. Nullam id porta ligula, eget feugiat risus. Nam varius nunc eu elit sodales, congue molestie turpis bibendum. Aenean eu diam dapibus, luctus odio vitae, laoreet ipsum. In vehicula purus id suscipit tristique. Praesent ultrices risus risus, eget imperdiet est rutrum et. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam ac ante non turpis interdum sodales. Fusce a ligula eget enim cursus mollis. Maecenas et est magna. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent consectetur tempus viverra. Sed ultricies molestie blandit.\n\nPraesent eu dictum sem. Proin porta elit lacus, vel ornare arcu cursus eu. Integer a dolor ut arcu pellentesque fringilla. Nunc eget suscipit sem, sed pellentesque elit. Nam arcu nisi, condimentum at quam pretium, semper hendrerit massa. Fusce maximus turpis velit, vel molestie est volutpat rhoncus. Praesent finibus lacinia feugiat. Sed in nulla luctus, imperdiet urna et, sodales neque. Mauris commodo mattis sapien id pulvinar.\n\nNulla in nisi eget sem tempus placerat. Nunc at mi sit amet tellus pulvinar imperdiet. Sed consequat efficitur felis, at aliquam est imperdiet quis. In in scelerisque lectus. Suspendisse luctus pretium tortor tincidunt interdum. Nullam ornare arcu vel magna auctor aliquam. Sed ut rutrum nunc. Pellentesque dignissim mattis iaculis. Morbi facilisis interdum neque, eu pulvinar est venenatis at. Fusce lobortis varius justo, in finibus turpis tincidunt ac. Suspendisse et ornare enim, vel placerat mauris.\n\nAliquam laoreet nunc eget ligula convallis, eget aliquet ipsum pulvinar. Praesent tempor nulla non magna dictum lobortis. Praesent eleifend, velit eu semper tristique, tortor velit pharetra metus, nec dignissim mi nibh ut sem. Nulla in finibus ipsum, vitae ullamcorper lorem. Donec ac ligula lacinia, placerat massa ac, mollis odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus imperdiet, ante sed pretium vestibulum, orci dui vulputate lacus, vel sodales metus ipsum ut velit.\n\nInteger tempus lobortis metus, eu vulputate leo fermentum id. Praesent ut velit ultricies, maximus risus eu, lobortis ante. Nam interdum finibus fermentum. Maecenas in semper purus. Fusce ac enim ac ligula aliquet egestas. In hac habitasse platea dictumst. Morbi purus enim, pellentesque eu sagittis ac, pharetra in leo. Phasellus fermentum felis vitae nisi pulvinar semper. Sed consequat ligula tortor, et posuere orci laoreet vitae. Ut ultrices, urna et volutpat scelerisque, justo neque porta ipsum, ac mollis urna nunc vitae eros. Sed rhoncus nunc et purus finibus, ut pretium libero finibus. Nulla condimentum, nulla ac mattis commodo, est mi imperdiet odio, sed elementum orci velit quis est. Vestibulum quis justo nibh. Ut egestas tellus eu diam commodo vulputate. Vestibulum suscipit, tellus non auctor bibendum, metus nisi hendrerit elit, ut euismod magna ipsum sed tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;\n\nProin varius mi augue, sed auctor eros sagittis sed. Phasellus vehicula ex ut venenatis sagittis. Vivamus volutpat euismod lorem. Aliquam tempor quam orci. Cras ut nulla metus. Donec tempor, leo a tempor venenatis, enim ipsum aliquam tellus, id faucibus turpis magna quis ante. Ut sit amet volutpat nisl, at venenatis tellus. Praesent auctor ut risus at accumsan. Vestibulum venenatis viverra tristique. Duis a velit ornare, ultricies ipsum eu, vehicula felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec et purus consequat, fringilla augue ut, rhoncus nisi. Duis semper metus nunc, non egestas tellus semper et. Suspendisse congue tempus nunc, id vulputate neque consectetur in. Pellentesque rhoncus, justo at dignissim feugiat, ex ligula gravida lacus, ut varius augue libero eget ante.\n`;\nconsole.log(lorem.length);\nconst msg = fromString(\n  `${lorem}`\n);\n\n\nexport const roundTrip = new Suite({\n  async before() {\n    await libsodiumWrapper.ready;\n    await jsNaCl.setInstance();\n    console.log('\\nRound-trip Box Encryption (long message)');\n  }\n});\n\nroundTrip.add({\n  name: \"libsodium\",\n  fun: () => libsodium(msg)\n});\n\nroundTrip.add({\n  name: \"tweetnacl\",\n  fun: () => tweetnacl(msg)\n});\n\nroundTrip.add({\n  name: \"js-nacl\",\n  fun: () => jsnacl(msg)\n});\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/round-trip.ts",
    "content": "import { Suite } from \"asyncmark\";\nimport libsodiumWrapper from \"libsodium-wrappers\";\n\nimport * as jsNaCl from \"../lib/js-nacl\";\nimport {\n  libsodium,\n  tweetnacl,\n  jsnacl\n} from \"../lib/round-trip-implementations\";\n\nconst msg = Uint8Array.from([104, 101, 108, 101, 111]); // hello\n\nexport const roundTrip = new Suite({\n  async before() {\n    await libsodiumWrapper.ready;\n    await jsNaCl.setInstance();\n\n    console.log(\"\\nRound-trip Box Encryption (short message)\");\n  }\n});\n\nroundTrip.add({\n  name: \"libsodium\",\n  fun: () => libsodium(msg)\n});\n\nroundTrip.add({\n  name: \"tweetnacl\",\n  fun: () => tweetnacl(msg)\n});\n\nroundTrip.add({\n  name: \"js-nacl\",\n  fun: () => jsnacl(msg)\n});\n"
  },
  {
    "path": "benchmarks/crypto/src/bench/stringConvension.ts",
    "content": "import { Suite } from \"asyncmark\";\nimport libsodiumWrapper from \"libsodium-wrappers\";\n\nimport * as jsNaCl from \"../lib/js-nacl\";\n\nimport {\n  libsodium,\n  tweetnacl,\n  jsnacl,\n} from \"../lib/utils\";\n\nconst msg = Uint8Array.from([104, 101, 108, 101, 111]); // hello\n\nexport const stringConversion = new Suite({\n  async before() {\n    await libsodiumWrapper.ready;\n    await jsNaCl.setInstance();\n\n    console.log(\"\\nround trip uint8 <-> string encode + decode\");\n  }\n});\n\nstringConversion.add({\n  name: \"libsodium\",\n  fun: async () => {\n    libsodium.fromString(await libsodium.toString(msg));\n  }\n});\n\nstringConversion.add({\n  name: \"tweet-nacl\",\n  fun: async () => {\n    tweetnacl.fromString(await tweetnacl.toString(msg));\n  }\n});\n\nstringConversion.add({\n  name: \"js-nacl\",\n  fun: async () => {\n    jsnacl.fromString(await jsnacl.toString(msg));\n  }\n});\n"
  },
  {
    "path": "benchmarks/crypto/src/index.ts",
    "content": "import { roundTrip } from './bench/round-trip';\nimport { keyGeneration } from './bench/key-generation';\nimport { nonceGeneration } from './bench/nonce-generation';\nimport { roundTrip as roundTripLong } from './bench/round-trip-long';\nimport { base64 } from './bench/base64';\nimport { stringConversion } from './bench/stringConvension';\nimport { hex } from './bench/hex';\n\nasync function runBenchmark() {\n  // encryption-related things\n  await roundTrip.run();\n  await keyGeneration.run();\n  await nonceGeneration.run();\n  await roundTripLong.run();\n\n  // conversion\n  await base64.run();\n  await stringConversion.run();\n  await hex.run();\n}\n\nconsole.log(`\n  Box Encryption Libraries:\n\n        Name     | Size (min + gzip)\n    -------------|------------------\n    tweet-nacl   | 10.4  kB\n    libsodium    | 192.1 kB\n    js-nacl      | 212.9 kB\n`);\n\nrunBenchmark();\n"
  },
  {
    "path": "benchmarks/crypto/src/lib/js-nacl.ts",
    "content": "\nimport NaClFactory, { Nacl } from 'js-nacl';\n\nimport { concat } from '../utils';\n\n\nexport let nacl: Nacl;\nexport function setInstance(): Promise<Nacl> {\n  return new Promise((resolve, reject) => {\n    // These apis are.... not good\n    NaClFactory.instantiate((instance: Nacl) => {\n      nacl = instance;\n      resolve(nacl);\n    });\n  });\n}\n\nexport function generateAsymmetricKeys() {\n  return nacl.crypto_box_keypair();\n}\n\nexport function generateNonce(): Uint8Array {\n  return nacl.crypto_box_random_nonce();\n}\n\nexport function encryptFor(\n  message: Uint8Array,\n  recipientPublicKey: Uint8Array,\n  senderPrivateKey: Uint8Array\n): Uint8Array {\n  const nonce = generateNonce();\n\n  const ciphertext = nacl.crypto_box(message, nonce, recipientPublicKey, senderPrivateKey);\n\n  return concat(nonce, ciphertext);\n}\n\nexport function decryptFrom(\n  ciphertextWithNonce: Uint8Array,\n  senderPublicKey: Uint8Array,\n  recipientPrivateKey: Uint8Array\n): Uint8Array {\n  const [nonce, ciphertext] = splitNonceFromMessage(ciphertextWithNonce);\n  const decrypted = nacl.crypto_box_open(ciphertext, nonce, senderPublicKey, recipientPrivateKey);\n\n  return decrypted as Uint8Array;\n}\n\n\nexport function splitNonceFromMessage(\n  messageWithNonce: Uint8Array\n): [Uint8Array, Uint8Array] {\n  const bytes = nacl.crypto_box_NONCEBYTES;\n\n  const nonce = messageWithNonce.slice(0, bytes);\n  const message = messageWithNonce.slice(bytes, messageWithNonce.length);\n\n  return [nonce, message];\n}\n\n\n\nexport function toHex(array: Uint8Array): string {\n  return nacl.to_hex(array);\n}\n\nexport function fromHex(hex: string): Uint8Array {\n  return nacl.from_hex(hex);\n}\n\n// export async function toBase64(array: Uint8Array): Promise<string> {\n//   return utils.encodeBase64(array);\n// }\n\n// export async function fromBase64(base64: string): Promise<Uint8Array> {\n//   return utils.decodeBase64(base64);\n// }\n\nexport function fromString(str: string): Uint8Array {\n  return nacl.encode_utf8(str);\n}\n\nexport const toUint8Array = fromString;\n\nexport function toString(uint8Array: Uint8Array): string {\n  return nacl.decode_utf8(uint8Array);\n}\n\n"
  },
  {
    "path": "benchmarks/crypto/src/lib/libsodium.ts",
    "content": "import libsodiumWrapper, { KeyPair } from 'libsodium-wrappers';\n\nimport { concat } from '../utils';\n\nexport async function libsodium(): Promise<typeof libsodiumWrapper> {\n  const sodium = (libsodiumWrapper as any);\n  await sodium.ready;\n\n  return sodium as typeof libsodiumWrapper;\n}\n\nexport async function genericHash(arr: Uint8Array): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return sodium.crypto_generichash(32, arr);\n}\n\nexport async function derivePublicKey(privateKey: Uint8Array): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return sodium.crypto_scalarmult_base(privateKey);\n}\n\nexport async function randomBytes(length: number): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return sodium.randombytes_buf(length);\n}\n\nexport async function generateNonce(): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return await randomBytes(sodium.crypto_box_NONCEBYTES);\n}\n\nexport async function generateAsymmetricKeys(): Promise<KeyPair> {\n  const sodium = await libsodium();\n\n  return sodium.crypto_box_keypair();\n}\n\nexport async function generateSymmetricKey(): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return await randomBytes(sodium.crypto_box_SECRETKEYBYTES);\n}\n\nexport async function encryptFor(\n  message: Uint8Array,\n  recipientPublicKey: Uint8Array,\n  senderPrivateKey: Uint8Array\n): Promise<Uint8Array> {\n  const sodium = await libsodium();\n  const nonce = await generateNonce();\n\n  const ciphertext = sodium.crypto_box_easy(message, nonce, recipientPublicKey, senderPrivateKey);\n\n  return concat(nonce, ciphertext);\n}\n\nexport async function decryptFrom(\n  ciphertextWithNonce: Uint8Array,\n  senderPublicKey: Uint8Array,\n  recipientPrivateKey: Uint8Array\n): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  const [nonce, ciphertext] = await splitNonceFromMessage(ciphertextWithNonce);\n  const decrypted = sodium.crypto_box_open_easy(\n    ciphertext,\n    nonce,\n    senderPublicKey,\n    recipientPrivateKey\n  );\n\n  return decrypted;\n}\n\nexport async function splitNonceFromMessage(\n  messageWithNonce: Uint8Array\n): Promise<[Uint8Array, Uint8Array]> {\n  const sodium = await libsodium();\n  const bytes = sodium.crypto_box_NONCEBYTES;\n\n  const nonce = messageWithNonce.slice(0, bytes);\n  const message = messageWithNonce.slice(bytes, messageWithNonce.length);\n\n  return [nonce, message];\n}\n\nexport function toHex(array: Uint8Array): string {\n  return libsodiumWrapper.to_hex(array);\n}\n\nexport function fromHex(hex: string): Uint8Array {\n  return libsodiumWrapper.from_hex(hex);\n}\n\nexport async function toBase64(array: Uint8Array): Promise<string> {\n  const sodium = await libsodium();\n\n  return sodium.to_base64(array, sodium.base64_variants.ORIGINAL);\n}\n\nexport async function fromBase64(base64: string): Promise<Uint8Array> {\n  const sodium = await libsodium();\n\n  return sodium.from_base64(base64, sodium.base64_variants.ORIGINAL);\n}\n\nexport function fromString(str: string): Uint8Array {\n  return libsodiumWrapper.from_string(str);\n}\n\nexport const toUint8Array = fromString;\n\nexport function toString(uint8Array: Uint8Array): string {\n  return libsodiumWrapper.to_string(uint8Array);\n}\n\n"
  },
  {
    "path": "benchmarks/crypto/src/lib/round-trip-implementations.ts",
    "content": "import * as libsodiumjs from \"../lib/libsodium\";\nimport * as tweetNaCl from \"../lib/tweet-nacl\";\nimport * as jsNaCl from \"../lib/js-nacl\";\n\nexport async function libsodium(msg: Uint8Array) {\n  const receiver = await libsodiumjs.generateAsymmetricKeys();\n  const sender = await libsodiumjs.generateAsymmetricKeys();\n\n  const cipherText = await libsodiumjs.encryptFor(\n    msg,\n    receiver.publicKey,\n    sender.privateKey\n  );\n  const decrypted = await libsodiumjs.decryptFrom(\n    cipherText,\n    sender.publicKey,\n    receiver.privateKey\n  );\n\n  ensureEquality(\"libsodium\", msg, decrypted, libsodiumjs.toString);\n}\n\nexport async function tweetnacl(msg: Uint8Array) {\n  const receiver = tweetNaCl.generateAsymmetricKeys();\n  const sender = tweetNaCl.generateAsymmetricKeys();\n\n  const cipherText = await tweetNaCl.encryptFor(\n    msg,\n    receiver.publicKey,\n    sender.secretKey\n  );\n  const decrypted = await tweetNaCl.decryptFrom(\n    cipherText,\n    sender.publicKey,\n    receiver.secretKey\n  );\n\n  ensureEquality(\"tweetnacl\", msg, decrypted, tweetNaCl.toString);\n}\n\nexport async function jsnacl(msg: Uint8Array) {\n  const receiver = jsNaCl.generateAsymmetricKeys();\n  const sender = jsNaCl.generateAsymmetricKeys();\n\n  const cipherText = jsNaCl.encryptFor(msg, receiver.boxPk, sender.boxSk);\n  const decrypted = await jsNaCl.decryptFrom(\n    cipherText,\n    sender.boxPk,\n    receiver.boxSk\n  );\n\n  ensureEquality(\"jsnacl\", msg, decrypted, jsNaCl.nacl.decode_utf8);\n}\n\nfunction ensureEquality(\n  label: string,\n  a: Uint8Array,\n  b: Uint8Array,\n  toString: any\n) {\n  const as = toString(a);\n  const bs = toString(b);\n  if (as !== bs) {\n    throw new Error(`\n      message was not encrypted and/or decrypted properly.\n\n      Expected ${as}\n      to equal ${bs}\n\n    `);\n  }\n}\n"
  },
  {
    "path": "benchmarks/crypto/src/lib/tweet-nacl.ts",
    "content": "import * as nacl from 'tweetnacl';\nimport * as utils from 'tweetnacl-util';\n\nimport { concat } from '../utils';\n\nexport function generateAsymmetricKeys() {\n  return nacl.box.keyPair();\n}\n\nexport function generateNonce(): Uint8Array {\n  return nacl.randomBytes(nacl.box.nonceLength);\n}\n\nexport function encryptFor(\n  message: Uint8Array,\n  recipientPublicKey: Uint8Array,\n  senderPrivateKey: Uint8Array\n): Uint8Array {\n\n  const nonce = generateNonce();\n\n  const ciphertext = nacl.box(message, nonce, recipientPublicKey, senderPrivateKey);\n\n  return concat(nonce, ciphertext);\n}\n\nexport function decryptFrom(\n  ciphertextWithNonce: Uint8Array,\n  senderPublicKey: Uint8Array,\n  recipientPrivateKey: Uint8Array\n): Uint8Array {\n\n  const [nonce, ciphertext] = splitNonceFromMessage(ciphertextWithNonce);\n  const decrypted = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientPrivateKey);\n\n  return decrypted as Uint8Array;\n}\n\n\nexport function splitNonceFromMessage(\n  messageWithNonce: Uint8Array\n): [Uint8Array, Uint8Array] {\n  const bytes = nacl.box.nonceLength;\n\n  const nonce = messageWithNonce.slice(0, bytes);\n  const message = messageWithNonce.slice(bytes, messageWithNonce.length);\n\n  return [nonce, message];\n}\n\n// export function toHex(array: Uint8Array): string {\n//   return libsodiumWrapper.to_hex(array);\n// }\n\n// export function fromHex(hex: string): Uint8Array {\n//   return libsodiumWrapper.from_hex(hex);\n// }\n\nexport async function toBase64(array: Uint8Array): Promise<string> {\n  return utils.encodeBase64(array);\n}\n\nexport async function fromBase64(base64: string): Promise<Uint8Array> {\n  return utils.decodeBase64(base64);\n}\n\nexport function fromString(str: string): Uint8Array {\n  return utils.decodeUTF8(str);\n}\n\nexport const toUint8Array = fromString;\n\nexport function toString(uint8Array: Uint8Array): string {\n  return utils.encodeUTF8(uint8Array);\n}\n\n"
  },
  {
    "path": "benchmarks/crypto/src/lib/utils.ts",
    "content": "import * as libsodumFns from './libsodium';\nimport * as tweetnaclFns from './tweet-nacl';\nimport * as jsNaclFns from './js-nacl';\n\nexport const libsodium = {\n    toHex: libsodumFns.toHex,\n    fromHex: libsodumFns.fromHex,\n    toBase64: libsodumFns.toBase64,\n    fromBase64: libsodumFns.fromBase64,\n    fromString: libsodumFns.fromString,\n    toString: libsodumFns.toString,\n};\n\nexport const tweetnacl = {\n    toBase64: tweetnaclFns.toBase64,\n    fromBase64: tweetnaclFns.fromBase64,\n    fromString: tweetnaclFns.fromString,\n    toString: tweetnaclFns.toString,\n}\n\nexport const jsnacl = {\n    toHex: jsNaclFns.toHex,\n    fromHex: jsNaclFns.fromHex,\n    fromString: jsNaclFns.fromString,\n    toString: jsNaclFns.toString,\n}"
  },
  {
    "path": "benchmarks/crypto/src/utils.ts",
    "content": "import libsodiumWrapper from 'libsodium-wrappers';\n\nexport async function wrapCatch(task) {\n  try {\n    await task();\n  } catch(e) {\n    console.error(e);\n    throw e;\n  }\n}\n\nexport function fromString(str: string): Uint8Array {\n  // return new TextEncoder().encode(str);\n  return libsodiumWrapper.from_string(str);\n}\n\nexport function toString(data: Uint8Array): string {\n  return libsodiumWrapper.to_string(data);\n}\n\nexport function concat(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {\n  const result = new Uint8Array(arr1.length + arr2.length);\n\n  result.set(arr1, 0);\n  result.set(arr2, arr1.length);\n\n  return result;\n}\n"
  },
  {
    "path": "benchmarks/crypto/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"allowJs\": true,\n    \"experimentalDecorators\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"noEmitOnError\": false,\n    \"noEmit\": true,\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"noImplicitReturns\": false,\n    \"alwaysStrict\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"baseUrl\": \".\",\n    \"module\": \"es6\",\n    \"paths\": {\n      \"lib/*\": [\n        \"src/lib/*\"\n      ],\n      \"bench/*\": [\n        \"src/bench/*\"\n      ],\n      \"*\": [\n        \"src*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "benchmarks/emoji-replace/.babelrc.js",
    "content": "module.exports = {\n  presets: [\n    ['@babel/env', {\n      targets: {\n        node: '10'\n      }\n    }],\n    '@babel/preset-typescript',\n  ],\n  plugins: [\n    '@babel/plugin-transform-modules-commonjs',\n    '@babel/plugin-syntax-dynamic-import',\n    '@babel/plugin-proposal-class-properties',\n    ['@babel/plugin-proposal-decorators', { legacy: true }],\n    '@babel/plugin-transform-runtime',\n  ]\n};\n"
  },
  {
    "path": "benchmarks/emoji-replace/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "benchmarks/emoji-replace/package.json",
    "content": "{\n  \"name\": \"emoji-replace-benchmarks\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"babel ./src --out-dir ./build --extensions '.ts'\",\n    \"start\": \"node build/index.js\",\n    \"bench\": \"yarn build && yarn start\",\n    \"debug\": \"node --inspect-brk=9229 build/index.js\"\n  },\n  \"dependencies\": {\n    \"emojis\": \"^1.0.10\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"7.6.2\",\n    \"@babel/core\": \"7.6.2\",\n    \"@babel/node\": \"7.6.2\",\n    \"@babel/plugin-proposal-class-properties\": \"7.5.5\",\n    \"@babel/plugin-proposal-decorators\": \"7.6.0\",\n    \"@babel/plugin-syntax-dynamic-import\": \"7.2.0\",\n    \"@babel/plugin-transform-modules-commonjs\": \"7.6.0\",\n    \"@babel/plugin-transform-runtime\": \"7.6.2\",\n    \"@babel/preset-env\": \"7.6.2\",\n    \"@babel/preset-typescript\": \"7.6.0\",\n    \"@babel/register\": \"7.6.2\",\n    \"@babel/runtime\": \"7.6.2\",\n    \"@types/js-nacl\": \"1.2.0\",\n    \"@types/libsodium-wrappers\": \"0.7.5\",\n    \"@types/node\": \"12.7.11\",\n    \"asyncmark\": \"0.3.1\",\n    \"typescript\": \"3.6.3\"\n  }\n}\n"
  },
  {
    "path": "benchmarks/emoji-replace/run",
    "content": "#!/bin/bash\n\nyarn\nyarn build\nyarn start\n"
  },
  {
    "path": "benchmarks/emoji-replace/src/bench/-utils.ts",
    "content": "import { Suite } from \"asyncmark\";\nimport { unicode } from \"emojis\";\n\nexport function assertEq<T>(expected: T, actual: T, msg: string) {\n  if (expected !== actual) {\n    throw new Error(`\n      ${ msg }\n\n      expected:\n\n        ${actual}\n\n        to equal\n\n        ${expected}\n    `);\n  }\n}\n\ninterface Args {\n  originalString: string;\n  expected: string;\n  benchName: string;\n}\n\nexport function generateEmojisBench({\n  originalString,\n  expected,\n  benchName\n}: Args) {\n\n  function directReplace(str: string) {\n    return unicode(str);\n  }\n\n  function condition(str: string) {\n    if (str.includes(\":\")) {\n      return unicode(str);\n    }\n\n    return str;\n  }\n\n  const EMOJI_REGEX = /:[^:]+:/;\n\n  function regexTest(str: string) {\n    if (EMOJI_REGEX.test(str)) {\n      return unicode(str);\n    }\n\n    return str;\n  }\n\n  function regexMatch(str: string) {\n    if (str.match(EMOJI_REGEX)) {\n      return unicode(str);\n    }\n\n    return str;\n  }\n\n  const bench = new Suite({\n    async before() {\n      assertEq(\n        expected, directReplace(originalString),\n        `${benchName}: direct replace failed`\n      );\n      assertEq(expected, condition(originalString), `${benchName}: condition failed`);\n      assertEq(expected, regexTest(originalString), `${benchName}: condition failed`);\n\n      console.log(`\\n -- ${benchName} -- `);\n    }\n  });\n\n  bench.add({\n    name: \"direct replace  \",\n    fun: () => {\n      directReplace(directReplace(originalString))\n    }\n  });\n\n  bench.add({\n    name: \"condition for   \",\n    fun: () => {\n      condition(condition(originalString))\n    }\n  });\n\n  bench.add({\n    name: \"regex test      \",\n    fun: () => {\n      regexTest(regexTest(originalString))\n    }\n  });\n\n  bench.add({\n    name: \"regex match     \",\n    fun: () => {\n      regexMatch(regexMatch(originalString))\n    }\n  });\n\n  return bench;\n}\n\n"
  },
  {
    "path": "benchmarks/emoji-replace/src/bench/long.ts",
    "content": "import { generateEmojisBench } from \"./-utils\";\n\nconst benchName = \"long\";\nconst originalString = `\n  I :heart: the :scream: emoji.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam,\nquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n:smile:\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident,\nsunt in culpa qui officia deserunt mollit anim id est laborum.\n\nI :heart: the :scream: emoji.\n`;\n\nconst expected = `\n  I ❤️ the 😱 emoji.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam,\nquis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n😄\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident,\nsunt in culpa qui officia deserunt mollit anim id est laborum.\n\nI ❤️ the 😱 emoji.\n`;\n\nexport const bench = generateEmojisBench({\n  originalString,\n  expected,\n  benchName\n});\n"
  },
  {
    "path": "benchmarks/emoji-replace/src/bench/micro.ts",
    "content": "import { generateEmojisBench } from \"./-utils\";\n\nconst originalString = \":scream:\";\nconst expected = \"😱\";\nconst benchName = \"micro\";\n\nexport const bench = generateEmojisBench({\n  originalString,\n  expected,\n  benchName\n});\n"
  },
  {
    "path": "benchmarks/emoji-replace/src/bench/short.ts",
    "content": "import { generateEmojisBench } from \"./-utils\";\n\nconst originalString = \"I :heart: the :scream: emoji.\";\nconst expected = \"I ❤️ the 😱 emoji.\";\nconst benchName = \"short\";\n\nexport const bench = generateEmojisBench({\n  originalString,\n  expected,\n  benchName\n});\n"
  },
  {
    "path": "benchmarks/emoji-replace/src/index.ts",
    "content": "import { bench as short } from \"./bench/short\";\nimport { bench as micro } from \"./bench/micro\";\nimport { bench as long } from \"./bench/long\";\n\nasync function runBenchmark() {\n  await micro.run();\n  await short.run();\n  await long.run();\n}\n\nconsole.log(`\n  Emoji Libraries:\n\n        Name     | Size (min + gzip)\n    -------------|------------------\n    emojis       | 376 Bytes\n`);\n\nrunBenchmark();\n"
  },
  {
    "path": "benchmarks/emoji-replace/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"allowJs\": true,\n    \"experimentalDecorators\": true,\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"noEmitOnError\": false,\n    \"noEmit\": true,\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"noImplicitReturns\": false,\n    \"alwaysStrict\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"baseUrl\": \".\",\n    \"module\": \"es6\",\n    \"paths\": {\n      \"lib/*\": [\n        \"src/lib/*\"\n      ],\n      \"bench/*\": [\n        \"src/bench/*\"\n      ],\n      \"*\": [\n        \"src*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "benchmarks/emoji-replace/types/emojis.d.ts",
    "content": "export function unicode(input: string): string;\nexport function html(input: string): string;\n"
  },
  {
    "path": "client/android-wrapper/.gitignore",
    "content": "# Built application files\n*.apk\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle/\nbuild/\nrelease/\nappmaker.keystore\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n.idea/caches\n# Android Studio 3 in .gitignore file.\n.idea/caches/build_file_checksums.ser\n.idea/modules.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n"
  },
  {
    "path": "client/android-wrapper/CONTRIBUTING.md",
    "content": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your sample apps and patches! Before we can take them, we\nhave to jump a couple of legal hurdles.\n\nPlease fill out either the individual or corporate Contributor License Agreement\n(CLA).\n\n  * If you are an individual writing original source code and you're sure you\n    own the intellectual property, then you'll need to sign an [individual CLA]\n    (https://developers.google.com/open-source/cla/individual).\n  * If you work for a company that wants to allow you to contribute your work,\n    then you'll need to sign a [corporate CLA]\n    (https://developers.google.com/open-source/cla/corporate).\n\nFollow either of the two links above to access the appropriate CLA and\ninstructions for how to sign and return it. Once we receive it, we'll be able to\naccept your pull requests.\n\n## Contributing A Patch\n\n1. Submit an issue describing your proposed change to the repo in question.\n1. The repo owner will respond to your issue promptly.\n1. If your proposed change is accepted, and you haven't already done so, sign a\n   Contributor License Agreement (see details above).\n1. Fork the desired repo, develop and test your code changes.\n1. Ensure that your code adheres to the existing style in the sample to which\n   you are contributing. Refer to the\n   [Google Cloud Platform Samples Style Guide]\n   (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the\n   recommended coding standards for this organization.\n1. Ensure that your code has an appropriate set of unit tests which all pass.\n1. Submit a pull request.\n\n"
  },
  {
    "path": "client/android-wrapper/LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability."
  },
  {
    "path": "client/android-wrapper/README.md",
    "content": "# SVGOMG / Trusted Web Activity\n\nThis project uses the\n[Trusted Web Activities](https://developers.google.com/web/updates/2017/10/using-twa) technology\nto wrap [SVGOMG](https://jakearchibald.github.io/svgomg/) in an Android Application.\n\n## Running the Demo\n\n1. Clone the project\n``\ngit clone https://github.com/GoogleChromeLabs/svgomg-twa.git\n``\n\n2. Import the Project into Android Studio, using File > New > Import Project, and select the folder\nto which the project was cloned.\n\n3. Run the Project (Ctrl+R)\n\n### Enabling Debug\n\nTWAs require [Digital AssetLinks](https://developers.google.com/digital-asset-links/) to be setup\non both the application and on the website, in order to enable the validation that allows Chrome to\nopen the page in full-screen.\n\nFor security reasons, the signing key compatible with the setup on\nhttps://svgomg.firebaseapp.com/ is not committed with the sample code.\n\nIt is possible to setup Chrome to skip validation on device to enable testing.\n\nHere are the 2 steps required to achieve this:\n\n1. Enable Chrome to accept command-line parameters:\n\nOn the Android Device, go to the Chrome version being used to test the TWA and navigate to\n`chrome://flags`. Search for a setting called `Enable commmand line on non-rooted devices` and\nchange it to `Enabled`. Restarting the browser *multiple* times may be required.\n\n2. Create an Android file with the command-line parameters that allow skipping the TWA validation.\n\nAdd a file at `/data/local/tmp/chrome-command-line`, with the content\n`_ --disable-digital-asset-link-verification-for-url=\"https://svgomg.firebaseapp.com\"`. Make sure\nthere's not newline at the end of the line, or it may break the launcher.\n\nFor convenience, a shell script that creates this file is available in this repository. Run it\nby executing `./enable-debug.sh https://svgomg.firebaseapp.com`.\n\nTo debug a different PWA, execute the script with a different host:\n`./enable-debug.sh https://example.com`\n\n## License\n\n```\nCopyright 2015 Google, Inc.\n\nLicensed to the Apache Software Foundation (ASF) under one or more contributor\nlicense agreements. See the NOTICE file distributed with this work for\nadditional information regarding copyright ownership. The ASF licenses this\nfile to you under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n```\n"
  },
  {
    "path": "client/android-wrapper/app/build.gradle",
    "content": "/*\n * Copyright 2019 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * appicon_ios_android.png\n */\n\napply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n    defaultConfig {\n        applicationId \"io.emberclear\"\n        minSdkVersion 16\n        targetSdkVersion 28\n        versionCode 343\n        versionName \"1\"\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n        manifestPlaceholders = [\n                // The hostname is used when building the intent-filter, so the TWA is able to\n                // handle Intents to open https://svgomg.firebaseapp.com.\n                hostName: \"io.emberclear\",\n                defaultUrl: \"https://emberclear.io\",\n                launcherName: \"emberclear\",\n                // This variable below expresses the relationship between the app and the site,\n                // as documented in the TWA documentation at\n                // https://developers.google.com/web/updates/2017/10/using-twa#set_up_digital_asset_links_in_an_android_app\n                // and is injected into the AndroidManifest.xml\n                assetStatements: '[{ \"relation\": [\"delegate_permission/common.handle_all_urls\"], ' +\n                        '\"target\": {\"namespace\": \"web\", \"site\": \"https://emberclear.io\"}}]'\n        ]\n    }\n    signingConfigs {\n        release {\n            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {\n                storeFile file(MYAPP_RELEASE_STORE_FILE)\n                storePassword MYAPP_RELEASE_STORE_PASSWORD\n                keyAlias MYAPP_RELEASE_KEY_ALIAS\n                keyPassword MYAPP_RELEASE_KEY_PASSWORD\n            }\n        }\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            signingConfig signingConfigs.release\n        }\n    }\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation 'com.github.GoogleChrome:custom-tabs-client:e446d08014'\n}\n"
  },
  {
    "path": "client/android-wrapper/app/src/main/AndroidManifest.xml",
    "content": "<!--\n    Copyright 2019 Google Inc. All Rights Reserved.\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n         http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n\n<!-- The \"package\" attribute is rewritten by the Gradle build with the value of applicationId.\n     It is still required here, as it is used to derive paths, for instance when referring\n     to an Activity by \".MyActivity\" instead of the full name. If more Activities are added to the\n     application, the package attribute will need to reflect the correct path in order to use\n     the abbreviated format. -->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.placeholder\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"${launcherName}\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.TwaSplash\">\n\n        <meta-data\n            android:name=\"asset_statements\"\n            android:value=\"${assetStatements}\" />\n\n        <activity android:name=\"android.support.customtabs.trusted.LauncherActivity\"\n            android:label=\"${launcherName}\">\n            <meta-data android:name=\"android.support.customtabs.trusted.DEFAULT_URL\"\n                android:value=\"${defaultUrl}\" />\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\"/>\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\"/>\n                <data android:scheme=\"https\"\n                    android:host=\"${hostName}\"/>\n            </intent-filter>\n        </activity>\n    </application>\n</manifest>\n"
  },
  {
    "path": "client/android-wrapper/app/src/main/res/values/styles.xml",
    "content": "<!--\n    Copyright 2019 Google Inc. All Rights Reserved.\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n         http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <!-- Theme to create a blank screen while the TWA is opening -->\n    <style name=\"Theme.TwaSplash\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:backgroundDimEnabled\">false</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "client/android-wrapper/build.gradle",
    "content": "/*\n * Copyright 2015 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    \n    repositories {\n        google()\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.3.2'\n        \n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n        // Jitpack is currently being used to publish beta versions of the TWA Support Library.\n        // This will change in the future to use the same approach as other Android Support\n        // Libraries.\n        maven { url \"https://jitpack.io\" }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "client/android-wrapper/enable-debug.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Copyright 2019 Google Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Check if at least 1 argument is provided\nif [[ $# -eq 0 ]]\n    then\n        echo \"Usage: 'enable-debug.sh <host>'\"\n        exit 1\n    fi\n\n# Invokes ADB and creates the file with the command line\nadb shell \"echo '_ --disable-digital-asset-link-verification-for-url=\\\"$1\\\"' > /data/local/tmp/chrome-command-line\"\n"
  },
  {
    "path": "client/android-wrapper/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Feb 18 11:18:13 EST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.4-all.zip\n"
  },
  {
    "path": "client/android-wrapper/gradle/wrapper/gradle.properties",
    "content": ""
  },
  {
    "path": "client/android-wrapper/gradle.properties",
    "content": "MYAPP_RELEASE_STORE_FILE=appmaker.keystore\nMYAPP_RELEASE_KEY_ALIAS=appmaker-store-Uy9EZlEN6ZDfet5g0KRX\nMYAPP_RELEASE_STORE_PASSWORD=oz963614f\nMYAPP_RELEASE_KEY_PASSWORD=n7kqya6cy"
  },
  {
    "path": "client/android-wrapper/gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "client/android-wrapper/gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "client/android-wrapper/settings.gradle",
    "content": "include ':app'\n"
  },
  {
    "path": "client/web/.eslintignore",
    "content": "**/declarations/\n**/concat-stats-for/\n**/dist/\n**/tmp/\n/node_modules/\n**/node_modules/\n**/public/\n**/vendor/\n"
  },
  {
    "path": "client/web/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.node();\n"
  },
  {
    "path": "client/web/.gitignore",
    "content": ".eslintcache\n.stylelintcache\ntsconfig.tsbuildinfo\n"
  },
  {
    "path": "client/web/.prettierignore",
    "content": "**/blueprints/*/files/**/*.js\n**/bip39/wordlists/english.ts\n"
  },
  {
    "path": "client/web/.prettierrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  singleQuote: true,\n  trailingComma: 'es5',\n  printWidth: 100,\n  semi: true,\n  bracketSpacing: true,\n  endOfLine: 'lf',\n  tabs: false,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "client/web/.stylelintignore",
    "content": "# Projects without CSS\nsmoke-tests/\n\n# Ignore supporting directories to improve scan time\n**/concat-stats-for/**\n**/dummy/**\n**/dist/**\n**/node_modules/**\nnode_modules/**\n**/tmp/**\n**/public/**\n**/vendor/**\n\n"
  },
  {
    "path": "client/web/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/.vim/coc-settings.json",
    "content": "{\n  \"cSpell.words\": [\n    \"Encryptable\",\n    \"Parallelizable\",\n    \"REFAN\",\n    \"REFLAT\",\n    \"Serializable\",\n    \"Synthwave\",\n    \"ciphertext\",\n    \"cond\",\n    \"emberclear\",\n    \"esbuild\",\n    \"keypair\",\n    \"klass\",\n    \"nacl\",\n    \"outfile\",\n    \"prismjs\",\n    \"sourcemap\",\n    \"tmpl\"\n  ]\n}"
  },
  {
    "path": "client/web/.vscode/settings.json",
    "content": "{\n    \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}"
  },
  {
    "path": "client/web/addons/crypto/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/crypto/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/crypto/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/crypto/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/crypto/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/crypto/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/crypto/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/crypto/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd crypto`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/crypto/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/crypto/README.md",
    "content": "crypto\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install crypto\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/crypto/addon/-private/types.ts",
    "content": "import type { handleMessage } from '../workers/crypto/messages';\n\nexport interface WorkerLike {\n  postMessage: typeof handleMessage;\n  _worker?: Worker;\n}\n\nexport type WorkerRegistry = { [path: string]: WorkerLike };\n"
  },
  {
    "path": "client/web/addons/crypto/addon/connector.ts",
    "content": "import type { WorkersService } from '@emberclear/crypto';\nimport type { WorkerLike } from '@emberclear/crypto/-private/types';\nimport type { EncryptedMessage, KeyPair, KeyPublic, Serializable } from '@emberclear/crypto/types';\n\ntype Args = {\n  workerService: WorkersService;\n  keys?: KeyPair;\n};\n\nconst Action = {\n  // Generation\n  LOGIN: 0,\n  GENERATE_KEYS: 1,\n  DECRYPT_FROM_SOCKET: 2,\n  ENCRYPT_FOR_SOCKET: 3,\n  GENERATE_SIGNING_KEYS: 4,\n  SIGN: 5,\n  OPEN_SIGNED: 6,\n  HASH: 7,\n\n  // Conversions\n  MNEMONIC_FROM_PRIVATE_KEY: 50,\n\n  // TODO: should find a way to not need these\n  DERIVE_PUBLIC_KEY: 100,\n  DERIVE_PUBLIC_SIGNING_KEY: 101,\n} as const;\n\nexport default class CryptoConnector {\n  getWorker: () => WorkerLike;\n  keys: KeyPair;\n\n  constructor({ workerService, keys }: Args) {\n    let { privateKey, publicKey } = keys || ({} as KeyPair);\n\n    this.getWorker = workerService.getCryptoWorker;\n    this.keys = { privateKey, publicKey };\n  }\n\n  async login(mnemonic: string) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.LOGIN,\n      args: [mnemonic],\n    });\n  }\n\n  async mnemonicFromNaClBoxPrivateKey(key?: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.MNEMONIC_FROM_PRIVATE_KEY,\n      args: [key || this.keys.publicKey],\n    });\n  }\n\n  async generateKeys() {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.GENERATE_KEYS,\n      args: [],\n    });\n  }\n\n  async generateSigningKeys() {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.GENERATE_SIGNING_KEYS,\n      args: [],\n    });\n  }\n\n  async derivePublicKey(privateKey: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.DERIVE_PUBLIC_KEY,\n      args: [privateKey],\n    });\n  }\n\n  async derivePublicSigningKey(privateSigningKey: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.DERIVE_PUBLIC_SIGNING_KEY,\n      args: [privateSigningKey],\n    });\n  }\n\n  async encryptForSocket(payload: Serializable, { publicKey }: KeyPublic) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.ENCRYPT_FOR_SOCKET,\n      args: [payload, { publicKey }, { privateKey: this.keys.privateKey }],\n    });\n  }\n\n  async decryptFromSocket<ExpectedReturn = unknown>(socketData: EncryptedMessage) {\n    let worker = this.getWorker();\n\n    return (await worker.postMessage({\n      action: Action.DECRYPT_FROM_SOCKET,\n      args: [socketData, this.keys.privateKey],\n    })) as Promise<ExpectedReturn>;\n  }\n\n  async sign(message: Uint8Array, senderPrivateKey: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.SIGN,\n      args: [message, senderPrivateKey],\n    });\n  }\n\n  async openSigned(signedMessage: Uint8Array, senderPublicKey: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.OPEN_SIGNED,\n      args: [signedMessage, senderPublicKey],\n    });\n  }\n\n  async hash(message: Uint8Array) {\n    let worker = this.getWorker();\n\n    return await worker.postMessage({\n      action: Action.HASH,\n      args: [message],\n    });\n  }\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/index.ts",
    "content": "export { default as CryptoConnector } from './connector';\nexport { default as WorkersService } from './services/workers';\nexport type {\n  KeyPair,\n  KeyPrivate,\n  KeyPublic,\n  SigningKeyPair,\n  SigningKeyPrivate,\n  SigningKeyPublic,\n} from './types';\n"
  },
  {
    "path": "client/web/addons/crypto/addon/services/workers.ts",
    "content": "import { action } from '@ember/object';\nimport Service from '@ember/service';\n\nimport { PWBHost } from 'promise-worker-bi';\n\nimport type { WorkerLike, WorkerRegistry } from '@emberclear/crypto/-private/types';\n\nexport const CRYPTO_PATH = '/workers/crypto';\nexport const NETWORKING_PATH = '/workers/networking';\n\nexport default class WorkersService extends Service {\n  registry: WorkerRegistry = {};\n\n  @action\n  getCryptoWorker() {\n    return this.getWorker(CRYPTO_PATH);\n  }\n\n  @action\n  getNetworkingWorker() {\n    return this.getWorker(NETWORKING_PATH);\n  }\n\n  protected getWorker(path: string): WorkerLike {\n    if (this.registry[path]) return this.registry[path];\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    let worker = new Worker(`${path}${(window as any).ASSET_FINGERPRINT_HASH || ''}.js`);\n    let promiseWorker = new PWBHost(worker);\n    // promiseWorker._hostIDQueue = undefined;\n\n    if (!promiseWorker) {\n      throw new Error('failed to create promiseWorker?');\n    }\n\n    promiseWorker.register(function (message: string) {\n      console.info(`Received message in ${path}: `, message);\n    });\n\n    promiseWorker.registerError(function (err: Error) {\n      console.error(`Error in ${path}: `, err);\n    });\n\n    this.registry[path] = promiseWorker as WorkerLike;\n\n    return this.registry[path];\n  }\n\n  willDestroy() {\n    Object.values(this.registry).forEach((promiseWorker) => {\n      promiseWorker._worker?.terminate();\n    });\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    workers: WorkersService;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/crypto\": [\".\"],\n      \"@emberclear/crypto/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../../libraries/questionably-typed\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/types.ts",
    "content": "export type KeyPublic = { publicKey: Uint8Array };\nexport type KeyPrivate = { privateKey: Uint8Array };\nexport type KeyPair = KeyPublic & KeyPrivate;\n\nexport type SigningKeyPublic = { publicSigningKey: Uint8Array };\nexport type SigningKeyPrivate = { privateSigningKey: Uint8Array };\nexport type SigningKeyPair = SigningKeyPublic & SigningKeyPrivate;\n\nexport interface EncryptedMessage {\n  // recipient\n  uid: string;\n  // ciphertext\n  message: string;\n}\n\nexport type Serializable =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | Date\n  | Serializable[]\n  | { [key: string]: Serializable };\n\nexport interface EncryptableObject {\n  [key: string]: Serializable;\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/actions.ts",
    "content": "import { naclBoxPrivateKeyFromMnemonic } from './utils/mnemonic';\nimport { derivePublicKey, generateSigningKeys } from './utils/nacl';\n\nimport type { KeyPair, SigningKeyPair } from '@emberclear/crypto/types';\n\nexport async function login(mnemonic: string): Promise<KeyPair & SigningKeyPair> {\n  let privateKey = await naclBoxPrivateKeyFromMnemonic(mnemonic);\n  let publicKey = await derivePublicKey(privateKey);\n\n  let { publicSigningKey, privateSigningKey } = await generateSigningKeys();\n\n  return { publicKey, privateKey, publicSigningKey, privateSigningKey };\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/index.ts",
    "content": "import { PWBWorker } from 'promise-worker-bi';\n\nimport { handleMessage } from './messages';\n\nimport type { CryptoMessage } from './messages';\n\nlet promiseWorker = new PWBWorker();\n\npromiseWorker.register(function (message: CryptoMessage) {\n  return handleMessage(message);\n});\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/messages.ts",
    "content": "import { login } from './actions';\nimport { mnemonicFromNaClBoxPrivateKey } from './utils/mnemonic';\nimport {\n  derivePublicKey,\n  derivePublicSigningKey,\n  generateAsymmetricKeys,\n  generateSigningKeys,\n  hash,\n  openSigned,\n  sign,\n} from './utils/nacl';\nimport { decryptFromSocket, encryptForSocket } from './utils/socket';\n\nconst Action = {\n  // Generation\n  LOGIN: 0,\n  GENERATE_KEYS: 1,\n  DECRYPT_FROM_SOCKET: 2,\n  ENCRYPT_FOR_SOCKET: 3,\n  GENERATE_SIGNING_KEYS: 4,\n  SIGN: 5,\n  OPEN_SIGNED: 6,\n  HASH: 7,\n\n  // Conversions\n  MNEMONIC_FROM_PRIVATE_KEY: 50,\n\n  // TODO: should find a way to not need these\n  DERIVE_PUBLIC_KEY: 100,\n  DERIVE_PUBLIC_SIGNING_KEY: 101,\n} as const;\n\nexport type API = {\n  [Action.LOGIN]: typeof login;\n  [Action.GENERATE_KEYS]: typeof generateAsymmetricKeys;\n  [Action.GENERATE_SIGNING_KEYS]: typeof generateSigningKeys;\n  [Action.DECRYPT_FROM_SOCKET]: typeof decryptFromSocket;\n  [Action.ENCRYPT_FOR_SOCKET]: typeof encryptForSocket;\n  [Action.SIGN]: typeof sign;\n  [Action.OPEN_SIGNED]: typeof openSigned;\n  [Action.HASH]: typeof hash;\n  [Action.DERIVE_PUBLIC_KEY]: typeof derivePublicKey;\n  [Action.DERIVE_PUBLIC_SIGNING_KEY]: typeof derivePublicSigningKey;\n  [Action.MNEMONIC_FROM_PRIVATE_KEY]: typeof mnemonicFromNaClBoxPrivateKey;\n};\n\nexport type CryptoMessage = {\n  action: keyof API;\n  args: Parameters<API[keyof API]>;\n};\n\nexport type Message<Action, Args> = { action: Action; args: Args };\n\nexport function handleMessage<Action extends keyof API>(\n  message: Message<Action, Parameters<API[Action]>>\n): ReturnType<API[Action]> {\n  const { action, args } = message as TODO;\n\n  switch (action) {\n    case Action.LOGIN:\n      return (login as TODO)(...args);\n    case Action.GENERATE_KEYS:\n      return (generateAsymmetricKeys as TODO)();\n    case Action.GENERATE_SIGNING_KEYS:\n      return (generateSigningKeys as TODO)();\n    case Action.DECRYPT_FROM_SOCKET:\n      return (decryptFromSocket as TODO)(...args);\n    case Action.ENCRYPT_FOR_SOCKET:\n      return (encryptForSocket as TODO)(...args);\n    case Action.SIGN:\n      return (sign as TODO)(...args);\n    case Action.OPEN_SIGNED:\n      return (openSigned as TODO)(...args);\n    case Action.HASH:\n      return (hash as TODO)(...args);\n    case Action.DERIVE_PUBLIC_KEY:\n      return (derivePublicKey as TODO)(...args);\n    case Action.DERIVE_PUBLIC_SIGNING_KEY:\n      return (derivePublicSigningKey as TODO)(...args);\n    case Action.MNEMONIC_FROM_PRIVATE_KEY:\n      return (mnemonicFromNaClBoxPrivateKey as TODO)(...args);\n    default:\n      throw new Error(`unknown message for crypto worker: ${action}`);\n  }\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/array.ts",
    "content": "export function concat(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {\n  const result = new Uint8Array(arr1.length + arr2.length);\n\n  result.set(arr1, 0);\n  result.set(arr2, arr1.length);\n\n  return result;\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/bip39/wordlists/english.ts",
    "content": "export const wordlist = [\n  'abandon',\n  'ability',\n  'able',\n  'about',\n  'above',\n  'absent',\n  'absorb',\n  'abstract',\n  'absurd',\n  'abuse',\n  'access',\n  'accident',\n  'account',\n  'accuse',\n  'achieve',\n  'acid',\n  'acoustic',\n  'acquire',\n  'across',\n  'act',\n  'action',\n  'actor',\n  'actress',\n  'actual',\n  'adapt',\n  'add',\n  'addict',\n  'address',\n  'adjust',\n  'admit',\n  'adult',\n  'advance',\n  'advice',\n  'aerobic',\n  'affair',\n  'afford',\n  'afraid',\n  'again',\n  'age',\n  'agent',\n  'agree',\n  'ahead',\n  'aim',\n  'air',\n  'airport',\n  'aisle',\n  'alarm',\n  'album',\n  'alcohol',\n  'alert',\n  'alien',\n  'all',\n  'alley',\n  'allow',\n  'almost',\n  'alone',\n  'alpha',\n  'already',\n  'also',\n  'alter',\n  'always',\n  'amateur',\n  'amazing',\n  'among',\n  'amount',\n  'amused',\n  'analyst',\n  'anchor',\n  'ancient',\n  'anger',\n  'angle',\n  'angry',\n  'animal',\n  'ankle',\n  'announce',\n  'annual',\n  'another',\n  'answer',\n  'antenna',\n  'antique',\n  'anxiety',\n  'any',\n  'apart',\n  'apology',\n  'appear',\n  'apple',\n  'approve',\n  'april',\n  'arch',\n  'arctic',\n  'area',\n  'arena',\n  'argue',\n  'arm',\n  'armed',\n  'armor',\n  'army',\n  'around',\n  'arrange',\n  'arrest',\n  'arrive',\n  'arrow',\n  'art',\n  'artefact',\n  'artist',\n  'artwork',\n  'ask',\n  'aspect',\n  'assault',\n  'asset',\n  'assist',\n  'assume',\n  'asthma',\n  'athlete',\n  'atom',\n  'attack',\n  'attend',\n  'attitude',\n  'attract',\n  'auction',\n  'audit',\n  'august',\n  'aunt',\n  'author',\n  'auto',\n  'autumn',\n  'average',\n  'avocado',\n  'avoid',\n  'awake',\n  'aware',\n  'away',\n  'awesome',\n  'awful',\n  'awkward',\n  'axis',\n  'baby',\n  'bachelor',\n  'bacon',\n  'badge',\n  'bag',\n  'balance',\n  'balcony',\n  'ball',\n  'bamboo',\n  'banana',\n  'banner',\n  'bar',\n  'barely',\n  'bargain',\n  'barrel',\n  'base',\n  'basic',\n  'basket',\n  'battle',\n  'beach',\n  'bean',\n  'beauty',\n  'because',\n  'become',\n  'beef',\n  'before',\n  'begin',\n  'behave',\n  'behind',\n  'believe',\n  'below',\n  'belt',\n  'bench',\n  'benefit',\n  'best',\n  'betray',\n  'better',\n  'between',\n  'beyond',\n  'bicycle',\n  'bid',\n  'bike',\n  'bind',\n  'biology',\n  'bird',\n  'birth',\n  'bitter',\n  'black',\n  'blade',\n  'blame',\n  'blanket',\n  'blast',\n  'bleak',\n  'bless',\n  'blind',\n  'blood',\n  'blossom',\n  'blouse',\n  'blue',\n  'blur',\n  'blush',\n  'board',\n  'boat',\n  'body',\n  'boil',\n  'bomb',\n  'bone',\n  'bonus',\n  'book',\n  'boost',\n  'border',\n  'boring',\n  'borrow',\n  'boss',\n  'bottom',\n  'bounce',\n  'box',\n  'boy',\n  'bracket',\n  'brain',\n  'brand',\n  'brass',\n  'brave',\n  'bread',\n  'breeze',\n  'brick',\n  'bridge',\n  'brief',\n  'bright',\n  'bring',\n  'brisk',\n  'broccoli',\n  'broken',\n  'bronze',\n  'broom',\n  'brother',\n  'brown',\n  'brush',\n  'bubble',\n  'buddy',\n  'budget',\n  'buffalo',\n  'build',\n  'bulb',\n  'bulk',\n  'bullet',\n  'bundle',\n  'bunker',\n  'burden',\n  'burger',\n  'burst',\n  'bus',\n  'business',\n  'busy',\n  'butter',\n  'buyer',\n  'buzz',\n  'cabbage',\n  'cabin',\n  'cable',\n  'cactus',\n  'cage',\n  'cake',\n  'call',\n  'calm',\n  'camera',\n  'camp',\n  'can',\n  'canal',\n  'cancel',\n  'candy',\n  'cannon',\n  'canoe',\n  'canvas',\n  'canyon',\n  'capable',\n  'capital',\n  'captain',\n  'car',\n  'carbon',\n  'card',\n  'cargo',\n  'carpet',\n  'carry',\n  'cart',\n  'case',\n  'cash',\n  'casino',\n  'castle',\n  'casual',\n  'cat',\n  'catalog',\n  'catch',\n  'category',\n  'cattle',\n  'caught',\n  'cause',\n  'caution',\n  'cave',\n  'ceiling',\n  'celery',\n  'cement',\n  'census',\n  'century',\n  'cereal',\n  'certain',\n  'chair',\n  'chalk',\n  'champion',\n  'change',\n  'chaos',\n  'chapter',\n  'charge',\n  'chase',\n  'chat',\n  'cheap',\n  'check',\n  'cheese',\n  'chef',\n  'cherry',\n  'chest',\n  'chicken',\n  'chief',\n  'child',\n  'chimney',\n  'choice',\n  'choose',\n  'chronic',\n  'chuckle',\n  'chunk',\n  'churn',\n  'cigar',\n  'cinnamon',\n  'circle',\n  'citizen',\n  'city',\n  'civil',\n  'claim',\n  'clap',\n  'clarify',\n  'claw',\n  'clay',\n  'clean',\n  'clerk',\n  'clever',\n  'click',\n  'client',\n  'cliff',\n  'climb',\n  'clinic',\n  'clip',\n  'clock',\n  'clog',\n  'close',\n  'cloth',\n  'cloud',\n  'clown',\n  'club',\n  'clump',\n  'cluster',\n  'clutch',\n  'coach',\n  'coast',\n  'coconut',\n  'code',\n  'coffee',\n  'coil',\n  'coin',\n  'collect',\n  'color',\n  'column',\n  'combine',\n  'come',\n  'comfort',\n  'comic',\n  'common',\n  'company',\n  'concert',\n  'conduct',\n  'confirm',\n  'congress',\n  'connect',\n  'consider',\n  'control',\n  'convince',\n  'cook',\n  'cool',\n  'copper',\n  'copy',\n  'coral',\n  'core',\n  'corn',\n  'correct',\n  'cost',\n  'cotton',\n  'couch',\n  'country',\n  'couple',\n  'course',\n  'cousin',\n  'cover',\n  'coyote',\n  'crack',\n  'cradle',\n  'craft',\n  'cram',\n  'crane',\n  'crash',\n  'crater',\n  'crawl',\n  'crazy',\n  'cream',\n  'credit',\n  'creek',\n  'crew',\n  'cricket',\n  'crime',\n  'crisp',\n  'critic',\n  'crop',\n  'cross',\n  'crouch',\n  'crowd',\n  'crucial',\n  'cruel',\n  'cruise',\n  'crumble',\n  'crunch',\n  'crush',\n  'cry',\n  'crystal',\n  'cube',\n  'culture',\n  'cup',\n  'cupboard',\n  'curious',\n  'current',\n  'curtain',\n  'curve',\n  'cushion',\n  'custom',\n  'cute',\n  'cycle',\n  'dad',\n  'damage',\n  'damp',\n  'dance',\n  'danger',\n  'daring',\n  'dash',\n  'daughter',\n  'dawn',\n  'day',\n  'deal',\n  'debate',\n  'debris',\n  'decade',\n  'december',\n  'decide',\n  'decline',\n  'decorate',\n  'decrease',\n  'deer',\n  'defense',\n  'define',\n  'defy',\n  'degree',\n  'delay',\n  'deliver',\n  'demand',\n  'demise',\n  'denial',\n  'dentist',\n  'deny',\n  'depart',\n  'depend',\n  'deposit',\n  'depth',\n  'deputy',\n  'derive',\n  'describe',\n  'desert',\n  'design',\n  'desk',\n  'despair',\n  'destroy',\n  'detail',\n  'detect',\n  'develop',\n  'device',\n  'devote',\n  'diagram',\n  'dial',\n  'diamond',\n  'diary',\n  'dice',\n  'diesel',\n  'diet',\n  'differ',\n  'digital',\n  'dignity',\n  'dilemma',\n  'dinner',\n  'dinosaur',\n  'direct',\n  'dirt',\n  'disagree',\n  'discover',\n  'disease',\n  'dish',\n  'dismiss',\n  'disorder',\n  'display',\n  'distance',\n  'divert',\n  'divide',\n  'divorce',\n  'dizzy',\n  'doctor',\n  'document',\n  'dog',\n  'doll',\n  'dolphin',\n  'domain',\n  'donate',\n  'donkey',\n  'donor',\n  'door',\n  'dose',\n  'double',\n  'dove',\n  'draft',\n  'dragon',\n  'drama',\n  'drastic',\n  'draw',\n  'dream',\n  'dress',\n  'drift',\n  'drill',\n  'drink',\n  'drip',\n  'drive',\n  'drop',\n  'drum',\n  'dry',\n  'duck',\n  'dumb',\n  'dune',\n  'during',\n  'dust',\n  'dutch',\n  'duty',\n  'dwarf',\n  'dynamic',\n  'eager',\n  'eagle',\n  'early',\n  'earn',\n  'earth',\n  'easily',\n  'east',\n  'easy',\n  'echo',\n  'ecology',\n  'economy',\n  'edge',\n  'edit',\n  'educate',\n  'effort',\n  'egg',\n  'eight',\n  'either',\n  'elbow',\n  'elder',\n  'electric',\n  'elegant',\n  'element',\n  'elephant',\n  'elevator',\n  'elite',\n  'else',\n  'embark',\n  'embody',\n  'embrace',\n  'emerge',\n  'emotion',\n  'employ',\n  'empower',\n  'empty',\n  'enable',\n  'enact',\n  'end',\n  'endless',\n  'endorse',\n  'enemy',\n  'energy',\n  'enforce',\n  'engage',\n  'engine',\n  'enhance',\n  'enjoy',\n  'enlist',\n  'enough',\n  'enrich',\n  'enroll',\n  'ensure',\n  'enter',\n  'entire',\n  'entry',\n  'envelope',\n  'episode',\n  'equal',\n  'equip',\n  'era',\n  'erase',\n  'erode',\n  'erosion',\n  'error',\n  'erupt',\n  'escape',\n  'essay',\n  'essence',\n  'estate',\n  'eternal',\n  'ethics',\n  'evidence',\n  'evil',\n  'evoke',\n  'evolve',\n  'exact',\n  'example',\n  'excess',\n  'exchange',\n  'excite',\n  'exclude',\n  'excuse',\n  'execute',\n  'exercise',\n  'exhaust',\n  'exhibit',\n  'exile',\n  'exist',\n  'exit',\n  'exotic',\n  'expand',\n  'expect',\n  'expire',\n  'explain',\n  'expose',\n  'express',\n  'extend',\n  'extra',\n  'eye',\n  'eyebrow',\n  'fabric',\n  'face',\n  'faculty',\n  'fade',\n  'faint',\n  'faith',\n  'fall',\n  'false',\n  'fame',\n  'family',\n  'famous',\n  'fan',\n  'fancy',\n  'fantasy',\n  'farm',\n  'fashion',\n  'fat',\n  'fatal',\n  'father',\n  'fatigue',\n  'fault',\n  'favorite',\n  'feature',\n  'february',\n  'federal',\n  'fee',\n  'feed',\n  'feel',\n  'female',\n  'fence',\n  'festival',\n  'fetch',\n  'fever',\n  'few',\n  'fiber',\n  'fiction',\n  'field',\n  'figure',\n  'file',\n  'film',\n  'filter',\n  'final',\n  'find',\n  'fine',\n  'finger',\n  'finish',\n  'fire',\n  'firm',\n  'first',\n  'fiscal',\n  'fish',\n  'fit',\n  'fitness',\n  'fix',\n  'flag',\n  'flame',\n  'flash',\n  'flat',\n  'flavor',\n  'flee',\n  'flight',\n  'flip',\n  'float',\n  'flock',\n  'floor',\n  'flower',\n  'fluid',\n  'flush',\n  'fly',\n  'foam',\n  'focus',\n  'fog',\n  'foil',\n  'fold',\n  'follow',\n  'food',\n  'foot',\n  'force',\n  'forest',\n  'forget',\n  'fork',\n  'fortune',\n  'forum',\n  'forward',\n  'fossil',\n  'foster',\n  'found',\n  'fox',\n  'fragile',\n  'frame',\n  'frequent',\n  'fresh',\n  'friend',\n  'fringe',\n  'frog',\n  'front',\n  'frost',\n  'frown',\n  'frozen',\n  'fruit',\n  'fuel',\n  'fun',\n  'funny',\n  'furnace',\n  'fury',\n  'future',\n  'gadget',\n  'gain',\n  'galaxy',\n  'gallery',\n  'game',\n  'gap',\n  'garage',\n  'garbage',\n  'garden',\n  'garlic',\n  'garment',\n  'gas',\n  'gasp',\n  'gate',\n  'gather',\n  'gauge',\n  'gaze',\n  'general',\n  'genius',\n  'genre',\n  'gentle',\n  'genuine',\n  'gesture',\n  'ghost',\n  'giant',\n  'gift',\n  'giggle',\n  'ginger',\n  'giraffe',\n  'girl',\n  'give',\n  'glad',\n  'glance',\n  'glare',\n  'glass',\n  'glide',\n  'glimpse',\n  'globe',\n  'gloom',\n  'glory',\n  'glove',\n  'glow',\n  'glue',\n  'goat',\n  'goddess',\n  'gold',\n  'good',\n  'goose',\n  'gorilla',\n  'gospel',\n  'gossip',\n  'govern',\n  'gown',\n  'grab',\n  'grace',\n  'grain',\n  'grant',\n  'grape',\n  'grass',\n  'gravity',\n  'great',\n  'green',\n  'grid',\n  'grief',\n  'grit',\n  'grocery',\n  'group',\n  'grow',\n  'grunt',\n  'guard',\n  'guess',\n  'guide',\n  'guilt',\n  'guitar',\n  'gun',\n  'gym',\n  'habit',\n  'hair',\n  'half',\n  'hammer',\n  'hamster',\n  'hand',\n  'happy',\n  'harbor',\n  'hard',\n  'harsh',\n  'harvest',\n  'hat',\n  'have',\n  'hawk',\n  'hazard',\n  'head',\n  'health',\n  'heart',\n  'heavy',\n  'hedgehog',\n  'height',\n  'hello',\n  'helmet',\n  'help',\n  'hen',\n  'hero',\n  'hidden',\n  'high',\n  'hill',\n  'hint',\n  'hip',\n  'hire',\n  'history',\n  'hobby',\n  'hockey',\n  'hold',\n  'hole',\n  'holiday',\n  'hollow',\n  'home',\n  'honey',\n  'hood',\n  'hope',\n  'horn',\n  'horror',\n  'horse',\n  'hospital',\n  'host',\n  'hotel',\n  'hour',\n  'hover',\n  'hub',\n  'huge',\n  'human',\n  'humble',\n  'humor',\n  'hundred',\n  'hungry',\n  'hunt',\n  'hurdle',\n  'hurry',\n  'hurt',\n  'husband',\n  'hybrid',\n  'ice',\n  'icon',\n  'idea',\n  'identify',\n  'idle',\n  'ignore',\n  'ill',\n  'illegal',\n  'illness',\n  'image',\n  'imitate',\n  'immense',\n  'immune',\n  'impact',\n  'impose',\n  'improve',\n  'impulse',\n  'inch',\n  'include',\n  'income',\n  'increase',\n  'index',\n  'indicate',\n  'indoor',\n  'industry',\n  'infant',\n  'inflict',\n  'inform',\n  'inhale',\n  'inherit',\n  'initial',\n  'inject',\n  'injury',\n  'inmate',\n  'inner',\n  'innocent',\n  'input',\n  'inquiry',\n  'insane',\n  'insect',\n  'inside',\n  'inspire',\n  'install',\n  'intact',\n  'interest',\n  'into',\n  'invest',\n  'invite',\n  'involve',\n  'iron',\n  'island',\n  'isolate',\n  'issue',\n  'item',\n  'ivory',\n  'jacket',\n  'jaguar',\n  'jar',\n  'jazz',\n  'jealous',\n  'jeans',\n  'jelly',\n  'jewel',\n  'job',\n  'join',\n  'joke',\n  'journey',\n  'joy',\n  'judge',\n  'juice',\n  'jump',\n  'jungle',\n  'junior',\n  'junk',\n  'just',\n  'kangaroo',\n  'keen',\n  'keep',\n  'ketchup',\n  'key',\n  'kick',\n  'kid',\n  'kidney',\n  'kind',\n  'kingdom',\n  'kiss',\n  'kit',\n  'kitchen',\n  'kite',\n  'kitten',\n  'kiwi',\n  'knee',\n  'knife',\n  'knock',\n  'know',\n  'lab',\n  'label',\n  'labor',\n  'ladder',\n  'lady',\n  'lake',\n  'lamp',\n  'language',\n  'laptop',\n  'large',\n  'later',\n  'latin',\n  'laugh',\n  'laundry',\n  'lava',\n  'law',\n  'lawn',\n  'lawsuit',\n  'layer',\n  'lazy',\n  'leader',\n  'leaf',\n  'learn',\n  'leave',\n  'lecture',\n  'left',\n  'leg',\n  'legal',\n  'legend',\n  'leisure',\n  'lemon',\n  'lend',\n  'length',\n  'lens',\n  'leopard',\n  'lesson',\n  'letter',\n  'level',\n  'liar',\n  'liberty',\n  'library',\n  'license',\n  'life',\n  'lift',\n  'light',\n  'like',\n  'limb',\n  'limit',\n  'link',\n  'lion',\n  'liquid',\n  'list',\n  'little',\n  'live',\n  'lizard',\n  'load',\n  'loan',\n  'lobster',\n  'local',\n  'lock',\n  'logic',\n  'lonely',\n  'long',\n  'loop',\n  'lottery',\n  'loud',\n  'lounge',\n  'love',\n  'loyal',\n  'lucky',\n  'luggage',\n  'lumber',\n  'lunar',\n  'lunch',\n  'luxury',\n  'lyrics',\n  'machine',\n  'mad',\n  'magic',\n  'magnet',\n  'maid',\n  'mail',\n  'main',\n  'major',\n  'make',\n  'mammal',\n  'man',\n  'manage',\n  'mandate',\n  'mango',\n  'mansion',\n  'manual',\n  'maple',\n  'marble',\n  'march',\n  'margin',\n  'marine',\n  'market',\n  'marriage',\n  'mask',\n  'mass',\n  'master',\n  'match',\n  'material',\n  'math',\n  'matrix',\n  'matter',\n  'maximum',\n  'maze',\n  'meadow',\n  'mean',\n  'measure',\n  'meat',\n  'mechanic',\n  'medal',\n  'media',\n  'melody',\n  'melt',\n  'member',\n  'memory',\n  'mention',\n  'menu',\n  'mercy',\n  'merge',\n  'merit',\n  'merry',\n  'mesh',\n  'message',\n  'metal',\n  'method',\n  'middle',\n  'midnight',\n  'milk',\n  'million',\n  'mimic',\n  'mind',\n  'minimum',\n  'minor',\n  'minute',\n  'miracle',\n  'mirror',\n  'misery',\n  'miss',\n  'mistake',\n  'mix',\n  'mixed',\n  'mixture',\n  'mobile',\n  'model',\n  'modify',\n  'mom',\n  'moment',\n  'monitor',\n  'monkey',\n  'monster',\n  'month',\n  'moon',\n  'moral',\n  'more',\n  'morning',\n  'mosquito',\n  'mother',\n  'motion',\n  'motor',\n  'mountain',\n  'mouse',\n  'move',\n  'movie',\n  'much',\n  'muffin',\n  'mule',\n  'multiply',\n  'muscle',\n  'museum',\n  'mushroom',\n  'music',\n  'must',\n  'mutual',\n  'myself',\n  'mystery',\n  'myth',\n  'naive',\n  'name',\n  'napkin',\n  'narrow',\n  'nasty',\n  'nation',\n  'nature',\n  'near',\n  'neck',\n  'need',\n  'negative',\n  'neglect',\n  'neither',\n  'nephew',\n  'nerve',\n  'nest',\n  'net',\n  'network',\n  'neutral',\n  'never',\n  'news',\n  'next',\n  'nice',\n  'night',\n  'noble',\n  'noise',\n  'nominee',\n  'noodle',\n  'normal',\n  'north',\n  'nose',\n  'notable',\n  'note',\n  'nothing',\n  'notice',\n  'novel',\n  'now',\n  'nuclear',\n  'number',\n  'nurse',\n  'nut',\n  'oak',\n  'obey',\n  'object',\n  'oblige',\n  'obscure',\n  'observe',\n  'obtain',\n  'obvious',\n  'occur',\n  'ocean',\n  'october',\n  'odor',\n  'off',\n  'offer',\n  'office',\n  'often',\n  'oil',\n  'okay',\n  'old',\n  'olive',\n  'olympic',\n  'omit',\n  'once',\n  'one',\n  'onion',\n  'online',\n  'only',\n  'open',\n  'opera',\n  'opinion',\n  'oppose',\n  'option',\n  'orange',\n  'orbit',\n  'orchard',\n  'order',\n  'ordinary',\n  'organ',\n  'orient',\n  'original',\n  'orphan',\n  'ostrich',\n  'other',\n  'outdoor',\n  'outer',\n  'output',\n  'outside',\n  'oval',\n  'oven',\n  'over',\n  'own',\n  'owner',\n  'oxygen',\n  'oyster',\n  'ozone',\n  'pact',\n  'paddle',\n  'page',\n  'pair',\n  'palace',\n  'palm',\n  'panda',\n  'panel',\n  'panic',\n  'panther',\n  'paper',\n  'parade',\n  'parent',\n  'park',\n  'parrot',\n  'party',\n  'pass',\n  'patch',\n  'path',\n  'patient',\n  'patrol',\n  'pattern',\n  'pause',\n  'pave',\n  'payment',\n  'peace',\n  'peanut',\n  'pear',\n  'peasant',\n  'pelican',\n  'pen',\n  'penalty',\n  'pencil',\n  'people',\n  'pepper',\n  'perfect',\n  'permit',\n  'person',\n  'pet',\n  'phone',\n  'photo',\n  'phrase',\n  'physical',\n  'piano',\n  'picnic',\n  'picture',\n  'piece',\n  'pig',\n  'pigeon',\n  'pill',\n  'pilot',\n  'pink',\n  'pioneer',\n  'pipe',\n  'pistol',\n  'pitch',\n  'pizza',\n  'place',\n  'planet',\n  'plastic',\n  'plate',\n  'play',\n  'please',\n  'pledge',\n  'pluck',\n  'plug',\n  'plunge',\n  'poem',\n  'poet',\n  'point',\n  'polar',\n  'pole',\n  'police',\n  'pond',\n  'pony',\n  'pool',\n  'popular',\n  'portion',\n  'position',\n  'possible',\n  'post',\n  'potato',\n  'pottery',\n  'poverty',\n  'powder',\n  'power',\n  'practice',\n  'praise',\n  'predict',\n  'prefer',\n  'prepare',\n  'present',\n  'pretty',\n  'prevent',\n  'price',\n  'pride',\n  'primary',\n  'print',\n  'priority',\n  'prison',\n  'private',\n  'prize',\n  'problem',\n  'process',\n  'produce',\n  'profit',\n  'program',\n  'project',\n  'promote',\n  'proof',\n  'property',\n  'prosper',\n  'protect',\n  'proud',\n  'provide',\n  'public',\n  'pudding',\n  'pull',\n  'pulp',\n  'pulse',\n  'pumpkin',\n  'punch',\n  'pupil',\n  'puppy',\n  'purchase',\n  'purity',\n  'purpose',\n  'purse',\n  'push',\n  'put',\n  'puzzle',\n  'pyramid',\n  'quality',\n  'quantum',\n  'quarter',\n  'question',\n  'quick',\n  'quit',\n  'quiz',\n  'quote',\n  'rabbit',\n  'raccoon',\n  'race',\n  'rack',\n  'radar',\n  'radio',\n  'rail',\n  'rain',\n  'raise',\n  'rally',\n  'ramp',\n  'ranch',\n  'random',\n  'range',\n  'rapid',\n  'rare',\n  'rate',\n  'rather',\n  'raven',\n  'raw',\n  'razor',\n  'ready',\n  'real',\n  'reason',\n  'rebel',\n  'rebuild',\n  'recall',\n  'receive',\n  'recipe',\n  'record',\n  'recycle',\n  'reduce',\n  'reflect',\n  'reform',\n  'refuse',\n  'region',\n  'regret',\n  'regular',\n  'reject',\n  'relax',\n  'release',\n  'relief',\n  'rely',\n  'remain',\n  'remember',\n  'remind',\n  'remove',\n  'render',\n  'renew',\n  'rent',\n  'reopen',\n  'repair',\n  'repeat',\n  'replace',\n  'report',\n  'require',\n  'rescue',\n  'resemble',\n  'resist',\n  'resource',\n  'response',\n  'result',\n  'retire',\n  'retreat',\n  'return',\n  'reunion',\n  'reveal',\n  'review',\n  'reward',\n  'rhythm',\n  'rib',\n  'ribbon',\n  'rice',\n  'rich',\n  'ride',\n  'ridge',\n  'rifle',\n  'right',\n  'rigid',\n  'ring',\n  'riot',\n  'ripple',\n  'risk',\n  'ritual',\n  'rival',\n  'river',\n  'road',\n  'roast',\n  'robot',\n  'robust',\n  'rocket',\n  'romance',\n  'roof',\n  'rookie',\n  'room',\n  'rose',\n  'rotate',\n  'rough',\n  'round',\n  'route',\n  'royal',\n  'rubber',\n  'rude',\n  'rug',\n  'rule',\n  'run',\n  'runway',\n  'rural',\n  'sad',\n  'saddle',\n  'sadness',\n  'safe',\n  'sail',\n  'salad',\n  'salmon',\n  'salon',\n  'salt',\n  'salute',\n  'same',\n  'sample',\n  'sand',\n  'satisfy',\n  'satoshi',\n  'sauce',\n  'sausage',\n  'save',\n  'say',\n  'scale',\n  'scan',\n  'scare',\n  'scatter',\n  'scene',\n  'scheme',\n  'school',\n  'science',\n  'scissors',\n  'scorpion',\n  'scout',\n  'scrap',\n  'screen',\n  'script',\n  'scrub',\n  'sea',\n  'search',\n  'season',\n  'seat',\n  'second',\n  'secret',\n  'section',\n  'security',\n  'seed',\n  'seek',\n  'segment',\n  'select',\n  'sell',\n  'seminar',\n  'senior',\n  'sense',\n  'sentence',\n  'series',\n  'service',\n  'session',\n  'settle',\n  'setup',\n  'seven',\n  'shadow',\n  'shaft',\n  'shallow',\n  'share',\n  'shed',\n  'shell',\n  'sheriff',\n  'shield',\n  'shift',\n  'shine',\n  'ship',\n  'shiver',\n  'shock',\n  'shoe',\n  'shoot',\n  'shop',\n  'short',\n  'shoulder',\n  'shove',\n  'shrimp',\n  'shrug',\n  'shuffle',\n  'shy',\n  'sibling',\n  'sick',\n  'side',\n  'siege',\n  'sight',\n  'sign',\n  'silent',\n  'silk',\n  'silly',\n  'silver',\n  'similar',\n  'simple',\n  'since',\n  'sing',\n  'siren',\n  'sister',\n  'situate',\n  'six',\n  'size',\n  'skate',\n  'sketch',\n  'ski',\n  'skill',\n  'skin',\n  'skirt',\n  'skull',\n  'slab',\n  'slam',\n  'sleep',\n  'slender',\n  'slice',\n  'slide',\n  'slight',\n  'slim',\n  'slogan',\n  'slot',\n  'slow',\n  'slush',\n  'small',\n  'smart',\n  'smile',\n  'smoke',\n  'smooth',\n  'snack',\n  'snake',\n  'snap',\n  'sniff',\n  'snow',\n  'soap',\n  'soccer',\n  'social',\n  'sock',\n  'soda',\n  'soft',\n  'solar',\n  'soldier',\n  'solid',\n  'solution',\n  'solve',\n  'someone',\n  'song',\n  'soon',\n  'sorry',\n  'sort',\n  'soul',\n  'sound',\n  'soup',\n  'source',\n  'south',\n  'space',\n  'spare',\n  'spatial',\n  'spawn',\n  'speak',\n  'special',\n  'speed',\n  'spell',\n  'spend',\n  'sphere',\n  'spice',\n  'spider',\n  'spike',\n  'spin',\n  'spirit',\n  'split',\n  'spoil',\n  'sponsor',\n  'spoon',\n  'sport',\n  'spot',\n  'spray',\n  'spread',\n  'spring',\n  'spy',\n  'square',\n  'squeeze',\n  'squirrel',\n  'stable',\n  'stadium',\n  'staff',\n  'stage',\n  'stairs',\n  'stamp',\n  'stand',\n  'start',\n  'state',\n  'stay',\n  'steak',\n  'steel',\n  'stem',\n  'step',\n  'stereo',\n  'stick',\n  'still',\n  'sting',\n  'stock',\n  'stomach',\n  'stone',\n  'stool',\n  'story',\n  'stove',\n  'strategy',\n  'street',\n  'strike',\n  'strong',\n  'struggle',\n  'student',\n  'stuff',\n  'stumble',\n  'style',\n  'subject',\n  'submit',\n  'subway',\n  'success',\n  'such',\n  'sudden',\n  'suffer',\n  'sugar',\n  'suggest',\n  'suit',\n  'summer',\n  'sun',\n  'sunny',\n  'sunset',\n  'super',\n  'supply',\n  'supreme',\n  'sure',\n  'surface',\n  'surge',\n  'surprise',\n  'surround',\n  'survey',\n  'suspect',\n  'sustain',\n  'swallow',\n  'swamp',\n  'swap',\n  'swarm',\n  'swear',\n  'sweet',\n  'swift',\n  'swim',\n  'swing',\n  'switch',\n  'sword',\n  'symbol',\n  'symptom',\n  'syrup',\n  'system',\n  'table',\n  'tackle',\n  'tag',\n  'tail',\n  'talent',\n  'talk',\n  'tank',\n  'tape',\n  'target',\n  'task',\n  'taste',\n  'tattoo',\n  'taxi',\n  'teach',\n  'team',\n  'tell',\n  'ten',\n  'tenant',\n  'tennis',\n  'tent',\n  'term',\n  'test',\n  'text',\n  'thank',\n  'that',\n  'theme',\n  'then',\n  'theory',\n  'there',\n  'they',\n  'thing',\n  'this',\n  'thought',\n  'three',\n  'thrive',\n  'throw',\n  'thumb',\n  'thunder',\n  'ticket',\n  'tide',\n  'tiger',\n  'tilt',\n  'timber',\n  'time',\n  'tiny',\n  'tip',\n  'tired',\n  'tissue',\n  'title',\n  'toast',\n  'tobacco',\n  'today',\n  'toddler',\n  'toe',\n  'together',\n  'toilet',\n  'token',\n  'tomato',\n  'tomorrow',\n  'tone',\n  'tongue',\n  'tonight',\n  'tool',\n  'tooth',\n  'top',\n  'topic',\n  'topple',\n  'torch',\n  'tornado',\n  'tortoise',\n  'toss',\n  'total',\n  'tourist',\n  'toward',\n  'tower',\n  'town',\n  'toy',\n  'track',\n  'trade',\n  'traffic',\n  'tragic',\n  'train',\n  'transfer',\n  'trap',\n  'trash',\n  'travel',\n  'tray',\n  'treat',\n  'tree',\n  'trend',\n  'trial',\n  'tribe',\n  'trick',\n  'trigger',\n  'trim',\n  'trip',\n  'trophy',\n  'trouble',\n  'truck',\n  'true',\n  'truly',\n  'trumpet',\n  'trust',\n  'truth',\n  'try',\n  'tube',\n  'tuition',\n  'tumble',\n  'tuna',\n  'tunnel',\n  'turkey',\n  'turn',\n  'turtle',\n  'twelve',\n  'twenty',\n  'twice',\n  'twin',\n  'twist',\n  'two',\n  'type',\n  'typical',\n  'ugly',\n  'umbrella',\n  'unable',\n  'unaware',\n  'uncle',\n  'uncover',\n  'under',\n  'undo',\n  'unfair',\n  'unfold',\n  'unhappy',\n  'uniform',\n  'unique',\n  'unit',\n  'universe',\n  'unknown',\n  'unlock',\n  'until',\n  'unusual',\n  'unveil',\n  'update',\n  'upgrade',\n  'uphold',\n  'upon',\n  'upper',\n  'upset',\n  'urban',\n  'urge',\n  'usage',\n  'use',\n  'used',\n  'useful',\n  'useless',\n  'usual',\n  'utility',\n  'vacant',\n  'vacuum',\n  'vague',\n  'valid',\n  'valley',\n  'valve',\n  'van',\n  'vanish',\n  'vapor',\n  'various',\n  'vast',\n  'vault',\n  'vehicle',\n  'velvet',\n  'vendor',\n  'venture',\n  'venue',\n  'verb',\n  'verify',\n  'version',\n  'very',\n  'vessel',\n  'veteran',\n  'viable',\n  'vibrant',\n  'vicious',\n  'victory',\n  'video',\n  'view',\n  'village',\n  'vintage',\n  'violin',\n  'virtual',\n  'virus',\n  'visa',\n  'visit',\n  'visual',\n  'vital',\n  'vivid',\n  'vocal',\n  'voice',\n  'void',\n  'volcano',\n  'volume',\n  'vote',\n  'voyage',\n  'wage',\n  'wagon',\n  'wait',\n  'walk',\n  'wall',\n  'walnut',\n  'want',\n  'warfare',\n  'warm',\n  'warrior',\n  'wash',\n  'wasp',\n  'waste',\n  'water',\n  'wave',\n  'way',\n  'wealth',\n  'weapon',\n  'wear',\n  'weasel',\n  'weather',\n  'web',\n  'wedding',\n  'weekend',\n  'weird',\n  'welcome',\n  'west',\n  'wet',\n  'whale',\n  'what',\n  'wheat',\n  'wheel',\n  'when',\n  'where',\n  'whip',\n  'whisper',\n  'wide',\n  'width',\n  'wife',\n  'wild',\n  'will',\n  'win',\n  'window',\n  'wine',\n  'wing',\n  'wink',\n  'winner',\n  'winter',\n  'wire',\n  'wisdom',\n  'wise',\n  'wish',\n  'witness',\n  'wolf',\n  'woman',\n  'wonder',\n  'wood',\n  'wool',\n  'word',\n  'work',\n  'world',\n  'worry',\n  'worth',\n  'wrap',\n  'wreck',\n  'wrestle',\n  'wrist',\n  'write',\n  'wrong',\n  'yard',\n  'year',\n  'yellow',\n  'you',\n  'young',\n  'youth',\n  'zebra',\n  'zero',\n  'zone',\n  'zoo',\n];\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/bip39/wordlists/index.ts",
    "content": "export { wordlist as english } from './english';\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/mnemonic.ts",
    "content": "import { english } from './bip39/wordlists';\nimport { genericHash } from './nacl';\n\n// https://crypto.stackexchange.com/a/50759/8245\n// BIP 39 describes the implementation of a mnemonic code or mnemonic sentence\n// -- a group of easy to remember words --\n// for the generation of deterministic wallets.\n//\n// Bitcoin private key is not stored in this way, rather seed to prng\n// which generated the private and public key pair is converted into\n// mnemonic so that its easy for human to type or remember.\n//\n// A list of 2048 words, which is indexed from 0-2047(11 bit information) is used.\n// 132 bit value (128 bit seed + 4 bit checksum) is divided into 12 chunks of 11 bits each,\n// then each 11 bit is used to select a word from dictionary.\n//\n// for more details see BIP39\n//\n// NOTE: 2^11 = 2048\n// NOTE: 2048 = how many words in a bip39 wordlist\n// NOTE: list is range 0 - 2047\nexport async function mnemonicFromNaClBoxPrivateKey(privateKey: Uint8Array): Promise<string> {\n  const uint11Array = toUint11Array(privateKey);\n  const words = applyWords(uint11Array);\n\n  const checksumWord = await computeChecksum(privateKey);\n\n  return words.join(' ') + ' ' + checksumWord;\n}\n\nexport async function naclBoxPrivateKeyFromMnemonic(mnemonic: string): Promise<Uint8Array> {\n  const words = mnemonic.split(' ');\n  const key = words.slice(0, 24);\n  const checksum = words[words.length - 1];\n  const nums = key.map((word) => english.indexOf(word));\n\n  // verify with the checksum, to see if we need to chop off the last\n  // byte or something\n  const fullResult = toUint8Array(nums);\n  const fullCheck = await computeChecksum(fullResult);\n  // because 256bits doesn't divide by 11, we will sometimes\n  // have a stray 0 at the end of the conversion\n  const shortResult = fullResult.slice(0, fullResult.length - 1);\n  const shortCheck = await computeChecksum(shortResult);\n\n  // success!\n  if (shortCheck === fullCheck) return shortResult;\n  if (fullCheck === checksum) return fullResult;\n\n  throw 'Checksum could not validate private key';\n}\n\nexport async function computeChecksum(nums: Uint8Array): Promise<string> {\n  const sum = nums.reduce((acc, v) => acc + v);\n  const arr32 = Uint32Array.from([sum]);\n  const arr8 = new Uint8Array(arr32.buffer, 0, 4);\n\n  const hashBuffer = await genericHash(arr8);\n  const uint11Hash = toUint11Array(hashBuffer);\n  const words = applyWords(uint11Hash);\n\n  return words[0];\n}\n\nfunction applyWords(nums: number[]): string[] {\n  return nums.map((n) => english[n]);\n}\n\n// https://stackoverflow.com/a/50285590/356849\nexport function toUint11Array(input: Uint8Array): number[] {\n  let buffer = 0;\n  let numbits = 0;\n  let output = [];\n\n  for (let i = 0; i < input.length; i++) {\n    // prepend bits to buffer\n    buffer |= input[i] << numbits;\n    numbits += 8;\n\n    // if there are enough bits, extract 11bit chunk\n    if (numbits >= 11) {\n      // 0x7FF is 2047, the max 11 bit number\n      output.push(buffer & 0x7ff);\n      // drop chunk from buffer\n      buffer = buffer >> 11;\n      numbits -= 11;\n    }\n  }\n\n  // also output leftover bits\n  if (numbits != 0) {\n    output.push(buffer & 0x7ff);\n  }\n\n  return output;\n}\n\n// from Uint11Array\nexport function toUint8Array(input: number[]): Uint8Array {\n  let buffer = 0;\n  let numbits = 0;\n  let output: number[] = [];\n\n  for (let i = 0; i < input.length; i++) {\n    // prepend bits to buffer\n    // buffer increments\n    // 11 -> 3 -> 14 -> 6 -> 17 -> 9 -> 1 -> 12 -> 4 -> 15\n    buffer |= input[i] << numbits;\n    numbits += 11;\n\n    // if there are enough bits, extract 8 bit number\n    while (numbits >= 8) {\n      // 0xff is 255\n      output.push(buffer & 0xff);\n\n      // drop chunk from buffer\n      buffer = buffer >> 8;\n      numbits -= 8;\n    }\n  }\n\n  return Uint8Array.from(output);\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/nacl.ts",
    "content": "import { blake2b } from 'blakejs';\nimport nacl from 'tweetnacl';\n\nimport { concat } from './array';\n\nexport async function genericHash(arr: Uint8Array): Promise<Uint8Array> {\n  return blake2b(arr, undefined, 32);\n}\n\nexport async function derivePublicKey(privateKey: Uint8Array) {\n  const keypair = nacl.box.keyPair.fromSecretKey(privateKey);\n\n  return keypair.publicKey;\n}\n\nexport async function derivePublicSigningKey(privateSigningKey: Uint8Array) {\n  const keyPair = nacl.sign.keyPair.fromSecretKey(privateSigningKey);\n\n  return keyPair.publicKey;\n}\n\nexport async function randomBytes(length: number) {\n  return nacl.randomBytes(length);\n}\n\nexport async function sign(message: Uint8Array, senderPrivateKey: Uint8Array): Promise<Uint8Array> {\n  return nacl.sign(message, senderPrivateKey);\n}\n\nexport async function openSigned(\n  signedMessage: Uint8Array,\n  senderPublicKey: Uint8Array\n): Promise<Uint8Array | null> {\n  return nacl.sign.open(signedMessage, senderPublicKey);\n}\n\nexport async function hash(message: Uint8Array): Promise<Uint8Array> {\n  return nacl.hash(message);\n}\n\nexport async function generateNonce() {\n  return nacl.randomBytes(nacl.box.nonceLength);\n}\n\nexport async function generateAsymmetricKeys() {\n  const keyPair = nacl.box.keyPair();\n\n  return {\n    publicKey: keyPair.publicKey,\n    privateKey: keyPair.secretKey,\n  };\n}\n\nexport async function generateSymmetricKeys() {\n  return nacl.randomBytes(nacl.secretbox.keyLength);\n}\n\nexport async function generateSigningKeys() {\n  const keyPair = nacl.sign.keyPair();\n\n  return {\n    publicSigningKey: keyPair.publicKey,\n    privateSigningKey: keyPair.secretKey,\n  };\n}\n\nexport async function encryptFor(\n  message: Uint8Array,\n  recipientPublicKey: Uint8Array,\n  senderPrivateKey: Uint8Array\n): Promise<Uint8Array> {\n  const nonce = await generateNonce();\n\n  const ciphertext = nacl.box(message, nonce, recipientPublicKey, senderPrivateKey);\n\n  return concat(nonce, ciphertext);\n}\n\nexport async function decryptFrom(\n  ciphertextWithNonce: Uint8Array,\n  senderPublicKey: Uint8Array,\n  recipientPrivateKey: Uint8Array\n): Promise<Uint8Array> {\n  const [nonce, ciphertext] = splitNonceFromMessage(ciphertextWithNonce);\n  const decrypted = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientPrivateKey);\n\n  return decrypted as Uint8Array;\n}\n\nexport function splitNonceFromMessage(messageWithNonce: Uint8Array): [Uint8Array, Uint8Array] {\n  const bytes = nacl.box.nonceLength;\n\n  const nonce = messageWithNonce.slice(0, bytes);\n  const message = messageWithNonce.slice(bytes, messageWithNonce.length);\n\n  return [nonce, message];\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/socket.ts",
    "content": "import { decryptFrom, encryptFor } from './nacl';\nimport { fromBase64, fromHex, toBase64, toString, toUint8Array } from './string-encoding';\n\nimport type {\n  EncryptedMessage,\n  KeyPrivate,\n  KeyPublic,\n  Serializable,\n} from '@emberclear/crypto/types';\n\nexport async function encryptForSocket(payload: Serializable, to: KeyPublic, from: KeyPrivate) {\n  const payloadString = JSON.stringify(payload);\n  const payloadBytes = toUint8Array(payloadString);\n\n  const encryptedMessage = await encryptFor(payloadBytes, to.publicKey, from.privateKey);\n\n  return await toBase64(encryptedMessage);\n}\n\nexport async function decryptFromSocket(socketData: EncryptedMessage, privateKey: Uint8Array) {\n  const { uid, message } = socketData;\n  const senderPublicKey = fromHex(uid);\n  const recipientPrivateKey = privateKey;\n\n  const decrypted = await decryptMessage(message, senderPublicKey, recipientPrivateKey);\n\n  return decrypted;\n}\n\nasync function decryptMessage(\n  message: string,\n  senderPublicKey: Uint8Array,\n  recipientPrivateKey: Uint8Array\n) {\n  const messageBytes = await fromBase64(message);\n\n  const decrypted = await decryptFrom(messageBytes, senderPublicKey, recipientPrivateKey);\n\n  // TODO: consider a binary format, instead of\n  //       converting to/from string and json\n  const payload = toString(decrypted);\n  const data = JSON.parse(payload);\n\n  return data;\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon/workers/crypto/utils/string-encoding.ts",
    "content": "import utils from 'tweetnacl-util';\n\nexport function toHex(array: Uint8Array): string {\n  return Array.from(array)\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('');\n}\n\nexport function fromHex(hex: string): Uint8Array {\n  if (Math.ceil(hex.length / 2) !== hex.length / 2) {\n    throw new Error('hex string is the wrong length');\n  }\n\n  const matches = hex.match(/.{1,2}/g) || [];\n\n  return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));\n}\n\nexport async function toBase64(array: Uint8Array): Promise<string> {\n  return utils.encodeBase64(array);\n}\n\nexport async function fromBase64(base64: string): Promise<Uint8Array> {\n  return utils.decodeBase64(base64);\n}\n\nexport function fromString(str: string): Uint8Array {\n  return utils.decodeUTF8(str);\n}\n\nexport const toUint8Array = fromString;\n\nexport function toString(uint8Array: Uint8Array): string {\n  return utils.encodeUTF8(uint8Array);\n}\n\nexport function ensureUint8Array(text: string | Uint8Array): Uint8Array {\n  if (text.constructor === Uint8Array) {\n    return text as Uint8Array;\n  }\n\n  return fromString(text as string);\n}\n\n// http://stackoverflow.com/a/39460727\nexport function base64ToHex(base64: string): string | undefined {\n  if (base64 === undefined) return undefined;\n\n  // convert to binary, than to hex\n  const raw = atob(base64);\n  let hex = '';\n\n  for (let i = 0; i < raw.length; i++) {\n    const hexChar = raw.charCodeAt(i).toString(16);\n\n    hex += hexChar.length === 2 ? hexChar : `0${hexChar}`;\n  }\n\n  return hex.toUpperCase();\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon-test-support/index.ts",
    "content": "import { getContext } from '@ember/test-helpers';\n\nimport { CryptoConnector } from '@emberclear/crypto';\nimport { toHex } from '@emberclear/crypto/workers/crypto/utils/string-encoding';\n\nimport type { WorkersService } from '@emberclear/crypto';\nimport type { TestContext } from 'ember-test-helpers';\n\nexport { setupWorkers } from './setup';\n\n// no one use this!\n// prettier-ignore\nexport const samplePrivateKey = Uint8Array.from([\n  43, 191, 106, 38, 141, 42, 151, 128,\n  227, 93, 124, 214, 166, 222, 144, 176,\n  162, 181, 203, 27, 39, 18, 37, 173,\n  2, 189, 139, 8, 181, 8, 171, 45\n]);\n\nexport async function newCrypto() {\n  const workers = (getContext() as TestContext).owner.lookup('service:workers') as WorkersService;\n  const crypto = new CryptoConnector({ workerService: workers });\n\n  let { publicKey, privateKey } = await crypto.generateKeys();\n\n  return {\n    publicKey,\n    privateKey,\n    hex: {\n      publicKey: toHex(publicKey),\n      privateKey: toHex(privateKey),\n    },\n    crypto,\n  };\n}\n"
  },
  {
    "path": "client/web/addons/crypto/addon-test-support/setup.ts",
    "content": "import WorkersService, { CRYPTO_PATH } from '@emberclear/crypto/services/workers';\nimport { handleMessage } from '@emberclear/crypto/workers/crypto/messages';\n\nimport type { TestContext } from 'ember-test-helpers';\n\n/**\n *\n * in a test environment, we can't assume a stable connection\n * to the internet.\n *\n * even though the test index.html and web workers live on the same\n * host, the tests freak out when the internet connection is interrupted.\n */\nexport function setupWorkers(hooks: NestedHooks) {\n  hooks.beforeEach(function (this: TestContext) {\n    class WorkersProxy extends WorkersService {\n      getWorker(path: string) {\n        switch (path) {\n          case CRYPTO_PATH:\n            return fakeCrypto;\n          default:\n            throw new Error(`No worker proxy exists for worker: ${path}`);\n        }\n      }\n    }\n\n    this.owner.register('service:workers', WorkersProxy);\n  });\n}\n\nconst fakeCrypto = {\n  postMessage: handleMessage,\n};\n"
  },
  {
    "path": "client/web/addons/crypto/addon-test-support/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/test-support\",\n    \"paths\": {\n      \"@emberclear/crypto\": [\"../declarations\"],\n      \"@emberclear/crypto/*\": [\"../declarations/*\"],\n      \"@emberclear/crypto/test-support\": [\".\"],\n      \"@emberclear/crypto/test-support/*\": [\"./*\"]\n    }\n  },\n  \"references\": [\n    { \"path\": \"../addon\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/app/services/workers.ts",
    "content": "export { default } from '@emberclear/crypto/services/workers';\n"
  },
  {
    "path": "client/web/addons/crypto/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n      {\n        name: 'ember-default-with-jquery',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'jquery-integration': true,\n          }),\n        },\n        npm: {\n          devDependencies: {\n            '@ember/jquery': '^1.1.0',\n          },\n        },\n      },\n      {\n        name: 'ember-classic',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'application-template-wrapper': true,\n            'default-async-observers': false,\n            'template-only-glimmer-components': false,\n          }),\n        },\n        npm: {\n          ember: {\n            edition: 'classic',\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/crypto/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/crypto/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    // Add options here\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/crypto/index.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst os = require('os');\nconst fs = require('fs');\n\nconst Funnel = require('broccoli-funnel');\n\nconst { buildWorkers } = require('./lib/worker-build');\n\nmodule.exports = {\n  name: require('./package').name,\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n\n  // override\n  treeForPublic() {\n    let buildDir = fs.mkdtempSync(path.join(os.tmpdir(), '@emberclear--crypto--'));\n\n    let options = {\n      isProduction: true,\n      buildDir,\n    };\n\n    // outputs {buildDir}/crypto.js\n    buildWorkers(options);\n\n    return new Funnel(buildDir, {\n      destDir: 'workers/',\n    });\n  },\n};\n"
  },
  {
    "path": "client/web/addons/crypto/lib/worker-build.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst fs = require('fs');\nconst esbuild = require('esbuild');\n\nconst addonFolder = path.join(__dirname, '..', 'addon');\nconst workerRoot = path.join(addonFolder, 'workers');\n\nfunction detectWorkers() {\n  let workers = {};\n  let dir = fs.readdirSync(workerRoot);\n\n  for (let i = 0; i < dir.length; i++) {\n    let name = dir[i];\n\n    workers[name] = path.join(workerRoot, name, 'index.ts');\n  }\n\n  return workers;\n}\n\nfunction configureWorkerTree({ isProduction, buildDir }) {\n  return ([name, entryPath]) => {\n    esbuild.buildSync({\n      loader: { '.ts': 'ts' },\n      entryPoints: [entryPath],\n      bundle: true,\n      outfile: path.join(buildDir, `${name}.js`),\n      format: 'esm',\n      minify: isProduction,\n      sourcemap: !isProduction,\n      // incremental: true,\n      tsconfig: path.join(addonFolder, 'tsconfig.json'),\n    });\n  };\n}\n\nfunction buildWorkers(env) {\n  let inputs = detectWorkers();\n  let workerBuilder = configureWorkerTree(env);\n\n  // separate build from ember, will be detached, won't watch\n  Object.entries(inputs).map(workerBuilder);\n}\n\nmodule.exports = { buildWorkers };\n"
  },
  {
    "path": "client/web/addons/crypto/package.json",
    "content": "{\n  \"name\": \"@emberclear/crypto\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Crypto workers for emberclear-related projects\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/crypto\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"blakejs\": \"1.1.0\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"esbuild\": \"0.11.23\",\n    \"promise-worker-bi\": \"4.0.2\",\n    \"tweetnacl\": \"1.0.3\",\n    \"tweetnacl-util\": \"0.15.1\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/crypto/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.21.2\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/crypto/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/crypto/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/crypto/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/crypto/tests/test-helper.ts",
    "content": "import { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/crypto/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"@emberclear/crypto\": [\"../declarations\"],\n      \"@emberclear/crypto/*\": [\"../declarations/*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\n    \".\",\n    \"../types\"\n  ],\n  \"references\": [\n    { \"path\": \"../addon\" },\n    { \"path\": \"../addon-test-support\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/tests/unit/services/workers-test.js",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Service | workers', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = this.owner.lookup('service:workers');\n\n    assert.ok(service);\n  });\n});\n"
  },
  {
    "path": "client/web/addons/crypto/tests/workers/crypto/mnemonic-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { samplePrivateKey } from '@emberclear/crypto/test-support';\nimport {\n  mnemonicFromNaClBoxPrivateKey,\n  naclBoxPrivateKeyFromMnemonic,\n  toUint8Array,\n  toUint11Array,\n} from '@emberclear/crypto/workers/crypto/utils/mnemonic';\n\nmodule('Workers | Crypto | mnemonic', function () {\n  const numbers = {\n    ['32']: new Uint8Array([0x20]),\n    ['64']: new Uint8Array([0x40]),\n    ['2048']: new Uint8Array([0x8, 0]),\n    ['4096']: new Uint8Array([0x10, 0]),\n    ['7331']: new Uint8Array([0xa3, 0x1c]),\n  };\n\n  test('mnemonicFromNaClBoxPrivateKey | converts a private key to english', async function (assert) {\n    const result = await mnemonicFromNaClBoxPrivateKey(samplePrivateKey);\n\n    // prettier-ignore\n    const expected = `\n      tornado priority nasty potato comic\n      then upper labor suspect kind\n      embody climb hero very decide\n      banana pigeon apple teach master\n      head season hood ability fossil\n    `.replace(/[ \\n\\r]+/g, ' ').trim();\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('key can be converted and recovered', async function (assert) {\n    const mnemonic = await mnemonicFromNaClBoxPrivateKey(samplePrivateKey);\n    const result = await naclBoxPrivateKeyFromMnemonic(mnemonic);\n\n    assert.deepEqual(result, samplePrivateKey);\n  });\n\n  test('toUint11Array | converts | 32 (8 bits)', function (assert) {\n    const result = toUint11Array(numbers['32']);\n    const expected = [32];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint11Array | converts | 2048 (12 bits)', function (assert) {\n    const result = toUint11Array(numbers['2048']);\n    const expected = [8, 0];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint11Array | converts | 4096 (13 bits)', function (assert) {\n    const result = toUint11Array(numbers['4096']);\n    const expected = [16, 0];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint11Array | converts | 7331 (13 bits)', function (assert) {\n    const result = toUint11Array(numbers['7331']);\n    const expected = [1187, 3];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint11Array | converts | private key', function (assert) {\n    const result = toUint11Array(samplePrivateKey);\n\n    // prettier-ignore\n    const expected = [\n      1835, 1367, 1177, 1350, 370,\n      1793, 1912, 994, 1750, 980,\n      579, 344, 858, 1943, 454,\n      145, 1317, 85, 1780, 1093,\n      848, 1553, 874, 1\n    ];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint8Array | converts | 32', function (assert) {\n    const result = toUint8Array([32]);\n    const expected = numbers['32'];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint8Array | converts | 2048', function (assert) {\n    const result = toUint8Array([8, 0]);\n    const expected = numbers['2048'];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint8Array | converts | 4096', function (assert) {\n    const result = toUint8Array([16, 0]);\n    const expected = numbers['4096'];\n\n    assert.deepEqual(result, expected);\n  });\n\n  test('toUint8Array | converts | 7331', function (assert) {\n    const result = toUint8Array([1187, 3]);\n    const expected = numbers['7331'];\n\n    assert.deepEqual(result, expected);\n  });\n});\n"
  },
  {
    "path": "client/web/addons/crypto/tests/workers/crypto/nacl-test.ts",
    "content": "import { module, skip, test } from 'qunit';\n\nimport * as nacl from '@emberclear/crypto/workers/crypto/utils/nacl';\n\nmodule('Workers | Crypto | nacl', function () {\n  skip('libsodium uses wasm', async function (assert) {\n    assert.expect(0);\n    // not using libsodium atm. WASM support seems unstable\n    // (or libsodium is unstable between updates)\n    // const sodium = await nacl.libsodium();\n    // const isUsingWasm = (sodium as any).libsodium.usingWasm;\n\n    // assert.ok(isUsingWasm);\n  });\n\n  test('generateAsymmetricKeys | works', async function (assert) {\n    const boxKeys = await nacl.generateAsymmetricKeys();\n\n    assert.ok(boxKeys.publicKey);\n    assert.ok(boxKeys.privateKey);\n  });\n\n  test('generateSigningKeys | works', async function (assert) {\n    const signingKeys = await nacl.generateSigningKeys();\n\n    assert.ok(signingKeys.publicSigningKey);\n    assert.ok(signingKeys.privateSigningKey);\n  });\n\n  test('encryptFor/decryptFrom | works with Uint8Array', async function (assert) {\n    const receiver = await nacl.generateAsymmetricKeys();\n    const sender = await nacl.generateAsymmetricKeys();\n\n    const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello\n    const ciphertext = await nacl.encryptFor(msgAsUint8, receiver.publicKey, sender.privateKey);\n    const decrypted = await nacl.decryptFrom(ciphertext, sender.publicKey, receiver.privateKey);\n\n    assert.deepEqual(msgAsUint8, decrypted);\n  });\n\n  test('encryptFor/decryptFrom | works with large data', async function (assert) {\n    const receiver = await nacl.generateAsymmetricKeys();\n    const sender = await nacl.generateAsymmetricKeys();\n\n    let bigMsg: number[] = [];\n\n    for (let i = 0; i < 128; i++) {\n      bigMsg = bigMsg.concat([104, 101, 108, 108, 111]);\n    }\n\n    const msgAsUint8 = Uint8Array.from(bigMsg); // hello * 128 = 640 Bytes\n    const ciphertext = await nacl.encryptFor(msgAsUint8, receiver.publicKey, sender.privateKey);\n    const decrypted = await nacl.decryptFrom(ciphertext, sender.publicKey, receiver.privateKey);\n\n    assert.deepEqual(msgAsUint8, decrypted);\n  });\n\n  test('sign/open | works with Uint8Array', async function (assert) {\n    const sender = await nacl.generateSigningKeys();\n\n    const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello\n    const signedText = await nacl.sign(msgAsUint8, sender.privateSigningKey);\n    const openedText = await nacl.openSigned(signedText, sender.publicSigningKey);\n\n    assert.deepEqual(msgAsUint8, openedText);\n  });\n\n  test('sign/open | works with large data', async function (assert) {\n    const sender = await nacl.generateSigningKeys();\n\n    let bigMsg: number[] = [];\n\n    for (let i = 0; i < 128; i++) {\n      bigMsg = bigMsg.concat([104, 101, 108, 108, 111]);\n    }\n\n    const msgAsUint8 = Uint8Array.from(bigMsg); // hello * 128 = 640 Bytes\n    const signedText = await nacl.sign(msgAsUint8, sender.privateSigningKey);\n    const openedText = await nacl.openSigned(signedText, sender.publicSigningKey);\n\n    assert.deepEqual(msgAsUint8, openedText);\n  });\n\n  test('splitNonceFromMessage | separates the nonce', async function (assert) {\n    // prettier-ignore\n    const msg = [\n      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24\n    ];\n\n    const messageWithNonce = Uint8Array.from([...msg, 25]);\n\n    const [nonce, notTheNonce] = await nacl.splitNonceFromMessage(messageWithNonce);\n\n    assert.deepEqual(nonce, Uint8Array.from(msg));\n    assert.deepEqual(notTheNonce, Uint8Array.from([25]));\n  });\n});\n"
  },
  {
    "path": "client/web/addons/crypto/tests/workers/crypto/string-encoding-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport * as stringEncoding from '@emberclear/crypto/workers/crypto/utils/string-encoding';\n\nmodule('Workers | Crypto | String Encoding', function () {\n  module('toString / fromString', function () {\n    test('converts a sting to and back from uint8', function (assert) {\n      const str = 'hello there';\n\n      const uint8Array = stringEncoding.fromString(str);\n      const original = stringEncoding.toString(uint8Array);\n\n      assert.equal(original, str);\n    });\n  });\n\n  module('toBase64 / fromBase64', function () {\n    test('converts uint8array and back', async function (assert) {\n      const msgAsUint8 = new Uint8Array([0, 1, 2, 3, 4]);\n\n      const base64 = await stringEncoding.toBase64(msgAsUint8);\n      const result = await stringEncoding.fromBase64(base64);\n\n      assert.deepEqual(result, msgAsUint8);\n    });\n  });\n\n  module('toHex / fromHex', function () {\n    test('converts uint8array and back', function (assert) {\n      const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello\n\n      const hex = stringEncoding.toHex(msgAsUint8);\n      const original = stringEncoding.fromHex(hex);\n\n      assert.deepEqual(original, msgAsUint8);\n    });\n  });\n\n  module('toString / fromString', function () {\n    test('converts to string and back', function (assert) {\n      const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello\n\n      const str = stringEncoding.toString(msgAsUint8);\n      const original = stringEncoding.fromString(str);\n\n      assert.deepEqual(original, msgAsUint8);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/addons/crypto/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/crypto/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"exclude\": [\"declarations\"],\n  \"references\": [\n    { \"path\": \"addon\" },\n    { \"path\": \"addon-test-support\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/crypto/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n"
  },
  {
    "path": "client/web/addons/crypto/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/encoding/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/encoding/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/encoding/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/encoding/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/encoding/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/encoding/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/encoding/.travis.yml",
    "content": "---\nlanguage: node_js\nnode_js:\n  # we recommend testing addons with the same minimum supported node version as Ember CLI\n  # so that your addon works for all apps\n  - \"10\"\n\ndist: xenial\n\naddons:\n  chrome: stable\n\ncache:\n  yarn: true\n\nenv:\n  global:\n    # See https://git.io/vdao3 for details.\n    - JOBS=1\n\nbranches:\n  only:\n    - master\n    # npm version tags\n    - /^v\\d+\\.\\d+\\.\\d+/\n\njobs:\n  fast_finish: true\n  allow_failures:\n    - env: EMBER_TRY_SCENARIO=ember-canary\n\n  include:\n    # runs linting and tests with current locked deps\n    - stage: \"Tests\"\n      name: \"Tests\"\n      script:\n        - yarn lint\n        - yarn test:ember\n\n    - stage: \"Additional Tests\"\n      name: \"Floating Dependencies\"\n      install:\n        - yarn install --no-lockfile --non-interactive\n      script:\n        - yarn test:ember\n\n    # we recommend new addons test the current and previous LTS\n    # as well as latest stable release (bonus points to beta/canary)\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.16\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.20\n    - env: EMBER_TRY_SCENARIO=ember-release\n    - env: EMBER_TRY_SCENARIO=ember-beta\n    - env: EMBER_TRY_SCENARIO=ember-canary\n    - env: EMBER_TRY_SCENARIO=ember-default-with-jquery\n    - env: EMBER_TRY_SCENARIO=ember-classic\n\nbefore_install:\n  - curl -o- -L https://yarnpkg.com/install.sh | bash\n  - export PATH=$HOME/.yarn/bin:$PATH\n\nscript:\n  - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO\n"
  },
  {
    "path": "client/web/addons/encoding/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/encoding/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd encoding`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/encoding/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/encoding/README.md",
    "content": "encoding\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install encoding\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/encoding/addon/string.ts",
    "content": "import QRCode from 'qrcode';\n\nexport function toHex(array: Uint8Array): string {\n  return Array.from(array)\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('');\n}\n\nexport function fromHex(hex: string): Uint8Array {\n  if (Math.ceil(hex.length / 2) !== hex.length / 2) {\n    throw new Error('hex string is the wrong length');\n  }\n\n  const matches = hex.match(/.{1,2}/g) || [];\n\n  return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));\n}\n\ntype Serializable =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | Date\n  | Serializable[]\n  | { [key: string]: Serializable };\n\nexport async function convertObjectToQRCodeDataURL<T extends Record<string, unknown>>(\n  object: T\n): Promise<string> {\n  const str = JSON.stringify(object);\n\n  return await QRCode.toDataURL(str);\n}\n\nexport function convertObjectToUint8Array<T>(object: T): Uint8Array {\n  const str = JSON.stringify(object);\n\n  return new TextEncoder().encode(str);\n}\n\nexport function convertUint8ArrayToObject<T>(array: Uint8Array): T {\n  const str = new TextDecoder().decode(array);\n\n  return JSON.parse(str);\n}\n\nexport function convertObjectToBase64String(object: Serializable): string {\n  const json = JSON.stringify(object);\n  const base64 = btoa(json);\n\n  return base64;\n}\n\nexport function convertBase64StringToObject(base64: string): Serializable {\n  const json = atob(base64);\n  const obj = JSON.parse(json);\n\n  return obj;\n}\n\nexport function objectToDataURL(obj: Serializable): string {\n  const str = JSON.stringify(obj);\n\n  return `data:text/json;charset=utf-8,${encodeURIComponent(str)}`;\n}\n\n// http://stackoverflow.com/a/39460727\nexport function base64ToHex(base64: string): string | undefined {\n  if (base64 === undefined) return undefined;\n\n  // convert to binary, than to hex\n  const raw = atob(base64);\n  let hex = '';\n\n  for (let i = 0; i < raw.length; i++) {\n    const hexChar = raw.charCodeAt(i).toString(16);\n\n    hex += hexChar.length === 2 ? hexChar : `0${hexChar}`;\n  }\n\n  return hex.toUpperCase();\n}\n"
  },
  {
    "path": "client/web/addons/encoding/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/encoding\": [\".\"],\n      \"@emberclear/encoding/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../../libraries/questionably-typed\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/encoding/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n      {\n        name: 'ember-default-with-jquery',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'jquery-integration': true,\n          }),\n        },\n        npm: {\n          devDependencies: {\n            '@ember/jquery': '^1.1.0',\n          },\n        },\n      },\n      {\n        name: 'ember-classic',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'application-template-wrapper': true,\n            'default-async-observers': false,\n            'template-only-glimmer-components': false,\n          }),\n        },\n        npm: {\n          ember: {\n            edition: 'classic',\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/encoding/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/encoding/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    // Add options here\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/encoding/index.js",
    "content": "'use strict';\n\nmodule.exports = {\n  name: require('./package').name,\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n};\n"
  },
  {
    "path": "client/web/addons/encoding/package.json",
    "content": "{\n  \"name\": \"@emberclear/encoding\",\n  \"version\": \"0.0.0\",\n  \"description\": \"The default blueprint for ember-cli addons.\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/crypto\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"@emberclear/config\": \"*\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"ember-auto-import\": \"^1.11.3\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"qrcode\": \"1.4.4\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qrcode\": \"1.4.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/encoding/testem.js",
    "content": "'use strict';\n\nmodule.exports = {\n  test_page: 'tests/index.html?hidepassed',\n  disable_watching: true,\n  launch_in_ci: ['Chrome'],\n  launch_in_dev: ['Chrome'],\n  browser_start_timeout: 120,\n  browser_args: {\n    Chrome: {\n      ci: [\n        // --no-sandbox is needed when running Chrome inside a container\n        process.env.CI ? '--no-sandbox' : null,\n        '--headless',\n        '--disable-dev-shm-usage',\n        '--disable-software-rasterizer',\n        '--mute-audio',\n        '--remote-debugging-port=0',\n        '--window-size=1440,900',\n      ].filter(Boolean),\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.23.0\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/encoding/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/encoding/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/encoding/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/test-helper.js",
    "content": "import { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/encoding/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"@emberclear/encoding\": [\"../declarations\"],\n      \"@emberclear/encoding/*\": [\"../declarations/*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\n    \".\",\n    \"../types\"\n  ],\n  \"references\": [\n    { \"path\": \"../addon\" }\n    /* { \"path\": \"../addon-test-support\" } */\n  ]\n}\n\n"
  },
  {
    "path": "client/web/addons/encoding/tests/unit/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/encoding/tests/unit/string-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport * as stringEncoding from '@emberclear/encoding/string';\n\nmodule('Unit | Utility | String Encoding', function () {\n  module('toHex / fromHex', function () {\n    test('converts uint8array and back', function (assert) {\n      const msgAsUint8 = Uint8Array.from([104, 101, 108, 108, 111]); // hello\n\n      const hex = stringEncoding.toHex(msgAsUint8);\n      const original = stringEncoding.fromHex(hex);\n\n      assert.deepEqual(original, msgAsUint8);\n    });\n  });\n\n  module('to/from base64 string / object', function () {\n    test('converts to base64 and back', function (assert) {\n      const obj = { hi: 'there' };\n\n      const base64 = stringEncoding.convertObjectToBase64String(obj);\n      const original = stringEncoding.convertBase64StringToObject(base64);\n\n      assert.deepEqual(original, obj);\n    });\n  });\n\n  module('base64ToHex', function () {\n    test('converts', function (assert) {\n      const base64 = 'aGVsbG8gdGhlcmU='; // hello there\n      const expected = '68656C6C6F207468657265';\n      const result = stringEncoding.base64ToHex(base64);\n\n      assert.equal(result, expected);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/addons/encoding/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/encoding/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"addon\" },\n    /* { \"path\": \"addon-test-support\" }, */\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/encoding/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/local-account/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/local-account/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/local-account/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/local-account/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/local-account/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/local-account/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/local-account/.travis.yml",
    "content": "---\nlanguage: node_js\nnode_js:\n  # we recommend testing addons with the same minimum supported node version as Ember CLI\n  # so that your addon works for all apps\n  - \"10\"\n\ndist: xenial\n\naddons:\n  chrome: stable\n\ncache:\n  yarn: true\n\nenv:\n  global:\n    # See https://git.io/vdao3 for details.\n    - JOBS=1\n\nbranches:\n  only:\n    - master\n    # npm version tags\n    - /^v\\d+\\.\\d+\\.\\d+/\n\njobs:\n  fast_finish: true\n  allow_failures:\n    - env: EMBER_TRY_SCENARIO=ember-canary\n\n  include:\n    # runs linting and tests with current locked deps\n    - stage: \"Tests\"\n      name: \"Tests\"\n      script:\n        - yarn lint\n        - yarn test:ember\n\n    - stage: \"Additional Tests\"\n      name: \"Floating Dependencies\"\n      install:\n        - yarn install --no-lockfile --non-interactive\n      script:\n        - yarn test:ember\n\n    # we recommend new addons test the current and previous LTS\n    # as well as latest stable release (bonus points to beta/canary)\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.16\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.20\n    - env: EMBER_TRY_SCENARIO=ember-release\n    - env: EMBER_TRY_SCENARIO=ember-beta\n    - env: EMBER_TRY_SCENARIO=ember-canary\n    - env: EMBER_TRY_SCENARIO=ember-default-with-jquery\n    - env: EMBER_TRY_SCENARIO=ember-classic\n\nbefore_install:\n  - curl -o- -L https://yarnpkg.com/install.sh | bash\n  - export PATH=$HOME/.yarn/bin:$PATH\n\nscript:\n  - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO\n"
  },
  {
    "path": "client/web/addons/local-account/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd local-account`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/local-account/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/local-account/README.md",
    "content": "local-account\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install local-account\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/local-account/addon/adapters/application.js",
    "content": "/* eslint-disable */\n// import LFAdapter from 'ember-localforage-adapter/adapters/localforage';\nimport EmberObject from '@ember/object';\nimport { reject, resolve, Promise as EmberPromise } from 'rsvp';\nimport Evented from '@ember/object/evented';\n// import LFQueue from 'ember-localforage-adapter/utils/queue';\n// import LFCache from 'ember-localforage-adapter/utils/cache';\nimport { v4 as uuid } from 'uuid';\nimport localforage from 'localforage';\n\nconst LFQueue = EmberObject.extend({\n  init: function () {\n    this.queue = [resolve()];\n  },\n\n  attach(callback) {\n    const queueKey = this.queue.length;\n\n    this.queue[queueKey] = new EmberPromise((resolve, reject) => {\n      this.queue[queueKey - 1].then(() => {\n        this.queue.splice(queueKey - 1, 1);\n        callback(resolve, reject);\n      });\n    });\n\n    return this.queue[queueKey];\n  },\n});\n\nconst LFCache = EmberObject.extend({\n  init: function () {\n    this.data = new Map();\n  },\n\n  clear() {\n    this.data.clear();\n  },\n\n  get(namespace) {\n    const data = this.data.get(namespace);\n\n    if (!data) {\n      return null;\n    }\n\n    return data;\n  },\n\n  set(namespace, objects) {\n    this.data.set(namespace, objects);\n  },\n\n  replace(data) {\n    this.clear();\n\n    for (let index of Object.keys(data)) {\n      this.set(index, data[index]);\n    }\n  },\n});\n\nconst LFAdapter = EmberObject.extend(Evented, {\n  queue: LFQueue.create(),\n  cache: LFCache.create(),\n  caching: 'model',\n\n  coalesceFindRequests: true,\n\n  groupRecordsForFindMany(store, snapshots) {\n    return [snapshots];\n  },\n\n  shouldBackgroundReloadRecord() {\n    return false;\n  },\n\n  shouldReloadAll() {\n    return true;\n  },\n\n  /**\n   * This is the main entry point into finding records. The first parameter to\n   * this method is the model's name as a string.\n   *\n   * @method findRecord\n   * @param store\n   * @param {DS.Model} type\n   * @param {Object|String|Integer|null} id\n   */\n  findRecord(store, type, id) {\n    return this._getNamespaceData(type).then((namespaceData) => {\n      const record = namespaceData.records[id];\n\n      if (!record) {\n        return reject();\n      }\n\n      return record;\n    });\n  },\n\n  findAll(store, type) {\n    return this._getNamespaceData(type).then((namespaceData) => {\n      const records = [];\n\n      for (let id in namespaceData.records) {\n        records.push(namespaceData.records[id]);\n      }\n\n      return { data: records.map((record) => record.data) };\n    });\n  },\n\n  findMany(store, type, ids) {\n    return this._getNamespaceData(type).then((namespaceData) => {\n      const records = [];\n\n      for (let i = 0; i < ids.length; i++) {\n        const record = namespaceData.records[ids[i]];\n\n        if (record) {\n          records.push(record);\n        }\n      }\n\n      return records;\n    });\n  },\n\n  queryRecord(store, type, query) {\n    return this._getNamespaceData(type).then((namespaceData) => {\n      const record = this._query(namespaceData.records, query, true);\n\n      if (!record) {\n        return reject();\n      }\n\n      return record;\n    });\n  },\n\n  /**\n   *  Supports queries that look like this:\n   *   {\n   *     <property to query>: <value or regex (for strings) to match>,\n   *     ...\n   *   }\n   *\n   * Every property added to the query is an \"AND\" query, not \"OR\"\n   *\n   * Example:\n   * match records with \"complete: true\" and the name \"foo\" or \"bar\"\n   *  { complete: true, name: /foo|bar/ }\n   */\n  query(store, type, query) {\n    return this._getNamespaceData(type).then((namespaceData) => {\n      let records = this._query(namespaceData.records, query);\n      let result = { data: records.map((record) => record.data) };\n\n      return result;\n    });\n  },\n\n  _query(records, query, singleMatch) {\n    const results = singleMatch ? null : [];\n\n    for (let id in records) {\n      const record = records[id];\n      const attributes = record.data.attributes;\n      let isMatching = false;\n\n      for (let property in query) {\n        const queryValue = query[property];\n\n        if (queryValue instanceof RegExp) {\n          isMatching = queryValue.test(attributes[property]);\n        } else {\n          isMatching = attributes[property] === queryValue;\n        }\n\n        if (!isMatching) {\n          break; // all criteria should pass\n        }\n      }\n\n      if (isMatching) {\n        if (singleMatch) {\n          return record;\n        }\n\n        results.push(record);\n      }\n    }\n\n    return results;\n  },\n\n  createRecord: updateOrCreate,\n\n  updateRecord: updateOrCreate,\n\n  deleteRecord(store, type, snapshot) {\n    return this.queue.attach((resolve) => {\n      this._getNamespaceData(type).then((namespaceData) => {\n        delete namespaceData.records[snapshot.id];\n\n        this._setNamespaceData(type, namespaceData).then(() => {\n          resolve();\n        });\n      });\n    });\n  },\n\n  generateIdForRecord() {\n    return uuid();\n  },\n\n  // private\n\n  _setNamespaceData(type, namespaceData) {\n    const modelNamespace = this._modelNamespace(type);\n\n    return this._loadData().then((storage) => {\n      if (this.caching !== 'none') {\n        this.cache.set(modelNamespace, namespaceData);\n      }\n\n      storage[modelNamespace] = namespaceData;\n\n      return localforage.setItem(this._adapterNamespace(), storage);\n    });\n  },\n\n  _getNamespaceData(type) {\n    const modelNamespace = this._modelNamespace(type);\n\n    if (this.caching !== 'none') {\n      const cache = this.cache.get(modelNamespace); // eslint-disable-line\n\n      if (cache) {\n        return resolve(cache);\n      }\n    }\n\n    return this._loadData().then((storage) => {\n      const namespaceData = storage?.[modelNamespace] || { records: {} };\n\n      if (this.caching === 'model') {\n        this.cache.set(modelNamespace, namespaceData);\n      } else if (this.caching === 'all') {\n        if (storage) {\n          this.cache.replace(storage);\n        }\n      }\n\n      return namespaceData;\n    });\n  },\n\n  _loadData() {\n    return localforage.getItem(this._adapterNamespace()).then((storage) => {\n      return storage ? storage : {};\n    });\n  },\n\n  _modelNamespace(type) {\n    return type.url || type.modelName;\n  },\n\n  _adapterNamespace() {\n    return this.namespace || 'DS.LFAdapter';\n  },\n});\n\nfunction updateOrCreate(store, type, snapshot) {\n  return this.queue.attach((resolve) => {\n    this._getNamespaceData(type).then((namespaceData) => {\n      const serializer = store.serializerFor(type.modelName);\n      const recordHash = serializer.serialize(snapshot, { includeId: true });\n      // update(id comes from snapshot) or create(id comes from serialization)\n      const id = snapshot.id || recordHash.id;\n\n      namespaceData.records[id] = recordHash;\n\n      this._setNamespaceData(type, namespaceData).then(() => {\n        resolve();\n      });\n    });\n  });\n}\nexport default LFAdapter.extend({\n  // do not change the namespace, as it would log everyone out\n  // need a migration path if the namespace is going to change\n  // namespace: 'emberclear',\n  caching: 'none',\n\n  shouldBackgroundReloadRecord() {\n    return true;\n  },\n\n  shouldBackgroundReloadAll() {\n    return true;\n  },\n});\n"
  },
  {
    "path": "client/web/addons/local-account/addon/index.ts",
    "content": "export { default as Channel } from './models/channel';\nexport { default as Contact, Status } from './models/contact';\nexport { default as Identity } from './models/identity';\nexport { default as User } from './models/user';\nexport { currentUserId, default as CurrentUserService } from './services/current-user';\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/README.md",
    "content": "# TODO\n\n## Channels\n\nThe data structures needs to be reworked -- there are too many models and the\nsplit doesn't provide much value as all the data is very tightly coupled and\nit's a lot of 1:1 relationships\n\nFor example, there are 7 relationships on identity _just_ for channels.\nDoes it really need to exist on `Identity`? We don't super care what\nother users' channels are -- we can't actually know that for certain anyway.\n\n_Proposal_:\n - Move channel related things to the `User`.\n - Manage members on the `Channel`.\n - Manage votes on the `Channel`.\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/channel-context-chain.ts",
    "content": "import Model, { belongsTo, hasMany } from '@ember-data/model';\n\nimport type Channel from './channel';\nimport type Identity from './identity';\nimport type VoteChain from './vote-chain';\n\n// TODO: CLEAN THIS UP\n//       SEE README\nexport default class ChannelContextChain extends Model {\n  @belongsTo('identity', { async: false, inverse: 'adminOf' }) admin!: Identity;\n  @hasMany('identity', { async: false, inverse: 'memberOf' }) members!: Identity[];\n  @belongsTo('vote-chain', { async: false }) supportingVote!: VoteChain;\n  @belongsTo('channel-context-chain', { async: false, inverse: 'parentChain' })\n  previousChain!: ChannelContextChain;\n\n  // Unused, but necessary to properly set up relationships, therefore async\n  @belongsTo('channel-context-chain', { async: true, inverse: 'previousChain' })\n  parentChain!: ChannelContextChain;\n  @belongsTo('channel', { async: true }) composesChannel!: Channel;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    'channel-context-chain': ChannelContextChain;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/channel.ts",
    "content": "import Model, { attr, belongsTo, hasMany } from '@ember-data/model';\n\nimport type ChannelContextChain from './channel-context-chain';\nimport type Vote from './vote';\n\n// TODO: CLEAN THIS UP\n//       SEE README\nexport default class Channel extends Model {\n  @attr() name!: string;\n\n  // Optional super-private channel.\n  // provides an additional layer of encryption\n  // to protect from other people you trust, but\n  // maybe don't trust *that* much.\n  // TODO: implement this.\n  // @attr() public protected!: boolean;\n  // @attr() decryptionKey!: string;\n\n  @hasMany('vote', { async: false }) activeVotes!: Vote[];\n  @belongsTo('channel-context-chain', { async: false }) contextChain!: ChannelContextChain;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    channel: Channel;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/contact.ts",
    "content": "import { attr } from '@ember-data/model';\n\nimport Identity from './identity';\n\nexport const Status = {\n  ONLINE: 'online',\n  OFFLINE: 'offline',\n  AWAY: 'away',\n  BUSY: 'busy',\n} as const;\n\ntype StatusKeys = keyof typeof Status;\ntype STATUS = typeof Status[StatusKeys];\n\nexport default class Contact extends Identity {\n  @attr() onlineStatus?: STATUS;\n  @attr() isPinned?: boolean;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    contact: Contact;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/identity.ts",
    "content": "import { tracked } from '@glimmer/tracking';\n// see note below -- needs investigating\n// eslint-disable-next-line\nimport { computed } from '@ember/object';\nimport Model, { attr, hasMany } from '@ember-data/model';\n\nimport { toHex } from '@emberclear/encoding/string';\n\nimport type ChannelContextChain from './channel-context-chain';\nimport type VoteChain from './vote-chain';\nimport type { KeyPublic, SigningKeyPublic } from '@emberclear/crypto';\n\nexport default class Identity extends Model implements Partial<KeyPublic & SigningKeyPublic> {\n  @attr() name!: string;\n  @attr() publicKey!: Uint8Array;\n  @attr() publicSigningKey!: Uint8Array;\n\n  // non-persisted data\n  @tracked numUnread = 0;\n\n  // human-readable data\n  get publicKeyAsHex() {\n    return toHex(this.publicKey);\n  }\n\n  get publicSigningKeyAsHex() {\n    return toHex(this.publicSigningKey);\n  }\n\n  // Needed otherwise this regularly invalidates\n  // TODO: will the public key ever change? who knows\n  // eslint-disable-next-line\n  @computed()\n  get uid() {\n    return this.publicKeyAsHex;\n  }\n\n  get displayName() {\n    const name = this.name;\n    const shortKey = this.publicKeyAsHex.substring(0, 8);\n\n    return `${name} (${shortKey})`;\n  }\n\n  // TODO: CLEAN THIS UP\n  //       SEE README\n  // Unused, but necessary to properly set up relationships, therefore async\n  // eslint-disable-next-line prettier/prettier\n  @hasMany('channel-context-chain', { async: true, inverse: 'admin' })\n  adminOf?: ChannelContextChain;\n  // eslint-disable-next-line prettier/prettier\n  @hasMany('channel-context-chain', { async: true, inverse: 'members' })\n  memberOf?: ChannelContextChain;\n  @hasMany('vote-chain', { async: true, inverse: 'target' }) targetOfVote?: VoteChain;\n  @hasMany('vote-chain', { async: true, inverse: 'key' }) voterOf?: VoteChain;\n  @hasMany('vote-chain', { async: true, inverse: 'yes' }) votedYesIn?: VoteChain;\n  @hasMany('vote-chain', { async: true, inverse: 'no' }) votedNoIn?: VoteChain;\n  @hasMany('vote-chain', { async: true, inverse: 'remaining' }) stillRemainingIn?: VoteChain;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    identity: Identity;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/user.ts",
    "content": "import { attr } from '@ember-data/model';\n\nimport Identity from './identity';\n\nimport type { KeyPair, SigningKeyPair } from '@emberclear/crypto';\n\nexport default class User extends Identity implements Partial<KeyPair>, Partial<SigningKeyPair> {\n  @attr() privateKey!: Uint8Array;\n  @attr() privateSigningKey!: Uint8Array;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    user: User;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/vote-chain.ts",
    "content": "import Model, { attr, belongsTo, hasMany } from '@ember-data/model';\n\nimport type ChannelContextChain from './channel-context-chain';\nimport type Identity from './identity';\nimport type Vote from './vote';\n\nexport enum VOTE_ACTION {\n  ADD = 'add',\n  REMOVE = 'remove',\n  PROMOTE = 'promote',\n}\n\n// TODO: CLEAN THIS UP\n//       SEE README\nexport default class VoteChain extends Model {\n  @hasMany('identity', { async: false, inverse: 'stillRemainingIn' }) remaining!: Identity[];\n  @hasMany('identity', { async: false, inverse: 'votedYesIn' }) yes!: Identity[];\n  @hasMany('identity', { async: false, inverse: 'votedNoIn' }) no!: Identity[];\n\n  @belongsTo('identity', { async: false, inverse: 'targetOfVote' }) target!: Identity;\n\n  @attr() action!: VOTE_ACTION;\n  @belongsTo('identity', { async: false, inverse: 'voterOf' }) key!: Identity;\n  @belongsTo('vote-chain', { async: false, inverse: 'parentChain' }) previousVoteChain!: VoteChain;\n\n  @attr() signature!: Uint8Array;\n\n  // Unused, but necessary to properly set up relationships, therefore async\n  @belongsTo('vote-chain', { async: true, inverse: 'previousVoteChain' }) parentChain!: VoteChain;\n  @belongsTo('channel-context-chain', { async: true }) supports!: ChannelContextChain;\n  @belongsTo('vote', { async: true }) wrappedIn!: Vote;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    'vote-chain': VoteChain;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/models/vote.ts",
    "content": "import Model, { belongsTo } from '@ember-data/model';\n\nimport type Channel from './channel';\nimport type VoteChain from './vote-chain';\n\n// TODO: CLEAN THIS UP\n//       SEE README\nexport default class Vote extends Model {\n  @belongsTo('vote-chain', { async: false }) voteChain!: VoteChain;\n\n  // Unused, but necessary to properly set up relationships, therefore async\n  @belongsTo('channel', { async: true }) activeVoteIn!: Channel;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    vote: Vote;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/serializers/application.js",
    "content": "const myFancyInflector = {\n  messages: 'message',\n  channels: 'channel',\n  contacts: 'contact',\n  identities: 'identity',\n  'invitation-results': 'invitations-result',\n  invitations: 'invitation',\n  'message-medias': 'message-media',\n  relays: 'relay',\n  users: 'user',\n};\n\nfunction enforceSingularTypesInDocument(document) {\n  if (Array.isArray(document.data)) {\n    document.data.map(singularizeResource);\n  } else if (document.data) {\n    singularizeResource(document.data);\n  }\n\n  if (Array.isArray(document.included)) {\n    document.included.map(singularizeResource);\n  }\n\n  return document;\n}\n\nfunction singularizeResource(resource) {\n  resource.type = singularize(resource.type);\n  if (resource.relationships) {\n    Object.keys(resource.relationships).forEach((key) => {\n      let data = resource.relationships[key].data;\n\n      if (Array.isArray(data)) {\n        data.forEach((r) => {\n          r.type = singularize(r.type);\n        });\n      } else if (data) {\n        data.type = singularize(data.type);\n      }\n    });\n  }\n}\n\nfunction singularize(str) {\n  return myFancyInflector[str] || str;\n}\n\nexport default class ApplicationSerializer {\n  static create() {\n    return new ApplicationSerializer();\n  }\n\n  normalizeResponse(_store, _schema, jsonApiDocument) {\n    let normalized = enforceSingularTypesInDocument(jsonApiDocument);\n\n    return normalized;\n  }\n\n  serialize(snapshot /*, options */) {\n    let type = snapshot.modelName;\n    let id = snapshot.id;\n\n    let attributes = snapshot.attributes();\n    let relationships = {};\n\n    snapshot.eachRelationship((key, meta) => {\n      let r = (relationships[key] = {});\n      if (meta.kind === 'belongsTo') {\n        let related = snapshot.belongsTo(key);\n\n        if (!related) {\n          r.data = null;\n        } else {\n          if (!related.id) {\n            throw new Error(`Attempting to save a relationship that has no id`);\n          }\n\n          r.data = { type: related.modelName, id: related.id };\n        }\n      } else {\n        let relatedSnapshots = snapshot.hasMany(key);\n        r.data = [];\n\n        if (!relatedSnapshots) {\n          return;\n        }\n\n        relatedSnapshots.map((related) => {\n          if (!related.id) {\n            throw new Error(`Attempting to save a relationship that has no id`);\n          }\n\n          r.data.push({ type: related.modelName, id: related.id });\n        });\n      }\n    });\n\n    return {\n      data: {\n        type,\n        id,\n        attributes,\n        relationships,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/services/channel-manager.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { Channel } from '@emberclear/local-account';\n\nexport default class ChannelManager extends Service {\n  @service declare store: StoreService;\n\n  async findOrCreate(id: string, name: string): Promise<Channel> {\n    try {\n      return await this.findAndSetName(id, name);\n    } catch (e) {\n      return await this.create(id, name);\n    }\n  }\n\n  async findAndSetName(id: string, name: string): Promise<Channel> {\n    let record = await this.find(id);\n\n    record.name = name;\n\n    await record.save();\n\n    return record;\n  }\n\n  async create(id: string, name: string): Promise<Channel> {\n    let record = this.store.createRecord('channel', { id, name });\n\n    await record.save();\n\n    return record;\n  }\n\n  async allChannels(): Promise<ArrayProxy<Channel>> {\n    const channels = await this.store.findAll('channel');\n\n    return channels;\n  }\n\n  async find(uid: string) {\n    return await this.store.findRecord('channel', uid);\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/services/contact-manager.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { fromHex } from '@emberclear/encoding/string';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type { ImportableIdentity } from '@emberclear/local-account/types';\n\nexport default class ContactManager extends Service {\n  @service declare store: StoreService;\n\n  @tracked isImporting = false;\n\n  find(uid: string) {\n    return this.store.findRecord('contact', uid);\n  }\n\n  async import(contacts: Partial<ImportableIdentity>[]) {\n    this.isImporting = true;\n\n    try {\n      for await (let contact of contacts) {\n        if (!contact.publicKey || !contact.name) return Promise.resolve();\n\n        await this.findOrCreate(contact.publicKey, contact.name);\n      }\n    } finally {\n      this.isImporting = false;\n    }\n  }\n\n  async findOrCreate(uid: string, name: string): Promise<Contact> {\n    try {\n      // an exception thrown here is never caught\n      return await this.findAndSetName(uid, name);\n    } catch (e) {\n      return await this.create(uid, name);\n    }\n  }\n\n  async allContacts(): Promise<ArrayProxy<Contact>> {\n    const contacts = await this.store.findAll('contact');\n\n    return contacts;\n  }\n\n  async create(uid: string, name: string): Promise<Contact> {\n    const publicKey = fromHex(uid);\n\n    let record = this.store.createRecord('contact', {\n      id: uid,\n      publicKey,\n      name,\n    });\n\n    await record.save();\n\n    return record;\n  }\n\n  async addContact(/* _info: any */) {\n    try {\n      // const existing = this.find(info.id);\n      // return? error?\n    } catch (e) {\n      // maybe find should do the try/catching...\n      // seems weird to try/catch every time we want to use find\n    }\n  }\n\n  private async findAndSetName(uid: string, name: string): Promise<Contact> {\n    let record = await this.find(uid);\n\n    // always update the name\n    record.name = name;\n\n    await record.save();\n\n    return record;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'contact-manager': ContactManager;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/services/current-user.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport { assert } from '@ember/debug';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\nimport { isPresent } from '@ember/utils';\n\nimport { timeout } from 'ember-concurrency';\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { CryptoConnector } from '@emberclear/crypto';\nimport { toHex } from '@emberclear/encoding/string';\n\nimport type StoreService from '@ember-data/store';\nimport type { KeyPair, SigningKeyPair, WorkersService } from '@emberclear/crypto';\nimport type User from '@emberclear/local-account/models/user';\n\nexport const currentUserId = 'me';\n\n// The purpose of this service is to be an interface that\n// handles syncing between the data store and persistent localstorage.\n//\n// if the identity doesn't already exist in the store, it will try\n// try to be loaded from localstorage.\n// if the identity does exist in in teh store, localstorage will\n// overwrite what is in the store.\n// the only time the localstorage copy of the identity is written to\n// is upon update and initial creation of the identity data.\nexport default class CurrentUserService extends Service {\n  @service declare workers: WorkersService;\n  @service declare store: StoreService;\n\n  declare __crypto__?: CryptoConnector;\n\n  @tracked declare __record__?: User;\n\n  get record() {\n    assert(`current-user.record cannot be accessed until the record is loaded`, this.__record__);\n\n    return this.__record__;\n  }\n\n  get crypto() {\n    assert(\n      `current-user.crypto cannot be accessed without loading the crypto web worker`,\n      this.__crypto__\n    );\n\n    return this.__crypto__;\n  }\n\n  get id() {\n    if (!this.__record__) return;\n\n    return this.__record__.id;\n  }\n\n  get name() {\n    if (!this.__record__) return;\n\n    return this.__record__.name;\n  }\n\n  get publicKey() {\n    if (!this.__record__) return;\n\n    return this.record.publicKey;\n  }\n\n  get privateKey() {\n    if (!this.__record__) return;\n\n    return this.__record__.privateKey;\n  }\n\n  get publicSigningKey() {\n    if (!this.__record__) return;\n\n    return this.__record__.publicSigningKey;\n  }\n\n  get privateSigningKey() {\n    if (!this.__record__) return;\n\n    return this.__record__.privateSigningKey;\n  }\n\n  get isLoggedIn(): boolean {\n    if (!this.__record__) {\n      return false;\n    }\n\n    return !!(this.privateKey && this.publicKey);\n  }\n\n  get uid(): string {\n    if (!this.publicKey) return '';\n\n    return toHex(this.publicKey);\n  }\n\n  get shareUrl(): string {\n    const uri = `${window.location.origin}/invite?name=${this.name}&publicKey=${this.uid}`;\n\n    return encodeURI(uri);\n  }\n\n  async create(name: string): Promise<void> {\n    await this.hydrateCrypto();\n\n    assert(`Expected crypto to be setup`, this.__crypto__);\n\n    const { publicKey, privateKey } = await this.__crypto__.generateKeys();\n    const { publicSigningKey, privateSigningKey } = await this.__crypto__.generateSigningKeys();\n\n    // remove existing record\n    await this.store.unloadAll('user');\n\n    await this.setIdentity(name, { privateKey, publicKey, privateSigningKey, publicSigningKey });\n\n    await this.load();\n  }\n\n  async setIdentity(name: string, keys: KeyPair & Partial<SigningKeyPair>) {\n    const record = this.store.createRecord('user', {\n      id: currentUserId,\n      name,\n      ...keys,\n    });\n\n    await record.save();\n\n    this.hydrateCrypto(record);\n\n    this.__record__ = record;\n  }\n\n  async exists(): Promise<boolean> {\n    let identity = await this.currentUser();\n\n    if (!identity) return false;\n\n    return isPresent(identity.privateKey);\n  }\n\n  async load(): Promise<User | null> {\n    try {\n      const existing = await this.store.findRecord('user', currentUserId, {\n        backgroundReload: true,\n      });\n\n      await this.hydrateCrypto(existing);\n\n      this.__record__ = existing;\n\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.migrate).perform();\n\n      return existing;\n    } catch (e) {\n      // When the user doesn't exist, e is undefined???\n      // why?\n      // TODO: when implementing custom indexeddb adapter..\n      //       don't throw undefined? lol\n      if (e) {\n        throw e;\n      }\n    }\n\n    return null;\n  }\n\n  async currentUser(): Promise<User | null> {\n    if (!this.__record__) return await this.load();\n\n    return this.__record__;\n  }\n\n  @dropTask\n  async migrate() {\n    // This is a super HACK :(\n    // for some reason, I can't find and update a record in the same\n    // async function... why?\n    await timeout(1000);\n\n    if (!this.__record__ || !this.__crypto__) {\n      return;\n    }\n\n    if (!this.privateSigningKey) {\n      let { publicSigningKey, privateSigningKey } = await this.__crypto__.generateSigningKeys();\n\n      this.__record__.setProperties({\n        publicSigningKey,\n        privateSigningKey,\n      });\n\n      await this.__record__?.save();\n    }\n  }\n\n  hydrateCrypto(user?: KeyPair) {\n    if (this.__crypto__) {\n      if (user) {\n        this.__crypto__.keys = user;\n      }\n\n      return;\n    }\n\n    this.__crypto__ = new CryptoConnector({\n      workerService: this.workers,\n      keys: user,\n    });\n  }\n\n  async importFromKey(name: string, privateKey: Uint8Array, privateSigningKey?: Uint8Array) {\n    this.hydrateCrypto();\n\n    assert(`Expected crypto to be setup`, this.__crypto__);\n\n    const publicKey = await this.__crypto__.derivePublicKey(privateKey);\n\n    let publicSigningKey;\n\n    if (privateSigningKey) {\n      publicSigningKey = await this.__crypto__.derivePublicSigningKey(privateSigningKey);\n    } else {\n      let signingKeys = await this.__crypto__.generateSigningKeys();\n\n      publicSigningKey = signingKeys.publicSigningKey;\n      privateSigningKey = signingKeys.privateSigningKey;\n    }\n\n    await this.setIdentity(name, { privateKey, publicKey, privateSigningKey, publicSigningKey });\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'current-user': CurrentUserService;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/local-account\": [\".\"],\n      \"@emberclear/local-account/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../../libraries/questionably-typed\" },\n    { \"path\": \"../../crypto\" },\n    { \"path\": \"../../encoding\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/types.ts",
    "content": "export interface ImportableIdentity {\n  name: string;\n  publicKey: string;\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon/utils.ts",
    "content": "import type Contact from './models/contact';\n\nexport function isContact(maybe: unknown): maybe is Contact {\n  if (typeof maybe !== 'object') return false;\n  if (!maybe) return false;\n\n  return 'isPinned' in maybe;\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/-private/contact.ts",
    "content": "import { generateAsymmetricKeys } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { toHex } from '@emberclear/encoding/string';\nimport { Status } from '@emberclear/local-account';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nimport { createRecord } from './utils';\n\nimport type { Contact } from '@emberclear/local-account';\n\nexport async function attributesForContact() {\n  const { publicKey } = await generateAsymmetricKeys();\n  const id = toHex(publicKey);\n\n  return { id, publicKey };\n}\n\nexport async function buildContact(name: string, attributes = {}): Promise<Contact> {\n  const store = getService('store');\n\n  const defaultAttributes = await attributesForContact();\n\n  const record = createRecord(store, 'contact', {\n    name,\n    onlineStatus: Status.OFFLINE,\n    ...defaultAttributes,\n    ...attributes,\n  });\n\n  return record;\n}\n\nexport async function createContact(name: string, attributes = {}): Promise<Contact> {\n  const record = await buildContact(name, attributes);\n\n  await record.save();\n\n  return record;\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/-private/current-user.ts",
    "content": "import { settled } from '@ember/test-helpers';\n\nimport { generateAsymmetricKeys } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { getService, getStore } from '@emberclear/test-helpers/test-support';\n\nimport type { User } from '@emberclear/local-account';\n\nexport async function createCurrentUser(): Promise<User> {\n  const store = getStore();\n  const currentUserService = getService('current-user');\n\n  const { publicKey, privateKey } = await generateAsymmetricKeys();\n\n  const record = store.createRecord('user', {\n    id: 'me',\n    name: 'Test User',\n    publicKey,\n    privateKey,\n  });\n\n  await record.save();\n\n  currentUserService.__record__ = record;\n  currentUserService.hydrateCrypto({ publicKey, privateKey });\n\n  await settled();\n\n  return record;\n}\n\nexport function setupCurrentUser(hooks: NestedHooks) {\n  hooks.beforeEach(async function () {\n    await createCurrentUser();\n  });\n}\n\nexport function getCurrentUser() {\n  return getService('current-user').record;\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/-private/storage.ts",
    "content": "import localforage from 'localforage';\n\nimport type Ember from 'ember';\nimport type { TestContext } from 'ember-test-helpers';\n\nasync function cleanEverything(owner: Ember.ApplicationInstance) {\n  const adapter = owner.lookup('adapter:application');\n\n  await adapter.cache.clear();\n\n  // specifically, offline storage\n  await localforage.clear();\n  localStorage.clear();\n}\n\nexport function clearLocalStorage(hooks: NestedHooks) {\n  hooks.beforeEach(async function (this: TestContext) {\n    await cleanEverything(this.owner);\n  });\n\n  hooks.afterEach(async function (this: TestContext) {\n    await cleanEverything(this.owner);\n  });\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/-private/user.ts",
    "content": "import {\n  generateAsymmetricKeys,\n  generateSigningKeys,\n} from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { toHex } from '@emberclear/encoding/string';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nimport { createRecord } from './utils';\n\nimport type { User } from '@emberclear/local-account';\n\nexport async function attributesForUser() {\n  const { publicKey } = await generateAsymmetricKeys();\n  const { publicSigningKey, privateSigningKey } = await generateSigningKeys();\n  const id = toHex(publicKey);\n\n  return { id, publicKey, publicSigningKey, privateSigningKey };\n}\n\nexport async function buildUser(name: string, attributes = {}): Promise<User> {\n  const store = getService('store');\n\n  const defaultAttributes = await attributesForUser();\n\n  const record = createRecord(store, 'user', {\n    name,\n    ...defaultAttributes,\n    ...attributes,\n  });\n\n  return record;\n}\n\nexport async function createUser(name: string, attributes = {}): Promise<User> {\n  const record = await buildUser(name, attributes);\n\n  await record.save();\n\n  return record;\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/-private/utils.ts",
    "content": "import type StoreService from '@ember-data/store';\nimport type { Contact, Identity, User } from '@emberclear/local-account';\n\ninterface ModelRegistry {\n  contact: Contact;\n  user: User;\n  identity: Identity;\n}\n\n/**\n * Helper that doesn't use the same type registry as ember-data.\n *\n * the regular store.createRecord uses ember-data/types/registries/model\n * which we can't use because we allow apps to create their own definitions\n * of these models.\n *\n */\nexport function createRecord<Key extends keyof ModelRegistry>(\n  store: StoreService,\n  modelName: Key,\n  attributes: Record<string, unknown>\n) {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return store.createRecord(modelName as any, attributes) as ModelRegistry[Key];\n}\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/index.ts",
    "content": "export { attributesForContact, buildContact, createContact } from './-private/contact';\nexport { createCurrentUser, getCurrentUser, setupCurrentUser } from './-private/current-user';\nexport { clearLocalStorage } from './-private/storage';\nexport { attributesForUser, buildUser, createUser } from './-private/user';\n"
  },
  {
    "path": "client/web/addons/local-account/addon-test-support/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/test-support\",\n    \"paths\": {\n      \"@emberclear/local-account\": [\"../declarations\"],\n      \"@emberclear/local-account/*\": [\"../declarations/*\"],\n      \"@emberclear/local-account/test-support\": [\".\"],\n      \"@emberclear/local-account/test-support/*\": [\"./*\"]\n    }\n  },\n  \"references\": [{ \"path\": \"../addon\" }, { \"path\": \"../../test-helpers\" }]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/app/adapters/application.js",
    "content": "export { default } from '@emberclear/local-account/adapters/application';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/channel-context-chain.js",
    "content": "export { default } from '@emberclear/local-account/models/channel-context-chain';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/channel.js",
    "content": "export { default } from '@emberclear/local-account/models/channel';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/contact.js",
    "content": "export { default } from '@emberclear/local-account/models/contact';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/identity.js",
    "content": "export { default } from '@emberclear/local-account/models/identity';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/user.js",
    "content": "export { default } from '@emberclear/local-account/models/user';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/vote-chain.js",
    "content": "export { default } from '@emberclear/local-account/models/vote-chain';\n"
  },
  {
    "path": "client/web/addons/local-account/app/models/vote.js",
    "content": "export { default } from '@emberclear/local-account/models/vote';\n"
  },
  {
    "path": "client/web/addons/local-account/app/serializers/application.js",
    "content": "export { default } from '@emberclear/local-account/serializers/application';\n"
  },
  {
    "path": "client/web/addons/local-account/app/services/channel-manager.js",
    "content": "export { default } from '@emberclear/local-account/services/channel-manager';\n"
  },
  {
    "path": "client/web/addons/local-account/app/services/contact-manager.js",
    "content": "export { default } from '@emberclear/local-account/services/contact-manager';\n"
  },
  {
    "path": "client/web/addons/local-account/app/services/current-user.js",
    "content": "export { default } from '@emberclear/local-account/services/current-user';\n"
  },
  {
    "path": "client/web/addons/local-account/app/services/store.js",
    "content": "import { RecordData } from '@ember-data/record-data/-private';\nimport { default as Store } from '@ember-data/store';\nimport { identifierCacheFor } from '@ember-data/store/-private';\n\nexport default class StoreService extends Store {\n  createRecordDataFor(modelName, id, clientId, storeWrapper) {\n    let identifier = identifierCacheFor(this).getOrCreateRecordIdentifier({\n      type: modelName,\n      id,\n      lid: clientId,\n    });\n    return new RecordData(identifier, storeWrapper);\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n      {\n        name: 'ember-default-with-jquery',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'jquery-integration': true,\n          }),\n        },\n        npm: {\n          devDependencies: {\n            '@ember/jquery': '^1.1.0',\n          },\n        },\n      },\n      {\n        name: 'ember-classic',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'application-template-wrapper': true,\n            'default-async-observers': false,\n            'template-only-glimmer-components': false,\n          }),\n        },\n        npm: {\n          ember: {\n            edition: 'classic',\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/local-account/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/local-account/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    // Add options here\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/local-account/index.js",
    "content": "'use strict';\n\nmodule.exports = {\n  name: require('./package').name,\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/addons/local-account/package.json",
    "content": "{\n  \"name\": \"@emberclear/local-account\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Implements client-side account management\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/local-account\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"@ember-data/debug\": \"3.26.0\",\n    \"@ember-data/model\": \"3.26.0\",\n    \"@ember-data/record-data\": \"3.26.0\",\n    \"@ember-data/store\": \"3.26.0\",\n    \"@emberclear/crypto\": \"*\",\n    \"@emberclear/encoding\": \"*\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-concurrency\": \"1.3.0\",\n    \"ember-concurrency-async\": \"0.3.2\",\n    \"ember-concurrency-decorators\": \"2.0.3\",\n    \"ember-concurrency-test-waiter\": \"0.4.0\",\n    \"ember-concurrency-ts\": \"0.2.2\",\n    \"ember-inflector\": \"^4.0.2\",\n    \"localforage\": \"1.9.0\",\n    \"uuid\": \"^8.3.2\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@emberclear/test-helpers\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-data\": \"3.16.14\",\n    \"@types/ember-data__model\": \"3.16.2\",\n    \"@types/ember-data__store\": \"3.16.1\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-assertions-extra\": \"0.8.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/local-account/testem.js",
    "content": "'use strict';\n\nmodule.exports = {\n  test_page: 'tests/index.html?hidepassed',\n  disable_watching: true,\n  launch_in_ci: ['Chrome'],\n  launch_in_dev: ['Chrome'],\n  browser_start_timeout: 120,\n  browser_args: {\n    Chrome: {\n      ci: [\n        // --no-sandbox is needed when running Chrome inside a container\n        process.env.CI ? '--no-sandbox' : null,\n        '--headless',\n        '--disable-dev-shm-usage',\n        '--disable-software-rasterizer',\n        '--mute-audio',\n        '--remote-debugging-port=0',\n        '--window-size=1440,900',\n      ].filter(Boolean),\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.23.0\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/local-account/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/local-account/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/local-account/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/local-account/tests/test-helper.ts",
    "content": "// Install Types and assertion extensions\nimport 'qunit-dom';\nimport 'qunit-assertions-extra';\n\nimport { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/local-account/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"@emberclear/local-account\": [\"../declarations\"],\n      \"@emberclear/local-account/*\": [\"../declarations/*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\n    \".\",\n    \"../types\"\n  ],\n  \"references\": [\n    { \"path\": \"../addon\" },\n    { \"path\": \"../addon-test-support\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/tests/unit/create-current-user-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport {\n  clearLocalStorage,\n  createCurrentUser,\n  setupCurrentUser,\n} from '@emberclear/local-account/test-support';\nimport { getService, getStore } from '@emberclear/test-helpers/test-support';\n\nmodule('TestHelper | create-current-user', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n\n  test('a new user is created and kept in cache', async function (assert) {\n    const before = getStore().peekAll('user');\n\n    assert.equal(before.length, 0);\n\n    await createCurrentUser();\n\n    const after = getStore().peekAll('user');\n\n    assert.equal(after.length, 1);\n    assert.equal(after.toArray()[0].id, 'me');\n  });\n\n  test('a new user is created and stored', async function (assert) {\n    const before = await getStore().findAll('user');\n\n    assert.equal(before.length, 0);\n\n    await createCurrentUser();\n\n    const after = await getStore().findAll('user');\n\n    assert.equal(after.length, 1);\n    assert.equal(after.toArray()[0].id, 'me');\n  });\n\n  test('the user is set on the identity service', async function (assert) {\n    const before = getService('current-user').__record__;\n\n    assert.notOk(before);\n\n    const user = await createCurrentUser();\n\n    const after = getService('current-user').__record__;\n\n    assert.deepEqual(after, user);\n  });\n\n  module('user is setup in a beforeEach', function (hooks) {\n    setupCurrentUser(hooks);\n\n    test('the user is logged in', function (assert) {\n      const isLoggedIn = getService('current-user').isLoggedIn;\n\n      assert.ok(isLoggedIn);\n    });\n\n    test('identity exists', async function (assert) {\n      const exists = await getService('current-user').exists();\n\n      assert.ok(exists);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/addons/local-account/tests/unit/models/contact-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { buildContact } from '@emberclear/local-account/test-support';\nimport { getStore } from '@emberclear/test-helpers/test-support';\n\nmodule('Unit | Model | contact', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let model = getStore().createRecord('contact', {});\n\n    assert.ok(model);\n  });\n\n  module('displayName', function () {\n    test('is derived from name and public key', async function (assert) {\n      let contact = await buildContact('NullVoxPopuli');\n\n      assert.matches(contact.displayName, /^NullVoxPopuli \\([0-9abcdef]{8}\\)/);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/addons/local-account/tests/unit/models/user-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { getStore } from '@emberclear/test-helpers/test-support';\n\nmodule('Unit | Model | user', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let model = getStore().createRecord('user', {});\n\n    assert.ok(model);\n  });\n});\n"
  },
  {
    "path": "client/web/addons/local-account/tests/unit/services/current-user-test.ts",
    "content": "import { module, skip } from 'qunit';\nimport { setupTest, test } from 'ember-qunit';\n\nimport { generateAsymmetricKeys } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nmodule('Unit | Service | identity', function (hooks) {\n  setupTest(hooks);\n\n  let service: CurrentUserService;\n\n  hooks.beforeEach(() => {\n    service = getService('current-user');\n  });\n\n  test('importFromKey where privateSigningKey is not present generates signing keys', async function (assert) {\n    let keys = await generateAsymmetricKeys();\n\n    await service.importFromKey('name', keys.privateKey);\n\n    assert.ok(service.record);\n    assert.ok(service.record.publicSigningKey);\n    assert.ok(service.record.privateSigningKey);\n  });\n\n  skip('can dump and reload', async function (assert) {\n    assert.expect(0);\n  });\n});\n"
  },
  {
    "path": "client/web/addons/local-account/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/local-account/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"addon\" },\n    { \"path\": \"addon-test-support\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/local-account/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n\nimport 'ember-concurrency-decorators';\nimport 'ember-concurrency-async';\nimport 'ember-concurrency-ts/async';\nimport 'ember-concurrency-test-waiter';\n"
  },
  {
    "path": "client/web/addons/local-account/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/networking/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/networking/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/networking/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/networking/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/networking/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/networking/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/networking/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/networking/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd networking`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/networking/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/networking/README.md",
    "content": "networking\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install networking\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/networking/addon/errors.ts",
    "content": "export class UnknownMessageError extends Error {}\nexport class DataTransferFailed extends Error {}\nexport class CurrentUserMustHaveAName extends Error {}\n"
  },
  {
    "path": "client/web/addons/networking/addon/index.ts",
    "content": "export { default as Message } from './models/message';\nexport { default as Relay } from './models/relay';\nexport { ensureRelays } from './required-data';\nexport { default as ConnectionService } from './services/connection';\nexport { EphemeralConnection } from './services/connection/ephemeral/ephemeral-connection';\nexport { default as ConnectionStatus } from './services/connection/status';\nexport { default as MessageDispatcher } from './services/messages/dispatcher';\nexport { default as MessageFactory } from './services/messages/factory';\nexport { Connection } from './utils/connection/connection';\n"
  },
  {
    "path": "client/web/addons/networking/addon/models/message/utils.ts",
    "content": "import { TARGET, TYPE } from '../message';\n\nimport type Message from '../message';\n\ntype RecordArray<T> = Array<T>;\n\nexport function selectUnreadDirectMessages(\n  messages: Message[] | RecordArray<Message> | TODO,\n  fromId: string\n): Message[] {\n  const filtered = selectUnreadMessages(messages).filter((m) => {\n    return m.from === fromId;\n  });\n\n  return filtered;\n}\n\nexport function selectUnreadMessages(messages: Message[] | RecordArray<Message> | TODO): Message[] {\n  const filtered = messages.filter((m: Message) => {\n    return (\n      // ember-data in-flight messages\n      // don't yet have any fields\n      m.from &&\n      m.unread &&\n      // ensure the correct type of message\n      m.target !== TARGET.NONE &&\n      m.target !== TARGET.MESSAGE &&\n      m.type !== TYPE.PING\n    );\n  });\n\n  return filtered;\n}\n\nexport async function markAsRead(message: Message) {\n  message.readAt = new Date();\n\n  await message.save();\n}\n\nexport function messagesForDM(\n  messages: RecordArray<Message> | TODO,\n  me: string,\n  chattingWithId: string\n): Message[] {\n  let result = messages.filter((message: Message) => {\n    return isMessageDMBetween(message, me, chattingWithId);\n  });\n\n  return result;\n}\n\nexport function isMessageDMBetween(message: Message, me: string, chattingWithId: string) {\n  const isRelevant =\n    message.target === TARGET.WHISPER &&\n    message.type === TYPE.CHAT &&\n    // we sent this message to someone else (this could incude ourselves)\n    ((message.to === chattingWithId && message.from === me) ||\n      // we received a message from someone else to us (including from ourselves)\n      (message.from === chattingWithId && message.to === me));\n\n  return isRelevant;\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/models/message.ts",
    "content": "import Model, { attr, belongsTo, hasMany } from '@ember-data/model';\n\nimport type { Identity } from '@emberclear/local-account';\n\nexport const MESSAGE_LIMIT = 75;\n\n/**\n * types:\n *\n * CHAT:       a standard message sent to a person or room.\n *\n * EMOTE:      same as chat, but with special formatting for\n *             talking about oneself in the 3rd person.\n *\n * WHISPER:    same as chat, but explictly only intended for a single person\n *\n * PING:       a system message used to determine who is online upon app-boot\n *\n * DELIVERY_CONFIRMATION: a system message automatically sent back to someone\n *                        who sent you a message so that they know you received it\n *\n * DISCONNECT: a courtesy message to notify your contacts that you\n *             are about to go offline.\n *\n * INFO_CHANNEL_SYNC: a system message that is sent without a body to request channel context and returned with a body of channel context\n *\n * CHANNEL_VOTE: a system message used to distribute votes within a channel\n *\n * Properties of:\n *   Chat, Emote\n *   - channel: the id of the channel this message is intended for\n *              NOTE: additional channel properties (such as encryption, members, etc)\n *                    will ultimately be stored on the channel.\n *                    However, in order to make sure everyone's member list is up to date,\n *                    the member list will be sent along wich each message\n *              TODO: decide whether these extra properties live in the body json\n *              TODO: do we want structureless data in the body?\n *\n *   Whisper, Ping, Disconnect\n *   - no properties that alter behavior / message routing\n *\n * Currently Unused Properties:\n *  - contentType, thread\n *\n * Currently Unused Message Types:\n *  - emote, delivery confirmation, info channel sync, channel vote\n *\n * */\n\nexport enum TYPE {\n  CHAT = 'chat',\n  EMOTE = 'emote',\n  PING = 'ping',\n  DISCONNECT = 'disconnect',\n  DELIVERY_CONFIRMATION = 'delivery-confirmation',\n  INFO_CHANNEL_SYNC_REQUEST = 'info-channel-sync-request',\n  INFO_CHANNEL_SYNC_FULFILL = 'info-channel-sync-fulfill',\n  CHANNEL_VOTE = 'channtel-vote',\n}\n\nexport enum TARGET {\n  NONE = '',\n  WHISPER = 'whisper',\n  CHANNEL = 'channel',\n  MESSAGE = 'message',\n}\n\n/**\n * NOTE:\n * GUID - used for message receipts / delivery confirmation\n *        and threads\n * */\nexport default class Message extends Model {\n  /**\n   * from: the id of an identity\n   * */\n  @attr() from!: string;\n\n  /**\n   * identityId | channelId | messageId\n   *\n   * TODO: should these have different formats?;\n   * TODO: change this from ids to a polymorphic belongs to\n   * */\n  @attr() to!: string;\n\n  /**\n   * Contents of body may depend on the TYPE/TARGET\n   * */\n  @attr() body!: string;\n\n  /**\n   * Additional information for aiding in protocols\n   *\n   * For example:\n   *\n   *   In channel messages the following needs to be included,\n   *    - the creator of the channel\n   *    - the member list\n   *    - invites are pending\n   *      - who in the channel has approved the invites (for consensus)\n   *    - blacklisted members (blacklist by consensus as well)\n   *\n   *    TODO: maybe in the first iteration of channels, just the channel creator\n   *          can perform memberlist changes\n   * */\n  @attr() metadata!: Record<string, unknown>;\n\n  @attr() type!: TYPE;\n  @attr() target!: TARGET;\n\n  @attr() thread!: string;\n\n  @attr() receivedAt?: Date;\n  @attr() sentAt!: Date;\n\n  /**\n   * The Date/Time that the current user has viewed the message.\n   * Can also be artifically set via a \"Mark all as read\" button.\n   * */\n  @attr() readAt!: Date;\n\n  @attr() sendError?: string;\n\n  /**\n   * When a user comes online, they dispatch a bunch of pings to their contacts.\n   * If any of those contacts have queue messages (designated by this boolean)\n   * the messages will automatically be sent to the user who jest came online\n   * */\n  @attr() queueForResend?: boolean;\n\n  @belongsTo('identity', { async: false, polymorphic: true }) sender?: Identity;\n\n  // @belongsTo('message', { async: false, inverse: 'deliveryConfirmations' }) confirmationFor?: Message;\n  // @hasMany('message', { async: false, inverse: 'confirmationFor' }) deliveryConfirmations?: Message[];\n  @hasMany('message', { async: false }) deliveryConfirmations?: Message[];\n\n  get unread() {\n    return !this.readAt;\n  }\n\n  // currently unused\n  @attr() contentType!: string;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    message: Message;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/models/relay.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Model, { attr } from '@ember-data/model';\n\nexport default class Relay extends Model {\n  @attr('string') socket!: string;\n  @attr('string') og!: string;\n  @attr('string') host!: string;\n\n  @attr() priority!: number;\n\n  @tracked connectionCount = 0;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    relay: Relay;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/required-data.ts",
    "content": "import Ember from 'ember';\n\nimport type ApplicationInstance from '@ember/application/instance';\nimport type { Relay } from '@emberclear/networking';\n\nexport const defaultRelays = [\n  {\n    socket: 'wss://mesh-relay-in-us-1.herokuapp.com/socket',\n    og: 'https://mesh-relay-in-us-1.herokuapp.com/open_graph',\n    host: 'mesh-relay-in-us-1.herokuapp.com',\n  },\n  // {\n  //   socket: 'wss://mesh-relay-eu-1.herokuapp.com/socket',\n  //   og: 'https://mesh-relay-eu-1.herokuapp.com/open_graph',\n  //   host: 'mesh-relay-eu-1.herokuapp.com',\n  // },\n  // {\n  //   socket: 'ws://localhost:4301/socket',\n  //   og: 'http://localhost:4301/open_graph',\n  //   host: 'localhost:4301',\n  // },\n];\n\nexport async function ensureRelays(applicationInstance: ApplicationInstance) {\n  if (Ember.testing) return;\n\n  const store = applicationInstance.lookup('service:store');\n  const existing = await store.findAll('relay');\n  const existingHosts = existing.map((e: Relay) => e.host);\n\n  return await Promise.all(\n    defaultRelays.map((defaultRelay, i) => {\n      if (existingHosts.includes(defaultRelay.host)) {\n        return;\n      }\n\n      const record = store.createRecord('relay', {\n        ...defaultRelay,\n        priority: i + 1,\n      });\n\n      return record.save();\n    })\n  );\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/connection/ephemeral/ephemeral-connection.ts",
    "content": "import { getOwner, setOwner } from '@ember/application';\nimport { assert } from '@ember/debug';\nimport {\n  associateDestroyableChild,\n  isDestroyed,\n  isDestroying,\n  registerDestructor,\n} from '@ember/destroyable';\nimport { inject as service } from '@ember/service';\n\nimport { CryptoConnector } from '@emberclear/crypto';\nimport { fromHex, toHex } from '@emberclear/encoding/string';\nimport { Connection } from '@emberclear/networking';\nimport { defaultRelays } from '@emberclear/networking/required-data';\nimport { pool } from '@emberclear/networking/utils/connection/connection-pool';\n\nimport type StoreService from '@ember-data/store';\nimport type { WorkersService } from '@emberclear/crypto';\nimport type { EncryptableObject, EncryptedMessage, KeyPair } from '@emberclear/crypto/types';\nimport type { EndpointInfo } from '@emberclear/networking/types';\nimport type {\n  ConnectionPool,\n  STATUS,\n} from '@emberclear/networking/utils/connection/connection-pool';\n\ntype Target = {\n  pub: Uint8Array;\n  hex: string;\n};\n\nconst DEFAULT_GETTER = () => defaultRelays;\n\nexport type GetEndpoints = () => EndpointInfo[] | Promise<EndpointInfo[]>;\n\nexport interface Options {\n  getEndpoints?: GetEndpoints | undefined;\n  publicKeyAsHex?: string | undefined;\n  keys?: KeyPair | undefined;\n}\n\nexport class EphemeralConnection {\n  @service declare store: StoreService;\n  @service declare workers: WorkersService;\n\n  // setup in the psuedo constructor (static method: build)\n  // (build is an \"async constructor\")\n  declare connectionPool: ConnectionPool<Connection>;\n  declare crypto: CryptoConnector;\n  declare hexId: string;\n\n  /**\n   * Static information about who we're connecting to\n   * - useful if the connection is only meant for one person\n   */\n  declare target?: Target;\n\n  getEndpoints: GetEndpoints = () => [];\n\n  /**\n   * For creating new instances of ephemeral connections\n   * within ember apps.\n   * ( requires on object with an owner and is destroyable )\n   *\n   * This is deliberately not called create, because\n   * this should not be called be the Ember D.I. System\n   * as creation of these instances is *async*... and async\n   * constructors don't exist.\n   *\n   * Additionally, it has a different signature.\n   * There is no \"automatic\" way to have a destroyable created,\n   * so given a \"caller\", we can register the destructor.\n   *\n   * When this method finishes running, you will have\n   * - public/private keys\n   * - a new connection(pool) to the relays\n   *\n   * @param parent Ember Container Object (must be destroyable)\n   * @param publicKeyAsHex string\n   */\n  static async build<SubClass extends EphemeralConnection>(\n    /* hack to get inheritence in static methods */\n    this: { new (hex?: string): SubClass },\n    /* the actual params to this method */\n    // eslint-disable-next-line @typescript-eslint/ban-types\n    parent: object,\n    options?: Options\n  ): Promise<SubClass> {\n    let { getEndpoints, publicKeyAsHex, keys } = options || {};\n    let instance = new this(publicKeyAsHex);\n\n    setOwner(instance, getOwner(parent));\n    associateDestroyableChild(parent, instance);\n    registerDestructor(instance, instance.teardown.bind(instance));\n\n    await instance.hydrateCrypto(keys);\n    assert('Crypto failed to initialize', instance.crypto);\n    assert('Failed to generate an ephemeral identifier', instance.hexId);\n\n    instance.getEndpoints = getEndpoints || DEFAULT_GETTER;\n\n    await instance.establishConnection();\n    assert('Connection Pool failed to be set up', instance.connectionPool);\n\n    return instance;\n  }\n\n  constructor(publicKeyAsHex?: string) {\n    this.setTarget(publicKeyAsHex);\n  }\n\n  setTarget(publicKeyAsHex?: string) {\n    if (publicKeyAsHex) {\n      this.target = {\n        pub: fromHex(publicKeyAsHex),\n        hex: publicKeyAsHex,\n      };\n    }\n  }\n\n  setCrypto(keys: KeyPair) {\n    this.crypto.keys = keys;\n    this.hexId = toHex(keys.publicKey);\n  }\n\n  disconnect() {\n    if (this.connectionPool) {\n      this.connectionPool.drain();\n    }\n  }\n\n  teardown() {\n    this.disconnect();\n  }\n\n  onData(_data: EncryptedMessage) {\n    throw new Error('onData must be overridden in a subclass');\n  }\n\n  ///////////////////////////////////////\n\n  async hydrateCrypto(keys?: KeyPair) {\n    let { hex, crypto } = await generateEphemeralKeys(this.workers, keys);\n\n    if (isDestroying(this) || isDestroyed(this)) return;\n\n    this.crypto = crypto;\n    this.hexId = hex;\n  }\n\n  sendToHex(message: EncryptableObject, hexPub: string) {\n    let pub = fromHex(hexPub);\n\n    return this.send(message, { hex: hexPub, pub });\n  }\n\n  async send(message: EncryptableObject, target?: Target) {\n    if (isDestroying(this) || isDestroyed(this)) return;\n\n    let _target = this.target || target;\n\n    if (!_target) {\n      throw new Error('Cannot send a message with no target');\n    }\n\n    if (!this.connectionPool) {\n      await this.establishConnection();\n\n      if (isDestroying(this) || isDestroyed(this)) return;\n      // throw new Error('Cannot send a message with no connection to the target');\n    }\n\n    let to = _target.pub;\n    let connection = await this.connectionPool.acquire();\n\n    if (isDestroying(this) || isDestroyed(this)) return;\n\n    let encryptedMessage = await this.crypto.encryptForSocket({ ...message }, { publicKey: to });\n\n    if (isDestroying(this) || isDestroyed(this)) return;\n\n    await connection.send({ to: toHex(to), message: encryptedMessage });\n  }\n\n  async establishConnection() {\n    if (this.connectionPool) return;\n\n    if (isDestroying(this) || isDestroyed(this)) return;\n\n    this.connectionPool = await pool<Connection>({\n      endpoints: await this.getEndpoints(),\n      create: createConnection.bind(null, this.hexId, this.onData),\n      destroy: (instance) => instance.destroy(),\n      isOk: (instance) => instance.isConnected,\n      onStatusChange: (status: STATUS) => console.info({ status }),\n      minConnections: 1,\n    });\n  }\n}\n\nasync function generateEphemeralKeys(workers: WorkersService, keys?: KeyPair) {\n  let crypto = new CryptoConnector({ workerService: workers });\n\n  if (!keys) {\n    keys = await crypto.generateKeys();\n  }\n\n  crypto.keys = keys;\n\n  let hex = toHex(keys.publicKey);\n\n  return { hex, crypto, ...keys };\n}\n\nasync function createConnection(\n  publicKey: string,\n  onData: (data: EncryptedMessage) => void,\n  relay: EndpointInfo\n) {\n  let instance = new Connection({\n    relay,\n    onData,\n    publicKey,\n    onInfo: (_info) => ({}),\n  });\n\n  await instance.connect();\n\n  return instance;\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/connection/manager.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { Connection } from '@emberclear/networking';\nimport { pool } from '@emberclear/networking/utils/connection/connection-pool';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { ConnectionStatus, Relay } from '@emberclear/networking';\nimport type MessageProcessor from '@emberclear/networking/services/messages/processor';\nimport type { OpenGraphData } from '@emberclear/networking/types';\nimport type {\n  ConnectionPool,\n  STATUS,\n} from '@emberclear/networking/utils/connection/connection-pool';\n\nexport default class ConnectionManager extends Service {\n  @service declare store: StoreService;\n  @service('messages/processor') declare processor: MessageProcessor;\n  @service('connection/status') declare status: ConnectionStatus;\n  @service declare currentUser: CurrentUserService;\n\n  declare connectionPool?: ConnectionPool<Connection>;\n\n  async getOpenGraph(url: string): Promise<OpenGraphData> {\n    if (!this.connectionPool) {\n      return {};\n    }\n\n    let connection = await this.connectionPool.acquire();\n    let safeUrl = encodeURIComponent(url);\n    let ogUrl = `${connection.relay.og}?url=${safeUrl}`;\n\n    let response = await fetch(ogUrl, {\n      credentials: 'omit',\n      referrer: 'no-referrer',\n      cache: 'no-cache',\n      headers: {\n        ['Accept']: 'application/json',\n      },\n    });\n\n    try {\n      let json = await response.json();\n\n      return (json || {}).data;\n    } catch (e) {\n      return {};\n    }\n  }\n\n  acquire() {\n    if (!this.connectionPool) {\n      return;\n    }\n\n    return this.connectionPool.acquire();\n  }\n\n  async setup() {\n    if (this.connectionPool) {\n      return;\n    }\n\n    let relays: ArrayProxy<Relay> = await this.store.findAll('relay');\n\n    // TODO:\n    //   figure out how to handle message received concurrency.\n    //   - what happens if the connection pool is all connected to\n    //     the same relay?\n    //   - should we prevent the ability to connect to the same relay\n    //     multiple times\n    //   - what happens if we can't mean our min-connections?\n    this.connectionPool = await pool<Connection>({\n      endpoints: relays.toArray(),\n\n      create: this.createConnection.bind(this),\n      destroy: (instance) => instance.destroy(),\n      isOk: (instance) => instance.isConnected,\n      onStatusChange: this.updateStatus.bind(this),\n\n      minConnections: 1,\n    });\n  }\n\n  disconnect() {\n    if (this.connectionPool) {\n      this.connectionPool.drain();\n    }\n  }\n\n  willDestroy() {\n    this.disconnect();\n\n    return super.destroy();\n  }\n\n  private updateStatus(status: STATUS) {\n    this.status.updateStatus(status);\n  }\n\n  private async createConnection(relay: Relay) {\n    let instance = new Connection({\n      relay,\n      publicKey: this.currentUser.uid,\n      onData: this.processor.receive.bind(this.processor),\n      onInfo: (info) => {\n        // TODO: Temporary\n        // TODO: This needs to be pulled out into a web worker (the whole class)\n        // TODO: don't set these directly on the relay?\n        Object.assign(relay, info);\n      },\n    });\n\n    // Do connect / subscribe, etc\n    await instance.connect();\n\n    return instance;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'connection/manager': ConnectionManager;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/connection/status.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { restartableTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport {\n  STATUS_CONNECTED,\n  STATUS_CONNECTING,\n  STATUS_DEGRADED,\n  STATUS_DISCONNECTED,\n  STATUS_UNKNOWN,\n} from '@emberclear/networking/utils/connection/connection-pool';\n\nimport type { STATUS } from '@emberclear/networking/utils/connection/connection-pool';\n\nconst STATUS_LEVEL_MAP = {\n  [STATUS_UNKNOWN]: 'warning',\n  [STATUS_DEGRADED]: 'warning',\n  [STATUS_CONNECTED]: 'info',\n  [STATUS_CONNECTING]: 'info',\n  [STATUS_DISCONNECTED]: 'danger',\n};\n\nexport default class ConnectionStatusService extends Service {\n  @tracked hasUpdate = false;\n  @tracked hadUpdate = false;\n\n  @tracked text = '';\n  @tracked level = '';\n\n  get isConnected() {\n    switch (this.text) {\n      case STATUS_CONNECTED:\n      case STATUS_DEGRADED:\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  get isConnecting() {\n    return this.text === STATUS_CONNECTING;\n  }\n\n  updateStatus(text: STATUS) {\n    this.text = text;\n    this.level = STATUS_LEVEL_MAP[text];\n\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.showStatusChange).perform();\n  }\n\n  @restartableTask({ withTestWaiter: true })\n  async showStatusChange() {\n    this.hasUpdate = true;\n    this.hadUpdate = false;\n\n    await timeout(2000);\n    this.hadUpdate = true;\n\n    await timeout(1000);\n\n    this.hasUpdate = false;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'connection/status': ConnectionStatusService;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/connection.ts",
    "content": "import Service, { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { Message } from '@emberclear/networking';\nimport type ConnectionManager from '@emberclear/networking/services/connection/manager';\nimport type ContactsOnlineChecker from '@emberclear/networking/services/contacts/online-checker';\nimport type MessageDispatcher from '@emberclear/networking/services/messages/dispatcher';\nimport type { OutgoingPayload } from '@emberclear/networking/utils/connection/connection';\n\ntype ConnectionHooks = {\n  onReceive(message: Message): Promise<unknown>;\n};\n\nexport default class ConnectionService extends Service {\n  @service declare currentUser: CurrentUserService;\n  @service('connection/manager') declare manager: ConnectionManager;\n  @service('messages/dispatcher') declare dispatcher: MessageDispatcher;\n  @service('contacts/online-checker') declare onlineChecker: ContactsOnlineChecker;\n\n  hooks?: ConnectionHooks;\n\n  connect() {\n    return taskFor(this._connect).perform();\n  }\n\n  disconnect() {\n    this.manager.disconnect();\n  }\n\n  async getOpenGraph(url: string) {\n    return this.manager.getOpenGraph(url);\n  }\n\n  async send(payload: OutgoingPayload) {\n    let instance = await this.manager.acquire();\n\n    if (instance) {\n      await instance.send(payload);\n    }\n  }\n\n  @dropTask({ withTestWaiter: true })\n  private async _connect() {\n    let canConnect = await this.canConnect();\n\n    if (!canConnect) return;\n\n    await this.manager.setup();\n\n    await this.dispatcher.pingAll();\n\n    return taskFor(this.onlineChecker.checkOnlineStatus).perform();\n  }\n\n  private canConnect(): Promise<boolean> {\n    return this.currentUser.exists();\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    connection: ConnectionService;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/contacts/online-checker.ts",
    "content": "import Ember from 'ember';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { Status } from '@emberclear/local-account';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type { MessageDispatcher, MessageFactory } from '@emberclear/networking';\n\nconst THIRTY_SECONDS = 30000;\n\nexport default class ContactsOnlineChecker extends Service {\n  @service declare store: StoreService;\n  @service('messages/dispatcher') declare dispatcher: MessageDispatcher;\n  @service('messages/factory') declare messageFactory: MessageFactory;\n\n  @task({ withTestWaiter: true })\n  async checkOnlineStatus() {\n    if (Ember.testing) return;\n\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      await timeout(THIRTY_SECONDS);\n\n      const ping = this.messageFactory.buildPing();\n\n      this.store\n        .peekAll('contact')\n        .filter((contact: Contact) => contact.onlineStatus !== Status.OFFLINE)\n        .forEach((contact) => {\n          return taskFor(this.dispatcher.sendToUser).perform(ping, contact);\n        });\n    }\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'contacts/online-checker': ContactsOnlineChecker;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/-utils/builder.ts",
    "content": "import type { Message } from '@emberclear/networking';\n\ninterface Sender {\n  name: string;\n  uid: string;\n}\n\nexport function buildSender(sender: Sender) {\n  return {\n    name: sender.name,\n    uid: sender.uid,\n    location: '',\n  };\n}\n\nexport function buildMessage(msg: Message) {\n  const { body, contentType } = msg;\n\n  return {\n    body,\n    contentType,\n  };\n}\n\nexport function build(msg: Message, sender: Sender) {\n  return {\n    id: msg.id,\n    to: msg.to,\n    type: msg.type,\n    target: msg.target,\n    client: '',\n    ['client_version']: '',\n    ['time_sent']: msg.sentAt,\n    sender: buildSender(sender),\n    message: buildMessage(msg),\n  };\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/auto-responder.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type { Message, MessageDispatcher, MessageFactory } from '@emberclear/networking';\n\n/**\n * Nothing here should be blocking, as these responses should not matter\n * to the receiver, but are for the sender's benefit.\n *\n * It is up to the invoker to not await these methods.\n * */\nexport default class MessageAutoResponder extends Service {\n  @service('messages/dispatcher') declare dispatcher: MessageDispatcher;\n  @service('messages/factory') declare factory: MessageFactory;\n  @service declare store: StoreService;\n\n  async messageReceived(respondToMessage: Message) {\n    const sender = respondToMessage.sender;\n    const response = this.factory.buildDeliveryConfirmation(respondToMessage);\n\n    if (sender) {\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.dispatcher.sendToUser).perform(response, sender);\n    }\n  }\n\n  async cameOnline(contact: Contact) {\n    const pendingMessages = await this.store.query('message', {\n      queueForResend: true,\n      to: contact.uid,\n    });\n\n    pendingMessages.forEach(async (message: Message) => {\n      message.queueForResend = false;\n      await message.save();\n\n      return taskFor(this.dispatcher.sendToUser).perform(message, contact);\n    });\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'messages/auto-responder': MessageAutoResponder;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/dispatcher.ts",
    "content": "import { assert } from '@ember/debug';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { toHex } from '@emberclear/encoding/string';\nimport { Contact, User } from '@emberclear/local-account';\n\nimport { build as toPayloadJson } from './-utils/builder';\n\nimport type StoreService from '@ember-data/store';\nimport type { Channel, CurrentUserService } from '@emberclear/local-account';\nimport type { ConnectionService, Message, MessageFactory } from '@emberclear/networking';\nimport type StatusManager from '@emberclear/networking/services/status-manager';\n\nexport default class MessageDispatcher extends Service {\n  @service declare store: StoreService;\n  @service declare connection: ConnectionService;\n  @service declare currentUser: CurrentUserService;\n  @service declare statusManager: StatusManager;\n  @service('messages/factory') declare messageFactory: MessageFactory;\n\n  async send(text: string, to: Contact | Channel) {\n    const message = this.messageFactory.buildChat(text, to);\n\n    await message.save();\n\n    this.sendTo(message, to);\n  }\n\n  // there needs to be a polymorphic relationship in order for this to work\n  // sendMessage(message: Message) {\n  //   return sendTo(message, message.to);\n  // }\n\n  sendTo(message: Message, to: Contact | Channel) {\n    message.queueForResend = false;\n\n    if (to instanceof User) {\n      return;\n    }\n\n    if (to instanceof Contact) {\n      taskFor(this.sendToUser).perform(message, to);\n\n      return;\n    }\n\n    // Otherwise, Channel Message\n    this.sendToChannel(message, to);\n  }\n\n  async pingAll() {\n    const ping = this.messageFactory.buildPing();\n\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.sendToAll).perform(ping);\n  }\n\n  // the downside to end-to-end encryption\n  // the bigger the list of identities, the longer this takes\n  //\n  // TODO: should this be hard-limited to just messages like PINGs?\n  @task\n  async sendToAll(msg: Message) {\n    const everyone = await this.store.findAll('contact');\n\n    everyone.forEach((contact: Contact) => {\n      return taskFor(this.sendToUser).perform(msg, contact);\n    });\n  }\n\n  sendToChannel(msg: Message, channel: Channel) {\n    const members = channel.contextChain.members;\n\n    members.forEach((member) => {\n      if (member.id === this.currentUser.id) return; // don't send to self\n\n      return taskFor(this.sendToUser).perform(msg, member);\n    });\n  }\n\n  @task\n  async sendToUser(msg: Message, to: Contact) {\n    if (!this.currentUser.crypto) {\n      console.info('Crypto Worker not available');\n\n      return;\n    }\n\n    const theirPublicKey = to.publicKey as Uint8Array;\n    const uid = toHex(theirPublicKey);\n\n    assert(`expected currentUser to exist`, this.currentUser.record);\n\n    const payload = toPayloadJson(msg, this.currentUser.record);\n\n    const encryptedMessage = await this.currentUser.crypto.encryptForSocket(payload, to);\n\n    try {\n      await this.connection.send({ to: uid, message: encryptedMessage });\n\n      msg.receivedAt = new Date();\n    } catch (e) {\n      const { reason, to_uid: toUid } = e;\n\n      if (reason) {\n        const error: string = reason;\n\n        msg.sendError = error;\n\n        if (error.match(/not found/)) {\n          // eslint-disable-next-line @typescript-eslint/no-floating-promises\n          this.statusManager.markOffline(toUid);\n\n          return;\n        } else if (error.match(/timed out/)) {\n          return;\n        }\n      }\n\n      console.debug(e.name, e);\n    }\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'messages/dispatcher': MessageDispatcher;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/factory.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { v4 as uuid } from 'uuid';\n\nimport { Identity } from '@emberclear/local-account';\nimport Channel from '@emberclear/local-account/models/channel';\nimport { TARGET, TYPE } from '@emberclear/networking/models/message';\n\nimport type StoreService from '@ember-data/store';\n// import { buildChannelInfo, buildVote } from '../channels/-utils/channel-factory';\nimport type { CurrentUserService } from '@emberclear/local-account';\n// import type Vote from '@emberclear/local-account/models/vote';\nimport type { Message } from '@emberclear/networking';\nimport type { P2PMessage } from '@emberclear/networking/types';\n\nexport default class MessageFactory extends Service {\n  @service declare store: StoreService;\n  @service declare currentUser: CurrentUserService;\n\n  buildNewReceivedMessage(json: P2PMessage, sender: Identity) {\n    const { id, type, target, message: msg } = json;\n\n    const message = this.store.createRecord('message', {\n      id,\n      type,\n      target,\n      sender,\n      from: sender.uid,\n      to: this.currentUser.uid,\n      sentAt: new Date(json.time_sent),\n      receivedAt: new Date(),\n      body: msg.body,\n      // thread: msg.thread,\n      contentType: msg.contentType,\n    });\n\n    return message;\n  }\n\n  buildChat(text: string, to: Identity | Channel) {\n    let attributes = {};\n\n    if (to instanceof Identity) {\n      attributes = { target: TARGET.WHISPER, to: to.uid };\n    } else if (to instanceof Channel) {\n      attributes = {\n        target: TARGET.CHANNEL,\n        to: to.id,\n        // channelInfo: buildChannelInfo(to),\n      };\n    }\n\n    let message = this.build({\n      body: text,\n      type: TYPE.CHAT,\n      // all messages sent are read... beacuse..\n      // we sent them, so... they are read already...\n      readAt: new Date(),\n      ...attributes,\n    });\n\n    return message;\n  }\n\n  // buildChannelVote(vote: Vote, to: Channel) {\n  //   return this.build({\n  //     type: TYPE.CHANNEL_VOTE,\n  //     to: to.id,\n  //     metadata: buildVote(vote),\n  //     channelInfo: buildChannelInfo(to),\n  //   });\n  // }\n\n  // buildChannelInfoSyncRequest(to: Channel) {\n  //   return this.build({\n  //     type: TYPE.INFO_CHANNEL_SYNC_REQUEST,\n  //     to: to.id,\n  //     channelInfo: buildChannelInfo(to),\n  //   });\n  // }\n\n  // buildChannelInfoSyncFulfill(to: Identity, data: Channel) {\n  //   return this.build({\n  //     type: TYPE.INFO_CHANNEL_SYNC_FULFILL,\n  //     to: to.uid,\n  //     channelInfo: buildChannelInfo(data),\n  //   });\n  // }\n\n  buildPing() {\n    return this.build({ type: TYPE.PING });\n  }\n\n  buildDeliveryConfirmation(forMessage: Message): Message {\n    return this.build({\n      target: TARGET.MESSAGE,\n      type: TYPE.DELIVERY_CONFIRMATION,\n      to: forMessage.id,\n    });\n  }\n\n  private build(attributes = {}) {\n    return this.store.createRecord('message', {\n      id: uuid(),\n      sentAt: new Date(),\n      from: this.currentUser.uid,\n      sender: this.currentUser.record,\n      ...attributes,\n    });\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'messages/factory': MessageFactory;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/handler.ts",
    "content": "import Service, { inject as service } from '@ember/service';\n\nimport { isContact } from '@emberclear/local-account/utils';\nimport { MESSAGE_LIMIT, TARGET, TYPE } from '@emberclear/networking/models/message';\nimport { isMessageDMBetween, messagesForDM } from '@emberclear/networking/models/message/utils';\n\nimport type MessageFactory from './factory';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\nimport type { Message } from '@emberclear/networking';\nimport type AutoResponder from '@emberclear/networking/services/messages/auto-responder';\nimport type StatusManager from '@emberclear/networking/services/status-manager';\nimport type { P2PMessage } from '@emberclear/networking/types';\n\nexport default class ReceivedMessageHandler extends Service {\n  @service declare store: StoreService;\n  @service declare currentUser: CurrentUserService;\n  @service declare contactManager: ContactManager;\n  @service declare statusManager: StatusManager;\n  @service('messages/factory') declare messageFactory: MessageFactory;\n  @service('messages/auto-responder') declare autoResponder: AutoResponder;\n\n  async handle(raw: P2PMessage) {\n    let message = await this.decomposeMessage(raw);\n\n    if (!message) {\n      console.info('Message could not be decomposed', raw);\n\n      return;\n    }\n\n    switch (message.type) {\n      case TYPE.CHAT:\n        return this.handleChat(message, raw);\n\n      case TYPE.EMOTE:\n        return this.handleChat(message, raw);\n\n      case TYPE.DELIVERY_CONFIRMATION:\n        return this.handleDeliveryConfirmation(message, raw);\n\n      case TYPE.DISCONNECT:\n        return this.handleDisconnect(message);\n\n      case TYPE.INFO_CHANNEL_SYNC_REQUEST:\n        return this.handleInfoChannelInfo(message, raw);\n\n      case TYPE.PING:\n        // do nothing, we do not need to send a response\n        // at least for now, we have socket-level tools to know\n        // when a message was sent successfully\n        return message;\n\n      default:\n        console.info('Unrecognized message to handle...', raw);\n\n        return message;\n    }\n  }\n\n  private async handleDeliveryConfirmation(message: Message, raw: P2PMessage) {\n    const targetMessage = await this.store.findRecord('message', raw.to);\n\n    // targetMessage.set('confirmationFor', message);\n    // TODO: see if ember data relationships can use normal push\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    (message.deliveryConfirmations as any).pushObject(targetMessage);\n\n    // blocking?\n    await message.save();\n\n    return message;\n  }\n\n  private async handleInfoChannelInfo(message: Message, _raw: P2PMessage) {\n    return message;\n  }\n\n  private async handleDisconnect(message: Message) {\n    // non-blocking\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.statusManager.markOffline(message.from);\n  }\n\n  private async handleChat(message: Message, raw: P2PMessage) {\n    // non-blocking\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.autoResponder.messageReceived(message);\n\n    switch (message.target) {\n      case TARGET.WHISPER:\n        return this.handleWhisperChat(message);\n\n      case TARGET.CHANNEL:\n        return this.handleChannelChat(message, raw);\n\n      default:\n        console.info('TARGET INVALID', raw);\n\n        return message;\n    }\n  }\n\n  private async handleWhisperChat(message: Message) {\n    await this.trimMessages(message);\n    await message.save();\n\n    if (message.sender) {\n      message.sender.numUnread++;\n    }\n\n    return message;\n  }\n\n  private async handleChannelChat(message: Message, _raw: P2PMessage) {\n    // TODO: if message is a channel message, deconstruct the channel info\n\n    return message;\n  }\n\n  private async decomposeMessage(json: P2PMessage) {\n    let { id, sender: senderInfo } = json;\n\n    let sender = await this.findOrCreateSender(senderInfo);\n\n    if (!isContact(sender)) {\n      return;\n    }\n\n    await this.statusManager.markOnline(sender);\n    await this.autoResponder.cameOnline(sender);\n\n    try {\n      // we've already received this message.\n      // it's possible to receive the same message multiple\n      // times if the sending client doesn't properly\n      // make the message as sent\n      let existing = await this.store.findRecord('message', id);\n\n      return existing;\n    } catch (e) {\n      // we have not yet received this message\n      // build a new message record\n      return this.messageFactory.buildNewReceivedMessage(json, sender);\n    }\n  }\n\n  /**\n   * TODO: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB\n   * TODO: https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex/openCursor\n   *\n   * Trims messages for a message group down to 100.... because list occlusion isn't a thing yet\n   * on the web (or is very very difficult to implement in JS)\n   *\n   * @param lastReceived this message is used to determine which chat DM / Channel the message\n   *                     belongs to, and which set of messages will be trimmed.\n   */\n  private async trimMessages(lastReceived: Message): Promise<void> {\n    let me = this.currentUser.uid;\n\n    // if the most recently receive message belongs to a stack of DMs,\n    // trim the DMs to be at most 100 messages.\n    let isApplicableForTrim = isMessageDMBetween(lastReceived, me, lastReceived.from);\n\n    if (isApplicableForTrim) {\n      let allMessages = this.store.peekAll('message');\n      let forDM = messagesForDM(allMessages, me, lastReceived.from);\n\n      let numTooMany = forDM.length - MESSAGE_LIMIT;\n\n      if (numTooMany > 0) {\n        let oldMessages = forDM.splice(0, numTooMany);\n\n        await Promise.all(oldMessages.map((oldMessage: Message) => oldMessage.destroyRecord()));\n      }\n    }\n  }\n\n  private async findOrCreateSender(senderData: { uid: string; name: string }) {\n    const { name, uid } = senderData;\n\n    if (uid === this.currentUser.uid) {\n      return this.currentUser.record;\n    }\n\n    return await this.contactManager.findOrCreate(uid, name);\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'messages/handler': ReceivedMessageHandler;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/messages/processor.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { enqueueTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { ConnectionService } from '@emberclear/networking';\nimport type ReceivedMessageHandler from '@emberclear/networking/services/messages/handler';\nimport type { P2PMessage } from '@emberclear/networking/types';\n\nexport default class MessageProcessor extends Service {\n  @service declare currentUser: CurrentUserService;\n  @service('messages/handler') declare handler: ReceivedMessageHandler;\n  @service declare connection: ConnectionService;\n\n  /**\n   * Because we could potentially be receiving multiple\n   * messages from a new contact, we need to queue multiple\n   * calls to receive.\n   *\n   * Without queueing them, we can run in to concurrency issues\n   * when interacting with the ember-data store / any \"saving\"\n   * behavior.\n   *\n   */\n  receive(socketData: EncryptedMessage) {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this._receive).perform(socketData);\n  }\n\n  @enqueueTask({ withTestWaiter: true, maxConcurrency: 1 })\n  async _receive(socketData: EncryptedMessage) {\n    const decrypted = await this.currentUser.crypto.decryptFromSocket<P2PMessage>(socketData);\n\n    let message = await this.handler.handle(decrypted);\n\n    if (message) {\n      await this.connection.hooks?.onReceive(message);\n    }\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'messages/processor': MessageProcessor;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/services/status-manager.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { Status } from '@emberclear/local-account/models/contact';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\n\n// TODO: does this need to be its own service?\n//       should these functions move to the ContactManager?\nexport default class StatusManager extends Service {\n  @service declare store: StoreService;\n  @service declare contactManager: ContactManager;\n\n  async markOffline(uid: string) {\n    const contact = await this.contactManager.find(uid);\n\n    if (!contact) return;\n\n    contact.onlineStatus = Status.OFFLINE;\n\n    return contact.save();\n  }\n\n  async markOnline(uid: string | Contact) {\n    let contact;\n\n    if (typeof uid === 'string') {\n      contact = await this.contactManager.find(uid);\n    } else {\n      contact = uid;\n    }\n\n    contact.onlineStatus = Status.ONLINE;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'status-manager': StatusManager;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/networking\": [\".\"],\n      \"@emberclear/networking/*\": [\"./*\"],\n\n      \"@ember/destroyable\": [\"../../../node_modules/ember-destroyable-polyfill\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../../libraries/questionably-typed\" },\n    { \"path\": \"../../../addons/crypto\" },\n    { \"path\": \"../../../addons/local-account\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/type-support.ts",
    "content": "import '@ember/service';\n// Services need to be imported in order to be added to the Service Registry\nimport './services/connection/status';\nimport './services/connection/manager';\nimport './services/contacts/online-checker';\nimport './services/messages/auto-responder';\nimport './services/messages/dispatcher';\nimport './services/messages/factory';\nimport './services/messages/handler';\nimport './services/messages/processor';\nimport './services/connection';\nimport './services/status-manager';\n"
  },
  {
    "path": "client/web/addons/networking/addon/types.ts",
    "content": "export interface EndpointInfo {\n  socket: string;\n  og: string;\n}\n\nexport interface P2PMessage {\n  id: string;\n  to: string;\n  type: string;\n  target: string;\n  client: string;\n  client_version: string;\n  time_sent: Date;\n  sender: {\n    name: string;\n    uid: string;\n    location: string;\n  };\n  message: {\n    body: string;\n    contentType: string;\n    metadata?: Record<string, unknown>;\n  };\n}\n\nexport interface RelayStateJson {\n  relay: { [key: string]: string };\n  ['connection_count']: number;\n  ['connected_relays']: number;\n  ['connected_to_relays']: Record<string, unknown>;\n}\n\nexport interface RelayState {\n  relay: { [key: string]: string };\n  connectionCount?: number;\n  connectedRelays?: number;\n  connectedToRelays?: Record<string, unknown>;\n}\n\nexport interface OpenGraphData {\n  audio?: string;\n  ['audio:secure_url']?: string;\n  ['audio:type']?: string;\n  description?: string;\n  determiner?: string;\n  image?: string;\n  ['image:alt']?: string;\n  ['image:height']?: string;\n  ['image:secure_url']?: string;\n  ['image:width']?: string;\n  locale?: string;\n  site_name?: string;\n  title?: string;\n  type?: string;\n  url?: string;\n  video?: string;\n  ['video:alt']?: string;\n  ['video:height']?: string;\n  ['video:secure_url']?: string;\n  ['video:type']?: string;\n  ['video:width']?: string;\n}\n\nexport interface RelayOpenGraphResponse {\n  data: OpenGraphData;\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/utils/connection/connection-pool.ts",
    "content": "import type { EndpointInfo } from '@emberclear/networking/types';\n\n// min-connections are met\nexport const STATUS_CONNECTED = 'connected';\n// initial attempt at achieving min-connections\nexport const STATUS_CONNECTING = 'connecting';\n// min-connections are not met\nexport const STATUS_DEGRADED = 'degraded';\n// there are no connections\nexport const STATUS_DISCONNECTED = 'disconnected';\n\n// hopefully this is never used\nexport const STATUS_UNKNOWN = 'unknown';\n\nexport type STATUS =\n  | typeof STATUS_CONNECTED\n  | typeof STATUS_CONNECTING\n  | typeof STATUS_DEGRADED\n  | typeof STATUS_DISCONNECTED\n  | typeof STATUS_UNKNOWN;\n\nexport interface PoolConfig<Connectable> {\n  // send: <Args extends Array<any>>(instance, ...args: Args) => Promise<void>;\n\n  // Available URLs / whatever that will be randomly selected when creating\n  // a new connection\n  endpoints: EndpointInfo[];\n\n  // How to create and destroy instances\n  // isOk is watched to know whether or not new\n  // instances need to be made\n  create: (endpoint: EndpointInfo) => Connectable | Promise<Connectable>;\n  destroy: (instance: Connectable) => void | Promise<void>;\n  isOk: (instance: Connectable) => boolean;\n\n  // hooks\n  // After Initial setup, degraded may be a possibility\n  // Connecting will only show up during the initial setup.\n  // Connecting kinda means, no connections / degraded, but\n  // we expected connections to happen\n  onStatusChange: (status: STATUS) => void;\n\n  // Min and Max number of instances to make\n  minConnections?: number;\n  maxConnections?: number;\n}\n\n/**\n * A pool is responsible for:\n * - creating new connections\n * - disposing of connections\n *\n * A pool is not responsible for:\n * - how to connect\n * - how to dispose\n * - receiving/sending messages\n *\n *\n * @param [PoolConfig] config;\n *\n */\nexport async function pool<Connectable>(\n  config: PoolConfig<Connectable>\n): Promise<ConnectionPool<Connectable>> {\n  let connectionPool = new ConnectionPool<Connectable>(config);\n\n  await connectionPool.hydrate();\n\n  return connectionPool;\n}\n\nexport class ConnectionPool<Connectable> {\n  private config: PoolConfig<Connectable>;\n\n  private connections: Connectable[] = [];\n\n  constructor(config: PoolConfig<Connectable>) {\n    this.config = config;\n  }\n\n  get activeConnections() {\n    return this.connections.filter(this.config.isOk);\n  }\n\n  get status(): STATUS {\n    let count = this.activeConnections.length;\n\n    if (count === 0) {\n      return STATUS_DISCONNECTED;\n    } else if (count < this.minConnections) {\n      return STATUS_DEGRADED;\n    } else if (count >= this.minConnections) {\n      return STATUS_CONNECTED;\n    }\n\n    return STATUS_UNKNOWN;\n  }\n\n  get minConnections() {\n    return this.config.minConnections || 1;\n  }\n\n  get minimumMet() {\n    return this.activeConnections.length >= this.minConnections;\n  }\n\n  // TODO: implement logic for selecting the \"best\" connection\n  async acquire(): Promise<Connectable> {\n    await this.hydrate();\n\n    let pseudoBestIndex = Math.floor(Math.random() * this.activeConnections.length);\n\n    return this.activeConnections[pseudoBestIndex];\n  }\n\n  // TODO: we need a way to monitor status changes within\n  //       a connection\n  async hydrate(): Promise<void> {\n    if (this.minimumMet) return;\n\n    this.notifyOfStatusChange(STATUS_CONNECTING);\n\n    for (let i = 0; i < this.minConnections; i++) {\n      let endpoint = this.nextEndpoint();\n\n      if (!endpoint) {\n        throw new Error(\n          `No available endpoint. Are too many minimum connections specified? Current: ${this.minConnections}`\n        );\n      }\n\n      let connection = await this.config.create(endpoint);\n\n      this.connections.push(connection);\n      this.notifyOfStatusChange();\n    }\n  }\n\n  drain() {\n    this.activeConnections.forEach(this.config.destroy);\n  }\n\n  private notifyOfStatusChange(status?: STATUS) {\n    if (!this.config.onStatusChange) {\n      return;\n    }\n\n    this.config.onStatusChange(status || this.status);\n  }\n\n  private nextEndpoint(): EndpointInfo {\n    if (this.config.endpoints.length === 0) {\n      throw new Error(`There are no endpoints in the connection pool`);\n    }\n\n    return this.config.endpoints[0];\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon/utils/connection/connection.ts",
    "content": "import { Socket } from 'phoenix';\n\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\nimport type { EndpointInfo, RelayState, RelayStateJson } from '@emberclear/networking/types';\nimport type { Channel } from 'phoenix';\n\nexport const NAME = Symbol('__PHOENIX_SOCKET__');\n\n// Side-effect bad.\n// Need a better way to mock this rather than just\n// changing what this is assigned to.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n(window as any)[NAME] = (window as any)[NAME] || Socket;\n\ninterface Args {\n  relay: EndpointInfo;\n  publicKey: string;\n  onData: (data: EncryptedMessage) => void;\n  onInfo: (data: RelayState) => void;\n}\n\nexport interface OutgoingPayload {\n  to: string;\n  message: string;\n}\n\nfunction phoenixSocket(): typeof Socket {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return (window as any)[NAME];\n}\n\nexport class Connection {\n  declare relay: EndpointInfo;\n  declare url: string;\n  declare publicKey: string;\n  declare channelName: string;\n  declare onData: (data: EncryptedMessage) => void;\n  declare onInfo: (data: RelayState) => void;\n\n  isConnected = false;\n  isConnecting = false;\n\n  private declare socket?: Socket;\n  private declare channel?: Channel;\n\n  /**\n   * @param [Relay] relay\n   * @param [string] publicKey: hex\n   */\n  constructor({ relay, publicKey, onData, onInfo }: Args) {\n    this.relay = relay;\n    this.url = relay.socket;\n    this.publicKey = publicKey;\n    this.channelName = `user:${publicKey}`;\n    this.onData = onData;\n    this.onInfo = onInfo;\n  }\n\n  async connect() {\n    if (this.isConnected || this.isConnecting) return;\n\n    await this.setupSocket();\n    await this.setupChannels();\n  }\n\n  private async setupSocket() {\n    return new Promise((resolve, reject) => {\n      let Klass = phoenixSocket();\n\n      this.isConnecting = true;\n\n      this.socket = new Klass(this.url, {\n        params: { uid: this.publicKey },\n      });\n\n      this.socket.onOpen(resolve);\n      this.socket.onError(reject);\n\n      this.socket.onClose(() => {\n        this.isConnected = false;\n      });\n\n      this.socket.connect();\n    });\n  }\n\n  private async setupChannels() {\n    await this.setupChatChannel();\n    await this.setupStatsChannel();\n  }\n\n  private async setupStatsChannel() {\n    return new Promise((resolve, reject) => {\n      if (!this.socket) return reject();\n\n      let channel = this.socket.channel(`stats`, {});\n\n      channel.on('state', (data: RelayStateJson) => {\n        let connectionCount = data['connection_count'];\n\n        this.onInfo({ relay: data.relay, connectionCount });\n      });\n\n      channel\n        .join()\n        .receive('ok', () => {\n          resolve(undefined);\n        })\n        .receive('error', reject);\n    });\n  }\n\n  private async setupChatChannel() {\n    return new Promise((resolve, reject) => {\n      if (!this.socket) return reject();\n\n      this.channel = this.socket.channel(this.channelName, {});\n\n      this.channel.on('chat', this.onData);\n\n      this.channel.onError(console.error);\n\n      this.channel.onClose(() => {\n        if (this.socket) {\n          this.socket.disconnect();\n        }\n      });\n\n      this.channel\n        .join()\n        .receive('ok', () => {\n          this.isConnected = true;\n          this.isConnecting = false;\n\n          resolve(this.channel);\n        })\n        .receive('error', (...args: unknown[]) => {\n          return reject(...args);\n        })\n        .receive('timeout', (...args: unknown[]) => {\n          console.info('channel timed out', ...args);\n        });\n    });\n  }\n\n  send(payload: OutgoingPayload) {\n    return new Promise((resolve, reject) => {\n      if (!this.channel) {\n        console.error('no channel present...');\n\n        return reject();\n      }\n\n      this.channel\n        .push('chat', payload)\n        .receive('ok', resolve)\n        .receive('error', reject)\n        .receive('timeout', () => reject({ reason: 'timed out' }));\n    });\n  }\n\n  destroy() {\n    if (this.socket) {\n      this.socket.disconnect();\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/addon-test-support/index.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { later, schedule } from '@ember/runloop';\n\nimport { NAME } from '@emberclear/networking/utils/connection/connection';\n\nimport type ApplicationInstance from '@ember/application/instance';\nimport type { Socket } from 'phoenix';\n\ntype Callback = (...args: unknown[]) => void;\n\n/**\n * Somewhat re-implements the relay behavior\n *\n * See: @emberclear/networking/utils/connection/connection.ts\n */\nexport function setupSocketServer(hooks: NestedHooks) {\n  let oldSocket: Socket;\n  let owner: ApplicationInstance | undefined;\n  let users: Record<string, ReturnType<typeof fakeServer['channel']>> = {};\n\n  function FakeSocket(_url: string, _opts: Options) {\n    return fakeServer;\n  }\n\n  const fakeServer: any = {\n    onOpen: (fn: any) => (fakeServer._onOpen = fn),\n    onError: () => {\n      /* not needed */\n    },\n    onClose: () => {\n      /* not needed */\n    },\n\n    connect: () => fakeServer._onOpen(),\n    disconnect: () => {\n      /* eh */\n    },\n    channel(channelName: string) {\n      let id = '';\n\n      if (channelName.startsWith('user')) {\n        let [, _id] = channelName.split(':');\n\n        id = _id;\n      }\n\n      const channel = {\n        _handle: {} as Record<string, Callback>,\n\n        push(kind: string, payload: any) {\n          const pushHandler = {\n            _receive: {} as Record<string, Callback>,\n\n            /**\n             * @public\n             */\n            receive(kind: string, callback: Callback) {\n              pushHandler._receive[kind] = callback;\n\n              return pushHandler;\n            },\n          };\n\n          switch (kind) {\n            case 'chat': {\n              let { to, message } = payload;\n\n              // use runloop to hold up tests until this finishes\n              schedule('afterRender', () => {\n                if (!owner) return;\n\n                if (!users[to]) {\n                  console.info({ users, payload, callback: pushHandler._receive?.error });\n\n                  return pushHandler._receive?.error('user not found');\n                }\n\n                users[to]._handle['chat']({ uid: id, message });\n\n                pushHandler._receive?.ok?.();\n              });\n              break;\n            }\n\n            default:\n              console.debug('unknown push', { channelName, msg: kind, payload });\n          }\n\n          return pushHandler;\n        },\n        join() {\n          return channel;\n        },\n\n        receive(kind: string, callback: () => void) {\n          switch (kind) {\n            // on channel join\n            case 'ok':\n              requestAnimationFrame(callback);\n\n              return channel;\n            case 'error':\n              // no tests of errors so far\n              return channel;\n            case 'timeout':\n              // no tests of timeouts so far\n              return channel;\n            default:\n              later(\n                null,\n                () => {\n                  console.debug('unnamed receive', { msg: kind });\n\n                  callback();\n                },\n                10\n              );\n          }\n\n          return channel;\n        },\n\n        on<T>(msgType: string, callback: (data: T) => void) {\n          channel._handle[msgType] = callback;\n        },\n        leave() {\n          /* not needed */\n        },\n        onClose() {\n          /* not needed */\n        },\n        onError() {\n          /* not needed */\n        },\n      };\n\n      if (id) {\n        users[id] = channel;\n      }\n\n      return channel;\n    },\n  };\n\n  hooks.beforeEach(function () {\n    users = {};\n    oldSocket = (window as any)[NAME];\n    owner = this.owner;\n\n    (window as any)[NAME] = FakeSocket;\n  });\n\n  hooks.afterEach(function () {\n    (window as any)[NAME] = oldSocket;\n  });\n}\n\ntype Options = {\n  params: { uid: string };\n};\n\n// export function mockSocketServer(url?: string) {\n//   const fakeURL = url || `wss://${defaultRelays[0].host}/`;\n//   const mockServer = new Server(fakeURL);\n\n//   mockServer.on('connection', (socket) => {\n//     console.log('connect', { socket, mockServer });\n//     socket.on('message', (data) => {\n//       console.log({ data, socket, mockServer });\n//       socket.send('test message from mock server');\n//     });\n//   });\n\n//   return mockServer;\n// }\n"
  },
  {
    "path": "client/web/addons/networking/addon-test-support/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/test-support\",\n    \"paths\": {\n      \"@emberclear/networking\": [\"../declarations\"],\n      \"@emberclear/networking/*\": [\"../declarations/*\"],\n      \"@emberclear/networking/test-support\": [\".\"],\n      \"@emberclear/networking/test-support/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [{ \"path\": \"../addon\" }]\n}\n"
  },
  {
    "path": "client/web/addons/networking/app/models/message.js",
    "content": "export { default } from '@emberclear/networking/models/message';\n"
  },
  {
    "path": "client/web/addons/networking/app/models/relay.js",
    "content": "export { default } from '@emberclear/networking/models/relay';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/connection/manager.js",
    "content": "export { default } from '@emberclear/networking/services/connection/manager';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/connection/status.js",
    "content": "export { default } from '@emberclear/networking/services/connection/status';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/connection.js",
    "content": "export { default } from '@emberclear/networking/services/connection';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/contacts/online-checker.js",
    "content": "export { default } from '@emberclear/networking/services/contacts/online-checker';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/messages/auto-responder.js",
    "content": "export { default } from '@emberclear/networking/services/messages/auto-responder';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/messages/dispatcher.js",
    "content": "export { default } from '@emberclear/networking/services/messages/dispatcher';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/messages/factory.js",
    "content": "export { default } from '@emberclear/networking/services/messages/factory';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/messages/handler.js",
    "content": "export { default } from '@emberclear/networking/services/messages/handler';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/messages/processor.js",
    "content": "export { default } from '@emberclear/networking/services/messages/processor';\n"
  },
  {
    "path": "client/web/addons/networking/app/services/status-manager.js",
    "content": "export { default } from '@emberclear/networking/services/status-manager';\n"
  },
  {
    "path": "client/web/addons/networking/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n      {\n        name: 'ember-default-with-jquery',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'jquery-integration': true,\n          }),\n        },\n        npm: {\n          devDependencies: {\n            '@ember/jquery': '^1.1.0',\n          },\n        },\n      },\n      {\n        name: 'ember-classic',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'application-template-wrapper': true,\n            'default-async-observers': false,\n            'template-only-glimmer-components': false,\n          }),\n        },\n        npm: {\n          ember: {\n            edition: 'classic',\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/networking/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/networking/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    // Add options here\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/networking/index.js",
    "content": "'use strict';\n\nmodule.exports = {\n  name: require('./package').name,\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n};\n"
  },
  {
    "path": "client/web/addons/networking/package.json",
    "content": "{\n  \"name\": \"@emberclear/networking\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Handles all the networking concerns of emberclear projects\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/networking\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"@emberclear/crypto\": \"*\",\n    \"@emberclear/local-account\": \"*\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-concurrency\": \"1.3.0\",\n    \"ember-concurrency-async\": \"0.3.2\",\n    \"ember-concurrency-decorators\": \"2.0.3\",\n    \"ember-concurrency-test-waiter\": \"0.4.0\",\n    \"ember-concurrency-ts\": \"0.2.2\",\n    \"ember-destroyable-polyfill\": \"2.0.3\",\n    \"phoenix\": \"1.5.9\",\n    \"uuid\": \"8.3.2\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@emberclear/test-helpers\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-data\": \"3.16.14\",\n    \"@types/ember-data__model\": \"3.16.2\",\n    \"@types/ember-data__store\": \"3.16.1\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/phoenix\": \"1.5.1\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"@types/uuid\": \"8.3.1\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-dom\": \"1.6.0\"\n  },\n  \"engines\": {\n    \"node\": \"10.* || >= 12\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/networking/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/app.ts",
    "content": "import './with-test-waiter';\n\nimport Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/router.ts",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/app/with-test-waiter.js",
    "content": "import defineModifier from 'ember-concurrency-test-waiter/define-modifier';\n\ndefineModifier();\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.21.2\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/networking/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/networking/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/networking/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/networking/tests/test-helper.ts",
    "content": "import { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/networking/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"@emberclear/networking\": [\"../declarations\"],\n      \"@emberclear/networking/*\": [\"../declarations/*\"]\n    }\n  },\n  \"references\": [\n    { \"path\": \"../../test-helpers\" },\n    { \"path\": \"../addon\" },\n    { \"path\": \"../addon-test-support\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/networking/tests/unit/services/connection/status-test.js",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { timeout } from 'ember-concurrency';\n\nimport {\n  STATUS_CONNECTED,\n  STATUS_CONNECTING,\n  STATUS_DEGRADED,\n  STATUS_UNKNOWN,\n} from '@emberclear/networking/utils/connection/connection-pool';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Unit | Service | connection/status', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = getService('connection/status');\n\n    assert.ok(service);\n  });\n\n  test('updating the status sets properties', async function (assert) {\n    let service = getService('connection/status');\n\n    assert.equal(service.text, '');\n    assert.equal(service.level, '');\n    assert.notOk(service.hasUpdate);\n    assert.notOk(service.hadUpdate);\n\n    service.updateStatus(STATUS_CONNECTING);\n\n    assert.equal(service.text, STATUS_CONNECTING);\n    assert.equal(service.level, 'info');\n\n    assert.ok(service.hasUpdate);\n    assert.notOk(service.hadUpdate);\n\n    await timeout(2200);\n\n    assert.ok(service.hadUpdate);\n    assert.ok(service.hasUpdate);\n\n    await timeout(1200);\n\n    assert.notOk(service.hasUpdate);\n  });\n\n  test('isConnected', function (assert) {\n    let service = getService('connection/status');\n\n    service.updateStatus(STATUS_CONNECTING);\n\n    assert.notOk(service.isConnected);\n\n    service.updateStatus(STATUS_CONNECTED);\n\n    assert.ok(service.isConnected);\n\n    service.updateStatus(STATUS_UNKNOWN);\n\n    assert.notOk(service.isConnected);\n\n    service.updateStatus(STATUS_DEGRADED);\n\n    assert.ok(service.isConnected);\n  });\n});\n"
  },
  {
    "path": "client/web/addons/networking/tests/unit/services/messages/auto-responder-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { v4 as uuid } from 'uuid';\n\nimport {\n  clearLocalStorage,\n  createContact,\n  setupCurrentUser,\n} from '@emberclear/local-account/test-support';\nimport { TARGET, TYPE } from '@emberclear/networking/models/message';\nimport { getService, stubService, waitUntil } from '@emberclear/test-helpers/test-support';\n\nimport type { Identity } from '@emberclear/local-account';\nimport type { Message } from '@emberclear/networking';\nimport type AutoResponder from '@emberclear/networking/services/messages/auto-responder';\n\nmodule('Unit | Service | messages/auto-responder', function (hooks) {\n  setupTest(hooks);\n  setupCurrentUser(hooks);\n  clearLocalStorage(hooks);\n\n  test('it exists', function (assert) {\n    let service = getService('messages/auto-responder');\n\n    assert.ok(service);\n  });\n\n  module('cameOnline', function () {\n    module('handling messages queued for resend', function () {\n      module('there are no pending messages', function (hooks) {\n        hooks.beforeEach(async function (assert) {\n          const store = getService('store');\n          const messages = await store.findAll('message');\n\n          assert.equal(messages.length, 0, 'there are no messages');\n        });\n\n        test('no messages are sent', async function (assert) {\n          assert.expect(1);\n\n          const service = getService('messages/auto-responder');\n          const somePerson = await createContact('some person');\n\n          stubService('messages/dispatcher', {\n            sendToUser: {\n              perform(/* _response: Message, _to: Identity */) {\n                assert.ok(false, 'this method should not get called');\n              },\n            },\n          });\n\n          service.cameOnline(somePerson);\n        });\n      });\n\n      module('there are pending messages', function (hooks) {\n        let somePerson: Identity;\n        let service: AutoResponder;\n\n        hooks.beforeEach(async function (assert) {\n          service = getService('messages/auto-responder');\n          somePerson = await createContact('some person');\n\n          const store = getService('store');\n          const me = getService('current-user');\n          let messages = await store.findAll('message');\n\n          assert.equal(messages.length, 0, 'there are no messages');\n\n          await store\n            .createRecord('message', {\n              to: somePerson.uid,\n              queueForResend: true,\n              sender: me.record,\n            })\n            .save();\n          await store\n            .createRecord('message', {\n              to: somePerson.uid,\n              queueForResend: true,\n              sender: me.record,\n            })\n            .save();\n\n          messages = await store.findAll('message');\n          assert.equal(messages.length, 2, 'there are 2 messages');\n\n          const pendingMessages = await store.query('message', {\n            queueForResend: true,\n            to: somePerson.uid,\n          });\n\n          assert.equal(pendingMessages.length, 2, 'there are 2 pending messages');\n        });\n\n        test('there are no longer any queued messages', async function (assert) {\n          assert.expect(6);\n\n          stubService('messages/dispatcher', {\n            sendToUser: {\n              perform(_response: Message, to: Identity) {\n                assert.equal(to.uid, somePerson.uid, `message sent to: ${somePerson.name}`);\n              },\n            },\n          });\n\n          await service.cameOnline(somePerson);\n\n          const store = getService('store');\n\n          await waitUntil(async () => {\n            let messages = await store.query('message', {\n              queueForResend: true,\n              to: somePerson.uid,\n            });\n\n            return messages.length === 0;\n          });\n\n          const messages = await store.query('message', {\n            queueForResend: true,\n            to: somePerson.uid,\n          });\n\n          assert.equal(messages.length, 0, 'there are no messages');\n        });\n      });\n    });\n  });\n\n  module('messageReceived', function () {\n    test('a delivery confirmation is built', async function (assert) {\n      assert.expect(5);\n\n      const me = getService('current-user');\n\n      await me.exists();\n\n      const store = getService('store');\n      const service = getService('messages/auto-responder');\n\n      const sender = await createContact('some user');\n      const receivedMessage = store.createRecord('message', {\n        id: uuid(),\n        sender,\n        body: 'test message',\n      });\n\n      stubService('messages/dispatcher', {\n        sendToUser: {\n          perform(response: Message, to: Identity) {\n            assert.equal(response.target, TARGET.MESSAGE);\n            assert.equal(response.type, TYPE.DELIVERY_CONFIRMATION);\n            assert.equal(response.to, receivedMessage.id);\n            assert.equal(response.sender, me.record);\n            assert.equal(to.publicKey, sender.publicKey);\n          },\n        },\n      });\n\n      service.messageReceived(receivedMessage);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/addons/networking/tests/unit/services/messages/handler-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { v4 as uuid } from 'uuid';\n\nimport {\n  attributesForContact,\n  clearLocalStorage,\n  setupCurrentUser,\n} from '@emberclear/local-account/test-support';\nimport { TARGET, TYPE } from '@emberclear/networking/models/message';\nimport { getService, getStore, stubService } from '@emberclear/test-helpers/test-support';\n\nmodule('Unit | Service | messages/handler', function (hooks) {\n  setupTest(hooks);\n  setupCurrentUser(hooks);\n  clearLocalStorage(hooks);\n\n  test('it exists', function (assert) {\n    let service = getService('messages/handler');\n\n    assert.ok(service);\n  });\n\n  module('handle', function () {\n    module('a chat message', function (hooks) {\n      hooks.beforeEach(async function () {\n        stubService('messages/auto-responder', {\n          messageReceived() {},\n          cameOnline() {},\n        });\n      });\n\n      test('the message is saved', async function (assert) {\n        const store = getStore();\n        const service = getService('messages/handler');\n        const me = getService('current-user');\n        const sender = await attributesForContact();\n\n        const before = await store.findAll('message');\n        const beforeCount = before.toArray().length;\n\n        await service.handle({\n          id: uuid(),\n          type: TYPE.CHAT,\n          target: TARGET.WHISPER,\n          to: me.record.uid,\n          ['time_sent']: new Date(),\n          client: 'tests',\n          ['client_version']: '0',\n          sender: {\n            uid: sender.id,\n            name: `user with id: ${sender.id}`,\n            location: '',\n          },\n          message: {\n            body: 'malformed, cleartext body',\n            contentType: 'is this used?',\n          },\n        });\n\n        const after = await store.findAll('message');\n\n        assert.equal(after.length, beforeCount + 1);\n      });\n    });\n\n    module('an emote message', function () {});\n\n    module('a delivery confirmation', function () {});\n\n    module('a disconnect message', function () {});\n\n    module('a ping', function () {});\n\n    module('an unknown type of message', function () {});\n  });\n});\n"
  },
  {
    "path": "client/web/addons/networking/tests/unit/services/messages/utils/-encryption-test.ts",
    "content": "// TODO:\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { module, test } from 'qunit';\n\nimport { generateAsymmetricKeys } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport {\n  decryptFromSocket,\n  encryptForSocket,\n} from '@emberclear/crypto/workers/crypto/utils/socket';\nimport { toHex } from '@emberclear/encoding/string';\nimport { build as toPayloadJson } from '@emberclear/networking/services/messages/-utils/builder';\n\nimport type { KeyPair } from '@emberclear/crypto';\n\nmodule('Integration | Send/Receive Encryption', function (hooks) {\n  let bob!: KeyPair;\n  let alice!: KeyPair;\n\n  hooks.beforeEach(async function () {\n    const bobKeys = await generateAsymmetricKeys();\n    const aliceKeys = await generateAsymmetricKeys();\n\n    bob = {\n      privateKey: bobKeys.privateKey,\n      publicKey: bobKeys.publicKey,\n    };\n\n    alice = {\n      privateKey: aliceKeys.privateKey,\n      publicKey: aliceKeys.publicKey,\n    };\n  });\n\n  test('round-trip encrypt-decrypt should return the same message', async function (assert) {\n    const message = {\n      body: 'hi',\n    } as any;\n\n    const payload = toPayloadJson(message, alice as any);\n    const encrypted = await encryptForSocket(payload, bob, alice);\n    const fakeSocketMessage = { message: encrypted, uid: toHex(alice.publicKey) };\n    const decrypted = await decryptFromSocket(fakeSocketMessage, bob.privateKey);\n\n    assert.equal(decrypted.message.body, 'hi');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/networking/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/networking/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"addon\" },\n    { \"path\": \"addon-test-support\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/networking/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n\nimport 'ember-concurrency-decorators';\nimport 'ember-concurrency-async';\nimport 'ember-concurrency-ts/async';\nimport 'ember-concurrency-test-waiter';\n\nimport '@emberclear/networking/type-support';\n"
  },
  {
    "path": "client/web/addons/networking/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/prism/README.md",
    "content": "extract dynamic loading of prism.js.\n\nThis will eliminate the need for ember-prism's configuration objects.\nAlso, maybe submit a PR to them?\n"
  },
  {
    "path": "client/web/addons/test-helpers/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/test-helpers/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/test-helpers/.eslintrc.js",
    "content": "const { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/test-helpers/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/test-helpers/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/test-helpers/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/test-helpers/.travis.yml",
    "content": "---\nlanguage: node_js\nnode_js:\n  # we recommend testing addons with the same minimum supported node version as Ember CLI\n  # so that your addon works for all apps\n  - \"10\"\n\ndist: xenial\n\naddons:\n  chrome: stable\n\ncache:\n  yarn: true\n\nenv:\n  global:\n    # See https://git.io/vdao3 for details.\n    - JOBS=1\n\nbranches:\n  only:\n    - master\n    # npm version tags\n    - /^v\\d+\\.\\d+\\.\\d+/\n\njobs:\n  fast_finish: true\n  allow_failures:\n    - env: EMBER_TRY_SCENARIO=ember-canary\n\n  include:\n    # runs linting and tests with current locked deps\n    - stage: \"Tests\"\n      name: \"Tests\"\n      script:\n        - yarn lint\n        - yarn test:ember\n\n    - stage: \"Additional Tests\"\n      name: \"Floating Dependencies\"\n      install:\n        - yarn install --no-lockfile --non-interactive\n      script:\n        - yarn test:ember\n\n    # we recommend new addons test the current and previous LTS\n    # as well as latest stable release (bonus points to beta/canary)\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.16\n    - env: EMBER_TRY_SCENARIO=ember-lts-3.20\n    - env: EMBER_TRY_SCENARIO=ember-release\n    - env: EMBER_TRY_SCENARIO=ember-beta\n    - env: EMBER_TRY_SCENARIO=ember-canary\n    - env: EMBER_TRY_SCENARIO=ember-default-with-jquery\n    - env: EMBER_TRY_SCENARIO=ember-classic\n\nbefore_install:\n  - curl -o- -L https://yarnpkg.com/install.sh | bash\n  - export PATH=$HOME/.yarn/bin:$PATH\n\nscript:\n  - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO\n"
  },
  {
    "path": "client/web/addons/test-helpers/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd test-helpers`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/test-helpers/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/test-helpers/README.md",
    "content": "test-helpers\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install test-helpers\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/get-service.ts",
    "content": "import { getContext } from '@ember/test-helpers';\n\nimport type { Registry } from '@ember/service';\nimport type { TestContext } from 'ember-test-helpers';\n\nexport function getService<K extends keyof Registry>(name: K): Registry[K] {\n  const { owner } = getContext() as TestContext;\n\n  const service = owner.lookup(`service:${name}`);\n\n  return service as Registry[K];\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/get-store.ts",
    "content": "import { getService } from './get-service';\n\nimport type StoreService from '@ember-data/store';\n\nexport function getStore(): StoreService {\n  return getService('store');\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/refresh.ts",
    "content": "import { currentURL, getContext, setupContext, teardownContext } from '@ember/test-helpers';\n\nimport { visit } from './visit';\n\nexport async function refresh<T = unknown>(mocking: () => T | Promise<T>) {\n  const url = currentURL();\n  const ctx = getContext();\n\n  await teardownContext(ctx);\n  await setupContext(ctx);\n  await mocking();\n  await visit(url);\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/setup-router.ts",
    "content": "import { getContext } from '@ember/test-helpers';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nexport function setupRouter(hooks: NestedHooks) {\n  hooks.beforeEach(function () {\n    let { owner } = getContext() as TestContext;\n\n    // eslint-disable-next-line ember/no-private-routing-service\n    owner.lookup('router:main').setupRouter();\n  });\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/stub-service.ts",
    "content": "import Service from '@ember/service';\nimport { getContext } from '@ember/test-helpers';\n\nimport type { Registry } from '@ember/service';\nimport type { TestContext } from 'ember-test-helpers';\n\nexport const stubService = (name: keyof Registry, hash = {}) => {\n  let stubbedService;\n\n  // TODO: need to be able to use an extended service that uses services. :)\n  if (hash instanceof Function) {\n    stubbedService = hash;\n  } else {\n    stubbedService = Service.extend(hash);\n  }\n\n  let { owner } = getContext() as TestContext;\n  let serviceName = `service:${name}`;\n\n  owner.register(serviceName, stubbedService);\n};\n\nexport default stubService;\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/visit.ts",
    "content": "import { visit as dangerousVisit } from '@ember/test-helpers';\n\nexport async function visit(url: string) {\n  try {\n    await dangerousVisit(url);\n  } catch (e) {\n    if (!e.message.includes('TransitionAborted')) {\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/-private/wait-until.ts",
    "content": "/**\n * @ember/test-helpers' waitUntil does not take a Promise for the callback\n */\nexport async function waitUntil(func: () => Promise<boolean>, timeoutMs = 500) {\n  let interval: NodeJS.Timeout;\n\n  const timeout = new Promise((_resolve, reject) => {\n    const id = setTimeout(() => {\n      clearTimeout(id);\n      clearInterval(interval);\n      reject(`Timed out after ${timeoutMs} ms.`);\n    }, timeoutMs);\n  });\n\n  let startTime = new Date();\n\n  return Promise.race([\n    new Promise((resolve, reject) => {\n      let interval = setInterval(async () => {\n        if (new Date().getTime() - startTime.getTime() > 500) {\n          clearInterval(interval);\n          reject(`Timed out after ${timeoutMs}`);\n        }\n\n        let result = false;\n\n        try {\n          result = await func();\n        } catch (e) {\n          // ignored\n        }\n\n        if (result) {\n          clearInterval(interval);\n          resolve(undefined);\n        }\n      }, 10);\n    }),\n    timeout,\n  ]);\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/index.ts",
    "content": "export { getService } from './-private/get-service';\nexport { getStore } from './-private/get-store';\nexport { refresh } from './-private/refresh';\nexport { setupRouter } from './-private/setup-router';\nexport { stubService } from './-private/stub-service';\nexport { visit } from './-private/visit';\nexport { waitUntil } from './-private/wait-until';\n"
  },
  {
    "path": "client/web/addons/test-helpers/addon-test-support/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/test-support\",\n    \"paths\": {\n      \"@emberclear/test-helpers\": [\"../declarations\"],\n      \"@emberclear/test-helpers/*\": [\"../declarations/*\"],\n      \"@emberclear/test-helpers/test-support\": [\".\"],\n      \"@emberclear/test-helpers/test-support/*\": [\"./*\"]\n    }\n  },\n  \"references\": [\n    /* { \"path\": \"../addon\" } */\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/app/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n      {\n        name: 'ember-default-with-jquery',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'jquery-integration': true,\n          }),\n        },\n        npm: {\n          devDependencies: {\n            '@ember/jquery': '^1.1.0',\n          },\n        },\n      },\n      {\n        name: 'ember-classic',\n        env: {\n          EMBER_OPTIONAL_FEATURES: JSON.stringify({\n            'application-template-wrapper': true,\n            'default-async-observers': false,\n            'template-only-glimmer-components': false,\n          }),\n        },\n        npm: {\n          ember: {\n            edition: 'classic',\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    // Add options here\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/index.js",
    "content": "'use strict';\n\nmodule.exports = {\n  name: require('./package').name,\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/package.json",
    "content": "{\n  \"name\": \"@emberclear/test-helpers\",\n  \"version\": \"0.0.0\",\n  \"description\": \"The default blueprint for ember-cli addons.\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/tracked-local-storage\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-data\": \"3.16.14\",\n    \"@types/ember-data__model\": \"3.16.2\",\n    \"@types/ember-data__store\": \"3.16.1\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-auto-import\": \"^1.11.3\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.23.0\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tests/test-helper.js",
    "content": "import { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/test-helpers/tests/unit/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/test-helpers/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    /* { \"path\": \"addon\" }, */\n    { \"path\": \"addon-test-support\" }\n    /* { \"path\": \"tests\" } */\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/test-helpers/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n"
  },
  {
    "path": "client/web/addons/test-helpers/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.eslintrc.js",
    "content": "const { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.npmignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n\n# misc\n/.bowerrc\n/.editorconfig\n/.ember-cli\n/.env*\n/.eslintignore\n/.eslintrc.js\n/.git/\n/.gitignore\n/.template-lintrc.js\n/.travis.yml\n/.watchmanconfig\n/bower.json\n/config/ember-try.js\n/CONTRIBUTING.md\n/ember-cli-build.js\n/testem.js\n/tests/\n/yarn.lock\n.gitkeep\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Installation\n\n* `git clone <repository-url>`\n* `cd tracked-local-storage`\n* `yarn install`\n\n## Linting\n\n* `yarn lint:hbs`\n* `yarn lint:js`\n* `yarn lint:js --fix`\n\n## Running tests\n\n* `ember test` – Runs the test suite on the current Ember version\n* `ember test --server` – Runs the test suite in \"watch mode\"\n* `ember try:each` – Runs the test suite against multiple Ember versions\n\n## Running the dummy application\n\n* `ember serve`\n* Visit the dummy application at [http://localhost:4200](http://localhost:4200).\n\nFor more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/README.md",
    "content": "tracked-local-storage\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.16 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install tracked-local-storage\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/addon/index.ts",
    "content": "import { assert } from '@ember/debug';\nimport { get, notifyPropertyChange } from '@ember/object';\n\ninterface WhyCantTSGetDecoratorsRight<InitializedValue> {\n  initializer?: () => InitializedValue;\n}\n\n/**\n * Kinda Pre-stage 2 decorator.\n *\n * Will need to update when decorators hit stage 3\n *\n */\n// TODO: figure out a better default type for Klass\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport function inLocalStorage<T = boolean, Klass extends Object = Object>(\n  target: Klass,\n  propertyKey: keyof Klass,\n  // descriptor is undefined for properties\n  // it's only available on methods and such\n  descriptor?: WhyCantTSGetDecoratorsRight<T>\n): void /* TS says the return value is ignored... idk if I believe it */ {\n  let targetName = target.constructor.name;\n\n  assert(`@inLocalStorage is only usable on class properties`, descriptor);\n\n  let { initializer } = descriptor;\n\n  const newDescriptor: PropertyDescriptor = {\n    configurable: true,\n    enumerable: true,\n    get: function (this: Klass): T {\n      let key = `${targetName}-${propertyKey}`;\n      const lsValue = localStorage.getItem(key);\n      const value = (lsValue && JSON.parse(lsValue))?.value || initializer?.();\n\n      // Entagle with tracking system\n      // This is a bit of a hack and the returned value doesn't matter\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      get(this, key as any);\n\n      return value;\n    },\n    set: function (value: T) {\n      const key = `${targetName}-${propertyKey}`;\n      const lsValue = JSON.stringify({ value });\n\n      localStorage.setItem(key, lsValue);\n\n      // this is required to dirty the change tracking system\n      notifyPropertyChange(this, key);\n    },\n  };\n\n  // I think TypeScript is wrong when it comes to decorators...\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return newDescriptor as any;\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"ember-tracked-local-storage\": [\".\"],\n      \"ember-tracked-local-storage/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/config/ember-try.js",
    "content": "'use strict';\n\nconst getChannelURL = require('ember-source-channel-url');\n\nmodule.exports = async function () {\n  return {\n    useYarn: true,\n    scenarios: [\n      {\n        name: 'ember-lts-3.16',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.16.0',\n          },\n        },\n      },\n      {\n        name: 'ember-lts-3.20',\n        npm: {\n          devDependencies: {\n            'ember-source': '~3.20.5',\n          },\n        },\n      },\n      {\n        name: 'ember-release',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('release'),\n          },\n        },\n      },\n      {\n        name: 'ember-beta',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('beta'),\n          },\n        },\n      },\n      {\n        name: 'ember-canary',\n        npm: {\n          devDependencies: {\n            'ember-source': await getChannelURL('canary'),\n          },\n        },\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/ember-cli-build.js",
    "content": "'use strict';\n\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/index.js",
    "content": "'use strict';\n\nmodule.exports = {\n  name: require('./package').name,\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/package.json",
    "content": "{\n  \"name\": \"ember-tracked-local-storage\",\n  \"version\": \"0.0.0\",\n  \"description\": \"stores the return value of getters / properties in local-storage\",\n  \"keywords\": [\n    \"ember-addon\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/tracked-local-storage\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint . --ext js,ts\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"test:try-one\": \"ember try:one\",\n    \"test:ember-compatibility\": \"ember try:each\"\n  },\n  \"dependencies\": {\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"^2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"^3.0.0\",\n    \"ember-auto-import\": \"^1.11.3\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"^3.2.0\",\n    \"ember-cli-inject-live-reload\": \"^2.1.0\",\n    \"ember-cli-sri\": \"^2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"^1.1.3\",\n    \"ember-export-application-global\": \"^2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"^0.1.6\",\n    \"ember-qunit\": \"^4.6.0\",\n    \"ember-resolver\": \"^8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-source-channel-url\": \"^3.0.0\",\n    \"ember-try\": \"^1.4.0\",\n    \"loader.js\": \"^4.7.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"configPath\": \"tests/dummy/config\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport config from 'dummy/config/environment';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'dummy/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/styles/app.css",
    "content": ""
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/app/templates/application.hbs",
    "content": "{{outlet}}"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.23.0\",\n      \"blueprints\": [\n        {\n          \"name\": \"addon\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-addon-output\",\n          \"codemodsSource\": \"ember-addon-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": [\n            \"--yarn\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = Boolean(process.env.CI);\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/test-helper.ts",
    "content": "import { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"ember-tracked-local-storage\": [\"../declarations\"],\n      \"ember-tracked-local-storage/*\": [\"../declarations/*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\n    \".\",\n    \"../types\"\n  ],\n  \"references\": [\n    { \"path\": \"../addon\" }\n    /* { \"path\": \"../addon-test-support\" } */\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tests/unit/in-local-storage-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\n/**\n * When using @inLocalStorage,\n * anything stored in there from this decorator is considered private.\n */\nmodule('Unit | inLocalStorage', function (hooks) {\n  hooks.afterEach(function () {\n    localStorage.clear();\n  });\n\n  test('starts with no initial value', function (assert) {\n    class Foo {\n      @inLocalStorage bar?: number;\n    }\n\n    let foo = new Foo();\n\n    assert.equal(localStorage.getItem('Foo-bar'), null);\n\n    foo.bar = 2;\n\n    assert.equal(localStorage.getItem('Foo-bar'), storedValueOf(2));\n  });\n\n  test('starts with a pre-existing value', function (assert) {\n    localStorage.setItem('Foo-bar', storedValueOf(2));\n\n    class Foo {\n      @inLocalStorage bar?: number;\n    }\n\n    let foo = new Foo();\n\n    assert.equal(foo.bar, 2);\n\n    foo.bar = 3;\n\n    assert.equal(localStorage.getItem('Foo-bar'), storedValueOf(3));\n\n    assert.equal(foo.bar, 3);\n  });\n\n  test('handles any data type that is serializable via JSON', function (assert) {\n    class Foo {\n      @inLocalStorage num?: number;\n      @inLocalStorage str?: string;\n      @inLocalStorage bool?: boolean;\n      @inLocalStorage numberArray?: number[];\n      @inLocalStorage stringArray?: string[];\n      @inLocalStorage complex?: unknown;\n    }\n\n    let foo: Foo | undefined;\n\n    foo = new Foo();\n\n    let fixtureData = {\n      num: 1,\n      str: 'hi',\n      bool: true,\n      numberArray: [1, 3],\n      stringArray: ['hello', 'there'],\n      complex: {\n        arr: [1, 2, 'string', true, false],\n        nested: {\n          obj: true,\n        },\n      },\n    };\n\n    foo.num = fixtureData.num;\n    foo.str = fixtureData.str;\n    foo.bool = fixtureData.bool;\n    foo.numberArray = fixtureData.numberArray;\n    foo.stringArray = fixtureData.stringArray;\n    foo.complex = fixtureData.complex;\n\n    assert.equal(localStorage.getItem('Foo-num'), storedValueOf(fixtureData.num));\n    assert.equal(localStorage.getItem('Foo-str'), storedValueOf(fixtureData.str));\n    assert.equal(localStorage.getItem('Foo-bool'), storedValueOf(fixtureData.bool));\n    assert.equal(localStorage.getItem('Foo-numberArray'), storedValueOf(fixtureData.numberArray));\n    assert.equal(localStorage.getItem('Foo-stringArray'), storedValueOf(fixtureData.stringArray));\n    assert.equal(localStorage.getItem('Foo-complex'), storedValueOf(fixtureData.complex));\n\n    // Get a new instance so that the loading logic needs to apply\n    foo = undefined;\n    foo = new Foo();\n\n    assert.equal(foo.num, fixtureData.num);\n    assert.equal(foo.str, fixtureData.str);\n    assert.equal(foo.bool, fixtureData.bool);\n    assert.deepEqual(foo.numberArray, fixtureData.numberArray);\n    assert.deepEqual(foo.stringArray, fixtureData.stringArray);\n    assert.deepEqual(foo.complex, fixtureData.complex);\n  });\n});\n\nfunction storedValueOf(value: unknown) {\n  return JSON.stringify({ value });\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"addon\" },\n    /* { \"path\": \"addon-test-support\" }, */\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/tracked-local-storage/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/tsconfig.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./crypto\" },\n    { \"path\": \"./encoding\" },\n    { \"path\": \"./local-account\" },\n    { \"path\": \"./networking\" },\n    { \"path\": \"./test-helpers\" },\n    { \"path\": \"./tracked-local-storage\" },\n    { \"path\": \"./ui\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/ui/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/addons/ui/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false,\n  \"liveReload\": true,\n  \"usePods\": false\n}\n"
  },
  {
    "path": "client/web/addons/ui/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/ui/.eslintrc.js",
    "content": "const { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/addons/ui/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/addons/ui/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/addons/ui/README.md",
    "content": "ui\n==============================================================================\n\n[Short description of the addon.]\n\n\nCompatibility\n------------------------------------------------------------------------------\n\n* Ember.js v3.12 or above\n* Ember CLI v2.13 or above\n* Node.js v10 or above\n\n\nInstallation\n------------------------------------------------------------------------------\n\n```\nember install ui\n```\n\n\nUsage\n------------------------------------------------------------------------------\n\n[Longer description of how to use the addon in apps.]\n\n\nContributing\n------------------------------------------------------------------------------\n\nSee the [Contributing](CONTRIBUTING.md) guide for details.\n\n\nLicense\n------------------------------------------------------------------------------\n\nThis project is licensed under the [MIT License](LICENSE.md).\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/backdrop.hbs",
    "content": "<div\n  ...attributes\n  role='button'\n  class='\n   full-overlay\n   {{if @isInvisible 'invisible'}}\n   {{if @isActive 'active'}}\n  '\n  {{on 'click' @onClick}}\n/>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/collapsible/icon.hbs",
    "content": "<span class='flex-row justify-content-center algin-items-center'...attributes>\n  {{#if @isOpen}}\n    <FaIcon @icon='angle-down' />\n  {{else}}\n    <FaIcon @icon='angle-right' />\n  {{/if}}\n</span>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/collapsible/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { setComponentTemplate } from '@ember/component';\nimport { action } from '@ember/object';\nimport { hbs } from 'ember-cli-htmlbars';\n\nclass Collapsible extends Component {\n  @tracked isOpen = true;\n\n  @action\n  toggle() {\n    this.isOpen = !this.isOpen;\n  }\n}\n\nexport default setComponentTemplate(\n  hbs`\n  {{yield this.isOpen\n    this.toggle\n    (component 'collapsible/icon')\n  }}\n`,\n  Collapsible\n);\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/dropdown/index.hbs",
    "content": "<div\n  data-test-dropdown\n  class='\n    dropdown\n    {{if (eq @dir 'up') 'dropdown-top'}}\n    {{if (eq @dir 'left') 'dropdown-left'}}\n    {{if this.isOpen 'active'}}\n  '\n  ...attributes\n>\n\n  {{yield this.toggleMenu to='trigger'}}\n\n  {{#if this.isOpen}}\n    <div\n      class='no-select full-overlay invisible'\n      role='button'\n      {{on 'click' this.toggleMenu}}\n      {{on-key 'Escape' (prevent-default this.toggleMenu)}}\n    />\n\n  {{/if}}\n\n  <div class='dropdown-menu'>\n    {{yield this.closeMenu to='content'}}\n  </div>\n</div>\n\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/dropdown/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\n\ninterface Args {\n  buttonIcon?: string;\n  buttonText?: string;\n  dir?: string;\n}\n\nexport default class Dropdown extends Component<Args> {\n  @tracked isOpen = false;\n\n  get hasButtonText() {\n    return Boolean(this.args.buttonText);\n  }\n\n  get hasButtonIcon() {\n    return Boolean(this.args.buttonIcon);\n  }\n\n  @action\n  toggleMenu() {\n    this.isOpen = !this.isOpen;\n  }\n\n  @action\n  closeMenu() {\n    this.isOpen = false;\n  }\n}\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/ellipsis-loader.hbs",
    "content": "<span class='ellipsis-loader' ...attributes>\n  <span>•</span>\n  <span>•</span>\n  <span>•</span>\n</span>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/external-link.hbs",
    "content": "{{!-- make a required attribute modifier? --}}\n<a href='' target='_blank' rel='noopener noreferrer' ...attributes>\n  {{yield}}\n</a>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/field.ts",
    "content": "import Component from '@glimmer/component';\nimport { setComponentTemplate } from '@ember/component';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport { v4 as uuid } from 'uuid';\n\nclass Field extends Component {\n  id = uuid();\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <div class='input-field' ...attributes>\n\n    {{#if @label}}\n      <label\n        class={{if @hidden 'visually-hidden'}}\n        for={{this.id}}\n      >\n        {{@label}}\n      </label>\n    {{/if}}\n\n    {{yield this.id}}\n  </div>\n  `,\n  Field\n);\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/focus-card.hbs",
    "content": "<div data-test-focus-card class='row row-center'>\n  <div class='col-12 col-auto flex-center' ...attributes>\n    <h1 class='title'>{{@title}}</h1>\n    {{yield}}\n  </div>\n</div>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/hamburger-button.hbs",
    "content": "<button\n  data-test-hamburger-toggle\n  type='button'\n  class='button-link navbar-burger burger {{@class}} {{if @isActive 'is-active'}}'\n  aria-expanded='false'\n  {{on 'click' @onClick}}\n>\n  <FaIcon @icon='bars' @prefix='fas' />\n</button>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/hover-tip.hbs",
    "content": "<span\n  class='\n    hover-tip\n    has-text-grey-darker has-background-white-ter\n    {{@class}}\n    {{@animationClasses}}'\n>\n  {{yield}}\n</span>\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/media-info-card.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { setComponentTemplate } from '@ember/component';\nimport { action } from '@ember/object';\nimport { hbs } from 'ember-cli-htmlbars';\n\nclass MediaInfoCard extends Component {\n  @tracked isCollapsed = false;\n\n  get isOpen() {\n    return !this.isCollapsed;\n  }\n\n  @action\n  toggleShow() {\n    this.isCollapsed = !this.isCollapsed;\n  }\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <div class='grid:column justify:start'>\n    <button\n      type='button'\n      class='button:as-link w:2x h:2x'\n      {{on 'click' this.toggleShow}}\n    >\n      <FaIcon @icon='angle-down' class='animated {{if this.isCollapsed \"rotate:180\"}}'/>\n    </button>\n\n    <section class='align-self:center'>\n      <header class='card-header line-height:2x'>\n        {{yield to='header'}}\n      </header>\n\n      {{#if this.isOpen}}\n        {{yield to='content'}}\n      {{/if}}\n    </section>\n  </div>\n  `,\n  MediaInfoCard\n);\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/modal.hbs",
    "content": "\n<div\n  ...attributes\n  data-test-modal\n  aria-modal\n  aria-hidden={{not @isActive}}\n  class='modal {{if @isActive 'is-active'}}'\n  {{focus-trap isActive=(and @isActive (not @disableFocusTrap))}}\n>\n\n  <div\n    data-test-modal-content\n    class='modal-content'\n  >\n    {{#if @isActive}}\n      {{yield}}\n\n      {{on-key 'Escape' (prevent-default @close)}}\n    {{/if}}\n  </div>\n\n\n  <Backdrop\n    data-test-modal-backdrop\n    @isActive={{true}} @onClick={{@close}}\n  />\n\n</div>\n\n\n"
  },
  {
    "path": "client/web/addons/ui/addon/components/switch.ts",
    "content": "import Component from '@glimmer/component';\nimport { setComponentTemplate } from '@ember/component';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport { v4 as uuid } from 'uuid';\n\nclass Field extends Component {\n  id = uuid();\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <span class='switch'>\n    <input\n      ...attributes\n      type='checkbox'\n      id={{this.id}}\n      checked={{@value}}\n    >\n\n    <label for={{this.id}}>{{@label}}</label>\n  </span>\n`,\n  Field\n);\n"
  },
  {
    "path": "client/web/addons/ui/addon/helpers/and.ts",
    "content": "import { helper } from '@ember/component/helper';\n\nexport function and(params: unknown[] /*, hash*/) {\n  return params[0] && params[1];\n}\n\nexport default helper(and);\n"
  },
  {
    "path": "client/web/addons/ui/addon/helpers/eq.ts",
    "content": "import { helper } from '@ember/component/helper';\n\nexport function eq(params: unknown[] /*, hash*/) {\n  return params[0] === params[1];\n}\n\nexport default helper(eq);\n"
  },
  {
    "path": "client/web/addons/ui/addon/helpers/not.ts",
    "content": "// https://github.com/DockYard/ember-composable-helpers/blob/master/addon/helpers/toggle.js\nimport { helper as buildHelper } from '@ember/component/helper';\n\nexport function not(params: unknown[] /*, hash*/) {\n  return !params[0];\n}\n\nexport default buildHelper(not);\n"
  },
  {
    "path": "client/web/addons/ui/addon/helpers/prevent-default.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\ntype PositionalArgs = [(...args: unknown[]) => void];\n\nexport function preventDefault([fn]: PositionalArgs /*, hash*/) {\n  return (...args: unknown[]) => {\n    const firstArg = args[0];\n\n    if (firstArg instanceof Event) {\n      firstArg.preventDefault();\n    }\n\n    return fn(...args);\n  };\n}\n\nexport default buildHelper(preventDefault);\n"
  },
  {
    "path": "client/web/addons/ui/addon/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/ui\": [\".\"],\n      \"@emberclear/ui/*\": [\"./*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../../libraries/questionably-typed\" },\n    { \"path\": \"../../../addons/tracked-local-storage\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/ui/addon-test-support/key-events.ts",
    "content": "import { triggerKeyEvent } from '@ember/test-helpers';\n\nimport type { KeyModifiers } from '@ember/test-helpers/dom/trigger-key-event';\n\nexport async function keyPressFor(\n  selector: string | Element,\n  key: string | number,\n  modifiers?: KeyModifiers\n) {\n  await triggerKeyEvent(selector, 'keydown', key, modifiers);\n  await triggerKeyEvent(selector, 'keyup', key, modifiers);\n  await triggerKeyEvent(selector, 'keypress', key, modifiers);\n}\n\nexport function keyEvents(selector: string) {\n  return {\n    pressEscape() {\n      return keyPressFor(selector, 'Escape');\n    },\n    pressEnter() {\n      return keyPressFor(selector, 'Enter');\n    },\n    pressTab() {\n      return keyPressFor(selector, 'Tab');\n    },\n    pressUp() {\n      return keyPressFor(selector, 'ArrowUp');\n    },\n    pressDown() {\n      return keyPressFor(selector, 'ArrowDown');\n    },\n    pressLeft() {\n      return keyPressFor(selector, 'ArrowLeft');\n    },\n    pressRight() {\n      return keyPressFor(selector, 'ArrowRight');\n    },\n  };\n}\n"
  },
  {
    "path": "client/web/addons/ui/addon-test-support/page-objects.ts",
    "content": "import { clickable, hasClass, property, text } from 'ember-cli-page-object';\n\nimport { keyEvents } from './key-events';\n\nexport const switchWrapper = {\n  scope: '.switch',\n\n  label: text('label'),\n\n  isChecked: property('checked', 'input'),\n  check: clickable('input'),\n};\n\nexport const switchInput = {\n  isChecked: property('checked'),\n  check: clickable(),\n};\n\nexport const modal = {\n  ...keyEvents('[data-test-modal-content]'),\n  modalContent: {\n    scope: '[data-test-modal-content]',\n  },\n  backdrop: {\n    scope: '[data-test-modal-backdrop]',\n    click: clickable(),\n  },\n};\n\nexport const dropdown = {\n  scope: '[data-test-dropdown]',\n  isOpen: hasClass('active'),\n  toggle: clickable('.dropdown-trigger'),\n  backdrop: {\n    scope: '.full-overlay',\n  },\n};\n"
  },
  {
    "path": "client/web/addons/ui/addon-test-support/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/test-support\",\n    \"paths\": {\n      \"@emberclear/ui\": [\"../declarations\"],\n      \"@emberclear/ui/*\": [\"../declarations/*\"],\n      \"@emberclear/ui/test-support\": [\".\"],\n      \"@emberclear/ui/test-support/*\": [\"./*\"]\n    }\n  },\n  \"references\": [\n    { \"path\": \"../addon\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/components/backdrop.js",
    "content": "export { default } from '@emberclear/ui/components/backdrop';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/collapsible/icon.js",
    "content": "export { default } from '@emberclear/ui/components/collapsible/icon';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/collapsible.js",
    "content": "export { default } from '@emberclear/ui/components/collapsible';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/dropdown.js",
    "content": "export { default } from '@emberclear/ui/components/dropdown';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/ellipsis-loader.js",
    "content": "export { default } from '@emberclear/ui/components/ellipsis-loader';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/external-link.js",
    "content": "export { default } from '@emberclear/ui/components/external-link';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/field.js",
    "content": "export { default } from '@emberclear/ui/components/field';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/focus-card.js",
    "content": "export { default } from '@emberclear/ui/components/focus-card';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/hamburger-button.js",
    "content": "export { default } from '@emberclear/ui/components/hamburger-button';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/hover-tip.js",
    "content": "export { default } from '@emberclear/ui/components/hover-tip';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/media-info-card.js",
    "content": "export { default } from '@emberclear/ui/components/media-info-card';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/modal.js",
    "content": "export { default } from '@emberclear/ui/components/modal';\n"
  },
  {
    "path": "client/web/addons/ui/app/components/switch.js",
    "content": "export { default } from '@emberclear/ui/components/switch';\n"
  },
  {
    "path": "client/web/addons/ui/app/helpers/and.js",
    "content": "export { and, default } from '@emberclear/ui/helpers/and';\n"
  },
  {
    "path": "client/web/addons/ui/app/helpers/eq.js",
    "content": "export { default, eq } from '@emberclear/ui/helpers/eq';\n"
  },
  {
    "path": "client/web/addons/ui/app/helpers/not.js",
    "content": "export { default, not } from '@emberclear/ui/helpers/not';\n"
  },
  {
    "path": "client/web/addons/ui/app/helpers/prevent-default.js",
    "content": "export { default } from '@emberclear/ui/helpers/prevent-default';\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/animation.css",
    "content": ".animated {\n  transition-duration: 0.2s;\n}\n\n.rotate\\:180 {\n  transform: rotateZ(180deg);\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/buttons.css",
    "content": ".button\\:as-link {\n  border: none;\n  padding: 0;\n  background-color: transparent;\n  border-color: transparent;\n  box-shadow: none;\n  color: var(--link-color);\n\n  &:hover {\n    text-decoration: var(--link-text-decoration);\n    color: var(--link-color-hover);\n  }\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/defaults.css",
    "content": "* {\n  box-sizing: border-box;\n}\n\nhtml {\n  overflow-x: hidden;\n\n  & body,\n  & .ember-application {\n    overflow: hidden;\n\n    /* overflow-x: hidden; */\n  }\n}\n\n/* either on body or some div during testing */\n.ember-application {\n  margin-top: var(--top-nav-height);\n  min-height: calc(100vh - var(--top-nav-height));\n}\n\nhr {\n  margin: var(--grid-gap) 0;\n\n  &.vertical {\n    margin: 0 var(--grid-gap);\n    border-top: none;\n    border-left: 1px solid var(--hr-border-color);\n    width: 1px;\n    height: 100%;\n  }\n}\n\nbody > main > section {\n  padding: var(--grid-gap);\n  padding-bottom: calc(var(--top-nav-height) + var(--grid-gap));\n\n  & > .center-self {\n    /* kinda */\n    align-self: center;\n    justify-self: center;\n  }\n}\n\nbutton {\n  /* check scan button if changing this */\n\n  /* display: grid; */\n  & svg {\n    align-self: center;\n  }\n}\n\na {\n  cursor: pointer;\n\n  & svg + span,\n  & span + svg {\n    margin-left: var(--grid-gap);\n  }\n}\n\na:hover {\n  text-decoration: none;\n}\n\na .icon,\nbutton .icon {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\npre.wrap {\n  white-space: inherit;\n}\n\npre {\n  user-select: text;\n  touch-action: unset;\n}\n\nsvg.svg-inline--fa {\n  width: 1rem;\n  height: 1rem;\n  line-height: 1.5rem;\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/layout.css",
    "content": ".grid {\n  @apply --grid;\n}\n\n/* remove this */\n.grid-column {\n  @apply --column-grid;\n}\n\n.grid\\:column {\n  @apply --column-grid;\n}\n\n.grid\\:col\\:2 {\n  grid-template-columns: 1fr 1fr;\n}\n\n.justify\\:around {\n  justify-content: space-around;\n}\n\n.grid-autoflow {\n  grid-template-columns: repeat(auto-fit, minmax(186px, 1fr));\n}\n\n.justify\\:start {\n  justify-content: start;\n}\n\n.align-self\\:center {\n  align-self: center;\n}\n\n.flex-center {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/mixins.css",
    "content": ":root {\n  --font-system:\n    -apple-system,\n    system-ui,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    'Helvetica Neue',\n    Ubuntu,\n    Arial,\n    sans-serif;\n\n  --grid {\n    display: grid;\n    grid-gap: var(--grid-gap);\n  }\n\n  --column-grid {\n    display: grid;\n    grid-auto-flow: column;\n    grid-gap: var(--grid-gap);\n  }\n\n  --grid-space-between {\n    justify-content: space-between;\n    align-items: center;\n  }\n\n  --grid-stretch {\n    justify-content: stretch;\n    align-items: center;\n  }\n\n  --no-select {\n    -webkit-tap-highlight-color: transparent;\n    -webkit-touch-callout: none;\n    -webkit-user-select: none;\n    -khtml-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n  }\n\n  --transition-all {\n    transition: all 0.2s ease-in-out;\n\n    /* transition: all 0.2s cubic-bezier(0.6, 0, 0.735, 0.045); */\n  }\n\n  --transition-touch {\n    transition: all 0.1s linear;\n  }\n\n  --button-as-link {\n    height: unset;\n    line-height: unset;\n    text-align: unset;\n    display: unset;\n    border: none;\n    padding: 0;\n    background: none;\n    color: var(--link-color);\n\n    &:hover {\n      background: none;\n      color: var(--link-color-hover);\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/shoelace-overrides.css",
    "content": ".dropdown {\n  @apply --no-select;\n}\n\n.dropdown .dropdown-trigger {\n  display: block;\n}\n\n.dropdown .dropdown-menu {\n  box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1);\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/sizes.css",
    "content": ".line-height\\:2x {\n  line-height: 2rem;\n}\n\n.w\\:50 {\n  width: 50%;\n}\n\n.w\\:2x {\n  width: 2rem;\n}\n\n.h\\:2x {\n  height: 2rem;\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/spacing.css",
    "content": ".mt-4 {\n  margin-top: 1rem;\n}\n\n.mb-4 {\n  margin-top: 1rem;\n}\n\n.pt-4 {\n  padding-top: 1rem;\n}\n\n.list\\:styleless {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/themes/default.css",
    "content": ":root {\n  /* functional bits */\n  --sidenav-width: 300px;\n\n  /* colors */\n\n  /* NOTE: luminosity of border colors is half of background color */\n\n  /* TODO: switch all colors to HSL */\n  --color-purple: hsl(253, 84%, 53%);\n  --color-red: hsl(348, 100%, 46%);\n  --color-green: hsl(144, 100%, 33%);\n  --color-blue: hsl(208, 100%, 43%);\n  --color-yellow: hsl(28, 100%, 55%);\n\n  /* grays */\n  --color-gray: hsl(240, 14%, 23%);\n  --color-medium-gray: hsl(0, 0%, 33%);\n  --color-medium-light-gray: hsl(0, 0%, 76%);\n  --color-light-gray: hsl(0, 0%, 94%);\n\n  /* states */\n  --state-primary: var(--color-purple);\n  --state-info: var(--color-blue);\n  --state-success: var(--color-green);\n  --state-warning: var(--color-yellow);\n\n  /* shadows */\n  --shadow: 0 0 2px 2px rgba(0, 0, 0, 0.2);\n  --shadow-lg:\n    0 0 4px rgba(0, 0, 0, 0.1),\n    0 4px 16px rgba(0, 0, 0, 0.1),\n    0 16px 64px rgba(0, 0, 0, 0.1);\n\n  /* grid */\n  --grid-gap: 0.75rem;\n  --grid-gap-small: 0.5rem;\n  --grid-gap-tiny: 0.25rem;\n\n  /* the rest of the things */\n  --link-color: var(--state-dark);\n  --placeholder-color: var(--color-medium-light-gray);\n  --body-color-muted: var(--color-medium-light-gray);\n  --hint-color: var(--color-medium-gray);\n\n  /* top navigation / titlebar? */\n  --top-nav-bg: var(--state-light);\n  --top-nav-shadow: var(--shadow);\n  --top-nav-height: 3.125rem; /* 50px */\n  --top-nav-inverted-color: var(--state-light);\n\n  /* sidebar */\n  --sidebar-bg-color: var(--state-light);\n  --sidebar-nav-active-color: var(--color-light-gray);\n  --sidebar-hint-text: var(--hint-color);\n  --sidebar-hr-border-color: var(--hr-border-color);\n  --sidebar-footer-shadow-opacity: 0.2;\n\n  /* modal */\n  --modal-overlay-color: rgba(0, 0, 0, 0.12);\n  --modal-content-bg: var(--state-light);\n  --modal-shadow: var(--shadow-lg);\n\n  /* search results / modal? */\n  --search-result-text-color: var(--color-gray);\n  --search-result-hover-bg: var(--color--gray);\n  --search-result-border-color: var(--color-gray);\n  --search-divider-color: var(--color-light-gray);\n\n  /* key */\n  --key-color: var(--color-light-gray);\n  --key-bg-color: #72767d;\n  --key-border-color: #202225;\n  --key-shadow-color: rgba(35, 39, 42, 0.6);\n\n  /* chat */\n  --chat-entry-bg-color: #f3f3f3;\n  --chat-entry-line-height: 1.5rem;\n  --chat-message-hover-color: #f8f8f8;\n  --chat-message-unread-bg-color: #f1f1fa;\n  --chat-message-sender-name-incoming: var(--body-color);\n  --chat-message-sender-name-outgoing: var(--body-color);\n  --chat-message-sent-at: var(--color-medium-gray);\n  --chat-new-messages-notice-color: var(--state-light);\n  --chat-new-messages-notice-bg-color: var(--state-secondary);\n\n  /* alerts */\n  --alert-padding-y: 0.5rem;\n  --alert-padding-x: 0.5rem;\n  --alert-bg-color: var(--state-primary);\n  --alert-color: #fff;\n  --alert-border-color: hsl(253, 84%, 25%);\n\n  /* success alerts */\n  --alert-bg-color-success: var(--state-success);\n  --alert-color-success: #000;\n  --alert-border-color-success: hsl(144, 14%, 15%);\n\n  /* info alerts */\n  --alert-bg-color-info: var(--state-info);\n  --alert-color-info: #fff;\n  --alert-border-color-info: hsl(208, 100%, 21%);\n\n  /* warning alerts */\n  --alert-bg-color-warning: var(--state-warning);\n  --alert-color-warning: #000;\n  --alert-border-color-warning: hsl(28, 100%, 26%);\n\n  /* danger alerts */\n  --alert-bg-color-danger: var(--state-danger);\n  --alert-color-danger: #000;\n  --alert-border-color-danger: hsl(348, 100%, 23%);\n\n  /* the scrollbar that never works / does what anyone wants */\n  --scrollbar-track-hover: #fafafa;\n  --scrollbar-track-thumb: #bababa;\n  --scrollbar-track-thumb-hover: #acacac;\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/themes/midnight.css",
    "content": ".midnight-theme {\n  --color-purple: hsl(253, 84%, 45%);\n  --color-dark-gray: #111;\n  --color-gray: #222;\n  --color-almost-white: #ddd;\n  --color-white: #fff;\n  --state-secondary: var(--color-gray);\n  --body-bg-color: var(--color-gray);\n  --body-color: var(--color-almost-white);\n  --body-color-muted: #777;\n  --link-color: var(--color-almost-white);\n  --link-color-hover: var(--color-white);\n  --pre-bg-color: #1a1a1a;\n  --pre-color: var(--color-almost-white);\n  --code-color: var(--pre-color);\n  --code-bg-color: var(--pre-pg-color);\n  --table-bg-color: var(--color-gray);\n  --top-nav-bg: #1a1a1a;\n  --top-nav-inverted-color: #aaa;\n  --sidebar-bg-color: var(--color-gray);\n  --sidebar-nav-active-color: var(--color-dark-gray);\n  --sidebar-hr-border-color: var(--color-dark-gray);\n  --sidebar-footer-shadow-opacity: 0.6;\n  --modal-content-bg: var(--color-gray);\n  --modal-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.5);\n  --search-result-text-color: var(--link-color);\n  --search-result-hover-bg: var(--color-medium-gray);\n  --search-divider-color: var(--color-medium-gray);\n  --input-color: var(--color-almost-white);\n  --input-bg-color: var(--color-medium-gray);\n  --input-border-color: black;\n  --chat-entry-bg-color: var(--top-nav-bg);\n  --chat-message-hover-color: #333;\n  --chat-message-unread-bg-color: #2a2a2a;\n  --chat-message-sender-name-incoming: var(--color-fuchsia);\n  --chat-message-sender-name-outgoing: var(--color-orange);\n  --chat-new-messages-notice-bg-color: var(--color-medium-gray);\n  --scrollbar-track-hover: #292929;\n  --scrollbar-track-thumb: #666;\n  --scrollbar-track-thumb-hover: #777;\n  --dropdown-bg-color: #333;\n  --dropdown-color: var(--body-color);\n  --dropdown-border-color: var(--color-medium-gray);\n  --dropdown-divider-color: var(--color-medium-gray);\n}\n"
  },
  {
    "path": "client/web/addons/ui/app/styles/@emberclear/ui.css",
    "content": "@import 'shoelace-css/source/css/normalize';\n@import 'shoelace-css/source/css/content';\n@import 'shoelace-css/source/css/alerts';\n@import 'shoelace-css/source/css/badges';\n@import 'shoelace-css/source/css/buttons';\n@import 'shoelace-css/source/css/dropdowns';\n\n/* @import 'shoelace-css/source/css/dropdowns'; */\n@import 'shoelace-css/source/css/file-buttons';\n@import 'shoelace-css/source/css/forms';\n@import 'shoelace-css/source/css/grid';\n\n/* @import url('progress-bars.css'); */\n\n/* @import url('loaders.css'); */\n@import 'shoelace-css/source/css/switches';\n@import 'shoelace-css/source/css/tabs';\n@import 'shoelace-css/source/css/tables';\n@import 'shoelace-css/source/css/utilities';\n@import 'shoelace-css/source/css/variables';\n@import './themes/default';\n@import './themes/midnight';\n@import './defaults';\n@import './shoelace-overrides';\n@import './layout';\n@import './mixins';\n@import './buttons';\n@import './sizes';\n@import './animation';\n@import './spacing';\n\n.visually-hidden {\n  position: absolute;\n  left: -1000px;\n  top: -100px;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n}\n\n.is-radiusless {\n  border-radius: 0;\n}\n\n.no-select {\n  @apply --no-select;\n}\n\n.has-shadow {\n  box-shadow: var(--shadow);\n}\n\n.button-group {\n  display: grid;\n  grid-gap: var(--grid-gap);\n  grid-auto-flow: column;\n\n  &.right-aligned {\n    justify-content: end;\n  }\n}\n\n.hero {\n  min-height: calc(100vh - var(--top-nav-height));\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-direction: column;\n}\n\n.cta {\n  display: flex;\n  justify-content: flex-end;\n}\n\n.cta-with-fallback {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.overflows {\n  overflow-y: auto;\n  overflow-x: hidden;\n}\n"
  },
  {
    "path": "client/web/addons/ui/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (/* environment, appConfig */) {\n  return {};\n};\n"
  },
  {
    "path": "client/web/addons/ui/ember-cli-build.js",
    "content": "'use strict';\n\n// eslint-disable-next-line node/no-unpublished-require\nconst EmberAddon = require('ember-cli/lib/broccoli/ember-addon');\n\nmodule.exports = function (defaults) {\n  let app = new EmberAddon(defaults, {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n  });\n\n  /*\n    This build file specifies the options for the dummy test app of this\n    addon, located in `/tests/dummy`\n    This build file does *not* influence how the addon or the app using it\n    behave. You most likely want to be modifying `./index.js` or app's build file\n  */\n\n  return app.toTree();\n};\n"
  },
  {
    "path": "client/web/addons/ui/index.js",
    "content": "'use strict';\n\nconst path = require('path');\n\nconst PostCSSImport = require('postcss-import');\nconst PostCSSNext = require('postcss-cssnext');\n\nconst nodeModules = path.join(__dirname, '..', '..', 'node_modules');\nconst addonStyles = path.join(__dirname, 'addon', 'styles');\n\nconst importConfig = PostCSSImport({\n  path: ['app/styles', nodeModules, addonStyles],\n});\n\nconst cssNextConfig = PostCSSNext({\n  features: {\n    colorFunction: {\n      preserveCustomProps: false,\n    },\n    customProperties: {\n      preserve: true,\n    },\n    rem: false,\n  },\n});\n\nmodule.exports = {\n  name: require('./package').name,\n\n  // override\n  isDevelopingAddon() {\n    return true;\n  },\n\n  options: {\n    'ember-cli-babel': {\n      enableTypeScriptTransform: true,\n    },\n\n    /**\n     * The addon needs to support PostCSS, but not actually compile any of it.\n     *\n     * This is so that the consuming app can use all the same features.\n     */\n    postcssOptions: {\n      compile: {\n        plugins: [importConfig, cssNextConfig],\n      },\n    },\n  },\n\n  included: function (app) {\n    this._super.included.apply(this, arguments);\n\n    app.options = app.options || {};\n    app.options.postcssOptions = {\n      compile: {\n        plugins: [importConfig, cssNextConfig],\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "client/web/addons/ui/package.json",
    "content": "{\n  \"name\": \"@emberclear/ui\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Base ui components for emberclear.io\",\n  \"private\": true,\n  \"keywords\": [\n    \"ember-addon\",\n    \"emberclear\"\n  ],\n  \"repository\": {\n    \"url\": \"https://github.com/NullVoxPopuli/emberclear\",\n    \"directory\": \"client/web/addons/ui\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint . --ext js,ts\",\n    \"start\": \"ember serve\",\n    \"test\": \"ember test\",\n    \"prepublish\": \"tsc --build\"\n  },\n  \"dependencies\": {\n    \"@fortawesome/ember-fontawesome\": \"0.2.3\",\n    \"@fortawesome/free-brands-svg-icons\": \"5.15.3\",\n    \"@fortawesome/free-solid-svg-icons\": \"5.15.3\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-cli-postcss\": \"7.0.2\",\n    \"ember-focus-trap\": \"0.7.0\",\n    \"ember-keyboard\": \"6.0.3\",\n    \"ember-named-blocks-polyfill\": \"0.2.4\",\n    \"ember-tracked-local-storage\": \"*\",\n    \"enhanced-resolve\": \"5.8.2\",\n    \"postcss-cssnext\": \"3.1.0\",\n    \"postcss-import\": \"14.0.2\",\n    \"shoelace-css\": \"1.0.0-beta24\",\n    \"typescript\": \"4.3.4\",\n    \"uuid\": \"8.3.2\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"2.0.0\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"^5.0.10\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"broccoli-asset-rev\": \"3.0.0\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-dependency-checker\": \"3.2.0\",\n    \"ember-cli-inject-live-reload\": \"2.1.0\",\n    \"ember-cli-page-object\": \"1.17.7\",\n    \"ember-cli-sri\": \"2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-disable-prototype-extensions\": \"1.1.3\",\n    \"ember-export-application-global\": \"2.0.1\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"0.1.6\",\n    \"ember-named-blocks-polyfill\": \"0.2.4\",\n    \"ember-qunit\": \"4.6.0\",\n    \"ember-resolver\": \"8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"loader.js\": \"4.7.0\",\n    \"npm-run-all\": \"4.1.5\",\n    \"qunit-dom\": \"1.6.0\",\n    \"testem-failure-only-reporter\": \"0.0.1\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"ember-addon\": {\n    \"before\": [\n      \"ember-cli-postcss\"\n    ],\n    \"configPath\": \"tests/dummy/config\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/addons/ui/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nimport config from './config/environment';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/components/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/controllers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/models/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/router.js",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from './config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {});\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/routes/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/styles/app.css",
    "content": "/* intentionally empty */\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/app/templates/application.hbs",
    "content": "\n{{outlet}}"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'dummy',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/config/targets.js",
    "content": "'use strict';\n\nconst browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions'];\n\nconst isCI = !!process.env.CI;\nconst isProduction = process.env.EMBER_ENV === 'production';\n\nif (isCI || isProduction) {\n  browsers.push('ie 11');\n}\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/addons/ui/tests/dummy/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/addons/ui/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Dummy Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/dummy.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/dummy.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/backdrop-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | backdrop', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    this.setProperties({\n      onClick: () => {},\n    });\n\n    await render(hbs`<Backdrop @onClick={{this.onClick}} />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/collapsible-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | collapsible', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    // Set any properties with this.set('myProperty', 'value');\n    // Handle any actions with this.set('myAction', function(val) { ... });\n\n    await render(hbs`<Collapsible />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n\n    // Template block usage:\n    await render(hbs`\n      <Collapsible>\n        template block text\n      </Collapsible>\n    `);\n\n    assert.equal(this.element.textContent.trim(), 'template block text');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/dropdown-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | dropdown', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`\n      <Dropdown>\n        <:trigger as |toggle|>\n          <button {{on 'click' toggle}} type='button'>Toggle</button>\n        </:trigger>\n\n        <:content>\n          Content Here\n        </:content>\n      </Dropdown>\n    `);\n\n    assert\n      .dom(this.element)\n      .hasText('Toggle Content Here', 'all text is shown, cause this is a CSS effect');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/ellipsis-loader-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | ellipsis-loader', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<EllipsisLoader />`);\n\n    assert.dom(this.element).hasText('• • •');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/external-link-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | external-link', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<ExternalLink />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n\n    await render(hbs`\n      <ExternalLink>\n        template block text\n      </ExternalLink>\n    `);\n\n    assert.equal(this.element.textContent.trim(), 'template block text');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/field-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | field', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    // Set any properties with this.set('myProperty', 'value');\n    // Handle any actions with this.set('myAction', function(val) { ... });\n\n    await render(hbs`<Field />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n\n    // Template block usage:\n    await render(hbs`\n      <Field>\n        template block text\n      </Field>\n    `);\n\n    assert.equal(this.element.textContent.trim(), 'template block text');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/focus-card-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Component | focus-card', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    // Set any properties with this.set('myProperty', 'value');\n    // Handle any actions with this.set('myAction', function(val) { ... });\n\n    await render(hbs`{{focus-card}}`);\n\n    assert.equal(this.element.textContent?.trim(), '');\n\n    // Template block usage:\n    await render(hbs`\n      {{#focus-card}}\n        template block text\n      {{/focus-card}}\n    `);\n\n    assert.equal(this.element.textContent?.trim(), 'template block text');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/hamburger-button-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | hamburger-button', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    this.setProperties({\n      onClick: () => {},\n    });\n\n    await render(hbs`<HamburgerButton @onClick={{this.onClick}} />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/hover-tip-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | hover-tip', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    // Set any properties with this.set('myProperty', 'value');\n    // Handle any actions with this.set('myAction', function(val) { ... });\n\n    await render(hbs`<HoverTip />`);\n\n    assert.equal(this.element.textContent.trim(), '');\n\n    // Template block usage:\n    await render(hbs`\n      <HoverTip>\n        template block text\n      </HoverTip>\n    `);\n\n    assert.equal(this.element.textContent.trim(), 'template block text');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/media-info-card-test.ts",
    "content": "// import { module, test } from 'qunit';\n// import { setupRenderingTest } from 'ember-qunit';\n// import { render } from '@ember/test-helpers';\n// import hbs from 'htmlbars-inline-precompile';\n\n// module('Integration | Component | media-info-card', function (hooks) {\n//   setupRenderingTest(hooks);\n// });\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/modal-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { create } from 'ember-cli-page-object';\nimport hbs from 'htmlbars-inline-precompile';\n\nimport { modal } from '@emberclear/ui/test-support/page-objects';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nmodule('Integration | Component | modal', function (hooks) {\n  setupRenderingTest(hooks);\n\n  let page = create(modal);\n\n  hooks.beforeEach(async function (this: TestContext) {\n    this.setProperties({\n      active: true,\n      close: () => {\n        this.set('active', false);\n      },\n    });\n\n    await render(hbs`\n      <Modal\n        @isActive={{this.active}}\n        @close={{this.close}}\n      >\n        <a href=''>Modal Content</a>\n      </Modal>`);\n  });\n\n  test('it renders and pressing escape closes', async function (assert) {\n    assert.equal(page.modalContent.text, 'Modal Content');\n\n    await page.pressEscape();\n\n    assert.equal(page.modalContent.text, '');\n  });\n\n  test('it renders and clicking the backdrop closes', async function (assert) {\n    assert.equal(page.modalContent.text, 'Modal Content');\n\n    await page.backdrop.click();\n\n    assert.equal(page.modalContent.text, '');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/components/switch-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { create } from 'ember-cli-page-object';\nimport hbs from 'htmlbars-inline-precompile';\n\nimport { switchWrapper } from '@emberclear/ui/test-support/page-objects';\n\nmodule('Integration | Component | switch', function (hooks) {\n  setupRenderingTest(hooks);\n\n  let page = create(switchWrapper);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<Switch @label='test' />`);\n\n    assert.equal(page.label, 'test');\n  });\n\n  test('can be toggled', async function (assert) {\n    await render(hbs`\n      <Switch\n        @value={{true}}\n        @label='test'\n      />\n    `);\n\n    assert.true(page.isChecked, 'is checked');\n\n    await page.check();\n\n    assert.false(page.isChecked, 'not checked');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/helpers/and-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Helper | and', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    await render(hbs`{{and true false}}`);\n\n    assert.dom().hasText('false');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/helpers/eq-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Helper | eq', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    await render(hbs`{{eq 1 1}}`);\n\n    assert.dom().hasText('true');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/integration/helpers/not-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Helper | not', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    this.set('inputValue', '1234');\n\n    await render(hbs`{{not this.inputValue}}`);\n\n    assert.dom().hasText('false');\n  });\n});\n"
  },
  {
    "path": "client/web/addons/ui/tests/test-helper.ts",
    "content": "// Install Types and assertion extensions\nimport 'qunit-dom';\n\nimport { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\n\nimport Application from 'dummy/app';\nimport config from 'dummy/config/environment';\n\nsetApplication(Application.create(config.APP));\n\nstart();\n"
  },
  {
    "path": "client/web/addons/ui/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    // Question: what's the best place for test and dummy declarations to go? They\n    // aren't actually needed for anything other than to satisfy the requirements\n    // for a composite build.\n    \"declarationDir\": \"./dummy/declarations\",\n    \"paths\": {\n      \"dummy/tests/*\": [\"./*\"],\n      \"dummy/*\": [\"./dummy/app/*\"],\n      \"@emberclear/ui\": [\"../declarations\"],\n      \"@emberclear/ui/*\": [\"../declarations/*\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\n    \".\",\n    \"../types\"\n  ],\n  \"references\": [\n    { \"path\": \"../addon\" },\n    { \"path\": \"../addon-test-support\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/ui/tests/unit/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/tsconfig.compiler-options.json",
    "content": "{\n  // Alias to reduce the number of ../ in paths\n  \"extends\": \"../../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/addons/ui/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"addon\" },\n    { \"path\": \"addon-test-support\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/addons/ui/types/dummy/index.d.ts",
    "content": ""
  },
  {
    "path": "client/web/addons/ui/types/global.d.ts",
    "content": "// Types for compiled templates\ndeclare module '@emberclear/ui/templates/*' {\n  import type { TemplateFactory } from 'htmlbars-inline-precompile';\n  const tmpl: TemplateFactory;\n  export default tmpl;\n}\n\n"
  },
  {
    "path": "client/web/addons/ui/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n"
  },
  {
    "path": "client/web/addons/ui/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/config/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.node();\n"
  },
  {
    "path": "client/web/config/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  extends: 'octane',\n  rules: {\n    quotes: 'single',\n  },\n};\n"
  },
  {
    "path": "client/web/config/package.json",
    "content": "{\n  \"name\": \"@emberclear/config\",\n  \"version\": \"0.0.0\",\n  \"description\": \"shared config files\",\n  \"main\": \"index.js\",\n  \"author\": \"NullVoxPopuli\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"devDependencies\": {\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\"\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/config/testem.js",
    "content": "'use strict';\n\nconst FailureOnlyReporter = require('testem-failure-only-reporter');\n\nconst CI_BROWSER = process.env.CI_BROWSER || 'Chrome';\n\nmodule.exports = {\n  test_page: 'tests/index.html?hidepassed',\n  disable_watching: true,\n\n  launch_in_ci: [CI_BROWSER],\n  launch_in_dev: ['Chrome'],\n\n  reporter: FailureOnlyReporter,\n\n  browser_args: {\n    Firefox: {\n      mode: 'ci',\n      args: ['-headless'],\n    },\n    Chrome: {\n      ci: [\n        // --no-sandbox is needed when running Chrome inside a container\n        process.env.CI ? '--no-sandbox' : null,\n\n        '--headless',\n        '--disable-dev-shm-usage',\n        '--disable-software-rasterizer',\n        '--mute-audio',\n        '--remote-debugging-port=0',\n        '--window-size=1280,720', // 720p\n      ].filter(Boolean),\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/config/tsconfig.compiler-options.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"composite\": true,\n\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"node\",\n\n    // The JS ecosystem has 4+ module formats and many packages do not use ES modules...\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n\n    // Correctness checks\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Footgun prevention\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": false,\n\n    // We're not using typescript to transpile, so noEmitOnError would not\n    // prevent invalid code from compiling\n    \"noEmitOnError\": false,\n    \"noEmit\": false,\n\n    \"allowJs\": false,\n\n    // Babel handles source maps\n    \"inlineSourceMap\": false,\n    \"inlineSources\": false,\n\n    \"module\": \"ESNext\",\n\n    // Stage 3 one day\n    \"experimentalDecorators\": true,\n\n    // tsc will only be used for declarations\n    \"emitDeclarationOnly\": true\n  }\n}\n"
  },
  {
    "path": "client/web/config/utils/ember-build.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst yn = require('yn');\nconst { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');\n\nconst { CONCAT_STATS, SOURCEMAPS_DISABLED, MINIFY_DISABLED } = process.env;\n\nfunction configureBabel(appConfig) {\n  appConfig.babel = appConfig.babel || {};\n  appConfig.babel = {\n    ...(appConfig.babel || {}),\n    plugins: [\n      ...(appConfig.babel.plugins || []),\n      // for enabling dynamic import.\n      require.resolve('ember-auto-import/babel-plugin'),\n    ],\n  };\n\n  appConfig['ember-cli-babel'] = {\n    ...(appConfig['ember-cli-babel'] || {}),\n    enableTypeScriptTransform: true,\n    throwUnlessParallelizable: true,\n  };\n}\n\nfunction applyEnvironmentVariables(appOptions) {\n  if (yn(SOURCEMAPS_DISABLED)) {\n    appOptions['sourcemaps'] = { enabled: false };\n    appOptions.babel.sourceMaps = false;\n  }\n\n  if (yn(MINIFY_DISABLED)) {\n    appOptions['ember-cli-terser'] = { enabled: false };\n    appOptions.minifyCSS = { enabled: false };\n  }\n\n  if (yn(CONCAT_STATS)) {\n    appOptions.autoImport = appOptions.autoImport || {};\n    appOptions.autoImport.webpack = appOptions.autoImport.webpack || {};\n    appOptions.autoImport.webpack.plugins = [\n      new BundleAnalyzerPlugin({\n        analyzerMode: 'static',\n        openAnalyzer: false,\n        reportFilename: path.join(process.cwd(), 'concat-stats-for', 'ember-auto-import.html'),\n      }),\n    ];\n  }\n}\n\nmodule.exports = { applyEnvironmentVariables, configureBabel };\n"
  },
  {
    "path": "client/web/config/utils/log.js",
    "content": "/* eslint-disable no-console */\n'use strict';\n\nfunction logWithAttention(...thingsToLog) {\n  let longestLength = Math.max(thingsToLog.map((str) => str.length || 0)) || 80;\n\n  let divider = '-'.repeat(longestLength);\n\n  console.log(divider);\n\n  for (let data of thingsToLog) {\n    console[typeof data === 'string' ? 'log' : 'dir'](data);\n  }\n\n  console.log(divider);\n}\n\nmodule.exports = { logWithAttention };\n"
  },
  {
    "path": "client/web/emberclear/.dockerignore",
    "content": ".git\ntmp\nnode_modules\n"
  },
  {
    "path": "client/web/emberclear/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/emberclear/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false,\n  \"liveReload\": true,\n  \"host\": \"0.0.0.0\",\n\n  /* no pods, use co-located components */\n  \"usePods\": false,\n  \"componentStructure\": \"nested\",\n\n  /* for service-worker and other PWA testing */\n  \"ssl\": true,\n  \"ssl-key\": \"./config/emberclear-local.key\",\n  \"ssl-cert\": \"./config/emberclear-local.crt\"\n}\n"
  },
  {
    "path": "client/web/emberclear/.eslintignore",
    "content": "/tmp\n/blueprints/*/files/**/*.js\n\n**/bip39/wordlists/english.ts\n\n/src/data/models/application/*.js\n/config/optional-features.json\n/concat-stats-for\n/node_modules\nnode_modules/\n/dist\n*.lock\n*.log\n*.json\n*.yaml\n\n\n# unconventional js\n/public/workers/\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/emberclear/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nconst config = configs.ember();\n\nmodule.exports = {\n  ...config,\n  overrides: [\n    ...config.overrides,\n    {\n      files: ['app/services/prism-manager.ts'],\n      rules: {\n        'no-undef': 'off',\n      },\n    },\n    {\n      files: ['app/**/*.ts', 'tests/**/*.ts'],\n      rules: {\n        // This project didn't start in TypeScript\n        '@typescript-eslint/no-explicit-any': 'warn',\n        '@typescript-eslint/no-non-null-assertion': 'warn',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "client/web/emberclear/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n/coverage*\n/junit.xml\n\n# bundle analysis\n/concat-stats-for\n\n# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# generated as a part of deployment\n/public/bundle/\n/public/workers/\n\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.eslintcache\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/emberclear/.template-lintrc.js",
    "content": "'use strict';\n\nconst config = require('@emberclear/config/.template-lintrc');\n\nmodule.exports = {\n  ...config,\n  rules: {\n    ...config.rules,\n    'no-inline-styles': false, // TODO enable :(\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/Dockerfile",
    "content": "# FROM node:9-alpine\nFROM node:8.11.1\n\nENV CORE_DEPS apt-transport-https gnupg\nENV WATCHMAN_DEPS python-dev\nENV TESTING_DEPS google-chrome-stable\nENV BUILD_DEPS \"\"\n\n# Install chrome and watchman deps\nRUN echo \\\n\t&& apt-get update \\\n\t&& apt-get install -y $CORE_DEPS $WATCHMAN_DEPS --no-install-recommends \\\n  && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \\\n  && echo \"deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main\" > /etc/apt/sources.list.d/google-chrome.list \\\n  && apt-get update \\\n  && apt-get install -y $TESTING_DEPS $BUILD_DEPS --no-install-recommends\n\n# Install watchman\n# Not installed due to the default fs.inotify.max_user_watches boing too low\n# RUN echo \\\n# \t&& git clone https://github.com/facebook/watchman.git \\\n# \t&& cd watchman \\\n# \t&& git checkout v4.9.0 \\\n# \t&& ./autogen.sh \\\n# \t&& ./configure \\\n# \t&& make \\\n# \t&& make install\n\nRUN mkdir -p /app\nWORKDIR /app\n\n# install npm (for security updates), and ember-cli\nRUN echo \\\n  && npm install --global npm@latest\n\nCMD [\"yarn\", \"start\"]\n"
  },
  {
    "path": "client/web/emberclear/Dockerfile.release",
    "content": "FROM nginx:alpine\n\nCOPY dist/ /emberclear\nCOPY scripts/docker/run-nginx.sh /usr/local/bin\nCOPY scripts/docker/nginx.conf etc/nginx/conf.d/default.conf.template\n\nEXPOSE 4201\nCMD [\"/usr/local/bin/run-nginx.sh\"]\n\n\n"
  },
  {
    "path": "client/web/emberclear/README.md",
    "content": "# emberclear PWA\n\n<table>\n<tr>\n<td>Pipelines</td>\n<td>\n\n![Tests](https://github.com/NullVoxPopuli/emberclear/workflows/Frontend%20Tests/badge.svg)\n![Quality](https://github.com/NullVoxPopuli/emberclear/workflows/Frontend%20Quality/badge.svg)\n![Deploy](https://github.com/NullVoxPopuli/emberclear/workflows/Frontend%20Deploy/badge.svg)\n\n</td>\n</tr>\n<tr>\n<td>Quality</td>\n<td>\n\n\n[![Maintainability](https://api.codeclimate.com/v1/badges/3f2faa686db3db3a52f8/maintainability)](https://codeclimate.com/github/NullVoxPopuli/emberclear/maintainability)\n[![Coverage Status](https://coveralls.io/repos/github/NullVoxPopuli/emberclear/badge.svg?branch=master)](https://coveralls.io/github/NullVoxPopuli/emberclear?branch=master)\n\n</td>\n</tr>\n<tr>\n<td>Info</td>\n<td>\n\n[![bundle analysis](https://img.shields.io/badge/bundle-analysis-blue.svg)](https://emberclear.io/bundle.html)\n[![current test coverage](https://img.shields.io/badge/test%20coverage-deployed-blue.svg)](https://emberclear.io/coverage/index.html)\n\n\n</td>\n</tr>\n<tr>\n<td>External Services</td>\n<td>\n\n\n[![Crowdin](https://d322cqt584bo4o.cloudfront.net/emberclear/localized.svg)](https://crowdin.com/project/emberclear) \n\n[![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=SDYxMWtDbjBhcnZnOTBpdGZMbzl6Mktyb2QyT0FUZTlwazByUWF2ZEFUUT0tLVZKaFBZR0kzdTlmZEUxM202QnA3aVE9PQ==--58be570679305f818be70e6aef2c24f1d4dc1698)](https://automate.browserstack.com/public-build/SDYxMWtDbjBhcnZnOTBpdGZMbzl6Mktyb2QyT0FUZTlwazByUWF2ZEFUUT0tLVZKaFBZR0kzdTlmZEUxM202QnA3aVE9PQ==--58be570679305f818be70e6aef2c24f1d4dc1698)\n[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/Open-Source/emberclear)\n\n\n</td>\n\n</tr>\n</table>\n\n\n\n\n\n## Development\n\nWritten in ember for demonstration of\n - Progressive Web Apps\n - Service Workers\n - WebSockets\n - TypeScript\n - all the modern features / best practices of ember\n\nSee: [CONTRIBUTING.md](https://github.com/NullVoxPopuli/emberclear/blob/master/CONTRIBUTING.md)\n\n"
  },
  {
    "path": "client/web/emberclear/app/app.ts",
    "content": "import Application from '@ember/application';\n\nimport defineModifier from 'ember-concurrency-test-waiter/define-modifier';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nimport config from 'emberclear/config/environment';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\n/*\n * This line is added to support initializers in the `app/` directory\n */\nloadInitializers(App, config.modulePrefix);\n\ndefineModifier();\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/app-shell-remover.ts",
    "content": "import Component from '@glimmer/component';\n\nexport default class extends Component {\n  constructor(owner: any, args: any) {\n    super(owner, args);\n\n    this.removeAppLoader();\n  }\n\n  private removeAppLoader() {\n    const loader = document.querySelector('#app-loader');\n\n    if (loader) {\n      loader.remove();\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/container/hero.hbs",
    "content": "<App::Container::Main>\n  <div class='hero text-light text-center'>\n    {{yield}}\n  </div>\n</App::Container::Main>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/container/main.hbs",
    "content": "<section class='sticky-footer-content'>\n  {{yield}}\n</section>\n\n<AppFooter class='sticky-footer' />\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/container/primary-hero.hbs",
    "content": "<section class='sticky-footer-content bg-secondary'>\n  <div class='hero text-light text-center'>\n    {{yield}}\n  </div>\n</section>\n\n<AppFooter class='sticky-footer' />\n\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/modals.hbs",
    "content": "{{!--\n  Static Modals / Modals that will /\n  could get toggleed from _multiple_ places.\n--}}\n<ModalStatic @name='keyboard-shortcuts' as |isActive actions|>\n  <KeyboardShortcuts @isActive={{isActive}} @close={{actions.close}} />\n\n\n  {{on-key 'ctrl+h' (prevent-default actions.toggle)}}\n  {{on-key 'ctrl+/' (prevent-default actions.toggle)}}\n</ModalStatic>\n\n<ModalStatic @name='search' as |isActive actions|>\n  <Search @isActive={{isActive}} @close={{actions.close}} />\n\n  {{on-key 'ctrl+k' (prevent-default actions.toggle)}}\n</ModalStatic>\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/off-canvas/-page.ts",
    "content": "import { find } from '@ember/test-helpers';\n\nimport { clickable, create, fillable, isPresent, text } from 'ember-cli-page-object';\nimport { getter } from 'ember-cli-page-object/macros';\n\nimport {\n  sidebarActionsPage,\n  sidebarChannelsPage,\n  sidebarContactsPage,\n} from 'emberclear/components/app/sidebar/chats/-page';\n\nconst wrapper = '[data-test-offcanvas-wrapper]';\nconst toggleButton = '[data-test-hamburger-toggle]';\nconst sidebarContainer = '[data-test-sidebar-container]';\nconst channelForm = '[data-test-channel-form]';\nconst contacts = '[data-test-sidebar-contacts-list]';\nconst offlineCount = '[data-test-offline-count]';\n\nexport const selectors = {\n  wrapper,\n  toggleButton,\n  sidebarContainer,\n  channelForm,\n  contacts,\n  offlineCount,\n};\n\nexport const page = create({\n  toggle: clickable('.top-nav button.navbar-burger'),\n  isPresent: isPresent('aside'),\n\n  isOpen: getter(function () {\n    let element = find('.mobile-menu') as HTMLElement;\n\n    return element.classList.contains('mobile-menu--open');\n  }),\n\n  content: {\n    scope: '#scrollContainer',\n  },\n\n  sidebar: {\n    scope: 'aside',\n\n    search: fillable('[data-test-sidebar-search]'),\n    searchInfo: text('[data-test-search-info]'),\n\n    selectContactsTab: clickable('[data-test-tab-contacts]'),\n    selectChannelsTab: clickable('[data-test-tab-channels]'),\n    selectActionsTab: clickable('[data-test-tab-actions]'),\n\n    header: {\n      scope: '[data-test-sidebar-content-header]',\n    },\n\n    contacts: sidebarContactsPage,\n    channels: sidebarChannelsPage,\n    actions: sidebarActionsPage,\n\n    footer: {\n      scope: 'footer',\n    },\n  },\n});\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/off-canvas/index.hbs",
    "content": "<MobileMenuWrapper @preventScroll={{true}} as |mmw|>\n\n  {{#if this.currentUser.isLoggedIn}}\n    <mmw.MobileMenu\n      @mode={{this.mode}}\n      @isOpen={{this.sidebar.isShown}}\n    >\n\n      <App::Sidebar\n        class='sidebar-wrapper'\n        id='sidebar'\n        {{on-key 'ctrl+space' this.sidebar.toggle}}\n      />\n\n    </mmw.MobileMenu>\n  {{/if}}\n\n  <mmw.Content id='scrollContainer'>\n    {{yield}}\n  </mmw.Content>\n</MobileMenuWrapper>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/off-canvas/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { debounce } from '@ember/runloop';\nimport { inject as service } from '@ember/service';\n\nimport { TABLET_WIDTH } from 'emberclear/utils/breakpoints';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Sidebar from 'emberclear/services/sidebar';\n\ntype Args = Record<string, unknown>;\n\nexport default class OffCanvasContainer extends Component<Args> {\n  @service declare currentUser: CurrentUserService;\n  @service declare sidebar: Sidebar;\n\n  get mode() {\n    if (this.isTabletOrSmaller) {\n      return 'reveal';\n    }\n\n    return 'squeeze-reveal';\n  }\n\n  // ----------------------------------------\n  // TODO: extract all this to a resource?\n  // ----------------------------------------\n  declare handler: any; // where is ember run timer?\n\n  // @service declare window: Window;\n\n  @tracked lastWindowWidth = window.innerWidth;\n\n  // window.screen => physical screen\n  // window.innerWidth => full width of window, excluding dev tools\n  get isTabletOrSmaller() {\n    return this.lastWindowWidth <= TABLET_WIDTH;\n  }\n\n  @action\n  updateWidth() {\n    this.lastWindowWidth = window.innerWidth;\n  }\n\n  constructor(owner: unknown, args: Args) {\n    super(owner, args);\n\n    this.handler = () => debounce(this, 'updateWidth', 100);\n\n    window.addEventListener('resize', this.handler);\n  }\n\n  willDestroy() {\n    window.removeEventListener('resize', this.handler);\n    super.willDestroy();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/index.hbs",
    "content": "{{!-- template-lint-disable no-inline-styles --}}\n<nav class='actions' style='height: 100%; overflow-y: auto;' data-test-actions-list>\n\n\t<div data-test-sidebar-content-header class='menu-label'>\n    {{t 'ui.sidebar.actions.title'}}\n    <button\n      class='button button-xs'\n      type='button'\n      {{on 'click' this.addAction}}\n    >\n      <span class='icon'>{{fa-icon 'plus'}}</span>\n    </button>\n  </div>\n\n\t{{#each this.new_actions as |action_item|}}\n\t\t<App::Sidebar::Actions::ResponseAction @action={{action_item}} />\n\t\t<App::Sidebar::Actions::ResponsePanel @vote={{action_item.vote}}/>\n\t{{/each}}\n\n</nav>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { ACTION_RESPONSE } from 'emberclear/models/action';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Action from 'emberclear/models/action';\nimport type SettingsService from 'emberclear/services/settings';\nimport type SidebarService from 'emberclear/services/sidebar';\n\n// import { VOTE_ACTION } from 'emberclear/models/vote-chain';\n// import { generateSortedVote } from 'emberclear/services/channels/-utils/vote-sorter';\n// import { sign, hash } from 'emberclear/workers/crypto/utils/nacl';\n\n// import { toHex } from '@emberclear/encoding/string';\n// import { generateAsymmetricKeys, generateSigningKeys } from 'emberclear/workers/crypto/utils/nacl';\n\ninterface IArgs {\n  actions: Action[];\n  closeSidebar: () => void;\n}\n\nexport default class ActionsSidebar extends Component<IArgs> {\n  @service currentUser!: CurrentUserService;\n  @service settings!: SettingsService;\n  @service router!: RouterService;\n  @service store!: StoreService;\n  @service sidebar!: SidebarService;\n\n  get allActions(): Action[] {\n    //TODO: filter to only current channel\n    return this.store\n      .peekAll('action')\n      .toArray()\n      .filter((action) => action.response !== ACTION_RESPONSE.DISMISSED);\n  }\n\n  get newActions(): Action[] {\n    return this.allActions.sort(sortByOldest);\n  }\n\n  @action\n  async addAction() {\n    //TODO: Construct a new action, add to the store, and broadcast\n  }\n}\n\nfunction sortByOldest(action1: Action, action2: Action) {\n  if (action1.timestamp < action2.timestamp) {\n    return 1;\n  }\n\n  if (action1.timestamp > action2.timestamp) {\n    return -1;\n  }\n\n  return 0;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/response-action/index.hbs",
    "content": "<div data-test-action-row class='sidebar__actions__action'>\n\n  <span data-test-action-name>\n    {{t this.header}}\n  </span>\n\n</div>\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/response-action/index.ts",
    "content": "import Component from '@glimmer/component';\n\nimport type Action from 'emberclear/models/action';\n\ninterface IArgs {\n  action: Action;\n}\n\nexport default class ResponseAction extends Component<IArgs> {\n  get header() {\n    return (\n      this.args.action.vote.voteChain.action +\n      ' ' +\n      this.args.action.vote.voteChain.target.displayName +\n      ': ' +\n      this.args.action.response\n    );\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/response-panel/index.hbs",
    "content": "<button class='button button-xs' type='button' data-test-action-response-yes {{on 'click' this.yes}}>\n\t<span class='icon'>{{fa-icon 'check'}}</span>\n</button>\n\n<button class='button button-xs' type='button' data-test-action-response-no {{on 'click' this.no}}>\n\t<span class='icon'>{{fa-icon 'times'}}</span>\n</button>\n\n<button class='button button-xs' type='button' data-test-action-response-dismiss {{on 'click' this.dismiss}}>\n\t<span class='icon'>{{fa-icon 'minus'}}</span>\n</button>"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/actions/response-panel/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\n\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\n\ninterface IArgs {\n  vote: VoteChain;\n}\n\nexport default class ResponsePanel extends Component<IArgs> {\n  @action\n  yes() {\n    //TODO: Construct and send a yes vote\n  }\n\n  @action\n  no() {\n    //TODO: Construct and send a no vote\n  }\n\n  @action\n  dismiss() {\n    //TODO: Mark the action as dismissed\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/-page.ts",
    "content": "import { triggerKeyEvent } from '@ember/test-helpers';\n\nimport { clickable, collection, create, fillable, text } from 'ember-cli-page-object';\n\nexport const sidebarContactsPage = create({\n  header: {\n    scope: '[data-test-sidebar-contacts-header]',\n    clickAdd: clickable('[data-test-add-friend]'),\n  },\n  listText: text('[data-test-contacts-list]'),\n  list: collection('[data-test-contact-row]', {\n    name: text('[data-test-contact-name]'),\n    pin: clickable('[data-test-contact-pin]'),\n  }),\n  offlineCount: {\n    scope: '[data-test-offline-count]',\n  },\n});\n\nconst channelForm = '[data-test-channel-form]';\n\nexport const sidebarChannelsPage = create({\n  header: {\n    scope: '[data-test-sidebar-channels-header]',\n  },\n  listText: text('[data-test-channels-list]'),\n  list: collection('[data-test-channel-row]', {\n    name: text('[data-test-channel-name]'),\n  }),\n  toggleForm: clickable('[data-test-channel-form-toggle]'),\n  form: {\n    scope: channelForm,\n    fill: fillable('input'),\n    submit: () => triggerKeyEvent(`${channelForm} input`, 'keypress', 'Enter'),\n  },\n});\n\nexport const sidebarActionsPage = create({\n  listText: text('[data-test-actions-list]'),\n  list: collection('[data-test-action-row]', {\n    name: text('[data-test-action-name]'),\n    voteYes: clickable('[data-test-action-response-yes]'),\n    voteNo: clickable('[data-test-action-response-no]'),\n    voteDismiss: clickable('[data-test-action-response-dismiss]'),\n  }),\n});\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/channel/index.hbs",
    "content": "<a\n  href={{url-for 'chat.in-channel' @channel.id}}\n  {{on 'click' (prevent-default (fn this.onClickChannel @channel))}}\n>\n  <span>#</span><span>{{@channel.name}}</span>\n</a>"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/channel/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { TABLET_WIDTH } from 'emberclear/utils/breakpoints';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type { Channel } from '@emberclear/local-account';\nimport type SidebarService from 'emberclear/services/sidebar';\n\ntype Args = {\n  channel: Channel;\n};\n\nexport default class SidebarChannel extends Component<Args> {\n  @service declare sidebar: SidebarService;\n  @service declare router: RouterService;\n\n  @action\n  async onClickChannel(channel: Channel) {\n    if (window.innerWidth < TABLET_WIDTH) {\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      this.sidebar.hide();\n    }\n\n    await this.router.transitionTo('chat.in-channel', channel.id);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/channel-form/index.hbs",
    "content": "<form data-test-channel-form {{on 'submit' (prevent-default this.onFormSubmit)}}>\n  {{!-- template-lint-disable require-input-label --}}\n  <input\n    class='input'\n    type='text'\n    placeholder={{t 'ui.sidebar.channels.placeholder'}}\n    value={{this.newChannelName}}\n    {{on 'input' this.onInput}}\n    {{on 'keypress' this.onKeyPress}}\n  >\n</form>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/channel-form/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type StoreService from '@ember-data/store';\nimport type ChannelManager from '@emberclear/local-account/services/channel-manager';\n\ntype Args = {\n  onSubmit: () => void;\n};\n\nexport default class ChannelForm extends Component<Args> {\n  @service store!: StoreService;\n  @service channelManager!: ChannelManager;\n\n  @tracked newChannelName = '';\n\n  @action\n  onFormSubmit() {\n    return taskFor(this.didSubmitChannelName).perform();\n  }\n\n  @action\n  onInput({ target = {} as HTMLInputElement }) {\n    this.newChannelName = target.value;\n  }\n\n  @action\n  onKeyPress(this: ChannelForm, event: KeyboardEvent) {\n    const { keyCode } = event;\n\n    if (keyCode === 13) {\n      // non-blocking\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.didSubmitChannelName).perform();\n\n      return false;\n    }\n\n    return true;\n  }\n\n  @dropTask\n  async didSubmitChannelName() {\n    await this.createChannel();\n\n    this.newChannelName = '';\n\n    await this.args.onSubmit();\n  }\n\n  private async createChannel() {\n    const id = this.newChannelName;\n\n    // TODO: using both will likely lead to problems in the future.\n    //       id should maybe be a guid which will allow the channel\n    //       name to change\n    return await this.channelManager.findOrCreate(id, id);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/contact/index.hbs",
    "content": "<div\n  data-test-contact-row\n  class='sidebar__contacts__contact'\n  {{has-unread @contact}}\n>\n\n  <a\n    href={{url-for 'chat.privately-with' @contact.id}}\n    class={{if this.isActive 'is-active'}}\n    {{on 'click' (prevent-default this.onClick)}}\n  >\n    <span data-test-contact-name>\n      <StatusIcon @contact={{@contact}} />\n      <span>{{or @contact.name '[unnamed]'}}</span>\n    </span>\n\n    {{#if this.hasUnread}}\n      <span\n        class='badge badge-danger'\n        {{unread-messages-intersection-observer}}\n      >\n        {{@contact.numUnread}}\n      </span>\n    {{/if}}\n  </a>\n\n  {{#if this.canBePinned}}\n    <button\n      data-test-contact-pin\n      type='button'\n      class='button-link'\n      {{on 'click' this.togglePin}}\n    >\n      <FaIcon\n        class='transition-all-fast'\n        @icon='thumbtack'\n        @rotation={{if @contact.isPinned 0 90}}\n      />\n    </button>\n  {{/if}}\n\n</div>\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/contact/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { TABLET_WIDTH } from 'emberclear/utils/breakpoints';\n\nimport { currentUserId } from '@emberclear/local-account';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type SettingsService from 'emberclear/services/settings';\nimport type SidebarService from 'emberclear/services/sidebar';\n\ninterface IArgs {\n  contact: Contact;\n}\n\n// TODO: need to write tests for the different states\n//       of a contact list row.\nexport default class SidebarContact extends Component<IArgs> {\n  @service router!: RouterService;\n  @service store!: StoreService;\n  @service settings!: SettingsService;\n  @service sidebar!: SidebarService;\n\n  get isActive() {\n    return this.router.currentURL.includes(this.args.contact.id);\n  }\n\n  get hasUnread() {\n    return this.args.contact.numUnread > 0;\n  }\n\n  @action\n  async onClick() {\n    if (window.innerWidth < TABLET_WIDTH) {\n      // non-blocking\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      this.sidebar.hide();\n    }\n\n    await this.router.transitionTo('chat.privately-with', this.args.contact.id);\n  }\n\n  @action\n  togglePin() {\n    const { contact } = this.args;\n\n    contact.isPinned = !contact.isPinned;\n\n    return contact.save();\n  }\n\n  get canBePinned() {\n    const { contact } = this.args;\n\n    // can't pin your own chat\n    return contact.id !== currentUserId;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/data/index.hbs",
    "content": "{{yield\n  (hash\n    chats=this.chats\n    contacts=this.displayedChats\n    channels=this.displayedChannels\n    numberOffline=this.numberOffline\n    showOfflineCounter=this.showOfflineCounter\n  )\n}}"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/data/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport { idFrom, PRIVATE_CHAT_REGEX } from 'emberclear/utils/route-matchers';\n\nimport { Status } from '@emberclear/local-account';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { Channel, Contact, CurrentUserService } from '@emberclear/local-account';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\nimport type SettingsService from 'emberclear/services/settings';\nimport type SidebarService from 'emberclear/services/sidebar';\n\ntype Args = {\n  searchText?: string;\n};\n\nexport default class SidebarChatData extends Component<Args> {\n  @service currentUser!: CurrentUserService;\n  @service settings!: SettingsService;\n  @service router!: RouterService;\n  @service store!: StoreService;\n  @service contactManager!: ContactManager;\n  @service sidebar!: SidebarService;\n\n  get allContacts() {\n    return this.store.peekAll('contact').toArray();\n  }\n\n  get allChannels() {\n    return this.store.peekAll('channel').toArray();\n  }\n\n  get channels() {\n    let { searchText } = this.args;\n    let channels = this.allChannels;\n\n    if (!searchText) {\n      return channels;\n    }\n\n    return channels.filter(searchByName(searchText));\n  }\n\n  get contacts() {\n    let { searchText } = this.args;\n    let contacts = this.allContacts;\n    let urlId = this.idFromURL;\n\n    if (this.hideOfflineContacts && !searchText) {\n      contacts = contacts.filter((contact) => {\n        return (\n          // online or other online~ish status\n          contact.onlineStatus !== Status.OFFLINE ||\n          // pinned contacts always show\n          contact.isPinned ||\n          // we are currently viewing the contact\n          contact.uid === urlId ||\n          // the contact has sent us messages that we haven't seen yet\n          contact.numUnread > 0\n        );\n      });\n    }\n\n    if (searchText !== undefined) {\n      contacts = contacts.filter(searchByName(searchText));\n    }\n\n    return contacts.sort(sortByPinned).slice(0, 40);\n  }\n\n  get chats() {\n    return ['add-contact', this.currentUser.record, ...this.contacts, ...this.channels];\n  }\n\n  get displayedChats() {\n    return ['add-contact', this.currentUser.record, ...this.contacts];\n  }\n\n  get displayedChannels() {\n    return ['add-channel', ...this.channels];\n  }\n\n  get idFromURL() {\n    let url = this.router.currentURL;\n\n    return idFrom(PRIVATE_CHAT_REGEX, url);\n  }\n\n  get hideOfflineContacts() {\n    return this.settings.hideOfflineContacts;\n  }\n\n  get offlineContacts() {\n    let contacts = this.contacts;\n\n    return this.allContacts.filter((contact) => {\n      return !contacts.includes(contact);\n    });\n  }\n\n  get numberOffline() {\n    return this.offlineContacts.length;\n  }\n\n  get showOfflineCounter() {\n    return this.hideOfflineContacts && this.numberOffline > 0;\n  }\n}\n\nfunction sortByPinned(contact1: Contact, contact2: Contact) {\n  if (contact1.isPinned === contact2.isPinned) {\n    return 0;\n  } else if (contact1.isPinned) {\n    return -1;\n  }\n\n  return 1;\n}\n\nfunction searchByName<T extends Contact | Channel>(text: string) {\n  let term = new RegExp(text, 'i');\n\n  return (contact: T) => {\n    return term.test(contact.name);\n  };\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/index.hbs",
    "content": "{{!-- template-lint-disable no-inline-styles --}}\n<nav class='contacts' style='overflow-y: hidden;' data-test-contacts-list>\n  {{!-- template-lint-disable require-input-label --}}\n  {{#if this.contactManager.isImporting}}\n    <EllipsisLoader />\n  {{else}}\n\n    <input\n      class='input mar-b-sm'\n      type='text'\n      data-test-sidebar-search\n      placeholder={{t 'ui.sidebar.find'}}\n      {{on 'input' this.handleSearch}}\n    >\n\n    <App::Sidebar::Chats::Data @searchText={{this.searchText}} as |data|>\n\n      {{#if this._searchText}}\n        <span data-test-search-info class='results-info'>\n          {{#if this.hasEnoughToSearch}}\n            {{t 'ui.sidebar.results' num=(sub data.chats.length 2)}}\n          {{else}}\n            {{t 'ui.sidebar.keepTyping' num=this.remainingCharacters}}\n          {{/if}}\n        </span>\n      {{/if}}\n\n      {{#if (eq @type 'all')}}\n        <App::Sidebar::Chats::List @chats={{data.chats}} />\n      {{else if (eq @type 'chats')}}\n        <App::Sidebar::Chats::List @chats={{data.contacts}} />\n      {{else if (eq @type 'channels')}}\n        <App::Sidebar::Chats::List @chats={{data.channels}} />\n      {{/if}}\n\n      {{#if data.showOfflineCounter}}\n        <App::Sidebar::Chats::OfflineCounter @numberOffline={{data.numberOffline}} />\n      {{/if}}\n\n    </App::Sidebar::Chats::Data>\n\n  {{/if}}\n\n</nav>\n\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\n\nconst REQUIRED_CHARACTERS_TO_SEARCH = 2;\n\nexport default class ContactsSidebar extends Component {\n  @service contactManager!: ContactManager;\n  @service intl!: Intl;\n\n  @tracked searchText?: string;\n\n  @tracked _searchText = '';\n\n  @action\n  handleSearch(e: Event) {\n    this._searchText = (e.target as HTMLInputElement).value;\n\n    if (this.hasEnoughToSearch) {\n      this.searchText = this._searchText;\n\n      return;\n    }\n\n    if (this.searchText !== undefined) {\n      this.searchText = undefined;\n    }\n  }\n\n  get hasEnoughToSearch() {\n    return this._searchText.length >= REQUIRED_CHARACTERS_TO_SEARCH;\n  }\n\n  get remainingCharacters() {\n    return REQUIRED_CHARACTERS_TO_SEARCH - this._searchText.length;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/list.hbs",
    "content": "{{#each @chats as |chat|}}\n{{!--\n{{#vertical-collection\n  @chats\n  estimateHeight=40 staticHeight=true bufferSize=5\n  as |chat|\n}}\n--}}\n  {{#if (eq chat 'add-contact')}}\n    <div \n      data-test-sidebar-content-header\n      data-test-sidebar-contacts-header\n      class='menu-label'\n    >\n      {{t 'ui.sidebar.contacts.title'}}\n\n      <a\n        data-test-add-friend\n        class='button button-xs'\n        href={{url-for 'add-friend'}}\n        {{on 'click' (handle-sidebar-click (transition-to 'add-friend'))}}\n      >\n        <span class='icon'><FaIcon @icon='plus' /></span>\n      </a>\n    </div>\n  {{else if (eq chat 'add-channel')}}\n    <div\n      data-test-sidebar-content-header\n      class='menu-label'\n    >\n      {{t 'ui.sidebar.channels.title'}}\n\n      <a\n        data-test-add-channel\n        class='button button-xs'\n        {{!-- TODO: link to creating channels --}} href={{url-for 'add-friend'}}\n        {{!-- TODO: link to creating channels --}} {{on 'click' (handle-sidebar-click (transition-to 'add-friend'))}}\n      >\n        <span class='icon'>{{fa-icon 'plus'}}</span>\n      </a>\n    </div>\n  {{else if (or (is-contact chat) (is-current-user chat))}}\n    <App::Sidebar::Chats::Contact @contact={{chat}} />\n  {{else if (is-channel chat)}}\n    <App::Sidebar::Chats::Channel @channel={{chat}} />\n  {{/if}}\n{{!-- {{/vertical-collection}} --}}\n{{/each}}"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/chats/offline-counter.hbs",
    "content": "<em data-test-offline-count class='offline'>\n  <small>{{t 'ui.sidebar.contacts.numOffline' num=@numberOffline}}</small>\n</em>"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/footer.hbs",
    "content": "<footer>\n  <hr>\n  <div>\n    <nav>\n      <ModalStatic @name='search' as |isActive actions|>\n        <button class='button button-link' type='button' {{on 'click' actions.toggle}}>\n          <FaIcon @icon='search' @prefix='fas' />\n        </button>\n      </ModalStatic>\n\n      <a\n        class='button button-link'\n        href={{url-for 'qr'}}\n        title={{t 'routes.qr'}}\n        {{on 'click' (handle-sidebar-click (transition-to 'qr'))}}\n      >\n        <FaIcon @icon='qrcode' />\n      </a>\n\n      <a\n        class='button button-link'\n        href={{url-for 'contacts'}}\n        title={{t 'routes.contacts'}}\n        {{on 'click' (handle-sidebar-click (transition-to 'contacts'))}}\n      >\n        <FaIcon @icon='address-book' @prefix='fas' />\n      </a>\n\n      <a\n        class='button button-link'\n        href={{url-for 'settings'}}\n        title={{t 'routes.settings'}}\n        {{on 'click' (handle-sidebar-click (transition-to 'settings'))}}\n      >\n        <FaIcon @icon='sliders-h' />\n      </a>\n\n      <a\n        class='button button-link'\n        href={{url-for 'logout'}}\n        title={{t 'routes.logout'}}\n        {{on 'click' (handle-sidebar-click (transition-to 'logout'))}}\n      >\n        <FaIcon @icon='sign-out-alt' @prefix='fas' />\n      </a>\n    </nav>\n  </div>\n</footer>"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/index.hbs",
    "content": "<aside ...attributes>\n  {{#if (has-feature-flag 'democracy-ui')}}\n    <nav class='sidebar-nav'>\n      <button\n        id='contacts-tab'\n        type='button'\n        class='sidebar-tab {{if this.isTabContacts 'sidebar-tab-selected'}}'\n        data-test-tab-contacts\n        {{on 'click' (fn this.switchToTab this.Tab.Contacts)}}\n      >\n        {{t 'ui.sidebar.contacts.title'}}\n      </button>\n\n      <button\n        id='channels-tab'\n        type='button'\n        class='sidebar-tab {{if this.isTabChannels 'sidebar-tab-selected'}}'\n        data-test-tab-channels\n        {{on 'click' (fn this.switchToTab this.Tab.Channels)}}\n      >\n        {{t 'ui.sidebar.channels.title'}}\n      </button>\n\n      <button\n        id='actions-tab'\n        type='button'\n        class='sidebar-tab {{if this.isTabActions 'sidebar-tab-selected'}}'\n        data-test-tab-actions\n        {{on 'click' (fn this.switchToTab this.Tab.Actions)}}\n      >\n        {{t 'ui.sidebar.actions.title'}}\n      </button>\n    </nav>\n  {{/if}}\n\n  <div>\n    {{#if (has-feature-flag 'democracy-ui')}}\n      {{#if this.isTabContacts}}\n        {{#if this.hasUnreadAbove}}\n          <App::Sidebar::UnreadIndicator\n            @onClick={{this.scrollUpToNearestUnread}}\n            @direction='up'\n          />\n        {{/if}}\n\n        <App::Sidebar::Chats @type='chats'/>\n\n        {{#if this.hasUnreadBelow}}\n          <App::Sidebar::UnreadIndicator\n            @onClick={{this.scrollDownToNearestUnread}}\n            @direction='down'\n          />\n        {{/if}}\n      {{else if this.isTabChannels}}\n        {{#if this.hasUnreadAbove}}\n          <App::Sidebar::UnreadIndicator\n            @onClick={{this.scrollUpToNearestUnread}}\n            @direction='up'\n          />\n        {{/if}}\n\n        <App::Sidebar::Chats @type='channels'/>\n\n        {{#if this.hasUnreadBelow}}\n          <App::Sidebar::UnreadIndicator\n            @onClick={{this.scrollDownToNearestUnread}}\n            @direction='down'\n          />\n        {{/if}}\n      {{else}}\n        <App::Sidebar::Actions />\n      {{/if}}\n    {{else}}\n      {{#if this.hasUnreadAbove}}\n        <App::Sidebar::UnreadIndicator\n          @onClick={{this.scrollUpToNearestUnread}}\n          @direction='up'\n        />\n      {{/if}}\n\n      <App::Sidebar::Chats @type='all'/>\n\n      {{!-- <SlideTransition @isVisible={{this.hasUnreadBelow}}> --}}\n      {{#if this.hasUnreadBelow}}\n        <App::Sidebar::UnreadIndicator\n          @onClick={{this.scrollDownToNearestUnread}}\n          @direction='down'\n        />\n      {{/if}}\n    {{/if}}\n\n  </div>\n\n  {{#if this.isLoggedIn}}\n    <App::Sidebar::Footer />\n  {{/if}}\n</aside>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { scrollIntoViewOfParent } from 'emberclear/utils/dom/utils';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Modals from 'emberclear/services/modals';\nimport type SidebarService from 'emberclear/services/sidebar';\n\nconst Tab = {\n  Contacts: 'contacts',\n  Channels: 'channels',\n  Actions: 'actions',\n} as const;\n\ntype TabKeys = keyof typeof Tab;\ntype TAB = typeof Tab[TabKeys];\n\nexport default class Sidebar extends Component {\n  Tab = Tab;\n\n  @service sidebar!: SidebarService;\n  @service currentUser!: CurrentUserService;\n  @service modals!: Modals;\n  @tracked selectedTab: TAB = this.Tab.Contacts;\n\n  get isShown() {\n    return this.sidebar.isShown;\n  }\n\n  get hasUnreadAbove() {\n    return this.sidebar.hasUnreadAbove;\n  }\n\n  get hasUnreadBelow() {\n    return this.sidebar.hasUnreadBelow;\n  }\n\n  get name() {\n    return this.currentUser.name;\n  }\n\n  get isLoggedIn() {\n    return this.currentUser.isLoggedIn;\n  }\n\n  @action\n  scrollDownToNearestUnread() {\n    const scrollable = document.querySelector('.sidebar-wrapper aside')!;\n    const lastRow = scrollable.querySelector('.tag')!;\n\n    scrollIntoViewOfParent(scrollable, lastRow);\n    this.sidebar.clearUnreadBelow();\n  }\n\n  @action\n  scrollUpToNearestUnread() {\n    const scrollable = document.querySelector('.sidebar-wrapper aside')!;\n    const lastRow = scrollable.querySelector('.tag')!;\n\n    scrollIntoViewOfParent(scrollable, lastRow);\n    this.sidebar.clearUnreadAbove();\n  }\n\n  @action\n  switchToTab(tab: TAB) {\n    this.selectedTab = tab;\n  }\n\n  get isTabContacts(): boolean {\n    return this.selectedTab === this.Tab.Contacts;\n  }\n\n  get isTabChannels(): boolean {\n    return this.selectedTab === this.Tab.Channels;\n  }\n\n  get isTabActions(): boolean {\n    return this.selectedTab === this.Tab.Actions;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/sidebar/unread-indicator.hbs",
    "content": "{{!-- template-lint-disable no-inline-styles --}}\n<button\n  type='button'\n  class='\n    p-absolute {{if (eq @direction 'down') 'bottom' 'top'}}\n    button is-info is-small\n    m-b-sm\n    '\n  style='margin-top: 60px; z-index: 100;'\n  {{on 'click' @onClick}}\n>\n  <span class='icon'>\n    {{#if (eq @direction 'down')}}\n      <FaIcon @icon='angle-down' />\n    {{else}}\n      <FaIcon @icon='angle-up' />\n    {{/if}}\n  </span>\n\n  {{t 'ui.sidebar.unread'}}\n\n  <span class='icon'>\n    {{#if (eq @direction 'down')}}\n      <FaIcon @icon='angle-down' />\n    {{else}}\n      <FaIcon @icon='angle-up' />\n    {{/if}}\n  </span>\n</button>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/connection-status/index.hbs",
    "content": "<small class='link-status has-hover-reveal'>\n\n  {{#if this.status.isConnecting}}\n    <EllipsisLoader />\n  {{else if this.status.isConnected}}\n    <FaIcon @icon='wifi' />\n  {{else}}\n    <FaIcon @icon='exclamation-circle' />\n  {{/if}}\n\n  <span class='hover-reveal'>\n    {{t (concat 'connection.' (or this.status.text this.DISCONNECTED))}}\n  </span>\n\n</small>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/connection-status/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport { STATUS_DISCONNECTED } from '@emberclear/networking/utils/connection/connection-pool';\n\nimport type { ConnectionStatus as StatusService } from '@emberclear/networking';\n\nexport default class ConnectionStatus extends Component {\n  DISCONNECTED = STATUS_DISCONNECTED;\n\n  @service('connection/status') declare status: StatusService;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/current-chat-name/index.hbs",
    "content": "{{#if this.isChatVisible}}\n\n  <div class='current-chat' ...attributes>\n    <hr class='vertical hide-sm-down'>\n\n    <span class='chat-name'>\n      {{this.currentChat.name}}\n    </span>\n\n    {{#if (is-contact this.currentChat.record)}}\n      <StatusIcon @contact={{this.currentChat.record}} />\n    {{/if}}\n  </div>\n\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/current-chat-name/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\nimport { isPresent } from '@ember/utils';\n\nimport type CurrentChatService from 'emberclear/services/current-chat';\n\nexport default class extends Component {\n  @service currentChat!: CurrentChatService;\n\n  get isChatVisible() {\n    return isPresent(this.currentChat.name);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/index.hbs",
    "content": "<div data-test-top-nav class='top-nav {{if this.isInverted 'inverted'}}'>\n\n  <div class='left-nav'>\n    {{#if this.currentUser.isLoggedIn}}\n      <div {{update-document-title this.unreadMessageText}}>\n        <HamburgerButton\n          @onClick={{this.sidebar.toggle}}\n          @isActive={{this.sidebar.isShown}}\n        />\n\n        {{#if this.hasUnread}}\n          {{!-- template-lint-disable no-inline-styles --}}\n          <span\n            data-test-unread-count\n            class='badge badge-warning'\n            style='left: 2rem; bottom: 0.5rem;'\n          >\n            {{this.unreadMessageCount}}\n          </span>\n        {{/if}}\n      </div>\n\n      <LinkTo @route='application' class='left hide-sm-down'>\n        {{t 'appname'}}\n      </LinkTo>\n\n      <App::TopNav::CurrentChatName />\n      <App::TopNav::ConnectionStatus />\n\n    {{else}}\n\n      <LinkTo @route='application' class='left {{if this.isChat this.textColor}}'>\n        {{t 'appname'}}\n      </LinkTo>\n\n    {{/if}}\n\n  </div>\n\n\n  <div class='right-nav'>\n    {{#unless this.currentUser.isLoggedIn}}\n      <LinkTo @route='login' class='hide-md-up'>\n        {{t 'routes.login'}}\n      </LinkTo>\n    {{/unless}}\n\n    <App::TopNav::Install />\n\n    <LinkTo @route='qr'>\n      <FaIcon @icon='qrcode' />\n    </LinkTo>\n\n    <App::TopNav::LocaleSelect />\n\n    {{#if this.currentUser.isLoggedIn}}\n      <LinkTo @route='chat' class='hide-sm-down'>\n        {{t 'routes.chat'}}\n      </LinkTo>\n\n      <App::TopNav::UserDropMenu class='hide-xs-down' />\n    {{else}}\n      <LinkTo @route='login' class='hide-sm-down'>\n        {{t 'routes.login'}}\n      </LinkTo>\n\n      <LinkTo @route='setup' class='hide-sm-down'>\n        {{t 'routes.createNewUser'}}\n      </LinkTo>\n    {{/if}}\n  </div>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport { selectUnreadMessages } from '@emberclear/networking/models/message/utils';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Sidebar from 'emberclear/services/sidebar';\n\nexport default class TopNav extends Component {\n  @service declare currentUser: CurrentUserService;\n  @service declare router: RouterService;\n  @service declare sidebar: Sidebar;\n  @service declare store: StoreService;\n\n  get isInverted(): boolean {\n    return this.router.currentRouteName !== 'index';\n  }\n\n  get allMessages() {\n    return this.store.peekAll('message');\n  }\n\n  get hasUnread() {\n    return this.unreadMessageCount > 0;\n  }\n\n  get unreadMessageCount() {\n    if (!this.allMessages) return 0;\n\n    const unread = selectUnreadMessages(this.allMessages.toArray());\n\n    return unread.length;\n  }\n\n  get unreadMessageText() {\n    return this.unreadMessageCount || '';\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/install/index.hbs",
    "content": "{{#if this.window.canInstall}}\n  <button type='button' {{on 'click' this.install}}>\n    {{t 'install'}}\n  </button>\n{{/if}}"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/install/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type WindowService from 'emberclear/services/window';\n\nexport default class Install extends Component {\n  @service window!: WindowService;\n\n  @action\n  install() {\n    return this.window.promptInstall();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/locale-select/-page.ts",
    "content": "import { clickable, collection, create, hasClass, text } from 'ember-cli-page-object';\n\nexport const definition = {\n  scope: '[data-test-locale-select]',\n  isOpen: hasClass('active'),\n  toggle: clickable('[data-test-locale-toggle]'),\n  options: collection('.navbar-item', {\n    text: text('span'),\n  }),\n  optionFor(lang: string) {\n    return this.options.findOne((option) => {\n      return option.text === lang;\n    });\n  },\n};\n\nexport const page = create(definition);\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/locale-select/index.hbs",
    "content": "<div\n  data-test-locale-select\n  class='\n    dropdown dropdown-left\n    {{if this.isActive 'active'}}\n  '\n>\n  <button\n    data-test-locale-toggle\n    type='button'\n    class='button-secondary'\n    aria-haspopup='true'\n    aria-controls='dropdown-menu'\n    {{on 'click' this.toggleMenu}}\n  >\n    <FaIcon @icon='globe' />\n  </button>\n\n  <div\n    {{did-insert this.didInsert}}\n    class='dropdown-menu'\n    role='menu'\n  >\n    {{#each this.options as |opt|}}\n      <button\n        type='button'\n        class='button-link navbar-item'\n        {{on 'click' (queue\n          (fn this.chooseLanguage opt.locale)\n          this.closeMenu\n        )}}\n      >\n        <span>{{opt.label}}</span>\n      </button>\n    {{/each}}\n  </div>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/locale-select/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type LocaleService from 'emberclear/services/locale';\n\nexport default class LocaleSwitcher extends Component {\n  @service locale!: LocaleService;\n\n  @tracked isActive = false;\n\n  dropdown!: HTMLDivElement;\n\n  options = [\n    { locale: 'de-de', label: 'Deutsch' },\n    { locale: 'en-us', label: 'English' },\n    { locale: 'es-es', label: 'Español' },\n    { locale: 'fr-fr', label: 'Français' },\n    { locale: 'pt-pt', label: 'Português' },\n    { locale: 'ru-ru', label: 'Русский' },\n  ];\n\n  get currentLanguage() {\n    const current = this.options.find((opt: any) => opt.locale === this.locale.currentLocale);\n\n    if (current) {\n      return current.label;\n    }\n\n    return this.options[1].label;\n  }\n\n  @action\n  didInsert(element: HTMLDivElement) {\n    this.dropdown = element;\n  }\n\n  @action\n  toggleMenu() {\n    this.isActive = !this.isActive;\n  }\n\n  @action\n  closeMenu() {\n    this.isActive = false;\n  }\n\n  @action\n  chooseLanguage(locale: string) {\n    return this.locale.setLocale(locale);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/user-drop-menu/-page.ts",
    "content": "import { clickable, create, hasClass, text } from 'ember-cli-page-object';\n\nexport const page = create({\n  scope: '[data-test-top-nav] .right-nav [data-test-dropdown]',\n\n  toggle: clickable('.dropdown-trigger'),\n\n  isOpen: hasClass('active'),\n\n  userName: text('section strong'),\n  uid: text('section em'),\n\n  logout: clickable('[data-test-logout]'),\n});\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/user-drop-menu/index.hbs",
    "content": "<Dropdown\n  @dir='left'\n  ...attributes\n  class='button-link'\n  data-test-user-dropdown\n>\n  <:trigger as |toggle|>\n    <button\n      type='button'\n      class='dropdown-trigger hide-xs-down button-link'\n      {{on 'click' toggle}}\n    >\n      {{this.currentUser.name}}\n    </button>\n  </:trigger>\n\n  <:content as |close|>\n    <div class='user-dropdown-content'>\n      <section>\n        <strong>{{this.currentUser.name}}</strong>\n        <em>{{this.currentUser.uid}}</em>\n\n        <br>\n        <br>\n\n        <div class='cta-with-fallback'>\n          <LinkTo @route='faq'>\n            {{t 'routes.faq'}}\n          </LinkTo>\n\n          <CopyTextButton\n            class='button-link'\n            @text={{this.currentUser.shareUrl}}\n            @label={{t 'ui.invite.copyProfile'}}\n          />\n\n        </div>\n      </section>\n\n      <hr class='navbar-divider'>\n\n      <div class='cta-with-fallback'>\n        <a\n          href={{url-for 'settings'}}\n          {{on 'click' (queue close (transition-to 'settings'))}}\n        >\n          <FaIcon @icon='sliders-h' @prefix='fas' />\n          <span>{{t 'routes.settings'}}</span>\n        </a>\n        <a\n          data-test-logout\n          href={{url-for 'logout'}}\n          {{on 'click' (queue close (transition-to 'logout'))}}\n        >\n          <FaIcon @icon='sign-out-alt' @prefix='fas' />\n          <span>{{t 'routes.logout'}}</span>\n        </a>\n      </div>\n    </div>\n  </:content>\n</Dropdown>\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/top-nav/user-drop-menu/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class UserDropMenu extends Component {\n  @service currentUser!: CurrentUserService;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/app/update-checker.ts",
    "content": "import Component from '@glimmer/component';\nimport { setComponentTemplate } from '@ember/component';\nimport { inject as service } from '@ember/service';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport type RouterService from '@ember/routing/router-service';\n\nclass UpdateChecker extends Component {\n  @service declare router: RouterService;\n}\n\n// add @hasUpdate={{true}} to test manually\nexport default setComponentTemplate(\n  hbs`\n  <ServiceWorkerUpdateNotify>\n    <a\n      class='service-worker-update-notify alert alert-info has-shadow'\n      href={{this.router.currentURL}}\n      style='z-index: 100;'\n    >\n      {{t 'status.updateAvailable'}}\n    </a>\n  </ServiceWorkerUpdateNotify>\n`,\n  UpdateChecker\n);\n"
  },
  {
    "path": "client/web/emberclear/app/components/app-footer.hbs",
    "content": "{{! template-lint-disable no-bare-strings }}\n<footer class='footer'>\n  <div class='container pad-b-md'>\n    <div class='row'>\n      <div class='col mar-t-md'>\n        <h4>{{t 'ui.footer.navigation'}}</h4>\n\n        <LinkTo @route='application'>{{t 'routes.home'}}</LinkTo><br>\n        <LinkTo @route='chat'>{{t 'routes.chat'}}</LinkTo><br>\n        <LinkTo @route='settings'>{{t 'routes.settings'}}</LinkTo><br>\n        <LinkTo @route='setup'>{{t 'routes.createNewUser'}}</LinkTo><br>\n\n        <br>\n        <h4>{{t 'ui.footer.about'}}</h4>\n\n        <LinkTo @route='faq' data-test-footer-faq>{{t 'routes.faq'}}</LinkTo><br>\n        <ExternalLink href='/bundle.html'>{{t 'bundle-analysis'}}</ExternalLink><br>\n        Made with ❤️ in <ExternalLink href='https://emberjs.com'>{{t 'emberjs'}}</ExternalLink>\n      </div>\n\n\n      <div class='col mar-t-md'>\n        <strong>{{t 'appname'}}</strong> {{t 'authoredBy'}}\n        <ExternalLink href='https://github.com/NullVoxPopuli/'>NullVoxPopuli</ExternalLink>.\n        <br><br>\n        {{t 'ui.footer.license' htmlSafe=true}}\n        <br><br>\n\n        <ExternalLink href='https://commerce.coinbase.com/checkout/377a2db4-46eb-41df-9206-1cada43f050f'>\n          {{t 'ui.footer.wantToSupport'}}\n          <br>\n          {{t 'buttons.donate'}}\n        </ExternalLink>\n        <br>\n        <br>\n\n        <ExternalLink href='https://reddit.com/r/emberclear'>\n          <FaIcon @icon='reddit' @prefix='fab' /> {{t 'redditCommunity'}}\n        </ExternalLink>\n        <br>\n        <ExternalLink href='https://twitter.com/emberclear_io'>\n          <FaIcon @icon='twitter' @prefix='fab' /> {{t 'twitter'}}\n        </ExternalLink>\n      </div>\n    </div>\n  </div>\n</footer>\n"
  },
  {
    "path": "client/web/emberclear/app/components/copy-text-button.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { setComponentTemplate } from '@ember/component';\nimport { action } from '@ember/object';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport { timeout } from 'ember-concurrency';\n\n// TODO: use {{#if (is-clipboard-supported)}}\n//       to not show the clipboard, maybe the URL instead?\nclass CopyTextButton extends Component {\n  @tracked copied = false;\n\n  @action\n  async copySuccess() {\n    this.copied = true;\n\n    await timeout(2000);\n\n    this.copied = false;\n  }\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <CopyButton\n    @success={{this.copySuccess}}\n    @clipboardText={{@text}}\n    class='has-status-tip {{if this.copied 'is-active'}}'\n    ...attributes\n  >\n    {{@label}}\n\n    <HoverTip @animationClasses='floats-up'>\n      {{t 'ui.invite.copied'}}\n    </HoverTip>\n  </CopyButton>\n  `,\n  CopyTextButton\n);\n"
  },
  {
    "path": "client/web/emberclear/app/components/embedded-media.hbs",
    "content": "{{#if @meta.isYouTube}}\n  <iframe\n    title={{@meta.title}}\n    allowfullscreen\n    class='card-content'\n    src={{@meta.embedUrl}}\n    width='560'\n    height='315'\n    frameborder='0'\n    allow='autoplay; encrypted-media'\n  >\n  </iframe>\n\n{{else if @meta.isImage}}\n\n  <img alt={{@meta.alt}} class='card-content' src={{@url}}>\n\n{{else if @meta.isVideo }}\n\n  <video class='card-content' controls loop>\n    <source src={{@url}} type='video/mp4'>\n  </video>\n\n{{/if}}"
  },
  {
    "path": "client/web/emberclear/app/components/error-card/index.hbs",
    "content": "<div data-test-error class='card' ...attributes>\n\n  <header>{{t 'errors.genericTitle'}}</header>\n\n  <p data-test-message>\n    {{@message}}\n  </p>\n\n  <footer class='cta'>\n    {{#if @retry}}\n      <button data-test-retry type='button' class='button-secondary' {{on 'click' @retry}}>\n        {{t 'buttons.retry'}}\n      </button>\n    {{/if}}\n  </footer>\n</div>"
  },
  {
    "path": "client/web/emberclear/app/components/fetch-open-graph.ts",
    "content": "import Component from '@glimmer/component';\nimport { setComponentTemplate } from '@ember/component';\nimport { inject as service } from '@ember/service';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { normalizeMeta } from 'emberclear/utils/normalized-meta';\n\nimport type ConnectionService from '@emberclear/networking/services/connection';\nimport type ConnectionStatusService from '@emberclear/networking/services/connection/status';\n\ntype Args = {\n  url: string;\n};\n\nclass FetchOpenGraphComponent extends Component<Args> {\n  @service('connection/status') declare status: ConnectionStatusService;\n  @service('connection') declare connection: ConnectionService;\n\n  constructor(owner: any, args: Args) {\n    super(owner, args);\n\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.request).perform();\n  }\n\n  // everything is private API in here.\n\n  get meta() {\n    let { url } = this.args;\n\n    return normalizeMeta({\n      url,\n      openGraph: taskFor(this.request).lastSuccessful?.value,\n    });\n  }\n\n  @task({ withTestWaiter: true })\n  async request() {\n    await waitUntil(() => this.status.isConnected);\n\n    let og = await this.connection.getOpenGraph(this.args.url);\n\n    return og;\n  }\n}\n\nexport default setComponentTemplate(\n  hbs`\n  {{!--\n    <FetchOpenGraph @url={{...}} as |isLoading data|>\n\n    </FetchOpenGraph>\n\n  --}}\n  {{yield\n    this.request.isRunning\n    (hash\n      result=this.request.lastSuccessful.value\n      error=this.request.lastErrored.value\n      meta=this.meta\n    )\n  }}\n  `,\n  FetchOpenGraphComponent\n);\n\nfunction waitUntil(callback: () => boolean): Promise<void> {\n  return new Promise((resolve) => {\n    let interval: any;\n\n    interval = setInterval(() => {\n      let result = callback();\n\n      if (result) {\n        clearInterval(interval);\n        resolve();\n      }\n    }, 100);\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/file-chooser/index.hbs",
    "content": "{{!-- template-lint-disable no-inline-styles --}}\n{{!-- template-lint-disable require-input-label --}}\n<input\n  style='display: none;'\n  type='file'\n  {{on 'change' this.didChooseFile}}\n  {{did-insert this.bindInput}}\n>\n\n{{yield (hash\n  openFileChooser=this.openFileChooser\n)}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/file-chooser/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\n\ninterface IArgs {\n  onChoose: (content: string) => void;\n}\n\nexport default class FileChooser extends Component<IArgs> {\n  inputElement!: HTMLInputElement;\n\n  @action\n  bindInput(element: HTMLInputElement) {\n    this.inputElement = element;\n  }\n\n  @action\n  openFileChooser() {\n    this.inputElement.click();\n  }\n\n  @action\n  didChooseFile(e: Event) {\n    const fileReader = new FileReader();\n    const fileInput = e.target as HTMLInputElement;\n    const file = fileInput.files?.[0] || new Blob();\n\n    if (!file) return;\n    if (file.size === 0) return;\n\n    fileReader.onload = (event) => {\n      const content = (event.target as any)!.result;\n\n      this.args.onChoose(content);\n    };\n\n    fileReader.readAsText(file);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/keyboard-shortcuts/index.hbs",
    "content": "<Modal\n  @isActive={{@isActive}}\n  @close={{@close}}\n  @disableFocusTrap={{true}}\n  data-test-keyboard-shortcuts\n>\n\n  <div class='flex-row align-items-center'>\n    <h3 class='is-size-3'>\n      {{t 'ui.shortcuts.title'}}\n    </h3>\n    <span class='m-l-md'>\n      <KeyboardShortcuts::Key @label='ctrl' /> + <KeyboardShortcuts::Key @label='slash' /> <br>\n    </span>\n  </div>\n\n  <hr>\n\n  <p class='content'>\n    <label class='is-uppercase'>\n      {{t 'ui.shortcuts.actions.search'}}\n    </label><br>\n    <KeyboardShortcuts::Key @label='ctrl' /> + <KeyboardShortcuts::Key @label='k' /> <br>\n  </p>\n\n  <p class='content'>\n    <label class='is-uppercase'>\n      {{t 'ui.shortcuts.actions.toggleSidebar'}}\n    </label><br>\n    <KeyboardShortcuts::Key @label='ctrl' /> + <KeyboardShortcuts::Key @label='space' /> <br>\n  </p>\n\n  <p class='content'>\n    <label class='is-uppercase'>\n      {{t 'ui.shortcuts.actions.toggleHelp'}}\n    </label><br>\n    <KeyboardShortcuts::Key @label='ctrl' /> + <KeyboardShortcuts::Key @label='h' /> <br>\n    <KeyboardShortcuts::Key @label='ctrl' /> + <KeyboardShortcuts::Key @label='slash' /> <br>\n\n  </p>\n\n</Modal>\n"
  },
  {
    "path": "client/web/emberclear/app/components/keyboard-shortcuts/key/index.hbs",
    "content": "<i class='key'>{{t (concat 'ui.shortcuts.key.' @label)}}</i>\n"
  },
  {
    "path": "client/web/emberclear/app/components/mnemonic-display.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { setComponentTemplate } from '@ember/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport type { CryptoConnector, WorkersService } from '@emberclear/crypto';\n\ntype Args = {\n  crypto?: CryptoConnector;\n};\n\nclass Mnemonic extends Component<Args> {\n  @service('intl') declare intl: Intl;\n  @service declare workers: WorkersService;\n\n  @tracked privateKey?: Uint8Array;\n  @tracked mnemonic?: string;\n\n  @action\n  async updateMnemonic() {\n    let result = '';\n    let { crypto } = this.args;\n\n    if (!crypto) {\n      result = this.intl.t('services.crypto.keyGenFailed');\n    } else {\n      result = await crypto.mnemonicFromNaClBoxPrivateKey();\n    }\n\n    this.mnemonic = result\n      .split(/((?:\\w+ ){5})/g)\n      .filter(Boolean)\n      .join('\\n')\n      .trim();\n  }\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <pre\n    data-test-mnemonic\n    class='wrap'\n    {{did-insert this.updateMnemonic}}\n  >\n    {{this.mnemonic}}\n  </pre>\n`,\n  Mnemonic\n);\n"
  },
  {
    "path": "client/web/emberclear/app/components/modal-static/index.hbs",
    "content": "{{yield\n  this.isActive\n  (hash\n    toggle=this.toggle\n    close=this.close\n    open=this.open\n  )\n}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/modal-static/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type Modals from 'emberclear/services/modals';\n\ninterface IArgs {\n  name: string;\n  initiallyActive?: boolean;\n}\n\nexport default class ModalStatic extends Component<IArgs> {\n  @service declare modals: Modals;\n\n  constructor(owner: any, args: any) {\n    super(owner, args);\n\n    let { initiallyActive, name } = this.args;\n\n    if (initiallyActive) {\n      this.modals.open(name);\n    }\n  }\n\n  get modal() {\n    return this.modals.find(this.args.name);\n  }\n\n  get isActive() {\n    return this.modal.isActive;\n  }\n\n  @action\n  toggle() {\n    if (this.isActive) {\n      this.close();\n    } else {\n      this.open();\n    }\n  }\n\n  @action\n  close() {\n    this.modals.close(this.args.name);\n  }\n\n  @action\n  open() {\n    this.modals.open(this.args.name);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/add-friend/add-contact/index.hbs",
    "content": "<h3>{{t 'ui.addContact.title'}}</h3>\n\n{{#if this.scanning}}\n  <QRScanner @onScan={{this.onScan}} />\n{{else}}\n  <QRCode\n    @data={{this.publicIdentity}}\n    @alt={{t 'images.alt.ownIdentityQR'}}\n    class='qr-code-large inline-block'\n  />\n{{/if}}\n\n<div class='cta-with-fallback pad-t-md'>\n  <button type='button' {{on 'click' this.toggleScanning}}>\n    <FaIcon @icon='qrcode' @prefix='fas' />\n    <span>\n      {{#if this.scanning}}\n        {{t 'buttons.back'}}\n      {{else}}\n        {{t 'buttons.scan'}}\n      {{/if}}\n    </span>\n  </button>\n\n  <CopyTextButton\n    @text={{this.url}}\n    @label={{t 'ui.invite.copyProfile'}}\n  />\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/add-friend/add-contact/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\nimport type { ImportableIdentity } from '@emberclear/local-account/types';\n\nexport default class AddModal extends Component {\n  @service toast!: Toast;\n  @service currentUser!: CurrentUserService;\n  @service store!: StoreService;\n  @service contactManager!: ContactManager;\n\n  @tracked scanning = false;\n\n  get isLoggedIn() {\n    return this.currentUser.isLoggedIn;\n  }\n\n  get publicIdentity() {\n    if (!this.isLoggedIn) return {};\n\n    const { name, uid } = this.currentUser;\n\n    return { name, publicKey: uid };\n  }\n\n  get url() {\n    return this.currentUser.shareUrl;\n  }\n\n  @action\n  toggleScanning() {\n    this.scanning = !this.scanning;\n  }\n\n  @task\n  async handleScan(identityJson: string) {\n    try {\n      const identity = JSON.parse(identityJson);\n\n      await this.tryCreate(identity);\n    } catch (e) {\n      this.toast.error(e);\n    }\n\n    this.scanning = false;\n  }\n\n  @action\n  onScan(json: string) {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.handleScan).perform(json);\n  }\n\n  async tryCreate(identity: ImportableIdentity) {\n    const { name, publicKey } = identity;\n\n    if (!name || !publicKey) {\n      this.toast.error('Scan did not contain required information. Please try again.');\n      console.error(identity);\n\n      return;\n    }\n\n    let exists = this.store.peekRecord('contact', publicKey);\n\n    if (exists) {\n      this.toast.info('Friend already added!');\n\n      return;\n    }\n\n    await this.contactManager.create(publicKey, name);\n\n    this.toast.info(`${identity.name || 'Friend'} added!`);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/embeds-menu/index.hbs",
    "content": "<Dropdown\n  @dir={{'up'}}\n  class='button-secondary'\n  data-test-chat-options-dropdown\n>\n  <:trigger as |toggle|>\n   <button\n      type='button'\n      class='dropdown-trigger'\n      {{on 'click' toggle}}\n      ...attributes\n    >\n      <FaIcon @icon='plus' />\n    </button>\n  </:trigger>\n\n  <:content as |closeMenu|>\n    <ModalStatic @name='code-or-snippet' as |_isActive actions|>\n      <button\n        type='button'\n        class='button-link'\n        data-test-embeds-toggle\n        {{on 'click' (queue actions.toggle closeMenu)}}\n      >\n        <FaIcon @icon='code' />\n        <span>{{t 'ui.chat.embedMenu.code'}}</span>\n      </button>\n    </ModalStatic>\n\n    <button type='button' class='button-link text-muted' disabled>\n      <FaIcon @icon='bed' />\n      <span>{{t 'ui.chat.embedMenu.embed'}}</span>\n    </button>\n\n    <hr class='dropdown-divider'>\n\n    <button type='button' class='button-link text-muted' disabled>\n      <FaIcon @icon='desktop' />\n      <span>{{t 'ui.chat.embedMenu.file'}}</span>\n    </button>\n\n    <hr class='dropdown-divider'>\n\n    <button type='button' class='button-link text-muted' disabled>\n      <FaIcon @icon='video' />\n      <span>{{t 'ui.chat.embedMenu.video'}}</span>\n    </button>\n\n    <button type='button' class='button-link text-muted' disabled>\n      <FaIcon @icon='phone' />\n      <span>{{t 'ui.chat.embedMenu.audio'}}</span>\n    </button>\n  </:content>\n\n</Dropdown>\n\n<ModalStatic @name='code-or-snippet' as |isActive actions|>\n  <Pod::Chat::ChatEntry::EmbedsMenu::Snippet\n    @isActive={{isActive}}\n    @close={{actions.close}}\n    @sendTo={{@sendTo}}\n  />\n</ModalStatic>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/embeds-menu/snippet/-page.ts",
    "content": "import { click } from '@ember/test-helpers';\n\nimport { clickable, fillable, text } from 'ember-cli-page-object';\n\nimport { modal } from '@emberclear/ui/test-support/page-objects';\n\nexport const definition = {\n  scope: '[data-test-embeds-snippet-modal]',\n  ...modal,\n\n  title: text('h5'),\n  fillInTitle: fillable('input'),\n  fillInCode: fillable('textarea'),\n  selectLanguage(language: string) {\n    let scope = this.scope;\n\n    let select = document.querySelector(`${scope} select`)!;\n    let options = select.querySelectorAll('option')!;\n\n    let option = Array.from(options).find((option) => {\n      return option.textContent!.toLowerCase().includes(language.toLowerCase());\n    });\n\n    if (!option) {\n      throw new Error(`Option for text \\`${language}\\` not found`);\n    }\n\n    return click(option);\n  },\n\n  cancel: clickable('[data-test-cancel]'),\n  submit: clickable('[data-test-submit]'),\n};\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/embeds-menu/snippet/index.hbs",
    "content": "<Modal\n  data-test-embeds-snippet-modal\n  @isActive={{@isActive}}\n  @close={{@close}}\n>\n  <h5>\n    {{t 'ui.chat.codeModal.title'}} <strong>{{@sendTo.name}}</strong>\n  </h5>\n\n  <hr>\n\n  <div class='snippet-title-bar'>\n    {{!-- template-lint-disable require-input-label --}}\n    <Input\n      placeholder='Title (optional)'\n      @value={{this.title}}\n    />\n\n    <div>\n      <select {{on 'change' this.chooseLanguage}}>\n        {{#each this.languages as |availableLanguage|}}\n          <option value={{availableLanguage}}>{{availableLanguage}}</option>\n        {{/each}}\n      </select>\n    </div>\n  </div>\n\n  {{!-- template-lint-disable require-input-label --}}\n  <Textarea\n    autofocus={{true}}\n    class='snippet-entry'\n    @value={{this.text}}\n  />\n\n  <div class='button-group right-aligned'>\n    <button\n      data-test-cancel\n      type='button'\n      class='button-secondary'\n      {{on 'click' @close}}\n    >\n      {{t 'buttons.cancel'}}\n    </button>\n    <button\n      data-test-submit\n      type='button'\n      {{on 'click' this.sendMessage}}\n    >\n      {{t 'buttons.sendSnippet'}}\n    </button>\n  </div>\n</Modal>\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/embeds-menu/snippet/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { languages as allLanguages } from 'emberclear/services/prism-manager';\n\nimport type { Channel } from '@emberclear/local-account';\nimport type { Contact } from '@emberclear/local-account';\nimport type { MessageDispatcher } from '@emberclear/networking';\n\nconst codeDelimiter = '```';\n\ninterface Args {\n  isActive: boolean;\n  close: () => void;\n  sendTo: Contact | Channel;\n}\n\nexport default class SnippetModal extends Component<Args> {\n  @service('messages/dispatcher') declare messageDispatcher: MessageDispatcher;\n\n  languages = allLanguages;\n\n  @tracked text = '';\n  @tracked title = '';\n  @tracked language = '';\n\n  @action\n  sendMessage() {\n    let { sendTo, close } = this.args;\n\n    const messageParts = [\n      `*${this.title}*`,\n      '\\n',\n      `${codeDelimiter}${this.language}`,\n      this.text,\n      codeDelimiter,\n    ];\n\n    const message = messageParts.join('\\n');\n\n    // non-blocking\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.messageDispatcher.send(message, sendTo);\n    close();\n  }\n\n  @action\n  chooseLanguage({ target = {} as HTMLSelectElement }) {\n    this.language = target.value;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/index.hbs",
    "content": "<div class='chat-entry-container' ...attributes>\n  <div class='control'>\n    <Pod::Chat::ChatEntry::EmbedsMenu @sendTo={{@to}} />\n  </div>\n\n  {{!-- template-lint-disable require-input-label --}}\n  <form\n    data-test-chat-entry-form\n    class='chat-entry'\n    {{on 'submit' (prevent-default this.sendMessage)}}\n  >\n\n    <Field @label={{this.placeholder}} @hidden={{true}} as |id|>\n      {{!-- TODO: extract to a resizable textarea component --}}\n      <textarea\n        {{autofocus this.isDisabled}}\n        {{autoresize this.text}}\n        {{autostash @to.id this.text}}\n\n        {{on 'input' this.onInput}}\n        {{on 'keypress' this.onKeyPress}}\n\n        rows='1'\n        data-test-chat-entry\n        id={{id}}\n        disabled={{this.isDisabled}}\n        value={{this.text}}\n        class='transition-all-fast'\n        placeholder={{this.placeholder}}\n      />\n    </Field>\n\n    <input\n      data-test-chat-submit\n      disabled={{this.isSubmitDisabled}}\n      class='button flex-grow'\n      role='button'\n      type='submit'\n      value='Send'\n    >\n  </form>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-entry/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action, set } from '@ember/object';\nimport { inject as service } from '@ember/service';\nimport { waitForPromise } from '@ember/test-waiters';\n\nimport { unicode } from 'emojis';\n\nimport { Channel } from '@emberclear/local-account';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\nimport type { MessageDispatcher, MessageFactory } from '@emberclear/networking';\n\nconst EMOJI_REGEX = /:[^:]+:/g;\n\ninterface IArgs {\n  to: Contact | Channel;\n}\n\nexport default class ChatEntry extends Component<IArgs> {\n  @service('messages/dispatcher') messageDispatcher!: MessageDispatcher;\n  @service('messages/factory') messageFactory!: MessageFactory;\n  @service store!: StoreService;\n\n  @tracked isDisabled = false;\n  @tracked isSubmitDisabled = true;\n\n  declare text?: string;\n\n  get placeholder() {\n    const { to } = this.args;\n    let prefix = '';\n\n    if (to instanceof Channel) {\n      prefix = 'everyone in ';\n    }\n\n    return `Send a message to ${prefix}${to.name}`;\n  }\n\n  @action\n  async sendMessage() {\n    if (!this.text) return;\n\n    this.isDisabled = true;\n\n    await this.dispatchMessage(this.text);\n\n    this.isDisabled = false;\n    this.updateText('');\n  }\n\n  @action\n  updateText(text: string) {\n    set(this, 'text', text);\n\n    this.isSubmitDisabled = this.isDisabled || !text || text.length === 0;\n  }\n\n  @action\n  onInput(event: KeyboardEvent) {\n    const value = (event.target as any).value;\n\n    // TODO: only test the regex since the last detected `:`\n    // (for perf)\n    if (EMOJI_REGEX.test(value)) {\n      this.updateText(unicode(value));\n    } else {\n      this.updateText(value);\n    }\n  }\n\n  @action\n  onKeyPress(event: KeyboardEvent) {\n    const { keyCode, shiftKey } = event;\n\n    // don't submit when shift is being held.\n    if (!shiftKey && keyCode === 13) {\n      // non-blocking\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      this.sendMessage();\n\n      // prevent regular 'Enter' from inserting a linebreak\n      return false;\n    }\n\n    return true;\n  }\n\n  async dispatchMessage(text: string) {\n    await waitForPromise(this.messageDispatcher.send(text, this.args.to));\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/connection-status/index.hbs",
    "content": "{{#if this.status.hasUpdate}}\n  <div\n    class='\n      transition-all connection-status\n      alert alert-{{this.status.level}}\n      {{if this.status.hadUpdate 'fade-out'}}\n    '\n  >\n    {{this.status.text}}\n  </div>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/connection-status/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport type ConnectionStatusService from '@emberclear/networking/services/connection/status';\n\nexport default class ConnectionStatus extends Component {\n  @service('connection/status') declare status: ConnectionStatusService;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/index.hbs",
    "content": "<div\n  class='message-list'\n  {{unread-message-list-observer}}\n  {{message-scroll-listener @messages}}\n>\n\n  <Pod::Chat::ChatHistory::NotificationPrompt />\n  <Pod::Chat::ChatHistory::ConnectionStatus />\n  <Pod::Chat::ChatHistory::UnreadManagement @messages={{@messages}} @to={{@to}} />\n\n  <div class='messages'>\n    {{#each @messages as |message|}}\n      <Pod::Chat::ChatHistory::Message\n        @message={{message}}\n        {{maybe-nudge-to-bottom @messages message}}\n      />\n    {{/each}}\n  </div>\n\n  <Pod::Chat::ChatHistory::NewMessages />\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/delivery-confirmations/index.hbs",
    "content": "<div\n  data-test-confirmations\n  class='confirmations'\n  {{did-insert this.doWait}}\n>\n\n  {{#if this.wasSent}}\n    {{#if this.hasDeliveryConfirmations}}\n\n      <FaIcon @icon='check' @size='sm' aria-label='message delivered successfully' />\n\n    {{else if this.timedOut}}\n\n      <div class='has-hover-tip'>\n        {{#if this.deleteMessage.isIdle}}\n          <button\n            data-test-delete\n            type='button'\n            class='button-link'\n            {{on 'click' this.doDelete}}\n          >\n            {{t 'buttons.delete'}}\n          </button>\n        {{/if}}\n        |\n        {{#if this.resend.isIdle}}\n          <button\n            data-test-resend\n            type='button'\n            class='button-link'\n            {{on 'click' this.doResend}}\n          >\n            {{t 'buttons.resend'}}\n          </button>\n        {{/if}}\n        |\n        {{#if this.resendAutomatically.isIdle}}\n          {{#if @message.queueForResend}}\n            {{t 'models.message.autosendPending'}}\n          {{else}}\n            <button\n              data-test-autosend\n              type='button'\n              class='button-link'\n              {{on 'click' this.doQueue}}\n            >\n              {{t 'buttons.resendAutomatically'}}\n            </button>\n          {{/if}}\n        {{/if}}\n\n        <FaIcon @icon='exclamation-circle' />\n\n        <HoverTip @class='w-left-200'>\n          {{t 'status.deliveryFailed'}}\n        </HoverTip>\n      </div>\n\n    {{else}}\n\n      <EllipsisLoader />\n\n    {{/if}}\n  {{/if}}\n\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/delivery-confirmations/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { TARGET } from '@emberclear/networking/models/message';\n\nimport type StoreService from '@ember-data/store';\nimport type { Channel, Contact, CurrentUserService } from '@emberclear/local-account';\nimport type { Message, MessageDispatcher } from '@emberclear/networking';\n\nconst TIMEOUT_MS = 1000;\n\ninterface IArgs {\n  message: Message;\n}\n\nexport default class DeliveryConfirmation extends Component<IArgs> {\n  @service currentUser!: CurrentUserService;\n  @service store!: StoreService;\n  @service('messages/dispatcher') dispatcher!: MessageDispatcher;\n\n  @tracked timedOut = false;\n\n  get wasReceived() {\n    return this.args.message.to === this.currentUser.uid;\n  }\n\n  get wasSent() {\n    return !this.wasReceived;\n  }\n\n  get hasDeliveryConfirmations() {\n    try {\n      let confirmations = this.args.message.deliveryConfirmations;\n\n      if (confirmations) {\n        return confirmations.length > 0;\n      }\n    } catch (e) {\n      console.info(e);\n    }\n\n    return false;\n  }\n\n  @dropTask({ withTestWaiter: true })\n  async waitForConfirmation() {\n    if (this.timedOut) return;\n\n    await timeout(TIMEOUT_MS);\n\n    if (!this.hasDeliveryConfirmations) {\n      this.timedOut = true;\n    }\n  }\n\n  @dropTask({ withTestWaiter: true })\n  async resend() {\n    const { message } = this.args;\n    let to: Contact | Channel;\n\n    // TODO: make the to a polymorphic relationship\n    switch (message.target) {\n      case TARGET.WHISPER:\n        to = await this.store.findRecord('contact', message.to);\n        break;\n      case TARGET.CHANNEL:\n        to = await this.store.findRecord('channel', message.to);\n        break;\n      default:\n        return;\n    }\n\n    this.timedOut = false;\n\n    await this.dispatcher.sendTo(message, to);\n\n    await taskFor(this.waitForConfirmation).perform();\n  }\n\n  @dropTask({ withTestWaiter: true })\n  async deleteMessage() {\n    const { message } = this.args;\n\n    await message.destroyRecord();\n  }\n\n  @dropTask({ withTestWaiter: true })\n  async resendAutomatically() {\n    const { message } = this.args;\n\n    message.queueForResend = true;\n\n    await message.save();\n  }\n\n  // TODO: does this have to be redundant? I have to be doing something wrong\n  @action\n  doDelete() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.deleteMessage).perform();\n  }\n\n  @action\n  doResend() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.resend).perform();\n  }\n\n  @action\n  doQueue() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.resendAutomatically).perform();\n  }\n\n  @action\n  doWait() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.waitForConfirmation).perform();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/embedded-resource/index.hbs",
    "content": "<MediaInfoCard>\n\n  <:header>\n    {{#if (is-present @meta.siteName)}}\n      <p class='card-header-title'>\n        {{@meta.siteName}}\n      </p>\n    {{/if}}\n  </:header>\n\n  <:content>\n    {{#if @meta.hasExtension}}\n      <EmbeddedMedia @url={{@url}} @meta={{@meta}} />\n    {{else}}\n\n      <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview\n        @url={{@url}}\n        @ogData={{@meta.openGraph}}\n      />\n\n    {{/if}}\n  </:content>\n\n\n</MediaInfoCard>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/embedded-resource/metadata-preview/index.hbs",
    "content": "{{! template-lint-disable no-triple-curlies }}\n\n{{#if this.hasOgData}}\n  <div class='metadata-preview'>\n    {{#if this.hasImage}}\n      <span class='thumbnail-container'>\n        <img alt={{t 'images.alt.thumbnail'}} src={{@ogData.image}}>\n      </span>\n    {{/if}}\n\n    <div class='metadata-preview__text'>\n      {{#if this.title}}\n        <ExternalLink href={{@url}}>\n          {{{this.title}}}\n        </ExternalLink>\n      {{/if}}\n\n      {{#if this.description}}\n        <p>{{{this.description}}}</p>\n      {{/if}}\n    </div>\n  </div>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/embedded-resource/metadata-preview/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport DOMPurify from 'dompurify';\n\nimport type { OpenGraphData } from '@emberclear/networking/types';\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\ntype Args = {\n  ogData: OpenGraphData;\n};\n\nexport default class MetadataPreview extends Component<Args> {\n  @service declare chatScroller: ChatScroller;\n\n  get hasOgData() {\n    return this.hasImage || this.title || this.description;\n  }\n\n  get hasImage() {\n    return Boolean(this.args.ogData?.image);\n  }\n\n  get title() {\n    let { ogData } = this.args;\n\n    if (!ogData) return '';\n\n    return DOMPurify.sanitize(ogData.title || '');\n  }\n\n  get description() {\n    let { ogData } = this.args;\n\n    if (!ogData) return '';\n\n    return DOMPurify.sanitize(ogData.description || '');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/header/index.hbs",
    "content": "<div class='message-header'>\n  <span class='from'>\n    {{#if this.hasSender}}\n\n      <LinkTo @route='chat.privately-with' @model={{@message.from}}>\n        {{this.senderName}}\n      </LinkTo>\n\n    {{else}}\n      <em>{{t 'ui.chat.sender.removed'}}</em>\n    {{/if}}\n  </span>\n\n  <span class='sentAt'>\n    {{format-date\n      @message.sentAt\n      year='numeric'\n      month='numeric'\n      day='numeric'\n      hour='numeric'\n      minute='numeric'\n    }}\n\n    {{#if @message.receivedAt}}\n      <HoverTip>\n        {{t 'ui.chat.messages.received' at=(format-date @message.receivedAt)}}\n      </HoverTip>\n    {{/if}}\n  </span>\n</div>"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/header/index.ts",
    "content": "import Component from '@glimmer/component';\n\nimport type { Message } from '@emberclear/networking';\n\ninterface IArgs {\n  message: Message;\n}\n\nexport default class MessageHeader extends Component<IArgs> {\n  get sender() {\n    return this.args.message.sender;\n  }\n\n  get hasSender() {\n    return this.sender;\n  }\n\n  get senderName() {\n    if (this.sender) {\n      return this.sender.name;\n    }\n\n    return '';\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/index.hbs",
    "content": "{{! template-lint-disable no-triple-curlies }}\n\n<div\n  ...attributes\n  id={{@message.id}}\n  data-id={{@message.id}}\n  data-direction={{this.direction}}\n  role='button'\n  {{read-watcher @message}}\n  {{format-code @message.body}}\n\n  class='\n    message m-l-md m-r-md m-b-md\n    {{if @message.unread 'unread'}}\n  '\n  data-test-chat-message\n>\n\n  <Pod::Chat::ChatHistory::Message::Header @message={{@message}} />\n\n  <p class='message-body'>\n    {{{this.messageBody}}}\n\n    <Pod::Chat::ChatHistory::Message::LinkedMedia @urls={{this.urls}} />\n\n  </p>\n\n  <Pod::Chat::ChatHistory::Message::DeliveryConfirmations @message={{@message}} />\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport { convertAndSanitizeMarkdown } from 'emberclear/utils/dom/utils';\nimport { parseURLs } from 'emberclear/utils/string/utils';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { Message } from '@emberclear/networking';\n\ninterface IArgs {\n  message: Message;\n}\n\nexport default class MessageDisplay extends Component<IArgs> {\n  @service currentUser!: CurrentUserService;\n\n  get messageBody() {\n    const markdown = this.args.message.body;\n\n    return convertAndSanitizeMarkdown(markdown);\n  }\n\n  get direction() {\n    if (this.args.message.sender === this.currentUser.record) {\n      return 'outgoing';\n    }\n\n    return 'incoming';\n  }\n\n  get urls() {\n    const content = this.args.message.body!;\n\n    return parseURLs(content);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/message/linked-media.hbs",
    "content": "{{#if (is-present @urls)}}\n  <div class='flex flex-wrap'>\n    {{#each @urls as |url|}}\n      <FetchOpenGraph @url={{url}} as |isLoading data|>\n\n        {{#unless isLoading}}\n          {{#if (or data.meta.hasInfo data.meta.hasMedia)}}\n\n            <Pod::Chat::ChatHistory::Message::EmbeddedResource\n              class='flex'\n              @url={{url}}\n              @meta={{data.meta}}\n            />\n\n          {{/if}}\n        {{/unless}}\n\n      </FetchOpenGraph>\n    {{/each}}\n  </div>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/new-messages/index.hbs",
    "content": "<button\n  type='button'\n  data-test-new-messages-available\n  class='\n    alert pad-xs\n    new-messages transition-all\n    {{if this.chatScroller.isLastVisible 'hidden'}}\n  '\n  {{on 'click' this.scrollToBottom}}\n>\n\n  <span class='is-hidden-mobile m-r-sm'>\n    {{t 'ui.chat.newMessages'}}\n  </span>\n\n  <span class='underline'>\n    {{t 'ui.chat.viewRecent'}}\n  </span>\n</button>"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/new-messages/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\nexport default class ChatNewMessages extends Component {\n  @service chatScroller!: ChatScroller;\n\n  @action\n  scrollToBottom() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.chatScroller.scrollToBottom).perform();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/notification-prompt/index.hbs",
    "content": "{{#if this.isVisible}}\n  <div\n    data-test-notification-prompt\n    class='\n      chat-history-notification-prompt\n      alert alert-info\n      is-radiusless\n      text-center\n      transition-all\n      has-shadow\n    '\n  >\n    <button\n      data-test-dismiss\n      type='button'\n      class='delete'\n      {{on 'click' this.askNextTime}}\n    >\n      <FaIcon @icon='times' />\n    </button>\n\n    <section>\n\n      <strong>{{t 'ui.notifications.prompt.title'}}</strong>\n\n      <div data-test-enable>\n        <button type='button' {{on 'click' this.enableNotifications}}>\n          {{t 'ui.notifications.prompt.enable'}}\n        </button>\n\n        <button data-test-ask-later type='button' {{on 'click' this.askNextTime}}>\n          {{t 'ui.notifications.prompt.askLater'}}\n        </button>\n\n        <button data-test-ask-never type='button' {{on 'click' this.neverAskAgain}}>\n          {{t 'ui.notifications.prompt.neverAsk'}}\n        </button>\n      </div>\n\n    </section>\n\n  </div>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/notification-prompt/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type Notifications from 'emberclear/services/notifications';\n\nexport default class NotificationPrompt extends Component {\n  @service notifications!: Notifications;\n\n  get isVisible() {\n    return this.notifications.showInAppPrompt;\n  }\n\n  @action\n  enableNotifications() {\n    return this.notifications.askPermission();\n  }\n\n  @action\n  neverAskAgain() {\n    this.notifications.isNeverGoingToAskAgain = true;\n  }\n\n  @action\n  askNextTime() {\n    this.notifications.isHiddenUntilBrowserRefresh = true;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/unread-management/-page.ts",
    "content": "import { clickable } from 'ember-cli-page-object';\n\nexport const definition = {\n  scope: '[data-test-unread-floater]',\n\n  scrollToFirstUnread: clickable('[data-test-scroll-to-unread]'),\n  markAllRead: clickable('[data-test-mark-all-read]'),\n};\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/unread-management/index.hbs",
    "content": "{{#if this.shouldRender}}\n  <div\n    data-test-unread-floater\n    class='unread-indicator'\n    ...attributes\n    {{did-insert this.findMessagesContainer}}\n  >\n    <div class='input-group has-shadow'>\n      <button\n        data-test-scroll-to-unread\n        type='button'\n        class='button-sm'\n        {{on 'click' this.scrollToFirstUnread}}\n        >\n        <span>\n          {{t 'ui.chat.unreadMessages'\n            number=this.numberOfUnread\n            time=(\n              format-date this.dateOfFirstUnreadMessage\n              month='short'\n              day='numeric'\n              hour='numeric'\n              minute='numeric'\n            )\n          }}\n        </span>\n      </button>\n      <button\n        data-test-mark-all-read\n        type='button'\n        class='button-sm button-danger'\n        {{on 'click' this.markAllAsRead}}\n        >\n        <span>{{t 'ui.chat.markAllAsRead'}}</span>\n      </button>\n    </div>\n  </div>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/chat/chat-history/unread-management/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\n\nimport { isInElementWithinViewport, scrollIntoViewOfParent } from 'emberclear/utils/dom/utils';\n\nimport {\n  markAsRead,\n  selectUnreadDirectMessages,\n} from '@emberclear/networking/models/message/utils';\n\nimport type { Channel } from '@emberclear/local-account';\nimport type { Contact } from '@emberclear/local-account';\nimport type { Message } from '@emberclear/networking';\n\ninterface IArgs {\n  to: Contact | Channel;\n  messages: Message[];\n}\n\nexport default class UnreadManagement extends Component<IArgs> {\n  messagesElement!: HTMLElement;\n\n  @action\n  findMessagesContainer() {\n    // TODO: remove this,\n    //       scrolling should be handled by the chat-scroller service\n    this.messagesElement = document.querySelector('.messages') as HTMLElement;\n  }\n\n  get unreadMessages() {\n    const { to, messages } = this.args;\n    const unread = selectUnreadDirectMessages(messages, to.id);\n\n    return unread;\n  }\n\n  get numberOfUnread() {\n    return this.unreadMessages.length;\n  }\n\n  get hasUnreadMessages() {\n    return this.numberOfUnread > 0;\n  }\n\n  get shouldRender() {\n    if (!this.hasUnreadMessages) return false;\n\n    return this.hasUnreadOffScreen();\n  }\n\n  get firstUnreadMessage(): Message | undefined {\n    return this.unreadMessages[0];\n  }\n\n  get dateOfFirstUnreadMessage(): Date | undefined {\n    if (this.firstUnreadMessage) {\n      return this.firstUnreadMessage.receivedAt;\n    }\n\n    return;\n  }\n\n  @action\n  markAllAsRead() {\n    return Promise.all(\n      this.unreadMessages.map((message) => {\n        return markAsRead(message);\n      })\n    );\n  }\n\n  @action\n  scrollToFirstUnread() {\n    if (this.firstUnreadMessage) {\n      const firstUnread = document.getElementById(this.firstUnreadMessage.id)!;\n\n      scrollIntoViewOfParent(this.messagesElement, firstUnread);\n    }\n  }\n\n  private hasUnreadOffScreen() {\n    if (this.firstUnreadMessage) {\n      const firstUnread = document.getElementById(this.firstUnreadMessage.id);\n\n      if (firstUnread) {\n        const isOnScreen = isInElementWithinViewport(firstUnread, this.messagesElement);\n\n        return !isOnScreen;\n      }\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/contacts/contact-table/index.hbs",
    "content": "<table data-test-contacts>\n  <thead>\n    <tr>\n      <th>{{t 'models.identity.name'}}</th>\n      <th>{{t 'models.identity.publicKey'}}</th>\n      <th></th>\n    </tr>\n  </thead>\n  <tbody>\n\n    {{#each this.contacts key='id' as |contact|}}\n      <tr data-test-contact-row>\n        <td>\n          {{contact.name}}\n        </td>\n        <td>\n          {{first-8 contact.publicKeyAsHex}}...\n        </td>\n        <td class='flex justify-content-end'>\n          <button\n            type='button'\n            class='button-danger'\n            {{on 'click' (fn this.remove contact)}}\n          >\n            {{t 'buttons.remove'}}\n          </button>\n        </td>\n      </tr>\n    {{/each}}\n\n    {{#if (eq this.contacts.length 0)}}\n      <tr>\n        <td colspan='3'>\n          {{t 'ui.contacts.noContacts'}}\n        </td>\n      </tr>\n    {{/if}}\n\n  </tbody>\n</table>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/contacts/contact-table/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\n\nexport default class ContactsTable extends Component {\n  @service store!: StoreService;\n\n  get contacts() {\n    return this.store.peekAll('contact');\n  }\n\n  @action\n  async remove(contact: Contact) {\n    contact.deleteRecord();\n    await contact.save();\n    contact.unloadRecord();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/contacts/header/index.hbs",
    "content": "<header class='contacts'>\n  <h1>\n    {{t 'ui.contacts.title' number=this.contacts.length}}\n  </h1>\n\n  <LinkTo @route='add-friend' class='button'>\n    <FaIcon @icon='plus' />\n    <span>{{t 'buttons.addFriend'}}</span>\n  </LinkTo>\n</header>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/contacts/header/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport type StoreService from '@ember-data/store';\n\nexport default class extends Component {\n  @service store!: StoreService;\n\n  get contacts() {\n    return this.store.peekAll('contact');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/faq/q-and-a.hbs",
    "content": "<strong>{{t @question}}</strong>\n\n<p class='m-b-lg'>\n  {{t @answer}}\n</p>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/begin-button.ts",
    "content": "import Component from '@glimmer/component';\nimport { setComponentTemplate } from '@ember/component';\nimport { inject as service } from '@ember/service';\nimport { hbs } from 'ember-cli-htmlbars';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nclass BeginButton extends Component {\n  @service declare currentUser: CurrentUserService;\n}\n\nexport default setComponentTemplate(\n  hbs`\n  <LinkTo @route='chat' class='button'>\n    {{#if this.currentUser.isLoggedIn}}\n      {{t 'routes.chat'}}\n    {{else}}\n      {{t 'buttons.begin'}}\n    {{/if}}\n  </LinkTo>\n`,\n  BeginButton\n);\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/compatibility/-utils/detection.ts",
    "content": "// https://stackoverflow.com/questions/47879864/how-can-i-check-if-a-browser-supports-webassembly\n// export function hasWASM() {\n//   try {\n//     if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {\n//       const module = new WebAssembly.Module(\n//         Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)\n//       );\n\n//       if (module instanceof WebAssembly.Module) {\n//         new WebAssembly.Instance(module) instanceof WebAssembly.Instance;\n//         return true;\n//       }\n//     }\n//   } catch (e) {\n//     console.error('hasWASM check:', e);\n//     // deliberately empty\n//   }\n//   return false;\n// }\n\nexport function hasCamera() {\n  return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;\n}\n\nexport function hasNotifications() {\n  return 'Notification' in window;\n}\n\nexport function hasServiceWorker() {\n  return 'ServiceWorker' in window;\n}\n\nexport function hasWebWorker() {\n  return 'Worker' in window;\n}\n\nexport function hasIndexedDb() {\n  return new Promise((resolve /* , reject */) => {\n    const hasIDb = 'indexedDB' in window;\n\n    if (!hasIDb) resolve(false);\n\n    let request = window.indexedDB.open('MyTestDatabase');\n\n    request.onerror = function () {\n      resolve(false);\n    };\n\n    request.onsuccess = function () {\n      resolve(true);\n    };\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/compatibility/feature/boolean-icon.hbs",
    "content": "{{#if @value}}\n  <span class='text-success' ...attributes>\n    <FaIcon @icon='check-circle' />\n    <span class='visually-hidden'>{{t 'compatibility.status.exists'}}</span>\n  </span>\n{{else if @required}}\n  <span class='text-danger' ...attributes>\n    <FaIcon @icon='times-circle' />\n    <span class='visually-hidden'>{{t 'compatibility.status.missing'}}</span>\n  </span>\n{{else}}\n  <span class='text-warning' ...attributes>\n    <FaIcon @icon='exclamation-circle' />\n  </span>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/compatibility/feature/index.hbs",
    "content": "<li class='grid:column grid:col:2'>\n  <Pod::Index::Compatibility::Feature::BooleanIcon\n    @required={{@required}}\n    @value={{@value}}\n  />\n  <span class='text-left'>{{@label}}</span>\n</li>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/compatibility/index.hbs",
    "content": "{{#if this.isNotCompatible}}\n  <ModalStatic @initiallyActive={{true}} @name='compatibility' as |isActive actions|>\n    <button type='button' {{on 'click' actions.open}}>\n      <FaIcon @icon='exclamation-circle' />\n      {{this.successCount}} / {{this.totalCount}}\n      {{t 'compatibility.compatibility'}}\n    </button>\n\n    <Modal\n      data-test-compatibility-modal\n      @isActive={{isActive}}\n      @close={{actions.close}}\n      @boxClasses='has-background-none'\n    >\n      <div class='text-dark p-md'>\n        <h2 class='is-size-3'>\n          <FaIcon @icon='exclamation-circle' />\n          {{t 'compatibility.title'}}\n        </h2>\n        <p>\n          {{t 'compatibility.description'}}\n        </p>\n\n        <div class='flex-row justify-content-space-between align-items-end'>\n          <div class='flex-column align-items-start'>\n            <ul class='list:styleless'>\n              <Pod::Index::Compatibility::Feature @value={{this.hasCamera}} @label={{t 'compatibility.camera'}} />\n              <Pod::Index::Compatibility::Feature @value={{this.hasNotifications}} @label={{t 'compatibility.notifications'}} />\n              <Pod::Index::Compatibility::Feature @required={{true}} @value={{this.hasIndexedDb}} @label={{t 'compatibility.indexeddb'}} />\n              {{!-- <Pod::Index::Compatibility::Feature @value={{this.hasWASM}} @label={{t 'compatibility.wasm'}} /> --}}\n              <Pod::Index::Compatibility::Feature @value={{this.hasServiceWorker}} @label={{t 'compatibility.serviceWorker'}} />\n              <Pod::Index::Compatibility::Feature @value={{this.hasWebWorker}} @label={{t 'compatibility.webWorker'}} />\n            </ul>\n\n            <h3 class='is-size-4 mt-4'>\n              {{t 'compatibility.score'}}: {{this.successCount}} / {{this.totalCount}}\n            </h3>\n            <h4 class='mb-4'>\n              {{t 'compatibility.required'}}: {{this.requiredSuccessCount}} / {{this.totalRequiredCount}}\n            </h4>\n\n          </div>\n\n          <button\n            type='button'\n            class='button'\n            {{on 'click' actions.close}}\n          >\n            {{t 'buttons.close'}}\n          </button>\n        </div>\n      </div>\n    </Modal>\n  </ModalStatic>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/index/compatibility/index.ts",
    "content": "import Ember from 'ember';\nimport Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport {\n  hasCamera,\n  hasIndexedDb,\n  hasNotifications,\n  hasServiceWorker,\n  hasWebWorker,\n} from './-utils/detection';\n\nexport default class Compatibility extends Component {\n  @tracked hasCamera!: boolean;\n  @tracked hasIndexedDb!: boolean;\n  // @tracked hasWASM!: boolean;\n  @tracked hasNotifications!: boolean;\n  @tracked hasServiceWorker!: boolean;\n  @tracked hasWebWorker!: boolean;\n\n  @tracked successCount = 0;\n  @tracked totalCount = 0;\n  @tracked requiredSuccessCount = 0;\n  @tracked totalRequiredCount = 0;\n\n  get isNotCompatible() {\n    return this.totalRequiredCount !== this.requiredSuccessCount;\n  }\n\n  constructor(owner: any, args: any) {\n    super(owner, args);\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.detectFeatures).perform();\n  }\n\n  @dropTask\n  async detectFeatures() {\n    let check = this.checkSuccess.bind(this);\n\n    this.hasIndexedDb = check(await hasIndexedDb(), { required: true });\n\n    if (!Ember.testing) {\n      this.hasCamera = check(hasCamera());\n      this.hasWebWorker = check(hasWebWorker());\n      this.hasServiceWorker = check(hasServiceWorker());\n    }\n\n    this.hasNotifications = check(hasNotifications());\n  }\n\n  private checkSuccess(value: boolean, { required = false }: { required?: boolean } = {}) {\n    if (required) {\n      this.totalRequiredCount++;\n    }\n\n    if (value) {\n      this.successCount++;\n\n      if (required) {\n        this.requiredSuccessCount++;\n      }\n    }\n\n    this.totalCount++;\n\n    return value;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/-machine.ts",
    "content": "import type { EventObject, MachineConfig } from 'xstate';\n\n// https://xstate.js.org/viz/?gist=15a3eb8b7d7c391bcce21d86a041a497\n\ntype Context = Record<string, unknown>;\n\nexport type Events =\n  | { type: 'SUBMIT' }\n  | { type: 'UPLOAD' }\n  | { type: 'RECEIVE_QR_SYN' }\n  | { type: 'IMPORT' }\n  | { type: 'CANCEL' }\n  | { type: 'ERROR' }\n  | { type: 'DONE' }\n  | { type: 'RETRY' }\n  | EventObject;\n\ntype EmptySubState = Record<string, unknown>;\n\nexport interface Schema {\n  states: {\n    waitForData: EmptySubState;\n    qrScanned: EmptySubState;\n    uploadFile: EmptySubState;\n    importData: EmptySubState;\n    processLogin: EmptySubState;\n    success: EmptySubState;\n    error: EmptySubState;\n  };\n  actions: {\n    restartEphemeralConnection: () => Promise<void>;\n  };\n}\n\nexport const machineConfig: MachineConfig<Context, Schema, Events> = {\n  id: 'login',\n  initial: 'waitForData',\n  context: {},\n  states: {\n    waitForData: {\n      onEntry: 'restartEphemeralConnection',\n      on: {\n        // after typing it all in\n        SUBMIT: 'processLogin',\n        // after selecting a file?\n        UPLOAD: 'uploadFile',\n        // triggered by connection class\n        RECEIVE_OR_SYN: 'qrScanned',\n      },\n    },\n\n    qrScanned: {\n      on: {\n        IMPORT: 'importData',\n        CANCEL: 'waitForData',\n        ERROR: 'error',\n      },\n    },\n\n    uploadFile: {\n      invoke: {\n        id: 'handle-file',\n        src: 'handleFile',\n        onDone: 'importData',\n        onError: 'error',\n      },\n    },\n\n    importData: {\n      // shows progress\n      on: {\n        DONE: 'success',\n        ERROR: 'error',\n      },\n    },\n\n    error: {\n      on: { RETRY: '#login' },\n    },\n\n    processLogin: {\n      invoke: {\n        id: 'handle-login',\n        src: 'handleLogin',\n        onDone: 'success',\n        onError: 'error',\n      },\n    },\n\n    success: {\n      onEntry: ['teardownConnection', 'toChats'],\n      type: 'final',\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/login-form/index.hbs",
    "content": "{{!-- data-transfer-started value is string true/false --}}\n<FocusCard @title={{t 'ui.login.title'}} data-transfer-started='{{this.hasTransferStarted}}'>\n\n  {{#if this.uploadSettings.isRunning}}\n    <EllipsisLoader /><br>\n    {{t 'ui.login.loading' num=this.contacts.length}}\n  {{/if}}\n\n  <div class='login-form-contents'>\n    <div class='left'>\n      <Field @label={{t 'input.label.name'}}>\n        {{!-- template-lint-disable require-input-label --}}\n        <Input data-test-name @value={{this.name}} />\n      </Field>\n\n      <Field @label={{t 'input.label.mnemonic'}}>\n        {{!-- template-lint-disable require-input-label --}}\n        <Input data-test-mnemonic @value={{this.mnemonic}} />\n      </Field>\n\n      <div class='cta-with-fallback'>\n        <FileChooser @onChoose={{this.onChooseFile}} as |chooser|>\n          <button\n            data-test-upload-settings\n            type='button'\n            class='button-secondary'\n            {{on 'click' chooser.openFileChooser}}\n          >\n            {{t 'buttons.uploadSettings'}}\n          </button>\n        </FileChooser>\n\n        <button\n          data-test-submit-login\n          type='submit'\n          {{on 'click' this.onSubmit}}\n        >\n          {{t 'buttons.login'}}\n        </button>\n\n      </div>\n\n      <hr>\n\n      <footer>{{t 'ui.login.warning'}}</footer>\n    </div>\n\n    <Pod::Login::LoginForm::TransferPrompt\n      @updateTransferStatus={{this.updateTransferStatus}}\n    />\n\n  </div>\n\n\n\n</FocusCard>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/login-form/index.ts",
    "content": "/* eslint-disable @typescript-eslint/no-floating-promises */\nimport Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { CryptoConnector } from '@emberclear/crypto';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { WorkersService } from '@emberclear/crypto';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Settings from 'emberclear/services/settings';\nimport type Toast from 'emberclear/services/toast';\n\nexport default class LoginForm extends Component {\n  @service declare currentUser: CurrentUserService;\n  @service declare settings: Settings;\n  @service declare toast: Toast;\n  @service declare router: RouterService;\n  @service declare store: StoreService;\n  @service declare workers: WorkersService;\n\n  @tracked mnemonic = '';\n  @tracked name = '';\n  @tracked hasTransferStarted = false;\n\n  get contacts() {\n    return this.store.peekAll('contact');\n  }\n\n  get isLoggedIn() {\n    return this.currentUser.isLoggedIn;\n  }\n\n  @dropTask\n  async login() {\n    try {\n      let name = this.name;\n      let crypto = new CryptoConnector({ workerService: this.workers });\n      let keys = await crypto.login(this.mnemonic);\n\n      await this.currentUser.setIdentity(name, keys);\n\n      await this.router.transitionTo('chat');\n    } catch (e) {\n      console.error(e);\n      this.toast.error('There was a problem logging in...');\n    }\n  }\n\n  @dropTask\n  async uploadSettings(data: string) {\n    try {\n      await this.settings.import(data);\n\n      await this.router.transitionTo('settings');\n    } catch (e) {\n      console.error(e);\n      this.toast.error('There was a problem processing your file...');\n    }\n  }\n\n  @action\n  updateTransferStatus(nextValue: boolean) {\n    this.hasTransferStarted = nextValue;\n  }\n\n  @action\n  onChooseFile(data: string) {\n    taskFor(this.uploadSettings).perform(data);\n  }\n\n  @action\n  onSubmit() {\n    taskFor(this.login).perform();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/login-form/transfer-prompt/index.hbs",
    "content": "<div class='right text-center'>\n  {{#if this.isLoading}}\n    {{on-key 'Escape' (prevent-default this.cancel)}}\n\n    {{#if this.fromUser}}\n      <h3 class='text-center'>\n        {{this.fromUser}}\n      </h3>\n      <br>\n      <h4 class='text-bold text-center'>\n        <code class='has-shadow'>\n          {{this.verification}}\n        </code>\n      </h4>\n      <br>\n    {{/if}}\n\n    <EllipsisLoader />\n    <br>\n    {{this.taskMessage}}\n\n    {{#if this.fromUser}}\n      <div class='text-center mar-t-md' {{focus-trap}}>\n        <button\n          type='button'\n          class='button-secondary button-xs'\n          {{on 'click' this.cancel}}\n        >\n          {{t 'buttons.cancel'}}\n        </button>\n      </div>\n    {{/if}}\n  {{else}}\n    <QRCode @data={{this.qrData}} />\n\n    <h3>{{t 'ui.login.transfer.title'}}</h3>\n    <p>{{t 'ui.login.transfer.prompt'}}\n    </p>\n  {{/if}}\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/login-form/transfer-prompt/index.ts",
    "content": "import Ember from 'ember';\nimport Component from '@glimmer/component';\nimport { action } from '@ember/object';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { ReceiveDataConnection } from 'emberclear/services/connection/ephemeral/login/receive-data';\n\ntype Args = {\n  updateTransferStatus: (status: boolean) => void;\n};\n\nexport default class TransferPrompt extends Component<Args> {\n  constructor(owner: unknown, args: Args) {\n    super(owner, args);\n\n    if (!Ember.testing) {\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.setupEphemeralConnection).perform();\n    }\n  }\n\n  get verification() {\n    return this.result?.verification;\n  }\n\n  get fromUser() {\n    return this.result?.ephemeralConnection?.senderName;\n  }\n\n  get qrData() {\n    return this.result?.qrData;\n  }\n\n  get taskMessage() {\n    let message = taskFor(this.setupEphemeralConnection).lastSuccessful?.value?.ephemeralConnection\n      ?.taskMsg;\n\n    return message;\n  }\n\n  get isLoading() {\n    return Boolean(this.taskMessage) || !this.result;\n  }\n\n  get result() {\n    return taskFor(this.setupEphemeralConnection).lastSuccessful?.value;\n  }\n\n  @action\n  cancel() {\n    this.args.updateTransferStatus(false);\n\n    let task = taskFor(this.setupEphemeralConnection);\n\n    // TODO: send CANCEL message to sender\n    task.cancelAll();\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    task.perform();\n  }\n\n  @dropTask\n  async setupEphemeralConnection() {\n    let { updateTransferStatus } = this.args;\n\n    let ephemeralConnection: ReceiveDataConnection = await ReceiveDataConnection.build(this);\n\n    let { hexId: pub } = ephemeralConnection;\n    let verification = randomFourLetters();\n\n    let qrData = ['login', { pub, verify: verification }];\n\n    ephemeralConnection.wait(updateTransferStatus);\n\n    return { qrData, ephemeralConnection, verification };\n  }\n}\n\nfunction randomFourLetters() {\n  return Math.random()\n    .toString(36)\n    .replace(/[^a-z]+/g, '')\n    .substr(0, 4)\n    .toUpperCase();\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/login/template.hbs",
    "content": ""
  },
  {
    "path": "client/web/emberclear/app/components/pod/q-r/-machine.ts",
    "content": "import { assign, send } from 'xstate';\n\nimport { MalformedQRCodeError, UnrecognizedQRCodeError } from 'emberclear/utils/errors';\n\nimport type { Context, Events, QRData, ScanEvent, Schema } from './-types';\nimport type { MachineConfig } from 'xstate';\n\nfunction parseScannedData(_: Context, event: ScanEvent) {\n  let { data } = event;\n  let parsed = JSON.parse(data) as QRData;\n\n  let isValid = Array.isArray(parsed);\n\n  if (!isValid) {\n    throw new MalformedQRCodeError();\n  }\n\n  // TODO: assert using JSON schema\n  if (parsed[0] === 'login') {\n    if (!parsed[1].pub) {\n      throw new UnrecognizedQRCodeError();\n    }\n  }\n\n  return Promise.resolve(parsed);\n}\n\n// https://xstate.js.org/viz/?gist=ae6c4b8e7d1c9b05a5510c5bf0303890\nexport const machineConfig: MachineConfig<Context, Schema, Events> = {\n  id: 'scan-qr-code',\n  strict: true,\n  initial: 'scanner',\n  context: {\n    intent: undefined,\n    data: undefined,\n    error: undefined,\n  },\n  states: {\n    scanner: {\n      id: 'scanner',\n      initial: 'scanning',\n      onDone: [\n        {\n          target: '#loginToDevice',\n          cond: 'isQRLogin',\n        },\n        {\n          target: '#addFriend',\n          cond: 'isQRAddFriend',\n        },\n        // else: unrecognized target, ignore and\n        // keep scanning until we recognize a code\n      ],\n      states: {\n        scanning: {\n          on: {\n            SCAN: 'parsing',\n          },\n        },\n        parsing: {\n          invoke: {\n            id: 'parseScan',\n            src: parseScannedData,\n            onDone: {\n              // target: 'scanned',\n              actions: [\n                assign({\n                  intent: (_, event) => event.data[0],\n                  data: (_, event) => event.data[1],\n                }),\n                send('PARSED'),\n              ],\n            },\n            onError: { target: '#error' },\n          },\n          on: {\n            PARSED: 'scanned',\n          },\n        },\n        scanned: {\n          type: 'final',\n        },\n      },\n    },\n    error: {\n      id: 'error',\n      on: {\n        RETRY: '#scanner',\n      },\n    },\n    loginToDevice: {\n      id: 'loginToDevice',\n      initial: 'checkLogin',\n      states: {\n        checkLogin: {\n          on: {\n            '': [\n              {\n                target: 'setupConnection',\n                cond: 'isLoggedIn',\n              },\n              { target: '#error' },\n            ],\n          },\n        },\n        setupConnection: {\n          invoke: {\n            id: 'setup-connection',\n            src: 'setupConnection',\n            onDone: 'askPermission',\n            onError: '#error',\n          },\n        },\n        askPermission: {\n          on: {\n            DENY: '#error',\n            ALLOW: 'transferAllowed',\n          },\n        },\n        transferAllowed: {\n          exit: ['destroyConnection'],\n          invoke: {\n            id: 'transfer-data',\n            src: 'transferData',\n            onDone: 'transferComplete',\n            onError: '#error',\n          },\n        },\n        transferComplete: {\n          type: 'final',\n        },\n      },\n    },\n    addFriend: {\n      id: 'addFriend',\n      initial: 'determineExistence',\n      // context: {\n      //   exists: false,\n      // },\n      states: {\n        determineExistence: {\n          entry: ['doesContactExist', send('HANDLE_EXISTENCE')],\n          on: {\n            HANDLE_EXISTENCE: [\n              {\n                target: 'contactExists',\n                cond: 'isContactKnown',\n              },\n              {\n                target: 'needToAddContact',\n                actions: ['addContact'],\n              },\n            ],\n          },\n        },\n        needToAddContact: {},\n        contactExists: {},\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/q-r/-types.ts",
    "content": "import type { EventObject } from 'xstate';\n\nexport interface Context {\n  intent?: string;\n  data?: QRData[1];\n  error?: string;\n}\n\nexport type LoginQRData = [\n  string,\n  {\n    pub: string;\n    verify: string;\n  }\n];\n\nexport type QRData = LoginQRData;\n\nexport type ErrorEvent = { type: 'error.execution'; data: string };\nexport type ScanEvent = { type: 'SCAN'; data: string };\ntype AllowEvent = { type: 'ALLOW' };\ntype RetryEvent = { type: 'RETRY' };\ntype DenyEvent = { type: 'DENY' };\ntype HandleExistenceEvent = { type: 'HANDLE_EXISTENCE' };\n\ntype ParsedEvent = { type: 'PARSED' };\n\ntype EmptySubState = Record<string, unknown>;\n\ninterface ScannerSubMachine {\n  Schema: {\n    states: {\n      scanning: EmptySubState;\n      parsing: EmptySubState;\n      scanned: EmptySubState;\n    };\n  };\n\n  Event: ScanEvent | ParsedEvent | RetryEvent | EventObject;\n}\n\ninterface LoginSubMachine {\n  Schema: {\n    states: {\n      checkLogin: EmptySubState;\n      setupConnection: EmptySubState;\n      askPermission: EmptySubState;\n      transferAllowed: EmptySubState;\n      transferComplete: EmptySubState;\n    };\n  };\n\n  Event: AllowEvent | DenyEvent | RetryEvent | ErrorEvent | ScanEvent | ParsedEvent | EventObject;\n}\n\ninterface AddContactSubMachine {\n  Schema: {\n    states: {\n      determineExistence: EmptySubState;\n      needToAddContact: EmptySubState;\n      contactExists: EmptySubState;\n    };\n  };\n\n  Event: HandleExistenceEvent | EventObject;\n}\n\nexport interface Schema {\n  states: {\n    error: EmptySubState;\n    scanner: ScannerSubMachine['Schema'];\n    loginToDevice: LoginSubMachine['Schema'];\n    addFriend: AddContactSubMachine['Schema'];\n  };\n  actions: {\n    doesContactExist: (publicKeyAsHex: string) => boolean;\n  };\n  services: {\n    transferData: (ephemeralPublicKeyAsHex: string) => Promise<void>;\n    addContact: (publicKeyAsHex: string, name: string) => Promise<void>;\n    parseScannedData: (ctx: Context, event: ScanEvent) => Promise<QRData>;\n  };\n}\n\nexport type Events =\n  | LoginSubMachine['Event']\n  | AddContactSubMachine['Event']\n  | ScannerSubMachine['Event'];\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/q-r/index.hbs",
    "content": "{{#if (includes this.state 'error')}}\n\n  {{!-- template-lint-disable --}}\n  {{log 'error occurred'}}\n  {{log this.current}}\n  {{!-- template-lint-enable --}}\n\n  <ErrorCard\n    data-test-error\n    @message={{this.ctx.error}}\n    @retry={{fn this.transition 'RETRY'}}\n  />\n\n{{else if (includes this.state 'scanner')}}\n\n  <QRScanner @onScan={{this.handleScan}} />\n\n{{else if (includes this.state 'loginToDevice.setupConnection' )}}\n\n  <p>\n    <EllipsisLoader /><br>\n    {{t 'ui.login.transfer.establishConnection'}}\n  </p>\n\n{{else if (includes this.state 'loginToDevice.askPermission')}}\n\n  <Pod::QR::Login::Ask\n    @code={{this.ctx.data.verify}}\n    @allow={{fn this.transition 'ALLOW'}}\n    @deny={{fn this.transition 'DENY'}}\n  />\n\n{{else if (includes this.state 'loginToDevice.transferAllowed')}}\n\n  <p>\n    <EllipsisLoader /><br>\n    {{t 'ui.login.transfer.inProgress'}}\n  </p>\n\n{{else if (includes this.state 'loginToDevice.transferComplete')}}\n\n  {{t 'ui.login.transfer.success'}}\n\n{{else}}\n\n  {{!-- template-lint-disable --}}\n  {{log 'error occurred'}}\n  {{log this.current}}\n  {{!-- template-lint-enable --}}\n\n  <ErrorCard\n    data-test-unknown-state\n    @message={{t 'errors.genericRetry'}}\n    @retry={{fn this.transition 'RETRY'}}\n  />\n\n{{/if}}\n\n\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/q-r/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { interpreterFor, useMachine } from 'ember-statecharts';\nimport { use } from 'ember-usable';\n\nimport { ConnectionDoesNotExistError } from 'emberclear/utils/errors';\n\nimport { machineConfig } from './-machine';\n\nimport type { LoginQRData } from './-types';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { SendDataConnection } from 'emberclear/services/connection/ephemeral/login/send-data';\nimport type QRManager from 'emberclear/services/qr-manager';\n\nexport default class QRScan extends Component {\n  @service intl!: Intl;\n  @service currentUser!: CurrentUserService;\n  @service qrManager!: QRManager;\n\n  @use\n  interpreter = interpreterFor(\n    useMachine(machineConfig).withConfig({\n      services: {\n        setupConnection: this.setupConnection.bind(this),\n        transferData: this.transferData.bind(this),\n        addContact: this.addContact.bind(this),\n      },\n      guards: {\n        isQRLogin: ({ intent }) => intent === 'login',\n        isQRAddFriend: ({ intent }) => intent === 'add-friend',\n        hasError: ({ error }) => error === 'error',\n        isContactKnown: () => false,\n        isLoggedIn: () => this.currentUser.isLoggedIn,\n      },\n    })\n  );\n\n  connection?: SendDataConnection;\n\n  get state() {\n    return this.interpreter?.state?.toStrings();\n  }\n\n  get ctx() {\n    return this.interpreter?.state?.context;\n  }\n\n  @action\n  handleScan(data: string) {\n    this.interpreter.send('SCAN', { data });\n  }\n\n  @action\n  transition(transitionName: string) {\n    this.interpreter.send(transitionName);\n  }\n\n  async setupConnection() {\n    if (!this.interpreter?.state) {\n      throw new Error('No State' /* but what we make */);\n    }\n\n    // The State-Machine prevents this method from being called\n    // without an existing `pub` (which is always present when)\n    // the `intent` is \"login\"\n    let { pub } = this.interpreter?.state?.context.data as LoginQRData[1];\n\n    this.connection = await this.qrManager.login.setupConnection(this, pub);\n  }\n\n  async transferData() {\n    if (!this.connection) {\n      throw new ConnectionDoesNotExistError();\n    }\n\n    await this.connection.transferToDevice();\n  }\n\n  addContact() {\n    // TODO\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/q-r/login/ask.hbs",
    "content": "<div class='row row-center' data-test-login-confirm>\n  <div class='col-12 col-sm-8'>\n    <header>\n      <h2>{{t 'ui.login.verify.title'}}</h2>\n    </header>\n    <p>\n      {{t 'ui.login.verify.prompt1'}}\n      <br>\n      {{t 'ui.login.verify.prompt2'}}\n\n      <h2 data-test-code class='text-center mar-xs mar-b-xl'>\n        <code class='has-shadow'>{{@code}}</code>\n      </h2>\n    </p>\n\n    <footer class='cta-with-fallback'>\n      <button data-test-deny type='button' class='button-secondary' {{on 'click' @deny}}>\n        {{t 'buttons.deny'}}\n      </button>\n\n      <button data-test-allow type='button' {{on 'click' @allow}}>\n        {{t 'buttons.allow'}}\n      </button>\n    </footer>\n  </div>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/danger-zone/index.hbs",
    "content": "<button\n  data-test-show-private-key-toggle\n  type='button'\n  class='button'\n  {{on 'click' this.togglePrivateKey}}\n>\n\n  {{#if this.showPrivateKey}}\n    {{t 'ui.settings.hideKey'}}\n  {{else}}\n    {{t 'ui.settings.showKey'}}\n  {{/if}}\n\n</button>\n\n{{#if this.showPrivateKey}}\n  <MnemonicDisplay @crypto={{this.currentUser.crypto}} />\n{{/if}}\n\n<hr>\n\n\n<button\n  data-test-delete-messages\n  type='button'\n  disabled={{this.messagesDeleted}}\n  class='button button-danger'\n  {{on 'click' this.deleteMessages}}\n>\n\n  <FaIcon @icon='times' />\n\n  <span>{{t 'ui.settings.danger.deleteMessages'}}</span>\n</button>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/danger-zone/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class DangerSettings extends Component {\n  @service declare store: StoreService;\n  @service declare toast: Toast;\n  @service declare currentUser: CurrentUserService;\n\n  @tracked showPrivateKey = false;\n  // TODO: should this actually check existence of messages?\n  @tracked messagesDeleted = false;\n\n  @action\n  togglePrivateKey() {\n    this.showPrivateKey = !this.showPrivateKey;\n  }\n\n  @action\n  async deleteMessages() {\n    this.toast.info('Deleting messages...');\n    this.messagesDeleted = true;\n\n    const messages = await this.store.findAll('message');\n\n    await messages.invoke('destroyRecord');\n\n    this.toast.info('All messages have been cleared.');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/interface/-page.ts",
    "content": "import { create } from 'ember-cli-page-object';\n\nimport { switchInput } from '@emberclear/ui/test-support/page-objects';\n\nexport const definition = {\n  scope: '[data-test-interface]',\n  hideOfflineContacts: {\n    scope: '[data-test-hide-offline-contacts]',\n    ...switchInput,\n  },\n  themes: {\n    selectMidnight: {\n      scope: '[data-test-theme-midnight]',\n      ...switchInput,\n    },\n  },\n};\n\nexport const page = create(definition);\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/interface/index.hbs",
    "content": "<div class='m-b-lg' data-test-interface>\n\n  <Switch\n    data-test-hide-offline-contacts\n    @value={{this.settings.hideOfflineContacts}}\n    @label={{t 'ui.settings.hideOfflineContacts'}}\n    {{on 'click' this.toggleHideOffline}}\n  />\n\n  <hr>\n\n\n  <h2>{{t 'ui.settings.themes.title'}}</h2>\n\n  <Switch\n    data-test-theme-midnight\n    @value={{eq 'midnight' this.settings.theme}}\n    @label={{t 'ui.settings.themes.midnight'}}\n    {{on 'click' this.useDarkTheme}}\n  />\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/interface/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { THEMES } from 'emberclear/services/settings';\n\nimport type Settings from 'emberclear/services/settings';\n\nexport default class InterfaceSettings extends Component {\n  @service settings!: Settings;\n\n  @action\n  useDarkTheme(e: any) {\n    if (e.target.checked) {\n      this.settings.selectTheme(THEMES.midnight);\n    } else {\n      this.settings.selectTheme(THEMES.default);\n    }\n  }\n\n  @action\n  toggleHideOffline() {\n    this.settings.hideOfflineContacts = !this.settings.hideOfflineContacts;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/navigation.hbs",
    "content": "<div class='tabs'>\n  <nav>\n    <LinkTo @route='settings' class={{if (is-active 'settings.index') 'active'}}>\n      {{t 'ui.settings.tabs.profile'}}\n    </LinkTo>\n\n    <LinkTo @route='settings.interface' class={{if (is-active 'settings.interface') 'is-active'}}>\n      {{t 'ui.settings.tabs.interface'}}\n    </LinkTo>\n\n    <LinkTo @route='settings.relays' class={{if (is-active 'settings.relays') 'is-active'}}>\n      {{t 'ui.settings.tabs.relays'}}\n    </LinkTo>\n\n    <LinkTo @route='settings.danger-zone' class={{if (is-active 'settings.danger-zone') 'is-active'}}>\n      {{t 'ui.settings.tabs.dangerZone'}}\n    </LinkTo>\n  </nav>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/profile/index.hbs",
    "content": "<div>\n  <form {{on 'submit' this.save}}>\n\n    <Field @label='Name' as |id|>\n      <Input\n        data-test-name-field\n        id={{id}}\n        @value={{this.currentUser.record.name}}\n      />\n\n    </Field>\n\n    <div class='cta'>\n      {{!-- template-lint-disable require-input-label --}}\n      <input\n        data-test-save\n        class='button'\n        type='submit'\n        value={{t 'buttons.save' }}\n      >\n    </div>\n\n  </form>\n\n  <hr>\n\n  <button type='button' {{on 'click' this.downloadSettings}} class='button'>\n    {{t 'ui.settings.download' }}\n  </button>\n\n  <button\n    disabled\n    type='button'\n    class='button'\n  >\n    {{t 'ui.settings.copyProfileToDevice'}}\n  </button>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/profile/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Settings from 'emberclear/services/settings';\n\nexport default class ProfileSettings extends Component {\n  @service declare currentUser: CurrentUserService;\n  @service declare toast: Toast;\n  @service declare settings: Settings;\n\n  get name() {\n    return this.currentUser.record?.name;\n  }\n\n  @action\n  async save(e: Event) {\n    e.preventDefault();\n\n    await this.currentUser.record!.save();\n\n    this.toast.success('Identity Updated');\n  }\n\n  @action\n  async downloadSettings() {\n    const settings = await this.settings.buildData();\n\n    if (!settings) return;\n\n    const link = document.createElement('a');\n\n    link.setAttribute('download', 'emberclear.settings');\n    link.setAttribute('target', '_blank');\n    link.setAttribute('rel', 'noopener');\n    link.setAttribute('href', settings);\n\n    link.click();\n    link.remove();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/relays/new-relay-form/index.hbs",
    "content": "<button\n  data-test-add-relay\n  type='button'\n  class='button'\n  {{on 'click' this.toggleForm}}\n>\n  {{#if this.isVisible}}\n    {{t 'buttons.cancel'}}\n  {{else}}\n    {{t 'ui.settings.relays.add'}}\n  {{/if}}\n</button>\n\n{{#if this.isVisible}}\n  <form {{on 'submit' (prevent-default this.submit)}} data-test-add-relay-form>\n    <Field @label='Socket URL'>\n      {{!-- template-lint-disable require-input-label --}}\n      <Input\n        data-test-socket-field\n        class='input'\n        @value={{this.socketURL}}\n      />\n    </Field>\n\n    <Field @label='Open Graph URL'>\n      {{!-- template-lint-disable require-input-label --}}\n      <Input\n        data-test-og-field\n        class='input'\n        @value={{this.openGraphURL}}\n      />\n    </Field>\n\n    <div class='flex justify-content-end'>\n      {{!-- template-lint-disable require-input-label --}}\n      <input data-test-save-relay class='button' type='submit' value={{t 'buttons.save'}}>\n    </div>\n  </form>\n{{/if}}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/relays/new-relay-form/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport { hostFromURL } from 'emberclear/utils/string/utils';\n\nimport type StoreService from '@ember-data/store';\nimport type { Relay } from '@emberclear/networking';\n\nexport default class NewRelayForm extends Component {\n  @service store!: StoreService;\n\n  @tracked isVisible = false;\n  @tracked socketURL = '';\n  @tracked openGraphURL = '';\n\n  @action\n  submit() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.save).perform();\n  }\n\n  @dropTask\n  async save() {\n    const host = hostFromURL(this.socketURL);\n    const existing = await this.store.findAll('relay');\n    const priority = ((existing as any) as Relay[]).length + 1;\n    const record = this.store.createRecord('relay', {\n      socket: this.socketURL,\n      og: this.openGraphURL,\n      host,\n      priority,\n    });\n\n    await record.save();\n\n    this.reset();\n  }\n\n  @action\n  toggleForm() {\n    this.isVisible = !this.isVisible;\n  }\n\n  @action\n  private reset() {\n    this.isVisible = false;\n    this.socketURL = '';\n    this.openGraphURL = '';\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/relays/relay-table/index.hbs",
    "content": "<table data-test-relays>\n  <thead>\n    <tr>\n      <th></th>\n      <th>#</th>\n      <th>{{t 'ui.settings.relays.URLs'}}</th>\n      <th></th>\n    </tr>\n  </thead>\n  <tbody>\n    {{#each @relays as |relay|}}\n      <Pod::Settings::Relays::RelayTable::Row @relay={{relay}} />\n    {{/each}}\n  </tbody>\n</table>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/relays/relay-table/row/index.hbs",
    "content": "<tr>\n  <td>\n    {{#if this.isActive}}\n      <span data-test-connected class='tag is-primary'>{{t 'status.connected'}}</span>\n    {{/if}}\n\n    {{#if (not (eq @relay.priority 1))}}\n      <button\n        data-test-make-default\n        type='button'\n        {{on 'click' this.makeDefault}}\n      >\n        {{t 'buttons.makeDefault'}}\n      </button>\n    {{/if}}\n  </td>\n  <td>{{@relay.priority}}</td>\n  <td>\n    <label class='has-text-weight-bold'>\n      {{t 'ui.settings.relays.socket'}}:\n    </label> {{@relay.socket}}\n    <br>\n\n    <label class='has-text-weight-bold'>\n      {{t 'ui.settings.relays.openGraph'}}:\n    </label> {{@relay.og}}\n\n    <label>\n      {{t 'ui.settings.relays.connectedUsers'}}:\n    </label> {{or @relay.connectionCount 'Unknown'}}\n  </td>\n  <td>\n    <button\n      data-test-remove\n      type='button'\n      {{on 'click' this.remove}}\n    >\n      {{t 'buttons.remove'}}\n    </button>\n  </td>\n</tr>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/settings/relays/relay-table/row/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { Relay } from '@emberclear/networking';\nimport type ConnectionManager from '@emberclear/networking/services/connection/manager';\n\ninterface Args {\n  relay: Relay;\n}\n\nexport default class RelayTableRow extends Component<Args> {\n  @service declare store: StoreService;\n  @service('connection/manager') declare connectionManager: ConnectionManager;\n\n  get isActive() {\n    let pool = this.connectionManager.connectionPool;\n\n    if (!pool) {\n      return false;\n    }\n\n    let active = pool.activeConnections.map((connection) => connection.relay);\n\n    return active.includes(this.args.relay);\n  }\n\n  @action\n  remove() {\n    let { relay } = this.args;\n\n    relay.deleteRecord();\n\n    return relay.save();\n  }\n\n  @action\n  async makeDefault() {\n    let { relay } = this.args;\n\n    relay.priority = 1;\n\n    await relay.save();\n\n    const relays: ArrayProxy<Relay> = await this.store.findAll('relay');\n\n    let nextHighestPriority = 2;\n\n    let sortedRelays = relays.toArray().sort((r) => r.priority);\n\n    for (let nonDefaultRelay of sortedRelays) {\n      if (nonDefaultRelay.id === relay.id) return;\n\n      nonDefaultRelay.priority = nextHighestPriority;\n      await nonDefaultRelay.save();\n      nextHighestPriority++;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/-machine.ts",
    "content": "import { send } from 'xstate';\n\nimport type { EventObject, MachineConfig } from 'xstate';\n\nexport interface Context {\n  next?: string;\n  prev?: string;\n}\n\ntype EmptySubState = Record<string, unknown>;\n\nexport interface Schema {\n  states: {\n    idle: EmptySubState;\n    creating: EmptySubState;\n    overwrite: EmptySubState;\n    completed: EmptySubState;\n  };\n  actions: {\n    logout: <T>() => T;\n    redirectHome: <T>() => T;\n  };\n  guards: {\n    isLoggedIn: () => boolean;\n  };\n}\n\nexport type Event = { type: 'NEXT' } | { type: 'PREV' } | EventObject;\n\nexport const machineConfig: MachineConfig<Context, Schema, Event> = {\n  id: 'setup-user',\n  strict: true,\n  initial: 'idle',\n  states: {\n    idle: {\n      entry: send('NEXT'),\n      on: {\n        NEXT: [\n          {\n            target: 'overwrite',\n            cond: 'isLoggedIn',\n          },\n          {\n            target: 'creating',\n          },\n        ],\n      },\n    },\n    creating: {\n      on: {\n        NEXT: 'completed',\n        PREV: 'idle',\n      },\n    },\n    overwrite: {\n      on: {\n        NEXT: {\n          target: 'creating',\n          actions: ['logout'],\n        },\n        PREV: {\n          actions: ['redirectHome'],\n        },\n      },\n    },\n    completed: {\n      type: 'final',\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/completed/index.hbs",
    "content": "<FocusCard @title={{t 'ui.setup.almostReady'}}>\n\n  <p data-test-setup-mnemonic>\n    {{t 'ui.setup.mnemonicPrompt'}}\n\n    <MnemonicDisplay @crypto={{this.currentUser.crypto}} />\n\n    {{t 'ui.setup.note'}}\n  </p>\n\n  <div class='cta'>\n    <LinkTo @route='chat' class='button' data-test-next>\n      {{t 'buttons.next'}}\n    </LinkTo>\n  </div>\n\n</FocusCard>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/completed/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class SetupCompleted extends Component {\n  @service declare currentUser: CurrentUserService;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/creating/index.hbs",
    "content": "<FocusCard @title={{t 'ui.setup.introQuestion'}}>\n\n  <form data-test-name-form {{on 'submit' this.createIdentity}}>\n\n    <Field @label={{t 'models.identity.name'}} @hidden={{true}}>\n      {{!-- template-lint-disable require-input-label --}}\n      <Input\n        data-test-name-field\n        class='input-xl'\n        placeholder={{t 'ui.setup.nameLabel'}}\n        @value={{this.name}}\n      />\n    </Field>\n\n    <div class='cta-with-fallback'>\n      <LinkTo @route='login'>\n        {{t 'buttons.loginInstead'}}\n      </LinkTo>\n\n      <button\n        data-test-next\n        type='submit'\n        disabled={{or this.nameIsBlank this.create.isRunning}}\n      >\n        {{t 'buttons.next'}}\n      </button>\n    </div>\n  </form>\n\n</FocusCard>\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/creating/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\nimport { isBlank } from '@ember/utils';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\ntype Args = {\n  next: () => void;\n};\n\nexport default class NameEntry extends Component<Args> {\n  @service declare currentUser: CurrentUserService;\n\n  @tracked name!: string;\n\n  get nameIsBlank(): boolean {\n    return isBlank(this.name);\n  }\n\n  @action\n  createIdentity(e: Event) {\n    e.preventDefault();\n\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.create).perform();\n  }\n\n  @dropTask({ withTestWaiter: true })\n  async create() {\n    if (this.nameIsBlank) return;\n\n    const exists = await this.currentUser.exists();\n\n    if (!exists) {\n      await this.currentUser.create(this.name);\n    }\n\n    this.args.next();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/index.hbs",
    "content": "{{#if this.interpreter.state.value}}\n  {{#let\n    (component (concat 'pod/setup/' this.interpreter.state.value))\n    as |ComponentForCurrentState|\n  }}\n\n    <div class='container grid center-self'>\n\n      <ComponentForCurrentState\n        @next={{fn this.interpreter.send 'NEXT'}}\n        @prev={{fn this.interpreter.send 'PREV'}}\n      />\n\n    </div>\n\n  {{/let}}\n{{else}}\n  <ErrorCard @message={{t 'ui.login.invalidState'}} />\n{{/if}}"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { inject as service } from '@ember/service';\n\nimport { interpreterFor, useMachine } from 'ember-statecharts';\nimport { use } from 'ember-usable';\n\nimport { machineConfig } from './-machine';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type SessionService from 'emberclear/services/session';\n\nexport default class SetupWizard extends Component {\n  @service currentUser!: CurrentUserService;\n  @service session!: SessionService;\n  @service router!: RouterService;\n\n  @use\n  interpreter = interpreterFor(\n    useMachine(machineConfig).withConfig({\n      actions: {\n        logout: () => this.session.logout(),\n        redirectHome: () => this.router.transitionTo('application'),\n      },\n      guards: {\n        isLoggedIn: () => this.currentUser.isLoggedIn,\n      },\n    })\n  );\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/pod/setup/overwrite.hbs",
    "content": "<FocusCard @title={{t 'ui.setup.overwriteTitle'}}>\n  <p>{{t 'ui.setup.overwriteQuestion' htmlSafe=true}}</p>\n\n  <div class='cta-with-fallback'>\n    <button\n      data-test-overwrite-abort\n      type='button'\n      class='card-footer-item'\n      {{on  'click' @prev}}\n    >\n      {{t 'ui.setup.overwriteAbort'}}\n    </button>\n\n    <button\n      data-test-overwrite-confirm\n      type='button'\n      class='card-footer-item'\n      {{on  'click' @next}}\n    >\n      {{t 'ui.setup.overwriteConfirm'}}\n    </button>\n  </div>\n</FocusCard>\n"
  },
  {
    "path": "client/web/emberclear/app/components/q-r-code.hbs",
    "content": "<div class='qr-code' ...attributes>\n  <img\n    alt={{@alt}}\n    {{qr-image @data}}\n  >\n</div>\n\n"
  },
  {
    "path": "client/web/emberclear/app/components/q-r-scanner/index.hbs",
    "content": "<div\n  data-test-qr-scanner\n  class='qr-scanner_container'\n  ...attributes\n  {{did-insert this.start}}\n>\n\n  {{#if this.cameraStream}}\n    <JSQR::Scanner\n      @cameraStream={{this.cameraStream}}\n      @onData={{this.handleData}}\n    >\n      <div class='loader'></div>\n    </JSQR::Scanner>\n  {{else}}\n    {{t 'qrCode.waitingForCamera'}}\n  {{/if}}\n\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/components/q-r-scanner/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type { NavigatorService } from 'ember-browser-services/types';\n\ninterface IArgs {\n  onScan: (qrContent: string) => void;\n}\n\nexport default class QRScanner extends Component<IArgs> {\n  @service declare intl: Intl;\n  @service declare toast: Toast;\n  @service('browser/navigator') declare navigator: NavigatorService;\n\n  @tracked cameraStream?: MediaStream;\n  @tracked lastDetectedData?: string;\n\n  get isCameraActive() {\n    return this.cameraStream !== undefined;\n  }\n\n  @action\n  async toggleCamera() {\n    this.isCameraActive ? this.stop() : await this.start();\n  }\n\n  @action\n  handleData(data: string) {\n    this.args.onScan(data);\n  }\n\n  @action\n  async start() {\n    let options = { video: { facingMode: 'environment' } };\n\n    try {\n      let stream = await this.navigator.mediaDevices.getUserMedia(options);\n\n      this.cameraStream = stream;\n    } catch (e) {\n      console.error(e);\n      let msg = this.intl.t('errors.permissions.enableCamera');\n\n      this.toast.error(msg);\n    }\n  }\n\n  private stop() {\n    this.cameraStream?.getTracks()?.forEach((track) => track.stop());\n    this.cameraStream = undefined;\n  }\n\n  willDestroy() {\n    this.stop();\n    super.willDestroy();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/search/-page.ts",
    "content": "import { collection, create, fillable } from 'ember-cli-page-object';\n\nimport { keyEvents } from '@emberclear/ui/test-support/key-events';\n\nconst selector = '[data-test-search-modal]';\n\nexport const page = create({\n  ...keyEvents(selector),\n  input: {\n    scope: `${selector} input`,\n    fillIn: fillable(),\n  },\n  results: {\n    scope: `${selector} .results`,\n\n    contacts: {\n      scope: '[data-test-contacts-results]',\n      links: collection('a', {}),\n    },\n    channels: {\n      scope: '[data-test-channels-results]',\n      links: collection('a', {}),\n    },\n  },\n});\n"
  },
  {
    "path": "client/web/emberclear/app/components/search/index.hbs",
    "content": "<Modal\n  data-test-search-modal\n  @isActive={{@isActive}}\n  @close={{@close}}\n  role='search'>\n\n  <Field @label={{t 'ui.search.title'}} @hidden={{true}}>\n    {{!-- template-lint-disable require-input-label --}}\n    <input\n      type='text'\n      class='input-lg'\n      value={{this.searchText}}\n      placeholder={{t 'ui.search.title'}}\n      {{autofocus}}\n      {{on 'input' this.onInput}}\n    >\n  </Field>\n\n  <div class='results' {{did-insert this.submitSearch}}>\n    {{#unless this.hasResults}}\n      {{t 'ui.search.nothingFound'}}\n    {{/unless}}\n\n    {{#if this.hasResults}}\n      <span class='section-title'>{{t 'ui.search.contacts'}}</span>\n\n      <div tablist data-test-contacts-results>\n        {{#each this.contactResults as |identity|}}\n          <Search::Result\n            @to='chat.privately-with'\n            @id={{identity.uid}}\n            @afterSelect={{@close}}\n          >\n            <span>\n              @ <strong>{{identity.name}}</strong>\n            </span>\n            <span>\n              {{first-8 identity.uid}}\n            </span>\n          </Search::Result>\n        {{else}}\n          <span class='no-results'>{{t 'ui.search.noContacts'}}</span>\n        {{/each}}\n      </div>\n\n      <br>\n      <span class='section-title'>{{t 'ui.search.channels'}}</span>\n\n      <div tablist data-test-channels-results>\n        {{#each this.channelResults as |channel|}}\n          <Search::Result\n            @to='chat.in-channel'\n            @id={{channel.id}}\n            @afterSelect={{@close}}\n          >\n            <span>\n              # <strong>{{channel.name}}</strong>\n            </span>\n            <span>\n              {{first-8 channel.id}}\n            </span>\n          </Search::Result>\n        {{else}}\n          <span class='no-results'>{{t 'ui.search.noChannels'}}</span>\n        {{/each}}\n      </div>\n    {{/if}}\n  </div>\n\n  <footer>\n    <div class='left'>\n      <KeyboardShortcuts::Key @label='tab' />\n      {{t 'ui.shortcuts.label.search.tab'}}\n    </div>\n\n    <div class='right'>\n      <KeyboardShortcuts::Key @label='enter' />\n      {{t 'ui.shortcuts.label.search.enter'}}\n\n      <hr class='vertical'>\n\n      <KeyboardShortcuts::Key @label='esc' />\n      {{t 'ui.shortcuts.label.search.esc'}}\n    </div>\n  </footer>\n</Modal>\n"
  },
  {
    "path": "client/web/emberclear/app/components/search/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { restartableTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type StoreService from '@ember-data/store';\nimport type { Channel } from '@emberclear/local-account';\nimport type { Contact } from '@emberclear/local-account';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\ninterface IArgs {\n  isActive: boolean;\n  close: () => void;\n}\n\nconst MAX_RESULTS = 5;\n\nexport default class SearchModal extends Component<IArgs> {\n  @service store!: StoreService;\n  @service currentUser!: CurrentUserService;\n\n  @tracked searchText = '';\n  @tracked contactResults: Contact[] = [];\n  @tracked channelResults: Channel[] = [];\n\n  get numContacts() {\n    return this.contactResults.length;\n  }\n\n  get hasResults() {\n    return this.contactResults.length > 0 || this.channelResults.length > 0;\n  }\n\n  @action\n  submitSearch() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.search).perform(this.searchText);\n  }\n\n  @action\n  onInput(e: Event) {\n    this.searchText = (e.target as HTMLInputElement).value;\n    this.submitSearch();\n  }\n\n  @restartableTask({ withTestWaiter: true })\n  async search(searchTerm: string) {\n    const term = new RegExp(searchTerm, 'i');\n\n    let [contactResults, channelResults] = await Promise.all([\n      this.store.query('contact', { name: term }),\n      this.store.query('channel', { name: term }),\n    ]);\n\n    if (term.test(this.currentUser.name || '')) {\n      contactResults = contactResults.toArray() as any;\n      contactResults.push(this.currentUser.record);\n    }\n\n    this.contactResults = contactResults.slice(0, MAX_RESULTS);\n    this.channelResults = channelResults.slice(0, MAX_RESULTS);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/components/search/result.hbs",
    "content": "<a\n  href={{url-for @to @id}}\n  {{on 'click' (prevent-default\n    (queue\n      (transition-to (url-for @to @id))\n      @afterSelect\n    )\n  )}}\n  ...attributes\n>\n  {{yield}}\n</a>\n"
  },
  {
    "path": "client/web/emberclear/app/components/status-icon/index.hbs",
    "content": "<FaIcon @icon='dot-circle' @size='xs' class={{concat 'text-' this.color}} />"
  },
  {
    "path": "client/web/emberclear/app/components/status-icon/index.ts",
    "content": "import Component from '@glimmer/component';\n\nimport type { Contact } from '@emberclear/local-account';\n\ninterface IArgs {\n  contact: Contact;\n}\n\nexport default class StatusIcon extends Component<IArgs> {\n  get color() {\n    switch (this.args.contact.onlineStatus) {\n      case 'online':\n        return 'success';\n      case 'offline':\n        return 'lighter';\n      case 'away':\n        return 'warning';\n      case 'busy':\n        return 'lighter';\n      default:\n        return 'darker';\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: any;\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/emberclear/app/controllers/application.ts",
    "content": "import Controller from '@ember/controller';\n\nexport default class ApplicationController extends Controller {\n  queryParams = ['_features'];\n\n  _features = undefined;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/controllers/chat/in-channel.ts",
    "content": "import Controller from '@ember/controller';\nimport { inject as service } from '@ember/service';\n\nimport { TARGET } from '@emberclear/networking/models/message';\n\nimport type StoreService from '@ember-data/store';\n\nexport default class extends Controller {\n  @service store!: StoreService;\n\n  get id() {\n    return this.model.targetChannel.id;\n  }\n\n  get messages() {\n    return this.store.peekAll('message').filter((message) => {\n      const target = this.id;\n\n      return message.target === TARGET.CHANNEL && message.to === target;\n    });\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/controllers/chat/privately-with.ts",
    "content": "import Controller from '@ember/controller';\nimport { inject as service } from '@ember/service';\n\nimport { MESSAGE_LIMIT } from '@emberclear/networking/models/message';\nimport { messagesForDM } from '@emberclear/networking/models/message/utils';\n\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class extends Controller {\n  @service currentUser!: CurrentUserService;\n  @service store!: StoreService;\n\n  get uid() {\n    return this.model.targetIdentity.uid;\n  }\n\n  get messages() {\n    let allMessages = this.store.peekAll('message');\n    let me = this.currentUser.uid;\n    let chattingWithId = this.uid;\n    let filteredMessages = messagesForDM(allMessages, me, chattingWithId);\n    let mostRecent = filteredMessages.slice(0 - MESSAGE_LIMIT);\n\n    return mostRecent;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/controllers/invite.ts",
    "content": "import Controller from '@ember/controller';\n\nexport interface IQueryParams {\n  name?: string;\n  publicKey?: string;\n}\n\nexport default class InviteController extends Controller {\n  queryParams = ['name', 'publicKey'];\n}\n// DO NOT DELETE: this is how TypeScript knows how to look up your controllers.\ndeclare module '@ember/controller' {\n  interface Registry {\n    'invite-controller': InviteController;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/controllers/logout.ts",
    "content": "import Controller from '@ember/controller';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type SessionService from 'emberclear/services/session';\n\nexport default class LogoutController extends Controller {\n  @service declare session: SessionService;\n  @service declare router: RouterService;\n\n  @action\n  async logout() {\n    await this.session.logout();\n    await this.router.transitionTo('application');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/formats.js",
    "content": "export default {\n  time: {\n    hhmmss: {\n      hour: 'numeric',\n      minute: 'numeric',\n      second: 'numeric',\n    },\n  },\n  date: {\n    hhmmss: {\n      hour: 'numeric',\n      minute: 'numeric',\n      second: 'numeric',\n    },\n  },\n  number: {\n    EUR: {\n      style: 'currency',\n      currency: 'EUR',\n      minimumFractionDigits: 2,\n      maximumFractionDigits: 2,\n    },\n    USD: {\n      style: 'currency',\n      currency: 'USD',\n      minimumFractionDigits: 2,\n      maximumFractionDigits: 2,\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/first-8.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nexport function first8(params: any[] /*, hash*/) {\n  return params[0].substring(0, 8);\n}\n\nexport default buildHelper(first8);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/handle-sidebar-click.ts",
    "content": "import Helper from '@ember/component/helper';\nimport { inject as service } from '@ember/service';\n\nimport { TABLET_WIDTH } from 'emberclear/utils/breakpoints';\n\nimport type SidebarService from 'emberclear/services/sidebar';\n\ntype Args = [() => void];\n\nexport default class HandleSidebarClick extends Helper {\n  @service declare sidebar: SidebarService;\n\n  compute([handler]: Args) {\n    return (e?: Event) => {\n      e?.preventDefault?.();\n\n      if (window.innerWidth < TABLET_WIDTH) {\n        // non-blocking\n        // eslint-disable-next-line @typescript-eslint/no-floating-promises\n        this.sidebar.hide();\n      }\n\n      handler();\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/has-feature-flag.ts",
    "content": "import Helper from '@ember/component/helper';\nimport { inject as service } from '@ember/service';\n\nimport type SessionService from 'emberclear/services/session';\n\nexport default class extends Helper {\n  @service declare session: SessionService;\n\n  compute([flag]: string[]) {\n    return this.session.hasFeatureFlag(flag);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/includes.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\ntype PositionalArgs<T> = [T[], T];\n\nexport function includes<T>([collection, element]: PositionalArgs<T> /*, hash*/) {\n  return collection?.includes(element);\n}\n\nexport default buildHelper(includes);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/is-channel.js",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nimport Channel from 'emberclear/models/channel';\n\nexport function isChannel([record] /*, hash*/) {\n  return record instanceof Channel;\n}\n\nexport default buildHelper(isChannel);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/is-contact.js",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nimport Contact from 'emberclear/models/contact';\n\nexport function isContact([record] /*, hash*/) {\n  return record instanceof Contact;\n}\n\nexport default buildHelper(isContact);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/is-current-user.js",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nimport User from 'emberclear/models/user';\n\nexport function isContact([record] /*, hash*/) {\n  return record instanceof User;\n}\n\nexport default buildHelper(isContact);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/is-present.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\nimport { isPresent as eIsPresent } from '@ember/utils';\n\nexport function isPresent(params: any[] /*, hash*/) {\n  return eIsPresent(params[0]);\n}\n\nexport default buildHelper(isPresent);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/or.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nexport function or(params: any[] /*, hash*/) {\n  let result = false;\n\n  for (let i of params) {\n    result = result || i;\n  }\n\n  return result;\n}\n\nexport default buildHelper(or);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/queue.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\ntype Action = (...args: any[]) => any;\n\n// similar to\n// https://github.com/DockYard/ember-composable-helpers/blob/master/addon/helpers/queue.js\n// but way simpler... cause no promises\nexport function queue(actions: Action[] = []) {\n  return function (...args: any[]) {\n    return actions.forEach((action: Action) => action(...args));\n  };\n}\n\nexport default buildHelper(queue);\n"
  },
  {
    "path": "client/web/emberclear/app/helpers/sub.ts",
    "content": "import { helper as buildHelper } from '@ember/component/helper';\n\nexport function sub(params: any[] /*, hash*/) {\n  return params[0] - params[1];\n}\n\nexport default buildHelper(sub);\n"
  },
  {
    "path": "client/web/emberclear/app/index.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>emberclear</title>\n    <meta name=\"description\" content=\"emberclear is a privacy-first, end-to-end encrypted, chat. no phone numbers. no history or message storage. you are in control.\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"referrer\" content=\"no-referrer\">\n    <meta name=\"robots\" content=\"index, follow\" />\n    <meta name='keywords' content='emberclear, chat, ember, javascript, typescript, concurrency, privacy, secure, private, decentralized'>\n\n\n    {{content-for \"head\"}}\n\n    <link rel=\"manifest\" href=\"{{rootURL}}manifest.webmanifest\">\n    <meta name=\"msapplication-config\" content=\"{{rootURL}}browserconfig.xml\">\n\n    {{content-for \"head-footer\"}}\n    <style>\n      @keyframes spinAround {\n        from {\n          transform: rotate(0deg);\n        }\n        to {\n          transform: rotate(359deg);\n        }\n      }\n    </style>\n    <script>\n      window.global = window;\n      window.deferredInstallPrompt = null;\n\n      function captureInstallPrompt() {\n        if (!window || typeof window.addEventListener !== 'function') return;\n\n        window.addEventListener('beforeinstallprompt', (event) => {\n          event.preventDefault();\n          window.deferredInstallPrompt = event;\n        });\n      }\n\n\n      captureInstallPrompt();\n    </script>\n  </head>\n\n\n  <body>\n\n    {{content-for \"body\"}}\n\n    <div id='app-loader'\n      style='\n      z-index: 1000;\n      background: #333344; color: white;\n      position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n      display: flex;\n      padding-top: 13px; /* adjust this for vertical alignment */\n      flex-direction: column;\n      justify-content: center;\n      overflow-y: scroll;\n      align-items: center;'>\n\n      <div\n        style='\n          box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.12);\n          top: 0;\n          left: 0;\n          right: 0;\n          z-index: 1001;\n          position: fixed;\n          height: 50px;\n          background: white;\n        '\n       ></div>\n\n      <h1\n        style='\n          margin-top: 0;\n          margin-bottom: 0.5rem;\n          font-size: 2.5rem;\n          font-weight: 300;\n          line-height: 1.1;\n          font-family: -apple-system, system-ui, BlinkMacSystemFont,\n             \"Segoe UI\", Roboto, \"Helvetica Neue\",\n             Ubuntu, Arial, sans-serif;\n        '>\n        emberclear\n      </h1>\n\n      <p\n        style='\n          margin-top: 0;\n          margin-bottom: 1.5rem;\n          font-weight: 400;\n          font-family: -apple-system, system-ui, BlinkMacSystemFont,\n             \"Segoe UI\", Roboto, \"Helvetica Neue\",\n             Ubuntu, Arial, sans-serif;\n        '\n      >\n        Encrypted Chat. No History. No Logs.\n      </p>\n      <br>\n\n      <div\n        style='\n          position: fixed;\n          bottom: 100px;\n          width: 50px; height: 50px;\n          animation: spinAround 500ms infinite linear;\n          border: 2px solid #dbdbdb;\n          border-radius: 290486px;\n          border-right-color: transparent;\n          border-top-color: transparent;\n          content: \"\";\n          display: block;\n        '>\n      </div>\n    </div>\n\n    <link async rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link async rel=\"stylesheet\" href=\"{{rootURL}}assets/emberclear.css\">\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/emberclear.js\"></script>\n    <script src=\"{{rootURL}}assets/assets-fingerprint.js\"></script>\n\n    {{content-for \"body-footer\"}}\n\n    <a rel=\"me\" style=\"display: none;\" href=\"https://mastodon.coffee/@nullvoxpopuli\">Mastodon</a>\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/emberclear/app/models/action.ts",
    "content": "import Model, { attr } from '@ember-data/model';\n\nimport type Vote from '@emberclear/local-account/models/vote';\n\nexport enum ACTION_RESPONSE {\n  NONE = 'none',\n  APPROVED = 'approved',\n  DENIED = 'denied',\n  DISMISSED = 'dismissed',\n}\n\nexport default class Action extends Model {\n  @attr() vote!: Vote;\n  @attr() response!: ACTION_RESPONSE;\n  @attr() timestamp!: Date;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    action: Action;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/models/invitation-result.ts",
    "content": "import Model, { attr, belongsTo } from '@ember-data/model';\n\nimport type { Identity } from '@emberclear/local-account';\n\nexport default class InvitationResult extends Model {\n  @attr() createdAt!: Date;\n  @attr() isApproved!: boolean;\n\n  @belongsTo('invitation', { async: false }) invitation!: Identity;\n  @belongsTo('identity', { async: false }) responder!: Identity[];\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    'invitation-result': InvitationResult;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/models/invitation.ts",
    "content": "import Model, { attr, belongsTo, hasMany } from '@ember-data/model';\n\nimport type { Identity } from '@emberclear/local-account';\nimport type InvitationResult from 'emberclear/models/invitation-result';\n\nexport default class Invitation extends Model {\n  @attr() createdAt!: Date;\n\n  @belongsTo('identity', { async: false }) invited!: Identity;\n  @hasMany('invitation-result', { async: false }) results!: InvitationResult[];\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    invitation: Invitation;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/models/message-media.ts",
    "content": "import Model, { attr, belongsTo } from '@ember-data/model';\n\nimport type { Message } from '@emberclear/networking';\n\nexport default class MessageMedia extends Model {\n  @attr('string') url?: string;\n  @attr('string') mime?: string;\n\n  @belongsTo('message', { async: false }) message?: Message;\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your models.\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    'message-media': Message;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/format-code.ts",
    "content": "import { later } from '@ember/runloop';\nimport { inject as service } from '@ember/service';\n\nimport Modifier from 'ember-modifier';\n\nimport { parseLanguages } from 'emberclear/utils/string/utils';\n\nimport type PrismManager from 'emberclear/services/prism-manager';\n\ninterface Args {\n  positional: [string];\n  named: { [key: string]: unknown };\n}\n\nexport default class FormatCode extends Modifier<Args> {\n  @service declare prismManager: PrismManager;\n\n  didInstall() {\n    let text = this.args.positional[0];\n\n    // extra code features\n    this.makeCodeBlocksFancy();\n\n    // non-blocking\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.addLanguages(text);\n  }\n\n  private makeCodeBlocksFancy() {\n    if (!this.element) return;\n\n    const pres = this.element.querySelectorAll('pre');\n\n    if (pres?.length > 0) {\n      pres.forEach((p) => p.classList.add('line-numbers'));\n    }\n  }\n\n  private async addLanguages(text: string) {\n    const languages = parseLanguages(text);\n\n    languages.forEach((language) => {\n      (later as any)(() => {\n        (this.prismManager.addLanguage as TODO).perform(language, this.element);\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/has-unread.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { restartableTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport Modifier from 'ember-modifier';\n\nimport { selectUnreadDirectMessages } from '@emberclear/networking/models/message/utils';\n\nimport type StoreService from '@ember-data/store';\nimport type { Contact } from '@emberclear/local-account';\n\ninterface Args {\n  positional: [Contact];\n  named: EmptyRecord;\n}\n\nexport default class HasUnread extends Modifier<Args> {\n  @service declare store: StoreService;\n\n  get contact() {\n    return this.args.positional[0];\n  }\n\n  didReceiveArguments() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.findUnread).perform();\n  }\n\n  @restartableTask\n  async findUnread() {\n    await timeout(1000);\n    // TODO: are messages always going to be stored in memory?\n    //       no, that'd be ridiculous -- we want emberclear to work\n    //       on phones, too.\n    //\n    // potentially long operations are still yielded so that execution can be cancelled\n    let allMessages = await this.store.peekAll('message');\n\n    let unreadMessages = await selectUnreadDirectMessages(allMessages, this.contact.id);\n\n    this.contact.numUnread = unreadMessages.length;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/maybe-nudge-to-bottom.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport { taskFor } from 'ember-concurrency-ts';\nimport Modifier from 'ember-modifier';\n\nimport type { Message } from '@emberclear/networking';\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\ntype Args = {\n  positional: [Message[], Message];\n  named: EmptyRecord;\n};\n\nexport default class MaybeNudgeToBottom extends Modifier<Args> {\n  @service declare chatScroller: ChatScroller;\n\n  get messages() {\n    return this.args.positional[0];\n  }\n\n  get appendedMessage() {\n    return this.args.positional[1];\n  }\n\n  get lastMessage() {\n    let messages = this.messages;\n\n    return messages[messages.length - 1];\n  }\n\n  didInstall() {\n    if (this.appendedMessage.id !== this.lastMessage.id) return;\n\n    if (this.element) {\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.chatScroller.maybeNudge).perform(this.element as HTMLElement);\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/message-scroll-listener.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport Modifier from 'ember-modifier';\n\nimport type { Message } from '@emberclear/networking';\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\ninterface Args {\n  positional: [Message[]];\n  named: EmptyRecord;\n}\n\nexport default class MessageScrollListener extends Modifier<Args> {\n  @service declare chatScroller: ChatScroller;\n\n  scrollHandler!: () => void;\n  messagesElement!: Element;\n\n  get messages() {\n    return this.args.positional[0];\n  }\n\n  didInstall() {\n    let ticking = false;\n    let determine = this.determineIfLastIsVisible.bind(this);\n\n    this.scrollHandler = () => {\n      if (!ticking) {\n        window.requestAnimationFrame(function () {\n          determine();\n          ticking = false;\n        });\n\n        ticking = true;\n      }\n    };\n  }\n\n  didReceiveArguments() {\n    this.messagesElement = this.element!.querySelector('.messages')!;\n\n    if (this.messagesElement) {\n      if (!this.chatScroller.isViewingOlderMessages) {\n        this.messagesElement.scrollTop = this.messagesElement.scrollHeight;\n      }\n\n      this.messagesElement.removeEventListener('scroll', this.scrollHandler);\n      this.messagesElement.addEventListener('scroll', this.scrollHandler);\n    }\n  }\n\n  willRemove() {\n    if (this.messagesElement) {\n      this.messagesElement.removeEventListener('scroll', this.scrollHandler);\n    }\n  }\n\n  private determineIfLastIsVisible() {\n    let last = this.messages[this.messages.length - 1];\n\n    this.chatScroller.isLastVisible = this.chatScroller._isLastVisible(last);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/qr-image.ts",
    "content": "import { restartableTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport Modifier from 'ember-modifier';\n\nimport { convertObjectToQRCodeDataURL } from '@emberclear/encoding/string';\n\ntype Args = {\n  positional: [Record<string, unknown>];\n  named: EmptyRecord;\n};\n\nexport default class QRImageModifier extends Modifier<Args> {\n  didReceiveArguments() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this.dataToQR).perform();\n  }\n\n  @restartableTask\n  async dataToQR() {\n    let data = this.args.positional[0];\n\n    let urlData = await convertObjectToQRCodeDataURL(data || {});\n\n    if (isImage(this.element)) {\n      this.element.src = urlData;\n    }\n  }\n}\n\nfunction isImage(element?: Element | null): element is HTMLImageElement {\n  return 'src' in (element || {});\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/read-watcher.ts",
    "content": "import { action } from '@ember/object';\n\nimport { timeout } from 'ember-concurrency';\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport Modifier from 'ember-modifier';\n\nimport { markAsRead } from '@emberclear/networking/models/message/utils';\n\nimport type { Message } from '@emberclear/networking';\n\ninterface Args {\n  positional: [Message];\n  named: EmptyRecord;\n}\n\nexport default class ReadWatcher extends Modifier<Args> {\n  io?: IntersectionObserver;\n  message!: Message;\n\n  didInstall() {\n    let [message] = this.args.positional;\n\n    this.message = message;\n    this.maybeSetupReadWatcher();\n  }\n\n  // NOTE: this method should not exist, but does\n  //       because vertical-collection recycles\n  //       nodes\n  didUpdateArguments() {\n    this.willRemove();\n    this.didInstall();\n  }\n\n  willRemove() {\n    this.disconnect();\n  }\n\n  /**\n   * if already read, this method happens to do nothing\n   * */\n  private disconnect() {\n    if (this.element) {\n      this.io?.unobserve(this.element);\n      this.element.removeEventListener('click', this.markRead);\n    }\n\n    this.io?.disconnect();\n    this.io = undefined;\n  }\n\n  // Needs the `this` bound, because of eventListener\n  @action\n  private markRead() {\n    if (this.message.unread) {\n      // eslint-disable-next-line @typescript-eslint/no-floating-promises\n      taskFor(this.markReadTask).perform();\n    }\n\n    this.disconnect();\n  }\n\n  private maybeSetupReadWatcher() {\n    if (this.message.readAt) return;\n\n    this.setupIntersectionObserver();\n\n    if (this.element) {\n      this.element.addEventListener('click', this.markRead);\n    }\n  }\n\n  private setupIntersectionObserver() {\n    const io = new IntersectionObserver(\n      (entries) => {\n        const isVisible = entries[0].intersectionRatio !== 0;\n        const canBeSeen = !this.message.isSaving && document.hasFocus();\n\n        if (isVisible && canBeSeen) {\n          this.markRead();\n        }\n      },\n      {\n        root: document.querySelector('.messages'),\n      }\n    );\n\n    if (this.element) {\n      io.observe(this.element);\n    }\n\n    this.io = io;\n  }\n\n  @task({ withTestWaiter: true })\n  async markReadTask() {\n    let attempts = 0;\n\n    while (attempts < 100) {\n      attempts++;\n\n      if (this.message.readAt) {\n        return;\n      }\n\n      if (this.message.isSaving || !document.hasFocus()) {\n        await timeout(5);\n      } else {\n        await markAsRead(this.message);\n\n        return;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/unread-message-list-observer.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { enqueueTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport Modifier from 'ember-modifier';\n\nimport { isInElementWithinViewport } from 'emberclear/utils/dom/utils';\n\nimport { markAsRead } from '@emberclear/networking/models/message/utils';\n\nimport type StoreService from '@ember-data/store';\nimport type { Message } from '@emberclear/networking';\nimport type SidebarService from 'emberclear/services/sidebar';\n\nexport default class UnreadMessagesIntersectionObserver extends Modifier {\n  @service declare sidebar: SidebarService;\n  @service declare store: StoreService;\n\n  focusHandler!: () => void;\n\n  didInstall() {\n    this.focusHandler = this.respondToWindowFocus.bind(this);\n\n    window.addEventListener('focus', this.focusHandler);\n  }\n\n  willRemove() {\n    window.removeEventListener('focus', this.focusHandler);\n  }\n\n  private respondToWindowFocus() {\n    const container = document.querySelector('.messages')!;\n    // TODO: add unread status to the DOM, and then only query unread messages\n    const messages = container.querySelectorAll('.message');\n\n    messages.forEach((message) => {\n      const isVisible = isInElementWithinViewport(message, container);\n\n      if (isVisible) {\n        const record = this.store.peekRecord('message', message.id);\n\n        if (record) {\n          // eslint-disable-next-line @typescript-eslint/no-floating-promises\n          taskFor(this.markRead).perform(record);\n        }\n      }\n    });\n  }\n\n  @enqueueTask({ withTestWaiter: true, maxConcurrency: 30 })\n  async markRead(message: Message) {\n    let attempts = 0;\n\n    while (attempts < 100) {\n      attempts++;\n\n      if (message.readAt) {\n        return;\n      }\n\n      if (message.isSaving || !document.hasFocus()) {\n        await timeout(10);\n      } else {\n        await markAsRead(message);\n\n        if (message.sender && message.sender.numUnread > 0) {\n          message.sender.numUnread--;\n        }\n\n        return;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/unread-messages-intersection-observer.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport Modifier from 'ember-modifier';\n\nimport type SidebarService from 'emberclear/services/sidebar';\n\nexport default class UnreadMessagesIntersectionObserver extends Modifier {\n  @service declare sidebar: SidebarService;\n\n  didInstall() {\n    this.sidebar.ensureUnreadIntersectionObserverExists();\n\n    if (this.sidebar.unreadObserver && this.element) {\n      this.sidebar.unreadObserver.observe(this.element);\n    }\n  }\n\n  willRemove() {\n    if (this.sidebar.unreadObserver && this.element) {\n      this.sidebar.unreadObserver.unobserve(this.element);\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/modifiers/update-document-title.ts",
    "content": "import { inject as service } from '@ember/service';\n\nimport Modifier from 'ember-modifier';\n\nimport type CurrentChatService from '../services/current-chat';\n\nexport default class UpdateDocumentTitle extends Modifier {\n  @service declare currentChat: CurrentChatService;\n  @service declare intl: Intl;\n\n  originalDocumentTitle: string;\n  appName: string;\n\n  constructor(owner: any, args: any) {\n    super(owner, args);\n    this.originalDocumentTitle = document.title;\n    this.appName = this.intl.t('appname');\n  }\n\n  get tokens() {\n    return this.args.positional.filter((token) => token);\n  }\n\n  willDestroy() {\n    document.title = this.originalDocumentTitle;\n  }\n\n  didReceiveArguments() {\n    let tokens = this.tokens;\n\n    let currentChat = this.currentChat.name;\n\n    if (currentChat) {\n      tokens.push(currentChat);\n    }\n\n    tokens.push(this.appName);\n\n    document.title = tokens.join(' | ');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/router.js",
    "content": "import Ember from 'ember';\nimport EmberRouter from '@ember/routing/router';\n\nimport config from 'emberclear/config/environment';\n\nconst scrollContainer = '.mobile-menu-wrapper';\n\nclass Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n\n  constructor() {\n    super(...arguments);\n\n    this.on('routeDidChange', () => {\n      // window would normally be used for scrolling, but that doesn't work\n      // for testing...\n      if (Ember.testing) {\n        document.querySelector(scrollContainer)?.scrollTo(0, 0);\n      } else {\n        window.scrollTo(0, 0);\n      }\n    });\n  }\n}\n\nRouter.map(function () {\n  this.route('chat', function () {\n    this.route('privately-with', { path: '/privately-with/:id' });\n    this.route('in-channel', { path: '/in-channel/:id' });\n  });\n\n  this.route('setup');\n\n  this.route('contacts');\n  this.route('login');\n  this.route('invite');\n  this.route('logout');\n  this.route('settings', function () {\n    this.route('interface');\n    this.route('relays');\n    this.route('danger-zone');\n  });\n  this.route('faq');\n\n  this.route('add-friend');\n\n  this.route('not-found', { path: '/*path' });\n  this.route('qr');\n});\n\nexport default Router;\n"
  },
  {
    "path": "client/web/emberclear/app/routes/add-friend.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type RedirectManager from 'emberclear/services/redirect-manager';\n\nexport default class AddFriendRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n  @service declare redirectManager: RedirectManager;\n\n  async beforeModel() {\n    // identity should be loaded from application route\n    if (this.currentUser.isLoggedIn) {\n      await this.redirectManager.evaluate();\n\n      return;\n    }\n\n    // no identity, need to create one\n    await this.transitionTo('setup');\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/application.ts",
    "content": "import { getOwner } from '@ember/application';\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport { ensureRelays } from '@emberclear/networking';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Channel from '@emberclear/local-account/models/channel';\nimport type Contact from '@emberclear/local-account/models/contact';\nimport type { ConnectionService, Message } from '@emberclear/networking';\nimport type LocaleService from 'emberclear/services/locale';\nimport type Notifications from 'emberclear/services/notifications';\nimport type Settings from 'emberclear/services/settings';\n\ninterface Model {\n  contacts: ArrayProxy<Contact>;\n  channels: ArrayProxy<Channel>;\n}\n\nexport default class ApplicationRoute extends Route {\n  @service declare store: StoreService;\n  @service declare currentUser: CurrentUserService;\n  @service declare locale: LocaleService;\n  @service declare notifications: Notifications;\n  @service declare intl: Intl;\n  @service declare settings: Settings;\n  @service declare connection: ConnectionService;\n\n  async beforeModel() {\n    (this.store as any).shouldTrackAsyncRequests = true;\n    (this.store as any).generateStackTracesForTrackedRequests = true;\n\n    // TODO: check all the modern web requirements\n    this.settings.applyTheme();\n\n    await this.locale.setLocale(this.locale.currentLocale);\n    await ensureRelays(getOwner(this));\n    await this.currentUser.load();\n  }\n\n  async model(): Promise<Model> {\n    // While these models aren't used directly, we'll be reading\n    // from cache deep in the component tree\n    const [contacts, channels /*messages*/] = await Promise.all([\n      this.store.findAll('contact', { backgroundReload: true }),\n      this.store.findAll('channel', { backgroundReload: true }),\n      this.store.findAll('message', { backgroundReload: true }),\n    ]);\n\n    return { contacts, channels };\n  }\n\n  afterModel() {\n    if (this.currentUser.isLoggedIn) {\n      this.connection.connect();\n      this.connection.hooks = {\n        onReceive: async (message: Message) => {\n          if (message.sender) {\n            let name = message.sender.name;\n            let msg = this.intl.t('ui.notifications.from', { name });\n\n            await this.notifications.info(msg);\n          }\n        },\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/chat/in-channel.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\ninterface IModelParams {\n  id: string;\n}\n\nexport default class ChatInChannelRoute extends Route {\n  @service declare store: StoreService;\n  @service declare currentUser: CurrentUserService;\n  @service declare toast: Toast;\n  @service declare intl: Intl;\n\n  async model(params: IModelParams) {\n    let { id } = params;\n\n    let targetChannel;\n\n    try {\n      targetChannel = await this.store.findRecord('channel', id);\n    } catch (error) {\n      this.toast.error(error || this.intl.t('ui.chat.errors.channelNotFound'));\n      await this.transitionTo('chat.index');\n    }\n\n    return {\n      targetChannel,\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/chat/privately-with.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport { currentUserId } from '@emberclear/local-account';\n\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\ninterface IModelParams {\n  id: string;\n}\n\nexport default class ChatPrivatelyRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n  @service declare chatScroller: ChatScroller;\n  @service declare toast: Toast;\n  @service declare intl: Intl;\n  @service declare store: StoreService;\n\n  async beforeModel(transition: any) {\n    let params = transition.to.params;\n    let { id } = params as IModelParams;\n\n    if (id === this.currentUser.uid) {\n      await this.transitionTo('chat.privately-with', currentUserId);\n    }\n\n    // Tells the view to scroll to the bottom.\n    // TODO: is there a way to do this with just CSS?\n    this.chatScroller.isLastVisible = true;\n  }\n\n  async model(params: IModelParams) {\n    const { id } = params;\n\n    let record;\n\n    try {\n      if (id === currentUserId) {\n        record = this.currentUser.record;\n      } else {\n        record = await this.store.findRecord('contact', id);\n      }\n    } catch (error) {\n      this.toast.error(error || this.intl.t('ui.chat.errors.contactNotFound'));\n\n      await this.transitionTo('chat.index');\n    }\n\n    return {\n      targetIdentity: record,\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/chat.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { ConnectionService, Message } from '@emberclear/networking';\nimport type RedirectManager from 'emberclear/services/redirect-manager';\n\nexport interface Model {\n  messages: ArrayProxy<Message>;\n}\n\nexport default class ChatRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n  @service declare redirectManager: RedirectManager;\n  @service declare connection: ConnectionService;\n  @service declare store: StoreService;\n\n  async beforeModel() {\n    // identity should be loaded from application route\n    if (this.currentUser.isLoggedIn) {\n      await this.redirectManager.evaluate();\n\n      return;\n    }\n\n    // no identity, need to create one\n    await this.transitionTo('setup');\n  }\n\n  async model(): Promise<Model> {\n    const messages = await this.store.findAll('message', {\n      backgroundReload: true,\n      include: 'sender',\n    });\n\n    return { messages };\n  }\n\n  afterModel() {\n    this.connection.connect();\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/contacts.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class ContactsRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n\n  async beforeModel() {\n    const exists = await this.currentUser.exists();\n\n    if (!exists) {\n      await this.transitionTo('setup');\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/invite.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\nimport { isPresent } from '@ember/utils';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type ChannelManager from '@emberclear/local-account/services/channel-manager';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\nimport type { IQueryParams } from 'emberclear/controllers/invite';\nimport type RedirectManager from 'emberclear/services/redirect-manager';\n\nexport default class InviteRoute extends Route {\n  @service declare toast: Toast;\n  @service declare currentUser: CurrentUserService;\n  @service declare contactManager: ContactManager;\n  @service declare channelManager: ChannelManager;\n  @service declare redirectManager: RedirectManager;\n\n  async beforeModel(transition: any) {\n    transition.abort();\n\n    // identity should be loaded from application route\n    if (this.currentUser.isLoggedIn) {\n      await this.acceptInvite(transition);\n\n      return;\n    }\n\n    this.toast.info('Please login or create your account before the invite can be accepted');\n\n    this.redirectManager.persistURL(transition.intent.url);\n\n    // no identity, need to create one\n    await this.transitionTo('setup');\n  }\n\n  async acceptInvite(transition: any) {\n    const query = transition.to.queryParams as IQueryParams;\n\n    if (this.hasParams(query)) {\n      const { name, publicKey } = query;\n\n      // if (isPresent(publicKey)) {\n      return await this.acceptContactInvite(name!, publicKey!);\n      // }\n    }\n\n    this.toast.error('Invalid Invite Link');\n\n    return this.transitionTo('chat');\n  }\n\n  private async acceptContactInvite(name: string, publicKey: string) {\n    if (publicKey === this.currentUser.record!.publicKeyAsHex) {\n      this.toast.warning(`You can't invite yourself... but you can talk to yourself!`);\n\n      return this.transitionTo('/chat/privately-with/me');\n    }\n\n    try {\n      await this.contactManager.findOrCreate(publicKey!, name!);\n    } catch (e) {\n      this.toast.error(`There was a problem importing ${name}: ${e.message}`);\n\n      return this.transitionTo('chat');\n    }\n\n    this.toast.success(`${name} has been successfully imported!`);\n\n    await this.transitionTo(`/chat/privately-with/${publicKey}`);\n  }\n\n  private hasParams({ name, publicKey }: IQueryParams) {\n    // TODO: support additional / different params for private channels\n    return isPresent(name) && isPresent(publicKey);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/login.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type { WorkersService } from '@emberclear/crypto';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class SettingsRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n  @service declare workers: WorkersService;\n\n  async beforeModel() {\n    // don't need to login, if we are already logged in\n    const exists = await this.currentUser.exists();\n\n    if (exists) {\n      await this.transitionTo('/');\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/logout.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Sidebar from 'emberclear/services/sidebar';\n\nexport default class LogoutRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n  @service declare sidebar: Sidebar;\n\n  // ensure we are allowed to be here\n  async beforeModel() {\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.sidebar.hide();\n\n    const exists = await this.currentUser.exists();\n\n    if (!exists) {\n      await this.transitionTo('setup');\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/qr.js",
    "content": "import Route from '@ember/routing/route';\n\nexport default class QrRoute extends Route {}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/settings/relays.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type ArrayProxy from '@ember/array/proxy';\nimport type StoreService from '@ember-data/store';\nimport type { Relay } from '@emberclear/networking';\n\nexport interface Model {\n  relays: ArrayProxy<Relay>;\n}\n\nexport default class SettingsRelayRoute extends Route {\n  @service declare store: StoreService;\n\n  async model(): Promise<Model> {\n    const relays = await this.store.findAll('relay');\n\n    return { relays };\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/settings.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class SettingsRoute extends Route {\n  @service declare currentUser: CurrentUserService;\n\n  async beforeModel() {\n    const exists = await this.currentUser.exists();\n\n    if (!exists) {\n      await this.transitionTo('setup');\n\n      return;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/routes/setup.ts",
    "content": "import Route from '@ember/routing/route';\n\nexport default class SetupRoute extends Route {\n  async beforeModel() {\n    /* intentionally empty due to some weird bug that I don't know about */\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/channels/-utils/channel-factory.ts",
    "content": "import { toHex } from '@emberclear/encoding/string';\n\nimport type { Channel } from '@emberclear/local-account';\nimport type ChannelContextChain from '@emberclear/local-account/models/channel-context-chain';\nimport type Identity from '@emberclear/local-account/models/identity';\nimport type Vote from '@emberclear/local-account/models/vote';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\n\nexport function buildChannelInfo(channel: Channel): StandardMessage['channelInfo'] {\n  return {\n    uid: channel.id,\n    name: channel.name,\n    activeVotes: channel.activeVotes.map((activeVote) => buildVote(activeVote)),\n    contextChain: buildChannelContextChain(channel.contextChain),\n  };\n}\n\nexport function buildChannelContextChain(\n  contextChain: ChannelContextChain\n): StandardChannelContextChain {\n  return {\n    id: contextChain.id,\n    admin: buildChannelMember(contextChain.admin),\n    members: contextChain.members.map((member) => buildChannelMember(member)),\n    supportingVote:\n      !contextChain.previousChain && !contextChain.supportingVote\n        ? undefined\n        : buildVoteChain(contextChain.supportingVote),\n    previousChain:\n      !contextChain.previousChain && !contextChain.supportingVote\n        ? undefined\n        : buildChannelContextChain(contextChain.previousChain),\n  };\n}\n\nexport function buildChannelMember(member: Identity): ChannelMember {\n  return {\n    id: member.uid,\n    name: member.name,\n    signingKey: member.publicSigningKeyAsHex,\n  };\n}\n\nexport function buildVote(vote: Vote): StandardVote {\n  return {\n    id: vote.id,\n    voteChain: buildVoteChain(vote.voteChain),\n  };\n}\n\nexport function buildVoteChain(voteChain: VoteChain): StandardVoteChain {\n  return {\n    id: voteChain.id,\n    remaining: voteChain.remaining.map((member) => buildChannelMember(member)),\n    yes: voteChain.yes.map((member) => buildChannelMember(member)),\n    no: voteChain.no.map((member) => buildChannelMember(member)),\n    target: buildChannelMember(voteChain.target),\n    action: voteChain.action,\n    key: buildChannelMember(voteChain.key),\n    previousVoteChain: !voteChain.previousVoteChain\n      ? undefined\n      : buildVoteChain(voteChain.previousVoteChain),\n    signature: toHex(voteChain.signature),\n  };\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/channels/-utils/vote-sorter.ts",
    "content": "import { convertObjectToUint8Array, toHex } from '@emberclear/encoding/string';\n\nimport type Identity from '@emberclear/local-account/models/identity';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\nimport type { VOTE_ACTION } from '@emberclear/local-account/models/vote-chain';\n\nexport const VOTE_ORDERING = {\n  remaining: 0,\n  yes: 1,\n  no: 2,\n  targetKey: 3,\n  action: 4,\n  voterSigningKey: 5,\n  previousChainSignature: 6,\n} as const;\n\nexport type SortedVote = [\n  Uint8Array[],\n  Uint8Array[],\n  Uint8Array[],\n  Uint8Array,\n  VOTE_ACTION,\n  Uint8Array,\n  Uint8Array | undefined\n];\n\nexport type SortedVoteHex = [\n  string[],\n  string[],\n  string[],\n  string,\n  VOTE_ACTION,\n  string,\n  string | undefined\n];\n\nexport function generateSortedVote(vote: VoteChain): Uint8Array {\n  let toReturn: SortedVoteHex = [\n    toSortedPublicKeys(vote.remaining),\n    toSortedPublicKeys(vote.yes),\n    toSortedPublicKeys(vote.no),\n    toHex(vote.target.publicKey),\n    vote.action,\n    toHex(vote.key.publicSigningKey),\n    vote.previousVoteChain ? toHex(vote.previousVoteChain.signature) : undefined,\n  ];\n\n  return convertObjectToUint8Array<SortedVoteHex>(toReturn);\n}\n\nfunction toSortedPublicKeys(identities: Identity[]): string[] {\n  return identities\n    .map((identity) => identity.publicKey)\n    .sort()\n    .map((key) => toHex(key));\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/channels/channel-verifier.ts",
    "content": "import Service, { inject as service } from '@ember/service';\n\nimport { identitiesIncludes, identityEquals } from 'emberclear/utils/identity-comparison';\n\nimport { VOTE_ACTION } from '@emberclear/local-account/models/vote-chain';\n\nimport type { Identity } from '@emberclear/local-account';\nimport type ChannelContextChain from '@emberclear/local-account/models/channel-context-chain';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\nimport type VoteVerifier from 'emberclear/services/channels/vote-verifier';\n\nexport default class ChannelVerifier extends Service {\n  @service('channels/vote-verifier') voteVerifier!: VoteVerifier;\n\n  async isValidChain(channel: ChannelContextChain): Promise<boolean> {\n    let isFirstContextChain: boolean = !channel.previousChain && !channel.supportingVote;\n\n    if (isFirstContextChain) {\n      return this.isValidSingleChain(channel);\n    }\n\n    let somethingWrongWithPastOrSupportingVote =\n      !(await this.isValidChain(channel.previousChain)) ||\n      !(await this.voteVerifier.isValid(channel.supportingVote)) ||\n      !this.isVoteCompletedPositive(channel.supportingVote, channel.previousChain.admin);\n\n    if (somethingWrongWithPastOrSupportingVote) {\n      return false;\n    }\n\n    switch (channel.supportingVote.action) {\n      case VOTE_ACTION.ADD:\n        return this.isAddValid(channel);\n      case VOTE_ACTION.PROMOTE:\n        return this.isPromoteValid(channel);\n      case VOTE_ACTION.REMOVE:\n        return this.isRemoveValid(channel);\n      default:\n        return false;\n    }\n  }\n\n  private isValidSingleChain(channel: ChannelContextChain): boolean {\n    return !(\n      channel.admin === undefined ||\n      channel.members.length !== 1 ||\n      !identityEquals(channel.members.objectAt(0)!, channel.admin)\n    );\n  }\n\n  private isVoteCompletedPositive(vote: VoteChain, admin: Identity): boolean {\n    if (\n      vote.yes.length > vote.no.length + vote.remaining.length ||\n      (vote.yes.length === vote.no.length + vote.remaining.length &&\n        identitiesIncludes(vote.yes.toArray(), admin))\n    ) {\n      return true;\n    }\n\n    return false;\n  }\n\n  private getDiffs(previousMembers: Identity[], currentMembers: Identity[]) {\n    let currentMembersDiff = currentMembers.filter(\n      (identity) => !identitiesIncludes(previousMembers, identity)\n    );\n    let pastMembersDiff = previousMembers.filter(\n      (identity) => !identitiesIncludes(currentMembers, identity)\n    );\n\n    return { currentMembersDiff, pastMembersDiff };\n  }\n\n  private isAddValid(channel: ChannelContextChain): boolean {\n    let previousMembers = channel.previousChain.members.toArray();\n    let target = channel.supportingVote.target;\n    let currentMembers = channel.members.toArray();\n\n    if (!identityEquals(channel.admin, channel.previousChain.admin)) {\n      return false;\n    }\n\n    let { currentMembersDiff, pastMembersDiff } = this.getDiffs(previousMembers, currentMembers);\n\n    if (pastMembersDiff.length > 0) {\n      return false;\n    }\n\n    if (currentMembersDiff.length === 1 && currentMembersDiff[0] === target) {\n      return true;\n    }\n\n    return false;\n  }\n\n  private isPromoteValid(channel: ChannelContextChain): boolean {\n    let previousMembers = channel.previousChain.members.toArray();\n    let target = channel.supportingVote.target;\n    let currentMembers = channel.members.toArray();\n\n    let { currentMembersDiff, pastMembersDiff } = this.getDiffs(previousMembers, currentMembers);\n\n    if (pastMembersDiff.length > 0 || currentMembersDiff.length > 0) {\n      return false;\n    }\n\n    if (identityEquals(channel.admin, target)) {\n      return true;\n    }\n\n    return false;\n  }\n\n  private isRemoveValid(channel: ChannelContextChain): boolean {\n    let previousMembers = channel.previousChain.members.toArray();\n    let target = channel.supportingVote.target;\n    let currentMembers = channel.members.toArray();\n\n    if (!identityEquals(channel.admin, channel.previousChain.admin)) {\n      return false;\n    }\n\n    let { currentMembersDiff, pastMembersDiff } = this.getDiffs(previousMembers, currentMembers);\n\n    if (currentMembersDiff.length > 0) {\n      return false;\n    }\n\n    if (pastMembersDiff.length === 1 && pastMembersDiff[0] === target) {\n      return true;\n    }\n\n    return false;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'channels/channel-verifier': ChannelVerifier;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/channels/vote-verifier.ts",
    "content": "import { assert } from '@ember/debug';\nimport Service, { inject as service } from '@ember/service';\n\nimport { identitiesIncludes, identityEquals } from 'emberclear/utils/identity-comparison';\nimport { equalsUint8Array } from 'emberclear/utils/uint8array-equality';\n\nimport { CryptoConnector } from '@emberclear/crypto';\n\nimport { generateSortedVote } from './-utils/vote-sorter';\n\nimport type { WorkersService } from '@emberclear/crypto';\nimport type Identity from '@emberclear/local-account/models/identity';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\n\nexport default class VoteVerifier extends Service {\n  @service workers!: WorkersService;\n  crypto?: CryptoConnector;\n\n  async isValid(voteToVerify: VoteChain): Promise<boolean> {\n    this.connectCrypto();\n\n    if (\n      !this.crypto ||\n      !this.isKeyMatchingVoteDiff(voteToVerify) ||\n      !this.isTargetAndActionUnchanged(voteToVerify)\n    ) {\n      return false;\n    }\n\n    let voteToVerifyActual: Uint8Array = generateSortedVote(voteToVerify);\n    let voteToVerifyActualHash: Uint8Array = await this.crypto.hash(voteToVerifyActual);\n\n    let voteToVerifyExpectedHash = await this.crypto.openSigned(\n      voteToVerify.signature,\n      voteToVerify.key.publicSigningKey\n    );\n\n    assert(\n      `Something went wrong with opening the sign message, figure this out`,\n      voteToVerifyExpectedHash\n    );\n\n    if (!equalsUint8Array(voteToVerifyActualHash, voteToVerifyExpectedHash)) {\n      return false;\n    }\n\n    if (!voteToVerify.previousVoteChain) {\n      return true;\n    }\n\n    return this.isValid(voteToVerify.previousVoteChain);\n  }\n\n  private connectCrypto() {\n    if (this.crypto) return;\n\n    this.crypto = new CryptoConnector({\n      workerService: this.workers,\n    });\n  }\n\n  // Checks to make sure that target and action haven't been modified from one vote to another\n  private isTargetAndActionUnchanged(vote: VoteChain): boolean {\n    if (!vote.previousVoteChain) {\n      return true;\n    }\n\n    return (\n      identityEquals(vote.previousVoteChain.target, vote.target) &&\n      vote.action === vote.previousVoteChain.action\n    );\n  }\n\n  // Checks that the key of the signer matches the change in yes/no/remaining\n  // Makes sure that a vote entails a shift of the signer from one category to another\n  private isKeyMatchingVoteDiff(vote: VoteChain): boolean {\n    if (!vote.previousVoteChain) {\n      return this.isProperMoveBase(vote);\n    }\n\n    let isValid = false;\n\n    if (identitiesIncludes(vote.previousVoteChain.yes.toArray(), vote.key)) {\n      isValid = this.isProperMove(\n        vote.yes.toArray(),\n        vote.remaining.toArray(),\n        vote.no.toArray(),\n        vote.key,\n        vote.previousVoteChain.yes.toArray(),\n        vote.previousVoteChain.remaining.toArray(),\n        vote.previousVoteChain.no.toArray()\n      );\n    } else if (identitiesIncludes(vote.previousVoteChain.no.toArray(), vote.key)) {\n      isValid = this.isProperMove(\n        vote.no.toArray(),\n        vote.yes.toArray(),\n        vote.remaining.toArray(),\n        vote.key,\n        vote.previousVoteChain.no.toArray(),\n        vote.previousVoteChain.remaining.toArray(),\n        vote.previousVoteChain.yes.toArray()\n      );\n    } else if (identitiesIncludes(vote.previousVoteChain.remaining.toArray(), vote.key)) {\n      isValid = this.isProperMove(\n        vote.remaining.toArray(),\n        vote.yes.toArray(),\n        vote.no.toArray(),\n        vote.key,\n        vote.previousVoteChain.remaining.toArray(),\n        vote.previousVoteChain.yes.toArray(),\n        vote.previousVoteChain.no.toArray()\n      );\n    }\n\n    return isValid;\n  }\n\n  //Checks that the only movement of votes from the previous vote to the current vote is the voter\n  private isProperMove(\n    origin: Identity[],\n    possibility1: Identity[],\n    possibility2: Identity[],\n    key: Identity,\n    originPast: Identity[],\n    possibility1Past: Identity[],\n    possibility2Past: Identity[]\n  ): boolean {\n    let originDiffs = this.getVoterDiffs(originPast, origin);\n    let possibility1Diffs = this.getVoterDiffs(possibility1Past, possibility1);\n    let possibility2Diffs = this.getVoterDiffs(possibility2Past, possibility2);\n    let isOriginDiffCorrect =\n      originDiffs.currentVoterDiffs.length === 0 &&\n      originDiffs.pastVoterDiffs.length === 1 &&\n      identityEquals(originDiffs.pastVoterDiffs[0], key);\n    let isPossibiltiesDiffsCorrect =\n      ((possibility1Diffs.currentVoterDiffs.length === 1 &&\n        possibility1Diffs.pastVoterDiffs.length === 0 &&\n        identityEquals(possibility1Diffs.currentVoterDiffs[0], key)) ||\n        (possibility2Diffs.currentVoterDiffs.length === 1 &&\n          possibility2Diffs.pastVoterDiffs.length === 0 &&\n          identityEquals(possibility2Diffs.currentVoterDiffs[0], key))) &&\n      !(\n        possibility1Diffs.currentVoterDiffs.length === 1 &&\n        possibility1Diffs.pastVoterDiffs.length === 0 &&\n        identityEquals(possibility1Diffs.currentVoterDiffs[0], key) &&\n        possibility2Diffs.currentVoterDiffs.length === 1 &&\n        possibility2Diffs.pastVoterDiffs.length === 0 &&\n        identityEquals(possibility2Diffs.currentVoterDiffs[0], key)\n      );\n\n    return isOriginDiffCorrect && isPossibiltiesDiffsCorrect;\n  }\n\n  private getVoterDiffs(previousVoters: Identity[], currentVoters: Identity[]): VoterDiffs {\n    let currentVoterDiff = currentVoters.filter(\n      (identity) => !identitiesIncludes(previousVoters, identity)\n    );\n    let pastVoterDiff = previousVoters.filter(\n      (identity) => !identitiesIncludes(currentVoters, identity)\n    );\n\n    return { currentVoterDiffs: currentVoterDiff, pastVoterDiffs: pastVoterDiff };\n  }\n\n  // Checks that a base vote moves the voter from remaining to either yes or no\n  private isProperMoveBase(vote: VoteChain): boolean {\n    return (\n      !identitiesIncludes(vote.remaining.toArray(), vote.key) &&\n      this.isInOneButNotBoth(vote.yes.toArray(), vote.no.toArray(), vote.key) &&\n      (vote.yes.toArray().length === 1 || vote.no.toArray().length === 1) &&\n      !(vote.yes.toArray().length === 1 && vote.no.toArray().length === 1)\n    );\n  }\n\n  private isInOneButNotBoth(arr1: Identity[], arr2: Identity[], key: Identity): boolean {\n    return (\n      (identitiesIncludes(arr1, key) || identitiesIncludes(arr2, key)) &&\n      !(identitiesIncludes(arr1, key) && identitiesIncludes(arr2, key))\n    );\n  }\n}\n\nexport type VoterDiffs = {\n  currentVoterDiffs: Identity[];\n  pastVoterDiffs: Identity[];\n};\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'channels/vote-verifier': VoteVerifier;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/chat-scroller.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\nimport { dropTask, restartableTask } from 'ember-concurrency-decorators';\n\nimport type { Message } from '@emberclear/networking';\n\n// This is used to give the task time to restart as the view settles\n// and tries to scroll multiple times\nconst SCROLL_DELAY = 100;\n\nexport default class ChatScroller extends Service {\n  @tracked isLastVisible = true;\n\n  get isViewingOlderMessages() {\n    return !this.isLastVisible;\n  }\n\n  _isLastVisible(message: Message) {\n    // nothing to show, don't indicate that the last message isn't visible.\n    if (!message) return true;\n\n    return isLastVisible(message.id);\n  }\n\n  // if the last message is close enough to being in view,\n  // scroll to the bottom\n  @dropTask\n  async maybeNudge(appendedMessage: HTMLElement) {\n    await timeout(SCROLL_DELAY);\n\n    if (this.shouldScroll(appendedMessage)) {\n      appendedMessage.scrollIntoView({ behavior: 'smooth' });\n    }\n  }\n\n  @restartableTask\n  async scrollToBottom() {\n    const element = document.querySelector('.messages');\n\n    if (element) {\n      await element.scrollTo({ left: 0, top: element.scrollHeight, behavior: 'smooth' });\n    }\n  }\n\n  private shouldScroll(appendedMessage: HTMLElement) {\n    const container = document.querySelector('.messages') as HTMLElement;\n\n    if (!container) return false;\n\n    if (appendedMessage) {\n      const rect = appendedMessage.getBoundingClientRect();\n      const containerRect = container.getBoundingClientRect();\n\n      let fuzzyness = 50; // px\n      // Check if there the message is within height's delta of the bottom\n      // of the container.\n      const isJustOffScreen =\n        rect.top >= containerRect.bottom - rect.height - fuzzyness &&\n        containerRect.bottom + rect.height + fuzzyness > rect.bottom;\n\n      return isJustOffScreen;\n    }\n\n    // Can something that doesn't exist be visible?\n    return false;\n  }\n}\n\nfunction isLastVisible(id: string) {\n  const container = document.querySelector('.message-list') as HTMLElement;\n\n  if (!container) return false;\n\n  const messages = container.querySelectorAll('.message')!;\n\n  if (!messages) return false;\n\n  const lastMessage = document.getElementById(id) || document.querySelector(`[data-id=\"${id}\"]`);\n\n  if (lastMessage) {\n    return isBottomOfMessageVisible(lastMessage, container);\n  }\n\n  // nothing to show. last is like... square root of -1... or something.\n  // if there are indeed messages, then the last one might be occluded\n  return messages.length === 0;\n}\n\nfunction isBottomOfMessageVisible(element: HTMLElement, container: HTMLElement): boolean {\n  const rect = element.getBoundingClientRect();\n  const containerRect = container.getBoundingClientRect();\n\n  const isVisible = rect.bottom <= containerRect.bottom;\n\n  return isVisible;\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    'chat-scroller': ChatScroller;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/connection/ephemeral/login/receive-data.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport RSVP from 'rsvp';\n\nimport { EphemeralConnection } from '@emberclear/networking';\nimport { UnknownMessageError } from '@emberclear/networking/errors';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\nimport type SettingsService from 'emberclear/services/settings';\nimport type Toast from 'emberclear/services/toast';\n\ntype UpdateStatus = (status: boolean) => void;\n\nexport class ReceiveDataConnection extends EphemeralConnection {\n  @service declare router: RouterService;\n  @service declare settings: SettingsService;\n  @service declare toast: Toast;\n  @service declare intl: Intl;\n\n  waitForSYN = RSVP.defer<PublicIdentity>();\n  waitForData = RSVP.defer<LoginData>();\n\n  @tracked taskMsg = '';\n  @tracked senderName = '';\n\n  @action\n  wait(updateTransferStatus: UpdateStatus) {\n    this.waitForSYN = RSVP.defer();\n    this.waitForData = RSVP.defer();\n\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    taskFor(this._wait).perform(updateTransferStatus);\n  }\n\n  @dropTask\n  async _wait(updateTransferStatus: UpdateStatus) {\n    let { id: senderPublicKey, name } = await this.waitForSYN.promise;\n\n    this.senderName = name;\n    this.taskMsg = this.intl.t('ui.login.verify.received');\n\n    updateTransferStatus(true);\n\n    this.setTarget(senderPublicKey);\n\n    await this.send({ type: 'ACK' });\n    this.taskMsg = this.intl.t('ui.login.verify.waitingOnApproval');\n\n    let { hash, data } = await this.waitForData.promise;\n\n    this.taskMsg = this.intl.t('ui.login.verify.receivedData');\n\n    let dataHash = '111'; // TODO implement this\n\n    await this.send({ type: 'HASH', data: dataHash });\n\n    if (hash === dataHash) {\n      this.taskMsg = this.intl.t('ui.login.verify.importing');\n\n      await this.settings.importData(data);\n\n      this.taskMsg = '';\n      await this.toast.success(this.intl.t('ui.login.success'));\n      await this.router.transitionTo('chat');\n\n      updateTransferStatus(false);\n\n      return;\n    }\n\n    this.taskMsg = this.intl.t('ui.login.verify.failed');\n    updateTransferStatus(false);\n  }\n\n  @action\n  async onData(data: EncryptedMessage) {\n    let decrypted: LoginMessage = await this.crypto.decryptFromSocket(data);\n\n    switch (decrypted.type) {\n      case 'SYN':\n        return this.waitForSYN.resolve(decrypted.data);\n      case 'DATA':\n        return this.waitForData.resolve(decrypted);\n      default:\n        throw new UnknownMessageError();\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/connection/ephemeral/login/send-data.ts",
    "content": "import { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport RSVP from 'rsvp';\n\nimport { EphemeralConnection } from '@emberclear/networking';\nimport {\n  CurrentUserMustHaveAName,\n  DataTransferFailed,\n  UnknownMessageError,\n} from '@emberclear/networking/errors';\n\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type SettingsService from 'emberclear/services/settings';\n\nexport class SendDataConnection extends EphemeralConnection {\n  @service declare settings: SettingsService;\n  @service declare currentUser: CurrentUserService;\n\n  waitForACK = RSVP.defer<void>();\n  waitForHash = RSVP.defer<string>();\n\n  @action\n  establishContact() {\n    this.waitForACK = RSVP.defer();\n\n    return taskFor(this._establishContact).perform();\n  }\n\n  @action\n  transferToDevice() {\n    this.waitForHash = RSVP.defer();\n\n    return taskFor(this._transferToDevice).perform();\n  }\n\n  @dropTask\n  async _establishContact() {\n    if (!this.currentUser.name) {\n      throw new CurrentUserMustHaveAName();\n    }\n\n    await this.send({ type: 'SYN', data: { id: this.hexId, name: this.currentUser.name } });\n    await this.waitForACK.promise;\n  }\n\n  @dropTask\n  async _transferToDevice() {\n    let data = await this.settings.buildSettings();\n    let hash = '111'; // TODO: implement this\n\n    await this.send({ type: 'DATA', hash, data } as LoginData);\n\n    let confirmedHash = await this.waitForHash.promise;\n\n    if (hash !== confirmedHash) {\n      throw new DataTransferFailed();\n    }\n  }\n\n  @action\n  async onData(data: EncryptedMessage) {\n    let decrypted: LoginMessage = await this.crypto.decryptFromSocket(data);\n\n    switch (decrypted.type) {\n      case 'ACK':\n        return this.waitForACK.resolve();\n      case 'HASH':\n        return this.waitForHash.resolve(decrypted.data);\n      default:\n        throw new UnknownMessageError();\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/current-chat.ts",
    "content": "import Service, { inject as service } from '@ember/service';\n\nimport { CHANNEL_REGEX, PRIVATE_CHAT_REGEX } from 'emberclear/utils/route-matchers';\n\nimport { currentUserId } from '@emberclear/local-account';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class CurrentChatService extends Service {\n  @service declare store: StoreService;\n  @service declare router: RouterService;\n  @service declare currentUser: CurrentUserService;\n\n  get name() {\n    if (this.recordId === currentUserId) {\n      return this.currentUser.name;\n    }\n\n    if (this.recordId) {\n      if (this.channelId) {\n        return `#${this.record?.name}`;\n      }\n\n      return `${this.record?.name}`;\n    }\n\n    return '';\n  }\n\n  get record() {\n    if (!this.recordId || !this.recordType) return;\n\n    const record = this.store.peekRecord(this.recordType, this.recordId);\n\n    return record;\n  }\n\n  // private\n\n  get recordId() {\n    return this.contactId || this.channelId;\n  }\n\n  get recordType() {\n    return this.contactId ? 'contact' : 'channel';\n  }\n\n  get contactId() {\n    return contactIdFrom(this.router.currentURL);\n  }\n\n  get channelId() {\n    return channelIdFrom(this.router.currentURL);\n  }\n}\n\nfunction contactIdFrom(url: string) {\n  const privateMatches = PRIVATE_CHAT_REGEX.exec(url);\n  const encodedId = privateMatches?.[1];\n\n  if (encodedId) {\n    const id = decodeURI(encodedId);\n\n    return id;\n  }\n}\n\nfunction channelIdFrom(url: string) {\n  const channelMatches = CHANNEL_REGEX.exec(url);\n  const encodedId = channelMatches?.[1];\n\n  if (encodedId) {\n    const id = decodeURI(encodedId);\n\n    return id;\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    currentChat: CurrentChatService;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/locale.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nimport type IntlService from 'ember-intl/services/intl';\n\nconst DEFAULT_LOCALE = 'en-us';\n\nexport default class LocaleService extends Service {\n  @service declare intl: IntlService;\n\n  @inLocalStorage currentLocale = DEFAULT_LOCALE;\n\n  async setLocale(locale: string = DEFAULT_LOCALE) {\n    this.currentLocale = locale || DEFAULT_LOCALE;\n\n    // uncomment for asyncily loaded translations\n    // const request = await fetch(`/translations/${locale}.json`);\n    // const translations = await request.json();\n\n    // this.intl.addTranslations(locale, translations);\n    this.intl.setLocale([locale]);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/modals.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\n\ninterface IModalArgs {\n  name: string;\n  isActive: boolean;\n}\n\nexport class ModalState {\n  @tracked declare name: string;\n  @tracked declare isActive: boolean;\n\n  constructor(args: IModalArgs) {\n    this.name = args.name;\n    this.isActive = args.isActive;\n  }\n\n  open() {\n    this.isActive = true;\n  }\n\n  close() {\n    this.isActive = false;\n  }\n\n  toggle() {\n    this.isActive = !this.isActive;\n  }\n}\n\nexport default class Modals extends Service {\n  @tracked modals: ModalState[] = [];\n\n  toggle(name: string) {\n    this.find(name).toggle();\n  }\n\n  close(name: string) {\n    this.find(name).close();\n  }\n\n  open(name: string) {\n    this.find(name).open();\n  }\n\n  isVisible(name: string) {\n    return this.find(name).isActive;\n  }\n\n  find(name: string) {\n    let modal = this.modals.find((m) => m.name === name);\n\n    if (!modal) {\n      let newModal = new ModalState({ name, isActive: false });\n\n      this.modals.push(newModal);\n\n      return newModal;\n    }\n\n    return modal;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/notifications.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type Toast from 'emberclear/services/toast';\nimport type WindowService from 'emberclear/services/window';\n\nexport default class Notifications extends Service {\n  @service declare toast: Toast;\n  @service declare intl: Intl;\n  @service declare currentUser: CurrentUserService;\n  @service declare router: RouterService;\n  @service declare window: WindowService;\n\n  @tracked askToEnableNotifications = true;\n  @tracked isHiddenUntilBrowserRefresh = false;\n\n  @inLocalStorage isNeverGoingToAskAgain = false;\n\n  get showInAppPrompt() {\n    const promptShouldNotBeShown =\n      !this.currentUser.isLoggedIn ||\n      this.isOnRouteThatDoesNotShowNotifications ||\n      !this.isBrowserCapableOfNotifications ||\n      this.isPermissionGranted ||\n      this.isPermissionDenied ||\n      this.isNeverGoingToAskAgain ||\n      this.isHiddenUntilBrowserRefresh;\n\n    if (promptShouldNotBeShown) return false;\n\n    return this.askToEnableNotifications;\n  }\n\n  get isOnRouteThatDoesNotShowNotifications() {\n    const { currentRouteName } = this.router;\n\n    if (!currentRouteName) return false;\n\n    return currentRouteName.match(/setup/) || currentRouteName.match(/logout/);\n  }\n\n  info(msg: string, title = '', options = {}) {\n    return this.display(msg, title, options);\n  }\n\n  async display(msg: string, title: string, options = {}) {\n    if (this.isPermissionGranted) {\n      this.showNotification(msg, title, options);\n\n      return;\n    }\n\n    // Permission to display desktop notifications has not yet been granted.\n    // ask the user if they would like to enable those.\n    this.askToEnableNotifications = true;\n\n    return this.toast.info(msg, title, options);\n  }\n\n  get isPermissionGranted() {\n    if (this.isBrowserCapableOfNotifications) {\n      return this.window.Notification.permission === 'granted';\n    }\n\n    return false;\n  }\n\n  get isPermissionDenied() {\n    return this.window.Notification.permission === 'denied';\n  }\n\n  askPermission() {\n    return new Promise((resolve, reject) => {\n      if (!this.isBrowserCapableOfNotifications) return reject();\n      if (this.isPermissionDenied) return reject();\n\n      return this.window.Notification.requestPermission((permission) => {\n        this.askToEnableNotifications = false;\n\n        return resolve(permission);\n      });\n    });\n  }\n\n  get isBrowserCapableOfNotifications() {\n    return 'Notification' in window;\n  }\n\n  showNotification(msg: string, title = '', options = {}) {\n    const defaultTitle = this.intl.t('ui.notifications.title');\n    const notificationOptions = {\n      body: msg,\n      // tag needed to prevent duplicates\n      tag: msg,\n      // icon: ''\n      ...options,\n    };\n\n    return new Notification(title || defaultTitle, notificationOptions);\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    notifications: Notifications;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/prism-manager.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unsafe-assignment */\n/* eslint-disable @typescript-eslint/no-unsafe-return */\n/* eslint-disable @typescript-eslint/restrict-template-expressions */\n/* eslint-disable @typescript-eslint/no-unsafe-call */\n/* eslint-disable @typescript-eslint/no-unsafe-member-access */\nimport Service from '@ember/service';\n\n// NOTE: using task from ember-concurrency-decorators doesn't\n//       allow for types to be recognized.\n//       Continue using task from ember-concurrency instead.\n//\n//       This file will be where we periodically check if TypeScript\n//       does what we want.\nimport { task } from 'ember-concurrency-decorators';\n\nconst aliases: Dict = {\n  ts: 'typescript',\n  rb: 'ruby',\n  hbs: 'handlebars',\n  js: 'javascript',\n};\n\nexport default class PrismManager extends Service {\n  areEssentialsPresent = false;\n  alreadyAdded: string[] = [];\n  prismLoader: any = undefined;\n\n  @task({ maxConcurrency: 1, enqueue: true })\n  async addLanguage(language: string, element?: HTMLElement) {\n    await (this.addEssentials as any).perform();\n    await this.ensureLanguage(language);\n\n    if (element) {\n      Prism.highlightAllUnder(element);\n    }\n  }\n\n  async ensureLanguage(language: string) {\n    let name = aliases[language] || language;\n    let hasAbbr = name !== language;\n    let abbr = hasAbbr ? language : undefined;\n\n    if (this.alreadyAdded.includes(language)) {\n      return;\n    }\n\n    console.groupCollapsed(`PrismManager: loading: ${name}`);\n    await this.prismLoader.load(Prism, name);\n    console.debug(`Success: ${Boolean(Prism.languages[name])}`);\n    console.groupEnd();\n\n    if (abbr) {\n      // eslint-disable-next-line require-atomic-updates\n      Prism.languages[abbr] = Prism.languages[name];\n    }\n\n    this.alreadyAdded.push(language);\n  }\n\n  @task({ drop: true })\n  async addEssentials() {\n    if (this.areEssentialsPresent) return;\n\n    let prismLoader = await addScripts();\n\n    addStyles();\n\n    this.prismLoader = prismLoader;\n    this.areEssentialsPresent = true;\n  }\n}\n\nasync function addScripts() {\n  await import('prismjs').then((Prism) => ((window as any).Prism = Prism));\n\n  let modules = await Promise.all([\n    import('prismjs/plugins/line-numbers/prism-line-numbers.min.js'),\n    import('prismjs/plugins/show-language/prism-show-language.min.js'),\n    import('prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.min.js'),\n    import('prismjs/plugins/autolinker/prism-autolinker.min.js'),\n    import('prismjs-components-loader'),\n    import('prismjs-components-loader/dist/all-components'),\n  ]);\n\n  let [, , , , prismLoader, allComponents] = modules;\n\n  const PrismLoader = prismLoader.default;\n\n  let loader = new PrismLoader(allComponents.default);\n\n  return loader;\n}\n\nfunction addStyles() {\n  addStyle('/prismjs/themes/prism.css');\n  // addStyle('/prismjs/themes/prism-twilight.css');\n  addStyle('/prismjs/plugins/line-numbers/prism-line-numbers.css');\n  addStyle('/prismjs/plugins/autolinker/prism-autolinker.css');\n}\n\nfunction addStyle(path: string) {\n  let head = document.querySelector('head')!;\n  let link = document.createElement('link');\n\n  link.setAttribute('href', path);\n  link.setAttribute('rel', 'stylesheet');\n\n  head.appendChild(link);\n}\n\nexport const languages = [\n  'actionscript',\n  'arduino',\n  'basic',\n  'c',\n  'clojure',\n  'coffeescript',\n  'cpp',\n  'crystal',\n  'csharp',\n  'css',\n  'd',\n  'dart',\n  'django',\n  'docker',\n  'elixir',\n  'elm',\n  'erb',\n  'erlang',\n  'flow',\n  'fsharp',\n  'git',\n  'go',\n  'graphql',\n  'haml',\n  'haskell',\n  'ini',\n  'java',\n  'javascript',\n  'json',\n  'jsx',\n  'kotlin',\n  'latex',\n  'less',\n  'lisp',\n  'lua',\n  'makefile',\n  'markdown',\n  'markup',\n  'matlab',\n  'objectivec',\n  'perl',\n  'php',\n  'plsql',\n  'powershell',\n  'processing',\n  'protobuf',\n  'pug',\n  'python',\n  'r',\n  'ruby',\n  'rust',\n  'sass',\n  'scala',\n  'scheme',\n  'scss',\n  'sql',\n  'swift',\n  'tsx',\n  'typescript',\n  'vbnet',\n  'verilog',\n  'vim',\n  'visual-basic',\n  'wasm',\n  'wiki',\n  'yaml',\n];\n"
  },
  {
    "path": "client/web/emberclear/app/services/qr-manager.ts",
    "content": "import Service from '@ember/service';\n\nimport { SendDataConnection } from 'emberclear/services/connection/ephemeral/login/send-data';\n\n/**\n * NOTE: This should be a local service to the /qr route.\n *       How do we re-register / test with local services?\n */\nexport default class QRManager extends Service {\n  login = {\n    // eslint-disable-next-line @typescript-eslint/ban-types\n    async setupConnection(context: object, publicKeyAsHex: string) {\n      let connection = await SendDataConnection.build(context, { publicKeyAsHex });\n\n      await connection.establishContact();\n\n      return connection;\n    },\n  };\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'qr-manager': QRManager;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/redirect-manager.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nimport type RouterService from '@ember/routing/router-service';\n\nexport default class RedirectManager extends Service {\n  @service declare router: RouterService;\n\n  @inLocalStorage attemptedRoute?: string;\n\n  get hasPendingRedirect() {\n    return Boolean(this.attemptedRoute);\n  }\n\n  persistURL(path: string) {\n    this.attemptedRoute = path;\n  }\n\n  async evaluate() {\n    if (this.hasPendingRedirect) {\n      let next = this.attemptedRoute;\n\n      this.attemptedRoute = undefined;\n\n      if (next) {\n        await this.router.transitionTo(next);\n      }\n\n      return true;\n    }\n\n    return false;\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'redirect-manager': RedirectManager;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/session.ts",
    "content": "import { action } from '@ember/object';\nimport Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport localforage from 'localforage';\n\nimport type WindowService from './window';\nimport type RouterService from '@ember/routing/router-service';\nimport type StoreService from '@ember-data/store';\nimport type { CurrentUserService } from '@emberclear/local-account';\nimport type { ConnectionService } from '@emberclear/networking';\n\nconst FLAG_KEY = '_features';\n\nexport default class SessionService extends Service {\n  @service declare currentUser: CurrentUserService;\n  @service declare connection: ConnectionService;\n  @service declare router: RouterService;\n  @service declare store: StoreService;\n  @service declare window: WindowService;\n\n  @action\n  async logout() {\n    this.connection.disconnect();\n\n    // clears the store after a refresh\n    await localforage.clear();\n    localStorage.clear();\n\n    // lazy way to reset all the services\n    this.window.location.href = '/';\n  }\n\n  @action\n  hasFeatureFlag(flag: string) {\n    let searchParams = new URLSearchParams(this.router.currentURL.split('?')[1]);\n    let ffs = searchParams.get(FLAG_KEY) || '';\n    let hasFF = ffs.split(',').includes(flag);\n\n    return hasFF;\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    session: SessionService;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/settings.ts",
    "content": "import Service from '@ember/service';\nimport { inject as service } from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\nimport localforage from 'localforage';\n\nimport { fromHex, objectToDataURL, toHex } from '@emberclear/encoding/string';\n\nimport type { Serializable } from '@emberclear/crypto/types';\nimport type { Channel, Contact } from '@emberclear/local-account';\nimport type ChannelManager from '@emberclear/local-account/services/channel-manager';\nimport type ContactManager from '@emberclear/local-account/services/contact-manager';\nimport type CurrentUserService from '@emberclear/local-account/services/current-user';\n\ninterface IContactJson {\n  name?: string;\n  publicKey?: string /* hex */;\n}\n\ninterface IChannelJson {\n  id: string;\n  name: string;\n}\n\ninterface ISettingsJson {\n  version: number;\n  name: string;\n  privateKey: string; // hex\n  privateSigningKey?: string; // hex\n  contacts: IContactJson[];\n  channels: IChannelJson[];\n}\n\nexport const THEMES = {\n  midnight: 'midnight',\n  default: 'default',\n};\n\nconst availableThemes = {\n  [THEMES.midnight]: 'midnight-theme',\n  [THEMES.default]: 'default-theme',\n};\n\nexport default class Settings extends Service {\n  @service declare currentUser: CurrentUserService;\n  @service declare contactManager: ContactManager;\n  @service declare channelManager: ChannelManager;\n\n  @inLocalStorage hideOfflineContacts = false;\n  @inLocalStorage theme = THEMES.default;\n\n  selectTheme(themeKey: string) {\n    this.theme = themeKey;\n    this.applyTheme();\n  }\n\n  applyTheme() {\n    let themeClass = availableThemes[this.theme];\n    let classList = document.body.classList;\n\n    Object.values(availableThemes).forEach((currentClass) => {\n      classList.remove(currentClass);\n    });\n\n    classList.add(themeClass);\n  }\n\n  async import(settings: string) {\n    const json = JSON.parse(settings) as ISettingsJson;\n\n    await this.importData(json);\n  }\n\n  async importData(json: ISettingsJson) {\n    const {\n      name,\n      privateKey: privateKeyHex,\n      privateSigningKey: privateSigningKeyHex,\n      contacts,\n      channels,\n    } = json;\n\n    // start by clearing everything!\n    await localforage.clear();\n    const privateKey = fromHex(privateKeyHex);\n\n    let privateSigningKey;\n\n    if (privateSigningKeyHex) {\n      privateSigningKey = fromHex(privateSigningKeyHex);\n    }\n\n    await this.currentUser.importFromKey(name, privateKey, privateSigningKey);\n\n    for await (let channel of channels) {\n      await this.channelManager.findOrCreate(channel.id, channel.name);\n    }\n\n    await this.contactManager.import(contacts);\n  }\n\n  async buildData(): Promise<string | undefined> {\n    const toDownload = await this.buildSettings();\n\n    return objectToDataURL(toDownload);\n  }\n\n  async buildSettings(): Promise<Serializable> {\n    const { name, privateKey, privateSigningKey } = this.currentUser;\n\n    if (!privateKey) {\n      throw new Error('User does not have a private key');\n    }\n\n    if (!privateSigningKey) {\n      throw new Error('User does not have a signing key');\n    }\n\n    const contacts = await this.contactManager.allContacts();\n    const channels = await this.channelManager.allChannels();\n\n    const toDownload = {\n      version: 1,\n      name: name || '',\n      privateKey: toHex(privateKey),\n      privateSigningKey: toHex(privateSigningKey),\n      contacts: contacts.toArray().map((c: Contact) => ({\n        name: c.name,\n        publicKey: c.publicKey && toHex(c.publicKey),\n      })),\n      channels: channels.toArray().map((c: Channel) => ({\n        // TODO: add members list\n        id: c.id,\n        name: c.name,\n      })),\n    };\n\n    return toDownload;\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    settings: Settings;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/sidebar.ts",
    "content": "import { A } from '@ember/array';\nimport { action } from '@ember/object';\nimport Service, { inject as service } from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nimport type { CurrentUserService } from '@emberclear/local-account';\n\nexport default class Sidebar extends Service {\n  @service declare currentUser: CurrentUserService;\n\n  unreadAbove = A();\n  unreadBelow = A();\n\n  declare unreadObserver?: IntersectionObserver;\n\n  get hasUnreadAbove() {\n    return this.unreadAbove.length > 0;\n  }\n\n  get hasUnreadBelow() {\n    return this.unreadBelow.length > 0;\n  }\n\n  @inLocalStorage isShown = false;\n\n  @action\n  show() {\n    this.isShown = true;\n  }\n\n  @action\n  hide() {\n    this.isShown = false;\n  }\n\n  @action\n  toggle() {\n    return this.isShown ? this.hide() : this.show();\n  }\n\n  clearUnreadBelow() {\n    this.unreadBelow.clear();\n  }\n\n  clearUnreadAbove() {\n    this.unreadAbove.clear();\n  }\n\n  ensureUnreadIntersectionObserverExists() {\n    if (this.unreadObserver) return;\n\n    this.unreadObserver = this.createUnreadObserver();\n  }\n\n  private createUnreadObserver(): IntersectionObserver {\n    const callback = this.handleIntersectionEvent.bind(this);\n    const io = new IntersectionObserver(callback, {\n      root: document.querySelector('.sidebar-wrapper aside.menu'),\n      rootMargin: '-50px 0px -50px 0px',\n    });\n\n    return io;\n  }\n\n  private handleIntersectionEvent(entries: IntersectionObserverEntry[]) {\n    entries.forEach((entry) => {\n      const target = entry.target;\n      const id = target.id;\n      const { boundingClientRect, rootBounds, isIntersecting } = entry;\n      const isBelow = rootBounds ? boundingClientRect.top > rootBounds.bottom : false;\n      const isAbove = rootBounds ? boundingClientRect.top < rootBounds.top : false;\n\n      if (isIntersecting) {\n        this.unreadAbove.removeObject(id);\n        this.unreadBelow.removeObject(id);\n      }\n\n      if (isBelow) {\n        this.unreadBelow.addObject(id);\n      }\n\n      if (isAbove) {\n        this.unreadAbove.addObject(id);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/toast.ts",
    "content": "import Ember from 'ember';\nimport Service from '@ember/service';\nimport { waitForPromise } from '@ember/test-waiters';\nimport { isPresent } from '@ember/utils';\n\nimport Toastify from 'toastify-js';\n\nexport default class Toast extends Service {\n  info(msg: string, title = '', options = {}) {\n    return this.createToast('alert-info', msg, title, options);\n  }\n\n  success(msg: string, title = '', options = {}) {\n    return this.createToast('alert-success', msg, title, options);\n  }\n\n  warning(msg: string, title = '', options = {}) {\n    return this.createToast('alert-warning', msg, title, options);\n  }\n\n  error(msg: string, title = '', options = {}) {\n    return this.createToast('alert-danger', msg, title, options);\n  }\n\n  createToast(status: string, msg: string, title: string, options: any) {\n    let message = isPresent(title) ? `${title}: ${msg}` : msg;\n\n    return waitForPromise(createToast(status, message, options));\n  }\n}\n\nfunction createToast(status: string, message: string, options: any) {\n  let timeout = Ember.testing ? 300 : 4000;\n\n  let toast = Toastify({\n    text: message,\n    duration: timeout,\n    // only relevant on\n    newWindow: true,\n    close: true,\n    // `top` or `bottom`\n    gravity: 'top',\n    // `left`, `center` or `right`\n    position: 'right',\n    className: `toast-alert alert ${status}`,\n    // Prevents dismissing of toast on hover\n    stopOnFocus: true,\n    // overrides and such\n    ...options,\n    // destination / url\n  });\n\n  toast.showToast();\n\n  // this is a hack, and this should really be an\n  // onRemove callback\n  return new Promise((resolve) => {\n    setTimeout(resolve, timeout + 5);\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/app/services/window.ts",
    "content": "import { tracked } from '@glimmer/tracking';\nimport Service from '@ember/service';\n\nexport default class WindowService extends Service {\n  @tracked declare deferredInstallPrompt?: FakeBeforeInstallPromptEvent;\n  @tracked isInstalled = false;\n  @tracked hasDeferredInstall = false;\n\n  cleanup: any[] = [];\n\n  // aliases, to allow for easier / more predictable stubbing\n  Notification = window.Notification;\n\n  constructor(...args: any[]) {\n    super(...args);\n\n    this.cleanup.push(this.checkForDeferredInstall());\n  }\n\n  get location() {\n    return window.location;\n  }\n\n  willDestroy() {\n    this.cleanup.forEach((method) => method());\n  }\n\n  checkForDeferredInstall() {\n    const interval = setInterval(() => {\n      this.deferredInstallPrompt = window.deferredInstallPrompt;\n\n      if (this.deferredInstallPrompt) {\n        clearInterval(interval);\n        this.hasDeferredInstall = true;\n\n        return this.deferredInstallPrompt.userChoice.then((choice) => {\n          this.isInstalled = choice.outcome === 'accepted';\n        });\n      }\n    }, 250);\n\n    return () => clearInterval(interval);\n  }\n\n  get canInstall() {\n    return this.hasDeferredInstall && !this.isInstalled;\n  }\n\n  async promptInstall() {\n    if (!this.deferredInstallPrompt) return;\n\n    await this.deferredInstallPrompt.prompt();\n\n    await this.evaluateInstallPrompt();\n  }\n\n  async evaluateInstallPrompt() {\n    if (!this.deferredInstallPrompt) return;\n\n    const choice = await this.deferredInstallPrompt.userChoice;\n\n    if (choice.outcome === 'accepted') {\n      this.isInstalled = true;\n    } else {\n      this.deferredInstallPrompt = undefined;\n    }\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    window: WindowService;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/app.css",
    "content": "@import '@emberclear/ui';\n@import './components/backdrop';\n@import './components/card';\n@import './components/chat-entry';\n@import './components/connection-status';\n@import './components/contacts';\n@import './components/dismissable-warning';\n@import './components/ellipses-loader';\n@import './components/hover-tip';\n@import './components/key';\n@import './components/logout';\n@import './components/message';\n@import './components/messages';\n@import './components/metadata-preview';\n@import './components/modal';\n@import './components/notification-prompt';\n@import './components/q-r-code';\n@import './components/q-r-scanner';\n@import './components/search';\n@import './components/settings-nav';\n@import './components/sidebar-contact';\n@import './components/sidebar-nav';\n@import './components/sidebar';\n@import './components/snippet';\n@import './components/toastify-overrides';\n@import './components/top-nav';\n@import './components/unread-management';\n@import './utility/transitions';\n@import './utility/height';\n\n.wrap-anywhere {\n  word-wrap: anywhere;\n}\n\n.icon-button {\n  width: 1rem;\n  height: 1rem;\n}\n\n.service-worker-update-notify {\n  position: fixed;\n  z-index: 2;\n  right: 0;\n  left: 0;\n  border-radius: 0;\n  padding: 1rem;\n\n  /*   @include mobile { */\n\n  /*     top: 0; */\n\n  /*   } */\n\n  /*   @include tablet-only { */\n\n  /*     top: 0px; */\n\n  /*   } */\n}\n\n.not-allowed {\n  cursor: not-allowed !important;\n}\n\n.shareable-url {\n  overflow-x: auto;\n  padding: 10px 5px;\n\n  & a {\n    white-space: nowrap;\n  }\n\n  &.wrap {\n    & a {\n      white-space: initial;\n      word-wrap: break-word;\n    }\n  }\n}\n\n.login-form-contents {\n  @apply --grid;\n\n  align-items: center;\n  justify-content: space-between;\n  justify-items: center;\n  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n\n  /* For animating the transition to data Transfer */\n  position: relative;\n\n  & .left {\n    & p {\n      @apply --column-grid;\n\n      align-items: center;\n      justify-content: start;\n    }\n  }\n\n  & .right {\n    background: var(--body-bg-color);\n\n    /* For animating the transition to data Transfer */\n    box-shadow: 0 0 0 0;\n    padding: 0;\n  }\n}\n\n@media (--breakpoint-sm-up) {\n  /* sm screens and above */\n  .login-form-contents {\n    grid-template-columns: 55% 35%;\n  }\n\n  [data-transfer-started] {\n    & h1.title,\n    & .login-form-contents .left,\n    & .login-form-contents .right {\n      opacity: 100%;\n\n      /* transition: all 0.4s cubic-bezier(0.6, 0, 0.735, 0.045); */\n      transition: all 0.4s ease-in-out;\n    }\n  }\n\n  div[data-transfer-started='true'] {\n    & h1.title,\n    & .login-form-contents .left {\n      opacity: 30%;\n    }\n\n    & .right {\n      width: max-content;\n      box-shadow: var(--shadow-lg);\n      padding: 2rem;\n      border-radius: var(--component-border-radius);\n      transform: scale(1.5) translateX(-25%);\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/backdrop.css",
    "content": ".full-overlay {\n  display: none;\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  animation: fadein 0.5s;\n  background-color: var(--modal-overlay-color);\n  opacity: 0;\n\n  &.active {\n    display: block;\n    opacity: 1;\n  }\n\n  &.invisible {\n    background-color: transparent;\n    display: block;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/card.css",
    "content": ".card {\n  min-width: 300px;\n  border-radius: var(--component-border-radius);\n\n  @apply --transition-all;\n\n  & header {\n    padding: var(--spacing-sm) var(--spacing-md);\n    color: var(--state-danger);\n  }\n\n  & p {\n    margin: 0;\n    padding: var(--spacing-sm) var(--spacing-md);\n    font-style: italic;\n  }\n\n  & footer {\n    padding: var(--spacing-md);\n    padding-top: 0;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/chat-entry.css",
    "content": ".chat-entry-container {\n  @apply --grid;\n  @apply --grid-stretch;\n\n  background: var(--chat-entry-bg-color);\n  border-radius: var(--component-border-radius);\n  padding: var(--grid-gap);\n  box-shadow: inset 0 2px 2px 0 rgba(0, 0, 0, 0.2);\n  grid-template-columns: auto 1fr auto;\n\n  & .dropdown-trigger {\n    &::after {\n      display: none;\n    }\n  }\n}\n\n.chat-entry {\n  @apply --grid;\n  @apply --grid-stretch;\n\n  grid-template-columns: 1fr auto;\n\n  & .input-field {\n    margin: 0;\n  }\n\n  & textarea {\n    border: none;\n    background: transparent;\n    box-shadow: none;\n    resize: none;\n    line-height: var(--line-height);\n    box-sizing: border-box;\n\n    /* height is managed by ember-autoresize-modifier */\n    max-height: 12rem; /* 10x line-height */\n\n    /* overflow-x: hidden is so that ember-autoresize doesn't incorrectly set the height to anything other than a multiple of the line-height. */\n    overflow-x: hidden;\n\n    /* required for line breaks to render correctly during authoring */\n    white-space: pre-wrap;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/connection-status.css",
    "content": ".connection-status {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  border-radius: 0;\n  padding: var(--grid-gap);\n  box-shadow: var(--shadow);\n  z-index: 10;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/contacts.css",
    "content": "header.contacts {\n  @apply --column-grid;\n  @apply --grid-space-between;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/dismissable-warning.css",
    "content": ".dismissable-warning {\n  align-self: start;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/ellipses-loader.css",
    "content": ".ellipsis-loader span {\n  opacity: 0;\n  animation: ellipsis-dot 1s infinite;\n}\n\n.ellipsis-loader span:nth-child(1) {\n  animation-delay: 0s;\n}\n\n.ellipsis-loader span:nth-child(2) {\n  animation-delay: 0.1s;\n}\n\n.ellipsis-loader span:nth-child(3) {\n  animation-delay: 0.2s;\n}\n\n@-webkit-keyframes ellipsis-dot {\n  0% { opacity: 0; }\n  50% { opacity: 1; }\n  100% { opacity: 0; }\n}\n\n@keyframes ellipsis-dot {\n  0% { opacity: 0; }\n  50% { opacity: 1; }\n  100% { opacity: 0; }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/embedded-resource.css",
    "content": ".embedded-resource {\n  @apply --grid;\n\n  margin: 5px;\n\n  & img,\n  & iframe {\n    max-width: 480px;\n    max-height: 270px;\n  }\n\n  & .card-content {\n    & > div {\n      @apply --column-grid;\n\n      justify-content: start;\n\n      & > span img {\n        max-height: 50px;\n        max-width: 50px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/hover-tip.css",
    "content": ".hover-tip {\n  opacity: 0;\n  color: var(--body-color);\n  background: var(--body-bg-color);\n  z-index: 10;\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  padding: 2px 5px;\n  border-radius: var(--component-border-radius);\n  line-height: 1.5rem;\n  transition-property: all;\n  transition-duration: 0.5s;\n  transition-timing-function: cubic-bezier(0.86, 0, 0.07, 1);\n  box-shadow: 0 3px 6px 0 rgba(10, 10, 10, 0.2);\n\n  &.overlay {\n    bottom: 0;\n    right: 0;\n    left: 0;\n  }\n}\n\n/* custom animations... */\n.has-status-tip.is-active,\n.has-hover-tip:hover {\n  z-index: 1;\n\n  & .floats-up {\n    bottom: 50px;\n  }\n}\n\n.has-hover-tip {\n  position: relative;\n\n  &:hover {\n    .hover-tip {\n      opacity: 1;\n      bottom: 20px;\n    }\n  }\n\n  & .hover-tip {\n    &:hover {\n      opacity: 1;\n    }\n\n    &.w-left-200 {\n      width: 200px;\n      left: -200px;\n    }\n  }\n}\n\n.has-status-tip {\n  position: relative;\n\n  &.is-active {\n    & .hover-tip {\n      opacity: 1;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/key.css",
    "content": ".key {\n  display: inline-block;\n  font-size: 0.6rem;\n  line-height: 0.8rem;\n  height: 1.1rem;\n  font-style: normal;\n  text-transform: uppercase;\n  font-weight: 600;\n  color: var(--key-color);\n  box-shadow: inset 0 -3px 0 var(--key-shadow-color);\n  border: 1px solid var(--key-border-color);\n  background-color: var(--key-bg-color);\n  padding-left: 0.2rem;\n  padding-right: 0.2rem;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/logout.css",
    "content": ".logout-warning {\n  & > div {\n    @apply --grid;\n\n    grid-gap: calc(var(--grid-gap) * 2);\n\n    & h2 > a {\n      color: var(--color-light);\n\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/message.css",
    "content": ".message {\n  @apply --grid;\n  @apply --grid-stretch;\n\n  position: relative;\n  background-color: transparent;\n  border-radius: var(--component-border-radius);\n  padding: var(--grid-gap);\n  margin: calc(var(--grid-gap) / 2) 0;\n\n  &:hover {\n    background: var(--chat-message-hover-color);\n  }\n\n  &.unread {\n    background-color: var(--chat-message-unread-bg-color);\n  }\n\n  &:not(:last-child) {\n    margin-bottom: 0;\n  }\n\n  &[data-direction='outgoing'] {\n    & .message-header {\n      & .from {\n        color: var(--chat-message-sender-name-outgoing);\n\n        & a {\n          color: var(--chat-message-sender-name-outgoing);\n        }\n      }\n    }\n  }\n\n  &[data-direction='incoming'] {\n    & .message-header {\n      & .from {\n        color: var(--chat-message-sender-name-incoming);\n\n        & a {\n          color: var(--chat-message-sender-name-incoming);\n        }\n      }\n    }\n  }\n\n  &[data-direction='outgoing'] + [data-direction='outgoing'],\n  &[data-direction='incoming'] + [data-direction='incoming'] {\n    margin: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n\n    & .message-header {\n      display: none;\n    }\n  }\n\n  & p {\n    margin: 0;\n  }\n\n  & .confirmations {\n    font-size: 0.8rem;\n    position: absolute;\n    right: 0.5rem;\n    bottom: 0;\n\n    svg {\n      opacity: 0.5;\n    }\n  }\n\n  & .message-body {\n    border: none;\n    background-color: transparent;\n\n    & video.card-content,\n    & img.card-content,\n    & iframe.card-content {\n      width: 100%;\n      max-width: 200px;\n      max-height: 200px;\n    }\n  }\n\n  & .message-header {\n    @apply --column-grid;\n    @apply --grid-space-between;\n\n    background-color: transparent;\n    cursor: default;\n\n    & .from {\n      font-style: bold;\n      font-size: 0.9rem;\n      text-decoration: none;\n    }\n\n    & .sentAt {\n      font-weight: lighter;\n      font-size: 0.75rem;\n      color: var(--chat-message-sent-at);\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/messages.css",
    "content": "\n.chat-container {\n  height: calc(100vh - var(--top-nav-height));\n  display: grid;\n  grid-template-rows: 1fr auto;\n  padding: 0 var(--grid-gap);\n  padding-bottom: var(--grid-gap);\n  overflow: hidden;\n\n  & .chat-entry-container {\n    align-self: end;\n  }\n\n  & .message-list {\n    flex: 1 1 0%;\n    display: flex;\n    position: relative;\n\n    /* flex-direction: column; */\n\n    overflow: hidden;\n    padding-bottom: var(--grid-gap);\n\n    & .messages {\n      @apply --grid;\n\n      /* grid-gap of half is also applied to margin of each message */\n      grid-gap: calc(var(--grid-gap) / 2);\n      align-self: flex-end;\n      max-height: 100%;\n      flex-grow: 1;\n      overflow-y: auto;\n    }\n  }\n}\n\nbutton.new-messages {\n  @apply --no-select;\n\n  position: absolute;\n  bottom: 10px;\n  left: var(--grid-gap);\n  opacity: 1;\n  z-index: 1;\n  border-width: 0;\n  border-radius: var(--component-border-radius);\n  color: var(--chat-new-messages-notice-color);\n  background: var(--chat-new-messages-notice-bg-color);\n  box-shadow: var(--shadow);\n  margin: 0;\n  line-height: 0.75rem;\n\n  &.hidden {\n    /* TODO: use ember-animated */\n    bottom: 50px;\n    opacity: 0;\n    z-index: -1;\n  }\n\n  & span + span {\n    text-decoration: underline;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/metadata-preview.css",
    "content": ".metadata-preview {\n  @apply --column-grid;\n\n  justify-content: start;\n\n  & .thumbnail-container {\n    & img {\n      max-width: 100px;\n      max-height: 100px;\n    }\n  }\n\n  & .metadata-preview__text {\n    @apply --grid;\n\n    & a {\n      display: block;\n      font-size: 0.85rem;\n    }\n\n    & p {\n      font-size: 0.75rem;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/modal.css",
    "content": ".modal {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  z-index: 1000000;\n  display: none;\n  justify-content: center;\n  align-items: center;\n\n  &.is-active {\n    display: flex;\n  }\n\n  & .modal-content {\n    z-index: 1;\n    transition: all 2s ease 0s;\n    margin: 0 var(--grid-gap);\n    overflow: auto;\n    position: relative;\n    background: var(--modal-content-bg);\n    padding: var(--grid-gap);\n    border-radius: var(--component-border-radius);\n    box-shadow: var(--modal-shadow);\n    animation: slide-up-fade-in 0.2s;\n  }\n}\n\n@media (--breakpoint-sm-up) {\n  .modal {\n    & .modal-content {\n      width: 66%;\n    }\n  }\n}\n\n@media (--breakpoint-md-up) {\n  .modal {\n    & .modal-content {\n      width: 33%;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/notification-prompt.css",
    "content": ".chat-history-notification-prompt {\n  @apply --column-grid;\n\n  grid-template-columns: auto 1fr;\n  padding: var(--grid-gap-small);\n  position: fixed;\n  z-index: 11;\n  left: 0;\n  right: 0;\n\n  & button {\n    @apply --button-as-link;\n\n    color: currentColor;\n  }\n\n  & > section {\n    & div {\n      display: flex;\n      justify-content: space-around;\n      flex-wrap: wrap;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/q-r-code.css",
    "content": ".qr-code-large {\n  min-height: 250px;\n  min-width: 250px;\n  max-height: 300px;\n  max-width: 300px;\n\n  &.inline-block {\n    display: inline-block;\n  }\n}\n\n.qr-code {\n  min-width: 180px;\n  min-height: 180px;\n\n  & > div {\n    min-width: 160px;\n    min-height: 160px;\n    padding: 10px;\n  }\n\n  & > img {\n    min-width: 100%;\n    height: auto;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/q-r-scanner.css",
    "content": ".qr-scanner_container {\n  width: 100%;\n  height: auto;\n  overflow: hidden;\n\n  & .qr-scanner_error {\n    @apply --grid;\n\n    width: 250px;\n    height: 250px;\n    text-align: center;\n    margin: 0 auto;\n\n    & span {\n      align-self: center;\n      justify-self: center;\n    }\n  }\n\n  .loader {\n    width: 250px;\n    height: 250px;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/search.css",
    "content": "div[role='search'].modal {\n  overflow: hidden;\n  padding-top: 15vh;\n\n  input {\n    box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.2);\n  }\n\n  & .modal-content {\n    align-self: start;\n    overflow: hidden;\n    min-width: 350px;\n  }\n\n  & .results {\n    max-height: 250px;\n    min-height: 100px;\n    height: auto;\n    display: block;\n    width: 100%;\n    overflow: auto;\n\n    /* height: calc(100vh - 250px); */\n    padding-right: var(--grid-gap);\n    padding-bottom: var(--grid-gap);\n    margin-right: calc(0px - var(--grid-gap));\n    box-sizing: content-box;\n\n    & .no-results {\n      display: block;\n      font-size: var(--font-size-small);\n      font-style: italic;\n      color: var(--hint-color);\n      padding-left: var(--grid-gap);\n    }\n  }\n\n  & .section-title {\n    font-size: 0.8rem;\n    text-transform: uppercase;\n    position: sticky;\n    top: 0;\n    background: var(--modal-content-bg);\n    display: block;\n    box-shadow: 0 2px 6px -6px;\n  }\n\n  & .input-field {\n    margin-bottom: var(--grid-gap);\n  }\n\n  & div[tablist] {\n    & a {\n      display: flex;\n      justify-content: space-between;\n      border-radius: var(--component-border-radius);\n      color: var(--search-result-text-color);\n      outline: none;\n      padding: 0 var(--grid-gap-tiny);\n      border: 1px solid transparent;\n\n      &:hover,\n      &:active,\n      &:focus {\n        border: 1px solid var(--search-result-border-color);\n        background: var(--search-result-hover-bg);\n      }\n    }\n  }\n\n  & footer {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    font-size: 0.7rem;\n    border-top: 1px solid var(--search-divider-color);\n    padding-top: var(--grid-gap-tiny);\n    margin-bottom: calc(0px - var(--grid-gap-tiny));\n\n    & div {\n      display: grid;\n      grid-gap: var(--grid-gap-tiny);\n      grid-auto-flow: column;\n      align-items: end;\n      line-height: 1.4rem;\n\n      & hr {\n        margin: 0 var(--grid-gap-tiny);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/settings-nav.css",
    "content": ".settings {\n  & .tabs nav {\n    @apply --column-grid;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/sidebar-contact.css",
    "content": "div.sidebar__contacts__contact {\n  @apply --column-grid;\n\n  grid-template-columns: 1fr auto;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/sidebar-nav.css",
    "content": ".sidebar-nav {\n  margin-bottom: 10px;\n}\n\n.sidebar-tab {\n  display: inline-block;\n  border-radius: 0;\n  background: none;\n  border: none;\n  color: var(--body-color);\n  padding-bottom: 2px;\n}\n\n.sidebar-tab:hover {\n  background: none;\n  color: var(--body-color);\n}\n\n.sidebar-tab:active {\n  box-shadow: none;\n  background: none;\n}\n\n.sidebar-tab:focus {\n  box-shadow: none;\n}\n\n.sidebar-tab-selected,\n.sidebar-tab-selected:hover,\n.sidebar-tab-selected:focus {\n  box-shadow: none;\n  border-bottom: 2px solid #334;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/sidebar.css",
    "content": "aside {\n  display: grid;\n  grid-template-rows: auto 1fr auto;\n  background: var(--sidebar-bg-color);\n  padding: var(--grid-gap) 0;\n  position: fixed;\n  top: 0;\n  bottom: 0;\n\n  & > * {\n    padding-left: var(--grid-gap);\n    padding-right: var(--grid-gap);\n  }\n\n  /*\n  The sidebar is broken up into three sections:\n   - top (navigation tabs)\n   - middle (most things you'd care about)\n   - bottom (logout)\n  */\n\n  /* the middle */\n  & > div {\n    overflow-y: auto;\n    justify-content: start;\n\n    & nav {\n      &.contacts,\n      &.channels {\n        padding-bottom: var(--grid-gap);\n\n        & .results-info {\n          font-size: 0.75rem;\n          position: absolute;\n          top: 1.25rem;\n          right: 1.25rem;\n          height: 1rem;\n          line-height: 1rem;\n        }\n      }\n\n      & a {\n        display: grid;\n        grid-auto-flow: column;\n        grid-gap: var(--grid-gap);\n        align-items: center;\n        justify-content: start;\n        padding: var(--grid-gap-small) var(--grid-gap);\n        border-radius: var(--button-border-radius);\n\n        &.is-active {\n          background: var(--sidebar-nav-active-color);\n        }\n\n        & > span {\n          display: grid;\n          grid-auto-flow: column;\n          grid-gap: var(--grid-gap);\n          align-items: center;\n          justify-content: start;\n        }\n      }\n\n      & em.offline {\n        color: var(--sidebar-hint-text);\n        padding-left: var(--grid-gap);\n      }\n    }\n\n    & .menu-label {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      padding-bottom: var(--grid-gap);\n\n      & a.button-xs,\n      & button.button-xs {\n        height: var(--button-height-sm);\n        width: var(--button-height-sm);\n      }\n    }\n  }\n\n  /* the bottom */\n  & footer {\n    align-self: end;\n    box-shadow: 0 -6px 8px -7px rgba(0, 0, 0, var(--sidebar-footer-shadow-opacity));\n\n    & > hr {\n      margin-top: 0;\n      border-color: var(--sidebar-hr-border-color);\n    }\n\n    & > div {\n      @apply --column-grid;\n\n      & nav {\n        align-self: end;\n\n        @apply --column-grid;\n\n        & button {\n          @apply --button-as-link;\n        }\n      }\n    }\n  }\n}\n\n.mobile-menu-wrapper {\n  max-height: calc(100% - var(--top-nav-height));\n  min-height: calc(100% - var(--top-nav-height));\n  background-color: var(--body-bg-color);\n  overflow: auto;\n\n  /* #scrollContainer */\n  & .mobile-menu-wrapper__content {\n    height: calc(100vh - var(--top-nav-height));\n    background: var(--body-bg-color);\n    min-height: auto;\n  }\n}\n\n.mobile-menu .mobile-menu__tray {\n  background: var(--body-bg-color);\n  top: var(--top-nav-height);\n  height: calc(100vh - var(--top-nav-height));\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/snippet.css",
    "content": ".snippet-title-bar {\n  display: grid;\n  grid-gap: var(--grid-gap);\n  grid-auto-flow: column;\n  padding-bottom: var(--grid-gap);\n}\n\n.snippet-entry {\n  min-height: 10rem;\n  margin-bottom: var(--grid-gap);\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/toastify-overrides.css",
    "content": "/*!\n * Copied from original, and changed to have better\n * shadows and borders\n *\n * -------------------------------------------\n *\n * Toastify js 1.6.1\n * https://github.com/apvarun/toastify-js\n * @license MIT licensed\n *\n * Copyright (C) 2018 Varun A P\n */\n\n.toastify {\n  padding: 12px 20px;\n  color: #fff;\n  display: inline-block;\n  box-shadow: var(--shadow-lg);\n  position: fixed;\n  opacity: 0;\n  transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);\n  cursor: pointer;\n  text-decoration: none;\n  max-width: calc(50% - 20px);\n  z-index: 2147483647;\n}\n\n.toastify.on {\n  opacity: 1;\n}\n\n.toast-close {\n  opacity: 0.4;\n  padding: 0 5px;\n}\n\n.toastify-right {\n  right: 15px;\n}\n\n.toastify-left {\n  left: 15px;\n}\n\n.toastify-top {\n  top: -150px;\n}\n\n.toastify-bottom {\n  bottom: -150px;\n}\n\n.toastify-rounded {\n  border-radius: 25px;\n}\n\n.toastify-avatar {\n  width: 1.5em;\n  height: 1.5em;\n  margin: 0 5px;\n  border-radius: 2px;\n}\n\n.toastify-center {\n  margin-left: auto;\n  margin-right: auto;\n  left: 0;\n  right: 0;\n  max-width: fit-content;\n}\n\n@media only screen and (max-width: 360px) {\n  .toastify-right,\n  .toastify-left {\n    margin-left: auto;\n    margin-right: auto;\n    left: 0;\n    right: 0;\n    max-width: fit-content;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/top-nav.css",
    "content": ".top-nav {\n  @apply --no-select;\n  @apply --column-grid;\n  @apply --grid-space-betwen;\n\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: var(--top-nav-height);\n  background: var(--top-nav-bg);\n  box-shadow: var(--top-nav-shadow);\n  padding-left: var(--grid-gap);\n  padding-right: var(--grid-gap);\n  justify-items: start;\n  z-index: 100;\n}\n\n.top-nav a:active,\n.top-nav a:hover {\n  text-decoration: none;\n}\n\n.top-nav.inverted {\n  background: var(--state-secondary);\n}\n\n.top-nav .navbar-burger {\n  color: var(--link-color);\n  margin-left: calc(var(--grid-gap) * -1);\n}\n\n.top-nav .user-dropdown-content {\n  @apply --grid;\n\n  padding: var(--grid-gap);\n  overflow: hidden;\n  font-size: 0.85rem;\n  min-width: 300px;\n\n  & section {\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  & a,\n  & button {\n    font-size: 0.85rem;\n    padding: 0;\n    height: var(--dropdown-line-height);\n    line-height: var(--dropdown-line-height);\n\n    &:hover {\n      background-color: unset;\n      color: var(--color-medium-gray);\n      text-decoration: none;\n    }\n  }\n}\n\n.top-nav.inverted .burger,\n.top-nav.inverted .chat-name,\n.top-nav.inverted .dropdown-trigger,\n.top-nav.inverted .link-status,\n.top-nav.inverted > .left-nav > a,\n.top-nav.inverted > .right-nav > a {\n  color: var(--top-nav-inverted-color);\n}\n\n.top-nav hr {\n  width: 1px;\n  height: 100%;\n}\n\n.top-nav .left-nav,\n.top-nav .right-nav {\n  @apply --column-grid;\n\n  align-items: center;\n}\n\n.top-nav .right-nav {\n  justify-self: end;\n}\n\n.top-nav .current-chat {\n  @apply --column-grid;\n\n  align-items: center;\n\n  & hr {\n    margin: 0 var(--grid-gap-tiny);\n  }\n}\n\n.top-nav .link-status {\n  @apply --column-grid;\n\n  align-items: center;\n  justify-content: center;\n\n  & svg {\n    opacity: 0.5;\n  }\n\n  & .hover-reveal {\n    display: none;\n  }\n\n  &:hover {\n    & .hover-reveal {\n      display: block;\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/components/unread-management.css",
    "content": ".unread-indicator {\n  z-index: 1000;\n  top: var(--grid-gap);\n  position: absolute;\n  margin: 0 auto;\n\n  & .input-group {\n    border-radius: var(--button-border-radius);\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/utility/height.css",
    "content": ".min-height\\:under-top-bar {\n  min-height: calc(100vh - var(--top-nav-height));\n}\n"
  },
  {
    "path": "client/web/emberclear/app/styles/utility/transitions.css",
    "content": "@keyframes fadein {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}\n\n@keyframes slide-up-fade-in {\n  0% {\n    opacity: 0;\n    transform: translate(0, 40px);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translate(0, 0);\n  }\n}\n\n.transition-all {\n  transition: all 0.2s cubic-bezier(0.6, 0, 0.735, 0.045);\n}\n\n.transition-all-fast {\n  transition: all 0.1s cubic-bezier(0.6, 0, 0.735, 0.045);\n}\n\n.fade-out {\n  opacity: 0;\n}\n\n.fade-in {\n  opacity: 1;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/templates/add-friend.hbs",
    "content": "<section class='container text-center'>\n  <Pod::AddFriend::AddContact />\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/application.hbs",
    "content": "<App::TopNav />\n\n<App::OffCanvas>\n  <App::UpdateChecker />\n\n  {{outlet}}\n\n</App::OffCanvas>\n\n<App::Modals />\n<App::AppShellRemover />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/chat/in-channel.hbs",
    "content": "<Pod::Chat::ChatHistory\n  class='flex-grow flex-column'\n  @messages={{this.messages}}\n  @to={{this.model.targetChannel}}/>\n\n<Pod::Chat::ChatEntry class='p-l-md p-r-md' @to={{this.model.targetChannel}} />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/chat/index.hbs",
    "content": "<div class='hero pad-md'>\n  <p>\n    <h2>{{t 'ui.chat.welcome'}}</h2>\n\n    {{t 'ui.chat.message' htmlSafe=true}}\n  </p>\n\n  <LinkTo @route='add-friend' class='button'>\n    <FaIcon @icon='plus' />\n    <span>{{t 'buttons.addFriend'}}</span>\n  </LinkTo>\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/chat/privately-with.hbs",
    "content": "<Pod::Chat::ChatHistory\n  @to={{this.model.targetIdentity}}\n  @messages={{this.messages}}\n/>\n\n<Pod::Chat::ChatEntry @to={{this.model.targetIdentity}} />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/chat.hbs",
    "content": "<div class='chat-container'>\n\n  {{outlet}}\n\n</div>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/contacts.hbs",
    "content": "<section class='pt-4'>\n  <div class='container'>\n    <Pod::Contacts::Header @identities={{this.model.identities}} />\n\n    <Pod::Contacts::ContactTable />\n  </div>\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/faq.hbs",
    "content": "<App::Container::Main>\n\n  <div class='container'>\n    <h1 class='title'>{{t 'ui.faq.title'}}</h1>\n\n    <Pod::Faq::QAndA @question='ui.faq.whatIsQ' @answer='ui.faq.whatIsA' />\n    <Pod::Faq::QAndA @question='ui.faq.howDoesWorkQ' @answer='ui.faq.howDoesWorkA' />\n    <Pod::Faq::QAndA @question='ui.faq.whyQ' @answer='ui.faq.whyA' />\n    <Pod::Faq::QAndA @question='ui.faq.notOnlineQ' @answer='ui.faq.notOnlineA' />\n    <Pod::Faq::QAndA @question='ui.faq.serverStorageQ' @answer='ui.faq.serverStorageA' />\n\n  </div>\n\n</App::Container::Main>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/index.hbs",
    "content": "<App::Container::PrimaryHero>\n  <h1>{{t 'appname'}}</h1>\n\n  <p>{{t 'subheader'}}</p>\n\n  <br>\n\n  <Pod::Index::BeginButton />\n  <Pod::Index::Compatibility />\n</App::Container::PrimaryHero>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/invite.hbs",
    "content": "{{!-- template-lint-disable no-inline-styles --}}\n<div\n  id='app-loader'\n  style='\n    position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n    display: flex;\n    flix-direction: column;\n    justify-content: center;\n    align-items: center;'\n  >\n\n  <div style='width: 180px; height: 180px;' class='loader'>\n  </div>\n\n  <span class='m-l-md'>{{t 'status.importing'}}</span>\n</div>\n\n"
  },
  {
    "path": "client/web/emberclear/app/templates/login.hbs",
    "content": "<section class='hero'>\n  <div class='hero-body'>\n    <div class='container'>\n      <Pod::Login::LoginForm />\n    </div>\n  </div>\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/logout.hbs",
    "content": "<section class='logout-warning hero bg-danger text-light'>\n\n  <div class='container'>\n    <h1>\n      {{t 'ui.logout.title'}}\n    </h1>\n\n    <h2>\n      {{t 'ui.logout.warning'}}\n\n      <LinkTo @route='settings'>\n        {{t 'ui.logout.theSettingsPage'}}\n      </LinkTo>\n    </h2>\n\n    <div class='cta'>\n      <button\n        data-test-confirm-logout\n        type='button'\n        {{on 'click' this.logout}}\n      >\n        <FaIcon @icon='sign-out-alt' @prefix='fas' />\n        <span>{{t 'ui.logout.confirm'}}</span>\n      </button>\n    </div>\n  </div>\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/not-found.hbs",
    "content": "<section class='sticky-footer-content'>\n  <div class='hero is-medium'>\n    <div class='hero-body'>\n      <div class='container has-text-centered'>\n        <h1 class='title'>\n          {{t 'errors.notFound.title'}}\n        </h1>\n\n        <h2>\n          {{t 'errors.notFound.help'}}\n          <ExternalLink href='https://github.com/NullVoxPopuli/emberclear/issues/new'>\n            &nbsp;\n            {{t 'errors.notFound.link'}}\n          </ExternalLink>\n        </h2>\n\n        <br>\n      </div>\n    </div>\n  </div>\n</section>\n\n<AppFooter class='sticky-footer' />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/qr.hbs",
    "content": "<section class='hero'>\n  <div class='hero-body'>\n    <div class='container' data-test-qr-container>\n      <Pod::QR />\n    </div>\n  </div>\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/settings/danger-zone.hbs",
    "content": "<Pod::Settings::DangerZone />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/settings/index.hbs",
    "content": "<Pod::Settings::Profile />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/settings/interface.hbs",
    "content": "<Pod::Settings::Interface />\n"
  },
  {
    "path": "client/web/emberclear/app/templates/settings/relays.hbs",
    "content": "<Pod::Settings::Relays::RelayTable @relays={{this.model.relays}} />\n<Pod::Settings::Relays::NewRelayForm />\n\n"
  },
  {
    "path": "client/web/emberclear/app/templates/settings.hbs",
    "content": "<section data-test-settings-wrapper class='settings pt-4'>\n  <div class='container'>\n    <h1>{{t 'ui.settings.title' }}</h1>\n    <Pod::Settings::Navigation />\n\n    {{outlet}}\n  </div>\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/templates/setup.hbs",
    "content": "<section class='grid min-height:under-top-bar'>\n  <Pod::Setup />\n</section>\n"
  },
  {
    "path": "client/web/emberclear/app/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"composite\": true,\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"emberclear/*\": [\"*\"],\n\n      \"ember-browser-services/*\": [\n        \"../../node_modules/ember-browser-services\"\n      ],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../libraries/questionably-typed\" },\n    { \"path\": \"../../addons/tracked-local-storage\" },\n    { \"path\": \"../../addons/ui\" },\n    { \"path\": \"../../addons/crypto\" },\n    { \"path\": \"../../addons/local-account\" },\n    { \"path\": \"../../addons/networking\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/README.md",
    "content": "This is, unfortunately, a dumping ground for things I don't know where to put\n"
  },
  {
    "path": "client/web/emberclear/app/utils/breakpoints.ts",
    "content": "export const TABLET_WIDTH = 832;\n"
  },
  {
    "path": "client/web/emberclear/app/utils/dom/css.ts",
    "content": "// export function valueOfProperty(name: string): string {\n//   return getComputedStyle(document.documentElement)\n//     .getPropertyValue(`--${name}`)\n//     .trim()\n//     .split(/px|rem/)[0];\n// }\n"
  },
  {
    "path": "client/web/emberclear/app/utils/dom/utils.ts",
    "content": "import DOMPurify from 'dompurify';\nimport showdown from 'showdown';\n\nexport function isElementWithin(element: HTMLElement, container: HTMLElement): boolean {\n  const rect = element.getBoundingClientRect();\n  const containerRect = container.getBoundingClientRect();\n\n  const isVisible =\n    rect.top >= containerRect.top &&\n    rect.left >= containerRect.left &&\n    rect.bottom <= containerRect.bottom &&\n    rect.right <= containerRect.right;\n\n  return isVisible;\n}\n\nexport function isInElementWithinViewport(element: Element, container: Element): boolean {\n  if (!element || !container) {\n    return false;\n  }\n\n  const containerRect = container.getBoundingClientRect();\n  const childRect = element.getBoundingClientRect();\n\n  const containerViewableArea = {\n    height: container.clientHeight,\n    width: container.clientWidth,\n  };\n\n  const isViewable =\n    childRect.top >= containerRect.top &&\n    childRect.top <= containerRect.top + containerViewableArea.height;\n\n  return isViewable;\n}\n\nexport function keepInViewPort(element: HTMLElement, margin = 20 /* px */) {\n  const rect = element.getBoundingClientRect();\n\n  if (rect.left < 0) {\n    element.style.left = `${margin}px`;\n  }\n\n  if (rect.right > window.innerWidth) {\n    const delta = window.innerWidth - rect.right;\n\n    element.style.left = `${delta - margin}px`;\n  }\n\n  if (rect.top < 0) {\n    element.style.top = `${margin}px`;\n  }\n\n  if (rect.bottom > window.innerHeight) {\n    element.style.bottom = `${margin}px`;\n  }\n}\n\nconst converter = new showdown.Converter({\n  simplifiedAutoLink: true,\n  simpleLineBreaks: true,\n  openLinksInNewWindow: true,\n});\n\n// NOTE: sanitizing by default removes target=\"_blank\"\nDOMPurify.addHook('afterSanitizeAttributes', function (node: any) {\n  if ('target' in node) {\n    node.setAttribute('target', '_blank');\n    node.setAttribute('rel', 'noopener');\n  }\n});\n\nexport function convertAndSanitizeMarkdown(markdown: string) {\n  const html = converter.makeHtml(markdown);\n  const sanitized = DOMPurify.sanitize(html);\n\n  return sanitized;\n}\n\n// https://stackoverflow.com/questions/45408920/plain-javascript-scrollintoview-inside-div\nexport function scrollIntoViewOfParent(parent: Element, child: Element) {\n  // Where is the parent on page\n  const parentRect = parent.getBoundingClientRect();\n  // What can you see?\n  const parentViewableArea = {\n    height: parent.clientHeight,\n    width: parent.clientWidth,\n  };\n\n  // Where is the child\n  const childRect = child.getBoundingClientRect();\n  // Is the child viewable?\n  const isViewable =\n    childRect.top >= parentRect.top && childRect.top <= parentRect.top + parentViewableArea.height;\n\n  // if you can't see the child try to scroll parent\n  if (!isViewable) {\n    // scroll by offset relative to parent\n    const amount = childRect.top - parentRect.top - parentViewableArea.height / 2;\n\n    parent.scrollBy({ top: amount, behavior: 'smooth' });\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/ember-concurrency.ts",
    "content": "import { didCancel } from 'ember-concurrency';\nimport { taskFor } from 'ember-concurrency-ts';\n\nimport type { Event } from 'xstate';\n\n/**\n * Wraps an ember-concurrency task into an XState service.\n *\n *     Machine({\n *       id: 'example',\n *       initial: 'fetch',\n *       states: {\n *         'fetch': {\n *           invoke: { src: 'fetch' },\n *           on: {\n *             DONE: 'done',\n *             CANCEL: 'cancelled',\n *             ERROR: 'errored',\n *           },\n *         },\n *         'cancelled': { type: 'final' },\n *         'errored': { type: 'final' },\n *         'done': { type: 'final' },\n *       },\n *     }).withConfig({\n *       services: {\n *         fetch: taskService(this.fetchTask),\n *       },\n *     });\n *\n * @function\n * @param {TaskProp} taskProp the task property (not instance) to call perform() on\n * @return {CallbackService} an XState compatable callback based service\n */\nexport function taskService<TEvent extends Event<any>, Args extends unknown[], Return = void>(\n  taskProp: (...args: Args) => Promise<Return>\n): (...args: Args) => (callback: TEvent) => void {\n  return (...args: Args) => (callback: Event<any>) => {\n    let taskInstance = taskFor(taskProp).perform(...args);\n\n    taskInstance.then(\n      (data) => callback({ type: 'DONE', data }),\n      (error) =>\n        didCancel(error) ? callback({ type: 'CANCEL' }) : callback({ type: 'ERROR', error })\n    );\n\n    return () => taskInstance.cancel();\n  };\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/errors.ts",
    "content": "export class ConnectionDoesNotExistError extends Error {}\nexport class MalformedQRCodeError extends Error {}\nexport class UnrecognizedQRCodeError extends Error {}\n\nexport class NoCameraError extends Error {\n  constructor(...props: any[]) {\n    super(...props);\n\n    this.name = 'NoCameraError';\n  }\n}\n\nexport class ConnectionError extends Error {}\nexport class RelayNotSetError extends Error {\n  name = 'RelayNotSet';\n}\n\nexport class CurrentUserNotFound extends Error {\n  name = 'CurrentUserNotFound';\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/identity-comparison.ts",
    "content": "import { equalsUint8Array } from './uint8array-equality';\n\nimport type Identity from '@emberclear/local-account/models/identity';\n\nexport function identitiesIncludes(identities: Identity[], identity: Identity): boolean {\n  return identities.some((identityToCheck) => identityEquals(identityToCheck, identity));\n}\n\nexport function identityEquals(identity1: Identity, identity2: Identity): boolean {\n  return equalsUint8Array(identity1.publicKey, identity2.publicKey);\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/normalized-meta.ts",
    "content": "import type { OpenGraphData } from '@emberclear/networking/types';\n\ntype Args = {\n  url: string;\n  openGraph?: OpenGraphData | null;\n};\n\n// https://stackoverflow.com/a/8260383/356849\nconst YT_PATTERN = /^.*(youtu.be\\/|\\/v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/;\nconst IMAGE_PATTERN = /(jpg|png|gif|webp)/;\nconst VIDEO_PATTERN = /(\\.(mp4)$)/;\nconst EXT_PATTERN = /\\.[\\w]{2,4}$/;\n\nexport type NormalizedMeta = {\n  alt?: string;\n  title?: string;\n  siteName?: string;\n  description?: string;\n  hasExtension: boolean;\n  openGraph?: OpenGraphData | null;\n\n  embedUrl?: string;\n  isVideo: boolean;\n  isImage: boolean;\n  isYouTube: boolean;\n\n  hasMedia: boolean;\n  hasInfo: boolean;\n};\n\nexport function normalizeMeta(data: Args): NormalizedMeta {\n  let og = data.openGraph || {};\n\n  let embedUrl = embedUrlFrom(data.url);\n\n  let types = {\n    isVideo: VIDEO_PATTERN.test(data.url),\n    isImage: IMAGE_PATTERN.test(data.url),\n    isYouTube: Boolean(embedUrl),\n  };\n\n  return {\n    ...types,\n    hasMedia: types.isVideo || types.isImage || types.isYouTube,\n    hasInfo: Boolean(og.title || og.description),\n\n    hasExtension: EXT_PATTERN.test(data.url),\n    alt: og['image:alt'],\n    title: og.title,\n    siteName: og['site_name'],\n    description: og.description,\n    openGraph: og,\n\n    embedUrl,\n  };\n}\n\nfunction embedUrlFrom(url: string) {\n  let ytMatches = url.match(YT_PATTERN);\n\n  if (ytMatches?.[2]) {\n    let videoId = ytMatches[2];\n    let embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}`;\n\n    return embedUrl;\n  }\n\n  return undefined;\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/route-matchers.ts",
    "content": "export const PRIVATE_CHAT_REGEX = /chat\\/privately-with\\/(.+)/;\nexport const CHANNEL_REGEX = /chat\\/in-channel\\/(.+)/;\n\nexport function idFrom(regex: RegExp, url: string) {\n  let matches = regex.exec(url);\n  let id = matches?.[1];\n\n  return id || '';\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/string/utils.ts",
    "content": "export function matchAll(str: string, regex: RegExp) {\n  let match;\n  let result = [];\n\n  while ((match = regex.exec(str))) {\n    result.push(match);\n  }\n\n  return result;\n}\n\nexport function parseLanguages(text: string): string[] {\n  let languages: string[] = [];\n\n  const matches = matchAll(text, /```(\\w+)/g);\n\n  matches.forEach((match) => languages.push(match[1]));\n\n  return languages;\n}\n\n// https://www.regextester.com/98192\nconst URL_PATTERN = /(((http|https):\\/\\/)|(www)){1}[a-zA-Z0-9./?:@\\-_=#]+\\.([a-zA-Z0-9&./?:@\\-_=#])*/gi;\n\nexport function parseURLs(text: string): string[] {\n  const urls = text.match(URL_PATTERN);\n\n  if (urls === null) return [];\n\n  return urls.map((u) => u.replace('gifv', 'mp4'));\n}\n\nconst HOST_FROM_URL_REGEX = /\\/\\/(.+)\\//;\n\nexport function hostFromURL(url: string) {\n  const matches = url.match(HOST_FROM_URL_REGEX);\n\n  return matches?.[1];\n}\n"
  },
  {
    "path": "client/web/emberclear/app/utils/uint8array-equality.ts",
    "content": "export function equalsUint8Array(arr1: Uint8Array, arr2: Uint8Array): boolean {\n  if (arr1.length !== arr2.length) {\n    return false;\n  }\n\n  for (let i = 0; i < arr1.length; i++) {\n    if (arr1[i] !== arr2[i]) {\n      return false;\n    }\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "client/web/emberclear/config/addons.js",
    "content": "const ENV = {};\n\nENV['fontawesome'] = {\n  defaultPrefix: 'fas', // free-and-solid\n};\n\nENV['ember-a11y-testing'] = {\n  componentOptions: {\n    turnAuditOff: true,\n  },\n};\n\nENV['ember-component-css'] = {\n  namespacing: false,\n};\n\nENV['ember-cli-notifications'] = {\n  icons: 'fa-5',\n};\n\nENV['ember-service-worker-update-notify'] = {\n  pollingInterval: 120000, // two minutes\n};\n\nmodule.exports = ENV;\n"
  },
  {
    "path": "client/web/emberclear/config/build/addons.js",
    "content": "'use strict';\n\nconst simpleAddonConfigs = {\n  'ember-test-selectors': {\n    strip: false, // isProduction,\n  },\n};\n\nfunction serviceWorkerConfig({ version }) {\n  return {\n    'asset-cache': {\n      version,\n      include: ['assets/**/*', '**/*.html', 'index.html'],\n      exclude: [\n        '.well-known/**/*',\n        'bundle.html',\n        'bundle/**/*.html',\n        'bundle/*.html',\n        'favicon.ico',\n        'robots.txt',\n      ],\n    },\n    'esw-index': {\n      version,\n      excludeScope: [\n        /\\.well-known/,\n        /bundle.html/,\n        /bundle\\/(.+)\\.html/,\n        /favicon.ico/,\n        /robots.txt/,\n      ],\n    },\n    'esw-cache-fallback': {\n      patterns: ['https://(.+)/open_graph?(.+)'],\n    },\n  };\n}\n\nfunction addonConfig(env) {\n  return {\n    ...simpleAddonConfigs,\n    ...serviceWorkerConfig(env),\n  };\n}\n\nmodule.exports = {\n  addonConfig,\n};\n"
  },
  {
    "path": "client/web/emberclear/config/build/static.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst Funnel = require('broccoli-funnel');\nconst writeFile = require('broccoli-file-creator');\n\nconst prismPath = path.join(__dirname, '..', '..', '..', 'node_modules', 'prismjs');\n\nmodule.exports = {\n  buildStaticTrees({ isProduction, hash }) {\n    let prism = new Funnel(prismPath, {\n      include: ['prism.js', 'themes/*', 'plugins/**', 'components/**'],\n      destDir: '/prismjs/',\n    });\n\n    // source: https://codeburst.io/ember-js-lazy-assets-fingerprinting-loading-static-dynamic-assets-on-demand-f09cd7568155\n    // ------------------------------------------------------------------------------------------\n    // Create a asset-fingerprint.js file which holds the fingerprintHash value\n    // This hash value is used by all the asset loaders to load the assets on-demand\n    // ------------------------------------------------------------------------------------------\n    let assetFingerprintTree = writeFile(\n      './assets/assets-fingerprint.js',\n      `(function(_window){ _window.ASSET_FINGERPRINT_HASH = \"${\n        isProduction ? `-${hash}` : ''\n      }\"; })(window);`\n    );\n\n    return [prism, assetFingerprintTree];\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/config/coverage.js",
    "content": "module.exports = {\n  useBabelInstrumenter: true,\n  excludes: [\n    '*/concat-stats-for/**/*',\n    '*/public/**/*',\n    '*/translations/**/*',\n    '*/vendor/**/*',\n    '**/*-test*',\n  ],\n  parallel: false,\n};\n"
  },
  {
    "path": "client/web/emberclear/config/dependency-lint.js",
    "content": "'use strict';\n\nmodule.exports = {};\n"
  },
  {
    "path": "client/web/emberclear/config/ember-intl.js",
    "content": "/*jshint node:true*/\n\nmodule.exports = function (/* env */) {\n  return {\n    /**\n     * prevents the translations from being bundled with the application code.\n     * This enables asynchronously loading the translations for the active locale\n     * by fetching them from the asset folder of the build.\n     *\n     * See: https://github.com/jasonmit/ember-intl/blob/master/docs/asynchronously-loading-translations.md\n     *\n     * @property publicOnly\n     * @type {Boolean}\n     * @default \"false\"\n     */\n    publicOnly: false,\n\n    /**\n     * Path where translations are kept.  This is relative to the project root.\n     * For example, if your translations are an npm dependency, set this to:\n     *`'./node_modules/path/to/translations'`\n     *\n     * @property inputPath\n     * @type {String}\n     * @default \"translations\"\n     */\n    inputPath: 'translations',\n  };\n};\n"
  },
  {
    "path": "client/web/emberclear/config/emberclear-local.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBrTCCAVMCFCuYeirr4e2pIP2ByKeOVmEE5wq8MAoGCCqGSM49BAMCMFgxCzAJ\nBgNVBAYTAlVTMQswCQYDVQQIDAJJTjEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMRkwFwYDVQQDDBBlbWJlcmNsZWFyLWxvY2FsMCAXDTIwMDIwNzAx\nNDIzNVoYDzIxMjAwMTE0MDE0MjM1WjBYMQswCQYDVQQGEwJVUzELMAkGA1UECAwC\nSU4xITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEZMBcGA1UEAwwQ\nZW1iZXJjbGVhci1sb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJGRiQGB\nOg4d/MDdtjfcuWE/DL582Cee5aXsN8Ebuy972LlyeYplVQAWuPznsc9gyuoLCq2p\nP9gmNIHIES4TPREwCgYIKoZIzj0EAwIDSAAwRQIhAKmmOo2HkoUlP3z6hi6jv93e\nUrxExTujaAXeC1MjEMdQAiBvlWSAmryh9Iu9Arzh8kqnryi+cDs9zJBePbyz8fjF\nUA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "client/web/emberclear/config/emberclear-local.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBFDCBugIBADBYMQswCQYDVQQGEwJVUzELMAkGA1UECAwCSU4xITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEZMBcGA1UEAwwQZW1iZXJjbGVhci1s\nb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJGRiQGBOg4d/MDdtjfcuWE/\nDL582Cee5aXsN8Ebuy972LlyeYplVQAWuPznsc9gyuoLCq2pP9gmNIHIES4TPRGg\nADAKBggqhkjOPQQDAgNJADBGAiEAyxY26SD1n26Xodi707vuCIME47QmXojCxtCu\nI5gVTJwCIQCdJ4nr7p1Uq94uRUsZS0OYjRAVPAjCJmPYlWX2Pu4o7Q==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "client/web/emberclear/config/emberclear-local.key",
    "content": "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPfRPzH2uCRViYgXdJn506QIkYgDwTGNZsA8lq/vNfWHoAoGCCqGSM49\nAwEHoUQDQgAEkZGJAYE6Dh38wN22N9y5YT8MvnzYJ57lpew3wRu7L3vYuXJ5imVV\nABa4/Oexz2DK6gsKrak/2CY0gcgRLhM9EQ==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "client/web/emberclear/config/environment.js",
    "content": "'use strict';\n\nconst ADDON_ENV = require('./addons');\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'emberclear',\n\n    environment,\n    rootURL: '/',\n    locationType: 'auto', // default\n    historySupportMiddleware: true,\n\n    EmberENV: {\n      FEATURES: {},\n      EXTEND_PROTOTYPES: false,\n    },\n\n    ...ADDON_ENV,\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n    ENV.host = 'http://localhost:4201';\n    ENV.SW_DISABLED = process.env.SW_DISABLED;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n    ENV.SW_DISABLED = process.env.SW_DISABLED;\n\n    ENV.percy = {\n      // breakpoints from bulma.io\n      // mobile: 768,\n      // desktop: 1024,\n      // widescreen: 1216,\n      breakpointsConfig: {\n        phone: 540,\n        mobile: 768,\n        desktop: 1024,\n        // widescreen: 1216,\n      },\n      defaultBreakpoints: [\n        'phone',\n        'mobile',\n        'desktop',\n        // 'widescreen',\n      ],\n    };\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n    // github pages:\n    ENV.host = process.env.HOST || 'https://emberclear.io';\n    ENV.rootURL = '/';\n    ENV.baseURL = '/';\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/emberclear/config/icons.js",
    "content": "'use strict';\n\nmodule.exports = function customFAIconSet() {\n  return {\n    'free-brands-svg-icons': ['reddit', 'twitter', 'monero'],\n    'free-solid-svg-icons': [\n      'qrcode',\n      'user-circle',\n      'address-book',\n      'sliders-h',\n      'sign-out-alt',\n      'dot-circle',\n      'plus',\n      'code',\n      'desktop',\n      'bed',\n      'video',\n      'angle-down',\n      'angle-up',\n      'angle-right',\n      'times',\n      'times-circle',\n      'phone',\n      'phone-volume',\n      'share',\n      'check-circle',\n      'exclamation-circle',\n      'check',\n      'ellipsis-h',\n      'globe',\n      'bars',\n      'search',\n      'thumbtack',\n      'minus',\n      'wifi',\n    ],\n  };\n};\n"
  },
  {
    "path": "client/web/emberclear/config/manifest.js",
    "content": "/* eslint-env node */\n'use strict';\n\n// handled by ember-web-app\nmodule.exports = function (environment /*, appConfig */) {\n  // See https://github.com/san650/ember-web-app#documentation for a list of\n  // supported properties\n\n  // rootURL should end in a slash\n  const rootURL = environment.rootURL || '/';\n\n  return {\n    name: 'emberclear',\n    short_name: 'emberclear',\n    description: 'Encrypted Chat. No History. No Logs.',\n    start_url: `${rootURL}`,\n    display: 'standalone',\n    background_color: '#fff',\n    theme_color: '#fff',\n    icons: [\n      {\n        src: `${rootURL}assets/images/icons/android-chrome-192x192.png`,\n        sizes: '192x192',\n        type: 'image/png',\n      },\n      {\n        src: `${rootURL}assets/images/icons/android-chrome-512x512.png`,\n        sizes: '512x512',\n        type: 'image/png',\n      },\n    ],\n    ms: {\n      tileColor: '#fff',\n    },\n  };\n};\n"
  },
  {
    "path": "client/web/emberclear/config/netlify/_redirects",
    "content": "https://emberclear.netlify.com/* https://emberclear.io/:splat 301!\n\n/bundle.html /bundle.html 200\n/bundle/* /bundle/:splat 200\n\n/.well-known/assetlinks.json /.well-known/assetlinks.json 200\n\n/*    /index.html   200\n"
  },
  {
    "path": "client/web/emberclear/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/emberclear/config/targets.js",
    "content": "'use strict';\n\n// prettier-ignore\nconst browsers = [\n  '> 2%',\n  'not IE 11',\n  'not dead',\n];\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/emberclear/ember-cli-build.js",
    "content": "'use strict';\n\nconst crypto = require('crypto');\nconst gitRev = require('git-rev-sync');\n\nconst mergeTrees = require('broccoli-merge-trees');\nconst EmberApp = require('ember-cli/lib/broccoli/ember-app');\nconst { UnwatchedDir } = require('broccoli-source');\n\nconst { logWithAttention } = require('@emberclear/config/utils/log');\nconst {\n  applyEnvironmentVariables,\n  configureBabel,\n} = require('@emberclear/config/utils/ember-build');\n\nconst { addonConfig } = require('./config/build/addons');\nconst { buildStaticTrees } = require('./config/build/static');\n\nconst { EMBROIDER, CONCAT_STATS } = process.env;\n\nconst version = gitRev.short();\nconst hash = crypto.createHash('md5').update(new Date().getTime().toString()).digest('hex');\n\nmodule.exports = function (defaults) {\n  let environment = EmberApp.env();\n  let isProduction = environment === 'production';\n\n  let env = {\n    isProduction,\n    isTest: environment === 'test',\n    environment,\n    version,\n    hash,\n    CONCAT_STATS,\n  };\n\n  let appOptions = {\n    hinting: false,\n\n    fingerprint: {\n      // why customHash?\n      // so we can reference the hash from an global variable\n      // (used in build/static.js), which allows us to reference\n      // the fingerprint from runtime code.\n      //\n      // It'd be great if there was a built-in API to do this by default,\n      // but I think each static asset gets its own fingerprint\n      // which means we'd need a lookup table for asset to hash.\n      //\n      // Using the same hash for everything simplifies a lot.\n      // However, it does mean that we bust cache more often.\n      //\n      // The hash is available as an IIFE at /assets/assets-fingerprint.js\n      // built by buildStaticTrees(...)\n      customHash: hash,\n    },\n\n    emberData: {\n      compatWith: '3.16.0',\n    },\n\n    // Why are configs split up this way?\n    // To reduce mental load when parsing the build configuration.\n    // We don't need to view everything all at once.\n    ...addonConfig(env),\n  };\n\n  configureBabel(appOptions);\n  applyEnvironmentVariables(appOptions);\n\n  if (isProduction) {\n    appOptions.autoImport = appOptions.autoImport || {};\n    appOptions.autoImport.exclude = ['tweetnacl'];\n  }\n\n  logWithAttention(env, appOptions);\n\n  let app = new EmberApp(defaults, appOptions);\n\n  // Additional paths to copy to the public directory in the final build.\n  let additionalTrees = [...buildStaticTrees(env)];\n\n  if (!isProduction) {\n    app.trees.public = new UnwatchedDir('public');\n  }\n\n  if (EMBROIDER) {\n    logWithAttention('E M B R O I D E R');\n\n    const { compatBuild } = require('@embroider/compat');\n    const { Webpack } = require('@embroider/webpack');\n\n    return compatBuild(app, Webpack, {\n      extraPublicTrees: additionalTrees,\n      // staticAddonTestSupportTrees: true,\n      // staticAddonTrees: true,\n      // staticHelpers: true,\n      // staticComponents: true,\n      // splitAtRoutes: true,\n      // skipBabel: [],\n    });\n  }\n\n  // Old-style broccoli-build\n  return mergeTrees([app.toTree(), ...additionalTrees]);\n};\n"
  },
  {
    "path": "client/web/emberclear/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true\n  },\n  \"exclude\": [\"node_modules\", \"bower_components\", \"tmp\", \"vendor\", \".git\", \"dist\"]\n}\n\n"
  },
  {
    "path": "client/web/emberclear/package.json",
    "content": "{\n  \"name\": \"emberclear\",\n  \"version\": \"0.0.1\",\n  \"private\": false,\n  \"description\": \"Small description for emberclear goes here\",\n  \"license\": \"GPL-3.0\",\n  \"author\": \"NullVoxPopuli\",\n  \"main\": \"app/app.js\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"repository\": \"https://github.com/NullVoxPopuli/emberclear.git\",\n  \"scripts\": {\n    \"clean\": \"rm -rf dist tmp node_modules concat-stats-for coverage*\",\n    \"build\": \"yarn ember build\",\n    \"build:production\": \"yarn build --environment production && sed -i -e 's/{{ROOT_URL}}/\\\\//g' dist/index.html\",\n    \"ramdisk\": \"./scripts/node_modules-to-ramdisk.sh\",\n    \"start:sw\": \"yarn ember serve -p 4201\",\n    \"start:dev\": \"yarn start\",\n    \"start:prod\": \"SOURCEMAPS_DISABLED=true MINIFY_DISABLED=true yarn start --environment production\",\n    \"start:fast\": \"yarn ramdisk && SOURCEMAPS_DISABLED=true MINIFY_DISABLED=true yarn start\",\n    \"start\": \"cross-env SW_DISABLED=true yarn ember serve -p 4201\",\n    \"test\": \"cross-env SW_DISABLED=true yarn ember exam --random\",\n    \"merge-coverage\": \"ember coverage-merge\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:types\": \"tsc --skipLibCheck --noEmit\",\n    \"lint:js\": \"yarn eslint . --ext .ts --ext .js\",\n    \"bundle-analyze\": \"yarn broccoli-concat-analyser ./concat-stats-for\",\n    \"analyze\": \"./scripts/analyze.sh\"\n  },\n  \"browserslist\": [\n    \"> 3%\",\n    \"not IE 11\"\n  ],\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"resolutions\": {\n    \"ember-test-waiters\": \"2.1.3\",\n    \"@glimmer/tracking\": \"1.0.4\"\n  },\n  \"dependencies\": {\n    \"@emberclear/crypto\": \"*\",\n    \"@emberclear/local-account\": \"*\",\n    \"@emberclear/networking\": \"*\",\n    \"@emberclear/ui\": \"*\",\n    \"ember-browser-services\": \"1.1.6\",\n    \"ember-concurrency\": \"1.3.0\",\n    \"ember-concurrency-async\": \"0.3.2\",\n    \"ember-concurrency-decorators\": \"2.0.3\",\n    \"ember-concurrency-test-waiter\": \"0.4.0\",\n    \"ember-concurrency-ts\": \"0.2.2\",\n    \"ember-tracked-local-storage\": \"*\",\n    \"emojis\": \"1.0.10\",\n    \"hammerjs\": \"2.0.8\",\n    \"prismjs\": \"1.24.0\",\n    \"prismjs-components-loader\": \"3.0.1\",\n    \"showdown\": \"1.9.1\",\n    \"toastify-js\": \"1.10.0\",\n    \"url-parse\": \"1.5.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/preset-typescript\": \"7.14.5\",\n    \"@ember-data/debug\": \"3.26.0\",\n    \"@ember-data/model\": \"3.26.0\",\n    \"@ember-data/record-data\": \"3.26.0\",\n    \"@ember-data/store\": \"3.26.0\",\n    \"@ember/edition-utils\": \"1.2.0\",\n    \"@ember/optional-features\": \"2.0.0\",\n    \"@ember/render-modifiers\": \"1.0.2\",\n    \"@ember/test-helpers\": \"2.2.8\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/questionably-typed\": \"*\",\n    \"@emberclear/test-helpers\": \"*\",\n    \"@embroider/compat\": \"0.40.0\",\n    \"@embroider/core\": \"0.40.0\",\n    \"@embroider/webpack\": \"0.40.0\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@html-next/vertical-collection\": \"2.0.0\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/bip39\": \"3.0.0\",\n    \"@types/common-tags\": \"1.8.0\",\n    \"@types/dompurify\": \"2.2.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-data\": \"3.16.14\",\n    \"@types/ember-data__model\": \"3.16.2\",\n    \"@types/ember-data__store\": \"3.16.1\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"5.0.10\",\n    \"@types/ember-test-helpers\": \"1.0.9\",\n    \"@types/ember-testing-helpers\": \"0.0.4\",\n    \"@types/ember__application\": \"3.16.2\",\n    \"@types/ember__array\": \"3.16.4\",\n    \"@types/ember__component\": \"3.16.5\",\n    \"@types/ember__controller\": \"3.16.4\",\n    \"@types/ember__debug\": \"3.16.3\",\n    \"@types/ember__engine\": \"3.16.2\",\n    \"@types/ember__error\": \"3.16.1\",\n    \"@types/ember__object\": \"3.12.5\",\n    \"@types/ember__polyfills\": \"3.12.1\",\n    \"@types/ember__routing\": \"3.16.14\",\n    \"@types/ember__runloop\": \"3.16.3\",\n    \"@types/ember__service\": \"3.16.1\",\n    \"@types/ember__string\": \"3.16.3\",\n    \"@types/ember__template\": \"3.16.1\",\n    \"@types/ember__test\": \"3.16.1\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/ember__utils\": \"3.16.2\",\n    \"@types/hammerjs\": \"2.0.39\",\n    \"@types/prismjs\": \"1.16.5\",\n    \"@types/qrcode\": \"1.4.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"@types/showdown\": \"1.9.3\",\n    \"@types/sinon\": \"10.0.2\",\n    \"@types/url-parse\": \"1.4.3\",\n    \"@xstate/test\": \"0.4.2\",\n    \"autoprefixer\": \"10.2.6\",\n    \"broccoli-asset-rev\": \"3.0.0\",\n    \"broccoli-concat-analyser\": \"5.0.0\",\n    \"broccoli-file-creator\": \"2.1.1\",\n    \"broccoli-persistent-filter\": \"3.1.2\",\n    \"codecov\": \"3.8.2\",\n    \"colors\": \"1.4.0\",\n    \"common-tags\": \"1.8.0\",\n    \"core-js\": \"3.15.0\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-autofocus-modifier\": \"1.2.1\",\n    \"ember-autoresize-modifier\": \"0.3.0\",\n    \"ember-autostash-modifier\": \"0.1.20\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-app-version\": \"5.0.0\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-clipboard\": \"github:NullVoxPopuli/ember-cli-clipboard#octaneify\",\n    \"ember-cli-code-coverage\": \"1.0.3\",\n    \"ember-cli-dependency-checker\": \"3.2.0\",\n    \"ember-cli-dependency-lint\": \"2.0.0\",\n    \"ember-cli-deprecation-workflow\": \"1.0.1\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-cli-inject-live-reload\": \"2.1.0\",\n    \"ember-cli-page-object\": \"1.17.7\",\n    \"ember-cli-postcss\": \"7.0.2\",\n    \"ember-cli-sri\": \"2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-destroyable-polyfill\": \"2.0.3\",\n    \"ember-exam\": \"6.1.0\",\n    \"ember-export-application-global\": \"2.0.1\",\n    \"ember-focus-trap\": \"0.7.0\",\n    \"ember-hbs-minifier\": \"0.5.0\",\n    \"ember-inflector\": \"4.0.2\",\n    \"ember-intl\": \"5.7.0\",\n    \"ember-jsqr\": \"1.2.14\",\n    \"ember-keyboard\": \"6.0.3\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-localforage-adapter\": \"github:NullVoxPopuli/ember-localforage-adapter#9423bebe9c72efce0a6c1bbf478e8af8d140b165\",\n    \"ember-maybe-import-regenerator\": \"0.1.6\",\n    \"ember-mobile-menu\": \"2.1.1\",\n    \"ember-modifier\": \"2.1.2\",\n    \"ember-named-blocks-polyfill\": \"0.2.4\",\n    \"ember-percy\": \"1.6.0\",\n    \"ember-purify\": \"5.0.1\",\n    \"ember-qunit\": \"4.6.0\",\n    \"ember-render-helpers\": \"0.2.0\",\n    \"ember-resolver\": \"8.0.2\",\n    \"ember-router-helpers\": \"github:rwjblue/ember-router-helpers\",\n    \"ember-service-worker\": \"9.0.1\",\n    \"ember-service-worker-asset-cache\": \"0.6.4\",\n    \"ember-service-worker-cache-fallback\": \"0.6.2\",\n    \"ember-service-worker-index\": \"0.7.2\",\n    \"ember-service-worker-update-notify\": \"3.0.0\",\n    \"ember-sinon\": \"5.0.0\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-statecharts\": \"0.13.2\",\n    \"ember-template-lint\": \"3.5.0\",\n    \"ember-test-helpers-extra\": \"github:NullVoxPopuli/ember-test-helpers-extra\",\n    \"ember-test-selectors\": \"5.0.0\",\n    \"ember-test-waiters\": \"2.1.3\",\n    \"ember-web-app\": \"5.0.1\",\n    \"git-rev-sync\": \"3.0.1\",\n    \"loader.js\": \"4.7.0\",\n    \"qunit-assertions-extra\": \"0.8.5\",\n    \"qunit-console-grouper\": \"0.3.0\",\n    \"qunit-dom\": \"1.6.0\",\n    \"qunit-xstate-test\": \"0.1.0\",\n    \"stylelint\": \"13.13.1\",\n    \"testdouble\": \"3.16.1\",\n    \"testem-failure-only-reporter\": \"0.0.1\",\n    \"ts-node\": \"9.1.1\",\n    \"typescript\": \"4.3.4\",\n    \"webpack-bundle-analyzer\": \"4.4.2\",\n    \"yn\": \"4.0.0\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"config/environment\": [\n        \"app/config/environment\"\n      ],\n      \"*\": [\n        \"declarations/*\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/public/.well-known/assetlinks.json",
    "content": "[\n  {\n    \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n    \"target\": {\n      \"namespace\": \"android_app\",\n      \"package_name\": \"io.emberclear\",\n      \"sha256_cert_fingerprints\": [\"6E:70:B5:30:63:17:C9:C4:E7:E0:CD:95:E3:52:AA:69:85:38:B3:FC:15:61:1C:A7:49:F6:36:15:71:66:3B:30\"]\n    }\n  }\n]\n"
  },
  {
    "path": "client/web/emberclear/public/assets/images/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/emberclear/public/assets/images/icons/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/emberclear/public/bundle.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <meta name=\"referrer\" content=\"no-referrer\">\n  <meta name=\"robots\" content=\"index, follow\" />\n\n  <title>emberclear build stats</title>\n\n  <link rel=\"stylesheet\" href=\"https://cdn.shoelace.style/1.0.0-beta24/shoelace.css\">\n</head>\n<body class='pad-xl'>\n  <h1>emberclear build stats</h1>\n  <br>\n  <br>\n\n\n  <ul>\n    <li>\n      <a href='/bundle/broccoli.html'>\n        Broccoli Build\n      </a>\n    </li>\n\n    <!--\n      Waiting on ESBuild for bundle analyzer\n    <li>\n      <a href='/bundle/crypto.html'>\n        Crypto Web Worker Bundle\n      </a>\n    </li>\n    -->\n\n    <li>\n      <a href='/bundle/ember-auto-import.html'>\n        Ember Auto Import (Webpack) Chunks\n      </a>\n    </li>\n  </ul>\n\n</body>\n</html>\n"
  },
  {
    "path": "client/web/emberclear/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/emberclear/scripts/analyze-broccoli.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst fs = require('fs');\nconst { createOutput, summarizeAll } = require('broccoli-concat-analyser');\n\n/**\n * Inspiration / stolen from:\n *   https://github.com/kaliber5/ember-cli-bundle-analyzer/pull/70\n *\n */\nasync function analyzeBroccoli() {\n  let ignoredFiles = [\n    'ember.js',\n    'ember-testing.js',\n    'tests.js',\n    'test-support.js',\n    'test-support.css',\n    '*-test.js',\n    '*.out.json',\n  ];\n\n  let outputPath = path.join(process.cwd(), 'concat-stats-for');\n\n  process.env.CONCAT_STATS_PATH = outputPath;\n\n  await summarizeAll(outputPath, ignoredFiles);\n\n  let content = createOutput(outputPath);\n\n  fs.writeFileSync(path.join(outputPath, 'index.html'), content);\n}\n\nanalyzeBroccoli();\n"
  },
  {
    "path": "client/web/emberclear/scripts/analyze.sh",
    "content": "#!/bin/bash\n\nR=\"\\e[31m\"\nY=\"\\e[33m\"\nN=\"\\e[0m\"\n\n# outputs the following files:\n# - concats-stats-for/\n#   - #-emberclear.js.json\n#   - #-vendor.js.json\n#   - #-vendor.css.json\n#   - ember-auto-import.json\n#\nrm -rf public/bundle\n# ember-auto-import.json was not a part of\n# broccoli-concat-analyzer, so that file needs\n# to be stitched into the index.html file\necho -e \"${Y}Building App with Stats${N}\"\n# echo -e \"${Y}Outputs:${N}\"\n# echo -e \"${Y}- dist/bundle/crypto.html${N}\"\n# ^ There is currently now esbuild bundle analyzer\nCONCAT_STATS=true yarn build:production\n\n# begin analysis\necho -e \"${Y}Analyzing Broccoli Output${N}\"\necho -e \"${Y}Outputs:${N}\"\necho -e \"${Y}- concat-stats-for/index.html${N}\"\necho -e \"${Y}- concat-stats-for/ember-auto-import.html${N}\"\nmkdir -p concat-stats-for/\nnode ./scripts/analyze-broccoli.js\n\n# copy to public folder for deployment\n# (we build again without CONCAT_STATS)\necho -e \"${Y}Copying HTML Analysis files to public/bundle for later deployment/${N}\"\nmkdir -p ./public/bundle\ncp ./concat-stats-for/index.html ./public/bundle/broccoli.html\ncp ./concat-stats-for/ember-auto-import.html ./public/bundle/\n# cp ./dist/bundle/* ./public/bundle/\n"
  },
  {
    "path": "client/web/emberclear/scripts/docker/nginx.conf",
    "content": "server {\n    listen       ${NGINX_LISTENING_PORT};\n    server_name  localhost;\n\n    gzip on;\n    gzip_disable \"msie6\";\n\n    gzip_vary on;\n    gzip_proxied any;\n    gzip_comp_level 6;\n    gzip_buffers 16 8k;\n    gzip_http_version 1.1;\n    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd+api.json;\n\n    location / {\n        root   /emberclear;\n\n        try_files $uri /index.html;\n    }\n}\n"
  },
  {
    "path": "client/web/emberclear/scripts/docker/run-nginx.sh",
    "content": "#!/usr/bin/env sh\n\nexport NGINX_LISTENING_PORT=${PORT:-80}\nexport VARS_TO_REPLACE='$NGINX_LISTENING_PORT'\n\nif [ \"$NGINX_CONF_DIR\" = \"\" ]\nthen\n\tNGINX_CONF_DIR=/etc/nginx/conf.d\nfi\n\nenvsubst \"$VARS_TO_REPLACE\" < $NGINX_CONF_DIR/default.conf.template > $NGINX_CONF_DIR/default.conf\ncat $NGINX_CONF_DIR/default.conf\n\necho \"Starting emberclear...\"\nnginx -g 'daemon off;'\n"
  },
  {
    "path": "client/web/emberclear/scripts/generate-self-signed-cert.sh",
    "content": "#!/bin/bash\n\nopenssl genrsa \\\n  -des3 \\\n  -passout pass:fakepassword \\\n  -out local.pass.key 2048\n\nopenssl rsa \\\n  -passin pass:fakepassword \\\n  -in local.pass.key \\\n  -out local.key\n\nrm local.pass.key\n\nopenssl req \\\n  -new \\\n  -key local.key \\\n  -out local.csr \\\n  -subj \"/C=EA/ST=Earth/L=Earth/O=emberclear/OU=local-dev/CN=localhost\"\n\nopenssl x509 \\\n  -req \\\n  -sha256 \\\n  -days 365 \\\n  -in local.csr \\\n  -signkey local.key \\\n  -out local.crt\n\n# openssl req \\\n#   -newkey rsa:4096 \\\n#   -new \\\n#   -nodes \\\n#   -x509 \\\n#   -days 3650 \\\n#   -out local.crt \\\n#   -keyout local.key \\\n#   -subj \"/C=EA/ST=Earth/L=Earth/O=emberclear/OU=local-dev/CN=localhost\"\n"
  },
  {
    "path": "client/web/emberclear/scripts/node_modules-to-ramdisk.sh",
    "content": "#!/bin/bash\n\nmkdir -p $PWD/dist\nmkdir -p $PWD/node_modules\n\nsudo mount -t tmpfs -o rw,size=50M tmpfs $PWD/dist\nsudo mount -t tmpfs -o rw,size=1G tmpfs $PWD/node_modules\n\n# have to re-install, because node_modules is now empty\nyarn\n"
  },
  {
    "path": "client/web/emberclear/scripts/test-with-coverage.sh",
    "content": "#!/bin/bash\n\nexport GIT_COMMIT_SHA=$GITHUB_SHA\nexport GIT_BRANCH=${GITHUB_REF#refs/heads/}\nexport COVERAGE=true\n\ntime yarn test\n\nexit_code=$?\n\n# fix the paths, since we aren't generating coverage from\n# the root of the mono repo\nsed -i -E \"s/^SF:(.+)$/SF:client\\/web\\/emberclear\\/\\1/\" coverage/lcov.info\necho \"Successfully fixed the test coverage paths!\"\n\nexit $exit_code\n"
  },
  {
    "path": "client/web/emberclear/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/emberclear/tests/-temp/qunit-xstate-test.ts",
    "content": "/* eslint-disable */\nimport { module, test } from 'qunit';\nimport { TestModel } from '@xstate/test';\nimport { TestPlan } from '@xstate/test/lib/types';\nimport { TestContext as EmberTestContext } from 'ember-test-helpers';\n\ntype TestCallbackFn<TestContext, TContext, TReturn> = (\n  this: EmberTestContext,\n  assert: Assert,\n  path: TestPlan<TestContext, TContext>['paths'][0]\n) => TReturn;\n\nexport const testShortestPaths = <TestContext, TContext, TReturn>(\n  testModel: TestModel<TestContext, TContext>,\n  testCallback: TestCallbackFn<TestContext, TContext, TReturn>\n) => {\n  testModel.getShortestPathPlans().forEach((plan) => {\n    module(plan.description, function () {\n      plan.paths.forEach((path) => {\n        test(path.description, function (assert) {\n          return testCallback.bind(this)(assert, path);\n        });\n      });\n    });\n  });\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/chat/-unread-acceptance-test.ts",
    "content": "import { waitFor } from '@ember/test-helpers';\nimport { module, skip, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page, selectors } from 'emberclear/tests/helpers/pages/app';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Chat', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n  setupWorkers(hooks);\n  setupCurrentUser(hooks);\n\n  module('Unread Messages', function (hooks) {\n    hooks.beforeEach(async function () {\n      // we can't receive unread messages from ourselves\n      // so start on that screen\n      await visit('/chat/privately-with/me');\n    });\n\n    // TODO: this indicator is a mobile only thing, so..\n    //       maybe we need some sort of breakpoint testing?\n    test('when there are 0 messages', function (assert) {\n      assert.notOk(page.headerUnread.isPresent, 'indicator is rendered');\n    });\n\n    module('Has unread messages', function (hooks) {\n      hooks.beforeEach(async function () {\n        const store = getService('store');\n        const record = store.createRecord('message', {\n          target: 'whatever',\n          type: 'not ping',\n          body: 'a test message',\n          to: 'me',\n          readAt: null,\n        });\n\n        await record.save();\n        await waitFor(selectors.headerUnread);\n      });\n\n      // this can only show up when the window doesn't have focus?\n      // maybe?\n      skip('1 message is unread', function (assert) {\n        assert.ok(page.headerUnread.isPresent, 'indicator is rendered');\n        assert.ok(\n          page.headerUnread.text!.includes('1'),\n          `has one unread message. detected text: ${page.headerUnread.text}`\n        );\n\n        percySnapshot(assert as any);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/chat/acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Chat', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/chat');\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/chat/in-channel/-acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest, skip } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { createChannel } from 'emberclear/tests/helpers/factories/channel-factory';\nimport { page } from 'emberclear/tests/helpers/pages/chat';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nimport type { Channel } from '@emberclear/local-account';\n\nmodule('Acceptance | Chat | Privately With', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n\n  module('is not logged in', function (hooks) {\n    setupRelayConnectionMocks(hooks);\n\n    hooks.beforeEach(async function () {\n      await visit('/chat/in-channel');\n    });\n\n    test('document.title is unchanged', async function (assert) {\n      assert.ok(document.title.startsWith('emberclear'));\n    });\n  });\n\n  module('is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    module('channel does not exist', function (hooks) {\n      setupRelayConnectionMocks(hooks);\n\n      hooks.beforeEach(async function () {\n        await visit('/chat/in-channel/nowhere');\n      });\n\n      test('redirects', async function (assert) {\n        await toast.waitForToast();\n\n        assert.equal(currentURL(), '/chat');\n        assert.contains(toast.text, 'not be located');\n      });\n    });\n\n    module('channel exists', function (hooks) {\n      setupRelayConnectionMocks(hooks);\n\n      let channel!: Channel;\n\n      hooks.beforeEach(async function () {\n        channel = await createChannel('Vertical Flat Plates');\n\n        await visit(`/chat/in-channel/${channel.id}`);\n      });\n\n      skip('does not redirect', function (assert) {\n        assert.equal(currentURL(), `/chat/in-channel/${channel.id}`);\n        assert.equal(page.messages.length, 0);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/chat/privately-with/-acceptance-test.ts",
    "content": "import { currentURL, settled, triggerEvent, waitFor } from '@ember/test-helpers';\nimport { waitUntil } from '@ember/test-helpers';\nimport { module, skip, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { timeout } from 'ember-concurrency';\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { createMessage } from 'emberclear/tests/helpers/factories/message-factory';\nimport { page, selectors } from 'emberclear/tests/helpers/pages/chat';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { createContact } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nimport type { Contact } from '@emberclear/local-account';\nimport type { Message } from '@emberclear/networking';\n\nmodule('Acceptance | Chat | Privately With', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n\n  module('is not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/chat/privately-with');\n    });\n\n    test('document.title is unchanged', async function (assert) {\n      assert.ok(document.title.startsWith('emberclear'));\n    });\n  });\n\n  module('is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    test('document.title is unchanged', async function (assert) {\n      assert.ok(document.title.startsWith('emberclear'));\n    });\n\n    module('anyone', function (hooks) {\n      setupRelayConnectionMocks(hooks);\n\n      hooks.beforeEach(async function () {\n        await visit('/chat/privately-with');\n      });\n\n      test('document.title is properly changed', async function (assert) {\n        assert.equal(document.title, 'emberclear');\n      });\n    });\n\n    module('yourself', function (hooks) {\n      setupRelayConnectionMocks(hooks);\n\n      hooks.beforeEach(async function () {\n        await visit('/chat/privately-with/me');\n      });\n\n      test('page renders with default states', function (assert) {\n        assert.equal(currentURL(), '/chat/privately-with/me');\n\n        assert.notOk(page.textarea.isDisabled, 'textarea is enabled');\n        assert.equal(page.submitButton.isDisabled, 'disabled', 'submit button is disabled');\n        assert.equal(page.messages.length, 0, 'history is blank');\n      });\n\n      test('there are 0 messages to start with', function (assert) {\n        assert.equal(page.messages.length, 0);\n      });\n\n      module('text is entered', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.textarea.fillIn('a message');\n        });\n\n        test('the chat button is not disabled', function (assert) {\n          assert.notOk(page.submitButton.isDisabled);\n        });\n\n        module('submit is clicked', function (hooks) {\n          hooks.beforeEach(async function () {\n            page.submitButton.click();\n            await waitFor(selectors.submitButton + '[disabled]');\n          });\n\n          test('inputs are disabled', function (assert) {\n            // assert.equal(chat.messages.all().length, 0, 'history is blank');\n            // assert.ok(chat.textarea.isDisabled(), 'textarea is disabled');\n            assert.equal(page.submitButton.isDisabled, 'disabled', 'submitButton is disabled');\n\n            percySnapshot(assert as any);\n          });\n        });\n\n        module('enter is pressed', function (hooks) {\n          hooks.beforeEach(async function () {\n            triggerEvent(selectors.form, 'submit');\n            await waitFor(selectors.submitButton + '[disabled]');\n          });\n\n          test('inputs are disabled', function (assert) {\n            assert.equal(page.submitButton.isDisabled, 'disabled', 'submitButton is disabled');\n\n            percySnapshot(assert as any);\n          });\n        });\n      });\n    });\n\n    module('someone that does not exist', function (hooks) {\n      setupRelayConnectionMocks(hooks);\n\n      hooks.beforeEach(async function () {\n        await visit('/chat/privately-with/nobody');\n      });\n\n      test('redirects', async function (assert) {\n        await toast.waitForToast();\n\n        assert.equal(currentURL(), '/chat');\n      });\n\n      test('a message is displayed', async function (assert) {\n        await toast.waitForToast();\n\n        assert.contains(toast.text, 'not be located');\n\n        percySnapshot(assert as any);\n      });\n    });\n\n    module('someone else', function (hooks) {\n      let someone!: Contact;\n      let id!: string;\n\n      hooks.beforeEach(async function () {\n        someone = await createContact('someone else');\n        id = someone.id;\n      });\n\n      module('when first visiting the page', function (hooks) {\n        setupRelayConnectionMocks(hooks);\n\n        hooks.beforeEach(async function () {\n          await visit(`/chat/privately-with/${id}`);\n        });\n\n        test('does not redirect', function (assert) {\n          assert.equal(currentURL(), `/chat/privately-with/${id}`);\n        });\n\n        test('there are 0 messages to start with', function (assert) {\n          assert.equal(page.messages.length, 0);\n        });\n\n        test('document.title is changed', async function (assert) {\n          assert.equal(document.title, `${someone.name} | emberclear`);\n        });\n      });\n\n      module('the person is not online', function (hooks) {\n        setupRelayConnectionMocks(hooks, {\n          send() {\n            // this error comes from the relay\n            throw {\n              reason: `user with id ${id} not found!`,\n              ['to_uid']: id,\n            };\n          },\n        });\n\n        hooks.beforeEach(async function () {\n          await visit(`/chat/privately-with/${id}`);\n        });\n\n        module('a message is sent', function (hooks) {\n          hooks.beforeEach(async function () {\n            await page.textarea.fillIn('a message');\n            page.submitButton.click();\n          });\n\n          module('when the message first shows up in the chat history', function (hooks) {\n            hooks.beforeEach(async function () {\n              await waitFor(selectors.confirmations);\n            });\n\n            test('the message is shown, but is waiting for a confirmation', async function (assert) {\n              let { confirmations } = page.messages.objectAt(0)!;\n\n              assert.ok(confirmations.isLoading, 'a loader is rendererd');\n              assert.notContains(confirmations.text, 'could not be delivered');\n\n              percySnapshot(assert as any);\n\n              await settled();\n            });\n          });\n\n          module('the view has settled', function (hooks) {\n            hooks.beforeEach(async function () {\n              // waiting on network stuff\n              // impossible to tie in to test waiters\n              await waitUntil(() => page.messages.objectAt(0)!.confirmations.isLoading);\n              await settled();\n            });\n\n            test('there is 1 message in the history window', function (assert) {\n              assert.equal(page.messages.length, 1);\n            });\n\n            test('the message is shown, but with an error', function (assert) {\n              let { confirmations } = page.messages.objectAt(0)!;\n\n              assert.notOk(confirmations.isLoading, 'loader is no longer present');\n              assert.ok(confirmations.text.includes('could not be delivered'));\n\n              percySnapshot(assert as any);\n            });\n\n            module('resend is clicked', function () {\n              skip('implement tests for resending');\n            });\n\n            module('auto-resend is clicked', function (hooks) {\n              hooks.beforeEach(async function () {\n                // eslint-disable-next-line no-console\n                await page.messages.objectAt(0)!.confirmations.autosend();\n              });\n\n              test('the message is queued for resend', async function (assert) {\n                const store = getService('store');\n                const messages = await store.query('message', { queueForResend: true });\n\n                assert.equal(messages.length, 1, 'there should only be one queued message');\n\n                percySnapshot(assert as any);\n              });\n\n              test('the confirmation action area shows that autosend is now pending', function (assert) {\n                const text = page.messages.objectAt(0)!.confirmations.text;\n\n                assert.notOk(\n                  text.match(/resend automatically/),\n                  'does not show the resend automatically link'\n                );\n\n                assert.ok(text.match(/autosend pending/), 'shows that autosend is pending');\n              });\n            });\n          });\n        });\n      });\n\n      module('a message is sent to the person', function (hooks) {\n        setupRelayConnectionMocks(hooks, {\n          send() {\n            // should something be asserted here?\n          },\n        });\n\n        hooks.beforeEach(async function () {\n          await visit(`/chat/privately-with/${id}`);\n          await page.textarea.fillIn('a message');\n          page.submitButton.click();\n        });\n\n        module('when the message shows up in the chat history', function (hooks) {\n          hooks.beforeEach(async function () {\n            await waitFor(selectors.message);\n          });\n\n          test('the message is shown, but is waiting for a confirmation', function (assert) {\n            let { confirmations } = page.messages.objectAt(0)!;\n\n            assert.ok(confirmations.isLoading, 'a loader is rendererd');\n            assert.notContains(confirmations.text, 'could not be delivered');\n\n            percySnapshot(assert as any);\n          });\n        });\n\n        module('the view has been settled', function (hooks) {\n          hooks.beforeEach(async function () {\n            await settled();\n          });\n\n          test('there is 1 message in the history window', function (assert) {\n            assert.equal(page.messages.length, 1);\n\n            percySnapshot(assert as any);\n          });\n\n          module('a confirmation is received', function () {\n            skip('the message is shown, with successful confirmation', function () {});\n          });\n        });\n      });\n\n      module('scrolling', function (hooks) {\n        setupRelayConnectionMocks(hooks);\n\n        // let firstMessage: Message;\n        let lastMessage: Message;\n\n        module('there are no messages', function (hooks) {\n          hooks.beforeEach(async function () {\n            await visit(`/chat/privately-with/${id}`);\n          });\n\n          test('UI elements are configured appropriately', function (assert) {\n            assert.false(page.isScrollable(), 'is not scrollable');\n            assert.true(page.newMessagesFloater.isHidden, 'new messages floater is not shown');\n          });\n        });\n\n        module('there are many messages', function (hooks) {\n          let numMessages = 50;\n\n          hooks.beforeEach(async function (assert) {\n            let currentUser = getService('current-user').record!;\n\n            for (let i = 0; i < numMessages; i++) {\n              let message = await createMessage(\n                someone,\n                currentUser,\n                `Test Message\\n\n                Line 2\\n\n                A third`\n              );\n\n              if (i === numMessages - 1) {\n                lastMessage = message;\n              }\n            }\n\n            let store = getService('store');\n            let messages = store.peekAll('message');\n\n            assert.equal(messages.length, numMessages, 'messages are created');\n\n            await visit(`/chat/privately-with/${id}`);\n            // because scrollIntoView doesn't tie in to the test waiters?\n            // TODO: make this happen with a special version of scroll in to view\n            await timeout(1000);\n            // for some reason scroll events aren't triggered unless a message is deleted?\n            // but only while testing?\n            // NOTE: don't delete the last message, because we test that it exists\n            await page.messages[page.messages.length - 2].confirmations.delete();\n          });\n\n          test('most recent messages are shown', async function (assert) {\n            assert.true(page.isScrollable(), 'is scrollable');\n\n            assert.true(page.newMessagesFloater.isHidden, 'more messages below is not visible');\n            // TODO: Investigate the implementation of isNotVisible\n            //assert.dom(`[data-id=\"${firstMessage.id}\"]`).isNotVisible();\n            assert.dom(`[data-id=\"${lastMessage.id}\"]`).exists();\n            assert.dom(page.unreadMessagesFloater.scope).doesNotExist();\n          });\n\n          module('after scrolling up a bit', function (hooks) {\n            hooks.beforeEach(async function () {\n              page.scroll(-400);\n              // for animations\n              await timeout(400);\n              await settled();\n            });\n\n            test('the more messages floater is visible', function (assert) {\n              assert.false(page.newMessagesFloater.isHidden);\n            });\n\n            module('after clicking the new messages floater', function (hooks) {\n              hooks.beforeEach(async function () {\n                await page.newMessagesFloater.click();\n                await timeout(1400);\n                await settled();\n              });\n\n              skip('most recent messages are shown', async function (assert) {\n                assert.equal(\n                  page.newMessagesFloater.isHidden,\n                  true,\n                  'more messages below is not visible'\n                );\n              });\n            });\n          });\n        });\n\n        module('there are many unread messages', function (hooks) {\n          hooks.beforeEach(async function () {\n            // let currentUser = getService('current-user').record!;\n            // let numMessages = 30;\n            // for (let i = 0; i < numMessages; i++) {\n            //   await createMessage(currentUser, someone, 'Test Message');\n            // }\n            // let store = getService('store');\n            // let messages = store.peekAll('message');\n            // assert.equal(messages.length, numMessages, 'messages are created');\n            // await visit(`/chat/privately-with/${id}`);\n            // await this.pauseTest();\n          });\n\n          skip('the unread above floater appears', function (assert) {\n            assert.dom(page.unreadMessagesFloater.scope).exists();\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/chat/privately-with/format-code-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { stripIndent } from 'common-tags';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page } from 'emberclear/tests/helpers/pages/chat';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { createContact } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nimport type { Contact } from '@emberclear/local-account';\n\nlet codeA = stripIndent`\n  \\`\\`\\`ts\n  let forA = 2;\n  \\`\\`\\`\n`;\n\nlet codeB = stripIndent`\n  \\`\\`\\`ts\n  let forB = 3;\n  \\`\\`\\`\n`;\n\nasync function submitCodeTo(code: string, to: Contact, assert: Assert) {\n  await visit(`/chat/privately-with/${to.id}`);\n  await page.textarea.fillIn(code);\n  await page.submitButton.click();\n\n  let { hasCode } = page.messages.objectAt(0)!;\n\n  assert.ok(hasCode, `code for ${to.name} is present`);\n}\n\nmodule('Acceptance | Chat | Privately With | format-code', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n\n  module('is logged in with two contacts', function (hooks) {\n    setupCurrentUser(hooks);\n    setupRelayConnectionMocks(hooks, {\n      send() {\n        // should something be asserted here?\n      },\n    });\n\n    let contactA!: Contact;\n    let contactB!: Contact;\n\n    hooks.beforeEach(async function () {\n      contactA = await createContact('A Contact A');\n      contactB = await createContact('B Contact B');\n    });\n\n    module('a message with code is sent to Contact A & B', function (hooks) {\n      hooks.beforeEach(async function (assert) {\n        await submitCodeTo(codeA, contactA, assert);\n        await submitCodeTo(codeB, contactB, assert);\n      });\n\n      module('when navigating back to A', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit(`/chat/privately-with/${contactA.id}`);\n        });\n\n        test('the chat history still renders the code snippet', function (assert) {\n          let { hasCode } = page.messages.objectAt(0)!;\n\n          assert.ok(hasCode, 'code for A is present');\n        });\n\n        module('when navigating back to B', function (hooks) {\n          hooks.beforeEach(async function () {\n            await visit(`/chat/privately-with/${contactB.id}`);\n          });\n\n          test('the chat history still renders the code snippet', function (assert) {\n            let { hasCode } = page.messages.objectAt(0)!;\n\n            assert.ok(hasCode, 'code for B is present');\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/compatibility-test.ts",
    "content": "import { find } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Compatibility', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  // stub things that may not exist (esp in headless / c.i.)\n  hooks.beforeEach(function () {\n    window.ServiceWorker = window.ServiceWorker || 'for testing';\n  });\n\n  module('the browser supports all required features', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/');\n    });\n\n    test('the compatibility message is not shown', function (assert) {\n      let modal = find('[data-test-compatibility-modal]');\n\n      assert.notOk(modal, 'the modal should not be in the dom');\n    });\n  });\n\n  module('the browser does not support a required feature', function (hooks) {\n    let backupDb: any;\n\n    hooks.beforeEach(async function () {\n      backupDb = window.indexedDB;\n      delete (window as any).indexedDB;\n      await visit('/');\n    });\n    hooks.afterEach(function () {\n      (window as any).indexedDB = backupDb;\n    });\n\n    test('the compatibility message is shown', function (assert) {\n      let modal = find('[data-test-compatibility-modal]');\n\n      assert.ok(modal, 'the modal should be in the dom');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/contacts/acceptance-test.ts",
    "content": "import { currentURL, settled, waitUntil } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { contacts } from 'emberclear/tests/helpers/pages/contacts';\n\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { createContact } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nimport type { User } from '@emberclear/local-account';\n\nmodule('Acceptance | Contacts', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/contacts');\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n\n  module('Is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    test('visiting /contacts | does not redirect', async function (assert) {\n      await visit('/contacts');\n\n      assert.equal(currentURL(), '/contacts');\n      percySnapshot(assert as any);\n    });\n\n    module('a couple contacts exist', function (hooks) {\n      let me: User;\n\n      hooks.beforeEach(async function () {\n        me = getService('current-user').record!;\n\n        await createContact('First Contact');\n        await createContact('Second Contact');\n\n        await visit('/contacts');\n      });\n\n      test('there are two contacts', function (assert) {\n        const result = contacts.rows.dom().length;\n\n        assert.equal(result, 2);\n        percySnapshot(assert as any);\n      });\n\n      test('current user does not show up in the contacts', function (assert) {\n        const text = contacts.table()!.textContent;\n        const myName = me.name!;\n\n        assert.notOk(text!.includes(myName));\n      });\n\n      module('a contact is removed', function (hooks) {\n        hooks.beforeEach(async function () {\n          await contacts.rows.removeAt(1);\n          await settled();\n          // TODO: find a better way to do this\n          await waitUntil(() => contacts.rows.dom().length === 1);\n        });\n\n        test('there is one less contact', function (assert) {\n          const result = contacts.rows.dom().length;\n\n          assert.equal(result, 1);\n          percySnapshot(assert as any);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/invite/acceptance-test.ts",
    "content": "import { currentURL, waitFor } from '@ember/test-helpers';\nimport { settled } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { selectors as chatSelectors } from 'emberclear/tests/helpers/pages/chat';\nimport { completedPage, nameForm } from 'emberclear/tests/helpers/pages/setup';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nimport type RedirectManager from 'emberclear/services/redirect-manager';\n\nmodule('Acceptance | Invitations', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('Is not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/invite?name=Test&publicKey=abcdef123456');\n    });\n\n    test('a redirect to setup occurs', function (assert) {\n      assert.equal(currentURL(), '/setup');\n      percySnapshot(assert as any);\n    });\n\n    test('there is now a pending redirect', function (assert) {\n      const redirect = getService('redirect-manager') as RedirectManager;\n\n      assert.ok(redirect.hasPendingRedirect);\n    });\n\n    test('the url is stored in localstorage for use later', function (assert) {\n      const redirect = getService('redirect-manager');\n      const url = redirect.attemptedRoute;\n\n      assert.equal(url, '/invite?name=Test&publicKey=abcdef123456');\n    });\n\n    module('the user fills in their name and proceeds', function (hooks) {\n      hooks.beforeEach(async function () {\n        await nameForm.enterName('My Name');\n        await nameForm.clickNext();\n        await waitFor('[data-test-setup-mnemonic]');\n      });\n\n      test('redirect is still pending', function (assert) {\n        const redirect = getService('redirect-manager');\n        const url = redirect.attemptedRoute;\n\n        assert.ok(redirect.hasPendingRedirect, 'redirect is pending');\n        assert.equal(url, '/invite?name=Test&publicKey=abcdef123456', 'url is present');\n      });\n\n      module('the user clicks passed the mnemonic screen', function (hooks) {\n        hooks.beforeEach(async function () {\n          await completedPage.clickNext();\n          await waitFor(chatSelectors.form);\n          await settled();\n        });\n\n        test('the redirect has been evaluated', function (assert) {\n          assert.equal(currentURL(), '/chat/privately-with/abcdef123456');\n          percySnapshot(assert as any);\n        });\n\n        test('the redirect manager has been cleared', function (assert) {\n          const redirect = getService('redirect-manager');\n          const url = redirect.attemptedRoute;\n\n          assert.notOk(redirect.hasPendingRedirect, 'redirect is not pending');\n          assert.equal(url, null, 'url is no longer present');\n        });\n      });\n    });\n  });\n\n  module('Is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    module('the url does not have the required params', function () {\n      module('name is missing from a contact invitation', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/invite?publicKey=whatever');\n          await toast.waitForToast();\n        });\n\n        test('a redirect to chat occurs', function (assert) {\n          assert.equal(currentURL(), '/chat');\n        });\n\n        test('a toast is displayed with an error', function (assert) {\n          assert.contains(toast.text, 'Invalid Invite Link');\n          percySnapshot(assert as any);\n        });\n      });\n\n      module('publicKey is missing from a contact invitation', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/invite?name=Test');\n          await toast.waitForToast();\n        });\n\n        test('a redirect to chat occurs', function (assert) {\n          assert.equal(currentURL(), '/chat');\n        });\n\n        test('a toast is displayed with an error', function (assert) {\n          assert.contains(toast.text, 'Invalid Invite Link');\n          percySnapshot(assert as any);\n        });\n      });\n    });\n\n    module('the url has the required params for a contact invitation', function () {\n      module('the params are invalid', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/invite?name=Test&publicKey=abz');\n          await toast.waitForToast();\n        });\n\n        test('a redirect to chat occurs', function (assert) {\n          assert.equal(currentURL(), '/chat');\n        });\n\n        test('a toast is displayed with an error', function (assert) {\n          assert.contains(toast.text, 'There was a problem');\n          percySnapshot(assert as any);\n        });\n      });\n\n      module('the params are valid', function () {\n        module('but the user clicks their own contact invite link', function (hooks) {\n          hooks.beforeEach(async function () {\n            const identity = getService('current-user');\n            const record = identity.record;\n            const { name, publicKeyAsHex } = record!;\n\n            await visit(`/invite?name=${name}&publicKey=${publicKeyAsHex}`);\n            await toast.waitForToast();\n          });\n\n          test('a redirect to your own chat occurs', function (assert) {\n            assert.equal(currentURL(), '/chat/privately-with/me');\n            percySnapshot(assert as any);\n          });\n\n          test('a toast is displayed with a warning', function (assert) {\n            assert.contains(toast.text, `You can't invite yourself...`);\n          });\n        });\n\n        module('the params belong to a different user', function (hooks) {\n          const escapedName = 'Test%20User';\n          const publicKey = '53edcbe7d1cdd289e9f4ea74eab12c6dd78720124efd9ad331d6e174aae5677c';\n\n          hooks.beforeEach(async function () {\n            const url = `/invite?name=${escapedName}&publicKey=${publicKey}`;\n\n            await visit(url);\n          });\n\n          test('a redirect to the correct direct message chat', function (assert) {\n            assert.expect(1);\n\n            assert.equal(currentURL(), `/chat/privately-with/${publicKey}`);\n            percySnapshot(assert as any);\n          });\n\n          test('the contact is added to the list of contacts', async function (assert) {\n            const store = getService('store');\n\n            const record = await store.findRecord('contact', publicKey);\n\n            assert.ok(record);\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/login/acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupRelayConnectionMocks, trackAsyncDataRequests } from 'emberclear/tests/helpers';\nimport { loginForm } from 'emberclear/tests/helpers/pages/login';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { samplePrivateKey, setupWorkers } from '@emberclear/crypto/test-support';\nimport { mnemonicFromNaClBoxPrivateKey } from '@emberclear/crypto/workers/crypto/utils/mnemonic';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nconst behaviors = {\n  invalid: {\n    clickLogin() {\n      module('the login button is clicked', function (hooks) {\n        hooks.beforeEach(async function () {\n          // loginForm.submit must not be awaited because it\n          // calls an ember-concurrency task which will\n          // also be awaited and not allow us to test the\n          // side-effects\n          loginForm.submit();\n          await toast.waitForToast();\n        });\n\n        test('an error message appears', function (assert) {\n          const expected = 'There was a problem logging in...';\n\n          assert.contains(toast.text, expected);\n          percySnapshot(assert as any);\n        });\n\n        test('navigation does not occur', function (assert) {\n          assert.equal(currentURL(), '/login');\n        });\n      });\n    },\n  },\n};\n\nmodule('Acceptance | Login', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n  trackAsyncDataRequests(hooks);\n\n  module('is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    module('visits /login', function (hooks) {\n      hooks.beforeEach(async function () {\n        await visit('/login');\n      });\n\n      test('redirects', function (assert) {\n        assert.equal(currentURL(), '/');\n      });\n    });\n  });\n\n  module('is not logged in and visits /login', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/login');\n    });\n\n    test('is not redirected', function (assert) {\n      assert.equal(currentURL(), '/login');\n      percySnapshot(assert as any);\n    });\n\n    behaviors.invalid.clickLogin();\n\n    module('the name field is filled in', function (hooks) {\n      hooks.beforeEach(async function () {\n        await loginForm.typeName('NullVoxPopuli');\n      });\n\n      behaviors.invalid.clickLogin();\n    });\n\n    module('the mnemonic is filled in', function (hooks) {\n      hooks.beforeEach(async function () {\n        await loginForm.typeMnemonic('this is fake');\n      });\n\n      behaviors.invalid.clickLogin();\n    });\n\n    module('both name and mnemonic are filled in', function () {\n      module('with valid values', function (hooks) {\n        hooks.beforeEach(async function () {\n          const mnemonic = await mnemonicFromNaClBoxPrivateKey(samplePrivateKey);\n\n          await loginForm.typeName('NullVoxPopuli');\n          await loginForm.typeMnemonic(mnemonic);\n          await loginForm.submit();\n        });\n\n        test('redirects to chat', function (assert) {\n          assert.equal(currentURL(), '/chat');\n          percySnapshot(assert as any);\n        });\n\n        test('sets the \"me\" user', function (assert) {\n          const store = getService('store');\n          const known = store.peekAll('user');\n\n          assert.equal(known.length, 1);\n          assert.equal(known.toArray()[0].id, 'me');\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/logout/acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Logout', function (hooks) {\n  setupApplicationTest(hooks);\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/logout');\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/logout-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { page } from 'emberclear/components/app/top-nav/user-drop-menu/-page';\nimport {\n  assertExternal,\n  setupEmberclearTest,\n  setupRelayConnectionMocks,\n} from 'emberclear/tests/helpers';\nimport { page as logoutPage } from 'emberclear/tests/helpers/pages/logout';\n\nimport { setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { stubService, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Logout', function (hooks) {\n  setupApplicationTest(hooks);\n  setupEmberclearTest(hooks);\n\n  module('When not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      stubService('current-user', {\n        isLoggedIn: false,\n        load() {},\n        exists: () => false,\n      });\n\n      try {\n        await visit('/logout');\n      } catch (e) {\n        console.error('hi', e);\n      }\n    });\n\n    test('redirects to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n      assertExternal(assert as any);\n    });\n  });\n\n  module('When logged in', function (hooks) {\n    setupCurrentUser(hooks);\n    setupRelayConnectionMocks(hooks);\n\n    hooks.beforeEach(async function () {\n      await visit('/');\n    });\n\n    module('user dropdown is open', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.toggle();\n      });\n\n      module('clicking logout', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.logout();\n        });\n\n        test('navigates to the logout warning page', function (assert) {\n          assert.equal(currentURL(), '/logout');\n          assertExternal(assert as any);\n        });\n\n        module('confirm logout', function (hooks) {\n          hooks.beforeEach(async function () {\n            await logoutPage.confirmLogout();\n          });\n\n          test('the user is logged out', function (assert) {\n            assert.equal(currentURL(), '/');\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/navigation-scroll-to-top-test.ts",
    "content": "import { triggerEvent } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { timeout } from 'ember-concurrency';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { app } from 'emberclear/tests/helpers/pages/app';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nconst scrollContainer = '.mobile-menu-wrapper';\n\nmodule('Acceptance | Navigation Scrolling', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  hooks.beforeEach(async function () {\n    await visit('/');\n  });\n\n  module('When in a short viewport', function (hooks) {\n    hooks.beforeEach(function () {\n      (document.querySelector('.ember-application') as any).style = 'height: 300px';\n    });\n\n    hooks.afterEach(function () {\n      (document.querySelector('.ember-application') as any).style = '';\n    });\n\n    module('When scrolled to the bottom', function (hooks) {\n      hooks.beforeEach(async function () {\n        app.footer.faq().scrollIntoView(false);\n\n        await triggerEvent(window as any, 'scroll');\n        // wait for scroll animation\n        await timeout(250);\n      });\n\n      test('the top of the page is not visible', function (assert) {\n        const position = document.querySelector(scrollContainer)!.scrollTop;\n\n        assert.notEqual(position, 0, 'the scroll container is not at the top');\n      });\n\n      module('Clicking to another page', function (hooks) {\n        hooks.beforeEach(async function () {\n          await app.footer.clickFaq();\n        });\n\n        test('the top of the page is visible', function (assert) {\n          const position = document.querySelector(scrollContainer)!.scrollTop;\n\n          assert.equal(position, 0, 'the scroll container is at the top');\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/notification-permission-prompt-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page as app } from 'emberclear/tests/helpers/pages/app';\nimport { stubConnection } from 'emberclear/tests/helpers/setup-relay-connection-mocks';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, refresh, visit } from '@emberclear/test-helpers/test-support';\n\nconst { notificationPrompt: prompt } = app;\n\nmodule('Acceptance | Notification Permission Prompt', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  setupRelayConnectionMocks(hooks);\n  clearLocalStorage(hooks);\n  setupCurrentUser(hooks);\n\n  module('permission has not yet been asked for', function (hooks) {\n    hooks.beforeEach(async function () {\n      getService<any>('window').Notification = { permission: 'default' };\n\n      await visit('/chat/privately-with/me');\n    });\n\n    test('the prompt is shown', function (assert) {\n      assert.true(prompt.isVisible);\n    });\n\n    test('never ask again is clicked', async function (assert) {\n      assert.expect(2);\n\n      await prompt.askNever();\n\n      assert.false(prompt.isVisible, 'prompt hides initially');\n\n      await refresh(() => stubConnection());\n\n      assert.false(prompt.isVisible, 'still is not shown even after refresh');\n    });\n\n    module('ask later is clicked', function (hooks) {\n      hooks.beforeEach(async function () {\n        await prompt.askLater();\n      });\n\n      test('the prompt is not shown', function (assert) {\n        assert.false(prompt.isVisible);\n      });\n\n      module('on refresh', function (hooks) {\n        hooks.beforeEach(async function () {\n          await refresh(() => {\n            stubConnection();\n            // stub doesn't hold between refreshes\n            getService<any>('window').Notification = { permission: 'default' };\n          });\n        });\n\n        test('the prompt is shown', function (assert) {\n          assert.true(prompt.isVisible);\n        });\n      });\n    });\n\n    module('enabled is clicked', function () {});\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/notifications-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nimport type Notifications from 'emberclear/services/notifications';\n\nmodule('Acceptance | Notifications Service', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('permission has not yet been asked for', function (hooks) {\n    let notifications!: Notifications;\n\n    hooks.beforeEach(async function () {\n      await visit('/');\n\n      getService<any>('window').Notification = {\n        permission: 'default',\n      };\n\n      notifications = getService('notifications');\n    });\n\n    module('is logged in', function (hooks) {\n      setupCurrentUser(hooks);\n\n      module('visits the setup route', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/setup');\n        });\n\n        test('notifications should not be shown', function (assert) {\n          assert.ok(\n            notifications.isOnRouteThatDoesNotShowNotifications,\n            'the logout route does not allow notifications'\n          );\n\n          assert.notOk(notifications.showInAppPrompt, 'The in-app prompt should not be shown');\n        });\n      });\n\n      module('visits the logout page', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/logout');\n        });\n\n        test('notifications should not be shown', function (assert) {\n          assert.ok(\n            notifications.isOnRouteThatDoesNotShowNotifications,\n            'the logout route does not allow notifications'\n          );\n\n          assert.notOk(notifications.showInAppPrompt, 'The in-app prompt should not be shown');\n        });\n      });\n\n      module('visits the chat route', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('chat');\n        });\n\n        module('permission: undecided', function () {\n          test('initial checks', function (assert) {\n            getService<any>('window').Notification = { permission: 'default' };\n\n            let service = getService('notifications');\n\n            assert.ok(\n              service.isBrowserCapableOfNotifications,\n              'Browser is capable of notifications'\n            );\n            assert.notOk(service.isPermissionDenied, 'Permission has not previously been denied');\n            assert.notOk(service.isPermissionGranted, 'Permission has not previously been granted');\n            assert.notOk(service.isNeverGoingToAskAgain, 'User did not say to never ask again');\n            assert.notOk(service.isHiddenUntilBrowserRefresh, 'User did not say to ask later');\n            assert.ok(service.showInAppPrompt, 'The in-app prompt should be shown right away');\n          });\n        });\n\n        module('permission: denied', function () {\n          test('service state checks', function (assert) {\n            getService<any>('window').Notification = { permission: 'denied' };\n\n            let service = getService('notifications');\n\n            assert.ok(\n              service.isBrowserCapableOfNotifications,\n              'Browser is capable of notifications'\n            );\n            assert.ok(service.isPermissionDenied, 'Permission has been denied');\n            assert.notOk(service.isPermissionGranted, 'Permission has not previously been granted');\n            assert.notOk(service.isNeverGoingToAskAgain, 'User did not say to never ask again');\n            assert.notOk(service.isHiddenUntilBrowserRefresh, 'User did not say to ask later');\n            assert.notOk(service.showInAppPrompt, 'The in-app prompt should not be shown');\n          });\n        });\n\n        module('permission: granted', function () {\n          test('initial checks', function (assert) {\n            getService<any>('window').Notification = { permission: 'granted' };\n            let service = getService('notifications');\n\n            assert.ok(\n              service.isBrowserCapableOfNotifications,\n              'Browser is capable of notifications'\n            );\n            assert.notOk(service.isPermissionDenied, 'Permission has not previously been denied');\n            assert.ok(service.isPermissionGranted, 'Permission has been granted');\n            assert.notOk(service.isNeverGoingToAskAgain, 'User did not say to never ask again');\n            assert.notOk(service.isHiddenUntilBrowserRefresh, 'User did not say to ask later');\n            assert.notOk(\n              service.showInAppPrompt,\n              'The in-app prompt should not be shown right away'\n            );\n          });\n        });\n      });\n    });\n\n    module('permission denied', function (hooks) {\n      hooks.beforeEach(function () {\n        getService<any>('window').Notification = { permission: 'denied' };\n      });\n\n      module('a notification is attempted', function (hooks) {\n        hooks.beforeEach(async function () {\n          notifications.info('a test message');\n          await toast.waitForToast();\n        });\n\n        test('a toast is displayed', function (assert) {\n          assert.contains(toast.text, 'a test message');\n        });\n      });\n    });\n\n    // module('permission granted', function(hooks) {});\n\n    // module('permission: later', function(hooks) {});\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/qr/login/receiver-test.ts",
    "content": ""
  },
  {
    "path": "client/web/emberclear/tests/acceptance/qr/login/sender-test.ts",
    "content": "import Service from '@ember/service';\nimport { settled } from '@ember/test-helpers';\nimport { module } from 'qunit';\nimport { setupXStateTest } from 'qunit-xstate-test';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { createModel } from '@xstate/test';\nimport { scanQR } from 'ember-jsqr/test-support';\nimport RSVP from 'rsvp';\nimport { Machine } from 'xstate';\n\nimport { testShortestPaths } from 'emberclear/tests/-temp/qunit-xstate-test';\nimport { setupEmberclearTest } from 'emberclear/tests/helpers';\nimport { page } from 'emberclear/tests/helpers/pages/qr';\n\nimport { setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nimport type ApplicationInstance from '@ember/application/instance';\nimport type { TestContext } from 'ember-test-helpers';\n\ninterface TestMachineContext {\n  assert: Assert;\n  owner: ApplicationInstance;\n  t: Intl['t'];\n  connection: {\n    fakeTransfer: RSVP.Deferred<void>;\n    setup: RSVP.Deferred<void>;\n    transferToDevice: () => Promise<void>;\n  };\n}\n\nconst testModel = createModel<TestMachineContext, EmptyRecord>(\n  Machine({\n    id: 'login-test',\n    initial: 'begin',\n    states: {\n      begin: {\n        on: {\n          SCAN_LOGIN_VALID: 'scannedValidLoginQR',\n          SCAN_LOGIN_INVALID: 'begin',\n          SCAN_INVALID: 'begin',\n        },\n        meta: {\n          async test({ assert, t }: TestMachineContext) {\n            await settled();\n            assert.notContains(page.text, t('qrCode.waitingForCamera'));\n\n            assert.equal(page.error.isPresent, false, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, true, 'Scanner');\n          },\n        },\n      },\n      scannedValidLoginQR: {\n        on: {\n          CONNECTION_SUCCESS: 'establishedConnection',\n          CONNECTION_FAILURE: 'connectionFailed',\n        },\n        meta: {\n          async test({ assert, t }: TestMachineContext) {\n            await settled();\n            assert.contains(page.text, t('ui.login.transfer.establishConnection'));\n\n            assert.equal(page.error.isPresent, false, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n      establishedConnection: {\n        on: {\n          CLICK_ALLOW: 'transferData',\n          CLICK_DENY: 'userDeniedTransfer',\n        },\n        meta: {\n          async test({ assert, t }: TestMachineContext) {\n            await settled();\n            assert.contains(page.text, t('ui.login.verify.title'));\n            assert.contains(page.confirm.code, 'AB12');\n\n            assert.equal(page.error.isPresent, false, 'Error Message');\n            assert.equal(page.confirm.isPresent, true, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n      connectionFailed: {\n        meta: {\n          async test({ assert }: TestMachineContext) {\n            await settled();\n\n            assert.equal(page.error.isPresent, true, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n      transferData: {\n        on: {\n          TRANSFER_SUCCESS: 'transferSuccessful',\n          TRANSFER_FAILED: 'transferFailed',\n        },\n        meta: {\n          async test({ assert, t }: TestMachineContext) {\n            assert.contains(page.text, t('ui.login.transfer.inProgress'));\n          },\n        },\n      },\n      userDeniedTransfer: {\n        meta: {\n          async test({ assert }: TestMachineContext) {\n            await settled();\n\n            assert.equal(page.error.isPresent, true, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n      transferSuccessful: {\n        meta: {\n          async test({ assert, t }: TestMachineContext) {\n            await settled();\n\n            assert.contains(page.text, t('ui.login.transfer.success'));\n\n            assert.equal(page.error.isPresent, false, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n      transferFailed: {\n        meta: {\n          async test({ assert }: TestMachineContext) {\n            await settled();\n\n            assert.equal(page.error.isPresent, true, 'Error Message');\n            assert.equal(page.confirm.isPresent, false, 'Confirm Prompt');\n            assert.equal(page.unknownState.isPresent, false, 'Unknown Error');\n            assert.equal(page.scanner.isPresent, false, 'Scanner');\n          },\n        },\n      },\n    },\n  })\n).withEvents({\n  ////////////////////////////////////////////////////\n  // SCANNING\n  SCAN_LOGIN_VALID: {\n    async exec({ owner }) {\n      await scanQR(owner, ['login', { pub: 'abcdef123', verify: 'AB12' }]);\n    },\n  },\n  SCAN_LOGIN_INVALID: {\n    async exec({ owner }) {\n      scanQR(owner, ['login', 'abcdef123']);\n    },\n  },\n  SCAN_INVALID: {\n    async exec({ owner }) {\n      scanQR(owner, { obj: 'not allowed' });\n    },\n  },\n  // END SCANNING\n  ////////////////////////////////////////////////////\n  // CONNECTING\n  CONNECTION_SUCCESS: {\n    async exec({ connection }) {\n      connection.setup.resolve();\n    },\n  },\n  CONNECTION_FAILURE: {\n    async exec({ connection }) {\n      connection.setup.reject('Some Error');\n    },\n  },\n  // CONNECTING\n  ////////////////////////////////////////////////////\n  // Authorization from User\n  CLICK_ALLOW: {\n    async exec() {\n      await page.confirm.clickAllow();\n    },\n  },\n  CLICK_DENY: {\n    async exec() {\n      await page.confirm.clickDeny();\n    },\n  },\n  // Authorization from User\n  ////////////////////////////////////////////////////\n  // TRANSFERRING\n  TRANSFER_SUCCESS: {\n    async exec({ connection }) {\n      connection.fakeTransfer.resolve();\n    },\n  },\n  TRANSFER_FAILED: {\n    async exec({ connection }) {\n      connection.fakeTransfer.reject();\n    },\n  },\n  // TRANSFERRING\n  ////////////////////////////////////////////////////\n});\n\nmodule('Acceptance | QR | Login | Sender', function (hooks) {\n  setupApplicationTest(hooks);\n  setupEmberclearTest(hooks);\n\n  let fakeTransfer: RSVP.Deferred<void>;\n  let fakeConnection: TestMachineContext['connection'];\n\n  hooks.beforeEach(function (this: TestContext) {\n    fakeTransfer = RSVP.defer<void>();\n    fakeConnection = {\n      fakeTransfer,\n      setup: RSVP.defer<void>(),\n      transferToDevice() {\n        return fakeTransfer.promise;\n      },\n    };\n\n    class TestNavigator extends Service {\n      get mediaDevices(): any {\n        return {\n          getUserMedia() {\n            return Promise.resolve(\n              {\n                getTracks: () => [],\n              } /* fake cameraStream */\n            );\n          },\n        };\n      }\n    }\n\n    class TestQRManager extends Service {\n      login = {\n        async setupConnection() {\n          await fakeConnection.setup.promise;\n\n          return fakeConnection;\n        },\n      };\n    }\n\n    this.owner.register('service:browser/navigator', TestNavigator);\n    this.owner.register('service:qr-manager', TestQRManager);\n  });\n\n  module('User is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n    setupXStateTest(hooks, testModel);\n\n    testShortestPaths(testModel, async function (assert, path) {\n      await visit('/qr');\n\n      let intl = getService('intl');\n      let t = intl.t.bind(intl);\n\n      return path.test({\n        t,\n        assert,\n        owner: this.owner,\n        connection: fakeConnection,\n      });\n    });\n  });\n\n  module('User is not logged in', function () {});\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/search-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { page } from 'emberclear/components/search/-page';\nimport { setupRelayConnectionMocks, trackAsyncDataRequests } from 'emberclear/tests/helpers';\nimport { page as app } from 'emberclear/tests/helpers/pages/app';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport {\n  clearLocalStorage,\n  createContact,\n  setupCurrentUser,\n} from '@emberclear/local-account/test-support';\nimport { getStore, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Search Modal', function (hooks) {\n  setupApplicationTest(hooks);\n  trackAsyncDataRequests(hooks);\n  setupWorkers(hooks);\n  setupCurrentUser(hooks);\n  setupRelayConnectionMocks(hooks);\n  clearLocalStorage(hooks);\n\n  module('there are more results than what are initially displayed', function (hooks) {\n    hooks.beforeEach(async function (assert) {\n      await createContact(`Contact #1`);\n      await createContact(`Contact #2`);\n      await createContact(`Contact #3`);\n      await createContact(`Contact #4`);\n      await createContact(`Contact #5`);\n      await createContact(`Contact #6`);\n      await createContact(`Contact #7`);\n      await createContact(`Contact #8`);\n      await createContact(`Contact #9`);\n      await createContact(`Contact #10`);\n\n      let contacts = await getStore().findAll('contact');\n\n      assert.equal(contacts.length, 10);\n\n      await visit('/');\n      await app.modals.search.open();\n    });\n\n    test('there is a max set of contacts rendered', function (assert) {\n      assert.equal(page.results.contacts.links.length, 5);\n    });\n\n    module('filtering', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.input.fillIn('1');\n      });\n\n      test('the list is shorter', function (assert) {\n        assert.equal(page.results.contacts.links.length, 2);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/settings/acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, skip, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { settings } from 'emberclear/tests/helpers/pages/settings';\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getService, getStore, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Settings', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/settings');\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n\n  module('when logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    hooks.beforeEach(async function (assert) {\n      await visit('/settings');\n\n      assert.equal(currentURL(), '/settings');\n    });\n\n    module('Changing Name', function () {\n      module('name field changes to some other text', function (hooks) {\n        const newName = 'whatever, this is a test or something';\n\n        hooks.beforeEach(async function () {\n          await settings.fillNameField(newName);\n          await settings.save();\n        });\n\n        test('the name has changed', function (assert) {\n          const service = getService('current-user');\n          const actual = service.name;\n\n          assert.equal(actual, newName);\n        });\n\n        skip('confirmation is display', function (assert) {\n          assert.contains(toast.text, 'Identity Updated');\n        });\n      });\n    });\n\n    module('Downloading settings', function () {\n      // TODO: how to test downloads?\n    });\n\n    module('Messages exist', function (hooks) {\n      hooks.beforeEach(async function (assert) {\n        const store = getStore();\n\n        await store.createRecord('message', {}).save();\n        await store.createRecord('message', {}).save();\n\n        const messages = await store.findAll('message');\n\n        assert.equal(messages.length, 2);\n      });\n\n      module('Clicking the Delete Messages button', function (hooks) {\n        hooks.beforeEach(async function () {\n          await visit('/settings/danger-zone');\n          await settings.deleteMessages();\n        });\n\n        test('deletes the messages', async function (assert) {\n          const messages = await getStore().findAll('message');\n\n          assert.equal(messages.length, 0);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/settings/danger-zone/-acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page as settings } from 'emberclear/tests/helpers/pages/settings';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nconst page = settings.dangerZone;\n\nmodule('Acceptance | Settings | Danger Zone', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  let path = '/settings/danger-zone';\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit(path);\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n\n  module('when logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    hooks.beforeEach(async function () {\n      await visit(path);\n    });\n\n    test('delete messages button is visible', function (assert) {\n      assert.ok(page.deleteMessages.isVisible, 'button is visible');\n    });\n\n    module('Showing the private key', function () {\n      test('key is not shown by default', function (assert) {\n        assert.notOk(page.privateKey.isPresent);\n      });\n\n      module('Show private key is clicked', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.togglePrivateKey();\n        });\n\n        test('the private key is shown', function (assert) {\n          assert.ok(page.privateKey.isPresent);\n          assert.ok(page.privateKey.text);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/settings/interface/-acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page as settings } from 'emberclear/tests/helpers/pages/settings';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nconst page = settings.interface;\n\nmodule('Acceptance | Settings | Relays', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  let path = '/settings/interface';\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit(path);\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n\n  module('when logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    hooks.beforeEach(async function () {\n      await visit(path);\n    });\n\n    test('options are visible', function (assert) {\n      assert.ok(page.isVisible, 'option is visible');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/settings/relays/-acceptance-test.ts",
    "content": "import { currentURL, waitUntil } from '@ember/test-helpers';\nimport { settled } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page as settings } from 'emberclear/tests/helpers/pages/settings';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { defaultRelays } from '@emberclear/networking/required-data';\nimport { getService, visit } from '@emberclear/test-helpers/test-support';\n\nconst page = settings.relays;\n\nmodule('Acceptance | Settings | Relays', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  let path = '/settings/relays';\n\n  module('when not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit(path);\n    });\n\n    test('is redirected to setup', function (assert) {\n      assert.equal(currentURL(), '/setup');\n    });\n  });\n\n  module('when logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    hooks.beforeEach(async function () {\n      const store = getService('store');\n\n      await store.createRecord('relay', defaultRelays[0]).save();\n      await store.createRecord('relay', defaultRelays[1]).save();\n      await store.createRecord('relay', defaultRelays[2]).save();\n      await visit(path);\n    });\n\n    test('the default relays are rendered', function (assert) {\n      assert.equal(page.table.rows.length, 3, '1 row per relay');\n    });\n\n    // TODO: convert these to integration tests\n    module('user removes a relay', function (hooks) {\n      hooks.beforeEach(async function (assert) {\n        assert.equal(page.table.rows.length, 3, 'there are 3 relays');\n\n        await page.table.rows.objectAt(1)!.remove();\n        await settled();\n        // TODO: find a way to make this better\n        await waitUntil(() => page.table.rows.length === 2);\n      });\n\n      test('there is one less row', function (assert) {\n        assert.equal(page.table.rows.length, 2, 'there are 2 relays');\n      });\n    });\n\n    // TODO: convert these to integration tests\n    module('user clicks add relay', function (hooks) {\n      hooks.beforeEach(async function (assert) {\n        assert.notOk(page.form.isVisible, 'form is not visible');\n        await page.addRelay();\n      });\n\n      test('the form appears', function (assert) {\n        assert.ok(page.form.isVisible, 'form is visible');\n      });\n\n      module('user saves the relay', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.form.fillSocket('socket url');\n          await page.form.fillOg('og url');\n          await page.form.save();\n          await waitUntil(() => !page.form.isVisible);\n        });\n\n        test('the form hides', function (assert) {\n          assert.notOk(page.form.isVisible, 'form is not visible');\n        });\n\n        test('there is now one additional relay in the table', function (assert) {\n          assert.equal(page.table.rows.length, 4, '1 row per relay');\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/setup/acceptance-test.ts",
    "content": "import { currentURL } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { setupEmberclearTest, trackAsyncDataRequests } from 'emberclear/tests/helpers';\nimport { nameForm, overwritePage } from 'emberclear/tests/helpers/pages/setup';\n\nimport { setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { getStore, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Setup', function (hooks) {\n  setupApplicationTest(hooks);\n  setupEmberclearTest(hooks);\n  trackAsyncDataRequests(hooks);\n\n  module('is logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    module('visits /setup', function (hooks) {\n      hooks.beforeEach(async function () {\n        await visit('/setup');\n      });\n\n      test('redirects to warning', function (assert) {\n        let text = this.owner.lookup('service:intl').t('ui.setup.overwriteTitle');\n\n        assert.dom('[data-test-focus-card]').containsText(text);\n      });\n\n      module('desires to navigate away', function (hooks) {\n        hooks.beforeEach(async function () {\n          await overwritePage.abort();\n        });\n\n        test('redirect to root', function (assert) {\n          assert.equal(currentURL(), '/');\n        });\n      });\n\n      module('confirms re-setup', function (hooks) {\n        hooks.beforeEach(async function () {\n          await overwritePage.confirm();\n        });\n\n        test('redirect to main setup', function (assert) {\n          let text = this.owner.lookup('service:intl').t('ui.setup.introQuestion');\n\n          assert.dom('[data-test-focus-card]').containsText(text);\n\n          assert.equal(currentURL(), '/setup');\n        });\n      });\n    });\n  });\n\n  module('visiting /setup', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/setup');\n    });\n\n    module('name is not filled in', function () {\n      test('proceeding is disallowed', async function (assert) {\n        await nameForm.clickNext();\n\n        let text = this.owner.lookup('service:intl').t('ui.setup.almostReady');\n\n        assert.dom('[data-test-focus-card]').doesNotContainText(text);\n      });\n\n      test('no record was created', async function (assert) {\n        const store = getStore();\n        const known = await store.findAll('identity');\n\n        assert.equal(known.length, 0);\n      });\n    });\n\n    module('name is filled in', function (hooks) {\n      hooks.beforeEach(async function () {\n        await nameForm.enterName('My Name');\n\n        await nameForm.clickNext();\n      });\n\n      test('proceeds to next page', function (assert) {\n        let text = this.owner.lookup('service:intl').t('ui.setup.almostReady');\n\n        assert.dom('[data-test-focus-card]').containsText(text);\n\n        percySnapshot(assert as any);\n      });\n\n      test('sets the \"me\" identity', function (assert) {\n        const store = getStore();\n        const known = store.peekAll('user');\n\n        assert.equal(known.length, 1);\n        assert.equal(known.toArray()[0].id, 'me');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/sidebar-test.ts",
    "content": "import { currentURL, settled, waitFor, waitUntil } from '@ember/test-helpers';\nimport { module, skip, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { page, selectors } from 'emberclear/components/app/off-canvas/-page';\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { createMessage } from 'emberclear/tests/helpers/factories/message-factory';\nimport { page as settings } from 'emberclear/tests/helpers/pages/settings';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport {\n  clearLocalStorage,\n  getCurrentUser,\n  setupCurrentUser,\n} from '@emberclear/local-account/test-support';\nimport { createContact } from '@emberclear/local-account/test-support';\nimport { getService, getStore, visit } from '@emberclear/test-helpers/test-support';\n\nimport type { Contact } from '@emberclear/local-account';\n\nmodule('Acceptance | Sidebar', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n  setupCurrentUser(hooks);\n\n  let t: Intl['t'];\n\n  hooks.beforeEach(async function () {\n    await visit('/chat');\n    await page.toggle();\n\n    let intl = getService('intl');\n\n    t = intl.t.bind(intl);\n  });\n\n  module('Tabs', function (hooks) {\n    hooks.beforeEach(async function () {\n      await visit('/chat?_features=democracy-ui');\n    });\n\n    test('default tab is contacts', function (assert) {\n      let content = page.sidebar.header.text;\n\n      assert.equal(content, t('ui.sidebar.contacts.title'));\n    });\n\n    module('switch tab to channels', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.sidebar.selectChannelsTab();\n      });\n\n      test('channels tab is displayed', async function (assert) {\n        let content = page.sidebar.header.text;\n\n        assert.equal(content, t('ui.sidebar.channels.title'));\n      });\n    });\n\n    module('switch tab to actions', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.sidebar.selectActionsTab();\n      });\n\n      test('actions tab is displayed', async function (assert) {\n        let content = page.sidebar.header.text;\n\n        assert.equal(content, t('ui.sidebar.actions.title'));\n      });\n    });\n\n    module('switch tab to contacts', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.sidebar.selectContactsTab();\n      });\n\n      test('contacts tab is displayed', function (assert) {\n        let content = page.sidebar.contacts.header.text;\n\n        assert.equal(content, t('ui.sidebar.contacts.title'));\n      });\n    });\n  });\n\n  module('Search', function () {\n    module('There are contacts', function (hooks) {\n      let contacts!: Contact[];\n\n      hooks.beforeEach(async function () {\n        contacts = [\n          await createContact('Contact AA'),\n          await createContact('Contact BB'),\n          await createContact('Contact BBC'),\n        ];\n      });\n\n      test('all contacts are visible', function (assert) {\n        assert.expect(3);\n\n        let content = page.sidebar.contacts.listText;\n\n        for (let contact of contacts) {\n          assert.contains(content, contact.name);\n        }\n      });\n\n      module('1 letter is entered', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.sidebar.search('A');\n        });\n\n        test('help text is shown', function (assert) {\n          assert.contains(page.sidebar.searchInfo, t('ui.sidebar.keepTyping', { num: 1 }));\n        });\n\n        test('all contacts are still visible', function (assert) {\n          assert.expect(3);\n\n          let content = page.sidebar.contacts.listText;\n\n          for (let contact of contacts) {\n            assert.contains(content, contact.name);\n          }\n        });\n      });\n\n      module('2 letters are entered', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.sidebar.search('BB');\n        });\n\n        test('help text is shown', function (assert) {\n          assert.contains(page.sidebar.searchInfo, t('ui.sidebar.results', { num: 2 }));\n        });\n\n        test('Matches are shown', function (assert) {\n          let content = page.sidebar.contacts.listText;\n\n          assert.notContains(content, 'AA');\n          assert.contains(content, 'BB');\n          assert.contains(content, 'BBC');\n        });\n      });\n\n      module('offline contacts are hidden', function (hooks) {\n        hooks.beforeEach(async function () {\n          await setupOfflineContactsTest();\n        });\n\n        module('1 letter is entered', function (hooks) {\n          hooks.beforeEach(async function () {\n            await page.sidebar.search('B');\n          });\n        });\n\n        module('2 letters are entered', function (hooks) {\n          hooks.beforeEach(async function () {\n            await page.sidebar.search('B');\n          });\n        });\n      });\n    });\n  });\n\n  module('Contacts', function () {\n    module('the add contact button is clicked', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.sidebar.contacts.header.clickAdd();\n      });\n\n      test('a navigation occurred', function (assert) {\n        assert.equal(currentURL(), '/add-friend');\n      });\n    });\n\n    module('the actual list of contacts', function () {\n      module('there are 0 contacts', function () {\n        test('only the current user is shown', async function (assert) {\n          const name = getService('current-user')!.name!;\n          const content = page.sidebar.contacts.list.map((c: any) => c.text).join();\n\n          assert.equal(content, name);\n        });\n\n        test('offline count does not show', function (assert) {\n          assert.notOk(page.sidebar.contacts.offlineCount.isVisible);\n        });\n      });\n\n      module('there is 1 contact', function (hooks) {\n        hooks.beforeEach(async function () {\n          await createContact('first contact');\n        });\n\n        test('there are 2 rows of names', function (assert) {\n          assert.equal(page.sidebar.contacts.list.length, 2);\n        });\n\n        test('offline count does not show', function (assert) {\n          assert.notOk(page.sidebar.contacts.offlineCount.isVisible, 'offline count is shown');\n        });\n\n        module('pinned contact are to be shown', function (hooks) {\n          hooks.beforeEach(async function () {\n            await page.sidebar.contacts.list[1].pin();\n            await visit('/settings/interface');\n            await settings.ui.toggleHideOfflineContacts();\n          });\n\n          test('both contacts should be shown', function (assert) {\n            assert.equal(page.sidebar.contacts.list.length, 2, 'two users in the contacts list');\n          });\n\n          test('offline count does not show', function (assert) {\n            assert.notOk(page.sidebar.contacts.offlineCount.isVisible, 'offline count is shown');\n          });\n\n          test('pinned contact should appear below current user', async function (assert) {\n            const contacts = page.sidebar.contacts.list;\n\n            assert.equal(contacts[0].name, 'Test User');\n            assert.equal(contacts[1].name, 'first contact');\n          });\n        });\n\n        module('offline contacts are to be hidden', function (hooks) {\n          hooks.beforeEach(async function () {\n            await setupOfflineContactsTest();\n          });\n\n          test('only the current user is shown', function (assert) {\n            assert.expect(2);\n            onlyCurrentUserIsShownTest(assert);\n          });\n\n          test('offline count is shown', function (assert) {\n            const result = page.sidebar.contacts.offlineCount.text;\n\n            assert.matches(result, /1/);\n          });\n        });\n      });\n\n      module('there are 2 contacts', function (hooks) {\n        let firstContact!: Contact;\n        let secondContact!: Contact;\n\n        hooks.beforeEach(async function () {\n          firstContact = await createContact('first contact');\n          secondContact = await createContact('second contact');\n        });\n\n        test('there are 3 rows of names', function (assert) {\n          assert.equal(page.sidebar.contacts.list.length, 3, 'there are 3 contacts');\n        });\n\n        module('pinned contacts are to be shown', function (hooks) {\n          hooks.beforeEach(async function () {\n            const contacts = page.sidebar.contacts.list;\n\n            await contacts[1].pin();\n            await contacts[2].pin();\n            await visit('/settings/interface');\n            await settings.ui.toggleHideOfflineContacts();\n          });\n\n          test('all contacts should be shown', function (assert) {\n            assert.equal(page.sidebar.contacts.list.length, 3, 'three users in the contacts list');\n          });\n\n          test('two contacts should be shown and one hidden', async function (assert) {\n            const contacts = page.sidebar.contacts;\n\n            await contacts.list[1].pin();\n            assert.equal(contacts.list.length, 2, 'two users in the contacts list');\n            assert.matches(contacts.offlineCount.text, /1/);\n          });\n\n          test('offline count does not show', function (assert) {\n            assert.notOk(page.sidebar.contacts.offlineCount.isVisible, 'offline count is shown');\n          });\n\n          test('pinned contacts should appear above offline contacts', async function (assert) {\n            const contacts = page.sidebar.contacts.list;\n\n            assert.equal(contacts[0].name, 'Test User');\n            assert.equal(contacts[1].name, 'first contact');\n            assert.equal(contacts[2].name, 'second contact');\n\n            await contacts[1].pin();\n            await visit('/settings/interface');\n            await settings.ui.toggleHideOfflineContacts();\n\n            assert.equal(contacts[0].name, 'Test User');\n            assert.equal(contacts[1].name, 'second contact');\n            assert.equal(contacts[2].name, 'first contact');\n          });\n        });\n\n        module('offline contacts are to be hidden', function (hooks) {\n          hooks.beforeEach(async function () {\n            await setupOfflineContactsTest();\n          });\n\n          test('only the current user is shown', function (assert) {\n            assert.expect(4);\n\n            onlyCurrentUserIsShownTest(assert);\n\n            const content = page.sidebar.contacts.listText;\n\n            assert.notContains(content, firstContact.name);\n            assert.notContains(content, secondContact.name);\n          });\n\n          test('offline count is shown', function (assert) {\n            const result = page.sidebar.contacts.offlineCount.text;\n\n            assert.matches(result, /2/);\n          });\n\n          module(`but one of them has sent us a message we haven't read`, function (hooks) {\n            hooks.beforeEach(async function () {\n              await createMessage(getCurrentUser()!, firstContact, 'test', {\n                readAt: undefined,\n              });\n            });\n\n            skip('the unread message causes the person to be shown', function (assert) {\n              const content = page.sidebar.contacts.listText;\n\n              assert.contains(content, firstContact.name);\n            });\n          });\n        });\n      });\n\n      module('there are enough contacts to scroll', function (hooks) {\n        hooks.before(async function () {\n          // TODO: these need implementing\n          // Need a way to set the window size\n        });\n      });\n    });\n  });\n\n  module('Channels', function () {\n    test('the channel form is not visible', function (assert) {\n      const form = page.sidebar.channels.form.isVisible;\n\n      assert.notOk(form);\n    });\n\n    test('there are 0 channels', async function (assert) {\n      const store = getStore();\n      const known = await store.findAll('channel');\n\n      assert.equal(known.length, 0);\n    });\n\n    module('the add channel button is clicked', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.sidebar.channels.toggleForm();\n      });\n\n      skip('the channel form is now visible', function (assert) {\n        const form = page.sidebar.channels.form.isVisible;\n\n        assert.ok(form);\n      });\n\n      module('the cancel button is clicked', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.sidebar.channels.toggleForm();\n          await settled();\n        });\n\n        skip('the channel form is not visible', function (assert) {\n          const form = page.sidebar.channels.form.isVisible;\n\n          assert.notOk(form);\n        });\n      });\n\n      module('the channel form is submitted', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.sidebar.channels.form.fill('Vertical Flat Plates');\n          await page.sidebar.channels.form.submit();\n          await settled();\n        });\n\n        skip('the form becomes hidden', async function (assert) {\n          // TODO: figure out why settled state doesn't capture this behavior\n          await waitUntil(() => !page.sidebar.channels.form.isVisible);\n          const form = page.sidebar.channels.form.isVisible;\n\n          assert.notOk(form);\n        });\n\n        skip('a channel is created', function (assert) {\n          const store = getService('store');\n          const known = store.peekAll('channel');\n\n          assert.equal(known.length, 1);\n          assert.equal(known.firstObject?.name, 'Vertical Flat Plates');\n        });\n      });\n    });\n  });\n});\n\nasync function setupOfflineContactsTest() {\n  await visit('/settings/interface');\n  await settings.ui.toggleHideOfflineContacts();\n  await waitFor(selectors.offlineCount);\n}\n\nfunction onlyCurrentUserIsShownTest(assert: Assert) {\n  const name = getService('current-user')!.name!;\n  const content = page.sidebar.contacts.listText;\n\n  assert.contains(content, name);\n  assert.equal(page.sidebar.contacts.list.length, 1, 'one user in the contacts list');\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/sidebar-visibility-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { percySnapshot } from 'ember-percy';\n\nimport { page } from 'emberclear/components/app/off-canvas/-page';\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage, setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { stubService, visit } from '@emberclear/test-helpers/test-support';\n\nmodule('Acceptance | Sidebar Visibility', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  clearLocalStorage(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  module('When not logged in', function (hooks) {\n    hooks.beforeEach(async function () {\n      stubService('current-user', {\n        isLoggedIn: false,\n        load() {},\n        exists: () => false,\n      });\n\n      await visit('/');\n    });\n\n    test('the sidebar is not visible', function (assert) {\n      assert.notOk(page.isPresent);\n      percySnapshot(assert as any);\n    });\n  });\n\n  module('When logged in', function (hooks) {\n    setupCurrentUser(hooks);\n\n    hooks.beforeEach(async function () {\n      await visit('/');\n    });\n\n    test('the sidebar is visible', function (assert) {\n      assert.ok(page.isPresent);\n      percySnapshot(assert as any);\n    });\n\n    test('the sidebar is not open', function (assert) {\n      assert.notOk(page.isOpen);\n    });\n\n    module('the sidebar hamburger is clicked', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.toggle();\n      });\n\n      test('the sidebar is open', function (assert) {\n        assert.ok(page.isOpen);\n        percySnapshot(assert as any);\n      });\n\n      module('the sidebar close button is clicked', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.toggle();\n        });\n\n        test('the sidebar is closed', function (assert) {\n          assert.notOk(page.isOpen);\n          percySnapshot(assert as any);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/acceptance/update-banner-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport {\n  serviceWorkerUpdate,\n  setupServiceWorkerUpdater,\n} from 'ember-service-worker-update-notify/test-support/updater';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { visit } from '@emberclear/test-helpers/test-support';\n\nconst selector = '.service-worker-update-notify';\n\nmodule('Acceptance | Update Banner', function (hooks) {\n  setupApplicationTest(hooks);\n  setupWorkers(hooks);\n  setupServiceWorkerUpdater(hooks);\n  setupRelayConnectionMocks(hooks);\n\n  hooks.beforeEach(async function () {\n    await visit('/');\n  });\n\n  test('the notifier is not visible', function (assert) {\n    assert.dom(selector).doesNotExist();\n  });\n\n  module('an update is ready', function () {\n    test('the notifier can become visible', async function (assert) {\n      await serviceWorkerUpdate();\n\n      assert.dom(selector).exists();\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/factories/channel-factory.ts",
    "content": "import { getService } from '@emberclear/test-helpers/test-support';\n\nexport async function createChannel(name: string, attributes = {}) {\n  let store = getService('store');\n\n  let record = store.createRecord('channel', {\n    name,\n    // channels aren't implemented, so I don't know\n    // what other defaults would be good here\n    ...attributes,\n  });\n\n  await record.save();\n\n  return record;\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/factories/message-factory.ts",
    "content": "import { TARGET, TYPE } from '@emberclear/networking/models/message';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nimport type { Identity } from '@emberclear/local-account';\n\nexport async function createMessage(to: Identity, from: Identity, body: string, attributes = {}) {\n  let store = getService('store');\n\n  let record = store.createRecord('message', {\n    body,\n    to: to.uid,\n    from: from.uid,\n    type: TYPE.CHAT,\n    target: TARGET.WHISPER,\n    sentAt: new Date(),\n    receivedAt: new Date(),\n    readAt: undefined,\n    queueForResend: false,\n    sender: from,\n    ...attributes,\n  });\n\n  await record.save();\n\n  return record;\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/index.ts",
    "content": "// import a11yAuditIf from 'ember-a11y-testing/test-support/audit-if';\nimport { percySnapshot } from 'ember-percy';\n\nexport { setupRelayConnectionMocks } from './setup-relay-connection-mocks';\nexport { setupEmberclearTest } from './setup-test';\nexport { trackAsyncDataRequests } from './track-async-data';\n\nexport function assertExternal(assert: any) {\n  percySnapshot(assert);\n  // a11yAuditIf();\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/page-objects.ts",
    "content": "export * from './pages/components/chat';\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/app.ts",
    "content": "import { click, find } from '@ember/test-helpers';\n\nimport { clickable, create, isVisible } from 'ember-cli-page-object';\n\nimport { keyPressFor } from '@emberclear/ui/test-support/key-events';\n\nexport const selectors = {\n  headerUnread: '[data-test-unread-count]',\n};\n\nexport const page = create({\n  headerUnread: {\n    scope: selectors.headerUnread,\n  },\n  notificationPrompt: {\n    scope: '[data-test-notification-prompt]',\n    isVisible: isVisible(),\n    askLater: clickable('[data-test-ask-later]'),\n    askNever: clickable('[data-test-ask-never]'),\n    enable: clickable('[data-test-enable]'),\n    dismiss: clickable('[data-test-dismiss]'),\n  },\n  modals: {\n    search: {\n      async open() {\n        await keyPressFor(document.body, 75, { ctrlKey: true });\n      },\n    },\n  },\n});\n\nexport const app = {\n  scrollContainer: () => find('#scrollContainer') as HTMLElement,\n\n  modals: {},\n\n  footer: {\n    faq: () => find('[data-test-footer-faq]') as HTMLElement,\n    clickFaq: () => click('[data-test-footer-faq]'),\n  },\n};\n\nexport default {\n  app,\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/chat.ts",
    "content": "import { typeIn } from '@ember/test-helpers';\n\nimport {\n  attribute,\n  clickable,\n  collection,\n  count,\n  create,\n  fillable,\n  isVisible,\n  text,\n} from 'ember-cli-page-object';\n\nimport { definition as embedModal } from 'emberclear/components/pod/chat/chat-entry/embeds-menu/snippet/-page';\nimport { definition as unreadMessagesFloater } from 'emberclear/components/pod/chat/chat-history/unread-management/-page';\nimport { newMessages } from 'emberclear/tests/helpers/pages/components/chat';\n\nimport { dropdown } from '@emberclear/ui/test-support/page-objects';\n\nexport const selectors = {\n  form: '[data-test-chat-entry-form]',\n  textarea: '[data-test-chat-entry]',\n  message: '[data-test-chat-message]',\n  submitButton: '[data-test-chat-submit]',\n  confirmations: '[data-test-confirmations]',\n};\n\nexport const page = create({\n  isScrollable() {\n    let messagesElement = document.querySelector('.messages')!;\n\n    return isScrollable(messagesElement);\n  },\n\n  scroll(amountInPx: number) {\n    let messagesElement = document.querySelector('.messages')!;\n\n    let current = messagesElement.scrollTop;\n    let next = current + amountInPx;\n\n    messagesElement.scrollTo({ left: 0, top: next });\n  },\n\n  // actual page object things\n  textarea: {\n    scope: '[data-test-chat-entry]',\n    isDisabled: attribute('disabled'),\n    fillIn: fillable(),\n    typeIn(substring: string) {\n      return typeIn('[data-test-chat-entry]', substring);\n    },\n  },\n\n  chatOptions: {\n    ...dropdown,\n    toggleEmbedModal: clickable('[data-test-embeds-toggle]'),\n  },\n\n  embedModal,\n  unreadMessagesFloater,\n\n  submitButton: {\n    scope: '[data-test-chat-submit]',\n    isDisabled: attribute('disabled'),\n  },\n  newMessagesFloater: newMessages,\n  numberOfMessages: count('[data-test-chat-message]'),\n  messages: collection('[data-test-chat-message]', {\n    hasLoader: isVisible('.ellipsis-loader'),\n    hasCode: isVisible('.token', { multiple: true }),\n    confirmations: {\n      scope: '[data-test-confirmations]',\n      text: text(),\n      hoverTip: text('.hover-tip'),\n      delete: clickable('[data-test-delete]'),\n      resend: clickable('[data-test-resend]'),\n      autosend: clickable('[data-test-autosend]'),\n      isLoading: isVisible('.ellipsis-loader'),\n    },\n  }),\n});\n\nexport function isScrollable(element: Element) {\n  return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/components/chat.ts",
    "content": "import { hasClass } from 'ember-cli-page-object';\n\nexport const newMessages = {\n  scope: '[data-test-new-messages-available]',\n  isHidden: hasClass('hidden'),\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/contacts.ts",
    "content": "import { click, find, findAll } from '@ember/test-helpers';\n\nexport const contacts = {\n  rows: {\n    dom: () => findAll('[data-test-contacts] [data-test-contact-row]'),\n    removeAt: (index: number) => {\n      const row = findAll('[data-test-contacts] [data-test-contact-row]')[index];\n      const link = row.querySelector('button')!;\n\n      return click(link);\n    },\n  },\n  table: () => find('[data-test-contacts]'),\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/login.ts",
    "content": "import { click, fillIn } from '@ember/test-helpers';\n\nconst wrapper = '[data-test-focus-card]';\n\nexport const loginForm = {\n  typeName: (name: string) => fillIn(`${wrapper} [data-test-name]`, name),\n  typeMnemonic: (mnemonic: string) => fillIn(`${wrapper} [data-test-mnemonic]`, mnemonic),\n\n  submit: () => click(`${wrapper} [data-test-submit-login]`),\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/logout.ts",
    "content": "import { clickable, create } from 'ember-cli-page-object';\n\nexport const page = create({\n  confirmLogout: clickable('[data-test-confirm-logout]'),\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/qr.ts",
    "content": "import { clickable, create, text } from 'ember-cli-page-object';\n\nexport const page = create({\n  scope: '[data-test-qr-container]',\n  text: text(),\n\n  error: {\n    scope: '[data-test-error]',\n    message: text('[data-test-message]'),\n    clickRetry: clickable('[data-test-retry]'),\n  },\n\n  scanner: {\n    scope: '[data-test-qr-scanner]',\n  },\n\n  confirm: {\n    scope: '[data-test-login-confirm]',\n    code: text('[data-test-code]'),\n\n    clickAllow: clickable('[data-test-allow]'),\n    clickDeny: clickable('[data-test-deny]'),\n  },\n\n  unknownState: {\n    scope: '[data-test-unkown-state]',\n  },\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/settings.ts",
    "content": "import { click, fillIn } from '@ember/test-helpers';\n\nimport { clickable, collection, create, fillable, isVisible } from 'ember-cli-page-object';\n\nconst wrapper = '[data-test-settings-wrapper]';\n\nexport const page = create({\n  ui: {\n    toggleHideOfflineContacts: clickable('[data-test-hide-offline-contacts]'),\n  },\n  relays: {\n    addRelay: clickable('[data-test-add-relay]'),\n    form: {\n      scope: '[data-test-add-relay-form]',\n      isVisible: isVisible(),\n      fillSocket: fillable('[data-test-socket-field]'),\n      fillOg: fillable('[data-test-og-field]'),\n      save: clickable('[data-test-save-relay]'),\n    },\n    table: {\n      rows: collection('[data-test-relays] tbody tr', {\n        isConnected: isVisible('[data-test-connected]'),\n        remove: clickable('[data-test-remove]'),\n        makeDefault: clickable('[data-test-make-default]'),\n      }),\n    },\n  },\n  permissions: {\n    notifications: {\n      scope: '[data-test-notifications]',\n      isVisible: isVisible(),\n    },\n  },\n  dangerZone: {\n    deleteMessages: {\n      scope: '[data-test-delete-messages]',\n      click: clickable(),\n      isVisible: isVisible(),\n    },\n    togglePrivateKey: clickable(`[data-test-show-private-key-toggle]`),\n\n    privateKey: {\n      scope: `[data-test-mnemonic]`,\n    },\n  },\n  interface: {\n    scope: '[data-test-interface]',\n    isVisible: isVisible(),\n  },\n});\n\nexport const settings = {\n  save: () => click(`${wrapper} [data-test-save]`),\n  fillNameField: (text: string) => fillIn(`${wrapper} [data-test-name-field]`, text),\n\n  deleteMessages: () => click(`${wrapper} [data-test-delete-messages]`),\n\n  toggleHideOfflineContacts: () => click(`${wrapper} [data-test-hide-offline-contacts]`),\n};\n\nexport default {\n  settings,\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/setup.ts",
    "content": "import { clickable, create, fillable } from 'ember-cli-page-object';\n\nexport const nameForm = create({\n  scope: '[data-test-name-form]',\n  clickNext: clickable('[data-test-next]'),\n  enterName: fillable(`[data-test-name-field]`),\n});\n\nexport const completedPage = create({\n  selectors: {\n    mnemonic: '[data-test-setup-mnemonic]',\n  },\n  mnemonic: {\n    scope: '[data-test-setup-mnemonic]',\n  },\n  clickNext: clickable(`[data-test-next]`),\n});\n\nexport const overwritePage = create({\n  confirm: clickable('[data-test-overwrite-confirm]'),\n  abort: clickable('[data-test-overwrite-abort]'),\n});\n\nexport default {\n  nameForm,\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/pages/toast.ts",
    "content": "import { click, waitUntil } from '@ember/test-helpers';\n\n/*\n * This is a special page object, because it\n * lives outside the ember-app.\n *\n */\nconst selector = '.toastify';\n\nfunction findToast(): Element {\n  let toast = document.querySelector(selector);\n\n  if (!toast) throw new Error('Toast not found');\n\n  return toast;\n}\n\nexport const toast = {\n  get isVisible() {\n    return findToast();\n  },\n  get text() {\n    return findToast().textContent;\n  },\n\n  dismiss() {\n    return click(findToast());\n  },\n  waitForToast() {\n    return waitUntil(() => document.querySelector(selector), { timeout: 300 });\n  },\n};\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/setup-relay-connection-mocks.ts",
    "content": "import { stubService } from '@emberclear/test-helpers/test-support';\n\nexport function stubConnection(overrides = {}) {\n  stubService('connection', {\n    getOpenGraph() {},\n    connect() {},\n    disconnect() {},\n    send() {},\n    ...overrides,\n  });\n  stubService('connection/manager', {\n    setup() {},\n    acquire() {},\n    getOpenGraph() {},\n    disconnect() {},\n    updateStatus() {},\n    createConnection() {},\n    connectionPool: {\n      activeConnections: [],\n    },\n  });\n}\n\nexport function setupRelayConnectionMocks(hooks: NestedHooks, overrides = {}) {\n  hooks.beforeEach(function () {\n    stubConnection(overrides);\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/setup-test.ts",
    "content": "import WindowService from 'emberclear/services/window';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\n\nimport { setupRelayConnectionMocks } from './setup-relay-connection-mocks';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nexport function setupEmberclearTest(hooks: NestedHooks) {\n  clearLocalStorage(hooks);\n  setupWorkers(hooks);\n  setupRelayConnectionMocks(hooks);\n  setupWindow(hooks);\n}\n\n//////////////////////////////////\n\nfunction setupWindow(hooks: NestedHooks) {\n  hooks.beforeEach(function (this: TestContext) {\n    class TestWindow extends WindowService {\n      get location(): any {\n        return {\n          href: '',\n        };\n      }\n    }\n\n    this.owner.register('service:window', TestWindow);\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/helpers/track-async-data.ts",
    "content": "import { getStore } from '@emberclear/test-helpers/test-support';\n\nexport function trackAsyncDataRequests(hooks: NestedHooks) {\n  hooks.beforeEach(function () {\n    const store = getStore();\n\n    (store as any).generateStackTracesForTrackedRequests = true;\n    (store as any).shouldTrackAsyncRequests = true;\n  });\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/index.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>emberclear</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/emberclear.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n\n    <script>\n      window.global = window;\n    </script>\n\n    <style>\n      /*\n         real -> test\n         html => #ember-testing-container\n         body => #ember-testing\n       */\n      #ember-testing-container {\n        overflow: hidden;\n      }\n    </style>\n\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/emberclear.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n\n    <div id='app-loader'></div>\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/embedded-media-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | embedded-media', function (hooks) {\n  setupRenderingTest(hooks);\n\n  module('rendered media is different based on meta', function () {\n    test('youtube', async function (assert) {\n      this.setProperties({ url: 'url', meta: { isYouTube: true } });\n\n      await render(hbs`<EmbeddedMedia @url={{this.url}} @meta={{this.meta}} />`);\n\n      assert.dom('iframe').exists();\n    });\n\n    test('image', async function (assert) {\n      this.setProperties({ url: 'url', meta: { isImage: true } });\n\n      await render(hbs`<EmbeddedMedia @url={{this.url}} @meta={{this.meta}} />`);\n\n      assert.dom('img').exists();\n    });\n\n    test('video', async function (assert) {\n      this.setProperties({ url: 'url', meta: { isVideo: true } });\n\n      await render(hbs`<EmbeddedMedia @url={{this.url}} @meta={{this.meta}} />`);\n\n      assert.dom('video').exists();\n    });\n\n    test('other', async function (assert) {\n      this.setProperties({ url: 'url', meta: {} });\n\n      await render(hbs`<EmbeddedMedia @url={{this.url}} @meta={{this.meta}} />`);\n\n      assert.dom('*').doesNotExist();\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/error-card-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Integration | Component | error-card', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<ErrorCard />`);\n\n    let expected = getService('intl').t('errors.genericTitle');\n\n    assert.equal(this.element.textContent.trim(), expected);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/fetch-open-graph-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { settled } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { stubConnection } from 'emberclear/tests/helpers/setup-relay-connection-mocks';\n\nimport { stubService } from '@emberclear/test-helpers/test-support';\n\nconst LINKS = {\n  youtube: {\n    webUrl: 'https://www.youtube.com/watch?v=w84fToQ6BXY',\n  },\n  imgur: {\n    page: 'https://imgur.com/msqKHPa',\n    direct: 'https://i.imgur.com/msqKHPa.jpg',\n  },\n  giphy: {\n    page: 'https://giphy.com/gifs/art-control-remedy-kyv4512wnwDOKn9rWq',\n    gif: 'https://media.giphy.com/media/kyv4512wnwDOKn9rWq/giphy.gif',\n    html5: 'https://giphy.com/gifs/kyv4512wnwDOKn9rWq/html5',\n  },\n\n  other: {\n    github: {\n      repo: 'https://github.com/NullVoxPopuli/emberclear',\n    },\n    coveralls: 'https://coveralls.io/github/NullVoxPopuli/emberclear',\n  },\n};\n\nmodule('Integration | Component | fetch-open-graph', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    assert.expect(3);\n\n    this.setProperties({ url: LINKS.youtube.webUrl });\n\n    stubService('connection/status', {\n      isConnected: true,\n    });\n\n    stubConnection({\n      getOpenGraph(...args) {\n        assert.equal(args[0], LINKS.youtube.webUrl);\n\n        return new Promise((resolve) => {\n          setTimeout(() => {\n            resolve(...args);\n          }, 10);\n        });\n      },\n    });\n\n    await render(hbs`\n      <FetchOpenGraph @url={{this.url}} as |isLoading data|>\n        <div data-test-content>\n          {{#if (not isLoading)}}\n            {{data}}\n          {{/if}}\n        </div>\n      </FetchOpenGraph>\n    `);\n\n    assert.dom('[data-test-content]').hasNoText();\n\n    await settled();\n\n    assert.dom('[data-test-content]').hasAnyText();\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/application/top-nav/locale-select-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { page } from 'emberclear/components/app/top-nav/locale-select/-page';\n\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\n\nmodule('Integration | Component | application/top-nav/locale-select', function (hooks) {\n  setupRenderingTest(hooks);\n  clearLocalStorage(hooks);\n\n  hooks.beforeEach(async function () {\n    await render(hbs`<App::TopNav::LocaleSelect />`);\n  });\n\n  test('starts closed', function (assert) {\n    assert.notOk(page.isOpen, 'is closed');\n  });\n\n  module('is opened', function (hooks) {\n    hooks.beforeEach(async function (assert) {\n      await page.toggle();\n\n      assert.ok(page.isOpen);\n    });\n\n    module('a non-default lang is selected', function (hooks) {\n      hooks.beforeEach(async function () {\n        await page.optionFor('Español').click();\n      });\n\n      test('the menu is closed', function (assert) {\n        assert.notOk(page.isOpen);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/chat/chat-entry/embeds-menu/component-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\nimport { page } from 'emberclear/tests/helpers/pages/chat';\n\nimport { createContact, setupCurrentUser } from '@emberclear/local-account/test-support';\n\nimport type { Contact } from '@emberclear/local-account';\nimport type { TestContext } from 'ember-test-helpers';\n\nmodule('Integration | Component | embeds-menu', function (hooks) {\n  setupRenderingTest(hooks);\n  setupRelayConnectionMocks(hooks);\n  setupCurrentUser(hooks);\n\n  let to!: Contact;\n\n  hooks.beforeEach(async function (this: TestContext) {\n    to = await createContact('Test Recipient');\n\n    this.setProperties({ to });\n\n    await render(hbs`\n      <Pod::Chat::ChatEntry::EmbedsMenu @sendTo={{this.to}} />\n    `);\n  });\n\n  module('the menu is opened', function (hooks) {\n    hooks.beforeEach(async function (assert) {\n      await page.chatOptions.toggle();\n\n      assert.ok(page.chatOptions.isOpen, 'menu is open');\n    });\n\n    test('can be closed', async function (assert) {\n      await page.chatOptions.toggle();\n\n      assert.notOk(page.chatOptions.isOpen, 'is closed');\n    });\n\n    module('the embeds modal is opened', function (hooks) {\n      hooks.beforeEach(async function (assert) {\n        await page.chatOptions.toggleEmbedModal();\n\n        assert.ok(page.embedModal.modalContent.isVisible, 'modal is visible');\n      });\n\n      test('can be cancelled', async function (assert) {\n        await page.embedModal.backdrop.click();\n\n        assert.notOk(page.embedModal.isVisible, 'modal is hidden');\n      });\n\n      test('the title includes the recipient name', function (assert) {\n        assert.contains(page.embedModal.title.toString(), to.name);\n      });\n\n      module('some code is submitted', function (hooks) {\n        hooks.beforeEach(async function () {\n          await page.embedModal.fillInTitle('Some TypeScript');\n          await page.embedModal.fillInCode(`let two = 2;`);\n          await page.embedModal.selectLanguage('TypeScript');\n          await page.embedModal.submit();\n        });\n\n        test('the modal hides', function (assert) {\n          assert.notOk(page.embedModal.isVisible, 'modal is hidden');\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/chat/chat-entry/emoji-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { stripIndent } from 'common-tags';\n\nimport { page } from 'emberclear/tests/helpers/pages/chat';\n\nimport { getStore } from '@emberclear/test-helpers/test-support';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nmodule('Integration | Component | chat-entry', function (hooks) {\n  setupRenderingTest(hooks);\n\n  module('emoji code replacement', function (hooks) {\n    hooks.beforeEach(async function (this: TestContext) {\n      const store = getStore();\n\n      this.setProperties({\n        contact: store.createRecord('contact', { name: 'test' }),\n      });\n\n      await render(hbs`<Pod::Chat::ChatEntry @to={{this.contact}} />`);\n    });\n\n    module('there are no emoji codes to replace', function () {\n      test('result does not contain emoji', async function (assert) {\n        const expected = 'This is a test string with no emoji codes to replace.';\n\n        await page.textarea.fillIn(expected);\n\n        assert.equal(page.textarea.value, expected);\n      });\n\n      test('emoji codes are not replaced when not between colons', async function (assert) {\n        const expected = 'scream smile heartheart heart wave';\n\n        await page.textarea.fillIn(expected);\n\n        assert.equal(page.textarea.value, expected);\n      });\n    });\n\n    module('there are emoji codes to replace', function () {\n      test('result contains emoji', async function (assert) {\n        await page.textarea.fillIn(stripIndent`\n          This is a test string with emoji codes to replace. :smile:\n          The quick :b:rown fox jumps over the lazy dog.\n          A :dog: and a :cat: napped.\n          ¡:b::a::m:!\n        `);\n\n        const expectedString = stripIndent`\n          This is a test string with emoji codes to replace. 😄\n          The quick 🅱️rown fox jumps over the lazy dog.\n          A 🐶 and a 🐱 napped.\n          ¡🅱️🅰️Ⓜ️!\n        `;\n\n        assert.equal(page.textarea.value, expectedString);\n      });\n\n      test('there are multiple transformations of text', async function (assert) {\n        await page.textarea.typeIn('A :dog:');\n        assert.equal(page.textarea.value, 'A 🐶');\n\n        await page.textarea.typeIn(' and a :cat: napped.');\n        assert.equal(page.textarea.value, 'A 🐶 and a 🐱 napped.');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/chat/chat-history/message/embedded-resource-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, skip, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { stubService } from '@emberclear/test-helpers/test-support';\n\nimport type { TestContext } from 'ember-test-helpers';\nimport type ChatScroller from 'emberclear/services/chat-scroller';\n\nmodule('Integration | Component | embedded-resource', function (hooks) {\n  setupRenderingTest(hooks);\n\n  hooks.beforeEach(function () {\n    stubService('chat-scroller', {} as LIES<ChatScroller>);\n  });\n\n  module('shouldRender', function () {\n    module('there is nothing to display', function (hooks) {\n      hooks.beforeEach(async function () {\n        await render(hbs`\n          <Pod::Chat::ChatHistory::Message::EmbeddedResource />\n        `);\n      });\n\n      test('nothing is rendered', async function (assert) {\n        const text = (this.element as HTMLElement).innerText.trim();\n\n        assert.equal(text, '');\n      });\n    });\n\n    module('the url is embeddable', function (hooks) {\n      hooks.beforeEach(async function (this: TestContext) {\n        this.setProperties({\n          someUrl: 'https://i.imgur.com/gCyUdeb.gifv',\n          meta: {\n            hasExtension: true,\n            isImage: true,\n          },\n        });\n\n        await render(hbs`\n          <Pod::Chat::ChatHistory::Message::EmbeddedResource\n            @url={{this.someUrl}}\n            @meta={{this.meta}}\n          />\n        `);\n      });\n\n      // TODO: for some reason I can't stub this component's services\n      test('the rendered content is not blank', function (assert) {\n        let text = this.element.innerHTML;\n\n        assert.notEqual(text, '', 'html is not empty');\n        assert.contains(text, 'imgur');\n      });\n    });\n  });\n\n  module('The media preview is collapsable', function () {\n    module('when collapsed', function () {\n      skip('shows nothing', async function () {});\n\n      module('clicking the expand icon', function () {\n        skip('shows the content', function () {});\n      });\n    });\n\n    module('when open', function () {\n      skip('the content is visible', function () {});\n\n      module('clicking the collapse icon', function () {\n        skip('hides the content', function () {});\n      });\n    });\n  });\n\n  module('Open Graph Data exists', function () {\n    skip('renders the image', async function () {});\n\n    skip('there is no sitename', async function () {});\n  });\n\n  module('Open Graph Data does not exist', function () {});\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/chat/chat-history/message/metadata-preview-test.ts",
    "content": "import { find, render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nmodule('Integration | Component | metadata-preview', function (hooks) {\n  setupRenderingTest(hooks);\n\n  module('no ogData', function (hooks) {\n    hooks.beforeEach(function (this: TestContext) {\n      this.set('data', {});\n    });\n\n    test('nothing is shown', async function (assert) {\n      await render(hbs`\n        <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview\n          @ogData={{this.data}}\n        />\n      `);\n\n      assert.dom(this.element).hasNoText();\n    });\n  });\n\n  module('has Open Graph data', function () {\n    test('there is a title', async function (assert) {\n      this.set('data', { title: 'a title' });\n\n      await render(hbs`\n        <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview\n          @ogData={{this.data}}\n        />\n      `);\n\n      assert.dom(this.element).hasText('a title');\n    });\n\n    test('there is a description', async function (assert) {\n      this.set('data', { description: 'a description' });\n\n      await render(hbs`\n        <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview\n          @ogData={{this.data}}\n        />\n      `);\n\n      assert.dom(this.element).hasText('a description');\n    });\n  });\n\n  module('image / thumbnail preview', function () {\n    module('there is no image in the og data', function (hooks) {\n      hooks.beforeEach(async function (this: TestContext) {\n        this.set('data', {});\n        await render(hbs`\n         <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview />\n        `);\n      });\n\n      test('no image is shown', function (assert) {\n        const img = find('img');\n\n        assert.notOk(img);\n      });\n    });\n\n    module('there is an image in the og data', function (hooks) {\n      hooks.beforeEach(async function (this: TestContext) {\n        this.setProperties({\n          data: {\n            image: 'https://something',\n          },\n        });\n\n        await render(hbs`\n          <Pod::Chat::ChatHistory::Message::EmbeddedResource::MetadataPreview\n            @ogData={{this.data}}\n          />\n        `);\n      });\n\n      test('an image tag is present', function (assert) {\n        const img = find('img');\n\n        assert.ok(img, 'the html tag is present');\n        assert.dom('img').hasAttribute('src');\n        assert.dom('img').hasAttribute('alt');\n        assert.contains(img!.getAttribute('src'), 'https://');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/pod/chat/chat-history/new-messages-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { settled } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { create } from 'ember-cli-page-object';\n\nimport { newMessages as definition } from 'emberclear/tests/helpers/page-objects';\n\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Integration | Component | chat/chat-history/new-messages', function (hooks) {\n  setupRenderingTest(hooks);\n\n  let page = create(definition);\n\n  test('it renders', async function (assert) {\n    let chatScroller = getService('chat-scroller');\n\n    await render(hbs`<Pod::Chat::ChatHistory::NewMessages />`);\n\n    assert.dom(page.scope).exists();\n    assert.dom(page.scope).hasClass('hidden');\n\n    chatScroller.isLastVisible = false;\n    await settled();\n\n    assert.dom(page.scope).doesNotHaveClass('hidden');\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/components/settings/interface-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { page } from 'emberclear/components/pod/settings/interface/-page';\nimport { THEMES } from 'emberclear/services/settings';\n\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Integration | Component | settings/interface', function (hooks) {\n  setupRenderingTest(hooks);\n  clearLocalStorage(hooks);\n\n  module('Hide Offline Contacts', function () {\n    test('state matches service', async function (assert) {\n      await render(hbs`<Pod::Settings::Interface />`);\n\n      let settingsService = getService('settings');\n\n      assert.notOk(settingsService.hideOfflineContacts, 'Service is false');\n      assert.notOk(page.hideOfflineContacts.isChecked, 'Checkbox is unchecked');\n\n      await page.hideOfflineContacts.check();\n\n      assert.ok(settingsService.hideOfflineContacts, 'Service is true');\n      assert.ok(page.hideOfflineContacts.isChecked, 'Checkbox is checked');\n    });\n  });\n\n  module('Themes', function () {\n    module('Selecting Midnight', function () {\n      test('data is sync with the settings service', async function (assert) {\n        await render(hbs`<Pod::Settings::Interface />`);\n\n        let settingsService = getService('settings');\n\n        assert.equal(settingsService.theme, THEMES.default);\n        assert.notOk(page.themes.selectMidnight.isChecked, 'is not checked');\n\n        await page.themes.selectMidnight.check();\n\n        assert.equal(settingsService.theme, THEMES.midnight);\n        assert.ok(page.themes.selectMidnight.isChecked, 'Midnight is selected');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/collapsible/component-test.ts",
    "content": "import { click, find, render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Component | collapsible', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('is interactible', async function (assert) {\n    await render(hbs`\n      <Collapsible as |isOpen toggle Icon|>\n        <span class='is-open'>{{isOpen}}</span>\n\n        <button class='toggle' {{on 'click' toggle}}>\n          <Icon @isOpen={{isOpen}} />\n          Toggle\n        </button>\n      </Collapsible>\n    `);\n\n    assert.equal(find('.is-open')!.textContent, 'true');\n\n    await click('.toggle');\n\n    assert.equal(find('.is-open')!.textContent, 'false');\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/copy-text-button/component-test.ts",
    "content": "import { render, settled } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { triggerCopySuccess } from 'ember-cli-clipboard/test-support';\nimport hbs from 'htmlbars-inline-precompile';\n\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Integration | Component | copy-text-button', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<CopyTextButton @text=\"To be copied\" @label=\"Copy\" />`);\n    assert.dom('button').hasNoClass('is-active');\n  });\n\n  test('when clicked, the copied message appears', async function (assert) {\n    let expectedText = getService('intl').t('ui.invite.copied');\n\n    await render(hbs`\n      <CopyTextButton\n        data-test-copy-text\n        @text=\"To be copied\"\n        @label=\"Copy\" />\n    `);\n\n    // Fake click. Clipboard not supported in testing.\n    triggerCopySuccess('[data-test-copy-text]');\n\n    assert.dom('button').hasClass('is-active');\n    assert.dom('button').containsText(expectedText);\n\n    await settled();\n    assert.dom('button').hasNoClass('is-active');\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/keyboard-shortcuts/component-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nmodule('Integration | Component | keyboard-shortcuts', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    this.setProperties({\n      noop: () => undefined,\n    });\n\n    await render(hbs`<KeyboardShortcuts @isActive={{true}} @close={{this.noop}}/>`);\n\n    assert.dom('[data-test-keyboard-shortcuts]').exists();\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/modal-static/component-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { clickable, create, text } from 'ember-cli-page-object';\nimport hbs from 'htmlbars-inline-precompile';\n\nlet page = create({\n  isActive: text('[data-test-active]'),\n  toggle: clickable('[data-test-toggle]'),\n  open: clickable('[data-test-open]'),\n  close: clickable('[data-test-close]'),\n});\n\nmodule('Integration | Component | modal-static', function (hooks) {\n  setupRenderingTest(hooks);\n\n  module('yielded data is interactable', function (hooks) {\n    hooks.beforeEach(async function () {\n      await render(hbs`\n        <ModalStatic @name='interactable' as |isActive actions|>\n          <span data-test-active>{{isActive}}</span>\n\n          <button data-test-toggle {{on 'click' actions.toggle}}></button>\n          <button data-test-open {{on 'click' actions.open}}></button>\n          <button data-test-close {{on 'click' actions.close}}></button>\n        </ModalStatic>\n      `);\n    });\n\n    test('is toggleable', async function (assert) {\n      assert.equal(page.isActive, 'false');\n\n      await page.toggle();\n      assert.equal(page.isActive, 'true');\n\n      await page.toggle();\n      assert.equal(page.isActive, 'false');\n    });\n\n    test('can be opened', async function (assert) {\n      assert.equal(page.isActive, 'false');\n\n      await page.open();\n      assert.equal(page.isActive, 'true');\n\n      await page.open();\n      assert.equal(page.isActive, 'true');\n    });\n\n    test('can be closed', async function (assert) {\n      assert.equal(page.isActive, 'false');\n\n      await page.toggle();\n      assert.equal(page.isActive, 'true');\n\n      await page.close();\n      assert.equal(page.isActive, 'false');\n\n      await page.close();\n      assert.equal(page.isActive, 'false');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/q-r-scanner/component-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nimport { toast } from 'emberclear/tests/helpers/pages/toast';\n\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nmodule('Integration | Component | q-r-scanner', function (hooks) {\n  setupRenderingTest(hooks);\n\n  let original: any;\n\n  hooks.beforeEach(function () {\n    // specific to qr-scanner.js~ish\n    if (!navigator.mediaDevices) {\n      (navigator.mediaDevices as any) = {};\n    }\n\n    original = navigator.mediaDevices.getUserMedia;\n    navigator.mediaDevices.getUserMedia = () => Promise.reject('Camera not found');\n  });\n\n  hooks.afterEach(function () {\n    navigator.mediaDevices.getUserMedia = original;\n  });\n\n  test('it renders', async function (assert) {\n    let intl = getService('intl');\n\n    this.setProperties({\n      onScan: () => undefined,\n    });\n\n    await render(hbs`<QRScanner @onScan={{this.onScan}} />`);\n\n    assert.contains(toast.text, intl.t('errors.permissions.enableCamera'));\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/pods/components/status-icon/component-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport hbs from 'htmlbars-inline-precompile';\n\nimport { Status } from '@emberclear/local-account';\n\nmodule('Integration | Component | status-icon', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('online', async function (assert) {\n    this.setProperties({\n      contact: { onlineStatus: Status.ONLINE },\n    });\n    await render(hbs`<StatusIcon @contact={{this.contact}}/>`);\n\n    assert.dom('svg').hasClass('text-success');\n  });\n\n  test('offline', async function (assert) {\n    this.setProperties({\n      contact: { onlineStatus: Status.OFFLINE },\n    });\n    await render(hbs`<StatusIcon @contact={{this.contact}}/>`);\n\n    assert.dom('svg').hasClass('text-lighter');\n  });\n\n  test('away', async function (assert) {\n    this.setProperties({\n      contact: { onlineStatus: Status.AWAY },\n    });\n    await render(hbs`<StatusIcon @contact={{this.contact}}/>`);\n\n    assert.dom('svg').hasClass('text-warning');\n  });\n\n  test('busy', async function (assert) {\n    this.setProperties({\n      contact: { onlineStatus: Status.BUSY },\n    });\n    await render(hbs`<StatusIcon @contact={{this.contact}}/>`);\n\n    assert.dom('svg').hasClass('text-lighter');\n  });\n\n  test('other', async function (assert) {\n    this.setProperties({\n      contact: { onlineStatus: 'invalid-tanohusoatuhoeasut' },\n    });\n    await render(hbs`<StatusIcon @contact={{this.contact}}/>`);\n\n    assert.dom('svg').hasClass('text-darker');\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/integration/routing/feature-flags-test.ts",
    "content": "import { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { setupRelayConnectionMocks } from 'emberclear/tests/helpers';\n\nimport { setupCurrentUser } from '@emberclear/local-account/test-support';\nimport { setupRouter, visit } from '@emberclear/test-helpers/test-support';\n\nimport type { TestContext } from 'ember-test-helpers';\n\nmodule('Routing | Feature Flags', function (hooks) {\n  setupApplicationTest(hooks);\n  setupCurrentUser(hooks);\n  setupRelayConnectionMocks(hooks);\n  setupRouter(hooks);\n\n  hooks.beforeEach(function (this: TestContext) {\n    this.owner.register(\n      'template:application',\n      hbs`<div id='foo'>{{has-feature-flag 'channels'}}</div>{{outlet}}`\n    );\n    this.owner.register('template:chat', hbs`<div id='bar'>{{has-feature-flag 'channels'}}</div>`);\n  });\n\n  hooks.afterEach(async function () {\n    await visit('/'); // clear search params\n  });\n\n  test('Query Param is present', async function (assert) {\n    await visit('/?_features=channels');\n\n    assert.dom('#foo').hasText('true');\n\n    await visit('/chat?_features=channels');\n\n    assert.dom('#bar').hasText('true');\n  });\n\n  test('No Query Param are initially present', async function (assert) {\n    await visit('/');\n\n    assert.dom('#foo').hasText('false');\n\n    await visit('/chat?_features=channels');\n\n    assert.dom('#bar').hasText('true');\n  });\n\n  module('Different Query Param is present', function () {\n    test('helpers work', async function (assert) {\n      await visit('/?_features=video-calling');\n\n      assert.dom('#foo').hasText('false');\n\n      await visit('/chat?_features=channels');\n\n      assert.dom('#bar').hasText('true');\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/test-helper.ts",
    "content": "// Install Types and assertion extensions\nimport 'qunit-dom';\nimport 'qunit-assertions-extra';\n\nimport { setApplication } from '@ember/test-helpers';\nimport QUnit from 'qunit';\n// import start from 'ember-exam/test-support/start';\nimport { start } from 'ember-qunit';\n\nimport registerWaiter from 'ember-raf-scheduler/test-support/register-waiter';\n\nimport Application from 'emberclear/app';\nimport {\n  // hasWASM,\n  hasCamera,\n  hasIndexedDb,\n  hasNotifications,\n  hasWebWorker,\n} from 'emberclear/components/pod/index/compatibility/-utils/detection';\nimport config from 'emberclear/config/environment';\n\nconst seed = Math.random().toString(36).substr(2, 5);\n\nQUnit.config.seed = seed;\nQUnit.config.reorder = false;\n\nQUnit.begin(async () => {\n  console.info(`Using seed for Qunit: ${seed}`);\n\n  console.info(`\n\n    ------------------------ Compatibility ------------------------\n\n    --- The test environment must support all of these features ---\n\n    IndexedDb: ${await hasIndexedDb()}\n    Camera: ${hasCamera()}\n    Notifications: ${hasNotifications()}\n    ServiceWorker: not tested\n    WebWorker: ${hasWebWorker()}\n  `);\n});\n\nsetApplication(Application.create(config.APP));\n\nregisterWaiter();\n\nstart({\n  setupTestIsolationValidation: true,\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/tests\",\n    \"paths\": {\n      \"emberclear/tests/*\": [\"*\"],\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../app\" },\n    { \"path\": \"../../addons/test-helpers\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/routes/add-friend/route-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Route | add-friend', function (hooks) {\n  setupTest(hooks);\n\n  test('it exists', function (assert) {\n    let route = this.owner.lookup('route:add-friend');\n\n    assert.ok(route);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/routes/chat/route-unit-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Route | chat', function (hooks) {\n  setupTest(hooks);\n\n  test('it exists', function (assert) {\n    let route = this.owner.lookup('route:chat');\n\n    assert.ok(route);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/routes/qr-test.js",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Route | qr', function (hooks) {\n  setupTest(hooks);\n\n  test('it exists', function (assert) {\n    let route = this.owner.lookup('route:qr');\n\n    assert.ok(route);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/channels/channel-verifier-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { generateSortedVote } from 'emberclear/services/channels/-utils/vote-sorter';\n\nimport { hash, sign } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { VOTE_ACTION } from '@emberclear/local-account/models/vote-chain';\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { buildUser } from '@emberclear/local-account/test-support';\nimport { getService, getStore } from '@emberclear/test-helpers/test-support';\n\nimport type { User } from '@emberclear/local-account';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\nimport type ChannelVerifier from 'emberclear/services/channels/channel-verifier';\n\nmodule('Unit | Service | channels/channel-verifier', function (hooks) {\n  setupTest(hooks);\n  clearLocalStorage(hooks);\n\n  let service: ChannelVerifier;\n\n  hooks.beforeEach(function () {\n    service = getService('channels/channel-verifier');\n  });\n\n  module('starting channel', function () {\n    module('invalid', function () {\n      test('no admin', async function (assert) {\n        const store = getStore();\n        let channelContextChain = store.createRecord('channel-context-chain', {\n          members: [],\n        });\n\n        assert.notOk(await service.isValidChain(channelContextChain));\n      });\n\n      test('no members', async function (assert) {\n        const store = getStore();\n        let admin = await buildUser('admin');\n        let channelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [],\n        });\n\n        assert.notOk(await service.isValidChain(channelContextChain));\n      });\n\n      test('more than 1 member', async function (assert) {\n        const store = getStore();\n        let admin = await buildUser('admin');\n        let member1 = await buildUser('member1');\n        let channelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin, member1],\n        });\n\n        assert.notOk(await service.isValidChain(channelContextChain));\n      });\n\n      test('member is not admin', async function (assert) {\n        const store = getStore();\n        let admin = await buildUser('admin');\n        let member1 = await buildUser('member');\n        let channelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1],\n        });\n\n        assert.notOk(await service.isValidChain(channelContextChain));\n      });\n    });\n\n    test('valid', async function (assert) {\n      const store = getStore();\n      let admin = await buildUser('admin');\n      let channelContextChain = store.createRecord('channel-context-chain', {\n        admin: admin,\n        members: [admin],\n      });\n\n      assert.ok(await service.isValidChain(channelContextChain));\n    });\n  });\n\n  module('subsequent chains', function (hooks) {\n    let addMember1VoteChain!: VoteChain;\n    let admin!: User;\n    let member1!: User;\n\n    hooks.beforeEach(async function () {\n      const store = getStore();\n\n      admin = await buildUser('admin');\n      member1 = await buildUser('member1');\n      addMember1VoteChain = store.createRecord('vote-chain', {\n        yes: [admin],\n        no: [],\n        remaining: [],\n        action: VOTE_ACTION.ADD,\n        target: member1,\n        key: admin,\n        previousVoteChain: undefined,\n        signature: undefined,\n      });\n      addMember1VoteChain.signature = await signatureOf(addMember1VoteChain, admin);\n    });\n\n    module('valid', function () {\n      test('add a member', async function (assert) {\n        const store = getStore();\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n        });\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n          supportingVote: addMember1VoteChain,\n        });\n\n        assert.ok(await service.isValidChain(currentChannelContextChain));\n      });\n\n      test('remove a member', async function (assert) {\n        const store = getStore();\n        let vote2 = store.createRecord('vote-chain', {\n          yes: [admin],\n          no: [],\n          remaining: [member1],\n          action: VOTE_ACTION.REMOVE,\n          target: member1,\n          key: admin,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        vote2.signature = await signatureOf(vote2, admin);\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n        });\n        let previousChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n          supportingVote: addMember1VoteChain,\n        });\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n          previousChain: previousChannelContextChain,\n          supportingVote: vote2,\n        });\n\n        assert.ok(await service.isValidChain(currentChannelContextChain));\n      });\n\n      test('promote a member', async function (assert) {\n        const store = getStore();\n        let vote2 = store.createRecord('vote-chain', {\n          yes: [admin],\n          no: [],\n          remaining: [member1],\n          action: VOTE_ACTION.PROMOTE,\n          target: member1,\n          key: admin,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        vote2.signature = await signatureOf(vote2, admin);\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n        });\n        let previousChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n          supportingVote: addMember1VoteChain,\n        });\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: member1,\n          members: [admin, member1],\n          previousChain: previousChannelContextChain,\n          supportingVote: vote2,\n        });\n\n        assert.ok(await service.isValidChain(currentChannelContextChain));\n      });\n    });\n\n    module('invalid', function () {\n      test('when a previous chain is invalid', async function (assert) {\n        const store = getStore();\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1],\n        });\n        // No need to generate supporting vote as previous channel chains are checked first\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n        });\n\n        assert.notOk(await service.isValidChain(currentChannelContextChain));\n      });\n\n      test('when the supporting vote chain is invalid', async function (assert) {\n        const store = getStore();\n        let admin = await buildUser('admin');\n        let member1 = await buildUser('member1');\n        let vote = store.createRecord('vote-chain', {\n          yes: [admin],\n          no: [],\n          remaining: [],\n          action: VOTE_ACTION.ADD,\n          target: member1,\n          key: admin,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        vote.signature = await signatureOf(vote, admin);\n        vote.action = VOTE_ACTION.REMOVE;\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n        });\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n          supportingVote: vote,\n        });\n\n        assert.notOk(await service.isValidChain(currentChannelContextChain));\n      });\n\n      module('when vote is not completed positive', function () {\n        test('when votes are in a tie and admin has not voted yes', async function (assert) {\n          const store = getStore();\n          let member2 = await buildUser('member2');\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [member1],\n            no: [],\n            remaining: [admin],\n            action: VOTE_ACTION.ADD,\n            target: member2,\n            key: member1,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, member1);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, member2, admin],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when yes does not outweigh no and remaining', async function (assert) {\n          const store = getStore();\n          let vote = store.createRecord('vote-chain', {\n            yes: [],\n            no: [],\n            remaining: [admin],\n            action: VOTE_ACTION.ADD,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote.signature = await signatureOf(vote, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: vote,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n      });\n\n      test('VOTE_ACTION not valid option', async function (assert) {\n        const store = getStore();\n        let vote = store.createRecord('vote-chain', {\n          yes: [],\n          no: [],\n          remaining: [admin],\n          action: 'invalid vote action',\n          target: member1,\n          key: admin,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        vote.signature = await signatureOf(vote, admin);\n        let originalChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [admin],\n        });\n        let currentChannelContextChain = store.createRecord('channel-context-chain', {\n          admin: admin,\n          members: [member1, admin],\n          previousChain: originalChannelContextChain,\n          supportingVote: vote,\n        });\n\n        assert.notOk(await service.isValidChain(currentChannelContextChain));\n      });\n\n      module('VOTE_ACTION = ADD', function () {\n        test('when other members are added outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let member2 = await buildUser('member2');\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin, member2],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when other members are removed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when admin is changed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: member1,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n      });\n\n      module('VOTE_ACTION = REMOVE', function () {\n        test('when other members are added outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when other members are removed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [admin],\n            no: [],\n            remaining: [member1],\n            action: VOTE_ACTION.REMOVE,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when admin is changed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [admin],\n            no: [],\n            remaining: [member1],\n            action: VOTE_ACTION.REMOVE,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: member1,\n            members: [admin],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n      });\n\n      module('VOTE_ACTION = PROMOTE', function () {\n        test('when other members are added outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let member2 = await buildUser('member2');\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [admin],\n            no: [],\n            remaining: [member1],\n            action: VOTE_ACTION.PROMOTE,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: member1,\n            members: [member1, admin, member2],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when other members are removed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [admin],\n            no: [],\n            remaining: [member1],\n            action: VOTE_ACTION.PROMOTE,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: member1,\n            members: [member1],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n\n        test('when admin is changed outside of what the target specifies', async function (assert) {\n          const store = getStore();\n          let vote2 = store.createRecord('vote-chain', {\n            yes: [admin],\n            no: [],\n            remaining: [member1],\n            action: VOTE_ACTION.PROMOTE,\n            target: member1,\n            key: admin,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n\n          vote2.signature = await signatureOf(vote2, admin);\n          let originalChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin],\n          });\n          let previousChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [member1, admin],\n            previousChain: originalChannelContextChain,\n            supportingVote: addMember1VoteChain,\n          });\n          let currentChannelContextChain = store.createRecord('channel-context-chain', {\n            admin: admin,\n            members: [admin, member1],\n            previousChain: previousChannelContextChain,\n            supportingVote: vote2,\n          });\n\n          assert.notOk(await service.isValidChain(currentChannelContextChain));\n        });\n      });\n    });\n  });\n});\n\nasync function signatureOf(vote: VoteChain, user: User): Promise<Uint8Array> {\n  return sign(await hash(generateSortedVote(vote)), user.privateSigningKey);\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/channels/utils/-vote-sorter-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { generateSortedVote, VOTE_ORDERING } from 'emberclear/services/channels/-utils/vote-sorter';\nimport { equalsUint8Array } from 'emberclear/utils/uint8array-equality';\n\nimport {\n  convertObjectToUint8Array,\n  convertUint8ArrayToObject,\n  fromHex,\n} from '@emberclear/encoding/string';\nimport { VOTE_ACTION } from '@emberclear/local-account/models/vote-chain';\nimport { buildUser, clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { getStore } from '@emberclear/test-helpers/test-support';\n\nimport type { SortedVote, SortedVoteHex } from 'emberclear/services/channels/-utils/vote-sorter';\n\nmodule('Unit | Service | channels/utils/vote-sorter', function (hooks) {\n  setupTest(hooks);\n  clearLocalStorage(hooks);\n\n  module('vote is sorted properly', function () {\n    test('when ran with previous vote', async function (assert) {\n      const store = getStore();\n      let yes1 = await buildUser('yes1');\n      let yes2 = await buildUser('yes2');\n      let no1 = await buildUser('no1');\n      let no2 = await buildUser('no2');\n      let remaining1 = await buildUser('remaining1');\n      let remaining2 = await buildUser('remaining2');\n      let currentUser = await buildUser('currentUser');\n      let firstVote = store.createRecord('vote-chain', {\n        signature: convertObjectToUint8Array('firstVoteSignature'),\n      });\n      let currentVote = store.createRecord('vote-chain', {\n        previousVoteChain: firstVote,\n        action: VOTE_ACTION.ADD,\n        key: currentUser,\n        target: currentUser,\n        yes: [yes1, yes2],\n        no: [no1, no2],\n        remaining: [remaining2, remaining1],\n      });\n\n      let resultHex = convertUint8ArrayToObject<SortedVoteHex>(generateSortedVote(currentVote));\n      let result = sortedVoteHexToSortedVote(resultHex);\n\n      assert.ok(\n        result[VOTE_ORDERING.remaining].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure remaining keys are sorted'\n      );\n      assert.ok(\n        result[VOTE_ORDERING.yes].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure yes keys are sorted'\n      );\n      assert.ok(\n        result[VOTE_ORDERING.no].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure no keys are sorted'\n      );\n\n      assert.ok(equalsUint8Array(result[VOTE_ORDERING.targetKey], currentUser.publicKey));\n      assert.equal(result[VOTE_ORDERING.action], VOTE_ACTION.ADD);\n      assert.ok(\n        equalsUint8Array(result[VOTE_ORDERING.voterSigningKey], currentUser.publicSigningKey)\n      );\n      assert.ok(\n        equalsUint8Array(result[VOTE_ORDERING.previousChainSignature]!, firstVote.signature)\n      );\n      assert.equal(result.length, 7);\n    });\n\n    test('when ran without previous vote', async function (assert) {\n      const store = getStore();\n      let yes1 = await buildUser('yes1');\n      let yes2 = await buildUser('yes2');\n      let no1 = await buildUser('no1');\n      let no2 = await buildUser('no2');\n      let remaining1 = await buildUser('remaining1');\n      let remaining2 = await buildUser('remaining2');\n      let currentUser = await buildUser('currentUser');\n      let currentVote = store.createRecord('vote-chain', {\n        action: VOTE_ACTION.ADD,\n        key: currentUser,\n        target: currentUser,\n        yes: [yes1, yes2],\n        no: [no1, no2],\n        remaining: [remaining2, remaining1],\n      });\n\n      let resultHex = convertUint8ArrayToObject<SortedVoteHex>(generateSortedVote(currentVote));\n      let result = sortedVoteHexToSortedVote(resultHex);\n\n      assert.ok(\n        result[VOTE_ORDERING.remaining].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure remaining keys are sorted'\n      );\n      assert.ok(\n        result[VOTE_ORDERING.yes].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure yes keys are sorted'\n      );\n      assert.ok(\n        result[VOTE_ORDERING.no].every(\n          (current: Uint8Array, index: number, array: Uint8Array[]) =>\n            !index || array[index - 1] <= current\n        ),\n        'ensure no keys are sorted'\n      );\n\n      assert.ok(equalsUint8Array(result[VOTE_ORDERING.targetKey], currentUser.publicKey));\n      assert.equal(result[VOTE_ORDERING.action], VOTE_ACTION.ADD);\n      assert.ok(\n        equalsUint8Array(result[VOTE_ORDERING.voterSigningKey], currentUser.publicSigningKey)\n      );\n      assert.equal(result[VOTE_ORDERING.previousChainSignature], undefined);\n      assert.equal(result.length, 7);\n    });\n  });\n});\n\nfunction sortedVoteHexToSortedVote(sortedVoteHex: SortedVoteHex): SortedVote {\n  let sortedVote: SortedVote = [\n    sortedVoteHex[VOTE_ORDERING.remaining].map((vote) => fromHex(vote)),\n    sortedVoteHex[VOTE_ORDERING.yes].map((vote) => fromHex(vote)),\n    sortedVoteHex[VOTE_ORDERING.no].map((vote) => fromHex(vote)),\n    fromHex(sortedVoteHex[VOTE_ORDERING.targetKey]),\n    sortedVoteHex[VOTE_ORDERING.action],\n    fromHex(sortedVoteHex[VOTE_ORDERING.voterSigningKey]),\n    sortedVoteHex[VOTE_ORDERING.previousChainSignature]\n      ? fromHex(sortedVoteHex[VOTE_ORDERING.previousChainSignature]!)\n      : undefined,\n  ];\n\n  return sortedVote;\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/channels/vote-verifier-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { generateSortedVote } from 'emberclear/services/channels/-utils/vote-sorter';\n\n// TODO: use the crypto worker instead of importing these provite apis\nimport { hash, sign } from '@emberclear/crypto/workers/crypto/utils/nacl';\nimport { VOTE_ACTION } from '@emberclear/local-account/models/vote-chain';\nimport { buildUser, clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { getService, getStore } from '@emberclear/test-helpers/test-support';\n\nimport type User from '@emberclear/local-account/models/user';\nimport type VoteChain from '@emberclear/local-account/models/vote-chain';\nimport type VoteVerifier from 'emberclear/services/channels/vote-verifier';\n\nmodule('Unit | Service | channels/vote-verifier', function (hooks) {\n  setupTest(hooks);\n  clearLocalStorage(hooks);\n\n  let service!: VoteVerifier;\n\n  hooks.beforeEach(function () {\n    service = getService('channels/vote-verifier');\n  });\n\n  module('vote is valid', function () {\n    test('when only one chain', async function (assert) {\n      const store = getStore();\n      let user1 = await buildUser('user1');\n      let userToAdd = await buildUser('userToAdd');\n\n      let currentVote = store.createRecord('vote-chain', {\n        yes: [user1],\n        no: [],\n        remaining: [],\n        action: VOTE_ACTION.ADD,\n        target: userToAdd,\n        key: user1,\n        previousVoteChain: undefined,\n        signature: undefined,\n      });\n\n      currentVote.signature = await signatureOf(currentVote, user1);\n\n      assert.ok(await service.isValid(currentVote));\n    });\n\n    test('when there are many chains', async function (assert) {\n      const store = getStore();\n\n      let user1 = await buildUser('user1');\n      let user2 = await buildUser('user2');\n      let user3 = await buildUser('user3');\n      let user4 = await buildUser('user4');\n      let userToAdd = await buildUser('userToAdd');\n\n      let firstVote = store.createRecord('vote-chain', {\n        yes: [user1],\n        no: [],\n        remaining: [user2, user3, user4],\n        action: VOTE_ACTION.ADD,\n        target: userToAdd,\n        key: user1,\n        previousVoteChain: undefined,\n        signature: undefined,\n      });\n\n      firstVote.signature = await signatureOf(firstVote, user1);\n\n      let secondVote = store.createRecord('vote-chain', {\n        yes: [user1, user2],\n        no: [],\n        remaining: [user3, user4],\n        action: VOTE_ACTION.ADD,\n        target: userToAdd,\n        key: user2,\n        previousVoteChain: firstVote,\n        signature: undefined,\n      });\n\n      secondVote.signature = await signatureOf(secondVote, user2);\n\n      let currentVote = store.createRecord('vote-chain', {\n        yes: [user1, user2],\n        no: [user3],\n        remaining: [user4],\n        action: VOTE_ACTION.ADD,\n        target: userToAdd,\n        key: user3,\n        previousVoteChain: secondVote,\n        signature: undefined,\n      });\n\n      currentVote.signature = await signatureOf(currentVote, user3);\n\n      assert.ok(await service.isValid(currentVote));\n    });\n  });\n\n  module('vote is not valid', function () {\n    module('when there is a single chain', function () {\n      test('when first voter votes both yes and no', async function (assert) {\n        const store = getStore();\n        let user1 = await buildUser('user1');\n        let userToAdd = await buildUser('userToAdd');\n\n        let currentVote = store.createRecord('vote-chain', {\n          yes: [user1],\n          no: [user1],\n          remaining: [],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user1,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        currentVote.signature = await signatureOf(currentVote, user1);\n\n        assert.notOk(await service.isValid(currentVote));\n      });\n\n      test('when first voter does not vote yes or no', async function (assert) {\n        const store = getStore();\n        let user1 = await buildUser('user1');\n        let userToAdd = await buildUser('userToAdd');\n\n        let currentVote = store.createRecord('vote-chain', {\n          yes: [],\n          no: [],\n          remaining: [user1],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user1,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        currentVote.signature = await signatureOf(currentVote, user1);\n\n        assert.notOk(await service.isValid(currentVote));\n      });\n    });\n\n    module('when there are many chains', function () {\n      module('when voter was previously undecided', function (hooks) {\n        let user1: User;\n        let user2: User;\n        let userToAdd: User;\n        let firstVote: VoteChain;\n\n        hooks.beforeEach(async function () {\n          const store = getStore();\n\n          user1 = await buildUser('user1');\n          user2 = await buildUser('user2');\n          userToAdd = await buildUser('userToAdd');\n          firstVote = store.createRecord('vote-chain', {\n            yes: [user1],\n            no: [],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n          firstVote.signature = await signatureOf(firstVote, user1);\n        });\n\n        test('when stays undecided after voting', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [user1],\n            no: [],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user2,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user2);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n\n        test('when votes both yes and no', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [user1, user2],\n            no: [user2],\n            remaining: [],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user2,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user2);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n      });\n\n      module('when voter previously voted yes', function (hooks) {\n        let user1: User;\n        let user2: User;\n        let userToAdd: User;\n        let firstVote: VoteChain;\n\n        hooks.beforeEach(async function () {\n          const store = getStore();\n\n          user1 = await buildUser('user1');\n          user2 = await buildUser('user2');\n          userToAdd = await buildUser('userToAdd');\n          firstVote = store.createRecord('vote-chain', {\n            yes: [user1],\n            no: [],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n          firstVote.signature = await signatureOf(firstVote, user1);\n        });\n\n        test('when stays yes', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [user1],\n            no: [],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user1);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n\n        test('when votes both undecided and no', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [],\n            no: [user1],\n            remaining: [user1, user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user1);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n      });\n\n      module('when voter previously voted no', function (hooks) {\n        let user1: User;\n        let user2: User;\n        let userToAdd: User;\n        let firstVote: VoteChain;\n\n        hooks.beforeEach(async function () {\n          const store = getStore();\n\n          user1 = await buildUser('user1');\n          user2 = await buildUser('user2');\n          userToAdd = await buildUser('userToAdd');\n          firstVote = store.createRecord('vote-chain', {\n            yes: [],\n            no: [user1],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: undefined,\n            signature: undefined,\n          });\n          firstVote.signature = await signatureOf(firstVote, user1);\n        });\n\n        test('when stays no', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [],\n            no: [user1],\n            remaining: [user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user1);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n\n        test('when votes both yes and undecided', async function (assert) {\n          const store = getStore();\n          let currentVote = store.createRecord('vote-chain', {\n            yes: [user1],\n            no: [],\n            remaining: [user1, user2],\n            action: VOTE_ACTION.ADD,\n            target: userToAdd,\n            key: user1,\n            previousVoteChain: firstVote,\n            signature: undefined,\n          });\n\n          currentVote.signature = await signatureOf(currentVote, user1);\n\n          assert.notOk(await service.isValid(currentVote));\n        });\n      });\n\n      test('when target is changed', async function (assert) {\n        const store = getStore();\n\n        let user1 = await buildUser('user1');\n        let user2 = await buildUser('user2');\n        let user3 = await buildUser('user3');\n        let userToAdd = await buildUser('userToAdd');\n\n        let firstVote = store.createRecord('vote-chain', {\n          yes: [user1],\n          no: [],\n          remaining: [user2],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user1,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        firstVote.signature = await sign(\n          await hash(generateSortedVote(firstVote)),\n          user1.privateSigningKey\n        );\n\n        let currentVote = store.createRecord('vote-chain', {\n          yes: [user1, user2],\n          no: [],\n          remaining: [],\n          action: VOTE_ACTION.ADD,\n          target: user3,\n          key: user2,\n          previousVoteChain: firstVote,\n          signature: undefined,\n        });\n\n        currentVote.signature = await signatureOf(currentVote, user2);\n\n        assert.notOk(await service.isValid(currentVote));\n      });\n\n      test('when action is changed', async function (assert) {\n        const store = getStore();\n\n        let user1 = await buildUser('user1');\n        let user2 = await buildUser('user2');\n        let userToAdd = await buildUser('userToAdd');\n\n        let firstVote = store.createRecord('vote-chain', {\n          yes: [user1],\n          no: [],\n          remaining: [user2],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user1,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        firstVote.signature = await sign(\n          await hash(generateSortedVote(firstVote)),\n          user1.privateSigningKey\n        );\n\n        let currentVote = store.createRecord('vote-chain', {\n          yes: [user1, user2],\n          no: [],\n          remaining: [],\n          action: VOTE_ACTION.PROMOTE,\n          target: userToAdd,\n          key: user2,\n          previousVoteChain: firstVote,\n          signature: undefined,\n        });\n\n        currentVote.signature = await signatureOf(currentVote, user2);\n\n        assert.notOk(await service.isValid(currentVote));\n      });\n\n      test('when a previous vote is modified', async function (assert) {\n        const store = getStore();\n\n        let user1 = await buildUser('user1');\n        let user2 = await buildUser('user2');\n        let user3 = await buildUser('user3');\n        let user4 = await buildUser('user4');\n        let userToAdd = await buildUser('userToAdd');\n\n        let firstVote = store.createRecord('vote-chain', {\n          yes: [user1],\n          no: [],\n          remaining: [user2, user3, user4],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user1,\n          previousVoteChain: undefined,\n          signature: undefined,\n        });\n\n        firstVote.signature = await signatureOf(firstVote, user1);\n\n        let secondVote = store.createRecord('vote-chain', {\n          yes: [user1, user2],\n          no: [],\n          remaining: [user3, user4],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user2,\n          previousVoteChain: firstVote,\n          signature: undefined,\n        });\n\n        secondVote.signature = await await signatureOf(secondVote, user2);\n        secondVote.no = [user4];\n        secondVote.remaining = [user3];\n\n        let currentVote = store.createRecord('vote-chain', {\n          yes: [user1, user2],\n          no: [user3, user4],\n          remaining: [],\n          action: VOTE_ACTION.ADD,\n          target: userToAdd,\n          key: user3,\n          previousVoteChain: secondVote,\n          signature: undefined,\n        });\n\n        currentVote.signature = await signatureOf(currentVote, user3);\n\n        assert.notOk(await service.isValid(currentVote));\n      });\n    });\n  });\n});\n\nasync function signatureOf(vote: VoteChain, user: User): Promise<Uint8Array> {\n  return sign(await hash(generateSortedVote(vote)), user.privateSigningKey);\n}\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/contacts/online-checker-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Service | contacts/online-checker', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = this.owner.lookup('service:contacts/online-checker');\n\n    assert.ok(service);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/notifications-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport td from 'testdouble';\n\nimport { clearLocalStorage } from '@emberclear/local-account/test-support';\nimport { getService, stubService } from '@emberclear/test-helpers/test-support';\n\nimport type Notifications from 'emberclear/services/notifications';\nimport type WindowService from 'emberclear/services/window';\n\nmodule('Unit | Service | notifications', function (hooks) {\n  setupTest(hooks);\n  clearLocalStorage(hooks);\n\n  let service!: Notifications;\n  let windowService!: WindowService;\n\n  hooks.beforeEach(function () {\n    service = getService('notifications');\n    windowService = getService('window');\n\n    windowService.Notification = td.object({\n      permission: 'default',\n      requestPermission: () => {},\n    }) as any;\n  });\n\n  hooks.afterEach(function () {\n    td.reset();\n  });\n\n  module('not logged in', function (hooks) {\n    hooks.beforeEach(function () {\n      stubService('current-user', { isLoggedIn: false });\n    });\n\n    test('when undecided', function (assert) {\n      td.replace(windowService.Notification, 'permission', 'default');\n\n      assert.notOk(service.showInAppPrompt, 'The in-app prompt should not be shown');\n    });\n  });\n\n  module('already logged in', function (hooks) {\n    hooks.beforeEach(function () {\n      stubService('current-user', { isLoggedIn: true });\n    });\n\n    module('a notification is attempted', function (hooks) {\n      hooks.beforeEach(async function () {\n        td.replace(windowService.Notification, 'permission', 'default');\n\n        await service.info('eh, no one will see this :(');\n      });\n\n      test('the showInAppPrompt property should still be true', function (assert) {\n        assert.ok(service.isBrowserCapableOfNotifications, 'Browser is capable of notifications');\n        assert.notOk(service.isPermissionGranted, 'Permission has not previously been granted');\n        assert.notOk(service.isPermissionDenied, 'Permission has not previously been denied');\n        assert.notOk(service.isNeverGoingToAskAgain, 'User did not say to never ask again');\n        assert.notOk(service.isHiddenUntilBrowserRefresh, 'User did not say to ask later');\n\n        assert.ok(service.showInAppPrompt, 'The in-app prompt should be shown');\n      });\n    });\n\n    module('permission is asked', function (hooks) {\n      hooks.beforeEach(function () {\n        service.askToEnableNotifications = true;\n      });\n\n      test('and permission is granted', async function (assert) {\n        assert.expect(1);\n\n        // TODO: remove testdouble\n        (td.when(windowService.Notification.requestPermission()) as any).thenCallback('granted');\n\n        await service.askPermission();\n\n        assert.false(service.askToEnableNotifications);\n      });\n\n      test('and permission is denied', async function (assert) {\n        assert.expect(1);\n\n        (td.when(windowService.Notification.requestPermission()) as any).thenCallback('denied');\n\n        await service.askPermission();\n\n        assert.false(service.askToEnableNotifications);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/service/redirect-manager-test.ts",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Service | redirect-manager', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = this.owner.lookup('service:redirect-manager');\n\n    assert.ok(service);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/services/session-test.js",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Service | session', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = this.owner.lookup('service:session');\n\n    assert.ok(service);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/services/store-test.js",
    "content": "import { module, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nmodule('Unit | Service | store', function (hooks) {\n  setupTest(hooks);\n\n  // Replace this with your real tests.\n  test('it exists', function (assert) {\n    let service = this.owner.lookup('service:store');\n\n    assert.ok(service);\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/utils/dom-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport sinon from 'sinon';\n\nimport { isElementWithin, keepInViewPort } from 'emberclear/utils/dom/utils';\n\nmodule('Unit | Utility | dom', function () {\n  let container: any;\n  let element: any;\n  let getBoundingClientRect: any;\n  const rect = {\n    top: 0,\n    bottom: 10,\n    left: 0,\n    right: 10,\n  };\n\n  module('isElementWithin', function (hooks) {\n    hooks.beforeEach(() => {\n      container = {\n        getBoundingClientRect() {\n          return rect;\n        },\n      };\n\n      getBoundingClientRect = sinon.stub();\n      element = { getBoundingClientRect };\n    });\n\n    test('clips above container', function (assert) {\n      getBoundingClientRect.returns({ top: -1 });\n\n      assert.notOk(isElementWithin(element, container));\n    });\n\n    test('clips to the left of the container', function (assert) {\n      getBoundingClientRect.returns({ left: -1 });\n\n      assert.notOk(isElementWithin(element, container));\n    });\n\n    test('clips to the right of the container', function (assert) {\n      getBoundingClientRect.returns({ right: 11 });\n\n      assert.notOk(isElementWithin(element, container));\n    });\n\n    test('clips below the container', function (assert) {\n      getBoundingClientRect.returns({ bottom: 11 });\n\n      assert.notOk(isElementWithin(element, container));\n    });\n\n    test('is within the container', function (assert) {\n      getBoundingClientRect.returns({ top: 1, bottom: 9, left: 1, right: 9 });\n\n      assert.ok(isElementWithin(element, container));\n    });\n\n    test('overlaps the container', function (assert) {\n      getBoundingClientRect.returns(rect);\n\n      assert.ok(isElementWithin(element, container));\n    });\n  });\n\n  module('keepInViewPort', function (hooks) {\n    let windowWidth!: number;\n    let windowHeight!: number;\n\n    hooks.beforeEach(() => {\n      windowWidth = window.innerWidth;\n      windowHeight = window.innerHeight;\n\n      getBoundingClientRect = sinon.stub();\n      element = { getBoundingClientRect, style: {} };\n    });\n\n    test('clips above window', function (assert) {\n      getBoundingClientRect.returns({ top: -1 });\n\n      keepInViewPort(element);\n\n      assert.equal(element.style.top, '20px');\n    });\n\n    test('clips to the left of the window', function (assert) {\n      getBoundingClientRect.returns({ left: -1 });\n\n      keepInViewPort(element);\n\n      assert.equal(element.style.left, '20px');\n    });\n\n    test('clips to the right of the window', function (assert) {\n      getBoundingClientRect.returns({ right: windowWidth + 1 });\n\n      keepInViewPort(element);\n\n      assert.equal(element.style.left, '-21px');\n    });\n\n    test('clips below the container', function (assert) {\n      getBoundingClientRect.returns({ bottom: windowHeight + 1 });\n\n      keepInViewPort(element);\n\n      assert.equal(element.style.bottom, '20px');\n    });\n\n    test('is within the container', function (assert) {\n      getBoundingClientRect.returns({ top: 1, bottom: 9, left: 1, right: 9 });\n\n      keepInViewPort(element);\n\n      assert.deepEqual(element.style, {});\n    });\n\n    test('overlaps the container', function (assert) {\n      getBoundingClientRect.returns(rect);\n\n      keepInViewPort(element);\n\n      assert.deepEqual(element.style, {});\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/tests/unit/utils/string-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport * as string from 'emberclear/utils/string/utils';\n\nmodule('Unit | Utility | string', function () {\n  module('hostFromURL', function () {\n    test('finds the host from a URL', function (assert) {\n      const url = 'wss://mesh-relay-in-us-1.herokuapp.com/socket';\n      const expected = 'mesh-relay-in-us-1.herokuapp.com';\n      const result = string.hostFromURL(url);\n\n      assert.equal(result, expected);\n    });\n\n    test('when the pattern is not matched, null is returned', function (assert) {\n      const url = 'mesh-relay-in-us-1.herokuapp.com/socket';\n      const expected = null;\n      const result = string.hostFromURL(url);\n\n      assert.equal(result, expected);\n    });\n  });\n\n  module('parseURLS', function () {\n    test('parses one URL', function (assert) {\n      const url = 'https://github.com/emberjs/rfcs/blob/master/text/0143-module-unification.md';\n      const input = `sometext something ${url}`;\n      const expected = [url];\n      const result = string.parseURLs(input);\n\n      assert.deepEqual(result, expected);\n    });\n\n    test('parses two URLs', function (assert) {\n      const url1 = 'https://github.com/emberjs/rfcs/blob/master/text/0143-module-unification.md';\n      const url2 =\n        'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace';\n      const input = `sometext something ${url1} sometheng else ${url2}`;\n      const expected = [url1, url2];\n      const result = string.parseURLs(input);\n\n      assert.deepEqual(result, expected);\n    });\n  });\n\n  module('parseLanguages', function () {\n    test('parses code content', function (assert) {\n      const input = '```hbs {{some-helper}}```';\n      const expected = ['hbs'];\n      const result = string.parseLanguages(input);\n\n      assert.deepEqual(result, expected);\n    });\n\n    test('parses code in the middle of text', function (assert) {\n      const input = `\n        some text\n        \\`\\`\\`hbs\n        <SomeComponent @argument={{value}} />\n        \\`\\`\\`\n      `;\n      const expected = ['hbs'];\n      const result = string.parseLanguages(input);\n\n      assert.deepEqual(result, expected);\n    });\n\n    test('parses multiple languages from large text', function (assert) {\n      const input = `\n        some text\n        blah blah blah\n\n        something else\n        \\`\\`\\`ts\n        export default class extends Component {\n          someProperty!: boolean;\n        }\n        \\`\\`\\`\n        \\`\\`\\`hbs\n        <SomeComponent @argument={{value}} />\n        \\`\\`\\`\n      `;\n      const expected = ['ts', 'hbs'];\n      const result = string.parseLanguages(input);\n\n      assert.deepEqual(result, expected);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/emberclear/translations/de-DE.yaml",
    "content": "---\nappname: emberclear\nsubheader: Verschlüsselter Chat. Kein Verlauf. Keine Logs.\nauthoredBy: von\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'TODO: Fülle dies korrekt aus'\ninstall: Installieren\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: QR Code konnte nicht erzeugt werden\n  malformed: QR-Code ist ungültig\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Verbinde...\n  connected: Verbunden!\n  disconnected: Verbindung getrennt.\n  degraded: Heruntergestuft\n  unknown: Unbekannt\n  status:\n    socket:\n      error: Bei der Socket Verbindung ist ein Fehler aufgetreten!\n      close: Die Socket Verbindung wurde unterbrochen!\n    timeout: Ein Verbindungsfehler ist aufgetreten. Bitte warten...\n  errors:\n    send:\n      notConnected: Nachrichten können ohne Internetverbindung nicht gesendet werden!\n    subscribe:\n      notConnected: Abonnieren eines Kanals ohne Socket Verbindung nicht möglich!\nerrors:\n  genericTitle: \"Ein Fehler ist aufgetreten!\"\n  genericRetry: \"Hoppla, da ist etwas schiefgelaufen. Erneut versuchen?\"\n  notFound:\n    title: Nicht gefunden\n    help: Die URL konnte nicht aufgerufen werden. Wenn das Problem weiterhin besteht,\n    link: erstelle ein Ticket.\n  permissions:\n    enableCamera: Du musst dieser Seite möglicherweise Zugriff zu deiner Kamera geben.\nbuttons:\n  allow: Zulassen\n  deny: Ablehnen\n  retry: Erneut versuchen\n  delete: Löschen\n  resend: Erneut senden\n  resendAutomatically: automatisch erneut senden\n  enable: Aktivieren\n  save: Speichern\n  next: Weiter\n  back: Zurück\n  begin: Starten\n  logout: Abmelden\n  login: Anmelden\n  cancel: Abbrechen\n  close: Schließen\n  import: Importieren\n  scan: Scannen\n  sendSnippet: Ausschnitt senden\n  remove: Entfernen\n  donate: Spenden\n  donateEth: Ethereum spenden\n  donateXmr: Monero spenden\n  loginInstead: Stattdessen anmelden\n  uploadSettings: Upload Einstellungen\n  showMyInfo: Meine Info anzeigen\n  invite: Einladen\n  addFriend: Freund hinzufügen\n  makeDefault: Als Standard festlegen\n  ariaLabel:\n    sidebar: 'Seitenleiste anzeigen/ausblenden'\n    dropdownBackdrop: Klickbarer Hintergrund, um das Dropdown Menü mittels Klick zu schließen\n    modalBackdrop: Klickbarer Hintergrund, um den Dialog mittels Klick zu schließen\n    menuBackdrop: Klickbarer Hintergrund, um das Menü mittels Klick zu schließen\n    sidebarBackdrop: Klickbarer Hintergrund, um die Sidebar mittels Klick auszublenden\n    closeModal: Dialog schließen\ncompatibility:\n  title: Leider stimmt etwas nicht!\n  description: Um emberclear nutzen zu können, muss dein Browser die folgenden Technologien und Funktionen unterstützen\n  score: Ergebnis\n  required: Benötigt\n  compatibility: Kompatibilität\n  camera: Kamera\n  notifications: Benachrichtigungen\n  indexeddb: Indexierte Datenbank\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser hat diese Fähigkeit\n    missing: Browser fehlt diese Fähigkeit\nimages:\n  alt:\n    thumbnail: Vorschaubild\n    ownIdentityQR: Dies ist deine Identität, verpackt in einem QR Code\ninput:\n  label:\n    mnemonic: Kürzel\n    name: Name\nstatus:\n  connected: Verbunden\n  enabled: Aktiviert\n  deliveryFailed: 'Die Nachricht konnte nicht zugestellt werden.'\n  importing: Wird importiert...\n  updateAvailable: Eine neue Version ist verfügbar. Klicke hier, um die Aktualisierung durchzuführen.\nservices:\n  crypto:\n    keyGenFailed: Erzeugung der Schlüssel fehlgeschlagen\nroutes:\n  home: Startseite\n  chat: Chat\n  qr: QR\n  logout: Abmelden\n  login: Anmelden\n  contacts: Kontakte\n  settings: Einstellungen\n  profile: Profil\n  faq: FAQ\n  createNewUser: Neuen Benutzer erstellen\nmodels:\n  identity:\n    name: Name\n    publicKey: Öffentlicher Schlüssel\n  message:\n    autosendPending: automatische Sendung steht aus\n    errors:\n      timeout: 'Sendezeit überschritten.'\nui:\n  invite:\n    copyProfile: Link kopieren\n    copied: Kopiert!\n  notifications:\n    title: Emberclear Nachricht\n    from: Nachricht von {name}\n    prompt:\n      title: Emberclear benötigt deine Erlaubnis, um die Benachrichtigungen aktivieren zu können.\n      enable: Benachrichtigungen aktivieren\n      askLater: Nächstes Mal fragen\n      neverAsk: Nicht erneut nachfragen\n  search:\n    title: Nach Kontakten und Kanälen suchen\n    description: Suchfunktion folgt in kürze\n    noContacts: Keine Kontakte gefunden\n    noChannels: Keine Kanäle gefunden\n    contacts: Kontakte\n    channels: Kanäle\n    nothingFound: Keine Ergebnisse gefunden\n  shortcuts:\n    title: Tastatenkombination\n    key:\n      ctrl: strg\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: Leertaste\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Alles durchsuchen\n      toggleSidebar: Seitenleiste anzeigen/ausblenden\n      toggleHelp: Tastaturhilfe einblenden\n    label:\n      search:\n        tab: zum Navigieren\n        enter: zum Auswählen\n        esc: zum Abbrechen\n  sidebar:\n    find: Finden...\n    keepTyping: 'Noch {num} Zeichen zum Suchen...'\n    results: '{num} results'\n    unread: Ungelesen\n    channels:\n      title: Kanäle\n      none: Du bist noch keinem Raum beigetreten\n      placeholder: Name des neuen Kanals\n    contacts:\n      title: 'Kontakte'\n      none: Keine Kontakte vorhanden\n      numOffline: '{num} offline'\n    actions:\n      title: 'Aktionen'\n      none: Keine Aktionen.\n  addContact:\n    title: Lass deine Freunden deinen QR Code einscannen, teile deinen Link, oder scannen den QR Code eines Freundes ein\n  chat:\n    welcome: Willkommen!\n    message: >\n      Du brauchst mindestens einen Freund, um anfangen zu können.'<br />''<br />' Danach kannst du mit deinen Kontakten chatten und über Kanäle mit Gruppen kommunizieren.\n    newMessages: Es existieren neuere Nachrichten\n    viewRecent: Letzte Nachrichten ansehen\n    unreadMessages: '{number} neue Nachrichten seit {time}'\n    markAllAsRead: Alle als gelesen markieren\n    errors:\n      contactNotFound: Dein Kontakt konnte nicht lokalisiert werden\n      channelNotFound: Der Kanal konnte nicht gefunden werden.\n    sender:\n      removed: Entfernt\n    messages:\n      received: 'Empfangen: {at}'\n    embedMenu:\n      code: Code oder Text Ausschnitt\n      embed: Aus URL hinzufügen\n      file: Datei von PC senden\n      video: Video Chat starten\n      audio: Audio Chat starten\n    codeModal:\n      title: Sende Code oder Ausschnitt an '<strong>'{to}'</strong>'\n  faq:\n    title: Häufig gestellte Fragen (FAQ)\n    whatIsQ: Was ist Emberclear?\n    whatIsA: >\n      Emberclear ist ein quelloffener und verschlüsselter p2p Chat Client, der via Open Source Mesh Knoten im kostenlosen Kontingent der großen Cloud Anbieter läuft.\n    howDoesWorkQ: Wie funtioniert das?\n    howDoesWorkA: >\n      Die gesamte Verschlüsselung findet lokal im Browser statt -- zu keiner Zeit verlassen die privaten Schlüssel deinen Computer. Emberclear nutzt Libsodium's öffentliche Schlüssel-Kryptographie, um die Sicherheit der Nachrichten über das gesamte Internet und in jedem Netzwerk zu garantieren -- selbst wenn das genutzte Netzwerk bereits kompromitiert ist.\n    whyQ: Warum?\n    whyA: >\n      Zum Einen aus Spaß, zum Anderen aus der Freude am Lernen -- Ich wollte schon immer ein Framework für Entwickler erstellen, welches diese dann in bestehende Web Anwendungen integrieren können. Dieses Projekt wurde außerdem von meinem Misstrauen gegenüber bestehender Chat Apps und deren Infrastruktur inspiriert. Am Ende soll das Projekt über alle Funktion der populären Messenger verfügen -- mit einem Unterschied: Weder Server, Relays noch die förderierten Knoten tracken deine Inhalte.\n    notOnlineQ: Was passiert wenn ich eine Nachricht sende und der Empfänger nich mehr online ist.\n    notOnlineA: >\n      Der Server speichert keine Nachrichten und besitzt auch keine Warteschlange für nicht zugestellte Inhalte. Wenn ein Empfenger nicht online ist, muss die Nachricht erneut manuell gesendet werden.\n    serverStorageQ: Speichert der Server / das Relay meine Nachrichten?\n    serverStorageA: \"Nein.\"\n  footer:\n    navigation: Navigation\n    about: Über\n    wantToSupport: Möchtest du das Projekt unterstützen?\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Willkommen zurück!\"\n    instructions: Bitte füge oder tippe deine mnemonic Passphrase ein\n    loading: '{num} Kontakte geladen...'\n    success: \"Angemeldet!\"\n    warning: \"Beachten, dass dein Konto ohne ein anderes eingeloggtes Gerät oder deines Mnemonic nicht wiederhergestellt werden kann, da kein zentraler Speicher zum Wiederherstellen vorhanden ist.\"\n    invalidState: 'Ein unbekannter Fehler trat auf beim Versuch, die Registrierung oder Anmeldung vorzubereiten'\n    transfer:\n      title: \"Mit QR-Code anmelden\"\n      prompt: \"Scannen dies mit emberclear um sich anzumelden und deine Daten sofort zu übertragen.\"\n      establishConnection: \"Verbindung herstellen...\"\n      inProgress: Datenübertragung läuft...\n      success: \"Du bist daran, dich auf dem anderen Gerät anzumelden!\"\n    verify:\n      title: Verifizieren\n      prompt1: \"Möchtest du dich bei einem neuen Gerät anmelden?\"\n      prompt2: \"Stelle sicher, dass der Code mit dem Code unter dem QR Image auf dem Gerät übereinstimmt.\"\n      received: Login Anfrage erhalten\n      waitingOnApproval: Warten auf Genehmigung\n      receivedData: Empfangene Daten\n      importing: Importierte Daten\n      failed: \"Etwas ist schiefgelaufen. Bitte versuche es erneut.\"\n  setup:\n    overwriteTitle: Möchtest du wirklich eine neue Identität anlegen?\n    overwriteQuestion: >\n      Dies ist eine zerstörerische Handlung, welche die aktuell konfigurierte Identität löscht und ersetzt. Es existiert keine Möglichkeit zur Wiederherstellung und ohne die mnemonic Passphrase wirst du dich auch nicht mehr mit zuvor konfigurerten Chats verbinden können. '<br>' '<br>' Möchtest du wirklich fortfahren?\n    overwriteAbort: Nein, bring mich zurück in Sicherheit\n    overwriteConfirm: Ja, ich bin mir der Risiken bewusst\n    introQuestion: Wie möchtest du genannt werden?\n    almostReady: Du hast es fast geschafft!\n    nameLabel: Dein gewünschter Alias\n    mnemonicPrompt: >\n      Wenn du einen Account auch auf anderen Computern nutzen willst, dann speichere diese mnemonic Passphrase an einem sicheren Ort. Diese wird zum Anmelden benötigt.\n    note: >\n      Du kannst jederzeit deine Einstellungen herunterladen, um diese dann später auf einem anderen PC nutzen zu können. Die Einstellungen beinhalten mehr als nur deine Identität.\n  contacts:\n    title: 'Kontakte ({number})'\n    noContacts: Du hast noch keine Kontakte. Über das Benutzer Menü in der oberen rechten Ecke kannst du welche importieren.\n  logout:\n    title: Abmelden löscht die vorhandenen Daten\n    warning: >\n      Das Abmelden führt zur Löschung aller lokalen Daten. Bitte stelle sicher, dass du dir deine mnemonic Passphrase aufgeschrieben oder gemerkt hast. Herunterladen kannst du deine Einstellungen alternativ auch unter\n    theSettingsPage: der Einstellungsseite.\n    confirm: Abmeldung bestätigen\n  settings:\n    title: Einstellungen\n    tabs:\n      profile: Profilinfo\n      interface: Benutzeroberfläche\n      permissions: Berechtigungen\n      relays: Relais\n      dangerZone: Gefahrenzone\n    relays:\n      add: Relais hinzufügen\n      URLs: URLs\n      socket: Socket\n      openGraph: Open Graph\n      connectedUsers: Verbundene Nutzer\n    themes:\n      title: Themen\n      midnight: Mitternacht\n    copyProfileToDevice: Profil auf Gerät kopieren\n    hideKey: Privaten Schlüssel ausblenden\n    showKey: Privaten Schlüssel anzeigen\n    download: Einstellungen herunterladen\n    hideOfflineContacts: Offline Kontakte ausblenden\n    useLeftRightJustificationForMessages: Nachrichten Links/Rechts Ausrichten\n    danger:\n      title: Gefahrenzone\n      deleteMessages: Nachrichten löschen\n"
  },
  {
    "path": "client/web/emberclear/translations/en-AU.yaml",
    "content": "---\nappname: emberclear\nsubheader: Encrypted Chat. No History. No Logs.\nauthoredBy: by\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'TODO: fill this out correctly'\ninstall: Install\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: QR Code could not be generated\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Connecting...\n  connected: Connected!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: An error occurred in the socket connection!\n      close: The socket connection has been dropped!\n    timeout: There is a networking issue. waiting...\n  errors:\n    send:\n      notConnected: Cannot send messages without a connection!\n    subscribe:\n      notConnected: Cannot subscribe to a channel without a socket connection!\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: delete\n  resend: resend\n  resendAutomatically: resend automatically\n  enable: Enable\n  save: Save\n  next: Next\n  back: Back\n  begin: Begin\n  logout: Logout\n  login: Login\n  cancel: Cancel\n  close: Close\n  import: Import\n  scan: Scan\n  sendSnippet: Send Snippet\n  remove: Remove\n  donate: Donate\n  donateEth: Donate Ethereum\n  donateXmr: Donate Monero\n  loginInstead: Login instead\n  uploadSettings: Upload Settings\n  showMyInfo: Show My Info\n  invite: Invite\n  addFriend: Add Friend\n  makeDefault: Make Default\n  ariaLabel:\n    sidebar: 'Toggles the sidebar'\n    dropdownBackdrop: Clickable backdrop for clicking off a dropdown to close it\n    modalBackdrop: Clickable backdrop for clicking off a modal to close it\n    menuBackdrop: Clickable backdrop for clicking off the menu to close it\n    sidebarBackdrop: Clickable backdrop for clicking off the sidebar to hide it\n    closeModal: Close Modal\ncompatibility:\n  title: Something is wrong!\n  description: In order to use emberclear, your browser must support the following technologies and features\n  score: Score\n  required: Required\n  compatibility: Compatibility\n  camera: Camera\n  notifications: Notifications\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Thumbnail\n    ownIdentityQR: This is your identity information represented as a QR Code\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Message could not be delivered'\n  importing: Importing ...\n  updateAvailable: A new version is available, click here to update.\nservices:\n  crypto:\n    keyGenFailed: Key Generation Failed\nroutes:\n  home: Home\n  chat: Chat\n  qr: QR\n  logout: Logout\n  login: Login\n  contacts: Contacts\n  settings: Settings\n  profile: Profile\n  faq: F.A.Q.\n  createNewUser: Create New User\nmodels:\n  identity:\n    name: Name\n    publicKey: Public Key\n  message:\n    autosendPending: autosend pending\n    errors:\n      timeout: 'Sending timed out.'\nui:\n  invite:\n    copyProfile: Copy Link\n    copied: Copied!\n  notifications:\n    title: emberclear message\n    from: message from {name}\n    prompt:\n      title: emberclear needs your permission to enable notifications.\n      enable: Enable Notifications\n      askLater: Ask Next Time\n      neverAsk: Never Ask Again\n  search:\n    title: Search for contacts and channels\n    description: Search coming soon\n    noContacts: No contacts found\n    noChannels: No channels found\n    contacts: Contacts\n    channels: Channels\n    nothingFound: No results found\n  shortcuts:\n    title: Keyboard Shortcuts\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: space\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Search Everything\n      toggleSidebar: Toggle the Sidebar\n      toggleHelp: Toggle Keyboard Help\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Unread\n    channels:\n      title: Channels\n      none: You are not in any channels\n      placeholder: New Channel Name\n    contacts:\n      title: 'Contacts'\n      none: You have no contacts.\n      numOffline: '{num} offline'\n    actions:\n      title: 'Actions'\n      none: No actions at this time.\n  addContact:\n    title: Have a friend scan your QR Code, share your link, or scan their QR Code\n  chat:\n    welcome: Welcome!\n    message: >\n      To get started, you'll need at least one friend.'<br />''<br />' Then you'll be able to chat with them and create channels for communicating with groups of friends.\n    newMessages: There are newer messages\n    viewRecent: view recent messages\n    unreadMessages: '{number} new messages since {time}'\n    markAllAsRead: Mark all as read\n    errors:\n      contactNotFound: Your contact could not be located\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: removed\n    messages:\n      received: 'Received: {at}'\n    embedMenu:\n      code: Code or text snippet\n      embed: Embed from URL\n      file: Send File from Computer\n      video: Start Video Chat\n      audio: Start Audio Chat\n    codeModal:\n      title: Send Code or Snippet to '<strong>'{to}'</strong>'\n  faq:\n    title: F.A.Q.\n    whatIsQ: What is emberclear\n    whatIsA: >\n      emberclear is the open source p2p encrypted chat client that operates over open source mesh nodes on free-tier cloud services.\n    howDoesWorkQ: How does it work?\n    howDoesWorkA: >\n      All the encryption is done in the browser -- at no time do private keys leave your computer. Emberclear uses libsodium's public key-cryptography to ensure cryptographically safe messaging across the internet, on any network -- even if the network is compromised.\n    whyQ: Why?\n    whyA: >\n      Partially for fun and learning -- wanting to provide a framework for people wanting to get in to web applications so that they pre-existing tools that can interface with their creations. This project is also inspired by a lack of trust in existing chat apps and their infrastructure. Ultimately, this project, by functionality, will be a clone of whatever is popular - with the one key difference that nothing is ever tracked by the relays / federated nodes.\n    notOnlineQ: What happens when I send a message and the recipient is no longer online.\n    notOnlineA: >\n      The server does not store any messages, and does not keep a queue of undelivered messages. If a recipient is not online, the message will manually need to be resent.\n    serverStorageQ: Does the server / relay store any of my messages ever?\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Navigation\n    about: About\n    wantToSupport: Want to support this project?\n    license: >\n      The '<a href=\"https://github.com/NullVoxPopuli/emberclear\" target=\"_blank\" rel=\"noopener\">'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target=\"_blank\" rel=\"noopener\">'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Please paste or type your mnemonic key.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n  setup:\n    overwriteTitle: Are you sure you want to create a new identity?\n    overwriteQuestion: >\n      This is a destructive action, that will cause the currently configured identity to be forgotten and replaced. There is no way to recover, and without having the mnemonic saved somewhere, you will be unable to connect with any of the previously configured chats as no one will know who you are. '<br>' '<br>' Are you sure you want to do this?\n    overwriteAbort: No, take me back to safety\n    overwriteConfirm: Yes, I understand the risks\n    introQuestion: What would you like to be called?\n    almostReady: You are almost ready to begin chatting!\n    nameLabel: Your desired alias\n    mnemonicPrompt: >\n      If you would like to use this account on other computers, please store this mnemonic in a secure place. It will be used to login.\n    note: >\n      You may download your settings at any time so that you can upload them to another computer. The settings will include more than just your identity.\n  contacts:\n    title: 'Contacts ({number})'\n    noContacts: You do not have any contacts. Feel free to import some from your user menu in the top-right corner.\n  logout:\n    title: Logging out is destructive\n    warning: >\n      Logging out will delete all local data. Please make sure you either have your mnemonic written down or memorized. Alternatively, you download your settings on\n    theSettingsPage: the Settings page.\n    confirm: Confirm Logout\n  settings:\n    title: Settings\n    tabs:\n      profile: Profile Info\n      interface: Interface\n      permissions: Permissions\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n    copyProfileToDevice: Copy Profile to Device\n    hideKey: Hide private key\n    showKey: Show private key\n    download: Download Settings\n    hideOfflineContacts: Hide offline contacts\n    useLeftRightJustificationForMessages: Use Left/Right Message Justification\n    danger:\n      title: Danger Zone\n      deleteMessages: Delete Messages\n"
  },
  {
    "path": "client/web/emberclear/translations/en-us.yaml",
    "content": "---\nappname: emberclear\nsubheader: Encrypted Chat. No History. No Logs.\nauthoredBy: by\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\n\ntodo: 'TODO: fill this out correctly'\ninstall: Install\n\nbundle-analysis: JS Bundle Analysis\n\nqrCode:\n  error: QR Code could not be generated\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaiting Camera Access\n\nconnection:\n  connecting: Connecting...\n  connected: Connected!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n\n  status:\n    socket:\n      error: An error occurred in the socket connection!\n      close: The socket connection has been dropped!\n    timeout: There is a networking issue. waiting...\n\n  errors:\n    send:\n      notConnected: Cannot send messages without a connection!\n    subscribe:\n      notConnected: Cannot subscribe to a channel without a socket connection!\n\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\n\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: delete\n  resend: resend\n  resendAutomatically: resend automatically\n  enable: Enable\n  save: Save\n  next: Next\n  back: Back\n  begin: Begin\n  logout: Logout\n  login: Login\n  cancel: Cancel\n  close: Close\n  import: Import\n  scan: Scan\n  sendSnippet: Send Snippet\n  remove: Remove\n  donate: Donate\n  donateEth: Donate Ethereum\n  donateXmr: Donate Monero\n  loginInstead: Login instead\n  uploadSettings: Upload Settings\n  showMyInfo: Show My Info\n  invite: Invite\n  addFriend: Add Friend\n  makeDefault: Make Default\n  ariaLabel:\n    sidebar: 'Toggles the sidebar'\n    dropdownBackdrop: Clickable backdrop for clicking off a dropdown to close it\n    modalBackdrop: Clickable backdrop for clicking off a modal to close it\n    menuBackdrop: Clickable backdrop for clicking off the menu to close it\n    sidebarBackdrop: Clickable backdrop for clicking off the sidebar to hide it\n    closeModal: Close Modal\n\ncompatibility:\n  title: Something is wrong!\n  description: In order to use emberclear, your browser must support the following technologies and features\n  score: Score\n  required: Required\n  compatibility: Compatibility\n  camera: Camera\n  notifications: Notifications\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\n\n\nimages:\n  alt:\n    thumbnail: Thumbnail\n    ownIdentityQR: This is your identity information represented as a QR Code\n\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\n\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Message could not be delivered'\n  importing: Importing ...\n  updateAvailable: A new version is available, click here to update.\n\nservices:\n  crypto:\n    keyGenFailed: Key Generation Failed\n\nroutes:\n  home: Home\n  chat: Chat\n  qr: QR\n  logout: Logout\n  login: Login\n  contacts: Contacts\n  settings: Settings\n  profile: Profile\n  faq: F.A.Q.\n  createNewUser: Create New User\n\nmodels:\n  identity:\n    name: Name\n    publicKey: Public Key\n\n  message:\n    autosendPending: autosend pending\n    errors:\n      timeout: 'Sending timed out.'\n\nui:\n  invite:\n    copyProfile: Copy Link\n    copied: Copied!\n\n  notifications:\n    title: emberclear message\n    from: message from {name}\n    prompt:\n      title: emberclear needs your permission to enable chat notifications.\n      enable: Enable Notifications\n      askLater: Ask Next Time\n      neverAsk: Never Ask Again\n\n  search:\n    title: Search for contacts and channels\n    description: Search coming soon\n    noContacts: No contacts found\n    noChannels: No channels found\n    contacts: Contacts\n    channels: Channels\n    nothingFound: No results found\n\n\n  shortcuts:\n    title: Keyboard Shortcuts\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: space\n      tab: tab\n      h: h\n      k: k\n\n    actions:\n      search: Search Everything\n      toggleSidebar: Toggle the Sidebar\n      toggleHelp: Toggle Keyboard Help\n\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n\n    unread: Unread\n    channels:\n      title: Channels\n      none: You are not in any channels\n      placeholder: New Channel Name\n    contacts:\n      title: 'Contacts'\n      none: You have no contacts.\n      numOffline: '{num} offline'\n    actions:\n      title: 'Actions'\n      none: No actions at this time.\n\n  addContact:\n    title: Have a friend scan your QR Code, share your link, or scan their QR Code\n\n  chat:\n    welcome: Welcome!\n    message: >\n      To get started, you'll need at least one friend.'<br />'\n\n      '<br />'\n      Then you'll be able to chat with them and create channels for communicating with groups of friends.\n    newMessages: There are newer messages\n    viewRecent: view recent messages\n    unreadMessages: '{number} new messages since {time}'\n    markAllAsRead: Mark all as read\n    errors:\n      contactNotFound: Your contact could not be located\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: removed\n    messages:\n      received: 'Received: {at}'\n\n    embedMenu:\n      code: Code or text snippet\n      embed: Embed from URL\n      file: Send File from Computer\n      video: Start Video Chat\n      audio: Start Audio Chat\n\n    codeModal:\n      title: Send Code or Snippet to\n\n  faq:\n    title: F.A.Q\n    whatIsQ: What is emberclear\n    whatIsA: >\n      emberclear is the open source p2p encrypted chat\n      client that operates over open source\n      mesh nodes on free-tier cloud services.\n    howDoesWorkQ: How does it work?\n    howDoesWorkA: >\n      All the encryption is done in the browser -- at no time do private keys leave your computer.\n      Emberclear uses libsodium's public key-cryptography to ensure cryptographically safe messaging\n      across the internet, on any network -- even if the network is compromised.\n    whyQ: Why?\n    whyA: >\n      Partially for fun and learning -- wanting to provide a framework\n      for people wanting to get in to web applications so that they pre-existing\n      tools that can interface with their creations.\n\n      This project is also inspired by a lack of trust in existing chat apps and their infrastructure.\n\n      Ultimately, this project, by functionality, will be a clone of whatever\n      is popular - with the one key difference that nothing is ever tracked\n      by the relays / federated nodes.\n\n    notOnlineQ: What happens when I send a message and the recipient is no longer online.\n    notOnlineA: >\n      The server does not store any messages, and does not keep a queue of undelivered messages.\n      If a recipient is not online, the message will manually need to be resent.\n\n    serverStorageQ: Does the server / relay store any of my messages ever?\n    serverStorageA: \"No.\"\n\n\n  footer:\n    navigation: Navigation\n    about: About\n    wantToSupport: Want to support this project?\n    license: >\n      The '<a href=\"https://github.com/NullVoxPopuli/emberclear\" target=\"_blank\" rel=\"noopener\">'source code'</a>'\n      uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target=\"_blank\" rel=\"noopener\">'GPL-3'</a>' license.\n\n  login:\n    title: \"Welcome Back!\"\n    instructions: Please paste or type your mnemonic key.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n\n  setup:\n    overwriteTitle: Are you sure you want to create a new identity?\n    overwriteQuestion: >\n      This is a destructive action, that will\n      cause the currently configured identity to be forgotten and\n      replaced. There is no way to recover, and without having the\n      mnemonic saved somewhere, you will be unable to connect with any\n      of the previously configured chats as no one will know who you are.\n      '<br>'\n      '<br>'\n      Are you sure you want to do this?\n    overwriteAbort: No, take me back to safety\n    overwriteConfirm: Yes, I understand the risks\n    introQuestion: What would you like to be called?\n    almostReady: You are almost ready to begin chatting!\n    nameLabel: Your desired alias\n    mnemonicPrompt: >\n      If you would like to use this account on other computers,\n      please store this mnemonic in a secure place. It will be used\n      to login.\n    note: >\n      You may download your settings at any time so that you can upload\n      them to another computer. The settings will include more than just\n      your identity.\n\n  contacts:\n    title: 'Contacts ({number})'\n    noContacts: No contacts found. Feel free to add one of your friends.\n\n  logout:\n    title: Logging out is destructive\n    warning: >\n      Logging out will delete all local data.\n      Please make sure you either have your\n      mnemonic written down or memorized.\n\n      Alternatively, you download your settings on\n    theSettingsPage: the Settings page.\n    confirm: Confirm Logout\n\n  settings:\n    title: Settings\n    tabs:\n      profile: Profile Info\n      interface: Interface\n      permissions: Permissions\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n\n    copyProfileToDevice: Copy Profile to Device\n    hideKey: Hide private key\n    showKey: Show private key\n    download: Download Settings\n    hideOfflineContacts: Hide offline contacts\n    useLeftRightJustificationForMessages: Use Left/Right Message Justification\n    danger:\n      title: Danger Zone\n      deleteMessages: Delete Messages\n"
  },
  {
    "path": "client/web/emberclear/translations/es-ES.yaml",
    "content": "---\nappname: emberclear\nsubheader: Chat encriptado. Sin historia. Sin registros.\nauthoredBy: por\nredditCommunity: Comunidad de Reddit\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'Por hacer: llenar esto correctamente'\ninstall: Instalar\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: No se pudo generar el código QR\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Conectando...\n  connected: Conectado!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: '¡Se ha producido un error en la conexión por socket!'\n      close: '¡La conexión por socket se ha caído!'\n    timeout: Problema de red. esperando...\n  errors:\n    send:\n      notConnected: '¡No se puede enviar mensajes sin conexión!'\n    subscribe:\n      notConnected: '¡No puede suscribirse a un canal sin una conexión de socket!'\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: eliminar\n  resend: volver a enviar\n  resendAutomatically: reenviar automáticamente\n  enable: Habilitar\n  save: Guardar\n  next: Siguiente\n  back: Regresar\n  begin: Comenzar\n  logout: Desconectar\n  login: Conectarse\n  cancel: Cancelar\n  close: Cerrar\n  import: Importar\n  scan: Escanear\n  sendSnippet: Enviar Snippet\n  remove: Remover\n  donate: Donate\n  donateEth: Donar Ethereum\n  donateXmr: Donar dinero\n  loginInstead: Iniciar sesión\n  uploadSettings: Configuración de carga\n  showMyInfo: Mostrar mi información\n  invite: Invitar\n  addFriend: Agregar amigo\n  makeDefault: Hacer Predeterminado\n  ariaLabel:\n    sidebar: 'Ocultar/mostrar barra lateral'\n    dropdownBackdrop: Elemento accionable para cerrar el menú desplegable\n    modalBackdrop: Elemento accionable para cerrar el modal\n    menuBackdrop: Elemento accionable para cerrar el menú\n    sidebarBackdrop: Elemento accionable para cerrar la barra lateral para esconderla\n    closeModal: Cerrar Modal\ncompatibility:\n  title: '¡Algo salió mal!'\n  description: Para poder utilizar el emberclear, su navegador debe soportar las siguientes tecnologías y características\n  score: Calificación\n  required: Required\n  compatibility: Compatibilidad\n  camera: Cámara\n  notifications: Notificaciones\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Vista previa\n    ownIdentityQR: Esta es tu información de identidad representada como un código QR\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Conectado\n  enabled: Habilitado\n  deliveryFailed: 'Su mensaje no ha podido ser entregado'\n  importing: Importando ...\n  updateAvailable: Hay una nueva versión disponible, da clic aquí para actualizar.\nservices:\n  crypto:\n    keyGenFailed: Error en la generación de la llave\nroutes:\n  home: Inicio\n  chat: Chat\n  qr: QR\n  logout: Desconectar\n  login: Conectarse\n  contacts: Contactos\n  settings: Configuración\n  profile: Perfil\n  faq: Preguntas frecuentes\n  createNewUser: Crear Nuevo Usuario\nmodels:\n  identity:\n    name: Nombre\n    publicKey: Llave Pública\n  message:\n    autosendPending: reenvío automático pendiente\n    errors:\n      timeout: 'Tiempo de espera agotado.'\nui:\n  invite:\n    copyProfile: Copiar enlace\n    copied: '¡Copiado!'\n  notifications:\n    title: mensaje de emberclear\n    from: mensaje de {name}\n    prompt:\n      title: emberclear necesita su permiso para habilitar las notificaciones.\n      enable: Habilitar notificaciones\n      askLater: Preguntar siempre\n      neverAsk: No volver a preguntar\n  search:\n    title: Buscar por contactos y canales\n    description: La búsqueda vendrá pronto\n    noContacts: No se han encontrado contactos\n    noChannels: Ningún canal encontrado\n    contacts: Contactos\n    channels: Canales\n    nothingFound: No se encontraron resultados\n  shortcuts:\n    title: Atajos de teclado\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: space\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Buscar en todo\n      toggleSidebar: Ocultar/mostrar barra lateral\n      toggleHelp: Ocultar/Mostar ayuda de teclado\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Sin leer\n    channels:\n      title: Canales\n      none: No estás en ningún canal\n      placeholder: Nombre del Canal\n    contacts:\n      title: 'Contactos'\n      none: No tienes contactos.\n      numOffline: '{num} fuera de línea'\n    actions:\n      title: 'Acciones'\n      none: No hay acciones en este momento.\n  addContact:\n    title: Pide a un amigo que escanee tu Código QR, comparte tu enlace, o escanea su código QR\n  chat:\n    welcome: '¡Bienvenido!'\n    message: >\n      Para comenzar, vas a necesitar al menos un amigo.'<br />''<br />' Después vas a poder conversar con ellos y crear canales para comunicarte con grupos de amigos.\n    newMessages: Hay nuevos mensajes\n    viewRecent: ver mensajes recientes\n    unreadMessages: '{number} mensajes nuevos desde {time}'\n    markAllAsRead: Marcar todo como leído\n    errors:\n      contactNotFound: Tu contacto no pudo ser ubicado\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: eliminado\n    messages:\n      received: 'Recibido: {at}'\n    embedMenu:\n      code: Código o texto\n      embed: Añadir desde URL\n      file: Seleccionar archivo de mi computadora\n      video: Iniciar Video Chat\n      audio: Iniciar Chat de Audio\n    codeModal:\n      title: Enviar código o fragmento a '<strong>'{to}'</strong>'\n  faq:\n    title: Preguntas frecuentes\n    whatIsQ: Qué es emberclear\n    whatIsA: >\n      emberclear es el cliente de chat encriptado p2p que opera sobre nodos de malla de código libre y que corre en servicios gratuitos en la nube.\n    howDoesWorkQ: '¿Cómo funciona?'\n    howDoesWorkA: >\n      Toda la encriptación es hecha en el navegador, -- las llaves privadas nunca salen de tu computadora. Emberclear utiliza la llave criptográfica pública de libsodium para asegurar el intercambio de mensajes encriptados a través del internet y en cualquier red -- inclusive si la red está comprometida.\n    whyQ: '¿Por què?'\n    whyA: >\n      Por diversión y aprendizaje más que nada -- con el deseo de proveer un framework para la gente que quiere meterse en aplicaciónes web para que sus herramientas actuales puedan interactuar con sus creaciones. Este proyecto está inspirado también en la falta de confianza en las aplicaciones de chat existentes y de su infraestructura. Por último, la funcionalidad de este proyecto será siempre un clon de lo que sea popular - con la única diferencia de que nunca nada es comprometido por los nodos relays / federated.\n    notOnlineQ: Qué pasa cuando mando un mensaje y la persona ya no está conectada.\n    notOnlineA: >\n      El servidor no guarda ningún mensaje y tampoco mantiene una lista de mensajes por enviar. Si una persona no está en línea, el mensaje tiene que ser reenviado manualmente.\n    serverStorageQ: '¿El servidor / relay almacena alguno de mis mensajes alguna vez?'\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Navegación\n    about: About\n    wantToSupport: '¿Quieres soportar este proyecto?'\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Por favor pega o escribe tu llave mnemonic.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n  setup:\n    overwriteTitle: '¿Estás seguro de crear una nueva identidad?'\n    overwriteQuestion: >\n      Esta es una acción destructiva que causará que la identidad actual sea olvidada y reemplazada. No hay manera de recuperarla y sin tener la mnemonic guardada en algún lugar, no podrás conectarte con ninguno de los chats previos ya que ninguno sabrá quién eres. '<br>''<br>' ¿Estás seguro de querer hacer esto?\n    overwriteAbort: No, regresar\n    overwriteConfirm: Sí, entiendo los riesgos\n    introQuestion: '¿Como quieras ser llamado?'\n    almostReady: '¡Estás casi listo para comenzar a chatear!'\n    nameLabel: Tu alias deseado\n    mnemonicPrompt: >\n      Si quieres utilizar esta cuenta en otras computadoras, por favor guarda este mnemonic en un lugar seguro, será utilizado para iniciar sesión.\n    note: >\n      Puedes descargar tu configuración en cualquier momento para que puedas importarlo en otra computadora. Esta configuración incluye más que sólo tu identidad.\n  contacts:\n    title: 'Contactos ({number})'\n    noContacts: No tienes ningún contacto. No dudes en importar algunos desde el menú de usuario en la esquina superior derecha.\n  logout:\n    title: Cerrar sesión es destructivo\n    warning: >\n      Cerrar la sesión va a borrar todos los datos locales. Por favor asegúrate de tener tu mnemonic apuntada o memorizada. O bien, puedes descargar la configuración en\n    theSettingsPage: la página de Configuración.\n    confirm: Confirmar el cierre de sesión\n  settings:\n    title: Configuración\n    tabs:\n      profile: Información de perfil\n      interface: Interface\n      permissions: Permisos\n      relays: Relays\n      dangerZone: Zona Peligrosa\n    relays:\n      add: Añadir Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n    copyProfileToDevice: Copiar perfil a dispositivo\n    hideKey: Ocultar clave privada\n    showKey: Mostrar clave privada\n    download: Configurar descargas\n    hideOfflineContacts: Ocultar los contactos sin conexión\n    useLeftRightJustificationForMessages: Justificar Mensaje a Izquierda/Derecha\n    danger:\n      title: Zona de Peligro\n      deleteMessages: Eliminar mensajes\n"
  },
  {
    "path": "client/web/emberclear/translations/fr-FR.yaml",
    "content": "---\nappname: emberclear\nsubheader: Tchat cryptée. Pas d'historique. Pas de log.\nauthoredBy: par\nredditCommunity: Communautée Reddit\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'A faire: Remplir ceci correctement'\ninstall: Install\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: QR Code n'a pas pu être généré\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Connexion en cours...\n  connected: Connecté !\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: Une erreur s'est produite dans la connexion socket !\n      close: La connexion de socket a été abandonnée !\n    timeout: Problème réseau. Veuillez patienter ...\n  errors:\n    send:\n      notConnected: Impossible d’envoyer des messages sans connexion !\n    subscribe:\n      notConnected: Impossible de s’abonner à une chaine sans une connexion socket !\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: supprimer\n  resend: ré-envoyer\n  resendAutomatically: renvoyer automatiquement\n  enable: Activer\n  save: Sauvegarder\n  next: Suivant\n  back: Précédent\n  begin: Démarrer\n  logout: Se déconnecter\n  login: Se connecter\n  cancel: Annuler\n  close: Fermer\n  import: Importer\n  scan: Scanner\n  sendSnippet: Envoyer un Snippet\n  remove: Supprimer\n  donate: Donate\n  donateEth: Faire un donation en Ethereum\n  donateXmr: Faire un donation en Monero\n  loginInstead: Connexion à la place\n  uploadSettings: Uploader les paramètres\n  showMyInfo: Afficher mes infos\n  invite: Inviter\n  addFriend: Ajouter un ami\n  makeDefault: Définir par défaut\n  ariaLabel:\n    sidebar: 'Afficher/Masquer la barre latérale'\n    dropdownBackdrop: Clickable backdrop for clicking off a dropdown to close it\n    modalBackdrop: Clickable backdrop for clicking off a modal to close it\n    menuBackdrop: Clickable backdrop for clicking off the menu to close it\n    sidebarBackdrop: Clickable backdrop for clicking off the sidebar to hide it\n    closeModal: Fermer\ncompatibility:\n  title: Something is wrong!\n  description: In order to use emberclear, your browser must support the following technologies and features\n  score: Score\n  required: Required\n  compatibility: Compatibility\n  camera: Camera\n  notifications: Notifications\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Aperçu\n    ownIdentityQR: This is your identity information represented as a QR Code\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Message could not be delivered'\n  importing: Importation en cours...\n  updateAvailable: Une nouvelle version est disponible, cliquez ici pour mettre à jour.\nservices:\n  crypto:\n    keyGenFailed: La génération de clé a échoué\nroutes:\n  home: Accueil\n  chat: Chat\n  qr: QR\n  logout: Se déconnecter\n  login: Se connecter\n  contacts: Contacts\n  settings: Réglages\n  profile: Profil\n  faq: F.A.Q.\n  createNewUser: Créer un nouvel utilisateur\nmodels:\n  identity:\n    name: Nom\n    publicKey: Clé publique\n  message:\n    autosendPending: autosend pending\n    errors:\n      timeout: 'Sending timed out.'\nui:\n  invite:\n    copyProfile: Copier le Lien\n    copied: Copié !\n  notifications:\n    title: emberclear message\n    from: message de {name}\n    prompt:\n      title: emberclear a besoin de votre autorisation pour activer les notifications.\n      enable: Activer les notifications\n      askLater: Ask Next Time\n      neverAsk: Never Ask Again\n  search:\n    title: Search for contacts and channels\n    description: Search coming soon\n    noContacts: No contacts found\n    noChannels: No channels found\n    contacts: Contacts\n    channels: Channels\n    nothingFound: No results found\n  shortcuts:\n    title: Raccourcis clavier\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: space\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Search Everything\n      toggleSidebar: Afficher/Masquer la barre latérale\n      toggleHelp: Toggle Keyboard Help\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Non lu\n    channels:\n      title: Chaînes\n      none: You are not in any channels\n      placeholder: New Channel Name\n    contacts:\n      title: 'Contacts'\n      none: You have no contacts.\n      numOffline: '{num} hors connexion'\n    actions:\n      title: 'Actions'\n      none: Aucune action pour le moment\n  addContact:\n    title: Have a friend scan your QR Code, share your link, or scan their QR Code\n  chat:\n    welcome: Bienvenue !\n    message: >\n      To get started, you'll need at least one friend.'<br />''<br />' Then you'll be able to chat with them and create channels for communicating with groups of friends.\n    newMessages: There are newer messages\n    viewRecent: voir les messages récents\n    unreadMessages: '{number} new messages since {time}'\n    markAllAsRead: Marquer tout comme lu\n    errors:\n      contactNotFound: Your contact could not be located\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: supprimée\n    messages:\n      received: 'Reçu le: {at}'\n    embedMenu:\n      code: Code or text snippet\n      embed: Embed from URL\n      file: Send File from Computer\n      video: Start Video Chat\n      audio: Start Audio Chat\n    codeModal:\n      title: Send Code or Snippet to '<strong>'{to}'</strong>'\n  faq:\n    title: F.A.Q\n    whatIsQ: What is emberclear\n    whatIsA: >\n      emberclear is the open source p2p encrypted chat client that operates over open source mesh nodes on free-tier cloud services.\n    howDoesWorkQ: Comment ça marche ?\n    howDoesWorkA: >\n      All the encryption is done in the browser -- at no time do private keys leave your computer. Emberclear uses libsodium's public key-cryptography to ensure cryptographically safe messaging across the internet, on any network -- even if the network is compromised.\n    whyQ: Pourquoi ?\n    whyA: >\n      Partially for fun and learning -- wanting to provide a framework for people wanting to get in to web applications so that they pre-existing tools that can interface with their creations. This project is also inspired by a lack of trust in existing chat apps and their infrastructure. Ultimately, this project, by functionality, will be a clone of whatever is popular - with the one key difference that nothing is ever tracked by the relays / federated nodes.\n    notOnlineQ: What happens when I send a message and the recipient is no longer online.\n    notOnlineA: >\n      The server does not store any messages, and does not keep a queue of undelivered messages. If a recipient is not online, the message will manually need to be resent.\n    serverStorageQ: Does the server / relay store any of my messages ever?\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Navigation\n    about: About\n    wantToSupport: Want to support this project?\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Please paste or type your mnemonic key.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n  setup:\n    overwriteTitle: Are you sure you want to create a new identity?\n    overwriteQuestion: >\n      This is a destructive action, that will cause the currently configured identity to be forgotten and replaced. There is no way to recover, and without having the mnemonic saved somewhere, you will be unable to connect with any of the previously configured chats as no one will know who you are. '<br>' '<br>' Are you sure you want to do this?\n    overwriteAbort: No, take me back to safety\n    overwriteConfirm: Oui, je comprends les risques\n    introQuestion: What would you like to be called?\n    almostReady: You are almost ready to begin chatting!\n    nameLabel: Your desired alias\n    mnemonicPrompt: >\n      If you would like to use this account on other computers, please store this mnemonic in a secure place. It will be used to login.\n    note: >\n      You may download your settings at any time so that you can upload them to another computer. The settings will include more than just your identity.\n  contacts:\n    title: 'Contacts ({number})'\n    noContacts: You do not have any contacts. Feel free to import some from your user menu in the top-right corner.\n  logout:\n    title: Logging out is destructive\n    warning: >\n      Logging out will delete all local data. Please make sure you either have your mnemonic written down or memorized. Alternatively, you download your settings on\n    theSettingsPage: the Settings page.\n    confirm: Confirmer la déconnexion\n  settings:\n    title: Réglages\n    tabs:\n      profile: Profile Info\n      interface: Interface\n      permissions: Permissions\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n    copyProfileToDevice: Copy Profile to Device\n    hideKey: Masquer la clé privée\n    showKey: Afficher la clé privée\n    download: Télécharger les paramètres\n    hideOfflineContacts: Masquer les contacts hors-ligne\n    useLeftRightJustificationForMessages: Use Left/Right Message Justification\n    danger:\n      title: Danger Zone\n      deleteMessages: Supprimer les messages\n"
  },
  {
    "path": "client/web/emberclear/translations/ko-KR.yaml",
    "content": "---\nappname: emberclear\nsubheader: Encrypted Chat. No History. No Logs.\nauthoredBy: by\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'TODO: fill this out correctly'\ninstall: Install\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: QR Code could not be generated\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Connecting...\n  connected: Connected!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: An error occurred in the socket connection!\n      close: The socket connection has been dropped!\n    timeout: There is a networking issue. waiting...\n  errors:\n    send:\n      notConnected: Cannot send messages without a connection!\n    subscribe:\n      notConnected: Cannot subscribe to a channel without a socket connection!\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: delete\n  resend: resend\n  resendAutomatically: resend automatically\n  enable: Enable\n  save: Save\n  next: Next\n  back: Back\n  begin: Begin\n  logout: Logout\n  login: Login\n  cancel: Cancel\n  close: Close\n  import: Import\n  scan: Scan\n  sendSnippet: Send Snippet\n  remove: Remove\n  donate: Donate\n  donateEth: Donate Ethereum\n  donateXmr: Donate Monero\n  loginInstead: Login instead\n  uploadSettings: Upload Settings\n  showMyInfo: Show My Info\n  invite: Invite\n  addFriend: Add Friend\n  makeDefault: Make Default\n  ariaLabel:\n    sidebar: 'Toggles the sidebar'\n    dropdownBackdrop: Clickable backdrop for clicking off a dropdown to close it\n    modalBackdrop: Clickable backdrop for clicking off a modal to close it\n    menuBackdrop: Clickable backdrop for clicking off the menu to close it\n    sidebarBackdrop: Clickable backdrop for clicking off the sidebar to hide it\n    closeModal: Close Modal\ncompatibility:\n  title: Something is wrong!\n  description: In order to use emberclear, your browser must support the following technologies and features\n  score: Score\n  required: Required\n  compatibility: Compatibility\n  camera: Camera\n  notifications: Notifications\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Thumbnail\n    ownIdentityQR: This is your identity information represented as a QR Code\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Message could not be delivered'\n  importing: Importing ...\n  updateAvailable: A new version is available, click here to update.\nservices:\n  crypto:\n    keyGenFailed: Key Generation Failed\nroutes:\n  home: Home\n  chat: Chat\n  qr: QR\n  logout: Logout\n  login: Login\n  contacts: Contacts\n  settings: Settings\n  profile: Profile\n  faq: F.A.Q.\n  createNewUser: Create New User\nmodels:\n  identity:\n    name: Name\n    publicKey: Public Key\n  message:\n    autosendPending: autosend pending\n    errors:\n      timeout: 'Sending timed out.'\nui:\n  invite:\n    copyProfile: Copy Link\n    copied: Copied!\n  notifications:\n    title: emberclear message\n    from: message from {name}\n    prompt:\n      title: emberclear needs your permission to enable notifications.\n      enable: Enable Notifications\n      askLater: Ask Next Time\n      neverAsk: Never Ask Again\n  search:\n    title: Search for contacts and channels\n    description: Search coming soon\n    noContacts: No contacts found\n    noChannels: No channels found\n    contacts: Contacts\n    channels: Channels\n    nothingFound: No results found\n  shortcuts:\n    title: Keyboard Shortcuts\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: space\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Search Everything\n      toggleSidebar: Toggle the Sidebar\n      toggleHelp: Toggle Keyboard Help\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Unread\n    channels:\n      title: Channels\n      none: You are not in any channels\n      placeholder: New Channel Name\n    contacts:\n      title: 'Contacts'\n      none: You have no contacts.\n      numOffline: '{num} offline'\n    actions:\n      title: 'Actions'\n      none: No actions at this time\n  addContact:\n    title: Have a friend scan your QR Code, share your link, or scan their QR Code\n  chat:\n    welcome: Welcome!\n    message: >\n      To get started, you'll need at least one friend.'<br />''<br />' Then you'll be able to chat with them and create channels for communicating with groups of friends.\n    newMessages: There are newer messages\n    viewRecent: view recent messages\n    unreadMessages: '{number} new messages since {time}'\n    markAllAsRead: Mark all as read\n    errors:\n      contactNotFound: Your contact could not be located\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: removed\n    messages:\n      received: 'Received: {at}'\n    embedMenu:\n      code: Code or text snippet\n      embed: Embed from URL\n      file: Send File from Computer\n      video: Start Video Chat\n      audio: Start Audio Chat\n    codeModal:\n      title: Send Code or Snippet to '<strong>'{to}'</strong>'\n  faq:\n    title: F.A.Q\n    whatIsQ: What is emberclear\n    whatIsA: >\n      emberclear is the open source p2p encrypted chat client that operates over open source mesh nodes on free-tier cloud services.\n    howDoesWorkQ: How does it work?\n    howDoesWorkA: >\n      All the encryption is done in the browser -- at no time do private keys leave your computer. Emberclear uses libsodium's public key-cryptography to ensure cryptographically safe messaging across the internet, on any network -- even if the network is compromised.\n    whyQ: Why?\n    whyA: >\n      Partially for fun and learning -- wanting to provide a framework for people wanting to get in to web applications so that they pre-existing tools that can interface with their creations. This project is also inspired by a lack of trust in existing chat apps and their infrastructure. Ultimately, this project, by functionality, will be a clone of whatever is popular - with the one key difference that nothing is ever tracked by the relays / federated nodes.\n    notOnlineQ: What happens when I send a message and the recipient is no longer online.\n    notOnlineA: >\n      The server does not store any messages, and does not keep a queue of undelivered messages. If a recipient is not online, the message will manually need to be resent.\n    serverStorageQ: Does the server / relay store any of my messages ever?\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Navigation\n    about: About\n    wantToSupport: Want to support this project?\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Please paste or type your mnemonic key.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n  setup:\n    overwriteTitle: Are you sure you want to create a new identity?\n    overwriteQuestion: >\n      This is a destructive action, that will cause the currently configured identity to be forgotten and replaced. There is no way to recover, and without having the mnemonic saved somewhere, you will be unable to connect with any of the previously configured chats as no one will know who you are. '<br>' '<br>' Are you sure you want to do this?\n    overwriteAbort: No, take me back to safety\n    overwriteConfirm: Yes, I understand the risks\n    introQuestion: What would you like to be called?\n    almostReady: You are almost ready to begin chatting!\n    nameLabel: Your desired alias\n    mnemonicPrompt: >\n      If you would like to use this account on other computers, please store this mnemonic in a secure place. It will be used to login.\n    note: >\n      You may download your settings at any time so that you can upload them to another computer. The settings will include more than just your identity.\n  contacts:\n    title: 'Contacts ({number})'\n    noContacts: You do not have any contacts. Feel free to import some from your user menu in the top-right corner.\n  logout:\n    title: Logging out is destructive\n    warning: >\n      Logging out will delete all local data. Please make sure you either have your mnemonic written down or memorized. Alternatively, you download your settings on\n    theSettingsPage: the Settings page.\n    confirm: Confirm Logout\n  settings:\n    title: Settings\n    tabs:\n      profile: Profile Info\n      interface: Interface\n      permissions: Permissions\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n    copyProfileToDevice: Copy Profile to Device\n    hideKey: Hide private key\n    showKey: Show private key\n    download: Download Settings\n    hideOfflineContacts: Hide offline contacts\n    useLeftRightJustificationForMessages: Use Left/Right Message Justification\n    danger:\n      title: Danger Zone\n      deleteMessages: Delete Messages\n"
  },
  {
    "path": "client/web/emberclear/translations/pt-PT.yaml",
    "content": "---\nappname: emberclear\nsubheader: Conversação encriptada. Sem histórico. Sem logs.\nauthoredBy: por\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'A fazer: preencher isto corretamente'\ninstall: Install\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: Código QR não pôde ser gerado\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: A ligar...\n  connected: Ligado!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: Ocorreu um erro na conexão do socket!\n      close: A conexão do socket caíu!\n    timeout: Há um problema de rede. A aguardar...\n  errors:\n    send:\n      notConnected: Não pode enviar mensagens sem uma conexão!\n    subscribe:\n      notConnected: Não pode subscrever um canal sem uma conexão de socket!\nerrors:\n  genericTitle: \"An Error occurred!\"\n  genericRetry: \"Oh no! Something went wrong. Retry?\"\n  notFound:\n    title: Not Found\n    help: The URL could not be navigated to. If the problem persists,\n    link: please file an issue.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Allow\n  deny: Deny\n  retry: Retry\n  delete: apagar\n  resend: reenviar\n  resendAutomatically: reenviar automaticamente\n  enable: Enable\n  save: Guardar\n  next: Seguinte\n  back: Anterior\n  begin: Começar\n  logout: Terminar Sessão\n  login: Iniciar sessão\n  cancel: Cancelar\n  close: Fechar\n  import: Importar\n  scan: Procurar\n  sendSnippet: Enviar snippet\n  remove: Remover\n  donate: Donate\n  donateEth: Doar Ethereum\n  donateXmr: Doar Monero\n  loginInstead: Iniciar sessão\n  uploadSettings: Definições de carregamento de ficheiros\n  showMyInfo: Mostrar minha informação\n  invite: Convidar\n  addFriend: Adicionar amigo\n  makeDefault: Make Default\n  ariaLabel:\n    sidebar: 'Ativa/desativar barra lateral'\n    dropdownBackdrop: Elemento acionável para fechar o menu suspenso\n    modalBackdrop: Elemento acionável para fechar o diálogo\n    menuBackdrop: Elemento acionável para fechar o menu\n    sidebarBackdrop: Elemento acionável para ocultar a barra lateral\n    closeModal: Fechar diálogo\ncompatibility:\n  title: Algo está errado!\n  description: Para usar o emberclear, o seu browser deve suportar as seguintes tecnologias e funcionalidades\n  score: Pontuação\n  required: Required\n  compatibility: Compatibilidade\n  camera: Câmera\n  notifications: Notificações\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Miniatura\n    ownIdentityQR: Esta é a tua informação de identidade representada como um código QR\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Mensagem não pôde ser entregue'\n  importing: A importar...\n  updateAvailable: Está disponível uma nova versão. Clique para atualizar.\nservices:\n  crypto:\n    keyGenFailed: Geração de chave falhou\nroutes:\n  home: Início\n  chat: Conversação\n  qr: QR\n  logout: Terminar Sessão\n  login: Iniciar sessão\n  contacts: Contactos\n  settings: Definições\n  profile: Perfil\n  faq: Questões Frequentes\n  createNewUser: Criar Novo Utilizador\nmodels:\n  identity:\n    name: Nome\n    publicKey: Chave Pública\n  message:\n    autosendPending: reenvio pendente\n    errors:\n      timeout: 'O envio expirou.'\nui:\n  invite:\n    copyProfile: Copiar ligação\n    copied: Copiado!\n  notifications:\n    title: mensagem de emberclear\n    from: mensagem de {name}\n    prompt:\n      title: emberclear precisa de sua permissão para ativar as notificações.\n      enable: Ativar Notificações\n      askLater: Perguntar da próxima vez\n      neverAsk: Nunca perguntar novamente\n  search:\n    title: Procurar contactos e canais\n    description: Pesquisa brevemente\n    noContacts: Nenhum contacto encontrado\n    noChannels: Nenhum canal encontrado\n    contacts: Contactos\n    channels: Canais\n    nothingFound: Nenhum resultado encontrado\n  shortcuts:\n    title: Atalhos de teclado\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: espaço\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Pesquisar tudo\n      toggleSidebar: Ativar/desativar barra lateral\n      toggleHelp: Ativar/desativar ajuda de teclado\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Não lido\n    channels:\n      title: Canais\n      none: Não estás em nenhum canal\n      placeholder: Nome do novo canal\n    contacts:\n      title: 'Contactos'\n      none: Não tens contactos.\n      numOffline: '{num} offline'\n    actions:\n      title: 'Ações'\n      none: Nenhuma ação no momento.\n  addContact:\n    title: Pede a um amigo que faça scan ao teu QR Code, partilhe o teu link ou faça scan do seu QR Code\n  chat:\n    welcome: Bem-vindo!\n    message: >\n      Para começar, precisas de pelo menos um amigo.'<br />''<br />' Depois poderás conversar com ele e criar canais para comunicar com grupos de amigos.\n    newMessages: Existem novas mensagens\n    viewRecent: ver mensagens recentes\n    unreadMessages: '{number} novas mensagens desde {time}'\n    markAllAsRead: Marcar todas como lidas\n    errors:\n      contactNotFound: O teu contacto não pôde ser localizado\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: removido\n    messages:\n      received: 'Recebido: {at}'\n    embedMenu:\n      code: Código ou excerto de texto\n      embed: Incorporar de um URL\n      file: Enviar ficheiro do computador\n      video: Iniciar conversação vídeo\n      audio: Iniciar conversação áudio\n    codeModal:\n      title: Enviar código ou excerto para '<strong>'{to}'</strong>'\n  faq:\n    title: Questões frequentes\n    whatIsQ: O que é emberclear\n    whatIsA: >\n      emberclear é o cliente de conversações p2p encriptado de código aberto que opera sobre nós de rede de código aberto em serviços de nuvem de camada livre.\n    howDoesWorkQ: Como funciona?\n    howDoesWorkA: >\n      Toda a encriptação é feita no browser -- em circunstância alguma as chaves privadas saem do computador. Emberclear usa a criptografia de chave pública libsodium para se certificar mensagens seguras e encriptadas na internet ou em qualquer rede -- mesmo que a rede esteja comprometida.\n    whyQ: Porquê?\n    whyA: >\n      Parcialmente por diversão e aprendizagem -- pretende-se fornecer uma estrutura para pessoas que desejam entrar em aplicativos web para que suas ferramentas pré-existentes possam interagir com suas criações. Este projeto também é inspirado pela falta de confiança em aplicativos de conversação existentes e na sua infraestrutura. Por fim, este projeto, no que toca à funcionalidade, será um clone do que for popular - com a única diferença fundamental de que nada é registado pelos nós de retransmissão / federados.\n    notOnlineQ: O que acontece quando envio uma mensagem e o destinatário já não está online?\n    notOnlineA: >\n      The server does not store any messages, and does not keep a queue of undelivered messages. If a recipient is not online, the message will manually need to be resent.\n    serverStorageQ: O servidor / retransmissor armazena alguma das minhas mensagens?\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Navegação\n    about: About\n    wantToSupport: Queres apoiar este projeto?\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Por favor cola ou digita a tua chave-mnemónica.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Transferring Data...\n      success: \"You are about to be logged in on the other device!\"\n    verify:\n      title: Verify\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Waiting for approval\n      receivedData: Received data\n      importing: Importing data\n      failed: \"Something went wrong. Please try again.\"\n  setup:\n    overwriteTitle: Tens a certeza que pretendes criar uma nova identidade?\n    overwriteQuestion: >\n      Esta é uma ação destrutiva, que fará com que a identidade atualmente configurada seja esquecida e substituída. Não há como recuperar, e sem ter a mnemónica guardada em algum lugar, não conseguirás ligar-te com nenhuma das conversações configuradas anteriormente, pois ninguém saberá quem és. '<br>' '<br>' Tens a certeza que queres fazer isto?\n    overwriteAbort: Não, regressar\n    overwriteConfirm: Sim, eu compreendo os riscos\n    introQuestion: Como gostarias de ser chamado?\n    almostReady: Estás quase pronto para começar a conversação!\n    nameLabel: A tua alcunha desejada\n    mnemonicPrompt: >\n      Se gostarias de usar esta conta em outros computadores, por favor guarda esta mnemónica num local seguro. Será usada para iniciar sessão.\n    note: >\n      Podes descarregar as tuas configurações a qualquer momento para poder enviá-las para outro computador. As configurações incluirão mais do que apenas tua identidade.\n  contacts:\n    title: 'Contactos ({number})'\n    noContacts: Não tens contatos. Sente-te à vontade para importar alguns do menu de utilizador no canto superior direito.\n  logout:\n    title: Terminar sessão é destrutivo\n    warning: >\n      Terminar sessão excluirá todos os dados locais. Por favor, certifica-te que tens a tua mnemónica escrita ou memorizada. Como alternativa, podes descarregar as tuas configurações em\n    theSettingsPage: a página de Configurações.\n    confirm: Confirmar terminar sessão\n  settings:\n    title: Configurações\n    tabs:\n      profile: Profile Info\n      interface: Interface\n      permissions: Permissions\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: socket\n      openGraph: open graph\n      connectedUsers: connected users\n    themes:\n      title: Themes\n      midnight: Midnight\n    copyProfileToDevice: Copiar perfil para dispositivo\n    hideKey: Esconder chave privada\n    showKey: Mostrar chave privada\n    download: Descarregar configurações\n    hideOfflineContacts: Esconder contactos offline\n    useLeftRightJustificationForMessages: Usar justificação de mensagens à esquerda/direita\n    danger:\n      title: Zona de perigo\n      deleteMessages: Eliminar mensagens\n"
  },
  {
    "path": "client/web/emberclear/translations/ru-RU.yaml",
    "content": "---\nappname: emberclear\nsubheader: Зашифрованный чат. Без истории. Без логов.\nauthoredBy: от\nredditCommunity: Reddit Community\ntwitter: Twitter\nemberjs: Ember.JS\ntodo: 'Позднее: заполнить правильно'\ninstall: Install\nbundle-analysis: JS Bundle Analysis\nqrCode:\n  error: Ошибка генерации QR кода\n  malformed: QR Code is malfomed\n  waitingForCamera: Awaaiting Camera Access\nconnection:\n  connecting: Подключение...\n  connected: Подключено!\n  disconnected: Disconnected.\n  degraded: Degraded\n  unknown: Unknown\n  status:\n    socket:\n      error: Ошибка подключения к сокету.\n      close: Соединение с сокетом было сброшено!\n    timeout: Сетевая ошибка. Ждём..\n  errors:\n    send:\n      notConnected: Не могу отправить сообщение без подключения к сети.\n    subscribe:\n      notConnected: Не могу подписаться на канал без подключения к сети.\nerrors:\n  genericTitle: \"Произошла ошибка!\"\n  genericRetry: \"О нет! Что-то пошло не так. Повторить?\"\n  notFound:\n    title: Не найдено\n    help: Страницу невозможно открыть. Если ошибка не исчезнет,\n    link: пожалуйста, сообщите о проблеме.\n  permissions:\n    enableCamera: You may need to allow this site access to your camera.\nbuttons:\n  allow: Разрешить\n  deny: Отклонить\n  retry: Retry\n  delete: удалить\n  resend: переотправить\n  resendAutomatically: переотправлять автоматически\n  enable: Включить\n  save: Сохранить\n  next: Далее\n  back: Назад\n  begin: В начало\n  logout: Выход\n  login: Логин\n  cancel: Отмена\n  close: Закрыть\n  import: Импорт\n  scan: Сканировать\n  sendSnippet: Отправить сниппет\n  remove: Удалить\n  donate: Donate\n  donateEth: Поддержать в Ethereum\n  donateXmr: Поддержать в Monero\n  loginInstead: Войти с логином/паролем\n  uploadSettings: Загрузить настройки\n  showMyInfo: Обо мне\n  invite: Пригласить\n  addFriend: Добавить друга\n  makeDefault: Сделать по умолчанию\n  ariaLabel:\n    sidebar: 'Переключить панель'\n    dropdownBackdrop: Кликните на фон для закрытия дропдауна\n    modalBackdrop: Кликните на фон для закрытия модального диалога\n    menuBackdrop: Кликните на фон для закрытия меню\n    sidebarBackdrop: Кликните на фон для скрытия панели\n    closeModal: Закрыть окно\ncompatibility:\n  title: Что-то пошло не так!\n  description: Для работы emberclear ваш браузер должен поддерживать следующие возможности\n  score: Поддержка\n  required: Обязательно\n  compatibility: Совместимость\n  camera: Камера\n  notifications: Оповещения\n  indexeddb: Indexed DB\n  wasm: WebAssembly\n  serviceWorker: Service Worker\n  webWorker: Web Worker\n  status:\n    exists: Browser has this capability\n    missing: Browser missing this capability\nimages:\n  alt:\n    thumbnail: Миниатюра\n    ownIdentityQR: Это ваш профиль, представленный в виде QR кода\ninput:\n  label:\n    mnemonic: Mnemonic\n    name: Name\nstatus:\n  connected: Connected\n  enabled: Enabled\n  deliveryFailed: 'Не удалось доставить сообщение'\n  importing: Импорт ...\n  updateAvailable: Доступна новая версия, нажмите сюда для обновления.\nservices:\n  crypto:\n    keyGenFailed: Ошибка при генерации ключа\nroutes:\n  home: Домой\n  chat: Чат\n  qr: QR\n  logout: Выход\n  login: Вход\n  contacts: Контакты\n  settings: Настройки\n  profile: Профиль\n  faq: F.A.Q.\n  createNewUser: Создать нового пользователя\nmodels:\n  identity:\n    name: Имя\n    publicKey: Публичный ключ\n  message:\n    autosendPending: ожидание отправки\n    errors:\n      timeout: 'Таймаут отправки.'\nui:\n  invite:\n    copyProfile: Копировать Ссылку\n    copied: Скопировано!\n  notifications:\n    title: emberclear сообщение\n    from: сообщение от {name}\n    prompt:\n      title: emberclear необходимо ваше разрешения для включения оповещений.\n      enable: Включить Оповещения\n      askLater: Спросить в другой раз\n      neverAsk: Больше не спрашивать\n  search:\n    title: Поиск контактов и каналов\n    description: Поиск скоро появится\n    noContacts: Контакты не найдены\n    noChannels: Каналы не найдены\n    contacts: Контакты\n    channels: Каналы\n    nothingFound: Результатов не найдено\n  shortcuts:\n    title: Горячие клавиши\n    key:\n      ctrl: ctrl\n      esc: esc\n      enter: enter\n      i: i\n      slash: /\n      space: пробел\n      tab: tab\n      h: h\n      k: k\n    actions:\n      search: Искать везде\n      toggleSidebar: Переключить панель\n      toggleHelp: Переключить подсказки клавиатуры\n    label:\n      search:\n        tab: to navigate\n        enter: to select\n        esc: to dismiss\n  sidebar:\n    find: Find...\n    keepTyping: '{num} characters left to search...'\n    results: '{num} results'\n    unread: Новые\n    channels:\n      title: Каналы\n      none: Вы не состоите не в одном канале\n      placeholder: Название нового канала\n    contacts:\n      title: 'Контакты'\n      none: У вас пока нет контактов.\n      numOffline: '{num} не в сети'\n    actions:\n      title: 'действия'\n      none: Никаких действий в это время\n  addContact:\n    title: Попросите друга отсканировать ваш QR код, отправив ему ссылку или отсканируйте его код\n  chat:\n    welcome: Добро пожаловать!\n    message: >\n      Для начала вам необходим хотя бы один контакт.'<br />''<br />' После чего вы сможете начать общение, создавать каналы и общаться с друзьями.\n    newMessages: Новые сообщения\n    viewRecent: посмотреть недавние сообщения\n    unreadMessages: '{number} новых сообщений с {time}'\n    markAllAsRead: Пометить все как прочитанные\n    errors:\n      contactNotFound: Не удаётся найти ваш контакт\n      channelNotFound: The channel could not be located.\n    sender:\n      removed: удалено\n    messages:\n      received: 'Получено: {at}'\n    embedMenu:\n      code: Фрагмент кода или текста\n      embed: Вставить по ссылке\n      file: Отправить файл с компьютера\n      video: Начать видео чат\n      audio: Начать аудио чат\n    codeModal:\n      title: Отправить код или сниппет для '<strong>'{to}'</strong>'\n  faq:\n    title: F.A.Q\n    whatIsQ: Что такое emberclear\n    whatIsA: >\n      emberclear - это зашифрованный p2p-клиент с открытым исходным кодом, работающий открытые сетевые узлы.\n    howDoesWorkQ: Как это работает?\n    howDoesWorkA: >\n      Всё шифрование происходит в браузере - на вашем компьютере ничего не сохраняется. Emberclear использует криптографию libsodium для обеспечения приватности ваших сообщений, даже в случае компроментации сетевого соединения.\n    whyQ: Зачем?\n    whyA: >\n      Частично для развлечения и обучения, как пример использования фреймворка, для людей, желающих создать своё собственное веб приложение. Этот проект создан после утраты доверия к существующим мессенджерам и их инфраструктуре. В конечном счете, этот проект по функциональности приблизится к существующим решениям, с одним ключевым отличием в том, что узлы, обслуживающие приложение ничего не отслеживают.\n    notOnlineQ: Что произойдёт если я отправил сообщение, а получатель не в сети.\n    notOnlineA: >\n      Сервер не хранит никаких сообщений и не сохраняет очередь недоставленных сообщений. Если получатель не в сети, сообщение необходимо будет переслать вручную.\n    serverStorageQ: Сервер хранит мои сообщения?\n    serverStorageA: \"No.\"\n  footer:\n    navigation: Навигация\n    about: About\n    wantToSupport: Хотите поддержать проект?\n    license: >\n      The '<a href='https://github.com/NullVoxPopuli/emberclear' target='_blank' rel='noopener'>'source code'</a>' uses the '<a href=\"https://opensource.org/licenses/GPL-3.0\" target='_blank' rel='noopener'>'GPL-3'</a>' license.\n  login:\n    title: \"Welcome Back!\"\n    instructions: Вставьте или напишите ваш мнемонический ключ.\n    loading: Loaded {num} Contacts...\n    success: \"Logged In!\"\n    warning: \"Note that without another logged in device or your mnemonic, your account will be unrecoverable as there is no central storage to recovery from.\"\n    invalidState: 'An unknown error occurred while trying to get things ready for signup or login'\n    transfer:\n      title: \"Log in with QR Code\"\n      prompt: \"Scan this with emberclear to login and have all your data transferred instantly.\"\n      establishConnection: \"Establishing Connection...\"\n      inProgress: Передача данных...\n      success: \"Вы собираетесь войти на другое устройство!\"\n    verify:\n      title: Подтвердить\n      prompt1: \"Do you want to login to a new device?\"\n      prompt2: \"Be sure that the code matches the code below the QR Image on the device.\"\n      received: Received login request\n      waitingOnApproval: Ожидает подтверждения\n      receivedData: Received data\n      importing: Импорт данных\n      failed: \"Что-то пошло не так. Попробуйте еще раз.\"\n  setup:\n    overwriteTitle: Вы уверены что хотите создать новый профиль?\n    overwriteQuestion: >\n      Это действие необратимо и приведёт к тому что текущий профиль будет забыт и перезаписан без возможности восстановления. Без сохранённой мнемонической фразы вы не сможете присоединиться к предыдущим чатам и никто не сможет вас идентифицировать. '<br>' '<br>' Вы уверены что хотите продолжить?\n    overwriteAbort: Нет, вернуться назад\n    overwriteConfirm: Да, я понимаю последствия\n    introQuestion: Как вас называть?\n    almostReady: Ваш профиль практически готов!\n    nameLabel: Никнейм\n    mnemonicPrompt: >\n      Если вы хотите использовать этот аккаунт на других компьютерах, пожалуйста сохраните мнемоническую фразу в безопасном месте. Она потребуется для логина.\n    note: >\n      Вы можете загрузить настройки в любое время, после чего перенести их на другой компьютер. Настройки хранят немного больше чем просто ваш аккаунт.\n  contacts:\n    title: 'Контакты ({number})'\n    noContacts: У вас пока нет контактов. Не стесняйтесь воспользоваться меню в правом верхнем углу для добавления новых.\n  logout:\n    title: Выход из системы необратим\n    warning: >\n      Выход из профиля приведёт к удалению всех сохранённых локально данных. Пожалуйста, убедитесь что вы запомнили или записали мнемонический ключ. Или, вы можете скачать ваши настройки\n    theSettingsPage: на странице Настроек.\n    confirm: Подтвердить выход\n  settings:\n    title: Настройки\n    tabs:\n      profile: Данные профиля\n      interface: Интерфейс\n      permissions: Разрешения\n      relays: Relays\n      dangerZone: Danger Zone\n    relays:\n      add: Add Relay\n      URLs: URLs\n      socket: сокет\n      openGraph: open graph\n      connectedUsers: подключенные пользователи\n    themes:\n      title: Темы\n      midnight: Темная тема\n    copyProfileToDevice: Скопировать профиль на устройство\n    hideKey: Скрыть приватный ключ\n    showKey: Показать приватный ключ\n    download: Скачать настройки\n    hideOfflineContacts: Скрыть оффлайн контакты\n    useLeftRightJustificationForMessages: Левое/Правое выравнивание для сообщений\n    danger:\n      title: Опасная зона\n      deleteMessages: Удалить сообщения\n"
  },
  {
    "path": "client/web/emberclear/tsconfig.compiler-options.json",
    "content": "{\n  \"extends\": \"../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/emberclear/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"app\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/emberclear/types/dom-purify.d.ts",
    "content": "// the dompurify type package is named wrong\nimport DOMPurify from 'dompurify';\n\nexport default DOMPurify;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-a11y-testing/test-support/audit-if.d.ts",
    "content": "export default function (): void;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-cli-clipboard/test-support/index.d.ts",
    "content": "export function triggerCopySuccess(selector: string): void;\nexport function triggerCopyError(selector: string): void;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-could-get-used-to-this.d.ts",
    "content": "export const use: PropertyDecorator;\nexport class Resource {\n  constructor(fnCallback: <T = any>() => T | T[] | void);\n}\n"
  },
  {
    "path": "client/web/emberclear/types/ember-intl/services/intl.d.ts",
    "content": "import Service from '@ember/service';\n\nexport default class IntlService extends Service {\n  addTranslations: (locale: string, translations: any) => void;\n  localeWithDefault: (locale: string) => string[];\n  setLocale: (locale: string[] | string) => void;\n\n  _adapter: any;\n  _owner: any;\n}\n"
  },
  {
    "path": "client/web/emberclear/types/ember-localforage-adapter/adapters/localforage.d.ts",
    "content": "declare const lfadapter: any;\nexport default lfadapter;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-localforage-adapter/serializers/localforage.d.ts",
    "content": "declare const lfserializer: any;\nexport default lfserializer;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-modifier.d.ts",
    "content": "interface ModifierArgs {\n  positional: unknown[];\n  named: { [key: string]: unknown };\n}\n\ninterface IModifier<Args extends ModifierArgs = ModifierArgs> {\n  args: Args;\n  element: Element | null;\n  isDestroying: boolean;\n  isDestroyed: boolean;\n  didReceiveArguments(): void;\n  didUpdateArguments(): void;\n  didInstall(): void;\n  willRemove(): void;\n  willDestroy(): void;\n}\n\ntype Owner = unknown;\n\ndeclare module 'ember-modifier' {\n  export default class Modifier<Args extends ModifierArgs = ModifierArgs>\n    implements IModifier<Args> {\n    args: Args;\n    element: Element | null;\n    isDestroying: boolean;\n    isDestroyed: boolean;\n    constructor(owner: Owner, args: Args);\n    didReceiveArguments(): void;\n    didUpdateArguments(): void;\n    didInstall(): void;\n    willRemove(): void;\n    willDestroy(): void;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/types/ember-service-worker-update-notify/test-support/updater.d.ts",
    "content": "export function setupServiceWorkerUpdater(hooks: NestedHooks): void;\nexport function serviceWorkerUpdate(): Promise<void>;\n"
  },
  {
    "path": "client/web/emberclear/types/ember-usable.d.ts",
    "content": "export const use: PropertyDecorator;\n"
  },
  {
    "path": "client/web/emberclear/types/emberclear/addon-services.d.ts",
    "content": "import '@ember/service';\n\ndeclare global {\n  // https://github.com/knownasilya/ember-toastr/blob/master/addon/services/toast.js\n  interface Toast {\n    [method: string]: (message: string, title?: string, options?: any) => void;\n    success(message: string, title?: string, options?: any): void;\n    info(message: string, title?: string, options?: any): void;\n    warning(message: string, title?: string, options?: any): void;\n    error(message: string, title?: string, options?: any): void;\n  }\n\n  // https://github.com/jamesarosen/ember-i18n/blob/master/addon/services/i18n.js\n  interface Intl {\n    t(translation: string, options?: any): string;\n  }\n}\n\ndeclare module '@ember/service' {\n  interface Registry {\n    toast: Toast;\n    intl: Intl;\n    ['notification-messages']: {\n      clear(): void;\n      clearAll(): void;\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/types/emberclear/ember-data.d.ts",
    "content": "/**\n * Catch-all for ember-data.\n */\ndeclare module 'ember-data/types/registries/model' {\n  export default interface ModelRegistry {\n    [key: string]: any;\n  }\n}\n"
  },
  {
    "path": "client/web/emberclear/types/emberclear/index.d.ts",
    "content": "import type Ember from 'ember';\nimport '@ember/component';\nimport '@ember/test-helpers';\nimport 'ember-cli-htmlbars';\nimport 'qunit';\n\nimport 'ember-concurrency-decorators';\nimport 'ember-concurrency-async';\nimport 'ember-concurrency-ts/async';\n\nimport './addon-services.d';\nimport './addon-augmentations.d';\n\ndeclare global {\n  type EmptyRecord = Record<string, unknown>;\n\n  interface Prism {\n    highlightAll: () => void;\n  }\n\n  interface PublicIdentity {\n    id: string;\n    name: string;\n  }\n\n  interface Array<T> extends Ember.ArrayPrototypeExtensions<T> {\n    /* this enables ember-data arrays to have .toArray() and such */\n    _____fake: unknown;\n  }\n  // interface Function extends Ember.FunctionPrototypeExtensions {}\n\n  interface UserIdentifier {\n    uid: string;\n  }\n\n  interface NamedUser {\n    uid: string;\n    name: string;\n  }\n\n  interface ChannelMember {\n    id: string;\n    name: string;\n    signingKey: string; // This is the public signing key\n  }\n\n  interface MemberResult {\n    id: string;\n    result: boolean;\n    time: string;\n  }\n\n  interface StandardVoteChain {\n    id: string;\n    remaining: ChannelMember[];\n    yes: ChannelMember[];\n    no: ChannelMember[];\n    target: ChannelMember;\n    action: string;\n    key: ChannelMember;\n    previousVoteChain?: StandardVoteChain;\n    signature: string;\n  }\n\n  interface StandardVote {\n    id: string;\n    voteChain: StandardVoteChain;\n  }\n\n  interface StandardChannelContextChain {\n    id: string;\n    admin: ChannelMember;\n    members: ChannelMember[];\n    supportingVote?: StandardVoteChain;\n    previousChain?: StandardChannelContextChain;\n  }\n\n  interface StandardMessage {\n    id: string;\n    to: string;\n    type: string;\n    target: string;\n    client: string;\n    client_version: string;\n    time_sent: Date;\n    sender: {\n      name: string;\n      uid: string;\n      location: string;\n    };\n    message: {\n      body: string;\n      contentType: string;\n      metadata?: Record<string, unknown>;\n    };\n    channelInfo?: {\n      uid: string;\n      name: string;\n      activeVotes: StandardVote[];\n      contextChain: StandardChannelContextChain;\n    };\n  }\n\n  type LoginSYN = { type: 'SYN'; data: PublicIdentity };\n  type LoginACK = { type: 'ACK' };\n  type LoginHash = { type: 'HASH'; data: string };\n  type LoginData = {\n    type: 'DATA';\n    hash: string;\n    data: {\n      version: number;\n      name: string;\n      privateKey: string;\n      privateSigningKey?: string;\n      contacts: { name: string; publicKey: string }[];\n      channels: { id: string; name: string }[];\n    };\n  };\n\n  type LoginMessage = LoginData | LoginACK | LoginSYN | LoginHash;\n\n  type RelayJson = StandardMessage | LoginMessage;\n}\n"
  },
  {
    "path": "client/web/emberclear/types/emojis.d.ts",
    "content": "export function unicode(input: string): string;\nexport function html(input: string): string;\n"
  },
  {
    "path": "client/web/emberclear/types/global.d.ts",
    "content": "// Types for compiled templates\n// declare module 'emberclear/templates/*' {\n//   import type { TemplateFactory } from 'htmlbars-inline-precompile';\n//   const tmpl: TemplateFactory;\n//   export default tmpl;\n// }\n\ndeclare module '@ember/destroyable' {\n  export const associateDestroyableChild: any;\n  export const registerDestructor: any;\n}\n\ndeclare module 'ember-concurrency-test-waiter/define-modifier' {\n  const foo: any;\n  export default foo;\n}\n\ndeclare module 'ember-raf-scheduler/test-support/register-waiter' {\n  const foo: any;\n  export default foo;\n}\n"
  },
  {
    "path": "client/web/emberclear/types/index.d.ts",
    "content": "type Dict<T = string> = { [key: string]: T };\n\n//////////////////////////////////////////////\n// Things that TypeScript should already have\n//////////////////////////////////////////////\ninterface Window {\n  // Notification: Partial<Notification> & {\n  //   permission: 'denied' | 'granted' | undefined;\n  // };\n  ServiceWorker: unknown;\n  deferredInstallPrompt?: FakeBeforeInstallPromptEvent;\n  ASSET_FINGERPRINT_HASH: string;\n}\n\ninterface UserChoice {\n  outcome: 'accepted' | undefined;\n}\n// why is this not a built in type?\ninterface FakeBeforeInstallPromptEvent {\n  prompt: () => Promise<void>;\n  userChoice: Promise<UserChoice>;\n}\n\n"
  },
  {
    "path": "client/web/emberclear/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n\nimport 'ember-concurrency-decorators';\nimport 'ember-concurrency-async';\nimport 'ember-concurrency-ts/async';\nimport 'ember-concurrency-test-waiter';\n\nimport '@emberclear/networking/type-support';\n\ndeclare module '@ember/component' {\n    export function setComponentTemplate(template: any, klass: any): any;\n}\n"
  },
  {
    "path": "client/web/emberclear/types/prismjs/index.d.ts",
    "content": "declare module 'prismjs';\ndeclare module 'prismjs/plugins/line-numbers/prism-line-numbers.min.js' {}\ndeclare module 'prismjs/plugins/show-language/prism-show-language.min.js' {}\ndeclare module 'prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.min.js';\ndeclare module 'prismjs/plugins/autolinker/prism-autolinker.min.js';\n\ndeclare module 'prismjs-components-loader' {\n  export default class {\n    constructor(components: any);\n  }\n}\ndeclare module 'prismjs-components-loader/dist/all-components' {\n  export default class {}\n}\n"
  },
  {
    "path": "client/web/emberclear/types/qr-scanner.d.ts",
    "content": "declare module 'qr-scanner' {\n  class QrScanner {\n    static WORKER_PATH: string;\n\n    constructor(element: Element, onDecode: (result: string) => void, canvasSize?: number);\n\n    _qrWorker: Worker;\n\n    start: () => Promise<void>;\n    stop: () => Promise<void>;\n  }\n\n  export default QrScanner;\n}\n"
  },
  {
    "path": "client/web/emberclear/types/qunit-xstate-test.d.ts",
    "content": "import type { TestModel } from '@xstate/test';\nimport type { TestContext } from 'ember-test-helpers';\n\nexport function setupXStateTest(hooks: NestedHooks, testModel: TestModel<unknown, any>): void;\nexport function testShortestPaths(\n  testModel: TestModel<unknown, any>,\n  callback: (this: TestContext, assert: Assert, path: any) => Promise<boolean>\n): void;\n"
  },
  {
    "path": "client/web/emberclear/types/toastify-js.d.ts",
    "content": "interface ToastifyToast {\n  showToast(): void;\n  hideToast(): void;\n  toastElement: HTMLElement;\n}\n\ninterface ToastifyOptions {\n  text: string;\n  duration?: 3000;\n  destination?: string;\n  newWindow?: boolean;\n  close?: boolean;\n  gravity?: 'top' | 'bottom';\n  position?: 'left' | 'center' | 'right';\n  backgroundColor?: string;\n  className?: string;\n  stopOnFocus?: boolean;\n  onClick?: () => void;\n}\n\nexport default function Toastify(options: ToastifyOptions): ToastifyToast;\n"
  },
  {
    "path": "client/web/emberclear/vendor/shims/libsodium-wrappers.js",
    "content": "(function() {\n  function vendorModule() {\n    'use strict';\n\n    return {\n      default: self,\n      __esModule: true,\n    };\n  }\n\n  define('libsodium-wrappers', [], vendorModule);\n})();\n"
  },
  {
    "path": "client/web/emberclear/vendor/shims/libsodium.js",
    "content": "(function() {\n  function vendorModule() {\n    'use strict';\n\n    return {\n      'default': self['libsodium'],\n      __esModule: true,\n    };\n  }\n\n  define('libsodium', [], vendorModule);\n})();\n"
  },
  {
    "path": "client/web/emberclear/vendor/shims/localforage.js",
    "content": "(function() {\n  function vendorModule() {\n    'use strict';\n\n    return {\n      'default': self['localforage'],\n      __esModule: true,\n    };\n  }\n\n  define('localforage', [], vendorModule);\n})();\n"
  },
  {
    "path": "client/web/emberclear/vendor/shims/qrcode.js",
    "content": "(function() {\n  function vendorModule() {\n    'use strict';\n\n    return {\n      'default': self['QRCode'],\n      __esModule: true,\n    };\n  }\n\n  define('qrcode', [], vendorModule);\n})();\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/.eslintignore",
    "content": "# compiled output\n/dist/\n/tmp/\n\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/.eslintrc.js",
    "content": "'use strict';\n\nconst { tsBase } = require('@nullvoxpopuli/eslint-configs/configs/base');\nconst { baseConfig: nodeBase } = require('@nullvoxpopuli/eslint-configs/configs/node');\nconst { createConfig } = require('@nullvoxpopuli/eslint-configs/utils');\n\nmodule.exports = createConfig(\n  {\n    ...tsBase,\n    plugins: [tsBase.plugins, '@typescript-eslint'].flat(),\n    files: ['types/**'],\n    rules: {\n      ...tsBase.rules,\n      '@typescript-eslint/no-unused-vars': 'off',\n    },\n  },\n  {\n    ...nodeBase,\n    files: ['.eslintrc.js'],\n  }\n);\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/.gitignore",
    "content": "# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n# dependencies\n/bower_components/\n/node_modules/\n\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/package.json",
    "content": "{\n  \"name\": \"@emberclear/questionably-typed\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"lint:js\": \"eslint . --ext js,ts\"\n  },\n  \"devDependencies\": {\n    \"@emberclear/config\": \"*\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"peerDependencies\": {\n    \"@types/ember\": \"3.16.5\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-concurrency\": \"2.0.3\",\n    \"ember-concurrency-ts\": \"0.2.2\",\n    \"ember-concurrency-test-waiter\": \"0.4.0\",\n    \"@html-next/vertical-collection\": \"2.0.0\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"*\": [\n        \"declarations/*\",\n        \"declarations/*/index\"\n      ]\n    }\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"types\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/globals.ts",
    "content": "type LIES<T> = T;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype TODO<T = any> = T;\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/libraries/blakejs.ts",
    "content": "declare module 'blakejs' {\n  export function blake2b(\n    input: string | Uint8Array,\n    key?: Uint8Array | undefined,\n    outlen?: number\n  ): Uint8Array;\n}\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/libraries/ember.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport 'ember';\nimport '@ember/component';\n// import '@ember/component/helper';\n// import '@ember/helper';\n\nimport type { TemplateFactory } from 'ember-cli-htmlbars';\n\ntype TF = TemplateFactory;\n\ndeclare module '@ember/component' {\n  // TODO:  remove when this is actually a thing that exists?\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  export function setComponentTemplate(template: TF, klass: any): any;\n}\n\n// Types are not published. Must use the polyfill for its types\n// declare module '@ember/destroyable' {\n//   export function associateDestroyableChild(parent: any, child: any): any;\n//   export function registerDestructor(child: any, childMethod: () => any): any;\n// }\n\n// import type Helper from '@ember/component/helper';\n\n// type EmberHelper = Helper;\n\n// declare module '@ember/helper' {\n//   // TODO:  remove when this is actually a thing that exists?\n//   export function invokeHelper<Klass extends EmberHelper>(\n//     ctx: unknown,\n//     klass: { new(): Klass },\n//     argFactory: () => unknown[]\n//   ): ReturnType<Klass['compute']>;\n// }\n\n// no longer needed\n// import '@ember/test-waiters';\n// declare module '@ember/test-waiters' {\n//   type WaitForPromise = (x: any) => PromiseLike<void>;\n\n//   export const waitForPromise: WaitForPromise;\n// }\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/libraries/promise-worker-bi.ts",
    "content": "declare module 'promise-worker-bi' {\n  export class PWBWorker {\n    register<T>(message: T): void;\n  }\n\n  // https://github.com/nolanlawson/promise-worker/blob/master/index.d.ts\n  export class PWBHost {\n    _worker: Worker;\n\n    /**\n     * Pass in the worker instance to promisify\n     *\n     * @param worker The worker instance to wrap\n     */\n    constructor(worker: Worker);\n\n    register<T>(message: T): void;\n    registerError<T>(message: T): void;\n\n    /**\n     * Send a message to the worker\n     *\n     * The message you send can be any object, array, string, number, etc.\n     * Note that the message will be `JSON.stringify`d, so you can't send functions, `Date`s, custom classes, etc.\n     *\n     * @param userMessage Data or message to send to the worker\n     * @returns Promise resolved with the processed result or rejected with an error\n     */\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    public postMessage<TResult = any, TInput = any>(userMessage: TInput): Promise<TResult>;\n  }\n}\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/overrides.ts",
    "content": "import './globals';\nimport './package-augmentations';\n\n\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/package-augmentations.ts",
    "content": "import './libraries/ember';\nimport './libraries/promise-worker-bi';\nimport './libraries/blakejs';\n\n// Invalid module name in augmentation\n// declare module 'ember-concurrency-test-waiter/define-modifier' {\n//   const foo: any;\n//   export default foo;\n// }\n\n// declare module 'ember-raf-scheduler/test-support/register-waiter' {\n//   const foo: any;\n//   export default foo;\n// }\n"
  },
  {
    "path": "client/web/libraries/questionably-typed/types/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../config/tsconfig.compiler-options.json\",\n\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"@emberclear/questionably-typed\": [\"*\"],\n      \"@emberclear/questionably-typed/*\": [\"./*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/libraries/tsconfig.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./questionably-typed\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/package.json",
    "content": "{\n  \"name\": \"emberclear-web\",\n  \"version\": \"0.0.0\",\n  \"description\": \"PWA client for emberclear\",\n  \"main\": \"index.js\",\n  \"repository\": \"https://github.com/NullVoxPopuli/emberclear\",\n  \"author\": \"NullVoxPopuli\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"workspaces\": [\n    \"pinochle\",\n    \"emberclear\",\n    \"smoke-tests\",\n    \"config\",\n    \"addons/*\",\n    \"libraries/*\",\n    \"lint/*\"\n  ],\n  \"nohoist\": [\n    \"emberclear\",\n    \"addons/*\"\n  ],\n  \"scripts\": {\n    \"lint:js\": \"eslint .\",\n    \"types:build\": \"tsc --build\",\n    \"types:clean\": \"tsc --build --clean\",\n    \"types:rebuild\": \"tsc --build --clean && tsc --build\",\n    \"types:watch\": \"tsc --build --clean && tsc --build --watch\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"*.{ts,js}\": \"yarn eslint --fix --quiet --cache\",\n    \"*.hbs\": \"yarn ember-template-lint --fix\",\n    \"*.css\": \"yarn stylelint --fix --cache\",\n    \"translations/*.yml\": \"yarn ember-intl-analyzer\"\n  },\n  \"resolutions\": {\n    \"ember-test-waiters\": \"2.1.3\",\n    \"xstate\": \"^4.18.0\"\n  },\n  \"devDependencies\": {\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"cross-env\": \"7.0.3\",\n    \"deepmerge\": \"4.2.2\",\n    \"ember-intl-analyzer\": \"3.0.0\",\n    \"ember-template-lint\": \"3.5.0\",\n    \"husky\": \"4.3.8\",\n    \"lint-staged\": \"11.0.0\",\n    \"sass-lint\": \"1.13.1\",\n    \"stylelint\": \"13.13.1\",\n    \"stylelint-config-standard\": \"22.0.0\",\n    \"typescript\": \"4.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/.editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.{diff,md}]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": "client/web/pinochle/.ember-cli",
    "content": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times when you might want to disable this behavior.\n\n    Setting `disableAnalytics` to true will prevent any data from being sent.\n  */\n  \"disableAnalytics\": false\n}\n"
  },
  {
    "path": "client/web/pinochle/.eslintignore",
    "content": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n# TypeScript output\ndeclarations/\ntsconfig.tsbuildinfo\n\n/bower_components/\n/node_modules/\n\n# misc\n/coverage/\n!.*\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/pinochle/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nmodule.exports = configs.ember();\n"
  },
  {
    "path": "client/web/pinochle/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/node_modules/\n\n# misc\n/.env*\n/.pnp*\n/.sass-cache\n/connect.lock\n/coverage/\n/libpeerconnection.log\n/npm-debug.log*\n/testem.log\n/yarn-error.log\n\n# ember-try\n/.node_modules.ember-try/\n/bower.json.ember-try\n/package.json.ember-try\n"
  },
  {
    "path": "client/web/pinochle/.template-lintrc.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/.template-lintrc');\n"
  },
  {
    "path": "client/web/pinochle/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\"tmp\", \"dist\"]\n}\n"
  },
  {
    "path": "client/web/pinochle/README.md",
    "content": "# pinochle\n\nThis README outlines the details of collaborating on this Ember application.\nA short introduction of this app could easily go here.\n\n## Prerequisites\n\nYou will need the following things properly installed on your computer.\n\n* [Git](https://git-scm.com/)\n* [Node.js](https://nodejs.org/) (with npm)\n* [Ember CLI](https://ember-cli.com/)\n* [Google Chrome](https://google.com/chrome/)\n\n## Installation\n\n* `git clone <repository-url>` this repository\n* `cd pinochle`\n* `npm install`\n\n## Running / Development\n\n* `ember serve`\n* Visit your app at [http://localhost:4200](http://localhost:4200).\n* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).\n\n### Code Generators\n\nMake use of the many generators for code, try `ember help generate` for more details\n\n### Running Tests\n\n* `ember test`\n* `ember test --server`\n\n### Linting\n\n* `npm run lint:hbs`\n* `npm run lint:js`\n* `npm run lint:js -- --fix`\n\n### Building\n\n* `ember build` (development)\n* `ember build --environment production` (production)\n\n### Deploying\n\nSpecify what it takes to deploy your app.\n\n## Further Reading / Useful Links\n\n* [ember.js](https://emberjs.com/)\n* [ember-cli](https://ember-cli.com/)\n* Development Browser Extensions\n  * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)\n  * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)\n"
  },
  {
    "path": "client/web/pinochle/app/app.ts",
    "content": "import 'focus-visible';\n\nimport Application from '@ember/application';\n\nimport defineModifier from 'ember-concurrency-test-waiter/define-modifier';\nimport loadInitializers from 'ember-load-initializers';\nimport Resolver from 'ember-resolver';\n\nimport config from 'pinochle/config/environment';\n\nexport default class App extends Application {\n  modulePrefix = config.modulePrefix;\n  podModulePrefix = config.podModulePrefix;\n  Resolver = Resolver;\n}\n\nloadInitializers(App, config.modulePrefix);\n\ndefineModifier();\n"
  },
  {
    "path": "client/web/pinochle/app/components/back-of-cards.hbs",
    "content": "<div\n  class='other-player'\n  ...attributes\n>\n  {{#if this.isOffline}}\n    <span class='player-offline-indicator'>\n      <Loader::Ellipsis />\n      <span>Waiting...</span>\n    </span>\n  {{/if}}\n\n  <div class='display {{if this.isOffline 'player-offline'}}'>\n    <ul class='back-of-hand playing-hand'>\n      <li class='non-player-card card1'>\n        <button type='button' disable></button>\n      </li>\n      <li class='non-player-card card2'>\n        <button type='button' disable></button>\n      </li>\n      <li class='non-player-card card3'>\n        <button type='button' disable></button>\n      </li>\n    </ul>\n\n    <span class='hand-name'>\n      {{@info.name}}\n    </span>\n  </div>\n</div>\n"
  },
  {
    "path": "client/web/pinochle/app/components/back-of-cards.ts",
    "content": "import Component from '@glimmer/component';\n\nimport type { GuestPlayer } from 'pinochle/game/networking/types';\n\ntype Args = {\n  info: GuestPlayer;\n};\n\nexport default class BackOfCards extends Component<Args> {\n  get isOffline() {\n    let { id, isOnline } = this.args.info;\n\n    return id && !isOnline;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/-animation/card-chart.ts",
    "content": "import { assign, send } from 'xstate';\nimport { choose } from 'xstate/lib/actions';\n\nimport type { MachineConfig, StateSchema } from 'xstate';\n\nexport type Keyframes = {\n  fan: Keyframe;\n  flat: Keyframe;\n  stack: Keyframe;\n  selected: Keyframe;\n};\n\nexport type Context = {\n  // card: Card;\n  element: HTMLElement;\n  keyframes: Keyframes;\n  isSmallScreen: boolean;\n\n  // Temp state for animating\n  previousFrames: Keyframes;\n  delay?: number;\n  currentName: keyof Keyframes;\n  previousName: keyof Keyframes;\n};\n\ntype AdjustEvent = {\n  type: 'ADJUST';\n  frames: Keyframes;\n};\n\ntype ToggleEvent = {\n  type: 'TOGGLE_FAN';\n  delay: number;\n};\n\nexport type Event =\n  | { type: 'SELECT' }\n  | { type: 'DESELECT' }\n  | { type: 'FAN' }\n  | { type: 'REFAN' }\n  | { type: 'STACK' }\n  | { type: 'FLAT' }\n  | { type: 'REFLAT' }\n  | { type: 'ANIMATE_ADJUSTMENT' }\n  | ToggleEvent\n  | AdjustEvent;\n\nexport interface Schema extends StateSchema<Context> {\n  states: {\n    fanned: StateSchema<Context>;\n    stacked: StateSchema<Context>;\n    flat: StateSchema<Context>;\n    selected: StateSchema<Context>;\n  };\n}\n\nconst SMALL_SCREEN = 1000;\nconst ANIMATION_DURATION = 250;\nconst DEFAULT_ANIMATION_OPTIONS: KeyframeAnimationOptions = {\n  duration: ANIMATION_DURATION,\n  iterations: 1,\n  fill: 'both',\n};\n\nfunction isBigScreen() {\n  return window.innerWidth >= SMALL_SCREEN;\n}\n\nexport function isSmallScreen() {\n  return !isBigScreen();\n}\n\nfunction animate(context: Context, next: Keyframe, options: KeyframeAnimationOptions = {}) {\n  /**\n   * NOTE: pausing causes jitters\n   * NOTE: cancelling removes the possibility of resuming mid-way through a transition\n   *\n   * Not doing anything lets the built-in tweening happen and provides smooth\n   * transitions between states.\n   */\n  // if (context.animation) {\n  // context.animation.pause();\n  // context.animation.cancel();\n  // }\n\n  return context.element.animate([next], {\n    ...DEFAULT_ANIMATION_OPTIONS,\n    delay: context.delay,\n    ...options,\n  });\n}\n\nfunction toSelection(ctx: Context) {\n  return animate(ctx, ctx.keyframes.selected, {\n    delay: 0,\n  });\n}\n\nfunction toFlat(ctx: Context) {\n  return animate(ctx, ctx.keyframes.flat, {\n    delay: ctx.previousName === 'selected' ? 0 : ctx.delay,\n  });\n}\n\nfunction toStack(ctx: Context) {\n  return animate(ctx, ctx.keyframes.stack);\n}\n\nfunction toFan(ctx: Context) {\n  return animate(ctx, ctx.keyframes.fan, {\n    delay: ctx.previousName === 'selected' ? 0 : ctx.delay,\n  });\n}\n\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  id: 'card-chart',\n  initial: 'stacked',\n  on: {\n    ADJUST: {\n      actions: [\n        assign<Context>({\n          previousFrames: (ctx) => ctx.keyframes,\n          keyframes: (_: Context, { frames }: AdjustEvent) => frames,\n          isSmallScreen: () => isSmallScreen(),\n        }),\n        send('ANIMATE_ADJUSTMENT'),\n      ],\n    },\n  },\n  states: {\n    fanned: {\n      entry: [\n        assign<Context>({\n          currentName: 'fan',\n        }),\n        toFan,\n      ],\n      exit: [\n        assign<Context>({\n          previousName: 'fan',\n        }),\n      ],\n      on: {\n        ANIMATE_ADJUSTMENT: {\n          actions: choose([\n            {\n              cond: isBigScreen,\n              actions: [send('REFAN')],\n            },\n            {\n              actions: [send('FLAT')],\n            },\n          ]),\n        },\n        FLAT: 'flat',\n        SELECT: 'selected',\n        TOGGLE_FAN: 'stacked',\n        REFAN: 'fanned',\n      },\n    },\n    stacked: {\n      entry: [\n        assign<Context>({\n          currentName: 'stack',\n          previousFrames: (ctx) => ctx.keyframes,\n        }),\n        toStack,\n      ],\n      exit: [\n        assign<Context>({\n          previousName: 'stack',\n        }),\n      ],\n      on: {\n        TOGGLE_FAN: [\n          {\n            target: 'fanned',\n            cond: isBigScreen,\n            actions: [assign({ delay: (_ctx, event: ToggleEvent) => event.delay })],\n          },\n          {\n            target: 'flat',\n          },\n        ],\n      },\n    },\n    flat: {\n      entry: [\n        assign<Context>({\n          currentName: 'flat',\n          previousFrames: (ctx) => ctx.keyframes,\n        }),\n        toFlat,\n      ],\n      exit: [\n        assign<Context>({\n          previousName: 'flat',\n        }),\n      ],\n      on: {\n        ANIMATE_ADJUSTMENT: {\n          actions: choose([\n            {\n              cond: isSmallScreen,\n              actions: [send('REFLAT')],\n            },\n            {\n              actions: [send('FAN')],\n            },\n          ]),\n        },\n        FAN: 'fanned',\n        REFLAT: 'flat',\n        SELECT: 'selected',\n        TOGGLE_FAN: 'stacked',\n      },\n    },\n    selected: {\n      entry: [\n        assign<Context>({\n          currentName: 'selected',\n          previousFrames: (ctx) => ctx.keyframes,\n        }),\n        toSelection,\n      ],\n      exit: [\n        assign<Context>({\n          previousName: 'selected',\n        }),\n      ],\n      on: {\n        SELECT: [\n          {\n            target: 'fanned',\n            cond: isBigScreen,\n          },\n          {\n            target: 'flat',\n          },\n        ],\n        DESELECT: [\n          {\n            target: 'fanned',\n            cond: isBigScreen,\n          },\n          {\n            target: 'flat',\n          },\n        ],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/-animation/card.ts",
    "content": "import { action } from '@ember/object';\n\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { isSmallScreen, statechart } from './card-chart';\n\nimport type { Keyframes } from './card-chart';\n\nexport const SELECTED_TRANSFORM = {\n  transform: `\n    rotate(0deg)\n    translate3d(-50%, -70%, 0)`,\n};\n\nexport class CardAnimation {\n  constructor(public element: HTMLElement, public frames: Keyframes) {}\n\n  @use\n  interpreter = new Statechart(() => {\n    let { element, frames } = this;\n\n    return {\n      named: {\n        chart: statechart,\n        context: {\n          element,\n          keyframes: frames,\n          previousFrames: frames,\n          currentName: 'stack',\n          previousName: 'stack',\n          isSmallScreen: isSmallScreen(),\n        },\n        config: {\n          actions: {},\n        },\n      },\n    };\n  });\n\n  @action\n  select() {\n    this.interpreter.send('SELECT');\n  }\n\n  @action\n  deselect() {\n    this.interpreter.send('DESELECT');\n  }\n\n  @action\n  toggle(delay: number) {\n    this.interpreter.send('TOGGLE_FAN', { delay });\n  }\n\n  @action\n  adjust(frames: Keyframes) {\n    this.interpreter.send('ADJUST', { frames });\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/-animation/hand-chart.ts",
    "content": "import { assign } from 'xstate';\n\nimport { getPoints } from './key-frames';\n\nimport type { CardAnimation } from './card';\nimport type { Card } from 'pinochle/game/card';\nimport type { MachineConfig, StateSchema } from 'xstate';\n\nexport type Context = {\n  cards: Card[];\n  selected?: Card;\n  animations: WeakMap<Card, CardAnimation>;\n  points?: ReturnType<typeof getPoints>;\n  isOpen: boolean;\n};\n\nexport type SelectEvent = { type: 'SELECT'; card: Card };\nexport type Event =\n  | { type: 'ADJUST' }\n  | { type: 'CONFIRM' }\n  | { type: 'CANCEL' }\n  | { type: 'TOGGLE_FAN' }\n  | SelectEvent\n  | { type: 'start-animation'; animation: Animation };\n\nexport interface Schema extends StateSchema<Context> {\n  states: {\n    'fanned-out': {\n      states: {\n        idle: StateSchema<Context>;\n        'has-selection': {\n          states: {\n            idle: StateSchema<Context>;\n            'confirm-play': StateSchema<Context>;\n          };\n        };\n      };\n    };\n    closed: StateSchema<Context>;\n  };\n}\n\n// Almost all of this below here is copy-pastable to the xstate visualizer.\n// Just need to not include the const declaration and only copy the\n// object contents to the visualizer\n\nfunction canPlayCard() {\n  return false;\n}\n\nfunction isClosed(context: Context) {\n  return !context.isOpen;\n}\n\nfunction isOpen(context: Context) {\n  return context.isOpen;\n}\n\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  id: 'hand-chart',\n  context: { cards: [], isOpen: false, animations: new WeakMap() },\n  initial: 'fanned-out',\n  on: {\n    // called when the window resizes\n    ADJUST: {\n      actions: [\n        assign({\n          points: (context) => {\n            return getPoints(context.cards.length);\n          },\n        }),\n        'adjustHand',\n      ],\n    },\n    TOGGLE_FAN: [\n      {\n        target: 'fanned-out',\n        cond: isClosed,\n        actions: ['fanOpen', assign<Context>({ isOpen: () => true, selected: () => undefined })],\n      },\n      {\n        target: 'closed',\n        cond: isOpen,\n        actions: ['closeHand', assign<Context>({ isOpen: () => false, selected: () => undefined })],\n      },\n    ],\n  },\n  states: {\n    'fanned-out': {\n      id: 'fanned-out',\n      initial: 'idle',\n      states: {\n        idle: {\n          on: {\n            SELECT: 'has-selection',\n          },\n        },\n        'has-selection': {\n          id: 'selected',\n          initial: 'idle',\n          entry: [\n            assign<Context>({ selected: (_, event: SelectEvent) => event.card }),\n            'showSelected',\n          ],\n          states: {\n            idle: {\n              on: {\n                SELECT: [\n                  { target: 'confirm-play', cond: canPlayCard },\n                  {\n                    target: '#fanned-out.has-selection',\n                    actions: ['showSelected'],\n                  },\n                ],\n              },\n            },\n            'confirm-play': {\n              on: {\n                CONFIRM: {},\n                CANCEL: {},\n              },\n            },\n          },\n        },\n      },\n    },\n    closed: {\n      on: {},\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/-animation/hand.ts",
    "content": "import { assert } from '@ember/debug';\nimport { action } from '@ember/object';\n\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { CardAnimation, SELECTED_TRANSFORM } from './card';\nimport { statechart } from './hand-chart';\nimport { fannedKeyframes, flatKeyframes, getPoints, stackedKeyframes } from './key-frames';\n\nimport type { Context, Event, Schema, SelectEvent } from './hand-chart';\nimport type { Card } from 'pinochle/game/card';\n\nfunction getAnimations(points: ReturnType<typeof getPoints>, cards: Card[]) {\n  let result = new WeakMap<Card, CardAnimation>();\n  let stackedFrames = stackedKeyframes(points);\n  let fannedFrames = fannedKeyframes(points);\n  let flatFrames = flatKeyframes(points);\n\n  for (let i = 0; i < cards.length; i++) {\n    const card = cards[i];\n    const stackFrame = stackedFrames[i];\n    const fanFrame = fannedFrames[i];\n    const flatFrame = flatFrames[i];\n    const element = document.getElementById(card.id);\n\n    assert(`Expected element to exist`, element && element instanceof HTMLElement);\n\n    result.set(\n      card,\n      new CardAnimation(element, {\n        stack: stackFrame,\n        flat: flatFrame,\n        fan: fanFrame,\n        selected: SELECTED_TRANSFORM,\n      })\n    );\n  }\n\n  return result;\n}\n\ntype ToggleOptions = {\n  cards: Card[];\n  animations: WeakMap<Card, CardAnimation>;\n};\n\nexport function adjustHand({ cards, animations }: ToggleOptions) {\n  let points = getPoints(cards.length);\n\n  let stackedFrames = stackedKeyframes(points);\n  let flatFrames = flatKeyframes(points);\n  let fannedFrames = fannedKeyframes(points);\n\n  for (let i = 0; i < cards.length; i++) {\n    const card = cards[i];\n    const existing = animations.get(card);\n\n    if (!existing) {\n      continue; // not-possible? maybe?\n    }\n\n    let stack = stackedFrames[i];\n    let flat = flatFrames[i];\n    let fan = fannedFrames[i];\n\n    existing.adjust({ stack, flat, fan, selected: SELECTED_TRANSFORM });\n  }\n}\n\nexport class HandAnimation {\n  constructor(_owner: unknown, public cards: Card[]) {}\n\n  @use\n  interpreter = new Statechart(() => {\n    return {\n      named: {\n        chart: statechart,\n        context: this.context,\n        config: {\n          actions: {\n            closeHand: this._closeHand,\n            fanOpen: this._fanOpen,\n            adjustHand: this._adjustHand,\n            returnSelectedToHand: this._returnSelected,\n            showSelected: this._showSelected,\n          },\n          guards: {},\n        },\n      },\n    };\n  });\n\n  @action\n  toggle() {\n    this.interpreter.send('TOGGLE_FAN');\n  }\n\n  @action\n  send(...args: Parameters<Statechart<Context, Schema, Event>['send']>) {\n    this.interpreter.send(...args);\n  }\n\n  /**\n   * Private\n   */\n  get context() {\n    let points = getPoints(this.cards.length);\n\n    return {\n      isOpen: false,\n      cards: this.cards,\n      points,\n      animations: getAnimations(points, this.cards),\n    };\n  }\n\n  // Actions used to tell the cards' machines what to do\n  @action\n  _fanOpen({ cards, animations }: Context) {\n    for (let i = 0; i < cards.length; i++) {\n      const card = cards[i];\n\n      let existing = animations.get(card);\n\n      assert(\n        `something went wrong at position ${i} retrieving the animation for ${card}`,\n        existing\n      );\n\n      existing.toggle(i * 10);\n    }\n  }\n\n  @action\n  _closeHand(context: Context) {\n    // secretly a toggle\n    this._fanOpen(context);\n  }\n\n  @action\n  _adjustHand({ cards, animations }: Context) {\n    return adjustHand({ cards, animations });\n  }\n\n  @action\n  _showSelected({ animations, cards }: Context, { card }: SelectEvent) {\n    for (let current of cards) {\n      let existing = animations.get(current);\n\n      assert(`something went wrong retrieving the animation for ${card}`, existing);\n\n      if (current === card) {\n        existing.select();\n        continue;\n      }\n\n      existing.deselect();\n    }\n  }\n\n  @action\n  _returnSelected({ animations, selected }: Context) {\n    assert(`can't return unselected card`, selected);\n\n    let existing = animations.get(selected);\n\n    assert(`something went wrong retrieving the animation for ${selected}`, existing);\n\n    existing.select();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/-animation/key-frames.ts",
    "content": "import { circleFromThreePoints } from 'pinochle/utils/trig';\nimport { radiansToDegrees } from 'pinochle/utils/trig';\n\nexport function stackedKeyframes({ path, positions }: ReturnType<typeof getPoints>) {\n  return positions.map((_position, i) => {\n    return {\n      transform: `translate3d(${0 - 0.5 * i}%, ${0 - 0.5 * i}%, 0)`,\n      transformOrigin: `50% ${path.y}px`,\n    };\n  });\n}\n\nexport function fannedKeyframes({ path, positions }: ReturnType<typeof getPoints>) {\n  let { viewportWidth } = path;\n  let numCards = positions.length;\n  let widthOfCard = viewportWidth / numCards;\n\n  return positions.map((position, i) => {\n    return {\n      transform: `\n        rotate(${radiansToDegrees(position.rad)}deg)\n        translate3d(calc(${0 - 0.5 * i}% - ${widthOfCard}px), ${0 - 0.5 * i}%, 0)\n      `,\n      transformOrigin: `50% ${path.y / 2}px`,\n    };\n  });\n}\n\nexport function flatKeyframes({ path, positions }: ReturnType<typeof getPoints>) {\n  let { viewportWidth } = path;\n  let numCards = positions.length;\n\n  return positions.map((_position, i) => {\n    return {\n      transform: `\n        rotate(0deg)\n        translate3d(${((viewportWidth * 0.8) / numCards) * (i - numCards / 2)}px, 0, 0)\n      `,\n      transformOrigin: `50% ${path.y / 2}px`,\n    };\n  });\n}\n\n/**\n *\n * Returns points along the arc of a circle clipped by the viewport where\n * the outside points have a reasonable amount of padding from the window\n * edge\n *\n * The circle is initally defined by 3 points:\n *  - midpoint along X + some percent height for Y / top of the circle\n *  - bottom-left corner\n *  - bottom-right corner\n *\n * NOTES:\n *   rad = Math.atan2(y - cy, x - cx)\n *\n *   when a is radians:\n *     x = cx + r * cos(a)\n *     y = cy + r * sin(a)\n *\n * It's been a long while since I've done trig. :D\n */\nexport function getPoints(num: number) {\n  let viewportWidth = window.innerWidth;\n  let left = 0;\n  let right = viewportWidth;\n  let bottom = window.innerHeight;\n\n  let { x: circleX, y: circleY, r: circleRadius } = circleFromThreePoints(\n    { x: left, y: bottom * 0.8 },\n    { x: viewportWidth * 0.6, y: bottom * 0.7 },\n    { x: right, y: bottom + 0.8 }\n  );\n\n  // given the bottom of the window as an \"ok\" Y, find the two X values for the circle at that Y\n  let leftAngle = Math.atan2(bottom - circleY, left - circleX);\n  let rightAngle = Math.atan2(bottom - circleY, right - circleX);\n\n  // divide the angle by num + 2 to account for some padding\n  let totalAngle = rightAngle - leftAngle; // leftAngle - rightAngle;\n  let arcWidth = totalAngle / (num + 6);\n\n  let positions = Array(num)\n    .fill(undefined)\n    .map((_, i) => {\n      // let angle = rightAngle + i * arcWidth;\n\n      return {\n        // x: circleX + circleRadius * Math.cos(angle),\n        // y: circleY + circleRadius + Math.sin(angle),\n        // rad: rightAngle - (i * arcWidth),\n        rad: (i - num / 2) * arcWidth,\n      };\n    });\n\n  return {\n    path: {\n      x: circleY,\n      y: circleY,\n      radius: circleRadius,\n      viewportWidth,\n    },\n    positions,\n  };\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/hand/index.hbs",
    "content": "<div class='perspective grid bottom-favored center' ...attributes>\n  <ul\n    class='player-hand {{if this.isActive 'spread'}}'\n    {{!-- template-lint-disable no-invalid-interactive --}}\n    {{stack this.toggle}}\n    {{resize this.adjust}}\n  >\n    {{#each @cards as |card|}}\n      <PlayingCard\n        id={{card.id}}\n        @suit={{card.suit}}\n        @value={{card.value}}\n        {{on 'click' (fn this.selectCard card)}}\n      />\n    {{/each}}\n  </ul>\n\n\n  {{!--\n    For additional game info to overlay the hand, but controlled from above\n    The hand component only knows how to handle hand stuff\n  --}}\n  {{yield}}\n\n  <span class='player-hand-info'>\n    <button type='button' {{on 'click' this.toggle}}>\n      Fan\n    </button>\n\n    Meld: {{this.meld.score}}\n  </span>\n</div>"
  },
  {
    "path": "client/web/pinochle/app/components/hand/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { cached } from '@glimmer/tracking';\nimport { getOwner } from '@ember/application';\nimport { action } from '@ember/object';\n\nimport { Meld } from 'pinochle/game/meld';\n\nimport { HandAnimation } from './-animation/hand';\n\nimport type { Card } from 'pinochle/game/card';\n\ntype Args = {\n  cards: Card[];\n  onSelect: (card: Card) => void;\n};\n\nexport default class HandComponent extends Component<Args> {\n  @cached\n  get hand() {\n    return new HandAnimation(getOwner(this), this.args.cards);\n  }\n\n  /**\n   * Given the current cards, what are all the meld combinations and score?\n   */\n  @cached\n  get meld() {\n    return new Meld([...(this.args.cards as Card[])]);\n  }\n\n  @action\n  toggle() {\n    this.hand.toggle();\n  }\n\n  @action\n  selectCard(card: Card) {\n    this.hand.send('SELECT', { card });\n  }\n\n  @action\n  adjust() {\n    this.hand.send('ADJUST');\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/host-game/-statechart.ts",
    "content": "import { assign } from 'xstate';\n\nimport type { MachineConfig, StateSchema } from 'xstate';\n\nexport type Context = {\n  name: string;\n  numPlayers: number;\n  connectedPlayers: string[];\n};\n\ntype SubmitNameEvent = { type: 'SUBMIT_NAME'; name: string };\nexport type Event =\n  | { type: 'START_GAME_FAILED' }\n  | { type: 'BOOT_HOST' }\n  | { type: 'START_GAME' }\n  | SubmitNameEvent;\n\nexport interface Schema extends StateSchema<Context> {\n  states: {\n    'needs-name': StateSchema<Context>;\n    'waiting-for-players': StateSchema<Context>;\n    'starting-game': StateSchema<Context>;\n  };\n}\n\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  id: 'game-host',\n  on: {},\n  context: { name: '', numPlayers: 0, connectedPlayers: [] },\n  initial: 'needs-name',\n  states: {\n    'needs-name': {\n      on: {\n        SUBMIT_NAME: 'waiting-for-players',\n      },\n    },\n    'waiting-for-players': {\n      entry: [\n        assign<Context>({ name: (_, { name }: SubmitNameEvent) => name }),\n        'establishConnection',\n      ],\n\n      on: {\n        START_GAME: 'starting-game',\n      },\n    },\n    'starting-game': {\n      entry: ['startGame'],\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/components/host-game/index.hbs",
    "content": "<main class='grid center fullscreen'>\n  <section>\n\n    {{#if (contains this.state 'needs-name')}}\n      <NameEntry @onSubmit={{this.handleSubmit}} />\n    {{else if (contains this.state 'waiting-for-players')}}\n      <div class='grid gap-4'>\n        <h2>Waiting for players</h2>\n\n        <PlayerList\n          @players={{this.gameHost.players}}\n          @currentName={{this.context.name}} />\n\n        {{#unless this.canStartGame}}\n          Need {{this.numRemaining}} more player(s)\n        {{/unless}}\n\n        {{#if this.canStartGame}}\n          <button type='button' {{on 'click' this.start}}>\n            Start Game\n          </button>\n        {{else}}\n          <Loader::Indeterminate />\n\n          <ShareLink @url={{this.joinUrl}} />\n        {{/if}}\n      </div>\n    {{else if (contains this.state 'starting-game')}}\n      Starting game...\n    {{else}}\n      Unknown State\n    {{/if}}\n  </section>\n</main>"
  },
  {
    "path": "client/web/pinochle/app/components/host-game/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { statechart } from './-statechart';\n\nimport type { Context } from './-statechart';\nimport type RouterService from '@ember/routing/router-service';\nimport type { GameGuest } from 'pinochle/game/networking/guest';\nimport type { GameHost } from 'pinochle/game/networking/host';\nimport type GameManager from 'pinochle/services/game-manager';\n\ntype Args = {\n  numPlayers: number;\n};\n\nexport default class HostGame extends Component<Args> {\n  @service declare router: RouterService;\n  @service declare gameManager: GameManager;\n\n  @tracked gameHost?: GameHost;\n  @tracked gameGuest?: GameGuest;\n\n  @use\n  interpreter = new Statechart(() => {\n    return {\n      named: {\n        chart: statechart,\n        context: { numPlayers: this.args.numPlayers },\n        config: {\n          actions: {\n            startGame: this._startGame,\n            establishConnection: this._establishConnection,\n          },\n        },\n      },\n    };\n  });\n\n  get state() {\n    return this.interpreter.state?.toStrings();\n  }\n\n  get context() {\n    return this.interpreter.state?.context;\n  }\n\n  get connectedPlayers() {\n    return this.gameHost?.numConnected || 0;\n  }\n\n  get joinUrl() {\n    return this.gameHost?.joinUrl;\n  }\n\n  get canStartGame() {\n    return this.connectedPlayers >= 3;\n  }\n\n  get numRemaining() {\n    return 3 - this.connectedPlayers;\n  }\n\n  @action\n  handleSubmit(name: string) {\n    this.interpreter.send({ type: 'SUBMIT_NAME', name });\n  }\n\n  @action\n  start() {\n    this.interpreter.send({ type: 'START_GAME' });\n  }\n\n  /*********************************\n   * Machine Actions\n   ********************************/\n\n  @action\n  _startGame() {\n    if (this.gameHost) {\n      this.gameHost.startGame();\n      this.router.transitionTo(`/game/${this.gameHost.hexId}`);\n\n      return;\n    }\n\n    this.interpreter.send('START_GAME_FAILED');\n  }\n\n  /**\n   * Create Identity if it doesn't exist\n   * also connect to the relay\n   */\n  @action\n  async _establishConnection({ name }: Context) {\n    this.gameHost = await this.gameManager.createHost();\n\n    // TODO: error handling?\n    this.gameGuest = await this.gameManager.connectToHost(this.gameHost.hexId);\n    await this.gameGuest.checkHost();\n    await this.gameGuest.joinHost(name);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/join-game/-statechart.ts",
    "content": "import Ember from 'ember';\n\nimport { assign, send } from 'xstate';\n\nimport type { MachineConfig, StateSchema } from 'xstate';\n\nexport type Context = {\n  name: string;\n  retryCount?: number;\n};\n\ntype SubmitNameEvent = { type: 'SUBMIT_NAME'; name: string };\n\nexport type Event =\n  | { type: 'START_GAME_FAILED' }\n  | { type: 'CONNECTED' }\n  | { type: 'RETRY' }\n  | { type: 'ERROR'; error?: Error }\n  | { type: 'JOINED' }\n  | { type: 'START' }\n  | SubmitNameEvent;\n\nexport interface Schema extends StateSchema<Context> {\n  states: {\n    'game-does-not-exist': StateSchema<Context>;\n    'needs-name': StateSchema<Context>;\n    joining: StateSchema<Context>;\n    begin: StateSchema<Context>;\n    waiting: StateSchema<Context>;\n    starting: StateSchema<Context>;\n  };\n}\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  initial: 'begin',\n  states: {\n    begin: {\n      entry: 'establishConnection',\n      on: {\n        CONNECTED: [\n          {\n            target: 'joining',\n            cond: (ctx) => ctx.name.length > 0,\n          },\n          {\n            target: 'needs-name',\n          },\n        ],\n        RETRY: 'begin',\n        ERROR: [\n          {\n            actions: [\n              // eslint-disable-next-line ember/no-ember-testing-in-module-scope\n              send('RETRY', { delay: Ember.testing ? 100 : 1000 }),\n              assign({ retryCount: (ctx) => (ctx.retryCount || 0) + 1 }),\n            ],\n            cond: (ctx) => (ctx.retryCount || 0) < 5,\n          },\n          { target: 'game-does-not-exist' },\n        ],\n      },\n    },\n    'game-does-not-exist': {},\n    'needs-name': {\n      on: {\n        SUBMIT_NAME: 'joining',\n      },\n    },\n    joining: {\n      entry: [assign<Context>({ name: (_, { name }: SubmitNameEvent) => name }), 'joinGame'],\n      on: {\n        JOINED: 'waiting',\n      },\n    },\n    waiting: {\n      on: {\n        START: 'starting',\n        ERROR: 'game-does-not-exist',\n      },\n    },\n    starting: {\n      entry: 'startGame',\n    },\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/components/join-game/index.hbs",
    "content": "<main class='grid center fullscreen' data-test-join-game>\n  <section>\n    {{#if (contains this.state 'begin')}}\n        <h2 data-test-connecting>Connecting...</h2>\n\n    {{else if (contains this.state 'needs-name')}}\n      <NameEntry @onSubmit={{this.handleSubmit}} />\n    {{else if this.isPending}}\n\n      {{#if this.isJoining}}\n        <h2 data-test-joining>Joining game...</h2>\n      {{else if this.isWaiting}}\n        <h2 data-test-waiting>Waiting for game to start</h2>\n      {{/if}}\n\n      <PlayerList\n        @players={{this.gameHost.players}}\n        @currentName={{this.context.name}} />\n\n      <Loader::Indeterminate />\n\n    {{else if (contains this.state 'game-does-not-exist')}}\n      <span data-test-404>Game does not exist. Is the URL correct?</span>\n    {{else if (contains this.state 'starting')}}\n      <span data-test-starting>Starting game...</span>\n    {{else}}\n      <span data-test-unknown>Unknown State</span>\n    {{/if}}\n  </section>\n</main>"
  },
  {
    "path": "client/web/pinochle/app/components/join-game/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { loadWithDefault } from 'pinochle/services/game-manager';\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { fromHex } from '@emberclear/encoding/string';\n\nimport { statechart } from './-statechart';\n\nimport type { Context } from './-statechart';\nimport type RouterService from '@ember/routing/router-service';\nimport type { GameGuest, SerializedGuest } from 'pinochle/game/networking/guest';\nimport type GameManager from 'pinochle/services/game-manager';\nimport type PlayerInfo from 'pinochle/services/player-info';\n\ntype Args = {\n  hostId: string;\n  skipName: boolean;\n};\n\nexport default class JoinGame extends Component<Args> {\n  @service declare gameManager: GameManager;\n  @service declare router: RouterService;\n  @service declare playerInfo: PlayerInfo;\n\n  @tracked gameHost?: GameGuest;\n\n  @use\n  interpreter = new Statechart(() => {\n    let name = this.args.skipName ? this.playerInfo.name : '';\n\n    return {\n      named: {\n        chart: statechart,\n        context: { name },\n        config: {\n          actions: {\n            establishConnection: this._establishConnection,\n            joinGame: this._joinGame,\n            startGame: this._startGame,\n          },\n        },\n      },\n    };\n  });\n\n  get state() {\n    return this.interpreter.state?.toStrings();\n  }\n\n  get isJoining() {\n    return this.state?.includes('joining');\n  }\n\n  get isWaiting() {\n    return this.state?.includes('waiting');\n  }\n\n  get isPending() {\n    return this.isWaiting || this.isJoining;\n  }\n\n  @action\n  handleSubmit(name: string) {\n    this.interpreter.send('SUBMIT_NAME', { name });\n  }\n\n  /*********************************\n   * Machine Actions\n   ********************************/\n  @action\n  async _establishConnection() {\n    if (!this.gameHost) {\n      let keys = undefined;\n      let previous = loadWithDefault(`guest-${this.args.hostId}`) as SerializedGuest;\n\n      if (previous) {\n        keys = {\n          publicKey: fromHex(previous.publicKey),\n          privateKey: fromHex(previous.privateKey),\n        };\n      }\n\n      this.gameHost = await this.gameManager.connectToHost(this.args.hostId, keys);\n    }\n\n    try {\n      if (this.isDestroyed || this.isDestroying) return;\n\n      await this.gameHost.checkHost();\n\n      if (this.isDestroyed || this.isDestroying) return;\n\n      this.interpreter.send('CONNECTED');\n    } catch (e) {\n      console.error(e);\n      this.interpreter.send('ERROR', { error: e });\n    }\n  }\n\n  @action\n  _joinGame({ name }: Context) {\n    this._joinGameTask.perform(name);\n  }\n\n  @action\n  _startGame() {\n    if (this.gameHost?.gameId) {\n      this.router.transitionTo(`/game/${this.gameHost?.gameId}`);\n\n      return;\n    }\n\n    this.interpreter.send('ERROR');\n  }\n\n  @task\n  _joinGameTask = taskFor(async (name: string) => {\n    await this.gameHost?.joinHost(name);\n\n    this.interpreter.send('JOINED');\n\n    await this.gameHost?.waitForStart();\n\n    this.interpreter.send('START');\n  });\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/lazy/index.hbs",
    "content": "{{#if this.isShowing}}\n  {{yield}}\n{{/if}}"
  },
  {
    "path": "client/web/pinochle/app/components/lazy/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { tracked } from '@glimmer/tracking';\nimport { later, run } from '@ember/runloop';\n\ntype Args = {\n  wait: number;\n  when: boolean;\n};\n\nexport default class LazyComponent extends Component<Args> {\n  @tracked _isShowing = false;\n\n  constructor(owner: unknown, args: Args) {\n    super(owner, args);\n\n    run('afterRender', () => {\n      later(this, 'toggleContent', args.wait);\n    });\n  }\n\n  get isShowing() {\n    return this._isShowing && this.cond;\n  }\n\n  get cond() {\n    return this.args.when || !('when' in this.args);\n  }\n\n  toggleContent() {\n    if (this.isDestroying || this.isDestroyed) {\n      return;\n    }\n\n    this._isShowing = !this._isShowing;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/loader/ellipsis.hbs",
    "content": "<span class='ellipsis-loader' ...attributes>\n  <span>•</span>\n  <span>•</span>\n  <span>•</span>\n</span>\n"
  },
  {
    "path": "client/web/pinochle/app/components/loader/indeterminate.hbs",
    "content": "<div class='indeterminate-loader'>\n  <div class='line'></div>\n  <div class='subline inc'></div>\n  <div class='subline dec'></div>\n</div>\n"
  },
  {
    "path": "client/web/pinochle/app/components/name-entry/index.hbs",
    "content": "<form {{on 'submit' this.submitName}} ...attributes data-test-name-entry>\n  <label for='name-input'>\n    <h2>Please enter your name</h2>\n  </label>\n\n  <div class='input-box grid:cols gap-2 left-column'>\n    <input\n      id='name-input'\n      type='text'\n      data-test-input\n      value={{this.playerInfo.name}}\n      {{on 'input' this.updateName}}\n    >\n\n    <button\n      type='submit'\n      data-test-submit\n      disabled={{this.isNameMissing}}\n      {{on 'click' this.submitName}}\n    >\n      Submit\n    </button>\n  </div>\n</form>\n"
  },
  {
    "path": "client/web/pinochle/app/components/name-entry/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { assert } from '@ember/debug';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\n\nimport type PlayerInfo from 'pinochle/services/player-info';\n\ntype Args = {\n  onSubmit: (name: string) => void;\n};\n\nexport default class NameEntry extends Component<Args> {\n  @service declare playerInfo: PlayerInfo;\n\n  get hasName() {\n    return this.playerInfo.name.length > 0;\n  }\n\n  get isNameMissing() {\n    return !this.hasName;\n  }\n\n  @action\n  updateName(e: KeyboardEvent) {\n    assert(`Expected event to be from an input field`, e.currentTarget instanceof HTMLInputElement);\n\n    this.playerInfo.name = e.currentTarget.value;\n  }\n\n  @action\n  submitName(e: Event) {\n    e.preventDefault();\n\n    if (!this.hasName) return;\n\n    this.args.onSubmit(this.playerInfo.name);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/options.hbs",
    "content": "{{!--\n{{#if this.DEBUG}}\n  {{this.poorMansEffect}}\n\n  <label class='fixed:top-right grid:cols gap-2 align-items:center'>\n    <input type='checkbox' {{on 'click' this.toggle}} checked={{this.isSynthwave}}>\n    <span>Synthwave</span>\n  </label>\n{{/if}}\n--}}"
  },
  {
    "path": "client/web/pinochle/app/components/options.ts",
    "content": "import Component from '@glimmer/component';\nimport { DEBUG } from '@glimmer/env';\nimport { action } from '@ember/object';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nexport default class Options extends Component {\n  DEBUG = DEBUG;\n\n  @inLocalStorage isSynthwave = false;\n\n  @action\n  toggle() {\n    this.isSynthwave = !this.isSynthwave;\n  }\n\n  get poorMansEffect() {\n    if (this.isSynthwave) {\n      document.body.classList.add('synthwave');\n\n      return;\n    }\n\n    document.body.classList.remove('synthwave');\n\n    return;\n  }\n\n  willDestroy() {\n    document.body.classList.remove('synthwave');\n    super.willDestroy();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/play/as-guest/-statechart.ts",
    "content": "import type { MachineConfig, StateSchema } from 'xstate';\n\nexport type Context = {\n  name: string;\n};\n\nexport type Event = { type: 'START_GAME_FAILED' } | { type: 'CONNECTED' };\n\nexport interface Schema extends StateSchema<Context> {\n  states: {\n    'waiting-for-hand': StateSchema<Context>;\n  };\n}\n\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  states: {\n    'waiting-for-hand': {},\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/components/play/as-guest/index.hbs",
    "content": "<div class='game-layout'>\n  <PlayerOrder @id={{@game.gameId}} class='area:info' />\n\n  {{#if @game.display.left}}\n    <BackOfCards class='area:left' @game={{@game}} @info={{@game.display.left}} />\n  {{/if}}\n\n  {{#if @game.display.top}}\n    <BackOfCards class='area-top' @game={{@game}} @info={{@game.display.top}} />\n  {{/if}}\n\n  {{#if @game.display.right}}\n    <BackOfCards class='area:right' @game={{@game}} @info={{@game.display.right}} />\n  {{/if}}\n\n  {{#if @game.trick}}\n  <div class='area:trick'>\n    Tricks here\n  </div>\n  {{/if}}\n\n  {{#if this.hasHand}}\n    <Hand\n      @cards={{this.hand}}\n      @onSelect={{this.chooseCard}}\n      class='area:bottom'\n    />\n  {{/if}}\n</div>\n\n{{#if @game.display.hasOfflinePlayers}}\n  <Lazy @wait={{2000}} @when={{@game.display.hasOfflinePlayers}}>\n    <Play::AsGuest::OverlayInfo class='fade-in'>\n      <Play::AsGuest::PlayersOffline @game={{@game}} />\n    </Play::AsGuest::OverlayInfo>\n  </Lazy>\n{{/if}}\n\n"
  },
  {
    "path": "client/web/pinochle/app/components/play/as-guest/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { cached } from '@glimmer/tracking';\nimport { action } from '@ember/object';\n\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { sortHand } from 'pinochle/game/deck';\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { statechart } from './-statechart';\n\nimport type { Card } from 'pinochle/game/card';\nimport type { GameGuest } from 'pinochle/game/networking/guest';\ntype Args = {\n  id: string;\n  game: GameGuest;\n};\n\nexport default class PlayAsGuest extends Component<Args> {\n  @cached\n  get hand() {\n    return sortHand(this.args.game.gameState.hand);\n  }\n\n  get hasHand() {\n    return (this.hand?.length || 0) > 0;\n  }\n\n  @use\n  interpreter = new Statechart(() => {\n    return {\n      named: {\n        chart: statechart,\n        context: {},\n        config: {\n          actions: {},\n        },\n      },\n    };\n  });\n\n  @action\n  chooseCard(card: Card) {\n    this.args.game.playCard(card);\n  }\n\n  /****************************\n   * Machine Actions\n   ***************************/\n  @action\n  getHand() {\n    // return sortHand(this.hostGame);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/play/as-guest/overlay-info.hbs",
    "content": "<div class='fixed:cover grid center current-player-info' ...attributes>\n  {{yield}}\n</div>\n"
  },
  {
    "path": "client/web/pinochle/app/components/play/as-guest/players-offline.hbs",
    "content": "<ShareLink @url={{@game.joinUrl}} ...attributes  {{focus-trap}}>\n  <p class='line-height-8' data-test-waiting-for-players>\n    Waiting for {{english-list @game.display.offlinePlayerNames}} to join.\n    <br>\n    Share this link with them in case they lost it.\n  </p>\n</ShareLink>\n"
  },
  {
    "path": "client/web/pinochle/app/components/player-list.hbs",
    "content": "{{#if @players}}\n\n  <ul class='list:flat'>\n    {{#each @players as |player|}}\n      <li>\n        {{player.name}}\n\n        {{#if (eq @currentName player.name)}}\n          &nbsp;(you)\n        {{/if}}\n      </li>\n    {{/each}}\n  </ul>\n\n{{/if}}\n\n"
  },
  {
    "path": "client/web/pinochle/app/components/player-order/index.hbs",
    "content": "<div class='player-order m-4' ...attributes>\n  Turn order\n\n  <ol>\n    {{#each this.players as |player|}}\n      <li>\n        {{#if (eq player.id this.currentPlayer)}}\n          <span class='turn-marker'>\n            →\n          </span>\n        {{/if}}\n\n        {{player.name}}\n\n        {{#if (eq this.gameInfo.me.id player.id)}}\n          &nbsp;(you)\n        {{/if}}\n      </li>\n    {{/each}}\n  </ol>\n</div>\n"
  },
  {
    "path": "client/web/pinochle/app/components/player-order/index.ts",
    "content": "import Component from '@glimmer/component';\nimport { assert } from '@ember/debug';\nimport { inject as service } from '@ember/service';\n\nimport type GameManager from 'pinochle/services/game-manager';\nimport type PlayerInfo from 'pinochle/services/player-info';\n\ntype Args = {\n  id: string;\n};\n\nexport default class PlayerOrder extends Component<Args> {\n  @service declare gameManager: GameManager;\n  @service declare playerInfo: PlayerInfo;\n\n  get gameInfo() {\n    let info = this.gameManager.isGuestOf.get(this.args.id);\n\n    assert(`This component can't be used without a an active game session`, info);\n\n    return info;\n  }\n\n  get currentPlayer() {\n    return this.gameInfo.gameState.currentPlayer;\n  }\n\n  get players() {\n    return this.gameInfo.playerOrder;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/components/playing-card/corner-value.hbs",
    "content": "<div\n  {{fit-text}}\n  aria-hidden='true'\n  class='card-pip {{@suit}} value-{{@value}}'\n  ...attributes\n>\n  <div class='value'>\n    {{#if (is-number @value)}}\n      {{@value}}\n    {{else}}\n      {{get @value 0}}\n    {{/if}}\n  </div>\n  <div class='suit'>\n    {{suit-to-symbol @suit}}\n  </div>\n</div>"
  },
  {
    "path": "client/web/pinochle/app/components/playing-card/face-value.hbs",
    "content": "<div aria-hidden='true' class='card-face'>\n  {{#let (suit-to-symbol @suit) as |symbol|}}\n    {{#if (is-number @value)}}\n      <div class='three-column fill' {{fit-text scale=0.3}}>\n      {{!--\n        For numbered face cards, there are 10-fixed locations for\n        where the value icons can exist\n      --}}\n      {{#if (eq 2 @value)}}\n        <div></div>\n        <div class='opposite-ends'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div></div>\n      {{else if (eq 3 @value)}}\n        <div></div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div></div>\n      {{else if (eq 4 @value)}}\n        <div class='opposite-ends'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div></div>\n        <div class='opposite-ends'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{else if (eq 5 @value)}}\n        <div class='opposite-ends'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='opposite-ends'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n\n      {{else if (eq 6 @value)}}\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div></div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{else if (eq 7 @value)}}\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span></span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{else if (eq 8 @value)}}\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{else if (eq 9 @value)}}\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{else if (eq 10 @value)}}\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n        <div class='grid center'>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n          <span>{{symbol}}</span>\n        </div>\n      {{/if}}\n      </div>\n    {{else if (eq @value 'ace')}}\n      <div class='fill grid center double-font-size' {{fit-text scale=0.8}}>\n        <span>{{symbol}}</span>\n      </div>\n    {{else}}\n      <div class='fill grid center center-squished' {{fit-text scale=0.4}}>\n        <span>{{@value}}</span>\n        <span class='double-font-size'>{{symbol}}</span>\n      </div>\n    {{/if}}\n\n  {{/let}}\n</div>\n"
  },
  {
    "path": "client/web/pinochle/app/components/playing-card/index.hbs",
    "content": "<li class='playing-card {{@suit}}' ...attributes>\n  <button type='button'>\n    <span class='sr-only'>{{@value}} of {{@suit}}</span>\n\n    <PlayingCard::CornerValue @suit={{@suit}} @value={{@value}} class='top-left' />\n    <PlayingCard::FaceValue @suit={{@suit}} @value={{@value}} />\n    <PlayingCard::CornerValue @suit={{@suit}} @value={{@value}} class='bottom-right' />\n  </button>\n</li>"
  },
  {
    "path": "client/web/pinochle/app/components/share-link.hbs",
    "content": "<div class='share-url grid gap-2' ...attributes>\n  <div class='input-box grid:cols gap-2 left-column'>\n    <input id='url' value={{@url}} readonly {{on 'click' this.highlight}}>\n    <button type='button' {{on 'click' this.copy}}>\n      Copy\n    </button>\n  </div>\n  <label for='url' class={{if @hideLabel 'sr-only'}}>\n    {{#if (has-block)}}\n      {{yield}}\n    {{else}}\n      Share this link with people to join your game\n    {{/if}}\n  </label>\n</div>"
  },
  {
    "path": "client/web/pinochle/app/components/share-link.ts",
    "content": "import Component from '@glimmer/component';\nimport { assert } from '@ember/debug';\nimport { action } from '@ember/object';\n\ntype Args = {\n  url: string;\n};\n\nexport default class ShareLink extends Component<Args> {\n  @action\n  copy() {\n    let url = this.args.url;\n\n    navigator.clipboard.writeText(url);\n  }\n\n  @action\n  highlight(event: MouseEvent) {\n    assert(\n      `event expected to come from an input element`,\n      event.target instanceof HTMLInputElement\n    );\n\n    event.target.select();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/config/environment.d.ts",
    "content": "export default config;\n\n/**\n * Type declarations for\n *    import config from './config/environment'\n *\n * For now these need to be managed by the developer\n * since different ember addons can materialize new entries.\n */\ndeclare const config: {\n  environment: 'production' | 'test' | 'development';\n  modulePrefix: string;\n  podModulePrefix: string;\n  locationType: string;\n  rootURL: string;\n  host: string;\n\n  APP: Record<string, unknown>;\n};\n"
  },
  {
    "path": "client/web/pinochle/app/game/card.ts",
    "content": "import { v4 as uuid } from 'uuid';\n\nexport type Suit = 'hearts' | 'spades' | 'diamonds' | 'clubs';\nexport type Value = 9 | 'jack' | 'queen' | 'king' | 10 | 'ace';\n\nexport const VALUES: Value[] = [9, 'jack', 'queen', 'king', 10, 'ace'];\nexport const SUITS: Suit[] = ['hearts', 'spades', 'diamonds', 'clubs'];\n\n/**\n *\n *\n */\nexport class Card {\n  id = uuid();\n  constructor(public suit: Suit, public value: Value) {}\n\n  toString() {\n    return `${this.suit} : ${this.value} :: ${this.id}`;\n  }\n}\n\nconst VALUE_TO_NUMBER = {\n  9: 9,\n  jack: 10,\n  queen: 11,\n  king: 12,\n  10: 13,\n  ace: 14,\n};\n\nexport function isEqualOrHigherValue(a: Card, b: Card) {\n  return a.suit === b.suit && VALUE_TO_NUMBER[a.value] >= VALUE_TO_NUMBER[b.value];\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/deck.ts",
    "content": "import { assert } from '@ember/debug';\n\nimport { Card, SUITS, VALUES } from './card';\n\nimport type { Suit } from './card';\n\n/**\n * Returns an array of new cards -- this is the \"shuffling\"\n * a \"deck\" is not actually an object or instance of anything\n */\nexport function newDeck() {\n  let deck = [];\n\n  for (let value of VALUES) {\n    for (let suit of SUITS) {\n      // Pinochle has two of every card\n      deck.push(new Card(suit, value));\n      deck.push(new Card(suit, value));\n    }\n  }\n\n  shuffle(deck);\n  // return deck.sort(() => Math.random() - 0.5)\n\n  return deck;\n}\n\n/**\n * Deck should be pre-shuffled when passed in\n */\nexport function splitDeck(deck: Card[], splits: number) {\n  assert(`Deck must have 48 cards`, deck.length === 48);\n\n  let hands: Card[][] = new Array(splits).fill(splits).map(() => []);\n  let remaining = [];\n\n  let handSize;\n\n  if (splits === 3) {\n    handSize = 15;\n  } else if (splits === 4) {\n    handSize = 12;\n  } else {\n    handSize = Math.floor(deck.length / splits);\n  }\n\n  let hand = 0;\n\n  for (let i = 0; i < deck.length; i++) {\n    let card = deck[i];\n\n    if (handSize * splits <= i) {\n      remaining.push(card);\n    } else {\n      hands[hand].push(card);\n\n      hand = (hand + 1) % splits;\n    }\n  }\n\n  return {\n    hands,\n    remaining,\n  };\n}\n\nexport function sortHand(hand: Card[]) {\n  return hand.sort((a, b) => {\n    let indexSuitA = SUITS.indexOf(a.suit);\n    let indexSuitB = SUITS.indexOf(b.suit);\n\n    let indexValueA = VALUES.indexOf(a.value);\n    let indexValueB = VALUES.indexOf(b.value);\n\n    if (indexSuitB === indexSuitA) {\n      if (indexValueA < indexValueB) {\n        return -1;\n      }\n\n      return indexValueB < indexValueA ? 1 : 0;\n    }\n\n    if (indexSuitA < indexSuitB) {\n      return -1;\n    }\n\n    return 1;\n  });\n}\n\nexport function hasSuit(hand: Card[], suit: Suit) {\n  let card = hand.find((card) => card.suit === suit);\n\n  return Boolean(card);\n}\n\n// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Fisher_and_Yates'_original_method\nfunction shuffle<T>(array: T[]) {\n  for (let i = array.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * i);\n    const temp = array[i];\n\n    array[i] = array[j];\n    array[j] = temp;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/meld.ts",
    "content": "import { cached } from '@glimmer/tracking';\n\nimport type { Card, Suit, Value } from 'pinochle/game/card';\n\ninterface MeldResultMatch {\n  trump?: boolean;\n  double?: boolean;\n}\n\ntype MeldResult = {\n  [pointName: string]: {\n    value: number;\n    matches: {\n      [suit in Suit]?: MeldResultMatch;\n    };\n  };\n};\n\ntype SuitInfo = {\n  values: Value[];\n  counts: Map<Value, number>;\n};\n\ntype ValueInfo = {\n  suits: Suit[];\n  counts: Map<Suit, number>;\n};\n\nexport class Meld {\n  declare bySuit: Map<Suit, SuitInfo>;\n  declare byValue: Map<Value, ValueInfo>;\n\n  constructor(public cards: Card[], public trump?: Suit) {\n    this.bySuit = groupBySuit(cards);\n    this.byValue = groupByValue(cards);\n  }\n\n  get score() {\n    return Object.entries(this.matches)\n      .map(([_, info]) => info.value)\n      .reduce((acc, value) => {\n        acc += value;\n\n        return acc;\n      }, 0);\n  }\n\n  @cached\n  get matches(): MeldResult {\n    let result: MeldResult = {};\n\n    for (let [name, fn] of Object.entries(calculator)) {\n      result[name as keyof typeof calculator] = fn(\n        this.cards,\n        this.bySuit,\n        this.byValue,\n        this.trump\n      );\n    }\n\n    if (this.trump && result.run.value > 0) {\n      let localValue = result.marriage.matches[this.trump];\n\n      if (localValue) {\n        let isDouble = localValue.double;\n\n        // TODO: store value with group of cards, rather than a total.\n        //       total should be calculated in 'score'\n        result.marriage.value -= 40;\n\n        if (isDouble && result.run.value === points.run * ROYAL_MULTIPLIER) {\n          result.marriage.value -= 40;\n        }\n      }\n    }\n\n    return result;\n  }\n}\n\ntype CalculatorArgs = [Card[], Map<Suit, SuitInfo>, Map<Value, ValueInfo>, Suit | undefined];\n\n/**\n * I'm realizing that maybe the official rules are different\n * from how my family has played all these years\n * https://bicyclecards.com/how-to-play/pinochle-2/\n *\n * For example:\n *  - official rules state that you can't have a marriage and a pinochle\n *    sharing the queen. psh\n *\n * Apparently different rule sites have different values for what\n * \"double\" of something means. Adding a 0 is def more fun.\n *\n */\nconst ROYAL_MULTIPLIER = 10;\nconst calculator = {\n  /**\n   * Need to keep in mind that a run doessn't count\n   * unless it's in trump\n   *\n   */\n  run(...[, bySuit, _, trump]: CalculatorArgs) {\n    let value = 0;\n\n    if (!trump) {\n      return { value, matches: {} };\n    }\n\n    let suit = bySuit.get(trump);\n\n    if (!suit) {\n      return { value, matches: {} };\n    }\n\n    let jack = suit.counts.get('jack') || 0;\n    let queen = suit.counts.get('queen') || 0;\n    let king = suit.counts.get('king') || 0;\n    let ten = suit.counts.get(10) || 0;\n    let ace = suit.counts.get('ace') || 0;\n\n    let isDouble = jack === 2 && queen === 2 && king === 2 && ten === 2 && ace === 2;\n    let hasRun = jack > 0 && queen > 0 && king > 0 && ten > 0 && ace > 0;\n\n    if (hasRun) {\n      value = points.run;\n    }\n\n    if (isDouble) {\n      value *= ROYAL_MULTIPLIER;\n    }\n\n    return { value, matches: {} };\n  },\n  aces(...[, , byValue]: CalculatorArgs) {\n    let value = 0;\n\n    let hearts = byValue.get('ace')?.counts.get('hearts') || 0;\n    let diamonds = byValue.get('ace')?.counts.get('diamonds') || 0;\n    let spades = byValue.get('ace')?.counts.get('spades') || 0;\n    let clubs = byValue.get('ace')?.counts.get('clubs') || 0;\n    let isDouble = hearts === 2 && diamonds === 2 && spades === 2 && clubs === 2;\n\n    if (hearts > 0 && diamonds > 0 && spades > 0 && clubs > 0) {\n      value = points.hundredAces;\n    }\n\n    // is this a house rule I didn't know about?\n    // the pattern fro real rules should be 200, not 1000?\n    if (isDouble) {\n      value *= ROYAL_MULTIPLIER;\n    }\n\n    return { value, matches: {} };\n  },\n\n  kings(...[, , byValue]: CalculatorArgs) {\n    let value = 0;\n\n    let hearts = byValue.get('king')?.counts.get('hearts') || 0;\n    let diamonds = byValue.get('king')?.counts.get('diamonds') || 0;\n    let spades = byValue.get('king')?.counts.get('spades') || 0;\n    let clubs = byValue.get('king')?.counts.get('clubs') || 0;\n    let isDouble = hearts === 2 && diamonds === 2 && spades === 2 && clubs === 2;\n\n    if (hearts > 0 && diamonds > 0 && spades > 0 && clubs > 0) {\n      value = points.eightyKings;\n    }\n\n    if (isDouble) {\n      value *= ROYAL_MULTIPLIER;\n    }\n\n    return { value, matches: {} };\n  },\n\n  queens(...[, , byValue]: CalculatorArgs) {\n    let value = 0;\n\n    let hearts = byValue.get('queen')?.counts.get('hearts') || 0;\n    let diamonds = byValue.get('queen')?.counts.get('diamonds') || 0;\n    let spades = byValue.get('queen')?.counts.get('spades') || 0;\n    let clubs = byValue.get('queen')?.counts.get('clubs') || 0;\n    let isDouble = hearts === 2 && diamonds === 2 && spades === 2 && clubs === 2;\n\n    if (hearts > 0 && diamonds > 0 && spades > 0 && clubs > 0) {\n      value = points.sixtyQueens;\n    }\n\n    if (isDouble) {\n      value *= ROYAL_MULTIPLIER;\n    }\n\n    return { value, matches: {} };\n  },\n\n  jacks(...[, , byValue]: CalculatorArgs) {\n    let value = 0;\n\n    let hearts = byValue.get('jack')?.counts.get('hearts') || 0;\n    let diamonds = byValue.get('jack')?.counts.get('diamonds') || 0;\n    let spades = byValue.get('jack')?.counts.get('spades') || 0;\n    let clubs = byValue.get('jack')?.counts.get('clubs') || 0;\n    let isDouble = hearts === 2 && diamonds === 2 && spades === 2 && clubs === 2;\n\n    if (hearts > 0 && diamonds > 0 && spades > 0 && clubs > 0) {\n      value = points.fortyJacks;\n    }\n\n    if (isDouble) {\n      value *= ROYAL_MULTIPLIER;\n    }\n\n    return { value, matches: {} };\n  },\n\n  pinochle(...[, bySuit]: CalculatorArgs) {\n    let value = 0;\n\n    let jacks = bySuit.get('diamonds')?.counts.get('jack') || 0;\n    let queens = bySuit.get('spades')?.counts.get('queen') || 0;\n    let isDouble = jacks === 2 && queens === 2;\n\n    if (jacks > 0 && queens > 0 && !isDouble) {\n      value = points.pinochle;\n    }\n\n    if (isDouble) {\n      value = points.doublePinochle;\n    }\n\n    return { value, matches: {} };\n  },\n\n  marriage(...[, bySuit, _, trump]: CalculatorArgs) {\n    let value = 0;\n    let matches = {} as Record<Suit, MeldResultMatch>;\n\n    for (let [suit, info] of bySuit.entries()) {\n      let queens = info.counts.get('queen') || 0;\n      let kings = info.counts.get('king') || 0;\n\n      if (queens > 0 && kings > 0) {\n        let isTrump = suit === trump;\n        let multiplier = queens === 2 && kings === 2 ? 2 : 1;\n        let result: MeldResultMatch = { trump: isTrump };\n\n        if (multiplier === 2) {\n          result.double = true;\n        }\n\n        matches[suit as Suit] = result;\n\n        if (isTrump) {\n          value += multiplier * points['marriageOfTrump'];\n        } else {\n          value += multiplier * points['marriage'];\n        }\n      }\n    }\n\n    return { value, matches };\n  },\n\n  nineOfTrump(...[, , byValue, trump]: CalculatorArgs) {\n    let nines = (trump && byValue.get(9)?.counts.get(trump)) || 0;\n\n    return { value: nines * points.nineOfTrump, matches: {} };\n  },\n};\n\nconst points = {\n  run: 150,\n  marriage: 20,\n  marriageOfTrump: 40,\n  pinochle: 30,\n  doublePinochle: 300,\n  nineOfTrump: 10,\n  hundredAces: 100,\n  thousandAces: 1000,\n  eightyKings: 80,\n  sixtyQueens: 60,\n  fortyJacks: 40,\n  roundHouse: 240,\n};\n\nexport const pointKeyToName = {\n  marriage: 'Marriage',\n};\n\nfunction groupBySuit(cards: Card[]) {\n  return cards.reduce((acc, card) => {\n    let info = acc.get(card.suit) || ({ values: [], counts: new Map() } as SuitInfo);\n\n    info.values.push(card.value);\n    info.counts.set(card.value, (info.counts.get(card.value) || 0) + 1);\n\n    if (!acc.get(card.suit)) {\n      acc.set(card.suit, info);\n    }\n\n    return acc;\n  }, new Map<Suit, SuitInfo>());\n}\n\nfunction groupByValue(cards: Card[]) {\n  return cards.reduce((acc, card) => {\n    let info = acc.get(card.value) || ({ suits: [], counts: new Map() } as ValueInfo);\n\n    info.suits.push(card.suit);\n    info.counts.set(card.suit, (info.counts.get(card.suit) || 0) + 1);\n\n    if (!acc.get(card.value)) {\n      acc.set(card.value, info);\n    }\n\n    return acc;\n  }, new Map<Value, ValueInfo>());\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/-requirements.ts",
    "content": "import { ensureRelays } from '@emberclear/networking';\n\nimport type ApplicationInstance from '@ember/application/instance';\n\nexport async function ensureRequirementsAreMet(owner: ApplicationInstance) {\n  await ensureRelays(owner);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/constants.ts",
    "content": "export const GAME_PHASES = {\n  MELD: 'meld',\n  TRICK: 'trick',\n} as const;\n\nexport type GamePhase = typeof GAME_PHASES[keyof typeof GAME_PHASES];\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/guest/display-info.ts",
    "content": "import { tracked } from '@glimmer/tracking';\n\nimport { next, prev } from 'pinochle/utils/array';\n\nimport type { GameInfo } from '../types';\nimport type { GuestGameRound } from './game-round';\n\nexport class DisplayInfo {\n  @tracked declare info: GameInfo;\n\n  constructor(public currentPlayerId: string, public state: GuestGameRound) {}\n\n  update(info: GameInfo, state?: GuestGameRound) {\n    this.info = info;\n\n    if (state) {\n      this.state = state;\n    }\n  }\n\n  get offlinePlayers() {\n    return Object.values(this.state.playersById).filter((player) => !player.isOnline);\n  }\n\n  get offlinePlayerNames() {\n    return this.offlinePlayers.map((player) => player.name);\n  }\n\n  get hasOfflinePlayers() {\n    return this.offlinePlayers.length > 0;\n  }\n\n  get left() {\n    let leftPlayer = next(this.info.playerOrder, this.currentPlayerId);\n\n    return this.state.playersById[leftPlayer];\n  }\n\n  get right() {\n    let rightPlayer = prev(this.info.playerOrder, this.currentPlayerId);\n\n    return this.state.playersById[rightPlayer];\n  }\n\n  get top() {\n    let playerIds = this.info.playerOrder;\n\n    if (playerIds.length === 3) {\n      if (this.info.playerWhoTookTheBid) {\n        return false;\n      }\n\n      return {\n        name: 'Blind',\n        blind: true,\n        cards: [],\n      };\n    }\n\n    let across = next(playerIds, next(playerIds, this.currentPlayerId));\n\n    return this.state.playersById[across];\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/guest/game-round.ts",
    "content": "import { cached, tracked } from '@glimmer/tracking';\nimport { action } from '@ember/object';\n\nimport { TrackedObject } from 'tracked-built-ins';\n\nimport { fromHex } from '@emberclear/encoding/string';\n\nimport type { GamePhase } from '../constants';\nimport type { GameInfo, GameResult, GameState, GuestPlayer, SerializablePlayer } from '../types';\nimport type { Card } from 'pinochle/game/card';\n\nexport class GuestGameRound {\n  @tracked hand: Card[] = [];\n  @tracked currentPlayer?: string;\n  @tracked scoreHistory: GameResult[] = [];\n  @tracked gamePhase: GamePhase = 'meld';\n  @tracked info?: GameInfo;\n  @tracked playersById: Record<string, GuestPlayer> = new TrackedObject();\n\n  @cached\n  get playerList() {\n    return Object.values(this.playersById);\n  }\n\n  @cached\n  get playerOrder() {\n    if (!this.info) {\n      return [];\n    }\n\n    return this.info.playerOrder.map((id) => {\n      return this.playersById[id];\n    });\n  }\n\n  @action\n  update({ currentPlayer, hand, scoreHistory, info, gamePhase }: GameState) {\n    this._updatePlayers(info);\n\n    Object.assign(this, {\n      currentPlayer,\n      hand,\n      scoreHistory,\n      info,\n      gamePhase,\n    });\n  }\n\n  @action\n  _updatePlayers(msg: { players: SerializablePlayer[] }) {\n    for (let { name, id, isOnline } of msg.players) {\n      this.playersById[id] = {\n        id,\n        name,\n        isOnline,\n        publicKeyAsHex: id,\n        publicKey: fromHex(id),\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/guest/utils.ts",
    "content": "import type { FromHostMessage } from '../types';\n\nexport function verifyMessage(msg: FromHostMessage) {\n  switch (msg.type) {\n    case 'GUEST_UPDATE':\n      return 'info' in msg && 'players' in msg.info && 'playerOrder' in msg.info;\n    default:\n      return true;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/guest.ts",
    "content": "import Ember from 'ember';\nimport { cached, tracked } from '@glimmer/tracking';\nimport { assert } from '@ember/debug';\nimport { action } from '@ember/object';\nimport { inject as service } from '@ember/service';\nimport { waitFor } from '@ember/test-waiters';\n\nimport { timeout } from 'ember-concurrency';\nimport { task } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport RSVP from 'rsvp';\n\nimport { isDestroyed } from 'pinochle/utils/container';\n\nimport { toHex } from '@emberclear/encoding/string';\nimport { EphemeralConnection } from '@emberclear/networking';\nimport { UnknownMessageError } from '@emberclear/networking/errors';\n\nimport { DisplayInfo } from './guest/display-info';\nimport { GuestGameRound } from './guest/game-round';\nimport { verifyMessage } from './guest/utils';\n\nimport type { Card } from '../card';\nimport type { GameMessage, GameState, WelcomeMessage } from './types';\nimport type RouterService from '@ember/routing/router-service';\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\n\nexport type SerializedGuest = {\n  gameId: string;\n  publicKey: string;\n  privateKey: string;\n};\n\n/**\n * TODO:\n * - given a hex / public key as hex, connect to a host\n * - receive turn order\n * - receive hands\n * - handle when the host says a new game should happen\n *\n */\nexport class GameGuest extends EphemeralConnection {\n  @service declare router: RouterService;\n\n  hostExists = RSVP.defer();\n  isWelcomed = RSVP.defer();\n  isStarted = RSVP.defer();\n\n  waitingForCardPlayConfirmation = RSVP.defer();\n  waitingForBidConfirmation = RSVP.defer();\n  waitingForTrumpDeclaration = RSVP.defer();\n\n  /**\n   * Initialized upon receiving host game state\n   */\n  @tracked declare display: DisplayInfo;\n  @tracked declare gameId?: string;\n\n  gameState = new GuestGameRound();\n\n  constructor(publicKeyAsHex: string) {\n    super(publicKeyAsHex);\n\n    this.gameId = this.target?.hex;\n  }\n\n  get playerOrder() {\n    return this.gameState.playerOrder;\n  }\n\n  get joinUrl() {\n    let { origin } = window.location;\n\n    return `${origin}/join/${this.gameId}`;\n  }\n\n  @cached\n  get me() {\n    let id = toHex(this.crypto.keys.publicKey);\n\n    return this.gameState.playersById[id];\n  }\n\n  @action\n  async checkHost() {\n    try {\n      this._checkHost.perform();\n    } catch {\n      /* host doesn't exist here. report error? */\n    }\n\n    return this.hostExists.promise;\n  }\n\n  @task\n  _checkHost = taskFor(async () => {\n    let backoff = 1;\n    let waitingForHost = true;\n\n    this.hostExists.promise.then(() => {\n      waitingForHost = false;\n    });\n\n    while (waitingForHost) {\n      await timeout(1000 * backoff);\n\n      try {\n        await this.send({ type: 'SYN' });\n      } catch (e) {\n        // this is a healthcheck, we don't care about failures\n        console.debug(e);\n      }\n\n      backoff = backoff * 1.5;\n\n      if (Ember.testing && backoff > 5) {\n        break;\n      }\n    }\n  });\n\n  @action\n  async joinHost(name: string) {\n    await this.send({ type: 'JOIN', name });\n\n    return this.isWelcomed.promise;\n  }\n\n  @action\n  waitForStart() {\n    return this.isStarted.promise;\n  }\n\n  @action\n  @waitFor\n  async onData(data: EncryptedMessage) {\n    if (isDestroyed(this)) return;\n\n    let decrypted: GameMessage = await this.crypto.decryptFromSocket(data);\n\n    // console.log('guest received:', {\n    //   gameId: data.uid,\n    //   ...decrypted,\n    // });\n\n    if (isDestroyed(this)) return;\n\n    switch (decrypted.type) {\n      case 'ACK':\n        this.hostExists.resolve();\n        this.gameId = data.uid;\n        await this.sendToHex({ type: 'PRESENT' }, data.uid);\n\n        return;\n      case 'WELCOME':\n        this.handleWelcome(decrypted);\n\n        return;\n\n      case 'START':\n        this.startGame(decrypted);\n\n        return;\n      case 'GAME_FULL':\n        this.router.transitionTo('/game-full');\n\n        return;\n      case 'NOT_RECOGNIZED':\n        this.router.transitionTo('/not-recognized');\n\n        return;\n      case 'GUEST_UPDATE':\n        assert(\n          `${decrypted.type} has invalid payload: ${JSON.stringify(decrypted)}`,\n          verifyMessage(decrypted)\n        );\n        this.updateGameState(decrypted);\n        this.redirectToGame();\n\n        return;\n      case 'CONNECTIVITY_CHECK':\n        await this.sendToHex({ type: 'PRESENT' }, data.uid);\n\n        return;\n      default:\n        console.debug('guest received:', data, decrypted);\n        throw new UnknownMessageError();\n    }\n  }\n\n  /**\n   * All dispatched commands are merely suggestions to the host\n   * the host must verify and \"OK\" all actions\n   *\n   *\n   */\n  @action\n  async playCard(card: Card) {\n    await this.send({ type: 'PLAY_CARD', id: card.id });\n  }\n\n  @action\n  startGame(decrypted: GameState) {\n    this.updateGameState(decrypted);\n    this.isStarted.resolve();\n  }\n\n  @action\n  updateGameState(decrypted: GameState) {\n    if (!this.display) {\n      this.display = new DisplayInfo(this.hexId, this.gameState);\n    }\n\n    this.gameState.update(decrypted);\n    this.display.update(decrypted.info);\n  }\n\n  @action\n  handleWelcome(decrypted: WelcomeMessage) {\n    this.gameState._updatePlayers(decrypted);\n    this.isWelcomed.resolve();\n  }\n\n  @action\n  redirectToGame() {\n    if (this.router.currentRouteName !== 'game') {\n      if (this.gameId) {\n        this.router.transitionTo(`/game/${this.gameId}`);\n      }\n    }\n  }\n\n  /**\n   * Guests don't need to store much, because the host stores all the data\n   *\n   * Guests just need to be aware that they existed.\n   */\n  @action\n  serialize() {\n    if (!this.gameId) return;\n\n    let keys = this.crypto.keys;\n\n    return {\n      gameId: this.gameId,\n      publicKey: toHex(keys.publicKey),\n      privateKey: toHex(keys.privateKey),\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/host/game-round.ts",
    "content": "import { action } from '@ember/object';\n\nimport { use } from 'ember-could-get-used-to-this';\n\nimport { Statechart } from 'pinochle/utils/use-machine';\n\nimport { statechart } from './game-state';\nimport { handById, serializePlayer } from './utils';\n\nimport type { GamePhase } from '../constants';\nimport type { Bid } from '../types';\nimport type { Context, Event, Schema } from './game-state';\nimport type { PlayerInfo } from './types';\nimport type { EncryptableObject } from '@emberclear/crypto/types';\nimport type { Card, Suit } from 'pinochle/game/card';\nimport type { State } from 'xstate';\n\nexport type SerializedRound = {\n  hands: Record<string, Card[]>;\n  blind: Card[];\n  playerOrder: string[];\n  playerWhoTookTheBid: string;\n  currentPlayer: string;\n  phase: GamePhase;\n  bid: number;\n  trump: Suit;\n  state: State<Context, Event>;\n};\n\nexport class GameRound {\n  static loadFrom(players: Record<string, PlayerInfo>, data: SerializedRound) {\n    let round = new GameRound(players, data?.state);\n\n    return round;\n  }\n\n  declare _initialState?: State<Context, Event>;\n\n  /**\n   * players must be passed in in-order\n   *\n   */\n  constructor(protected playersById: Record<string, PlayerInfo>, state?: State<Context, Event>) {\n    this._initialState = state;\n  }\n\n  @use\n  interpreter = new Statechart<Context, Schema, Event>(() => {\n    return {\n      named: {\n        chart: statechart,\n        ...(this._initialState ? { initialState: this._initialState } : {}),\n        context: {\n          playersById: this.playersById as TODO,\n          hasBlind: false,\n          currentPlayer: '',\n          bids: {},\n          blind: [],\n          playerOrder: [],\n          melds: {},\n          isForfeiting: false,\n        },\n        config: {\n          actions: {\n            // sendState: this._sendState,\n            // sendWelcome: this._broadcastJoin,\n            // addPlayer: this._addPlayer,\n            // Networky things\n            // Game Actions\n            // deal: this._deal,\n          },\n        },\n      },\n    };\n  });\n\n  get context() {\n    return this.interpreter?.state?.context || ({} as Context);\n  }\n\n  get currentPlayer() {\n    return this.context.currentPlayer;\n  }\n\n  get info() {\n    let { trump, bid, playerWhoTookTheBid, playerOrder } = this.context;\n\n    return {\n      trump,\n      bid,\n      playerWhoTookTheBid,\n      playerOrder,\n    };\n  }\n\n  @action\n  stateForPlayer(id: string) {\n    let { playersById, currentPlayer, playerOrder, playerWhoTookTheBid, bid, trump } = this.context;\n    let player = playersById[id];\n\n    return ({\n      hand: player.hand,\n      currentPlayer,\n      info: {\n        playerOrder,\n        players: Object.values(playersById).map(serializePlayer),\n        playerWhoTookTheBid,\n        bid,\n        trump,\n      },\n    } as unknown) as EncryptableObject;\n  }\n\n  @action\n  bid({ bid }: Pick<Bid, 'bid'>) {\n    this.interpreter.send('BID', { bid });\n  }\n\n  /*************************************\n   * State Machine Helpers\n   ************************************/\n\n  /**************************************\n   * Utilities\n   *************************************/\n  toJSON() {\n    let {\n      playersById,\n      playerOrder,\n      playerWhoTookTheBid,\n      currentPlayer,\n      bid,\n      trump,\n    } = this.interpreter.state?.context;\n\n    let hands = handById(playersById);\n\n    return {\n      hands,\n      playerOrder,\n      playerWhoTookTheBid,\n      currentPlayer,\n      bid,\n      trump,\n      state: this.interpreter.state?.toJSON(),\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/host/game-state.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { actions, assign, send } from 'xstate';\n\nimport { newDeck, splitDeck } from 'pinochle/game/deck';\n\nimport type { JoinMessage } from '../types';\nimport type { PlayerInfo } from './types';\nimport type { Card, Suit } from 'pinochle/game/card';\nimport type { MachineConfig, StateSchema } from 'xstate';\n\ntype Bid = {\n  type: 'BID';\n  bid: number | 'passed';\n};\ntype Pass = { type: 'PASS' };\ntype DeclareTrump = { type: 'DECLARE_TRUMP'; trump: Suit };\n\ntype StartEvent = { type: 'START' } & Context;\nexport type Event =\n  | Bid\n  | Pass\n  | ({ type: 'JOIN' } & JoinMessage)\n  | { type: 'WON_BID' }\n  | DeclareTrump\n  | StartEvent\n  | { type: 'DISCARD'; cards: Card[] }\n  | { type: 'READY'; player: string }\n  | { type: 'FINISHED' }\n  | { type: 'ACCEPT' }\n  | { type: '__START_ROUND' }\n  | { type: 'PLAY_CARD'; card: Card }\n  | { type: 'TRICK_CONTINUES' }\n  | { type: 'TRICK_ENDS' }\n  | { type: 'SUBMIT_MELD'; player: string }\n  | { type: 'FORFEIT' };\n\nexport interface Context {\n  hasBlind: boolean;\n  blind: Card[];\n  trick?: Card[];\n  currentPlayer: string;\n  playersById: Record<string, PlayerInfo & { hand: Card[] }>;\n  playerOrder: string[];\n  trump?: Suit;\n  bid?: number;\n  playerWhoTookTheBid?: string;\n  bids: Record<string, number | 'passed'>;\n  melds: Record<string, number>;\n  isForfeiting: boolean;\n}\n\nexport interface Schema extends StateSchema<Context> {\n  idle: StateSchema<Context>;\n  bidding: StateSchema<Context>;\n  'won-bid': {\n    states: {\n      'pending-acceptance': StateSchema<Context>;\n      accepted: StateSchema<Context>;\n      discard: StateSchema<Context>;\n    };\n  };\n  'declare-meld': StateSchema<Context>;\n  'phase-trick-taking': {\n    'pending-play': StateSchema<Context>;\n    'evaluate-play': StateSchema<Context>;\n  };\n  'end-game': StateSchema<Context>;\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction didPass(ctx: Context) {\n  return ctx.bids[ctx.currentPlayer] === 'passed';\n}\n\nfunction _nextPlayer(ctx: Pick<Context, 'currentPlayer' | 'playerOrder'>) {\n  let players = ctx.playerOrder;\n  let current = players.indexOf(ctx.currentPlayer);\n  let nextIndex = (current + 1) % players.length;\n\n  return ctx.playerOrder[nextIndex];\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction nextPlayer() {\n  return assign({\n    currentPlayer: _nextPlayer,\n  });\n}\n\nfunction nextBiddingPlayer({ currentPlayer, playerOrder, bids }: Context) {\n  let nextPlayer: undefined | string = undefined;\n\n  for (let i = 0; i < playerOrder.length; i++) {\n    nextPlayer = _nextPlayer({\n      currentPlayer: nextPlayer || currentPlayer,\n      playerOrder,\n    });\n\n    let bid = bids[nextPlayer];\n\n    if (bid !== 'passed') {\n      break;\n    }\n  }\n\n  if (!nextPlayer) {\n    throw new Error('all players are not allowed to pass');\n  }\n\n  return nextPlayer;\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction setTrump() {\n  return assign<Context>({ trump: (_ctx: Context, event: DeclareTrump) => event.trump });\n}\n\nfunction playersWithBids(ctx: Context) {\n  let ids = [];\n\n  for (let [playerId, bidValue] of Object.entries(ctx.bids)) {\n    if (bidValue !== 'passed') {\n      ids.push(playerId);\n    }\n  }\n\n  return ids;\n}\n\nfunction isBiddingOver(ctx: Context) {\n  return (\n    playersWithBids(ctx).length === 1 && ctx.playerOrder.length === Object.keys(ctx.bids).length\n  );\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction setBidWinnerInfo() {\n  return assign<Context>({\n    currentPlayer: (ctx: Context) => playersWithBids(ctx)[0],\n    playerWhoTookTheBid: (ctx: Context) => playersWithBids(ctx)[0],\n    bid: (ctx: Context) => {\n      let player = playersWithBids(ctx)[0];\n      let bid = ctx.bids[player];\n\n      if (!bid || bid === 'passed') {\n        throw new Error('Invalid bid');\n      }\n\n      return bid;\n    },\n  });\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction hasEveryoneSubmittedMeld() {\n  return false;\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nfunction hasBlind(ctx: Context) {\n  return ctx.hasBlind;\n}\n\nfunction deal(context: Context) {\n  let { playersById, playerOrder } = context;\n  let deck = newDeck();\n  let { hands, remaining } = splitDeck(deck, playerOrder.length);\n\n  for (let i = 0; i < playerOrder.length; i++) {\n    let id = playerOrder[i];\n    let player = playersById[id];\n\n    player.hand = hands[i];\n  }\n\n  return { blind: remaining };\n}\n\n// TODO: extract sub machines to test individually?\nfunction nextPlayerOrder(context: Context) {\n  let order = Object.keys(context.playersById);\n\n  return order;\n}\n\nexport const statechart: MachineConfig<Context, Schema, Event> = {\n  id: 'game',\n  initial: 'idle' as TODO /* initial has incorrect type? or MachineConfig takes extra type args? */,\n  states: {\n    idle: {\n      entry: assign<Context>((context) => {\n        let order = nextPlayerOrder(context);\n\n        return {\n          playerOrder: order,\n          currentPlayer: order[0],\n        };\n      }),\n      always: 'dealing',\n    },\n    dealing: {\n      entry: assign<Context>((context) => {\n        let { blind } = deal(context);\n\n        return {\n          hasBlind: blind.length > 0,\n          blind: blind,\n        };\n      }),\n      always: 'bidding',\n    },\n    bidding: {\n      entry: [\n        actions.choose([\n          {\n            cond: isBiddingOver,\n            actions: [send('__BIDDING_OVER__')],\n          },\n        ]),\n      ],\n      on: {\n        BID: [\n          {\n            target: 'bidding',\n            actions: assign<Context>((ctx, event: Bid) => {\n              let bids = {\n                ...ctx.bids,\n                [ctx.currentPlayer]: event.bid,\n              };\n              let currentPlayer = nextBiddingPlayer(ctx);\n\n              return {\n                bids,\n                currentPlayer,\n              };\n            }),\n          },\n        ],\n        PASS: [\n          {\n            target: 'bidding',\n            actions: assign<Context>((ctx) => {\n              let bids = {\n                ...ctx.bids,\n                [ctx.currentPlayer]: 'passed',\n              } as Context['bids'];\n\n              let currentPlayer = nextBiddingPlayer(ctx);\n\n              return {\n                bids,\n                currentPlayer,\n              };\n            }),\n          },\n        ],\n        __BIDDING_OVER__: 'won-bid',\n      },\n    },\n    'won-bid': {\n      id: 'won-bid',\n      initial: 'pending-acceptance',\n      entry: ['setBidWinnerInfo', 'giveBlind'],\n      states: {\n        'pending-acceptance': {\n          on: {\n            ACCEPT: {\n              target: 'accepted',\n            },\n            FORFEIT: {\n              target: '#game.declare-meld',\n              actions: [assign<Context>({ isForfeiting: () => true })],\n            },\n          },\n        },\n        accepted: {\n          on: {\n            DECLARE_TRUMP: [\n              {\n                cond: hasBlind,\n                actions: setTrump,\n                target: '#won-bid.discard',\n              },\n              {\n                target: '#game.declare-meld',\n                actions: setTrump,\n              },\n            ],\n          },\n        },\n        discard: {\n          on: {\n            DISCARD: {\n              // TODO: loop until 3 cards are discarded\n              //       prevent setting the discarded cards\n              //       if more than 3 cards are discarded\n              target: '#game.declare-meld',\n            },\n          },\n        },\n      },\n    },\n\n    'declare-meld': {\n      on: {\n        SUBMIT_MELD: [\n          {\n            cond: hasEveryoneSubmittedMeld,\n            target: '#game.phase-trick-taking',\n          },\n          { target: '#game.declare-meld' },\n        ],\n      },\n    },\n    'phase-trick-taking': {\n      entry: ['storePreviousTrick', 'newTrick', 'determineFirstPlayer'],\n      initial: 'pending-play',\n      states: {\n        'pending-play': {\n          on: {\n            PLAY_CARD: 'evaluate-play',\n          },\n        },\n        'evaluate-play': {\n          on: {\n            TRICK_CONTINUES: { actions: 'nextPlayer', target: 'pending-play' },\n            TRICK_ENDS: [\n              {\n                cond: 'isGameOver',\n                target: '#game.end-game',\n              },\n              { target: '#game.phase-trick-taking' },\n            ],\n          },\n        },\n      },\n    },\n    'end-game': {},\n  },\n};\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/host/types.ts",
    "content": "import type { SerializablePlayer } from '../types';\nimport type { SerializedRound } from './game-round';\nimport type RSVP from 'rsvp';\n\nexport type PlayerInfo = {\n  id: string;\n  name: string;\n  publicKeyAsHex: string;\n  publicKey: Uint8Array;\n  onlineCheck?: RSVP.Deferred<unknown>;\n  isOnline?: boolean;\n};\n\nexport type SerializedHost = {\n  id: string;\n  privateKey: string;\n  players: SerializablePlayer[];\n  gameRound: SerializedRound;\n};\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/host/utils.ts",
    "content": "import type { PlayerInfo } from './types';\nimport type { Card } from 'pinochle/game/card';\n\ntype HasHand = { hand: Card[] };\n\nexport function serializePlayer(player: PlayerInfo) {\n  return {\n    id: player.id,\n    name: player.name,\n    publicKeyAsHex: player.id,\n    isOnline: player.isOnline,\n  };\n}\n\nexport function handById(playersById: Record<string, HasHand>) {\n  return Object.entries(playersById).reduce((acc, [id, { hand }]) => {\n    acc[id] = hand;\n\n    return acc;\n  }, {} as Record<string, Card[]>);\n}\n\nexport function unwrapObject<T = unknown>(obj: Record<string, T>) {\n  return Object.entries(obj).reduce((acc, [k, v]) => {\n    acc[k] = v;\n\n    return acc;\n  }, {} as Record<string, T>);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/host.ts",
    "content": "import { cached } from '@glimmer/tracking';\nimport { action } from '@ember/object';\nimport { waitFor } from '@ember/test-waiters';\n\nimport { timeout } from 'ember-concurrency';\nimport { dropTask } from 'ember-concurrency-decorators';\nimport { taskFor } from 'ember-concurrency-ts';\nimport RSVP from 'rsvp';\nimport { TrackedObject } from 'tracked-built-ins';\n\nimport { isDestroyed } from 'pinochle/utils/container';\n\nimport { fromHex, toHex } from '@emberclear/encoding/string';\nimport { EphemeralConnection } from '@emberclear/networking';\n\n// import { UnknownMessageError } from '@emberclear/networking/errors';\nimport { GameRound } from './host/game-round';\nimport { unwrapObject } from './host/utils';\n\nimport type { PlayerInfo } from './host/types';\nimport type { GameMessage } from './types';\nimport type { EncryptedMessage } from '@emberclear/crypto/types';\n\nconst MAX_PLAYERS = 4;\n// const MIN_PLAYERS = 3;\n\n/**\n * TODO:\n *  - track number of people connected\n *  - put those people in an order, with option to shuffle (us included)\n *  - implement card communication\n *  - add the ability to restart a game at any time\n *    - useful for the home rule of too many 9s\n */\nexport class GameHost extends EphemeralConnection {\n  playersById = new TrackedObject<Record<string, PlayerInfo>>();\n\n  shouldCheckConnectivity = true;\n\n  declare currentGame: GameRound;\n\n  teardown() {\n    this.onlineChecker.cancelAll();\n    // this.currentGame?.interpreter\n\n    super.teardown();\n  }\n\n  @cached\n  get players() {\n    return Object.values(this.playersById);\n  }\n\n  get otherPlayers() {\n    return this.players.filter((player) => player.publicKeyAsHex !== this.hexId);\n  }\n\n  get numConnected() {\n    return this.players.length;\n  }\n\n  get joinUrl() {\n    let { origin } = window.location;\n\n    return `${origin}/join/${this.hexId}`;\n  }\n\n  /**\n   * 4 Types of messages:\n   * - 1. Always\n   * - 2. Only during a game\n   * - 3. Always outside of a game\n   * - 4. When the player is known\n   *\n   * All other messages a dropped\n   *\n   */\n  @action\n  @waitFor\n  async onData(data: EncryptedMessage) {\n    if (isDestroyed(this)) return;\n\n    let decrypted: GameMessage = await this.crypto.decryptFromSocket(data);\n\n    if (isDestroyed(this)) return;\n\n    // console.debug('host received:', {\n    //   from: data.uid,\n    //   ...decrypted,\n    //   isKnown: this._isPlayerKnown(data.uid),\n    //   hasGame: Boolean(this.currentGame),\n    // });\n\n    if (this.currentGame) {\n      if (!this._isPlayerKnown(data.uid)) {\n        return this._notRecognized(data.uid);\n      }\n\n      switch (decrypted.type) {\n        case 'SYN':\n          this._ack(data.uid);\n\n          return this._broadcastPlayerList();\n        case 'JOIN':\n          return this._sendState(data.uid);\n        case 'REQUEST_STATE':\n          return this._sendState(data.uid);\n        case 'PRESENT':\n          return this._markOnline(data.uid);\n        case 'BID':\n          this.currentGame.bid(decrypted);\n\n          return;\n        case 'PLAY_CARD':\n          // TODO:\n          // - is valid play\n          // - make the play\n          // - remove card from hand\n          // - send update to everyone\n          return;\n      }\n\n      return;\n    }\n\n    switch (decrypted.type) {\n      case 'SYN':\n        return this._ack(data.uid);\n      case 'JOIN':\n        if (this.players.length <= MAX_PLAYERS) {\n          return this._addPlayer(decrypted, data.uid);\n        }\n\n        return;\n    }\n\n    if (this._isPlayerKnown(data.uid)) {\n      switch (decrypted.type) {\n        case 'REQUEST_STATE':\n          return this._sendState(data.uid);\n        case 'PRESENT':\n          return this._markOnline(data.uid);\n      }\n    }\n\n    console.debug('host unexpectedly received:', {\n      data,\n      ...decrypted,\n      isKnown: this._isPlayerKnown(data.uid),\n      hasGame: Boolean(this.currentGame),\n    });\n  }\n\n  /**\n   * Called from button in the UI from the Host\n   */\n  @action\n  startGame() {\n    this.onlineChecker.perform();\n    this.currentGame = new GameRound(unwrapObject(this.playersById));\n\n    this._broadcastStart();\n  }\n\n  /*************************************\n   * State Machine Helpers\n   ************************************/\n  @action\n  _broadcastStart() {\n    for (let player of this.players) {\n      this.sendToHex(\n        {\n          type: 'START',\n          ...this.currentGame.stateForPlayer(player.publicKeyAsHex),\n        },\n        player.publicKeyAsHex\n      );\n    }\n  }\n\n  @action\n  _broadcastState() {\n    for (let player of this.players) {\n      this._sendState(player.id);\n    }\n  }\n\n  @action\n  _sendState(id: string) {\n    return this.sendToHex(\n      {\n        type: 'GUEST_UPDATE',\n        ...this.currentGame.stateForPlayer(id),\n      },\n      id\n    );\n  }\n\n  // @action\n  // _requestBid(id: string) {}\n\n  // @action\n  // _requestTurn(id: string) {}\n\n  @action\n  _notRecognized(id: string) {\n    this.sendToHex({ type: 'NOT_RECOGNIZED' }, id);\n  }\n\n  @action\n  _ack(id: string) {\n    this.sendToHex({ type: 'ACK' }, id);\n  }\n\n  @action\n  _gameFull(id: string) {\n    this.sendToHex({ type: 'GAME_FULL' }, id);\n  }\n\n  @action\n  _isPlayerKnown(id: string) {\n    let player = this.players.find((player) => player.publicKeyAsHex === id);\n\n    return Boolean(player);\n  }\n\n  @action\n  _addPlayer({ name }: { name: string }, publicKeyAsHex: string) {\n    if (this.players.length === MAX_PLAYERS) {\n      this.sendToHex({ type: 'GAME_FULL' }, publicKeyAsHex);\n\n      return;\n    }\n\n    this.playersById[publicKeyAsHex] = {\n      publicKeyAsHex,\n      name,\n      id: publicKeyAsHex,\n      publicKey: fromHex(publicKeyAsHex),\n    };\n\n    this._broadcastPlayerList();\n  }\n\n  @action\n  _broadcastPlayerList() {\n    let serializablePlayers = this.players.map((player) => ({\n      id: player.publicKeyAsHex,\n      name: player.name,\n      isOnline: player.isOnline,\n    }));\n\n    for (let player of this.otherPlayers) {\n      this.sendToHex({ type: 'WELCOME', players: serializablePlayers }, player.publicKeyAsHex);\n    }\n  }\n\n  @action\n  _markOnline(uid: string) {\n    let player = this.playersById[uid];\n\n    player.isOnline = true;\n    player.onlineCheck?.resolve(true);\n  }\n\n  /**************************************\n   * Utilities\n   *************************************/\n\n  @action\n  serialize() {\n    return {\n      id: this.hexId,\n      privateKey: toHex(this.crypto.keys.privateKey),\n      players: this.players.map((player) => ({\n        name: player.name,\n        id: player.publicKeyAsHex,\n        isOnline: player.isOnline,\n      })),\n      gameRound: this.currentGame?.toJSON(),\n    };\n  }\n\n  @dropTask\n  onlineChecker = taskFor(async () => {\n    // this loop takes 7s per iteration\n    // eslint-disable-next-line no-constant-condition\n    while (this.shouldCheckConnectivity) {\n      await timeout(2000);\n\n      let promises = this.players.map(async (player) => {\n        if (!player.onlineCheck) {\n          player.onlineCheck = RSVP.defer();\n\n          this.sendToHex({ type: 'CONNECTIVITY_CHECK' }, player.publicKeyAsHex);\n        }\n\n        return RSVP.race([player.onlineCheck.promise, timeout(5000)]).then((value) => {\n          if (!value) {\n            player.isOnline = false;\n          }\n        });\n      });\n\n      await Promise.all(promises);\n\n      this._broadcastPlayerList();\n\n      this.players.map((player) => (player.onlineCheck = undefined));\n    }\n  });\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/networking/types.ts",
    "content": "import type { Card, Suit } from '../card';\nimport type { GamePhase } from './constants';\n\nexport type GuestPlayer = {\n  id: string;\n  name: string;\n  publicKeyAsHex: string;\n  publicKey: Uint8Array;\n  isOnline: boolean;\n};\n\nexport type SerializablePlayer = {\n  name: string;\n  id: string;\n  isOnline: boolean;\n};\n\nexport type GameInfo = {\n  trump: Suit;\n  bid: number;\n  playerWhoTookTheBid: string;\n  playerOrder: string[];\n  players: SerializablePlayer[];\n};\n\nexport type GameResult = {\n  info: GameInfo;\n  players: {\n    [playerId: string]: {\n      // TODO: have card values and specific history?\n      meld: number;\n      tricks: number;\n      lastTrick: boolean;\n      // including last trick\n      pointsFromTricks: number;\n    };\n  };\n};\n\nexport type GameState = {\n  info: GameInfo;\n  gamePhase: GamePhase;\n  currentPlayer: string;\n  // Players are not aware of other player's hands\n  hand: Card[];\n  scoreHistory: GameResult[];\n  // ... etc\n};\n\nexport type JoinMessage = { type: 'JOIN'; name: string };\nexport type Syn = { type: 'SYN' };\nexport type Ack = { type: 'ACK' };\nexport type GameFull = { type: 'GAME_FULL' };\nexport type ConnectivityCheck = { type: 'CONNECTIVITY_CHECK' };\nexport type Present = { type: 'PRESENT' };\nexport type NotRecognized = { type: 'NOT_RECOGNIZED' };\nexport type RequestState = { type: 'REQUEST_STATE' };\n\nexport type Start = { type: 'START' } & GameState;\nexport type UpdateForGuest = { type: 'GUEST_UPDATE' } & GameState;\nexport type WelcomeMessage = { type: 'WELCOME'; players: SerializablePlayer[] };\n\nexport type PlayCard = { type: 'PLAY_CARD'; id: string };\nexport type Bid = { type: 'BID'; bid: number };\nexport type DeclareTrump = { type: 'DECLARE_TRUMP'; trump: Suit };\nexport type DeclareMeld = { type: 'DECLARE_MELD'; meld: unknown };\n\nexport type FromHostMessage =\n  | Ack\n  | WelcomeMessage\n  | UpdateForGuest\n  | Start\n  | GameFull\n  | NotRecognized\n  | ConnectivityCheck;\n\ntype FromGuestMessage =\n  | JoinMessage\n  | Syn\n  | RequestState\n  | PlayCard\n  | Bid\n  | Present\n  | DeclareTrump\n  | DeclareMeld;\n\nexport type GameMessage = FromHostMessage | FromGuestMessage;\n"
  },
  {
    "path": "client/web/pinochle/app/game/trick.ts",
    "content": "import type { Card } from './card';\n\nconst POINTS = ['king', 'ace', 10];\n\nexport class Trick {\n  private stack: Card[] = [];\n\n  static from(stack: Card[]) {\n    let trick = new Trick(stack.length);\n\n    for (let card of stack) {\n      trick.add(card);\n    }\n\n    return trick;\n  }\n\n  constructor(public maxSize: number) {}\n\n  get points() {\n    return this.stack.filter((card) => POINTS.includes(card.value)).length;\n  }\n\n  get suit() {\n    return this.stack[0]?.suit;\n  }\n\n  get last() {\n    return this.stack[this.stack.length - 1];\n  }\n\n  get isEmpty() {\n    return this.stack.length === 0;\n  }\n\n  add(card: Card) {\n    if (this.stack.length === this.maxSize) {\n      throw new Error('This trick may not have more cards added');\n    }\n\n    this.stack.push(card);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/game/utils/move-validation.ts",
    "content": "import { isEqualOrHigherValue } from '../card';\nimport { hasSuit } from '../deck';\n\nimport type { Card, Suit } from '../card';\nimport type { Trick } from '../trick';\n\n/**\n * NOTE: for scenarios where the suit does not match the last\n *       card in the trick, more information is required than\n *       just the played card.\n *\n * TODO?: pass the whole hand?\n *\n */\nexport function isValidMove(trick: Trick, card: Card, trump: Suit) {\n  return availableMoves(trick, [card], trump).includes(card);\n}\n\nexport function availableMoves(trick: Trick, hand: Card[], trump: Suit) {\n  if (trick.isEmpty) {\n    return hand;\n  }\n\n  let hasMatchingSuit = hasSuit(hand, trick.suit);\n\n  if (!hasMatchingSuit) {\n    let hasTrump = hasSuit(hand, trump);\n\n    if (hasTrump) {\n      return hand.filter((card) => card.suit === trump);\n    }\n\n    return hand;\n  }\n\n  let matchingSuits = hand.filter((card) => isEqualOrHigherValue(card, trick.last));\n\n  if (matchingSuits.length) {\n    return matchingSuits;\n  }\n\n  return hand.filter((card) => isEqualOrHigherValue(card, trick.last));\n}\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/and.ts",
    "content": "export function and<T = unknown>(a: T, b: T) {\n  return a && b;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/contains.ts",
    "content": "import { helper } from '@ember/component/helper';\n\ntype PositionalArgs = [unknown[], unknown];\n\nexport function contains([list, element]: PositionalArgs /*, hash*/) {\n  return list.includes(element);\n}\n\nexport default helper(contains);\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/english-list.ts",
    "content": "export default function englishList(list: string[]) {\n  let amount = list.length;\n\n  if (amount > 2) {\n    let [last, ...parts] = list.reverse();\n\n    return `${parts.reverse().join(', ')}, and ${last}`;\n  }\n\n  return list.join(' and ');\n}\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/eq.ts",
    "content": "import { helper } from '@ember/component/helper';\n\ntype PositionalParams = unknown[];\n\nexport function eq([a, b]: PositionalParams /*, hash*/) {\n  return a === b;\n}\n\nexport default helper(eq);\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/is-number.ts",
    "content": "import { helper } from '@ember/component/helper';\n\ntype PositionalParams = [unknown];\n\nexport function isNumber([maybe]: PositionalParams /*, hash*/) {\n  return Number.isFinite(maybe);\n}\n\nexport default helper(isNumber);\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/not.ts",
    "content": "export function not<T = unknown>([element]: T[]) {\n  return !element;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/or.ts",
    "content": "export function or<T = unknown>(...elements: T[]) {\n  return elements.some((element) => Boolean(element));\n}\n"
  },
  {
    "path": "client/web/pinochle/app/helpers/suit-to-symbol.ts",
    "content": "import type { Suit } from 'pinochle/game/card';\n\nexport const NAME_MAP = {\n  hearts: '♥',\n  spades: '♠',\n  diamonds: '♦',\n  clubs: '♣',\n};\n\nexport function suitToSymbol(name: Suit) {\n  return NAME_MAP[name];\n}\n\n// Ember requires default exports for helpers/**\nexport default suitToSymbol;\n"
  },
  {
    "path": "client/web/pinochle/app/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Pinochle</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link integrity=\"\" rel=\"stylesheet\" href=\"{{rootURL}}assets/pinochle.css\">\n\n    {{content-for \"head-footer\"}}\n  </head>\n  <body>\n    {{content-for \"body\"}}\n\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/pinochle.js\"></script>\n\n    {{content-for \"body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/pinochle/app/modifiers/fit-text.ts",
    "content": "import { assert } from '@ember/debug';\nimport { action } from '@ember/object';\nimport { debounce } from '@ember/runloop';\n\nimport Modifier from 'ember-modifier';\n\ntype Args = {\n  positional: [];\n  named: { scale: number };\n};\n\nexport default class FitTextModifier extends Modifier<Args> {\n  didInstall() {\n    window.addEventListener('resize', this.resizeText);\n    this.resizeText();\n  }\n\n  willRemove() {\n    window.removeEventListener('resize', this.resizeText);\n  }\n\n  @action\n  resizeText() {\n    return debounce(this, '_resizeText', 200);\n  }\n\n  @action\n  _resizeText() {\n    // It seems that if the fontSize is changed to quickly fater a resize\n    // even an animationFrame is too soo before the CSS Animations have finished\n    // computing\n    setTimeout(() => {\n      requestAnimationFrame(() => {\n        if (this.isDestroyed || this.isDestroying) {\n          return;\n        }\n\n        assert('expected element to be an HTML Element', this.element instanceof HTMLElement);\n\n        let elementWidth = this.element.clientWidth;\n        let compressor = 0.12;\n\n        let fontSize = (elementWidth / (compressor * 10)) * (this.args.named.scale || 1);\n\n        this.element.style.fontSize = `${fontSize}px`;\n      });\n    }, 10);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/modifiers/resize.ts",
    "content": "import { action } from '@ember/object';\nimport { debounce } from '@ember/runloop';\n\nimport Modifier from 'ember-modifier';\n\ntype Args = {\n  positional: [() => unknown];\n  named: Record<string, unknown>;\n};\n\nexport default class ResizeModifier extends Modifier<Args> {\n  didInstall() {\n    window.addEventListener('resize', this.callback);\n  }\n\n  willRemove() {\n    window.removeEventListener('resize', this.callback);\n  }\n\n  @action\n  callback() {\n    return debounce(this, '_callback', 100);\n  }\n\n  @action\n  _callback() {\n    return this.args.positional[0]();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/modifiers/stack.ts",
    "content": "import { assert } from '@ember/debug';\nimport { action } from '@ember/object';\n\nimport Modifier from 'ember-modifier';\n\nimport { getPoints } from 'pinochle/components/hand/-animation/key-frames';\n\ntype Args = {\n  positional: [() => void];\n  // eslint-disable-next-line @typescript-eslint/ban-types\n  named: {};\n};\n\n/**\n * as soon as able, make this modifier local to the hand\n */\nexport default class StackModifier extends Modifier<Args> {\n  didInstall() {\n    requestAnimationFrame(this.stack);\n  }\n\n  @action\n  stack() {\n    let cards = this.element.querySelectorAll('.playing-card');\n\n    let { path } = getPoints(cards.length);\n\n    for (let i = 0; i < cards.length; i++) {\n      let card = cards[i];\n\n      assert(`expected to be an html element`, card instanceof HTMLElement);\n\n      card.animate(\n        [\n          {\n            transform: `translate3d(${0 - 0.5 * i}%, ${0 - 0.5 * i}%, 0)`,\n            transformOrigin: `50% ${path.y}px`,\n          },\n        ],\n        {\n          duration: 250,\n          iterations: 1,\n          fill: 'both',\n        }\n      );\n    }\n\n    setTimeout(() => {\n      if (this.isDestroyed || this.isDestroying) {\n        return;\n      }\n\n      let callback = this.args.positional[0];\n\n      if (callback) {\n        callback();\n      }\n    }, 300);\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/router.ts",
    "content": "import EmberRouter from '@ember/routing/router';\n\nimport config from 'pinochle/config/environment';\n\nexport default class Router extends EmberRouter {\n  location = config.locationType;\n  rootURL = config.rootURL;\n}\n\nRouter.map(function () {\n  this.route('host');\n  this.route('join', { path: 'join/:idOfHost/' });\n  this.route('game', { path: 'game/:idOfHost/' });\n  this.route('game-full');\n  this.route('not-recognized');\n});\n"
  },
  {
    "path": "client/web/pinochle/app/routes/game.ts",
    "content": "import { assert } from '@ember/debug';\nimport Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type GameManager from 'pinochle/services/game-manager';\nimport type PlayerInfo from 'pinochle/services/player-info';\ntype Transition = ReturnType<RouterService['transitionTo']>;\n\ninterface Params {\n  idOfHost: string;\n}\n\n/**\n * /game/:id can only be visited by players\n *\n * :id is the public key as hex of the host game\n *\n * NOTE: the person who is hosting the game also has a player\n * identity.\n *\n */\nexport default class GameRoute extends Route {\n  @service declare gameManager: GameManager;\n  @service declare playerInfo: PlayerInfo;\n  @service declare router: RouterService;\n\n  async beforeModel(transition: Transition) {\n    let hostId = transition.to.params.idOfHost;\n    let gameGuest = this.gameManager.isGuestOf.get(hostId || '');\n\n    if (hostId === 'undefined') {\n      console.debug(transition.to);\n      throw new Error(`Undefined hostId`);\n    }\n\n    /**\n     * TODO: add some global error / toast / flash messages\n     */\n    if (!gameGuest) {\n      this.router.transitionTo(`/join/${hostId}`);\n    }\n  }\n\n  async model(params: Params) {\n    let hostId = params.idOfHost;\n\n    let gameGuest = this.gameManager.isGuestOf.get(hostId || '');\n\n    assert(`Cannot play a game that has not been joined`, gameGuest);\n\n    return {\n      hostId,\n      game: gameGuest,\n    };\n  }\n\n  async afterModel() {\n    this.gameManager.storeAll();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/routes/join.ts",
    "content": "import Route from '@ember/routing/route';\nimport { inject as service } from '@ember/service';\n\nimport type RouterService from '@ember/routing/router-service';\nimport type GameManager from 'pinochle/services/game-manager';\nimport type PlayerInfo from 'pinochle/services/player-info';\n\ntype Transition = ReturnType<RouterService['transitionTo']>;\n\ninterface Params {\n  idOfHost: string;\n}\n\nexport default class JoinRoute extends Route {\n  @service declare gameManager: GameManager;\n  @service declare playerInfo: PlayerInfo;\n\n  async beforeModel(transition: Transition) {\n    let hostId = transition.to.params.idOfHost;\n\n    if (hostId === 'undefined') {\n      console.debug(transition.to);\n      throw new Error(`Undefined hostId`);\n    }\n\n    if (hostId) {\n      try {\n        await this.gameManager.loadHost(hostId);\n      } catch (e) {\n        console.error(e);\n      }\n    }\n  }\n\n  async model(params: Params) {\n    let { idOfHost } = params;\n\n    let skipName = Boolean(localStorage.getItem(`guest-${idOfHost}`));\n\n    return {\n      hostId: params.idOfHost,\n      skipName,\n    };\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/services/game-manager.ts",
    "content": "import { getOwner } from '@ember/application';\nimport Service from '@ember/service';\n\nimport { timeout } from 'ember-concurrency';\n\nimport { ensureRequirementsAreMet } from 'pinochle/game/networking/-requirements';\nimport { GameGuest } from 'pinochle/game/networking/guest';\nimport { GameHost } from 'pinochle/game/networking/host';\nimport { GameRound } from 'pinochle/game/networking/host/game-round';\n\nimport { fromHex } from '@emberclear/encoding/string';\n\nimport type { KeyPair } from '@emberclear/crypto';\nimport type { SerializedGuest } from 'pinochle/game/networking/guest';\nimport type { SerializedHost } from 'pinochle/game/networking/host/types';\n\nexport default class GameManager extends Service {\n  /**\n   * Note, the player that hosts will also be a guest\n   */\n  isGuestOf = new Map<string, GameGuest>();\n  isHosting = new Map<string, GameHost>();\n\n  async createHost(keys?: KeyPair) {\n    await ensureRequirementsAreMet(getOwner(this));\n\n    let host = await GameHost.build(this, { keys });\n\n    this.isHosting.set(host.hexId, host);\n\n    return host;\n  }\n\n  async connectToHost(publicKeyAsHex: string, keys?: KeyPair) {\n    await ensureRequirementsAreMet(getOwner(this));\n\n    let guest = await GameGuest.build(this, { publicKeyAsHex, keys });\n\n    this.isGuestOf.set(publicKeyAsHex, guest);\n\n    return guest;\n  }\n\n  storeAll() {\n    let allHosts = [...this.isHosting.entries()];\n    let allGuests = [...this.isGuestOf.entries()];\n\n    let hostIds = allHosts.map(([id]) => id);\n    let guestIds = allGuests.map(([id]) => id);\n\n    store('hosting', hostIds);\n    store('guests', guestIds);\n\n    for (let [id, host] of allHosts) {\n      store(`host-${id}`, host.serialize());\n    }\n\n    for (let [gameId, guest] of allGuests) {\n      store(`guest-${gameId}`, guest.serialize());\n    }\n  }\n\n  async loadHosts() {\n    try {\n      await this._loadHosts();\n    } catch (e) {\n      localStorage.setItem('GameManager#loadAll:error (hosts)', JSON.stringify(e));\n    }\n\n    let hostIds = loadWithDefault('hosting', []) as string[];\n\n    hostIds.map((id) => localStorage.removeItem(`host-${id}`));\n    localStorage.removeItem('hosting');\n  }\n\n  async loadGuests() {\n    try {\n      await this._loadGuests();\n    } catch (e) {\n      localStorage.setItem('GameManager#loadAll:error (guests)', JSON.stringify(e));\n    }\n\n    let hostIds = loadWithDefault('guests', []) as string[];\n\n    hostIds.map((id) => localStorage.removeItem(`guest-${id}`));\n    localStorage.removeItem('guests');\n  }\n\n  async loadHost(id: string) {\n    let hostData = loadWithDefault(`host-${id}`) as SerializedHost;\n\n    if (hostData) {\n      let publicKey = fromHex(hostData.id);\n      let privateKey = fromHex(hostData.privateKey);\n      let host = await this.createHost({\n        publicKey,\n        privateKey,\n      });\n\n      for (let player of hostData.players) {\n        host.playersById[player.id] = {\n          id: player.id,\n          name: player.name,\n          publicKeyAsHex: player.id,\n          publicKey: fromHex(player.id),\n          isOnline: false,\n        };\n      }\n\n      host.currentGame = GameRound.loadFrom(host.playersById, hostData.gameRound);\n      host._broadcastState();\n    }\n  }\n\n  async _loadHosts() {\n    let hostIds = loadWithDefault('hosting', []);\n\n    for (let id of hostIds) {\n      await this.loadHost(id);\n    }\n  }\n\n  async _loadGuests() {\n    let guestIds = loadWithDefault('guests', []);\n\n    await timeout(2000);\n\n    for (let gameId of guestIds) {\n      let guestData = loadWithDefault(`guest-${gameId}`) as SerializedGuest;\n\n      if (!guestData) {\n        localStorage.removeItem(`guest-${gameId}`);\n        continue;\n      }\n\n      if (guestData) {\n        let publicKey = fromHex(guestData.publicKey);\n        let privateKey = fromHex(guestData.privateKey);\n\n        let guest = await this.connectToHost(guestData.gameId, { publicKey, privateKey });\n\n        guest.setTarget(guestData.gameId);\n\n        await guest.sendToHex({ type: 'REQUEST_STATE' }, guestData.gameId);\n      }\n    }\n  }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'game-manager': GameManager;\n  }\n}\n\nexport function loadWithDefault<T>(key: string, defaultValue?: T) {\n  let lsValue = localStorage.getItem(key);\n\n  if (!lsValue) return defaultValue;\n\n  let { value } = JSON.parse(lsValue) || {};\n\n  return value || defaultValue;\n}\n\nexport function store<T>(key: string, value: T) {\n  localStorage.setItem(key, JSON.stringify({ value }));\n}\n"
  },
  {
    "path": "client/web/pinochle/app/services/guest/dispatcher.ts",
    "content": "import Service from '@ember/service';\n\nexport default class GuestDispatcher extends Service {}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'guest/dispatcher': GuestDispatcher;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/services/guest/handler.ts",
    "content": "import Service, { inject as service } from '@ember/service';\n\n// import { UnknownMessageError } from 'pinochle/../../addons/networking/addon/errors';\n// import type GuestDispatcher from './dispatcher';\nimport type RouterService from '@ember/routing/router-service';\n// import type { EncryptedMessage } from 'pinochle/../../addons/crypto/addon/types';\n// import type { GameGuest } from 'pinochle/game/networking/guest';\n// import type { GameMessage } from 'pinochle/game/networking/types';\n\nexport default class GuestMessageHandler extends Service {\n  @service declare router: RouterService;\n  // @service declare dispat'ffcher: GuestDispatcher;\n\n  // async onData(connection: GameGuest, data: EncryptedMessage) {\n  //   let decrypted: GameMessage = await connection.crypto.decryptFromSocket(data);\n\n  //   switch (decrypted.type) {\n  //     case 'ACK':\n  //       connection.hostExists.resolve();\n  //       connection.gameId = data.uid;\n\n  //       return;\n  //     case 'WELCOME':\n  //       connection.updatePlayers(decrypted);\n  //       connection.isWelcomed.resolve();\n\n  //       return;\n\n  //     case 'START':\n  //       connection.startGame(decrypted);\n\n  //       return;\n  //     case 'GAME_FULL':\n  //       this.router.transitionTo('/game-full');\n\n  //       return;\n  //     case 'GUEST_UPDATE':\n  //       connection.updateGameState(decrypted);\n\n  //       return;\n  //     default:\n  //       console.debug(data, decrypted);\n  //       throw new UnknownMessageError();\n  //   }\n  // }\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'guest/handler': GuestMessageHandler;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/services/player-info.ts",
    "content": "import Service from '@ember/service';\n\nimport { inLocalStorage } from 'ember-tracked-local-storage';\n\nexport default class PlayerInfo extends Service {\n  @inLocalStorage name = '';\n}\n\n// DO NOT DELETE: this is how TypeScript knows how to look up your services.\ndeclare module '@ember/service' {\n  interface Registry {\n    'player-info': PlayerInfo;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/-variables.css",
    "content": ":root {\n  /* Sizes */\n  --size-0-5: 0.125rem;\n  --size-1: 0.25rem;\n  --size-2: 0.5rem;\n  --size-3: 0.75rem;\n  --size-4: 1rem;\n  --size-5: 1.25rem;\n  --size-6: 1.5rem;\n  --size-8: 2rem;\n\n  /* Responsiveness */\n  --screen-small: 400px;\n\n  /* A11y Helpers */\n  --outline: 0 0 0 3px rgba(0, 123, 255, 0.5);\n\n  /* Playing Cards */\n  --deck-red: #c65355;\n  --deck-black: #534f4c;\n  --card-gutter: 15%;\n  --deck-rotation: -30deg;\n  --deck-height-offset-x: -50%;\n  --deck-height-offset-y: -10%;\n  --deck-shadow: 0 1px 2px 0 rgba(151, 150, 146, 0.4), 0 4px 12px 0 rgba(151, 150, 146, 0.4);\n\n  /* hand open / spread */\n  --spread-shadow: 0 1px 2px 0 rgba(151, 150, 146, 0.4), 0 4px 4px 0 rgba(151, 150, 146, 0.4);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/app.css",
    "content": "/* @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,700,900'); */\n@import './-variables.css';\n@import './resets.css';\n@import './playing-card.css';\n@import './z-indexes.css';\n@import './utils.css';\n@import './page/ask-game-type.css';\n@import './synthwave.css';\n@import './loading.css';\n@import './game.css';\n@import './transitions.css';\n\n/* \"body\", but doesn't also style the whole test UI */\n.ember-application {\n  background: #e7e7e7;\n  font-weight: 200;\n  font-size: 1.4rem;\n  height: 100vh;\n  overflow: hidden;\n  width: 100vw;\n}\n\n.share-url {\n  max-width: calc(100vw - var(--size-4));\n  min-width: 25vw;\n}\n\n.input-box {\n  background: white;\n  box-shadow: inset 0 1px 2px rgb(0, 0, 0, 0.9);\n  padding: var(--size-3) var(--size-4);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/game.css",
    "content": ".player-order .turn-marker {\n  position: absolute;\n  margin-left: -3rem;\n}\n\n.game-layout {\n  width: 100vw;\n  height: 100vh;\n  display: grid;\n  grid-template-areas:\n    'info          player-top    the-blind'\n    'player-left   tricks        player-right'\n    'player-bottom player-bottom player-bottom';\n  grid-template-rows: 1fr 1fr 2fr;\n  grid-template-columns: 1fr 1fr 1fr;\n}\n\n.hand-name {\n  /* margin-top: 2rem; */\n}\n\n/* .game-layout > div[class*='area:'] {} */\n\n.game-layout .area\\:info {\n  grid-area: info;\n}\n\n.game-layout .area\\:blind {\n  grid-area: the-blind;\n  justify-self: center;\n}\n\n.game-layout .area\\:trick {\n  grid-area: tricks;\n  justify-self: center;\n}\n\n.game-layout .area\\:top {\n  grid-area: player-top;\n  justify-self: center;\n}\n\n.game-layout .area\\:left {\n  grid-area: player-left;\n  justify-self: center;\n}\n\n.game-layout .area\\:right {\n  grid-area: player-right;\n  justify-self: center;\n}\n\n.game-layout .area\\:bottom {\n  grid-area: player-bottom;\n  justify-self: center;\n  align-items: center;\n}\n\n.current-player-info {\n  background: rgba(255, 255, 255, 0.8);\n}\n\n@media only screen and (min-width: 1000px) {\n  .current-player-info .width-container {\n    max-width: 500px;\n  }\n}\n\n.game-layout .area\\:left,\n.game-layout .area\\:right,\n.game-layout .area\\:top {\n  height: 100%;\n  width: 100%;\n  text-align: center;\n  justify-items: center;\n  position: relative;\n}\n\n.game-layout .other-player .display {\n  display: grid;\n  grid-template-rows: 1fr auto;\n  height: 100%;\n  width: 100%;\n  text-align: center;\n  justify-items: center;\n}\n\n.game-layout .player-offline {\n  opacity: 0.5;\n}\n\n.game-layout .player-offline-indicator {\n  position: absolute;\n  z-index: 1;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  transform: translateY(40%);\n  display: flex;\n  gap: var(--size-2);\n  flex-direction: column;\n  font-size: 1.5rem;\n  font-weight: bold;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/loading.css",
    "content": ".indeterminate-loader {\n  height: 5px;\n  overflow-x: hidden;\n  position: relative;\n}\n\n.indeterminate-loader .line {\n  position: absolute;\n  opacity: 0.4;\n  background: #4a8df8;\n  width: 150%;\n  height: 5px;\n}\n\n.indeterminate-loader .subline {\n  position: absolute;\n  background: #4a8df8;\n  height: 5px;\n}\n\n.indeterminate-loader .inc {\n  animation: increase 2s infinite;\n}\n\n.indeterminate-loader .dec {\n  animation: decrease 2s 0.5s infinite;\n}\n\n@keyframes increase {\n  from {\n    left: -5%;\n    width: 5%;\n  }\n\n  to {\n    left: 130%;\n    width: 100%;\n  }\n}\n\n@keyframes decrease {\n  from {\n    left: -80%;\n    width: 80%;\n  }\n\n  to {\n    left: 110%;\n    width: 10%;\n  }\n}\n\n/* ehh, super WIP, need to figure out text effect */\n.skeleton {\n  /* background-image: linear-gradient(90deg, red 0px, green 40px, red 80px); */\n\n  background-clip: text;\n  background-size: 600px;\n  animation: shine-avatar 1s infinite linear;\n}\n\n@keyframes shine-avatar {\n  0% {\n    background-position: -100px;\n  }\n\n  40%,\n  100% {\n    background-position: 140px;\n  }\n}\n\n.ellipsis-loader span {\n  opacity: 0;\n  animation: ellipsis-dot 1s infinite;\n}\n\n.ellipsis-loader span:nth-child(1) {\n  animation-delay: 0s;\n}\n\n.ellipsis-loader span:nth-child(2) {\n  animation-delay: 0.1s;\n}\n\n.ellipsis-loader span:nth-child(3) {\n  animation-delay: 0.2s;\n}\n\n@-webkit-keyframes ellipsis-dot {\n  0% {\n    opacity: 0;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n@keyframes ellipsis-dot {\n  0% {\n    opacity: 0;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/page/ask-game-type.css",
    "content": ".ask-game-type {\n  padding: 1rem;\n  text-align: center;\n  font-size: 2rem;\n  display: grid;\n  align-items: center;\n}\n\n.ask-game-type section {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n\n  /* actual center looks weird for some reason */\n  transform: translateY(-20%);\n}\n\n.ask-game-type .game-type-options {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 2rem;\n  justify-content: center;\n  align-items: center;\n}\n\n.ask-game-type .game-type-options a {\n  font-size: 3rem;\n  text-decoration: none;\n  text-align: center;\n  padding: 1rem;\n  background: #efefef;\n  border-radius: 0.25rem;\n\n  /* border: 1px solid black; */\n  box-shadow: 0 3px 3px rgba(0, 0, 0, 0.3);\n  transition: all 0.2s;\n  color: black;\n}\n\n.ask-game-type .game-type-options a:hover {\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 1px 1px rgba(0, 0, 0, 0.1);\n}\n\n.ask-game-type .game-type-options a:active {\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 2px 2px rgba(0, 0, 0, 0.1);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/playing-card.css",
    "content": ".player-hand {\n  cursor: pointer;\n  position: relative;\n  transform: translateX(2rem);\n  list-style: none;\n  width: 100%;\n  padding: 0;\n  height: 66%;\n}\n\n@media only screen and (min-width: 1000px) {\n  .player-hand {\n    transform: rotateX(15deg) translate3d(50%, 0%, 0);\n  }\n}\n\n.player-hand-info {\n  position: fixed;\n  left: 0.5rem;\n  bottom: 0.5rem;\n}\n\n.back-of-hand {\n  margin: 0;\n  padding: 0;\n  position: relative;\n  list-style: none;\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.back-of-hand.playing-hand .non-player-card {\n  position: absolute;\n  transition: all ease-in 0.3s;\n  user-select: none;\n}\n\n.back-of-hand.playing-hand .non-player-card button {\n  background: #333;\n  border: 1px solid;\n  border-radius: 1rem;\n  border-radius: min(1rem, 2vw);\n  width: 100px;\n  height: 150px;\n}\n\n.back-of-hand .card1 {\n  transform: rotateZ(15deg) translate3d(-50%, -10%, 0);\n}\n\n.back-of-hand .card2 {\n  transform: rotateZ(10deg) translateX(-22%);\n}\n\n.back-of-hand .card3 {\n  transform: translateX(10%);\n}\n\n.back-of-hand .card4 {\n  transform: rotate(22.5deg) translate3d(40%, -5%, 0);\n}\n\n.playing-card {\n  background: #eeeae7;\n  border-radius: 1rem;\n  border-radius: min(1rem, 2vw);\n  box-shadow: 0 1px 2px 0 rgba(151, 150, 146, 0.8);\n  height: 45vw;\n  max-height: 450px;\n  max-width: 320px;\n  min-height: 225px;\n  min-width: 160px;\n  position: absolute;\n  transition: all ease-in 0.3s;\n  width: 32vw;\n  user-select: none;\n}\n\n.playing-card button {\n  width: 100%;\n  height: 100%;\n  border: none;\n  background: none;\n  border-radius: min(1rem, 2vw);\n  display: flex;\n}\n\n.playing-card:first-child {\n  /* box-shadow: var(--deck-shadow); */\n}\n\n.playing-card.hearts,\n.playing-card.hearts button,\n.playing-card.diamonds,\n.playing-card.diamonds button {\n  color: var(--deck-red);\n}\n\n.playing-card.spades,\n.playing-card.clubs {\n  color: var(--deck-black);\n}\n\n.player-hand.spread .playing-card {\n  box-shadow: var(--spread-shadow);\n}\n\n.card-pip .value,\n.card-pip .suit {\n  display: block;\n  text-transform: uppercase;\n}\n\n.card-pip .suit {\n  font-size: 0.9em;\n}\n\n.card-pip {\n  /* padding: 1.8rem 1.6rem; */\n  padding: 2% 1%;\n  width: var(--card-gutter);\n  position: absolute;\n  display: grid;\n  grid-gap: 0.5rem;\n  text-align: center;\n  transition: all ease-in 0.1s;\n}\n\n.card-pip.top-left {\n  top: 0;\n  left: 0;\n}\n\n.card-pip.bottom-right {\n  bottom: 0;\n  right: 0;\n  transform: rotate(180deg);\n}\n\n.card-face {\n  position: absolute;\n  left: var(--card-gutter);\n  right: var(--card-gutter);\n  top: 10%;\n  bottom: 10%;\n\n  /* max-width: 70%; */\n\n  /* left: 0; right: 0; top: 0; bottom: 0; */\n\n  /* padding: 10% var(--card-gutter); */\n  transition: all ease-in 0.1s;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/resets.css",
    "content": "* {\n  box-sizing: border-box;\n  font-family: 'System-UI', 'Segoe UI', sans-serif;\n}\n\nbody,\nhtml {\n  padding: 0;\n  margin: 0;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-weight: 200;\n}\n\ninput {\n  font-size: 1rem;\n  border: 0;\n  background: none;\n}\n\ninput:focus-visible,\ninput.focus-visible:focus {\n  outline: none;\n  box-shadow: var(--outline);\n}\n\nbutton {\n  border-radius: var(--size-0-5);\n  padding: var(--size-2) var(--size-4);\n  box-shadow: 0 1px 2px rgb(0, 0, 0, 0.5);\n  border: 0;\n  transition: all 0.1s;\n}\n\nbutton:hover:active,\nbutton:active {\n  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5), 0 1px 2px rgb(0, 0, 0, 0.5);\n}\n\nbutton:hover {\n  box-shadow: 0 1px 1px rgb(0, 0, 0, 0.5);\n}\n\nul {\n  list-style: none;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/synthwave.css",
    "content": ".synthwave h1 {\n  text-shadow: 3px 3px 65px #f82425, 6px 8px 0 #9f00fd, 10px -8px 0 #de016a;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/transitions.css",
    "content": ".fade-in {\n  animation: fadeIn ease 0.2s;\n}\n\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n  }\n\n  100% {\n    opacity: 1;\n  }\n}\n\n.fade-out {\n  animation: fadeOut ease 0.2s;\n}\n\n@keyframes fadeOut {\n  0% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/app/styles/utils.css",
    "content": ".sr-only:not(:focus):not(:active) {\n  clip: rect(0 0 0 0);\n  clip-path: inset(50%);\n  height: 1px;\n  overflow: hidden;\n  position: absolute;\n  white-space: nowrap;\n  width: 1px;\n}\n\n.fullscreen {\n  width: 100vw;\n  height: 100vh;\n}\n\n.grid {\n  display: grid;\n}\n\n.grid\\:cols {\n  display: grid;\n  grid-auto-flow: column;\n}\n\n.gap-2 {\n  gap: var(--size-2);\n}\n\n.gap-3 {\n  gap: var(--size-3);\n}\n\n.gap-4 {\n  gap: var(--size-4);\n}\n\n.fixed\\:top-right {\n  position: fixed;\n  top: 0.5rem;\n  right: 0.5rem;\n  z-index: var(--z-options);\n}\n\n.fixed\\:cover {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  z-index: 1;\n  padding: var(--size-4);\n}\n\n.left-column {\n  grid-template-columns: 1fr auto;\n}\n\n.bottom-favored {\n  grid-template-rows: 1fr auto;\n}\n\n.fill {\n  width: 100%;\n  height: 100%;\n  max-width: 100%;\n  max-height: 100%;\n}\n\n.space-between {\n  justify-content: space-between;\n}\n\n.center {\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n}\n\n.list\\:flat,\n.list\\:flat li {\n  padding: 0;\n}\n\n.m-0 {\n  margin: 0;\n}\n\n.m-4 {\n  margin: var(--size-4);\n}\n\n.align-items\\:center {\n  align-items: center;\n}\n\n.three-column {\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr;\n}\n\n.opposite-ends {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.center-squished {\n  align-content: center;\n}\n\n.font-5 {\n  font-size: var(--size-5);\n}\n\n.line-height-8 {\n  line-height: var(--size-8);\n}\n\n.double-font-size {\n  font-size: 2em;\n}\n\n/* TODO: move to own sheet */\n\n/*\n  Provide basic, default focus styles.\n*/\nbutton:focus {\n  outline: none;\n}\n\n/*\n  Remove default focus styles for mouse users ONLY if\n  :focus-visible is supported on this platform.\n*/\nbutton:focus:not(:focus-visible),\nbutton:focus:not(.focus-visible) {\n  outline: none;\n}\n\n/*\n  Optionally: If :focus-visible is supported on this\n  platform, provide enhanced focus styles for keyboard\n  focus.\n*/\nbutton:focus-visible,\nbutton.focus-visible:focus {\n  outline: none;\n  box-shadow: var(--outline);\n}\n\n/*\n * button:focus-ring,\n button:-moz-focusring {\n   box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5);\n }\n */\n"
  },
  {
    "path": "client/web/pinochle/app/styles/z-indexes.css",
    "content": ":root {\n  --z-options: 1000;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/templates/application.hbs",
    "content": "<Options />\n\n{{outlet}}\n"
  },
  {
    "path": "client/web/pinochle/app/templates/game-full.hbs",
    "content": "<main class='grid center fullscreen'>\n  <section>\n    <h2>Game is full</h2>\n  </section>\n</main>\n"
  },
  {
    "path": "client/web/pinochle/app/templates/game.hbs",
    "content": "<Play::AsGuest\n  @id={{@model.hostId}}\n  @game={{@model.game}}\n/>"
  },
  {
    "path": "client/web/pinochle/app/templates/host.hbs",
    "content": "<HostGame />\n"
  },
  {
    "path": "client/web/pinochle/app/templates/index.hbs",
    "content": "<main class='ask-game-type fullscreen'>\n  <section>\n    <h1>Pinochle</h1>\n\n    <div class='game-type-options'>\n      <LinkTo @route='host'>Start a new game</LinkTo>\n    </div>\n  </section>\n</main>\n"
  },
  {
    "path": "client/web/pinochle/app/templates/join.hbs",
    "content": "<JoinGame @hostId={{@model.hostId}} @skipName={{@model.skipName}} />\n"
  },
  {
    "path": "client/web/pinochle/app/templates/not-recognized.hbs",
    "content": "<main class='grid center fullscreen'>\n  <section>\n    <h2>You are not a known member of the game you tried joining</h2>\n  </section>\n</main>\n"
  },
  {
    "path": "client/web/pinochle/app/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"composite\": true,\n    \"declarationDir\": \"../declarations\",\n    \"paths\": {\n      \"pinochle/*\": [\"*\"],\n\n      // https://github.com/ember-polyfills/ember-cached-decorator-polyfill#typescript-usage\n      \"@glimmer/tracking\": [\n        \"../../node_modules/ember-cached-decorator-polyfill\",\n        \"../../node_modules/@glimmer/tracking/dist/types\"\n      ],\n\n      \"@ember/destroyable\": [\"../../node_modules/ember-destroyable-polyfill\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../../libraries/questionably-typed\" },\n    { \"path\": \"../../addons/tracked-local-storage\" },\n    { \"path\": \"../../addons/crypto\" },\n    { \"path\": \"../../addons/encoding\" },\n    { \"path\": \"../../addons/local-account\" },\n    { \"path\": \"../../addons/networking\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/pinochle/app/utils/array.ts",
    "content": "export function prev<T>(arr: T[], value: T) {\n  let currentIndex = arr.indexOf(value);\n  let maybePrev = currentIndex - 1;\n  let prevIndex = maybePrev < 0 ? arr.length - 1 : maybePrev;\n  let element = arr[prevIndex];\n\n  return element;\n}\n\nexport function next<T>(arr: T[], value: T) {\n  let currentIndex = arr.indexOf(value);\n  let maybeNext = currentIndex + 1;\n  let nextIndex = maybeNext === arr.length ? 0 : maybeNext;\n  let element = arr[nextIndex];\n\n  return element;\n}\n"
  },
  {
    "path": "client/web/pinochle/app/utils/container.ts",
    "content": "/* eslint-disable @typescript-eslint/ban-types */\nimport { isDestroyed as _isDestroyed, isDestroying as _isDestroying } from '@ember/destroyable';\n\n/**\n * Wraps isDestroyed and isDestroying under one function\n * because for 99% of the time, we don't care about the difference.\n */\nexport function isDestroyed(context: object) {\n  return _isDestroyed(context) || _isDestroying(context);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/utils/dom.ts",
    "content": "import { assert } from '@ember/debug';\n\n/**\n * TODO: write babel plugin that removes these from the build during production build\n */\n\nexport function isHTMLElement(\n  element: null | undefined | Element | EventTarget\n): asserts element is HTMLElement {\n  return assert(`expected ${element} to be an HTML Element`, element);\n}\n"
  },
  {
    "path": "client/web/pinochle/app/utils/trig.ts",
    "content": "type XYPoint = { x: number; y: number };\n\nexport function degreesToRadians(degrees: number) {\n  return (degrees * Math.PI) / 180;\n}\n\nexport function radiansToDegrees(radians: number) {\n  return (radians * 180) / Math.PI;\n}\n\n/**\n * https://www.xarg.org/2018/02/create-a-circle-out-of-three-points/\n *\n * NOTE: this'll likely break if two of the points have the same value for either x or y\n */\nexport function circleFromThreePoints(p1: XYPoint, p2: XYPoint, p3: XYPoint) {\n  let x1 = p1.x;\n  let y1 = p1.y;\n  let x2 = p2.x;\n  let y2 = p2.y;\n  let x3 = p3.x;\n  let y3 = p3.y;\n\n  let a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2;\n\n  let b =\n    (x1 * x1 + y1 * y1) * (y3 - y2) +\n    (x2 * x2 + y2 * y2) * (y1 - y3) +\n    (x3 * x3 + y3 * y3) * (y2 - y1);\n\n  let c =\n    (x1 * x1 + y1 * y1) * (x2 - x3) +\n    (x2 * x2 + y2 * y2) * (x3 - x1) +\n    (x3 * x3 + y3 * y3) * (x1 - x2);\n\n  let x = -b / (2 * a);\n  let y = -c / (2 * a);\n\n  return {\n    x: x,\n    y: y,\n    r: Math.hypot(x - x1, y - y1),\n  };\n}\n"
  },
  {
    "path": "client/web/pinochle/app/utils/use-machine.ts",
    "content": "import { DEBUG } from '@glimmer/env';\nimport { tracked } from '@glimmer/tracking';\nimport { assert } from '@ember/debug';\nimport { action } from '@ember/object';\nimport { cancel, later } from '@ember/runloop';\n\nimport { Resource } from 'ember-could-get-used-to-this';\nimport { createMachine, interpret } from 'xstate';\n\nimport type {\n  EventObject,\n  Interpreter,\n  MachineConfig,\n  MachineOptions,\n  State,\n  StateMachine,\n  StateSchema,\n  Typestate,\n} from 'xstate';\nimport type { StateListener } from 'xstate/lib/interpreter';\n\nconst INTERPRETER = Symbol('interpreter');\nconst CONFIG = Symbol('config');\nconst MACHINE = Symbol('machine');\n\nconst ERROR_CANT_RECONFIGURE = `Cannot re-invoke withContext after the interpreter has been initialized`;\nconst ERROR_CHART_MISSING = `A statechart was not passed`;\n\ntype Args<Context, Schema extends StateSchema, Event extends EventObject> = {\n  positional?: [MachineConfig<Context, Schema, Event>];\n  named?: {\n    chart: MachineConfig<Context, Schema, Event>;\n    config?: Partial<MachineOptions<Context, Event>>;\n    context?: Context;\n    initialState?: State<Context, Event>;\n    onTransition?: StateListener<Context, Event, Schema, Typestate<Context>>;\n  };\n};\n\ntype SendArgs<Context, Schema extends StateSchema, Event extends EventObject> = Parameters<\n  Interpreter<Context, Schema, Event>['send']\n>;\n\n/**\n *\n  @use\n  interpreter = new Statechart(() => [statechart])\n    .withContext(this.context).withConfig({\n      actions: {\n        returnSelectedToHand: this._returnSelectedToHand,\n        showSelected: this._showSelected,\n        closeHand: this._closeHand,\n        fanOpen: this._fanOpen,\n        adjustHand: this._adjustHand,\n      },\n      guards: {},\n    });\n */\nexport class Statechart<\n  Context,\n  Schema extends StateSchema,\n  Event extends EventObject\n> extends Resource<Args<Context, Schema, Event>> {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  declare [MACHINE]: StateMachine<any, Schema, Event>;\n  declare [INTERPRETER]: Interpreter<Context, Schema, Event>;\n\n  @tracked declare state: State<Context, Event>;\n\n  /**\n   * This is the return value of `new Statechart(() => ...)`\n   */\n  get value(): {\n    state: State<Context, Event>;\n    send: Statechart<Context, Schema, Event>['send'];\n    // withContext: Statechart<Context, Schema, Event>['withContext'];\n    // withConfig: Statechart<Context, Schema, Event>['withConfig'];\n    // onTransition: Statechart<Context, Schema, Event>['onTransition'];\n  } {\n    if (!this[INTERPRETER]) {\n      this._setupMachine();\n    }\n\n    assert(`Expected state to exist`, this.state);\n\n    return {\n      // For TypeScript, this is tricky because this is what is accessible at the call site\n      // but typescript thinks the context is the class instance.\n      //\n      // To remedy, each property has to also exist on the class body under the same name\n      state: this.state,\n      send: this.send,\n\n      /**\n       * One Time methods\n       * Currently disabled due to issues with the use/resource transform not allowing\n       * the builder pattern\n       *\n       * If the transform is fixed, we can remove the protected visibility modifier\n       * and uncomment out these three lines in a back-compat way for existing users\n       */\n      // withContext: this.withContext,\n      // withConfig: this.withConfig,\n      // onTransition: this.onTransition,\n    };\n  }\n\n  @action\n  protected withContext(context?: Context) {\n    assert(ERROR_CANT_RECONFIGURE, !this[INTERPRETER]);\n\n    if (context) {\n      this[MACHINE] = this[MACHINE].withContext(context);\n    }\n\n    return this;\n  }\n\n  @action\n  protected withConfig(config?: Partial<MachineOptions<Context, Event>>) {\n    assert(ERROR_CANT_RECONFIGURE, !this[INTERPRETER]);\n\n    if (config) {\n      this[MACHINE] = this[MACHINE].withConfig({\n        ...config,\n        actions: {\n          ...config.actions,\n        },\n      });\n    }\n\n    return this;\n  }\n\n  @action\n  protected onTransition(fn?: StateListener<Context, Event, Schema, Typestate<Context>>) {\n    if (!this[INTERPRETER]) {\n      this._setupMachine();\n    }\n\n    if (fn) {\n      this[INTERPRETER].onTransition(fn);\n    }\n\n    return this;\n  }\n\n  /**\n   * Private\n   */\n\n  private get [CONFIG]() {\n    return this.args.named;\n  }\n\n  @action\n  send(...args: SendArgs<Context, Schema, Event>) {\n    return this[INTERPRETER].send(...args);\n  }\n\n  @action\n  private _setupMachine() {\n    this.withContext(this[CONFIG]?.context);\n    this.withConfig(this[CONFIG]?.config);\n\n    let state = this[CONFIG]?.initialState;\n\n    if (state) {\n      // this[MACHINE].resolveState(State.from(state));\n    }\n\n    this[INTERPRETER] = interpret(this[MACHINE], {\n      devTools: DEBUG,\n      clock: {\n        setTimeout(fn, ms) {\n          return later.call(null, fn, ms);\n        },\n        clearTimeout(timer) {\n          return cancel.call(null, timer);\n        },\n      },\n    }).onTransition((state) => {\n      this.state = state;\n\n      // console.log('state:', state.toJSON());\n    });\n\n    this.onTransition(this[CONFIG]?.onTransition);\n\n    this[INTERPRETER].start();\n  }\n\n  /**\n   * Lifecycle methods on Resource\n   *\n   */\n  @action\n  protected setup() {\n    let statechart = this.args.positional?.[0] || this.args.named?.chart;\n\n    assert(ERROR_CHART_MISSING, statechart);\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    this[MACHINE] = createMachine(statechart as any);\n  }\n\n  protected teardown() {\n    if (!this[INTERPRETER]) return;\n\n    this[INTERPRETER].stop();\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/config/ember-cli-update.json",
    "content": "{\n  \"schemaVersion\": \"1.0.0\",\n  \"packages\": [\n    {\n      \"name\": \"ember-cli\",\n      \"version\": \"3.21.2\",\n      \"blueprints\": [\n        {\n          \"name\": \"app\",\n          \"outputRepo\": \"https://github.com/ember-cli/ember-new-output\",\n          \"codemodsSource\": \"ember-app-codemods-manifest@1\",\n          \"isBaseBlueprint\": true,\n          \"options\": []\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "client/web/pinochle/config/environment.js",
    "content": "'use strict';\n\nmodule.exports = function (environment) {\n  let ENV = {\n    modulePrefix: 'pinochle',\n    environment,\n    rootURL: '/',\n    locationType: 'auto',\n    EmberENV: {\n      FEATURES: {\n        // Here you can enable experimental features on an ember canary build\n        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true\n      },\n      EXTEND_PROTOTYPES: {\n        // Prevent Ember Data from overriding Date.parse.\n        Date: false,\n      },\n    },\n\n    APP: {\n      // Here you can pass flags/options to your application instance\n      // when it is created\n    },\n  };\n\n  if (environment === 'development') {\n    // ENV.APP.LOG_RESOLVER = true;\n    // ENV.APP.LOG_ACTIVE_GENERATION = true;\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n    // ENV.APP.LOG_VIEW_LOOKUPS = true;\n  }\n\n  if (environment === 'test') {\n    // Testem prefers this...\n    ENV.locationType = 'none';\n\n    // keep test console output quieter\n    ENV.APP.LOG_ACTIVE_GENERATION = false;\n    ENV.APP.LOG_VIEW_LOOKUPS = false;\n\n    ENV.APP.rootElement = '#ember-testing';\n    ENV.APP.autoboot = false;\n\n    // ENV.APP.LOG_TRANSITIONS = true;\n    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;\n  }\n\n  if (environment === 'production') {\n    // here you can enable a production-specific feature\n  }\n\n  return ENV;\n};\n"
  },
  {
    "path": "client/web/pinochle/config/netlify/_redirects",
    "content": "https://pinochle.emberclear.netlify.com/* https://pinochle.emberclear.io/:splat 301!\n\n/bundle.html /bundle.html 200\n/bundle/* /bundle/:splat 200\n\n/.well-known/assetlinks.json /.well-known/assetlinks.json 200\n\n/*    /index.html   200\n"
  },
  {
    "path": "client/web/pinochle/config/optional-features.json",
    "content": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": false,\n  \"template-only-glimmer-components\": true\n}\n"
  },
  {
    "path": "client/web/pinochle/config/targets.js",
    "content": "'use strict';\n\n// prettier-ignore\nconst browsers = [\n  '> 2%',\n  'not IE 11',\n  'not dead',\n];\n\nmodule.exports = {\n  browsers,\n};\n"
  },
  {
    "path": "client/web/pinochle/ember-cli-build.js",
    "content": "'use strict';\n\nconst mergeTrees = require('broccoli-merge-trees');\nconst EmberApp = require('ember-cli/lib/broccoli/ember-app');\n\nconst { logWithAttention } = require('@emberclear/config/utils/log');\nconst {\n  applyEnvironmentVariables,\n  configureBabel,\n} = require('@emberclear/config/utils/ember-build');\n\nconst { EMBROIDER } = process.env;\n\nmodule.exports = function (defaults) {\n  let appOptions = {};\n\n  configureBabel(appOptions);\n  applyEnvironmentVariables(appOptions);\n\n  let app = new EmberApp(defaults, appOptions);\n\n  if (EMBROIDER) {\n    logWithAttention('E M B R O I D E R');\n\n    const { compatBuild } = require('@embroider/compat');\n    const { Webpack } = require('@embroider/webpack');\n\n    return compatBuild(app, Webpack, {\n      extraPublicTrees: additionalTrees,\n      // staticAddonTestSupportTrees: true,\n      // staticAddonTrees: true,\n      // staticHelpers: true,\n      // staticComponents: true,\n      // splitAtRoutes: true,\n      // skipBabel: [],\n    });\n  }\n\n  // Old-style broccoli-build\n  return mergeTrees([app.toTree()]);\n};\n"
  },
  {
    "path": "client/web/pinochle/package.json",
    "content": "{\n  \"name\": \"pinochle\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"description\": \"Small description for pinochle goes here\",\n  \"repository\": \"\",\n  \"license\": \"MIT\",\n  \"author\": \"\",\n  \"directories\": {\n    \"doc\": \"doc\",\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"build\": \"ember build --environment=production\",\n    \"build:production\": \"yarn build\",\n    \"lint\": \"npm-run-all --aggregate-output --continue-on-error --parallel lint:*\",\n    \"lint:hbs\": \"ember-template-lint .\",\n    \"lint:js\": \"eslint .\",\n    \"start\": \"ember serve\",\n    \"test\": \"npm-run-all lint:* test:*\",\n    \"test:ember\": \"ember test\"\n  },\n  \"devDependencies\": {\n    \"@ember/optional-features\": \"2.0.0\",\n    \"@ember/test-waiters\": \"2.4.4\",\n    \"@emberclear/config\": \"*\",\n    \"@emberclear/test-helpers\": \"*\",\n    \"@embroider/compat\": \"0.40.0\",\n    \"@embroider/core\": \"0.40.0\",\n    \"@embroider/webpack\": \"0.40.0\",\n    \"@glimmer/component\": \"1.0.4\",\n    \"@glimmer/tracking\": \"1.0.4\",\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@types/ember\": \"3.16.5\",\n    \"@types/ember-data\": \"3.16.14\",\n    \"@types/ember-data__model\": \"3.16.2\",\n    \"@types/ember-data__store\": \"3.16.1\",\n    \"@types/ember-qunit\": \"3.4.13\",\n    \"@types/ember-resolver\": \"5.0.10\",\n    \"@types/ember-test-helpers\": \"1.0.9\",\n    \"@types/ember-testing-helpers\": \"0.0.4\",\n    \"@types/ember__test-helpers\": \"2.0.0\",\n    \"@types/qunit\": \"2.11.1\",\n    \"@types/rsvp\": \"4.0.3\",\n    \"@types/uuid\": \"8.3.1\",\n    \"broccoli-asset-rev\": \"3.0.0\",\n    \"ember-auto-import\": \"1.11.3\",\n    \"ember-cli\": \"3.26.1\",\n    \"ember-cli-app-version\": \"5.0.0\",\n    \"ember-cli-babel\": \"7.26.6\",\n    \"ember-cli-dependency-checker\": \"3.2.0\",\n    \"ember-cli-htmlbars\": \"5.7.1\",\n    \"ember-cli-inject-live-reload\": \"2.1.0\",\n    \"ember-cli-sri\": \"2.1.1\",\n    \"ember-cli-terser\": \"4.0.2\",\n    \"ember-data\": \"3.27.1\",\n    \"ember-export-application-global\": \"2.0.1\",\n    \"ember-fetch\": \"8.0.5\",\n    \"ember-load-initializers\": \"2.1.2\",\n    \"ember-maybe-import-regenerator\": \"0.1.6\",\n    \"ember-qunit\": \"4.6.0\",\n    \"ember-resolver\": \"8.0.2\",\n    \"ember-source\": \"3.26.1\",\n    \"ember-template-lint\": \"3.5.0\",\n    \"fractal-page-object\": \"0.1.0\",\n    \"husky\": \"4.3.8\",\n    \"loader.js\": \"4.7.0\",\n    \"npm-run-all\": \"4.1.5\",\n    \"qunit-console-grouper\": \"0.3.0\",\n    \"qunit-dom\": \"1.6.0\",\n    \"stylelint\": \"13.13.1\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"ember\": {\n    \"edition\": \"octane\"\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  },\n  \"dependencies\": {\n    \"@emberclear/crypto\": \"*\",\n    \"@emberclear/encoding\": \"*\",\n    \"@emberclear/local-account\": \"*\",\n    \"@emberclear/networking\": \"*\",\n    \"ember-cached-decorator-polyfill\": \"0.1.3\",\n    \"ember-concurrency\": \"1.3.0\",\n    \"ember-concurrency-async\": \"0.3.2\",\n    \"ember-concurrency-decorators\": \"2.0.3\",\n    \"ember-concurrency-test-waiter\": \"0.4.0\",\n    \"ember-concurrency-ts\": \"0.2.2\",\n    \"ember-could-get-used-to-this\": \"1.0.1\",\n    \"ember-focus-trap\": \"0.7.0\",\n    \"ember-modifier\": \"2.1.2\",\n    \"ember-tracked-local-storage\": \"*\",\n    \"focus-visible\": \"5.2.0\",\n    \"tracked-built-ins\": \"1.0.2\",\n    \"uuid\": \"8.3.2\",\n    \"xstate\": \"4.20.2\"\n  },\n  \"typesVersions\": {\n    \"*\": {\n      \"config/environment\": [\n        \"app/config/environment\"\n      ],\n      \"*\": [\n        \"declarations/*\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "client/web/pinochle/public/robots.txt",
    "content": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "client/web/pinochle/testem.js",
    "content": "'use strict';\n\nmodule.exports = require('@emberclear/config/testem');\n"
  },
  {
    "path": "client/web/pinochle/tests/-pages/join.ts",
    "content": "import { assert } from '@ember/debug';\nimport { click, fillIn, visit, waitUntil } from '@ember/test-helpers';\nimport QUnit from 'qunit';\n\nimport { PageObject, selector } from 'fractal-page-object';\n\nclass NameEntryPage extends PageObject {\n  typeName(name: string) {\n    assert(`Field not found, cannot fill with text`, this._input.element);\n\n    return fillIn(this._input.element, name);\n  }\n\n  submit() {\n    assert(`Cannot click button that doesn't exist`, this._submit.element);\n\n    return click(this._submit.element);\n  }\n\n  /*******************************************\n   * @private\n   *******************************************/\n  _input = selector('[data-test-input]');\n  _submit = selector('[data-test-submit]');\n}\n\n// https://github.com/bendemboski/fractal-page-object#in-ember\nexport class JoinPage extends PageObject {\n  connecting = selector('[data-test-connecting]');\n  joining = selector('[data-test-joining]');\n  waiting = selector('[data-test-waiting]');\n  gameNotFound = selector('[data-test-404]');\n  starting = selector('[data-test-starting]');\n  unknown = selector('[data-test-unknown]');\n  waitingForPlayers = selector('[data-test-waiting-for-players]');\n\n  async waitFor(url: string) {\n    await waitUntil(() => currentURL() === url);\n\n    QUnit.assert.equal(currentURL(), url, `Visited ${url}`);\n  }\n\n  async joinGame(gameId: string, name?: string) {\n    await visit(`/join/${gameId}`);\n\n    QUnit.assert.equal(currentURL(), `/join/${gameId}`, `arrives at /join/${gameId}`);\n\n    await waitUntil(() => {\n      return this.nameEntry._input.element;\n    });\n\n    if (name) {\n      this.submit(name);\n    }\n  }\n\n  async submit(name: string) {\n    await this.typeName(name);\n    await this.submitName();\n  }\n\n  async rejoin(gameId: string) {\n    await visit(`/join/${gameId}`);\n\n    // QUnit.assert.equal(currentURL(), `/join/${gameId}`, `arrives at /join/${gameId}`);\n\n    await waitUntil(() => {\n      return currentURL() === `/game/${gameId}`;\n    });\n\n    QUnit.assert.equal(\n      currentURL(),\n      `/game/${gameId}`,\n      `Is redirected to the game at /game/${gameId}`\n    );\n  }\n\n  typeName(name: string) {\n    return this.nameEntry.typeName(name);\n  }\n  submitName() {\n    return this.nameEntry.submit();\n  }\n\n  /*******************************************\n   * @private~ish\n   *******************************************/\n  nameEntry = selector('[data-test-name-entry]', NameEntryPage);\n}\n"
  },
  {
    "path": "client/web/pinochle/tests/acceptance/game-test.ts",
    "content": "import { currentURL, visit } from '@ember/test-helpers';\nimport { module, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { newCrypto, setupWorkers } from '@emberclear/crypto/test-support';\nimport { setupSocketServer } from '@emberclear/networking/test-support';\n\nmodule('Acceptance | game', function (hooks) {\n  setupApplicationTest(hooks);\n  setupSocketServer(hooks);\n  setupWorkers(hooks);\n\n  module('Has not previously joined a game', function () {\n    module('there is no game', function () {\n      test('visiting /game', async function (assert) {\n        let host = await newCrypto();\n\n        await visit(`/game/${host.hex.publicKey}`);\n\n        assert.equal(currentURL(), `/join/${host.hex.publicKey}`);\n      });\n    });\n\n    module('the game exists', function () {});\n  });\n\n  module('Has a pre-existing game', function () {});\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/acceptance/join-test.ts",
    "content": "import { currentURL, settled, visit, waitUntil } from '@ember/test-helpers';\nimport { module, skip, test } from 'qunit';\nimport { setupApplicationTest } from 'ember-qunit';\n\nimport { timeout } from 'ember-concurrency';\n\nimport { JoinPage } from 'pinochle/tests/-pages/join';\nimport {\n  addPlayerToHost,\n  clearGuests,\n  clearHosts,\n  setupGameHost,\n  setupPlayerTest,\n  stopConnectivityChecking,\n} from 'pinochle/tests/helpers';\n\nimport { newCrypto } from '@emberclear/crypto/test-support';\n\nimport type { GameHost } from 'pinochle/game/networking/host';\n\nmodule('Acceptance | join', function (hooks) {\n  setupApplicationTest(hooks);\n  setupPlayerTest(hooks);\n\n  let page = new JoinPage();\n\n  module('Has not previously joined a game', function () {\n    module('there is no game', function () {\n      skip('visiting /join', async function (assert) {\n        assert.expect(2);\n\n        let host = await newCrypto();\n\n        await visit(`/join/${host.hex.publicKey}`);\n\n        assert.equal(currentURL(), `/join/${host.hex.publicKey}`);\n\n        await waitUntil(() => page.gameNotFound.element);\n\n        assert.dom(page.gameNotFound.element).exists();\n      });\n    });\n\n    module('the game exists', function (hooks) {\n      let host: GameHost;\n\n      setupGameHost(hooks, (gameHost) => (host = gameHost));\n\n      hooks.beforeEach(async function (assert) {\n        await page.joinGame(host.hexId);\n\n        assert.dom(page.nameEntry.element).exists();\n      });\n\n      test('can join the game', async function (assert) {\n        assert.expect(2);\n\n        await page.submit('Test Player');\n        await waitUntil(() => page.waiting.element);\n      });\n\n      module('the game starts with the current player', function (hooks) {\n        hooks.beforeEach(async function () {\n          await addPlayerToHost(host, 'Player 1');\n          await addPlayerToHost(host, 'Player 2');\n\n          await page.submit('Test Player');\n          await waitUntil(() => page.waiting.element);\n\n          host.startGame();\n\n          await page.waitFor(`/game/${host.hexId}`);\n          await settled();\n        });\n\n        test('a game can be played', async function (assert) {\n          assert.equal(currentURL(), `/game/${host.hexId}`);\n        });\n      });\n\n      module('The game becomes full while entering info', function (hooks) {\n        hooks.beforeEach(async function () {\n          await addPlayerToHost(host, 'Player 1');\n          await addPlayerToHost(host, 'Player 2');\n          await addPlayerToHost(host, 'Player 3');\n          await addPlayerToHost(host, 'Player 4');\n        });\n\n        test('is not allowed in the game', async function () {\n          await page.submit('Player 5');\n          await page.waitFor('/game-full');\n        });\n      });\n\n      module('The game has already started', function (hooks) {\n        hooks.beforeEach(async function () {\n          await addPlayerToHost(host, 'Player 1');\n          await addPlayerToHost(host, 'Player 2');\n          await addPlayerToHost(host, 'Player 3');\n\n          host.startGame();\n          await settled();\n        });\n\n        test('is not allowed in the game', async function () {\n          await page.submit('Player 5');\n          await page.waitFor('/not-recognized');\n        });\n      });\n    });\n  });\n\n  module('Has a pre-existing game', function (hooks) {\n    let host: GameHost;\n    let hostId: string;\n\n    setupGameHost(hooks, (gameHost) => (host = gameHost));\n\n    hooks.beforeEach(async function (assert) {\n      hostId = host.hexId;\n\n      await addPlayerToHost(host, 'Player 1');\n      await addPlayerToHost(host, 'Player 2');\n      assert.equal(host.players.length, 2, 'host has two players');\n\n      await page.joinGame(hostId, 'Test Player');\n      await waitUntil(() => page.waiting.element);\n\n      host.startGame();\n\n      await page.waitFor(`/game/${hostId}`);\n      assert.equal(host.players.length, 3, 'host has three players');\n\n      stopConnectivityChecking(hostId);\n\n      await settled();\n      await visit('/');\n\n      clearGuests();\n\n      await settled();\n    });\n\n    module('the host is online', function () {\n      test('the player re-joins and the game is loaded', async function (assert) {\n        await page.rejoin(hostId);\n        assert.equal(currentURL(), `/game/${hostId}`, 'has rejoined');\n\n        stopConnectivityChecking(hostId);\n\n        await settled();\n        assert.equal(host.players.length, 3, 'host has three players');\n      });\n    });\n\n    module('the host is offline', function (hooks) {\n      hooks.beforeEach(async function () {\n        clearHosts();\n      });\n\n      test('the game is not loaded', async function () {\n        await page.rejoin(hostId);\n        await timeout(3000);\n        await waitUntil(() => page.waitingForPlayers.element);\n        // await page.waitFor('/not-recognized');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/helpers/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/pinochle/tests/helpers/index.ts",
    "content": "import { assert } from '@ember/debug';\nimport { destroy } from '@ember/destroyable';\n\nimport { setupWorkers } from '@emberclear/crypto/test-support';\nimport { setupSocketServer } from '@emberclear/networking/test-support';\nimport { getService } from '@emberclear/test-helpers/test-support';\n\nimport type { GameGuest } from 'pinochle/game/networking/guest';\nimport type { GameHost } from 'pinochle/game/networking/host';\n\nexport function setupGameHost(hooks: NestedHooks, onDone: (host: GameHost) => void) {\n  let host: GameHost;\n\n  hooks.beforeEach(async function () {\n    let gameManager = getService('game-manager');\n\n    host = await gameManager.createHost();\n    host.shouldCheckConnectivity = false;\n\n    onDone(host);\n\n    assert(`Only one host is allowed at a time`, gameManager.isHosting.size <= 1);\n  });\n\n  hooks.afterEach(function () {\n    host.disconnect();\n  });\n}\n\nexport async function createHost() {\n  let gameManager = getService('game-manager');\n\n  let host = await gameManager.createHost();\n\n  host.shouldCheckConnectivity = false;\n  host.onlineChecker.cancelAll();\n\n  assert(`Only one host is allowed at a time`, gameManager.isHosting.size <= 1);\n\n  return host;\n}\n\nexport function setupPlayer(hooks: NestedHooks, host: GameHost, name: string) {\n  let player: GameGuest;\n\n  hooks.beforeEach(async function () {\n    player = await addPlayerToHost(host, name);\n  });\n\n  hooks.afterEach(function () {\n    player.disconnect();\n  });\n}\n\nexport async function addPlayerToHost(host: GameHost, name?: string) {\n  let gameManager = getService('game-manager');\n\n  let playerGame = await gameManager.connectToHost(host.hexId);\n\n  await playerGame.checkHost();\n  await playerGame.joinHost(name || 'Test Player');\n\n  return playerGame;\n}\n\nexport function clearGuests() {\n  let gameManager = getService('game-manager');\n\n  for (let [id, game] of gameManager.isGuestOf.entries()) {\n    destroy(game);\n\n    gameManager.isGuestOf.delete(id);\n  }\n}\n\nexport function clearHosts() {\n  let gameManager = getService('game-manager');\n\n  for (let [id, game] of gameManager.isHosting.entries()) {\n    game.disconnect();\n\n    gameManager.isHosting.delete(id);\n  }\n}\n\nexport function stopConnectivityChecking(hexId: string) {\n  let gameManager = getService('game-manager');\n\n  let host = gameManager.isHosting.get(hexId);\n\n  assert(`Host does not exist`, host);\n\n  host.shouldCheckConnectivity = false;\n  host.onlineChecker.cancelAll();\n}\n\nexport async function setupPlayerTest(hooks: NestedHooks) {\n  setupSocketServer(hooks);\n  setupWorkers(hooks);\n\n  hooks.afterEach(function () {\n    let gameManager = getService('game-manager');\n\n    for (let [, game] of gameManager.isGuestOf.entries()) {\n      game.disconnect();\n    }\n\n    for (let [, game] of gameManager.isHosting.entries()) {\n      game.disconnect();\n    }\n  });\n}\n"
  },
  {
    "path": "client/web/pinochle/tests/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>Pinochle Tests</title>\n    <meta name=\"description\" content=\"\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    {{content-for \"head\"}}\n    {{content-for \"test-head\"}}\n\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/vendor.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/pinochle.css\">\n    <link rel=\"stylesheet\" href=\"{{rootURL}}assets/test-support.css\">\n\n    {{content-for \"head-footer\"}}\n    {{content-for \"test-head-footer\"}}\n\n    <style>\n      #qunit input,\n      #qunit input[type='text'] {\n        background: white;\n        height: auto;\n        padding: 0.5rem 1rem;\n        box-shadow: inset 0 1px 2px 0px rgb(0 0 0 / 50%);\n      }\n      #qunit-testrunner-toolbar {\n        position: sticky;\n        top: 0px;\n      }\n      #qunit-userAgent {\n        position: sticky;\n        top: 77px;\n      }\n      #qunit-testresult {\n        position: sticky;\n        top: 105px;\n        box-shadow: 0 2px 20px rgb(0 0 0 / 40%);\n      }\n      #ember-testing-container {\n        /* width: 100vw; */\n        width: auto;\n        height: 100vh;\n        min-width: 640px;\n        min-height: 320px;\n        border: 3px dashed rgb(100, 100, 0);\n        /* transform: scaleX(75%) scaleY(75%); */\n        overflow: hidden;\n      }\n      #ember-testing {\n        width: unset;\n        height: unset;\n        transform: unset;\n        transform-origin: unset;\n      }\n    </style>\n  </head>\n  <body>\n    {{content-for \"body\"}}\n    {{content-for \"test-body\"}}\n\n    <script src=\"/testem.js\" integrity=\"\"></script>\n    <script src=\"{{rootURL}}assets/vendor.js\"></script>\n    <script src=\"{{rootURL}}assets/test-support.js\"></script>\n    <script src=\"{{rootURL}}assets/pinochle.js\"></script>\n    <script src=\"{{rootURL}}assets/tests.js\"></script>\n\n    {{content-for \"body-footer\"}}\n    {{content-for \"test-body-footer\"}}\n  </body>\n</html>\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/components/host-game-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | host-game', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<HostGame />`);\n\n    assert.dom().containsText('Please enter your name');\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/components/lazy-test.ts",
    "content": "import { render, settled } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nimport { timeout } from 'ember-concurrency';\n\nmodule('Integration | Component | lazy', function (hooks) {\n  setupRenderingTest(hooks);\n\n  module('args', function () {\n    test('@wait', async function (assert) {\n      assert.expect(2);\n\n      let text = 'wait for 10ms';\n\n      this.setProperties({ text });\n\n      render(hbs`\n      <Lazy @wait={{10}}>{{this.text}}</Lazy>\n    `);\n\n      assert.dom().doesNotContainText(text);\n\n      await timeout(15);\n\n      assert.dom().hasText(text);\n    });\n\n    test('@wait & @when', async function (assert) {\n      let text = 'wait for 10ms';\n      let cond = false;\n\n      this.setProperties({ text, cond });\n\n      render(hbs`\n        <Lazy @wait={{10}} @when={{this.cond}}>{{this.text}}</Lazy>\n      `);\n\n      assert.dom().doesNotContainText(text);\n\n      await timeout(15);\n\n      assert.dom().doesNotContainText(text);\n\n      this.setProperties({ cond: true });\n\n      await settled();\n\n      assert.dom().hasText(text);\n    });\n\n    test('@when', async function (assert) {\n      let text = 'wait for 10ms';\n      let cond = false;\n\n      this.setProperties({ text, cond });\n\n      render(hbs`\n        <Lazy @when={{this.cond}}>{{this.text}}</Lazy>\n      `);\n\n      assert.dom().doesNotContainText(text);\n\n      await timeout(15);\n\n      assert.dom().doesNotContainText(text);\n\n      this.setProperties({ cond: true });\n\n      await settled();\n\n      assert.dom().hasText(text);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/components/playing-card-test.ts",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | playing-card', function (hooks) {\n  setupRenderingTest(hooks);\n\n  test('it renders', async function (assert) {\n    await render(hbs`<PlayingCard @suit='hearts' @value={{10}} />`);\n\n    assert.dom().containsText('10 of hearts');\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/components/share-link-test.js",
    "content": "import { click, render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, skip } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Component | share-link', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // How do you test the clipboard?\n  // Do I need to stub this out during tests?\n  // Need ember-browser-services\n  skip('it renders', async function (assert) {\n    let text = 'some text';\n    this.setProperties({ text });\n\n    await render(hbs`<ShareLink @url={{this.text}} />`);\n\n    await click('button');\n\n    assert.equal(navigator.clipboard.readText(), text);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/modifiers/fit-text-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Modifier | fit-text', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    await render(hbs`<div {{fit-text}}></div>`);\n\n    assert.ok(true);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/modifiers/resize-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Modifier | resize', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    await render(hbs`<div {{resize}}></div>`);\n\n    assert.ok(true);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/integration/modifiers/stack-test.js",
    "content": "import { render } from '@ember/test-helpers';\nimport { hbs } from 'ember-cli-htmlbars';\nimport { module, test } from 'qunit';\nimport { setupRenderingTest } from 'ember-qunit';\n\nmodule('Integration | Modifier | stack', function (hooks) {\n  setupRenderingTest(hooks);\n\n  // Replace this with your real tests.\n  test('it renders', async function (assert) {\n    await render(hbs`<div {{stack}}></div>`);\n\n    assert.ok(true);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/test-helper.ts",
    "content": "/* eslint-disable ember/new-module-imports */\n// Install Types and assertion extensions\nimport 'qunit-dom';\n\nimport Ember from 'ember';\n// import 'qunit-assertions-extra';\nimport { currentURL, getSettledState, setApplication } from '@ember/test-helpers';\nimport QUnit from 'qunit';\nimport { start } from 'ember-qunit';\n\nimport Application from 'pinochle/app';\nimport config from 'pinochle/config/environment';\n\nconst seed = Math.random().toString(36).substr(2, 5);\n\n// Slow runtime is worth the backburner unwinding\n(Ember.run as TODO).backburner.DEBUG = true;\n\nQUnit.config.seed = seed;\nQUnit.config.reorder = false;\n\nQUnit.begin(async () => {\n  console.info(`Using seed for Qunit: ${seed}`);\n});\n\nQUnit.testStart(() => {\n  localStorage.clear();\n});\n\nQUnit.testDone(() => {\n  localStorage.clear();\n});\n\n// easy access debugging tools during a paused test\nObject.assign(window, { getSettledState, currentURL });\n\nsetApplication(Application.create(config.APP));\n\nstart({\n  setupTestIsolationValidation: true,\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.compiler-options.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"declarationDir\": \"../declarations/tests\",\n    \"paths\": {\n      \"pinochle/tests/*\": [\"*\"],\n      \"pinochle/*\": [\"../declarations/*\"],\n      \"@ember/destroyable\": [\"../../node_modules/ember-destroyable-polyfill\"],\n\n      \"*\": [\"../types/*\"]\n    }\n  },\n  \"include\": [\".\", \"../types\"],\n  \"references\": [\n    { \"path\": \"../app\" },\n    { \"path\": \"../../addons/test-helpers\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/pinochle/tests/type-support.ts",
    "content": "import 'pinochle/services/game-manager';\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/deck-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { newDeck, splitDeck } from 'pinochle/game/deck';\n\nimport type { Card } from 'pinochle/game/card';\n\nmodule('Unit | Game | Deck', function () {\n  module('splitDeck', function (hooks) {\n    let deck: Card[];\n\n    hooks.beforeEach(function () {\n      deck = newDeck();\n    });\n\n    test('3 players have 15 cards each and a blind', function (assert) {\n      let { hands, remaining } = splitDeck(deck, 3);\n\n      assert.equal(hands.length, 3, 'has 3 hands');\n      assert.equal(remaining.length, 3, 'has a blind of 3 cards');\n      assert.equal(hands[0].length, 15);\n      assert.equal(hands[1].length, 15);\n      assert.equal(hands[2].length, 15);\n    });\n\n    test('4 players have 12 cards each', function (assert) {\n      let { hands, remaining } = splitDeck(deck, 4);\n\n      assert.equal(hands.length, 4, 'has 3 hands');\n      assert.equal(remaining.length, 0, 'has no blind');\n      assert.equal(hands[0].length, 12);\n      assert.equal(hands[1].length, 12);\n      assert.equal(hands[2].length, 12);\n      assert.equal(hands[3].length, 12);\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/meld-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { Card } from 'pinochle/game/card';\nimport { Meld } from 'pinochle/game/meld';\n\nmodule('Unit | Game | Meld', function () {\n  module('marriage', function () {\n    test('plain', function (assert) {\n      let meld = new Meld([new Card('clubs', 'queen'), new Card('clubs', 'king')]);\n\n      assert.equal(meld.score, 20);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 20,\n        matches: {\n          clubs: { trump: false },\n        },\n      });\n    });\n\n    test('marriage (of trump)', function (assert) {\n      let meld = new Meld([new Card('clubs', 'queen'), new Card('clubs', 'king')], 'clubs');\n\n      assert.equal(meld.score, 40);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 40,\n        matches: {\n          clubs: { trump: true },\n        },\n      });\n    });\n\n    test('double marriage', function (assert) {\n      let meld = new Meld([\n        new Card('clubs', 'queen'),\n        new Card('clubs', 'king'),\n        new Card('clubs', 'queen'),\n        new Card('clubs', 'king'),\n      ]);\n\n      assert.equal(meld.score, 40);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 40,\n        matches: {\n          clubs: { trump: false, double: true },\n        },\n      });\n    });\n\n    test('double marriage (of trump)', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('clubs', 'queen'),\n          new Card('clubs', 'king'),\n          new Card('clubs', 'queen'),\n          new Card('clubs', 'king'),\n        ],\n        'clubs'\n      );\n\n      assert.equal(meld.score, 80);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 80,\n        matches: {\n          clubs: { trump: true, double: true },\n        },\n      });\n    });\n\n    test('multiple marriages', function (assert) {\n      let meld = new Meld([\n        new Card('clubs', 'queen'),\n        new Card('clubs', 'king'),\n        new Card('hearts', 'queen'),\n        new Card('hearts', 'king'),\n        new Card('spades', 'queen'),\n        new Card('spades', 'king'),\n      ]);\n\n      assert.equal(meld.score, 60);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 60,\n        matches: {\n          clubs: { trump: false },\n          hearts: { trump: false },\n          spades: { trump: false },\n        },\n      });\n    });\n\n    test('multiple marriages (with one of trump)', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('clubs', 'queen'),\n          new Card('clubs', 'king'),\n          new Card('hearts', 'queen'),\n          new Card('hearts', 'king'),\n          new Card('spades', 'queen'),\n          new Card('spades', 'king'),\n        ],\n        'hearts'\n      );\n\n      assert.equal(meld.score, 80);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 80,\n        matches: {\n          clubs: { trump: false },\n          hearts: { trump: true },\n          spades: { trump: false },\n        },\n      });\n    });\n\n    test('multiple marriages (with double of trump)', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('clubs', 'queen'),\n          new Card('clubs', 'king'),\n          new Card('hearts', 'queen'),\n          new Card('hearts', 'king'),\n          new Card('spades', 'queen'),\n          new Card('spades', 'king'),\n          new Card('spades', 'queen'),\n          new Card('spades', 'king'),\n        ],\n        'spades'\n      );\n\n      assert.equal(meld.score, 120);\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 120,\n        matches: {\n          clubs: { trump: false },\n          hearts: { trump: false },\n          spades: { trump: true, double: true },\n        },\n      });\n    });\n  });\n\n  module('pinochle', function () {\n    test('single', function (assert) {\n      let meld = new Meld([new Card('diamonds', 'jack'), new Card('spades', 'queen')]);\n\n      assert.equal(meld.score, 30);\n      assert.ok(meld.matches.pinochle);\n      assert.deepEqual(meld.matches.pinochle, {\n        value: 30,\n        matches: {},\n      });\n    });\n\n    test('double', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('spades', 'queen'),\n        new Card('diamonds', 'jack'),\n        new Card('spades', 'queen'),\n      ]);\n\n      assert.equal(meld.score, 300);\n      assert.ok(meld.matches.pinochle);\n      assert.deepEqual(meld.matches.pinochle, {\n        value: 300,\n        matches: {},\n      });\n    });\n\n    test('not quite double', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('spades', 'queen'),\n        new Card('spades', 'queen'),\n      ]);\n\n      assert.equal(meld.score, 30);\n      assert.ok(meld.matches.pinochle);\n      assert.deepEqual(meld.matches.pinochle, {\n        value: 30,\n        matches: {},\n      });\n    });\n  });\n\n  module('nines', function () {\n    test('pre trump declaration', function (assert) {\n      let meld = new Meld([new Card('diamonds', 9)]);\n\n      assert.equal(meld.score, 0);\n      assert.ok(meld.matches.nineOfTrump);\n      assert.deepEqual(meld.matches.nineOfTrump, {\n        value: 0,\n        matches: {},\n      });\n    });\n\n    test('has nine of trump', function (assert) {\n      let meld = new Meld([new Card('diamonds', 9)], 'diamonds');\n\n      assert.equal(meld.score, 10);\n      assert.ok(meld.matches.nineOfTrump);\n      assert.deepEqual(meld.matches.nineOfTrump, {\n        value: 10,\n        matches: {},\n      });\n    });\n\n    test('has both nines of trump', function (assert) {\n      let meld = new Meld([new Card('diamonds', 9), new Card('diamonds', 9)], 'diamonds');\n\n      assert.equal(meld.score, 20);\n      assert.ok(meld.matches.nineOfTrump);\n      assert.deepEqual(meld.matches.nineOfTrump, {\n        value: 20,\n        matches: {},\n      });\n    });\n\n    test('has *all* the nines', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 9),\n          new Card('diamonds', 9),\n          new Card('spades', 9),\n          new Card('spades', 9),\n          new Card('clubs', 9),\n          new Card('clubs', 9),\n          new Card('hearts', 9),\n          new Card('hearts', 9),\n        ],\n        'diamonds'\n      );\n\n      assert.equal(meld.score, 20);\n      assert.ok(meld.matches.nineOfTrump);\n      assert.deepEqual(meld.matches.nineOfTrump, {\n        value: 20,\n        matches: {},\n      });\n    });\n  });\n\n  module('aces', function () {\n    test('not enough', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n        new Card('hearts', 9),\n      ]);\n\n      assert.equal(meld.score, 0);\n      assert.ok(meld.matches.aces);\n      assert.deepEqual(meld.matches.aces, {\n        value: 0,\n        matches: {},\n      });\n    });\n\n    test('100 aces', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n        new Card('hearts', 'ace'),\n      ]);\n\n      assert.equal(meld.score, 100);\n      assert.ok(meld.matches.aces);\n      assert.deepEqual(meld.matches.aces, {\n        value: 100,\n        matches: {},\n      });\n    });\n\n    test('not 1000 aces', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n        new Card('hearts', 'ace'),\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n      ]);\n\n      assert.equal(meld.score, 100);\n      assert.ok(meld.matches.aces);\n      assert.deepEqual(meld.matches.aces, {\n        value: 100,\n        matches: {},\n      });\n    });\n\n    test('1000 aces', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n        new Card('hearts', 'ace'),\n        new Card('diamonds', 'ace'),\n        new Card('clubs', 'ace'),\n        new Card('spades', 'ace'),\n        new Card('hearts', 'ace'),\n      ]);\n\n      assert.equal(meld.score, 1000);\n      assert.ok(meld.matches.aces);\n      assert.deepEqual(meld.matches.aces, {\n        value: 1000,\n        matches: {},\n      });\n    });\n  });\n\n  module('kings', function () {\n    test('not enough', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n        new Card('hearts', 9),\n      ]);\n\n      assert.equal(meld.score, 0);\n      assert.ok(meld.matches.kings);\n      assert.deepEqual(meld.matches.kings, {\n        value: 0,\n        matches: {},\n      });\n    });\n\n    test('80 kings', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n        new Card('hearts', 'king'),\n      ]);\n\n      assert.equal(meld.score, 80);\n      assert.ok(meld.matches.kings);\n      assert.deepEqual(meld.matches.kings, {\n        value: 80,\n        matches: {},\n      });\n    });\n\n    test('not quite double', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n        new Card('hearts', 'king'),\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n      ]);\n\n      assert.equal(meld.score, 80);\n      assert.ok(meld.matches.kings);\n      assert.deepEqual(meld.matches.kings, {\n        value: 80,\n        matches: {},\n      });\n    });\n\n    test('double 80 kings', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n        new Card('hearts', 'king'),\n        new Card('diamonds', 'king'),\n        new Card('clubs', 'king'),\n        new Card('spades', 'king'),\n        new Card('hearts', 'king'),\n      ]);\n\n      assert.equal(meld.score, 800);\n      assert.ok(meld.matches.kings);\n      assert.deepEqual(meld.matches.kings, {\n        value: 800,\n        matches: {},\n      });\n    });\n  });\n\n  module('queens', function () {\n    test('not enough', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n        new Card('hearts', 9),\n      ]);\n\n      assert.equal(meld.score, 0);\n      assert.ok(meld.matches.queens);\n      assert.deepEqual(meld.matches.queens, {\n        value: 0,\n        matches: {},\n      });\n    });\n\n    test('60 queens', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n        new Card('hearts', 'queen'),\n      ]);\n\n      assert.equal(meld.score, 60);\n      assert.ok(meld.matches.queens);\n      assert.deepEqual(meld.matches.queens, {\n        value: 60,\n        matches: {},\n      });\n    });\n\n    test('not quite double', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n        new Card('hearts', 'queen'),\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n      ]);\n\n      assert.equal(meld.score, 60);\n      assert.ok(meld.matches.queens);\n      assert.deepEqual(meld.matches.queens, {\n        value: 60,\n        matches: {},\n      });\n    });\n\n    test('double 60 queens', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n        new Card('hearts', 'queen'),\n        new Card('diamonds', 'queen'),\n        new Card('clubs', 'queen'),\n        new Card('spades', 'queen'),\n        new Card('hearts', 'queen'),\n      ]);\n\n      assert.equal(meld.score, 600);\n      assert.ok(meld.matches.queens);\n      assert.deepEqual(meld.matches.queens, {\n        value: 600,\n        matches: {},\n      });\n    });\n  });\n\n  module('jacks', function () {\n    test('not enough', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n        new Card('hearts', 9),\n      ]);\n\n      assert.equal(meld.score, 0);\n      assert.ok(meld.matches.jacks);\n      assert.deepEqual(meld.matches.jacks, {\n        value: 0,\n        matches: {},\n      });\n    });\n\n    test('40 jacks', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n        new Card('hearts', 'jack'),\n      ]);\n\n      assert.equal(meld.score, 40);\n      assert.ok(meld.matches.jacks);\n      assert.deepEqual(meld.matches.jacks, {\n        value: 40,\n        matches: {},\n      });\n    });\n\n    test('not quite double', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n        new Card('hearts', 'jack'),\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n      ]);\n\n      assert.equal(meld.score, 40);\n      assert.ok(meld.matches.jacks);\n      assert.deepEqual(meld.matches.jacks, {\n        value: 40,\n        matches: {},\n      });\n    });\n\n    test('double 40 jacks', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n        new Card('hearts', 'jack'),\n        new Card('diamonds', 'jack'),\n        new Card('clubs', 'jack'),\n        new Card('spades', 'jack'),\n        new Card('hearts', 'jack'),\n      ]);\n\n      assert.equal(meld.score, 400);\n      assert.ok(meld.matches.jacks);\n      assert.deepEqual(meld.matches.jacks, {\n        value: 400,\n        matches: {},\n      });\n    });\n  });\n\n  module('run', function () {\n    test('run is not in trump', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n          new Card('diamonds', 'ace'),\n        ],\n        'hearts'\n      );\n\n      assert.equal(meld.score, 20);\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 0,\n        matches: {},\n      });\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 20,\n        matches: {\n          diamonds: {\n            trump: false,\n          },\n        },\n      });\n    });\n\n    test('trump not declared', function (assert) {\n      let meld = new Meld([\n        new Card('diamonds', 'jack'),\n        new Card('diamonds', 'queen'),\n        new Card('diamonds', 'king'),\n        new Card('diamonds', 10),\n        new Card('diamonds', 'ace'),\n      ]);\n\n      assert.equal(meld.score, 20);\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 0,\n        matches: {},\n      });\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 20,\n        matches: {\n          diamonds: {\n            trump: false,\n          },\n        },\n      });\n    });\n\n    test('not enough', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 9),\n          new Card('diamonds', 'ace'),\n        ],\n        'diamonds'\n      );\n\n      assert.equal(meld.score, 50);\n\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 0,\n        matches: {},\n      });\n\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 40,\n        matches: {\n          diamonds: {\n            trump: true,\n          },\n        },\n      });\n\n      assert.ok(meld.matches.nineOfTrump);\n      assert.deepEqual(meld.matches.nineOfTrump, {\n        value: 10,\n        matches: {},\n      });\n    });\n\n    test('single run', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n          new Card('diamonds', 'ace'),\n        ],\n        'diamonds'\n      );\n\n      assert.equal(meld.score, 150);\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 150,\n        matches: {},\n      });\n    });\n\n    test('not quite double', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n          new Card('diamonds', 'ace'),\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n        ],\n        'diamonds'\n      );\n\n      assert.equal(meld.score, 190);\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 150,\n        matches: {},\n      });\n      assert.ok(meld.matches.marriage);\n      assert.deepEqual(meld.matches.marriage, {\n        value: 40,\n        matches: {\n          diamonds: {\n            trump: true,\n            double: true,\n          },\n        },\n      });\n    });\n\n    test('double run', function (assert) {\n      let meld = new Meld(\n        [\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n          new Card('diamonds', 'ace'),\n          new Card('diamonds', 'jack'),\n          new Card('diamonds', 'queen'),\n          new Card('diamonds', 'king'),\n          new Card('diamonds', 10),\n          new Card('diamonds', 'ace'),\n        ],\n        'diamonds'\n      );\n\n      assert.equal(meld.score, 1500);\n      assert.ok(meld.matches.run);\n      assert.deepEqual(meld.matches.run, {\n        value: 1500,\n        matches: {},\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/state-test.ts",
    "content": "import Ember from 'ember';\nimport { module, skip, test } from 'qunit';\nimport { setupTest } from 'ember-qunit';\n\nimport { GameRound } from 'pinochle/game/networking/host/game-round';\nimport { Trick } from 'pinochle/game/trick';\nimport { availableMoves } from 'pinochle/game/utils/move-validation';\n\nimport { newCrypto } from '@emberclear/crypto/test-support';\n\nimport type { PlayerInfo } from 'pinochle/game/networking/host/types';\n\nfunction debugAssert(str: string, cond: unknown): asserts cond {\n  // eslint-disable-next-line ember/new-module-imports\n  return Ember.assert(str, cond);\n}\n\nmodule('Unit | Game | Host | GameRound', function (hooks) {\n  setupTest(hooks);\n\n  module('game starts fresh', function () {\n    module('with 3 players', function (hooks) {\n      let players: PlayerInfo[] = [];\n      let playersById: Record<string, PlayerInfo> = {};\n\n      hooks.beforeEach(async function () {\n        for (let i = 0; i < 3; i++) {\n          let info = await newCrypto();\n\n          let player = {\n            id: info.hex.publicKey,\n            name: `Player ${i}`,\n            publicKey: info.publicKey,\n            publicKeyAsHex: info.hex.publicKey,\n          };\n\n          players[i] = player;\n          playersById[player.id] = player;\n        }\n      });\n\n      skip('can play a game', async function (assert) {\n        let game = new GameRound(playersById);\n        let ctx = game.context;\n        let currentPlayer = game.currentPlayer;\n        let playerOrder = ctx.playerOrder;\n\n        assert.equal(game.info.playerOrder.length, players.length);\n        assert.ok(currentPlayer);\n        assert.equal(ctx.currentPlayer, playerOrder[0], `it is player 1's turn`);\n        assert.equal(ctx.hasBlind, true, 'there is a blind');\n        assert.equal(ctx.blind?.length, 3, 'blind has 3 cards');\n        assert.equal(ctx.playersById[players[0].id].hand?.length, 15);\n        assert.equal(ctx.playersById[players[1].id].hand?.length, 15);\n        assert.equal(ctx.playersById[players[2].id].hand?.length, 15);\n        assert.equal(game.interpreter.state.value, 'bidding', 'game starts in bidding phase');\n\n        game.bid({ bid: 15 });\n\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 1);\n        assert.deepEqual(Object.values(ctx.bids), [15]);\n        assert.equal(ctx.currentPlayer, playerOrder[1], `it is player 2's turn`);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 16 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 2);\n        assert.deepEqual(Object.values(ctx.bids), [15, 16]);\n        assert.equal(ctx.currentPlayer, playerOrder[2], `it is player 3's turn`);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 17 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 3);\n        assert.deepEqual(Object.values(ctx.bids), [15, 16, 17]);\n        assert.equal(ctx.currentPlayer, playerOrder[0], `it is player 1's turn`);\n        assert.equal(ctx.bids[ctx.currentPlayer], 15);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        // game.bid({ bid: 'passed' });\n        game.interpreter.send('PASS');\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 3);\n        assert.deepEqual(Object.values(ctx.bids), ['passed', 16, 17]);\n        assert.equal(ctx.currentPlayer, playerOrder[1], `it is player 2's turn`);\n        assert.equal(ctx.bids[ctx.currentPlayer], 16);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 18 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 3);\n        assert.deepEqual(Object.values(ctx.bids), ['passed', 18, 17]);\n        assert.equal(ctx.currentPlayer, playerOrder[2], `it is player 3's turn`);\n        assert.equal(ctx.bids[ctx.currentPlayer], 17);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        // game.bid({ bid: 'passed' });\n        game.interpreter.send('PASS');\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 3);\n        assert.deepEqual(Object.values(ctx.bids), ['passed', 18, 'passed']);\n        assert.equal(ctx.bids[ctx.currentPlayer], 18);\n        assert.equal(\n          ctx.currentPlayer,\n          playerOrder[1],\n          `it is player 2's turn, player 1 was skipped due to passing earlier in the bidding phase`\n        );\n        assert.deepEqual(game.interpreter.state.value, { 'won-bid': 'pending-acceptance' });\n\n        game.interpreter.send('ACCEPT');\n\n        assert.deepEqual(game.interpreter.state.value, { 'won-bid': 'accepted' });\n\n        game.interpreter.send({ type: 'DECLARE_TRUMP', trump: 'clubs' });\n\n        assert.deepEqual(game.interpreter.state.value, { 'won-bid': 'discard' });\n\n        game.interpreter.send({ type: 'DISCARD', cards: [] });\n\n        assert.deepEqual(game.interpreter.state.value, 'declare-meld');\n\n        // async, not dependent on turn order\n        game.interpreter.send({ type: 'SUBMIT_MELD', player: players[0].id });\n        game.interpreter.send({ type: 'SUBMIT_MELD', player: players[1].id });\n        game.interpreter.send({ type: 'SUBMIT_MELD', player: players[2].id });\n\n        // async, not dependent on turn order\n        game.interpreter.send({ type: 'READY', player: players[0].id });\n        game.interpreter.send({ type: 'READY', player: players[1].id });\n        game.interpreter.send({ type: 'READY', player: players[2].id });\n\n        // there are 15 rounds of trick taking in a 3 player game\n        let trump = game.context.trump;\n\n        debugAssert('expected trump to exist', trump);\n\n        for (let trick = 0; trick < 15; trick++) {\n          for (let player = 0; player < players.length; player++) {\n            let trickCards = game.context.trick;\n\n            debugAssert('expected trick to exist', trickCards);\n\n            let trick = Trick.from(trickCards);\n\n            let hand = game.context.playersById[game.context.currentPlayer].hand;\n            let validMoves = availableMoves(trick, hand, trump);\n            let card = validMoves[0];\n\n            game.interpreter.send({ type: 'PLAY_CARD', card });\n          }\n        }\n\n        assert.equal(game.interpreter.state.value, 'end-game');\n      });\n    });\n\n    module('with 4 players', function (hooks) {\n      let players: PlayerInfo[] = [];\n      let playersById: Record<string, PlayerInfo> = {};\n\n      hooks.beforeEach(async function () {\n        for (let i = 0; i < 4; i++) {\n          let info = await newCrypto();\n\n          let player = {\n            id: info.hex.publicKey,\n            name: `Player ${i}`,\n            publicKey: info.publicKey,\n            publicKeyAsHex: info.hex.publicKey,\n          };\n\n          players[i] = player;\n          playersById[player.id] = player;\n        }\n      });\n\n      test('can play a game', async function (assert) {\n        let game = new GameRound(playersById);\n        let ctx = game.context;\n        let currentPlayer = game.currentPlayer;\n        let playerOrder = ctx.playerOrder;\n\n        assert.equal(game.info.playerOrder.length, players.length);\n        assert.ok(currentPlayer);\n        assert.equal(ctx.currentPlayer, playerOrder[0], `it is player 1's turn`);\n        assert.false(ctx.hasBlind, 'no blind');\n        assert.equal(ctx.blind?.length, 0, 'no blind');\n        assert.equal(ctx.playersById[players[0].id].hand?.length, 12);\n        assert.equal(ctx.playersById[players[1].id].hand?.length, 12);\n        assert.equal(ctx.playersById[players[2].id].hand?.length, 12);\n        assert.equal(ctx.playersById[players[3].id].hand?.length, 12);\n        assert.equal(game.interpreter.state.value, 'bidding', 'game starts in bidding phase');\n\n        game.bid({ bid: 15 });\n\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 1);\n        assert.deepEqual(Object.values(ctx.bids), [15]);\n        assert.equal(ctx.currentPlayer, playerOrder[1], `it is player 2's turn`);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 16 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 2);\n        assert.deepEqual(Object.values(ctx.bids), [15, 16]);\n        assert.equal(ctx.currentPlayer, playerOrder[2], `it is player 3's turn`);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 17 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 3);\n        assert.deepEqual(Object.values(ctx.bids), [15, 16, 17]);\n        assert.equal(ctx.currentPlayer, playerOrder[3], `it is player 4's turn`);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        // game.bid({ bid: 'passed' });\n        game.interpreter.send('PASS');\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 4);\n        assert.deepEqual(Object.values(ctx.bids), [15, 16, 17, 'passed']);\n        assert.equal(ctx.currentPlayer, playerOrder[0], `it is player 1's turn`);\n        assert.equal(ctx.bids[ctx.currentPlayer], 15);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        game.bid({ bid: 18 });\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 4);\n        assert.deepEqual(Object.values(ctx.bids), [18, 16, 17, 'passed']);\n        assert.equal(ctx.currentPlayer, playerOrder[1], `it is player 2's turn`);\n        assert.equal(ctx.bids[ctx.currentPlayer], 16);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        // game.bid({ bid: 'passed' });\n        game.interpreter.send('PASS');\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 4);\n        assert.deepEqual(Object.values(ctx.bids), [18, 'passed', 17, 'passed']);\n        assert.equal(ctx.bids[ctx.currentPlayer], 17);\n        assert.equal(game.interpreter.state.value, 'bidding');\n\n        // game.bid({ bid: 'passed' });\n        game.interpreter.send('PASS');\n        ctx = game.context;\n\n        assert.equal(Object.keys(ctx.bids).length, 4);\n        assert.deepEqual(Object.values(ctx.bids), [18, 'passed', 'passed', 'passed']);\n        assert.equal(ctx.bids[ctx.currentPlayer], 18);\n        assert.deepEqual(game.interpreter.state.value, { 'won-bid': 'pending-acceptance' });\n        assert.equal(\n          ctx.currentPlayer,\n          playerOrder[0],\n          `it is player 1's turn, player 3 and 4 were skipped due to passing earlier in the bidding phase`\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/trick-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { Card } from 'pinochle/game/card';\nimport { Trick } from 'pinochle/game/trick';\n\nmodule('Unit | Game | Trick', function () {\n  module('three players', function () {\n    const NUM_PLAYERS = 3;\n\n    test('max cards may not exceed number of players', function (assert) {\n      let trick = new Trick(NUM_PLAYERS);\n\n      assert.equal(trick.points, 0);\n\n      trick.add(new Card('diamonds', 9));\n\n      assert.equal(trick.points, 0);\n\n      trick.add(new Card('diamonds', 10));\n\n      assert.equal(trick.points, 1);\n\n      trick.add(new Card('diamonds', 'ace'));\n\n      assert.equal(trick.points, 2);\n\n      assert.throws(() => {\n        trick.add(new Card('diamonds', 'jack'));\n      });\n    });\n  });\n\n  module('four players', function () {\n    const NUM_PLAYERS = 4;\n\n    test('max cards may not exceed number of players', function (assert) {\n      let trick = new Trick(NUM_PLAYERS);\n\n      assert.equal(trick.points, 0);\n\n      trick.add(new Card('diamonds', 9));\n\n      assert.equal(trick.points, 0);\n\n      trick.add(new Card('diamonds', 10));\n\n      assert.equal(trick.points, 1);\n\n      trick.add(new Card('diamonds', 'ace'));\n\n      assert.equal(trick.points, 2);\n\n      trick.add(new Card('diamonds', 'ace'));\n\n      assert.equal(trick.points, 3);\n\n      assert.throws(() => {\n        trick.add(new Card('diamonds', 'jack'));\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/utils/availableMoves-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { Card } from 'pinochle/game/card';\nimport { Trick } from 'pinochle/game/trick';\nimport { availableMoves } from 'pinochle/game/utils/move-validation';\n\nconst diamonds = {\n  9: new Card('diamonds', 9),\n  jack: new Card('diamonds', 'jack'),\n  queen: new Card('diamonds', 'queen'),\n  king: new Card('diamonds', 'king'),\n  10: new Card('diamonds', 10),\n  ace: new Card('diamonds', 'ace'),\n};\nconst clubs = {\n  9: new Card('clubs', 9),\n  jack: new Card('clubs', 'jack'),\n  queen: new Card('clubs', 'queen'),\n  king: new Card('clubs', 'king'),\n  10: new Card('clubs', 10),\n  ace: new Card('clubs', 'ace'),\n};\nconst spades = {\n  9: new Card('spades', 9),\n  jack: new Card('spades', 'jack'),\n  queen: new Card('spades', 'queen'),\n  king: new Card('spades', 'king'),\n  10: new Card('spades', 10),\n  ace: new Card('spades', 'ace'),\n};\nconst hearts = {\n  9: new Card('hearts', 9),\n  jack: new Card('hearts', 'jack'),\n  queen: new Card('hearts', 'queen'),\n  king: new Card('hearts', 'king'),\n  10: new Card('hearts', 10),\n  ace: new Card('hearts', 'ace'),\n};\n\nmodule('Unit | Game | Utils | availableMoves', function () {\n  const hand = [\n    hearts[9],\n    hearts.jack,\n    hearts.queen,\n    hearts.king,\n    hearts[10],\n    hearts.ace,\n    clubs.ace,\n    clubs.king,\n    spades[9],\n    spades.jack,\n    spades.queen,\n  ];\n\n  test('must play a trump card', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(diamonds.queen);\n\n    assert.deepEqual(availableMoves(trick, hand, 'clubs'), [clubs.ace, clubs.king]);\n  });\n\n  test('must play a higher card', async function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('hearts', 'queen'));\n\n    assert.deepEqual(availableMoves(trick, hand, 'clubs'), [\n      hearts.queen,\n      hearts.king,\n      hearts[10],\n      hearts.ace,\n    ]);\n  });\n\n  test('when there is no trump remaining', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('diamonds', 'queen'));\n\n    assert.deepEqual(availableMoves(trick, hand, 'diamonds'), hand);\n  });\n\n  test('can play card of equal value', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('spades', 'jack'));\n\n    assert.deepEqual(availableMoves(trick, hand, 'diamonds'), [spades.jack, spades.queen]);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/game/utils/isValidMove-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { Card } from 'pinochle/game/card';\nimport { Trick } from 'pinochle/game/trick';\nimport { isValidMove } from 'pinochle/game/utils/move-validation';\n\nmodule('Unit | Game | Utils | isValidMove', function () {\n  test('anything goes when the trick is empty', function (assert) {\n    let trick = new Trick(3);\n\n    assert.ok(isValidMove(trick, new Card('clubs', 9), 'diamonds'));\n  });\n\n  test('must match suit', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('clubs', 9));\n\n    assert.ok(isValidMove(trick, new Card('clubs', 'queen'), 'diamonds'));\n  });\n\n  test('can be equal', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('clubs', 9));\n\n    assert.ok(isValidMove(trick, new Card('clubs', 9), 'diamonds'));\n  });\n\n  test('cannot be less than', function (assert) {\n    let trick = new Trick(3);\n\n    trick.add(new Card('clubs', 10));\n\n    assert.notOk(isValidMove(trick, new Card('clubs', 9), 'diamonds'));\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/helpers/contains-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { contains } from 'pinochle/helpers/contains';\n\nmodule('Unit | Helper | contains', function () {\n  test('it works', function (assert) {\n    assert.true(contains([[1], 1]));\n    assert.false(contains([[2], 1]));\n    assert.false(contains([[2], '2']), 'no coercion ');\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/helpers/eq-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { eq } from 'pinochle/helpers/eq';\n\nmodule('Unit | Helper | eq', function () {\n  test('it works', function (assert) {\n    assert.true(eq([1, 1]));\n    assert.false(eq([1, '1']), 'no coercion');\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/helpers/is-number-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { isNumber } from 'pinochle/helpers/is-number';\n\nmodule('Integration | Helper | is-number', function () {\n  test('it works', async function (assert) {\n    assert.true(isNumber([1]));\n    assert.false(isNumber(['2']), 'no coercion');\n    assert.false(isNumber(['queen']));\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/tests/unit/helpers/suit-to-symbol-test.ts",
    "content": "import { module, test } from 'qunit';\n\nimport { NAME_MAP, suitToSymbol } from 'pinochle/helpers/suit-to-symbol';\n\nimport type { Suit } from 'pinochle/game/card';\n\nmodule('Unit | Helper | suit-to-symbol', function () {\n  test('it works', function (assert) {\n    assert.expect(5);\n\n    for (let [name, sym] of Object.entries(NAME_MAP)) {\n      assert.equal(suitToSymbol(name as LIES<Suit>), sym);\n    }\n\n    assert.equal(suitToSymbol('whatever' as LIES<Suit>), undefined);\n  });\n});\n"
  },
  {
    "path": "client/web/pinochle/translations/en-us.yaml",
    "content": "---\nappname: pinochle\nemberjs: Ember.JS\n\ntodo: \"TODO: fill this out correctly\"\n"
  },
  {
    "path": "client/web/pinochle/tsconfig.compiler-options.json",
    "content": "{\n  \"extends\": \"../config/tsconfig.compiler-options.json\"\n}\n"
  },
  {
    "path": "client/web/pinochle/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"compilerOptions\": {\n    \"composite\": true\n  },\n  \"references\": [\n    { \"path\": \"app\" },\n    { \"path\": \"tests\" }\n  ]\n}\n"
  },
  {
    "path": "client/web/pinochle/types/libraries.d.ts",
    "content": "\ndeclare module 'tracked-maps-and-sets' {\n  export class TrackedMap<K, V> extends Map<K, V> {}\n  export class TrackedWeakMap<K extends object, V> extends WeakMap<K, V> {}\n  export class TrackedSet<T> extends Set<T> {}\n  export class TrackedWeakSet<T extends object> extends WeakSet<T> {}\n}\n\ndeclare module 'focus-visible' {}\n\ntype LazyTrackedArgs = {\n  positional?: Array<unknown>;\n  named?: Record<string, unknown>;\n}\n\ndeclare module 'ember-could-get-used-to-this' {\n  export const use: PropertyDecorator;\n  export class Resource<Args extends LazyTrackedArgs> {\n    protected args: Args;\n\n    // This is a lie, but makes the call site nice\n    constructor(fn: () => Args['positional'] | Args);\n  }\n}\n\ndeclare module 'ember-concurrency-test-waiter/define-modifier' {\n  const foo: any;\n  export default foo;\n}\n\ndeclare module 'ember-raf-scheduler/test-support/register-waiter' {\n  const foo: any;\n  export default foo;\n}\n"
  },
  {
    "path": "client/web/pinochle/types/overrides.d.ts",
    "content": "import '@emberclear/questionably-typed/overrides';\n\nimport 'ember-concurrency-decorators';\nimport 'ember-concurrency-async';\nimport 'ember-concurrency-ts/async';\nimport 'ember-concurrency-test-waiter';\n\n\n"
  },
  {
    "path": "client/web/pinochle/vendor/.gitkeep",
    "content": ""
  },
  {
    "path": "client/web/scripts/clean.sh",
    "content": "#!/bin/bash\n\nfunction try_unmount {\n  if mountpoint -q -- \"$1\"; then\n    printf '%s\\n' \"$1 is in RAM, unmounting...\"\n    sudo umount -f $1\n  fi\n}\n\nfunction delete_excluding {\n  to_delete=$1\n  exclude=$2\n\n  find . \\\n    \\( -type d -name ${exclude} -prune \\) \\\n    -o \\( -type d -name ${to_delete} -prune \\\n          -exec rm -rf {} + \\)\n}\n\ntry_unmount \"emberclear/dist\"\ntry_unmount \"emberclear/node_modules\"\ntry_unmount \"node_modules\"\n\n# The simple way to recursively delete is\n#\n#  find . -type d -name \"declarations\" -exec rm -rf {} +\n#\n# But this also searches within node_modules\n\ndelete_excluding 'node_modules' 'declarations'\ndelete_excluding 'declarations' 'node_modules'\ndelete_excluding 'dist' 'node_modules'\n"
  },
  {
    "path": "client/web/smoke-tests/.eslintrc.js",
    "content": "'use strict';\n\nconst { configs } = require('@nullvoxpopuli/eslint-configs');\n\nconst config = configs.node();\n\n// TODO: add mocha config\nmodule.exports = {\n  ...config,\n  env: {\n    ...config.env,\n    mocha: true,\n    browser: true,\n    es6: true,\n  },\n  rules: {\n    ...config.rules,\n    'no-cond-assign': 'off',\n    'no-useless-escape': 'off',\n    'require-yield': 'off',\n  },\n};\n"
  },
  {
    "path": "client/web/smoke-tests/.faltestrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  options: {\n    targets: {\n      default: 'default',\n      list: ['default', 'local', 'ember', 'pull-request'],\n    },\n    browsers: {\n      firefox: {\n        args: [\n          // TODO\n        ],\n      },\n      chrome: {\n        args: [\n          process.env.CI ? '--no-sandbox' : null,\n          '--window-size=1280,720', // 720p\n          '--ignore-certificate-errors'\n        ].filter(Boolean)\n      }\n    }\n  },\n  globs: ['tests/**/*-test.js'],\n};\n"
  },
  {
    "path": "client/web/smoke-tests/.prettierrc.js",
    "content": "'use strict';\n\nmodule.exports = {\n  singleQuote: true,\n  trailingComma: 'es5',\n  printWidth: 80,\n  semi: true,\n  bracketSpacing: true,\n  endOfLine: 'lf',\n  tabs: false,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "client/web/smoke-tests/helpers/start-server.js",
    "content": "const path = require('path');\nconst execa = require('execa');\n\nconst distLocation = path.join(process.cwd(), '..', 'emberclear', 'dist');\n\nasync function startServer() {\n  console.info(`Starting server at ${distLocation}`);\n\n  let server = execa('http-server', [distLocation], {\n    preferLocal: true,\n  });\n\n  let port = await new Promise((resolve) => {\n    server.stdout.on('data', (data) => {\n      let str = data.toString();\n      let matches = str.match(/http:\\/\\/127\\.0\\.0\\.1:(\\d+)$/m);\n\n      if (matches) {\n        let currentPort = parseInt(matches[1]);\n\n        resolve(currentPort);\n      }\n    });\n  });\n\n  return { server, port };\n}\n\nmodule.exports = {\n  startServer,\n};\n"
  },
  {
    "path": "client/web/smoke-tests/package.json",
    "content": "{\n  \"name\": \"smoke-tests\",\n  \"version\": \"0.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"lint:js\": \"yarn eslint .\",\n    \"test\": \"yarn faltest --browsers 2 --timeouts-override 900000\"\n  },\n  \"devDependencies\": {\n    \"@nullvoxpopuli/eslint-configs\": \"1.3.2\",\n    \"@faltest/cli\": \"3.0.1\",\n    \"@faltest/lifecycle\": \"5.0.2\",\n    \"@faltest/page-objects\": \"4.0.2\",\n    \"http-server\": \"0.12.3\",\n    \"poll-pr-status\": \"3.0.0\",\n    \"prettier\": \"2.2.1\"\n  },\n  \"engines\": {\n    \"node\": \"14.17.1\"\n  },\n  \"volta\": {\n    \"node\": \"14.17.1\",\n    \"yarn\": \"1.22.10\"\n  }\n}\n"
  },
  {
    "path": "client/web/smoke-tests/page-objects/add-friend.js",
    "content": "'use strict';\n\nconst { BasePageObject } = require('@faltest/page-objects');\n\nclass AddFriend extends BasePageObject {\n  constructor(host, ...args) {\n    super(...args);\n\n    this.host = host;\n  }\n\n  get addFriendButton() {\n    // element click intercepted: Element <a class=\"button button-xs\" href=\"/add-friend\">...</a> is not clickable at point (274, 76). Other element would receive the click: <a class=\"service-worker-update-notify alert alert-info has-shadow\" href=\"/chat\" style=\"z-index: 100;\">...</a>\n    // return this._create('[href=\"/add-friend\"]');\n    return {\n      click: async () => {\n        await this._browser.execute(() => {\n          // eslint-disable-next-line no-undef\n          document.querySelector('[href=\"/add-friend\"]').click();\n        });\n      },\n    };\n  }\n\n  async addFriend(user) {\n    await this.addFriendButton.click();\n\n    await this._browser.url(`${this.host}/invite?name=${user.name}&publicKey=${user.publicKey}`);\n  }\n}\n\nmodule.exports = AddFriend;\n"
  },
  {
    "path": "client/web/smoke-tests/page-objects/chat.js",
    "content": "'use strict';\n\nconst { BasePageObject } = require('@faltest/page-objects');\n\nclass Chat extends BasePageObject {\n  get input() {\n    return this._create('textarea');\n  }\n\n  get sendButton() {\n    // TODO: this will be changed to a button soon, instead of an input\n    return this._create('.chat-entry-container input[type=\"submit\"]');\n  }\n\n  get messages() {\n    return this._createMany('.message', ({ each }) => {\n      each(({ pageObject }) => ({\n        name: pageObject.scopeChild('.message-header .from'),\n        text: pageObject.scopeChild('.message-body'),\n      }));\n    });\n  }\n\n  async sendMessage(message) {\n    await this.input.setValue(message);\n\n    // This is only necessary for CI, but not sure why.\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n\n    await this.sendButton.click();\n  }\n\n  async waitForResponse(user) {\n    await this._browser.waitUntil(async () => {\n      let messages = await this.messages.getPageObjects();\n\n      for (let message of messages) {\n        let name = await message.name.getText();\n        let text = await message.text.getText();\n\n        if (name === user.name && text === user.message) {\n          return true;\n        }\n      }\n    }, parseInt(process.env.WEBDRIVER_TIMEOUTS_OVERRIDE));\n  }\n}\n\nmodule.exports = Chat;\n"
  },
  {
    "path": "client/web/smoke-tests/page-objects/login.js",
    "content": "'use strict';\n\nconst { BasePageObject } = require('@faltest/page-objects');\n\nclass Login extends BasePageObject {\n  get beginButton() {\n    return this._create('[href=\"/chat\"]');\n  }\n\n  get logInInsteadButton() {\n    //element not interactable\n    return {\n      click: async () => {\n        await this._browser.execute(() => {\n          document.querySelector('[href=\"/login\"]').click();\n        });\n      },\n    };\n    // return this._create('[href=\"/login\"]');\n  }\n\n  get name() {\n    return this._createMany('input').first;\n  }\n\n  get mnemonic() {\n    // need a `.pageObjectAt(1)` or similar\n    return this._create(async () => {\n      return (await this._browser.$$('input'))[1];\n    });\n  }\n\n  get logInButton() {\n    return this._createMany('button').last;\n  }\n\n  async logIn(user) {\n    await this.beginButton.click();\n\n    await this.logInInsteadButton.click();\n\n    await this.name.setValue(user.name);\n    await this.mnemonic.setValue(user.mnemonic);\n\n    await this.logInButton.click();\n  }\n}\n\nmodule.exports = Login;\n"
  },
  {
    "path": "client/web/smoke-tests/tests/smoke-test.js",
    "content": "'use strict';\n\nconst assert = require('assert');\nconst { setUpWebDriver } = require('@faltest/lifecycle');\nconst Login = require('../page-objects/login');\nconst AddFriend = require('../page-objects/add-friend');\nconst Chat = require('../page-objects/chat');\nconst { startServer } = require('../helpers/start-server');\n\ndescribe('smoke', function () {\n  before(async function () {\n    switch (process.env.WEBDRIVER_TARGET) {\n      case 'pull-request': {\n        console.info('---- DEPLOY PREVIEW ----');\n        console.info(process.env.DEPLOY_URL);\n\n        this.host = process.env.DEPLOY_URL;\n\n        if (!this.host) {\n          throw new Error(`host not set. Did you forget to set $DEPLOY_URL?`);\n        }\n\n        break;\n      }\n\n      case 'local': {\n        let { server, port } = await startServer();\n\n        this.server = server;\n\n        this.host = `http://localhost:${port}`;\n\n        break;\n      }\n\n      case 'ember': {\n        this.host = `https://localhost:4201`;\n\n        break;\n      }\n\n      default: {\n        this.host = 'https://emberclear.io';\n\n        break;\n      }\n    }\n  });\n\n  setUpWebDriver.call(this);\n\n  beforeEach(async function () {\n    this.loginPage1 = new Login(this.browsers[0]);\n    this.loginPage2 = new Login(this.browsers[1]);\n    this.addFriendPage1 = new AddFriend(this.host, this.browsers[0]);\n    this.addFriendPage2 = new AddFriend(this.host, this.browsers[1]);\n    this.chatPage1 = new Chat(this.browsers[0]);\n    this.chatPage2 = new Chat(this.browsers[1]);\n\n    await Promise.all([this.browsers[0].url(this.host), this.browsers[1].url(this.host)]);\n  });\n\n  after(async function () {\n    if (this.server) {\n      this.server.kill();\n\n      await this.server;\n    }\n  });\n\n  it('works', async function () {\n    let users = [\n      {\n        name: 'jRA0gfR7',\n        mnemonic:\n          'assist lounge buyer clump marble vital check ordinary liar resemble fantasy vapor snow stool myth mention mention ask tiger video ball suspect lens above loan',\n        message: 'Hello Browser 2!',\n        publicKey: 'b4645cdeec6889d7515aeadab66b2b4fd0fbac5751f701e0289a1add7822a739',\n      },\n      {\n        name: 'SpxDqBPG',\n        mnemonic:\n          'glimpse moment duck pigeon awake gossip burger repair dizzy employ diary merge swarm select very liar rail exhibit space runway face inhale absorb able trigger',\n        message: 'Hello Browser 1!',\n        publicKey: 'e3ab4b615a00cacbd44d498cdc4d880bb484e2e6e0b1b02bbf3d393c12183047',\n      },\n    ];\n\n    await Promise.all([this.loginPage1.logIn(users[0]), this.loginPage2.logIn(users[1])]);\n\n    await Promise.all([\n      this.addFriendPage1.addFriend(users[1]),\n      this.addFriendPage2.addFriend(users[0]),\n    ]);\n\n    await Promise.all([\n      this.chatPage1.sendMessage(users[0].message),\n      this.chatPage2.sendMessage(users[1].message),\n    ]);\n\n    await Promise.all([\n      this.chatPage1.waitForResponse(users[1]),\n      this.chatPage2.waitForResponse(users[0]),\n    ]);\n\n    assert.ok(true);\n  });\n});\n"
  },
  {
    "path": "client/web/stylelint.config.js",
    "content": "'use strict';\n\nmodule.exports = {\n  extends: 'stylelint-config-standard',\n  rules: {\n    'selector-type-no-unknown': null,\n    'no-descending-specificity': null,\n    'value-keyword-case': null,\n  },\n};\n"
  },
  {
    "path": "client/web/tsconfig.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./libraries/\" },\n    { \"path\": \"./addons/\" },\n\n    // Apps\n    { \"path\": \"./emberclear\" },\n    { \"path\": \"./pinochle\" }\n  ]\n}\n"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\n  - source: /client/web/**/*/en-us.yaml\n    translation: '%original_path%/%locale%.yaml'\n"
  },
  {
    "path": "scripts/deploy",
    "content": "#!/bin/bash\n# https://stackoverflow.com/questions/6059336/how-to-find-the-current-git-branch-in-detached-head-state\n\nBRANCH=$(\\\n  git for-each-ref \\\n  --format='%(objectname) %(refname:short)' refs/heads \\\n  | awk \"/^$(git rev-parse HEAD)/ {print \\$2}\"\\\n)\n\necho \"Current branch: $BRANCH\"\n\nif [[ \"$BRANCH\" == \"master\" ]]; then\n  echo \"Current branch is master. Starting deploy...\"\n\n  # exit if any of the following errors\n  set -e\n\n  ( cd ./client/web/emberclear && rm -rf dist tmp )\n  time ./run yarn build:production\n\n  ( ./scripts/publish )\nelse\n  echo \"current branch ($BRANCH) did not equal 'master'.\"\nfi\n"
  },
  {
    "path": "scripts/docker-compose.yml",
    "content": "version: '3.4'\nservices:\n  phoenix-relay:\n    build:\n      context: ./relays/phoenix-relay\n\n    command: bash -c \"mix deps.get && mix phx.server\"\n    container_name: phoenix-relay\n    volumes:\n      - .:/app\n    ports:\n      - \"4301:4301\"\n    volumes:\n      - phoenix-deps:/app/deps\n      - phoenix-build:/app/_build\n      - ./relays/phoenix-relay:/app\n\n  emberclear:\n    build:\n      context: ./client/web/emberclear\n\n    # Default command to run when the container starts\n    command: bash -c \"yarn && yarn start:dev -p 4201\"\n\n    # To prevent files created in the container from being owned by root\n    # to get these numbers: `echo \"$(id -u):$(id -g)\"`\n    # user: 1000:1000\n\n    # A friendly name for finding the container with\n    # docker commands easier\n    container_name: emberclear\n\n    # NOTE: Ports Mappings are HOST:CONTAINER\n    ports:\n      # Test Server\n      - 7357:7357\n      # Ember Dev Server\n      - 4201:4201\n      # Live Reload\n      - 7020:7020\n    volumes:\n      - ember-tmp:/app/tmp\n      - ember-dist:/app/dist\n      - ember-node_modules:/app/node_modules\n      # Copies the whole app into the container\n      # - ./client-web/emberclear:/app:delegated\n\n      # Copy invidial files/folders so container contents don't leak out\n      - ./client/web/emberclear/config:/app/config\n      - ./client/web/emberclear/fastboot:/app/fastboot\n      - ./client/web/emberclear/public:/app/public\n      - ./client/web/emberclear/src:/app/src\n      - ./client/web/emberclear/tests:/app/tests\n      - ./client/web/emberclear/translations:/app/translations\n      - ./client/web/emberclear/types:/app/types\n      - ./client/web/emberclear/vendor:/app/vendor\n\n      - ./client/web/emberclear/.ember-cli:/app/.ember-cli\n      - ./client/web/emberclear/.eslintignore:/app/.eslintignore\n      - ./client/web/emberclear/.eslintrc.js:/app/.eslintrc.js\n      - ./client/web/emberclear/.watchmanconfig:/app/.watchmanconfig\n      - ./client/web/emberclear/ember-cli-build.js:/app/ember-cli-build.js\n      - ./client/web/emberclear/package.json:/app/package.json\n      - ./client/web/emberclear/testem.js:/app/testem.js\n      - ./client/web/emberclear/tsconfig.json:/app/tsconfig.json\n      - ./client/web/emberclear/yarn.lock:/app/yarn.lock\n\n      # Override the above mapping for the following\n      # noise directories:\n\n\n\nvolumes:\n  ember-tmp:\n  ember-dist:\n  ember-node_modules:\n  phoenix-deps:\n  phoenix-build:\n"
  },
  {
    "path": "scripts/dockerhub",
    "content": "#!/bin/bash\ncd client/web/emberclear/\n\nGIT_TAG=$GITHUB_SHA\nREPO_NAME=\"nullvoxpopuli/emberclear\"\nGIT_IMAGE=\"$REPO_NAME:$GIT_TAG\"\n\necho \"GIT_TAG: $GIT_TAG\"\necho \"REPO_NAME: $REPO_NAME\"\necho \"GIT_IMAGE: $GIT_IMAGE\"\n\ndocker build -t \"$REPO_NAME\" -f Dockerfile.release .\n\necho $DOCKERHUB_PASSWORD | docker login --username $DOCKERHUB_USER --password-stdin\n\ndocker tag \"$REPO_NAME\" \"$REPO_NAME:latest\"\ndocker tag \"$REPO_NAME\" \"$GIT_IMAGE\"\n\ndocker push $GIT_IMAGE\ndocker push \"$REPO_NAME:latest\"\n\ndeploy_status=$?\necho \"Deploy finished with status: $deploy_status\"\n\nexit $deploy_status\n"
  },
  {
    "path": "scripts/install-chrome-apt",
    "content": "# Installs Chrome\nwget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub |  apt-key add -\necho 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' |  tee /etc/apt/sources.list.d/google-chrome.list\napt-get update\napt-get install google-chrome-stable -y\n"
  },
  {
    "path": "scripts/publish",
    "content": "#!/bin/bash\ncp $FRONTEND/config/netlify/_redirects $FRONTEND/dist\n\n# don't exit if a script errors, because we want custom logging\nset +e\n\nnetlify deploy \\\n  --auth $NETLIFY_ACCESS_TOKEN \\\n  --site $NETLIFY_SITE_ID \\\n  --prod \\\n  --dir \"./$FRONTEND/dist\" \\\n  --message \"Deploying emberclear.io for revision: $GITHUB_SHA\"\n\ndeploy_status=$?\necho \"Deploy finished with status: $deploy_status\"\n\nexit $deploy_status\n"
  }
]