diff --git a/.babelrc b/.babelrc index 2d91b3635..f36dbeadb 100644 --- a/.babelrc +++ b/.babelrc @@ -8,5 +8,8 @@ } } ] + ], + "plugins": [ + "@babel/plugin-proposal-throw-expressions" ] } diff --git a/.env.template b/.env.template index 42211b184..abc62b2dc 100644 --- a/.env.template +++ b/.env.template @@ -8,3 +8,5 @@ MOCK=false JWT_SECRET="b/&&7b78BF&fv/Vd" MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" + +PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78" diff --git a/.travis.yml b/.travis.yml index e699197cf..3110f31af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,9 @@ install: script: - docker-compose exec backend yarn run lint - docker-compose exec backend yarn run test --ci + - docker-compose exec backend yarn run db:reset + - docker-compose exec backend yarn run db:seed + - docker-compose exec backend yarn run test:cucumber - docker-compose exec backend yarn run test:coverage - docker-compose exec backend yarn run db:reset - docker-compose exec backend yarn run db:seed diff --git a/README.md b/README.md index 2717e7ed5..7883828d6 100644 --- a/README.md +++ b/README.md @@ -148,13 +148,20 @@ npm run db:reset **Beware**: We have no multiple database setup at the moment. We clean the database after each test, running the tests will wipe out all your data! -Run the tests: +Run the **_jest_** tests: ```bash yarn run test # -or- npm run test ``` +Run the **_cucumber_** features: +```bash +yarn run test:cucumber +# -or- +npm run test:cucumber +``` +When some tests fail, try `yarn db:reset` and after that `yarn db:seed`. Then run the tests again ## Todo`s - [x] add jwt authentication diff --git a/docker-compose.yml b/docker-compose.yml index 1e8c9158c..30d102f96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: - JWT_SECRET=b/&&7b78BF&fv/Vd - MOCK=false - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ + - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 neo4j: image: humanconnection/neo4j:latest diff --git a/package.json b/package.json index bc5b1a1df..5b653ba9d 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,15 @@ "dev": "nodemon --exec babel-node src/ -e js,graphql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql", "lint": "eslint src --config .eslintrc.js", - "test": "nyc --reporter=text-lcov yarn run test:jest", + "test": "nyc --reporter=text-lcov yarn test:jest", "test:cypress": "run-p --race test:before:*", "test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 babel-node src/ 2> /dev/null", "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 PERMISSIONS=disabled babel-node src/ 2> /dev/null", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", + "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", "test:jest": "run-p --race test:before:* 'test:jest:cmd {@}' --", + "test:cucumber": "run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "test:coverage": "nyc report --reporter=text-lcov > coverage.lcov", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", @@ -34,15 +36,19 @@ ] }, "dependencies": { + "activitystrea.ms": "~2.1.3", "apollo-cache-inmemory": "~1.5.1", "apollo-client": "~2.5.1", + "apollo-link-context": "~1.0.14", "apollo-link-http": "~1.5.13", "apollo-server": "~2.4.8", "bcryptjs": "~2.4.3", "cheerio": "~1.0.0-rc.2", + "cors": "~2.8.5", "cross-env": "~5.2.0", "date-fns": "2.0.0-alpha.27", "dotenv": "~7.0.0", + "express": "~4.16.4", "faker": "~4.1.0", "graphql": "~14.1.1", "graphql-custom-directives": "~0.2.14", @@ -51,6 +57,7 @@ "graphql-shield": "~5.3.0", "graphql-tag": "~2.10.1", "graphql-yoga": "~1.17.4", + "helmet": "~3.15.1", "jsonwebtoken": "~8.5.1", "linkifyjs": "~2.1.8", "lodash": "~4.17.11", @@ -59,15 +66,18 @@ "neo4j-graphql-js": "~2.4.2", "node-fetch": "~2.3.0", "npm-run-all": "~4.1.5", + "request": "~2.88.0", "sanitize-html": "~1.20.0", "slug": "~1.0.0", "trunc-html": "~1.1.2", + "uuid": "~3.3.2", "wait-on": "~3.2.0" }, "devDependencies": { "@babel/cli": "~7.2.3", "@babel/core": "~7.3.4", "@babel/node": "~7.2.2", + "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/preset-env": "~7.3.4", "@babel/register": "~7.0.0", "apollo-server-testing": "~2.4.8", @@ -75,6 +85,8 @@ "babel-eslint": "~10.0.1", "babel-jest": "~24.5.0", "chai": "~4.2.0", + "cucumber": "~5.1.0", + "debug": "~4.1.1", "eslint": "~5.15.1", "eslint-config-standard": "~12.0.0", "eslint-plugin-import": "~2.16.0", diff --git a/src/activitypub/ActivityPub.js b/src/activitypub/ActivityPub.js new file mode 100644 index 000000000..bcebf4d49 --- /dev/null +++ b/src/activitypub/ActivityPub.js @@ -0,0 +1,231 @@ +import { + extractNameFromId, + extractDomainFromUrl, + signAndSend +} from './utils' +import { + isPublicAddressed, + sendAcceptActivity, + sendRejectActivity +} from './utils/activity' +import request from 'request' +import as from 'activitystrea.ms' +import NitroDataSource from './NitroDataSource' +import router from './routes' +import dotenv from 'dotenv' +import Collections from './Collections' +import uuid from 'uuid/v4' +const debug = require('debug')('ea') + +let activityPub = null + +export { activityPub } + +export default class ActivityPub { + constructor (domain, port, uri) { + if (domain === 'localhost') { this.domain = `${domain}:${port}` } else { this.domain = domain } + this.port = port + this.dataSource = new NitroDataSource(uri) + this.collections = new Collections(this.dataSource) + } + + static init (server) { + if (!activityPub) { + dotenv.config() + const url = new URL(process.env.GRAPHQL_URI) + activityPub = new ActivityPub(url.hostname || 'localhost', url.port || 4000, url.origin) + + // integrate into running graphql express server + server.express.set('ap', activityPub) + server.express.set('port', url.port) + server.express.use(router) + console.log('-> ActivityPub middleware added to the graphql express server') + } else { + console.log('-> ActivityPub middleware already added to the graphql express server') + } + } + + handleFollowActivity (activity) { + debug(`inside FOLLOW ${activity.actor}`) + let toActorName = extractNameFromId(activity.object) + let fromDomain = extractDomainFromUrl(activity.actor) + const dataSource = this.dataSource + + return new Promise((resolve, reject) => { + request({ + url: activity.actor, + headers: { + 'Accept': 'application/activity+json' + } + }, async (err, response, toActorObject) => { + if (err) return reject(err) + debug(`name = ${toActorName}@${this.domain}`) + // save shared inbox + toActorObject = JSON.parse(toActorObject) + await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox) + + let followersCollectionPage = await this.dataSource.getFollowersCollectionPage(activity.object) + + const followActivity = as.follow() + .id(activity.id) + .actor(activity.actor) + .object(activity.object) + + // add follower if not already in collection + if (followersCollectionPage.orderedItems.includes(activity.actor)) { + debug('follower already in collection!') + debug(`inbox = ${toActorObject.inbox}`) + resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox)) + } else { + followersCollectionPage.orderedItems.push(activity.actor) + } + debug(`toActorObject = ${toActorObject}`) + toActorObject = typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject + debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`) + debug(`inbox = ${toActorObject.inbox}`) + debug(`outbox = ${toActorObject.outbox}`) + debug(`followers = ${toActorObject.followers}`) + debug(`following = ${toActorObject.following}`) + + try { + await dataSource.saveFollowersCollectionPage(followersCollectionPage) + debug('follow activity saved') + resolve(sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox)) + } catch (e) { + debug('followers update error!', e) + resolve(sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox)) + } + }) + }) + } + + handleUndoActivity (activity) { + debug('inside UNDO') + switch (activity.object.type) { + case 'Follow': + const followActivity = activity.object + return this.dataSource.undoFollowActivity(followActivity.actor, followActivity.object) + case 'Like': + return this.dataSource.deleteShouted(activity) + default: + } + } + + handleCreateActivity (activity) { + debug('inside create') + switch (activity.object.type) { + case 'Article': + case 'Note': + const articleObject = activity.object + if (articleObject.inReplyTo) { + return this.dataSource.createComment(activity) + } else { + return this.dataSource.createPost(activity) + } + default: + } + } + + handleDeleteActivity (activity) { + debug('inside delete') + switch (activity.object.type) { + case 'Article': + case 'Note': + return this.dataSource.deletePost(activity) + default: + } + } + + handleUpdateActivity (activity) { + debug('inside update') + switch (activity.object.type) { + case 'Note': + case 'Article': + return this.dataSource.updatePost(activity) + default: + } + } + + handleLikeActivity (activity) { + // TODO differ if activity is an Article/Note/etc. + return this.dataSource.createShouted(activity) + } + + handleDislikeActivity (activity) { + // TODO differ if activity is an Article/Note/etc. + return this.dataSource.deleteShouted(activity) + } + + async handleAcceptActivity (activity) { + debug('inside accept') + switch (activity.object.type) { + case 'Follow': + const followObject = activity.object + const followingCollectionPage = await this.collections.getFollowingCollectionPage(followObject.actor) + followingCollectionPage.orderedItems.push(followObject.object) + await this.dataSource.saveFollowingCollectionPage(followingCollectionPage) + } + } + + getActorObject (url) { + return new Promise((resolve, reject) => { + request({ + url: url, + headers: { + 'Accept': 'application/json' + } + }, (err, response, body) => { + if (err) { + reject(err) + } + resolve(JSON.parse(body)) + }) + }) + } + + generateStatusId (slug) { + return `http://${this.domain}/activitypub/users/${slug}/status/${uuid()}` + } + + async sendActivity (activity) { + delete activity.send + const fromName = extractNameFromId(activity.actor) + if (Array.isArray(activity.to) && isPublicAddressed(activity)) { + debug('is public addressed') + const sharedInboxEndpoints = await this.dataSource.getSharedInboxEndpoints() + // serve shared inbox endpoints + sharedInboxEndpoints.map((sharedInbox) => { + return this.trySend(activity, fromName, new URL(sharedInbox).host, sharedInbox) + }) + activity.to = activity.to.filter((recipient) => { + return !(isPublicAddressed({ to: recipient })) + }) + // serve the rest + activity.to.map(async (recipient) => { + debug('serve rest') + const actorObject = await this.getActorObject(recipient) + return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) + }) + } else if (typeof activity.to === 'string') { + debug('is string') + const actorObject = await this.getActorObject(activity.to) + return this.trySend(activity, fromName, new URL(activity.to).host, actorObject.inbox) + } else if (Array.isArray(activity.to)) { + activity.to.map(async (recipient) => { + const actorObject = await this.getActorObject(recipient) + return this.trySend(activity, fromName, new URL(recipient).host, actorObject.inbox) + }) + } + } + async trySend (activity, fromName, host, url, tries = 5) { + try { + return await signAndSend(activity, fromName, host, url) + } catch (e) { + if (tries > 0) { + setTimeout(function () { + return this.trySend(activity, fromName, host, url, --tries) + }, 20000) + } + } + } +} diff --git a/src/activitypub/Collections.js b/src/activitypub/Collections.js new file mode 100644 index 000000000..227e1717b --- /dev/null +++ b/src/activitypub/Collections.js @@ -0,0 +1,28 @@ +export default class Collections { + constructor (dataSource) { + this.dataSource = dataSource + } + getFollowersCollection (actorId) { + return this.dataSource.getFollowersCollection(actorId) + } + + getFollowersCollectionPage (actorId) { + return this.dataSource.getFollowersCollectionPage(actorId) + } + + getFollowingCollection (actorId) { + return this.dataSource.getFollowingCollection(actorId) + } + + getFollowingCollectionPage (actorId) { + return this.dataSource.getFollowingCollectionPage(actorId) + } + + getOutboxCollection (actorId) { + return this.dataSource.getOutboxCollection(actorId) + } + + getOutboxCollectionPage (actorId) { + return this.dataSource.getOutboxCollectionPage(actorId) + } +} diff --git a/src/activitypub/NitroDataSource.js b/src/activitypub/NitroDataSource.js new file mode 100644 index 000000000..4225e02ea --- /dev/null +++ b/src/activitypub/NitroDataSource.js @@ -0,0 +1,552 @@ +import { + throwErrorIfApolloErrorOccurred, + extractIdFromActivityId, + extractNameFromId, + constructIdFromName +} from './utils' +import { + createOrderedCollection, + createOrderedCollectionPage +} from './utils/collection' +import { + createArticleObject, + isPublicAddressed +} from './utils/activity' +import crypto from 'crypto' +import gql from 'graphql-tag' +import { createHttpLink } from 'apollo-link-http' +import { setContext } from 'apollo-link-context' +import { InMemoryCache } from 'apollo-cache-inmemory' +import fetch from 'node-fetch' +import { ApolloClient } from 'apollo-client' +import trunc from 'trunc-html' +const debug = require('debug')('ea:nitro-datasource') + +export default class NitroDataSource { + constructor (uri) { + this.uri = uri + const defaultOptions = { + query: { + fetchPolicy: 'network-only', + errorPolicy: 'all' + } + } + const link = createHttpLink({ uri: this.uri, fetch: fetch }) // eslint-disable-line + const cache = new InMemoryCache() + const authLink = setContext((_, { headers }) => { + // generate the authentication token (maybe from env? Which user?) + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJuYW1lIjoiUGV0ZXIgTHVzdGlnIiwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9qb2huY2FmYXp6YS8xMjguanBnIiwiaWQiOiJ1MSIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5vcmciLCJzbHVnIjoicGV0ZXItbHVzdGlnIiwiaWF0IjoxNTUyNDIwMTExLCJleHAiOjE2Mzg4MjAxMTEsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUxIn0.G7An1yeQUViJs-0Qj-Tc-zm0WrLCMB3M02pfPnm6xzw' + // return the headers to the context so httpLink can read them + return { + headers: { + ...headers, + Authorization: token ? `Bearer ${token}` : '' + } + } + }) + this.client = new ApolloClient({ + link: authLink.concat(link), + cache: cache, + defaultOptions + }) + } + + async getFollowersCollection (actorId) { + const slug = extractNameFromId(actorId) + debug(`slug= ${slug}`) + const result = await this.client.query({ + query: gql` + query { + User(slug: "${slug}") { + followedByCount + } + } + ` + }) + debug('successfully fetched followers') + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const followersCount = actor.followedByCount + + const followersCollection = createOrderedCollection(slug, 'followers') + followersCollection.totalItems = followersCount + + return followersCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async getFollowersCollectionPage (actorId) { + const slug = extractNameFromId(actorId) + debug(`getFollowersPage slug = ${slug}`) + const result = await this.client.query({ + query: gql` + query { + User(slug:"${slug}") { + followedBy { + slug + } + followedByCount + } + } + ` + }) + + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const followers = actor.followedBy + const followersCount = actor.followedByCount + + const followersCollection = createOrderedCollectionPage(slug, 'followers') + followersCollection.totalItems = followersCount + debug(`followers = ${JSON.stringify(followers, null, 2)}`) + await Promise.all( + followers.map(async (follower) => { + followersCollection.orderedItems.push(constructIdFromName(follower.slug)) + }) + ) + + return followersCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async getFollowingCollection (actorId) { + const slug = extractNameFromId(actorId) + const result = await this.client.query({ + query: gql` + query { + User(slug:"${slug}") { + followingCount + } + } + ` + }) + + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const followingCount = actor.followingCount + + const followingCollection = createOrderedCollection(slug, 'following') + followingCollection.totalItems = followingCount + + return followingCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async getFollowingCollectionPage (actorId) { + const slug = extractNameFromId(actorId) + const result = await this.client.query({ + query: gql` + query { + User(slug:"${slug}") { + following { + slug + } + followingCount + } + } + ` + }) + + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const following = actor.following + const followingCount = actor.followingCount + + const followingCollection = createOrderedCollectionPage(slug, 'following') + followingCollection.totalItems = followingCount + + await Promise.all( + following.map(async (user) => { + followingCollection.orderedItems.push(await constructIdFromName(user.slug)) + }) + ) + + return followingCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async getOutboxCollection (actorId) { + const slug = extractNameFromId(actorId) + const result = await this.client.query({ + query: gql` + query { + User(slug:"${slug}") { + contributions { + title + slug + content + contentExcerpt + createdAt + } + } + } + ` + }) + + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const posts = actor.contributions + + const outboxCollection = createOrderedCollection(slug, 'outbox') + outboxCollection.totalItems = posts.length + + return outboxCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async getOutboxCollectionPage (actorId) { + const slug = extractNameFromId(actorId) + debug(`inside getting outbox collection page => ${slug}`) + const result = await this.client.query({ + query: gql` + query { + User(slug:"${slug}") { + actorId + contributions { + id + activityId + objectId + title + slug + content + contentExcerpt + createdAt + author { + name + } + } + } + } + ` + }) + + debug(result.data) + if (result.data) { + const actor = result.data.User[0] + const posts = actor.contributions + + const outboxCollection = createOrderedCollectionPage(slug, 'outbox') + outboxCollection.totalItems = posts.length + await Promise.all( + posts.map(async (post) => { + outboxCollection.orderedItems.push(await createArticleObject(post.activityId, post.objectId, post.content, post.author.name, post.id, post.createdAt)) + }) + ) + + debug('after createNote') + return outboxCollection + } else { + throwErrorIfApolloErrorOccurred(result) + } + } + + async undoFollowActivity (fromActorId, toActorId) { + const fromUserId = await this.ensureUser(fromActorId) + const toUserId = await this.ensureUser(toActorId) + const result = await this.client.mutate({ + mutation: gql` + mutation { + RemoveUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { + from { name } + } + } + ` + }) + debug(`undoFollowActivity result = ${JSON.stringify(result, null, 2)}`) + throwErrorIfApolloErrorOccurred(result) + } + + async saveFollowersCollectionPage (followersCollection, onlyNewestItem = true) { + debug('inside saveFollowers') + let orderedItems = followersCollection.orderedItems + const toUserName = extractNameFromId(followersCollection.id) + const toUserId = await this.ensureUser(constructIdFromName(toUserName)) + orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems + + return Promise.all( + orderedItems.map(async (follower) => { + debug(`follower = ${follower}`) + const fromUserId = await this.ensureUser(follower) + debug(`fromUserId = ${fromUserId}`) + debug(`toUserId = ${toUserId}`) + const result = await this.client.mutate({ + mutation: gql` + mutation { + AddUserFollowedBy(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { + from { name } + } + } + ` + }) + debug(`addUserFollowedBy edge = ${JSON.stringify(result, null, 2)}`) + throwErrorIfApolloErrorOccurred(result) + debug('saveFollowers: added follow edge successfully') + }) + ) + } + async saveFollowingCollectionPage (followingCollection, onlyNewestItem = true) { + debug('inside saveFollowers') + let orderedItems = followingCollection.orderedItems + const fromUserName = extractNameFromId(followingCollection.id) + const fromUserId = await this.ensureUser(constructIdFromName(fromUserName)) + orderedItems = onlyNewestItem ? [orderedItems.pop()] : orderedItems + return Promise.all( + orderedItems.map(async (following) => { + debug(`follower = ${following}`) + const toUserId = await this.ensureUser(following) + debug(`fromUserId = ${fromUserId}`) + debug(`toUserId = ${toUserId}`) + const result = await this.client.mutate({ + mutation: gql` + mutation { + AddUserFollowing(from: {id: "${fromUserId}"}, to: {id: "${toUserId}"}) { + from { name } + } + } + ` + }) + debug(`addUserFollowing edge = ${JSON.stringify(result, null, 2)}`) + throwErrorIfApolloErrorOccurred(result) + debug('saveFollowing: added follow edge successfully') + }) + ) + } + + async createPost (activity) { + // TODO how to handle the to field? Now the post is just created, doesn't matter who is the recipient + // createPost + const postObject = activity.object + if (!isPublicAddressed(postObject)) { + return debug('createPost: not send to public (sending to specific persons is not implemented yet)') + } + const title = postObject.summary ? postObject.summary : postObject.content.split(' ').slice(0, 5).join(' ') + const postId = extractIdFromActivityId(postObject.id) + debug('inside create post') + let result = await this.client.mutate({ + mutation: gql` + mutation { + CreatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120)}", title: "${title}", id: "${postId}", objectId: "${postObject.id}", activityId: "${activity.id}") { + id + } + } + ` + }) + + throwErrorIfApolloErrorOccurred(result) + + // ensure user and add author to post + const userId = await this.ensureUser(postObject.attributedTo) + debug(`userId = ${userId}`) + debug(`postId = ${postId}`) + result = await this.client.mutate({ + mutation: gql` + mutation { + AddPostAuthor(from: {id: "${userId}"}, to: {id: "${postId}"}) { + from { + name + } + } + } + ` + }) + + throwErrorIfApolloErrorOccurred(result) + } + + async deletePost (activity) { + const result = await this.client.mutate({ + mutation: gql` + mutation { + DeletePost(id: "${extractIdFromActivityId(activity.object.id)}") { + title + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + } + + async updatePost (activity) { + const postObject = activity.object + const postId = extractIdFromActivityId(postObject.id) + const date = postObject.updated ? postObject.updated : new Date().toISOString() + const result = await this.client.mutate({ + mutation: gql` + mutation { + UpdatePost(content: "${postObject.content}", contentExcerpt: "${trunc(postObject.content, 120).html}", id: "${postId}", updatedAt: "${date}") { + title + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + } + + async createShouted (activity) { + const userId = await this.ensureUser(activity.actor) + const postId = extractIdFromActivityId(activity.object) + const result = await this.client.mutate({ + mutation: gql` + mutation { + AddUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) { + from { + name + } + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + if (!result.data.AddUserShouted) { + debug('something went wrong shouting post') + throw Error('User or Post not exists') + } + } + + async deleteShouted (activity) { + const userId = await this.ensureUser(activity.actor) + const postId = extractIdFromActivityId(activity.object) + const result = await this.client.mutate({ + mutation: gql` + mutation { + RemoveUserShouted(from: {id: "${userId}"}, to: {id: "${postId}"}) { + from { + name + } + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + if (!result.data.AddUserShouted) { + debug('something went wrong disliking a post') + throw Error('User or Post not exists') + } + } + + async getSharedInboxEndpoints () { + const result = await this.client.query({ + query: gql` + query { + SharedInboxEndpoint { + uri + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + return result.data.SharedInboxEnpoint + } + async addSharedInboxEndpoint (uri) { + try { + const result = await this.client.mutate({ + mutation: gql` + mutation { + CreateSharedInboxEndpoint(uri: "${uri}") + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + return true + } catch (e) { + return false + } + } + + async createComment (activity) { + const postObject = activity.object + let result = await this.client.mutate({ + mutation: gql` + mutation { + CreateComment(content: "${postObject.content}", activityId: "${extractIdFromActivityId(activity.id)}") { + id + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + + const toUserId = await this.ensureUser(activity.actor) + const result2 = await this.client.mutate({ + mutation: gql` + mutation { + AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) { + id + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result2) + + const postId = extractIdFromActivityId(postObject.inReplyTo) + result = await this.client.mutate({ + mutation: gql` + mutation { + AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) { + id + } + } + ` + }) + + throwErrorIfApolloErrorOccurred(result) + } + + /** + * This function will search for user existence and will create a disabled user with a random 16 bytes password when no user is found. + * + * @param actorId + * @returns {Promise<*>} + */ + async ensureUser (actorId) { + debug(`inside ensureUser = ${actorId}`) + const name = extractNameFromId(actorId) + const queryResult = await this.client.query({ + query: gql` + query { + User(slug: "${name}") { + id + } + } + ` + }) + + if (queryResult.data && Array.isArray(queryResult.data.User) && queryResult.data.User.length > 0) { + debug('ensureUser: user exists.. return id') + // user already exists.. return the id + return queryResult.data.User[0].id + } else { + debug('ensureUser: user not exists.. createUser') + // user does not exist.. create it + const pw = crypto.randomBytes(16).toString('hex') + const slug = name.toLowerCase().split(' ').join('-') + const result = await this.client.mutate({ + mutation: gql` + mutation { + CreateUser(password: "${pw}", slug:"${slug}", actorId: "${actorId}", name: "${name}", email: "${slug}@test.org") { + id + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + + return result.data.CreateUser.id + } + } +} diff --git a/src/activitypub/routes/inbox.js b/src/activitypub/routes/inbox.js new file mode 100644 index 000000000..f9cfb3794 --- /dev/null +++ b/src/activitypub/routes/inbox.js @@ -0,0 +1,54 @@ +import express from 'express' +import { activityPub } from '../ActivityPub' + +const debug = require('debug')('ea:inbox') + +const router = express.Router() + +// Shared Inbox endpoint (federated Server) +// For now its only able to handle Note Activities!! +router.post('/', async function (req, res, next) { + debug(`Content-Type = ${req.get('Content-Type')}`) + debug(`body = ${JSON.stringify(req.body, null, 2)}`) + debug(`Request headers = ${JSON.stringify(req.headers, null, 2)}`) + switch (req.body.type) { + case 'Create': + await activityPub.handleCreateActivity(req.body).catch(next) + break + case 'Undo': + await activityPub.handleUndoActivity(req.body).catch(next) + break + case 'Follow': + await activityPub.handleFollowActivity(req.body).catch(next) + break + case 'Delete': + await activityPub.handleDeleteActivity(req.body).catch(next) + break + /* eslint-disable */ + case 'Update': + await activityPub.handleUpdateActivity(req.body).catch(next) + break + case 'Accept': + await activityPub.handleAcceptActivity(req.body).catch(next) + case 'Reject': + // Do nothing + break + case 'Add': + break + case 'Remove': + break + case 'Like': + await activityPub.handleLikeActivity(req.body).catch(next) + break + case 'Dislike': + await activityPub.handleDislikeActivity(req.body).catch(next) + break + case 'Announce': + debug('else!!') + debug(JSON.stringify(req.body, null, 2)) + } + /* eslint-enable */ + res.status(200).end() +}) + +export default router diff --git a/src/activitypub/routes/index.js b/src/activitypub/routes/index.js new file mode 100644 index 000000000..24898e766 --- /dev/null +++ b/src/activitypub/routes/index.js @@ -0,0 +1,29 @@ +import user from './user' +import inbox from './inbox' +import webFinger from './webFinger' +import express from 'express' +import cors from 'cors' +import verify from './verify' + +const router = express.Router() + +router.use('/.well-known/webFinger', + cors(), + express.urlencoded({ extended: true }), + webFinger +) +router.use('/activitypub/users', + cors(), + express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }), + express.urlencoded({ extended: true }), + user +) +router.use('/activitypub/inbox', + cors(), + express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }), + express.urlencoded({ extended: true }), + verify, + inbox +) + +export default router diff --git a/src/activitypub/routes/serveUser.js b/src/activitypub/routes/serveUser.js new file mode 100644 index 000000000..f65876741 --- /dev/null +++ b/src/activitypub/routes/serveUser.js @@ -0,0 +1,43 @@ +import { createActor } from '../utils/actor' +const gql = require('graphql-tag') +const debug = require('debug')('ea:serveUser') + +export async function serveUser (req, res, next) { + let name = req.params.name + + if (name.startsWith('@')) { + name = name.slice(1) + } + + debug(`name = ${name}`) + const result = await req.app.get('ap').dataSource.client.query({ + query: gql` + query { + User(slug: "${name}") { + publicKey + } + } + ` + }).catch(reason => { debug(`serveUser User fetch error: ${reason}`) }) + + if (result.data && Array.isArray(result.data.User) && result.data.User.length > 0) { + const publicKey = result.data.User[0].publicKey + const actor = createActor(name, publicKey) + debug(`actor = ${JSON.stringify(actor, null, 2)}`) + debug(`accepts json = ${req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])}`) + if (req.accepts(['application/activity+json', 'application/ld+json', 'application/json'])) { + return res.json(actor) + } else if (req.accepts('text/html')) { + // TODO show user's profile page instead of the actor object + /* const outbox = JSON.parse(result.outbox) + const posts = outbox.orderedItems.filter((el) => { return el.object.type === 'Note'}) + const actor = result.actor + debug(posts) */ + // res.render('user', { user: actor, posts: JSON.stringify(posts)}) + return res.json(actor) + } + } else { + debug(`error getting publicKey for actor ${name}`) + next() + } +} diff --git a/src/activitypub/routes/user.js b/src/activitypub/routes/user.js new file mode 100644 index 000000000..017891e61 --- /dev/null +++ b/src/activitypub/routes/user.js @@ -0,0 +1,92 @@ +import { sendCollection } from '../utils/collection' +import express from 'express' +import { serveUser } from './serveUser' +import { activityPub } from '../ActivityPub' +import verify from './verify' + +const router = express.Router() +const debug = require('debug')('ea:user') + +router.get('/:name', async function (req, res, next) { + debug('inside user.js -> serveUser') + await serveUser(req, res, next) +}) + +router.get('/:name/following', (req, res) => { + debug('inside user.js -> serveFollowingCollection') + const name = req.params.name + if (!name) { + res.status(400).send('Bad request! Please specify a name.') + } else { + const collectionName = req.query.page ? 'followingPage' : 'following' + sendCollection(collectionName, req, res) + } +}) + +router.get('/:name/followers', (req, res) => { + debug('inside user.js -> serveFollowersCollection') + const name = req.params.name + if (!name) { + return res.status(400).send('Bad request! Please specify a name.') + } else { + const collectionName = req.query.page ? 'followersPage' : 'followers' + sendCollection(collectionName, req, res) + } +}) + +router.get('/:name/outbox', (req, res) => { + debug('inside user.js -> serveOutboxCollection') + const name = req.params.name + if (!name) { + return res.status(400).send('Bad request! Please specify a name.') + } else { + const collectionName = req.query.page ? 'outboxPage' : 'outbox' + sendCollection(collectionName, req, res) + } +}) + +router.post('/:name/inbox', verify, async function (req, res, next) { + debug(`body = ${JSON.stringify(req.body, null, 2)}`) + debug(`actorId = ${req.body.actor}`) + // const result = await saveActorId(req.body.actor) + switch (req.body.type) { + case 'Create': + await activityPub.handleCreateActivity(req.body).catch(next) + break + case 'Undo': + await activityPub.handleUndoActivity(req.body).catch(next) + break + case 'Follow': + await activityPub.handleFollowActivity(req.body).catch(next) + break + case 'Delete': + await activityPub.handleDeleteActivity(req.body).catch(next) + break + /* eslint-disable */ + case 'Update': + await activityPub.handleUpdateActivity(req.body).catch(next) + break + case 'Accept': + await activityPub.handleAcceptActivity(req.body).catch(next) + case 'Reject': + // Do nothing + break + case 'Add': + break + case 'Remove': + break + case 'Like': + await activityPub.handleLikeActivity(req.body).catch(next) + break + case 'Dislike': + await activityPub.handleDislikeActivity(req.body).catch(next) + break + case 'Announce': + debug('else!!') + debug(JSON.stringify(req.body, null, 2)) + } + /* eslint-enable */ + res.status(200).end() +}) + +export default router diff --git a/src/activitypub/routes/verify.js b/src/activitypub/routes/verify.js new file mode 100644 index 000000000..bb5850b3e --- /dev/null +++ b/src/activitypub/routes/verify.js @@ -0,0 +1,15 @@ +import { verifySignature } from '../security' +const debug = require('debug')('ea:verify') + +export default async (req, res, next) => { + debug(`actorId = ${req.body.actor}`) + // TODO stop if signature validation fails + if (await verifySignature(`${req.protocol}://${req.hostname}:${req.app.get('port')}${req.originalUrl}`, req.headers)) { + debug('verify = true') + next() + } else { + // throw Error('Signature validation failed!') + debug('verify = false') + next() + } +} diff --git a/src/activitypub/routes/webFinger.js b/src/activitypub/routes/webFinger.js new file mode 100644 index 000000000..ad1c806ad --- /dev/null +++ b/src/activitypub/routes/webFinger.js @@ -0,0 +1,34 @@ +import express from 'express' +import { createWebFinger } from '../utils/actor' +import gql from 'graphql-tag' + +const router = express.Router() + +router.get('/', async function (req, res) { + const resource = req.query.resource + if (!resource || !resource.includes('acct:')) { + return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.') + } else { + const nameAndDomain = resource.replace('acct:', '') + const name = nameAndDomain.split('@')[0] + + const result = await req.app.get('ap').dataSource.client.query({ + query: gql` + query { + User(slug: "${name}") { + slug + } + } + ` + }) + + if (result.data && result.data.User.length > 0) { + const webFinger = createWebFinger(name) + return res.contentType('application/jrd+json').json(webFinger) + } else { + return res.status(404).json({ error: `No record found for ${nameAndDomain}.` }) + } + } +}) + +export default router diff --git a/src/activitypub/security/httpSignature.spec.js b/src/activitypub/security/httpSignature.spec.js new file mode 100644 index 000000000..fe09eda8a --- /dev/null +++ b/src/activitypub/security/httpSignature.spec.js @@ -0,0 +1,69 @@ +import { createSignature, verifySignature } from '.' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' +import { GraphQLClient } from 'graphql-request' +import crypto from 'crypto' +import { expect } from 'chai' +const factory = Factory() + +describe('Signature creation and verification', () => { + let user = null + let client = null + const headers = { + 'Date': '2019-03-08T14:35:45.759Z', + 'Host': 'democracy-app.de', + 'Content-Type': 'application/json' + } + + beforeEach(async () => { + await factory.create('User', { + 'slug': 'test-user', + 'name': 'Test User', + 'email': 'user@example.org', + 'password': 'swordfish' + }) + const headers = await login({ email: 'user@example.org', password: 'swordfish' }) + client = new GraphQLClient(host, { headers }) + const result = await client.request(`query { + User(slug: "test-user") { + privateKey + publicKey + } + }`) + user = result.User[0] + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('Signature creation', () => { + let signatureB64 = '' + beforeEach(() => { + const signer = crypto.createSign('rsa-sha256') + signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json') + signatureB64 = signer.sign({ key: user.privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64') + }) + it('creates a Signature with given privateKey, keyId, url and headers (default algorithm: "rsa-sha256")', () => { + const httpSignature = createSignature(user.privateKey, 'https://human-connection.org/activitypub/users/lea#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers) + + expect(httpSignature).to.contain('keyId="https://human-connection.org/activitypub/users/lea#main-key"') + expect(httpSignature).to.contain('algorithm="rsa-sha256"') + expect(httpSignature).to.contain('headers="(request-target) date host content-type"') + expect(httpSignature).to.contain('signature="' + signatureB64 + '"') + }) + }) + + describe('Signature verification', () => { + let httpSignature = '' + beforeEach(() => { + httpSignature = createSignature(user.privateKey, 'http://localhost:4001/activitypub/users/test-user#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers) + }) + + it('verifies a Signature correct', async () => { + headers['Signature'] = httpSignature + const isVerified = await verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers) + expect(isVerified).to.equal(true) + }) + }) +}) diff --git a/src/activitypub/security/index.js b/src/activitypub/security/index.js new file mode 100644 index 000000000..412084022 --- /dev/null +++ b/src/activitypub/security/index.js @@ -0,0 +1,147 @@ +import dotenv from 'dotenv' +import { resolve } from 'path' +import crypto from 'crypto' +import request from 'request' +const debug = require('debug')('ea:security') + +dotenv.config({ path: resolve('src', 'activitypub', '.env') }) + +export function generateRsaKeyPair () { + return crypto.generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: process.env.PRIVATE_KEY_PASSPHRASE + } + }) +} + +// signing +export function createSignature (privKey, keyId, url, headers = {}, algorithm = 'rsa-sha256') { + if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) } + const signer = crypto.createSign(algorithm) + const signingString = constructSigningString(url, headers) + signer.update(signingString) + const signatureB64 = signer.sign({ key: privKey, passphrase: process.env.PRIVATE_KEY_PASSPHRASE }, 'base64') + const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '') + return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"` +} + +// verifying +export function verifySignature (url, headers) { + return new Promise((resolve, reject) => { + const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature'] + if (!signatureHeader) { + debug('No Signature header present!') + resolve(false) + } + debug(`Signature Header = ${signatureHeader}`) + const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature') + const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm') + const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers') + const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId') + + if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { + debug('Unsupported hash algorithm specified!') + resolve(false) + } + + const usedHeaders = headersString.split(' ') + const verifyHeaders = {} + Object.keys(headers).forEach((key) => { + if (usedHeaders.includes(key.toLowerCase())) { + verifyHeaders[key.toLowerCase()] = headers[key] + } + }) + const signingString = constructSigningString(url, verifyHeaders) + debug(`keyId= ${keyId}`) + request({ + url: keyId, + headers: { + 'Accept': 'application/json' + } + }, (err, response, body) => { + if (err) reject(err) + debug(`body = ${body}`) + const actor = JSON.parse(body) + const publicKeyPem = actor.publicKey.publicKeyPem + resolve(httpVerify(publicKeyPem, signature, signingString, algorithm)) + }) + }) +} + +// private: signing +function constructSigningString (url, headers) { + const urlObj = new URL(url) + let signingString = `(request-target): post ${urlObj.pathname}${urlObj.search !== '' ? urlObj.search : ''}` + return Object.keys(headers).reduce((result, key) => { + return result + `\n${key.toLowerCase()}: ${headers[key]}` + }, signingString) +} + +// private: verifying +function httpVerify (pubKey, signature, signingString, algorithm) { + if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) } + const verifier = crypto.createVerify(algorithm) + verifier.update(signingString) + return verifier.verify(pubKey, signature, 'base64') +} + +// private: verifying +// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header. +// Just pass what you want as key +function extractKeyValueFromSignatureHeader (signatureHeader, key) { + const keyString = signatureHeader.split(',').filter((el) => { + return !!el.startsWith(key) + })[0] + + let firstEqualIndex = keyString.search('=') + // When headers are requested add 17 to the index to remove "(request-target) " from the string + if (key === 'headers') { firstEqualIndex += 17 } + return keyString.substring(firstEqualIndex + 2, keyString.length - 1) +} + +// Obtained from invoking crypto.getHashes() +export const SUPPORTED_HASH_ALGORITHMS = [ + 'rsa-md4', + 'rsa-md5', + 'rsa-mdC2', + 'rsa-ripemd160', + 'rsa-sha1', + 'rsa-sha1-2', + 'rsa-sha224', + 'rsa-sha256', + 'rsa-sha384', + 'rsa-sha512', + 'blake2b512', + 'blake2s256', + 'md4', + 'md4WithRSAEncryption', + 'md5', + 'md5-sha1', + 'md5WithRSAEncryption', + 'mdc2', + 'mdc2WithRSA', + 'ripemd', + 'ripemd160', + 'ripemd160WithRSA', + 'rmd160', + 'sha1', + 'sha1WithRSAEncryption', + 'sha224', + 'sha224WithRSAEncryption', + 'sha256', + 'sha256WithRSAEncryption', + 'sha384', + 'sha384WithRSAEncryption', + 'sha512', + 'sha512WithRSAEncryption', + 'ssl3-md5', + 'ssl3-sha1', + 'whirlpool'] diff --git a/src/activitypub/utils/activity.js b/src/activitypub/utils/activity.js new file mode 100644 index 000000000..53bcd37ea --- /dev/null +++ b/src/activitypub/utils/activity.js @@ -0,0 +1,108 @@ +import { activityPub } from '../ActivityPub' +import { signAndSend, throwErrorIfApolloErrorOccurred } from './index' + +import crypto from 'crypto' +import as from 'activitystrea.ms' +import gql from 'graphql-tag' +const debug = require('debug')('ea:utils:activity') + +export function createNoteObject (text, name, id, published) { + const createUuid = crypto.randomBytes(16).toString('hex') + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${createUuid}`, + 'type': 'Create', + 'actor': `https://${activityPub.domain}/activitypub/users/${name}`, + 'object': { + 'id': `https://${activityPub.domain}/activitypub/users/${name}/status/${id}`, + 'type': 'Note', + 'published': published, + 'attributedTo': `https://${activityPub.domain}/activitypub/users/${name}`, + 'content': text, + 'to': 'https://www.w3.org/ns/activitystreams#Public' + } + } +} + +export async function createArticleObject (activityId, objectId, text, name, id, published) { + const actorId = await getActorId(name) + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `${activityId}`, + 'type': 'Create', + 'actor': `${actorId}`, + 'object': { + 'id': `${objectId}`, + 'type': 'Article', + 'published': published, + 'attributedTo': `${actorId}`, + 'content': text, + 'to': 'https://www.w3.org/ns/activitystreams#Public' + } + } +} + +export async function getActorId (name) { + const result = await activityPub.dataSource.client.query({ + query: gql` + query { + User(slug: "${name}") { + actorId + } + } + ` + }) + throwErrorIfApolloErrorOccurred(result) + if (Array.isArray(result.data.User) && result.data.User[0]) { + return result.data.User[0].actorId + } else { + throw Error(`No user with name: ${name}`) + } +} + +export function sendAcceptActivity (theBody, name, targetDomain, url) { + as.accept() + .id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) + .actor(`https://${activityPub.domain}/activitypub/users/${name}`) + .object(theBody) + .prettyWrite((err, doc) => { + if (!err) { + return signAndSend(doc, name, targetDomain, url) + } else { + debug(`error serializing Accept object: ${err}`) + throw new Error('error serializing Accept object') + } + }) +} + +export function sendRejectActivity (theBody, name, targetDomain, url) { + as.reject() + .id(`https://${activityPub.domain}/activitypub/users/${name}/status/` + crypto.randomBytes(16).toString('hex')) + .actor(`https://${activityPub.domain}/activitypub/users/${name}`) + .object(theBody) + .prettyWrite((err, doc) => { + if (!err) { + return signAndSend(doc, name, targetDomain, url) + } else { + debug(`error serializing Accept object: ${err}`) + throw new Error('error serializing Accept object') + } + }) +} + +export function isPublicAddressed (postObject) { + if (typeof postObject.to === 'string') { + postObject.to = [postObject.to] + } + if (typeof postObject === 'string') { + postObject.to = [postObject] + } + if (Array.isArray(postObject)) { + postObject.to = postObject + } + return postObject.to.includes('Public') || + postObject.to.includes('as:Public') || + postObject.to.includes('https://www.w3.org/ns/activitystreams#Public') +} diff --git a/src/activitypub/utils/actor.js b/src/activitypub/utils/actor.js new file mode 100644 index 000000000..0b8cc7454 --- /dev/null +++ b/src/activitypub/utils/actor.js @@ -0,0 +1,40 @@ +import { activityPub } from '../ActivityPub' + +export function createActor (name, pubkey) { + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + 'id': `https://${activityPub.domain}/activitypub/users/${name}`, + 'type': 'Person', + 'preferredUsername': `${name}`, + 'name': `${name}`, + 'following': `https://${activityPub.domain}/activitypub/users/${name}/following`, + 'followers': `https://${activityPub.domain}/activitypub/users/${name}/followers`, + 'inbox': `https://${activityPub.domain}/activitypub/users/${name}/inbox`, + 'outbox': `https://${activityPub.domain}/activitypub/users/${name}/outbox`, + 'url': `https://${activityPub.domain}/activitypub/@${name}`, + 'endpoints': { + 'sharedInbox': `https://${activityPub.domain}/activitypub/inbox` + }, + 'publicKey': { + 'id': `https://${activityPub.domain}/activitypub/users/${name}#main-key`, + 'owner': `https://${activityPub.domain}/activitypub/users/${name}`, + 'publicKeyPem': pubkey + } + } +} + +export function createWebFinger (name) { + return { + 'subject': `acct:${name}@${activityPub.domain}`, + 'links': [ + { + 'rel': 'self', + 'type': 'application/activity+json', + 'href': `https://${activityPub.domain}/users/${name}` + } + ] + } +} diff --git a/src/activitypub/utils/collection.js b/src/activitypub/utils/collection.js new file mode 100644 index 000000000..4c46adbde --- /dev/null +++ b/src/activitypub/utils/collection.js @@ -0,0 +1,70 @@ +import { activityPub } from '../ActivityPub' +import { constructIdFromName } from './index' +const debug = require('debug')('ea:utils:collections') + +export function createOrderedCollection (name, collectionName) { + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`, + 'summary': `${name}s ${collectionName} collection`, + 'type': 'OrderedCollection', + 'first': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`, + 'totalItems': 0 + } +} + +export function createOrderedCollectionPage (name, collectionName) { + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}?page=true`, + 'summary': `${name}s ${collectionName} collection`, + 'type': 'OrderedCollectionPage', + 'totalItems': 0, + 'partOf': `https://${activityPub.domain}/activitypub/users/${name}/${collectionName}`, + 'orderedItems': [] + } +} +export function sendCollection (collectionName, req, res) { + const name = req.params.name + const id = constructIdFromName(name) + + switch (collectionName) { + case 'followers': + attachThenCatch(activityPub.collections.getFollowersCollection(id), res) + break + + case 'followersPage': + attachThenCatch(activityPub.collections.getFollowersCollectionPage(id), res) + break + + case 'following': + attachThenCatch(activityPub.collections.getFollowingCollection(id), res) + break + + case 'followingPage': + attachThenCatch(activityPub.collections.getFollowingCollectionPage(id), res) + break + + case 'outbox': + attachThenCatch(activityPub.collections.getOutboxCollection(id), res) + break + + case 'outboxPage': + attachThenCatch(activityPub.collections.getOutboxCollectionPage(id), res) + break + + default: + res.status(500).end() + } +} + +function attachThenCatch (promise, res) { + return promise + .then((collection) => { + res.status(200).contentType('application/activity+json').send(collection) + }) + .catch((err) => { + debug(`error getting a Collection: = ${err}`) + res.status(500).end() + }) +} diff --git a/src/activitypub/utils/index.js b/src/activitypub/utils/index.js new file mode 100644 index 000000000..0cb4b7d0d --- /dev/null +++ b/src/activitypub/utils/index.js @@ -0,0 +1,100 @@ +import { activityPub } from '../ActivityPub' +import gql from 'graphql-tag' +import { createSignature } from '../security' +import request from 'request' +const debug = require('debug')('ea:utils') + +export function extractNameFromId (uri) { + const urlObject = new URL(uri) + const pathname = urlObject.pathname + const splitted = pathname.split('/') + + return splitted[splitted.indexOf('users') + 1] +} + +export function extractIdFromActivityId (uri) { + const urlObject = new URL(uri) + const pathname = urlObject.pathname + const splitted = pathname.split('/') + + return splitted[splitted.indexOf('status') + 1] +} + +export function constructIdFromName (name, fromDomain = activityPub.domain) { + return `http://${fromDomain}/activitypub/users/${name}` +} + +export function extractDomainFromUrl (url) { + return new URL(url).hostname +} + +export function throwErrorIfApolloErrorOccurred (result) { + if (result.error && (result.error.message || result.error.errors)) { + throw new Error(`${result.error.message ? result.error.message : result.error.errors[0].message}`) + } +} + +export function signAndSend (activity, fromName, targetDomain, url) { + // fix for development: replace with http + url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url + debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`) + return new Promise(async (resolve, reject) => { + debug('inside signAndSend') + // get the private key + const result = await activityPub.dataSource.client.query({ + query: gql` + query { + User(slug: "${fromName}") { + privateKey + } + } + ` + }) + + if (result.error) { + reject(result.error) + } else { + // add security context + const parsedActivity = JSON.parse(activity) + if (Array.isArray(parsedActivity['@context'])) { + parsedActivity['@context'].push('https://w3id.org/security/v1') + } else { + const context = [parsedActivity['@context']] + context.push('https://w3id.org/security/v1') + parsedActivity['@context'] = context + } + + // deduplicate context strings + parsedActivity['@context'] = [...new Set(parsedActivity['@context'])] + const privateKey = result.data.User[0].privateKey + const date = new Date().toUTCString() + + debug(`url = ${url}`) + request({ + url: url, + headers: { + 'Host': targetDomain, + 'Date': date, + 'Signature': createSignature(privateKey, `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url, + { + 'Host': targetDomain, + 'Date': date, + 'Content-Type': 'application/activity+json' + }), + 'Content-Type': 'application/activity+json' + }, + method: 'POST', + body: JSON.stringify(parsedActivity) + }, (error, response) => { + if (error) { + debug(`Error = ${JSON.stringify(error, null, 2)}`) + reject(error) + } else { + debug('Response Headers:', JSON.stringify(response.headers, null, 2)) + debug('Response Body:', JSON.stringify(response.body, null, 2)) + resolve() + } + }) + } + }) +} diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 6832d2a7c..b9cdfdb37 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -1,5 +1,6 @@ import fs from 'fs' import path from 'path' + import userManagement from './resolvers/user_management.js' import statistics from './resolvers/statistics.js' import reports from './resolvers/reports.js' diff --git a/src/index.js b/src/index.js index f594cb7f7..c91f3e9be 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import createServer from './server' +import ActivityPub from './activitypub/ActivityPub' const serverConfig = { port: process.env.GRAPHQL_PORT || 4000 @@ -12,4 +13,5 @@ const server = createServer() server.start(serverConfig, options => { /* eslint-disable-next-line no-console */ console.log(`Server ready at ${process.env.GRAPHQL_URI} 🚀`) + ActivityPub.init(server) }) diff --git a/src/jwt/encode.js b/src/jwt/encode.js index 62e85616b..f32fc12da 100644 --- a/src/jwt/encode.js +++ b/src/jwt/encode.js @@ -10,7 +10,7 @@ export default function encode (user) { audience: process.env.CLIENT_URI, subject: user.id.toString() }) - // jwt.verify(token, process.env.JWT_SECRET, (err, data) => { + // jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => { // console.log('token verification:', err, data) // }) return token diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index 55b181bc9..a85bd1244 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,8 +1,15 @@ import createOrUpdateLocations from './nodes/locations' +import { generateRsaKeyPair } from '../activitypub/security' +import dotenv from 'dotenv' + +dotenv.config() export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { + const keys = generateRsaKeyPair() + Object.assign(args, keys) + args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}` const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result diff --git a/src/resolvers/badges.spec.js b/src/resolvers/badges.spec.js index e38f54381..1966ce241 100644 --- a/src/resolvers/badges.spec.js +++ b/src/resolvers/badges.spec.js @@ -3,6 +3,7 @@ import { GraphQLClient } from 'graphql-request' import { host, login } from '../jest/helpers' const factory = Factory() +let client describe('badges', () => { beforeEach(async () => { @@ -55,8 +56,6 @@ describe('badges', () => { ` describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( @@ -66,7 +65,6 @@ describe('badges', () => { }) describe('authenticated admin', () => { - let client beforeEach(async () => { const headers = await login({ email: 'admin@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) @@ -86,7 +84,6 @@ describe('badges', () => { }) describe('authenticated moderator', () => { - let client beforeEach(async () => { const headers = await login({ email: 'moderator@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) @@ -120,8 +117,6 @@ describe('badges', () => { ` describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( @@ -131,7 +126,6 @@ describe('badges', () => { }) describe('authenticated moderator', () => { - let client beforeEach(async () => { const headers = await login({ email: 'moderator@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) @@ -145,7 +139,6 @@ describe('badges', () => { }) describe('authenticated admin', () => { - let client beforeEach(async () => { const headers = await login({ email: 'admin@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) @@ -180,8 +173,6 @@ describe('badges', () => { ` describe('unauthenticated', () => { - let client - it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( @@ -191,7 +182,6 @@ describe('badges', () => { }) describe('authenticated moderator', () => { - let client beforeEach(async () => { const headers = await login({ email: 'moderator@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) @@ -205,7 +195,6 @@ describe('badges', () => { }) describe('authenticated admin', () => { - let client beforeEach(async () => { const headers = await login({ email: 'admin@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index abf91e047..b7a6d8a2a 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,20 +1,65 @@ import { neo4jgraphql } from 'neo4j-graphql-js' +import { activityPub } from '../activitypub/ActivityPub' +import as from 'activitystrea.ms' +import dotenv from 'dotenv' +/* +import as from 'activitystrea.ms' +import request from 'request' +*/ + +const debug = require('debug')('backend:schema') +dotenv.config() export default { Mutation: { CreatePost: async (object, params, context, resolveInfo) => { + params.activityId = activityPub.generateStatusId(context.user.slug) + params.objectId = activityPub.generateStatusId(context.user.slug) const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() - await session.run( + const author = await session.run( 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MERGE (post)<-[:WROTE]-(author) ' + 'RETURN author', { userId: context.user.id, postId: result.id - }) + } + ) session.close() + debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`) + if (Array.isArray(author.records) && author.records.length > 0) { + const actorId = author.records[0]._fields[0].properties.actorId + const createActivity = await new Promise((resolve, reject) => { + as.create() + .id(`${actorId}/status/${params.activityId}`) + .actor(`${actorId}`) + .object( + as.article() + .id(`${actorId}/status/${result.id}`) + .content(result.content) + .to('https://www.w3.org/ns/activitystreams#Public') + .publishedNow() + .attributedTo(`${actorId}`) + ).prettyWrite((err, doc) => { + if (err) { + reject(err) + } else { + debug(doc) + const parsedDoc = JSON.parse(doc) + parsedDoc.send = true + resolve(JSON.stringify(parsedDoc)) + } + }) + }) + try { + await activityPub.sendActivity(createActivity) + } catch (e) { + debug(`error sending post activity\n${e}`) + } + } + return result } } diff --git a/src/schema.graphql b/src/schema.graphql index 801eb4502..af7764f33 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -102,6 +102,7 @@ type Location { type User { id: ID! + actorId: String name: String email: String slug: String @@ -111,6 +112,8 @@ type User { disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") role: UserGroupEnum + publicKey: String + privateKey: String location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l") locationName: String @@ -166,6 +169,8 @@ type User { type Post { id: ID! + activityId: String + objectId: String author: User @relation(name: "WROTE", direction: "IN") title: String! slug: String @@ -203,6 +208,7 @@ type Post { type Comment { id: ID! + activityId: String author: User @relation(name: "WROTE", direction: "IN") content: String! contentExcerpt: String @@ -288,3 +294,7 @@ type Tag { deleted: Boolean disabled: Boolean } +type SharedInboxEndpoint { + id: ID! + uri: String +} diff --git a/src/server.js b/src/server.js index 8e1cd36da..c546c74c3 100644 --- a/src/server.js +++ b/src/server.js @@ -8,6 +8,7 @@ import middleware from './middleware' import applyDirectives from './bootstrap/directives' import applyScalars from './bootstrap/scalars' import { getDriver } from './bootstrap/neo4j' +import helmet from 'helmet' import decode from './jwt/decode' dotenv.config() @@ -59,6 +60,7 @@ const createServer = (options) => { } const server = new GraphQLServer(Object.assign({}, defaults, options)) + server.express.use(helmet()) server.express.use(express.static('public')) return server } diff --git a/test/features/activity-delete.feature b/test/features/activity-delete.feature new file mode 100644 index 000000000..f5e269cce --- /dev/null +++ b/test/features/activity-delete.feature @@ -0,0 +1,55 @@ +Feature: Delete an object + I want to delete objects + + Background: + Given our own server runs at "http://localhost:4123" + And we have the following users in our database: + | Slug | + | bernd-das-brot| + And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://aronda.org/users/bernd-das-brot/status/lka7dfzkjn2398hsfd", + "type": "Create", + "actor": "https://aronda.org/users/bernd-das-brot", + "object": { + "id": "https://aronda.org/users/bernd-das-brot/status/kljsdfg9843jknsdf234", + "type": "Article", + "published": "2019-02-07T19:37:55.002Z", + "attributedTo": "https://aronda.org/users/bernd-das-brot", + "content": "Hi Max, how are you?", + "to": "https://www.w3.org/ns/activitystreams#Public" + } + } + """ + + Scenario: Deleting a post (Article Object) + When I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/karl-heinz/status/a4DJ2afdg323v32641vna42lkj685kasd2", + "type": "Delete", + "object": { + "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", + "type": "Article", + "published": "2019-02-07T19:37:55.002Z", + "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", + "content": "Hi Max, how are you?", + "to": "https://www.w3.org/ns/activitystreams#Public" + } + } + """ + Then I expect the status code to be 200 + And the object is removed from the outbox collection of "bernd-das-brot" + """ + { + "id": "https://aronda.org/activitypub/users/bernd-das-brot/status/kljsdfg9843jknsdf234", + "type": "Article", + "published": "2019-02-07T19:37:55.002Z", + "attributedTo": "https://aronda.org/activitypub/users/bernd-das-brot", + "content": "Hi Max, how are you?", + "to": "https://www.w3.org/ns/activitystreams#Public" + } + """ diff --git a/test/features/activity-follow.feature b/test/features/activity-follow.feature new file mode 100644 index 000000000..6634a342b --- /dev/null +++ b/test/features/activity-follow.feature @@ -0,0 +1,50 @@ +Feature: Follow a user + I want to be able to follow a user on another instance. + Also if I do not want to follow a previous followed user anymore, + I want to undo the follow. + + Background: + Given our own server runs at "http://localhost:4123" + And we have the following users in our database: + | Slug | + | stuart-little | + | tero-vota | + + Scenario: Send a follow to a user inbox and make sure it's added to the right followers collection + When I send a POST request with the following activity to "/activitypub/users/tero-vota/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83", + "type": "Follow", + "actor": "http://localhost:4123/activitypub/users/stuart-little", + "object": "http://localhost:4123/activitypub/users/tero-vota" + } + """ + Then I expect the status code to be 200 + And the follower is added to the followers collection of "tero-vota" + """ + http://localhost:4123/activitypub/users/stuart-little + """ + + Scenario: Send an undo activity to revert the previous follow activity + When I send a POST request with the following activity to "/activitypub/users/stuart-little/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/tero-vota/status/a4DJ2afdg323v32641vna42lkj685kasd2", + "type": "Undo", + "actor": "http://localhost:4123/activitypub/users/tero-vota", + "object": { + "id": "https://localhost:4123/activitypub/users/stuart-little/status/83J23549sda1k72fsa4567na42312455kad83", + "type": "Follow", + "actor": "http://localhost:4123/activitypub/users/stuart-little", + "object": "http://localhost:4123/activitypub/users/tero-vota" + } + } + """ + Then I expect the status code to be 200 + And the follower is removed from the followers collection of "tero-vota" + """ + http://localhost:4123/activitypub/users/stuart-little + """ diff --git a/test/features/activity-like.feature b/test/features/activity-like.feature new file mode 100644 index 000000000..35d32c842 --- /dev/null +++ b/test/features/activity-like.feature @@ -0,0 +1,42 @@ +Feature: Like an object like an article or note + As a user I want to like others posts + Also if I do not want to follow a previous followed user anymore, + I want to undo the follow. + + Background: + Given our own server runs at "http://localhost:4123" + And we have the following users in our database: + | Slug | + | karl-heinz | + | peter-lustiger | + And I send a POST request with the following activity to "/activitypub/users/bernd-das-brot/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/karl-heinz/status/faslkasa7dasfzkjn2398hsfd", + "type": "Create", + "actor": "https://localhost:4123/activitypub/users/karl-heinz", + "object": { + "id": "https://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf", + "type": "Article", + "published": "2019-02-07T19:37:55.002Z", + "attributedTo": "https://localhost:4123/activitypub/users/karl-heinz", + "content": "Hi Max, how are you?", + "to": "https://www.w3.org/ns/activitystreams#Public" + } + } + """ + + Scenario: Send a like of a person to an users inbox and make sure it's added to the likes collection + When I send a POST request with the following activity to "/activitypub/users/karl-heinz/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/peter-lustiger/status/83J23549sda1k72fsa4567na42312455kad83", + "type": "Like", + "actor": "http://localhost:4123/activitypub/users/peter-lustiger", + "object": "http://localhost:4123/activitypub/users/karl-heinz/status/dkasfljsdfaafg9843jknsdf" + } + """ + Then I expect the status code to be 200 + And the post with id "dkasfljsdfaafg9843jknsdf" has been liked by "peter-lustiger" diff --git a/test/features/collection.feature b/test/features/collection.feature new file mode 100644 index 000000000..536d3aa2d --- /dev/null +++ b/test/features/collection.feature @@ -0,0 +1,101 @@ +Feature: Receiving collections + As a member of the Fediverse I want to be able of fetching collections + + Background: + Given our own server runs at "http://localhost:4123" + And we have the following users in our database: + | Slug | + | renate-oberdorfer | + + Scenario: Send a request to the outbox URI of peter-lustig and expect a ordered collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox", + "summary": "renate-oberdorfers outbox collection", + "type": "OrderedCollection", + "first": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true", + "totalItems": 0 + } + """ + + Scenario: Send a request to the following URI of peter-lustig and expect a ordered collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/following" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following", + "summary": "renate-oberdorfers following collection", + "type": "OrderedCollection", + "first": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true", + "totalItems": 0 + } + """ + + Scenario: Send a request to the followers URI of peter-lustig and expect a ordered collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/followers" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers", + "summary": "renate-oberdorfers followers collection", + "type": "OrderedCollection", + "first": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true", + "totalItems": 0 + } + """ + + Scenario: Send a request to the outbox URI of peter-lustig and expect a paginated outbox collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/outbox?page=true" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox?page=true", + "summary": "renate-oberdorfers outbox collection", + "type": "OrderedCollectionPage", + "totalItems": 0, + "partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/outbox", + "orderedItems": [] + } + """ + + Scenario: Send a request to the following URI of peter-lustig and expect a paginated following collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/following?page=true" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/following?page=true", + "summary": "renate-oberdorfers following collection", + "type": "OrderedCollectionPage", + "totalItems": 0, + "partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/following", + "orderedItems": [] + } + """ + + Scenario: Send a request to the followers URI of peter-lustig and expect a paginated followers collection + When I send a GET request to "/activitypub/users/renate-oberdorfer/followers?page=true" + Then I expect the status code to be 200 + And I receive the following json: + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers?page=true", + "summary": "renate-oberdorfers followers collection", + "type": "OrderedCollectionPage", + "totalItems": 0, + "partOf": "https://localhost:4123/activitypub/users/renate-oberdorfer/followers", + "orderedItems": [] + } + """ diff --git a/test/features/object-article.feature b/test/features/object-article.feature new file mode 100644 index 000000000..030e408e9 --- /dev/null +++ b/test/features/object-article.feature @@ -0,0 +1,30 @@ +Feature: Send and receive Articles + I want to send and receive article's via ActivityPub + + Background: + Given our own server runs at "http://localhost:4123" + And we have the following users in our database: + | Slug | + | marvin | + | max | + + Scenario: Send an article to a user inbox and make sure it's added to the inbox + When I send a POST request with the following activity to "/activitypub/users/max/inbox": + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://aronda.org/users/marvin/status/lka7dfzkjn2398hsfd", + "type": "Create", + "actor": "https://aronda.org/users/marvin", + "object": { + "id": "https://aronda.org/users/marvin/status/kljsdfg9843jknsdf", + "type": "Article", + "published": "2019-02-07T19:37:55.002Z", + "attributedTo": "https://aronda.org/users/marvin", + "content": "Hi Max, how are you?", + "to": "as:Public" + } + } + """ + Then I expect the status code to be 200 + And the post with id "kljsdfg9843jknsdf" to be created diff --git a/test/features/support/steps.js b/test/features/support/steps.js new file mode 100644 index 000000000..fdbca289c --- /dev/null +++ b/test/features/support/steps.js @@ -0,0 +1,158 @@ +// features/support/steps.js +import { Given, When, Then, AfterAll } from 'cucumber' +import { expect } from 'chai' +// import { client } from '../../../src/activitypub/apollo-client' +import { GraphQLClient } from 'graphql-request' +import Factory from '../../../src/seed/factories' +import { host } from '../../../src/jest/helpers' +const debug = require('debug')('ea:test:steps') + +const factory = Factory() +const client = new GraphQLClient(host) + +function createUser (slug) { + debug(`creating user ${slug}`) + return factory.create('User', { + name: slug, + email: 'example@test.org', + password: '1234' + }) + // await login({ email: 'example@test.org', password: '1234' }) +} + +Given('our own server runs at {string}', function (string) { + // just documenation +}) + +Given('we have the following users in our database:', function (dataTable) { + return Promise.all(dataTable.hashes().map((user) => { + return createUser(user.Slug) + })) +}) + +When('I send a GET request to {string}', async function (pathname) { + const response = await this.get(pathname) + this.lastContentType = response.lastContentType + + this.lastResponses.push(response.lastResponse) + this.statusCode = response.statusCode +}) + +When('I send a POST request with the following activity to {string}:', async function (inboxUrl, activity) { + debug(`inboxUrl = ${inboxUrl}`) + debug(`activity = ${activity}`) + const splitted = inboxUrl.split('/') + const slug = splitted[splitted.indexOf('users') + 1] + let result + do { + result = await client.request(` + query { + User(slug: "${slug}") { + id + slug + actorId + } + } + `) + } while (result.User.length === 0) + this.lastInboxUrl = inboxUrl + this.lastActivity = activity + const response = await this.post(inboxUrl, activity) + + this.lastResponses.push(response.lastResponse) + this.lastResponse = response.lastResponse + this.statusCode = response.statusCode +}) + +Then('I receive the following json:', function (docString) { + const parsedDocString = JSON.parse(docString) + const parsedLastResponse = JSON.parse(this.lastResponses.shift()) + if (Array.isArray(parsedDocString.orderedItems)) { + parsedDocString.orderedItems.forEach((el) => { + delete el.id + if (el.object) delete el.object.published + }) + parsedLastResponse.orderedItems.forEach((el) => { + delete el.id + if (el.object) delete el.object.published + }) + } + if (parsedDocString.publicKey && parsedDocString.publicKey.publicKeyPem) { + delete parsedDocString.publicKey.publicKeyPem + delete parsedLastResponse.publicKey.publicKeyPem + } + expect(parsedDocString).to.eql(parsedLastResponse) +}) + +Then('I expect the Content-Type to be {string}', function (contentType) { + expect(this.lastContentType).to.equal(contentType) +}) + +Then('I expect the status code to be {int}', function (statusCode) { + expect(this.statusCode).to.equal(statusCode) +}) + +Then('the activity is added to the {string} collection', async function (collectionName) { + const response = await this.get(this.lastInboxUrl.replace('inbox', collectionName) + '?page=true') + debug(`orderedItems = ${JSON.parse(response.lastResponse).orderedItems}`) + expect(JSON.parse(response.lastResponse).orderedItems).to.include(JSON.parse(this.lastActivity).object) +}) + +Then('the follower is added to the followers collection of {string}', async function (userName, follower) { + const response = await this.get(`/activitypub/users/${userName}/followers?page=true`) + const responseObject = JSON.parse(response.lastResponse) + expect(responseObject.orderedItems).to.include(follower) +}) + +Then('the follower is removed from the followers collection of {string}', async function (userName, follower) { + const response = await this.get(`/activitypub/users/${userName}/followers?page=true`) + const responseObject = JSON.parse(response.lastResponse) + expect(responseObject.orderedItems).to.not.include(follower) +}) + +Then('the post with id {string} to be created', async function (id) { + let result + do { + result = await client.request(` + query { + Post(id: "${id}") { + title + } + } + `) + } while (result.Post.length === 0) + + expect(result.Post).to.be.an('array').that.is.not.empty // eslint-disable-line +}) + +Then('the object is removed from the outbox collection of {string}', async function (name, object) { + const response = await this.get(`/activitypub/users/${name}/outbox?page=true`) + const parsedResponse = JSON.parse(response.lastResponse) + expect(parsedResponse.orderedItems).to.not.include(object) +}) + +Then('I send a GET request to {string} and expect a ordered collection', () => { + +}) + +Then('the activity is added to the users inbox collection', async function () { + +}) + +Then('the post with id {string} has been liked by {string}', async function (id, slug) { + let result + do { + result = await client.request(` + query { + Post(id: "${id}") { + shoutedBy { + slug + } + } + } + `) + } while (result.Post.length === 0) + + expect(result.Post[0].shoutedBy).to.be.an('array').that.is.not.empty // eslint-disable-line + expect(result.Post[0].shoutedBy[0].slug).to.equal(slug) +}) diff --git a/test/features/webfinger.feature b/test/features/webfinger.feature new file mode 100644 index 000000000..e7d1ace04 --- /dev/null +++ b/test/features/webfinger.feature @@ -0,0 +1,65 @@ +Feature: Webfinger discovery + From an external server, e.g. Mastodon + I want to search for an actor alias + In order to follow the actor + + Background: + Given our own server runs at "http://localhost:4100" + And we have the following users in our database: + | Slug | + | peter-lustiger | + + Scenario: Search + When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost" + Then I receive the following json: + """ + { + "subject": "acct:peter-lustiger@localhost:4123", + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": "https://localhost:4123/users/peter-lustiger" + } + ] + } + """ + And I expect the Content-Type to be "application/jrd+json; charset=utf-8" + + Scenario: User does not exist + When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost" + Then I receive the following json: + """ + { + "error": "No record found for nonexisting@localhost." + } + """ + + Scenario: Receiving an actor object + When I send a GET request to "/activitypub/users/peter-lustiger" + Then I receive the following json: + """ + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1" + ], + "id": "https://localhost:4123/activitypub/users/peter-lustiger", + "type": "Person", + "preferredUsername": "peter-lustiger", + "name": "peter-lustiger", + "following": "https://localhost:4123/activitypub/users/peter-lustiger/following", + "followers": "https://localhost:4123/activitypub/users/peter-lustiger/followers", + "inbox": "https://localhost:4123/activitypub/users/peter-lustiger/inbox", + "outbox": "https://localhost:4123/activitypub/users/peter-lustiger/outbox", + "url": "https://localhost:4123/activitypub/@peter-lustiger", + "endpoints": { + "sharedInbox": "https://localhost:4123/activitypub/inbox" + }, + "publicKey": { + "id": "https://localhost:4123/activitypub/users/peter-lustiger#main-key", + "owner": "https://localhost:4123/activitypub/users/peter-lustiger", + "publicKeyPem": "adglkjlk89235kjn8obn2384f89z5bv9..." + } + } + """ diff --git a/test/features/world.js b/test/features/world.js new file mode 100644 index 000000000..be436b536 --- /dev/null +++ b/test/features/world.js @@ -0,0 +1,58 @@ +// features/support/world.js +import { setWorldConstructor } from 'cucumber' +import request from 'request' +const debug = require('debug')('ea:test:world') + +class CustomWorld { + constructor () { + // webFinger.feature + this.lastResponses = [] + this.lastContentType = null + this.lastInboxUrl = null + this.lastActivity = null + // object-article.feature + this.statusCode = null + } + get (pathname) { + return new Promise((resolve, reject) => { + request(`http://localhost:4123/${this.replaceSlashes(pathname)}`, { + headers: { + 'Accept': 'application/activity+json' + }}, function (error, response, body) { + if (!error) { + debug(`get content-type = ${response.headers['content-type']}`) + debug(`get body = ${JSON.stringify(typeof body === 'string' ? JSON.parse(body) : body, null, 2)}`) + resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode }) + } else { + reject(error) + } + }) + }) + } + + replaceSlashes (pathname) { + return pathname.replace(/^\/+/, '') + } + + post (pathname, activity) { + return new Promise((resolve, reject) => { + request({ + url: `http://localhost:4123/${this.replaceSlashes(pathname)}`, + method: 'POST', + headers: { + 'Content-Type': 'application/activity+json' + }, + body: activity + }, function (error, response, body) { + if (!error) { + debug(`post response = ${response.headers['content-type']}`) + resolve({ lastResponse: body, lastContentType: response.headers['content-type'], statusCode: response.statusCode }) + } else { + reject(error) + } + }) + }) + } +} + +setWorldConstructor(CustomWorld) diff --git a/yarn.lock b/yarn.lock index d88dc494e..131d6a46d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -337,6 +337,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" +"@babel/plugin-proposal-throw-expressions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739" + integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-throw-expressions" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" @@ -374,6 +382,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-throw-expressions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz#79001ee2afe1b174b1733cdc2fc69c9a46a0f1f8" + integrity sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -604,6 +619,14 @@ core-js "^2.5.7" regenerator-runtime "^0.11.1" +"@babel/polyfill@^7.2.3": + version "7.2.5" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d" + integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug== + dependencies: + core-js "^2.5.7" + regenerator-runtime "^0.12.0" + "@babel/preset-env@~7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" @@ -1144,6 +1167,33 @@ acorn@^6.0.7: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== +activitystrea.ms@~2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/activitystrea.ms/-/activitystrea.ms-2.1.3.tgz#553548733e367dc0b6a7badc25fa6f8996cd80c3" + integrity sha512-iiG5g5fYgfdaaqqFPaFIZC/KX8/4mOWkvniK+BNwJY6XDDKdIu56wmc9r0x1INHVnbFOTGuM8mZEntaM3I+YXw== + dependencies: + activitystreams-context "^3.0.0" + jsonld "^0.4.11" + jsonld-signatures "^1.1.5" + moment "^2.17.1" + readable-stream "^2.2.3" + reasoner "2.0.0" + rfc5646 "^2.0.0" + vocabs-as "^3.0.0" + vocabs-asx "^0.11.1" + vocabs-interval "^0.11.1" + vocabs-ldp "^0.1.0" + vocabs-owl "^0.11.1" + vocabs-rdf "^0.11.1" + vocabs-rdfs "^0.11.1" + vocabs-social "^0.11.1" + vocabs-xsd "^0.11.1" + +activitystreams-context@>=3.0.0, activitystreams-context@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/activitystreams-context/-/activitystreams-context-3.1.0.tgz#28334e129f17cfb937e8c702c52c1bcb1d2830c7" + integrity sha512-KBQ+igwf1tezMXGVw5MvRSEm0gp97JI1hTZ45I6MEkWv25lEgNoA9L6wqfaOiCX8wnMRWw9pwRsPZKypdtxAtg== + ajv@^6.5.5, ajv@^6.9.1: version "6.9.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b" @@ -1193,6 +1243,11 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -1300,6 +1355,14 @@ apollo-graphql@^0.1.0: dependencies: lodash.sortby "^4.7.0" +apollo-link-context@~1.0.14: + version "1.0.17" + resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.17.tgz#439272cfb43ec1891506dd175ed907845b7de36c" + integrity sha512-W5UUfHcrrlP5uqJs5X1zbf84AMXhPZGAqX/7AQDgR6wY/7//sMGfJvm36KDkpIeSOElztGtM9z6zdPN1NbT41Q== + dependencies: + apollo-link "^1.2.11" + tslib "^1.9.3" + apollo-link-dedup@^1.0.0: version "1.0.11" resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.11.tgz#6f34ea748d2834850329ad03111ef18445232b05" @@ -1335,6 +1398,16 @@ apollo-link@^1.0.0, apollo-link@^1.2.10, apollo-link@^1.2.3, apollo-link@^1.2.4: tslib "^1.9.3" zen-observable-ts "^0.8.17" +apollo-link@^1.2.11: + version "1.2.11" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d" + integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA== + dependencies: + apollo-utilities "^1.2.1" + ts-invariant "^0.3.2" + tslib "^1.9.3" + zen-observable-ts "^0.8.18" + apollo-server-caching@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.3.1.tgz#63fcb2aaa176e1e101b36a8450e6b4c593d2767a" @@ -1592,6 +1665,15 @@ assert@^1.4.1: dependencies: util "0.10.3" +assertion-error-formatter@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error-formatter/-/assertion-error-formatter-2.0.1.tgz#6bbdffaec8e2fa9e2b0eb158bfe353132d7c0a9b" + integrity sha512-cjC3jUCh9spkroKue5PDSKH5RFQ/KNuZJhk3GwHYmB/8qqETxLOmMdLH+ohi/VukNzxDlMvIe7zScvLoOdhb6Q== + dependencies: + diff "^3.0.0" + pad-right "^0.2.2" + repeat-string "^1.6.1" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -1634,6 +1716,11 @@ async-retry@^1.2.1: dependencies: retry "0.12.0" +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + async@^2.5.0, async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" @@ -1787,11 +1874,49 @@ bcryptjs@~2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= +becke-ch--regex--s0-0-v1--base--pl--lib@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz#429ceebbfa5f7e936e78d73fbdc7da7162b20e20" + integrity sha1-Qpzuu/pffpNueNc/vcfacWKyDiA= + binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg== +bitcore-lib@^0.13.7: + version "0.13.19" + resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc" + integrity sha1-SK8em9oQBnwasWJjRyta3SAA89w= + dependencies: + bn.js "=2.0.4" + bs58 "=2.0.0" + buffer-compare "=1.0.0" + elliptic "=3.0.3" + inherits "=2.0.1" + lodash "=3.10.1" + +"bitcore-message@github:comakery/bitcore-message#dist": + version "1.0.2" + resolved "https://codeload.github.com/comakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf" + dependencies: + bitcore-lib "^0.13.7" + +bluebird@^3.4.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== + +bn.js@=2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480" + integrity sha1-Igp81nf38b+pNif/QZN3b+eBlIA= + +bn.js@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625" + integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU= + body-parser-graphql@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/body-parser-graphql/-/body-parser-graphql-1.1.0.tgz#80a80353c7cb623562fd375750dfe018d75f0f7c" @@ -1857,6 +1982,11 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + browser-process-hrtime@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" @@ -1878,6 +2008,11 @@ browserslist@^4.3.4: electron-to-chromium "^1.3.86" node-releases "^1.0.5" +bs58@=2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5" + integrity sha1-crcTvtIjoKxRi72g484/SBfznrU= + bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -1885,6 +2020,11 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" +buffer-compare@=1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2" + integrity sha1-rKp6lm6Y7un64Usxw5pfFY+zxKI= + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -1962,6 +2102,11 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== +camelize@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + caniuse-lite@^1.0.30000912: version "1.0.30000916" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000916.tgz#3428d3f529f0a7b2bfaaec65e796037bdd433aab" @@ -2082,7 +2227,7 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-table3@^0.5.0: +cli-table3@^0.5.0, cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -2148,7 +2293,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.5.0, commander@^2.8.1: +commander@^2.5.0, commander@^2.8.1, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2158,6 +2303,13 @@ commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== +commander@~2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= + dependencies: + graceful-readlink ">= 1.0.0" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2205,6 +2357,11 @@ content-disposition@0.5.2: resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= +content-security-policy-builder@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz#8749a1d542fcbe82237281ea9f716ce68b394dd2" + integrity sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w== + content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -2261,7 +2418,7 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cors@^2.8.4: +cors@^2.8.4, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -2352,6 +2509,60 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +cucumber-expressions@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-6.0.1.tgz#47c9c573781c2ff721d7ad5b2cd1c97f4399ab8e" + integrity sha1-R8nFc3gcL/ch161bLNHJf0OZq44= + dependencies: + becke-ch--regex--s0-0-v1--base--pl--lib "^1.2.0" + +cucumber-tag-expressions@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-1.1.1.tgz#7f5c7b70009bc2b666591bfe64854578bedee85a" + integrity sha1-f1x7cACbwrZmWRv+ZIVFeL7e6Fo= + +cucumber@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-5.1.0.tgz#7b166812c255bec7eac4b0df7007a40d089c895d" + integrity sha512-zrl2VYTBRgvxucwV2GKAvLqcfA1Naeax8plPvWgPEzl3SCJiuPPv3WxBHIRHtPYcEdbHDR6oqLpZP4bJ8UIdmA== + dependencies: + "@babel/polyfill" "^7.2.3" + assertion-error-formatter "^2.0.1" + bluebird "^3.4.1" + cli-table3 "^0.5.1" + colors "^1.1.2" + commander "^2.9.0" + cross-spawn "^6.0.5" + cucumber-expressions "^6.0.0" + cucumber-tag-expressions "^1.1.1" + duration "^0.2.1" + escape-string-regexp "^1.0.5" + figures "2.0.0" + gherkin "^5.0.0" + glob "^7.1.3" + indent-string "^3.1.0" + is-generator "^1.0.2" + is-stream "^1.1.0" + knuth-shuffle-seeded "^1.0.6" + lodash "^4.17.10" + mz "^2.4.0" + progress "^2.0.0" + resolve "^1.3.3" + serialize-error "^3.0.0" + stack-chain "^2.0.0" + stacktrace-js "^2.0.0" + string-argv "0.1.1" + title-case "^2.1.1" + util-arity "^1.0.2" + verror "^1.9.0" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8= + dependencies: + es5-ext "^0.10.9" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2359,6 +2570,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +dasherize@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308" + integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg= + data-urls@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" @@ -2387,7 +2603,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -2467,6 +2683,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2505,6 +2726,16 @@ diff-sequences@^24.3.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975" integrity sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw== +diff@^3.0.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dns-prefetch-control@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2" + integrity sha1-YN20V3dOF48flBXwyrsOhbCzALI= + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -2568,6 +2799,11 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +dont-sniff-mimetype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58" + integrity sha1-WTKJDcn04vGeXrAqIAJuXl78j1g= + dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -2590,6 +2826,14 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= +duration@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/duration/-/duration-0.2.2.tgz#ddf149bc3bc6901150fe9017111d016b3357f529" + integrity sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg== + dependencies: + d "1" + es5-ext "~0.10.46" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2615,6 +2859,16 @@ electron-to-chromium@^1.3.86: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.88.tgz#f36ab32634f49ef2b0fdc1e82e2d1cc17feb29e7" integrity sha512-UPV4NuQMKeUh1S0OWRvwg0PI8ASHN9kBC8yDTk1ROXLC85W5GnhTRu/MZu3Teqx3JjlQYuckuHYXSUSgtb3J+A== +elliptic@=3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595" + integrity sha1-hlybQgv75VAGuflp+XoNLESWZZU= + dependencies: + bn.js "^2.0.0" + brorand "^1.0.1" + hash.js "^1.0.0" + inherits "^2.0.1" + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -2644,6 +2898,13 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d" + integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw== + dependencies: + stackframe "^1.0.4" + es-abstract@^1.4.3: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" @@ -2676,11 +2937,52 @@ es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.46: + version "0.10.49" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e" + integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "^1.0.0" + es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-promise@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.3.0.tgz#96edb9f2fdb01995822b263dd8aadab6748181bc" + integrity sha1-lu258v2wGZWCKyY92KratnSBgbw= + +es6-promise@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.0.1.tgz#ccc4963e679f0ca9fb187c777b9e583d3c7573c2" + integrity sha1-zMSWPmefDKn7GHx3e55YPTx1c8I= + +es6-promise@~4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" + integrity sha1-eILzCt3lskDM+n99eMVIMwlRrkI= + +es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -2950,6 +3252,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expect-ct@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.1.1.tgz#de84476a2dbcb85000d5903737e9bc8a5ba7b897" + integrity sha512-ngXzTfoRGG7fYens3/RMb6yYoVLvLMfmsSllP/mZPxNHgFq41TmPSLF/nLY7fwoclI2vElvAmILFWGUYqdjfCg== + expect@^24.5.0: version "24.5.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.5.0.tgz#492fb0df8378d8474cc84b827776b069f46294ed" @@ -2962,7 +3269,7 @@ expect@^24.5.0: jest-message-util "^24.5.0" jest-regex-util "^24.3.0" -express@^4.0.0, express@^4.16.3: +express@^4.0.0, express@^4.16.3, express@~4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== @@ -3083,7 +3390,12 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -figures@^2.0.0: +feature-policy@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.2.0.tgz#22096de49ab240176878ffe2bde2f6ff04d48c43" + integrity sha512-2hGrlv6efG4hscYVZeaYjpzpT6I2OZgYqE2yDUzeAcKj2D1SH0AsEzqJNXzdoglEddcIXQQYop3lD97XpG75Jw== + +figures@2.0.0, figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= @@ -3223,6 +3535,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +frameguard@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.0.0.tgz#7bcad469ee7b96e91d12ceb3959c78235a9272e9" + integrity sha1-e8rUae57lukdEs6zlZx4I1qScuk= + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -3316,6 +3633,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gherkin@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/gherkin/-/gherkin-5.1.0.tgz#684bbb03add24eaf7bdf544f58033eb28fb3c6d5" + integrity sha1-aEu7A63STq9731RPWAM+so+zxtU= + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -3370,6 +3692,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= + graphql-auth-directives@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/graphql-auth-directives/-/graphql-auth-directives-2.1.0.tgz#85b83817844e2ec5fba8fe5de444287d6dd0f85a" @@ -3627,6 +3954,14 @@ has@^1.0.1, has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.0.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hasha@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" @@ -3639,6 +3974,47 @@ he@0.5.0: resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= +helmet-crossdomain@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz#707e2df930f13ad61f76ed08e1bb51ab2b2e85fa" + integrity sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg== + +helmet-csp@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.7.1.tgz#e8e0b5186ffd4db625cfcce523758adbfadb9dca" + integrity sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ== + dependencies: + camelize "1.0.0" + content-security-policy-builder "2.0.0" + dasherize "2.0.0" + platform "1.3.5" + +helmet@~3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.15.1.tgz#2c80d1a59138b6f23929605afca4b1c88b3298ec" + integrity sha512-hgoNe/sjKlKNvJ3g9Gz149H14BjMMWOCmW/DTXl7IfyKGtIK37GePwZrHNfr4aPXdKVyXcTj26RgRFbPKDy9lw== + dependencies: + depd "2.0.0" + dns-prefetch-control "0.1.0" + dont-sniff-mimetype "1.0.0" + expect-ct "0.1.1" + feature-policy "0.2.0" + frameguard "3.0.0" + helmet-crossdomain "0.3.0" + helmet-csp "2.7.1" + hide-powered-by "1.0.0" + hpkp "2.0.0" + hsts "2.1.0" + ienoopen "1.0.0" + nocache "2.0.0" + referrer-policy "1.1.0" + x-xss-protection "1.1.0" + +hide-powered-by@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b" + integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys= + hoek@5.x.x: version "5.0.4" resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da" @@ -3666,6 +4042,16 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== +hpkp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672" + integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI= + +hsts@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc" + integrity sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA== + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" @@ -3739,6 +4125,11 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== +ienoopen@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b" + integrity sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms= + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -3792,6 +4183,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3805,7 +4201,7 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.1: +inherits@2.0.1, inherits@=2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= @@ -3987,6 +4383,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== +is-generator@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-generator/-/is-generator-1.0.3.tgz#c14c21057ed36e328db80347966c693f886389f3" + integrity sha1-wUwhBX7TbjKNuANHlmxpP4hjifM= + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -4675,6 +5076,38 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonld-signatures@^1.1.5: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-1.2.1.tgz#493df5df9cd3a9f1b1cb296bbd3d081679f20ca8" + integrity sha1-ST3135zTqfGxyylrvT0IFnnyDKg= + dependencies: + async "^1.5.2" + bitcore-message "github:CoMakery/bitcore-message#dist" + commander "~2.9.0" + es6-promise "~4.0.5" + jsonld "0.4.3" + node-forge "~0.6.45" + +jsonld@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.3.tgz#0bbc929190064d6650a5af5876e1bfdf0ed288f3" + integrity sha1-C7ySkZAGTWZQpa9YduG/3w7SiPM= + dependencies: + es6-promise "~2.0.1" + pkginfo "~0.3.0" + request "^2.61.0" + xmldom "0.1.19" + +jsonld@^0.4.11: + version "0.4.12" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.12.tgz#a02f205d5341414df1b6d8414f1b967a712073e8" + integrity sha1-oC8gXVNBQU3xtthBTxuWenEgc+g= + dependencies: + es6-promise "^2.0.0" + pkginfo "~0.4.0" + request "^2.61.0" + xmldom "0.1.19" + jsonwebtoken@^8.3.0, jsonwebtoken@~8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -4747,6 +5180,13 @@ kleur@^3.0.0: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.1.tgz#4f5b313f5fa315432a400f19a24db78d451ede62" integrity sha512-P3kRv+B+Ra070ng2VKQqW4qW7gd/v3iD8sy/zOdcYRsfiD+QBokQNOps/AfP6Hr48cBhIIBFWckB9aO+IZhrWg== +knuth-shuffle-seeded@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz#01f1b65733aa7540ee08d8b0174164d22081e4e1" + integrity sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE= + dependencies: + seed-random "~2.2.0" + latest-version@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" @@ -4898,6 +5338,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash@=3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= + lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -4915,6 +5360,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -5057,6 +5507,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -5109,6 +5564,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" +moment@^2.17.1: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -5129,6 +5589,20 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +n3@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/n3/-/n3-0.9.1.tgz#430b547d58dc7381408c45784dd8058171903932" + integrity sha1-QwtUfVjcc4FAjEV4TdgFgXGQOTI= + nan@^2.9.2: version "2.11.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" @@ -5189,11 +5663,28 @@ neo4j-graphql-js@~2.4.2: lodash "^4.17.11" neo4j-driver "^1.7.2" +next-tick@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + +nocache@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980" + integrity sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA= + node-fetch@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" @@ -5204,6 +5695,11 @@ node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@~2.3.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== +node-forge@~0.6.45: + version "0.6.49" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.49.tgz#f1ee95d5d74623938fe19d698aa5a26d54d2f60f" + integrity sha1-8e6V1ddGI5OP4Z1piqWibVTS9g8= + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -5402,7 +5898,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -5622,6 +6118,13 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" +pad-right@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" + integrity sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q= + dependencies: + repeat-string "^1.5.2" + parent-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.0.tgz#df250bdc5391f4a085fb589dad761f5ad6b865b5" @@ -5773,6 +6276,21 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkginfo@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= + +pkginfo@~0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" + integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= + +platform@1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" + integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q== + pn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" @@ -6027,7 +6545,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.3: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -6065,6 +6583,23 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +reasoner@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reasoner/-/reasoner-2.0.0.tgz#6ccf76cb9baf96b82c45ab0bd60211c2aa1b701b" + integrity sha1-bM92y5uvlrgsRasL1gIRwqobcBs= + dependencies: + n3 "^0.9.1" + rfc5646 "^2.0.0" + vocabs-asx "^0.11.1" + vocabs-rdf "^0.11.1" + vocabs-rdfs "^0.11.1" + vocabs-xsd "^0.11.1" + +referrer-policy@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79" + integrity sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk= + regenerate-unicode-properties@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" @@ -6172,7 +6707,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -6193,7 +6728,7 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@^2.87.0, request@^2.88.0: +request@^2.61.0, request@^2.87.0, request@^2.88.0, request@~2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -6256,7 +6791,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: +resolve@^1.3.2, resolve@^1.3.3, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== @@ -6281,6 +6816,11 @@ retry@0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +rfc5646@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/rfc5646/-/rfc5646-2.0.0.tgz#ac0c67b6cd04411ef7c80751ba159d9371ce116c" + integrity sha1-rAxnts0EQR73yAdRuhWdk3HOEWw= + rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -6378,6 +6918,11 @@ scheduler@^0.11.2: loose-envify "^1.1.0" object-assign "^4.1.1" +seed-random@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" + integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -6414,6 +6959,11 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" +serialize-error@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-3.0.0.tgz#80100282b09be33c611536f50033481cb9cc87cf" + integrity sha512-+y3nkkG/go1Vdw+2f/+XUXM1DXX1XcxTl99FfiD/OEPUNw4uo0i6FKABfTAN5ZcgGtjTRZcEbxcE/jtXbEY19A== + serve-static@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" @@ -6574,6 +7124,11 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6657,11 +7212,45 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-chain@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-2.0.0.tgz#d73d1172af89565f07438b5bcc086831b6689b2d" + integrity sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg== + +stack-generator@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.3.tgz#bb74385c67ffc4ccf3c4dee5831832d4e509c8a0" + integrity sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg== + dependencies: + stackframe "^1.0.4" + stack-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stackframe@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" + integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw== + +stacktrace-gps@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc" + integrity sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg== + dependencies: + source-map "0.5.6" + stackframe "^1.0.4" + +stacktrace-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58" + integrity sha1-d2ymRqlbxsayuQd2U2p/xyxt21g= + dependencies: + error-stack-parser "^2.0.1" + stack-generator "^2.0.1" + stacktrace-gps "^3.0.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -6690,6 +7279,11 @@ streamsearch@0.1.2: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +string-argv@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.1.tgz#66bd5ae3823708eaa1916fa5412703150d4ddfaf" + integrity sha512-El1Va5ehZ0XTj3Ekw4WFidXvTmt9SrC0+eigdojgtJMVtPkF0qbBe9fyNSl9eQf+kUHnTSQxdQYzuHfZy8V+DQ== + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -6901,6 +7495,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk= + dependencies: + any-promise "^1.0.0" + throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" @@ -6916,6 +7524,14 @@ timed-out@^4.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= +title-case@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" + integrity sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o= + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7174,6 +7790,11 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +upper-case@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + uri-js@^4.2.1, uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -7206,6 +7827,11 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +util-arity@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/util-arity/-/util-arity-1.1.0.tgz#59d01af1fdb3fede0ac4e632b0ab5f6ce97c9330" + integrity sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA= + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -7236,7 +7862,7 @@ uuid@3.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" integrity sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g== -uuid@^3.1.0, uuid@^3.3.2: +uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== @@ -7261,7 +7887,7 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.10.0: +verror@1.10.0, verror@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= @@ -7270,6 +7896,75 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vocabs-as@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vocabs-as/-/vocabs-as-3.0.0.tgz#0dd0549cecb331ba4e917d2c5a4e83b146865c23" + integrity sha512-Dfze+B0CYZzhSK12jWvbxaL8/vXPnlzhhqhQTrEVxkGht+qzU4MmSLXSomQrdiSNKokVVtt16tyKoJWBW9TdNQ== + dependencies: + activitystreams-context ">=3.0.0" + vocabs ">=0.11.2" + +vocabs-asx@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-asx/-/vocabs-asx-0.11.1.tgz#6667e4e174dc4556722b6cb1b9619fb16491519a" + integrity sha1-Zmfk4XTcRVZyK2yxuWGfsWSRUZo= + dependencies: + vocabs ">=0.11.1" + +vocabs-interval@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-interval/-/vocabs-interval-0.11.1.tgz#1c009421f3e88a307aafbb75bfa670ff0f4f6d3c" + integrity sha1-HACUIfPoijB6r7t1v6Zw/w9PbTw= + dependencies: + vocabs ">=0.11.1" + +vocabs-ldp@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vocabs-ldp/-/vocabs-ldp-0.1.0.tgz#da1728df560471750dfc7050e7e2df1bab901ce6" + integrity sha1-2hco31YEcXUN/HBQ5+LfG6uQHOY= + dependencies: + vocabs ">=0.11.1" + +vocabs-owl@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-owl/-/vocabs-owl-0.11.1.tgz#2355bbd27bfc19c5992d98079bbab3d7d65459e9" + integrity sha1-I1W70nv8GcWZLZgHm7qz19ZUWek= + dependencies: + vocabs ">=0.11.1" + +vocabs-rdf@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-rdf/-/vocabs-rdf-0.11.1.tgz#c7fa91d83b050ffb7b98ce2c72ab25c6fbcd1194" + integrity sha1-x/qR2DsFD/t7mM4scqslxvvNEZQ= + dependencies: + vocabs ">=0.11.1" + +vocabs-rdfs@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-rdfs/-/vocabs-rdfs-0.11.1.tgz#2e2df56ae0de008585b21057570386018da455bf" + integrity sha1-Li31auDeAIWFshBXVwOGAY2kVb8= + dependencies: + vocabs ">=0.11.1" + +vocabs-social@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-social/-/vocabs-social-0.11.1.tgz#d28545868cce325ba0c88e394f3de6e03fad85b1" + integrity sha1-0oVFhozOMlugyI45Tz3m4D+thbE= + dependencies: + vocabs ">=0.11.1" + +vocabs-xsd@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/vocabs-xsd/-/vocabs-xsd-0.11.1.tgz#20e201d8fd0fd330d6650d9061fda60baae6cd6c" + integrity sha1-IOIB2P0P0zDWZQ2QYf2mC6rmzWw= + dependencies: + vocabs ">=0.11.1" + +vocabs@>=0.11.1, vocabs@>=0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/vocabs/-/vocabs-0.11.2.tgz#8944b40f11d415f07db6e259804024a1dbfaa4d4" + integrity sha512-OIon2MWA21ZO42UBsTa5DuMsk5zv72DxMdQNvLsPN1M9GrjVTovn3LgWUZdPVnKBpdWhqWV7Mfbq/Sh0vkHIBw== + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" @@ -7432,6 +8127,11 @@ ws@^6.0.0: dependencies: async-limiter "~1.0.0" +x-xss-protection@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7" + integrity sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg== + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" @@ -7455,6 +8155,11 @@ xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xmldom@0.1.19: + version "0.1.19" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" + integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= + xtend@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -7521,6 +8226,14 @@ zen-observable-ts@^0.8.17: tslib "^1.9.3" zen-observable "^0.8.0" +zen-observable-ts@^0.8.18: + version "0.8.18" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8" + integrity sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable@^0.8.0: version "0.8.11" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199"