[
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"env\": {\n        \"browser\": true,\n        \"es6\": true,\n        \"node\": true\n    },\n    \"extends\": [\n        \"eslint:recommended\",\n        \"plugin:@typescript-eslint/eslint-recommended\",\n        \"plugin:@typescript-eslint/recommended\",\n        \"plugin:import/errors\",\n        \"plugin:import/warnings\",\n        \"plugin:import/typescript\",\n        \"plugin:react/recommended\",\n        \"airbnb-typescript\",\n        \"plugin:jsx-a11y/recommended\"\n    ],\n    \"parser\": \"@typescript-eslint/parser\",\n    \"parserOptions\": {\n        \"tsConfigRootDir\": \"./\",\n        \"project\": \"./tsconfig.json\"\n    },\n    \"plugins\": [\n        \"@typescript-eslint\",\n        \"import\",\n        \"deprecation\",\n        \"jsx-a11y\"\n    ],\n    \"rules\": {\n        \"indent\": [\"error\", 4, {\n            \"SwitchCase\": 1\n        }],\n        \"react/jsx-indent\": [\"error\", 4],\n        \"react/jsx-indent-props\": [\"error\", 4],\n        \"@typescript-eslint/indent\": [\"error\", 4],\n        \"react/prop-types\": \"off\",\n        \"arrow-parens\": \"error\",\n        \"deprecation/deprecation\": \"warn\"\n    },\n    \"settings\": {\n        \"import/resolver\": {\n            \"typescript\": {}, // this loads <rootdir>/tsconfig.json to eslint\n            \"node\": {\n                \"paths\": [ \"src\" ]\n            }\n        },\n        \"import/core-modules\": [ \"electron\", \"nodegit\" ],\n        \"react\": {\n            \"version\": \"detect\"\n        }\n    }\n}\n"
  },
  {
    "path": ".gitbook.yaml",
    "content": "root: ./docs/\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json\nname: Build\n\non: [push]\n\njobs:\n  lint:\n    name: 'Lint'\n    runs-on: macos-latest\n\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 1\n          submodules: true\n\n      - name: Setup Node\n        uses: actions/setup-node@v3\n        with:\n          node-version: '16'\n\n      - uses: actions/cache@v3\n        with:\n          path: '**/node_modules'\n          key: linter-node_modules-${{hashFiles('**/package-lock.json') }}\n\n      - name: Install dependencies\n        env:\n          JOBS: 'max'\n        run: npm install\n\n      - name: Run linter\n        run: npm run lint\n\n  build:\n    name: Build (${{ matrix.os }} - ${{ matrix.arch }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        # Build for supported platforms\n        # https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9\n        include:\n        - os: ubuntu-20.04\n          arch: arm64\n        - os: ubuntu-20.04\n          arch: x64\n        - os: macOS-11\n          arch: arm64\n          openssl_dir: '/tmp/openssl@1.1/1.1.1l_1'\n        - os: macOS-11\n          arch: x64\n          openssl_dir: '/usr/local/opt/openssl@1.1'\n        - os: windows-2019\n          arch: x64\n          openssl_dir: 'C:\\Program Files\\OpenSSL-Win64\\'\n        # - os: ubuntu-20.04\n        #   arch: armv7l\n        # - os: windows-2019\n        #   arch: arm64\n        #   openssl_dir: 'C:\\Program Files\\OpenSSL-Win64\\'\n        \n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 1\n          submodules: true\n\n      - name: Setup Node\n        uses: actions/setup-node@v3\n        with:\n          node-version: '16'\n\n      - uses: actions/cache@v3\n        with:\n          path: '**/node_modules'\n          key: ${{ matrix.os }}-${{ matrix.arch }}-node_modules-${{hashFiles('**/package-lock.json') }}\n          \n      - name: Install Build dependencies (macOS-arm64)\n        if: |\n          matrix.os == 'macOS-11'\n            && matrix.arch == 'arm64'\n        run: |\n          curl -L -H \"Authorization: Bearer QQ==\" -o /tmp/openssl-1.1.1l_1-arm64.tar.gz https://ghcr.io/v2/homebrew/core/openssl/1.1/blobs/sha256:8f5b0bee61c1570b9f0fc0a21d6c322e904ae7975bdaada5787451d18e9677a6\n          tar -xvf /tmp/openssl-1.1.1l_1-arm64.tar.gz -C /tmp\n          \n      - name: Install Build dependencies (Windows)\n        if: matrix.os == 'windows-2019'\n        run: choco install openssl\n\n      - name: Install Build dependencies (Ubuntu)\n        if: matrix.os == 'ubuntu-20.04'\n        run: |\n          sudo apt update\n          sudo apt install libkrb5-dev\n\n      - name: Install dependencies\n        env:\n          JOBS: 'max'\n          npm_config_openssl_dir: ${{ matrix.openssl_dir }}\n        run: |\n          npm install\n\n      - name: Rebuild native modules\n        env:\n          JOBS: 'max'\n          npm_config_openssl_dir: ${{ matrix.openssl_dir }}\n        run: |\n          npm run rebuild:native-modules -- --arch=${{ matrix.arch }}\n          npm run prepare:nodegit\n\n      - name: Load Developer Certificates (macOS)\n        if: |\n          startsWith(github.ref, 'refs/tags/')\n            && matrix.os == 'macOS-11'\n        env:\n          MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}\n          MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}\n        run:\n          chmod +x scripts/setupMacOSCertificates.sh && ./scripts/setupMacOSCertificates.sh\n          \n      - name: Compile and Sign\n        if: startsWith(github.ref, 'refs/tags/')\n        env:\n          npm_config_openssl_dir: ${{ matrix.openssl_dir }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}\n          MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}\n          MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}\n          GMAIL_OAUTH_CLIENT_ID: ${{ secrets.GMAIL_OAUTH_CLIENT_ID }}\n          GMAIL_OAUTH_CLIENT_SECRET: ${{ secrets.GMAIL_OAUTH_CLIENT_SECRET }}\n        run:\n          npm run make -- --arch=${{ matrix.arch }}\n\n      - name: Compile\n        if: startsWith(github.ref, 'refs/tags/') == false\n        env:\n          npm_config_openssl_dir: ${{ matrix.openssl_dir }}\n          GMAIL_OAUTH_CLIENT_ID: ${{ secrets.GMAIL_OAUTH_CLIENT_ID }}\n          GMAIL_OAUTH_CLIENT_SECRET: ${{ secrets.GMAIL_OAUTH_CLIENT_SECRET }}\n        run: npm run make -- --arch=${{ matrix.arch }}\n\n      - name: Test\n        if: |\n          matrix.os != 'ubuntu-20.04'\n            && matrix.arch == 'x64'\n            && matrix.os != 'macOS-11'\n            && matrix.os != 'windows-2019'\n        run: npm run test\n\n      - name: Test (Ubuntu)\n        if: |\n          matrix.os == 'ubuntu-20.04'\n            && matrix.arch == 'x64'\n        run: xvfb-run --auto-servernum -- npm test\n\n      - name: Upload artifacts\n        if: ${{ always() }}\n        uses: actions/upload-artifact@v3\n        with:\n          name: ${{ matrix.os }}-${{ matrix.arch }}\n          path: |\n            out/make\n            test/output\n          \n      - name: Release\n        uses: softprops/action-gh-release@v1\n        if: startsWith(github.ref, 'refs/tags/')\n        with:\n          files: \"out/make/**\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: '0 5 * * 1'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        language: ['javascript']\n        # Learn more...\n        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n      with:\n        # We must fetch at least the immediate parents so that if this is\n        # a pull request then we can checkout the head.\n        fetch-depth: 2\n\n    # If this run was triggered by a pull request event, then checkout\n    # the head of the pull request instead of the merge commit.\n    - run: git checkout HEAD^2\n      if: ${{ github.event_name == 'pull_request' }}\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file. \n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n"
  },
  {
    "path": ".github/workflows/electronegativity.yml",
    "content": "name: \"Electronegativity\"\n\non: \n  push:\n    \njobs:\n  build_job:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      \n      - uses: actions/setup-node@v3\n        with:\n          node-version: '12'\n\n      - uses: doyensec/electronegativity-action@v1.1\n\n      - name: Upload sarif\n        uses: github/codeql-action/upload-sarif@v2\n        with:\n          sarif_file: ../results"
  },
  {
    "path": ".github/workflows/release-notes.yml",
    "content": "name: Release Notes\n\non:\n  release:\n    types:\n    - created\n\njobs:\n  create_notes:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Context\n        env:\n          GITHUB_CONTEXT: ${{ toJson(github) }}\n        run: echo \"$GITHUB_CONTEXT\"\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 1\n      - name: Setup Node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 14.x\n      - name: Run Gren\n        env:\n          GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          npx github-release-notes release -o"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n.DS_Store\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# next.js build output\n.next\n\n# nuxt.js build output\n.nuxt\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# Webpack\n.webpack/\n\n# Electron-Forge\nout/\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n# empty for now\n"
  },
  {
    "path": ".husky/pre-push",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n# Check if there are any outstanding changes\n# NOTE: This is necessary as husky will run checks on pushing,\n# while there might be changes that are not yet committed interfering\n# with the tests and linter\ngit diff HEAD --quiet\n\nnpm run lint\nnpm run make\nnpm test\n"
  },
  {
    "path": ".npmrc",
    "content": "legacy-peer-deps=true"
  },
  {
    "path": ".vscode/debug-launcher.sh",
    "content": "#!/usr/bin/env bash\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nARGS=$@\nARGS=${ARGS// /\\~ \\~}\n\nnode \"$DIR/../node_modules/@electron-forge/cli/dist/electron-forge-start\" --vscode -- \\~$ARGS\\~\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\": \"pwa-node\",\n            \"request\": \"launch\",\n            \"name\": \"Electron Main\",\n            \"program\": \"${workspaceFolder}/node_modules/@electron-forge/cli/dist/electron-forge-start.js\",\n            \"args\": [\n              \"--vscode\"\n            ],\n            \"cwd\": \"${workspaceFolder}\",\n            \"sourceMaps\": true,\n            \"windows\": {\n              \"runtimeExecutable\": \"${workspaceFolder}/node_modules/.bin/electron-forge-vscode-win.cmd\",\n              \"console\": \"internalConsole\", \n              \"outputCapture\": \"std\",\n            },\n            \"resolveSourceMapLocations\": [\n                \"${workspaceFolder}/**\",\n                \"!**/node_modules/**\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"eslint.enable\": true\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at lei@codified.nl. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Aeon\nAeon welcomes all contributions to it warmly and with open arms. \n\n## GitHub Issues\nGitHub issues is our main method for tracking project progress, feature requests, bugs, etc. If you have an idea for a new implementation, just open a new GitHub issue, and we'll try to get you on the way to help with contributing.\n\n## Documentation\nDocumentation for inner workings of Aeon are available as part of the [Aeon documentation](https://docs.aeon.technology). Please refer to this document when you are trying to implement new features. In case you get stuck, or need help, please create a GitHub issue!\n\n## Coding Conventions\nAll code is written with four-spaced tabs. We have ESLint setup, and we kindly request you to lint your code before submitting it for a pull request: `npm run lint`.\n"
  },
  {
    "path": "LICENSE",
    "content": "European Union Public Licence\nV. 1.2\n\nEUPL © the European Union 2007, 2016\n\nThis European Union Public Licence (the ‘EUPL’) applies to the Work (as\ndefined below) which is provided under the terms of this Licence. Any use of\nthe Work, other than as authorised under this Licence is prohibited (to the\nextent such use is covered by a right of the copyright holder of the Work).\n\nThe Work is provided under the terms of this Licence when the Licensor (as\ndefined below) has placed the following notice immediately following the\ncopyright notice for the Work: “Licensed under the EUPL”, or has expressed by\nany other means his willingness to license under the EUPL.\n\n1. Definitions\n\nIn this Licence, the following terms have the following meaning:\n— ‘The Licence’: this Licence.\n— ‘The Original Work’: the work or software distributed or communicated by the\n  ‘Licensor under this Licence, available as Source Code and also as\n  ‘Executable Code as the case may be.\n— ‘Derivative Works’: the works or software that could be created by the\n  ‘Licensee, based upon the Original Work or modifications thereof. This\n  ‘Licence does not define the extent of modification or dependence on the\n  ‘Original Work required in order to classify a work as a Derivative Work;\n  ‘this extent is determined by copyright law applicable in the country\n  ‘mentioned in Article 15.\n— ‘The Work’: the Original Work or its Derivative Works.\n— ‘The Source Code’: the human-readable form of the Work which is the most\n  convenient for people to study and modify.\n\n— ‘The Executable Code’: any code which has generally been compiled and which\n  is meant to be interpreted by a computer as a program.\n— ‘The Licensor’: the natural or legal person that distributes or communicates\n  the Work under the Licence.\n— ‘Contributor(s)’: any natural or legal person who modifies the Work under\n  the Licence, or otherwise contributes to the creation of a Derivative Work.\n— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of\n  the Work under the terms of the Licence.\n— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,\n  renting, distributing, communicating, transmitting, or otherwise making\n  available, online or offline, copies of the Work or providing access to its\n  essential functionalities at the disposal of any other natural or legal\n  person.\n\n2. Scope of the rights granted by the Licence\n\nThe Licensor hereby grants You a worldwide, royalty-free, non-exclusive,\nsublicensable licence to do the following, for the duration of copyright\nvested in the Original Work:\n\n— use the Work in any circumstance and for all usage,\n— reproduce the Work,\n— modify the Work, and make Derivative Works based upon the Work,\n— communicate to the public, including the right to make available or display\n  the Work or copies thereof to the public and perform publicly, as the case\n  may be, the Work,\n— distribute the Work or copies thereof,\n— lend and rent the Work or copies thereof,\n— sublicense rights in the Work or copies thereof.\n\nThose rights can be exercised on any media, supports and formats, whether now\nknown or later invented, as far as the applicable law permits so.\n\nIn the countries where moral rights apply, the Licensor waives his right to\nexercise his moral right to the extent allowed by law in order to make\neffective the licence of the economic rights here above listed.\n\nThe Licensor grants to the Licensee royalty-free, non-exclusive usage rights\nto any patents held by the Licensor, to the extent necessary to make use of\nthe rights granted on the Work under this Licence.\n\n3. Communication of the Source Code\n\nThe Licensor may provide the Work either in its Source Code form, or as\nExecutable Code. If the Work is provided as Executable Code, the Licensor\nprovides in addition a machine-readable copy of the Source Code of the Work\nalong with each copy of the Work that the Licensor distributes or indicates,\nin a notice following the copyright notice attached to the Work, a repository\nwhere the Source Code is easily and freely accessible for as long as the\nLicensor continues to distribute or communicate the Work.\n\n4. Limitations on copyright\n\nNothing in this Licence is intended to deprive the Licensee of the benefits\nfrom any exception or limitation to the exclusive rights of the rights owners\nin the Work, of the exhaustion of those rights or of other applicable\nlimitations thereto.\n\n5. Obligations of the Licensee\n\nThe grant of the rights mentioned above is subject to some restrictions and\nobligations imposed on the Licensee. Those obligations are the following:\n\nAttribution right: The Licensee shall keep intact all copyright, patent or\ntrademarks notices and all notices that refer to the Licence and to the\ndisclaimer of warranties. The Licensee must include a copy of such notices and\na copy of the Licence with every copy of the Work he/she distributes or\ncommunicates. The Licensee must cause any Derivative Work to carry prominent\nnotices stating that the Work has been modified and the date of modification.\n\nCopyleft clause: If the Licensee distributes or communicates copies of the\nOriginal Works or Derivative Works, this Distribution or Communication will be\ndone under the terms of this Licence or of a later version of this Licence\nunless the Original Work is expressly distributed only under this version of\nthe Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee\n(becoming Licensor) cannot offer or impose any additional terms or conditions\non the Work or Derivative Work that alter or restrict the terms of the\nLicence.\n\nCompatibility clause: If the Licensee Distributes or Communicates Derivative\nWorks or copies thereof based upon both the Work and another work licensed\nunder a Compatible Licence, this Distribution or Communication can be done\nunder the terms of this Compatible Licence. For the sake of this clause,\n‘Compatible Licence’ refers to the licences listed in the appendix attached to\nthis Licence. Should the Licensee's obligations under the Compatible Licence\nconflict with his/her obligations under this Licence, the obligations of the\nCompatible Licence shall prevail.\n\nProvision of Source Code: When distributing or communicating copies of the\nWork, the Licensee will provide a machine-readable copy of the Source Code or\nindicate a repository where this Source will be easily and freely available\nfor as long as the Licensee continues to distribute or communicate the Work.\n\nLegal Protection: This Licence does not grant permission to use the trade\nnames, trademarks, service marks, or names of the Licensor, except as required\nfor reasonable and customary use in describing the origin of the Work and\nreproducing the content of the copyright notice.\n\n6. Chain of Authorship\n\nThe original Licensor warrants that the copyright in the Original Work granted\nhereunder is owned by him/her or licensed to him/her and that he/she has the\npower and authority to grant the Licence.\n\nEach Contributor warrants that the copyright in the modifications he/she\nbrings to the Work are owned by him/her or licensed to him/her and that he/she\nhas the power and authority to grant the Licence.\n\nEach time You accept the Licence, the original Licensor and subsequent\nContributors grant You a licence to their contributions to the Work, under the\nterms of this Licence.\n\n7. Disclaimer of Warranty\n\nThe Work is a work in progress, which is continuously improved by numerous\nContributors. It is not a finished work and may therefore contain defects or\n‘bugs’ inherent to this type of development.\n\nFor the above reason, the Work is provided under the Licence on an ‘as is’\nbasis and without warranties of any kind concerning the Work, including\nwithout limitation merchantability, fitness for a particular purpose, absence\nof defects or errors, accuracy, non-infringement of intellectual property\nrights other than copyright as stated in Article 6 of this Licence.\n\nThis disclaimer of warranty is an essential part of the Licence and a\ncondition for the grant of any rights to the Work.\n\n8. Disclaimer of Liability\n\nExcept in the cases of wilful misconduct or damages directly caused to natural\npersons, the Licensor will in no event be liable for any direct or indirect,\nmaterial or moral, damages of any kind, arising out of the Licence or of the\nuse of the Work, including without limitation, damages for loss of goodwill,\nwork stoppage, computer failure or malfunction, loss of data or any commercial\ndamage, even if the Licensor has been advised of the possibility of such\ndamage. However, the Licensor will be liable under statutory product liability\nlaws as far such laws apply to the Work.\n\n9. Additional agreements\n\nWhile distributing the Work, You may choose to conclude an additional\nagreement, defining obligations or services consistent with this Licence.\nHowever, if accepting obligations, You may act only on your own behalf and on\nyour sole responsibility, not on behalf of the original Licensor or any other\nContributor, and only if You agree to indemnify, defend, and hold each\nContributor harmless for any liability incurred by, or claims asserted against\nsuch Contributor by the fact You have accepted any warranty or additional\nliability.\n\n10. Acceptance of the Licence\n\nThe provisions of this Licence can be accepted by clicking on an icon ‘I\nagree’ placed under the bottom of a window displaying the text of this Licence\nor by affirming consent in any other similar way, in accordance with the rules\nof applicable law. Clicking on that icon indicates your clear and irrevocable\nacceptance of this Licence and all of its terms and conditions.\n\nSimilarly, you irrevocably accept this Licence and all of its terms and\nconditions by exercising any rights granted to You by Article 2 of this\nLicence, such as the use of the Work, the creation by You of a Derivative Work\nor the Distribution or Communication by You of the Work or copies thereof.\n\n11. Information to the public\n\nIn case of any Distribution or Communication of the Work by means of\nelectronic communication by You (for example, by offering to download the Work\nfrom a remote location) the distribution channel or media (for example, a\nwebsite) must at least provide to the public the information requested by the\napplicable law regarding the Licensor, the Licence and the way it may be\naccessible, concluded, stored and reproduced by the Licensee.\n\n12. Termination of the Licence\n\nThe Licence and the rights granted hereunder will terminate automatically upon\nany breach by the Licensee of the terms of the Licence. Such a termination\nwill not terminate the licences of any person who has received the Work from\nthe Licensee under the Licence, provided such persons remain in full\ncompliance with the Licence.\n\n13. Miscellaneous\n\nWithout prejudice of Article 9 above, the Licence represents the complete\nagreement between the Parties as to the Work.\n\nIf any provision of the Licence is invalid or unenforceable under applicable\nlaw, this will not affect the validity or enforceability of the Licence as a\nwhole. Such provision will be construed or reformed so as necessary to make it\nvalid and enforceable.\n\nThe European Commission may publish other linguistic versions or new versions\nof this Licence or updated versions of the Appendix, so far this is required\nand reasonable, without reducing the scope of the rights granted by the\nLicence. New versions of the Licence will be published with a unique version\nnumber.\n\nAll linguistic versions of this Licence, approved by the European Commission,\nhave identical value. Parties can take advantage of the linguistic version of\ntheir choice.\n\n14. Jurisdiction\n\nWithout prejudice to specific agreement between parties,\n— any litigation resulting from the interpretation of this License, arising\n  between the European Union institutions, bodies, offices or agencies, as a\n  Licensor, and any Licensee, will be subject to the jurisdiction of the Court\n  of Justice of the European Union, as laid down in article 272 of the Treaty\n  on the Functioning of the European Union,\n— any litigation arising between other parties and resulting from the\n  interpretation of this License, will be subject to the exclusive\n  jurisdiction of the competent court where the Licensor resides or conducts\n  its primary business.\n\n15. Applicable Law\n\nWithout prejudice to specific agreement between parties,\n— this Licence shall be governed by the law of the European Union Member State\n  where the Licensor has his seat, resides or has his registered office,\n— this licence shall be governed by Belgian law if the Licensor has no seat,\n  residence or registered office inside a European Union Member State.\n\nAppendix\n\n‘Compatible Licences’ according to Article 5 EUPL are:\n— GNU General Public License (GPL) v. 2, v. 3\n— GNU Affero General Public License (AGPL) v. 3\n— Open Software License (OSL) v. 2.1, v. 3.0\n— Eclipse Public License (EPL) v. 1.0\n— CeCILL v. 2.0, v. 2.1\n— Mozilla Public Licence (MPL) v. 2\n— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3\n— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for\n  works other than software\n— European Union Public Licence (EUPL) v. 1.1, v. 1.2\n— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or\n  Strong Reciprocity (LiLiQ-R+)\n\n— The European Commission may update this Appendix to later versions of the\n  above licences without producing a new version of the EUPL, as long as they\n  provide the rights granted in Article 2 of this Licence and protect the\n  covered Source Code from exclusive appropriation.\n— All other changes or additions to this Appendix require the production of a\n  new EUPL version.\n"
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://aeon.technology\">![Aeon](./docs/.gitbook/assets/aeon-logo-whitespace.png)</a>\n\n<p align=\"center\">\n    <em>📡 Scan the internet for your personal information and modify or remove it</em>\n</p>\n\n<br />\n\n<div align=\"center\">\n    <img src=\"https://github.com/leinelissen/aeon/workflows/Build/badge.svg\" />\n    <img alt=\"License\" src=\"https://img.shields.io/badge/license-EUPL-green\">\n    <img alt=\"GitHub package.json version\" src=\"https://img.shields.io/github/package-json/v/leinelissen/aeon?color=green\">\n    <a href=\"https://docs.aeon.technology\"><img alt=\"Documentation\" src=\"https://img.shields.io/badge/documentation-up-green\"></a>\n</div>\n\n<br />\n<br />\n\n# What is Aeon?\n📡&nbsp; Ever wondered what personal information is scattered around the internet? Aeon scans popuplar platforms for your personal information and (*almost*) automatically retrieves it.\n\n👀&nbsp; Use Aeon to download, archive and visualise your personal information. \n\n❌&nbsp; Don't agree with the data Facebook (or another platform) knows about you? Generate a request for modification or deletion with the click of a button!\n\n<br />\n<p align=\"center\">\n    <img src=\"./docs/.gitbook/assets/aeon-demo.gif\" width=\"600\" />\n</p>\n<br />\n\n# Installing Aeon\n<svg width=\"285\" height=\"50\" viewBox=\"0 0 285 50\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M21.544 32L22.7457 28.5355H27.8594L29.0547 32H31.1513L26.4403 18.9091H24.1584L19.4474 32H21.544ZM23.321 26.8736L25.2514 21.2869H25.3537L27.2841 26.8736H23.321ZM40.057 22.1818H38.0051L35.525 29.7372H35.4228L32.9363 22.1818H30.8844L34.4512 32H36.4966L40.057 22.1818ZM44.4116 32.2173C46.0352 32.2173 46.9492 31.3928 47.3136 30.6577H47.3903V32H49.2567V25.4801C49.2567 22.6229 47.0067 22.054 45.4471 22.054C43.6701 22.054 42.0337 22.7699 41.3945 24.5597L43.1907 24.9688C43.4719 24.272 44.1879 23.6009 45.4727 23.6009C46.7063 23.6009 47.3391 24.2464 47.3391 25.3587V25.4034C47.3391 26.1001 46.6232 26.0874 44.859 26.2919C42.9989 26.5092 41.0941 26.995 41.0941 29.2259C41.0941 31.1562 42.5451 32.2173 44.4116 32.2173ZM44.8271 30.6832C43.7468 30.6832 42.967 30.1974 42.967 29.2514C42.967 28.2287 43.8746 27.8643 44.9805 27.7173C45.6005 27.6342 47.0707 27.468 47.3455 27.1932V28.4588C47.3455 29.6222 46.4187 30.6832 44.8271 30.6832ZM51.8024 32H53.7136V22.1818H51.8024V32ZM52.7676 20.6669C53.426 20.6669 53.9757 20.1555 53.9757 19.5291C53.9757 18.9027 53.426 18.3849 52.7676 18.3849C52.1028 18.3849 51.5595 18.9027 51.5595 19.5291C51.5595 20.1555 52.1028 20.6669 52.7676 20.6669ZM58.196 18.9091H56.2848V32H58.196V18.9091ZM63.6245 32.2173C65.248 32.2173 66.1621 31.3928 66.5265 30.6577H66.6032V32H68.4696V25.4801C68.4696 22.6229 66.2196 22.054 64.66 22.054C62.883 22.054 61.2466 22.7699 60.6074 24.5597L62.4036 24.9688C62.6848 24.272 63.4007 23.6009 64.6855 23.6009C65.9192 23.6009 66.552 24.2464 66.552 25.3587V25.4034C66.552 26.1001 65.8361 26.0874 64.0719 26.2919C62.2118 26.5092 60.307 26.995 60.307 29.2259C60.307 31.1562 61.758 32.2173 63.6245 32.2173ZM64.04 30.6832C62.9597 30.6832 62.1799 30.1974 62.1799 29.2514C62.1799 28.2287 63.0875 27.8643 64.1934 27.7173C64.8134 27.6342 66.2836 27.468 66.5584 27.1932V28.4588C66.5584 29.6222 65.6316 30.6832 64.04 30.6832ZM71.1687 32H73.0352V30.4723H73.195C73.5401 31.0987 74.2433 32.1918 76.033 32.1918C78.4109 32.1918 80.1367 30.2869 80.1367 27.1101C80.1367 23.9268 78.3853 22.054 76.0138 22.054C74.1921 22.054 73.5337 23.1662 73.195 23.7734H73.0799V18.9091H71.1687V32ZM73.0415 27.0909C73.0415 25.0391 73.9364 23.6776 75.6048 23.6776C77.337 23.6776 78.2063 25.1413 78.2063 27.0909C78.2063 29.0597 77.3114 30.5618 75.6048 30.5618C73.962 30.5618 73.0415 29.1555 73.0415 27.0909ZM84.1941 18.9091H82.2828V32H84.1941V18.9091ZM90.9968 32.1982C93.1381 32.1982 94.6531 31.1435 95.0877 29.5455L93.2788 29.2195C92.9336 30.1463 92.1026 30.6193 91.016 30.6193C89.3796 30.6193 88.2802 29.5582 88.229 27.6662H95.2092V26.9886C95.2092 23.4411 93.087 22.054 90.8626 22.054C88.1268 22.054 86.3242 24.1378 86.3242 27.1548C86.3242 30.2038 88.1012 32.1982 90.9968 32.1982ZM88.2354 26.2344C88.3121 24.8409 89.3221 23.6328 90.8754 23.6328C92.3583 23.6328 93.3299 24.7322 93.3363 26.2344H88.2354ZM106.766 22.1818H104.65V21.2997C104.65 20.4304 105.008 19.9574 105.941 19.9574C106.338 19.9574 106.619 20.0469 106.798 20.1044L107.245 18.5575C106.977 18.4553 106.421 18.2955 105.647 18.2955C104.094 18.2955 102.733 19.2031 102.733 21.044V22.1818H101.218V23.7159H102.733V32H104.65V23.7159H106.766V22.1818ZM112.487 32.1982C115.255 32.1982 117.064 30.1719 117.064 27.1357C117.064 24.0803 115.255 22.054 112.487 22.054C109.719 22.054 107.91 24.0803 107.91 27.1357C107.91 30.1719 109.719 32.1982 112.487 32.1982ZM112.493 30.5938C110.684 30.5938 109.841 29.0149 109.841 27.1293C109.841 25.25 110.684 23.652 112.493 23.652C114.289 23.652 115.133 25.25 115.133 27.1293C115.133 29.0149 114.289 30.5938 112.493 30.5938ZM119.197 32H121.108V26.0043C121.108 24.7195 122.099 23.7926 123.454 23.7926C123.85 23.7926 124.298 23.8629 124.451 23.9077V22.0795C124.259 22.054 123.882 22.0348 123.639 22.0348C122.489 22.0348 121.504 22.6868 121.146 23.7415H121.044V22.1818H119.197V32Z\" fill=\"currentColor\"/>\n<path d=\"M172.18 26.707C172.18 25.4062 172.777 24.457 173.938 23.7188C173.27 22.7695 172.285 22.2773 170.984 22.1719C169.719 22.0664 168.348 22.875 167.855 22.875C167.328 22.875 166.133 22.207 165.184 22.207C163.215 22.2422 161.141 23.7539 161.141 26.8828C161.141 27.7969 161.281 28.7461 161.633 29.7305C162.09 31.0312 163.707 34.1953 165.395 34.125C166.273 34.125 166.906 33.4922 168.066 33.4922C169.191 33.4922 169.754 34.125 170.738 34.125C172.461 34.125 173.938 31.2422 174.359 29.9414C172.074 28.8516 172.18 26.7773 172.18 26.707ZM170.211 20.9414C171.16 19.8164 171.055 18.7617 171.055 18.375C170.211 18.4453 169.227 18.9727 168.664 19.6055C168.031 20.3086 167.68 21.1875 167.75 22.1367C168.664 22.207 169.508 21.75 170.211 20.9414Z\" fill=\"currentColor\"/>\n<path d=\"M135 20.5547V25.8984H141.434V19.6758L135 20.5547ZM135 31.9805L141.434 32.8594V26.707H135V31.9805ZM142.137 32.9648L150.75 34.125V26.707H142.137V32.9648ZM142.137 19.5703V25.8984H150.75V18.375L142.137 19.5703Z\" fill=\"currentColor\"/>\n<path d=\"M193.719 17.5312C188.902 17.5312 185 21.4336 185 26.25C185 31.0664 188.902 34.9688 193.719 34.9688C198.535 34.9688 202.438 31.0664 202.438 26.25C202.438 21.4336 198.535 17.5312 193.719 17.5312ZM195.547 20.8008C195.863 20.2734 196.566 20.0977 197.094 20.4141C197.621 20.7305 197.797 21.3984 197.48 21.9258C197.199 22.4883 196.496 22.6641 195.969 22.3477C195.441 22.0312 195.23 21.3633 195.547 20.8008ZM188.059 27.375C187.426 27.375 186.934 26.8828 186.934 26.25C186.934 25.6523 187.426 25.1602 188.059 25.1602C188.691 25.1602 189.184 25.6523 189.184 26.25C189.184 26.8828 188.691 27.375 188.059 27.375ZM189.043 27.4805C189.816 26.8828 189.816 25.6875 189.043 25.0547C189.359 23.8945 190.062 22.9102 191.047 22.2422L191.855 23.6484C190.062 24.9141 190.062 27.6211 191.855 28.8867L191.047 30.2578C190.062 29.625 189.359 28.6406 189.043 27.4805ZM197.094 32.1211C196.531 32.4375 195.863 32.2617 195.547 31.6992C195.23 31.1719 195.441 30.5039 195.969 30.1875C196.496 29.8711 197.199 30.0469 197.48 30.6094C197.797 31.1367 197.621 31.8047 197.094 32.1211ZM197.094 29.6953C196.145 29.3086 195.125 29.9062 194.984 30.9258C194.773 30.9609 193.262 31.418 191.574 30.5742L192.348 29.168C194.352 30.082 196.707 28.7461 196.883 26.5664H198.5C198.43 27.7969 197.902 28.8867 197.094 29.6953ZM196.883 25.9688C196.707 23.7891 194.387 22.418 192.348 23.3672L191.574 21.9609C193.262 21.1172 194.773 21.5742 194.949 21.6094C195.125 22.6289 196.145 23.2266 197.094 22.8398C197.902 23.6484 198.43 24.7383 198.5 25.9688H196.883Z\" fill=\"currentColor\"/>\n<path d=\"M220.91 18.375C216.551 18.375 213 21.8906 213 26.25V32.2266C212.965 32.2266 212.965 32.2266 212.965 32.2617C212.965 33.2812 213.844 34.125 214.863 34.125H220.84C225.199 34.1602 228.75 30.6445 228.75 26.2852C228.75 21.9258 225.234 18.4102 220.91 18.375ZM226.852 23.9297L224.707 21.7148C224.777 21.5391 224.812 21.3984 224.812 21.2227V21.1523L226.781 23.1211C226.816 23.4023 226.852 23.6484 226.852 23.9297ZM224.637 20.5898C225.586 21.0117 226.324 21.8203 226.676 22.8398L224.777 20.9062C224.742 20.8008 224.672 20.6953 224.637 20.5898ZM217.148 25.9688C217.043 26.0742 216.938 26.2148 216.867 26.3555L216.551 26.0742C216.762 26.0039 216.938 25.9688 217.148 25.9688ZM216.41 26.1094L216.797 26.5312L216.762 26.8125C216.762 26.9531 216.797 27.0938 216.867 27.2344L215.918 26.2852C216.059 26.2148 216.234 26.1445 216.41 26.1094ZM215.742 26.3555L217.113 27.7266C216.938 27.7617 216.762 27.7969 216.586 27.8672L215.355 26.6016C215.496 26.5312 215.602 26.4258 215.742 26.3555ZM215.215 26.707L216.445 27.9727C216.34 28.043 216.199 28.1484 216.094 28.2539L214.863 27.0234C214.969 26.918 215.074 26.8125 215.215 26.707ZM214.758 27.1289L215.988 28.3594C215.883 28.5 215.777 28.6406 215.707 28.7812L214.441 27.5156C214.547 27.375 214.652 27.2695 214.758 27.1289ZM214.371 27.6562L215.637 28.9219C215.566 29.0977 215.531 29.2734 215.531 29.4492L214.125 28.0781C214.195 27.9375 214.266 27.7969 214.371 27.6562ZM214.055 28.2188L215.496 29.6953C215.531 30.0117 215.602 30.3281 215.742 30.6094L213.879 28.7109C213.949 28.5703 213.984 28.3945 214.055 28.2188ZM213.773 29.7305L215.953 31.9102C215.883 32.0508 215.812 32.2266 215.812 32.4023V32.4727L213.879 30.5039C213.809 30.2578 213.773 30.0117 213.773 29.7305ZM213.949 30.8203L215.883 32.7188C215.918 32.8594 215.953 32.9648 216.023 33.0352C215.039 32.6484 214.301 31.8047 213.949 30.8203ZM213.773 29.5195C213.773 29.3086 213.809 29.0977 213.844 28.9219L216.375 31.4531C216.27 31.5586 216.129 31.6289 216.059 31.7695L213.773 29.5195ZM222.668 27.6914H221.191V29.6602C221.191 32.0859 218.906 33.7383 216.762 33.2812C216.551 33.2812 216.059 32.9648 216.059 32.4023C216.059 31.9453 216.445 31.5586 216.938 31.5586C217.148 31.5586 217.148 31.5938 217.465 31.5938C218.555 31.5938 219.434 30.7148 219.434 29.6602L219.469 28.0078C219.469 27.832 219.293 27.6914 219.152 27.6914L217.957 27.6562C216.797 27.6562 216.832 25.9336 217.957 25.9336H219.469V23.9648C219.469 21.9258 221.121 20.2734 223.16 20.2734C223.195 20.2734 223.195 20.2734 223.195 20.2734C223.441 20.2734 223.652 20.3086 223.898 20.3789C224.285 20.4141 224.602 20.7656 224.602 21.2227C224.602 21.75 224.074 22.1719 223.512 22.0312C222.562 21.8555 221.191 22.5586 221.191 23.9648V25.6172C221.191 25.793 221.332 25.9336 221.508 25.9336H222.703C223.828 25.9688 223.828 27.6914 222.668 27.6914ZM223.512 27.6914C223.617 27.5508 223.723 27.4102 223.793 27.2695L224.074 27.5859C223.898 27.6211 223.688 27.6562 223.512 27.6914ZM224.25 27.5156L223.828 27.0938L223.863 26.8125C223.863 26.6719 223.828 26.5312 223.793 26.3906L224.742 27.3398C224.566 27.4102 224.426 27.4805 224.25 27.5156ZM224.883 27.2695L223.547 25.9336C223.723 25.8633 223.898 25.8281 224.039 25.7578L225.305 27.0234C225.164 27.0938 225.023 27.1992 224.883 27.2695ZM225.41 26.918L224.18 25.6875C224.32 25.582 224.426 25.4766 224.566 25.3711L225.797 26.6016C225.656 26.707 225.551 26.8125 225.41 26.918ZM225.902 26.4961L224.672 25.2656C224.777 25.125 224.848 25.0195 224.918 24.8438L226.184 26.1094C226.078 26.25 226.008 26.3906 225.902 26.4961ZM226.289 26.0039L224.988 24.7031C225.059 24.5273 225.094 24.3516 225.129 24.1758L226.5 25.582C226.43 25.7227 226.359 25.8633 226.289 26.0039ZM226.746 24.9141C226.711 25.0898 226.641 25.2305 226.57 25.4062L225.129 23.9297C225.129 23.6133 225.023 23.2969 224.883 23.0156L226.746 24.9141ZM226.781 24.7031L224.25 22.1719C224.391 22.1016 224.496 21.9961 224.602 21.8555L226.852 24.1055C226.852 24.3164 226.816 24.5273 226.781 24.7031Z\" fill=\"currentColor\"/>\n<path d=\"M249.16 20.6953H247.578V24.4922L250.285 21.8203L249.16 20.6953ZM243.465 21.8203L246.137 24.4922V20.6953H244.555L243.465 21.8203ZM244.906 20.3438H246.488V24.8438L246.875 25.2305L247.262 24.8438V20.3438H248.809L246.875 18.375L244.906 20.3438ZM245.82 26.25L245.469 25.8633H240.934V24.3164L239 26.25L240.934 28.2188V26.6367H245.469L245.82 26.25ZM248.633 25.5117H252.43V23.9648L251.305 22.8398L248.633 25.5117ZM254.715 26.25L252.781 24.3164V25.8633H248.281L247.895 26.25L248.281 26.6367H252.781V28.2188L254.715 26.25ZM241.285 23.4727L242.41 22.3477L245.609 25.5117H246.137V24.9844L242.973 21.8203L244.062 20.6953H241.285V23.4727ZM252.43 20.6953H249.652L250.777 21.8203L247.578 24.9844V25.5117H248.141L251.305 22.3477L252.43 23.4727V20.6953ZM246.137 31.8047V28.0078L243.465 30.7148L244.555 31.8047H246.137ZM241.285 25.5117H245.117L242.41 22.8398L241.285 23.9648V25.5117ZM252.43 29.0625L251.305 30.1523L248.141 26.9883H247.578V27.5156L250.777 30.7148L249.652 31.8047H252.43V29.0625ZM252.43 26.9883H248.633L251.305 29.6602L252.43 28.5703V26.9883ZM250.285 30.7148L247.578 28.0078V31.8047H249.16L250.285 30.7148ZM242.41 29.6602L245.117 26.9883H241.285V28.5703L242.41 29.6602ZM248.809 32.1562H247.262V27.6562L246.875 27.3047L246.488 27.6562V32.1562H244.906L246.875 34.125L248.809 32.1562ZM242.973 30.7148L246.137 27.5156V26.9883H245.609L242.41 30.1523L241.285 29.0625V31.8047H244.062L242.973 30.7148Z\" fill=\"currentColor\"/>\n</svg>\n\nAeon is available for Windows, macOS, apt and yum! Download the application here and follow the instructions on [the download page](https://aeon.technology/download).\n\n[![Download Aeon](./docs/.gitbook/assets/download-button.svg)](https://aeon.technology/download)\n\nNot sure how to start using Aeon? Follow the [Getting Started guide](https://docs.aeon.technology/using-aeon/getting-started) for detailed instructions.\n\n# How does it work?\nAll companies worldwide are required to offer you access to all data they retain about you. But most of the time, this process is hard, convoluted, slow or all three at once! Aeon has rules for each platform that make requesting your personal information just a couple of clicks!\n\n<p align=\"center\">\n    <img src=\"./docs/.gitbook/assets/timeline.png\" width=\"600\" />\n</p>\n\nThe resulting data is downloaded on your local computer for safekeeping. Often, data is formatted in machine-readable formats such as JSON and CSV. To make it easier to digest your personal information, Aeon visualises it for you.\n\n<p align=\"center\">\n    <img src=\"./docs/.gitbook/assets/graph.png\" width=\"600\" />\n</p>\n\nCompanies are not only required to grant access, they must respect your wishes too. This means you can require them to delete or modify the personal information they have. Aeon contains a generator for data subject rights emails, that help you take control of your personal data.\n\n<p align=\"center\">\n    <img src=\"./docs/.gitbook/assets/erasure.png\" width=\"600\" />\n</p>\n\n# Supported Platforms\n<svg width=\"363\" height=\"50\" viewBox=\"0 0 363 50\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M31.3111 23.1662C30.8636 20.3665 28.6712 18.7301 25.9034 18.7301C22.5156 18.7301 20.0163 21.2678 20.0163 25.4545C20.0163 29.6413 22.5028 32.179 25.9034 32.179C28.7798 32.179 30.8828 30.3764 31.3111 27.7876L29.3168 27.7812C28.978 29.456 27.5717 30.3764 25.9162 30.3764C23.6726 30.3764 21.9787 28.657 21.9787 25.4545C21.9787 22.2777 23.6662 20.5327 25.9226 20.5327C27.5909 20.5327 28.9908 21.4723 29.3168 23.1662H31.3111ZM39.7406 27.9283C39.747 29.5966 38.5069 30.3892 37.4331 30.3892C36.2505 30.3892 35.4324 29.5327 35.4324 28.1967V22.1818H33.5211V28.4268C33.5211 30.8622 34.8571 32.1278 36.7427 32.1278C38.2193 32.1278 39.2228 31.348 39.6767 30.2997H39.7789V32H41.6582V22.1818H39.7406V27.9283ZM44.2262 32H46.1374V26.0043C46.1374 24.7195 47.1282 23.7926 48.4833 23.7926C48.8796 23.7926 49.3271 23.8629 49.4805 23.9077V22.0795C49.2887 22.054 48.9116 22.0348 48.6687 22.0348C47.5181 22.0348 46.5337 22.6868 46.1758 23.7415H46.0735V22.1818H44.2262V32ZM51.1344 32H53.0456V26.0043C53.0456 24.7195 54.0364 23.7926 55.3915 23.7926C55.7878 23.7926 56.2353 23.8629 56.3887 23.9077V22.0795C56.1969 22.054 55.8198 22.0348 55.5769 22.0348C54.4263 22.0348 53.4419 22.6868 53.084 23.7415H52.9817V22.1818H51.1344V32ZM61.9226 32.1982C64.0639 32.1982 65.5788 31.1435 66.0135 29.5455L64.2045 29.2195C63.8594 30.1463 63.0284 30.6193 61.9418 30.6193C60.3054 30.6193 59.206 29.5582 59.1548 27.6662H66.1349V26.9886C66.1349 23.4411 64.0128 22.054 61.7884 22.054C59.0526 22.054 57.25 24.1378 57.25 27.1548C57.25 30.2038 59.027 32.1982 61.9226 32.1982ZM59.1612 26.2344C59.2379 24.8409 60.2479 23.6328 61.8011 23.6328C63.2841 23.6328 64.2557 24.7322 64.2621 26.2344H59.1612ZM70.1667 26.1705C70.1667 24.6044 71.1255 23.7095 72.4551 23.7095C73.7527 23.7095 74.5389 24.5597 74.5389 25.9851V32H76.4501V25.755C76.4501 23.326 75.1142 22.054 73.1071 22.054C71.6305 22.054 70.6653 22.7379 70.2115 23.7798H70.09V22.1818H68.2555V32H70.1667V26.1705ZM83.5964 22.1818H81.5829V19.8295H79.6717V22.1818H78.2335V23.7159H79.6717V29.5135C79.6653 31.2969 81.0268 32.1598 82.5353 32.1278C83.1426 32.1214 83.5517 32.0064 83.7754 31.9233L83.4302 30.3445C83.3024 30.37 83.0659 30.4276 82.7591 30.4276C82.139 30.4276 81.5829 30.223 81.5829 29.1172V23.7159H83.5964V22.1818ZM87.8327 18.9091H85.9215V32H87.8327V18.9091ZM91.612 35.6818C93.1909 35.6818 94.188 34.8572 94.7569 33.3104L98.8159 22.201L96.7512 22.1818L94.2647 29.8011H94.1625L91.676 22.1818H89.6305L93.2228 32.1278L92.9863 32.7798C92.5005 34.0838 91.8166 34.1925 90.7683 33.9048L90.3081 35.4709C90.5382 35.5732 91.0368 35.6818 91.612 35.6818ZM112.738 24.5788C112.341 23.0511 111.146 22.054 109.024 22.054C106.806 22.054 105.233 23.2237 105.233 24.9624C105.233 26.3558 106.077 27.2827 107.918 27.6918L109.58 28.0561C110.526 28.267 110.967 28.6889 110.967 29.3026C110.967 30.0632 110.155 30.6577 108.903 30.6577C107.758 30.6577 107.023 30.1655 106.793 29.2003L104.946 29.4815C105.265 31.2202 106.71 32.1982 108.915 32.1982C111.287 32.1982 112.93 30.9389 112.93 29.1619C112.93 27.7749 112.047 26.9183 110.245 26.5028L108.685 26.1449C107.605 25.8892 107.138 25.5249 107.145 24.8601C107.138 24.1058 107.956 23.5689 109.043 23.5689C110.232 23.5689 110.782 24.2273 111.006 24.8857L112.738 24.5788ZM121.25 27.9283C121.257 29.5966 120.017 30.3892 118.943 30.3892C117.76 30.3892 116.942 29.5327 116.942 28.1967V22.1818H115.031V28.4268C115.031 30.8622 116.367 32.1278 118.252 32.1278C119.729 32.1278 120.733 31.348 121.186 30.2997H121.289V32H123.168V22.1818H121.25V27.9283ZM125.736 35.6818H127.647V30.4723H127.762C128.107 31.0987 128.811 32.1918 130.6 32.1918C132.978 32.1918 134.704 30.2869 134.704 27.1101C134.704 23.9268 132.953 22.054 130.581 22.054C128.759 22.054 128.101 23.1662 127.762 23.7734H127.602V22.1818H125.736V35.6818ZM127.609 27.0909C127.609 25.0391 128.504 23.6776 130.172 23.6776C131.904 23.6776 132.774 25.1413 132.774 27.0909C132.774 29.0597 131.879 30.5618 130.172 30.5618C128.529 30.5618 127.609 29.1555 127.609 27.0909ZM136.845 35.6818H138.757V30.4723H138.872C139.217 31.0987 139.92 32.1918 141.71 32.1918C144.088 32.1918 145.813 30.2869 145.813 27.1101C145.813 23.9268 144.062 22.054 141.691 22.054C139.869 22.054 139.21 23.1662 138.872 23.7734H138.712V22.1818H136.845V35.6818ZM138.718 27.0909C138.718 25.0391 139.613 23.6776 141.281 23.6776C143.014 23.6776 143.883 25.1413 143.883 27.0909C143.883 29.0597 142.988 30.5618 141.281 30.5618C139.639 30.5618 138.718 29.1555 138.718 27.0909ZM152.09 32.1982C154.858 32.1982 156.667 30.1719 156.667 27.1357C156.667 24.0803 154.858 22.054 152.09 22.054C149.323 22.054 147.514 24.0803 147.514 27.1357C147.514 30.1719 149.323 32.1982 152.09 32.1982ZM152.097 30.5938C150.288 30.5938 149.444 29.0149 149.444 27.1293C149.444 25.25 150.288 23.652 152.097 23.652C153.893 23.652 154.737 25.25 154.737 27.1293C154.737 29.0149 153.893 30.5938 152.097 30.5938ZM158.8 32H160.712V26.0043C160.712 24.7195 161.702 23.7926 163.058 23.7926C163.454 23.7926 163.901 23.8629 164.055 23.9077V22.0795C163.863 22.054 163.486 22.0348 163.243 22.0348C162.092 22.0348 161.108 22.6868 160.75 23.7415H160.648V22.1818H158.8V32ZM170.872 22.1818H168.858V19.8295H166.947V22.1818H165.509V23.7159H166.947V29.5135C166.941 31.2969 168.302 32.1598 169.811 32.1278C170.418 32.1214 170.827 32.0064 171.051 31.9233L170.706 30.3445C170.578 30.37 170.341 30.4276 170.034 30.4276C169.414 30.4276 168.858 30.223 168.858 29.1172V23.7159H170.872V22.1818ZM172.986 32H174.897V22.1818H172.986V32ZM173.951 20.6669C174.61 20.6669 175.159 20.1555 175.159 19.5291C175.159 18.9027 174.61 18.3849 173.951 18.3849C173.286 18.3849 172.743 18.9027 172.743 19.5291C172.743 20.1555 173.286 20.6669 173.951 20.6669ZM179.38 26.1705C179.38 24.6044 180.338 23.7095 181.668 23.7095C182.966 23.7095 183.752 24.5597 183.752 25.9851V32H185.663V25.755C185.663 23.326 184.327 22.054 182.32 22.054C180.843 22.054 179.878 22.7379 179.424 23.7798H179.303V22.1818H177.468V32H179.38V26.1705ZM192.343 35.8864C194.842 35.8864 196.772 34.7422 196.772 32.2173V22.1818H194.9V23.7734H194.759C194.42 23.1662 193.743 22.054 191.914 22.054C189.543 22.054 187.798 23.9268 187.798 27.0526C187.798 30.1847 189.581 31.853 191.902 31.853C193.704 31.853 194.401 30.8366 194.746 30.2102H194.868V32.1406C194.868 33.6811 193.813 34.3459 192.362 34.3459C190.77 34.3459 190.15 33.5469 189.811 32.9844L188.169 33.6619C188.686 34.8636 189.997 35.8864 192.343 35.8864ZM192.324 30.2678C190.617 30.2678 189.728 28.9574 189.728 27.027C189.728 25.1413 190.598 23.6776 192.324 23.6776C193.992 23.6776 194.887 25.0391 194.887 27.027C194.887 29.0533 193.973 30.2678 192.324 30.2678Z\" fill=\"currentColor\"/>\n<path d=\"M230.719 25.25C230.719 20.4336 226.816 16.5312 222 16.5312C217.184 16.5312 213.281 20.4336 213.281 25.25C213.281 29.6094 216.445 33.2305 220.629 33.8633V27.7812H218.414V25.25H220.629V23.3516C220.629 21.1719 221.93 19.9414 223.898 19.9414C224.883 19.9414 225.867 20.1172 225.867 20.1172V22.2617H224.777C223.688 22.2617 223.336 22.9297 223.336 23.6328V25.25H225.762L225.375 27.7812H223.336V33.8633C227.52 33.2305 230.719 29.6094 230.719 25.25Z\" fill=\"currentColor\"/>\n<path d=\"M255.625 17.375H242.09C241.492 17.375 241 17.9023 241 18.5352V32C241 32.6328 241.492 33.125 242.09 33.125H255.625C256.223 33.125 256.75 32.6328 256.75 32V18.5352C256.75 17.9023 256.223 17.375 255.625 17.375ZM245.746 30.875H243.426V23.3867H245.746V30.875ZM244.586 22.332C243.812 22.332 243.215 21.7344 243.215 20.9961C243.215 20.2578 243.812 19.625 244.586 19.625C245.324 19.625 245.922 20.2578 245.922 20.9961C245.922 21.7344 245.324 22.332 244.586 22.332ZM254.5 30.875H252.145V27.2188C252.145 26.375 252.145 25.25 250.949 25.25C249.719 25.25 249.543 26.1992 249.543 27.1836V30.875H247.223V23.3867H249.438V24.4062H249.473C249.789 23.8086 250.562 23.1758 251.688 23.1758C254.043 23.1758 254.5 24.7578 254.5 26.7617V30.875Z\" fill=\"currentColor\"/>\n<path d=\"M275.719 16.5312C270.902 16.5312 267 20.4688 267 25.25C267 30.0664 270.902 33.9688 275.719 33.9688C280.5 33.9688 284.438 30.0664 284.438 25.25C284.438 20.4688 280.5 16.5312 275.719 16.5312ZM279.234 29.3633C279.094 29.3633 278.988 29.3281 278.883 29.2578C276.668 27.9219 274.137 27.8867 271.605 28.3789C271.465 28.4141 271.289 28.4844 271.184 28.4844C270.832 28.4844 270.621 28.2031 270.621 27.9219C270.621 27.5703 270.832 27.3945 271.113 27.3242C273.996 26.6914 276.914 26.7617 279.445 28.2383C279.656 28.3789 279.762 28.5195 279.762 28.8359C279.762 29.1523 279.516 29.3633 279.234 29.3633ZM280.184 27.0781C280.008 27.0781 279.867 26.9727 279.762 26.9375C277.547 25.6367 274.277 25.1094 271.359 25.8828C271.184 25.918 271.113 25.9883 270.938 25.9883C270.586 25.9883 270.27 25.6719 270.27 25.2852C270.27 24.9336 270.445 24.6875 270.797 24.582C271.781 24.3008 272.801 24.0898 274.242 24.0898C276.527 24.0898 278.742 24.6523 280.465 25.707C280.746 25.8477 280.887 26.0938 280.887 26.375C280.852 26.7617 280.57 27.0781 280.184 27.0781ZM281.273 24.4062C281.098 24.4062 280.992 24.3359 280.816 24.2656C278.32 22.7539 273.855 22.4023 270.938 23.2109C270.832 23.2461 270.656 23.3164 270.48 23.3164C270.023 23.3164 269.672 22.9297 269.672 22.4727C269.672 21.9805 269.988 21.7344 270.305 21.6289C271.535 21.2773 272.906 21.1016 274.418 21.1016C276.984 21.1016 279.691 21.6289 281.625 22.7891C281.906 22.9297 282.082 23.1406 282.082 23.5625C282.082 24.0547 281.695 24.4062 281.273 24.4062Z\" fill=\"currentColor\"/>\n<path d=\"M302.875 21.207C300.625 21.207 298.832 23.0352 298.832 25.25C298.832 27.5 300.625 29.293 302.875 29.293C305.09 29.293 306.918 27.5 306.918 25.25C306.918 23.0352 305.09 21.207 302.875 21.207ZM302.875 27.8867C301.434 27.8867 300.238 26.7266 300.238 25.25C300.238 23.8086 301.398 22.6484 302.875 22.6484C304.316 22.6484 305.477 23.8086 305.477 25.25C305.477 26.7266 304.316 27.8867 302.875 27.8867ZM308.008 21.0664C308.008 20.5391 307.586 20.1172 307.059 20.1172C306.531 20.1172 306.109 20.5391 306.109 21.0664C306.109 21.5938 306.531 22.0156 307.059 22.0156C307.586 22.0156 308.008 21.5938 308.008 21.0664ZM310.68 22.0156C310.609 20.75 310.328 19.625 309.414 18.7109C308.5 17.7969 307.375 17.5156 306.109 17.4453C304.809 17.375 300.906 17.375 299.605 17.4453C298.34 17.5156 297.25 17.7969 296.301 18.7109C295.387 19.625 295.105 20.75 295.035 22.0156C294.965 23.3164 294.965 27.2188 295.035 28.5195C295.105 29.7852 295.387 30.875 296.301 31.8242C297.25 32.7383 298.34 33.0195 299.605 33.0898C300.906 33.1602 304.809 33.1602 306.109 33.0898C307.375 33.0195 308.5 32.7383 309.414 31.8242C310.328 30.875 310.609 29.7852 310.68 28.5195C310.75 27.2188 310.75 23.3164 310.68 22.0156ZM308.992 29.8906C308.746 30.5938 308.184 31.1211 307.516 31.4023C306.461 31.8242 304 31.7188 302.875 31.7188C301.715 31.7188 299.254 31.8242 298.234 31.4023C297.531 31.1211 297.004 30.5938 296.723 29.8906C296.301 28.8711 296.406 26.4102 296.406 25.25C296.406 24.125 296.301 21.6641 296.723 20.6094C297.004 19.9414 297.531 19.4141 298.234 19.1328C299.254 18.7109 301.715 18.8164 302.875 18.8164C304 18.8164 306.461 18.7109 307.516 19.1328C308.184 19.3789 308.711 19.9414 308.992 20.6094C309.414 21.6641 309.309 24.125 309.309 25.25C309.309 26.4102 309.414 28.8711 308.992 29.8906Z\" fill=\"currentColor\"/>\n<path d=\"M334.219 23.4062H328.594V17.7812C328.594 17.6406 328.453 17.5 328.312 17.5H327.188C327.012 17.5 326.906 17.6406 326.906 17.7812V23.4062H321.281C321.105 23.4062 321 23.5469 321 23.6875V24.8125C321 24.9883 321.105 25.0938 321.281 25.0938H326.906V30.7188C326.906 30.8945 327.012 31 327.188 31H328.312C328.453 31 328.594 30.8945 328.594 30.7188V25.0938H334.219C334.359 25.0938 334.5 24.9883 334.5 24.8125V23.6875C334.5 23.5469 334.359 23.4062 334.219 23.4062Z\" fill=\"currentColor\"/>\n</svg>\n\n\nAeon currently has native support for the following platforms:\n* Facebook\n* Instagram\n* LinkedIn\n* Spotify\n* ...more coming soon\n\nWant to see a particular platform added? [Create a GitHub issue](https://github.com/leinelissen/aeon/issues/new/choose) with the name of the particular provider. \n\nWant to help out with adding new platforms? Providers are easily defined with a Provider config. [Check out the documentation on Providers](https://docs.aeon.technology/extending-aeon/architecture/providers) to find out how they work. You can always create a Pull Request \n\n# Contributing\nAeon is being developed out in the open. Have an idea for a feature or a suggestion for a new provider? [Create a GitHub issue](https://github.com/leinelissen/aeon/issues/new/choose) and tag me (@leinelissen) if you need any help.\n\n# Documentation\n[![Read the documentation](./docs/.gitbook/assets/documentation-button.svg)](https://aeon.technology/download)\n\n## Using Aeon\nYou can find the latest build of Aeon over at the [releases page](https://github.com/leinelissen/aeon/releases). There's builds for Windows, macOS and Linux. \n\nIf you're feeling more adventurous, clone the repository and compile your own nightly build. The only dependency is [NodeJS](https://nodejs.org/en/download/package-manager/). Prepare the codebase and start a development build by running the following commands: \n```\nnpm install\nnpm start\n```\n\n## The Technical Stuff\nAeon is an [Electron](https://www.electronjs.org/)-based app, a mature platform for building JavaScript applications on the desktop. It is backed by a locally encrypted Git repository, made available through use of the excellent [nodegit](https://www.nodegit.org/) package.\n\nA custom and modular back-end allows for tracking and retrieving data from multiple sources. This is done through retrieval from an API, asynchronous data requests or a combination of both. Parser logic then allows for extracting common data types from the resulting JSON or CSV. "
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nAeon is currently in open beta. Until a v1 is released, only the latest version (as per GitHub releases) is supported. No official support is offered for self-compiled Aeon versions, but any questions are allowed to be posted to the GitHub Issues.\n## Reporting a Vulnerability\n\nIn case you find a vulnerability in Aeon, please contact [lei@codified.nl](mailto:lei@codified.nl). You can find a PGP key at the bottom of this file.\n\nAs soon as your report is received, we will respond to your report within 7 days, indicating necessary changes and required time for addressing the vulnerability. We kindly ask you to not share any details until a fix adressing the issue has been released.\n\n\n<details>\n<summary>GPG Key (lei@codified.nl)</summary>\n\n```\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFgrN6gBEADDyFkpAlIkLFC19FD3K9At1liZUbrcxGdxpsihbd9di/15wTnY\nxMJgAQRZCZ68AXUqkmJaxEgNonfItSOcR8M+kBViNeOj+7hRUAy04QzjCleNDQ2Z\nY6tfo7/b1uTbwkq7tWcMLP3MKQHuLLq8JQywWbq1hNKH3Ap7kJatO7UOyA0aL7lM\n2Vo8fjRLdenQizJryrA6BqCglmSz/kKcbvOgES6fPMP2MllVxLM1wQDAhyv8VsKM\nX5Fjts2vBacGDxafIRVQzadE98fSKg0nlndY+ITRsV0dtNeN7rnqsuPIADLHpDDO\ntJ0H3haC4o/AuE01UtwhveLyu6RiMqEVvCGohLKPSCVbqvhQ+MDlH5xpHtI49fF6\ndWLdAgMuTEynGiqU/TZR9LpYXdK6uRPQW4VEebRyo6oP7bDFh9uVlhRZnXc3ypxe\nYy2Y1wxzAOv5dSFCH8jxoUWGIOL0YDPjCjoYR9GtujFkKQt5Y5BRiiXfY7vYVpFd\nNorLJbvj6+iXzQIA4uoPLC31SETlMy3toJnWSIjEe9dp/6TVXzyuApFBa00lm6pz\nzgz2nyDrtSE8+44LjHbe3m6w6Wtet2B2jKFOvfnqfS7QzQIyiJhufNOoXDkLrZV8\nRoKQg0DUtfxCKR7LDYk6GSWmp55OOGF/0z3N3wBfFOc00XMVEXE/3t8HtQARAQAB\ntB5MZWkgTmVsaXNzZW4gPGxlaUBjb2RpZmllZC5ubD6JAjkEEwEIACMFAlgrN6gC\nGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRAln6nAUV1QX/oNEAC9o/aX\nCbWzErt6UmHGFI1lWcvFeGDJnTP7ZppFnY6Ru0zxjYrNs1tXjROxsUZoTA1Z8H3Y\nFTSK27HT0d0MtCAMfwzWaUvfOTKMeoTZY8Tn6XLV6gHSlnCadTfmd7Os+V7qy2xA\nMF/OKTnskQsObJnYxMuungHMC6XFzE895OKnGl+uhrJyZ8Pw475bODVbIyIFLuJc\n+Tm/Ty9euL/EwDfw6CW3S7FBQH+gAP+CN0wF6efRfYICvRIaFsPwJkeWuWyfKfCL\ncptXakLi+RwK/f71nf/pX+uEe4xLPHAQWTeIWPKu4Z4kIkDU0Wesd5V9MCFZ9U4t\nSVbAL634gGz88x6UVKtFhG1GDhU4QO4hIFBtDFescAAZ8wmfeAGoYt7BKRGk7VwS\nojNj5cpcQCMGLq2kMb6Io6A2RTw+W3BgdRJ3Qkz79k1AU/COF/48H2SCt6gNWbdm\n37RpkUInWXo6UHd7kQmwttAlbA+UXykU9YZtVlzgSCBA8Y1qdPjqlmlSJiHVHKex\noYVSZIpNmo5XSWmrMyj441UJGUaSP/ABh47H4DBSe1XVADhdNpLei0el0aCYscvp\nkkML4NdG9Jg7z2AFuMwAJEPM+Wqkio1RgaM+ppG2eKsqi1Je6kWBL+dGl6Ho9zew\nFPXDbjppPi4rn4G2KwWih/O2GkCuiTmeGlvEvbkCDQRYKzeoARAAwgE/0oUWKhHi\nLG0CDxahkxKLF7npLa+DAxtpvVgYTp34W9etN85KutqHYc6eJQmR4JGUe0oS6Pz+\nvS5lHxbMfz+7UoLbYheepjGyQ/BlU+9Xu1EoM2LVRtSVBvKgvlNOaHQWWOfjvqvf\nPWIlejKpQjm+JtGn1/1T+ogC8u3RqDdp8m0l7pfqvTJ41Di3kXtQvwlqXbMQLrHG\nzFrmyq9q7psAWUm4oEMVsBgUQBjbDrMx+IDpxldrjjhLwBjWNAB6+KF34ALjF7jY\n/0qb80t3GkLVl1D+OMiyoR7TDyHYWLyQseLEqjgfEIFjJyTeWmvZPSHFjfwZ0lKv\npPbFjh3bJNaEiFdOV1gyYLjuJCGTw2wes01qQbvh3Ovsz8gpPsRaxD2uy5+6/EvK\ndnUNKd5W66pwiAYCIf7FoLUnwSyie12CM5TvbDXDaGgnI8UQQ4UyQXCcCDL5YigM\nozvJAYuKJ6V3Aou5T+ub4y+jJOZSICWzkwtgYtzj3Jn0C01KvoK7zunkefMXzSSL\nnEAJiw/pbpDy0cKZRsq8wol4S14dwuQSXMFt0plIb2Hs4oWMmmqPg5rfKzybMpmU\nggkjiCVI5sd5KpRtkm/QVcUsS+j2DTQ4alrHJHpmILGkxTJ4ahE/+06LZyEO8wYN\n2AMEShKhy4PTiLihY4J7eoL4LFefPNEAEQEAAYkCHwQYAQgACQUCWCs3qAIbDAAK\nCRAln6nAUV1QX2rUD/4nPos5hJadjrbsj3vzx9/IBby7oXQe7sq3za0vCZpe4pmF\nNRE1GEujnDp7NdRkS7i0CLA4PI/CHAYr5ltkmJoeYI9RP+Ix2nvlykRgbPrpFbdh\n2P9FY+zF3p/7elW91/zOKQ2GQcOOYewEu5Q7zuLCfuDz7crf7pImzxs7BO974s3p\n4qL+PkOOK+YczGjvMT6Ewglz0oEdMUuvUNYhIdRsoTvSmYsMbhSMt1aKTGrkkG0g\ng3pRWrswdKMzoBvheSgFt/jdm2prQLjjmjUXqOsyiZPI787MgEv60n2rkScl1IwM\n2ia04hU644jrGwpddyD0CtisRDa/aDljRNU/0K1VYGjN2rAcdMP2vg52kpDzpAMs\n9t6HbqIFcTFKFhFrQtRy8Rt+gm6fLm164Pm0dPpTzZhrkyTWQOjp4X5TvL6uOh4L\ndnID0SlFu4iw0HHDqqynkRgABSHyo5V333bsSuuxhzhwt69tb5ipGxr+ZQ3c1e7R\nqtAERkY4Zzer2im/CW+2qg3wyB/FQWsaEy9gtfqUCUiOMcaYNQEhPtWu3YgEcJYP\nhdngamPzSpkMf5KbPbQxz8Sz2OyZJMf4vTH7VpG4RhDceY22grzGknl/NxZ4Cdqo\nk+6qAbrYL6msx9TAH2Q0HuWujyvilepHIvR8yAVjUHcoqkjyNk5bBJVAmC/Uwg==\n=MlxM\n-----END PGP PUBLIC KEY BLOCK-----\n```\n</details>\n"
  },
  {
    "path": "data/.gitignore",
    "content": "*\n!.gitignore"
  },
  {
    "path": "docs/README.md",
    "content": "# Aeon Documentation\n\n![](<.gitbook/assets/aeon-whitespace-2x (1).png>)\n\nWelcome to the Aeon documentation, which attempts to make it as easy as possible for you to use and extend the Aeon tool.\n\n![](<.gitbook/assets/schermafbeelding-2020-12-15-om-15.48.04 (1).png>)\n\n### What does Aeon do?\n\nAeon is a tool that is designed to make getting your personal data that is stored by organisations as easy as possible. You can add all the tools and organisations you work with, and Aeon will reach out to them to get your data back.\n\nOnce you do get the data, Aeon shows you what has changed in it, and give you the tools to start adding, modifying or updating said data. All made as easy as possible.\n\n![](<.gitbook/assets/schermafbeelding-2020-12-15-om-16.41.06 (1).png>)\n\n### How do I use the documentation?\n\nThis documentation is split into two sections. First, we discuss how to use Aeon as an end-user. Secondly, we discuss how Aeon can be extended as a developer, to tailor it to the use cases you desire.\n\n{% content-ref url=\"using-aeon/installation.md\" %}\n[installation.md](using-aeon/installation.md)\n{% endcontent-ref %}\n\n{% content-ref url=\"extending-aeon/local-development.md\" %}\n[local-development.md](extending-aeon/local-development.md)\n{% endcontent-ref %}\n"
  },
  {
    "path": "docs/SUMMARY.md",
    "content": "# Table of contents\n\n* [Aeon Documentation](README.md)\n\n## Using Aeon\n\n* [Installation](using-aeon/installation.md)\n* [Getting Started](using-aeon/getting-started.md)\n\n## Extending Aeon\n\n* [Local Development](extending-aeon/local-development.md)\n* [Reporting Issues](extending-aeon/reporting-issues.md)\n* [Architecture](extending-aeon/architecture/README.md)\n  * [Providers](extending-aeon/architecture/providers.md)\n"
  },
  {
    "path": "docs/extending-aeon/architecture/README.md",
    "content": "# Architecture\n\nAeon is built to reach out to various platforms, gather data, store it and then report it to the user. This page will go into detail into how these goals are worked out technically. \n\n## Technologies\n\n### TypeScript\n\nThe entire Aeon codebase is written in [TypeScript](https://www.typescriptlang.org/). TypeScript is a language that compiles to Javascript, with the additional benefit of types, describing inputs outputs and datatypes. TypeScript is not only used for a better developer experience, but the data typing system actually relies heavily on TypeScript enums and data formats.\n\n### Electron\n\nAeon is built on Electron, and leverages its integration with browsers heavily to use existing data download workflows to gather data. Electronc consists of a NodeJS back-end \\(called `main` in Electron parlance and the Aeon codebase\\) coupled with a Chromium front-end \\(dito, called `app`\\). You'll find that the codebase is neatly seperated along these lines, with the `src/main` folder containg all back-end code and the `src/app` folder containing all front-end code.\n\nFollowing Electron best-practices, these environments run seperately and are sandboxes, to prevent external webpages from getting system-level access through NodeJS. This means that communication between the front-end and back-end happens through a set of [bridges](https://www.electronjs.org/docs/api/context-bridge) \\(for example, see the [RepositoryBridge](https://github.com/leinelissen/aeon/blob/master/src/main/lib/repository/bridge.ts) and the app-side [Repository utility](https://github.com/leinelissen/aeon/blob/master/src/app/utilities/Repository.ts)\\). All front-end calls to the back-end should go through these bridges to ensure security. Additionally, [the preload file](https://github.com/leinelissen/aeon/blob/master/src/app/preload.ts) specifies some unique cases where the front-end has access to back-end methods.\n\n### React\n\nThe front-end of the application is built out using [React](https://reactjs.org/), a Javascript framework that allows the building out of reactive interfaces in browsers. The `src/app` folder contains the entire front-end and is seperated out into assets, components, pages, store, style and utilities. Each interface screen is specified in `src/app/pages` and may pull in components from other directories. Styling is done using [Styled Components](https://styled-components.com/), routing using [React Router](https://reactrouter.com/) and the store is backed by [undux](https://undux.org/).\n\n### Git\n\nIn order to version incoming data, Aeon builds and updates a local Git repository that is bundled with the application. Aeon interfaces with this repository through [NodeGit](https://www.nodegit.org/), a set of NodeJS bindings for the widely used libgit2 library. All interfacing with the repository is done in NodeJS, though there is a bridge available for calling certain functions from the front-end.\n\n## Concepts\n\n### Providers\n\nThe first point of Aeon is making sure data is retrieved from a series of sources. These sources are abstracted into **Providers**. In Aeon, a _Provider_ is a binding for a particular platform \\(e.g. Facebook, Instagram, LinkedIn\\) that can directly pull data, initiate data requests, download data and parse data. For more detailed info on the inner workings of providers, and how to write one, find the particular page.\n\n>\n\n{% page-ref page=\"providers.md\" %}\n\n\n\n\n\n"
  },
  {
    "path": "docs/extending-aeon/architecture/providers.md",
    "content": "# Providers\n\nThe first point of Aeon is making sure data is retrieved from a series of sources. These sources are abstracted into **Providers**. In Aeon, a _Provider_ is a binding for a particular platform \\(e.g. Facebook, Instagram, LinkedIn\\) that can directly pull data, initiate data requests, download data and parse data. \n\n### Anatomy\n\nEach Provider owns their own folder in `src/main/providers` , and may consist of several files making up two distinct elements: the Provider itself \\(a class that either implements `Provider` or `DataRequestProvider`\\), and a Parser that interprets the resulting data for visualisation. For instance, see the [Instagram Provider](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/index.ts) and [Instagram Parser](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/parser.ts).\n\nThe anatomy of what a Provider, Parser and datatypes look like are described in Typescript, and are available in the [Provider types file](https://github.com/leinelissen/aeon/blob/master/src/main/providers/types.ts). This should be a regular reference when starting to build out your own providers.\n\n### Provider\n\nA Provider is a class that implements the bindings for a particular service. In this example we'll focus on Instagram. Each provider follows a particular workflow that covers a set of methods a platform must support: `initialise`, `update`, `dispatchDataRequest`, `isDataRequestComplete` and `parseDataRequest`.\n\nPlease note that a Provider distinguishes between immediately accessible data \\(i.e. that can be directly downloaded via an API\\) as _updates,_ while data that takes a while to gather and is downloaded in one go is considered as _data requests_.\n\n#### Initialisation\n\nWhen the user wants to add an account for a certain platform, a new Provider is instantiated, and the thew `initialise` function is called. The point of this function is to authenticate a user to said platform, so that we can make further calls to the platform. In the case of Instagram, a very basic implementation of this looks as follows:\n\n```typescript\nclass Instagram extends Provider {\n    initialise(): Promise<boolean> {\n        return withSecureWindow<boolean>(windowParams, (window) => {\n            // Load a URL in the browser, and see if we get redirected or not\n            const profileUrl = 'https://www.instagram.com/accounts/access_tool/ads_interests';\n            window.loadURL(profileUrl);\n            \n            // Create a promise that should resolve when the user is logged in\n            return new Promise((resolve) => {\n                window.webContents.on('did-navigate', () => {\n                    if (profileUrl === window.webContents.getURL()) {\n                        resolve();\n                    }\n                });\n            });\n        });\n    }\n}\n```\n\nA BrowserWindow is opened containing a link to a protected URL. Instagram will normally redirect us to the login page, which is then shown to the user. We then wait for the login form to succeed and redirect us back to the original, protected URL. When we detect this change, the user has successfully logged in and we can return true. We can later use the cookies that are set in this instance, to send out specific requests.\n\n_Small note: you'll find that this implementation slightly differs from the_ [_actual implementation_](https://github.com/leinelissen/aeon/blob/master/src/main/providers/instagram/index.ts)_, mostly in retrieving and setting cookies relevant for later use. This is the simplest implementation, but you should find inspiration in the fully-fledged implementations that are available in the repository._\n\n#### Updates\n\nNow that we have a set of cookies, we can talk to any Instagram data as if we are the user. Fortunately, Instagram has a set of data APIs available that we can immediately gather our data from:import scrapingUrls from './urls.json';\n\n```typescript\nimport scrapingUrls from './urls.json';\n// eg: [\n//    \"https://www.instagram.com/accounts/access_tool/account_privacy_changes?__a=1\",\n//    \"https://www.instagram.com/accounts/access_tool/password_changes?__a=1\",\n//    \"https://www.instagram.com/accounts/access_tool/former_emails?__a=1\",\n//     ....\n\nclass Instagram extends Provider {\n    update = async (): Promise<ProviderFile[]> => {\n        const cookies = await this.verifyLoggedInStatus();\n\n        // We extract the right cookies, and create a config we can then\n        // use for successive requests\n        const sessionid = cookies.find(cookie => cookie.name === 'sessionid').value;\n        \n        // Then, we'll setup a config for each individual fetch requests\n        const fetchConfig =  {\n            method: 'GET',\n            headers: {\n                Accept: 'application/json',\n                Referer: 'https://www.instagram.com/accounts/access_tool/ads_interests',\n                'X-CSRFToken': crypto.randomBytes(20).toString('hex'),\n                cookie: `sessionid=${sessionid}; shbid=${''}`\n            },\n        };\n\n        // Now we do all API requests in order to retrieve the data\n        const responses = await Promise.all(\n            scrapingUrls.map(url => \n                fetch(url, fetchConfig).then(response => response.json())\n            )\n        );\n\n        // We then transform the data so that we can return it to the handler\n        return responses.map(response => {\n            return {\n                filepath: `${response.page_name}.json`,\n                data: JSON.stringify(response.data.data, null, 4),\n            };\n        });\n    }\n}\n```\n\nAs you can see, we have a set of URLs available, and by stealing some of the cookies from the browser, we can just call all APIs, and return the data gathered from it to the ProviderManager. For this we follow the format `{ filepath: 'path_to_file', data: { //data }` . \n\n_Note that retrieving data from APIs may not as easy with all services. If you do not want to implement immediate updates, you can return `null` from this function._\n\n#### Dispatch Data Requests\n\nLots of services have an automated way of downloading your data. You click a button, wait some time, and then download a .ZIP file. Our goal is to automate the entirety of this process, starting with actually requesting the data. In our reference, Instagram has a dedicated page for these kinds of requests:\n\n```typescript\nclass Instagram extends Provider {\n    async dispatchDataRequest(): Promise<void> {\n        return withSecureWindow<void>(windowParams, async (window) => {\n            // Load the dispatched window\n            window.hide();\n            await new Promise((resolve) => {\n                window.webContents.on('did-finish-load', resolve)\n                window.loadURL('https://www.instagram.com/download/request/');\n            });\n\n            // We'll click the button for the user, but we'll need to defer to the\n            // user for a password\n            window.webContents.executeJavaScript(`\n                Array.from(document.querySelectorAll('button'))\n                    .find(el => el.textContent === 'Next')\n                    .click?.()\n            `);\n            window.show();\n\n            // Now we must defer the page to the user, so that they can enter their\n            // password. We then listen for a succesfull AJAX call \n            return new Promise((resolve) => {\n                window.webContents.session.webRequest.onCompleted({\n                    urls: [ 'https://*.facebook.com/*' ]\n                }, (details: Electron.OnCompletedListenerDetails) => {\n                    console.log('NEW REQUEST', details);\n\n                    if (details.url === 'https://www.facebook.com/api/graphql/'\n                        && details.statusCode === 200) {\n                        resolve();\n                    }\n                });\n            });             \n        });\n    }\n}\n```\n\nAs you can see, Instagram requires their user so enter a password before they can send out a request. To resolve this, we present this page to the user and wait for them to enter their password. We then use particular Electron listeners to wait for the form to be submitted.\n\n#### Check if Data Request is complete\n\nIt then might take a while for the data request to resolve. We currently use a polling approach to periodically check the particular page to see if the request has been completed:\n\n```typescript\nclass Instagram extends Provider {\n    async isDataRequestComplete(): Promise<boolean> {\n        return withSecureWindow<boolean>(windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve)\n                window.loadURL('https://www.instagram.com/download/request/');\n            });\n            \n            // Find a heading that reads 'Your Download is Ready'\n            return window.webContents.executeJavaScript(`\n                !!Array.from(document.querySelectorAll('h1'))\n                    .find(el => el.textContent === 'Your Download is Ready');\n            `);\n        });\n    }\n}\n```\n\nThis comes down to loading the page and seeing if a particular element \\(in this case a h1 tag reading \"Your download is ready\"\\) is present on the page. This functions thus return true or false depending on whether the request is complete.\n\n#### Download the Data Request\n\nIf the data request is complete, all that is left is downloading it and passing it back to Aeon. This is done as follows:\n\n```typescript\nimport { app } from 'electron';\nimport AdmZip from 'adm-zip';\nimport fs from 'fs';\n\nconst requestSavePath = path.join(app.getAppPath(), 'data');\n\nclass Instagram extends Provider {\n    async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {\n        return withSecureWindow<ProviderFile[]>(windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve)\n                window.loadURL('https://www.instagram.com/download/request/');\n            });\n\n            await new Promise((resolve) => {\n                // Now we defer to the user to enter their credentials\n                window.webContents.once('did-navigate', resolve); \n                window.webContents.executeJavaScript(`\n                    Array.from(document.querySelectorAll('button'))\n                        .find(el => el.textContent === 'Log In Again')\n                        .click?.()\n                `);\n            });\n\n\n            // We can now show the window for the login screen\n            window.show();\n\n            // Then we'll await the navigation back to the data download page from\n            // the login page\n            await new Promise((resolve) => {\n                window.webContents.once('will-navigate', resolve); \n            });\n\n            // We can now close the window\n            window.hide();\n\n            // Now that we're successfully authenticated on the data download page,\n            // the only thing we have to do is download the data.\n            const filePath = path.join(requestSavePath, 'instagram.zip');\n            await new Promise((resolve) => {\n                // Create a handler for any file saving actions\n                window.webContents.session.once('will-download', (event, item) => {\n                    // Save the item to the data folder temporarily\n                    item.setSavePath(filePath);\n                    item.once('done', resolve);\n                });\n\n                // And then trigger the button click\n                window.webContents.executeJavaScript(`\n                    Array.from(document.querySelectorAll('button'))\n                        .find(el => el.textContent === 'Download Data')\n                        .click?.()\n                `);\n            });\n\n            // We have the ZIP, all that's left to do is unpack it and pipe it to\n            // the repository\n            const zip = new AdmZip(filePath);\n            await new Promise((resolve) => \n                zip.extractAllToAsync(extractionPath, true, resolve)\n            );\n\n            // Translate this into a form that is readable for the ParserManager\n            const files = zip.getEntries().map((entry): ProviderFile => {\n                return {\n                    filepath: entry.entryName,\n                    data: null,\n                };\n            });\n\n            // And dont forget to remove the zip file after it's been processed\n            await fs.promises.unlink(filePath);\n\n            return files;\n        });\n    }\n}\n```\n\nWhile this piece of code is a bit longer, the process is still quite simple. First, we click the download button for the user. As Instagram requires users to enter their password before downloading the file, we open the window to the user and wait for them to enter their password. We then wait for the download to be registered by the BrowserWindow, save it to a particular location, unpack it and return a list of the files that were created by this dump. Aeon will recognise this data, and create a new commit that includes all changed files from the dump.\n\n#### withSecureWindow\n\nIn the reference implementations, you'll find ample use of the `withSecureWindow` function. This functions creates and spawns an [Electron BrowserWindow](https://www.electronjs.org/docs/api/browser-window) for the duration of a set of requests. It returns a promise that can be returned from the callback, and rejects if an error occurs, or the user closes the window. It makes it easy to create windows a user should interact with \\(e.g. to log in or to manually click a button\\), while keeping everything secure. If you wish to keep state \\(i.e. cookies\\) between different windows, you should specify a global `windowParams` object that is implemented in each call to `withSecureWindow`.\n\n### Parsers\n\nNow that data is actually present in the repository, we need to parse into a readable format for the front-end. We do this by means of parsers. The basic layout for a parser is as follows:\n\n```typescript\nconst parsers: ProviderParser[] = [\n    {\n        source: 'accounts_following_you.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.FOLLOWER\n            }\n        ],\n    },\n];\n```\n\nA parser is an array of files that have specific schemas attached to them. First, a new object is specified with a source: this is the filename for a particular file that is found in the repository folder for the particular Provider. Next, a schema is a mapping of a key found anywhere in that file, to a particular datatype. This means that in the file `accounts_following_you.json` , the schema looks for any value that is associated with the key `text`. This pieces of data is then associated with the `FOLLOWER` datatype when Aeon attempts to parse the data.\n\nIn case there isn't a neat mapping, or some data needs to be converted, a `transformer` can be set on the schema that interprets and transforms the data:\n\n```typescript\nimport { parseISO } from 'date-fns';\n\n/**\n * This specifies an input object in which the data is structured in an object,\n * in which the keys represent usernames, and the values are ISO dates. These\n * can be iterated upon to extract the desired data\n */\ntype GenericKeyedData = {\n    [key: string]: string\n};\n\nconst parsers: ProviderParser[] = [\n    {\n        source: 'connections.json',\n        schemas: [\n            {\n                key: 'following',\n                type: ProvidedDataTypes.ACCOUNT_FOLLOWING,\n                // Transform the data from { username: '9 Jan 2011', ... } to an array of \n                // objects that store both the username and the date\n                transformer: (obj: GenericKeyedData): Partial<AccountFollowing>[] => {\n                    // Loop through each available key on the object\n                    return Object.keys(obj).map((key): Partial<AccountFollowing> => {\n                        // Return an object in which the data is the key (username)\n                        // and the timestamp is a JS Date object.\n                        return {\n                            data: key,\n                            timestamp: parseISO(obj[key]),\n                        };\n                    });\n                }\n            }\n        ],\n    },\n];\n```\n\nThere are many more possibilities with the returned data and the transformer. Consult the [`ProviderParser` types](https://github.com/leinelissen/aeon/blob/master/src/main/providers/types.ts) to see what possibilities you can use. For instance, you can also specify multiple keys for the same schema.\n\n### Registering Providers and Parsers\n\nIf you have managed to setup your own providers and parsers, the last thing that is left is registering them with Aeon. This is done in three places:\n\nFirst, add the `Provider` class you created to the `providers` array in [`src/main/providers/index.ts`](https://github.com/leinelissen/aeon/blob/master/src/main/providers/index.ts). Then, add the resulting `ProviderParser` object to the `providerParsers` object in [`src/main/providers/parsers.ts`](https://github.com/leinelissen/aeon/blob/master/src/main/providers/parsers.ts). Lastly, add an icon for your provider to the `getIcon` method in [`src/app/utilities/Providers.ts`](https://github.com/leinelissen/aeon/blob/master/src/app/utilities/Providers.ts) to make sure your Provider appears pretty in the interface. Job well done!\n\n"
  },
  {
    "path": "docs/extending-aeon/local-development.md",
    "content": "# Local Development\n\nAs Aeon is a desktop app, yout development happens locally as well. In this guide, we'll go step by step what you need to do, to get Aeon set up locally and get started on its development.\n\n### Working with the terminal\n\nAeon is built using command line tools \\(sometimes abbreviated as CLI\\). These are tools that don't use neat graphical interfaces, but a terminal interface: the old-school style text-based screen you usually only see in movies. To access the command line, you need to open up a terminal in your operating system. Either use one of your OS-provided terminals \\(Terminal for macOS and Ubuntu; Command Prompt for Windows\\), or find a cross-platform terminal app such as [Hyper](https://hyper.is/).\n\nWe actually recommend working with [Visual Studio Code](https://code.visualstudio.com/) for this reason: it comes with a terminal built-in! You can open it up with Ctrl + Backtick on Windows/Linux and Cmd + Backtick on macOS. Do make sure you know how to operate your terminal before you start working on Aeon. To help you get along, find this [guide on how to operate the Ubuntu terminal](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview). You'll find that the macOS and Linux terminal are actually pretty similar once you get inside.\n\nThese guides will assume you are working on a UNIX terminal \\(e.g. bash, zsh, fish\\), as these work across macOS, FreeBSD and Linux distributions. If you are on a Windows machine and you cannot translate these to Command Prompt or Powershell commands yourself, consider installing the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10). This allows you to run a bash terminal on Windows, on which all commands in this guide should work flawlessly.\n\n### Prerequisites\n\nAeon is based on [Electron](https://www.electronjs.org/), a tool that combines the [Chromium](https://www.chromium.org/) browser \\(on which [Chrome](https://www.google.com/chrome/) is built\\) with a NodeJS \\(best described as server-side Chromium again\\) back-end in a single, executable desktop package. It is no suprise that everything is written in Javascript. In order to get started, you're going to need a couple of tools so that we can actually compile and run the application. These are as follows:\n\n#### Git\n\nA version control tool for code: Git helps you make small changes to a large codebase such as Aeon's. Either [install the binary directly](https://git-scm.com/downloads), or use one of your favorite package managers:\n\nWindows \\([Chocolatey](https://chocolatey.org/docs/installation)\\)\n\n```text\nchoco install git\n```\n\nmacOS \\([Homebrew](https://brew.sh/)\\)\n\n```text\nbrew install git\n```\n\nUbuntu \\(apt\\)\n\n```text\nsudo apt-get install git\n```\n\n#### NodeJS\n\nA server runtime for JavaScript: helps you build powerful Javascript-based applications that can run on the desktop rather than the web! Either [install the binary directly](https://nodejs.org/en/) or use one of your favorite package managers:\n\nWindows \\([Chocolatey](https://chocolatey.org/docs/installation)\\)\n\n```text\nchoco install nodejs\n```\n\nmacOS \\([Homebrew](https://brew.sh/)\\)\n\n```text\nbrew install node\n```\n\nFor Linux installs, find the relevant package manager of your choice in the [NodeJS install guide](https://nodejs.org/en/download/package-manager/).\n\n### Cloning the repository\n\nNow that you have installed all prerequisites, you need the source code that compiles to the Aeon application. Fortunately, this is accessible from GitHub and you can copy it to your computer very easily. First, make sure you navigate to the folder that will hold the folder containing all the Aeon code. Make sure you navigate to a particular folder on your own system.\n\n```text\ncd Documents/Code\n```\n\nNow that we're in the directory, we're going to copy the entire codebase to a folder within this directory. This operation is called cloning in Git terminology. You do it as follows:\n\n```text\ngit clone https://github.com/leinelissen/aeon\n```\n\nWhen the command finishes, the Aeon codebase should be found in a folder called `aeon`. Navigate into this folder to get started on installing dependencies.\n\n```text\ncd aeon\n```\n\n### Installing dependencies\n\nAeon builds upon lots of tools and libraries that make working with data, interfaces and other stuff a lot easier. These dependencies must be installed first, before we can start compiling the application. You do this as follows:\n\n```text\nnpm install\n```\n\n### Development Mode\n\nIn order to make development really easy, Aeon has a development mode that incrementally compiles Aeon and runs it immediately. This means that when you change a file, the application is re-compiled and reloaded. To start development mode, run the following command:\n\n```text\nnpm run start\n```\n\n#### Using the Visual Studio Code Debugger\n\nThe repository also includes a basic setup for integrating the VSCode debugger. This allows you to inspect objects that are logged, set breakpoints and do object inspection while you are developing. To use it, either press `F5` or run `Electron Main` from the debugger tab.\n\n### Compiling\n\nIf you want to generate an application package for distribution, you can run a build command as follows:\n\n```text\nnpm run make\n```\n\nWhen the command finishes \\(note: this may take a while\\), you can find a build for your specific platform in the `out/make` folder.\n\n### What's next?\n\nNow that you know how that make local development work, have a look at the architecture diagram, and how specific core concepts work. If you feel adventurous, you can get to work and start creating pull requests with new features.\n\n{% page-ref page=\"architecture/\" %}\n\n{% page-ref page=\"reporting-issues.md\" %}\n\n\n\n\n\n"
  },
  {
    "path": "docs/extending-aeon/reporting-issues.md",
    "content": "# Reporting Issues\n\nAeon is still a work in progress, so you might run into issues while using, building or abusing the application. This is expected. Alternatively, you might have suggestions for new platforms to add, or other features you think are crucial to its development. We accept and welcome any issues or feedback you have for this application.\n\nIn order to give some shape to this feedback, please use the GitHub issue tracker to report anything you find interesting. You can [create a new issue here](https://github.com/leinelissen/aeon/issues/new/choose) or [browse through existing issues here](https://github.com/leinelissen/aeon/issues). If you find an existing issue already matches your feedback, please consider adding your feedback to the already existing issue.\n\n"
  },
  {
    "path": "docs/using-aeon/getting-started.md",
    "content": "# Getting Started\n\nNot sure how to get started with Aeon? Let us guide you through a few crucial steps to get started with requesting and owning your personal information!\n\nMake sure you have installed Aeon first. If you don't know how to do so, [follow our guide](installation.md).\n\n### Adding Accounts\n\nFirst, find the accounts tab in the left menu.\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.49.20.png>)\n\nThen, click the **Add New Account** button on the bottom of the screen.\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.49.58.png>)\n\nA screen pops up, in which you can select a particular provided you would like to add. Click the platform of your choice (in this case Facebook) and the click the **Add New Facebook Account** button.&#x20;\n\nOnce you have clicked the button, a new window will pop up for logging in to the platform. Enter your credentials and click login.\n\nNote: Aeon never stores your password, and all tokens that are kept on your behalf are stored locally and safely.\n\nYour account is now set up, and you can feel free to add other accounts too!\n\n### Starting Data Requests\n\nIf your accounts are all set up, you can browser through them in the Accounts tab.\n\n&#x20;\n\n![](../.gitbook/assets/accounts.webp)\n\n&#x20;If you wish to retrieve data, select an account and click **Start Data Request**. You might need to enter your credentials again.\n\nData requests often take time. If your request is started successfully, you can kick it back and wait the hours / days it takes for the data request to complete. Aeon will let you know when the request is done.\n\n### Completing Data Requests\n\nWhenever a data request is finished, Aeon will let you know. You can complete a finished data request by going to the account and clicking **Complete Data Request**. Your data will be downloaded into Aeon.\n\n### Visualising Data\n\nOnce your data is in Aeon, you can use its powerful visualisation tools to see what is going on with your data. Find the Graph tab on the left side, for instance.\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.50.48.png>)\n\nThe large blue balls represent platforms, the squares represent data types, and the small dots represent a single data point that was retrieved from the platform.\n\nIf you like, you can click an individual data point to inspect it:\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.51.31.png>)\n\n### Removing Data\n\nFeel like a platform should not be having one of your data points? You can automatically generate a request for deleting it. Click the **Delete This Data Point** button in any data visualisation to add it to data points slated for deleting.\n\n&#x20;\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.51.31 (1).png>)\n\nIf you are sure you want to delete the data points, go to the **Erasure (1)** tab on the left-hand side.\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 14.51.40.png>)\n\nAeon will then display a list of datapoints that you have selected. Click **Remove 1 Data Point **to generate the request.\n\n![](<../.gitbook/assets/Schermafbeelding 2021-11-12 om 15.05.08.png>)\n\nAn email has been generated for each platform of which data is deleted. You will need to send this email yourself. Click **Send in E-mail Client** to open your default email client, so you can send the email.&#x20;\n"
  },
  {
    "path": "docs/using-aeon/installation.md",
    "content": "# Installation\n\nInstalling Aeon is really easy: [find the latest release on GitHub](https://github.com/leinelissen/aeon/releases/latest), and download the right package for your architecture. At this moment the following operating systems are supported.\n\n* Windows (with setup.exe or with NuGet: .nupkg)\n* macOS (the darwin .zip)\n* Ubuntu / Debian (the .deb file)\n* Fedora / CentOS / RHEL (the .rpm file)\n\nIf you find a platform you are regularly using missing, please create an issue, or find our guide to compiling your own version. ARM is theoretically supported, but please do report any issues you encounter while trying to do so.\n\n### Getting Started\n\nNot sure how to get started? Follow our guide:\n\n{% content-ref url=\"getting-started.md\" %}\n[getting-started.md](getting-started.md)\n{% endcontent-ref %}\n\nRun into issues? Make sure you report them, so that we can improve on the software:\n\n{% content-ref url=\"../extending-aeon/reporting-issues.md\" %}\n[reporting-issues.md](../extending-aeon/reporting-issues.md)\n{% endcontent-ref %}\n"
  },
  {
    "path": "entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.debugger</key>\n    <true/>\n  </dict>\n</plist>"
  },
  {
    "path": "forge.config.js",
    "content": "const package = require('./package.json');\nconst hash = require('child_process')\n  .execSync('git rev-parse --short HEAD')\n  .toString().trim()\n\n/**\n* This is the base electron-forge config\n*/\nconst config = {\n    packagerConfig: {\n        name: process.platform === 'linux' ? 'aeon' : 'Aeon',\n        icon: './src/icon',\n        executableName: process.platform === 'linux' ? 'aeon' : 'Aeon',\n        asar: false,\n        buildVersion: `${package.version}-${hash}`,\n        protocols: [\n            {\n              name: \"Aeon\",\n              schemes: [\"aeon\"]\n            }\n        ]\n    },\n    makers: [\n        {\n            name: '@electron-forge/maker-squirrel',\n            config: {\n                iconUrl: 'https://raw.githubusercontent.com/leinelissen/aeon/master/src/icon.ico',\n                setupIcon: './src/icon.ico'\n            }\n        },\n        {\n            name: '@electron-forge/maker-dmg',\n            config: {\n                // background: './assets/dmg-background.png',\n                format: 'ULFO'\n            }\n        },\n        {\n            name: \"@electron-forge/maker-zip\",\n            platforms: [ \"darwin\" ],\n        },\n        {\n            name: '@electron-forge/maker-deb',\n            config: {}\n        },\n        {\n            name: '@electron-forge/maker-rpm',\n            config: {}\n        }\n    ],\n    plugins: [\n        {\n            name: '@electron-forge/plugin-webpack',\n            config: {\n                // HMR Woes: https://github.com/electron-userland/electron-forge/issues/2560\n                devServer: {\n                    liveReload: false,\n                },\n                mainConfig: './webpack.main.config.js',\n                renderer: {\n                    config: './webpack.renderer.config.js',\n                    entryPoints: [\n                        {\n                            html: './src/app/index.ejs',\n                            js: './src/app/index.tsx',\n                            name: 'main_window',\n                            preload: {\n                                js: './src/app/preload.ts',\n                            },\n                        }\n                    ]\n                },\n                loggerPort: 9001\n            }\n        },\n    ],\n};\n\n/**\n* This function inserts config for notarizing applications.\n* Idea stolen from: https://github.com/electron/fiddle/blob/master/forge.config.js\n*/\nfunction notarizeMaybe() {\n    // GUARD: Only notarize macOS-based applications\n    if (process.platform !== 'darwin') {\n        return;\n    }\n    \n    // Only notarize in CI\n    if (!process.env.CI) {\n        console.log(`Not in CI, skipping notarization`);\n        return;\n    }\n    \n    // GUARD: Credentials are required\n    if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {\n        console.warn(\n            'Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!',\n        );\n        return;\n    }\n    \n    // Inject the notarization config if everything is right\n    config.packagerConfig.osxNotarize = {\n        appBundleId: 'nl.leinelissen.aeon',\n        appleId: process.env.APPLE_ID,\n        appleIdPassword: process.env.APPLE_ID_PASSWORD,\n        ascProvider: '238P3C58WC',\n    };\n\n    // Also inject signing config\n    config.packagerConfig.osxSign = {\n        identity: 'Developer ID Application: Bureau Moeilijke Dingen BV (238P3C58WC)',\n        'hardened-runtime': true,\n        entitlements: 'entitlements.plist',\n        'entitlements-inherit': 'entitlements.plist',\n        'signature-flags': 'library',\n    };\n}\n\nnotarizeMaybe();\n\n/**\n * For some reason OpenSSL isn't compiled directly into the NodeGit native module. We\n * thus have to include manually on Windows only.\n */\nfunction bundleOpenSSLMaybe() {\n    if (process.platform !== 'win32') {\n        return;\n    }\n\n    // Add a hook to include the file\n    config.hooks = {\n        postPackage: async () => {\n            const fs = require('fs');\n            const path = require('path');\n\n            await fs.promises.copyFile(            \n                // TODO: It's probably a bad idea to hardcode the DLL location here. Maybe it \n                // is a good idea to pull it from some side of config or environment variable.\n                'C:\\\\WINDOWS\\\\system32\\\\libcrypto-1_1-x64.dll',\n                path.join(__dirname, 'out', 'Aeon-win32-x64', 'resources', 'app', '.webpack', 'main', 'native_modules', 'build', 'Release', 'libcrypto-1_1-x64.dll'),\n            );\n        }\n    };\n}\n\nbundleOpenSSLMaybe();\n\n// Finally, export it\nmodule.exports = config;"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"aeon\",\n    \"productName\": \"Aeon\",\n    \"version\": \"0.2.22\",\n    \"description\": \"Online identity versioning for the masses\",\n    \"main\": \".webpack/main\",\n    \"scripts\": {\n        \"start\": \"electron-forge start\",\n        \"package\": \"electron-forge package\",\n        \"make\": \"cross-env NODE_ENV=production electron-forge make\",\n        \"publish\": \"electron-forge publish\",\n        \"lint\": \"eslint --ext .ts,.tsx ./src && tsc --noEmit\",\n        \"test\": \"playwright test\",\n        \"prepare:nodegit\": \"node scripts/prepareNodegit.js\",\n        \"rebuild:native-modules\": \"electron-rebuild\",\n        \"postinstall\": \"npm run prepare:nodegit\",\n        \"prepare\": \"husky install\"\n    },\n    \"keywords\": [],\n    \"author\": {\n        \"name\": \"Lei Nelissen\",\n        \"email\": \"lei@codified.nl\"\n    },\n    \"license\": \"EUPL-1.2\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/leinelissen/aeon\"\n    },\n    \"devDependencies\": {\n        \"@electron-forge/cli\": \"6.1.0\",\n        \"@electron-forge/maker-deb\": \"6.1.0\",\n        \"@electron-forge/maker-dmg\": \"6.1.0\",\n        \"@electron-forge/maker-rpm\": \"6.1.0\",\n        \"@electron-forge/maker-squirrel\": \"6.1.0\",\n        \"@electron-forge/maker-zip\": \"6.1.0\",\n        \"@electron-forge/plugin-webpack\": \"6.1.0\",\n        \"@playwright/test\": \"1.31\",\n        \"@swc/core\": \"1.3.42\",\n        \"@types/adm-zip\": \"0.5.0\",\n        \"@types/cytoscape\": \"3.19.9\",\n        \"@types/imapflow\": \"^1.0.9\",\n        \"@types/lodash-es\": \"4.17.7\",\n        \"@types/mailparser\": \"3.4.0\",\n        \"@types/node\": \"16.18.22\",\n        \"@types/node-fetch\": \"3.0.3\",\n        \"@types/nodegit\": \"0.28.3\",\n        \"@types/nodemailer\": \"6.4.7\",\n        \"@types/react\": \"17.0.55\",\n        \"@types/react-dom\": \"18.0.11\",\n        \"@types/react-redux\": \"7.1.25\",\n        \"@types/react-select\": \"5.0.1\",\n        \"@types/source-map-support\": \"0.5.6\",\n        \"@types/stream-chain\": \"2.0.1\",\n        \"@types/styled-components\": \"5.1.26\",\n        \"@types/yargs\": \"17.0.24\",\n        \"@typescript-eslint/eslint-plugin\": \"5.57.0\",\n        \"@typescript-eslint/parser\": \"5.57.0\",\n        \"@vercel/webpack-asset-relocator-loader\": \"1.7.3\",\n        \"cross-env\": \"7.0.3\",\n        \"csp-html-webpack-plugin\": \"5.1.0\",\n        \"css-loader\": \"6.7.3\",\n        \"dotenv-webpack\": \"7.1.1\",\n        \"electron\": \"23.2.0\",\n        \"electron-devtools-installer\": \"3.2.0\",\n        \"eslint\": \"8.37.0\",\n        \"eslint-config-airbnb\": \"19.0.4\",\n        \"eslint-config-airbnb-typescript\": \"17.0.0\",\n        \"eslint-import-resolver-typescript\": \"3.5.4\",\n        \"eslint-plugin-deprecation\": \"1.3.3\",\n        \"eslint-plugin-import\": \"2.27.5\",\n        \"eslint-plugin-jsx-a11y\": \"6.7.1\",\n        \"eslint-plugin-react\": \"7.32.2\",\n        \"eslint-plugin-react-hooks\": \"4.6.0\",\n        \"husky\": \"8.0.3\",\n        \"mini-css-extract-plugin\": \"2.7.5\",\n        \"node-loader\": \"2.0.0\",\n        \"playwright\": \"1.31\",\n        \"swc-loader\": \"0.2.3\",\n        \"typescript\": \"4.9.5\",\n        \"webpack-bundle-analyzer\": \"4.8.0\"\n    },\n    \"dependencies\": {\n        \"@fast-csv/parse\": \"4.3.6\",\n        \"@fontsource/ibm-plex-mono\": \"4.5.13\",\n        \"@fontsource/ibm-plex-sans\": \"4.5.13\",\n        \"@fontsource/inter\": \"4.5.15\",\n        \"@fortawesome/fontawesome-svg-core\": \"^6.2.1\",\n        \"@fortawesome/free-brands-svg-icons\": \"6.4.0\",\n        \"@fortawesome/free-solid-svg-icons\": \"6.4.0\",\n        \"@fortawesome/react-fontawesome\": \"0.2.0\",\n        \"@metrichor/jmespath\": \"0.3.1\",\n        \"@pmmmwh/react-refresh-webpack-plugin\": \"0.5.10\",\n        \"@popperjs/core\": \"2.11.7\",\n        \"@reactour/tour\": \"2.13.0\",\n        \"@reduxjs/toolkit\": \"1.9.3\",\n        \"adm-zip\": \"0.5.10\",\n        \"cytoscape\": \"3.23.0\",\n        \"cytoscape-fcose\": \"2.2.0\",\n        \"date-fns\": \"2.29.3\",\n        \"electron-squirrel-startup\": \"1.0.0\",\n        \"electron-store\": \"8.1.0\",\n        \"eventemitter2\": \"6.4.9\",\n        \"history\": \"5.3.0\",\n        \"imapflow\": \"1.0.125\",\n        \"keytar\": \"7.9.0\",\n        \"lodash-es\": \"4.17.21\",\n        \"mailparser\": \"3.6.4\",\n        \"node-abi\": \"3.33.0\",\n        \"node-fetch\": \"3.3.1\",\n        \"nodegit\": \"0.28.0-alpha.21\",\n        \"nodemailer\": \"6.9.1\",\n        \"react\": \"18.2.0\",\n        \"react-dom\": \"18.2.0\",\n        \"react-popper\": \"2.3.0\",\n        \"react-redux\": \"8.0.5\",\n        \"react-refresh\": \"0.14.0\",\n        \"react-router-dom\": \"6.9.0\",\n        \"react-select\": \"5.7.2\",\n        \"react-spring\": \"9.7.1\",\n        \"redux\": \"4.2.1\",\n        \"redux-persist\": \"6.0.0\",\n        \"stream-chain\": \"2.2.5\",\n        \"styled-components\": \"5.3.9\",\n        \"v8-compile-cache\": \"2.3.0\",\n        \"winston\": \"3.8.2\",\n        \"yargs\": \"17.7.1\"\n    }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { PlaywrightTestConfig } from '@playwright/test';\nimport path from 'path';\n\nconst config: PlaywrightTestConfig = {\n    testDir: './test',\n    maxFailures: 0,\n    outputDir: path.join(__dirname, 'test', 'output', 'playwright'),\n    workers: 1,\n    use: {\n        screenshot: 'on',\n        trace: 'on',\n    },\n};\n\nexport default config;\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\n    \"config:base\",\n    \"group:allNonMajor\",\n    \"schedule:earlyMondays\"\n  ],\n  \"git-submodules\": {\n    \"enabled\": true\n  }\n}\n"
  },
  {
    "path": "scripts/prepareNodegit.js",
    "content": "const path = require('path');\nconst fs = require('fs');\n\nconst debugPath = path.join(__dirname, '..', 'node_modules', 'nodegit', 'build', 'Debug');\nconst mainEntry = path.join(__dirname, '..', 'node_modules', 'nodegit', 'lib', 'nodegit.js');\n\n// First, we add an empty debug file, so we can satisfy webpack one exists\nfs.promises.mkdir(debugPath, { recursive: true })\n    .then(() => {\n        return fs.promises.writeFile(path.join(debugPath, 'nodegit.node'), '');\n    });\n\n// Then we modify the base files to not use dynamic requires\nfs.promises.readFile(mainEntry)\n    .then((file) => {\n        const data = file.toString('utf-8');\n        const newData = data.replace(/importExtension\\(\\\"(.*?)\\\"\\);/g, (match, name) => {\n            return `try { require('./${name}'); } catch { }`;\n        });\n        return fs.promises.writeFile(mainEntry, newData);\n    })"
  },
  {
    "path": "scripts/setupMacOSCertificates.sh",
    "content": "#!/usr/bin/env sh\n\n# Based on https://github.com/electron/fiddle/blob/master/tools/add-macos-cert.sh\n\nKEY_CHAIN=build.keychain\nMACOS_CERT_P12_FILE=certificate.p12\n\n# Recreate the certificate from the secure environment variable\necho $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE\n\n#create a keychain\nsecurity create-keychain -p actions $KEY_CHAIN\n\n# Make the keychain the default so identities are found\nsecurity default-keychain -s $KEY_CHAIN\n\n# Unlock the keychain\nsecurity unlock-keychain -p actions $KEY_CHAIN\n\nsecurity import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign;\n\nsecurity set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN\n\n# remove certs\nrm -fr *.p12"
  },
  {
    "path": "src/app/assets/open-data-rights.ts",
    "content": "import { IconDefinition, IconName } from '@fortawesome/fontawesome-svg-core';\n\nconst faOpenDataRights: IconDefinition = {\n    prefix: 'fab',\n    iconName: 'open-data-rights' as IconName,\n    icon: [\n        512,\n        512,\n        [],\n        'f1e9',\n        'M500.2,399.6L392.4,291.8C452.5,272.4,496,216,496,149.5C496,66.9,429.1,0,346.5,0S197,66.9,197,149.5v47.6h-47.6 C66.9,197.1,0,264,0,346.5S66.9,496,149.5,496c66.5,0,122.9-43.5,142.3-103.6l107.8,107.8c7,7,16.1,10.5,25.2,10.5 s18.3-3.5,25.2-10.5l50.2-50.2C514.2,436.2,514.2,413.5,500.2,399.6z M299,149.5c0-26.2,21.3-47.6,47.6-47.6s47.6,21.3,47.6,47.6 c0,26.2-21.3,47.6-47.6,47.6H299V149.5z M247.8,299H264v16.2L247.8,299z M230.4,427.5c-21.6,21.6-50.4,33.5-81,33.5 c-30.6,0-59.3-11.9-81-33.5c-21.6-21.6-33.5-50.4-33.5-81c0-30.6,11.9-59.3,33.5-81c21.6-21.6,50.4-33.5,81-33.5H197v49.4h-47.6 c-35.9,0-65.1,29.2-65.1,65.1s29.2,65.1,65.1,65.1s65.1-29.2,65.1-65.1v-35.9c1.7,4.7,4.5,9,8.2,12.8l40,40 C259.2,387.6,248,409.9,230.4,427.5z M179.5,316.5v30.1c0,16.6-13.5,30.1-30.1,30.1c-16.6,0-30.1-13.5-30.1-30.1 s13.5-30.1,30.1-30.1H179.5z M475.5,425.3l-50.2,50.2c-0.1,0.1-0.2,0.2-0.5,0.2s-0.4-0.1-0.5-0.2L298.9,350.1c0-1.2,0-2.4,0-3.6V299 h47.6c1.2,0,2.4,0,3.5,0l125.4,125.4c0.1,0.1,0.2,0.2,0.2,0.5C475.7,425.1,475.6,425.3,475.5,425.3z',    \n    ],\n};\n\nexport default faOpenDataRights;"
  },
  {
    "path": "src/app/components/App.tsx",
    "content": "import React, { Component } from 'react';\nimport styled, { StyleSheetManager } from 'styled-components';\nimport { HashRouter } from 'react-router-dom';\nimport { Provider } from 'react-redux';\nimport { PersistGate } from 'redux-persist/integration/react';\n\nimport 'app/styles';\nimport Pages from 'app/screens';\nimport store, { persistor } from 'app/store';\nimport Loading from './Loading';\nimport { RepositorySubscription } from 'app/store/data/selectors';\nimport { ProviderSubscription } from 'app/store/accounts/selectors';\nimport { EmailSubscription } from 'app/store/email/selectors';\nimport Tour from './Tour';\n\nconst Main = styled.main`\n    position: relative;\n`;\n\nclass App extends Component {\n    componentDidMount(): void {\n        document.getElementById('loader').style.display = 'none';\n    }\n\n    render(): JSX.Element {\n        return (\n            <HashRouter>\n                <StyleSheetManager disableVendorPrefixes>\n                    <Provider store={store}>\n                        <Tour>\n                            <PersistGate loading={<Loading />} persistor={persistor}>\n                                {/** Presentational components */}\n                                <Main>\n                                    <Pages />\n                                </Main>\n                                {/** Subscription managers */}\n                                <ProviderSubscription />\n                                <EmailSubscription />\n                                <RepositorySubscription />\n                            </PersistGate>\n                        </Tour>\n                    </Provider>\n                </StyleSheetManager>\n            </HashRouter>\n        );\n    }\n}\n\nexport default App;"
  },
  {
    "path": "src/app/components/Button.tsx",
    "content": "import React, { CSSProperties, HTMLAttributes, PropsWithChildren } from 'react';\nimport styled, { css } from 'styled-components';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { Ball } from './Loading';\nimport { IconProp } from '@fortawesome/fontawesome-svg-core';\nimport { Link } from 'react-router-dom';\n\ntype ThemeColor = 'blue' | 'red' | 'yellow' | 'green' | 'gray';\n\ninterface ButtonProps extends HTMLAttributes<HTMLButtonElement> {\n    backgroundColor?: ThemeColor;\n    fullWidth?: boolean;\n    color?: string;\n}\n\nconst StyledButtonStyles = css<ButtonProps>`\n    background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-500);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.color || 'var(--color-white)'};\n    height: 40px;\n    font-size: 14px;\n    font-weight: 500;\n    text-decoration: none !important;\n    font-family: var(--font-heading);\n    border-radius: 8px;\n    outline: 0 !important;\n    margin: 4px 0;\n    border: 0;\n    padding: 0 16px;\n    border: 1px solid var(--color-${(props) => props.backgroundColor || 'blue'}-600);\n    overflow-wrap: break-word;\n    letter-spacing: -0.3px;\n\n    &:hover&:not(:disabled) {\n        cursor: default;\n        background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-600);\n    }\n\n    &:disabled {\n        background-color: var(--color-gray-300);\n        cursor: not-allowed;\n        color: var(--color-gray-600);\n        border-color: var(--color-gray-400);\n    }\n\n    ${(props) => props.fullWidth && css`\n        width: 100%;\n    `}\n`;\n\nconst StyledButton = styled.button<ButtonProps>`\n    ${StyledButtonStyles}\n`;\n\nconst Label = styled.div`\n    flex: 0 1 auto;\n    min-width: 0;\n    white-space: nowrap;\n    overflow: hidden;\n`;\n\nconst Margin = styled.div`\n    width: 8px;\n`;\n\nexport const LinkButton = styled(Link)`\n    ${StyledButtonStyles};\n`;\n\nexport const SimpleButton = styled.button`\n    border: 0;\n    margin: 0;\n    padding: 0;\n    background-color: inherit;\n    color: inherit;\n`;\n\nexport const StyledGhostButton = styled(StyledButton)`\n    color: ${(props) => props.backgroundColor && props.backgroundColor !== 'blue' \n        ? `var(--color-${props.backgroundColor}-500)`\n        : 'var(--color-heading)'};\n    font-size: 14px;\n    padding: 8px 16px;\n    background-color: ${() => 'transparent'};\n    border: 0;\n    font-weight: 500;\n\n    &:hover&:not(:disabled) {\n        background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-50);\n    }\n\n    &:active {\n        background-color: var(--color-${(props) => props.backgroundColor || 'blue'}-100);\n    }\n`;\n\n\ninterface Props extends ButtonProps {\n    loading?: boolean;\n    onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n    disabled?: boolean;\n    icon?: IconProp;\n    style?: CSSProperties;\n}\n\nconst Button = ({ children, loading, onClick, disabled, icon, ...props }: PropsWithChildren<Props>): JSX.Element => {\n    return (\n        <StyledButton onClick={onClick} disabled={loading || disabled} {...props}>\n            {icon && !loading ? <FontAwesomeIcon icon={icon} style={{ marginRight: 8 }} fixedWidth /> : null}\n            <Label>\n                {children}\n            </Label>\n            {loading ? (<><Margin /><Ball size={10} /></>) : null}\n        </StyledButton>\n    );\n};\n\nexport const GhostButton = ({ children, loading, onClick, disabled, icon, ...props }: PropsWithChildren<Props>): JSX.Element => {\n    return (\n        <StyledGhostButton onClick={onClick} disabled={loading || disabled} {...props}>\n            {icon ? <FontAwesomeIcon icon={icon} style={{ marginRight: 8 }} fixedWidth /> : null}\n            <Label>\n                {children}\n            </Label>\n            {loading ? (<><Margin /><Ball size={10} /></>) : null}\n        </StyledGhostButton>\n    );\n};\n\nexport default Button;"
  },
  {
    "path": "src/app/components/Code.tsx",
    "content": "import styled, { css } from 'styled-components';\n\nconst Code = styled.div<{ removed?: boolean; added?: boolean; updated?: boolean }>`\n    font-family: var(--font-mono);\n    background-color: #f8f8f8;\n    padding: 16px 24px;\n    max-width: 100%;\n    line-height: 2;\n    white-space: pre-wrap;\n    user-select: text;\n    display: flex;\n    flex-direction: column;\n    word-break: break-all;\n    font-size: 12px;\n    margin: 4px 0;\n    border-radius: 8px;\n\n    h5 {\n        margin-top: 0;\n        opacity: 1;\n        font-weight: 600;\n    }\n\n    &.icon {\n        height: 1em;\n    }\n\n    ${(props) => props.added && css`\n        background-color: var(--color-green-100);\n        color: var(--color-green-500);\n    `}\n\n    ${(props) => props.removed && css`\n        background-color: var(--color-red-100);\n        color: var(--color-red-500);\n    `}\n\n    ${(props) => props.updated && css`\n        background-color: var(--color-yellow-100);\n        color: var(--color-yellow-500);\n    `}\n`;\n\nexport default Code;"
  },
  {
    "path": "src/app/components/IconBadge.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport styled from 'styled-components';\nimport { IconDefinition } from '@fortawesome/fontawesome-svg-core';\nimport { H2 } from './Typography';\nimport { PullContainer } from './Utility';\n\nconst Container = styled.div`\n    background-color: var(--color-blue-200);\n    color: var(--color-blue-700);\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: 8px;\n    font-size: 2em;\n    width: 2em;\n    height: 2em;\n    flex: 0 0 auto;\n    margin-right: 0.5em;\n`;\n\ninterface IconBadgeProps {\n    icon: IconDefinition;\n}\n\nfunction IconBadge({ icon }: IconBadgeProps) {\n    return (\n        <Container>\n            <FontAwesomeIcon\n                icon={icon}\n                fixedWidth\n            />\n        </Container>\n    );\n}\n\nexport function IconBadgeWithTitle({ children, icon }: PropsWithChildren<IconBadgeProps>) {\n    return (\n        <PullContainer verticalAlign>\n            <IconBadge icon={icon} />\n            <H2 lines={1}>{children}</H2>\n        </PullContainer>\n    );\n}\n\nexport default IconBadge;"
  },
  {
    "path": "src/app/components/Input.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown } from '@fortawesome/free-solid-svg-icons';\nimport React, { useCallback } from 'react';\nimport styled, { css } from 'styled-components';\n\nexport const TextInput = styled.input`\n    padding: 16px;\n    border-radius: 4px;\n    font-size: 16px;\n    margin-bottom: 16px;\n\n    background-color: var(--color-gray-50);\n    border: 1px solid var(--color-border);\n    color: var(--color-gray-800);\n\n    &::placeholder {\n        color: var(--color-gray-500);\n    }\n`;\n\nexport const Label = styled.label`\n    font-size: 0.8em;\n    display: flex;\n    flex-direction: column;\n    font-family: var(--font-mono);\n    color: var(--color-gray-600);\n\n    & > span {\n        margin-left: 4px;\n        margin-bottom: 2px;\n    }\n`;\n\nconst Select = styled.select<{ hasPlaceholder?: boolean }>`\n    height: 50px;\n    padding: 16px;\n    border-radius: 4px;\n    width: 100%;\n    margin-bottom: 16px;\n\n    background-color: var(--color-gray-50);\n    border: 1px solid var(--color-border);\n\n    color: inherit;\n    appearance: none;\n\n    ${(props) => props.hasPlaceholder && css`\n        color: #00000066;\n    `};\n\n    &:disabled {\n        background-color: var(--color-gray-300);\n        color: var(--color-gray-700);\n        cursor: not-allowed;\n    }\n`;\n\nconst SelectContainer = styled.div`\n    position: relative;\n\n    svg {\n        position: absolute;\n        right: 16px;\n        top: calc(50% - 8px);\n        transform: translateY(-50%);\n    }\n`;\n\ninterface DropdownProps {\n    label: string;\n    options: string[] | Record<string, JSX.Element>;\n    value: string;\n    onSelect: (selectedValue: string) => void;\n    disabled?: boolean;\n    placeholder?: string;\n}\n\ntype Option = { key: string, value: unknown };\n\nexport function Dropdown(props: DropdownProps): JSX.Element {\n    const { label, options, value, disabled, placeholder, onSelect } = props;\n\n    const availableOptions: Option[] = Array.isArray(options)\n        ? options.map<Option>((o) => ({ key: o, value: o }))\n        : Object.keys(options).map((key) => ({ key, value: options[key] }));\n\n    const handleChange = useCallback((event) => {\n        onSelect(event.target.value);\n    }, [onSelect]);\n\n    return (\n        <Label>\n            <span>{label}</span>\n            <SelectContainer>\n                <Select\n                    value={value}\n                    disabled={disabled}\n                    onChange={handleChange}\n                    placeholder=\"Please select an account\"\n                    // hasPlaceholder={value === ''}\n                >\n                    <option key=\"\" disabled={value !== ''}>{placeholder || 'Please select an option'}</option>\n                    {availableOptions.map((option) =>\n                        <option key={option.key}>{option.value}</option>,    \n                    )}\n                </Select>\n                <FontAwesomeIcon icon={faChevronDown} />\n            </SelectContainer>\n        </Label>\n    );\n}"
  },
  {
    "path": "src/app/components/Loading.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\nconst LoadingContainer = styled.div`\n    display: flex;\n    width: 100%;\n    height: 100%;\n    justify-content: center;\n    align-items: center;\n`;\n\ninterface Props {\n    size?: number;\n    color?: string;\n}\n\nexport const Ball = styled.div<Props>`\n    height: ${(props: Props): number => props.size || 20}px;\n    width: ${(props: Props): number => props.size || 20}px;\n    border-radius: 50px;\n    background-color: ${(props: Props): string => props.color || 'var(--color-primary)'};\n    animation: bounce 0.5s alternate infinite cubic-bezier(.5, 0.05, 1, .5),\n        fade-in 1s; \n    margin-top: -${(props: Props): number => props.size ? props.size * 2.5 : 40}px;\n\n    @keyframes bounce { \n        from { \n            transform: translate3d(0, 0px, 0); \n        } \n        to { \n            transform: translate3d(0, ${(props: Props): number => props.size ? props.size * 2 : 40}px, 0); \n        } \n    } \n    @keyframes fade-in { \n        from { \n            opacity: 0;\n        } \n        to { \n            opacity: 1;\n        } \n    } \n`;\n\nconst Loading = (): JSX.Element => (\n    <LoadingContainer>\n        <Ball />\n    </LoadingContainer>\n);\n\nexport default Loading;"
  },
  {
    "path": "src/app/components/Menu.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faProjectDiagram, faClock, faCog, faUser, faTable, faTrash } from '@fortawesome/free-solid-svg-icons';\nimport React from 'react';\nimport { NavLink, useLocation } from 'react-router-dom';\nimport styled from 'styled-components';\nimport { useSelector } from 'react-redux';\nimport { State } from 'app/store';\nimport { PullDown } from './Utility';\nimport useTour from './Tour/useTour';\n\nexport const PageContainer = styled.div`\n    display: grid;\n    grid-template-columns: 200px 1fr;\n    gap: 0px 0px;\n    grid-template-areas:\n        \"menu content\";\n    height: 100vh;\n    width: 100vw;\n    overflow: hidden;\n`;\n\nconst TitleBarContainer = styled.div`\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 200;\n    width: 100%;\n    height: 50px;\n    -webkit-app-region: drag;\n    background: transparent;\n    transition: all 0.3s ease;\n    backdrop-filter: blur(25px) brightness(1.1);\n    text-transform: capitalize;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-family: var(--font-heading);\n    border-bottom: 1px solid var(--color-border);\n\n    span {\n        font-size: 14px;\n        font-weight: 600;\n    }\n`;\n\nexport const ContentContainer = styled.div`\n    grid-area: content;\n    overflow: auto;\n    position: relative;\n    background-color: var(--color-background);\n`;\n\nconst Container = styled.div`\n    grid-area: menu;\n    display: flex;\n    flex-direction: column;\n    padding: 5px 0;\n    -webkit-app-region: drag;\n    padding: 50px 0 10px 0;\n    border-right: 1px solid var(--color-gray-400);\n    box-shadow: inset -10px 0 8px -10px rgba(0,0,0,0.04);\n`;\n\nconst Link = styled(NavLink)`\n    background: none;\n    border: 0;\n    font-family: var(--font-heading);\n    text-align: left;\n    height: 36px;\n    display: flex;\n    align-items: center;\n    padding: 0 12px;\n    margin: 0px 8px;\n    font-weight: 400;\n    -webkit-app-region: no-drag;\n    color: var(--color-heading);\n    border-radius: 6px;\n    cursor: default;\n\n    span:not(.icon) {\n        margin-left: 8px;\n        font-size: 14px;\n    }\n\n    span.icon {\n        /* font-size: 16px; */\n        color: var(--color-gray-700);\n    }\n\n    &.active {\n        @media (prefers-color-scheme: dark) {\n            background-color: #FFFFFF26;\n        }\n        @media (prefers-color-scheme: light) {\n            background-color: #00000018;\n        }\n    }\n\n    &:hover:not(.active) {\n        @media (prefers-color-scheme: dark) {\n            background-color: #FFFFFF12;\n        }\n        @media (prefers-color-scheme: light) {\n            background-color: #00000008;\n        }\n    }\n`;\n\nexport function TitleBar(): JSX.Element {\n    const location = useLocation();\n    const title = location.pathname.split('/')[1];\n\n    return (\n        <TitleBarContainer>\n            <span>{title}</span>\n        </TitleBarContainer>\n    );\n}\n\nexport default function Menu(): JSX.Element {\n    const deleted = useSelector((state: State) => state.data.deleted);\n    useTour(deleted.length ? '/screen/erasure' : null);\n\n    return (\n        <Container id=\"menu\">\n            <Link to=\"/timeline\" id=\"timeline\" className={({ isActive }) => isActive ? 'active' : ''}>\n                <span className=\"icon\"><FontAwesomeIcon icon={faClock} fixedWidth /></span>\n                <span>Timeline</span>\n            </Link>\n            <Link to=\"/accounts\" id=\"accounts\" className={({ isActive }) => isActive ? 'active' : ''}>\n                <span className=\"icon\"><FontAwesomeIcon icon={faUser} fixedWidth /></span>\n                <span>Accounts</span>\n            </Link>\n            <Link to=\"/data\" id=\"data\" className={({ isActive }) => isActive ? 'active' : ''}>\n                <span className=\"icon\"><FontAwesomeIcon icon={faTable} fixedWidth /></span>\n                <span>Data</span>\n            </Link>\n            <Link to=\"/graph\" id=\"graph\" className={({ isActive }) => isActive ? 'active' : ''}>\n                <span className=\"icon\"><FontAwesomeIcon icon={faProjectDiagram} fixedWidth /></span>\n                <span>Graph</span>\n            </Link>\n            {deleted.length ? (\n                <>\n                    <Link to=\"/erasure\" id=\"erasure\" className={({ isActive }) => isActive ? 'active' : ''} data-tour=\"erasure-screen\">\n                        <span className=\"icon\"><FontAwesomeIcon icon={faTrash} fixedWidth /></span>\n                        <span>Erasure ({deleted.length})</span>\n                    </Link>\n                </>\n            ) : null}\n            <PullDown>\n                <Link to=\"/settings\" id=\"settings\" className={({ isActive }) => isActive ? 'active' : ''}>\n                    <span className=\"icon\"><FontAwesomeIcon icon={faCog} fixedWidth /></span>\n                    <span>Settings</span>\n                </Link>            \n            </PullDown>\n        </Container>\n    );\n}"
  },
  {
    "path": "src/app/components/Modal/Menu.tsx",
    "content": "import React, { useState } from 'react';\nimport styled from 'styled-components';\nimport { SimpleButton } from '../Button';\n\nconst MenuContainer = styled.div<{ active?: boolean; }>`\n    display: flex;\n    justify-content: flex-end;\n    margin-top: -25px;\n    border-bottom: 1px solid var(--color-border);\n\n    ${SimpleButton} {\n        height: 40px;\n        margin-right: 16px;\n        color: var(--color-gray-700);\n        text-transform: capitalize;\n        position: relative;\n\n        &.active {\n            color: var(--color-primary);\n            font-weight: 600;\n            letter-spacing: -0.35px;\n\n            &::after {\n                content: \" \";\n                background-color: var(--color-primary);\n                color: inherit;\n                position: absolute;\n                bottom: 0;\n                left: -4px;\n                right: -4px;\n                height: 3px;\n                border-top-left-radius: 4px;\n                border-top-right-radius: 4px;\n            }\n        }\n\n        &:hover:not(.active) {\n            color: var(--color-gray-800);\n        }\n\n        &:first-child {\n            margin-left: 40px;\n        }\n    }\n`;\n\ntype Props = {\n    children: JSX.Element[] | JSX.Element;\n    labels?: JSX.Element[] | string[];\n};\n\nfunction ModalMenu({ children, labels = [] }: Props): JSX.Element {\n    const [selectedItem, setSelectedItem] = useState(0);\n    \n    return (\n        <>\n            <MenuContainer data-tour=\"modal-menu-options\">\n                {[...new Array(Array.isArray(children) ? children.length : 1)].map((a, i) =>\n                    <SimpleButton key={i} onClick={() => setSelectedItem(i)} className={selectedItem === i ? 'active' : ''}>\n                        {labels[i] || i}\n                    </SimpleButton>,\n                )}\n            </MenuContainer>\n            <div data-tour=\"modal-menu-container\">\n                {Array.isArray(children) ? children[selectedItem] : children}\n            </div>\n        </>\n    );\n}\n\nexport default ModalMenu;"
  },
  {
    "path": "src/app/components/Modal/index.tsx",
    "content": "import React, { Component, useRef, useEffect, PropsWithChildren, HTMLAttributes, CSSProperties } from 'react';\nimport { createPortal } from 'react-dom';\nimport styled from 'styled-components';\nimport { Transition, config, animated } from 'react-spring';\nimport { GhostButton } from '../Button';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faArrowUp } from '@fortawesome/free-solid-svg-icons';\nimport { LargeShadow } from 'app/styles/snippets';\n\ninterface SpringProps {\n    transform?: string;\n    backgroundOpacity?: number;\n    opacity?: number;\n    pointerEvents?: CSSProperties['pointerEvents'];\n}\ntype NextFunc = (props: SpringProps) => Promise<void>;\n\ninterface Props {\n    isOpen?: boolean;\n    onRequestClose?: () => void;\n}\n\nconst Container = styled(animated.div)`\n    position: fixed;\n    left: 0;\n    top: 0;\n    height: 100vh;\n    width: 100vw;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    padding: 10vh 0;\n    z-index: 5000;\n    /* backdrop-filter: blur(50px); */\n    \n    @media (prefers-color-scheme: dark) {\n        background-color: #222222f0;\n    }\n    \n    @media (prefers-color-scheme: light) {\n        background-color: #eeeeeef0;\n    }\n`;\n\nconst StyledDialog = styled(animated.div)`\n    border-radius: 8px;\n    margin-top: 10vh;\n    min-width: 50vw;\n    min-height: 25h;\n    max-height: 80vh;\n    max-width: 700px;\n    padding-top: 32px;\n    overflow-y: auto;\n    background-color: var(--color-modal-background);\n    ${LargeShadow}\n`;\n\ntype DialogProps = PropsWithChildren<\nHTMLAttributes<HTMLDivElement>\n>;\n\nfunction Dialog(props: DialogProps): JSX.Element {\n    const { children, ...rest } = props;\n    const ref = useRef<HTMLDivElement>();\n\n    useEffect(() => {\n        ref.current?.focus();\n    }, []);\n\n    return (\n        <StyledDialog  {...rest} ref={ref} tabIndex={0}>\n            {children}\n        </StyledDialog>\n    );\n}\n\nconst CloseButton = styled(GhostButton)`\n    position: fixed;\n    top: 0;\n    left: 4px;\n    padding: 16px;\n`;\n\nclass Modal extends Component<Props> {\n    element = document.getElementById('modal');\n\n    handleBlur = (): void => this.props.onRequestClose();\n\n    componentDidMount(): void {\n        document.addEventListener('keydown', this.handleKeyDown);\n    }\n\n    componentWillUnmount(): void {\n        document.removeEventListener('keydown', this.handleKeyDown);\n    }\n\n    handleKeyDown = (event: KeyboardEvent): void => {\n        if (event.key === 'Escape') {\n            this.props.onRequestClose();\n        }\n    };\n\n    handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {\n        // GUARD: Check if the currenttarget is the parent where the events\n        // orginate from.\n        if (event.currentTarget === event.target) {\n            this.props.onRequestClose();\n        }   \n    };\n\n    render(): JSX.Element {\n        const { isOpen, onRequestClose, children } = this.props;\n\n        return createPortal((\n            <Transition\n                items={isOpen}\n                from={{ transform: 'translate3d(0,-40px,0)', opacity: 0, backgroundOpacity: 0, pointerEvents: 'all' }}\n                enter={() => async (next: NextFunc) => {\n                    try {\n                        next({ backgroundOpacity: 1, pointerEvents: 'all' }).catch(() => null);\n                        await new Promise((resolve) => setTimeout(resolve, 200));\n                        await next({ transform: 'translate3d(0,0px,0)', opacity: 1 });\n                    } catch {\n                        // Async animation may be interruped. We don't care\n                        // about this, but we still need to catch an error so we\n                        // can suppress any warnings about it to the user.\n                    }\n                }}\n                leave={{ transform: 'translate3d(0,-40px,0)', opacity: 0, backgroundOpacity: 0, pointerEvents: 'none' }}\n                config={config.wobbly}\n            >\n                {({ backgroundOpacity, pointerEvents, ...props }, item) => {\n                    return item && (\n                        <Container style={{ opacity: backgroundOpacity, pointerEvents }} onClick={this.handleContainerClick}>\n                            <Dialog \n                                style={props}\n                                // onBlur={this.handleBlur}\n                            >\n                                {children}\n                                <CloseButton onClick={onRequestClose} data-telemetry-id=\"modal-close\">\n                                    <FontAwesomeIcon icon={faArrowUp} fixedWidth />\n                                </CloseButton>\n                            </Dialog>\n                        </Container>\n                    );\n                }}\n            </Transition>\n        ), this.element);\n    }\n}\n\nexport default Modal;"
  },
  {
    "path": "src/app/components/NoData.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faPlus } from '@fortawesome/free-solid-svg-icons';\nimport { LinkButton } from 'app/components/Button';\nimport { MarginRight } from 'app/components/Utility';\nimport React from 'react';\nimport styled from 'styled-components';\nimport Graphic from '../assets/undraw_empty_xct9.svg';\nimport { H2, H3 } from './Typography';\n\nconst Container = styled.div`\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n    height: 100%;\n    text-align: center;\n\n    img {\n        width: 400px;\n        max-width: 75vw;\n        margin-bottom: 48px;\n    }\n`;\n\nfunction NoData(): JSX.Element {\n    return (\n        <Container>\n            <img src={Graphic} alt=\"Man with an empty box\" />\n            <H2>Nothing here...</H2>\n            <br />\n            <H3>You haven&apos;t requested any data yet.<br /> Start your first data request by heading over to Accounts.</H3>\n            <br />\n            <LinkButton to=\"/accounts?create-new-account\">\n                <MarginRight><FontAwesomeIcon icon={faPlus} /></MarginRight>\n                Add your first account\n            </LinkButton>\n        </Container>\n    );\n}\n\nexport default NoData;"
  },
  {
    "path": "src/app/components/PanelGrid.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { IconProp } from '@fortawesome/fontawesome-svg-core';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { NavLink, NavLinkProps } from 'react-router-dom';\nimport styled, { css } from 'styled-components';\nimport { faChevronRight } from '@fortawesome/free-solid-svg-icons';\nimport { PullRight } from './Utility';\nimport { Section } from './RightSideOverlay';\n\nexport const ListItem = styled.div`\n    padding: 8px 32px;\n    flex-grow: 0;\n    flex-shrink: 0;\n`;\n\nexport const RowHeading = styled(ListItem)`\n    font-weight: 400;\n    position: sticky;\n    top: 0;\n    align-self: flex-start;\n    z-index: 2;\n    font-size: 14px;\n    width: 100%;\n    border-bottom: 1px solid var(--color-border);\n    padding: 16px 32px;\n    font-family: var(--font-heading);\n    font-weight: 600;\n`;\n\nexport const RowDescription = styled(RowHeading)`\n    font-weight: 400;\n    font-size: 11px;\n    margin-bottom: 8px;\n`;\n\nexport const SubHeading = styled(RowHeading)`\n    font-size: 10px;\n    font-family: 'IBM Plex Mono';\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    background-color: var(--color-background);\n    color: var(--color-gray-700);\n    border: 0;\n    padding: 16px 32px 8px 16px;\n`;\n\nexport const PanelGrid = styled.div<{ columns?: number; noTopPadding?: boolean; }>`\n    display: grid;\n    grid-auto-columns: auto;\n    grid-template-columns: repeat(${(props) => props.columns || 3}, 1fr);\n    padding-top: ${(props) => props.noTopPadding ? 0 : 50}px;\n    height: 100%;\n    position: relative;\n    overflow: hidden;\n\n    ${(props) => (!props.columns || props.columns === 3) && css`\n        & ${Section} {\n            margin: 24px;\n        }\n    `}\n`;\n\nexport const List = styled.div<{ topMargin?: boolean }>`\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    overflow-y: auto;\n    position: relative;\n    border-right: 1px solid var(--color-border);\n\n    ${(props) => props.topMargin && css`\n        margin-top: 0px;\n    `}\n`;\n\nexport const SplitPanel = styled.div`\n    display: flex;\n    flex-direction: column;\n`;\n\nexport const PanelBottomButtons = styled.div`\n    margin-top: auto;\n    padding: 16px;\n    border-top: 1px solid var(--color-border);\n    border-right: 1px solid var(--color-border);\n`;\n\nconst IconWrapper = styled.div`\n    margin-right: 8px;\n    font-size: 1.25em;\n`;\n\nconst ChevronWrapper = styled(PullRight)`\n    opacity: 0.25;\n    font-size: 0.7em;\n`;\n\ntype ListButtonProps = {\n    deleted?: boolean;\n    modified?: boolean;\n    added?: boolean;\n    large?: boolean;\n} & NavLinkProps & React.RefAttributes<HTMLAnchorElement>;\n\nexport const NavigatableListEntryContainer = styled<React.ForwardRefExoticComponent<ListButtonProps>>(NavLink).withConfig({\n    shouldForwardProp: (prop) => typeof prop === 'string' && !['deleted', 'modified', 'added', 'large'].includes(prop),\n})`\n    border: 0;\n    background: transparent;\n    display: flex;\n    align-items: center;\n    font-size: 14px;\n    margin: 1px 8px;\n    padding: 8px 12px;\n    font-weight: 400;\n    border-radius: 8px;\n    overflow: hidden;\n    white-space: nowrap; \n    color: var(--color-heading);\n    font-family: var(--font-heading);\n\n    img {\n        max-height: 100px;\n        width: auto;\n        border-radius: 5px;\n    }\n\n    svg {\n        flex: 0 0 auto;\n        color: var(--color-gray-700);\n    }\n\n    &.active {\n        background: var(--color-blue-500);\n        color: var(--color-white);\n\n        svg {\n            flex: 0 0 auto;\n            color: var(--color-white);\n        }\n    }\n\n    &:hover:not(.active) {\n        background: var(--color-blue-50);\n    }\n\n    &:active {\n        background: var(--color-blue-100);\n    }\n\n    ${(props) => props.added && css`\n        background-color: var(--color-green-100);\n\n        &.active {\n            background-color: var(--color-green-500);\n        }\n    `}\n\n    ${(props) => props.deleted && css`\n        background-color: var(--color-red-100);\n\n        &.active {\n            background-color: var(--color-red-500);\n        }\n    `}\n\n    ${(props) => props.modified && css`\n        background-color: var(--color-yellow-100);\n\n        &.active {\n            background-color: var(--color-yellow-500);\n        }\n    `}\n\n    ${(props) => props.large && css`\n        font-size: 16px;\n        font-weight: 500;\n    `}\n\n    &:disabled {\n        opacity: 0.25;\n    }\n\n    ${PullRight} {\n        padding-left: 0.5em;\n    }\n`;\n\nconst CategoryContainer = styled.div`\n    margin-bottom: 16px;\n`;\n\nexport function Category({ title, children, id }: PropsWithChildren<{ title?: string, id?: string }>) {\n    return (\n        <>\n            {title && <SubHeading>{title}</SubHeading>}\n            {children && (\n                <CategoryContainer id={id}>\n                    {children}\n                </CategoryContainer>\n            )}\n        </>\n    );\n}\n\ntype NavigatableListEntryProps = PropsWithChildren<{\n    to: string,\n    icon?: IconProp,\n} & ListButtonProps>;\n\nexport function NavigatableListEntry({ \n    icon,\n    children,\n    ...props\n}: NavigatableListEntryProps): JSX.Element {\n    return (\n        <NavigatableListEntryContainer {...props}>\n            {icon ? \n                <IconWrapper>\n                    <FontAwesomeIcon fixedWidth icon={icon} />\n                </IconWrapper>\n                : null}\n            {children}\n            <ChevronWrapper>\n                <FontAwesomeIcon fixedWidth icon={faChevronRight} />\n            </ChevronWrapper>\n        </NavigatableListEntryContainer>\n    );\n}"
  },
  {
    "path": "src/app/components/RightSideOverlay.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { GhostButton } from 'app/components/Button';\nimport styled, { css } from 'styled-components';\nimport { animated, useTransition } from 'react-spring';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faArrowLeft } from '@fortawesome/free-solid-svg-icons';\nimport { Shadow } from 'app/styles/snippets';\nimport usePrevious from 'app/utilities/usePrevious';\n\nexport type RightSideOverlayProps = PropsWithChildren<{\n    onClose?: () => void;\n    marginTop?: number;\n    overlay?: boolean;\n}>;\n\nconst Container = styled.div`\n    pointer-events: all;\n    padding-bottom: 1em;\n\n    code {\n        margin-bottom: 0;\n    }\n`;\n\nexport const CloseButton = styled(GhostButton)`\n    margin-left: 8px;\n    color: var(--color-gray-400);\n    \n    &:hover {\n        color: var(--color-gray-800);\n    }\n`;\n\nexport const Section = styled.div<{ well?: boolean }>`\n    margin: 24px 36px;\n\n    p:first-child {\n        margin-top: 0;\n    }\n\n    p:last-child {\n        margin-bottom: 0;\n    }\n\n    img {\n        max-width: 100%;\n        height: auto;\n        border-radius: 5px;\n    }\n\n    ${(props) => props.well && css`\n        background-color: var(--color-gray-100);\n        padding: 16px 24px;\n        border-radius: 8px;\n        margin: 8px 16px;\n        overflow: hidden;\n        font-family: var(--font-heading);\n    `}\n`;\n\nexport const DetailListItem = styled.div`\n    display: flex;\n\n    &:not(:last-child) {\n        margin-bottom: 4px;\n    }\n\n    & > * {\n       flex: 0 1 auto; \n       color: var(--color-gray-800);\n       overflow: hidden;\n       white-space: nowrap;\n    }\n\n    & > *:first-child {\n        margin-right: 12px;\n        flex: 0 0 auto;\n    }\n`;\n\nexport const RightSideOverlayOffset = styled.div`\n    margin-top: 50px;\n    position: relative;\n    height: 100%;\n`;\n\nexport const AbsolutePosition = styled(animated.div)`\n    position: absolute;\n    right: 8px;\n    top: 58px;\n    background-color: var(--color-background);\n    border-radius: 8px;\n    width: 33%;\n    ${Shadow};\n`;\n\nconst RightSideOverlay = (props: RightSideOverlayProps): JSX.Element => {\n    const { \n        onClose: handleClose,\n        children,\n        marginTop,\n        overlay,\n        ...otherProps\n    } = props;\n\n    const previousChildren = usePrevious(children);\n    const transitions = useTransition(children, {\n        from: { opacity: 0 },\n        enter: { opacity: 1 },\n        leave: { opacity: 0 },\n        config: {\n            tension: 300,\n            friction: 20,\n        },\n        immediate: !!children && !!previousChildren,\n    });\n\n    // Optionally wrap the overlay in an absolute position\n    const Wrapper = overlay ? AbsolutePosition : animated.div;\n\n    return transitions(({ opacity }, items) => \n        items ? (\n            <Wrapper\n                style={{ \n                    opacity: opacity.to({ range: [0.0, 1.0], output: [0, 1] }),\n                    transform: opacity.to((x: number) => `translateX(${-x * 20 + 20}%)`),\n                    marginTop,\n                }}\n                {...otherProps}\n            >\n                <Container>\n                    {handleClose ? \n                        <CloseButton onClick={handleClose}>\n                            <FontAwesomeIcon icon={faArrowLeft} />\n                        </CloseButton>\n                        : null}\n                    {items || ''}\n                </Container>\n            </Wrapper>\n        ) : null,\n    );\n};\n\nexport default RightSideOverlay;"
  },
  {
    "path": "src/app/components/Timestamp.tsx",
    "content": "import { formatDistanceToNow } from 'date-fns';\nimport React, { useMemo } from 'react';\nimport Tooltip from './Tooltip';\n\ninterface TimestampProps {\n    children?: Date | string | number;\n}\n\nfunction Timestamp({ children: date }: TimestampProps) {\n    const parsedDate = useMemo(() => (\n        typeof date === 'string'\n            ? new Date(date)\n            : date\n    ), []);\n\n    const relativeTime = useMemo(() => (\n        parsedDate ? formatDistanceToNow(parsedDate) : null\n    ), [parsedDate]);\n\n    return (\n        <Tooltip title={parsedDate?.toLocaleString()}>\n            <time dateTime={parsedDate instanceof Date ? parsedDate?.toISOString() : parsedDate?.toString()}>\n                {relativeTime ? `${relativeTime} ago` : 'Never'}\n            </time>\n        </Tooltip>\n    );\n}\n\nexport default Timestamp;\n"
  },
  {
    "path": "src/app/components/Tooltip.tsx",
    "content": "import { Placement } from '@popperjs/core';\nimport { Shadow } from 'app/styles/snippets';\nimport React, { PropsWithChildren, ReactNode, useCallback, useMemo, useState } from 'react';\nimport { usePopper } from 'react-popper';\nimport { animated, useTransition } from 'react-spring';\nimport styled from 'styled-components';\n\nconst StyledTooltip = styled(animated.div)`\n    background-color: var(--color-background);\n    border-radius: 4px;\n    padding: 4px 8px;\n    ${Shadow}\n`;\n\nfunction getTransitionConfig(placement: Placement) {\n    switch (placement) {\n        case 'left':\n        case 'left-end':\n        case 'left-start':\n            return {\n                from: { opacity: 0, pos: [-25, 0] },\n                enter: { opacity: 1, pos: [0, 0] },\n                leave: { opacity: 0, pos: [-25, 0] },\n            };\n        case 'right':\n        case 'right-end':\n        case 'right-start':\n            return {\n                from: { opacity: 0, pos: [25, 0] },\n                enter: { opacity: 1, pos: [0, 0] },\n                leave: { opacity: 0, pos: [25, 0] },\n            };\n        case 'bottom':\n        case 'bottom-end':\n        case 'bottom-start':\n        default:\n            return {\n                from: { opacity: 0, pos: [0, -25] },\n                enter: { opacity: 1, pos: [0, 0] },\n                leave: { opacity: 0, pos: [0, -25] },\n            };\n        case 'top':\n        case 'top-end':\n        case 'top-start':\n            return {\n                from: { opacity: 0, pos: [0, 25] },\n                enter: { opacity: 1, pos: [0, 0] },\n                leave: { opacity: 0, pos: [0, 25] },\n            };\n    }\n}\n\ntype TooltipProps = PropsWithChildren<{\n    title: ReactNode;\n    placement?: Placement;\n}>;\n\n/**\n * This component will render a tooltip (passed as `title`) when hovering over a\n * component (passed as `children`). The implementation is deliberately simple.\n */\nfunction Tooltip({ title, children, placement = 'auto' }: TooltipProps) {\n    // Track whether the child components are being hovered\n    const [isHovered, setHovered] = useState(false);\n\n    // Set refs for Popper\n    const [referenceElement, setReferenceElement] = useState(null);\n    const [popperElement, setPopperElement] = useState(null);\n\n    // Create config for popper\n    const { styles, attributes, state } = usePopper(referenceElement, popperElement, {\n        placement,\n        modifiers: [\n            { name: 'offset', options: { offset: [0, 8] } },\n            { name: 'hide' },\n        ],\n    });\n\n    // Define callback handlers for hover events\n    const handleMouseOver = useCallback(() => setHovered(true), [setHovered]);\n    const handleMouseOut = useCallback(() => setHovered(false), [setHovered]);\n\n    // Create config for react-spring\n    const config = useMemo(() => getTransitionConfig(state?.placement), [state?.placement]);\n    const transitions = useTransition(isHovered, {\n        ...config,\n        config: {\n            tension: 300,\n            friction: 20,\n        },\n    });\n\n    return (\n        <>\n            <span\n                ref={setReferenceElement}\n                onMouseOver={handleMouseOver}\n                onMouseOut={handleMouseOut}\n                onFocus={handleMouseOver}\n                onBlur={handleMouseOut}\n            >\n                {children}\n            </span>\n            {transitions(({ opacity, pos }, item) => (\n                item && (\n                    <div ref={setPopperElement} style={styles.popper} {...attributes.popper}>\n                        <StyledTooltip style={{\n                            opacity,\n                            transform: pos.to((x, y) => `translate3d(${x}%,${y}%,0)`),\n                        }}>\n                            {title}\n                        </StyledTooltip>\n                    </div>\n                )\n            ))}\n        </>\n    );\n}\n\nexport default Tooltip;"
  },
  {
    "path": "src/app/components/Tour/index.tsx",
    "content": "import React, { PropsWithChildren } from 'react';\nimport { TourProvider } from '@reactour/tour';\nimport steps, { TourKeys } from './steps';\nimport { faCheck } from '@fortawesome/free-solid-svg-icons';\nimport Button from '../Button';\n\nfunction Tour({ children }: PropsWithChildren<unknown>): JSX.Element {\n    return (\n        <TourProvider\n            steps={Object.keys(steps).flatMap((key) => steps[key as TourKeys])}\n            nextButton={({\n                Button: BaseButton,\n                currentStep,\n                stepsLength,\n                setIsOpen,\n                setCurrentStep,\n            }) => {\n                const last = currentStep === stepsLength - 1;\n                return (\n                    <BaseButton\n                        hideArrow={last}\n                        onClick={() => {\n                            if (last) {\n                                setIsOpen(false);\n                            } else {\n                                setCurrentStep((s) => (s === Object.keys(steps).length - 1 ? 0 : s + 1));\n                            }\n                        }}\n                    >\n                        {last ? <Button icon={faCheck}>Done</Button> : null}\n                    </BaseButton>\n                );\n            }}\n            styles={{\n                maskWrapper: (base) => ({ ...base, color: 'var(--color-gray-400)', opacity: 0.9 }),\n                popover: (base) => ({ \n                    ...base, \n                    borderRadius: 8,\n                    '--reactour-accent': 'var(--color-primary)', \n                }),\n            }}\n        >\n            {children}\n        </TourProvider>\n    );\n}\n\nexport default Tour;"
  },
  {
    "path": "src/app/components/Tour/steps.tsx",
    "content": "import GraphExplainer from 'app/screens/Graph/explainer';\nimport React, { useEffect } from 'react';\nimport { StepType } from '@reactour/tour';\nimport { demoMode } from 'app/utilities/env';\n\n/**\n * Clicks an element that is targeted by the supplied selector on mount. This is\n * especially helpful if you need to guide a user through multiple screens.\n */\nconst ClickOnMount = ({ selector }: { selector: string }): JSX.Element => {\n    useEffect(() => {\n        const el = document.querySelector(selector);\n        (el as HTMLElement)?.click();\n    }, [selector]);\n\n    return null;\n};\n\nexport type TourKeys = '/screen/timeline'\n| '/screen/graph'\n| '/screen/data'\n| '/screen/settings'\n| '/screen/accounts/has-accounts'\n| '/screen/accounts/no-accounts'\n| '/screen/accounts/new-account'\n| '/screen/erasure';\n\nconst steps: Record<TourKeys, StepType[]> = {\n    '/screen/timeline': [\n        {\n            selector: null,\n            content: 'This is the timeline! The timeline is where you get a chronological overview of all of your data. Every time Aeon retrieves data from the internet, a new entry is added to this list. Use this screen to get a grip on how your online identity has evolved over time.',\n        },\n        {\n            selector: '[data-tour=\"timeline-commits-list\"]',\n            content: 'This is the list of recent changes to data belonging to you. Every time Aeon finds new data, or data is removed, this list will show it.',\n        },\n        {\n            selector: '[data-tour=\"timeline-first-commit\"]',\n            content: 'This is the most recent change to your identity. By clicking it, you can select it.',\n        },\n        {\n            selector: '[data-tour=\"timeline-diff-container\"]',\n            content: 'Here, we go slightly more in-depth what the last change is actually about.',\n        },\n        {\n            selector: '[data-tour=\"timeline-diff-info\"]',\n            content: 'This contains information about the circumstances in which the change was made. It contains when the change was made, which platform the change is from and which account was involved with the change.',\n        },\n        {\n            selector: '[data-tour=\"timeline-diff-data\"]',\n            content: 'Here, all data points that were added or removed are gathered.',\n        },\n        {\n            selector: null,\n            content: 'If you find this view a little overwhelming, go over to the Graph screen. There, you\\'ll find a slightly more convenient overview.',\n        },\n    ],\n    '/screen/accounts/no-accounts': [\n        {\n            selector: null,\n            content: 'The accounts screen is all about the accounts you use in your daily life. By adding them, Aeon can automatically gather data from them.',\n        },\n        {\n            selector: '[data-tour=\"accounts-create-account\"]',\n            content: 'Click this button to get started with adding your first account!',\n        },\n    ],\n    '/screen/accounts/new-account': [\n        {\n            selector: null,\n            content: 'This pop-up will help you create a new account.',\n        },\n        {\n            selector: '[data-tour=\"modal-menu-options\"]',\n            content: (\n                <>\n                    <p>These are all currently supported options for gathering your data. You&apos;ll recognise a number of popular social platforms.</p>\n                    <p>If you miss any particular platform, please consider contributing these yourself.</p>\n                </>\n            ),\n        },\n        {\n            selector: '[data-tour=\"accounts-create-account-open-data-rights\"]',\n            content: (\n                <>\n                    <p>This one is a special one. Rather than being tied to a particular website, it allows data requests from any website that supports the Open Data Rights API.</p>\n                    <p>Check if the organisation you want to retrieve data from supports it. If not, petition them to start doing so. Implementing it is quite straightforward!</p>\n                </>\n            ),\n        },\n    ],\n    '/screen/accounts/has-accounts': [\n        {\n            selector: null,\n            content: 'Now that you\\'ve added your first account, you can get started on issuing data requests for this account.',\n        },\n        {\n            selector: '[data-tour=\"accounts-first-account\"]',\n            content:  (\n                <>\n                    <p>This is the account you just created. By clicking on it, you get further information on what you can use it for.</p>\n                    <ClickOnMount selector='a[data-tour=\"accounts-first-account\"]' />\n                </>\n            ),\n        },\n        {\n            selector: '[data-tour=\"accounts-account-overlay\"]',\n            content: 'This details all the information for this account, such as when data was requested.',\n        },\n        {\n            selector: '[data-tour=\"accounts-start-data-request\"]',\n            content: 'While Aeon will automatically gather data from some platforms, you will need to explicitly request it for all. If you\\'re ready to get started, feel free to click it!',\n        },\n    ],\n    '/screen/data': [\n        {\n            selector: null,\n            content: 'This screen gives you a bit more detailed insight into all of the data that is currently active on the internet.',\n        },\n        {\n            selector: '[data-tour=\"data-categories-list\"]',\n            content: <>\n                <p>This list contains every type of data that Aeon is capable of processing. When you click on them, you get a list of all data points of this type that are present.</p>\n                <ClickOnMount selector='[data-tour=\"data-category-button\"]:not([disabled])' />\n            </>,\n        },\n        {\n            selector: '[data-tour=\"data-data-points-list\"]',\n            content: <>\n                <p>The second column contains all of the data points that exist in this particular category. When you click on a single data point, you can get a closer look.</p>\n                <ClickOnMount selector='[data-tour=\"data-data-point-button\"]:not([disabled])' />\n            </>,\n        },\n        {\n            selector: '[data-tour=\"data-datum-overlay\"]',\n            content: 'The right column shows where the data point has come from, why it\\'s there and more.',\n        },\n        {\n            selector: '[data-tour=\"data-delete-datum-button\"]',\n            content: 'If you don\\'t feel whoever has created the data point should be holding on the data, you can choose to remove it. When you do so, Aeon will send out an email to ther organisation with a request to remove the data points.',\n        },\n    ],\n    '/screen/graph': [\n        {\n            selector: null,\n            content: 'This screen contains a visualisation of all of your data points. It should help you get an insight.',\n        },\n        {\n            selector: '[data-tour=\"graph-container\"]',\n            content: <>\n                <p>In this visualisation, the following elements are used:</p>\n                <GraphExplainer />\n            </>,\n        },\n        {\n            selector: '[data-tour=\"graph-container\"]',\n            content: 'Tip: move your mouse over the various elements, and they will highlight connections!',\n        },\n    ],\n    '/screen/settings': [],\n    '/screen/erasure': [\n        {\n            content: 'You\\'ve just tentatively deleted a data point. When you\\'re done selecting data points, head over to the Erasure screen to create a erasure request.',\n            selector: '[data-tour=\"erasure-screen\"]',\n        },\n    ],\n};\n\n// Tentatively add a tour step for creating email-based data requests\nif (demoMode) {\n    steps['/screen/accounts/new-account'].push({\n        selector: '[data-tour=\"accounts-create-account-email\"]',\n        content: 'Or, if the organisation you want to get data from an organisation that does not support any of the other methods, you can just send a plain old email! Just link an email account to Aeon and select the organisation you want to get your data from.',\n    });\n}\n\nexport default steps;"
  },
  {
    "path": "src/app/components/Tour/useTour.tsx",
    "content": "import { State, useAppDispatch } from 'app/store';\nimport { completeTour } from 'app/store/onboarding/actions';\nimport { useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport steps, { TourKeys } from './steps';\nimport { useTour as useBaseTour } from '@reactour/tour';\nimport { tour } from 'app/utilities/env';\n\nfunction useTour(screen: TourKeys) {\n    const dispatch = useAppDispatch();\n    const isTourComplete = useSelector((state: State) => state.onboarding.tour.includes(screen));\n    const { setIsOpen, setSteps, setCurrentStep } = useBaseTour();\n\n    useEffect(() => {\n        // GUARD: Check if tours are enabled application-wide\n        if (!tour) {\n            return;\n        }\n\n        if (!isTourComplete) {\n            setSteps(steps[screen]);\n            setCurrentStep(0);\n            setIsOpen(true);\n            dispatch(completeTour(screen));\n        }\n    }, [setIsOpen, setSteps, isTourComplete, screen]);\n}\n\nexport function Tour({ screen }: { screen: TourKeys }): null {\n    useTour(screen);\n    return null;\n}\n\nexport default useTour;\n"
  },
  {
    "path": "src/app/components/Typography.tsx",
    "content": "import styled, { css } from 'styled-components';\n\nexport interface BaseTextProps {\n    lines?: number;\n}\n\nexport const BaseText = styled.span.attrs((props) => ({\n    title: typeof props.children === 'string' ? props.children : undefined,\n}))<BaseTextProps>`\n    ${(props) => props.lines && css`\n        -webkit-line-clamp: ${props.lines};\n        overflow: hidden;\n        white-space: nowrap;\n    `}\n`;\n\nexport const H1 = styled(BaseText).attrs({ as: 'h1' })`\n    font-weight: 600;\n    color: var(--color-heading);\n`;\n\nexport const H2 = styled(BaseText).attrs({ as: 'h2' })`\n    font-weight: 600;\n    font-size: 24px;\n    margin: 8px 0;\n    line-height: 1.3;\n    font-family: var(--font-heading);\n    color: var(--color-heading);\n`;\n\nexport const H3 = styled(BaseText).attrs({ as: 'h3' })`\n    font-weight: 400;\n    font-size: 16px;\n    margin: 8px 0;\n    color: var(--color-heading);\n`;\n\nexport const H5 = styled(BaseText).attrs({ as: 'h5' })`\n    opacity: 0.5;\n    font-size: 12px;\n    text-transform: uppercase;\n    font-weight: 400;\n    margin: 8px 0;\n`;\n\nexport const Badge = styled(BaseText).attrs({ as: 'div' })`\n    background-color: var(--color-blue-100);\n    color: var(--color-blue-500);\n    font-weight: 500;\n    font-family: var(--font-heading);\n    letter-spacing: 0.2px;\n    padding: 4px 8px;\n    display: inline-block;\n    border-radius: 4px;\n    text-transform: uppercase;\n    font-size: 12px;\n    flex-shrink: 0;\n    white-space: nowrap;\n`;\n\nexport const FontLarge = styled(BaseText).attrs({ as: 'span' })`\n    font-size: 14px;\n`;"
  },
  {
    "path": "src/app/components/Utility.tsx",
    "content": "import styled, { css } from 'styled-components';\n\nexport const Margin = styled.div`\n    padding: 32px;\n`;\n\nexport const MarginSmall = styled.div`\n    padding: 16px 32px;\n`;\n\ninterface PullContainerProps {\n    vertical?: boolean;\n    verticalAlign?: boolean;\n    center?: boolean;\n    gap?: boolean;\n}\n\nexport const PullContainer = styled.div<PullContainerProps>`\n    display: flex;\n\n    ${(props) => props.vertical && css`\n        flex-direction: column;\n    `}\n\n    ${(props) => props.verticalAlign && css`\n        align-items: center;\n    `}\n\n    ${(props) => props.center && css`\n        justify-content: center;\n    `}\n\n    ${(props) => props.gap && css`\n        gap: 8px;\n    `}\n`;\n\nexport const PullLeft = styled.div`\n    margin-right: auto;\n`;\n\nexport const PullRight = styled.div`\n    margin-left: auto;\n`;\n\nexport const PullDown = styled.div`\n    margin-top: auto;\n`;\n\nexport const PullCenter = styled.div`\n    display: flex;\n    justify-content: center;\n`;\n\nexport const EmptyIcon = styled.span`\n    display: inline-block;\n    width: 1.25em;\n`;\n\nexport const MarginLeft = styled.span`\n    margin-left: 1em;\n`;\n\nexport const MarginRight = styled.span`\n    margin-right: 1em;\n`;\n\nexport const Ellipsis = styled.div.attrs((props) => ({\n    title: props.children,\n}))`\n    white-space: nowrap;\n    overflow: hidden;\n    margin-right: 5px;\n`;"
  },
  {
    "path": "src/app/index.ejs",
    "content": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"UTF-8\">\n        <title>Aeon</title>\n        <style type=\"text/css\">\n            #loader {\n                display: flex;\n                width: 100vw;\n                height: 100vh;\n                justify-content: center;\n                align-items: center;\n                color: #0000FF;\n                user-select: none;\n                animation: bounce 0.5s; \n                animation-direction: alternate; \n                animation-timing-function: cubic-bezier(.5, 0.05, 1, .5); \n                animation-iteration-count: infinite; \n                margin-top: -50px;\n            }\n               \n            @keyframes bounce { \n                from { \n                    transform: translate3d(0, 0px, 0); \n                } \n                to { \n                    transform: translate3d(0, 50px, 0); \n                } \n            } \n        </style>\n    </head>\n    <body>\n        <div id=\"root\"></div>\n        <div id=\"modal\"></div>\n        <div id=\"loader\" >\n            <svg viewBox=\"0 0 50 50\" width=\"25\" height=\"25\">\n                <circle cx=\"25\" cy=\"25\" r=\"25\" fill=\"currentColor\" />\n            </svg>\n        </div>\n    </body>\n</html>\n        "
  },
  {
    "path": "src/app/index.tsx",
    "content": "import './polyfill';\nimport React from 'react';\nimport { render } from 'react-dom';\nimport App from './components/App';\n\n// Activate the sourcemaps\nwindow.api.sourceMapSupport.install();\n\n// Log unhandlred rejections as warnings to the console\n// NOTE: This mainly serves to catch async react-spring errors\n// that are thrown when an animation is cancelled early.\nwindow.addEventListener('unhandledrejection', (err) => {\n    console.warn(err.reason);\n    err.preventDefault();\n});\n\n// Initialise React\nrender(<App />, document.getElementById('root'));"
  },
  {
    "path": "src/app/polyfill.ts",
    "content": "/* eslint-disable */\n// @ts-ignore\nglobal = globalThis;"
  },
  {
    "path": "src/app/preload.ts",
    "content": "import {\n    contextBridge,\n    ipcRenderer,\n    shell,\n} from 'electron';\nimport ElectronStore from 'electron-store';\nimport type { CommandLineArguments } from 'main/lib/constants';\n// eslint-disable-next-line\nimport sourceMapSupport from 'source-map-support';\nimport type { State } from './store';\n\nconst store = new ElectronStore();\n\ninterface WindowApi {\n    send: typeof ipcRenderer.send;\n    invoke: typeof ipcRenderer.invoke;\n    on: typeof ipcRenderer.on;\n    removeListener: typeof ipcRenderer.removeListener;\n    sourceMapSupport: typeof sourceMapSupport;\n    store: {\n        persist: (store: unknown) => void;\n        retrieve: () => State;\n        clear: () => void;\n    },\n    openEmailClient: (email: string, subject: string, body: string) => Promise<void>;\n    env: CommandLineArguments;\n}\n\ndeclare global {\n    interface Window {\n        api: WindowApi\n    }\n}\n\nconst channelWhitelist = ['repository', 'providers', 'notifications', 'email' ];\n\nconst windowApi: WindowApi = {\n    send: (channel, ...args) => {\n        // whitelist channels\n        if (channelWhitelist.includes(channel)) {\n            ipcRenderer.send(channel, ...args);\n        }\n    },\n    invoke: (channel, ...args) => {\n        if (channelWhitelist.includes(channel)) {\n            return ipcRenderer.invoke(channel, ...args);\n        }\n    },\n    on: (channel, listener) => {\n        if (channelWhitelist.includes(channel)) {\n            return ipcRenderer.on(channel, listener);\n        }\n    },\n    removeListener: (channel, listener) => {\n        if (channelWhitelist.includes(channel)) {\n            return ipcRenderer.removeListener(channel, listener);\n        }\n    },\n    store: {\n        persist: (state: State) => {\n            return store.set('app_store', state);\n        },\n        retrieve: () => {\n            return store.get('app_store') as State;\n        },\n        clear: () => {\n            return store.clear();\n        },\n    },\n    sourceMapSupport: sourceMapSupport,\n    openEmailClient: (email, subject, body) => shell.openExternal(\n        `mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`,\n    ),\n    env: ipcRenderer.sendSync('env') as CommandLineArguments,\n};\n\n// Expose protected methods that allow the renderer process to use\n// the ipcRenderer without exposing the entire object\ncontextBridge.exposeInMainWorld('api', windowApi);"
  },
  {
    "path": "src/app/screens/Accounts/components/AccountOverlay.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faBell, faCheck, faClock, faLink, faPlus, faQuestion, faUpload } from '@fortawesome/free-solid-svg-icons';\nimport faOpenDataRights from 'app/assets/open-data-rights';\nimport Button, { GhostButton } from 'app/components/Button';\nimport RightSideOverlay, { DetailListItem, Section } from 'app/components/RightSideOverlay';\nimport { FontLarge } from 'app/components/Typography';\nimport { State, useAppDispatch } from 'app/store';\nimport { dispatchEmailRequest } from 'app/store/accounts/actions';\nimport { EmailProvider } from 'app/store/accounts/types';\nimport Providers from 'app/utilities/Providers';\nimport { InitialisedAccount } from 'main/providers/types';\nimport React, { useCallback, useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useNavigate } from 'react-router-dom';\nimport { IconBadgeWithTitle } from 'app/components/IconBadge';\nimport Timestamp from 'app/components/Timestamp';\n\ninterface Props {\n    selectedAccount: string;\n}\n\nfunction hasUrl(account: InitialisedAccount | EmailProvider): account is InitialisedAccount {\n    return 'url' in (account as InitialisedAccount);\n}\n\nfunction AccountOverlay({ selectedAccount }: Props): JSX.Element {\n    const dispatch = useAppDispatch();\n    const navigate = useNavigate();\n    const account = useSelector((state: State) => {\n        if (!selectedAccount) {\n            return;\n        }\n\n        return selectedAccount.startsWith('email_')\n            ? state.accounts.emailProviders.byKey[selectedAccount.split('email_')[1]]\n            : state.accounts.byKey[selectedAccount];\n    });\n    const [isLoading, setLoading] = useState(false);\n\n    // Handle a request for a new request\n    const handleNewRequest = useCallback(async () => {\n        // If the provider is of email type, we just dispatch directly to\n        // the store.\n        if (account.provider === 'email') {\n            // Add some arbitrary delay\n            setLoading(true);\n            await new Promise((resolve) => setTimeout(resolve, 1500));\n            dispatch(dispatchEmailRequest(selectedAccount.split('email_')[1]));\n            setLoading(false);\n        } else {\n            // If not, we dispatch to the backend and have the back-end deal\n            // with it\n            setLoading(true);\n            await Providers.dispatchDataRequest(selectedAccount).catch(() => null);\n            setLoading(false);\n        }\n    }, [selectedAccount, dispatch, setLoading]);\n\n    const handleClose = useCallback(() => {\n        navigate('/accounts');\n    }, [navigate]);\n\n    // GUARD: If there is no account data available (sometimes the data hasn't\n    // yet been retrieved from the back-end), don't render anything.\n    if (!account) {\n        return null;\n    }\n\n    return (\n        <RightSideOverlay data-tour=\"accounts-account-overlay\" onClose={handleClose}>\n            {selectedAccount && (\n                <>\n                    <Section>\n                        <IconBadgeWithTitle icon={Providers.getIcon(account.provider)}>\n                            {account.account}\n                        </IconBadgeWithTitle>\n                    </Section>\n                    <Section well>\n                        <FontLarge>\n                            {hasUrl(account) && \n                                <>\n                                    <DetailListItem>\n                                        <span>\n                                            <FontAwesomeIcon\n                                                icon={faOpenDataRights}\n                                                fixedWidth\n                                            />\n                                        </span>\n                                        <span>Open Data Rights API-based</span>\n                                    </DetailListItem>\n                                    <DetailListItem>\n                                        <span>\n                                            <FontAwesomeIcon\n                                                icon={faLink}\n                                                fixedWidth\n                                            />\n                                        </span>\n                                        <span>Host: {account.url}</span>\n                                    </DetailListItem>\n                                </>\n                            }\n                            <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon\n                                        icon={faQuestion}\n                                        fixedWidth\n                                    />\n                                </span>\n                                <span>Data requested: <Timestamp>{account.status?.dispatched}</Timestamp></span>\n                            </DetailListItem>\n                            <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon\n                                        icon={faClock}\n                                        fixedWidth\n                                    />\n                                </span>\n                                <span>\n                                    Last check: <Timestamp>{account.status.lastCheck}</Timestamp>\n                                </span>\n                            </DetailListItem>\n                            {account.status?.completed && \n                                <DetailListItem>\n                                    <span>\n                                        <FontAwesomeIcon\n                                            icon={faCheck}\n                                            fixedWidth\n                                        />\n                                    </span>\n                                    <span>Completed: <Timestamp>{account.status?.completed}</Timestamp></span>\n                                </DetailListItem>\n                            }\n                        </FontLarge>\n                    </Section>\n                    {account.status?.dispatched && !account.status.completed ? \n                        <Section>\n                            <p>The data request you issued has not been completed yet. We&apos;ll let you know as soon as it&apos;s completed. We&apos;ll notify you if the request exceeds the legal limit of thirty days.</p>\n                            {account.provider !== 'email'\n                                ? (\n                                    <Button\n                                        fullWidth\n                                        icon={faCheck}\n                                        onClick={handleNewRequest}\n                                        disabled\n                                    >\n                                        Complete Data Request\n                                    </Button>\n                                ) : (\n                                    <>\n                                        <Button icon={faUpload} fullWidth>\n                                            Upload archive\n                                        </Button>\n                                        <Button icon={faBell} disabled fullWidth>\n                                            Send reminder\n                                        </Button>\n                                    </>\n                                )\n                            }\n                        </Section>\n                        : \n                        <Section data-tour=\"accounts-start-data-request\">\n                            <p>If you would like to retrieve your data, use the button below to start a new data request.</p>\n                            {account.provider !== 'email'\n                                ? <p>When you click the button, a new window will appear, in which you will asked to enter your credentials. Aeon does not store any of your credentials. Rather, the window is used to perform actions on your behalf.</p>\n                                : <p>When you click the button, we will send out an email on your behalf on the account {(account as EmailProvider).emailAccount}. You have to check if the request is completed yourself, and then upload the resulting archive here.</p>\n                            }\n                            <GhostButton\n                                fullWidth\n                                icon={faPlus}\n                                onClick={handleNewRequest}\n                                disabled={account.status?.dispatched && !account.status.completed}\n                                loading={isLoading}\n                            >\n                                Start Data Request\n                            </GhostButton>\n                        </Section>\n                    }\n                </>\n            )}\n        </RightSideOverlay>\n    );\n}\n\nexport default AccountOverlay;"
  },
  {
    "path": "src/app/screens/Accounts/components/EmailProvider.tsx",
    "content": "import { Dropdown, Label } from 'app/components/Input';\nimport { Margin, MarginSmall, PullCenter } from 'app/components/Utility';\nimport { State, useAppDispatch } from 'app/store';\nimport React, { useCallback, useEffect, useState } from 'react';\nimport AsyncSelect, { AsyncProps } from 'react-select/async';\nimport { useSelector } from 'react-redux';\nimport { GroupBase } from 'react-select';\nimport styled from 'styled-components';\nimport Button from 'app/components/Button';\nimport { faEnvelope } from '@fortawesome/free-solid-svg-icons';\nimport { createEmailAccount } from 'app/store/accounts/actions';\nimport { useNavigate } from 'react-router-dom';\n\nexport interface APIResponse {\n    count: number;\n    next: string;\n    previous: null;\n    results: Result[];\n}\n\nexport interface Result {\n    id: number;\n    display_name: string;\n    legal_name: string;\n    url: string;\n    department: string;\n    street_address: null | string;\n    city: null | string;\n    neighbourhood: null | string;\n    postal_code: null | string;\n    region: null | string;\n    country: string;\n    postal_address: string;\n    requires_identification: boolean | null;\n    operating_countries: string[];\n    custom_identifier: null | string;\n    identifiers: string[];\n    requests:Requests;\n}\n\nexport interface Requests {\n    url?:string;\n    note?: string;\n    email: string;\n    access: Access;\n    deletion:Access;\n    portability: Access;\n    correction: Access;\n}\n\nexport interface Access {\n    url?: string;\n    note?: string;\n}\n\nconst Select = styled(AsyncSelect)`\n    & .Select__control {\n        border: 1px solid #eee;\n        padding: 6px 8px;\n        font-family: var(--font-body);\n    }\n`;\n\nfunction EmailProvider(): JSX.Element {\n    const dispatch = useAppDispatch();\n    const navigate = useNavigate();\n\n    const emailAccounts = useSelector((state: State) => state.email.accounts.all);\n\n    const [selectedEmail, setSelectedEmail] = useState(emailAccounts.length ? emailAccounts[0] : '');\n    const [selectedOrganisation, setSelectedOrganisation] = useState<Result>(undefined);\n    const [organisations, setOrganisations] = useState<Result[]>([]);\n    \n    // A handler responding to React-Select requesting more options\n    const handleSearch: AsyncProps<unknown, false, GroupBase<unknown>>['loadOptions'] = useCallback(async (inputValue: string) => {\n        // Retrieve a set of organisations from the MyDataDone right API\n        const response = await fetch(`https://api.mydatadoneright.eu/api/v1/organizations.json?limit=5&q=${inputValue}`)\n            .then((res) => res.json()) as APIResponse;\n\n        // Translate the response to a react-select readable set of options\n        const options = response.results.map((organisation) => {\n            return {\n                label: organisation.display_name,\n                value: organisation.id,\n            };\n        });\n\n        // Feed them back to react-select\n        setOrganisations(response.results);\n        return options;\n    }, []);\n\n    // Also create a handler for an organisation being selected\n    const handleSelect = useCallback(({ value }: { value: number }) => {\n        const organisation = organisations.find((d) => d.id === value);\n        setSelectedOrganisation(organisation);\n    }, [setSelectedOrganisation, organisations]);\n\n    // Lastly, handle creating a email-account for the selected provider\n    const handleCreate = useCallback(() => {\n        dispatch(createEmailAccount({\n            account: `${selectedOrganisation.display_name}_${selectedEmail}`,\n            emailAccount: selectedEmail,\n            organisation: selectedOrganisation.display_name,\n            status: {},\n            provider: 'email',\n        }));\n        navigate('/accounts');\n    }, [selectedOrganisation, selectedEmail, navigate]);\n    \n    // Redirect a user to the Create New Email Account modal, when they select\n    // the option from the email accounts dropdown\n    useEffect(() => {\n        if (selectedEmail === 'Create New Email Account...') {\n            navigate('/settings/email-accounts?create-new-email-account');\n            setSelectedEmail(emailAccounts.length ? emailAccounts[0] : '');\n        }\n    }, [selectedEmail, setSelectedEmail, navigate]);\n\n    return (\n        <Margin>\n            <p>For organisations that do not offer dedicated manners of requesting data, there is always email. You will be able to send out a generated email, after which you can upload the retrieved data yourself.</p>\n            <p>In order to use this provider, you must link an email address to Aeon. Selected a previously linked email address in the list below or link one first. Also select the organisation you wish to receive data from.</p>\n            <Dropdown \n                options={[...emailAccounts, 'Create New Email Account...']}\n                label=\"Email Account\" \n                value={selectedEmail}\n                onSelect={setSelectedEmail}\n            />\n            <Label>\n                Organisation\n                <Select\n                    classNamePrefix=\"Select\"\n                    loadOptions={handleSearch}\n                    onChange={handleSelect}\n                    placeholder=\"Start typing to select...\"\n                />\n            </Label>\n            <MarginSmall>\n                <PullCenter>\n                    <Button \n                        disabled={!selectedEmail || !selectedOrganisation}\n                        onClick={handleCreate}\n                        icon={faEnvelope}\n                    >\n                        Create account for {selectedOrganisation?.display_name}\n                    </Button>\n                </PullCenter>\n            </MarginSmall>\n        </Margin>\n    );\n}\n\nexport default EmailProvider;"
  },
  {
    "path": "src/app/screens/Accounts/components/NewAccountModal.tsx",
    "content": "import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';\nimport { faPlus } from '@fortawesome/free-solid-svg-icons';\nimport Button from 'app/components/Button';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport Modal from 'app/components/Modal';\nimport ModalMenu from 'app/components/Modal/Menu';\nimport { PullContainer, Margin, PullCenter } from 'app/components/Utility';\nimport { addProviderAccount } from 'app/store/accounts/actions';\nimport { State, useAppDispatch } from 'app/store';\nimport { useSelector } from 'react-redux';\nimport Providers from 'app/utilities/Providers';\nimport { Dropdown, Label, TextInput } from 'app/components/Input';\nimport { InitOptionalParameters } from 'main/providers/types';\nimport isValidUrl from 'app/utilities/isValidUrl';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport EmailProvider from './EmailProvider';\nimport useTour from 'app/components/Tour/useTour';\nimport { demoMode } from 'app/utilities/env';\n\ntype NewAccountProps = PropsWithChildren<{ \n    client: string, \n    onComplete: () => void,\n    disabled?: boolean;\n    optionalParameters: InitOptionalParameters;\n    selectedEmail?: string;\n}>;\n\nfunction NewAccountButton({ client, children, onComplete, optionalParameters, ...props }: NewAccountProps): JSX.Element {\n    const [isActive, setActive] = useState(false);\n    const dispatch = useAppDispatch();\n\n    // A handler for creating a new email account\n    const handleClick = useCallback(async () => {\n        // Set activity flag\n        setActive(true);\n\n        // Actually create a new account\n        await dispatch(addProviderAccount({ client, optionalParameters }));\n\n        // Set new activity flag, and let parent component know we're done\n        setActive(false);\n        onComplete();\n    }, [dispatch, client, setActive, optionalParameters]);\n\n    return (\n        <Button icon={faPlus} {...props} onClick={handleClick} loading={isActive}>{children}</Button>\n    );\n}\n\nfunction NewAccountModal(): JSX.Element {\n    useTour('/screen/accounts/new-account');\n    const location = useLocation();\n    const navigate = useNavigate();\n\n    // Selectors\n    const { allProviders, availableProviders } = useSelector((state: State) => state.accounts);\n    const emailAccounts = useSelector((state: State) => state.email.accounts.all);\n    \n    // If demo mode is activated, we insert a dummy 'email' provider option,\n    // which theoretically can be used to automatically send emails to\n    // providers, but as of yet is not working. Lets call it Wizard of Oz.\n    const all = demoMode\n        ? [...allProviders, 'email']\n        : allProviders;\n\n    // State    \n    const [modalIsOpen, setModal] = useState(false);\n    const [selectedEmail, setSelectedEmail] = useState(emailAccounts.length ? emailAccounts[0] : '');\n    const [selectedUrl, setSelectedUrl] = useState('');\n\n    // Handlers\n    const closeModal = useCallback(() => {\n        navigate(location.pathname);\n    }, [location]);\n\n    const openModal = useCallback(() => { \n        navigate(location.pathname + '?create-new-account');\n    }, [location]);\n\n    const handleUrlChange = useCallback((event) => { \n        setSelectedUrl(event.target.value);\n    }, [setSelectedUrl]);\n    \n    // Make whether the modal is open dependent on the query parameters\n    useEffect(() => {\n        const params = new URLSearchParams(location.search);\n        setModal(params.has('create-new-account'));\n    }, [location, setModal]);\n    \n    // Redirect a user to the Create New Email Account modal, when they select\n    // the option from the email accounts dropdown\n    useEffect(() => {\n        if (selectedEmail === 'Create New Email Account...') {\n            navigate('/settings/email-accounts?create-new-email-account');\n            setSelectedEmail(emailAccounts.length ? emailAccounts[0] : '');\n        }\n    }, [selectedEmail, setSelectedEmail, navigate]);\n    \n    return (\n        <>\n            <Button fullWidth icon={faPlus} onClick={openModal} data-tour=\"accounts-create-account\">\n                Add new account\n            </Button>\n            <Modal isOpen={modalIsOpen} onRequestClose={closeModal}>\n                <ModalMenu labels={all.map((key) =>\n                    <PullContainer verticalAlign key={key} data-tour={`accounts-create-account-${key}`}>\n                        <FontAwesomeIcon icon={Providers.getIcon(key)} style={{ marginRight: '1em' }} />\n                        {key.replace(/(-|_)/g, ' ')}\n                    </PullContainer>,\n                )}>\n                    {all.map((key) => key === 'email' ? \n                        <EmailProvider key=\"email\" />\n                        : (\n                            <Margin key={key}>\n                                {key !== 'email' && key !== 'open-data-rights'\n                                    ? <p>By adding a new account for {key}, you are able to retrieve your data from them.</p>\n                                    : <p>With this option, you are requesting your data for another organisation that is served by this method.</p>}\n                                {key !== 'email' \n                                    ? <p>When you create a new account, a window will pop up asking for your credentials. Aeon will never store your credentials. Rather, when you log in, Aeon can hijack the window to perform actions on your behalf. These actions are limited to doing data requests for you.</p> \n                                    : null}\n                                {availableProviders[key].requiresEmail &&\n                                <>\n                                    <p>In order to use this provider, you must link an email address to Aeon. Selected a previously linked email address in the list below or link one first.</p>\n                                    <Dropdown \n                                        options={[...emailAccounts, 'Create New Email Account...']}\n                                        label=\"Email Account\" \n                                        value={selectedEmail}\n                                        onSelect={setSelectedEmail}\n                                        placeholder=\"Please select an email account\"\n                                    />\n                                </>\n                                }\n                                {availableProviders[key].requiresUrl &&\n                                <>\n                                    <p>This provider allows you to get data from any organisation that supports the Open Data Rights API. Please enter the URL for the organisation:</p>\n                                    <Label>\n                                        <span>Open Data Rights API URL</span>\n                                        <TextInput \n                                            value={selectedUrl}\n                                            onChange={handleUrlChange}\n                                            placeholder=\"https://open-data.acme-corp.com\"\n                                            type=\"url\"\n                                        />\n                                    </Label>\n                                </>\n                                }\n                                <PullCenter>\n                                    <NewAccountButton\n                                        client={key}\n                                        optionalParameters={{\n                                            accountName: availableProviders[key].requiresEmail ? selectedEmail : undefined,\n                                            apiUrl: availableProviders[key].requiresUrl ? selectedUrl : undefined,\n                                        }}\n                                        onComplete={closeModal}\n                                        disabled={\n                                            availableProviders[key].requiresEmail && selectedEmail === ''\n                                        || availableProviders[key].requiresUrl && !isValidUrl(selectedUrl)\n                                        }\n                                    >\n                                        Add new {key.replace(/(-|_)/g, ' ')} account\n                                    </NewAccountButton>\n                                </PullCenter>\n                            </Margin>\n                        ))}\n                </ModalMenu>\n            </Modal>\n        </>\n    );\n}\n\nexport default NewAccountModal;"
  },
  {
    "path": "src/app/screens/Accounts/getDescription.ts",
    "content": "import { formatDistanceToNow } from 'date-fns';\nimport { DataRequestStatus } from 'main/providers/types';\n\n/**\n * A helper to convert a particular DataRequestStatus to a human-readable string\n */\nexport default function getDescription(status?: DataRequestStatus): string {\n    if (status?.completed) {\n        return `Received data ${formatDistanceToNow(new Date(status.completed))} ago`;\n    }\n\n    if (status?.dispatched) {\n        return `Requested data ${formatDistanceToNow(new Date(status.dispatched))} ago`;\n    }\n\n    return 'No data requested yet';\n}"
  },
  {
    "path": "src/app/screens/Accounts/index.tsx",
    "content": "import React, { useCallback } from 'react';\nimport { Category, List, NavigatableListEntry, PanelBottomButtons, PanelGrid, SplitPanel } from 'app/components/PanelGrid';\nimport Providers from 'app/utilities/Providers';\nimport { useParams } from 'react-router-dom';\nimport { RouteProps } from '../types';\nimport AccountOverlay from './components/AccountOverlay';\nimport getDescription from './getDescription';\nimport styled from 'styled-components';\nimport { GhostButton } from 'app/components/Button';\nimport { faEnvelope, faSync } from '@fortawesome/free-solid-svg-icons';\nimport { useAccounts } from 'app/store/accounts/selectors';\nimport { State, useAppDispatch } from 'app/store';\nimport { useSelector } from 'react-redux';\nimport { refreshRequests } from 'app/store/accounts/actions';\nimport NewAccountModal from './components/NewAccountModal';\nimport useTour from 'app/components/Tour/useTour';\n\nconst StatusDescription = styled.span`\n    font-size: 12px;\n    opacity: 0.5;\n    font-weight: 400;\n`;\n\nconst Rows = styled.div`\n    display: flex;\n    flex-direction: column;\n    line-height: 1.5;\n`;\n\nfunction Accounts(): JSX.Element {\n    const dispatch = useAppDispatch();\n    const isLoadingRefresh = useSelector((state: State) => state.accounts.isLoading.refresh);\n    const { accounts, map, email } = useAccounts();\n    const { account: selectedAccount } = useParams<RouteProps['requests']>();\n\n    // Callback for refreshing all requests\n    const refresh = useCallback(() => dispatch(refreshRequests()), [dispatch]);\n\n    useTour(accounts.length ? '/screen/accounts/has-accounts' : '/screen/accounts/no-accounts');\n\n    return (\n        <>\n            <PanelGrid columns={2}>\n                <SplitPanel>\n                    <List>\n                        <Category title=\"Automated Requests\" id=\"automated-accounts\">\n                            {accounts.map((account, i) => \n                                <NavigatableListEntry\n                                    key={account}\n                                    to={`/accounts/${account}`}\n                                    icon={Providers.getIcon(map[account].provider)}\n                                    data-tour={i === 0 ? 'accounts-first-account' : undefined}\n                                    large\n                                >\n                                    <Rows>\n                                        <span>{map[account].account}</span>\n                                        <StatusDescription>{getDescription(map[account].status)}</StatusDescription>\n                                    </Rows>\n                                </NavigatableListEntry>,\n                            )}\n                        </Category>\n                        <Category title=\"Email-based Requests\" id=\"email-accounts\">\n                            {email.all.map((account) => (\n                                <NavigatableListEntry\n                                    key={account}\n                                    to={`/accounts/email_${account}`}\n                                    icon={faEnvelope}\n                                    large\n                                >\n                                    <Rows>\n                                        <span>{email.byKey[account].organisation} ({email.byKey[account].emailAccount})</span>\n                                        <StatusDescription>{getDescription(email.byKey[account].status)}</StatusDescription>\n                                    </Rows>\n                                </NavigatableListEntry>\n                            ))}\n                        </Category>\n                    </List>\n                    <PanelBottomButtons>\n                        <NewAccountModal />\n                        <GhostButton fullWidth icon={faSync} loading={isLoadingRefresh} onClick={refresh}>\n                            Refresh requests\n                        </GhostButton>\n                    </PanelBottomButtons>\n                </SplitPanel>\n                <List>\n                    <AccountOverlay selectedAccount={selectedAccount} />\n                </List>\n            </PanelGrid>\n        </>\n    );\n}\n\nexport default Accounts;"
  },
  {
    "path": "src/app/screens/Data/components/DatumOverlay.tsx",
    "content": "import React, { memo, useCallback } from 'react';\nimport { GhostButton } from 'app/components/Button';\nimport { ProvidedDataTypes } from 'main/providers/types/Data';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faFile, faClock, faLink, faTrash, faUser } from '@fortawesome/free-solid-svg-icons';\nimport { FontLarge } from 'app/components/Typography';\nimport DataType from 'app/utilities/DataType';\nimport Providers from 'app/utilities/Providers';\nimport RightSideOverlay, { DetailListItem, Section } from 'app/components/RightSideOverlay';\nimport { useLocation, useNavigate, useParams } from 'react-router-dom';\nimport { RouteProps } from 'app/screens/types';\nimport { State, useAppDispatch } from 'app/store';\nimport { deleteDatum } from 'app/store/data/actions';\nimport { useSelector } from 'react-redux';\nimport { IconBadgeWithTitle } from 'app/components/IconBadge';\n\ninterface Props {\n    datumId: number;\n    overlay?: boolean;\n}\n\nconst DatumOverlay = memo(function DatumOverlay({ datumId, overlay }: Props): JSX.Element {\n    const { byKey, deleted } = useSelector((state: State) => state.data);\n    const datum = datumId && byKey[datumId];\n    const isDeleted = datumId && deleted.includes(datumId);\n\n    // Create handler that redirects the page if the overlay is closed\n    const { category } = useParams<RouteProps['data']>();\n    const navigate = useNavigate();\n    const location = useLocation();\n    const handleClose = useCallback(() => {\n        if (location.pathname.startsWith('/data')) {\n            navigate(`/data/${category}`);\n        } else if (location.pathname.startsWith('/graph')) {\n            navigate('/graph');\n        }\n    }, [navigate, category, location]);\n\n    // Create handler for deleting data points\n    const dispatch = useAppDispatch();\n    const handleDelete = useCallback(() => {\n        dispatch(deleteDatum(datumId));\n    }, [dispatch, datumId]);\n\n    return (\n        <RightSideOverlay onClose={handleClose} data-tour=\"data-datum-overlay\" overlay={overlay}>\n            {datum && (\n                <>\n                    <Section>\n                        <IconBadgeWithTitle icon={DataType.getIcon(datum.type as ProvidedDataTypes)}>\n                            {DataType.toString(datum)}\n                        </IconBadgeWithTitle>\n                        {isDeleted && <p>\n                            This data point is marked for erasure    \n                        </p>}\n                    </Section>\n                    <Section well>\n                        <FontLarge>\n\n                            <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon\n                                        icon={Providers.getIcon(datum.provider)}\n                                        fixedWidth\n                                    />\n                                </span>\n                                <span style={{ textTransform: 'capitalize' }}>\n                                    {datum.provider.replace(/(_|-)/g, ' ')}\n                                </span>\n                            </DetailListItem>\n                            {datum.hostname && \n                                <DetailListItem>\n                                    <span>\n                                        <FontAwesomeIcon\n                                            icon={faLink}\n                                            fixedWidth\n                                        />\n                                    </span>\n                                    <span>{datum.hostname}</span>\n                                </DetailListItem>\n                            }\n                            {datum.account && \n                                <DetailListItem>\n                                    <span>\n                                        <FontAwesomeIcon\n                                            icon={faUser}\n                                            fixedWidth\n                                        />\n                                    </span>\n                                    <span>{datum.account}</span>\n                                </DetailListItem>\n                            }\n                            <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon icon={faFile} fixedWidth />\n                                </span>\n                                <span style={{ textTransform: 'uppercase' }}>\n                                    {datum.type}\n                                </span>\n                            </DetailListItem>\n                            {datum.timestamp &&                            \n                                <DetailListItem>\n                                    <span>\n                                        <FontAwesomeIcon icon={faClock} fixedWidth />\n                                    </span>\n                                    <span>\n                                        {datum.timestamp?.toLocaleString()}\n                                    </span>\n                                </DetailListItem>\n                            }\n                            {/* <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon icon={faHashtag} fixedWidth />\n                                </span>\n                                <span>\n                                    2 other occurrences on other platforms\n                                </span>\n                            </DetailListItem>\n                            <DetailListItem>\n                                <span>\n                                    <FontAwesomeIcon icon={faEye} fixedWidth />\n                                </span>\n                                <span>\n                                    Data is visisble\n                                </span>\n                            </DetailListItem> */}\n                        </FontLarge>\n                    </Section>\n                    <Section>\n                        <code style={{ textTransform: 'uppercase' }}>\n                            {datum.type}\n                        </code>\n                        <p>{DataType.getDescription(datum)}</p>\n                    </Section>\n                    <Section>\n                        <GhostButton\n                            fullWidth\n                            onClick={handleDelete}\n                            backgroundColor=\"red\"\n                            data-tour=\"data-delete-datum-button\"\n                            data-telemetry-id=\"datum-overlay-delete-datapoint\"\n                            disabled={isDeleted}\n                            icon={faTrash}\n                        >\n                            Delete this data point\n                        </GhostButton>\n                        {/* <Button\n                            fullWidth\n                            onClick={handleModify}\n                            backgroundColor=\"yellow\"\n                        >\n                            Modify this data point\n                        </Button> */}\n                    </Section>\n                </>\n            )}\n        </RightSideOverlay>\n    );\n});\n\nexport default DatumOverlay;"
  },
  {
    "path": "src/app/screens/Data/index.tsx",
    "content": "import React from 'react';\nimport { ProvidedDataTypes } from 'main/providers/types/Data';\nimport Loading from 'app/components/Loading';\nimport {\n    ClickableCategory,\n    ClickableDataPoint,\n} from './styles';\nimport DatumOverlay from './components/DatumOverlay';\nimport { RouteProps } from '../types';\nimport { useParams } from 'react-router-dom';\nimport { Category, List, PanelGrid } from 'app/components/PanelGrid';\nimport NoData from 'app/components/NoData';\nimport { useSelector } from 'react-redux';\nimport { State } from 'app/store';\nimport useTour from 'app/components/Tour/useTour';\n\nfunction Data(): JSX.Element {\n    useTour('/screen/data');\n    const { category, datumId } = useParams<RouteProps['data']>();\n    const {\n        isLoading,\n        byKey,\n        byType,\n        deleted,\n        deletedByType,\n    } = useSelector((state: State) => state.data);\n    const parsedDatumId = Number.parseInt(datumId);\n\n    if (isLoading) {\n        return <Loading />;\n    }\n\n    if (!byKey.length) {\n        return <NoData />;\n    }\n\n    return (\n        <PanelGrid>\n            <List data-tour=\"data-categories-list\">\n                <Category title=\"Categories\">\n                    {Object.values(ProvidedDataTypes)\n                        .filter((k) => byType[k].length > 0)\n                        .sort()\n                        .map((key) => (\n                            <ClickableCategory\n                                key={key}\n                                type={key}\n                                items={byType[key].length}\n                                disabled={byType[key].length > 0}\n                                deleted={deletedByType[key].length > 0}\n                                data-tour={byType[key].length ? 'data-category-button' : ''}\n                                data-telemetry-id={`new-commit-select-category-${key}`}\n                            />\n                        ))\n                    }\n                </Category>\n            </List>\n            <List data-tour=\"data-data-points-list\">\n                <Category title=\"Data Points\">\n                    {category && byType[category].map((datum) => (\n                        <ClickableDataPoint\n                            type={category as ProvidedDataTypes}\n                            datum={byKey[datum]}\n                            index={datum}\n                            key={`datum-${datum}`} \n                            deleted={deleted.includes(datum)}\n                            data-tour=\"data-data-point-button\"\n                            data-telemetry-id={`new-commit-select-data-point-${datum}`}\n                        />\n                    ))}\n                </Category>\n            </List>\n            <List>\n                <DatumOverlay datumId={parsedDatumId} />\n            </List>\n        </PanelGrid>\n    );\n}\n\nexport default Data;"
  },
  {
    "path": "src/app/screens/Data/styles.tsx",
    "content": "import React, { HTMLAttributes } from 'react';\nimport { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Data';\nimport styled, { css } from 'styled-components';\nimport DataType from 'app/utilities/DataType';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { Ellipsis } from 'app/components/Utility';\nimport { NavigatableListEntry } from 'app/components/PanelGrid';\n\ninterface ListButtonProps extends HTMLAttributes<HTMLAnchorElement> {\n    disabled?: boolean;\n    deleted?: boolean;\n    modified?: boolean;\n    added?: boolean;\n}\n\nexport const StyledListButton = styled(NavigatableListEntry)<ListButtonProps>`\n    img {\n        max-height: 100px;\n        width: auto;\n        border-radius: 5px;\n    }\n\n    ${(props) => props.added && css`\n    background-color: var(--color-green-100);\n\n        &:hover:not(.active) {\n            background-color: var(--color-green-200) !important;\n        }\n\n        &.active {\n            background-color: var(--color-green-500);\n        }\n    `}\n\n    ${(props) => props.deleted && css`\n        background-color: var(--color-red-100);\n\n        &:hover:not(.active) {\n            background-color: var(--color-red-200) !important;\n        }\n\n        &.active {\n            background-color: var(--color-red-500);\n        }\n    `}\n\n    ${(props) => props.modified && css`\n        background-color: var(--color-yellow-100);\n\n        &:hover:not(.active) {\n            background-color: var(--color-yellow-200) !important;\n        }\n\n        &.active {\n            background-color: var(--color-yellow-500);\n        }\n    `}\n\n    &:disabled {\n        opacity: 0.25;\n    }\n`;\n\n\nconst NumberOfItems = styled.span`\n    margin-left: 4px;\n    opacity: 0.25;\n    font-weight: 300;\n`;\n\ninterface ClickableCategoryProps extends Omit<ListButtonProps, 'onClick'> {\n    type: ProvidedDataTypes;\n    items?: number;\n}\n\nexport const ClickableCategory = ({ type, items, ...props }: ClickableCategoryProps): JSX.Element => {\n    return (\n        <StyledListButton to={`/data/${type}`} {...props}>\n            <FontAwesomeIcon icon={DataType.getIcon(type)} fixedWidth style={{ marginRight: 8 }} />\n            {type} <NumberOfItems>{items > 0 ? `(${items})` : null}</NumberOfItems>\n        </StyledListButton>\n    );\n};\n\ninterface ClickableDataPointProps extends Omit<ListButtonProps, 'onClick'> {\n    datum: ProviderDatum<unknown, unknown>;\n    index: number;\n    type: ProvidedDataTypes;\n}\n\nexport const ClickableDataPoint = ({ datum, type, index, ...props }: ClickableDataPointProps): JSX.Element => {\n    return (\n        <StyledListButton to={`/data/${type}/${index}`} {...props}>\n            <FontAwesomeIcon icon={DataType.getIcon(datum.type as ProvidedDataTypes)} fixedWidth style={{ marginRight: 8 }} />\n            <Ellipsis>{DataType.toString(datum)}</Ellipsis>\n        </StyledListButton>\n    );\n};"
  },
  {
    "path": "src/app/screens/Data/types.tsx",
    "content": "import { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Data';\n\nexport type GroupedData =  { [key: string]: ProviderDatum<string, ProvidedDataTypes>[] };\nexport type DeletedData = { [key: string]: number[] };"
  },
  {
    "path": "src/app/screens/Erasure/Emails.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faEnvelope } from '@fortawesome/free-solid-svg-icons';\nimport Button from 'app/components/Button';\nimport Modal from 'app/components/Modal';\nimport ModalMenu from 'app/components/Modal/Menu';\nimport { PullContainer, MarginLeft, MarginSmall } from 'app/components/Utility';\nimport { State } from 'app/store';\nimport Providers from 'app/utilities/Providers';\nimport React, { useCallback } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useNavigate } from 'react-router-dom';\nimport styled from 'styled-components';\nimport generateEmail from './generateEmail';\n\nconst ScrollContainer = styled.div`\n    background-color: var(--color-gray-200);\n    font-family: var(--font-mono);\n    max-height: 40vh;\n    overflow: auto;\n    padding: 16px;\n    font-size: 12px;\n    white-space: pre-line;\n`;\n\nfunction ErasureEmails(): JSX.Element {\n    const navigate = useNavigate();\n    const { byKey, deletedByProvider } = useSelector((state: State) => state.data);\n\n    // Redirect to timeline on menu item close\n    const handleClose = useCallback(() => {\n        navigate('/timeline');\n    }, [navigate]);\n\n    return (\n        <Modal isOpen onRequestClose={handleClose}>\n            <ModalMenu labels={Object.keys(deletedByProvider).map((key) =>\n                <PullContainer verticalAlign key={key}>\n                    <FontAwesomeIcon icon={Providers.getIcon(key)} />\n                    <MarginLeft>{key.replace(/(-|_)/g, ' ')}</MarginLeft>\n                </PullContainer>,\n            )}>\n                {Object.keys(deletedByProvider).map((provider) => {\n                    const data = deletedByProvider[provider].map((key) => byKey[key]);\n                    const email = generateEmail(data, provider);\n\n                    return (\n                        <>\n                            <ScrollContainer>\n                                {email}\n                            </ScrollContainer>\n                            <MarginSmall>\n                                <Button\n                                    icon={faEnvelope}\n                                    fullWidth\n                                    onClick={() => window.api.openEmailClient(Providers.getPrivacyEmail(provider), 'Request for Erasure', email)}\n                                >\n                                    Send in e-mail client\n                                </Button>\n                            </MarginSmall>\n                        </>\n                    );\n                })}\n            </ModalMenu>\n        </Modal>\n    );\n}\n\nexport default ErasureEmails;"
  },
  {
    "path": "src/app/screens/Erasure/generateEmail.ts",
    "content": "import DataType from 'app/utilities/DataType';\nimport { ProviderDatum } from 'main/providers/types/Data';\n\n/**\n * Generate an email body using the supplied data, with the selected provider.\n * Email text courtesy of Bits of Freedom, adapted 09/12/2020, from:\n * https://code.bitsoffreedom.nl/bitsoffreedom/mydatadoneright/frontend/-/tree/master/assets/locales/en\n */\nfunction generateEmail(data: ProviderDatum<unknown, unknown>[], provider: string): string {\n    return `\n        Date: ${new Date().toLocaleDateString()}\n        To Whom It May Concern:\n\n        I am invoking my right to erasure as specified in Article 17 of the General Data Protection Regulation. I am requesting ${provider} to erase the following personal data it processes about me without undue delay:\n        ${data.map((datum) => `* ${datum.type}: ${DataType.toString(datum)} (found in: ${Array.isArray(datum.source) ? datum.source.join('/') : datum.source})`).join('\\n')}\n\n        The reason for my removal request is that I withdraw my prior given consent. This request relates to any processing of my personal data by ${provider}, including any processors processing personal data on behalf of ${provider}.\n\n        Please provide your response through secure means by email to ${data[0].account}. I look forward to receiving a response within one month of receipt of my request.\n    `;\n}\n\nexport default generateEmail;"
  },
  {
    "path": "src/app/screens/Erasure/index.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faTrash } from '@fortawesome/free-solid-svg-icons';\nimport Button, { GhostButton } from 'app/components/Button';\nimport Modal from 'app/components/Modal';\nimport { H3 } from 'app/components/Typography';\nimport { MarginSmall, PullContainer } from 'app/components/Utility';\nimport { State, useAppDispatch } from 'app/store';\nimport { resetDeletedData } from 'app/store/data/actions';\nimport DataType from 'app/utilities/DataType';\nimport Providers from 'app/utilities/Providers';\nimport { ProvidedDataTypes } from 'main/providers/types/Data';\nimport React, { useCallback } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useNavigate } from 'react-router-dom';\nimport styled from 'styled-components';\n\nconst IconWrapper = styled.div`\n    margin: 0 8px;\n`;\n\nconst Datum = styled.div`\n    display: flex;\n`;\n\nconst Heading = styled(H3)`\n    display: flex;\n    font-weight: 600;\n`;\n\nconst Provider = styled.div`\n    margin-bottom: 25px;\n`;\n\nconst ResetButton = styled(GhostButton)`\n    margin-left: 8px;\n    background-color: transparent;\n`;\n\nconst ScrollContainer = styled.div`\n    background-color: var(--color-red-50);\n    font-family: var(--font-mono);\n    max-height: 25vh;\n    overflow: auto;\n    padding: 16px;\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n`;\n\nfunction Erasure(): JSX.Element {\n    const navigate = useNavigate();\n    const dispatch = useAppDispatch();\n    const { byKey, deleted, deletedByProvider } = useSelector((state: State) => state.data);\n\n    // Redirect to timeline on menu item close\n    const handleClose = useCallback(() => {\n        navigate('/timeline');\n    }, [navigate]);\n\n    // Redirect to next window if the user wants to delete stuff\n    const handleDelete = useCallback(() => {\n        navigate('/erasure/emails');\n    }, [navigate]);\n\n    // Reset the deleted items, and close the modal\n    const handleReset = useCallback(() => {\n        dispatch(resetDeletedData());\n        handleClose();\n    }, [handleClose, dispatch]);\n\n    return (\n        <Modal isOpen onRequestClose={handleClose}>\n            <MarginSmall><p>You have selected the following data point{deleted.length > 1 ? 's' : ''} for removal</p></MarginSmall>\n            <ScrollContainer>\n                {Object.keys(deletedByProvider).map((provider) => (\n                    <Provider key={provider}>\n                        <Heading>\n                            <IconWrapper>\n                                <FontAwesomeIcon icon={Providers.getIcon(provider)} fixedWidth />\n                            </IconWrapper>\n                            {provider}\n                        </Heading>\n                        {deletedByProvider[provider].map((key) => (\n                            <Datum key={key}>\n                                <IconWrapper>\n                                    <FontAwesomeIcon icon={DataType.getIcon(byKey[key].type as ProvidedDataTypes)} fixedWidth />\n                                </IconWrapper>\n                                {DataType.toString(byKey[key])} [{byKey[key].type}]\n                            </Datum>\n                        ))}\n                    </Provider>\n                ))}\n            </ScrollContainer>\n            <MarginSmall>\n                <p>To actually remove this data from their origins, you must send a request for erasure to the organisation processing it. So long as the organisation is processing this data, it will remain in Aeon.</p>\n                <p>When you&apos;re ready to erase these data-points with their respective providers, click the button below. This will generate a seperate email for each source, which will you need to send out yourself.</p>\n                <PullContainer center>\n                    <Button backgroundColor=\"red\" icon={faTrash} onClick={handleDelete}>\n                        Remove {deleted.length} data {deleted.length > 1 ? 'points' : 'point'}\n                    </Button>\n                    <ResetButton onClick={handleReset}>\n                        Reset removed data points\n                    </ResetButton>\n                </PullContainer>\n            </MarginSmall>\n        </Modal>\n    );\n}\n\nexport default Erasure;"
  },
  {
    "path": "src/app/screens/Graph/calculateGraph.ts",
    "content": "import DataType from 'app/utilities/DataType';\nimport { EdgeDefinition, ElementsDefinition, NodeDefinition } from 'cytoscape';\nimport { uniqBy } from 'lodash-es';\nimport { ProviderDatum } from 'main/providers/types/Data';\n\n/**\n * Transforms an incoming set of ProivderDatums into a graph that can be\n * consumed by Cytoscape\n * @param data \n */\nexport default function calculateGraph(data: ProviderDatum<unknown, unknown>[]): ElementsDefinition {\n    // Retrieve all unique providers, accounts and sources that are present on the data\n    const uniqueProviders = uniqBy(data, 'provider');\n    const uniqueAccounts = uniqBy(data, (datum) => `${datum.provider}_${datum.account}`);\n    // const uniqueSources = uniqBy(data, 'source');\n    const uniqueTypes = uniqBy(data, 'type');\n\n    // Create a variable that will hold all the future nodes\n    const nodes: NodeDefinition[]  = [\n        ...uniqueProviders.map((datum): NodeDefinition => ({\n            data: {\n                id: `provider_${datum.provider}`,\n                label: datum.provider,\n                type: 'provider',\n            },\n        })),\n        ...uniqueAccounts.map((datum): NodeDefinition => ({\n            data: {\n                id: `account_${datum.provider}_${datum.account}`,\n                label: datum.account,\n                type: 'account',\n            },\n        })),\n        ...uniqueTypes.map((datum): NodeDefinition => ({\n            data: {\n                id: `type_${datum.type}`,\n                label: datum.type,\n                type: 'type',\n                datumType: datum.type,\n            },\n        })),\n        ...data.map((datum, i): NodeDefinition => ({\n            data: {\n                id: `datum_${i}`,\n                label: DataType.toString(datum),\n                type: 'datum',\n                datumType: datum.type,\n                parent: `parent_type_${datum.type}`,\n                i,\n            },\n        })),\n    ];\n\n    // Create a variable that will hold all future edges\n    const edges: EdgeDefinition[] = [\n        ...uniqueAccounts.map((datum): EdgeDefinition => ({\n            data: {\n                source: `account_${datum.provider}_${datum.account}`,\n                target: `provider_${datum.provider}`,\n                type: 'account_provider',\n            },\n        })),\n        ...data.flatMap((datum, i): EdgeDefinition[] => ([\n            {\n                data: {\n                    source: `datum_${i}`,\n                    target: `account_${datum.provider}_${datum.account}`,\n                    type: 'datum_account',\n                },\n            },\n            {\n                data: {\n                    source: `type_${datum.type}`,\n                    target: `datum_${i}`,\n                    type: 'datum_type',\n                },\n            },\n        ])),\n    ];\n\n    return {\n        nodes,\n        edges,\n    };\n}"
  },
  {
    "path": "src/app/screens/Graph/explainer.tsx",
    "content": "import React from 'react';\nimport styled from 'styled-components';\n\nconst Provider = styled.div`\n    width: 25px;\n    height: 25px;\n    border-radius: 25px;\n    background-color: var(--color-primary);\n    border: 4px solid var(--color-blue-200);\n`;\n\nconst Account = styled.div`\n    width: 25px;\n    height: 25px;\n    border-radius: 25px;\n    background-color: var(--color-blue-200);\n`;\n\nconst Type = styled.div`\n    width: 25px;\n    height: 25px;\n    border-radius: 6px;\n    background-color: var(--color-gray-300);\n`;\n\nconst DataPoint = styled.div`\n    width: 10px;\n    height: 10px;\n    border-radius: 10px;\n    margin-left: 6.5px;\n    margin-right: 6.5px;\n    background-color: var(--color-gray-300);\n`;\n\nconst Line = styled.div`\n    display: flex;\n    align-items: center;\n    margin-bottom: 16px;\n\n    & > div {\n        flex: 0 0 auto;\n    }\n\n    & > span {\n        margin-left: 16px;\n        line-height: 1.5;\n    }\n`;\n\nfunction GraphExplainer(): JSX.Element {\n    return (\n        <div>\n            <Line>\n                <Provider />\n                <span>The platform from which the data was accessed</span>\n            </Line>\n            <Line>\n                <Account />\n                <span>The account belonging to the data</span>\n            </Line>\n            <Line>\n                <Type />\n                <span>A type of data</span>\n            </Line>\n            <Line>\n                <DataPoint />\n                <span>A single data point</span>\n            </Line>\n        </div>\n    );\n}   \n\nexport default GraphExplainer;"
  },
  {
    "path": "src/app/screens/Graph/index.tsx",
    "content": "\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { debounce } from 'lodash-es';\nimport cytoscape, { NodeSingular, Position } from 'cytoscape';\nimport fcose from 'cytoscape-fcose';\n\nimport { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Data';\nimport Repository from 'app/utilities/Repository';\nimport calculateGraph from './calculateGraph';\nimport DatumOverlay from '../Data/components/DatumOverlay';\nimport { RouteProps } from '../types';\nimport style, { Container, ResetButton, Tooltip } from './style';\nimport { faUndo } from '@fortawesome/free-solid-svg-icons';\nimport Loading from 'app/components/Loading';\nimport NoData from 'app/components/NoData';\nimport useTour from 'app/components/Tour/useTour';\n\ntype HoveredNode = {\n    position: Position;\n    type: string;\n    datumType?: string;\n    label: string;\n};\n\ntype CytoEvent = { target: NodeSingular };\n\nfunction Graph(): JSX.Element {\n    useTour('/screen/graph');\n\n    // Register refs for cytoscape and the container to which it is assigned respectively\n    const cy = useRef<cytoscape.Core>();\n    const container = useRef<HTMLDivElement>();\n\n    // Retrieve the params for selected nodes\n    const navigate = useNavigate();\n    const selectedNode = useParams<RouteProps['graph']>().datumId;\n\n    // Assign state for hovered nodes and all data\n    const [hoveredNode, setHoveredNode] = useState<HoveredNode>(null);\n    const [data, setData] = useState(null);\n\n    /**\n     * Handle a mouseover on one of the Cytoscape node elements\n     */\n    const handleMouseOver = useCallback((event: CytoEvent) => {\n        // Retrieve the node from the event\n        const node = event.target;\n\n        // Assign the hovered class to the node\n        node.addClass('hover');\n\n        // Assign a secondary hover class to connected nodes\n        if (node.data('type') === 'type') {\n            const neighbours = node.openNeighborhood();\n            neighbours.addClass('secondary-hover');\n        }\n\n        // Then store the hovered node in state for the hover element\n        setHoveredNode({\n            position: node.renderedPosition(),\n            label: node.data('label'),\n            type: node.data('type'),\n            datumType: node.data('datumType'),\n        });\n    }, [setHoveredNode]);\n\n    /**\n     * Handle the mouseout from one of the Cytoscape nodes\n     */\n    const handleMouseOut = useCallback((event: CytoEvent) => {\n        // Extract node\n        const node = event.target as NodeSingular;\n\n        // Reset stored node\n        setHoveredNode(null);\n\n        // Remove class from the unhovered node, but also all elements that have\n        // been assigned a secondary hover previously\n        node.removeClass('hover');\n        cy.current.elements('.secondary-hover').removeClass('secondary-hover');\n    }, [setHoveredNode, cy]);\n\n    /**\n     * Handle a mouse tap on of the Cytoscape nodes\n     */\n    const handleSelectNode = useCallback((event: CytoEvent) => {\n        // GUARD: If it's not a datum node, we don't handle it\n        if (event.target.data('type') !== 'datum') {\n            return;\n        }\n\n        // Retrieve the particular datum\n        const i = event.target.data('i');\n        if (i) {\n            navigate(`/graph/${i}`);\n        }\n    }, []);\n\n    const handleReset = useCallback(() => {\n        cy.current.fit();\n    }, [cy]);\n\n    useEffect((): void => {\n        // GUARD: If the container hasn't mounted yet, we cannot initialise\n        // cytoscape in it.\n        if (!container.current) {\n            return;\n        }\n\n        /**\n         * Create a new Cytoscape instance\n         */\n        async function createCytoInstance() {\n            // Retrieved all data for this commit from the repository\n            const commit = await Repository.parsedCommit() as ProviderDatum<string, ProvidedDataTypes>[];\n            setData(commit);\n\n            // GUARD: Don't render anything if no data is present. It will crash cytoscape\n            if (commit.length === 0) {\n                return;\n            }\n            \n            // Add the fcose layout to cytoscape\n            // eslint-disable-next-line\n            cytoscape.use(fcose);\n\n            // Init Cytoscape\n            cy.current = cytoscape({\n                container: container.current,\n                minZoom: 0.5,\n                maxZoom: 2,\n                layout: {\n                    name: 'fcose',\n                },\n                elements: calculateGraph(commit),\n                style,\n            });\n\n            // Initialise the hover handlers\n            cy.current.on('mouseover', 'node', handleMouseOver);\n            cy.current.on('drag', 'node', handleMouseOver);\n            cy.current.on('mouseout', 'node', handleMouseOut);\n            cy.current.on('tap', handleSelectNode);\n        }\n\n        createCytoInstance();\n    }, [container, setData]);\n\n    /**\n     * Add a handler for window resizes, so Cytoscape participates neatly when\n     * someone wants to see a bit more\n     */\n    useEffect(() => {\n        // Register handler\n        const handleResize = () => cy.current.fit();\n        // Debounce the function so we don't spam cytoscape too often\n        const debouncedHandler = debounce(handleResize, 100);\n\n        // Register listener, and cleanup function\n        window.addEventListener('resize', debouncedHandler);\n        return () => window.removeEventListener('resize', debouncedHandler);\n    });\n\n    // If there is not data to display, render a view imploring the user to\n    // start adding accounts\n    if (data && !data.length) {\n        return <NoData />;\n    }\n\n    return (\n        <>\n            <Container\n                ref={container}\n                isHovered={hoveredNode && hoveredNode.type === 'datum'}\n                data-tour=\"graph-container\" \n            />\n            {!data && <Loading />}\n            {hoveredNode && \n                <Tooltip top={hoveredNode.position.y} left={hoveredNode.position.x}>\n                    {hoveredNode.label}\n                    {hoveredNode.type === 'datum' &&\n                        <span> \n                            <br />\n                            [{hoveredNode.datumType}]\n                        </span>\n                    }\n                </Tooltip>\n            }\n            <DatumOverlay datumId={Number.parseInt(selectedNode) || null} overlay />\n            <ResetButton icon={faUndo} onClick={handleReset}>Reset View</ResetButton>\n        </>\n    );\n}\n\nexport default Graph;"
  },
  {
    "path": "src/app/screens/Graph/renderNode.ts",
    "content": "import { IconDefinition } from '@fortawesome/fontawesome-svg-core';\nimport DataType from 'app/utilities/DataType';\nimport Providers from 'app/utilities/Providers';\nimport { NodeSingular } from 'cytoscape';\n\n/**\n * Convert a Cytoscape-provided element into a FontAwesome Icon\n * @param element \n */\nexport function getIconFromNode(element: NodeSingular): IconDefinition {\n    switch (element.data('type')) {\n        case 'type':\n        case 'datum': {\n            const type = element.data('datumType');\n            return DataType.getIcon(type);\n        }\n        case 'provider': {\n            const provider = element.data('label');\n            return Providers.getIcon(provider);\n        }\n    }\n}\n\n/**\n * Render a FontAwesome SVG icon as background-image for any node\n * @param element \n */\nexport default function renderNode(color: string, size: number): (element: NodeSingular) => string {\n    return function (element: NodeSingular) {\n        const [width, height, , , path] = getIconFromNode(element).icon;\n        const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${width} ${height}\" width=\"${size * 1.25}\" height=\"${size}\" style=\"text-align: center\">\n            <path d=\"${path}\" fill=\"${color}\" transform=\"translate(0,0)\"></path>\n          </svg>`;\n        /* eslint-disable deprecation/deprecation -- btoa is only deprecated in NodeJS environments. This code runs in the browser */\n        return 'data:image/svg+xml;base64,' + btoa(svg);\n    };\n}\n"
  },
  {
    "path": "src/app/screens/Graph/style.ts",
    "content": "import { GhostButton } from 'app/components/Button';\nimport { Shadow } from 'app/styles/snippets';\nimport { Stylesheet } from 'cytoscape';\nimport styled from 'styled-components';\nimport renderNode from './renderNode';\n\nconst bodyStyles = window.getComputedStyle(document.body);\nconst cssVar = (name: string) => bodyStyles.getPropertyValue(name);\n\nconst style: Stylesheet[] = [\n    {\n        selector: 'node',\n        style: {\n            shape: 'ellipse',\n            width: 50,\n            height: 50,\n            'text-wrap': 'wrap',\n            'text-valign': 'center',\n            'text-halign': 'center',\n            'text-max-width': '10px',\n            'background-color': cssVar('--color-gray-300'),\n            'color': cssVar('--color-text'),\n            'font-size': '10px',\n            'font-family': cssVar('--font-mono'),\n        },\n    },\n    {\n        selector: 'node.hover',\n        style: {\n            'background-color': cssVar('--color-primary'),\n            'color': cssVar('--color-white'),\n        },\n    },\n    {\n        selector: 'node[type=\"provider\"]',\n        style: {\n            'background-color': cssVar('--color-primary'),\n            'background-image': renderNode(cssVar('--color-white'), 36),\n            'background-position-x': '50%',\n            'background-position-y': '50%',\n            'color': '#FFF',\n            'font-size': 16,\n            'border-width': 10,\n            'border-color': cssVar('--color-blue-200'),\n            width: 100,\n            height: 100,\n        },\n    },\n    {\n        selector: 'node[type=\"provider\"].hover',\n        style: {\n            'background-color': cssVar('--color-blue-600'),\n        },\n    },\n    {\n        selector: 'node[type=\"account\"]',\n        style: {\n            width: 75,\n            height: 75,\n            'background-color': cssVar('--color-blue-200'),\n            content: 'data(label)',\n        },\n    },\n    {\n        selector: 'node[type=\"type\"]',\n        style: {\n            shape: 'roundrectangle',\n            'background-image': renderNode(cssVar('--color-text'), 24),\n            'background-position-x': '50%',\n            'background-position-y': '50%',\n            content: '',\n            color: '#ccc',\n        },\n    },\n    {\n        selector: 'node[type=\"type\"].hover',\n        style: {\n            'background-image': renderNode(cssVar('--color-white'), 24),\n        },\n    },\n    {\n        selector: 'node[type=\"account\"].hover',\n        style: {\n            'background-color': cssVar('--color-blue-400'),\n        },\n    },\n    {\n        selector: 'node[type=\"datum\"]',\n        style: {\n            width: 10,\n            height: 10,\n            label: '',\n        },\n    },\n    {\n        selector: 'node.secondary-hover',\n        style: {\n            'background-color': cssVar('--color-primary'),\n        },\n    },\n    {\n        selector: 'edge',\n        style: {\n            'line-color': cssVar('--color-gray-200'),\n            width: 2,\n            // label: 'data(label)',\n            'font-size': '8px',\n            'font-family': cssVar('--font-mono'),\n        },\n    },\n    {\n        selector: 'edge[type=\"datum_type\"]',\n        style: {\n            'line-color': cssVar('--color-gray-300'),\n            width: 1,\n        },\n    },\n    {\n        selector: 'edge[type=\"datum_account\"]',\n        style: {\n            \n        },\n    },\n    {\n        selector: 'edge[type=\"account_provider\"]',\n        style: {\n            'line-color': cssVar('--color-blue-200'),\n            width: 10,\n        },\n    },\n    {\n        selector: 'edge.secondary-hover',\n        style: {\n            'line-color': cssVar('--color-primary'),\n        },\n    },\n];\n\nexport const Container = styled.div<{ isHovered?: boolean }>`\n    width: 100%;\n    height: 100%;\n`;\n\nexport const Tooltip = styled.div<{ top: number, left: number }>`\n    position: absolute;\n    top: ${(props) => props.top + 15}px;\n    left: ${(props) => props.left}px;\n    transform: translateX(-50%);\n    background-color: var(--color-modal-background);\n    border-radius: 4px;\n    padding: 4px 8px;\n    font-size: 12px;\n    line-height: 1.5;\n    text-align: center;\n    pointer-events: none;\n    ${Shadow}\n\n    span {\n        opacity: 0.5;\n        font-family: var(--font-mono);\n        font-size: 10px;\n    }\n`;\n\nexport const ResetButton = styled(GhostButton)`\n    position: fixed;\n    bottom: 25px;\n    left: 250px;\n`;\n\nexport default style;"
  },
  {
    "path": "src/app/screens/Onboarding/index.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faPlus } from '@fortawesome/free-solid-svg-icons';\nimport { LinkButton } from 'app/components/Button';\nimport { H2 } from 'app/components/Typography';\nimport { MarginLeft } from 'app/components/Utility';\nimport { State } from 'app/store';\nimport { completeOnboarding } from 'app/store/onboarding/actions';\nimport React, { useEffect } from 'react';\n\nimport { useDispatch, useSelector } from 'react-redux';\nimport styled from 'styled-components';\nimport Logo from 'app/assets/aeon-logo.svg';\nimport { useNavigate } from 'react-router-dom';\n\nconst Container = styled.div`\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n    height: 100%;\n    text-align: center;\n    max-width: 500px;\n    margin: 0 auto;\n\n    img {\n        width: 250px;\n        margin-bottom: 48px;\n    }\n`;\n\nfunction Onboarding(): JSX.Element {\n    const dispatch = useDispatch();\n    const navigate = useNavigate();\n    const isOnboardingComplete = useSelector((state: State) => state.onboarding.initialisation);\n    \n    useEffect(() => {\n        // Redirect to /timeline if the user has already completed the\n        // first-time application onboarding screen\n        if (isOnboardingComplete) {\n            navigate('/timeline');\n        }\n\n        return () => {\n            // If the user has not completed onboarding yet, dispatch the\n            // completion action for this onboarding when the component is unmounted.\n            if (!isOnboardingComplete) {\n                dispatch(completeOnboarding('initialisation'));\n            }\n        };\n    }, []);\n\n    return (\n        <Container>\n            <img src={Logo} alt=\"Aeon Logo\" />\n            <H2>Welcome to Aeon!</H2>\n            <p>Aeon is an application that makes managing where your data is found online easy. If you&apos;re feeling adventerous, feel free to explore around. Adding your first account is also a good place to start.</p>\n            <LinkButton to=\"/accounts\">\n                Add your first account\n                <MarginLeft><FontAwesomeIcon icon={faPlus} /></MarginLeft>\n            </LinkButton>\n        </Container>\n    );\n}\n\nexport default Onboarding;"
  },
  {
    "path": "src/app/screens/Settings/email/components/ImapInitialiser.tsx",
    "content": "import React, { ChangeEvent, useCallback, useState } from 'react';\nimport { Label, TextInput } from 'app/components/Input';\nimport Button from 'app/components/Button';\nimport { PullContainer } from 'app/components/Utility';\nimport { createImapAccount, testImapConnection } from 'app/store/email/actions';\nimport { useAppDispatch } from 'app/store';\nimport styled from 'styled-components';\n\nconst ErrorMessage = styled.div`\n    background-color: var(--color-red-100);\n    color: var(--color-red-600);;\n    padding: 8px 16px;\n    border-radius: 8px;\n    margin-bottom: 8px;\n`;\n\ninterface Props {\n    onComplete: () => void;\n}\n\nfunction ImapInitialiser({ onComplete }: Props) {\n    const [username, setUsername] = useState('');\n    const [password, setPassword] = useState('');\n    const [host, setHost] = useState('');\n    const [port, setPort] = useState(993);\n\n    const [hasTested, setHasTested] = useState(null);\n    const [isLoading, setIsLoading] = useState(false);\n    const dispatch = useAppDispatch();\n\n    const handleChangeUsername = useCallback((e: ChangeEvent<HTMLInputElement>) => { \n        setUsername(e.currentTarget.value); \n        setHasTested(null);\n    }, [setUsername]);\n    const handleChangePassword = useCallback((e: ChangeEvent<HTMLInputElement>) => { \n        setPassword(e.currentTarget.value); \n        setHasTested(null);\n    }, [setPassword]);\n    const handleChangeHost = useCallback((e: ChangeEvent<HTMLInputElement>) => { \n        setHost(e.currentTarget.value); \n        setHasTested(null);\n    }, [setHost]);\n    const handleChangePort = useCallback((e: ChangeEvent<HTMLInputElement>) => { \n        setPort(Number.parseInt(e.currentTarget.value)); \n        setHasTested(null);\n    }, [setPort]);\n\n    const testConnection = useCallback(() => {\n        setHasTested(null);\n        setIsLoading(true);\n        dispatch(testImapConnection({\n            user: username,\n            pass: password,\n            host,\n            port,\n            secure: true,\n        })).unwrap().then((result) => {\n            if (result === true) {\n                dispatch(createImapAccount({\n                    user: username,\n                    pass: password,\n                    host,\n                    port,\n                    secure: true,\n                })).unwrap().then(onComplete);\n            } else {\n                setHasTested(result);\n                setIsLoading(false);\n            }\n        });\n    }, [username, password, host, port]);\n\n    return (\n        <>\n            <p>Using IMAP, you can add any regular email address to Aeon. After filling in your details, Aeon will test the connection to make sure your credentials are correct.</p>\n            <Label>\n                Email Address\n                <TextInput value={username} onChange={handleChangeUsername} placeholder=\"john@doe.com\" />\n            </Label>\n            <Label>\n                Password\n                <TextInput value={password} onChange={handleChangePassword} type=\"password\" />\n            </Label>\n            <Label>\n                Host\n                <TextInput value={host} onChange={handleChangeHost} placeholder=\"imap.doe.com\" />\n            </Label>\n            <Label>\n                Port\n                <TextInput value={port} onChange={handleChangePort} />\n            </Label>\n            {hasTested === false && (\n                <ErrorMessage>Failed to connect to {host}</ErrorMessage>\n            )}\n            <PullContainer gap>\n                <Button onClick={testConnection} loading={isLoading}>Add {username}</Button>\n            </PullContainer>\n        </>\n    );\n}\n\nexport default ImapInitialiser;\n"
  },
  {
    "path": "src/app/screens/Settings/email/components/NewAccountModal.tsx",
    "content": "import { faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faEnvelope, faPlus } from '@fortawesome/free-solid-svg-icons';\nimport Button from 'app/components/Button';\nimport Modal from 'app/components/Modal';\nimport ModalMenu from 'app/components/Modal/Menu';\nimport { Margin, MarginLeft, PullCenter, PullContainer } from 'app/components/Utility';\nimport { useAppDispatch } from 'app/store';\nimport { createEmailAccount } from 'app/store/email/actions';\nimport React, { PropsWithChildren, useCallback, useEffect, useState } from 'react';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport ImapInitialiser from './ImapInitialiser';\n\ntype NewAccountProps = PropsWithChildren<{ \n    client: string, \n    onComplete: () => void,\n}>;\n\nfunction NewAccountButton({ client, children, onComplete, ...props }: NewAccountProps): JSX.Element {\n    const [isActive, setActive] = useState(false);\n    const dispatch = useAppDispatch();\n\n    // A handler for creating a new email account\n    const handleClick = useCallback(async () => {\n        // Set activity flag\n        setActive(true);\n\n        // Actually create a new account\n        await dispatch(createEmailAccount(client));\n\n        // Set new activity flag, and let parent component know we're done\n        setActive(false);\n        onComplete();\n    }, [dispatch, client, setActive]);\n\n    return (\n        <Button icon={faPlus} {...props} onClick={handleClick} loading={isActive}>{children}</Button>\n    );\n}\n\nfunction NewAccountModal(): JSX.Element {\n    const location = useLocation();\n    const navigate = useNavigate();\n    const [modalIsOpen, setModal] = useState(false);\n    useEffect(() => {\n        const params = new URLSearchParams(location.search);\n        setModal(params.has('create-new-email-account'));\n    }, [location, setModal]);\n    const closeModal = useCallback(() => navigate(location.pathname), [location]);\n    const openModal = useCallback(() => navigate(location.pathname + '?create-new-email-account'), [location]);\n    \n    return (\n        <>\n            <Button fullWidth icon={faPlus} onClick={openModal}>Add Email Account</Button>\n            <Modal isOpen={modalIsOpen} onRequestClose={closeModal}>\n                <ModalMenu labels={[\n                    <PullContainer verticalAlign key='imap'><FontAwesomeIcon icon={faEnvelope} /><MarginLeft>IMAP</MarginLeft></PullContainer>,\n                    <PullContainer verticalAlign key='outlook'><FontAwesomeIcon icon={faMicrosoft} /><MarginLeft>Outlook</MarginLeft></PullContainer>, \n                    <PullContainer verticalAlign key='google'><FontAwesomeIcon icon={faGoogle} /><MarginLeft>Gmail</MarginLeft></PullContainer>, \n                ]}>\n                    <Margin>\n                        <ImapInitialiser onComplete={closeModal} />\n                    </Margin>\n                    <Margin>\n                        <p>By connecting your Outlook account, Aeon can send and check emails on your behalf. This makes it easier to submit and check on data requests. When clicking the button below, a browser window will open that allows you to connect to a particular Outlook account. Aeon does not store any credentials for your email accounts. Rather, the Outlook API is used to do actions on your behalf. You can revoke this access at any time.</p>\n                        <PullCenter><NewAccountButton client='outlook' onComplete={closeModal}>Add new Outlook account</NewAccountButton></PullCenter>\n                    </Margin>\n                    <Margin>\n                        <p>By connecting your Gmail account, Aeon can send and check emails on your behalf. This makes it easier to submit and check on data requests. When clicking the button below, a browser window will open that allows you to connect to a particular Gmail account. Aeon does not store any credentials for your email accounts. Rather, the Gmail API is used to do actions on your behalf. You can revoke this access at any time.</p>\n                        <PullCenter><NewAccountButton client='gmail' onComplete={closeModal}>Add new Gmail account</NewAccountButton></PullCenter>\n                    </Margin>\n                </ModalMenu>\n            </Modal>\n        </>\n    );\n}\n\nexport default NewAccountModal;"
  },
  {
    "path": "src/app/screens/Settings/email/index.tsx",
    "content": "import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faTrash } from '@fortawesome/free-solid-svg-icons';\nimport { GhostButton } from 'app/components/Button';\nimport { List, NavigatableListEntry, PanelBottomButtons, RowDescription, SplitPanel } from 'app/components/PanelGrid';\nimport RightSideOverlay, { Section } from 'app/components/RightSideOverlay';\nimport { MarginLeft } from 'app/components/Utility';\nimport { State } from 'app/store';\nimport Email from 'app/utilities/Email';\nimport React, { useCallback } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useNavigate } from 'react-router-dom';\nimport NewAccountModal from './components/NewAccountModal';\nimport { IconBadgeWithTitle } from 'app/components/IconBadge';\n\nfunction EmailSettings({ settingId: selectedAccount }: { settingId?: string }): JSX.Element {\n    const navigate = useNavigate();\n    const { all, byId } = useSelector((state: State) => state.email.accounts);\n    const deleteAccount = useCallback(async () => {\n        // GUARD: Double-check the user wants to actually delete the account\n        if (window.confirm(`Are you sure you want to delete ${selectedAccount}?`)) {\n            // Delete the account\n            await Email.delete(selectedAccount); \n            \n            // Then return to the previous menu\n            navigate('/settings/email-accounts');\n        }\n    }, [selectedAccount]);\n\n    return (\n        <>\n            <SplitPanel>\n                <List>\n                    <RowDescription>\n                        Email accounts are necessary for some providers that do not have an automated way of processing data requests. By linking an email-address associated with your accounts, Aeon can send emails and track responses to help make it easy for you.\n                    </RowDescription>\n                    {all.map((account) => (\n                        <NavigatableListEntry key={account} to={`/settings/email-accounts/${account}`}>\n                            <FontAwesomeIcon icon={Email.getIcon(byId[account])} />\n                            <MarginLeft>{account}</MarginLeft>\n                        </NavigatableListEntry>\n                    ))}\n                </List>\n                <PanelBottomButtons>\n                    <NewAccountModal />\n                </PanelBottomButtons>\n            </SplitPanel>\n            <List>\n                {selectedAccount && \n                    <RightSideOverlay>\n                        <>\n                            <Section>\n                                <IconBadgeWithTitle icon={Email.getIcon(selectedAccount)}>\n                                    {selectedAccount}\n                                </IconBadgeWithTitle>\n                            </Section>\n                            <Section>\n                                <p>This email address is used to check on data requests associated with the account.</p>\n                            </Section>\n                            <Section>\n                                <p>By deleting this account, Aeon will no longer have access to it. Requests that are in progress with this e-mail address may be cancelled as a result.</p>\n                                <GhostButton backgroundColor=\"red\" icon={faTrash} fullWidth onClick={deleteAccount}>\n                                    Delete {selectedAccount}\n                                </GhostButton>\n                            </Section>\n                        </>\n                    </RightSideOverlay>\n                }\n            </List>\n        </>\n    );\n}\n\nexport default EmailSettings;"
  },
  {
    "path": "src/app/screens/Settings/index.tsx",
    "content": "import React from 'react';\nimport { Category, List, NavigatableListEntry, PanelGrid } from 'app/components/PanelGrid';\nimport { faEnvelope } from '@fortawesome/free-solid-svg-icons';\nimport EmailSettings from './email';\nimport { useParams } from 'react-router-dom';\nimport { RouteProps } from '../types';\n\ntype CategoryPanel = (props: { settingId?: string }) => JSX.Element;\n\n/**\n * This objects maps a category paramater to a particular settings panel, so\n * that all settings are seperated out into their own components.\n */\nconst mapCategoryToPanel: Record<string, CategoryPanel> = {\n    'email-accounts': EmailSettings,\n};\n\nfunction Settings(): JSX.Element {\n    const { category, settingId } = useParams<RouteProps['settings']>();\n    const SettingsPanel = category && category in mapCategoryToPanel\n        ? mapCategoryToPanel[category] \n        : List;\n\n    return (\n        <PanelGrid>\n            <List>\n                <Category title=\"Categories\">\n                    <NavigatableListEntry to=\"/settings/email-accounts\" icon={faEnvelope}>\n                        Email Accounts\n                    </NavigatableListEntry>\n                </Category>\n            </List>\n            <SettingsPanel settingId={settingId} />\n        </PanelGrid>\n    );\n}\n\nexport default Settings;"
  },
  {
    "path": "src/app/screens/Timeline/components/Commit.tsx",
    "content": "\nimport styled, { css } from 'styled-components';\nimport React, { Component, MouseEventHandler } from 'react';\nimport { Badge } from 'app/components/Typography';\nimport { Commit as CommitType } from 'main/lib/repository/types';\nimport { Shadow } from 'app/styles/snippets';\n\ninterface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'onClick'> {\n    onClick: (hash: string) => void;\n    entry: CommitType;\n    active?: boolean;\n    latestCommit?: boolean;\n}\n\nexport const StyledCommit = styled.button<{ active?: boolean }>`\n    position: relative;\n    z-index: 3;\n    padding: 35px 25px;\n    margin: 4px;\n    text-align: left;\n    border: 0;\n    font-size: 14px;\n    font-weight: 400;\n    max-width: 100%;\n    display: flex;\n    align-items: center;\n    position: relative;\n    background-color: transparent;\n    color: var(--color-gray-700);\n    font-family: var(--font-heading);\n\n    &:focus {\n        outline: 0;\n    }\n\n    ${(props) => props.active ? css`\n        border-radius: 12px;\n        color: var(--color-blue-500);\n        font-weight: 600;\n        background-color: var(--color-background);\n        border: 1px solid var(--color-gray-100);\n        ${Shadow}\n    ` : css`\n        &:hover {\n            background-color: var(--color-background);\n        }\n    `}\n`;\n\nconst Dot = styled.div<{ active?: boolean }>`\n    width: 24px;\n    height: 24px;\n    border-radius: 32px;\n    margin-left: -5px;\n    margin-right: 16px; \n    z-index: 2;\n    flex-shrink: 0;\n    transition: transform 0.3s ease;\n    ${Shadow}\n\n    border: 4px solid var(--color-gray-200);\n    background-color: var(--color-gray-100);\n\n    ${(props) => props.active && css`\n        background-color: var(--color-blue-500) !important; \n        border: 4px solid var(--color-blue-200) !important;\n        color: inherit;\n        transform: scale(1.25);\n    `}\n`;\n\nexport const TimelineLine = styled.div`\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 32px;\n    width: 10px;\n    min-height: 100%;\n    z-index: 0;\n    background-color: var(--color-gray-50);\n`;\n\nclass Commit extends Component<Props> {\n    handleClick: MouseEventHandler<HTMLButtonElement> = () => {\n        this.props.onClick(this.props.entry.oid);\n    };\n\n    render(): JSX.Element {\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { entry, active, latestCommit, onClick, ...props } = this.props;\n\n        return (\n            <StyledCommit active={active} onClick={this.handleClick} {...props}>\n                <Dot active={active} />\n                <div>\n                    {entry.message.split('\\n')[0]}\n                    {latestCommit && <>\n                        <br /><br />\n                        <Badge>Current Identity</Badge>\n                    </>}\n                </div>\n            </StyledCommit>\n        );\n    }\n}\n\nexport default Commit;"
  },
  {
    "path": "src/app/screens/Timeline/components/Diff.tsx",
    "content": "import React, { PureComponent } from 'react';\nimport { DiffResult, ExtractedDataDiff, Commit } from 'main/lib/repository/types';\nimport Repository from 'app/utilities/Repository';\nimport styled from 'styled-components';\nimport DataType from 'app/utilities/DataType';\nimport Loading from 'app/components/Loading';\nimport { ProviderDatum } from 'main/providers/types/Data';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { FontLarge, H2, H5 } from 'app/components/Typography';\nimport { formatDistanceToNow } from 'date-fns';\nimport Code from 'app/components/Code';\nimport RightSideOverlay, { DetailListItem, RightSideOverlayOffset, Section } from 'app/components/RightSideOverlay';\nimport { faCodeBranch, faLink, faPlus, faSync, faUser } from '@fortawesome/free-solid-svg-icons';\nimport { MarginLeft, PullContainer } from 'app/components/Utility';\nimport convertMetaToObject from 'app/utilities/convertMetaToObject';\nimport Providers from 'app/utilities/Providers';\n\ninterface Props {\n    commit: Commit;\n    diff?: ExtractedDataDiff;\n}\n\ninterface State {\n    diff?: ExtractedDataDiff;\n}\n\nconst PullRight = styled.span`\n    margin-left: auto;\n    opacity: 0.5;\n    text-transform: uppercase;\n    flex-shrink: 0;\n`;\n\nconst DiffItem = styled.div`\n    display: flex;\n`;\n\nconst CodeRectifier = styled.div`\n    margin: 16px;\n    \n    ${Code}:first-child {\n        margin-top: 0px;\n    }\n\n    ${Code}:last-child {\n        margin-bottom: 0px;\n    }\n`;\n\nclass Diff extends PureComponent<Props, State> {\n    state: State = {\n        diff: null,\n    };\n\n    componentDidMount(): void {\n        this.fetchDiff();\n    }\n\n    componentDidUpdate(prevProps: Props): void {\n        if (prevProps.commit !== this.props.commit) {\n            this.fetchDiff();\n        }\n    }\n\n    fetchDiff = async (): Promise<void> => {\n        this.setState({ diff: null });\n        const diff = await Repository.diff(this.props.commit?.oid) as DiffResult<ExtractedDataDiff>[];\n        this.setState({ diff: this.filterAndSortExtractedData(diff) });\n    };\n\n    filterAndSortExtractedData(diff: DiffResult<ExtractedDataDiff>[]): ExtractedDataDiff {\n        const sortingFunction = (a: ProviderDatum<unknown>, b: ProviderDatum<unknown>): number => {\n            return a.type.localeCompare(b.type);\n        };\n\n        const added = diff.flatMap((file) => file.diff.added || []).sort(sortingFunction);\n        const updated = diff.flatMap((file) => file.diff.updated || []).sort(sortingFunction);\n        const deleted = diff.flatMap((file) => file.diff.deleted || []).sort(sortingFunction);\n\n        return {\n            added,\n            updated,\n            deleted,\n        };\n    }\n\n    render(): JSX.Element {\n        const diff = this.props.diff || this.state.diff;\n        const { commit } = this.props;\n\n        if (!diff || !commit) {\n            return (\n                <RightSideOverlay>\n                    <Loading />\n                </RightSideOverlay>\n            );\n        }\n\n        const meta = convertMetaToObject(commit?.message);\n\n        return (\n            <RightSideOverlayOffset>\n                <RightSideOverlay data-tour=\"timeline-diff-container\">\n                    <>\n                        <Section>\n                            <H2>\n                                <PullContainer verticalAlign>\n                                    <FontAwesomeIcon icon={faCodeBranch} fixedWidth />\n                                    <MarginLeft>{meta.title}</MarginLeft>\n                                </PullContainer>\n                            </H2>\n                        </Section>\n                        <Section data-tour=\"timeline-diff-info\" well>\n                            <FontLarge>\n                                <DetailListItem>\n                                    <span><FontAwesomeIcon fixedWidth icon={faPlus} /></span>\n                                    <span>Committed {formatDistanceToNow(new Date(commit.author.when))} ago</span>\n                                </DetailListItem>\n                                {meta.provider && <DetailListItem>\n                                    <span><FontAwesomeIcon fixedWidth icon={Providers.getIcon(meta.provider)} /></span>\n                                    <span>{meta.provider}</span>\n                                </DetailListItem>}\n                                {meta.account && <DetailListItem>\n                                    <span><FontAwesomeIcon fixedWidth icon={faUser} /></span>\n                                    <span>{meta.account}</span>\n                                </DetailListItem>}\n                                {meta.updateType && <DetailListItem>\n                                    <span><FontAwesomeIcon fixedWidth icon={faSync} /></span>\n                                    <span>{meta.updateType}</span>\n                                </DetailListItem>}\n                                {meta.url && <DetailListItem>\n                                    <span><FontAwesomeIcon fixedWidth icon={faLink} /></span>\n                                    <span>{meta.url}</span>\n                                </DetailListItem>}\n                            </FontLarge>\n                        </Section>\n                        <CodeRectifier data-tour=\"timeline-diff-data\">\n                            {(diff.added.length || diff.updated.length || diff.deleted.length) ?\n                                <>\n                                    {diff.added.length ? (\n                                        <Code added>\n                                            <H5>DATA ADDED</H5>\n                                            {diff.added.map((datum, index) => (\n                                                <DiffItem key={'data_added_' + index}>\n                                                    <span><FontAwesomeIcon icon={DataType.getIcon(datum.type)} fixedWidth /></span>\n                                                    <MarginLeft>{DataType.toString(datum)}</MarginLeft>\n                                                    <PullRight>{datum.type}</PullRight>\n                                                </DiffItem>\n                                            ))}\n                                        </Code>\n                                    ) : null}\n                                    {diff.updated.length ? (\n                                        <Code updated>\n                                            <H5>DATA UPDATED</H5>\n                                            {diff.updated.map((datum, index) => (\n                                                <DiffItem key={'data_updated_' + index}>\n                                                    <span><FontAwesomeIcon icon={DataType.getIcon(datum.type)} fixedWidth /></span>\n                                                    <MarginLeft>{DataType.toString(datum)}</MarginLeft>\n                                                    <PullRight>{datum.type}</PullRight>\n                                                </DiffItem>\n                                            ))}\n                                        </Code>\n                                    ) : null}\n                                    {diff.deleted.length ? (\n                                        <Code removed>\n                                            <H5>DATA REMOVED</H5>\n                                            {diff.deleted.map((datum, index) => (\n                                                <DiffItem key={'data_removed_' + index}>\n                                                    <span><FontAwesomeIcon icon={DataType.getIcon(datum.type)} fixedWidth /></span>\n                                                    <MarginLeft>{DataType.toString(datum)}</MarginLeft>\n                                                    <PullRight>{datum.type}</PullRight>\n                                                </DiffItem>\n                                            ))}\n                                        </Code>\n                                    ) : null}\n                                </>\n                                : null}\n                        </CodeRectifier>\n                    </>\n                </RightSideOverlay>\n            </RightSideOverlayOffset>\n        );\n    }\n}\n\nexport default Diff;"
  },
  {
    "path": "src/app/screens/Timeline/index.tsx",
    "content": "import React, { Component } from 'react';\nimport Repository from 'app/utilities/Repository';\nimport styled from 'styled-components';\nimport Commit, { TimelineLine } from './components/Commit';\nimport Diff from './components/Diff';\nimport Loading from 'app/components/Loading';\nimport { RepositoryEvents, Commit as CommitType } from 'main/lib/repository/types';\nimport { IpcRendererEvent } from 'electron';\nimport { State as AppState } from 'app/store';\nimport { NavigateFunction, Location, useLocation, useNavigate, useParams } from 'react-router-dom';\nimport { RouteProps } from '../types';\nimport { List, PanelGrid } from 'app/components/PanelGrid';\nimport { connect } from 'react-redux';\nimport NoData from '../../components/NoData';\nimport { Tour } from 'app/components/Tour/useTour';\n\ninterface State {\n    log: CommitType[];\n    updating: boolean;\n}\n\ninterface Props {\n    params: RouteProps['timeline'];\n    navigate: NavigateFunction;\n    location: Location;\n    newCommits: AppState['newCommits'];\n}\n\nconst CommitContainer = styled.div`\n    display: flex;\n    grid-area: \"commits\";\n    flex-direction: column;\n    position: relative;\n    top: 0;\n    flex-shrink: 0;\n    overflow-y: auto;\n    padding-top: 55px;\n`;\n\nclass Timeline extends Component<Props, State> {\n    state: State = {\n        log: null,\n        updating: false,\n    };\n\n    componentDidMount(): void {\n        this.fetchLog();\n        Repository.subscribe(this.handleEvent);\n    }\n\n    componentDidUpdate(prevProps: Props) {\n        if (prevProps.params.commitHash !== this.props.params.commitHash) {\n            this.fetchLog();\n        }\n    }\n\n    componentWillUnmount(): void {\n        Repository.unsubscribe(this.handleEvent);\n    }\n\n    handleEvent = (event: IpcRendererEvent, type: RepositoryEvents): void => {\n        if (type === RepositoryEvents.NEW_COMMIT) {\n            this.fetchLog();\n        }\n    };\n\n    fetchLog = (): Promise<void> => {\n        return Repository.log()\n            .then((log) => {\n                // Save log to state\n                this.setState({ log });\n\n                // Redirect to most recent commit if none is set\n                if (!this.props.params.commitHash) {\n                    this.props.navigate('/timeline/' + log[0].oid);\n                }\n            });\n    };\n\n    handleClick = (hash: string): void => {\n        this.props.navigate('/timeline/' + hash);\n        // this.setState({ selectedCommit: hash });\n    };\n\n    render(): JSX.Element {\n        const { log } = this.state;\n        const { params: { commitHash }, newCommits } = this.props;\n\n        if (!log) {\n            return <Loading />;\n        }\n        \n        if (log.length <= 1) {\n            return <NoData />;\n        }\n        \n        const selectedTree = commitHash === 'new-commit'\n            ? newCommits[0]\n            : log.find((d) => d.oid === commitHash);\n\n        return (\n            <>\n                <PanelGrid columns={2} noTopPadding> \n                    <List>\n                        <TimelineLine />\n                        <CommitContainer data-tour=\"timeline-commits-list\">\n                            {newCommits.length ? \n                                <Commit\n                                    entry={newCommits[0]}\n                                    active={'new-commit' === commitHash}\n                                    onClick={this.handleClick}\n                                />\n                                : null}\n                            {log.map((entry, i) => (\n                                <Commit\n                                    key={entry.oid}\n                                    entry={entry}\n                                    onClick={this.handleClick}\n                                    active={entry.oid === commitHash}\n                                    latestCommit={i === 0}\n                                    data-telemetry-id=\"timeline-view-commit\"\n                                    data-tour={i === 0 ? 'timeline-first-commit' : undefined}\n                                />\n                            ))}\n                        </CommitContainer>\n                    </List>\n                    <List topMargin>\n                        <Diff commit={selectedTree} diff={newCommits.length && commitHash === 'new-commit' && newCommits[0].diff} />\n                    </List>\n                </PanelGrid>\n                <Tour screen=\"/screen/timeline\" />\n            </>\n        );\n    }\n}\n\nconst RouterWrapper = (props: Pick<Props, 'newCommits'>): JSX.Element => {\n    const params = useParams();\n    const navigate = useNavigate();\n    const location = useLocation();\n\n    return <Timeline params={params} navigate={navigate} location={location} {...props} />;\n};\n\nconst mapStateToProps = (state: AppState) => {\n    return {\n        newCommits: state.newCommits,\n    };\n};\n\nexport default connect(mapStateToProps)(RouterWrapper);"
  },
  {
    "path": "src/app/screens/index.tsx",
    "content": "import React from 'react';\nimport { Route, Routes } from 'react-router-dom';\nimport Menu, { ContentContainer, PageContainer, TitleBar } from 'app/components/Menu';\nimport Onboarding from './Onboarding';\nimport Timeline from './Timeline';\nimport Data from './Data';\nimport Accounts from './Accounts';\nimport Settings from './Settings';\nimport Graph from './Graph';\nimport Erasure from './Erasure';\nimport ErasureEmails from './Erasure/Emails';\n\nfunction Router(): JSX.Element {\n    return (\n        <PageContainer>\n            <Menu />\n            <ContentContainer id=\"content\">\n                <TitleBar />\n                <Routes>\n                    <Route path=\"/timeline\">\n                        <Route index element={<Timeline />} />\n                        <Route path=\":commitHash\" element={<Timeline />} />\n                    </Route>\n                    <Route path=\"/onboarding\" element={<Onboarding />} />\n                    <Route path=\"/accounts\">\n                        <Route index element={<Accounts />} />\n                        <Route path=\":account\" element={<Accounts />} />\n                    </Route>\n                    <Route path=\"/data\">\n                        <Route index element={<Data />} />\n                        <Route path=\":category\" element={<Data />} />\n                        <Route path=\":category/:datumId\" element={<Data />} />\n                    </Route>\n                    <Route path=\"/graph\">\n                        <Route index element={<Graph />} />\n                        <Route path=\":datumId\" element={<Graph />} />\n                    </Route>\n                    <Route path=\"/settings\">\n                        <Route index element={<Settings />} />\n                        <Route path=\":category\" element={<Settings />} />\n                        <Route path=\":category/:settingId\" element={<Settings />} />\n                    </Route>\n                    <Route path=\"/erasure\">\n                        <Route index element={<Erasure />} />\n                        <Route path=\"emails\" element={<ErasureEmails />} />\n                    </Route>\n                    <Route path=\"/\" element={<Onboarding />} />\n                </Routes>\n            </ContentContainer>\n        </PageContainer>\n    );\n}\n\nexport default Router;"
  },
  {
    "path": "src/app/screens/types.ts",
    "content": "import { ProvidedDataTypes } from 'main/providers/types/Data';\n\nexport interface RouteProps {\n    timeline: {\n        commitHash?: string;\n    }\n    data: {\n        category?: ProvidedDataTypes;\n        datumId?: string;\n    },\n    requests: {\n        account?: string;\n    },\n    settings: {\n        category?: string;\n        settingId?: string;\n    },\n    graph: {\n        datumId?: string;\n    }\n}"
  },
  {
    "path": "src/app/store/accounts/actions.ts",
    "content": "import { createAction, createAsyncThunk } from '@reduxjs/toolkit';\nimport Providers from 'app/utilities/Providers';\nimport { InitOptionalParameters } from 'main/providers/types';\nimport { EmailProvider } from './types';\n\nexport const fetchProviderAccounts = createAsyncThunk(\n    'requests/fetch/accounts',\n    Providers.getAccounts,\n);\n\nexport const refreshRequests = createAsyncThunk(\n    'requests/refresh',\n    async (arg, { dispatch }) => {\n        await Providers.refresh();\n        await dispatch(fetchProviderAccounts());\n    },\n);\n\nexport const addProviderAccount = createAsyncThunk(\n    'requests/new-account',\n    ({ client, optionalParameters }: { client: string, optionalParameters: InitOptionalParameters }, { dispatch }) => {\n        const value = Providers.initialise(client, optionalParameters);\n        dispatch(fetchProviderAccounts());\n        return value;\n    },\n);\n\nexport const fetchAvailableProviders = createAsyncThunk(\n    'requests/fetch/providers',\n    Providers.getAvailableProviders,\n);\n\nexport const createEmailAccount = createAction<EmailProvider>(\n    'requests/new-email-account',\n);\n\nexport const dispatchEmailRequest = createAction<string>(\n    'requests/email/dispatch',\n);"
  },
  {
    "path": "src/app/store/accounts/index.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { InitialisedAccount } from 'main/providers/types';\nimport { createEmailAccount, dispatchEmailRequest, fetchAvailableProviders, fetchProviderAccounts, refreshRequests } from './actions';\nimport { EmailProvider } from './types';\n\ninterface AccountsState {\n    byKey: Record<string, InitialisedAccount>;\n    all: string[];\n    allProviders: string[];\n    availableProviders: Record<string, { requiresEmail: boolean, requiresUrl: boolean }>;\n    isLoading: {\n        requests: boolean;\n        refresh: boolean;\n    }\n    emailProviders: {\n        all: string[];\n        byKey: Record<string, EmailProvider>;\n    }\n}\n\nconst initialState: AccountsState = {\n    byKey: {},\n    all: [],\n    allProviders: [],\n    availableProviders: {},\n    isLoading: {\n        requests: false,\n        refresh: false,\n    },\n    emailProviders: {\n        all: [],\n        byKey: {},\n    },\n};\n\nconst requestsSlice = createSlice({\n    name: 'requests',\n    initialState,\n    reducers: {},\n    extraReducers: (builder) => {\n        builder.addCase(refreshRequests.pending, (state) => { state.isLoading.refresh = true; });\n        builder.addCase(refreshRequests.rejected, (state) => { state.isLoading.refresh = false; });\n        builder.addCase(refreshRequests.fulfilled, (state) => { state.isLoading.refresh = false; });\n        builder.addCase(fetchProviderAccounts.pending, (state) => { state.isLoading.requests = true; });\n        builder.addCase(fetchProviderAccounts.rejected, (state) => { state.isLoading.requests = false; });\n        builder.addCase(fetchProviderAccounts.fulfilled, (state, action) => { \n            state.isLoading.requests = false;\n            state.byKey = action.payload.accounts;\n            state.all = Object.keys(action.payload.accounts);\n        });\n        builder.addCase(fetchAvailableProviders.fulfilled, (state, action) => {\n            state.allProviders = Object.keys(action.payload);\n            state.availableProviders = action.payload;\n        });\n        builder.addCase(createEmailAccount, (state, action) => {\n            if (!state.emailProviders.all.includes(action.payload.account)) {\n                state.emailProviders.all.push(action.payload.account);\n            }\n            state.emailProviders.byKey[action.payload.account] = action.payload;\n        });\n        builder.addCase(dispatchEmailRequest, (state, action) => {\n            state.emailProviders.byKey[action.payload].status.dispatched = new Date().toString();\n        });\n    },\n});\n\nexport default requestsSlice.reducer;"
  },
  {
    "path": "src/app/store/accounts/selectors.ts",
    "content": "import Providers from 'app/utilities/Providers';\nimport { InitialisedAccount } from 'main/providers/types';\nimport { DataRequestCompleted, ProviderEvents, UpdateComplete } from 'main/providers/types/Events';\nimport { useCallback, useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useNavigate } from 'react-router-dom';\nimport { State, useAppDispatch } from '..';\nimport { fetchAvailableProviders, fetchProviderAccounts } from './actions';\n\ntype RequestState = {\n    accounts: State['accounts']['all']\n    map: State['accounts']['byKey']\n    isLoading: State['accounts']['isLoading']['requests'];\n    email: State['accounts']['emailProviders'];\n};\n\n/**\n * Retrieve all currently active requests\n */\nexport function useAccounts(): RequestState {\n    const requests = useSelector((state: State) => state.accounts);\n\n    return {\n        accounts: requests.all,\n        map: requests.byKey,\n        isLoading: requests.isLoading.requests,\n        email: requests.emailProviders,\n    };\n}\n\n/**\n * Retrieve a single provider by key name\n * @param key Provider key\n */\nexport function useProvider(key: string): InitialisedAccount {\n    return useSelector((state: State) => state.accounts.byKey[key]);\n}\n\n/**\n * A helper that keeps the providers up-to-date in Redux\n */\nexport function ProviderSubscription(): null {\n    const dispatch = useAppDispatch();\n    const navigate = useNavigate();\n\n    // Callback that fetches all requests and providers\n    const refreshProviders = useCallback(() => {\n        dispatch(fetchProviderAccounts());\n        dispatch(fetchAvailableProviders());\n    }, [dispatch]);\n\n    // Handler for the events\n    const handleEvent = useCallback((none, type: ProviderEvents, ...props) => {\n        // Forcefully re-fetch all providers and accounts\n        refreshProviders();\n\n        // Optionally send out a notification. based on which event has been returned.\n        switch (type) {\n            case ProviderEvents.UPDATE_COMPLETE: {\n                const [ event ] = props as [ UpdateComplete ];\n                const notification = new Notification(\n                    'Update Complete',\n                    { \n                        body: `There's a new update for ${event.account} on ${event.provider}${event.url && ` (${event.url})`}`,\n                    },\n                );\n                notification.onclick = () => navigate(`/timeline/${event.commitHash}`);\n                break;\n            }\n            case ProviderEvents.DATA_REQUEST_COMPLETED: {\n                const [ event ] = props as [ DataRequestCompleted ];\n                const notification = new Notification(\n                    'Data Request Complete',\n                    {\n                        body: `A data request for ${event.account} on ${event.provider}${event.url ? ` (${event.url})` : ''} was just completed`,\n                    },\n                );\n                notification.onclick = () => navigate(`/timeline/${event.commitHash}`);\n                break;\n            }\n        }\n\n    }, [refreshProviders]);\n\n    useEffect(() => {\n        // Subscribe to any events eminating from the providers helpers, on\n        // which we'll refresh our set of providers\n        Providers.subscribe(handleEvent);\n\n        // Also fetch the providers on mount\n        refreshProviders();\n\n        // Unsubscribe when this component is unmounted\n        return () => {\n            Providers.unsubscribe(handleEvent);\n        };\n    }, []);\n\n    // This component doesn't render anything\n    return null;\n}"
  },
  {
    "path": "src/app/store/accounts/types.ts",
    "content": "import { DataRequestStatus } from 'main/providers/types';\n\nexport interface EmailProvider {\n    organisation: string;\n    emailAccount: string;\n    status: DataRequestStatus;\n    account: string;\n    provider: 'email';\n}"
  },
  {
    "path": "src/app/store/data/actions.ts",
    "content": "import { createAction, createAsyncThunk } from '@reduxjs/toolkit';\nimport Repository from 'app/utilities/Repository';\n\nexport const fetchParsedCommit = createAsyncThunk(\n    'data/fetch/parsed-commit',\n    Repository.parsedCommit,\n);\n\nexport const deleteDatum = createAction<number>(\n    '/data/datum/delete',\n);\n\nexport const resetDeletedData = createAction(\n    '/data/datum/reset-deleted',\n);"
  },
  {
    "path": "src/app/store/data/index.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { ProvidedDataTypes, ProviderDatum } from 'main/providers/types/Data';\nimport { deleteDatum, fetchParsedCommit, resetDeletedData } from './actions';\n\ninterface DataState {\n    all: number[];\n    byKey: ProviderDatum<unknown, unknown>[];\n    byType: Record<ProvidedDataTypes, number[]>;\n    deletedByType: Record<ProvidedDataTypes, number[]>;\n    deletedByProvider: Record<string, number[]>;\n    deleted: number[];\n    isLoading: boolean;\n}\n\nconst byType = Object.values(ProvidedDataTypes).reduce<DataState['byType']>((sum, key) => {\n    sum[key] = [];\n    return sum;\n}, {} as DataState['byType']);\n\nconst initialState: DataState = {\n    all: [],\n    byKey: [],\n    byType,\n    deletedByType: byType,\n    deletedByProvider: {},\n    deleted: [],\n    isLoading: false,\n};\n\nconst data = createSlice({\n    name: 'data',\n    initialState,\n    reducers: {},\n    extraReducers: (builder) => {\n        builder.addCase(fetchParsedCommit.pending, (state) => { state.isLoading = true; });\n        builder.addCase(fetchParsedCommit.rejected, (state) => { state.isLoading = false; });\n        builder.addCase(fetchParsedCommit.fulfilled, (state, action) => { \n            state.isLoading = false;\n\n            // Unfortunately, as data points might change when a new update is\n            // coming in, we cannot retain deleted data points across parsed\n            // commits. Thus we reset the entire state every time a new parsed\n            // commit comes in\n            Object.values(ProvidedDataTypes).map((type) => {\n                state.byType[type] = [];\n                state.deletedByType[type] = []; \n            });\n            state.deletedByProvider = {};\n            state.deleted = [];\n\n            // Then assign the payload directly to the state\n            state.all = Array.from(action.payload.keys());\n            state.byKey = action.payload;\n\n            // Then loop through all data points\n            action.payload.forEach((datum, i): void => {\n                // And push the key for each datum type to the byType array\n                state.byType[datum.type as ProvidedDataTypes].push(i);\n            }, {});\n        });\n        builder.addCase(deleteDatum, (state, action) => {\n            const datum = state.byKey[action.payload];\n\n            // Add the deleted datum to the list for deleted items\n            state.deleted.push(action.payload);\n\n            // Also add it to the list for deleted types\n            state.deletedByType[datum.type as ProvidedDataTypes].push(action.payload);\n            \n            // Also add it to the list sorted by provider\n            if (!(datum.provider in state.deletedByProvider)) {\n                state.deletedByProvider[datum.provider] = [];\n            }\n            state.deletedByProvider[datum.provider].push(action.payload);\n        });\n        builder.addCase(resetDeletedData, (state) => {\n            state.deleted = [];\n            state.deletedByType = initialState.deletedByType;\n            state.deletedByProvider = {};\n        });\n    },\n});\n\nexport default data.reducer;"
  },
  {
    "path": "src/app/store/data/selectors.ts",
    "content": "import Repository from 'app/utilities/Repository';\nimport { useCallback, useEffect } from 'react';\nimport { useAppDispatch } from '..';\nimport { fetchParsedCommit } from './actions';\n\nexport function RepositorySubscription(): JSX.Element {\n    const dispatch = useAppDispatch();\n\n    // A handler for when a new commit is made by the repository\n    const handleRepositoryEvent = useCallback(() => {\n        dispatch(fetchParsedCommit(undefined));\n    }, [dispatch]);\n\n    useEffect(() => {\n        // Fetch the parsed commit on application mount\n        dispatch(fetchParsedCommit(undefined));\n\n        // Then listen for any new commits\n        Repository.subscribe(handleRepositoryEvent);\n\n        // And unsubscribe when the application is unmounted\n        return () => {\n            Repository.unsubscribe(handleRepositoryEvent);\n        };\n    }, []);\n\n    return null;\n}"
  },
  {
    "path": "src/app/store/email/actions.ts",
    "content": "import { createAsyncThunk } from '@reduxjs/toolkit';\nimport Email from 'app/utilities/Email';\n\nexport const fetchEmailAccounts = createAsyncThunk(\n    'email/accounts/fetch',\n    Email.getAccounts,\n);\n\nexport const fetchEmailClients = createAsyncThunk(\n    'email/clients/fetch',\n    Email.getClients,\n);\n\nexport const createEmailAccount = createAsyncThunk(\n    'email/accounts/create',\n    Email.initialise,\n);\n\nexport const testImapConnection = createAsyncThunk(\n    'email/imap/test',\n    Email.testImap,\n);\n\nexport const createImapAccount = createAsyncThunk(\n    'email/accounts/create/imap',\n    Email.initialiseImap,\n);\n"
  },
  {
    "path": "src/app/store/email/index.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { createEmailAccount, fetchEmailAccounts, fetchEmailClients } from './actions';\n\ninterface EmailState {\n    clients: string[];\n    accounts: {\n        byId: Record<string, string>,\n        all: string[]\n    },\n    isLoading: {\n        newAccount: boolean;\n    }\n}\n\nconst initialState: EmailState = {\n    clients: [],\n    accounts: {\n        byId: {},\n        all: [],\n    },\n    isLoading: {\n        newAccount: false,\n    },\n};\n\nconst emailSlice = createSlice({\n    name: 'email',\n    initialState,\n    reducers: {},\n    extraReducers: (builder) => {\n        builder.addCase(fetchEmailAccounts.fulfilled, (state, action) => {\n            state.accounts.byId = action.payload;\n            state.accounts.all = Object.keys(action.payload);\n        });\n        builder.addCase(fetchEmailClients.fulfilled, (state, action) => {\n            state.clients = action.payload;\n        });\n        builder.addCase(createEmailAccount.pending, (state) => { state.isLoading.newAccount = true; });\n        builder.addCase(createEmailAccount.fulfilled, (state) => { state.isLoading.newAccount = false; });\n    },\n});\n\nexport default emailSlice.reducer;"
  },
  {
    "path": "src/app/store/email/selectors.ts",
    "content": "import Email from 'app/utilities/Email';\nimport { useCallback, useEffect } from 'react';\nimport { useAppDispatch } from '..';\nimport { fetchEmailAccounts } from './actions';\n\n/**\n * A helper that keeps the providers up-to-date in Redux\n */\nexport function EmailSubscription(): null {\n    const dispatch = useAppDispatch();\n\n    // Callback that fetched all requests\n    const refreshAccounts = useCallback(() => {\n        dispatch(fetchEmailAccounts());\n    }, [dispatch]);\n\n    useEffect(() => {\n        // Subscribe to any events eminating from the providers helpers, on\n        // which we'll refresh our set of providers\n        Email.subscribe(refreshAccounts);\n\n        // Also fetch the providers on mount\n        refreshAccounts();\n\n        // Unsubscribe when this component is unmounted\n        return () => {\n            Email.unsubscribe(refreshAccounts);\n        };\n    }, []);\n\n    // This component doesn't render anything\n    return null;\n}"
  },
  {
    "path": "src/app/store/index.ts",
    "content": "import { configureStore } from '@reduxjs/toolkit';\nimport { combineReducers } from 'redux';\nimport { \n    createMigrate, \n    PersistConfig, \n    persistReducer, \n    persistStore,\n    FLUSH,\n    REHYDRATE,\n    PAUSE,\n    PERSIST,\n    PURGE,\n    REGISTER,\n} from 'redux-persist';\nimport { useDispatch } from 'react-redux';\nimport ElectronStorage from './persist';\nimport migrations from './migrations';\n\nimport newCommits from './new-commits';\nimport onboarding from './onboarding';\nimport telemetry from './telemetry';\nimport accounts from './accounts';\nimport email from './email';\nimport data from './data';\n\n// The root reducer contains all the individual reducers that make up the store\nconst rootReducer = combineReducers({\n    newCommits,\n    onboarding,\n    telemetry,\n    accounts,\n    email,\n    data,\n});\n\n// Export types for later inclusion\nexport type State = ReturnType<typeof rootReducer>;\n\n// Using this config, the store will be persisted using electron-store\nconst persistConfig: PersistConfig<State> = {\n    key: 'app_store',\n    storage: ElectronStorage(),\n    version: 9,\n    migrate: createMigrate(migrations),\n    serialize: false,\n    deserialize: false,\n    blacklist: [\n        'data',\n        'accounts',\n        'email',\n    ],\n};\n\n// Create a persisted reducer\nconst persistedReducer = persistReducer(persistConfig, rootReducer);\n\n\n// Create a store from the persisted root reducer, optionally applying middleware\nconst store = configureStore({\n    reducer: persistedReducer,\n    middleware: (getDefaultMiddleware) => getDefaultMiddleware({\n        // We need to ignore all redux-persist actions\n        serializableCheck: {\n            ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],\n        },\n    }),\n});\n\n// Create a persisted store\nexport const persistor = persistStore(store);\n\nexport type AppDispatch = typeof store.dispatch;\n\n// Export hooks with injected store types for ease-of-use\nexport const useAppDispatch = (): typeof store.dispatch => useDispatch<AppDispatch>();\n\n// Export the store\nexport default store;"
  },
  {
    "path": "src/app/store/migrations.ts",
    "content": "import { MigrationManifest } from 'redux-persist';\n\nconst migrations: MigrationManifest = {\n    /**\n     * Migrate from the undux-based store to Redux\n     */\n    5: (state: any) => {\n        return {\n            _persist: state._persist,\n            onboarding: state.onboardingComplete,\n            telemetry: state.telemetry,\n            newCommits: state.newCommit || [],\n        };\n    },\n    6: (state: any) => {\n        return {\n            ...state,\n            requests: {\n                availableProviders: [],\n                all: [],\n                byKey: {},\n                isLoading: state.accounts.isLoading,\n            },\n        };\n    },\n    7: (state: any) => {\n        const { requests, ...rest } = state;\n        return {\n            ...rest,\n            accounts: requests,\n        };\n    },\n    8: (state) => {\n        return {\n            ...state,\n            onboarding: {\n                initialisation: false,\n                tour: [],\n            },\n        };\n    },\n    9: (state: any) => {\n        return {\n            ...state,\n            accounts: {\n                ...state.accounts,\n                emailProviders: {\n                    all: [],\n                    byKey: {},\n                },\n            },\n        };\n    },\n};\n\nexport default migrations;"
  },
  {
    "path": "src/app/store/new-commits/actions.ts",
    "content": "import { newCommitsSlice } from '.';\n\nexport const { add: addNewCommit } = newCommitsSlice.actions;"
  },
  {
    "path": "src/app/store/new-commits/index.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport { Commit, ExtractedDataDiff } from 'main/lib/repository/types';\n\ninterface NewCommit extends Commit {\n    diff: ExtractedDataDiff\n}\n\ntype NewCommitState = NewCommit[];\n\nexport const newCommitsSlice = createSlice({\n    name: 'newCommit',\n    initialState: [] as NewCommitState,\n    reducers: {\n        add(state, action: PayloadAction<NewCommit>) {\n            state.unshift(action.payload);\n        },\n    },\n});\n\nexport default newCommitsSlice.reducer;"
  },
  {
    "path": "src/app/store/onboarding/actions.ts",
    "content": "import { onboardingSlice } from '.';\n\nexport const { complete: completeOnboarding } = onboardingSlice.actions;\nexport const { completeTour } = onboardingSlice.actions;"
  },
  {
    "path": "src/app/store/onboarding/index.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\nimport { TourKeys } from 'app/components/Tour/steps';\n\ninterface OnboardingState {\n    initialisation: boolean;\n    tour: TourKeys[];\n}\n\nconst initialState: OnboardingState = {\n    initialisation: false,\n    tour: [],\n};\n\nexport const onboardingSlice = createSlice({\n    name: 'onboardingComplete',\n    initialState,\n    reducers: {\n        complete(state, action: PayloadAction<keyof Omit<OnboardingState, 'tour'>>) {\n            state[action.payload] = true;\n        },\n        completeTour(state, action: PayloadAction<TourKeys>) {\n            state.tour.push(action.payload);\n        },\n    },\n});\n\nexport default onboardingSlice.reducer;"
  },
  {
    "path": "src/app/store/persist.ts",
    "content": "import {  Storage } from 'redux-persist';\n\nexport default function ElectronStorage(): Storage {\n    return {\n        getItem: () => {\n            return Promise.resolve(window.api.store.retrieve());\n        },\n        setItem: (key: string, item: string) => {\n            return Promise.resolve(window.api.store.persist(item));\n        },\n        removeItem: () => {\n            return Promise.resolve(window.api.store.clear());\n        },\n    };\n}"
  },
  {
    "path": "src/app/store/telemetry/actions.ts",
    "content": "import { telemetrySlice } from '.';\n\nexport const { log: addTelemetryLog } = telemetrySlice.actions;"
  },
  {
    "path": "src/app/store/telemetry/index.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit';\n\nexport interface Event {\n    event: string;\n    element: string;\n    timestamp: string;\n    route: string;\n}\n\nconst initialState: Event[] = [];\n\nexport const telemetrySlice = createSlice({\n    name: 'telemetry',\n    initialState,\n    reducers: {\n        log(state, action: PayloadAction<Event>) {\n            state.push(action.payload);\n        },\n    },\n});\n\nexport default telemetrySlice.reducer;"
  },
  {
    "path": "src/app/styles/global.css",
    "content": "html {\n    min-height: 100%;\n}\n\nbody {\n    min-height: 100vh;\n    font-family: var(--font-body), -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n    Helvetica, Arial, sans-serif;\n    margin: 0;\n    padding: 0;\n    font-size: 14px;\n    color: var(--color-text);\n    line-height: 1.7;\n    text-rendering: optimizeLegibility;\n    -webkit-font-smoothing: antialiased;\n    background-color: transparent;\n}\n\nbody * {\n    box-sizing: border-box;\n    user-select: none;\n    text-decoration: none;\n    overflow-wrap: break-word;\n    hyphens: auto;\n    text-overflow: ellipsis;\n}\n\n*:focus {\n    outline: none;\n}\n\na {\n    cursor: default;\n}\n\nbutton:disabled {\n    cursor: not-allowed;\n}\n\ncode {\n    font-family: var(--font-mono);\n    border-radius: 4px;\n    background-color: var(--color-background);\n}\n\na {\n    color: inherit;\n}\n"
  },
  {
    "path": "src/app/styles/index.ts",
    "content": "import '@fontsource/ibm-plex-mono';\nimport '@fontsource/ibm-plex-sans';\nimport '@fontsource/inter/variable.css';\nimport './theme.css';\nimport './global.css';"
  },
  {
    "path": "src/app/styles/snippets.ts",
    "content": "import { css } from 'styled-components';\n\nexport const LargeShadow = css`\n    box-shadow: 0 1px 2px rgba(0,0,0,0.07), \n                0 2px 4px rgba(0,0,0,0.07), \n                0 4px 8px rgba(0,0,0,0.07), \n                0 8px 16px rgba(0,0,0,0.07),\n                0 16px 32px rgba(0,0,0,0.07), \n                0 32px 64px rgba(0,0,0,0.07),\n                0 64px 128px rgba(0,0,0,0.07);\n`;\n\nexport const Shadow = css`\n    box-shadow: 0 1px 2px rgba(0,0,0,0.04), \n                    0 2px 4px rgba(0,0,0,0.04), \n                    0 4px 8px rgba(0,0,0,0.04), \n                    0 8px 16px rgba(0,0,0,0.04),\n                    0 16px 32px rgba(0,0,0,0.04);\n`;"
  },
  {
    "path": "src/app/styles/theme.css",
    "content": ":root {\n    /* Blue */\n    --color-blue-50: hsl(223, 100%, 97%);\n    --color-blue-100: hsl(223, 100%, 95%);\n    --color-blue-200: hsl(223, 100%, 85%);\n    --color-blue-300: hsl(223, 100%, 70%);\n    --color-blue-400: hsl(223, 100%, 60%);\n    --color-blue-500: hsl(223, 100%, 50%);\n    --color-blue-600: hsl(223, 100%, 45%);\n    --color-blue-700: hsl(223, 100%, 30%);\n    --color-blue-800: hsl(223, 100%, 20%);\n    --color-blue-900: hsl(223, 100%, 10%);\n\n    /* Grey */\n    --color-gray-50: hsl(223, 5%, 98%);\n    --color-gray-100: hsl(223, 5%, 97%);\n    --color-gray-200: hsl(223, 5%, 95%);\n    --color-gray-300: hsl(223, 5%, 90%);\n    --color-gray-400: hsl(223, 5%, 87%);\n    --color-gray-500: hsl(223, 5%, 73%);\n    --color-gray-600: hsl(223, 5%, 67%);\n    --color-gray-700: hsl(223, 5%, 47%);\n    --color-gray-800: hsl(223, 5%, 33%);\n    --color-gray-900: hsl(223, 5%, 20%);\n\n    /* Black and White */\n    --color-white: #FFFFFF;\n    --color-black: #000000;\n\n    /* Other Colors */\n    --color-green-50: hsl(81, 90%, 97%);\n    --color-green-100: hsl(81, 90%, 95%);\n    --color-green-200: hsl(81, 90%, 80%);\n    --color-green-500: hsl(81, 90%, 42%);\n    --color-green-600: hsl(81, 90%, 37%);\n    --color-red-50: hsl(10, 100%, 97%);\n    --color-red-100: hsl(10, 100%, 95%);\n    --color-red-200: hsl(10, 100%, 80%);\n    --color-red-500: hsl(10, 100%, 65%);\n    --color-red-600: hsl(10, 100%, 58%);\n    --color-yellow-50: hsl(42, 100%, 97%);\n    --color-yellow-100: hsl(42, 100%, 95%);\n    --color-yellow-200: hsl(42, 100%, 80%);\n    --color-yellow-500: hsl(42, 100%, 58%);\n    --color-yellow-600: hsl(42, 100%, 50%);\n    \n    /* Specific implementations */\n    --color-primary: var(--color-blue-500);\n    --color-border: var(--color-gray-300);\n    --color-background: var(--color-white);\n    --color-modal-background: var(--color-white);\n    --color-text: var(--color-gray-800);\n    --color-header: var(--color-black);\n\n    /* Typography */\n    --font-body: 'IBM Plex Sans';\n    --font-mono: 'IBM Plex Mono';\n    --font-heading: 'InterVariable';\n}\n\n@media (prefers-color-scheme: dark) {\n    :root {\n        /* Blue */\n        --color-blue-50: hsl(216, 10%, 12%);\n        --color-blue-100: hsl(216, 20%, 15%);\n        --color-blue-200: hsl(216, 30%, 20%);\n        --color-blue-300: hsl(216, 50%, 25%);\n        --color-blue-400: hsl(216, 65%, 40%);\n        --color-blue-500: hsl(216, 100%, 50%);\n        --color-blue-600: hsl(216, 100%, 57%);\n        --color-blue-700: hsl(216, 100%, 65%);\n        --color-blue-800: hsl(216, 100%, 75%);\n        --color-blue-900: hsl(216, 100%, 85%);\n\n        /* Grey */\n        --color-gray-50: hsl(216, 8%, 9%);\n        --color-gray-100: hsl(216, 8%, 13%);\n        --color-gray-200: hsl(216, 8%, 15%);\n        --color-gray-300: hsl(216, 8%, 20%);\n        --color-gray-400: hsl(216, 8%, 27%);\n        --color-gray-500: hsl(216, 8%, 40%);\n        --color-gray-600: hsl(216, 8%, 67%);\n        --color-gray-700: hsl(216, 8%, 87%);\n        --color-gray-800: hsl(216, 8%, 93%);\n        --color-gray-900: hsl(216, 8%, 97%);\n\n        /* Black and White */\n        --color-white: #FFFFFF;\n        --color-black: #121212;\n\n        /* Other Colors */\n        --color-green-50: hsl(81, 10%, 10%);\n        --color-green-100: hsl(81, 55%, 12%);\n        --color-green-200: hsl(81, 100%, 30%);\n        --color-green-500: hsl(81, 100%, 50%);\n        --color-green-600: hsl(81, 100%, 54%);\n        --color-red-50: hsl(10, 10%, 13%);\n        --color-red-100: hsl(10, 35%, 17%);\n        --color-red-200: hsl(10, 50%, 30%);\n        --color-red-500: hsl(10, 100%, 63%);\n        --color-red-600: hsl(10, 100%, 66%);\n        --color-yellow-50: hsl(42, 75%, 6%);\n        --color-yellow-100: hsl(42, 100%, 12%);\n        --color-yellow-200: hsl(42, 100%, 23%);\n        --color-yellow-500: hsl(42, 100%, 58%);\n        --color-yellow-600: hsl(42, 100%, 62%);\n        \n        /* Specific implementations */\n        /* --color-primary: var(--color-blue-500); */\n        /* --color-border: var(--color-gray-200); */\n        --color-background: var(--color-black);\n        --color-modal-background: var(--color-black);\n        /* --color-text: var(--color-white); */\n        --color-header: var(--color-white);\n    }\n}"
  },
  {
    "path": "src/app/utilities/DataType.tsx",
    "content": "import React from 'react';\nimport {\n    ProvidedDataTypes,\n    ProviderDatum,\n    Address,\n    Photo,\n    PrivacySetting,\n    LoginInstance,\n    ProfilePicture,\n    Session, Employment, EventResponse, VisitedPage, OffSiteActivity, EducationExperience, MobileDevice, RegistrationDate, PlayedSong,\n} from 'main/providers/types/Data';\nimport {\n    IconDefinition,\n    faSquare,\n    faCookieBite,\n    faBan,\n    faHashtag,\n    faPhone,\n    faComment,\n    faAd,\n    faHome,\n    faAddressCard,\n    faFlag,\n    faHeart,\n    faSignInAlt,\n    faSignOutAlt,\n    faImage,\n    faComments,\n    faVenusMars,\n    faIdCard,\n    faLanguage,\n    faIdBadge,\n    faCalendar,\n    faSearch,\n    faEye,\n    faUserCog,\n    faAddressBook,\n    faEnvelope,\n    faNetworkWired,\n    faTablet,\n    faUserCircle,\n    faUserFriends,\n    faShoePrints,\n    faBook, \n    faUsers, \n    faBriefcase, \n    faFile, \n    faMoneyBillWaveAlt,\n    faListAlt,\n    faCheck,\n    faClock,\n    faUniversity,\n    faUserPlus,\n    faMobile,\n    faMusic,\n    faRobot,\n    faBirthdayCake,\n} from '@fortawesome/free-solid-svg-icons';\n\nclass DataType {\n    /**\n     * Retrieve an icon for a given ProvidedDateType\n     * @param type \n     */\n    static getIcon(type: ProvidedDataTypes): IconDefinition {\n        switch (type) {\n            case ProvidedDataTypes.EMAIL:\n                return faEnvelope;\n            case ProvidedDataTypes.FIRST_NAME:\n                return faIdCard;\n            case ProvidedDataTypes.LAST_NAME:\n                return faIdCard;\n            case ProvidedDataTypes.FULL_NAME:\n                return faIdCard;\n            case ProvidedDataTypes.USER_AGENT:\n                return faIdBadge;\n            case ProvidedDataTypes.USER_LANGUAGE:\n                return faLanguage;\n            case ProvidedDataTypes.COOKIE:\n                return faCookieBite;\n            case ProvidedDataTypes.BLOCKED_ACCOUNT:\n                return faBan;\n            case ProvidedDataTypes.HASHTAG_FOLLOWING:\n                return faHashtag;\n            case ProvidedDataTypes.AD_INTEREST:\n                return faAd;\n            case ProvidedDataTypes.TELEPHONE_NUMBER:\n                return faPhone;\n            case ProvidedDataTypes.COMMENT:\n                return faComment;\n            case ProvidedDataTypes.PLACE_OF_RESIDENCE:\n                return faHome;\n            case ProvidedDataTypes.ADDRESS:\n                return faAddressCard;\n            case ProvidedDataTypes.COUNTRY:\n                return faFlag;\n            case ProvidedDataTypes.LIKE:\n                return faHeart;\n            case ProvidedDataTypes.LOGIN_INSTANCE:\n                return faSignInAlt;\n            case ProvidedDataTypes.LOGOUT_INSTANCE:\n                return faSignOutAlt;\n            case ProvidedDataTypes.PHOTO:\n                return faImage;\n            case ProvidedDataTypes.MESSAGE:\n                return faComments;\n            case ProvidedDataTypes.GENDER:\n                return faVenusMars;\n            case ProvidedDataTypes.DATE_OF_BIRTH:\n                return faBirthdayCake;\n            case ProvidedDataTypes.JOIN_DATE:\n                return faCalendar;\n            case ProvidedDataTypes.SEARCH_QUERY:\n                return faSearch;\n            case ProvidedDataTypes.POST_SEEN:\n                return faEye;\n            case ProvidedDataTypes.PRIVACY_SETTING:\n                return faUserCog;\n            case ProvidedDataTypes.UPLOADED_CONTACT:\n                return faAddressBook;\n            case ProvidedDataTypes.IP_ADDRESS:\n                return faNetworkWired;\n            case ProvidedDataTypes.DEVICE:\n                return faTablet;\n            case ProvidedDataTypes.PROFILE_PICTURE:\n                return faUserCircle;\n            case ProvidedDataTypes.FOLLOWER:\n                return faUserFriends;\n            case ProvidedDataTypes.ACCOUNT_FOLLOWING:\n                return faShoePrints;\n            case ProvidedDataTypes.USERNAME:\n                return faIdBadge;\n            case ProvidedDataTypes.SESSION:\n                return faBook;\n            case ProvidedDataTypes.PEER_GROUP:\n                return faUsers;\n            case ProvidedDataTypes.EMPLOYMENT:\n                return faBriefcase;\n            case ProvidedDataTypes.VISITED_PAGE:\n                return faFile;\n            case ProvidedDataTypes.OFF_SITE_ACTIVITY:\n                return faListAlt;\n            case ProvidedDataTypes.EVENT_RESPONSE:\n                return faCheck;\n            case ProvidedDataTypes.TIMEZONE:\n                return faClock;\n            case ProvidedDataTypes.CURRENCY:\n                return faMoneyBillWaveAlt;\n            case ProvidedDataTypes.EDUCATION_EXPERIENCE:\n                return faUniversity;\n            case ProvidedDataTypes.REGISTRATION_DATE:\n                return faUserPlus;\n            case ProvidedDataTypes.MOBILE_DEVICE:\n                return faMobile;\n            case ProvidedDataTypes.PLAYED_SONG:\n                return faMusic;\n            case ProvidedDataTypes.INFERENCE:\n                return faRobot;\n            case ProvidedDataTypes.BIOGRAPHY:\n                return faBook;\n            case ProvidedDataTypes.ADVERTISER:\n                return faAd;\n            default:\n                return faSquare;\n        }\n    }\n\n    /**\n     * Convert the datum provided by a data extraction diff into a string\n     * @param datum ProviderDatum\n     */\n    static toString(datum: ProviderDatum<unknown, unknown>): JSX.Element | string {\n        switch (datum.type) {\n            case ProvidedDataTypes.PHOTO: {\n                const { data: photo } = datum as Photo;   \n                return <img src={photo.url} alt=\"\" />;\n            }\n            case ProvidedDataTypes.ADDRESS: {\n                const { data: address } = datum as Address;   \n                return `${address.street} ${address.number} ${address.state && address.state + '\\n'}`;\n            }\n            case ProvidedDataTypes.JOIN_DATE: {\n                const { timestamp } = datum as ProviderDatum<Date, unknown>;\n                return timestamp.toLocaleString();\n            }\n            case ProvidedDataTypes.PRIVACY_SETTING: {\n                const { data: setting } = datum as PrivacySetting;\n                return `${setting.key}: ${setting.value}`;\n            }\n            case ProvidedDataTypes.LOGIN_INSTANCE: {\n                const { data: instance } = datum as LoginInstance;\n                return new Date(instance * 1000).toLocaleString();\n            }\n            case ProvidedDataTypes.PROFILE_PICTURE: {\n                const { data: src } = datum as ProfilePicture;\n                return <img src={src} alt=\"Profile\" />;\n            }\n            case ProvidedDataTypes.SESSION: {\n                const { data: session } = datum as Session;\n                return `${session?.user_agent}, ${session?.ip_address} at ${session?.timestamp}`;\n            }\n            case ProvidedDataTypes.EMPLOYMENT: {\n                const { data: { jobTitle, company } } = datum as Employment;\n                return `${jobTitle} at ${company}`;\n            }\n            case ProvidedDataTypes.EVENT_RESPONSE: {\n                const { data: { name, response } } = datum as EventResponse;\n                return `${name} ${response && `(${response})`}`;\n            }\n            case ProvidedDataTypes.VISITED_PAGE: {\n                const { data: { name } } = datum as VisitedPage;\n                return name;\n            }\n            case ProvidedDataTypes.OFF_SITE_ACTIVITY: {\n                const { data: { website, type } } = datum as OffSiteActivity;\n                return `${type} at ${website}`;\n            }\n            case ProvidedDataTypes.EDUCATION_EXPERIENCE: {\n                const { data: { institution, type } } = datum as EducationExperience;\n                return `${type ? type + ' at ' : ''} ${institution}`;\n            }\n            case ProvidedDataTypes.MOBILE_DEVICE: {\n                const { data: { type, os } } = datum as MobileDevice;\n                return `${type} (${os})`;\n            }\n            case ProvidedDataTypes.REGISTRATION_DATE: {\n                const { data: registrationDate } = datum as RegistrationDate;\n                return registrationDate.toLocaleString();\n            }\n            case ProvidedDataTypes.PLAYED_SONG: {\n                const { data: { track, artist } } = datum as PlayedSong;\n                return `${artist} - ${track}`;\n            }\n            case ProvidedDataTypes.EMAIL:\n            case ProvidedDataTypes.FIRST_NAME:\n            case ProvidedDataTypes.LAST_NAME:\n            case ProvidedDataTypes.FULL_NAME:\n            case ProvidedDataTypes.IP_ADDRESS:\n            case ProvidedDataTypes.USER_AGENT:\n            case ProvidedDataTypes.USER_LANGUAGE:\n            case ProvidedDataTypes.FOLLOWER:\n            case ProvidedDataTypes.ACCOUNT_FOLLOWING:\n            case ProvidedDataTypes.BLOCKED_ACCOUNT:\n            case ProvidedDataTypes.HASHTAG_FOLLOWING:\n            case ProvidedDataTypes.AD_INTEREST:\n            case ProvidedDataTypes.TELEPHONE_NUMBER:\n            case ProvidedDataTypes.DEVICE:\n            case ProvidedDataTypes.USERNAME:\n            case ProvidedDataTypes.PLACE_OF_RESIDENCE:\n            case ProvidedDataTypes.COUNTRY:\n            case ProvidedDataTypes.LIKE:\n            case ProvidedDataTypes.MESSAGE:\n            case ProvidedDataTypes.GENDER:\n            case ProvidedDataTypes.SEARCH_QUERY:\n            case ProvidedDataTypes.POST_SEEN:\n            case ProvidedDataTypes.INFERENCE:\n            case ProvidedDataTypes.BIOGRAPHY:\n            default:\n                return datum.data as string;\n        }\n    }\n\n    /**\n     * Convert the datum type to a description\n     * @param datum \n     */\n    static getDescription(datum: ProviderDatum<unknown, unknown>): string {\n        switch (datum.type) {\n            case ProvidedDataTypes.EMAIL:\n                return 'A email adress';\n            case ProvidedDataTypes.FIRST_NAME:\n                return 'A first name';\n            case ProvidedDataTypes.LAST_NAME:\n                return 'A last name';\n            case ProvidedDataTypes.FULL_NAME:\n                return 'A full name, including first and last name';\n            case ProvidedDataTypes.IP_ADDRESS:\n                return 'An IP address';\n            case ProvidedDataTypes.USER_AGENT:\n                return 'A user-agent that was saved as part as a log file';\n            case ProvidedDataTypes.USER_LANGUAGE:\n                return 'A language that is used for browsing the platform';\n            case ProvidedDataTypes.COOKIE:\n                return 'A cookie that was saved';\n            case ProvidedDataTypes.FOLLOWER:\n                return 'A follower for the user';\n            case ProvidedDataTypes.ACCOUNT_FOLLOWING:\n                return 'Another account that the user is following';\n            case ProvidedDataTypes.BLOCKED_ACCOUNT:\n                return 'Another account that the user has blocked';\n            case ProvidedDataTypes.HASHTAG_FOLLOWING:\n                return 'A hashtag the user is following';\n            case ProvidedDataTypes.AD_INTEREST:\n                return 'An ad interest that was flagged by the system for the user';\n            case ProvidedDataTypes.TELEPHONE_NUMBER:\n                return 'A telephone number';\n            case ProvidedDataTypes.COMMENT:\n                return 'A comment made by the user';\n            case ProvidedDataTypes.DEVICE:\n                return 'A device that was used by the user on the platform';\n            case ProvidedDataTypes.USERNAME:\n                return 'A username that is used for a particular platform';\n            case ProvidedDataTypes.PLACE_OF_RESIDENCE:\n                return 'A place (city, town, village, etc.) where the user resides';\n            case ProvidedDataTypes.ADDRESS:\n                return 'A full adress, including street, number, ZIP-code and optionally state';\n            case ProvidedDataTypes.COUNTRY:\n                return 'The country where a user resides';\n            case ProvidedDataTypes.LIKE:\n                return 'A like thas been placed on a particular post';\n            case ProvidedDataTypes.LOGIN_INSTANCE:\n                return 'A saved instance of the user logging in';\n            case ProvidedDataTypes.LOGOUT_INSTANCE:\n                return 'A saved instance of the user logging out';\n            case ProvidedDataTypes.PHOTO:\n                return 'A photo';\n            case ProvidedDataTypes.MESSAGE:\n                return 'A message by the user, to another user';\n            case ProvidedDataTypes.GENDER:\n                return 'A gender';\n            case ProvidedDataTypes.PROFILE_PICTURE:\n                return 'A profile picture';\n            case ProvidedDataTypes.DATE_OF_BIRTH:\n                return 'A birth date';\n            case ProvidedDataTypes.JOIN_DATE:\n                return 'The date on which the user joined a platform';\n            case ProvidedDataTypes.SEARCH_QUERY:\n                return 'A search query by the user';\n            case ProvidedDataTypes.POST_SEEN:\n                return 'A post that the user has seen';\n            case ProvidedDataTypes.PRIVACY_SETTING:\n                return 'A privacy setting for the user';\n            case ProvidedDataTypes.UPLOADED_CONTACT:\n                return 'A telephone contact that has been uploaded by the user';\n            case ProvidedDataTypes.SESSION:\n                return 'A saved cookie with possibly extra information';\n            case ProvidedDataTypes.PEER_GROUP:\n                return 'A categorisation of the peer group you belong to';\n            case ProvidedDataTypes.EMPLOYMENT:\n                return 'A job held currently or in the past';\n            case ProvidedDataTypes.VISITED_PAGE:\n                return 'An in-site visited page';\n            case ProvidedDataTypes.OFF_SITE_ACTIVITY:\n                return 'Recorded activity outside of the platform website';\n            case ProvidedDataTypes.EVENT_RESPONSE:\n                return 'Response to an event invitation';\n            case ProvidedDataTypes.TIMEZONE:\n                return 'Timezone associated with the user';\n            case ProvidedDataTypes.CURRENCY:\n                return 'Currency associated with the user';\n            case ProvidedDataTypes.EDUCATION_EXPERIENCE:\n                return 'An education experience held currently or in the past';\n            case ProvidedDataTypes.REGISTRATION_DATE:\n                return 'Registration date for the platform';\n            case ProvidedDataTypes.MOBILE_DEVICE:\n                return 'A mobile device associated with the platform';\n            case ProvidedDataTypes.INFERENCE:\n                return 'An inference about an individual';\n            case ProvidedDataTypes.PLAYED_SONG:\n                return 'A song that has been played by the user';\n            case ProvidedDataTypes.BIOGRAPHY:\n                return 'A biography that describes the user';\n            case ProvidedDataTypes.ADVERTISER:\n                return 'An advertiser that is advertising to the user';\n            default:\n                return '';\n        }\n    }\n}\n\nexport default DataType;"
  },
  {
    "path": "src/app/utilities/Email.ts",
    "content": "import { faGoogle, faMicrosoft, IconDefinition } from '@fortawesome/free-brands-svg-icons';\nimport { faEnvelope } from '@fortawesome/free-solid-svg-icons';\nimport { IpcRendererEvent } from 'electron';\nimport { ImapCredentials } from 'main/email-client/imap';\nimport { EmailCommands, EmailEvents } from 'main/email-client/types';\n\nconst channelName = 'email';\n\ntype SubscriptionHandler = (event: IpcRendererEvent, type: EmailEvents) => void;\n\nclass Email {\n    static subscribe(handler: SubscriptionHandler): void {\n        window.api.on(channelName, handler);\n    }\n\n    static unsubscribe(handler: SubscriptionHandler): void {\n        window.api.removeListener(channelName, handler);\n    }\n\n    static initialise(client: string): Promise<string> {\n        return window.api.invoke(channelName, EmailCommands.ADD_ACCOUNT, client);\n    }\n\n    static initialiseImap({ user, pass, host, port, secure = true }: ImapCredentials) {\n        return window.api.invoke(channelName, EmailCommands.ADD_ACCOUNT, 'imap', user, pass, host, port, secure);\n    }\n\n    static delete(account: string): Promise<string> {\n        return window.api.invoke(channelName, EmailCommands.DELETE_ACCOUNT, account);\n    }\n\n    static getAccounts(): Promise<Record<string, string>> {\n        return window.api.invoke(channelName, EmailCommands.GET_ACCOUNTS);\n    }\n\n    static getClients(): Promise<string[]> {\n        return window.api.invoke(channelName, EmailCommands.GET_CLIENTS);\n    }\n\n    static testImap({ \n        user,\n        pass,\n        host,\n        port,\n        secure = true,\n    }: ImapCredentials): Promise<boolean> {\n        return window.api.invoke(channelName, EmailCommands.TEST_IMAP, user, pass, host, port, secure);\n    }\n\n    static getIcon(clientKey: string): IconDefinition {\n        switch (clientKey) {\n            case 'gmail':\n                return faGoogle;\n            case 'outlook':\n                return faMicrosoft;\n            default:\n                return faEnvelope;\n        }\n    }\n}\n\nexport default Email;"
  },
  {
    "path": "src/app/utilities/Providers.ts",
    "content": "import { InitOptionalParameters, InitialisedAccount } from 'main/providers/types';\nimport { ProviderCommands, ProviderEvents } from 'main/providers/types/Events';\nimport { faFacebookF, faInstagram, faLinkedinIn, faSpotify, IconDefinition } from '@fortawesome/free-brands-svg-icons';\nimport { faEnvelope, faSquare } from '@fortawesome/free-solid-svg-icons';\nimport { IpcRendererEvent } from 'electron';\nimport faOpenDataRights from 'app/assets/open-data-rights';\n\nconst channel = 'providers';\n\ntype DataRequestReturnType = {\n    lastChecked: Date;\n    accounts: Record<string, InitialisedAccount>;\n};\n\ntype SubscriptionHandler = (event: IpcRendererEvent, type: ProviderEvents, ...props: unknown[]) => void;\n\nclass Providers {\n    static subscribe(handler: SubscriptionHandler): void {\n        window.api.on(channel, handler);\n    }\n\n    static unsubscribe(handler: SubscriptionHandler): void {\n        window.api.removeListener(channel, handler);\n    }\n\n    static initialise(key: string, optional?: InitOptionalParameters): Promise<boolean> {\n        return window.api.invoke(channel, ProviderCommands.INITIALISE, key, optional);\n    }\n\n    static update(key: string): Promise<void> {\n        return window.api.invoke(channel, ProviderCommands.UPDATE, key);\n    }\n\n    static updateAll(): Promise<void> {\n        return window.api.invoke(channel, ProviderCommands.UPDATE_ALL);\n    }\n\n    static dispatchDataRequest(key: string): Promise<void> {\n        return window.api.invoke(channel, ProviderCommands.DISPATCH_DATA_REQUEST, key);\n    }\n\n    static dispatchDataRequestToAll(): Promise<void> {\n        return window.api.invoke(channel, ProviderCommands.DISPATCH_DATA_REQUEST_TO_ALL);\n    }\n\n    static refresh(): Promise<void> {\n        return window.api.invoke(channel, ProviderCommands.REFRESH);\n    }\n\n    static getAccounts(): Promise<DataRequestReturnType> {\n        return window.api.invoke(channel, ProviderCommands.GET_ACCOUNTS);\n    }\n\n    static getAvailableProviders(): Promise<Record<string, { requiresEmail: boolean, requiresUrl: boolean }>> {\n        return window.api.invoke(channel, ProviderCommands.GET_AVAILABLE_PROVIDERS);\n    }\n\n    static getIcon(key: string): IconDefinition {\n        switch (key) {\n            case 'instagram':\n                return faInstagram;\n            case 'facebook':\n                return faFacebookF;\n            case 'linkedin':\n                return faLinkedinIn;\n            case 'spotify':\n                return faSpotify;\n            case 'open-data-rights':\n                return faOpenDataRights;\n            case 'email':\n                return faEnvelope;\n            default:\n                return faSquare;\n        }\n    }\n\n    static getPrivacyEmail(key: string): string {\n        switch (key) {\n            case 'spotify':\n                return 'privacy@spotify.com';\n            default: \n                return '';\n        }\n    }\n\n}\n\nexport default Providers;"
  },
  {
    "path": "src/app/utilities/Repository.ts",
    "content": "import { DiffResult, RepositoryCommands, RepositoryArguments, RepositoryEvents, Commit } from 'main/lib/repository/types';\nimport { ProviderDatum } from 'main/providers/types/Data';\nimport type { StatusFile } from 'nodegit';\nimport { IpcRendererEvent } from 'electron';\n\nconst channel = 'repository';\n\ntype SubscriptionHandler = (event: IpcRendererEvent, type: RepositoryEvents) => void;\n\nclass Repository {\n    static subscribe(handler: SubscriptionHandler): void {\n        window.api.on(channel, handler);\n    }\n\n    static unsubscribe(handler: SubscriptionHandler): void {\n        window.api.removeListener(channel, handler);\n    }\n\n    static diff(refTree?: string | RepositoryArguments, comparedTree?: string | RepositoryArguments): Promise<DiffResult<unknown>[]> {\n        return window.api.invoke(channel, RepositoryCommands.DIFF, refTree, comparedTree);\n    }\n\n    static parsedCommit(tree?: string | RepositoryArguments): Promise<ProviderDatum<unknown, unknown>[]> {\n        return window.api.invoke(channel, RepositoryCommands.PARSED_COMMIT, tree);\n    }\n\n    static log(): Promise<Commit[]> {\n        return window.api.invoke(channel, RepositoryCommands.LOG);\n    }\n\n    static status(): Promise<StatusFile[]> {\n        return window.api.invoke(channel, RepositoryCommands.STATUS);\n    }\n}\n\nexport default Repository;"
  },
  {
    "path": "src/app/utilities/convertMetaToObject.ts",
    "content": "import { ProviderUpdateType } from 'main/providers/types';\n\ninterface CommitMetadata {\n    title: string;\n    provider?: string;\n    account?: string;\n    url?: string;\n    hostname?: string;\n    updateType?: ProviderUpdateType;\n}\n\n/**\n * Convert a full commit message description into a JS-compatible metadata\n * object that can easilby be accessed by the UI\n */\nfunction convertMetaToObject(message: string): CommitMetadata {\n    // Split between the title (first line) and the metadata (all other lines)\n    const [title, ...metaProperties] = message.split('\\n');\n\n    // Loop over each meta line and parse it into an object\n    const meta = metaProperties.reduce<CommitMetadata>((sum, line) => {\n        // First, split the line by the ': ' seperator, into the key and value\n        const [key, value] = line.split(': ');\n\n        // Then, switch on the key and then add the values to the sum object\n        // based on any hits\n        switch (key) {\n            case 'Aeon-Update-Type': {\n                // Access the type on the enum\n                const type = ProviderUpdateType[value as keyof typeof ProviderUpdateType];\n                // Only add the property if the type is solid\n                if (type) sum.updateType = type;\n                break;\n            }\n            case 'Aeon-Account':\n                sum.account = value;\n                break;\n            case 'Aeon-Provider':\n                sum.provider = value;\n                break;\n            case 'Aeon-URL':\n                sum.url = value;\n                break;\n            case 'Aeon-Hostname':\n                sum.hostname = value;\n                break;\n        }\n\n        return sum;\n    }, { title });\n\n    return meta;\n}\n\nexport default convertMetaToObject;"
  },
  {
    "path": "src/app/utilities/env.ts",
    "content": "// Retrieve environment sychronously from main\nconst env = window.api.env;\n\n// Destructure all individual options\nexport const {\n    appDataPath,\n    autoUpdates,\n    repositoryPath,\n    logPath,\n    storePath,\n    tour,\n} = env;\n\n// DEPRECATED: Support the legacy demoMode variable\nexport const demoMode = false;\n\nexport default env;"
  },
  {
    "path": "src/app/utilities/isValidUrl.ts",
    "content": "/**\n * Determine if the input is a valid URL\n * https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url\n * @param input \n */\nfunction isValidUrl(input: string): boolean {\n    try {\n        new URL(input);\n    } catch (_) {\n        return false;  \n    }\n\n    return true;\n}\n\nexport default isValidUrl;"
  },
  {
    "path": "src/app/utilities/usePrevious.ts",
    "content": "import { useRef, useEffect } from 'react';\n\n/**\n * This will return the previous value of an input supplied to the hook\n * @see https://usehooks.com/usePrevious/\n */\nfunction usePrevious<T>(value: T): T {\n    // The ref object is a generic container whose current property is mutable ...\n    // ... and can hold any value, similar to an instance property on a class\n    const ref = useRef(value);\n    // Store current value in ref\n    useEffect(() => {\n        ref.current = value;\n    }, [value]); // Only re-run if value changes\n    // Return previous value (happens before update in useEffect above)\n    return ref.current;\n}\n\nexport default usePrevious;"
  },
  {
    "path": "src/main/email-client/bridge.ts",
    "content": "import { ipcMain, IpcMainInvokeEvent } from 'electron';\nimport logger from 'main/lib/logger';\nimport WindowStore from 'main/lib/window-store';\nimport EmailManager from '.';\nimport { testImap } from './imap';\nimport { EmailCommands } from './types';\n\nconst channelName = 'email';\n\n/**\n * This class acts a bridge between the class that handles all email aspects,\n * and the app-side frontend.\n */\nclass EmailBridge {\n    manager: EmailManager;\n\n    constructor(manager: EmailManager) {\n        this.manager = manager;\n\n        // Start listening for commands on the set channel name\n        ipcMain.handle(channelName, this.handleMessage);\n\n        // Subscribe to manager-initated events\n        this.manager.addListener('*', function () {\n            // Log the event\n            logger.email.info(`New event: ${JSON.stringify(this.event)}`);\n\n            // And pass them on to the app\n            EmailBridge.send(this.event);\n        });\n    }\n\n    // eslint-disable-next-line\n    private handleMessage = (event: IpcMainInvokeEvent, command: number, ...args: any[]): Promise<any> => {\n        switch (command) {\n            case EmailCommands.GET_CLIENTS:\n                return Promise.resolve(this.manager.emailClients.keys());\n            case EmailCommands.GET_ACCOUNTS:\n                return Promise.resolve(Object.fromEntries(this.manager.initialisedEmailAddress));\n            case EmailCommands.ADD_ACCOUNT: {\n                const [key, ...rest] = args;\n                return this.manager.initialiseNewAddress(key, ...rest);\n            }\n            case EmailCommands.DELETE_ACCOUNT:\n                return Promise.resolve(this.manager.deleteAccount(args[0]));\n            case EmailCommands.TEST_IMAP:\n                return testImap(args[0], args[1], args[2], args[3], args[4]);\n        }\n    };\n\n    /**\n     * Send an event to the renderer\n     * @param event The event to send out\n     */\n    public static send(event: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, event);\n    }\n}\n\nexport default EmailBridge;"
  },
  {
    "path": "src/main/email-client/gmail/index.ts",
    "content": "import authenticateGmailUser, { refreshGmailTokens } from './oauth';\nimport { GmailTokenResponse } from './types';\nimport { Email, EmailQuery } from '../types';\nimport { simpleParser } from 'mailparser';\nimport Mail from 'nodemailer/lib/mailer';\nimport MailComposer from 'nodemailer/lib/mail-composer';\nimport { OauthEmailClient } from 'main/lib/oauth';\n\ninterface ListResponse {\n    messages: {\n        id: string;\n    }[]\n}\n\ninterface MessageResponse {\n    raw: string;\n}\n\nexport default class GmailEmailClient extends OauthEmailClient<GmailTokenResponse>  {\n    key = 'gmail';\n\n    async initialize(): Promise<string> {\n        // Retrieve the tokens\n        const tokens = await authenticateGmailUser();\n        \n        // Retrieve the email address\n        await this.storeTokens(tokens, false);\n        this.emailAddress = await this.getEmailAddress();\n\n        // Persist the tokens\n        await this.storeTokens(tokens);\n\n        return this.emailAddress;\n    }\n\n    async refreshTokens(expiredTokens: GmailTokenResponse): Promise<GmailTokenResponse> {\n        const tokens = await refreshGmailTokens(expiredTokens.refresh_token);\n\n        return tokens;\n    }\n\n    async findMessages(query: EmailQuery): Promise<Email[]> {\n        // First we'll need to convert the query object\n        const q = Object.keys(query).reduce((sum, key: keyof EmailQuery) => {\n            return `${sum} ${key}:${query[key]}`;\n        }, '');\n\n        // Then we'll retrieve all emails\n        const response = await this.get('https://www.googleapis.com/gmail/v1/users/me/messages?q=' + q) as ListResponse;\n        \n        // GUARD: Check if the response is proper\n        if (!response.messages) {\n            throw new Error('Invalid messages response: ' + JSON.stringify(response));\n        }\n        \n        // Then, we'll gather all the parsed email queries\n        const emails = response.messages.map((message) => {\n            return this.getMessage(message.id);\n        });\n\n        // And hand back the result\n        return Promise.all(emails);\n    }\n\n    /**\n     * Retrieve a single message\n     * @param messageId The message to be retrieved\n     */\n    async getMessage(messageId: string): Promise<Email> {\n        // Read the response\n        const rawResponse = await this.get('https://www.googleapis.com/gmail/v1/users/me/messages/' + messageId + '?format=RAW') as MessageResponse;\n        \n        // GUARD: Check for valid response\n        if (!rawResponse.raw) {\n            throw new Error('Invalid message response: ' + JSON.stringify(rawResponse));\n        }\n\n        // Convert and return\n        const response = Buffer.from(rawResponse.raw, 'base64').toString();\n        return simpleParser(response);\n    }\n\n    async sendMessage(options: Mail.Options): Promise<void> {\n        // Compile the supplied email object\n        const mail = await new MailComposer(options).compile().build();\n\n        // Setup the right parameters for chucking it to google\n        const params = {\n            method: 'POST',\n            body: JSON.stringify({\n                raw: mail.toString('base64'),\n            }),\n        };\n\n        await this.get('https://gmail.googleapis.com/upload/gmail/v1/users/me/messages/send', params); \n    }\n\n    /**\n     * Retrieve the email address for the current user\n     */\n    async getEmailAddress(): Promise<string> {\n        return this.get('https://gmail.googleapis.com/gmail/v1/users/me/profile')\n            .then((data: ({ emailAddress: string })) => data.emailAddress);\n    }\n}"
  },
  {
    "path": "src/main/email-client/gmail/oauth.ts",
    "content": "import http from 'http';\nimport fetch from 'node-fetch';\nimport { URLSearchParams } from 'url';\nimport { Socket } from 'net';\nimport { shell } from 'electron';\nimport { GmailTokenResponse } from './types';\nimport logger from 'main/lib/logger';\nimport { generateVerifier, objectToUrlParams } from 'main/lib/oauth';\n\n// Pull the Gmail config variables from the environment\nconst GMAIL_OAUTH_CLIENT_ID = process.env.GMAIL_OAUTH_CLIENT_ID;\nconst GMAIL_OAUTH_CLIENT_SECRET = process.env.GMAIL_OAUTH_CLIENT_SECRET;\n\ninterface CodeAndRedirectUri {\n    code: string;\n    redirect_uri: string;\n}\n\n/**\n * Exchange a received authorization code for a full blown access token\n * @param response \n * @param verifier \n */\nasync function exchangeAccessCode(response: CodeAndRedirectUri, verifier: string): Promise<GmailTokenResponse> {\n    // Gather form parameters\n    const formData = new URLSearchParams();\n    formData.append('code', response.code);\n    formData.append('client_id', GMAIL_OAUTH_CLIENT_ID);\n    formData.append('client_secret', GMAIL_OAUTH_CLIENT_SECRET);\n    formData.append('code_verifier', verifier);\n    formData.append('grant_type', 'authorization_code');\n    formData.append('redirect_uri', response.redirect_uri);\n\n    // Send out request\n    return fetch('https://oauth2.googleapis.com/token', {\n        method: 'POST',\n        body: formData,\n    }).then((res) => res.json() as Promise<GmailTokenResponse>);\n}\n\ntype CodeCallback = (code: CodeAndRedirectUri) => void;\n\n/**\n * We authenticate using the local loopback method. This requires that we start\n * a local webserver in order to catch the redirect.\n */\nasync function setupRedirectListener(callback: CodeCallback): Promise<string> {\n    return new Promise((resolve, reject) => {\n        // Store the redirect_uri for use\n        let redirectUri: string = null;\n\n        // Store any sockets so that we can forcibly delete them\n        const sockets = new Set<Socket>();\n\n        // Create basic server that logs incoming requests\n        const server = http.createServer((req, res) => {\n            // Write reponse\n            res.writeHead(200);\n            res.end('Your account has been successfully connected. You can close this window and return to Aeon.');\n\n            // Parse parameters\n            const params = new URL(`http://localhost${req.url}`).searchParams;\n\n            // GUARD: The response must have a code and scope parameter\n            if (params.has('code') && params.has('scope')) {\n                // First, we'll shutdown the server, by closing it and forcibly\n                // destroying all sockets.\n                server.close();\n                for (const socket of sockets) {\n                    socket.destroy();\n                }\n                logger.email.info(`Received authentication code, ${redirectUri} destroyed.`);\n\n                // Lastly, we'll pass along the code and redirect uri to the\n                // callback handler\n                callback({\n                    code: params.get('code'),\n                    redirect_uri: redirectUri,\n                });\n            }\n        });\n\n        // Assign listener that returns server object when ready\n        server.on('listening', () => {\n            // Retrieve the server port\n            const address = server.address();\n\n            // Double-check that the address is TCP\n            if (typeof address === 'string') {\n                throw new Error('Could not retrieve port from HTTP server...');\n            }\n\n            // Assign the redirect_uri for later use\n            redirectUri = new URL(`http://127.0.0.1:${address.port}`).toString();\n            \n            // Then resolve it\n            resolve(redirectUri);\n\n            // Also log it\n            logger.email.info('A gmail authentication server is listening at: ' + redirectUri);\n        });\n\n        // Store any sockets, so that we can delete them when the server is done\n        server.on('connection', (socket) => {\n            sockets.add(socket);\n\n            // Also auto-remove them when the socket gets closed\n            socket.on('close', () => {\n                sockets.delete(socket);\n            });\n        });\n\n        server.on('error', reject);\n\n        // Start listening on a random OS-assigned port\n        server.listen(0);\n    });\n}\n\nexport function refreshGmailTokens(refresh_token: string): Promise<GmailTokenResponse> {\n    // Gather form parameters\n    const formData = new URLSearchParams();\n    formData.append('refresh_token', refresh_token);\n    formData.append('client_id', GMAIL_OAUTH_CLIENT_ID);\n    formData.append('client_secret', GMAIL_OAUTH_CLIENT_SECRET);\n    formData.append('grant_type', 'refresh_token');\n\n    // Send out request\n    return fetch('https://oauth2.googleapis.com/token', {\n        method: 'POST',\n        body: formData,\n    }).then((response) => response.json() as Promise<GmailTokenResponse>)\n        .then((response) => ({\n            ...response,\n            refresh_token,\n        }));\n}\n\nasync function retrieveAuthenticationCode(verifier: string): Promise<CodeAndRedirectUri> {\n    if (!GMAIL_OAUTH_CLIENT_ID || !GMAIL_OAUTH_CLIENT_SECRET) {\n        throw new Error('GMAIL_OAUTH_CLIENT_ID and/or GMAIL_OAUTH_CLIENT_SECRET wasn\\'t set in the environment');\n    }\n\n    // eslint-disable-next-line\n    return new Promise(async (resolve) => {\n        // Create server and retrieve redirect URI\n        const redirectUri = await setupRedirectListener(resolve);\n\n        // POST parameters for the redirect URI request\n        const data: Record<string, string> = {\n            scope: 'https://mail.google.com/',\n            response_type: 'code',\n            code_challenge: verifier,\n            redirect_uri: redirectUri,\n            client_id: GMAIL_OAUTH_CLIENT_ID,\n        };\n        const params = objectToUrlParams(data);\n        const uri = new URL('https://accounts.google.com/o/oauth2/v2/auth' + params);\n\n        // Then open the generated URL in the OS-default browser\n        shell.openExternal(uri.href);\n    });\n}\n\n/**\n * Starts the authentication workflow for a Gmail-based email API\n */\nexport default async function authenticateGmailUser(): Promise<GmailTokenResponse> {\n    const verifier = generateVerifier();\n    const response = await retrieveAuthenticationCode(verifier);\n    const tokens = await exchangeAccessCode(response, verifier);\n    return tokens;\n}"
  },
  {
    "path": "src/main/email-client/gmail/types.ts",
    "content": "export interface GmailTokenResponse {\n    access_token: string;\n    expires_in: number;\n    refresh_token: string;\n    scope: string;\n    token_type: string;\n}"
  },
  {
    "path": "src/main/email-client/imap/index.ts",
    "content": "import { ImapFlow } from 'imapflow';\nimport { ParsedMail, simpleParser } from 'mailparser';\nimport logger from 'main/lib/logger';\nimport { KeyStore } from 'main/store';\nimport { EmailClient, EmailQuery } from '../types';\n\nexport interface ImapCredentials {\n    user: string;\n    pass: string;\n    host: string;\n    port: number;\n    secure: boolean;\n}\n\n/**\n * Attempt to connect to an IMAP server with a bunch of credentials in order to\n * ascertain whether all the settings are right.\n */\nexport async function testImap(\n    user: string,\n    pass: string,\n    host: string,\n    port: number,\n    secure = true,\n): Promise<boolean> {\n    const client = new ImapFlow({\n        host, \n        port, \n        auth: {\n            user,\n            pass,\n        },\n        secure,\n        logger: logger.email,\n    });\n\n    try {\n        // Attempt to connect\n        await client.connect();\n    } catch {\n        // If an error is thrown, return false\n        return false;\n    }\n\n    // If nothing gets thrown, disconnect the client.\n    await client.close();\n\n    return true;\n}\n\nexport class ImapClient extends EmailClient {\n    client: ImapFlow | null;\n\n    key = 'imap';\n\n    async initialize(): Promise<string> {\n        throw new Error('The IMAP email client should be initialized using the initializeImap function');\n    }\n\n    async initializeImap(\n        user: string,\n        pass: string,\n        host: string,\n        port: number,\n        secure = true,\n    ): Promise<string> {\n        // Initialize the client\n        this.client = new ImapFlow({\n            host, \n            port, \n            auth: {\n                user,\n                pass,\n            },\n            secure,\n            logger: logger.email,\n        });\n\n        // Attempt to connect in order to ensure the credentials are correct\n        await this.client.connect();        \n\n        // First set the email address\n        this.emailAddress = user;\n        \n        // Persist the credentials\n        await KeyStore.set(`${this.key}_${this.emailAddress}`, JSON.stringify({ user, pass, host, port, secure }));\n        this.isInitialized = true;\n\n        // Return the emailaddress to the manager\n        return user;\n    }\n\n    async delete(): Promise<void> {\n        // Delete the credentials from the store, and that's that\n        await KeyStore.delete(`${this.key}_${this.emailAddress}`);\n    }\n\n    async findMessages(query?: EmailQuery): Promise<ParsedMail[]> {\n        // GUARD: Check whether the client is already created and connected \n        if (!this.isInitialized) {\n            // Retrieve the raw JSON string from the keystore and parse it\n            const rawCredentials = await KeyStore.get(`${this.key}_${this.emailAddress}`);\n            const { host, port, user, pass, secure } = JSON.parse(rawCredentials) as ImapCredentials;\n\n            // Initialize the client\n            this.client = new ImapFlow({\n                host, \n                port, \n                auth: {\n                    user,\n                    pass,\n                },\n                secure,\n                logger: logger.email,\n            });\n\n            // Attempt to connect in order to ensure the credentials are correct\n            await this.client.connect();   \n            this.isInitialized = true;\n        }\n\n        // Then try and search for messages\n        const generator = await this.client.fetch({\n            to: query.to,\n            from: query.from,\n            subject: query.subject,\n            emailId: query.messageId,\n            since: query.after && new Date(query.after * 1000),\n        }, {\n            uid: true,\n            source: true,\n            headers: true,\n        });\n        \n        // Loop through every available message and parse the body using mailparser\n        const messages: ParsedMail[] = [];\n        for await (const message of generator) {\n            const parsedMessage = await simpleParser(message.source);\n            messages.push(parsedMessage);\n        }\n\n        return messages;\n    }\n}\n"
  },
  {
    "path": "src/main/email-client/index.ts",
    "content": "import { EventEmitter2 } from 'eventemitter2';\nimport logger from 'main/lib/logger';\nimport PersistedMap from 'main/lib/persisted-map';\nimport store from 'main/store';\nimport GmailEmailClient from './gmail';\nimport { ImapClient } from './imap';\nimport OutlookEmailClient from './outlook';\nimport { EmailClient, EmailEvents } from './types';\n\nconst clients: Map<string, { new(email?: string): EmailClient }> = new Map();\nclients.set('gmail', GmailEmailClient);\nclients.set('outlook', OutlookEmailClient);\nclients.set('imap', ImapClient);\n\nexport default class EmailManager extends EventEmitter2 {\n    // A set of email addresses that maps to the client that is handling the\n    // particular email addresses\n    initialisedEmailAddress: PersistedMap<string, string>;\n\n    // A set of the initialised emailaddressses linked to the particular\n    // initialised email client\n    emailClients: Map<string, EmailClient> = new Map();\n\n    constructor() {\n        super({ wildcard: true });\n\n        // Retrieve the initialised emailaddresses that have been stored in the store\n        const addresses = store.get('initialised-email-addresses', []) as [string, string][];\n        this.initialisedEmailAddress = new PersistedMap(addresses, (map) => {\n            store.set('initialised-email-addresses', Array.from(map));\n        });\n\n        // Then start up all the clients again with the right emailaddresses\n        for (const [address, clientKey] of this.initialisedEmailAddress) {\n            const Client = clients.get(clientKey);\n            this.emailClients.set(address, new Client(address));\n        }\n    }\n\n    /**\n     * This call creates a new email client instance, which is then free to be\n     * assigned to an emailadress entered as part of its initialisation logic.\n     */\n    async initialiseNewAddress(clientKey: string, ...args: unknown[]): Promise<string> {\n        logger.email.info('Initialising new email client: ' + clientKey);\n        \n        // Retrieve the correct client\n        const Client = clients.get(clientKey);\n\n        // GUARD: Check if retrieved client exists\n        if (!Client) {\n            throw new Error('Could not find email client with name ' + clientKey);\n        }\n\n        // Then, initialize the new client\n        const client = new Client();\n        try {    \n            // We initialize imap clients somewhat differently, so we need to split\n            // it out\n            const emailAddress = clientKey === 'imap'\n                ? await(client as ImapClient).initializeImap(\n                    args[0] as string,\n                    args[1] as string,\n                    args[2] as string,\n                    args[3] as number,\n                    args[4] as boolean | undefined,\n                )\n                : await client.initialize();\n\n            this.initialisedEmailAddress.set(emailAddress, clientKey);\n            this.emailClients.set(emailAddress, client);\n    \n            // Send out event\n            this.emit(EmailEvents.NEW_ACCOUNT);\n    \n            // Return the emailaddress\n            return emailAddress;\n        } catch (e) {\n            logger.email.error({ message: e });\n            throw e;\n        }\n    }\n\n    /**\n     * This call instructs a particularl instantiated email client to\n     * self-destruct, after which all the remaining pieces are deleted as well.\n     * @param address The email address to be deleted\n     */\n    deleteAccount(address: string): void {\n        logger.email.info('Deleting email account: ' + address);\n\n        // GUARD: Check if the address actually exists\n        if (!this.emailClients.has(address)) {\n            throw new Error('Email account not found');\n        }\n\n        // Delete the account\n        this.emailClients.get(address).delete();\n        this.emailClients.delete(address);\n        this.initialisedEmailAddress.delete(address);\n\n        // Send out event\n        this.emit(EmailEvents.ACCOUNT_DELETED);\n    }\n}"
  },
  {
    "path": "src/main/email-client/outlook/index.ts",
    "content": "import { formatISO } from 'date-fns';\nimport { ParsedMail, simpleParser } from 'mailparser';\nimport { OauthEmailClient } from 'main/lib/oauth';\nimport { EmailQuery } from '../types';\nimport authenticateOutlookUser, { OutlookTokenResponse, refreshOutlookTokens } from './oauth';\n\ninterface ProfileResponse {\n    '@odata.context': string;\n    businessPhones: string[];\n    displayName: string;\n    givenName: string;\n    jobTitle: string;\n    mail: string;\n    mobilePhone?: string;\n    officeLocation: string;\n    preferredLanguage: string;\n    surname: string;\n    userPrincipalName: string;\n    id: string;\n}\n\nexport default class OutlookEmailClient extends OauthEmailClient<OutlookTokenResponse> {\n    key = 'outlook';\n\n    async initialize(): Promise<string> {\n        // Retrieve access token from user\n        const tokens = await authenticateOutlookUser();\n        \n        // Retrieve and store email address\n        await this.storeTokens(tokens, false);\n        const email = await this.getEmailAddress();\n        this.emailAddress = email;\n        \n        // Persist the tokens\n        await this.storeTokens(tokens);\n\n        return email;\n    }\n\n    async getEmailAddress(): Promise<string> {\n        const response = await this.get('https://graph.microsoft.com/v1.0/me') as ProfileResponse;\n        return response.userPrincipalName;\n    }\n\n    async findMessages(query?: EmailQuery): Promise<ParsedMail[]> {\n        // Setup an array storing all potential filters\n        const filters: string[] = [];\n\n        // We then make quite the ugly long if-statement converting from the\n        // Gmail-format, to the weird filter-format handled by Microsoft Graph\n        \n        if (query.from) {\n            filters.push(`from/emailAddress/address eq '${query.from}'`);\n        }\n\n        if (query.to) {\n            // This filter is not supported in Microsoft Graph, so we cannot\n            // implement it\n        }\n\n        if (query.subject) {\n            filters.push(`contains(subject, '${query.subject}')`);\n        }\n\n        if (query.after) {\n            const date = new Date(query.after * 1000);\n            filters.push(`receivedDateTime ge ${formatISO(date, { representation: 'date' })}`);\n        }\n\n        // Lastly, we convert the filters to a single string using or-chains\n        const uri = 'https://graph.microsoft.com/v1.0/me/messages?$select=id&$filter=' + filters.join(' or ');\n\n        // We then send out the request\n        const { value: messages } = await this.get(uri) as { value: { id: string }[] };\n\n        return Promise.all(\n            messages\n                .map((m) => m.id)\n                .map(this.getMessage),\n        );\n    }\n\n    /**\n     * Retrieve a single message from Graph\n     */\n    async getMessage(id: string): Promise<ParsedMail> {\n        const uri = `https://graph.microsoft.com/v1.0/me/messages/${id}/$value`;\n        const body = await this.get(uri, null, 'text') as string;\n        return simpleParser(body);\n    }\n\n    async refreshTokens(expiredTokens: OutlookTokenResponse): Promise<OutlookTokenResponse> {\n        return refreshOutlookTokens(expiredTokens);\n    }\n}"
  },
  {
    "path": "src/main/email-client/outlook/oauth.ts",
    "content": "import { withSecureWindow } from 'main/lib/create-secure-window';\nimport crypto from 'crypto';\nimport { generateVerifier, objectToUrlParams } from 'main/lib/oauth';\nimport fetch from 'node-fetch';\n\n// Pull the Outlook config variables from the environment\nconst OUTLOOK_OAUTH_CLIENT_ID = process.env.OUTLOOK_OAUTH_CLIENT_ID;\nconst OUTLOOK_OAUTH_CLIENT_SECRET = process.env.OUTLOOK_OAUTH_CLIENT_SECRET;\n\n// OAuth constants\nconst REDIRECT_URI = 'https://login.microsoftonline.com/common/oauth2/nativeclient';\nconst SCOPES = [\n    'User.Read',\n    'Mail.Read',\n];\n\nexport interface OutlookTokenResponse {\n    access_token: string;\n    token_type: string;\n    expires_in: number;\n    scope: string;\n    refresh_token: string,\n    id_token: string;\n    windowKey: string;\n}\n\ninterface TokenError {\n    error: string;\n    error_description: string;\n    error_codes: number[];\n    timestamp: string;\n    trace_id: string;\n    correlation_id: string;\n}\n\n/**\n * Attemps to authenticate an Outlook user\n */\nexport default async function authenticateOutlookUser(): Promise<OutlookTokenResponse> {\n    // GUARD: Check that the env variables were set\n    if (!OUTLOOK_OAUTH_CLIENT_ID || !OUTLOOK_OAUTH_CLIENT_SECRET) {\n        throw new Error('OUTLOOK_OAUTH_CLIENT_ID and/or OUTLOOK_OAUTH_CLIENT_SECRET wasn\\'t set in the environment');\n    }\n    \n    // Prepare the oauth auth params\n    const verifier = generateVerifier();\n    const authSettings = {\n        client_id: OUTLOOK_OAUTH_CLIENT_ID,\n        response_type: 'code',\n        redirect_uri: REDIRECT_URI,\n        scope: SCOPES.join(','),\n        response_mode: 'query',\n        code_challenge: verifier,\n        code_challenge_method: 'plain',\n    };\n\n    // Convert the params to a full URI\n    const authParams = objectToUrlParams(authSettings);\n    const authUri = new URL('https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize' + authParams);\n    \n    // Spawn window pointing the user to the Microsoft Graph OAuth flow\n    const windowKey = crypto.randomBytes(32).toString('hex');\n    const authToken = await withSecureWindow<string>({\n        key: windowKey,\n        origin: ['microsoftonline.com', 'live.com'],\n    }, (window) => {\n        return new Promise((resolve, reject) => {\n            // Load the URL for the user\n            window.loadURL(authUri.href);\n            window.show();\n    \n            // Check for navigation towards the redirect URI\n            window.webContents.on('did-navigate', () => {\n                const responseUri = window.webContents.getURL();\n                if (!responseUri.startsWith(REDIRECT_URI)) {\n                    return;\n                }\n\n                // Parse the url and extract the token\n                const tokenParams = new URL(responseUri).searchParams;\n\n                // GUARD: Check for errors\n                if (tokenParams.has('error')) {\n                    reject(`Outlook OAuth request failed with error code '${tokenParams.get('error')}' (${tokenParams.get('error_description')})`);\n                    return;\n                }\n\n                // GUARD: Check the token is there\n                if (!tokenParams.has('code')) {\n                    reject('No token could be found in OAuth response for Outlook');\n                    return;\n                }\n\n                resolve(tokenParams.get('code'));\n            });\n        });\n    });\n\n    // Then exchange the token for an access token\n    const tokenParams = new URLSearchParams();\n    tokenParams.append('client_id', OUTLOOK_OAUTH_CLIENT_ID);\n    tokenParams.append('scope', SCOPES.join(','));\n    tokenParams.append('code', authToken);\n    tokenParams.append('redirect_uri', REDIRECT_URI);\n    tokenParams.append('code_verifier', verifier);\n    tokenParams.append('grant_type', 'authorization_code');\n    \n    // Send off the request to the API\n    const tokenUri = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';\n    const tokenResponse = await fetch(tokenUri, { \n        method: 'POST',\n        body: tokenParams,\n    }).then((r) => r.json()) as OutlookTokenResponse | TokenError;\n\n    // GUARD: Check for errors in exchanging the token\n    if ('error' in tokenResponse) {\n        throw new Error(`Failed to exchange auth token for access token in Outlook email client. Error code: '${tokenResponse.error}' (${tokenResponse.error_description})`);\n    } else {\n        // Append the windowKey to the tokenResponse\n        tokenResponse.windowKey = windowKey;\n\n        return tokenResponse;\n    }\n}\n\n/**\n * Exchange a previously received refresh_token for a new access_token\n */\nexport async function refreshOutlookTokens({ refresh_token, windowKey }: OutlookTokenResponse) {\n    const refreshParams = new URLSearchParams();\n    refreshParams.append('client_id', OUTLOOK_OAUTH_CLIENT_ID);\n    refreshParams.append('grant_type', refresh_token);\n    refreshParams.append('scope', SCOPES.join(','));\n    refreshParams.append('refresh_token', refresh_token);\n    const refreshUri = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';\n\n    // Send off the request to the API\n    const tokenResponse = await fetch(refreshUri, {\n        method: 'POST',\n        body: refreshParams,\n    }).then((r) => r.json()) as OutlookTokenResponse | TokenError;\n\n    // GUARD: Check for errors in exchanging the token\n    if ('error' in tokenResponse) {\n        throw new Error(`Failed to exchange auth token for access token in Outlook email client. Error code: '${tokenResponse.error}' (${tokenResponse.error_description})`);\n    } else {\n        // Append the windowKey to the tokenResponse\n        tokenResponse.windowKey = windowKey;\n\n        return tokenResponse;\n    }\n}"
  },
  {
    "path": "src/main/email-client/types.ts",
    "content": "import type { ParsedMail } from 'mailparser';\nimport type { Options } from 'nodemailer/lib/mailer';\n\nexport interface EmailQuery {\n    from?: string;\n    to?: string;\n    subject?: string;\n    messageId?: string;\n    /** UNIX time in seconds after which the message was sent */\n    after?: number;\n}\n\nexport type Email = ParsedMail;\n\nexport abstract class EmailClient {\n    /**\n     * Whether the current client has been initialized or not\n     */\n    protected isInitialized: boolean;\n\n    /**\n     * The emailaddress that this particular client resolves to. Should not be\n     * set if the client has not been initialised yet.\n     */\n    protected emailAddress?: string;\n\n    /**\n     * A unique identifier for this type of emailclient. This should be pathname\n     * safe, i.e. all lowercase, no spaces.\n     */\n    protected key: string;\n\n    /**\n     * Initialize a new email client. Prepare it for use through either login\n     * screens, consent, or whatever. Only called when first setting up the\n     * email client. This function returns the emailaddress that has just been\n     * successfully initialised.\n     */\n    abstract initialize(): Promise<string>;\n\n    /**\n     * Remove a previously registered and initialized account completely.\n     */\n    abstract delete(): Promise<void> | void;\n    \n    /**\n     * Retrieve a set of messages using the query object\n     * @param query EmailQuery\n     */\n    abstract findMessages(query?: EmailQuery): Promise<Email[]>;\n\n\n    constructor(emailAddress: string | null) {\n        this.emailAddress = emailAddress;\n    }\n}\n\nexport interface EmailClient {\n    /**\n     * Send an email with the client, using the specified options\n     * @param options Mail.Options\n     */\n    sendMessage?(options: Options): Promise<void>;\n}\n\nexport enum EmailCommands {\n    ADD_ACCOUNT,\n    DELETE_ACCOUNT,\n    GET_ACCOUNTS,\n    GET_CLIENTS,\n    TEST_IMAP,\n}\n\nexport enum EmailEvents {\n    NEW_ACCOUNT = 'new-account',\n    ACCOUNT_DELETED = 'account-deleted',\n}"
  },
  {
    "path": "src/main/index.ts",
    "content": "import 'v8-compile-cache';\n// eslint-disable-next-line\nrequire('source-map-support').install();\n\nimport './lib/map-map';\nimport './updates';\nimport { app, BrowserWindow } from 'electron';\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer';\nimport initialise from './initialise';\nimport WindowStore from './lib/window-store';\n\ndeclare const MAIN_WINDOW_WEBPACK_ENTRY: string;\ndeclare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;\n\nlet mainWindow: BrowserWindow;\n\n// Handle creating/removing shortcuts on Windows when installing/uninstalling.\nif (require('electron-squirrel-startup')) { // eslint-disable-line global-require\n    app.quit();\n}\n\nconst createWindow = (): void => {\n    // Create the browser window.\n    mainWindow = new BrowserWindow({\n        height: 800,\n        width: 1000,\n        minWidth: 600,\n        minHeight: 600,\n        titleBarStyle: 'hiddenInset',\n        vibrancy: 'menu',\n        transparent: true,\n        webPreferences: {\n            preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,\n            webSecurity: process.env.NODE_ENV === 'production',\n            sandbox: false,\n        },\n    });\n    \n    // Hide menu bar on windows\n    if (process.env.NODE_ENV === 'production') {\n        mainWindow.setMenu(null);\n    } else {\n        // Install devtools when in development mode\n        app.whenReady()\n            .then(() => Promise.all([\n                installExtension(REDUX_DEVTOOLS),\n                installExtension(REACT_DEVELOPER_TOOLS),\n            ]));\n    }\n    \n    // and load the index.html of the app.\n    mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);\n    \n    // save the window to a singleton so that we can access it later\n    WindowStore.getInstance().window = mainWindow;\n    \n    // Open the DevTools.\n    // mainWindow.webContents.openDevTools();\n};\n\n// This method will be called when Electron has finished\n// initialization and is ready to create browser windows.\n// Some APIs can only be used after this event occurs.\napp.on('ready', (): void =>  { \n    createWindow();\n    initialise();\n});\n\n// Quit when all windows are closed.\napp.on('window-all-closed', () => {\n    // On OS X it is common for applications and their menu bar\n    // to stay active until the user quits explicitly with Cmd + Q\n    if (process.platform !== 'darwin') {\n        app.quit();\n    }\n});\n\napp.on('activate', () => {\n    // On OS X it's common to re-create a window in the app when the\n    // dock icon is clicked and there are no other windows open.\n    if (BrowserWindow.getAllWindows().length === 0) {\n        createWindow();\n    }\n});\n\n// Windows can only handle single instance when processing deep links. Hence, we\n// only allow a single instance to be open at any time.\nconst gotTheLock = app.requestSingleInstanceLock();\n\n// GUARD: Check if we're the only instance of the app running\nif (!gotTheLock) {\n    app.quit();\n} else {\n    app.on('second-instance', () => {\n        // Someone tried to run a second instance, we should focus our window.\n        if (mainWindow) {\n            if (mainWindow.isMinimized()) mainWindow.restore();\n            mainWindow.focus();\n        }\n    });\n}"
  },
  {
    "path": "src/main/initialise.ts",
    "content": "import { autoUpdater } from 'electron';\nimport EmailManager from './email-client';\nimport EmailBridge from './email-client/bridge';\nimport { autoUpdates } from './lib/constants';\nimport logger from './lib/logger';\nimport { setupProtocolHandlers } from './lib/protocol-handler';\nimport Repository from './lib/repository';\nimport RepositoryBridge from './lib/repository/bridge';\nimport ProviderManager from './providers';\nimport ProviderBridge from './providers/bridge';\n\nfunction initialise(): void {\n    // Register the protocol handlers\n    setupProtocolHandlers();\n\n    // Initialise the Git repository handler\n    const repository = new Repository();\n    // Inject the repository handler into the bridge for communication with the rendered\n    new RepositoryBridge(repository);\n\n    // Also set up the email manager and bridge\n    const email = new EmailManager();\n    new EmailBridge(email);\n\n    // Add an event listener for the repository being ready, so that we don't\n    // start any Git action before it is ready.\n    repository.addListener('ready', () => {\n        // Also initialise the Provider Manager\n        const providerManager = new ProviderManager(repository, email);\n        // And also inject this into its respective bridge\n        new ProviderBridge(providerManager);\n    });\n\n    // Check for updates when everything is sort of set.\n    // NOTE: This call may fail for a number of reasons (e.g. when the --no-auto-updates \n    // flag is set.). Thus we swallow the error and report it out.\n    if (autoUpdates) {\n        try {\n            autoUpdater.checkForUpdates();\n        } catch (e) {\n            logger.autoUpdater.error(e);\n        }\n    }\n}\n\nexport default initialise;"
  },
  {
    "path": "src/main/lib/app-path.ts",
    "content": "import { app } from 'electron';\n\n// This is the path our files should be stored under\nexport const APP_DATA_PATH = process.env.NODE_ENV === 'production' ? app.getPath('userData') : app.getAppPath();\n"
  },
  {
    "path": "src/main/lib/constants.ts",
    "content": "import path from 'path';\nimport yargs from 'yargs';\nimport { app, ipcMain } from 'electron';\nimport { hideBin } from 'yargs/helpers';\n\n// Determine the default appData path\nconst defaultAppDataPath = process.env.NODE_ENV === 'production' \n    // Default to the default userData directory for production usage\n    ? app.getPath('userData')\n    // For dev purpose, store it in the same directory\n    : path.join(process.cwd(), 'data');\n\nexport interface CommandLineArguments {\n    /** The path where all data is saved */\n    appDataPath: string;\n    /** Whether the application should perform any automatic updates */\n    autoUpdates: boolean;\n    /** The path where the repository should be saved */\n    repositoryPath: string;\n    /** The path where the logs should be saved */\n    logPath: string;\n    /* The path where the store should be saved */\n    storePath: string;\n    /* Whether the product-tour should be enabled or not */\n    tour: boolean;\n}\n\n// Set defaults for the command-line arguments\n// NOTE: We don't use the yargs default options because it applies conflict\n// after resolving the defaults, rather than before.\n// NOTE: Defaults are applied in a particular order. Firstly, anything that is set via\n// the CLI arguments takes precendence over any defaults. Secondly, if\n// appDataPath is set and none of the dependent variables are set, appDataPath\n// from the CLI takes precendence over the default appDataPath.\nfunction setDefaults(cliArgs: Partial<CommandLineArguments>): CommandLineArguments {\n    return {\n        appDataPath: defaultAppDataPath,\n        autoUpdates: true,\n        repositoryPath: path.join(cliArgs.appDataPath || defaultAppDataPath, 'repository'),\n        logPath: path.join(cliArgs.appDataPath || defaultAppDataPath, 'logs'),\n        storePath: path.join(cliArgs.appDataPath || defaultAppDataPath, 'store'),\n        tour: false,\n        ...cliArgs,\n    };\n}\n\n// Parse all the command line arguments using a set schema, using yargs\nconst cliArguments = yargs(hideBin(process.argv))\n    .option('appDataPath', {\n        desc: 'Specify the location where all data is saved',\n        type: 'string',\n    })\n    .option('autoUpdates', {\n        desc: 'Indicate whether the application should perform any automatic updates',\n        type: 'boolean',\n    })\n    .option('repositoryPath', {\n        desc: 'Specify the location where the repository should be saved',\n        type: 'string',\n        conflicts: ['appDataPath'],\n    })\n    .option('logPath', {\n        desc: 'Specify the location where the logs should be saved',\n        type: 'string',\n        conflicts: ['appDataPath'],\n    })\n    .option('storePath', {\n        desc: 'Specify the location where the logs should be saved',\n        type: 'string',\n        conflicts: ['appDataPath'],\n    })\n    .option('tour', {\n        desc: 'Indicate whether the applications should include a tour highlighting available features',\n        type: 'boolean',\n    })\n    .parserConfiguration({\n        'camel-case-expansion': true,\n        'boolean-negation': true,\n    })\n    .help()\n    .parseSync() as CommandLineArguments;\n\n// Assign the defaults\nconst constants = setDefaults(cliArguments);\n\n// Destructure all arguments for easy import\nexport const {\n    appDataPath,\n    autoUpdates,\n    repositoryPath,\n    logPath,\n    storePath,\n    tour,\n} = constants;\n\n// Register handler for retrieving constants on app\nipcMain.on('env', (event) => { event.returnValue = constants; });\n    \nexport default constants;\n"
  },
  {
    "path": "src/main/lib/create-secure-window.ts",
    "content": "import { BrowserWindow } from 'electron';\nimport { URL } from 'url';\nimport crypto from 'crypto';\nimport logger from './logger';\n\nexport interface SecureWindowParameters {\n    key?: string;\n    /** The origin to which the BrowserWindow should be limited. If the page\n     * navigates to another origin, the request is denied */\n    origin: string | string[];\n    /** An optional options object that should be passed to the\n     * BrowserWindow constructor. This may not contain webPreferences */\n    options?: Electron.BrowserWindowConstructorOptions;\n}\n\n/**\n * This function will create a secure BrowserWindow that implements Electron\n * best practices for loading remote content. See https://www.electronjs.org/docs/tutorial/security\n */\nfunction createSecureWindow(params: SecureWindowParameters): BrowserWindow {\n    const { key, origin, options = {} } = params;\n\n    // GUARD: webPreferences are off-limits\n    if (Object.prototype.hasOwnProperty.call(options, 'webPreferences')) {\n        throw new Error('InvalidBrowserWindowConfiguration');\n    }\n\n    const persistKey = key || crypto.randomBytes(64).toString('base64');\n\n    // Initialise the window\n    const window = new BrowserWindow({ \n        width: 800, \n        height: 600, \n        alwaysOnTop: true, \n        show: false, \n        webPreferences: {\n            sandbox: true,\n            contextIsolation: true,\n            partition: `persist:${persistKey}`,\n        },\n        ...options,\n    });\n\n    // Disable menu bar in windows and linux\n    window.setMenu(null);\n\n    // Register the aeon:// protocol\n    window.webContents.session.protocol.registerHttpProtocol('aeon', (request, callback) => {\n        callback({ data: 'OK' }); \n    });\n\n    // Deny any request for extra permissions in this handler\n    window.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => callback(false));\n    \n    // Create a generic navigation handler so we can assign it to both will-navigate and will-redirect\n    const navigationHandler = (event: Electron.Event, navigationUrl: string): void => {\n        const parsedUrl = new URL(navigationUrl); \n\n        // Loop through all origins and see if there is a match\n        const matchesOrigin = Array.isArray(origin)\n            ? !!origin.find((o) => parsedUrl.origin.endsWith(o))\n            : parsedUrl.origin.endsWith(origin);\n\n        // GUARD: Kill the request if it exceeds the origin\n        if (!matchesOrigin && parsedUrl.protocol !== 'aeon:') {\n            logger.provider.error(`A request for ${navigationUrl} was blocked because it did not match the predefined domain (${origin}, read ${parsedUrl.origin})`);\n            event.preventDefault();\n        }\n    };\n\n    // Restrict navigation to a particular origin\n    window.webContents.on('will-navigate', navigationHandler);\n    window.webContents.on('will-redirect', navigationHandler);\n\n    // Log navigated URLs\n    window.webContents.on('did-navigate', (event, url) => {\n        logger.provider.info(`Secure window navigating to ${url}`);\n    });\n\n    // Open the devtools on development builds\n    if (process.env.NODE_ENV !== 'production') {\n        window.webContents.openDevTools();\n    }\n    \n    return window;\n} \n\n/**\n * This is a HOC wrapper function that transparently makes available a window\n * function, while accepting a return Promise. It adds another check to see if\n * the Window is closed by the user, so that the aborted function can be\n * transparently passed back to the caller.\n * @param fn The function that needs the window object\n */\nexport function withSecureWindow<U>(\n    params: SecureWindowParameters,\n    fn: (window: BrowserWindow) => Promise<U>,\n): Promise<U> {\n    // Create new Window with the given parameters\n    const window = createSecureWindow(params);\n\n    // Create a Promise that inspects the window 'close' event, and rejects if\n    // it is ever called.\n    const closePromise = new Promise((resolve, reject) => {\n        window.on('close', () => reject(new Error('SecureWindowUserAbort')));\n    });\n\n    // Create a timeout promise, in order to ensure we don't leavy any zombie\n    // windows open in the background\n    const timeoutPromise = new Promise((resolve, reject) => {\n        setTimeout(() => reject(new Error('SecureWindowTimeout')), 18_000_000);\n    });\n\n    // Race the function against the other promises, so that the whole function is\n    // rejected if the window is ever closed or encounters the timeout.\n    return (Promise.race([fn(window), closePromise, timeoutPromise]) as Promise<U>)\n        .then((values): U => {\n            // Also destroy the window when the function has been successfully completed.\n            window.destroy();\n            return values;\n        });\n}\n\nexport default createSecureWindow;"
  },
  {
    "path": "src/main/lib/crypto-fs/index.ts",
    "content": "import crypto from 'crypto';\nimport fs from 'fs';\n\nconst ALGORITHM = 'aes-256-cbc';\n\nclass CryptoFs {\n    private key?: Buffer = null;\n\n    constructor(password: string) {\n        this.key = crypto.createHash('sha256').update(password).digest();\n    }\n\n    /**\n     * Return a FS Client that overrides the relevant methods on FS\n     */\n    init(): typeof fs {\n        return {\n            ...fs,\n            promises: {\n                ...fs.promises,\n                writeFile: this.writeFile,\n                readFile: this.readFile as typeof fs.promises.readFile,\n            },\n        };\n    }\n\n    public writeFile = (filepath: string, data: Uint8Array): Promise<void> => {\n        return new Promise((resolve, reject) => {\n            // Initialise the stream\n            const stream = fs.createWriteStream(filepath);\n            stream.on('error', reject);\n    \n            // Create the encryption variables\n            const initVect = crypto.randomBytes(16);\n            const cipher = crypto.createCipheriv(ALGORITHM, this.key, initVect);\n            const cipheredContents = Buffer.concat([ initVect, cipher.update(data), cipher.final() ]);\n    \n            // Pipe output to file\n            stream.write(cipheredContents, (error) => { return error ? reject(error) : resolve(); });\n            stream.end();\n        });\n    };\n\n    public readFile = (filepath: string, opts: { encoding?: BufferEncoding } = {}): Promise<string | Buffer> => {\n        return fs.promises.readFile(filepath)\n            .then((data: Buffer) => {\n                // Retrieve the initialisation vector from the first sixteen bytes\n                const initVect = data.slice(0, 16);\n                const encrypted = data.slice(16);\n\n                // Initialise the decipherer\n                const decipher = crypto.createDecipheriv(ALGORITHM, this.key, initVect);\n                const result = Buffer.concat([ decipher.update(encrypted), decipher.final() ]);\n                \n                return opts.encoding ? result.toString(opts.encoding) : result;\n            });\n\n    };\n}\n\nexport default CryptoFs;"
  },
  {
    "path": "src/main/lib/logger.ts",
    "content": "import { existsSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { Container, Logger as WinstonLogger, format } from 'winston';\nimport { Console, File } from 'winston/lib/winston/transports';\nimport { logPath } from './constants';\n\n/**\n * Define the types of loggers that should be available for the application.\n * Each string will be automatically assigned a Winston logger.\n */\nconst loggerCategories = [\n    'autoUpdater',\n    'email',\n    'provider',\n    'repository',\n] as const;\n\n// GUARD: Check if the log directory already exists\nif (!existsSync(logPath)) {\n    // Create the directory recursively if it doesn't\n    mkdirSync(logPath, { recursive: true });\n}\n\n/**\n * This object contains all the loggers that are available in this application.\n */\ntype Logger = {\n    [K in typeof loggerCategories[number]]: WinstonLogger;\n};\n\n// This container will hold all the loggers.\nexport const container = new Container();\n\nconst customFormatter = format.printf((entry) => {\n    // Get all the variables from the entry\n    const { timestamp, label, level, message, ...rest } = entry;\n    const splat: unknown[] = entry[Symbol.for('splat') as unknown as string] || [];\n    const args = splat.map((arg) => JSON.stringify(arg)).join(' ');\n\n    // JSON stringify any additional parameters\n    const stringifiedRest = JSON.stringify(rest);\n\n    // Rewrite the mssage with our custom format\n    return `[${timestamp}][${label}] ${level}: ${message as unknown instanceof Object ? JSON.stringify(message) : message} ${args} ${stringifiedRest === '{}' ? '' : stringifiedRest}`;\n});\n\n// Transform the array to an object\nconst logger = loggerCategories.reduce<Logger>((loggers, categoryName) => {\n    // Create the logger object using the default options\n    loggers[categoryName] = container.add(categoryName, {\n        level: 'info',\n        transports: [\n            new Console({\n                format: format.combine(\n                    format.timestamp(),\n                    format.errors({ stack: true }),\n                    format.colorize(),\n                    format.label({ label: categoryName }),\n                    customFormatter,\n                ),\n            }),\n            new File({\n                filename: path.join(logPath, `${categoryName}.log`),\n                format: format.combine(\n                    format.timestamp(),\n                    format.label({ label: categoryName }),\n                    customFormatter,\n                ),\n            }),\n        ],\n    });\n    return loggers;\n}, {} as Logger);\n\nexport default logger;\n"
  },
  {
    "path": "src/main/lib/map-map.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\ninterface Map<K, V> {\n    // eslint-disable-next-line\n    map(callback: (value: V, key: K, index: number) => any): any\n}\n\nMap.prototype.map = function (callback) {\n    // Retrieve all keys from the Map\n    const entries = Array.from(this.entries());\n\n    // Then map over the keys, while continusouly retrieving the value\n    return entries.map(([key, value], index) => {\n        // Then call the callback for each item\n        return callback(value, key, index);\n    });\n};"
  },
  {
    "path": "src/main/lib/map-object-to-key-value.ts",
    "content": "import { ProviderDatum } from 'main/providers/types/Data';\n\n/**\n * Maps an object that is written as { key: value } to an object that is written\n * as { key: key, value: value }[].\n * @param obj \n */\n// eslint-disable-next-line\nfunction mapObjectToKeyValue(obj: { [key: string]: any }): { key: any, value: any }[] {\n    return Object.keys(obj).map((key) => ({\n        key,\n        value: obj[key],\n    }));\n}\n\n/**\n * A transformer that can be used in the schema builder to transform keyed\n * object to array with key and value keys.\n * @param obj \n */\n// eslint-disable-next-line\nexport function objectToKeyValueTransformer(obj: { [key: string]: any }): Partial<ProviderDatum<{ key: any, value: any}, any>>[] {\n    return mapObjectToKeyValue(obj)\n        .map((data) => ({\n            data,\n        }));\n}\n\nexport default mapObjectToKeyValue;"
  },
  {
    "path": "src/main/lib/notifications/index.ts",
    "content": "import { NotificationTypes } from './types';\nimport WindowStore from 'main/lib/window-store';\n\nconst channelName = 'notifications';\n\nclass Notifications {\n    public static success(message: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, NotificationTypes.SUCCESS, message);\n    }\n    \n    public static info(message: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, NotificationTypes.INFO, message);\n    }\n\n    public static loading(message: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, NotificationTypes.LOADING, message);\n    }\n\n    public static warn(message: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, NotificationTypes.WARN, message);\n    }\n\n    public static error(message: string): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, NotificationTypes.ERROR, message);\n    }\n}\n\nexport default Notifications;"
  },
  {
    "path": "src/main/lib/notifications/types.ts",
    "content": "export enum NotificationTypes {\n    SUCCESS,\n    INFO,\n    LOADING,\n    WARN,\n    ERROR,\n}"
  },
  {
    "path": "src/main/lib/oauth.ts",
    "content": "import crypto from 'crypto';\nimport { EmailClient } from 'main/email-client/types';\nimport { KeyStore } from 'main/store';\nimport fetch, { RequestInit, Response } from 'node-fetch';\n\n/**\n * Generate a secure code_verifier for further use with, e.g. Google Oauth\n */\nexport function generateVerifier(): string {\n    // This is a verifier code used by the API to check stuff\n    const randomString = crypto.randomBytes(96).toString('base64');\n\n    // The valid characters in the code_verifier are [A-Z]/[a-z]/[0-9]/\n    // \"-\"/\".\"/\"_\"/\"~\". Base64 encoded strings are pretty close, so we're just\n    // swapping out a few chars.\n    // src: https://github.com/googleapis/google-auth-library-nodejs/blob/d4e56c0937adbf9561d5ca3860a1bde623696db7/src/auth/oauth2client.ts#L560\n    const verifier = randomString\n        .replace(/\\+/g, '~')\n        .replace(/=/g, '_')\n        .replace(/\\//g, '-');\n\n    return verifier;\n}\n\n/**\n * Convert a dictionary JS object to a URL-encoded parameter string\n */\nexport function objectToUrlParams(data: Record<string, unknown>) {\n    return Object.keys(data).reduce((uri, key, i) => {\n        return `${uri}${i === 0 ? '?' : '&'}${key}=${data[key]}`;\n    }, '');\n}\n\n\n/**\n * This defines a basic object that can store tokens for OAuth-powered APIs.\n * Since they must include an access_token and refresh_token, these should be\n * the minimum of data that is necessary for use.\n */\nexport interface BaseTokenResponse {\n    access_token: string;\n    refresh_token: string;\n}\n\n/**\n * This is the base for an emal provider class that implements some helper\n * functions for dealing with OAuth-based APIs. This particularly concerns\n * catching access token expiries and re-trying the requests when a access token\n * has been renewed.\n */\nexport abstract class OauthEmailClient<T extends BaseTokenResponse = BaseTokenResponse> extends EmailClient {\n    private tokens: T | null;\n\n    /**\n     * Implement a function that takes an existing token that might be expired,\n     * exchanges it for a new token and returns it. \n     */\n    abstract refreshTokens(tokens: T): Promise<T>;\n\n    async delete(): Promise<void> {\n        await KeyStore.delete(`${this.key}_${this.emailAddress}`);\n    }\n\n    /**\n     * Retrieve the OAuth tokens for this client. \n     */\n    async getTokens(): Promise<T> {\n        if (this.isInitialized) {\n            return this.tokens;\n        }\n\n        // Retrieve the tokens from the Keytar store and parse it as JSON\n        const rawTokens = await KeyStore.get(`${this.key}_${this.emailAddress}`);\n        const tokens = JSON.parse(rawTokens);\n            \n        // GUARD: Double-check that whats coming back from the store is\n        // actually a token.\n        if (!tokens) {\n            throw new Error(`Email address '${this.emailAddress}' was supposed to be initialised already with the Gmail Client, but no tokens could be retrieved from the store.`);\n        }\n\n        // Store the tokens in the class and set the initialisation flag\n        this.tokens = tokens;\n        this.isInitialized = true;\n\n        return tokens;\n    }\n\n    /**\n     * Store a new set of tokens for this particular client.\n     */\n    async storeTokens(tokens: T, persist = true): Promise<void> {\n        // GUARD: Optionally persist the key to the user's keychain. This should\n        // only be done in cases where the email address is not yet available.\n        // Any implementer setting persist to false is responsible for making\n        // sure a later call is made to this function with the persist key set\n        // to true.\n        if (persist) {\n            await KeyStore.set(`${this.key}_${this.emailAddress}`, JSON.stringify(tokens));\n        }\n        this.tokens = tokens;\n        this.isInitialized = true;\n    }\n\n    /**\n     * Send out a GET request to the Gmail API\n     * @param url URL of the API endpoint\n     * @param init Extra parameters to be sent along with the fetch request\n     */\n    async get(url: string, init: RequestInit = null, parseType: 'json' | 'text' = 'json'): Promise<unknown> {\n        const tokens = await this.getTokens();\n        // GUARD: Check if a token is present before sending request\n        if (!tokens) {\n            throw new Error('Cant refresh tokens if no tokens are present');\n        }\n\n        const options = {\n            headers: {\n                'Authorization': `Bearer ${tokens.access_token}`,\n            },\n            ...init,\n        };\n\n        return fetch(url, options)\n            .then(this.tokenMiddleware(url, options))\n            .then(this.errorMiddleware)\n            .then((response) => response[parseType]());\n    }\n\n    /**\n     * A piece of fetch middleware that checks if the response is of status 401.\n     * If so, the access token has expired. It will fetch a new one and mount a\n     * new request to continue the chain.\n     */\n    tokenMiddleware = (url: string, init: RequestInit = null): ((response: Response) => Promise<Response>) => {\n        return async (response: Response): Promise<Response> => {\n            // GUARD: Check if the token has expired\n            if (response.status === 401) {\n                // If so, refresh the token\n                const expiredTokens = await this.getTokens();\n                const tokens = await this.refreshTokens(expiredTokens);\n                await this.storeTokens(tokens);\n    \n                // Then send out a new request\n                return fetch(url, {\n                    ...init,\n                    headers: {\n                        'Authorization': `Bearer ${tokens.access_token}`,\n                    },\n                });\n            }\n    \n            return response;\n        };\n    };\n\n    /**\n     * Will pick off any responses that result in errors\n     */\n    async errorMiddleware(response: Response): Promise<Response> {\n        if (response.status > 400) {\n            const text = await response.text();\n            throw new Error(`Error while sending request: ${text}`);\n        }\n\n        return response;\n    }\n}"
  },
  {
    "path": "src/main/lib/object-to-map.ts",
    "content": ""
  },
  {
    "path": "src/main/lib/persisted-map.ts",
    "content": "/**\n * A class that accepts a function that is called each time the map is updated,\n * as to make persisting it (ie. in electron-store) a bit easier.\n */\nclass PersistedMap<K, V> extends Map<K, V> {\n    private callback: (map: PersistedMap<K, V>) => void;\n\n    constructor(rows: [K, V][], callback: (map: PersistedMap<K, V>) => void) {\n        super(rows);\n        \n        // Bind the callback to the instance of this class\n        this.callback = callback.bind(this);\n    }\n\n    set(key: K, value: V): this {\n        const ret = super.set(key, value);\n\n        // GUARD: the super call in the in the constructor calls set internally.\n        // Since the callback isn't yet set at that point, we disregard any\n        // callback calls when this happens.\n        if (this.callback) {\n            this.callback(this);\n        }\n\n        return ret;\n    }\n\n    delete(key: K): boolean {\n        const ret = super.delete(key);\n        this.callback(this);\n        return ret;\n    }\n\n    clear(): void {\n        super.clear();\n        this.callback(this);\n    }\n}\n\nexport default PersistedMap;"
  },
  {
    "path": "src/main/lib/protocol-handler.ts",
    "content": "import { app } from 'electron';\nimport path from 'path';\n\n/** The registered callback  */\ntype ProtocolCallback = (url: string) => void;\n\n/**\n* This stores Promise functions for protocols that expect to await a protocol response\n*/\nconst registeredDeferreds: Map<string, [(value: string) => void, (reason: string) => void]> = new Map();\n\n/**\n* This stores permanent callbacks for protocol responses\n*/\nconst registeredCallbacks: Map<string, ProtocolCallback> = new Map();\n\n/**\n* This function will return a promise that resolves once a protocol call is\n* received that matches the URL partial. This only works with the aeon://\n* protocol. The returned string in the promise is the protocol URL.\n* NOTE: when a Promise already exists for the exact urlPath, it will be rejected.\n*/\nexport function getProtocolResultForPath(urlPath: string) {\n    // GUARD: Check if the urlPath is already registered\n    if (registeredDeferreds.has(urlPath)) {\n        // If so, we reject the promise\n        const [, reject] = registeredDeferreds.get(urlPath);\n        reject('The protocol handler for ' + urlPath + 'was overwritten.');\n    }\n    \n    // Create a new promise\n    return new Promise<string>((resolve, reject) => {\n        // Register the resolve and reject functions as deferreds\n        registeredDeferreds.set(urlPath, [resolve, reject]);\n    });\n}\n\n/**\n* This will register a handler for whenever a protocol is called that matches a\n* particular URL partial path. The callback will resolve with the full URL. \n* Only a single callback can be registered for an unique partial path. \n*/\nexport function registerProtocolCallbackForPath(urlPath: string, callback: ProtocolCallback) {\n    registeredCallbacks.set(urlPath, callback);\n}\n\n/**\n* This is an internal function to register all the protocol handlers.\n*/\nexport function setupProtocolHandlers() {\n    // Register the protocol with the OS\n    if (process.defaultApp) {\n        if (process.argv.length >= 2) {\n            app.setAsDefaultProtocolClient(\n                'aeon',\n                process.execPath,\n                [path.resolve(process.argv[1])],\n            );\n        }\n    } else {\n        app.setAsDefaultProtocolClient('aeon');\n    }\n        \n    // Register the global protocol handler\n    app.on('open-url', function (event, url) {\n        // Prevent the event from bubbling up the tree\n        event.preventDefault();\n        \n        // Get the urlPath by parsing the incoming URL.\n        // NOTE: The urlPath does not contain query parameters, but the url that\n        // is returned to the callbacks does.\n        const { origin, pathname } = new URL(url);\n        const urlPath = origin + pathname;\n        \n        // GUARD: If a promise is registered for this path, execute it and then\n        // remove it from the map.\n        if (registeredDeferreds.has(urlPath)) {\n            const [resolve] = registeredDeferreds.get(urlPath);\n            resolve(url);\n            registeredDeferreds.delete(urlPath);\n        }\n        \n        // GUARD: If a callback is registered for this path, execute it.\n        if (registeredCallbacks.has(urlPath)) {\n            const callback = registeredCallbacks.get(urlPath);\n            callback(url);\n        }\n    });\n}\n    "
  },
  {
    "path": "src/main/lib/repository/bridge.ts",
    "content": "import Repository from '.';\nimport { RepositoryCommands, RepositoryEvents } from './types';\nimport { ipcMain, IpcMainInvokeEvent } from 'electron';\nimport WindowStore from '../window-store';\n\nconst channelName = 'repository';\n\nclass RepositoryBridge {\n    repository: Repository = null;\n\n    messageCache: [IpcMainInvokeEvent, number][] = [];\n\n    constructor(repository: Repository) {\n        this.repository = repository;\n        this.repository.on('ready', this.clearMessageCache);\n\n        ipcMain.handle(channelName, this.handleMessage);\n    }\n\n    // eslint-disable-next-line\n    private handleMessage = async (event: IpcMainInvokeEvent, command: number, ...args: any[]): Promise<any> => {\n        // GUARD: Check if the repository is initialised. If not, defer to the\n        // messagecache, so that it can be injected later.\n        if (!this.repository.isInitialised) {\n            this.messageCache.push([event, command]);\n            return;\n        }\n        \n        switch (command) {\n            case RepositoryCommands.LOG:\n                return this.repository.log();\n            case RepositoryCommands.DIFF:\n                return this.repository.diff(...args);\n            case RepositoryCommands.STATUS:\n                return this.repository.status();\n            case RepositoryCommands.PARSED_COMMIT: {\n                return this.repository.getParsedCommit(...args);\n            }\n        }\n    };\n\n    private clearMessageCache = (): void => {\n        this.messageCache.forEach((args) => this.handleMessage(...args));\n        this.messageCache = [];\n    };\n\n    /**\n     * Send an event to the renderer\n     * @param event The event to send out\n     */\n    public static send(event: RepositoryEvents): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, event);\n    }\n}\n\nexport default RepositoryBridge;"
  },
  {
    "path": "src/main/lib/repository/index.ts",
    "content": "import path from 'path';\nimport { EventEmitter } from 'events';\nimport {\n    TreeEntry,\n    Repository as NodeGitRepository,\n    Signature,\n    Index,\n    Revwalk,\n    Commit as NodeGitCommit,\n    Reference,\n    StatusFile,\n} from 'nodegit';\nimport { DiffResult, RepositoryEvents, Commit } from './types';\nimport CryptoFs from '../crypto-fs';\nimport nonCryptoFs from 'fs';\nimport diffMapFunction from './utilities/diff-map';\nimport generateParsedCommit from './utilities/generate-parsed-commit';\nimport { ProviderDatum } from 'main/providers/types/Data';\nimport RepositoryBridge from './bridge';\nimport logger from '../logger';\nimport { repositoryPath } from '../constants';\n\n// Define a location where the repository will be saved\n// TODO: Encrypt this filesystem\nexport const EMPTY_REPO_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';\n\nconst ENABLE_ENCRYPTION = process.env.ENABLE_ENCRYPTION === 'true';\nconst fs = ENABLE_ENCRYPTION ? new CryptoFs('password').init() : nonCryptoFs;\n\nclass Repository extends EventEmitter {\n    /**\n     * The repository path for nodegit\n     */\n    dir = path.join(repositoryPath, '.git').replace(/\\\\/g, '/');\n\n    /**\n     * Whether the git repository is ready for querying\n     */\n    isInitialised = false;\n\n    /**\n     * The default author for all commits made\n     */\n    author = Signature.now(\n        'Aeon',\n        'aeon@codified.nl',\n    );\n\n    /**\n     * A reference to an instance of Nodegit's repository class\n     */\n    repository: NodeGitRepository = null;\n\n    index: Index = null;\n\n    constructor() {\n        super();\n\n        NodeGitRepository.open(this.dir)\n            .catch((err) => {\n                console.error(err);\n                return this.initialiseRepository();\n            })\n            .then(async (repository) => {\n                this.repository = repository;\n                this.index = await repository.refreshIndex();\n                this.isInitialised = true;\n                this.emit('ready');\n                logger.repository.info(`Repository was succesfully opened at ${repositoryPath}`);\n            })\n            .catch(console.error);\n    }\n\n    /**\n     * Initialise a repository if one doesn't exist already. Also add a base\n     * files and commit, so that we can work from there.\n     */\n    private async initialiseRepository(): Promise<NodeGitRepository> {\n        logger.repository.info('Repository was not found, creating a new one');\n\n        // First we'll initiate the repository\n        await fs.promises.mkdir(this.dir, { recursive: true });\n        const repository = await NodeGitRepository.init(this.dir, 0);\n\n        // Then we'll write a file to disk so that the repository is populated\n        const readmePath = 'README.md';\n        await fs.promises.writeFile(path.resolve(repositoryPath, readmePath), Buffer.from('# Aeon Repository', 'utf8'));\n\n        // And create a first commit with the file\n        const index = await repository.refreshIndex();\n        await index.addByPath(readmePath);\n        await index.write();\n        const oid = await index.writeTree();\n        await repository.createCommit('HEAD', this.author, this.author, 'Initial Commit', oid, []);\n        \n        logger.repository.info('Initialised new repository at ' + repositoryPath);\n\n        // Then we return the commit log\n        return repository;\n    }\n\n    /**\n     * Generate a complete repository diff for two supplied trees\n     * @param refTree The reference tree. Defaults to HEAD\n     * @param comparedTree The tree the reference is compared to. Defaults to\n     * the previous commit.\n     */\n    public async diff(\n        ref = 'HEAD', \n        compared: string = null,\n        options: { showUnchangedFiles?: boolean } = {},\n    ): Promise<DiffResult<unknown>[]> {\n        // Retrieve the commit based on either a supplied OID or otherwise HEAD\n        const refCommit = ref === 'HEAD' \n            ? await this.repository.getHeadCommit()\n            : await this.repository.getCommit(ref);\n        // Then retrieve the tree for the the retrieved commit\n        const refTree = await refCommit.getTree();\n\n        // Then retrieve either a supplied commit or alternatively the parent\n        // for the refCommit\n        const comparedCommit = compared\n            ? await this.repository.getCommit(compared)\n            : await refCommit.parent(0).catch(() => null);\n        // We then retrieve the tree for said commit\n        const comparedTree = comparedCommit\n            ? await comparedCommit.getTree()\n            : await this.repository.getTree(EMPTY_REPO_HASH);\n\n        // First off, we have to retrieve the diff object for the compared tree\n        const diff = await refTree.diff(comparedTree);\n        // Then, we'll retrieve the patch to signify the diff between the two\n        const patches = await diff.patches();\n        // Lastly, we'll map over all the individual patches (files) for this diff\n        const diffs = (await Promise.all(\n            patches.map(async (patch) => {\n                // Retrieve the filepaths for both versions of the tree\n                const oldFile = patch.oldFile().path();\n                const newFile = patch.newFile().path();\n                \n                // Then retrieve the actual files\n                const [oldEntry, newEntry] = await Promise.all([\n                    await comparedTree.getEntry(oldFile).catch((): null => null),\n                    await refTree.getEntry(newFile).catch((): null => null),\n                ]);\n\n                return diffMapFunction(newFile, [newEntry, oldEntry]);\n            }),\n        )).flat();\n\n        // Optionally remove all files from the diff without changes\n        if (!options.showUnchangedFiles) {\n            // Loop through all files one-by-one\n            return diffs.filter((file) => file && file.hasChanges);\n        }\n\n        // Lastly, remove any of the diffs that are empty\n        return diffs.filter((file) => !!file);\n    }\n\n    /**\n     * Save a file to disk\n     * @param filepath The path to the file relative to the repository root\n     * @param data The data that needs to be written to disk\n     */\n    public async save(filepath: string, data: string | Buffer): Promise<void> {\n        const absolutePath = path.resolve(repositoryPath, filepath);\n        const dirPath = path.dirname(absolutePath);\n\n        // Check if the directory already exists\n        if (!fs.existsSync(dirPath)) {\n            // If not, create the full path\n            await fs.promises.mkdir(dirPath, { recursive: true });\n        }\n\n        // Write file to disk\n        await fs.promises.writeFile(absolutePath, data);\n    }\n\n    /**\n     * Generate a fully parsed tree\n     */\n    public async getParsedCommit(ref = 'HEAD'): Promise<ProviderDatum<unknown, unknown>[]> {\n        // Retrieve the commit based on either a supplied OID or otherwise HEAD\n        const refCommit = ref === 'HEAD' \n            ? await this.repository.getHeadCommit()\n            : await this.repository.getCommit(ref);\n\n        // GUARD: Before any data requests are issued, there is only a single\n        // commit in the repository. This means we cannot compare anything.\n        if (!refCommit) {\n            throw new Error('No reference commit to parse');\n        }\n\n        // Then retrieve the tree for the the retrieved commit\n        const refTree = await refCommit.getTree();\n\n        // Then parse all entries through the generateParsedCommit function\n        return new Promise((resolve, reject) => {\n            // Create a nodegit walker object\n            const walker = refTree.walk();\n    \n            // Whenever we're done, we sort the data and pass it back\n            walker.on('end', async (entries: TreeEntry[]) => {\n                const data = await Promise.all(\n                    entries.map(async (entry) => {\n                        // GUARD: Only process files, as opposed to directories\n                        if (!entry.isFile()) {\n                            return;\n                        }\n            \n                        const parsedCommit = await generateParsedCommit(\n                            entry.path(),\n                            entry,\n                        );\n        \n                        // GUARD: Only push data if the file is successfully parsed\n                        if (parsedCommit?.length) {\n                            return parsedCommit;\n                        }\n                    }),\n                );\n\n                // Flatten array and filter any undefined values\n                const filteredData = data.flat().filter((d) => !!d);\n\n                // Sort data by type, so that we can render it more easily in\n                // the UI\n                const sortedData = filteredData.sort((a: ProviderDatum<unknown>, b: ProviderDatum<unknown>): number => {\n                    return a.type.localeCompare(b.type);\n                });\n\n                // Then return it!\n                resolve(sortedData);\n            });\n    \n            // Also catch any errors\n            walker.on('error', reject);\n\n            // And fire off the walker!\n            walker.start();\n        });\n    }\n\n    /** \n     * Expose a number of Git functions directly\n    */\n    public async add(filepath: string): Promise<void> {\n        await this.index.addByPath(filepath);\n    }\n    \n    public async log(): Promise<Commit[]> {\n        // Create new revwalk to gather all commits\n        const walker = Revwalk.create(this.repository);\n\n        // Start from HEAD and retrieve all commits\n        walker.pushHead();\n        const commits = await walker.getCommitsUntil(() => true) as NodeGitCommit[];\n\n        return Promise.all(\n            commits.map(async (commit) => {\n                const author = commit.author();\n\n                return {\n                    oid: commit.sha(),\n                    // Only show the first line of a commit\n                    message: commit.message(),\n                    author: {\n                        email: author.email(),\n                        name: author.name(),\n                        when: commit.time() * 1000,\n                    },\n                    parents: commit.parents().map((oid) => oid.tostrS()),\n                };\n            }),\n        );\n    }\n    \n    public async commit(message: string): Promise<string> {\n        // Retrieve and write new index\n        await this.index.write();\n        const oid = await this.index.writeTree();\n\n        // Retrieve the commit that is to serve as the parent commit\n        const head = await Reference.nameToId(this.repository, 'HEAD');\n        const parent = await this.repository.getCommit(head);\n\n        // Create commit using new index\n        const commit = await this.repository.createCommit('HEAD', this.author, this.author, message, oid, [parent]);\n\n        // Refresh index, just in case\n        this.index = await this.repository.refreshIndex();\n\n        // Notify app of new commit\n        RepositoryBridge.send(RepositoryEvents.NEW_COMMIT);\n\n        return commit.tostrS();\n    }\n\n    public status(): Promise<StatusFile[]> {\n        return this.repository.getStatus();\n    }\n    \n    public readFile = (filePath: string): Promise<Buffer> => fs.promises.readFile(filePath);\n}\n\nexport default Repository;"
  },
  {
    "path": "src/main/lib/repository/types.ts",
    "content": "import { ProviderDatum } from 'main/providers/types/Data';\n\nexport enum DiffType {\n    // An object DiffType is the diff of an regular object\n    OBJECT,\n    // For the extracted data type, a given object has been run through its\n    // parser and thus contains instances of ProviderDatum, rather than just the\n    // bare object\n    EXTRACTED_DATA,\n    // Any unparseable content, such as pictures, videos, etc.\n    BINARY_BLOB,\n    // If not a blob, we assume the file is a text file, and thus do a\n    // line-by-line diff\n    TEXT,\n}\n\nexport interface ObjectChange<O = Record<string, unknown>> {\n    added: O;\n    deleted: O;\n    updated: O;\n}\n\n/**\n * Represents a result from the Diff function.\n * NOTE: The diff result is dependent on the type of file that served as input\n * for it. This means you HAVE TO switch on the DiffType in order to deal with\n * the correct diff.\n */\nexport interface DiffResult<D> {\n    filepath: string;\n    diff: D;\n    type: DiffType;\n    hasChanges: boolean;\n}\n\nexport type ObjectDiff = ObjectChange<unknown>;\nexport type ExtractedDataDiff = ObjectChange<ProviderDatum<unknown>[]>;\n// export type BlobDiff = Change[];\n// export type TextDiff = Change[];\n\ntype Status = 'ignored' | 'unmodified' | '*modified' | '*deleted' | '*added' | 'absent' | 'modified' | 'deleted' | 'added' | '*unmodified' | '*absent' | '*undeleted' | '*undeletemodified';\n\nexport interface StatusResult {\n    filepath: string;\n    status: Status;\n}\n\nexport enum RepositoryCommands {\n    LOG,\n    DIFF,\n    STATUS,\n    PARSED_COMMIT,\n} \n\nexport enum RepositoryArguments {\n    WORKDIR,\n    STAGE,\n    HEAD,\n}\n\nexport enum RepositoryEvents {\n    NEW_COMMIT,\n}\n\nexport interface Commit {\n    oid: string;\n    parents: string[];\n    message: string;\n    author: {\n        email: string;\n        name: string;\n        when: number;\n    };\n}"
  },
  {
    "path": "src/main/lib/repository/utilities/diff-map.ts",
    "content": "import generateDiff from './generate-diff';\nimport { DiffResult } from '../types';\nimport { TreeEntry } from 'nodegit';\n\n\n/**\n * The map function that loops through a repository and returns a diff\n * @param filepath The filepath for the currently handled file\n * @param entries The references to the walker functions for the individual trees\n */\nconst diffMapFunction = async function (filepath: string, entries: Array<TreeEntry>): Promise<DiffResult<unknown>> {\n    // Extract entries and file contents\n    const [ refTree, comparedTree ] = entries;\n\n    // Calculate the diff\n    const diff = await generateDiff(filepath, comparedTree, refTree);\n\n    // Filter any instances where there are no changes\n    if (!diff || !diff.hasChanges) {\n        return;\n    }\n\n    // Then return the data as expected\n    return diff;\n};\n\nexport default diffMapFunction;"
  },
  {
    "path": "src/main/lib/repository/utilities/generate-diff.ts",
    "content": "import { DiffType, DiffResult } from '../types';\nimport generateParsedCommit from './generate-parsed-commit';\nimport { ProviderDatum } from 'main/providers/types/Data';\nimport { isEqual } from 'lodash-es';\nimport { TreeEntry } from 'nodegit';\n\ninterface DataArrayDiff {\n    added: ProviderDatum<unknown, unknown>[],\n    deleted: ProviderDatum<unknown, unknown>[],\n    updated: ProviderDatum<unknown, unknown>[],\n}\n\n/**\n * Check which elements from a are not present on elements from b. The right way\n * to look at this is to see which elements were 'added' when going from array\n * to array b.\n * @param a \n * @param b \n */\nfunction diffDataArray(\n    before: ProviderDatum<unknown, unknown>[], \n    after: ProviderDatum<unknown, unknown>[],\n): DataArrayDiff {\n    // GUARD: If any of the two diffs is empty, return the whole object as diff\n    if (!before.length) {\n        // There was nothing before, so everything remains after\n        return {\n            added: after,\n            deleted: [],\n            updated: [],\n        };\n    } else if (!after.length) {\n        // Everything existed before, and nothing remains\n        return {\n            added: [],\n            deleted: before,\n            updated: [],\n        };\n    }\n\n    // Initialise sorted arrays\n    const added: ProviderDatum<unknown, unknown>[] = [];\n    const deleted: ProviderDatum<unknown, unknown>[] = [];\n    const updated: ProviderDatum<unknown, unknown>[] = [];\n\n    // TODO: This block is ripe for optimisation. Currently, both arrays are\n    // fully looped. If saving matches from the first loop, we can skip the\n    // largest part of the second loop. This hould increase performance more\n    // than two-fold.\n\n    // Loop through the before array to see if any of its elements have been deleted\n    for (const dBefore of before) {\n        // Find any object on the after array that matches this datapoint\n        const match = after.find((dAfter) => {\n            return typeof dAfter.data === 'object' && dAfter.data !== null\n                ? isEqual(dBefore.data, dAfter.data)\n                : dBefore.data === dAfter.data;\n        });\n\n        // If a match is found, we can exit the loop. No change has been made to\n        // the datapoint.\n        if (match) {\n            continue;\n        }\n\n        deleted.push(dBefore);\n    }\n\n    // Now we'll sort through the after array and see if any elements have been added\n    for (const dAfter of after) {\n        // Find any object on the after array that matches this datapoint\n        const match = before.find((dBefore) => {\n            return typeof dAfter.data === 'object' && dAfter.data !== null\n                ? isEqual(dAfter.data, dBefore.data)\n                : dAfter.data === dBefore.data;\n        });\n\n        // If a match is found, we can exit the loop. No change has been made to\n        // the datapoint.\n        if (match) {\n            continue;\n        }\n\n        added.push(dAfter);\n    }\n\n    return {\n        added,\n        deleted,\n        updated,\n    };\n}\n\n/**\n * Generates a specific diff according to filetype\n * @param filepath The filepath of the diffed file\n * @param ref The ref Buffer\n * @param compared The compared Buffer\n */\nasync function generateDiff(\n    filepath: string, \n    ref: TreeEntry, \n    compared: TreeEntry,\n): Promise<DiffResult<DataArrayDiff>> {\n    // Parse all the data from the files for both commits\n    const [ refData, comparedData ] = await Promise.all([\n        generateParsedCommit(filepath, ref),\n        generateParsedCommit(filepath, compared),\n    ]);\n\n    // GUARD: The parsed commit handler may reject any file under certain\n    // circumstances. If this is the case, we discard the file\n    if (refData === undefined && comparedData === undefined) {\n        return;\n    }\n\n    // Then diff the two datasets\n    const diff = diffDataArray(refData || [], comparedData || []);\n\n    return {\n        filepath, \n        diff,\n        type: DiffType.EXTRACTED_DATA,\n        hasChanges: Object.keys(diff.added).length > 0\n            || Object.keys(diff.deleted).length > 0,\n    };\n}\n\nexport default generateDiff;"
  },
  {
    "path": "src/main/lib/repository/utilities/generate-parsed-commit.ts",
    "content": "import path from 'path';\nimport { getParserByFileName } from 'main/providers/parsers';\nimport parseSchema from './parse-schema';\nimport { ProviderDatum } from 'main/providers/types/Data';\nimport { Blob, TreeEntry } from 'nodegit';\nimport parseCsv from './parse-csv';\nimport parseOpenDataRights, { OpenDataRightsDatum } from './parse-open-data-rights';\n\nconst utfDecoder = new TextDecoder('utf-8');\n\n/**\n * The extensions that shall be parsed\n */\nconst parsedExtensions = [\n    '.json',\n    '.csv',\n] as const;\n\ntype ParsedExtension = typeof parsedExtensions[number];\n\n/**\n * \n * @param extension The file extension\n * @param blob The nodegit-provided blob object that is referencing the actual data\n */\nfunction getObjectByExtension(extension: ParsedExtension, blob: Blob) {\n    switch (extension) {\n        case '.json': {\n            return JSON.parse(utfDecoder.decode(blob.content()));\n        }\n        case '.csv': {\n            return parseCsv(blob);\n        }\n        default: {\n            throw Error(`Cannot handle filetype of ${extension}`);\n        }\n    }\n}\n\n/**\n * A walker function that parses files from a single tree\n */\nasync function generateParsedCommit(\n    filepath: string, \n    tree: TreeEntry,\n): Promise<ProviderDatum<unknown, unknown>[]> {\n    // GUARD: The tree must exist\n    if (!tree) {\n        return;\n    }\n\n    // GUARD: We only work with parseable data\n    const fileExtension = path.extname(filepath) as ParsedExtension;\n    if (!parsedExtensions.includes(fileExtension)) {\n        return;\n    }\n\n    // Retrieve the data from the tree\n    const data = await tree.getBlob();\n    const object = await getObjectByExtension(fileExtension, data);\n\n    // GUARD: If the file is open-data-rights based, we don't need any fancy\n    // parsers. We can just directly import the data and add the right flags.\n    if (filepath.startsWith('open-data-rights')) {\n        // Retrieve the hostname and account from the path. Also gather the rest\n        // of the path so we can clarify the exact source\n        const [, hostname, account, ...rest] = filepath.split('/');\n        return parseOpenDataRights(\n            object as OpenDataRightsDatum[],\n            hostname,\n            account,\n            rest.join('/'),\n        );\n    }\n    \n    // We then parse the content and get the relevant parser\n    const parser = getParserByFileName(filepath);\n\n    // GUARD: If there's not parser for the file, there's nothing we can do\n    if (!parser) {\n        return;\n    }\n\n\n    // Retrieve the account from the pathname\n    const [, account] = filepath.split('/');\n\n    return parseSchema(object, parser, account);\n}\n\nexport default generateParsedCommit;"
  },
  {
    "path": "src/main/lib/repository/utilities/parse-csv.ts",
    "content": "import { Blob } from 'nodegit';\nimport { parse } from '@fast-csv/parse';\nimport { Readable } from 'stream-chain';\n\nconst utfDecoder = new TextDecoder('utf-8');\n\n/**\n * Transforms a nodegit provided CSV blob to an array of objects\n * @param blob \n */\nfunction parseCsv(blob: Blob): Promise<unknown> {\n    // Create readstream from blob\n    const readStream = Readable.from(utfDecoder.decode(blob.content()));\n\n    return new Promise((resolve, reject) => {\n        // Create holding array for feature headers and rows\n        let headers: string[] = [];\n        const rows: unknown[] = [];\n\n        // Then setup the parse\n        const parser = parse()\n            .on('error', reject)\n            .on('data', (data: string[]) => {\n                // The first piece of data is the header, which we will save appropriately\n                if (!headers.length) {\n                    headers = data;\n                    return;\n                }\n\n                // For normal operation, we assign the headers as key names\n                const object = data.reduce<Record<string, unknown>>((sum, value, i) => {\n                    // GUARD: Skip the loop if the value is empty\n                    if (!value) {\n                        return sum;\n                    }\n                    \n                    // Retrieve the appropriate key\n                    const key = headers[i];\n\n                    // GUARD: If the sum object already contains this key, we\n                    // append the value rather than replacing it\n                    if (Object.prototype.hasOwnProperty.call(sum, key)) {\n                        sum[key] += '; ';\n                        sum[key] += value;\n                    } else {\n                        sum[key] = value;\n                    }\n\n                    return sum;\n                }, {});\n\n                // Push the resulting object to rows\n                rows.push(object);\n            })\n            .on('end', () => {\n                return rows.length > 1\n                    ? resolve(rows)\n                    : resolve(rows.length === 0 ? {} : rows[0]);\n            });\n        \n        readStream.pipe(parser);\n    });\n}\n\nexport default parseCsv;"
  },
  {
    "path": "src/main/lib/repository/utilities/parse-open-data-rights.ts",
    "content": "import { ProviderDatum } from 'main/providers/types/Data';\n\nexport type OpenDataRightsDatum = Pick<\nProviderDatum<unknown, unknown>,\n'data' | 'timestamp' | 'type'\n>;\n\n/**\n * Parse a Open Data Rights API-generated data file, and add the requisite\n * metadata to the invididual datapoints. \n */\nfunction parseOpenDataRights(\n    data: OpenDataRightsDatum[],\n    hostname: string,\n    account: string,\n    source: string,\n): ProviderDatum<unknown, unknown>[] {\n    return data.map((datum: OpenDataRightsDatum) => {\n        return {\n            ...datum,\n            hostname,\n            account,\n            provider: 'open-data-rights',\n            source,\n        };\n    });\n}\n\nexport default parseOpenDataRights;"
  },
  {
    "path": "src/main/lib/repository/utilities/parse-schema.ts",
    "content": "import { compile, TreeInterpreter } from '@metrichor/jmespath';\nimport { ExpressionNodeTree } from '@metrichor/jmespath/dist/types/Lexer';\nimport logger from 'main/lib/logger';\nimport unwrapParserSource from 'main/lib/unwrap-provider-source';\nimport { ProviderDatum, ProviderParser } from 'main/providers/types/Data';\n\n// This is a single TextDecoder instance that converts the Nodegit blob to\n// something we can parse.\nconst decoder = new TextDecoder('utf-8');\n\n// This keeps cached instances of compiled JMESPath selectors\nconst selectorCache = new Map<string, ExpressionNodeTree>();\n\n/**\n * This function recursively travels through an object in order to retrieve any\n * object that contains a particular key. An array with all instances of this\n * key is returned.\n * @param haystack The data which we need to sort through\n * @param needle The key we're looking for\n */\n// eslint-disable-next-line\nfunction recursivelyExtractData(haystack: {[key: string]: any}, needle: string | string[]): any[] {\n    // eslint-disable-next-line\n    let data = [];\n\n    // GUARD: If an array does happen to enter the function, we need to send the\n    // individual object through the function\n    if (Array.isArray(haystack)) {\n        // Loop through all items that are in the array\n        for (const item of haystack) {\n            // Run them through the function\n            const result = recursivelyExtractData(item, needle);\n\n            // And then append the result to the return array. Use a spread\n            // operator so that we flatten the array that is introduced by this function\n            if (result && result.length) {\n                data.push(...result);\n            }\n        }\n\n        return data;\n    }\n\n    // In case of an object, we'll loop through all keys and check for a match,\n    // or possibly further iteration possibilities\n    for (const [key, item] of Object.entries(haystack)) {\n        // If the key matches, we just return the whole bunch of data\n        if (Array.isArray(needle)\n            ? needle.includes(key)\n            : key === needle) {\n            // GUARD: Double-check if item contains valid date\n            if (item) {\n                // Make sure to unpack any arrays that come along\n                data.push(...(Array.isArray(item) ? item : [item]));\n            }\n            \n            // Exit the loop\n            break;\n        }\n\n        // If the data is an array, we pipe the whole thing back to the\n        // recursive function\n        if (typeof item === 'object' && item) {\n            // Optionally wrap the item in an array so that we can process both\n            // cases at once. This is necessary because the item could either be\n            // an object with keys or an array with objects.\n            const iterable = Array.isArray(item) ? item : [item];\n\n            // Loop through options and extract optional data\n            for (const nestedItem of iterable) {\n                const result = recursivelyExtractData(nestedItem, needle);\n                \n                if (result && result.length) {\n                    data.push(...result);\n                }\n            }\n        }\n    }\n\n    return data;\n}\n\n/**\n * Extract all data from a given file according to the provided schema\n * @param file The provided file in buffer format\n * @param provider The provider for this specific set of schemas\n * @param schema A ProviderParser that gives the rules\n */\n// eslint-disable-next-line\nfunction parseSchema(file: Buffer | { [key: string] : any }, parser: ProviderParser, account: string): ProviderDatum<any, any>[] {\n    const { source: baseSource, provider } = parser;\n\n    // Unwrap a possible string array\n    const source = unwrapParserSource(baseSource);\n\n    // Then we decode the file\n    const object = file instanceof Buffer \n        ? JSON.parse(decoder.decode(file))\n        : file;\n\n    // GUARD: If there is no object, we cannot extract data from it\n    if (!object) {\n        return [];\n    }\n\n    // Now we can start parsing the file\n    return parser.schemas.map((schema): ProviderDatum<unknown, unknown> => {\n        const { type, transformer, key, selector } = schema;\n\n        // GUARD: Check if we already have a cached version of the selector\n        if (selector && !selectorCache.has(selector)) {\n            // If we don't, we generate it\n            selectorCache.set(selector, compile(selector));\n        }\n\n        try {\n            // We then recursively extract and possibly transform the data. This\n            // is preferably done with the selector if it is done at all.\n            const extractedWithSelector = selector && TreeInterpreter.search(selectorCache.get(selector), object);\n            const extractedWithKey = key && recursivelyExtractData(object, key);\n            const extractedData = extractedWithSelector || extractedWithKey || object;\n\n            const transformedData = transformer \n                ? (Array.isArray(extractedData) ? extractedData.map(transformer) : transformer(extractedData))\n                : extractedData;\n\n            /** \n             * The next thing is a bit tricky because the transformed data\n             * might be in one of three forms:\n             * 1. The data is untransformed and is basically an array\n             *    containing all single values that were extracted from the\n             *    file\n             * 2. The data is transformed into an array of single items\n             * 3. The data is transformed and has yielded multiple items per\n             *   original item. It is now basically an array of arrays\n             */\n\n            // First we'll handle the cases where the data is transformed.\n            // In this case, we expect the transformer to already wrap\n            // everything in the correct format, which we should then just\n            // append to the normal values.\n            if (transformer) {\n                // GUARD: Check if what is returned by the transformed is an\n                // array as expected.\n                if (!Array.isArray(transformedData)) {\n                    throw new Error(`A schema transformer must return an array of data (key: '${key}', type: '${type}', file: '${source}')`);\n                }\n\n                // eslint-disable-next-line\n                // @ts-ignore\n                return transformedData.flatMap((data: Partial<ProviderDatum<unknown, unknown>> | Partial<ProviderDatum<unknown, unknown>>[]) => {                \n                    // We also introduce another loop so that we can deal\n                    // with the case where multiple items are returned per\n                    // original item\n                    return (Array.isArray(data) ? data : [data]).flatMap((item) => ({\n                        type,\n                        provider,\n                        source,\n                        account,\n                        ...item,\n                    }));\n                });\n            }\n            \n            // In case nothing is transformed, we just insert the data as-is\n            return transformedData.map((data: Partial<ProviderDatum<unknown, unknown>> | Partial<ProviderDatum<unknown, unknown>>[]) => ({\n                type,\n                provider,\n                source,\n                account,\n                data,\n            }));\n        } catch (e) {\n            logger.provider.error(`An error occurred while trying to parse a schema (key: '${key}', type: '${type}', file: '${source})'`, e);\n        }\n    }).flat();\n}\n\nexport default parseSchema;"
  },
  {
    "path": "src/main/lib/unwrap-provider-source.ts",
    "content": "import path from 'path';\n\n/**\n * Unwrap a provider source that is either a string or a string array. As per\n * the provider spec, we need to concatenate a string array with `path.join`.\n */\nfunction unwrapParserSource(source: string | string[]) {\n    return Array.isArray(source)\n        ? path.join(...source)\n        : source;\n}\n\nexport default unwrapParserSource;"
  },
  {
    "path": "src/main/lib/window-store.ts",
    "content": "import { BrowserWindow } from 'electron';\n\nclass WindowStore {\n    private static instance: WindowStore;\n\n    private _window: BrowserWindow;\n\n    static getInstance(): WindowStore {\n        if (!WindowStore.instance) {\n            WindowStore.instance = new WindowStore();\n        }\n\n        return WindowStore.instance;\n    }\n\n    static getWindow(): BrowserWindow {\n        return this.getInstance().window;\n    }\n\n    set window(window: BrowserWindow) {\n        this._window = window;\n    }\n\n    get window(): BrowserWindow {\n        return this._window;\n    }\n}\n\nexport default WindowStore;"
  },
  {
    "path": "src/main/providers/bridge.ts",
    "content": "import Providers, { providers as availableProviders }  from '.';\nimport { ProviderCommands, ProviderEvents } from './types/Events';\nimport { ipcMain, IpcMainInvokeEvent } from 'electron';\nimport WindowStore from 'main/lib/window-store';\nimport { ProviderUnion } from './types/Provider';\nimport logger from 'main/lib/logger';\n\nconst channelName = 'providers';\n\nclass ProviderBridge {\n    providers: Providers = null;\n\n    messageCache: [IpcMainInvokeEvent, number][] = [];\n\n    constructor(providers: Providers) {\n        this.providers = providers;\n        this.providers.on('ready', this.clearMessageCache);\n\n        ipcMain.handle(channelName, this.handleMessage);\n\n        // Subscribe to manager-initated events\n        this.providers.addListener('*', function (...props) {\n            // Log the event\n            logger.provider.info('New event: ' + JSON.stringify(this.event));\n\n            // And pass them on to the app\n            ProviderBridge.send(this.event, ...props);\n        });\n    }\n\n    // eslint-disable-next-line\n    private handleMessage = async (event: IpcMainInvokeEvent, command: number, ...args: any[]): Promise<any> => {\n        // GUARD: Check if the repository is initialised. If not, defer to the\n        // messagecache, so that it can be injected later.\n        if (!this.providers.isInitialised) {\n            this.messageCache.push([event, command]);\n            return;\n        }\n        \n        switch (command) {\n            case ProviderCommands.INITIALISE:\n                return this.providers.initialise(args[0], args[1]);\n            case ProviderCommands.UPDATE:\n                return this.providers.update(args[0]);\n            case ProviderCommands.UPDATE_ALL:\n                return this.providers.updateAll();\n            case ProviderCommands.DISPATCH_DATA_REQUEST:\n                return this.providers.dispatchDataRequest(args[0]);\n            case ProviderCommands.DISPATCH_DATA_REQUEST_TO_ALL:\n                return this.providers.dispatchDataRequestToAll();\n            case ProviderCommands.REFRESH:\n                return this.providers.refresh();\n            case ProviderCommands.GET_AVAILABLE_PROVIDERS:\n                return availableProviders.reduce<Record<string, { requiresEmail: boolean, requiresUrl: boolean, }>>((sum, Client) => {\n                    sum[(Client as unknown as ProviderUnion).key] = {\n                        requiresEmail: (Client as unknown as ProviderUnion).requiresEmail,\n                        requiresUrl: (Client as unknown as ProviderUnion).requiresUrl,\n                    };\n                    return sum;\n                }, {});\n            case ProviderCommands.GET_ACCOUNTS:\n                return {\n                    lastChecked: this.providers.lastDataRequestCheck?.toString(),\n                    accounts: Object.fromEntries(this.providers.accounts),\n                };\n        }\n    };\n\n    private clearMessageCache = (): void => {\n        this.messageCache.forEach((args) => this.handleMessage(...args));\n        this.messageCache = [];\n    };\n\n    /**\n     * Send an event to the renderer\n     * @param event The event to send out\n     */\n    public static send(event: ProviderEvents, ...props: unknown[]): void {\n        const window = WindowStore.getInstance().window;\n        window?.webContents.send(channelName, event, ...props);\n    }\n}\n\nexport default ProviderBridge;"
  },
  {
    "path": "src/main/providers/facebook/index.ts",
    "content": "import { app } from 'electron';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport { ProviderFile } from '../types';\nimport { DataRequestProvider } from '../types/Provider';\nimport path from 'path';\nimport fs from 'fs';\nimport AdmZip from 'adm-zip';\n\nconst requestSavePath = path.join(app.getAppPath(), 'data');\n\nclass Facebook extends DataRequestProvider {\n    public static key = 'facebook';\n\n    public static dataRequestIntervalDays = 5;\n\n    public static requiresEmailAccount = false;\n\n    /**\n     * The parameters to be stored for the secure windows\n     */\n    windowParams = {\n        key: this.windowKey,\n        origin: 'facebook.com',\n    };\n\n    async initialise(): Promise<string> {\n        await this.verifyLoggedInStatus();\n        return this.getAccountName();\n    }\n\n    /**\n     * Get the account name for the logged-in Facebook account\n     */\n    getAccountName = async (): Promise<string> => {\n        return withSecureWindow<string>(this.windowParams, async (window) => {\n            await window.loadURL('https://www.facebook.com/settings?tab=account&view');\n\n            // Wait for two seconds for React to mount its components and load\n            // some friggin iframes\n            // TODO: Wait for Facebook to implement sound engineering practices\n            await new Promise((resolve) => setTimeout(resolve, 2000));\n\n            return window.webContents.executeJavaScript(`\n                document.body.querySelector('a[href=\"/settings?tab=account&section=email\"]');\n            `); \n        });\n    };\n\n    verifyLoggedInStatus = async (): Promise<Electron.Cookie[]> => {\n        return withSecureWindow<Electron.Cookie[]>(this.windowParams, (window) => {\n            const settingsUrl = 'https://www.facebook.com/settings';\n            window.loadURL('https://www.facebook.com/login.php?next=https%3A%2F%2Fwww.facebook.com%2Fsettings');\n\n            return new Promise((resolve) => {\n                const eventHandler = async (): Promise<void> => {\n                    // Check if we ended up at the page in an authenticated form\n                    if (window.webContents.getURL().startsWith(settingsUrl)) {\n                        // If so, we retrieve the cookies\n                        const cookies = await window.webContents.session.cookies.get({});\n                        \n                        resolve(cookies);\n                    } else if (!window.isVisible()) {\n                        // If not, we'll check if we need to open the window for the\n                        // user to enter their credentials.\n                        window.show();\n                    }\n                };\n\n                window.webContents.on('did-navigate', eventHandler);\n                window.webContents.once('did-finish-load', eventHandler);\n            });\n        });\n    };\n\n    update = async (): Promise<false> => {\n        // NOTE: Updating is not supported by Facebook since it's internal API\n        // is a enormous clusterfuck and cannot be trusted.\n        return false;\n    };\n\n    dispatchDataRequest = async (): Promise<void> => {\n        await this.verifyLoggedInStatus();\n\n        return withSecureWindow<void>(this.windowParams, async (window) => {\n            window.show();\n\n            await new Promise((resolve) => {\n                window.webContents.on('did-finish-load', resolve);\n                window.loadURL('https://www.facebook.com/dyi/?referrer=yfi_settings&tab=new_archive');\n            });\n\n            // Wait for all the iframes to load\n            await new Promise((resolve) => setTimeout(resolve, 2000));\n\n            // Now we must defer the page to the user, so that they can enter their\n            // password. We then listen for a succesfull AJAX call \n            return new Promise((resolve) => {\n                window.webContents.session.webRequest.onBeforeRequest({\n                    urls: [ 'https://www.facebook.com/api/graphql/' ],\n                }, (details, callback) => {\n                    // Parse the upload object that is passed to the GraphQL API\n                    const data = details.uploadData[0]?.bytes?.toString('utf8');\n\n                    // GUARD: If there is not data, we're parsing the wrong requests\n                    if (!data) {\n                        callback({});\n                        return;\n                    }\n\n                    // Then parse the params that are sent to the GraphQL API\n                    const params = new URLSearchParams(data);\n\n                    // Check if we're capturing the right call\n                    if (params && params.get('fb_api_req_friendly_name') === 'DYISubmitRequestMutation') {\n                        // If so, setup a listener to check if the request is\n                        // completed correctly.\n                        resolve();\n                    }\n\n                    callback({});\n                });\n\n                // Ensure that the data request is in JSON format\n                window.webContents.executeJavaScript(`\n                    (async function() {\n                        document.body.querySelector('label[aria-label=Format]').click();\n                        await new Promise((resolve) => setTimeout(resolve, 50));\n                        Array.from(document.querySelectorAll('[role=option]'))\n                            .find((el) => el.textContent === 'JSON')?.click();\n                        await new Promise((resolve) => setTimeout(resolve, 50));\n                        document.body.querySelector('label[aria-label=\"Date range (required)\"]').click();\n                        await new Promise((resolve) => setTimeout(resolve, 50));\n                        Array.from(document.querySelectorAll('[role=option]'))\n                            .find((el) => el.textContent === 'All time')?.click();\n                        await new Promise((resolve) => setTimeout(resolve, 50));\n                        document.body.querySelector('[aria-label=\"Request a download\"]').click();\n                    })()\n                `);\n            });     \n        });\n    };\n\n    async isDataRequestComplete(): Promise<boolean> {\n        await this.verifyLoggedInStatus();\n\n        return withSecureWindow<boolean>(this.windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL('https://www.facebook.com/dyi/?referrer=yfi_settings&tab=all_archives');\n            });\n\n            // Find a download button and make sure no *Pending* spans exist\n            return window.webContents.executeJavaScript(`\n                document.querySelector('div[aria-label=\"Download\"]')\n                    && !Array.from(document.querySelectorAll('span')).map((el) => el.textContent).includes('Pending');\n            `);\n        });\n    }\n\n    async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {\n        return withSecureWindow<ProviderFile[]>(this.windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('dom-ready', resolve);\n                window.loadURL('https://www.facebook.com/dyi/?referrer=yfi_settings&tab=all_archives');\n            });\n\n            const filePath = path.join(requestSavePath, 'facebook.zip');\n            await new Promise((resolve) => {\n                // Create a handler for any file saving actions\n                window.webContents.session.once('will-download', (event, item) => {\n                    // Save the item to the data folder temporarily\n                    item.setSavePath(filePath);\n                    item.once('done', resolve);\n                });\n\n                // And then trigger the button click\n                window.webContents.executeJavaScript(`\n                    document.querySelector('div[aria-label=\"Download\"]').click();\n                `);\n\n                window.show();\n            });\n\n            // We have the ZIP, all that's left to do is unpack it and pipe it to\n            // the repository\n            const zip = new AdmZip(filePath);\n            await new Promise((resolve) => \n                // Fix underway: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59070\n                zip.extractAllToAsync(extractionPath, true, false, resolve),\n            );\n\n            // Translate this into a form that is readable for the ParserManager\n            const files = zip.getEntries().map((entry): ProviderFile => {\n                return {\n                    filepath: entry.entryName,\n                    data: null,\n                    // data: entry.getData(),\n                };\n            });\n\n            // And dont forget to remove the zip file after it's been processed\n            await fs.promises.unlink(filePath);\n\n            return files;\n        });\n    }\n}\n\nexport default Facebook;"
  },
  {
    "path": "src/main/providers/facebook/parser.ts",
    "content": "import { EducationExperience, Employment, EventResponse, MobileDevice, OffSiteActivity, ProvidedDataTypes, ProviderParser, SearchQuery, VisitedPage } from '../types/Data';\nimport path from 'path';\n\nconst parsers: ProviderParser[] = [\n    {\n        source: path.join('about_you', 'friend_peer_group.json'),\n        schemas: [{\n            type: ProvidedDataTypes.PEER_GROUP,\n            key: 'friend_peer_group',\n        }],\n    },\n    {\n        source: path.join('about_you', 'messenger.json'),\n        schemas: [\n            {\n                key: 'CITY',\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n            },\n            {\n                key: 'COUNTRY',\n                type: ProvidedDataTypes.COUNTRY,\n            },\n            {\n                key: 'GENDER',\n                type: ProvidedDataTypes.GENDER,\n            },\n            {\n                key: 'EMAIL',\n                type: ProvidedDataTypes.EMAIL,\n            },\n            {\n                type: ProvidedDataTypes.EMPLOYMENT,\n                transformer: (data: any): Partial<Employment>[] => {\n                    return [{\n                        data: {\n                            jobTitle: data?.messenger?.autofill_information?.JOB_TITLE,\n                            company: data?.messenger?.autofill_information?.COMPANY_NAME,\n                        },\n                    }];\n                },\n            },\n        ],\n    },\n    {\n        source: path.join('about_you', 'preferences.json'),\n        schemas: [\n            {\n                type: ProvidedDataTypes.USER_LANGUAGE,\n                key: 'value',\n            },\n        ],\n    },\n    {\n        source: path.join('about_you', 'visited.json'),\n        schemas: [\n            {\n                key: 'entries',\n                type: ProvidedDataTypes.VISITED_PAGE,\n                transformer: (entry: any): Partial<VisitedPage> => {\n                    return {\n                        data: {\n                            name: entry?.data?.name,\n                            uri: entry?.data?.uri,\n                        },\n                        timestamp: entry.timestamp && new Date(entry.timestamp * 1000).toString(),\n                    };\n                },\n            },\n        ],\n    },\n    {\n        source: path.join('ads_and_businesses', 'ads_interests.json'),\n        schemas: [\n            {\n                key: 'topics',\n                type: ProvidedDataTypes.AD_INTEREST,\n            },\n        ],\n    },\n    {\n        source: path.join('ads_and_businesses', 'your_off-facebook_activity.json'),\n        schemas: [\n            {\n                key: 'off_facebook_activity',\n                type: ProvidedDataTypes.OFF_SITE_ACTIVITY,\n                transformer: (website: any): Partial<OffSiteActivity>[] => {\n                    return website.events.map((event: any): Partial<OffSiteActivity> => {\n                        return {\n                            data: {\n                                website: website.name,\n                                type: event.type,\n                            },\n                            timestamp: new Date(event.timestamp * 1000).toString(),\n                        };\n                    });\n                },\n            },\n        ],\n    },\n    {\n        source: path.join('events', 'your_event_responses.json'),\n        schemas: [\n            {\n                key: 'events_interested',\n                type: ProvidedDataTypes.EVENT_RESPONSE,\n                transformer: (data: any): Partial<EventResponse> => {\n                    return {\n                        data: {\n                            name: data.name,\n                            response: 'interested',\n                        },\n                    };\n                },\n            },\n        ],\n    },\n    {\n        source: path.join('likes_and_reactions', 'pages.json'),\n        schemas: [\n            {\n                key: 'name',\n                type: ProvidedDataTypes.LIKE,\n            },\n        ],\n    },\n    {\n        source: path.join('location', 'primary_location.json'),\n        schemas: [\n            {\n                key: 'city_region_pairs',\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n            },\n            {\n                key: 'zipcode',\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n            },\n        ],\n    },\n    {\n        source: path.join('location', 'timezone.json'),\n        schemas: [\n            {\n                key: 'timezone',\n                type: ProvidedDataTypes.TIMEZONE,\n            },\n        ],\n    },\n    {\n        source: path.join('payment_history', 'payment_history.json'),\n        schemas: [\n            {\n                key: 'preferred_currency',\n                type: ProvidedDataTypes.CURRENCY,\n            },\n        ],\n    },\n    {\n        source: path.join('profile_information', 'profile_information.json'),\n        schemas: [\n            {\n                key: 'full_name',\n                type: ProvidedDataTypes.FULL_NAME,\n            },\n            {\n                key: 'first_name',\n                type: ProvidedDataTypes.FIRST_NAME,\n            },\n            {\n                key: 'last_name',\n                type: ProvidedDataTypes.LAST_NAME,\n            },\n            {\n                key: 'emails',\n                type: ProvidedDataTypes.EMAIL,\n                transformer: (data: any) => {\n                    return Object.keys(data).reduce((emails, key): string[] => {\n                        emails.push(...data[key]);\n                        return emails;\n                    }, []).map((email) => {\n                        return {\n                            data: email,\n                        };\n                    });\n                },\n            },\n            {\n                key: ['current_city', 'hometown'],\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n                transformer: (data: any) => {\n                    return [{\n                        data: data.name,\n                    }];\n                },\n            },\n            {\n                key: ['gender_option', 'pronoun'],\n                type: ProvidedDataTypes.GENDER,\n            },\n            {\n                key: 'education_experiences',\n                type: ProvidedDataTypes.EDUCATION_EXPERIENCE,\n                transformer: (experience: any): Partial<EducationExperience> => {\n                    return {\n                        data: {\n                            institution: experience.name,\n                            type: experience.school_type,\n                            graduated: experience.graduated,\n                        },\n                    };\n                },\n            },\n            {\n                key: 'work_experiences',\n                type: ProvidedDataTypes.EMPLOYMENT,\n                transformer: (experience: any): Partial<Employment>  => {\n                    return {\n                        data: {\n                            jobTitle: experience.title,\n                            company: experience.employer,\n                        },\n                    };\n                },\n            },\n            {\n                key: 'registration_timestamp',\n                type: ProvidedDataTypes.REGISTRATION_DATE,\n                transformer: (timestamp: number) => [{ data: new Date(timestamp * 1000).toString() }],\n            },\n        ],\n    },\n    {\n        source: path.join('search_history', 'your_search_history.json'),\n        schemas: [{\n            key: 'searches',\n            type: ProvidedDataTypes.SEARCH_QUERY,\n            transformer: (query: any): Partial<SearchQuery> => {\n                return {\n                    data: query.data.reduce((sum: string, q: any) => sum + q.text, ''),\n                    timestamp: new Date(query.timestamp * 1000).toString(),\n                };\n            },\n        }],\n    },\n    {\n        source: path.join('security_and_login_information', 'mobile_devices.json'),\n        schemas: [{\n            key: 'devices',\n            type: ProvidedDataTypes.MOBILE_DEVICE,\n            transformer: (device: any): Partial<MobileDevice> => {\n                return {\n                    data: {\n                        type: device.type,\n                        os: device.os,\n                        advertiser_id: device.advertiser_id,\n                        device_locale: device.device_locale,\n                    },\n                    timestamp: new Date(device.update_time * 1000).toString(),\n                };\n            },\n        }],\n    },\n    {\n        source: path.join('security_and_login_information', 'user_ip_addresses.json'),\n        schemas: [{\n            key: 'user_ip_address',\n            type: ProvidedDataTypes.IP_ADDRESS,\n            transformer: (entry: any) => {\n                return {\n                    data: entry.ip,\n                    timestamp: new Date(entry.timestamp * 1000).toString(),\n                };\n            },\n        }],\n    },\n];\n\nexport default parsers;"
  },
  {
    "path": "src/main/providers/index.ts",
    "content": "import { differenceInDays } from 'date-fns';\nimport path from 'path';\nimport crypto from 'crypto';\nimport { EventEmitter2 } from 'eventemitter2';\n\nimport { ProviderFile, \n    ProviderUpdateType, \n    InitOptionalParameters, \n    InitialisedAccount, \n} from './types';\nimport { Provider, \n    DataRequestProvider, \n    EmailDataRequestProvider, \n    OpenDataRightsProvider, \n    UninstatiatedProvider,\n} from './types/Provider';\nimport {\n    AccountCreated,\n    DataRequestCompleted,\n    DataRequestDispatched,\n    ProviderEvents, \n    UpdateComplete,\n} from './types/Events';\n\n\nimport PersistedMap from 'main/lib/persisted-map';\nimport store from 'main/store';\nimport EmailManager from 'main/email-client';\nimport Repository from '../lib/repository';\nimport ProviderBridge from './bridge';\n\nimport Facebook from './facebook';\nimport LinkedIn from './linkedin';\nimport Spotify from './spotify';\nimport Instagram from './instagram';\nimport OpenDataRights from './open-data-rights';\nimport logger from 'main/lib/logger';\nimport { repositoryPath } from 'main/lib/constants';\n\nexport const providers: Array<UninstatiatedProvider> = [\n    Instagram,\n    Facebook,\n    LinkedIn,\n    Spotify,\n    OpenDataRights,\n];\n\nconst mapProviderToKey = providers.reduce<Record<string, UninstatiatedProvider>>((sum, provider) => {\n    sum[(provider as unknown as typeof Provider).key] = provider;\n    return sum;\n}, {});\n\nclass ProviderManager extends EventEmitter2 {\n    // Refers to the repository obejct\n    repository: Repository;\n\n    // The email manager\n    email: EmailManager;\n\n    // Whether the manager is initialised\n    isInitialised = false;\n\n    // Contains all provider instances\n    instances: Map<string, Provider & Partial<DataRequestProvider>> = new Map();\n\n    // Contains the keys of all providers that have been initialised by the user\n    accounts: PersistedMap<string, InitialisedAccount>;\n\n    // The last time the data requests were checked \n    lastDataRequestCheck: Date;\n\n    constructor(repository: Repository, email: EmailManager) {\n        super({ wildcard: true });\n\n        // Store all instances of other classes\n        this.repository = repository;\n        this.email = email;\n\n        // Construct the initialised providers from the store\n        const retrievedAccounts = store.get('provider-accounts', []) as [string, InitialisedAccount][];\n        this.accounts = new PersistedMap(retrievedAccounts, (map) => {\n            store.set('provider-accounts', Array.from(map));\n        });\n\n        // Then create instances for each provider that is retrieved from the store\n        this.accounts.forEach((account, key) => {\n            const ProviderClass = mapProviderToKey[account.provider];\n\n            // GUARD: If the provider hinges on email, we must inject the client\n            // into the class\n            const instance = new ProviderClass(account.windowKey, account.account);\n            if (instance instanceof EmailDataRequestProvider) {\n                const emailAccount = this.email.emailClients.get(account.account);\n                \n                if (!emailAccount) {\n                    logger.email.error(`Email account (${account.account}) used to initialize a provider is no longer available...`);\n                    return;\n                }\n\n                instance.setEmailClient(emailAccount);\n            } \n\n            // GUARD: If the provider based on an API, we must inject in into\n            // the provider\n            if (instance instanceof OpenDataRightsProvider) {\n                instance.setUrl(account.url);\n            }\n\n            this.instances.set(key, instance);\n        });\n\n        // Then we create a timeout function that checks for completed data\n        // requests every five minutes. Also immediately commence with queueing\n        // the refresher\n        setInterval(this.refresh, 300_000);\n        this.refresh();\n\n        // Then initialise all classes\n        // And after send out a ready event\n        this.isInitialised = true;\n        this.emit(ProviderEvents.READY);\n    }\n\n    /**\n     * Update all providers\n     */\n    updateAll = async (): Promise<void> => {\n        // Loop through all registered providers and execute their updates\n        await Promise.allSettled(this.instances.map((provider, key) => \n            this.update(key),\n        ));\n    };\n\n    /**\n     * Initialise a new provider account. This will return the unique key for\n     * the account that has just been created.\n     * @param key \n     */\n    initialise = async (provider: string, optional: InitOptionalParameters): Promise<string> => {\n        logger.provider.info(`Attempting to initialise a new ${provider} (${optional.accountName})`);\n        // Generate a random string that is used to refer to the sessions for\n        // this particular account\n        const windowKey = crypto.randomBytes(32).toString('hex');\n\n        if (!(provider in mapProviderToKey)) {\n            throw new Error('No provider registered with name');\n        }\n\n        // Call the respective initialise function\n        const instance = new mapProviderToKey[provider](windowKey, optional.accountName);\n\n        // GUARD: If we are dealing with a provider that implements email, we\n        // must inject an email client into the class\n        if (instance instanceof EmailDataRequestProvider) {\n            // Retrieve an email client that matches the supplied email address\n            const emailAccount = this.email.emailClients.get(optional.accountName);\n                \n            // GUARD: The address must actually exist\n            if (!optional.accountName || !emailAccount) {\n                throw new Error('Could not find email client withs suppled account name...');\n            }\n\n            // Inject the client into the provider\n            instance.setEmailClient(emailAccount);\n        }\n\n        // GUARD: If we are dealing with a provider that implements the Open\n        // Data Rights API, we must inject the URL into the provider\n        if (instance instanceof OpenDataRightsProvider) {\n            // Attempt to parse the URL. If it's not a valid URL, this\n            // should throw.\n            new URL(optional.apiUrl);\n            // Then set the URL with trailing slashes removed\n            instance.setUrl(optional.apiUrl.replace(/\\/+$/, ''));\n        }\n\n        // Then initialise the provider\n        const account = await instance.initialise();\n\n        // GUARD: Check if the instance has correctly returned an account name\n        if (!account) {\n            throw new Error('Initialising provider did not return account name');\n        }\n\n        const hostname = optional.apiUrl\n            ? new URL(optional.apiUrl).host\n            : undefined;\n\n        // Save the key to the accounts array\n        const key = optional.apiUrl\n            ? `${provider}_${hostname}_${account}`\n            : `${provider}_${account}`;\n\n        // Construct the details for the provider\n        const newAccount: InitialisedAccount = {\n            account,\n            provider,\n            windowKey,\n            url: optional.apiUrl && optional.apiUrl.replace(/\\/+$/, ''),\n            hostname,\n            status: {},\n        };\n\n        // Save the instance as well\n        this.accounts.set(key, newAccount);\n        this.instances.set(key, instance);\n\n        // Emit event\n        this.emit(ProviderEvents.ACCOUNT_CREATED, newAccount as AccountCreated);\n\n        return key;\n    };\n\n    /**\n     * Update a single service, either by key (ie `instagram`) or index\n     */\n    update = async (key: string): Promise<void> => {\n        // Retrieve the instance based on whether the supplied argument is an\n        // index or class key\n        const instance = this.instances.get(key);\n\n        // GUARD: Check if we've found an instance\n        if (!instance) {\n            throw new Error('NotFoundError');\n        }\n\n        // GUARD: Check if the provider is already initialised\n        if (!this.accounts.has(key)) {\n            throw new Error('ProviderWasNotInitialised');\n        }\n\n        // Execute individual update, which should return a list of files to\n        // be saved to disk\n        const files = await instance.update();\n\n        // GUARD: If the functino returns false, the provider does not implement\n        // an update flow, and we end the function\n        if (files === false) {\n            return;\n        }\n\n        // Alternatively, we save the files and attempt to commit\n        const commit = await this.saveFilesAndCommit(\n            files,\n            key,\n            `Auto-update ${new Date().toLocaleString()}`,\n            ProviderUpdateType.UPDATE,\n        );\n        \n        // GUARD: Only log stuff if new data is found\n        if (commit.changedFiles) {\n            const event: UpdateComplete = {\n                ...this.accounts.get(key),\n                ...commit,\n            };\n            this.emit(ProviderEvents.UPDATE_COMPLETE, event);\n        }\n    };\n\n    /**\n     * Save a bunch of files and auto-commit the result\n     */\n    saveFilesAndCommit = async (\n        files: ProviderFile[],\n        key: string, \n        message: string,\n        updateType: ProviderUpdateType,\n    ): Promise<{ changedFiles: number; commitHash: string; }> => {\n        const account = this.accounts.get(key);\n        logger.provider.info(`Saving and committing files for ${account.account} [${account.provider}]...`);\n\n        // Then store all files using the repositor save and add handler\n        await Promise.all(files.map(async (file: ProviderFile): Promise<void> => {\n            // Prepend the supplied path with the key from the specific service\n            const location = account.hostname\n                ? `${account.provider}/${account.hostname}/${account.account}/${file.filepath}`\n                : `${account.provider}/${account.account}/${file.filepath}`;\n\n            // Save the files to disk, and add the files\n            if (file.data) {\n                await this.repository.save(location, file.data);\n            }\n            await this.repository.add(location);\n        })).catch(console.error);\n\n        // Retrieve repository status and check if any files have actually changed\n        const status = await this.repository.status();\n\n        // GUARD: We must check if the changed files have been added to the\n        // index, as they will not be part of a commit when it is made.\n        const changedFiles = status.filter((file) => file.inIndex());\n        logger.provider.info('Files changed: ' + JSON.stringify(changedFiles));\n\n        // GUARD: If no files have changed, it is no longer neccessary to create\n        // a new commit.\n        if (!changedFiles.length) {\n            logger.provider.info('No files have changed since last data request, skipping commit.');\n            return;\n        }\n\n        // Gather the set of data that is to be appended to the commit\n        const messageData: Record<string, string> = {\n            'Aeon-Provider': account.provider,\n            'Aeon-Account': account.account,\n            'Aeon-Update-Type': updateType,\n        };\n\n        // Also add optional parameters\n        if (account.url && account.hostname) {\n            messageData['Aeon-Provider-Hostname'] = account.hostname;\n            messageData['Aeon-Provider-URL'] = account.url;\n        }\n\n        // Parse the object as a series of \"key: value \\n\" statements\n        const augmentedMessage = Object.keys(messageData).reduce((sum, k) => {\n            return `${sum}\\n${k}: ${messageData[k]}`;\n        }, message);\n\n        // Acutally create the commit\n        logger.provider.info('Creating commit: ' + message);\n        const commit = await this.repository.commit(augmentedMessage);\n\n        return {\n            changedFiles: changedFiles.length,\n            commitHash: commit,\n        };\n    };\n\n    /**\n     * Do a data request for a single instance\n     */\n    dispatchDataRequest = async (key: string): Promise<void> => {\n        // Retrieve the instance based on whether the supplied argument is an\n        // index or class key\n        const instance = this.instances.get(key);\n        const account = this.accounts.get(key);\n\n        // GUARD: Check if we've found an instance\n        if (!instance) {\n            throw new Error('NotFoundError');\n        }\n\n        // GUARD: Check if the provider is already initialised\n        if (!this.accounts.has(key)) {\n            throw new Error('ProviderWasNotInitialised');\n        }\n\n        // GUARD: Check if the instance supports data request dispatching\n        if (!(instance instanceof DataRequestProvider)) {\n            throw new Error('DataRequestNotSupportedError');\n        }\n\n        // GUARD: Check if a data request hasn't already been dispatched\n        if (account.status.dispatched && !account.status.completed) {\n            throw new Error('DataRequestAlreadyInProgress');\n        }\n\n        // Dispatch the request and wait for it to complete\n        const requestId = await instance.dispatchDataRequest();\n\n        // Then store the update time\n        account.status = {};\n        account.status.dispatched = new Date().toString();\n        if (requestId) account.status.requestId = requestId;\n        this.accounts.set(key, account);\n\n        this.emit(ProviderEvents.DATA_REQUEST_DISPATCHED, account as DataRequestDispatched);\n        logger.provider.info('Dispatched data request for ' + key);\n    };\n\n    /**\n     * Dispatch data requests for all instances that support it\n     */\n    dispatchDataRequestToAll = async (): Promise<void> => {\n        await Promise.allSettled(this.instances.map((instance, key) =>\n            this.dispatchDataRequest(key),\n        ));\n    };\n\n    refresh = async (): Promise<void> => {\n        // Send out an event so the front-end knows we are busy checking\n        // outstanding data requests\n        ProviderBridge.send(ProviderEvents.CHECKING_DATA_REQUESTS);\n        logger.provider.info('Checking for completed data requests...');\n\n        const dataRequests = Promise.all(this.accounts.map(async (account, key): Promise<void> => {\n            const instance = this.instances.get(key);\n\n            // GUARD: If we cannot find an instance for this provider type, we\n            // skip it\n            if (!instance) {\n                return;\n            }\n\n            // GUARD: If there is not active request for this account, we don't\n            // need to force it\n            if (!account.status.dispatched) {\n                return;\n            }\n\n            // GUARD: If a request has already been completed, we do not need to\n            // check upon it further\n            if (account.status.completed) {\n                // However, we will check if we need to purge it from the map if\n                // it has been completed for x days\n                const ProviderClass: typeof DataRequestProvider = Object.getPrototypeOf(instance).constructor;\n                if (differenceInDays(new Date(), new Date(account.status.completed)) > ProviderClass.dataRequestIntervalDays) {\n                    logger.provider.info(`Data request for ${key} was completed long enough to be purged`);\n                    account.status = {};\n                    this.accounts.set(key, account);\n                } \n                    \n                return;\n            }\n\n            // If it is uncompleted, we need to check upon it\n            if (await instance.isDataRequestComplete(account.status.requestId).catch(logger.provider.error)) {\n                logger.provider.info('A data request has completed! Starting to parse...');\n\n                // If it is complete now, we'll fetch the data and parse it\n                const dirPath = account.url && account.hostname\n                    ? path.join(repositoryPath, account.provider, account.hostname,  account.account)\n                    : path.join(repositoryPath, account.provider, account.account);\n                const files = await instance.parseDataRequest(dirPath, account.status.requestId);\n                const commit = await this.saveFilesAndCommit(\n                    files,\n                    key,\n                    `Data Request [${key}] ${new Date().toLocaleString()}`,\n                    ProviderUpdateType.DATA_REQUEST,\n                );\n                \n                // Set the flag for completion\n                account.status.lastCheck = new Date().toString();\n                account.status.completed = new Date().toString();\n                this.accounts.set(key, account);\n\n                // Emit an event for completion\n                const event: DataRequestCompleted = {\n                    ...account,\n                    ...commit,\n                };\n                this.emit(ProviderEvents.DATA_REQUEST_COMPLETED, event);\n\n                return;\n            }\n\n            account.status.lastCheck = new Date().toString();\n            this.accounts.set(key, account);\n        }));\n\n        // Also dispatch regular update requests\n        await(Promise.allSettled([dataRequests, await this.updateAll()]));\n        this.lastDataRequestCheck = new Date();\n        logger.provider.info('Check completed.');\n    };\n\n}\n\nexport default ProviderManager;\n"
  },
  {
    "path": "src/main/providers/instagram/index.ts",
    "content": "import { ProviderFile } from '../types';\nimport { EmailDataRequestProvider } from '../types/Provider';\nimport crypto from 'crypto';\nimport path from 'path';\nimport fetch from 'node-fetch';\nimport { app } from 'electron';\nimport scrapingUrls from './urls.json';\nimport AdmZip from 'adm-zip';\nimport fs from 'fs';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport logger from 'main/lib/logger';\n\nconst requestSavePath = path.join(app.getAppPath(), 'data');\n\nconst downloadUrlRegex = /https:\\/\\/www\\.instagram\\.com\\/dyi\\/download\\/auth\\/([a-zA-Z\\d]+)\\/\\?dyi_job_id=([a-zA-Z\\d]+)/;\n\nclass Instagram extends EmailDataRequestProvider {\n    public static key = 'instagram';\n\n    public static dataRequestIntervalDays = 5;\n\n    public static requiresEmailAccount = false;\n\n    /**\n     * The parameters to be stored for the secure windows\n     */\n    windowParams = {\n        key: this.windowKey,\n        origin: 'instagram.com',\n    };\n\n    async initialise(): Promise<string> {\n        await this.verifyLoggedInStatus();\n        return this.getAccountName();\n    }\n\n    /**\n     * Retrieve the username for this account\n     */\n    async getAccountName(): Promise<string> {\n        return withSecureWindow<string>(this.windowParams, async (window) => {\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL('https://www.instagram.com/accounts/edit/');\n            });\n\n            return window.webContents.executeJavaScript(`\n                document.querySelector('input#pepEmail').value\n            `) as Promise<string>;\n        });\n    }\n\n    verifyLoggedInStatus = async (): Promise<Electron.Cookie[]> => {\n        return withSecureWindow<Electron.Cookie[]>(this.windowParams, (window) => {\n            // Load a URL in the browser, and see if we get redirected or not\n            const profileUrl = 'https://www.instagram.com/accounts/access_tool/ads_interests';\n            window.loadURL(profileUrl);\n\n            // TODO: Introduce optimisation so that we don't have to issue the\n            // request every time\n            return new Promise((resolve) => {\n                const eventHandler = async (): Promise<void> => {\n                    // Check if we ended up at the right page\n                    if (profileUrl === window.webContents.getURL()) {\n                        // If we did, we can siphon off the cookies from this page\n                        // for API requests\n                        const cookies = await window.webContents.session.cookies.get({});\n                        \n                        // Do a check if the language is set to English, and if not,\n                        // change it to English\n                        const lang = cookies.find((cookie) => cookie.name === 'ig_lang');\n                        if (lang?.value !== 'en') {\n                            await window.webContents.session.cookies.set({ \n                                url: 'https://instagram.com',\n                                name: 'ig_lang',\n                                value: 'en',\n                                domain: '.instagram.com',\n                                secure: true,\n                                expirationDate: Math.pow(2, 31) - 1,\n                            });\n                        }\n\n                        // We can then return the cookies and clean up the window\n                        resolve(cookies);\n                    } else if (!window.isVisible()) {\n                        // If not, we'll check if we need to open the window for the\n                        // user to enter their credentials.\n                        window.show();\n                    }\n                };\n\n                window.webContents.on('did-navigate', eventHandler);\n                window.webContents.once('did-finish-load', eventHandler);\n            });\n        });\n    };\n\n    update = async (): Promise<ProviderFile[]> => {\n        const cookies = await this.verifyLoggedInStatus();\n\n        // We extract the right cookies, and create a config we can then\n        // use for successive requests\n        const sessionid = cookies.find((cookie) => cookie.name === 'sessionid').value;\n        // const shbid = this.cookies.find(cookie => cookie.name === 'shbid').value;\n        const fetchConfig =  {\n            method: 'GET',\n            headers: {\n                Accept: 'application/json',\n                Referer: 'https://www.instagram.com/accounts/access_tool/ads_interests',\n                'X-CSRFToken': crypto.randomBytes(20).toString('hex'),\n                cookie: `sessionid=${sessionid}; shbid=${''}`,\n            },\n        };\n\n        // Now we do all API requests in order to retrieve the data\n        const responses = await Promise.all(\n            scrapingUrls.map((url) => \n                fetch(url, fetchConfig).then((response) => response.json()),\n            ),\n        );\n\n        // We then transform the data so that we can return it to the handler\n        return responses.map((response: any) => {\n            return {\n                filepath: `${response.page_name}.json`,\n                data: JSON.stringify(response.data.data, null, 4),\n            };\n        });\n    };\n\n    async dispatchDataRequest(): Promise<number> {\n        await this.verifyLoggedInStatus();\n\n        return withSecureWindow<number>(this.windowParams, async (window) => {\n            // Load the dispatched window\n            window.hide();\n            await new Promise((resolve) => {\n                window.webContents.on('did-finish-load', resolve);\n                window.loadURL('https://www.instagram.com/download/request/');\n            });\n\n            // Set the output format to JSON\n            window.webContents.executeJavaScript(`\n                document.querySelector('input[value=\"JSON\"]')?.click();\n            `);\n\n            // We'll click the button for the user, but we'll need to defer to the\n            // user for a password\n            window.webContents.executeJavaScript(`\n                Array.from(document.querySelectorAll('button'))\n                    .find(el => el.textContent === 'Next')\n                    .click?.()\n            `);\n            window.show();\n\n            // Now we must defer the page to the user, so that they can enter their\n            // password. We then listen for a succesfull AJAX call \n            return new Promise((resolve) => {\n                window.webContents.session.webRequest.onCompleted({\n                    urls: [ 'https://www.instagram.com/api/v1/web/download/request_download_data_ajax/' ],\n                }, (details: Electron.OnCompletedListenerDetails) => {\n                    if (details.statusCode === 200) {\n                        // Return the current UNIX time so we can filter on emails later\n                        resolve(Math.floor(new Date().getTime() / 1000));\n                    }\n                });\n            });             \n        });\n    }\n\n    async parseEmailForDownloadUrl(date: number): Promise<string | undefined> {\n        // Attempt to retrieve emails\n        const emails = await this.email.findMessages({\n            from: 'security@mail.instagram.com',\n            after: date,\n        });\n\n        let match: string | undefined;\n        for (const email of emails) {\n            // GUARD: Check that the email has a body\n            if (!email.html) {\n                continue;\n            }\n\n            const matches = email.html\n                // Replace any weird line endinges\n                .replace('=\\n', '')\n                // Then check if there's a download URL in there\n                .match(downloadUrlRegex);\n\n            if (Array.isArray(matches)) {\n                match = matches[0];\n            }\n\n            // GUARD: If a match is found, break the loop\n            if (match) {\n                break;\n            }\n        }\n\n        return match;\n    }\n\n    async isDataRequestComplete(date: number): Promise<boolean> {\n        await this.verifyLoggedInStatus();\n        return !!(await this.parseEmailForDownloadUrl(date));\n    }\n\n    async parseDataRequest(extractionPath: string, date: number): Promise<ProviderFile[]> {\n        const downloadUrl = await this.parseEmailForDownloadUrl(date);\n\n        if (!downloadUrl) {\n            throw new Error('Couldn\\'t parse download URL from email');\n        }\n\n        return withSecureWindow<ProviderFile[]>(this.windowParams, async (window) => {\n            logger.provider.info('Started parsing request');\n\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL(downloadUrl);\n            });\n\n            // Show the reauthentication window to the user\n            await new Promise<void>((resolve) => {\n                window.webContents.on('did-navigate', (event, url) => {\n                    if (url.startsWith('https://www.instagram.com/download/confirm')) {\n                        resolve();\n                    }\n                });\n                window.show();\n            });\n\n            // Now that we're successfully authenticated on the data download page,\n            // the only thing we have to do is download the data.\n            const filePath = path.join(requestSavePath, 'instagram.zip');\n            await new Promise((resolve) => {\n                // Create a handler for any file saving actions\n                window.webContents.session.once('will-download', (event, item) => {\n                    // Save the item to the data folder temporarily\n                    item.setSavePath(filePath);\n                    item.once('done', resolve);\n                });\n\n                // And then trigger the button click\n                window.webContents.executeJavaScript(`\n                    Array.from(document.querySelectorAll('button'))\n                        .find(el => el.textContent === 'Download information')\n                        ?.click?.()\n                `);\n            });\n\n            // We have the ZIP, all that's left to do is unpack it and pipe it to\n            // the repository\n            const zip = new AdmZip(filePath);\n            await new Promise((resolve) => \n                // Fix underway: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59070\n                zip.extractAllToAsync(extractionPath, true, false, resolve),\n            );\n\n            // Translate this into a form that is readable for the ParserManager\n            const files = zip.getEntries().map((entry): ProviderFile => {\n                return {\n                    filepath: entry.entryName,\n                    data: null,\n                };\n            });\n\n            // And dont forget to remove the zip file after it's been processed\n            await fs.promises.unlink(filePath);\n\n            return files;\n        });\n    }\n}\n\nexport default Instagram;"
  },
  {
    "path": "src/main/providers/instagram/parser.ts",
    "content": "import {\n    ProvidedDataTypes,\n    ProviderParser,\n    Follower,\n    AccountFollowing,\n    Photo,\n    PrivacySetting,\n    PostSeen,\n} from '../types/Data';\nimport { objectToKeyValueTransformer } from 'main/lib/map-object-to-key-value';\nimport path from 'path';\nimport Instagram from '.';\nimport { repositoryPath } from 'main/lib/constants';\n\n/**\n * This specifies an input object in which the data is structured in an object,\n * in which the keys represent usernames, and the values are ISO dates. These\n * can be iterated upon to extract the desired data\n */\ntype GenericKeyedData = {\n    [key: string]: string\n};\n\nconst parsers: ProviderParser[] = [\n    {\n        source: 'account_history.json',\n        schemas: [\n            {\n                type: ProvidedDataTypes.SESSION,\n                key: 'login_history',\n                // eslint-disable-next-line\n                transformer: (data: any[]): (Array<{ data: any }>) => data.map(d => ({ data: d })),\n            },\n        ],\n    },\n    {\n        source: 'accounts_following_you.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.FOLLOWER,\n            },\n        ],\n    },\n    {\n        source: 'accounts_following_you.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.FOLLOWER,\n            },\n        ],\n    },\n    {\n        source: 'accounts_you_follow.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.ACCOUNT_FOLLOWING,\n            },\n        ],\n    },\n    {\n        source: 'ads_interests.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.AD_INTEREST,\n            },\n        ],\n    },\n    {\n        source: 'ads_interests.json',\n        schemas: [\n            {\n                key: 'text',\n                type: ProvidedDataTypes.AD_INTEREST,\n            },\n        ],\n    },\n    {\n        source: 'connections.json',\n        schemas: [\n            {\n                key: 'followers',\n                type: ProvidedDataTypes.FOLLOWER,\n                transformer: (obj: GenericKeyedData): Partial<Follower>[] => {\n                    return Object.keys(obj).map((key): Partial<Follower> => {\n                        return {\n                            data: key,\n                            timestamp: obj[key],\n                        };\n                    });\n                },\n            }, {\n                key: 'following',\n                type: ProvidedDataTypes.ACCOUNT_FOLLOWING,\n                transformer: (obj: GenericKeyedData): Partial<AccountFollowing>[] => {\n                    return Object.keys(obj).map((key): Partial<AccountFollowing> => {\n                        return {\n                            data: key,\n                            timestamp: obj[key],\n                        };\n                    });\n                },\n            },\n        ],\n    },\n    {\n        source: 'information_about_you.json',\n        schemas: [\n            {\n                key: 'city_name',\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n            },\n        ],\n    },\n    {\n        source: 'media.json',\n        schemas: [\n            {\n                key: 'profile',\n                type: ProvidedDataTypes.PROFILE_PICTURE,\n                transformer: (obj: { caption: string; taken_at: string; path: string }[]): Partial<Photo>[] => {\n                    return obj.map((photo): Partial<Photo> => ({\n                        data: {\n                            url: 'file://' + path.join(repositoryPath, Instagram.key, photo.path),\n                            description: photo.caption,\n                        },\n                        timestamp: photo.taken_at,\n                    }));\n                },\n            },\n            {\n                key: 'photos',\n                type: ProvidedDataTypes.PHOTO,\n                transformer: (obj: { caption: string; taken_at: string; path: string }[]): Partial<Photo>[] => {\n                    return obj.map((photo): Partial<Photo> => ({\n                        data: {\n                            url: 'file://' + path.join(repositoryPath, Instagram.key, photo.path),\n                            description: photo.caption,\n                        },\n                        timestamp: photo.taken_at,\n                    }));\n                },\n            },\n        ],\n    }, \n    {\n        source: 'profile.json',\n        schemas: [\n            {\n                key: 'date_joined',\n                type: ProvidedDataTypes.JOIN_DATE,\n                // eslint-disable-next-line\n                transformer: (date: string): any => ({ timestamp: date })\n            }, \n            {\n                key: 'email',\n                type: ProvidedDataTypes.EMAIL,\n            }, \n            {\n                key: 'gender',\n                type: ProvidedDataTypes.GENDER,\n            },\n            {\n                key: 'private_account',\n                type: ProvidedDataTypes.PRIVACY_SETTING,\n                transformer: (value: boolean): Partial<PrivacySetting> => ({ \n                    data: {\n                        key: 'private_account',\n                        value,\n                    },\n                }),\n            },\n            {\n                key: 'name',\n                type: ProvidedDataTypes.FULL_NAME,\n            },\n            {\n                key: 'profile_pic_url',\n                type: ProvidedDataTypes.PROFILE_PICTURE,\n            },\n            {\n                key: 'username',\n                type: ProvidedDataTypes.USERNAME,\n            },\n            {\n                key: 'date_of_birth',\n                type: ProvidedDataTypes.DATE_OF_BIRTH,\n            },\n        ],\n    },\n    {\n        source: 'seen_content.json',\n        schemas: [\n            {\n                key: 'posts_seen',\n                type: ProvidedDataTypes.POST_SEEN,\n                transformer: (obj: { timestamp: string; author: string }[]): Partial<PostSeen>[] => {\n                    return obj.map((post): Partial<PostSeen> => ({\n                        data: post.author,\n                        timestamp: post.timestamp,\n                    }));\n                },\n            },\n        ],\n    },\n    {\n        source: 'settings.json',\n        schemas: [\n            {\n                type: ProvidedDataTypes.PRIVACY_SETTING,\n                transformer: objectToKeyValueTransformer,\n            },\n        ],\n    },\n    {\n        source: 'logins.json',\n        schemas: [\n            {\n                type: ProvidedDataTypes.LOGIN_INSTANCE,\n                key: 'timestamp',\n            },\n        ],\n    },\n    {\n        source: ['account_information', 'personal_information.json'],\n        schemas: [\n            {\n                selector: 'profile_user[].string_map_data.Email.value',\n                type: ProvidedDataTypes.EMAIL,\n            },\n            {\n                selector: 'profile_user[].string_map_data.Confirmed.value',\n                type: ProvidedDataTypes.TELEPHONE_NUMBER,\n            },\n            {\n                selector: 'profile_user[].string_map_data.Username.value',\n                type: ProvidedDataTypes.USERNAME,\n            },\n            {\n                selector: 'profile_user[].string_map_data.Name.value',\n                type: ProvidedDataTypes.FULL_NAME,\n            },\n            {\n                selector: 'profile_user[].string_map_data.Bio.value',\n                type: ProvidedDataTypes.BIOGRAPHY,\n            },\n            {\n                selector: 'profile_user[].string_map_data.Gender.value',\n                type: ProvidedDataTypes.GENDER,\n            },\n            {\n                selector: 'profile_user[].string_map_data.\"Date of birth\".value',\n                type: ProvidedDataTypes.DATE_OF_BIRTH,\n            },\n        ],\n    },\n    {\n        source: ['ads_and_businesses', 'advertisers_using_your_activity_or_information.json'],\n        schemas: [\n            {\n                selector: 'ig_custom_audiences_all_types[].advertiser_name',\n                type: ProvidedDataTypes.ADVERTISER,\n            },\n        ],\n    },\n    {\n        source: ['device_information', 'devices.json'],\n        schemas: [\n            {\n                selector: 'devices_devices[].string_map_data.\"User Agent\".value',\n                type: ProvidedDataTypes.USER_AGENT,\n            },\n        ],\n    },\n];\n\nexport default parsers;"
  },
  {
    "path": "src/main/providers/instagram/urls.json",
    "content": "[\n    \"https://www.instagram.com/accounts/access_tool/account_privacy_changes?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/password_changes?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_emails?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_phones?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/current_follow_requests?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/accounts_following_you?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/accounts_you_follow?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/hashtags_you_follow?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/accounts_you_blocked?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/logins?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/logouts?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/search_history?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_usernames?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_full_names?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_bio_texts?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/former_links_in_bio?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_poll_votes?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_emoji_slider_votes?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_question_responses?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_question_music_responses?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_countdown_follows?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/story_quiz_responses?__a=1\",\n    \"https://www.instagram.com/accounts/access_tool/ads_interests?__a=1\"\n]"
  },
  {
    "path": "src/main/providers/linkedin/index.ts",
    "content": "import { app } from 'electron';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport { ProviderFile } from '../types';\nimport { DataRequestProvider } from '../types/Provider';\nimport path from 'path';\nimport fs from 'fs';\nimport AdmZip from 'adm-zip';\n\nconst requestSavePath = path.join(app.getAppPath(), 'data');\n\nclass LinkedIn extends DataRequestProvider {\n    public static key = 'linkedin';\n\n    public static dataRequestIntervalDays = 14;\n\n    public static requiresEmailAccount = false;\n\n    /**\n     * The parameters to be stored for the secure windows\n     */\n    windowParams = {\n        key: this.windowKey,\n        origin: 'linkedin.com',\n    };\n\n    async initialise(): Promise<string> {\n        await this.verifyLoggedInStatus();\n        return this.getAccountName();\n    }\n\n    getAccountName = async (): Promise<string> => {\n        return withSecureWindow<string>(this.windowParams, async (window) => {\n            // Go to /me and wait for LinkedIn to redirect\n            await window.loadURL('https://www.linkedin.com/psettings/email');\n\n            // Then steal the accountname from the URL\n            return window.webContents.executeJavaScript(`\n                document.querySelector('.email-container p.email-address').textContent\n            `);\n        });\n    };\n\n    verifyLoggedInStatus = async (): Promise<Electron.Cookie[]> => {\n        return withSecureWindow<Electron.Cookie[]>(this.windowParams, (window) => {\n            const settingsUrl = 'https://www.linkedin.com/mypreferences/d/download-my-data';\n            window.loadURL(settingsUrl);\n\n            return new Promise((resolve) => {\n                const eventHandler = async (): Promise<void> => {\n                    // Check if we ended up at the page in an authenticated form\n                    if (settingsUrl === window.webContents.getURL()) {\n                        // If so, we retrieve the cookies\n                        const cookies = await window.webContents.session.cookies.get({});\n                        \n                        // Do a check if the language is set to English, and if not,\n                        // change it to English\n                        const lang = cookies.find((cookie) => cookie.name === 'lang');\n                        if (lang?.value !== 'v=2&lang=en-us') {\n                            await window.webContents.session.cookies.set({ \n                                url: 'https://linkedin.com',\n                                name: 'lang',\n                                value: 'v=2&lang=en-us',\n                                domain: '.linkedin.com',\n                                secure: true,\n                                expirationDate: Math.pow(2, 31) - 1,\n                            });\n                        }\n\n                        resolve(cookies);\n                    } else if (!window.isVisible()) {\n                        // If not, we'll check if we need to open the window for the\n                        // user to enter their credentials.\n                        window.show();\n                    }\n\n                    // LinkedIn redirects users to the signup page rather than\n                    // the login screen because of magic sauce, I guess. This\n                    // means we need to load the right page automatically.\n                    window.webContents.executeJavaScript(`\n                        document.querySelector('a.main__sign-in-link')?.click();\n                    `);\n                };\n\n                window.webContents.on('did-navigate', eventHandler);\n                window.webContents.once('did-finish-load', eventHandler);\n            });\n        });\n    };\n\n    update = async (): Promise<false> => {\n        // NOTE: LinkedIn has not accessible Privacy APIs at this point.\n        return false;\n    };\n\n    dispatchDataRequest = async (): Promise<void> => {\n        await this.verifyLoggedInStatus();\n\n        // GUARD: Check if a data request is already complete\n        if (await this.isDataRequestComplete()) {\n            return;\n        }\n\n        return withSecureWindow<void>(this.windowParams, async (window) => {\n            window.hide();\n\n            await new Promise((resolve) => {\n                window.webContents.on('did-finish-load', resolve);\n                window.loadURL('https://www.linkedin.com/psettings/member-data?li_theme=light&openInMobileMode=true');\n            });\n\n            // Now we must defer the page to the user, so that they can enter their\n            // password. We then listen for a succesfull AJAX call \n            return new Promise((resolve) => {\n                window.webContents.session.webRequest.onCompleted({\n                    urls: [ 'https://*.linkedin.com/*' ],\n                }, (details: Electron.OnCompletedListenerDetails) => {\n                    if (details.url === 'https://www.linkedin.com/psettings/member-data/export'\n                        && details.statusCode === 200) {\n                        resolve();\n                    }\n                });\n\n                // Select the full archive\n                window.webContents.executeJavaScript(`\n                    document.querySelector('input#fast-file-only-plus-other-data')?.click();\n                `);\n\n                // Then request the archive\n                window.webContents.executeJavaScript(`\n                    document.querySelector('button#download-button')?.click();\n                `);\n\n                window.show();\n            });\n        });\n    };\n\n    async isDataRequestComplete(): Promise<boolean> {\n        await this.verifyLoggedInStatus();\n\n        return withSecureWindow<boolean>(this.windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL('https://www.linkedin.com/psettings/member-data?li_theme=light&openInMobileMode=true');\n            });\n\n            // Find the right button and check if it reads 'Download archive'\n            // Also, LinkedIn only allows for a single download every 24hrs,\n            // after which you'll need to wait. We account for this by checking\n            // if the button is disabled\n            return window.webContents.executeJavaScript(`\n                var btn = document.querySelector('button.download-btn');\n                btn.textContent === 'Download archive'\n                    && !btn.disabled\n            `);\n        });\n    }\n\n    async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {\n        return withSecureWindow<ProviderFile[]>(this.windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL('https://www.linkedin.com/psettings/member-data?li_theme=light&openInMobileMode=true');\n            });\n\n            const filePath = path.join(requestSavePath, 'linkedin.zip');\n            await new Promise((resolve) => {\n                // Create a handler for any file saving actions\n                window.webContents.session.once('will-download', (event, item) => {\n                    // Save the item to the data folder temporarily\n                    item.setSavePath(filePath);\n                    item.once('done', resolve);\n                });\n\n                // And then trigger the button click\n                window.webContents.executeJavaScript(`\n                    document.querySelector('button.download-btn')?.click();\n                `);\n            });\n\n            // Close window\n            window.hide();\n\n            // Firstly, we'll save all files in a JSON format\n            const zip = new AdmZip(filePath);\n            await new Promise((resolve) => \n                // Fix underway: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59070\n                zip.extractAllToAsync(extractionPath, true, false, resolve),\n            );\n\n            // Then, we'll structure the returned data\n            const files = zip.getEntries().map((entry): ProviderFile => {\n                return {\n                    filepath: entry.entryName,\n                    data: null,\n                };\n            });\n\n            // And dont forget to remove the zip file after it's been processed\n            await fs.promises.unlink(filePath);\n\n            return files;\n        });\n    }\n}\n\nexport default LinkedIn;"
  },
  {
    "path": "src/main/providers/linkedin/parser.ts",
    "content": "import {\n    ProviderParser,\n    ProvidedDataTypes,\n    ProviderDatum,\n} from '../types/Data';\n\n/**\n * Will transform a string of values seperated by semicolons into an array of strings\n * @param object string\n */\nfunction semiColonSeperatedTransformer(object: string): Partial<ProviderDatum<unknown>>[] {\n    return object.split('; ').map((data) => {\n        return {\n            data,\n        };\n    });\n}\n\nconst parsers: ProviderParser[] = [\n    {\n        source: 'Ad_Targeting.csv',        \n        schemas: [\n            {\n                type: ProvidedDataTypes.AD_INTEREST,\n                key: 'Company Names',\n                transformer: semiColonSeperatedTransformer,\n            },\n            {\n                type: ProvidedDataTypes.AD_INTEREST,\n                key: 'Fields of Study',\n                transformer: semiColonSeperatedTransformer,\n            },\n            {\n                type: ProvidedDataTypes.GENDER,\n                key: 'Member Gender',\n            },\n            {\n                type: ProvidedDataTypes.AD_INTEREST,\n                key: 'Member Interests',\n                transformer: semiColonSeperatedTransformer,\n            },\n            {\n                type: ProvidedDataTypes.USER_LANGUAGE,\n                key: 'Interface Locales',\n                transformer: semiColonSeperatedTransformer,\n            },\n            {\n                type: ProvidedDataTypes.PLACE_OF_RESIDENCE,\n                key: 'Profile Locations',\n                transformer: semiColonSeperatedTransformer,\n            },\n            {\n                type: ProvidedDataTypes.AD_INTEREST,\n                key: 'Member Skills',\n                transformer: semiColonSeperatedTransformer,\n            },\n        ],\n    },\n];\n\nexport default parsers;"
  },
  {
    "path": "src/main/providers/open-data-rights/index.ts",
    "content": "import { ProviderFile } from '../types';\nimport { OpenDataRightsProvider } from '../types/Provider';\nimport fetch, { RequestInit } from 'node-fetch';\nimport store from 'main/store';\nimport AdmZip from 'adm-zip';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\n\ntype Token = {\n    access_token: string;\n    refresh_token: string;\n};\n\nclass OpenDataRights extends OpenDataRightsProvider {\n    public static key = 'open-data-rights';\n\n    public static dataRequestIntervalDays = 1;\n\n    private token: Token | null;\n\n    constructor(windowKey: string, accountName?: string) {\n        super(windowKey, accountName);\n        this.token = store.get(this.windowKey, null) as Token | null;\n    }\n\n    getInit = (): RequestInit => {\n        return {\n            headers: {\n                'Authorization': `Bearer ${this.token.access_token}`,\n            },\n        };\n    };\n\n    async initialise(): Promise<string> {\n        const code = await withSecureWindow<string>(this.windowParams, async (window) => {\n            // Load the redirect URI in the window\n            const url = `${this.url}/oauth/authorize?redirect_uri=${encodeURIComponent('aeon://provider/open-data-rights/callback')}`;\n            await window.loadURL(url);\n            window.show();\n\n            // Wait for any changes in location\n            return new Promise((resolve) => {\n                const eventHandler = () => {\n                    if (window.webContents.getURL().startsWith('aeon://provider/open-data-rights/callback')) {\n                        const replacedUrl = window.webContents.getURL().replace('aeon://provider/open-data-rights/callback', '');\n                        const token = new URLSearchParams(replacedUrl).get('token');\n                        resolve(token);\n                    }\n                };\n\n                window.webContents.on('did-navigate', eventHandler);\n                window.webContents.once('did-finish-load', eventHandler);\n            });\n        });\n\n        // Gather form parameters\n        const formData = new URLSearchParams();\n        formData.append('code', code);\n\n        // Exchange token for access token\n        this.token = await fetch(`${this.url}/oauth/token`, {\n            method: 'POST',\n            body: formData,\n        }).then((response) => response.json() as Promise<Token>);\n\n        // Also save it to disk\n        store.set(this.windowKey, this.token);\n\n        // Now we just need to fetch the account\n        return fetch(`${this.url}/data/me`, this.getInit())\n            .then((response) => response.json() as Promise<{ account: string }>)\n            .then((response) => response.account);\n    }\n\n    async update(): Promise<false> {\n        // NOTE: Updating is not supported by Facebook since it's internal API\n        // is a enormous clusterfuck and cannot be trusted.\n        return false;\n    }\n\n    dispatchDataRequest = async (): Promise<string> => {\n        return fetch(`${this.url}/requests`, {\n            ...this.getInit(),\n            method: 'POST',\n        }).then((response) => response.json() as Promise<{ requestId: string }>)\n            .then((response) => response.requestId);\n    };\n\n    isDataRequestComplete = (identifier: string): Promise<boolean> => {\n        return fetch(`${this.url}/requests/${identifier}/complete`, this.getInit())\n            .then((response) => response.text())\n            .then((response) => response === '1');\n    };\n\n    parseDataRequest = async (extractionPath: string, identifier: string): Promise<ProviderFile[]> => {\n        // Retrieve the archive from the API\n        const archive = await fetch(`${this.url}/requests/${identifier}/download`, this.getInit())\n            .then((response) => response.arrayBuffer());\n\n        // Then pass it over to adm-zip\n        const zip = new AdmZip(Buffer.from(archive));\n        await new Promise((resolve) => \n            // Fix underway: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59070\n            zip.extractAllToAsync(extractionPath, true, false, resolve),\n        );\n\n        // Translate this into a form that is readable for the ParserManager\n        const files = zip.getEntries().map((entry): ProviderFile => {\n            return {\n                filepath: entry.entryName,\n                data: null,\n                // data: entry.getData(),\n            };\n        });\n\n        return files;\n    };\n}\n\nexport default OpenDataRights;"
  },
  {
    "path": "src/main/providers/parsers.ts",
    "content": "import unwrapParserSource from 'main/lib/unwrap-provider-source';\nimport Facebook from './facebook/parser';\nimport Instagram from './instagram/parser';\nimport LinkedIn from './linkedin/parser';\nimport Spotify from './spotify/parser';\nimport { ProviderParser } from './types/Data';\n\n// Contains an overview of parsers, sorted by their provider\nconst providerParsers: [ ProviderParser[], string ][] = [\n    [Instagram, 'instagram'],\n    [Facebook, 'facebook'],\n    [LinkedIn, 'linkedin'],\n    [Spotify, 'spotify'],\n];\n\n// Contains an overview of parsers, sorted by the file they parse\nexport const parsersByFile: Map<string, ProviderParser> = new Map(\n    // Loop through all available providers\n    providerParsers.flatMap(([parsers, key]): [string, ProviderParser][] => {\n        // The loop over all parsers the provider provides\n        return parsers.map((parser): [string, ProviderParser] => {\n            // Inject the provider key into the parser, so that it may be dealt\n            // with later\n            parser.provider = key;\n\n            return [\n                // Also scope the path to the provider, as the data will be\n                // stored relatively to the parser as well\n                `${key}/${unwrapParserSource(parser.source)}`,\n                parser,\n            ];\n        });\n    }),\n);\n\n// First map all the providers, then show only the paths that are part of a\n// particular provider, rather than creating a long list of global filenames\nexport const parsersByProvider: Map<string, Map<string, ProviderParser>> = new Map(\n    providerParsers.map(([parsers, key]) => {\n        return [\n            key,\n            new Map(parsers.map((parser) => {\n                return [\n                    unwrapParserSource(parser.source),\n                    parser,\n                ];\n            })),\n        ];\n    }),\n);\n\n/**\n * More intelligently retrieve a particular parser using a filename that might\n * include an account name.\n */\nexport function getParserByFileName(filepath: string): ProviderParser | void {\n    // First, we'll split the filename, where the first directory should match\n    // the key of the provider\n    const [provider, ...rest] = filepath.split('/');\n\n    // Then we'll retrieve the particular set of parsers for this provider\n    const parserMap = parsersByProvider.get(provider);\n\n    // GUARD: If the root directory isn't recognized, we cannot parse this file\n    if (!parserMap) {\n        return;\n    }\n\n    // Attempt to retrieve the parser by omitting the first directory after the\n    // root, which in new versions should show the account name\n    let parser: ProviderParser;\n    parser = parserMap.get(rest.slice(1).join('/'));\n\n    // Alternatively, attempt to retrieve the parser using legacy methods\n    if (!parser) {\n        parser = parserMap.get(rest.join('/'));\n    }\n\n    return parser;\n}\n\nexport default providerParsers;"
  },
  {
    "path": "src/main/providers/spotify/index.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport AdmZip from 'adm-zip';\nimport { subHours } from 'date-fns';\nimport { withSecureWindow } from 'main/lib/create-secure-window';\nimport { ProviderFile } from '../types';\nimport { EmailDataRequestProvider } from '../types/Provider';\n\nclass Spotify extends EmailDataRequestProvider {\n    public static key = 'spotify';\n\n    public static dataRequestIntervalDays = 5;\n\n    windowParams = {\n        key: this.windowKey,\n        origin: 'spotify.com',\n    };\n\n    async initialise(): Promise<string> {\n        await this.verifyLoggedInStatus();\n        return this.accountName;\n    }\n\n    verifyLoggedInStatus = async (): Promise<Electron.Cookie[]> => {\n        return withSecureWindow<Electron.Cookie[]>(this.windowParams, (window) => {\n            const settingsUrl = 'https://www.spotify.com/us/account/privacy/';\n            window.loadURL(settingsUrl);\n\n            return new Promise((resolve) => {\n                const eventHandler = async (): Promise<void> => {\n                    // Check if we ended up at the page in an authenticated form\n                    if (settingsUrl === window.webContents.getURL()) {\n                        // If so, we retrieve the cookies\n                        const cookies = await window.webContents.session.cookies.get({});\n                        \n                        resolve(cookies);\n                    } else if (!window.isVisible()) {\n                        // If not, we'll check if we need to open the window for the\n                        // user to enter their credentials.\n                        window.show();\n                    }\n                };\n\n                window.webContents.on('did-navigate', eventHandler);\n                window.webContents.once('did-finish-load', eventHandler);\n            });\n        });\n    };\n\n    update = async (): Promise<false> => {\n        // NOTE: Updating is not supported by Spotify at this point\n        return false;\n    };\n\n    dispatchDataRequest = async (): Promise<void> => {\n        await this.verifyLoggedInStatus();\n\n        await withSecureWindow<void>(this.windowParams, async (window) => {\n            window.hide();\n\n            await new Promise((resolve) => {\n                window.webContents.on('did-finish-load', resolve);\n                window.loadURL('https://www.spotify.com/us/account/privacy/');\n            });\n\n            await new Promise((resolve) => setTimeout(resolve, 1000));\n\n            // Now we must defer the page to the user, so that they can confirm\n            // the request. We then listen for a succesfull AJAX call \n            await new Promise<void>((resolve) => {\n                window.webContents.session.webRequest.onCompleted({\n                    urls: [ \n                        'https://www.spotify.com/us/account/privacy/download/',\n                        'https://www.spotify.com/api/accountprivacy-api/v1/data-download/resend-confirmation-email/',\n                    ],\n                }, (details: Electron.OnCompletedListenerDetails) => {\n                    if (details.statusCode === 200) {\n                        resolve();\n                    } \n                });\n\n                // Ensure that the data request is in JSON format\n                window.webContents.executeJavaScript(`\n                    Array.from(document.querySelectorAll('button'))\n                        .find((b) => b.textContent === 'Resend email' || b.textContent === 'Download')\n                        ?.click();\n                `);\n\n                window.show();\n            });     \n        });\n\n        // Then, we'll poll for a particular email from Spotify coming in\n        // that we have to click a link from\n        return this.recursivelyWaitForConfirmationEmail();\n    };\n\n    /**\n     * A function that will poll the email account linked to this provider, to\n     * check for a particular email with a confirmation link. If it is found,\n     * the link is opened and the Promise is resolved.\n     */\n    async recursivelyWaitForConfirmationEmail(): Promise<void> {\n        // Retrieve all messages from Spotify from the server\n        const [ message ] = await this.email.findMessages({\n            from: 'noreply@spotify.com',\n        });\n\n        // Check if there is a message and that it has a date\n        if (message && message.date) {\n            // Then check if it's been sent over in the last two hours\n            const reference = subHours(new Date, 2);\n            if (reference < message.date) {\n                // If so, we find the link and click it\n                const link = message.text.replace('=\\n', '')\n                    .match(/https:\\/\\/www\\.spotify\\.com\\/account\\/privacy\\/download\\/confirm\\/([A-Za-z0-9.]+)/)[0];\n\n                // GUARD: Check if the link is correctly extracted, else we\n                // might be in the wrong email\n                if (link) {\n                    // If so, we open a new window in which we open the link\n                    return withSecureWindow(this.windowParams, (window) => {\n                        return window.loadURL(link);\n                    });\n                } else {\n                    throw new Error('Could not find Spotify email confirmation link...');\n                }\n            }\n        }\n\n        // If the mail was not found, wait 15 seconds and execute this method again.\n        await new Promise((resolve) => setTimeout(resolve, 15_000));\n        return this.recursivelyWaitForConfirmationEmail();\n    }\n\n    async isDataRequestComplete(): Promise<boolean> {\n        await this.verifyLoggedInStatus();\n\n        return withSecureWindow<boolean>(this.windowParams, async (window) => {\n            // Load page URL\n            await new Promise((resolve) => {\n                window.webContents.once('did-finish-load', resolve);\n                window.loadURL('https://www.spotify.com/us/account/privacy/');\n            });\n\n            // Add a timeout because it appears to solve problems 🤷‍♂️\n            // NOTE: This is a bad strategy...\n            await new Promise((resolve) => setTimeout(resolve, 2500));\n\n            // Check if the third div is grayed out\n            return window.webContents.executeJavaScript(`\n                document.querySelector('button[data-testid=\"resend-download-email\"]').disabled === false\n            `);\n        });\n    }\n\n    async parseDataRequest(extractionPath: string): Promise<ProviderFile[]> {\n        // Get the download link\n        const [message] = await this.email.findMessages({\n            from: 'noreply@spotify.com',\n        });\n        \n        // GUARD: Double-check that message.text exists\n        if (!message.text) {\n            throw new Error('Failed to find email text for Spotify');\n        }\n\n        const link = message.text.replace('=\\n', '')\n            .match(/https:\\/\\/www\\.spotify\\.com\\/account\\/privacy\\/download\\/retrieve\\/([a-f=\\d]+)/)[0];\n\n        // GUARD: Check if the download link was successfully retrieved\n        if (!message || !link) {\n            throw new Error('Could not find download link in email for Spotify');\n        }\n\n        // Open a window with the download link\n        return withSecureWindow<ProviderFile[]>(this.windowParams, async (window) => {\n            const filePath = path.join(extractionPath, 'spotify.zip');\n            await new Promise((resolve) => {\n                // Create a handler for any file saving actions\n                window.webContents.session.once('will-download', (event, item) => {\n                    // Save the item to the data folder temporarily\n                    item.setSavePath(filePath);\n                    item.once('done', resolve);\n                });\n\n                // And then open the URL and show the window\n                // TODO: Check if an additional click is necessary\n                window.loadURL(link);\n                window.show();\n            });\n\n            // We have the ZIP, all that's left to do is unpack it and pipe it to\n            // the repository\n            const zip = new AdmZip(filePath);\n            await new Promise((resolve) => \n                // Fix underway: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59070\n                zip.extractAllToAsync(extractionPath, true, false, resolve),\n            );\n\n            // Translate this into a form that is readable for the ParserManager\n            const files = zip.getEntries().map((entry): ProviderFile => {\n                return {\n                    filepath: entry.entryName,\n                    data: null,\n                };\n            });\n\n            // And dont forget to remove the zip file after it's been processed\n            await fs.promises.unlink(filePath);\n\n            return files;\n        });\n    }\n}\n\nexport default Spotify;"
  },
  {
    "path": "src/main/providers/spotify/parser.ts",
    "content": "import path from 'path';\nimport { Address, PlayedSong, ProvidedDataTypes, ProviderParser } from '../types/Data';\n\ntype SpotifyStreamHistorySong = {\n    endTime: string;\n    artistName: string;\n    trackName: string;\n    msPlayed: number;\n};\n\ntype SpotifyUserData = {\n    username: string;\n    email: string;\n    country: string;\n    createdFromFacebook: boolean;\n    facebookUid?: string;\n    birthdate: string;\n    gender: string;\n    postalCode?: string;\n    mobileNumber?: string;\n    mobileOperator?: string;\n    mobileBrand?: string;\n    creationTime: string;\n};\n\nconst parsers: ProviderParser[] = [\n    {\n        source: path.join('MyData', 'Follow.json'),\n        schemas: [\n            {\n                key: 'followingArtists',\n                type: ProvidedDataTypes.ACCOUNT_FOLLOWING,\n            },\n        ],\n    },\n    {\n        source: path.join('MyData', 'Identity.json'),\n        schemas: [\n            {\n                key: 'firstName',\n                type: ProvidedDataTypes.FIRST_NAME,\n            },\n            {\n                key: 'lastName',\n                type: ProvidedDataTypes.LAST_NAME,\n            },\n        ],\n    },\n    {\n        source: path.join('MyData', 'Inferences.json'),\n        schemas: [\n            {\n                key: 'inferences',\n                type: ProvidedDataTypes.INFERENCE,\n            },\n        ],\n    },\n    {\n        source: path.join('MyData', 'StreamingHistory0.json'),\n        schemas: [\n            {\n                type: ProvidedDataTypes.PLAYED_SONG,\n                transformer: (song: SpotifyStreamHistorySong): Partial<PlayedSong> => {\n                    return {\n                        data: {\n                            artist: song.artistName,\n                            track: song.trackName,\n                            playDuration: song.msPlayed,\n                        },\n                        timestamp: new Date(song.endTime).toString(),\n                    };\n                },\n            },\n        ],\n    },\n    {\n        source: path.join('MyData', 'Userdata.json'),\n        schemas: [\n            {\n                key: 'username',\n                type: ProvidedDataTypes.USERNAME,\n            },\n            {\n                key: 'email',\n                type: ProvidedDataTypes.EMAIL,\n            },\n            {\n                key: 'country',\n                type: ProvidedDataTypes.COUNTRY,\n            },\n            {\n                key: 'birthdate',\n                type: ProvidedDataTypes.DATE_OF_BIRTH,\n            },\n            {\n                key: 'gender',\n                type: ProvidedDataTypes.GENDER,\n            },\n            {\n                key: 'postcalCode',\n                type: ProvidedDataTypes.ADDRESS,\n                transformer: (data: SpotifyUserData['postalCode']): Partial<Address>[] => {\n                    return [{\n                        data: {\n                            zipCode: data,\n                        },\n                    }];\n                },\n            },\n            {\n                key: 'mobileNumber',\n                type: ProvidedDataTypes.TELEPHONE_NUMBER,\n            },\n            {\n                key: 'creationTime',\n                type: ProvidedDataTypes.REGISTRATION_DATE,\n            },\n        ],\n    },\n];\n\nexport default parsers;"
  },
  {
    "path": "src/main/providers/types/Data.ts",
    "content": "\nexport enum ProvidedDataTypes {\n    /** A email adress */\n    EMAIL = 'email',\n    /** A first name */\n    FIRST_NAME = 'first_name',\n    /** A last name */\n    LAST_NAME = 'last_name',\n    /** A full name, including first and last name */\n    FULL_NAME = 'full_name',\n    /** An IP address */\n    IP_ADDRESS = 'ip_address',\n    /** A user-agent that was saved as part as a log file */\n    USER_AGENT = 'user_agent',\n    /** A language that is used for browsing the platform */\n    USER_LANGUAGE = 'user_language',\n    /** A cookie that was saved */\n    COOKIE = 'cookie',\n    /** A follower for the user */\n    FOLLOWER = 'follower',\n    /** Another account that the user is following */\n    ACCOUNT_FOLLOWING = 'account_following',\n    /** Another account that the user has blocked */\n    BLOCKED_ACCOUNT = 'blocked_account',\n    /** A hashtag the user is following */\n    HASHTAG_FOLLOWING = 'hashtag_following',\n    /** An ad interest that was flagged by the system for the user */\n    AD_INTEREST = 'ad_interest',\n    /** A telephone number */\n    TELEPHONE_NUMBER = 'telephone_number',\n    /** A comment made by the user */\n    COMMENT = 'comment',\n    /** A device that was used by the user on the platform */\n    DEVICE = 'device',\n    /** A username that is used for a particular platform */\n    USERNAME = 'username',\n    /** A place (city, town, village, etc.) where the user resides */\n    PLACE_OF_RESIDENCE = 'place_of_residence',\n    /** A full adress, including street, number, ZIP-code and eventual state */\n    ADDRESS = 'address',\n    /** The country where a user resides */\n    COUNTRY = 'country',\n    /** A like thas been placed on a particular post */\n    LIKE = 'like',\n    /** A saved instance of the user logging in */\n    LOGIN_INSTANCE = 'login_instance',\n    /** A saved instance of the user logging out */\n    LOGOUT_INSTANCE = 'logout_instance',\n    /** A photo */\n    PHOTO = 'photo',\n    /** A message by the user, to another user */\n    MESSAGE = 'message',\n    /** A gender */\n    GENDER = 'gender',\n    /** A profile picture */\n    PROFILE_PICTURE = 'profile_picture',\n    /** A birth date */\n    DATE_OF_BIRTH = 'date_of_birth',\n    /** The date on which the user joined a platform */\n    JOIN_DATE = 'join_date',\n    /** A search query by the user */\n    SEARCH_QUERY = 'search_query',\n    /** A post that the user has seen */\n    POST_SEEN = 'post_seen',\n    /** A privacy setting for the user */\n    PRIVACY_SETTING = 'privacy_setting',\n    /** A telephone contact that has been uploaded by the user */\n    UPLOADED_CONTACT = 'uploaded_contact',\n    /** A saved cookie with possibly extra information */\n    SESSION = 'session',\n    /** A categorisation of the peer group you belong to */\n    PEER_GROUP = 'peer_group',\n    /** A job held currently or in the past */\n    EMPLOYMENT = 'employment',\n    /** An in-site visited page */\n    VISITED_PAGE = 'visited_page',\n    /** Recorded activity outside of the platform website */\n    OFF_SITE_ACTIVITY = 'off_site_activity',\n    /** Response to an event invitation */\n    EVENT_RESPONSE = 'event_response',\n    /** Timezone associated with the user */\n    TIMEZONE = 'timezone',\n    /** Currency associated with the user */\n    CURRENCY = 'currency',\n    /** An education experience held currently or in the past */\n    EDUCATION_EXPERIENCE = 'education_experience',\n    /** Registration date for the platform */\n    REGISTRATION_DATE = 'registration_date',\n    /** A mobile device associated with the platform */\n    MOBILE_DEVICE = 'mobile_device',\n    /** An inference about an individual */\n    INFERENCE = 'inference',\n    /** A song that has been played by the user */\n    PLAYED_SONG = 'played_song',\n    /** A biography that describes the user */\n    BIOGRAPHY = 'biography',\n    /** An advertiser that is advertising to the user */\n    ADVERTISER = 'advertiser',\n}\n\nexport interface ProviderDatum<D, T = ProvidedDataTypes> {\n    /** The data format, as it is retrieved from a file */\n    data: D;\n    /** The type of this datum */\n    type: T;\n    /** The provider from which this data was gained */\n    provider: string;\n    /** The account from which this data was gained */\n    account?: string;\n    /** An API host from where the data was gained */\n    hostname?: string;\n    /** The specific file from which the data was extracted */\n    source: string;\n    /** A timestamp that is associated with this specific datapoint. For\n     * instance, when a post was posted. */\n    timestamp?: string;\n    /** Wether this datum can be edited with the provider */\n    isEditable?: boolean;\n    /** Whether this datum can be deleted at the provider */\n    isDeletable?: boolean;\n}\n\nexport type Email = ProviderDatum<string, ProvidedDataTypes.EMAIL>;\nexport type FirstName = ProviderDatum<string, ProvidedDataTypes.FIRST_NAME>;\nexport type LastName = ProviderDatum<string, ProvidedDataTypes.LAST_NAME>;\nexport type FullName = ProviderDatum<string, ProvidedDataTypes.FULL_NAME>;\nexport type IpAddress = ProviderDatum<string, ProvidedDataTypes.IP_ADDRESS>;\nexport type UserAgent = ProviderDatum<string, ProvidedDataTypes.USER_AGENT>;\nexport type UserLanguage = ProviderDatum<string, ProvidedDataTypes.USER_LANGUAGE>;\nexport type Follower = ProviderDatum<string, ProvidedDataTypes.FOLLOWER>;\nexport type AccountFollowing = ProviderDatum<string, ProvidedDataTypes.ACCOUNT_FOLLOWING>;\nexport type BlockedAccount = ProviderDatum<string, ProvidedDataTypes.BLOCKED_ACCOUNT>;\nexport type HashtagFollowing = ProviderDatum<string, ProvidedDataTypes.HASHTAG_FOLLOWING>;\nexport type AdInterest = ProviderDatum<string, ProvidedDataTypes.AD_INTEREST>;\nexport type TelephoneNumber = ProviderDatum<string, ProvidedDataTypes.TELEPHONE_NUMBER>;\nexport type Device = ProviderDatum<string, ProvidedDataTypes.DEVICE>;\nexport type Username = ProviderDatum<string, ProvidedDataTypes.USERNAME>;\nexport type PlaceOfResidence = ProviderDatum<string, ProvidedDataTypes.PLACE_OF_RESIDENCE>;\nexport type Address = ProviderDatum<{ street?: string; number?: number; state?: string; zipCode?: string; }, ProvidedDataTypes.ADDRESS>;\nexport type Country = ProviderDatum<string, ProvidedDataTypes.COUNTRY>;\nexport type Like = ProviderDatum<string, ProvidedDataTypes.LIKE>;\nexport type LoginInstance = ProviderDatum<number, ProvidedDataTypes.LOGIN_INSTANCE>;\nexport type LogOutInstance = ProviderDatum<unknown, ProvidedDataTypes.LOGOUT_INSTANCE>;\nexport type Photo = ProviderDatum<{ url: string; description: string; }, ProvidedDataTypes.PHOTO>;\nexport type Message = ProviderDatum<string, ProvidedDataTypes.MESSAGE>;\nexport type Gender = ProviderDatum<string, ProvidedDataTypes.GENDER>;\nexport type ProfilePicture = ProviderDatum<string, ProvidedDataTypes.PROFILE_PICTURE>;\nexport type DateOfBirth = ProviderDatum<Date, ProvidedDataTypes.DATE_OF_BIRTH>;\nexport type JoinDate = ProviderDatum<Date, ProvidedDataTypes.JOIN_DATE>;\nexport type SearchQuery = ProviderDatum<string, ProvidedDataTypes.SEARCH_QUERY>;\nexport type PostSeen = ProviderDatum<string, ProvidedDataTypes.POST_SEEN>;\n/** eslint-disable-next-line */\nexport type PrivacySetting = ProviderDatum<{ key: string; value: any; }, ProvidedDataTypes.PRIVACY_SETTING>;\nexport type UploadedContact = ProviderDatum<unknown, ProvidedDataTypes.UPLOADED_CONTACT>;\ntype SessionData = {\n    cookie_name: string;\n    ip_address: string;\n    language_code: string;\n    timestamp: string;\n    user_agent: string;\n    device_id: string;\n};\nexport type Session = ProviderDatum<SessionData, ProvidedDataTypes.SESSION>;\nexport type PeerGroup = ProviderDatum<string, ProvidedDataTypes.PEER_GROUP>;\nexport type Employment = ProviderDatum<{ jobTitle: string; company: string; }, ProvidedDataTypes.EMPLOYMENT>;\nexport type VisitedPage = ProviderDatum<{ name: string; uri?: string; }, ProvidedDataTypes.VISITED_PAGE>;\nexport type OffSiteActivity = ProviderDatum<{ type?: string; website: string; }, ProvidedDataTypes.OFF_SITE_ACTIVITY>;\nexport type EventResponse = ProviderDatum<{ name?: string; response?: string; }, ProvidedDataTypes.EVENT_RESPONSE>;\nexport type Timezone = ProviderDatum<string, ProvidedDataTypes.TIMEZONE>;\nexport type Currency = ProviderDatum<string, ProvidedDataTypes.CURRENCY>;\nexport type EducationExperience = ProviderDatum<{ institution: string; graduated?: boolean; started_at?: Date; graduated_at?: Date; type?: string; }, ProvidedDataTypes.EDUCATION_EXPERIENCE>;\nexport type RegistrationDate = ProviderDatum<Date, ProvidedDataTypes.REGISTRATION_DATE>;\nexport type MobileDevice = ProviderDatum<{ type: string; os?: string; advertiser_id?: string; device_locale?: string; }, ProvidedDataTypes.MOBILE_DEVICE>;\nexport type Inference = ProviderDatum<string, ProvidedDataTypes.INFERENCE>;\nexport type PlayedSong = ProviderDatum<{\n    artist: string;\n    track: string;\n    /** The duration of play in milliseconds */\n    playDuration: number;\n}, ProvidedDataTypes.PLAYED_SONG>;\nexport type Biography = ProviderDatum<string, ProvidedDataTypes.BIOGRAPHY>;\nexport type Advertiser = ProviderDatum<string, ProvidedDataTypes.ADVERTISER>;\n\nexport interface ProviderParser {\n    /** The file from which the data has originated. Please take care to not\n     * include any path separators (e.g. `/` or `\\`) so that code is as platform\n     * independent as possible. Optionally, you may supply an array with path\n     * segments that is concatenated with `path.join()`. */\n    source: string | string[];\n    /** An optional provider string */\n    provider?: string;\n    /** The schema for accessing the data in the particular file.*/\n    schemas: {\n        /** The key which is used to access the data. This key may be nested. If \n         * the key is not set, the root object is assumed to be the key \n         * @deprecated This method is deprecated for the new `selector`\n         * property. */\n        key?: string | string[];\n        /** A JMESPath expression that select one or more pieces piece of data\n         * from the source file.\n         * @example 'foo.bar[].baz'\n         * @see https://jmespath.org */\n        selector?: string;\n        /** The type that is found at the particular key */\n        type: ProvidedDataTypes;\n        /** An optional transformer that is used to translate complex objects \n         * into the required shape */\n        transformer?(object: unknown): Partial<ProviderDatum<unknown, unknown>>[] | Partial<ProviderDatum<unknown, unknown>>;\n    }[];\n}\n"
  },
  {
    "path": "src/main/providers/types/Events.ts",
    "content": "import { DataRequestStatus } from '.';\n\nexport enum ProviderCommands {\n    UPDATE,\n    UPDATE_ALL,\n    DISPATCH_DATA_REQUEST,\n    DISPATCH_DATA_REQUEST_TO_ALL,\n    REFRESH,\n    INITIALISE,\n    GET_ACCOUNTS,\n    GET_AVAILABLE_PROVIDERS,\n}\n\nexport enum ProviderEvents {\n    CHECKING_DATA_REQUESTS = 'checking_data_requests',\n    DATA_REQUEST_ACTION_REQUIRED = 'data_request_action_required',\n    DATA_REQUEST_COMPLETED = 'data_request_completed',\n    DATA_REQUEST_DISPATCHED = 'data_request_dispatched',\n    UPDATE_COMPLETE = 'update_complete',\n    ACCOUNT_CREATED = 'account_created',\n    ACCOUNT_DELETED = 'account_deleted',\n    READY = 'ready',\n}\n\nexport type CheckingDataRequests = Record<string, never>;\n\nexport interface DataRequestActionRequired {\n    provider: string;\n    account?: string;\n    hostname?: string;\n    url?: string;\n    status: DataRequestStatus;\n}\n\nexport interface DataRequestCompleted extends DataRequestActionRequired {\n    changedFiles: number;\n    commitHash: string;\n}\nexport type DataRequestDispatched = DataRequestActionRequired;\n\nexport interface UpdateComplete {\n    provider: string;\n    account?: string;\n    url?: string;\n    changedFiles: number;\n    commitHash: string;\n}\n\nexport interface AccountCreated {\n    provider: string;\n    account?: string;\n    hostname?: string;\n    url?: string;\n}\n\nexport type AccountDeleted = AccountCreated;\n\nexport type ProvidersReady = Record<string, never>;"
  },
  {
    "path": "src/main/providers/types/Provider.ts",
    "content": "import { EmailClient } from 'main/email-client/types';\nimport { ProviderFile } from '.';\n\n/**\n * The base structure for any provider. A provider is a back-end (e.g. website,\n * API, organisation, etc.) that can provider data to Aeon.\n */\nexport abstract class Provider {\n    /* The account name for the account this provider provides */\n    protected accountName?: string;\n\n    /* A key that is used by withSecureWindow to keep all windows that are\n    opened from the provider safe and secure from other. */\n    protected windowKey: string;\n\n    /* Wehther this provider requires email to operate. May be overriden by\n    other abstract classes */\n    public static requiresEmail = false;\n\n    /* Wehther this provider requires a URL to operate. May be overriden by\n    other abstract classes */\n    public static requiresUrl = false;\n\n    /** The key under which all files will be stored. Should be filesystem-safe\n     * (no spaces, all-lowercase) */\n    public static key: string;\n\n    /** Update the data that is retrieved by this Provider. Should return an\n     * object with all new files, so they can be saved to disk. Alternatively,\n     * should return false to indicate that no update was carried out. */\n    abstract update(): Promise<ProviderFile[]> | Promise<false>;\n\n    /** Initialise the provider. This function is called only when it is\n     * initialised for the first time during onboarding. The return boolean\n     * indicates whether the provider succeeded in initialising, ie. by logging\n     * into a particular service */\n    abstract initialise(accountName?: string): Promise<string>;\n\n    constructor(windowKey: string, accountName?: string) {\n        this.accountName = accountName;\n        this.windowKey = windowKey;\n    }\n}\n\n/**\n * A specialised form of a Provider that is able to handle Data Requests as\n * opposed to simple, linear updates. A data request is asynchronous request for\n * data that may take some time to complete.\n */\nexport interface DataRequestProvider extends Provider {\n    /** Dispatch a data request to this Provider. The difference between a\n     * regular update and a data request, is that it is asynchronous, and might\n     * take a couple hours or even days to complete.\n     * Optionall, this request may return a string or number that indicates some\n     * request identifier. This id is then reinserted into the other two methods */\n    dispatchDataRequest?(): Promise<void> | Promise<string | number>;\n    /** Check if the data request is already complete */\n    isDataRequestComplete?(identifier?: string | number): Promise<boolean>;\n    /** If the data request has been completed, download the resulting dump and\n     * parse it, so that it can be processed and saved to the repository */\n    parseDataRequest?(extractionPath: string, identifier?: string | number): Promise<ProviderFile[]>;\n}\n\n/**\n * A specialised form of a Provider that is able to handle Data Requests as\n * opposed to simple, linear updates. A data request is asynchronous request for\n * data that may take some time to complete.\n */\nexport abstract class DataRequestProvider extends Provider {\n    /** The amount of days that are required between successive data requests */\n    public static dataRequestIntervalDays: number;\n}\n\n/**\n * A specialised form of a DataRequestProvider that requires email in order to\n * operate. This means concretely, that a user will be required to link an email\n * account to this provider, which is then accessible in the class.\n */\nexport abstract class EmailDataRequestProvider extends DataRequestProvider {\n    /* Set a flag that email is required for this provider */\n    public static requiresEmail = true;\n\n    /* An email client that is available for use in this provider. This will\n    automatically be set by Aeon when constructing the class. */\n    protected email: EmailClient;\n\n    setEmailClient(email: EmailClient): void {\n        this.email = email;\n    }\n}\n\n/**\n * A specialised form of a DataRequestProvider that implements the Open Data\n * Rights API. This practically means that users will be required to enter a URL\n * when creating an instance of this provider, which is then available for use\n * in the class itself.\n * NOTE: This type is used primarily in an implemeting class called\n * OpenDataRights (see ../open-data-rights). You don't need to create a seperate\n * class for each organisation implementing an Open Data Rights API.\n * @see https://whitepaper.open-data-rights.org\n */\nexport abstract class OpenDataRightsProvider extends DataRequestProvider {\n    /* Set a flag that email is required for this provider */\n    public static requiresUrl = true;\n\n    /* The Open Data Rights API URL, for use in the class */\n    protected url: string;\n\n    /* A convenience method to pass to withSecureWindow */\n    protected windowParams: {\n        key: string;\n        origin: string;\n    };\n\n    setUrl(url: string): void {\n        this.url = url;\n        this.windowParams = {\n            key: this.windowKey,\n            origin: url,\n        };\n    }\n}\n\nexport type ProviderUnion = typeof DataRequestProvider | typeof Provider | typeof EmailDataRequestProvider;\nexport type UninstatiatedProvider = new (windowKey: string, accountName?: string) => DataRequestProvider | Provider | EmailDataRequestProvider;\n"
  },
  {
    "path": "src/main/providers/types/index.ts",
    "content": "export interface ProviderFile {\n    filepath: string;\n    data: Buffer | string;\n}\n\nexport interface DataRequestStatus {\n    // An ISO date describing when the request was dispatched\n    dispatched?: string;\n    // An ISO data describing when the request was completed\n    completed?: string;\n    // An ISO data describing when the request was last checked\n    lastCheck?: string;\n    // An optional identifier for the request\n    requestId?: string | number;\n}\n\nexport interface InitialisedAccount {\n    // The key for the provider that supplies the data\n    provider: string;\n    // The account from which the data emanates\n    account?: string;\n    // A URL that is associated with a provider that handles APIs\n    url?: string;\n    // A path- and URL-safe version of the URL\n    hostname?: string;\n    // A random hash which ensures that sessions are kept between various\n    // invocations of browser windows.\n    windowKey: string;\n    status: DataRequestStatus;\n}\n\nexport enum ProviderUpdateType {\n    UPDATE = 'update',\n    DATA_REQUEST = 'data_request',\n}\n\nexport type InitOptionalParameters = {\n    accountName?: string;\n    apiUrl?: string;\n};\n\n"
  },
  {
    "path": "src/main/store.ts",
    "content": "import ElectronStore from 'electron-store';\nimport Keytar from 'keytar';\nimport { storePath } from './lib/constants';\n\nconst store = new ElectronStore({\n    cwd: storePath,\n    accessPropertiesByDotNotation: false,\n});\n\nexport class KeyStore {\n    static serviceName = 'nl.leinelissen.aeon';\n\n    static get(account: string) {\n        return Keytar.getPassword(KeyStore.serviceName, account);\n    }\n\n    static set(account: string, password: string) {\n        return Keytar.setPassword(KeyStore.serviceName, account, password);\n    }\n\n    static delete(account: string) {\n        return Keytar.deletePassword(KeyStore.serviceName, account);\n    }\n}\n\n\nexport default store;"
  },
  {
    "path": "src/main/updates.ts",
    "content": "import { app, autoUpdater, dialog } from 'electron';\nimport { autoUpdates } from './lib/constants';\nimport logger from './lib/logger';\n\n// GUARD: Check if auto updates are not flagged to be disabled\n// We use this particularly on macOS when testing so that we don't run into\n// codesigning issues.\nif (autoUpdates\n    && process.env.NODE_ENV === 'production'\n) {\n    // Generate feed URL\n    const server = 'https://updates.aeon.technology';\n    const url = `${server}/update/${process.platform}${process.arch === 'arm64' ? '_arm64' : ''}/${app.getVersion()}`;\n\n    // The application needs to be codesigned in order to accept updates. If the\n    // application is not codesigned, it will crash when calling this function.\n    // Since we have no way of knowing whether the application is codesigned,\n    // we'll just swallow the error.\n    try {\n        autoUpdater.setFeedURL({ url });\n    } catch (e) {\n        logger.autoUpdater.error(e);\n    }\n\n    // Periodically check for updates. The default is every six hours.\n    setInterval(() => {\n        autoUpdater.checkForUpdates();\n    }, 21600000);\n\n    // If an update is downloaded, prompt the user to install it.\n    autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {\n        const dialogOpts = {\n            type: 'info',\n            buttons: ['Restart', 'Later'],\n            title: 'Application Update',\n            message: process.platform === 'win32' ? releaseNotes : releaseName,\n            detail: 'A new version has been downloaded. Restart the application to apply the updates.',\n        };\n    \n        dialog.showMessageBox(dialogOpts).then((returnValue) => {\n            if (returnValue.response === 0) autoUpdater.quitAndInstall();\n        });\n    });\n\n    logger.autoUpdater.info('Auto-updates enabled.');\n} else {\n    logger.autoUpdater.info('Auto-updates disabled.');\n}"
  },
  {
    "path": "src/typings/cytoscape-fcose.d.ts",
    "content": "declare module 'cytoscape-fcose' {\n    import type { Ext } from 'cytoscape';\n\n    declare const fcose: Ext;\n    export default fcose;\n}"
  },
  {
    "path": "src/typings/fonts.d.ts",
    "content": "declare module '*.woff';\ndeclare module '*.woff2';"
  },
  {
    "path": "src/typings/images.d.ts",
    "content": "declare module '*.svg';"
  },
  {
    "path": "src/typings/redux-persist.d.ts",
    "content": "import { PersistConfig as BasePersistConfig } from 'redux-persist';\n\ndeclare module 'redux-persist' {\n    export interface PersistConfig extends BasePersistConfig {\n        deserialize: boolean | ((serializedState: string) => unknown);\n    }\n}"
  },
  {
    "path": "test/.gitignore",
    "content": "test-outputs\noutput"
  },
  {
    "path": "test/spec.ts",
    "content": "import { ElectronApplication, _electron as electron, ConsoleMessage, Page, BrowserContext } from 'playwright';\nimport { expect, test } from '@playwright/test';\nimport path from 'path';\nimport { existsSync } from 'fs';\nimport { mkdir, rm } from 'fs/promises';\nimport getRoute from './utilities/getRoute';\nimport getRandomNode from './utilities/getRandomNode';\n\n// Store the Electron app so we can use it in tests\nlet app: ElectronApplication;\nlet page: Page;\nlet context: BrowserContext;\n\nlet pageErrors: Error[] = [];\nlet consoleMessages: ConsoleMessage[] = [];\n\n// Annotate entire file as serial.\ntest.describe.configure({ mode: 'serial' });\n\n// Prepare the application by launching it\ntest.beforeAll(async () => {\n    // Define outputh paths\n    const outputPath = path.resolve('test', 'output');\n    const dataPath = path.resolve(outputPath, 'data');\n\n    // Delete any pre-existing files and create directory form scratch\n    if (existsSync(outputPath)) {\n        await rm(outputPath, { recursive: true });\n    }\n    await mkdir(dataPath, { recursive: true });\n\n    // Launch electron\n    const mainJsPath = path.resolve('.webpack', 'main', 'index.js');\n    app = await electron.launch({\n        args: [\n            mainJsPath,\n            '--no-auto-updates',\n            '--no-tour',\n            `--app-data-path=${dataPath}`,\n        ],\n    });\n\n    context = app.context();\n    await context.tracing.start({ screenshots: true, snapshots: true });\n});\n\ntest.beforeEach(async () => {\n    page = await app.firstWindow();\n\n    // Capture all page errors\n    page.on('pageerror', (error) => {\n        pageErrors.push(error);\n        console.error(error);\n    });\n\n    // Capature all console errors\n    page.on('console', (message) => {\n        consoleMessages.push(message);\n        console.log(message.type(), message.text(), message.location());\n    });\n});\n\ntest.afterEach(async () => {\n    await page.screenshot();\n\n    // Check if there aren't any console errors\n    expect(consoleMessages.filter((msg) => msg.type() === 'error').length).toBe(0);\n    expect(pageErrors.length).toBe(0);\n\n    // Clear messages after each test\n    consoleMessages = [];\n    pageErrors = [];\n\n    // Close the window\n    await page.reload();\n\n    // Reset the URL\n    await page.goto(page.url().split('#')[0]);\n});\n\n// Remove the temporary directory after the tests finish\n// eslint-disable-next-line no-empty-pattern\ntest.afterAll(async ({ }, testInfo) => {\n    const tracePath = testInfo.outputPath('trace.zip');\n    await context.tracing.stop({ path: tracePath });\n\n    // Close the app first\n    await app.close();\n});\n\ntest('it renders all pages', async () => {\n    await page.click('a >> text=Timeline');\n    await page.click('a >> text=Accounts');\n    await page.click('a >> text=Data');\n    await page.click('a >> text=Graph');\n    await page.click('a >> text=Settings');\n});\n\ntest('it renders menu items correctly', async () => {\n    // Check that the menu items have the correct length\n    const menuItems = await page.$$('#menu > *');\n    await expect(menuItems.length).toBe(5);\n\n    // Check that all menu items exist\n    await expect(page.locator('#menu > * >> nth=0')).toHaveText('Timeline');\n    await expect(page.locator('#menu > * >> nth=1')).toHaveText('Accounts');\n    await expect(page.locator('#menu > * >> nth=2')).toHaveText('Data');\n    await expect(page.locator('#menu > * >> nth=3')).toHaveText('Graph');\n    await expect(page.locator('#menu > * >> nth=4')).toHaveText('Settings');\n});\n\ntest('it renders the timeline correctly', async () => {\n    // Navigate to page\n    await page.click('a >> text=Timeline');\n    await page.waitForLoadState();\n\n    // Expect it to show a button to add first account\n    await expect(page.locator('#content a')).toHaveText('Add your first account');\n\n    // Expect the button to navigate to the accounts page\n    await page.click('#content a >> text=\"Add your first account\"');\n    await expect(getRoute(page)).toBe('/accounts?create-new-account');\n});\n\ntest('it renders the accounts page correctly', async () => {\n    // Navigate to page\n    await page.click('a >> text=Accounts');\n    await page.waitForLoadState();\n\n    // Check if the buttons are there\n    await expect(page.locator('button >> text=Add New Account')).toBeVisible();\n    await expect(page.locator('button >> text=Refresh Requests')).toBeVisible();\n\n});\n\ntest('it can successfully create an open data request account', async () => {\n    // Navigate to page\n    await page.click('a >> text=Accounts');\n    await page.waitForLoadState();\n\n    // Define locators for accounts\n    const emailAccounts = page.locator('#email-accounts > *');\n    const automatedAccounts = page.locator('#automated-accounts > *');\n\n    // There should not be any accounts\n    await expect(await emailAccounts.count()).toBe(0);\n    await expect(await automatedAccounts.count()).toBe(0);\n\n    // Attempt to create a new account\n    const input = page.locator('#modal input[type=url]');\n    const submitButton = page.locator('#modal button >> text=Add new open data rights account');\n    const odrDemoUrl = 'https://demo.open-data-rights.org';\n    await page.click('button >> text=Add New Account');\n    await page.click('button >> text=open data rights');\n    await expect(input).toBeVisible();\n    await expect(input).toBeEditable();\n    await expect(submitButton).toBeVisible();\n    await expect(submitButton).toBeDisabled();\n    await input.fill(odrDemoUrl);\n    await expect(input).toHaveValue(odrDemoUrl);\n    await expect(submitButton).toBeEnabled();\n    \n    // Wait for the Open Data Rights screen to open up and then capture it\n    const [odrPage] = await Promise.all([\n        app.waitForEvent('window'),\n        submitButton.click(),\n    ]);\n    await odrPage.waitForLoadState();\n    const proceedButton = odrPage.locator('a >> text=Give Aeon access');\n    await expect(proceedButton).toBeVisible();\n    await expect(proceedButton).toBeEnabled();\n    await proceedButton.click();\n    await page.waitForLoadState();\n\n    // The account should now exist and be ready to go\n    await page.waitForSelector('#automated-accounts > *:first-child');\n    await expect(await emailAccounts.count()).toBe(0);\n    await expect(await automatedAccounts.count()).toBe(1);\n\n});\n\ntest('it can successfully initiate an open data requests request', async () => {\n    // Navigate to page\n    await page.click('a >> text=Accounts');\n    await page.waitForLoadState();\n    \n    // First, open the account\n    const automatedAccounts = page.locator('#automated-accounts > *');\n    const startRequest = page.locator('button >> text=Start Data Request');\n    const completeRequest = page.locator('button >> text=Complete Data Request');\n    await expect(await automatedAccounts.first().textContent()).toContain('No data requested yet');\n    await automatedAccounts.first().click();\n\n    // Then, start the request\n    await expect(startRequest).toBeVisible();\n    await expect(startRequest).toBeEnabled();\n    await startRequest.click();\n    await page.waitForSelector('button >> text=Complete Data Request');\n    await expect(completeRequest).toBeVisible();\n    await expect(completeRequest).toBeDisabled();\n    await expect(await automatedAccounts.first().textContent()).toContain('Requested data less than a minute ago');\n});\n\ntest('it can successfully complete an open data rights requests request', async () => {\n    // Navigate to page\n    await page.click('a >> text=Accounts');\n    await page.waitForLoadState();\n\n    // Check if the refresh-button is right\n    const refresh = page.locator('button >> text=Refresh Requests');\n    await expect(refresh).toBeVisible();\n    await expect(refresh).toBeEnabled();\n\n    // Then click it\n    await Promise.all([\n        refresh.click(),\n        page.waitForSelector('span >> text=Received data less than a minute ago'),\n    ]);\n});\n\ntest('it can successfully show timeline for a completed request', async () => {\n    // Navigate to page\n    await page.click('a >> text=Timeline');\n    await page.waitForLoadState();\n    \n    // Expect a single commit\n    const commits = page.locator('[data-tour=timeline-commits-list] > *');\n    const commit = commits.first();\n    await expect(commits).toHaveCount(2);\n    await expect(commit).toBeVisible();\n    await expect(commit).toBeEnabled();\n    await commit.click();\n});\n\ntest('it can successfully show data for a completed request', async () => {\n    // Navigate to page\n    await page.click('a >> text=Data');\n    await page.waitForLoadState();\n\n    // Check the categories and find a random one\n    const categories = page.locator('[data-tour=data-categories-list] a');\n    const category = await getRandomNode(categories);\n    await expect(await categories.count()).toBeGreaterThan(0);\n    await expect(category).toBeEnabled();\n    await category.click();\n    \n    // Check the data points and find a random one\n    const data = page.locator('a[data-tour=data-data-point-button]');\n    const datum = await getRandomNode(data);\n    await expect(await data.count()).toBeGreaterThan(0);\n    await expect(datum).toBeEnabled();\n    await datum.click();\n\n    // Expect the delete data point button to be there\n    const deleteButton = page.locator('button >> text=Delete this data point >> nth=-1');\n    const menuItems = page.locator('#menu > *');\n    await expect(deleteButton).toBeVisible();\n    await expect(deleteButton).toBeEnabled();\n    await expect(menuItems).toHaveCount(5);\n    await deleteButton.click();\n    await expect(menuItems).toHaveCount(6);\n    await expect(deleteButton).toBeDisabled();\n});\n\ntest('it can erase data points', async () => {\n    // Navigate to a datum and erase it\n    await page.click('a >> text=Data');\n    await page.waitForLoadState();\n    await (await getRandomNode(page.locator('[data-tour=data-categories-list] a'))).click();\n    await (await getRandomNode(page.locator('a[data-tour=data-data-point-button]'))).click();\n    await page.locator('button >> text=Delete this data point >> nth=-1').click();\n\n    // Navigate to page \n    await page.click('a >> text=Erasure (1)');\n    await page.waitForLoadState();\n\n    // Check that everything's there\n    const remove = page.locator('#modal button >> text=Remove 1 data point');\n    const reset = page.locator('#modal button >> text=Reset removed data points');\n    const send = page.locator('#modal button >> text=Send in e-mail client');\n    await expect(remove).toBeVisible();\n    await expect(reset).toBeVisible();\n    await expect(remove).toBeEnabled();\n    await expect(reset).toBeEnabled();\n    await remove.click();\n    await expect(send).toBeVisible();\n    await expect(send).toBeEnabled();\n    await page.click('#modal button[data-telemetry-id=modal-close]');\n\n    // See if we can actually reset the removed data points\n    const menuItems = page.locator('#menu > *');\n    await expect(menuItems).toHaveCount(6);\n    await page.click('a >> text=Erasure (1)');\n    await page.waitForLoadState();\n    await reset.click();\n    await expect(menuItems).toHaveCount(5);\n});"
  },
  {
    "path": "test/utilities/getRandomNode.ts",
    "content": "import { Locator } from 'playwright';\n\n/**\n * Retrieve a random child from a locator.\n */\nasync function getRandomNode(locator: Locator): Promise<Locator> {\n    // Count the number of nodes\n    const numberOfNodes = await locator.count();\n\n    // GUARD: Check that the call did not match any nodes\n    if (numberOfNodes === 0) {\n        throw new Error('Cannot get random node from locator without matches');\n    }\n\n    // Then get a random index\n    // NOTE: locator.nth takes a zero-indexed index, so we need to subtract 1 as\n    // locator.count() is one-indexed.\n    const randomIndex = Math.floor(Math.random() * numberOfNodes - 1);\n\n    // Then return the index\n    return locator.nth(randomIndex);\n}\n\nexport default getRandomNode;"
  },
  {
    "path": "test/utilities/getRoute.ts",
    "content": "import { Page } from 'playwright';\n\n/**\n * Retrieve the react-router MemoryRouter route from the location hash\n */\nfunction getRoute(page: Page): string | null {\n    const url = page.url();\n\n    // GUARD: Check that the URL includes a hash\n    if (!url.includes('#')) {\n        return null;\n    }\n\n    // Split the URL on the hash and only retain the last part\n    const [, hash] = url.split('#');\n\n    return hash;\n}\n\nexport default getRoute;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"module\": \"commonjs\",\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"noImplicitAny\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \"./src\",\n    \"outDir\": \"dist\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"paths\": {\n      \"main\": [\"./main\"],\n      \"app\": [\"./app\"],\n    },\n    \"jsx\": \"react\",\n    \"target\": \"es6\",\n  },\n  \"include\": [\n    \"src/**/*\",\n    \"test/**/*\",\n    \"playwright.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "webpack.main.config.js",
    "content": "const [ Dotenv ] = require('./webpack.plugins');\nconst path = require('path');\n\nmodule.exports = {\n    /**\n    * This is the main entry point for your application, it's the first file\n    * that runs in the main process.\n    */\n    entry: './src/main/index.ts',\n    // Put your normal webpack config below here\n    module: {\n        rules: require('./webpack.rules'),\n    },\n    resolve: {\n        extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],\n        alias: {\n            app: path.resolve(__dirname, 'src', 'app'),\n            main: path.resolve(__dirname, 'src', 'main'),\n            // We override the call to the debug version of Nodegit, since we are\n            // not building it anyways, and webpack trips over the conditional\n            // require statement in Nodegit.\n            '../build/Debug/nodegit.node': false,\n        }\n    },\n    plugins: [\n        Dotenv,\n    ],\n    // devtool: 'source-map',\n};"
  },
  {
    "path": "webpack.plugins.js",
    "content": "const Dotenv = require('dotenv-webpack');\nconst CspHtmlWebpackPlugin = require('csp-html-webpack-plugin');\n// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;\n \nmodule.exports = [\n    new Dotenv({\n        systemvars: true,\n    }),\n    // new BundleAnalyzerPlugin(),\n    new CspHtmlWebpackPlugin({\n        'script-src': [\"'self'\", ...process.env.NODE_ENV !== 'production' ? [\"'unsafe-eval'\"] : [] ],\n        'style-src': [\"'self'\", \"'unsafe-inline'\"],\n    }, {\n        hashEnabled: {\n            'script-src': true,\n            'style-src': false\n        },\n        nonceEnabled: {\n            'script-src': true,\n            'style-src': false\n        }\n    }),\n];\n"
  },
  {
    "path": "webpack.renderer.config.js",
    "content": "const path = require('path');\nconst rules = require('./webpack.rules');\nconst plugins = require('./webpack.plugins');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\n\nconst isDevelopment = process.env.NODE_ENV !== 'production';\n\nrules.push({\n    test: /\\.(woff|woff2|svg)$/,\n    sideEffects: true,\n    type: 'asset/resource'\n});\n\nrules.push({\n    test: /\\.css$/,\n    sideEffects: true,\n    use: [{\n        loader: MiniCssExtractPlugin.loader,\n        options: {\n            publicPath: \"../\",\n        }\n    }, 'css-loader'],\n});\n\nplugins.push(new MiniCssExtractPlugin({\n    filename: \"assets/[name].css\",\n}));\n\nif (isDevelopment) {\n    const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');\n\n    plugins.push(new ReactRefreshWebpackPlugin({\n        esModule: true,\n    }));\n}\n\nmodule.exports = {\n    module: {\n        rules,\n    },\n    devServer: {\n        hot: isDevelopment,\n    },\n    plugins,\n    resolve: {\n        extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],\n        alias: {\n            app: path.resolve(__dirname, 'src', 'app'),\n            main: path.resolve(__dirname, 'src', 'main'),\n        }\n    },\n    devtool: isDevelopment ? 'source-map' : false,\n};\n"
  },
  {
    "path": "webpack.rules.js",
    "content": "module.exports = [\n    // Add support for native node modules\n    {\n        // We're specifying native_modules in the test because the asset relocator loader generates a\n        // \"fake\" .node file which is really a cjs file.\n        test: /native_modules\\/.+\\.node$/,\n        use: 'node-loader',\n    },\n    {\n        test: /\\.(m?js|node)$/,\n        parser: { amd: false },\n        use: {\n            loader: '@vercel/webpack-asset-relocator-loader',\n            options: {\n                outputAssetBase: 'native_modules',\n            },\n        },\n    },\n    {\n        test: /\\.tsx?$/,\n        exclude: /(node_modules|\\.webpack)/,\n        use: {\n            loader: 'swc-loader',\n            options: {\n                jsc: {\n                    target: \"es2020\",\n                    transform: {\n                        react: {\n                            development: process.env.NODE_ENV !== 'production',\n                            refresh: process.env.NODE_ENV !== 'production',\n                        }\n                    }\n                },\n            }\n        }\n    },\n];\n"
  }
]