├── .babelrc ├── .circleci └── config.yml ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── radiks-server.js ├── package.json ├── radiks-typeface.svg ├── radiks.js@2x.png ├── src ├── controllers │ ├── CentralController.ts │ ├── ModelsController.ts │ ├── RadiksController.ts │ └── StreamingController.ts ├── database │ └── mongodb.ts ├── index.ts ├── lib │ ├── constants.ts │ └── validator.ts └── types │ ├── custom.d.ts │ └── index.ts ├── test ├── controllers │ └── models-controller.test.ts ├── db.ts ├── mocks.ts ├── setup.ts ├── signer.ts ├── test-server.ts └── validator.test.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-object-rest-spread", 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-transform-classes", 17 | "add-module-exports" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # a collection of steps 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | working_directory: ~/mern-starter # directory where steps will run 5 | docker: # run the steps with Docker 6 | - image: circleci/node:10.15.1 # ...with this image as the primary container; this is where all `steps` will run 7 | - image: mongo:3.4.4 # and this image as the secondary service container 8 | steps: # a collection of executable commands 9 | - checkout # special step to check out source code to working directory 10 | - restore_cache: # special step to restore the dependency cache 11 | # Read about caching dependencies: https://circleci.com/docs/2.0/caching/ 12 | key: yarn-packages-{{ checksum "package.json" }} 13 | - run: 14 | name: Install Dependencies 15 | command: yarn install --frozen-lockfile 16 | - save_cache: # special step to save the dependency cache 17 | key: yarn-packages-{{ checksum "package.json" }} 18 | paths: 19 | - ./.cache/yarn 20 | - run: # run tests 21 | name: test 22 | command: yarn test --forceExit 23 | # - run: # run coverage report 24 | # name: code-coverage 25 | # command: './node_modules/.bin/nyc report --reporter=text-lcov' 26 | - run: # run lint 27 | name: lint 28 | command: yarn eslint 29 | - store_artifacts: # special step to save test results as as artifact 30 | # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 31 | path: test-results.xml 32 | prefix: tests 33 | - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ 34 | path: coverage 35 | prefix: coverage 36 | - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ 37 | path: test-results.xml 38 | # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost:27017/radiks-local-testing -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb-base', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'prettier/@typescript-eslint', 7 | 'plugin:prettier/recommended', 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: 'module', 12 | }, 13 | env: { 14 | 'jest/globals': true, 15 | }, 16 | rules: { 17 | 'import/prefer-default-export': [0], 18 | 'no-underscore-dangle': [0], 19 | 'no-console': [0], 20 | 'new-cap': [0], 21 | 'import/no-unresolved': [0], 22 | semi: [2, 'always'], 23 | '@typescript-eslint/explicit-function-return-type': [0], 24 | '@typescript-eslint/no-explicit-any': [0], 25 | '@typescript-eslint/explicit-member-accessibility': [0], 26 | }, 27 | plugins: ['jest'], 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | typed/ 4 | lib/ 5 | !src/lib/ 6 | app 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | typed 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Radiks-Server Changelog 2 | 3 | ## 0.2.3 - September 11th, 2019 4 | 5 | - Dependency updates from @dependabot: 6 | - [#17](https://github.com/blockstack-radiks/radiks-server/pull/17) 7 | - [#18](https://github.com/blockstack-radiks/radiks-server/pull/18) 8 | - [#23](https://github.com/blockstack-radiks/radiks-server/pull/23) 9 | - [#24](https://github.com/blockstack-radiks/radiks-server/pull/24) 10 | 11 | ## 0.2.2 - September 11th, 2019 12 | 13 | - README updates, thanks @moxiegirl! [#25](https://github.com/blockstack-radiks/radiks-server/pull/25) 14 | 15 | ## 0.2.1 - September 9th, 2019 16 | 17 | - Fixes the `radiks-server` bin command, [#16](https://github.com/blockstack-radiks/radiks-server/issues/16) 18 | 19 | ## 0.2.0 - July 26th, 2019 20 | 21 | - All code from the `v1.0.0` betas has been made backwards compatible, so we're publishing these changes as `v0.2.0`. 22 | - Port to Typescript (Thanks [@pradel](https://github.com/pradel)) 23 | - Automatically reconnect to MongoDB if the connection was closed 24 | - Fixed a bug around validating models before saving them 25 | 26 | ## 1.0.0-beta.3 - July 26th, 2019 27 | 28 | - Fix from @pradel around validating models before saving them. [#20](https://github.com/blockstack-radiks/radiks-server/pull/20) 29 | 30 | ## 1.0.0-beta.3 - July 22nd, 2019 31 | 32 | - Adds configuration to automatically try reconnecting to MongoDB if the connection was destroyed 33 | 34 | ## 1.0.0-beta.2 - July 22nd, 2019 35 | 36 | - Updated `radiks-server` bin file to use correct path, which was broken during the TypeScript merge 37 | 38 | ## 1.0.0-beta.1 - July 1st, 2019 39 | 40 | - Ported existing codebase to Typescript. Thanks to [@pradel](https://github.com/blockstack-radiks/radiks-server/pull/14)! 41 | 42 | ## 0.1.13 - June 22, 2019 43 | 44 | - Fixed CORS error blocking DELETE requests 45 | 46 | ## 0.1.12 - June 9th, 2019 47 | 48 | - Added route to return count of models for a certain query, thanks to @pradel [#9] 49 | - Update blockstack.js to v19.2.1 50 | 51 | ## 0.1.11 - April 5, 2019 52 | 53 | - Fixed an issue caused when fixing eslint errors from the [maxLimit PR](https://github.com/blockstack-radiks/radiks-server/pull/5) 54 | 55 | ## 0.1.10 - April 1, 2019 56 | 57 | - Support for adding a `maxLimit` configuration to radiks, so that you can limit the maximum number of records that can be fetched from the API. Thanks to @pradel for [their contribution!](https://github.com/blockstack-radiks/radiks-server/pull/5) 58 | 59 | ## 0.1.9 - March 25, 2019 60 | 61 | - Added support for deleting models 62 | 63 | ## 0.1.8 - March 1, 2019 64 | 65 | - Fixes a bug where saving data wouldn't work in Firefox. This was due to Firefox not accepting a wildcard (`*`) for the `Access-Control-Allow-Headers` response header. 66 | - New allowed headers: `origin`, `content-type` 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hank Stoever 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 | # radiks-server 2 | 3 | ![radiks.js](./radiks.js@2x.png) 4 | 5 | 6 | 7 | - [radiks-server](#radiks-server) 8 | - [Introduction](#introduction) 9 | - [Privacy](#privacy) 10 | - [Multi-user scenarios](#multi-user-scenarios) 11 | - [Authorization](#authorization) 12 | - [Use with built-in CLI server](#use-with-built-in-cli-server) 13 | - [Running a custom Radiks-server](#running-a-custom-radiks-server) 14 | - [Accessing the MongoDB Collection](#accessing-the-mongodb-collection) 15 | - [Using `getDB` to manually connecting to the MongoDB collection](#using-getdb-to-manually-connecting-to-the-mongodb-collection) 16 | - [Migration from Firebase (or anywhere else)](#migration-from-firebase-or-anywhere-else) 17 | - [Options](#options) 18 | 19 | 20 | 21 | ## Introduction 22 | 23 | Radiks-server is a pre-built server to index and serve data that lives in decentralized services. Specifically, it is built to index data that is stored in [Gaia](https://github.com/blockstack/gaia), and created using the front-end companion library, [radiks.js](https://github.com/hstove/radiks). 24 | 25 | Because Gaia is just a key-value store, many applications end up needing to store an 'index' of all underlying data, in order to query it in a performant and flexible way. 26 | 27 | ### Privacy 28 | 29 | Radiks is designed to store highly private information. Because radiks.js encrypts all sensitive data before it ever leaves the client, this server ends up being a 'dumb' database that simply stores all encrypted data in an easily-queryable format. 30 | 31 | This means that the server is only able to return queries for unencrypted data. Radiks.js is designed to be able to query for non-private information, and then decrypt the sensitive information on the client. 32 | 33 | ### Multi-user scenarios 34 | 35 | Many decentralized apps include publicly sharable information that is created with the intent of sharing that data with the world. In these situations, you want to be able to query across all user's data, using complicated queries like text search, joins, and filters. 36 | 37 | For example, consider a Twitter-clone app. You have many different users creating their own tweets, and those tweets are stored in their own storage backends. This ensures that the user has full control and ownership of their data. You still need a central server to keep track of everyone's tweets, so that you can serve combined timelines and perform searches. Radiks-server excels in this scenario. 38 | 39 | ### Authorization 40 | 41 | Although Radiks-server is mostly a 'dumb' database that stores an index of decentralized data, there are specific authorization rules that only allow writes with the correct demonstration that the user owns the data they're writing. 42 | 43 | Radiks.js creates and manages 'signing keys' that it uses to sign all writes that a user performs. Radiks-server is aware of this mechanism, and validates all signatures before performing a write. This guarantees that a user is not able to over-write a different user's data. 44 | 45 | Radiks-server also is built to support writes in a collaborative, but private, situation. For example, consider a collaborative document editing application, where users can create 'organizations' and invite users to that organization. All users in that organization have read and write priveleges to data related to that organization. These organizations have single 'shared key' that is used to sign and encrypt data. When an organization administrator needs to remove a user from the group, they'll revoke a previous key and create a new one. Radiks is aware of these relationships, and will only support writes that are signed with the current active key related to a group. 46 | 47 | ## Use with built-in CLI server 48 | 49 | Radiks-server is a `node.js` application that uses [MongoDB](https://www.mongodb.com/) as an underlying database. The easiest way to run `radiks-server` is to use the pre-packaged `node.js` server that is included with this `npm` package. 50 | 51 | In the future, Radiks-server will support various different databases, but right now only MongoDB is supported. 52 | 53 | 1. Install and run MongoDB 3.6 or higher. 54 | 55 | **You must use MongoDB >=3.6, because they fixed an issue with naming patterns in keys.** 56 | 57 | If you are testing on a local workstation, you can install locally or use a `docker` image. To install, visit their [download page](https://www.mongodb.com/download-center/community). You can also download MongoDB using your favorite package manager. On Mac OS, [homebrew is recommended](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/#install-mongodb-community-edition-with-homebrew). 58 | 59 | 2. On your MongoDB instance, create a database for your application data. 60 | 61 | 3. Create a username/password combination with `root` privileges on your new database. 62 | 63 | 4. Install the `radiks-server` on a workstation or server. 64 | 65 | ```bash 66 | npm install -g radiks-server 67 | ``` 68 | Or, if you prefer `yarn`: 69 | 70 | ```bash 71 | yarn global add radiks-server 72 | ``` 73 | 74 | 5. Create an `MONGODB_URI` environment variable on the same machine where you are running the `radiks-server`. 75 | 76 | Use the `mongodb://username:password@host:port/db_name` format for your variable. For example, to set this variable in a `bash` shell: 77 | 78 | ```bash 79 | export MONGODB_URI="mongodb://admin:mongome@157.245.167.8:27017/mycoolapp" 80 | ``` 81 | 82 | The default port for Mongodb is `27017`, your instance may be configured differently. By default, Radiks-server will use `'mongodb://localhost:27017/radiks-server'` as the `MONGODB_URI` value. This is suitable for local testing, but in production, you'll want to change the hostname and possible the database name. 83 | 84 | 6. Run `radiks-server` in the command line to start a server. 85 | 86 | The `radiks-server` defaults to running on port `1260`, but you can use the `PORT` environment variable to modify this. 87 | 88 | 7. Configure your application to use your `radiks-server`. 89 | 90 | To configure your applciation as a `radiks` client, use code that looks like this when starting up your application: 91 | 92 | ```js 93 | import { UserSession, AppConfig } from 'blockstack'; 94 | import { configure } from 'radiks'; 95 | 96 | const userSession = new UserSession({ 97 | appConfig: new AppConfig(['store_write', 'publish_data']) 98 | }) 99 | 100 | configure({ 101 | apiServer: 'http://my-radiks-server.com', 102 | userSession 103 | }); 104 | ``` 105 | 106 | For more information on configuring and writing a Radiks a client application, see [the Radiks client](https://github.com/blockstack-radiks/radiks) repository. 107 | 108 | 8. Build and run your application. 109 | 110 | 111 | ### Running a custom Radiks-server 112 | 113 | If you're using an [express.js](https://expressjs.com/) server to run your application, it's probably easiest to use the Radiks-server middleware. This way, you won't have to run a separate application server and Radiks server. 114 | 115 | Radiks-server includes an easy-to-use middleware that you can include in your application: 116 | 117 | ```javascript 118 | const express = require('express'); 119 | 120 | const { setup } = require('radiks-server'); 121 | 122 | const app = express(); 123 | 124 | setup().then(RadiksController => { 125 | app.use('/radiks', RadiksController); 126 | }); 127 | ``` 128 | 129 | The `setup` method returns a promise, and that promise resolves to the actual middleware that your server can use. This is because it first connects to MongoDB, and then sets up the middleware with that database connection. 130 | 131 | The `setup` function accepts an `options` object as the first argument. Right now, the only option supported is `mongoDBUrl`. If you aren't using environment variables, you can explicitly pass in a MongoDB URL here: 132 | 133 | ```javascript 134 | setup({ 135 | mongoDBUrl: 'mongodb://localhost:27017/my-custom-database', 136 | }).then(RadiksController => { 137 | app.use('/radiks', RadiksController); 138 | }); 139 | ``` 140 | 141 | ### Accessing the MongoDB Collection 142 | 143 | #### Using `getDB` to manually connecting to the MongoDB collection 144 | 145 | Radiks-server keeps all models inside of a collection. You can use the `getDB` function to access this collection. [See the MongoDB Collection reference](https://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html) for documentation about how you can interact with this collection. 146 | 147 | ```js 148 | const { getDB } = require('radiks-server'); 149 | 150 | const mongo = await getDB(MONGODB_URL); 151 | ``` 152 | 153 | #### Migration from Firebase (or anywhere else) 154 | 155 | Migrating data from Firebase to Radiks-server is simple and painless. You can create a script file to fetch all the firebase data using their API. Then, you can use your MONGOD_URI config to use the `mongodb` npm package. 156 | 157 | ```js 158 | // Script for transfering users from Firebase to Radiks-server 159 | 160 | const { getDB } = require('radiks-server'); 161 | const { mongoURI } = require('......'); // How you import/require your mongoURI is up to you 162 | 163 | const migrate = async () => { 164 | // `mongo` is a reference to the MongoDB collection that radiks-server uses. 165 | // You can add or edit or update data as necessary. 166 | const mongo = await getDB(mongoURI); 167 | 168 | /** 169 | * Call code to get your users from firebase 170 | * const users = await getUsersFromFirebase(); 171 | * OR grab the Firebase JSON file and set users to that value 172 | * How you saved your user data will proably be different than the example below 173 | */ 174 | 175 | const users = { 176 | '-LV1HAQToANRvhysSClr': { 177 | blockstackId: '1N1DzKgizU4rCEaxAU21EgMaHGB5hprcBM', 178 | username: 'kkomaz.id', 179 | }, 180 | }; 181 | 182 | const usersToInsert = Object.values(users).map(user => { 183 | const { username } = user; 184 | const doc = { 185 | username, 186 | _id: username, 187 | radiksType: 'BlockstackUser', 188 | }; 189 | const op = { 190 | updateOne: { 191 | filter: { 192 | _id: username, 193 | }, 194 | update: { 195 | $setOnInsert: doc, 196 | }, 197 | upsert: true, 198 | }, 199 | }; 200 | return op; 201 | }); 202 | 203 | await mongo.bulkWrite(usersToInsert); 204 | }; 205 | 206 | migrate() 207 | .then(() => { 208 | console.log('Done!'); 209 | process.exit(); 210 | }) 211 | .catch(error => { 212 | console.error(error); 213 | process.exit(); 214 | }); 215 | ``` 216 | 217 | ### Options 218 | 219 | You can specify some options while initiating the Radiks server. 220 | 221 | ```javascript 222 | const { setup } = require('radiks-server'); 223 | 224 | setup({ 225 | ...myOptions, 226 | }); 227 | ``` 228 | 229 | Available options: 230 | 231 | - `mongoDBUrl` - The MongoDB URL for the Radiks server 232 | - `maxLimit` - The maximum `limit` field used inside the mongo queries - default to 1000 233 | -------------------------------------------------------------------------------- /bin/radiks-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const express = require('express'); 4 | 5 | const { setup } = require('../app/index'); 6 | 7 | const run = () => { 8 | setup() 9 | .then(RadiksController => { 10 | const port = parseInt(process.env.PORT, 10) || 1260; 11 | 12 | const server = express(); 13 | 14 | server.use('/radiks', RadiksController); 15 | 16 | server.listen(port, err => { 17 | if (err) throw err; 18 | console.log(`radiks-server is ready on http://localhost:${port}`); 19 | }); 20 | }) 21 | .catch(e => { 22 | console.error('Caught an error while setting up MongoDB:', e); 23 | }); 24 | }; 25 | 26 | run(); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radiks-server", 3 | "version": "0.2.3", 4 | "description": "An express plugin for building a Radiks server", 5 | "main": "app/index.js", 6 | "types": "typed", 7 | "author": "Hank Stoever", 8 | "license": "MIT", 9 | "bin": { 10 | "radiks-server": "./bin/radiks-server.js" 11 | }, 12 | "scripts": { 13 | "build": "rm -rf app typed && yarn compile && yarn typescript", 14 | "compile": "babel src -d app --extensions '.ts,.js'", 15 | "compile-watch": "babel src -d app --watch --extensions '.ts,.js'", 16 | "typescript": "tsc", 17 | "prettier": "prettier --write '**/*.{ts,tsx,json}'", 18 | "eslint": "eslint 'src/**/*'", 19 | "test": "jest --verbose=true --ci --runInBand test/", 20 | "test-watch": "jest --verbose=true --ci --runInBand --watch test/", 21 | "prepublishOnly": "yarn build" 22 | }, 23 | "prettier": { 24 | "singleQuote": true, 25 | "trailingComma": "es5" 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "lint-staged" 30 | } 31 | }, 32 | "lint-staged": { 33 | "*.{js,ts,json,md}": [ 34 | "prettier --write", 35 | "git add" 36 | ] 37 | }, 38 | "dependencies": { 39 | "@awaitjs/express": "^0.3.0", 40 | "blockstack": "^19.2.2", 41 | "body-parser": "^1.19.0", 42 | "elliptic": "^6.5.0", 43 | "express": "^4.17.1", 44 | "express-ws": "^4.0.0", 45 | "lodash": "^4.17.11", 46 | "mongodb": "^3.2.7", 47 | "query-to-mongo": "^0.9.0", 48 | "request": "^2.88.0", 49 | "request-promise": "^4.2.4", 50 | "wolfy87-eventemitter": "^5.2.6" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.4.4", 54 | "@babel/core": "^7.4.5", 55 | "@babel/plugin-proposal-class-properties": "^7.5.5", 56 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 57 | "@babel/plugin-transform-classes": "^7.5.5", 58 | "@babel/preset-env": "^7.4.5", 59 | "@babel/preset-typescript": "^7.3.3", 60 | "@types/body-parser": "^1.17.0", 61 | "@types/dotenv": "^6.1.1", 62 | "@types/elliptic": "^6.4.9", 63 | "@types/express": "^4.17.0", 64 | "@types/express-ws": "^3.0.0", 65 | "@types/faker": "^4.1.5", 66 | "@types/jest": "^24.0.15", 67 | "@types/mongodb": "^3.1.28", 68 | "@types/node": "^12.0.10", 69 | "@types/request-promise": "^4.1.44", 70 | "@types/supertest": "^2.0.7", 71 | "@types/uuid": "^3.4.5", 72 | "@typescript-eslint/eslint-plugin": "^1.11.0", 73 | "@typescript-eslint/parser": "^1.11.0", 74 | "babel-plugin-add-module-exports": "^1.0.2", 75 | "dotenv": "^6.2.0", 76 | "eslint": "^6.0.1", 77 | "eslint-config-airbnb-base": "^13.1.0", 78 | "eslint-config-prettier": "^6.0.0", 79 | "eslint-plugin-import": "^2.18.0", 80 | "eslint-plugin-jest": "^22.7.1", 81 | "eslint-plugin-prettier": "^3.1.0", 82 | "faker": "^4.1.0", 83 | "husky": "^2.7.0", 84 | "istanbul-reports": "2.2.6", 85 | "jest": "^24.8.0", 86 | "lint-staged": "^8.2.1", 87 | "prettier": "^1.18.2", 88 | "supertest": "^3.3.0", 89 | "typescript": "^3.5.2", 90 | "uuid": "^3.3.2" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /radiks-typeface.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | radiks.js 5 | Created with Sketch. 6 | 7 | 8 | 9 | radiks.js 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /radiks.js@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacks-archive/radiks-server/99f2da87285427d8cec34569b1ac69e3644ce52c/radiks.js@2x.png -------------------------------------------------------------------------------- /src/controllers/CentralController.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { verifyECDSA } from 'blockstack/lib/encryption'; 4 | import { addAsync } from '@awaitjs/express'; 5 | import { Collection } from 'mongodb'; 6 | 7 | export default ( 8 | radiksCollection: Collection, 9 | centralCollection: Collection 10 | ) => { 11 | const CentralController = addAsync(express.Router()); 12 | CentralController.use(bodyParser.json()); 13 | 14 | CentralController.postAsync('/', async (req, res) => { 15 | const { key, value, signature, username } = req.body; 16 | 17 | const { publicKey } = await radiksCollection.findOne({ 18 | username, 19 | radiksType: 'BlockstackUser', 20 | }); 21 | const _id = `${username}-${key}`; 22 | if (verifyECDSA(_id, publicKey, signature)) { 23 | await centralCollection.updateOne( 24 | { _id }, 25 | { $set: value }, 26 | { upsert: true } 27 | ); 28 | return res.json({ 29 | success: true, 30 | }); 31 | } 32 | return res.status(400).json({ 33 | success: false, 34 | }); 35 | }); 36 | 37 | CentralController.getAsync('/:key', async (req, res) => { 38 | const { username, signature } = req.query; 39 | const { key } = req.params; 40 | const _id = `${username}-${key}`; 41 | const { publicKey } = await radiksCollection.findOne({ 42 | username, 43 | radiksType: 'BlockstackUser', 44 | }); 45 | if (verifyECDSA(_id, publicKey, signature)) { 46 | const value = await centralCollection.findOne({ _id }); 47 | return res.json(value); 48 | } 49 | return res.status(400).json({ success: false }); 50 | }); 51 | 52 | return CentralController; 53 | }; 54 | -------------------------------------------------------------------------------- /src/controllers/ModelsController.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import request from 'request-promise'; 4 | import queryToMongo from 'query-to-mongo'; 5 | import { addAsync } from '@awaitjs/express'; 6 | import { verifyECDSA } from 'blockstack/lib/encryption/ec'; 7 | import { Collection } from 'mongodb'; 8 | import EventEmitter from 'wolfy87-eventemitter'; 9 | 10 | import { Config } from '../types'; 11 | import Validator from '../lib/validator'; 12 | import constants from '../lib/constants'; 13 | 14 | const makeModelsController = ( 15 | radiksCollection: Collection, 16 | config: Config, 17 | emitter: EventEmitter 18 | ) => { 19 | const ModelsController = addAsync(express.Router()); 20 | ModelsController.use(bodyParser.json()); 21 | 22 | ModelsController.postAsync('/crawl', async (req, res) => { 23 | const { gaiaURL } = req.body; 24 | const attrs = await request({ 25 | uri: gaiaURL, 26 | json: true, 27 | }); 28 | const validator = new Validator(radiksCollection, attrs); 29 | try { 30 | await validator.validate(); 31 | await radiksCollection.save(attrs); 32 | emitter.emit(constants.STREAM_CRAWL_EVENT, [attrs]); 33 | 34 | res.json({ 35 | success: true, 36 | }); 37 | } catch (error) { 38 | console.error(error); 39 | res.status(400).json({ 40 | success: false, 41 | message: error.message, 42 | }); 43 | } 44 | }); 45 | 46 | ModelsController.getAsync('/find', async (req, res) => { 47 | const mongo = queryToMongo(req.query, { 48 | maxLimit: config.maxLimit, 49 | }); 50 | 51 | const cursor = radiksCollection.find(mongo.criteria, mongo.options); 52 | const results = await cursor.toArray(); 53 | const total = await cursor.count(); 54 | 55 | const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; 56 | const pageLinks = mongo.links(fullUrl.split('?')[0], total); 57 | 58 | res.json({ 59 | ...pageLinks, 60 | total, 61 | results, 62 | }); 63 | }); 64 | 65 | ModelsController.getAsync('/count', async (req, res) => { 66 | const mongo = queryToMongo(req.query, { 67 | maxLimit: config.maxLimit, 68 | }); 69 | 70 | const total = await radiksCollection.countDocuments( 71 | mongo.criteria, 72 | mongo.options 73 | ); 74 | 75 | res.json({ 76 | total, 77 | }); 78 | }); 79 | 80 | ModelsController.getAsync('/:id', async (req, res) => { 81 | const { id } = req.params; 82 | const doc = await radiksCollection.findOne({ _id: id }); 83 | res.json(doc); 84 | }); 85 | 86 | ModelsController.deleteAsync('/:id', async (req, res) => { 87 | try { 88 | const attrs = await radiksCollection.findOne({ _id: req.params.id }); 89 | const { publicKey } = await radiksCollection.findOne({ 90 | _id: attrs.signingKeyId, 91 | radiksType: 'SigningKey', 92 | }); 93 | const message = `${attrs._id}-${attrs.updatedAt}`; 94 | if (verifyECDSA(message, publicKey, req.query.signature)) { 95 | await radiksCollection.deleteOne({ _id: req.params.id }); 96 | return res.json({ 97 | success: true, 98 | }); 99 | } 100 | } catch (error) { 101 | console.error(error); 102 | } 103 | 104 | return res.json({ 105 | success: false, 106 | error: 'Invalid signature', 107 | }); 108 | }); 109 | 110 | return ModelsController; 111 | }; 112 | 113 | export default makeModelsController; 114 | -------------------------------------------------------------------------------- /src/controllers/RadiksController.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import EventEmitter from 'wolfy87-eventemitter'; 3 | import { Db } from 'mongodb'; 4 | 5 | import { Config } from '../types'; 6 | import makeModelsController from './ModelsController'; 7 | import makeStreamingController from './StreamingController'; 8 | import makeCentralController from './CentralController'; 9 | import constants from '../lib/constants'; 10 | 11 | const makeController = (db: Db, config: Config) => { 12 | const router = express.Router(); 13 | 14 | const radiksCollection = db.collection(constants.COLLECTION); 15 | const centralCollection = db.collection(constants.CENTRAL_COLLECTION); 16 | 17 | router.options('*', (req, res, next) => { 18 | res.header('Access-Control-Allow-Origin', '*'); 19 | res.header('Access-Control-Allow-Headers', 'origin, content-type'); 20 | res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE'); 21 | next(); 22 | }); 23 | 24 | router.use((req, res, next) => { 25 | res.header('Access-Control-Allow-Origin', '*'); 26 | res.header('Access-Control-Allow-Headers', 'origin, content-type'); 27 | res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE'); 28 | next(); 29 | }); 30 | 31 | const emitter = new EventEmitter(); 32 | 33 | router.use( 34 | '/models', 35 | makeModelsController(radiksCollection, config, emitter) 36 | ); 37 | 38 | router.use('/stream', makeStreamingController(radiksCollection, emitter)); 39 | 40 | router.use( 41 | '/central', 42 | makeCentralController(radiksCollection, centralCollection) 43 | ); 44 | 45 | (router as any).db = radiksCollection; // for backwards compatibility 46 | (router as any).DB = db; 47 | (router as any).radiksCollection = radiksCollection; 48 | (router as any).centralCollection = centralCollection; 49 | (router as any).emitter = emitter; 50 | 51 | return router; 52 | }; 53 | 54 | export default makeController; 55 | -------------------------------------------------------------------------------- /src/controllers/StreamingController.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import expressWS from 'express-ws'; 3 | import EventEmitter from 'wolfy87-eventemitter'; 4 | import { Collection } from 'mongodb'; 5 | import constants from '../lib/constants'; 6 | 7 | export default (db: Collection, emitter: EventEmitter) => { 8 | const StreamingController: any = express.Router(); 9 | expressWS(StreamingController); 10 | 11 | StreamingController.ws('/', (ws: any) => { 12 | const listener = ([attributes]: any) => { 13 | ws.send(JSON.stringify(attributes), (error: Error) => { 14 | console.error(error); 15 | }); 16 | }; 17 | const ping = setInterval(() => { 18 | ws.send('ping'); 19 | }, 15000); 20 | emitter.addListener(constants.STREAM_CRAWL_EVENT, listener); 21 | ws.on('close', () => { 22 | clearTimeout(ping); 23 | emitter.removeListener(constants.STREAM_CRAWL_EVENT, listener); 24 | }); 25 | }); 26 | 27 | return StreamingController; 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/mongodb.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient, Db } from 'mongodb'; 2 | 3 | export const getDB = async (url?: string): Promise => { 4 | const _url = 5 | url || process.env.MONGODB_URI || 'mongodb://localhost:27017/radiks-server'; 6 | const client = new MongoClient(_url, { 7 | useNewUrlParser: true, 8 | reconnectTries: Number.MAX_VALUE, 9 | reconnectInterval: 1000, // every 1 second 10 | }); 11 | await client.connect(); 12 | return client.db(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import makeModelsController from './controllers/ModelsController'; 2 | import setupController from './controllers/RadiksController'; 3 | import { getDB } from './database/mongodb'; 4 | 5 | interface Options { 6 | mongoDBUrl?: string; 7 | maxLimit?: number; 8 | } 9 | 10 | const setup = async (config: Options = {}) => { 11 | const db = await getDB(config.mongoDBUrl); 12 | const newConfig = { 13 | ...config, 14 | maxLimit: config.maxLimit ? config.maxLimit : 1000, 15 | }; 16 | const controller = setupController(db, newConfig); 17 | return controller; 18 | }; 19 | 20 | export { makeModelsController, setup, getDB, setupController }; 21 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | const constants = { 2 | STREAM_CRAWL_EVENT: 'STREAM_CRAWL_EVENT', 3 | COLLECTION: 'radiks-server-data', 4 | CENTRAL_COLLECTION: 'radiks-central-data', 5 | }; 6 | 7 | export default constants; 8 | -------------------------------------------------------------------------------- /src/lib/validator.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'mongodb'; 2 | import { verifyECDSA } from 'blockstack/lib/encryption'; 3 | 4 | const errorMessage = (message: string) => { 5 | throw new Error(`Error when validating: ${message}`); 6 | }; 7 | 8 | class Validator { 9 | private db: Collection; 10 | 11 | private attrs: any; 12 | 13 | private previous: any; 14 | 15 | constructor(db: Collection, attrs: any) { 16 | this.db = db; 17 | this.attrs = attrs; 18 | } 19 | 20 | async validate() { 21 | this.validatePresent('_id'); 22 | await this.fetchPrevious(); 23 | await this.validateSignature(); 24 | await this.validatePrevious(); 25 | return true; 26 | } 27 | 28 | async fetchPrevious() { 29 | const { _id } = this.attrs; 30 | this.previous = await this.db.findOne({ _id }); 31 | } 32 | 33 | async validateSignature() { 34 | const { signingKeyId } = this.attrs.userGroupId ? this.attrs : this.previous || this.attrs; 35 | const { 36 | radiksSignature, updatable, updatedAt, _id, 37 | } = this.attrs; 38 | if (updatable === false) { 39 | return true; 40 | } 41 | this.validatePresent('radiksSignature'); 42 | this.validatePresent('signingKeyId'); 43 | this.validatePresent('updatedAt'); 44 | await this.signingKeyMatchesGroup(); 45 | let signingKey; 46 | if (signingKeyId === 'personal') { 47 | const { publicKey } = this.previous || this.attrs; 48 | signingKey = { 49 | publicKey, 50 | }; 51 | } else { 52 | signingKey = await this.db.findOne({ _id: signingKeyId }); 53 | if (!signingKey) { 54 | errorMessage(`No signing key is present with id: '${signingKeyId}'`); 55 | } 56 | } 57 | const { publicKey } = signingKey; 58 | const message = `${_id}-${updatedAt}`; 59 | const isValidSignature = verifyECDSA(message, publicKey, radiksSignature); 60 | if (!isValidSignature) { 61 | errorMessage('Invalid radiksSignature provided'); 62 | } 63 | return true; 64 | } 65 | 66 | async signingKeyMatchesGroup() { 67 | if (this.attrs.userGroupId) { 68 | const userGroup = await this.db.findOne({ _id: this.attrs.userGroupId }); 69 | if (userGroup && userGroup.signingKeyId !== this.attrs.signingKeyId) { 70 | errorMessage('Signing key does not match UserGroup signing key'); 71 | } 72 | } 73 | return true; 74 | } 75 | 76 | async validatePrevious() { 77 | if (this.previous && (this.attrs.updatable === false)) { 78 | errorMessage('Tried to update a non-updatable model'); 79 | } 80 | } 81 | 82 | validatePresent(key: string) { 83 | if (!this.attrs[key]) { 84 | errorMessage(`No '${key}' attribute, which is required.`); 85 | } 86 | } 87 | } 88 | 89 | export default Validator; -------------------------------------------------------------------------------- /src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@awaitjs/express' { 2 | import express from 'express'; 3 | 4 | type AsyncHandler = ( 5 | req: express.Request, 6 | res: express.Response, 7 | next: express.NextFunction 8 | ) => Promise; 9 | 10 | interface AsyncRouter extends express.Router { 11 | getAsync(path: string, handler: AsyncHandler): void; 12 | postAsync(path: string, handler: AsyncHandler): void; 13 | deleteAsync(path: string, handler: AsyncHandler): void; 14 | } 15 | 16 | export function addAsync(router: express.Router): AsyncRouter; 17 | } 18 | 19 | declare module 'query-to-mongo'; 20 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | maxLimit: number; 3 | } 4 | -------------------------------------------------------------------------------- /test/controllers/models-controller.test.ts: -------------------------------------------------------------------------------- 1 | import '../setup'; 2 | import request from 'supertest'; 3 | import { signECDSA } from 'blockstack/lib/encryption'; 4 | import { makeECPrivateKey } from 'blockstack/lib/keys'; 5 | import getApp from '../test-server'; 6 | import { models, saveAll } from '../mocks'; 7 | import Signer from '../signer'; 8 | import getDB from '../db'; 9 | import constants from '../../src/lib/constants'; 10 | 11 | jest.mock( 12 | '../../src/lib/validator', 13 | () => 14 | class FakeValidator { 15 | validate() { 16 | return true; 17 | } 18 | } 19 | ); 20 | 21 | test('it can crawl a gaia url', async () => { 22 | const app = await getApp(); 23 | 24 | const model = { ...models.test1 }; 25 | 26 | let response = await request(app) 27 | .post('/radiks/models/crawl') 28 | .send({ gaiaURL: 'test1' }); 29 | expect(response.body.success).toEqual(true); 30 | 31 | response = await request(app).get(`/radiks/models/${model._id}`); 32 | expect(response.body.name).toEqual(model.name); 33 | }); 34 | 35 | test('it can save the same model twice', async () => { 36 | const app = await getApp(); 37 | let response = await request(app) 38 | .post('/radiks/models/crawl') 39 | .send({ gaiaURL: 'test1' }); 40 | expect(response.body.success).toEqual(true); 41 | response = await request(app) 42 | .post('/radiks/models/crawl') 43 | .send({ gaiaURL: 'test1' }); 44 | expect(response.body.success).toEqual(true); 45 | }); 46 | 47 | const getDocs = async (app, query) => { 48 | const req = request(app) 49 | .get('/radiks/models/find') 50 | .query(query); 51 | const response = await req; 52 | const { results } = response.body; 53 | return results; 54 | }; 55 | 56 | test('it can query', async () => { 57 | const app = await getApp(); 58 | await saveAll(); 59 | 60 | const query = { 61 | name: { 62 | $regex: 'k sto', 63 | }, 64 | }; 65 | const [model] = await getDocs(app, query); 66 | expect(model.name).toEqual(models.hank.name); 67 | }); 68 | 69 | test('it can query with options', async () => { 70 | const app = await getApp(); 71 | await saveAll(); 72 | const query = { 73 | name: { 74 | $exists: true, 75 | }, 76 | limit: 1, 77 | }; 78 | const results = await getDocs(app, query); 79 | expect(results.length).toEqual(1); 80 | }); 81 | 82 | test('it includes pagination links', async () => { 83 | const app = await getApp(); 84 | await saveAll(); 85 | const response = await request(app) 86 | .get('/radiks/models/find') 87 | .query({ limit: 1 }); 88 | expect(response.body.next).not.toBeFalsy(); 89 | expect(response.body.last).not.toBeFalsy(); 90 | expect(response.body.total).not.toBeFalsy(); 91 | }); 92 | 93 | test('it can delete a model', async () => { 94 | const app = await getApp(); 95 | const model = { ...models.test1 }; 96 | const signer = new Signer(); 97 | signer.sign(model); 98 | const db = await getDB(); 99 | const radiksData = db.collection(constants.COLLECTION); 100 | await signer.save(db); 101 | await radiksData.insertOne(model); 102 | const { signature } = signECDSA( 103 | signer.privateKey, 104 | `${model._id}-${model.updatedAt}` 105 | ); 106 | const response = await request(app) 107 | .del(`/radiks/models/${model._id}`) 108 | .query({ signature }); 109 | expect(response.body.success).toEqual(true); 110 | const dbModel = await radiksData.findOne({ _id: model._id }); 111 | expect(dbModel).toBeNull(); 112 | }); 113 | 114 | test('it cannot delete with an invalid signature', async () => { 115 | const app = await getApp(); 116 | const model = { ...models.test1 }; 117 | const signer = new Signer(); 118 | signer.sign(model); 119 | const db = await getDB(); 120 | const radiksData = db.collection(constants.COLLECTION); 121 | await signer.save(db); 122 | await radiksData.insertOne(model); 123 | const { signature } = signECDSA( 124 | makeECPrivateKey(), 125 | `${model._id}-${model.updatedAt}` 126 | ); 127 | const response = await request(app) 128 | .del(`/radiks/models/${model._id}`) 129 | .query({ signature }); 130 | expect(response.body.success).toEqual(false); 131 | const dbModel = await radiksData.findOne({ _id: model._id }); 132 | expect(dbModel).not.toBeNull(); 133 | }); 134 | 135 | test('it can count', async () => { 136 | const app = await getApp(); 137 | await saveAll(); 138 | const response = await request(app) 139 | .get('/radiks/models/count') 140 | .query({}); 141 | expect(response.body.total).toBe(Object.keys(models).length); 142 | }); 143 | -------------------------------------------------------------------------------- /test/db.ts: -------------------------------------------------------------------------------- 1 | import { getDB } from '../src/database/mongodb'; 2 | 3 | const db = async () => { 4 | const url = process.env.MONGODB_URI; 5 | return getDB(url); 6 | }; 7 | 8 | export default db; 9 | -------------------------------------------------------------------------------- /test/mocks.ts: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import getDB from './db'; 3 | import constants from '../src/lib/constants'; 4 | 5 | const userGroupId = faker.random.uuid(); 6 | 7 | const models = { 8 | test1: { 9 | name: faker.name.findName(), 10 | email: faker.internet.email(), 11 | info: faker.helpers.createCard(), 12 | _id: faker.random.uuid(), 13 | }, 14 | hank: { 15 | name: 'hank stoever', 16 | age: 120, 17 | _id: faker.random.uuid(), 18 | }, 19 | myla: { 20 | name: 'Myla', 21 | age: 4.5, 22 | _id: faker.random.uuid(), 23 | }, 24 | notUpdatable: { 25 | updatable: false, 26 | _id: faker.random.uuid(), 27 | }, 28 | userGroup: { 29 | radiksType: 'UserGroup', 30 | _id: userGroupId, 31 | }, 32 | withGroup: { 33 | _id: faker.random.uuid(), 34 | userGroupId, 35 | }, 36 | user: { 37 | username: 'hankstoever.id', 38 | signingKeyId: 'personal', 39 | _id: faker.random.uuid(), 40 | }, 41 | }; 42 | 43 | const saveAll = async () => { 44 | const db = await getDB(); 45 | const data = Object.values(models); 46 | await db.collection(constants.COLLECTION).insertMany(data); 47 | }; 48 | 49 | export { models, saveAll }; 50 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import path from 'path'; 3 | import getDB from './db'; 4 | import constants from '../src/lib/constants'; 5 | 6 | dotenv.config({ 7 | path: path.resolve(process.cwd(), '.env.test'), 8 | }); 9 | 10 | jest.mock('request-promise', () => options => { 11 | const { models } = require('./mocks'); // eslint-disable-line 12 | const { uri } = options; 13 | return Promise.resolve(models[uri]); 14 | }); 15 | 16 | beforeEach(async done => { 17 | const db = await getDB(); 18 | try { 19 | await db.collection(constants.COLLECTION).drop(); 20 | } catch (error) { 21 | // collection doesn't exist 22 | // console.error(error); 23 | } 24 | done(); 25 | }); 26 | -------------------------------------------------------------------------------- /test/signer.ts: -------------------------------------------------------------------------------- 1 | import { makeECPrivateKey, getPublicKeyFromPrivate } from 'blockstack/lib/keys'; 2 | import { signECDSA } from 'blockstack/lib/encryption'; 3 | import uuid from 'uuid/v4'; 4 | import constants from '../src/lib/constants'; 5 | 6 | export default class Signer { 7 | private _id: string; 8 | private privateKey: string; 9 | private publicKey: string; 10 | 11 | constructor(privateKey?: string) { 12 | this.privateKey = privateKey || makeECPrivateKey(); 13 | this.publicKey = getPublicKeyFromPrivate(this.privateKey); 14 | this._id = uuid(); 15 | } 16 | 17 | save(db) { 18 | const { _id, privateKey, publicKey } = this; 19 | return db.collection(constants.COLLECTION).insertOne({ 20 | _id, 21 | privateKey, 22 | publicKey, 23 | radiksType: 'SigningKey', 24 | }); 25 | } 26 | 27 | sign(doc) { 28 | const now = new Date().getTime(); 29 | doc.updatedAt = now; 30 | doc.signingKeyId = doc.signingKeyId || this._id; 31 | const message = `${doc._id}-${doc.updatedAt}`; 32 | const { signature } = signECDSA(this.privateKey, message); 33 | doc.radiksSignature = signature; 34 | return doc; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test-server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { setup } from '../src'; 3 | 4 | const getApp = async () => { 5 | const app = express(); 6 | 7 | const radiksMiddleware = await setup({ 8 | mongoDBUrl: 'mongodb://localhost:27017/radiks-local-testing', 9 | }); 10 | app.use('/radiks', radiksMiddleware); 11 | 12 | return app; 13 | }; 14 | 15 | export default getApp; 16 | -------------------------------------------------------------------------------- /test/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { getPublicKeyFromPrivate } from 'blockstack/lib/keys'; 2 | import './setup'; 3 | import { models } from './mocks'; 4 | import getDB from './db'; 5 | import Signer from './signer'; 6 | import constants from '../src/lib/constants'; 7 | import Validator from '../src/lib/validator'; 8 | 9 | test('it validates new models', async () => { 10 | const signer = new Signer(); 11 | const db = await getDB(); 12 | await signer.save(db); 13 | const model: any = { 14 | ...models.hank, 15 | }; 16 | signer.sign(model); 17 | expect(model.radiksSignature).not.toBeFalsy(); 18 | const validator = new Validator(db.collection(constants.COLLECTION), model); 19 | expect(await validator.validate()).toEqual(true); 20 | expect(validator.attrs).toEqual(model); 21 | expect(validator.previous).toBeNull(); 22 | }); 23 | 24 | test('it doesnt allow mismatched signingKeyId', async () => { 25 | const signer = new Signer(); 26 | const db = await getDB(); 27 | await signer.save(db); 28 | const model = { 29 | ...models.hank, 30 | }; 31 | signer.sign(model); 32 | await db.collection(constants.COLLECTION).insertOne(model); 33 | let validator = new Validator(db.collection(constants.COLLECTION), model); 34 | expect(await validator.validate()).toEqual(true); 35 | 36 | const secondSigner = new Signer(); 37 | await secondSigner.save(db); 38 | secondSigner.sign(model); 39 | validator = new Validator(db.collection(constants.COLLECTION), model); 40 | try { 41 | await validator.validate(); 42 | } catch (error) { 43 | expect(error.message.indexOf('Invalid radiksSignature')).not.toEqual(-1); 44 | } 45 | }); 46 | 47 | test('it allows changing the signing key if signed with previous signing key', async () => { 48 | const signer = new Signer(); 49 | const db = await getDB(); 50 | await signer.save(db); 51 | const model: any = { 52 | ...models.hank, 53 | }; 54 | signer.sign(model); 55 | await db.collection(constants.COLLECTION).insertOne(model); 56 | const secondSigner = new Signer(); 57 | await secondSigner.save(db); 58 | model.signingKeyId = secondSigner._id; 59 | signer.sign(model); 60 | const validator = new Validator(db.collection(constants.COLLECTION), model); 61 | expect(await validator.validate()).toEqual(true); 62 | }); 63 | 64 | test('it doesnt allow older updatedAt', async () => { 65 | const model = { 66 | ...models.notUpdatable, 67 | }; 68 | const signer = new Signer(); 69 | const db = await getDB(); 70 | await signer.save(db); 71 | signer.sign(model); 72 | await db.collection(constants.COLLECTION).insertOne(model); 73 | signer.sign(model); 74 | const validator = new Validator(db.collection(constants.COLLECTION), model); 75 | await expect(validator.validate()).rejects.toThrow( 76 | 'Tried to update a non-updatable model' 77 | ); 78 | }); 79 | 80 | test('a model signing key must match the user group signing key', async () => { 81 | const model: any = { 82 | ...models.withGroup, 83 | }; 84 | const group = { 85 | ...models.userGroup, 86 | }; 87 | const signer = new Signer(); 88 | const db = await getDB(); 89 | await signer.save(db); 90 | await signer.sign(group); 91 | const groupValidator = new Validator( 92 | db.collection(constants.COLLECTION), 93 | group 94 | ); 95 | const modelValidator = new Validator( 96 | db.collection(constants.COLLECTION), 97 | model 98 | ); 99 | expect(await groupValidator.validate()).toEqual(true); 100 | signer.sign(model); 101 | expect(await modelValidator.validate()).toEqual(true); 102 | await db.collection(constants.COLLECTION).insertMany([model, group]); 103 | const newSigner = new Signer(); 104 | model.signingKeyId = newSigner._id; 105 | await newSigner.save(db); 106 | signer.sign(model); 107 | const newModelValidator = new Validator( 108 | db.collection(constants.COLLECTION), 109 | model 110 | ); 111 | await expect(newModelValidator.validate()).rejects.toThrow(); 112 | }); 113 | 114 | test('allows signing with new key if it matches the user group key', async () => { 115 | const model: any = { 116 | ...models.withGroup, 117 | }; 118 | const group: any = { 119 | ...models.userGroup, 120 | }; 121 | const oldSigner = new Signer(); 122 | const db = await getDB(); 123 | oldSigner.sign(group); 124 | oldSigner.sign(model); 125 | await db.collection(constants.COLLECTION).insertMany([group, model]); 126 | const newSigner = new Signer(); 127 | group.signingKeyId = newSigner._id; 128 | newSigner.sign(group); 129 | await db.collection(constants.COLLECTION).save(group); 130 | model.signingKeyId = newSigner._id; 131 | newSigner.sign(model); 132 | await newSigner.save(db); 133 | const validator = new Validator(db.collection(constants.COLLECTION), model); 134 | expect(await validator.validate()).toEqual(true); 135 | }); 136 | 137 | test('allows users to use personal signing key', async () => { 138 | const privateKey = 139 | '476055baaef9224ad0f9d082696a35b03f0a75100948d8b76ae1e859946297dd'; 140 | const publicKey = getPublicKeyFromPrivate(privateKey); 141 | const user = { 142 | ...models.user, 143 | publicKey, 144 | }; 145 | const signer = new Signer(privateKey); 146 | const db = await getDB(); 147 | signer.sign(user); 148 | const validator = new Validator(db.collection(constants.COLLECTION), user); 149 | expect(await validator.validate()).toEqual(true); 150 | }); 151 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "declaration": true, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "declaration": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "emitDeclarationOnly": true, 10 | "outDir": "typed", 11 | "strict": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | --------------------------------------------------------------------------------