├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------