diff --git a/app/controllers/admin.js b/app/controllers/admin.js index 09b5de7..ec88690 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -55,6 +55,7 @@ class AdminController extends SiteController { router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post'))); router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings'))); router.use('/service-node', await this.loadChild(path.join(__dirname, 'admin', 'service-node'))); + router.use('/site-link', await this.loadChild(path.join(__dirname, 'admin', 'site-link'))); router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user'))); router.use('/venue', await this.loadChild(path.join(__dirname, 'admin', 'venue'))); diff --git a/app/controllers/admin/site-link.js b/app/controllers/admin/site-link.js new file mode 100644 index 0000000..0409256 --- /dev/null +++ b/app/controllers/admin/site-link.js @@ -0,0 +1,109 @@ +// admin/site-link.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const express = require('express'); + +const { SiteController, SiteError } = require('../../../lib/site-lib'); + +class SiteLinkAdminController extends SiteController { + + constructor (dtp) { + super(dtp, module.exports); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'venue'; + return next(); + }); + + router.param('linkId', this.populateLinkId.bind(this)); + + router.post('/:linkId', this.postUpdateLink.bind(this)); + router.post('/', this.postCreateLink.bind(this)); + + router.get('/create', this.getLinkEditor.bind(this)); + router.get('/:linkId', this.getLinkEditor.bind(this)); + + router.get('/', this.getHomeView.bind(this)); + + router.delete('/:linkId', this.deleteLink.bind(this)); + + return router; + } + + async populateLinkId (req, res, next, linkId) { + const { siteLink: siteLinkService } = this.dtp.services; + try { + res.locals.link = await siteLinkService.getById(linkId); + return next(); + } catch (error) { + this.log.error('failed to populate site link', { linkId, error }); + return next(error); + } + } + + async postUpdateLink (req, res, next) { + const { siteLink: siteLinkService } = this.dtp.services; + try { + await siteLinkService.update(res.locals.link, req.body); + res.redirect('/admin/site-link'); + } catch (error) { + this.log.error('failed to update site link', { error }); + return next(error); + } + } + + async postCreateLink (req, res, next) { + const { siteLink: siteLinkService } = this.dtp.services; + try { + await siteLinkService.create(req.body); + res.redirect('/admin/site-link'); + } catch (error) { + this.log.error('failed to create site link', { error }); + return next(error); + } + } + + async getLinkEditor (req, res) { + res.render('admin/site-link/editor'); + } + + async getHomeView (req, res, next) { + const { siteLink: siteLinkService } = this.dtp.services; + try { + res.locals.links = await siteLinkService.getLinks(); + res.render('admin/site-link/index'); + } catch (error) { + this.log.error('failed to present the Site Link Admin home view', { error }); + return next(error); + } + } + + async deleteLink (req, res) { + const { siteLink: siteLinkService } = this.dtp.services; + try { + const displayList = this.createDisplayList('delete-site-link'); + await siteLinkService.remove(res.locals.link); + displayList.navigateTo('/admin/site-link'); + res.status(200).json({ success: true, displayList }); + } catch (error) { + this.log.error('failed to delete site link', { error }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } +} + +module.exports = { + name: 'adminSiteLink', + slug: 'admin-site-link', + create: async (dtp) => { return new SiteLinkAdminController(dtp); }, +}; \ No newline at end of file diff --git a/app/controllers/admin/venue.js b/app/controllers/admin/venue.js index 89d8858..f742b2c 100644 --- a/app/controllers/admin/venue.js +++ b/app/controllers/admin/venue.js @@ -23,12 +23,14 @@ class VenueAdminController extends SiteController { }); router.param('channelId', this.populateChannelId.bind(this)); + router.param('channelSlug', this.populateChannelSlug.bind(this)); router.post('/channel/:channelId', this.postUpdateChannel.bind(this)); router.post('/channel', this.postCreateChannel.bind(this)); router.get('/channel/create', this.getChannelEditor.bind(this)); - router.get('/channel/:channelId', this.getChannelEditor.bind(this)); + router.get('/channel/:channelSlug', this.getChannelEditor.bind(this)); + router.get('/channel', this.getChannelHome.bind(this)); router.get('/', this.getHomeView.bind(this)); @@ -40,7 +42,7 @@ class VenueAdminController extends SiteController { async populateChannelId (req, res, next, channelId) { const { venue: venueService } = this.dtp.services; try { - res.locals.channel = await venueService.getChannelById(channelId); + res.locals.channel = await venueService.getChannelById(channelId, { withCredentials: true }); return next(); } catch (error) { this.log.error('failed to populate Venue channel', { channelId, error }); @@ -48,6 +50,17 @@ class VenueAdminController extends SiteController { } } + async populateChannelSlug (req, res, next, channelSlug) { + const { venue: venueService } = this.dtp.services; + try { + res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true }); + return next(); + } catch (error) { + this.log.error('failed to populate Venue channel by slug', { channelSlug, error }); + return next(error); + } + } + async postUpdateChannel (req, res, next) { const { venue: venueService } = this.dtp.services; try { @@ -62,11 +75,11 @@ class VenueAdminController extends SiteController { async postCreateChannel (req, res, next) { const { user: userService, venue: venueService } = this.dtp.services; try { - const owner = await userService.getUserAccount(req.body.ownerId); + const owner = await userService.lookup(req.body.owner); if (!owner) { throw new SiteError(400, 'Channel owner is empty or invalid'); } - await venueService.create(owner, req.body); + await venueService.createChannel(owner, req.body); res.redirect('/admin/venue/channel'); } catch (error) { this.log.error('failed to create Venue channel', { error }); @@ -78,6 +91,18 @@ class VenueAdminController extends SiteController { res.render('admin/venue/channel/editor'); } + async getChannelHome (req, res, next) { + const { venue: venueService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.channels = await venueService.getChannels(res.locals.pagination); + res.render('admin/venue/channel/index'); + } catch (error) { + this.log.error('failed to present the Venue Admin home view', { error }); + return next(error); + } + } + async getHomeView (req, res, next) { const { venue: venueService } = this.dtp.services; try { diff --git a/app/controllers/venue.js b/app/controllers/venue.js index 66980e1..5efcd74 100644 --- a/app/controllers/venue.js +++ b/app/controllers/venue.js @@ -5,7 +5,7 @@ 'use strict'; const express = require('express'); -const { SiteController } = require('../../lib/site-lib'); +const { SiteController, SiteError } = require('../../lib/site-lib'); class VenueController extends SiteController { @@ -25,18 +25,55 @@ class VenueController extends SiteController { return next(); }, router); + router.param('channelSlug', this.populateChannelSlug.bind(this)); + router.get( - '/', + '/:channelSlug', limiterService.createMiddleware(limiterService.config.venue.getVenueEmbed), this.getVenueEmbed.bind(this), ); + router.get( + '/', + this.getHome.bind(this), + ); + return router; } + async populateChannelSlug (req, res, next, channelSlug) { + const { venue: venueService } = this.dtp.services; + try { + res.locals.channel = await venueService.getChannelBySlug(channelSlug, { withCredentials: true }); + if (!res.locals.channel) { + throw new SiteError(404, `Channel "${channelSlug}" does not exists on ${this.dtp.config.site.name}. Please check the link/URL you used, and try again.`); + } + + res.locals.channelCredentials = res.locals.channel.credentials; + delete res.locals.channel.credentials; + + return next(); + } catch (error) { + this.log.error('failed to populate Venue channel by slug', { channelSlug, error }); + return next(error); + } + } + async getVenueEmbed (req, res) { res.render('venue/embed'); } + + async getHome (req, res, next) { + const { venue: venueService} = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 10); + res.locals.channels = await venueService.getChannels(res.locals.pagination); + res.render('venue/index'); + } catch (error) { + this.log.error('failed to present the Venue home', { error }); + return next(error); + } + } } module.exports = { diff --git a/app/models/site-link.js b/app/models/site-link.js new file mode 100644 index 0000000..c62cb84 --- /dev/null +++ b/app/models/site-link.js @@ -0,0 +1,19 @@ +// site-link.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const SiteLinkSchema = new Schema({ + parent: { type: Schema.ObjectId, index: 1 }, + label: { type: String, required: true }, + url: { type: String, required: true }, + iconUrl: { type: String, required: true }, + target: { type: String }, +}); + +module.exports = mongoose.model('SiteLink', SiteLinkSchema); \ No newline at end of file diff --git a/app/models/venue-channel.js b/app/models/venue-channel.js index 1a844f4..f259167 100644 --- a/app/models/venue-channel.js +++ b/app/models/venue-channel.js @@ -18,7 +18,7 @@ const VenueChannelSchema = new Schema({ owner: { type: Schema.ObjectId, required: true, index: 1, refPath: 'ownerType' }, slug: { type: String, lowercase: true, unique: true, index: 1 }, lastStatus: { type: Schema.ObjectId, ref: 'VenueChannelStatus' }, - credentials: { type: ChannelCredentialsSchema, select: false }, + credentials: { type: ChannelCredentialsSchema, required: true, select: false }, }); module.exports = mongoose.model('VenueChannel', VenueChannelSchema); \ No newline at end of file diff --git a/app/services/site-link.js b/app/services/site-link.js new file mode 100644 index 0000000..542fe6f --- /dev/null +++ b/app/services/site-link.js @@ -0,0 +1,89 @@ +// announcement.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const SiteLink = mongoose.model('SiteLink'); + +const { URL } = require('url'); // jshint ignore:line +const favicon = require('favicon'); + +const { SiteService } = require('../../lib/site-lib'); + +class SiteLinkService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + } + + middleware ( ) { + return async (req, res, next) => { + if (req.path.startsWith('/auth') || req.path.startsWith('/image') || req.path.startsWith('/manifest')) { + return next(); + } + res.locals.links = await this.getLinks(); + this.log.debug('site links', { count: res.locals.links.length, path: req.path }); + return next(); + }; + } + + async create (linkDefinition) { + const link = new SiteLink(); + link.label = linkDefinition.label; + link.url = linkDefinition.url; + link.iconUrl = await this.getSiteFavicon(linkDefinition.url); + + if (linkDefinition.targetBlank) { + link.target = '_blank'; + } + await link.save(); + return link.toObject(); + } + + async update (link, linkDefinition) { + const updateOp = { $set: { }, $unset: { } }; + + updateOp.$set.label = linkDefinition.label; + updateOp.$set.url = linkDefinition.url; + updateOp.$set.iconUrl = await this.getSiteFavicon(linkDefinition.url); + + if (linkDefinition.targetBlank) { + updateOp.$set.target = '_blank'; + } else { + updateOp.$unset.target = 1; + } + + return SiteLink.findOneAndUpdate({ _id: link._id }, updateOp, { new: true }); + } + + async getLinks ( ) { + return SiteLink.find().sort({ label: 1 }).lean(); + } + + async getById (linkId) { + return SiteLink.findOne({ _id: linkId }).lean(); + } + + async remove (link) { + await SiteLink.deleteOne({ _id: link._id }); + } + + async getSiteFavicon (url) { + return new Promise((resolve, reject) => { + favicon(url, (err, iconUrl) => { + if (err) { + return reject(err); + } + return resolve(iconUrl); + }); + }); + } +} + +module.exports = { + slug: 'site-link', + name: 'siteLink', + create: (dtp) => { return new SiteLinkService(dtp); }, +}; \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index 9dbed90..fd6faa1 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -357,6 +357,61 @@ class UserService extends SiteService { return user; } + async lookup (account, options) { + options = Object.assign({ withEmail: false, withCredentials: false }, options); + this.log.debug('locating user record', { account }); + + const selects = [ + '_id', 'created', + 'username', 'username_lc', + 'displayName', 'picture', + 'flags', 'permissions', + ]; + if (options.withEmail) { + selects.push('email'); + } + if (options.withCredentials) { + selects.push('passwordSalt'); + selects.push('password'); + } + + const usernameRegex = new RegExp(`^${account}.*`); + + /* + * First, check our local db + */ + let user = await User + .findOne({ + $or: [ + { email: account.email }, + { username_lc: usernameRegex }, + ] + }) + .select(selects.join(' ')) + .lean(); + + if (user) { + // found, mark as 'User' + user.type = 'User'; + } else { + // check for a matching CoreUser + user = await CoreUser + .findOne({ + $or: [ + { username_lc: usernameRegex }, + ] + }) + .select(selects.join(' ')) + .lean(); + if (user) { + // mark as CoreUser + user.type = 'CoreUser'; + } + } + + return user; // undefined means not found + } + registerPassportLocal ( ) { const options = { usernameField: 'username', diff --git a/app/services/venue.js b/app/services/venue.js index aec4b94..d449855 100644 --- a/app/services/venue.js +++ b/app/services/venue.js @@ -13,6 +13,7 @@ const https = require('https'); const fetch = require('node-fetch'); // jshint ignore:line const striptags = require('striptags'); +const slug = require('slug'); const CACHE_DURATION = 60 * 5; @@ -26,21 +27,32 @@ class VenueService extends SiteService { } async start ( ) { + const { user: userService } = this.dtp.services; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; this.httpsAgent = new https.Agent({ rejectUnauthorized: false, }); + + this.populateVenueChannel = [ + { + path: 'owner', + select: userService.USER_SELECT, + }, + { + path: 'lastStatus', + }, + ]; } channelMiddleware ( ) { return async (req, res, next) => { try { - if (!res.locals.site || !res.locals.site.shingChannelSlug) { - return next(); - } - res.locals.shingChannelFeed = await this.getChannelFeed(res.locals.site.shingChannelSlug, { allowCache: true }); - res.locals.shingChannelStatus = await this.getChannelStatus(res.locals.site.shingChannelSlug, { allowCache: true }); - this.log.debug('channel status', { status: res.locals.shingChannelStatus }); + res.locals.venue = res.locals.venue || { }; + res.locals.venue.channels = await VenueChannel + .find() + .populate(this.populateVenueChannel) + .lean(); return next(); } catch (error) { this.log.error('failed to populate Soapbox channel feed', { error }); @@ -51,25 +63,27 @@ class VenueService extends SiteService { async createChannel (owner, channelDefinition) { const channel = new VenueChannel(); + channel.ownerType = owner.type; channel.owner = owner._id; - if (!channelDefinition.slug || (channelDefinition.slug === '')) { - throw new SiteError(400, 'Must provide a channel URL slug'); - } - channel.slug = striptags(channelDefinition.slug.trim()); + channel.slug = this.getChannelSlug(channelDefinition.url); if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) { throw new SiteError(400, 'Must provide a stream key'); } - channel['credentials.streamKey'] = channelDefinition['credentials.streamKey'].trim(); if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) { throw new SiteError(400, 'Must provide a widget key'); } - channel['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim(); + + channel.credentials = { + streamKey: channelDefinition['credentials.streamKey'].trim(), + widgetKey: channelDefinition['credentials.widgetKey'].trim(), + }; await channel.save(); + await this.updateChannelStatus(channel); return channel.toObject(); } @@ -77,10 +91,7 @@ class VenueService extends SiteService { async updateChannel (channel, channelDefinition) { const updateOp = { $set: { } }; - if (!channelDefinition.slug || (channelDefinition.slug === '')) { - throw new SiteError(400, 'Must provide a channel URL slug'); - } - updateOp.$set.slug = striptags(channelDefinition.slug.trim()); + updateOp.$set.slug = this.getChannelSlug(channelDefinition.url); if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) { throw new SiteError(400, 'Must provide a stream key'); @@ -92,31 +103,44 @@ class VenueService extends SiteService { } updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim(); - await VenueChannel.updateOne({ _id: channel._id }, updateOp); + channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true }); + await this.updateChannelStatus(channel); } - async getChannels (pagination) { + async getChannels (pagination, options) { + options = Object.assign({ withCredentials: false }, options); pagination = Object.assign({ skip: 0, cpp: 10 }, pagination); const search = { }; - const channels = await VenueChannel + let q = VenueChannel .find(search) .sort({ slug: 1 }) .skip(pagination.skip) - .limit(pagination.cpp) - .populate(this.populateVenueChannel) - .lean(); + .limit(pagination.cpp); - return channels; + if (options.withCredentials) { + q = q.select('+credentials'); + } + return q.populate(this.populateVenueChannel).lean(); } - async getChannelById (channelId) { - const channel = await VenueChannel - .findOne({ _id: channelId }) - .populate(this.populateVenueChannel) - .lean(); - return channel; + async getChannelById (channelId, options) { + options = Object.assign({ withCredentials: false }, options); + let q = VenueChannel.findOne({ _id: channelId }); + if (options.withCredentials) { + q = q.select('+credentials'); + } + return q.populate(this.populateVenueChannel).lean(); + } + + async getChannelBySlug (channelSlug, options) { + options = Object.assign({ withCredentials: false }, options); + let q = VenueChannel.findOne({ slug: channelSlug.toLowerCase().trim() }); + if (options.withCredentials) { + q = q.select('+credentials'); + } + return q.populate(this.populateVenueChannel).lean(); } async getChannelFeed (channelSlug, options) { @@ -145,23 +169,6 @@ class VenueService extends SiteService { return json; } - async getChannelStatus (channel, options) { - const { cache: cacheService } = this.dtp.services; - const cacheKey = `venue:ch:${channel.slug}:status`; - options = Object.assign({ allowCache: true }, options); - - let json; - if (options.allowCache) { - json = await cacheService.getObject(cacheKey); - if (json) { return json; } - } - - const channelStatus = await this.updateChannelStatus(channel); - await cacheService.setObjectEx(cacheKey, 30, channelStatus); - - return channel; - } - async updateChannelStatus (channel) { const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`; this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl }); @@ -176,13 +183,30 @@ class VenueService extends SiteService { throw new Error(`failed to fetch channel status: ${json.message}`); } - const status = new VenueChannelStatus(json.channel); + let status = new VenueChannelStatus(json.channel); status.created = new Date(); + status.channel = channel._id; await status.save(); + await VenueChannel.updateOne({ _id: channel._id }, { $set: { lastStatus: status._id } }); return status.toObject(); } + + getChannelSlug (channelUrl) { + const { URL } = require('url'); + const url = new URL(channelUrl); + if (url.host !== this.soapboxDomain) { + throw new SiteError(400, 'This is not a valid DTP stream channel URL: Domain mismatch.'); + } + + const channelUrlParts = url.pathname.split('/').filter((part) => part.length > 0); + if (channelUrlParts[0] !== 'channel') { + throw new SiteError(400, 'This is not a valid DTP stream channel URL: Not on channel path.'); + } + + return slug(striptags(channelUrlParts[1].trim())); + } } module.exports = { diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 3f0783b..54df834 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -30,6 +30,11 @@ ul(uk-nav).uk-nav-default span.nav-item-icon i.fas.fa-file span.uk-margin-small-left Pages + li(class={ 'uk-active': (adminView === 'site-link') }) + a(href="/admin/site-link") + span.nav-item-icon + i.fas.fa-link + span.uk-margin-small-left Links li(class={ 'uk-active': (adminView === 'newsroom') }) a(href="/admin/newsroom") diff --git a/app/views/admin/site-link/editor.pug b/app/views/admin/site-link/editor.pug new file mode 100644 index 0000000..f529e79 --- /dev/null +++ b/app/views/admin/site-link/editor.pug @@ -0,0 +1,42 @@ +extends ../layouts/main +block content + + - var formAction = link ? `/admin/site-link/${link._id}` : '/admin/site-link'; + + form(method="POST", action= formAction).uk-form + .uk-card.uk-card-secondary.uk-card-small + .uk-card-header + h1.uk-card-title= link ? 'Update Site Link' : 'Add Site Link' + .uk-card-body + .uk-margin + label(for="label").uk-form-label Label + input(id="label", name="label", type="text", placeholder="Enter menu label", value= link ? link.label : undefined).uk-input + + .uk-margin + label(for="url").uk-form-label Target URL + input(id="url", name="url", type="url", placeholder="Enter URL", value= link ? link.url : undefined).uk-input + + .uk-margin + .pretty.p-default + input(id="target-blank", name="targetBlank", type="checkbox", checked= link ? link.target === '_blank' : false) + .state + label Open in new window/tab + + div(uk-grid).uk-card-footer + .uk-width-expand + +renderBackButton({ includeLabel: true, label: 'Cancel' }) + if link + .uk-width-auto + button( + type="button", + data-link={ _id: link._id, label: link.label }, + onclick="return dtp.adminApp.deleteSiteLink(event);", + ).uk-button.dtp-button-danger.uk-border-rounded + span + i.fas.fa-trash + span.uk-margin-small-left DELETE LINK + .uk-width-auto + button(type="submit").uk-button.dtp-button-primary.uk-border-rounded + span + i.fas.fa-save + span.uk-margin-small-left= link ? 'Update Link' : 'Add Link' \ No newline at end of file diff --git a/app/views/admin/site-link/index.pug b/app/views/admin/site-link/index.pug new file mode 100644 index 0000000..7ee2631 --- /dev/null +++ b/app/views/admin/site-link/index.pug @@ -0,0 +1,16 @@ +extends ../layouts/main +block content + + div(uk-grid) + .uk-width-expand + h1 Link home + .uk-width-auto + a(href="/admin/site-link/create").uk-button.dtp-button-primary.uk-border-rounded + span + i.fas.fa-plus + span.uk-margin-small-left Add Link + + ul.uk-list.uk-list-divider + each link of links + li + a(href=`/admin/site-link/${link._id}`)= link.label \ No newline at end of file diff --git a/app/views/admin/venue/channel/editor.pug b/app/views/admin/venue/channel/editor.pug index e8533f2..29450af 100644 --- a/app/views/admin/venue/channel/editor.pug +++ b/app/views/admin/venue/channel/editor.pug @@ -12,21 +12,22 @@ block content .uk-card-body .uk-margin label(for="slug").uk-form-label Channel URL - input(type="url", name="url", placeholder="Paste Shing.tv channel URL").uk-input + input(type="url", name="url", placeholder="Paste Shing.tv channel URL", value= channel ? `https://${dtp.services.venue.soapboxDomain}/channel/${channel.slug}` : undefined).uk-input + .uk-text-small.uk-text-muted #{site.name} integrates #{dtp.services.venue.soapboxDomain} and wants the channel URL from there. .uk-margin label(for="owner").uk-form-label Owner - input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : undefined).uk-input + input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : 'rob').uk-input .uk-text-small.uk-text-muted Enter the user's username here on #{site.name}, not from Shing.tv. div(uk-grid) div(class="uk-width-1-1 uk-width-2-3@m") label(for="stream-key").uk-form-label Stream Key - input(id="stream-key", type="text", name="credentials.streamKey", placeholder="Paste Shing.tv stream key", value= channel ? channel.credentials.streamKey : undefined).uk-input + input(id="stream-key", name="credentials.streamKey", type="text", placeholder="Paste Shing.tv stream key", value= channel ? channel.credentials.streamKey : undefined).uk-input div(class="uk-width-1-1 uk-width-1-3@m") label(for="widget-key").uk-form-label Widget Key - input(id="widget-key", type="text", name="credentials.widgetKey", placeholder="Paste Shing.tv widget key", value= channel ? channel.credentials.widgetKey : undefined).uk-input + input(id="widget-key", name="credentials.widgetKey", type="text", placeholder="Paste Shing.tv widget key", value= channel ? channel.credentials.widgetKey : undefined).uk-input .uk-card-footer.uk-flex.uk-flex-between .uk-width-auto diff --git a/app/views/admin/venue/channel/index.pug b/app/views/admin/venue/channel/index.pug index 466726b..a897715 100644 --- a/app/views/admin/venue/channel/index.pug +++ b/app/views/admin/venue/channel/index.pug @@ -1,3 +1,17 @@ extends ../../layouts/main block content - h1 VenueChannel Home \ No newline at end of file + + - + var onlineChannels = channels.filter((channel) => channel.lastUpdate && (channel.lastUpdate.status === 'live')) + var offlineChannels = channels.filter((channel) => !channel.lastUpdate || (channel.lastUpdate.status !== 'live')) + + h1 Manage Your Venue Channels + a(href="/admin/venue/channel/create").uk-button.dtp-button-primary.uk-border-rounded Add Channel + + if Array.isArray(offlineChannels) && (channels.length > 0) + uk.uk-list.uk-list-divider + each channel of offlineChannels + li + +renderVenueChannelListItem(channel, { baseUrl: '/admin/venue/channel' }) + else + div There are no channels integrated with #{site.name}. \ No newline at end of file diff --git a/app/views/admin/venue/index.pug b/app/views/admin/venue/index.pug index 3425b6f..ea80708 100644 --- a/app/views/admin/venue/index.pug +++ b/app/views/admin/venue/index.pug @@ -1,5 +1,20 @@ extends ../layouts/main block content + include ../../venue/components/channel-list-item + h1 Manage Your DTP Venue - a(href="/admin/venue/channel/create").uk-button.dtp-button-primary.uk-border-rounded Add channel + + - + var onlineChannels = channels.filter((channel) => channel.lastUpdate && (channel.lastUpdate.status === 'live')) + var offlineChannels = channels.filter((channel) => !channel.lastUpdate || (channel.lastUpdate.status !== 'live')) + + if Array.isArray(offlineChannels) && (channels.length > 0) + uk.uk-list.uk-list-divider + each channel of offlineChannels + li + +renderVenueChannelListItem(channel, { baseUrl: '/admin/venue/channel' }) + else + div There are no channels integrated with #{site.name}. + + pre= JSON.stringify(channels, null, 2) \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index 8f8bba1..978305c 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -1,6 +1,6 @@ include ../user/components/profile-icon -- var isLive = shingChannelStatus && shingChannelStatus.isLive; +- var isLive = venue && !!venue.channels.find((channel) => channel.lastStatus && (channel.lastStatus.status === 'live')); nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top .uk-navbar-left @@ -32,13 +32,31 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top else span i.fas.fa-tv - span(class="uk-visible@m").uk-margin-small-left= isLive ? 'Live Now' : 'Live' + span(class="uk-visible@m").uk-margin-small-left= isLive ? 'On Air' : 'Channels' each menuItem in mainMenu li(class={ 'uk-active': (pageSlug === menuItem.slug) }) a(href= menuItem.url, title= menuItem.label) +renderButtonIcon(menuItem.icon || 'fa-file', menuItem.label) + if Array.isArray(links) && (links.length > 0) + li + a(href="") + +renderButtonIcon('fa-link', 'Links') + .uk-navbar-dropdown + ul.uk-nav.uk-navbar-dropdown-nav + each link in links + li + a(href= link.url, target= link.target) + div(uk-grid).uk-grid-collapse.uk-flex-middle + div(style="width: 24px;") + img( + src= link.iconUrl, + style="width: 16px; height: auto;", + onerror=`this.src= "/img/icon/${site.domainKey}/icon-16x16.png";`, + ) + .uk-width-expand= link.label + div(class="uk-hidden@m").uk-navbar-center //- Site name ul.uk-navbar-nav diff --git a/app/views/components/page-sidebar.pug b/app/views/components/page-sidebar.pug index 8c6b1be..da5247a 100644 --- a/app/views/components/page-sidebar.pug +++ b/app/views/components/page-sidebar.pug @@ -1,6 +1,9 @@ include ../announcement/components/announcement include ../newsroom/components/feed-entry-list-item +include ../venue/components/channel-card +include ../venue/components/channel-list-item + - var isLive = !!shingChannelStatus && shingChannelStatus.isLive && !!shingChannelStatus.liveEpisode; mixin renderSidebarEpisode(episode) @@ -32,32 +35,28 @@ mixin renderPageSidebar ( ) //- //- Shing.tv Channel Integration //- - if isLive - .uk-margin-medium - +renderSectionTitle('Live Now!', { - label: 'Tune In', - title: shingChannelStatus.name, - url: '/venue', - }) - .uk-card.uk-card-default.uk-card-small.uk-card-hover.uk-margin - if shingChannelStatus.liveThumbnail - .uk-card-media-top - a(href="/venue") - img( - src= shingChannelStatus.liveThumbnail.url, - onerror=`this.src = '${shingChannelStatus.thumbnailUrl}';`, - title="Tune in now", - ) - if shingChannelStatus.liveEpisode && shingChannelStatus.liveEpisode.title - .uk-card-body - .uk-card-title.uk-margin-remove.uk-text-truncate - a(href="/venue", uk-tooltip= `Watch "${shingChannelStatus.liveEpisode.title}" now!`)= shingChannelStatus.liveEpisode.title - .uk-text-small - div(uk-grid).uk-grid-small.uk-flex-between - .uk-width-auto - div Started: #{moment(shingChannelStatus.liveEpisode.created).fromNow()} - .uk-width-auto #[i.fas.fa-eye] #{formatCount(shingChannelStatus.liveEpisode.stats.currentViewerCount)} + - + var onlineChannels = venue.channels.filter((channel) => channel.lastStatus && (channel.lastStatus.status === 'live')); + var offlineChannels = venue.channels.filter((channel) => !channel.lastStatus || (channel.lastStatus.status !== 'live')); + + if Array.isArray(onlineChannels) && (onlineChannels.length > 0) + each channel of onlineChannels + .uk-margin-medium + +renderSectionTitle('Live Now!', { + label: 'Tune In', + title: channel.lastStatus.name, + url: '/venue', + }) + +renderVenueChannelCard(channel) + + if Array.isArray(offlineChannels) && (offlineChannels.length > 0) + .uk-margin-medium + +renderSectionTitle('Offline Channels') + ul.uk-list.uk-list-divider + each venueChannel of offlineChannels + li + +renderVenueChannelListItem(offlineChannels[0]) //- //- Shing.tv Channel Feed diff --git a/app/views/venue/components/channel-card.pug b/app/views/venue/components/channel-card.pug new file mode 100644 index 0000000..b5d7ecb --- /dev/null +++ b/app/views/venue/components/channel-card.pug @@ -0,0 +1,21 @@ +mixin renderVenueChannelCard (channel) + .uk-card.uk-card-default.uk-card-small.uk-card-hover.uk-margin + + if channel.lastStatus && channel.lastStatus.liveThumbnail + .uk-card-media-top + a(href=`/venue/${channel.slug}`) + img( + src= channel.lastStatus.liveThumbnail.url, + onerror=`this.src = '${channel.lastStatus.thumbnailUrl}';`, + title="Tune in now", + ) + + if channel.lastStatus && channel.lastStatus.liveEpisode && channel.lastStatus.liveEpisode.title + .uk-card-body + .uk-text-bold.uk-text-truncate + a(href="/venue", uk-tooltip= `Watch "${channel.lastStatus.liveEpisode.title}" now!`)= channel.lastStatus.liveEpisode.title + .uk-text-small + div(uk-grid).uk-grid-small.uk-flex-between + .uk-width-auto + div Started: #{moment(channel.lastStatus.liveEpisode.created).fromNow()} + .uk-width-auto #[i.fas.fa-eye] #{formatCount(channel.lastStatus.liveEpisode.stats.currentViewerCount)} \ No newline at end of file diff --git a/app/views/venue/components/channel-list-item.pug b/app/views/venue/components/channel-list-item.pug new file mode 100644 index 0000000..4beb0ee --- /dev/null +++ b/app/views/venue/components/channel-list-item.pug @@ -0,0 +1,24 @@ +mixin renderVenueChannelListItem (channel, options) + - options = Object.assign({ baseUrl: '/venue' }, options); + div(uk-grid).uk-grid-small + .uk-width-auto + a(href= getUserProfileUrl(channel.owner), uk-tooltip=`Visit ${channel.owner.displayName || channel.owner.username}'s profile`) + +renderProfileIcon(channel.owner) + + .uk-width-expand + .uk-margin-small + if channel.lastStatus + .uk-text-bold + a(href=`${options.baseUrl}/${channel.slug}`, uk-tooltip=`Visit ${channel.lastStatus.name}`)= channel.lastStatus.name + else + div ...waiting for first channel status update to arrive + + .uk-text-small.uk-text-meta + div(uk-grid).uk-grid-small.uk-grid-divider + .uk-width-expand.uk-text-truncate + +renderUserLink(channel.owner) + .uk-width-auto + if channel.lastStatus.status === 'live' + span.uk-text-success LIVE + else + span= moment(channel.lastStatus.lastLive).fromNow() \ No newline at end of file diff --git a/app/views/venue/embed.pug b/app/views/venue/embed.pug index f4c6f8f..065d6bc 100644 --- a/app/views/venue/embed.pug +++ b/app/views/venue/embed.pug @@ -2,4 +2,9 @@ extends ../layouts/main block content - var shingBaseUrl = `https://${dtp.services.venue.soapboxDomain}`; - iframe(src= `${shingBaseUrl}/channel/${site.shingChannelSlug}/embed/venue?k=${site.shingWidgetKey}`, style="width: 100%; height: 720px;", allowfullscreen) \ No newline at end of file + iframe(src= `${shingBaseUrl}/channel/${channel.slug}/embed/venue?k=${channelCredentials.widgetKey}`, style="width: 100%; height: 720px;", allowfullscreen) + +block viewjs + script. + window.dtp = window.dtp || { }; + window.dtp.channel = !{JSON.stringify(channel)}; \ No newline at end of file diff --git a/app/views/venue/index.pug b/app/views/venue/index.pug new file mode 100644 index 0000000..3e57a0c --- /dev/null +++ b/app/views/venue/index.pug @@ -0,0 +1,38 @@ +extends ../layouts/main +block content + + include ../components/pagination-bar + + - + var onlineChannels = channels.filter((channel) => channel.lastStatus && (channel.lastStatus.status === 'live')) + var offlineChannels = channels.filter((channel) => !channel.lastStatus || (channel.lastStatus.status !== 'live')) + + section.uk-section.uk-section-default.uk-section-small + .uk-container.uk-container-expand + + .uk-margin-large + div(uk-grid) + .uk-width-2-3 + +renderSectionTitle('Live Channels') + .uk-margin-small + if Array.isArray(onlineChannels) && (onlineChannels.length > 0) + div(uk-grid).uk-grid-small + each channel of onlineChannels + .uk-width-1-3 + +renderVenueChannelCard(channel) + else + div There are no live channels. Please check back later! + + .uk-width-1-3 + +renderSectionTitle('Offline Channels') + .uk-margin-small + if Array.isArray(offlineChannels) && (offlineChannels.length > 0) + ul.uk-list.uk-list-divider + each channel of offlineChannels + li + +renderVenueChannelListItem(channel) + else + div There are no offline channels. + + //- pre= JSON.stringify(onlineChannels, null, 2) + //- pre= JSON.stringify(offlineChannels, null, 2) \ No newline at end of file diff --git a/app/workers/venue.js b/app/workers/venue.js new file mode 100644 index 0000000..de899cc --- /dev/null +++ b/app/workers/venue.js @@ -0,0 +1,59 @@ +// venue.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const path = require('path'); + +require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') }); + +const { + SiteLog, + SiteWorker, +} = require(path.join(__dirname, '..', '..', 'lib', 'site-lib')); + +module.rootPath = path.resolve(__dirname, '..', '..'); +module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); + +module.config = { + environment: process.env.NODE_ENV, + root: module.rootPath, + component: { name: 'venue', slug: 'venue' }, +}; + +module.config.site = require(path.join(module.rootPath, 'config', 'site')); +module.config.http = require(path.join(module.rootPath, 'config', 'http')); + +class VenueWorker extends SiteWorker { + + constructor (dtp) { + super(dtp, dtp.config.component); + } + + async start ( ) { + await super.start(); + + await this.loadProcessor(path.join(__dirname, 'venue', 'cron', 'update-channel-status.js')); + await this.startProcessors(); + } + + async stop ( ) { + await super.stop(); + } +} + +(async ( ) => { + try { + module.log = new SiteLog(module, module.config.component); + + module.worker = new VenueWorker(module); + await module.worker.start(); + + module.log.info(`${module.pkg.name} v${module.pkg.version} ${module.config.component.name} started`); + } catch (error) { + module.log.error('failed to start worker', { component: module.config.component, error }); + process.exit(-1); + } + +})(); \ No newline at end of file diff --git a/app/workers/venue/cron/update-channel-status.js b/app/workers/venue/cron/update-channel-status.js new file mode 100644 index 0000000..cc39df8 --- /dev/null +++ b/app/workers/venue/cron/update-channel-status.js @@ -0,0 +1,70 @@ +// venue/cron/update-channel-status.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const path = require('path'); + +const mongoose = require('mongoose'); +const VenueChannel = mongoose.model('VenueChannel'); + +const { CronJob } = require('cron'); + +const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib')); + +class UpdateChannelStatusCron extends SiteWorkerProcess { + + static get COMPONENT ( ) { + return { + name: 'updateChannelStatus', + slug: 'update-channel-status', + }; + } + + constructor (worker) { + super(worker, UpdateChannelStatusCron.COMPONENT); + } + + async start ( ) { + await super.start(); + + await this.updateChannelStatus(); // first-run the expirations + + this.job = new CronJob( + '*/15 * * * * *', + this.updateChannelStatus.bind(this), + null, + true, + process.env.DTP_CRON_TIMEZONE || 'America/New_York', + ); + } + + async stop ( ) { + if (this.job) { + this.log.info('stopping channel update job'); + this.job.stop(); + delete this.job; + } + await super.stop(); + } + + async updateChannelStatus ( ) { + const { venue: venueService } = this.dtp.services; + try { + await VenueChannel + .find() + .lean() + .cursor() + .eachAsync(async (channel) => { + this.log.info('updating channel status', { channel: channel.slug }); + const status = await venueService.updateChannelStatus(channel); + this.log.info('channel status updated', { channel: status.name, status: status.status }); + }); + } catch (error) { + this.log.error('failed to expire crashed hosts', { error }); + } + } +} + +module.exports = UpdateChannelStatusCron; \ No newline at end of file diff --git a/client/js/site-admin-app.js b/client/js/site-admin-app.js index 1f8a837..0eae61b 100644 --- a/client/js/site-admin-app.js +++ b/client/js/site-admin-app.js @@ -442,6 +442,30 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp { return false; } + + async deleteSiteLink (event) { + event.preventDefault(); + event.stopPropagation(); + + const target = event.currentTarget || event.target; + const link = JSON.parse(target.getAttribute('data-link')); + + try { + await UIkit.modal.confirm(`Are you sure you want to remove sit elink "${link.label}"?`); + } catch (error) { + // canceled + return false; + } + + try { + const response = await fetch(`/admin/site-link/${link._id}`, { method: 'DELETE' }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to remove site link: ${error.message}`); + } + + return false; + } } dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp; \ No newline at end of file diff --git a/dtp-sites.js b/dtp-sites.js index e0efc57..2625f11 100644 --- a/dtp-sites.js +++ b/dtp-sites.js @@ -25,8 +25,12 @@ module.config = { module.log.info('registering Page service middleware'); const { page: pageService } = module.services; - app.use(pageService.menuMiddleware.bind(pageService)); + const { siteLink: siteLinkService } = module.services; + app.use( + pageService.menuMiddleware.bind(pageService), + siteLinkService.middleware(), + ); }, }; diff --git a/yarn.lock b/yarn.lock index e00f966..d40d18d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1302,7 +1302,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.12.5: +ajv@^6.12.3, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1563,11 +1563,23 @@ asn1.js@^5.2.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + assert-never@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -1642,6 +1654,16 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + axios@0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -1762,6 +1784,13 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + bellajs@^11.0.7: version "11.1.1" resolved "https://registry.yarnpkg.com/bellajs/-/bellajs-11.1.1.tgz#1828dae65e396bf6c199fa8e0e402597b387ce29" @@ -2165,6 +2194,11 @@ caniuse-lite@^1.0.30001280: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz" integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ== +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + "chalk@4.1 - 4.1.2", chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -2430,7 +2464,7 @@ colors@^1.2.1: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.8: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2631,6 +2665,11 @@ core-js-compat@^3.18.0, core-js-compat@^3.19.1: browserslist "^4.18.1" semver "7.0.0" +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2757,6 +2796,13 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + data-urls@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8" @@ -3138,6 +3184,14 @@ eazy-logger@3.1.0: dependencies: tfunk "^4.0.0" +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3613,7 +3667,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3643,6 +3697,16 @@ extract-zip@2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + fancy-log@^1.3.2, fancy-log@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" @@ -3687,6 +3751,13 @@ fast-xml-parser@^4.0.10: dependencies: strnum "^1.0.5" +favicon@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/favicon/-/favicon-0.0.2.tgz#5a650d1e6684d300822ea8fe1f1f2eefcb4d3f91" + integrity sha512-n7CzPjyg2DbbwAvMMpJ13Ek+I4Fl5fXl+jtdhpQoPKmMD7hsGVZVC41yWwdeS7TZa9WYoyhhhY1/VuTJT5cnNg== + dependencies: + request "2.x.x" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -3848,6 +3919,11 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3857,6 +3933,15 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4022,6 +4107,13 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -4262,6 +4354,19 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -4471,6 +4576,15 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + https-proxy-agent@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -4995,7 +5109,7 @@ is-typed-array@^1.1.3, is-typed-array@^1.1.7: foreach "^2.0.5" has-tostringtag "^1.0.0" -is-typedarray@^1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -5081,6 +5195,11 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + jake@^10.6.1: version "10.8.2" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" @@ -5133,6 +5252,11 @@ jsbn@1.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA= +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + jsdom@^19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" @@ -5209,7 +5333,7 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@^0.4.0: +json-schema@0.4.0, json-schema@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== @@ -5224,6 +5348,11 @@ json-stream@^1.0.0: resolved "https://registry.yarnpkg.com/json-stream/-/json-stream-1.0.0.tgz#1a3854e28d2bbeeab31cc7ddf683d2ddc5652708" integrity sha1-GjhU4o0rvuqzHMfd9oPS3cVlJwg= +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json5@^2.1.2, json5@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -5252,6 +5381,16 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + jstransformer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" @@ -5669,6 +5808,11 @@ mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.34" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" @@ -5676,6 +5820,13 @@ mime-types@^2.1.12, mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.17, dependencies: mime-db "1.51.0" +mime-types@~2.1.19: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -6098,10 +6249,10 @@ o-stream@^0.3.0: resolved "https://registry.yarnpkg.com/o-stream/-/o-stream-0.3.0.tgz#204d27bc3fb395164507d79b381e91752e8daedc" integrity sha512-gbzl6qCJZ609x/M2t25HqCYQagFzWYCtQ84jcuObGr+V8D1Am4EVubkF4J+XFs6ukfiv96vNeiBb8FrbbMZYiQ== -oauth@0.9.x: - version "0.9.15" - resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" - integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== oauth2orize@^1.11.1: version "1.11.1" @@ -6525,6 +6676,11 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picmo@^5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/picmo/-/picmo-5.4.0.tgz#d51c9258031b351217e2d165ed3781f4a192c938" @@ -6682,6 +6838,11 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -6881,6 +7042,11 @@ qs@6.9.7: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -7213,6 +7379,32 @@ replace-homedir@^1.0.0: is-absolute "^1.0.0" remove-trailing-separator "^1.1.0" +request@2.x.x: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7369,7 +7561,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -7911,6 +8103,21 @@ sprintf-js@1.1.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + stack-trace@0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -8386,6 +8593,14 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -8417,6 +8632,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -8711,6 +8931,11 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -8741,6 +8966,15 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + vinyl-fs@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7"