Browse Source
System: - config updates - added HostCacheService - added LinkService - added SiteHostServices worker - start-local now starts/stops host-services worker - added ImageController Models: - Added NetHost and NetHostStatus Dependencies: - added NPM cron, diskusage-ng, jsdom, systeminformation, user-agentsdevelop
18 changed files with 2114 additions and 11 deletions
@ -0,0 +1,184 @@ |
|||
// page.js
|
|||
// Copyright (C) 2022,2023 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import fs from 'node:fs'; |
|||
import express from 'express'; |
|||
import mongoose from 'mongoose'; |
|||
|
|||
import { SiteController, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class ImageController extends SiteController { |
|||
|
|||
static get name ( ) { return 'ImageController'; } |
|||
static get slug ( ) { return 'image'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, ImageController); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const { dtp } = this; |
|||
const { limiter: limiterService } = dtp.services; |
|||
|
|||
const router = express.Router(); |
|||
dtp.app.use('/image', router); |
|||
|
|||
const imageUpload = this.createMulter(ImageController.slug, { |
|||
limits: { |
|||
fileSize: 1024 * 1000 * 5, |
|||
}, |
|||
}); |
|||
|
|||
router.use(async (req, res, next) => { |
|||
res.locals.currentView = 'image'; |
|||
return next(); |
|||
}); |
|||
|
|||
router.param('imageId', this.populateImage.bind(this)); |
|||
|
|||
router.post('/', |
|||
limiterService.create(limiterService.config.image.postCreateImage), |
|||
imageUpload.single('file'), |
|||
this.postCreateImage.bind(this), |
|||
); |
|||
|
|||
router.get('/proxy', |
|||
limiterService.create(limiterService.config.image.getProxyImage), |
|||
this.getProxyImage.bind(this), |
|||
); |
|||
|
|||
router.get('/:imageId', |
|||
limiterService.create(limiterService.config.image.getImage), |
|||
this.getHostCacheImage.bind(this), |
|||
// this.getImage.bind(this),
|
|||
); |
|||
|
|||
router.delete('/:imageId', |
|||
limiterService.create(limiterService.config.image.deleteImage), |
|||
this.deleteImage.bind(this), |
|||
); |
|||
} |
|||
|
|||
async populateImage (req, res, next, imageId) { |
|||
const { image: imageService } = this.dtp.services; |
|||
try { |
|||
res.locals.imageId = mongoose.Types.ObjectId(imageId); |
|||
res.locals.image = await imageService.getImageById(res.locals.imageId); |
|||
if (!res.locals.image) { |
|||
throw new SiteError(404, 'Image not found'); |
|||
} |
|||
return next(); |
|||
} catch (error) { |
|||
this.log.error('failed to populate image', { error }); |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async postCreateImage (req, res, next) { |
|||
const { image: imageService } = this.dtp.services; |
|||
try { |
|||
res.locals.image = await imageService.create(req.user, req.body, req.file); |
|||
res.status(200).json({ |
|||
success: true, |
|||
imageId: res.locals.image._id.toString(), |
|||
}); |
|||
} catch (error) { |
|||
this.log.error('failed to create image', { error }); |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async getProxyImage (req, res) { |
|||
const { hostCache: hostCacheService } = this.dtp.services; |
|||
try { |
|||
if (!req.query || !req.query.url) { |
|||
throw new SiteError(400, 'Missing url parameter'); |
|||
} |
|||
const fileInfo = await hostCacheService.fetchUrl(req.query.url); |
|||
const stream = fs.createReadStream(fileInfo.file.path); |
|||
|
|||
res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); |
|||
res.setHeader('Content-Type', fileInfo.file.meta.contentType); |
|||
res.setHeader('Content-Length', fileInfo.file.stats.size); |
|||
|
|||
res.status(200); |
|||
stream.pipe(res); |
|||
} catch (error) { |
|||
this.log.error('failed to fetch web resource', { url: req.query.url, error }); |
|||
return res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async getHostCacheImage (req, res) { |
|||
const { hostCache: hostCacheService } = this.dtp.services; |
|||
const { image } = res.locals; |
|||
try { |
|||
const fileInfo = await hostCacheService.getFile(image.file.bucket, image.file.key); |
|||
const stream = fs.createReadStream(fileInfo.file.path); |
|||
|
|||
res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); |
|||
res.setHeader('Content-Type', image.type); |
|||
res.setHeader('Content-Length', fileInfo.file.stats.size); |
|||
|
|||
res.status(200); |
|||
stream.pipe(res); |
|||
} catch (error) { |
|||
this.log.error('failed to fetch image', { image, error }); |
|||
return res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async getImage (req, res) { |
|||
const { minio: minioService } = this.dtp.services; |
|||
try { |
|||
const stream = await minioService.openDownloadStream({ |
|||
bucket: res.locals.image.file.bucket, |
|||
key: res.locals.image.file.key |
|||
}); |
|||
|
|||
res.setHeader('Cache-Control', 'public, maxage=86400, s-maxage=86400, immutable'); |
|||
res.setHeader('Content-Type', res.locals.image.type); |
|||
res.setHeader('Content-Length', res.locals.image.size); |
|||
|
|||
res.status(200); |
|||
stream.pipe(res); |
|||
} catch (error) { |
|||
return res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async deleteImage (req, res) { |
|||
const { image: imageService } = this.dtp.services; |
|||
try { |
|||
if (!req.user) { |
|||
throw new SiteError(403, 'Must be logged in to delete images you own'); |
|||
} |
|||
if (!req.user._id.equals(res.locals.image.owner._id)) { |
|||
throw new SiteError(403, 'You are not the owner of the requested image'); |
|||
} |
|||
|
|||
this.log.debug('deleting image', { imageId: res.locals.image._id }); |
|||
await imageService.deleteImage(res.locals.image); |
|||
|
|||
res.status(200).json({ success: true }); |
|||
} catch (error) { |
|||
this.log.error('failed to delete image', { error }); |
|||
res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,67 @@ |
|||
// net-host-stats.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const CpuInfoSchema = new Schema({ |
|||
user: { type: Number }, |
|||
nice: { type: Number }, |
|||
sys: { type: Number }, |
|||
idle: { type: Number }, |
|||
irq: { type: Number }, |
|||
}); |
|||
|
|||
const MemoryInfoSchema = new Schema({ |
|||
total: { type: Number }, |
|||
free: { type: Number }, |
|||
used: { type: Number }, |
|||
active: { type: Number }, |
|||
available: { type: Number }, |
|||
buffers: { type: Number }, |
|||
cached: { type: Number }, |
|||
slab: { type: Number }, |
|||
buffcache: { type: Number }, |
|||
swaptotal: { type: Number }, |
|||
swapused: { type: Number }, |
|||
swapfree: { type: Number }, |
|||
}); |
|||
|
|||
const CacheStatsSchema = new Schema({ |
|||
itemCount: { type: Number, required: true }, |
|||
dataSize: { type: Number, required: true }, |
|||
expireCount: { type: Number, required: true }, |
|||
expireDataSize: { type: Number, required: true }, |
|||
hitCount: { type: Number, required: true }, |
|||
hitDataSize: { type: Number, required: true }, |
|||
missCount: { type: Number, required: true }, |
|||
missDataSize: { type: Number, required: true }, |
|||
}); |
|||
|
|||
const DiskUsageSchema = new Schema({ |
|||
total: { type: Number }, |
|||
used: { type: Number }, |
|||
available: { type: Number }, |
|||
pctUsed: { type: Number }, |
|||
}); |
|||
|
|||
const NetworkInterfaceStatsSchema = new Schema({ |
|||
iface: { type: String, required: true }, |
|||
rxPerSecond: { type: Number }, |
|||
rxDropped: { type: Number }, |
|||
rxErrors: { type: Number }, |
|||
txPerSecond: { type: Number }, |
|||
txDropped: { type: Number }, |
|||
txErrors: { type: Number }, |
|||
}); |
|||
|
|||
export { |
|||
CpuInfoSchema, |
|||
MemoryInfoSchema, |
|||
CacheStatsSchema, |
|||
DiskUsageSchema, |
|||
NetworkInterfaceStatsSchema, |
|||
}; |
@ -0,0 +1,31 @@ |
|||
// net-host-stats.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
import { |
|||
CpuInfoSchema, |
|||
MemoryInfoSchema, |
|||
CacheStatsSchema, |
|||
DiskUsageSchema, |
|||
NetworkInterfaceStatsSchema, |
|||
} from './lib/host-stats.js'; |
|||
|
|||
const NetHostStatsSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: 1, expires: '7d' }, |
|||
host: { type: Schema.ObjectId, required: true, index: 1, ref: 'NetHost' }, |
|||
load: { type: [Number], required: true }, |
|||
cpus: { type: [CpuInfoSchema], required: true }, |
|||
memory: { type: MemoryInfoSchema, required: true }, |
|||
cache: { type: CacheStatsSchema, required: true }, |
|||
disk: { |
|||
cache: { type: DiskUsageSchema, required: true }, |
|||
}, |
|||
network: { type: [NetworkInterfaceStatsSchema], required: true }, |
|||
}); |
|||
|
|||
export default mongoose.model('NetHostStats', NetHostStatsSchema); |
@ -0,0 +1,53 @@ |
|||
// net-host.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const NETHOST_STATUS_LIST = [ |
|||
'starting', |
|||
'active', |
|||
'shutdown', |
|||
'inactive', |
|||
'crashed', |
|||
]; |
|||
|
|||
const CpuInfoSchema = new Schema({ |
|||
model: { type: String }, |
|||
speed: { type: Number }, |
|||
}); |
|||
|
|||
const NetworkInterfaceSchema = new Schema({ |
|||
iface: { type: String, required: true }, |
|||
speed: { type: Number }, |
|||
mac: { type: String }, |
|||
ip4: { type: String }, |
|||
ip4subnet: { type: String }, |
|||
ip6: { type: String }, |
|||
ip6subnet: { type: String }, |
|||
flags: { |
|||
internal: { type: Boolean }, |
|||
virtual: { type: Boolean }, |
|||
}, |
|||
}); |
|||
|
|||
const NetHostSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: 1 }, |
|||
updated: { type: Date }, |
|||
status: { type: String, enum: NETHOST_STATUS_LIST, required: true, index: 1 }, |
|||
hostname: { type: String, required: true, index: 1 }, |
|||
arch: { type: String, required: true }, |
|||
cpus: { type: [CpuInfoSchema], required: true }, |
|||
totalmem: { type: Number, required: true }, |
|||
freemem: { type: Number, required: true }, |
|||
node: { type: String, required: true }, |
|||
platform: { type: String, required: true }, |
|||
release: { type: String, required: true }, |
|||
version: { type: String, required: true }, |
|||
network: { type: [NetworkInterfaceSchema] }, |
|||
}); |
|||
|
|||
export default mongoose.model('NetHost', NetHostSchema); |
@ -0,0 +1,140 @@ |
|||
// host-cache.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import dgram from 'node:dgram'; |
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class HostCacheService extends SiteService { |
|||
|
|||
static get name ( ) { return 'HostCacheService'; } |
|||
static get slug ( ) { return 'hostCache'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, HostCacheService); |
|||
this.transactions = { }; |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
|
|||
this.log.info('creating UDP host-cache socket'); |
|||
this.hostCache = dgram.createSocket('udp4', this.onMessage.bind(this)); |
|||
this.hostCache.on('error', this.onError.bind(this)); |
|||
|
|||
this.log.info('connecting UDP host-cache socket'); |
|||
// this.hostCache.bind(0, '127.0.0.1');
|
|||
this.hostCache.connect( |
|||
parseInt(process.env.HOST_CACHE_PORT || '8000', 10), |
|||
process.env.HOST_CACHE_HOST || '127.0.0.1', |
|||
); |
|||
} |
|||
|
|||
async stop ( ) { |
|||
if (this.hostCache) { |
|||
this.log.info('disconnecting UDP host-cache socket'); |
|||
this.hostCache.disconnect(); |
|||
delete this.hostCache; |
|||
} |
|||
} |
|||
|
|||
async getFile (bucket, key) { |
|||
return new Promise((resolve, reject) => { |
|||
const transaction = { tid: uuidv4(), bucket, key, resolve, reject }; |
|||
this.transactions[transaction.tid] = transaction; |
|||
const message = JSON.stringify({ |
|||
tid: transaction.tid, |
|||
cmd: 'getFile', |
|||
params: { bucket, key }, |
|||
}); |
|||
this.hostCache.send(message); |
|||
}); |
|||
} |
|||
|
|||
async fetchUrl (url) { |
|||
return new Promise((resolve, reject) => { |
|||
const transaction = { tid: uuidv4(), url, resolve, reject }; |
|||
this.transactions[transaction.tid] = transaction; |
|||
const message = JSON.stringify({ |
|||
tid: transaction.tid, |
|||
cmd: 'fetchUrl', |
|||
params: { url }, |
|||
}); |
|||
this.hostCache.send(message); |
|||
}); |
|||
} |
|||
|
|||
async onMessage (message, rinfo) { |
|||
message = message.toString('utf8'); |
|||
message = JSON.parse(message); |
|||
switch (message.res.cmd) { |
|||
case 'getFile': |
|||
return this.onGetFile(message, rinfo); |
|||
case 'fetchUrl': |
|||
return this.onFetchUrl(message, rinfo); |
|||
} |
|||
} |
|||
|
|||
async onGetFile (message) { |
|||
const transaction = this.transactions[message.tid]; |
|||
if (!transaction) { |
|||
this.log.error('getFile response received with no matching transaction', { tid: message.tid }); |
|||
return; |
|||
} |
|||
|
|||
delete this.transactions[message.tid]; |
|||
|
|||
if (!message.res.success) { |
|||
transaction.reject(new SiteError(message.res.statusCode, message.res.message)); |
|||
return; |
|||
} |
|||
|
|||
transaction.resolve({ |
|||
success: message.res.success, |
|||
message: message.res.message, |
|||
file: message.res.file, |
|||
flags: message.flags, |
|||
duration: message.duration, |
|||
}); |
|||
} |
|||
|
|||
async onFetchUrl (message) { |
|||
const transaction = this.transactions[message.tid]; |
|||
if (!transaction) { |
|||
this.log.error('fetchUrl response received with no matching transaction', { tid: message.tid }); |
|||
return; |
|||
} |
|||
|
|||
delete this.transactions[message.tid]; |
|||
|
|||
if (!message.res.success) { |
|||
transaction.reject(new SiteError(message.res.statusCode || 500, message.res.message)); |
|||
return; |
|||
} |
|||
|
|||
transaction.resolve({ |
|||
success: message.res.success, |
|||
message: message.res.message, |
|||
file: message.res.file, |
|||
flags: message.flags, |
|||
duration: message.duration, |
|||
}); |
|||
} |
|||
|
|||
async onError (error) { |
|||
this.log.error('onError', { error }); |
|||
if ((error.errno !== -111) || (error.code !== 'ECONNREFUSED')) { |
|||
return; |
|||
} |
|||
for (const key of this.transactions) { |
|||
this.log.alert('destroying host cache transaction', { key }); |
|||
const transaction = this.transactions[key]; |
|||
transaction.reject(error); |
|||
delete this.transactions[key]; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,293 @@ |
|||
// link.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Link = mongoose.model('Link'); |
|||
|
|||
import UserAgent from 'user-agents'; |
|||
import { JSDOM } from 'jsdom'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class LinkService extends SiteService { |
|||
|
|||
static get name ( ) { return 'LinkService'; } |
|||
static get slug ( ) { return 'link'; } |
|||
constructor (dtp) { |
|||
super(dtp, LinkService); |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
|
|||
const userAgent = new UserAgent(); |
|||
this.userAgent = userAgent.toString(); |
|||
|
|||
this.templates = { |
|||
linkPreview: this.loadViewTemplate('link/components/preview-standalone.pug'), |
|||
}; |
|||
|
|||
const { user: userService } = this.dtp.services; |
|||
this.populateLink = [ |
|||
{ |
|||
path: 'submittedBy', |
|||
select: userService.USER_SELECT, |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
async getRecent (pagination) { |
|||
const search = { }; |
|||
const links = await Link |
|||
.find(search) |
|||
.sort({ created: -1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.populate(this.populateLink) |
|||
.lean(); |
|||
const totalLinkCount = await Link.estimatedDocumentCount(); |
|||
return { links, totalLinkCount }; |
|||
} |
|||
|
|||
async getById (linkId) { |
|||
return Link |
|||
.findOne({ _id: linkId }) |
|||
.populate(this.populateLink) |
|||
.lean(); |
|||
} |
|||
|
|||
async recordVisit (link) { |
|||
await Link.updateOne( |
|||
{ _id: link._id }, |
|||
{ |
|||
$inc: { 'stats.visitCount': 1 }, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async isDomainBlocked (domain) { |
|||
const { domain: domainService } = this.dtp.services; |
|||
return domainService.isDomainBlacklisted(domain); |
|||
} |
|||
|
|||
async ingest (author, url) { |
|||
const NOW = new Date(); |
|||
const domain = new URL(url).hostname.toLowerCase(); |
|||
if (domain.endsWith('.cn')) { |
|||
throw new SiteError(403, 'Linking to Chinese websites is prohibited.'); |
|||
} |
|||
if (domain.endsWith('.il')) { |
|||
throw new SiteError(403, 'Linking to websites in Israel is prohibited.'); |
|||
} |
|||
|
|||
if (await this.isDomainBlocked(domain)) { |
|||
this.log.alert('detected blocked domain in shared link', { |
|||
author: { _id: author._id, username: author.username }, |
|||
domain, url, |
|||
}); |
|||
throw new SiteError(403, `All links/URLs pointing to ${domain} are prohibited.`); |
|||
} |
|||
|
|||
/* |
|||
* An upsert is used to create a document if one doesn't exist. The domain |
|||
* and url are set on insert, and lastShared is always set so it will be |
|||
* current. |
|||
* |
|||
* submittedBy is an array that holds the User._id of each member that |
|||
* submitted the link. This enables their Link History view, which becomes |
|||
* it's own feed. |
|||
*/ |
|||
const link = await Link.findOneAndUpdate( |
|||
{ domain, url }, |
|||
{ |
|||
$setOnInsert: { |
|||
created: NOW, |
|||
domain, url, |
|||
}, |
|||
$addToSet: { submittedBy: author._id }, |
|||
$set: { lastShared: NOW }, |
|||
}, |
|||
{ upsert: true, new: true }, |
|||
); |
|||
|
|||
/* |
|||
* link is now the document from MongoDB and will contain additional |
|||
* information about the link, or not. If not, create a job to fetch link |
|||
* preview data, and to scan the link for malicious intent (unless we know |
|||
* the link has been administratively blocked). |
|||
*/ |
|||
// if (!link.flags || (!link.flags.isBlocked && !link.flags.havePreview)) {
|
|||
this.socialQueue.add('link-ingest', { |
|||
submitterId: author._id, |
|||
linkId: link._id, |
|||
}); |
|||
// }
|
|||
|
|||
return link; |
|||
} |
|||
|
|||
async generatePagePreview (url, options) { |
|||
const NOW = new Date(); |
|||
const linkUrlObj = new URL(url); |
|||
this.log.debug('generating page preview', { url, linkUrlObj }); |
|||
|
|||
const { /*window,*/ document } = await this.loadUrlAsDOM(url, options); |
|||
const preview = { |
|||
fetched: NOW, |
|||
domain: linkUrlObj.hostname, |
|||
tags: [ ], |
|||
images: [ ], |
|||
videos: [ ], |
|||
audios: [ ], |
|||
favicons: [ ], |
|||
}; |
|||
|
|||
function getMetaContent (selector) { |
|||
const element = document.querySelector(selector); |
|||
if (!element) { |
|||
return; |
|||
} |
|||
return element.getAttribute('content'); |
|||
} |
|||
|
|||
function getElementContent (selector) { |
|||
const element = document.querySelector(selector); |
|||
if (!element) { |
|||
return; |
|||
} |
|||
return element.textContent; |
|||
} |
|||
|
|||
function getLinkHref (selector) { |
|||
const element = document.querySelector(selector); |
|||
if (!element) { |
|||
return; |
|||
} |
|||
return element.getAttribute('href'); |
|||
} |
|||
|
|||
preview.mediaType = getMetaContent('head meta[property="og:type"]'); |
|||
preview.title = |
|||
getMetaContent('head meta[property="og:title"]') || |
|||
getElementContent(`head title`); |
|||
preview.siteName = |
|||
getMetaContent('head meta[property="og:site_name') || |
|||
getElementContent(`head title`); |
|||
|
|||
preview.description = |
|||
getMetaContent('head meta[property="og:description"]') || |
|||
getMetaContent('head meta[name="description"]'); |
|||
|
|||
let href = |
|||
getMetaContent('head meta[property="og:image:secure_url') || |
|||
getMetaContent('head meta[property="og:image') || |
|||
getMetaContent('head meta[name="twitter:image:src"]'); |
|||
if (href) { |
|||
preview.images.push(href); |
|||
} |
|||
|
|||
href = getLinkHref('head link[rel="shortcut icon"]'); |
|||
if (href) { |
|||
preview.favicons.push(href); |
|||
} |
|||
|
|||
const keywords = getMetaContent('head meta[name="keywords"]'); |
|||
if (keywords) { |
|||
preview.tags = keywords.split(',').map((keyword) => keyword.trim()); |
|||
} |
|||
|
|||
const videoTags = document.querySelectorAll('head meta[property="og:video:tag"]'); |
|||
if (videoTags) { |
|||
videoTags.forEach((tag) => { |
|||
tag = tag.getAttribute('content'); |
|||
if (!tag) { |
|||
return; |
|||
} |
|||
tag = tag.trim().toLowerCase(); |
|||
if (!tag.length) { |
|||
return; |
|||
} |
|||
preview.tags.push(tag); |
|||
}); |
|||
} |
|||
|
|||
const icons = document.querySelectorAll('head link[rel="icon"]'); |
|||
if (icons) { |
|||
icons.forEach((icon) => { |
|||
preview.favicons.push(icon.getAttribute('href')); |
|||
}); |
|||
} |
|||
|
|||
//TODO: oEmbed spec allows for JSON and XML. May need to implement an XML
|
|||
// reader for `head link[rel="alternate"][type="text/xml+oembed"]`
|
|||
|
|||
preview.oembed = { }; |
|||
preview.oembed.href = getLinkHref('head link[type="application/json+oembed"]'); |
|||
if (preview.oembed.href) { |
|||
this.log.info('fetching oEmbed data for url', { url, href: preview.oembed.href }); |
|||
const json = await this.fetchOembedJson(preview.oembed.href); |
|||
|
|||
preview.oembed.version = json.version; |
|||
preview.oembed.type = json.type; |
|||
if (json.cache_age) { |
|||
preview.oembed.cache_age = json.cache_age; |
|||
} |
|||
preview.oembed.title = json.title; |
|||
preview.oembed.provider_name = json.provider_name; |
|||
preview.oembed.provider_url = json.provider_url; |
|||
preview.oembed.author_name = json.author_name; |
|||
preview.oembed.author_url = json.author_url; |
|||
preview.oembed.thumbnail_url = json.thumbnail_url; |
|||
preview.oembed.thumbnail_width = json.thumbnail_width; |
|||
preview.oembed.thumbnail_height = json.thumbnail_height; |
|||
|
|||
switch (json.type) { |
|||
case 'video': |
|||
preview.oembed.html = json.html; |
|||
preview.oembed.width = json.width; |
|||
preview.oembed.height = json.height; |
|||
break; |
|||
|
|||
case 'photo': |
|||
preview.oembed.url = json.url; |
|||
preview.oembed.width = json.width; |
|||
preview.oembed.height = json.height; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return preview; |
|||
} |
|||
|
|||
async fetchOembedJson (url) { |
|||
const response = await fetch(url); |
|||
const json = await response.json(); |
|||
return json; |
|||
} |
|||
|
|||
async loadUrlAsDOM (url, options) { |
|||
options = Object.assign({ |
|||
userAgent: this.userAgent, |
|||
acceptLanguage: 'en-US', |
|||
}, options); |
|||
|
|||
const response = await fetch(url, { |
|||
method: "GET", |
|||
headers: { |
|||
"User-Agent": options.userAgent, |
|||
"Accept-Language": options.acceptLanguage, |
|||
}, |
|||
}); |
|||
|
|||
const html = await response.text(); |
|||
const { window } = new JSDOM(html); |
|||
return { window, document: window.document }; |
|||
} |
|||
|
|||
async renderPreview (viewModel) { |
|||
return this.renderTemplate(this.templates.linkPreview, viewModel); |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
include preview |
|||
+renderLinkPreview(link) |
@ -0,0 +1,60 @@ |
|||
mixin renderLinkPreview (link, options) |
|||
- |
|||
options = Object.assign({ layout: 'responsive' }, options); |
|||
function proxyUrl (url) { |
|||
return `/image/proxy?url=${encodeURIComponent(url)}`; |
|||
} |
|||
|
|||
div(data-link-id= link._id).link-container |
|||
case link.mediaType |
|||
when 'video.other' |
|||
.link-preview |
|||
if !link.oembed |
|||
pre= JSON.stringify(link, null, 2) |
|||
else |
|||
if link.oembed.html |
|||
div!= link.oembed.html |
|||
else |
|||
.uk-margin-small |
|||
a(href= link.url, target="_blank", uk-tooltip={ title: `Watch ${link.title} on ${link.oembed.provider_name}` }).uk-link-reset |
|||
img(src= link.images[0]) |
|||
.uk-margin-small |
|||
.uk-text-lead.uk-text-truncate |
|||
a(href= link.url, target="_blank", uk-tooltip={ title: `Watch ${link.title} on ${link.oembed.provider_name}` }).uk-link-reset= link.title |
|||
.uk-text-small author: #[a(href= link.oembed.author_url, target="_blank", uk-tooltip={ title: `Visit ${link.oembed.author_name} on ${link.oembed.provider_name}` })= link.oembed.author_name] |
|||
.markdown-block!= marked.parse(dtpparse(link.oembed.description || link.description)) |
|||
|
|||
default |
|||
div(uk-grid).uk-grid-small.link-preview |
|||
if Array.isArray(link.images) && (link.images.length > 0) |
|||
div(class= (options.layout === 'responsive') ? "uk-width-1-1 uk-width-auto@m" : "uk-width-1-1", data-layout= options.layout) |
|||
a(href= link.url, |
|||
data-link-id= link._id, |
|||
onclick= "return dtp.app.visitLink(event);", |
|||
).uk-link-reset |
|||
img(src= proxyUrl(link.images[0])).link-thumbnail.uk-border-rounded |
|||
|
|||
div(class="uk-width-1-1 uk-width-expand@s") |
|||
.uk-margin-small |
|||
.uk-text-bold |
|||
a(ref= link.url, |
|||
data-link-id= link._id, |
|||
onclick= "return dtp.app.visitLink(event);", |
|||
).uk-link-reset= link.title |
|||
|
|||
.link-description.uk-text-small!= marked.parse(dtpparse(link.description)) |
|||
|
|||
.uk-flex.uk-flex-middle.uk-text-small |
|||
if Array.isArray(link.favicons) && (link.favicons.length > 0) |
|||
.uk-width-auto |
|||
img( |
|||
src= proxyUrl(link.favicons[0]), |
|||
style="height: 1em; width: auto;", |
|||
onerror=`this.src = '/img/icon/globe-icon.svg';`, |
|||
) |
|||
.uk-width-expand |
|||
.uk-margin-small-left |
|||
a(href=`//${link.domain}`, target="_blank", uk-tooltip={ title: link.mediaType })= link.siteName || link.domain |
|||
.uk-width-auto |
|||
.uk-margin-small-left |
|||
a(href=`/link/${link._id}/feed`, uk-tooltip={ title: 'Visit link' }) link feed |
@ -0,0 +1,64 @@ |
|||
extends ../layouts/main |
|||
block viewcss |
|||
link(rel='stylesheet', href=`/highlight.js/styles/obsidian.css?v=${pkg.version}`) |
|||
block content |
|||
|
|||
include ../member/components/status |
|||
include ../components/pagination-bar |
|||
include ../user/components/user-icon |
|||
|
|||
include components/preview |
|||
|
|||
section.uk-section.uk-section-default.uk-section-small |
|||
.uk-container |
|||
|
|||
h1 |
|||
div(uk-grid).uk-grid-small.uk-flex-middle |
|||
.uk-width-auto |
|||
+renderBackButton() |
|||
.uk-width-expand |
|||
span Link Timeline |
|||
|
|||
div(uk-grid) |
|||
div(class="uk-width-1-1 uk-width-1-3@s") |
|||
.uk-margin |
|||
+renderLinkPreview(link, { layout: 'sidebar' }) |
|||
|
|||
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded |
|||
.uk-card-body |
|||
.uk-margin-small |
|||
.uk-text-small Link URL |
|||
.uk-text-bold.uk-text-break= link.url |
|||
|
|||
.uk-margin-small |
|||
.uk-text-small Site |
|||
.uk-text-bold= link.siteName || link.domain |
|||
|
|||
.uk-margin-small |
|||
div(uk-grid).uk-grid-small |
|||
.uk-width-expand |
|||
.uk-text-small Last shared |
|||
.uk-text-bold= moment(link.lastShared).format('MMM DD, YYYY') |
|||
.uk-width-auto |
|||
.uk-text-small Shares |
|||
.uk-text-bold= formatCount(link.stats.shareCount) |
|||
.uk-width-auto |
|||
.uk-text-small Visits |
|||
.uk-text-bold= formatCount(link.stats.visitCount) |
|||
|
|||
.uk-margin-small |
|||
.uk-text-small Submitted by: |
|||
div(uk-grid).uk-grid-small |
|||
each submitter in link.submittedBy |
|||
.uk-width-auto |
|||
a(href=`/member/${submitter.username}`, uk-tooltip={ title: submitter.displayName || submitter.username }) |
|||
+renderUserIcon(submitter) |
|||
|
|||
div(class="uk-width-1-1 uk-width-2-3@s") |
|||
if Array.isArray(timeline.statuses) && (timeline.statuses.length > 0) |
|||
each status in timeline.statuses |
|||
+renderStatus(status, { statusToken, commentToken }) |
|||
else |
|||
.uk-text-center #{site.name} has no remaining posts sharing this link. |
|||
|
|||
+renderPaginationBar(timelineUrl, timeline.totalStatusCount) |
@ -0,0 +1,808 @@ |
|||
// host-services.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import 'dotenv/config'; |
|||
|
|||
import os from 'node:os'; |
|||
import path, { dirname } from 'path'; |
|||
import fs from 'node:fs'; |
|||
|
|||
import diskusage from 'diskusage-ng'; |
|||
import sysinfo from 'systeminformation'; |
|||
|
|||
import si from 'systeminformation'; |
|||
|
|||
import dgram from 'node:dgram'; |
|||
|
|||
import { SiteRuntime, SiteAsync, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
import { CronJob } from 'cron'; |
|||
|
|||
import { createRequire } from 'module'; |
|||
const require = createRequire(import.meta.url); // jshint ignore:line
|
|||
|
|||
const CRON_TIMEZONE = 'America/New_York'; |
|||
|
|||
class CacheStats { |
|||
|
|||
constructor ( ) { |
|||
this.itemCount = 0; |
|||
this.dataSize = 0; |
|||
|
|||
this.expireCount = 0; |
|||
this.expireDataSize = 0; |
|||
|
|||
this.hitCount = 0; |
|||
this.hitDataSize = 0; |
|||
|
|||
this.missCount = 0; |
|||
this.missDataSize = 0; |
|||
} |
|||
|
|||
add (size) { |
|||
this.itemCount += 1; |
|||
this.dataSize += (size / 1024.0); |
|||
} |
|||
|
|||
remove (size) { |
|||
this.itemCount -= 1; |
|||
this.dataSize -= size / 1024.0; |
|||
} |
|||
|
|||
hit (size) { |
|||
this.hitCount += 1; |
|||
this.hitDataSize += (size / 1024.0); |
|||
} |
|||
|
|||
miss (size) { |
|||
this.missCount += 1; |
|||
this.missDataSize += (size / 1024.0); |
|||
} |
|||
|
|||
expire (size) { |
|||
this.expireCount += 1; |
|||
this.expireDataSize += size; |
|||
} |
|||
|
|||
report ( ) { |
|||
const report = { |
|||
itemCount: this.itemCount, |
|||
dataSize: this.dataSize, |
|||
|
|||
expireCount: this.expireCount, |
|||
expireDataSize: this.expireDataSize, |
|||
|
|||
hitCount: this.hitCount, |
|||
hitDataSize: this.hitDataSize, |
|||
|
|||
missCount: this.missCount, |
|||
missDataSize: this.missDataSize, |
|||
}; |
|||
|
|||
this.resetCounters(); |
|||
|
|||
return report; |
|||
} |
|||
|
|||
resetCounters ( ) { |
|||
this.expireCount = 0; |
|||
this.expireDataSize = 0; |
|||
|
|||
this.hitCount = 0; |
|||
this.hitDataSize = 0; |
|||
|
|||
this.missCount = 0; |
|||
this.missDataSize = 0; |
|||
} |
|||
} |
|||
|
|||
class HostCacheTransaction { |
|||
|
|||
get tid ( ) { return this.message.tid; } |
|||
get cmd ( ) { return this.message.cmd; } |
|||
get params ( ) { return this.message.params; } |
|||
|
|||
get address ( ) { return this.rinfo.address; } |
|||
get port ( ) { return this.rinfo.port; } |
|||
get size ( ) { return this.rinfo.size; } |
|||
|
|||
constructor (dtp, message, rinfo) { |
|||
this.dtp = dtp; |
|||
this.created = Date.now(); // timestamp, not Date instance
|
|||
|
|||
this.message = message; |
|||
this.rinfo = rinfo; |
|||
|
|||
this.flags = { |
|||
isFetched: false, |
|||
isCached: false, |
|||
isResolved: false, |
|||
isError: false, |
|||
}; |
|||
} |
|||
|
|||
async getFile ( ) { |
|||
const { minio: minioService } = this.dtp.services; |
|||
const filePath = path.join( |
|||
process.env.HOST_CACHE_PATH, |
|||
this.params.bucket, |
|||
this.params.key, |
|||
); |
|||
const res = { |
|||
cmd: this.cmd, |
|||
success: true, |
|||
message: undefined, |
|||
file: { |
|||
stats: undefined, |
|||
path: undefined, |
|||
}, |
|||
}; |
|||
|
|||
try { |
|||
res.file.stats = await fs.promises.stat(filePath); |
|||
if (!res.file.stats.isFile()) { |
|||
throw new SiteError(500, 'invalid object requested'); |
|||
} |
|||
res.file.path = filePath; |
|||
this.flags.isCached = true; |
|||
this.dtp.cacheStats.hit(res.file.stats.size); |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} catch (error) { |
|||
if (error.code !== 'ENOENT') { |
|||
this.dtp.log.error('failed to stat requested object', { transaction: this, error }); |
|||
res.success = false; |
|||
res.statusCode = 500; |
|||
res.message = error.message; |
|||
this.error = error; |
|||
this.flags.isError = true; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
// fall through to MinIO fetch since file not found in cache
|
|||
} |
|||
|
|||
try { |
|||
await minioService.downloadFile({ |
|||
bucket: this.params.bucket, |
|||
key: this.params.key, |
|||
filePath, |
|||
}); |
|||
res.file.path = filePath; |
|||
res.file.stats = await fs.promises.stat(filePath); |
|||
if (!res.file.stats.isFile()) { |
|||
throw new SiteError(500, 'invalid object requested'); |
|||
} |
|||
this.flags.isFetched = true; |
|||
this.dtp.cacheStats.add(res.file.stats.size); |
|||
this.dtp.cacheStats.miss(res.file.stats.size); |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} catch (error) { |
|||
if (error.code !== 'NotFound') { |
|||
this.dtp.log.error('failed to fetch requested object from MinIO', { transaction: this, error }); |
|||
res.success = false; |
|||
res.statusCode = 500; |
|||
res.message = error.message; |
|||
this.error = error; |
|||
this.flags.isError = true; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
} |
|||
|
|||
res.success = false; |
|||
res.statusCode = 404; |
|||
res.message = 'Not Found'; |
|||
this.error = new SiteError(404, 'Not Found'); |
|||
this.flags.isError = true; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
|
|||
/** |
|||
* Will fetch the contents of a URL to local storage with a separate JSON |
|||
* metadata file to describe the data file. |
|||
*/ |
|||
async fetchUrl ( ) { |
|||
const { crypto: cryptoService } = this.dtp.services; |
|||
|
|||
const res = { |
|||
cmd: this.cmd, |
|||
success: true, |
|||
message: undefined, |
|||
file: { |
|||
stats: undefined, |
|||
path: undefined, |
|||
}, |
|||
}; |
|||
|
|||
const urlHash = cryptoService.createHash(this.params.url, 'sha256'); |
|||
const basePath = path.join(process.env.HOST_CACHE_PATH, 'web-resource', urlHash.slice(0, 4)); |
|||
await fs.promises.mkdir(basePath, { recursive: true }); |
|||
|
|||
const resourceFilename = path.join(basePath, `${urlHash}.dat`); |
|||
const resourceMetaFilename = path.join(basePath, `${urlHash}.json`); |
|||
|
|||
/* |
|||
* Try first to read from local storage. If not found, proceed to fetch |
|||
* and store logic below. |
|||
*/ |
|||
try { |
|||
res.file.stats = await fs.promises.stat(resourceFilename); |
|||
if (!res.file.stats.isFile()) { |
|||
throw new SiteError(500, 'invalid object requested'); |
|||
} |
|||
res.file.path = resourceFilename; |
|||
res.file.meta = require(resourceMetaFilename); |
|||
this.flags.isCached = true; |
|||
this.dtp.cacheStats.hit(res.file.stats.size); |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} catch (error) { |
|||
if (error.code !== 'ENOENT') { |
|||
this.dtp.log.error('failed to stat requested object', { transaction: this, error }); |
|||
res.success = false; |
|||
res.statusCode = 500; |
|||
res.message = error.message; |
|||
this.error = error; |
|||
this.flags.isError = true; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
// fall through to HTTP fetch since file not found in cache
|
|||
} |
|||
|
|||
/* |
|||
* HTTP fetch of URL to retrieve the resource from its origin source. |
|||
* |
|||
* It is commonly advised and good practice to operate these fetches through |
|||
* an HTTP Proxy to prevent exposing your origin server IP. |
|||
*/ |
|||
try { |
|||
const response = await fetch(this.params.url); |
|||
if (!response.ok) { |
|||
this.error = new Error('Failed to fetch URL'); |
|||
this.flags.isError = true; |
|||
this.dtp.log.error(this.error.message, { transaction: this, status: response.status }); |
|||
|
|||
res.success = false; |
|||
res.statusCode = response.status; |
|||
res.message = this.error.message; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
|
|||
/* |
|||
* Set up to receive binary data stream as the resource file contents. |
|||
*/ |
|||
|
|||
let contentType = response.headers.get('content-type'); |
|||
let contentSize = response.headers.get('content-length'); |
|||
if (contentSize) { |
|||
contentSize = parseInt(contentSize, 10); |
|||
} |
|||
|
|||
this.dtp.log.debug('writing initial meta file', { resourceMetaFilename }); |
|||
await fs.promises.writeFile(resourceMetaFilename, JSON.stringify({ contentType, contentSize })); |
|||
|
|||
this.dtp.log.info('writing web resource file', resourceFilename); |
|||
let writeStream = fs.createWriteStream(resourceFilename, { |
|||
autoClose: true, |
|||
encoding: 'binary', |
|||
}); |
|||
|
|||
writeStream.on('close', async ( ) => { |
|||
res.file.path = resourceFilename; |
|||
res.file.stats = await fs.promises.stat(resourceFilename); |
|||
if (!res.file.stats.isFile()) { |
|||
throw new SiteError(500, 'invalid object requested'); |
|||
} |
|||
|
|||
// now that it's our own file, ensure that contentSize reflects our answer
|
|||
contentSize = res.file.stats.size; |
|||
res.file.meta = { contentType, contentSize }; |
|||
|
|||
this.dtp.log.debug('writing meta file', { resourceMetaFilename }); |
|||
await fs.promises.writeFile(resourceMetaFilename, JSON.stringify(res.file.meta)); |
|||
|
|||
this.flags.isFetched = true; |
|||
this.dtp.cacheStats.add(res.file.stats.size); |
|||
this.dtp.cacheStats.miss(res.file.stats.size); |
|||
|
|||
this.dtp.manager.resolveTransaction(this, res); |
|||
}); |
|||
|
|||
const { Readable } = await import('stream'); |
|||
Readable.fromWeb(response.body).pipe(writeStream); |
|||
} catch (error) { |
|||
this.error = error; |
|||
this.flags.isError = true; |
|||
this.dtp.log.error(this.error.message, { transaction: this, error }); |
|||
|
|||
res.success = false; |
|||
res.statusCode = error.statusCode || 500; |
|||
res.message = this.error.message; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
} |
|||
|
|||
async cancel (reason) { |
|||
const res = { |
|||
res: this.cmd, |
|||
success: false, |
|||
message: `Operation canceled: ${reason}`, |
|||
}; |
|||
return this.dtp.manager.resolveTransaction(this, res); |
|||
} |
|||
|
|||
async sendResponse (res) { |
|||
const NOW = Date.now(); |
|||
const duration = this.duration = (NOW - this.created) / 1000.0; |
|||
|
|||
const { flags } = this; |
|||
flags.isResolved = true; |
|||
|
|||
const payload = { tid: this.tid, res, flags, duration }; |
|||
const reply = Buffer.from(JSON.stringify(payload)); |
|||
this.dtp.server.send(reply, this.port, this.address); |
|||
} |
|||
} |
|||
|
|||
class TransactionManager { |
|||
|
|||
constructor (dtp) { |
|||
this.dtp = dtp; |
|||
this.transactions = { }; |
|||
} |
|||
|
|||
async addTransaction (transaction) { |
|||
if (this.hasPendingRequest(transaction)) { |
|||
this.transactions[transaction.tid] = transaction; // queue it and be done
|
|||
return; |
|||
} |
|||
|
|||
this.transactions[transaction.tid] = transaction; // queue it and process the command
|
|||
switch (transaction.cmd) { |
|||
case 'getFile': |
|||
return transaction.getFile(); |
|||
|
|||
case 'fetchUrl': |
|||
return transaction.fetchUrl(); |
|||
|
|||
default: |
|||
break; // unknown/undefined command
|
|||
} |
|||
|
|||
this.dtp.log.error('invalid host-services command', { |
|||
cmd: transaction.cmd, |
|||
params: transaction.params, |
|||
from: { |
|||
address: transaction.address, |
|||
port: transaction.port, |
|||
}, |
|||
}); |
|||
await this.dtp.manager.cancelTransaction(transaction, 'Rejected'); |
|||
} |
|||
|
|||
hasPendingRequest (transaction) { |
|||
if (!transaction) { return false; } |
|||
const keys = Object.keys(this.transactions); |
|||
const match = keys.find((key) => { |
|||
const cmp = this.transactions[key]; |
|||
if (!cmp) { return false; } |
|||
if (cmp.cmd !== transaction.cmd) { return false; } |
|||
if (cmp.params.bucket !== transaction.params.bucket) { return false; } |
|||
if (cmp.params.key !== transaction.params.key) { return false; } |
|||
return true; |
|||
}); |
|||
return !!match; |
|||
} |
|||
|
|||
async resolveTransaction (transaction, res) { |
|||
await transaction.sendResponse(res); |
|||
this.removeTransaction(transaction, 'resolved'); |
|||
|
|||
const removed = [ ]; |
|||
for (const key in this.transactions) { |
|||
const t = this.transactions[key]; |
|||
if (!t) { |
|||
delete this.transactions[key]; |
|||
return; |
|||
} |
|||
if ((transaction.params.bucket === t.params.bucket) && (transaction.params.key === t.params.key)) { |
|||
await t.sendResponse(res); |
|||
removed.push(t); |
|||
} |
|||
} |
|||
|
|||
for (const t of removed) { |
|||
this.removeTransaction(t, 'resolved'); |
|||
} |
|||
} |
|||
|
|||
async cancelTransaction (transaction, reason) { |
|||
await transaction.cancel(); |
|||
this.removeTransaction(transaction, reason); |
|||
} |
|||
|
|||
removeTransaction (transaction) { |
|||
if (this.transactions[transaction.tid]) { |
|||
delete this.transactions[transaction.tid]; |
|||
} |
|||
} |
|||
|
|||
async expireTransactions ( ) { |
|||
const NOW = Date.now(); |
|||
let expired = 0; |
|||
|
|||
for (const key in this.transactions) { |
|||
const transaction = this.transactions[key]; |
|||
const age = NOW - transaction.created; |
|||
if (age > (1000 * 30)) { |
|||
this.dtp.log.alert('expiring transaction', { transaction }); |
|||
await this.cancelTransaction(transaction, 'expired'); |
|||
++expired; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
class SiteHostServices extends SiteRuntime { |
|||
|
|||
static get name ( ) { return 'SiteHostServices'; } |
|||
static get slug ( ) { return 'hostServices'; } |
|||
|
|||
constructor (rootPath) { |
|||
super(SiteHostServices, rootPath); |
|||
} |
|||
|
|||
async start (basePath) { |
|||
await super.start(); |
|||
this.config.hostCache = { |
|||
host: process.env.HOST_CACHE_HOST || 'localhost', |
|||
port: parseInt(process.env.HOST_CACHE_PORT || '8000', 10), |
|||
}; |
|||
|
|||
basePath = basePath || process.env.HOST_CACHE_PATH; |
|||
this.log.info('ensuring host-services path exists', { basePath }); |
|||
await fs.promises.mkdir(basePath, { recursive: true }); |
|||
await this.cleanHostCache(basePath); |
|||
|
|||
this.networkStats = await si.networkStats('*'); |
|||
|
|||
this.log.info('starting cache service', { basePath }); |
|||
this.cacheStats = new CacheStats(); |
|||
|
|||
/* |
|||
* Host Cache server socket setup |
|||
*/ |
|||
this.log.info('creating server UDP socket'); |
|||
this.server = dgram.createSocket('udp4', this.onHostCacheMessage.bind(this)); |
|||
this.log.info('binding server UDP socket', { |
|||
port: this.config.hostCache.port, |
|||
host: this.config.hostCache.host, |
|||
}); |
|||
this.server.bind(this.config.hostCache.port, this.config.hostCache.host); |
|||
|
|||
this.manager = new TransactionManager(this); |
|||
this.expireJob = new CronJob( |
|||
'*/5 * * * * *', |
|||
this.expireTransactions.bind(this), |
|||
null, |
|||
true, |
|||
CRON_TIMEZONE, |
|||
); |
|||
|
|||
const cleanCronJob = process.env.HOST_CACHE_CLEAN_CRON || '*/30 * * * * *'; |
|||
this.log.info('starting host cache clean cron', { cleanCronJob }); |
|||
this.cleanupJob = new CronJob( |
|||
cleanCronJob, |
|||
this.cleanHostCache.bind(this), |
|||
null, |
|||
true, |
|||
CRON_TIMEZONE, |
|||
); |
|||
|
|||
this.log.info('starting stats report job'); |
|||
this.statsReportJob = new CronJob( |
|||
'*/5 * * * * *', |
|||
this.reportHostStats.bind(this), |
|||
null, |
|||
true, |
|||
CRON_TIMEZONE, |
|||
); |
|||
|
|||
this.log.info('starting host expiration job'); |
|||
this.expireHostsJob = new CronJob( |
|||
'*/20 * * * * *', |
|||
this.expireNetHosts.bind(this), |
|||
null, |
|||
true, |
|||
CRON_TIMEZONE, |
|||
); |
|||
|
|||
this.log.info('registering host with Site platform'); |
|||
await this.registerHost(); |
|||
await this.setHostStatus('active'); |
|||
|
|||
this.log.info(`${this.config.pkg.name} v${this.config.pkg.version} ${SiteHostServices.name} started`); |
|||
} |
|||
|
|||
async shutdown ( ) { |
|||
await this.setHostStatus('shutdown'); |
|||
} |
|||
|
|||
async onHostCacheMessage (message, rinfo) { |
|||
try { |
|||
message = message.toString('utf8'); |
|||
message = JSON.parse(message); |
|||
|
|||
const transaction = new HostCacheTransaction(this, message, rinfo); |
|||
this.manager.addTransaction(transaction); |
|||
} catch (error) { |
|||
this.log.error('failed to receive UDP message', { message, error }); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* When a file is accessed for read or otherwise, it's atime is updated. If the |
|||
* atime of a file exceeds the configured max file idle time, the file is |
|||
* removed. |
|||
* @param {String} basePath |
|||
*/ |
|||
async cleanHostCache (basePath) { |
|||
const NOW = Date.now(); // timestamp, not Date instance
|
|||
basePath = basePath || process.env.HOST_CACHE_PATH; |
|||
|
|||
const dir = await fs.promises.opendir(basePath); |
|||
for await (const dirent of dir) { |
|||
if (dirent.isDirectory()) { |
|||
await this.cleanHostCache(path.join(basePath, dirent.name)); |
|||
} |
|||
if (dirent.isFile()) { |
|||
const filePath = path.join(basePath, dirent.name); |
|||
const stats = await fs.promises.stat(filePath); |
|||
const age = NOW - stats.atime.valueOf(); |
|||
if ((age / 1000.0 / 60.0) > 60.0) { |
|||
await fs.promises.rm(filePath, { force: true }); |
|||
this.cacheStats.remove(stats.size); |
|||
this.cacheStats.expire(stats.size); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
async expireTransactions ( ) { |
|||
await this.manager.expireTransactions(); |
|||
} |
|||
|
|||
async registerHost ( ) { |
|||
const NOW = new Date(); |
|||
|
|||
const mongoose = await import('mongoose'); |
|||
const NetHost = mongoose.model('NetHost'); |
|||
|
|||
const memory = await si.mem(); |
|||
|
|||
this.host = new NetHost(); |
|||
this.host.created = NOW; |
|||
this.host.status = 'starting'; |
|||
this.host.hostname = os.hostname(); |
|||
|
|||
this.host.arch = os.arch(); |
|||
this.host.cpus = os.cpus().map((cpu) => { |
|||
return { |
|||
model: cpu.model, |
|||
speed: cpu.speed, |
|||
}; |
|||
}); |
|||
|
|||
this.host.totalmem = memory.total; |
|||
this.host.freemem = memory.available; |
|||
|
|||
this.host.node = process.version; |
|||
this.host.platform = os.platform(); |
|||
this.host.release = os.release(); |
|||
this.host.version = os.version(); |
|||
|
|||
this.host.network = (await si.networkInterfaces()).map((iface) => { |
|||
return { |
|||
iface: iface.iface, |
|||
speed: iface.speed, |
|||
mac: iface.mac, |
|||
ip4: iface.ip4, |
|||
ip4subnet: iface.ip4subnet, |
|||
ip6: iface.ip6, |
|||
ip6subnet: iface.ip6subnet, |
|||
flags: { |
|||
internal: iface.internal, |
|||
virtual: iface.virtual, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
await this.host.save(); |
|||
this.host = this.host.toObject(); |
|||
|
|||
return this.host; |
|||
} |
|||
|
|||
async reportHostStats ( ) { |
|||
const NOW = new Date(); |
|||
|
|||
const mongoose = await import('mongoose'); |
|||
const NetHost = mongoose.model('NetHost'); |
|||
const NetHostStats = mongoose.model('NetHostStats'); |
|||
|
|||
const memory = await this.reportMemoryInformation(); |
|||
const network = await this.reportNetworkInformation(); |
|||
|
|||
const load = this.reportLoadAvg(); |
|||
const cache = this.cacheStats.report(); |
|||
const disk = { |
|||
cache: await this.reportDiskUsage(process.env.HOST_CACHE_PATH), |
|||
}; |
|||
|
|||
const newCpuTimes = this.reportCpuTimes(); |
|||
const cpuDeltas = this.reportCpuTimeDeltas(this.oldCpuTimes, newCpuTimes); |
|||
this.oldCpuTimes = newCpuTimes; |
|||
|
|||
await NetHostStats.create({ |
|||
created: NOW, |
|||
host: this.host._id, |
|||
load, |
|||
cpus: cpuDeltas, |
|||
memory, cache, disk, network, |
|||
}); |
|||
await NetHost.updateOne( |
|||
{ _id: this.host._id }, |
|||
{ |
|||
updated: NOW, |
|||
freemem: memory.available, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async setHostStatus (status) { |
|||
if (!this.host) { |
|||
return; |
|||
} |
|||
|
|||
const NOW = new Date(); |
|||
const mongoose = await import('mongoose'); |
|||
const NetHost = mongoose.model('NetHost'); |
|||
|
|||
await NetHost.updateOne( |
|||
{ _id: this.host._id }, |
|||
{ |
|||
$set: { |
|||
updated: NOW, |
|||
status, |
|||
}, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async expireNetHosts ( ) { |
|||
const NOW = new Date(); |
|||
const OLDEST = new Date(Date.now() - 1000 * 60 * 2); |
|||
|
|||
const mongoose = await import('mongoose'); |
|||
const NetHost = mongoose.model('NetHost'); |
|||
|
|||
const hosts = await NetHost.find({ |
|||
status: { $nin: ['inactive', 'crashed'] }, |
|||
updated: { $lt: OLDEST } |
|||
}); |
|||
|
|||
await SiteAsync.each(hosts, async (host) => { |
|||
try { |
|||
await NetHost.updateOne( |
|||
{ _id: host._id }, |
|||
{ |
|||
$set: { |
|||
updated: NOW, |
|||
status: 'crashed', |
|||
}, |
|||
}, |
|||
); |
|||
} catch (error) { |
|||
this.log.error('failed to clean expired host', { host, error }); |
|||
} |
|||
}, 4); |
|||
} |
|||
|
|||
async reportMemoryInformation ( ) { |
|||
return sysinfo.mem(); |
|||
} |
|||
|
|||
async reportNetworkInformation ( ) { |
|||
return (await sysinfo.networkStats('*')).map((iface) => { |
|||
const record = { iface: iface.iface }; |
|||
|
|||
record.rxDropped = iface.rx_dropped; |
|||
record.rxErrors = iface.rx_errors; |
|||
|
|||
record.txDropped = iface.tx_dropped; |
|||
record.txErrors = iface.tx_errors; |
|||
|
|||
if (iface.ms !== 0) { |
|||
record.rxPerSecond = iface.rx_sec / (iface.ms / 1000.0); |
|||
record.txPerSecond = iface.tx_sec / (iface.ms / 1000.0); |
|||
} else { |
|||
record.rxPerSecond = 0; |
|||
record.txPerSecond = 0; |
|||
} |
|||
|
|||
return record; |
|||
}); |
|||
} |
|||
|
|||
reportDiskUsage (pathname) { |
|||
return new Promise((resolve, reject) => { |
|||
diskusage(pathname, (err, usage) => { |
|||
if (err) { |
|||
return reject(err); |
|||
} |
|||
usage.pctUsed = (usage.used / usage.total) * 100.0; |
|||
return resolve(usage); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
reportLoadAvg ( ) { |
|||
return os.loadavg(); |
|||
} |
|||
|
|||
reportCpuTimes ( ) { |
|||
return os |
|||
.cpus() |
|||
.map((cpu) => { |
|||
return { |
|||
user: cpu.times.user, |
|||
nice: cpu.times.nice, |
|||
sys: cpu.times.sys, |
|||
idle: cpu.times.idle, |
|||
irq: cpu.times.irq, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
reportCpuTimeDeltas (oldCpuStats, newCpuStats) { |
|||
const cpuDeltas = [ ]; |
|||
if (oldCpuStats) { |
|||
for (let idx = 0; idx < newCpuStats.length; ++idx) { |
|||
cpuDeltas.push({ |
|||
user: newCpuStats[idx].user - oldCpuStats[idx].user, |
|||
nice: newCpuStats[idx].nice - oldCpuStats[idx].nice, |
|||
sys: newCpuStats[idx].sys - oldCpuStats[idx].sys, |
|||
idle: newCpuStats[idx].idle - oldCpuStats[idx].idle, |
|||
irq: newCpuStats[idx].irq - oldCpuStats[idx].irq, |
|||
}); |
|||
} |
|||
} else { |
|||
newCpuStats.forEach(( ) => { |
|||
cpuDeltas.push({ |
|||
user: 0, |
|||
nice: 0, |
|||
sys: 0, |
|||
idle: 0, |
|||
irq: 0, |
|||
}); |
|||
}); |
|||
} |
|||
return cpuDeltas; |
|||
} |
|||
} |
|||
|
|||
(async ( ) => { |
|||
|
|||
try { |
|||
const { fileURLToPath } = await import('node:url'); |
|||
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
|||
|
|||
const worker = new SiteHostServices(path.resolve(__dirname, '..', '..')); |
|||
await worker.start(); |
|||
|
|||
} catch (error) { |
|||
console.error('failed to start Host Cache worker', { error }); |
|||
process.exit(-1); |
|||
} |
|||
|
|||
})(); |
@ -1211,6 +1211,11 @@ |
|||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" |
|||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== |
|||
|
|||
"@types/luxon@~3.4.0": |
|||
version "3.4.2" |
|||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" |
|||
integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA== |
|||
|
|||
"@types/ms@*": |
|||
version "0.7.34" |
|||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" |
|||
@ -1433,6 +1438,13 @@ acorn@^8.7.1, acorn@^8.8.2: |
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" |
|||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== |
|||
|
|||
agent-base@^7.0.2, agent-base@^7.1.0: |
|||
version "7.1.1" |
|||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" |
|||
integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== |
|||
dependencies: |
|||
debug "^4.3.4" |
|||
|
|||
ajv-formats@^2.1.1: |
|||
version "2.1.1" |
|||
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" |
|||
@ -1597,6 +1609,11 @@ async@^3.2.3, async@^3.2.4: |
|||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" |
|||
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== |
|||
|
|||
asynckit@^0.4.0: |
|||
version "0.4.0" |
|||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" |
|||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== |
|||
|
|||
at-least-node@^1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" |
|||
@ -2022,6 +2039,13 @@ colorette@^2.0.10, colorette@^2.0.14: |
|||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" |
|||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== |
|||
|
|||
combined-stream@^1.0.8: |
|||
version "1.0.8" |
|||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" |
|||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== |
|||
dependencies: |
|||
delayed-stream "~1.0.0" |
|||
|
|||
commander@^10.0.1: |
|||
version "10.0.1" |
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" |
|||
@ -2171,6 +2195,14 @@ cron-parser@^4.2.1: |
|||
dependencies: |
|||
luxon "^3.2.1" |
|||
|
|||
cron@^3.1.7: |
|||
version "3.1.7" |
|||
resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.7.tgz#3423d618ba625e78458fff8cb67001672d49ba0d" |
|||
integrity sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw== |
|||
dependencies: |
|||
"@types/luxon" "~3.4.0" |
|||
luxon "~3.4.0" |
|||
|
|||
cropperjs@^1.6.1: |
|||
version "1.6.1" |
|||
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.6.1.tgz#fd132021d93b824b1b0f2c2c3b763419fb792d89" |
|||
@ -2209,11 +2241,26 @@ cssesc@^3.0.0: |
|||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" |
|||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== |
|||
|
|||
cssstyle@^4.0.1: |
|||
version "4.0.1" |
|||
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a" |
|||
integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ== |
|||
dependencies: |
|||
rrweb-cssom "^0.6.0" |
|||
|
|||
data-uri-to-buffer@^4.0.0: |
|||
version "4.0.1" |
|||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" |
|||
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== |
|||
|
|||
data-urls@^5.0.0: |
|||
version "5.0.0" |
|||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" |
|||
integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== |
|||
dependencies: |
|||
whatwg-mimetype "^4.0.0" |
|||
whatwg-url "^14.0.0" |
|||
|
|||
data-view-buffer@^1.0.1: |
|||
version "1.0.1" |
|||
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" |
|||
@ -2258,7 +2305,7 @@ [email protected], debug@^2.2.0: |
|||
dependencies: |
|||
ms "2.0.0" |
|||
|
|||
[email protected], debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: |
|||
debug@4, [email protected], debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: |
|||
version "4.3.4" |
|||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" |
|||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== |
|||
@ -2277,6 +2324,11 @@ decamelize@^1.2.0: |
|||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" |
|||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== |
|||
|
|||
decimal.js@^10.4.3: |
|||
version "10.4.3" |
|||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" |
|||
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== |
|||
|
|||
decode-uri-component@^0.2.2: |
|||
version "0.2.2" |
|||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" |
|||
@ -2305,6 +2357,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: |
|||
has-property-descriptors "^1.0.0" |
|||
object-keys "^1.1.1" |
|||
|
|||
delayed-stream@~1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" |
|||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== |
|||
|
|||
denque@^2.1.0: |
|||
version "2.1.0" |
|||
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" |
|||
@ -2345,6 +2402,11 @@ dijkstrajs@^1.0.1: |
|||
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" |
|||
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== |
|||
|
|||
diskusage-ng@^1.0.4: |
|||
version "1.0.4" |
|||
resolved "https://registry.yarnpkg.com/diskusage-ng/-/diskusage-ng-1.0.4.tgz#20eee47d647304b6031f743974b29edc328da52f" |
|||
integrity sha512-30qT0Bn2dNGii6dAXllGgYRaIt+7gPhwCJks+byhutkCOCgsqfqvXmAV2zTDOvU1ylnGHUjZMMaKhM95FyQQrg== |
|||
|
|||
disposable-email-provider-domains@^1.0.9: |
|||
version "1.0.9" |
|||
resolved "https://registry.yarnpkg.com/disposable-email-provider-domains/-/disposable-email-provider-domains-1.0.9.tgz#0ac18ca5477a8d5e6f7f53c5862de8f0dcdee055" |
|||
@ -2515,6 +2577,11 @@ entities@^2.0.0: |
|||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" |
|||
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== |
|||
|
|||
entities@^4.4.0: |
|||
version "4.5.0" |
|||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" |
|||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== |
|||
|
|||
envinfo@^7.7.3: |
|||
version "7.11.1" |
|||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.1.tgz#2ffef77591057081b0129a8fd8cf6118da1b94e1" |
|||
@ -2870,6 +2937,15 @@ for-each@^0.3.3: |
|||
dependencies: |
|||
is-callable "^1.1.3" |
|||
|
|||
form-data@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" |
|||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== |
|||
dependencies: |
|||
asynckit "^0.4.0" |
|||
combined-stream "^1.0.8" |
|||
mime-types "^2.1.12" |
|||
|
|||
formdata-polyfill@^4.0.10: |
|||
version "4.0.10" |
|||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" |
|||
@ -3090,6 +3166,13 @@ highlight.js@^11.9.0: |
|||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" |
|||
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== |
|||
|
|||
html-encoding-sniffer@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" |
|||
integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== |
|||
dependencies: |
|||
whatwg-encoding "^3.1.1" |
|||
|
|||
[email protected]: |
|||
version "3.8.3" |
|||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" |
|||
@ -3122,6 +3205,14 @@ http-errors@~1.6.2: |
|||
setprototypeof "1.1.0" |
|||
statuses ">= 1.4.0 < 2" |
|||
|
|||
http-proxy-agent@^7.0.0: |
|||
version "7.0.2" |
|||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" |
|||
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== |
|||
dependencies: |
|||
agent-base "^7.1.0" |
|||
debug "^4.3.4" |
|||
|
|||
http-proxy@^1.18.1: |
|||
version "1.18.1" |
|||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" |
|||
@ -3131,6 +3222,14 @@ http-proxy@^1.18.1: |
|||
follow-redirects "^1.0.0" |
|||
requires-port "^1.0.0" |
|||
|
|||
https-proxy-agent@^7.0.2: |
|||
version "7.0.4" |
|||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" |
|||
integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== |
|||
dependencies: |
|||
agent-base "^7.0.2" |
|||
debug "4" |
|||
|
|||
[email protected]: |
|||
version "0.4.24" |
|||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" |
|||
@ -3138,7 +3237,7 @@ [email protected]: |
|||
dependencies: |
|||
safer-buffer ">= 2.1.2 < 3" |
|||
|
|||
iconv-lite@^0.6.3: |
|||
[email protected], iconv-lite@^0.6.3: |
|||
version "0.6.3" |
|||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" |
|||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== |
|||
@ -3384,6 +3483,11 @@ is-plain-object@^2.0.4: |
|||
dependencies: |
|||
isobject "^3.0.1" |
|||
|
|||
is-potential-custom-element-name@^1.0.1: |
|||
version "1.0.1" |
|||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" |
|||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== |
|||
|
|||
is-promise@^2.0.0: |
|||
version "2.2.2" |
|||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" |
|||
@ -3520,6 +3624,33 @@ js-tokens@^4.0.0: |
|||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" |
|||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== |
|||
|
|||
jsdom@^24.0.0: |
|||
version "24.0.0" |
|||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.0.0.tgz#e2dc04e4c79da368481659818ee2b0cd7c39007c" |
|||
integrity sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A== |
|||
dependencies: |
|||
cssstyle "^4.0.1" |
|||
data-urls "^5.0.0" |
|||
decimal.js "^10.4.3" |
|||
form-data "^4.0.0" |
|||
html-encoding-sniffer "^4.0.0" |
|||
http-proxy-agent "^7.0.0" |
|||
https-proxy-agent "^7.0.2" |
|||
is-potential-custom-element-name "^1.0.1" |
|||
nwsapi "^2.2.7" |
|||
parse5 "^7.1.2" |
|||
rrweb-cssom "^0.6.0" |
|||
saxes "^6.0.0" |
|||
symbol-tree "^3.2.4" |
|||
tough-cookie "^4.1.3" |
|||
w3c-xmlserializer "^5.0.0" |
|||
webidl-conversions "^7.0.0" |
|||
whatwg-encoding "^3.1.1" |
|||
whatwg-mimetype "^4.0.0" |
|||
whatwg-url "^14.0.0" |
|||
ws "^8.16.0" |
|||
xml-name-validator "^5.0.0" |
|||
|
|||
jsesc@^2.5.1: |
|||
version "2.5.2" |
|||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" |
|||
@ -3661,6 +3792,11 @@ lodash.clone@^4.3.2, lodash.clone@^4.5.0: |
|||
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" |
|||
integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== |
|||
|
|||
lodash.clonedeep@^4.5.0: |
|||
version "4.5.0" |
|||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" |
|||
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== |
|||
|
|||
lodash.debounce@^4.0.8: |
|||
version "4.0.8" |
|||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" |
|||
@ -3710,7 +3846,7 @@ lru-cache@^6.0.0: |
|||
dependencies: |
|||
yallist "^4.0.0" |
|||
|
|||
luxon@^3.2.1: |
|||
luxon@^3.2.1, luxon@~3.4.0: |
|||
version "3.4.4" |
|||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" |
|||
integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== |
|||
@ -3802,7 +3938,7 @@ [email protected]: |
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" |
|||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== |
|||
|
|||
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: |
|||
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: |
|||
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== |
|||
@ -4110,6 +4246,11 @@ numeral@^2.0.6: |
|||
resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" |
|||
integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== |
|||
|
|||
nwsapi@^2.2.7: |
|||
version "2.2.7" |
|||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" |
|||
integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== |
|||
|
|||
object-assign@^4, object-assign@^4.1.1: |
|||
version "4.1.1" |
|||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" |
|||
@ -4208,6 +4349,13 @@ parse-node-version@^1.0.0, parse-node-version@^1.0.1: |
|||
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" |
|||
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== |
|||
|
|||
parse5@^7.1.2: |
|||
version "7.1.2" |
|||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" |
|||
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== |
|||
dependencies: |
|||
entities "^4.4.0" |
|||
|
|||
parseurl@~1.3.2, parseurl@~1.3.3: |
|||
version "1.3.3" |
|||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" |
|||
@ -4399,6 +4547,11 @@ prr@~1.0.1: |
|||
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" |
|||
integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== |
|||
|
|||
psl@^1.1.33: |
|||
version "1.9.0" |
|||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" |
|||
integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== |
|||
|
|||
pstree.remy@^1.1.8: |
|||
version "1.1.8" |
|||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" |
|||
@ -4507,7 +4660,7 @@ pug@^3.0.2: |
|||
pug-runtime "^3.0.1" |
|||
pug-strip-comments "^2.0.0" |
|||
|
|||
punycode@^2.1.0, punycode@^2.3.0: |
|||
punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0, punycode@^2.3.1: |
|||
version "2.3.1" |
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" |
|||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== |
|||
@ -4539,6 +4692,11 @@ query-string@^7.1.3: |
|||
split-on-first "^1.0.0" |
|||
strict-uri-encode "^2.0.0" |
|||
|
|||
querystringify@^2.1.1: |
|||
version "2.2.0" |
|||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" |
|||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== |
|||
|
|||
randexp@^0.5.3: |
|||
version "0.5.3" |
|||
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738" |
|||
@ -4776,6 +4934,11 @@ rotating-file-stream@^3.2.1: |
|||
resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-3.2.1.tgz#1d0a536d75884eedc3a677f5b0871fdc69f97d22" |
|||
integrity sha512-n2B18CJb+n2VA5Tdle+1NP2toEcRv68CjAOBjHmwcyswNwMVsrN3gVRZ9ymH3sapaiGY8jc9OhhV5b6I5rAeiA== |
|||
|
|||
rrweb-cssom@^0.6.0: |
|||
version "0.6.0" |
|||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" |
|||
integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== |
|||
|
|||
[email protected]: |
|||
version "4.1.0" |
|||
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" |
|||
@ -4820,6 +4983,13 @@ sax@>=0.6.0, sax@^1.2.4: |
|||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" |
|||
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== |
|||
|
|||
saxes@^6.0.0: |
|||
version "6.0.0" |
|||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" |
|||
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== |
|||
dependencies: |
|||
xmlchars "^2.2.0" |
|||
|
|||
schema-utils@^3.1.1, schema-utils@^3.2.0: |
|||
version "3.3.0" |
|||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" |
|||
@ -5327,6 +5497,16 @@ svg-captcha@^1.4.0: |
|||
dependencies: |
|||
opentype.js "^0.7.3" |
|||
|
|||
symbol-tree@^3.2.4: |
|||
version "3.2.4" |
|||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" |
|||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== |
|||
|
|||
systeminformation@^5.22.7: |
|||
version "5.22.7" |
|||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.22.7.tgz#9a20810c7eacad4aebe7591cb7c78c0dd96dbd1a" |
|||
integrity sha512-AWxlP05KeHbpGdgvZkcudJpsmChc2Y5Eo/GvxG/iUA/Aws5LZKHAMSeAo+V+nD+nxWZaxrwpWcnx4SH3oxNL3A== |
|||
|
|||
tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: |
|||
version "2.2.1" |
|||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" |
|||
@ -5436,6 +5616,16 @@ touch@^3.1.0: |
|||
dependencies: |
|||
nopt "~1.0.10" |
|||
|
|||
tough-cookie@^4.1.3: |
|||
version "4.1.3" |
|||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" |
|||
integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== |
|||
dependencies: |
|||
psl "^1.1.33" |
|||
punycode "^2.1.1" |
|||
universalify "^0.2.0" |
|||
url-parse "^1.5.3" |
|||
|
|||
tr46@^1.0.1: |
|||
version "1.0.1" |
|||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" |
|||
@ -5450,6 +5640,13 @@ tr46@^4.1.1: |
|||
dependencies: |
|||
punycode "^2.3.0" |
|||
|
|||
tr46@^5.0.0: |
|||
version "5.0.0" |
|||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" |
|||
integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== |
|||
dependencies: |
|||
punycode "^2.3.1" |
|||
|
|||
tslib@^2.0.0, tslib@^2.3.0: |
|||
version "2.6.2" |
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" |
|||
@ -5594,6 +5791,11 @@ universalify@^0.1.0: |
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" |
|||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== |
|||
|
|||
universalify@^0.2.0: |
|||
version "0.2.0" |
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" |
|||
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== |
|||
|
|||
universalify@^2.0.0: |
|||
version "2.0.1" |
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" |
|||
@ -5631,6 +5833,21 @@ uri-js@^4.2.2: |
|||
dependencies: |
|||
punycode "^2.1.0" |
|||
|
|||
url-parse@^1.5.3: |
|||
version "1.5.10" |
|||
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" |
|||
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== |
|||
dependencies: |
|||
querystringify "^2.1.1" |
|||
requires-port "^1.0.0" |
|||
|
|||
user-agents@^1.1.174: |
|||
version "1.1.174" |
|||
resolved "https://registry.yarnpkg.com/user-agents/-/user-agents-1.1.174.tgz#9bccbcf48fd68fc7523e4003882e0e5ad577ce5a" |
|||
integrity sha512-S4WyQZTs85TOJ2ABEF+0K9mT9sp3j2SsXEiYODOCwNNWnuacheNsngLV1zGNkmLyCE2D5e4a1/f0DaStIFQQRA== |
|||
dependencies: |
|||
lodash.clonedeep "^4.5.0" |
|||
|
|||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |
|||
@ -5684,6 +5901,13 @@ void-elements@^3.1.0: |
|||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" |
|||
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== |
|||
|
|||
w3c-xmlserializer@^5.0.0: |
|||
version "5.0.0" |
|||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" |
|||
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== |
|||
dependencies: |
|||
xml-name-validator "^5.0.0" |
|||
|
|||
watchpack@^2.4.1: |
|||
version "2.4.1" |
|||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" |
|||
@ -5813,6 +6037,18 @@ webpack@^5.91.0: |
|||
watchpack "^2.4.1" |
|||
webpack-sources "^3.2.3" |
|||
|
|||
whatwg-encoding@^3.1.1: |
|||
version "3.1.1" |
|||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" |
|||
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== |
|||
dependencies: |
|||
iconv-lite "0.6.3" |
|||
|
|||
whatwg-mimetype@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" |
|||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== |
|||
|
|||
whatwg-url@^13.0.0: |
|||
version "13.0.0" |
|||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-13.0.0.tgz#b7b536aca48306394a34e44bda8e99f332410f8f" |
|||
@ -5821,6 +6057,14 @@ whatwg-url@^13.0.0: |
|||
tr46 "^4.1.1" |
|||
webidl-conversions "^7.0.0" |
|||
|
|||
whatwg-url@^14.0.0: |
|||
version "14.0.0" |
|||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" |
|||
integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== |
|||
dependencies: |
|||
tr46 "^5.0.0" |
|||
webidl-conversions "^7.0.0" |
|||
|
|||
whatwg-url@^7.0.0: |
|||
version "7.1.0" |
|||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" |
|||
@ -6072,11 +6316,21 @@ wrappy@1: |
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" |
|||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== |
|||
|
|||
ws@^8.16.0: |
|||
version "8.16.0" |
|||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" |
|||
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== |
|||
|
|||
ws@~8.11.0: |
|||
version "8.11.0" |
|||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" |
|||
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== |
|||
|
|||
xml-name-validator@^5.0.0: |
|||
version "5.0.0" |
|||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" |
|||
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== |
|||
|
|||
xml2js@^0.5.0: |
|||
version "0.5.0" |
|||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7" |
|||
@ -6095,6 +6349,11 @@ xmlbuilder@~11.0.0: |
|||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" |
|||
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== |
|||
|
|||
xmlchars@^2.2.0: |
|||
version "2.2.0" |
|||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" |
|||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== |
|||
|
|||
xmlhttprequest-ssl@~2.0.0: |
|||
version "2.0.0" |
|||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" |
|||
|
Loading…
Reference in new issue