From dc0b6b1e64e8b6f219fdc2feb4b161c911b19880 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sat, 8 Dec 2018 19:36:51 -0600 Subject: [PATCH] feat: context is entity with services, replace migrations remove a bunch of service locator/direct wiring of service dependencies in favor of context BREAKING CHANGE: database must be reset and migrations run again --- docs/listener/discord-listener.yml | 5 +- docs/listener/express-listener.yml | 7 +- docs/listener/slack-listener.yml | 6 +- docs/sessions.md | 30 +++++ docs/style.md | 12 ++ package.json | 4 + src/BaseService.ts | 4 +- src/Bot.ts | 12 +- src/controller/AuthController.ts | 97 +++----------- src/controller/CountController.ts | 6 +- src/controller/SedController.ts | 6 +- src/entity/Command.ts | 5 +- src/entity/Context.ts | 103 +++++++++------ src/entity/Message.ts | 4 +- src/entity/auth/Role.ts | 4 +- src/entity/auth/Session.ts | 72 ---------- src/entity/auth/Token.ts | 31 ++++- src/entity/auth/User.ts | 9 +- src/filter/UserFilter.ts | 4 +- src/listener/BaseListener.ts | 12 +- src/listener/DiscordListener.ts | 107 +++++++-------- src/listener/ExpressListener.ts | 123 ++++++++++++------ src/listener/Listener.ts | 15 ++- src/listener/SessionListener.ts | 47 +++++++ src/listener/SlackListener.ts | 23 ++-- src/migration/0001526853117-InitialSetup.ts | 102 --------------- src/migration/0001529018132-CounterRoom.ts | 15 --- src/migration/0001542414714-MessageType.ts | 15 --- src/migration/0001543704185-AddAuth.ts | 62 --------- src/migration/0001544311178-CreateContext.ts | 29 +++++ src/migration/0001544311565-CreateCommand.ts | 37 ++++++ src/migration/0001544311687-CreateMessage.ts | 37 ++++++ src/migration/0001544311784-CreateKeyword.ts | 24 ++++ ...nter.ts => 0001544311799-CreateCounter.ts} | 5 +- ...ent.ts => 0001544311954-CreateFragment.ts} | 2 +- src/migration/0001544312069-CreateRole.ts | 25 ++++ src/migration/0001544312112-CreateUser.ts | 25 ++++ src/migration/0001544317462-CreateToken.ts | 39 ++++++ src/module/EntityModule.ts | 2 - src/module/MigrationModule.ts | 28 ++-- src/parser/LexParser.ts | 2 +- src/schema.gql | 33 ++--- src/utils/SessionProvider.ts | 6 - src/utils/TemplateCompiler.ts | 2 +- test/filter/TestUserFilter.ts | 2 +- yarn.lock | 116 +++++++++++++++++ 46 files changed, 787 insertions(+), 569 deletions(-) create mode 100644 docs/sessions.md delete mode 100644 src/entity/auth/Session.ts create mode 100644 src/listener/SessionListener.ts delete mode 100644 src/migration/0001526853117-InitialSetup.ts delete mode 100644 src/migration/0001529018132-CounterRoom.ts delete mode 100644 src/migration/0001542414714-MessageType.ts delete mode 100644 src/migration/0001543704185-AddAuth.ts create mode 100644 src/migration/0001544311178-CreateContext.ts create mode 100644 src/migration/0001544311565-CreateCommand.ts create mode 100644 src/migration/0001544311687-CreateMessage.ts create mode 100644 src/migration/0001544311784-CreateKeyword.ts rename src/migration/{0001527939908-AddCounter.ts => 0001544311799-CreateCounter.ts} (79%) rename src/migration/{0001543794891-AddFragment.ts => 0001544311954-CreateFragment.ts} (91%) create mode 100644 src/migration/0001544312069-CreateRole.ts create mode 100644 src/migration/0001544312112-CreateUser.ts create mode 100644 src/migration/0001544317462-CreateToken.ts delete mode 100644 src/utils/SessionProvider.ts diff --git a/docs/listener/discord-listener.yml b/docs/listener/discord-listener.yml index 1aa895abc..567ba7db2 100644 --- a/docs/listener/discord-listener.yml +++ b/docs/listener/discord-listener.yml @@ -6,7 +6,6 @@ data: game: name: Global Thermonuclear Warfare sessionProvider: - metadata: - kind: auth-controller - name: default-auth + kind: auth-controller + name: default-auth token: !env ISOLEX_DISCORD_TOKEN \ No newline at end of file diff --git a/docs/listener/express-listener.yml b/docs/listener/express-listener.yml index 122258cc4..d1fa93a37 100644 --- a/docs/listener/express-listener.yml +++ b/docs/listener/express-listener.yml @@ -8,4 +8,9 @@ data: metrics: true listen: port: 4000 - address: "0.0.0.0" \ No newline at end of file + address: "0.0.0.0" + token: + audience: test + issuer: test + scheme: isolex + secret: test-secret-foo \ No newline at end of file diff --git a/docs/listener/slack-listener.yml b/docs/listener/slack-listener.yml index 7d0b747c3..500d3871f 100644 --- a/docs/listener/slack-listener.yml +++ b/docs/listener/slack-listener.yml @@ -2,8 +2,10 @@ metadata: kind: slack-listener name: slack-isolex data: - token: !env ISOLEX_SLACK_TOKEN presence: game: name: Global Thermonuclear Warfare - + sessionProvider: + kind: auth-controller + name: default-auth + token: !env ISOLEX_SLACK_TOKEN \ No newline at end of file diff --git a/docs/sessions.md b/docs/sessions.md new file mode 100644 index 000000000..479f5c901 --- /dev/null +++ b/docs/sessions.md @@ -0,0 +1,30 @@ +# Sessions + +The authentication classes in isolex supports JWT-based RBAC and sessions. + +While authentication for chat and HTTP is fundamentally the same, the protocols have radically different ways +of tracking users. Most chat applications, for example, have already authenticated a user and established a session +of their own. HTTP has no such session, until provided by a cookie. + +The authentication controller calls the underlying whatever to create a session. This includes the listener from the +context. That means context should not be saved in the database, which makes sense. + +The session whatever notifies the listener that a session has been established between a user (based on the context +passed) and the user fetched. + +TOKENS HAVE NOTHING TO DO WITH SESSIONS +TOKENS PROVIDE THE INITIAL LOOKUP TO ASSOCIATE A USER WITH A LISTENER +THAT IS A SESSION + +What is context? + +Context is: + +- source listener (service) +- target ? (always starts equal to source, can change when message becomes command or for completion) +- optional user (entity, loaded) +- session data (flash) + +## Tokens + +Tokens are the only \ No newline at end of file diff --git a/docs/style.md b/docs/style.md index 6c774cf41..636a1569f 100644 --- a/docs/style.md +++ b/docs/style.md @@ -12,9 +12,11 @@ This document covers Typescript and YAML style, explains some lint rules, and ma - [Paths](#paths) - [Typescript](#typescript) - [Destructuring](#destructuring) + - [Entities](#entities) - [Exports](#exports) - [Imports](#imports) - [Order](#order) + - [Properties](#properties) - [Tests](#tests) - [Async](#async) - [Assertions](#assertions) @@ -61,6 +63,8 @@ Messages are sent. ## Typescript +Dictionary objects (`{...}`) must always be treated as immutable. + ### Destructuring Destructuring is great, use it! Groups should be `{ spaced, out }` like imports (lint will warn about this, code can @@ -70,6 +74,10 @@ Never nest destructuring. Defaults are ok. Prefer destructuring with default over `||`. For example, `const { foo = 3 } = bar;` over `const foo = bar.foo || 3;`. +### Entities + +Always provide the table name as an exported constant and use it in `@Entity(TABLE_FOO)` and the migrations. + ### Exports Never use default exports. @@ -92,6 +100,10 @@ Always `import { by, name }`, unless using a broken old library that required `i Ensure imports are sorted alphabetically, even within a single line. Your editor should be able to do this for you, because it is extremely tedious to do by hand. +### Properties + +Object properties should not be nullable or optional unless absolutely needed. Prefer sensible defaults. + ### Tests Typescript tests (small, unit tests) are run using Mocha and Chai. diff --git a/package.json b/package.json index 983b5f23f..5d71eece7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@types/mathjs": "~4.4", "@types/mocha": "~5.2", "@types/node-emoji": "~1.8", + "@types/passport": "^0.4.7", + "@types/passport-jwt": "^3.0.1", "@types/request": "~2.48", "@types/request-promise": "~4.1", "@types/request-promise-native": "~1.0", @@ -63,6 +65,8 @@ "node-emoji": "~1.8", "noicejs": "~2.3.3", "nyc": "~13.1", + "passport": "^0.4.0", + "passport-jwt": "^4.0.0", "prom-client": "^11.2.0", "raw-loader": "^0.5.1", "reflect-metadata": "^0.1.0", diff --git a/src/BaseService.ts b/src/BaseService.ts index 4ae5707b0..05d771fa4 100644 --- a/src/BaseService.ts +++ b/src/BaseService.ts @@ -1,7 +1,9 @@ +import { kebabCase } from 'lodash'; import { Logger } from 'noicejs/logger/Logger'; import * as uuid from 'uuid/v4'; import { Service, ServiceOptions } from 'src/Service'; + import { dictToMap } from './utils/Map'; export abstract class BaseService implements Service { @@ -27,7 +29,7 @@ export abstract class BaseService implements Service { } this.logger = options.logger.child({ - class: Reflect.getPrototypeOf(this).constructor.name, + kind: kebabCase(Reflect.getPrototypeOf(this).constructor.name), service: options.metadata.name, }); } diff --git a/src/Bot.ts b/src/Bot.ts index 62622f4e1..188bc4bdc 100644 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -1,5 +1,5 @@ import { bindAll } from 'lodash'; -import { Container, Inject } from 'noicejs'; +import { Container, Inject, MissingValueError } from 'noicejs'; import { BaseOptions } from 'noicejs/Container'; import { Logger, LogLevel } from 'noicejs/logger/Logger'; import { collectDefaultMetrics, Counter, Registry } from 'prom-client'; @@ -135,6 +135,10 @@ export class Bot extends BaseService implements Service { public async receive(msg: Message) { this.logger.debug({ msg }, 'received incoming message'); + if (!msg.context.name || !msg.context.uid) { + throw new MissingValueError('msg context name and uid required'); + } + if (!await this.checkFilters(msg)) { this.logger.warn({ msg }, 'dropped incoming message due to filters'); return; @@ -145,10 +149,9 @@ export class Bot extends BaseService implements Service { try { if (await parser.match(msg)) { matched = true; + this.logger.debug({ msg, parser: parser.name }, 'parsing message'); const commands = await parser.parse(msg); - for (const cmd of commands) { - this.commands.next(cmd); - } + this.emitCommand(...commands); } } catch (err) { this.logger.error(err, 'error running parser'); @@ -187,6 +190,7 @@ export class Bot extends BaseService implements Service { const results = []; for (const data of messages) { const msg = await this.storage.getRepository(Message).save(data); + this.logger.debug({ msg }, 'message saved'); this.outgoing.next(msg); results.push(msg); } diff --git a/src/controller/AuthController.ts b/src/controller/AuthController.ts index 7a54d6d72..b5ecfb7a0 100644 --- a/src/controller/AuthController.ts +++ b/src/controller/AuthController.ts @@ -3,13 +3,12 @@ import { Inject } from 'noicejs'; import { Connection, Repository } from 'typeorm'; import { Role } from 'src/entity/auth/Role'; -import { Session } from 'src/entity/auth/Session'; +import { Token } from 'src/entity/auth/Token'; import { User } from 'src/entity/auth/User'; import { Command, CommandVerb } from 'src/entity/Command'; -import { Context, ContextData } from 'src/entity/Context'; +import { Context } from 'src/entity/Context'; import { Message } from 'src/entity/Message'; import { TYPE_JSON, TYPE_TEXT } from 'src/utils/Mime'; -import { SessionProvider } from 'src/utils/SessionProvider'; import { BaseController } from './BaseController'; import { Controller, ControllerData, ControllerOptions } from './Controller'; @@ -21,10 +20,10 @@ export type AuthControllerData = ControllerData; export type AuthControllerOptions = ControllerOptions; @Inject('bot', 'storage') -export class AuthController extends BaseController implements Controller, SessionProvider { +export class AuthController extends BaseController implements Controller { protected storage: Connection; protected roleRepository: Repository; - protected sessionRepository: Repository; + protected tokenRepository: Repository; protected userRepository: Repository; constructor(options: AuthControllerOptions) { @@ -35,7 +34,7 @@ export class AuthController extends BaseController implement this.storage = options.storage; this.roleRepository = this.storage.getRepository(Role); - this.sessionRepository = this.storage.getRepository(Session); + this.tokenRepository = this.storage.getRepository(Token); this.userRepository = this.storage.getRepository(User); } @@ -73,16 +72,10 @@ export class AuthController extends BaseController implement } public async createUser(cmd: Command): Promise { - if (cmd.context.session) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot create users while logged in')); - return; - } - - const name = cmd.getHeadOrDefault('name', cmd.context.userName); - const roles = cmd.get('roles'); + const name = cmd.getHeadOrDefault('name', cmd.context.name); const user = await this.userRepository.save(this.userRepository.create({ name, - roles, + roles: [], })); this.logger.debug({ user }, 'created user'); @@ -91,35 +84,19 @@ export class AuthController extends BaseController implement } public async createSession(cmd: Command): Promise { - if (cmd.context.session) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot create sessions while logged in')); - return; - } - - const sessionKey = AuthController.getSessionKey(cmd.context); - const existingSession = await this.sessionRepository.findOne(sessionKey); - if (existingSession) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session already exists')); - return; - } - - const userName = cmd.getHeadOrDefault('name', cmd.context.userName); + const userName = cmd.getHeadOrDefault('name', cmd.context.name); const user = await this.userRepository.findOne({ name: userName, }); if (isNil(user)) { - this.logger.warn({ sessionKey, userName }, 'user not found for new session'); + this.logger.warn({ userName }, 'user not found for new session'); await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'user not found')); return; } this.logger.debug({ user }, 'logging in user'); - - const session = await this.sessionRepository.save(this.sessionRepository.create({ - ...AuthController.getSessionKey(cmd.context), - user, - })); + const session = await cmd.context.source.createSession(cmd.context.uid, user); this.logger.debug({ session, user, userName }, 'created session'); await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'created session')); @@ -127,34 +104,20 @@ export class AuthController extends BaseController implement } public async getUser(cmd: Command): Promise { - if (!cmd.context.session) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get users unless logged in')); - return; - } - - const session = await this.sessionRepository.findOne({ - id: cmd.context.session.id, - }); - if (isNil(session)) { + const { token } = cmd.context; + if (isNil(token)) { await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session does not exist')); return; } - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_JSON, session.user.toString())); + await this.bot.sendMessage(Message.reply(cmd.context, TYPE_JSON, token.user.name)); return; } public async getSession(cmd: Command): Promise { - if (!cmd.context.session) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get sessions unless logged in')); - return; - } - - const session = await this.sessionRepository.findOne({ - id: cmd.context.session.id, - }); + const session = cmd.context.source.getSession(cmd.context.uid); if (isNil(session)) { - await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'session does not exist')); + await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, 'cannot get sessions unless logged in')); return; } @@ -167,34 +130,4 @@ export class AuthController extends BaseController implement public async checkPermissions(ctx: Context, perms: Array): Promise { return false; } - - /** - * Attach session information to the provided context. - */ - public async createSessionContext(data: ContextData): Promise { - this.logger.debug({ data }, 'decorating context with session'); - - const sessionKey = AuthController.getSessionKey(data); - const session = await this.sessionRepository.findOne(sessionKey); - - if (isNil(session)) { - this.logger.debug({ data }, 'no session for context'); - return new Context(data); - } - - const context = new Context({ - ...data, - session, - }); - this.logger.debug({ context, session }, 'found session for context'); - - return context; - } - - protected static getSessionKey(ctx: ContextData) { - return { - listenerId: ctx.listenerId, - userName: ctx.userId, - }; - } } diff --git a/src/controller/CountController.ts b/src/controller/CountController.ts index 45371acf9..29f92858c 100644 --- a/src/controller/CountController.ts +++ b/src/controller/CountController.ts @@ -45,14 +45,14 @@ export class CountController extends BaseController impleme public async handle(cmd: Command): Promise { const count = cmd.getHeadOrDefault(this.data.field.count, this.data.default.count); - const name = cmd.getHeadOrDefault(this.data.field.name, cmd.context.threadId); + const name = cmd.getHeadOrDefault(this.data.field.name, cmd.context.channel.thread); this.logger.debug({ count, counterName: name }, 'finding counter'); - const counter = await this.findOrCreateCounter(name, cmd.context.roomId); + const counter = await this.findOrCreateCounter(name, cmd.context.channel.id); switch (count) { case 'ls': - const body = await this.listCounters(cmd.context.roomId); + const body = await this.listCounters(cmd.context.channel.id); await this.bot.sendMessage(Message.reply(cmd.context, TYPE_TEXT, body)); break; case '++': diff --git a/src/controller/SedController.ts b/src/controller/SedController.ts index f48d65895..da4254488 100644 --- a/src/controller/SedController.ts +++ b/src/controller/SedController.ts @@ -33,8 +33,8 @@ export class SedController extends BaseController implements let messages: Array = []; try { messages = await this.bot.fetch({ - channel: cmd.context.roomId, - listenerId: cmd.context.listenerId, + channel: cmd.context.channel.id, + listenerId: cmd.context.source.id, useFilters: true, }); } catch (error) { @@ -52,7 +52,7 @@ export class SedController extends BaseController implements } private async processMessage(message: Message, command: Command, parts: RegExpMatchArray): Promise { - if (message.context.threadId === command.context.threadId) { + if (message.context.channel.thread === command.context.channel.thread) { return false; } diff --git a/src/entity/Command.ts b/src/entity/Command.ts index e0f203833..952a5067e 100644 --- a/src/entity/Command.ts +++ b/src/entity/Command.ts @@ -20,8 +20,9 @@ export interface CommandOptions extends BaseCommandOptions { } export type CommandDataValue = Array; +export const TABLE_COMMAND = 'command'; -@Entity() +@Entity(TABLE_COMMAND) export class Command extends BaseCommand implements CommandOptions { /** * @TODO: merge emit data and passed data @@ -65,7 +66,7 @@ export class Command extends BaseCommand implements CommandOptions { const cmd = new Command(this); if (options.context) { - cmd.context = new Context(options.context); + cmd.context = options.context; } if (options.data) { cmd.data = mergeMap(cmd.data, dictToMap(options.data)); diff --git a/src/entity/Context.ts b/src/entity/Context.ts index e367651d1..79fb7c370 100644 --- a/src/entity/Context.ts +++ b/src/entity/Context.ts @@ -1,71 +1,100 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { MissingValueError } from 'noicejs'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; -import { Session } from './auth/Session'; +import { Listener } from 'src/listener/Listener'; +import { Parser } from 'src/parser/Parser'; + +import { Token } from './auth/Token'; +import { User } from './auth/User'; + +export interface ChannelData { + id: string; + thread: string; +} export interface ContextData { - listenerId: string; - roomId: string; - session?: Session; - threadId: string; - userId: string; - userName: string; + channel: ChannelData; + + /** + * User's display name. + */ + name: string; + + source: Listener; + + token?: Token; + + /** + * User authenticated with this context. + */ + user?: User; + + /** + * Unique ID for this user, only meaningful to/within the listener. + */ + uid: string; } -@Entity() +export const TABLE_CONTEXT = 'context'; + +@Entity(TABLE_CONTEXT) export class Context implements ContextData { + @Column('simple-json') + public channel: ChannelData; + @PrimaryGeneratedColumn('uuid') public id: string; @Column() - public listenerId: string; + public name: string; - @Column() - public roomId: string; + public parser?: Parser; - @ManyToOne((type) => Session, (session) => session.id, { - cascade: true, - nullable: true, - }) - public session?: Session; + public source: Listener; - @Column() - public threadId: string; + public target?: Listener; - @Column() - public userId: string; + public token?: Token; @Column() - public userName: string; + public uid: string; + + public user?: User; constructor(options?: ContextData) { if (options) { - this.listenerId = options.listenerId; - this.roomId = options.roomId; - this.session = options.session; - this.threadId = options.threadId; - this.userId = options.userId; - this.userName = options.userName; + if (!options.name || !options.uid) { + throw new MissingValueError('name and uid must be specified in context options'); + } + this.channel = { + id: options.channel.id, + thread: options.channel.thread, + }; + this.name = options.name; + this.source = options.source; + this.token = options.token; + this.uid = options.uid; + this.user = options.user; } } public extend(options: Partial): Context { const ctx = new Context(this); - if (options.session) { - ctx.session = new Session(options.session); + if (options.token) { + ctx.token = options.token; + } + if (options.user) { + ctx.user = options.user; } return ctx; } + /** + * @TODO: meaningful return value + */ public toJSON(): any { - const session = this.session ? this.session.toJSON() : {}; return { id: this.id, - listenerId: this.listenerId, - roomId: this.roomId, - session, - threadId: this.threadId, - userId: this.userId, - userName: this.userName, }; } } diff --git a/src/entity/Message.ts b/src/entity/Message.ts index 282512ee7..8a91c5b62 100644 --- a/src/entity/Message.ts +++ b/src/entity/Message.ts @@ -12,7 +12,9 @@ export interface MessageOptions { type: string; } -@Entity() +export const TABLE_MESSAGE = 'message'; + +@Entity(TABLE_MESSAGE) export class Message extends LabelEntity implements MessageOptions { public static isMessage(it: any): it is Message { return it instanceof Message; diff --git a/src/entity/auth/Role.ts b/src/entity/auth/Role.ts index 34741d6dd..8c1e8b5ed 100644 --- a/src/entity/auth/Role.ts +++ b/src/entity/auth/Role.ts @@ -10,7 +10,9 @@ export class Role implements RoleOptions { @PrimaryGeneratedColumn('uuid') public id: string; - @Column() + @Column({ + unique: true, + }) public name: string; @Column('simple-array') diff --git a/src/entity/auth/Session.ts b/src/entity/auth/Session.ts deleted file mode 100644 index f588bea0d..000000000 --- a/src/entity/auth/Session.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { AfterLoad, BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; - -import { BaseEntity } from 'src/entity/base/BaseEntity'; - -import { User } from './User'; - -export interface SessionData { - listenerId: string; - user: User; - userName: string; -} - -@Entity() -export class Session extends BaseEntity { - public data: Map>; - - @PrimaryGeneratedColumn('uuid') - public id: string; - - @Column() - public listenerId: string; - - @ManyToOne((type) => User, (user) => user.id, { - cascade: true, - }) - public user: User; - - /** - * The user ID (typically from context) which the listener name uses to associate sessions. - * - * This is a listener-defined value and may be meaningless. - */ - @Column() - public userName: string; - - @Column({ - name: 'data', - }) - protected dataStr: string; - - constructor(options?: SessionData) { - super(); - - this.data = new Map(); - - if (options) { - this.listenerId = options.listenerId; - this.user = options.user; - this.userName = options.userName; - } - } - - @AfterLoad() - public syncMap() { - this.data = new Map(JSON.parse(this.dataStr)); - } - - @BeforeInsert() - @BeforeUpdate() - public syncStr() { - this.dataStr = JSON.stringify(Array.from(this.data)); - } - - public toJSON(): object { - return { - id: this.id, - listenerId: this.listenerId, - // user: this.user.toJSON(), - userName: this.userName, - }; - } -} diff --git a/src/entity/auth/Token.ts b/src/entity/auth/Token.ts index 4f1b38064..c1092206d 100644 --- a/src/entity/auth/Token.ts +++ b/src/entity/auth/Token.ts @@ -1,6 +1,11 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -export interface TokenOptions { +import { Session } from 'src/listener/SessionListener'; + +import { DataEntity, DataEntityOptions } from '../base/DataEntity'; +import { User } from './User'; + +export interface TokenOptions extends Session, DataEntityOptions> { audience: Array; createdAt: number; expiresAt: number; @@ -9,7 +14,7 @@ export interface TokenOptions { } @Entity() -export class Token implements TokenOptions { +export class Token extends DataEntity> implements TokenOptions { /** * `aud` (Audience) claim * https://tools.ietf.org/html/rfc7519#section-4.1.3 @@ -42,6 +47,8 @@ export class Token implements TokenOptions { /** * `iss` (Issuer) claim * https://tools.ietf.org/html/rfc7519#section-4.1.1 + * + * listener identifier */ @Column() public issuer: string; @@ -49,11 +56,23 @@ export class Token implements TokenOptions { /** * `sub` (Subject) claim * https://tools.ietf.org/html/rfc7519#section-4.1.2 + * + * userName */ @Column() public subject: string; + @ManyToOne((type) => User, (user) => user.id, { + cascade: true, + }) + @JoinColumn({ + name: 'subject', + }) + public user: User; + constructor(options?: TokenOptions) { + super(options); + if (options) { this.audience = options.audience; this.createdAt = options.createdAt; @@ -62,4 +81,10 @@ export class Token implements TokenOptions { this.subject = options.subject; } } + + public toJSON(): object { + return { + id: this.id, + }; + } } diff --git a/src/entity/auth/User.ts b/src/entity/auth/User.ts index fbd510ee9..838c3f90f 100644 --- a/src/entity/auth/User.ts +++ b/src/entity/auth/User.ts @@ -1,10 +1,11 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { BaseEntity } from 'src/entity/base/BaseEntity'; +import { Role } from './Role'; export interface UserOptions { name: string; - roles: Array; + roles: Array; } @Entity() @@ -12,11 +13,13 @@ export class User extends BaseEntity implements UserOptions { @PrimaryGeneratedColumn('uuid') public id: string; - @Column() + @Column({ + unique: true, + }) public name: string; @Column('simple-array') - public roles: Array; + public roles: Array; constructor(options?: UserOptions) { super(); diff --git a/src/filter/UserFilter.ts b/src/filter/UserFilter.ts index a31806467..c05438c74 100644 --- a/src/filter/UserFilter.ts +++ b/src/filter/UserFilter.ts @@ -19,12 +19,12 @@ export class UserFilter extends BaseFilter implements Filter { public async check(value: FilterValue): Promise { const context = value.context; - if (!this.list.check(context.userId)) { + if (!this.list.check(context.uid)) { this.logger.debug({ context }, 'filter ignoring user id'); return FilterBehavior.Drop; } - if (!this.list.check(context.userName)) { + if (!this.list.check(context.name)) { this.logger.debug({ context }, 'filter ignoring user name'); return FilterBehavior.Drop; } diff --git a/src/listener/BaseListener.ts b/src/listener/BaseListener.ts index 43ff96079..87b194abb 100644 --- a/src/listener/BaseListener.ts +++ b/src/listener/BaseListener.ts @@ -1,8 +1,11 @@ import { ChildService } from 'src/ChildService'; +import { User } from 'src/entity/auth/User'; import { Context } from 'src/entity/Context'; import { Message } from 'src/entity/Message'; import { FetchOptions, Listener } from 'src/listener/Listener'; +import { Session } from './SessionListener'; + export abstract class BaseListener extends ChildService implements Listener { /** * Check if this listener can receive messages from this context. @@ -10,18 +13,17 @@ export abstract class BaseListener extends ChildService implements * Defaults to checking that the context came from this very same listener, by id. */ public async check(context: Context): Promise { - return context.listenerId === this.id; + return context.source.id === this.id; } public abstract send(msg: Message): Promise; public abstract fetch(options: FetchOptions): Promise>; - public async receive(value: Message) { - return this.bot.receive(value); - } - public abstract start(): Promise; public abstract stop(): Promise; + + public abstract createSession(uid: string, user: User): Promise; + public abstract getSession(uid: string): Promise; } diff --git a/src/listener/DiscordListener.ts b/src/listener/DiscordListener.ts index d4d29e3be..d6e0f86a8 100644 --- a/src/listener/DiscordListener.ts +++ b/src/listener/DiscordListener.ts @@ -15,24 +15,25 @@ import { Inject } from 'noicejs'; import { Counter } from 'prom-client'; import { ChildServiceOptions } from 'src/ChildService'; +import { Context, ContextData } from 'src/entity/Context'; import { Message } from 'src/entity/Message'; -import { BaseListener } from 'src/listener/BaseListener'; import { FetchOptions, Listener } from 'src/listener/Listener'; import { ServiceModule } from 'src/module/ServiceModule'; -import { ServiceDefinition } from 'src/Service'; +import { ServiceMetadata } from 'src/Service'; import { TYPE_TEXT } from 'src/utils/Mime'; -import { SessionProvider } from 'src/utils/SessionProvider'; + +import { SessionListener } from './SessionListener'; export interface DiscordListenerData { presence?: PresenceData; - sessionProvider: ServiceDefinition; + sessionProvider: ServiceMetadata; token: string; } export type DiscordListenerOptions = ChildServiceOptions; @Inject('bot', 'metrics', 'services') -export class DiscordListener extends BaseListener implements Listener { +export class DiscordListener extends SessionListener implements Listener { public static isTextChannel(chan: Channel | undefined): chan is TextChannel { return !isNil(chan) && chan.type === 'text'; } @@ -43,8 +44,6 @@ export class DiscordListener extends BaseListener implement protected readonly onCounter: Counter; - protected sessionProvider: SessionProvider; - constructor(options: DiscordListenerOptions) { super(options); @@ -61,71 +60,39 @@ export class DiscordListener extends BaseListener implement } public async start() { - this.sessionProvider = await this.services.createService(this.data.sessionProvider); - this.client.on('ready', () => { - this.onCounter.inc({ - eventKind: 'ready', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); + this.countEvent('ready'); this.logger.debug('discord listener ready'); }); this.client.on('message', (msg) => { - this.onCounter.inc({ - eventKind: 'message', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); + this.countEvent('message'); this.threads.set(msg.id, msg); - this.convertMessage(msg).then((it) => this.receive(it)).catch((err) => { + this.convertMessage(msg).then((it) => this.bot.receive(it)).catch((err) => { this.logger.error(err, 'error receiving message'); }); }); this.client.on('messageReactionAdd', (msgReaction, user) => { - this.onCounter.inc({ - eventKind: 'messageReactionAdd', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); - this.convertReaction(msgReaction, user).then((msg) => this.receive(msg)).catch((err) => { + this.countEvent('messageReactionAdd'); + this.convertReaction(msgReaction, user).then((msg) => this.bot.receive(msg)).catch((err) => { this.logger.error(err, 'error receiving reaction'); }); }); this.client.on('debug', (msg) => { - this.onCounter.inc({ - eventKind: 'debug', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); + this.countEvent('debug'); this.logger.debug({ upstream: msg }, 'debug from server'); }); this.client.on('error', (err) => { - this.onCounter.inc({ - eventKind: 'error', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); + this.countEvent('error'); this.logger.error(err, 'error from server'); }); this.client.on('warn', (msg) => { - this.onCounter.inc({ - eventKind: 'warn', - serviceId: this.id, - serviceKind: this.kind, - serviceName: this.name, - }); + this.countEvent('warn'); this.logger.warn({ upstream: msg }, 'warn from server'); }); @@ -143,12 +110,12 @@ export class DiscordListener extends BaseListener implement public async send(msg: Message): Promise { // direct reply to message - if (msg.context.threadId) { + if (msg.context.channel.thread) { return this.replyToThread(msg); } // broad reply to channel - if (msg.context.roomId) { + if (msg.context.channel.id) { return this.replyToChannel(msg); } @@ -157,7 +124,7 @@ export class DiscordListener extends BaseListener implement } public async replyToThread(msg: Message) { - const thread = this.threads.get(msg.context.threadId); + const thread = this.threads.get(msg.context.channel.thread); if (!thread) { this.logger.warn({ msg }, 'message thread is missing'); return; @@ -177,7 +144,7 @@ export class DiscordListener extends BaseListener implement } public async replyToChannel(msg: Message) { - const channel = this.client.channels.get(msg.context.roomId); + const channel = this.client.channels.get(msg.context.channel.id); if (!channel) { this.logger.warn({ msg }, 'message channel is missing'); return; @@ -231,13 +198,35 @@ export class DiscordListener extends BaseListener implement return Promise.all(messages); } + protected countEvent(eventKind: string) { + this.onCounter.inc({ + eventKind, + serviceId: this.id, + serviceKind: this.kind, + serviceName: this.name, + }); + } + protected async convertMessage(msg: DiscordMessage): Promise { - const context = await this.sessionProvider.createSessionContext({ - listenerId: this.id, - roomId: msg.channel.id, - threadId: msg.id, - userId: msg.author.id, - userName: msg.author.username, + this.logger.debug('converting discord message'); + const contextData: ContextData = { + channel: { + id: msg.channel.id, + thread: msg.id, + }, + name: msg.author.username, + source: this, + uid: msg.author.id, + }; + + const session = await this.getSession(msg.author.id); + if (session) { + contextData.user = session.user; + } + + const context = new Context({ + ...contextData, + source: this, }); return new Message({ body: msg.content, @@ -256,8 +245,8 @@ export class DiscordListener extends BaseListener implement msg.body = reaction.emoji.name; } - msg.context.userId = user.id; - msg.context.userName = user.username; + msg.context.uid = user.id; + msg.context.name = user.username; return msg; } diff --git a/src/listener/ExpressListener.ts b/src/listener/ExpressListener.ts index 13c637a3d..5cbe43f79 100644 --- a/src/listener/ExpressListener.ts +++ b/src/listener/ExpressListener.ts @@ -2,20 +2,24 @@ import * as express from 'express'; import * as expressGraphQl from 'express-graphql'; import { buildSchema } from 'graphql'; import * as http from 'http'; +import { isNil } from 'lodash'; import { Inject } from 'noicejs'; +import * as passport from 'passport'; +import { ExtractJwt, Strategy as JwtStrategy, VerifiedCallback } from 'passport-jwt'; import { Counter, Registry } from 'prom-client'; -import { Connection } from 'typeorm'; +import { Connection, Repository } from 'typeorm'; import { ChildServiceOptions } from 'src/ChildService'; +import { Token } from 'src/entity/auth/Token'; +import { User } from 'src/entity/auth/User'; import { Command } from 'src/entity/Command'; -import { Context } from 'src/entity/Context'; +import { Context, ContextData } from 'src/entity/Context'; import { Message } from 'src/entity/Message'; -import { NotImplementedError } from 'src/error/NotImplementedError'; import { ServiceModule } from 'src/module/ServiceModule'; import { pairsToDict } from 'src/utils/Map'; -import { BaseListener } from './BaseListener'; import { Listener } from './Listener'; +import { Session, SessionListener } from './SessionListener'; const schema = buildSchema(require('../schema.gql')); @@ -29,19 +33,26 @@ export interface ExpressListenerData { address: string; port: number; }; + token: { + audience: string; + issuer: string; + scheme: string; + secret: string; + }; } export type ExpressListenerOptions = ChildServiceOptions; @Inject('bot', 'metrics', 'services', 'storage') -export class ExpressListener extends BaseListener implements Listener { +export class ExpressListener extends SessionListener implements Listener { + protected readonly app: express.Express; + protected readonly authenticator: passport.Authenticator; protected readonly metrics: Registry; + protected readonly requestCounter: Counter; protected readonly services: ServiceModule; protected readonly storage: Connection; + protected readonly tokenRepository: Repository; - protected requestCounter: Counter; - - protected app: express.Express; protected server?: http.Server; constructor(options: ExpressListenerOptions) { @@ -51,7 +62,25 @@ export class ExpressListener extends BaseListener implement this.services = options.services; this.storage = options.storage; + this.requestCounter = new Counter({ + help: 'all requests through this express listener', + labelNames: ['serviceId', 'serviceKind', 'serviceName', 'requestClient', 'requestHost', 'requestPath'], + name: 'express_requests', + registers: [this.metrics], + }); + + this.tokenRepository = this.storage.getRepository(Token); + + this.authenticator = new passport.Passport(); + this.authenticator.use(new JwtStrategy({ + audience: this.data.token.audience, + issuer: this.data.token.issuer, + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme(this.data.token.scheme), + secretOrKey: this.data.token.secret, + }, (req: express.Request, payload: any, done: VerifiedCallback) => this.createTokenSession(req, payload, done))); + this.app = express(); + this.app.use(this.authenticator.initialize()); if (this.data.expose.metrics) { this.app.use((req, res, next) => this.traceRequest(req, res, next)); @@ -63,8 +92,8 @@ export class ExpressListener extends BaseListener implement graphiql: this.data.expose.graphiql, rootValue: { // mutation - emitCommands: (args: any) => this.emitCommands(args), - sendMessages: (args: any) => this.sendMessages(args), + emitCommands: (args: any, req: express.Request) => this.emitCommands(args, req), + sendMessages: (args: any, req: express.Request) => this.sendMessages(args, req), // query command: (args: any) => this.getCommand(args), message: (args: any) => this.getCommand(args), @@ -83,13 +112,6 @@ export class ExpressListener extends BaseListener implement res(server); }); }); - - this.requestCounter = new Counter({ - help: 'all requests through this express listener', - labelNames: ['serviceId', 'serviceKind', 'serviceName', 'requestClient', 'requestHost', 'requestPath'], - name: 'express_requests', - registers: [this.metrics], - }); } public async stop() { @@ -107,32 +129,36 @@ export class ExpressListener extends BaseListener implement return []; } - public emitCommands(args: any) { + public async emitCommands(args: any, req: express.Request) { this.logger.debug({ args }, 'emit command'); - const commands = args.commands.map((data: any) => { - const { context = {}, labels: rawLabels, noun, verb } = data; - return new Command({ - context: this.createContext(context), + const commands = []; + for (const data of args.commands) { + const { context: contextData = {}, labels: rawLabels, noun, verb } = data; + const context = await this.createContext(req, contextData); + commands.push(new Command({ + context, data: args, labels: pairsToDict(rawLabels), noun, verb, - }); - }); + })); + } return this.bot.emitCommand(...commands); } - public sendMessages(args: any) { + public async sendMessages(args: any, req: express.Request) { this.logger.debug({ args }, 'send message'); - const messages = args.messages.map((data: any) => { - const { body, context = {}, type } = data; - return new Message({ + const messages = []; + for (const data of args.messages) { + const { body, context: contextData = {}, type } = data; + const context = await this.createContext(req, contextData); + messages.push(new Message({ body, - context: this.createContext(context), + context, reactions: [], type, - }); - }); + })); + } return this.bot.sendMessage(...messages); } @@ -171,7 +197,7 @@ export class ExpressListener extends BaseListener implement res.end(this.metrics.metrics()); } - public traceRequest(req: express.Request, res: express.Response, next: Function) { + public async traceRequest(req: express.Request, res: express.Response, next: Function) { this.logger.debug({ req, res }, 'handling request'); this.requestCounter.inc({ requestClient: req.ip, @@ -184,13 +210,34 @@ export class ExpressListener extends BaseListener implement next(); } - protected createContext(args: any): Context { + protected async createTokenSession(req: express.Request, data: any, done: VerifiedCallback) { + const token = await this.tokenRepository.findOne(data); + if (isNil(token)) { + return done(undefined, false); + } + + const session = await this.getOrCreateSession(token.user); + done(undefined, session); + } + + protected async getOrCreateSession(user: User): Promise { + const session = await this.getSession(user.id); + if (isNil(session)) { + return this.createSession(user.id, user); + } else { + return session; + } + } + + protected async createContext(req: express.Request, data: ContextData): Promise { + const session = req.user as Session | undefined; + const user = session ? session.user : undefined; + + this.logger.debug({ data, req, session }, 'creating context for request'); return new Context({ - listenerId: this.id, - roomId: args.roomId || '', - threadId: args.threadId || '', - userId: args.userId || '', - userName: args.userName || '', + ...data, + source: this, + user, }); } } diff --git a/src/listener/Listener.ts b/src/listener/Listener.ts index 776c080d3..b3b44d28f 100644 --- a/src/listener/Listener.ts +++ b/src/listener/Listener.ts @@ -1,7 +1,11 @@ +import { ChildServiceOptions } from 'src/ChildService'; +import { User } from 'src/entity/auth/User'; import { Context } from 'src/entity/Context'; import { Message } from 'src/entity/Message'; import { Service } from 'src/Service'; +import { Session } from './SessionListener'; + export interface FetchOptions { after?: boolean; before?: boolean; @@ -10,6 +14,8 @@ export interface FetchOptions { id?: string; } +export type ListenerOptions = ChildServiceOptions; + export interface ContextFetchOptions extends FetchOptions { listenerId: string; useFilters: boolean; @@ -29,7 +35,12 @@ export interface Listener extends Service { fetch(options: FetchOptions): Promise>; /** - * Receive an incoming event and pass it on to the bot. + * Callback from the auth controller to associate a particular uid with a user, thus establishing a session. + */ + createSession(uid: string, user: User): Promise; + + /** + * Callback from the auth controller to get a session from a uid. */ - receive(msg: Message): Promise; + getSession(uid: string): Promise } diff --git a/src/listener/SessionListener.ts b/src/listener/SessionListener.ts new file mode 100644 index 000000000..9a8b31617 --- /dev/null +++ b/src/listener/SessionListener.ts @@ -0,0 +1,47 @@ +import { ChildServiceOptions } from 'src/ChildService'; +import { User } from 'src/entity/auth/User'; +import { Message } from 'src/entity/Message'; + +import { BaseListener } from './BaseListener'; +import { FetchOptions } from './Listener'; + +export interface Session { + createdAt: number; + expiresAt: number; + user: User; +} + +/** + * A listener that tracks sessions. + */ + +export abstract class SessionListener extends BaseListener { + protected sessions: Map; + + constructor(options: ChildServiceOptions) { + super(options); + this.sessions = new Map(); + } + + public abstract send(msg: Message): Promise; + + public abstract fetch(options: FetchOptions): Promise>; + + public abstract start(): Promise; + + public abstract stop(): Promise; + + public async createSession(uid: string, user: User): Promise { + const session = { + createdAt: Date.now(), + expiresAt: Date.now(), + user, + }; + this.sessions.set(uid, session); + return session; + } + + public async getSession(uid: string): Promise { + return this.sessions.get(uid); + } +} diff --git a/src/listener/SlackListener.ts b/src/listener/SlackListener.ts index a0a16927b..b4f7ec303 100644 --- a/src/listener/SlackListener.ts +++ b/src/listener/SlackListener.ts @@ -10,6 +10,7 @@ import { TYPE_TEXT } from 'src/utils/Mime'; import { BaseListener } from './BaseListener'; import { Listener } from './Listener'; +import { SessionListener } from './SessionListener'; export interface SlackListenerData { token: string; @@ -17,7 +18,7 @@ export interface SlackListenerData { export type SlackListenerOptions = ChildServiceOptions; -export class SlackListener extends BaseListener implements Listener { +export class SlackListener extends SessionListener implements Listener { protected client: RTMClient; protected webClient: WebClient; @@ -26,8 +27,8 @@ export class SlackListener extends BaseListener implements Li } public async send(msg: Message): Promise { - if (msg.context.roomId) { - const result = await this.client.sendMessage(msg.body, msg.context.roomId); + if (msg.context.channel.id) { + const result = await this.client.sendMessage(msg.body, msg.context.channel.id); if (result.error) { const err = new BaseError(result.error.msg); this.logger.error(err, 'error sending slack message'); @@ -50,11 +51,11 @@ export class SlackListener extends BaseListener implements Li }); this.client.on('message', (msg) => { - this.convertMessage(msg).then((it) => this.receive(it)).catch((err) => this.logger.error(err, 'error receiving message')); + this.convertMessage(msg).then((it) => this.bot.receive(it)).catch((err) => this.logger.error(err, 'error receiving message')); }); this.client.on('reaction_added', (reaction) => { - this.convertReaction(reaction).then((msg) => this.receive(msg)).catch((err) => this.logger.error(err, 'error adding reaction')); + this.convertReaction(reaction).then((msg) => this.bot.receive(msg)).catch((err) => this.logger.error(err, 'error adding reaction')); }); await this.client.start(); @@ -87,11 +88,13 @@ export class SlackListener extends BaseListener implements Li const {type, channel, user, text, ts} = msg; this.logger.debug({ channel, text, ts, type, user }, 'converting slack message'); const context = new Context({ - listenerId: this.id, - roomId: channel, - threadId: '', - userId: user, - userName: user, + channel: { + id: channel, + thread: '', + }, + name: user, + source: this, + uid: user, }); return new Message({ body: text, diff --git a/src/migration/0001526853117-InitialSetup.ts b/src/migration/0001526853117-InitialSetup.ts deleted file mode 100644 index 5c304f650..000000000 --- a/src/migration/0001526853117-InitialSetup.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; - -export class InitialSetup0001526853117 implements MigrationInterface { - public async up(query: QueryRunner): Promise { - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'listenerId', - type: 'varchar', - }, { - name: 'roomId', - type: 'varchar', - }, { - isNullable: true, - name: 'sessionId', - type: 'varchar', - }, { - name: 'threadId', - type: 'varchar', - }, { - name: 'userId', - type: 'varchar', - }, { - name: 'userName', - type: 'varchar', - }], - name: 'context', - })); - - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'contextId', - type: 'varchar', - }, { - name: 'data', - type: 'varchar', - }, { - isNullable: true, - name: 'labels', - type: 'varchar', - }, { - name: 'noun', - type: 'varchar', - }, { - name: 'verb', - type: 'varchar', - }], - name: 'command', - })); - - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'body', - type: 'varchar', - }, { - name: 'contextId', - type: 'varchar', - }, { - isNullable: true, - name: 'labels', - type: 'varchar', - }, { - name: 'reactions', - type: 'varchar', - }], - name: 'message', - })); - - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'name', - type: 'varchar', - }, { - name: 'commandId', - type: 'varchar', - }, { - name: 'controller', - type: 'varchar', - }], - name: 'keyword', - })); - } - - public async down(query: QueryRunner): Promise { - await query.dropTable('context'); - await query.dropTable('command'); - await query.dropTable('message'); - await query.dropTable('keyword'); - } -} diff --git a/src/migration/0001529018132-CounterRoom.ts b/src/migration/0001529018132-CounterRoom.ts deleted file mode 100644 index 236b058aa..000000000 --- a/src/migration/0001529018132-CounterRoom.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -export class CounterRoom0001529018132 implements MigrationInterface { - public async up(query: QueryRunner): Promise { - await query.addColumn('counter', new TableColumn({ - isNullable: true, - name: 'roomId', - type: 'varchar', - })); - } - - public async down(query: QueryRunner): Promise { - await query.dropColumn('counter', 'roomId'); - } -} diff --git a/src/migration/0001542414714-MessageType.ts b/src/migration/0001542414714-MessageType.ts deleted file mode 100644 index 081daabbc..000000000 --- a/src/migration/0001542414714-MessageType.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -export class MessageType0001542414714 implements MigrationInterface { - public async up(query: QueryRunner): Promise { - await query.addColumn('message', new TableColumn({ - isNullable: false, - name: 'type', - type: 'varchar', - })); - } - - public async down(query: QueryRunner): Promise { - await query.dropColumn('message', 'type'); - } -} diff --git a/src/migration/0001543704185-AddAuth.ts b/src/migration/0001543704185-AddAuth.ts deleted file mode 100644 index 809d098e8..000000000 --- a/src/migration/0001543704185-AddAuth.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; - -export class AddAuth0001543704185 implements MigrationInterface { - public async up(query: QueryRunner): Promise { - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'name', - type: 'varchar', - }, { - name: 'grants', - type: 'varchar', - }], - name: 'role', - })); - - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'data', - type: 'varchar', - }, { - name: 'listenerId', - type: 'varchar', - }, { - name: 'userId', - type: 'varchar', - }, { - name: 'userName', - type: 'varchar', - }], - name: 'session', - })); - - await query.createTable(new Table({ - columns: [{ - isPrimary: true, - name: 'id', - type: 'varchar', - }, { - name: 'name', - type: 'varchar', - }, { - name: 'roles', - type: 'varchar', - }], - name: 'user', - })); - } - - public async down(query: QueryRunner): Promise { - await query.dropTable('role'); - await query.dropTable('session'); - await query.dropTable('user'); - } -} diff --git a/src/migration/0001544311178-CreateContext.ts b/src/migration/0001544311178-CreateContext.ts new file mode 100644 index 000000000..5c58b1858 --- /dev/null +++ b/src/migration/0001544311178-CreateContext.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +import { TABLE_CONTEXT } from 'src/entity/Context'; + +export class CreateContext0001544311178 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + name: 'channel', + type: 'varchar', + }, { + name: 'name', + type: 'varchar', + }, { + name: 'uid', + type: 'varchar', + }], + name: TABLE_CONTEXT, + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable(TABLE_CONTEXT); + } +} diff --git a/src/migration/0001544311565-CreateCommand.ts b/src/migration/0001544311565-CreateCommand.ts new file mode 100644 index 000000000..dcc7660b7 --- /dev/null +++ b/src/migration/0001544311565-CreateCommand.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +import { TABLE_COMMAND } from 'src/entity/Command'; + +export class CreateCommand0001544311565 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + name: 'contextId', + type: 'varchar', + }, { + name: 'data', + type: 'varchar', + }, { + isNullable: true, + name: 'labels', + type: 'varchar', + }, { + name: 'noun', + type: 'varchar', + }, { + name: 'verb', + type: 'varchar', + }], + name: TABLE_COMMAND, + })); + + } + + public async down(query: QueryRunner): Promise { + await query.dropTable(TABLE_COMMAND); + } +} diff --git a/src/migration/0001544311687-CreateMessage.ts b/src/migration/0001544311687-CreateMessage.ts new file mode 100644 index 000000000..a9642aa3b --- /dev/null +++ b/src/migration/0001544311687-CreateMessage.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +import { TABLE_MESSAGE } from 'src/entity/Message'; + +export class CreateMessage0001544311687 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + name: 'body', + type: 'varchar', + }, { + name: 'contextId', + type: 'varchar', + }, { + isNullable: true, + name: 'labels', + type: 'varchar', + }, { + name: 'reactions', + type: 'varchar', + }, { + isNullable: false, + name: 'type', + type: 'varchar', + }], + name: TABLE_MESSAGE, + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable(TABLE_MESSAGE); + } +} diff --git a/src/migration/0001544311784-CreateKeyword.ts b/src/migration/0001544311784-CreateKeyword.ts new file mode 100644 index 000000000..513bbd1ac --- /dev/null +++ b/src/migration/0001544311784-CreateKeyword.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateKeyword0001544311784 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'name', + type: 'varchar', + }, { + name: 'commandId', + type: 'varchar', + }, { + name: 'controller', + type: 'varchar', + }], + name: 'keyword', + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable('keyword'); + } +} diff --git a/src/migration/0001527939908-AddCounter.ts b/src/migration/0001544311799-CreateCounter.ts similarity index 79% rename from src/migration/0001527939908-AddCounter.ts rename to src/migration/0001544311799-CreateCounter.ts index 6a7f9033c..1509a9cea 100644 --- a/src/migration/0001527939908-AddCounter.ts +++ b/src/migration/0001544311799-CreateCounter.ts @@ -1,9 +1,12 @@ import { MigrationInterface, QueryRunner, Table } from 'typeorm'; -export class AddCounter0001527939908 implements MigrationInterface { +export class CreateCounter0001544311799 implements MigrationInterface { public async up(query: QueryRunner): Promise { await query.createTable(new Table({ columns: [{ + name: 'channel', + type: 'varchar', + }, { isPrimary: true, name: 'id', type: 'varchar', diff --git a/src/migration/0001543794891-AddFragment.ts b/src/migration/0001544311954-CreateFragment.ts similarity index 91% rename from src/migration/0001543794891-AddFragment.ts rename to src/migration/0001544311954-CreateFragment.ts index 4c228911c..ddf8d3733 100644 --- a/src/migration/0001543794891-AddFragment.ts +++ b/src/migration/0001544311954-CreateFragment.ts @@ -1,6 +1,6 @@ import { MigrationInterface, QueryRunner, Table } from 'typeorm'; -export class AddFragment0001543794891 implements MigrationInterface { +export class CreateFragment0001544311954 implements MigrationInterface { public async up(query: QueryRunner): Promise { await query.createTable(new Table({ columns: [{ diff --git a/src/migration/0001544312069-CreateRole.ts b/src/migration/0001544312069-CreateRole.ts new file mode 100644 index 000000000..99dd9b4e6 --- /dev/null +++ b/src/migration/0001544312069-CreateRole.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateRole0001544312069 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + isUnique: true, + name: 'name', + type: 'varchar', + }, { + name: 'grants', + type: 'varchar', + }], + name: 'role', + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable('role'); + } +} diff --git a/src/migration/0001544312112-CreateUser.ts b/src/migration/0001544312112-CreateUser.ts new file mode 100644 index 000000000..8728dc4b0 --- /dev/null +++ b/src/migration/0001544312112-CreateUser.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateUser0001544312112 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + isUnique: true, + name: 'name', + type: 'varchar', + }, { + name: 'roles', + type: 'varchar', + }], + name: 'user', + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable('user'); + } +} diff --git a/src/migration/0001544317462-CreateToken.ts b/src/migration/0001544317462-CreateToken.ts new file mode 100644 index 000000000..c699f2870 --- /dev/null +++ b/src/migration/0001544317462-CreateToken.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateToken0001544317462 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + name: 'audience', + type: 'varchar', + }, { + name: 'createdAt', + type: 'int', + }, { + name: 'data', + type: 'varchar', + }, { + name: 'expiresAt', + type: 'int', + }, { + name: 'issuer', + type: 'varchar', + }, { + name: 'labels', + type: 'varchar', + }, { + name: 'subject', + type: 'varchar', + }], + name: 'token', + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable('token'); + } +} diff --git a/src/module/EntityModule.ts b/src/module/EntityModule.ts index 52a64fbd9..6410ffd02 100644 --- a/src/module/EntityModule.ts +++ b/src/module/EntityModule.ts @@ -2,7 +2,6 @@ import { Module, Provides } from 'noicejs'; import { ModuleOptions } from 'noicejs/Module'; import { Role } from 'src/entity/auth/Role'; -import { Session } from 'src/entity/auth/Session'; import { Token } from 'src/entity/auth/Token'; import { User } from 'src/entity/auth/User'; import { Command } from 'src/entity/Command'; @@ -26,7 +25,6 @@ export class EntityModule extends Module { Message, /* auth */ Role, - Session, Token, User, /* misc */ diff --git a/src/module/MigrationModule.ts b/src/module/MigrationModule.ts index a87f68a68..53b66774a 100644 --- a/src/module/MigrationModule.ts +++ b/src/module/MigrationModule.ts @@ -1,24 +1,28 @@ import { Module } from 'noicejs'; import { ModuleOptions } from 'noicejs/Module'; -import { InitialSetup0001526853117 } from 'src/migration/0001526853117-InitialSetup'; -import { AddCounter0001527939908 } from 'src/migration/0001527939908-AddCounter'; -import { CounterRoom0001529018132 } from 'src/migration/0001529018132-CounterRoom'; -import { MessageType0001542414714 } from 'src/migration/0001542414714-MessageType'; -import { AddAuth0001543704185 } from 'src/migration/0001543704185-AddAuth'; -import { AddFragment0001543794891 } from 'src/migration/0001543794891-AddFragment'; +import { CreateContext0001544311178 } from 'src/migration/0001544311178-CreateContext'; +import { CreateCommand0001544311565 } from 'src/migration/0001544311565-CreateCommand'; +import { CreateMessage0001544311687 } from 'src/migration/0001544311687-CreateMessage'; +import { CreateKeyword0001544311784 } from 'src/migration/0001544311784-CreateKeyword'; +import { CreateCounter0001544311799 } from 'src/migration/0001544311799-CreateCounter'; +import { CreateRole0001544312069 } from 'src/migration/0001544312069-CreateRole'; +import { CreateUser0001544312112 } from 'src/migration/0001544312112-CreateUser'; +import { CreateToken0001544317462 } from 'src/migration/0001544317462-CreateToken'; export class MigrationModule extends Module { public async configure(options: ModuleOptions): Promise { await super.configure(options); this.bind('migrations').toInstance([ - InitialSetup0001526853117, - AddCounter0001527939908, - CounterRoom0001529018132, - MessageType0001542414714, - AddAuth0001543704185, - AddFragment0001543794891, + CreateContext0001544311178, + CreateCommand0001544311565, + CreateMessage0001544311687, + CreateKeyword0001544311784, + CreateCounter0001544311799, + CreateRole0001544312069, + CreateUser0001544312112, + CreateToken0001544317462, ]); } } diff --git a/src/parser/LexParser.ts b/src/parser/LexParser.ts index 00387dbfc..542f569a2 100644 --- a/src/parser/LexParser.ts +++ b/src/parser/LexParser.ts @@ -80,7 +80,7 @@ export class LexParser extends BaseParser implements Parser { botAlias: this.data.bot.alias, botName: this.data.bot.name, inputText: msg.body, - userId: leftPad(msg.context.userId), + userId: leftPad(msg.context.uid), }); this.logger.debug({ msg, post }, 'lex parsed message'); diff --git a/src/schema.gql b/src/schema.gql index cf2965c2a..0c092f377 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -20,11 +20,15 @@ input NameMultiValuePairInput { values: [String!]! } +input ChannelInput { + id: String! + thread: String +} + input ContextInput { - roomId: String - threadId: String - userId: String - userName: String + channel: ChannelInput! + name: String! + uid: String! } input CommandInput { @@ -57,7 +61,8 @@ type NameMultiValuePair { values: [String!]! } -type Group { +type Role { + grants: [String!]! id: String! name: String! } @@ -65,24 +70,20 @@ type Group { type User { id: String! name: String! - groups: [Group!] + roles: [Role!] } -type Session { +type Channel { id: String! - listenerId: String! - user: User - userName: String! + thread: String } type Context { + channel: Channel! id: String! - listenerId: String! - roomId: String! - session: Session - threadId: String! - userId: String! - userName: String! + name: String! + uid: String! + user: User } type Fragment implements Labeled { diff --git a/src/utils/SessionProvider.ts b/src/utils/SessionProvider.ts deleted file mode 100644 index 6b6bea924..000000000 --- a/src/utils/SessionProvider.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Context, ContextData } from 'src/entity/Context'; -import { Service } from 'src/Service'; - -export interface SessionProvider extends Service { - createSessionContext(ctx: ContextData): Promise; -} diff --git a/src/utils/TemplateCompiler.ts b/src/utils/TemplateCompiler.ts index 25dea8278..5e60f0285 100644 --- a/src/utils/TemplateCompiler.ts +++ b/src/utils/TemplateCompiler.ts @@ -37,7 +37,7 @@ export class TemplateCompiler { * @TODO: move this to each Listener */ public formatContext(context: Context): string { - return `@${context.userName}`; + return `@${context.name}`; } public formatEntries(map: Map, block: any): string { diff --git a/test/filter/TestUserFilter.ts b/test/filter/TestUserFilter.ts index 4b45c6f6e..2c19fdcba 100644 --- a/test/filter/TestUserFilter.ts +++ b/test/filter/TestUserFilter.ts @@ -71,7 +71,7 @@ describeAsync('user filter', async () => { const cmd = new Command({ context: ineeda({ - userName: 'test', + name: 'test', }), data: {}, labels: {}, diff --git a/yarn.lock b/yarn.lock index 080a065d2..88d7748d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -298,6 +298,12 @@ version "0.2.0" resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.0.tgz#13c62db22a34d9c411364fac79fd374d63445aa1" +"@types/jsonwebtoken@*": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#1fe79489df97b49273401ac3c8019cbf1dae4578" + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.110", "@types/lodash@~4.14": version "4.14.118" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.118.tgz#247bab39bfcc6d910d4927c6e06cbc70ec376f27" @@ -350,6 +356,27 @@ dependencies: "@types/retry" "*" +"@types/passport-jwt@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.1.tgz#bc4c2610815565de977ea1a580c047d71c646084" + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c" + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-0.4.7.tgz#2b7f29bf61df91cf77023b3777e940b613d4b4d8" + dependencies: + "@types/express" "*" + "@types/range-parser@*": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d" @@ -1076,6 +1103,10 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +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" + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -1977,6 +2008,12 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3296,6 +3333,20 @@ jsonpath@^1.0.0, jsonpath@~1.0: static-eval "2.0.0" underscore "1.7.0" +jsonwebtoken@^8.2.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz#8757f7b4cb7440d86d5e2f3becefa70536c8e46a" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -3309,6 +3360,21 @@ just-extend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-3.0.0.tgz#cee004031eaabf6406da03a7b84e4fe9d78ef288" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3422,6 +3488,34 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.template@^4.0.2: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" @@ -4295,6 +4389,24 @@ pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" +passport-jwt@^4.0.0: + version "4.0.0" + resolved "http://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065" + dependencies: + jsonwebtoken "^8.2.0" + passport-strategy "^1.0.0" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + +passport@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.0.tgz#c5095691347bd5ad3b5e180238c3914d16f05811" + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" @@ -4359,6 +4471,10 @@ pathval@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"