├── .babelrc ├── .circleci └── config.yml ├── .dependabot └── config.yml ├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CHANGELOG.md ├── Dockerfile.offline ├── LICENSE ├── README.md ├── SECRETS.md ├── kms-secrets.develop.us-west-2.yml ├── kms-secrets.master.us-west-2.yml ├── package-lock.json ├── package.json ├── sample_events ├── hash_get_addr.event.json ├── hash_get_did.event.json ├── hash_post.event.json └── link_post.event.json ├── serverless.yml ├── sql ├── create_links.sql └── create_root_store_addresses.sql ├── src ├── __tests__ │ ├── api_handler.test.js │ └── api_handler_init.js ├── api │ ├── __tests__ │ │ ├── link_post.test.js │ │ ├── root_store_address_get.test.js │ │ ├── root_store_address_post.test.js │ │ └── root_store_addresses_post.test.js │ ├── link_delete.js │ ├── link_post.js │ ├── root_store_address_get.js │ ├── root_store_address_post.js │ └── root_store_addresses_post.js ├── api_handler.js ├── lib │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── uPortMgr.test.js.snap │ │ ├── addressMgr.test.js │ │ ├── linkMgr.test.js │ │ ├── sigMgr.test.js │ │ └── uPortMgr.test.js │ ├── addressMgr.js │ ├── linkMgr.js │ ├── sigMgr.js │ ├── uPortMgr.js │ └── validator.js └── logger.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/3box-address-server 5 | docker: 6 | - image: circleci/node:10 7 | steps: 8 | - checkout 9 | 10 | # Download and cache dependencies 11 | - restore_cache: 12 | keys: 13 | - dependencies-cache-{{ checksum "package.json" }} 14 | 15 | - run: 16 | name: install-serverless-and dependencies 17 | command: | 18 | sudo npm i -g serverless codecov 19 | npm i 20 | 21 | - run: 22 | name: test 23 | command: npm test && codecov 24 | 25 | - run: 26 | name: code-coverage 27 | command: bash <(curl -s https://codecov.io/bash) 28 | 29 | - run: 30 | name: deploy-to-serverless 31 | command: | 32 | sls package 33 | export CODE_VERSION=${CIRCLE_SHA1:0:7} 34 | if [ "${CIRCLE_BRANCH}" == "develop" ]; then 35 | sls deploy --stage develop 36 | fi 37 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 38 | sls deploy --stage master 39 | fi 40 | 41 | - save_cache: 42 | key: dependency-cache-{{ checksum "package.json" }} 43 | paths: 44 | - ./node_modules 45 | 46 | workflows: 47 | version: 2 48 | build-and-deploy: 49 | jobs: 50 | - build 51 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "javascript" 4 | target_branch: "develop" 5 | directory: "/" 6 | update_schedule: "weekly" 7 | default_reviewers: 8 | - "simonovic86" 9 | default_assignees: 10 | - "simonovic86" 11 | allowed_updates: 12 | - match: 13 | dependency_type: "production" 14 | update_type: "all" 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | .webpack 8 | .DS_Store 9 | 10 | coverage 11 | 12 | *.log 13 | *.nvmrc 14 | .idea/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.4.0 - 2020-07-09 2 | * feat: upgrade s3 ipfs repo, shard blockstore support 3 | 4 | ## v1.3.0 - 2020-05-01 5 | * chore: upgrade did-resolver and did-jwt libraries 6 | 7 | ## v1.2.0 - 2020-03-12 8 | * feat: add bunyan logger 9 | * feat: add fetch mutliple links and root store address queries 10 | 11 | ## v1.1.6 - 2020-01-24 12 | * fix: don't verify EOA type signature for erc1271 contracts 13 | 14 | ## v1.1.5 - 2020-01-15 15 | * fix: let AWS S3 client load credentials from environment variables 16 | 17 | ## v1.1.4 - 2020-01-07 18 | * feat: load secrets from env vars only (if valid) 19 | * feat: add dockerfile for local development using serverless offline 20 | * feat: allow S3 client options to be set for endpoint, addressing style and signature version 21 | 22 | ## v1.1.3 - 2019-12-16 23 | * feat: configure separate dev and prod environments 24 | * feat: add parameter validation for ethereum addresses 25 | * feat: re-enable CI 26 | * security: upgrade webpack to v4.41.3 27 | -------------------------------------------------------------------------------- /Dockerfile.offline: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | RUN npm install -g serverless 4 | 5 | WORKDIR /3box-address-server 6 | 7 | COPY package.json package-lock.json ./ 8 | RUN npm install 9 | 10 | COPY src ./src 11 | COPY serverless.yml webpack.config.js .babelrc ./ 12 | 13 | EXPOSE 3000 14 | 15 | CMD serverless offline start --noEnvironment --printOutput 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joel Thorstensson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ ⚠️ Deprecated in favor of Ceramic ⚠️ ⚠️ 2 | > 3box.js and related tools built by 3Box Labs are deprecated and no loger supported. Developers are encurraged to build with https://ceramic.network which is a more secure and decentralized protocol for sovereign data. 3 | 4 | # 3Box Address Server 5 | 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/3box/3box-address-server.svg)](https://greenkeeper.io/) 7 | 8 | `3box-address-server` is a server utilizing a REST-API that is used to associate an Ethereum address with its root OrbitDB address. This is what must be looked up to sync the user's data. 9 | 10 | ## API Endpoint 11 | 12 | ``` 13 | https://beta.3box.io/address-server/ 14 | ``` 15 | 16 | ## API Description 17 | 18 | 19 | ### Set 20 | 21 | `POST /odbAddress` 22 | 23 | #### Body 24 | 25 | ``` 26 | { 27 | address_token: 28 | } 29 | ``` 30 | 31 | The `address_token` is a [DID signed jwt](https://github.com/uport-project/did-jwt.git) of the address to be published. The payload of the `address_token` is: 32 | ``` 33 | { 34 | rootStoreAddress: 35 | } 36 | ``` 37 | 38 | #### Response 39 | 40 | | Status | Message | | 41 | |:------:|----------------|---------------------------------------------------| 42 | | 200 | Ok. | rootStoreAddress stored | 43 | | 401 | Invalid JWT | Posted token is invalid (signature, expired, etc) | 44 | | 403 | Missing data | no `rootStoreAddress` in `address_token` | 45 | | 500 | Internal Error | Internal Error | 46 | 47 | The response data follows the [`jsend`](https://labs.omniti.com/labs/jsend) standard. 48 | 49 | #### Response data 50 | ``` 51 | { 52 | status: 'success', 53 | data: { 54 | rootStoreAddress: 55 | } 56 | } 57 | ``` 58 | 59 | ### Link an ethereum address to a DID 60 | 61 | `POST /link` 62 | 63 | 64 | #### Body 65 | 66 | ``` 67 | { 68 | consent_signature: , 69 | linked_did: , 70 | consent_msg: , 71 | type: , 72 | chainId: , 73 | address: , 74 | timestamp: 75 | } 76 | ``` 77 | 78 | The `consent_signature` is a [personal_sign signature](https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html) of a consent message and the DID to be linked. The data(text) of the signature is: 79 | 80 | ``` 81 | Create a new 3Box profile 82 | 83 | - 84 | Your unique profile ID is did:muport:Qmsd89f7hg0w845hsdd 85 | ``` 86 | 87 | 88 | The ethereum address to be linked is recovered from the signature. 89 | 90 | #### Response 91 | 92 | | Status | Message | | 93 | |:------:|-----------------|--------------------------------------------------| 94 | | 200 | Ok. | Link created and stored | 95 | | 400 | Bad request | No did on the message or dids does not match | 96 | | 401 | Invalid consent | Posted signature is invalid (wrong DID, etc) | 97 | | 403 | Missing data | no `consent_signature` or `linked_did` | 98 | | 500 | Internal Error | Internal Error | 99 | 100 | The response data follows the [`jsend`](https://labs.omniti.com/labs/jsend) standard. 101 | 102 | #### Response data 103 | ``` 104 | { 105 | status: 'success', 106 | data: { 107 | did: , 108 | address: 109 | } 110 | } 111 | ``` 112 | 113 | ### Delete a link between an ethereum address and a DID 114 | 115 | `POST /linkdelete` 116 | 117 | #### Body 118 | 119 | ``` 120 | { 121 | delete_token: 122 | } 123 | ``` 124 | 125 | The `delete_token` is a [DID signed jwt](https://github.com/uport-project/did-jwt.git) with a 1 hour expiry, of the address to be deleted. The payload of the `delete_token` is: 126 | 127 | ``` 128 | { 129 | "address": "0x19bd82476fa8799e0eb7bbb03ee2a67678c01cdc", 130 | "type": "delete-address-link" 131 | } 132 | ``` 133 | 134 | #### Response 135 | 136 | | Status | Message | | 137 | |:------:|-----------------|--------------------------------------------------| 138 | | 200 | Ok. | Link deleted | 139 | | 401 | Invalid consent | delete token failed verification | 140 | | 403 | Missing data | no delete_token | 141 | | 500 | Internal Error | Internal Error | 142 | 143 | The response data follows the [`jsend`](https://labs.omniti.com/labs/jsend) standard. 144 | 145 | #### Response data 146 | ``` 147 | { 148 | status: 'success', 149 | data: 'address of link deleted' 150 | } 151 | ``` 152 | 153 | ### Get orbitDB root store address for given identity 154 | 155 | `GET /odbAddress/{identity}` 156 | 157 | Here the `ìdentity` is either a `DID` or an `ethereum address`. 158 | 159 | #### Response 160 | 161 | | Status | Message | | 162 | |:------:|-----------------|--------------------------------------------------| 163 | | 200 | Ok. | An orbitDB address is returned | 164 | | 404 | Not found | The DID or ethereum address was not found | 165 | | 500 | Internal Error | Internal Error | 166 | 167 | The response data follows the [`jsend`](https://labs.omniti.com/labs/jsend) standard. 168 | 169 | #### Response data 170 | ``` 171 | { 172 | status: 'success', 173 | data: { 174 | rootStoreAddress: 175 | } 176 | } 177 | ``` 178 | 179 | 180 | ### Get multiple orbitDB root store addresses for given identities 181 | 182 | `Post /odbAddresses` 183 | 184 | Returns multiple root store addresses from multiple identities. If some identities are present but not all, the response will only contain the ones that do have root store addresses associated with them. 185 | 186 | #### Body 187 | 188 | ``` 189 | { 190 | identities: 191 | } 192 | ``` 193 | 194 | Here the `ìdentity` is either a `DID` or an `ethereum address`. 195 | 196 | #### Response 197 | 198 | | Status | Message | | 199 | |:------:|-----------------|----------------------------------------------------| 200 | | 200 | Ok. | All or some of the orbitDB addresses are returned | 201 | | 400 | Bad request | No identities found in request | 202 | | 404 | Not found | No matching orbitDB addresses where found | 203 | | 500 | Internal Error | Internal Error | 204 | 205 | The response data follows the [`jsend`](https://labs.omniti.com/labs/jsend) standard. 206 | 207 | #### Response data 208 | ``` 209 | { 210 | status: 'success', 211 | data: { 212 | rootStoreAddresses: { 213 | : 214 | } 215 | } 216 | } 217 | ``` 218 | 219 | ## Maintainers 220 | [@simonovic86](https://github.com/simonovic86) 221 | -------------------------------------------------------------------------------- /SECRETS.md: -------------------------------------------------------------------------------- 1 | # Secrets 2 | 3 | `sls encrypt -n SECRETS:VARIABLE_NAME -v myvalue [-k keyId]` 4 | 5 | `sls decrypt [-n SECRETS:VARIABLE_NAME]` 6 | 7 | Run Local 8 | 9 | `AWS_PROFILE="profile" sls invoke local` 10 | -------------------------------------------------------------------------------- /kms-secrets.develop.us-west-2.yml: -------------------------------------------------------------------------------- 1 | secrets: 2 | SECRETS: AQICAHjHkVxHY8dk4wKV7JyoKushbE+j3zsd9T+POPjRjk4kBAEeQvQqH3RGBOojiviErnZeAAAA7zCB7AYJKoZIhvcNAQcGoIHeMIHbAgEAMIHVBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDM0AIBPQ3plANIApaQIBEICBp5sk+ZcAf19da8RRNf62IsG2/uh7pl5ssRzPGPb9PLIAMVKco/inxMnf9+0LwevKPZIeYogQaz8pBhVcHzUIJdItcmPyHA7IOh7Sfk7DmzWLKu4mRAy/vfPG/VDRNaCWECxLksIHPfeDSjQXRMhTEramyrT6VrUpw4FtxJnEQe+IbmRRD4bUcq9BpzSPNfF/33z6Oe3rO7nhrYHVmGgmEsdWBqTfKuPG 3 | keyArn: 'arn:aws:kms:us-west-2:967314784947:key/04237c90-5dbe-4bf5-ad23-c1384bbf9d17' 4 | -------------------------------------------------------------------------------- /kms-secrets.master.us-west-2.yml: -------------------------------------------------------------------------------- 1 | secrets: 2 | SECRETS: AQICAHgBbcIg6dDlPO5m6tJomMrV/9aniqMH2ULcNxfO591A7gEZ4wMSA9C4Fyac9nTf/5m5AAAA6DCB5QYJKoZIhvcNAQcGoIHXMIHUAgEAMIHOBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDDTb/pZqrqhe5bIEYAIBEICBoLIhr7vbFdW4XblIEKajHHO2TunrXSd+TPAXY8+TzqmQNaBHbdLlyLqdrHgCkEfzoJDyXtJYktRhDrH0kpujKvXkweX84VXAWck9RN6/DwE0Z/yJ5JX6dVr4Kp3iAxfuvyw/11p+MHDT1aig/e+4FGXPbDHr+m6eur6N3Zg25NnkPdIF5r5Uo5To3lWne+jtZNJa5x6uaCaH/8ZDUm22Y1g= 3 | keyArn: 'arn:aws:kms:us-west-2:967314784947:key/f80b0c26-5b70-43db-9f8a-93659f1be54a' 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3box-address-server", 3 | "version": "1.5.0", 4 | "description": "Keeps track of the root store addresses to the user managed data platform", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --verbose --runInBand", 8 | "coverage": "jest --coverage" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/3box/3box-address-server.git" 13 | }, 14 | "keywords": [ 15 | "3box", 16 | "self-managed-data", 17 | "distributed-storage", 18 | "ipfs", 19 | "uport" 20 | ], 21 | "author": "Cristóbal Castillo ", 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/3box/3box-address-server/issues" 25 | }, 26 | "homepage": "https://github.com/3box/3box-addresses-server#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.1.6", 29 | "aws-sdk-mock": "^4.1.0", 30 | "babel-core": "^6.26.3", 31 | "babel-jest": "^24.8.0", 32 | "babel-loader": "^8.0.4", 33 | "babel-preset-env": "^1.7.0", 34 | "jest": "^24.8.0", 35 | "mockdate": "^2.0.2", 36 | "serverless-kms-secrets": "^1.0.3", 37 | "serverless-offline": "^5.10.1", 38 | "serverless-webpack": "^5.2.0", 39 | "webpack": "^4.41.3", 40 | "webpack-node-externals": "^1.6.0" 41 | }, 42 | "dependencies": { 43 | "3id-resolver": "^1.0.0", 44 | "aws-sdk": "^2.574.0", 45 | "bunyan": "^1.8.12", 46 | "cids": "0.7.1", 47 | "did-jwt": "^4.2.0", 48 | "did-resolver": "^1.1.0", 49 | "eth-sig-util": "^2.0.2", 50 | "ethers": "^4.0.37", 51 | "ipfs-s3-dag-get": "^0.2.0", 52 | "is-ipfs": "^0.6.1", 53 | "js-sha256": "^0.9.0", 54 | "muport-did-resolver": "^1.0.1", 55 | "pg": "^7.4.3" 56 | }, 57 | "jest": { 58 | "coverageDirectory": "./coverage/", 59 | "collectCoverage": true, 60 | "testURL": "http://localhost" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sample_events/hash_get_addr.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "pathParameters":{ 3 | "id": "0xbf7571b900839fa871e6f6efbbfd238eaa502735" 4 | } 5 | } -------------------------------------------------------------------------------- /sample_events/hash_get_did.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "pathParameters":{ 3 | "id": "did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuBV" 4 | } 5 | } -------------------------------------------------------------------------------- /sample_events/hash_post.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"address_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImhhc2giOiJRbVdZcHpYNmhuMkpnaE5WaFNaR2NNbTlkYW1ydTZ4andaWVk5TXBaWXAzY3FIIiwiaXNzIjoiZGlkOnVwb3J0OjJvc25mSjRXeTdMQkFtMm5QQlhpcmUxV2ZRbjc1UnJWNlRzIn0.gwkXqTB0MiPLj12wwyRddgq67GFZLHegCdtnOTnc6wrmA-6u8y0Am1BKne3-5CB5_uuxpqIWNLS2ZIpdW9s0LA\"}" 3 | } 4 | -------------------------------------------------------------------------------- /sample_events/link_post.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body":"{\"consent_msg\":\"0x4920636f6e73656e7420746f206c696e6b206d7920616464726573733a200a3078626637353731623930303833396661383731653666366566626266643233386561613530323733350a746f206d79207075626c69632070726f66696c650a0a446973636c61696d65723a207075626c69632064617461206973207075626c696320666f726576657220616e642063616e206e6f7420626520756e6173736f6369617465642066726f6d20746869732070726f66696c652e204576656e20696620757064617465732c20746865206f726967696e616c20656e74726965732077696c6c20706572736973742e\",\"consent_signature\":\"0xc0e8fb9dea14122d68fd32489a49e063a58553dc6f37038f49671276177507f93096c14ea6f20b40f309add9459f49f1d06afa5331d06d75f90fb1163bf41d341c\",\"linked_did\":\"did:muport:QmQ6M4JNQAeg4o2qticS4FViozKKqEx8zzZgAKtMS8a3WW\"}" 3 | } -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: threebox-address-server 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs10.x 6 | stage: ${opt:stage, 'develop'} 7 | region: ${opt:region, 'us-west-2'} 8 | iamRoleStatements: 9 | - Effect: Allow 10 | Action: 11 | - KMS:Decrypt 12 | Resource: ${self:custom.kmsSecrets.keyArn} 13 | - Effect: Allow 14 | Action: 15 | - s3:* 16 | Resource: 'arn:aws:s3:::*' 17 | 18 | environment: 19 | SECRETS: ${self:custom.kmsSecrets.secrets.SECRETS} 20 | IPFS_PATH: /ipfs 21 | AWS_BUCKET_NAME: ${self:custom.awsBucketName.${self:provider.stage}} 22 | CODE_VERSION: ${env:CODE_VERSION} 23 | 24 | # Use the serverless-webpack plugin to transpile ES6 25 | plugins: 26 | - serverless-webpack 27 | - serverless-kms-secrets 28 | - serverless-offline 29 | 30 | # Enable auto-packing of external modules 31 | custom: 32 | awsBucketName: 33 | develop: ipfs-dev.3box.io 34 | master: ipfs-bucket-test.3box.io 35 | webpackIncludeModules: true 36 | serverless-kms-secrets: 37 | secretsFile: kms-secrets.${self:provider.stage}.${self:provider.region}.yml 38 | kmsSecrets: ${file(kms-secrets.${self:provider.stage}.${self:provider.region}.yml)} 39 | contentCompression: 1024 40 | serverless-offline: 41 | host: 0.0.0.0 42 | 43 | functions: 44 | 45 | root_store_address_post: 46 | handler: src/api_handler.root_store_address_post 47 | timeout: 30 48 | events: 49 | - http: 50 | path: odbAddress 51 | method: post 52 | cors: true 53 | 54 | root_store_address_get: 55 | handler: src/api_handler.root_store_address_get 56 | timeout: 30 57 | events: 58 | - http: 59 | path: odbAddress/{id} 60 | method: get 61 | cors: true 62 | request: 63 | parameters: 64 | paths: 65 | id: true 66 | 67 | root_store_addresses_post: 68 | handler: src/api_handler.root_store_addresses_post 69 | timeout: 30 70 | events: 71 | - http: 72 | path: odbAddresses 73 | method: post 74 | cors: true 75 | 76 | link_post: 77 | handler: src/api_handler.link_post 78 | timeout: 30 79 | events: 80 | - http: 81 | path: link 82 | method: post 83 | cors: true 84 | 85 | link_delete: 86 | handler: src/api_handler.link_delete 87 | timeout: 30 88 | events: 89 | - http: 90 | path: linkdelete 91 | method: post 92 | cors: true 93 | -------------------------------------------------------------------------------- /sql/create_links.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.links 2 | 3 | -- DROP TABLE public.links; 4 | 5 | CREATE TABLE public.links 6 | ( 7 | address VARCHAR(46) NOT NULL, -- ethereum address 8 | did VARCHAR(100) NOT NULL, -- did, 9 | consent text, 10 | type VARCHAR(64), -- account type(ethereum-eoa, erc1271) 11 | chainId VARCHAR(30), 12 | contractAddress VARCHAR(46), 13 | timestamp INT NOT NULL DEFAULT 0, 14 | CONSTRAINT links_pkey PRIMARY KEY (address) 15 | ) 16 | WITH ( 17 | OIDS=FALSE 18 | ); 19 | ALTER TABLE public.links 20 | OWNER TO threebox; 21 | -------------------------------------------------------------------------------- /sql/create_root_store_addresses.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.root_store_addresses 2 | 3 | -- DROP TABLE public.root_store_addresses; 4 | 5 | CREATE TABLE public.root_store_addresses 6 | ( 7 | root_store_address VARCHAR(200) NOT NULL, --OrbitDB root store address 8 | did VARCHAR(100), -- did 9 | CONSTRAINT root_store_addresses_pkey PRIMARY KEY (did) 10 | ) 11 | WITH ( 12 | OIDS=FALSE 13 | ); 14 | ALTER TABLE public.root_store_addresses 15 | OWNER TO threebox; 16 | -------------------------------------------------------------------------------- /src/__tests__/api_handler.test.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk' 2 | import MockAWS from 'aws-sdk-mock' 3 | MockAWS.setSDKInstance(AWS) 4 | 5 | const apiHandler = require('../api_handler') 6 | 7 | jest.mock('ipfs-s3-dag-get', () => { 8 | return { 9 | initIPFS: () => 'ipfs' 10 | } 11 | }) 12 | 13 | jest.mock('pg') 14 | import { Client } from 'pg' 15 | let pgClientMock = { 16 | connect: jest.fn(), 17 | end: jest.fn() 18 | } 19 | Client.mockImplementation(() => { 20 | return pgClientMock 21 | }) 22 | 23 | describe('apiHandler', () => { 24 | beforeAll(() => { 25 | MockAWS.mock('KMS', 'decrypt', Promise.resolve({ Plaintext: '{}' })) 26 | process.env.SECRETS = 'badSecret' 27 | }) 28 | 29 | test('root_store_address_post()', done => { 30 | apiHandler.root_store_address_post({}, {}, (err, res) => { 31 | expect(err).toBeNull() 32 | expect(res).not.toBeNull() 33 | done() 34 | }) 35 | }) 36 | 37 | test('root_store_address_get()', done => { 38 | apiHandler.root_store_address_get({}, {}, (err, res) => { 39 | expect(err).toBeNull() 40 | expect(res).not.toBeNull() 41 | done() 42 | }) 43 | }) 44 | 45 | test('link_post()', done => { 46 | apiHandler.link_post({}, {}, (err, res) => { 47 | expect(err).toBeNull() 48 | expect(res).not.toBeNull() 49 | done() 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/__tests__/api_handler_init.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk' 2 | import MockAWS from 'aws-sdk-mock' 3 | MockAWS.setSDKInstance(AWS) 4 | 5 | jest.mock('ipfs-s3-dag-get', () => { 6 | return { 7 | initIPFS: jest.fn() 8 | } 9 | }) 10 | 11 | jest.mock('pg') 12 | import { Client } from 'pg' 13 | let pgClientMock = { 14 | connect: jest.fn(), 15 | end: jest.fn() 16 | } 17 | Client.mockImplementation(() => { 18 | return pgClientMock 19 | }) 20 | 21 | const { initIPFS: initIPFSMock } = require('ipfs-s3-dag-get') 22 | 23 | describe('apiHandler', () => { 24 | const AddressMgr = require('../lib/addressMgr') 25 | const LinkMgr = require('../lib/linkMgr') 26 | const UportMgr = require('../lib/uPortMgr') 27 | let originalEnv 28 | let addressSetSecretsMock 29 | let linkSetSecretsMock 30 | let uPortSetSecretsMock 31 | let kmsDecryptMock 32 | let addressSetClientMock 33 | let linkSetClientMock 34 | 35 | beforeAll(() => { 36 | kmsDecryptMock = jest.fn() 37 | MockAWS.mock('KMS', 'decrypt', kmsDecryptMock) 38 | process.env.SECRETS = 'badSecret' 39 | }) 40 | 41 | beforeEach(() => { 42 | initIPFSMock.mockReset().mockResolvedValue('ipfs') 43 | kmsDecryptMock.mockReset().mockResolvedValue({ Plaintext: '{}' }) 44 | originalEnv = { ...process.env } 45 | addressSetSecretsMock = jest.spyOn(AddressMgr.prototype, 'setSecrets') 46 | linkSetSecretsMock = jest.spyOn(LinkMgr.prototype, 'setSecrets') 47 | uPortSetSecretsMock = jest.spyOn(UportMgr.prototype, 'setSecrets') 48 | addressSetClientMock = jest.spyOn(AddressMgr.prototype, 'setClient') 49 | linkSetClientMock = jest.spyOn(LinkMgr.prototype, 'setClient') 50 | }) 51 | 52 | afterEach(() => { 53 | process.env = originalEnv 54 | addressSetSecretsMock.mockRestore() 55 | linkSetSecretsMock.mockRestore() 56 | uPortSetSecretsMock.mockRestore() 57 | addressSetClientMock.mockRestore() 58 | linkSetClientMock.mockRestore() 59 | }) 60 | 61 | test('should be configured from environment variables if they are valid', (done) => { 62 | process.env.PG_URL = 'postgresql://user:pass@host/db' 63 | jest.isolateModules(() => { 64 | const apiHandler = require('../api_handler') 65 | 66 | apiHandler.root_store_address_get({}, {}, (err, res) => { 67 | 68 | expect(addressSetSecretsMock).toHaveBeenCalledTimes(1) 69 | expect(linkSetSecretsMock).toHaveBeenCalledTimes(1) 70 | expect(uPortSetSecretsMock).toHaveBeenCalledTimes(1) 71 | expect(kmsDecryptMock).not.toHaveBeenCalled() 72 | expect(addressSetClientMock).toHaveBeenCalledTimes(1) 73 | expect(linkSetClientMock).toHaveBeenCalledTimes(1) 74 | 75 | done() 76 | }) 77 | }) 78 | }) 79 | 80 | test('should be configured from KMS if environment variables are not valid', (done) => { 81 | jest.isolateModules(() => { 82 | const apiHandler = require('../api_handler') 83 | 84 | apiHandler.root_store_address_get({}, {}, (err, res) => { 85 | 86 | expect(addressSetSecretsMock).toHaveBeenCalledTimes(2) 87 | expect(linkSetSecretsMock).toHaveBeenCalledTimes(2) 88 | expect(uPortSetSecretsMock).toHaveBeenCalledTimes(2) 89 | expect(kmsDecryptMock).toHaveBeenCalled() 90 | expect(addressSetClientMock).toHaveBeenCalledTimes(1) 91 | expect(linkSetClientMock).toHaveBeenCalledTimes(1) 92 | 93 | done() 94 | }) 95 | }) 96 | }) 97 | 98 | test('should be configured from KMS if IPFS can\'t be initialized from environment variables', (done) => { 99 | initIPFSMock.mockRejectedValueOnce() 100 | process.env.PG_URL = 'postgresql://user:pass@host/db' 101 | jest.isolateModules(() => { 102 | const apiHandler = require('../api_handler') 103 | 104 | apiHandler.root_store_address_get({}, {}, (err, res) => { 105 | 106 | expect(addressSetSecretsMock).toHaveBeenCalledTimes(2) 107 | expect(linkSetSecretsMock).toHaveBeenCalledTimes(2) 108 | expect(uPortSetSecretsMock).toHaveBeenCalledTimes(2) 109 | expect(kmsDecryptMock).toHaveBeenCalled() 110 | expect(addressSetClientMock).toHaveBeenCalledTimes(1) 111 | expect(linkSetClientMock).toHaveBeenCalledTimes(1) 112 | 113 | done() 114 | }) 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /src/api/__tests__/link_post.test.js: -------------------------------------------------------------------------------- 1 | const LinkPostHandler = require('../link_post') 2 | 3 | describe('LinkPostHandler', () => { 4 | let sut 5 | let sigMgrMock = { 6 | verify: jest.fn() 7 | } 8 | let linkMgrMock = { 9 | store: jest.fn() 10 | } 11 | 12 | beforeAll(() => { 13 | sut = new LinkPostHandler(sigMgrMock,linkMgrMock) 14 | }) 15 | 16 | test('empty constructor', () => { 17 | expect(sut).not.toBeUndefined() 18 | }) 19 | 20 | test('handle null body', done => { 21 | sut.handle({}, {}, (err, res) => { 22 | expect(err).not.toBeNull() 23 | expect(err.code).toEqual(403) 24 | expect(err.message).toBeDefined() 25 | done() 26 | }) 27 | }) 28 | 29 | test('no consent_signature', done => { 30 | sut.handle({ body: '{}' }, {}, (err, res) => { 31 | expect(err).not.toBeNull() 32 | expect(err.code).toEqual(403) 33 | expect(err.message).toEqual('no consent_signature') 34 | done() 35 | }) 36 | }) 37 | 38 | test('no consent_msg', done => { 39 | sut.handle( 40 | { body: JSON.stringify({ consent_signature: 'somesignature' }) }, 41 | {}, 42 | (err, res) => { 43 | expect(err).not.toBeNull() 44 | expect(err.code).toEqual(403) 45 | expect(err.message).toEqual('no consent_msg') 46 | done() 47 | } 48 | ) 49 | }) 50 | 51 | test('no linked_did', done => { 52 | sut.handle( 53 | { 54 | body: JSON.stringify({ 55 | consent_signature: 'somesignature', 56 | consent_msg: 'hello world' 57 | }) 58 | }, 59 | {}, 60 | (err, res) => { 61 | expect(err).not.toBeNull() 62 | expect(err.code).toEqual(403) 63 | expect(err.message).toEqual('no linked_did') 64 | done() 65 | } 66 | ) 67 | }) 68 | 69 | test('no did present on the message', done => { 70 | sut.handle( 71 | { 72 | body: JSON.stringify({ 73 | consent_signature: 'somesignature', 74 | consent_msg: 'hello world', 75 | linked_did: 'did:muport:fake' 76 | }) 77 | }, 78 | {}, 79 | (err, res) => { 80 | expect(err).not.toBeNull() 81 | expect(err.code).toEqual(400) 82 | expect(err.message).toEqual('no did on the consent_msg') 83 | done() 84 | } 85 | ) 86 | }) 87 | 88 | test('linked_did does not match with message did', done => { 89 | sut.handle( 90 | { 91 | body: JSON.stringify({ 92 | consent_signature: 'somesignature', 93 | consent_msg: 'Create a new 3Box profile' + 94 | '\n\n' + 95 | '- \n' + 96 | 'Your unique profile ID is did:muport:other', 97 | linked_did: 'did:muport:fake' 98 | }) 99 | }, 100 | {}, 101 | (err, res) => { 102 | expect(err).not.toBeNull() 103 | expect(err.code).toEqual(400) 104 | expect(err.message).toEqual('dids does not match') 105 | done() 106 | } 107 | ) 108 | }) 109 | 110 | test('happy path', done => { 111 | sigMgrMock.verify.mockReturnValue('0xaddr') 112 | 113 | sut.handle( 114 | { 115 | body: JSON.stringify({ 116 | consent_signature: 'somesignature', 117 | consent_msg: 'Create a new 3Box profile' + 118 | '\n\n' + 119 | '- \n' + 120 | 'Your unique profile ID is did:muport:fake', 121 | linked_did: 'did:muport:fake' 122 | }) 123 | }, 124 | {}, 125 | (err, res) => { 126 | expect(err).toBeNull() 127 | expect(res).not.toBeNull() 128 | expect(res).toEqual({ did: 'did:muport:fake', address: '0xaddr' }) 129 | done() 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /src/api/__tests__/root_store_address_get.test.js: -------------------------------------------------------------------------------- 1 | const RootStoreAddressGet = require('../root_store_address_get') 2 | 3 | describe('RootStoreAddressGet', () => { 4 | let sut 5 | let addressMgrMock 6 | let linkMgrMock 7 | let address = '0xbf7571b900839fa871e6f6efbbfd238eaa502735' 8 | let did = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuBV' 9 | let rsAddress = '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVSU/first-database' 10 | 11 | beforeAll(() => { 12 | addressMgrMock = { 13 | get: jest.fn() 14 | } 15 | linkMgrMock = { 16 | get: jest.fn() 17 | } 18 | sut = new RootStoreAddressGet(addressMgrMock, linkMgrMock) 19 | }) 20 | 21 | afterEach(() => { 22 | addressMgrMock.get.mockReset() 23 | linkMgrMock.get.mockReset() 24 | }) 25 | 26 | test('empty constructor', () => { 27 | expect(sut).not.toBeUndefined() 28 | }) 29 | 30 | test('no parameters', done => { 31 | sut.handle({}, {}, (err, res) => { 32 | expect(err).not.toBeNull() 33 | expect(err.code).toEqual(400) 34 | expect(err.message).toEqual('no id parameter') 35 | done() 36 | }) 37 | }) 38 | 39 | test('handle not linked address', done => { 40 | sut.handle({ pathParameters: { id: address } }, {}, (err, res) => { 41 | linkMgrMock.get.mockReturnValue(null) 42 | 43 | expect(linkMgrMock.get).toBeCalledWith(address) 44 | 45 | expect(err).not.toBeNull() 46 | expect(err.code).toEqual(404) 47 | expect(err.message).toEqual('address not linked') 48 | done() 49 | }) 50 | }) 51 | 52 | test('handle invalid id', done => { 53 | sut.handle({ pathParameters: { id: 'badId' } }, {}, (err, res) => { 54 | expect(err).not.toBeNull() 55 | expect(err.code).toEqual(401) 56 | expect(err.message).toEqual('invalid id') 57 | done() 58 | }) 59 | }) 60 | 61 | test('handle rootStoreAddress not found', done => { 62 | sut.handle({ pathParameters: { id: did } }, {}, (err, res) => { 63 | addressMgrMock.get.mockReturnValue(null) 64 | 65 | expect(addressMgrMock.get).toBeCalledWith(did) 66 | 67 | expect(err).not.toBeNull() 68 | expect(err.code).toEqual(404) 69 | expect(err.message).toEqual('root store address not found') 70 | done() 71 | }) 72 | }) 73 | 74 | test('adress contains uppercase', done => { 75 | let invalidAddress = '0xBF7571b900839fa871e6f6efbbfd238eaa502735' 76 | linkMgrMock.get.mockReturnValue({ did: did }) 77 | addressMgrMock.get.mockReturnValue({ root_store_address: rsAddress }) 78 | 79 | sut.handle({ pathParameters: { id: invalidAddress } }, {}, (err, res) => { 80 | expect(err).not.toBeNull() 81 | expect(err.code).toEqual(403) 82 | expect(err.message).toEqual('Error: must be a lowercase hex string') 83 | done() 84 | }) 85 | }) 86 | 87 | test('happy path (address)', done => { 88 | linkMgrMock.get.mockReturnValue({ did: did }) 89 | addressMgrMock.get.mockReturnValue({ root_store_address: rsAddress }) 90 | 91 | sut.handle({ pathParameters: { id: address } }, {}, (err, res) => { 92 | expect(linkMgrMock.get).toBeCalledWith(address) 93 | expect(addressMgrMock.get).toBeCalledWith(did) 94 | 95 | expect(err).toBeNull() 96 | expect(res).not.toBeNull() 97 | expect(res).toEqual({ rootStoreAddress: rsAddress, did }) 98 | done() 99 | }) 100 | }) 101 | 102 | test('happy path (did)', done => { 103 | addressMgrMock.get.mockReturnValue({ root_store_address: rsAddress }) 104 | 105 | sut.handle({ pathParameters: { id: did } }, {}, (err, res) => { 106 | 107 | expect(addressMgrMock.get).toBeCalledWith(did) 108 | expect(err).toBeNull() 109 | expect(res).not.toBeNull() 110 | expect(res).toEqual({ rootStoreAddress: rsAddress, did }) 111 | done() 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/api/__tests__/root_store_address_post.test.js: -------------------------------------------------------------------------------- 1 | const RootStoreAddressPost = require('../root_store_address_post') 2 | const UportMgr = require('../../lib/uPortMgr') 3 | const MockDate = require('mockdate') 4 | 5 | describe('RootStoreAddressPost', () => { 6 | let sut 7 | let uPortMgrMock = new UportMgr() 8 | let addressMgrMock = { 9 | store: jest.fn() 10 | } 11 | 12 | const validToken = 13 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOiIxNDg1MzIxMTMzIiwicm9vdFN0b3JlQWRkcmVzcyI6Ii9vcmJpdGRiL1FtZDhUbVpyV0FTeXBFcDRFcjl0Z1dQNGtDTlFuVzRuY1Nudmp2eUhRM0VWU1UvZmlyc3QtZGF0YWJhc2UiLCJpc3MiOiJkaWQ6dXBvcnQ6Mm9zbmZKNFd5N0xCQW0yblBCWGlyZTFXZlFuNzVSclY2VHMifQ.teUclheFRoojKYNy4l7mbWF3PY1jAOpHj5hKdJHOfVxjTdr3wsd5-dPeCFIemtWxoLDhd7NAdKxk-GLR7xscIQ' 14 | const invalidAudToken = 15 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOiIxNDg1MzIxMTMzIiwicm9vdFN0b3JlQWRkcmVzcyI6Ii9vcmJpdGRiL1FtZDhUbVpyV0FTeXBFcDRFcjl0Z1dQNGtDTlFuVzRuY1Nudmp2eUhRM0VWU1UvZmlyc3QtZGF0YWJhc2UiLCJhdWQiOiJkaWQ6dXBvcnQ6Mm9zbmZKNFd5N0xCQW0yblBCWGlyZTFXZlFuNzVSclY2VHMiLCJpc3MiOiJkaWQ6dXBvcnQ6Mm9zbmZKNFd5N0xCQW0yblBCWGlyZTFXZlFuNzVSclY2VHMifQ.zOoruTDRqmjXXqlwOh260_EaLYX0BtySZLk8R0Emqxza7Xm_oB5zaxOrph9oShMaVJVBuZUactnDeevfqZMpVQ' 16 | 17 | const invalidToken = 18 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImhhcyI6IlFtV1lwelg2aG4ySmdoTlZoU1pHY01tOWRhbXJ1Nnhqd1pZWTlNcFpZcDNjcUgiLCJpc3MiOiJkaWQ6dXBvcnQ6Mm9zbmZKNFd5N0xCQW0yblBCWGlyZTFXZlFuNzVSclY2VHMifQ.EpAYedYq9IEqgGkvGyvUPsrqCKIqs98YlwpYyPKc46rlZcrJozrNog6lH4AyBW1d3ecJgdxwzq7PNzpgJFWY6A' 19 | beforeAll(() => { 20 | sut = new RootStoreAddressPost(uPortMgrMock, addressMgrMock) 21 | }) 22 | 23 | test('empty constructor', () => { 24 | expect(sut).not.toBeUndefined() 25 | }) 26 | 27 | test('handle null body', done => { 28 | sut.handle({}, {}, (err, res) => { 29 | expect(err).not.toBeNull() 30 | expect(err.code).toEqual(403) 31 | expect(err.message).toBeDefined() 32 | done() 33 | }) 34 | }) 35 | 36 | test('handle empty body', done => { 37 | sut.handle({ body: '{}' }, {}, (err, res) => { 38 | expect(err).not.toBeNull() 39 | expect(err.code).toEqual(401) 40 | expect(err.message).toEqual('Invalid JWT') 41 | done() 42 | }) 43 | }) 44 | 45 | test('handle invalid token', done => { 46 | sut.handle( 47 | { body: JSON.stringify({ event_token: 'a.s.df' }) }, 48 | {}, 49 | (err, res) => { 50 | expect(err).not.toBeNull() 51 | expect(err.code).toEqual(401) 52 | expect(err.message).toEqual('Invalid JWT') 53 | done() 54 | } 55 | ) 56 | }) 57 | 58 | test('handle invalid token', done => { 59 | const NOW = 1485321133 60 | MockDate.set(NOW * 1000 + 123) 61 | 62 | sut.handle( 63 | { body: JSON.stringify({ address_token: invalidToken }) }, 64 | {}, 65 | (err, res) => { 66 | expect(err).toBeDefined() 67 | expect(err.code).toEqual(401) 68 | expect(err.message).toEqual('Invalid JWT') 69 | done() 70 | } 71 | ) 72 | }) 73 | 74 | test('handle valid token, but error decoding it', done => { 75 | sut.handle( 76 | { body: JSON.stringify({ address_token: invalidAudToken }) }, 77 | {}, 78 | (err, res) => { 79 | expect(err).toBeDefined() 80 | expect(err.code).toEqual(401) 81 | expect(err.message).toEqual('Invalid JWT') 82 | done() 83 | } 84 | ) 85 | }) 86 | 87 | // This test isn't working, using uport did resolver 88 | // We need to update it to use muport (and 3id in future) 89 | test.skip('handle valid token', done => { 90 | sut.handle( 91 | { 92 | body: JSON.stringify({ 93 | address_token: validToken 94 | }) 95 | }, 96 | {}, 97 | (err, res) => { 98 | expect(err).toBeNull() 99 | expect(res).toEqual( 100 | '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVSU/first-database' 101 | ) 102 | done() 103 | } 104 | ) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /src/api/__tests__/root_store_addresses_post.test.js: -------------------------------------------------------------------------------- 1 | const RootStoreAddressesPost = require('../root_store_addresses_post') 2 | 3 | const formatEvent = obj => { 4 | return { body: JSON.stringify(obj) } 5 | } 6 | 7 | describe('RootStoreAddressesPost', () => { 8 | let sut 9 | let addressMgrMock 10 | let linkMgrMock 11 | let address1 = '0xbf7571b900839fa871e6f6efbbfd238eaa502735' 12 | let address2 = '0xbf7571b900839fa871e6f6efbbfd238eaa502736' 13 | let did1 = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuB1' 14 | let did2 = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuB2' 15 | let did3 = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuB3' 16 | let did4 = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuB4' 17 | let rsAddress1 = '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVS1/first-database' 18 | let rsAddress2 = '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVS2/first-database' 19 | let rsAddress3 = '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVS3/first-database' 20 | let rsAddress4 = '/orbitdb/Qmd8TmZrWASypEp4Er9tgWP4kCNQnW4ncSnvjvyHQ3EVS4/first-database' 21 | 22 | beforeEach(() => { 23 | addressMgrMock = { 24 | get: jest.fn().mockImplementation(did => { 25 | if (did === did1) { 26 | return { root_store_address: rsAddress1 } 27 | } else if (did === did2) { 28 | return { root_store_address: rsAddress2 } 29 | } else if (did === did3) { 30 | return { root_store_address: rsAddress3 } 31 | } else if (did === did4) { 32 | return { root_store_address: rsAddress4 } 33 | } 34 | }), 35 | getMultiple: jest.fn().mockImplementation(dids => { 36 | return dids.map((did) => { 37 | const rsAddressRow = addressMgrMock.get(did) 38 | if (rsAddressRow) { 39 | rsAddressRow.did = did 40 | return rsAddressRow 41 | } 42 | }).filter(rsAddressRow => !!rsAddressRow) 43 | }) 44 | } 45 | linkMgrMock = { 46 | get: jest.fn().mockImplementation(addr => { 47 | if (addr === address1) { 48 | return { did: did1 } 49 | } else if (addr === address2) { 50 | return { did: did2 } 51 | } 52 | }), 53 | getMultiple: jest.fn().mockImplementation(addresses => { 54 | return addresses.map((addr) => { 55 | const didRow = linkMgrMock.get(addr) 56 | if (didRow) { 57 | didRow.address = addr 58 | return didRow 59 | } 60 | }).filter(didRow => !!didRow) 61 | }) 62 | } 63 | sut = new RootStoreAddressesPost(addressMgrMock, linkMgrMock) 64 | }) 65 | 66 | afterEach(() => { 67 | addressMgrMock.get.mockReset() 68 | linkMgrMock.get.mockReset() 69 | }) 70 | 71 | test('empty constructor', () => { 72 | expect(sut).not.toBeUndefined() 73 | }) 74 | 75 | test('no body', done => { 76 | sut.handle({}, {}, (err, res) => { 77 | expect(err).not.toBeNull() 78 | expect(err.code).toEqual(403) 79 | expect(err.message).toContain('no json body: ') 80 | done() 81 | }) 82 | }) 83 | 84 | test('no parameters', done => { 85 | sut.handle(formatEvent({}), {}, (err, res) => { 86 | expect(err).not.toBeNull() 87 | expect(err.code).toEqual(400) 88 | expect(err.message).toContain('no identities in parameter') 89 | sut.handle(formatEvent({ identities: "asdf" }), {}, (err, res) => { 90 | expect(err).not.toBeNull() 91 | expect(err.code).toEqual(400) 92 | expect(err.message).toContain('no identities in parameter') 93 | sut.handle(formatEvent({ identities: [] }), {}, (err, res) => { 94 | expect(err).not.toBeNull() 95 | expect(err.code).toEqual(400) 96 | expect(err.message).toContain('no identities in parameter') 97 | done() 98 | }) 99 | }) 100 | }) 101 | }) 102 | 103 | test('happy path (dids)', done => { 104 | 105 | sut.handle(formatEvent({ identities: [did1, did2, did3, did4] }), {}, (err, res) => { 106 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 107 | expect(addressMgrMock.get).toHaveBeenCalledWith(did2) 108 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 109 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 110 | expect(addressMgrMock.get).toHaveBeenCalledTimes(4) 111 | expect(err).toBeNull() 112 | const expectedRes = { rootStoreAddresses: {} } 113 | expectedRes.rootStoreAddresses[did1] = rsAddress1 114 | expectedRes.rootStoreAddresses[did2] = rsAddress2 115 | expectedRes.rootStoreAddresses[did3] = rsAddress3 116 | expectedRes.rootStoreAddresses[did4] = rsAddress4 117 | expect(res).toEqual(expectedRes) 118 | done() 119 | }) 120 | }) 121 | 122 | test('happy path (addresses)', done => { 123 | 124 | sut.handle(formatEvent({ identities: [address1, address2] }), {}, (err, res) => { 125 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 126 | expect(linkMgrMock.get).toHaveBeenCalledWith(address2) 127 | expect(linkMgrMock.get).toHaveBeenCalledTimes(2) 128 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 129 | expect(addressMgrMock.get).toHaveBeenCalledWith(did2) 130 | expect(addressMgrMock.get).toHaveBeenCalledTimes(2) 131 | expect(err).toBeNull() 132 | const expectedRes = { rootStoreAddresses: {} } 133 | expectedRes.rootStoreAddresses[address1] = rsAddress1 134 | expectedRes.rootStoreAddresses[address2] = rsAddress2 135 | expect(res).toEqual(expectedRes) 136 | done() 137 | }) 138 | }) 139 | 140 | test('happy path (mixed dids, addresses)', done => { 141 | 142 | sut.handle(formatEvent({ identities: [address1, address2, did3, did4] }), {}, (err, res) => { 143 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 144 | expect(linkMgrMock.get).toHaveBeenCalledWith(address2) 145 | expect(linkMgrMock.get).toHaveBeenCalledTimes(2) 146 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 147 | expect(addressMgrMock.get).toHaveBeenCalledWith(did2) 148 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 149 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 150 | expect(addressMgrMock.get).toHaveBeenCalledTimes(4) 151 | expect(err).toBeNull() 152 | const expectedRes = { rootStoreAddresses: {} } 153 | expectedRes.rootStoreAddresses[address1] = rsAddress1 154 | expectedRes.rootStoreAddresses[address2] = rsAddress2 155 | expectedRes.rootStoreAddresses[did3] = rsAddress3 156 | expectedRes.rootStoreAddresses[did4] = rsAddress4 157 | expect(res).toEqual(expectedRes) 158 | done() 159 | }) 160 | }) 161 | 162 | test('happy path (mixed dids, addresses, non linked address)', done => { 163 | linkMgrMock.get.mockReset() 164 | linkMgrMock.get.mockReturnValueOnce({ did: did1 }) 165 | // here address2 is not linked 166 | sut.handle(formatEvent({ identities: [address1, address2, did3, did4] }), {}, (err, res) => { 167 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 168 | expect(linkMgrMock.get).toHaveBeenCalledWith(address2) 169 | expect(linkMgrMock.get).toHaveBeenCalledTimes(2) 170 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 171 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 172 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 173 | expect(addressMgrMock.get).toHaveBeenCalledTimes(3) 174 | expect(err).toBeNull() 175 | const expectedRes = { rootStoreAddresses: {} } 176 | expectedRes.rootStoreAddresses[address1] = rsAddress1 177 | expectedRes.rootStoreAddresses[did3] = rsAddress3 178 | expectedRes.rootStoreAddresses[did4] = rsAddress4 179 | expect(res).toEqual(expectedRes) 180 | done() 181 | }) 182 | }) 183 | 184 | test('happy path (mixed dids, addresses, non published rootStore)', done => { 185 | addressMgrMock.get = jest.fn().mockImplementation(did => { 186 | if (did === did1) { 187 | return { root_store_address: rsAddress1 } 188 | } else if (did === did3) { 189 | return { root_store_address: rsAddress3 } 190 | } 191 | }) 192 | 193 | sut.handle(formatEvent({ identities: [address1, address2, did3, did4] }), {}, (err, res) => { 194 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 195 | expect(linkMgrMock.get).toHaveBeenCalledWith(address2) 196 | expect(linkMgrMock.get).toHaveBeenCalledTimes(2) 197 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 198 | expect(addressMgrMock.get).toHaveBeenCalledWith(did2) 199 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 200 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 201 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 202 | expect(addressMgrMock.get).toHaveBeenCalledTimes(4) 203 | expect(err).toBeNull() 204 | const expectedRes = { rootStoreAddresses: {} } 205 | expectedRes.rootStoreAddresses[address1] = rsAddress1 206 | expectedRes.rootStoreAddresses[did3] = rsAddress3 207 | expect(res).toEqual(expectedRes) 208 | done() 209 | }) 210 | }) 211 | 212 | test('happy path (mixed dids, addresses, invalid id)', done => { 213 | sut.handle(formatEvent({ identities: [address1, 'invalidId', did3, did4] }), {}, (err, res) => { 214 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 215 | expect(linkMgrMock.get).toHaveBeenCalledTimes(1) 216 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 217 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 218 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 219 | expect(addressMgrMock.get).toHaveBeenCalledTimes(3) 220 | expect(err).toBeNull() 221 | const expectedRes = { rootStoreAddresses: {} } 222 | expectedRes.rootStoreAddresses[address1] = rsAddress1 223 | expectedRes.rootStoreAddresses[did3] = rsAddress3 224 | expectedRes.rootStoreAddresses[did4] = rsAddress4 225 | expect(res).toEqual(expectedRes) 226 | done() 227 | }) 228 | }) 229 | 230 | test('happy path (mixed dids, addresses, upper-case addresses)', done => { 231 | sut.handle(formatEvent({ identities: [address1, '0xBF7571b900839fa871e6f6efbbfd238eaa502736', did3, did4] }), {}, (err, res) => { 232 | expect(linkMgrMock.get).toHaveBeenCalledWith(address1) 233 | expect(linkMgrMock.get).toHaveBeenCalledTimes(1) 234 | expect(addressMgrMock.get).toHaveBeenCalledWith(did1) 235 | expect(addressMgrMock.get).toHaveBeenCalledWith(did3) 236 | expect(addressMgrMock.get).toHaveBeenCalledWith(did4) 237 | expect(addressMgrMock.get).toHaveBeenCalledTimes(3) 238 | expect(err).toBeNull() 239 | const expectedRes = { rootStoreAddresses: {} } 240 | expectedRes.rootStoreAddresses[address1] = rsAddress1 241 | expectedRes.rootStoreAddresses[did3] = rsAddress3 242 | expectedRes.rootStoreAddresses[did4] = rsAddress4 243 | expect(res).toEqual(expectedRes) 244 | done() 245 | }) 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /src/api/link_delete.js: -------------------------------------------------------------------------------- 1 | const { createLogger } = require("../logger") 2 | 3 | class LinkDeleteHandler { 4 | constructor(uPortMgr, linkMgr) { 5 | this.uPortMgr = uPortMgr 6 | this.linkMgr = linkMgr 7 | this.logger = createLogger({ name: "api.link_delete" }) 8 | } 9 | 10 | async handle(event, context, cb) { 11 | // Parse body 12 | let body 13 | try { 14 | body = JSON.parse(event.body) 15 | } catch (e) { 16 | cb({ code: 403, message: 'no json body: ' + e.toString() }) 17 | return 18 | } 19 | 20 | // Check if address_token is present 21 | if (!body.delete_token) { 22 | cb({ code: 401, message: 'Invalid JWT' }) 23 | return 24 | } 25 | 26 | // Check that sig is valid, all set for 1hr valid, checks if expired 27 | let payload 28 | try { 29 | let token = await this.uPortMgr.verifyToken(body.delete_token) 30 | payload = token.payload 31 | } catch (error) { 32 | this.logger.error({ 33 | msg: 'Error verifying the token', 34 | err: error, 35 | }) 36 | cb({ code: 401, message: 'Invalid JWT' }) 37 | return 38 | } 39 | 40 | const address = payload.address 41 | const typeValid = payload.type === `delete-address-link` 42 | 43 | if (!typeValid) { 44 | cb({ code: 400, message: 'token message invalid' }) 45 | return 46 | } 47 | 48 | // check that addres -> did link in jwt is same as link in store already 49 | const link = await this.linkMgr.get(address) 50 | 51 | if (!link.did === payload.iss) { 52 | cb({ code: 400, message: 'attempting to delete link not created by same DID' }) 53 | return 54 | } 55 | 56 | await this.linkMgr.remove(address) 57 | 58 | cb(null, address) 59 | } 60 | } 61 | module.exports = LinkDeleteHandler 62 | -------------------------------------------------------------------------------- /src/api/link_post.js: -------------------------------------------------------------------------------- 1 | const isIPFS = require('is-ipfs') 2 | const ethers = require('ethers') 3 | 4 | const ACCOUNT_TYPES = { 5 | ethereum: 'ethereum', 6 | ethereumEOA: 'ethereum-eoa', 7 | erc1271: 'erc1271' 8 | } 9 | 10 | const SUPPORTED_CHAINS = { 11 | 1: 'homestead', 12 | 3: 'ropsten', 13 | 4: 'rinkeby', 14 | 42: 'kovan', 15 | 5: 'goerli' 16 | } 17 | 18 | const MAGIC_ERC1271_VALUE = '0x20c13b0b' 19 | 20 | // valid 3 DID or muport DID 21 | const isValidDID = did => { 22 | const parts = did.split(':') 23 | if (!parts[0] === 'did' || !(parts[1] === '3' || parts[1] === 'muport')) return false 24 | return isIPFS.cid(parts[2]) 25 | } 26 | 27 | const isValidAccountType = type => { 28 | return Object.values(ACCOUNT_TYPES).includes(type) 29 | } 30 | 31 | const isSupportedChainId = chainId => { 32 | return chainId && Boolean(SUPPORTED_CHAINS[chainId]) 33 | } 34 | 35 | const isValidTimestamp = timestamp => { 36 | return parseInt(timestamp, 10) > 0 && parseInt(timestamp, 10) <= +new Date() 37 | } 38 | 39 | const isValidSignature = async (contractAddress, chainId, sig, msg) => { 40 | const abi = [ 41 | 'function isValidSignature(bytes _messageHash, bytes _signature) public view returns (bytes4 magicValue)' 42 | ] 43 | const network = SUPPORTED_CHAINS[chainId] 44 | const ethersProvider = ethers.getDefaultProvider(network) 45 | const contract = new ethers.Contract(contractAddress, abi, ethersProvider) 46 | const message = '0x' + Buffer.from(msg, 'utf8').toString('hex') 47 | const returnValue = await contract.isValidSignature(message, sig) 48 | 49 | return returnValue === MAGIC_ERC1271_VALUE 50 | } 51 | 52 | class LinkPostHandler { 53 | constructor (sigMgr, linkMgr) { 54 | this.sigMgr = sigMgr 55 | this.linkMgr = linkMgr 56 | } 57 | async handle (event, context, cb) { 58 | // Parse body 59 | let body 60 | try { 61 | body = JSON.parse(event.body) 62 | } catch (e) { 63 | cb({ code: 403, message: 'no json body: ' + e.toString() }) 64 | return 65 | } 66 | 67 | const handlers = { 68 | '0': this.v0Handler, 69 | '1': this.v1Handler, 70 | } 71 | 72 | const version = body.version || 0 73 | 74 | if (!Object.keys(handlers).includes(version.toString())) { 75 | cb({ code: 403, message: 'invalid link proof version' }) 76 | return 77 | } 78 | 79 | let { 80 | msg, 81 | sig, 82 | did, 83 | type, 84 | chainId, 85 | contractAddress, 86 | timestamp, 87 | } = await handlers[version](body, cb) 88 | 89 | // Get address from signature + msg 90 | const address = type === ACCOUNT_TYPES.erc1271 ? contractAddress : await this.sigMgr.verify(msg, sig) 91 | const consent = JSON.stringify({ msg, sig }) 92 | 93 | await this.linkMgr.store(address, did, consent, type, chainId, contractAddress, timestamp) 94 | 95 | cb(null, { did: did, address: address }) 96 | } 97 | 98 | async v0Handler(body, cb) { 99 | // Check if data is present 100 | if (!body.consent_signature) { 101 | cb({ code: 403, message: 'no consent_signature' }) 102 | return 103 | } 104 | if (!body.consent_msg) { 105 | cb({ code: 403, message: 'no consent_msg' }) 106 | return 107 | } 108 | if (!body.linked_did) { 109 | cb({ code: 403, message: 'no linked_did' }) 110 | return 111 | } 112 | 113 | const sig = body.consent_signature 114 | const msg = body.consent_msg 115 | const did = body.linked_did 116 | 117 | const regex = /Your unique profile ID is (.*)/ 118 | 119 | let msg_did = regex.exec(msg) 120 | if (!msg_did) { 121 | cb({ code: 400, message: 'no did on the consent_msg' }) 122 | return 123 | } 124 | 125 | if (msg_did[1] !== did) { 126 | cb({ code: 400, message: 'dids does not match' }) 127 | return 128 | } 129 | 130 | return { 131 | msg, 132 | sig, 133 | did, 134 | type: ACCOUNT_TYPES.ethereumEOA, 135 | chainId: null, 136 | contractAddress: null, 137 | timestamp: null 138 | } 139 | } 140 | 141 | async v1Handler(body, cb) { 142 | // Check if data is present 143 | if (!body.signature) { 144 | cb({ code: 403, message: 'no signature' }) 145 | return 146 | } 147 | if (!body.message) { 148 | cb({ code: 403, message: 'no message' }) 149 | return 150 | } 151 | 152 | const sig = body.signature 153 | const msg = body.message 154 | const type = body.type 155 | const chainId = body.chainId 156 | const address = body.address 157 | const timestamp = body.timestamp 158 | 159 | const regex = /(did:\S+)/ 160 | 161 | const didMatch = regex.exec(msg) 162 | if (!didMatch || !isValidDID(didMatch[0])) { 163 | cb({ code: 400, message: 'no valid did in the consent_msg' }) 164 | return 165 | } 166 | const did = didMatch[0] 167 | 168 | if (Boolean(chainId) && !isSupportedChainId(chainId)) { 169 | cb({ code: 400, message: 'unsupported chainId provided' }) 170 | return 171 | } 172 | 173 | if (Boolean(type) && !isValidAccountType(type)) { 174 | cb({ code: 400, message: 'unsupported type provided' }) 175 | return 176 | } 177 | 178 | if (type === ACCOUNT_TYPES.erc1271 && !address) { 179 | cb({ code: 403, message: 'Parameter "address" needed for type erc1271' }) 180 | return 181 | } 182 | 183 | if (type === ACCOUNT_TYPES.erc1271 && !(await isValidSignature(address, chainId, sig, msg))) { 184 | cb({ code: 400, message: 'invalid signature provided' }) 185 | return 186 | } 187 | 188 | if (Boolean(timestamp) && !isValidTimestamp(timestamp)) { 189 | cb({ code: 400, message: 'invalid timestamp provided' }) 190 | return 191 | } 192 | 193 | return { 194 | msg, 195 | sig, 196 | did, 197 | type: type || ACCOUNT_TYPES.ethereumEOA, 198 | chainId: chainId || null, 199 | contractAddress: address || null, 200 | timestamp: timestamp || null 201 | } 202 | } 203 | } 204 | module.exports = LinkPostHandler 205 | -------------------------------------------------------------------------------- /src/api/root_store_address_get.js: -------------------------------------------------------------------------------- 1 | const { hexString } = require('../lib/validator') 2 | 3 | class RootStoreAddressGetHandler { 4 | constructor(addressMgr, linkMgr) { 5 | this.addressMgr = addressMgr 6 | this.linkMgr = linkMgr 7 | } 8 | 9 | async handle(event, context, cb) { 10 | if (!event.pathParameters || !event.pathParameters.id) { 11 | cb({ code: 400, message: 'no id parameter' }) 12 | return 13 | } 14 | 15 | const id = event.pathParameters.id 16 | // Check if id is an address or a did 17 | let did 18 | if (id.startsWith('0x')) { 19 | const { error } = hexString.validate(id) 20 | if (error) { 21 | cb({ code: 403, message: error.toString() }) 22 | } 23 | const didRow = await this.linkMgr.get(id) 24 | if (!didRow) { 25 | cb({ code: 404, message: 'address not linked' }) 26 | return 27 | } 28 | did = didRow.did 29 | } else if (id.startsWith('did:')) { 30 | did = id 31 | } else { 32 | cb({ code: 401, message: 'invalid id' }) 33 | return 34 | } 35 | 36 | // Get rsAddress for did from db 37 | const rsAddress = await this.addressMgr.get(did) 38 | if (!rsAddress) { 39 | cb({ code: 404, message: 'root store address not found' }) 40 | } else { 41 | cb(null, { rootStoreAddress: rsAddress.root_store_address, did }) 42 | } 43 | } 44 | } 45 | module.exports = RootStoreAddressGetHandler 46 | -------------------------------------------------------------------------------- /src/api/root_store_address_post.js: -------------------------------------------------------------------------------- 1 | const { createLogger } = require("../logger") 2 | 3 | class RootStoreAddressPost{ 4 | constructor (uPortMgr, addressMgr) { 5 | this.uPortMgr = uPortMgr 6 | this.addressMgr = addressMgr 7 | this.logger = createLogger({ name: "api.root_store_address_post" }) 8 | } 9 | 10 | async handle (event, context, cb) { 11 | // Parse body 12 | let body 13 | try { 14 | body = JSON.parse(event.body) 15 | } catch (e) { 16 | cb({ code: 403, message: 'no json body: ' + e.toString() }) 17 | return 18 | } 19 | 20 | // Check if address_token is present 21 | if (!body.address_token) { 22 | cb({ code: 401, message: 'Invalid JWT' }) 23 | return 24 | } 25 | 26 | let payload 27 | try { 28 | let dtoken = await this.uPortMgr.verifyToken(body.address_token) 29 | payload = dtoken.payload 30 | } catch (error) { 31 | this.logger.error({ 32 | msg: 'Error verifying the token', 33 | err: error, 34 | }) 35 | cb({ code: 401, message: 'Invalid JWT' }) 36 | return 37 | } 38 | 39 | // Check if root store address is present 40 | if (!payload.rootStoreAddress) { 41 | cb({ code: 403, message: 'Missing data' }) 42 | return 43 | } 44 | 45 | await this.addressMgr.store(payload.rootStoreAddress, payload.iss) 46 | 47 | cb(null, payload.rootStoreAddress) 48 | } 49 | } 50 | module.exports = RootStoreAddressPost 51 | -------------------------------------------------------------------------------- /src/api/root_store_addresses_post.js: -------------------------------------------------------------------------------- 1 | const { hexString } = require('../lib/validator') 2 | 3 | class RootStoreAddressesPostHandler { 4 | constructor(addressMgr, linkMgr) { 5 | this.addressMgr = addressMgr 6 | this.linkMgr = linkMgr 7 | } 8 | 9 | async handle(event, context, cb) { 10 | let body 11 | try { 12 | body = JSON.parse(event.body) 13 | } catch (e) { 14 | cb({ code: 403, message: 'no json body: ' + e.toString() }) 15 | return 16 | } 17 | if (!body.identities || !Array.isArray(body.identities) || body.identities.length === 0) { 18 | cb({ code: 400, message: 'no identities in parameter' }) 19 | return 20 | } 21 | 22 | const idDidMap = await this.getDIDs(body.identities) 23 | const didRootStoreMap = await this.getRootStores(Object.values(idDidMap)) 24 | 25 | const rootStoreAddresses = Object.keys(idDidMap).reduce((acc, id) => { 26 | if (didRootStoreMap[idDidMap[id]]) { 27 | acc[id] = didRootStoreMap[idDidMap[id]] 28 | } 29 | return acc 30 | }, {}) 31 | cb(null, { rootStoreAddresses }) 32 | } 33 | 34 | async getRootStores(dids) { 35 | if (!dids || !dids.length) { 36 | return {} 37 | } 38 | // Get rsAddress for did from db 39 | const rsAddressRows = await this.addressMgr.getMultiple(dids) 40 | return rsAddressRows.reduce((acc, row) => { 41 | acc[row.did] = row.root_store_address 42 | return acc 43 | }, {}) 44 | } 45 | 46 | async getDIDs(ids) { 47 | if (!ids || !ids.length) { 48 | return {} 49 | } 50 | const { dids, addresses } = ids.reduce((acc, id) => { 51 | if (id.startsWith('0x') && !hexString.validate(id).error) { 52 | acc.addresses.push(id) 53 | } else if (id.startsWith('did:')) { 54 | acc.dids.push(id) 55 | } 56 | return acc 57 | }, { dids: [], addresses: [] }) 58 | const didRows = addresses.length == 0 ? [] : await this.linkMgr.getMultiple(addresses) 59 | 60 | const results = {} 61 | didRows.forEach(row => results[row.address] = row.did) 62 | dids.forEach(did => results[did] = did) 63 | return results 64 | } 65 | } 66 | module.exports = RootStoreAddressesPostHandler 67 | -------------------------------------------------------------------------------- /src/api_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AWS = require('aws-sdk') 4 | 5 | const AddressMgr = require('./lib/addressMgr') 6 | const LinkMgr = require('./lib/linkMgr') 7 | const UportMgr = require('./lib/uPortMgr') 8 | const SigMgr = require('./lib/sigMgr') 9 | const { Client } = require('pg') 10 | 11 | const RootStoreAddressPostHanlder = require('./api/root_store_address_post') 12 | const RootStoreAddressGetHanlder = require('./api/root_store_address_get') 13 | const RootStoreAddressesPostHanlder = require('./api/root_store_addresses_post') 14 | const LinkPostHandler = require('./api/link_post') 15 | const LinkDeleteHandler = require('./api/link_delete') 16 | 17 | let uPortMgr = new UportMgr() 18 | let sigMgr = new SigMgr() 19 | let addressMgr = new AddressMgr() 20 | let linkMgr = new LinkMgr() 21 | 22 | const doHandler = async (handler, event, context, callback) => { 23 | if (!addressMgr.isDBClientSet() || !linkMgr.isDBClientSet()) { 24 | const client = new Client({ connectionString: addressMgr.pgUrl }) 25 | addressMgr.setClient(client) 26 | linkMgr.setClient(client) 27 | await client.connect() 28 | } 29 | try { 30 | handler.handle(event, context, (err, resp) => { 31 | let response 32 | if (err == null) { 33 | response = { 34 | statusCode: 200, 35 | headers: { 36 | 'Access-Control-Allow-Origin': '*', 37 | 'Access-Control-Allow-Credentials': true, 38 | 'Access-Control-Allow-Headers': 'snaphuntjwttoken', 39 | 'Access-Control-Allow-Methods': 'GET,HEAD,OPTIONS,POST,PUT' 40 | }, 41 | body: JSON.stringify({ 42 | status: 'success', 43 | data: resp 44 | }) 45 | } 46 | } else { 47 | let code = 500 48 | if (err.code) code = err.code 49 | let message = err 50 | if (err.message) message = err.message 51 | 52 | response = { 53 | statusCode: code, 54 | headers: { 55 | 'Access-Control-Allow-Origin': '*', 56 | 'Access-Control-Allow-Credentials': true, 57 | 'Access-Control-Allow-Headers': 'snaphuntjwttoken', 58 | 'Access-Control-Allow-Methods': 'GET,HEAD,OPTIONS,POST,PUT' 59 | }, 60 | body: JSON.stringify({ 61 | status: 'error', 62 | message: message 63 | }) 64 | } 65 | } 66 | 67 | callback(null, response) 68 | }) 69 | } catch (e) { 70 | callback(null, { 71 | statusCode: code, 72 | headers: { 73 | 'Access-Control-Allow-Origin': '*', 74 | 'Access-Control-Allow-Credentials': true, 75 | 'Access-Control-Allow-Headers': 'snaphuntjwttoken', 76 | 'Access-Control-Allow-Methods': 'GET,HEAD,OPTIONS,POST,PUT' 77 | }, 78 | body: JSON.stringify({ 79 | status: 'error', 80 | message: e.message 81 | }) 82 | }) 83 | } 84 | } 85 | 86 | const pick = (obj, keys) => { 87 | return keys.reduce((acc, key) => { 88 | if (key in obj) acc[key] = obj[key] 89 | return acc 90 | }, {}) 91 | } 92 | 93 | // Get config values from environment if set 94 | const configKeys = [ 95 | 'PG_URL', 96 | 'IPFS_PATH', 97 | 'AWS_BUCKET_NAME', 98 | 'AWS_S3_ENDPOINT', 99 | 'AWS_S3_ADDRESSING_STYLE', 100 | 'AWS_S3_SIGNATURE_VERSION', 101 | ] 102 | const envConfig = pick(process.env, configKeys) 103 | 104 | const preHandler = (handler, event, context, callback) => { 105 | // allows lambda function to return without closing db connection 106 | context.callbackWaitsForEmptyEventLoop = false 107 | if (!addressMgr.isSecretsSet() || !linkMgr.isSecretsSet() || !uPortMgr.isSecretsSet()) { 108 | // Try setting from environment first 109 | addressMgr.setSecrets(envConfig) 110 | linkMgr.setSecrets(envConfig) 111 | uPortMgr.setSecrets(envConfig) 112 | .catch(e => {}) // ignore error since we are retrying below if needed 113 | .finally((res) => { 114 | if (addressMgr.isSecretsSet() && linkMgr.isSecretsSet() && uPortMgr.isSecretsSet()) { 115 | return doHandler(handler, event, context, callback) 116 | } 117 | // If secrets not set form environment, get values from KMS 118 | const kms = new AWS.KMS() 119 | return kms 120 | .decrypt({ CiphertextBlob: Buffer(process.env.SECRETS, 'base64') }) 121 | .promise() 122 | .then(data => { 123 | const decrypted = String(data.Plaintext) 124 | const config = Object.assign(JSON.parse(decrypted), envConfig) 125 | addressMgr.setSecrets(config) 126 | linkMgr.setSecrets(config) 127 | return uPortMgr.setSecrets(config) 128 | }).then(res => { 129 | doHandler(handler, event, context, callback) 130 | }) 131 | }) 132 | } else { 133 | doHandler(handler, event, context, callback) 134 | } 135 | } 136 | 137 | let rsAddressPostHanlder = new RootStoreAddressPostHanlder(uPortMgr, addressMgr) 138 | module.exports.root_store_address_post = (event, context, callback) => { 139 | preHandler(rsAddressPostHanlder, event, context, callback) 140 | } 141 | 142 | let rsAddressGetHanlder = new RootStoreAddressGetHanlder(addressMgr, linkMgr) 143 | module.exports.root_store_address_get = (event, context, callback) => { 144 | preHandler(rsAddressGetHanlder, event, context, callback) 145 | } 146 | 147 | let rsAddressesPostHanlder = new RootStoreAddressesPostHanlder(addressMgr, linkMgr) 148 | module.exports.root_store_addresses_post = (event, context, callback) => { 149 | preHandler(rsAddressesPostHanlder, event, context, callback) 150 | } 151 | 152 | let linkPostHandler = new LinkPostHandler(sigMgr, linkMgr) 153 | module.exports.link_post = (event, context, callback) => { 154 | preHandler(linkPostHandler, event, context, callback) 155 | } 156 | 157 | let linkDeleteHandler = new LinkDeleteHandler(uPortMgr, linkMgr) 158 | module.exports.link_delete = (event, context, callback) => { 159 | preHandler(linkDeleteHandler, event, context, callback) 160 | } 161 | -------------------------------------------------------------------------------- /src/lib/__tests__/__snapshots__/uPortMgr.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UportMgr should verify 3id JWT correctly 1`] = ` 4 | Object { 5 | "doc": Object { 6 | "@context": "https://w3id.org/did/v1", 7 | "authentication": Array [ 8 | Object { 9 | "publicKey": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda#signingKey", 10 | "type": "Secp256k1SignatureAuthentication2018", 11 | }, 12 | ], 13 | "id": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda", 14 | "publicKey": Array [ 15 | Object { 16 | "id": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda#signingKey", 17 | "publicKeyHex": "04113970c9f2818bf183ee0cce54446fce4f6e1303d5b0d380ec95b5cf97e2f866403a204a0fda234f132a437ee0c56bd2a44dff661d8546dcb4132930ed36907a", 18 | "type": "Secp256k1VerificationKey2018", 19 | }, 20 | Object { 21 | "id": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda#encryptionKey", 22 | "publicKeyBase64": "somakoiV8ZenByzOhk2Z6jxM2WCHtPB6KoPKj26YMFM=", 23 | "type": "Curve25519EncryptionPublicKey", 24 | }, 25 | Object { 26 | "ethereumAddress": "0x456bf7002f2377798ef546fb7a0D0eE6155E02e5", 27 | "id": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda#managementKey", 28 | "type": "Secp256k1VerificationKey2018", 29 | }, 30 | ], 31 | }, 32 | "issuer": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda", 33 | "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1NzQ2NzIyNTQsInJvb3RTdG9yZUFkZHJlc3MiOiIvb3JiaXRkYi9RbVNQS2lGaWQxdGRKWlhKcEptY2RNYm9ZakhiaDM0VGM5cTM4VTJvNlc4Z0VDLzEyMjBkNjE2NjljNzU5MGQ1MjI2NDU1MjIzNGMwOWM3ZmRmZTM3MTlmNDg3NjhkMGU0ZmNiNzA2YmJhYTg4YzQ4OGI0LnJvb3QiLCJpc3MiOiJkaWQ6MzpiYWZ5cmVpZmhwbXB3cHdtamhkNjR2Y3NjMzVmc2dqZ3BkcnFxZG5zeWp2NDVnZ2J6bjNsNW00ZmVkYSJ9.PG4U6Awwu-VCMDS12hnGeJ1Zop2cyL-LbXJnZGVgYsxZzTCmfrz-uNlMbRZ2r0al4WQg-dMGN6FgSNWmqEIPMQ", 34 | "payload": Object { 35 | "iat": 1574672254, 36 | "iss": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda", 37 | "rootStoreAddress": "/orbitdb/QmSPKiFid1tdJZXJpJmcdMboYjHbh34Tc9q38U2o6W8gEC/1220d61669c7590d52264552234c09c7fdfe3719f48768d0e4fcb706bbaa88c488b4.root", 38 | }, 39 | "signer": Object { 40 | "id": "did:3:bafyreifhpmpwpwmjhd64vcsc35fsgjgpdrqqdnsyjv45ggbzn3l5m4feda#signingKey", 41 | "publicKeyHex": "04113970c9f2818bf183ee0cce54446fce4f6e1303d5b0d380ec95b5cf97e2f866403a204a0fda234f132a437ee0c56bd2a44dff661d8546dcb4132930ed36907a", 42 | "type": "Secp256k1VerificationKey2018", 43 | }, 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /src/lib/__tests__/addressMgr.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('pg') 2 | import { Client } from 'pg' 3 | let pgClientMock = { 4 | connect: jest.fn(), 5 | end: jest.fn() 6 | } 7 | Client.mockImplementation(() => { 8 | return pgClientMock 9 | }) 10 | const AddressMgr = require('../addressMgr') 11 | 12 | describe('AddressMgr', () => { 13 | let sut 14 | let rsAddress = 'QmWYpzX6hn2JghNVhSZGcMm9damru6xjwZYY9MpZYp3cqH' 15 | let did = 'did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuBV' 16 | 17 | beforeEach(() => { 18 | sut = new AddressMgr() 19 | }) 20 | 21 | test('is isSecretsSet', () => { 22 | let secretSet = sut.isSecretsSet() 23 | expect(secretSet).toEqual(false) 24 | }) 25 | 26 | test('get() no pgUrl set', done => { 27 | sut 28 | .get(did) 29 | .then(resp => { 30 | fail("shouldn't return") 31 | done() 32 | }) 33 | .catch(err => { 34 | expect(err.message).toEqual('no pgUrl set') 35 | done() 36 | }) 37 | }) 38 | 39 | test('store() no pgUrl set', done => { 40 | sut 41 | .store(rsAddress,did) 42 | .then(resp => { 43 | fail("shouldn't return") 44 | done() 45 | }) 46 | .catch(err => { 47 | expect(err.message).toEqual('no pgUrl set') 48 | done() 49 | }) 50 | }) 51 | 52 | test('setSecrets', () => { 53 | expect(sut.isSecretsSet()).toEqual(false) 54 | sut.setSecrets({ PG_URL: 'fake' }) 55 | expect(sut.isSecretsSet()).toEqual(true) 56 | expect(sut.pgUrl).not.toBeUndefined() 57 | }) 58 | 59 | test('get() no did', done => { 60 | sut 61 | .get() 62 | .then(resp => { 63 | fail("shouldn't return") 64 | done() 65 | }) 66 | .catch(err => { 67 | expect(err.message).toEqual('no did') 68 | done() 69 | }) 70 | }) 71 | 72 | test.skip('get() fail pg', done => { 73 | sut.setSecrets({ PG_URL: 'fake' }) 74 | pgClientMock.connect = jest.fn( () =>{ 75 | throw new Error("pg failed"); 76 | }) 77 | sut 78 | .get(did) 79 | .then(resp => { 80 | fail("shouldn't return") 81 | done() 82 | }) 83 | .catch(err => { 84 | expect(err.message).toEqual('pg failed') 85 | done() 86 | }) 87 | }) 88 | 89 | 90 | test('get() did', done => { 91 | sut.setSecrets({ PG_URL: 'fake' }) 92 | 93 | pgClientMock.connect = jest.fn() 94 | pgClientMock.connect.mockClear() 95 | pgClientMock.end.mockClear() 96 | pgClientMock.query = jest.fn(() => { 97 | return Promise.resolve({ rows: [rsAddress] }) 98 | }) 99 | 100 | sut.setClient(pgClientMock) 101 | sut.get(did).then(resp => { 102 | //expect(pgClientMock.connect).toBeCalled() 103 | expect(pgClientMock.query).toBeCalled() 104 | expect( 105 | pgClientMock.query 106 | ).toBeCalledWith( 107 | `SELECT root_store_address FROM root_store_addresses WHERE did = $1`, 108 | [did] 109 | ) 110 | //expect(pgClientMock.end).toBeCalled() 111 | expect(resp).toEqual(rsAddress) 112 | 113 | done() 114 | }) 115 | }) 116 | 117 | test('store() no root store address', done => { 118 | sut 119 | .store() 120 | .then(resp => { 121 | fail("shouldn't return") 122 | done() 123 | }) 124 | .catch(err => { 125 | expect(err.message).toEqual('no root store address') 126 | done() 127 | }) 128 | }) 129 | 130 | test('store() no did', done => { 131 | sut 132 | .store(rsAddress) 133 | .then(resp => { 134 | fail("shouldn't return") 135 | done() 136 | }) 137 | .catch(err => { 138 | expect(err.message).toEqual('no did') 139 | done() 140 | }) 141 | }) 142 | 143 | test.skip('store() fail pg', done => { 144 | sut.setSecrets({ PG_URL: 'fake' }) 145 | pgClientMock.connect = jest.fn( () =>{ 146 | throw new Error("pg failed"); 147 | }) 148 | sut 149 | .store(rsAddress,did) 150 | .then(resp => { 151 | fail("shouldn't return") 152 | done() 153 | }) 154 | .catch(err => { 155 | expect(err.message).toEqual('pg failed') 156 | done() 157 | }) 158 | }) 159 | 160 | test('store() happy path', done => { 161 | sut.setSecrets({ PG_URL: 'fake' }) 162 | 163 | pgClientMock.connect = jest.fn() 164 | pgClientMock.connect.mockClear() 165 | pgClientMock.end.mockClear() 166 | pgClientMock.query = jest.fn(() => { 167 | return Promise.resolve(true) 168 | }) 169 | 170 | sut.setClient(pgClientMock) 171 | sut.store(rsAddress, did).then(resp => { 172 | //expect(pgClientMock.connect).toBeCalled() 173 | expect(pgClientMock.query).toBeCalled() 174 | expect( 175 | pgClientMock.query 176 | ).toBeCalledWith( 177 | `INSERT INTO root_store_addresses(root_store_address, did) VALUES ($1, $2) ON CONFLICT (did) DO UPDATE SET root_store_address = EXCLUDED.root_store_address`, 178 | [rsAddress, did] 179 | ) 180 | //expect(pgClientMock.end).toBeCalled() 181 | expect(resp).toBeTruthy() 182 | done() 183 | }) 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /src/lib/__tests__/linkMgr.test.js: -------------------------------------------------------------------------------- 1 | jest.mock("pg"); 2 | import { Client } from "pg"; 3 | let pgClientMock = { 4 | connect: jest.fn(), 5 | end: jest.fn() 6 | }; 7 | Client.mockImplementation(() => { 8 | return pgClientMock; 9 | }); 10 | const LinkMgr = require("../linkMgr"); 11 | 12 | describe("LinkMgr", () => { 13 | let sut; 14 | let address = "0xbf7571b900839fa871e6f6efbbfd238eaa502735"; 15 | let did = "did:muport:QmRhjfL4HLdB8LovGf1o43NJ8QnbfqmpdnTuBvZTewnuBV"; 16 | let consent = '0xc0e8fb9dea14122d68fd32489a49e063a58553dc6f37038f49671276177507f93096c14ea6f20b40f309add9459f49f1d06afa5331d06d75f90fb1163bf41d341c'; 17 | 18 | beforeEach(() => { 19 | sut = new LinkMgr(); 20 | }); 21 | 22 | test("is isSecretsSet", () => { 23 | let secretSet = sut.isSecretsSet(); 24 | expect(secretSet).toEqual(false); 25 | }); 26 | 27 | test.skip("get() no pgUrl set", done => { 28 | sut 29 | .get(did) 30 | .then(resp => { 31 | fail("shouldn't return"); 32 | done(); 33 | }) 34 | .catch(err => { 35 | expect(err.message).toEqual("no pgUrl set"); 36 | done(); 37 | }); 38 | }); 39 | 40 | test.skip('store() no pgUrl set', done => { 41 | sut 42 | .store(address,did,consent) 43 | .then(resp => { 44 | fail("shouldn't return") 45 | done() 46 | }) 47 | .catch(err => { 48 | expect(err.message).toEqual('no pgUrl set') 49 | done() 50 | }) 51 | }) 52 | 53 | 54 | test("setSecrets", () => { 55 | expect(sut.isSecretsSet()).toEqual(false); 56 | sut.setSecrets({ PG_URL: "fake" }); 57 | expect(sut.isSecretsSet()).toEqual(true); 58 | expect(sut.pgUrl).not.toBeUndefined(); 59 | }); 60 | 61 | test("get() no address", done => { 62 | sut 63 | .get() 64 | .then(resp => { 65 | fail("shouldn't return"); 66 | done(); 67 | }) 68 | .catch(err => { 69 | expect(err.message).toEqual("no address"); 70 | done(); 71 | }); 72 | }); 73 | 74 | test.skip('get() fail pg', done => { 75 | sut.setSecrets({ PG_URL: 'fake' }) 76 | pgClientMock.connect = jest.fn( () =>{ 77 | throw new Error("pg failed"); 78 | }) 79 | sut 80 | .get(address) 81 | .then(resp => { 82 | fail("shouldn't return") 83 | done() 84 | }) 85 | .catch(err => { 86 | expect(err.message).toEqual('pg failed') 87 | done() 88 | }) 89 | }) 90 | 91 | test("get() address", done => { 92 | sut.setSecrets({ PG_URL: "fake" }); 93 | sut.setClient(pgClientMock) 94 | 95 | pgClientMock.connect = jest.fn(); 96 | pgClientMock.connect.mockClear(); 97 | pgClientMock.end.mockClear(); 98 | pgClientMock.query = jest.fn(() => { 99 | return Promise.resolve({ rows: [did] }); 100 | }); 101 | 102 | sut.get(address).then(resp => { 103 | //expect(pgClientMock.connect).toBeCalled(); 104 | expect(pgClientMock.query).toBeCalled(); 105 | expect(pgClientMock.query).toBeCalledWith( 106 | `SELECT did FROM links WHERE address = $1`, 107 | [address] 108 | ); 109 | //expect(pgClientMock.end).toBeCalled(); 110 | expect(resp).toEqual(did); 111 | 112 | done(); 113 | }); 114 | }); 115 | 116 | test("store() no address", done => { 117 | sut 118 | .store() 119 | .then(resp => { 120 | fail("shouldn't return"); 121 | done(); 122 | }) 123 | .catch(err => { 124 | expect(err.message).toEqual("no address"); 125 | done(); 126 | }); 127 | }); 128 | 129 | test("store() no did", done => { 130 | sut 131 | .store(address) 132 | .then(resp => { 133 | fail("shouldn't return"); 134 | done(); 135 | }) 136 | .catch(err => { 137 | expect(err.message).toEqual("no did"); 138 | done(); 139 | }); 140 | }); 141 | 142 | test("store() no consent", done => { 143 | sut 144 | .store(address,did) 145 | .then(resp => { 146 | fail("shouldn't return"); 147 | done(); 148 | }) 149 | .catch(err => { 150 | expect(err.message).toEqual("no consent"); 151 | done(); 152 | }); 153 | }); 154 | 155 | test.skip('store() fail pg', done => { 156 | sut.setSecrets({ PG_URL: 'fake' }) 157 | pgClientMock.connect = jest.fn( () =>{ 158 | throw new Error("pg failed"); 159 | }) 160 | sut 161 | sut.store(address, did, consent) 162 | .then(resp => { 163 | fail("shouldn't return") 164 | done() 165 | }) 166 | .catch(err => { 167 | expect(err.message).toEqual('pg failed') 168 | done() 169 | }) 170 | }) 171 | 172 | 173 | test("store() happy path", done => { 174 | sut.setSecrets({ PG_URL: "fake" }); 175 | sut.setClient(pgClientMock) 176 | 177 | pgClientMock.connect = jest.fn(); 178 | pgClientMock.connect.mockClear(); 179 | pgClientMock.end.mockClear(); 180 | pgClientMock.query = jest.fn(() => { 181 | return Promise.resolve(true); 182 | }); 183 | 184 | sut.store(address, did, consent).then(resp => { 185 | //expect(pgClientMock.connect).toBeCalled(); 186 | expect(pgClientMock.query).toBeCalled(); 187 | expect(pgClientMock.query).toBeCalledWith( 188 | `INSERT INTO links(address, did, consent, type, chainId, contractAddress, timestamp) 189 | VALUES ($1, $2, $3, $4, $5, $6, $7) 190 | ON CONFLICT (address) DO UPDATE SET did = EXCLUDED.did, consent = EXCLUDED.consent`, 191 | [address, did, consent, undefined, undefined, undefined, undefined] 192 | ); 193 | //expect(pgClientMock.end).toBeCalled(); 194 | expect(resp).toBeTruthy(); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /src/lib/__tests__/sigMgr.test.js: -------------------------------------------------------------------------------- 1 | const SigMgr = require('../sigMgr') 2 | 3 | describe('SigMgr', () => { 4 | let sut 5 | let msg = 6 | '0x4920636f6e73656e7420746f206c696e6b206d7920616464726573733a200a3078626637353731623930303833396661383731653666366566626266643233386561613530323733350a746f206d79207075626c69632070726f66696c650a0a446973636c61696d65723a207075626c69632064617461206973207075626c696320666f726576657220616e642063616e206e6f7420626520756e6173736f6369617465642066726f6d20746869732070726f66696c652e204576656e20696620757064617465732c20746865206f726967696e616c20656e74726965732077696c6c20706572736973742e' 7 | let personalSig = 8 | '0xc0e8fb9dea14122d68fd32489a49e063a58553dc6f37038f49671276177507f93096c14ea6f20b40f309add9459f49f1d06afa5331d06d75f90fb1163bf41d341c' 9 | let addr = '0xbf7571b900839fa871e6f6efbbfd238eaa502735' 10 | 11 | beforeAll(() => { 12 | sut = new SigMgr() 13 | }) 14 | 15 | test('empty constructor', () => { 16 | expect(sut).not.toBeUndefined() 17 | }) 18 | 19 | test('verify() no msg', done => { 20 | sut 21 | .verify(null) 22 | .then(resp => { 23 | fail("shouldn't return") 24 | done() 25 | }) 26 | .catch(err => { 27 | expect(err.message).toEqual('no msg') 28 | done() 29 | }) 30 | }) 31 | 32 | test('verify() no personalSig', done => { 33 | sut 34 | .verify(msg) 35 | .then(resp => { 36 | fail("shouldn't return") 37 | done() 38 | }) 39 | .catch(err => { 40 | expect(err.message).toEqual('no personalSig') 41 | done() 42 | }) 43 | }) 44 | 45 | test('verify() happy path', done => { 46 | sut 47 | .verify(msg, personalSig) 48 | .then(resp => { 49 | expect(resp).toEqual(addr) 50 | done() 51 | }) 52 | .catch(err => { 53 | fail(err) 54 | done() 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/lib/__tests__/uPortMgr.test.js: -------------------------------------------------------------------------------- 1 | const UportMgr = require("../uPortMgr"); 2 | 3 | jest.mock('ipfs-s3-dag-get', () => ({ 4 | initIPFS: async () => { 5 | const doc = { id: 'did:3:GENESIS', '@context': 'https://w3id.org/did/v1', publicKey: [ { id: 'did:3:GENESIS#signingKey', type: 'Secp256k1VerificationKey2018', publicKeyHex: '04113970c9f2818bf183ee0cce54446fce4f6e1303d5b0d380ec95b5cf97e2f866403a204a0fda234f132a437ee0c56bd2a44dff661d8546dcb4132930ed36907a' }, { id: 'did:3:GENESIS#encryptionKey', type: 'Curve25519EncryptionPublicKey', publicKeyBase64: 'somakoiV8ZenByzOhk2Z6jxM2WCHtPB6KoPKj26YMFM=' }, { id: 'did:3:GENESIS#managementKey', type: 'Secp256k1VerificationKey2018', ethereumAddress: '0x456bf7002f2377798ef546fb7a0D0eE6155E02e5' } ], authentication: [ { type: 'Secp256k1SignatureAuthentication2018', publicKey: 'did:3:GENESIS#signingKey' } ] } 6 | return { 7 | dag: { 8 | get: async () => ({ value: doc }) 9 | } 10 | } 11 | } 12 | })) 13 | 14 | describe("UportMgr", () => { 15 | let sut; 16 | let jwts = { 17 | legacy: 18 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiIyb3pzRlFXQVU3Q3BIWkxxdTJ3U1liSkZXekROQjI2YW9DRiIsImlhdCI6MTUxMzM1MjgzNCwiZXZlbnQiOnsidHlwZSI6IlNUT1JFX0NPTk5FQ1RJT04iLCJhZGRyZXNzIjoiMm96c0ZRV0FVN0NwSFpMcXUyd1NZYkpGV3pETkIyNmFvQ0YiLCJjb25uZWN0aW9uVHlwZSI6ImNvbnRyYWN0cyIsImNvbm5lY3Rpb24iOiIweDJjYzMxOTEyYjJiMGYzMDc1YTg3YjM2NDA5MjNkNDVhMjZjZWYzZWUifSwiZXhwIjoxNTEzNDM5MjM0fQ.tqX5eEuaTEyYPUSgatK5zEsj_WpE-dIEHDc4ItpOvAZuBkSyV9_zbb0puNtDrZTVA7MlZ43FSSpf9CGIUxup-w" 19 | }; 20 | const threeIdJWT = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1NzQ2NzIyNTQsInJvb3RTdG9yZUFkZHJlc3MiOiIvb3JiaXRkYi9RbVNQS2lGaWQxdGRKWlhKcEptY2RNYm9ZakhiaDM0VGM5cTM4VTJvNlc4Z0VDLzEyMjBkNjE2NjljNzU5MGQ1MjI2NDU1MjIzNGMwOWM3ZmRmZTM3MTlmNDg3NjhkMGU0ZmNiNzA2YmJhYTg4YzQ4OGI0LnJvb3QiLCJpc3MiOiJkaWQ6MzpiYWZ5cmVpZmhwbXB3cHdtamhkNjR2Y3NjMzVmc2dqZ3BkcnFxZG5zeWp2NDVnZ2J6bjNsNW00ZmVkYSJ9.PG4U6Awwu-VCMDS12hnGeJ1Zop2cyL-LbXJnZGVgYsxZzTCmfrz-uNlMbRZ2r0al4WQg-dMGN6FgSNWmqEIPMQ' 21 | beforeAll(async () => { 22 | sut = new UportMgr(); 23 | await sut.setSecrets({ IPFS_PATH: '', AWS_BUCKET_NAME: '' }) 24 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; 25 | }); 26 | 27 | test("empty constructor", () => { 28 | expect(sut).not.toBeUndefined(); 29 | }); 30 | 31 | test("verifyToken() no token", done => { 32 | sut 33 | .verifyToken(null) 34 | .then(resp => { 35 | fail("shouldn't return"); 36 | done(); 37 | }) 38 | .catch(err => { 39 | expect(err.message).toEqual("no token"); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should verify 3id JWT correctly', async () => { 45 | expect(await sut.verifyToken(threeIdJWT)).toMatchSnapshot() 46 | }) 47 | }); 48 | -------------------------------------------------------------------------------- /src/lib/addressMgr.js: -------------------------------------------------------------------------------- 1 | class AddressMgr { 2 | constructor () { 3 | this.pgUrl = null 4 | this.client = null 5 | } 6 | 7 | isSecretsSet () { 8 | return this.pgUrl != null 9 | } 10 | 11 | setSecrets (secrets) { 12 | this.pgUrl = secrets.PG_URL 13 | } 14 | 15 | setClient(client) { 16 | this.client = client 17 | } 18 | 19 | isDBClientSet() { 20 | return this.client != null; 21 | } 22 | 23 | async store (rsAddress, did) { 24 | if (!rsAddress) throw new Error('no root store address') 25 | if (!did) throw new Error('no did') 26 | if (!this.pgUrl) throw new Error('no pgUrl set') 27 | if (!this.client) throw new Error('no client set') 28 | 29 | try { 30 | const res = await this.client.query( 31 | `INSERT INTO root_store_addresses(root_store_address, did) VALUES ($1, $2) ON CONFLICT (did) DO UPDATE SET root_store_address = EXCLUDED.root_store_address`, 32 | [rsAddress, did] 33 | ) 34 | return res 35 | } catch (e) { 36 | throw e 37 | } 38 | } 39 | 40 | async get (did) { 41 | if (!did) throw new Error('no did') 42 | if (!this.pgUrl) throw new Error('no pgUrl set') 43 | if (!this.client) throw new Error('no client set') 44 | 45 | try { 46 | const res = await this.client.query( 47 | `SELECT root_store_address FROM root_store_addresses WHERE did = $1`, 48 | [did] 49 | ) 50 | return res.rows[0] 51 | } catch (e) { 52 | throw e 53 | } 54 | } 55 | 56 | async getMultiple(dids) { 57 | if (!dids || !dids.length) throw new Error('no dids') 58 | if (!this.pgUrl) throw new Error('no pgUrl set') 59 | if (!this.client) throw new Error('no client set') 60 | 61 | try { 62 | const res = await this.client.query( 63 | `SELECT did, root_store_address FROM root_store_addresses WHERE did = ANY ($1)`, 64 | [dids] 65 | ) 66 | return res.rows 67 | } catch (e) { 68 | throw e 69 | } 70 | } 71 | } 72 | 73 | module.exports = AddressMgr 74 | -------------------------------------------------------------------------------- /src/lib/linkMgr.js: -------------------------------------------------------------------------------- 1 | class LinkMgr { 2 | constructor() { 3 | this.pgUrl = null; 4 | this.client = null; 5 | } 6 | 7 | isSecretsSet() { 8 | return this.pgUrl != null; 9 | } 10 | 11 | setSecrets(secrets) { 12 | this.pgUrl = secrets.PG_URL; 13 | } 14 | 15 | setClient(client) { 16 | this.client = client 17 | } 18 | 19 | isDBClientSet() { 20 | return this.client != null; 21 | } 22 | 23 | async store(address, did, consent, type, chainId, contractAddress, timestamp) { 24 | if (!address) throw new Error("no address"); 25 | if (!did) throw new Error("no did"); 26 | if (!consent) throw new Error("no consent"); 27 | if (!this.pgUrl) throw new Error("no pgUrl set"); 28 | if (!this.client) throw new Error('no client set') 29 | 30 | try { 31 | const res = await this.client.query( 32 | `INSERT INTO links(address, did, consent, type, chainId, contractAddress, timestamp) 33 | VALUES ($1, $2, $3, $4, $5, $6, $7) 34 | ON CONFLICT (address) DO UPDATE SET did = EXCLUDED.did, consent = EXCLUDED.consent`, 35 | [address, did, consent, type, chainId, contractAddress, timestamp] 36 | ); 37 | return res; 38 | } catch (e) { 39 | throw e; 40 | } 41 | } 42 | 43 | async get(address) { 44 | if (!address) throw new Error("no address"); 45 | if (!this.pgUrl) throw new Error("no pgUrl set"); 46 | if (!this.client) throw new Error('no client set') 47 | 48 | try { 49 | const res = await this.client.query( 50 | `SELECT did FROM links WHERE address = $1`, 51 | [address] 52 | ); 53 | return res.rows[0]; 54 | } catch (e) { 55 | throw e; 56 | } 57 | } 58 | 59 | async getMultiple(addresses) { 60 | if (!addresses || !addresses.length) throw new Error("no addresses"); 61 | if (!this.pgUrl) throw new Error("no pgUrl set"); 62 | if (!this.client) throw new Error('no client set') 63 | 64 | try { 65 | const res = await this.client.query( 66 | `SELECT address, did FROM links WHERE address = ANY ($1)`, 67 | [addresses] 68 | ); 69 | return res.rows 70 | } catch (e) { 71 | throw e; 72 | } 73 | } 74 | 75 | async remove(address) { 76 | if (!address) throw new Error("no address"); 77 | if (!this.pgUrl) throw new Error("no pgUrl set"); 78 | if (!this.client) throw new Error('no client set') 79 | 80 | try { 81 | const res = await this.client.query( 82 | `DELETE FROM links WHERE address = $1`, 83 | [address] 84 | ); 85 | return res; 86 | } catch (e) { 87 | throw e; 88 | } 89 | } 90 | } 91 | 92 | module.exports = LinkMgr; 93 | -------------------------------------------------------------------------------- /src/lib/sigMgr.js: -------------------------------------------------------------------------------- 1 | const sigUtil = require('eth-sig-util') 2 | 3 | class SigMgr { 4 | async verify (msg, personalSig) { 5 | if (!msg) throw new Error('no msg') 6 | if (!personalSig) throw new Error('no personalSig') 7 | 8 | const msgParams = { 9 | data: msg, 10 | sig: personalSig 11 | } 12 | const recovered = sigUtil.recoverPersonalSignature(msgParams) 13 | return recovered 14 | } 15 | } 16 | 17 | module.exports = SigMgr 18 | -------------------------------------------------------------------------------- /src/lib/uPortMgr.js: -------------------------------------------------------------------------------- 1 | import { verifyJWT } from "did-jwt" 2 | import { initIPFS } from "ipfs-s3-dag-get" 3 | const { Resolver } = require('did-resolver') 4 | const get3IdResolver = require('3id-resolver').getResolver 5 | const getMuportResolver = require("muport-did-resolver").getResolver 6 | 7 | 8 | class UportMgr { 9 | isSecretsSet() { 10 | return this.ipfs != null; 11 | } 12 | 13 | async setSecrets(secrets) { 14 | const config = { 15 | ipfsPath: secrets.IPFS_PATH, 16 | bucket: 'ipfs.3box.io', 17 | endpoint: secrets.AWS_S3_ENDPOINT, 18 | s3ForcePathStyle: secrets.AWS_S3_ADDRESSING_STYLE === 'path', 19 | signatureVersion: secrets.AWS_S3_SIGNATURE_VERSION, 20 | shardBlockstore: true 21 | } 22 | this.ipfs = await initIPFS(config) 23 | this.resolver = new Resolver({ 24 | ...get3IdResolver(this.ipfs), 25 | ...getMuportResolver(this.ipfs) 26 | }) 27 | } 28 | 29 | async verifyToken(token) { 30 | if (!token) throw new Error("no token"); 31 | return verifyJWT(token, { resolver: this.resolver }); 32 | } 33 | } 34 | 35 | module.exports = UportMgr; 36 | -------------------------------------------------------------------------------- /src/lib/validator.js: -------------------------------------------------------------------------------- 1 | class RegexValidator { 2 | constructor(regex, error) { 3 | this.regex = regex 4 | this.error = error 5 | } 6 | 7 | validate(input) { 8 | if (this.regex.test(input)) { 9 | return { error: null, value: input } 10 | } 11 | return { error: this.error, value: null } 12 | } 13 | } 14 | 15 | module.exports = { 16 | hexString: new RegexValidator(/^0x[0-9a-f]+$/, new Error('must be a lowercase hex string')) 17 | } 18 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const bunyan = require('bunyan') 2 | 3 | const defaultOptions = { 4 | codeVersion: process.env.CODE_VERSION, 5 | } 6 | 7 | module.exports.createLogger = (opts) => bunyan.createLogger(Object.assign({}, defaultOptions, opts)) 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var slsw = require("serverless-webpack"); 2 | var nodeExternals = require("webpack-node-externals"); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: "node", 7 | // Generate sourcemaps for proper error messages 8 | devtool: "source-map", 9 | // Since 'aws-sdk' is not compatible with webpack, 10 | // we exclude all node dependencies 11 | externals: [nodeExternals()], 12 | // Run babel on all .js files and skip those in node_modules 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | loader: "babel-loader", 18 | include: __dirname, 19 | exclude: /node_modules/ 20 | } 21 | ] 22 | } 23 | }; 24 | --------------------------------------------------------------------------------