diff --git a/.flowconfig b/.flowconfig index 099accb..fa998b3 100644 --- a/.flowconfig +++ b/.flowconfig @@ -10,4 +10,5 @@ [options] esproposal.optional_chaining=enable munge_underscores=true -emoji=true \ No newline at end of file +emoji=true +esproposal.export_star_as=enable \ No newline at end of file diff --git a/packages/roleypoly-bot/Bot.js b/packages/roleypoly-bot/Bot.js index ce76cbf..461e91b 100644 --- a/packages/roleypoly-bot/Bot.js +++ b/packages/roleypoly-bot/Bot.js @@ -1,6 +1,5 @@ // @flow -import Eris, { type Message, type TextChannel } from 'eris' -import logger from './logger.js' +import Eris, { type Message, type TextChannel, type Guild } from 'eris' import RPCClient from '@roleypoly/rpc-client' import randomEmoji from './random-emoji' import DMCommands from './commands/dm' @@ -8,6 +7,8 @@ import TextCommands from './commands/text' import RootCommands from './commands/root' import type { Command } from './commands/_types' import retry from 'async-retry' + +import logger from './logger' const log = logger(__filename) export type BotConfig = $Shape<{ @@ -228,8 +229,18 @@ export default class Bot { } } + async syncGuild (type: string, guild: Guild) { + await this.rpc.syncGuild(type, guild.id) + } + async start () { this.client.on('messageCreate', this.handleMessage) + + const guildSyncTriggers = ['guildCreate', 'guildDelete', 'guildRoleAdd', 'guildRoleDelete', 'guildRoleUpdate'] + for (const trigger of guildSyncTriggers) { + this.client.on(trigger, this.syncGuild.bind(this, trigger)) + } + await this.client.connect() log.info('started bot') } diff --git a/packages/roleypoly-bot/package.json b/packages/roleypoly-bot/package.json index 759e38c..328e6fe 100644 --- a/packages/roleypoly-bot/package.json +++ b/packages/roleypoly-bot/package.json @@ -7,7 +7,6 @@ "dev": "yarn build --watch" }, "dependencies": { - "@roleypoly/rpc-client": "2.0.0", "async-retry": "^1.2.3", "dotenv": "^7.0.0", "eris": "^0.9.0" diff --git a/packages/roleypoly-rpc-client/package.json b/packages/roleypoly-rpc-client/package.json index 67dd514..b47dbc3 100644 --- a/packages/roleypoly-rpc-client/package.json +++ b/packages/roleypoly-rpc-client/package.json @@ -7,7 +7,7 @@ "dev": "yarn build --watch", "postinstall": "test -d lib || npm run build" }, - "types": "src/", + "types": "src/index.js", "main": "lib/index.js", "files": ["lib"], "dependencies": { diff --git a/packages/roleypoly-rpc-client/src/index.js b/packages/roleypoly-rpc-client/src/index.js index 937189e..e942589 100644 --- a/packages/roleypoly-rpc-client/src/index.js +++ b/packages/roleypoly-rpc-client/src/index.js @@ -1,8 +1,10 @@ // @flow import superagent from 'superagent' -import RPCError from './error' +import RPCError from './error.js' import retry from 'async-retry' +export { RPCError } + export type RPCResponse = { response?: mixed, hash?: string, diff --git a/packages/roleypoly-rpc-server/.babelrc b/packages/roleypoly-rpc-server/.babelrc new file mode 100644 index 0000000..775d1a0 --- /dev/null +++ b/packages/roleypoly-rpc-server/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ ["@babel/preset-env", { "targets": {"node": "current"} }], "@babel/preset-flow" ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-export-namespace-from", + ["@babel/plugin-transform-runtime", + { "helpers": false }] + ] +} diff --git a/packages/roleypoly-rpc-server/package.json b/packages/roleypoly-rpc-server/package.json new file mode 100644 index 0000000..91b7cf8 --- /dev/null +++ b/packages/roleypoly-rpc-server/package.json @@ -0,0 +1,24 @@ +{ + "private": true, + "name": "@roleypoly/rpc-server", + "version": "2.0.0", + "scripts": { + "build": "babel -d ./lib ./src", + "dev": "yarn build --watch", + "postinstall": "test -d lib || npm run build" + }, + "types": "src/index.js", + "main": "lib/index.js", + "files": [ + "lib" + ], + "dependencies": { + "async-retry": "^1.2.3", + "glob": "^7.1.3", + "nats": "^1.2.10" + }, + "devDependencies": { + "@babel/cli": "^7.4.3", + "@babel/plugin-proposal-export-namespace-from": "^7.2.0" + } +} diff --git a/packages/roleypoly-server/rpc/_autoloader.js b/packages/roleypoly-rpc-server/src/autoloader.js similarity index 71% rename from packages/roleypoly-server/rpc/_autoloader.js rename to packages/roleypoly-rpc-server/src/autoloader.js index 717cc34..8c0c9f7 100644 --- a/packages/roleypoly-server/rpc/_autoloader.js +++ b/packages/roleypoly-rpc-server/src/autoloader.js @@ -1,22 +1,21 @@ // @flow -import logger from '../logger' +import logger from './logger' import glob from 'glob' import path from 'path' -import { type AppContext } from '../Roleypoly' const log = logger(__filename) const PROD: boolean = process.env.NODE_ENV === 'production' -export default (ctx: AppContext, forceClear: ?boolean = false): { +export default (globString: string, ctx: any, forceClear: ?boolean = false): { [rpc: string]: Function } => { let map = {} - const apis = glob.sync(`${__dirname}/**/!(index).js`).map(v => v.replace(__dirname, '.')) + const apis = glob.sync(globString) log.debug('found rpcs', apis) for (let a of apis) { const filename = path.basename(a) - const dirname = path.dirname(a) + // const dirname = path.dirname(a) const pathname = a delete require.cache[require.resolve(pathname)] @@ -27,11 +26,6 @@ export default (ctx: AppContext, forceClear: ?boolean = false): { continue } - if (dirname === 'client') { - log.debug(`skipping ${a}`) - continue - } - // testing only if (filename.endsWith('_test.js') && PROD) { log.debug(`skipping ${a}`) diff --git a/packages/roleypoly-server/rpc/index.js b/packages/roleypoly-rpc-server/src/index.js similarity index 93% rename from packages/roleypoly-server/rpc/index.js rename to packages/roleypoly-rpc-server/src/index.js index 25b2301..f72aa07 100644 --- a/packages/roleypoly-server/rpc/index.js +++ b/packages/roleypoly-rpc-server/src/index.js @@ -1,9 +1,10 @@ // @flow import fnv from 'fnv-plus' -import autoloader from './_autoloader' -import RPCError from '@roleypoly/rpc-client/error' -import type Roleypoly, { Router } from '../Roleypoly' +import autoloader from './autoloader' +import { RPCError } from '@roleypoly/rpc-client' +import type Roleypoly, { Router } from '@roleypoly/server/Roleypoly' import type { Context } from 'koa' +export * as secureAs from './security' // import logger from '../logger' // const log = logger(__filename) @@ -68,7 +69,7 @@ export default class RPCServer { } // check if RPC exists - const { fn, args } = (ctx.request.body: RPCIncoming) + const { fn, args } = ((ctx.request.body: any): RPCIncoming) if (!(fn in this.rpcMap)) { return this.rpcError(ctx, null, new RPCError(`RPC call ${fn}(...) not found.`, 404)) diff --git a/packages/roleypoly-rpc-server/src/logger.js b/packages/roleypoly-rpc-server/src/logger.js new file mode 100644 index 0000000..93b52b3 --- /dev/null +++ b/packages/roleypoly-rpc-server/src/logger.js @@ -0,0 +1,70 @@ +// @flow +import chalk from 'chalk' + +export class Logger { + debugOn: boolean + name: string + quietSql: boolean + + constructor (name: string, debugOverride: boolean = false) { + this.name = name + this.debugOn = (process.env.DEBUG === 'true' || process.env.DEBUG === '*') || debugOverride + this.quietSql = (process.env.DEBUG_SQL !== 'true') + } + + fatal (text: string, ...data: any) { + this.error(text, data) + + if (typeof data[data.length - 1] === 'number') { + process.exit(data[data.length - 1]) + } else { + process.exit(1) + } + } + + error (text: string, ...data: any) { + console.error(chalk.red.bold(`ERR ${this.name}:`) + `\n ${text}`, data) + } + + warn (text: string, ...data: any) { + console.warn(chalk.yellow.bold(`WARN ${this.name}:`) + `\n ${text}`, data) + } + + notice (text: string, ...data: any) { + console.log(chalk.cyan.bold(`NOTICE ${this.name}:`) + `\n ${text}`, data) + } + + info (text: string, ...data: any) { + console.info(chalk.blue.bold(`INFO ${this.name}:`) + `\n ${text}`, data) + } + + deprecated (text: string, ...data: any) { + console.warn(chalk.yellowBright(`DEPRECATED ${this.name}:`) + `\n ${text}`, data) + console.trace() + } + + request (text: string, ...data: any) { + console.info(chalk.green.bold(`HTTP ${this.name}:`) + `\n ${text}`) + } + + debug (text: string, ...data: any) { + if (this.debugOn) { + console.log(chalk.gray.bold(`DEBUG ${this.name}:`) + `\n ${text}`, data) + } + } + + rpc (call: string, ...data: any) { + console.log(chalk.redBright.bold`RPC` + chalk.redBright(` ${call}():`), data) + } + + sql (logger: Logger, ...data: any) { + if (logger.debugOn && !logger.quietSql) { + console.log(chalk.bold('DEBUG SQL:\n '), data) + } + } +} + +export default (pathname: string) => { + const name = pathname.replace(__dirname, '').replace('.js', '') + return new Logger(name) +} diff --git a/packages/roleypoly-server/rpc/_security.js b/packages/roleypoly-rpc-server/src/security.js similarity index 95% rename from packages/roleypoly-server/rpc/_security.js rename to packages/roleypoly-rpc-server/src/security.js index 01764c4..d6abd33 100644 --- a/packages/roleypoly-server/rpc/_security.js +++ b/packages/roleypoly-rpc-server/src/security.js @@ -1,9 +1,9 @@ // @flow -import { type AppContext } from '../Roleypoly' +import { type AppContext } from '@roleypoly/server/Roleypoly' import { type Context } from 'koa' -import RPCError from '@roleypoly/rpc-client/error' +import { RPCError } from '@roleypoly/rpc-client' -import logger from '../logger' +import logger from '../../roleypoly-server/logger' const log = logger(__filename) const PermissionError = new RPCError('User does not have permission to call this RPC.', 403) diff --git a/packages/roleypoly-server/Roleypoly.js b/packages/roleypoly-server/Roleypoly.js index 16d30b9..c420885 100644 --- a/packages/roleypoly-server/Roleypoly.js +++ b/packages/roleypoly-server/Roleypoly.js @@ -10,7 +10,7 @@ import DiscordService from './services/discord' import SessionService from './services/sessions' import AuthService from './services/auth' import PresentationService from './services/presentation' -import RPCServer from './rpc' +import RPCServer from '@roleypoly/rpc-server' import fetchModels, { type Models } from './models' import fetchApis from './api' import retry from 'async-retry' diff --git a/packages/roleypoly-server/package.json b/packages/roleypoly-server/package.json index adc5441..55d52e1 100644 --- a/packages/roleypoly-server/package.json +++ b/packages/roleypoly-server/package.json @@ -10,6 +10,7 @@ "dependencies": { "@discordjs/uws": "^11.149.1", "@roleypoly/types": "^2.0.0", + "@roleypoly/rpc-server": "^2.0.0", "@roleypoly/ui": "^2.0.0", "async-retry": "^1.2.3", "bufferutil": "^4.0.1", diff --git a/packages/roleypoly-server/rpc/servers.js b/packages/roleypoly-server/rpc/servers.js index 9f93325..bb71e76 100644 --- a/packages/roleypoly-server/rpc/servers.js +++ b/packages/roleypoly-server/rpc/servers.js @@ -2,18 +2,18 @@ import { type AppContext } from '../Roleypoly' import { type Context } from 'koa' import { type Guild } from 'eris' -import * as secureAs from './_security' -import RPCError from '@roleypoly/rpc-client/error' +import { secureAs } from '@roleypoly/rpc-server' +import { RPCError } from '@roleypoly/rpc-client' export default ($: AppContext) => ({ rootGetAllServers: secureAs.root($, (ctx: Context) => { - return $.discord.client.guilds.map<{ + return $.discord.guilds.valueSeq().map<{ url: string, name: string, members: number, roles: number - }>((g: Guild) => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.memberCount, roles: g.roles.size })) + }>((g: Guild) => ({ url: `${$.config.appUrl}/s/${g.id}`, name: g.name, members: g.members.size, roles: g.roles.size })).toJS() }), getServerSlug (ctx: Context, id: string) { @@ -40,5 +40,18 @@ export default ($: AppContext) => ({ } return $.P.presentableServer(srv, gm) + }), + + listOwnServers: secureAs.authed($, async (ctx: Context, id: string) => { + const { userId } = (ctx.session: { userId: string }) + const srv = $.discord.getRelevantServers(userId) + return $.P.presentableServers(srv, userId) + }), + + syncGuild: secureAs.bot($, async (ctx: Context, type: string, guildId: string) => { + const g = await $.discord.guild(guildId, true) + if (g != null && type === 'guildCreate') { + $.server.ensure(g) + } }) }) diff --git a/packages/roleypoly-server/services/discord.js b/packages/roleypoly-server/services/discord.js index e072443..2870cb4 100644 --- a/packages/roleypoly-server/services/discord.js +++ b/packages/roleypoly-server/services/discord.js @@ -4,7 +4,7 @@ import type { AppContext } from '../Roleypoly' import Eris, { type Member, Role, type Guild, type Permission as ErisPermission } from 'eris' import LRU from 'lru-cache' // $FlowFixMe -import { OrderedSet } from 'immutable' +import { OrderedSet, OrderedMap } from 'immutable' import superagent from 'superagent' import type { AuthTokens } from './auth' import type { IFetcher } from './discord/types' @@ -54,6 +54,8 @@ export type MemberExt = Member & { } export default class DiscordService extends Service { + static _guildExpiration = +(process.env.GUILD_INVALIDATION_TIME || 36e5) + ctx: AppContext client: Eris @@ -67,6 +69,8 @@ export default class DiscordService extends Service { fetcher: IFetcher + guilds: OrderedMap = OrderedMap() + _lastGuildFetch: number constructor (ctx: AppContext) { super(ctx) this.ctx = ctx @@ -88,14 +92,15 @@ export default class DiscordService extends Service { }) this.fetcher = new RestFetcher(this) + this.fetchGuilds(true) } isRoot (id: string): boolean { return this.cfg.rootUsers.has(id) } - getRelevantServers (user: string) { - return this.client.guilds.filter(guild => guild.members.has(user)) + getRelevantServers (user: string): OrderedSet { + return this.guilds.filter(guild => guild.members.has(user)) } async gm (serverId: string, userId: string, { canFake = false }: { canFake: boolean } = {}): Promise { @@ -280,6 +285,36 @@ export default class DiscordService extends Service { return `https://discordapp.com/oauth2/authorize?client_id=${this.cfg.clientId}&scope=bot&permissions=268435456` } + async fetchGuilds (force: boolean = false) { + if ( + force || + this.guilds.isEmpty() || + this._lastGuildFetch + DiscordService._guildExpiration < Date.now() + ) { + const g = await this.fetcher.getGuilds() + this.guilds = OrderedMap(g.reduce((acc, g) => ({ ...acc, [g.id]: g }), {})) + } + } + + async guild (id: string, invalidate: boolean = false): Promise { + // fetch if needed + await this.fetchGuilds() + + // do we know about it? + // (also don't get this if we're invalidating) + if (invalidate === false && this.guilds.has(id)) { + return this.guilds.get(id) + } + + // else let's fetch and cache. + const g = await this.fetcher.getGuild(id) + if (g != null) { + this.guilds = this.guilds.set(g.id, g) + } + + return g + } + async issueChallenge (author: string) { // Create a challenge const chall = await this.ctx.auth.createDMChallenge(author) diff --git a/packages/roleypoly-server/services/discord/restFetcher.js b/packages/roleypoly-server/services/discord/restFetcher.js index ffc3c7c..6a04994 100644 --- a/packages/roleypoly-server/services/discord/restFetcher.js +++ b/packages/roleypoly-server/services/discord/restFetcher.js @@ -4,6 +4,8 @@ import type DiscordSvc from '../discord' import type ErisClient, { User, Member, Guild } from 'eris' import LRU from 'lru-cache' import logger from '../../logger' +// $FlowFixMe +import { OrderedSet } from 'immutable' const log = logger(__filename) export default class BotFetcher implements IFetcher { @@ -71,4 +73,31 @@ export default class BotFetcher implements IFetcher { return null } } + + getGuilds = async (): Promise => { + const last: ?string = undefined + const limit: number = 100 + let out = OrderedSet() + + try { + while (true) { + // $FlowFixMe -- last is optional, but typedef isn't. + const gl = await this.client.getRESTGuilds(limit, last) + + out = out.union(gl) + if (gl.length !== limit) { + break + } + } + } catch (e) { + log.error('getAllGuilds failed', e) + throw e + } + + return out.toArray() + } + + invalidateGuild (id: string) { + this.cache.del(`G:${id}`) + } } diff --git a/packages/roleypoly-server/services/discord/types.js b/packages/roleypoly-server/services/discord/types.js index 431495e..294754a 100644 --- a/packages/roleypoly-server/services/discord/types.js +++ b/packages/roleypoly-server/services/discord/types.js @@ -11,4 +11,6 @@ export interface IFetcher { getGuild: (id: string) => Promise; getMember: (server: string, user: string) => Promise; + + getGuilds: () => Promise; } diff --git a/packages/roleypoly-server/services/presentation.js b/packages/roleypoly-server/services/presentation.js index e26fd26..cf63f31 100644 --- a/packages/roleypoly-server/services/presentation.js +++ b/packages/roleypoly-server/services/presentation.js @@ -5,6 +5,8 @@ import { type AppContext } from '../Roleypoly' import { type Models } from '../models' import { type ServerModel } from '../models/Server' import type DiscordService from './discord' +// $FlowFixMe +import type { Sequence } from 'immutable' import { type Guild, type Collection @@ -38,7 +40,7 @@ class PresentationService extends Service { } } - presentableServers (collection: Collection, userId: string) { + presentableServers (collection: Collection | Sequence, userId: string): Promise { return areduce(Array.from(collection.values()), async (acc, server) => { const gm = server.members.get(userId) if (gm == null) { diff --git a/packages/roleypoly-server/services/server.js b/packages/roleypoly-server/services/server.js index f12967f..6eb3e03 100644 --- a/packages/roleypoly-server/services/server.js +++ b/packages/roleypoly-server/services/server.js @@ -5,10 +5,10 @@ import type PresentationService from './presentation' import { type Guild } from 'eris' -import { type ServerModel, type Category } from '../models/Server' +import { type ServerModel, type Category } from '@roleypoly/types' export default class ServerService extends Service { - Server: any // Model but flowtype is bugged + Server: * // Model but flowtype is bugged P: PresentationService constructor (ctx: AppContext) { super(ctx) diff --git a/packages/roleypoly-server/rpc/_error.js b/packages/roleypoly-ui/components/header/server-selector.js similarity index 100% rename from packages/roleypoly-server/rpc/_error.js rename to packages/roleypoly-ui/components/header/server-selector.js diff --git a/packages/roleypoly-ui/stores/servers.js b/packages/roleypoly-ui/stores/servers.js index a1c1d20..7938a72 100644 --- a/packages/roleypoly-ui/stores/servers.js +++ b/packages/roleypoly-ui/stores/servers.js @@ -1,16 +1,29 @@ // @flow import { namespaceConfig } from 'fast-redux' +import RPC from '../config/rpc' // import { Map } from 'immutable' +import type { PresentableServer as Server } from '@roleypoly/types' -const DEFAULT_STATE = {} +export type ServersState = { + [id: string]: Server +} -export type ServersState = typeof DEFAULT_STATE +const DEFAULT_STATE: ServersState = {} export const { action, getState: getServerState } = namespaceConfig('servers', DEFAULT_STATE) -export const updateServers = action('updateServers', (state: ServersState, serverData) => ({ +export const updateServers = action('updateServers', (state: ServersState, serverData: Server[]) => ({ ...state, - servers: serverData + ...serverData.reduce((acc, s) => ({ ...acc, [s.id]: s }), {}) })) export const updateSingleServer = action('updateSingleServer', (state, data, server) => ({ ...state, [server]: data })) + +export const fetchServerList = (rpc?: typeof RPC) => async (dispatch: *) => { + if (rpc == null) { + rpc = RPC + } + + const servers: Server[] = await rpc.listOwnServers() + dispatch(updateServers(servers)) +} diff --git a/yarn.lock b/yarn.lock index e8b3cd3..7f15c76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -368,6 +368,14 @@ "@babel/helper-create-class-features-plugin" "^7.4.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-proposal-export-namespace-from@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.2.0.tgz#308fd4d04ff257fc3e4be090550840eeabad5dd9" + integrity sha512-DZUxbHYxQ5fUFIkMEnh75ogEdBLPfL+mQUqrO2hNY2LGm+tqFnxE924+mhAcCOh/8za8AaZsWHbq6bBoS3TAzA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-export-namespace-from" "^7.2.0" + "@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" @@ -453,6 +461,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-export-namespace-from@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz#8d257838c6b3b779db52c0224443459bd27fb039" + integrity sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-flow@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz#a765f061f803bc48f240c26f8747faf97c26bf7c" @@ -8173,6 +8188,14 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +nats@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/nats/-/nats-1.2.10.tgz#3f673e80d3a513f7802b0a5c1f227127a3d1d780" + integrity sha512-0FQMINZbyRkFMRbrpc6+IkKMQ+Zi2Ibr4YPhoEBlbP0Gw3ta23e/GB+LvXNqnV3htOPJNJ54+ToMI43BCYATGQ== + dependencies: + nuid "^1.0.0" + ts-nkeys "^1.0.8" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -8553,6 +8576,11 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" +nuid@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/nuid/-/nuid-1.1.0.tgz#e0c5746350a25562f58283197f34c50861f44675" + integrity sha512-C/JdZ6PtCqKsCEs4ni76nhBsdmuQgLAT/CTLNprkcLViDAnkk7qx5sSA8PVC2vmSsdBlSsFuGb52v6pwn1oaeg== + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -11600,6 +11628,13 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.3.tgz#e29bd1614c6458d44869fc28b255ab7857ef7c24" integrity sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw== +ts-nkeys@^1.0.8: + version "1.0.12" + resolved "https://registry.yarnpkg.com/ts-nkeys/-/ts-nkeys-1.0.12.tgz#cda47f4842fe2c4f88b1303817050673e16acb89" + integrity sha512-5TgA+wbfxTy/9pdSuAhvneuL65KKoI7phonzNQH2UhnorAQAWehUwHNLEuli596wu/Fxh0SAhMeKZVLNx4s7Ow== + dependencies: + tweetnacl "^1.0.1" + tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -11629,7 +11664,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -tweetnacl@^1.0.0: +tweetnacl@^1.0.0, tweetnacl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.1.tgz#2594d42da73cd036bd0d2a54683dd35a6b55ca17" integrity sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==