Streaming: replace npmlog with pino & pino-http (#27828)
This commit is contained in:
parent
f866413e72
commit
1335083bed
5 changed files with 593 additions and 252 deletions
|
@ -15,7 +15,18 @@ module.exports = defineConfig({
|
||||||
ecmaVersion: 2021,
|
ecmaVersion: 2021,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
// In the streaming server we need to delete some variables to ensure
|
||||||
|
// garbage collection takes place on the values referenced by those objects;
|
||||||
|
// The alternative is to declare the variable as nullable, but then we need
|
||||||
|
// to assert it's in existence before every use, which becomes much harder
|
||||||
|
// to maintain.
|
||||||
|
'no-delete-var': 'off',
|
||||||
|
|
||||||
|
// The streaming server is written in commonjs, not ESM for now:
|
||||||
'import/no-commonjs': 'off',
|
'import/no-commonjs': 'off',
|
||||||
|
|
||||||
|
// This overrides the base configuration for this rule to pick up
|
||||||
|
// dependencies for the streaming server from the correct package.json file.
|
||||||
'import/no-extraneous-dependencies': [
|
'import/no-extraneous-dependencies': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,12 +10,11 @@ const dotenv = require('dotenv');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const Redis = require('ioredis');
|
const Redis = require('ioredis');
|
||||||
const { JSDOM } = require('jsdom');
|
const { JSDOM } = require('jsdom');
|
||||||
const log = require('npmlog');
|
|
||||||
const pg = require('pg');
|
const pg = require('pg');
|
||||||
const dbUrlToConfig = require('pg-connection-string').parse;
|
const dbUrlToConfig = require('pg-connection-string').parse;
|
||||||
const uuid = require('uuid');
|
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } = require('./logging');
|
||||||
const { setupMetrics } = require('./metrics');
|
const { setupMetrics } = require('./metrics');
|
||||||
const { isTruthy } = require("./utils");
|
const { isTruthy } = require("./utils");
|
||||||
|
|
||||||
|
@ -28,15 +27,30 @@ dotenv.config({
|
||||||
path: path.resolve(__dirname, path.join('..', dotenvFile))
|
path: path.resolve(__dirname, path.join('..', dotenvFile))
|
||||||
});
|
});
|
||||||
|
|
||||||
log.level = process.env.LOG_LEVEL || 'verbose';
|
initializeLogLevel(process.env, environment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares the result type for accountFromToken / accountFromRequest.
|
||||||
|
*
|
||||||
|
* Note: This is here because jsdoc doesn't like importing types that
|
||||||
|
* are nested in functions
|
||||||
|
* @typedef ResolvedAccount
|
||||||
|
* @property {string} accessTokenId
|
||||||
|
* @property {string[]} scopes
|
||||||
|
* @property {string} accountId
|
||||||
|
* @property {string[]} chosenLanguages
|
||||||
|
* @property {string} deviceId
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object.<string, any>} config
|
* @param {Object.<string, any>} config
|
||||||
*/
|
*/
|
||||||
const createRedisClient = async (config) => {
|
const createRedisClient = async (config) => {
|
||||||
const { redisParams, redisUrl } = config;
|
const { redisParams, redisUrl } = config;
|
||||||
|
// @ts-ignore
|
||||||
const client = new Redis(redisUrl, redisParams);
|
const client = new Redis(redisUrl, redisParams);
|
||||||
client.on('error', (err) => log.error('Redis Client Error!', err));
|
// @ts-ignore
|
||||||
|
client.on('error', (err) => logger.error({ err }, 'Redis Client Error!'));
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
@ -61,12 +75,12 @@ const parseJSON = (json, req) => {
|
||||||
*/
|
*/
|
||||||
if (req) {
|
if (req) {
|
||||||
if (req.accountId) {
|
if (req.accountId) {
|
||||||
log.warn(req.requestId, `Error parsing message from user ${req.accountId}: ${err}`);
|
req.log.error({ err }, `Error parsing message from user ${req.accountId}`);
|
||||||
} else {
|
} else {
|
||||||
log.silly(req.requestId, `Error parsing message from ${req.remoteAddress}: ${err}`);
|
req.log.error({ err }, `Error parsing message from ${req.remoteAddress}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn(`Error parsing message from redis: ${err}`);
|
logger.error({ err }, `Error parsing message from redis`);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +119,7 @@ const pgConfigFromEnv = (env) => {
|
||||||
baseConfig.password = env.DB_PASS;
|
baseConfig.password = env.DB_PASS;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
baseConfig = pgConfigs[environment];
|
baseConfig = pgConfigs[environment];
|
||||||
|
|
||||||
if (env.DB_SSLMODE) {
|
if (env.DB_SSLMODE) {
|
||||||
|
@ -149,6 +164,7 @@ const redisConfigFromEnv = (env) => {
|
||||||
|
|
||||||
// redisParams.path takes precedence over host and port.
|
// redisParams.path takes precedence over host and port.
|
||||||
if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
|
if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
|
||||||
|
// @ts-ignore
|
||||||
redisParams.path = env.REDIS_URL.slice(7);
|
redisParams.path = env.REDIS_URL.slice(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +211,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal');
|
app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal');
|
||||||
|
|
||||||
|
app.use(httpLogger);
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// Handle eventsource & other http requests:
|
// Handle eventsource & other http requests:
|
||||||
|
@ -202,32 +219,37 @@ const startServer = async () => {
|
||||||
|
|
||||||
// Handle upgrade requests:
|
// Handle upgrade requests:
|
||||||
server.on('upgrade', async function handleUpgrade(request, socket, head) {
|
server.on('upgrade', async function handleUpgrade(request, socket, head) {
|
||||||
|
// Setup the HTTP logger, since websocket upgrades don't get the usual http
|
||||||
|
// logger. This decorates the `request` object.
|
||||||
|
attachWebsocketHttpLogger(request);
|
||||||
|
|
||||||
|
request.log.info("HTTP Upgrade Requested");
|
||||||
|
|
||||||
/** @param {Error} err */
|
/** @param {Error} err */
|
||||||
const onSocketError = (err) => {
|
const onSocketError = (err) => {
|
||||||
log.error(`Error with websocket upgrade: ${err}`);
|
request.log.error({ error: err }, err.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('error', onSocketError);
|
socket.on('error', onSocketError);
|
||||||
|
|
||||||
// Authenticate:
|
/** @type {ResolvedAccount} */
|
||||||
try {
|
let resolvedAccount;
|
||||||
await accountFromRequest(request);
|
|
||||||
} catch (err) {
|
|
||||||
log.error(`Error authenticating request: ${err}`);
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolvedAccount = await accountFromRequest(request);
|
||||||
|
} catch (err) {
|
||||||
// Unfortunately for using the on('upgrade') setup, we need to manually
|
// Unfortunately for using the on('upgrade') setup, we need to manually
|
||||||
// write a HTTP Response to the Socket to close the connection upgrade
|
// write a HTTP Response to the Socket to close the connection upgrade
|
||||||
// attempt, so the following code is to handle all of that.
|
// attempt, so the following code is to handle all of that.
|
||||||
const statusCode = err.status ?? 401;
|
const statusCode = err.status ?? 401;
|
||||||
|
|
||||||
/** @type {Record<string, string | number>} */
|
/** @type {Record<string, string | number | import('pino-http').ReqId>} */
|
||||||
const headers = {
|
const headers = {
|
||||||
'Connection': 'close',
|
'Connection': 'close',
|
||||||
'Content-Type': 'text/plain',
|
'Content-Type': 'text/plain',
|
||||||
'Content-Length': 0,
|
'Content-Length': 0,
|
||||||
'X-Request-Id': request.id,
|
'X-Request-Id': request.id,
|
||||||
// TODO: Send the error message via header so it can be debugged in
|
'X-Error-Message': err.status ? err.toString() : 'An unexpected error occurred'
|
||||||
// developer tools
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure the socket is closed once we've finished writing to it:
|
// Ensure the socket is closed once we've finished writing to it:
|
||||||
|
@ -238,15 +260,28 @@ const startServer = async () => {
|
||||||
// Write the HTTP response manually:
|
// Write the HTTP response manually:
|
||||||
socket.end(`HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode]}\r\n${Object.keys(headers).map((key) => `${key}: ${headers[key]}`).join('\r\n')}\r\n\r\n`);
|
socket.end(`HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode]}\r\n${Object.keys(headers).map((key) => `${key}: ${headers[key]}`).join('\r\n')}\r\n\r\n`);
|
||||||
|
|
||||||
|
// Finally, log the error:
|
||||||
|
request.log.error({
|
||||||
|
err,
|
||||||
|
res: {
|
||||||
|
statusCode,
|
||||||
|
headers
|
||||||
|
}
|
||||||
|
}, err.toString());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the error handler, wss.handleUpgrade has its own:
|
||||||
|
socket.removeListener('error', onSocketError);
|
||||||
|
|
||||||
wss.handleUpgrade(request, socket, head, function done(ws) {
|
wss.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
// Remove the error handler:
|
request.log.info("Authenticated request & upgraded to WebSocket connection");
|
||||||
socket.removeListener('error', onSocketError);
|
|
||||||
|
const wsLogger = createWebsocketLogger(request, resolvedAccount);
|
||||||
|
|
||||||
// Start the connection:
|
// Start the connection:
|
||||||
wss.emit('connection', ws, request);
|
wss.emit('connection', ws, request, wsLogger);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -273,9 +308,9 @@ const startServer = async () => {
|
||||||
// When checking metrics in the browser, the favicon is requested this
|
// When checking metrics in the browser, the favicon is requested this
|
||||||
// prevents the request from falling through to the API Router, which would
|
// prevents the request from falling through to the API Router, which would
|
||||||
// error for this endpoint:
|
// error for this endpoint:
|
||||||
app.get('/favicon.ico', (req, res) => res.status(404).end());
|
app.get('/favicon.ico', (_req, res) => res.status(404).end());
|
||||||
|
|
||||||
app.get('/api/v1/streaming/health', (req, res) => {
|
app.get('/api/v1/streaming/health', (_req, res) => {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
res.end('OK');
|
res.end('OK');
|
||||||
});
|
});
|
||||||
|
@ -285,7 +320,7 @@ const startServer = async () => {
|
||||||
res.set('Content-Type', metrics.register.contentType);
|
res.set('Content-Type', metrics.register.contentType);
|
||||||
res.end(await metrics.register.metrics());
|
res.end(await metrics.register.metrics());
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.error(ex);
|
req.log.error(ex);
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -319,7 +354,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
const callbacks = subs[channel];
|
const callbacks = subs[channel];
|
||||||
|
|
||||||
log.silly(`New message on channel ${redisPrefix}${channel}`);
|
logger.debug(`New message on channel ${redisPrefix}${channel}`);
|
||||||
|
|
||||||
if (!callbacks) {
|
if (!callbacks) {
|
||||||
return;
|
return;
|
||||||
|
@ -343,17 +378,16 @@ const startServer = async () => {
|
||||||
* @param {SubscriptionListener} callback
|
* @param {SubscriptionListener} callback
|
||||||
*/
|
*/
|
||||||
const subscribe = (channel, callback) => {
|
const subscribe = (channel, callback) => {
|
||||||
log.silly(`Adding listener for ${channel}`);
|
logger.debug(`Adding listener for ${channel}`);
|
||||||
|
|
||||||
subs[channel] = subs[channel] || [];
|
subs[channel] = subs[channel] || [];
|
||||||
|
|
||||||
if (subs[channel].length === 0) {
|
if (subs[channel].length === 0) {
|
||||||
log.verbose(`Subscribe ${channel}`);
|
logger.debug(`Subscribe ${channel}`);
|
||||||
redisSubscribeClient.subscribe(channel, (err, count) => {
|
redisSubscribeClient.subscribe(channel, (err, count) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error(`Error subscribing to ${channel}`);
|
logger.error(`Error subscribing to ${channel}`);
|
||||||
}
|
} else if (typeof count === 'number') {
|
||||||
else {
|
|
||||||
redisSubscriptions.set(count);
|
redisSubscriptions.set(count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -367,7 +401,7 @@ const startServer = async () => {
|
||||||
* @param {SubscriptionListener} callback
|
* @param {SubscriptionListener} callback
|
||||||
*/
|
*/
|
||||||
const unsubscribe = (channel, callback) => {
|
const unsubscribe = (channel, callback) => {
|
||||||
log.silly(`Removing listener for ${channel}`);
|
logger.debug(`Removing listener for ${channel}`);
|
||||||
|
|
||||||
if (!subs[channel]) {
|
if (!subs[channel]) {
|
||||||
return;
|
return;
|
||||||
|
@ -376,12 +410,11 @@ const startServer = async () => {
|
||||||
subs[channel] = subs[channel].filter(item => item !== callback);
|
subs[channel] = subs[channel].filter(item => item !== callback);
|
||||||
|
|
||||||
if (subs[channel].length === 0) {
|
if (subs[channel].length === 0) {
|
||||||
log.verbose(`Unsubscribe ${channel}`);
|
logger.debug(`Unsubscribe ${channel}`);
|
||||||
redisSubscribeClient.unsubscribe(channel, (err, count) => {
|
redisSubscribeClient.unsubscribe(channel, (err, count) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error(`Error unsubscribing to ${channel}`);
|
logger.error(`Error unsubscribing to ${channel}`);
|
||||||
}
|
} else if (typeof count === 'number') {
|
||||||
else {
|
|
||||||
redisSubscriptions.set(count);
|
redisSubscriptions.set(count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -390,45 +423,13 @@ const startServer = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req
|
* @param {http.IncomingMessage & ResolvedAccount} req
|
||||||
* @param {any} res
|
|
||||||
* @param {function(Error=): void} next
|
|
||||||
*/
|
|
||||||
const setRequestId = (req, res, next) => {
|
|
||||||
req.requestId = uuid.v4();
|
|
||||||
res.header('X-Request-Id', req.requestId);
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} req
|
|
||||||
* @param {any} res
|
|
||||||
* @param {function(Error=): void} next
|
|
||||||
*/
|
|
||||||
const setRemoteAddress = (req, res, next) => {
|
|
||||||
req.remoteAddress = req.connection.remoteAddress;
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} req
|
|
||||||
* @param {string[]} necessaryScopes
|
* @param {string[]} necessaryScopes
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
const isInScope = (req, necessaryScopes) =>
|
const isInScope = (req, necessaryScopes) =>
|
||||||
req.scopes.some(scope => necessaryScopes.includes(scope));
|
req.scopes.some(scope => necessaryScopes.includes(scope));
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef ResolvedAccount
|
|
||||||
* @property {string} accessTokenId
|
|
||||||
* @property {string[]} scopes
|
|
||||||
* @property {string} accountId
|
|
||||||
* @property {string[]} chosenLanguages
|
|
||||||
* @property {string} deviceId
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
* @param {any} req
|
* @param {any} req
|
||||||
|
@ -441,6 +442,7 @@ const startServer = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||||
done();
|
done();
|
||||||
|
|
||||||
|
@ -451,6 +453,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
err = new Error('Invalid access token');
|
err = new Error('Invalid access token');
|
||||||
|
// @ts-ignore
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
|
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -485,6 +488,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
if (!authorization && !accessToken) {
|
if (!authorization && !accessToken) {
|
||||||
const err = new Error('Missing access token');
|
const err = new Error('Missing access token');
|
||||||
|
// @ts-ignore
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
|
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -529,15 +533,16 @@ const startServer = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req
|
* @param {http.IncomingMessage & ResolvedAccount} req
|
||||||
|
* @param {import('pino').Logger} logger
|
||||||
* @param {string|undefined} channelName
|
* @param {string|undefined} channelName
|
||||||
* @returns {Promise.<void>}
|
* @returns {Promise.<void>}
|
||||||
*/
|
*/
|
||||||
const checkScopes = (req, channelName) => new Promise((resolve, reject) => {
|
const checkScopes = (req, logger, channelName) => new Promise((resolve, reject) => {
|
||||||
log.silly(req.requestId, `Checking OAuth scopes for ${channelName}`);
|
logger.debug(`Checking OAuth scopes for ${channelName}`);
|
||||||
|
|
||||||
// When accessing public channels, no scopes are needed
|
// When accessing public channels, no scopes are needed
|
||||||
if (PUBLIC_CHANNELS.includes(channelName)) {
|
if (channelName && PUBLIC_CHANNELS.includes(channelName)) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -564,6 +569,7 @@ const startServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const err = new Error('Access token does not cover required scopes');
|
const err = new Error('Access token does not cover required scopes');
|
||||||
|
// @ts-ignore
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
|
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -577,38 +583,40 @@ const startServer = async () => {
|
||||||
/**
|
/**
|
||||||
* @param {any} req
|
* @param {any} req
|
||||||
* @param {SystemMessageHandlers} eventHandlers
|
* @param {SystemMessageHandlers} eventHandlers
|
||||||
* @returns {function(object): void}
|
* @returns {SubscriptionListener}
|
||||||
*/
|
*/
|
||||||
const createSystemMessageListener = (req, eventHandlers) => {
|
const createSystemMessageListener = (req, eventHandlers) => {
|
||||||
return message => {
|
return message => {
|
||||||
|
if (!message?.event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { event } = message;
|
const { event } = message;
|
||||||
|
|
||||||
log.silly(req.requestId, `System message for ${req.accountId}: ${event}`);
|
req.log.debug(`System message for ${req.accountId}: ${event}`);
|
||||||
|
|
||||||
if (event === 'kill') {
|
if (event === 'kill') {
|
||||||
log.verbose(req.requestId, `Closing connection for ${req.accountId} due to expired access token`);
|
req.log.debug(`Closing connection for ${req.accountId} due to expired access token`);
|
||||||
eventHandlers.onKill();
|
eventHandlers.onKill();
|
||||||
} else if (event === 'filters_changed') {
|
} else if (event === 'filters_changed') {
|
||||||
log.verbose(req.requestId, `Invalidating filters cache for ${req.accountId}`);
|
req.log.debug(`Invalidating filters cache for ${req.accountId}`);
|
||||||
req.cachedFilters = null;
|
req.cachedFilters = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req
|
* @param {http.IncomingMessage & ResolvedAccount} req
|
||||||
* @param {any} res
|
* @param {http.OutgoingMessage} res
|
||||||
*/
|
*/
|
||||||
const subscribeHttpToSystemChannel = (req, res) => {
|
const subscribeHttpToSystemChannel = (req, res) => {
|
||||||
const accessTokenChannelId = `timeline:access_token:${req.accessTokenId}`;
|
const accessTokenChannelId = `timeline:access_token:${req.accessTokenId}`;
|
||||||
const systemChannelId = `timeline:system:${req.accountId}`;
|
const systemChannelId = `timeline:system:${req.accountId}`;
|
||||||
|
|
||||||
const listener = createSystemMessageListener(req, {
|
const listener = createSystemMessageListener(req, {
|
||||||
|
|
||||||
onKill() {
|
onKill() {
|
||||||
res.end();
|
res.end();
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('close', () => {
|
res.on('close', () => {
|
||||||
|
@ -641,13 +649,14 @@ const startServer = async () => {
|
||||||
// the connection, as there's nothing to stream back
|
// the connection, as there's nothing to stream back
|
||||||
if (!channelName) {
|
if (!channelName) {
|
||||||
const err = new Error('Unknown channel requested');
|
const err = new Error('Unknown channel requested');
|
||||||
|
// @ts-ignore
|
||||||
err.status = 400;
|
err.status = 400;
|
||||||
|
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountFromRequest(req).then(() => checkScopes(req, channelName)).then(() => {
|
accountFromRequest(req).then(() => checkScopes(req, req.log, channelName)).then(() => {
|
||||||
subscribeHttpToSystemChannel(req, res);
|
subscribeHttpToSystemChannel(req, res);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
next();
|
next();
|
||||||
|
@ -663,22 +672,28 @@ const startServer = async () => {
|
||||||
* @param {function(Error=): void} next
|
* @param {function(Error=): void} next
|
||||||
*/
|
*/
|
||||||
const errorMiddleware = (err, req, res, next) => {
|
const errorMiddleware = (err, req, res, next) => {
|
||||||
log.error(req.requestId, err.toString());
|
req.log.error({ err }, err.toString());
|
||||||
|
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeHead(err.status || 500, { 'Content-Type': 'application/json' });
|
const hasStatusCode = Object.hasOwnProperty.call(err, 'status');
|
||||||
res.end(JSON.stringify({ error: err.status ? err.toString() : 'An unexpected error occurred' }));
|
// @ts-ignore
|
||||||
|
const statusCode = hasStatusCode ? err.status : 500;
|
||||||
|
const errorMessage = hasStatusCode ? err.toString() : 'An unexpected error occurred';
|
||||||
|
|
||||||
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: errorMessage }));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {array} arr
|
* @param {any[]} arr
|
||||||
* @param {number=} shift
|
* @param {number=} shift
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore
|
||||||
const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
|
const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -695,6 +710,7 @@ const startServer = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [listId], (err, result) => {
|
client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [listId], (err, result) => {
|
||||||
done();
|
done();
|
||||||
|
|
||||||
|
@ -709,34 +725,43 @@ const startServer = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[]} ids
|
* @param {string[]} channelIds
|
||||||
* @param {any} req
|
* @param {http.IncomingMessage & ResolvedAccount} req
|
||||||
|
* @param {import('pino').Logger} log
|
||||||
* @param {function(string, string): void} output
|
* @param {function(string, string): void} output
|
||||||
* @param {undefined | function(string[], SubscriptionListener): void} attachCloseHandler
|
* @param {undefined | function(string[], SubscriptionListener): void} attachCloseHandler
|
||||||
* @param {'websocket' | 'eventsource'} destinationType
|
* @param {'websocket' | 'eventsource'} destinationType
|
||||||
* @param {boolean=} needsFiltering
|
* @param {boolean=} needsFiltering
|
||||||
* @returns {SubscriptionListener}
|
* @returns {SubscriptionListener}
|
||||||
*/
|
*/
|
||||||
const streamFrom = (ids, req, output, attachCloseHandler, destinationType, needsFiltering = false) => {
|
const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, needsFiltering = false) => {
|
||||||
const accountId = req.accountId || req.remoteAddress;
|
log.info({ channelIds }, `Starting stream`);
|
||||||
|
|
||||||
log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} event
|
||||||
|
* @param {object|string} payload
|
||||||
|
*/
|
||||||
const transmit = (event, payload) => {
|
const transmit = (event, payload) => {
|
||||||
// TODO: Replace "string"-based delete payloads with object payloads:
|
// TODO: Replace "string"-based delete payloads with object payloads:
|
||||||
const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
|
const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
|
||||||
|
|
||||||
messagesSent.labels({ type: destinationType }).inc(1);
|
messagesSent.labels({ type: destinationType }).inc(1);
|
||||||
|
|
||||||
log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload}`);
|
log.debug({ event, payload }, `Transmitting ${event} to ${req.accountId}`);
|
||||||
|
|
||||||
output(event, encodedPayload);
|
output(event, encodedPayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
// The listener used to process each message off the redis subscription,
|
// The listener used to process each message off the redis subscription,
|
||||||
// message here is an object with an `event` and `payload` property. Some
|
// message here is an object with an `event` and `payload` property. Some
|
||||||
// events also include a queued_at value, but this is being removed shortly.
|
// events also include a queued_at value, but this is being removed shortly.
|
||||||
|
|
||||||
/** @type {SubscriptionListener} */
|
/** @type {SubscriptionListener} */
|
||||||
const listener = message => {
|
const listener = message => {
|
||||||
|
if (!message?.event || !message?.payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { event, payload } = message;
|
const { event, payload } = message;
|
||||||
|
|
||||||
// Streaming only needs to apply filtering to some channels and only to
|
// Streaming only needs to apply filtering to some channels and only to
|
||||||
|
@ -759,7 +784,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
// Filter based on language:
|
// Filter based on language:
|
||||||
if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) {
|
if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) {
|
||||||
log.silly(req.requestId, `Message ${payload.id} filtered by language (${payload.language})`);
|
log.debug(`Message ${payload.id} filtered by language (${payload.language})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -770,6 +795,7 @@ const startServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter based on domain blocks, blocks, mutes, or custom filters:
|
// Filter based on domain blocks, blocks, mutes, or custom filters:
|
||||||
|
// @ts-ignore
|
||||||
const targetAccountIds = [payload.account.id].concat(payload.mentions.map(item => item.id));
|
const targetAccountIds = [payload.account.id].concat(payload.mentions.map(item => item.id));
|
||||||
const accountDomain = payload.account.acct.split('@')[1];
|
const accountDomain = payload.account.acct.split('@')[1];
|
||||||
|
|
||||||
|
@ -781,6 +807,7 @@ const startServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const queries = [
|
const queries = [
|
||||||
|
// @ts-ignore
|
||||||
client.query(`SELECT 1
|
client.query(`SELECT 1
|
||||||
FROM blocks
|
FROM blocks
|
||||||
WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)}))
|
WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)}))
|
||||||
|
@ -793,10 +820,13 @@ const startServer = async () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (accountDomain) {
|
if (accountDomain) {
|
||||||
|
// @ts-ignore
|
||||||
queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
|
queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
if (!payload.filtered && !req.cachedFilters) {
|
if (!payload.filtered && !req.cachedFilters) {
|
||||||
|
// @ts-ignore
|
||||||
queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, keyword.keyword AS keyword, keyword.whole_word AS whole_word FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId]));
|
queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, keyword.keyword AS keyword, keyword.whole_word AS whole_word FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -819,9 +849,11 @@ const startServer = async () => {
|
||||||
|
|
||||||
// Handling for constructing the custom filters and caching them on the request
|
// Handling for constructing the custom filters and caching them on the request
|
||||||
// TODO: Move this logic out of the message handling lifecycle
|
// TODO: Move this logic out of the message handling lifecycle
|
||||||
|
// @ts-ignore
|
||||||
if (!req.cachedFilters) {
|
if (!req.cachedFilters) {
|
||||||
const filterRows = values[accountDomain ? 2 : 1].rows;
|
const filterRows = values[accountDomain ? 2 : 1].rows;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
req.cachedFilters = filterRows.reduce((cache, filter) => {
|
req.cachedFilters = filterRows.reduce((cache, filter) => {
|
||||||
if (cache[filter.id]) {
|
if (cache[filter.id]) {
|
||||||
cache[filter.id].keywords.push([filter.keyword, filter.whole_word]);
|
cache[filter.id].keywords.push([filter.keyword, filter.whole_word]);
|
||||||
|
@ -851,7 +883,9 @@ const startServer = async () => {
|
||||||
// needs to be done in a separate loop as the database returns one
|
// needs to be done in a separate loop as the database returns one
|
||||||
// filterRow per keyword, so we need all the keywords before
|
// filterRow per keyword, so we need all the keywords before
|
||||||
// constructing the regular expression
|
// constructing the regular expression
|
||||||
|
// @ts-ignore
|
||||||
Object.keys(req.cachedFilters).forEach((key) => {
|
Object.keys(req.cachedFilters).forEach((key) => {
|
||||||
|
// @ts-ignore
|
||||||
req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => {
|
req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => {
|
||||||
let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
@ -872,13 +906,16 @@ const startServer = async () => {
|
||||||
|
|
||||||
// Apply cachedFilters against the payload, constructing a
|
// Apply cachedFilters against the payload, constructing a
|
||||||
// `filter_results` array of FilterResult entities
|
// `filter_results` array of FilterResult entities
|
||||||
|
// @ts-ignore
|
||||||
if (req.cachedFilters) {
|
if (req.cachedFilters) {
|
||||||
const status = payload;
|
const status = payload;
|
||||||
// TODO: Calculate searchableContent in Ruby on Rails:
|
// TODO: Calculate searchableContent in Ruby on Rails:
|
||||||
|
// @ts-ignore
|
||||||
const searchableContent = ([status.spoiler_text || '', status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchableContent = ([status.spoiler_text || '', status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const searchableTextContent = JSDOM.fragment(searchableContent).textContent;
|
const searchableTextContent = JSDOM.fragment(searchableContent).textContent;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
// @ts-ignore
|
||||||
const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => {
|
const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => {
|
||||||
// Check the filter hasn't expired before applying:
|
// Check the filter hasn't expired before applying:
|
||||||
if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) {
|
if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) {
|
||||||
|
@ -926,12 +963,12 @@ const startServer = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ids.forEach(id => {
|
channelIds.forEach(id => {
|
||||||
subscribe(`${redisPrefix}${id}`, listener);
|
subscribe(`${redisPrefix}${id}`, listener);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof attachCloseHandler === 'function') {
|
if (typeof attachCloseHandler === 'function') {
|
||||||
attachCloseHandler(ids.map(id => `${redisPrefix}${id}`), listener);
|
attachCloseHandler(channelIds.map(id => `${redisPrefix}${id}`), listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
return listener;
|
return listener;
|
||||||
|
@ -943,8 +980,6 @@ const startServer = async () => {
|
||||||
* @returns {function(string, string): void}
|
* @returns {function(string, string): void}
|
||||||
*/
|
*/
|
||||||
const streamToHttp = (req, res) => {
|
const streamToHttp = (req, res) => {
|
||||||
const accountId = req.accountId || req.remoteAddress;
|
|
||||||
|
|
||||||
const channelName = channelNameFromPath(req);
|
const channelName = channelNameFromPath(req);
|
||||||
|
|
||||||
connectedClients.labels({ type: 'eventsource' }).inc();
|
connectedClients.labels({ type: 'eventsource' }).inc();
|
||||||
|
@ -963,7 +998,8 @@ const startServer = async () => {
|
||||||
const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
|
const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
|
||||||
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
log.verbose(req.requestId, `Ending stream for ${accountId}`);
|
req.log.info({ accountId: req.accountId }, `Ending stream`);
|
||||||
|
|
||||||
// We decrement these counters here instead of in streamHttpEnd as in that
|
// We decrement these counters here instead of in streamHttpEnd as in that
|
||||||
// method we don't have knowledge of the channel names
|
// method we don't have knowledge of the channel names
|
||||||
connectedClients.labels({ type: 'eventsource' }).dec();
|
connectedClients.labels({ type: 'eventsource' }).dec();
|
||||||
|
@ -1007,15 +1043,15 @@ const startServer = async () => {
|
||||||
*/
|
*/
|
||||||
const streamToWs = (req, ws, streamName) => (event, payload) => {
|
const streamToWs = (req, ws, streamName) => (event, payload) => {
|
||||||
if (ws.readyState !== ws.OPEN) {
|
if (ws.readyState !== ws.OPEN) {
|
||||||
log.error(req.requestId, 'Tried writing to closed socket');
|
req.log.error('Tried writing to closed socket');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = JSON.stringify({ stream: streamName, event, payload });
|
const message = JSON.stringify({ stream: streamName, event, payload });
|
||||||
|
|
||||||
ws.send(message, (/** @type {Error} */ err) => {
|
ws.send(message, (/** @type {Error|undefined} */ err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error(req.requestId, `Failed to send to websocket: ${err}`);
|
req.log.error({err}, `Failed to send to websocket`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1032,20 +1068,19 @@ const startServer = async () => {
|
||||||
|
|
||||||
app.use(api);
|
app.use(api);
|
||||||
|
|
||||||
api.use(setRequestId);
|
|
||||||
api.use(setRemoteAddress);
|
|
||||||
|
|
||||||
api.use(authenticationMiddleware);
|
api.use(authenticationMiddleware);
|
||||||
api.use(errorMiddleware);
|
api.use(errorMiddleware);
|
||||||
|
|
||||||
api.get('/api/v1/streaming/*', (req, res) => {
|
api.get('/api/v1/streaming/*', (req, res) => {
|
||||||
|
// @ts-ignore
|
||||||
channelNameToIds(req, channelNameFromPath(req), req.query).then(({ channelIds, options }) => {
|
channelNameToIds(req, channelNameFromPath(req), req.query).then(({ channelIds, options }) => {
|
||||||
const onSend = streamToHttp(req, res);
|
const onSend = streamToHttp(req, res);
|
||||||
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
|
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
|
||||||
|
|
||||||
streamFrom(channelIds, req, onSend, onEnd, 'eventsource', options.needsFiltering);
|
// @ts-ignore
|
||||||
|
streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
log.verbose(req.requestId, 'Subscription error:', err.toString());
|
res.log.info({ err }, 'Subscription error:', err.toString());
|
||||||
httpNotFound(res);
|
httpNotFound(res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1197,6 +1232,7 @@ const startServer = async () => {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'list':
|
case 'list':
|
||||||
|
// @ts-ignore
|
||||||
authorizeListAccess(params.list, req).then(() => {
|
authorizeListAccess(params.list, req).then(() => {
|
||||||
resolve({
|
resolve({
|
||||||
channelIds: [`timeline:list:${params.list}`],
|
channelIds: [`timeline:list:${params.list}`],
|
||||||
|
@ -1218,9 +1254,9 @@ const startServer = async () => {
|
||||||
* @returns {string[]}
|
* @returns {string[]}
|
||||||
*/
|
*/
|
||||||
const streamNameFromChannelName = (channelName, params) => {
|
const streamNameFromChannelName = (channelName, params) => {
|
||||||
if (channelName === 'list') {
|
if (channelName === 'list' && params.list) {
|
||||||
return [channelName, params.list];
|
return [channelName, params.list];
|
||||||
} else if (['hashtag', 'hashtag:local'].includes(channelName)) {
|
} else if (['hashtag', 'hashtag:local'].includes(channelName) && params.tag) {
|
||||||
return [channelName, params.tag];
|
return [channelName, params.tag];
|
||||||
} else {
|
} else {
|
||||||
return [channelName];
|
return [channelName];
|
||||||
|
@ -1229,8 +1265,9 @@ const startServer = async () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef WebSocketSession
|
* @typedef WebSocketSession
|
||||||
* @property {WebSocket} websocket
|
* @property {WebSocket & { isAlive: boolean}} websocket
|
||||||
* @property {http.IncomingMessage} request
|
* @property {http.IncomingMessage & ResolvedAccount} request
|
||||||
|
* @property {import('pino').Logger} logger
|
||||||
* @property {Object.<string, { channelName: string, listener: SubscriptionListener, stopHeartbeat: function(): void }>} subscriptions
|
* @property {Object.<string, { channelName: string, listener: SubscriptionListener, stopHeartbeat: function(): void }>} subscriptions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -1240,8 +1277,8 @@ const startServer = async () => {
|
||||||
* @param {StreamParams} params
|
* @param {StreamParams} params
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const subscribeWebsocketToChannel = ({ socket, request, subscriptions }, channelName, params) => {
|
const subscribeWebsocketToChannel = ({ websocket, request, logger, subscriptions }, channelName, params) => {
|
||||||
checkScopes(request, channelName).then(() => channelNameToIds(request, channelName, params)).then(({
|
checkScopes(request, logger, channelName).then(() => channelNameToIds(request, channelName, params)).then(({
|
||||||
channelIds,
|
channelIds,
|
||||||
options,
|
options,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -1249,9 +1286,9 @@ const startServer = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params));
|
const onSend = streamToWs(request, websocket, streamNameFromChannelName(channelName, params));
|
||||||
const stopHeartbeat = subscriptionHeartbeat(channelIds);
|
const stopHeartbeat = subscriptionHeartbeat(channelIds);
|
||||||
const listener = streamFrom(channelIds, request, onSend, undefined, 'websocket', options.needsFiltering);
|
const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options.needsFiltering);
|
||||||
|
|
||||||
connectedChannels.labels({ type: 'websocket', channel: channelName }).inc();
|
connectedChannels.labels({ type: 'websocket', channel: channelName }).inc();
|
||||||
|
|
||||||
|
@ -1261,14 +1298,17 @@ const startServer = async () => {
|
||||||
stopHeartbeat,
|
stopHeartbeat,
|
||||||
};
|
};
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
log.verbose(request.requestId, 'Subscription error:', err.toString());
|
logger.error({ err }, 'Subscription error');
|
||||||
socket.send(JSON.stringify({ error: err.toString() }));
|
websocket.send(JSON.stringify({ error: err.toString() }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
const removeSubscription = (subscriptions, channelIds, request) => {
|
* @param {WebSocketSession} session
|
||||||
log.verbose(request.requestId, `Ending stream from ${channelIds.join(', ')} for ${request.accountId}`);
|
* @param {string[]} channelIds
|
||||||
|
*/
|
||||||
|
const removeSubscription = ({ request, logger, subscriptions }, channelIds) => {
|
||||||
|
logger.info({ channelIds, accountId: request.accountId }, `Ending stream`);
|
||||||
|
|
||||||
const subscription = subscriptions[channelIds.join(';')];
|
const subscription = subscriptions[channelIds.join(';')];
|
||||||
|
|
||||||
|
@ -1292,16 +1332,17 @@ const startServer = async () => {
|
||||||
* @param {StreamParams} params
|
* @param {StreamParams} params
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const unsubscribeWebsocketFromChannel = ({ socket, request, subscriptions }, channelName, params) => {
|
const unsubscribeWebsocketFromChannel = (session, channelName, params) => {
|
||||||
|
const { websocket, request, logger } = session;
|
||||||
|
|
||||||
channelNameToIds(request, channelName, params).then(({ channelIds }) => {
|
channelNameToIds(request, channelName, params).then(({ channelIds }) => {
|
||||||
removeSubscription(subscriptions, channelIds, request);
|
removeSubscription(session, channelIds);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
log.verbose(request.requestId, 'Unsubscribe error:', err);
|
logger.error({err}, 'Unsubscribe error');
|
||||||
|
|
||||||
// If we have a socket that is alive and open still, send the error back to the client:
|
// If we have a socket that is alive and open still, send the error back to the client:
|
||||||
// FIXME: In other parts of the code ws === socket
|
if (websocket.isAlive && websocket.readyState === websocket.OPEN) {
|
||||||
if (socket.isAlive && socket.readyState === socket.OPEN) {
|
websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
|
||||||
socket.send(JSON.stringify({ error: "Error unsubscribing from channel" }));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1309,16 +1350,14 @@ const startServer = async () => {
|
||||||
/**
|
/**
|
||||||
* @param {WebSocketSession} session
|
* @param {WebSocketSession} session
|
||||||
*/
|
*/
|
||||||
const subscribeWebsocketToSystemChannel = ({ socket, request, subscriptions }) => {
|
const subscribeWebsocketToSystemChannel = ({ websocket, request, subscriptions }) => {
|
||||||
const accessTokenChannelId = `timeline:access_token:${request.accessTokenId}`;
|
const accessTokenChannelId = `timeline:access_token:${request.accessTokenId}`;
|
||||||
const systemChannelId = `timeline:system:${request.accountId}`;
|
const systemChannelId = `timeline:system:${request.accountId}`;
|
||||||
|
|
||||||
const listener = createSystemMessageListener(request, {
|
const listener = createSystemMessageListener(request, {
|
||||||
|
|
||||||
onKill() {
|
onKill() {
|
||||||
socket.close();
|
websocket.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
subscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
|
subscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
|
||||||
|
@ -1355,18 +1394,15 @@ const startServer = async () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {WebSocket & { isAlive: boolean }} ws
|
* @param {WebSocket & { isAlive: boolean }} ws
|
||||||
* @param {http.IncomingMessage} req
|
* @param {http.IncomingMessage & ResolvedAccount} req
|
||||||
|
* @param {import('pino').Logger} log
|
||||||
*/
|
*/
|
||||||
function onConnection(ws, req) {
|
function onConnection(ws, req, log) {
|
||||||
// Note: url.parse could throw, which would terminate the connection, so we
|
// Note: url.parse could throw, which would terminate the connection, so we
|
||||||
// increment the connected clients metric straight away when we establish
|
// increment the connected clients metric straight away when we establish
|
||||||
// the connection, without waiting:
|
// the connection, without waiting:
|
||||||
connectedClients.labels({ type: 'websocket' }).inc();
|
connectedClients.labels({ type: 'websocket' }).inc();
|
||||||
|
|
||||||
// Setup request properties:
|
|
||||||
req.requestId = uuid.v4();
|
|
||||||
req.remoteAddress = ws._socket.remoteAddress;
|
|
||||||
|
|
||||||
// Setup connection keep-alive state:
|
// Setup connection keep-alive state:
|
||||||
ws.isAlive = true;
|
ws.isAlive = true;
|
||||||
ws.on('pong', () => {
|
ws.on('pong', () => {
|
||||||
|
@ -1377,8 +1413,9 @@ const startServer = async () => {
|
||||||
* @type {WebSocketSession}
|
* @type {WebSocketSession}
|
||||||
*/
|
*/
|
||||||
const session = {
|
const session = {
|
||||||
socket: ws,
|
websocket: ws,
|
||||||
request: req,
|
request: req,
|
||||||
|
logger: log,
|
||||||
subscriptions: {},
|
subscriptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1386,27 +1423,30 @@ const startServer = async () => {
|
||||||
const subscriptions = Object.keys(session.subscriptions);
|
const subscriptions = Object.keys(session.subscriptions);
|
||||||
|
|
||||||
subscriptions.forEach(channelIds => {
|
subscriptions.forEach(channelIds => {
|
||||||
removeSubscription(session.subscriptions, channelIds.split(';'), req);
|
removeSubscription(session, channelIds.split(';'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Decrement the metrics for connected clients:
|
// Decrement the metrics for connected clients:
|
||||||
connectedClients.labels({ type: 'websocket' }).dec();
|
connectedClients.labels({ type: 'websocket' }).dec();
|
||||||
|
|
||||||
// ensure garbage collection:
|
// We need to delete the session object as to ensure it correctly gets
|
||||||
session.socket = null;
|
// garbage collected, without doing this we could accidentally hold on to
|
||||||
session.request = null;
|
// references to the websocket, the request, and the logger, causing
|
||||||
session.subscriptions = {};
|
// memory leaks.
|
||||||
|
//
|
||||||
|
// @ts-ignore
|
||||||
|
delete session;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: immediately after the `error` event is emitted, the `close` event
|
// Note: immediately after the `error` event is emitted, the `close` event
|
||||||
// is emitted. As such, all we need to do is log the error here.
|
// is emitted. As such, all we need to do is log the error here.
|
||||||
ws.on('error', (err) => {
|
ws.on('error', (/** @type {Error} */ err) => {
|
||||||
log.error('websocket', err.toString());
|
log.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', (data, isBinary) => {
|
ws.on('message', (data, isBinary) => {
|
||||||
if (isBinary) {
|
if (isBinary) {
|
||||||
log.warn('websocket', 'Received binary data, closing connection');
|
log.warn('Received binary data, closing connection');
|
||||||
ws.close(1003, 'The mastodon streaming server does not support binary messages');
|
ws.close(1003, 'The mastodon streaming server does not support binary messages');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1441,18 +1481,20 @@ const startServer = async () => {
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
wss.clients.forEach(ws => {
|
wss.clients.forEach(ws => {
|
||||||
|
// @ts-ignore
|
||||||
if (ws.isAlive === false) {
|
if (ws.isAlive === false) {
|
||||||
ws.terminate();
|
ws.terminate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
ws.isAlive = false;
|
ws.isAlive = false;
|
||||||
ws.ping('', false);
|
ws.ping('', false);
|
||||||
});
|
});
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
attachServerWithConfig(server, address => {
|
attachServerWithConfig(server, address => {
|
||||||
log.warn(`Streaming API now listening on ${address}`);
|
logger.info(`Streaming API now listening on ${address}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onExit = () => {
|
const onExit = () => {
|
||||||
|
@ -1460,8 +1502,10 @@ const startServer = async () => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @param {Error} err */
|
||||||
const onError = (err) => {
|
const onError = (err) => {
|
||||||
log.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
server.close();
|
server.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
@ -1485,7 +1529,7 @@ const attachServerWithConfig = (server, onSuccess) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => {
|
server.listen(+(process.env.PORT || 4000), process.env.BIND || '127.0.0.1', () => {
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(`${server.address().address}:${server.address().port}`);
|
onSuccess(`${server.address().address}:${server.address().port}`);
|
||||||
}
|
}
|
||||||
|
|
119
streaming/logging.js
Normal file
119
streaming/logging.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
const { pino } = require('pino');
|
||||||
|
const { pinoHttp, stdSerializers: pinoHttpSerializers } = require('pino-http');
|
||||||
|
const uuid = require('uuid');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the Request ID for logging and setting on responses
|
||||||
|
* @param {http.IncomingMessage} req
|
||||||
|
* @param {http.ServerResponse} [res]
|
||||||
|
* @returns {import("pino-http").ReqId}
|
||||||
|
*/
|
||||||
|
function generateRequestId(req, res) {
|
||||||
|
if (req.id) {
|
||||||
|
return req.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.id = uuid.v4();
|
||||||
|
|
||||||
|
// Allow for usage with WebSockets:
|
||||||
|
if (res) {
|
||||||
|
res.setHeader('X-Request-Id', req.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request log sanitizer to prevent logging access tokens in URLs
|
||||||
|
* @param {http.IncomingMessage} req
|
||||||
|
*/
|
||||||
|
function sanitizeRequestLog(req) {
|
||||||
|
const log = pinoHttpSerializers.req(req);
|
||||||
|
if (typeof log.url === 'string' && log.url.includes('access_token')) {
|
||||||
|
// Doorkeeper uses SecureRandom.urlsafe_base64 per RFC 6749 / RFC 6750
|
||||||
|
log.url = log.url.replace(/(access_token)=([a-zA-Z0-9\-_]+)/gi, '$1=[Redacted]');
|
||||||
|
}
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = pino({
|
||||||
|
name: "streaming",
|
||||||
|
// Reformat the log level to a string:
|
||||||
|
formatters: {
|
||||||
|
level: (label) => {
|
||||||
|
return {
|
||||||
|
level: label
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redact: {
|
||||||
|
paths: [
|
||||||
|
'req.headers["sec-websocket-key"]',
|
||||||
|
// Note: we currently pass the AccessToken via the websocket subprotocol
|
||||||
|
// field, an anti-pattern, but this ensures it doesn't end up in logs.
|
||||||
|
'req.headers["sec-websocket-protocol"]',
|
||||||
|
'req.headers.authorization',
|
||||||
|
'req.headers.cookie',
|
||||||
|
'req.query.access_token'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpLogger = pinoHttp({
|
||||||
|
logger,
|
||||||
|
genReqId: generateRequestId,
|
||||||
|
serializers: {
|
||||||
|
req: sanitizeRequestLog
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a logger to the request object received by http upgrade handlers
|
||||||
|
* @param {http.IncomingMessage} request
|
||||||
|
*/
|
||||||
|
function attachWebsocketHttpLogger(request) {
|
||||||
|
generateRequestId(request);
|
||||||
|
|
||||||
|
request.log = logger.child({
|
||||||
|
req: sanitizeRequestLog(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a logger instance for the Websocket connection to use.
|
||||||
|
* @param {http.IncomingMessage} request
|
||||||
|
* @param {import('./index.js').ResolvedAccount} resolvedAccount
|
||||||
|
*/
|
||||||
|
function createWebsocketLogger(request, resolvedAccount) {
|
||||||
|
// ensure the request.id is always present.
|
||||||
|
generateRequestId(request);
|
||||||
|
|
||||||
|
return logger.child({
|
||||||
|
req: {
|
||||||
|
id: request.id
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
id: resolvedAccount.accountId ?? null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.logger = logger;
|
||||||
|
exports.httpLogger = httpLogger;
|
||||||
|
exports.attachWebsocketHttpLogger = attachWebsocketHttpLogger;
|
||||||
|
exports.createWebsocketLogger = createWebsocketLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the log level based on the environment
|
||||||
|
* @param {Object<string, any>} env
|
||||||
|
* @param {string} environment
|
||||||
|
*/
|
||||||
|
exports.initializeLogLevel = function initializeLogLevel(env, environment) {
|
||||||
|
if (env.LOG_LEVEL && Object.keys(logger.levels.values).includes(env.LOG_LEVEL)) {
|
||||||
|
logger.level = env.LOG_LEVEL;
|
||||||
|
} else if (environment === 'development') {
|
||||||
|
logger.level = 'debug';
|
||||||
|
} else {
|
||||||
|
logger.level = 'info';
|
||||||
|
}
|
||||||
|
};
|
|
@ -21,9 +21,10 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"jsdom": "^23.0.0",
|
"jsdom": "^23.0.0",
|
||||||
"npmlog": "^7.0.1",
|
|
||||||
"pg": "^8.5.0",
|
"pg": "^8.5.0",
|
||||||
"pg-connection-string": "^2.6.0",
|
"pg-connection-string": "^2.6.0",
|
||||||
|
"pino": "^8.17.2",
|
||||||
|
"pino-http": "^9.0.0",
|
||||||
"prom-client": "^15.0.0",
|
"prom-client": "^15.0.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
|
@ -31,11 +32,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.16",
|
"@types/cors": "^2.8.16",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/npmlog": "^7.0.0",
|
|
||||||
"@types/pg": "^8.6.6",
|
"@types/pg": "^8.6.6",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"@types/ws": "^8.5.9",
|
"@types/ws": "^8.5.9",
|
||||||
"eslint-define-config": "^2.0.0",
|
"eslint-define-config": "^2.0.0",
|
||||||
|
"pino-pretty": "^10.3.1",
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
376
yarn.lock
376
yarn.lock
|
@ -2536,7 +2536,6 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/cors": "npm:^2.8.16"
|
"@types/cors": "npm:^2.8.16"
|
||||||
"@types/express": "npm:^4.17.17"
|
"@types/express": "npm:^4.17.17"
|
||||||
"@types/npmlog": "npm:^7.0.0"
|
|
||||||
"@types/pg": "npm:^8.6.6"
|
"@types/pg": "npm:^8.6.6"
|
||||||
"@types/uuid": "npm:^9.0.0"
|
"@types/uuid": "npm:^9.0.0"
|
||||||
"@types/ws": "npm:^8.5.9"
|
"@types/ws": "npm:^8.5.9"
|
||||||
|
@ -2547,9 +2546,11 @@ __metadata:
|
||||||
express: "npm:^4.18.2"
|
express: "npm:^4.18.2"
|
||||||
ioredis: "npm:^5.3.2"
|
ioredis: "npm:^5.3.2"
|
||||||
jsdom: "npm:^23.0.0"
|
jsdom: "npm:^23.0.0"
|
||||||
npmlog: "npm:^7.0.1"
|
|
||||||
pg: "npm:^8.5.0"
|
pg: "npm:^8.5.0"
|
||||||
pg-connection-string: "npm:^2.6.0"
|
pg-connection-string: "npm:^2.6.0"
|
||||||
|
pino: "npm:^8.17.2"
|
||||||
|
pino-http: "npm:^9.0.0"
|
||||||
|
pino-pretty: "npm:^10.3.1"
|
||||||
prom-client: "npm:^15.0.0"
|
prom-client: "npm:^15.0.0"
|
||||||
typescript: "npm:^5.0.4"
|
typescript: "npm:^5.0.4"
|
||||||
utf-8-validate: "npm:^6.0.3"
|
utf-8-validate: "npm:^6.0.3"
|
||||||
|
@ -3338,15 +3339,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/npmlog@npm:^7.0.0":
|
|
||||||
version: 7.0.0
|
|
||||||
resolution: "@types/npmlog@npm:7.0.0"
|
|
||||||
dependencies:
|
|
||||||
"@types/node": "npm:*"
|
|
||||||
checksum: e94cb1d7dc6b1251d58d0a3cbf0c5b9e9b7c7649774cf816b9277fc10e1a09e65f2854357c4972d04d477f8beca3c8accb5e8546d594776e59e35ddfee79aff2
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/object-assign@npm:^4.0.30":
|
"@types/object-assign@npm:^4.0.30":
|
||||||
version: 4.0.33
|
version: 4.0.33
|
||||||
resolution: "@types/object-assign@npm:4.0.33"
|
resolution: "@types/object-assign@npm:4.0.33"
|
||||||
|
@ -3791,6 +3783,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager@npm:6.9.1":
|
||||||
|
version: 6.9.1
|
||||||
|
resolution: "@typescript-eslint/scope-manager@npm:6.9.1"
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types": "npm:6.9.1"
|
||||||
|
"@typescript-eslint/visitor-keys": "npm:6.9.1"
|
||||||
|
checksum: 53fa7c3813d22b119e464f9b6d7d23407dfe103ee8ad2dcacf9ad6d656fda20e2bb3346df39e62b0e6b6ce71572ce5838071c5d2cca6daa4e0ce117ff22eafe5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@npm:6.19.0":
|
"@typescript-eslint/type-utils@npm:6.19.0":
|
||||||
version: 6.19.0
|
version: 6.19.0
|
||||||
resolution: "@typescript-eslint/type-utils@npm:6.19.0"
|
resolution: "@typescript-eslint/type-utils@npm:6.19.0"
|
||||||
|
@ -3815,6 +3817,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@typescript-eslint/types@npm:6.9.1":
|
||||||
|
version: 6.9.1
|
||||||
|
resolution: "@typescript-eslint/types@npm:6.9.1"
|
||||||
|
checksum: 4ba21ba18e256da210a4caedfbc5d4927cf8cb4f2c4d74f8ccc865576f3659b974e79119d3c94db2b68a4cec9cd687e43971d355450b7082d6d1736a5dd6db85
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@npm:6.19.0":
|
"@typescript-eslint/typescript-estree@npm:6.19.0":
|
||||||
version: 6.19.0
|
version: 6.19.0
|
||||||
resolution: "@typescript-eslint/typescript-estree@npm:6.19.0"
|
resolution: "@typescript-eslint/typescript-estree@npm:6.19.0"
|
||||||
|
@ -3834,7 +3843,25 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/utils@npm:6.19.0, @typescript-eslint/utils@npm:^6.5.0":
|
"@typescript-eslint/typescript-estree@npm:6.9.1":
|
||||||
|
version: 6.9.1
|
||||||
|
resolution: "@typescript-eslint/typescript-estree@npm:6.9.1"
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types": "npm:6.9.1"
|
||||||
|
"@typescript-eslint/visitor-keys": "npm:6.9.1"
|
||||||
|
debug: "npm:^4.3.4"
|
||||||
|
globby: "npm:^11.1.0"
|
||||||
|
is-glob: "npm:^4.0.3"
|
||||||
|
semver: "npm:^7.5.4"
|
||||||
|
ts-api-utils: "npm:^1.0.1"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
checksum: 850b1865a90107879186c3f2969968a2c08fc6fcc56d146483c297cf5be376e33d505ac81533ba8e8103ca4d2edfea7d21b178de9e52217f7ee2922f51a445fa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@typescript-eslint/utils@npm:6.19.0":
|
||||||
version: 6.19.0
|
version: 6.19.0
|
||||||
resolution: "@typescript-eslint/utils@npm:6.19.0"
|
resolution: "@typescript-eslint/utils@npm:6.19.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3851,6 +3878,23 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@typescript-eslint/utils@npm:^6.5.0":
|
||||||
|
version: 6.9.1
|
||||||
|
resolution: "@typescript-eslint/utils@npm:6.9.1"
|
||||||
|
dependencies:
|
||||||
|
"@eslint-community/eslint-utils": "npm:^4.4.0"
|
||||||
|
"@types/json-schema": "npm:^7.0.12"
|
||||||
|
"@types/semver": "npm:^7.5.0"
|
||||||
|
"@typescript-eslint/scope-manager": "npm:6.9.1"
|
||||||
|
"@typescript-eslint/types": "npm:6.9.1"
|
||||||
|
"@typescript-eslint/typescript-estree": "npm:6.9.1"
|
||||||
|
semver: "npm:^7.5.4"
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^7.0.0 || ^8.0.0
|
||||||
|
checksum: 3d329d54c3d155ed29e2b456a602aef76bda1b88dfcf847145849362e4ddefabe5c95de236de750d08d5da9bedcfb2131bdfd784ce4eb87cf82728f0b6662033
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@npm:6.19.0":
|
"@typescript-eslint/visitor-keys@npm:6.19.0":
|
||||||
version: 6.19.0
|
version: 6.19.0
|
||||||
resolution: "@typescript-eslint/visitor-keys@npm:6.19.0"
|
resolution: "@typescript-eslint/visitor-keys@npm:6.19.0"
|
||||||
|
@ -3861,6 +3905,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys@npm:6.9.1":
|
||||||
|
version: 6.9.1
|
||||||
|
resolution: "@typescript-eslint/visitor-keys@npm:6.9.1"
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types": "npm:6.9.1"
|
||||||
|
eslint-visitor-keys: "npm:^3.4.1"
|
||||||
|
checksum: ac5f375a177add30489e5b63cafa8d82a196b33624bb36418422ebe0d7973b3ba550dc7e0dda36ea75a94cf9b200b4fb5f5fb4d77c027fd801201c1a269d343b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ungap/structured-clone@npm:^1.2.0":
|
"@ungap/structured-clone@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "@ungap/structured-clone@npm:1.2.0"
|
resolution: "@ungap/structured-clone@npm:1.2.0"
|
||||||
|
@ -4324,13 +4378,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"aproba@npm:^1.0.3 || ^2.0.0":
|
|
||||||
version: 2.0.0
|
|
||||||
resolution: "aproba@npm:2.0.0"
|
|
||||||
checksum: d06e26384a8f6245d8c8896e138c0388824e259a329e0c9f196b4fa533c82502a6fd449586e3604950a0c42921832a458bb3aa0aa9f0ba449cfd4f50fd0d09b5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"are-docs-informative@npm:^0.0.2":
|
"are-docs-informative@npm:^0.0.2":
|
||||||
version: 0.0.2
|
version: 0.0.2
|
||||||
resolution: "are-docs-informative@npm:0.0.2"
|
resolution: "are-docs-informative@npm:0.0.2"
|
||||||
|
@ -4338,16 +4385,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"are-we-there-yet@npm:^4.0.0":
|
|
||||||
version: 4.0.0
|
|
||||||
resolution: "are-we-there-yet@npm:4.0.0"
|
|
||||||
dependencies:
|
|
||||||
delegates: "npm:^1.0.0"
|
|
||||||
readable-stream: "npm:^4.1.0"
|
|
||||||
checksum: 760008e32948e9f738c5a288792d187e235fee0f170e042850bc7ff242f2a499f3f2874d6dd43ac06f5d9f5306137bc51bbdd4ae0bb11379c58b01678e0f684d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"argparse@npm:^1.0.7":
|
"argparse@npm:^1.0.7":
|
||||||
version: 1.0.10
|
version: 1.0.10
|
||||||
resolution: "argparse@npm:1.0.10"
|
resolution: "argparse@npm:1.0.10"
|
||||||
|
@ -4669,6 +4706,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"atomic-sleep@npm:^1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "atomic-sleep@npm:1.0.0"
|
||||||
|
checksum: e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"autoprefixer@npm:^10.4.14":
|
"autoprefixer@npm:^10.4.14":
|
||||||
version: 10.4.17
|
version: 10.4.17
|
||||||
resolution: "autoprefixer@npm:10.4.17"
|
resolution: "autoprefixer@npm:10.4.17"
|
||||||
|
@ -5763,15 +5807,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"color-support@npm:^1.1.3":
|
|
||||||
version: 1.1.3
|
|
||||||
resolution: "color-support@npm:1.1.3"
|
|
||||||
bin:
|
|
||||||
color-support: bin.js
|
|
||||||
checksum: 8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"colord@npm:^2.9.1, colord@npm:^2.9.3":
|
"colord@npm:^2.9.1, colord@npm:^2.9.3":
|
||||||
version: 2.9.3
|
version: 2.9.3
|
||||||
resolution: "colord@npm:2.9.3"
|
resolution: "colord@npm:2.9.3"
|
||||||
|
@ -5779,7 +5814,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"colorette@npm:^2.0.20":
|
"colorette@npm:^2.0.20, colorette@npm:^2.0.7":
|
||||||
version: 2.0.20
|
version: 2.0.20
|
||||||
resolution: "colorette@npm:2.0.20"
|
resolution: "colorette@npm:2.0.20"
|
||||||
checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40
|
checksum: e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40
|
||||||
|
@ -5911,13 +5946,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"console-control-strings@npm:^1.1.0":
|
|
||||||
version: 1.1.0
|
|
||||||
resolution: "console-control-strings@npm:1.1.0"
|
|
||||||
checksum: 7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"constants-browserify@npm:^1.0.0":
|
"constants-browserify@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "constants-browserify@npm:1.0.0"
|
resolution: "constants-browserify@npm:1.0.0"
|
||||||
|
@ -6445,6 +6473,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dateformat@npm:^4.6.3":
|
||||||
|
version: 4.6.3
|
||||||
|
resolution: "dateformat@npm:4.6.3"
|
||||||
|
checksum: e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"debounce@npm:^1.2.1":
|
"debounce@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "debounce@npm:1.2.1"
|
resolution: "debounce@npm:1.2.1"
|
||||||
|
@ -6680,13 +6715,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"delegates@npm:^1.0.0":
|
|
||||||
version: 1.0.0
|
|
||||||
resolution: "delegates@npm:1.0.0"
|
|
||||||
checksum: ba05874b91148e1db4bf254750c042bf2215febd23a6d3cda2e64896aef79745fbd4b9996488bd3cafb39ce19dbce0fd6e3b6665275638befffe1c9b312b91b5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"denque@npm:^2.1.0":
|
"denque@npm:^2.1.0":
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
resolution: "denque@npm:2.1.0"
|
resolution: "denque@npm:2.1.0"
|
||||||
|
@ -7952,6 +7980,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fast-copy@npm:^3.0.0":
|
||||||
|
version: 3.0.1
|
||||||
|
resolution: "fast-copy@npm:3.0.1"
|
||||||
|
checksum: a8310dbcc4c94ed001dc3e0bbc3c3f0491bb04e6c17163abe441a54997ba06cdf1eb532c2f05e54777c6f072c84548c23ef0ecd54665cd611be1d42f37eca258
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
|
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
resolution: "fast-deep-equal@npm:3.1.3"
|
resolution: "fast-deep-equal@npm:3.1.3"
|
||||||
|
@ -7993,6 +8028,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fast-redact@npm:^3.1.1":
|
||||||
|
version: 3.3.0
|
||||||
|
resolution: "fast-redact@npm:3.3.0"
|
||||||
|
checksum: d81562510681e9ba6404ee5d3838ff5257a44d2f80937f5024c099049ff805437d0fae0124458a7e87535cc9dcf4de305bb075cab8f08d6c720bbc3447861b4e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"fast-safe-stringify@npm:^2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "fast-safe-stringify@npm:2.1.1"
|
||||||
|
checksum: d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fastest-levenshtein@npm:^1.0.16":
|
"fastest-levenshtein@npm:^1.0.16":
|
||||||
version: 1.0.16
|
version: 1.0.16
|
||||||
resolution: "fastest-levenshtein@npm:1.0.16"
|
resolution: "fastest-levenshtein@npm:1.0.16"
|
||||||
|
@ -8407,22 +8456,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"gauge@npm:^5.0.0":
|
|
||||||
version: 5.0.1
|
|
||||||
resolution: "gauge@npm:5.0.1"
|
|
||||||
dependencies:
|
|
||||||
aproba: "npm:^1.0.3 || ^2.0.0"
|
|
||||||
color-support: "npm:^1.1.3"
|
|
||||||
console-control-strings: "npm:^1.1.0"
|
|
||||||
has-unicode: "npm:^2.0.1"
|
|
||||||
signal-exit: "npm:^4.0.1"
|
|
||||||
string-width: "npm:^4.2.3"
|
|
||||||
strip-ansi: "npm:^6.0.1"
|
|
||||||
wide-align: "npm:^1.1.5"
|
|
||||||
checksum: 845f9a2534356cd0e9c1ae590ed471bbe8d74c318915b92a34e8813b8d3441ca8e0eb0fa87a48081e70b63b84d398c5e66a13b8e8040181c10b9d77e9fe3287f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"gensync@npm:^1.0.0-beta.2":
|
"gensync@npm:^1.0.0-beta.2":
|
||||||
version: 1.0.0-beta.2
|
version: 1.0.0-beta.2
|
||||||
resolution: "gensync@npm:1.0.0-beta.2"
|
resolution: "gensync@npm:1.0.0-beta.2"
|
||||||
|
@ -8771,13 +8804,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"has-unicode@npm:^2.0.1":
|
|
||||||
version: 2.0.1
|
|
||||||
resolution: "has-unicode@npm:2.0.1"
|
|
||||||
checksum: ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"has-value@npm:^0.3.1":
|
"has-value@npm:^0.3.1":
|
||||||
version: 0.3.1
|
version: 0.3.1
|
||||||
resolution: "has-value@npm:0.3.1"
|
resolution: "has-value@npm:0.3.1"
|
||||||
|
@ -8854,6 +8880,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"help-me@npm:^5.0.0":
|
||||||
|
version: 5.0.0
|
||||||
|
resolution: "help-me@npm:5.0.0"
|
||||||
|
checksum: 054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"history@npm:^4.10.1, history@npm:^4.9.0":
|
"history@npm:^4.10.1, history@npm:^4.9.0":
|
||||||
version: 4.10.1
|
version: 4.10.1
|
||||||
resolution: "history@npm:4.10.1"
|
resolution: "history@npm:4.10.1"
|
||||||
|
@ -9320,7 +9353,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"intl-messageformat@npm:10.5.10, intl-messageformat@npm:^10.3.5":
|
"intl-messageformat@npm:10.5.10":
|
||||||
version: 10.5.10
|
version: 10.5.10
|
||||||
resolution: "intl-messageformat@npm:10.5.10"
|
resolution: "intl-messageformat@npm:10.5.10"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9332,6 +9365,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"intl-messageformat@npm:^10.3.5":
|
||||||
|
version: 10.5.8
|
||||||
|
resolution: "intl-messageformat@npm:10.5.8"
|
||||||
|
dependencies:
|
||||||
|
"@formatjs/ecma402-abstract": "npm:1.18.0"
|
||||||
|
"@formatjs/fast-memoize": "npm:2.2.0"
|
||||||
|
"@formatjs/icu-messageformat-parser": "npm:2.7.3"
|
||||||
|
tslib: "npm:^2.4.0"
|
||||||
|
checksum: 1d2854aae8471ec48165ca265760d6c5b1814eca831c88db698eb29b5ed20bee21ca8533090c9d28d9c6f1d844dda210b0bc58a2e036446158fae0845e5eed4f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"invariant@npm:^2.2.2, invariant@npm:^2.2.4":
|
"invariant@npm:^2.2.2, invariant@npm:^2.2.4":
|
||||||
version: 2.2.4
|
version: 2.2.4
|
||||||
resolution: "invariant@npm:2.2.4"
|
resolution: "invariant@npm:2.2.4"
|
||||||
|
@ -10570,6 +10615,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"joycon@npm:^3.1.1":
|
||||||
|
version: 3.1.1
|
||||||
|
resolution: "joycon@npm:3.1.1"
|
||||||
|
checksum: 131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jpeg-autorotate@npm:^7.1.1":
|
"jpeg-autorotate@npm:^7.1.1":
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
resolution: "jpeg-autorotate@npm:7.1.1"
|
resolution: "jpeg-autorotate@npm:7.1.1"
|
||||||
|
@ -11966,18 +12018,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"npmlog@npm:^7.0.1":
|
|
||||||
version: 7.0.1
|
|
||||||
resolution: "npmlog@npm:7.0.1"
|
|
||||||
dependencies:
|
|
||||||
are-we-there-yet: "npm:^4.0.0"
|
|
||||||
console-control-strings: "npm:^1.1.0"
|
|
||||||
gauge: "npm:^5.0.0"
|
|
||||||
set-blocking: "npm:^2.0.0"
|
|
||||||
checksum: d4e6a2aaa7b5b5d2e2ed8f8ac3770789ca0691a49f3576b6a8c97d560a4c3305d2c233a9173d62be737e6e4506bf9e89debd6120a3843c1d37315c34f90fef71
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"nth-check@npm:^1.0.2":
|
"nth-check@npm:^1.0.2":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "nth-check@npm:1.0.2"
|
resolution: "nth-check@npm:1.0.2"
|
||||||
|
@ -12150,6 +12190,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"on-exit-leak-free@npm:^2.1.0":
|
||||||
|
version: 2.1.2
|
||||||
|
resolution: "on-exit-leak-free@npm:2.1.2"
|
||||||
|
checksum: faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"on-finished@npm:2.4.1":
|
"on-finished@npm:2.4.1":
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
resolution: "on-finished@npm:2.4.1"
|
resolution: "on-finished@npm:2.4.1"
|
||||||
|
@ -12717,6 +12764,80 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:v1.1.0":
|
||||||
|
version: 1.1.0
|
||||||
|
resolution: "pino-abstract-transport@npm:1.1.0"
|
||||||
|
dependencies:
|
||||||
|
readable-stream: "npm:^4.0.0"
|
||||||
|
split2: "npm:^4.0.0"
|
||||||
|
checksum: 6e9b9d5a2c0a37f91ecaf224d335daae1ae682b1c79a05b06ef9e0f0a5d289f8e597992217efc857796dae6f1067e9b4882f95c6228ff433ddc153532cae8aca
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"pino-http@npm:^9.0.0":
|
||||||
|
version: 9.0.0
|
||||||
|
resolution: "pino-http@npm:9.0.0"
|
||||||
|
dependencies:
|
||||||
|
get-caller-file: "npm:^2.0.5"
|
||||||
|
pino: "npm:^8.17.1"
|
||||||
|
pino-std-serializers: "npm:^6.2.2"
|
||||||
|
process-warning: "npm:^3.0.0"
|
||||||
|
checksum: 05496cb76cc9908658e50c4620fbdf7b0b5d99fb529493d601c3e4635b0bf7ce12b8a8eed7b5b520089f643b099233d61dd71f7cdfad8b66e59b9b81d79b6512
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"pino-pretty@npm:^10.3.1":
|
||||||
|
version: 10.3.1
|
||||||
|
resolution: "pino-pretty@npm:10.3.1"
|
||||||
|
dependencies:
|
||||||
|
colorette: "npm:^2.0.7"
|
||||||
|
dateformat: "npm:^4.6.3"
|
||||||
|
fast-copy: "npm:^3.0.0"
|
||||||
|
fast-safe-stringify: "npm:^2.1.1"
|
||||||
|
help-me: "npm:^5.0.0"
|
||||||
|
joycon: "npm:^3.1.1"
|
||||||
|
minimist: "npm:^1.2.6"
|
||||||
|
on-exit-leak-free: "npm:^2.1.0"
|
||||||
|
pino-abstract-transport: "npm:^1.0.0"
|
||||||
|
pump: "npm:^3.0.0"
|
||||||
|
readable-stream: "npm:^4.0.0"
|
||||||
|
secure-json-parse: "npm:^2.4.0"
|
||||||
|
sonic-boom: "npm:^3.0.0"
|
||||||
|
strip-json-comments: "npm:^3.1.1"
|
||||||
|
bin:
|
||||||
|
pino-pretty: bin.js
|
||||||
|
checksum: 6964fba5acc7a9f112e4c6738d602e123daf16cb5f6ddc56ab4b6bb05059f28876d51da8f72358cf1172e95fa12496b70465431a0836df693c462986d050686b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"pino-std-serializers@npm:^6.0.0, pino-std-serializers@npm:^6.2.2":
|
||||||
|
version: 6.2.2
|
||||||
|
resolution: "pino-std-serializers@npm:6.2.2"
|
||||||
|
checksum: 8f1c7f0f0d8f91e6c6b5b2a6bfb48f06441abeb85f1c2288319f736f9c6d814fbeebe928d2314efc2ba6018fa7db9357a105eca9fc99fc1f28945a8a8b28d3d5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"pino@npm:^8.17.1, pino@npm:^8.17.2":
|
||||||
|
version: 8.17.2
|
||||||
|
resolution: "pino@npm:8.17.2"
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep: "npm:^1.0.0"
|
||||||
|
fast-redact: "npm:^3.1.1"
|
||||||
|
on-exit-leak-free: "npm:^2.1.0"
|
||||||
|
pino-abstract-transport: "npm:v1.1.0"
|
||||||
|
pino-std-serializers: "npm:^6.0.0"
|
||||||
|
process-warning: "npm:^3.0.0"
|
||||||
|
quick-format-unescaped: "npm:^4.0.3"
|
||||||
|
real-require: "npm:^0.2.0"
|
||||||
|
safe-stable-stringify: "npm:^2.3.1"
|
||||||
|
sonic-boom: "npm:^3.7.0"
|
||||||
|
thread-stream: "npm:^2.0.0"
|
||||||
|
bin:
|
||||||
|
pino: bin.js
|
||||||
|
checksum: 9e55af6cd9d1833a4dbe64924fc73163295acd3c988a9c7db88926669f2574ab7ec607e8487b6dd71dbdad2d7c1c1aac439f37e59233f37220b1a9d88fa2ce01
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"pirates@npm:^4.0.4":
|
"pirates@npm:^4.0.4":
|
||||||
version: 4.0.6
|
version: 4.0.6
|
||||||
resolution: "pirates@npm:4.0.6"
|
resolution: "pirates@npm:4.0.6"
|
||||||
|
@ -13319,6 +13440,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"process-warning@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "process-warning@npm:3.0.0"
|
||||||
|
checksum: 60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"process@npm:^0.11.10":
|
"process@npm:^0.11.10":
|
||||||
version: 0.11.10
|
version: 0.11.10
|
||||||
resolution: "process@npm:0.11.10"
|
resolution: "process@npm:0.11.10"
|
||||||
|
@ -13496,6 +13624,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"quick-format-unescaped@npm:^4.0.3":
|
||||||
|
version: 4.0.4
|
||||||
|
resolution: "quick-format-unescaped@npm:4.0.4"
|
||||||
|
checksum: fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"raf@npm:^3.1.0":
|
"raf@npm:^3.1.0":
|
||||||
version: 3.4.1
|
version: 3.4.1
|
||||||
resolution: "raf@npm:3.4.1"
|
resolution: "raf@npm:3.4.1"
|
||||||
|
@ -13991,15 +14126,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"readable-stream@npm:^4.1.0":
|
"readable-stream@npm:^4.0.0":
|
||||||
version: 4.4.0
|
version: 4.4.2
|
||||||
resolution: "readable-stream@npm:4.4.0"
|
resolution: "readable-stream@npm:4.4.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
abort-controller: "npm:^3.0.0"
|
abort-controller: "npm:^3.0.0"
|
||||||
buffer: "npm:^6.0.3"
|
buffer: "npm:^6.0.3"
|
||||||
events: "npm:^3.3.0"
|
events: "npm:^3.3.0"
|
||||||
process: "npm:^0.11.10"
|
process: "npm:^0.11.10"
|
||||||
checksum: 83f5a11285e5ebefb7b22a43ea77a2275075639325b4932a328a1fb0ee2475b83b9cc94326724d71c6aa3b60fa87e2b16623530b1cac34f3825dcea0996fdbe4
|
string_decoder: "npm:^1.3.0"
|
||||||
|
checksum: cf7cc8daa2b57872d120945a20a1458c13dcb6c6f352505421115827b18ac4df0e483ac1fe195cb1f5cd226e1073fc55b92b569269d8299e8530840bcdbba40c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -14023,6 +14159,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"real-require@npm:^0.2.0":
|
||||||
|
version: 0.2.0
|
||||||
|
resolution: "real-require@npm:0.2.0"
|
||||||
|
checksum: 23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"redent@npm:^3.0.0":
|
"redent@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "redent@npm:3.0.0"
|
resolution: "redent@npm:3.0.0"
|
||||||
|
@ -14568,6 +14711,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"safe-stable-stringify@npm:^2.3.1":
|
||||||
|
version: 2.4.3
|
||||||
|
resolution: "safe-stable-stringify@npm:2.4.3"
|
||||||
|
checksum: 81dede06b8f2ae794efd868b1e281e3c9000e57b39801c6c162267eb9efda17bd7a9eafa7379e1f1cacd528d4ced7c80d7460ad26f62ada7c9e01dec61b2e768
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0":
|
"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0":
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
resolution: "safer-buffer@npm:2.1.2"
|
resolution: "safer-buffer@npm:2.1.2"
|
||||||
|
@ -14681,6 +14831,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"secure-json-parse@npm:^2.4.0":
|
||||||
|
version: 2.7.0
|
||||||
|
resolution: "secure-json-parse@npm:2.7.0"
|
||||||
|
checksum: f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"select-hose@npm:^2.0.0":
|
"select-hose@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "select-hose@npm:2.0.0"
|
resolution: "select-hose@npm:2.0.0"
|
||||||
|
@ -15084,6 +15241,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"sonic-boom@npm:^3.0.0, sonic-boom@npm:^3.7.0":
|
||||||
|
version: 3.7.0
|
||||||
|
resolution: "sonic-boom@npm:3.7.0"
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep: "npm:^1.0.0"
|
||||||
|
checksum: 57a3d560efb77f4576db111168ee2649c99e7869fda6ce0ec2a4e5458832d290ba58d74b073ddb5827d9a30f96d23cff79157993d919e1a6d5f28d8b6391c7f0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"source-list-map@npm:^2.0.0":
|
"source-list-map@npm:^2.0.0":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "source-list-map@npm:2.0.1"
|
resolution: "source-list-map@npm:2.0.1"
|
||||||
|
@ -15242,7 +15408,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"split2@npm:^4.1.0":
|
"split2@npm:^4.0.0, split2@npm:^4.1.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "split2@npm:4.2.0"
|
resolution: "split2@npm:4.2.0"
|
||||||
checksum: b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534
|
checksum: b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534
|
||||||
|
@ -15407,7 +15573,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
resolution: "string-width@npm:4.2.3"
|
resolution: "string-width@npm:4.2.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -15500,7 +15666,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1":
|
"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "string_decoder@npm:1.3.0"
|
resolution: "string_decoder@npm:1.3.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -16046,6 +16212,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"thread-stream@npm:^2.0.0":
|
||||||
|
version: 2.4.1
|
||||||
|
resolution: "thread-stream@npm:2.4.1"
|
||||||
|
dependencies:
|
||||||
|
real-require: "npm:^0.2.0"
|
||||||
|
checksum: ce29265810b9550ce896726301ff006ebfe96b90292728f07cfa4c379740585583046e2a8018afc53aca66b18fed12b33a84f3883e7ebc317185f6682898b8f8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"thunky@npm:^1.0.2":
|
"thunky@npm:^1.0.2":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "thunky@npm:1.1.0"
|
resolution: "thunky@npm:1.1.0"
|
||||||
|
@ -17283,15 +17458,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"wide-align@npm:^1.1.5":
|
|
||||||
version: 1.1.5
|
|
||||||
resolution: "wide-align@npm:1.1.5"
|
|
||||||
dependencies:
|
|
||||||
string-width: "npm:^1.0.2 || 2 || 3 || 4"
|
|
||||||
checksum: 1d9c2a3e36dfb09832f38e2e699c367ef190f96b82c71f809bc0822c306f5379df87bab47bed27ea99106d86447e50eb972d3c516c2f95782807a9d082fbea95
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"wildcard@npm:^2.0.0":
|
"wildcard@npm:^2.0.0":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "wildcard@npm:2.0.1"
|
resolution: "wildcard@npm:2.0.1"
|
||||||
|
|
Loading…
Reference in a new issue